picker2.rs

  1use editor::Editor;
  2use gpui::{
  3    div, uniform_list, Component, Div, MouseButton, ParentElement, Render, StatelessInteractive,
  4    Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext,
  5};
  6use std::{cmp, sync::Arc};
  7use ui::{prelude::*, v_stack, Divider, Label, LabelColor};
  8
  9pub struct Picker<D: PickerDelegate> {
 10    pub delegate: D,
 11    scroll_handle: UniformListScrollHandle,
 12    editor: View<Editor>,
 13    pending_update_matches: Option<Task<()>>,
 14    confirm_on_update: Option<bool>,
 15}
 16
 17pub trait PickerDelegate: Sized + 'static {
 18    type ListItem: Component<Picker<Self>>;
 19
 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    ) -> Self::ListItem;
 36}
 37
 38impl<D: PickerDelegate> Picker<D> {
 39    pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
 40        let editor = cx.build_view(|cx| {
 41            let mut editor = Editor::single_line(cx);
 42            editor.set_placeholder_text(delegate.placeholder_text(), cx);
 43            editor
 44        });
 45        cx.subscribe(&editor, Self::on_input_editor_event).detach();
 46        let mut this = Self {
 47            delegate,
 48            editor,
 49            scroll_handle: UniformListScrollHandle::new(),
 50            pending_update_matches: None,
 51            confirm_on_update: None,
 52        };
 53        this.update_matches("".to_string(), cx);
 54        this
 55    }
 56
 57    pub fn focus(&self, cx: &mut WindowContext) {
 58        self.editor.update(cx, |editor, cx| editor.focus(cx));
 59    }
 60
 61    pub fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
 62        let count = self.delegate.match_count();
 63        if count > 0 {
 64            let index = self.delegate.selected_index();
 65            let ix = cmp::min(index + 1, count - 1);
 66            self.delegate.set_selected_index(ix, cx);
 67            self.scroll_handle.scroll_to_item(ix);
 68            cx.notify();
 69        }
 70    }
 71
 72    fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
 73        let count = self.delegate.match_count();
 74        if count > 0 {
 75            let index = self.delegate.selected_index();
 76            let ix = index.saturating_sub(1);
 77            self.delegate.set_selected_index(ix, cx);
 78            self.scroll_handle.scroll_to_item(ix);
 79            cx.notify();
 80        }
 81    }
 82
 83    fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext<Self>) {
 84        let count = self.delegate.match_count();
 85        if count > 0 {
 86            self.delegate.set_selected_index(0, cx);
 87            self.scroll_handle.scroll_to_item(0);
 88            cx.notify();
 89        }
 90    }
 91
 92    fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
 93        let count = self.delegate.match_count();
 94        if count > 0 {
 95            self.delegate.set_selected_index(count - 1, cx);
 96            self.scroll_handle.scroll_to_item(count - 1);
 97            cx.notify();
 98        }
 99    }
100
101    pub fn cycle_selection(&mut self, cx: &mut ViewContext<Self>) {
102        let count = self.delegate.match_count();
103        let index = self.delegate.selected_index();
104        let new_index = if index + 1 == count { 0 } else { index + 1 };
105        self.delegate.set_selected_index(new_index, cx);
106        self.scroll_handle.scroll_to_item(new_index);
107        cx.notify();
108    }
109
110    fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
111        self.delegate.dismissed(cx);
112    }
113
114    fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
115        if self.pending_update_matches.is_some() {
116            self.confirm_on_update = Some(false)
117        } else {
118            self.delegate.confirm(false, cx);
119        }
120    }
121
122    fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
123        if self.pending_update_matches.is_some() {
124            self.confirm_on_update = Some(true)
125        } else {
126            self.delegate.confirm(true, cx);
127        }
128    }
129
130    fn handle_click(&mut self, ix: usize, secondary: bool, cx: &mut ViewContext<Self>) {
131        cx.stop_propagation();
132        cx.prevent_default();
133        self.delegate.set_selected_index(ix, cx);
134        self.delegate.confirm(secondary, cx);
135    }
136
137    fn on_input_editor_event(
138        &mut self,
139        _: View<Editor>,
140        event: &editor::Event,
141        cx: &mut ViewContext<Self>,
142    ) {
143        if let editor::Event::BufferEdited = event {
144            let query = self.editor.read(cx).text(cx);
145            self.update_matches(query, cx);
146        }
147    }
148
149    pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
150        let update = self.delegate.update_matches(query, cx);
151        self.matches_updated(cx);
152        self.pending_update_matches = Some(cx.spawn(|this, mut cx| async move {
153            update.await;
154            this.update(&mut cx, |this, cx| {
155                this.matches_updated(cx);
156            })
157            .ok();
158        }));
159    }
160
161    fn matches_updated(&mut self, cx: &mut ViewContext<Self>) {
162        let index = self.delegate.selected_index();
163        self.scroll_handle.scroll_to_item(index);
164        self.pending_update_matches = None;
165        if let Some(secondary) = self.confirm_on_update.take() {
166            self.delegate.confirm(secondary, cx);
167        }
168        cx.notify();
169    }
170}
171
172impl<D: PickerDelegate> Render for Picker<D> {
173    type Element = Div<Self>;
174
175    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
176        div()
177            .context("picker")
178            .size_full()
179            .elevation_2(cx)
180            .on_action(Self::select_next)
181            .on_action(Self::select_prev)
182            .on_action(Self::select_first)
183            .on_action(Self::select_last)
184            .on_action(Self::cancel)
185            .on_action(Self::confirm)
186            .on_action(Self::secondary_confirm)
187            .child(
188                v_stack()
189                    .py_0p5()
190                    .px_1()
191                    .child(div().px_1().py_0p5().child(self.editor.clone())),
192            )
193            .child(Divider::horizontal())
194            .when(self.delegate.match_count() > 0, |el| {
195                el.child(
196                    v_stack()
197                        .p_1()
198                        .grow()
199                        .child(
200                            uniform_list("candidates", self.delegate.match_count(), {
201                                move |this: &mut Self, visible_range, cx| {
202                                    let selected_ix = this.delegate.selected_index();
203                                    visible_range
204                                        .map(|ix| {
205                                            div()
206                                                .on_mouse_down(
207                                                    MouseButton::Left,
208                                                    move |this: &mut Self, event, cx| {
209                                                        this.handle_click(
210                                                            ix,
211                                                            event.modifiers.command,
212                                                            cx,
213                                                        )
214                                                    },
215                                                )
216                                                .child(this.delegate.render_match(
217                                                    ix,
218                                                    ix == selected_ix,
219                                                    cx,
220                                                ))
221                                        })
222                                        .collect()
223                                }
224                            })
225                            .track_scroll(self.scroll_handle.clone()),
226                        )
227                        .max_h_72()
228                        .overflow_hidden(),
229                )
230            })
231            .when(self.delegate.match_count() == 0, |el| {
232                el.child(
233                    v_stack().p_1().grow().child(
234                        div()
235                            .px_1()
236                            .child(Label::new("No matches").color(LabelColor::Muted)),
237                    ),
238                )
239            })
240    }
241}