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