theme_selector.rs

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