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