use crate::contrast::{DIFF_ROLES, MIN_SEPARATION, SYNTAX_ROLES, apart, contrast};
use crate::schema::{COLOR_ROLE_COUNT, Color, ColorRole, ColorScheme, DesignSystem};
use crate::tokens::table;
pub const PALETTE_ENTRY_LEN: usize = 8;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Palette {
light: [Option<Color>; COLOR_ROLE_COUNT],
dark: [Option<Color>; COLOR_ROLE_COUNT],
}
impl Default for Palette {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PaletteEntry {
pub role: ColorRole,
pub scheme: ColorScheme,
pub color: Color,
}
impl Palette {
pub const fn new() -> Self {
Self {
light: [None; COLOR_ROLE_COUNT],
dark: [None; COLOR_ROLE_COUNT],
}
}
pub const fn with(mut self, role: ColorRole, light: Color, dark: Color) -> Self {
self.light[role as usize - 1] = Some(light);
self.dark[role as usize - 1] = Some(dark);
self
}
pub const fn with_light(mut self, role: ColorRole, light: Color) -> Self {
self.light[role as usize - 1] = Some(light);
self
}
pub const fn with_dark(mut self, role: ColorRole, dark: Color) -> Self {
self.dark[role as usize - 1] = Some(dark);
self
}
pub const fn get(&self, role: ColorRole, scheme: ColorScheme) -> Option<Color> {
match scheme {
ColorScheme::Light => self.light[role as usize - 1],
ColorScheme::Dark => self.dark[role as usize - 1],
ColorScheme::FollowSystem => None,
}
}
pub fn entries(&self) -> impl Iterator<Item = PaletteEntry> + '_ {
crate::schema::COLOR_ROLE_SCHEMA
.iter()
.flat_map(move |variant| {
let role = ColorRole::try_from(variant.tag).expect("a role from the role schema");
[ColorScheme::Light, ColorScheme::Dark]
.into_iter()
.filter_map(move |scheme| {
self.get(role, scheme).map(|color| PaletteEntry {
role,
scheme,
color,
})
})
})
}
pub fn len(&self) -> usize {
self.light.iter().chain(self.dark.iter()).flatten().count()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn write_into(&self, out: &mut Vec<u8>) {
for entry in self.entries() {
out.extend_from_slice(&(entry.role as u16).to_le_bytes());
out.extend_from_slice(&(entry.scheme as u16).to_le_bytes());
out.extend_from_slice(&entry.color.to_argb().to_le_bytes());
}
}
pub fn resolve(&self, system: DesignSystem, scheme: ColorScheme) -> [Color; COLOR_ROLE_COUNT] {
let base = table(system);
let mut out = [Color::default(); COLOR_ROLE_COUNT];
for (index, slot) in out.iter_mut().enumerate() {
let role = base.colors[index].role;
*slot = self
.get(role, scheme)
.unwrap_or_else(|| base.color(role, scheme));
}
for (fill, ink, required) in INK_PAIRS {
if self.get(fill, scheme).is_none() || self.get(ink, scheme).is_some() {
continue;
}
out[ink as usize - 1] =
ink_for(out[fill as usize - 1], out[ink as usize - 1], required);
}
out
}
pub fn check(&self, system: DesignSystem) -> Vec<PaletteViolation> {
let mut found = Vec::new();
for scheme in [ColorScheme::Light, ColorScheme::Dark] {
let colors = self.resolve(system, scheme);
let color = |role: ColorRole| colors[role as usize - 1];
let mut contrast_pair = |ink: ColorRole, fill: ColorRole, required: f64| {
let ratio = contrast(color(ink), color(fill));
if ratio < required {
found.push(PaletteViolation::Contrast {
scheme,
ink,
fill,
ratio,
required,
});
}
};
for (fill, ink, required) in INK_PAIRS {
contrast_pair(ink, fill, required);
}
contrast_pair(ColorRole::OnSurface, ColorRole::SurfaceContainer, 4.5);
for ink in SYNTAX_ROLES {
contrast_pair(ink, ColorRole::SurfaceContainer, 4.5);
}
for (ink, line, _) in DIFF_ROLES {
contrast_pair(ink, line, 4.5);
contrast_pair(ColorRole::OnSurface, line, 4.5);
}
let mut apart_pair = |first: ColorRole, second: ColorRole| {
let distance = apart(color(first), color(second));
if distance < MIN_SEPARATION {
found.push(PaletteViolation::Indistinct {
scheme,
first,
second,
distance,
});
}
};
for container in [
ColorRole::SurfaceContainer,
ColorRole::PrimaryContainer,
ColorRole::SecondaryContainer,
ColorRole::TertiaryContainer,
] {
apart_pair(container, ColorRole::Background);
}
for (_, line, word) in DIFF_ROLES {
apart_pair(word, line);
}
}
found
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum PaletteViolation {
Contrast {
scheme: ColorScheme,
ink: ColorRole,
fill: ColorRole,
ratio: f64,
required: f64,
},
Indistinct {
scheme: ColorScheme,
first: ColorRole,
second: ColorRole,
distance: u32,
},
}
impl std::fmt::Display for PaletteViolation {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Contrast {
scheme,
ink,
fill,
ratio,
required,
} => write!(
formatter,
"{scheme:?}: {ink:?} on {fill:?} is {ratio:.2}:1, under the {required}:1 it needs to be read"
),
Self::Indistinct {
scheme,
first,
second,
distance,
} => write!(
formatter,
"{scheme:?}: {first:?} and {second:?} are {distance} apart, so one cannot be seen on the other"
),
}
}
}
pub const INK_PAIRS: [(ColorRole, ColorRole, f64); 10] = [
(ColorRole::Primary, ColorRole::OnPrimary, 3.0),
(ColorRole::Secondary, ColorRole::OnSecondary, 3.0),
(ColorRole::Tertiary, ColorRole::OnTertiary, 3.0),
(ColorRole::Error, ColorRole::OnError, 3.0),
(
ColorRole::PrimaryContainer,
ColorRole::OnPrimaryContainer,
4.5,
),
(
ColorRole::SecondaryContainer,
ColorRole::OnSecondaryContainer,
4.5,
),
(
ColorRole::TertiaryContainer,
ColorRole::OnTertiaryContainer,
4.5,
),
(ColorRole::Surface, ColorRole::OnSurface, 4.5),
(ColorRole::SurfaceVariant, ColorRole::OnSurfaceVariant, 4.5),
(ColorRole::Background, ColorRole::OnBackground, 4.5),
];
pub fn ink_for(fill: Color, system_ink: Color, required: f64) -> Color {
if contrast(system_ink, fill) >= required {
return system_ink;
}
let white = Color::rgb(0xffffff);
let black = Color::rgb(0x000000);
if contrast(white, fill) >= contrast(black, fill) {
white
} else {
black
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PaletteProblem {
Length {
len: usize,
},
UnknownRole {
index: usize,
tag: u16,
},
InvalidScheme {
index: usize,
scheme: u16,
},
Duplicate {
index: usize,
role: ColorRole,
scheme: ColorScheme,
},
}
pub fn decode_palette(bytes: &[u8]) -> (Palette, Vec<PaletteProblem>) {
let mut palette = Palette::new();
let mut problems = Vec::new();
if bytes.len() % PALETTE_ENTRY_LEN != 0 {
problems.push(PaletteProblem::Length { len: bytes.len() });
}
for (index, entry) in bytes.chunks_exact(PALETTE_ENTRY_LEN).enumerate() {
let tag = u16::from_le_bytes([entry[0], entry[1]]);
let scheme_tag = u16::from_le_bytes([entry[2], entry[3]]);
let argb = u32::from_le_bytes([entry[4], entry[5], entry[6], entry[7]]);
let Ok(role) = ColorRole::try_from(tag) else {
problems.push(PaletteProblem::UnknownRole { index, tag });
continue;
};
let scheme = match ColorScheme::try_from(scheme_tag) {
Ok(scheme @ (ColorScheme::Light | ColorScheme::Dark)) => scheme,
_ => {
problems.push(PaletteProblem::InvalidScheme {
index,
scheme: scheme_tag,
});
continue;
}
};
if palette.get(role, scheme).is_some() {
problems.push(PaletteProblem::Duplicate {
index,
role,
scheme,
});
continue;
}
let color = Color::argb(argb);
palette = match scheme {
ColorScheme::Light => palette.with_light(role, color),
_ => palette.with_dark(role, color),
};
}
(palette, problems)
}