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, 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
 55pub struct ThemeSelector {
 56    picker: View<Picker<ThemeSelectorDelegate>>,
 57}
 58
 59impl EventEmitter<DismissEvent> for ThemeSelector {}
 60
 61impl FocusableView for ThemeSelector {
 62    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
 63        self.picker.focus_handle(cx)
 64    }
 65}
 66
 67impl Render for ThemeSelector {
 68    type Element = Div;
 69
 70    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::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.build_view(|cx| Picker::new(delegate, cx));
 78        Self { picker }
 79    }
 80}
 81
 82pub struct ThemeSelectorDelegate {
 83    fs: Arc<dyn Fs>,
 84    theme_names: Vec<SharedString>,
 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 theme_names = registry.list(staff_mode).collect::<Vec<_>>();
103        //todo!(theme sorting)
104        // theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name)));
105        let matches = theme_names
106            .iter()
107            .map(|meta| StringMatch {
108                candidate_id: 0,
109                score: 0.0,
110                positions: Default::default(),
111                string: meta.to_string(),
112            })
113            .collect();
114        let mut this = Self {
115            fs,
116            theme_names,
117            matches,
118            original_theme: original_theme.clone(),
119            selected_index: 0,
120            selection_completed: false,
121            view: weak_view,
122        };
123        this.select_if_matching(&original_theme.name);
124        this
125    }
126
127    fn show_selected_theme(&mut self, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
128        if let Some(mat) = self.matches.get(self.selected_index) {
129            let registry = cx.global::<ThemeRegistry>();
130            match registry.get(&mat.string) {
131                Ok(theme) => {
132                    Self::set_theme(theme, cx);
133                }
134                Err(error) => {
135                    log::error!("error loading theme {}: {}", mat.string, error)
136                }
137            }
138        }
139    }
140
141    fn select_if_matching(&mut self, theme_name: &str) {
142        self.selected_index = self
143            .matches
144            .iter()
145            .position(|mat| mat.string == theme_name)
146            .unwrap_or(self.selected_index);
147    }
148
149    fn set_theme(theme: Arc<Theme>, cx: &mut AppContext) {
150        cx.update_global(|store: &mut SettingsStore, cx| {
151            let mut theme_settings = store.get::<ThemeSettings>(None).clone();
152            theme_settings.active_theme = theme;
153            store.override_global(theme_settings);
154            cx.refresh();
155        });
156    }
157}
158
159impl PickerDelegate for ThemeSelectorDelegate {
160    type ListItem = ui::ListItem;
161
162    fn placeholder_text(&self) -> Arc<str> {
163        "Select Theme...".into()
164    }
165
166    fn match_count(&self) -> usize {
167        self.matches.len()
168    }
169
170    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
171        self.selection_completed = true;
172
173        let theme_name = cx.theme().name.clone();
174        update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings| {
175            settings.theme = Some(theme_name.to_string());
176        });
177
178        self.view
179            .update(cx, |_, cx| {
180                cx.emit(DismissEvent);
181            })
182            .ok();
183    }
184
185    fn dismissed(&mut self, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
186        if !self.selection_completed {
187            Self::set_theme(self.original_theme.clone(), cx);
188            self.selection_completed = true;
189        }
190
191        self.view
192            .update(cx, |_, cx| cx.emit(DismissEvent))
193            .log_err();
194    }
195
196    fn selected_index(&self) -> usize {
197        self.selected_index
198    }
199
200    fn set_selected_index(
201        &mut self,
202        ix: usize,
203        cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>,
204    ) {
205        self.selected_index = ix;
206        self.show_selected_theme(cx);
207    }
208
209    fn update_matches(
210        &mut self,
211        query: String,
212        cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>,
213    ) -> gpui::Task<()> {
214        let background = cx.background_executor().clone();
215        let candidates = self
216            .theme_names
217            .iter()
218            .enumerate()
219            .map(|(id, meta)| StringMatchCandidate {
220                id,
221                char_bag: meta.as_ref().into(),
222                string: meta.to_string(),
223            })
224            .collect::<Vec<_>>();
225
226        cx.spawn(|this, mut cx| async move {
227            let matches = if query.is_empty() {
228                candidates
229                    .into_iter()
230                    .enumerate()
231                    .map(|(index, candidate)| StringMatch {
232                        candidate_id: index,
233                        string: candidate.string,
234                        positions: Vec::new(),
235                        score: 0.0,
236                    })
237                    .collect()
238            } else {
239                match_strings(
240                    &candidates,
241                    &query,
242                    false,
243                    100,
244                    &Default::default(),
245                    background,
246                )
247                .await
248            };
249
250            this.update(&mut cx, |this, cx| {
251                this.delegate.matches = matches;
252                this.delegate.selected_index = this
253                    .delegate
254                    .selected_index
255                    .min(this.delegate.matches.len().saturating_sub(1));
256                this.delegate.show_selected_theme(cx);
257            })
258            .log_err();
259        })
260    }
261
262    fn render_match(
263        &self,
264        ix: usize,
265        selected: bool,
266        _cx: &mut ViewContext<Picker<Self>>,
267    ) -> Option<Self::ListItem> {
268        let theme_match = &self.matches[ix];
269
270        Some(
271            ListItem::new(ix)
272                .inset(true)
273                .selected(selected)
274                .child(HighlightedLabel::new(
275                    theme_match.string.clone(),
276                    theme_match.positions.clone(),
277                )),
278        )
279    }
280}