theme_selector.rs

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