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 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 ViewContext<Picker<Self>>) {
68 self.selected_ix = ix;
69 cx.notify();
70 }
71
72 fn confirm(&mut self, secondary: bool, _cx: &mut 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 ViewContext<Picker<Self>>) {
84 cx.quit();
85 }
86
87 fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
88 let candidates = self.candidates.clone();
89 self.matches = cx
90 .background_executor()
91 .block(fuzzy::match_strings(
92 &candidates,
93 &query,
94 true,
95 100,
96 &Default::default(),
97 cx.background_executor().clone(),
98 ))
99 .into_iter()
100 .map(|r| r.candidate_id)
101 .collect();
102 self.selected_ix = 0;
103 Task::ready(())
104 }
105}
106
107impl PickerStory {
108 pub fn new(cx: &mut WindowContext) -> View<Self> {
109 cx.new_view(|cx| {
110 cx.bind_keys([
111 KeyBinding::new("up", menu::SelectPrev, Some("picker")),
112 KeyBinding::new("pageup", menu::SelectFirst, Some("picker")),
113 KeyBinding::new("shift-pageup", menu::SelectFirst, Some("picker")),
114 KeyBinding::new("ctrl-p", menu::SelectPrev, Some("picker")),
115 KeyBinding::new("down", menu::SelectNext, Some("picker")),
116 KeyBinding::new("pagedown", menu::SelectLast, Some("picker")),
117 KeyBinding::new("shift-pagedown", menu::SelectFirst, Some("picker")),
118 KeyBinding::new("ctrl-n", menu::SelectNext, Some("picker")),
119 KeyBinding::new("cmd-up", menu::SelectFirst, Some("picker")),
120 KeyBinding::new("cmd-down", menu::SelectLast, Some("picker")),
121 KeyBinding::new("enter", menu::Confirm, Some("picker")),
122 KeyBinding::new("ctrl-enter", menu::SecondaryConfirm, Some("picker")),
123 KeyBinding::new("cmd-enter", menu::SecondaryConfirm, Some("picker")),
124 KeyBinding::new("escape", menu::Cancel, Some("picker")),
125 KeyBinding::new("ctrl-c", menu::Cancel, Some("picker")),
126 ]);
127
128 PickerStory {
129 picker: cx.new_view(|cx| {
130 let mut delegate = Delegate::new(&[
131 "Baguette (France)",
132 "Baklava (Turkey)",
133 "Beef Wellington (UK)",
134 "Biryani (India)",
135 "Borscht (Ukraine)",
136 "Bratwurst (Germany)",
137 "Bulgogi (Korea)",
138 "Burrito (USA)",
139 "Ceviche (Peru)",
140 "Chicken Tikka Masala (India)",
141 "Churrasco (Brazil)",
142 "Couscous (North Africa)",
143 "Croissant (France)",
144 "Dim Sum (China)",
145 "Empanada (Argentina)",
146 "Fajitas (Mexico)",
147 "Falafel (Middle East)",
148 "Feijoada (Brazil)",
149 "Fish and Chips (UK)",
150 "Fondue (Switzerland)",
151 "Goulash (Hungary)",
152 "Haggis (Scotland)",
153 "Kebab (Middle East)",
154 "Kimchi (Korea)",
155 "Lasagna (Italy)",
156 "Maple Syrup Pancakes (Canada)",
157 "Moussaka (Greece)",
158 "Pad Thai (Thailand)",
159 "Paella (Spain)",
160 "Pancakes (USA)",
161 "Pasta Carbonara (Italy)",
162 "Pavlova (Australia)",
163 "Peking Duck (China)",
164 "Pho (Vietnam)",
165 "Pierogi (Poland)",
166 "Pizza (Italy)",
167 "Poutine (Canada)",
168 "Pretzel (Germany)",
169 "Ramen (Japan)",
170 "Rendang (Indonesia)",
171 "Sashimi (Japan)",
172 "Satay (Indonesia)",
173 "Shepherd's Pie (Ireland)",
174 "Sushi (Japan)",
175 "Tacos (Mexico)",
176 "Tandoori Chicken (India)",
177 "Tortilla (Spain)",
178 "Tzatziki (Greece)",
179 "Wiener Schnitzel (Austria)",
180 ]);
181 delegate.update_matches("".into(), cx).detach();
182
183 let picker = Picker::uniform_list(delegate, cx);
184 picker.focus(cx);
185 picker
186 }),
187 }
188 })
189 }
190}
191
192impl Render for PickerStory {
193 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
194 div()
195 .bg(cx.theme().styles.colors.background)
196 .size_full()
197 .child(self.picker.clone())
198 }
199}