1use crate::theme::{MacAppearance, MacTheme};
10use egui::{Color32, Ui};
11use std::cell::Cell;
12use std::sync::OnceLock;
13
14const MAX_PASS_SURFACE_RECTS: usize = 32;
15
16#[derive(Clone, Copy)]
17struct PassSurfaceRects {
18 pass_nr: u64,
19 ctx_token: u64,
20 len: usize,
21 entries: [(egui::Rect, Color32); MAX_PASS_SURFACE_RECTS],
22}
23
24impl PassSurfaceRects {
25 const EMPTY: Self = Self {
26 pass_nr: u64::MAX,
27 ctx_token: 0,
28 len: 0,
29 entries: [(egui::Rect::NOTHING, Color32::TRANSPARENT); MAX_PASS_SURFACE_RECTS],
30 };
31}
32
33thread_local! {
34 static PASS_SURFACE_RECTS: Cell<PassSurfaceRects> = const { Cell::new(PassSurfaceRects::EMPTY) };
35 static NEXT_PASS_TOKEN: Cell<u64> = const { Cell::new(1) };
36 static EXPLICIT_SURFACE_CELL: Cell<Option<Color32>> = const { Cell::new(None) };
37}
38
39static EGUI_DEFAULT_PANEL_FILLS: OnceLock<(Color32, Color32)> = OnceLock::new();
40
41struct ExplicitSurfaceGuard {
42 prev: Option<Color32>,
43}
44
45impl Drop for ExplicitSurfaceGuard {
46 fn drop(&mut self) {
47 EXPLICIT_SURFACE_CELL.with(|cell| cell.set(self.prev));
48 }
49}
50
51pub fn srgb_channel_to_linear(c: u8) -> f32 {
53 let s = c as f32 / 255.0;
54 if s <= 0.04045 {
55 s / 12.92
56 } else {
57 ((s + 0.055) / 1.055).powf(2.4)
58 }
59}
60
61pub fn relative_luminance(color: Color32) -> f32 {
64 let r = srgb_channel_to_linear(color.r());
65 let g = srgb_channel_to_linear(color.g());
66 let b = srgb_channel_to_linear(color.b());
67 0.2126 * r + 0.7152 * g + 0.0722 * b
68}
69
70pub fn alpha_composite(fg: Color32, bg: Color32) -> Color32 {
73 let [fr, fg_c, fb, fa] = fg.to_srgba_unmultiplied();
74 if fa == 255 {
75 return Color32::from_rgb(fr, fg_c, fb);
76 }
77 let alpha = fa as f32 / 255.0;
78 let inv = 1.0 - alpha;
79 let r = (fr as f32 * alpha + bg.r() as f32 * inv).round() as u8;
80 let g = (fg_c as f32 * alpha + bg.g() as f32 * inv).round() as u8;
81 let b = (fb as f32 * alpha + bg.b() as f32 * inv).round() as u8;
82 Color32::from_rgb(r, g, b)
83}
84
85pub fn contrast_ratio(fg: Color32, bg: Color32) -> f32 {
88 let effective_fg = alpha_composite(fg, bg);
89 let l1 = relative_luminance(effective_fg);
90 let l2 = relative_luminance(bg);
91
92 let lighter = l1.max(l2);
93 let darker = l1.min(l2);
94
95 (lighter + 0.05) / (darker + 0.05)
96}
97
98pub fn plus_darker(fg: Color32, surface_bg: Color32, reference_bg: Color32, gain: f32) -> Color32 {
104 let base = alpha_composite(fg, surface_bg);
105 let dr = ((reference_bg.r() as i16 - surface_bg.r() as i16).unsigned_abs() as f32 * gain).round() as u8;
106 let dg = ((reference_bg.g() as i16 - surface_bg.g() as i16).unsigned_abs() as f32 * gain).round() as u8;
107 let db = ((reference_bg.b() as i16 - surface_bg.b() as i16).unsigned_abs() as f32 * gain).round() as u8;
108 Color32::from_rgb(
109 base.r().saturating_sub(dr),
110 base.g().saturating_sub(dg),
111 base.b().saturating_sub(db),
112 )
113}
114
115pub fn plus_lighter(fg: Color32, surface_bg: Color32, reference_bg: Color32, gain: f32) -> Color32 {
121 let base = alpha_composite(fg, surface_bg);
122 let dr = ((reference_bg.r() as i16 - surface_bg.r() as i16).unsigned_abs() as f32 * gain).round() as u8;
123 let dg = ((reference_bg.g() as i16 - surface_bg.g() as i16).unsigned_abs() as f32 * gain).round() as u8;
124 let db = ((reference_bg.b() as i16 - surface_bg.b() as i16).unsigned_abs() as f32 * gain).round() as u8;
125 Color32::from_rgb(
126 base.r().saturating_add(dr),
127 base.g().saturating_add(dg),
128 base.b().saturating_add(db),
129 )
130}
131
132pub fn ensure_wcag_contrast(fg: Color32, bg: Color32, min_ratio: f32) -> Color32 {
136 let bg_opaque = Color32::from_rgb(bg.r(), bg.g(), bg.b());
137 let effective_fg = alpha_composite(fg, bg_opaque);
138 if contrast_ratio(effective_fg, bg_opaque) >= min_ratio {
139 return effective_fg;
140 }
141
142 let preferred_pole = match MacBackgroundStyle::from_color(bg_opaque) {
143 MacBackgroundStyle::Light => Color32::BLACK,
144 MacBackgroundStyle::Dark => Color32::WHITE,
145 };
146 let pole = if contrast_ratio(preferred_pole, bg_opaque) >= min_ratio {
147 preferred_pole
148 } else if relative_luminance(bg_opaque) > 0.179 {
149 Color32::BLACK
150 } else {
151 Color32::WHITE
152 };
153
154 let mut low = 0.0_f32;
155 let mut high = 1.0_f32;
156 let mut best = pole;
157 for _ in 0..10 {
158 let mid = (low + high) * 0.5;
159 let r = (effective_fg.r() as f32 + (pole.r() as f32 - effective_fg.r() as f32) * mid).round() as u8;
160 let g = (effective_fg.g() as f32 + (pole.g() as f32 - effective_fg.g() as f32) * mid).round() as u8;
161 let b = (effective_fg.b() as f32 + (pole.b() as f32 - effective_fg.b() as f32) * mid).round() as u8;
162 let candidate = Color32::from_rgb(r, g, b);
163 if contrast_ratio(candidate, bg_opaque) >= min_ratio {
164 best = candidate;
165 high = mid;
166 } else {
167 low = mid;
168 }
169 }
170 best
171}
172
173impl MacTheme {
174 pub fn with_surface<R>(&self, surface_bg: Color32, f: impl FnOnce() -> R) -> R {
177 let prev = EXPLICIT_SURFACE_CELL.with(|cell| {
178 let old = cell.get();
179 cell.set(Some(surface_bg));
180 old
181 });
182 let _guard = ExplicitSurfaceGuard { prev };
183 f()
184 }
185
186 fn is_default_panel_fill(&self, panel_fill: Color32) -> bool {
187 let (egui_light, egui_dark) = *EGUI_DEFAULT_PANEL_FILLS
188 .get_or_init(|| (egui::Visuals::light().panel_fill, egui::Visuals::dark().panel_fill));
189 let colors = self.tokens().colors();
190 panel_fill == egui_light
191 || panel_fill == egui_dark
192 || panel_fill == colors.aqua.window_background.to_color32()
193 || panel_fill == colors.anthracite.window_background.to_color32()
194 || panel_fill == colors.dark_aqua.window_background.to_color32()
195 }
196
197 pub fn register_surface_rect(
201 &self,
202 ctx: &egui::Context,
203 rect: egui::Rect,
204 surface_bg: Color32,
205 ) {
206 let _ = self.register_surface_rect_slot(ctx, rect, surface_bg);
207 }
208
209 pub(crate) fn register_surface_rect_slot(
210 &self,
211 ctx: &egui::Context,
212 rect: egui::Rect,
213 surface_bg: Color32,
214 ) -> usize {
215 let pass_nr = ctx.cumulative_pass_nr();
216 let pass_id = egui::Id::unique("macview_surface_pass_token");
217 PASS_SURFACE_RECTS.with(|cell| {
218 let mut state = cell.get();
219 let same_pass = state.pass_nr == pass_nr
220 && state.ctx_token != 0
221 && ctx.data(|d| d.get_temp::<(u64, u64)>(pass_id))
222 == Some((pass_nr, state.ctx_token));
223 if !same_pass {
224 let token = NEXT_PASS_TOKEN.with(|t| {
225 let next = t.get().wrapping_add(1).max(1);
226 t.set(next);
227 next
228 });
229 ctx.data_mut(|d| d.insert_temp(pass_id, (pass_nr, token)));
230 state.pass_nr = pass_nr;
231 state.ctx_token = token;
232 state.len = 0;
233 }
234 let slot = if state.len < MAX_PASS_SURFACE_RECTS {
235 let s = state.len;
236 state.entries[s] = (rect, surface_bg);
237 state.len += 1;
238 s
239 } else {
240 state.entries.copy_within(1..MAX_PASS_SURFACE_RECTS, 0);
241 state.entries[MAX_PASS_SURFACE_RECTS - 1] = (rect, surface_bg);
242 MAX_PASS_SURFACE_RECTS - 1
243 };
244 cell.set(state);
245 slot
246 })
247 }
248
249 pub(crate) fn update_surface_rect_slot(
250 &self,
251 ctx: &egui::Context,
252 slot: usize,
253 rect: egui::Rect,
254 ) {
255 let pass_nr = ctx.cumulative_pass_nr();
256 PASS_SURFACE_RECTS.with(|cell| {
257 let mut state = cell.get();
258 if state.pass_nr == pass_nr && slot < state.len {
259 state.entries[slot].0 = rect;
260 cell.set(state);
261 }
262 });
263 }
264
265 fn lookup_registered_surface(ctx: &egui::Context, pos: egui::Pos2) -> Option<Color32> {
266 let pass_nr = ctx.cumulative_pass_nr();
267 let (token, bg) = PASS_SURFACE_RECTS.with(|cell| {
268 let state = cell.get();
269 if state.pass_nr != pass_nr || state.len == 0 {
270 return None;
271 }
272 for idx in (0..state.len).rev() {
273 let (rect, bg) = state.entries[idx];
274 if rect == egui::Rect::NOTHING {
275 return None;
276 }
277 if rect.contains(pos) {
278 return Some((state.ctx_token, bg));
279 }
280 }
281 None
282 })?;
283 let pass_id = egui::Id::unique("macview_surface_pass_token");
284 (ctx.data(|d| d.get_temp::<(u64, u64)>(pass_id)) == Some((pass_nr, token))).then_some(bg)
285 }
286
287 pub fn active_surface_at(&self, ui: &Ui, pos: egui::Pos2) -> Color32 {
296 if let Some(bg) = Self::lookup_registered_surface(ui.ctx(), pos) {
297 return bg;
298 }
299 let panel_fill = ui.visuals().panel_fill;
300 if !self.is_default_panel_fill(panel_fill) {
301 return panel_fill;
302 }
303 if let Some(override_bg) = EXPLICIT_SURFACE_CELL.with(|cell| cell.get()) {
304 return override_bg;
305 }
306 self.window_background()
307 }
308
309 pub fn active_surface(&self, ui: &Ui) -> Color32 {
311 self.active_surface_at(ui, ui.next_widget_position())
312 }
313
314 fn resolve_vibrant_on(&self, surface_bg: Color32, tier: u8, min_ratio: f32) -> Color32 {
315 let gain = self.label_metrics().vibrancy_gain.points();
316 let style = MacBackgroundStyle::from_color(surface_bg);
317 let vibrant = match style {
318 MacBackgroundStyle::Light => {
319 let pal = &self.tokens().colors().aqua;
320 let ref_bg = pal.window_background.to_color32();
321 let base = match tier {
322 0 => pal.label.to_color32(),
323 1 => pal.secondary_label.to_color32(),
324 _ => pal.tertiary_label.to_color32(),
325 };
326 plus_darker(base, surface_bg, ref_bg, gain)
327 }
328 MacBackgroundStyle::Dark => {
329 let pal = match self.appearance {
330 MacAppearance::Anthracite => &self.tokens().colors().anthracite,
331 MacAppearance::Aqua | MacAppearance::DarkAqua => {
332 &self.tokens().colors().dark_aqua
333 }
334 };
335 let ref_bg = pal.window_background.to_color32();
336 let base = match tier {
337 0 => pal.label.to_color32(),
338 1 => pal.secondary_label.to_color32(),
339 _ => pal.tertiary_label.to_color32(),
340 };
341 plus_lighter(base, surface_bg, ref_bg, gain)
342 }
343 };
344 ensure_wcag_contrast(vibrant, surface_bg, min_ratio)
345 }
346
347 pub fn label_on(&self, surface_bg: Color32) -> Color32 {
350 let min_ratio = self.label_metrics().wcag_aa_normal_ratio.points();
351 self.resolve_vibrant_on(surface_bg, 0, min_ratio)
352 }
353
354 pub fn secondary_label_on(&self, surface_bg: Color32) -> Color32 {
357 let min_ratio = self.label_metrics().wcag_aa_normal_ratio.points();
358 self.resolve_vibrant_on(surface_bg, 1, min_ratio)
359 }
360
361 pub fn tertiary_label_on(&self, surface_bg: Color32) -> Color32 {
364 let min_ratio = self.label_metrics().wcag_aa_large_ratio.points();
365 self.resolve_vibrant_on(surface_bg, 2, min_ratio)
366 }
367}
368
369#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
374pub enum MacBackgroundStyle {
375 #[default]
377 Light,
378 Dark,
380}
381
382impl MacBackgroundStyle {
383 pub fn from_color(bg: Color32) -> Self {
393 let lum = relative_luminance(bg);
394 let max_c = bg.r().max(bg.g()).max(bg.b());
395 let min_c = bg.r().min(bg.g()).min(bg.b());
396 let is_chromatic = (max_c - min_c) > 30;
397 let threshold = if is_chromatic { 0.26 } else { 0.179 };
398 if lum <= threshold {
399 Self::Dark
400 } else {
401 Self::Light
402 }
403 }
404
405 pub fn primary_text_color(&self, theme: &MacTheme) -> Color32 {
410 match self {
411 Self::Dark => Color32::from_rgb(255, 255, 255),
412 Self::Light => theme.tokens().colors().aqua.label.to_color32(),
413 }
414 }
415
416 pub fn secondary_text_color(&self, theme: &MacTheme) -> Color32 {
421 match self {
422 Self::Dark => Color32::from_white_alpha(217),
423 Self::Light => theme.tokens().colors().aqua.secondary_label.to_color32(),
424 }
425 }
426
427 pub fn tertiary_text_color(&self, theme: &MacTheme) -> Color32 {
432 match self {
433 Self::Dark => Color32::from_white_alpha(179),
434 Self::Light => theme.tokens().colors().aqua.placeholder_text.to_color32(),
435 }
436 }
437
438 pub fn primary_text(&self) -> Color32 {
440 match self {
441 Self::Dark => Color32::from_rgb(255, 255, 255),
442 Self::Light => Color32::from_rgb(0, 0, 0),
443 }
444 }
445
446 pub fn secondary_text(&self) -> Color32 {
448 match self {
449 Self::Dark => Color32::from_white_alpha(217),
450 Self::Light => Color32::from_black_alpha(166),
451 }
452 }
453
454 pub fn tertiary_text(&self) -> Color32 {
456 match self {
457 Self::Dark => Color32::from_white_alpha(179),
458 Self::Light => Color32::from_black_alpha(115),
459 }
460 }
461}