picker.rs

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