picker2.rs

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