port bezel theme system into the theme crate
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
//! Palette contrast tests, mirroring bezel's `tests/theme.rs`: each light text
|
||||
//! token must land within ~0.5 of its dark counterpart's contrast ratio
|
||||
//! against its own background, and the primary text must keep the ratio the
|
||||
//! palette was tuned for.
|
||||
|
||||
use gpui::Hsla;
|
||||
use theme::{Brand, ThemeColors, ThemeFamily, ThemeMode, Tint, contrast_ratio, flatten};
|
||||
|
||||
// The shipped palette's measured floors (AppKit-style ink, per bezel):
|
||||
// text/bg is 14.4:1 dark and 15.1:1 light, muted lands near 6.3 dark and
|
||||
// 4.0 light, and faint sits below 3:1 in both appearances by design.
|
||||
const TEXT_RATIO: f32 = 13.9;
|
||||
const MUTED_RATIO: f32 = 3.9;
|
||||
const FAINT_RATIO: f32 = 1.8;
|
||||
const TEXT_PARITY: f32 = 0.7;
|
||||
|
||||
fn ratio(fg: Hsla, bg: Hsla) -> f32 {
|
||||
contrast_ratio(flatten(fg, bg), bg)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_keeps_its_contrast() {
|
||||
for appearance in [ThemeMode::Dark, ThemeMode::Light] {
|
||||
let colors = ThemeColors::for_appearance(appearance);
|
||||
assert!(
|
||||
ratio(colors.text, colors.bg) >= TEXT_RATIO,
|
||||
"{appearance:?}: text on bg = {}",
|
||||
ratio(colors.text, colors.bg)
|
||||
);
|
||||
assert!(
|
||||
ratio(colors.text, colors.surface) >= TEXT_RATIO,
|
||||
"{appearance:?}: text on surface"
|
||||
);
|
||||
assert!(
|
||||
ratio(colors.text_muted, colors.bg) >= MUTED_RATIO,
|
||||
"{appearance:?}: muted on bg = {}",
|
||||
ratio(colors.text_muted, colors.bg)
|
||||
);
|
||||
assert!(
|
||||
ratio(colors.text_faint, colors.bg) >= FAINT_RATIO,
|
||||
"{appearance:?}: faint on bg = {}",
|
||||
ratio(colors.text_faint, colors.bg)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn light_matches_dark_contrast() {
|
||||
let dark = ThemeColors::for_appearance(ThemeMode::Dark);
|
||||
let light = ThemeColors::for_appearance(ThemeMode::Light);
|
||||
|
||||
// Primary text keeps near-parity across appearances. Muted and faint do
|
||||
// not (the light values sit further from the background by design, see
|
||||
// Ink::APPKIT); their floors are asserted in `text_keeps_its_contrast`.
|
||||
let pairs = [
|
||||
("text/bg", dark.text, dark.bg, light.text, light.bg),
|
||||
(
|
||||
"text/surface",
|
||||
dark.text,
|
||||
dark.surface,
|
||||
light.text,
|
||||
light.surface,
|
||||
),
|
||||
];
|
||||
for (name, dark_fg, dark_bg, light_fg, light_bg) in pairs {
|
||||
let dark_ratio = ratio(dark_fg, dark_bg);
|
||||
let light_ratio = ratio(light_fg, light_bg);
|
||||
let delta = (dark_ratio - light_ratio).abs();
|
||||
assert!(
|
||||
delta <= TEXT_PARITY,
|
||||
"{name}: light deviates by {delta} (dark {dark_ratio:.3}, light {light_ratio:.3})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_brand_is_a_no_op() {
|
||||
let brand = Brand::default();
|
||||
assert!(brand.is_default());
|
||||
for appearance in [ThemeMode::Dark, ThemeMode::Light] {
|
||||
let base = ThemeColors::for_appearance(appearance);
|
||||
let branded = brand.branded(appearance);
|
||||
assert_eq!(
|
||||
base, branded,
|
||||
"{appearance:?}: default brand must not move the palette"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn brand_tint_rotates_greys_and_keeps_semantics() {
|
||||
let brand = Brand {
|
||||
tint: Tint::new(257.417, 0.046),
|
||||
accent: Tint::new(276.935, 0.182),
|
||||
..Brand::default()
|
||||
};
|
||||
let base = ThemeColors::for_appearance(ThemeMode::Dark);
|
||||
let branded = brand.branded(ThemeMode::Dark);
|
||||
|
||||
assert_ne!(branded.bg, base.bg, "bg takes the tint");
|
||||
assert_ne!(branded.accent, base.accent, "accent takes the accent hue");
|
||||
assert_eq!(
|
||||
branded.danger, base.danger,
|
||||
"danger is semantic and keeps its hue"
|
||||
);
|
||||
assert_eq!(
|
||||
branded.warning, base.warning,
|
||||
"warning is semantic and keeps its hue"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_aliases_track_tokens() {
|
||||
for appearance in [ThemeMode::Dark, ThemeMode::Light] {
|
||||
let colors = ThemeColors::for_appearance(appearance);
|
||||
assert_eq!(colors.background, colors.bg);
|
||||
assert_eq!(colors.surface_background, colors.surface);
|
||||
assert_eq!(colors.text_placeholder, colors.text_faint);
|
||||
assert_eq!(colors.element_background, colors.solid);
|
||||
assert_eq!(colors.secondary_background, colors.surface_raised);
|
||||
assert_eq!(colors.tab_active_background, colors.bg);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rederive_round_trips_a_legacy_theme() {
|
||||
let json = br##"{
|
||||
"id": "test", "name": "Test", "author": "coop", "url": "",
|
||||
"light": {
|
||||
"background": "#ffffff", "surface_background": "#fafafa",
|
||||
"elevated_surface_background": "#f5f5f5", "panel_background": "#ffffff",
|
||||
"overlay": "#0000001a", "title_bar": "#00000000",
|
||||
"title_bar_inactive": "#ffffff", "window_border": "#c7c7cf",
|
||||
"border": "#dbdbdb", "border_variant": "#d1d1d1",
|
||||
"border_focused": "#3366cc", "border_selected": "#3366cc",
|
||||
"border_transparent": "#00000000", "border_disabled": "#e6e6e6",
|
||||
"ring": "#4d79d6", "text": "#1a1a1a", "text_muted": "#4d4d4d",
|
||||
"text_placeholder": "#808080", "text_accent": "#3366cc",
|
||||
"text_danger": "#cc3333", "text_warning": "#cc9933",
|
||||
"icon": "#4d4d4d", "icon_muted": "#808080", "icon_accent": "#3366cc",
|
||||
"element_foreground": "#ffffff", "element_background": "#3366cc",
|
||||
"element_hover": "#3366cce6", "element_active": "#2e5cb8",
|
||||
"element_selected": "#2952a3", "element_disabled": "#3366cc4d",
|
||||
"secondary_foreground": "#2952a3", "secondary_background": "#e6ecf5",
|
||||
"secondary_hover": "#3366cc1a", "secondary_active": "#d9e2f0",
|
||||
"secondary_selected": "#d9e2f0", "secondary_disabled": "#3366cc4d",
|
||||
"danger_foreground": "#ffffff", "danger_background": "#f5e6e6",
|
||||
"danger_hover": "#cc33331a", "danger_active": "#f0d9d9",
|
||||
"danger_selected": "#f0d9d9", "danger_disabled": "#cc33334d",
|
||||
"warning_foreground": "#1a1a1a", "warning_background": "#f5f0e6",
|
||||
"warning_hover": "#cc99331a", "warning_active": "#f0ead9",
|
||||
"warning_selected": "#f0ead9", "warning_disabled": "#cc99334d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#e6e6e6",
|
||||
"ghost_element_hover": "#0000001a", "ghost_element_active": "#d9d9d9",
|
||||
"ghost_element_selected": "#d9d9d9", "ghost_element_disabled": "#0000000d",
|
||||
"tab_inactive_background": "#e6e6e6", "tab_hover_background": "#e0e0e0",
|
||||
"tab_active_background": "#d9d9d9",
|
||||
"scrollbar_thumb_background": "#00000033",
|
||||
"scrollbar_thumb_hover_background": "#0000004d",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#d9d9d9",
|
||||
"drop_target_background": "#3366cc1a",
|
||||
"cursor": "#3399ff", "selection": "#3399ff40"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#1a1a1a", "surface_background": "#1f1f1f",
|
||||
"elevated_surface_background": "#242424", "panel_background": "#262626",
|
||||
"overlay": "#ffffff1a", "title_bar": "#00000000",
|
||||
"title_bar_inactive": "#1a1a1a", "window_border": "#404046",
|
||||
"border": "#404040", "border_variant": "#383838",
|
||||
"border_focused": "#4d79d6", "border_selected": "#4d79d6",
|
||||
"border_transparent": "#00000000", "border_disabled": "#2e2e2e",
|
||||
"ring": "#668cdf", "text": "#f2f2f2", "text_muted": "#b3b3b3",
|
||||
"text_placeholder": "#808080", "text_accent": "#668cdf",
|
||||
"text_danger": "#d64d4d", "text_warning": "#d6b34d",
|
||||
"icon": "#b3b3b3", "icon_muted": "#808080", "icon_accent": "#668cdf",
|
||||
"element_foreground": "#ffffff", "element_background": "#4d79d6",
|
||||
"element_hover": "#4d79d6e6", "element_active": "#456dc1",
|
||||
"element_selected": "#3e62ac", "element_disabled": "#4d79d64d",
|
||||
"secondary_foreground": "#3e62ac", "secondary_background": "#2a3652",
|
||||
"secondary_hover": "#4d79d61a", "secondary_active": "#303d5c",
|
||||
"secondary_selected": "#303d5c", "secondary_disabled": "#4d79d64d",
|
||||
"danger_foreground": "#ffffff", "danger_background": "#522a2a",
|
||||
"danger_hover": "#d64d4d1a", "danger_active": "#5c3030",
|
||||
"danger_selected": "#5c3030", "danger_disabled": "#d64d4d4d",
|
||||
"warning_foreground": "#f2f2f2", "warning_background": "#52482a",
|
||||
"warning_hover": "#d6b34d1a", "warning_active": "#5c5430",
|
||||
"warning_selected": "#5c5430", "warning_disabled": "#d6b34d4d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#2e2e2e",
|
||||
"ghost_element_hover": "#ffffff1a", "ghost_element_active": "#383838",
|
||||
"ghost_element_selected": "#383838", "ghost_element_disabled": "#ffffff0d",
|
||||
"tab_inactive_background": "#2e2e2e", "tab_hover_background": "#333333",
|
||||
"tab_active_background": "#383838",
|
||||
"scrollbar_thumb_background": "#ffffff33",
|
||||
"scrollbar_thumb_hover_background": "#ffffff4d",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#383838",
|
||||
"drop_target_background": "#4d79d61a",
|
||||
"cursor": "#4db3ff", "selection": "#4db3ff40"
|
||||
}
|
||||
}"##;
|
||||
|
||||
let family: ThemeFamily = serde_json::from_slice(json).expect("parse");
|
||||
let mut light = family.light;
|
||||
light.rederive_tokens(ThemeMode::Light);
|
||||
assert_eq!(light.bg, light.background, "bg tracks the JSON background");
|
||||
assert_eq!(light.surface, light.surface_background);
|
||||
assert_eq!(light.solid, light.element_background);
|
||||
// Fields with no legacy source keep the built-in palette's value.
|
||||
assert_eq!(
|
||||
light.success,
|
||||
ThemeColors::for_appearance(ThemeMode::Light).success
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user