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::{
8    ButtonMetrics, ColorWellMetrics, FocusRingMetrics, LayoutMetrics, MacAppearanceColors,
9    MacControlMetrics, MacDesignTokens, MacMotionTokens, MacTypographyTokens, OutlineViewMetrics,
10    PopUpButtonMetrics, PopoverMetrics, ScrollViewMetrics, SearchFieldMetrics, SegmentedMetrics,
11    SliderMetrics, SplitViewMetrics, StackViewMetrics, StepperMetrics, TextFieldMetrics,
12    ThumbnailCellMetrics, ToggleMetrics, ToolbarMetrics,
13};
14use egui::Color32;
15
16use std::sync::{Arc, OnceLock};
17
18pub mod contrast;
19pub use contrast::{
20    contrast_ratio, relative_luminance, srgb_channel_to_linear, MacBackgroundStyle,
21};
22
23static DEFAULT_TOKENS: OnceLock<Arc<MacDesignTokens>> = OnceLock::new();
24
25fn default_cached_tokens() -> Arc<MacDesignTokens> {
26    DEFAULT_TOKENS
27        .get_or_init(|| Arc::new(MacDesignTokens::load_default()))
28        .clone()
29}
30
31/// macOS system appearance mode.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33pub enum MacAppearance {
34    /// Authentic Apple macOS light appearance (`NSAppearanceNameAqua`).
35    #[default]
36    Aqua,
37    /// Authentic Apple macOS dark appearance (`NSAppearanceNameDarkAqua`).
38    DarkAqua,
39}
40
41/// Window caption controls presentation style (Strategy 2: Token-Driven / Configurable Presentation).
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
43pub enum WindowControlsStyle {
44    /// Authentic Apple macOS traffic lights on the leading (left) edge.
45    #[default]
46    AppleTrafficLights,
47    /// Authentic Microsoft Windows 11 caption buttons on the trailing (right) edge.
48    WindowsCaptionButtons,
49    /// Native host OS window manager decorations (decorations provided by OS).
50    NativeHost,
51}
52
53/// Dynamic theme resolving AppKit semantic styling from [`MacDesignTokens`].
54///
55/// # Example
56///
57/// ```rust
58/// use egui_macview::{MacAppearance, MacTheme, WindowControlsStyle};
59///
60/// // Create a dark aqua macOS theme
61/// let theme = MacTheme::new(MacAppearance::DarkAqua)
62///     .with_window_controls_style(WindowControlsStyle::AppleTrafficLights);
63///
64/// let bg = theme.window_background();
65/// let text_color = theme.label();
66/// let corner_radius = theme.button().regular.corner_radius.points();
67/// assert_eq!(corner_radius, 6.0);
68/// ```
69#[derive(Debug, Clone)]
70pub struct MacTheme {
71    /// Active macOS appearance mode ([`MacAppearance::Aqua`] or [`MacAppearance::DarkAqua`]).
72    pub appearance: MacAppearance,
73    /// Window caption controls presentation style.
74    pub window_controls_style: WindowControlsStyle,
75    tokens: Arc<MacDesignTokens>,
76    custom_accent: Option<Color32>,
77}
78
79impl Default for MacTheme {
80    fn default() -> Self {
81        Self::new(MacAppearance::Aqua)
82    }
83}
84
85impl MacTheme {
86    /// Creates a new `MacTheme` for the given appearance using the embedded default token catalog.
87    pub fn new(appearance: MacAppearance) -> Self {
88        Self {
89            appearance,
90            window_controls_style: WindowControlsStyle::AppleTrafficLights,
91            tokens: default_cached_tokens(),
92            custom_accent: None,
93        }
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    /// Synchronizes `egui::Context` theme and visuals with the macOS appearance.
204    pub fn apply_to_egui(&self, ctx: &egui::Context) {
205        let egui_theme = match self.appearance {
206            MacAppearance::Aqua => egui::Theme::Light,
207            MacAppearance::DarkAqua => egui::Theme::Dark,
208        };
209        ctx.set_theme(egui_theme);
210
211        let mut visuals = match self.appearance {
212            MacAppearance::Aqua => egui::Visuals::light(),
213            MacAppearance::DarkAqua => egui::Visuals::dark(),
214        };
215
216        let label_color = self.label();
217        visuals.override_text_color = Some(label_color);
218        visuals.widgets.noninteractive.fg_stroke.color = label_color;
219        visuals.widgets.inactive.fg_stroke.color = label_color;
220        visuals.panel_fill = self.window_background();
221        visuals.window_fill = self.window_background();
222
223        ctx.set_visuals_of(egui_theme, visuals.clone());
224        ctx.set_visuals(visuals);
225    }
226
227    /// Resolves top-level control metrics collection.
228    pub fn control_metrics(&self) -> &MacControlMetrics {
229        self.tokens.control_metrics()
230    }
231
232    /// Resolves focus ring geometry metrics.
233    pub fn focus_ring(&self) -> &FocusRingMetrics {
234        &self.control_metrics().focus_ring
235    }
236
237    /// Resolves text field geometry metrics.
238    pub fn text_field(&self) -> &TextFieldMetrics {
239        &self.control_metrics().text_field
240    }
241
242    /// Resolves push button geometry metrics.
243    pub fn button(&self) -> &ButtonMetrics {
244        &self.control_metrics().button
245    }
246
247    /// Resolves segmented control geometry metrics.
248    pub fn segmented(&self) -> &SegmentedMetrics {
249        &self.control_metrics().segmented
250    }
251
252    /// Resolves popover and HUD panel geometry metrics.
253    pub fn popover_metrics(&self) -> &PopoverMetrics {
254        self.tokens.popover_metrics()
255    }
256
257    /// Resolves window toolbar geometry metrics.
258    pub fn toolbar_metrics(&self) -> &ToolbarMetrics {
259        self.tokens.toolbar_metrics()
260    }
261
262    /// Resolves slider control geometry metrics.
263    pub fn slider(&self) -> &SliderMetrics {
264        self.tokens.slider_metrics()
265    }
266
267    /// Resolves thumbnail cell geometry metrics.
268    pub fn thumbnail(&self) -> &ThumbnailCellMetrics {
269        self.tokens.thumbnail_metrics()
270    }
271
272    /// Resolves split view divider and handle geometry metrics.
273    pub fn split_view(&self) -> &SplitViewMetrics {
274        self.tokens.split_view_metrics()
275    }
276
277    /// Resolves layout container geometry metrics.
278    pub fn layout_metrics(&self) -> &LayoutMetrics {
279        self.tokens.layout_metrics()
280    }
281
282    /// Resolves scroll view and overlay scrollbar geometry metrics.
283    pub fn scroll_view(&self) -> &ScrollViewMetrics {
284        self.tokens.scroll_view_metrics()
285    }
286
287    /// Resolves stack view linear layout geometry metrics.
288    pub fn stack_view(&self) -> &StackViewMetrics {
289        self.tokens.stack_view_metrics()
290    }
291
292    /// Resolves outline view hierarchical table geometry metrics.
293    pub fn outline_view(&self) -> &OutlineViewMetrics {
294        self.tokens.outline_view_metrics()
295    }
296
297    /// Resolves pop-up button geometry metrics.
298    pub fn popup_button(&self) -> &PopUpButtonMetrics {
299        self.tokens.popup_button_metrics()
300    }
301
302    /// Resolves search field geometry metrics.
303    pub fn search_field(&self) -> &SearchFieldMetrics {
304        self.tokens.search_field_metrics()
305    }
306
307    /// Resolves numeric stepper control geometry metrics.
308    pub fn stepper(&self) -> &StepperMetrics {
309        self.tokens.stepper_metrics()
310    }
311
312    /// Resolves color well swatch control geometry metrics.
313    pub fn color_well(&self) -> &ColorWellMetrics {
314        self.tokens.color_well_metrics()
315    }
316
317    /// Resolves toggle switch control geometry metrics.
318    pub fn toggle(&self) -> &ToggleMetrics {
319        self.tokens.toggle_metrics()
320    }
321
322    /// Resolves CoreAnimation motion preset tokens.
323    pub fn motion(&self) -> &MacMotionTokens {
324        self.tokens.motion()
325    }
326
327    /// Resolves typography and SF Pro tracking tokens.
328    pub fn typography(&self) -> &MacTypographyTokens {
329        self.tokens.typography()
330    }
331
332    /// Resolves Apple system green semantic color (`systemGreenColor`).
333    pub fn system_green(&self) -> Color32 {
334        self.colors().system_green.to_color32()
335    }
336
337    /// Resolves macOS window close traffic light fill and border colors `(fill, border)`.
338    pub fn traffic_light_close(&self) -> (Color32, Color32) {
339        (
340            self.colors().traffic_light_close.to_color32(),
341            self.colors().traffic_light_close_border.to_color32(),
342        )
343    }
344
345    /// Resolves macOS window minimize traffic light fill and border colors `(fill, border)`.
346    pub fn traffic_light_minimize(&self) -> (Color32, Color32) {
347        (
348            self.colors().traffic_light_minimize.to_color32(),
349            self.colors().traffic_light_minimize_border.to_color32(),
350        )
351    }
352
353    /// Resolves macOS window zoom/fullscreen traffic light fill and border colors `(fill, border)`.
354    pub fn traffic_light_zoom(&self) -> (Color32, Color32) {
355        (
356            self.colors().traffic_light_zoom.to_color32(),
357            self.colors().traffic_light_zoom_border.to_color32(),
358        )
359    }
360
361    /// Background color for Windows 11 close caption button on hover (Fluent crimson red `#E81123`).
362    pub fn windows_caption_close_hover_bg(&self) -> Color32 {
363        Color32::from_rgb(232, 17, 35)
364    }
365
366    /// Background color for Windows 11 close caption button when pressed (`#C42B1C`).
367    pub fn windows_caption_close_pressed_bg(&self) -> Color32 {
368        Color32::from_rgb(196, 43, 28)
369    }
370
371    /// Background color for Windows 11 minimize/maximize caption buttons on hover.
372    pub fn windows_caption_hover_bg(&self) -> Color32 {
373        match self.appearance {
374            MacAppearance::Aqua => Color32::from_black_alpha(15),
375            MacAppearance::DarkAqua => Color32::from_white_alpha(20),
376        }
377    }
378
379    /// Background color for Windows 11 minimize/maximize caption buttons when pressed.
380    pub fn windows_caption_pressed_bg(&self) -> Color32 {
381        match self.appearance {
382            MacAppearance::Aqua => Color32::from_black_alpha(25),
383            MacAppearance::DarkAqua => Color32::from_white_alpha(35),
384        }
385    }
386
387    /// Resolves the `MacBackgroundStyle` (Light or Dark) for any background color.
388    pub fn background_style(&self, bg: Color32) -> MacBackgroundStyle {
389        MacBackgroundStyle::from_color(bg)
390    }
391
392    /// Resolves the `MacBackgroundStyle` for the window background.
393    pub fn window_background_style(&self) -> MacBackgroundStyle {
394        MacBackgroundStyle::from_color(self.window_background())
395    }
396
397    /// Resolves the `MacBackgroundStyle` for the control background.
398    pub fn control_background_style(&self) -> MacBackgroundStyle {
399        MacBackgroundStyle::from_color(self.control_background())
400    }
401
402    /// Resolves the `MacBackgroundStyle` for the selected control background.
403    pub fn selected_control_background_style(&self) -> MacBackgroundStyle {
404        MacBackgroundStyle::from_color(self.selected_control())
405    }
406}