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