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