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                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}