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