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