theme_selector.rs

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