1use crate::tokens::{MacAppearanceColors, MacDesignTokens};
4use egui::{Color32, Id};
5use std::sync::{Arc, OnceLock};
6
7pub mod accessors;
8pub mod contrast;
9pub use contrast::{
10 alpha_composite, contrast_ratio, ensure_wcag_contrast, plus_darker, plus_lighter,
11 relative_luminance, srgb_channel_to_linear, MacBackgroundStyle,
12};
13
14static DEFAULT_TOKENS: OnceLock<Arc<MacDesignTokens>> = OnceLock::new();
15
16fn default_cached_tokens() -> Arc<MacDesignTokens> {
17 DEFAULT_TOKENS
18 .get_or_init(|| Arc::new(MacDesignTokens::load_default()))
19 .clone()
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
24pub enum MacAppearance {
25 #[default]
27 Aqua,
28 Anthracite,
30 DarkAqua,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
36pub enum WindowControlsStyle {
37 #[default]
39 AppleTrafficLights,
40 WindowsCaptionButtons,
42 NativeHost,
44}
45
46#[derive(Debug, Clone)]
62pub struct MacTheme {
63 pub appearance: MacAppearance,
65 pub window_controls_style: WindowControlsStyle,
67 tokens: Arc<MacDesignTokens>,
68 custom_accent: Option<Color32>,
69}
70
71impl Default for MacTheme {
72 fn default() -> Self {
73 Self::new(MacAppearance::Aqua)
74 }
75}
76
77impl MacTheme {
78 pub fn new(appearance: MacAppearance) -> Self {
80 Self {
81 appearance,
82 window_controls_style: WindowControlsStyle::AppleTrafficLights,
83 tokens: default_cached_tokens(),
84 custom_accent: None,
85 }
86 }
87
88 pub fn from_ctx(ctx: &egui::Context) -> Self {
91 ctx.data(|d| d.get_temp::<MacTheme>(Id::NULL)).unwrap_or_default()
92 }
93
94 pub fn resolve<'a>(theme: Option<&'a Self>, ctx: &egui::Context) -> std::borrow::Cow<'a, Self> {
96 match theme {
97 Some(t) => std::borrow::Cow::Borrowed(t),
98 None => std::borrow::Cow::Owned(Self::from_ctx(ctx)),
99 }
100 }
101
102 pub fn resolve_or_default<'a>(theme: Option<&'a Self>) -> std::borrow::Cow<'a, Self> {
104 match theme {
105 Some(t) => std::borrow::Cow::Borrowed(t),
106 None => std::borrow::Cow::Owned(Self::default()),
107 }
108 }
109
110 pub fn with_window_controls_style(mut self, style: WindowControlsStyle) -> Self {
112 self.window_controls_style = style;
113 self
114 }
115
116 pub fn window_controls_style(&self) -> WindowControlsStyle { self.window_controls_style }
118
119 pub fn with_accent(mut self, accent: Color32) -> Self {
121 self.custom_accent = Some(accent);
122 self
123 }
124
125 pub fn with_tokens(appearance: MacAppearance, tokens: MacDesignTokens) -> Self {
127 Self { appearance, window_controls_style: WindowControlsStyle::AppleTrafficLights, tokens: Arc::new(tokens), custom_accent: None }
128 }
129
130 pub fn with_arc_tokens(appearance: MacAppearance, tokens: Arc<MacDesignTokens>) -> Self {
132 Self { appearance, window_controls_style: WindowControlsStyle::AppleTrafficLights, tokens, custom_accent: None }
133 }
134
135 pub fn tokens(&self) -> &MacDesignTokens { &self.tokens }
137
138 pub fn colors(&self) -> &MacAppearanceColors {
140 match self.appearance {
141 MacAppearance::Aqua => &self.tokens.colors().aqua,
142 MacAppearance::Anthracite => &self.tokens.colors().anthracite,
143 MacAppearance::DarkAqua => &self.tokens.colors().dark_aqua,
144 }
145 }
146
147 pub fn window_background(&self) -> Color32 { self.colors().window_background.to_color32() }
149 pub fn control_background(&self) -> Color32 { self.colors().control_background.to_color32() }
151 pub fn control(&self) -> Color32 { self.colors().control.to_color32() }
153 pub fn control_accent(&self) -> Color32 {
155 self.custom_accent.unwrap_or_else(|| self.colors().control_accent.to_color32())
156 }
157 pub fn selected_control(&self) -> Color32 {
159 self.custom_accent.unwrap_or_else(|| self.colors().selected_control.to_color32())
160 }
161 pub fn selected_control_inactive(&self) -> Color32 { self.colors().selected_control_inactive.to_color32() }
163 pub fn separator(&self) -> Color32 { self.colors().separator.to_color32() }
165 pub fn placeholder_text(&self) -> Color32 { self.colors().placeholder_text.to_color32() }
167 pub fn keyboard_focus_indicator(&self) -> Color32 { self.colors().keyboard_focus_indicator.to_color32() }
169 pub fn label(&self) -> Color32 { self.colors().label.to_color32() }
171 pub fn secondary_label(&self) -> Color32 { self.colors().secondary_label.to_color32() }
173 pub fn tertiary_label(&self) -> Color32 { self.colors().tertiary_label.to_color32() }
175 pub fn box_background(&self) -> Color32 { self.colors().box_background.to_color32() }
177 pub fn box_border(&self) -> Color32 { self.colors().box_border.to_color32() }
179 pub fn system_green(&self) -> Color32 { self.colors().system_green.to_color32() }
181 pub fn system_yellow(&self) -> Color32 { self.colors().system_yellow.to_color32() }
183 pub fn system_orange(&self) -> Color32 { self.colors().system_orange.to_color32() }
185 pub fn system_red(&self) -> Color32 { self.colors().system_red.to_color32() }
187 pub fn alternating_row_even(&self) -> Color32 { self.colors().alternating_row_even.to_color32() }
189 pub fn alternating_row_odd(&self) -> Color32 { self.colors().alternating_row_odd.to_color32() }
191 pub fn alternating_content_background(&self, row_index: usize) -> Color32 {
193 if row_index.is_multiple_of(2) { self.alternating_row_even() } else { self.alternating_row_odd() }
194 }
195 pub fn scrubber_selection_border(&self) -> Color32 { self.colors().scrubber_selection_border.to_color32() }
197 pub fn scrubber_outside_dim(&self) -> Color32 { self.colors().scrubber_outside_dim.to_color32() }
199 pub fn scrubber_waveform(&self) -> Color32 { self.colors().scrubber_waveform.to_color32() }
201 pub fn scrubber_playhead(&self) -> Color32 { self.colors().scrubber_playhead.to_color32() }
203 pub fn on_accent_text(&self) -> Color32 { self.colors().on_accent_text.to_color32() }
205 pub fn knob_fill(&self) -> Color32 { self.colors().knob_fill.to_color32() }
207 pub fn drop_shadow(&self) -> Color32 { self.colors().drop_shadow.to_color32() }
209 pub fn backdrop_dim(&self) -> Color32 { self.colors().backdrop_dim.to_color32() }
211 pub fn hud_background(&self) -> Color32 { self.colors().hud_background.to_color32() }
213 pub fn hud_border(&self) -> Color32 { self.colors().hud_border.to_color32() }
215 pub fn hud_text(&self) -> Color32 { self.colors().hud_text.to_color32() }
217 pub fn hud_secondary_text(&self) -> Color32 { self.colors().hud_secondary_text.to_color32() }
219 pub fn traffic_light_close(&self) -> (Color32, Color32) {
221 (self.colors().traffic_light_close.to_color32(), self.colors().traffic_light_close_border.to_color32())
222 }
223 pub fn traffic_light_minimize(&self) -> (Color32, Color32) {
225 (self.colors().traffic_light_minimize.to_color32(), self.colors().traffic_light_minimize_border.to_color32())
226 }
227 pub fn traffic_light_zoom(&self) -> (Color32, Color32) {
229 (self.colors().traffic_light_zoom.to_color32(), self.colors().traffic_light_zoom_border.to_color32())
230 }
231 pub fn traffic_light_glyph(&self) -> Color32 { self.colors().traffic_light_glyph.to_color32() }
233 pub fn windows_caption_close_hover_bg(&self) -> Color32 { self.colors().caption_close_hover.to_color32() }
235 pub fn windows_caption_close_pressed_bg(&self) -> Color32 { self.colors().caption_close_pressed.to_color32() }
237 pub fn windows_caption_hover_bg(&self) -> Color32 { self.colors().caption_hover.to_color32() }
239 pub fn windows_caption_pressed_bg(&self) -> Color32 { self.colors().caption_pressed.to_color32() }
241 pub fn scrollbar_thumb_color(&self, active: bool, alpha_factor: f32) -> Color32 {
243 let base = if active { self.colors().scrollbar_thumb_active.to_color32() } else { self.colors().scrollbar_thumb.to_color32() };
244 base.gamma_multiply(alpha_factor.clamp(0.0, 1.0))
245 }
246
247 pub fn apply_to_egui(&self, ctx: &egui::Context) {
249 let unchanged = ctx.data(|d| {
250 d.get_temp::<MacTheme>(Id::NULL).is_some_and(|prev| {
251 prev.appearance == self.appearance
252 && prev.window_controls_style == self.window_controls_style
253 && prev.custom_accent == self.custom_accent
254 && Arc::ptr_eq(&prev.tokens, &self.tokens)
255 })
256 });
257 if unchanged { return; }
258 ctx.data_mut(|d| d.insert_temp(Id::NULL, self.clone()));
259 let egui_theme = match self.appearance {
260 MacAppearance::Aqua => egui::Theme::Light,
261 MacAppearance::Anthracite | MacAppearance::DarkAqua => egui::Theme::Dark,
262 };
263 ctx.set_theme(egui_theme);
264 let mut visuals = match self.appearance {
265 MacAppearance::Aqua => egui::Visuals::light(),
266 MacAppearance::Anthracite | MacAppearance::DarkAqua => egui::Visuals::dark(),
267 };
268 let border_w = self.button().regular.border_width.points();
269 let label_color = self.label();
270 visuals.override_text_color = Some(label_color);
271 visuals.widgets.noninteractive.fg_stroke.color = label_color;
272 visuals.widgets.noninteractive.bg_stroke = egui::Stroke::new(border_w, self.separator());
273 visuals.widgets.inactive.fg_stroke.color = label_color;
274 visuals.widgets.inactive.bg_stroke = egui::Stroke::new(border_w, self.separator());
275 visuals.panel_fill = self.window_background();
276 visuals.window_fill = self.window_background();
277 visuals.window_stroke = egui::Stroke::new(border_w, self.separator());
278 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());
279 ctx.all_styles_mut(|style| {
280 use egui::{FontFamily, FontId, TextStyle};
281 style.text_styles = [
282 (TextStyle::Small, FontId::new(small_pt, FontFamily::Proportional)),
283 (TextStyle::Body, FontId::new(body_pt, FontFamily::Proportional)),
284 (TextStyle::Button, FontId::new(body_pt, FontFamily::Proportional)),
285 (TextStyle::Heading, FontId::new(heading_pt, FontFamily::Proportional)),
286 (TextStyle::Monospace, FontId::new(body_pt, FontFamily::Monospace)),
287 ].into();
288 });
289 ctx.set_visuals_of(egui_theme, visuals.clone());
290 ctx.set_visuals(visuals);
291 }
292}