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 pub fn content_color_for(self, background: Color) -> Option<Color> {
317 if background == self.primary {
318 Some(self.on_primary)
319 } else if background == self.primary_container {
320 Some(self.on_primary_container)
321 } else if background == self.secondary {
322 Some(self.on_secondary)
323 } else if background == self.secondary_container {
324 Some(self.on_secondary_container)
325 } else if background == self.tertiary {
326 Some(self.on_tertiary)
327 } else if background == self.tertiary_container {
328 Some(self.on_tertiary_container)
329 } else if background == self.background {
330 Some(self.on_background)
331 } else if background == self.surface
332 || background == self.surface_bright
333 || background == self.surface_dim
334 || background == self.surface_container
335 || background == self.surface_container_low
336 || background == self.surface_container_high
337 || background == self.surface_container_highest
338 {
339 Some(self.on_surface)
340 } else if background == self.inverse_surface {
341 Some(self.inverse_on_surface)
342 } else if background == self.error {
343 Some(self.on_error)
344 } else if background == self.error_container {
345 Some(self.on_error_container)
346 } else {
347 None
348 }
349 }
350}
351
352impl Default for ColorScheme {
353 fn default() -> Self {
354 Self::dark()
355 }
356}
357
358#[derive(Clone, Copy, Debug)]
359#[must_use]
360pub struct Typography {
361 pub display_large: f32,
362 pub display_medium: f32,
363 pub display_small: f32,
364 pub headline_large: f32,
365 pub headline_medium: f32,
366 pub headline_small: f32,
367 pub title_large: f32,
368 pub title_medium: f32,
369 pub title_small: f32,
370 pub body_large: f32,
371 pub body_medium: f32,
372 pub body_small: f32,
373 pub label_large: f32,
374 pub label_medium: f32,
375 pub label_small: f32,
376}
377
378impl Default for Typography {
379 fn default() -> Self {
380 Self {
381 display_large: 57.0,
382 display_medium: 45.0,
383 display_small: 36.0,
384 headline_large: 32.0,
385 headline_medium: 28.0,
386 headline_small: 24.0,
387 title_large: 22.0,
388 title_medium: 16.0,
389 title_small: 14.0,
390 body_large: 16.0,
391 body_medium: 14.0,
392 body_small: 12.0,
393 label_large: 14.0,
394 label_medium: 12.0,
395 label_small: 11.0,
396 }
397 }
398}
399
400#[derive(Clone, Copy, Debug)]
401#[must_use]
402pub struct Shapes {
403 pub extra_small: f32,
404 pub small: f32,
405 pub medium: f32,
406 pub large: f32,
407 pub extra_large: f32,
408}
409
410impl Default for Shapes {
411 fn default() -> Self {
412 Self {
413 extra_small: 4.0,
414 small: 8.0,
415 medium: 12.0,
416 large: 16.0,
417 extra_large: 28.0,
418 }
419 }
420}
421
422#[derive(Clone, Copy, Debug)]
423#[must_use]
424pub struct Spacing {
425 pub xs: f32,
426 pub sm: f32,
427 pub md: f32,
428 pub lg: f32,
429 pub xl: f32,
430 pub xxl: f32,
431}
432
433impl Default for Spacing {
434 fn default() -> Self {
435 Self {
436 xs: 4.0,
437 sm: 8.0,
438 md: 12.0,
439 lg: 16.0,
440 xl: 24.0,
441 xxl: 32.0,
442 }
443 }
444}
445
446#[derive(Clone, Copy, Debug)]
447#[must_use]
448pub struct Elevation {
449 pub level0: f32,
450 pub level1: f32,
451 pub level2: f32,
452 pub level3: f32,
453 pub level4: f32,
454 pub level5: f32,
455}
456
457impl Default for Elevation {
458 fn default() -> Self {
459 Self {
460 level0: 0.0,
461 level1: 1.0,
462 level2: 3.0,
463 level3: 6.0,
464 level4: 8.0,
465 level5: 12.0,
466 }
467 }
468}
469
470#[derive(Clone, Copy, Debug)]
472#[must_use]
473pub struct MotionScheme {
474 pub shape: AnimationSpec,
477 pub color: AnimationSpec,
480 pub color_fast: AnimationSpec,
483 pub overlay: AnimationSpec,
486 pub spring: AnimationSpec,
489 pub expand: AnimationSpec,
492 pub layout: AnimationSpec,
495}
496
497impl Default for MotionScheme {
498 fn default() -> Self {
499 Self {
500 shape: AnimationSpec::tween(Duration::from_millis(200), Easing::FastOutSlowIn),
501 color: AnimationSpec::tween(Duration::from_millis(150), Easing::FastOutSlowIn),
502 color_fast: AnimationSpec::tween(Duration::from_millis(100), Easing::FastOutSlowIn),
503 overlay: AnimationSpec::tween(Duration::from_millis(120), Easing::FastOutSlowIn),
504 spring: AnimationSpec::spring_gentle(),
505 expand: AnimationSpec::tween(Duration::from_millis(250), Easing::FastOutSlowIn),
506 layout: AnimationSpec::tween(Duration::from_millis(300), Easing::EaseOut),
507 }
508 }
509}
510
511#[derive(Clone, Copy, Debug)]
512#[must_use]
513pub struct Theme {
514 pub colors: ColorScheme,
515 pub typography: Typography,
516 pub shapes: Shapes,
517 pub spacing: Spacing,
518 pub elevation: Elevation,
519 pub motion: MotionScheme,
520
521 pub focus: Color,
522 pub scrollbar_track: Color,
523 pub scrollbar_thumb: Color,
524 pub button_bg: Color,
525 pub button_bg_hover: Color,
526 pub button_bg_pressed: Color,
527}
528
529impl Deref for Theme {
530 type Target = ColorScheme;
531 fn deref(&self) -> &Self::Target {
532 &self.colors
533 }
534}
535
536impl Default for Theme {
537 fn default() -> Self {
538 let colors = ColorScheme::default();
539 Self {
540 colors,
541 typography: Typography::default(),
542 shapes: Shapes::default(),
543 spacing: Spacing::default(),
544 elevation: Elevation::default(),
545 motion: MotionScheme::default(),
546 focus: colors.focus,
547 scrollbar_track: Color::TRANSPARENT,
548 scrollbar_thumb: colors.outline.with_alpha(179),
549 button_bg: colors.primary,
550 button_bg_hover: colors.primary_container,
551 button_bg_pressed: colors.secondary_container,
552 }
553 }
554}
555
556impl Theme {
557 pub fn with_colors(mut self, colors: ColorScheme) -> Self {
558 self.colors = colors;
559 self
560 }
561
562 pub fn dark() -> Self {
564 Self::default().with_colors(ColorScheme::dark())
565 }
566
567 pub fn light() -> Self {
570 let colors = ColorScheme::light();
571 Self {
572 focus: colors.focus,
573 scrollbar_thumb: colors.outline.with_alpha(179),
574 button_bg: colors.primary,
575 button_bg_hover: colors.primary_container,
576 button_bg_pressed: colors.secondary_container,
577 colors,
578 ..Self::default()
579 }
580 }
581
582 pub fn is_dark(&self) -> bool {
585 self.colors.background.is_dark()
586 }
587}
588
589#[derive(Clone, Copy, Debug)]
591pub struct Density {
592 pub scale: f32,
593}
594impl Default for Density {
595 fn default() -> Self {
596 Self { scale: 1.0 }
597 }
598}
599
600#[derive(Clone, Copy, Debug)]
602pub struct UiScale(pub f32);
603impl Default for UiScale {
604 fn default() -> Self {
605 Self(1.0)
606 }
607}
608
609#[derive(Clone, Copy, Debug)]
610pub struct TextScale(pub f32);
611impl Default for TextScale {
612 fn default() -> Self {
613 Self(1.0)
614 }
615}
616
617pub fn with_theme<R>(theme: Theme, f: impl FnOnce() -> R) -> R {
618 with_locals_frame(|| {
619 set_local_boxed(TypeId::of::<Theme>(), Box::new(theme));
620 f()
621 })
622}
623
624pub fn with_density<R>(density: Density, f: impl FnOnce() -> R) -> R {
625 with_locals_frame(|| {
626 set_local_boxed(TypeId::of::<Density>(), Box::new(density));
627 f()
628 })
629}
630
631pub fn with_ui_scale<R>(s: UiScale, f: impl FnOnce() -> R) -> R {
632 with_locals_frame(|| {
633 set_local_boxed(TypeId::of::<UiScale>(), Box::new(s));
634 f()
635 })
636}
637
638pub fn with_text_scale<R>(ts: TextScale, f: impl FnOnce() -> R) -> R {
639 with_locals_frame(|| {
640 set_local_boxed(TypeId::of::<TextScale>(), Box::new(ts));
641 f()
642 })
643}
644
645pub fn with_text_direction<R>(dir: TextDirection, f: impl FnOnce() -> R) -> R {
646 with_locals_frame(|| {
647 set_local_boxed(TypeId::of::<TextDirection>(), Box::new(dir));
648 f()
649 })
650}
651
652pub fn with_window_insets<R>(insets: WindowInsets, f: impl FnOnce() -> R) -> R {
653 with_locals_frame(|| {
654 set_local_boxed(TypeId::of::<WindowInsets>(), Box::new(insets));
655 f()
656 })
657}
658
659#[derive(Clone, Copy, Debug)]
660pub struct ContentColor(pub Color);
661
662pub fn with_content_color<R>(color: Color, f: impl FnOnce() -> R) -> R {
663 with_locals_frame(|| {
664 set_local_boxed(TypeId::of::<ContentColor>(), Box::new(ContentColor(color)));
665 f()
666 })
667}
668
669pub fn content_color() -> Color {
670 get_local::<ContentColor>()
671 .map(|c| c.0)
672 .unwrap_or_else(|| theme().on_surface)
673}
674
675pub fn content_color_for(background: Color) -> Color {
679 if let Some(c) = theme().colors.content_color_for(background) {
680 return c;
681 }
682 let local = content_color();
683 if (background.relative_luminance() - local.relative_luminance()).abs() < 0.25 {
685 if background.is_dark() {
686 Color::WHITE
687 } else {
688 Color::BLACK
689 }
690 } else {
691 local
692 }
693}
694
695#[derive(Clone, Copy, Debug)]
698pub struct TextSize(pub f32);
699
700pub fn with_text_size<R>(size: f32, f: impl FnOnce() -> R) -> R {
701 with_locals_frame(|| {
702 set_local_boxed(TypeId::of::<TextSize>(), Box::new(TextSize(size)));
703 f()
704 })
705}
706
707pub fn text_size() -> Option<f32> {
708 get_local::<TextSize>().map(|t| t.0)
709}
710
711#[derive(Clone, Debug, Default)]
715pub struct LocalIndication(pub Option<Rc<dyn IndicationNodeFactory>>);
716
717pub fn with_local_indication<R>(
718 indication: Option<Rc<dyn IndicationNodeFactory>>,
719 f: impl FnOnce() -> R,
720) -> R {
721 with_locals_frame(|| {
722 set_local_boxed(
723 std::any::TypeId::of::<LocalIndication>(),
724 Box::new(LocalIndication(indication)),
725 );
726 f()
727 })
728}
729
730#[derive(Clone, Copy, Debug)]
733struct LocalInputMode(pub crate::input::InputMode);
734
735pub fn with_input_mode<R>(mode: crate::input::InputMode, f: impl FnOnce() -> R) -> R {
737 with_locals_frame(|| {
738 set_local_boxed(
739 TypeId::of::<LocalInputMode>(),
740 Box::new(LocalInputMode(mode)),
741 );
742 f()
743 })
744}
745
746pub(crate) fn local_input_mode() -> Option<crate::input::InputMode> {
748 get_local::<LocalInputMode>().map(|m| m.0)
749}
750
751pub fn local_indication() -> Option<Rc<dyn IndicationNodeFactory>> {
752 LOCALS_STACK.with(|st| {
755 for frame in st.borrow().iter().rev() {
756 if let Some(v) = frame.get(&TypeId::of::<LocalIndication>())
757 && let Some(li) = v.downcast_ref::<LocalIndication>()
758 {
759 return li.0.clone();
760 }
761 }
762 None::<Rc<dyn IndicationNodeFactory>>
763 })
764}
765
766#[derive(Clone, Copy, Debug, Default, PartialEq)]
768pub struct WindowInsets {
769 pub top: f32,
770 pub bottom: f32,
771 pub left: f32,
772 pub right: f32,
773 pub ime_bottom: f32,
776}
777
778pub fn set_window_insets_default(insets: WindowInsets) {
780 defaults().write().window_insets = insets;
781 set_local_boxed(TypeId::of::<WindowInsets>(), Box::new(insets));
782}
783
784pub fn set_ime_inset(height_px: f32) {
787 let mut insets = defaults().write().window_insets;
788 insets.ime_bottom = height_px;
789 set_local_boxed(TypeId::of::<WindowInsets>(), Box::new(insets));
791}
792
793pub fn window_insets() -> WindowInsets {
795 get_local::<WindowInsets>().unwrap_or_else(|| defaults().read().window_insets)
796}
797
798pub fn set_window_container_size(width_dp: f32, height_dp: f32) {
801 let mut d = defaults().write();
802 d.container_width = width_dp;
803 d.container_height = height_dp;
804}
805
806pub fn set_window_container_width(w_dp: f32) {
809 defaults().write().container_width = w_dp;
810}
811
812pub fn set_window_container_height(h_dp: f32) {
815 defaults().write().container_height = h_dp;
816}
817
818pub fn get_window_container_width() -> f32 {
820 defaults().read().container_width
821}
822
823pub fn get_window_container_height() -> f32 {
825 defaults().read().container_height
826}
827
828#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
836pub enum WidthClass {
837 #[default]
838 Compact,
839 Medium,
840 Expanded,
841}
842
843#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
851pub enum HeightClass {
852 #[default]
853 Compact,
854 Medium,
855 Expanded,
856}
857
858#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
864pub struct WindowSizeClass {
865 pub width: WidthClass,
866 pub height: HeightClass,
867}
868
869impl WindowSizeClass {
870 pub fn is_expanded_width(&self) -> bool {
872 matches!(self.width, WidthClass::Expanded)
873 }
874 pub fn is_at_least_medium_width(&self) -> bool {
877 matches!(self.width, WidthClass::Medium | WidthClass::Expanded)
878 }
879}
880
881pub fn calculate_window_size_class(
884 width_px: u32,
885 height_px: u32,
886 density_scale: f32,
887) -> WindowSizeClass {
888 let density = density_scale.max(0.0001);
889 let width_dp = (width_px as f32) / density;
890 let height_dp = (height_px as f32) / density;
891
892 let width = if width_dp < 600.0 {
893 WidthClass::Compact
894 } else if width_dp < 840.0 {
895 WidthClass::Medium
896 } else {
897 WidthClass::Expanded
898 };
899 let height = if height_dp < 480.0 {
900 HeightClass::Compact
901 } else if height_dp < 900.0 {
902 HeightClass::Medium
903 } else {
904 HeightClass::Expanded
905 };
906
907 WindowSizeClass { width, height }
908}
909
910pub fn set_window_size_class_default(class: WindowSizeClass) {
913 defaults().write().window_size_class = class;
914}
915
916pub fn with_window_size_class<R>(class: WindowSizeClass, f: impl FnOnce() -> R) -> R {
918 with_locals_frame(|| {
919 set_local_boxed(TypeId::of::<WindowSizeClass>(), Box::new(class));
920 f()
921 })
922}
923
924pub fn window_size_class() -> WindowSizeClass {
927 get_local::<WindowSizeClass>().unwrap_or_else(|| defaults().read().window_size_class)
928}
929
930macro_rules! def_local_getter {
931 ($fn_name:ident, $ty:ty, $default_field:ident) => {
932 pub fn $fn_name() -> $ty {
933 get_local::<$ty>().unwrap_or_else(|| defaults().read().$default_field)
934 }
935 };
936}
937
938def_local_getter!(theme, Theme, theme);
939def_local_getter!(density, Density, density);
940def_local_getter!(ui_scale, UiScale, ui_scale);
941def_local_getter!(text_scale, TextScale, text_scale);
942def_local_getter!(text_direction, TextDirection, text_direction);
943
944#[cfg(test)]
945mod tests {
946 use super::*;
947
948 #[test]
949 fn width_class_thresholds_match_m3() {
950 assert_eq!(
952 calculate_window_size_class(100, 100, 1.0).width,
953 WidthClass::Compact
954 );
955 assert_eq!(
956 calculate_window_size_class(599, 100, 1.0).width,
957 WidthClass::Compact
958 );
959 assert_eq!(
960 calculate_window_size_class(600, 100, 1.0).width,
961 WidthClass::Medium
962 );
963 assert_eq!(
964 calculate_window_size_class(839, 100, 1.0).width,
965 WidthClass::Medium
966 );
967 assert_eq!(
968 calculate_window_size_class(840, 100, 1.0).width,
969 WidthClass::Expanded
970 );
971 assert_eq!(
972 calculate_window_size_class(2000, 100, 1.0).width,
973 WidthClass::Expanded
974 );
975 }
976
977 #[test]
978 fn height_class_thresholds_match_m3() {
979 assert_eq!(
980 calculate_window_size_class(100, 100, 1.0).height,
981 HeightClass::Compact
982 );
983 assert_eq!(
984 calculate_window_size_class(100, 479, 1.0).height,
985 HeightClass::Compact
986 );
987 assert_eq!(
988 calculate_window_size_class(100, 480, 1.0).height,
989 HeightClass::Medium
990 );
991 assert_eq!(
992 calculate_window_size_class(100, 899, 1.0).height,
993 HeightClass::Medium
994 );
995 assert_eq!(
996 calculate_window_size_class(100, 900, 1.0).height,
997 HeightClass::Expanded
998 );
999 }
1000
1001 #[test]
1002 fn density_scales_thresholds() {
1003 let c = calculate_window_size_class(1199, 100, 2.0);
1005 assert_eq!(c.width, WidthClass::Compact);
1006 let c = calculate_window_size_class(1200, 100, 2.0);
1007 assert_eq!(c.width, WidthClass::Medium);
1008 }
1009
1010 #[test]
1011 fn is_at_least_medium_width() {
1012 let c = WindowSizeClass {
1013 width: WidthClass::Compact,
1014 height: HeightClass::Compact,
1015 };
1016 assert!(!c.is_at_least_medium_width());
1017 let c = WindowSizeClass {
1018 width: WidthClass::Medium,
1019 height: HeightClass::Compact,
1020 };
1021 assert!(c.is_at_least_medium_width());
1022 let c = WindowSizeClass {
1023 width: WidthClass::Expanded,
1024 height: HeightClass::Compact,
1025 };
1026 assert!(c.is_at_least_medium_width());
1027 }
1028}