theme_selector.rs

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