1use editor::Editor;
2use gpui::{
3 div, prelude::*, uniform_list, AppContext, Div, FocusHandle, FocusableView, MouseButton,
4 MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,
5};
6use std::{cmp, sync::Arc};
7use ui::{prelude::*, v_stack, Color, Divider, Label};
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: IntoElement;
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 ) -> Option<Self::ListItem>;
36}
37
38impl<D: PickerDelegate> FocusableView for Picker<D> {
39 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
40 self.editor.focus_handle(cx)
41 }
42}
43
44impl<D: PickerDelegate> Picker<D> {
45 pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
46 let editor = cx.build_view(|cx| {
47 let mut editor = Editor::single_line(cx);
48 editor.set_placeholder_text(delegate.placeholder_text(), cx);
49 editor
50 });
51 cx.subscribe(&editor, Self::on_input_editor_event).detach();
52 let mut this = Self {
53 delegate,
54 editor,
55 scroll_handle: UniformListScrollHandle::new(),
56 pending_update_matches: None,
57 confirm_on_update: None,
58 };
59 this.update_matches("".to_string(), cx);
60 this
61 }
62
63 pub fn focus(&self, cx: &mut WindowContext) {
64 self.editor.update(cx, |editor, cx| editor.focus(cx));
65 }
66
67 pub fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
68 let count = self.delegate.match_count();
69 if count > 0 {
70 let index = self.delegate.selected_index();
71 let ix = cmp::min(index + 1, count - 1);
72 self.delegate.set_selected_index(ix, cx);
73 self.scroll_handle.scroll_to_item(ix);
74 cx.notify();
75 }
76 }
77
78 fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
79 let count = self.delegate.match_count();
80 if count > 0 {
81 let index = self.delegate.selected_index();
82 let ix = index.saturating_sub(1);
83 self.delegate.set_selected_index(ix, cx);
84 self.scroll_handle.scroll_to_item(ix);
85 cx.notify();
86 }
87 }
88
89 fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext<Self>) {
90 let count = self.delegate.match_count();
91 if count > 0 {
92 self.delegate.set_selected_index(0, cx);
93 self.scroll_handle.scroll_to_item(0);
94 cx.notify();
95 }
96 }
97
98 fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
99 let count = self.delegate.match_count();
100 if count > 0 {
101 self.delegate.set_selected_index(count - 1, cx);
102 self.scroll_handle.scroll_to_item(count - 1);
103 cx.notify();
104 }
105 }
106
107 pub fn cycle_selection(&mut self, cx: &mut ViewContext<Self>) {
108 let count = self.delegate.match_count();
109 let index = self.delegate.selected_index();
110 let new_index = if index + 1 == count { 0 } else { index + 1 };
111 self.delegate.set_selected_index(new_index, cx);
112 self.scroll_handle.scroll_to_item(new_index);
113 cx.notify();
114 }
115
116 fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
117 dbg!("canceling!");
118 self.delegate.dismissed(cx);
119 }
120
121 fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
122 if self.pending_update_matches.is_some() {
123 self.confirm_on_update = Some(false)
124 } else {
125 self.delegate.confirm(false, cx);
126 }
127 }
128
129 fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
130 if self.pending_update_matches.is_some() {
131 self.confirm_on_update = Some(true)
132 } else {
133 self.delegate.confirm(true, cx);
134 }
135 }
136
137 fn handle_click(&mut self, ix: usize, secondary: bool, cx: &mut ViewContext<Self>) {
138 cx.stop_propagation();
139 cx.prevent_default();
140 self.delegate.set_selected_index(ix, cx);
141 self.delegate.confirm(secondary, cx);
142 }
143
144 fn on_input_editor_event(
145 &mut self,
146 _: View<Editor>,
147 event: &editor::EditorEvent,
148 cx: &mut ViewContext<Self>,
149 ) {
150 if let editor::EditorEvent::BufferEdited = event {
151 let query = self.editor.read(cx).text(cx);
152 self.update_matches(query, cx);
153 }
154 }
155
156 pub fn refresh(&mut self, cx: &mut ViewContext<Self>) {
157 let query = self.editor.read(cx).text(cx);
158 self.update_matches(query, cx);
159 }
160
161 pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
162 let update = self.delegate.update_matches(query, cx);
163 self.matches_updated(cx);
164 self.pending_update_matches = Some(cx.spawn(|this, mut cx| async move {
165 update.await;
166 this.update(&mut cx, |this, cx| {
167 this.matches_updated(cx);
168 })
169 .ok();
170 }));
171 }
172
173 fn matches_updated(&mut self, cx: &mut ViewContext<Self>) {
174 let index = self.delegate.selected_index();
175 self.scroll_handle.scroll_to_item(index);
176 self.pending_update_matches = None;
177 if let Some(secondary) = self.confirm_on_update.take() {
178 self.delegate.confirm(secondary, cx);
179 }
180 cx.notify();
181 }
182}
183
184impl<D: PickerDelegate> Render for Picker<D> {
185 type Element = Div;
186
187 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
188 div()
189 .key_context("picker")
190 .size_full()
191 .elevation_2(cx)
192 .on_action(cx.listener(Self::select_next))
193 .on_action(cx.listener(Self::select_prev))
194 .on_action(cx.listener(Self::select_first))
195 .on_action(cx.listener(Self::select_last))
196 .on_action(cx.listener(Self::cancel))
197 .on_action(cx.listener(Self::confirm))
198 .on_action(cx.listener(Self::secondary_confirm))
199 .child(
200 v_stack()
201 .py_0p5()
202 .px_1()
203 .child(div().px_1().py_0p5().child(self.editor.clone())),
204 )
205 .child(Divider::horizontal())
206 .when(self.delegate.match_count() > 0, |el| {
207 el.child(
208 v_stack()
209 .p_1()
210 .grow()
211 .child(
212 uniform_list(
213 cx.view().clone(),
214 "candidates",
215 self.delegate.match_count(),
216 {
217 let selected_index = self.delegate.selected_index();
218
219 move |picker, visible_range, cx| {
220 visible_range
221 .map(|ix| {
222 div()
223 .on_mouse_down(
224 MouseButton::Left,
225 cx.listener(move |this, event: &MouseDownEvent, cx| {
226 this.handle_click(
227 ix,
228 event.modifiers.command,
229 cx,
230 )
231 }),
232 )
233 .children(picker.delegate.render_match(
234 ix,
235 ix == selected_index,
236 cx,
237 ))
238 })
239 .collect()
240 }
241 },
242 )
243 .track_scroll(self.scroll_handle.clone()),
244 )
245 .max_h_72()
246 .overflow_hidden(),
247 )
248 })
249 .when(self.delegate.match_count() == 0, |el| {
250 el.child(
251 v_stack().p_1().grow().child(
252 div()
253 .px_1()
254 .child(Label::new("No matches").color(Color::Muted)),
255 ),
256 )
257 })
258 }
259}