theme_selector.rs

  1use client::{telemetry::Telemetry, TelemetrySettings};
  2use feature_flags::FeatureFlagAppExt;
  3use fs::Fs;
  4use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
  5use gpui::{actions, elements::*, AnyElement, AppContext, Element, MouseState, ViewContext};
  6use picker::{Picker, PickerDelegate, PickerEvent};
  7use settings::{update_settings_file, SettingsStore};
  8use std::sync::Arc;
  9use theme::{Theme, ThemeMeta, ThemeRegistry, ThemeSettings};
 10use util::ResultExt;
 11use workspace::Workspace;
 12
 13actions!(theme_selector, [Toggle, Reload]);
 14
 15pub fn init(cx: &mut AppContext) {
 16    cx.add_action(toggle);
 17    ThemeSelector::init(cx);
 18}
 19
 20pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
 21    workspace.toggle_modal(cx, |workspace, cx| {
 22        let fs = workspace.app_state().fs.clone();
 23        let telemetry = workspace.client().telemetry().clone();
 24        cx.add_view(|cx| ThemeSelector::new(ThemeSelectorDelegate::new(fs, telemetry, cx), cx))
 25    });
 26}
 27
 28#[cfg(debug_assertions)]
 29pub fn reload(cx: &mut AppContext) {
 30    let current_theme_name = theme::current(cx).meta.name.clone();
 31    let registry = cx.global::<Arc<ThemeRegistry>>();
 32    registry.clear();
 33    match registry.get(&current_theme_name) {
 34        Ok(theme) => {
 35            ThemeSelectorDelegate::set_theme(theme, cx);
 36            log::info!("reloaded theme {}", current_theme_name);
 37        }
 38        Err(error) => {
 39            log::error!("failed to load theme {}: {:?}", current_theme_name, error)
 40        }
 41    }
 42}
 43
 44pub type ThemeSelector = Picker<ThemeSelectorDelegate>;
 45
 46pub struct ThemeSelectorDelegate {
 47    fs: Arc<dyn Fs>,
 48    theme_data: Vec<ThemeMeta>,
 49    matches: Vec<StringMatch>,
 50    original_theme: Arc<Theme>,
 51    selection_completed: bool,
 52    selected_index: usize,
 53    telemetry: Arc<Telemetry>,
 54}
 55
 56impl ThemeSelectorDelegate {
 57    fn new(
 58        fs: Arc<dyn Fs>,
 59        telemetry: Arc<Telemetry>,
 60        cx: &mut ViewContext<ThemeSelector>,
 61    ) -> Self {
 62        let original_theme = theme::current(cx).clone();
 63
 64        let staff_mode = cx.is_staff();
 65        let registry = cx.global::<Arc<ThemeRegistry>>();
 66        let mut theme_names = registry.list(staff_mode).collect::<Vec<_>>();
 67        theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name)));
 68        let matches = theme_names
 69            .iter()
 70            .map(|meta| StringMatch {
 71                candidate_id: 0,
 72                score: 0.0,
 73                positions: Default::default(),
 74                string: meta.name.clone(),
 75            })
 76            .collect();
 77        let mut this = Self {
 78            fs,
 79            theme_data: theme_names,
 80            matches,
 81            original_theme: original_theme.clone(),
 82            selected_index: 0,
 83            selection_completed: false,
 84            telemetry,
 85        };
 86        this.select_if_matching(&original_theme.meta.name);
 87        this
 88    }
 89
 90    fn show_selected_theme(&mut self, cx: &mut ViewContext<ThemeSelector>) {
 91        if let Some(mat) = self.matches.get(self.selected_index) {
 92            let registry = cx.global::<Arc<ThemeRegistry>>();
 93            match registry.get(&mat.string) {
 94                Ok(theme) => {
 95                    Self::set_theme(theme, cx);
 96                }
 97                Err(error) => {
 98                    log::error!("error loading theme {}: {}", mat.string, error)
 99                }
100            }
101        }
102    }
103
104    fn select_if_matching(&mut self, theme_name: &str) {
105        self.selected_index = self
106            .matches
107            .iter()
108            .position(|mat| mat.string == theme_name)
109            .unwrap_or(self.selected_index);
110    }
111
112    fn set_theme(theme: Arc<Theme>, cx: &mut AppContext) {
113        cx.update_global::<SettingsStore, _, _>(|store, cx| {
114            let mut theme_settings = store.get::<ThemeSettings>(None).clone();
115            theme_settings.theme = theme;
116            store.override_global(theme_settings);
117            cx.refresh_windows();
118        });
119    }
120}
121
122impl PickerDelegate for ThemeSelectorDelegate {
123    fn placeholder_text(&self) -> Arc<str> {
124        "Select Theme...".into()
125    }
126
127    fn match_count(&self) -> usize {
128        self.matches.len()
129    }
130
131    fn confirm(&mut self, _: bool, cx: &mut ViewContext<ThemeSelector>) {
132        self.selection_completed = true;
133
134        let theme_name = theme::current(cx).meta.name.clone();
135
136        let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
137        self.telemetry
138            .report_setting_event(telemetry_settings, "theme", theme_name.to_string());
139
140        update_settings_file::<ThemeSettings>(self.fs.clone(), cx, |settings| {
141            settings.theme = Some(theme_name);
142        });
143
144        cx.emit(PickerEvent::Dismiss);
145    }
146
147    fn dismissed(&mut self, cx: &mut ViewContext<ThemeSelector>) {
148        if !self.selection_completed {
149            Self::set_theme(self.original_theme.clone(), cx);
150            self.selection_completed = true;
151        }
152    }
153
154    fn selected_index(&self) -> usize {
155        self.selected_index
156    }
157
158    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<ThemeSelector>) {
159        self.selected_index = ix;
160        self.show_selected_theme(cx);
161    }
162
163    fn update_matches(
164        &mut self,
165        query: String,
166        cx: &mut ViewContext<ThemeSelector>,
167    ) -> gpui::Task<()> {
168        let background = cx.background().clone();
169        let candidates = self
170            .theme_data
171            .iter()
172            .enumerate()
173            .map(|(id, meta)| StringMatchCandidate {
174                id,
175                char_bag: meta.name.as_str().into(),
176                string: meta.name.clone(),
177            })
178            .collect::<Vec<_>>();
179
180        cx.spawn(|this, mut cx| async move {
181            let matches = if query.is_empty() {
182                candidates
183                    .into_iter()
184                    .enumerate()
185                    .map(|(index, candidate)| StringMatch {
186                        candidate_id: index,
187                        string: candidate.string,
188                        positions: Vec::new(),
189                        score: 0.0,
190                    })
191                    .collect()
192            } else {
193                match_strings(
194                    &candidates,
195                    &query,
196                    false,
197                    100,
198                    &Default::default(),
199                    background,
200                )
201                .await
202            };
203
204            this.update(&mut cx, |this, cx| {
205                let delegate = this.delegate_mut();
206                delegate.matches = matches;
207                delegate.selected_index = delegate
208                    .selected_index
209                    .min(delegate.matches.len().saturating_sub(1));
210                delegate.show_selected_theme(cx);
211            })
212            .log_err();
213        })
214    }
215
216    fn render_match(
217        &self,
218        ix: usize,
219        mouse_state: &mut MouseState,
220        selected: bool,
221        cx: &AppContext,
222    ) -> AnyElement<Picker<Self>> {
223        let theme = theme::current(cx);
224        let style = theme.picker.item.in_state(selected).style_for(mouse_state);
225
226        let theme_match = &self.matches[ix];
227        Label::new(theme_match.string.clone(), style.label.clone())
228            .with_highlights(theme_match.positions.clone())
229            .contained()
230            .with_style(style.container)
231            .into_any()
232    }
233}