rlvgl_core/style_cascade.rs
1//! LPAR-07 style cascade substrate: `Part`, `Selector`, `StylePatch`,
2//! `StyleState`, `InheritedContext`, and top-down cascade resolution.
3//!
4//! This module implements the LPAR-07 style cascade layer that resolves
5//! `(node, part, state)` queries into a [`Style`] value for draw-time
6//! consumption. It sits *above* [`Style`] — the cascade output is a
7//! [`Style`] value assembled from per-property winner lookups, and the
8//! draw path continues to use [`Style`] unchanged.
9//!
10//! # Cascade precedence (§7.2)
11//!
12//! For a query `(node, part, property)`:
13//!
14//! 0. **Transition override** (Tier 0) — highest precedence, set during
15//! an active style transition via [`start_transition`].
16//! 1. **Local styles** — last-added wins among matching selectors.
17//! 2. **Added (shared) styles** — last-added wins among matching selectors.
18//! 3. For inheritable properties (`alpha` plus LPAR-08 text properties), take
19//! the value from the [`InheritedContext`] propagated top-down from
20//! ancestors.
21//! 4. Property default value (from [`Style::default()`]).
22//!
23//! # Top-down inheritance (§7.3)
24//!
25//! Inheritance is propagated top-down during the resolve/draw traversal.
26//! Each parent node resolves its own `alpha` and [`TextStyle`] for `MAIN`; the
27//! resolved values become the [`InheritedContext`] handed to that node's
28//! children. No parent pointer, no ancestor slice — the caller threads the
29//! context down.
30
31use alloc::rc::Rc;
32use alloc::vec::Vec;
33use core::cell::RefCell;
34
35use crate::anim::{ANIM_SCALE, Easing, Tween};
36use crate::font::FontId;
37use crate::object::ObjectStates;
38use crate::object_anim::{ObjectAnimId, ObjectAnims};
39use crate::style::Style;
40use crate::widget::{Color, Rect};
41
42// ---------------------------------------------------------------------------
43// TransitionOverride — Tier-0 override slot for active style transitions
44// ---------------------------------------------------------------------------
45
46/// Live interpolated values for properties currently undergoing a style
47/// transition (LPAR-07 §8).
48///
49/// Fields are `Some` while the corresponding property is being animated and
50/// `None` once the animation completes or is cancelled. The override is stored
51/// inside an `Rc<RefCell<TransitionOverride>>` on [`StyleState`] so that
52/// animation apply closures captured during [`start_transition`] can write into
53/// it without holding a borrow on the owning [`crate::object::ObjectNode`].
54#[derive(Debug, Default, Clone)]
55pub struct TransitionOverride {
56 /// Overridden background color during a transition.
57 pub bg_color: Option<Color>,
58 /// Overridden border color during a transition.
59 pub border_color: Option<Color>,
60 /// Overridden border width during a transition.
61 pub border_width: Option<u8>,
62 /// Overridden alpha during a transition.
63 pub alpha: Option<u8>,
64 /// Overridden corner radius during a transition.
65 pub radius: Option<u8>,
66}
67
68// ---------------------------------------------------------------------------
69// AnimProp / AnimPropValue
70// ---------------------------------------------------------------------------
71
72/// Identifies which style property a transition animates (LPAR-07 §8.1).
73///
74/// Registration policy: **Specification Required** — adding a variant requires
75/// a phase-doc amendment to the §8.1 table.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum AnimProp {
78 /// The background color (`Style::bg_color`).
79 BgColor,
80 /// The border color (`Style::border_color`).
81 BorderColor,
82 /// The border width in pixels (`Style::border_width`).
83 BorderWidth,
84 /// The alpha / opacity (`Style::alpha`).
85 Alpha,
86 /// The corner radius in pixels (`Style::radius`).
87 Radius,
88}
89
90/// Value of an animatable style property for transition endpoints (LPAR-07 §8.1).
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum AnimPropValue {
93 /// A color-typed property value.
94 Color(Color),
95 /// A scalar property value (border width, alpha, or radius).
96 Scalar(u8),
97}
98
99// ---------------------------------------------------------------------------
100// TransitionDesc
101// ---------------------------------------------------------------------------
102
103/// Parameters describing a style transition animation (LPAR-07 §8.2).
104#[derive(Debug, Clone, Copy)]
105pub struct TransitionDesc {
106 /// Duration of the transition in ticks.
107 ///
108 /// A value of `0` snaps the property immediately to the `to` value.
109 pub duration_ticks: u32,
110 /// Delay before the transition begins (ticks).
111 pub delay_ticks: u32,
112 /// Easing curve applied to the progress value.
113 pub easing: Easing,
114}
115
116// ---------------------------------------------------------------------------
117// Part
118// ---------------------------------------------------------------------------
119
120/// Sub-region or visual component of a widget used as a style selector key.
121///
122/// `Part` is a transparent newtype over `u32`. Named constants mirror LVGL's
123/// `lv_part_t` vocabulary. Custom part ids are allocated in the `id >= 8`
124/// range via [`Part::custom`].
125///
126/// Registration policy: **Specification Required**. Adding a named constant
127/// requires a phase-doc amendment that updates the §6.1 table and cites the
128/// owning widget phase.
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130#[repr(transparent)]
131pub struct Part(pub u32);
132
133impl Part {
134 /// The main body of the widget (`LV_PART_MAIN = 0`).
135 pub const MAIN: Self = Self(0);
136 /// The scroll-bar of a scrollable container (`LV_PART_SCROLLBAR = 1`).
137 pub const SCROLLBAR: Self = Self(1);
138 /// The filled portion of a slider or progress bar (`LV_PART_INDICATOR = 2`).
139 pub const INDICATOR: Self = Self(2);
140 /// The draggable thumb of a slider or knob widget (`LV_PART_KNOB = 3`).
141 pub const KNOB: Self = Self(3);
142 /// The currently selected item in a list or similar widget (`LV_PART_SELECTED = 4`).
143 pub const SELECTED: Self = Self(4);
144 /// Individual items inside a list or roller (`LV_PART_ITEMS = 5`).
145 pub const ITEMS: Self = Self(5);
146 /// The text-input cursor (`LV_PART_CURSOR = 6`).
147 pub const CURSOR: Self = Self(6);
148
149 /// Construct a custom part id.
150 ///
151 /// By convention `id >= 8` to avoid collisions with the named constants
152 /// above (mirroring `LV_PART_CUSTOM_FIRST`). Widget phases use this to
153 /// reserve widget-local part ids without a Specification Required amendment.
154 ///
155 /// # Example
156 ///
157 /// ```
158 /// use rlvgl_core::style_cascade::Part;
159 /// const MY_PART: Part = Part::custom(8);
160 /// ```
161 pub const fn custom(id: u32) -> Self {
162 Self(id)
163 }
164}
165
166// ---------------------------------------------------------------------------
167// TextAlign / TextStyle
168// ---------------------------------------------------------------------------
169
170/// Horizontal text alignment resolved by the style cascade.
171#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
172pub enum TextAlign {
173 /// Align text to the left edge.
174 #[default]
175 Left,
176 /// Align text to the horizontal center.
177 Center,
178 /// Align text to the right edge.
179 Right,
180 /// Resolve automatically from paragraph direction; v1 maps this to left.
181 Auto,
182}
183
184/// Fully resolved inheritable text properties.
185///
186/// This is the LPAR-08 text companion to the existing visual [`Style`].
187/// Keeping it separate preserves the frozen public-field shape of [`Style`].
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189pub struct TextStyle {
190 /// Text color.
191 pub text_color: Color,
192 /// Font registry identifier resolved by the display/platform at draw time.
193 pub font_id: FontId,
194 /// Extra pixels inserted between shaped glyphs.
195 pub letter_spacing: i8,
196 /// Extra pixels inserted between wrapped lines.
197 pub line_spacing: i8,
198 /// Horizontal text alignment.
199 pub text_align: TextAlign,
200}
201
202impl Default for TextStyle {
203 fn default() -> Self {
204 Self {
205 text_color: Color(0, 0, 0, 255),
206 font_id: FontId::DEFAULT,
207 letter_spacing: 0,
208 line_spacing: 0,
209 text_align: TextAlign::Left,
210 }
211 }
212}
213
214// ---------------------------------------------------------------------------
215// Selector
216// ---------------------------------------------------------------------------
217
218/// A `(Part, ObjectStates mask)` selector stored alongside a style patch.
219///
220/// A selector *matches* a query `(part, node_states)` when:
221///
222/// - `self.part == part`, **and**
223/// - `(node_states & self.states) == self.states` — i.e. every state bit in
224/// the selector is set on the node.
225///
226/// A selector constructed with [`ObjectStates::DEFAULT`] (bits == 0) as the
227/// state mask matches **any** node regardless of which state bits are set
228/// (§6.3, the DEFAULT-matches-any rule).
229#[derive(Debug, Clone, Copy, PartialEq, Eq)]
230pub struct Selector {
231 /// The widget part this selector targets.
232 pub part: Part,
233 /// The required state bits. Zero (`DEFAULT`) matches any state.
234 pub states: ObjectStates,
235}
236
237impl Selector {
238 /// Create a selector for `part` that matches **any** state (DEFAULT mask).
239 ///
240 /// Use this as a base/always-on style entry for a given part.
241 pub const fn part(part: Part) -> Self {
242 Self {
243 part,
244 states: ObjectStates::DEFAULT,
245 }
246 }
247
248 /// Create a selector for a specific `(part, states)` combination.
249 pub const fn new(part: Part, states: ObjectStates) -> Self {
250 Self { part, states }
251 }
252
253 /// Return `true` when this selector matches the given `(part, node_states)` query.
254 ///
255 /// Matching rules (§6.3):
256 /// - parts must be equal.
257 /// - every bit in `self.states` must be set in `node_states`; a zero mask
258 /// (`DEFAULT`) satisfies this for any `node_states`.
259 pub const fn matches(&self, part: Part, node_states: ObjectStates) -> bool {
260 self.part.0 == part.0 && node_states.contains(self.states)
261 }
262}
263
264// ---------------------------------------------------------------------------
265// StylePatch
266// ---------------------------------------------------------------------------
267
268/// A *partial* style entry: a per-property override record added to a node.
269///
270/// Each field is an `Option` — only fields that carry a `Some` value override
271/// the lower-precedence or default value. This is the LVGL `lv_style_t`
272/// analogue: a patch carries only the properties it intends to change.
273///
274/// Use [`StylePatch::builder`] (or set fields directly) to construct patches.
275/// The cascade merges patches per-property — a patch that sets only `bg_color`
276/// does not affect `border_color`, `alpha`, etc.
277///
278/// # Distinction from [`Style`]
279///
280/// [`Style`] (in `core::style`) is the *resolved*, fully-materialised property
281/// bag used at draw time. `StylePatch` is the sparse "intent" stored in the
282/// cascade; it is resolved into a [`Style`] by [`resolve`].
283///
284/// # LPAR-10 layout fields (§5.G)
285///
286/// The `padding_*`, `margin_*`, `gap_row`, and `gap_col` fields are additive
287/// extensions for layout-related style properties. They are resolved into a
288/// [`crate::layout::LayoutStyle`] resolved struct by
289/// [`crate::layout::resolve_layout_style`]. The frozen 5-field
290/// [`crate::style::Style`] is **NOT** extended by LPAR-10.
291///
292/// Registration policy for new named layout properties: **Standards Action**
293/// (cross-phase style contracts).
294#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
295pub struct StylePatch {
296 /// Override for `Style::bg_color`.
297 pub bg_color: Option<Color>,
298 /// Override for `Style::border_color`.
299 pub border_color: Option<Color>,
300 /// Override for `Style::border_width`.
301 pub border_width: Option<u8>,
302 /// Override for `Style::alpha`.
303 ///
304 /// `alpha` is the one **inheritable** property in v1 (§7.3). When no
305 /// patch in the cascade supplies a value, the resolved value falls back to
306 /// the [`InheritedContext`] handed down from the parent node.
307 pub alpha: Option<u8>,
308 /// Override for `Style::radius`.
309 pub radius: Option<u8>,
310 /// Override for [`TextStyle::text_color`].
311 pub text_color: Option<Color>,
312 /// Override for [`TextStyle::font_id`].
313 pub font_id: Option<FontId>,
314 /// Override for [`TextStyle::letter_spacing`].
315 pub letter_spacing: Option<i8>,
316 /// Override for [`TextStyle::line_spacing`].
317 pub line_spacing: Option<i8>,
318 /// Override for [`TextStyle::text_align`].
319 pub text_align: Option<TextAlign>,
320
321 // --- LPAR-10 layout properties (§5.G) ---
322 /// Top padding in pixels (resolves to `0` when `None`).
323 pub padding_top: Option<i32>,
324 /// Bottom padding in pixels (resolves to `0` when `None`).
325 pub padding_bottom: Option<i32>,
326 /// Left padding in pixels (resolves to `0` when `None`).
327 pub padding_left: Option<i32>,
328 /// Right padding in pixels (resolves to `0` when `None`).
329 pub padding_right: Option<i32>,
330 /// Top margin in pixels (resolves to `0` when `None`).
331 pub margin_top: Option<i32>,
332 /// Bottom margin in pixels (resolves to `0` when `None`).
333 pub margin_bottom: Option<i32>,
334 /// Left margin in pixels (resolves to `0` when `None`).
335 pub margin_left: Option<i32>,
336 /// Right margin in pixels (resolves to `0` when `None`).
337 pub margin_right: Option<i32>,
338 /// Row gap (spacing between rows / cross-axis tracks) in pixels.
339 pub gap_row: Option<i32>,
340 /// Column gap (spacing between columns / main-axis items) in pixels.
341 pub gap_col: Option<i32>,
342}
343
344impl StylePatch {
345 /// Return a new empty patch (all fields `None`).
346 pub const fn new() -> Self {
347 Self {
348 bg_color: None,
349 border_color: None,
350 border_width: None,
351 alpha: None,
352 radius: None,
353 text_color: None,
354 font_id: None,
355 letter_spacing: None,
356 line_spacing: None,
357 text_align: None,
358 padding_top: None,
359 padding_bottom: None,
360 padding_left: None,
361 padding_right: None,
362 margin_top: None,
363 margin_bottom: None,
364 margin_left: None,
365 margin_right: None,
366 gap_row: None,
367 gap_col: None,
368 }
369 }
370
371 /// Return a builder for constructing a patch via method chaining.
372 pub fn builder() -> StylePatchBuilder {
373 StylePatchBuilder(Self::new())
374 }
375}
376
377/// Builder for [`StylePatch`] values.
378#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
379pub struct StylePatchBuilder(StylePatch);
380
381impl StylePatchBuilder {
382 /// Create a new builder starting from an empty patch.
383 pub fn new() -> Self {
384 Self(StylePatch::new())
385 }
386
387 /// Set the background color override.
388 pub fn bg_color(mut self, color: Color) -> Self {
389 self.0.bg_color = Some(color);
390 self
391 }
392
393 /// Set the border color override.
394 pub fn border_color(mut self, color: Color) -> Self {
395 self.0.border_color = Some(color);
396 self
397 }
398
399 /// Set the border width override.
400 pub fn border_width(mut self, w: u8) -> Self {
401 self.0.border_width = Some(w);
402 self
403 }
404
405 /// Set the alpha (opacity) override.
406 pub fn alpha(mut self, a: u8) -> Self {
407 self.0.alpha = Some(a);
408 self
409 }
410
411 /// Set the corner radius override.
412 pub fn radius(mut self, r: u8) -> Self {
413 self.0.radius = Some(r);
414 self
415 }
416
417 /// Set the text color override.
418 pub fn text_color(mut self, color: Color) -> Self {
419 self.0.text_color = Some(color);
420 self
421 }
422
423 /// Set the font registry identifier override.
424 pub fn font_id(mut self, font_id: FontId) -> Self {
425 self.0.font_id = Some(font_id);
426 self
427 }
428
429 /// Set the letter spacing override in pixels.
430 pub fn letter_spacing(mut self, spacing: i8) -> Self {
431 self.0.letter_spacing = Some(spacing);
432 self
433 }
434
435 /// Set the line spacing override in pixels.
436 pub fn line_spacing(mut self, spacing: i8) -> Self {
437 self.0.line_spacing = Some(spacing);
438 self
439 }
440
441 /// Set the text alignment override.
442 pub fn text_align(mut self, align: TextAlign) -> Self {
443 self.0.text_align = Some(align);
444 self
445 }
446
447 /// Consume the builder and return the constructed [`StylePatch`].
448 pub fn build(self) -> StylePatch {
449 self.0
450 }
451}
452
453// ---------------------------------------------------------------------------
454// InheritedContext
455// ---------------------------------------------------------------------------
456
457/// Inheritable property values threaded top-down through the resolve/draw
458/// descent (§7.3).
459///
460/// At the root the context is empty (`InheritedContext::EMPTY`). After
461/// resolving a node's `MAIN` properties, the caller constructs a *child
462/// context* by calling [`InheritedContext::with_resolved`] and passes that
463/// to each child's resolver call.
464///
465/// LPAR-08 extends the inherited set with text properties while preserving the
466/// existing visual [`Style`] shape.
467///
468/// # No parent pointer
469///
470/// This type exists precisely *because* `ObjectNode` has no parent pointer
471/// (LPAR-02 / LPAR-04 §7.2). The context travels down the call stack during
472/// the traversal; a node never needs to walk upward.
473#[derive(Debug, Clone, Copy, PartialEq, Eq)]
474pub struct InheritedContext {
475 /// Effective `alpha` from the nearest ancestor that resolved one, or `None`
476 /// if no ancestor has set an alpha (falls to the §7.4 default `255`).
477 pub alpha: Option<u8>,
478 /// Effective inherited text color.
479 pub text_color: Option<Color>,
480 /// Effective inherited font identifier.
481 pub font_id: Option<FontId>,
482 /// Effective inherited letter spacing.
483 pub letter_spacing: Option<i8>,
484 /// Effective inherited line spacing.
485 pub line_spacing: Option<i8>,
486 /// Effective inherited text alignment.
487 pub text_align: Option<TextAlign>,
488}
489
490impl InheritedContext {
491 /// An empty context — no ancestor has supplied any inheritable value.
492 ///
493 /// Pass this as the root context in a top-down traversal.
494 pub const EMPTY: Self = Self {
495 alpha: None,
496 text_color: None,
497 font_id: None,
498 letter_spacing: None,
499 line_spacing: None,
500 text_align: None,
501 };
502
503 /// Construct the context to pass to a node's children.
504 ///
505 /// `resolved_alpha` is the `alpha` the **current** node resolved (the
506 /// value from the cascade, or the inherited value if no patch overrides it,
507 /// or the §7.4 default). Children inherit this value if they carry no
508 /// alpha override of their own.
509 pub const fn with_resolved(resolved_alpha: u8) -> Self {
510 let text = TextStyle {
511 text_color: Color(0, 0, 0, 255),
512 font_id: FontId::DEFAULT,
513 letter_spacing: 0,
514 line_spacing: 0,
515 text_align: TextAlign::Left,
516 };
517 Self::with_resolved_styles(resolved_alpha, text)
518 }
519
520 /// Construct the context to pass to children after resolving both visual
521 /// and text properties for the current node.
522 pub const fn with_resolved_styles(resolved_alpha: u8, resolved_text: TextStyle) -> Self {
523 Self {
524 alpha: Some(resolved_alpha),
525 text_color: Some(resolved_text.text_color),
526 font_id: Some(resolved_text.font_id),
527 letter_spacing: Some(resolved_text.letter_spacing),
528 line_spacing: Some(resolved_text.line_spacing),
529 text_align: Some(resolved_text.text_align),
530 }
531 }
532}
533
534// ---------------------------------------------------------------------------
535// StyleState (the per-node additive slot)
536// ---------------------------------------------------------------------------
537
538/// Per-node style slot holding local and shared style entries (LPAR-07 §7.1).
539///
540/// This type is stored in an `Option<Box<StyleState>>` on [`ObjectNode`]
541/// (the `style` field), following the same lazy-alloc pattern as
542/// `ScrollState` (LPAR-05) and `NodeAnimSet` (LPAR-06). The slot is `None`
543/// for nodes that carry no style overrides, keeping `ObjectNode` small.
544///
545/// Style entries are keyed by [`Selector`] and carry a [`StylePatch`].
546///
547/// - **Transition override** (Tier 0) — highest precedence; written by
548/// active [`start_transition`] animations.
549/// - **Local entries** are owned by the node (`add_local_style`). They take
550/// priority over added entries.
551/// - **Added (shared) entries** are `'static` references (`add_style`).
552/// Lower precedence than local entries.
553///
554/// Within each tier, the last-added entry wins for a given matching selector
555/// (§7.2 reverse-registration-order rule).
556pub struct StyleState {
557 /// Local style entries: `(selector, patch)` in registration order.
558 local: Vec<(Selector, StylePatch)>,
559 /// Added/shared style references: `(selector, patch)` in registration order.
560 added: Vec<(Selector, &'static StylePatch)>,
561 /// Default-theme entries (LPAR-07 §9.1): owned `(selector, patch)` applied
562 /// at the **lowest** precedence — below local and added styles — so widget
563 /// and application styles always win regardless of registration order.
564 theme: Vec<(Selector, StylePatch)>,
565 /// Tier-0 transition override slot.
566 ///
567 /// Shared with animation apply closures via `Rc<RefCell<...>>` so that
568 /// closures can write interpolated values without holding a borrow on
569 /// the owning node.
570 pub(crate) transition_override: Rc<RefCell<TransitionOverride>>,
571}
572
573impl StyleState {
574 /// Create an empty style state with an empty transition override slot.
575 pub fn new() -> Self {
576 Self {
577 local: Vec::new(),
578 added: Vec::new(),
579 theme: Vec::new(),
580 transition_override: Rc::new(RefCell::new(TransitionOverride::default())),
581 }
582 }
583
584 /// Return a cloned handle to the transition override slot.
585 ///
586 /// Callers (typically [`start_transition`]) clone this handle so that
587 /// animation apply closures can share ownership of the override cell
588 /// without holding a borrow on the [`crate::object::ObjectNode`].
589 pub fn transition_override_handle(&self) -> Rc<RefCell<TransitionOverride>> {
590 Rc::clone(&self.transition_override)
591 }
592
593 // -----------------------------------------------------------------------
594 // LPAR-10: read-only accessors for layout cascade resolution
595 // -----------------------------------------------------------------------
596
597 /// Return the local style entries as a `(Selector, StylePatch)` slice.
598 ///
599 /// Used by [`crate::layout::resolve_layout_style`] to iterate local entries
600 /// in precedence order.
601 pub fn local_entries(&self) -> &[(Selector, StylePatch)] {
602 &self.local
603 }
604
605 /// Return the added (shared) style entries as a `(Selector, &StylePatch)` slice.
606 ///
607 /// Used by [`crate::layout::resolve_layout_style`] to iterate added entries
608 /// in precedence order.
609 pub fn added_entries(&self) -> &[(Selector, &'static StylePatch)] {
610 &self.added
611 }
612}
613
614impl Default for StyleState {
615 fn default() -> Self {
616 Self::new()
617 }
618}
619
620// ---------------------------------------------------------------------------
621// ObjectNode style methods (free functions operating on StyleState)
622// ---------------------------------------------------------------------------
623// These are not `impl ObjectNode` here to avoid a circular dependency between
624// modules; ObjectNode adds the `pub(crate) style: Option<Box<StyleState>>`
625// field and calls these free functions, or the methods are placed in object.rs
626// directly (see below — we expose them via object.rs additions).
627// Here we expose the free-function primitives so `object.rs` can delegate.
628
629/// Add a locally owned style patch on a [`StyleState`].
630///
631/// Exposed so `ObjectNode::add_local_style` can delegate to this without
632/// exposing `StyleState`'s internals at the object level.
633pub fn push_local(state: &mut StyleState, patch: StylePatch, selector: Selector) {
634 state.local.push((selector, patch));
635}
636
637/// Add a shared (added) style patch reference on a [`StyleState`].
638pub fn push_added(state: &mut StyleState, patch: &'static StylePatch, selector: Selector) {
639 state.added.push((selector, patch));
640}
641
642/// Add a default-theme style patch on a [`StyleState`] (LPAR-07 §9.1).
643///
644/// Theme entries resolve at the lowest style precedence — below local and
645/// added styles — so a widget or application style always wins over the
646/// theme regardless of which was registered first.
647pub fn push_theme(state: &mut StyleState, patch: StylePatch, selector: Selector) {
648 state.theme.push((selector, patch));
649}
650
651/// Remove all default-theme entries on a [`StyleState`].
652///
653/// Used to replace a theme (re-apply) without leaving stale entries; returns
654/// the number of entries cleared.
655pub fn clear_theme(state: &mut StyleState) -> usize {
656 let n = state.theme.len();
657 state.theme.clear();
658 n
659}
660
661/// Remove local style entries whose selector matches `(part, states)`.
662///
663/// Matching uses the same predicate as [`Selector::matches`]. Returns the
664/// number of entries removed.
665///
666/// Pass [`ObjectStates::DEFAULT`] with a wildcard intent: any selector whose
667/// *part* matches and whose *state mask* is contained by `states` will be
668/// removed. For a full "remove all" for a part, use
669/// [`remove_all_local_by_part`]. For an unconditional full clear, use
670/// [`remove_all_local`].
671pub fn remove_local_matching(state: &mut StyleState, part: Part, states: ObjectStates) -> usize {
672 let before = state.local.len();
673 state.local.retain(|(sel, _)| !sel.matches(part, states));
674 before - state.local.len()
675}
676
677/// Remove all local style entries for the given `part` regardless of state mask.
678///
679/// This is the "remove all local styles for a part" wildcard form described in
680/// §7.5.
681pub fn remove_all_local_by_part(state: &mut StyleState, part: Part) -> usize {
682 let before = state.local.len();
683 state.local.retain(|(sel, _)| sel.part != part);
684 before - state.local.len()
685}
686
687/// Remove all local style entries on a node (full clear).
688pub fn remove_all_local(state: &mut StyleState) -> usize {
689 let count = state.local.len();
690 state.local.clear();
691 count
692}
693
694// ---------------------------------------------------------------------------
695// resolve_styles / resolve — cascade resolution for (node-states, part, inherited_ctx)
696// ---------------------------------------------------------------------------
697
698/// Fully resolved visual and text styles for one cascade query.
699#[derive(Debug, Clone, Copy, PartialEq, Eq)]
700pub struct ResolvedStyles {
701 /// Fully materialized visual style for draw-time background/border use.
702 pub style: Style,
703 /// Fully materialized inheritable text style.
704 pub text: TextStyle,
705 /// Context to pass to this node's children during a top-down traversal.
706 pub child_context: InheritedContext,
707}
708
709#[derive(Debug, Default)]
710struct CascadeWinners {
711 bg_color: Option<Color>,
712 border_color: Option<Color>,
713 border_width: Option<u8>,
714 alpha: Option<u8>,
715 radius: Option<u8>,
716 text_color: Option<Color>,
717 font_id: Option<FontId>,
718 letter_spacing: Option<i8>,
719 line_spacing: Option<i8>,
720 text_align: Option<TextAlign>,
721}
722
723impl CascadeWinners {
724 fn fill_from_patch(&mut self, patch: &StylePatch) {
725 if self.bg_color.is_none() {
726 self.bg_color = patch.bg_color;
727 }
728 if self.border_color.is_none() {
729 self.border_color = patch.border_color;
730 }
731 if self.border_width.is_none() {
732 self.border_width = patch.border_width;
733 }
734 if self.alpha.is_none() {
735 self.alpha = patch.alpha;
736 }
737 if self.radius.is_none() {
738 self.radius = patch.radius;
739 }
740 if self.text_color.is_none() {
741 self.text_color = patch.text_color;
742 }
743 if self.font_id.is_none() {
744 self.font_id = patch.font_id;
745 }
746 if self.letter_spacing.is_none() {
747 self.letter_spacing = patch.letter_spacing;
748 }
749 if self.line_spacing.is_none() {
750 self.line_spacing = patch.line_spacing;
751 }
752 if self.text_align.is_none() {
753 self.text_align = patch.text_align;
754 }
755 }
756}
757
758/// Resolve visual [`Style`] and inheritable [`TextStyle`] for one query.
759///
760/// This is the LPAR-08 text-aware cascade entry point. It implements LPAR-07
761/// precedence for the existing visual properties and extends the same
762/// top-down inheritance model to text color, font id, spacing, and alignment.
763pub fn resolve_styles(
764 node_style: Option<&StyleState>,
765 node_states: ObjectStates,
766 part: Part,
767 inherited: &InheritedContext,
768) -> ResolvedStyles {
769 // Walk the tiers in precedence order (transition override first, then
770 // local, then added), in reverse-registration order within each tier
771 // (last added wins). We collect the first `Some` for each property.
772 let mut winners = CascadeWinners::default();
773
774 if let Some(s) = node_style {
775 // --- Tier 0: transition override (highest precedence) ---
776 {
777 let ov = s.transition_override.borrow();
778 if ov.bg_color.is_some() {
779 winners.bg_color = ov.bg_color;
780 }
781 if ov.border_color.is_some() {
782 winners.border_color = ov.border_color;
783 }
784 if ov.border_width.is_some() {
785 winners.border_width = ov.border_width;
786 }
787 if ov.alpha.is_some() {
788 winners.alpha = ov.alpha;
789 }
790 if ov.radius.is_some() {
791 winners.radius = ov.radius;
792 }
793 }
794
795 // --- Tier 1: local entries (reverse registration order = last added wins) ---
796 for (sel, patch) in s.local.iter().rev() {
797 if sel.matches(part, node_states) {
798 winners.fill_from_patch(patch);
799 }
800 }
801
802 // --- Tier 2: added entries (reverse registration order) ---
803 for (sel, patch) in s.added.iter().rev() {
804 if sel.matches(part, node_states) {
805 winners.fill_from_patch(patch);
806 }
807 }
808
809 // --- Tier 3: default-theme entries (LPAR-07 §9.1, lowest style tier) ---
810 // Consulted below local and added styles so widget/application styles
811 // always win over the theme regardless of registration order.
812 for (sel, patch) in s.theme.iter().rev() {
813 if sel.matches(part, node_states) {
814 winners.fill_from_patch(patch);
815 }
816 }
817 }
818
819 // --- Tier 4: take inheritable values from inherited context ---
820 if winners.alpha.is_none() {
821 winners.alpha = inherited.alpha;
822 }
823 if winners.text_color.is_none() {
824 winners.text_color = inherited.text_color;
825 }
826 if winners.font_id.is_none() {
827 winners.font_id = inherited.font_id;
828 }
829 if winners.letter_spacing.is_none() {
830 winners.letter_spacing = inherited.letter_spacing;
831 }
832 if winners.line_spacing.is_none() {
833 winners.line_spacing = inherited.line_spacing;
834 }
835 if winners.text_align.is_none() {
836 winners.text_align = inherited.text_align;
837 }
838
839 // --- Tier 5: property defaults (§7.4) ---
840 let defaults = Style::default();
841 let text_defaults = TextStyle::default();
842 let style = Style {
843 bg_color: winners.bg_color.unwrap_or(defaults.bg_color),
844 border_color: winners.border_color.unwrap_or(defaults.border_color),
845 border_width: winners.border_width.unwrap_or(defaults.border_width),
846 alpha: winners.alpha.unwrap_or(defaults.alpha),
847 radius: winners.radius.unwrap_or(defaults.radius),
848 };
849 let text = TextStyle {
850 text_color: winners.text_color.unwrap_or(text_defaults.text_color),
851 font_id: winners.font_id.unwrap_or(text_defaults.font_id),
852 letter_spacing: winners
853 .letter_spacing
854 .unwrap_or(text_defaults.letter_spacing),
855 line_spacing: winners.line_spacing.unwrap_or(text_defaults.line_spacing),
856 text_align: winners.text_align.unwrap_or(text_defaults.text_align),
857 };
858
859 let child_context = InheritedContext::with_resolved_styles(style.alpha, text);
860
861 ResolvedStyles {
862 style,
863 text,
864 child_context,
865 }
866}
867
868/// Resolve the visual cascade for a given `(node_style, node_states, part)` query.
869///
870/// This compatibility wrapper preserves the LPAR-07 return shape for existing
871/// callers. New text-aware callers should use [`resolve_styles`].
872pub fn resolve(
873 node_style: Option<&StyleState>,
874 node_states: ObjectStates,
875 part: Part,
876 inherited: &InheritedContext,
877) -> (Style, InheritedContext) {
878 let resolved = resolve_styles(node_style, node_states, part, inherited);
879 (resolved.style, resolved.child_context)
880}
881
882// ---------------------------------------------------------------------------
883// start_transition / cancel_transition — LPAR-07 §8
884// ---------------------------------------------------------------------------
885
886/// Begin a style transition on `node` for property `prop`.
887///
888/// The transition interpolates `from → to` according to `desc` using
889/// [`ObjectAnims::bind`]. While the animation runs, each tick writes the
890/// interpolated value into the node's [`TransitionOverride`] (Tier-0 cascade
891/// slot), so that subsequent calls to [`resolve`] return the in-flight value
892/// at highest precedence.
893///
894/// When the tween finishes naturally, the property's override slot is cleared
895/// so the cascade falls back to the layers below (local styles, added styles,
896/// defaults). If you wish to stop an in-flight transition early, call
897/// [`cancel_transition`] with the returned [`ObjectAnimId`].
898///
899/// # Arguments
900///
901/// - `node` — the target node. If it carries no style slot yet, one is
902/// allocated lazily.
903/// - `anims` — the `ObjectAnims` id allocator / walker for this tree.
904/// - `prop` — which property to animate.
905/// - `from` — starting value (must match the type expected for `prop`).
906/// - `to` — ending value (must match the type expected for `prop`).
907/// - `desc` — timing and easing parameters.
908/// - `node_bounds` — the dirty rect returned by each apply tick so that
909/// callers can route it into an invalidation planner.
910///
911/// # Type contract
912///
913/// `from` and `to` must both be [`AnimPropValue::Color`] when `prop` is
914/// `BgColor` or `BorderColor`, and both [`AnimPropValue::Scalar`] when `prop`
915/// is `BorderWidth`, `Alpha`, or `Radius`. Mismatched types produce a
916/// `Some(node_bounds)` dirty rect every tick but leave the property at its
917/// `from` value (the interpolation falls through to the scalar branch which
918/// returns 0 for both `Color` variants — treat this as a programming error
919/// to be caught in tests).
920pub fn start_transition(
921 node: &mut crate::object::ObjectNode,
922 anims: &mut ObjectAnims,
923 prop: AnimProp,
924 from: AnimPropValue,
925 to: AnimPropValue,
926 desc: TransitionDesc,
927 node_bounds: Rect,
928) -> ObjectAnimId {
929 // 1. Lazily allocate the style slot so the Rc handle is always present.
930 node.style
931 .get_or_insert_with(|| alloc::boxed::Box::new(StyleState::new()));
932
933 // 2. Clone the Rc handle *before* borrowing node mutably through bind().
934 let override_rc = Rc::clone(
935 &node
936 .style
937 .as_ref()
938 .expect("just inserted above")
939 .transition_override,
940 );
941
942 // 3. Build the tween (0 → ANIM_SCALE over duration_ticks).
943 let tween = Tween::new(0, ANIM_SCALE, desc.duration_ticks).with_easing(desc.easing);
944
945 // 4. Build the apply closure. Captures `override_rc`, `prop`, `from`, `to`.
946 let apply_rc = Rc::clone(&override_rc);
947 let apply: alloc::boxed::Box<dyn FnMut(i32) -> Option<Rect>> =
948 alloc::boxed::Box::new(move |v: i32| {
949 let interpolated = interpolate_prop(from, to, v);
950 let mut ov = apply_rc.borrow_mut();
951 write_override(&mut ov, prop, interpolated);
952 Some(node_bounds)
953 });
954
955 // 5. Build the on_complete closure: clears the override for this property.
956 let complete_rc = Rc::clone(&override_rc);
957 let on_complete: alloc::boxed::Box<dyn FnOnce()> = alloc::boxed::Box::new(move || {
958 let mut ov = complete_rc.borrow_mut();
959 clear_override(&mut ov, prop);
960 });
961
962 anims.bind(node, tween, apply, desc.delay_ticks, Some(on_complete))
963}
964
965/// Cancel an in-flight style transition and clear the property's override slot.
966///
967/// Calls [`ObjectAnims::cancel`] (which removes the entry without firing
968/// `on_complete`), then explicitly clears `prop` in the transition override so
969/// that subsequent [`resolve`] calls fall through to the lower cascade tiers.
970///
971/// Returns `true` if the animation was found and removed, `false` if the id
972/// was unknown or already completed.
973pub fn cancel_transition(
974 node: &mut crate::object::ObjectNode,
975 anims: &mut ObjectAnims,
976 old_id: ObjectAnimId,
977 prop: AnimProp,
978) -> bool {
979 // Cancel the animation entry (no on_complete fired).
980 let found = anims.cancel(node, old_id);
981
982 // Always clear the override slot regardless of whether the id was still live
983 // — if the animation completed naturally between the call to start_transition
984 // and this call, the slot is already cleared, so borrow + clear is harmless.
985 if let Some(style) = node.style.as_ref() {
986 let mut ov = style.transition_override.borrow_mut();
987 clear_override(&mut ov, prop);
988 }
989
990 found
991}
992
993// ---------------------------------------------------------------------------
994// Interpolation helpers (private)
995// ---------------------------------------------------------------------------
996
997/// Interpolate between two [`AnimPropValue`]s at progress `v` (0..=ANIM_SCALE).
998fn interpolate_prop(from: AnimPropValue, to: AnimPropValue, v: i32) -> AnimPropValue {
999 match (from, to) {
1000 (AnimPropValue::Color(c_from), AnimPropValue::Color(c_to)) => {
1001 AnimPropValue::Color(c_from.lerp(c_to, v, ANIM_SCALE))
1002 }
1003 (AnimPropValue::Scalar(s_from), AnimPropValue::Scalar(s_to)) => {
1004 let interp = (s_from as i32 + (s_to as i32 - s_from as i32) * v / ANIM_SCALE)
1005 .clamp(0, 255) as u8;
1006 AnimPropValue::Scalar(interp)
1007 }
1008 // Type mismatch: fall through to the from value unchanged.
1009 (from, _) => from,
1010 }
1011}
1012
1013/// Write an interpolated value into the appropriate override slot.
1014fn write_override(ov: &mut TransitionOverride, prop: AnimProp, value: AnimPropValue) {
1015 match (prop, value) {
1016 (AnimProp::BgColor, AnimPropValue::Color(c)) => ov.bg_color = Some(c),
1017 (AnimProp::BorderColor, AnimPropValue::Color(c)) => ov.border_color = Some(c),
1018 (AnimProp::BorderWidth, AnimPropValue::Scalar(s)) => ov.border_width = Some(s),
1019 (AnimProp::Alpha, AnimPropValue::Scalar(s)) => ov.alpha = Some(s),
1020 (AnimProp::Radius, AnimPropValue::Scalar(s)) => ov.radius = Some(s),
1021 _ => {}
1022 }
1023}
1024
1025/// Clear the override slot for the given property.
1026fn clear_override(ov: &mut TransitionOverride, prop: AnimProp) {
1027 match prop {
1028 AnimProp::BgColor => ov.bg_color = None,
1029 AnimProp::BorderColor => ov.border_color = None,
1030 AnimProp::BorderWidth => ov.border_width = None,
1031 AnimProp::Alpha => ov.alpha = None,
1032 AnimProp::Radius => ov.radius = None,
1033 }
1034}
1035
1036/// Convenience top-down traversal that resolves each node and calls `visitor`.
1037///
1038/// Descends `root → children` threading the [`InheritedContext`] through the
1039/// recursion. For each node, `visitor` is called with a reference to the
1040/// node's [`crate::object::ObjectNode`] and the resolved [`Style`] for
1041/// `Part::MAIN`. The child context produced for each node is passed to all of
1042/// its children.
1043///
1044/// This demonstrates the §7.3 top-down inheritance mechanism and provides
1045/// LPAR-08/draw with a ready entry point. For per-part draw calls you should
1046/// call [`resolve`] directly with the desired `Part`.
1047pub fn resolve_tree<F>(root: &crate::object::ObjectNode, visitor: &mut F)
1048where
1049 F: FnMut(&crate::object::ObjectNode, &Style),
1050{
1051 resolve_tree_inner(root, &InheritedContext::EMPTY, visitor);
1052}
1053
1054fn resolve_tree_inner<F>(
1055 node: &crate::object::ObjectNode,
1056 inherited: &InheritedContext,
1057 visitor: &mut F,
1058) where
1059 F: FnMut(&crate::object::ObjectNode, &Style),
1060{
1061 let (style, child_ctx) = resolve(node.style.as_deref(), node.states(), Part::MAIN, inherited);
1062 visitor(node, &style);
1063 for child in node.children() {
1064 resolve_tree_inner(child, &child_ctx, visitor);
1065 }
1066}
1067
1068/// Convenience top-down traversal that resolves visual and text styles.
1069///
1070/// This is the text-aware companion to [`resolve_tree`]. It threads the same
1071/// inherited context through the tree but exposes both [`Style`] and
1072/// [`TextStyle`] to the visitor.
1073pub fn resolve_tree_with_text<F>(root: &crate::object::ObjectNode, visitor: &mut F)
1074where
1075 F: FnMut(&crate::object::ObjectNode, &Style, &TextStyle),
1076{
1077 resolve_tree_with_text_inner(root, &InheritedContext::EMPTY, visitor);
1078}
1079
1080fn resolve_tree_with_text_inner<F>(
1081 node: &crate::object::ObjectNode,
1082 inherited: &InheritedContext,
1083 visitor: &mut F,
1084) where
1085 F: FnMut(&crate::object::ObjectNode, &Style, &TextStyle),
1086{
1087 let resolved = resolve_styles(node.style.as_deref(), node.states(), Part::MAIN, inherited);
1088 visitor(node, &resolved.style, &resolved.text);
1089 for child in node.children() {
1090 resolve_tree_with_text_inner(child, &resolved.child_context, visitor);
1091 }
1092}
1093
1094// ---------------------------------------------------------------------------
1095// Tests
1096// ---------------------------------------------------------------------------
1097
1098#[cfg(test)]
1099mod tests {
1100 use alloc::rc::Rc;
1101 use core::cell::RefCell;
1102
1103 use super::*;
1104 use crate::object::{ObjectNode, ObjectStates};
1105 use crate::widget::{Color, Rect, Widget};
1106
1107 // -----------------------------------------------------------------------
1108 // Minimal test widget
1109 // -----------------------------------------------------------------------
1110
1111 struct Dummy;
1112
1113 impl Widget for Dummy {
1114 fn bounds(&self) -> Rect {
1115 Rect {
1116 x: 0,
1117 y: 0,
1118 width: 10,
1119 height: 10,
1120 }
1121 }
1122 fn draw(&self, _r: &mut dyn crate::renderer::Renderer) {}
1123 fn handle_event(&mut self, _e: &crate::event::Event) -> bool {
1124 false
1125 }
1126 }
1127
1128 fn make_node() -> ObjectNode {
1129 ObjectNode::new(Rc::new(RefCell::new(Dummy)))
1130 }
1131
1132 fn red() -> Color {
1133 Color(255, 0, 0, 255)
1134 }
1135 fn blue() -> Color {
1136 Color(0, 0, 255, 255)
1137 }
1138 fn green() -> Color {
1139 Color(0, 255, 0, 255)
1140 }
1141
1142 // -----------------------------------------------------------------------
1143 // Part / Selector matching
1144 // -----------------------------------------------------------------------
1145
1146 #[test]
1147 fn selector_main_pressed_matches_pressed_state() {
1148 let sel = Selector::new(Part::MAIN, ObjectStates::PRESSED);
1149 // Matches when PRESSED is set.
1150 assert!(sel.matches(Part::MAIN, ObjectStates::PRESSED));
1151 // Does not match when PRESSED is absent.
1152 assert!(!sel.matches(Part::MAIN, ObjectStates::DEFAULT));
1153 }
1154
1155 #[test]
1156 fn selector_part_matches_any_state() {
1157 let sel = Selector::part(Part::MAIN);
1158 assert!(sel.matches(Part::MAIN, ObjectStates::DEFAULT));
1159 assert!(sel.matches(Part::MAIN, ObjectStates::PRESSED));
1160 assert!(sel.matches(Part::MAIN, ObjectStates::FOCUSED));
1161 }
1162
1163 #[test]
1164 fn selector_does_not_match_wrong_part() {
1165 let sel = Selector::part(Part::INDICATOR);
1166 assert!(!sel.matches(Part::MAIN, ObjectStates::DEFAULT));
1167 }
1168
1169 #[test]
1170 fn selector_multi_state_requires_all_bits() {
1171 let mask = ObjectStates::from_bits_truncate(
1172 ObjectStates::PRESSED.bits() | ObjectStates::FOCUSED.bits(),
1173 );
1174 let sel = Selector::new(Part::MAIN, mask);
1175 // Both bits set → matches.
1176 assert!(sel.matches(Part::MAIN, mask));
1177 // Only one bit set → no match.
1178 assert!(!sel.matches(Part::MAIN, ObjectStates::PRESSED));
1179 }
1180
1181 // -----------------------------------------------------------------------
1182 // Cascade precedence: local overrides added
1183 // -----------------------------------------------------------------------
1184
1185 #[test]
1186 fn local_overrides_added_for_same_field() {
1187 let mut node = make_node();
1188
1189 // Added (lower precedence) patch: bg = blue.
1190 static ADDED_PATCH: StylePatch = StylePatch {
1191 bg_color: Some(Color(0, 0, 255, 255)),
1192 border_color: None,
1193 border_width: None,
1194 alpha: None,
1195 radius: None,
1196 text_color: None,
1197 font_id: None,
1198 letter_spacing: None,
1199 line_spacing: None,
1200 text_align: None,
1201 padding_top: None,
1202 padding_bottom: None,
1203 padding_left: None,
1204 padding_right: None,
1205 margin_top: None,
1206 margin_bottom: None,
1207 margin_left: None,
1208 margin_right: None,
1209 gap_row: None,
1210 gap_col: None,
1211 };
1212 node.add_style(&ADDED_PATCH, Selector::part(Part::MAIN));
1213
1214 // Local (higher precedence) patch: bg = red.
1215 let local_patch = StylePatch {
1216 bg_color: Some(Color(255, 0, 0, 255)),
1217 ..StylePatch::new()
1218 };
1219 node.add_local_style(local_patch, Selector::part(Part::MAIN));
1220
1221 let (style, _) = resolve(
1222 node.style.as_deref(),
1223 node.states(),
1224 Part::MAIN,
1225 &InheritedContext::EMPTY,
1226 );
1227 // Local patch wins: red.
1228 assert_eq!(style.bg_color, red());
1229 }
1230
1231 // -----------------------------------------------------------------------
1232 // Last-added local wins among multiple matching locals
1233 // -----------------------------------------------------------------------
1234
1235 #[test]
1236 fn last_added_local_wins_among_matching_locals() {
1237 let mut node = make_node();
1238
1239 // First local: bg = red.
1240 node.add_local_style(
1241 StylePatch {
1242 bg_color: Some(red()),
1243 ..StylePatch::new()
1244 },
1245 Selector::part(Part::MAIN),
1246 );
1247 // Second local (added later): bg = blue.
1248 node.add_local_style(
1249 StylePatch {
1250 bg_color: Some(blue()),
1251 ..StylePatch::new()
1252 },
1253 Selector::part(Part::MAIN),
1254 );
1255
1256 let (style, _) = resolve(
1257 node.style.as_deref(),
1258 node.states(),
1259 Part::MAIN,
1260 &InheritedContext::EMPTY,
1261 );
1262 // Last-added wins: blue.
1263 assert_eq!(style.bg_color, blue());
1264 }
1265
1266 // -----------------------------------------------------------------------
1267 // Per-field merge: only set fields override; others fall to default
1268 // -----------------------------------------------------------------------
1269
1270 #[test]
1271 fn patch_only_bg_leaves_other_fields_at_default() {
1272 let mut node = make_node();
1273 node.add_local_style(
1274 StylePatch {
1275 bg_color: Some(red()),
1276 ..StylePatch::new()
1277 },
1278 Selector::part(Part::MAIN),
1279 );
1280
1281 let (style, _) = resolve(
1282 node.style.as_deref(),
1283 node.states(),
1284 Part::MAIN,
1285 &InheritedContext::EMPTY,
1286 );
1287 let defaults = Style::default();
1288
1289 assert_eq!(style.bg_color, red(), "bg_color from patch");
1290 assert_eq!(
1291 style.border_color, defaults.border_color,
1292 "border_color is default"
1293 );
1294 assert_eq!(
1295 style.border_width, defaults.border_width,
1296 "border_width is default"
1297 );
1298 assert_eq!(style.alpha, defaults.alpha, "alpha is default");
1299 assert_eq!(style.radius, defaults.radius, "radius is default");
1300 }
1301
1302 // -----------------------------------------------------------------------
1303 // State-driven: base + pressed override
1304 // -----------------------------------------------------------------------
1305
1306 #[test]
1307 fn state_driven_base_and_pressed_override() {
1308 let mut node = make_node();
1309
1310 // Base patch: bg = blue (matches any state via DEFAULT mask).
1311 node.add_local_style(
1312 StylePatch {
1313 bg_color: Some(blue()),
1314 ..StylePatch::new()
1315 },
1316 Selector::new(Part::MAIN, ObjectStates::DEFAULT),
1317 );
1318 // Pressed override: bg = red (only matches when PRESSED).
1319 node.add_local_style(
1320 StylePatch {
1321 bg_color: Some(red()),
1322 ..StylePatch::new()
1323 },
1324 Selector::new(Part::MAIN, ObjectStates::PRESSED),
1325 );
1326
1327 // Without PRESSED: base applies → blue.
1328 let (style_default, _) = resolve(
1329 node.style.as_deref(),
1330 ObjectStates::DEFAULT,
1331 Part::MAIN,
1332 &InheritedContext::EMPTY,
1333 );
1334 assert_eq!(
1335 style_default.bg_color,
1336 blue(),
1337 "default state should give blue"
1338 );
1339
1340 // With PRESSED: DEFAULT still matches, but the pressed patch was added
1341 // AFTER the base patch, and also matches because PRESSED bits are all
1342 // set — so last-added-wins gives red.
1343 let (style_pressed, _) = resolve(
1344 node.style.as_deref(),
1345 ObjectStates::PRESSED,
1346 Part::MAIN,
1347 &InheritedContext::EMPTY,
1348 );
1349 assert_eq!(
1350 style_pressed.bg_color,
1351 red(),
1352 "pressed state should give red"
1353 );
1354 }
1355
1356 // -----------------------------------------------------------------------
1357 // remove_local_styles
1358 // -----------------------------------------------------------------------
1359
1360 #[test]
1361 fn remove_local_styles_removes_matching_entries() {
1362 let mut node = make_node();
1363 node.add_local_style(
1364 StylePatch {
1365 bg_color: Some(red()),
1366 ..StylePatch::new()
1367 },
1368 Selector::part(Part::MAIN),
1369 );
1370 node.add_local_style(
1371 StylePatch {
1372 bg_color: Some(green()),
1373 ..StylePatch::new()
1374 },
1375 Selector::part(Part::SCROLLBAR),
1376 );
1377
1378 // Remove MAIN local styles.
1379 let removed = node.remove_local_styles(Part::MAIN, ObjectStates::DEFAULT);
1380 assert_eq!(removed, 1);
1381
1382 // SCROLLBAR entry survives; resolving MAIN now gives default bg.
1383 let (style, _) = resolve(
1384 node.style.as_deref(),
1385 ObjectStates::DEFAULT,
1386 Part::MAIN,
1387 &InheritedContext::EMPTY,
1388 );
1389 assert_eq!(style.bg_color, Style::default().bg_color);
1390 }
1391
1392 #[test]
1393 fn remove_all_local_styles_clears_everything() {
1394 let mut node = make_node();
1395 node.add_local_style(
1396 StylePatch {
1397 bg_color: Some(red()),
1398 ..StylePatch::new()
1399 },
1400 Selector::part(Part::MAIN),
1401 );
1402 node.add_local_style(
1403 StylePatch {
1404 bg_color: Some(blue()),
1405 ..StylePatch::new()
1406 },
1407 Selector::part(Part::INDICATOR),
1408 );
1409
1410 let removed = node.remove_all_local_styles();
1411 assert_eq!(removed, 2);
1412
1413 let (style, _) = resolve(
1414 node.style.as_deref(),
1415 ObjectStates::DEFAULT,
1416 Part::MAIN,
1417 &InheritedContext::EMPTY,
1418 );
1419 assert_eq!(style.bg_color, Style::default().bg_color);
1420 }
1421
1422 // -----------------------------------------------------------------------
1423 // Top-down alpha inheritance
1424 // -----------------------------------------------------------------------
1425
1426 #[test]
1427 fn alpha_inherits_from_parent_context() {
1428 // Parent resolves alpha = 128; child has no alpha patch → inherits.
1429 let parent_ctx = InheritedContext::with_resolved(128);
1430 let mut child = make_node();
1431 // Child has no alpha patch.
1432 child.add_local_style(
1433 StylePatch {
1434 bg_color: Some(red()),
1435 ..StylePatch::new()
1436 },
1437 Selector::part(Part::MAIN),
1438 );
1439
1440 let (child_style, _) = resolve(
1441 child.style.as_deref(),
1442 child.states(),
1443 Part::MAIN,
1444 &parent_ctx,
1445 );
1446 assert_eq!(child_style.alpha, 128, "child should inherit parent alpha");
1447 }
1448
1449 #[test]
1450 fn alpha_own_patch_overrides_inheritance() {
1451 let parent_ctx = InheritedContext::with_resolved(128);
1452 let mut child = make_node();
1453 child.add_local_style(
1454 StylePatch {
1455 alpha: Some(200),
1456 ..StylePatch::new()
1457 },
1458 Selector::part(Part::MAIN),
1459 );
1460
1461 let (child_style, grandchild_ctx) = resolve(
1462 child.style.as_deref(),
1463 child.states(),
1464 Part::MAIN,
1465 &parent_ctx,
1466 );
1467 assert_eq!(child_style.alpha, 200, "child's own patch overrides");
1468 // Grandchild gets child's resolved alpha.
1469 assert_eq!(grandchild_ctx.alpha, Some(200));
1470 }
1471
1472 #[test]
1473 fn grandchild_inherits_overriding_ancestor_alpha() {
1474 // parent resolves alpha 100; child sets alpha 200; grandchild has none.
1475 let parent_ctx = InheritedContext::with_resolved(100);
1476
1477 let mut child = make_node();
1478 child.add_local_style(
1479 StylePatch {
1480 alpha: Some(200),
1481 ..StylePatch::new()
1482 },
1483 Selector::part(Part::MAIN),
1484 );
1485 let (_, grandchild_ctx) = resolve(
1486 child.style.as_deref(),
1487 child.states(),
1488 Part::MAIN,
1489 &parent_ctx,
1490 );
1491 // Grandchild context carries child's alpha = 200.
1492 assert_eq!(grandchild_ctx.alpha, Some(200));
1493
1494 let grandchild = make_node();
1495 let (gs, _) = resolve(
1496 grandchild.style.as_deref(),
1497 grandchild.states(),
1498 Part::MAIN,
1499 &grandchild_ctx,
1500 );
1501 assert_eq!(gs.alpha, 200);
1502 }
1503
1504 #[test]
1505 fn no_alpha_anywhere_resolves_to_default_255() {
1506 let node = make_node();
1507 let (style, _) = resolve(
1508 node.style.as_deref(),
1509 node.states(),
1510 Part::MAIN,
1511 &InheritedContext::EMPTY,
1512 );
1513 assert_eq!(style.alpha, 255);
1514 }
1515
1516 #[test]
1517 fn bg_color_does_not_inherit() {
1518 // Parent sets bg_color; child has no bg_color patch → child gets default.
1519 let mut parent = make_node();
1520 parent.add_local_style(
1521 StylePatch {
1522 bg_color: Some(red()),
1523 ..StylePatch::new()
1524 },
1525 Selector::part(Part::MAIN),
1526 );
1527 let (_, child_ctx) = resolve(
1528 parent.style.as_deref(),
1529 parent.states(),
1530 Part::MAIN,
1531 &InheritedContext::EMPTY,
1532 );
1533
1534 let child = make_node();
1535 let (child_style, _) = resolve(
1536 child.style.as_deref(),
1537 child.states(),
1538 Part::MAIN,
1539 &child_ctx,
1540 );
1541 // bg_color is not inheritable: child gets the default (white), not red.
1542 assert_eq!(child_style.bg_color, Style::default().bg_color);
1543 }
1544
1545 // -----------------------------------------------------------------------
1546 // TextStyle cascade and inheritance
1547 // -----------------------------------------------------------------------
1548
1549 #[test]
1550 fn text_style_defaults_resolve_without_patches() {
1551 let node = make_node();
1552 let resolved = resolve_styles(
1553 node.style.as_deref(),
1554 node.states(),
1555 Part::MAIN,
1556 &InheritedContext::EMPTY,
1557 );
1558 assert_eq!(resolved.text, TextStyle::default());
1559 assert_eq!(resolved.child_context.text_color, Some(Color(0, 0, 0, 255)));
1560 assert_eq!(resolved.child_context.font_id, Some(FontId::DEFAULT));
1561 }
1562
1563 #[test]
1564 fn text_style_inherits_from_parent_context() {
1565 let parent_text = TextStyle {
1566 text_color: green(),
1567 font_id: FontId(7),
1568 letter_spacing: 2,
1569 line_spacing: 3,
1570 text_align: TextAlign::Center,
1571 };
1572 let parent_ctx = InheritedContext::with_resolved_styles(128, parent_text);
1573 let child = make_node();
1574
1575 let resolved = resolve_styles(
1576 child.style.as_deref(),
1577 child.states(),
1578 Part::MAIN,
1579 &parent_ctx,
1580 );
1581
1582 assert_eq!(resolved.text, parent_text);
1583 assert_eq!(resolved.style.alpha, 128);
1584 }
1585
1586 #[test]
1587 fn local_text_patch_overrides_inherited_text() {
1588 let parent_text = TextStyle {
1589 text_color: green(),
1590 font_id: FontId(7),
1591 letter_spacing: 2,
1592 line_spacing: 3,
1593 text_align: TextAlign::Right,
1594 };
1595 let parent_ctx = InheritedContext::with_resolved_styles(255, parent_text);
1596 let mut child = make_node();
1597 child.add_local_style(
1598 StylePatch::builder()
1599 .text_color(red())
1600 .font_id(FontId(9))
1601 .letter_spacing(4)
1602 .line_spacing(5)
1603 .text_align(TextAlign::Center)
1604 .build(),
1605 Selector::part(Part::MAIN),
1606 );
1607
1608 let resolved = resolve_styles(
1609 child.style.as_deref(),
1610 child.states(),
1611 Part::MAIN,
1612 &parent_ctx,
1613 );
1614
1615 assert_eq!(resolved.text.text_color, red());
1616 assert_eq!(resolved.text.font_id, FontId(9));
1617 assert_eq!(resolved.text.letter_spacing, 4);
1618 assert_eq!(resolved.text.line_spacing, 5);
1619 assert_eq!(resolved.text.text_align, TextAlign::Center);
1620 }
1621
1622 // -----------------------------------------------------------------------
1623 // resolve_tree threads context through the tree
1624 // -----------------------------------------------------------------------
1625
1626 #[test]
1627 fn resolve_tree_visits_all_nodes_with_inherited_alpha() {
1628 use alloc::collections::BTreeMap;
1629 use alloc::string::String;
1630
1631 // Build a two-level tree with a tagged root and child.
1632 let root_widget = Rc::new(RefCell::new(Dummy));
1633 let child_widget = Rc::new(RefCell::new(Dummy));
1634
1635 let mut root = ObjectNode::new(root_widget).with_tag("root");
1636 let child = ObjectNode::new(child_widget).with_tag("child");
1637
1638 // Root has alpha = 50.
1639 root.add_local_style(
1640 StylePatch {
1641 alpha: Some(50),
1642 ..StylePatch::new()
1643 },
1644 Selector::part(Part::MAIN),
1645 );
1646
1647 root.append_child(child);
1648
1649 let mut alpha_map: BTreeMap<String, u8> = BTreeMap::new();
1650 resolve_tree(&root, &mut |node, style| {
1651 if let Some(tag) = node.tag() {
1652 alpha_map.insert(tag.to_string(), style.alpha);
1653 }
1654 });
1655
1656 assert_eq!(alpha_map.get("root"), Some(&50), "root has its own alpha");
1657 assert_eq!(
1658 alpha_map.get("child"),
1659 Some(&50),
1660 "child inherits root alpha"
1661 );
1662 }
1663
1664 #[test]
1665 fn resolve_tree_with_text_threads_text_context() {
1666 use alloc::collections::BTreeMap;
1667 use alloc::string::String;
1668
1669 let root_widget = Rc::new(RefCell::new(Dummy));
1670 let child_widget = Rc::new(RefCell::new(Dummy));
1671
1672 let mut root = ObjectNode::new(root_widget).with_tag("root");
1673 let child = ObjectNode::new(child_widget).with_tag("child");
1674
1675 root.add_local_style(
1676 StylePatch::builder()
1677 .text_color(blue())
1678 .font_id(FontId(11))
1679 .letter_spacing(1)
1680 .line_spacing(2)
1681 .text_align(TextAlign::Right)
1682 .build(),
1683 Selector::part(Part::MAIN),
1684 );
1685 root.append_child(child);
1686
1687 let mut text_colors: BTreeMap<String, Color> = BTreeMap::new();
1688 let mut font_ids: BTreeMap<String, FontId> = BTreeMap::new();
1689 resolve_tree_with_text(&root, &mut |node, _style, text| {
1690 if let Some(tag) = node.tag() {
1691 text_colors.insert(tag.to_string(), text.text_color);
1692 font_ids.insert(tag.to_string(), text.font_id);
1693 }
1694 });
1695
1696 assert_eq!(text_colors.get("root"), Some(&blue()));
1697 assert_eq!(text_colors.get("child"), Some(&blue()));
1698 assert_eq!(font_ids.get("root"), Some(&FontId(11)));
1699 assert_eq!(font_ids.get("child"), Some(&FontId(11)));
1700 }
1701
1702 // -----------------------------------------------------------------------
1703 // Existing Style/StyleBuilder still work unchanged
1704 // -----------------------------------------------------------------------
1705
1706 #[test]
1707 fn style_builder_still_works() {
1708 use crate::style::StyleBuilder;
1709 let s = StyleBuilder::new()
1710 .bg_color(red())
1711 .border_width(2)
1712 .alpha(128)
1713 .build();
1714 assert_eq!(s.bg_color, red());
1715 assert_eq!(s.border_width, 2);
1716 assert_eq!(s.alpha, 128);
1717 }
1718
1719 // -----------------------------------------------------------------------
1720 // ObjectNode convenience methods compile and run
1721 // -----------------------------------------------------------------------
1722
1723 #[test]
1724 fn object_node_methods_round_trip() {
1725 let mut node = make_node();
1726 assert!(node.style.is_none(), "initially no style slot");
1727
1728 node.add_local_style(
1729 StylePatch {
1730 bg_color: Some(green()),
1731 ..StylePatch::new()
1732 },
1733 Selector::part(Part::MAIN),
1734 );
1735 assert!(node.style.is_some(), "slot allocated after add");
1736
1737 // Adding a shared patch.
1738 static SP: StylePatch = StylePatch {
1739 border_width: Some(3),
1740 bg_color: None,
1741 border_color: None,
1742 alpha: None,
1743 radius: None,
1744 text_color: None,
1745 font_id: None,
1746 letter_spacing: None,
1747 line_spacing: None,
1748 text_align: None,
1749 padding_top: None,
1750 padding_bottom: None,
1751 padding_left: None,
1752 padding_right: None,
1753 margin_top: None,
1754 margin_bottom: None,
1755 margin_left: None,
1756 margin_right: None,
1757 gap_row: None,
1758 gap_col: None,
1759 };
1760 node.add_style(&SP, Selector::part(Part::MAIN));
1761
1762 let (s, _) = resolve(
1763 node.style.as_deref(),
1764 node.states(),
1765 Part::MAIN,
1766 &InheritedContext::EMPTY,
1767 );
1768 assert_eq!(s.bg_color, green());
1769 assert_eq!(s.border_width, 3);
1770
1771 let removed = node.remove_local_styles(Part::MAIN, ObjectStates::DEFAULT);
1772 assert_eq!(removed, 1);
1773 }
1774
1775 // -----------------------------------------------------------------------
1776 // LPAR-07 Transition override (Tier-0) tests
1777 // -----------------------------------------------------------------------
1778
1779 /// Helper: tick ObjectAnims N times and collect dirty rects.
1780 fn tick_n(oa: &mut crate::object_anim::ObjectAnims, root: &mut ObjectNode, n: u32) {
1781 for _ in 0..n {
1782 oa.tick(root, &mut |_| {});
1783 }
1784 }
1785
1786 #[test]
1787 fn transition_override_wins_above_local_styles() {
1788 let mut node = make_node();
1789 // Local patch: bg = red.
1790 node.add_local_style(
1791 StylePatch {
1792 bg_color: Some(red()),
1793 ..StylePatch::new()
1794 },
1795 Selector::part(Part::MAIN),
1796 );
1797
1798 // Manually write an override value directly into the Rc<RefCell<>>.
1799 let override_rc = node
1800 .style
1801 .get_or_insert_with(|| alloc::boxed::Box::new(StyleState::new()))
1802 .transition_override_handle();
1803 override_rc.borrow_mut().bg_color = Some(blue());
1804
1805 let (style, _) = resolve(
1806 node.style.as_deref(),
1807 node.states(),
1808 Part::MAIN,
1809 &InheritedContext::EMPTY,
1810 );
1811 // Tier-0 override wins: blue.
1812 assert_eq!(
1813 style.bg_color,
1814 blue(),
1815 "transition override must beat local styles"
1816 );
1817 }
1818
1819 #[test]
1820 fn resolve_without_override_identical_to_before() {
1821 let mut node = make_node();
1822 node.add_local_style(
1823 StylePatch {
1824 bg_color: Some(red()),
1825 border_width: Some(3),
1826 ..StylePatch::new()
1827 },
1828 Selector::part(Part::MAIN),
1829 );
1830
1831 // No override set — transition_override is all None by default.
1832 let (style, _) = resolve(
1833 node.style.as_deref(),
1834 node.states(),
1835 Part::MAIN,
1836 &InheritedContext::EMPTY,
1837 );
1838 assert_eq!(style.bg_color, red(), "bg_color from local patch unchanged");
1839 assert_eq!(
1840 style.border_width, 3,
1841 "border_width from local patch unchanged"
1842 );
1843 }
1844
1845 #[test]
1846 fn start_transition_animates_override() {
1847 let mut node = make_node();
1848 let mut oa = crate::object_anim::ObjectAnims::new();
1849 let bounds = Rect {
1850 x: 0,
1851 y: 0,
1852 width: 10,
1853 height: 10,
1854 };
1855
1856 // Transition bg_color from red → blue over 256 ticks (ANIM_SCALE duration
1857 // means progress == tick at each step, making interpolation trivial).
1858 let _id = start_transition(
1859 &mut node,
1860 &mut oa,
1861 AnimProp::BgColor,
1862 AnimPropValue::Color(red()),
1863 AnimPropValue::Color(blue()),
1864 TransitionDesc {
1865 duration_ticks: 256,
1866 delay_ticks: 0,
1867 easing: crate::anim::Easing::Linear,
1868 },
1869 bounds,
1870 );
1871
1872 // After 128 ticks at linear interpolation over 256 ticks, progress v = 128.
1873 // lerp(red, blue, 128, 256):
1874 // r: 255 + (0-255)*128/256 = 255-127 = 128
1875 // b: 0 + 255*128/256 = 127
1876 // So the color is neither pure red nor pure blue.
1877 tick_n(&mut oa, &mut node, 128);
1878
1879 let (style_mid, _) = resolve(
1880 node.style.as_deref(),
1881 node.states(),
1882 Part::MAIN,
1883 &InheritedContext::EMPTY,
1884 );
1885 // The override is now active: bg_color should be neither pure red nor pure blue.
1886 assert_ne!(
1887 style_mid.bg_color,
1888 red(),
1889 "should have started transitioning"
1890 );
1891 assert_ne!(style_mid.bg_color, blue(), "should not be at end yet");
1892
1893 // Run remaining 128 ticks → tween completes → on_complete clears override.
1894 tick_n(&mut oa, &mut node, 128);
1895
1896 let (style_after, _) = resolve(
1897 node.style.as_deref(),
1898 node.states(),
1899 Part::MAIN,
1900 &InheritedContext::EMPTY,
1901 );
1902 // Override cleared → falls back to Style::default().
1903 assert_eq!(
1904 style_after.bg_color,
1905 Style::default().bg_color,
1906 "override should be cleared after transition completes"
1907 );
1908 }
1909
1910 #[test]
1911 fn transition_cancel_stops_without_completion() {
1912 let mut node = make_node();
1913 let mut oa = crate::object_anim::ObjectAnims::new();
1914 let bounds = Rect {
1915 x: 0,
1916 y: 0,
1917 width: 10,
1918 height: 10,
1919 };
1920
1921 let id = start_transition(
1922 &mut node,
1923 &mut oa,
1924 AnimProp::Alpha,
1925 AnimPropValue::Scalar(255),
1926 AnimPropValue::Scalar(0),
1927 TransitionDesc {
1928 duration_ticks: 100,
1929 delay_ticks: 0,
1930 easing: crate::anim::Easing::Linear,
1931 },
1932 bounds,
1933 );
1934
1935 // Advance a few ticks so the override is non-None.
1936 tick_n(&mut oa, &mut node, 10);
1937
1938 // The override should now be Some (intermediate alpha).
1939 {
1940 let ov_rc = node.style.as_ref().unwrap().transition_override_handle();
1941 assert!(
1942 ov_rc.borrow().alpha.is_some(),
1943 "override should be set after a few ticks"
1944 );
1945 }
1946
1947 // Cancel: removes animation and clears the override.
1948 cancel_transition(&mut node, &mut oa, id, AnimProp::Alpha);
1949
1950 let ov_rc = node.style.as_ref().unwrap().transition_override_handle();
1951 assert!(
1952 ov_rc.borrow().alpha.is_none(),
1953 "override should be cleared after cancel_transition"
1954 );
1955 }
1956
1957 #[test]
1958 fn determinism_check() {
1959 use crate::anim::Easing;
1960
1961 // Two independent runs of the same transition must produce the same
1962 // intermediate values at the same tick.
1963 let bounds = Rect {
1964 x: 0,
1965 y: 0,
1966 width: 10,
1967 height: 10,
1968 };
1969 let desc = TransitionDesc {
1970 duration_ticks: 50,
1971 delay_ticks: 0,
1972 easing: Easing::EaseOut,
1973 };
1974
1975 let sample = || {
1976 let mut node = make_node();
1977 let mut oa = crate::object_anim::ObjectAnims::new();
1978 start_transition(
1979 &mut node,
1980 &mut oa,
1981 AnimProp::BgColor,
1982 AnimPropValue::Color(red()),
1983 AnimPropValue::Color(blue()),
1984 desc,
1985 bounds,
1986 );
1987 tick_n(&mut oa, &mut node, 25);
1988 let (style, _) = resolve(
1989 node.style.as_deref(),
1990 node.states(),
1991 Part::MAIN,
1992 &InheritedContext::EMPTY,
1993 );
1994 style.bg_color
1995 };
1996
1997 assert_eq!(sample(), sample(), "transitions are deterministic");
1998 }
1999}