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