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;
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
26pub enum TextDirection {
27    #[default]
28    Ltr,
29    Rtl,
30}
31
32thread_local! {
33    static LOCALS_STACK: RefCell<Vec<HashMap<TypeId, Box<dyn Any>>>> = RefCell::new(Vec::new());
34}
35
36#[derive(Clone, Copy, Debug)]
37struct Defaults {
38    theme: Theme,
39    text_direction: TextDirection,
40    ui_scale: UiScale,
41    text_scale: TextScale,
42    density: Density,
43}
44
45impl Default for Defaults {
46    fn default() -> Self {
47        Self {
48            theme: Theme::default(),
49            text_direction: TextDirection::default(),
50            ui_scale: UiScale::default(),
51            text_scale: TextScale::default(),
52            density: Density::default(),
53        }
54    }
55}
56
57static DEFAULTS: OnceLock<RwLock<Defaults>> = OnceLock::new();
58
59fn defaults() -> &'static RwLock<Defaults> {
60    DEFAULTS.get_or_init(|| RwLock::new(Defaults::default()))
61}
62
63/// Set the global default theme used when no local Theme is active.
64pub fn set_theme_default(t: Theme) {
65    defaults().write().theme = t;
66}
67
68/// Set the global default text direction used when no local TextDirection is active.
69pub fn set_text_direction_default(d: TextDirection) {
70    defaults().write().text_direction = d;
71}
72
73/// Set the global default UI scale used when no local UiScale is active.
74pub fn set_ui_scale_default(s: UiScale) {
75    defaults().write().ui_scale = UiScale(s.0.max(0.0));
76}
77
78/// Set the global default text scale used when no local TextScale is active.
79pub fn set_text_scale_default(s: TextScale) {
80    defaults().write().text_scale = TextScale(s.0.max(0.0));
81}
82
83/// Set the global default device density (dp→px) used when no local Density is active.
84/// Platform runners should call this whenever the window scale factor changes.
85pub fn set_density_default(d: Density) {
86    defaults().write().density = Density {
87        scale: d.scale.max(0.0),
88    };
89}
90
91// ---- Units ----
92
93/// density‑independent pixels (dp)
94#[derive(Clone, Copy, Debug, PartialEq)]
95pub struct Dp(pub f32);
96
97impl Dp {
98    /// Converts this dp value into physical pixels using current Density * UiScale.
99    pub fn to_px(self) -> f32 {
100        self.0 * density().scale * ui_scale().0
101    }
102}
103
104/// Convenience: convert a raw dp scalar into px using current Density * UiScale.
105pub fn dp_to_px(dp: f32) -> f32 {
106    Dp(dp).to_px()
107}
108
109fn with_locals_frame<R>(f: impl FnOnce() -> R) -> R {
110    struct Guard;
111    impl Drop for Guard {
112        fn drop(&mut self) {
113            LOCALS_STACK.with(|st| {
114                st.borrow_mut().pop();
115            });
116        }
117    }
118    LOCALS_STACK.with(|st| st.borrow_mut().push(HashMap::new()));
119    let _guard = Guard;
120    f()
121}
122
123fn set_local_boxed(t: TypeId, v: Box<dyn Any>) {
124    LOCALS_STACK.with(|st| {
125        if let Some(top) = st.borrow_mut().last_mut() {
126            top.insert(t, v);
127        } else {
128            // no frame: create a temporary one
129            let mut m = HashMap::new();
130            m.insert(t, v);
131            st.borrow_mut().push(m);
132        }
133    });
134}
135
136fn get_local<T: 'static + Copy>() -> Option<T> {
137    LOCALS_STACK.with(|st| {
138        for frame in st.borrow().iter().rev() {
139            if let Some(v) = frame.get(&TypeId::of::<T>())
140                && let Some(t) = v.downcast_ref::<T>()
141            {
142                return Some(*t);
143            }
144        }
145        None
146    })
147}
148
149#[derive(Clone, Copy, Debug)]
150#[must_use]
151pub struct ColorScheme {
152    pub primary: Color,
153    pub on_primary: Color,
154    pub primary_container: Color,
155    pub on_primary_container: Color,
156
157    pub secondary: Color,
158    pub on_secondary: Color,
159    pub secondary_container: Color,
160    pub on_secondary_container: Color,
161
162    pub tertiary: Color,
163    pub on_tertiary: Color,
164    pub tertiary_container: Color,
165    pub on_tertiary_container: Color,
166
167    pub error: Color,
168    pub on_error: Color,
169    pub error_container: Color,
170    pub on_error_container: Color,
171
172    pub background: Color,
173    pub on_background: Color,
174    pub surface: Color,
175    pub on_surface: Color,
176    pub surface_variant: Color,
177    pub on_surface_variant: Color,
178    pub surface_container_lowest: Color,
179    pub surface_container_low: Color,
180    pub surface_container: Color,
181    pub surface_container_high: Color,
182    pub surface_container_highest: Color,
183    pub surface_bright: Color,
184    pub surface_dim: Color,
185    pub surface_tint: Color,
186
187    pub inverse_surface: Color,
188    pub inverse_on_surface: Color,
189    pub inverse_primary: Color,
190
191    pub outline: Color,
192    pub outline_variant: Color,
193
194    pub scrim: Color,
195    pub shadow: Color,
196    pub focus: Color,
197}
198
199impl ColorScheme {
200    pub fn dark() -> Self {
201        Self {
202            primary: Color::from_hex("#69FDBE"),
203            on_primary: Color::from_hex("#003020"),
204            primary_container: Color::from_hex("#004D40"),
205            on_primary_container: Color::from_hex("#6FF7F6"),
206
207            secondary: Color::from_hex("#B3C9A7"),
208            on_secondary: Color::from_hex("#1C3519"),
209            secondary_container: Color::from_hex("#334D2E"),
210            on_secondary_container: Color::from_hex("#CCE8B3"),
211
212            tertiary: Color::from_hex("#FFC9C1"),
213            on_tertiary: Color::from_hex("#3F1619"),
214            tertiary_container: Color::from_hex("#5D1F22"),
215            on_tertiary_container: Color::from_hex("#FFDBD8"),
216
217            error: Color::from_hex("#F2B8B5"),
218            on_error: Color::from_hex("#601410"),
219            error_container: Color::from_hex("#8C1D18"),
220            on_error_container: Color::from_hex("#F9DEDC"),
221
222            background: Color::from_hex("#1A1C1E"),
223            on_background: Color::from_hex("#E6E1E5"),
224            surface: Color::from_hex("#1A1C1E"),
225            on_surface: Color::from_hex("#E6E1E5"),
226            surface_variant: Color::from_hex("#44474E"),
227            on_surface_variant: Color::from_hex("#C4C6CE"),
228            surface_container_lowest: Color::from_hex("#0A0A0C"),
229            surface_container_low: Color::from_hex("#141115"),
230            surface_container: Color::from_hex("#19131A"),
231            surface_container_high: Color::from_hex("#1F1B22"),
232            surface_container_highest: Color::from_hex("#2A2930"),
233            surface_bright: Color::from_hex("#26292F"),
234            surface_dim: Color::from_hex("#1A1C1E"),
235            surface_tint: Color::from_hex("#69FDBE"),
236
237            inverse_surface: Color::from_hex("#E6E1E5"),
238            inverse_on_surface: Color::from_hex("#2A2930"),
239            inverse_primary: Color::from_hex("#005048"),
240
241            outline: Color::from_hex("#74777F"),
242            outline_variant: Color::from_hex("#44474E"),
243
244            scrim: Color::from_hex("#000000"),
245            shadow: Color::from_hex("#000000"),
246            focus: Color::from_hex("#006A6A"),
247        }
248    }
249
250    pub fn light() -> Self {
251        Self {
252            primary: Color::from_hex("#006A6A"),
253            on_primary: Color::WHITE,
254            primary_container: Color::from_hex("#9EF0EC"),
255            on_primary_container: Color::from_hex("#002020"),
256
257            secondary: Color::from_hex("#586146"),
258            on_secondary: Color::WHITE,
259            secondary_container: Color::from_hex("#D8E3B8"),
260            on_secondary_container: Color::from_hex("#161C0A"),
261
262            tertiary: Color::from_hex("#744639"),
263            on_tertiary: Color::WHITE,
264            tertiary_container: Color::from_hex("#FFD9CD"),
265            on_tertiary_container: Color::from_hex("#2C0E07"),
266
267            error: Color::from_hex("#BA1A1A"),
268            on_error: Color::WHITE,
269            error_container: Color::from_hex("#FFDAD6"),
270            on_error_container: Color::from_hex("#410002"),
271
272            background: Color::from_hex("#FEF7FF"),
273            on_background: Color::from_hex("#1A1C1E"),
274            surface: Color::from_hex("#FEF7FF"),
275            on_surface: Color::from_hex("#1A1C1E"),
276            surface_variant: Color::from_hex("#E1E3DE"),
277            on_surface_variant: Color::from_hex("#44474E"),
278            surface_container_lowest: Color::WHITE,
279            surface_container_low: Color::from_hex("#F4F5F0"),
280            surface_container: Color::from_hex("#EEF0E9"),
281            surface_container_high: Color::from_hex("#E9EAE4"),
282            surface_container_highest: Color::from_hex("#E3E5DF"),
283            surface_bright: Color::from_hex("#FEF7FF"),
284            surface_dim: Color::from_hex("#DEDAD0"),
285            surface_tint: Color::from_hex("#006A6A"),
286
287            inverse_surface: Color::from_hex("#2F3033"),
288            inverse_on_surface: Color::from_hex("#F1F0F4"),
289            inverse_primary: Color::from_hex("#69FDBE"),
290
291            outline: Color::from_hex("#74777F"),
292            outline_variant: Color::from_hex("#C4C6CE"),
293
294            scrim: Color::from_hex("#000000"),
295            shadow: Color::from_hex("#000000"),
296            focus: Color::from_hex("#1D4ED8"),
297        }
298    }
299}
300
301impl Default for ColorScheme {
302    fn default() -> Self {
303        Self::dark()
304    }
305}
306
307#[derive(Clone, Copy, Debug)]
308#[must_use]
309pub struct Typography {
310    pub display_large: f32,
311    pub display_medium: f32,
312    pub display_small: f32,
313    pub headline_large: f32,
314    pub headline_medium: f32,
315    pub headline_small: f32,
316    pub title_large: f32,
317    pub title_medium: f32,
318    pub title_small: f32,
319    pub body_large: f32,
320    pub body_medium: f32,
321    pub body_small: f32,
322    pub label_large: f32,
323    pub label_medium: f32,
324    pub label_small: f32,
325}
326
327impl Default for Typography {
328    fn default() -> Self {
329        Self {
330            display_large: 57.0,
331            display_medium: 45.0,
332            display_small: 36.0,
333            headline_large: 32.0,
334            headline_medium: 28.0,
335            headline_small: 24.0,
336            title_large: 22.0,
337            title_medium: 16.0,
338            title_small: 14.0,
339            body_large: 16.0,
340            body_medium: 14.0,
341            body_small: 12.0,
342            label_large: 14.0,
343            label_medium: 12.0,
344            label_small: 11.0,
345        }
346    }
347}
348
349#[derive(Clone, Copy, Debug)]
350#[must_use]
351pub struct Shapes {
352    pub extra_small: f32,
353    pub small: f32,
354    pub medium: f32,
355    pub large: f32,
356    pub extra_large: f32,
357}
358
359impl Default for Shapes {
360    fn default() -> Self {
361        Self {
362            extra_small: 4.0,
363            small: 8.0,
364            medium: 12.0,
365            large: 16.0,
366            extra_large: 28.0,
367        }
368    }
369}
370
371#[derive(Clone, Copy, Debug)]
372#[must_use]
373pub struct Spacing {
374    pub xs: f32,
375    pub sm: f32,
376    pub md: f32,
377    pub lg: f32,
378    pub xl: f32,
379    pub xxl: f32,
380}
381
382impl Default for Spacing {
383    fn default() -> Self {
384        Self {
385            xs: 4.0,
386            sm: 8.0,
387            md: 12.0,
388            lg: 16.0,
389            xl: 24.0,
390            xxl: 32.0,
391        }
392    }
393}
394
395#[derive(Clone, Copy, Debug)]
396#[must_use]
397pub struct Elevation {
398    pub level0: f32,
399    pub level1: f32,
400    pub level2: f32,
401    pub level3: f32,
402    pub level4: f32,
403    pub level5: f32,
404}
405
406impl Default for Elevation {
407    fn default() -> Self {
408        Self {
409            level0: 0.0,
410            level1: 1.0,
411            level2: 3.0,
412            level3: 6.0,
413            level4: 8.0,
414            level5: 12.0,
415        }
416    }
417}
418
419#[derive(Clone, Copy, Debug)]
420#[must_use]
421pub struct Motion {
422    pub fast_ms: u32,
423    pub medium_ms: u32,
424    pub slow_ms: u32,
425}
426
427impl Default for Motion {
428    fn default() -> Self {
429        Self {
430            fast_ms: 120,
431            medium_ms: 240,
432            slow_ms: 360,
433        }
434    }
435}
436
437#[derive(Clone, Copy, Debug)]
438#[must_use]
439pub struct Theme {
440    pub colors: ColorScheme,
441    pub typography: Typography,
442    pub shapes: Shapes,
443    pub spacing: Spacing,
444    pub elevation: Elevation,
445    pub motion: Motion,
446
447    pub focus: Color,
448    pub scrollbar_track: Color,
449    pub scrollbar_thumb: Color,
450    pub button_bg: Color,
451    pub button_bg_hover: Color,
452    pub button_bg_pressed: Color,
453}
454
455impl Deref for Theme {
456    type Target = ColorScheme;
457    fn deref(&self) -> &Self::Target {
458        &self.colors
459    }
460}
461
462impl Default for Theme {
463    fn default() -> Self {
464        let colors = ColorScheme::default();
465        Self {
466            colors,
467            typography: Typography::default(),
468            shapes: Shapes::default(),
469            spacing: Spacing::default(),
470            elevation: Elevation::default(),
471            motion: Motion::default(),
472            focus: colors.focus,
473            scrollbar_track: Color(0xDD, 0xDD, 0xDD, 32),
474            scrollbar_thumb: Color(0xDD, 0xDD, 0xDD, 140),
475            button_bg: colors.primary,
476            button_bg_hover: colors.primary_container,
477            button_bg_pressed: colors.on_primary_container,
478        }
479    }
480}
481
482impl Theme {
483    pub fn with_colors(mut self, colors: ColorScheme) -> Self {
484        self.colors = colors;
485        self
486    }
487}
488
489/// Platform/device scale (dp→px multiplier). Platform runner should set this.
490#[derive(Clone, Copy, Debug)]
491pub struct Density {
492    pub scale: f32,
493}
494impl Default for Density {
495    fn default() -> Self {
496        Self { scale: 1.0 }
497    }
498}
499
500/// Additional UI scale multiplier (app-controlled).
501#[derive(Clone, Copy, Debug)]
502pub struct UiScale(pub f32);
503impl Default for UiScale {
504    fn default() -> Self {
505        Self(1.0)
506    }
507}
508
509#[derive(Clone, Copy, Debug)]
510pub struct TextScale(pub f32);
511impl Default for TextScale {
512    fn default() -> Self {
513        Self(1.0)
514    }
515}
516
517pub fn with_theme<R>(theme: Theme, f: impl FnOnce() -> R) -> R {
518    with_locals_frame(|| {
519        set_local_boxed(TypeId::of::<Theme>(), Box::new(theme));
520        f()
521    })
522}
523
524pub fn with_density<R>(density: Density, f: impl FnOnce() -> R) -> R {
525    with_locals_frame(|| {
526        set_local_boxed(TypeId::of::<Density>(), Box::new(density));
527        f()
528    })
529}
530
531pub fn with_ui_scale<R>(s: UiScale, f: impl FnOnce() -> R) -> R {
532    with_locals_frame(|| {
533        set_local_boxed(TypeId::of::<UiScale>(), Box::new(s));
534        f()
535    })
536}
537
538pub fn with_text_scale<R>(ts: TextScale, f: impl FnOnce() -> R) -> R {
539    with_locals_frame(|| {
540        set_local_boxed(TypeId::of::<TextScale>(), Box::new(ts));
541        f()
542    })
543}
544
545pub fn with_text_direction<R>(dir: TextDirection, f: impl FnOnce() -> R) -> R {
546    with_locals_frame(|| {
547        set_local_boxed(TypeId::of::<TextDirection>(), Box::new(dir));
548        f()
549    })
550}
551
552#[derive(Clone, Copy, Debug)]
553pub struct ContentColor(pub Color);
554
555pub fn with_content_color<R>(color: Color, f: impl FnOnce() -> R) -> R {
556    with_locals_frame(|| {
557        set_local_boxed(TypeId::of::<ContentColor>(), Box::new(ContentColor(color)));
558        f()
559    })
560}
561
562pub fn content_color() -> Color {
563    get_local::<ContentColor>()
564        .map(|c| c.0)
565        .unwrap_or_else(|| theme().on_surface)
566}
567
568macro_rules! def_local_getter {
569    ($fn_name:ident, $ty:ty, $default_field:ident) => {
570        pub fn $fn_name() -> $ty {
571            get_local::<$ty>().unwrap_or_else(|| defaults().read().$default_field)
572        }
573    };
574}
575
576def_local_getter!(theme, Theme, theme);
577def_local_getter!(density, Density, density);
578def_local_getter!(ui_scale, UiScale, ui_scale);
579def_local_getter!(text_scale, TextScale, text_scale);
580def_local_getter!(text_direction, TextDirection, text_direction);