picker2.rs

  1use editor::Editor;
  2use gpui::{
  3    div, uniform_list, Component, Div, ParentElement, Render, StatelessInteractive, Styled, Task,
  4    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}
 15
 16pub trait PickerDelegate: Sized + 'static {
 17    type ListItem: Component<Picker<Self>>;
 18
 19    fn match_count(&self) -> usize;
 20    fn selected_index(&self) -> usize;
 21    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>);
 22
 23    fn placeholder_text(&self) -> Arc<str>;
 24    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()>;
 25
 26    fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>);
 27    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>);
 28
 29    fn render_match(
 30        &self,
 31        ix: usize,
 32        selected: bool,
 33        cx: &mut ViewContext<Picker<Self>>,
 34    ) -> Self::ListItem;
 35}
 36
 37impl<D: PickerDelegate> Picker<D> {
 38    pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
 39        let editor = cx.build_view(|cx| {
 40            let mut editor = Editor::single_line(cx);
 41            editor.set_placeholder_text(delegate.placeholder_text(), cx);
 42            editor
 43        });
 44        cx.subscribe(&editor, Self::on_input_editor_event).detach();
 45        let mut this = Self {
 46            delegate,
 47            scroll_handle: UniformListScrollHandle::new(),
 48            pending_update_matches: None,
 49            editor,
 50        };
 51        this.update_matches("".to_string(), cx);
 52        this
 53    }
 54
 55    pub fn focus(&self, cx: &mut WindowContext) {
 56        self.editor.update(cx, |editor, cx| editor.focus(cx));
 57    }
 58
 59    fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
 60        let count = self.delegate.match_count();
 61        if count > 0 {
 62            let index = self.delegate.selected_index();
 63            let ix = cmp::min(index + 1, count - 1);
 64            self.delegate.set_selected_index(ix, cx);
 65            self.scroll_handle.scroll_to_item(ix);
 66            cx.notify();
 67        }
 68    }
 69
 70    fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
 71        let count = self.delegate.match_count();
 72        if count > 0 {
 73            let index = self.delegate.selected_index();
 74            let ix = index.saturating_sub(1);
 75            self.delegate.set_selected_index(ix, cx);
 76            self.scroll_handle.scroll_to_item(ix);
 77            cx.notify();
 78        }
 79    }
 80
 81    fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext<Self>) {
 82        let count = self.delegate.match_count();
 83        if count > 0 {
 84            self.delegate.set_selected_index(0, cx);
 85            self.scroll_handle.scroll_to_item(0);
 86            cx.notify();
 87        }
 88    }
 89
 90    fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
 91        let count = self.delegate.match_count();
 92        if count > 0 {
 93            self.delegate.set_selected_index(count - 1, cx);
 94            self.scroll_handle.scroll_to_item(count - 1);
 95            cx.notify();
 96        }
 97    }
 98
 99    fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
100        self.delegate.dismissed(cx);
101    }
102
103    fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
104        self.delegate.confirm(false, cx);
105    }
106
107    fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
108        self.delegate.confirm(true, cx);
109    }
110
111    fn on_input_editor_event(
112        &mut self,
113        _: View<Editor>,
114        event: &editor::Event,
115        cx: &mut ViewContext<Self>,
116    ) {
117        if let editor::Event::BufferEdited = event {
118            let query = self.editor.read(cx).text(cx);
119            self.update_matches(query, cx);
120        }
121    }
122
123    pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
124        let update = self.delegate.update_matches(query, cx);
125        self.matches_updated(cx);
126        self.pending_update_matches = Some(cx.spawn(|this, mut cx| async move {
127            update.await;
128            this.update(&mut cx, |this, cx| {
129                this.matches_updated(cx);
130            })
131            .ok();
132        }));
133    }
134
135    fn matches_updated(&mut self, cx: &mut ViewContext<Self>) {
136        let index = self.delegate.selected_index();
137        self.scroll_handle.scroll_to_item(index);
138        self.pending_update_matches = None;
139        cx.notify();
140    }
141}
142
143impl<D: PickerDelegate> Render for Picker<D> {
144    type Element = Div<Self>;
145
146    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
147        div()
148            .context("picker")
149            .size_full()
150            .elevation_2(cx)
151            .on_action(Self::select_next)
152            .on_action(Self::select_prev)
153            .on_action(Self::select_first)
154            .on_action(Self::select_last)
155            .on_action(Self::cancel)
156            .on_action(Self::confirm)
157            .on_action(Self::secondary_confirm)
158            .child(
159                v_stack()
160                    .py_0p5()
161                    .px_1()
162                    .child(div().px_1().py_0p5().child(self.editor.clone())),
163            )
164            .child(Divider::horizontal())
165            .when(self.delegate.match_count() > 0, |el| {
166                el.child(
167                    v_stack()
168                        .p_1()
169                        .grow()
170                        .child(
171                            uniform_list("candidates", self.delegate.match_count(), {
172                                move |this: &mut Self, visible_range, cx| {
173                                    let selected_ix = this.delegate.selected_index();
174                                    visible_range
175                                        .map(|ix| {
176                                            this.delegate.render_match(ix, ix == selected_ix, cx)
177                                        })
178                                        .collect()
179                                }
180                            })
181                            .track_scroll(self.scroll_handle.clone()),
182                        )
183                        .max_h_72()
184                        .overflow_hidden(),
185                )
186            })
187            .when(self.delegate.match_count() == 0, |el| {
188                el.child(
189                    v_stack()
190                        .p_1()
191                        .grow()
192                        .child(Label::new("No matches").color(LabelColor::Muted)),
193                )
194            })
195    }
196}