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