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, TextColor};
  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    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    fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
102        self.delegate.dismissed(cx);
103    }
104
105    fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
106        if self.pending_update_matches.is_some() {
107            self.confirm_on_update = Some(false)
108        } else {
109            self.delegate.confirm(false, cx);
110        }
111    }
112
113    fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
114        if self.pending_update_matches.is_some() {
115            self.confirm_on_update = Some(true)
116        } else {
117            self.delegate.confirm(true, cx);
118        }
119    }
120
121    fn handle_click(&mut self, ix: usize, secondary: bool, cx: &mut ViewContext<Self>) {
122        cx.stop_propagation();
123        cx.prevent_default();
124        self.delegate.set_selected_index(ix, cx);
125        self.delegate.confirm(secondary, cx);
126    }
127
128    fn on_input_editor_event(
129        &mut self,
130        _: View<Editor>,
131        event: &editor::Event,
132        cx: &mut ViewContext<Self>,
133    ) {
134        if let editor::Event::BufferEdited = event {
135            let query = self.editor.read(cx).text(cx);
136            self.update_matches(query, cx);
137        }
138    }
139
140    pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
141        let update = self.delegate.update_matches(query, cx);
142        self.matches_updated(cx);
143        self.pending_update_matches = Some(cx.spawn(|this, mut cx| async move {
144            update.await;
145            this.update(&mut cx, |this, cx| {
146                this.matches_updated(cx);
147            })
148            .ok();
149        }));
150    }
151
152    fn matches_updated(&mut self, cx: &mut ViewContext<Self>) {
153        let index = self.delegate.selected_index();
154        self.scroll_handle.scroll_to_item(index);
155        self.pending_update_matches = None;
156        if let Some(secondary) = self.confirm_on_update.take() {
157            self.delegate.confirm(secondary, cx);
158        }
159        cx.notify();
160    }
161}
162
163impl<D: PickerDelegate> Render for Picker<D> {
164    type Element = Div<Self>;
165
166    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
167        div()
168            .context("picker")
169            .size_full()
170            .elevation_2(cx)
171            .on_action(Self::select_next)
172            .on_action(Self::select_prev)
173            .on_action(Self::select_first)
174            .on_action(Self::select_last)
175            .on_action(Self::cancel)
176            .on_action(Self::confirm)
177            .on_action(Self::secondary_confirm)
178            .child(
179                v_stack()
180                    .py_0p5()
181                    .px_1()
182                    .child(div().px_1().py_0p5().child(self.editor.clone())),
183            )
184            .child(Divider::horizontal())
185            .when(self.delegate.match_count() > 0, |el| {
186                el.child(
187                    v_stack()
188                        .p_1()
189                        .grow()
190                        .child(
191                            uniform_list("candidates", self.delegate.match_count(), {
192                                move |this: &mut Self, visible_range, cx| {
193                                    let selected_ix = this.delegate.selected_index();
194                                    visible_range
195                                        .map(|ix| {
196                                            div()
197                                                .on_mouse_down(
198                                                    MouseButton::Left,
199                                                    move |this: &mut Self, event, cx| {
200                                                        this.handle_click(
201                                                            ix,
202                                                            event.modifiers.command,
203                                                            cx,
204                                                        )
205                                                    },
206                                                )
207                                                .child(this.delegate.render_match(
208                                                    ix,
209                                                    ix == selected_ix,
210                                                    cx,
211                                                ))
212                                        })
213                                        .collect()
214                                }
215                            })
216                            .track_scroll(self.scroll_handle.clone()),
217                        )
218                        .max_h_72()
219                        .overflow_hidden(),
220                )
221            })
222            .when(self.delegate.match_count() == 0, |el| {
223                el.child(
224                    v_stack().p_1().grow().child(
225                        div()
226                            .px_1()
227                            .child(Label::new("No matches").color(TextColor::Muted)),
228                    ),
229                )
230            })
231    }
232}