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 with_window_controls_style(mut self, style: WindowControlsStyle) -> Self {
108 self.window_controls_style = style;
109 self
110 }
111
112 pub fn window_controls_style(&self) -> WindowControlsStyle {
114 self.window_controls_style
115 }
116
117 pub fn with_accent(mut self, accent: Color32) -> Self {
119 self.custom_accent = Some(accent);
120 self
121 }
122
123 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 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 pub fn tokens(&self) -> &MacDesignTokens {
145 &self.tokens
146 }
147
148 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 pub fn window_background(&self) -> Color32 {
159 self.colors().window_background.to_color32()
160 }
161
162 pub fn control_background(&self) -> Color32 {
164 self.colors().control_background.to_color32()
165 }
166
167 pub fn control(&self) -> Color32 {
169 self.colors().control.to_color32()
170 }
171
172 pub fn control_accent(&self) -> Color32 {
174 self.custom_accent
175 .unwrap_or_else(|| self.colors().control_accent.to_color32())
176 }
177
178 pub fn selected_control(&self) -> Color32 {
180 self.custom_accent
181 .unwrap_or_else(|| self.colors().selected_control.to_color32())
182 }
183
184 pub fn selected_control_inactive(&self) -> Color32 {
186 self.colors().selected_control_inactive.to_color32()
187 }
188
189 pub fn separator(&self) -> Color32 {
191 self.colors().separator.to_color32()
192 }
193
194 pub fn placeholder_text(&self) -> Color32 {
196 self.colors().placeholder_text.to_color32()
197 }
198
199 pub fn keyboard_focus_indicator(&self) -> Color32 {
201 self.colors().keyboard_focus_indicator.to_color32()
202 }
203
204 pub fn label(&self) -> Color32 {
206 self.colors().label.to_color32()
207 }
208
209 pub fn secondary_label(&self) -> Color32 {
211 self.colors().secondary_label.to_color32()
212 }
213
214 pub fn system_green(&self) -> Color32 {
216 self.colors().system_green.to_color32()
217 }
218
219 pub fn system_yellow(&self) -> Color32 {
221 self.colors().system_yellow.to_color32()
222 }
223
224 pub fn system_orange(&self) -> Color32 {
226 self.colors().system_orange.to_color32()
227 }
228
229 pub fn system_red(&self) -> Color32 {
231 self.colors().system_red.to_color32()
232 }
233
234 pub fn alternating_row_even(&self) -> Color32 {
236 self.colors().alternating_row_even.to_color32()
237 }
238
239 pub fn alternating_row_odd(&self) -> Color32 {
241 self.colors().alternating_row_odd.to_color32()
242 }
243
244 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 pub fn scrubber_selection_border(&self) -> Color32 {
255 self.colors().scrubber_selection_border.to_color32()
256 }
257
258 pub fn scrubber_outside_dim(&self) -> Color32 {
260 self.colors().scrubber_outside_dim.to_color32()
261 }
262
263 pub fn scrubber_waveform(&self) -> Color32 {
265 self.colors().scrubber_waveform.to_color32()
266 }
267
268 pub fn scrubber_playhead(&self) -> Color32 {
270 self.colors().scrubber_playhead.to_color32()
271 }
272
273 pub fn on_accent_text(&self) -> Color32 {
275 self.colors().on_accent_text.to_color32()
276 }
277
278 pub fn knob_fill(&self) -> Color32 {
280 self.colors().knob_fill.to_color32()
281 }
282
283 pub fn drop_shadow(&self) -> Color32 {
285 self.colors().drop_shadow.to_color32()
286 }
287
288 pub fn backdrop_dim(&self) -> Color32 {
290 self.colors().backdrop_dim.to_color32()
291 }
292
293 pub fn hud_background(&self) -> Color32 {
295 self.colors().hud_background.to_color32()
296 }
297
298 pub fn hud_border(&self) -> Color32 {
300 self.colors().hud_border.to_color32()
301 }
302
303 pub fn hud_text(&self) -> Color32 {
305 self.colors().hud_text.to_color32()
306 }
307
308 pub fn hud_secondary_text(&self) -> Color32 {
310 self.colors().hud_secondary_text.to_color32()
311 }
312
313 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 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 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 pub fn traffic_light_glyph(&self) -> Color32 {
339 self.colors().traffic_light_glyph.to_color32()
340 }
341
342 pub fn windows_caption_close_hover_bg(&self) -> Color32 {
344 self.colors().caption_close_hover.to_color32()
345 }
346
347 pub fn windows_caption_close_pressed_bg(&self) -> Color32 {
349 self.colors().caption_close_pressed.to_color32()
350 }
351
352 pub fn windows_caption_hover_bg(&self) -> Color32 {
354 self.colors().caption_hover.to_color32()
355 }
356
357 pub fn windows_caption_pressed_bg(&self) -> Color32 {
359 self.colors().caption_pressed.to_color32()
360 }
361
362 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 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}