picker2.rs

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