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 registry;
 15mod scale;
 16mod schema;
 17mod settings;
 18mod styles;
 19
 20use std::path::Path;
 21use std::sync::Arc;
 22
 23use ::settings::{Settings, SettingsStore};
 24use anyhow::Result;
 25use fs::Fs;
 26use gpui::{
 27    px, AppContext, AssetSource, HighlightStyle, Hsla, Pixels, Refineable, SharedString,
 28    WindowAppearance, WindowBackgroundAppearance,
 29};
 30use serde::Deserialize;
 31use uuid::Uuid;
 32
 33pub use crate::default_colors::*;
 34pub use crate::font_family_cache::*;
 35pub use crate::registry::*;
 36pub use crate::scale::*;
 37pub use crate::schema::*;
 38pub use crate::settings::*;
 39pub use crate::styles::*;
 40
 41/// Defines window border radius for platforms that use client side decorations.
 42pub const CLIENT_SIDE_DECORATION_ROUNDING: Pixels = px(10.0);
 43/// Defines window shadow size for platforms that use client side decorations.
 44pub const CLIENT_SIDE_DECORATION_SHADOW: Pixels = px(10.0);
 45
 46/// The appearance of the theme.
 47#[derive(Debug, PartialEq, Clone, Copy, Deserialize)]
 48pub enum Appearance {
 49    /// A light appearance.
 50    Light,
 51    /// A dark appearance.
 52    Dark,
 53}
 54
 55impl Appearance {
 56    /// Returns whether the appearance is light.
 57    pub fn is_light(&self) -> bool {
 58        match self {
 59            Self::Light => true,
 60            Self::Dark => false,
 61        }
 62    }
 63}
 64
 65impl From<WindowAppearance> for Appearance {
 66    fn from(value: WindowAppearance) -> Self {
 67        match value {
 68            WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark,
 69            WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light,
 70        }
 71    }
 72}
 73
 74/// Which themes should be loaded. This is used primarlily for testing.
 75pub enum LoadThemes {
 76    /// Only load the base theme.
 77    ///
 78    /// No user themes will be loaded.
 79    JustBase,
 80
 81    /// Load all of the built-in themes.
 82    All(Box<dyn AssetSource>),
 83}
 84
 85/// Initialize the theme system.
 86pub fn init(themes_to_load: LoadThemes, cx: &mut AppContext) {
 87    let (assets, load_user_themes) = match themes_to_load {
 88        LoadThemes::JustBase => (Box::new(()) as Box<dyn AssetSource>, false),
 89        LoadThemes::All(assets) => (assets, true),
 90    };
 91    let registry = Arc::new(RealThemeRegistry::new(assets));
 92    registry.clone().set_global(cx);
 93
 94    if load_user_themes {
 95        registry.load_bundled_themes();
 96    }
 97
 98    ThemeSettings::register(cx);
 99    FontFamilyCache::init_global(cx);
100
101    let mut prev_buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
102    cx.observe_global::<SettingsStore>(move |cx| {
103        let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
104        if buffer_font_size != prev_buffer_font_size {
105            prev_buffer_font_size = buffer_font_size;
106            reset_buffer_font_size(cx);
107        }
108    })
109    .detach();
110}
111
112/// Implementing this trait allows accessing the active theme.
113pub trait ActiveTheme {
114    /// Returns the active theme.
115    fn theme(&self) -> &Arc<Theme>;
116}
117
118impl ActiveTheme for AppContext {
119    fn theme(&self) -> &Arc<Theme> {
120        &ThemeSettings::get_global(self).active_theme
121    }
122}
123
124/// A theme family is a grouping of themes under a single name.
125///
126/// For example, the "One" theme family contains the "One Light" and "One Dark" themes.
127///
128/// It can also be used to package themes with many variants.
129///
130/// For example, the "Atelier" theme family contains "Cave", "Dune", "Estuary", "Forest", "Heath", etc.
131pub struct ThemeFamily {
132    /// The unique identifier for the theme family.
133    pub id: String,
134    /// The name of the theme family. This will be displayed in the UI, such as when adding or removing a theme family.
135    pub name: SharedString,
136    /// The author of the theme family.
137    pub author: SharedString,
138    /// The [Theme]s in the family.
139    pub themes: Vec<Theme>,
140    /// The color scales used by the themes in the family.
141    /// Note: This will be removed in the future.
142    pub scales: ColorScales,
143}
144
145impl ThemeFamily {
146    // This is on ThemeFamily because we will have variables here we will need
147    // in the future to resolve @references.
148    /// Refines ThemeContent into a theme, merging it's contents with the base theme.
149    pub fn refine_theme(&self, theme: &ThemeContent) -> Theme {
150        let appearance = match theme.appearance {
151            AppearanceContent::Light => Appearance::Light,
152            AppearanceContent::Dark => Appearance::Dark,
153        };
154
155        let mut refined_theme_colors = match theme.appearance {
156            AppearanceContent::Light => ThemeColors::light(),
157            AppearanceContent::Dark => ThemeColors::dark(),
158        };
159        refined_theme_colors.refine(&theme.style.theme_colors_refinement());
160
161        let mut refined_status_colors = match theme.appearance {
162            AppearanceContent::Light => StatusColors::light(),
163            AppearanceContent::Dark => StatusColors::dark(),
164        };
165        refined_status_colors.refine(&theme.style.status_colors_refinement());
166
167        let mut refined_player_colors = match theme.appearance {
168            AppearanceContent::Light => PlayerColors::light(),
169            AppearanceContent::Dark => PlayerColors::dark(),
170        };
171        refined_player_colors.merge(&theme.style.players);
172
173        let mut refined_accent_colors = match theme.appearance {
174            AppearanceContent::Light => AccentColors::light(),
175            AppearanceContent::Dark => AccentColors::dark(),
176        };
177        refined_accent_colors.merge(&theme.style.accents);
178
179        let syntax_highlights = theme
180            .style
181            .syntax
182            .iter()
183            .map(|(syntax_token, highlight)| {
184                (
185                    syntax_token.clone(),
186                    HighlightStyle {
187                        color: highlight
188                            .color
189                            .as_ref()
190                            .and_then(|color| try_parse_color(color).ok()),
191                        background_color: highlight
192                            .background_color
193                            .as_ref()
194                            .and_then(|color| try_parse_color(color).ok()),
195                        font_style: highlight.font_style.map(Into::into),
196                        font_weight: highlight.font_weight.map(Into::into),
197                        ..Default::default()
198                    },
199                )
200            })
201            .collect::<Vec<_>>();
202        let syntax_theme = SyntaxTheme::merge(Arc::new(SyntaxTheme::default()), syntax_highlights);
203
204        let window_background_appearance = theme
205            .style
206            .window_background_appearance
207            .map(Into::into)
208            .unwrap_or_default();
209
210        Theme {
211            id: uuid::Uuid::new_v4().to_string(),
212            name: theme.name.clone().into(),
213            appearance,
214            styles: ThemeStyles {
215                system: SystemColors::default(),
216                window_background_appearance,
217                accents: refined_accent_colors,
218                colors: refined_theme_colors,
219                status: refined_status_colors,
220                player: refined_player_colors,
221                syntax: syntax_theme,
222            },
223        }
224    }
225}
226
227/// Refines a [ThemeFamilyContent] and it's [ThemeContent]s into a [ThemeFamily].
228pub fn refine_theme_family(theme_family_content: ThemeFamilyContent) -> ThemeFamily {
229    let id = Uuid::new_v4().to_string();
230    let name = theme_family_content.name.clone();
231    let author = theme_family_content.author.clone();
232
233    let mut theme_family = ThemeFamily {
234        id: id.clone(),
235        name: name.clone().into(),
236        author: author.clone().into(),
237        themes: vec![],
238        scales: default_color_scales(),
239    };
240
241    let refined_themes = theme_family_content
242        .themes
243        .iter()
244        .map(|theme_content| theme_family.refine_theme(theme_content))
245        .collect();
246
247    theme_family.themes = refined_themes;
248
249    theme_family
250}
251
252/// A theme is the primary mechanism for defining the appearance of the UI.
253#[derive(Clone, PartialEq)]
254pub struct Theme {
255    /// The unique identifier for the theme.
256    pub id: String,
257    /// The name of the theme.
258    pub name: SharedString,
259    /// The appearance of the theme (light or dark).
260    pub appearance: Appearance,
261    /// The colors and other styles for the theme.
262    pub styles: ThemeStyles,
263}
264
265impl Theme {
266    /// Returns the [`SystemColors`] for the theme.
267    #[inline(always)]
268    pub fn system(&self) -> &SystemColors {
269        &self.styles.system
270    }
271
272    /// Returns the [`AccentColors`] for the theme.
273    #[inline(always)]
274    pub fn accents(&self) -> &AccentColors {
275        &self.styles.accents
276    }
277
278    /// Returns the [`PlayerColors`] for the theme.
279    #[inline(always)]
280    pub fn players(&self) -> &PlayerColors {
281        &self.styles.player
282    }
283
284    /// Returns the [`ThemeColors`] for the theme.
285    #[inline(always)]
286    pub fn colors(&self) -> &ThemeColors {
287        &self.styles.colors
288    }
289
290    /// Returns the [`SyntaxTheme`] for the theme.
291    #[inline(always)]
292    pub fn syntax(&self) -> &Arc<SyntaxTheme> {
293        &self.styles.syntax
294    }
295
296    /// Returns the [`StatusColors`] for the theme.
297    #[inline(always)]
298    pub fn status(&self) -> &StatusColors {
299        &self.styles.status
300    }
301
302    /// Returns the color for the syntax node with the given name.
303    #[inline(always)]
304    pub fn syntax_color(&self, name: &str) -> Hsla {
305        self.syntax().color(name)
306    }
307
308    /// Returns the [`Appearance`] for the theme.
309    #[inline(always)]
310    pub fn appearance(&self) -> Appearance {
311        self.appearance
312    }
313
314    /// Returns the [`WindowBackgroundAppearance`] for the theme.
315    #[inline(always)]
316    pub fn window_background_appearance(&self) -> WindowBackgroundAppearance {
317        self.styles.window_background_appearance
318    }
319}
320
321/// Compounds a color with an alpha value.
322/// TODO: Replace this with a method on Hsla.
323pub fn color_alpha(color: Hsla, alpha: f32) -> Hsla {
324    let mut color = color;
325    color.a = alpha;
326    color
327}
328
329/// Asynchronously reads the user theme from the specified path.
330pub async fn read_user_theme(theme_path: &Path, fs: Arc<dyn Fs>) -> Result<ThemeFamilyContent> {
331    let reader = fs.open_sync(theme_path).await?;
332    let theme_family: ThemeFamilyContent = serde_json_lenient::from_reader(reader)?;
333
334    for theme in &theme_family.themes {
335        if theme
336            .style
337            .colors
338            .deprecated_scrollbar_thumb_background
339            .is_some()
340        {
341            log::warn!(
342                r#"Theme "{theme_name}" is using a deprecated style property: scrollbar_thumb.background. Use `scrollbar.thumb.background` instead."#,
343                theme_name = theme.name
344            )
345        }
346    }
347
348    Ok(theme_family)
349}