theme_selector.rs

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