theme_selector.rs

  1use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
  2use gpui::{
  3    actions, elements::*, AnyViewHandle, AppContext, Element, ElementBox, Entity, MouseState,
  4    MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
  5};
  6use picker::{Picker, PickerDelegate};
  7use settings::Settings;
  8use std::sync::Arc;
  9use theme::{Theme, ThemeMeta, ThemeRegistry};
 10use workspace::{AppState, Workspace};
 11
 12pub struct ThemeSelector {
 13    registry: Arc<ThemeRegistry>,
 14    theme_data: Vec<ThemeMeta>,
 15    matches: Vec<StringMatch>,
 16    original_theme: Arc<Theme>,
 17    picker: ViewHandle<Picker<Self>>,
 18    selection_completed: bool,
 19    selected_index: usize,
 20}
 21
 22actions!(theme_selector, [Toggle, Reload]);
 23
 24pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
 25    Picker::<ThemeSelector>::init(cx);
 26    cx.add_action({
 27        let theme_registry = app_state.themes.clone();
 28        move |workspace, _: &Toggle, cx| {
 29            ThemeSelector::toggle(workspace, theme_registry.clone(), cx)
 30        }
 31    });
 32}
 33
 34pub enum Event {
 35    Dismissed,
 36}
 37
 38impl ThemeSelector {
 39    fn new(registry: Arc<ThemeRegistry>, cx: &mut ViewContext<Self>) -> Self {
 40        let handle = cx.weak_handle();
 41        let picker = cx.add_view(|cx| Picker::new(handle, cx));
 42        let settings = cx.global::<Settings>();
 43        let original_theme = settings.theme.clone();
 44
 45        let mut theme_names = registry
 46            .list(settings.internal, settings.experiments.experimental_themes)
 47            .collect::<Vec<_>>();
 48        theme_names.sort_unstable_by(|a, b| {
 49            a.is_light
 50                .cmp(&b.is_light)
 51                .reverse()
 52                .then(a.name.cmp(&b.name))
 53        });
 54        let matches = theme_names
 55            .iter()
 56            .map(|meta| StringMatch {
 57                candidate_id: 0,
 58                score: 0.0,
 59                positions: Default::default(),
 60                string: meta.name.clone(),
 61            })
 62            .collect();
 63        let mut this = Self {
 64            registry,
 65            theme_data: theme_names,
 66            matches,
 67            picker,
 68            original_theme: original_theme.clone(),
 69            selected_index: 0,
 70            selection_completed: false,
 71        };
 72        this.select_if_matching(&original_theme.meta.name);
 73        this
 74    }
 75
 76    fn toggle(
 77        workspace: &mut Workspace,
 78        themes: Arc<ThemeRegistry>,
 79        cx: &mut ViewContext<Workspace>,
 80    ) {
 81        workspace.toggle_modal(cx, |_, cx| {
 82            let this = cx.add_view(|cx| Self::new(themes, cx));
 83            cx.subscribe(&this, Self::on_event).detach();
 84            this
 85        });
 86    }
 87
 88    #[cfg(debug_assertions)]
 89    pub fn reload(themes: Arc<ThemeRegistry>, cx: &mut MutableAppContext) {
 90        let current_theme_name = cx.global::<Settings>().theme.meta.name.clone();
 91        themes.clear();
 92        match themes.get(&current_theme_name) {
 93            Ok(theme) => {
 94                Self::set_theme(theme, cx);
 95                log::info!("reloaded theme {}", current_theme_name);
 96            }
 97            Err(error) => {
 98                log::error!("failed to load theme {}: {:?}", current_theme_name, error)
 99            }
100        }
101    }
102
103    fn show_selected_theme(&mut self, cx: &mut ViewContext<Self>) {
104        if let Some(mat) = self.matches.get(self.selected_index) {
105            match self.registry.get(&mat.string) {
106                Ok(theme) => Self::set_theme(theme, cx),
107                Err(error) => {
108                    log::error!("error loading theme {}: {}", mat.string, error)
109                }
110            }
111        }
112    }
113
114    fn select_if_matching(&mut self, theme_name: &str) {
115        self.selected_index = self
116            .matches
117            .iter()
118            .position(|mat| mat.string == theme_name)
119            .unwrap_or(self.selected_index);
120    }
121
122    fn on_event(
123        workspace: &mut Workspace,
124        _: ViewHandle<ThemeSelector>,
125        event: &Event,
126        cx: &mut ViewContext<Workspace>,
127    ) {
128        match event {
129            Event::Dismissed => {
130                workspace.dismiss_modal(cx);
131            }
132        }
133    }
134
135    fn set_theme(theme: Arc<Theme>, cx: &mut MutableAppContext) {
136        cx.update_global::<Settings, _, _>(|settings, cx| {
137            settings.theme = theme;
138            cx.refresh_windows();
139        });
140    }
141}
142
143impl PickerDelegate for ThemeSelector {
144    fn match_count(&self) -> usize {
145        self.matches.len()
146    }
147
148    fn confirm(&mut self, cx: &mut ViewContext<Self>) {
149        self.selection_completed = true;
150        cx.emit(Event::Dismissed);
151    }
152
153    fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
154        if !self.selection_completed {
155            Self::set_theme(self.original_theme.clone(), cx);
156            self.selection_completed = true;
157        }
158        cx.emit(Event::Dismissed);
159    }
160
161    fn selected_index(&self) -> usize {
162        self.selected_index
163    }
164
165    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
166        self.selected_index = ix;
167        self.show_selected_theme(cx);
168    }
169
170    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> gpui::Task<()> {
171        let background = cx.background().clone();
172        let candidates = self
173            .theme_data
174            .iter()
175            .enumerate()
176            .map(|(id, meta)| StringMatchCandidate {
177                id,
178                char_bag: meta.name.as_str().into(),
179                string: meta.name.clone(),
180            })
181            .collect::<Vec<_>>();
182
183        cx.spawn(|this, mut cx| async move {
184            let matches = if query.is_empty() {
185                candidates
186                    .into_iter()
187                    .enumerate()
188                    .map(|(index, candidate)| StringMatch {
189                        candidate_id: index,
190                        string: candidate.string,
191                        positions: Vec::new(),
192                        score: 0.0,
193                    })
194                    .collect()
195            } else {
196                match_strings(
197                    &candidates,
198                    &query,
199                    false,
200                    100,
201                    &Default::default(),
202                    background,
203                )
204                .await
205            };
206
207            this.update(&mut cx, |this, cx| {
208                this.matches = matches;
209                this.selected_index = this
210                    .selected_index
211                    .min(this.matches.len().saturating_sub(1));
212                this.show_selected_theme(cx);
213                cx.notify();
214            });
215        })
216    }
217
218    fn render_match(
219        &self,
220        ix: usize,
221        mouse_state: MouseState,
222        selected: bool,
223        cx: &AppContext,
224    ) -> ElementBox {
225        let settings = cx.global::<Settings>();
226        let theme = &settings.theme;
227        let theme_match = &self.matches[ix];
228        let style = theme.picker.item.style_for(mouse_state, selected);
229
230        Label::new(theme_match.string.clone(), style.label.clone())
231            .with_highlights(theme_match.positions.clone())
232            .contained()
233            .with_style(style.container)
234            .boxed()
235    }
236}
237
238impl Entity for ThemeSelector {
239    type Event = Event;
240
241    fn release(&mut self, cx: &mut MutableAppContext) {
242        if !self.selection_completed {
243            Self::set_theme(self.original_theme.clone(), cx);
244        }
245    }
246}
247
248impl View for ThemeSelector {
249    fn ui_name() -> &'static str {
250        "ThemeSelector"
251    }
252
253    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
254        ChildView::new(self.picker.clone()).boxed()
255    }
256
257    fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
258        if cx.is_self_focused() {
259            cx.focus(&self.picker);
260        }
261    }
262}