use frust::{Color, ColorScheme, StatusColors, StatusPalette};
const D_BG_VOID: Color = Color::from_rgb8(0x0a, 0x0c, 0x11); const D_BG_BASE: Color = Color::from_rgb8(0x10, 0x13, 0x1a); const D_BG_SURFACE: Color = Color::from_rgb8(0x16, 0x1a, 0x23); const D_BG_RAISED: Color = Color::from_rgb8(0x1e, 0x23, 0x30); const D_BG_OVERLAY: Color = Color::from_rgb8(0x27, 0x2d, 0x3d); const D_BG_HOVER: Color = Color::from_rgb8(0x2f, 0x36, 0x48); const D_AMBER: Color = Color::from_rgb8(0xff, 0xb6, 0x27); const D_AMBER_DIM: Color = Color::from_rgb8(0xc9, 0x8a, 0x12);
const D_CYAN: Color = Color::from_rgb8(0x5e, 0xc8, 0xd8); const D_ERROR: Color = Color::from_rgb8(0xff, 0x6b, 0x6b);
const D_FG: Color = Color::from_rgb8(0xf2, 0xea, 0xd9); const D_FG_MUTED: Color = Color::from_rgb8(0xa3, 0x9c, 0x88);
const D_ON_AMBER: Color = Color::from_rgb8(0x24, 0x1a, 0x04);
const L_ON_AMBER: Color = Color::from_rgb8(0x2a, 0x1c, 0x04);
const L_BG_VOID: Color = Color::from_rgb8(0xf6, 0xf2, 0xe9); const L_BG_BASE: Color = Color::from_rgb8(0xfd, 0xfb, 0xf6);
const L_BG_SURFACE: Color = Color::from_rgb8(0xff, 0xff, 0xff); const L_BG_RAISED: Color = Color::from_rgb8(0xf1, 0xec, 0xe0);
const L_BG_OVERLAY: Color = Color::from_rgb8(0xe8, 0xe1, 0xd0);
const L_BG_HOVER: Color = Color::from_rgb8(0xec, 0xe5, 0xd5); const L_AMBER_TEXT: Color = Color::from_rgb8(0xa3, 0x65, 0x0a); const L_AMBER_FILL: Color = Color::from_rgb8(0xff, 0xb6, 0x27); const L_CYAN: Color = Color::from_rgb8(0x0f, 0x7c, 0x8c); const L_ERROR: Color = Color::from_rgb8(0xc2, 0x48, 0x3f);
const L_FG: Color = Color::from_rgb8(0x22, 0x1d, 0x12); const L_FG_MUTED: Color = Color::from_rgb8(0x65, 0x5d, 0x48);
const WHITE: Color = Color::from_rgb8(0xff, 0xff, 0xff);
const BLACK: Color = Color::from_rgb8(0x00, 0x00, 0x00);
pub const fn dark() -> ColorScheme {
ColorScheme {
primary: D_AMBER,
on_primary: D_ON_AMBER,
primary_container: D_AMBER, on_primary_container: D_ON_AMBER,
primary_fixed: FIXED_PRIMARY,
primary_fixed_dim: FIXED_PRIMARY_DIM,
on_primary_fixed: FIXED_ON_PRIMARY,
on_primary_fixed_variant: FIXED_ON_PRIMARY_VARIANT,
secondary: D_FG_MUTED,
on_secondary: D_BG_VOID,
secondary_container: D_BG_RAISED,
on_secondary_container: D_FG,
secondary_fixed: FIXED_SECONDARY,
secondary_fixed_dim: FIXED_SECONDARY_DIM,
on_secondary_fixed: FIXED_ON_SECONDARY,
on_secondary_fixed_variant: FIXED_ON_SECONDARY_VARIANT,
tertiary: D_CYAN,
on_tertiary: D_BG_VOID,
tertiary_container: Color::from_rgb8(0x20, 0x32, 0x3C),
on_tertiary_container: D_CYAN,
tertiary_fixed: FIXED_TERTIARY,
tertiary_fixed_dim: FIXED_TERTIARY_DIM,
on_tertiary_fixed: FIXED_ON_TERTIARY,
on_tertiary_fixed_variant: FIXED_ON_TERTIARY_VARIANT,
error: D_ERROR,
on_error: D_BG_VOID,
error_container: Color::from_rgb8(0x32, 0x24, 0x2C),
on_error_container: D_ERROR,
surface: D_BG_SURFACE,
on_surface: D_FG,
on_surface_variant: D_FG_MUTED,
surface_dim: D_BG_VOID,
surface_bright: D_BG_HOVER,
surface_container_lowest: D_BG_VOID,
surface_container_low: D_BG_BASE,
surface_container: D_BG_SURFACE,
surface_container_high: D_BG_RAISED,
surface_container_highest: D_BG_OVERLAY,
outline: Color::from_rgb8(0x3E, 0x3F, 0x44),
outline_variant: Color::from_rgb8(0x2A, 0x2D, 0x33),
shadow: BLACK,
scrim: BLACK,
inverse_surface: D_FG,
inverse_on_surface: D_BG_VOID,
inverse_primary: L_AMBER_TEXT,
surface_tint: D_AMBER,
}
}
pub const fn light() -> ColorScheme {
ColorScheme {
primary: L_AMBER_TEXT,
on_primary: WHITE, primary_container: L_AMBER_FILL, on_primary_container: L_ON_AMBER,
primary_fixed: FIXED_PRIMARY,
primary_fixed_dim: FIXED_PRIMARY_DIM,
on_primary_fixed: FIXED_ON_PRIMARY,
on_primary_fixed_variant: FIXED_ON_PRIMARY_VARIANT,
secondary: L_FG_MUTED,
on_secondary: WHITE,
secondary_container: L_BG_RAISED,
on_secondary_container: L_FG,
secondary_fixed: FIXED_SECONDARY,
secondary_fixed_dim: FIXED_SECONDARY_DIM,
on_secondary_fixed: FIXED_ON_SECONDARY,
on_secondary_fixed_variant: FIXED_ON_SECONDARY_VARIANT,
tertiary: L_CYAN,
on_tertiary: WHITE,
tertiary_container: Color::from_rgb8(0xE7, 0xF2, 0xF4),
on_tertiary_container: L_CYAN,
tertiary_fixed: FIXED_TERTIARY,
tertiary_fixed_dim: FIXED_TERTIARY_DIM,
on_tertiary_fixed: FIXED_ON_TERTIARY,
on_tertiary_fixed_variant: FIXED_ON_TERTIARY_VARIANT,
error: L_ERROR,
on_error: WHITE,
error_container: Color::from_rgb8(0xF9, 0xED, 0xEC),
on_error_container: L_ERROR,
surface: L_BG_SURFACE,
on_surface: L_FG,
on_surface_variant: L_FG_MUTED,
surface_dim: L_BG_HOVER, surface_bright: L_BG_SURFACE,
surface_container_lowest: L_BG_SURFACE, surface_container_low: L_BG_BASE,
surface_container: L_BG_VOID, surface_container_high: L_BG_RAISED,
surface_container_highest: L_BG_OVERLAY,
outline: Color::from_rgb8(0xD7, 0xD6, 0xD4),
outline_variant: Color::from_rgb8(0xE9, 0xE8, 0xE7),
shadow: L_FG,
scrim: BLACK,
inverse_surface: L_FG,
inverse_on_surface: L_BG_VOID,
inverse_primary: D_AMBER,
surface_tint: L_AMBER_TEXT,
}
}
const FIXED_PRIMARY: Color = D_AMBER; const FIXED_PRIMARY_DIM: Color = D_AMBER_DIM; const FIXED_ON_PRIMARY: Color = D_ON_AMBER; const FIXED_ON_PRIMARY_VARIANT: Color = Color::from_rgb8(0x3d, 0x2c, 0x08);
const FIXED_SECONDARY: Color = Color::from_rgb8(0xf1, 0xec, 0xe0); const FIXED_SECONDARY_DIM: Color = Color::from_rgb8(0xe8, 0xe1, 0xd0);
const FIXED_ON_SECONDARY: Color = Color::from_rgb8(0x22, 0x1d, 0x12); const FIXED_ON_SECONDARY_VARIANT: Color = Color::from_rgb8(0x65, 0x5d, 0x48);
const FIXED_TERTIARY: Color = D_CYAN; const FIXED_TERTIARY_DIM: Color = L_CYAN; const FIXED_ON_TERTIARY: Color = Color::from_rgb8(0x00, 0x2b, 0x31); const FIXED_ON_TERTIARY_VARIANT: Color = L_CYAN;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GlyphInk {
pub terminal_bg: Color,
pub terminal_head: Color,
pub terminal_fg: Color,
pub terminal_out: Color,
pub terminal_comment: Color,
pub terminal_prompt: Color,
pub terminal_border: Color,
pub tooltip_bg: Color,
pub tooltip_fg: Color,
}
impl GlyphInk {
pub const fn default_ink() -> Self {
Self {
terminal_bg: Color::from_rgb8(0x1c, 0x19, 0x12),
terminal_head: Color::from_rgb8(0x25, 0x21, 0x1a),
terminal_fg: Color::from_rgb8(0xd8, 0xcf, 0xb8),
terminal_out: Color::from_rgb8(0xa8, 0x9e, 0x85),
terminal_comment: Color::from_rgb8(0x6b, 0x62, 0x52),
terminal_prompt: D_AMBER, terminal_border: Color::from_rgb8(0x2c, 0x28, 0x1f),
tooltip_bg: Color::from_rgb8(0x2a, 0x25, 0x19),
tooltip_fg: D_FG, }
}
}
impl Default for GlyphInk {
fn default() -> Self {
Self::default_ink()
}
}
pub const fn status() -> StatusPalette {
StatusPalette {
light: StatusColors {
success: Color::from_rgb8(0x2f, 0x8f, 0x5b),
on_success: WHITE,
success_container: Color::from_rgb8(0xEA, 0xF4, 0xEF),
on_success_container: Color::from_rgb8(0x2f, 0x8f, 0x5b),
warning: Color::from_rgb8(0x96, 0x69, 0x0a),
on_warning: WHITE,
warning_container: Color::from_rgb8(0xF2, 0xED, 0xE2),
on_warning_container: Color::from_rgb8(0x96, 0x69, 0x0a),
info: L_CYAN,
on_info: WHITE,
info_container: Color::from_rgb8(0xE7, 0xF2, 0xF4),
on_info_container: L_CYAN,
},
dark: StatusColors {
success: Color::from_rgb8(0x5f, 0xd8, 0x8f),
on_success: D_BG_VOID,
success_container: Color::from_rgb8(0x1F, 0x31, 0x30),
on_success_container: Color::from_rgb8(0x5f, 0xd8, 0x8f),
warning: Color::from_rgb8(0xf5, 0xc8, 0x60),
on_warning: D_BG_VOID,
warning_container: Color::from_rgb8(0x31, 0x2F, 0x2A),
on_warning_container: Color::from_rgb8(0xf5, 0xc8, 0x60),
info: D_CYAN,
on_info: D_BG_VOID,
info_container: Color::from_rgb8(0x20, 0x32, 0x3C),
on_info_container: D_CYAN,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use frust::Brightness;
fn linearize(c: u8) -> f64 {
let c = c as f64 / 255.0;
if c <= 0.03928 {
c / 12.92
} else {
((c + 0.055) / 1.055).powf(2.4)
}
}
fn luminance(color: Color) -> f64 {
let [r, g, b, _] = color.to_rgba8().to_u8_array();
0.2126 * linearize(r) + 0.7152 * linearize(g) + 0.0722 * linearize(b)
}
fn contrast(a: Color, b: Color) -> f64 {
let (la, lb) = (luminance(a), luminance(b));
let (hi, lo) = if la >= lb { (la, lb) } else { (lb, la) };
(hi + 0.05) / (lo + 0.05)
}
fn flatten(fg: Color, bg: Color, alpha: f64) -> Color {
let [fr, fg_, fb, _] = fg.to_rgba8().to_u8_array();
let [br, bg_, bb, _] = bg.to_rgba8().to_u8_array();
let ch = |f: u8, b: u8| ((f as f64) * alpha + (b as f64) * (1.0 - alpha)).round() as u8;
Color::from_rgb8(ch(fr, br), ch(fg_, bg_), ch(fb, bb))
}
fn assert_close(a: Color, b: Color) {
let [ar, ag, ab, _] = a.to_rgba8().to_u8_array();
let [br, bg, bb, _] = b.to_rgba8().to_u8_array();
let d = |x: u8, y: u8| (x as i32 - y as i32).abs();
assert!(
d(ar, br) <= 1 && d(ag, bg) <= 1 && d(ab, bb) <= 1,
"colors differ by >1/channel: {:?} vs {:?}",
a.to_rgba8().to_u8_array(),
b.to_rgba8().to_u8_array()
);
}
const AA: f64 = 4.5;
#[test]
fn glyph_dark_accent_split_matches_source() {
let d = dark();
assert_eq!(d.primary, Color::from_rgb8(0xff, 0xb6, 0x27));
assert_eq!(d.primary_container, Color::from_rgb8(0xff, 0xb6, 0x27));
assert_eq!(d.on_primary_container, Color::from_rgb8(0x24, 0x1a, 0x04));
assert_eq!(d.surface_tint, d.primary);
}
#[test]
fn glyph_dark_surface_ramp_matches_source() {
let d = dark();
assert_eq!(
d.surface_container_lowest,
Color::from_rgb8(0x0a, 0x0c, 0x11)
); assert_eq!(d.surface_container_low, Color::from_rgb8(0x10, 0x13, 0x1a)); assert_eq!(d.surface, Color::from_rgb8(0x16, 0x1a, 0x23)); assert_eq!(d.surface_container_high, Color::from_rgb8(0x1e, 0x23, 0x30)); assert_eq!(
d.surface_container_highest,
Color::from_rgb8(0x27, 0x2d, 0x3d)
); assert_eq!(d.surface_bright, Color::from_rgb8(0x2f, 0x36, 0x48)); }
#[test]
fn glyph_dark_foreground_and_semantic_match_source() {
let d = dark();
assert_eq!(d.on_surface, Color::from_rgb8(0xf2, 0xea, 0xd9)); assert_eq!(d.on_surface_variant, Color::from_rgb8(0xa3, 0x9c, 0x88)); assert_eq!(d.error, Color::from_rgb8(0xff, 0x6b, 0x6b));
assert_eq!(d.tertiary, Color::from_rgb8(0x5e, 0xc8, 0xd8)); }
#[test]
fn glyph_dark_alpha_roles_flatten_from_source_over_surface() {
let d = dark();
let surface = Color::from_rgb8(0x16, 0x1a, 0x23);
let bfg = Color::from_rgb8(0xf2, 0xea, 0xd9);
assert_close(d.outline, flatten(bfg, surface, 0.18));
assert_close(d.outline_variant, flatten(bfg, surface, 0.09));
assert_close(
d.error_container,
flatten(Color::from_rgb8(0xff, 0x6b, 0x6b), surface, 0.12),
);
assert_close(
d.tertiary_container,
flatten(Color::from_rgb8(0x5e, 0xc8, 0xd8), surface, 0.14),
);
}
#[test]
fn glyph_light_accent_split_matches_source() {
let l = light();
assert_eq!(l.primary, Color::from_rgb8(0xa3, 0x65, 0x0a)); assert_eq!(l.primary_container, Color::from_rgb8(0xff, 0xb6, 0x27)); assert_eq!(l.on_primary_container, Color::from_rgb8(0x2a, 0x1c, 0x04));
assert_eq!(l.surface_tint, l.primary);
}
#[test]
fn glyph_light_surface_is_lightest_and_ramp_darkens_upward() {
let l = light();
assert_eq!(l.surface, Color::from_rgb8(0xff, 0xff, 0xff));
assert_eq!(
l.surface_container_lowest,
Color::from_rgb8(0xff, 0xff, 0xff)
);
assert_eq!(l.surface_container_low, Color::from_rgb8(0xfd, 0xfb, 0xf6));
assert_eq!(l.surface_container, Color::from_rgb8(0xf6, 0xf2, 0xe9)); assert_eq!(l.surface_container_high, Color::from_rgb8(0xf1, 0xec, 0xe0));
assert_eq!(
l.surface_container_highest,
Color::from_rgb8(0xe8, 0xe1, 0xd0)
);
}
#[test]
fn glyph_light_foreground_and_semantic_match_source() {
let l = light();
assert_eq!(l.on_surface, Color::from_rgb8(0x22, 0x1d, 0x12)); assert_eq!(l.on_surface_variant, Color::from_rgb8(0x65, 0x5d, 0x48)); assert_eq!(l.error, Color::from_rgb8(0xc2, 0x48, 0x3f));
assert_eq!(l.tertiary, Color::from_rgb8(0x0f, 0x7c, 0x8c)); }
#[test]
fn glyph_light_alpha_roles_flatten_from_source_over_white() {
let l = light();
let surface = Color::from_rgb8(0xff, 0xff, 0xff);
let bfg = Color::from_rgb8(0x22, 0x1d, 0x12); assert_close(l.outline, flatten(bfg, surface, 0.18));
assert_close(l.outline_variant, flatten(bfg, surface, 0.10));
assert_close(
l.error_container,
flatten(Color::from_rgb8(0xc2, 0x48, 0x3f), surface, 0.10),
);
}
#[test]
fn fixed_roles_are_brightness_invariant() {
let d = dark();
let l = light();
assert_eq!(d.primary_fixed, l.primary_fixed);
assert_eq!(d.primary_fixed_dim, l.primary_fixed_dim);
assert_eq!(d.on_primary_fixed, l.on_primary_fixed);
assert_eq!(d.on_primary_fixed_variant, l.on_primary_fixed_variant);
assert_eq!(d.secondary_fixed, l.secondary_fixed);
assert_eq!(d.secondary_fixed_dim, l.secondary_fixed_dim);
assert_eq!(d.tertiary_fixed, l.tertiary_fixed);
assert_eq!(d.tertiary_fixed_dim, l.tertiary_fixed_dim);
}
#[test]
fn aa_primary_on_surface_both_brightnesses() {
let d = dark();
let l = light();
assert!(
contrast(d.primary, d.surface) >= AA,
"dark primary-on-surface"
);
assert!(
contrast(l.primary, l.surface) >= AA,
"light primary-on-surface"
);
}
#[test]
fn aa_on_primary_container_on_container_both_brightnesses() {
let d = dark();
let l = light();
assert!(
contrast(d.on_primary_container, d.primary_container) >= AA,
"dark on-container ink on the amber fill"
);
assert!(
contrast(l.on_primary_container, l.primary_container) >= AA,
"light on-container ink on the amber fill"
);
}
#[test]
fn aa_fg_ramp_on_surfaces_both_brightnesses() {
let d = dark();
let l = light();
for (fg, bg) in [
(d.on_surface, d.surface),
(d.on_surface_variant, d.surface),
(d.on_surface, d.surface_container_lowest),
] {
assert!(contrast(fg, bg) >= AA, "dark fg ramp");
}
for (fg, bg) in [
(l.on_surface, l.surface),
(l.on_surface_variant, l.surface),
(l.on_surface, l.surface_container),
] {
assert!(contrast(fg, bg) >= AA, "light fg ramp");
}
}
#[test]
fn glyph_ink_matches_source_dark_swatches() {
let ink = GlyphInk::default_ink();
assert_eq!(ink.terminal_bg, Color::from_rgb8(0x1c, 0x19, 0x12));
assert_eq!(ink.terminal_head, Color::from_rgb8(0x25, 0x21, 0x1a));
assert_eq!(ink.terminal_fg, Color::from_rgb8(0xd8, 0xcf, 0xb8));
assert_eq!(ink.terminal_out, Color::from_rgb8(0xa8, 0x9e, 0x85));
assert_eq!(ink.terminal_comment, Color::from_rgb8(0x6b, 0x62, 0x52));
assert_eq!(ink.terminal_prompt, Color::from_rgb8(0xff, 0xb6, 0x27));
assert_eq!(ink.terminal_border, Color::from_rgb8(0x2c, 0x28, 0x1f));
assert_eq!(ink.tooltip_bg, Color::from_rgb8(0x2a, 0x25, 0x19));
assert_eq!(ink.tooltip_fg, Color::from_rgb8(0xf2, 0xea, 0xd9));
}
#[test]
fn glyph_ink_terminal_border_flattens_from_source_alpha() {
let ink = GlyphInk::default_ink();
let bg = Color::from_rgb8(0x1c, 0x19, 0x12);
let border_color = Color::from_rgb8(0xff, 0xee, 0xd2);
assert_close(ink.terminal_border, flatten(border_color, bg, 0.07));
}
#[test]
fn glyph_ink_terminal_text_is_aa_on_terminal_bg() {
let ink = GlyphInk::default_ink();
assert!(contrast(ink.terminal_fg, ink.terminal_bg) >= AA);
}
#[test]
fn glyph_ink_is_const_and_copy() {
const INK: GlyphInk = GlyphInk::default_ink();
let copied = INK;
assert_eq!(copied, INK);
assert_eq!(GlyphInk::default(), INK);
}
#[test]
fn glyph_status_selects_by_brightness_and_matches_source() {
let p = status();
assert_eq!(p.colors(Brightness::Dark), &p.dark);
assert_eq!(p.colors(Brightness::Light), &p.light);
assert_eq!(p.dark.success, Color::from_rgb8(0x5f, 0xd8, 0x8f));
assert_eq!(p.dark.warning, Color::from_rgb8(0xf5, 0xc8, 0x60));
assert_eq!(p.dark.info, Color::from_rgb8(0x5e, 0xc8, 0xd8)); assert_eq!(p.light.success, Color::from_rgb8(0x2f, 0x8f, 0x5b));
assert_eq!(p.light.warning, Color::from_rgb8(0x96, 0x69, 0x0a));
assert_eq!(p.light.info, Color::from_rgb8(0x0f, 0x7c, 0x8c));
}
#[test]
fn glyph_status_containers_flatten_from_source_alpha() {
let p = status();
let d_surface = Color::from_rgb8(0x16, 0x1a, 0x23);
assert_close(
p.dark.success_container,
flatten(Color::from_rgb8(0x5f, 0xd8, 0x8f), d_surface, 0.12),
);
assert_close(
p.dark.warning_container,
flatten(Color::from_rgb8(0xf5, 0xc8, 0x60), d_surface, 0.12),
);
let l_surface = Color::from_rgb8(0xff, 0xff, 0xff);
assert_close(
p.light.success_container,
flatten(Color::from_rgb8(0x2f, 0x8f, 0x5b), l_surface, 0.10),
);
}
#[test]
fn glyph_status_is_const_constructible() {
const P: StatusPalette = status();
assert_eq!(P.dark.success, Color::from_rgb8(0x5f, 0xd8, 0x8f));
}
}