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