simplify theme crate to builtin-only registry

This commit is contained in:
2026-10-08 09:25:24 +07:00
parent af20a1b150
commit 4dfc9fd668
10 changed files with 82 additions and 299 deletions
Generated
-1
View File
@@ -7436,7 +7436,6 @@ dependencies = [
name = "theme" name = "theme"
version = "1.0.2" version = "1.0.2"
dependencies = [ dependencies = [
"anyhow",
"gpui-base", "gpui-base",
"gpui-pre", "gpui-pre",
"log", "log",
-1
View File
@@ -7,7 +7,6 @@ publish.workspace = true
[dependencies] [dependencies]
gpui.workspace = true gpui.workspace = true
gpui-base.workspace = true gpui-base.workspace = true
anyhow.workspace = true
log.workspace = true log.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
+16 -17
View File
@@ -1,4 +1,3 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::sync::OnceLock; use std::sync::OnceLock;
@@ -14,6 +13,17 @@ pub fn builtin_registry() -> &'static ThemeRegistry {
REGISTRY.get_or_init(|| ThemeRegistry { REGISTRY.get_or_init(|| ThemeRegistry {
families: vec![ families: vec![
family("zeron", "Zeron", vec![zeron_light(), zeron_dark()]), family("zeron", "Zeron", vec![zeron_light(), zeron_dark()]),
family("dracula", "Dracula", vec![dracula()]),
family("github", "GitHub", vec![github_light(), github_dark()]),
family("ayu", "Ayu", vec![ayu_light(), ayu_dark(), ayu_mirage()]),
family("gruvbox", "Gruvbox", vec![gruvbox_light(), gruvbox_dark()]),
family("nord", "Nord", vec![nord()]),
family("one-dark-pro", "One Dark Pro", vec![one_dark_pro()]),
family("atom-one-dark", "Atom One Dark", vec![atom_one_dark()]),
family("palenight", "Palenight", vec![palenight()]),
family("synthwave-84", "SynthWave '84", vec![synthwave_84()]),
family("cobalt2", "Cobalt2", vec![cobalt2()]),
family("andromeda", "Andromeda", vec![andromeda()]),
family( family(
"vscode-default", "vscode-default",
"VS Code Default", "VS Code Default",
@@ -29,37 +39,26 @@ pub fn builtin_registry() -> &'static ThemeRegistry {
"Tokyo Night", "Tokyo Night",
vec![tokyo_light(), tokyo_dark()], vec![tokyo_light(), tokyo_dark()],
), ),
family("dracula", "Dracula", vec![dracula()]),
family("github", "GitHub", vec![github_light(), github_dark()]),
family("ayu", "Ayu", vec![ayu_light(), ayu_dark(), ayu_mirage()]),
family("gruvbox", "Gruvbox", vec![gruvbox_light(), gruvbox_dark()]),
family( family(
"rose-pine", "rose-pine",
"Rosé Pine", "Rosé Pine",
vec![rose_pine_dawn(), rose_pine_moon()], vec![rose_pine_dawn(), rose_pine_moon()],
), ),
family("nord", "Nord", vec![nord()]),
family("one-dark-pro", "One Dark Pro", vec![one_dark_pro()]),
family("atom-one-dark", "Atom One Dark", vec![atom_one_dark()]),
family( family(
"night-owl", "night-owl",
"Night Owl", "Night Owl",
vec![night_owl_light(), night_owl()], vec![night_owl_light(), night_owl()],
), ),
family(
"winter-is-coming",
"Winter is Coming",
vec![winter_light(), winter_dark_blue()],
),
family("palenight", "Palenight", vec![palenight()]),
family("synthwave-84", "SynthWave '84", vec![synthwave_84()]),
family( family(
"shades-of-purple", "shades-of-purple",
"Shades of Purple", "Shades of Purple",
vec![shades_of_purple()], vec![shades_of_purple()],
), ),
family("cobalt2", "Cobalt2", vec![cobalt2()]), family(
family("andromeda", "Andromeda", vec![andromeda()]), "winter-is-coming",
"Winter is Coming",
vec![winter_light(), winter_dark_blue()],
),
], ],
}) })
} }
+1 -11
View File
@@ -3,7 +3,6 @@ use std::fmt::Debug;
use gpui::Axis; use gpui::Axis;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// A enum for defining the side of the element.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Side { pub enum Side {
#[serde(rename = "left")] #[serde(rename = "left")]
@@ -13,20 +12,17 @@ pub enum Side {
} }
impl Side { impl Side {
/// Returns true if the side is left.
#[inline] #[inline]
pub fn is_left(&self) -> bool { pub fn is_left(&self) -> bool {
matches!(self, Self::Left) matches!(self, Self::Left)
} }
/// Returns true if the side is right.
#[inline] #[inline]
pub fn is_right(&self) -> bool { pub fn is_right(&self) -> bool {
matches!(self, Self::Right) matches!(self, Self::Right)
} }
} }
/// A trait to extend the [`Axis`] enum with utility methods.
pub trait AxisExt { pub trait AxisExt {
#[allow(clippy::wrong_self_convention)] #[allow(clippy::wrong_self_convention)]
fn is_horizontal(self) -> bool; fn is_horizontal(self) -> bool;
@@ -46,18 +42,12 @@ impl AxisExt for Axis {
} }
} }
/// A struct for defining the edges of an element. /// [`gpui::Edges`] with serde support.
///
/// A extend version of [`gpui::Edges`] to serialize/deserialize.
#[derive(Debug, Clone, Default, Serialize, Deserialize, Eq, PartialEq)] #[derive(Debug, Clone, Default, Serialize, Deserialize, Eq, PartialEq)]
#[repr(C)] #[repr(C)]
pub struct Edges<T: Clone + Debug + Default + PartialEq> { pub struct Edges<T: Clone + Debug + Default + PartialEq> {
/// The size of the top edge.
pub top: T, pub top: T,
/// The size of the right edge.
pub right: T, pub right: T,
/// The size of the bottom edge.
pub bottom: T, pub bottom: T,
/// The size of the left edge.
pub left: T, pub left: T,
} }
+4 -27
View File
@@ -1,8 +1,8 @@
use gpui::{App, Pixels, px}; use gpui::{App, Pixels, px};
pub mod builtins; mod builtins;
mod geometry; mod geometry;
pub mod model; mod model;
mod notification; mod notification;
mod platform_kind; mod platform_kind;
mod resolved; mod resolved;
@@ -17,26 +17,15 @@ pub use resolved::{ResolvedAccentRoles, ResolvedColors};
pub use runtime::Theme; pub use runtime::Theme;
pub use scrollbar_mode::*; pub use scrollbar_mode::*;
/// Defines window border radius for platforms that use client side decorations.
pub const CLIENT_SIDE_DECORATION_ROUNDING: Pixels = px(10.0); pub const CLIENT_SIDE_DECORATION_ROUNDING: Pixels = px(10.0);
/// Defines window shadow size for platforms that use client side decorations.
pub const CLIENT_SIDE_DECORATION_SHADOW: Pixels = px(10.0); pub const CLIENT_SIDE_DECORATION_SHADOW: Pixels = px(10.0);
/// Defines window border size for platforms that use client side decorations.
pub const CLIENT_SIDE_DECORATION_BORDER: Pixels = px(1.0); pub const CLIENT_SIDE_DECORATION_BORDER: Pixels = px(1.0);
/// Defines window titlebar height
pub(crate) const TITLEBAR_HEIGHT: Pixels = px(36.0); pub(crate) const TITLEBAR_HEIGHT: Pixels = px(36.0);
/// Defines workspace tabbar height
pub const TABBAR_HEIGHT: Pixels = px(36.0); pub const TABBAR_HEIGHT: Pixels = px(36.0);
/// Defines default sidebar width
pub const SIDEBAR_WIDTH: Pixels = px(240.); pub const SIDEBAR_WIDTH: Pixels = px(240.);
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
for issue in ThemeRegistry::active().validate() { for issue in ThemeRegistry::builtin().validate() {
log::warn!("theme {}: {}", issue.variant_id, issue.message); log::warn!("theme {}: {}", issue.variant_id, issue.message);
} }
@@ -44,19 +33,7 @@ pub fn init(cx: &mut App) {
Theme::sync_scrollbar_appearance(cx); Theme::sync_scrollbar_appearance(cx);
} }
/// Mirror the active coop theme into the `gpui-base` global theme. /// Project the active theme onto the `gpui-base` global (focus ring, selection wash, scrollbars, backdrops).
///
/// Base paints a few things from its own tokens -- the focus ring, the wash
/// behind selected text, scrollbars, and overlay backdrops -- so the two
/// globals have to agree or those details drift away from the palette.
///
/// Only roles base can act on are projected. Radius, spacing, typography sizes,
/// shadows, and scrollbar geometry keep their base defaults: coop has a single
/// `radius`/`radius_lg`/`font_size` where base has six-point scales, so any
/// mapping would be invented rather than derived.
///
/// This is a no-op before the coop theme global exists; [`Theme::change`] is the
/// authoritative hook that keeps the projection current.
pub fn sync_base(cx: &mut App) { pub fn sync_base(cx: &mut App) {
let Some(theme) = cx.try_global::<Theme>() else { let Some(theme) = cx.try_global::<Theme>() else {
return; return;
+14 -96
View File
@@ -1,23 +1,10 @@
use std::collections::{BTreeMap, HashSet}; use std::collections::{BTreeMap, HashSet};
use std::fmt; use std::fmt;
use std::str::FromStr; use std::str::FromStr;
use std::sync::{OnceLock, RwLock};
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub use crate::builtins::builtin_registry; use crate::builtins::builtin_registry;
fn custom_families() -> &'static RwLock<Vec<ThemeFamily>> {
static CUSTOM: OnceLock<RwLock<Vec<ThemeFamily>>> = OnceLock::new();
CUSTOM.get_or_init(|| RwLock::new(Vec::new()))
}
/// Replace the process-wide custom portion of the runtime registry.
pub fn replace_custom_families(families: Vec<ThemeFamily>) {
*custom_families()
.write()
.expect("custom theme registry lock was poisoned") = families;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@@ -28,8 +15,6 @@ pub enum Appearance {
} }
impl Appearance { impl Appearance {
pub const ALL: [Self; 2] = [Self::Light, Self::Dark];
pub fn is_dark(self) -> bool { pub fn is_dark(self) -> bool {
matches!(self, Self::Dark) matches!(self, Self::Dark)
} }
@@ -53,8 +38,6 @@ pub enum SurfacePreference {
} }
impl SurfacePreference { impl SurfacePreference {
pub const ALL: [Self; 3] = [Self::ThemeDefault, Self::Frosted, Self::Opaque];
pub fn resolve(self, recommended: SurfaceTreatment) -> SurfaceTreatment { pub fn resolve(self, recommended: SurfaceTreatment) -> SurfaceTreatment {
match self { match self {
Self::ThemeDefault => recommended, Self::ThemeDefault => recommended,
@@ -455,7 +438,6 @@ pub struct ThemeVariant {
pub family_id: String, pub family_id: String,
pub name: String, pub name: String,
pub appearance: Appearance, pub appearance: Appearance,
/// The author's recommended treatment.
#[serde(alias = "surfaceTreatment")] #[serde(alias = "surfaceTreatment")]
pub recommended_surface_treatment: SurfaceTreatment, pub recommended_surface_treatment: SurfaceTreatment,
pub colors: ThemeColors, pub colors: ThemeColors,
@@ -486,7 +468,7 @@ pub struct ThemeFamily {
pub variants: Vec<ThemeVariant>, pub variants: Vec<ThemeVariant>,
} }
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct ThemeRegistry { pub struct ThemeRegistry {
pub families: Vec<ThemeFamily>, pub families: Vec<ThemeFamily>,
} }
@@ -496,19 +478,6 @@ impl ThemeRegistry {
builtin_registry() builtin_registry()
} }
/// Built-ins plus the currently installed custom families.
pub fn active() -> Self {
let mut families = Self::builtin().families.clone();
families.extend(
custom_families()
.read()
.expect("custom theme registry lock was poisoned")
.iter()
.cloned(),
);
Self { families }
}
pub fn variant(&self, id: &str) -> Option<&ThemeVariant> { pub fn variant(&self, id: &str) -> Option<&ThemeVariant> {
self.families self.families
.iter() .iter()
@@ -516,13 +485,6 @@ impl ThemeRegistry {
.find(|variant| variant.id == id) .find(|variant| variant.id == id)
} }
pub fn variants_for(&self, appearance: Appearance) -> impl Iterator<Item = &ThemeVariant> {
self.families
.iter()
.flat_map(|family| &family.variants)
.filter(move |variant| variant.appearance == appearance)
}
pub fn resolve(&self, selection: &ThemeSelection, appearance: Appearance) -> &ThemeVariant { pub fn resolve(&self, selection: &ThemeSelection, appearance: Appearance) -> &ThemeVariant {
self.variant(selection.variant_id(appearance)) self.variant(selection.variant_id(appearance))
.or_else(|| { .or_else(|| {
@@ -631,25 +593,21 @@ fn validate_contrast(
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum ValidationSeverity { pub enum ValidationSeverity {
Warning, Warning,
Error, Error,
} }
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum ValidationCategory { pub enum ValidationCategory {
Structural, Structural,
#[default]
Contrast, Contrast,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone)]
pub struct ValidationIssue { pub struct ValidationIssue {
pub variant_id: String, pub variant_id: String,
#[serde(default)]
pub category: ValidationCategory, pub category: ValidationCategory,
pub severity: ValidationSeverity, pub severity: ValidationSeverity,
pub message: String, pub message: String,
@@ -682,42 +640,6 @@ impl ValidationIssue {
message: message.into(), message: message.into(),
} }
} }
pub fn is_blocking(&self) -> bool {
self.category == ValidationCategory::Structural
&& self.severity == ValidationSeverity::Error
}
}
/// Stable visual QA scenes every bundled variant must be reviewed against.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum VisualFixture {
Sidebar,
TranscriptMarkdown,
TranscriptCode,
Composer,
PickerPopover,
AppearanceSettings,
Diff,
Terminal,
EmptyState,
Dialog,
}
impl VisualFixture {
pub const ALL: [Self; 10] = [
Self::Sidebar,
Self::TranscriptMarkdown,
Self::TranscriptCode,
Self::Composer,
Self::PickerPopover,
Self::AppearanceSettings,
Self::Diff,
Self::Terminal,
Self::EmptyState,
Self::Dialog,
];
} }
#[cfg(test)] #[cfg(test)]
@@ -735,7 +657,7 @@ mod tests {
#[test] #[test]
fn accent_overlay_meets_interaction_contrast() { fn accent_overlay_meets_interaction_contrast() {
for appearance in Appearance::ALL { for appearance in [Appearance::Light, Appearance::Dark] {
let background = if appearance.is_dark() { let background = if appearance.is_dark() {
Color::rgb(6, 6, 6) Color::rgb(6, 6, 6)
} else { } else {
@@ -781,6 +703,14 @@ mod tests {
fn builtins_have_complete_provenance_and_no_validation_errors() { fn builtins_have_complete_provenance_and_no_validation_errors() {
let registry = ThemeRegistry::builtin(); let registry = ThemeRegistry::builtin();
assert_eq!(registry.families.len(), 19); assert_eq!(registry.families.len(), 19);
assert_eq!(
registry
.families
.iter()
.map(|family| family.variants.len())
.sum::<usize>(),
30
);
assert!(registry.variant("zeron-light").is_some()); assert!(registry.variant("zeron-light").is_some());
assert!(registry.variant("zeron-dark").is_some()); assert!(registry.variant("zeron-dark").is_some());
let errors: Vec<_> = registry let errors: Vec<_> = registry
@@ -790,16 +720,4 @@ mod tests {
.collect(); .collect();
assert!(errors.is_empty(), "{errors:#?}"); assert!(errors.is_empty(), "{errors:#?}");
} }
#[test]
fn visual_fixture_matrix_covers_every_variant_and_scene() {
let registry = ThemeRegistry::builtin();
let variants = registry
.families
.iter()
.map(|family| family.variants.len())
.sum::<usize>();
assert_eq!(variants, 30);
assert_eq!(variants * VisualFixture::ALL.len(), 300);
}
} }
-4
View File
@@ -2,14 +2,10 @@ use gpui::{Anchor, Pixels, px};
use crate::{Edges, TITLEBAR_HEIGHT}; use crate::{Edges, TITLEBAR_HEIGHT};
/// The settings for notifications.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct NotificationSettings { pub struct NotificationSettings {
/// The placement of the notification, default: [`Anchor::TopRight`]
pub placement: Anchor, pub placement: Anchor,
/// The margins of the notification with respect to the window edges.
pub margins: Edges<Pixels>, pub margins: Edges<Pixels>,
/// The maximum number of notifications to show at once, default: 10
pub max_items: usize, pub max_items: usize,
} }
+32 -76
View File
@@ -1,8 +1,8 @@
use gpui::Hsla; use gpui::Hsla;
use crate::model::{AccentRoles, ThemeColors, ThemeVariant}; use crate::model::{AccentRoles, Color, ThemeColors, ThemeVariant};
fn hsla(color: crate::model::Color) -> Hsla { fn hsla(color: Color) -> Hsla {
let packed = ((color.r as u32) << 24) let packed = ((color.r as u32) << 24)
| ((color.g as u32) << 16) | ((color.g as u32) << 16)
| ((color.b as u32) << 8) | ((color.b as u32) << 8)
@@ -10,23 +10,17 @@ fn hsla(color: crate::model::Color) -> Hsla {
Hsla::from(gpui::rgba(packed)) Hsla::from(gpui::rgba(packed))
} }
/// The accent roles a component can consume, pre-resolved to `Hsla` together /// Accent roles resolved to [`Hsla`], with derived interaction states precomputed.
/// with the derived states (hover/active, contrast-safe text) that several
/// components need; computing them here keeps call sites free of color math.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct ResolvedAccentRoles { pub struct ResolvedAccentRoles {
pub primary: Hsla, pub primary: Hsla,
pub strong: Hsla,
pub wash: Hsla, pub wash: Hsla,
pub on: Hsla, pub on: Hsla,
pub selection: Hsla, pub selection: Hsla,
pub caret: Hsla,
pub activity: Hsla,
pub glyph: [Hsla; 3],
pub hover: Hsla, pub hover: Hsla,
pub active: Hsla, pub active: Hsla,
pub selected: Hsla, pub selected: Hsla,
/// Accent text guaranteed readable on `wash`. /// Text guaranteed 4.5:1 readable on `wash`.
pub text_on_wash: Hsla, pub text_on_wash: Hsla,
/// Text guaranteed readable on the flat `danger` color. /// Text guaranteed readable on the flat `danger` color.
pub on_danger: Hsla, pub on_danger: Hsla,
@@ -38,17 +32,9 @@ impl ResolvedAccentRoles {
let wash_background = accent.wash.blend_over(background); let wash_background = accent.wash.blend_over(background);
Self { Self {
primary: hsla(accent.primary), primary: hsla(accent.primary),
strong: hsla(accent.strong),
wash: hsla(accent.wash), wash: hsla(accent.wash),
on: hsla(accent.on), on: hsla(accent.on),
selection: hsla(accent.selection), selection: hsla(accent.selection),
caret: hsla(accent.caret),
activity: hsla(accent.activity),
glyph: [
hsla(accent.glyph[0]),
hsla(accent.glyph[1]),
hsla(accent.glyph[2]),
],
hover: hsla(accent.primary.mix(background, 0.12)), hover: hsla(accent.primary.mix(background, 0.12)),
active: hsla(accent.primary.mix(background, 0.2)), active: hsla(accent.primary.mix(background, 0.2)),
selected: hsla(accent.primary.mix(background, 0.28)), selected: hsla(accent.primary.mix(background, 0.28)),
@@ -58,8 +44,7 @@ impl ResolvedAccentRoles {
} }
} }
/// The `Hsla` snapshot components render from, rebuilt whenever the active /// The `Hsla` snapshot components render from, rebuilt on every theme change.
/// variant, accent, or appearance changes.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct ResolvedColors { pub struct ResolvedColors {
pub background: Hsla, pub background: Hsla,
@@ -92,63 +77,34 @@ pub struct ResolvedColors {
impl ResolvedColors { impl ResolvedColors {
pub fn resolve(variant: &ThemeVariant) -> Self { pub fn resolve(variant: &ThemeVariant) -> Self {
let colors = variant.colors.clone(); let colors = &variant.colors;
let ThemeColors {
background,
shell,
raised,
card,
dialog,
overlay,
hover,
active,
border,
border_strong,
text,
text_muted,
text_faint,
solid,
on_solid,
danger,
danger_muted,
warning,
warning_muted,
success,
success_muted,
input,
cursor,
diff_add,
diff_delete,
diff_hunk,
} = colors;
Self { Self {
background: hsla(background), background: hsla(colors.background),
shell: hsla(shell), shell: hsla(colors.shell),
raised: hsla(raised), raised: hsla(colors.raised),
card: hsla(card), card: hsla(colors.card),
dialog: hsla(dialog), dialog: hsla(colors.dialog),
overlay: hsla(overlay), overlay: hsla(colors.overlay),
hover: hsla(hover), hover: hsla(colors.hover),
active: hsla(active), active: hsla(colors.active),
border: hsla(border), border: hsla(colors.border),
border_strong: hsla(border_strong), border_strong: hsla(colors.border_strong),
text: hsla(text), text: hsla(colors.text),
text_muted: hsla(text_muted), text_muted: hsla(colors.text_muted),
text_faint: hsla(text_faint), text_faint: hsla(colors.text_faint),
solid: hsla(solid), solid: hsla(colors.solid),
on_solid: hsla(on_solid), on_solid: hsla(colors.on_solid),
danger: hsla(danger), danger: hsla(colors.danger),
danger_muted: hsla(danger_muted), danger_muted: hsla(colors.danger_muted),
warning: hsla(warning), warning: hsla(colors.warning),
warning_muted: hsla(warning_muted), warning_muted: hsla(colors.warning_muted),
success: hsla(success), success: hsla(colors.success),
success_muted: hsla(success_muted), success_muted: hsla(colors.success_muted),
input: hsla(input), input: hsla(colors.input),
cursor: hsla(cursor), cursor: hsla(colors.cursor),
diff_add: hsla(diff_add), diff_add: hsla(colors.diff_add),
diff_delete: hsla(diff_delete), diff_delete: hsla(colors.diff_delete),
diff_hunk: hsla(diff_hunk), diff_hunk: hsla(colors.diff_hunk),
} }
} }
} }
+14 -65
View File
@@ -1,4 +1,4 @@
use std::ops::{Deref, DerefMut}; use std::ops::Deref;
use std::sync::Arc; use std::sync::Arc;
use gpui::{App, Global, Pixels, SharedString, Window, px}; use gpui::{App, Global, Pixels, SharedString, Window, px};
@@ -21,49 +21,21 @@ impl From<gpui::WindowAppearance> for Appearance {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Theme { pub struct Theme {
/// The active theme variant.
pub variant: Arc<ThemeVariant>, pub variant: Arc<ThemeVariant>,
/// The active appearance (light or dark).
pub appearance: Appearance, pub appearance: Appearance,
/// Named `theme_selection` to avoid shadowing the `selection` color role via `Deref`.
/// The per-appearance variant selection.
pub theme_selection: ThemeSelection, pub theme_selection: ThemeSelection,
/// The accent override applied on top of the variant.
pub accent_selection: AccentSelection, pub accent_selection: AccentSelection,
/// The user's surface treatment preference.
pub surface_preference: SurfacePreference, pub surface_preference: SurfacePreference,
/// The surface treatment in effect for this session.
pub surface: SurfaceTreatment, pub surface: SurfaceTreatment,
/// The resolved accent roles.
pub accent: ResolvedAccentRoles, pub accent: ResolvedAccentRoles,
/// The resolved color snapshot.
pub colors: ResolvedColors, pub colors: ResolvedColors,
/// The font family for the application.
pub font_family: SharedString, pub font_family: SharedString,
/// The root font size for the application, default is 15px.
pub font_size: Pixels, pub font_size: Pixels,
/// Radius for the general elements.
pub radius: Pixels, pub radius: Pixels,
/// Radius for the large elements, e.g.: dialog, notification.
pub radius_lg: Pixels, pub radius_lg: Pixels,
/// Enable shadow for the general elements. default is true
pub shadow: bool, pub shadow: bool,
/// Show the scrollbar mode, default: scrolling
pub scrollbar_mode: ScrollbarMode, pub scrollbar_mode: ScrollbarMode,
/// Notification settings
pub notification: NotificationSettings, pub notification: NotificationSettings,
} }
@@ -75,43 +47,34 @@ impl Deref for Theme {
} }
} }
impl DerefMut for Theme {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.colors
}
}
impl Global for Theme {} impl Global for Theme {}
impl Theme { impl Theme {
/// Returns the global theme reference
pub fn global(cx: &App) -> &Theme { pub fn global(cx: &App) -> &Theme {
cx.global::<Theme>() cx.global::<Theme>()
} }
/// Returns the global theme mutable reference
pub(crate) fn global_mut(cx: &mut App) -> &mut Theme { pub(crate) fn global_mut(cx: &mut App) -> &mut Theme {
cx.global_mut::<Theme>() cx.global_mut::<Theme>()
} }
/// Returns true if the theme is dark.
pub fn is_dark(&self) -> bool { pub fn is_dark(&self) -> bool {
self.appearance.is_dark() self.appearance.is_dark()
} }
fn new() -> Self { fn new() -> Self {
let platform = PlatformKind::platform();
let appearance = Appearance::default(); let appearance = Appearance::default();
let selection = ThemeSelection::default();
// Define the font family based on the platform. let font_family = match PlatformKind::platform() {
let font_family = match platform {
PlatformKind::Linux => "Inter", PlatformKind::Linux => "Inter",
_ => ".SystemUIFont", _ => ".SystemUIFont",
}; };
let selection = ThemeSelection::default(); let variant = ThemeRegistry::builtin()
let registry = ThemeRegistry::active(); .resolve(&selection, appearance)
let variant = registry.resolve(&selection, appearance).clone(); .clone();
let accent = variant.accent_for(AccentSelection::default()); let accent = variant.accent_for(AccentSelection::default());
let surface = SurfacePreference::default().resolve(variant.recommended_surface_treatment); let surface = SurfacePreference::default().resolve(variant.recommended_surface_treatment);
let resolved_accent = ResolvedAccentRoles::resolve(&accent, &variant.colors); let resolved_accent = ResolvedAccentRoles::resolve(&accent, &variant.colors);
@@ -136,7 +99,6 @@ impl Theme {
} }
} }
/// Sync the theme with the system appearance
pub fn sync_system_appearance(window: Option<&mut Window>, cx: &mut App) { pub fn sync_system_appearance(window: Option<&mut Window>, cx: &mut App) {
let appearance = window let appearance = window
.as_ref() .as_ref()
@@ -146,7 +108,6 @@ impl Theme {
Self::change(appearance, window, cx); Self::change(appearance, window, cx);
} }
/// Sync the Scrollbar showing behavior with the system
pub fn sync_scrollbar_appearance(cx: &mut App) { pub fn sync_scrollbar_appearance(cx: &mut App) {
Theme::global_mut(cx).scrollbar_mode = if cx.should_auto_hide_scrollbars() { Theme::global_mut(cx).scrollbar_mode = if cx.should_auto_hide_scrollbars() {
ScrollbarMode::Scrolling ScrollbarMode::Scrolling
@@ -155,32 +116,19 @@ impl Theme {
}; };
} }
/// Apply a theme variant by id for the current appearance.
pub fn apply_variant(id: &str, window: Option<&mut Window>, cx: &mut App) {
let appearance = Theme::global(cx).appearance;
Theme::global_mut(cx)
.theme_selection
.set_variant(appearance, id);
Self::change(appearance, window, cx);
}
/// Replace the per-appearance variant selection.
pub fn set_selection(selection: ThemeSelection, cx: &mut App) { pub fn set_selection(selection: ThemeSelection, cx: &mut App) {
Theme::global_mut(cx).theme_selection = selection; Theme::global_mut(cx).theme_selection = selection;
} }
/// Replace the accent override and re-resolve the theme.
pub fn set_accent_selection(selection: AccentSelection, cx: &mut App) { pub fn set_accent_selection(selection: AccentSelection, cx: &mut App) {
Theme::global_mut(cx).accent_selection = selection; Theme::global_mut(cx).accent_selection = selection;
} }
/// Replace the surface treatment preference.
pub fn set_surface_preference(preference: SurfacePreference, cx: &mut App) { pub fn set_surface_preference(preference: SurfacePreference, cx: &mut App) {
Theme::global_mut(cx).surface_preference = preference; Theme::global_mut(cx).surface_preference = preference;
} }
/// Change the app's appearance, resolving the selected variant (with its /// Switch appearance and re-resolve the selected variant, accent, and surface.
/// accent override) into a fresh color snapshot.
pub fn change<M>(appearance: M, window: Option<&mut Window>, cx: &mut App) pub fn change<M>(appearance: M, window: Option<&mut Window>, cx: &mut App)
where where
M: Into<Appearance>, M: Into<Appearance>,
@@ -190,6 +138,7 @@ impl Theme {
} }
let appearance = appearance.into(); let appearance = appearance.into();
let (selection, accent_selection, surface_preference) = { let (selection, accent_selection, surface_preference) = {
let theme = cx.global_mut::<Theme>(); let theme = cx.global_mut::<Theme>();
theme.appearance = appearance; theme.appearance = appearance;
@@ -200,8 +149,10 @@ impl Theme {
) )
}; };
let registry = ThemeRegistry::active(); let variant = ThemeRegistry::builtin()
let variant = registry.resolve(&selection, appearance).clone(); .resolve(&selection, appearance)
.clone();
let accent = variant.accent_for(accent_selection); let accent = variant.accent_for(accent_selection);
let surface = surface_preference.resolve(variant.recommended_surface_treatment); let surface = surface_preference.resolve(variant.recommended_surface_treatment);
let colors = ResolvedColors::resolve(&variant); let colors = ResolvedColors::resolve(&variant);
@@ -214,12 +165,10 @@ impl Theme {
theme.colors = colors; theme.colors = colors;
} }
// Refresh the window if available
if let Some(window) = window { if let Some(window) = window {
window.refresh(); window.refresh();
} }
// Keep the base-layer projection in step with the coop palette
sync_base(cx); sync_base(cx);
} }
} }
+1 -1
View File
@@ -465,7 +465,7 @@ impl Workspace {
fn theme_selector(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn theme_selector(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let appearance = cx.theme().appearance; let appearance = cx.theme().appearance;
let registry = ThemeRegistry::active(); let registry = ThemeRegistry::builtin();
let groups: Vec<(String, Vec<ThemeVariant>)> = registry let groups: Vec<(String, Vec<ThemeVariant>)> = registry
.families .families
.iter() .iter()