1use editor::Editor;
2use gpui::{
3 div, prelude::*, rems, uniform_list, AnyElement, AppContext, DismissEvent, Div, EventEmitter,
4 FocusHandle, FocusableView, MouseButton, MouseDownEvent, Render, Task, UniformListScrollHandle,
5 View, ViewContext, WindowContext,
6};
7use std::{cmp, sync::Arc};
8use ui::{prelude::*, v_stack, Color, Divider, Label};
9
10pub struct Picker<D: PickerDelegate> {
11 pub delegate: D,
12 scroll_handle: UniformListScrollHandle,
13 editor: View<Editor>,
14 pending_update_matches: Option<Task<()>>,
15 confirm_on_update: Option<bool>,
16}
17
18pub trait PickerDelegate: Sized + 'static {
19 type ListItem: IntoElement;
20 fn match_count(&self) -> usize;
21 fn selected_index(&self) -> usize;
22 fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>);
23
24 fn placeholder_text(&self) -> Arc<str>;
25 fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()>;
26
27 fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>);
28 fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>);
29
30 fn render_match(
31 &self,
32 ix: usize,
33 selected: bool,
34 cx: &mut ViewContext<Picker<Self>>,
35 ) -> Option<Self::ListItem>;
36}
37
38impl<D: PickerDelegate> FocusableView for Picker<D> {
39 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
40 self.editor.focus_handle(cx)
41 }
42}
43
44impl<D: PickerDelegate> Picker<D> {
45 pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
46 let editor = cx.build_view(|cx| {
47 let mut editor = Editor::single_line(cx);
48 editor.set_placeholder_text(delegate.placeholder_text(), cx);
49 editor
50 });
51 cx.subscribe(&editor, Self::on_input_editor_event).detach();
52 let mut this = Self {
53 delegate,
54 editor,
55 scroll_handle: UniformListScrollHandle::new(),
56 pending_update_matches: None,
57 confirm_on_update: None,
58 };
59 this.update_matches("".to_string(), cx);
60 this
61 }
62
63 pub fn focus(&self, cx: &mut WindowContext) {
64 self.editor.update(cx, |editor, cx| editor.focus(cx));
65 }
66
67 pub fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
68 let count = self.delegate.match_count();
69 if count > 0 {
70 let index = self.delegate.selected_index();
71 let ix = cmp::min(index + 1, count - 1);
72 self.delegate.set_selected_index(ix, cx);
73 self.scroll_handle.scroll_to_item(ix);
74 cx.notify();
75 }
76 }
77
78 fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
79 let count = self.delegate.match_count();
80 if count > 0 {
81 let index = self.delegate.selected_index();
82 let ix = index.saturating_sub(1);
83 self.delegate.set_selected_index(ix, cx);
84 self.scroll_handle.scroll_to_item(ix);
85 cx.notify();
86 }
87 }
88
89 fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext<Self>) {
90 let count = self.delegate.match_count();
91 if count > 0 {
92 self.delegate.set_selected_index(0, cx);
93 self.scroll_handle.scroll_to_item(0);
94 cx.notify();
95 }
96 }
97
98 fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
99 let count = self.delegate.match_count();
100 if count > 0 {
101 self.delegate.set_selected_index(count - 1, cx);
102 self.scroll_handle.scroll_to_item(count - 1);
103 cx.notify();
104 }
105 }
106
107 pub fn cycle_selection(&mut self, cx: &mut ViewContext<Self>) {
108 let count = self.delegate.match_count();
109 let index = self.delegate.selected_index();
110 let new_index = if index + 1 == count { 0 } else { index + 1 };
111 self.delegate.set_selected_index(new_index, cx);
112 self.scroll_handle.scroll_to_item(new_index);
113 cx.notify();
114 }
115
116 pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
117 self.delegate.dismissed(cx);
118 cx.emit(DismissEvent);
119 }
120
121 fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
122 if self.pending_update_matches.is_some() {
123 self.confirm_on_update = Some(false)
124 } else {
125 self.delegate.confirm(false, cx);
126 }
127 }
128
129 fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
130 if self.pending_update_matches.is_some() {
131 self.confirm_on_update = Some(true)
132 } else {
133 self.delegate.confirm(true, cx);
134 }
135 }
136
137 fn handle_click(&mut self, ix: usize, secondary: bool, cx: &mut ViewContext<Self>) {
138 cx.stop_propagation();
139 cx.prevent_default();
140 self.delegate.set_selected_index(ix, cx);
141 self.delegate.confirm(secondary, cx);
142 }
143
144 fn on_input_editor_event(
145 &mut self,
146 _: View<Editor>,
147 event: &editor::EditorEvent,
148 cx: &mut ViewContext<Self>,
149 ) {
150 match event {
151 editor::EditorEvent::BufferEdited => {
152 let query = self.editor.read(cx).text(cx);
153 self.update_matches(query, cx);
154 }
155 editor::EditorEvent::Blurred => {
156 self.cancel(&menu::Cancel, cx);
157 }
158 _ => {}
159 }
160 }
161
162 pub fn refresh(&mut self, cx: &mut ViewContext<Self>) {
163 let query = self.editor.read(cx).text(cx);
164 self.update_matches(query, cx);
165 }
166
167 pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
168 let update = self.delegate.update_matches(query, cx);
169 self.matches_updated(cx);
170 self.pending_update_matches = Some(cx.spawn(|this, mut cx| async move {
171 update.await;
172 this.update(&mut cx, |this, cx| {
173 this.matches_updated(cx);
174 })
175 .ok();
176 }));
177 }
178
179 fn matches_updated(&mut self, cx: &mut ViewContext<Self>) {
180 let index = self.delegate.selected_index();
181 self.scroll_handle.scroll_to_item(index);
182 self.pending_update_matches = None;
183 if let Some(secondary) = self.confirm_on_update.take() {
184 self.delegate.confirm(secondary, cx);
185 }
186 cx.notify();
187 }
188
189 pub fn query(&self, cx: &AppContext) -> String {
190 self.editor.read(cx).text(cx)
191 }
192
193 pub fn set_query(&self, query: impl Into<Arc<str>>, cx: &mut ViewContext<Self>) {
194 self.editor
195 .update(cx, |editor, cx| editor.set_text(query, cx));
196 }
197}
198
199impl<D: PickerDelegate> EventEmitter<DismissEvent> for Picker<D> {}
200
201impl<D: PickerDelegate> Render for Picker<D> {
202 type Element = Div;
203
204 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
205 let picker_editor = h_stack()
206 .overflow_hidden()
207 .flex_none()
208 .h_9()
209 .px_3()
210 .child(self.editor.clone());
211
212 let empty_state = div().p_1().child(
213 h_stack()
214 // TODO: This number matches the height of the uniform list items.
215 // Align these two with a less magic number.
216 .h(rems(1.4375))
217 .px_2()
218 .child(Label::new("No matches").color(Color::Muted)),
219 );
220
221 div()
222 .key_context("picker")
223 .size_full()
224 .overflow_hidden()
225 .elevation_3(cx)
226 .on_action(cx.listener(Self::select_next))
227 .on_action(cx.listener(Self::select_prev))
228 .on_action(cx.listener(Self::select_first))
229 .on_action(cx.listener(Self::select_last))
230 .on_action(cx.listener(Self::cancel))
231 .on_action(cx.listener(Self::confirm))
232 .on_action(cx.listener(Self::secondary_confirm))
233 .child(
234 picker_editor
235 )
236 .child(Divider::horizontal())
237 .when(self.delegate.match_count() > 0, |el| {
238 el.child(
239 v_stack()
240 .flex_grow()
241 .child(
242 uniform_list(
243 cx.view().clone(),
244 "candidates",
245 self.delegate.match_count(),
246 {
247 let selected_index = self.delegate.selected_index();
248
249 move |picker, visible_range, cx| {
250 visible_range
251 .map(|ix| {
252 div()
253 .on_mouse_down(
254 MouseButton::Left,
255 cx.listener(move |this, event: &MouseDownEvent, cx| {
256 this.handle_click(
257 ix,
258 event.modifiers.command,
259 cx,
260 )
261 }),
262 )
263 .children(picker.delegate.render_match(
264 ix,
265 ix == selected_index,
266 cx,
267 ))
268 })
269 .collect()
270 }
271 },
272 )
273 .track_scroll(self.scroll_handle.clone())
274 .p_1()
275 )
276 .max_h_72()
277 .overflow_hidden(),
278 )
279 })
280 .when(self.delegate.match_count() == 0, |el| {
281 el.child(
282 empty_state
283 )
284 })
285 }
286}
287
288pub fn simple_picker_match(
289 selected: bool,
290 cx: &mut WindowContext,
291 children: impl FnOnce(&mut WindowContext) -> AnyElement,
292) -> AnyElement {
293 let colors = cx.theme().colors();
294
295 div()
296 .px_1()
297 .text_color(colors.text)
298 .text_ui()
299 .bg(colors.ghost_element_background)
300 .rounded_md()
301 .when(selected, |this| this.bg(colors.ghost_element_selected))
302 .hover(|this| this.bg(colors.ghost_element_hover))
303 .child((children)(cx))
304 .into_any()
305}