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 contrast_ratio, relative_luminance, srgb_channel_to_linear, MacBackgroundStyle,
11};
12
13static DEFAULT_TOKENS: OnceLock<Arc<MacDesignTokens>> = OnceLock::new();
14
15fn default_cached_tokens() -> Arc<MacDesignTokens> {
16 DEFAULT_TOKENS
17 .get_or_init(|| Arc::new(MacDesignTokens::load_default()))
18 .clone()
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23pub enum MacAppearance {
24 #[default]
26 Aqua,
27 Anthracite,
29 DarkAqua,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35pub enum WindowControlsStyle {
36 #[default]
38 AppleTrafficLights,
39 WindowsCaptionButtons,
41 NativeHost,
43}
44
45#[derive(Debug, Clone)]
61pub struct MacTheme {
62 pub appearance: MacAppearance,
64 pub window_controls_style: WindowControlsStyle,
66 tokens: Arc<MacDesignTokens>,
67 custom_accent: Option<Color32>,
68}
69
70impl Default for MacTheme {
71 fn default() -> Self {
72 Self::new(MacAppearance::Aqua)
73 }
74}
75
76impl MacTheme {
77 pub fn new(appearance: MacAppearance) -> Self {
79 Self {
80 appearance,
81 window_controls_style: WindowControlsStyle::AppleTrafficLights,
82 tokens: default_cached_tokens(),
83 custom_accent: None,
84 }
85 }
86
87 pub fn from_ctx(ctx: &egui::Context) -> Self {
90 ctx.data(|d| d.get_temp::<MacTheme>(Id::NULL)).unwrap_or_default()
91 }
92
93 pub fn resolve<'a>(theme: Option<&'a Self>, ctx: &egui::Context) -> std::borrow::Cow<'a, Self> {
95 match theme {
96 Some(t) => std::borrow::Cow::Borrowed(t),
97 None => std::borrow::Cow::Owned(Self::from_ctx(ctx)),
98 }
99 }
100
101 pub fn resolve_or_default<'a>(theme: Option<&'a Self>) -> std::borrow::Cow<'a, Self> {
103 match theme {
104 Some(t) => std::borrow::Cow::Borrowed(t),
105 None => std::borrow::Cow::Owned(Self::default()),
106 }
107 }
108
109 pub fn with_window_controls_style(mut self, style: WindowControlsStyle) -> Self {
111 self.window_controls_style = style;
112 self
113 }
114
115 pub fn window_controls_style(&self) -> WindowControlsStyle { self.window_controls_style }
117
118 pub fn with_accent(mut self, accent: Color32) -> Self {
120 self.custom_accent = Some(accent);
121 self
122 }
123
124 pub fn with_tokens(appearance: MacAppearance, tokens: MacDesignTokens) -> Self {
126 Self { appearance, window_controls_style: WindowControlsStyle::AppleTrafficLights, tokens: Arc::new(tokens), custom_accent: None }
127 }
128
129 pub fn with_arc_tokens(appearance: MacAppearance, tokens: Arc<MacDesignTokens>) -> Self {
131 Self { appearance, window_controls_style: WindowControlsStyle::AppleTrafficLights, tokens, custom_accent: None }
132 }
133
134 pub fn tokens(&self) -> &MacDesignTokens { &self.tokens }
136
137 pub fn colors(&self) -> &MacAppearanceColors {
139 match self.appearance {
140 MacAppearance::Aqua => &self.tokens.colors().aqua,
141 MacAppearance::Anthracite => &self.tokens.colors().anthracite,
142 MacAppearance::DarkAqua => &self.tokens.colors().dark_aqua,
143 }
144 }
145
146 pub fn window_background(&self) -> Color32 { self.colors().window_background.to_color32() }
148 pub fn control_background(&self) -> Color32 { self.colors().control_background.to_color32() }
150 pub fn control(&self) -> Color32 { self.colors().control.to_color32() }
152 pub fn control_accent(&self) -> Color32 {
154 self.custom_accent.unwrap_or_else(|| self.colors().control_accent.to_color32())
155 }
156 pub fn selected_control(&self) -> Color32 {
158 self.custom_accent.unwrap_or_else(|| self.colors().selected_control.to_color32())
159 }
160 pub fn selected_control_inactive(&self) -> Color32 { self.colors().selected_control_inactive.to_color32() }
162 pub fn separator(&self) -> Color32 { self.colors().separator.to_color32() }
164 pub fn placeholder_text(&self) -> Color32 { self.colors().placeholder_text.to_color32() }
166 pub fn keyboard_focus_indicator(&self) -> Color32 { self.colors().keyboard_focus_indicator.to_color32() }
168 pub fn label(&self) -> Color32 { self.colors().label.to_color32() }
170 pub fn secondary_label(&self) -> Color32 { self.colors().secondary_label.to_color32() }
172 pub fn tertiary_label(&self) -> Color32 { self.colors().tertiary_label.to_color32() }
174 pub fn box_background(&self) -> Color32 { self.colors().box_background.to_color32() }
176 pub fn box_border(&self) -> Color32 { self.colors().box_border.to_color32() }
178 pub fn system_green(&self) -> Color32 { self.colors().system_green.to_color32() }
180 pub fn system_yellow(&self) -> Color32 { self.colors().system_yellow.to_color32() }
182 pub fn system_orange(&self) -> Color32 { self.colors().system_orange.to_color32() }
184 pub fn system_red(&self) -> Color32 { self.colors().system_red.to_color32() }
186 pub fn alternating_row_even(&self) -> Color32 { self.colors().alternating_row_even.to_color32() }
188 pub fn alternating_row_odd(&self) -> Color32 { self.colors().alternating_row_odd.to_color32() }
190 pub fn alternating_content_background(&self, row_index: usize) -> Color32 {
192 if row_index.is_multiple_of(2) { self.alternating_row_even() } else { self.alternating_row_odd() }
193 }
194 pub fn scrubber_selection_border(&self) -> Color32 { self.colors().scrubber_selection_border.to_color32() }
196 pub fn scrubber_outside_dim(&self) -> Color32 { self.colors().scrubber_outside_dim.to_color32() }
198 pub fn scrubber_waveform(&self) -> Color32 { self.colors().scrubber_waveform.to_color32() }
200 pub fn scrubber_playhead(&self) -> Color32 { self.colors().scrubber_playhead.to_color32() }
202 pub fn on_accent_text(&self) -> Color32 { self.colors().on_accent_text.to_color32() }
204 pub fn knob_fill(&self) -> Color32 { self.colors().knob_fill.to_color32() }
206 pub fn drop_shadow(&self) -> Color32 { self.colors().drop_shadow.to_color32() }
208 pub fn backdrop_dim(&self) -> Color32 { self.colors().backdrop_dim.to_color32() }
210 pub fn hud_background(&self) -> Color32 { self.colors().hud_background.to_color32() }
212 pub fn hud_border(&self) -> Color32 { self.colors().hud_border.to_color32() }
214 pub fn hud_text(&self) -> Color32 { self.colors().hud_text.to_color32() }
216 pub fn hud_secondary_text(&self) -> Color32 { self.colors().hud_secondary_text.to_color32() }
218 pub fn traffic_light_close(&self) -> (Color32, Color32) {
220 (self.colors().traffic_light_close.to_color32(), self.colors().traffic_light_close_border.to_color32())
221 }
222 pub fn traffic_light_minimize(&self) -> (Color32, Color32) {
224 (self.colors().traffic_light_minimize.to_color32(), self.colors().traffic_light_minimize_border.to_color32())
225 }
226 pub fn traffic_light_zoom(&self) -> (Color32, Color32) {
228 (self.colors().traffic_light_zoom.to_color32(), self.colors().traffic_light_zoom_border.to_color32())
229 }
230 pub fn traffic_light_glyph(&self) -> Color32 { self.colors().traffic_light_glyph.to_color32() }
232 pub fn windows_caption_close_hover_bg(&self) -> Color32 { self.colors().caption_close_hover.to_color32() }
234 pub fn windows_caption_close_pressed_bg(&self) -> Color32 { self.colors().caption_close_pressed.to_color32() }
236 pub fn windows_caption_hover_bg(&self) -> Color32 { self.colors().caption_hover.to_color32() }
238 pub fn windows_caption_pressed_bg(&self) -> Color32 { self.colors().caption_pressed.to_color32() }
240 pub fn scrollbar_thumb_color(&self, active: bool, alpha_factor: f32) -> Color32 {
242 let base = if active { self.colors().scrollbar_thumb_active.to_color32() } else { self.colors().scrollbar_thumb.to_color32() };
243 base.gamma_multiply(alpha_factor.clamp(0.0, 1.0))
244 }
245
246 pub fn apply_to_egui(&self, ctx: &egui::Context) {
248 let unchanged = ctx.data(|d| {
249 d.get_temp::<MacTheme>(Id::NULL).is_some_and(|prev| {
250 prev.appearance == self.appearance
251 && prev.window_controls_style == self.window_controls_style
252 && prev.custom_accent == self.custom_accent
253 && Arc::ptr_eq(&prev.tokens, &self.tokens)
254 })
255 });
256 if unchanged { return; }
257 ctx.data_mut(|d| d.insert_temp(Id::NULL, self.clone()));
258 let egui_theme = match self.appearance {
259 MacAppearance::Aqua => egui::Theme::Light,
260 MacAppearance::Anthracite | MacAppearance::DarkAqua => egui::Theme::Dark,
261 };
262 ctx.set_theme(egui_theme);
263 let mut visuals = match self.appearance {
264 MacAppearance::Aqua => egui::Visuals::light(),
265 MacAppearance::Anthracite | MacAppearance::DarkAqua => egui::Visuals::dark(),
266 };
267 let border_w = self.button().regular.border_width.points();
268 let label_color = self.label();
269 visuals.override_text_color = Some(label_color);
270 visuals.widgets.noninteractive.fg_stroke.color = label_color;
271 visuals.widgets.noninteractive.bg_stroke = egui::Stroke::new(border_w, self.separator());
272 visuals.widgets.inactive.fg_stroke.color = label_color;
273 visuals.widgets.inactive.bg_stroke = egui::Stroke::new(border_w, self.separator());
274 visuals.panel_fill = self.window_background();
275 visuals.window_fill = self.window_background();
276 visuals.window_stroke = egui::Stroke::new(border_w, self.separator());
277 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());
278 ctx.all_styles_mut(|style| {
279 use egui::{FontFamily, FontId, TextStyle};
280 style.text_styles = [
281 (TextStyle::Small, FontId::new(small_pt, FontFamily::Proportional)),
282 (TextStyle::Body, FontId::new(body_pt, FontFamily::Proportional)),
283 (TextStyle::Button, FontId::new(body_pt, FontFamily::Proportional)),
284 (TextStyle::Heading, FontId::new(heading_pt, FontFamily::Proportional)),
285 (TextStyle::Monospace, FontId::new(body_pt, FontFamily::Monospace)),
286 ].into();
287 });
288 ctx.set_visuals_of(egui_theme, visuals.clone());
289 ctx.set_visuals(visuals);
290 }
291}