icon_theme_selector.rs

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