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