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