egui_macview/theme/contrast.rs
1//! macOS AppKit Dynamic Contrast & Background Style Engine.
2//!
3//! Grounded in W3C WCAG 2.1 ยง1.4.3 (Level AA) and Apple AppKit `NSBackgroundStyle`.
4//! Automatically resolves foreground typography tints against dynamic or custom
5//! background fills (such as custom accent colors) to guarantee >= 4.5:1 contrast.
6
7use crate::theme::MacTheme;
8use egui::Color32;
9
10/// Converts an 8-bit sRGB channel component in `[0..255]` to linear luminance in `[0.0..1.0]`.
11pub fn srgb_channel_to_linear(c: u8) -> f32 {
12 let s = c as f32 / 255.0;
13 if s <= 0.04045 {
14 s / 12.92
15 } else {
16 ((s + 0.055) / 1.055).powf(2.4)
17 }
18}
19
20/// Computes the relative luminance of an sRGB color according to WCAG 2.1:
21/// `L = 0.2126 * R_lin + 0.7152 * G_lin + 0.0722 * B_lin`.
22pub fn relative_luminance(color: Color32) -> f32 {
23 let r = srgb_channel_to_linear(color.r());
24 let g = srgb_channel_to_linear(color.g());
25 let b = srgb_channel_to_linear(color.b());
26 0.2126 * r + 0.7152 * g + 0.0722 * b
27}
28
29/// Computes the WCAG 2.1 contrast ratio between foreground and background colors.
30/// If foreground has alpha < 255, it is alpha-blended over the background first.
31pub fn contrast_ratio(fg: Color32, bg: Color32) -> f32 {
32 let alpha = fg.a() as f32 / 255.0;
33 let blended_r = ((fg.r() as f32 * alpha) + (bg.r() as f32 * (1.0 - alpha))).round() as u8;
34 let blended_g = ((fg.g() as f32 * alpha) + (bg.g() as f32 * (1.0 - alpha))).round() as u8;
35 let blended_b = ((fg.b() as f32 * alpha) + (bg.b() as f32 * (1.0 - alpha))).round() as u8;
36 let effective_fg = Color32::from_rgb(blended_r, blended_g, blended_b);
37
38 let l1 = relative_luminance(effective_fg);
39 let l2 = relative_luminance(bg);
40
41 let lighter = l1.max(l2);
42 let darker = l1.min(l2);
43
44 (lighter + 0.05) / (darker + 0.05)
45}
46
47/// macOS AppKit background style (`NSBackgroundStyle`).
48///
49/// Indicates whether an element's background is light or dark, allowing child
50/// text, icons, and accessories to select high-contrast tints that satisfy WCAG 2.1 AA.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
52pub enum MacBackgroundStyle {
53 /// Background is light (relative luminance `L > 0.179`). Use dark foreground text.
54 #[default]
55 Light,
56 /// Background is dark (relative luminance `L <= 0.179`). Use light/white foreground text.
57 Dark,
58}
59
60impl MacBackgroundStyle {
61 /// Resolves `MacBackgroundStyle` from a background color's WCAG 2.1 relative luminance.
62 ///
63 /// Applies the normative WCAG 2.1 AA threshold `L = 0.179`:
64 /// - `L <= 0.179` => `Dark` style (white text guarantees >= 4.5:1 contrast)
65 /// - `L > 0.179` => `Light` style (dark text guarantees >= 4.5:1 contrast)
66 pub fn from_color(bg: Color32) -> Self {
67 let lum = relative_luminance(bg);
68 if lum <= 0.179 {
69 Self::Dark
70 } else {
71 Self::Light
72 }
73 }
74
75 /// Primary text color guaranteeing >= 4.5:1 WCAG contrast against this background.
76 ///
77 /// - `Dark` => 100% white (`#FFFFFF`)
78 /// - `Light` => dark primary label color from Aqua tokens (`#262626`)
79 pub fn primary_text_color(&self, theme: &MacTheme) -> Color32 {
80 match self {
81 Self::Dark => Color32::from_rgb(255, 255, 255),
82 Self::Light => theme.tokens().colors().aqua.label.to_color32(),
83 }
84 }
85
86 /// Secondary text color (subtitles, captions, metadata).
87 ///
88 /// - `Dark` => 85% white (`rgba(255, 255, 255, 0.85)`)
89 /// - `Light` => dark secondary label color from Aqua tokens (`#6E6E73`)
90 pub fn secondary_text_color(&self, theme: &MacTheme) -> Color32 {
91 match self {
92 Self::Dark => Color32::from_white_alpha(217),
93 Self::Light => theme.tokens().colors().aqua.secondary_label.to_color32(),
94 }
95 }
96
97 /// Tertiary text color (placeholders, disabled hints).
98 ///
99 /// - `Dark` => 70% white (`rgba(255, 255, 255, 0.70)`)
100 /// - `Light` => dark placeholder text color from Aqua tokens
101 pub fn tertiary_text_color(&self, theme: &MacTheme) -> Color32 {
102 match self {
103 Self::Dark => Color32::from_white_alpha(179),
104 Self::Light => theme.tokens().colors().aqua.placeholder_text.to_color32(),
105 }
106 }
107
108 /// Standalone primary text color without requiring a `MacTheme` reference.
109 pub fn primary_text(&self) -> Color32 {
110 match self {
111 Self::Dark => Color32::from_rgb(255, 255, 255),
112 Self::Light => Color32::from_rgb(0, 0, 0),
113 }
114 }
115
116 /// Standalone secondary text color without requiring a `MacTheme` reference.
117 pub fn secondary_text(&self) -> Color32 {
118 match self {
119 Self::Dark => Color32::from_white_alpha(217),
120 Self::Light => Color32::from_black_alpha(166),
121 }
122 }
123
124 /// Standalone tertiary text color without requiring a `MacTheme` reference.
125 pub fn tertiary_text(&self) -> Color32 {
126 match self {
127 Self::Dark => Color32::from_white_alpha(179),
128 Self::Light => Color32::from_black_alpha(115),
129 }
130 }
131}