picker2.rs

  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}