1#![deny(missing_docs)]
2
3//! # Theme
4//!
5//! This crate provides the theme system for Zed.
6//!
7//! ## Overview
8//!
9//! A theme is a collection of colors used to build a consistent appearance for UI components across the application.
10
11mod default_colors;
12mod fallback_themes;
13mod font_family_cache;
14mod icon_theme;
15mod icon_theme_schema;
16mod registry;
17mod scale;
18mod schema;
19mod settings;
20mod styles;
21
22use std::sync::Arc;
23
24use ::settings::DEFAULT_DARK_THEME;
25use ::settings::IntoGpui;
26use ::settings::Settings;
27use ::settings::SettingsStore;
28use anyhow::Result;
29use fallback_themes::apply_status_color_defaults;
30use gpui::BorrowAppContext;
31use gpui::Global;
32use gpui::{
33 App, AssetSource, HighlightStyle, Hsla, Pixels, Refineable, SharedString, WindowAppearance,
34 WindowBackgroundAppearance, px,
35};
36use serde::Deserialize;
37use uuid::Uuid;
38
39pub use crate::default_colors::*;
40use crate::fallback_themes::apply_theme_color_defaults;
41pub use crate::font_family_cache::*;
42pub use crate::icon_theme::*;
43pub use crate::icon_theme_schema::*;
44pub use crate::registry::*;
45pub use crate::scale::*;
46pub use crate::schema::*;
47pub use crate::settings::*;
48pub use crate::styles::*;
49pub use ::settings::{
50 FontStyleContent, HighlightStyleContent, StatusColorsContent, ThemeColorsContent,
51 ThemeStyleContent,
52};
53
54/// Defines window border radius for platforms that use client side decorations.
55pub const CLIENT_SIDE_DECORATION_ROUNDING: Pixels = px(10.0);
56/// Defines window shadow size for platforms that use client side decorations.
57pub const CLIENT_SIDE_DECORATION_SHADOW: Pixels = px(10.0);
58
59/// The appearance of the theme.
60#[derive(Debug, PartialEq, Clone, Copy, Deserialize)]
61pub enum Appearance {
62 /// A light appearance.
63 Light,
64 /// A dark appearance.
65 Dark,
66}
67
68impl Appearance {
69 /// Returns whether the appearance is light.
70 pub fn is_light(&self) -> bool {
71 match self {
72 Self::Light => true,
73 Self::Dark => false,
74 }
75 }
76}
77
78impl From<WindowAppearance> for Appearance {
79 fn from(value: WindowAppearance) -> Self {
80 match value {
81 WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark,
82 WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light,
83 }
84 }
85}
86
87impl From<Appearance> for ThemeAppearanceMode {
88 fn from(value: Appearance) -> Self {
89 match value {
90 Appearance::Light => Self::Light,
91 Appearance::Dark => Self::Dark,
92 }
93 }
94}
95
96/// Which themes should be loaded. This is used primarily for testing.
97pub enum LoadThemes {
98 /// Only load the base theme.
99 ///
100 /// No user themes will be loaded.
101 JustBase,
102
103 /// Load all of the built-in themes.
104 All(Box<dyn AssetSource>),
105}
106
107/// Initialize the theme system.
108pub fn init(themes_to_load: LoadThemes, cx: &mut App) {
109 SystemAppearance::init(cx);
110 let (assets, load_user_themes) = match themes_to_load {
111 LoadThemes::JustBase => (Box::new(()) as Box<dyn AssetSource>, false),
112 LoadThemes::All(assets) => (assets, true),
113 };
114 ThemeRegistry::set_global(assets, cx);
115
116 if load_user_themes {
117 ThemeRegistry::global(cx).load_bundled_themes();
118 }
119
120 FontFamilyCache::init_global(cx);
121
122 let theme = GlobalTheme::configured_theme(cx);
123 let icon_theme = GlobalTheme::configured_icon_theme(cx);
124 cx.set_global(GlobalTheme { theme, icon_theme });
125
126 let settings = ThemeSettings::get_global(cx);
127
128 let mut prev_buffer_font_size_settings = settings.buffer_font_size_settings();
129 let mut prev_ui_font_size_settings = settings.ui_font_size_settings();
130 let mut prev_agent_ui_font_size_settings = settings.agent_ui_font_size_settings();
131 let mut prev_agent_buffer_font_size_settings = settings.agent_buffer_font_size_settings();
132 let mut prev_theme_name = settings.theme.name(SystemAppearance::global(cx).0);
133 let mut prev_icon_theme_name = settings.icon_theme.name(SystemAppearance::global(cx).0);
134 let mut prev_theme_overrides = (
135 settings.experimental_theme_overrides.clone(),
136 settings.theme_overrides.clone(),
137 );
138
139 cx.observe_global::<SettingsStore>(move |cx| {
140 let settings = ThemeSettings::get_global(cx);
141
142 let buffer_font_size_settings = settings.buffer_font_size_settings();
143 let ui_font_size_settings = settings.ui_font_size_settings();
144 let agent_ui_font_size_settings = settings.agent_ui_font_size_settings();
145 let agent_buffer_font_size_settings = settings.agent_buffer_font_size_settings();
146 let theme_name = settings.theme.name(SystemAppearance::global(cx).0);
147 let icon_theme_name = settings.icon_theme.name(SystemAppearance::global(cx).0);
148 let theme_overrides = (
149 settings.experimental_theme_overrides.clone(),
150 settings.theme_overrides.clone(),
151 );
152
153 if buffer_font_size_settings != prev_buffer_font_size_settings {
154 prev_buffer_font_size_settings = buffer_font_size_settings;
155 reset_buffer_font_size(cx);
156 }
157
158 if ui_font_size_settings != prev_ui_font_size_settings {
159 prev_ui_font_size_settings = ui_font_size_settings;
160 reset_ui_font_size(cx);
161 }
162
163 if agent_ui_font_size_settings != prev_agent_ui_font_size_settings {
164 prev_agent_ui_font_size_settings = agent_ui_font_size_settings;
165 reset_agent_ui_font_size(cx);
166 }
167
168 if agent_buffer_font_size_settings != prev_agent_buffer_font_size_settings {
169 prev_agent_buffer_font_size_settings = agent_buffer_font_size_settings;
170 reset_agent_buffer_font_size(cx);
171 }
172
173 if theme_name != prev_theme_name || theme_overrides != prev_theme_overrides {
174 prev_theme_name = theme_name;
175 prev_theme_overrides = theme_overrides;
176 GlobalTheme::reload_theme(cx);
177 }
178
179 if icon_theme_name != prev_icon_theme_name {
180 prev_icon_theme_name = icon_theme_name;
181 GlobalTheme::reload_icon_theme(cx);
182 }
183 })
184 .detach();
185}
186
187/// Implementing this trait allows accessing the active theme.
188pub trait ActiveTheme {
189 /// Returns the active theme.
190 fn theme(&self) -> &Arc<Theme>;
191}
192
193impl ActiveTheme for App {
194 fn theme(&self) -> &Arc<Theme> {
195 GlobalTheme::theme(self)
196 }
197}
198
199/// A theme family is a grouping of themes under a single name.
200///
201/// For example, the "One" theme family contains the "One Light" and "One Dark" themes.
202///
203/// It can also be used to package themes with many variants.
204///
205/// For example, the "Atelier" theme family contains "Cave", "Dune", "Estuary", "Forest", "Heath", etc.
206pub struct ThemeFamily {
207 /// The unique identifier for the theme family.
208 pub id: String,
209 /// The name of the theme family. This will be displayed in the UI, such as when adding or removing a theme family.
210 pub name: SharedString,
211 /// The author of the theme family.
212 pub author: SharedString,
213 /// The [Theme]s in the family.
214 pub themes: Vec<Theme>,
215 /// The color scales used by the themes in the family.
216 /// Note: This will be removed in the future.
217 pub scales: ColorScales,
218}
219
220impl ThemeFamily {
221 // This is on ThemeFamily because we will have variables here we will need
222 // in the future to resolve @references.
223 /// Refines ThemeContent into a theme, merging it's contents with the base theme.
224 pub fn refine_theme(&self, theme: &ThemeContent) -> Theme {
225 let appearance = match theme.appearance {
226 AppearanceContent::Light => Appearance::Light,
227 AppearanceContent::Dark => Appearance::Dark,
228 };
229
230 let mut refined_status_colors = match theme.appearance {
231 AppearanceContent::Light => StatusColors::light(),
232 AppearanceContent::Dark => StatusColors::dark(),
233 };
234 let mut status_colors_refinement = status_colors_refinement(&theme.style.status);
235 apply_status_color_defaults(&mut status_colors_refinement);
236 refined_status_colors.refine(&status_colors_refinement);
237
238 let mut refined_player_colors = match theme.appearance {
239 AppearanceContent::Light => PlayerColors::light(),
240 AppearanceContent::Dark => PlayerColors::dark(),
241 };
242 refined_player_colors.merge(&theme.style.players);
243
244 let mut refined_theme_colors = match theme.appearance {
245 AppearanceContent::Light => ThemeColors::light(),
246 AppearanceContent::Dark => ThemeColors::dark(),
247 };
248 let mut theme_colors_refinement =
249 theme_colors_refinement(&theme.style.colors, &status_colors_refinement);
250 apply_theme_color_defaults(&mut theme_colors_refinement, &refined_player_colors);
251 refined_theme_colors.refine(&theme_colors_refinement);
252
253 let mut refined_accent_colors = match theme.appearance {
254 AppearanceContent::Light => AccentColors::light(),
255 AppearanceContent::Dark => AccentColors::dark(),
256 };
257 refined_accent_colors.merge(&theme.style.accents);
258
259 let syntax_highlights = theme.style.syntax.iter().map(|(syntax_token, highlight)| {
260 (
261 syntax_token.clone(),
262 HighlightStyle {
263 color: highlight
264 .color
265 .as_ref()
266 .and_then(|color| try_parse_color(color).ok()),
267 background_color: highlight
268 .background_color
269 .as_ref()
270 .and_then(|color| try_parse_color(color).ok()),
271 font_style: highlight.font_style.map(|s| s.into_gpui()),
272 font_weight: highlight.font_weight.map(|w| w.into_gpui()),
273 ..Default::default()
274 },
275 )
276 });
277 let syntax_theme = Arc::new(SyntaxTheme::new(syntax_highlights));
278
279 let window_background_appearance = theme
280 .style
281 .window_background_appearance
282 .map(|w| w.into_gpui())
283 .unwrap_or_default();
284
285 Theme {
286 id: uuid::Uuid::new_v4().to_string(),
287 name: theme.name.clone().into(),
288 appearance,
289 styles: ThemeStyles {
290 system: SystemColors::default(),
291 window_background_appearance,
292 accents: refined_accent_colors,
293 colors: refined_theme_colors,
294 status: refined_status_colors,
295 player: refined_player_colors,
296 syntax: syntax_theme,
297 },
298 }
299 }
300}
301
302/// Refines a [ThemeFamilyContent] and it's [ThemeContent]s into a [ThemeFamily].
303pub fn refine_theme_family(theme_family_content: ThemeFamilyContent) -> ThemeFamily {
304 let id = Uuid::new_v4().to_string();
305 let name = theme_family_content.name.clone();
306 let author = theme_family_content.author.clone();
307
308 let mut theme_family = ThemeFamily {
309 id,
310 name: name.into(),
311 author: author.into(),
312 themes: vec![],
313 scales: default_color_scales(),
314 };
315
316 let refined_themes = theme_family_content
317 .themes
318 .iter()
319 .map(|theme_content| theme_family.refine_theme(theme_content))
320 .collect();
321
322 theme_family.themes = refined_themes;
323
324 theme_family
325}
326
327/// A theme is the primary mechanism for defining the appearance of the UI.
328#[derive(Clone, Debug, PartialEq)]
329pub struct Theme {
330 /// The unique identifier for the theme.
331 pub id: String,
332 /// The name of the theme.
333 pub name: SharedString,
334 /// The appearance of the theme (light or dark).
335 pub appearance: Appearance,
336 /// The colors and other styles for the theme.
337 pub styles: ThemeStyles,
338}
339
340impl Theme {
341 /// Returns the [`SystemColors`] for the theme.
342 #[inline(always)]
343 pub fn system(&self) -> &SystemColors {
344 &self.styles.system
345 }
346
347 /// Returns the [`AccentColors`] for the theme.
348 #[inline(always)]
349 pub fn accents(&self) -> &AccentColors {
350 &self.styles.accents
351 }
352
353 /// Returns the [`PlayerColors`] for the theme.
354 #[inline(always)]
355 pub fn players(&self) -> &PlayerColors {
356 &self.styles.player
357 }
358
359 /// Returns the [`ThemeColors`] for the theme.
360 #[inline(always)]
361 pub fn colors(&self) -> &ThemeColors {
362 &self.styles.colors
363 }
364
365 /// Returns the [`SyntaxTheme`] for the theme.
366 #[inline(always)]
367 pub fn syntax(&self) -> &Arc<SyntaxTheme> {
368 &self.styles.syntax
369 }
370
371 /// Returns the [`StatusColors`] for the theme.
372 #[inline(always)]
373 pub fn status(&self) -> &StatusColors {
374 &self.styles.status
375 }
376
377 /// Returns the [`Appearance`] for the theme.
378 #[inline(always)]
379 pub fn appearance(&self) -> Appearance {
380 self.appearance
381 }
382
383 /// Returns the [`WindowBackgroundAppearance`] for the theme.
384 #[inline(always)]
385 pub fn window_background_appearance(&self) -> WindowBackgroundAppearance {
386 self.styles.window_background_appearance
387 }
388
389 /// Darkens the color by reducing its lightness.
390 /// The resulting lightness is clamped to ensure it doesn't go below 0.0.
391 ///
392 /// The first value darkens light appearance mode, the second darkens appearance dark mode.
393 ///
394 /// Note: This is a tentative solution and may be replaced with a more robust color system.
395 pub fn darken(&self, color: Hsla, light_amount: f32, dark_amount: f32) -> Hsla {
396 let amount = match self.appearance {
397 Appearance::Light => light_amount,
398 Appearance::Dark => dark_amount,
399 };
400 let mut hsla = color;
401 hsla.l = (hsla.l - amount).max(0.0);
402 hsla
403 }
404}
405
406/// Deserializes a user theme from the given bytes.
407pub fn deserialize_user_theme(bytes: &[u8]) -> Result<ThemeFamilyContent> {
408 let theme_family: ThemeFamilyContent = serde_json_lenient::from_slice(bytes)?;
409
410 for theme in &theme_family.themes {
411 if theme
412 .style
413 .colors
414 .deprecated_scrollbar_thumb_background
415 .is_some()
416 {
417 log::warn!(
418 r#"Theme "{theme_name}" is using a deprecated style property: scrollbar_thumb.background. Use `scrollbar.thumb.background` instead."#,
419 theme_name = theme.name
420 )
421 }
422 }
423
424 Ok(theme_family)
425}
426
427/// Deserializes a icon theme from the given bytes.
428pub fn deserialize_icon_theme(bytes: &[u8]) -> Result<IconThemeFamilyContent> {
429 let icon_theme_family: IconThemeFamilyContent = serde_json_lenient::from_slice(bytes)?;
430
431 Ok(icon_theme_family)
432}
433
434/// The active theme
435pub struct GlobalTheme {
436 theme: Arc<Theme>,
437 icon_theme: Arc<IconTheme>,
438}
439impl Global for GlobalTheme {}
440
441impl GlobalTheme {
442 fn configured_theme(cx: &mut App) -> Arc<Theme> {
443 let themes = ThemeRegistry::default_global(cx);
444 let theme_settings = ThemeSettings::get_global(cx);
445 let system_appearance = SystemAppearance::global(cx);
446
447 let theme_name = theme_settings.theme.name(*system_appearance);
448
449 let theme = match themes.get(&theme_name.0) {
450 Ok(theme) => theme,
451 Err(err) => {
452 if themes.extensions_loaded() {
453 log::error!("{err}");
454 }
455 themes
456 .get(default_theme(*system_appearance))
457 // fallback for tests.
458 .unwrap_or_else(|_| themes.get(DEFAULT_DARK_THEME).unwrap())
459 }
460 };
461 theme_settings.apply_theme_overrides(theme)
462 }
463
464 /// Reloads the current theme.
465 ///
466 /// Reads the [`ThemeSettings`] to know which theme should be loaded,
467 /// taking into account the current [`SystemAppearance`].
468 pub fn reload_theme(cx: &mut App) {
469 let theme = Self::configured_theme(cx);
470 cx.update_global::<Self, _>(|this, _| this.theme = theme);
471 cx.refresh_windows();
472 }
473
474 fn configured_icon_theme(cx: &mut App) -> Arc<IconTheme> {
475 let themes = ThemeRegistry::default_global(cx);
476 let theme_settings = ThemeSettings::get_global(cx);
477 let system_appearance = SystemAppearance::global(cx);
478
479 let icon_theme_name = theme_settings.icon_theme.name(*system_appearance);
480
481 match themes.get_icon_theme(&icon_theme_name.0) {
482 Ok(theme) => theme,
483 Err(err) => {
484 if themes.extensions_loaded() {
485 log::error!("{err}");
486 }
487 themes.get_icon_theme(DEFAULT_ICON_THEME_NAME).unwrap()
488 }
489 }
490 }
491
492 /// Reloads the current icon theme.
493 ///
494 /// Reads the [`ThemeSettings`] to know which icon theme should be loaded,
495 /// taking into account the current [`SystemAppearance`].
496 pub fn reload_icon_theme(cx: &mut App) {
497 let icon_theme = Self::configured_icon_theme(cx);
498 cx.update_global::<Self, _>(|this, _| this.icon_theme = icon_theme);
499 cx.refresh_windows();
500 }
501
502 /// the active theme
503 pub fn theme(cx: &App) -> &Arc<Theme> {
504 &cx.global::<Self>().theme
505 }
506
507 /// the active icon theme
508 pub fn icon_theme(cx: &App) -> &Arc<IconTheme> {
509 &cx.global::<Self>().icon_theme
510 }
511}