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