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