1#![deny(missing_docs)]
2
3//! # Theme Settings
4//!
5//! This crate provides theme settings integration for Zed,
6//! bridging the theme system with the settings infrastructure.
7
8mod schema;
9mod settings;
10
11use std::sync::Arc;
12
13use ::settings::{IntoGpui, Settings, SettingsStore};
14use anyhow::{Context as _, Result};
15use gpui::{App, HighlightStyle, Refineable};
16use gpui_util::ResultExt;
17use theme::{
18 AccentColors, Appearance, AppearanceContent, DEFAULT_DARK_THEME, DEFAULT_ICON_THEME_NAME,
19 GlobalTheme, LoadThemes, PlayerColor, PlayerColors, StatusColors, SyntaxTheme,
20 SystemAppearance, SystemColors, Theme, ThemeColors, ThemeFamily, ThemeRegistry, ThemeStyles,
21 default_color_scales, try_parse_color,
22};
23
24pub use crate::schema::{
25 FontStyleContent, FontWeightContent, HighlightStyleContent, StatusColorsContent,
26 ThemeColorsContent, ThemeContent, ThemeFamilyContent, ThemeStyleContent,
27 WindowBackgroundContent, status_colors_refinement, syntax_overrides, theme_colors_refinement,
28};
29pub use crate::settings::{
30 AgentFontSize, BufferLineHeight, FontFamilyName, IconThemeName, IconThemeSelection,
31 ThemeAppearanceMode, ThemeName, ThemeSelection, ThemeSettings, UiDensity,
32 adjust_agent_buffer_font_size, adjust_agent_ui_font_size, adjust_buffer_font_size,
33 adjust_ui_font_size, adjusted_font_size, appearance_to_mode, clamp_font_size, default_theme,
34 observe_buffer_font_size_adjustment, reset_agent_buffer_font_size, reset_agent_ui_font_size,
35 reset_buffer_font_size, reset_ui_font_size, set_icon_theme, set_mode, set_theme, setup_ui_font,
36};
37
38/// Initialize the theme system with settings integration.
39///
40/// This is the full initialization for the application. It calls [`theme::init`]
41/// and then wires up settings observation for theme/font changes.
42pub fn init(themes_to_load: LoadThemes, cx: &mut App) {
43 let load_user_themes = matches!(&themes_to_load, LoadThemes::All(_));
44
45 theme::init(themes_to_load, cx);
46
47 if load_user_themes {
48 let registry = ThemeRegistry::global(cx);
49 load_bundled_themes(®istry);
50 }
51
52 let theme = configured_theme(cx);
53 let icon_theme = configured_icon_theme(cx);
54 GlobalTheme::update_theme(cx, theme);
55 GlobalTheme::update_icon_theme(cx, icon_theme);
56
57 let settings = ThemeSettings::get_global(cx);
58
59 let mut prev_buffer_font_size_settings = settings.buffer_font_size_settings();
60 let mut prev_ui_font_size_settings = settings.ui_font_size_settings();
61 let mut prev_agent_ui_font_size_settings = settings.agent_ui_font_size_settings();
62 let mut prev_agent_buffer_font_size_settings = settings.agent_buffer_font_size_settings();
63 let mut prev_theme_name = settings.theme.name(SystemAppearance::global(cx).0);
64 let mut prev_icon_theme_name = settings.icon_theme.name(SystemAppearance::global(cx).0);
65 let mut prev_theme_overrides = (
66 settings.experimental_theme_overrides.clone(),
67 settings.theme_overrides.clone(),
68 );
69
70 cx.observe_global::<SettingsStore>(move |cx| {
71 let settings = ThemeSettings::get_global(cx);
72
73 let buffer_font_size_settings = settings.buffer_font_size_settings();
74 let ui_font_size_settings = settings.ui_font_size_settings();
75 let agent_ui_font_size_settings = settings.agent_ui_font_size_settings();
76 let agent_buffer_font_size_settings = settings.agent_buffer_font_size_settings();
77 let theme_name = settings.theme.name(SystemAppearance::global(cx).0);
78 let icon_theme_name = settings.icon_theme.name(SystemAppearance::global(cx).0);
79 let theme_overrides = (
80 settings.experimental_theme_overrides.clone(),
81 settings.theme_overrides.clone(),
82 );
83
84 if buffer_font_size_settings != prev_buffer_font_size_settings {
85 prev_buffer_font_size_settings = buffer_font_size_settings;
86 reset_buffer_font_size(cx);
87 }
88
89 if ui_font_size_settings != prev_ui_font_size_settings {
90 prev_ui_font_size_settings = ui_font_size_settings;
91 reset_ui_font_size(cx);
92 }
93
94 if agent_ui_font_size_settings != prev_agent_ui_font_size_settings {
95 prev_agent_ui_font_size_settings = agent_ui_font_size_settings;
96 reset_agent_ui_font_size(cx);
97 }
98
99 if agent_buffer_font_size_settings != prev_agent_buffer_font_size_settings {
100 prev_agent_buffer_font_size_settings = agent_buffer_font_size_settings;
101 reset_agent_buffer_font_size(cx);
102 }
103
104 if theme_name != prev_theme_name || theme_overrides != prev_theme_overrides {
105 prev_theme_name = theme_name;
106 prev_theme_overrides = theme_overrides;
107 reload_theme(cx);
108 }
109
110 if icon_theme_name != prev_icon_theme_name {
111 prev_icon_theme_name = icon_theme_name;
112 reload_icon_theme(cx);
113 }
114 })
115 .detach();
116}
117
118fn configured_theme(cx: &mut App) -> Arc<Theme> {
119 let themes = ThemeRegistry::default_global(cx);
120 let theme_settings = ThemeSettings::get_global(cx);
121 let system_appearance = SystemAppearance::global(cx);
122
123 let theme_name = theme_settings.theme.name(*system_appearance);
124
125 let theme = match themes.get(&theme_name.0) {
126 Ok(theme) => theme,
127 Err(err) => {
128 if themes.extensions_loaded() {
129 log::error!("{err}");
130 }
131 themes
132 .get(default_theme(*system_appearance))
133 .unwrap_or_else(|_| themes.get(DEFAULT_DARK_THEME).unwrap())
134 }
135 };
136 theme_settings.apply_theme_overrides(theme)
137}
138
139fn configured_icon_theme(cx: &mut App) -> Arc<theme::IconTheme> {
140 let themes = ThemeRegistry::default_global(cx);
141 let theme_settings = ThemeSettings::get_global(cx);
142 let system_appearance = SystemAppearance::global(cx);
143
144 let icon_theme_name = theme_settings.icon_theme.name(*system_appearance);
145
146 match themes.get_icon_theme(&icon_theme_name.0) {
147 Ok(theme) => theme,
148 Err(err) => {
149 if themes.extensions_loaded() {
150 log::error!("{err}");
151 }
152 themes.get_icon_theme(DEFAULT_ICON_THEME_NAME).unwrap()
153 }
154 }
155}
156
157/// Reloads the current theme from settings.
158pub fn reload_theme(cx: &mut App) {
159 let theme = configured_theme(cx);
160 GlobalTheme::update_theme(cx, theme);
161 cx.refresh_windows();
162}
163
164/// Reloads the current icon theme from settings.
165pub fn reload_icon_theme(cx: &mut App) {
166 let icon_theme = configured_icon_theme(cx);
167 GlobalTheme::update_icon_theme(cx, icon_theme);
168 cx.refresh_windows();
169}
170
171/// Loads the themes bundled with the Zed binary into the registry.
172pub fn load_bundled_themes(registry: &ThemeRegistry) {
173 let theme_paths = registry
174 .assets()
175 .list("themes/")
176 .expect("failed to list theme assets")
177 .into_iter()
178 .filter(|path| path.ends_with(".json"));
179
180 for path in theme_paths {
181 let Some(theme) = registry.assets().load(&path).log_err().flatten() else {
182 continue;
183 };
184
185 let Some(theme_family) = serde_json::from_slice(&theme)
186 .with_context(|| format!("failed to parse theme at path \"{path}\""))
187 .log_err()
188 else {
189 continue;
190 };
191
192 let refined = refine_theme_family(theme_family);
193 registry.insert_theme_families([refined]);
194 }
195}
196
197/// Loads a user theme from the given bytes into the registry.
198pub fn load_user_theme(registry: &ThemeRegistry, bytes: &[u8]) -> Result<()> {
199 let theme = deserialize_user_theme(bytes)?;
200 let refined = refine_theme_family(theme);
201 registry.insert_theme_families([refined]);
202 Ok(())
203}
204
205/// Deserializes a user theme from the given bytes.
206pub fn deserialize_user_theme(bytes: &[u8]) -> Result<ThemeFamilyContent> {
207 let theme_family: ThemeFamilyContent = serde_json_lenient::from_slice(bytes)?;
208
209 for theme in &theme_family.themes {
210 if theme
211 .style
212 .colors
213 .deprecated_scrollbar_thumb_background
214 .is_some()
215 {
216 log::warn!(
217 r#"Theme "{theme_name}" is using a deprecated style property: scrollbar_thumb.background. Use `scrollbar.thumb.background` instead."#,
218 theme_name = theme.name
219 )
220 }
221 }
222
223 Ok(theme_family)
224}
225
226/// Refines a [`ThemeFamilyContent`] and its [`ThemeContent`]s into a [`ThemeFamily`].
227pub fn refine_theme_family(theme_family_content: ThemeFamilyContent) -> ThemeFamily {
228 let id = uuid::Uuid::new_v4().to_string();
229 let name = theme_family_content.name.clone();
230 let author = theme_family_content.author.clone();
231
232 let themes: Vec<Theme> = theme_family_content
233 .themes
234 .iter()
235 .map(|theme_content| refine_theme(theme_content))
236 .collect();
237
238 ThemeFamily {
239 id,
240 name: name.into(),
241 author: author.into(),
242 themes,
243 scales: default_color_scales(),
244 }
245}
246
247/// Refines a [`ThemeContent`] into a [`Theme`].
248pub fn refine_theme(theme: &ThemeContent) -> Theme {
249 let appearance = match theme.appearance {
250 AppearanceContent::Light => Appearance::Light,
251 AppearanceContent::Dark => Appearance::Dark,
252 };
253
254 let mut refined_status_colors = match theme.appearance {
255 AppearanceContent::Light => StatusColors::light(),
256 AppearanceContent::Dark => StatusColors::dark(),
257 };
258 let mut status_colors_refinement = status_colors_refinement(&theme.style.status);
259 theme::apply_status_color_defaults(&mut status_colors_refinement);
260 refined_status_colors.refine(&status_colors_refinement);
261
262 let mut refined_player_colors = match theme.appearance {
263 AppearanceContent::Light => PlayerColors::light(),
264 AppearanceContent::Dark => PlayerColors::dark(),
265 };
266 merge_player_colors(&mut refined_player_colors, &theme.style.players);
267
268 let mut refined_theme_colors = match theme.appearance {
269 AppearanceContent::Light => ThemeColors::light(),
270 AppearanceContent::Dark => ThemeColors::dark(),
271 };
272 let mut theme_colors_refinement =
273 theme_colors_refinement(&theme.style.colors, &status_colors_refinement);
274 theme::apply_theme_color_defaults(&mut theme_colors_refinement, &refined_player_colors);
275 refined_theme_colors.refine(&theme_colors_refinement);
276
277 let mut refined_accent_colors = match theme.appearance {
278 AppearanceContent::Light => AccentColors::light(),
279 AppearanceContent::Dark => AccentColors::dark(),
280 };
281 merge_accent_colors(&mut refined_accent_colors, &theme.style.accents);
282
283 let syntax_highlights = theme.style.syntax.iter().map(|(syntax_token, highlight)| {
284 (
285 syntax_token.clone(),
286 HighlightStyle {
287 color: highlight
288 .color
289 .as_ref()
290 .and_then(|color| try_parse_color(color).ok()),
291 background_color: highlight
292 .background_color
293 .as_ref()
294 .and_then(|color| try_parse_color(color).ok()),
295 font_style: highlight.font_style.map(|s| s.into_gpui()),
296 font_weight: highlight.font_weight.map(|w| w.into_gpui()),
297 ..Default::default()
298 },
299 )
300 });
301 let syntax_theme = Arc::new(SyntaxTheme::new(syntax_highlights));
302
303 let window_background_appearance = theme
304 .style
305 .window_background_appearance
306 .map(|w| w.into_gpui())
307 .unwrap_or_default();
308
309 Theme {
310 id: uuid::Uuid::new_v4().to_string(),
311 name: theme.name.clone().into(),
312 appearance,
313 styles: ThemeStyles {
314 system: SystemColors::default(),
315 window_background_appearance,
316 accents: refined_accent_colors,
317 colors: refined_theme_colors,
318 status: refined_status_colors,
319 player: refined_player_colors,
320 syntax: syntax_theme,
321 },
322 }
323}
324
325/// Merges player color overrides into the given [`PlayerColors`].
326pub fn merge_player_colors(
327 player_colors: &mut PlayerColors,
328 user_player_colors: &[::settings::PlayerColorContent],
329) {
330 if user_player_colors.is_empty() {
331 return;
332 }
333
334 for (idx, player) in user_player_colors.iter().enumerate() {
335 let cursor = player
336 .cursor
337 .as_ref()
338 .and_then(|color| try_parse_color(color).ok());
339 let background = player
340 .background
341 .as_ref()
342 .and_then(|color| try_parse_color(color).ok());
343 let selection = player
344 .selection
345 .as_ref()
346 .and_then(|color| try_parse_color(color).ok());
347
348 if let Some(player_color) = player_colors.0.get_mut(idx) {
349 *player_color = PlayerColor {
350 cursor: cursor.unwrap_or(player_color.cursor),
351 background: background.unwrap_or(player_color.background),
352 selection: selection.unwrap_or(player_color.selection),
353 };
354 } else {
355 player_colors.0.push(PlayerColor {
356 cursor: cursor.unwrap_or_default(),
357 background: background.unwrap_or_default(),
358 selection: selection.unwrap_or_default(),
359 });
360 }
361 }
362}
363
364/// Merges accent color overrides into the given [`AccentColors`].
365pub fn merge_accent_colors(
366 accent_colors: &mut AccentColors,
367 user_accent_colors: &[::settings::AccentContent],
368) {
369 if user_accent_colors.is_empty() {
370 return;
371 }
372
373 let colors = user_accent_colors
374 .iter()
375 .filter_map(|accent_color| {
376 accent_color
377 .0
378 .as_ref()
379 .and_then(|color| try_parse_color(color).ok())
380 })
381 .collect::<Vec<_>>();
382
383 if !colors.is_empty() {
384 accent_colors.0 = Arc::from(colors);
385 }
386}