registry.rs

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