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