Skip to main content

rlvgl_core/
theme.rs

1//! Theme trait and basic implementations.
2//!
3//! This module provides two levels of theming:
4//!
5//! - [`Theme`] — a simple property-level hook on [`Style`]. Unchanged from
6//!   earlier LPAR phases.
7//! - [`LparTheme`] / [`WidgetClass`] / [`DefaultTheme`] — LPAR-07 §9 node-level
8//!   theme chaining. [`LparTheme::apply_to_node`] calls
9//!   [`ObjectNode::add_local_style`] with lowest-precedence defaults so that
10//!   per-node overrides continue to win.
11
12use crate::object::ObjectNode;
13use crate::style::Style;
14use crate::style_cascade::{Part, Selector, StylePatch};
15use crate::widget::Color;
16
17// ---------------------------------------------------------------------------
18// Existing Theme trait + implementations (unchanged)
19// ---------------------------------------------------------------------------
20
21/// Global theme that can modify widget styles.
22///
23/// Themes provide a simple hook to set initial colors and other stylistic
24/// properties for widgets. Applications can implement this trait to provide
25/// bespoke looks across the UI.
26pub trait Theme {
27    /// Apply the theme to the provided [`Style`].
28    fn apply(&self, style: &mut Style);
29}
30
31/// Simple light theme implementation.
32pub struct LightTheme;
33
34impl Theme for LightTheme {
35    fn apply(&self, style: &mut Style) {
36        style.bg_color = Color(255, 255, 255, 255);
37        style.border_color = Color(0, 0, 0, 255);
38    }
39}
40
41/// Simple dark theme implementation.
42pub struct DarkTheme;
43
44impl Theme for DarkTheme {
45    fn apply(&self, style: &mut Style) {
46        style.bg_color = Color(0, 0, 0, 255);
47        style.border_color = Color(255, 255, 255, 255);
48    }
49}
50
51// ---------------------------------------------------------------------------
52// WidgetClass — LPAR-07 §9
53// ---------------------------------------------------------------------------
54
55/// Identifies the semantic widget type for theme application (LPAR-07 §9.1).
56///
57/// Passed to [`LparTheme::apply_to_node`] so the theme can select per-class
58/// default patches without inspecting the node's widget instance.
59///
60/// Registration policy: **Specification Required** — adding a variant requires
61/// a phase-doc amendment that updates the §9.1 table and cites the owning
62/// widget phase.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum WidgetClass {
65    /// Root screen / top-level container.
66    Screen,
67    /// Push-button widget.
68    Button,
69    /// Text label widget.
70    Label,
71    /// Generic container or panel.
72    Container,
73    /// Slider control.
74    Slider,
75    /// On/off toggle switch.
76    Switch,
77    /// Checkbox control.
78    Checkbox,
79    /// Scrollable list widget.
80    List,
81}
82
83// ---------------------------------------------------------------------------
84// LparTheme trait
85// ---------------------------------------------------------------------------
86
87/// Node-level theme chaining for the LPAR-07 cascade (§9).
88///
89/// Implementors call [`ObjectNode::add_local_style`] (or equivalent cascade
90/// methods) to inject the lowest-precedence defaults for each [`WidgetClass`].
91/// Because these are added as local styles they have Tier-1 precedence and
92/// will be beaten by any explicit per-node style overrides added afterwards.
93///
94/// # Design note
95///
96/// The intentional split from [`Theme`] is that `Theme` modifies a bare
97/// [`Style`] value (useful for resolving a standalone style outside a tree),
98/// while `LparTheme` operates on a live [`ObjectNode`] and hooks into the full
99/// cascade. Both coexist; they target different use cases.
100pub trait LparTheme {
101    /// Apply class-appropriate style defaults to `node` via the cascade.
102    ///
103    /// Implementors should call `node.add_local_style(patch, selector)` for
104    /// each property they want to default. Per the LPAR-07 §9 contract, these
105    /// calls MUST use [`Selector::part`]`(`[`Part::MAIN`]`)` for base defaults
106    /// (state-specific overrides are allowed via [`Selector::new`]).
107    fn apply_to_node(&self, node: &mut ObjectNode, class: WidgetClass);
108}
109
110// ---------------------------------------------------------------------------
111// DefaultTheme — LPAR-07 §9.3 reference implementation
112// ---------------------------------------------------------------------------
113
114/// LPAR-07 default theme: Material-inspired light palette as lowest-precedence
115/// baseline defaults (§9.3).
116///
117/// Applying this theme injects local style patches into the cascade so that
118/// any later call to [`crate::style_cascade::resolve`] starts from a
119/// principled visual foundation rather than the bare property defaults in
120/// [`Style::default`].
121///
122/// Applications that want a custom look override on top of this baseline by
123/// calling [`ObjectNode::add_local_style`] after `DefaultTheme::apply_to_node`.
124pub struct DefaultTheme;
125
126/// Primary brand color used by [`DefaultTheme`] for interactive elements.
127const DEFAULT_PRIMARY: Color = Color(98, 0, 238, 255);
128/// Surface background color used by [`DefaultTheme`].
129const DEFAULT_SURFACE: Color = Color(255, 255, 255, 255);
130/// Transparent color constant for container backgrounds.
131const TRANSPARENT: Color = Color(0, 0, 0, 0);
132/// Default text label foreground.
133const DEFAULT_ON_SURFACE: Color = Color(33, 33, 33, 255);
134/// Default corner radius for buttons.
135const BUTTON_RADIUS: u8 = 6;
136/// Default border width for buttons.
137const BUTTON_BORDER_WIDTH: u8 = 1;
138
139impl LparTheme for DefaultTheme {
140    fn apply_to_node(&self, node: &mut ObjectNode, class: WidgetClass) {
141        let sel = Selector::part(Part::MAIN);
142        // Theme defaults go in the lowest-precedence theme tier (LPAR-07 §9.1),
143        // so widget/application styles always win over the theme regardless of
144        // registration order. Clear first so a re-apply replaces cleanly.
145        node.clear_theme_styles();
146        match class {
147            WidgetClass::Button => {
148                // Buttons: primary bg, rounded corners, thin border.
149                node.add_theme_style(
150                    StylePatch {
151                        bg_color: Some(DEFAULT_PRIMARY),
152                        border_color: Some(DEFAULT_PRIMARY),
153                        border_width: Some(BUTTON_BORDER_WIDTH),
154                        radius: Some(BUTTON_RADIUS),
155                        ..StylePatch::new()
156                    },
157                    sel,
158                );
159            }
160            WidgetClass::Label => {
161                // Labels: transparent background, visible text by default.
162                // (Text color is not yet in StylePatch; set bg/alpha).
163                node.add_theme_style(
164                    StylePatch {
165                        bg_color: Some(TRANSPARENT),
166                        alpha: Some(255),
167                        border_width: Some(0),
168                        radius: Some(0),
169                        ..StylePatch::new()
170                    },
171                    sel,
172                );
173            }
174            WidgetClass::Container | WidgetClass::Screen => {
175                // Containers and screens: white surface, no border.
176                node.add_theme_style(
177                    StylePatch {
178                        bg_color: Some(DEFAULT_SURFACE),
179                        border_width: Some(0),
180                        radius: Some(0),
181                        alpha: Some(255),
182                        ..StylePatch::new()
183                    },
184                    sel,
185                );
186            }
187            WidgetClass::Slider | WidgetClass::Switch | WidgetClass::Checkbox => {
188                // Interactive controls: primary accent color.
189                node.add_theme_style(
190                    StylePatch {
191                        bg_color: Some(DEFAULT_PRIMARY),
192                        alpha: Some(255),
193                        border_width: Some(0),
194                        radius: Some(4),
195                        ..StylePatch::new()
196                    },
197                    sel,
198                );
199            }
200            WidgetClass::List => {
201                // Lists: surface background, slight border.
202                node.add_theme_style(
203                    StylePatch {
204                        bg_color: Some(DEFAULT_SURFACE),
205                        border_color: Some(DEFAULT_ON_SURFACE),
206                        border_width: Some(1),
207                        radius: Some(2),
208                        alpha: Some(255),
209                        ..StylePatch::new()
210                    },
211                    sel,
212                );
213            }
214        }
215    }
216}
217
218// ---------------------------------------------------------------------------
219// Tests
220// ---------------------------------------------------------------------------
221
222#[cfg(test)]
223mod tests {
224    use alloc::rc::Rc;
225    use core::cell::RefCell;
226
227    use super::*;
228    use crate::object::ObjectNode;
229    use crate::style_cascade::{InheritedContext, Part, resolve};
230    use crate::widget::{Rect, Widget};
231
232    struct Dummy;
233
234    impl Widget for Dummy {
235        fn bounds(&self) -> Rect {
236            Rect {
237                x: 0,
238                y: 0,
239                width: 10,
240                height: 10,
241            }
242        }
243        fn draw(&self, _r: &mut dyn crate::renderer::Renderer) {}
244        fn handle_event(&mut self, _e: &crate::event::Event) -> bool {
245            false
246        }
247    }
248
249    fn make_node() -> ObjectNode {
250        ObjectNode::new(Rc::new(RefCell::new(Dummy)))
251    }
252
253    // -----------------------------------------------------------------------
254    // Theme (original)
255    // -----------------------------------------------------------------------
256
257    #[test]
258    fn light_theme_sets_bg_white() {
259        let mut style = Style::default();
260        LightTheme.apply(&mut style);
261        assert_eq!(style.bg_color, Color(255, 255, 255, 255));
262    }
263
264    #[test]
265    fn dark_theme_sets_bg_black() {
266        let mut style = Style::default();
267        DarkTheme.apply(&mut style);
268        assert_eq!(style.bg_color, Color(0, 0, 0, 255));
269    }
270
271    // -----------------------------------------------------------------------
272    // LPAR-07 §9 DefaultTheme / LparTheme
273    // -----------------------------------------------------------------------
274
275    #[test]
276    fn default_theme_applies_to_button() {
277        let mut node = make_node();
278        DefaultTheme.apply_to_node(&mut node, WidgetClass::Button);
279        let (style, _) = resolve(
280            node.style.as_deref(),
281            node.meta().states(),
282            Part::MAIN,
283            &InheritedContext::EMPTY,
284        );
285        assert_eq!(
286            style.bg_color, DEFAULT_PRIMARY,
287            "button bg should be primary color"
288        );
289        assert_eq!(style.radius, BUTTON_RADIUS, "button radius from theme");
290        assert_eq!(
291            style.border_width, BUTTON_BORDER_WIDTH,
292            "button border width from theme"
293        );
294    }
295
296    #[test]
297    fn local_override_wins_over_theme() {
298        let mut node = make_node();
299        // Apply theme first.
300        DefaultTheme.apply_to_node(&mut node, WidgetClass::Button);
301
302        // Then add a node-local override (higher precedence, added later).
303        node.add_local_style(
304            StylePatch {
305                bg_color: Some(Color(255, 0, 0, 255)),
306                ..StylePatch::new()
307            },
308            Selector::part(Part::MAIN),
309        );
310
311        let (style, _) = resolve(
312            node.style.as_deref(),
313            node.meta().states(),
314            Part::MAIN,
315            &InheritedContext::EMPTY,
316        );
317        // The per-node red override wins over the theme's primary color.
318        assert_eq!(
319            style.bg_color,
320            Color(255, 0, 0, 255),
321            "local override must win over DefaultTheme"
322        );
323    }
324
325    #[test]
326    fn local_override_wins_regardless_of_apply_order() {
327        // Order-independence: apply the local override FIRST, then the theme.
328        // The theme lives in the lowest-precedence theme tier (§9.1), so the
329        // local override still wins — this would fail if the theme used the
330        // local tier and relied on last-added-wins ordering.
331        let mut node = make_node();
332        node.add_local_style(
333            StylePatch {
334                bg_color: Some(Color(255, 0, 0, 255)),
335                ..StylePatch::new()
336            },
337            Selector::part(Part::MAIN),
338        );
339        DefaultTheme.apply_to_node(&mut node, WidgetClass::Button);
340
341        let (style, _) = resolve(
342            node.style.as_deref(),
343            node.meta().states(),
344            Part::MAIN,
345            &InheritedContext::EMPTY,
346        );
347        assert_eq!(
348            style.bg_color,
349            Color(255, 0, 0, 255),
350            "local override must win even when the theme is applied afterward"
351        );
352    }
353
354    #[test]
355    fn default_theme_container_sets_surface_bg() {
356        let mut node = make_node();
357        DefaultTheme.apply_to_node(&mut node, WidgetClass::Container);
358        let (style, _) = resolve(
359            node.style.as_deref(),
360            node.meta().states(),
361            Part::MAIN,
362            &InheritedContext::EMPTY,
363        );
364        assert_eq!(
365            style.bg_color, DEFAULT_SURFACE,
366            "container bg should be surface white"
367        );
368    }
369}