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