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