theme: Add support for per-theme overrides (#30860)

Aleksei Gusev , Peter Tripp , and Marshall Bowers created

Closes #14050

Release Notes:

- Added the ability to set theme-specific overrides via the
`theme_overrides` setting.

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>

Change summary

crates/theme/src/settings.rs | 69 ++++++++++++++++++++++++-------------
1 file changed, 45 insertions(+), 24 deletions(-)

Detailed changes

crates/theme/src/settings.rs 🔗

@@ -4,6 +4,7 @@ use crate::{
     ThemeNotFoundError, ThemeRegistry, ThemeStyleContent,
 };
 use anyhow::Result;
+use collections::HashMap;
 use derive_more::{Deref, DerefMut};
 use gpui::{
     App, Context, Font, FontFallbacks, FontFeatures, FontStyle, FontWeight, Global, Pixels,
@@ -117,7 +118,9 @@ pub struct ThemeSettings {
     /// Manual overrides for the active theme.
     ///
     /// Note: This setting is still experimental. See [this tracking issue](https://github.com/zed-industries/zed/issues/18078)
-    pub theme_overrides: Option<ThemeStyleContent>,
+    pub experimental_theme_overrides: Option<ThemeStyleContent>,
+    /// Manual overrides per theme
+    pub theme_overrides: HashMap<String, ThemeStyleContent>,
     /// The current icon theme selection.
     pub icon_theme_selection: Option<IconThemeSelection>,
     /// The active icon theme.
@@ -425,7 +428,13 @@ pub struct ThemeSettingsContent {
     ///
     /// These values will override the ones on the current theme specified in `theme`.
     #[serde(rename = "experimental.theme_overrides", default)]
-    pub theme_overrides: Option<ThemeStyleContent>,
+    pub experimental_theme_overrides: Option<ThemeStyleContent>,
+
+    /// Overrides per theme
+    ///
+    /// These values will override the ones on the specified theme
+    #[serde(default)]
+    pub theme_overrides: HashMap<String, ThemeStyleContent>,
 }
 
 fn default_font_features() -> Option<FontFeatures> {
@@ -647,30 +656,39 @@ impl ThemeSettings {
 
     /// Applies the theme overrides, if there are any, to the current theme.
     pub fn apply_theme_overrides(&mut self) {
-        if let Some(theme_overrides) = &self.theme_overrides {
-            let mut base_theme = (*self.active_theme).clone();
+        // Apply the old overrides setting first, so that the new setting can override those.
+        if let Some(experimental_theme_overrides) = &self.experimental_theme_overrides {
+            let mut theme = (*self.active_theme).clone();
+            ThemeSettings::modify_theme(&mut theme, experimental_theme_overrides);
+            self.active_theme = Arc::new(theme);
+        }
 
-            if let Some(window_background_appearance) = theme_overrides.window_background_appearance
-            {
-                base_theme.styles.window_background_appearance =
-                    window_background_appearance.into();
-            }
+        if let Some(theme_overrides) = self.theme_overrides.get(self.active_theme.name.as_ref()) {
+            let mut theme = (*self.active_theme).clone();
+            ThemeSettings::modify_theme(&mut theme, theme_overrides);
+            self.active_theme = Arc::new(theme);
+        }
+    }
 
-            base_theme
-                .styles
-                .colors
-                .refine(&theme_overrides.theme_colors_refinement());
-            base_theme
-                .styles
-                .status
-                .refine(&theme_overrides.status_colors_refinement());
-            base_theme.styles.player.merge(&theme_overrides.players);
-            base_theme.styles.accents.merge(&theme_overrides.accents);
-            base_theme.styles.syntax =
-                SyntaxTheme::merge(base_theme.styles.syntax, theme_overrides.syntax_overrides());
-
-            self.active_theme = Arc::new(base_theme);
+    fn modify_theme(base_theme: &mut Theme, theme_overrides: &ThemeStyleContent) {
+        if let Some(window_background_appearance) = theme_overrides.window_background_appearance {
+            base_theme.styles.window_background_appearance = window_background_appearance.into();
         }
+
+        base_theme
+            .styles
+            .colors
+            .refine(&theme_overrides.theme_colors_refinement());
+        base_theme
+            .styles
+            .status
+            .refine(&theme_overrides.status_colors_refinement());
+        base_theme.styles.player.merge(&theme_overrides.players);
+        base_theme.styles.accents.merge(&theme_overrides.accents);
+        base_theme.styles.syntax = SyntaxTheme::merge(
+            base_theme.styles.syntax.clone(),
+            theme_overrides.syntax_overrides(),
+        );
     }
 
     /// Switches to the icon theme with the given name, if it exists.
@@ -848,7 +866,8 @@ impl settings::Settings for ThemeSettings {
                 .get(defaults.theme.as_ref().unwrap().theme(*system_appearance))
                 .or(themes.get(&zed_default_dark().name))
                 .unwrap(),
-            theme_overrides: None,
+            experimental_theme_overrides: None,
+            theme_overrides: HashMap::default(),
             icon_theme_selection: defaults.icon_theme.clone(),
             active_icon_theme: defaults
                 .icon_theme
@@ -918,6 +937,8 @@ impl settings::Settings for ThemeSettings {
                 }
             }
 
+            this.experimental_theme_overrides
+                .clone_from(&value.experimental_theme_overrides);
             this.theme_overrides.clone_from(&value.theme_overrides);
             this.apply_theme_overrides();