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}