theme_selector.rs

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