theme_selector.rs

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