use crate::constraints::{Constraint, ConstraintContext, SpecReference, Violation, WcagLevel};
use accesskit::NodeId;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SrgbColor {
pub r: f32,
pub g: f32,
pub b: f32,
}
impl SrgbColor {
pub fn new(r: f32, g: f32, b: f32) -> Self {
Self { r, g, b }
}
pub fn from_u8(r: u8, g: u8, b: u8) -> Self {
Self {
r: f32::from(r) / 255.0,
g: f32::from(g) / 255.0,
b: f32::from(b) / 255.0,
}
}
pub fn to_hex(&self) -> String {
let r = (self.r * 255.0) as u8;
let g = (self.g * 255.0) as u8;
let b = (self.b * 255.0) as u8;
format!("#{r:02x}{g:02x}{b:02x}")
}
}
#[cfg(feature = "color")]
#[tracing::instrument(level = "debug")]
pub fn contrast_ratio(fg: &SrgbColor, bg: &SrgbColor) -> f32 {
use palette::Srgb;
use palette::color_difference::Wcag21RelativeContrast;
let fg_srgb: Srgb<f32> = Srgb::new(fg.r, fg.g, fg.b);
let bg_srgb: Srgb<f32> = Srgb::new(bg.r, bg.g, bg.b);
fg_srgb.relative_contrast(bg_srgb)
}
#[cfg(not(feature = "color"))]
#[tracing::instrument(level = "debug")]
pub fn contrast_ratio(fg: &SrgbColor, bg: &SrgbColor) -> f32 {
fn linearize(c: f32) -> f32 {
if c <= 0.04045 {
c / 12.92
} else {
((c + 0.055) / 1.055).powf(2.4)
}
}
fn luminance(color: &SrgbColor) -> f32 {
0.2126 * linearize(color.r) + 0.7152 * linearize(color.g) + 0.0722 * linearize(color.b)
}
let l1 = luminance(fg);
let l2 = luminance(bg);
let (lighter, darker) = if l1 > l2 { (l1, l2) } else { (l2, l1) };
(lighter + 0.05) / (darker + 0.05)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TextSize {
Normal,
Large,
}
#[derive(Debug, Clone)]
pub struct ContrastMinimum {
pub foreground: SrgbColor,
pub background: SrgbColor,
pub text_size: TextSize,
}
impl Constraint for ContrastMinimum {
#[tracing::instrument(level = "debug", skip(self, _ctx))]
fn check(&self, _node_id: NodeId, _ctx: &ConstraintContext<'_>) -> Result<(), Violation> {
let ratio = contrast_ratio(&self.foreground, &self.background);
let required = match self.text_size {
TextSize::Normal => 4.5,
TextSize::Large => 3.0,
};
if ratio >= required {
Ok(())
} else {
Err(Violation::ContrastInsufficient {
actual: ratio,
required,
foreground: self.foreground.to_hex(),
background: self.background.to_hex(),
})
}
}
fn spec_ref(&self) -> SpecReference {
SpecReference::Wcag {
criterion: "1.4.3",
level: WcagLevel::AA,
url: "https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum",
}
}
}
#[derive(Debug, Clone)]
pub struct ContrastEnhanced {
pub foreground: SrgbColor,
pub background: SrgbColor,
pub text_size: TextSize,
}
impl Constraint for ContrastEnhanced {
#[tracing::instrument(level = "debug", skip(self, _ctx))]
fn check(&self, _node_id: NodeId, _ctx: &ConstraintContext<'_>) -> Result<(), Violation> {
let ratio = contrast_ratio(&self.foreground, &self.background);
let required = match self.text_size {
TextSize::Normal => 7.0,
TextSize::Large => 4.5,
};
if ratio >= required {
Ok(())
} else {
Err(Violation::ContrastInsufficient {
actual: ratio,
required,
foreground: self.foreground.to_hex(),
background: self.background.to_hex(),
})
}
}
fn spec_ref(&self) -> SpecReference {
SpecReference::Wcag {
criterion: "1.4.6",
level: WcagLevel::AAA,
url: "https://www.w3.org/WAI/WCAG21/Understanding/contrast-enhanced",
}
}
}
#[derive(Debug, Clone)]
pub struct NonTextContrast {
pub foreground: SrgbColor,
pub background: SrgbColor,
}
impl Constraint for NonTextContrast {
#[tracing::instrument(level = "debug", skip(self, _ctx))]
fn check(&self, _node_id: NodeId, _ctx: &ConstraintContext<'_>) -> Result<(), Violation> {
let ratio = contrast_ratio(&self.foreground, &self.background);
let required = 3.0_f32;
if ratio >= required {
Ok(())
} else {
Err(Violation::ContrastInsufficient {
actual: ratio,
required,
foreground: self.foreground.to_hex(),
background: self.background.to_hex(),
})
}
}
fn spec_ref(&self) -> SpecReference {
SpecReference::Wcag {
criterion: "1.4.11",
level: WcagLevel::AA,
url: "https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast",
}
}
}