picker.rs

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