theme_selector.rs

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