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