theme.rs

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