1use crate::color::Color;
12use crate::contrast::{contrast_ratio, AA_NON_TEXT, DARK_BG, LIGHT_BG};
13
14#[derive(Debug, Clone, Copy)]
19pub struct AdaptiveBrand {
20 pub light: Color,
22 pub dark: Color,
24 pub light_adjusted: bool,
26 pub dark_adjusted: bool,
28 pub light_clears_aa: bool,
33 pub dark_clears_aa: bool,
35}
36
37pub fn adaptive_brand(brand: &Color) -> AdaptiveBrand {
39 let light_bg = Color::from_hex(LIGHT_BG).expect("constant");
40 let dark_bg = Color::from_hex(DARK_BG).expect("constant");
41
42 let (light, light_adjusted, light_clears_aa) =
43 adjust_for_bg(brand, &light_bg, false);
44 let (dark, dark_adjusted, dark_clears_aa) =
45 adjust_for_bg(brand, &dark_bg, true);
46
47 AdaptiveBrand {
48 light,
49 dark,
50 light_adjusted,
51 dark_adjusted,
52 light_clears_aa,
53 dark_clears_aa,
54 }
55}
56
57fn adjust_for_bg(color: &Color, bg: &Color, lighten: bool) -> (Color, bool, bool) {
61 if contrast_ratio(bg, color) >= AA_NON_TEXT {
62 return (*color, false, true);
63 }
64 let mut c = *color;
65 let mut cleared = false;
66 for _ in 0..20 {
67 let new_l = if lighten {
68 (c.l + 0.05).min(1.0)
69 } else {
70 (c.l - 0.05).max(0.0)
71 };
72 if (new_l - c.l).abs() < f64::EPSILON {
73 break;
74 }
75 c = Color::from_oklch(new_l, c.c, c.h);
76 if contrast_ratio(bg, &c) >= AA_NON_TEXT {
77 cleared = true;
78 break;
79 }
80 }
81 (c, true, cleared)
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 fn c(hex: &str) -> Color {
89 Color::from_hex(hex).unwrap()
90 }
91
92 #[test]
93 fn very_dark_navy_lightens_for_dark_mode_only() {
94 let navy = c("#0a1a2e");
96 let a = adaptive_brand(&navy);
97 assert!(!a.light_adjusted, "should pass against white");
98 assert!(a.dark_adjusted, "should fail against dark bg");
99 assert!(a.dark_clears_aa, "should clear AA after lightening");
100 assert!(a.dark.l > navy.l, "dark variant should be lighter");
101 }
102
103 #[test]
104 fn mid_tone_passes_both_modes_unchanged() {
105 let mid = c("#0d9488");
106 let a = adaptive_brand(&mid);
107 assert!(!a.light_adjusted);
108 assert!(!a.dark_adjusted);
109 assert!(a.light_clears_aa);
110 assert!(a.dark_clears_aa);
111 assert_eq!(a.light.to_hex(), mid.to_hex());
112 assert_eq!(a.dark.to_hex(), mid.to_hex());
113 }
114
115 #[test]
116 fn adjustment_flag_separates_attempted_from_succeeded() {
117 let navy = c("#0a1a2e");
120 let a = adaptive_brand(&navy);
121 assert!(a.dark_adjusted && a.dark_clears_aa);
122 }
123}