1use fuzzy::StringMatchCandidate;
2use gpui::{
3 div, prelude::*, Div, KeyBinding, Render, SharedString, Styled, Task, View, WindowContext,
4};
5use picker::{Picker, PickerDelegate};
6use std::sync::Arc;
7use theme2::ActiveTheme;
8use ui::{Label, ListItem};
9
10pub struct PickerStory {
11 picker: View<Picker<Delegate>>,
12}
13
14struct Delegate {
15 candidates: Arc<[StringMatchCandidate]>,
16 matches: Vec<usize>,
17 selected_ix: usize,
18}
19
20impl Delegate {
21 fn new(strings: &[&str]) -> Self {
22 Self {
23 candidates: strings
24 .iter()
25 .copied()
26 .enumerate()
27 .map(|(id, string)| StringMatchCandidate {
28 id,
29 char_bag: string.into(),
30 string: string.into(),
31 })
32 .collect(),
33 matches: vec![],
34 selected_ix: 0,
35 }
36 }
37}
38
39impl PickerDelegate for Delegate {
40 type ListItem = ListItem;
41
42 fn match_count(&self) -> usize {
43 self.candidates.len()
44 }
45
46 fn placeholder_text(&self) -> Arc<str> {
47 "Test".into()
48 }
49
50 fn render_match(
51 &self,
52 ix: usize,
53 selected: bool,
54 _cx: &mut gpui::ViewContext<Picker<Self>>,
55 ) -> Option<Self::ListItem> {
56 let Some(candidate_ix) = self.matches.get(ix) else {
57 return None;
58 };
59 // TASK: Make StringMatchCandidate::string a SharedString
60 let candidate = SharedString::from(self.candidates[*candidate_ix].string.clone());
61
62 Some(
63 ListItem::new(ix)
64 .inset(true)
65 .selected(selected)
66 .child(Label::new(candidate)),
67 )
68 }
69
70 fn selected_index(&self) -> usize {
71 self.selected_ix
72 }
73
74 fn set_selected_index(&mut self, ix: usize, cx: &mut gpui::ViewContext<Picker<Self>>) {
75 self.selected_ix = ix;
76 cx.notify();
77 }
78
79 fn confirm(&mut self, secondary: bool, _cx: &mut gpui::ViewContext<Picker<Self>>) {
80 let candidate_ix = self.matches[self.selected_ix];
81 let candidate = self.candidates[candidate_ix].string.clone();
82
83 if secondary {
84 eprintln!("Secondary confirmed {}", candidate)
85 } else {
86 eprintln!("Confirmed {}", candidate)
87 }
88 }
89
90 fn dismissed(&mut self, cx: &mut gpui::ViewContext<Picker<Self>>) {
91 cx.quit();
92 }
93
94 fn update_matches(
95 &mut self,
96 query: String,
97 cx: &mut gpui::ViewContext<Picker<Self>>,
98 ) -> Task<()> {
99 let candidates = self.candidates.clone();
100 self.matches = cx
101 .background_executor()
102 .block(fuzzy::match_strings(
103 &candidates,
104 &query,
105 true,
106 100,
107 &Default::default(),
108 cx.background_executor().clone(),
109 ))
110 .into_iter()
111 .map(|r| r.candidate_id)
112 .collect();
113 self.selected_ix = 0;
114 Task::ready(())
115 }
116}
117
118impl PickerStory {
119 pub fn new(cx: &mut WindowContext) -> View<Self> {
120 cx.build_view(|cx| {
121 cx.bind_keys([
122 KeyBinding::new("up", menu::SelectPrev, Some("picker")),
123 KeyBinding::new("pageup", menu::SelectFirst, Some("picker")),
124 KeyBinding::new("shift-pageup", menu::SelectFirst, Some("picker")),
125 KeyBinding::new("ctrl-p", menu::SelectPrev, Some("picker")),
126 KeyBinding::new("down", menu::SelectNext, Some("picker")),
127 KeyBinding::new("pagedown", menu::SelectLast, Some("picker")),
128 KeyBinding::new("shift-pagedown", menu::SelectFirst, Some("picker")),
129 KeyBinding::new("ctrl-n", menu::SelectNext, Some("picker")),
130 KeyBinding::new("cmd-up", menu::SelectFirst, Some("picker")),
131 KeyBinding::new("cmd-down", menu::SelectLast, Some("picker")),
132 KeyBinding::new("enter", menu::Confirm, Some("picker")),
133 KeyBinding::new("ctrl-enter", menu::ShowContextMenu, Some("picker")),
134 KeyBinding::new("cmd-enter", menu::SecondaryConfirm, Some("picker")),
135 KeyBinding::new("escape", menu::Cancel, Some("picker")),
136 KeyBinding::new("ctrl-c", menu::Cancel, Some("picker")),
137 ]);
138
139 PickerStory {
140 picker: cx.build_view(|cx| {
141 let mut delegate = Delegate::new(&[
142 "Baguette (France)",
143 "Baklava (Turkey)",
144 "Beef Wellington (UK)",
145 "Biryani (India)",
146 "Borscht (Ukraine)",
147 "Bratwurst (Germany)",
148 "Bulgogi (Korea)",
149 "Burrito (USA)",
150 "Ceviche (Peru)",
151 "Chicken Tikka Masala (India)",
152 "Churrasco (Brazil)",
153 "Couscous (North Africa)",
154 "Croissant (France)",
155 "Dim Sum (China)",
156 "Empanada (Argentina)",
157 "Fajitas (Mexico)",
158 "Falafel (Middle East)",
159 "Feijoada (Brazil)",
160 "Fish and Chips (UK)",
161 "Fondue (Switzerland)",
162 "Goulash (Hungary)",
163 "Haggis (Scotland)",
164 "Kebab (Middle East)",
165 "Kimchi (Korea)",
166 "Lasagna (Italy)",
167 "Maple Syrup Pancakes (Canada)",
168 "Moussaka (Greece)",
169 "Pad Thai (Thailand)",
170 "Paella (Spain)",
171 "Pancakes (USA)",
172 "Pasta Carbonara (Italy)",
173 "Pavlova (Australia)",
174 "Peking Duck (China)",
175 "Pho (Vietnam)",
176 "Pierogi (Poland)",
177 "Pizza (Italy)",
178 "Poutine (Canada)",
179 "Pretzel (Germany)",
180 "Ramen (Japan)",
181 "Rendang (Indonesia)",
182 "Sashimi (Japan)",
183 "Satay (Indonesia)",
184 "Shepherd's Pie (Ireland)",
185 "Sushi (Japan)",
186 "Tacos (Mexico)",
187 "Tandoori Chicken (India)",
188 "Tortilla (Spain)",
189 "Tzatziki (Greece)",
190 "Wiener Schnitzel (Austria)",
191 ]);
192 delegate.update_matches("".into(), cx).detach();
193
194 let picker = Picker::new(delegate, cx);
195 picker.focus(cx);
196 picker
197 }),
198 }
199 })
200 }
201}
202
203impl Render for PickerStory {
204 type Element = Div;
205
206 fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
207 div()
208 .bg(cx.theme().styles.colors.background)
209 .size_full()
210 .child(self.picker.clone())
211 }
212}