Skip to main content

egui_macview/theme/
mod.rs

1//! macOS AppKit Dynamic Theming System.
2
3use crate::tokens::{MacAppearanceColors, MacDesignTokens};
4use egui::{Color32, Id};
5use std::sync::{Arc, OnceLock};
6
7pub mod accessors;
8pub mod contrast;
9pub mod pro_accessors;
10pub use contrast::{
11    alpha_composite, contrast_ratio, ensure_wcag_contrast, plus_darker, plus_lighter,
12    relative_luminance, srgb_channel_to_linear, MacBackgroundStyle,
13};
14
15static DEFAULT_TOKENS: OnceLock<Arc<MacDesignTokens>> = OnceLock::new();
16
17fn default_cached_tokens() -> Arc<MacDesignTokens> {
18    DEFAULT_TOKENS
19        .get_or_init(|| Arc::new(MacDesignTokens::load_default()))
20        .clone()
21}
22
23/// macOS system appearance mode.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
25pub enum MacAppearance {
26    /// Authentic Apple macOS light appearance (`NSAppearanceNameAqua`).
27    #[default]
28    Aqua,
29    /// Anthracite calm intermediate studio appearance mode.
30    Anthracite,
31    /// Authentic Apple macOS dark appearance (`NSAppearanceNameDarkAqua`).
32    DarkAqua,
33}
34
35/// Window caption controls presentation style (Strategy 2: Token-Driven / Configurable Presentation).
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
37pub enum WindowControlsStyle {
38    /// Authentic Apple macOS traffic lights on the leading (left) edge.
39    #[default]
40    AppleTrafficLights,
41    /// Authentic Microsoft Windows 11 caption buttons on the trailing (right) edge.
42    WindowsCaptionButtons,
43    /// Native host OS window manager decorations (decorations provided by OS).
44    NativeHost,
45}
46
47/// Dynamic theme resolving AppKit semantic styling from [`MacDesignTokens`].
48///
49/// # Example
50///
51/// ```rust
52/// use egui_macview::{MacAppearance, MacTheme, WindowControlsStyle};
53///
54/// let theme = MacTheme::new(MacAppearance::DarkAqua)
55///     .with_window_controls_style(WindowControlsStyle::AppleTrafficLights);
56///
57/// let bg = theme.window_background();
58/// let text_color = theme.label();
59/// let corner_radius = theme.button().regular.corner_radius.points();
60/// assert_eq!(corner_radius, 6.0);
61/// ```
62#[derive(Debug, Clone)]
63pub struct MacTheme {
64    /// Active macOS appearance mode ([`MacAppearance::Aqua`] or [`MacAppearance::DarkAqua`]).
65    pub appearance: MacAppearance,
66    /// Window caption controls presentation style.
67    pub window_controls_style: WindowControlsStyle,
68    tokens: Arc<MacDesignTokens>,
69    custom_accent: Option<Color32>,
70}
71
72impl Default for MacTheme {
73    fn default() -> Self {
74        Self::new(MacAppearance::Aqua)
75    }
76}
77
78impl MacTheme {
79    /// Creates a new `MacTheme` for the given appearance using the embedded default token catalog.
80    pub fn new(appearance: MacAppearance) -> Self {
81        Self {
82            appearance,
83            window_controls_style: WindowControlsStyle::AppleTrafficLights,
84            tokens: default_cached_tokens(),
85            custom_accent: None,
86        }
87    }
88
89    /// Retrieves the active `MacTheme` stored in `egui::Context` by [`MacTheme::apply_to_egui`],
90    /// falling back to [`MacTheme::default`] (`Aqua`) if none was registered.
91    pub fn from_ctx(ctx: &egui::Context) -> Self {
92        ctx.data(|d| d.get_temp::<MacTheme>(Id::NULL)).unwrap_or_default()
93    }
94
95    /// Resolves an optional borrowed [`MacTheme`], falling back to [`MacTheme::from_ctx`].
96    pub fn resolve<'a>(theme: Option<&'a Self>, ctx: &egui::Context) -> std::borrow::Cow<'a, Self> {
97        match theme {
98            Some(t) => std::borrow::Cow::Borrowed(t),
99            None => std::borrow::Cow::Owned(Self::from_ctx(ctx)),
100        }
101    }
102
103    /// Resolves an optional borrowed [`MacTheme`], falling back to the cached [`MacTheme::default`].
104    pub fn resolve_or_default<'a>(theme: Option<&'a Self>) -> std::borrow::Cow<'a, Self> {
105        match theme {
106            Some(t) => std::borrow::Cow::Borrowed(t),
107            None => std::borrow::Cow::Owned(Self::default()),
108        }
109    }
110
111    /// Sets the window controls presentation style (Strategy 2).
112    pub fn with_window_controls_style(mut self, style: WindowControlsStyle) -> Self {
113        self.window_controls_style = style;
114        self
115    }
116
117    /// Returns the active window controls presentation style.
118    pub fn window_controls_style(&self) -> WindowControlsStyle { self.window_controls_style }
119
120    /// Sets a dynamic system accent color override.
121    pub fn with_accent(mut self, accent: Color32) -> Self {
122        self.custom_accent = Some(accent);
123        self
124    }
125
126    /// Creates a `MacTheme` with custom loaded tokens.
127    pub fn with_tokens(appearance: MacAppearance, tokens: MacDesignTokens) -> Self {
128        Self { appearance, window_controls_style: WindowControlsStyle::AppleTrafficLights, tokens: Arc::new(tokens), custom_accent: None }
129    }
130
131    /// Creates a `MacTheme` with custom loaded tokens wrapped in an `Arc`.
132    pub fn with_arc_tokens(appearance: MacAppearance, tokens: Arc<MacDesignTokens>) -> Self {
133        Self { appearance, window_controls_style: WindowControlsStyle::AppleTrafficLights, tokens, custom_accent: None }
134    }
135
136    /// Returns the underlying design token catalog.
137    pub fn tokens(&self) -> &MacDesignTokens { &self.tokens }
138
139    /// Active appearance color palette.
140    pub fn colors(&self) -> &MacAppearanceColors {
141        match self.appearance {
142            MacAppearance::Aqua => &self.tokens.colors().aqua,
143            MacAppearance::Anthracite => &self.tokens.colors().anthracite,
144            MacAppearance::DarkAqua => &self.tokens.colors().dark_aqua,
145        }
146    }
147
148    /// Resolves `windowBackgroundColor` (NSColor) for the active appearance.
149    pub fn window_background(&self) -> Color32 { self.colors().window_background.to_color32() }
150    /// Resolves `controlBackgroundColor` (NSColor) for the active appearance.
151    pub fn control_background(&self) -> Color32 { self.colors().control_background.to_color32() }
152    /// Resolves `controlColor` (NSColor) for the active appearance.
153    pub fn control(&self) -> Color32 { self.colors().control.to_color32() }
154    /// Resolves `controlAccentColor` (NSColor) or custom accent override.
155    pub fn control_accent(&self) -> Color32 {
156        self.custom_accent.unwrap_or_else(|| self.colors().control_accent.to_color32())
157    }
158    /// Resolves `selectedControlColor` (NSColor) or custom accent override.
159    pub fn selected_control(&self) -> Color32 {
160        self.custom_accent.unwrap_or_else(|| self.colors().selected_control.to_color32())
161    }
162    /// Resolves `selectedControlColor` (NSColor) when the window or control is inactive/unfocused.
163    pub fn selected_control_inactive(&self) -> Color32 { self.colors().selected_control_inactive.to_color32() }
164    /// Resolves `separatorColor` (NSColor) for hairlines and borders.
165    pub fn separator(&self) -> Color32 { self.colors().separator.to_color32() }
166    /// Resolves `placeholderTextColor` (NSColor) for empty text fields and search inputs.
167    pub fn placeholder_text(&self) -> Color32 { self.colors().placeholder_text.to_color32() }
168    /// Resolves `keyboardFocusIndicatorColor` (NSColor) for focus rings.
169    pub fn keyboard_focus_indicator(&self) -> Color32 { self.colors().keyboard_focus_indicator.to_color32() }
170    /// Resolves primary `labelColor` (NSColor).
171    pub fn label(&self) -> Color32 { self.colors().label.to_color32() }
172    /// Resolves `secondaryLabelColor` (NSColor) for captions, subtitles, and section headers.
173    pub fn secondary_label(&self) -> Color32 { self.colors().secondary_label.to_color32() }
174    /// Resolves `tertiaryLabelColor` (NSColor) for subtle hints and disabled captions.
175    pub fn tertiary_label(&self) -> Color32 { self.colors().tertiary_label.to_color32() }
176    /// Resolves recessed container surface color (`box_background`) for `NSBoxPrimary`.
177    pub fn box_background(&self) -> Color32 { self.colors().box_background.to_color32() }
178    /// Resolves perimeter border color (`box_border`) for `NSBoxPrimary`.
179    pub fn box_border(&self) -> Color32 { self.colors().box_border.to_color32() }
180    /// Resolves Apple system green semantic color (`systemGreenColor`).
181    pub fn system_green(&self) -> Color32 { self.colors().system_green.to_color32() }
182    /// Resolves Apple system yellow/amber semantic color (`systemYellowColor`).
183    pub fn system_yellow(&self) -> Color32 { self.colors().system_yellow.to_color32() }
184    /// Resolves Apple system orange semantic color (`systemOrangeColor`).
185    pub fn system_orange(&self) -> Color32 { self.colors().system_orange.to_color32() }
186    /// Resolves Apple system red semantic color (`systemRedColor`).
187    pub fn system_red(&self) -> Color32 { self.colors().system_red.to_color32() }
188    /// Resolves even table row background color (`alternatingContentBackgroundColors[0]`).
189    pub fn alternating_row_even(&self) -> Color32 { self.colors().alternating_row_even.to_color32() }
190    /// Resolves odd table row background color (`alternatingContentBackgroundColors[1]`).
191    pub fn alternating_row_odd(&self) -> Color32 { self.colors().alternating_row_odd.to_color32() }
192    /// Resolves alternating content background color for the given row index (`row_index % 2`).
193    pub fn alternating_content_background(&self, row_index: usize) -> Color32 {
194        if row_index.is_multiple_of(2) { self.alternating_row_even() } else { self.alternating_row_odd() }
195    }
196    /// Resolves QuickTime / AVKit trim selection border and handle color.
197    pub fn scrubber_selection_border(&self) -> Color32 { self.colors().scrubber_selection_border.to_color32() }
198    /// Resolves dimming overlay color for timeline regions outside active trim bounds.
199    pub fn scrubber_outside_dim(&self) -> Color32 { self.colors().scrubber_outside_dim.to_color32() }
200    /// Resolves waveform amplitude bar color inside trim scrubber track.
201    pub fn scrubber_waveform(&self) -> Color32 { self.colors().scrubber_waveform.to_color32() }
202    /// Resolves playhead cursor line and timecode badge accent color.
203    pub fn scrubber_playhead(&self) -> Color32 { self.colors().scrubber_playhead.to_color32() }
204    /// Resolves high-contrast white text color used on accent fills.
205    pub fn on_accent_text(&self) -> Color32 { self.colors().on_accent_text.to_color32() }
206    /// Resolves white thumb knob fill color for switches and sliders.
207    pub fn knob_fill(&self) -> Color32 { self.colors().knob_fill.to_color32() }
208    /// Resolves subtle ambient drop shadow color for controls and floating surfaces.
209    pub fn drop_shadow(&self) -> Color32 { self.colors().drop_shadow.to_color32() }
210    /// Resolves modal backdrop dimming overlay color.
211    pub fn backdrop_dim(&self) -> Color32 { self.colors().backdrop_dim.to_color32() }
212    /// Resolves translucent dark HUD palette background color (`SPEC-006 ยง2.2`).
213    pub fn hud_background(&self) -> Color32 { self.colors().hud_background.to_color32() }
214    /// Resolves dark HUD palette hairline border color.
215    pub fn hud_border(&self) -> Color32 { self.colors().hud_border.to_color32() }
216    /// Resolves dark HUD palette primary text color.
217    pub fn hud_text(&self) -> Color32 { self.colors().hud_text.to_color32() }
218    /// Resolves dark HUD palette secondary label color.
219    pub fn hud_secondary_text(&self) -> Color32 { self.colors().hud_secondary_text.to_color32() }
220    /// Resolves macOS window close traffic light fill and border colors `(fill, border)`.
221    pub fn traffic_light_close(&self) -> (Color32, Color32) {
222        (self.colors().traffic_light_close.to_color32(), self.colors().traffic_light_close_border.to_color32())
223    }
224    /// Resolves macOS window minimize traffic light fill and border colors `(fill, border)`.
225    pub fn traffic_light_minimize(&self) -> (Color32, Color32) {
226        (self.colors().traffic_light_minimize.to_color32(), self.colors().traffic_light_minimize_border.to_color32())
227    }
228    /// Resolves macOS window zoom/fullscreen traffic light fill and border colors `(fill, border)`.
229    pub fn traffic_light_zoom(&self) -> (Color32, Color32) {
230        (self.colors().traffic_light_zoom.to_color32(), self.colors().traffic_light_zoom_border.to_color32())
231    }
232    /// Interior glyph color for hovered macOS traffic light buttons.
233    pub fn traffic_light_glyph(&self) -> Color32 { self.colors().traffic_light_glyph.to_color32() }
234    /// Background color for Windows 11 close caption button on hover (`#E81123`).
235    pub fn windows_caption_close_hover_bg(&self) -> Color32 { self.colors().caption_close_hover.to_color32() }
236    /// Background color for Windows 11 close caption button when pressed (`#C42B1C`).
237    pub fn windows_caption_close_pressed_bg(&self) -> Color32 { self.colors().caption_close_pressed.to_color32() }
238    /// Background color for Windows 11 minimize/maximize caption buttons on hover.
239    pub fn windows_caption_hover_bg(&self) -> Color32 { self.colors().caption_hover.to_color32() }
240    /// Background color for Windows 11 minimize/maximize caption buttons when pressed.
241    pub fn windows_caption_pressed_bg(&self) -> Color32 { self.colors().caption_pressed.to_color32() }
242    /// Overlay scrollbar thumb color scaled by fade `alpha_factor` in `[0.0, 1.0]`.
243    pub fn scrollbar_thumb_color(&self, active: bool, alpha_factor: f32) -> Color32 {
244        let base = if active { self.colors().scrollbar_thumb_active.to_color32() } else { self.colors().scrollbar_thumb.to_color32() };
245        base.gamma_multiply(alpha_factor.clamp(0.0, 1.0))
246    }
247
248    /// Synchronizes `egui::Context` theme, visuals, and stored `MacTheme` with the macOS appearance.
249    pub fn apply_to_egui(&self, ctx: &egui::Context) {
250        let unchanged = ctx.data(|d| {
251            d.get_temp::<MacTheme>(Id::NULL).is_some_and(|prev| {
252                prev.appearance == self.appearance
253                    && prev.window_controls_style == self.window_controls_style
254                    && prev.custom_accent == self.custom_accent
255                    && Arc::ptr_eq(&prev.tokens, &self.tokens)
256            })
257        });
258        if unchanged { return; }
259        ctx.data_mut(|d| d.insert_temp(Id::NULL, self.clone()));
260        let egui_theme = match self.appearance {
261            MacAppearance::Aqua => egui::Theme::Light,
262            MacAppearance::Anthracite | MacAppearance::DarkAqua => egui::Theme::Dark,
263        };
264        ctx.set_theme(egui_theme);
265        let mut visuals = match self.appearance {
266            MacAppearance::Aqua => egui::Visuals::light(),
267            MacAppearance::Anthracite | MacAppearance::DarkAqua => egui::Visuals::dark(),
268        };
269        let border_w = self.button().regular.border_width.points();
270        let label_color = self.label();
271        visuals.override_text_color = Some(label_color);
272        visuals.widgets.noninteractive.fg_stroke.color = label_color;
273        visuals.widgets.noninteractive.bg_stroke = egui::Stroke::new(border_w, self.separator());
274        visuals.widgets.inactive.fg_stroke.color = label_color;
275        visuals.widgets.inactive.bg_stroke = egui::Stroke::new(border_w, self.separator());
276        visuals.panel_fill = self.window_background();
277        visuals.window_fill = self.window_background();
278        visuals.window_stroke = egui::Stroke::new(border_w, self.separator());
279        let (small_pt, body_pt, heading_pt) = (self.text_field().small.font_size.points(), self.outline_view().font_size.points(), self.text_field().regular.font_size.points());
280        ctx.all_styles_mut(|style| {
281            use egui::{FontFamily, FontId, TextStyle};
282            style.text_styles = [
283                (TextStyle::Small, FontId::new(small_pt, FontFamily::Proportional)),
284                (TextStyle::Body, FontId::new(body_pt, FontFamily::Proportional)),
285                (TextStyle::Button, FontId::new(body_pt, FontFamily::Proportional)),
286                (TextStyle::Heading, FontId::new(heading_pt, FontFamily::Proportional)),
287                (TextStyle::Monospace, FontId::new(body_pt, FontFamily::Monospace)),
288            ].into();
289        });
290        ctx.set_visuals_of(egui_theme, visuals.clone());
291        ctx.set_visuals(visuals);
292    }
293}