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 scale factor.
7//! - `TextScale` — user text scaling.
8//! - `TextDirection` — LTR or RTL.
9//!
10//! You can override these for a subtree using `with_theme`, `with_density`,
11//! `with_text_scale`, and `with_text_direction`:
12//!
13//! ```rust
14//! use repose_core::*;
15//!
16//! let light = Theme {
17//!     background: Color::WHITE,
18//!     surface: Color::from_hex("#F5F5F5"),
19//!     on_surface: Color::from_hex("#222222"),
20//!     primary: Color::from_hex("#0061A4"),
21//!     on_primary: Color::WHITE,
22//!     ..Theme::default()
23//! };
24//!
25//! with_theme(light, || {
26//!     // all views composed here will see the light theme
27//! });
28//! ```
29//!
30//! Widgets in `repose-ui` and `repose-material` read from `theme()` and
31//! should avoid hard‑coding colors where possible.
32
33use std::any::{Any, TypeId};
34use std::cell::RefCell;
35use std::collections::HashMap;
36
37use crate::Color;
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum TextDirection {
41    Ltr,
42    Rtl,
43}
44impl Default for TextDirection {
45    fn default() -> Self {
46        TextDirection::Ltr
47    }
48}
49
50thread_local! {
51    static LOCALS_STACK: RefCell<Vec<HashMap<TypeId, Box<dyn Any>>>> = RefCell::new(Vec::new());
52}
53
54/// density‑independent pixels (dp)
55#[derive(Clone, Copy, Debug, PartialEq)]
56pub struct Dp(pub f32);
57
58impl Dp {
59    /// Converts this dp value into physical pixels using the current Density.
60    pub fn to_px(self) -> f32 {
61        self.0 * density().scale
62    }
63}
64
65/// Convenience: convert a raw dp scalar into px using current Density.
66pub fn dp_to_px(dp: f32) -> f32 {
67    Dp(dp).to_px()
68}
69
70pub fn with_text_direction<R>(dir: TextDirection, f: impl FnOnce() -> R) -> R {
71    with_locals_frame(|| {
72        set_local_boxed(std::any::TypeId::of::<TextDirection>(), Box::new(dir));
73        f()
74    })
75}
76
77pub fn text_direction() -> TextDirection {
78    LOCALS_STACK.with(|st| {
79        for frame in st.borrow().iter().rev() {
80            if let Some(v) = frame.get(&std::any::TypeId::of::<TextDirection>()) {
81                if let Some(d) = v.downcast_ref::<TextDirection>() {
82                    return *d;
83                }
84            }
85        }
86        TextDirection::default()
87    })
88}
89
90fn with_locals_frame<R>(f: impl FnOnce() -> R) -> R {
91    // Non-panicking frame guard (ensures pop on unwind)
92    struct Guard;
93    impl Drop for Guard {
94        fn drop(&mut self) {
95            LOCALS_STACK.with(|st| {
96                st.borrow_mut().pop();
97            });
98        }
99    }
100    LOCALS_STACK.with(|st| st.borrow_mut().push(HashMap::new()));
101    let _guard = Guard;
102    f()
103}
104
105fn set_local_boxed(t: TypeId, v: Box<dyn Any>) {
106    LOCALS_STACK.with(|st| {
107        if let Some(top) = st.borrow_mut().last_mut() {
108            top.insert(t, v);
109        } else {
110            // no frame: create a temporary one
111            let mut m = HashMap::new();
112            m.insert(t, v);
113            st.borrow_mut().push(m);
114        }
115    });
116}
117
118// Typed API
119
120/// High‑level color theme used by widgets and layout.
121///
122/// This is intentionally small and semantic rather than a full Material 3
123/// spec. Higher‑level libraries (e.g. `repose-material`) can build richer
124/// schemes on top.
125#[derive(Clone, Copy, Debug)]
126pub struct Theme {
127    /// Window background / app root.
128    pub background: Color,
129    /// Default container surface (cards, sheets, panels).
130    pub surface: Color,
131    /// Primary foreground color on top of `surface`/`background`.
132    pub on_surface: Color,
133
134    /// Primary accent color for buttons, sliders, progress, etc.
135    pub primary: Color,
136    /// Foreground color used on top of `primary`.
137    pub on_primary: Color,
138
139    /// Low‑emphasis outline/border color.
140    pub outline: Color,
141    /// Color for focus rings and accessibility highlights.
142    pub focus: Color,
143
144    /// Default button background.
145    pub button_bg: Color,
146    /// Button background on hover.
147    pub button_bg_hover: Color,
148    /// Button background on pressed.
149    pub button_bg_pressed: Color,
150
151    /// Scrollbar track background (low emphasis).
152    pub scrollbar_track: Color,
153    /// Scrollbar thumb (higher emphasis).
154    pub scrollbar_thumb: Color,
155
156    ///Error
157    pub error: Color,
158}
159
160impl Default for Theme {
161    fn default() -> Self {
162        Self {
163            background: Color::from_hex("#121212"),
164            surface: Color::from_hex("#1E1E1E"),
165            on_surface: Color::from_hex("#DDDDDD"),
166            primary: Color::from_hex("#34AF82"),
167            on_primary: Color::WHITE,
168            outline: Color::from_hex("#555555"),
169            focus: Color::from_hex("#88CCFF"),
170            button_bg: Color::from_hex("#34AF82"),
171            button_bg_hover: Color::from_hex("#2A8F6A"),
172            button_bg_pressed: Color::from_hex("#1F7556"),
173            scrollbar_track: Color(0xDD, 0xDD, 0xDD, 32),
174            scrollbar_thumb: Color(0xDD, 0xDD, 0xDD, 140),
175
176            error: Color::from_hex("#ae3636"),
177        }
178    }
179}
180
181#[derive(Clone, Copy, Debug)]
182pub struct Density {
183    pub scale: f32, // dp→px multiplier
184}
185impl Default for Density {
186    fn default() -> Self {
187        Self { scale: 1.0 }
188    }
189}
190
191#[derive(Clone, Copy, Debug)]
192pub struct TextScale(pub f32);
193impl Default for TextScale {
194    fn default() -> Self {
195        Self(1.0)
196    }
197}
198
199// Provide helpers (push a new frame, set the local, run closure, pop frame)
200
201pub fn with_theme<R>(theme: Theme, f: impl FnOnce() -> R) -> R {
202    with_locals_frame(|| {
203        set_local_boxed(TypeId::of::<Theme>(), Box::new(theme));
204        f()
205    })
206}
207
208pub fn with_density<R>(density: Density, f: impl FnOnce() -> R) -> R {
209    with_locals_frame(|| {
210        set_local_boxed(TypeId::of::<Density>(), Box::new(density));
211        f()
212    })
213}
214
215pub fn with_text_scale<R>(ts: TextScale, f: impl FnOnce() -> R) -> R {
216    with_locals_frame(|| {
217        set_local_boxed(TypeId::of::<TextScale>(), Box::new(ts));
218        f()
219    })
220}
221
222// Getters with defaults if not set
223
224pub fn theme() -> Theme {
225    LOCALS_STACK.with(|st| {
226        for frame in st.borrow().iter().rev() {
227            if let Some(v) = frame.get(&TypeId::of::<Theme>()) {
228                if let Some(t) = v.downcast_ref::<Theme>() {
229                    return *t;
230                }
231            }
232        }
233        Theme::default()
234    })
235}
236
237pub fn density() -> Density {
238    LOCALS_STACK.with(|st| {
239        for frame in st.borrow().iter().rev() {
240            if let Some(v) = frame.get(&TypeId::of::<Density>()) {
241                if let Some(d) = v.downcast_ref::<Density>() {
242                    return *d;
243                }
244            }
245        }
246        Density::default()
247    })
248}
249
250pub fn text_scale() -> TextScale {
251    LOCALS_STACK.with(|st| {
252        for frame in st.borrow().iter().rev() {
253            if let Some(v) = frame.get(&TypeId::of::<TextScale>()) {
254                if let Some(ts) = v.downcast_ref::<TextScale>() {
255                    return *ts;
256                }
257            }
258        }
259        TextScale::default()
260    })
261}