1use editor::Editor;
2use gpui::{
3 div, prelude::*, uniform_list, Component, Div, MouseButton, Render, Task,
4 UniformListScrollHandle, View, ViewContext, WindowContext,
5};
6use std::{cmp, sync::Arc};
7use ui::{prelude::*, v_stack, Divider, Label, TextColor};
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 refresh(&mut self, cx: &mut ViewContext<Self>) {
150 let query = self.editor.read(cx).text(cx);
151 self.update_matches(query, cx);
152 }
153
154 pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
155 let update = self.delegate.update_matches(query, cx);
156 self.matches_updated(cx);
157 self.pending_update_matches = Some(cx.spawn(|this, mut cx| async move {
158 update.await;
159 this.update(&mut cx, |this, cx| {
160 this.matches_updated(cx);
161 })
162 .ok();
163 }));
164 }
165
166 fn matches_updated(&mut self, cx: &mut ViewContext<Self>) {
167 let index = self.delegate.selected_index();
168 self.scroll_handle.scroll_to_item(index);
169 self.pending_update_matches = None;
170 if let Some(secondary) = self.confirm_on_update.take() {
171 self.delegate.confirm(secondary, cx);
172 }
173 cx.notify();
174 }
175}
176
177impl<D: PickerDelegate> Render for Picker<D> {
178 type Element = Div<Self>;
179
180 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
181 div()
182 .key_context("picker")
183 .size_full()
184 .elevation_2(cx)
185 .on_action(Self::select_next)
186 .on_action(Self::select_prev)
187 .on_action(Self::select_first)
188 .on_action(Self::select_last)
189 .on_action(Self::cancel)
190 .on_action(Self::confirm)
191 .on_action(Self::secondary_confirm)
192 .child(
193 v_stack()
194 .py_0p5()
195 .px_1()
196 .child(div().px_1().py_0p5().child(self.editor.clone())),
197 )
198 .child(Divider::horizontal())
199 .when(self.delegate.match_count() > 0, |el| {
200 el.child(
201 v_stack()
202 .p_1()
203 .grow()
204 .child(
205 uniform_list("candidates", self.delegate.match_count(), {
206 move |this: &mut Self, visible_range, cx| {
207 let selected_ix = this.delegate.selected_index();
208 visible_range
209 .map(|ix| {
210 div()
211 .on_mouse_down(
212 MouseButton::Left,
213 move |this: &mut Self, event, cx| {
214 this.handle_click(
215 ix,
216 event.modifiers.command,
217 cx,
218 )
219 },
220 )
221 .child(this.delegate.render_match(
222 ix,
223 ix == selected_ix,
224 cx,
225 ))
226 })
227 .collect()
228 }
229 })
230 .track_scroll(self.scroll_handle.clone()),
231 )
232 .max_h_72()
233 .overflow_hidden(),
234 )
235 })
236 .when(self.delegate.match_count() == 0, |el| {
237 el.child(
238 v_stack().p_1().grow().child(
239 div()
240 .px_1()
241 .child(Label::new("No matches").color(TextColor::Muted)),
242 ),
243 )
244 })
245 }
246}