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