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