theme_selector.rs

  1use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
  2use gpui::{
  3    actions, anyhow::Result, elements::*, AnyViewHandle, AppContext, Element, ElementBox, Entity,
  4    MouseState, MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
  5};
  6use paths::SETTINGS;
  7use picker::{Picker, PickerDelegate};
  8use settings::{write_theme, Settings};
  9use smol::{fs::read_to_string, io::AsyncWriteExt};
 10use std::sync::Arc;
 11use theme::{Theme, ThemeMeta, ThemeRegistry};
 12use workspace::{AppState, Workspace};
 13
 14pub struct ThemeSelector {
 15    registry: Arc<ThemeRegistry>,
 16    theme_data: Vec<ThemeMeta>,
 17    matches: Vec<StringMatch>,
 18    original_theme: Arc<Theme>,
 19    picker: ViewHandle<Picker<Self>>,
 20    selection_completed: bool,
 21    selected_index: usize,
 22}
 23
 24actions!(theme_selector, [Toggle, Reload]);
 25
 26pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
 27    Picker::<ThemeSelector>::init(cx);
 28    cx.add_action({
 29        let theme_registry = app_state.themes.clone();
 30        move |workspace, _: &Toggle, cx| {
 31            ThemeSelector::toggle(workspace, theme_registry.clone(), cx)
 32        }
 33    });
 34}
 35
 36pub enum Event {
 37    Dismissed,
 38}
 39
 40impl ThemeSelector {
 41    fn new(registry: Arc<ThemeRegistry>, cx: &mut ViewContext<Self>) -> Self {
 42        let handle = cx.weak_handle();
 43        let picker = cx.add_view(|cx| Picker::new(handle, cx));
 44        let settings = cx.global::<Settings>();
 45
 46        let original_theme = settings.theme.clone();
 47
 48        let mut theme_names = registry
 49            .list(
 50                settings.staff_mode,
 51                settings.experiments.experimental_themes,
 52            )
 53            .collect::<Vec<_>>();
 54        theme_names.sort_unstable_by(|a, b| {
 55            a.is_light
 56                .cmp(&b.is_light)
 57                .reverse()
 58                .then(a.name.cmp(&b.name))
 59        });
 60        let matches = theme_names
 61            .iter()
 62            .map(|meta| StringMatch {
 63                candidate_id: 0,
 64                score: 0.0,
 65                positions: Default::default(),
 66                string: meta.name.clone(),
 67            })
 68            .collect();
 69        let mut this = Self {
 70            registry,
 71            theme_data: theme_names,
 72            matches,
 73            picker,
 74            original_theme: original_theme.clone(),
 75            selected_index: 0,
 76            selection_completed: false,
 77        };
 78        this.select_if_matching(&original_theme.meta.name);
 79        this
 80    }
 81
 82    fn toggle(
 83        workspace: &mut Workspace,
 84        themes: Arc<ThemeRegistry>,
 85        cx: &mut ViewContext<Workspace>,
 86    ) {
 87        workspace.toggle_modal(cx, |_, cx| {
 88            let this = cx.add_view(|cx| Self::new(themes, cx));
 89            cx.subscribe(&this, Self::on_event).detach();
 90            this
 91        });
 92    }
 93
 94    #[cfg(debug_assertions)]
 95    pub fn reload(themes: Arc<ThemeRegistry>, cx: &mut MutableAppContext) {
 96        let current_theme_name = cx.global::<Settings>().theme.meta.name.clone();
 97        themes.clear();
 98        match themes.get(&current_theme_name) {
 99            Ok(theme) => {
100                Self::set_theme(theme, cx);
101                log::info!("reloaded theme {}", current_theme_name);
102            }
103            Err(error) => {
104                log::error!("failed to load theme {}: {:?}", current_theme_name, error)
105            }
106        }
107    }
108
109    fn show_selected_theme(&mut self, cx: &mut ViewContext<Self>) {
110        if let Some(mat) = self.matches.get(self.selected_index) {
111            match self.registry.get(&mat.string) {
112                Ok(theme) => {
113                    Self::set_theme(theme, cx);
114
115                    let theme_name = mat.string.clone();
116
117                    cx.background()
118                        .spawn(async move {
119                            match write_theme_name(theme_name).await {
120                                Ok(_) => {}
121                                Err(_) => return, //TODO Pop toast
122                            }
123                        })
124                        .detach()
125                }
126                Err(error) => {
127                    log::error!("error loading theme {}: {}", mat.string, error)
128                }
129            }
130        }
131    }
132
133    fn select_if_matching(&mut self, theme_name: &str) {
134        self.selected_index = self
135            .matches
136            .iter()
137            .position(|mat| mat.string == theme_name)
138            .unwrap_or(self.selected_index);
139    }
140
141    fn on_event(
142        workspace: &mut Workspace,
143        _: ViewHandle<ThemeSelector>,
144        event: &Event,
145        cx: &mut ViewContext<Workspace>,
146    ) {
147        match event {
148            Event::Dismissed => {
149                workspace.dismiss_modal(cx);
150            }
151        }
152    }
153
154    fn set_theme(theme: Arc<Theme>, cx: &mut MutableAppContext) {
155        cx.update_global::<Settings, _, _>(|settings, cx| {
156            settings.theme = theme;
157            cx.refresh_windows();
158        });
159    }
160}
161
162impl PickerDelegate for ThemeSelector {
163    fn match_count(&self) -> usize {
164        self.matches.len()
165    }
166
167    fn confirm(&mut self, cx: &mut ViewContext<Self>) {
168        self.selection_completed = true;
169        cx.emit(Event::Dismissed);
170    }
171
172    fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
173        if !self.selection_completed {
174            Self::set_theme(self.original_theme.clone(), cx);
175            self.selection_completed = true;
176        }
177        cx.emit(Event::Dismissed);
178    }
179
180    fn selected_index(&self) -> usize {
181        self.selected_index
182    }
183
184    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
185        self.selected_index = ix;
186        self.show_selected_theme(cx);
187    }
188
189    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> gpui::Task<()> {
190        let background = cx.background().clone();
191        let candidates = self
192            .theme_data
193            .iter()
194            .enumerate()
195            .map(|(id, meta)| StringMatchCandidate {
196                id,
197                char_bag: meta.name.as_str().into(),
198                string: meta.name.clone(),
199            })
200            .collect::<Vec<_>>();
201
202        cx.spawn(|this, mut cx| async move {
203            let matches = if query.is_empty() {
204                candidates
205                    .into_iter()
206                    .enumerate()
207                    .map(|(index, candidate)| StringMatch {
208                        candidate_id: index,
209                        string: candidate.string,
210                        positions: Vec::new(),
211                        score: 0.0,
212                    })
213                    .collect()
214            } else {
215                match_strings(
216                    &candidates,
217                    &query,
218                    false,
219                    100,
220                    &Default::default(),
221                    background,
222                )
223                .await
224            };
225
226            this.update(&mut cx, |this, cx| {
227                this.matches = matches;
228                this.selected_index = this
229                    .selected_index
230                    .min(this.matches.len().saturating_sub(1));
231                this.show_selected_theme(cx);
232                cx.notify();
233            });
234        })
235    }
236
237    fn render_match(
238        &self,
239        ix: usize,
240        mouse_state: MouseState,
241        selected: bool,
242        cx: &AppContext,
243    ) -> ElementBox {
244        let settings = cx.global::<Settings>();
245        let theme = &settings.theme;
246        let theme_match = &self.matches[ix];
247        let style = theme.picker.item.style_for(mouse_state, selected);
248
249        Label::new(theme_match.string.clone(), style.label.clone())
250            .with_highlights(theme_match.positions.clone())
251            .contained()
252            .with_style(style.container)
253            .boxed()
254    }
255}
256
257impl Entity for ThemeSelector {
258    type Event = Event;
259
260    fn release(&mut self, cx: &mut MutableAppContext) {
261        if !self.selection_completed {
262            Self::set_theme(self.original_theme.clone(), cx);
263        }
264    }
265}
266
267impl View for ThemeSelector {
268    fn ui_name() -> &'static str {
269        "ThemeSelector"
270    }
271
272    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
273        ChildView::new(self.picker.clone()).boxed()
274    }
275
276    fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
277        if cx.is_self_focused() {
278            cx.focus(&self.picker);
279        }
280    }
281}
282
283async fn write_theme_name(theme_name: String) -> Result<()> {
284    let mut settings = read_to_string(SETTINGS.as_path()).await?;
285    settings = write_theme(settings, &theme_name);
286
287    let mut file = smol::fs::OpenOptions::new()
288        .truncate(true)
289        .write(true)
290        .open(SETTINGS.as_path())
291        .await?;
292
293    file.write_all(settings.as_bytes()).await?;
294
295    Ok(())
296}