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