use crate::theme::{MacAppearance, MacTheme};
use egui::{Color32, Ui};
use std::cell::Cell;
thread_local! {
static ACTIVE_SURFACE_OVERRIDE: Cell<Option<Color32>> = const { Cell::new(None) };
}
struct SurfaceScopeGuard {
prev: Option<Color32>,
}
impl Drop for SurfaceScopeGuard {
fn drop(&mut self) {
ACTIVE_SURFACE_OVERRIDE.with(|cell| cell.set(self.prev));
}
}
pub fn srgb_channel_to_linear(c: u8) -> f32 {
let s = c as f32 / 255.0;
if s <= 0.04045 {
s / 12.92
} else {
((s + 0.055) / 1.055).powf(2.4)
}
}
pub fn relative_luminance(color: Color32) -> f32 {
let r = srgb_channel_to_linear(color.r());
let g = srgb_channel_to_linear(color.g());
let b = srgb_channel_to_linear(color.b());
0.2126 * r + 0.7152 * g + 0.0722 * b
}
pub fn alpha_composite(fg: Color32, bg: Color32) -> Color32 {
let [fr, fg_c, fb, fa] = fg.to_srgba_unmultiplied();
if fa == 255 {
return Color32::from_rgb(fr, fg_c, fb);
}
let alpha = fa as f32 / 255.0;
let inv = 1.0 - alpha;
let r = (fr as f32 * alpha + bg.r() as f32 * inv).round() as u8;
let g = (fg_c as f32 * alpha + bg.g() as f32 * inv).round() as u8;
let b = (fb as f32 * alpha + bg.b() as f32 * inv).round() as u8;
Color32::from_rgb(r, g, b)
}
pub fn contrast_ratio(fg: Color32, bg: Color32) -> f32 {
let effective_fg = alpha_composite(fg, bg);
let l1 = relative_luminance(effective_fg);
let l2 = relative_luminance(bg);
let lighter = l1.max(l2);
let darker = l1.min(l2);
(lighter + 0.05) / (darker + 0.05)
}
pub fn plus_darker(fg: Color32, surface_bg: Color32, reference_bg: Color32, gain: f32) -> Color32 {
let base = alpha_composite(fg, surface_bg);
let dr = ((reference_bg.r() as i16 - surface_bg.r() as i16).unsigned_abs() as f32 * gain).round() as u8;
let dg = ((reference_bg.g() as i16 - surface_bg.g() as i16).unsigned_abs() as f32 * gain).round() as u8;
let db = ((reference_bg.b() as i16 - surface_bg.b() as i16).unsigned_abs() as f32 * gain).round() as u8;
Color32::from_rgb(
base.r().saturating_sub(dr),
base.g().saturating_sub(dg),
base.b().saturating_sub(db),
)
}
pub fn plus_lighter(fg: Color32, surface_bg: Color32, reference_bg: Color32, gain: f32) -> Color32 {
let base = alpha_composite(fg, surface_bg);
let dr = ((reference_bg.r() as i16 - surface_bg.r() as i16).unsigned_abs() as f32 * gain).round() as u8;
let dg = ((reference_bg.g() as i16 - surface_bg.g() as i16).unsigned_abs() as f32 * gain).round() as u8;
let db = ((reference_bg.b() as i16 - surface_bg.b() as i16).unsigned_abs() as f32 * gain).round() as u8;
Color32::from_rgb(
base.r().saturating_add(dr),
base.g().saturating_add(dg),
base.b().saturating_add(db),
)
}
pub fn ensure_wcag_contrast(fg: Color32, bg: Color32, min_ratio: f32) -> Color32 {
let bg_opaque = Color32::from_rgb(bg.r(), bg.g(), bg.b());
let effective_fg = alpha_composite(fg, bg_opaque);
if contrast_ratio(effective_fg, bg_opaque) >= min_ratio {
return effective_fg;
}
let preferred_pole = match MacBackgroundStyle::from_color(bg_opaque) {
MacBackgroundStyle::Light => Color32::BLACK,
MacBackgroundStyle::Dark => Color32::WHITE,
};
let pole = if contrast_ratio(preferred_pole, bg_opaque) >= min_ratio {
preferred_pole
} else if relative_luminance(bg_opaque) > 0.179 {
Color32::BLACK
} else {
Color32::WHITE
};
let mut low = 0.0_f32;
let mut high = 1.0_f32;
let mut best = pole;
for _ in 0..10 {
let mid = (low + high) * 0.5;
let r = (effective_fg.r() as f32 + (pole.r() as f32 - effective_fg.r() as f32) * mid).round() as u8;
let g = (effective_fg.g() as f32 + (pole.g() as f32 - effective_fg.g() as f32) * mid).round() as u8;
let b = (effective_fg.b() as f32 + (pole.b() as f32 - effective_fg.b() as f32) * mid).round() as u8;
let candidate = Color32::from_rgb(r, g, b);
if contrast_ratio(candidate, bg_opaque) >= min_ratio {
best = candidate;
high = mid;
} else {
low = mid;
}
}
best
}
impl MacTheme {
pub fn with_surface<R>(&self, surface_bg: Color32, f: impl FnOnce() -> R) -> R {
let prev = ACTIVE_SURFACE_OVERRIDE.with(|cell| {
let old = cell.get();
cell.set(Some(surface_bg));
old
});
let _guard = SurfaceScopeGuard { prev };
f()
}
pub fn active_surface(&self, ui: &Ui) -> Color32 {
if let Some(scoped) = ACTIVE_SURFACE_OVERRIDE.with(|cell| cell.get()) {
return scoped;
}
let panel_fill = ui.visuals().panel_fill;
let is_egui_untouched_default = panel_fill == Color32::from_rgb(248, 248, 248)
|| panel_fill == Color32::from_rgb(27, 27, 27);
if !is_egui_untouched_default {
panel_fill
} else {
self.window_background()
}
}
fn resolve_vibrant_on(&self, surface_bg: Color32, tier: u8, min_ratio: f32) -> Color32 {
let gain = self.label_metrics().vibrancy_gain.points();
let style = MacBackgroundStyle::from_color(surface_bg);
let vibrant = match style {
MacBackgroundStyle::Light => {
let pal = &self.tokens().colors().aqua;
let ref_bg = pal.window_background.to_color32();
let base = match tier {
0 => pal.label.to_color32(),
1 => pal.secondary_label.to_color32(),
_ => pal.tertiary_label.to_color32(),
};
plus_darker(base, surface_bg, ref_bg, gain)
}
MacBackgroundStyle::Dark => {
let pal = match self.appearance {
MacAppearance::Anthracite => &self.tokens().colors().anthracite,
MacAppearance::Aqua | MacAppearance::DarkAqua => {
&self.tokens().colors().dark_aqua
}
};
let ref_bg = pal.window_background.to_color32();
let base = match tier {
0 => pal.label.to_color32(),
1 => pal.secondary_label.to_color32(),
_ => pal.tertiary_label.to_color32(),
};
plus_lighter(base, surface_bg, ref_bg, gain)
}
};
ensure_wcag_contrast(vibrant, surface_bg, min_ratio)
}
pub fn label_on(&self, surface_bg: Color32) -> Color32 {
let min_ratio = self.label_metrics().wcag_aa_normal_ratio.points();
self.resolve_vibrant_on(surface_bg, 0, min_ratio)
}
pub fn secondary_label_on(&self, surface_bg: Color32) -> Color32 {
let min_ratio = self.label_metrics().wcag_aa_normal_ratio.points();
self.resolve_vibrant_on(surface_bg, 1, min_ratio)
}
pub fn tertiary_label_on(&self, surface_bg: Color32) -> Color32 {
let min_ratio = self.label_metrics().wcag_aa_large_ratio.points();
self.resolve_vibrant_on(surface_bg, 2, min_ratio)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MacBackgroundStyle {
#[default]
Light,
Dark,
}
impl MacBackgroundStyle {
pub fn from_color(bg: Color32) -> Self {
let lum = relative_luminance(bg);
let max_c = bg.r().max(bg.g()).max(bg.b());
let min_c = bg.r().min(bg.g()).min(bg.b());
let is_chromatic = (max_c - min_c) > 30;
let threshold = if is_chromatic { 0.26 } else { 0.179 };
if lum <= threshold {
Self::Dark
} else {
Self::Light
}
}
pub fn primary_text_color(&self, theme: &MacTheme) -> Color32 {
match self {
Self::Dark => Color32::from_rgb(255, 255, 255),
Self::Light => theme.tokens().colors().aqua.label.to_color32(),
}
}
pub fn secondary_text_color(&self, theme: &MacTheme) -> Color32 {
match self {
Self::Dark => Color32::from_white_alpha(217),
Self::Light => theme.tokens().colors().aqua.secondary_label.to_color32(),
}
}
pub fn tertiary_text_color(&self, theme: &MacTheme) -> Color32 {
match self {
Self::Dark => Color32::from_white_alpha(179),
Self::Light => theme.tokens().colors().aqua.placeholder_text.to_color32(),
}
}
pub fn primary_text(&self) -> Color32 {
match self {
Self::Dark => Color32::from_rgb(255, 255, 255),
Self::Light => Color32::from_rgb(0, 0, 0),
}
}
pub fn secondary_text(&self) -> Color32 {
match self {
Self::Dark => Color32::from_white_alpha(217),
Self::Light => Color32::from_black_alpha(166),
}
}
pub fn tertiary_text(&self) -> Color32 {
match self {
Self::Dark => Color32::from_white_alpha(179),
Self::Light => Color32::from_black_alpha(115),
}
}
}