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::any::{Any, TypeId};
15use std::cell::RefCell;
16use std::collections::HashMap;
17use std::sync::OnceLock;
18
19use parking_lot::RwLock;
20
21use crate::Color;
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
24pub enum TextDirection {
25    #[default]
26    Ltr,
27    Rtl,
28}
29
30thread_local! {
31    static LOCALS_STACK: RefCell<Vec<HashMap<TypeId, Box<dyn Any>>>> = RefCell::new(Vec::new());
32}
33
34#[derive(Clone, Copy, Debug)]
35struct Defaults {
36    theme: Theme,
37    text_direction: TextDirection,
38    ui_scale: UiScale,
39    text_scale: TextScale,
40    density: Density,
41}
42
43impl Default for Defaults {
44    fn default() -> Self {
45        Self {
46            theme: Theme::default(),
47            text_direction: TextDirection::default(),
48            ui_scale: UiScale::default(),
49            text_scale: TextScale::default(),
50            density: Density::default(),
51        }
52    }
53}
54
55static DEFAULTS: OnceLock<RwLock<Defaults>> = OnceLock::new();
56
57fn defaults() -> &'static RwLock<Defaults> {
58    DEFAULTS.get_or_init(|| RwLock::new(Defaults::default()))
59}
60
61/// Set the global default theme used when no local Theme is active.
62pub fn set_theme_default(t: Theme) {
63    defaults().write().theme = t;
64}
65
66/// Set the global default text direction used when no local TextDirection is active.
67pub fn set_text_direction_default(d: TextDirection) {
68    defaults().write().text_direction = d;
69}
70
71/// Set the global default UI scale used when no local UiScale is active.
72pub fn set_ui_scale_default(s: UiScale) {
73    defaults().write().ui_scale = UiScale(s.0.max(0.0));
74}
75
76/// Set the global default text scale used when no local TextScale is active.
77pub fn set_text_scale_default(s: TextScale) {
78    defaults().write().text_scale = TextScale(s.0.max(0.0));
79}
80
81/// Set the global default device density (dp→px) used when no local Density is active.
82/// Platform runners should call this whenever the window scale factor changes.
83pub fn set_density_default(d: Density) {
84    defaults().write().density = Density {
85        scale: d.scale.max(0.0),
86    };
87}
88
89// ---- Units ----
90
91/// density‑independent pixels (dp)
92#[derive(Clone, Copy, Debug, PartialEq)]
93pub struct Dp(pub f32);
94
95impl Dp {
96    /// Converts this dp value into physical pixels using current Density * UiScale.
97    pub fn to_px(self) -> f32 {
98        self.0 * density().scale * ui_scale().0
99    }
100}
101
102/// Convenience: convert a raw dp scalar into px using current Density * UiScale.
103pub fn dp_to_px(dp: f32) -> f32 {
104    Dp(dp).to_px()
105}
106
107fn with_locals_frame<R>(f: impl FnOnce() -> R) -> R {
108    struct Guard;
109    impl Drop for Guard {
110        fn drop(&mut self) {
111            LOCALS_STACK.with(|st| {
112                st.borrow_mut().pop();
113            });
114        }
115    }
116    LOCALS_STACK.with(|st| st.borrow_mut().push(HashMap::new()));
117    let _guard = Guard;
118    f()
119}
120
121fn set_local_boxed(t: TypeId, v: Box<dyn Any>) {
122    LOCALS_STACK.with(|st| {
123        if let Some(top) = st.borrow_mut().last_mut() {
124            top.insert(t, v);
125        } else {
126            // no frame: create a temporary one
127            let mut m = HashMap::new();
128            m.insert(t, v);
129            st.borrow_mut().push(m);
130        }
131    });
132}
133
134fn get_local<T: 'static + Copy>() -> Option<T> {
135    LOCALS_STACK.with(|st| {
136        for frame in st.borrow().iter().rev() {
137            if let Some(v) = frame.get(&TypeId::of::<T>())
138                && let Some(t) = v.downcast_ref::<T>()
139            {
140                return Some(*t);
141            }
142        }
143        None
144    })
145}
146
147#[derive(Clone, Copy, Debug)]
148#[must_use]
149pub struct ColorScheme {
150    pub background: Color,
151    pub surface: Color,
152    pub surface_variant: Color,
153    pub on_surface: Color,
154    pub on_surface_variant: Color,
155
156    pub primary: Color,
157    pub on_primary: Color,
158    pub secondary: Color,
159    pub on_secondary: Color,
160    pub tertiary: Color,
161    pub on_tertiary: Color,
162
163    pub outline: Color,
164    pub outline_variant: Color,
165    pub error: Color,
166    pub on_error: Color,
167    pub focus: Color,
168}
169
170impl Default for ColorScheme {
171    fn default() -> Self {
172        Self {
173            background: Color::from_hex("#121212"),
174            surface: Color::from_hex("#1E1E1E"),
175            surface_variant: Color::from_hex("#2A2A2A"),
176            on_surface: Color::from_hex("#DDDDDD"),
177            on_surface_variant: Color::from_hex("#B8B8B8"),
178            primary: Color::from_hex("#34AF82"),
179            on_primary: Color::WHITE,
180            secondary: Color::from_hex("#7BB6FF"),
181            on_secondary: Color::from_hex("#0E1A2B"),
182            tertiary: Color::from_hex("#E7A3FF"),
183            on_tertiary: Color::from_hex("#2B0E2B"),
184            outline: Color::from_hex("#555555"),
185            outline_variant: Color::from_hex("#3A3A3A"),
186            error: Color::from_hex("#AE3636"),
187            on_error: Color::WHITE,
188            focus: Color::from_hex("#88CCFF"),
189        }
190    }
191}
192
193#[derive(Clone, Copy, Debug)]
194#[must_use]
195pub struct Typography {
196    pub display_large: f32,
197    pub display_medium: f32,
198    pub display_small: f32,
199    pub headline_large: f32,
200    pub headline_medium: f32,
201    pub headline_small: f32,
202    pub title_large: f32,
203    pub title_medium: f32,
204    pub title_small: f32,
205    pub body_large: f32,
206    pub body_medium: f32,
207    pub body_small: f32,
208    pub label_large: f32,
209    pub label_medium: f32,
210    pub label_small: f32,
211}
212
213impl Default for Typography {
214    fn default() -> Self {
215        Self {
216            display_large: 57.0,
217            display_medium: 45.0,
218            display_small: 36.0,
219            headline_large: 32.0,
220            headline_medium: 28.0,
221            headline_small: 24.0,
222            title_large: 22.0,
223            title_medium: 16.0,
224            title_small: 14.0,
225            body_large: 16.0,
226            body_medium: 14.0,
227            body_small: 12.0,
228            label_large: 14.0,
229            label_medium: 12.0,
230            label_small: 11.0,
231        }
232    }
233}
234
235#[derive(Clone, Copy, Debug)]
236#[must_use]
237pub struct Shapes {
238    pub extra_small: f32,
239    pub small: f32,
240    pub medium: f32,
241    pub large: f32,
242    pub extra_large: f32,
243}
244
245impl Default for Shapes {
246    fn default() -> Self {
247        Self {
248            extra_small: 4.0,
249            small: 8.0,
250            medium: 12.0,
251            large: 16.0,
252            extra_large: 28.0,
253        }
254    }
255}
256
257#[derive(Clone, Copy, Debug)]
258#[must_use]
259pub struct Spacing {
260    pub xs: f32,
261    pub sm: f32,
262    pub md: f32,
263    pub lg: f32,
264    pub xl: f32,
265    pub xxl: f32,
266}
267
268impl Default for Spacing {
269    fn default() -> Self {
270        Self {
271            xs: 4.0,
272            sm: 8.0,
273            md: 12.0,
274            lg: 16.0,
275            xl: 24.0,
276            xxl: 32.0,
277        }
278    }
279}
280
281#[derive(Clone, Copy, Debug)]
282#[must_use]
283pub struct Elevation {
284    pub level0: f32,
285    pub level1: f32,
286    pub level2: f32,
287    pub level3: f32,
288    pub level4: f32,
289    pub level5: f32,
290}
291
292impl Default for Elevation {
293    fn default() -> Self {
294        Self {
295            level0: 0.0,
296            level1: 1.0,
297            level2: 3.0,
298            level3: 6.0,
299            level4: 8.0,
300            level5: 12.0,
301        }
302    }
303}
304
305#[derive(Clone, Copy, Debug)]
306#[must_use]
307pub struct Motion {
308    pub fast_ms: u32,
309    pub medium_ms: u32,
310    pub slow_ms: u32,
311}
312
313impl Default for Motion {
314    fn default() -> Self {
315        Self {
316            fast_ms: 120,
317            medium_ms: 240,
318            slow_ms: 360,
319        }
320    }
321}
322
323#[derive(Clone, Copy, Debug)]
324#[must_use]
325pub struct Theme {
326    pub colors: ColorScheme,
327    pub typography: Typography,
328    pub shapes: Shapes,
329    pub spacing: Spacing,
330    pub elevation: Elevation,
331    pub motion: Motion,
332
333    pub focus: Color,
334    pub scrollbar_track: Color,
335    pub scrollbar_thumb: Color,
336    pub button_bg: Color,
337    pub button_bg_hover: Color,
338    pub button_bg_pressed: Color,
339
340    pub background: Color,
341    pub surface: Color,
342    pub surface_variant: Color,
343    pub on_surface: Color,
344    pub on_surface_variant: Color,
345    pub primary: Color,
346    pub on_primary: Color,
347    pub secondary: Color,
348    pub on_secondary: Color,
349    pub tertiary: Color,
350    pub on_tertiary: Color,
351    pub outline: Color,
352    pub outline_variant: Color,
353    pub error: Color,
354    pub on_error: Color,
355}
356
357impl Default for Theme {
358    fn default() -> Self {
359        let colors = ColorScheme::default();
360        Self {
361            background: colors.background,
362            surface: colors.surface,
363            surface_variant: colors.surface_variant,
364            on_surface: colors.on_surface,
365            on_surface_variant: colors.on_surface_variant,
366            primary: colors.primary,
367            on_primary: colors.on_primary,
368            secondary: colors.secondary,
369            on_secondary: colors.on_secondary,
370            tertiary: colors.tertiary,
371            on_tertiary: colors.on_tertiary,
372            outline: colors.outline,
373            outline_variant: colors.outline_variant,
374            error: colors.error,
375            on_error: colors.on_error,
376            colors,
377            typography: Typography::default(),
378            shapes: Shapes::default(),
379            spacing: Spacing::default(),
380            elevation: Elevation::default(),
381            motion: Motion::default(),
382            focus: colors.focus,
383            scrollbar_track: Color(0xDD, 0xDD, 0xDD, 32),
384            scrollbar_thumb: Color(0xDD, 0xDD, 0xDD, 140),
385            button_bg: colors.primary,
386            button_bg_hover: Color::from_hex("#2A8F6A"),
387            button_bg_pressed: Color::from_hex("#1F7556"),
388        }
389    }
390}
391
392impl Theme {
393    pub fn apply_colors(&mut self) {
394        self.background = self.colors.background;
395        self.surface = self.colors.surface;
396        self.surface_variant = self.colors.surface_variant;
397        self.on_surface = self.colors.on_surface;
398        self.on_surface_variant = self.colors.on_surface_variant;
399        self.primary = self.colors.primary;
400        self.on_primary = self.colors.on_primary;
401        self.secondary = self.colors.secondary;
402        self.on_secondary = self.colors.on_secondary;
403        self.tertiary = self.colors.tertiary;
404        self.on_tertiary = self.colors.on_tertiary;
405        self.outline = self.colors.outline;
406        self.outline_variant = self.colors.outline_variant;
407        self.error = self.colors.error;
408        self.on_error = self.colors.on_error;
409        self.focus = self.colors.focus;
410        self.button_bg = self.colors.primary;
411    }
412
413    pub fn sync_colors_from_fields(&mut self) {
414        self.colors.background = self.background;
415        self.colors.surface = self.surface;
416        self.colors.surface_variant = self.surface_variant;
417        self.colors.on_surface = self.on_surface;
418        self.colors.on_surface_variant = self.on_surface_variant;
419        self.colors.primary = self.primary;
420        self.colors.on_primary = self.on_primary;
421        self.colors.secondary = self.secondary;
422        self.colors.on_secondary = self.on_secondary;
423        self.colors.tertiary = self.tertiary;
424        self.colors.on_tertiary = self.on_tertiary;
425        self.colors.outline = self.outline;
426        self.colors.outline_variant = self.outline_variant;
427        self.colors.error = self.error;
428        self.colors.on_error = self.on_error;
429        self.colors.focus = self.focus;
430    }
431
432    pub fn with_colors(mut self, colors: ColorScheme) -> Self {
433        self.colors = colors;
434        self.apply_colors();
435        self
436    }
437}
438
439/// Platform/device scale (dp→px multiplier). Platform runner should set this.
440#[derive(Clone, Copy, Debug)]
441pub struct Density {
442    pub scale: f32,
443}
444impl Default for Density {
445    fn default() -> Self {
446        Self { scale: 1.0 }
447    }
448}
449
450/// Additional UI scale multiplier (app-controlled).
451#[derive(Clone, Copy, Debug)]
452pub struct UiScale(pub f32);
453impl Default for UiScale {
454    fn default() -> Self {
455        Self(1.0)
456    }
457}
458
459#[derive(Clone, Copy, Debug)]
460pub struct TextScale(pub f32);
461impl Default for TextScale {
462    fn default() -> Self {
463        Self(1.0)
464    }
465}
466
467pub fn with_theme<R>(theme: Theme, f: impl FnOnce() -> R) -> R {
468    with_locals_frame(|| {
469        set_local_boxed(TypeId::of::<Theme>(), Box::new(theme));
470        f()
471    })
472}
473
474pub fn with_density<R>(density: Density, f: impl FnOnce() -> R) -> R {
475    with_locals_frame(|| {
476        set_local_boxed(TypeId::of::<Density>(), Box::new(density));
477        f()
478    })
479}
480
481pub fn with_ui_scale<R>(s: UiScale, f: impl FnOnce() -> R) -> R {
482    with_locals_frame(|| {
483        set_local_boxed(TypeId::of::<UiScale>(), Box::new(s));
484        f()
485    })
486}
487
488pub fn with_text_scale<R>(ts: TextScale, f: impl FnOnce() -> R) -> R {
489    with_locals_frame(|| {
490        set_local_boxed(TypeId::of::<TextScale>(), Box::new(ts));
491        f()
492    })
493}
494
495pub fn with_text_direction<R>(dir: TextDirection, f: impl FnOnce() -> R) -> R {
496    with_locals_frame(|| {
497        set_local_boxed(TypeId::of::<TextDirection>(), Box::new(dir));
498        f()
499    })
500}
501
502pub fn theme() -> Theme {
503    get_local::<Theme>().unwrap_or_else(|| defaults().read().theme)
504}
505
506pub fn density() -> Density {
507    get_local::<Density>().unwrap_or_else(|| defaults().read().density)
508}
509
510pub fn ui_scale() -> UiScale {
511    get_local::<UiScale>().unwrap_or_else(|| defaults().read().ui_scale)
512}
513
514pub fn text_scale() -> TextScale {
515    get_local::<TextScale>().unwrap_or_else(|| defaults().read().text_scale)
516}
517
518pub fn text_direction() -> TextDirection {
519    get_local::<TextDirection>().unwrap_or_else(|| defaults().read().text_direction)
520}