icon_theme_selector.rs

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