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 DarkAqua,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
37pub enum WindowControlsStyle {
38 #[default]
40 AppleTrafficLights,
41 WindowsCaptionButtons,
43 NativeHost,
45}
46
47#[derive(Debug, Clone)]
63pub struct MacTheme {
64 pub appearance: MacAppearance,
66 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 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 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 pub fn with_window_controls_style(mut self, style: WindowControlsStyle) -> Self {
98 self.window_controls_style = style;
99 self
100 }
101
102 pub fn window_controls_style(&self) -> WindowControlsStyle {
104 self.window_controls_style
105 }
106
107 pub fn with_accent(mut self, accent: Color32) -> Self {
109 self.custom_accent = Some(accent);
110 self
111 }
112
113 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 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 pub fn tokens(&self) -> &MacDesignTokens {
135 &self.tokens
136 }
137
138 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 pub fn window_background(&self) -> Color32 {
148 self.colors().window_background.to_color32()
149 }
150
151 pub fn control_background(&self) -> Color32 {
153 self.colors().control_background.to_color32()
154 }
155
156 pub fn control(&self) -> Color32 {
158 self.colors().control.to_color32()
159 }
160
161 pub fn control_accent(&self) -> Color32 {
163 self.custom_accent
164 .unwrap_or_else(|| self.colors().control_accent.to_color32())
165 }
166
167 pub fn selected_control(&self) -> Color32 {
169 self.custom_accent
170 .unwrap_or_else(|| self.colors().selected_control.to_color32())
171 }
172
173 pub fn selected_control_inactive(&self) -> Color32 {
175 self.colors().selected_control_inactive.to_color32()
176 }
177
178 pub fn separator(&self) -> Color32 {
180 self.colors().separator.to_color32()
181 }
182
183 pub fn placeholder_text(&self) -> Color32 {
185 self.colors().placeholder_text.to_color32()
186 }
187
188 pub fn keyboard_focus_indicator(&self) -> Color32 {
190 self.colors().keyboard_focus_indicator.to_color32()
191 }
192
193 pub fn label(&self) -> Color32 {
195 self.colors().label.to_color32()
196 }
197
198 pub fn secondary_label(&self) -> Color32 {
200 self.colors().secondary_label.to_color32()
201 }
202
203 pub fn system_green(&self) -> Color32 {
205 self.colors().system_green.to_color32()
206 }
207
208 pub fn system_yellow(&self) -> Color32 {
210 self.colors().system_yellow.to_color32()
211 }
212
213 pub fn system_orange(&self) -> Color32 {
215 self.colors().system_orange.to_color32()
216 }
217
218 pub fn system_red(&self) -> Color32 {
220 self.colors().system_red.to_color32()
221 }
222
223 pub fn alternating_row_even(&self) -> Color32 {
225 self.colors().alternating_row_even.to_color32()
226 }
227
228 pub fn alternating_row_odd(&self) -> Color32 {
230 self.colors().alternating_row_odd.to_color32()
231 }
232
233 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 pub fn scrubber_selection_border(&self) -> Color32 {
244 self.colors().scrubber_selection_border.to_color32()
245 }
246
247 pub fn scrubber_outside_dim(&self) -> Color32 {
249 self.colors().scrubber_outside_dim.to_color32()
250 }
251
252 pub fn scrubber_waveform(&self) -> Color32 {
254 self.colors().scrubber_waveform.to_color32()
255 }
256
257 pub fn scrubber_playhead(&self) -> Color32 {
259 self.colors().scrubber_playhead.to_color32()
260 }
261
262 pub fn on_accent_text(&self) -> Color32 {
264 self.colors().on_accent_text.to_color32()
265 }
266
267 pub fn knob_fill(&self) -> Color32 {
269 self.colors().knob_fill.to_color32()
270 }
271
272 pub fn drop_shadow(&self) -> Color32 {
274 self.colors().drop_shadow.to_color32()
275 }
276
277 pub fn backdrop_dim(&self) -> Color32 {
279 self.colors().backdrop_dim.to_color32()
280 }
281
282 pub fn hud_background(&self) -> Color32 {
284 self.colors().hud_background.to_color32()
285 }
286
287 pub fn hud_border(&self) -> Color32 {
289 self.colors().hud_border.to_color32()
290 }
291
292 pub fn hud_text(&self) -> Color32 {
294 self.colors().hud_text.to_color32()
295 }
296
297 pub fn hud_secondary_text(&self) -> Color32 {
299 self.colors().hud_secondary_text.to_color32()
300 }
301
302 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 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 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 pub fn traffic_light_glyph(&self) -> Color32 {
328 self.colors().traffic_light_glyph.to_color32()
329 }
330
331 pub fn windows_caption_close_hover_bg(&self) -> Color32 {
333 self.colors().caption_close_hover.to_color32()
334 }
335
336 pub fn windows_caption_close_pressed_bg(&self) -> Color32 {
338 self.colors().caption_close_pressed.to_color32()
339 }
340
341 pub fn windows_caption_hover_bg(&self) -> Color32 {
343 self.colors().caption_hover.to_color32()
344 }
345
346 pub fn windows_caption_pressed_bg(&self) -> Color32 {
348 self.colors().caption_pressed.to_color32()
349 }
350
351 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 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}