1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum MacAppearance {
28 #[default]
30 Aqua,
31 Anthracite,
33 DarkAqua,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39pub enum WindowControlsStyle {
40 #[default]
42 AppleTrafficLights,
43 WindowsCaptionButtons,
45 NativeHost,
47}
48
49#[derive(Debug, Clone)]
65pub struct MacTheme {
66 pub appearance: MacAppearance,
68 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 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 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 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 pub fn resolve_or_default<'a>(theme: Option<&'a Self>) -> std::borrow::Cow<'a, Self> {
109 match theme {
110 Some(t) => std::borrow::Cow::Borrowed(t),
111 None => std::borrow::Cow::Owned(Self::default()),
112 }
113 }
114
115 pub fn with_window_controls_style(mut self, style: WindowControlsStyle) -> Self {
117 self.window_controls_style = style;
118 self
119 }
120
121 pub fn window_controls_style(&self) -> WindowControlsStyle {
123 self.window_controls_style
124 }
125
126 pub fn with_accent(mut self, accent: Color32) -> Self {
128 self.custom_accent = Some(accent);
129 self
130 }
131
132 pub fn with_tokens(appearance: MacAppearance, tokens: MacDesignTokens) -> Self {
134 Self {
135 appearance,
136 window_controls_style: WindowControlsStyle::AppleTrafficLights,
137 tokens: Arc::new(tokens),
138 custom_accent: None,
139 }
140 }
141
142 pub fn with_arc_tokens(appearance: MacAppearance, tokens: Arc<MacDesignTokens>) -> Self {
144 Self {
145 appearance,
146 window_controls_style: WindowControlsStyle::AppleTrafficLights,
147 tokens,
148 custom_accent: None,
149 }
150 }
151
152 pub fn tokens(&self) -> &MacDesignTokens {
154 &self.tokens
155 }
156
157 pub fn colors(&self) -> &MacAppearanceColors {
159 match self.appearance {
160 MacAppearance::Aqua => &self.tokens.colors().aqua,
161 MacAppearance::Anthracite => &self.tokens.colors().anthracite,
162 MacAppearance::DarkAqua => &self.tokens.colors().dark_aqua,
163 }
164 }
165
166 pub fn window_background(&self) -> Color32 {
168 self.colors().window_background.to_color32()
169 }
170
171 pub fn control_background(&self) -> Color32 {
173 self.colors().control_background.to_color32()
174 }
175
176 pub fn control(&self) -> Color32 {
178 self.colors().control.to_color32()
179 }
180
181 pub fn control_accent(&self) -> Color32 {
183 self.custom_accent
184 .unwrap_or_else(|| self.colors().control_accent.to_color32())
185 }
186
187 pub fn selected_control(&self) -> Color32 {
189 self.custom_accent
190 .unwrap_or_else(|| self.colors().selected_control.to_color32())
191 }
192
193 pub fn selected_control_inactive(&self) -> Color32 {
195 self.colors().selected_control_inactive.to_color32()
196 }
197
198 pub fn separator(&self) -> Color32 {
200 self.colors().separator.to_color32()
201 }
202
203 pub fn placeholder_text(&self) -> Color32 {
205 self.colors().placeholder_text.to_color32()
206 }
207
208 pub fn keyboard_focus_indicator(&self) -> Color32 {
210 self.colors().keyboard_focus_indicator.to_color32()
211 }
212
213 pub fn label(&self) -> Color32 {
215 self.colors().label.to_color32()
216 }
217
218 pub fn secondary_label(&self) -> Color32 {
220 self.colors().secondary_label.to_color32()
221 }
222
223 pub fn system_green(&self) -> Color32 {
225 self.colors().system_green.to_color32()
226 }
227
228 pub fn system_yellow(&self) -> Color32 {
230 self.colors().system_yellow.to_color32()
231 }
232
233 pub fn system_orange(&self) -> Color32 {
235 self.colors().system_orange.to_color32()
236 }
237
238 pub fn system_red(&self) -> Color32 {
240 self.colors().system_red.to_color32()
241 }
242
243 pub fn alternating_row_even(&self) -> Color32 {
245 self.colors().alternating_row_even.to_color32()
246 }
247
248 pub fn alternating_row_odd(&self) -> Color32 {
250 self.colors().alternating_row_odd.to_color32()
251 }
252
253 pub fn alternating_content_background(&self, row_index: usize) -> Color32 {
255 if row_index.is_multiple_of(2) {
256 self.alternating_row_even()
257 } else {
258 self.alternating_row_odd()
259 }
260 }
261
262 pub fn scrubber_selection_border(&self) -> Color32 {
264 self.colors().scrubber_selection_border.to_color32()
265 }
266
267 pub fn scrubber_outside_dim(&self) -> Color32 {
269 self.colors().scrubber_outside_dim.to_color32()
270 }
271
272 pub fn scrubber_waveform(&self) -> Color32 {
274 self.colors().scrubber_waveform.to_color32()
275 }
276
277 pub fn scrubber_playhead(&self) -> Color32 {
279 self.colors().scrubber_playhead.to_color32()
280 }
281
282 pub fn on_accent_text(&self) -> Color32 {
284 self.colors().on_accent_text.to_color32()
285 }
286
287 pub fn knob_fill(&self) -> Color32 {
289 self.colors().knob_fill.to_color32()
290 }
291
292 pub fn drop_shadow(&self) -> Color32 {
294 self.colors().drop_shadow.to_color32()
295 }
296
297 pub fn backdrop_dim(&self) -> Color32 {
299 self.colors().backdrop_dim.to_color32()
300 }
301
302 pub fn hud_background(&self) -> Color32 {
304 self.colors().hud_background.to_color32()
305 }
306
307 pub fn hud_border(&self) -> Color32 {
309 self.colors().hud_border.to_color32()
310 }
311
312 pub fn hud_text(&self) -> Color32 {
314 self.colors().hud_text.to_color32()
315 }
316
317 pub fn hud_secondary_text(&self) -> Color32 {
319 self.colors().hud_secondary_text.to_color32()
320 }
321
322 pub fn traffic_light_close(&self) -> (Color32, Color32) {
324 (
325 self.colors().traffic_light_close.to_color32(),
326 self.colors().traffic_light_close_border.to_color32(),
327 )
328 }
329
330 pub fn traffic_light_minimize(&self) -> (Color32, Color32) {
332 (
333 self.colors().traffic_light_minimize.to_color32(),
334 self.colors().traffic_light_minimize_border.to_color32(),
335 )
336 }
337
338 pub fn traffic_light_zoom(&self) -> (Color32, Color32) {
340 (
341 self.colors().traffic_light_zoom.to_color32(),
342 self.colors().traffic_light_zoom_border.to_color32(),
343 )
344 }
345
346 pub fn traffic_light_glyph(&self) -> Color32 {
348 self.colors().traffic_light_glyph.to_color32()
349 }
350
351 pub fn windows_caption_close_hover_bg(&self) -> Color32 {
353 self.colors().caption_close_hover.to_color32()
354 }
355
356 pub fn windows_caption_close_pressed_bg(&self) -> Color32 {
358 self.colors().caption_close_pressed.to_color32()
359 }
360
361 pub fn windows_caption_hover_bg(&self) -> Color32 {
363 self.colors().caption_hover.to_color32()
364 }
365
366 pub fn windows_caption_pressed_bg(&self) -> Color32 {
368 self.colors().caption_pressed.to_color32()
369 }
370
371 pub fn scrollbar_thumb_color(&self, active: bool, alpha_factor: f32) -> Color32 {
373 let base = if active {
374 self.colors().scrollbar_thumb_active.to_color32()
375 } else {
376 self.colors().scrollbar_thumb.to_color32()
377 };
378 base.gamma_multiply(alpha_factor.clamp(0.0, 1.0))
379 }
380
381 pub fn apply_to_egui(&self, ctx: &egui::Context) {
386 let unchanged = ctx.data(|d| {
387 d.get_temp::<MacTheme>(Id::NULL).is_some_and(|prev| {
388 prev.appearance == self.appearance
389 && prev.window_controls_style == self.window_controls_style
390 && prev.custom_accent == self.custom_accent
391 && Arc::ptr_eq(&prev.tokens, &self.tokens)
392 })
393 });
394 if unchanged {
395 return;
396 }
397 ctx.data_mut(|d| d.insert_temp(Id::NULL, self.clone()));
398
399 let egui_theme = match self.appearance {
400 MacAppearance::Aqua => egui::Theme::Light,
401 MacAppearance::Anthracite | MacAppearance::DarkAqua => egui::Theme::Dark,
402 };
403 ctx.set_theme(egui_theme);
404
405 let mut visuals = match self.appearance {
406 MacAppearance::Aqua => egui::Visuals::light(),
407 MacAppearance::Anthracite | MacAppearance::DarkAqua => egui::Visuals::dark(),
408 };
409
410 let border_w = self.button().regular.border_width.points();
411 let label_color = self.label();
412 visuals.override_text_color = Some(label_color);
413 visuals.widgets.noninteractive.fg_stroke.color = label_color;
414 visuals.widgets.noninteractive.bg_stroke = egui::Stroke::new(border_w, self.separator());
415 visuals.widgets.inactive.fg_stroke.color = label_color;
416 visuals.widgets.inactive.bg_stroke = egui::Stroke::new(border_w, self.separator());
417 visuals.panel_fill = self.window_background();
418 visuals.window_fill = self.window_background();
419 visuals.window_stroke = egui::Stroke::new(border_w, self.separator());
420
421 let small_pt = self.text_field().small.font_size.points();
422 let body_pt = self.outline_view().font_size.points();
423 let heading_pt = self.text_field().regular.font_size.points();
424
425 ctx.all_styles_mut(|style| {
426 use egui::{FontFamily, FontId, TextStyle};
427 style.text_styles = [
428 (
429 TextStyle::Small,
430 FontId::new(small_pt, FontFamily::Proportional),
431 ),
432 (
433 TextStyle::Body,
434 FontId::new(body_pt, FontFamily::Proportional),
435 ),
436 (
437 TextStyle::Button,
438 FontId::new(body_pt, FontFamily::Proportional),
439 ),
440 (
441 TextStyle::Heading,
442 FontId::new(heading_pt, FontFamily::Proportional),
443 ),
444 (
445 TextStyle::Monospace,
446 FontId::new(body_pt, FontFamily::Monospace),
447 ),
448 ]
449 .into();
450 });
451
452 ctx.set_visuals_of(egui_theme, visuals.clone());
453 ctx.set_visuals(visuals);
454 }
455}