picker2.rs

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