Load JSON themes (#6893)

Marshall Bowers created

This PR changes the theme loading to use the JSON themes bundled with
the binary rather then the Rust theme definitions.

### Performance

I profiled this using `cargo run --release` to see what the speed
differences would be now that we're deserializing JSON:

**Before:** `ThemeRegistry::load_user_themes` took 16.656666ms
**After:** `ThemeRegistry::load_user_themes` took 18.784875ms

It's slightly slower, but not by much. There is probably some work we
could do here to bring down the theme loading time in general.

Release Notes:

- N/A

Change summary

assets/themes/LICENSES            |   0 
crates/storybook/src/storybook.rs |   2 
crates/theme/src/registry.rs      | 127 +++++++++++++++++++++++---------
crates/theme/src/schema.rs        |  30 +++++++
crates/theme/src/theme.rs         |  18 ++-
crates/zed/src/main.rs            |   2 
script/generate-licenses          |   2 
7 files changed, 133 insertions(+), 48 deletions(-)

Detailed changes

crates/storybook/src/storybook.rs 🔗

@@ -69,7 +69,7 @@ fn main() {
             .unwrap();
         cx.set_global(store);
 
-        theme::init(theme::LoadThemes::All, cx);
+        theme::init(theme::LoadThemes::All(Box::new(Assets)), cx);
 
         let selector = story_selector;
 

crates/theme/src/registry.rs 🔗

@@ -2,12 +2,13 @@ use std::collections::HashMap;
 use std::sync::Arc;
 
 use anyhow::{anyhow, Result};
-use gpui::{HighlightStyle, SharedString};
+use gpui::{AssetSource, HighlightStyle, SharedString};
 use refineable::Refineable;
 
 use crate::{
-    Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme, ThemeColors,
-    ThemeFamily, ThemeStyles, UserTheme, UserThemeFamily,
+    try_parse_color, Appearance, AppearanceContent, PlayerColor, PlayerColors, StatusColors,
+    SyntaxTheme, SystemColors, Theme, ThemeColors, ThemeContent, ThemeFamily, ThemeFamilyContent,
+    ThemeStyles,
 };
 
 #[derive(Debug, Clone)]
@@ -17,10 +18,27 @@ pub struct ThemeMeta {
 }
 
 pub struct ThemeRegistry {
+    assets: Box<dyn AssetSource>,
     themes: HashMap<SharedString, Arc<Theme>>,
 }
 
 impl ThemeRegistry {
+    pub fn new(assets: Box<dyn AssetSource>) -> Self {
+        let mut registry = Self {
+            assets,
+            themes: HashMap::new(),
+        };
+
+        // We're loading our new versions of the One themes by default, as
+        // we need them to be loaded for tests.
+        //
+        // These themes will get overwritten when `load_user_themes` is called
+        // when Zed starts, so the One variants used will be the ones ported from Zed1.
+        registry.insert_theme_families([crate::one_themes::one_family()]);
+
+        registry
+    }
+
     fn insert_theme_families(&mut self, families: impl IntoIterator<Item = ThemeFamily>) {
         for family in families.into_iter() {
             self.insert_themes(family.themes);
@@ -34,48 +52,78 @@ impl ThemeRegistry {
     }
 
     #[allow(unused)]
-    fn insert_user_theme_families(&mut self, families: impl IntoIterator<Item = UserThemeFamily>) {
+    fn insert_user_theme_families(
+        &mut self,
+        families: impl IntoIterator<Item = ThemeFamilyContent>,
+    ) {
         for family in families.into_iter() {
             self.insert_user_themes(family.themes);
         }
     }
 
     #[allow(unused)]
-    fn insert_user_themes(&mut self, themes: impl IntoIterator<Item = UserTheme>) {
+    fn insert_user_themes(&mut self, themes: impl IntoIterator<Item = ThemeContent>) {
         self.insert_themes(themes.into_iter().map(|user_theme| {
             let mut theme_colors = match user_theme.appearance {
-                Appearance::Light => ThemeColors::light(),
-                Appearance::Dark => ThemeColors::dark(),
+                AppearanceContent::Light => ThemeColors::light(),
+                AppearanceContent::Dark => ThemeColors::dark(),
             };
-            theme_colors.refine(&user_theme.styles.colors);
+            theme_colors.refine(&user_theme.style.theme_colors_refinement());
 
             let mut status_colors = match user_theme.appearance {
-                Appearance::Light => StatusColors::light(),
-                Appearance::Dark => StatusColors::dark(),
+                AppearanceContent::Light => StatusColors::light(),
+                AppearanceContent::Dark => StatusColors::dark(),
             };
-            status_colors.refine(&user_theme.styles.status);
+            status_colors.refine(&user_theme.style.status_colors_refinement());
 
             let mut player_colors = match user_theme.appearance {
-                Appearance::Light => PlayerColors::light(),
-                Appearance::Dark => PlayerColors::dark(),
+                AppearanceContent::Light => PlayerColors::light(),
+                AppearanceContent::Dark => PlayerColors::dark(),
             };
-            if let Some(player_colors_from_theme) = user_theme.styles.player {
-                player_colors = player_colors_from_theme;
+            if !user_theme.style.players.is_empty() {
+                player_colors = PlayerColors(
+                    user_theme
+                        .style
+                        .players
+                        .into_iter()
+                        .map(|player| PlayerColor {
+                            cursor: player
+                                .cursor
+                                .as_ref()
+                                .and_then(|color| try_parse_color(&color).ok())
+                                .unwrap_or_default(),
+                            background: player
+                                .background
+                                .as_ref()
+                                .and_then(|color| try_parse_color(&color).ok())
+                                .unwrap_or_default(),
+                            selection: player
+                                .selection
+                                .as_ref()
+                                .and_then(|color| try_parse_color(&color).ok())
+                                .unwrap_or_default(),
+                        })
+                        .collect(),
+                );
             }
 
             let mut syntax_colors = match user_theme.appearance {
-                Appearance::Light => SyntaxTheme::light(),
-                Appearance::Dark => SyntaxTheme::dark(),
+                AppearanceContent::Light => SyntaxTheme::light(),
+                AppearanceContent::Dark => SyntaxTheme::dark(),
             };
-            if let Some(user_syntax) = user_theme.styles.syntax {
-                syntax_colors.highlights = user_syntax
-                    .highlights
+            if !user_theme.style.syntax.is_empty() {
+                syntax_colors.highlights = user_theme
+                    .style
+                    .syntax
                     .iter()
                     .map(|(syntax_token, highlight)| {
                         (
                             syntax_token.clone(),
                             HighlightStyle {
-                                color: highlight.color,
+                                color: highlight
+                                    .color
+                                    .as_ref()
+                                    .and_then(|color| try_parse_color(&color).ok()),
                                 font_style: highlight.font_style.map(Into::into),
                                 font_weight: highlight.font_weight.map(Into::into),
                                 ..Default::default()
@@ -88,7 +136,10 @@ impl ThemeRegistry {
             Theme {
                 id: uuid::Uuid::new_v4().to_string(),
                 name: user_theme.name.into(),
-                appearance: user_theme.appearance,
+                appearance: match user_theme.appearance {
+                    AppearanceContent::Light => Appearance::Light,
+                    AppearanceContent::Dark => Appearance::Dark,
+                },
                 styles: ThemeStyles {
                     system: SystemColors::default(),
                     colors: theme_colors,
@@ -124,24 +175,28 @@ impl ThemeRegistry {
     }
 
     pub fn load_user_themes(&mut self) {
-        #[cfg(not(feature = "importing-themes"))]
-        self.insert_user_theme_families(crate::all_user_themes());
+        let theme_paths = self
+            .assets
+            .list("themes/")
+            .unwrap()
+            .into_iter()
+            .filter(|path| path.ends_with(".json"));
+
+        for path in theme_paths {
+            let theme = self
+                .assets
+                .load(&path)
+                .expect(&format!("Failed to load theme '{path}'"));
+
+            let theme_family: ThemeFamilyContent = serde_json::from_slice(&theme).unwrap();
+
+            self.insert_user_theme_families([theme_family]);
+        }
     }
 }
 
 impl Default for ThemeRegistry {
     fn default() -> Self {
-        let mut registry = Self {
-            themes: HashMap::default(),
-        };
-
-        // We're loading our new versions of the One themes by default, as
-        // we need them to be loaded for tests.
-        //
-        // These themes will get overwritten when `load_user_themes` is called
-        // when Zed starts, so the One variants used will be the ones ported from Zed1.
-        registry.insert_theme_families([crate::one_themes::one_family()]);
-
-        registry
+        Self::new(Box::new(()))
     }
 }

crates/theme/src/schema.rs 🔗

@@ -1,5 +1,5 @@
 use anyhow::Result;
-use gpui::{HighlightStyle, Hsla};
+use gpui::{FontStyle, FontWeight, HighlightStyle, Hsla};
 use indexmap::IndexMap;
 use palette::FromColor;
 use schemars::gen::SchemaGenerator;
@@ -11,7 +11,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr};
 
 use crate::{StatusColorsRefinement, ThemeColorsRefinement};
 
-fn try_parse_color(color: &str) -> Result<Hsla> {
+pub(crate) fn try_parse_color(color: &str) -> Result<Hsla> {
     let rgba = gpui::Rgba::try_from(color)?;
     let rgba = palette::rgb::Srgba::from_components((rgba.r, rgba.g, rgba.b, rgba.a));
     let hsla = palette::Hsla::from_color(rgba);
@@ -1171,6 +1171,16 @@ pub enum FontStyleContent {
     Oblique,
 }
 
+impl From<FontStyleContent> for FontStyle {
+    fn from(value: FontStyleContent) -> Self {
+        match value {
+            FontStyleContent::Normal => FontStyle::Normal,
+            FontStyleContent::Italic => FontStyle::Italic,
+            FontStyleContent::Oblique => FontStyle::Oblique,
+        }
+    }
+}
+
 #[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr)]
 #[repr(u16)]
 pub enum FontWeightContent {
@@ -1211,6 +1221,22 @@ impl JsonSchema for FontWeightContent {
     }
 }
 
+impl From<FontWeightContent> for FontWeight {
+    fn from(value: FontWeightContent) -> Self {
+        match value {
+            FontWeightContent::Thin => FontWeight::THIN,
+            FontWeightContent::ExtraLight => FontWeight::EXTRA_LIGHT,
+            FontWeightContent::Light => FontWeight::LIGHT,
+            FontWeightContent::Normal => FontWeight::NORMAL,
+            FontWeightContent::Medium => FontWeight::MEDIUM,
+            FontWeightContent::Semibold => FontWeight::SEMIBOLD,
+            FontWeightContent::Bold => FontWeight::BOLD,
+            FontWeightContent::ExtraBold => FontWeight::EXTRA_BOLD,
+            FontWeightContent::Black => FontWeight::BLACK,
+        }
+    }
+}
+
 #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
 #[serde(default)]
 pub struct HighlightStyleContent {

crates/theme/src/theme.rs 🔗

@@ -33,7 +33,7 @@ pub use styles::*;
 pub use themes::*;
 pub use user_theme::*;
 
-use gpui::{AppContext, Hsla, SharedString};
+use gpui::{AppContext, AssetSource, Hsla, SharedString};
 use serde::Deserialize;
 
 #[derive(Debug, PartialEq, Clone, Copy, Deserialize)]
@@ -51,7 +51,6 @@ impl Appearance {
     }
 }
 
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
 pub enum LoadThemes {
     /// Only load the base theme.
     ///
@@ -59,15 +58,20 @@ pub enum LoadThemes {
     JustBase,
 
     /// Load all of the built-in themes.
-    All,
+    All(Box<dyn AssetSource>),
 }
 
 pub fn init(themes_to_load: LoadThemes, cx: &mut AppContext) {
-    cx.set_global(ThemeRegistry::default());
-    match themes_to_load {
-        LoadThemes::JustBase => (),
-        LoadThemes::All => cx.global_mut::<ThemeRegistry>().load_user_themes(),
+    let (assets, load_user_themes) = match themes_to_load {
+        LoadThemes::JustBase => (Box::new(()) as Box<dyn AssetSource>, false),
+        LoadThemes::All(assets) => (assets, true),
+    };
+    cx.set_global(ThemeRegistry::new(assets));
+
+    if load_user_themes {
+        cx.global_mut::<ThemeRegistry>().load_user_themes();
     }
+
     ThemeSettings::register(cx);
 
     let mut prev_buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;

crates/zed/src/main.rs 🔗

@@ -149,7 +149,7 @@ fn main() {
         cx.set_global(client.clone());
 
         zed::init(cx);
-        theme::init(theme::LoadThemes::All, cx);
+        theme::init(theme::LoadThemes::All(Box::new(Assets)), cx);
         project::Project::init(&client, cx);
         client::init(&client, cx);
         command_palette::init(cx);

script/generate-licenses 🔗

@@ -9,7 +9,7 @@ OUTPUT_FILE=$(pwd)/assets/licenses.md
 echo -e "# ###### THEME LICENSES ######\n" >> $OUTPUT_FILE
 
 echo "Generating theme licenses"
-cat crates/theme/src/themes/LICENSES >> $OUTPUT_FILE
+cat assets/themes/LICENSES >> $OUTPUT_FILE
 
 echo -e "# ###### CODE LICENSES ######\n" >> $OUTPUT_FILE