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)]
148pub struct Theme {
149    pub background: Color,
150    pub surface: Color,
151    pub on_surface: Color,
152
153    pub primary: Color,
154    pub on_primary: Color,
155
156    pub outline: Color,
157    pub focus: Color,
158
159    pub button_bg: Color,
160    pub button_bg_hover: Color,
161    pub button_bg_pressed: Color,
162
163    pub scrollbar_track: Color,
164    pub scrollbar_thumb: Color,
165
166    pub error: Color,
167}
168
169impl Default for Theme {
170    fn default() -> Self {
171        Self {
172            background: Color::from_hex("#121212"),
173            surface: Color::from_hex("#1E1E1E"),
174            on_surface: Color::from_hex("#DDDDDD"),
175            primary: Color::from_hex("#34AF82"),
176            on_primary: Color::WHITE,
177            outline: Color::from_hex("#555555"),
178            focus: Color::from_hex("#88CCFF"),
179            button_bg: Color::from_hex("#34AF82"),
180            button_bg_hover: Color::from_hex("#2A8F6A"),
181            button_bg_pressed: Color::from_hex("#1F7556"),
182            scrollbar_track: Color(0xDD, 0xDD, 0xDD, 32),
183            scrollbar_thumb: Color(0xDD, 0xDD, 0xDD, 140),
184            error: Color::from_hex("#ae3636"),
185        }
186    }
187}
188
189/// Platform/device scale (dp→px multiplier). Platform runner should set this.
190#[derive(Clone, Copy, Debug)]
191pub struct Density {
192    pub scale: f32,
193}
194impl Default for Density {
195    fn default() -> Self {
196        Self { scale: 1.0 }
197    }
198}
199
200/// Additional UI scale multiplier (app-controlled).
201#[derive(Clone, Copy, Debug)]
202pub struct UiScale(pub f32);
203impl Default for UiScale {
204    fn default() -> Self {
205        Self(1.0)
206    }
207}
208
209#[derive(Clone, Copy, Debug)]
210pub struct TextScale(pub f32);
211impl Default for TextScale {
212    fn default() -> Self {
213        Self(1.0)
214    }
215}
216
217pub fn with_theme<R>(theme: Theme, f: impl FnOnce() -> R) -> R {
218    with_locals_frame(|| {
219        set_local_boxed(TypeId::of::<Theme>(), Box::new(theme));
220        f()
221    })
222}
223
224pub fn with_density<R>(density: Density, f: impl FnOnce() -> R) -> R {
225    with_locals_frame(|| {
226        set_local_boxed(TypeId::of::<Density>(), Box::new(density));
227        f()
228    })
229}
230
231pub fn with_ui_scale<R>(s: UiScale, f: impl FnOnce() -> R) -> R {
232    with_locals_frame(|| {
233        set_local_boxed(TypeId::of::<UiScale>(), Box::new(s));
234        f()
235    })
236}
237
238pub fn with_text_scale<R>(ts: TextScale, f: impl FnOnce() -> R) -> R {
239    with_locals_frame(|| {
240        set_local_boxed(TypeId::of::<TextScale>(), Box::new(ts));
241        f()
242    })
243}
244
245pub fn with_text_direction<R>(dir: TextDirection, f: impl FnOnce() -> R) -> R {
246    with_locals_frame(|| {
247        set_local_boxed(TypeId::of::<TextDirection>(), Box::new(dir));
248        f()
249    })
250}
251
252pub fn theme() -> Theme {
253    get_local::<Theme>().unwrap_or_else(|| defaults().read().theme)
254}
255
256pub fn density() -> Density {
257    get_local::<Density>().unwrap_or_else(|| defaults().read().density)
258}
259
260pub fn ui_scale() -> UiScale {
261    get_local::<UiScale>().unwrap_or_else(|| defaults().read().ui_scale)
262}
263
264pub fn text_scale() -> TextScale {
265    get_local::<TextScale>().unwrap_or_else(|| defaults().read().text_scale)
266}
267
268pub fn text_direction() -> TextDirection {
269    get_local::<TextDirection>().unwrap_or_else(|| defaults().read().text_direction)
270}