theme_selector.rs

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