theme_selector.rs

  1use feature_flags::FeatureFlagAppExt;
  2use fs::Fs;
  3use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
  4use gpui::{
  5    actions, AppContext, DismissEvent, Div, EventEmitter, FocusableView, Render, SharedString,
  6    View, ViewContext, VisualContext, WeakView,
  7};
  8use picker::{Picker, PickerDelegate};
  9use settings::{update_settings_file, SettingsStore};
 10use std::sync::Arc;
 11use theme::{Theme, ThemeRegistry, ThemeSettings};
 12use ui::{prelude::*, v_stack, ListItem};
 13use util::ResultExt;
 14use workspace::{ui::HighlightedLabel, ModalView, Workspace};
 15
 16actions!(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    type Element = Div;
 71
 72    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
 73        v_stack().w(rems(34.)).child(self.picker.clone())
 74    }
 75}
 76
 77impl ThemeSelector {
 78    pub fn new(delegate: ThemeSelectorDelegate, cx: &mut ViewContext<Self>) -> Self {
 79        let picker = cx.build_view(|cx| Picker::new(delegate, cx));
 80        Self { picker }
 81    }
 82}
 83
 84pub struct ThemeSelectorDelegate {
 85    fs: Arc<dyn Fs>,
 86    theme_names: Vec<SharedString>,
 87    matches: Vec<StringMatch>,
 88    original_theme: Arc<Theme>,
 89    selection_completed: bool,
 90    selected_index: usize,
 91    view: WeakView<ThemeSelector>,
 92}
 93
 94impl ThemeSelectorDelegate {
 95    fn new(
 96        weak_view: WeakView<ThemeSelector>,
 97        fs: Arc<dyn Fs>,
 98        cx: &mut ViewContext<ThemeSelector>,
 99    ) -> Self {
100        let original_theme = cx.theme().clone();
101
102        let staff_mode = cx.is_staff();
103        let registry = cx.global::<ThemeRegistry>();
104        let theme_names = registry.list(staff_mode).collect::<Vec<_>>();
105        //todo!(theme sorting)
106        // theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name)));
107        let matches = theme_names
108            .iter()
109            .map(|meta| StringMatch {
110                candidate_id: 0,
111                score: 0.0,
112                positions: Default::default(),
113                string: meta.to_string(),
114            })
115            .collect();
116        let mut this = Self {
117            fs,
118            theme_names,
119            matches,
120            original_theme: original_theme.clone(),
121            selected_index: 0,
122            selection_completed: false,
123            view: weak_view,
124        };
125        this.select_if_matching(&original_theme.name);
126        this
127    }
128
129    fn show_selected_theme(&mut self, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
130        if let Some(mat) = self.matches.get(self.selected_index) {
131            let registry = cx.global::<ThemeRegistry>();
132            match registry.get(&mat.string) {
133                Ok(theme) => {
134                    Self::set_theme(theme, cx);
135                }
136                Err(error) => {
137                    log::error!("error loading theme {}: {}", mat.string, error)
138                }
139            }
140        }
141    }
142
143    fn select_if_matching(&mut self, theme_name: &str) {
144        self.selected_index = self
145            .matches
146            .iter()
147            .position(|mat| mat.string == theme_name)
148            .unwrap_or(self.selected_index);
149    }
150
151    fn set_theme(theme: Arc<Theme>, cx: &mut AppContext) {
152        cx.update_global(|store: &mut SettingsStore, cx| {
153            let mut theme_settings = store.get::<ThemeSettings>(None).clone();
154            theme_settings.active_theme = theme;
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) -> 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        update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings| {
177            settings.theme = Some(theme_name.to_string());
178        });
179
180        self.view
181            .update(cx, |_, cx| {
182                cx.emit(DismissEvent);
183            })
184            .ok();
185    }
186
187    fn dismissed(&mut self, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
188        if !self.selection_completed {
189            Self::set_theme(self.original_theme.clone(), cx);
190            self.selection_completed = true;
191        }
192
193        self.view
194            .update(cx, |_, cx| cx.emit(DismissEvent))
195            .log_err();
196    }
197
198    fn selected_index(&self) -> usize {
199        self.selected_index
200    }
201
202    fn set_selected_index(
203        &mut self,
204        ix: usize,
205        cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>,
206    ) {
207        self.selected_index = ix;
208        self.show_selected_theme(cx);
209    }
210
211    fn update_matches(
212        &mut self,
213        query: String,
214        cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>,
215    ) -> gpui::Task<()> {
216        let background = cx.background_executor().clone();
217        let candidates = self
218            .theme_names
219            .iter()
220            .enumerate()
221            .map(|(id, meta)| StringMatchCandidate {
222                id,
223                char_bag: meta.as_ref().into(),
224                string: meta.to_string(),
225            })
226            .collect::<Vec<_>>();
227
228        cx.spawn(|this, mut cx| async move {
229            let matches = if query.is_empty() {
230                candidates
231                    .into_iter()
232                    .enumerate()
233                    .map(|(index, candidate)| StringMatch {
234                        candidate_id: index,
235                        string: candidate.string,
236                        positions: Vec::new(),
237                        score: 0.0,
238                    })
239                    .collect()
240            } else {
241                match_strings(
242                    &candidates,
243                    &query,
244                    false,
245                    100,
246                    &Default::default(),
247                    background,
248                )
249                .await
250            };
251
252            this.update(&mut cx, |this, cx| {
253                this.delegate.matches = matches;
254                this.delegate.selected_index = this
255                    .delegate
256                    .selected_index
257                    .min(this.delegate.matches.len().saturating_sub(1));
258                this.delegate.show_selected_theme(cx);
259            })
260            .log_err();
261        })
262    }
263
264    fn render_match(
265        &self,
266        ix: usize,
267        selected: bool,
268        _cx: &mut ViewContext<Picker<Self>>,
269    ) -> Option<Self::ListItem> {
270        let theme_match = &self.matches[ix];
271
272        Some(
273            ListItem::new(ix)
274                .inset(true)
275                .selected(selected)
276                .child(HighlightedLabel::new(
277                    theme_match.string.clone(),
278                    theme_match.positions.clone(),
279                )),
280        )
281    }
282}