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