theme_selector.rs

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