1use editor::Editor;
2use gpui::{
3 div, prelude::*, uniform_list, AnyElement, AppContext, DismissEvent, Div, EventEmitter,
4 FocusHandle, FocusableView, Length, MouseButton, MouseDownEvent, Render, Task,
5 UniformListScrollHandle, View, ViewContext, WindowContext,
6};
7use std::{cmp, sync::Arc};
8use ui::{prelude::*, v_stack, Color, Divider, Label, ListItem, ListItemSpacing};
9use workspace::ModalView;
10
11pub struct Picker<D: PickerDelegate> {
12 pub delegate: D,
13 scroll_handle: UniformListScrollHandle,
14 editor: View<Editor>,
15 pending_update_matches: Option<Task<()>>,
16 confirm_on_update: Option<bool>,
17 width: Option<Length>,
18
19 /// Whether the `Picker` is rendered as a self-contained modal.
20 ///
21 /// Set this to `false` when rendering the `Picker` as part of a larger modal.
22 is_modal: bool,
23}
24
25pub trait PickerDelegate: Sized + 'static {
26 type ListItem: IntoElement;
27 fn match_count(&self) -> usize;
28 fn selected_index(&self) -> usize;
29 fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>);
30
31 fn placeholder_text(&self) -> Arc<str>;
32 fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()>;
33
34 fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>);
35 fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>);
36
37 fn render_match(
38 &self,
39 ix: usize,
40 selected: bool,
41 cx: &mut ViewContext<Picker<Self>>,
42 ) -> Option<Self::ListItem>;
43 fn render_header(&self, _: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
44 None
45 }
46 fn render_footer(&self, _: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
47 None
48 }
49}
50
51impl<D: PickerDelegate> FocusableView for Picker<D> {
52 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
53 self.editor.focus_handle(cx)
54 }
55}
56
57impl<D: PickerDelegate> Picker<D> {
58 pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
59 let editor = cx.build_view(|cx| {
60 let mut editor = Editor::single_line(cx);
61 editor.set_placeholder_text(delegate.placeholder_text(), cx);
62 editor
63 });
64 cx.subscribe(&editor, Self::on_input_editor_event).detach();
65 let mut this = Self {
66 delegate,
67 editor,
68 scroll_handle: UniformListScrollHandle::new(),
69 pending_update_matches: None,
70 confirm_on_update: None,
71 width: None,
72 is_modal: true,
73 };
74 this.update_matches("".to_string(), cx);
75 this
76 }
77
78 pub fn width(mut self, width: impl Into<gpui::Length>) -> Self {
79 self.width = Some(width.into());
80 self
81 }
82
83 pub fn modal(mut self, modal: bool) -> Self {
84 self.is_modal = modal;
85 self
86 }
87
88 pub fn focus(&self, cx: &mut WindowContext) {
89 self.editor.update(cx, |editor, cx| editor.focus(cx));
90 }
91
92 pub fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
93 let count = self.delegate.match_count();
94 if count > 0 {
95 let index = self.delegate.selected_index();
96 let ix = cmp::min(index + 1, count - 1);
97 self.delegate.set_selected_index(ix, cx);
98 self.scroll_handle.scroll_to_item(ix);
99 cx.notify();
100 }
101 }
102
103 fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
104 let count = self.delegate.match_count();
105 if count > 0 {
106 let index = self.delegate.selected_index();
107 let ix = index.saturating_sub(1);
108 self.delegate.set_selected_index(ix, cx);
109 self.scroll_handle.scroll_to_item(ix);
110 cx.notify();
111 }
112 }
113
114 fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext<Self>) {
115 let count = self.delegate.match_count();
116 if count > 0 {
117 self.delegate.set_selected_index(0, cx);
118 self.scroll_handle.scroll_to_item(0);
119 cx.notify();
120 }
121 }
122
123 fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
124 let count = self.delegate.match_count();
125 if count > 0 {
126 self.delegate.set_selected_index(count - 1, cx);
127 self.scroll_handle.scroll_to_item(count - 1);
128 cx.notify();
129 }
130 }
131
132 pub fn cycle_selection(&mut self, cx: &mut ViewContext<Self>) {
133 let count = self.delegate.match_count();
134 let index = self.delegate.selected_index();
135 let new_index = if index + 1 == count { 0 } else { index + 1 };
136 self.delegate.set_selected_index(new_index, cx);
137 self.scroll_handle.scroll_to_item(new_index);
138 cx.notify();
139 }
140
141 pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
142 self.delegate.dismissed(cx);
143 cx.emit(DismissEvent);
144 }
145
146 fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
147 if self.pending_update_matches.is_some() {
148 self.confirm_on_update = Some(false)
149 } else {
150 self.delegate.confirm(false, cx);
151 }
152 }
153
154 fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
155 if self.pending_update_matches.is_some() {
156 self.confirm_on_update = Some(true)
157 } else {
158 self.delegate.confirm(true, cx);
159 }
160 }
161
162 fn handle_click(&mut self, ix: usize, secondary: bool, cx: &mut ViewContext<Self>) {
163 cx.stop_propagation();
164 cx.prevent_default();
165 self.delegate.set_selected_index(ix, cx);
166 self.delegate.confirm(secondary, cx);
167 }
168
169 fn on_input_editor_event(
170 &mut self,
171 _: View<Editor>,
172 event: &editor::EditorEvent,
173 cx: &mut ViewContext<Self>,
174 ) {
175 match event {
176 editor::EditorEvent::BufferEdited => {
177 let query = self.editor.read(cx).text(cx);
178 self.update_matches(query, cx);
179 }
180 editor::EditorEvent::Blurred => {
181 self.cancel(&menu::Cancel, cx);
182 }
183 _ => {}
184 }
185 }
186
187 pub fn refresh(&mut self, cx: &mut ViewContext<Self>) {
188 let query = self.editor.read(cx).text(cx);
189 self.update_matches(query, cx);
190 }
191
192 pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
193 let update = self.delegate.update_matches(query, cx);
194 self.matches_updated(cx);
195 self.pending_update_matches = Some(cx.spawn(|this, mut cx| async move {
196 update.await;
197 this.update(&mut cx, |this, cx| {
198 this.matches_updated(cx);
199 })
200 .ok();
201 }));
202 }
203
204 fn matches_updated(&mut self, cx: &mut ViewContext<Self>) {
205 let index = self.delegate.selected_index();
206 self.scroll_handle.scroll_to_item(index);
207 self.pending_update_matches = None;
208 if let Some(secondary) = self.confirm_on_update.take() {
209 self.delegate.confirm(secondary, cx);
210 }
211 cx.notify();
212 }
213
214 pub fn query(&self, cx: &AppContext) -> String {
215 self.editor.read(cx).text(cx)
216 }
217
218 pub fn set_query(&self, query: impl Into<Arc<str>>, cx: &mut ViewContext<Self>) {
219 self.editor
220 .update(cx, |editor, cx| editor.set_text(query, cx));
221 }
222}
223
224impl<D: PickerDelegate> EventEmitter<DismissEvent> for Picker<D> {}
225impl<D: PickerDelegate> ModalView for Picker<D> {}
226
227impl<D: PickerDelegate> Render for Picker<D> {
228 type Element = Div;
229
230 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
231 let picker_editor = h_stack()
232 .overflow_hidden()
233 .flex_none()
234 .h_9()
235 .px_4()
236 .child(self.editor.clone());
237
238 div()
239 .key_context("Picker")
240 .size_full()
241 .when_some(self.width, |el, width| el.w(width))
242 .overflow_hidden()
243 // This is a bit of a hack to remove the modal styling when we're rendering the `Picker`
244 // as a part of a modal rather than the entire modal.
245 //
246 // We should revisit how the `Picker` is styled to make it more composable.
247 .when(self.is_modal, |this| this.elevation_3(cx))
248 .on_action(cx.listener(Self::select_next))
249 .on_action(cx.listener(Self::select_prev))
250 .on_action(cx.listener(Self::select_first))
251 .on_action(cx.listener(Self::select_last))
252 .on_action(cx.listener(Self::cancel))
253 .on_action(cx.listener(Self::confirm))
254 .on_action(cx.listener(Self::secondary_confirm))
255 .child(picker_editor)
256 .child(Divider::horizontal())
257 .when(self.delegate.match_count() > 0, |el| {
258 el.child(
259 v_stack()
260 .flex_grow()
261 .py_2()
262 .children(self.delegate.render_header(cx))
263 .child(
264 uniform_list(
265 cx.view().clone(),
266 "candidates",
267 self.delegate.match_count(),
268 {
269 let selected_index = self.delegate.selected_index();
270 move |picker, visible_range, cx| {
271 visible_range
272 .map(|ix| {
273 div()
274 .on_mouse_down(
275 MouseButton::Left,
276 cx.listener(move |this, event: &MouseDownEvent, cx| {
277 this.handle_click(
278 ix,
279 event.modifiers.command,
280 cx,
281 )
282 }),
283 )
284 .children(picker.delegate.render_match(
285 ix,
286 ix == selected_index,
287 cx,
288 ))
289 })
290 .collect()
291 }
292 },
293 )
294 .track_scroll(self.scroll_handle.clone())
295 )
296
297 .max_h_72()
298 .overflow_hidden(),
299 )
300 })
301 .when(self.delegate.match_count() == 0, |el| {
302 el.child(
303 v_stack().flex_grow().py_2().child(
304 ListItem::new("empty_state")
305 .inset(true)
306 .spacing(ListItemSpacing::Sparse)
307 .disabled(true)
308 .child(Label::new("No matches").color(Color::Muted)),
309 ),
310 )
311 })
312 .children(self.delegate.render_footer(cx))
313 }
314}