1use std::ops::Deref;
15
16use std::any::{Any, TypeId};
17use std::cell::RefCell;
18use std::collections::HashMap;
19use std::sync::OnceLock;
20
21use parking_lot::RwLock;
22
23use crate::Color;
24use crate::animation::{AnimationSpec, Easing};
25use web_time::Duration;
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
28pub enum TextDirection {
29 #[default]
30 Ltr,
31 Rtl,
32}
33
34thread_local! {
35 static LOCALS_STACK: RefCell<Vec<HashMap<TypeId, Box<dyn Any>>>> = RefCell::new(Vec::new());
36}
37
38#[derive(Clone, Copy, Debug)]
39struct Defaults {
40 theme: Theme,
41 text_direction: TextDirection,
42 ui_scale: UiScale,
43 text_scale: TextScale,
44 density: Density,
45 window_insets: WindowInsets,
46 window_size_class: WindowSizeClass,
47}
48
49impl Default for Defaults {
50 fn default() -> Self {
51 Self {
52 theme: Theme::default(),
53 text_direction: TextDirection::default(),
54 ui_scale: UiScale::default(),
55 text_scale: TextScale::default(),
56 density: Density::default(),
57 window_insets: WindowInsets::default(),
58 window_size_class: WindowSizeClass::default(),
59 }
60 }
61}
62
63static DEFAULTS: OnceLock<RwLock<Defaults>> = OnceLock::new();
64
65fn defaults() -> &'static RwLock<Defaults> {
66 DEFAULTS.get_or_init(|| RwLock::new(Defaults::default()))
67}
68
69pub fn set_theme_default(t: Theme) {
71 defaults().write().theme = t;
72}
73
74pub fn set_text_direction_default(d: TextDirection) {
76 defaults().write().text_direction = d;
77}
78
79pub fn set_ui_scale_default(s: UiScale) {
81 defaults().write().ui_scale = UiScale(s.0.max(0.0));
82}
83
84pub fn set_text_scale_default(s: TextScale) {
86 defaults().write().text_scale = TextScale(s.0.max(0.0));
87}
88
89pub fn set_density_default(d: Density) {
92 defaults().write().density = Density {
93 scale: d.scale.max(0.0),
94 };
95}
96
97#[derive(Clone, Copy, Debug, PartialEq)]
99pub struct Dp(pub f32);
100
101impl Dp {
102 pub fn to_px(self) -> f32 {
104 self.0 * density().scale * ui_scale().0
105 }
106}
107
108pub fn dp_to_px(dp: f32) -> f32 {
110 Dp(dp).to_px()
111}
112
113pub fn px_to_dp(px: f32) -> f32 {
115 let scale = density().scale * ui_scale().0;
116 if scale <= 0.0 { 0.0 } else { px / scale }
117}
118
119fn with_locals_frame<R>(f: impl FnOnce() -> R) -> R {
120 struct Guard;
121 impl Drop for Guard {
122 fn drop(&mut self) {
123 LOCALS_STACK.with(|st| {
124 st.borrow_mut().pop();
125 });
126 }
127 }
128 LOCALS_STACK.with(|st| st.borrow_mut().push(HashMap::new()));
129 let _guard = Guard;
130 f()
131}
132
133fn set_local_boxed(t: TypeId, v: Box<dyn Any>) {
134 LOCALS_STACK.with(|st| {
135 if let Some(top) = st.borrow_mut().last_mut() {
136 top.insert(t, v);
137 } else {
138 let mut m = HashMap::new();
140 m.insert(t, v);
141 st.borrow_mut().push(m);
142 }
143 });
144}
145
146fn get_local<T: 'static + Copy>() -> Option<T> {
147 LOCALS_STACK.with(|st| {
148 for frame in st.borrow().iter().rev() {
149 if let Some(v) = frame.get(&TypeId::of::<T>())
150 && let Some(t) = v.downcast_ref::<T>()
151 {
152 return Some(*t);
153 }
154 }
155 None
156 })
157}
158
159#[derive(Clone, Copy, Debug)]
160#[must_use]
161pub struct ColorScheme {
162 pub primary: Color,
163 pub on_primary: Color,
164 pub primary_container: Color,
165 pub on_primary_container: Color,
166
167 pub secondary: Color,
168 pub on_secondary: Color,
169 pub secondary_container: Color,
170 pub on_secondary_container: Color,
171
172 pub tertiary: Color,
173 pub on_tertiary: Color,
174 pub tertiary_container: Color,
175 pub on_tertiary_container: Color,
176
177 pub error: Color,
178 pub on_error: Color,
179 pub error_container: Color,
180 pub on_error_container: Color,
181
182 pub background: Color,
183 pub on_background: Color,
184 pub surface: Color,
185 pub on_surface: Color,
186 pub surface_variant: Color,
187 pub on_surface_variant: Color,
188 pub surface_container_lowest: Color,
189 pub surface_container_low: Color,
190 pub surface_container: Color,
191 pub surface_container_high: Color,
192 pub surface_container_highest: Color,
193 pub surface_bright: Color,
194 pub surface_dim: Color,
195 pub surface_tint: Color,
196
197 pub inverse_surface: Color,
198 pub inverse_on_surface: Color,
199 pub inverse_primary: Color,
200
201 pub outline: Color,
202 pub outline_variant: Color,
203
204 pub scrim: Color,
205 pub shadow: Color,
206 pub focus: Color,
207}
208
209impl ColorScheme {
210 pub fn dark() -> Self {
211 Self {
212 primary: Color::from_hex("#69FDBE"),
213 on_primary: Color::from_hex("#003020"),
214 primary_container: Color::from_hex("#004D40"),
215 on_primary_container: Color::from_hex("#6FF7F6"),
216
217 secondary: Color::from_hex("#B3C9A7"),
218 on_secondary: Color::from_hex("#1C3519"),
219 secondary_container: Color::from_hex("#334D2E"),
220 on_secondary_container: Color::from_hex("#CCE8B3"),
221
222 tertiary: Color::from_hex("#FFC9C1"),
223 on_tertiary: Color::from_hex("#3F1619"),
224 tertiary_container: Color::from_hex("#5D1F22"),
225 on_tertiary_container: Color::from_hex("#FFDBD8"),
226
227 error: Color::from_hex("#F2B8B5"),
228 on_error: Color::from_hex("#601410"),
229 error_container: Color::from_hex("#8C1D18"),
230 on_error_container: Color::from_hex("#F9DEDC"),
231
232 background: Color::from_hex("#1A1C1E"),
233 on_background: Color::from_hex("#E6E1E5"),
234 surface: Color::from_hex("#1A1C1E"),
235 on_surface: Color::from_hex("#E6E1E5"),
236 surface_variant: Color::from_hex("#44474E"),
237 on_surface_variant: Color::from_hex("#C4C6CE"),
238 surface_container_lowest: Color::from_hex("#0A0A0C"),
239 surface_container_low: Color::from_hex("#141115"),
240 surface_container: Color::from_hex("#19131A"),
241 surface_container_high: Color::from_hex("#1F1B22"),
242 surface_container_highest: Color::from_hex("#2A2930"),
243 surface_bright: Color::from_hex("#26292F"),
244 surface_dim: Color::from_hex("#1A1C1E"),
245 surface_tint: Color::from_hex("#69FDBE"),
246
247 inverse_surface: Color::from_hex("#E6E1E5"),
248 inverse_on_surface: Color::from_hex("#2A2930"),
249 inverse_primary: Color::from_hex("#005048"),
250
251 outline: Color::from_hex("#74777F"),
252 outline_variant: Color::from_hex("#44474E"),
253
254 scrim: Color::from_hex("#000000"),
255 shadow: Color::from_hex("#000000"),
256 focus: Color::from_hex("#006A6A"),
257 }
258 }
259
260 pub fn light() -> Self {
261 Self {
262 primary: Color::from_hex("#006A6A"),
263 on_primary: Color::WHITE,
264 primary_container: Color::from_hex("#9EF0EC"),
265 on_primary_container: Color::from_hex("#002020"),
266
267 secondary: Color::from_hex("#586146"),
268 on_secondary: Color::WHITE,
269 secondary_container: Color::from_hex("#D8E3B8"),
270 on_secondary_container: Color::from_hex("#161C0A"),
271
272 tertiary: Color::from_hex("#744639"),
273 on_tertiary: Color::WHITE,
274 tertiary_container: Color::from_hex("#FFD9CD"),
275 on_tertiary_container: Color::from_hex("#2C0E07"),
276
277 error: Color::from_hex("#BA1A1A"),
278 on_error: Color::WHITE,
279 error_container: Color::from_hex("#FFDAD6"),
280 on_error_container: Color::from_hex("#410002"),
281
282 background: Color::from_hex("#FEF7FF"),
283 on_background: Color::from_hex("#1A1C1E"),
284 surface: Color::from_hex("#FEF7FF"),
285 on_surface: Color::from_hex("#1A1C1E"),
286 surface_variant: Color::from_hex("#E1E3DE"),
287 on_surface_variant: Color::from_hex("#44474E"),
288 surface_container_lowest: Color::WHITE,
289 surface_container_low: Color::from_hex("#F4F5F0"),
290 surface_container: Color::from_hex("#EEF0E9"),
291 surface_container_high: Color::from_hex("#E9EAE4"),
292 surface_container_highest: Color::from_hex("#E3E5DF"),
293 surface_bright: Color::from_hex("#FEF7FF"),
294 surface_dim: Color::from_hex("#DEDAD0"),
295 surface_tint: Color::from_hex("#006A6A"),
296
297 inverse_surface: Color::from_hex("#2F3033"),
298 inverse_on_surface: Color::from_hex("#F1F0F4"),
299 inverse_primary: Color::from_hex("#69FDBE"),
300
301 outline: Color::from_hex("#74777F"),
302 outline_variant: Color::from_hex("#C4C6CE"),
303
304 scrim: Color::from_hex("#000000"),
305 shadow: Color::from_hex("#000000"),
306 focus: Color::from_hex("#1D4ED8"),
307 }
308 }
309}
310
311impl Default for ColorScheme {
312 fn default() -> Self {
313 Self::dark()
314 }
315}
316
317#[derive(Clone, Copy, Debug)]
318#[must_use]
319pub struct Typography {
320 pub display_large: f32,
321 pub display_medium: f32,
322 pub display_small: f32,
323 pub headline_large: f32,
324 pub headline_medium: f32,
325 pub headline_small: f32,
326 pub title_large: f32,
327 pub title_medium: f32,
328 pub title_small: f32,
329 pub body_large: f32,
330 pub body_medium: f32,
331 pub body_small: f32,
332 pub label_large: f32,
333 pub label_medium: f32,
334 pub label_small: f32,
335}
336
337impl Default for Typography {
338 fn default() -> Self {
339 Self {
340 display_large: 57.0,
341 display_medium: 45.0,
342 display_small: 36.0,
343 headline_large: 32.0,
344 headline_medium: 28.0,
345 headline_small: 24.0,
346 title_large: 22.0,
347 title_medium: 16.0,
348 title_small: 14.0,
349 body_large: 16.0,
350 body_medium: 14.0,
351 body_small: 12.0,
352 label_large: 14.0,
353 label_medium: 12.0,
354 label_small: 11.0,
355 }
356 }
357}
358
359#[derive(Clone, Copy, Debug)]
360#[must_use]
361pub struct Shapes {
362 pub extra_small: f32,
363 pub small: f32,
364 pub medium: f32,
365 pub large: f32,
366 pub extra_large: f32,
367}
368
369impl Default for Shapes {
370 fn default() -> Self {
371 Self {
372 extra_small: 4.0,
373 small: 8.0,
374 medium: 12.0,
375 large: 16.0,
376 extra_large: 28.0,
377 }
378 }
379}
380
381#[derive(Clone, Copy, Debug)]
382#[must_use]
383pub struct Spacing {
384 pub xs: f32,
385 pub sm: f32,
386 pub md: f32,
387 pub lg: f32,
388 pub xl: f32,
389 pub xxl: f32,
390}
391
392impl Default for Spacing {
393 fn default() -> Self {
394 Self {
395 xs: 4.0,
396 sm: 8.0,
397 md: 12.0,
398 lg: 16.0,
399 xl: 24.0,
400 xxl: 32.0,
401 }
402 }
403}
404
405#[derive(Clone, Copy, Debug)]
406#[must_use]
407pub struct Elevation {
408 pub level0: f32,
409 pub level1: f32,
410 pub level2: f32,
411 pub level3: f32,
412 pub level4: f32,
413 pub level5: f32,
414}
415
416impl Default for Elevation {
417 fn default() -> Self {
418 Self {
419 level0: 0.0,
420 level1: 1.0,
421 level2: 3.0,
422 level3: 6.0,
423 level4: 8.0,
424 level5: 12.0,
425 }
426 }
427}
428
429#[derive(Clone, Copy, Debug)]
431#[must_use]
432pub struct MotionScheme {
433 pub shape: AnimationSpec,
436 pub color: AnimationSpec,
439 pub color_fast: AnimationSpec,
442 pub overlay: AnimationSpec,
445 pub spring: AnimationSpec,
448 pub expand: AnimationSpec,
451 pub layout: AnimationSpec,
454}
455
456impl Default for MotionScheme {
457 fn default() -> Self {
458 Self {
459 shape: AnimationSpec::tween(Duration::from_millis(200), Easing::FastOutSlowIn),
460 color: AnimationSpec::tween(Duration::from_millis(150), Easing::FastOutSlowIn),
461 color_fast: AnimationSpec::tween(Duration::from_millis(100), Easing::FastOutSlowIn),
462 overlay: AnimationSpec::tween(Duration::from_millis(120), Easing::FastOutSlowIn),
463 spring: AnimationSpec::spring_gentle(),
464 expand: AnimationSpec::tween(Duration::from_millis(250), Easing::FastOutSlowIn),
465 layout: AnimationSpec::tween(Duration::from_millis(300), Easing::EaseOut),
466 }
467 }
468}
469
470#[derive(Clone, Copy, Debug)]
471#[must_use]
472pub struct Theme {
473 pub colors: ColorScheme,
474 pub typography: Typography,
475 pub shapes: Shapes,
476 pub spacing: Spacing,
477 pub elevation: Elevation,
478 pub motion: MotionScheme,
479
480 pub focus: Color,
481 pub scrollbar_track: Color,
482 pub scrollbar_thumb: Color,
483 pub button_bg: Color,
484 pub button_bg_hover: Color,
485 pub button_bg_pressed: Color,
486}
487
488impl Deref for Theme {
489 type Target = ColorScheme;
490 fn deref(&self) -> &Self::Target {
491 &self.colors
492 }
493}
494
495impl Default for Theme {
496 fn default() -> Self {
497 let colors = ColorScheme::default();
498 Self {
499 colors,
500 typography: Typography::default(),
501 shapes: Shapes::default(),
502 spacing: Spacing::default(),
503 elevation: Elevation::default(),
504 motion: MotionScheme::default(),
505 focus: colors.focus,
506 scrollbar_track: Color(0xDD, 0xDD, 0xDD, 32),
507 scrollbar_thumb: Color(0xDD, 0xDD, 0xDD, 140),
508 button_bg: colors.primary,
509 button_bg_hover: colors.primary_container,
510 button_bg_pressed: colors.on_primary_container,
511 }
512 }
513}
514
515impl Theme {
516 pub fn with_colors(mut self, colors: ColorScheme) -> Self {
517 self.colors = colors;
518 self
519 }
520}
521
522#[derive(Clone, Copy, Debug)]
524pub struct Density {
525 pub scale: f32,
526}
527impl Default for Density {
528 fn default() -> Self {
529 Self { scale: 1.0 }
530 }
531}
532
533#[derive(Clone, Copy, Debug)]
535pub struct UiScale(pub f32);
536impl Default for UiScale {
537 fn default() -> Self {
538 Self(1.0)
539 }
540}
541
542#[derive(Clone, Copy, Debug)]
543pub struct TextScale(pub f32);
544impl Default for TextScale {
545 fn default() -> Self {
546 Self(1.0)
547 }
548}
549
550pub fn with_theme<R>(theme: Theme, f: impl FnOnce() -> R) -> R {
551 with_locals_frame(|| {
552 set_local_boxed(TypeId::of::<Theme>(), Box::new(theme));
553 f()
554 })
555}
556
557pub fn with_density<R>(density: Density, f: impl FnOnce() -> R) -> R {
558 with_locals_frame(|| {
559 set_local_boxed(TypeId::of::<Density>(), Box::new(density));
560 f()
561 })
562}
563
564pub fn with_ui_scale<R>(s: UiScale, f: impl FnOnce() -> R) -> R {
565 with_locals_frame(|| {
566 set_local_boxed(TypeId::of::<UiScale>(), Box::new(s));
567 f()
568 })
569}
570
571pub fn with_text_scale<R>(ts: TextScale, f: impl FnOnce() -> R) -> R {
572 with_locals_frame(|| {
573 set_local_boxed(TypeId::of::<TextScale>(), Box::new(ts));
574 f()
575 })
576}
577
578pub fn with_text_direction<R>(dir: TextDirection, f: impl FnOnce() -> R) -> R {
579 with_locals_frame(|| {
580 set_local_boxed(TypeId::of::<TextDirection>(), Box::new(dir));
581 f()
582 })
583}
584
585pub fn with_window_insets<R>(insets: WindowInsets, f: impl FnOnce() -> R) -> R {
586 with_locals_frame(|| {
587 set_local_boxed(TypeId::of::<WindowInsets>(), Box::new(insets));
588 f()
589 })
590}
591
592#[derive(Clone, Copy, Debug)]
593pub struct ContentColor(pub Color);
594
595pub fn with_content_color<R>(color: Color, f: impl FnOnce() -> R) -> R {
596 with_locals_frame(|| {
597 set_local_boxed(TypeId::of::<ContentColor>(), Box::new(ContentColor(color)));
598 f()
599 })
600}
601
602pub fn content_color() -> Color {
603 get_local::<ContentColor>()
604 .map(|c| c.0)
605 .unwrap_or_else(|| theme().on_surface)
606}
607
608#[derive(Clone, Copy, Debug, Default, PartialEq)]
610pub struct WindowInsets {
611 pub top: f32,
612 pub bottom: f32,
613 pub left: f32,
614 pub right: f32,
615 pub ime_bottom: f32,
618}
619
620pub fn set_window_insets_default(insets: WindowInsets) {
622 defaults().write().window_insets = insets;
623}
624
625pub fn set_ime_inset(height_px: f32) {
628 let mut insets = defaults().write().window_insets;
629 insets.ime_bottom = height_px;
630 set_local_boxed(TypeId::of::<WindowInsets>(), Box::new(insets));
632}
633
634pub fn window_insets() -> WindowInsets {
636 get_local::<WindowInsets>().unwrap_or_else(|| defaults().read().window_insets)
637}
638
639#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
647pub enum WidthClass {
648 Compact,
649 Medium,
650 Expanded,
651}
652
653impl Default for WidthClass {
654 fn default() -> Self {
655 WidthClass::Compact
656 }
657}
658
659#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
667pub enum HeightClass {
668 Compact,
669 Medium,
670 Expanded,
671}
672
673impl Default for HeightClass {
674 fn default() -> Self {
675 HeightClass::Compact
676 }
677}
678
679#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
685pub struct WindowSizeClass {
686 pub width: WidthClass,
687 pub height: HeightClass,
688}
689
690impl WindowSizeClass {
691 pub fn is_expanded_width(&self) -> bool {
693 matches!(self.width, WidthClass::Expanded)
694 }
695 pub fn is_at_least_medium_width(&self) -> bool {
698 matches!(self.width, WidthClass::Medium | WidthClass::Expanded)
699 }
700}
701
702pub fn calculate_window_size_class(
705 width_px: u32,
706 height_px: u32,
707 density_scale: f32,
708) -> WindowSizeClass {
709 let density = density_scale.max(0.0001);
710 let width_dp = (width_px as f32) / density;
711 let height_dp = (height_px as f32) / density;
712
713 let width = if width_dp < 600.0 {
714 WidthClass::Compact
715 } else if width_dp < 840.0 {
716 WidthClass::Medium
717 } else {
718 WidthClass::Expanded
719 };
720 let height = if height_dp < 480.0 {
721 HeightClass::Compact
722 } else if height_dp < 900.0 {
723 HeightClass::Medium
724 } else {
725 HeightClass::Expanded
726 };
727
728 WindowSizeClass { width, height }
729}
730
731pub fn set_window_size_class_default(class: WindowSizeClass) {
734 defaults().write().window_size_class = class;
735}
736
737pub fn with_window_size_class<R>(class: WindowSizeClass, f: impl FnOnce() -> R) -> R {
739 with_locals_frame(|| {
740 set_local_boxed(TypeId::of::<WindowSizeClass>(), Box::new(class));
741 f()
742 })
743}
744
745pub fn window_size_class() -> WindowSizeClass {
748 get_local::<WindowSizeClass>().unwrap_or_else(|| defaults().read().window_size_class)
749}
750
751macro_rules! def_local_getter {
752 ($fn_name:ident, $ty:ty, $default_field:ident) => {
753 pub fn $fn_name() -> $ty {
754 get_local::<$ty>().unwrap_or_else(|| defaults().read().$default_field)
755 }
756 };
757}
758
759def_local_getter!(theme, Theme, theme);
760def_local_getter!(density, Density, density);
761def_local_getter!(ui_scale, UiScale, ui_scale);
762def_local_getter!(text_scale, TextScale, text_scale);
763def_local_getter!(text_direction, TextDirection, text_direction);
764
765#[cfg(test)]
766mod tests {
767 use super::*;
768
769 #[test]
770 fn width_class_thresholds_match_m3() {
771 assert_eq!(calculate_window_size_class(100, 100, 1.0).width, WidthClass::Compact);
773 assert_eq!(calculate_window_size_class(599, 100, 1.0).width, WidthClass::Compact);
774 assert_eq!(calculate_window_size_class(600, 100, 1.0).width, WidthClass::Medium);
775 assert_eq!(calculate_window_size_class(839, 100, 1.0).width, WidthClass::Medium);
776 assert_eq!(calculate_window_size_class(840, 100, 1.0).width, WidthClass::Expanded);
777 assert_eq!(calculate_window_size_class(2000, 100, 1.0).width, WidthClass::Expanded);
778 }
779
780 #[test]
781 fn height_class_thresholds_match_m3() {
782 assert_eq!(calculate_window_size_class(100, 100, 1.0).height, HeightClass::Compact);
783 assert_eq!(calculate_window_size_class(100, 479, 1.0).height, HeightClass::Compact);
784 assert_eq!(calculate_window_size_class(100, 480, 1.0).height, HeightClass::Medium);
785 assert_eq!(calculate_window_size_class(100, 899, 1.0).height, HeightClass::Medium);
786 assert_eq!(calculate_window_size_class(100, 900, 1.0).height, HeightClass::Expanded);
787 }
788
789 #[test]
790 fn density_scales_thresholds() {
791 let c = calculate_window_size_class(1199, 100, 2.0);
793 assert_eq!(c.width, WidthClass::Compact);
794 let c = calculate_window_size_class(1200, 100, 2.0);
795 assert_eq!(c.width, WidthClass::Medium);
796 }
797
798 #[test]
799 fn is_at_least_medium_width() {
800 let c = WindowSizeClass {
801 width: WidthClass::Compact,
802 height: HeightClass::Compact,
803 };
804 assert!(!c.is_at_least_medium_width());
805 let c = WindowSizeClass {
806 width: WidthClass::Medium,
807 height: HeightClass::Compact,
808 };
809 assert!(c.is_at_least_medium_width());
810 let c = WindowSizeClass {
811 width: WidthClass::Expanded,
812 height: HeightClass::Compact,
813 };
814 assert!(c.is_at_least_medium_width());
815 }
816}