theme_selector.rs

  1mod icon_theme_selector;
  2
  3use fs::Fs;
  4use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
  5use gpui::{
  6    App, Context, DismissEvent, Entity, EventEmitter, Focusable, Render, UpdateGlobal, WeakEntity,
  7    Window, actions,
  8};
  9use picker::{Picker, PickerDelegate};
 10use settings::{SettingsStore, update_settings_file};
 11use std::sync::Arc;
 12use theme::{Appearance, Theme, ThemeMeta, ThemeRegistry, ThemeSettings};
 13use ui::{ListItem, ListItemSpacing, prelude::*, v_flex};
 14use util::ResultExt;
 15use workspace::{ModalView, Workspace, ui::HighlightedLabel, with_active_or_new_workspace};
 16use zed_actions::{ExtensionCategoryFilter, Extensions};
 17
 18use crate::icon_theme_selector::{IconThemeSelector, IconThemeSelectorDelegate};
 19
 20actions!(
 21    theme_selector,
 22    [
 23        /// Reloads all themes from disk.
 24        Reload
 25    ]
 26);
 27
 28pub fn init(cx: &mut App) {
 29    cx.on_action(|action: &zed_actions::theme_selector::Toggle, cx| {
 30        let action = action.clone();
 31        with_active_or_new_workspace(cx, move |workspace, window, cx| {
 32            toggle_theme_selector(workspace, &action, window, cx);
 33        });
 34    });
 35    cx.on_action(|action: &zed_actions::icon_theme_selector::Toggle, cx| {
 36        let action = action.clone();
 37        with_active_or_new_workspace(cx, move |workspace, window, cx| {
 38            toggle_icon_theme_selector(workspace, &action, window, cx);
 39        });
 40    });
 41}
 42
 43fn toggle_theme_selector(
 44    workspace: &mut Workspace,
 45    toggle: &zed_actions::theme_selector::Toggle,
 46    window: &mut Window,
 47    cx: &mut Context<Workspace>,
 48) {
 49    let fs = workspace.app_state().fs.clone();
 50    workspace.toggle_modal(window, cx, |window, cx| {
 51        let delegate = ThemeSelectorDelegate::new(
 52            cx.entity().downgrade(),
 53            fs,
 54            toggle.themes_filter.as_ref(),
 55            cx,
 56        );
 57        ThemeSelector::new(delegate, window, cx)
 58    });
 59}
 60
 61fn toggle_icon_theme_selector(
 62    workspace: &mut Workspace,
 63    toggle: &zed_actions::icon_theme_selector::Toggle,
 64    window: &mut Window,
 65    cx: &mut Context<Workspace>,
 66) {
 67    let fs = workspace.app_state().fs.clone();
 68    workspace.toggle_modal(window, cx, |window, cx| {
 69        let delegate = IconThemeSelectorDelegate::new(
 70            cx.entity().downgrade(),
 71            fs,
 72            toggle.themes_filter.as_ref(),
 73            cx,
 74        );
 75        IconThemeSelector::new(delegate, window, cx)
 76    });
 77}
 78
 79impl ModalView for ThemeSelector {}
 80
 81struct ThemeSelector {
 82    picker: Entity<Picker<ThemeSelectorDelegate>>,
 83}
 84
 85impl EventEmitter<DismissEvent> for ThemeSelector {}
 86
 87impl Focusable for ThemeSelector {
 88    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
 89        self.picker.focus_handle(cx)
 90    }
 91}
 92
 93impl Render for ThemeSelector {
 94    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
 95        v_flex().w(rems(34.)).child(self.picker.clone())
 96    }
 97}
 98
 99impl ThemeSelector {
100    pub fn new(
101        delegate: ThemeSelectorDelegate,
102        window: &mut Window,
103        cx: &mut Context<Self>,
104    ) -> Self {
105        let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
106        Self { picker }
107    }
108}
109
110struct ThemeSelectorDelegate {
111    fs: Arc<dyn Fs>,
112    themes: Vec<ThemeMeta>,
113    matches: Vec<StringMatch>,
114    original_theme: Arc<Theme>,
115    selection_completed: bool,
116    selected_theme: Option<Arc<Theme>>,
117    selected_index: usize,
118    selector: WeakEntity<ThemeSelector>,
119}
120
121impl ThemeSelectorDelegate {
122    fn new(
123        selector: WeakEntity<ThemeSelector>,
124        fs: Arc<dyn Fs>,
125        themes_filter: Option<&Vec<String>>,
126        cx: &mut Context<ThemeSelector>,
127    ) -> Self {
128        let original_theme = cx.theme().clone();
129
130        let registry = ThemeRegistry::global(cx);
131        let mut themes = registry
132            .list()
133            .into_iter()
134            .filter(|meta| {
135                if let Some(theme_filter) = themes_filter {
136                    theme_filter.contains(&meta.name.to_string())
137                } else {
138                    true
139                }
140            })
141            .collect::<Vec<_>>();
142
143        themes.sort_unstable_by(|a, b| {
144            a.appearance
145                .is_light()
146                .cmp(&b.appearance.is_light())
147                .then(a.name.cmp(&b.name))
148        });
149        let matches = themes
150            .iter()
151            .map(|meta| StringMatch {
152                candidate_id: 0,
153                score: 0.0,
154                positions: Default::default(),
155                string: meta.name.to_string(),
156            })
157            .collect();
158        let mut this = Self {
159            fs,
160            themes,
161            matches,
162            original_theme: original_theme.clone(),
163            selected_index: 0,
164            selection_completed: false,
165            selected_theme: None,
166            selector,
167        };
168
169        this.select_if_matching(&original_theme.name);
170        this
171    }
172
173    fn show_selected_theme(
174        &mut self,
175        cx: &mut Context<Picker<ThemeSelectorDelegate>>,
176    ) -> Option<Arc<Theme>> {
177        if let Some(mat) = self.matches.get(self.selected_index) {
178            let registry = ThemeRegistry::global(cx);
179            match registry.get(&mat.string) {
180                Ok(theme) => {
181                    Self::set_theme(theme.clone(), cx);
182                    Some(theme)
183                }
184                Err(error) => {
185                    log::error!("error loading theme {}: {}", mat.string, error);
186                    None
187                }
188            }
189        } else {
190            None
191        }
192    }
193
194    fn select_if_matching(&mut self, theme_name: &str) {
195        self.selected_index = self
196            .matches
197            .iter()
198            .position(|mat| mat.string == theme_name)
199            .unwrap_or(self.selected_index);
200    }
201
202    fn set_theme(theme: Arc<Theme>, cx: &mut App) {
203        SettingsStore::update_global(cx, |store, cx| {
204            let mut theme_settings = store.get::<ThemeSettings>(None).clone();
205            theme_settings.active_theme = theme;
206            theme_settings.apply_theme_overrides();
207            store.override_global(theme_settings);
208            cx.refresh_windows();
209        });
210    }
211}
212
213impl PickerDelegate for ThemeSelectorDelegate {
214    type ListItem = ui::ListItem;
215
216    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
217        "Select Theme...".into()
218    }
219
220    fn match_count(&self) -> usize {
221        self.matches.len()
222    }
223
224    fn confirm(
225        &mut self,
226        _: bool,
227        window: &mut Window,
228        cx: &mut Context<Picker<ThemeSelectorDelegate>>,
229    ) {
230        self.selection_completed = true;
231
232        let theme_name = cx.theme().name.clone();
233
234        telemetry::event!("Settings Changed", setting = "theme", value = theme_name);
235
236        let appearance = Appearance::from(window.appearance());
237
238        update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings, _| {
239            settings.set_theme(theme_name.to_string(), appearance);
240        });
241
242        self.selector
243            .update(cx, |_, cx| {
244                cx.emit(DismissEvent);
245            })
246            .ok();
247    }
248
249    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<ThemeSelectorDelegate>>) {
250        if !self.selection_completed {
251            Self::set_theme(self.original_theme.clone(), cx);
252            self.selection_completed = true;
253        }
254
255        self.selector
256            .update(cx, |_, cx| cx.emit(DismissEvent))
257            .log_err();
258    }
259
260    fn selected_index(&self) -> usize {
261        self.selected_index
262    }
263
264    fn set_selected_index(
265        &mut self,
266        ix: usize,
267        _: &mut Window,
268        cx: &mut Context<Picker<ThemeSelectorDelegate>>,
269    ) {
270        self.selected_index = ix;
271        self.selected_theme = self.show_selected_theme(cx);
272    }
273
274    fn update_matches(
275        &mut self,
276        query: String,
277        window: &mut Window,
278        cx: &mut Context<Picker<ThemeSelectorDelegate>>,
279    ) -> gpui::Task<()> {
280        let background = cx.background_executor().clone();
281        let candidates = self
282            .themes
283            .iter()
284            .enumerate()
285            .map(|(id, meta)| StringMatchCandidate::new(id, &meta.name))
286            .collect::<Vec<_>>();
287
288        cx.spawn_in(window, async move |this, cx| {
289            let matches = if query.is_empty() {
290                candidates
291                    .into_iter()
292                    .enumerate()
293                    .map(|(index, candidate)| StringMatch {
294                        candidate_id: index,
295                        string: candidate.string,
296                        positions: Vec::new(),
297                        score: 0.0,
298                    })
299                    .collect()
300            } else {
301                match_strings(
302                    &candidates,
303                    &query,
304                    false,
305                    true,
306                    100,
307                    &Default::default(),
308                    background,
309                )
310                .await
311            };
312
313            this.update(cx, |this, cx| {
314                this.delegate.matches = matches;
315                if query.is_empty() && this.delegate.selected_theme.is_none() {
316                    this.delegate.selected_index = this
317                        .delegate
318                        .selected_index
319                        .min(this.delegate.matches.len().saturating_sub(1));
320                } else if let Some(selected) = this.delegate.selected_theme.as_ref() {
321                    this.delegate.selected_index = this
322                        .delegate
323                        .matches
324                        .iter()
325                        .enumerate()
326                        .find(|(_, mtch)| mtch.string == selected.name)
327                        .map(|(ix, _)| ix)
328                        .unwrap_or_default();
329                } else {
330                    this.delegate.selected_index = 0;
331                }
332                this.delegate.selected_theme = this.delegate.show_selected_theme(cx);
333            })
334            .log_err();
335        })
336    }
337
338    fn render_match(
339        &self,
340        ix: usize,
341        selected: bool,
342        _window: &mut Window,
343        _cx: &mut Context<Picker<Self>>,
344    ) -> Option<Self::ListItem> {
345        let theme_match = &self.matches[ix];
346
347        Some(
348            ListItem::new(ix)
349                .inset(true)
350                .spacing(ListItemSpacing::Sparse)
351                .toggle_state(selected)
352                .child(HighlightedLabel::new(
353                    theme_match.string.clone(),
354                    theme_match.positions.clone(),
355                )),
356        )
357    }
358
359    fn render_footer(
360        &self,
361        _: &mut Window,
362        cx: &mut Context<Picker<Self>>,
363    ) -> Option<gpui::AnyElement> {
364        Some(
365            h_flex()
366                .p_2()
367                .w_full()
368                .justify_between()
369                .gap_2()
370                .border_t_1()
371                .border_color(cx.theme().colors().border_variant)
372                .child(
373                    Button::new("docs", "View Theme Docs")
374                        .icon(IconName::ArrowUpRight)
375                        .icon_position(IconPosition::End)
376                        .icon_size(IconSize::XSmall)
377                        .icon_color(Color::Muted)
378                        .on_click(cx.listener(|_, _, _, cx| {
379                            cx.open_url("https://zed.dev/docs/themes");
380                        })),
381                )
382                .child(
383                    Button::new("more-themes", "Install Themes").on_click(cx.listener({
384                        move |_, _, window, cx| {
385                            window.dispatch_action(
386                                Box::new(Extensions {
387                                    category_filter: Some(ExtensionCategoryFilter::Themes),
388                                    id: None,
389                                }),
390                                cx,
391                            );
392                        }
393                    })),
394                )
395                .into_any_element(),
396        )
397    }
398}