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, LabelColor};
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 pub 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 pub fn cycle_selection(&mut self, cx: &mut ViewContext<Self>) {
102 let count = self.delegate.match_count();
103 let index = self.delegate.selected_index();
104 let new_index = if index + 1 == count { 0 } else { index + 1 };
105 self.delegate.set_selected_index(new_index, cx);
106 self.scroll_handle.scroll_to_item(new_index);
107 cx.notify();
108 }
109
110 fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
111 self.delegate.dismissed(cx);
112 }
113
114 fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
115 if self.pending_update_matches.is_some() {
116 self.confirm_on_update = Some(false)
117 } else {
118 self.delegate.confirm(false, cx);
119 }
120 }
121
122 fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
123 if self.pending_update_matches.is_some() {
124 self.confirm_on_update = Some(true)
125 } else {
126 self.delegate.confirm(true, cx);
127 }
128 }
129
130 fn handle_click(&mut self, ix: usize, secondary: bool, cx: &mut ViewContext<Self>) {
131 cx.stop_propagation();
132 cx.prevent_default();
133 self.delegate.set_selected_index(ix, cx);
134 self.delegate.confirm(secondary, cx);
135 }
136
137 fn on_input_editor_event(
138 &mut self,
139 _: View<Editor>,
140 event: &editor::Event,
141 cx: &mut ViewContext<Self>,
142 ) {
143 if let editor::Event::BufferEdited = event {
144 let query = self.editor.read(cx).text(cx);
145 self.update_matches(query, cx);
146 }
147 }
148
149 pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
150 let update = self.delegate.update_matches(query, cx);
151 self.matches_updated(cx);
152 self.pending_update_matches = Some(cx.spawn(|this, mut cx| async move {
153 update.await;
154 this.update(&mut cx, |this, cx| {
155 this.matches_updated(cx);
156 })
157 .ok();
158 }));
159 }
160
161 fn matches_updated(&mut self, cx: &mut ViewContext<Self>) {
162 let index = self.delegate.selected_index();
163 self.scroll_handle.scroll_to_item(index);
164 self.pending_update_matches = None;
165 if let Some(secondary) = self.confirm_on_update.take() {
166 self.delegate.confirm(secondary, cx);
167 }
168 cx.notify();
169 }
170}
171
172impl<D: PickerDelegate> Render for Picker<D> {
173 type Element = Div<Self>;
174
175 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
176 div()
177 .context("picker")
178 .size_full()
179 .elevation_2(cx)
180 .on_action(Self::select_next)
181 .on_action(Self::select_prev)
182 .on_action(Self::select_first)
183 .on_action(Self::select_last)
184 .on_action(Self::cancel)
185 .on_action(Self::confirm)
186 .on_action(Self::secondary_confirm)
187 .child(
188 v_stack()
189 .py_0p5()
190 .px_1()
191 .child(div().px_1().py_0p5().child(self.editor.clone())),
192 )
193 .child(Divider::horizontal())
194 .when(self.delegate.match_count() > 0, |el| {
195 el.child(
196 v_stack()
197 .p_1()
198 .grow()
199 .child(
200 uniform_list("candidates", self.delegate.match_count(), {
201 move |this: &mut Self, visible_range, cx| {
202 let selected_ix = this.delegate.selected_index();
203 visible_range
204 .map(|ix| {
205 div()
206 .on_mouse_down(
207 MouseButton::Left,
208 move |this: &mut Self, event, cx| {
209 this.handle_click(
210 ix,
211 event.modifiers.command,
212 cx,
213 )
214 },
215 )
216 .child(this.delegate.render_match(
217 ix,
218 ix == selected_ix,
219 cx,
220 ))
221 })
222 .collect()
223 }
224 })
225 .track_scroll(self.scroll_handle.clone()),
226 )
227 .max_h_72()
228 .overflow_hidden(),
229 )
230 })
231 .when(self.delegate.match_count() == 0, |el| {
232 el.child(
233 v_stack().p_1().grow().child(
234 div()
235 .px_1()
236 .child(Label::new("No matches").color(LabelColor::Muted)),
237 ),
238 )
239 })
240 }
241}