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}