Skip to main content

repose_core/
locals.rs

1//! # Theming and locals
2//!
3//! Repose uses thread‑local “composition locals” for global UI parameters:
4//!
5//! - `Theme` - colors for surfaces, text, controls, etc.
6//! - `Density` - dp→px device scale factor (platform sets this).
7//! - `UiScale` - app-controlled UI scale multiplier (defaults to 1.0).
8//! - `TextScale` - user text scaling (defaults to 1.0).
9//! - `TextDirection` - LTR or RTL (defaults to LTR).
10//!
11//! Locals can be overridden for a subtree with `with_*`. If no local is set,
12//! getters fall back to global defaults (which an app can set each frame).
13
14use 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 crate::Color;
24use crate::animation::{AnimationSpec, Easing};
25use web_time::Duration;
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
28pub enum TextDirection {
29    #[default]
30    Ltr,
31    Rtl,
32}
33
34thread_local! {
35    static LOCALS_STACK: RefCell<Vec<HashMap<TypeId, Box<dyn Any>>>> = RefCell::new(Vec::new());
36}
37
38#[derive(Clone, Copy, Debug, Default)]
39struct Defaults {
40    theme: Theme,
41    text_direction: TextDirection,
42    ui_scale: UiScale,
43    text_scale: TextScale,
44    density: Density,
45    window_insets: WindowInsets,
46    window_size_class: WindowSizeClass,
47}
48
49static DEFAULTS: OnceLock<RwLock<Defaults>> = OnceLock::new();
50
51fn defaults() -> &'static RwLock<Defaults> {
52    DEFAULTS.get_or_init(|| RwLock::new(Defaults::default()))
53}
54
55/// Set the global default theme used when no local Theme is active.
56pub fn set_theme_default(t: Theme) {
57    defaults().write().theme = t;
58}
59
60/// Set the global default text direction used when no local TextDirection is active.
61pub fn set_text_direction_default(d: TextDirection) {
62    defaults().write().text_direction = d;
63}
64
65/// Set the global default UI scale used when no local UiScale is active.
66pub fn set_ui_scale_default(s: UiScale) {
67    defaults().write().ui_scale = UiScale(s.0.max(0.0));
68}
69
70/// Set the global default text scale used when no local TextScale is active.
71pub fn set_text_scale_default(s: TextScale) {
72    defaults().write().text_scale = TextScale(s.0.max(0.0));
73}
74
75/// Set the global default device density (dp→px) used when no local Density is active.
76/// Platform runners should call this whenever the window scale factor changes.
77pub fn set_density_default(d: Density) {
78    defaults().write().density = Density {
79        scale: d.scale.max(0.0),
80    };
81}
82
83/// density‑independent pixels (dp)
84#[derive(Clone, Copy, Debug, PartialEq)]
85pub struct Dp(pub f32);
86
87impl Dp {
88    /// Converts this dp value into physical pixels using current Density * UiScale.
89    pub fn to_px(self) -> f32 {
90        self.0 * density().scale * ui_scale().0
91    }
92}
93
94/// Convenience: convert a raw dp scalar into px using current Density * UiScale.
95pub fn dp_to_px(dp: f32) -> f32 {
96    Dp(dp).to_px()
97}
98
99/// Convenience: convert a raw px scalar into dp using current Density * UiScale.
100pub fn px_to_dp(px: f32) -> f32 {
101    let scale = density().scale * ui_scale().0;
102    if scale <= 0.0 { 0.0 } else { px / scale }
103}
104
105fn with_locals_frame<R>(f: impl FnOnce() -> R) -> R {
106    struct Guard;
107    impl Drop for Guard {
108        fn drop(&mut self) {
109            LOCALS_STACK.with(|st| {
110                st.borrow_mut().pop();
111            });
112        }
113    }
114    LOCALS_STACK.with(|st| st.borrow_mut().push(HashMap::new()));
115    let _guard = Guard;
116    f()
117}
118
119fn set_local_boxed(t: TypeId, v: Box<dyn Any>) {
120    LOCALS_STACK.with(|st| {
121        if let Some(top) = st.borrow_mut().last_mut() {
122            top.insert(t, v);
123        } else {
124            // no frame: create a temporary one
125            let mut m = HashMap::new();
126            m.insert(t, v);
127            st.borrow_mut().push(m);
128        }
129    });
130}
131
132fn get_local<T: 'static + Copy>() -> Option<T> {
133    LOCALS_STACK.with(|st| {
134        for frame in st.borrow().iter().rev() {
135            if let Some(v) = frame.get(&TypeId::of::<T>())
136                && let Some(t) = v.downcast_ref::<T>()
137            {
138                return Some(*t);
139            }
140        }
141        None
142    })
143}
144
145#[derive(Clone, Copy, Debug)]
146#[must_use]
147pub struct ColorScheme {
148    pub primary: Color,
149    pub on_primary: Color,
150    pub primary_container: Color,
151    pub on_primary_container: Color,
152
153    pub secondary: Color,
154    pub on_secondary: Color,
155    pub secondary_container: Color,
156    pub on_secondary_container: Color,
157
158    pub tertiary: Color,
159    pub on_tertiary: Color,
160    pub tertiary_container: Color,
161    pub on_tertiary_container: Color,
162
163    pub error: Color,
164    pub on_error: Color,
165    pub error_container: Color,
166    pub on_error_container: Color,
167
168    pub background: Color,
169    pub on_background: Color,
170    pub surface: Color,
171    pub on_surface: Color,
172    pub surface_variant: Color,
173    pub on_surface_variant: Color,
174    pub surface_container_lowest: Color,
175    pub surface_container_low: Color,
176    pub surface_container: Color,
177    pub surface_container_high: Color,
178    pub surface_container_highest: Color,
179    pub surface_bright: Color,
180    pub surface_dim: Color,
181    pub surface_tint: Color,
182
183    pub inverse_surface: Color,
184    pub inverse_on_surface: Color,
185    pub inverse_primary: Color,
186
187    pub outline: Color,
188    pub outline_variant: Color,
189
190    pub scrim: Color,
191    pub shadow: Color,
192    pub focus: Color,
193}
194
195impl ColorScheme {
196    pub fn dark() -> Self {
197        Self {
198            primary: Color::from_hex("#69FDBE"),
199            on_primary: Color::from_hex("#003020"),
200            primary_container: Color::from_hex("#004D40"),
201            on_primary_container: Color::from_hex("#6FF7F6"),
202
203            secondary: Color::from_hex("#B3C9A7"),
204            on_secondary: Color::from_hex("#1C3519"),
205            secondary_container: Color::from_hex("#334D2E"),
206            on_secondary_container: Color::from_hex("#CCE8B3"),
207
208            tertiary: Color::from_hex("#FFC9C1"),
209            on_tertiary: Color::from_hex("#3F1619"),
210            tertiary_container: Color::from_hex("#5D1F22"),
211            on_tertiary_container: Color::from_hex("#FFDBD8"),
212
213            error: Color::from_hex("#F2B8B5"),
214            on_error: Color::from_hex("#601410"),
215            error_container: Color::from_hex("#8C1D18"),
216            on_error_container: Color::from_hex("#F9DEDC"),
217
218            background: Color::from_hex("#1A1C1E"),
219            on_background: Color::from_hex("#E6E1E5"),
220            surface: Color::from_hex("#1A1C1E"),
221            on_surface: Color::from_hex("#E6E1E5"),
222            surface_variant: Color::from_hex("#44474E"),
223            on_surface_variant: Color::from_hex("#C4C6CE"),
224            surface_container_lowest: Color::from_hex("#0A0A0C"),
225            surface_container_low: Color::from_hex("#141115"),
226            surface_container: Color::from_hex("#19131A"),
227            surface_container_high: Color::from_hex("#1F1B22"),
228            surface_container_highest: Color::from_hex("#2A2930"),
229            surface_bright: Color::from_hex("#26292F"),
230            surface_dim: Color::from_hex("#1A1C1E"),
231            surface_tint: Color::from_hex("#69FDBE"),
232
233            inverse_surface: Color::from_hex("#E6E1E5"),
234            inverse_on_surface: Color::from_hex("#2A2930"),
235            inverse_primary: Color::from_hex("#005048"),
236
237            outline: Color::from_hex("#74777F"),
238            outline_variant: Color::from_hex("#44474E"),
239
240            scrim: Color::from_hex("#000000"),
241            shadow: Color::from_hex("#000000"),
242            focus: Color::from_hex("#006A6A"),
243        }
244    }
245
246    pub fn light() -> Self {
247        Self {
248            primary: Color::from_hex("#006A6A"),
249            on_primary: Color::WHITE,
250            primary_container: Color::from_hex("#9EF0EC"),
251            on_primary_container: Color::from_hex("#002020"),
252
253            secondary: Color::from_hex("#586146"),
254            on_secondary: Color::WHITE,
255            secondary_container: Color::from_hex("#D8E3B8"),
256            on_secondary_container: Color::from_hex("#161C0A"),
257
258            tertiary: Color::from_hex("#744639"),
259            on_tertiary: Color::WHITE,
260            tertiary_container: Color::from_hex("#FFD9CD"),
261            on_tertiary_container: Color::from_hex("#2C0E07"),
262
263            error: Color::from_hex("#BA1A1A"),
264            on_error: Color::WHITE,
265            error_container: Color::from_hex("#FFDAD6"),
266            on_error_container: Color::from_hex("#410002"),
267
268            background: Color::from_hex("#FEF7FF"),
269            on_background: Color::from_hex("#1A1C1E"),
270            surface: Color::from_hex("#FEF7FF"),
271            on_surface: Color::from_hex("#1A1C1E"),
272            surface_variant: Color::from_hex("#E1E3DE"),
273            on_surface_variant: Color::from_hex("#44474E"),
274            surface_container_lowest: Color::WHITE,
275            surface_container_low: Color::from_hex("#F4F5F0"),
276            surface_container: Color::from_hex("#EEF0E9"),
277            surface_container_high: Color::from_hex("#E9EAE4"),
278            surface_container_highest: Color::from_hex("#E3E5DF"),
279            surface_bright: Color::from_hex("#FEF7FF"),
280            surface_dim: Color::from_hex("#DEDAD0"),
281            surface_tint: Color::from_hex("#006A6A"),
282
283            inverse_surface: Color::from_hex("#2F3033"),
284            inverse_on_surface: Color::from_hex("#F1F0F4"),
285            inverse_primary: Color::from_hex("#69FDBE"),
286
287            outline: Color::from_hex("#74777F"),
288            outline_variant: Color::from_hex("#C4C6CE"),
289
290            scrim: Color::from_hex("#000000"),
291            shadow: Color::from_hex("#000000"),
292            focus: Color::from_hex("#1D4ED8"),
293        }
294    }
295}
296
297impl Default for ColorScheme {
298    fn default() -> Self {
299        Self::dark()
300    }
301}
302
303#[derive(Clone, Copy, Debug)]
304#[must_use]
305pub struct Typography {
306    pub display_large: f32,
307    pub display_medium: f32,
308    pub display_small: f32,
309    pub headline_large: f32,
310    pub headline_medium: f32,
311    pub headline_small: f32,
312    pub title_large: f32,
313    pub title_medium: f32,
314    pub title_small: f32,
315    pub body_large: f32,
316    pub body_medium: f32,
317    pub body_small: f32,
318    pub label_large: f32,
319    pub label_medium: f32,
320    pub label_small: f32,
321}
322
323impl Default for Typography {
324    fn default() -> Self {
325        Self {
326            display_large: 57.0,
327            display_medium: 45.0,
328            display_small: 36.0,
329            headline_large: 32.0,
330            headline_medium: 28.0,
331            headline_small: 24.0,
332            title_large: 22.0,
333            title_medium: 16.0,
334            title_small: 14.0,
335            body_large: 16.0,
336            body_medium: 14.0,
337            body_small: 12.0,
338            label_large: 14.0,
339            label_medium: 12.0,
340            label_small: 11.0,
341        }
342    }
343}
344
345#[derive(Clone, Copy, Debug)]
346#[must_use]
347pub struct Shapes {
348    pub extra_small: f32,
349    pub small: f32,
350    pub medium: f32,
351    pub large: f32,
352    pub extra_large: f32,
353}
354
355impl Default for Shapes {
356    fn default() -> Self {
357        Self {
358            extra_small: 4.0,
359            small: 8.0,
360            medium: 12.0,
361            large: 16.0,
362            extra_large: 28.0,
363        }
364    }
365}
366
367#[derive(Clone, Copy, Debug)]
368#[must_use]
369pub struct Spacing {
370    pub xs: f32,
371    pub sm: f32,
372    pub md: f32,
373    pub lg: f32,
374    pub xl: f32,
375    pub xxl: f32,
376}
377
378impl Default for Spacing {
379    fn default() -> Self {
380        Self {
381            xs: 4.0,
382            sm: 8.0,
383            md: 12.0,
384            lg: 16.0,
385            xl: 24.0,
386            xxl: 32.0,
387        }
388    }
389}
390
391#[derive(Clone, Copy, Debug)]
392#[must_use]
393pub struct Elevation {
394    pub level0: f32,
395    pub level1: f32,
396    pub level2: f32,
397    pub level3: f32,
398    pub level4: f32,
399    pub level5: f32,
400}
401
402impl Default for Elevation {
403    fn default() -> Self {
404        Self {
405            level0: 0.0,
406            level1: 1.0,
407            level2: 3.0,
408            level3: 6.0,
409            level4: 8.0,
410            level5: 12.0,
411        }
412    }
413}
414
415/// Centralized animation specs for M3 motion design.
416#[derive(Clone, Copy, Debug)]
417#[must_use]
418pub struct MotionScheme {
419    /// Shape / size / bounds transitions (e.g., indicator position, elevation).
420    /// M3 standard: 200 ms FastOutSlowIn.
421    pub shape: AnimationSpec,
422    /// Color state transitions (e.g., label, tab text, selection).
423    /// M3 standard: 150 ms FastOutSlowIn.
424    pub color: AnimationSpec,
425    /// Quick color changes (e.g., checkbox fill, switch track, radio ring).
426    /// M3 standard: 100 ms FastOutSlowIn.
427    pub color_fast: AnimationSpec,
428    /// Overlay / popup enter‑exit (menus, dialogs, tooltips).
429    /// M3 standard: 120 ms FastOutSlowIn.
430    pub overlay: AnimationSpec,
431    /// Spring‑based positional animation (sheets, drawers, swipe‑to‑dismiss).
432    /// M3 standard: gentle spring (ζ = 0.5, k = 200).
433    pub spring: AnimationSpec,
434    /// Expanding containers (search bar, docked search suggestions).
435    /// M3 standard: 250 ms FastOutSlowIn.
436    pub expand: AnimationSpec,
437    /// Large layout transitions (bottom sheet height, scaffold reflow).
438    /// M3 standard: 300 ms EaseOut.
439    pub layout: AnimationSpec,
440}
441
442impl Default for MotionScheme {
443    fn default() -> Self {
444        Self {
445            shape: AnimationSpec::tween(Duration::from_millis(200), Easing::FastOutSlowIn),
446            color: AnimationSpec::tween(Duration::from_millis(150), Easing::FastOutSlowIn),
447            color_fast: AnimationSpec::tween(Duration::from_millis(100), Easing::FastOutSlowIn),
448            overlay: AnimationSpec::tween(Duration::from_millis(120), Easing::FastOutSlowIn),
449            spring: AnimationSpec::spring_gentle(),
450            expand: AnimationSpec::tween(Duration::from_millis(250), Easing::FastOutSlowIn),
451            layout: AnimationSpec::tween(Duration::from_millis(300), Easing::EaseOut),
452        }
453    }
454}
455
456#[derive(Clone, Copy, Debug)]
457#[must_use]
458pub struct Theme {
459    pub colors: ColorScheme,
460    pub typography: Typography,
461    pub shapes: Shapes,
462    pub spacing: Spacing,
463    pub elevation: Elevation,
464    pub motion: MotionScheme,
465
466    pub focus: Color,
467    pub scrollbar_track: Color,
468    pub scrollbar_thumb: Color,
469    pub button_bg: Color,
470    pub button_bg_hover: Color,
471    pub button_bg_pressed: Color,
472}
473
474impl Deref for Theme {
475    type Target = ColorScheme;
476    fn deref(&self) -> &Self::Target {
477        &self.colors
478    }
479}
480
481impl Default for Theme {
482    fn default() -> Self {
483        let colors = ColorScheme::default();
484        Self {
485            colors,
486            typography: Typography::default(),
487            shapes: Shapes::default(),
488            spacing: Spacing::default(),
489            elevation: Elevation::default(),
490            motion: MotionScheme::default(),
491            focus: colors.focus,
492            scrollbar_track: Color(0xDD, 0xDD, 0xDD, 32),
493            scrollbar_thumb: Color(0xDD, 0xDD, 0xDD, 140),
494            button_bg: colors.primary,
495            button_bg_hover: colors.primary_container,
496            button_bg_pressed: colors.on_primary_container,
497        }
498    }
499}
500
501impl Theme {
502    pub fn with_colors(mut self, colors: ColorScheme) -> Self {
503        self.colors = colors;
504        self
505    }
506}
507
508/// Platform/device scale (dp→px multiplier). Platform runner should set this.
509#[derive(Clone, Copy, Debug)]
510pub struct Density {
511    pub scale: f32,
512}
513impl Default for Density {
514    fn default() -> Self {
515        Self { scale: 1.0 }
516    }
517}
518
519/// Additional UI scale multiplier (app-controlled).
520#[derive(Clone, Copy, Debug)]
521pub struct UiScale(pub f32);
522impl Default for UiScale {
523    fn default() -> Self {
524        Self(1.0)
525    }
526}
527
528#[derive(Clone, Copy, Debug)]
529pub struct TextScale(pub f32);
530impl Default for TextScale {
531    fn default() -> Self {
532        Self(1.0)
533    }
534}
535
536pub fn with_theme<R>(theme: Theme, f: impl FnOnce() -> R) -> R {
537    with_locals_frame(|| {
538        set_local_boxed(TypeId::of::<Theme>(), Box::new(theme));
539        f()
540    })
541}
542
543pub fn with_density<R>(density: Density, f: impl FnOnce() -> R) -> R {
544    with_locals_frame(|| {
545        set_local_boxed(TypeId::of::<Density>(), Box::new(density));
546        f()
547    })
548}
549
550pub fn with_ui_scale<R>(s: UiScale, f: impl FnOnce() -> R) -> R {
551    with_locals_frame(|| {
552        set_local_boxed(TypeId::of::<UiScale>(), Box::new(s));
553        f()
554    })
555}
556
557pub fn with_text_scale<R>(ts: TextScale, f: impl FnOnce() -> R) -> R {
558    with_locals_frame(|| {
559        set_local_boxed(TypeId::of::<TextScale>(), Box::new(ts));
560        f()
561    })
562}
563
564pub fn with_text_direction<R>(dir: TextDirection, f: impl FnOnce() -> R) -> R {
565    with_locals_frame(|| {
566        set_local_boxed(TypeId::of::<TextDirection>(), Box::new(dir));
567        f()
568    })
569}
570
571pub fn with_window_insets<R>(insets: WindowInsets, f: impl FnOnce() -> R) -> R {
572    with_locals_frame(|| {
573        set_local_boxed(TypeId::of::<WindowInsets>(), Box::new(insets));
574        f()
575    })
576}
577
578#[derive(Clone, Copy, Debug)]
579pub struct ContentColor(pub Color);
580
581pub fn with_content_color<R>(color: Color, f: impl FnOnce() -> R) -> R {
582    with_locals_frame(|| {
583        set_local_boxed(TypeId::of::<ContentColor>(), Box::new(ContentColor(color)));
584        f()
585    })
586}
587
588pub fn content_color() -> Color {
589    get_local::<ContentColor>()
590        .map(|c| c.0)
591        .unwrap_or_else(|| theme().on_surface)
592}
593
594/// System window insets (status bar, navigation bar, IME keyboard, etc.)
595#[derive(Clone, Copy, Debug, Default, PartialEq)]
596pub struct WindowInsets {
597    pub top: f32,
598    pub bottom: f32,
599    pub left: f32,
600    pub right: f32,
601    /// Soft keyboard (IME) inset from bottom of screen. Set by platform runner
602    /// when the keyboard opens/closes. Used by `imePadding()` modifier.
603    pub ime_bottom: f32,
604}
605
606/// Set the global default window insets (platform should call this when insets change).
607pub fn set_window_insets_default(insets: WindowInsets) {
608    defaults().write().window_insets = insets;
609    set_local_boxed(TypeId::of::<WindowInsets>(), Box::new(insets));
610}
611
612/// Update just the IME bottom inset (keyboard height in px). Platform runners
613/// call this when the soft keyboard opens/closes.
614pub fn set_ime_inset(height_px: f32) {
615    let mut insets = defaults().write().window_insets;
616    insets.ime_bottom = height_px;
617    // Also immediately set the thread-local so it's visible to the current frame
618    set_local_boxed(TypeId::of::<WindowInsets>(), Box::new(insets));
619}
620
621/// Query current window insets.
622pub fn window_insets() -> WindowInsets {
623    get_local::<WindowInsets>().unwrap_or_else(|| defaults().read().window_insets)
624}
625
626/// Coarse width category for a window, computed from its current size.
627///
628/// Thresholds (in dp) match the Material 3 adaptive spec:
629///
630/// - [`WidthClass::Compact`]  : width < 600 dp
631/// - [`WidthClass::Medium`]   : 600 dp <= width < 840 dp
632/// - [`WidthClass::Expanded`] : width >= 840 dp
633#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
634pub enum WidthClass {
635    #[default]
636    Compact,
637    Medium,
638    Expanded,
639}
640
641/// Coarse height category for a window, computed from its current size.
642///
643/// Thresholds (in dp) match the Material 3 adaptive spec:
644///
645/// - [`HeightClass::Compact`]  : height < 480 dp
646/// - [`HeightClass::Medium`]   : 480 dp <= height < 900 dp
647/// - [`HeightClass::Expanded`] : height >= 900 dp
648#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
649pub enum HeightClass {
650    #[default]
651    Compact,
652    Medium,
653    Expanded,
654}
655
656/// Snapshot of the current window's size category.
657///
658/// The `LayoutEngine` updates the `WindowSizeClass` default local every time
659/// the window is resized, so UI can read it via [`window_size_class()`] during
660/// composition.
661#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
662pub struct WindowSizeClass {
663    pub width: WidthClass,
664    pub height: HeightClass,
665}
666
667impl WindowSizeClass {
668    /// `true` when there is enough horizontal space for multi-pane layouts.
669    pub fn is_expanded_width(&self) -> bool {
670        matches!(self.width, WidthClass::Expanded)
671    }
672    /// `true` when there is enough horizontal space for a two-pane layout
673    /// (list + detail). Per M3 this is Medium or wider.
674    pub fn is_at_least_medium_width(&self) -> bool {
675        matches!(self.width, WidthClass::Medium | WidthClass::Expanded)
676    }
677}
678
679/// Compute a [`WindowSizeClass`] from a window size in physical pixels and
680/// the current dp→px density scale (`Density.scale * UiScale.0`).
681pub fn calculate_window_size_class(
682    width_px: u32,
683    height_px: u32,
684    density_scale: f32,
685) -> WindowSizeClass {
686    let density = density_scale.max(0.0001);
687    let width_dp = (width_px as f32) / density;
688    let height_dp = (height_px as f32) / density;
689
690    let width = if width_dp < 600.0 {
691        WidthClass::Compact
692    } else if width_dp < 840.0 {
693        WidthClass::Medium
694    } else {
695        WidthClass::Expanded
696    };
697    let height = if height_dp < 480.0 {
698        HeightClass::Compact
699    } else if height_dp < 900.0 {
700        HeightClass::Medium
701    } else {
702        HeightClass::Expanded
703    };
704
705    WindowSizeClass { width, height }
706}
707
708/// Set the global default window size class used when no local is active.
709/// Called by the `LayoutEngine` on resize.
710pub fn set_window_size_class_default(class: WindowSizeClass) {
711    defaults().write().window_size_class = class;
712}
713
714/// Override the window size class for a subtree of the composition.
715pub fn with_window_size_class<R>(class: WindowSizeClass, f: impl FnOnce() -> R) -> R {
716    with_locals_frame(|| {
717        set_local_boxed(TypeId::of::<WindowSizeClass>(), Box::new(class));
718        f()
719    })
720}
721
722/// Query current window size class. Returns a default-initialized
723/// `WindowSizeClass` (Compact/Compact) if nothing has been set yet.
724pub fn window_size_class() -> WindowSizeClass {
725    get_local::<WindowSizeClass>().unwrap_or_else(|| defaults().read().window_size_class)
726}
727
728macro_rules! def_local_getter {
729    ($fn_name:ident, $ty:ty, $default_field:ident) => {
730        pub fn $fn_name() -> $ty {
731            get_local::<$ty>().unwrap_or_else(|| defaults().read().$default_field)
732        }
733    };
734}
735
736def_local_getter!(theme, Theme, theme);
737def_local_getter!(density, Density, density);
738def_local_getter!(ui_scale, UiScale, ui_scale);
739def_local_getter!(text_scale, TextScale, text_scale);
740def_local_getter!(text_direction, TextDirection, text_direction);
741
742#[cfg(test)]
743mod tests {
744    use super::*;
745
746    #[test]
747    fn width_class_thresholds_match_m3() {
748        // Density 1.0 (1 dp = 1 px) for clarity.
749        assert_eq!(
750            calculate_window_size_class(100, 100, 1.0).width,
751            WidthClass::Compact
752        );
753        assert_eq!(
754            calculate_window_size_class(599, 100, 1.0).width,
755            WidthClass::Compact
756        );
757        assert_eq!(
758            calculate_window_size_class(600, 100, 1.0).width,
759            WidthClass::Medium
760        );
761        assert_eq!(
762            calculate_window_size_class(839, 100, 1.0).width,
763            WidthClass::Medium
764        );
765        assert_eq!(
766            calculate_window_size_class(840, 100, 1.0).width,
767            WidthClass::Expanded
768        );
769        assert_eq!(
770            calculate_window_size_class(2000, 100, 1.0).width,
771            WidthClass::Expanded
772        );
773    }
774
775    #[test]
776    fn height_class_thresholds_match_m3() {
777        assert_eq!(
778            calculate_window_size_class(100, 100, 1.0).height,
779            HeightClass::Compact
780        );
781        assert_eq!(
782            calculate_window_size_class(100, 479, 1.0).height,
783            HeightClass::Compact
784        );
785        assert_eq!(
786            calculate_window_size_class(100, 480, 1.0).height,
787            HeightClass::Medium
788        );
789        assert_eq!(
790            calculate_window_size_class(100, 899, 1.0).height,
791            HeightClass::Medium
792        );
793        assert_eq!(
794            calculate_window_size_class(100, 900, 1.0).height,
795            HeightClass::Expanded
796        );
797    }
798
799    #[test]
800    fn density_scales_thresholds() {
801        // 2.0x density: 600 dp = 1200 px.
802        let c = calculate_window_size_class(1199, 100, 2.0);
803        assert_eq!(c.width, WidthClass::Compact);
804        let c = calculate_window_size_class(1200, 100, 2.0);
805        assert_eq!(c.width, WidthClass::Medium);
806    }
807
808    #[test]
809    fn is_at_least_medium_width() {
810        let c = WindowSizeClass {
811            width: WidthClass::Compact,
812            height: HeightClass::Compact,
813        };
814        assert!(!c.is_at_least_medium_width());
815        let c = WindowSizeClass {
816            width: WidthClass::Medium,
817            height: HeightClass::Compact,
818        };
819        assert!(c.is_at_least_medium_width());
820        let c = WindowSizeClass {
821            width: WidthClass::Expanded,
822            height: HeightClass::Compact,
823        };
824        assert!(c.is_at_least_medium_width());
825    }
826}