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