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