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
95#[derive(Clone, Copy, Debug, PartialEq)]
97pub struct Dp(pub f32);
98
99impl Dp {
100 pub fn to_px(self) -> f32 {
102 self.0 * effective_density_scale()
103 }
104}
105
106#[inline]
108pub fn effective_density_scale() -> f32 {
109 (density().scale * ui_scale().0).max(0.0001)
110}
111
112pub fn dp_to_px(dp: f32) -> f32 {
114 Dp(dp).to_px()
115}
116
117pub fn px_to_dp(px: f32) -> f32 {
119 let scale = effective_density_scale();
120 if scale <= 0.0001 { 0.0 } else { px / scale }
121}
122
123fn with_locals_frame<R>(f: impl FnOnce() -> R) -> R {
124 struct Guard;
125 impl Drop for Guard {
126 fn drop(&mut self) {
127 let _ = LOCALS_STACK.try_with(|st| {
128 st.borrow_mut().pop();
129 });
130 }
131 }
132 LOCALS_STACK.with(|st| st.borrow_mut().push(HashMap::new()));
133 let _guard = Guard;
134 f()
135}
136
137fn set_local_boxed(t: TypeId, v: Box<dyn Any>) {
138 LOCALS_STACK.with(|st| {
139 if let Some(top) = st.borrow_mut().last_mut() {
140 top.insert(t, v);
141 } else {
142 let mut m = HashMap::new();
144 m.insert(t, v);
145 st.borrow_mut().push(m);
146 }
147 });
148}
149
150fn get_local<T: 'static + Copy>() -> Option<T> {
151 LOCALS_STACK.with(|st| {
152 for frame in st.borrow().iter().rev() {
153 if let Some(v) = frame.get(&TypeId::of::<T>())
154 && let Some(t) = v.downcast_ref::<T>()
155 {
156 return Some(*t);
157 }
158 }
159 None
160 })
161}
162
163#[derive(Clone, Copy, Debug)]
164#[must_use]
165pub struct ColorScheme {
166 pub primary: Color,
167 pub on_primary: Color,
168 pub primary_container: Color,
169 pub on_primary_container: Color,
170
171 pub secondary: Color,
172 pub on_secondary: Color,
173 pub secondary_container: Color,
174 pub on_secondary_container: Color,
175
176 pub tertiary: Color,
177 pub on_tertiary: Color,
178 pub tertiary_container: Color,
179 pub on_tertiary_container: Color,
180
181 pub error: Color,
182 pub on_error: Color,
183 pub error_container: Color,
184 pub on_error_container: Color,
185
186 pub background: Color,
187 pub on_background: Color,
188 pub surface: Color,
189 pub on_surface: Color,
190 pub surface_variant: Color,
191 pub on_surface_variant: Color,
192 pub surface_container_lowest: Color,
193 pub surface_container_low: Color,
194 pub surface_container: Color,
195 pub surface_container_high: Color,
196 pub surface_container_highest: Color,
197 pub surface_bright: Color,
198 pub surface_dim: Color,
199 pub surface_tint: Color,
200
201 pub inverse_surface: Color,
202 pub inverse_on_surface: Color,
203 pub inverse_primary: Color,
204
205 pub outline: Color,
206 pub outline_variant: Color,
207
208 pub scrim: Color,
209 pub shadow: Color,
210 pub focus: Color,
211}
212
213impl ColorScheme {
214 pub fn dark() -> Self {
215 Self {
216 primary: Color::from_hex("#69FDBE"),
217 on_primary: Color::from_hex("#003020"),
218 primary_container: Color::from_hex("#004D40"),
219 on_primary_container: Color::from_hex("#6FF7F6"),
220
221 secondary: Color::from_hex("#B3C9A7"),
222 on_secondary: Color::from_hex("#1C3519"),
223 secondary_container: Color::from_hex("#334D2E"),
224 on_secondary_container: Color::from_hex("#CCE8B3"),
225
226 tertiary: Color::from_hex("#FFC9C1"),
227 on_tertiary: Color::from_hex("#3F1619"),
228 tertiary_container: Color::from_hex("#5D1F22"),
229 on_tertiary_container: Color::from_hex("#FFDBD8"),
230
231 error: Color::from_hex("#F2B8B5"),
232 on_error: Color::from_hex("#601410"),
233 error_container: Color::from_hex("#8C1D18"),
234 on_error_container: Color::from_hex("#F9DEDC"),
235
236 background: Color::from_hex("#1A1C1E"),
237 on_background: Color::from_hex("#E6E1E5"),
238 surface: Color::from_hex("#1A1C1E"),
239 on_surface: Color::from_hex("#E6E1E5"),
240 surface_variant: Color::from_hex("#44474E"),
241 on_surface_variant: Color::from_hex("#C4C6CE"),
242 surface_container_lowest: Color::from_hex("#0A0A0C"),
243 surface_container_low: Color::from_hex("#141115"),
244 surface_container: Color::from_hex("#19131A"),
245 surface_container_high: Color::from_hex("#1F1B22"),
246 surface_container_highest: Color::from_hex("#2A2930"),
247 surface_bright: Color::from_hex("#26292F"),
248 surface_dim: Color::from_hex("#1A1C1E"),
249 surface_tint: Color::from_hex("#69FDBE"),
250
251 inverse_surface: Color::from_hex("#E6E1E5"),
252 inverse_on_surface: Color::from_hex("#2A2930"),
253 inverse_primary: Color::from_hex("#005048"),
254
255 outline: Color::from_hex("#74777F"),
256 outline_variant: Color::from_hex("#44474E"),
257
258 scrim: Color::from_hex("#000000"),
259 shadow: Color::from_hex("#000000"),
260 focus: Color::from_hex("#006A6A"),
261 }
262 }
263
264 pub fn light() -> Self {
265 Self {
266 primary: Color::from_hex("#006A6A"),
267 on_primary: Color::WHITE,
268 primary_container: Color::from_hex("#9EF0EC"),
269 on_primary_container: Color::from_hex("#002020"),
270
271 secondary: Color::from_hex("#586146"),
272 on_secondary: Color::WHITE,
273 secondary_container: Color::from_hex("#D8E3B8"),
274 on_secondary_container: Color::from_hex("#161C0A"),
275
276 tertiary: Color::from_hex("#744639"),
277 on_tertiary: Color::WHITE,
278 tertiary_container: Color::from_hex("#FFD9CD"),
279 on_tertiary_container: Color::from_hex("#2C0E07"),
280
281 error: Color::from_hex("#BA1A1A"),
282 on_error: Color::WHITE,
283 error_container: Color::from_hex("#FFDAD6"),
284 on_error_container: Color::from_hex("#410002"),
285
286 background: Color::from_hex("#FEF7FF"),
287 on_background: Color::from_hex("#1A1C1E"),
288 surface: Color::from_hex("#FEF7FF"),
289 on_surface: Color::from_hex("#1A1C1E"),
290 surface_variant: Color::from_hex("#E1E3DE"),
291 on_surface_variant: Color::from_hex("#44474E"),
292 surface_container_lowest: Color::WHITE,
293 surface_container_low: Color::from_hex("#F4F5F0"),
294 surface_container: Color::from_hex("#EEF0E9"),
295 surface_container_high: Color::from_hex("#E9EAE4"),
296 surface_container_highest: Color::from_hex("#E3E5DF"),
297 surface_bright: Color::from_hex("#FEF7FF"),
298 surface_dim: Color::from_hex("#DEDAD0"),
299 surface_tint: Color::from_hex("#006A6A"),
300
301 inverse_surface: Color::from_hex("#2F3033"),
302 inverse_on_surface: Color::from_hex("#F1F0F4"),
303 inverse_primary: Color::from_hex("#69FDBE"),
304
305 outline: Color::from_hex("#74777F"),
306 outline_variant: Color::from_hex("#C4C6CE"),
307
308 scrim: Color::from_hex("#000000"),
309 shadow: Color::from_hex("#000000"),
310 focus: Color::from_hex("#1D4ED8"),
311 }
312 }
313}
314
315impl Default for ColorScheme {
316 fn default() -> Self {
317 Self::dark()
318 }
319}
320
321#[derive(Clone, Copy, Debug)]
322#[must_use]
323pub struct Typography {
324 pub display_large: f32,
325 pub display_medium: f32,
326 pub display_small: f32,
327 pub headline_large: f32,
328 pub headline_medium: f32,
329 pub headline_small: f32,
330 pub title_large: f32,
331 pub title_medium: f32,
332 pub title_small: f32,
333 pub body_large: f32,
334 pub body_medium: f32,
335 pub body_small: f32,
336 pub label_large: f32,
337 pub label_medium: f32,
338 pub label_small: f32,
339}
340
341impl Default for Typography {
342 fn default() -> Self {
343 Self {
344 display_large: 57.0,
345 display_medium: 45.0,
346 display_small: 36.0,
347 headline_large: 32.0,
348 headline_medium: 28.0,
349 headline_small: 24.0,
350 title_large: 22.0,
351 title_medium: 16.0,
352 title_small: 14.0,
353 body_large: 16.0,
354 body_medium: 14.0,
355 body_small: 12.0,
356 label_large: 14.0,
357 label_medium: 12.0,
358 label_small: 11.0,
359 }
360 }
361}
362
363#[derive(Clone, Copy, Debug)]
364#[must_use]
365pub struct Shapes {
366 pub extra_small: f32,
367 pub small: f32,
368 pub medium: f32,
369 pub large: f32,
370 pub extra_large: f32,
371}
372
373impl Default for Shapes {
374 fn default() -> Self {
375 Self {
376 extra_small: 4.0,
377 small: 8.0,
378 medium: 12.0,
379 large: 16.0,
380 extra_large: 28.0,
381 }
382 }
383}
384
385#[derive(Clone, Copy, Debug)]
386#[must_use]
387pub struct Spacing {
388 pub xs: f32,
389 pub sm: f32,
390 pub md: f32,
391 pub lg: f32,
392 pub xl: f32,
393 pub xxl: f32,
394}
395
396impl Default for Spacing {
397 fn default() -> Self {
398 Self {
399 xs: 4.0,
400 sm: 8.0,
401 md: 12.0,
402 lg: 16.0,
403 xl: 24.0,
404 xxl: 32.0,
405 }
406 }
407}
408
409#[derive(Clone, Copy, Debug)]
410#[must_use]
411pub struct Elevation {
412 pub level0: f32,
413 pub level1: f32,
414 pub level2: f32,
415 pub level3: f32,
416 pub level4: f32,
417 pub level5: f32,
418}
419
420impl Default for Elevation {
421 fn default() -> Self {
422 Self {
423 level0: 0.0,
424 level1: 1.0,
425 level2: 3.0,
426 level3: 6.0,
427 level4: 8.0,
428 level5: 12.0,
429 }
430 }
431}
432
433#[derive(Clone, Copy, Debug)]
435#[must_use]
436pub struct MotionScheme {
437 pub shape: AnimationSpec,
440 pub color: AnimationSpec,
443 pub color_fast: AnimationSpec,
446 pub overlay: AnimationSpec,
449 pub spring: AnimationSpec,
452 pub expand: AnimationSpec,
455 pub layout: AnimationSpec,
458}
459
460impl Default for MotionScheme {
461 fn default() -> Self {
462 Self {
463 shape: AnimationSpec::tween(Duration::from_millis(200), Easing::FastOutSlowIn),
464 color: AnimationSpec::tween(Duration::from_millis(150), Easing::FastOutSlowIn),
465 color_fast: AnimationSpec::tween(Duration::from_millis(100), Easing::FastOutSlowIn),
466 overlay: AnimationSpec::tween(Duration::from_millis(120), Easing::FastOutSlowIn),
467 spring: AnimationSpec::spring_gentle(),
468 expand: AnimationSpec::tween(Duration::from_millis(250), Easing::FastOutSlowIn),
469 layout: AnimationSpec::tween(Duration::from_millis(300), Easing::EaseOut),
470 }
471 }
472}
473
474#[derive(Clone, Copy, Debug)]
475#[must_use]
476pub struct Theme {
477 pub colors: ColorScheme,
478 pub typography: Typography,
479 pub shapes: Shapes,
480 pub spacing: Spacing,
481 pub elevation: Elevation,
482 pub motion: MotionScheme,
483
484 pub focus: Color,
485 pub scrollbar_track: Color,
486 pub scrollbar_thumb: Color,
487 pub button_bg: Color,
488 pub button_bg_hover: Color,
489 pub button_bg_pressed: Color,
490}
491
492impl Deref for Theme {
493 type Target = ColorScheme;
494 fn deref(&self) -> &Self::Target {
495 &self.colors
496 }
497}
498
499impl Default for Theme {
500 fn default() -> Self {
501 let colors = ColorScheme::default();
502 Self {
503 colors,
504 typography: Typography::default(),
505 shapes: Shapes::default(),
506 spacing: Spacing::default(),
507 elevation: Elevation::default(),
508 motion: MotionScheme::default(),
509 focus: colors.focus,
510 scrollbar_track: Color::TRANSPARENT,
511 scrollbar_thumb: colors.outline.with_alpha(179),
512 button_bg: colors.primary,
513 button_bg_hover: colors.primary_container,
514 button_bg_pressed: colors.secondary_container,
515 }
516 }
517}
518
519impl Theme {
520 pub fn with_colors(mut self, colors: ColorScheme) -> Self {
521 self.colors = colors;
522 self
523 }
524
525 pub fn dark() -> Self {
527 Self::default().with_colors(ColorScheme::dark())
528 }
529
530 pub fn light() -> Self {
533 let colors = ColorScheme::light();
534 Self {
535 focus: colors.focus,
536 scrollbar_thumb: colors.outline.with_alpha(179),
537 button_bg: colors.primary,
538 button_bg_hover: colors.primary_container,
539 button_bg_pressed: colors.secondary_container,
540 colors,
541 ..Self::default()
542 }
543 }
544
545 pub fn is_dark(&self) -> bool {
548 self.colors.background.is_dark()
549 }
550}
551
552#[derive(Clone, Copy, Debug)]
554pub struct Density {
555 pub scale: f32,
556}
557impl Default for Density {
558 fn default() -> Self {
559 Self { scale: 1.0 }
560 }
561}
562
563#[derive(Clone, Copy, Debug)]
565pub struct UiScale(pub f32);
566impl Default for UiScale {
567 fn default() -> Self {
568 Self(1.0)
569 }
570}
571
572#[derive(Clone, Copy, Debug)]
573pub struct TextScale(pub f32);
574impl Default for TextScale {
575 fn default() -> Self {
576 Self(1.0)
577 }
578}
579
580pub fn with_theme<R>(theme: Theme, f: impl FnOnce() -> R) -> R {
581 with_locals_frame(|| {
582 set_local_boxed(TypeId::of::<Theme>(), Box::new(theme));
583 f()
584 })
585}
586
587pub fn with_density<R>(density: Density, f: impl FnOnce() -> R) -> R {
588 with_locals_frame(|| {
589 set_local_boxed(TypeId::of::<Density>(), Box::new(density));
590 f()
591 })
592}
593
594pub fn with_ui_scale<R>(s: UiScale, f: impl FnOnce() -> R) -> R {
595 with_locals_frame(|| {
596 set_local_boxed(TypeId::of::<UiScale>(), Box::new(s));
597 f()
598 })
599}
600
601pub fn with_text_scale<R>(ts: TextScale, f: impl FnOnce() -> R) -> R {
602 with_locals_frame(|| {
603 set_local_boxed(TypeId::of::<TextScale>(), Box::new(ts));
604 f()
605 })
606}
607
608pub fn with_text_direction<R>(dir: TextDirection, f: impl FnOnce() -> R) -> R {
609 with_locals_frame(|| {
610 set_local_boxed(TypeId::of::<TextDirection>(), Box::new(dir));
611 f()
612 })
613}
614
615pub fn with_window_insets<R>(insets: WindowInsets, f: impl FnOnce() -> R) -> R {
616 with_locals_frame(|| {
617 set_local_boxed(TypeId::of::<WindowInsets>(), Box::new(insets));
618 f()
619 })
620}
621
622#[derive(Clone, Copy, Debug)]
623pub struct ContentColor(pub Color);
624
625pub fn with_content_color<R>(color: Color, f: impl FnOnce() -> R) -> R {
626 with_locals_frame(|| {
627 set_local_boxed(TypeId::of::<ContentColor>(), Box::new(ContentColor(color)));
628 f()
629 })
630}
631
632pub fn content_color() -> Color {
633 get_local::<ContentColor>()
634 .map(|c| c.0)
635 .unwrap_or_else(|| theme().on_surface)
636}
637
638#[derive(Clone, Copy, Debug)]
641pub struct TextSize(pub f32);
642
643pub fn with_text_size<R>(size: f32, f: impl FnOnce() -> R) -> R {
644 with_locals_frame(|| {
645 set_local_boxed(TypeId::of::<TextSize>(), Box::new(TextSize(size)));
646 f()
647 })
648}
649
650pub fn text_size() -> Option<f32> {
651 get_local::<TextSize>().map(|t| t.0)
652}
653
654#[derive(Clone, Debug, Default)]
658pub struct LocalIndication(pub Option<Rc<dyn IndicationNodeFactory>>);
659
660pub fn with_local_indication<R>(
661 indication: Option<Rc<dyn IndicationNodeFactory>>,
662 f: impl FnOnce() -> R,
663) -> R {
664 with_locals_frame(|| {
665 set_local_boxed(
666 std::any::TypeId::of::<LocalIndication>(),
667 Box::new(LocalIndication(indication)),
668 );
669 f()
670 })
671}
672
673pub fn local_indication() -> Option<Rc<dyn IndicationNodeFactory>> {
674 LOCALS_STACK.with(|st| {
677 for frame in st.borrow().iter().rev() {
678 if let Some(v) = frame.get(&TypeId::of::<LocalIndication>())
679 && let Some(li) = v.downcast_ref::<LocalIndication>()
680 {
681 return li.0.clone();
682 }
683 }
684 None::<Rc<dyn IndicationNodeFactory>>
685 })
686}
687
688#[derive(Clone, Copy, Debug, Default, PartialEq)]
690pub struct WindowInsets {
691 pub top: f32,
692 pub bottom: f32,
693 pub left: f32,
694 pub right: f32,
695 pub ime_bottom: f32,
698}
699
700pub fn set_window_insets_default(insets: WindowInsets) {
702 defaults().write().window_insets = insets;
703 set_local_boxed(TypeId::of::<WindowInsets>(), Box::new(insets));
704}
705
706pub fn set_ime_inset(height_px: f32) {
709 let mut insets = defaults().write().window_insets;
710 insets.ime_bottom = height_px;
711 set_local_boxed(TypeId::of::<WindowInsets>(), Box::new(insets));
713}
714
715pub fn window_insets() -> WindowInsets {
717 get_local::<WindowInsets>().unwrap_or_else(|| defaults().read().window_insets)
718}
719
720pub fn set_window_container_size(width_dp: f32, height_dp: f32) {
723 let mut d = defaults().write();
724 d.container_width = width_dp;
725 d.container_height = height_dp;
726}
727
728pub fn set_window_container_width(w_dp: f32) {
731 defaults().write().container_width = w_dp;
732}
733
734pub fn set_window_container_height(h_dp: f32) {
737 defaults().write().container_height = h_dp;
738}
739
740pub fn get_window_container_width() -> f32 {
742 defaults().read().container_width
743}
744
745pub fn get_window_container_height() -> f32 {
747 defaults().read().container_height
748}
749
750#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
758pub enum WidthClass {
759 #[default]
760 Compact,
761 Medium,
762 Expanded,
763}
764
765#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
773pub enum HeightClass {
774 #[default]
775 Compact,
776 Medium,
777 Expanded,
778}
779
780#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
786pub struct WindowSizeClass {
787 pub width: WidthClass,
788 pub height: HeightClass,
789}
790
791impl WindowSizeClass {
792 pub fn is_expanded_width(&self) -> bool {
794 matches!(self.width, WidthClass::Expanded)
795 }
796 pub fn is_at_least_medium_width(&self) -> bool {
799 matches!(self.width, WidthClass::Medium | WidthClass::Expanded)
800 }
801}
802
803pub fn calculate_window_size_class(
806 width_px: u32,
807 height_px: u32,
808 density_scale: f32,
809) -> WindowSizeClass {
810 let density = density_scale.max(0.0001);
811 let width_dp = (width_px as f32) / density;
812 let height_dp = (height_px as f32) / density;
813
814 let width = if width_dp < 600.0 {
815 WidthClass::Compact
816 } else if width_dp < 840.0 {
817 WidthClass::Medium
818 } else {
819 WidthClass::Expanded
820 };
821 let height = if height_dp < 480.0 {
822 HeightClass::Compact
823 } else if height_dp < 900.0 {
824 HeightClass::Medium
825 } else {
826 HeightClass::Expanded
827 };
828
829 WindowSizeClass { width, height }
830}
831
832pub fn set_window_size_class_default(class: WindowSizeClass) {
835 defaults().write().window_size_class = class;
836}
837
838pub fn with_window_size_class<R>(class: WindowSizeClass, f: impl FnOnce() -> R) -> R {
840 with_locals_frame(|| {
841 set_local_boxed(TypeId::of::<WindowSizeClass>(), Box::new(class));
842 f()
843 })
844}
845
846pub fn window_size_class() -> WindowSizeClass {
849 get_local::<WindowSizeClass>().unwrap_or_else(|| defaults().read().window_size_class)
850}
851
852macro_rules! def_local_getter {
853 ($fn_name:ident, $ty:ty, $default_field:ident) => {
854 pub fn $fn_name() -> $ty {
855 get_local::<$ty>().unwrap_or_else(|| defaults().read().$default_field)
856 }
857 };
858}
859
860def_local_getter!(theme, Theme, theme);
861def_local_getter!(density, Density, density);
862def_local_getter!(ui_scale, UiScale, ui_scale);
863def_local_getter!(text_scale, TextScale, text_scale);
864def_local_getter!(text_direction, TextDirection, text_direction);
865
866#[cfg(test)]
867mod tests {
868 use super::*;
869
870 #[test]
871 fn width_class_thresholds_match_m3() {
872 assert_eq!(
874 calculate_window_size_class(100, 100, 1.0).width,
875 WidthClass::Compact
876 );
877 assert_eq!(
878 calculate_window_size_class(599, 100, 1.0).width,
879 WidthClass::Compact
880 );
881 assert_eq!(
882 calculate_window_size_class(600, 100, 1.0).width,
883 WidthClass::Medium
884 );
885 assert_eq!(
886 calculate_window_size_class(839, 100, 1.0).width,
887 WidthClass::Medium
888 );
889 assert_eq!(
890 calculate_window_size_class(840, 100, 1.0).width,
891 WidthClass::Expanded
892 );
893 assert_eq!(
894 calculate_window_size_class(2000, 100, 1.0).width,
895 WidthClass::Expanded
896 );
897 }
898
899 #[test]
900 fn height_class_thresholds_match_m3() {
901 assert_eq!(
902 calculate_window_size_class(100, 100, 1.0).height,
903 HeightClass::Compact
904 );
905 assert_eq!(
906 calculate_window_size_class(100, 479, 1.0).height,
907 HeightClass::Compact
908 );
909 assert_eq!(
910 calculate_window_size_class(100, 480, 1.0).height,
911 HeightClass::Medium
912 );
913 assert_eq!(
914 calculate_window_size_class(100, 899, 1.0).height,
915 HeightClass::Medium
916 );
917 assert_eq!(
918 calculate_window_size_class(100, 900, 1.0).height,
919 HeightClass::Expanded
920 );
921 }
922
923 #[test]
924 fn density_scales_thresholds() {
925 let c = calculate_window_size_class(1199, 100, 2.0);
927 assert_eq!(c.width, WidthClass::Compact);
928 let c = calculate_window_size_class(1200, 100, 2.0);
929 assert_eq!(c.width, WidthClass::Medium);
930 }
931
932 #[test]
933 fn is_at_least_medium_width() {
934 let c = WindowSizeClass {
935 width: WidthClass::Compact,
936 height: HeightClass::Compact,
937 };
938 assert!(!c.is_at_least_medium_width());
939 let c = WindowSizeClass {
940 width: WidthClass::Medium,
941 height: HeightClass::Compact,
942 };
943 assert!(c.is_at_least_medium_width());
944 let c = WindowSizeClass {
945 width: WidthClass::Expanded,
946 height: HeightClass::Compact,
947 };
948 assert!(c.is_at_least_medium_width());
949 }
950}