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    /// 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))
93            .unwrap_or_default()
94    }
95
96    /// Sets the window controls presentation style (Strategy 2).
97    pub fn with_window_controls_style(mut self, style: WindowControlsStyle) -> Self {
98        self.window_controls_style = style;
99        self
100    }
101
102    /// Returns the active window controls presentation style.
103    pub fn window_controls_style(&self) -> WindowControlsStyle {
104        self.window_controls_style
105    }
106
107    /// Sets a dynamic system accent color override (matching macOS System Settings Appearance Accent).
108    pub fn with_accent(mut self, accent: Color32) -> Self {
109        self.custom_accent = Some(accent);
110        self
111    }
112
113    /// Creates a `MacTheme` with custom loaded tokens.
114    pub fn with_tokens(appearance: MacAppearance, tokens: MacDesignTokens) -> Self {
115        Self {
116            appearance,
117            window_controls_style: WindowControlsStyle::AppleTrafficLights,
118            tokens: Arc::new(tokens),
119            custom_accent: None,
120        }
121    }
122
123    /// Creates a `MacTheme` with custom loaded tokens wrapped in an `Arc`.
124    pub fn with_arc_tokens(appearance: MacAppearance, tokens: Arc<MacDesignTokens>) -> Self {
125        Self {
126            appearance,
127            window_controls_style: WindowControlsStyle::AppleTrafficLights,
128            tokens,
129            custom_accent: None,
130        }
131    }
132
133    /// Returns the underlying design token catalog.
134    pub fn tokens(&self) -> &MacDesignTokens {
135        &self.tokens
136    }
137
138    /// Active appearance color palette.
139    pub fn colors(&self) -> &MacAppearanceColors {
140        match self.appearance {
141            MacAppearance::Aqua => &self.tokens.colors().aqua,
142            MacAppearance::DarkAqua => &self.tokens.colors().dark_aqua,
143        }
144    }
145
146    /// Resolves `windowBackgroundColor` (NSColor) for the active appearance.
147    pub fn window_background(&self) -> Color32 {
148        self.colors().window_background.to_color32()
149    }
150
151    /// Resolves `controlBackgroundColor` (NSColor) for the active appearance.
152    pub fn control_background(&self) -> Color32 {
153        self.colors().control_background.to_color32()
154    }
155
156    /// Resolves `controlColor` (NSColor) for the active appearance.
157    pub fn control(&self) -> Color32 {
158        self.colors().control.to_color32()
159    }
160
161    /// Resolves `controlAccentColor` (NSColor) or custom accent override.
162    pub fn control_accent(&self) -> Color32 {
163        self.custom_accent
164            .unwrap_or_else(|| self.colors().control_accent.to_color32())
165    }
166
167    /// Resolves `selectedControlColor` (NSColor) or custom accent override.
168    pub fn selected_control(&self) -> Color32 {
169        self.custom_accent
170            .unwrap_or_else(|| self.colors().selected_control.to_color32())
171    }
172
173    /// Resolves `selectedControlColor` (NSColor) when the window or control is inactive/unfocused.
174    pub fn selected_control_inactive(&self) -> Color32 {
175        self.colors().selected_control_inactive.to_color32()
176    }
177
178    /// Resolves `separatorColor` (NSColor) for hairlines and borders.
179    pub fn separator(&self) -> Color32 {
180        self.colors().separator.to_color32()
181    }
182
183    /// Resolves `placeholderTextColor` (NSColor) for empty text fields and search inputs.
184    pub fn placeholder_text(&self) -> Color32 {
185        self.colors().placeholder_text.to_color32()
186    }
187
188    /// Resolves `keyboardFocusIndicatorColor` (NSColor) for focus rings.
189    pub fn keyboard_focus_indicator(&self) -> Color32 {
190        self.colors().keyboard_focus_indicator.to_color32()
191    }
192
193    /// Resolves primary `labelColor` (NSColor).
194    pub fn label(&self) -> Color32 {
195        self.colors().label.to_color32()
196    }
197
198    /// Resolves `secondaryLabelColor` (NSColor) for captions, subtitles, and section headers.
199    pub fn secondary_label(&self) -> Color32 {
200        self.colors().secondary_label.to_color32()
201    }
202
203    /// Resolves Apple system green semantic color (`systemGreenColor`).
204    pub fn system_green(&self) -> Color32 {
205        self.colors().system_green.to_color32()
206    }
207
208    /// Resolves Apple system yellow/amber semantic color (`systemYellowColor`).
209    pub fn system_yellow(&self) -> Color32 {
210        self.colors().system_yellow.to_color32()
211    }
212
213    /// Resolves Apple system orange semantic color (`systemOrangeColor`).
214    pub fn system_orange(&self) -> Color32 {
215        self.colors().system_orange.to_color32()
216    }
217
218    /// Resolves Apple system red semantic color (`systemRedColor`).
219    pub fn system_red(&self) -> Color32 {
220        self.colors().system_red.to_color32()
221    }
222
223    /// Resolves even table row background color (`alternatingContentBackgroundColors[0]`).
224    pub fn alternating_row_even(&self) -> Color32 {
225        self.colors().alternating_row_even.to_color32()
226    }
227
228    /// Resolves odd table row background color (`alternatingContentBackgroundColors[1]`).
229    pub fn alternating_row_odd(&self) -> Color32 {
230        self.colors().alternating_row_odd.to_color32()
231    }
232
233    /// Resolves alternating content background color for the given row index (`row_index % 2`).
234    pub fn alternating_content_background(&self, row_index: usize) -> Color32 {
235        if row_index.is_multiple_of(2) {
236            self.alternating_row_even()
237        } else {
238            self.alternating_row_odd()
239        }
240    }
241
242    /// Resolves QuickTime / AVKit trim selection border and handle color.
243    pub fn scrubber_selection_border(&self) -> Color32 {
244        self.colors().scrubber_selection_border.to_color32()
245    }
246
247    /// Resolves dimming overlay color for timeline regions outside active trim bounds.
248    pub fn scrubber_outside_dim(&self) -> Color32 {
249        self.colors().scrubber_outside_dim.to_color32()
250    }
251
252    /// Resolves waveform amplitude bar color inside trim scrubber track.
253    pub fn scrubber_waveform(&self) -> Color32 {
254        self.colors().scrubber_waveform.to_color32()
255    }
256
257    /// Resolves playhead cursor line and timecode badge accent color.
258    pub fn scrubber_playhead(&self) -> Color32 {
259        self.colors().scrubber_playhead.to_color32()
260    }
261
262    /// Resolves high-contrast white text color used on accent fills.
263    pub fn on_accent_text(&self) -> Color32 {
264        self.colors().on_accent_text.to_color32()
265    }
266
267    /// Resolves white thumb knob fill color for switches and sliders.
268    pub fn knob_fill(&self) -> Color32 {
269        self.colors().knob_fill.to_color32()
270    }
271
272    /// Resolves subtle ambient drop shadow color for controls and floating surfaces.
273    pub fn drop_shadow(&self) -> Color32 {
274        self.colors().drop_shadow.to_color32()
275    }
276
277    /// Resolves modal backdrop dimming overlay color.
278    pub fn backdrop_dim(&self) -> Color32 {
279        self.colors().backdrop_dim.to_color32()
280    }
281
282    /// Resolves translucent dark HUD palette background color (`SPEC-006 ยง2.2`).
283    pub fn hud_background(&self) -> Color32 {
284        self.colors().hud_background.to_color32()
285    }
286
287    /// Resolves dark HUD palette hairline border color.
288    pub fn hud_border(&self) -> Color32 {
289        self.colors().hud_border.to_color32()
290    }
291
292    /// Resolves dark HUD palette primary text color.
293    pub fn hud_text(&self) -> Color32 {
294        self.colors().hud_text.to_color32()
295    }
296
297    /// Resolves dark HUD palette secondary label color.
298    pub fn hud_secondary_text(&self) -> Color32 {
299        self.colors().hud_secondary_text.to_color32()
300    }
301
302    /// Resolves macOS window close traffic light fill and border colors `(fill, border)`.
303    pub fn traffic_light_close(&self) -> (Color32, Color32) {
304        (
305            self.colors().traffic_light_close.to_color32(),
306            self.colors().traffic_light_close_border.to_color32(),
307        )
308    }
309
310    /// Resolves macOS window minimize traffic light fill and border colors `(fill, border)`.
311    pub fn traffic_light_minimize(&self) -> (Color32, Color32) {
312        (
313            self.colors().traffic_light_minimize.to_color32(),
314            self.colors().traffic_light_minimize_border.to_color32(),
315        )
316    }
317
318    /// Resolves macOS window zoom/fullscreen traffic light fill and border colors `(fill, border)`.
319    pub fn traffic_light_zoom(&self) -> (Color32, Color32) {
320        (
321            self.colors().traffic_light_zoom.to_color32(),
322            self.colors().traffic_light_zoom_border.to_color32(),
323        )
324    }
325
326    /// Interior glyph color for hovered macOS traffic light buttons.
327    pub fn traffic_light_glyph(&self) -> Color32 {
328        self.colors().traffic_light_glyph.to_color32()
329    }
330
331    /// Background color for Windows 11 close caption button on hover (`#E81123`).
332    pub fn windows_caption_close_hover_bg(&self) -> Color32 {
333        self.colors().caption_close_hover.to_color32()
334    }
335
336    /// Background color for Windows 11 close caption button when pressed (`#C42B1C`).
337    pub fn windows_caption_close_pressed_bg(&self) -> Color32 {
338        self.colors().caption_close_pressed.to_color32()
339    }
340
341    /// Background color for Windows 11 minimize/maximize caption buttons on hover.
342    pub fn windows_caption_hover_bg(&self) -> Color32 {
343        self.colors().caption_hover.to_color32()
344    }
345
346    /// Background color for Windows 11 minimize/maximize caption buttons when pressed.
347    pub fn windows_caption_pressed_bg(&self) -> Color32 {
348        self.colors().caption_pressed.to_color32()
349    }
350
351    /// Overlay scrollbar thumb color scaled by fade `alpha_factor` in `[0.0, 1.0]`.
352    pub fn scrollbar_thumb_color(&self, active: bool, alpha_factor: f32) -> Color32 {
353        let base = if active {
354            self.colors().scrollbar_thumb_active.to_color32()
355        } else {
356            self.colors().scrollbar_thumb.to_color32()
357        };
358        base.gamma_multiply(alpha_factor.clamp(0.0, 1.0))
359    }
360
361    /// Synchronizes `egui::Context` theme, visuals, and stored `MacTheme` with the macOS appearance.
362    pub fn apply_to_egui(&self, ctx: &egui::Context) {
363        ctx.data_mut(|d| d.insert_temp(Id::NULL, self.clone()));
364
365        let egui_theme = match self.appearance {
366            MacAppearance::Aqua => egui::Theme::Light,
367            MacAppearance::DarkAqua => egui::Theme::Dark,
368        };
369        ctx.set_theme(egui_theme);
370
371        let mut visuals = match self.appearance {
372            MacAppearance::Aqua => egui::Visuals::light(),
373            MacAppearance::DarkAqua => egui::Visuals::dark(),
374        };
375
376        let border_w = self.button().regular.border_width.points();
377        let label_color = self.label();
378        visuals.override_text_color = Some(label_color);
379        visuals.widgets.noninteractive.fg_stroke.color = label_color;
380        visuals.widgets.noninteractive.bg_stroke = egui::Stroke::new(border_w, self.separator());
381        visuals.widgets.inactive.fg_stroke.color = label_color;
382        visuals.widgets.inactive.bg_stroke = egui::Stroke::new(border_w, self.separator());
383        visuals.panel_fill = self.window_background();
384        visuals.window_fill = self.window_background();
385        visuals.window_stroke = egui::Stroke::new(border_w, self.separator());
386
387        let small_pt = self.text_field().small.font_size.points();
388        let body_pt = self.outline_view().font_size.points();
389        let heading_pt = self.text_field().regular.font_size.points();
390
391        ctx.all_styles_mut(|style| {
392            use egui::{FontFamily, FontId, TextStyle};
393            style.text_styles = [
394                (
395                    TextStyle::Small,
396                    FontId::new(small_pt, FontFamily::Proportional),
397                ),
398                (
399                    TextStyle::Body,
400                    FontId::new(body_pt, FontFamily::Proportional),
401                ),
402                (
403                    TextStyle::Button,
404                    FontId::new(body_pt, FontFamily::Proportional),
405                ),
406                (
407                    TextStyle::Heading,
408                    FontId::new(heading_pt, FontFamily::Proportional),
409                ),
410                (
411                    TextStyle::Monospace,
412                    FontId::new(body_pt, FontFamily::Monospace),
413                ),
414            ]
415            .into();
416        });
417
418        ctx.set_visuals_of(egui_theme, visuals.clone());
419        ctx.set_visuals(visuals);
420    }
421}