picker2.rs

  1use editor::Editor;
  2use gpui::{
  3    div, prelude::*, 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        div()
197            .key_context("picker")
198            .size_full()
199            .elevation_2(cx)
200            .on_action(cx.listener(Self::select_next))
201            .on_action(cx.listener(Self::select_prev))
202            .on_action(cx.listener(Self::select_first))
203            .on_action(cx.listener(Self::select_last))
204            .on_action(cx.listener(Self::cancel))
205            .on_action(cx.listener(Self::confirm))
206            .on_action(cx.listener(Self::secondary_confirm))
207            .child(
208                v_stack()
209                    .py_0p5()
210                    .px_1()
211                    .child(div().px_1().py_0p5().child(self.editor.clone())),
212            )
213            .child(Divider::horizontal())
214            .when(self.delegate.match_count() > 0, |el| {
215                el.child(
216                    v_stack()
217                        .grow()
218                        .child(
219                            uniform_list(
220                                cx.view().clone(),
221                                "candidates",
222                                self.delegate.match_count(),
223                                {
224                                    let selected_index = self.delegate.selected_index();
225
226                                    move |picker, visible_range, cx| {
227                                        visible_range
228                                            .map(|ix| {
229                                                div()
230                                                    .on_mouse_down(
231                                                        MouseButton::Left,
232                                                        cx.listener(move |this, event: &MouseDownEvent, cx| {
233                                                            this.handle_click(
234                                                                ix,
235                                                                event.modifiers.command,
236                                                                cx,
237                                                            )
238                                                        }),
239                                                    )
240                                                    .children(picker.delegate.render_match(
241                                                        ix,
242                                                        ix == selected_index,
243                                                        cx,
244                                                    ))
245                                            })
246                                            .collect()
247                                    }
248                                },
249                            )
250                            .track_scroll(self.scroll_handle.clone())
251                            .p_1()
252                        )
253                        .max_h_72()
254                        .overflow_hidden(),
255                )
256            })
257            .when(self.delegate.match_count() == 0, |el| {
258                el.child(
259                    v_stack().p_1().grow().child(
260                        div()
261                            .px_1()
262                            .child(Label::new("No matches").color(Color::Muted)),
263                    ),
264                )
265            })
266    }
267}
268
269pub fn simple_picker_match(
270    selected: bool,
271    cx: &mut WindowContext,
272    children: impl FnOnce(&mut WindowContext) -> AnyElement,
273) -> AnyElement {
274    let colors = cx.theme().colors();
275
276    div()
277        .px_1()
278        .text_color(colors.text)
279        .text_ui()
280        .bg(colors.ghost_element_background)
281        .rounded_md()
282        .when(selected, |this| this.bg(colors.ghost_element_selected))
283        .hover(|this| this.bg(colors.ghost_element_hover))
284        .child((children)(cx))
285        .into_any()
286}