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