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}