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