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 ui::{prelude::*, v_stack, Divider};
  9
 10pub struct Picker<D: PickerDelegate> {
 11    pub delegate: D,
 12    scroll_handle: UniformListScrollHandle,
 13    editor: View<Editor>,
 14    pending_update_matches: Option<Task<Option<()>>>,
 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| Editor::single_line(cx));
 41        cx.subscribe(&editor, Self::on_input_editor_event).detach();
 42        Self {
 43            delegate,
 44            scroll_handle: UniformListScrollHandle::new(),
 45            pending_update_matches: None,
 46            editor,
 47        }
 48    }
 49
 50    pub fn focus(&self, cx: &mut WindowContext) {
 51        self.editor.update(cx, |editor, cx| editor.focus(cx));
 52    }
 53
 54    fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
 55        let count = self.delegate.match_count();
 56        if count > 0 {
 57            let index = self.delegate.selected_index();
 58            let ix = cmp::min(index + 1, count - 1);
 59            self.delegate.set_selected_index(ix, cx);
 60            self.scroll_handle.scroll_to_item(ix);
 61            cx.notify();
 62        }
 63    }
 64
 65    fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
 66        let count = self.delegate.match_count();
 67        if count > 0 {
 68            let index = self.delegate.selected_index();
 69            let ix = index.saturating_sub(1);
 70            self.delegate.set_selected_index(ix, cx);
 71            self.scroll_handle.scroll_to_item(ix);
 72            cx.notify();
 73        }
 74    }
 75
 76    fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext<Self>) {
 77        let count = self.delegate.match_count();
 78        if count > 0 {
 79            self.delegate.set_selected_index(0, cx);
 80            self.scroll_handle.scroll_to_item(0);
 81            cx.notify();
 82        }
 83    }
 84
 85    fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
 86        let count = self.delegate.match_count();
 87        if count > 0 {
 88            self.delegate.set_selected_index(count - 1, cx);
 89            self.scroll_handle.scroll_to_item(count - 1);
 90            cx.notify();
 91        }
 92    }
 93
 94    fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
 95        self.delegate.dismissed(cx);
 96    }
 97
 98    fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
 99        self.delegate.confirm(false, cx);
100    }
101
102    fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
103        self.delegate.confirm(true, cx);
104    }
105
106    fn on_input_editor_event(
107        &mut self,
108        _: View<Editor>,
109        event: &editor::Event,
110        cx: &mut ViewContext<Self>,
111    ) {
112        if let editor::Event::BufferEdited = event {
113            let query = self.editor.read(cx).text(cx);
114            self.update_matches(query, cx);
115        }
116    }
117
118    pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
119        let update = self.delegate.update_matches(query, cx);
120        self.matches_updated(cx);
121        self.pending_update_matches = Some(cx.spawn(|this, mut cx| async move {
122            update.await;
123            this.update(&mut cx, |this, cx| {
124                this.matches_updated(cx);
125            })
126            .ok()
127        }));
128    }
129
130    fn matches_updated(&mut self, cx: &mut ViewContext<Self>) {
131        let index = self.delegate.selected_index();
132        self.scroll_handle.scroll_to_item(index);
133        self.pending_update_matches = None;
134        cx.notify();
135    }
136}
137
138impl<D: PickerDelegate> Render for Picker<D> {
139    type Element = Div<Self, StatefulInteractivity<Self>, FocusEnabled<Self>>;
140
141    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
142        div()
143            .context("picker")
144            .id("picker-container")
145            .focusable()
146            .size_full()
147            .elevation_2(cx)
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()
157                    .py_0p5()
158                    .px_1()
159                    .child(div().px_1().py_0p5().child(self.editor.clone())),
160            )
161            .child(Divider::horizontal())
162            .child(
163                v_stack()
164                    .p_1()
165                    .grow()
166                    .child(
167                        uniform_list("candidates", self.delegate.match_count(), {
168                            move |this: &mut Self, visible_range, cx| {
169                                let selected_ix = this.delegate.selected_index();
170                                visible_range
171                                    .map(|ix| this.delegate.render_match(ix, ix == selected_ix, cx))
172                                    .collect()
173                            }
174                        })
175                        .track_scroll(self.scroll_handle.clone()),
176                    )
177                    .max_h_72()
178                    .overflow_hidden(),
179            )
180    }
181}