1use editor::Editor;
2use gpui::{
3 div, uniform_list, Component, Div, ParentElement, Render, StatelessInteractive, Styled, Task,
4 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<Option<()>>>,
14}
15
16pub trait PickerDelegate: Sized + 'static {
17 type ListItem: Component<Picker<Self>>;
18
19 fn match_count(&self) -> usize;
20 fn selected_index(&self) -> usize;
21 fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>);
22
23 fn placeholder_text(&self) -> Arc<str>;
24 fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()>;
25
26 fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>);
27 fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>);
28
29 fn render_match(
30 &self,
31 ix: usize,
32 selected: bool,
33 cx: &mut ViewContext<Picker<Self>>,
34 ) -> Self::ListItem;
35}
36
37impl<D: PickerDelegate> Picker<D> {
38 pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
39 let editor = cx.build_view(|cx| {
40 let mut editor = Editor::single_line(cx);
41 editor.set_placeholder_text(delegate.placeholder_text(), cx);
42 editor
43 });
44 cx.subscribe(&editor, Self::on_input_editor_event).detach();
45 Self {
46 delegate,
47 scroll_handle: UniformListScrollHandle::new(),
48 pending_update_matches: None,
49 editor,
50 }
51 }
52
53 pub fn focus(&self, cx: &mut WindowContext) {
54 self.editor.update(cx, |editor, cx| editor.focus(cx));
55 }
56
57 fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
58 let count = self.delegate.match_count();
59 if count > 0 {
60 let index = self.delegate.selected_index();
61 let ix = cmp::min(index + 1, count - 1);
62 self.delegate.set_selected_index(ix, cx);
63 self.scroll_handle.scroll_to_item(ix);
64 cx.notify();
65 }
66 }
67
68 fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
69 let count = self.delegate.match_count();
70 if count > 0 {
71 let index = self.delegate.selected_index();
72 let ix = index.saturating_sub(1);
73 self.delegate.set_selected_index(ix, cx);
74 self.scroll_handle.scroll_to_item(ix);
75 cx.notify();
76 }
77 }
78
79 fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext<Self>) {
80 let count = self.delegate.match_count();
81 if count > 0 {
82 self.delegate.set_selected_index(0, cx);
83 self.scroll_handle.scroll_to_item(0);
84 cx.notify();
85 }
86 }
87
88 fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
89 let count = self.delegate.match_count();
90 if count > 0 {
91 self.delegate.set_selected_index(count - 1, cx);
92 self.scroll_handle.scroll_to_item(count - 1);
93 cx.notify();
94 }
95 }
96
97 fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
98 self.delegate.dismissed(cx);
99 }
100
101 fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
102 self.delegate.confirm(false, cx);
103 }
104
105 fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
106 self.delegate.confirm(true, cx);
107 }
108
109 fn on_input_editor_event(
110 &mut self,
111 _: View<Editor>,
112 event: &editor::Event,
113 cx: &mut ViewContext<Self>,
114 ) {
115 if let editor::Event::BufferEdited = event {
116 let query = self.editor.read(cx).text(cx);
117 self.update_matches(query, cx);
118 }
119 }
120
121 pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
122 let update = self.delegate.update_matches(query, cx);
123 self.matches_updated(cx);
124 self.pending_update_matches = Some(cx.spawn(|this, mut cx| async move {
125 update.await;
126 this.update(&mut cx, |this, cx| {
127 this.matches_updated(cx);
128 })
129 .ok()
130 }));
131 }
132
133 fn matches_updated(&mut self, cx: &mut ViewContext<Self>) {
134 let index = self.delegate.selected_index();
135 self.scroll_handle.scroll_to_item(index);
136 self.pending_update_matches = None;
137 cx.notify();
138 }
139}
140
141impl<D: PickerDelegate> Render for Picker<D> {
142 type Element = Div<Self>;
143
144 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
145 div()
146 .context("picker")
147 .size_full()
148 .elevation_2(cx)
149 .on_action(Self::select_next)
150 .on_action(Self::select_prev)
151 .on_action(Self::select_first)
152 .on_action(Self::select_last)
153 .on_action(Self::cancel)
154 .on_action(Self::confirm)
155 .on_action(Self::secondary_confirm)
156 .child(
157 v_stack()
158 .py_0p5()
159 .px_1()
160 .child(div().px_1().py_0p5().child(self.editor.clone())),
161 )
162 .child(Divider::horizontal())
163 .when(self.delegate.match_count() > 0, |el| {
164 el.child(
165 v_stack()
166 .p_1()
167 .grow()
168 .child(
169 uniform_list("candidates", self.delegate.match_count(), {
170 move |this: &mut Self, visible_range, cx| {
171 let selected_ix = this.delegate.selected_index();
172 visible_range
173 .map(|ix| {
174 this.delegate.render_match(ix, ix == selected_ix, cx)
175 })
176 .collect()
177 }
178 })
179 .track_scroll(self.scroll_handle.clone()),
180 )
181 .max_h_72()
182 .overflow_hidden(),
183 )
184 })
185 .when(self.delegate.match_count() == 0, |el| {
186 el.child(
187 v_stack()
188 .p_1()
189 .grow()
190 .child(Label::new("No matches").color(LabelColor::Muted)),
191 )
192 })
193 }
194}