picker.rs

  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}