theme_selector.rs

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