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