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}