Skip to main content

egui_macview/theme/
mod.rs

1//! macOS AppKit Dynamic Theming System.
2//!
3//! Grounded in Apple AppKit `NSAppearance` semantics and W3C DTCG design tokens.
4//! Guarantees zero hardcoded constants by deriving all geometry metrics,
5//! typography bounds, and semantic colors dynamically from [`MacDesignTokens`].
6
7use crate::tokens::{MacAppearanceColors, MacDesignTokens};
8use egui::{Color32, Id};
9use std::sync::{Arc, OnceLock};
10
11pub mod accessors;
12pub mod contrast;
13pub use contrast::{
14    contrast_ratio, relative_luminance, srgb_channel_to_linear, MacBackgroundStyle,
15};
16
17static DEFAULT_TOKENS: OnceLock<Arc<MacDesignTokens>> = OnceLock::new();
18
19fn default_cached_tokens() -> Arc<MacDesignTokens> {
20    DEFAULT_TOKENS
21        .get_or_init(|| Arc::new(MacDesignTokens::load_default()))
22        .clone()
23}
24
25/// macOS system appearance mode.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum MacAppearance {
28    /// Authentic Apple macOS light appearance (`NSAppearanceNameAqua`).
29    #[default]
30    Aqua,
31    /// Anthracite calm intermediate studio appearance mode.
32    Anthracite,
33    /// Authentic Apple macOS dark appearance (`NSAppearanceNameDarkAqua`).
34    DarkAqua,
35}
36
37/// Window caption controls presentation style (Strategy 2: Token-Driven / Configurable Presentation).
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39pub enum WindowControlsStyle {
40    /// Authentic Apple macOS traffic lights on the leading (left) edge.
41    #[default]
42    AppleTrafficLights,
43    /// Authentic Microsoft Windows 11 caption buttons on the trailing (right) edge.
44    WindowsCaptionButtons,
45    /// Native host OS window manager decorations (decorations provided by OS).
46    NativeHost,
47}
48
49/// Dynamic theme resolving AppKit semantic styling from [`MacDesignTokens`].
50///
51/// # Example
52///
53/// ```rust
54/// use egui_macview::{MacAppearance, MacTheme, WindowControlsStyle};
55///
56/// let theme = MacTheme::new(MacAppearance::DarkAqua)
57///     .with_window_controls_style(WindowControlsStyle::AppleTrafficLights);
58///
59/// let bg = theme.window_background();
60/// let text_color = theme.label();
61/// let corner_radius = theme.button().regular.corner_radius.points();
62/// assert_eq!(corner_radius, 6.0);
63/// ```
64#[derive(Debug, Clone)]
65pub struct MacTheme {
66    /// Active macOS appearance mode ([`MacAppearance::Aqua`] or [`MacAppearance::DarkAqua`]).
67    pub appearance: MacAppearance,
68    /// Window caption controls presentation style.
69    pub window_controls_style: WindowControlsStyle,
70    tokens: Arc<MacDesignTokens>,
71    custom_accent: Option<Color32>,
72}
73
74impl Default for MacTheme {
75    fn default() -> Self {
76        Self::new(MacAppearance::Aqua)
77    }
78}
79
80impl MacTheme {
81    /// Creates a new `MacTheme` for the given appearance using the embedded default token catalog.
82    pub fn new(appearance: MacAppearance) -> Self {
83        Self {
84            appearance,
85            window_controls_style: WindowControlsStyle::AppleTrafficLights,
86            tokens: default_cached_tokens(),
87            custom_accent: None,
88        }
89    }
90
91    /// Retrieves the active `MacTheme` stored in `egui::Context` by [`MacTheme::apply_to_egui`],
92    /// falling back to [`MacTheme::default`] (`Aqua`) if none was registered.
93    pub fn from_ctx(ctx: &egui::Context) -> Self {
94        ctx.data(|d| d.get_temp::<MacTheme>(Id::NULL))
95            .unwrap_or_default()
96    }
97
98    /// Resolves an optional borrowed [`MacTheme`], falling back to [`MacTheme::from_ctx`].
99    pub fn resolve<'a>(theme: Option<&'a Self>, ctx: &egui::Context) -> std::borrow::Cow<'a, Self> {
100        match theme {
101            Some(t) => std::borrow::Cow::Borrowed(t),
102            None => std::borrow::Cow::Owned(Self::from_ctx(ctx)),
103        }
104    }
105
106    /// Sets the window controls presentation style (Strategy 2).
107    pub fn with_window_controls_style(mut self, style: WindowControlsStyle) -> Self {
108        self.window_controls_style = style;
109        self
110    }
111
112    /// Returns the active window controls presentation style.
113    pub fn window_controls_style(&self) -> WindowControlsStyle {
114        self.window_controls_style
115    }
116
117    /// Sets a dynamic system accent color override (matching macOS System Settings Appearance Accent).
118    pub fn with_accent(mut self, accent: Color32) -> Self {
119        self.custom_accent = Some(accent);
120        self
121    }
122
123    /// Creates a `MacTheme` with custom loaded tokens.
124    pub fn with_tokens(appearance: MacAppearance, tokens: MacDesignTokens) -> Self {
125        Self {
126            appearance,
127            window_controls_style: WindowControlsStyle::AppleTrafficLights,
128            tokens: Arc::new(tokens),
129            custom_accent: None,
130        }
131    }
132
133    /// Creates a `MacTheme` with custom loaded tokens wrapped in an `Arc`.
134    pub fn with_arc_tokens(appearance: MacAppearance, tokens: Arc<MacDesignTokens>) -> Self {
135        Self {
136            appearance,
137            window_controls_style: WindowControlsStyle::AppleTrafficLights,
138            tokens,
139            custom_accent: None,
140        }
141    }
142
143    /// Returns the underlying design token catalog.
144    pub fn tokens(&self) -> &MacDesignTokens {
145        &self.tokens
146    }
147
148    /// Active appearance color palette.
149    pub fn colors(&self) -> &MacAppearanceColors {
150        match self.appearance {
151            MacAppearance::Aqua => &self.tokens.colors().aqua,
152            MacAppearance::Anthracite => &self.tokens.colors().anthracite,
153            MacAppearance::DarkAqua => &self.tokens.colors().dark_aqua,
154        }
155    }
156
157    /// Resolves `windowBackgroundColor` (NSColor) for the active appearance.
158    pub fn window_background(&self) -> Color32 {
159        self.colors().window_background.to_color32()
160    }
161
162    /// Resolves `controlBackgroundColor` (NSColor) for the active appearance.
163    pub fn control_background(&self) -> Color32 {
164        self.colors().control_background.to_color32()
165    }
166
167    /// Resolves `controlColor` (NSColor) for the active appearance.
168    pub fn control(&self) -> Color32 {
169        self.colors().control.to_color32()
170    }
171
172    /// Resolves `controlAccentColor` (NSColor) or custom accent override.
173    pub fn control_accent(&self) -> Color32 {
174        self.custom_accent
175            .unwrap_or_else(|| self.colors().control_accent.to_color32())
176    }
177
178    /// Resolves `selectedControlColor` (NSColor) or custom accent override.
179    pub fn selected_control(&self) -> Color32 {
180        self.custom_accent
181            .unwrap_or_else(|| self.colors().selected_control.to_color32())
182    }
183
184    /// Resolves `selectedControlColor` (NSColor) when the window or control is inactive/unfocused.
185    pub fn selected_control_inactive(&self) -> Color32 {
186        self.colors().selected_control_inactive.to_color32()
187    }
188
189    /// Resolves `separatorColor` (NSColor) for hairlines and borders.
190    pub fn separator(&self) -> Color32 {
191        self.colors().separator.to_color32()
192    }
193
194    /// Resolves `placeholderTextColor` (NSColor) for empty text fields and search inputs.
195    pub fn placeholder_text(&self) -> Color32 {
196        self.colors().placeholder_text.to_color32()
197    }
198
199    /// Resolves `keyboardFocusIndicatorColor` (NSColor) for focus rings.
200    pub fn keyboard_focus_indicator(&self) -> Color32 {
201        self.colors().keyboard_focus_indicator.to_color32()
202    }
203
204    /// Resolves primary `labelColor` (NSColor).
205    pub fn label(&self) -> Color32 {
206        self.colors().label.to_color32()
207    }
208
209    /// Resolves `secondaryLabelColor` (NSColor) for captions, subtitles, and section headers.
210    pub fn secondary_label(&self) -> Color32 {
211        self.colors().secondary_label.to_color32()
212    }
213
214    /// Resolves Apple system green semantic color (`systemGreenColor`).
215    pub fn system_green(&self) -> Color32 {
216        self.colors().system_green.to_color32()
217    }
218
219    /// Resolves Apple system yellow/amber semantic color (`systemYellowColor`).
220    pub fn system_yellow(&self) -> Color32 {
221        self.colors().system_yellow.to_color32()
222    }
223
224    /// Resolves Apple system orange semantic color (`systemOrangeColor`).
225    pub fn system_orange(&self) -> Color32 {
226        self.colors().system_orange.to_color32()
227    }
228
229    /// Resolves Apple system red semantic color (`systemRedColor`).
230    pub fn system_red(&self) -> Color32 {
231        self.colors().system_red.to_color32()
232    }
233
234    /// Resolves even table row background color (`alternatingContentBackgroundColors[0]`).
235    pub fn alternating_row_even(&self) -> Color32 {
236        self.colors().alternating_row_even.to_color32()
237    }
238
239    /// Resolves odd table row background color (`alternatingContentBackgroundColors[1]`).
240    pub fn alternating_row_odd(&self) -> Color32 {
241        self.colors().alternating_row_odd.to_color32()
242    }
243
244    /// Resolves alternating content background color for the given row index (`row_index % 2`).
245    pub fn alternating_content_background(&self, row_index: usize) -> Color32 {
246        if row_index.is_multiple_of(2) {
247            self.alternating_row_even()
248        } else {
249            self.alternating_row_odd()
250        }
251    }
252
253    /// Resolves QuickTime / AVKit trim selection border and handle color.
254    pub fn scrubber_selection_border(&self) -> Color32 {
255        self.colors().scrubber_selection_border.to_color32()
256    }
257
258    /// Resolves dimming overlay color for timeline regions outside active trim bounds.
259    pub fn scrubber_outside_dim(&self) -> Color32 {
260        self.colors().scrubber_outside_dim.to_color32()
261    }
262
263    /// Resolves waveform amplitude bar color inside trim scrubber track.
264    pub fn scrubber_waveform(&self) -> Color32 {
265        self.colors().scrubber_waveform.to_color32()
266    }
267
268    /// Resolves playhead cursor line and timecode badge accent color.
269    pub fn scrubber_playhead(&self) -> Color32 {
270        self.colors().scrubber_playhead.to_color32()
271    }
272
273    /// Resolves high-contrast white text color used on accent fills.
274    pub fn on_accent_text(&self) -> Color32 {
275        self.colors().on_accent_text.to_color32()
276    }
277
278    /// Resolves white thumb knob fill color for switches and sliders.
279    pub fn knob_fill(&self) -> Color32 {
280        self.colors().knob_fill.to_color32()
281    }
282
283    /// Resolves subtle ambient drop shadow color for controls and floating surfaces.
284    pub fn drop_shadow(&self) -> Color32 {
285        self.colors().drop_shadow.to_color32()
286    }
287
288    /// Resolves modal backdrop dimming overlay color.
289    pub fn backdrop_dim(&self) -> Color32 {
290        self.colors().backdrop_dim.to_color32()
291    }
292
293    /// Resolves translucent dark HUD palette background color (`SPEC-006 ยง2.2`).
294    pub fn hud_background(&self) -> Color32 {
295        self.colors().hud_background.to_color32()
296    }
297
298    /// Resolves dark HUD palette hairline border color.
299    pub fn hud_border(&self) -> Color32 {
300        self.colors().hud_border.to_color32()
301    }
302
303    /// Resolves dark HUD palette primary text color.
304    pub fn hud_text(&self) -> Color32 {
305        self.colors().hud_text.to_color32()
306    }
307
308    /// Resolves dark HUD palette secondary label color.
309    pub fn hud_secondary_text(&self) -> Color32 {
310        self.colors().hud_secondary_text.to_color32()
311    }
312
313    /// Resolves macOS window close traffic light fill and border colors `(fill, border)`.
314    pub fn traffic_light_close(&self) -> (Color32, Color32) {
315        (
316            self.colors().traffic_light_close.to_color32(),
317            self.colors().traffic_light_close_border.to_color32(),
318        )
319    }
320
321    /// Resolves macOS window minimize traffic light fill and border colors `(fill, border)`.
322    pub fn traffic_light_minimize(&self) -> (Color32, Color32) {
323        (
324            self.colors().traffic_light_minimize.to_color32(),
325            self.colors().traffic_light_minimize_border.to_color32(),
326        )
327    }
328
329    /// Resolves macOS window zoom/fullscreen traffic light fill and border colors `(fill, border)`.
330    pub fn traffic_light_zoom(&self) -> (Color32, Color32) {
331        (
332            self.colors().traffic_light_zoom.to_color32(),
333            self.colors().traffic_light_zoom_border.to_color32(),
334        )
335    }
336
337    /// Interior glyph color for hovered macOS traffic light buttons.
338    pub fn traffic_light_glyph(&self) -> Color32 {
339        self.colors().traffic_light_glyph.to_color32()
340    }
341
342    /// Background color for Windows 11 close caption button on hover (`#E81123`).
343    pub fn windows_caption_close_hover_bg(&self) -> Color32 {
344        self.colors().caption_close_hover.to_color32()
345    }
346
347    /// Background color for Windows 11 close caption button when pressed (`#C42B1C`).
348    pub fn windows_caption_close_pressed_bg(&self) -> Color32 {
349        self.colors().caption_close_pressed.to_color32()
350    }
351
352    /// Background color for Windows 11 minimize/maximize caption buttons on hover.
353    pub fn windows_caption_hover_bg(&self) -> Color32 {
354        self.colors().caption_hover.to_color32()
355    }
356
357    /// Background color for Windows 11 minimize/maximize caption buttons when pressed.
358    pub fn windows_caption_pressed_bg(&self) -> Color32 {
359        self.colors().caption_pressed.to_color32()
360    }
361
362    /// Overlay scrollbar thumb color scaled by fade `alpha_factor` in `[0.0, 1.0]`.
363    pub fn scrollbar_thumb_color(&self, active: bool, alpha_factor: f32) -> Color32 {
364        let base = if active {
365            self.colors().scrollbar_thumb_active.to_color32()
366        } else {
367            self.colors().scrollbar_thumb.to_color32()
368        };
369        base.gamma_multiply(alpha_factor.clamp(0.0, 1.0))
370    }
371
372    /// Synchronizes `egui::Context` theme, visuals, and stored `MacTheme` with the macOS appearance.
373    ///
374    /// Performs an $O(1)$ short-circuit when the active `MacTheme` stored in `ctx` already matches
375    /// the current appearance, window controls style, accent override, and token catalog pointer.
376    pub fn apply_to_egui(&self, ctx: &egui::Context) {
377        let unchanged = ctx.data(|d| {
378            d.get_temp::<MacTheme>(Id::NULL).is_some_and(|prev| {
379                prev.appearance == self.appearance
380                    && prev.window_controls_style == self.window_controls_style
381                    && prev.custom_accent == self.custom_accent
382                    && Arc::ptr_eq(&prev.tokens, &self.tokens)
383            })
384        });
385        if unchanged {
386            return;
387        }
388        ctx.data_mut(|d| d.insert_temp(Id::NULL, self.clone()));
389
390        let egui_theme = match self.appearance {
391            MacAppearance::Aqua => egui::Theme::Light,
392            MacAppearance::Anthracite | MacAppearance::DarkAqua => egui::Theme::Dark,
393        };
394        ctx.set_theme(egui_theme);
395
396        let mut visuals = match self.appearance {
397            MacAppearance::Aqua => egui::Visuals::light(),
398            MacAppearance::Anthracite | MacAppearance::DarkAqua => egui::Visuals::dark(),
399        };
400
401        let border_w = self.button().regular.border_width.points();
402        let label_color = self.label();
403        visuals.override_text_color = Some(label_color);
404        visuals.widgets.noninteractive.fg_stroke.color = label_color;
405        visuals.widgets.noninteractive.bg_stroke = egui::Stroke::new(border_w, self.separator());
406        visuals.widgets.inactive.fg_stroke.color = label_color;
407        visuals.widgets.inactive.bg_stroke = egui::Stroke::new(border_w, self.separator());
408        visuals.panel_fill = self.window_background();
409        visuals.window_fill = self.window_background();
410        visuals.window_stroke = egui::Stroke::new(border_w, self.separator());
411
412        let small_pt = self.text_field().small.font_size.points();
413        let body_pt = self.outline_view().font_size.points();
414        let heading_pt = self.text_field().regular.font_size.points();
415
416        ctx.all_styles_mut(|style| {
417            use egui::{FontFamily, FontId, TextStyle};
418            style.text_styles = [
419                (
420                    TextStyle::Small,
421                    FontId::new(small_pt, FontFamily::Proportional),
422                ),
423                (
424                    TextStyle::Body,
425                    FontId::new(body_pt, FontFamily::Proportional),
426                ),
427                (
428                    TextStyle::Button,
429                    FontId::new(body_pt, FontFamily::Proportional),
430                ),
431                (
432                    TextStyle::Heading,
433                    FontId::new(heading_pt, FontFamily::Proportional),
434                ),
435                (
436                    TextStyle::Monospace,
437                    FontId::new(body_pt, FontFamily::Monospace),
438                ),
439            ]
440            .into();
441        });
442
443        ctx.set_visuals_of(egui_theme, visuals.clone());
444        ctx.set_visuals(visuals);
445    }
446}