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