theme_selector.rs

  1use feature_flags::FeatureFlagAppExt;
  2use fs::Fs;
  3use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
  4use gpui::{
  5    actions, AppContext, DismissEvent, Div, EventEmitter, FocusableView, Render, View, ViewContext,
  6    VisualContext, WeakView,
  7};
  8use picker::{Picker, PickerDelegate};
  9use settings::{update_settings_file, SettingsStore};
 10use std::sync::Arc;
 11use theme::{Theme, ThemeMeta, ThemeRegistry, ThemeSettings};
 12use ui::{prelude::*, v_stack, ListItem};
 13use util::ResultExt;
 14use workspace::{ui::HighlightedLabel, ModalView, Workspace};
 15
 16actions!(theme_selector, [Toggle, Reload]);
 17
 18pub fn init(cx: &mut AppContext) {
 19    cx.observe_new_views(
 20        |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
 21            workspace.register_action(toggle);
 22        },
 23    )
 24    .detach();
 25}
 26
 27pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
 28    let fs = workspace.app_state().fs.clone();
 29    workspace.toggle_modal(cx, |cx| {
 30        ThemeSelector::new(
 31            ThemeSelectorDelegate::new(cx.view().downgrade(), fs, cx),
 32            cx,
 33        )
 34    });
 35}
 36
 37#[cfg(debug_assertions)]
 38pub fn reload(cx: &mut AppContext) {
 39    let current_theme_name = cx.theme().name.clone();
 40    let current_theme = cx.update_global(|registry: &mut ThemeRegistry, _cx| {
 41        registry.clear();
 42        registry.get(&current_theme_name)
 43    });
 44    match current_theme {
 45        Ok(theme) => {
 46            ThemeSelectorDelegate::set_theme(theme, cx);
 47            log::info!("reloaded theme {}", current_theme_name);
 48        }
 49        Err(error) => {
 50            log::error!("failed to load theme {}: {:?}", current_theme_name, error)
 51        }
 52    }
 53}
 54
 55impl ModalView for ThemeSelector {}
 56
 57pub struct ThemeSelector {
 58    picker: View<Picker<ThemeSelectorDelegate>>,
 59}
 60
 61impl EventEmitter<DismissEvent> for ThemeSelector {}
 62
 63impl FocusableView for ThemeSelector {
 64    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
 65        self.picker.focus_handle(cx)
 66    }
 67}
 68
 69impl Render for ThemeSelector {
 70    type Element = Div;
 71
 72    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
 73        v_stack().w(rems(34.)).child(self.picker.clone())
 74    }
 75}
 76
 77impl ThemeSelector {
 78    pub fn new(delegate: ThemeSelectorDelegate, cx: &mut ViewContext<Self>) -> Self {
 79        let picker = cx.build_view(|cx| Picker::new(delegate, cx));
 80        Self { picker }
 81    }
 82}
 83
 84pub struct ThemeSelectorDelegate {
 85    fs: Arc<dyn Fs>,
 86    themes: Vec<ThemeMeta>,
 87    matches: Vec<StringMatch>,
 88    original_theme: Arc<Theme>,
 89    selection_completed: bool,
 90    selected_index: usize,
 91    view: WeakView<ThemeSelector>,
 92}
 93
 94impl ThemeSelectorDelegate {
 95    fn new(
 96        weak_view: WeakView<ThemeSelector>,
 97        fs: Arc<dyn Fs>,
 98        cx: &mut ViewContext<ThemeSelector>,
 99    ) -> Self {
100        let original_theme = cx.theme().clone();
101
102        let staff_mode = cx.is_staff();
103        let registry = cx.global::<ThemeRegistry>();
104        let mut themes = registry.list(staff_mode).collect::<Vec<_>>();
105        themes.sort_unstable_by(|a, b| {
106            a.appearance
107                .is_light()
108                .cmp(&b.appearance.is_light())
109                .then(a.name.cmp(&b.name))
110        });
111        let matches = themes
112            .iter()
113            .map(|meta| StringMatch {
114                candidate_id: 0,
115                score: 0.0,
116                positions: Default::default(),
117                string: meta.name.to_string(),
118            })
119            .collect();
120        let mut this = Self {
121            fs,
122            themes,
123            matches,
124            original_theme: original_theme.clone(),
125            selected_index: 0,
126            selection_completed: false,
127            view: weak_view,
128        };
129        this.select_if_matching(&original_theme.name);
130        this
131    }
132
133    fn show_selected_theme(&mut self, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
134        if let Some(mat) = self.matches.get(self.selected_index) {
135            let registry = cx.global::<ThemeRegistry>();
136            match registry.get(&mat.string) {
137                Ok(theme) => {
138                    Self::set_theme(theme, cx);
139                }
140                Err(error) => {
141                    log::error!("error loading theme {}: {}", mat.string, error)
142                }
143            }
144        }
145    }
146
147    fn select_if_matching(&mut self, theme_name: &str) {
148        self.selected_index = self
149            .matches
150            .iter()
151            .position(|mat| mat.string == theme_name)
152            .unwrap_or(self.selected_index);
153    }
154
155    fn set_theme(theme: Arc<Theme>, cx: &mut AppContext) {
156        cx.update_global(|store: &mut SettingsStore, cx| {
157            let mut theme_settings = store.get::<ThemeSettings>(None).clone();
158            theme_settings.active_theme = theme;
159            store.override_global(theme_settings);
160            cx.refresh();
161        });
162    }
163}
164
165impl PickerDelegate for ThemeSelectorDelegate {
166    type ListItem = ui::ListItem;
167
168    fn placeholder_text(&self) -> Arc<str> {
169        "Select Theme...".into()
170    }
171
172    fn match_count(&self) -> usize {
173        self.matches.len()
174    }
175
176    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
177        self.selection_completed = true;
178
179        let theme_name = cx.theme().name.clone();
180        update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings| {
181            settings.theme = Some(theme_name.to_string());
182        });
183
184        self.view
185            .update(cx, |_, cx| {
186                cx.emit(DismissEvent);
187            })
188            .ok();
189    }
190
191    fn dismissed(&mut self, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
192        if !self.selection_completed {
193            Self::set_theme(self.original_theme.clone(), cx);
194            self.selection_completed = true;
195        }
196
197        self.view
198            .update(cx, |_, cx| cx.emit(DismissEvent))
199            .log_err();
200    }
201
202    fn selected_index(&self) -> usize {
203        self.selected_index
204    }
205
206    fn set_selected_index(
207        &mut self,
208        ix: usize,
209        cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>,
210    ) {
211        self.selected_index = ix;
212        self.show_selected_theme(cx);
213    }
214
215    fn update_matches(
216        &mut self,
217        query: String,
218        cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>,
219    ) -> gpui::Task<()> {
220        let background = cx.background_executor().clone();
221        let candidates = self
222            .themes
223            .iter()
224            .enumerate()
225            .map(|(id, meta)| StringMatchCandidate {
226                id,
227                char_bag: meta.name.as_ref().into(),
228                string: meta.name.to_string(),
229            })
230            .collect::<Vec<_>>();
231
232        cx.spawn(|this, mut cx| async move {
233            let matches = if query.is_empty() {
234                candidates
235                    .into_iter()
236                    .enumerate()
237                    .map(|(index, candidate)| StringMatch {
238                        candidate_id: index,
239                        string: candidate.string,
240                        positions: Vec::new(),
241                        score: 0.0,
242                    })
243                    .collect()
244            } else {
245                match_strings(
246                    &candidates,
247                    &query,
248                    false,
249                    100,
250                    &Default::default(),
251                    background,
252                )
253                .await
254            };
255
256            this.update(&mut cx, |this, cx| {
257                this.delegate.matches = matches;
258                this.delegate.selected_index = this
259                    .delegate
260                    .selected_index
261                    .min(this.delegate.matches.len().saturating_sub(1));
262                this.delegate.show_selected_theme(cx);
263            })
264            .log_err();
265        })
266    }
267
268    fn render_match(
269        &self,
270        ix: usize,
271        selected: bool,
272        _cx: &mut ViewContext<Picker<Self>>,
273    ) -> Option<Self::ListItem> {
274        let theme_match = &self.matches[ix];
275
276        Some(
277            ListItem::new(ix)
278                .inset(true)
279                .selected(selected)
280                .child(HighlightedLabel::new(
281                    theme_match.string.clone(),
282                    theme_match.positions.clone(),
283                )),
284        )
285    }
286}