From a80a3b8706b83e70476ed4a89252b3324edfb391 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 5 Feb 2024 15:39:01 -0500 Subject: [PATCH] Add support for specifying both light and dark themes in `settings.json` (#7404) This PR adds support for configuring both a light and dark theme in `settings.json`. In addition to accepting just a theme name, the `theme` field now also accepts an object in the following form: ```jsonc { "theme": { "mode": "system", "light": "One Light", "dark": "One Dark" } } ``` Both `light` and `dark` are required, and indicate which theme should be used when the system is in light mode and dark mode, respectively. The `mode` field is optional and indicates which theme should be used: - `"system"` - Use the theme that corresponds to the system's appearance. - `"light"` - Use the theme indicated by the `light` field. - `"dark"` - Use the theme indicated by the `dark` field. Thank you to @Yesterday17 for taking a first stab at this in #6881! Release Notes: - Added support for configuring both a light and dark theme and switching between them based on system preference. --- .../incoming_call_notification.rs | 4 +- .../project_shared_notification.rs | 4 +- crates/theme/src/settings.rs | 112 ++++++++++++++++-- crates/theme/src/theme.rs | 11 +- crates/theme_selector/src/theme_selector.rs | 24 +++- crates/workspace/src/workspace.rs | 19 ++- crates/zed/src/main.rs | 17 ++- 7 files changed, 167 insertions(+), 24 deletions(-) diff --git a/crates/collab_ui/src/notifications/incoming_call_notification.rs b/crates/collab_ui/src/notifications/incoming_call_notification.rs index f66194c52a..12662fe6cb 100644 --- a/crates/collab_ui/src/notifications/incoming_call_notification.rs +++ b/crates/collab_ui/src/notifications/incoming_call_notification.rs @@ -5,7 +5,7 @@ use futures::StreamExt; use gpui::{prelude::*, AppContext, WindowHandle}; use settings::Settings; use std::sync::{Arc, Weak}; -use theme::ThemeSettings; +use theme::{SystemAppearance, ThemeSettings}; use ui::{prelude::*, Button, Label}; use util::ResultExt; use workspace::AppState; @@ -35,6 +35,8 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { let options = notification_window_options(screen, window_size); let window = cx .open_window(options, |cx| { + SystemAppearance::init_for_window(cx); + cx.new_view(|_| { IncomingCallNotification::new( incoming_call.clone(), diff --git a/crates/collab_ui/src/notifications/project_shared_notification.rs b/crates/collab_ui/src/notifications/project_shared_notification.rs index b8ceefcd76..bb70fc9571 100644 --- a/crates/collab_ui/src/notifications/project_shared_notification.rs +++ b/crates/collab_ui/src/notifications/project_shared_notification.rs @@ -6,7 +6,7 @@ use collections::HashMap; use gpui::{AppContext, Size}; use settings::Settings; use std::sync::{Arc, Weak}; -use theme::ThemeSettings; +use theme::{SystemAppearance, ThemeSettings}; use ui::{prelude::*, Button, Label}; use workspace::AppState; @@ -28,6 +28,8 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { for screen in cx.displays() { let options = notification_window_options(screen, window_size); let window = cx.open_window(options, |cx| { + SystemAppearance::init_for_window(cx); + cx.new_view(|_| { ProjectSharedNotification::new( owner.clone(), diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index f7157fa139..17404d7c67 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::{SyntaxTheme, Theme, ThemeRegistry, ThemeStyleContent}; +use crate::{Appearance, SyntaxTheme, Theme, ThemeRegistry, ThemeStyleContent}; use anyhow::Result; +use derive_more::{Deref, DerefMut}; use gpui::{ px, AppContext, Font, FontFeatures, FontStyle, FontWeight, Global, Pixels, Subscription, - ViewContext, + ViewContext, WindowContext, }; use refineable::Refineable; use schemars::{ @@ -27,16 +28,104 @@ pub struct ThemeSettings { pub buffer_font: Font, pub buffer_font_size: Pixels, pub buffer_line_height: BufferLineHeight, - pub requested_theme: Option, + pub theme_selection: Option, pub active_theme: Arc, pub theme_overrides: Option, } +/// The appearance of the system. +#[derive(Debug, Clone, Copy, Deref)] +pub struct SystemAppearance(pub Appearance); + +impl Default for SystemAppearance { + fn default() -> Self { + Self(Appearance::Dark) + } +} + +#[derive(Deref, DerefMut, Default)] +struct GlobalSystemAppearance(SystemAppearance); + +impl Global for GlobalSystemAppearance {} + +impl SystemAppearance { + /// Returns the global [`SystemAppearance`]. + /// + /// Inserts a default [`SystemAppearance`] if one does not yet exist. + pub(crate) fn default_global(cx: &mut AppContext) -> Self { + cx.default_global::().0 + } + + /// Initializes the [`SystemAppearance`] for the current window. + pub fn init_for_window(cx: &mut WindowContext) { + *cx.default_global::() = + GlobalSystemAppearance(SystemAppearance(cx.appearance().into())); + } + + /// Returns the global [`SystemAppearance`]. + pub fn global(cx: &AppContext) -> Self { + cx.global::().0 + } + + /// Returns a mutable reference to the global [`SystemAppearance`]. + pub fn global_mut(cx: &mut AppContext) -> &mut Self { + cx.global_mut::() + } +} + #[derive(Default)] pub(crate) struct AdjustedBufferFontSize(Pixels); impl Global for AdjustedBufferFontSize {} +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(untagged)] +pub enum ThemeSelection { + Static(#[schemars(schema_with = "theme_name_ref")] String), + Dynamic { + #[serde(default)] + mode: ThemeMode, + #[schemars(schema_with = "theme_name_ref")] + light: String, + #[schemars(schema_with = "theme_name_ref")] + dark: String, + }, +} + +fn theme_name_ref(_: &mut SchemaGenerator) -> Schema { + Schema::new_ref("#/definitions/ThemeName".into()) +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ThemeMode { + /// Use the specified `light` theme. + Light, + + /// Use the specified `dark` theme. + Dark, + + /// Use the theme based on the system's appearance. + #[default] + System, +} + +impl ThemeSelection { + pub fn theme(&self, system_appearance: Appearance) -> &str { + match self { + Self::Static(theme) => theme, + Self::Dynamic { mode, light, dark } => match mode { + ThemeMode::Light => light, + ThemeMode::Dark => dark, + ThemeMode::System => match system_appearance { + Appearance::Light => light, + Appearance::Dark => dark, + }, + }, + } + } +} + #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct ThemeSettingsContent { #[serde(default)] @@ -54,7 +143,7 @@ pub struct ThemeSettingsContent { #[serde(default)] pub buffer_font_features: Option, #[serde(default)] - pub theme: Option, + pub theme: Option, /// EXPERIMENTAL: Overrides for the current theme. /// @@ -188,6 +277,7 @@ impl settings::Settings for ThemeSettings { cx: &mut AppContext, ) -> Result { let themes = ThemeRegistry::default_global(cx); + let system_appearance = SystemAppearance::default_global(cx); let mut this = Self { ui_font_size: defaults.ui_font_size.unwrap().into(), @@ -205,9 +295,9 @@ impl settings::Settings for ThemeSettings { }, buffer_font_size: defaults.buffer_font_size.unwrap().into(), buffer_line_height: defaults.buffer_line_height.unwrap(), - requested_theme: defaults.theme.clone(), + theme_selection: defaults.theme.clone(), active_theme: themes - .get(defaults.theme.as_ref().unwrap()) + .get(defaults.theme.as_ref().unwrap().theme(*system_appearance)) .or(themes.get(&one_dark().name)) .unwrap(), theme_overrides: None, @@ -229,9 +319,11 @@ impl settings::Settings for ThemeSettings { } if let Some(value) = &value.theme { - this.requested_theme = Some(value.clone()); + this.theme_selection = Some(value.clone()); - if let Some(theme) = themes.get(value).log_err() { + let theme_name = value.theme(*system_appearance); + + if let Some(theme) = themes.get(theme_name).log_err() { this.active_theme = theme; } } @@ -291,10 +383,6 @@ impl settings::Settings for ThemeSettings { .unwrap() .properties .extend([ - ( - "theme".to_owned(), - Schema::new_ref("#/definitions/ThemeName".into()), - ), ( "buffer_font_family".to_owned(), Schema::new_ref("#/definitions/FontFamilies".into()), diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 8161217c8f..14cddafa7a 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -27,7 +27,7 @@ pub use schema::*; pub use settings::*; pub use styles::*; -use gpui::{AppContext, AssetSource, Hsla, SharedString}; +use gpui::{AppContext, AssetSource, Hsla, SharedString, WindowAppearance}; use serde::Deserialize; #[derive(Debug, PartialEq, Clone, Copy, Deserialize)] @@ -45,6 +45,15 @@ impl Appearance { } } +impl From for Appearance { + fn from(value: WindowAppearance) -> Self { + match value { + WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark, + WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light, + } + } +} + pub enum LoadThemes { /// Only load the base theme. /// diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 21d9073570..f82a0c5ac7 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -9,7 +9,9 @@ use gpui::{ use picker::{Picker, PickerDelegate}; use settings::{update_settings_file, SettingsStore}; use std::sync::Arc; -use theme::{Theme, ThemeMeta, ThemeRegistry, ThemeSettings}; +use theme::{ + Appearance, Theme, ThemeMeta, ThemeMode, ThemeRegistry, ThemeSelection, ThemeSettings, +}; use ui::{prelude::*, v_flex, ListItem, ListItemSpacing}; use util::ResultExt; use workspace::{ui::HighlightedLabel, ModalView, Workspace}; @@ -167,8 +169,26 @@ impl PickerDelegate for ThemeSelectorDelegate { self.telemetry .report_setting_event("theme", theme_name.to_string()); + let appearance = Appearance::from(cx.appearance()); + update_settings_file::(self.fs.clone(), cx, move |settings| { - settings.theme = Some(theme_name.to_string()); + if let Some(selection) = settings.theme.as_mut() { + let theme_to_update = match selection { + ThemeSelection::Static(theme) => theme, + ThemeSelection::Dynamic { mode, light, dark } => match mode { + ThemeMode::Light => light, + ThemeMode::Dark => dark, + ThemeMode::System => match appearance { + Appearance::Light => light, + Appearance::Dark => dark, + }, + }, + }; + + *theme_to_update = theme_name.to_string(); + } else { + settings.theme = Some(ThemeSelection::Static(theme_name.to_string())); + } }); self.view diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index af8608776f..23c9b84f01 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -64,7 +64,7 @@ use std::{ sync::{atomic::AtomicUsize, Arc}, time::Duration, }; -use theme::{ActiveTheme, ThemeSettings}; +use theme::{ActiveTheme, SystemAppearance, ThemeSettings}; pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; pub use ui; use ui::Label; @@ -682,6 +682,21 @@ impl Workspace { } cx.notify(); }), + cx.observe_window_appearance(|_, cx| { + let window_appearance = cx.appearance(); + + *SystemAppearance::global_mut(cx) = SystemAppearance(window_appearance.into()); + + let mut theme_settings = ThemeSettings::get_global(cx).clone(); + + if let Some(theme_selection) = theme_settings.theme_selection.clone() { + let theme_name = theme_selection.theme(window_appearance.into()); + + if let Some(_theme) = theme_settings.switch_theme(&theme_name, cx) { + ThemeSettings::override_global(theme_settings, cx); + } + } + }), cx.observe(&left_dock, |this, _, cx| { this.serialize_workspace(cx); cx.notify(); @@ -840,6 +855,8 @@ impl Workspace { let workspace_id = workspace_id.clone(); let project_handle = project_handle.clone(); move |cx| { + SystemAppearance::init_for_window(cx); + cx.new_view(|cx| { Workspace::new(workspace_id, project_handle, app_state, cx) }) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 2f5db82dd6..ee17b16e4c 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -43,7 +43,7 @@ use std::{ thread, time::Duration, }; -use theme::{ActiveTheme, ThemeRegistry, ThemeSettings}; +use theme::{ActiveTheme, SystemAppearance, ThemeRegistry, ThemeSettings}; use util::{ async_maybe, http::{self, HttpClient, ZedHttpClient}, @@ -912,8 +912,10 @@ fn load_user_themes_in_background(fs: Arc, cx: &mut AppContext) { theme_registry.load_user_themes(themes_dir, fs).await?; cx.update(|cx| { let mut theme_settings = ThemeSettings::get_global(cx).clone(); - if let Some(requested_theme) = theme_settings.requested_theme.clone() { - if let Some(_theme) = theme_settings.switch_theme(&requested_theme, cx) { + if let Some(theme_selection) = theme_settings.theme_selection.clone() { + let theme_name = theme_selection.theme(*SystemAppearance::global(cx)); + + if let Some(_theme) = theme_settings.switch_theme(&theme_name, cx) { ThemeSettings::override_global(theme_settings, cx); } } @@ -949,11 +951,14 @@ fn watch_themes(fs: Arc, cx: &mut AppContext) { cx.update(|cx| { let mut theme_settings = ThemeSettings::get_global(cx).clone(); - if let Some(requested_theme) = - theme_settings.requested_theme.clone() + if let Some(theme_selection) = + theme_settings.theme_selection.clone() { + let theme_name = + theme_selection.theme(*SystemAppearance::global(cx)); + if let Some(_theme) = - theme_settings.switch_theme(&requested_theme, cx) + theme_settings.switch_theme(&theme_name, cx) { ThemeSettings::override_global(theme_settings, cx); }