picker.rs

  1use editor::Editor;
  2use gpui::{
  3    div, list, prelude::*, uniform_list, AnyElement, AppContext, DismissEvent, EventEmitter,
  4    FocusHandle, FocusableView, Length, ListState, MouseButton, MouseDownEvent, Render, Task,
  5    UniformListScrollHandle, View, ViewContext, WindowContext,
  6};
  7use std::sync::Arc;
  8use ui::{prelude::*, v_flex, Color, Divider, Label, ListItem, ListItemSpacing};
  9use workspace::ModalView;
 10
 11enum ElementContainer {
 12    List(ListState),
 13    UniformList(UniformListScrollHandle),
 14}
 15
 16pub struct Picker<D: PickerDelegate> {
 17    pub delegate: D,
 18    element_container: ElementContainer,
 19    editor: View<Editor>,
 20    pending_update_matches: Option<Task<()>>,
 21    confirm_on_update: Option<bool>,
 22    width: Option<Length>,
 23    max_height: Option<Length>,
 24
 25    /// Whether the `Picker` is rendered as a self-contained modal.
 26    ///
 27    /// Set this to `false` when rendering the `Picker` as part of a larger modal.
 28    is_modal: bool,
 29}
 30
 31pub trait PickerDelegate: Sized + 'static {
 32    type ListItem: IntoElement;
 33    fn match_count(&self) -> usize;
 34    fn selected_index(&self) -> usize;
 35    fn separators_after_indices(&self) -> Vec<usize> {
 36        Vec::new()
 37    }
 38    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>);
 39
 40    fn placeholder_text(&self) -> Arc<str>;
 41    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()>;
 42
 43    fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>);
 44    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>);
 45
 46    fn render_match(
 47        &self,
 48        ix: usize,
 49        selected: bool,
 50        cx: &mut ViewContext<Picker<Self>>,
 51    ) -> Option<Self::ListItem>;
 52    fn render_header(&self, _: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
 53        None
 54    }
 55    fn render_footer(&self, _: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
 56        None
 57    }
 58}
 59
 60impl<D: PickerDelegate> FocusableView for Picker<D> {
 61    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
 62        self.editor.focus_handle(cx)
 63    }
 64}
 65
 66fn create_editor(placeholder: Arc<str>, cx: &mut WindowContext<'_>) -> View<Editor> {
 67    cx.new_view(|cx| {
 68        let mut editor = Editor::single_line(cx);
 69        editor.set_placeholder_text(placeholder, cx);
 70        editor
 71    })
 72}
 73
 74impl<D: PickerDelegate> Picker<D> {
 75    /// A picker, which displays its matches using `gpui::uniform_list`, all matches should have the same height.
 76    /// If `PickerDelegate::render_match` can return items with different heights, use `Picker::list`.
 77    pub fn uniform_list(delegate: D, cx: &mut ViewContext<Self>) -> Self {
 78        Self::new(delegate, cx, true)
 79    }
 80
 81    /// A picker, which displays its matches using `gpui::list`, matches can have different heights.
 82    /// If `PickerDelegate::render_match` only returns items with the same height, use `Picker::uniform_list` as its implementation is optimized for that.
 83    pub fn list(delegate: D, cx: &mut ViewContext<Self>) -> Self {
 84        Self::new(delegate, cx, false)
 85    }
 86
 87    fn new(delegate: D, cx: &mut ViewContext<Self>, is_uniform: bool) -> Self {
 88        let editor = create_editor(delegate.placeholder_text(), cx);
 89        cx.subscribe(&editor, Self::on_input_editor_event).detach();
 90        let mut this = Self {
 91            delegate,
 92            editor,
 93            element_container: Self::crate_element_container(is_uniform, cx),
 94            pending_update_matches: None,
 95            confirm_on_update: None,
 96            width: None,
 97            max_height: None,
 98            is_modal: true,
 99        };
100        this.update_matches("".to_string(), cx);
101        this
102    }
103
104    fn crate_element_container(is_uniform: bool, cx: &mut ViewContext<Self>) -> ElementContainer {
105        if is_uniform {
106            ElementContainer::UniformList(UniformListScrollHandle::new())
107        } else {
108            let view = cx.view().downgrade();
109            ElementContainer::List(ListState::new(
110                0,
111                gpui::ListAlignment::Top,
112                px(1000.),
113                move |ix, cx| {
114                    view.upgrade()
115                        .map(|view| {
116                            view.update(cx, |this, cx| {
117                                this.render_element(cx, ix).into_any_element()
118                            })
119                        })
120                        .unwrap_or_else(|| div().into_any_element())
121                },
122            ))
123        }
124    }
125
126    pub fn width(mut self, width: impl Into<gpui::Length>) -> Self {
127        self.width = Some(width.into());
128        self
129    }
130
131    pub fn max_height(mut self, max_height: impl Into<gpui::Length>) -> Self {
132        self.max_height = Some(max_height.into());
133        self
134    }
135
136    pub fn modal(mut self, modal: bool) -> Self {
137        self.is_modal = modal;
138        self
139    }
140
141    pub fn focus(&self, cx: &mut WindowContext) {
142        self.editor.update(cx, |editor, cx| editor.focus(cx));
143    }
144
145    pub fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
146        let count = self.delegate.match_count();
147        if count > 0 {
148            let index = self.delegate.selected_index();
149            let ix = if index == count - 1 { 0 } else { index + 1 };
150            self.delegate.set_selected_index(ix, cx);
151            self.scroll_to_item_index(ix);
152            cx.notify();
153        }
154    }
155
156    fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
157        let count = self.delegate.match_count();
158        if count > 0 {
159            let index = self.delegate.selected_index();
160            let ix = if index == 0 { count - 1 } else { index - 1 };
161            self.delegate.set_selected_index(ix, cx);
162            self.scroll_to_item_index(ix);
163            cx.notify();
164        }
165    }
166
167    fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext<Self>) {
168        let count = self.delegate.match_count();
169        if count > 0 {
170            self.delegate.set_selected_index(0, cx);
171            self.scroll_to_item_index(0);
172            cx.notify();
173        }
174    }
175
176    fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
177        let count = self.delegate.match_count();
178        if count > 0 {
179            self.delegate.set_selected_index(count - 1, cx);
180            self.scroll_to_item_index(count - 1);
181            cx.notify();
182        }
183    }
184
185    pub fn cycle_selection(&mut self, cx: &mut ViewContext<Self>) {
186        let count = self.delegate.match_count();
187        let index = self.delegate.selected_index();
188        let new_index = if index + 1 == count { 0 } else { index + 1 };
189        self.delegate.set_selected_index(new_index, cx);
190        self.scroll_to_item_index(new_index);
191        cx.notify();
192    }
193
194    pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
195        self.delegate.dismissed(cx);
196        cx.emit(DismissEvent);
197    }
198
199    fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
200        if self.pending_update_matches.is_some() {
201            self.confirm_on_update = Some(false)
202        } else {
203            self.delegate.confirm(false, cx);
204        }
205    }
206
207    fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
208        if self.pending_update_matches.is_some() {
209            self.confirm_on_update = Some(true)
210        } else {
211            self.delegate.confirm(true, cx);
212        }
213    }
214
215    fn handle_click(&mut self, ix: usize, secondary: bool, cx: &mut ViewContext<Self>) {
216        cx.stop_propagation();
217        cx.prevent_default();
218        self.delegate.set_selected_index(ix, cx);
219        self.delegate.confirm(secondary, cx);
220    }
221
222    fn on_input_editor_event(
223        &mut self,
224        _: View<Editor>,
225        event: &editor::EditorEvent,
226        cx: &mut ViewContext<Self>,
227    ) {
228        match event {
229            editor::EditorEvent::BufferEdited => {
230                let query = self.editor.read(cx).text(cx);
231                self.update_matches(query, cx);
232            }
233            editor::EditorEvent::Blurred => {
234                self.cancel(&menu::Cancel, cx);
235            }
236            _ => {}
237        }
238    }
239
240    pub fn refresh(&mut self, cx: &mut ViewContext<Self>) {
241        let query = self.editor.read(cx).text(cx);
242        self.update_matches(query, cx);
243    }
244
245    pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
246        let update = self.delegate.update_matches(query, cx);
247        self.matches_updated(cx);
248        self.pending_update_matches = Some(cx.spawn(|this, mut cx| async move {
249            update.await;
250            this.update(&mut cx, |this, cx| {
251                this.matches_updated(cx);
252            })
253            .ok();
254        }));
255    }
256
257    fn matches_updated(&mut self, cx: &mut ViewContext<Self>) {
258        if let ElementContainer::List(state) = &mut self.element_container {
259            state.reset(self.delegate.match_count());
260        }
261
262        let index = self.delegate.selected_index();
263        self.scroll_to_item_index(index);
264        self.pending_update_matches = None;
265        if let Some(secondary) = self.confirm_on_update.take() {
266            self.delegate.confirm(secondary, cx);
267        }
268        cx.notify();
269    }
270
271    pub fn query(&self, cx: &AppContext) -> String {
272        self.editor.read(cx).text(cx)
273    }
274
275    pub fn set_query(&self, query: impl Into<Arc<str>>, cx: &mut ViewContext<Self>) {
276        self.editor
277            .update(cx, |editor, cx| editor.set_text(query, cx));
278    }
279
280    fn scroll_to_item_index(&mut self, ix: usize) {
281        match &mut self.element_container {
282            ElementContainer::List(state) => state.scroll_to_reveal_item(ix),
283            ElementContainer::UniformList(scroll_handle) => scroll_handle.scroll_to_item(ix),
284        }
285    }
286
287    fn render_element(&self, cx: &mut ViewContext<Self>, ix: usize) -> impl IntoElement {
288        div()
289            .on_mouse_down(
290                MouseButton::Left,
291                cx.listener(move |this, event: &MouseDownEvent, cx| {
292                    this.handle_click(ix, event.modifiers.command, cx)
293                }),
294            )
295            .children(
296                self.delegate
297                    .render_match(ix, ix == self.delegate.selected_index(), cx),
298            )
299            .when(
300                self.delegate.separators_after_indices().contains(&ix),
301                |picker| {
302                    picker
303                        .border_color(cx.theme().colors().border_variant)
304                        .border_b_1()
305                        .pb(px(-1.0))
306                },
307            )
308    }
309
310    fn render_element_container(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
311        match &self.element_container {
312            ElementContainer::UniformList(scroll_handle) => uniform_list(
313                cx.view().clone(),
314                "candidates",
315                self.delegate.match_count(),
316                move |picker, visible_range, cx| {
317                    visible_range
318                        .map(|ix| picker.render_element(cx, ix))
319                        .collect()
320                },
321            )
322            .py_2()
323            .track_scroll(scroll_handle.clone())
324            .into_any_element(),
325            ElementContainer::List(state) => list(state.clone())
326                .with_sizing_behavior(gpui::ListSizingBehavior::Infer)
327                .py_2()
328                .into_any_element(),
329        }
330    }
331}
332
333impl<D: PickerDelegate> EventEmitter<DismissEvent> for Picker<D> {}
334impl<D: PickerDelegate> ModalView for Picker<D> {}
335
336impl<D: PickerDelegate> Render for Picker<D> {
337    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
338        let picker_editor = h_flex()
339            .overflow_hidden()
340            .flex_none()
341            .h_9()
342            .px_4()
343            .child(self.editor.clone());
344
345        div()
346            .key_context("Picker")
347            .size_full()
348            .when_some(self.width, |el, width| el.w(width))
349            .overflow_hidden()
350            // This is a bit of a hack to remove the modal styling when we're rendering the `Picker`
351            // as a part of a modal rather than the entire modal.
352            //
353            // We should revisit how the `Picker` is styled to make it more composable.
354            .when(self.is_modal, |this| this.elevation_3(cx))
355            .on_action(cx.listener(Self::select_next))
356            .on_action(cx.listener(Self::select_prev))
357            .on_action(cx.listener(Self::select_first))
358            .on_action(cx.listener(Self::select_last))
359            .on_action(cx.listener(Self::cancel))
360            .on_action(cx.listener(Self::confirm))
361            .on_action(cx.listener(Self::secondary_confirm))
362            .child(picker_editor)
363            .child(Divider::horizontal())
364            .when(self.delegate.match_count() > 0, |el| {
365                el.child(
366                    v_flex()
367                        .flex_grow()
368                        .max_h(self.max_height.unwrap_or(rems(18.).into()))
369                        .overflow_hidden()
370                        .children(self.delegate.render_header(cx))
371                        .child(self.render_element_container(cx)),
372                )
373            })
374            .when(self.delegate.match_count() == 0, |el| {
375                el.child(
376                    v_flex().flex_grow().py_2().child(
377                        ListItem::new("empty_state")
378                            .inset(true)
379                            .spacing(ListItemSpacing::Sparse)
380                            .disabled(true)
381                            .child(Label::new("No matches").color(Color::Muted)),
382                    ),
383                )
384            })
385            .children(self.delegate.render_footer(cx))
386    }
387}