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