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