picker2.rs

  1use editor::Editor;
  2use gpui::{
  3    div, prelude::*, rems, uniform_list, AnyElement, AppContext, Div, FocusHandle, FocusableView,
  4    MouseButton, MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext,
  5    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    fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
117        self.delegate.dismissed(cx);
118    }
119
120    fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
121        if self.pending_update_matches.is_some() {
122            self.confirm_on_update = Some(false)
123        } else {
124            self.delegate.confirm(false, cx);
125        }
126    }
127
128    fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
129        if self.pending_update_matches.is_some() {
130            self.confirm_on_update = Some(true)
131        } else {
132            self.delegate.confirm(true, cx);
133        }
134    }
135
136    fn handle_click(&mut self, ix: usize, secondary: bool, cx: &mut ViewContext<Self>) {
137        cx.stop_propagation();
138        cx.prevent_default();
139        self.delegate.set_selected_index(ix, cx);
140        self.delegate.confirm(secondary, cx);
141    }
142
143    fn on_input_editor_event(
144        &mut self,
145        _: View<Editor>,
146        event: &editor::EditorEvent,
147        cx: &mut ViewContext<Self>,
148    ) {
149        if let editor::EditorEvent::BufferEdited = event {
150            let query = self.editor.read(cx).text(cx);
151            self.update_matches(query, cx);
152        }
153    }
154
155    pub fn refresh(&mut self, cx: &mut ViewContext<Self>) {
156        let query = self.editor.read(cx).text(cx);
157        self.update_matches(query, cx);
158    }
159
160    pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
161        let update = self.delegate.update_matches(query, cx);
162        self.matches_updated(cx);
163        self.pending_update_matches = Some(cx.spawn(|this, mut cx| async move {
164            update.await;
165            this.update(&mut cx, |this, cx| {
166                this.matches_updated(cx);
167            })
168            .ok();
169        }));
170    }
171
172    fn matches_updated(&mut self, cx: &mut ViewContext<Self>) {
173        let index = self.delegate.selected_index();
174        self.scroll_handle.scroll_to_item(index);
175        self.pending_update_matches = None;
176        if let Some(secondary) = self.confirm_on_update.take() {
177            self.delegate.confirm(secondary, cx);
178        }
179        cx.notify();
180    }
181
182    pub fn query(&self, cx: &AppContext) -> String {
183        self.editor.read(cx).text(cx)
184    }
185
186    pub fn set_query(&self, query: impl Into<Arc<str>>, cx: &mut ViewContext<Self>) {
187        self.editor
188            .update(cx, |editor, cx| editor.set_text(query, cx));
189    }
190}
191
192impl<D: PickerDelegate> Render for Picker<D> {
193    type Element = Div;
194
195    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
196        let picker_editor = h_stack()
197            .overflow_hidden()
198            .flex_none()
199            .h_9()
200            .px_3()
201            .child(self.editor.clone());
202
203        let empty_state = div().p_1().child(
204            h_stack()
205                // TODO: This number matches the height of the uniform list items.
206                // Align these two with a less magic number.
207                .h(rems(1.4375))
208                .px_2()
209                .child(Label::new("No matches").color(Color::Muted)),
210        );
211
212        div()
213            .key_context("picker")
214            .size_full()
215            .overflow_hidden()
216            .elevation_3(cx)
217            .on_action(cx.listener(Self::select_next))
218            .on_action(cx.listener(Self::select_prev))
219            .on_action(cx.listener(Self::select_first))
220            .on_action(cx.listener(Self::select_last))
221            .on_action(cx.listener(Self::cancel))
222            .on_action(cx.listener(Self::confirm))
223            .on_action(cx.listener(Self::secondary_confirm))
224            .child(
225                picker_editor
226            )
227            .child(Divider::horizontal())
228            .when(self.delegate.match_count() > 0, |el| {
229                el.child(
230                    v_stack()
231                        .flex_grow()
232                        .child(
233                            uniform_list(
234                                cx.view().clone(),
235                                "candidates",
236                                self.delegate.match_count(),
237                                {
238                                    let selected_index = self.delegate.selected_index();
239
240                                    move |picker, visible_range, cx| {
241                                        visible_range
242                                            .map(|ix| {
243                                                div()
244                                                    .on_mouse_down(
245                                                        MouseButton::Left,
246                                                        cx.listener(move |this, event: &MouseDownEvent, cx| {
247                                                            this.handle_click(
248                                                                ix,
249                                                                event.modifiers.command,
250                                                                cx,
251                                                            )
252                                                        }),
253                                                    )
254                                                    .children(picker.delegate.render_match(
255                                                        ix,
256                                                        ix == selected_index,
257                                                        cx,
258                                                    ))
259                                            })
260                                            .collect()
261                                    }
262                                },
263                            )
264                            .track_scroll(self.scroll_handle.clone())
265                            .p_1()
266                        )
267                        .max_h_72()
268                        .overflow_hidden(),
269                )
270            })
271            .when(self.delegate.match_count() == 0, |el| {
272                el.child(
273                    empty_state
274                )
275            })
276    }
277}
278
279pub fn simple_picker_match(
280    selected: bool,
281    cx: &mut WindowContext,
282    children: impl FnOnce(&mut WindowContext) -> AnyElement,
283) -> AnyElement {
284    let colors = cx.theme().colors();
285
286    div()
287        .px_1()
288        .text_color(colors.text)
289        .text_ui()
290        .bg(colors.ghost_element_background)
291        .rounded_md()
292        .when(selected, |this| this.bg(colors.ghost_element_selected))
293        .hover(|this| this.bg(colors.ghost_element_hover))
294        .child((children)(cx))
295        .into_any()
296}