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    /// Inserts the given themes into the registry.
130    pub fn insert_themes(&self, themes: impl IntoIterator<Item = Theme>) {
131        let mut state = self.state.write();
132        for theme in themes.into_iter() {
133            state.themes.insert(theme.name.clone(), Arc::new(theme));
134        }
135    }
136
137    /// Removes the themes with the given names from the registry.
138    pub fn remove_user_themes(&self, themes_to_remove: &[SharedString]) {
139        self.state
140            .write()
141            .themes
142            .retain(|name, _| !themes_to_remove.contains(name))
143    }
144
145    /// Removes all themes from the registry.
146    pub fn clear(&self) {
147        self.state.write().themes.clear();
148    }
149
150    /// Returns the names of all themes in the registry.
151    pub fn list_names(&self) -> Vec<SharedString> {
152        let mut names = self.state.read().themes.keys().cloned().collect::<Vec<_>>();
153        names.sort();
154        names
155    }
156
157    /// Returns the metadata of all themes in the registry.
158    pub fn list(&self) -> Vec<ThemeMeta> {
159        self.state
160            .read()
161            .themes
162            .values()
163            .map(|theme| ThemeMeta {
164                name: theme.name.clone(),
165                appearance: theme.appearance(),
166            })
167            .collect()
168    }
169
170    /// Returns the theme with the given name.
171    pub fn get(&self, name: &str) -> Result<Arc<Theme>, ThemeNotFoundError> {
172        self.state
173            .read()
174            .themes
175            .get(name)
176            .ok_or_else(|| ThemeNotFoundError(name.to_string().into()))
177            .cloned()
178    }
179
180    /// Returns the default icon theme.
181    pub fn default_icon_theme(&self) -> Result<Arc<IconTheme>, IconThemeNotFoundError> {
182        self.get_icon_theme(DEFAULT_ICON_THEME_NAME)
183    }
184
185    /// Returns the metadata of all icon themes in the registry.
186    pub fn list_icon_themes(&self) -> Vec<ThemeMeta> {
187        self.state
188            .read()
189            .icon_themes
190            .values()
191            .map(|theme| ThemeMeta {
192                name: theme.name.clone(),
193                appearance: theme.appearance,
194            })
195            .collect()
196    }
197
198    /// Returns the icon theme with the specified name.
199    pub fn get_icon_theme(&self, name: &str) -> Result<Arc<IconTheme>, IconThemeNotFoundError> {
200        self.state
201            .read()
202            .icon_themes
203            .get(name)
204            .ok_or_else(|| IconThemeNotFoundError(name.to_string().into()))
205            .cloned()
206    }
207
208    /// Removes the icon themes with the given names from the registry.
209    pub fn remove_icon_themes(&self, icon_themes_to_remove: &[SharedString]) {
210        self.state
211            .write()
212            .icon_themes
213            .retain(|name, _| !icon_themes_to_remove.contains(name))
214    }
215
216    /// Loads the icon theme from the icon theme family and adds it to the registry.
217    ///
218    /// The `icons_root_dir` parameter indicates the root directory from which
219    /// the relative paths to icons in the theme should be resolved against.
220    pub fn load_icon_theme(
221        &self,
222        icon_theme_family: IconThemeFamilyContent,
223        icons_root_dir: &Path,
224    ) -> Result<()> {
225        let resolve_icon_path = |path: SharedString| {
226            icons_root_dir
227                .join(path.as_ref())
228                .to_string_lossy()
229                .to_string()
230                .into()
231        };
232
233        let default_icon_theme = default_icon_theme();
234
235        let mut state = self.state.write();
236        for icon_theme in icon_theme_family.themes {
237            let mut file_stems = default_icon_theme.file_stems.clone();
238            file_stems.extend(icon_theme.file_stems);
239
240            let mut file_suffixes = default_icon_theme.file_suffixes.clone();
241            file_suffixes.extend(icon_theme.file_suffixes);
242
243            let mut named_directory_icons = default_icon_theme.named_directory_icons.clone();
244            named_directory_icons.extend(icon_theme.named_directory_icons.into_iter().map(
245                |(key, value)| {
246                    (
247                        key,
248                        DirectoryIcons {
249                            collapsed: value.collapsed.map(resolve_icon_path),
250                            expanded: value.expanded.map(resolve_icon_path),
251                        },
252                    )
253                },
254            ));
255
256            let icon_theme = IconTheme {
257                id: uuid::Uuid::new_v4().to_string(),
258                name: icon_theme.name.into(),
259                appearance: match icon_theme.appearance {
260                    AppearanceContent::Light => Appearance::Light,
261                    AppearanceContent::Dark => Appearance::Dark,
262                },
263                directory_icons: DirectoryIcons {
264                    collapsed: icon_theme.directory_icons.collapsed.map(resolve_icon_path),
265                    expanded: icon_theme.directory_icons.expanded.map(resolve_icon_path),
266                },
267                named_directory_icons,
268                chevron_icons: ChevronIcons {
269                    collapsed: icon_theme.chevron_icons.collapsed.map(resolve_icon_path),
270                    expanded: icon_theme.chevron_icons.expanded.map(resolve_icon_path),
271                },
272                file_stems,
273                file_suffixes,
274                file_icons: icon_theme
275                    .file_icons
276                    .into_iter()
277                    .map(|(key, icon)| {
278                        (
279                            key,
280                            IconDefinition {
281                                path: resolve_icon_path(icon.path),
282                            },
283                        )
284                    })
285                    .collect(),
286            };
287
288            state
289                .icon_themes
290                .insert(icon_theme.name.clone(), Arc::new(icon_theme));
291        }
292
293        Ok(())
294    }
295}
296
297impl Default for ThemeRegistry {
298    fn default() -> Self {
299        Self::new(Box::new(()))
300    }
301}