use crate::high_contrast::wcag_contrast;
use crate::CooljapanTheme;
use oxiui_core::{Color, FontSpec, Palette};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WcagLevel {
AA,
AAA,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ContrastWarning {
pub pair: (&'static str, &'static str),
pub ratio: f64,
pub required: f64,
pub level: WcagLevel,
}
#[derive(Clone, Debug)]
pub struct ValidationResult {
pub warnings: Vec<ContrastWarning>,
pub is_aa_compliant: bool,
pub is_aaa_compliant: bool,
}
#[derive(Clone, Debug, Default)]
pub struct PaletteBuilder {
background: Option<Color>,
surface: Option<Color>,
text_primary: Option<Color>,
text_secondary: Option<Color>,
primary: Option<Color>,
on_primary: Option<Color>,
}
impl PaletteBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn background(mut self, c: Color) -> Self {
self.background = Some(c);
self
}
pub fn surface(mut self, c: Color) -> Self {
self.surface = Some(c);
self
}
pub fn text_primary(mut self, c: Color) -> Self {
self.text_primary = Some(c);
self
}
pub fn text_secondary(mut self, c: Color) -> Self {
self.text_secondary = Some(c);
self
}
pub fn primary(mut self, c: Color) -> Self {
self.primary = Some(c);
self
}
pub fn on_primary(mut self, c: Color) -> Self {
self.on_primary = Some(c);
self
}
fn resolved_background(&self) -> Color {
self.background.unwrap_or(Color(255, 255, 255, 255))
}
fn resolved_surface(&self) -> Color {
self.surface.unwrap_or(Color(255, 255, 255, 255))
}
fn resolved_text_primary(&self) -> Color {
self.text_primary.unwrap_or(Color(0, 0, 0, 255))
}
fn resolved_text_secondary(&self) -> Color {
self.text_secondary.unwrap_or(Color(60, 60, 60, 255))
}
fn resolved_primary(&self) -> Color {
self.primary.unwrap_or(Color(0, 0, 200, 255))
}
fn resolved_on_primary(&self) -> Color {
self.on_primary.unwrap_or(Color(255, 255, 255, 255))
}
pub fn validate(&self) -> ValidationResult {
let bg = self.resolved_background();
let surface = self.resolved_surface();
let text = self.resolved_text_primary();
let muted = self.resolved_text_secondary();
let primary = self.resolved_primary();
let on_primary = self.resolved_on_primary();
let pairs: &[(Color, Color, &'static str, &'static str)] = &[
(text, bg, "text_primary", "background"),
(muted, bg, "text_secondary", "background"),
(text, surface, "text_primary", "surface"),
(muted, surface, "text_secondary", "surface"),
(on_primary, primary, "on_primary", "primary"),
];
let mut warnings = Vec::new();
for &(fg, back, fg_name, bg_name) in pairs {
let ratio = wcag_contrast((fg.0, fg.1, fg.2), (back.0, back.1, back.2));
if ratio < 4.5 {
warnings.push(ContrastWarning {
pair: (fg_name, bg_name),
ratio,
required: 4.5,
level: WcagLevel::AA,
});
} else if ratio < 7.0 {
warnings.push(ContrastWarning {
pair: (fg_name, bg_name),
ratio,
required: 7.0,
level: WcagLevel::AAA,
});
}
}
let is_aa_compliant = warnings.iter().all(|w| w.level != WcagLevel::AA);
let is_aaa_compliant = warnings.is_empty();
ValidationResult {
warnings,
is_aa_compliant,
is_aaa_compliant,
}
}
pub fn build(self) -> Result<CooljapanTheme, Vec<ContrastWarning>> {
let result = self.validate();
let aa_failures: Vec<ContrastWarning> = result
.warnings
.into_iter()
.filter(|w| w.level == WcagLevel::AA)
.collect();
if !aa_failures.is_empty() {
return Err(aa_failures);
}
let palette = Palette {
background: self.resolved_background(),
surface: self.resolved_surface(),
primary: self.resolved_primary(),
on_primary: self.resolved_on_primary(),
text: self.resolved_text_primary(),
muted: self.resolved_text_secondary(),
};
Ok(CooljapanTheme::new(
palette,
FontSpec::new("Inter", 14.0, 400),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use oxiui_core::Color;
fn high_contrast_builder() -> PaletteBuilder {
PaletteBuilder::new()
.background(Color(0, 0, 0, 255))
.surface(Color(10, 10, 26, 255))
.text_primary(Color(255, 255, 255, 255))
.text_secondary(Color(200, 200, 200, 255))
.primary(Color(255, 255, 0, 255))
.on_primary(Color(0, 0, 0, 255))
}
#[test]
fn builder_valid_palette_builds() {
let result = high_contrast_builder().build();
assert!(
result.is_ok(),
"high-contrast builder should succeed: {:?}",
result.err()
);
}
#[test]
fn builder_aaa_flag() {
let result = high_contrast_builder().validate();
assert!(
result.is_aaa_compliant,
"all pairs should be AAA; warnings: {:?}",
result.warnings
);
}
#[test]
fn builder_low_contrast_warns() {
let result = PaletteBuilder::new()
.background(Color(255, 255, 255, 255))
.text_primary(Color(200, 200, 200, 255)) .validate();
assert!(
!result.warnings.is_empty(),
"should warn about low contrast"
);
}
#[test]
fn builder_aa_failure_returns_err() {
let result = PaletteBuilder::new()
.background(Color(255, 255, 255, 255))
.text_primary(Color(240, 240, 240, 255))
.build();
assert!(result.is_err(), "near-white on white should fail AA build");
}
#[test]
fn builder_default_is_accessible() {
let result = PaletteBuilder::new().validate();
assert!(result.is_aa_compliant);
}
}