registry.rs

  1use std::sync::Arc;
  2use std::{fmt::Debug, path::Path};
  3
  4use anyhow::Result;
  5use collections::HashMap;
  6use gpui::{App, AssetSource, Global, SharedString};
  7use parking_lot::RwLock;
  8use thiserror::Error;
  9
 10use crate::{
 11    Appearance, AppearanceContent, ChevronIcons, DEFAULT_ICON_THEME_NAME, DirectoryIcons,
 12    IconDefinition, IconTheme, IconThemeFamilyContent, Theme, ThemeFamily, default_icon_theme,
 13};
 14
 15/// The metadata for a theme.
 16#[derive(Debug, Clone)]
 17pub struct ThemeMeta {
 18    /// The name of the theme.
 19    pub name: SharedString,
 20    /// The appearance of the theme.
 21    pub appearance: Appearance,
 22}
 23
 24/// An error indicating that the theme with the given name was not found.
 25#[derive(Debug, Error, Clone)]
 26#[error("theme not found: {0}")]
 27pub struct ThemeNotFoundError(pub SharedString);
 28
 29/// An error indicating that the icon theme with the given name was not found.
 30#[derive(Debug, Error, Clone)]
 31#[error("icon theme not found: {0}")]
 32pub struct IconThemeNotFoundError(pub SharedString);
 33
 34/// The global [`ThemeRegistry`].
 35///
 36/// This newtype exists for obtaining a unique [`TypeId`](std::any::TypeId) when
 37/// inserting the [`ThemeRegistry`] into the context as a global.
 38///
 39/// This should not be exposed outside of this module.
 40#[derive(Default)]
 41struct GlobalThemeRegistry(Arc<ThemeRegistry>);
 42
 43impl std::ops::DerefMut for GlobalThemeRegistry {
 44    fn deref_mut(&mut self) -> &mut Self::Target {
 45        &mut self.0
 46    }
 47}
 48
 49impl std::ops::Deref for GlobalThemeRegistry {
 50    type Target = Arc<ThemeRegistry>;
 51
 52    fn deref(&self) -> &Self::Target {
 53        &self.0
 54    }
 55}
 56
 57impl Global for GlobalThemeRegistry {}
 58
 59struct ThemeRegistryState {
 60    themes: HashMap<SharedString, Arc<Theme>>,
 61    icon_themes: HashMap<SharedString, Arc<IconTheme>>,
 62    /// Whether the extensions have been loaded yet.
 63    extensions_loaded: bool,
 64}
 65
 66/// The registry for themes.
 67pub struct ThemeRegistry {
 68    state: RwLock<ThemeRegistryState>,
 69    assets: Box<dyn AssetSource>,
 70}
 71
 72impl ThemeRegistry {
 73    /// Returns the global [`ThemeRegistry`].
 74    pub fn global(cx: &App) -> Arc<Self> {
 75        cx.global::<GlobalThemeRegistry>().0.clone()
 76    }
 77
 78    /// Returns the global [`ThemeRegistry`].
 79    ///
 80    /// Inserts a default [`ThemeRegistry`] if one does not yet exist.
 81    pub fn default_global(cx: &mut App) -> Arc<Self> {
 82        cx.default_global::<GlobalThemeRegistry>().0.clone()
 83    }
 84
 85    /// Returns the global [`ThemeRegistry`] if it exists.
 86    pub fn try_global(cx: &mut App) -> Option<Arc<Self>> {
 87        cx.try_global::<GlobalThemeRegistry>().map(|t| t.0.clone())
 88    }
 89
 90    /// Sets the global [`ThemeRegistry`].
 91    pub(crate) fn set_global(assets: Box<dyn AssetSource>, cx: &mut App) {
 92        cx.set_global(GlobalThemeRegistry(Arc::new(ThemeRegistry::new(assets))));
 93    }
 94
 95    /// Returns the asset source used by this registry.
 96    pub fn assets(&self) -> &dyn AssetSource {
 97        self.assets.as_ref()
 98    }
 99
100    /// Creates a new [`ThemeRegistry`] with the given [`AssetSource`].
101    pub fn new(assets: Box<dyn AssetSource>) -> Self {
102        let registry = Self {
103            state: RwLock::new(ThemeRegistryState {
104                themes: HashMap::default(),
105                icon_themes: HashMap::default(),
106                extensions_loaded: false,
107            }),
108            assets,
109        };
110
111        // We're loading the Zed default theme, as we need a theme to be loaded
112        // for tests.
113        registry.insert_theme_families([crate::fallback_themes::zed_default_themes()]);
114
115        let default_icon_theme = crate::default_icon_theme();
116        registry
117            .state
118            .write()
119            .icon_themes
120            .insert(default_icon_theme.name.clone(), default_icon_theme);
121
122        registry
123    }
124
125    /// Returns whether the extensions have been loaded.
126    pub fn extensions_loaded(&self) -> bool {
127        self.state.read().extensions_loaded
128    }
129
130    /// Sets the flag indicating that the extensions have loaded.
131    pub fn set_extensions_loaded(&self) {
132        self.state.write().extensions_loaded = true;
133    }
134
135    /// Inserts the given theme families into the registry.
136    pub fn insert_theme_families(&self, families: impl IntoIterator<Item = ThemeFamily>) {
137        for family in families.into_iter() {
138            self.insert_themes(family.themes);
139        }
140    }
141
142    /// Registers theme families for use in tests.
143    #[cfg(any(test, feature = "test-support"))]
144    pub fn register_test_themes(&self, families: impl IntoIterator<Item = ThemeFamily>) {
145        self.insert_theme_families(families);
146    }
147
148    /// Registers icon themes for use in tests.
149    #[cfg(any(test, feature = "test-support"))]
150    pub fn register_test_icon_themes(&self, icon_themes: impl IntoIterator<Item = IconTheme>) {
151        let mut state = self.state.write();
152        for icon_theme in icon_themes {
153            state
154                .icon_themes
155                .insert(icon_theme.name.clone(), Arc::new(icon_theme));
156        }
157    }
158
159    /// Inserts the given themes into the registry.
160    pub fn insert_themes(&self, themes: impl IntoIterator<Item = Theme>) {
161        let mut state = self.state.write();
162        for theme in themes.into_iter() {
163            state.themes.insert(theme.name.clone(), Arc::new(theme));
164        }
165    }
166
167    /// Removes the themes with the given names from the registry.
168    pub fn remove_user_themes(&self, themes_to_remove: &[SharedString]) {
169        self.state
170            .write()
171            .themes
172            .retain(|name, _| !themes_to_remove.contains(name))
173    }
174
175    /// Removes all themes from the registry.
176    pub fn clear(&self) {
177        self.state.write().themes.clear();
178    }
179
180    /// Returns the names of all themes in the registry.
181    pub fn list_names(&self) -> Vec<SharedString> {
182        let mut names = self.state.read().themes.keys().cloned().collect::<Vec<_>>();
183        names.sort();
184        names
185    }
186
187    /// Returns the metadata of all themes in the registry.
188    pub fn list(&self) -> Vec<ThemeMeta> {
189        self.state
190            .read()
191            .themes
192            .values()
193            .map(|theme| ThemeMeta {
194                name: theme.name.clone(),
195                appearance: theme.appearance(),
196            })
197            .collect()
198    }
199
200    /// Returns the theme with the given name.
201    pub fn get(&self, name: &str) -> Result<Arc<Theme>, ThemeNotFoundError> {
202        self.state
203            .read()
204            .themes
205            .get(name)
206            .ok_or_else(|| ThemeNotFoundError(name.to_string().into()))
207            .cloned()
208    }
209
210    /// Returns the default icon theme.
211    pub fn default_icon_theme(&self) -> Result<Arc<IconTheme>, IconThemeNotFoundError> {
212        self.get_icon_theme(DEFAULT_ICON_THEME_NAME)
213    }
214
215    /// Returns the metadata of all icon themes in the registry.
216    pub fn list_icon_themes(&self) -> Vec<ThemeMeta> {
217        self.state
218            .read()
219            .icon_themes
220            .values()
221            .map(|theme| ThemeMeta {
222                name: theme.name.clone(),
223                appearance: theme.appearance,
224            })
225            .collect()
226    }
227
228    /// Returns the icon theme with the specified name.
229    pub fn get_icon_theme(&self, name: &str) -> Result<Arc<IconTheme>, IconThemeNotFoundError> {
230        self.state
231            .read()
232            .icon_themes
233            .get(name)
234            .ok_or_else(|| IconThemeNotFoundError(name.to_string().into()))
235            .cloned()
236    }
237
238    /// Removes the icon themes with the given names from the registry.
239    pub fn remove_icon_themes(&self, icon_themes_to_remove: &[SharedString]) {
240        self.state
241            .write()
242            .icon_themes
243            .retain(|name, _| !icon_themes_to_remove.contains(name))
244    }
245
246    /// Loads the icon theme from the icon theme family and adds it to the registry.
247    ///
248    /// The `icons_root_dir` parameter indicates the root directory from which
249    /// the relative paths to icons in the theme should be resolved against.
250    pub fn load_icon_theme(
251        &self,
252        icon_theme_family: IconThemeFamilyContent,
253        icons_root_dir: &Path,
254    ) -> Result<()> {
255        let resolve_icon_path = |path: SharedString| {
256            icons_root_dir
257                .join(path.as_ref())
258                .to_string_lossy()
259                .to_string()
260                .into()
261        };
262
263        let default_icon_theme = default_icon_theme();
264
265        let mut state = self.state.write();
266        for icon_theme in icon_theme_family.themes {
267            let mut file_stems = default_icon_theme.file_stems.clone();
268            file_stems.extend(icon_theme.file_stems);
269
270            let mut file_suffixes = default_icon_theme.file_suffixes.clone();
271            file_suffixes.extend(icon_theme.file_suffixes);
272
273            let mut named_directory_icons = default_icon_theme.named_directory_icons.clone();
274            named_directory_icons.extend(icon_theme.named_directory_icons.into_iter().map(
275                |(key, value)| {
276                    (
277                        key,
278                        DirectoryIcons {
279                            collapsed: value.collapsed.map(resolve_icon_path),
280                            expanded: value.expanded.map(resolve_icon_path),
281                        },
282                    )
283                },
284            ));
285
286            let icon_theme = IconTheme {
287                id: uuid::Uuid::new_v4().to_string(),
288                name: icon_theme.name.into(),
289                appearance: match icon_theme.appearance {
290                    AppearanceContent::Light => Appearance::Light,
291                    AppearanceContent::Dark => Appearance::Dark,
292                },
293                directory_icons: DirectoryIcons {
294                    collapsed: icon_theme.directory_icons.collapsed.map(resolve_icon_path),
295                    expanded: icon_theme.directory_icons.expanded.map(resolve_icon_path),
296                },
297                named_directory_icons,
298                chevron_icons: ChevronIcons {
299                    collapsed: icon_theme.chevron_icons.collapsed.map(resolve_icon_path),
300                    expanded: icon_theme.chevron_icons.expanded.map(resolve_icon_path),
301                },
302                file_stems,
303                file_suffixes,
304                file_icons: icon_theme
305                    .file_icons
306                    .into_iter()
307                    .map(|(key, icon)| {
308                        (
309                            key,
310                            IconDefinition {
311                                path: resolve_icon_path(icon.path),
312                            },
313                        )
314                    })
315                    .collect(),
316            };
317
318            state
319                .icon_themes
320                .insert(icon_theme.name.clone(), Arc::new(icon_theme));
321        }
322
323        Ok(())
324    }
325}
326
327impl Default for ThemeRegistry {
328    fn default() -> Self {
329        Self::new(Box::new(()))
330    }
331}