Skip to main content

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}