1use crate::theme::{MacAppearance, MacTheme};
10use egui::{Color32, Ui};
11use std::cell::Cell;
12
13thread_local! {
14 static ACTIVE_SURFACE_OVERRIDE: Cell<Option<Color32>> = const { Cell::new(None) };
15}
16
17struct SurfaceScopeGuard {
18 prev: Option<Color32>,
19}
20
21impl Drop for SurfaceScopeGuard {
22 fn drop(&mut self) {
23 ACTIVE_SURFACE_OVERRIDE.with(|cell| cell.set(self.prev));
24 }
25}
26
27pub fn srgb_channel_to_linear(c: u8) -> f32 {
29 let s = c as f32 / 255.0;
30 if s <= 0.04045 {
31 s / 12.92
32 } else {
33 ((s + 0.055) / 1.055).powf(2.4)
34 }
35}
36
37pub fn relative_luminance(color: Color32) -> f32 {
40 let r = srgb_channel_to_linear(color.r());
41 let g = srgb_channel_to_linear(color.g());
42 let b = srgb_channel_to_linear(color.b());
43 0.2126 * r + 0.7152 * g + 0.0722 * b
44}
45
46pub fn alpha_composite(fg: Color32, bg: Color32) -> Color32 {
49 let [fr, fg_c, fb, fa] = fg.to_srgba_unmultiplied();
50 if fa == 255 {
51 return Color32::from_rgb(fr, fg_c, fb);
52 }
53 let alpha = fa as f32 / 255.0;
54 let inv = 1.0 - alpha;
55 let r = (fr as f32 * alpha + bg.r() as f32 * inv).round() as u8;
56 let g = (fg_c as f32 * alpha + bg.g() as f32 * inv).round() as u8;
57 let b = (fb as f32 * alpha + bg.b() as f32 * inv).round() as u8;
58 Color32::from_rgb(r, g, b)
59}
60
61pub fn contrast_ratio(fg: Color32, bg: Color32) -> f32 {
64 let effective_fg = alpha_composite(fg, bg);
65 let l1 = relative_luminance(effective_fg);
66 let l2 = relative_luminance(bg);
67
68 let lighter = l1.max(l2);
69 let darker = l1.min(l2);
70
71 (lighter + 0.05) / (darker + 0.05)
72}
73
74pub fn plus_darker(fg: Color32, surface_bg: Color32, reference_bg: Color32, gain: f32) -> Color32 {
80 let base = alpha_composite(fg, surface_bg);
81 let dr = ((reference_bg.r() as i16 - surface_bg.r() as i16).unsigned_abs() as f32 * gain).round() as u8;
82 let dg = ((reference_bg.g() as i16 - surface_bg.g() as i16).unsigned_abs() as f32 * gain).round() as u8;
83 let db = ((reference_bg.b() as i16 - surface_bg.b() as i16).unsigned_abs() as f32 * gain).round() as u8;
84 Color32::from_rgb(
85 base.r().saturating_sub(dr),
86 base.g().saturating_sub(dg),
87 base.b().saturating_sub(db),
88 )
89}
90
91pub fn plus_lighter(fg: Color32, surface_bg: Color32, reference_bg: Color32, gain: f32) -> Color32 {
97 let base = alpha_composite(fg, surface_bg);
98 let dr = ((reference_bg.r() as i16 - surface_bg.r() as i16).unsigned_abs() as f32 * gain).round() as u8;
99 let dg = ((reference_bg.g() as i16 - surface_bg.g() as i16).unsigned_abs() as f32 * gain).round() as u8;
100 let db = ((reference_bg.b() as i16 - surface_bg.b() as i16).unsigned_abs() as f32 * gain).round() as u8;
101 Color32::from_rgb(
102 base.r().saturating_add(dr),
103 base.g().saturating_add(dg),
104 base.b().saturating_add(db),
105 )
106}
107
108pub fn ensure_wcag_contrast(fg: Color32, bg: Color32, min_ratio: f32) -> Color32 {
112 let bg_opaque = Color32::from_rgb(bg.r(), bg.g(), bg.b());
113 let effective_fg = alpha_composite(fg, bg_opaque);
114 if contrast_ratio(effective_fg, bg_opaque) >= min_ratio {
115 return effective_fg;
116 }
117
118 let preferred_pole = match MacBackgroundStyle::from_color(bg_opaque) {
119 MacBackgroundStyle::Light => Color32::BLACK,
120 MacBackgroundStyle::Dark => Color32::WHITE,
121 };
122 let pole = if contrast_ratio(preferred_pole, bg_opaque) >= min_ratio {
123 preferred_pole
124 } else if relative_luminance(bg_opaque) > 0.179 {
125 Color32::BLACK
126 } else {
127 Color32::WHITE
128 };
129
130 let mut low = 0.0_f32;
131 let mut high = 1.0_f32;
132 let mut best = pole;
133 for _ in 0..10 {
134 let mid = (low + high) * 0.5;
135 let r = (effective_fg.r() as f32 + (pole.r() as f32 - effective_fg.r() as f32) * mid).round() as u8;
136 let g = (effective_fg.g() as f32 + (pole.g() as f32 - effective_fg.g() as f32) * mid).round() as u8;
137 let b = (effective_fg.b() as f32 + (pole.b() as f32 - effective_fg.b() as f32) * mid).round() as u8;
138 let candidate = Color32::from_rgb(r, g, b);
139 if contrast_ratio(candidate, bg_opaque) >= min_ratio {
140 best = candidate;
141 high = mid;
142 } else {
143 low = mid;
144 }
145 }
146 best
147}
148
149impl MacTheme {
150 pub fn with_surface<R>(&self, surface_bg: Color32, f: impl FnOnce() -> R) -> R {
153 let prev = ACTIVE_SURFACE_OVERRIDE.with(|cell| {
154 let old = cell.get();
155 cell.set(Some(surface_bg));
156 old
157 });
158 let _guard = SurfaceScopeGuard { prev };
159 f()
160 }
161
162 pub fn active_surface(&self, ui: &Ui) -> Color32 {
169 if let Some(scoped) = ACTIVE_SURFACE_OVERRIDE.with(|cell| cell.get()) {
170 return scoped;
171 }
172 let panel_fill = ui.visuals().panel_fill;
173 let is_egui_untouched_default = panel_fill == Color32::from_rgb(248, 248, 248)
174 || panel_fill == Color32::from_rgb(27, 27, 27);
175 if !is_egui_untouched_default {
176 panel_fill
177 } else {
178 self.window_background()
179 }
180 }
181
182 fn resolve_vibrant_on(&self, surface_bg: Color32, tier: u8, min_ratio: f32) -> Color32 {
183 let gain = self.label_metrics().vibrancy_gain.points();
184 let style = MacBackgroundStyle::from_color(surface_bg);
185 let vibrant = match style {
186 MacBackgroundStyle::Light => {
187 let pal = &self.tokens().colors().aqua;
188 let ref_bg = pal.window_background.to_color32();
189 let base = match tier {
190 0 => pal.label.to_color32(),
191 1 => pal.secondary_label.to_color32(),
192 _ => pal.tertiary_label.to_color32(),
193 };
194 plus_darker(base, surface_bg, ref_bg, gain)
195 }
196 MacBackgroundStyle::Dark => {
197 let pal = match self.appearance {
198 MacAppearance::Anthracite => &self.tokens().colors().anthracite,
199 MacAppearance::Aqua | MacAppearance::DarkAqua => {
200 &self.tokens().colors().dark_aqua
201 }
202 };
203 let ref_bg = pal.window_background.to_color32();
204 let base = match tier {
205 0 => pal.label.to_color32(),
206 1 => pal.secondary_label.to_color32(),
207 _ => pal.tertiary_label.to_color32(),
208 };
209 plus_lighter(base, surface_bg, ref_bg, gain)
210 }
211 };
212 ensure_wcag_contrast(vibrant, surface_bg, min_ratio)
213 }
214
215 pub fn label_on(&self, surface_bg: Color32) -> Color32 {
218 let min_ratio = self.label_metrics().wcag_aa_normal_ratio.points();
219 self.resolve_vibrant_on(surface_bg, 0, min_ratio)
220 }
221
222 pub fn secondary_label_on(&self, surface_bg: Color32) -> Color32 {
225 let min_ratio = self.label_metrics().wcag_aa_normal_ratio.points();
226 self.resolve_vibrant_on(surface_bg, 1, min_ratio)
227 }
228
229 pub fn tertiary_label_on(&self, surface_bg: Color32) -> Color32 {
232 let min_ratio = self.label_metrics().wcag_aa_large_ratio.points();
233 self.resolve_vibrant_on(surface_bg, 2, min_ratio)
234 }
235}
236
237#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
242pub enum MacBackgroundStyle {
243 #[default]
245 Light,
246 Dark,
248}
249
250impl MacBackgroundStyle {
251 pub fn from_color(bg: Color32) -> Self {
261 let lum = relative_luminance(bg);
262 let max_c = bg.r().max(bg.g()).max(bg.b());
263 let min_c = bg.r().min(bg.g()).min(bg.b());
264 let is_chromatic = (max_c - min_c) > 30;
265 let threshold = if is_chromatic { 0.26 } else { 0.179 };
266 if lum <= threshold {
267 Self::Dark
268 } else {
269 Self::Light
270 }
271 }
272
273 pub fn primary_text_color(&self, theme: &MacTheme) -> Color32 {
278 match self {
279 Self::Dark => Color32::from_rgb(255, 255, 255),
280 Self::Light => theme.tokens().colors().aqua.label.to_color32(),
281 }
282 }
283
284 pub fn secondary_text_color(&self, theme: &MacTheme) -> Color32 {
289 match self {
290 Self::Dark => Color32::from_white_alpha(217),
291 Self::Light => theme.tokens().colors().aqua.secondary_label.to_color32(),
292 }
293 }
294
295 pub fn tertiary_text_color(&self, theme: &MacTheme) -> Color32 {
300 match self {
301 Self::Dark => Color32::from_white_alpha(179),
302 Self::Light => theme.tokens().colors().aqua.placeholder_text.to_color32(),
303 }
304 }
305
306 pub fn primary_text(&self) -> Color32 {
308 match self {
309 Self::Dark => Color32::from_rgb(255, 255, 255),
310 Self::Light => Color32::from_rgb(0, 0, 0),
311 }
312 }
313
314 pub fn secondary_text(&self) -> Color32 {
316 match self {
317 Self::Dark => Color32::from_white_alpha(217),
318 Self::Light => Color32::from_black_alpha(166),
319 }
320 }
321
322 pub fn tertiary_text(&self) -> Color32 {
324 match self {
325 Self::Dark => Color32::from_white_alpha(179),
326 Self::Light => Color32::from_black_alpha(115),
327 }
328 }
329}