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