1use std::collections::HashMap;
2use std::sync::Arc;
3
4use anyhow::{anyhow, Context, Result};
5use gpui::{AssetSource, HighlightStyle, SharedString};
6use refineable::Refineable;
7use util::ResultExt;
8
9use crate::{
10 try_parse_color, Appearance, AppearanceContent, PlayerColor, PlayerColors, StatusColors,
11 SyntaxTheme, SystemColors, Theme, ThemeColors, ThemeContent, ThemeFamily, ThemeFamilyContent,
12 ThemeStyles,
13};
14
15#[derive(Debug, Clone)]
16pub struct ThemeMeta {
17 pub name: SharedString,
18 pub appearance: Appearance,
19}
20
21pub struct ThemeRegistry {
22 assets: Box<dyn AssetSource>,
23 themes: HashMap<SharedString, Arc<Theme>>,
24}
25
26impl ThemeRegistry {
27 pub fn new(assets: Box<dyn AssetSource>) -> Self {
28 let mut registry = Self {
29 assets,
30 themes: HashMap::new(),
31 };
32
33 // We're loading our new versions of the One themes by default, as
34 // we need them to be loaded for tests.
35 //
36 // These themes will get overwritten when `load_user_themes` is called
37 // when Zed starts, so the One variants used will be the ones ported from Zed1.
38 registry.insert_theme_families([crate::one_themes::one_family()]);
39
40 registry
41 }
42
43 fn insert_theme_families(&mut self, families: impl IntoIterator<Item = ThemeFamily>) {
44 for family in families.into_iter() {
45 self.insert_themes(family.themes);
46 }
47 }
48
49 fn insert_themes(&mut self, themes: impl IntoIterator<Item = Theme>) {
50 for theme in themes.into_iter() {
51 self.themes.insert(theme.name.clone(), Arc::new(theme));
52 }
53 }
54
55 #[allow(unused)]
56 fn insert_user_theme_families(
57 &mut self,
58 families: impl IntoIterator<Item = ThemeFamilyContent>,
59 ) {
60 for family in families.into_iter() {
61 self.insert_user_themes(family.themes);
62 }
63 }
64
65 #[allow(unused)]
66 fn insert_user_themes(&mut self, themes: impl IntoIterator<Item = ThemeContent>) {
67 self.insert_themes(themes.into_iter().map(|user_theme| {
68 let mut theme_colors = match user_theme.appearance {
69 AppearanceContent::Light => ThemeColors::light(),
70 AppearanceContent::Dark => ThemeColors::dark(),
71 };
72 theme_colors.refine(&user_theme.style.theme_colors_refinement());
73
74 let mut status_colors = match user_theme.appearance {
75 AppearanceContent::Light => StatusColors::light(),
76 AppearanceContent::Dark => StatusColors::dark(),
77 };
78 status_colors.refine(&user_theme.style.status_colors_refinement());
79
80 let mut player_colors = match user_theme.appearance {
81 AppearanceContent::Light => PlayerColors::light(),
82 AppearanceContent::Dark => PlayerColors::dark(),
83 };
84 if !user_theme.style.players.is_empty() {
85 player_colors = PlayerColors(
86 user_theme
87 .style
88 .players
89 .into_iter()
90 .map(|player| PlayerColor {
91 cursor: player
92 .cursor
93 .as_ref()
94 .and_then(|color| try_parse_color(&color).ok())
95 .unwrap_or_default(),
96 background: player
97 .background
98 .as_ref()
99 .and_then(|color| try_parse_color(&color).ok())
100 .unwrap_or_default(),
101 selection: player
102 .selection
103 .as_ref()
104 .and_then(|color| try_parse_color(&color).ok())
105 .unwrap_or_default(),
106 })
107 .collect(),
108 );
109 }
110
111 let mut syntax_colors = match user_theme.appearance {
112 AppearanceContent::Light => SyntaxTheme::light(),
113 AppearanceContent::Dark => SyntaxTheme::dark(),
114 };
115 if !user_theme.style.syntax.is_empty() {
116 syntax_colors.highlights = user_theme
117 .style
118 .syntax
119 .iter()
120 .map(|(syntax_token, highlight)| {
121 (
122 syntax_token.clone(),
123 HighlightStyle {
124 color: highlight
125 .color
126 .as_ref()
127 .and_then(|color| try_parse_color(&color).ok()),
128 font_style: highlight.font_style.map(Into::into),
129 font_weight: highlight.font_weight.map(Into::into),
130 ..Default::default()
131 },
132 )
133 })
134 .collect::<Vec<_>>();
135 }
136
137 Theme {
138 id: uuid::Uuid::new_v4().to_string(),
139 name: user_theme.name.into(),
140 appearance: match user_theme.appearance {
141 AppearanceContent::Light => Appearance::Light,
142 AppearanceContent::Dark => Appearance::Dark,
143 },
144 styles: ThemeStyles {
145 system: SystemColors::default(),
146 colors: theme_colors,
147 status: status_colors,
148 player: player_colors,
149 syntax: Arc::new(syntax_colors),
150 accents: Vec::new(),
151 },
152 }
153 }));
154 }
155
156 pub fn clear(&mut self) {
157 self.themes.clear();
158 }
159
160 pub fn list_names(&self, _staff: bool) -> impl Iterator<Item = SharedString> + '_ {
161 self.themes.keys().cloned()
162 }
163
164 pub fn list(&self, _staff: bool) -> impl Iterator<Item = ThemeMeta> + '_ {
165 self.themes.values().map(|theme| ThemeMeta {
166 name: theme.name.clone(),
167 appearance: theme.appearance(),
168 })
169 }
170
171 pub fn get(&self, name: &str) -> Result<Arc<Theme>> {
172 self.themes
173 .get(name)
174 .ok_or_else(|| anyhow!("theme not found: {}", name))
175 .cloned()
176 }
177
178 pub fn load_user_themes(&mut self) {
179 let theme_paths = self
180 .assets
181 .list("themes/")
182 .expect("failed to list theme assets")
183 .into_iter()
184 .filter(|path| path.ends_with(".json"));
185
186 for path in theme_paths {
187 let Some(theme) = self.assets.load(&path).log_err() else {
188 continue;
189 };
190
191 let Some(theme_family) = serde_json::from_slice(&theme)
192 .with_context(|| format!("failed to parse theme at path \"{path}\""))
193 .log_err()
194 else {
195 continue;
196 };
197
198 self.insert_user_theme_families([theme_family]);
199 }
200 }
201}
202
203impl Default for ThemeRegistry {
204 fn default() -> Self {
205 Self::new(Box::new(()))
206 }
207}