material_dioxus/
theming.rs1use std::fmt;
2
3use ::palette::{blend::Blend, color_difference::Wcag21RelativeContrast, Mix, Srgb, Srgba};
4use dioxus::prelude::*;
5
6use crate::palette::{self, Color};
7
8#[derive(Debug, PartialEq)]
9pub struct Colors {
10 pub primary: Color,
11 pub secondary: Option<Color>,
12 pub surface: Option<Color>,
13 pub inverse_surface: Color,
14 pub background: Color,
15 pub error: Color,
16
17 pub on_primary: Option<Color>,
18 pub on_secondary: Option<Color>,
19 pub on_surface: Option<Color>,
20 pub on_inverse_surface: Option<Color>,
21 pub on_error: Option<Color>,
22
23 pub four_color_progress: [Color; 4],
24 pub switch_use_secondary: bool,
25}
26
27impl Colors {
28 pub const DEFAULT_LIGHT: Self = Self {
29 primary: palette::PURPLE_500,
30 secondary: None,
31 surface: None,
32 inverse_surface: palette::from_u32(0x121212, 1.),
33 background: palette::from_u32(0xffffff, 1.),
34 error: palette::from_u32(0xb00020, 1.),
35
36 on_primary: None,
37 on_secondary: None,
38 on_surface: None,
39 on_inverse_surface: None,
40 on_error: None,
41
42 four_color_progress: [
43 palette::GREEN_500,
44 palette::YELLOW_500,
45 palette::RED_500,
46 palette::BLUE_500,
47 ],
48 switch_use_secondary: false,
49 };
50 pub const DEFAULT_DARK: Self = Self {
51 primary: palette::PURPLE_200,
52 inverse_surface: palette::from_u32(0xffffff, 1.),
53 background: palette::from_u32(0x121212, 1.),
54 error: palette::from_u32(0xcf6679, 1.),
55 ..Self::DEFAULT_LIGHT
56 };
57}
58
59#[derive(Props, PartialEq)]
60pub struct ThemeProps {
61 #[props(default = Colors::DEFAULT_LIGHT)]
62 theme: Colors,
63 #[props(!optional, default = Some(Colors::DEFAULT_DARK))]
64 dark_theme: Option<Colors>,
65}
66
67#[derive(Clone, Copy)]
68struct ColorDisplay(Color);
69impl fmt::Display for ColorDisplay {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 let (r, g, b, a) = self.0.into_components();
72 write!(f, "rgba({r}, {g}, {b}, {a})")
73 }
74}
75
76fn overlay(base: Color, overlay: Color, alpha: f32) -> Color {
77 let base: Srgba = base.into_format();
78 base.overlay(palette::with_alpha(overlay, alpha).into_format())
79 .into_format()
80}
81
82pub fn contrast_text(bg: Color) -> Color {
83 let bg = Srgba::<f32>::from_format(bg);
84 let light_contrast = bg.relative_contrast(Srgb::new(1., 1., 1.));
85 let dark_contrast = bg.relative_contrast(Srgb::new(0., 0., 0.));
86 const MIN_CONTRAST: f32 = 3.1;
87 if light_contrast < MIN_CONTRAST && dark_contrast > light_contrast {
88 palette::from_u32(0x000000, 0.87)
89 } else {
90 palette::from_u32(0xffffff, 1.)
91 }
92}
93
94fn mix(col1: Color, col2: Color, factor: f32) -> Color {
95 let col1 = Srgba::<f32>::from_format(col1);
96 let col2 = Srgba::<f32>::from_format(col2);
97 col2.mix(col1, factor).into_format()
98}
99
100#[rustfmt::skip]
101fn define_vars(colors: &Colors) -> String {
102 let primary = ColorDisplay(colors.primary);
103 let secondary = ColorDisplay(colors.secondary.unwrap_or(colors.primary));
104 let surface = ColorDisplay(colors.surface.unwrap_or(colors.background));
105 let background = ColorDisplay(colors.background);
106 let error = ColorDisplay(colors.error);
107
108 let on_primary = ColorDisplay(colors.on_primary.unwrap_or(contrast_text(primary.0)));
109 let on_secondary = ColorDisplay(colors.on_secondary.unwrap_or(contrast_text(secondary.0)));
110 let on_surface = ColorDisplay(colors.on_surface.unwrap_or(contrast_text(surface.0)));
111 let on_inverse_surface = ColorDisplay(colors.on_inverse_surface.unwrap_or(contrast_text(colors.inverse_surface)));
112 let on_error = ColorDisplay(colors.on_error.unwrap_or(contrast_text(colors.error)));
113
114 let surface_alpha = |alpha: f32| palette::with_alpha(on_surface.0, alpha);
115 let inverse_surface_alpha = |alpha: f32| palette::with_alpha(on_inverse_surface.0, alpha);
116
117 let text_primary_on_bg = ColorDisplay(surface_alpha(0.87));
118 let text_secondary_on_bg = ColorDisplay(surface_alpha(0.6));
119 let text_hint_on_bg = ColorDisplay(surface_alpha(0.6));
120 let text_disabled_on_bg = ColorDisplay(surface_alpha(0.38));
121 let text_icon_on_bg = ColorDisplay(surface_alpha(0.6));
122 let text_primary_on_inverse = ColorDisplay(inverse_surface_alpha(0.87));
123 let text_secondary_on_inverse = ColorDisplay(inverse_surface_alpha(0.6));
124 let text_hint_on_inverse = ColorDisplay(inverse_surface_alpha(0.6));
125 let text_disabled_on_inverse = ColorDisplay(inverse_surface_alpha(0.38));
126 let text_icon_on_inverse = ColorDisplay(inverse_surface_alpha(0.6));
127
128 let button_outline = ColorDisplay(surface_alpha(0.12));
129 let button_disabled_fill = ColorDisplay(surface_alpha(0.12));
130 let button_disabled_ink = ColorDisplay(surface_alpha(0.37));
131 let button_disabled_outline = ColorDisplay(surface_alpha(0.12));
132
133 let four_color_progress_1 = ColorDisplay(colors.four_color_progress[0]);
134 let four_color_progress_2 = ColorDisplay(colors.four_color_progress[1]);
135 let four_color_progress_3 = ColorDisplay(colors.four_color_progress[2]);
136 let four_color_progress_4 = ColorDisplay(colors.four_color_progress[3]);
137
138 let checkbox_ink = ColorDisplay(on_secondary.0);
139 let checkbox_unchecked = ColorDisplay(surface_alpha(0.54));
140 let checkbox_disabled = ColorDisplay(surface_alpha(0.38));
141
142 let switch_primary = ColorDisplay(if colors.switch_use_secondary { secondary.0 } else { colors.primary });
144 let switch_on_surface = ColorDisplay(palette::GREY_800);
145 let switch_on_surface_state_content = ColorDisplay(palette::GREY_700);
146 let switch_hairline = ColorDisplay(palette::GREY_400);
147 let switch_primary_state_content = switch_primary;
148 let switch_inverse_primary = ColorDisplay(overlay(switch_hairline.0, switch_primary.0, 0.25));
149
150 let textfield_idle_line = ColorDisplay(surface_alpha(0.42));
151 let textfield_hover_line = ColorDisplay(surface_alpha(0.87));
152 let textfield_disabled_line = ColorDisplay(surface_alpha(0.06));
153 let textfield_idle_border = ColorDisplay(surface_alpha(0.38));
154 let textfield_hover_border = ColorDisplay(surface_alpha(0.87));
155 let textfield_disabled_border = ColorDisplay(surface_alpha(0.06));
156 let textfield_fill = ColorDisplay(mix(on_surface.0, surface.0, 0.04));
157 let textfield_disabled_fill = ColorDisplay(mix(on_surface.0, surface.0, 0.02));
158 let textfield_ink = ColorDisplay(surface_alpha(0.87));
159 let textfield_label_ink = ColorDisplay(surface_alpha(0.6));
160 let textfield_disabled_ink = ColorDisplay(surface_alpha(0.37));
161 let textfield_icon = ColorDisplay(surface_alpha(0.54));
162 let textfield_disabled_icon = ColorDisplay(surface_alpha(0.3));
163
164 let dialog_scrim = ColorDisplay(surface_alpha(0.32));
165 let dialog_heading = ColorDisplay(surface_alpha(0.87));
166 let dialog_content = ColorDisplay(surface_alpha(0.6));
167 let dialog_divider = ColorDisplay(surface_alpha(0.12));
168
169 let textarea_idle_border = ColorDisplay(surface_alpha(0.38));
170 let textarea_hover_border = ColorDisplay(surface_alpha(0.87));
171 let textarea_disabled_border = ColorDisplay(surface_alpha(0.06));
172
173 let list_ripple = ColorDisplay(surface_alpha(1.));
174 let list_divider = ColorDisplay(surface_alpha(0.12));
175
176 format!(
177 "
178:root {{
179 --mdc-theme-primary: {primary};
180 --mdc-theme-secondary: {secondary};
181 --mdc-theme-surface: {surface};
182 --mdc-theme-background: {background};
183 --mdc-theme-error: {error};
184
185 --mdc-theme-on-primary: {on_primary};
186 --mdc-theme-on-secondary: {on_secondary};
187 --mdc-theme-on-surface: {on_surface};
188 --mdc-theme-on-error: {on_error};
189
190 --mdc-theme-text-primary-on-background: {text_primary_on_bg};
191 --mdc-theme-text-secondary-on-background: {text_secondary_on_bg};
192 --mdc-theme-text-hint-on-background: {text_hint_on_bg};
193 --mdc-theme-text-disabled-on-background: {text_disabled_on_bg};
194 --mdc-theme-text-icon-on-background: {text_icon_on_bg};
195 --mdc-theme-text-primary-on-light: {text_primary_on_bg};
196 --mdc-theme-text-secondary-on-light: {text_secondary_on_bg};
197 --mdc-theme-text-hint-on-light: {text_hint_on_bg};
198 --mdc-theme-text-disabled-on-light: {text_disabled_on_bg};
199 --mdc-theme-text-icon-on-light: {text_icon_on_bg};
200 --mdc-theme-text-primary-on-dark: {text_primary_on_inverse};
201 --mdc-theme-text-secondary-on-dark: {text_secondary_on_inverse};
202 --mdc-theme-text-hint-on-dark: {text_hint_on_inverse};
203 --mdc-theme-text-disabled-on-dark: {text_disabled_on_inverse};
204 --mdc-theme-text-icon-on-dark: {text_icon_on_inverse};
205
206 --mdc-button-outline-color: {button_outline};
207 --mdc-button-disabled-fill-color: {button_disabled_fill};
208 --mdc-button-disabled-ink-color: {button_disabled_ink};
209 --mdc-button-disabled-outline-color: {button_disabled_outline};
210
211 --mdc-circular-progress-bar-color-1: {four_color_progress_1};
212 --mdc-circular-progress-bar-color-2: {four_color_progress_2};
213 --mdc-circular-progress-bar-color-3: {four_color_progress_3};
214 --mdc-circular-progress-bar-color-4: {four_color_progress_4};
215
216 --mdc-checkbox-ink-color: {checkbox_ink};
217 --mdc-checkbox-unchecked-color: {checkbox_unchecked};
218 --mdc-checkbox-disabled-color: {checkbox_disabled};
219
220 --mdc-radio-unchecked-color: {checkbox_unchecked};
221 --mdc-radio-disabled-color: {checkbox_disabled};
222
223 --mdc-switch-disabled-selected-handle-color: {switch_on_surface};
224 --mdc-switch-disabled-selected-track-color: {switch_on_surface};
225 --mdc-switch-disabled-unselected-handle-color: {switch_on_surface};
226 --mdc-switch-disabled-unselected-track-color: {switch_on_surface};
227 --mdc-switch-selected-focus-handle-color: {switch_primary_state_content};
228 --mdc-switch-selected-focus-track-color: {switch_inverse_primary};
229 --mdc-switch-selected-hover-handle-color: {switch_primary_state_content};
230 --mdc-switch-selected-hover-track-color: {switch_inverse_primary};
231 --mdc-switch-selected-pressed-handle-color: {switch_primary_state_content};
232 --mdc-switch-selected-pressed-track-color: {switch_inverse_primary};
233 --mdc-switch-selected-track-color: {switch_inverse_primary};
234 --mdc-switch-unselected-focus-handle-color: {switch_on_surface_state_content};
235 --mdc-switch-unselected-focus-state-layer-color: {switch_on_surface};
236 --mdc-switch-unselected-focus-track-color: {switch_hairline};
237 --mdc-switch-unselected-handle-color: {switch_on_surface_state_content};
238 --mdc-switch-unselected-hover-handle-color: {switch_on_surface_state_content};
239 --mdc-switch-unselected-hover-state-layer-color: {switch_on_surface};
240 --mdc-switch-unselected-hover-track-color: {switch_hairline};
241 --mdc-switch-unselected-pressed-handle-color: {switch_on_surface_state_content};
242 --mdc-switch-unselected-pressed-state-layer-color: {switch_on_surface};
243 --mdc-switch-unselected-pressed-track-color: {switch_hairline};
244 --mdc-switch-unselected-track-color: {switch_hairline};
245
246 --mdc-text-field-idle-line-color: {textfield_idle_line};
247 --mdc-text-field-hover-line-color: {textfield_hover_line};
248 --mdc-text-field-disabled-line-color: {textfield_disabled_line};
249 --mdc-text-field-outlined-idle-border-color: {textfield_idle_border};
250 --mdc-text-field-outlined-hover-border-color: {textfield_hover_border};
251 --mdc-text-field-outlined-disabled-border-color: {textfield_disabled_border};
252 --mdc-text-field-fill-color: {textfield_fill};
253 --mdc-text-field-disabled-fill-color: {textfield_disabled_fill};
254 --mdc-text-field-ink-color: {textfield_ink};
255 --mdc-text-field-label-ink-color: {textfield_label_ink};
256 --mdc-text-field-disabled-ink-color: {textfield_disabled_ink};
257 --mdc-text-field-icon-color: {textfield_icon};
258 --mdc-text-field-disabled-icon-color: {textfield_disabled_icon};
259
260 --mdc-dialog-scrim-color: {dialog_scrim};
261 --mdc-dialog-heading-ink-color: {dialog_heading};
262 --mdc-dialog-content-ink-color: {dialog_content};
263 --mdc-dialog-scroll-divider-color: {dialog_divider};
264
265 --mdc-text-area-outlined-idle-border-color: {textarea_idle_border};
266 --mdc-text-area-outlined-hover-border-color: {textarea_hover_border};
267 --mdc-text-area-outlined-disabled-border-color: {textarea_disabled_border};
268
269 --mdc-deprecated-list-divider-color: {list_divider};
270}}
271
272mwc-switch {{
273 --mdc-theme-primary: {switch_primary};
274}}
275
276mwc-list {{
277 --mdc-ripple-color: {list_ripple};
278}}
279"
280 )
281}
282
283#[allow(non_snake_case)]
284pub fn MatTheme(cx: Scope<ThemeProps>) -> Element {
285 let light_theme = define_vars(&cx.props.theme);
286 let dark_theme = if let Some(colors) = &cx.props.dark_theme {
287 format!(
288 "@media screen and (prefers-color-scheme: dark) {{{}}}",
289 define_vars(colors)
290 )
291 } else {
292 String::new()
293 };
294
295 render! {
296 style { dangerous_inner_html: "{light_theme}{dark_theme}" }
297 }
298}