use crate::tokens::{MacAppearanceColors, MacDesignTokens};
use egui::{Color32, Id};
use std::sync::{Arc, OnceLock};
pub mod accessors;
pub mod contrast;
pub use contrast::{
contrast_ratio, relative_luminance, srgb_channel_to_linear, MacBackgroundStyle,
};
static DEFAULT_TOKENS: OnceLock<Arc<MacDesignTokens>> = OnceLock::new();
fn default_cached_tokens() -> Arc<MacDesignTokens> {
DEFAULT_TOKENS
.get_or_init(|| Arc::new(MacDesignTokens::load_default()))
.clone()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MacAppearance {
#[default]
Aqua,
Anthracite,
DarkAqua,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WindowControlsStyle {
#[default]
AppleTrafficLights,
WindowsCaptionButtons,
NativeHost,
}
#[derive(Debug, Clone)]
pub struct MacTheme {
pub appearance: MacAppearance,
pub window_controls_style: WindowControlsStyle,
tokens: Arc<MacDesignTokens>,
custom_accent: Option<Color32>,
}
impl Default for MacTheme {
fn default() -> Self {
Self::new(MacAppearance::Aqua)
}
}
impl MacTheme {
pub fn new(appearance: MacAppearance) -> Self {
Self {
appearance,
window_controls_style: WindowControlsStyle::AppleTrafficLights,
tokens: default_cached_tokens(),
custom_accent: None,
}
}
pub fn from_ctx(ctx: &egui::Context) -> Self {
ctx.data(|d| d.get_temp::<MacTheme>(Id::NULL))
.unwrap_or_default()
}
pub fn resolve<'a>(theme: Option<&'a Self>, ctx: &egui::Context) -> std::borrow::Cow<'a, Self> {
match theme {
Some(t) => std::borrow::Cow::Borrowed(t),
None => std::borrow::Cow::Owned(Self::from_ctx(ctx)),
}
}
pub fn resolve_or_default<'a>(theme: Option<&'a Self>) -> std::borrow::Cow<'a, Self> {
match theme {
Some(t) => std::borrow::Cow::Borrowed(t),
None => std::borrow::Cow::Owned(Self::default()),
}
}
pub fn with_window_controls_style(mut self, style: WindowControlsStyle) -> Self {
self.window_controls_style = style;
self
}
pub fn window_controls_style(&self) -> WindowControlsStyle {
self.window_controls_style
}
pub fn with_accent(mut self, accent: Color32) -> Self {
self.custom_accent = Some(accent);
self
}
pub fn with_tokens(appearance: MacAppearance, tokens: MacDesignTokens) -> Self {
Self {
appearance,
window_controls_style: WindowControlsStyle::AppleTrafficLights,
tokens: Arc::new(tokens),
custom_accent: None,
}
}
pub fn with_arc_tokens(appearance: MacAppearance, tokens: Arc<MacDesignTokens>) -> Self {
Self {
appearance,
window_controls_style: WindowControlsStyle::AppleTrafficLights,
tokens,
custom_accent: None,
}
}
pub fn tokens(&self) -> &MacDesignTokens {
&self.tokens
}
pub fn colors(&self) -> &MacAppearanceColors {
match self.appearance {
MacAppearance::Aqua => &self.tokens.colors().aqua,
MacAppearance::Anthracite => &self.tokens.colors().anthracite,
MacAppearance::DarkAqua => &self.tokens.colors().dark_aqua,
}
}
pub fn window_background(&self) -> Color32 {
self.colors().window_background.to_color32()
}
pub fn control_background(&self) -> Color32 {
self.colors().control_background.to_color32()
}
pub fn control(&self) -> Color32 {
self.colors().control.to_color32()
}
pub fn control_accent(&self) -> Color32 {
self.custom_accent
.unwrap_or_else(|| self.colors().control_accent.to_color32())
}
pub fn selected_control(&self) -> Color32 {
self.custom_accent
.unwrap_or_else(|| self.colors().selected_control.to_color32())
}
pub fn selected_control_inactive(&self) -> Color32 {
self.colors().selected_control_inactive.to_color32()
}
pub fn separator(&self) -> Color32 {
self.colors().separator.to_color32()
}
pub fn placeholder_text(&self) -> Color32 {
self.colors().placeholder_text.to_color32()
}
pub fn keyboard_focus_indicator(&self) -> Color32 {
self.colors().keyboard_focus_indicator.to_color32()
}
pub fn label(&self) -> Color32 {
self.colors().label.to_color32()
}
pub fn secondary_label(&self) -> Color32 {
self.colors().secondary_label.to_color32()
}
pub fn system_green(&self) -> Color32 {
self.colors().system_green.to_color32()
}
pub fn system_yellow(&self) -> Color32 {
self.colors().system_yellow.to_color32()
}
pub fn system_orange(&self) -> Color32 {
self.colors().system_orange.to_color32()
}
pub fn system_red(&self) -> Color32 {
self.colors().system_red.to_color32()
}
pub fn alternating_row_even(&self) -> Color32 {
self.colors().alternating_row_even.to_color32()
}
pub fn alternating_row_odd(&self) -> Color32 {
self.colors().alternating_row_odd.to_color32()
}
pub fn alternating_content_background(&self, row_index: usize) -> Color32 {
if row_index.is_multiple_of(2) {
self.alternating_row_even()
} else {
self.alternating_row_odd()
}
}
pub fn scrubber_selection_border(&self) -> Color32 {
self.colors().scrubber_selection_border.to_color32()
}
pub fn scrubber_outside_dim(&self) -> Color32 {
self.colors().scrubber_outside_dim.to_color32()
}
pub fn scrubber_waveform(&self) -> Color32 {
self.colors().scrubber_waveform.to_color32()
}
pub fn scrubber_playhead(&self) -> Color32 {
self.colors().scrubber_playhead.to_color32()
}
pub fn on_accent_text(&self) -> Color32 {
self.colors().on_accent_text.to_color32()
}
pub fn knob_fill(&self) -> Color32 {
self.colors().knob_fill.to_color32()
}
pub fn drop_shadow(&self) -> Color32 {
self.colors().drop_shadow.to_color32()
}
pub fn backdrop_dim(&self) -> Color32 {
self.colors().backdrop_dim.to_color32()
}
pub fn hud_background(&self) -> Color32 {
self.colors().hud_background.to_color32()
}
pub fn hud_border(&self) -> Color32 {
self.colors().hud_border.to_color32()
}
pub fn hud_text(&self) -> Color32 {
self.colors().hud_text.to_color32()
}
pub fn hud_secondary_text(&self) -> Color32 {
self.colors().hud_secondary_text.to_color32()
}
pub fn traffic_light_close(&self) -> (Color32, Color32) {
(
self.colors().traffic_light_close.to_color32(),
self.colors().traffic_light_close_border.to_color32(),
)
}
pub fn traffic_light_minimize(&self) -> (Color32, Color32) {
(
self.colors().traffic_light_minimize.to_color32(),
self.colors().traffic_light_minimize_border.to_color32(),
)
}
pub fn traffic_light_zoom(&self) -> (Color32, Color32) {
(
self.colors().traffic_light_zoom.to_color32(),
self.colors().traffic_light_zoom_border.to_color32(),
)
}
pub fn traffic_light_glyph(&self) -> Color32 {
self.colors().traffic_light_glyph.to_color32()
}
pub fn windows_caption_close_hover_bg(&self) -> Color32 {
self.colors().caption_close_hover.to_color32()
}
pub fn windows_caption_close_pressed_bg(&self) -> Color32 {
self.colors().caption_close_pressed.to_color32()
}
pub fn windows_caption_hover_bg(&self) -> Color32 {
self.colors().caption_hover.to_color32()
}
pub fn windows_caption_pressed_bg(&self) -> Color32 {
self.colors().caption_pressed.to_color32()
}
pub fn scrollbar_thumb_color(&self, active: bool, alpha_factor: f32) -> Color32 {
let base = if active {
self.colors().scrollbar_thumb_active.to_color32()
} else {
self.colors().scrollbar_thumb.to_color32()
};
base.gamma_multiply(alpha_factor.clamp(0.0, 1.0))
}
pub fn apply_to_egui(&self, ctx: &egui::Context) {
let unchanged = ctx.data(|d| {
d.get_temp::<MacTheme>(Id::NULL).is_some_and(|prev| {
prev.appearance == self.appearance
&& prev.window_controls_style == self.window_controls_style
&& prev.custom_accent == self.custom_accent
&& Arc::ptr_eq(&prev.tokens, &self.tokens)
})
});
if unchanged {
return;
}
ctx.data_mut(|d| d.insert_temp(Id::NULL, self.clone()));
let egui_theme = match self.appearance {
MacAppearance::Aqua => egui::Theme::Light,
MacAppearance::Anthracite | MacAppearance::DarkAqua => egui::Theme::Dark,
};
ctx.set_theme(egui_theme);
let mut visuals = match self.appearance {
MacAppearance::Aqua => egui::Visuals::light(),
MacAppearance::Anthracite | MacAppearance::DarkAqua => egui::Visuals::dark(),
};
let border_w = self.button().regular.border_width.points();
let label_color = self.label();
visuals.override_text_color = Some(label_color);
visuals.widgets.noninteractive.fg_stroke.color = label_color;
visuals.widgets.noninteractive.bg_stroke = egui::Stroke::new(border_w, self.separator());
visuals.widgets.inactive.fg_stroke.color = label_color;
visuals.widgets.inactive.bg_stroke = egui::Stroke::new(border_w, self.separator());
visuals.panel_fill = self.window_background();
visuals.window_fill = self.window_background();
visuals.window_stroke = egui::Stroke::new(border_w, self.separator());
let small_pt = self.text_field().small.font_size.points();
let body_pt = self.outline_view().font_size.points();
let heading_pt = self.text_field().regular.font_size.points();
ctx.all_styles_mut(|style| {
use egui::{FontFamily, FontId, TextStyle};
style.text_styles = [
(
TextStyle::Small,
FontId::new(small_pt, FontFamily::Proportional),
),
(
TextStyle::Body,
FontId::new(body_pt, FontFamily::Proportional),
),
(
TextStyle::Button,
FontId::new(body_pt, FontFamily::Proportional),
),
(
TextStyle::Heading,
FontId::new(heading_pt, FontFamily::Proportional),
),
(
TextStyle::Monospace,
FontId::new(body_pt, FontFamily::Monospace),
),
]
.into();
});
ctx.set_visuals_of(egui_theme, visuals.clone());
ctx.set_visuals(visuals);
}
}