picker.rs

  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}