picker.rs

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