picker.rs

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