simplify theme crate to builtin-only registry
This commit is contained in:
Generated
-1
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user