mirror of
https://github.com/zed-industries/zed.git
synced 2025-02-05 02:20:10 +00:00
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.
This commit is contained in:
parent
b59f925933
commit
a80a3b8706
7 changed files with 167 additions and 24 deletions
|
@ -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<AppState>, 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(),
|
||||
|
|
|
@ -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<AppState>, 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(),
|
||||
|
|
|
@ -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<String>,
|
||||
pub theme_selection: Option<ThemeSelection>,
|
||||
pub active_theme: Arc<Theme>,
|
||||
pub theme_overrides: Option<ThemeStyleContent>,
|
||||
}
|
||||
|
||||
/// 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::<GlobalSystemAppearance>().0
|
||||
}
|
||||
|
||||
/// Initializes the [`SystemAppearance`] for the current window.
|
||||
pub fn init_for_window(cx: &mut WindowContext) {
|
||||
*cx.default_global::<GlobalSystemAppearance>() =
|
||||
GlobalSystemAppearance(SystemAppearance(cx.appearance().into()));
|
||||
}
|
||||
|
||||
/// Returns the global [`SystemAppearance`].
|
||||
pub fn global(cx: &AppContext) -> Self {
|
||||
cx.global::<GlobalSystemAppearance>().0
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the global [`SystemAppearance`].
|
||||
pub fn global_mut(cx: &mut AppContext) -> &mut Self {
|
||||
cx.global_mut::<GlobalSystemAppearance>()
|
||||
}
|
||||
}
|
||||
|
||||
#[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<FontFeatures>,
|
||||
#[serde(default)]
|
||||
pub theme: Option<String>,
|
||||
pub theme: Option<ThemeSelection>,
|
||||
|
||||
/// EXPERIMENTAL: Overrides for the current theme.
|
||||
///
|
||||
|
@ -188,6 +277,7 @@ impl settings::Settings for ThemeSettings {
|
|||
cx: &mut AppContext,
|
||||
) -> Result<Self> {
|
||||
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()),
|
||||
|
|
|
@ -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<WindowAppearance> 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.
|
||||
///
|
||||
|
|
|
@ -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::<ThemeSettings>(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
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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<dyn fs::Fs>, 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<dyn fs::Fs>, 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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue