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