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