1use crate::components::ViewContext;
2use crate::components::component::PickerMessage;
3use crate::fuzzy_matcher::{FuzzyMatcher, Searchable};
4use crate::line::Line;
5use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
6use std::cmp::Ordering;
7
8const DEFAULT_MAX_VISIBLE: usize = 10;
9
10pub enum PickerKey {
11 Escape,
12 MoveUp,
13 MoveDown,
14 MoveLeft,
15 MoveRight,
16 Tab,
17 BackTab,
18 Confirm,
19 Char(char),
20 Backspace,
21 BackspaceOnEmpty,
22 ControlChar,
23 Other,
24}
25
26pub struct Combobox<T: Searchable + Send + Sync + 'static> {
27 fuzzy: FuzzyMatcher<T>,
28 selected_index: usize,
29 scroll_offset: usize,
30 max_visible: usize,
31}
32
33impl<T: Searchable + Send + Sync + 'static> Combobox<T> {
34 pub fn new(items: Vec<T>) -> Self {
35 Self {
36 fuzzy: FuzzyMatcher::new(items),
37 selected_index: 0,
38 scroll_offset: 0,
39 max_visible: DEFAULT_MAX_VISIBLE,
40 }
41 }
42
43 pub fn from_matches(matches: Vec<T>) -> Self {
44 Self {
45 fuzzy: FuzzyMatcher::from_matches(matches),
46 selected_index: 0,
47 scroll_offset: 0,
48 max_visible: DEFAULT_MAX_VISIBLE,
49 }
50 }
51
52 pub fn query(&self) -> &str {
53 self.fuzzy.query()
54 }
55
56 pub fn matches(&self) -> &[T] {
57 self.fuzzy.matches()
58 }
59
60 pub fn selected_index(&self) -> usize {
61 self.selected_index
62 }
63
64 pub fn set_max_visible(&mut self, max: usize) {
65 self.max_visible = max;
66 self.ensure_visible();
67 }
68
69 pub fn set_match_sort(&mut self, sort: fn(&T, &T) -> Ordering) {
70 self.fuzzy.set_match_sort(sort);
71 self.scroll_offset = 0;
72 if self.selected_index >= self.fuzzy.matches().len() {
73 self.selected_index = 0;
74 }
75 self.ensure_visible();
76 }
77
78 pub fn is_empty(&self) -> bool {
79 self.fuzzy.is_empty()
80 }
81
82 pub fn selected(&self) -> Option<&T> {
83 self.fuzzy.matches().get(self.selected_index)
84 }
85
86 pub fn push_query_char(&mut self, c: char) {
87 self.fuzzy.push_query_char(c);
88 self.reset_viewport();
89 }
90
91 pub fn pop_query_char(&mut self) {
92 if self.fuzzy.pop_query_char() {
93 self.reset_viewport();
94 }
95 }
96
97 pub fn set_selected_index(&mut self, index: usize) {
98 let len = self.fuzzy.matches().len();
99 if len == 0 {
100 return;
101 }
102 self.selected_index = index.min(len - 1);
103 self.ensure_visible();
104 }
105
106 pub fn move_up(&mut self) {
107 self.move_up_where(|_| true);
108 }
109
110 pub fn move_down(&mut self) {
111 self.move_down_where(|_| true);
112 }
113
114 pub fn move_up_where(&mut self, predicate: impl Fn(&T) -> bool) {
115 let len = self.fuzzy.matches().len();
116 if len == 0 {
117 return;
118 }
119 let matches = self.fuzzy.matches();
120 let mut idx = self.selected_index;
121 for _ in 0..len {
122 idx = if idx == 0 { len - 1 } else { idx - 1 };
123 if predicate(&matches[idx]) {
124 self.selected_index = idx;
125 self.ensure_visible();
126 return;
127 }
128 }
129 }
130
131 pub fn move_down_where(&mut self, predicate: impl Fn(&T) -> bool) {
132 let len = self.fuzzy.matches().len();
133 if len == 0 {
134 return;
135 }
136 let matches = self.fuzzy.matches();
137 let mut idx = self.selected_index;
138 for _ in 0..len {
139 idx = (idx + 1) % len;
140 if predicate(&matches[idx]) {
141 self.selected_index = idx;
142 self.ensure_visible();
143 return;
144 }
145 }
146 }
147
148 pub fn select_first_where(&mut self, predicate: impl Fn(&T) -> bool) {
149 if let Some(idx) = self.fuzzy.matches().iter().position(&predicate) {
150 self.selected_index = idx;
151 self.ensure_visible();
152 }
153 }
154
155 pub fn render_items(
156 &self,
157 context: &ViewContext,
158 render_item: impl Fn(&T, bool, &ViewContext) -> Line,
159 ) -> Vec<Line> {
160 let inner = context.with_size((context.size.width.saturating_sub(2), context.size.height));
161 self.visible_matches_with_selection()
162 .into_iter()
163 .map(|(item, is_selected)| render_item(item, is_selected, &inner).prepend(" "))
164 .collect()
165 }
166
167 pub fn visible_matches_with_selection(&self) -> Vec<(&T, bool)> {
168 let visible_selected_index = self.visible_selected_index();
169 self.visible_matches()
170 .iter()
171 .enumerate()
172 .map(|(i, item)| (item, Some(i) == visible_selected_index))
173 .collect()
174 }
175
176 pub fn handle_picker_event(
181 &mut self,
182 event: &crate::components::Event,
183 ) -> Option<Vec<PickerMessage<T>>> {
184 let crate::components::Event::Key(key_event) = event else {
185 return None;
186 };
187 match classify_key(*key_event, self.fuzzy.query().is_empty()) {
188 PickerKey::Escape => Some(vec![PickerMessage::Close]),
189 PickerKey::BackspaceOnEmpty => Some(vec![PickerMessage::CloseAndPopChar]),
190 PickerKey::MoveUp => {
191 self.move_up();
192 Some(vec![])
193 }
194 PickerKey::MoveDown => {
195 self.move_down();
196 Some(vec![])
197 }
198 PickerKey::Confirm => {
199 if let Some(item) = self.selected().cloned() {
200 Some(vec![PickerMessage::Confirm(item)])
201 } else {
202 Some(vec![PickerMessage::Close])
203 }
204 }
205 PickerKey::Char(c) => {
206 if c.is_whitespace() {
207 return Some(vec![PickerMessage::CloseWithChar(c)]);
208 }
209 self.push_query_char(c);
210 Some(vec![PickerMessage::CharTyped(c)])
211 }
212 PickerKey::Backspace => {
213 self.pop_query_char();
214 Some(vec![PickerMessage::PopChar])
215 }
216 PickerKey::MoveLeft
217 | PickerKey::MoveRight
218 | PickerKey::Tab
219 | PickerKey::BackTab
220 | PickerKey::ControlChar
221 | PickerKey::Other => Some(vec![]),
222 }
223 }
224
225 fn visible_matches(&self) -> &[T] {
226 let matches = self.fuzzy.matches();
227 let end = (self.scroll_offset + self.max_visible).min(matches.len());
228 &matches[self.scroll_offset..end]
229 }
230
231 fn visible_selected_index(&self) -> Option<usize> {
232 self.selected_index.checked_sub(self.scroll_offset)
233 }
234
235 fn ensure_visible(&mut self) {
236 if self.fuzzy.matches().is_empty() {
237 self.scroll_offset = 0;
238 return;
239 }
240 if self.selected_index < self.scroll_offset {
241 self.scroll_offset = self.selected_index;
242 } else if self.selected_index >= self.scroll_offset + self.max_visible {
243 self.scroll_offset = self.selected_index + 1 - self.max_visible;
244 }
245 }
246
247 fn reset_viewport(&mut self) {
248 self.scroll_offset = 0;
249 if self.selected_index >= self.fuzzy.matches().len() {
250 self.selected_index = 0;
251 }
252 }
253}
254
255pub fn classify_key(key: KeyEvent, query_is_empty: bool) -> PickerKey {
256 match key.code {
257 KeyCode::Esc => PickerKey::Escape,
258 KeyCode::Up => PickerKey::MoveUp,
259 KeyCode::Down => PickerKey::MoveDown,
260 KeyCode::Left => PickerKey::MoveLeft,
261 KeyCode::Right => PickerKey::MoveRight,
262 KeyCode::Tab => PickerKey::Tab,
263 KeyCode::BackTab => PickerKey::BackTab,
264 KeyCode::Char('p') if key.modifiers.contains(KeyModifiers::CONTROL) => PickerKey::MoveUp,
265 KeyCode::Char('n') if key.modifiers.contains(KeyModifiers::CONTROL) => PickerKey::MoveDown,
266 KeyCode::Enter => PickerKey::Confirm,
267 KeyCode::Char(c) if c.is_control() => PickerKey::ControlChar,
268 KeyCode::Char(c) => PickerKey::Char(c),
269 KeyCode::Backspace if query_is_empty => PickerKey::BackspaceOnEmpty,
270 KeyCode::Backspace => PickerKey::Backspace,
271 _ => PickerKey::Other,
272 }
273}