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 std::rc::Rc;
24
25use crate::Color;
26use crate::animation::{AnimationSpec, Easing};
27use crate::indication::IndicationNodeFactory;
28use web_time::Duration;
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
31pub enum TextDirection {
32 #[default]
33 Ltr,
34 Rtl,
35}
36
37thread_local! {
38 static LOCALS_STACK: RefCell<Vec<HashMap<TypeId, Box<dyn Any>>>> = RefCell::new(Vec::new());
39}
40
41#[derive(Clone, Copy, Debug, Default)]
42struct Defaults {
43 theme: Theme,
44 text_direction: TextDirection,
45 ui_scale: UiScale,
46 text_scale: TextScale,
47 density: Density,
48 window_insets: WindowInsets,
49 window_size_class: WindowSizeClass,
50 container_width: f32,
52 container_height: f32,
53}
54
55static DEFAULTS: OnceLock<RwLock<Defaults>> = OnceLock::new();
56
57fn defaults() -> &'static RwLock<Defaults> {
58 DEFAULTS.get_or_init(|| {
59 RwLock::new(Defaults {
60 container_width: 360.0,
61 container_height: 800.0,
62 ..Default::default()
63 })
64 })
65}
66
67pub fn set_theme_default(t: Theme) {
69 defaults().write().theme = t;
70}
71
72pub fn set_text_direction_default(d: TextDirection) {
74 defaults().write().text_direction = d;
75}
76
77pub fn set_ui_scale_default(s: UiScale) {
79 defaults().write().ui_scale = UiScale(s.0.max(0.0));
80}
81
82pub fn set_text_scale_default(s: TextScale) {
84 defaults().write().text_scale = TextScale(s.0.max(0.0));
85}
86
87pub fn set_density_default(d: Density) {
90 defaults().write().density = Density {
91 scale: d.scale.max(0.0),
92 };
93}
94
95pub use crate::units::{Dp, DpOffset, DpRect, DpSize, Px, Sp, UnitExt};
96
97#[inline]
99pub fn effective_density_scale() -> f32 {
100 (density().scale * ui_scale().0).max(0.0001)
101}
102
103#[inline]
105pub fn dp_to_px(dp: Dp) -> Px {
106 dp.to_px()
107}
108
109#[inline]
111pub fn px_to_dp(px: Px) -> Dp {
112 px.to_dp()
113}
114
115fn with_locals_frame<R>(f: impl FnOnce() -> R) -> R {
116 struct Guard;
117 impl Drop for Guard {
118 fn drop(&mut self) {
119 let _ = LOCALS_STACK.try_with(|st| {
120 st.borrow_mut().pop();
121 });
122 }
123 }
124 LOCALS_STACK.with(|st| st.borrow_mut().push(HashMap::new()));
125 let _guard = Guard;
126 f()
127}
128
129fn set_local_boxed(t: TypeId, v: Box<dyn Any>) {
130 LOCALS_STACK.with(|st| {
131 if let Some(top) = st.borrow_mut().last_mut() {
132 top.insert(t, v);
133 } else {
134 let mut m = HashMap::new();
136 m.insert(t, v);
137 st.borrow_mut().push(m);
138 }
139 });
140}
141
142fn get_local<T: 'static + Copy>() -> Option<T> {
143 LOCALS_STACK.with(|st| {
144 for frame in st.borrow().iter().rev() {
145 if let Some(v) = frame.get(&TypeId::of::<T>())
146 && let Some(t) = v.downcast_ref::<T>()
147 {
148 return Some(*t);
149 }
150 }
151 None
152 })
153}
154
155#[derive(Clone, Copy, Debug)]
156#[must_use]
157pub struct ColorScheme {
158 pub primary: Color,
159 pub on_primary: Color,
160 pub primary_container: Color,
161 pub on_primary_container: Color,
162
163 pub secondary: Color,
164 pub on_secondary: Color,
165 pub secondary_container: Color,
166 pub on_secondary_container: Color,
167
168 pub tertiary: Color,
169 pub on_tertiary: Color,
170 pub tertiary_container: Color,
171 pub on_tertiary_container: Color,
172
173 pub error: Color,
174 pub on_error: Color,
175 pub error_container: Color,
176 pub on_error_container: Color,
177
178 pub background: Color,
179 pub on_background: Color,
180 pub surface: Color,
181 pub on_surface: Color,
182 pub surface_variant: Color,
183 pub on_surface_variant: Color,
184 pub surface_container_lowest: Color,
185 pub surface_container_low: Color,
186 pub surface_container: Color,
187 pub surface_container_high: Color,
188 pub surface_container_highest: Color,
189 pub surface_bright: Color,
190 pub surface_dim: Color,
191 pub surface_tint: Color,
192
193 pub inverse_surface: Color,
194 pub inverse_on_surface: Color,
195 pub inverse_primary: Color,
196
197 pub outline: Color,
198 pub outline_variant: Color,
199
200 pub scrim: Color,
201 pub shadow: Color,
202 pub focus: Color,
203}
204
205impl ColorScheme {
206 pub fn dark() -> Self {
207 Self {
208 primary: Color::from_hex("#69FDBE"),
209 on_primary: Color::from_hex("#003020"),
210 primary_container: Color::from_hex("#004D40"),
211 on_primary_container: Color::from_hex("#6FF7F6"),
212
213 secondary: Color::from_hex("#B3C9A7"),
214 on_secondary: Color::from_hex("#1C3519"),
215 secondary_container: Color::from_hex("#334D2E"),
216 on_secondary_container: Color::from_hex("#CCE8B3"),
217
218 tertiary: Color::from_hex("#FFC9C1"),
219 on_tertiary: Color::from_hex("#3F1619"),
220 tertiary_container: Color::from_hex("#5D1F22"),
221 on_tertiary_container: Color::from_hex("#FFDBD8"),
222
223 error: Color::from_hex("#F2B8B5"),
224 on_error: Color::from_hex("#601410"),
225 error_container: Color::from_hex("#8C1D18"),
226 on_error_container: Color::from_hex("#F9DEDC"),
227
228 background: Color::from_hex("#1A1C1E"),
229 on_background: Color::from_hex("#E6E1E5"),
230 surface: Color::from_hex("#1A1C1E"),
231 on_surface: Color::from_hex("#E6E1E5"),
232 surface_variant: Color::from_hex("#44474E"),
233 on_surface_variant: Color::from_hex("#C4C6CE"),
234 surface_container_lowest: Color::from_hex("#0A0A0C"),
235 surface_container_low: Color::from_hex("#141115"),
236 surface_container: Color::from_hex("#19131A"),
237 surface_container_high: Color::from_hex("#1F1B22"),
238 surface_container_highest: Color::from_hex("#2A2930"),
239 surface_bright: Color::from_hex("#26292F"),
240 surface_dim: Color::from_hex("#1A1C1E"),
241 surface_tint: Color::from_hex("#69FDBE"),
242
243 inverse_surface: Color::from_hex("#E6E1E5"),
244 inverse_on_surface: Color::from_hex("#2A2930"),
245 inverse_primary: Color::from_hex("#005048"),
246
247 outline: Color::from_hex("#74777F"),
248 outline_variant: Color::from_hex("#44474E"),
249
250 scrim: Color::from_hex("#000000"),
251 shadow: Color::from_hex("#000000"),
252 focus: Color::from_hex("#006A6A"),
253 }
254 }
255
256 pub fn light() -> Self {
257 Self {
258 primary: Color::from_hex("#006A6A"),
259 on_primary: Color::WHITE,
260 primary_container: Color::from_hex("#9EF0EC"),
261 on_primary_container: Color::from_hex("#002020"),
262
263 secondary: Color::from_hex("#586146"),
264 on_secondary: Color::WHITE,
265 secondary_container: Color::from_hex("#D8E3B8"),
266 on_secondary_container: Color::from_hex("#161C0A"),
267
268 tertiary: Color::from_hex("#744639"),
269 on_tertiary: Color::WHITE,
270 tertiary_container: Color::from_hex("#FFD9CD"),
271 on_tertiary_container: Color::from_hex("#2C0E07"),
272
273 error: Color::from_hex("#BA1A1A"),
274 on_error: Color::WHITE,
275 error_container: Color::from_hex("#FFDAD6"),
276 on_error_container: Color::from_hex("#410002"),
277
278 background: Color::from_hex("#FEF7FF"),
279 on_background: Color::from_hex("#1A1C1E"),
280 surface: Color::from_hex("#FEF7FF"),
281 on_surface: Color::from_hex("#1A1C1E"),
282 surface_variant: Color::from_hex("#E1E3DE"),
283 on_surface_variant: Color::from_hex("#44474E"),
284 surface_container_lowest: Color::WHITE,
285 surface_container_low: Color::from_hex("#F4F5F0"),
286 surface_container: Color::from_hex("#EEF0E9"),
287 surface_container_high: Color::from_hex("#E9EAE4"),
288 surface_container_highest: Color::from_hex("#E3E5DF"),
289 surface_bright: Color::from_hex("#FEF7FF"),
290 surface_dim: Color::from_hex("#DEDAD0"),
291 surface_tint: Color::from_hex("#006A6A"),
292
293 inverse_surface: Color::from_hex("#2F3033"),
294 inverse_on_surface: Color::from_hex("#F1F0F4"),
295 inverse_primary: Color::from_hex("#69FDBE"),
296
297 outline: Color::from_hex("#74777F"),
298 outline_variant: Color::from_hex("#C4C6CE"),
299
300 scrim: Color::from_hex("#000000"),
301 shadow: Color::from_hex("#000000"),
302 focus: Color::from_hex("#1D4ED8"),
303 }
304 }
305
306 pub fn content_color_for(self, background: Color) -> Option<Color> {
309 if background == self.primary {
310 Some(self.on_primary)
311 } else if background == self.primary_container {
312 Some(self.on_primary_container)
313 } else if background == self.secondary {
314 Some(self.on_secondary)
315 } else if background == self.secondary_container {
316 Some(self.on_secondary_container)
317 } else if background == self.tertiary {
318 Some(self.on_tertiary)
319 } else if background == self.tertiary_container {
320 Some(self.on_tertiary_container)
321 } else if background == self.background {
322 Some(self.on_background)
323 } else if background == self.surface
324 || background == self.surface_bright
325 || background == self.surface_dim
326 || background == self.surface_container
327 || background == self.surface_container_low
328 || background == self.surface_container_high
329 || background == self.surface_container_highest
330 {
331 Some(self.on_surface)
332 } else if background == self.inverse_surface {
333 Some(self.inverse_on_surface)
334 } else if background == self.error {
335 Some(self.on_error)
336 } else if background == self.error_container {
337 Some(self.on_error_container)
338 } else {
339 None
340 }
341 }
342}
343
344impl Default for ColorScheme {
345 fn default() -> Self {
346 Self::dark()
347 }
348}
349
350#[derive(Clone, Copy, Debug)]
352#[must_use]
353pub struct Typography {
354 pub display_large: Sp,
355 pub display_medium: Sp,
356 pub display_small: Sp,
357 pub headline_large: Sp,
358 pub headline_medium: Sp,
359 pub headline_small: Sp,
360 pub title_large: Sp,
361 pub title_medium: Sp,
362 pub title_small: Sp,
363 pub body_large: Sp,
364 pub body_medium: Sp,
365 pub body_small: Sp,
366 pub label_large: Sp,
367 pub label_medium: Sp,
368 pub label_small: Sp,
369}
370
371impl Default for Typography {
372 fn default() -> Self {
373 Self {
374 display_large: Sp(57.0),
375 display_medium: Sp(45.0),
376 display_small: Sp(36.0),
377 headline_large: Sp(32.0),
378 headline_medium: Sp(28.0),
379 headline_small: Sp(24.0),
380 title_large: Sp(22.0),
381 title_medium: Sp(16.0),
382 title_small: Sp(14.0),
383 body_large: Sp(16.0),
384 body_medium: Sp(14.0),
385 body_small: Sp(12.0),
386 label_large: Sp(14.0),
387 label_medium: Sp(12.0),
388 label_small: Sp(11.0),
389 }
390 }
391}
392
393#[derive(Clone, Copy, Debug)]
395#[must_use]
396pub struct Shapes {
397 pub extra_small: Dp,
398 pub small: Dp,
399 pub medium: Dp,
400 pub large: Dp,
401 pub extra_large: Dp,
402}
403
404impl Default for Shapes {
405 fn default() -> Self {
406 Self {
407 extra_small: Dp(4.0),
408 small: Dp(8.0),
409 medium: Dp(12.0),
410 large: Dp(16.0),
411 extra_large: Dp(28.0),
412 }
413 }
414}
415
416#[derive(Clone, Copy, Debug)]
418#[must_use]
419pub struct Spacing {
420 pub xs: Dp,
421 pub sm: Dp,
422 pub md: Dp,
423 pub lg: Dp,
424 pub xl: Dp,
425 pub xxl: Dp,
426}
427
428impl Default for Spacing {
429 fn default() -> Self {
430 Self {
431 xs: Dp(4.0),
432 sm: Dp(8.0),
433 md: Dp(12.0),
434 lg: Dp(16.0),
435 xl: Dp(24.0),
436 xxl: Dp(32.0),
437 }
438 }
439}
440
441#[derive(Clone, Copy, Debug)]
443#[must_use]
444pub struct Elevation {
445 pub level0: Dp,
446 pub level1: Dp,
447 pub level2: Dp,
448 pub level3: Dp,
449 pub level4: Dp,
450 pub level5: Dp,
451}
452
453impl Default for Elevation {
454 fn default() -> Self {
455 Self {
456 level0: Dp(0.0),
457 level1: Dp(1.0),
458 level2: Dp(3.0),
459 level3: Dp(6.0),
460 level4: Dp(8.0),
461 level5: Dp(12.0),
462 }
463 }
464}
465
466#[derive(Clone, Copy, Debug)]
468#[must_use]
469pub struct MotionScheme {
470 pub shape: AnimationSpec,
473 pub color: AnimationSpec,
476 pub color_fast: AnimationSpec,
479 pub overlay: AnimationSpec,
482 pub spring: AnimationSpec,
485 pub expand: AnimationSpec,
488 pub layout: AnimationSpec,
491}
492
493impl Default for MotionScheme {
494 fn default() -> Self {
495 Self {
496 shape: AnimationSpec::tween(Duration::from_millis(200), Easing::FastOutSlowIn),
497 color: AnimationSpec::tween(Duration::from_millis(150), Easing::FastOutSlowIn),
498 color_fast: AnimationSpec::tween(Duration::from_millis(100), Easing::FastOutSlowIn),
499 overlay: AnimationSpec::tween(Duration::from_millis(120), Easing::FastOutSlowIn),
500 spring: AnimationSpec::spring_gentle(),
501 expand: AnimationSpec::tween(Duration::from_millis(250), Easing::FastOutSlowIn),
502 layout: AnimationSpec::tween(Duration::from_millis(300), Easing::EaseOut),
503 }
504 }
505}
506
507#[derive(Clone, Copy, Debug)]
508#[must_use]
509pub struct Theme {
510 pub colors: ColorScheme,
511 pub typography: Typography,
512 pub shapes: Shapes,
513 pub spacing: Spacing,
514 pub elevation: Elevation,
515 pub motion: MotionScheme,
516
517 pub focus: Color,
518 pub scrollbar_track: Color,
519 pub scrollbar_thumb: Color,
520 pub button_bg: Color,
521 pub button_bg_hover: Color,
522 pub button_bg_pressed: Color,
523}
524
525impl Deref for Theme {
526 type Target = ColorScheme;
527 fn deref(&self) -> &Self::Target {
528 &self.colors
529 }
530}
531
532impl Default for Theme {
533 fn default() -> Self {
534 let colors = ColorScheme::default();
535 Self {
536 colors,
537 typography: Typography::default(),
538 shapes: Shapes::default(),
539 spacing: Spacing::default(),
540 elevation: Elevation::default(),
541 motion: MotionScheme::default(),
542 focus: colors.focus,
543 scrollbar_track: Color::TRANSPARENT,
544 scrollbar_thumb: colors.outline.with_alpha(179),
545 button_bg: colors.primary,
546 button_bg_hover: colors.primary_container,
547 button_bg_pressed: colors.secondary_container,
548 }
549 }
550}
551
552impl Theme {
553 pub fn with_colors(mut self, colors: ColorScheme) -> Self {
554 self.colors = colors;
555 self
556 }
557
558 pub fn dark() -> Self {
560 Self::default().with_colors(ColorScheme::dark())
561 }
562
563 pub fn light() -> Self {
566 let colors = ColorScheme::light();
567 Self {
568 focus: colors.focus,
569 scrollbar_thumb: colors.outline.with_alpha(179),
570 button_bg: colors.primary,
571 button_bg_hover: colors.primary_container,
572 button_bg_pressed: colors.secondary_container,
573 colors,
574 ..Self::default()
575 }
576 }
577
578 pub fn is_dark(&self) -> bool {
581 self.colors.background.is_dark()
582 }
583}
584
585#[derive(Clone, Copy, Debug)]
587pub struct Density {
588 pub scale: f32,
589}
590impl Default for Density {
591 fn default() -> Self {
592 Self { scale: 1.0 }
593 }
594}
595
596#[derive(Clone, Copy, Debug)]
598pub struct UiScale(pub f32);
599impl Default for UiScale {
600 fn default() -> Self {
601 Self(1.0)
602 }
603}
604
605#[derive(Clone, Copy, Debug)]
606pub struct TextScale(pub f32);
607impl Default for TextScale {
608 fn default() -> Self {
609 Self(1.0)
610 }
611}
612
613pub fn with_theme<R>(theme: Theme, f: impl FnOnce() -> R) -> R {
614 with_locals_frame(|| {
615 set_local_boxed(TypeId::of::<Theme>(), Box::new(theme));
616 f()
617 })
618}
619
620pub fn with_density<R>(density: Density, f: impl FnOnce() -> R) -> R {
621 with_locals_frame(|| {
622 set_local_boxed(TypeId::of::<Density>(), Box::new(density));
623 f()
624 })
625}
626
627pub fn with_ui_scale<R>(s: UiScale, f: impl FnOnce() -> R) -> R {
628 with_locals_frame(|| {
629 set_local_boxed(TypeId::of::<UiScale>(), Box::new(s));
630 f()
631 })
632}
633
634pub fn with_text_scale<R>(ts: TextScale, f: impl FnOnce() -> R) -> R {
635 with_locals_frame(|| {
636 set_local_boxed(TypeId::of::<TextScale>(), Box::new(ts));
637 f()
638 })
639}
640
641pub fn with_text_direction<R>(dir: TextDirection, f: impl FnOnce() -> R) -> R {
642 with_locals_frame(|| {
643 set_local_boxed(TypeId::of::<TextDirection>(), Box::new(dir));
644 f()
645 })
646}
647
648pub fn with_window_insets<R>(insets: WindowInsets, f: impl FnOnce() -> R) -> R {
649 with_locals_frame(|| {
650 set_local_boxed(TypeId::of::<WindowInsets>(), Box::new(insets));
651 f()
652 })
653}
654
655#[derive(Clone, Copy, Debug)]
656pub struct ContentColor(pub Color);
657
658pub fn with_content_color<R>(color: Color, f: impl FnOnce() -> R) -> R {
659 with_locals_frame(|| {
660 set_local_boxed(TypeId::of::<ContentColor>(), Box::new(ContentColor(color)));
661 f()
662 })
663}
664
665pub fn content_color() -> Color {
666 get_local::<ContentColor>()
667 .map(|c| c.0)
668 .unwrap_or_else(|| theme().on_surface)
669}
670
671pub fn content_color_for(background: Color) -> Color {
675 if let Some(c) = theme().colors.content_color_for(background) {
676 return c;
677 }
678 let local = content_color();
679 if (background.relative_luminance() - local.relative_luminance()).abs() < 0.25 {
681 if background.is_dark() {
682 Color::WHITE
683 } else {
684 Color::BLACK
685 }
686 } else {
687 local
688 }
689}
690
691#[derive(Clone, Copy, Debug)]
694pub struct TextSize(pub Sp);
695
696pub fn with_text_size<R>(size: Sp, f: impl FnOnce() -> R) -> R {
697 with_locals_frame(|| {
698 set_local_boxed(TypeId::of::<TextSize>(), Box::new(TextSize(size)));
699 f()
700 })
701}
702
703pub fn text_size() -> Option<Sp> {
704 get_local::<TextSize>().map(|t| t.0)
705}
706
707#[derive(Clone, Debug, Default)]
711pub struct LocalIndication(pub Option<Rc<dyn IndicationNodeFactory>>);
712
713pub fn with_local_indication<R>(
714 indication: Option<Rc<dyn IndicationNodeFactory>>,
715 f: impl FnOnce() -> R,
716) -> R {
717 with_locals_frame(|| {
718 set_local_boxed(
719 std::any::TypeId::of::<LocalIndication>(),
720 Box::new(LocalIndication(indication)),
721 );
722 f()
723 })
724}
725
726#[derive(Clone, Copy, Debug)]
729struct LocalInputMode(pub crate::input::InputMode);
730
731pub fn with_input_mode<R>(mode: crate::input::InputMode, f: impl FnOnce() -> R) -> R {
733 with_locals_frame(|| {
734 set_local_boxed(
735 TypeId::of::<LocalInputMode>(),
736 Box::new(LocalInputMode(mode)),
737 );
738 f()
739 })
740}
741
742pub(crate) fn local_input_mode() -> Option<crate::input::InputMode> {
744 get_local::<LocalInputMode>().map(|m| m.0)
745}
746
747pub fn local_indication() -> Option<Rc<dyn IndicationNodeFactory>> {
748 LOCALS_STACK.with(|st| {
751 for frame in st.borrow().iter().rev() {
752 if let Some(v) = frame.get(&TypeId::of::<LocalIndication>())
753 && let Some(li) = v.downcast_ref::<LocalIndication>()
754 {
755 return li.0.clone();
756 }
757 }
758 None::<Rc<dyn IndicationNodeFactory>>
759 })
760}
761
762#[derive(Clone, Copy, Debug, Default, PartialEq)]
764pub struct WindowInsets {
765 pub top: f32,
766 pub bottom: f32,
767 pub left: f32,
768 pub right: f32,
769 pub ime_bottom: f32,
772}
773
774pub fn set_window_insets_default(insets: WindowInsets) {
776 defaults().write().window_insets = insets;
777 set_local_boxed(TypeId::of::<WindowInsets>(), Box::new(insets));
778}
779
780pub fn set_ime_inset(height_px: f32) {
783 let mut insets = defaults().write().window_insets;
784 insets.ime_bottom = height_px;
785 set_local_boxed(TypeId::of::<WindowInsets>(), Box::new(insets));
787}
788
789pub fn window_insets() -> WindowInsets {
791 get_local::<WindowInsets>().unwrap_or_else(|| defaults().read().window_insets)
792}
793
794pub fn set_window_container_size(width_dp: f32, height_dp: f32) {
797 let mut d = defaults().write();
798 d.container_width = width_dp;
799 d.container_height = height_dp;
800}
801
802pub fn set_window_container_width(w_dp: f32) {
805 defaults().write().container_width = w_dp;
806}
807
808pub fn set_window_container_height(h_dp: f32) {
811 defaults().write().container_height = h_dp;
812}
813
814pub fn get_window_container_width() -> f32 {
816 defaults().read().container_width
817}
818
819pub fn get_window_container_height() -> f32 {
821 defaults().read().container_height
822}
823
824#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
832pub enum WidthClass {
833 #[default]
834 Compact,
835 Medium,
836 Expanded,
837}
838
839#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
847pub enum HeightClass {
848 #[default]
849 Compact,
850 Medium,
851 Expanded,
852}
853
854#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
860pub struct WindowSizeClass {
861 pub width: WidthClass,
862 pub height: HeightClass,
863}
864
865impl WindowSizeClass {
866 pub fn is_expanded_width(&self) -> bool {
868 matches!(self.width, WidthClass::Expanded)
869 }
870 pub fn is_at_least_medium_width(&self) -> bool {
873 matches!(self.width, WidthClass::Medium | WidthClass::Expanded)
874 }
875}
876
877pub fn calculate_window_size_class(
880 width_px: u32,
881 height_px: u32,
882 density_scale: f32,
883) -> WindowSizeClass {
884 let density = density_scale.max(0.0001);
885 let width_dp = (width_px as f32) / density;
886 let height_dp = (height_px as f32) / density;
887
888 let width = if width_dp < 600.0 {
889 WidthClass::Compact
890 } else if width_dp < 840.0 {
891 WidthClass::Medium
892 } else {
893 WidthClass::Expanded
894 };
895 let height = if height_dp < 480.0 {
896 HeightClass::Compact
897 } else if height_dp < 900.0 {
898 HeightClass::Medium
899 } else {
900 HeightClass::Expanded
901 };
902
903 WindowSizeClass { width, height }
904}
905
906pub fn set_window_size_class_default(class: WindowSizeClass) {
909 defaults().write().window_size_class = class;
910}
911
912pub fn with_window_size_class<R>(class: WindowSizeClass, f: impl FnOnce() -> R) -> R {
914 with_locals_frame(|| {
915 set_local_boxed(TypeId::of::<WindowSizeClass>(), Box::new(class));
916 f()
917 })
918}
919
920pub fn window_size_class() -> WindowSizeClass {
923 get_local::<WindowSizeClass>().unwrap_or_else(|| defaults().read().window_size_class)
924}
925
926macro_rules! def_local_getter {
927 ($fn_name:ident, $ty:ty, $default_field:ident) => {
928 pub fn $fn_name() -> $ty {
929 get_local::<$ty>().unwrap_or_else(|| defaults().read().$default_field)
930 }
931 };
932}
933
934def_local_getter!(theme, Theme, theme);
935def_local_getter!(density, Density, density);
936def_local_getter!(ui_scale, UiScale, ui_scale);
937def_local_getter!(text_scale, TextScale, text_scale);
938def_local_getter!(text_direction, TextDirection, text_direction);
939
940#[cfg(test)]
941mod tests {
942 use super::*;
943
944 #[test]
945 fn width_class_thresholds_match_m3() {
946 assert_eq!(
948 calculate_window_size_class(100, 100, 1.0).width,
949 WidthClass::Compact
950 );
951 assert_eq!(
952 calculate_window_size_class(599, 100, 1.0).width,
953 WidthClass::Compact
954 );
955 assert_eq!(
956 calculate_window_size_class(600, 100, 1.0).width,
957 WidthClass::Medium
958 );
959 assert_eq!(
960 calculate_window_size_class(839, 100, 1.0).width,
961 WidthClass::Medium
962 );
963 assert_eq!(
964 calculate_window_size_class(840, 100, 1.0).width,
965 WidthClass::Expanded
966 );
967 assert_eq!(
968 calculate_window_size_class(2000, 100, 1.0).width,
969 WidthClass::Expanded
970 );
971 }
972
973 #[test]
974 fn height_class_thresholds_match_m3() {
975 assert_eq!(
976 calculate_window_size_class(100, 100, 1.0).height,
977 HeightClass::Compact
978 );
979 assert_eq!(
980 calculate_window_size_class(100, 479, 1.0).height,
981 HeightClass::Compact
982 );
983 assert_eq!(
984 calculate_window_size_class(100, 480, 1.0).height,
985 HeightClass::Medium
986 );
987 assert_eq!(
988 calculate_window_size_class(100, 899, 1.0).height,
989 HeightClass::Medium
990 );
991 assert_eq!(
992 calculate_window_size_class(100, 900, 1.0).height,
993 HeightClass::Expanded
994 );
995 }
996
997 #[test]
998 fn density_scales_thresholds() {
999 let c = calculate_window_size_class(1199, 100, 2.0);
1001 assert_eq!(c.width, WidthClass::Compact);
1002 let c = calculate_window_size_class(1200, 100, 2.0);
1003 assert_eq!(c.width, WidthClass::Medium);
1004 }
1005
1006 #[test]
1007 fn is_at_least_medium_width() {
1008 let c = WindowSizeClass {
1009 width: WidthClass::Compact,
1010 height: HeightClass::Compact,
1011 };
1012 assert!(!c.is_at_least_medium_width());
1013 let c = WindowSizeClass {
1014 width: WidthClass::Medium,
1015 height: HeightClass::Compact,
1016 };
1017 assert!(c.is_at_least_medium_width());
1018 let c = WindowSizeClass {
1019 width: WidthClass::Expanded,
1020 height: HeightClass::Compact,
1021 };
1022 assert!(c.is_at_least_medium_width());
1023 }
1024}