theme_selector.rs

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