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