theme_selector.rs

  1use std::{cmp, sync::Arc};
  2
  3use crate::{
  4    editor::{self, Editor},
  5    fuzzy::{match_strings, StringMatch, StringMatchCandidate},
  6    settings::ThemeRegistry,
  7    workspace::Workspace,
  8    AppState, Settings,
  9};
 10use gpui::{
 11    action,
 12    elements::*,
 13    keymap::{self, menu, Binding},
 14    AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, View,
 15    ViewContext, ViewHandle,
 16};
 17use parking_lot::Mutex;
 18use postage::watch;
 19
 20pub struct ThemeSelector {
 21    settings_tx: Arc<Mutex<watch::Sender<Settings>>>,
 22    settings: watch::Receiver<Settings>,
 23    registry: Arc<ThemeRegistry>,
 24    matches: Vec<StringMatch>,
 25    query_editor: ViewHandle<Editor>,
 26    list_state: UniformListState,
 27    selected_index: usize,
 28}
 29
 30action!(Confirm);
 31action!(Toggle, Arc<AppState>);
 32action!(Reload, Arc<AppState>);
 33
 34pub fn init(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
 35    cx.add_action(ThemeSelector::confirm);
 36    cx.add_action(ThemeSelector::select_prev);
 37    cx.add_action(ThemeSelector::select_next);
 38    cx.add_action(ThemeSelector::toggle);
 39    cx.add_action(ThemeSelector::reload);
 40
 41    cx.add_bindings(vec![
 42        Binding::new("cmd-k cmd-t", Toggle(app_state.clone()), None),
 43        Binding::new("cmd-k t", Reload(app_state.clone()), None),
 44        Binding::new("escape", Toggle(app_state.clone()), Some("ThemeSelector")),
 45        Binding::new("enter", Confirm, Some("ThemeSelector")),
 46    ]);
 47}
 48
 49pub enum Event {
 50    Dismissed,
 51}
 52
 53impl ThemeSelector {
 54    fn new(
 55        settings_tx: Arc<Mutex<watch::Sender<Settings>>>,
 56        settings: watch::Receiver<Settings>,
 57        registry: Arc<ThemeRegistry>,
 58        cx: &mut ViewContext<Self>,
 59    ) -> Self {
 60        let query_editor = cx.add_view(|cx| {
 61            Editor::single_line(settings.clone(), cx).with_style({
 62                let settings = settings.clone();
 63                move |_| settings.borrow().theme.selector.input_editor.as_editor()
 64            })
 65        });
 66
 67        cx.subscribe(&query_editor, Self::on_query_editor_event)
 68            .detach();
 69
 70        let mut this = Self {
 71            settings,
 72            settings_tx,
 73            registry,
 74            query_editor,
 75            matches: Vec::new(),
 76            list_state: Default::default(),
 77            selected_index: 0,
 78        };
 79        this.update_matches(cx);
 80        this
 81    }
 82
 83    fn toggle(workspace: &mut Workspace, action: &Toggle, cx: &mut ViewContext<Workspace>) {
 84        workspace.toggle_modal(cx, |cx, _| {
 85            let selector = cx.add_view(|cx| {
 86                Self::new(
 87                    action.0.settings_tx.clone(),
 88                    action.0.settings.clone(),
 89                    action.0.themes.clone(),
 90                    cx,
 91                )
 92            });
 93            cx.subscribe(&selector, Self::on_event).detach();
 94            selector
 95        });
 96    }
 97
 98    fn reload(_: &mut Workspace, action: &Reload, cx: &mut ViewContext<Workspace>) {
 99        let current_theme_name = action.0.settings.borrow().theme.name.clone();
100        action.0.themes.clear();
101        match action.0.themes.get(&current_theme_name) {
102            Ok(theme) => {
103                cx.refresh_windows();
104                action.0.settings_tx.lock().borrow_mut().theme = theme;
105                log::info!("reloaded theme {}", current_theme_name);
106            }
107            Err(error) => {
108                log::error!("failed to load theme {}: {:?}", current_theme_name, error)
109            }
110        }
111    }
112
113    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
114        if let Some(mat) = self.matches.get(self.selected_index) {
115            match self.registry.get(&mat.string) {
116                Ok(theme) => {
117                    self.settings_tx.lock().borrow_mut().theme = theme;
118                    cx.refresh_windows();
119                    cx.emit(Event::Dismissed);
120                }
121                Err(error) => log::error!("error loading theme {}: {}", mat.string, error),
122            }
123        }
124    }
125
126    fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
127        if self.selected_index > 0 {
128            self.selected_index -= 1;
129        }
130        self.list_state.scroll_to(self.selected_index);
131        cx.notify();
132    }
133
134    fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
135        if self.selected_index + 1 < self.matches.len() {
136            self.selected_index += 1;
137        }
138        self.list_state.scroll_to(self.selected_index);
139        cx.notify();
140    }
141
142    // fn select(&mut self, selected_index: &usize, cx: &mut ViewContext<Self>) {
143    //     self.selected_index = *selected_index;
144    //     self.confirm(&(), cx);
145    // }
146
147    fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
148        let background = cx.background().clone();
149        let candidates = self
150            .registry
151            .list()
152            .map(|name| StringMatchCandidate {
153                char_bag: name.as_str().into(),
154                string: name,
155            })
156            .collect::<Vec<_>>();
157        let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
158
159        self.matches = if query.is_empty() {
160            candidates
161                .into_iter()
162                .map(|candidate| StringMatch {
163                    string: candidate.string,
164                    positions: Vec::new(),
165                    score: 0.0,
166                })
167                .collect()
168        } else {
169            smol::block_on(match_strings(
170                &candidates,
171                &query,
172                false,
173                100,
174                &Default::default(),
175                background,
176            ))
177        };
178        cx.notify();
179    }
180
181    fn on_event(
182        workspace: &mut Workspace,
183        _: ViewHandle<ThemeSelector>,
184        event: &Event,
185        cx: &mut ViewContext<Workspace>,
186    ) {
187        match event {
188            Event::Dismissed => {
189                workspace.dismiss_modal(cx);
190            }
191        }
192    }
193
194    fn on_query_editor_event(
195        &mut self,
196        _: ViewHandle<Editor>,
197        event: &editor::Event,
198        cx: &mut ViewContext<Self>,
199    ) {
200        match event {
201            editor::Event::Edited => self.update_matches(cx),
202            editor::Event::Blurred => cx.emit(Event::Dismissed),
203            _ => {}
204        }
205    }
206
207    fn render_matches(&self, cx: &mut RenderContext<Self>) -> ElementBox {
208        if self.matches.is_empty() {
209            let settings = self.settings.borrow();
210            return Container::new(
211                Label::new("No matches".into(), settings.theme.selector.label.clone()).boxed(),
212            )
213            .with_margin_top(6.0)
214            .named("empty matches");
215        }
216
217        let handle = cx.handle();
218        let list = UniformList::new(
219            self.list_state.clone(),
220            self.matches.len(),
221            move |mut range, items, cx| {
222                let cx = cx.as_ref();
223                let selector = handle.upgrade(cx).unwrap();
224                let selector = selector.read(cx);
225                let start = range.start;
226                range.end = cmp::min(range.end, selector.matches.len());
227                items.extend(
228                    selector.matches[range]
229                        .iter()
230                        .enumerate()
231                        .map(move |(i, path_match)| selector.render_match(path_match, start + i)),
232                );
233            },
234        );
235
236        Container::new(list.boxed())
237            .with_margin_top(6.0)
238            .named("matches")
239    }
240
241    fn render_match(&self, theme_match: &StringMatch, index: usize) -> ElementBox {
242        let settings = self.settings.borrow();
243        let theme = &settings.theme;
244
245        let container = Container::new(
246            Label::new(
247                theme_match.string.clone(),
248                if index == self.selected_index {
249                    theme.selector.active_item.label.clone()
250                } else {
251                    theme.selector.item.label.clone()
252                },
253            )
254            .with_highlights(theme_match.positions.clone())
255            .boxed(),
256        )
257        .with_style(if index == self.selected_index {
258            &theme.selector.active_item.container
259        } else {
260            &theme.selector.item.container
261        });
262
263        container.boxed()
264    }
265}
266
267impl Entity for ThemeSelector {
268    type Event = Event;
269}
270
271impl View for ThemeSelector {
272    fn ui_name() -> &'static str {
273        "ThemeSelector"
274    }
275
276    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
277        let settings = self.settings.borrow();
278
279        Align::new(
280            ConstrainedBox::new(
281                Container::new(
282                    Flex::new(Axis::Vertical)
283                        .with_child(ChildView::new(self.query_editor.id()).boxed())
284                        .with_child(Flexible::new(1.0, self.render_matches(cx)).boxed())
285                        .boxed(),
286                )
287                .with_style(&settings.theme.selector.container)
288                .boxed(),
289            )
290            .with_max_width(600.0)
291            .with_max_height(400.0)
292            .boxed(),
293        )
294        .top()
295        .named("theme selector")
296    }
297
298    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
299        cx.focus(&self.query_editor);
300    }
301
302    fn keymap_context(&self, _: &AppContext) -> keymap::Context {
303        let mut cx = Self::default_keymap_context();
304        cx.set.insert("menu".into());
305        cx
306    }
307}