1use crate::{NamedTheme, ThemeColors};
2
3const AA_NORMAL: f64 = 4.5;
4const AA_LARGE: f64 = 3.0;
5const AAA_NORMAL: f64 = 7.0;
6
7pub struct ContrastResult {
8 pub theme_name: String,
9 pub variant: String,
10 pub pair: String,
11 pub bg_hex: String,
12 pub fg_hex: String,
13 pub ratio: f64,
14 pub grade: String,
15 pub pass: bool,
16}
17
18fn wcag_grade(ratio: f64) -> String {
19 if ratio >= AAA_NORMAL {
20 "AAA".to_string()
21 } else if ratio >= AA_NORMAL {
22 "AA".to_string()
23 } else if ratio >= AA_LARGE {
24 "AA-large".to_string()
25 } else {
26 "FAIL".to_string()
27 }
28}
29
30fn check_theme_colors(
31 theme_name: &str,
32 variant: &str,
33 colors: &ThemeColors,
34) -> Vec<ContrastResult> {
35 let checks = [
36 ("bg", "fg", colors.bg, colors.fg),
37 ("bg", "accent", colors.bg, colors.accent),
38 ("bg", "accent_secondary", colors.bg, colors.accent_secondary),
39 ("bg", "muted", colors.bg, colors.muted),
40 ("bg", "success", colors.bg, colors.success),
41 ("bg", "warning", colors.bg, colors.warning),
42 ("bg", "danger", colors.bg, colors.danger),
43 ("bg", "highlight", colors.bg, colors.highlight),
44 ("dialog_bg", "fg", colors.dialog_bg, colors.fg),
45 ("dialog_bg", "accent", colors.dialog_bg, colors.accent),
46 (
47 "selection_bg",
48 "selection_fg",
49 colors.selection_bg,
50 colors.selection_fg,
51 ),
52 ];
53
54 checks
55 .iter()
56 .map(|(bg_name, fg_name, bg, fg)| {
57 let ratio = bg.contrast_ratio(fg);
58 let grade = wcag_grade(ratio);
59 let pass = ratio >= AA_NORMAL;
60
61 ContrastResult {
62 theme_name: theme_name.to_string(),
63 variant: variant.to_string(),
64 pair: format!("{} ↔ {}", bg_name, fg_name),
65 bg_hex: bg.to_hex(),
66 fg_hex: fg.to_hex(),
67 ratio,
68 grade,
69 pass,
70 }
71 })
72 .collect()
73}
74
75pub fn check_all_themes(themes: &[NamedTheme]) -> Vec<ContrastResult> {
76 let mut results = Vec::new();
77
78 for theme in themes {
79 if let Some(ref dark) = theme.variants.dark {
80 results.extend(check_theme_colors(&theme.name, "dark", dark));
81 }
82 if let Some(ref light) = theme.variants.light {
83 results.extend(check_theme_colors(&theme.name, "light", light));
84 }
85 }
86
87 results
88}
89
90pub fn print_results(results: &[ContrastResult], verbose: bool) {
91 let failures: Vec<_> = results.iter().filter(|r| !r.pass).collect();
92 let passes: Vec<_> = results.iter().filter(|r| r.pass).collect();
93
94 println!("{}", "=".repeat(80));
95 println!("WCAG CONTRAST CHECK RESULTS");
96 println!("Target: {}:1 (WCAG AA Normal Text)", AA_NORMAL);
97 println!("{}", "=".repeat(80));
98
99 if failures.is_empty() {
100 println!(
101 "\n✅ All {} color pairs pass WCAG AA requirements!",
102 results.len()
103 );
104 } else {
105 println!("\n❌ FAILURES ({} issues)\n", failures.len());
106 println!(
107 "{:<20} {:<8} {:<30} {:>8} {:<10}",
108 "Theme", "Variant", "Pair", "Ratio", "Grade"
109 );
110 println!("{}", "-".repeat(80));
111
112 let mut current_theme = String::new();
113 for r in &failures {
114 let theme_label = if r.theme_name != current_theme {
115 current_theme = r.theme_name.clone();
116 &r.theme_name
117 } else {
118 ""
119 };
120 println!(
121 "{:<20} {:<8} {:<30} {:>7.2}:1 {:<10}",
122 theme_label, r.variant, r.pair, r.ratio, r.grade
123 );
124 println!("{:20} {:8} bg: {} fg: {}", "", "", r.bg_hex, r.fg_hex);
125 }
126 }
127
128 if verbose && !passes.is_empty() {
129 println!("\n✅ PASSING ({} checks)\n", passes.len());
130 for r in &passes {
131 println!(
132 "{:<20} {:<8} {:<30} {:>7.2}:1 {}",
133 r.theme_name, r.variant, r.pair, r.ratio, r.grade
134 );
135 }
136 }
137
138 println!("\n{}", "=".repeat(80));
139 println!("SUMMARY BY THEME");
140 println!("{}", "=".repeat(80));
141
142 let mut theme_stats: std::collections::HashMap<(String, String), (usize, usize)> =
143 std::collections::HashMap::new();
144
145 for r in results {
146 let key = (r.theme_name.clone(), r.variant.clone());
147 let entry = theme_stats.entry(key).or_insert((0, 0));
148 if r.pass {
149 entry.0 += 1;
150 } else {
151 entry.1 += 1;
152 }
153 }
154
155 println!(
156 "\n{:<20} {:<8} {:>6} {:>6} {:<10}",
157 "Theme", "Variant", "Pass", "Fail", "Status"
158 );
159 println!("{}", "-".repeat(55));
160
161 let mut keys: Vec<_> = theme_stats.keys().collect();
162 keys.sort();
163
164 for (theme, variant) in keys {
165 let (pass, fail) = theme_stats.get(&(theme.clone(), variant.clone())).unwrap();
166 let status = if *fail == 0 {
167 "✅ OK".to_string()
168 } else {
169 format!("❌ {} issues", fail)
170 };
171 println!(
172 "{:<20} {:<8} {:>6} {:>6} {}",
173 theme, variant, pass, fail, status
174 );
175 }
176}