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