use iced_widget::core::{Color, Theme};
use super::ramp;
const GRID_ELEVATION: f32 = 0.045;
const BODY_ELEVATION: f32 = 0.06;
const BORDER_ELEVATION: f32 = 0.17;
const WIRE_LEGIBILITY: f32 = 0.58;
const TERMINAL_LEGIBILITY: f32 = 0.78;
const ACCENT_SEPARATION: f32 = 0.34;
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct Roles {
pub canvas: Color,
pub grid: Color,
pub body: Color,
pub border: Color,
pub wire: Color,
pub terminal: Color,
pub accent: Color,
pub valid: Color,
pub danger: Color,
pub is_dark: bool,
}
impl Roles {
pub fn of(theme: &Theme) -> Self {
let palette = theme.extended_palette();
let canvas = palette.background.base.color;
let foreground = palette.background.base.text;
let up = if palette.is_dark { 1.0 } else { -1.0 };
Self {
canvas,
grid: ramp::shift(canvas, up * GRID_ELEVATION),
body: ramp::shift(canvas, up * BODY_ELEVATION),
border: ramp::shift(canvas, up * BORDER_ELEVATION),
wire: ramp::blend(canvas, foreground, WIRE_LEGIBILITY),
terminal: ramp::blend(canvas, foreground, TERMINAL_LEGIBILITY),
accent: ramp::separate(palette.primary.base.color, canvas, ACCENT_SEPARATION),
valid: ramp::separate(palette.success.base.color, canvas, ACCENT_SEPARATION),
danger: ramp::separate(palette.danger.base.color, canvas, ACCENT_SEPARATION),
is_dark: palette.is_dark,
}
}
pub fn legible(&self, hue: Color) -> Color {
ramp::separate(hue, self.canvas, ACCENT_SEPARATION)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn gap(a: Color, b: Color) -> f32 {
(ramp::lightness(a) - ramp::lightness(b)).abs()
}
fn rise(roles: &Roles, color: Color) -> f32 {
let signed = ramp::lightness(color) - ramp::lightness(roles.canvas);
if roles.is_dark { signed } else { -signed }
}
#[test]
fn the_elevation_ladder_holds_in_every_theme() {
for theme in Theme::ALL {
let roles = Roles::of(theme);
let (grid, body, border) = (
rise(&roles, roles.grid),
rise(&roles, roles.body),
rise(&roles, roles.border),
);
assert!(
grid < body && body < border,
"{theme}: surfaces out of order (grid {grid:.3}, body {body:.3}, \
border {border:.3})",
);
for (name, got, want) in [
("grid", grid, GRID_ELEVATION),
("body", body, BODY_ELEVATION),
("border", border, BORDER_ELEVATION),
] {
assert!(
(got - want).abs() < 0.01,
"{theme}: {name} rose {got:.3} instead of {want:.3}",
);
}
}
}
#[test]
fn a_node_body_keeps_most_of_the_canvas_text_contrast() {
for theme in Theme::ALL {
let roles = Roles::of(theme);
let text = theme.extended_palette().background.base.text;
let retained =
roles.body.relative_contrast(text) / roles.canvas.relative_contrast(text);
assert!(
retained >= 0.78,
"{theme}: a node body keeps only {:.0}% of the canvas text contrast",
retained * 100.0,
);
}
}
#[test]
fn marks_resolve_against_canvas_and_body_in_every_theme() {
for theme in Theme::ALL {
let roles = Roles::of(theme);
for (name, mark, floor) in [
("wire", roles.wire, 0.25),
("terminal", roles.terminal, 0.35),
] {
assert!(
gap(mark, roles.canvas) >= floor,
"{theme}: {name} is {:.3} from the canvas, under {floor}",
gap(mark, roles.canvas),
);
assert!(
gap(mark, roles.body) >= floor - 0.05,
"{theme}: {name} is {:.3} from a node body, under {:.2}",
gap(mark, roles.body),
floor - 0.05,
);
}
assert!(
rise(&roles, roles.terminal) > rise(&roles, roles.wire),
"{theme}: a pin must read above the wire it terminates",
);
}
}
#[test]
fn accents_separate_from_both_surfaces_in_every_theme() {
for theme in Theme::ALL {
let roles = Roles::of(theme);
for (name, accent) in [
("accent", roles.accent),
("valid", roles.valid),
("danger", roles.danger),
] {
assert!(
gap(accent, roles.canvas) >= 0.30,
"{theme}: {name} is {:.3} from the canvas",
gap(accent, roles.canvas),
);
assert!(
gap(accent, roles.body) >= 0.20,
"{theme}: {name} is {:.3} from a node body",
gap(accent, roles.body),
);
}
}
}
#[test]
fn the_three_accents_never_collide() {
for theme in Theme::ALL {
let roles = Roles::of(theme);
for (a, an, b, bn) in [
(roles.accent, "selection", roles.danger, "destruction"),
(roles.accent, "selection", roles.valid, "a valid target"),
(roles.valid, "a valid target", roles.danger, "destruction"),
] {
assert_ne!(a, b, "{theme}: {an} and {bn} resolve to one color");
}
}
}
}