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}