From f5f7be15001ecd3a332f66aea4783879a3b5b05b Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 26 Jan 2024 13:42:58 -0500 Subject: [PATCH] Add `experimental.theme_overrides` to settings file (#6791) This PR adds **experimental** support for overriding theme values in the current theme. Be advised that both the existence of this setting and the structure of the theme itself are subject to change. But this is a first step towards allowing Zed users to customize or bring their own themes. ### How it works There is a new `experimental.theme_overrides` setting in `settings.json`. This accepts an object containing overrides for values in the theme. All values are optional, and will be overlaid on top of whatever theme you currently have set by the `theme` field. There is JSON schema support to show which values are supported. ### Example Here's an example of it in action: https://github.com/zed-industries/zed/assets/1486634/173b94b1-4d88-4333-b980-8fed937e6f6d Release Notes: - Added `experimental.theme_overrides` to `settings.json` to allow for customizing the current theme. - This setting is experimental and subject to change. --- Cargo.lock | 2 + crates/theme/Cargo.toml | 5 +- crates/theme/src/schema.rs | 1133 +++++++++++++++++++ crates/theme/src/settings.rs | 41 +- crates/theme/src/theme.rs | 3 + crates/theme_selector/src/theme_selector.rs | 1 + 6 files changed, 1182 insertions(+), 3 deletions(-) create mode 100644 crates/theme/src/schema.rs diff --git a/Cargo.lock b/Cargo.lock index 870fd9a320..82aae22dca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6524,6 +6524,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f7b0ce13155372a76ee2e1c5ffba1fe61ede73fbea5630d61eee6fac4929c0c" dependencies = [ "dyn-clone", + "indexmap 1.9.3", "schemars_derive", "serde", "serde_json", @@ -7856,6 +7857,7 @@ dependencies = [ "gpui", "indexmap 1.9.3", "itertools 0.11.0", + "palette", "parking_lot 0.11.2", "refineable", "schemars", diff --git a/crates/theme/Cargo.toml b/crates/theme/Cargo.toml index 41617550ee..48f57f75b2 100644 --- a/crates/theme/Cargo.toml +++ b/crates/theme/Cargo.toml @@ -24,10 +24,11 @@ doctest = false anyhow.workspace = true fs = { path = "../fs" } gpui = { path = "../gpui" } -indexmap = "1.6.2" +indexmap = { version = "1.6.2", features = ["serde"] } +palette = { version = "0.7.3", default-features = false, features = ["std"] } parking_lot.workspace = true refineable.workspace = true -schemars.workspace = true +schemars = { workspace = true, features = ["indexmap"] } serde.workspace = true serde_derive.workspace = true serde_json.workspace = true diff --git a/crates/theme/src/schema.rs b/crates/theme/src/schema.rs new file mode 100644 index 0000000000..6b34c2075c --- /dev/null +++ b/crates/theme/src/schema.rs @@ -0,0 +1,1133 @@ +use anyhow::Result; +use gpui::{HighlightStyle, Hsla}; +use indexmap::IndexMap; +use palette::FromColor; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{StatusColorsRefinement, ThemeColorsRefinement}; + +fn try_parse_color(color: &str) -> Result { + 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); + + let hsla = gpui::hsla( + hsla.hue.into_positive_degrees() / 360., + hsla.saturation, + hsla.lightness, + hsla.alpha, + ); + + Ok(hsla) +} + +/// The content of a serialized theme. +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] +#[serde(default)] +pub struct ThemeContent { + #[serde(flatten, default)] + pub colors: ThemeColorsContent, + + #[serde(flatten, default)] + pub status: StatusColorsContent, + + /// The styles for syntax nodes. + #[serde(default)] + pub syntax: IndexMap, +} + +impl ThemeContent { + /// Returns a [`ThemeColorsRefinement`] based on the colors in the [`ThemeContent`]. + #[inline(always)] + pub fn theme_colors_refinement(&self) -> ThemeColorsRefinement { + self.colors.theme_colors_refinement() + } + + /// Returns a [`StatusColorsRefinement`] based on the colors in the [`ThemeContent`]. + #[inline(always)] + pub fn status_colors_refinement(&self) -> StatusColorsRefinement { + self.status.status_colors_refinement() + } + + /// Returns the syntax style overrides in the [`ThemeContent`]. + pub fn syntax_overrides(&self) -> Vec<(String, HighlightStyle)> { + self.syntax + .iter() + .map(|(key, style)| { + ( + key.clone(), + HighlightStyle { + color: style + .color + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + ..Default::default() + }, + ) + }) + .collect() + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] +#[serde(default)] +pub struct ThemeColorsContent { + /// Border color. Used for most borders, is usually a high contrast color. + #[serde(rename = "border")] + pub border: Option, + + /// Border color. Used for deemphasized borders, like a visual divider between two sections + #[serde(rename = "border.variant")] + pub border_variant: Option, + + /// Border color. Used for focused elements, like keyboard focused list item. + #[serde(rename = "border.focused")] + pub border_focused: Option, + + /// Border color. Used for selected elements, like an active search filter or selected checkbox. + #[serde(rename = "border.selected")] + pub border_selected: Option, + + /// Border color. Used for transparent borders. Used for placeholder borders when an element gains a border on state change. + #[serde(rename = "border.transparent")] + pub border_transparent: Option, + + /// Border color. Used for disabled elements, like a disabled input or button. + #[serde(rename = "border.disabled")] + pub border_disabled: Option, + + /// Border color. Used for elevated surfaces, like a context menu, popup, or dialog. + #[serde(rename = "elevated_surface.background")] + pub elevated_surface_background: Option, + + /// Background Color. Used for grounded surfaces like a panel or tab. + #[serde(rename = "surface.background")] + pub surface_background: Option, + + /// Background Color. Used for the app background and blank panels or windows. + #[serde(rename = "background")] + pub background: Option, + + /// Background Color. Used for the background of an element that should have a different background than the surface it's on. + /// + /// Elements might include: Buttons, Inputs, Checkboxes, Radio Buttons... + /// + /// For an element that should have the same background as the surface it's on, use `ghost_element_background`. + #[serde(rename = "element.background")] + pub element_background: Option, + + /// Background Color. Used for the hover state of an element that should have a different background than the surface it's on. + /// + /// Hover states are triggered by the mouse entering an element, or a finger touching an element on a touch screen. + #[serde(rename = "element.hover")] + pub element_hover: Option, + + /// Background Color. Used for the active state of an element that should have a different background than the surface it's on. + /// + /// Active states are triggered by the mouse button being pressed down on an element, or the Return button or other activator being pressd. + #[serde(rename = "element.active")] + pub element_active: Option, + + /// Background Color. Used for the selected state of an element that should have a different background than the surface it's on. + /// + /// Selected states are triggered by the element being selected (or "activated") by the user. + /// + /// This could include a selected checkbox, a toggleable button that is toggled on, etc. + #[serde(rename = "element.selected")] + pub element_selected: Option, + + /// Background Color. Used for the disabled state of an element that should have a different background than the surface it's on. + /// + /// Disabled states are shown when a user cannot interact with an element, like a disabled button or input. + #[serde(rename = "element.disabled")] + pub element_disabled: Option, + + /// Background Color. Used for the area that shows where a dragged element will be dropped. + #[serde(rename = "drop_target.background")] + pub drop_target_background: Option, + + /// Used for the background of a ghost element that should have the same background as the surface it's on. + /// + /// Elements might include: Buttons, Inputs, Checkboxes, Radio Buttons... + /// + /// For an element that should have a different background than the surface it's on, use `element_background`. + #[serde(rename = "ghost_element.background")] + pub ghost_element_background: Option, + + /// Background Color. Used for the hover state of a ghost element that should have the same background as the surface it's on. + /// + /// Hover states are triggered by the mouse entering an element, or a finger touching an element on a touch screen. + #[serde(rename = "ghost_element.hover")] + pub ghost_element_hover: Option, + + /// Background Color. Used for the active state of a ghost element that should have the same background as the surface it's on. + /// + /// Active states are triggered by the mouse button being pressed down on an element, or the Return button or other activator being pressd. + #[serde(rename = "ghost_element.active")] + pub ghost_element_active: Option, + + /// Background Color. Used for the selected state of a ghost element that should have the same background as the surface it's on. + /// + /// Selected states are triggered by the element being selected (or "activated") by the user. + /// + /// This could include a selected checkbox, a toggleable button that is toggled on, etc. + #[serde(rename = "ghost_element.selected")] + pub ghost_element_selected: Option, + + /// Background Color. Used for the disabled state of a ghost element that should have the same background as the surface it's on. + /// + /// Disabled states are shown when a user cannot interact with an element, like a disabled button or input. + #[serde(rename = "ghost_element.disabled")] + pub ghost_element_disabled: Option, + + /// Text Color. Default text color used for most text. + #[serde(rename = "text")] + pub text: Option, + + /// Text Color. Color of muted or deemphasized text. It is a subdued version of the standard text color. + #[serde(rename = "text.muted")] + pub text_muted: Option, + + /// Text Color. Color of the placeholder text typically shown in input fields to guide the user to enter valid data. + #[serde(rename = "text.placeholder")] + pub text_placeholder: Option, + + /// Text Color. Color used for text denoting disabled elements. Typically, the color is faded or grayed out to emphasize the disabled state. + #[serde(rename = "text.disabled")] + pub text_disabled: Option, + + /// Text Color. Color used for emphasis or highlighting certain text, like an active filter or a matched character in a search. + #[serde(rename = "text.accent")] + pub text_accent: Option, + + /// Fill Color. Used for the default fill color of an icon. + #[serde(rename = "icon")] + pub icon: Option, + + /// Fill Color. Used for the muted or deemphasized fill color of an icon. + /// + /// This might be used to show an icon in an inactive pane, or to demphasize a series of icons to give them less visual weight. + #[serde(rename = "icon.muted")] + pub icon_muted: Option, + + /// Fill Color. Used for the disabled fill color of an icon. + /// + /// Disabled states are shown when a user cannot interact with an element, like a icon button. + #[serde(rename = "icon.disabled")] + pub icon_disabled: Option, + + /// Fill Color. Used for the placeholder fill color of an icon. + /// + /// This might be used to show an icon in an input that disappears when the user enters text. + #[serde(rename = "icon.placeholder")] + pub icon_placeholder: Option, + + /// Fill Color. Used for the accent fill color of an icon. + /// + /// This might be used to show when a toggleable icon button is selected. + #[serde(rename = "con.accent")] + pub icon_accent: Option, + + #[serde(rename = "status_bar.background")] + pub status_bar_background: Option, + + #[serde(rename = "title_bar.background")] + pub title_bar_background: Option, + + #[serde(rename = "toolbar.background")] + pub toolbar_background: Option, + + #[serde(rename = "tab_bar.background")] + pub tab_bar_background: Option, + + #[serde(rename = "tab.inactive_background")] + pub tab_inactive_background: Option, + + #[serde(rename = "tab.active_background")] + pub tab_active_background: Option, + + #[serde(rename = "search.match_background")] + pub search_match_background: Option, + + #[serde(rename = "panel.background")] + pub panel_background: Option, + + #[serde(rename = "panel.focused_border")] + pub panel_focused_border: Option, + + #[serde(rename = "pane.focused_border")] + pub pane_focused_border: Option, + + /// The color of the scrollbar thumb. + #[serde(rename = "scrollbar_thumb.background")] + pub scrollbar_thumb_background: Option, + + /// The color of the scrollbar thumb when hovered over. + #[serde(rename = "scrollbar.thumb.hover_background")] + pub scrollbar_thumb_hover_background: Option, + + /// The border color of the scrollbar thumb. + #[serde(rename = "scrollbar.thumb.border")] + pub scrollbar_thumb_border: Option, + + /// The background color of the scrollbar track. + #[serde(rename = "scrollbar.track.background")] + pub scrollbar_track_background: Option, + + /// The border color of the scrollbar track. + #[serde(rename = "scrollbar.track.border")] + pub scrollbar_track_border: Option, + + #[serde(rename = "editor.foreground")] + pub editor_foreground: Option, + + #[serde(rename = "editor.background")] + pub editor_background: Option, + + #[serde(rename = "editor.gutter.background")] + pub editor_gutter_background: Option, + + #[serde(rename = "editor.subheader.background")] + pub editor_subheader_background: Option, + + #[serde(rename = "editor.active_line.background")] + pub editor_active_line_background: Option, + + #[serde(rename = "editor.highlighted_line.background")] + pub editor_highlighted_line_background: Option, + + /// Text Color. Used for the text of the line number in the editor gutter. + #[serde(rename = "editor.line_number")] + pub editor_line_number: Option, + + /// Text Color. Used for the text of the line number in the editor gutter when the line is highlighted. + #[serde(rename = "editor.active_line_number")] + pub editor_active_line_number: Option, + + /// Text Color. Used to mark invisible characters in the editor. + /// + /// Example: spaces, tabs, carriage returns, etc. + #[serde(rename = "editor.invisible")] + pub editor_invisible: Option, + + #[serde(rename = "editor.wrap_guide")] + pub editor_wrap_guide: Option, + + #[serde(rename = "editor.active_wrap_guide")] + pub editor_active_wrap_guide: Option, + + /// Read-access of a symbol, like reading a variable. + /// + /// A document highlight is a range inside a text document which deserves + /// special attention. Usually a document highlight is visualized by changing + /// the background color of its range. + #[serde(rename = "editor.document_highlight.read_background")] + pub editor_document_highlight_read_background: Option, + + /// Read-access of a symbol, like reading a variable. + /// + /// A document highlight is a range inside a text document which deserves + /// special attention. Usually a document highlight is visualized by changing + /// the background color of its range. + #[serde(rename = "editor.document_highlight.write_background")] + pub editor_document_highlight_write_background: Option, + + /// Terminal background color. + #[serde(rename = "terminal.background")] + pub terminal_background: Option, + + /// Terminal foreground color. + #[serde(rename = "terminal.foreground")] + pub terminal_foreground: Option, + + /// Bright terminal foreground color. + #[serde(rename = "terminal.bright_foreground")] + pub terminal_bright_foreground: Option, + + /// Dim terminal foreground color. + #[serde(rename = "terminal.dim_foreground")] + pub terminal_dim_foreground: Option, + + /// Black ANSI terminal color. + #[serde(rename = "terminal.ansi.black")] + pub terminal_ansi_black: Option, + + /// Bright black ANSI terminal color. + #[serde(rename = "terminal.ansi.bright_black")] + pub terminal_ansi_bright_black: Option, + + /// Dim black ANSI terminal color. + #[serde(rename = "terminal.ansi.dim_black")] + pub terminal_ansi_dim_black: Option, + + /// Red ANSI terminal color. + #[serde(rename = "terminal.ansi.red")] + pub terminal_ansi_red: Option, + + /// Bright red ANSI terminal color. + #[serde(rename = "terminal.ansi.bright_red")] + pub terminal_ansi_bright_red: Option, + + /// Dim red ANSI terminal color. + #[serde(rename = "terminal.ansi.dim_red")] + pub terminal_ansi_dim_red: Option, + + /// Green ANSI terminal color. + #[serde(rename = "terminal.ansi.green")] + pub terminal_ansi_green: Option, + + /// Bright green ANSI terminal color. + #[serde(rename = "terminal.ansi.bright_green")] + pub terminal_ansi_bright_green: Option, + + /// Dim green ANSI terminal color. + #[serde(rename = "terminal.ansi.dim_green")] + pub terminal_ansi_dim_green: Option, + + /// Yellow ANSI terminal color. + #[serde(rename = "terminal.ansi.yellow")] + pub terminal_ansi_yellow: Option, + + /// Bright yellow ANSI terminal color. + #[serde(rename = "terminal.ansi.bright_yellow")] + pub terminal_ansi_bright_yellow: Option, + + /// Dim yellow ANSI terminal color. + #[serde(rename = "terminal.ansi.dim_yellow")] + pub terminal_ansi_dim_yellow: Option, + + /// Blue ANSI terminal color. + #[serde(rename = "terminal.ansi.blue")] + pub terminal_ansi_blue: Option, + + /// Bright blue ANSI terminal color. + #[serde(rename = "terminal.ansi.bright_blue")] + pub terminal_ansi_bright_blue: Option, + + /// Dim blue ANSI terminal color. + #[serde(rename = "terminal.ansi.dim_blue")] + pub terminal_ansi_dim_blue: Option, + + /// Magenta ANSI terminal color. + #[serde(rename = "terminal.ansi.magenta")] + pub terminal_ansi_magenta: Option, + + /// Bright magenta ANSI terminal color. + #[serde(rename = "terminal.ansi.bright_magenta")] + pub terminal_ansi_bright_magenta: Option, + + /// Dim magenta ANSI terminal color. + #[serde(rename = "terminal.ansi.dim_magenta")] + pub terminal_ansi_dim_magenta: Option, + + /// Cyan ANSI terminal color. + #[serde(rename = "terminal.ansi.cyan")] + pub terminal_ansi_cyan: Option, + + /// Bright cyan ANSI terminal color. + #[serde(rename = "terminal.ansi.bright_cyan")] + pub terminal_ansi_bright_cyan: Option, + + /// Dim cyan ANSI terminal color. + #[serde(rename = "terminal.ansi.dim_cyan")] + pub terminal_ansi_dim_cyan: Option, + + /// White ANSI terminal color. + #[serde(rename = "terminal.ansi.white")] + pub terminal_ansi_white: Option, + + /// Bright white ANSI terminal color. + #[serde(rename = "terminal.ansi.bright_white")] + pub terminal_ansi_bright_white: Option, + + /// Dim white ANSI terminal color. + #[serde(rename = "terminal.ansi.dim_white")] + pub terminal_ansi_dim_white: Option, + + #[serde(rename = "link_text.hover")] + pub link_text_hover: Option, +} + +impl ThemeColorsContent { + /// Returns a [`ThemeColorsRefinement`] based on the colors in the [`ThemeColorsContent`]. + pub fn theme_colors_refinement(&self) -> ThemeColorsRefinement { + ThemeColorsRefinement { + border: self + .border + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + border_variant: self + .border_variant + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + border_focused: self + .border_focused + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + border_selected: self + .border_selected + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + border_transparent: self + .border_transparent + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + border_disabled: self + .border_disabled + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + elevated_surface_background: self + .elevated_surface_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + surface_background: self + .surface_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + background: self + .background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + element_background: self + .element_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + element_hover: self + .element_hover + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + element_active: self + .element_active + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + element_selected: self + .element_selected + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + element_disabled: self + .element_disabled + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + drop_target_background: self + .drop_target_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + ghost_element_background: self + .ghost_element_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + ghost_element_hover: self + .ghost_element_hover + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + ghost_element_active: self + .ghost_element_active + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + ghost_element_selected: self + .ghost_element_selected + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + ghost_element_disabled: self + .ghost_element_disabled + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + text: self + .text + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + text_muted: self + .text_muted + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + text_placeholder: self + .text_placeholder + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + text_disabled: self + .text_disabled + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + text_accent: self + .text_accent + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + icon: self + .icon + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + icon_muted: self + .icon_muted + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + icon_disabled: self + .icon_disabled + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + icon_placeholder: self + .icon_placeholder + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + icon_accent: self + .icon_accent + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + status_bar_background: self + .status_bar_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + title_bar_background: self + .title_bar_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + toolbar_background: self + .toolbar_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + tab_bar_background: self + .tab_bar_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + tab_inactive_background: self + .tab_inactive_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + tab_active_background: self + .tab_active_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + search_match_background: self + .search_match_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + panel_background: self + .panel_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + panel_focused_border: self + .panel_focused_border + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + pane_focused_border: self + .pane_focused_border + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + scrollbar_thumb_background: self + .scrollbar_thumb_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + scrollbar_thumb_hover_background: self + .scrollbar_thumb_hover_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + scrollbar_thumb_border: self + .scrollbar_thumb_border + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + scrollbar_track_background: self + .scrollbar_track_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + scrollbar_track_border: self + .scrollbar_track_border + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + editor_foreground: self + .editor_foreground + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + editor_background: self + .editor_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + editor_gutter_background: self + .editor_gutter_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + editor_subheader_background: self + .editor_subheader_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + editor_active_line_background: self + .editor_active_line_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + editor_highlighted_line_background: self + .editor_highlighted_line_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + editor_line_number: self + .editor_line_number + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + editor_active_line_number: self + .editor_active_line_number + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + editor_invisible: self + .editor_invisible + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + editor_wrap_guide: self + .editor_wrap_guide + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + editor_active_wrap_guide: self + .editor_active_wrap_guide + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + editor_document_highlight_read_background: self + .editor_document_highlight_read_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + editor_document_highlight_write_background: self + .editor_document_highlight_write_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_background: self + .terminal_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_foreground: self + .terminal_foreground + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_bright_foreground: self + .terminal_bright_foreground + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_dim_foreground: self + .terminal_dim_foreground + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_ansi_black: self + .terminal_ansi_black + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_ansi_bright_black: self + .terminal_ansi_bright_black + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_ansi_dim_black: self + .terminal_ansi_dim_black + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_ansi_red: self + .terminal_ansi_red + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_ansi_bright_red: self + .terminal_ansi_bright_red + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_ansi_dim_red: self + .terminal_ansi_dim_red + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_ansi_green: self + .terminal_ansi_green + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_ansi_bright_green: self + .terminal_ansi_bright_green + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_ansi_dim_green: self + .terminal_ansi_dim_green + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_ansi_yellow: self + .terminal_ansi_yellow + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_ansi_bright_yellow: self + .terminal_ansi_bright_yellow + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_ansi_dim_yellow: self + .terminal_ansi_dim_yellow + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_ansi_blue: self + .terminal_ansi_blue + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_ansi_bright_blue: self + .terminal_ansi_bright_blue + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_ansi_dim_blue: self + .terminal_ansi_dim_blue + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_ansi_magenta: self + .terminal_ansi_magenta + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_ansi_bright_magenta: self + .terminal_ansi_bright_magenta + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_ansi_dim_magenta: self + .terminal_ansi_dim_magenta + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_ansi_cyan: self + .terminal_ansi_cyan + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_ansi_bright_cyan: self + .terminal_ansi_bright_cyan + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_ansi_dim_cyan: self + .terminal_ansi_dim_cyan + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_ansi_white: self + .terminal_ansi_white + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_ansi_bright_white: self + .terminal_ansi_bright_white + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + terminal_ansi_dim_white: self + .terminal_ansi_dim_white + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + link_text_hover: self + .link_text_hover + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + } + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] +#[serde(default)] +pub struct StatusColorsContent { + /// Indicates some kind of conflict, like a file changed on disk while it was open, or + /// merge conflicts in a Git repository. + #[serde(rename = "conflict")] + pub conflict: Option, + + #[serde(rename = "conflict.background")] + pub conflict_background: Option, + + #[serde(rename = "conflict.border")] + pub conflict_border: Option, + + /// Indicates something new, like a new file added to a Git repository. + #[serde(rename = "created")] + pub created: Option, + + #[serde(rename = "created.background")] + pub created_background: Option, + + #[serde(rename = "created.border")] + pub created_border: Option, + + /// Indicates that something no longer exists, like a deleted file. + #[serde(rename = "deleted")] + pub deleted: Option, + + #[serde(rename = "deleted.background")] + pub deleted_background: Option, + + #[serde(rename = "deleted.border")] + pub deleted_border: Option, + + /// Indicates a system error, a failed operation or a diagnostic error. + #[serde(rename = "error")] + pub error: Option, + + #[serde(rename = "error.background")] + pub error_background: Option, + + #[serde(rename = "error.border")] + pub error_border: Option, + + /// Represents a hidden status, such as a file being hidden in a file tree. + #[serde(rename = "hidden")] + pub hidden: Option, + + #[serde(rename = "hidden.background")] + pub hidden_background: Option, + + #[serde(rename = "hidden.border")] + pub hidden_border: Option, + + /// Indicates a hint or some kind of additional information. + #[serde(rename = "hint")] + pub hint: Option, + + #[serde(rename = "hint.background")] + pub hint_background: Option, + + #[serde(rename = "hint.border")] + pub hint_border: Option, + + /// Indicates that something is deliberately ignored, such as a file or operation ignored by Git. + #[serde(rename = "ignored")] + pub ignored: Option, + + #[serde(rename = "ignored.background")] + pub ignored_background: Option, + + #[serde(rename = "ignored.border")] + pub ignored_border: Option, + + /// Represents informational status updates or messages. + #[serde(rename = "info")] + pub info: Option, + + #[serde(rename = "info.background")] + pub info_background: Option, + + #[serde(rename = "info.border")] + pub info_border: Option, + + /// Indicates a changed or altered status, like a file that has been edited. + #[serde(rename = "modified")] + pub modified: Option, + + #[serde(rename = "modified.background")] + pub modified_background: Option, + + #[serde(rename = "modified.border")] + pub modified_border: Option, + + /// Indicates something that is predicted, like automatic code completion, or generated code. + #[serde(rename = "predictive")] + pub predictive: Option, + + #[serde(rename = "predictive.background")] + pub predictive_background: Option, + + #[serde(rename = "predictive.border")] + pub predictive_border: Option, + + /// Represents a renamed status, such as a file that has been renamed. + #[serde(rename = "renamed")] + pub renamed: Option, + + #[serde(rename = "renamed.background")] + pub renamed_background: Option, + + #[serde(rename = "renamed.border")] + pub renamed_border: Option, + + /// Indicates a successful operation or task completion. + #[serde(rename = "success")] + pub success: Option, + + #[serde(rename = "success.background")] + pub success_background: Option, + + #[serde(rename = "success.border")] + pub success_border: Option, + + /// Indicates some kind of unreachable status, like a block of code that can never be reached. + #[serde(rename = "unreachable")] + pub unreachable: Option, + + #[serde(rename = "unreachable.background")] + pub unreachable_background: Option, + + #[serde(rename = "unreachable.border")] + pub unreachable_border: Option, + + /// Represents a warning status, like an operation that is about to fail. + #[serde(rename = "warning")] + pub warning: Option, + + #[serde(rename = "warning.background")] + pub warning_background: Option, + + #[serde(rename = "warning.border")] + pub warning_border: Option, +} + +impl StatusColorsContent { + /// Returns a [`StatusColorsRefinement`] based on the colors in the [`StatusColorsContent`]. + pub fn status_colors_refinement(&self) -> StatusColorsRefinement { + StatusColorsRefinement { + conflict: self + .conflict + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + conflict_background: self + .conflict_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + conflict_border: self + .conflict_border + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + created: self + .created + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + created_background: self + .created_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + created_border: self + .created_border + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + deleted: self + .deleted + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + deleted_background: self + .deleted_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + deleted_border: self + .deleted_border + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + error: self + .error + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + error_background: self + .error_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + error_border: self + .error_border + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + hidden: self + .hidden + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + hidden_background: self + .hidden_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + hidden_border: self + .hidden_border + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + hint: self + .hint + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + hint_background: self + .hint_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + hint_border: self + .hint_border + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + ignored: self + .ignored + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + ignored_background: self + .ignored_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + ignored_border: self + .ignored_border + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + info: self + .info + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + info_background: self + .info_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + info_border: self + .info_border + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + modified: self + .modified + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + modified_background: self + .modified_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + modified_border: self + .modified_border + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + predictive: self + .predictive + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + predictive_background: self + .predictive_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + predictive_border: self + .predictive_border + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + renamed: self + .renamed + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + renamed_background: self + .renamed_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + renamed_border: self + .renamed_border + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + success: self + .success + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + success_background: self + .success_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + success_border: self + .success_border + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + unreachable: self + .unreachable + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + unreachable_background: self + .unreachable_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + unreachable_border: self + .unreachable_border + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + warning: self + .warning + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + warning_background: self + .warning_background + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + warning_border: self + .warning_border + .as_ref() + .and_then(|color| try_parse_color(&color).ok()), + } + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] +#[serde(default)] +pub struct HighlightStyleContent { + pub color: Option, +} diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index bced187411..0a3c3fd168 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -1,9 +1,10 @@ use crate::one_themes::one_dark; -use crate::{Theme, ThemeRegistry}; +use crate::{SyntaxTheme, Theme, ThemeContent, ThemeRegistry}; use anyhow::Result; use gpui::{ px, AppContext, Font, FontFeatures, FontStyle, FontWeight, Pixels, Subscription, ViewContext, }; +use refineable::Refineable; use schemars::{ gen::SchemaGenerator, schema::{InstanceType, Schema, SchemaObject}, @@ -26,6 +27,7 @@ pub struct ThemeSettings { pub buffer_font_size: Pixels, pub buffer_line_height: BufferLineHeight, pub active_theme: Arc, + pub theme_overrides: Option, } #[derive(Default)] @@ -49,6 +51,12 @@ pub struct ThemeSettingsContent { pub buffer_font_features: Option, #[serde(default)] pub theme: Option, + + /// EXPERIMENTAL: Overrides for the current theme. + /// + /// These values will override the ones on the current theme specified in `theme`. + #[serde(rename = "experimental.theme_overrides", default)] + pub theme_overrides: Option, } #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)] @@ -80,6 +88,33 @@ impl ThemeSettings { pub fn line_height(&self) -> f32 { f32::max(self.buffer_line_height.value(), MIN_LINE_HEIGHT) } + + /// 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(); + + base_theme + .styles + .colors + .refine(&theme_overrides.theme_colors_refinement()); + base_theme + .styles + .status + .refine(&theme_overrides.status_colors_refinement()); + base_theme.styles.syntax = Arc::new(SyntaxTheme { + highlights: { + let mut highlights = base_theme.styles.syntax.highlights.clone(); + // Overrides come second in the highlight list so that they take precedence + // over the ones in the base theme. + highlights.extend(theme_overrides.syntax_overrides()); + highlights + }, + }); + + self.active_theme = Arc::new(base_theme); + } + } } pub fn observe_buffer_font_size_adjustment( @@ -151,6 +186,7 @@ impl settings::Settings for ThemeSettings { .get(defaults.theme.as_ref().unwrap()) .or(themes.get(&one_dark().name)) .unwrap(), + theme_overrides: None, }; for value in user_values.into_iter().copied().cloned() { @@ -174,6 +210,9 @@ impl settings::Settings for ThemeSettings { } } + this.theme_overrides = value.theme_overrides; + this.apply_theme_overrides(); + merge(&mut this.ui_font_size, value.ui_font_size.map(Into::into)); merge( &mut this.buffer_font_size, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 92a5877f51..b642c835e8 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -12,6 +12,7 @@ mod one_themes; pub mod prelude; mod registry; mod scale; +mod schema; mod settings; mod styles; #[cfg(not(feature = "importing-themes"))] @@ -25,6 +26,7 @@ pub use default_colors::*; pub use default_theme::*; pub use registry::*; pub use scale::*; +pub use schema::*; pub use settings::*; pub use styles::*; #[cfg(not(feature = "importing-themes"))] @@ -99,6 +101,7 @@ pub struct ThemeFamily { impl ThemeFamily {} +#[derive(Clone)] pub struct Theme { pub id: String, pub name: SharedString, diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index df66c746de..14251c4a53 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -159,6 +159,7 @@ impl ThemeSelectorDelegate { cx.update_global(|store: &mut SettingsStore, cx| { let mut theme_settings = store.get::(None).clone(); theme_settings.active_theme = theme; + theme_settings.apply_theme_overrides(); store.override_global(theme_settings); cx.refresh(); });