Skip to main content

rlvgl_core/
object.rs

1//! LVGL-parity object metadata and tree helpers.
2//!
3//! This module is the additive object substrate for the LPAR initiative. It
4//! deliberately leaves [`Widget`](crate::widget::Widget) and
5//! [`WidgetNode`](crate::WidgetNode) untouched while providing a richer node
6//! carrier for LVGL-like flags, states, ordering, detach semantics, hidden-aware
7//! drawing, hit testing, and the LPAR-04 event/focus/lifecycle runtime.
8//!
9//! # LPAR-04 additions
10//!
11//! - [`ObjectEvent`] — the object-semantic event vocabulary.
12//! - [`GestureDir`] — directional swipe summary payload for
13//!   [`ObjectEvent::Gesture`].
14//! - [`ObjectFlags::EVENT_BUBBLE`] — opt-in per-object bubbling flag.
15//! - Per-node handler lists (trickle, target, bubble phase).
16//! - [`dispatch_object_event`] — the routing entry point.
17//! - [`DispatchInput`] and [`Disposition`] — dispatch input/output types.
18//! - [`EventContext`] — dispatch context exposed to handlers.
19//! - Lifecycle emission from mutation helpers
20//!   ([`append_child`](ObjectNode::append_child),
21//!   [`insert_child`](ObjectNode::insert_child),
22//!   [`detach_child`](ObjectNode::detach_child)).
23
24use alloc::boxed::Box;
25use alloc::rc::Rc;
26use alloc::vec::Vec;
27use core::cell::RefCell;
28
29use crate::WidgetNode;
30use crate::event::{Event, Key};
31use crate::layout::LayoutState;
32use crate::renderer::{ClipRenderer, Renderer};
33use crate::widget::{Rect, Widget};
34
35// ---------------------------------------------------------------------------
36// ObjectEvent — the §5.3 v1 code set
37// ---------------------------------------------------------------------------
38
39/// Directional swipe summary delivered as the payload of
40/// [`ObjectEvent::Gesture`].
41///
42/// Derived from `DragStart`/`DragMove`/`DragEnd` displacement at the dispatch
43/// layer (LPAR-04 §9.6).  Scroll begin/end/throw semantics belong to LPAR-05
44/// and are **not** modelled here.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum GestureDir {
47    /// Swipe toward the top of the screen.
48    Up,
49    /// Swipe toward the bottom of the screen.
50    Down,
51    /// Swipe toward the left edge of the screen.
52    Left,
53    /// Swipe toward the right edge of the screen.
54    Right,
55}
56
57/// Object-semantic event vocabulary for the LPAR-04 dispatch model.
58///
59/// This is the **object-level** event type, distinct from the device/recognizer
60/// stream vocabulary in [`crate::event::Event`].  Stream events flow through
61/// pipelines and legacy dispatch; `ObjectEvent` codes are delivered only
62/// through [`dispatch_object_event`].
63///
64/// This enum is `#[non_exhaustive]`: consumers **must** include a wildcard
65/// arm.  New codes will be added in later LPAR phases under the Specification
66/// Required policy.
67///
68/// # LVGL analogue table (LPAR-04 §5.3 + LPAR-05 §6)
69///
70/// | Variant | Trigger | LVGL analogue |
71/// |---|---|---|
72/// | [`Pressed`](Self::Pressed) | Stream `PressDown` at target | `LV_EVENT_PRESSED` |
73/// | [`Released`](Self::Released) | Contact ended over target | `LV_EVENT_RELEASED` |
74/// | [`Clicked`](Self::Clicked) | Stream `PressRelease` at target | `LV_EVENT_CLICKED` |
75/// | [`DoubleClicked`](Self::DoubleClicked) | Stream `DoubleTap` at target | `LV_EVENT_DOUBLE_CLICKED` |
76/// | [`LongPressed`](Self::LongPressed) | Long-press recognizer output at target | `LV_EVENT_LONG_PRESSED` |
77/// | [`LongPressedRepeat`](Self::LongPressedRepeat) | Long-press repeat output | `LV_EVENT_LONG_PRESSED_REPEAT` |
78/// | [`Key`](Self::Key) | Key delivery to focused object | `LV_EVENT_KEY` |
79/// | [`Rotary`](Self::Rotary) | Encoder diff in editing mode | `LV_EVENT_ROTARY` |
80/// | [`Focused`](Self::Focused) | Object gained focus | `LV_EVENT_FOCUSED` |
81/// | [`Defocused`](Self::Defocused) | Object lost focus | `LV_EVENT_DEFOCUSED` |
82/// | [`Gesture`](Self::Gesture) | Directional swipe summary | `LV_EVENT_GESTURE` |
83/// | [`Attached`](Self::Attached) | Subtree root attached via `append_child`/`insert_child` | `LV_EVENT_CHILD_CREATED` (inverted) |
84/// | [`Detached`](Self::Detached) | Subtree root detached via `detach_child` | `LV_EVENT_DELETE` (narrowed) |
85/// | [`ChildChanged`](Self::ChildChanged) | Parent notified after child add/remove/reorder | `LV_EVENT_CHILD_CHANGED` |
86/// | [`ScrollBegin`](Self::ScrollBegin) | Scroll session began on this container | `LV_EVENT_SCROLL_BEGIN` |
87/// | [`Scroll`](Self::Scroll) | Scroll offset changed | `LV_EVENT_SCROLL` |
88/// | [`ScrollEnd`](Self::ScrollEnd) | Scroll session ended and offset settled | `LV_EVENT_SCROLL_END` |
89/// | [`ScrollThrow`](Self::ScrollThrow) | Throw/momentum phase initiated | `LV_EVENT_SCROLL_THROW_BEGIN` |
90#[non_exhaustive]
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub enum ObjectEvent {
93    /// A stable contact began at the given coordinates.
94    ///
95    /// Derived from stream [`Event::PressDown`] resolved at this target.
96    /// Use for visual press feedback (e.g. button highlight).
97    Pressed {
98        /// Horizontal coordinate of the contact.
99        x: i32,
100        /// Vertical coordinate of the contact.
101        y: i32,
102    },
103    /// A contact ended over this object (any cause).
104    ///
105    /// Always paired with a preceding [`Pressed`](Self::Pressed).
106    Released {
107        /// Horizontal coordinate at release.
108        x: i32,
109        /// Vertical coordinate at release.
110        y: i32,
111    },
112    /// A short clean tap was completed on this object.
113    ///
114    /// Derived from stream [`Event::PressRelease`].  Contacts that crossed
115    /// the drag threshold produce no `Clicked` (INPUT-00 §6 click-vs-drag
116    /// suppression).
117    Clicked {
118        /// Horizontal coordinate of the tap.
119        x: i32,
120        /// Vertical coordinate of the tap.
121        y: i32,
122    },
123    /// Two consecutive short taps were detected on this object.
124    ///
125    /// Derived from stream [`Event::DoubleTap`].
126    DoubleClicked {
127        /// Horizontal coordinate of the second tap.
128        x: i32,
129        /// Vertical coordinate of the second tap.
130        y: i32,
131    },
132    /// A held contact reached the long-press threshold.
133    ///
134    /// Emitted once per contact by the long-press recognizer (LPAR-04 §9).
135    /// Disarmed by `DragStart` before emission.
136    LongPressed {
137        /// Horizontal coordinate of the held contact.
138        x: i32,
139        /// Vertical coordinate of the held contact.
140        y: i32,
141    },
142    /// A long-pressed contact crossed another repeat interval.
143    ///
144    /// Emitted every repeat period after the initial [`LongPressed`](Self::LongPressed).
145    LongPressedRepeat {
146        /// Horizontal coordinate of the held contact.
147        x: i32,
148        /// Vertical coordinate of the held contact.
149        y: i32,
150    },
151    /// A key event was delivered to this focused object.
152    ///
153    /// Delivered when this object has focus and a keypad/encoder key event
154    /// is received.  In navigate mode, encoder presses map to
155    /// [`Key::Enter`]; in editing mode, all keys route here.
156    Key(
157        /// The key that was pressed or repeated.
158        Key,
159    ),
160    /// Encoder rotation delta delivered to this focused object in editing mode.
161    ///
162    /// In navigate mode, rotation drives `focus_next`/`focus_prev` instead.
163    Rotary {
164        /// Net rotation steps (positive = clockwise).
165        diff: i32,
166    },
167    /// This object gained keyboard focus.
168    ///
169    /// The [`ObjectStates::FOCUSED`] bit is set before this event is emitted.
170    Focused,
171    /// This object lost keyboard focus.
172    ///
173    /// The [`ObjectStates::FOCUSED`] bit is cleared before this event is emitted.
174    Defocused,
175    /// A directional swipe was recognized over this object.
176    ///
177    /// Derived from drag displacement (LPAR-04 §9.6).  Scroll semantics are
178    /// LPAR-05 non-goals.
179    Gesture(
180        /// Direction of the swipe.
181        GestureDir,
182    ),
183    /// This node was attached to a live tree via
184    /// [`append_child`](ObjectNode::append_child) or
185    /// [`insert_child`](ObjectNode::insert_child).
186    ///
187    /// Delivered synchronously to the attached subtree root after the
188    /// structural change.
189    Attached,
190    /// This node was detached from its parent via
191    /// [`detach_child`](ObjectNode::detach_child).
192    ///
193    /// Delivered synchronously to the detached subtree root after the
194    /// structural change.  This is the lifecycle event name reserved by
195    /// LPAR-02 §6.10.
196    Detached,
197    /// A child was added to or removed from this node.
198    ///
199    /// Delivered synchronously to the **parent** after
200    /// [`append_child`](ObjectNode::append_child),
201    /// [`insert_child`](ObjectNode::insert_child), or
202    /// [`detach_child`](ObjectNode::detach_child).
203    ChildChanged,
204    /// Scroll session began on this container.
205    ///
206    /// Emitted once per scroll session when a drag gesture transitions the
207    /// container into active scrolling. The offset is unchanged at this point;
208    /// the first [`Scroll`](Self::Scroll) event carries the new offset.
209    ///
210    /// LVGL analogue: `LV_EVENT_SCROLL_BEGIN`.
211    ScrollBegin,
212    /// Scroll offset changed.
213    ///
214    /// Emitted each tick/frame that the offset advances — during active drag
215    /// scrolling and during throw deceleration. The payload carries the new
216    /// logical scroll offset after the change.
217    ///
218    /// LVGL analogue: `LV_EVENT_SCROLL`.
219    Scroll {
220        /// New horizontal scroll offset in logical pixels.
221        scroll_x: i32,
222        /// New vertical scroll offset in logical pixels.
223        scroll_y: i32,
224    },
225    /// Scroll session ended and the offset has settled.
226    ///
227    /// Emitted once per scroll session, after the final [`Scroll`](Self::Scroll)
228    /// event (after throw deceleration or snap settle completes, or after a
229    /// drag ended with zero residual velocity).
230    ///
231    /// LVGL analogue: `LV_EVENT_SCROLL_END`.
232    ScrollEnd,
233    /// Throw/momentum phase initiated.
234    ///
235    /// Emitted once when the finger lifted with enough residual velocity to
236    /// initiate throw/momentum. Emitted between the last drag-driven
237    /// [`Scroll`](Self::Scroll) and the first momentum-driven
238    /// [`Scroll`](Self::Scroll).
239    ///
240    /// LVGL analogue: `LV_EVENT_SCROLL_THROW_BEGIN`.
241    ScrollThrow {
242        /// Estimated horizontal throw velocity in logical pixels per tick.
243        vel_x: i32,
244        /// Estimated throw velocity in logical pixels per tick.
245        vel_y: i32,
246    },
247
248    // -----------------------------------------------------------------------
249    // LPAR-10 layout events (§5.F — registered via Specification Required)
250    // -----------------------------------------------------------------------
251    /// This node's effective bounds changed as a result of a layout pass.
252    ///
253    /// Delivered to the node whose computed rect was updated.  Does **not**
254    /// bubble by default.  Widgets that maintain internal layout caches (e.g.
255    /// a label that wraps text at its container width) SHOULD listen for this
256    /// and invalidate their caches.
257    ///
258    /// LVGL analogue: `LV_EVENT_SIZE_CHANGED`.
259    SizeChanged,
260
261    /// All children of this layout container have been re-placed.
262    ///
263    /// Delivered to the container node after [`run_layout`](crate::layout::run_layout)
264    /// completes placing all children for this container.  Does **not** bubble
265    /// by default.
266    ///
267    /// LVGL analogue: `LV_EVENT_LAYOUT_CHANGED`.
268    LayoutChanged,
269}
270
271// ---------------------------------------------------------------------------
272// EventContext — dispatch context exposed to handlers
273// ---------------------------------------------------------------------------
274
275/// Dispatch context passed to every event handler during propagation.
276///
277/// `target_tag` and `current_tag` are the test-automation tags of the dispatch
278/// target and the node currently running its handlers, respectively.  Tags are
279/// optional; a `None` value means the node carries no tag.
280#[derive(Debug, Clone, Copy)]
281pub struct EventContext {
282    /// Test-automation tag of the ultimate dispatch target.
283    pub target_tag: Option<&'static str>,
284    /// Test-automation tag of the node whose handlers are currently executing.
285    pub current_tag: Option<&'static str>,
286}
287
288// ---------------------------------------------------------------------------
289// Handler type alias
290// ---------------------------------------------------------------------------
291
292/// Signature for an event handler stored on an [`ObjectNode`].
293///
294/// Receives the [`ObjectEvent`] and the current [`EventContext`].  Returns
295/// `true` to consume the event and stop propagation.
296pub type ObjectHandler = Box<dyn FnMut(&ObjectEvent, EventContext) -> bool>;
297
298// ---------------------------------------------------------------------------
299// ObjectFlags
300// ---------------------------------------------------------------------------
301
302/// LVGL-like object behavior flags.
303///
304/// Flags are stored as a compact bitset. Use the associated constants with
305/// [`contains`](Self::contains), [`insert`](Self::insert), and
306/// [`remove`](Self::remove).
307#[derive(Debug, Clone, Copy, PartialEq, Eq)]
308pub struct ObjectFlags(u32);
309
310impl ObjectFlags {
311    /// No flags set.
312    pub const EMPTY: Self = Self(0);
313    /// Skip drawing and targeting for the object and its subtree.
314    pub const HIDDEN: Self = Self(1 << 0);
315    /// Draw the object but exclude it from default interaction targeting.
316    pub const DISABLED: Self = Self(1 << 1);
317    /// Allow the object to be selected by pointer hit testing.
318    pub const CLICKABLE: Self = Self(1 << 2);
319    /// Allow the object to participate in future focus traversal.
320    pub const FOCUSABLE: Self = Self(1 << 3);
321    /// Allow the object to participate in future scroll behavior.
322    pub const SCROLLABLE: Self = Self(1 << 4);
323    /// Opt-in bubbling: propagate events toward root after the target phase
324    /// (LPAR-04 §6.4; mirrors `LV_OBJ_FLAG_EVENT_BUBBLE`).
325    ///
326    /// Default: clear.  When clear, bubble phase stops at this node.
327    pub const EVENT_BUBBLE: Self = Self(1 << 5);
328
329    /// Return a flag set from raw bits, dropping unknown bits.
330    pub const fn from_bits_truncate(bits: u32) -> Self {
331        Self(bits & Self::all_bits())
332    }
333
334    /// Return the raw flag bits.
335    pub const fn bits(self) -> u32 {
336        self.0
337    }
338
339    /// Return `true` when all flags in `other` are set.
340    pub const fn contains(self, other: Self) -> bool {
341        (self.0 & other.0) == other.0
342    }
343
344    /// Insert all flags in `other`.
345    pub fn insert(&mut self, other: Self) {
346        self.0 |= other.0;
347    }
348
349    /// Remove all flags in `other`.
350    pub fn remove(&mut self, other: Self) {
351        self.0 &= !other.0;
352    }
353
354    /// Set or clear all flags in `other`.
355    pub fn set(&mut self, other: Self, enabled: bool) {
356        if enabled {
357            self.insert(other);
358        } else {
359            self.remove(other);
360        }
361    }
362
363    const fn all_bits() -> u32 {
364        Self::HIDDEN.0
365            | Self::DISABLED.0
366            | Self::CLICKABLE.0
367            | Self::FOCUSABLE.0
368            | Self::SCROLLABLE.0
369            | Self::EVENT_BUBBLE.0
370    }
371}
372
373impl Default for ObjectFlags {
374    fn default() -> Self {
375        Self::EMPTY
376    }
377}
378
379// ---------------------------------------------------------------------------
380// ObjectStates
381// ---------------------------------------------------------------------------
382
383/// LVGL-like object state bits.
384///
385/// The default state is represented by no bits set. Style resolution consumes
386/// these bits in later LPAR phases; this module only stores and queries them.
387#[derive(Debug, Clone, Copy, PartialEq, Eq)]
388pub struct ObjectStates(u32);
389
390impl ObjectStates {
391    /// No state bits set.
392    pub const DEFAULT: Self = Self(0);
393    /// Disabled state for style selection.
394    pub const DISABLED: Self = Self(1 << 0);
395    /// Focused state for style selection and focus bookkeeping.
396    pub const FOCUSED: Self = Self(1 << 1);
397    /// Pressed state for pointer or key activation.
398    pub const PRESSED: Self = Self(1 << 2);
399    /// Checked state for toggleable controls.
400    pub const CHECKED: Self = Self(1 << 3);
401    /// Edited state for text or value editing modes.
402    pub const EDITED: Self = Self(1 << 4);
403
404    /// Return a state set from raw bits, dropping unknown bits.
405    pub const fn from_bits_truncate(bits: u32) -> Self {
406        Self(bits & Self::all_bits())
407    }
408
409    /// Return the raw state bits.
410    pub const fn bits(self) -> u32 {
411        self.0
412    }
413
414    /// Return `true` when all state bits in `other` are set.
415    pub const fn contains(self, other: Self) -> bool {
416        (self.0 & other.0) == other.0
417    }
418
419    /// Insert all state bits in `other`.
420    pub fn insert(&mut self, other: Self) {
421        self.0 |= other.0;
422    }
423
424    /// Remove all state bits in `other`.
425    pub fn remove(&mut self, other: Self) {
426        self.0 &= !other.0;
427    }
428
429    /// Set or clear all state bits in `other`.
430    pub fn set(&mut self, other: Self, enabled: bool) {
431        if enabled {
432            self.insert(other);
433        } else {
434            self.remove(other);
435        }
436    }
437
438    const fn all_bits() -> u32 {
439        Self::DISABLED.0 | Self::FOCUSED.0 | Self::PRESSED.0 | Self::CHECKED.0 | Self::EDITED.0
440    }
441}
442
443impl Default for ObjectStates {
444    fn default() -> Self {
445        Self::DEFAULT
446    }
447}
448
449// ---------------------------------------------------------------------------
450// ObjectMeta
451// ---------------------------------------------------------------------------
452
453/// Cross-cutting metadata stored by an [`ObjectNode`].
454#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
455pub struct ObjectMeta {
456    flags: ObjectFlags,
457    states: ObjectStates,
458    detached: bool,
459}
460
461impl ObjectMeta {
462    /// Create default object metadata.
463    pub const fn new() -> Self {
464        Self {
465            flags: ObjectFlags::EMPTY,
466            states: ObjectStates::DEFAULT,
467            detached: false,
468        }
469    }
470
471    /// Return the object flags.
472    pub const fn flags(&self) -> ObjectFlags {
473        self.flags
474    }
475
476    /// Mutably borrow the object flags.
477    pub fn flags_mut(&mut self) -> &mut ObjectFlags {
478        &mut self.flags
479    }
480
481    /// Return the object state bits.
482    pub const fn states(&self) -> ObjectStates {
483        self.states
484    }
485
486    /// Mutably borrow the object state bits.
487    pub fn states_mut(&mut self) -> &mut ObjectStates {
488        &mut self.states
489    }
490
491    /// Return whether this node has been detached from a live object tree.
492    pub const fn is_detached(&self) -> bool {
493        self.detached
494    }
495
496    fn set_detached(&mut self, detached: bool) {
497        self.detached = detached;
498    }
499}
500
501// ---------------------------------------------------------------------------
502// DispatchInput and Disposition
503// ---------------------------------------------------------------------------
504
505/// The input to [`dispatch_object_event`].
506///
507/// Pointer-positioned events carry coordinates used for hit-test target
508/// resolution.  Key and encoder events route to the focused node.
509#[derive(Debug, Clone, PartialEq, Eq)]
510pub enum DispatchInput {
511    /// A pointer stream event to be delivered to the hit-test target.
512    ///
513    /// The wrapped [`Event`] must be a pointer-positioned variant
514    /// (`PressDown`, `PressRelease`, `DoubleTap`, `LongPress`, etc.).  The
515    /// `x`/`y` coordinates are used for hit testing.
516    Pointer {
517        /// Pointer horizontal coordinate for hit-test resolution.
518        x: i32,
519        /// Pointer vertical coordinate for hit-test resolution.
520        y: i32,
521        /// The stream event to translate into an [`ObjectEvent`].
522        event: Event,
523    },
524    /// An [`ObjectEvent`] to deliver to the hit-test target at `(x, y)`.
525    PointerObject {
526        /// Pointer horizontal coordinate for hit-test resolution.
527        x: i32,
528        /// Pointer vertical coordinate for hit-test resolution.
529        y: i32,
530        /// The object-semantic event to dispatch.
531        event: ObjectEvent,
532    },
533    /// An [`ObjectEvent`] to deliver to the currently focused node.
534    Focused {
535        /// The object-semantic event to dispatch.
536        event: ObjectEvent,
537    },
538    /// An [`ObjectEvent`] targeted at the node identified by a structural path,
539    /// bypassing hit-test resolution.
540    ///
541    /// The event is delivered to that node's target handlers, then bubbles
542    /// through ancestors using the existing per-node [`ObjectFlags::EVENT_BUBBLE`]
543    /// gating — identically to how any other event bubbles. The
544    /// [`ObjectFlags::EVENT_BUBBLE`] flag is never mutated (LPAR-05 §6.3/§16).
545    Container {
546        /// Structural path from root to the target node (list of child indices).
547        path: Vec<usize>,
548        /// The object-semantic event to dispatch.
549        event: ObjectEvent,
550    },
551}
552
553/// The outcome of a [`dispatch_object_event`] call.
554#[derive(Debug, Clone, Copy, PartialEq, Eq)]
555pub enum Disposition {
556    /// The event was consumed by a handler during dispatch.
557    Consumed,
558    /// The event completed all phases without any handler consuming it.
559    Unconsumed,
560    /// No target could be resolved (no hit-test result or no focused node).
561    NoTarget,
562}
563
564// ---------------------------------------------------------------------------
565// ObjectNode
566// ---------------------------------------------------------------------------
567
568/// Additive LVGL-like object tree node.
569///
570/// `ObjectNode` mirrors the current widget-tree ownership shape while keeping
571/// object metadata in the node layer. Existing [`WidgetNode`](crate::WidgetNode)
572/// users remain source-compatible; new parity work can opt into this richer
573/// carrier.
574///
575/// # LPAR-04 handler storage
576///
577/// Each node owns three per-phase handler lists:
578///
579/// - **Trickle handlers** run root→target *before* the target phase.
580/// - **Target handlers** run at the target node.
581/// - **Bubble handlers** run target→root *after* the target phase, but only
582///   while the node has [`ObjectFlags::EVENT_BUBBLE`] set.
583///
584/// Handlers are closures stored as `Box<dyn FnMut(…)>` and are invoked in
585/// registration order.  A handler returning `true` consumes the event and
586/// stops all remaining phases.
587///
588/// # Mutation and dispatch
589///
590/// Tree mutation helpers ([`append_child`](Self::append_child),
591/// [`insert_child`](Self::insert_child), [`detach_child`](Self::detach_child))
592/// MUST NOT be called from inside an active dispatch.  They emit lifecycle
593/// events ([`ObjectEvent::Attached`], [`ObjectEvent::Detached`],
594/// [`ObjectEvent::ChildChanged`]) synchronously after the structural change,
595/// outside any active trickle/target/bubble traversal.
596pub struct ObjectNode {
597    widget: Rc<RefCell<dyn Widget>>,
598    children: Vec<ObjectNode>,
599    tag: Option<&'static str>,
600    meta: ObjectMeta,
601    /// Handlers run root→target (trickle/preprocess phase).
602    trickle_handlers: Vec<ObjectHandler>,
603    /// Handlers run at the target node (target phase).
604    target_handlers: Vec<ObjectHandler>,
605    /// Handlers run target→root (bubble phase; gated by `EVENT_BUBBLE` flag).
606    bubble_handlers: Vec<ObjectHandler>,
607    /// Optional scroll state, present when `ObjectFlags::SCROLLABLE` is set.
608    ///
609    /// Stored as a boxed value to keep `ObjectNode` from growing for
610    /// non-scroll nodes.
611    pub(crate) scroll: Option<Box<crate::scroll::ScrollState>>,
612    /// Optional node-resident animation entries (LPAR-06 §6.1).
613    ///
614    /// Stored as a boxed value to keep `ObjectNode` from growing for
615    /// non-animated nodes. Lazily allocated by
616    /// [`ObjectAnims::bind`](crate::object_anim::ObjectAnims::bind).
617    pub(crate) anims: Option<Box<crate::object_anim::NodeAnimSet>>,
618    /// Optional LPAR-07 style slot holding local and shared style entries.
619    ///
620    /// `None` for nodes that carry no style overrides, keeping `ObjectNode`
621    /// small. Lazily allocated by [`ObjectNode::add_local_style`] or
622    /// [`ObjectNode::add_style`].
623    pub(crate) style: Option<Box<crate::style_cascade::StyleState>>,
624    /// Optional LPAR-10 layout state: role (container/item/none), layout-computed
625    /// bounds override, and dirty flag.
626    ///
627    /// `None` for nodes that are not involved in object-managed layout, keeping
628    /// `ObjectNode` small. Lazily allocated by
629    /// [`set_layout_flex`](Self::set_layout_flex),
630    /// [`set_layout_grid`](Self::set_layout_grid), or
631    /// [`set_item_hints`](Self::set_item_hints).
632    pub(crate) layout: Option<Box<LayoutState>>,
633}
634
635impl ObjectNode {
636    /// Create a new object node with no children, tag, flags, states, or handlers.
637    pub fn new(widget: Rc<RefCell<dyn Widget>>) -> Self {
638        Self {
639            widget,
640            children: Vec::new(),
641            tag: None,
642            meta: ObjectMeta::new(),
643            trickle_handlers: Vec::new(),
644            target_handlers: Vec::new(),
645            bubble_handlers: Vec::new(),
646            scroll: None,
647            anims: None,
648            style: None,
649            layout: None,
650        }
651    }
652
653    /// Recursively adopt a compatibility [`WidgetNode`] into an object tree.
654    ///
655    /// The widget handle, child order, and test-automation tag are preserved.
656    /// New object metadata is initialized to defaults for every adopted node.
657    /// Nodes adopted this way start with empty handler lists.
658    pub fn adopt(node: WidgetNode) -> Self {
659        let mut object = Self::new(node.widget);
660        object.tag = node.tag;
661        object.children = node.children.into_iter().map(Self::adopt).collect();
662        object
663    }
664
665    /// Attach a test-automation tag to this node.
666    pub fn with_tag(mut self, tag: &'static str) -> Self {
667        self.tag = Some(tag);
668        self
669    }
670
671    /// Return the widget handle stored by this node.
672    pub fn widget(&self) -> &Rc<RefCell<dyn Widget>> {
673        &self.widget
674    }
675
676    /// Return this node's test-automation tag.
677    pub const fn tag(&self) -> Option<&'static str> {
678        self.tag
679    }
680
681    /// Return immutable access to the node's object metadata.
682    pub const fn meta(&self) -> &ObjectMeta {
683        &self.meta
684    }
685
686    /// Return mutable access to the node's object metadata.
687    pub fn meta_mut(&mut self) -> &mut ObjectMeta {
688        &mut self.meta
689    }
690
691    /// Return this node's object flags.
692    pub const fn flags(&self) -> ObjectFlags {
693        self.meta.flags()
694    }
695
696    /// Return this node's object state bits.
697    pub const fn states(&self) -> ObjectStates {
698        self.meta.states()
699    }
700
701    /// Set or clear a flag on this node.
702    pub fn set_flag(&mut self, flag: ObjectFlags, enabled: bool) {
703        self.meta.flags_mut().set(flag, enabled);
704    }
705
706    /// Set or clear a state bit on this node.
707    pub fn set_state(&mut self, state: ObjectStates, enabled: bool) {
708        self.meta.states_mut().set(state, enabled);
709    }
710
711    /// Return whether this node is detached from a live object tree.
712    pub const fn is_detached(&self) -> bool {
713        self.meta.is_detached()
714    }
715
716    /// Return this node's child list.
717    pub fn children(&self) -> &[ObjectNode] {
718        &self.children
719    }
720
721    /// Return this node's mutable child list.
722    pub fn children_mut(&mut self) -> &mut Vec<ObjectNode> {
723        &mut self.children
724    }
725
726    // -----------------------------------------------------------------------
727    // Scroll state accessors (LPAR-05 §5)
728    // -----------------------------------------------------------------------
729
730    /// Attach scroll state to this node, enabling it as a scroll container.
731    ///
732    /// The node's `SCROLLABLE` flag is set automatically. This is the primary
733    /// way to make a node a scroll container (LPAR-05 §5).
734    pub fn set_scroll_state(&mut self, state: Box<crate::scroll::ScrollState>) {
735        self.meta.flags_mut().insert(ObjectFlags::SCROLLABLE);
736        self.scroll = Some(state);
737    }
738
739    /// Return an immutable reference to the scroll state, if any.
740    pub fn scroll_state(&self) -> Option<&crate::scroll::ScrollState> {
741        self.scroll.as_deref()
742    }
743
744    /// Return a mutable reference to the scroll state, if any.
745    pub fn scroll_state_mut(&mut self) -> Option<&mut crate::scroll::ScrollState> {
746        self.scroll.as_deref_mut()
747    }
748
749    // -----------------------------------------------------------------------
750    // Style accessors (LPAR-07 §7.1)
751    // -----------------------------------------------------------------------
752
753    /// Add a locally owned style patch keyed by `selector`.
754    ///
755    /// Local style entries take priority over added (shared) entries in the
756    /// cascade. Within local entries, the last-added entry wins when multiple
757    /// selectors match (§7.2 reverse-registration-order rule).
758    ///
759    /// The style slot is lazily allocated on the first call.
760    pub fn add_local_style(
761        &mut self,
762        patch: crate::style_cascade::StylePatch,
763        selector: crate::style_cascade::Selector,
764    ) {
765        let slot = self
766            .style
767            .get_or_insert_with(|| Box::new(crate::style_cascade::StyleState::new()));
768        crate::style_cascade::push_local(slot, patch, selector);
769    }
770
771    /// Add a shared (added) style patch reference keyed by `selector`.
772    ///
773    /// Added entries have lower precedence than local entries. Within added
774    /// entries, the last-added entry wins when multiple selectors match (§7.2).
775    ///
776    /// The `'static` lifetime constraint is conservative in v1; see LPAR-07
777    /// §7.1 for the deferred object-lifetime-scoped extension.
778    ///
779    /// The style slot is lazily allocated on the first call.
780    pub fn add_style(
781        &mut self,
782        patch: &'static crate::style_cascade::StylePatch,
783        selector: crate::style_cascade::Selector,
784    ) {
785        let slot = self
786            .style
787            .get_or_insert_with(|| Box::new(crate::style_cascade::StyleState::new()));
788        crate::style_cascade::push_added(slot, patch, selector);
789    }
790
791    /// Add a default-theme style patch keyed by `selector` (LPAR-07 §9.1).
792    ///
793    /// Theme entries resolve at the **lowest** style precedence — below local
794    /// and added styles — so a widget or application style always wins over the
795    /// theme regardless of registration order. Themes write here via
796    /// [`LparTheme::apply_to_node`](crate::theme::LparTheme::apply_to_node).
797    ///
798    /// The style slot is lazily allocated on the first call.
799    pub fn add_theme_style(
800        &mut self,
801        patch: crate::style_cascade::StylePatch,
802        selector: crate::style_cascade::Selector,
803    ) {
804        let slot = self
805            .style
806            .get_or_insert_with(|| Box::new(crate::style_cascade::StyleState::new()));
807        crate::style_cascade::push_theme(slot, patch, selector);
808    }
809
810    /// Clear all default-theme style entries on this node (for theme re-apply).
811    ///
812    /// Returns the number of entries removed; `0` when the style slot is `None`.
813    pub fn clear_theme_styles(&mut self) -> usize {
814        match self.style.as_deref_mut() {
815            Some(slot) => crate::style_cascade::clear_theme(slot),
816            None => 0,
817        }
818    }
819
820    /// Remove local style entries whose selector matches `(part, states)`.
821    ///
822    /// A selector matches the `(part, states)` query when the selector's part
823    /// equals `part` and all state bits in `states` are present in the
824    /// selector's state mask (same predicate as cascade matching).
825    ///
826    /// Returns the number of entries removed. Returns `0` when the style slot
827    /// is `None`.
828    ///
829    /// To remove all local styles for a part regardless of state, call
830    /// [`remove_all_local_styles_by_part`](Self::remove_all_local_styles_by_part).
831    /// To clear everything, call [`remove_all_local_styles`](Self::remove_all_local_styles).
832    pub fn remove_local_styles(
833        &mut self,
834        part: crate::style_cascade::Part,
835        states: ObjectStates,
836    ) -> usize {
837        match self.style.as_deref_mut() {
838            Some(slot) => crate::style_cascade::remove_local_matching(slot, part, states),
839            None => 0,
840        }
841    }
842
843    /// Remove all local style entries for `part` regardless of state mask.
844    ///
845    /// This is the wildcard form described in §7.5 — removes every local entry
846    /// whose part equals `part`, regardless of its state selector.
847    ///
848    /// Returns the number of entries removed.
849    pub fn remove_all_local_styles_by_part(&mut self, part: crate::style_cascade::Part) -> usize {
850        match self.style.as_deref_mut() {
851            Some(slot) => crate::style_cascade::remove_all_local_by_part(slot, part),
852            None => 0,
853        }
854    }
855
856    /// Remove all local style entries from this node (unconditional clear).
857    ///
858    /// Returns the number of entries removed.
859    pub fn remove_all_local_styles(&mut self) -> usize {
860        match self.style.as_deref_mut() {
861            Some(slot) => crate::style_cascade::remove_all_local(slot),
862            None => 0,
863        }
864    }
865
866    // -----------------------------------------------------------------------
867    // Layout state accessors (LPAR-10 §5.A)
868    // -----------------------------------------------------------------------
869
870    /// Return the node's layout-computed bounds override if present, otherwise
871    /// the widget's intrinsic bounds (`Widget::bounds()`).
872    ///
873    /// This is the canonical bounds query for hit-testing and draw positioning.
874    /// Code that needs layout-aware placement MUST call this rather than
875    /// `widget.borrow().bounds()` directly.
876    pub fn effective_bounds(&self) -> Rect {
877        if let Some(ls) = self.layout.as_deref()
878            && let Some(computed) = ls.computed
879        {
880            return computed;
881        }
882        self.widget.borrow().bounds()
883    }
884
885    /// Configure this node as a flex layout container.
886    ///
887    /// Lazily allocates the `LayoutState` slot, sets the role to
888    /// `Container(Flex(config))`, and marks the node dirty.
889    pub fn set_layout_flex(&mut self, config: crate::layout::FlexConfig) {
890        let slot = self
891            .layout
892            .get_or_insert_with(|| Box::new(LayoutState::default()));
893        slot.role = crate::layout::LayoutRole::Container(crate::layout::EngineConfig::Flex(config));
894        slot.layout_dirty = true;
895    }
896
897    /// Configure this node as a grid layout container.
898    ///
899    /// Lazily allocates the `LayoutState` slot, sets the role to
900    /// `Container(Grid(config))`, and marks the node dirty.
901    pub fn set_layout_grid(&mut self, config: crate::layout::GridConfig) {
902        let slot = self
903            .layout
904            .get_or_insert_with(|| Box::new(LayoutState::default()));
905        slot.role = crate::layout::LayoutRole::Container(crate::layout::EngineConfig::Grid(config));
906        slot.layout_dirty = true;
907    }
908
909    /// Set the layout item hints for this node.
910    ///
911    /// Lazily allocates the `LayoutState` slot, sets the role to
912    /// `Item(hints)`, and marks the node dirty.
913    pub fn set_item_hints(&mut self, hints: crate::layout::ItemHints) {
914        let slot = self
915            .layout
916            .get_or_insert_with(|| Box::new(LayoutState::default()));
917        slot.role = crate::layout::LayoutRole::Item(hints);
918        slot.layout_dirty = true;
919    }
920
921    /// Mark this node's layout as dirty, triggering a re-layout on the next
922    /// [`run_layout`](crate::layout::run_layout) call.
923    pub fn mark_layout_dirty(&mut self) {
924        if let Some(ls) = self.layout.as_deref_mut() {
925            ls.layout_dirty = true;
926        }
927    }
928
929    // -----------------------------------------------------------------------
930    // Handler registration (LPAR-04 §6)
931    // -----------------------------------------------------------------------
932
933    /// Register a trickle-phase handler on this node.
934    ///
935    /// Trickle handlers run root→target before the target phase.  A handler
936    /// returning `true` consumes the event and stops all phases.
937    pub fn add_trickle_handler<F>(&mut self, handler: F)
938    where
939        F: FnMut(&ObjectEvent, EventContext) -> bool + 'static,
940    {
941        self.trickle_handlers.push(Box::new(handler));
942    }
943
944    /// Register a target-phase handler on this node.
945    ///
946    /// Target handlers run at the node when it is the dispatch target, after
947    /// the trickle phase.  A handler returning `true` consumes the event.
948    pub fn add_target_handler<F>(&mut self, handler: F)
949    where
950        F: FnMut(&ObjectEvent, EventContext) -> bool + 'static,
951    {
952        self.target_handlers.push(Box::new(handler));
953    }
954
955    /// Register a bubble-phase handler on this node.
956    ///
957    /// Bubble handlers run target→root after the target phase, but only while
958    /// [`ObjectFlags::EVENT_BUBBLE`] is set on the target (and on each
959    /// ancestor traversed).  A handler returning `true` consumes the event.
960    pub fn add_bubble_handler<F>(&mut self, handler: F)
961    where
962        F: FnMut(&ObjectEvent, EventContext) -> bool + 'static,
963    {
964        self.bubble_handlers.push(Box::new(handler));
965    }
966
967    /// Invoke all registered target-phase handlers for `event`.
968    ///
969    /// This is the direct handler-invocation path used by lifecycle delivery
970    /// (Attached/Detached/ChildChanged/Focused/Defocused), which runs outside
971    /// the trickle/bubble dispatch phases.
972    ///
973    /// Returns `true` if any handler consumed the event.
974    pub fn invoke_handlers_for(&mut self, event: &ObjectEvent) -> bool {
975        let ctx = EventContext {
976            target_tag: self.tag,
977            current_tag: self.tag,
978        };
979        for handler in &mut self.target_handlers {
980            if handler(event, ctx) {
981                return true;
982            }
983        }
984        false
985    }
986
987    // -----------------------------------------------------------------------
988    // Mutation helpers — emit lifecycle events (LPAR-04 §6.6)
989    // -----------------------------------------------------------------------
990
991    /// Append a child node and return its index.
992    ///
993    /// After the structural change, emits [`ObjectEvent::Attached`] to the
994    /// added subtree root's target handlers, then [`ObjectEvent::ChildChanged`]
995    /// to this node's target handlers.
996    ///
997    /// # Dispatch constraint
998    ///
999    /// This method MUST NOT be called from inside an active
1000    /// [`dispatch_object_event`] traversal (LPAR-02 §7.4 / LPAR-04 §6.6).
1001    pub fn append_child(&mut self, mut child: ObjectNode) -> usize {
1002        child.set_detached_recursive(false);
1003        self.children.push(child);
1004        let idx = self.children.len() - 1;
1005        // Lifecycle: Attached → subtree root.
1006        self.children[idx].invoke_handlers_for(&ObjectEvent::Attached);
1007        // Lifecycle: ChildChanged → parent.
1008        self.invoke_handlers_for(&ObjectEvent::ChildChanged);
1009        idx
1010    }
1011
1012    /// Insert a child node at `index`.
1013    ///
1014    /// Returns `false` when `index` is greater than the current child count.
1015    ///
1016    /// After the structural change, emits [`ObjectEvent::Attached`] to the
1017    /// added subtree root's target handlers, then [`ObjectEvent::ChildChanged`]
1018    /// to this node's target handlers.
1019    ///
1020    /// # Dispatch constraint
1021    ///
1022    /// This method MUST NOT be called from inside an active
1023    /// [`dispatch_object_event`] traversal (LPAR-02 §7.4 / LPAR-04 §6.6).
1024    pub fn insert_child(&mut self, index: usize, mut child: ObjectNode) -> bool {
1025        if index > self.children.len() {
1026            return false;
1027        }
1028        child.set_detached_recursive(false);
1029        self.children.insert(index, child);
1030        // Lifecycle: Attached → subtree root.
1031        self.children[index].invoke_handlers_for(&ObjectEvent::Attached);
1032        // Lifecycle: ChildChanged → parent.
1033        self.invoke_handlers_for(&ObjectEvent::ChildChanged);
1034        true
1035    }
1036
1037    /// Detach and return the child at `index`.
1038    ///
1039    /// The returned subtree is marked detached recursively.  After the
1040    /// structural change, emits [`ObjectEvent::Detached`] to the detached
1041    /// subtree root's target handlers, then [`ObjectEvent::ChildChanged`] to
1042    /// this node's target handlers.
1043    ///
1044    /// # Dispatch constraint
1045    ///
1046    /// This method MUST NOT be called from inside an active
1047    /// [`dispatch_object_event`] traversal (LPAR-02 §7.4 / LPAR-04 §6.6).
1048    pub fn detach_child(&mut self, index: usize) -> Option<ObjectNode> {
1049        if index >= self.children.len() {
1050            return None;
1051        }
1052        let mut child = self.children.remove(index);
1053        child.set_detached_recursive(true);
1054        // Lifecycle: Detached → detached subtree root.
1055        child.invoke_handlers_for(&ObjectEvent::Detached);
1056        // Lifecycle: ChildChanged → parent.
1057        self.invoke_handlers_for(&ObjectEvent::ChildChanged);
1058        Some(child)
1059    }
1060
1061    // -----------------------------------------------------------------------
1062    // Reorder helpers (emit ChildChanged → parent on an effective move,
1063    // per the LPAR-04 §5.3 "add/remove/reorder" rule)
1064    // -----------------------------------------------------------------------
1065
1066    /// Move the child at `index` to the front of the draw order.
1067    ///
1068    /// The front is the highest index and is considered visually topmost for
1069    /// hit testing. On a successful move, emits [`ObjectEvent::ChildChanged`]
1070    /// to this node's target handlers (LPAR-04 §5.3).
1071    pub fn raise_child(&mut self, index: usize) -> bool {
1072        if index >= self.children.len() {
1073            return false;
1074        }
1075        let child = self.children.remove(index);
1076        self.children.push(child);
1077        self.invoke_handlers_for(&ObjectEvent::ChildChanged);
1078        true
1079    }
1080
1081    /// Move the child at `index` to the back of the draw order.
1082    ///
1083    /// On a successful move, emits [`ObjectEvent::ChildChanged`] to this node's
1084    /// target handlers (LPAR-04 §5.3).
1085    pub fn lower_child(&mut self, index: usize) -> bool {
1086        if index >= self.children.len() {
1087            return false;
1088        }
1089        let child = self.children.remove(index);
1090        self.children.insert(0, child);
1091        self.invoke_handlers_for(&ObjectEvent::ChildChanged);
1092        true
1093    }
1094
1095    /// Move child `from` directly before child `before`.
1096    ///
1097    /// On an effective move (not a `from == before` no-op), emits
1098    /// [`ObjectEvent::ChildChanged`] to this node's target handlers
1099    /// (LPAR-04 §5.3).
1100    pub fn move_child_before(&mut self, from: usize, before: usize) -> bool {
1101        let len = self.children.len();
1102        if from >= len || before >= len {
1103            return false;
1104        }
1105        if from == before {
1106            return true;
1107        }
1108        let child = self.children.remove(from);
1109        let target = if from < before { before - 1 } else { before };
1110        self.children.insert(target, child);
1111        self.invoke_handlers_for(&ObjectEvent::ChildChanged);
1112        true
1113    }
1114
1115    /// Move child `from` directly after child `after`.
1116    ///
1117    /// On an effective move (not a `from == after` no-op), emits
1118    /// [`ObjectEvent::ChildChanged`] to this node's target handlers
1119    /// (LPAR-04 §5.3).
1120    pub fn move_child_after(&mut self, from: usize, after: usize) -> bool {
1121        let len = self.children.len();
1122        if from >= len || after >= len {
1123            return false;
1124        }
1125        if from == after {
1126            return true;
1127        }
1128        let child = self.children.remove(from);
1129        let mut target = after + 1;
1130        if from < target {
1131            target -= 1;
1132        }
1133        if target > self.children.len() {
1134            target = self.children.len();
1135        }
1136        self.children.insert(target, child);
1137        self.invoke_handlers_for(&ObjectEvent::ChildChanged);
1138        true
1139    }
1140
1141    // -----------------------------------------------------------------------
1142    // Draw and hit-test (unchanged from LPAR-02)
1143    // -----------------------------------------------------------------------
1144
1145    /// Recursively draw this object subtree.
1146    ///
1147    /// Hidden or detached subtrees are skipped. Visible nodes draw parent first
1148    /// and then children in sibling order.
1149    ///
1150    /// # LPAR-10 translation mechanism (§5.A)
1151    ///
1152    /// When a node has a layout-computed rect that differs from its widget's
1153    /// intrinsic bounds, drawing is routed through a
1154    /// [`ClipRenderer`](crate::renderer::ClipRenderer) that translates by
1155    /// `(effective_bounds.origin − widget.bounds().origin)` and clips to
1156    /// `effective_bounds`.  This repositions the widget's drawing to its
1157    /// computed origin without requiring the widget to know its external
1158    /// position.
1159    ///
1160    /// For resize-aware widgets that override `set_bounds`, the widget's own
1161    /// `bounds()` equals `effective_bounds` so the translation is zero and no
1162    /// `ClipRenderer` is interposed.
1163    pub fn draw(&self, renderer: &mut dyn Renderer) {
1164        if self.is_hidden_or_detached() {
1165            return;
1166        }
1167
1168        let intrinsic = self.widget.borrow().bounds();
1169        let effective = self.effective_bounds();
1170
1171        if intrinsic.x != effective.x || intrinsic.y != effective.y {
1172            // Translation needed: widget draws at intrinsic origin, but must
1173            // appear at effective origin.
1174            let dx = effective.x - intrinsic.x;
1175            let dy = effective.y - intrinsic.y;
1176            let mut clip_r = ClipRenderer::with_offset(renderer, effective, dx, dy);
1177            self.widget.borrow().draw(&mut clip_r);
1178        } else {
1179            self.widget.borrow().draw(renderer);
1180        }
1181
1182        for child in &self.children {
1183            child.draw(renderer);
1184        }
1185    }
1186
1187    /// Return the topmost targetable node at `x`, `y`.
1188    ///
1189    /// Hit testing searches children in reverse sibling order before testing
1190    /// the node itself. Hidden and detached subtrees are skipped. Disabled
1191    /// nodes are not targetable, but their visible children remain eligible in
1192    /// this base object phase.
1193    pub fn hit_test(&self, x: i32, y: i32) -> Option<&ObjectNode> {
1194        if self.is_hidden_or_detached() {
1195            return None;
1196        }
1197        for child in self.children.iter().rev() {
1198            if let Some(hit) = child.hit_test(x, y) {
1199                return Some(hit);
1200            }
1201        }
1202        if self.is_targetable_at(x, y) {
1203            Some(self)
1204        } else {
1205            None
1206        }
1207    }
1208
1209    /// Union of this widget's bounds with every visible descendant's bounds,
1210    /// in logical coordinates (LPAR-03 "subtree visual extent").
1211    ///
1212    /// Returns `None` when this node is hidden or detached, or when neither
1213    /// this widget nor any visible descendant has positive-area bounds.
1214    /// Zero-area bounds contribute nothing; hidden or detached subtrees are
1215    /// excluded entirely, exactly mirroring [`draw`](Self::draw) skipping.
1216    ///
1217    /// # Ordering contract (LPAR-03 §15)
1218    ///
1219    /// A hidden root yields `None` **by design**: invalidation callers order
1220    /// their operations as *compute-then-hide* (capture the extent before
1221    /// setting [`ObjectFlags::HIDDEN`]) and *show-then-compute* (clear the
1222    /// flag, then capture). The same applies to detach: capture the extent
1223    /// before [`detach_child`](Self::detach_child), then push the captured
1224    /// rect into the invalidation planner.
1225    pub fn visible_subtree_extent(&self) -> Option<Rect> {
1226        if self.is_hidden_or_detached() {
1227            return None;
1228        }
1229        // Use effective_bounds so layout-managed nodes contribute their
1230        // computed extent, not their intrinsic (possibly (0,0)) bounds.
1231        let bounds = self.effective_bounds();
1232        let mut extent = if bounds.width > 0 && bounds.height > 0 {
1233            Some(bounds)
1234        } else {
1235            None
1236        };
1237        for child in &self.children {
1238            if let Some(child_extent) = child.visible_subtree_extent() {
1239                extent = Some(match extent {
1240                    Some(current) => current.union(child_extent),
1241                    None => child_extent,
1242                });
1243            }
1244        }
1245        extent
1246    }
1247
1248    // -----------------------------------------------------------------------
1249    // Internal helpers
1250    // -----------------------------------------------------------------------
1251
1252    fn set_detached_recursive(&mut self, detached: bool) {
1253        self.meta.set_detached(detached);
1254        for child in &mut self.children {
1255            child.set_detached_recursive(detached);
1256        }
1257    }
1258
1259    fn is_hidden_or_detached(&self) -> bool {
1260        self.is_detached() || self.flags().contains(ObjectFlags::HIDDEN)
1261    }
1262
1263    /// Public(crate) version used by `focus.rs` for ancestor-visibility checks.
1264    pub(crate) fn is_hidden_or_detached_pub(&self) -> bool {
1265        self.is_hidden_or_detached()
1266    }
1267
1268    fn is_targetable_at(&self, x: i32, y: i32) -> bool {
1269        let flags = self.flags();
1270        flags.contains(ObjectFlags::CLICKABLE)
1271            && !flags.contains(ObjectFlags::DISABLED)
1272            && rect_contains(self.effective_bounds(), x, y)
1273    }
1274}
1275
1276// ---------------------------------------------------------------------------
1277// dispatch_object_event — the LPAR-04 routing entry point
1278// ---------------------------------------------------------------------------
1279
1280/// Dispatch an [`ObjectEvent`] through the `root` tree using
1281/// trickle → target → bubble propagation (LPAR-04 §6).
1282///
1283/// # Target resolution
1284///
1285/// - [`DispatchInput::Pointer`]: translates the stream `Event` into an
1286///   `ObjectEvent` and resolves the target via `hit_test(x, y)`.
1287/// - [`DispatchInput::PointerObject`]: dispatches the provided `ObjectEvent`
1288///   to the hit-test target at `(x, y)`.
1289/// - [`DispatchInput::Focused`]: dispatches to the node whose
1290///   [`ObjectStates::FOCUSED`] bit is set.
1291///
1292/// # Phase order
1293///
1294/// 1. **Trickle** — root→target, trickle handlers only.
1295/// 2. **Target** — target node: target handlers, then (for `Pointer` inputs)
1296///    the wrapped [`Widget::handle_event`].
1297/// 3. **Bubble** — target→root, bubble handlers; continues past the target
1298///    only when [`ObjectFlags::EVENT_BUBBLE`] is set on the target.
1299///
1300/// Any handler (or `Widget::handle_event` returning `true`) that consumes the
1301/// event terminates all remaining phases.
1302///
1303/// # Dispatch constraint
1304///
1305/// Mutation helpers ([`ObjectNode::append_child`] etc.) MUST NOT be called
1306/// from inside a handler during an active dispatch (LPAR-02 §7.4).
1307pub fn dispatch_object_event(root: &mut ObjectNode, input: DispatchInput) -> Disposition {
1308    // ----- 1. Resolve target path -----
1309    let (target_path, object_event, raw_stream_event) = match resolve_target(root, &input) {
1310        None => return Disposition::NoTarget,
1311        Some(t) => t,
1312    };
1313
1314    let target_tag = node_at_path(root, &target_path).tag();
1315
1316    // ----- 2. Trickle phase: root → target -----
1317    // Visit nodes [0..len] where path[0..i] is the prefix of the target path.
1318    // We visit ancestor nodes only (0..len), not the target itself.
1319    for depth in 0..target_path.len() {
1320        let node_path = &target_path[..depth];
1321        let ctx = EventContext {
1322            target_tag,
1323            current_tag: node_at_path(root, node_path).tag(),
1324        };
1325        let consumed = invoke_trickle(root, node_path, &object_event, ctx);
1326        if consumed {
1327            return Disposition::Consumed;
1328        }
1329    }
1330
1331    // ----- 3. Target phase -----
1332    {
1333        let ctx = EventContext {
1334            target_tag,
1335            current_tag: target_tag,
1336        };
1337        let target_node = node_at_path_mut(root, &target_path);
1338        // Run target handlers.
1339        for handler in &mut target_node.target_handlers {
1340            if handler(&object_event, ctx) {
1341                return Disposition::Consumed;
1342            }
1343        }
1344        // For pointer stream inputs, also invoke Widget::handle_event.
1345        if let Some(ref ev) = raw_stream_event
1346            && target_node.widget.borrow_mut().handle_event(ev)
1347        {
1348            return Disposition::Consumed;
1349        }
1350    }
1351
1352    // ----- 4. Bubble phase: target → root -----
1353    // The event is delivered to the target's bubble handlers, then ascends to
1354    // each ancestor's bubble handlers. Ascent from a node to its parent
1355    // continues only while that node has EVENT_BUBBLE set — the per-object rule
1356    // LVGL uses (LPAR-04 §6.4). A broken flag chain stops the bubble, so a
1357    // target whose ancestor clears the flag does not reach the root.
1358    let mut depth = target_path.len();
1359    loop {
1360        let node_path = &target_path[..depth];
1361        let ctx = EventContext {
1362            target_tag,
1363            current_tag: node_at_path(root, node_path).tag(),
1364        };
1365        if invoke_bubble(root, node_path, &object_event, ctx) {
1366            return Disposition::Consumed;
1367        }
1368        if depth == 0 {
1369            break;
1370        }
1371        // Continue to the parent only if THIS node opts into bubbling.
1372        if !node_at_path(root, node_path)
1373            .flags()
1374            .contains(ObjectFlags::EVENT_BUBBLE)
1375        {
1376            break;
1377        }
1378        depth -= 1;
1379    }
1380
1381    Disposition::Unconsumed
1382}
1383
1384// ---------------------------------------------------------------------------
1385// dispatch_object_event internals
1386// ---------------------------------------------------------------------------
1387
1388/// Resolve target path, object event, and optional raw stream event.
1389fn resolve_target(
1390    root: &ObjectNode,
1391    input: &DispatchInput,
1392) -> Option<(Vec<usize>, ObjectEvent, Option<Event>)> {
1393    match input {
1394        DispatchInput::Pointer { x, y, event } => {
1395            let obj_ev = stream_event_to_object_event(event)?;
1396            let path = hit_test_path(root, *x, *y)?;
1397            Some((path, obj_ev, Some(event.clone())))
1398        }
1399        DispatchInput::PointerObject { x, y, event } => {
1400            let path = hit_test_path(root, *x, *y)?;
1401            Some((path, event.clone(), None))
1402        }
1403        DispatchInput::Focused { event } => {
1404            let path = find_focused_path_ro(root)?;
1405            Some((path, event.clone(), None))
1406        }
1407        DispatchInput::Container { path, event } => Some((path.clone(), event.clone(), None)),
1408    }
1409}
1410
1411/// Map a stream [`Event`] to an [`ObjectEvent`] (pointer events only).
1412///
1413/// Returns `None` for events that have no direct pointer-target mapping
1414/// (e.g. `Tick`, `KeyDown`, `Encoder`).
1415fn stream_event_to_object_event(ev: &Event) -> Option<ObjectEvent> {
1416    match ev {
1417        Event::PressDown { x, y } => Some(ObjectEvent::Pressed { x: *x, y: *y }),
1418        Event::PressRelease { x, y } => Some(ObjectEvent::Clicked { x: *x, y: *y }),
1419        Event::DoubleTap { x, y } => Some(ObjectEvent::DoubleClicked { x: *x, y: *y }),
1420        Event::LongPress { x, y } => Some(ObjectEvent::LongPressed { x: *x, y: *y }),
1421        Event::LongPressRepeat { x, y } => Some(ObjectEvent::LongPressedRepeat { x: *x, y: *y }),
1422        Event::PointerUp { x, y } => Some(ObjectEvent::Released { x: *x, y: *y }),
1423        _ => None,
1424    }
1425}
1426
1427/// Hit-test and return the path to the topmost targetable node, as a sequence
1428/// of child indices from the root.
1429fn hit_test_path(root: &ObjectNode, x: i32, y: i32) -> Option<Vec<usize>> {
1430    hit_test_path_recursive(root, x, y, &mut Vec::new())
1431}
1432
1433fn hit_test_path_recursive(
1434    node: &ObjectNode,
1435    x: i32,
1436    y: i32,
1437    path: &mut Vec<usize>,
1438) -> Option<Vec<usize>> {
1439    if node.is_hidden_or_detached() {
1440        return None;
1441    }
1442    for (i, child) in node.children.iter().enumerate().rev() {
1443        path.push(i);
1444        if let Some(p) = hit_test_path_recursive(child, x, y, path) {
1445            return Some(p);
1446        }
1447        path.pop();
1448    }
1449    if node.is_targetable_at(x, y) {
1450        Some(path.clone())
1451    } else {
1452        None
1453    }
1454}
1455
1456/// Find the path to the focused node (read-only).
1457fn find_focused_path_ro(root: &ObjectNode) -> Option<Vec<usize>> {
1458    find_focused_ro_recursive(root, &mut Vec::new())
1459}
1460
1461fn find_focused_ro_recursive(node: &ObjectNode, path: &mut Vec<usize>) -> Option<Vec<usize>> {
1462    if node.states().contains(ObjectStates::FOCUSED) {
1463        return Some(path.clone());
1464    }
1465    for (i, child) in node.children.iter().enumerate() {
1466        path.push(i);
1467        if let Some(p) = find_focused_ro_recursive(child, path) {
1468            return Some(p);
1469        }
1470        path.pop();
1471    }
1472    None
1473}
1474
1475/// Navigate to a node by structural path (immutable).
1476fn node_at_path<'a>(root: &'a ObjectNode, path: &[usize]) -> &'a ObjectNode {
1477    let mut current = root;
1478    for &idx in path {
1479        current = &current.children[idx];
1480    }
1481    current
1482}
1483
1484/// Navigate to a node by structural path (mutable).
1485fn node_at_path_mut<'a>(root: &'a mut ObjectNode, path: &[usize]) -> &'a mut ObjectNode {
1486    let mut current = root;
1487    for &idx in path {
1488        current = &mut current.children[idx];
1489    }
1490    current
1491}
1492
1493/// Invoke trickle handlers at the node at `node_path`.
1494fn invoke_trickle(
1495    root: &mut ObjectNode,
1496    node_path: &[usize],
1497    event: &ObjectEvent,
1498    ctx: EventContext,
1499) -> bool {
1500    let node = node_at_path_mut(root, node_path);
1501    for handler in &mut node.trickle_handlers {
1502        if handler(event, ctx) {
1503            return true;
1504        }
1505    }
1506    false
1507}
1508
1509/// Invoke bubble handlers at the node at `node_path`.
1510fn invoke_bubble(
1511    root: &mut ObjectNode,
1512    node_path: &[usize],
1513    event: &ObjectEvent,
1514    ctx: EventContext,
1515) -> bool {
1516    let node = node_at_path_mut(root, node_path);
1517    for handler in &mut node.bubble_handlers {
1518        if handler(event, ctx) {
1519            return true;
1520        }
1521    }
1522    false
1523}
1524
1525fn rect_contains(rect: Rect, x: i32, y: i32) -> bool {
1526    rect.width > 0
1527        && rect.height > 0
1528        && x >= rect.x
1529        && y >= rect.y
1530        && x < rect.x + rect.width
1531        && y < rect.y + rect.height
1532}
1533
1534// ---------------------------------------------------------------------------
1535// Tests
1536// ---------------------------------------------------------------------------
1537
1538#[cfg(test)]
1539mod tests {
1540    use alloc::rc::Rc;
1541    use alloc::vec;
1542    use alloc::vec::Vec;
1543    use core::cell::RefCell;
1544
1545    use super::*;
1546    use crate::event::Event;
1547    use crate::renderer::Renderer;
1548    use crate::widget::{Color, Widget};
1549
1550    // -----------------------------------------------------------------------
1551    // Minimal test widget
1552    // -----------------------------------------------------------------------
1553
1554    struct TestWidget {
1555        tag: &'static str,
1556        bounds: Rect,
1557        /// If set, handle_event returns true for this event type.
1558        consume_clicks: bool,
1559    }
1560
1561    impl TestWidget {
1562        fn node(tag: &'static str, bounds: Rect) -> ObjectNode {
1563            ObjectNode::new(Rc::new(RefCell::new(Self {
1564                tag,
1565                bounds,
1566                consume_clicks: false,
1567            })))
1568            .with_tag(tag)
1569        }
1570    }
1571
1572    impl Widget for TestWidget {
1573        fn bounds(&self) -> Rect {
1574            self.bounds
1575        }
1576
1577        fn draw(&self, renderer: &mut dyn Renderer) {
1578            renderer.draw_text(
1579                (self.bounds.x, self.bounds.y),
1580                self.tag,
1581                Color(0, 0, 0, 255),
1582            );
1583        }
1584
1585        fn handle_event(&mut self, event: &Event) -> bool {
1586            self.consume_clicks && matches!(event, Event::PressRelease { .. })
1587        }
1588    }
1589
1590    struct TestRenderer {
1591        draws: Vec<&'static str>,
1592    }
1593
1594    impl Renderer for TestRenderer {
1595        fn fill_rect(&mut self, _rect: Rect, _color: Color) {}
1596
1597        fn draw_text(&mut self, _position: (i32, i32), text: &str, _color: Color) {
1598            match text {
1599                "root" => self.draws.push("root"),
1600                "a" => self.draws.push("a"),
1601                "b" => self.draws.push("b"),
1602                "c" => self.draws.push("c"),
1603                "grand" => self.draws.push("grand"),
1604                _ => {}
1605            }
1606        }
1607    }
1608
1609    fn rect(x: i32, y: i32, width: i32, height: i32) -> Rect {
1610        Rect {
1611            x,
1612            y,
1613            width,
1614            height,
1615        }
1616    }
1617
1618    // -----------------------------------------------------------------------
1619    // Existing LPAR-02 tests (must pass unchanged)
1620    // -----------------------------------------------------------------------
1621
1622    #[test]
1623    fn flags_and_states_store_and_clear() {
1624        let mut node = TestWidget::node("root", rect(0, 0, 10, 10));
1625
1626        node.set_flag(ObjectFlags::CLICKABLE, true);
1627        node.set_flag(ObjectFlags::FOCUSABLE, true);
1628        node.set_state(ObjectStates::FOCUSED, true);
1629
1630        assert!(node.flags().contains(ObjectFlags::CLICKABLE));
1631        assert!(node.flags().contains(ObjectFlags::FOCUSABLE));
1632        assert!(node.states().contains(ObjectStates::FOCUSED));
1633
1634        node.set_flag(ObjectFlags::FOCUSABLE, false);
1635        node.set_state(ObjectStates::FOCUSED, false);
1636
1637        assert!(!node.flags().contains(ObjectFlags::FOCUSABLE));
1638        assert!(!node.states().contains(ObjectStates::FOCUSED));
1639    }
1640
1641    #[test]
1642    fn draw_skips_hidden_subtree() {
1643        let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
1644        let mut hidden = TestWidget::node("a", rect(0, 0, 10, 10));
1645        hidden.set_flag(ObjectFlags::HIDDEN, true);
1646        hidden.append_child(TestWidget::node("grand", rect(0, 0, 10, 10)));
1647        root.append_child(hidden);
1648        root.append_child(TestWidget::node("b", rect(0, 0, 10, 10)));
1649
1650        let mut renderer = TestRenderer { draws: Vec::new() };
1651        root.draw(&mut renderer);
1652
1653        assert_eq!(renderer.draws, vec!["root", "b"]);
1654    }
1655
1656    #[test]
1657    fn hit_test_returns_topmost_clickable_node() {
1658        let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
1659        let mut a = TestWidget::node("a", rect(0, 0, 20, 20));
1660        let mut b = TestWidget::node("b", rect(0, 0, 20, 20));
1661        a.set_flag(ObjectFlags::CLICKABLE, true);
1662        b.set_flag(ObjectFlags::CLICKABLE, true);
1663        root.append_child(a);
1664        root.append_child(b);
1665
1666        assert_eq!(root.hit_test(5, 5).and_then(ObjectNode::tag), Some("b"));
1667
1668        root.children_mut()[1].set_flag(ObjectFlags::DISABLED, true);
1669        assert_eq!(root.hit_test(5, 5).and_then(ObjectNode::tag), Some("a"));
1670
1671        root.children_mut()[0].set_flag(ObjectFlags::HIDDEN, true);
1672        assert_eq!(root.hit_test(5, 5).and_then(ObjectNode::tag), None);
1673    }
1674
1675    #[test]
1676    fn child_reorder_helpers_update_sibling_order() {
1677        let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
1678        root.append_child(TestWidget::node("a", rect(0, 0, 10, 10)));
1679        root.append_child(TestWidget::node("b", rect(0, 0, 10, 10)));
1680        root.append_child(TestWidget::node("c", rect(0, 0, 10, 10)));
1681
1682        assert!(root.raise_child(0));
1683        assert_eq!(child_tags(&root), vec!["b", "c", "a"]);
1684
1685        assert!(root.lower_child(1));
1686        assert_eq!(child_tags(&root), vec!["c", "b", "a"]);
1687
1688        assert!(root.move_child_before(2, 1));
1689        assert_eq!(child_tags(&root), vec!["c", "a", "b"]);
1690
1691        assert!(root.move_child_after(0, 2));
1692        assert_eq!(child_tags(&root), vec!["a", "b", "c"]);
1693
1694        assert!(!root.raise_child(3));
1695        assert!(!root.move_child_before(0, 3));
1696    }
1697
1698    #[test]
1699    fn detach_child_marks_returned_subtree_detached() {
1700        let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
1701        let mut child = TestWidget::node("a", rect(0, 0, 10, 10));
1702        child.append_child(TestWidget::node("grand", rect(0, 0, 10, 10)));
1703        root.append_child(child);
1704
1705        let detached = root.detach_child(0).expect("child is present");
1706
1707        assert!(root.children().is_empty());
1708        assert!(detached.is_detached());
1709        assert!(detached.children()[0].is_detached());
1710    }
1711
1712    #[test]
1713    fn adopt_preserves_widget_node_shape() {
1714        let mut widget_root = crate::WidgetNode::new(Rc::new(RefCell::new(TestWidget {
1715            tag: "root",
1716            bounds: rect(0, 0, 100, 100),
1717            consume_clicks: false,
1718        })))
1719        .with_tag("root");
1720        widget_root.children.push(
1721            crate::WidgetNode::new(Rc::new(RefCell::new(TestWidget {
1722                tag: "a",
1723                bounds: rect(0, 0, 10, 10),
1724                consume_clicks: false,
1725            })))
1726            .with_tag("a"),
1727        );
1728
1729        let object_root = ObjectNode::adopt(widget_root);
1730
1731        assert_eq!(object_root.tag(), Some("root"));
1732        assert_eq!(object_root.children()[0].tag(), Some("a"));
1733        assert_eq!(object_root.flags(), ObjectFlags::EMPTY);
1734        assert_eq!(object_root.children()[0].states(), ObjectStates::DEFAULT);
1735    }
1736
1737    #[test]
1738    fn visible_subtree_extent_unions_children() {
1739        let mut root = TestWidget::node("root", rect(10, 10, 20, 20));
1740        root.append_child(TestWidget::node("a", rect(40, 5, 10, 10)));
1741        root.append_child(TestWidget::node("b", rect(0, 30, 10, 10)));
1742
1743        assert_eq!(root.visible_subtree_extent(), Some(rect(0, 5, 50, 35)));
1744    }
1745
1746    #[test]
1747    fn visible_subtree_extent_excludes_hidden_child() {
1748        let mut root = TestWidget::node("root", rect(0, 0, 10, 10));
1749        let mut hidden = TestWidget::node("a", rect(50, 50, 10, 10));
1750        hidden.set_flag(ObjectFlags::HIDDEN, true);
1751        root.append_child(hidden);
1752
1753        assert_eq!(root.visible_subtree_extent(), Some(rect(0, 0, 10, 10)));
1754    }
1755
1756    #[test]
1757    fn visible_subtree_extent_skips_zero_area_bounds() {
1758        let mut root = TestWidget::node("root", rect(0, 0, 0, 0));
1759        root.append_child(TestWidget::node("a", rect(5, 5, 10, 10)));
1760
1761        assert_eq!(root.visible_subtree_extent(), Some(rect(5, 5, 10, 10)));
1762    }
1763
1764    #[test]
1765    fn visible_subtree_extent_is_none_for_hidden_or_detached_root() {
1766        let mut hidden = TestWidget::node("a", rect(0, 0, 10, 10));
1767        hidden.set_flag(ObjectFlags::HIDDEN, true);
1768        assert_eq!(hidden.visible_subtree_extent(), None);
1769
1770        let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
1771        root.append_child(TestWidget::node("b", rect(0, 0, 10, 10)));
1772        let detached = root.detach_child(0).expect("child is present");
1773        assert_eq!(detached.visible_subtree_extent(), None);
1774    }
1775
1776    #[test]
1777    fn hide_flow_computes_extent_before_hiding() {
1778        use crate::invalidation::{InvalidationList, PresentPlan};
1779
1780        let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
1781        let mut panel = TestWidget::node("a", rect(10, 10, 20, 20));
1782        panel.append_child(TestWidget::node("grand", rect(25, 25, 20, 20)));
1783        root.append_child(panel);
1784
1785        let extent = root.children()[0].visible_subtree_extent();
1786        assert_eq!(extent, Some(rect(10, 10, 35, 35)));
1787        root.children_mut()[0].set_flag(ObjectFlags::HIDDEN, true);
1788        assert_eq!(root.children()[0].visible_subtree_extent(), None);
1789
1790        let mut list = InvalidationList::<4>::new(rect(0, 0, 100, 100));
1791        list.push_opt(extent);
1792        assert_eq!(list.plan(), PresentPlan::Rects(&[rect(10, 10, 35, 35)]));
1793    }
1794
1795    #[test]
1796    fn detach_flow_computes_extent_before_detaching() {
1797        use crate::invalidation::{InvalidationList, PresentPlan};
1798
1799        let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
1800        let mut panel = TestWidget::node("a", rect(60, 60, 30, 30));
1801        panel.append_child(TestWidget::node("grand", rect(80, 80, 30, 30)));
1802        root.append_child(panel);
1803
1804        let extent = root.children()[0]
1805            .visible_subtree_extent()
1806            .expect("visible subtree has an extent");
1807        assert_eq!(extent, rect(60, 60, 50, 50));
1808        let detached = root.detach_child(0).expect("child is present");
1809        assert_eq!(detached.visible_subtree_extent(), None);
1810
1811        let mut list = InvalidationList::<4>::new(rect(0, 0, 100, 100));
1812        list.push(extent);
1813        assert_eq!(list.plan(), PresentPlan::Rects(&[rect(60, 60, 40, 40)]));
1814    }
1815
1816    // -----------------------------------------------------------------------
1817    // LPAR-04: ObjectEvent tests
1818    // -----------------------------------------------------------------------
1819
1820    /// Build a clickable node in a 10x10 box at position (x,y).
1821    fn clickable(tag: &'static str, x: i32, y: i32) -> ObjectNode {
1822        let mut n = TestWidget::node(tag, rect(x, y, 10, 10));
1823        n.set_flag(ObjectFlags::CLICKABLE, true);
1824        n
1825    }
1826
1827    // -----------------------------------------------------------------------
1828    // LPAR-04: dispatch phase order trickle → target → bubble
1829    // -----------------------------------------------------------------------
1830
1831    #[test]
1832    fn dispatch_phase_order_trickle_target_bubble() {
1833        let visit_log: Rc<RefCell<Vec<(&'static str, &'static str)>>> =
1834            Rc::new(RefCell::new(Vec::new()));
1835
1836        let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
1837
1838        // Child node "a" at (0,0) is the click target.
1839        let mut a = clickable("a", 0, 0);
1840        // "a" opts into bubbling.
1841        a.set_flag(ObjectFlags::EVENT_BUBBLE, true);
1842        // Append child BEFORE registering handlers so the Attached lifecycle
1843        // event does not pollute the visit log.
1844        root.append_child(a);
1845
1846        // Register handlers after append so lifecycle emission is isolated.
1847
1848        // Root carries a trickle handler (records "root/trickle").
1849        {
1850            let log = visit_log.clone();
1851            root.add_trickle_handler(move |_ev, ctx| {
1852                log.borrow_mut()
1853                    .push((ctx.current_tag.unwrap_or("?"), "trickle"));
1854                false
1855            });
1856        }
1857        // Root also carries a bubble handler.
1858        {
1859            let log = visit_log.clone();
1860            root.add_bubble_handler(move |_ev, ctx| {
1861                log.borrow_mut()
1862                    .push((ctx.current_tag.unwrap_or("?"), "bubble"));
1863                false
1864            });
1865        }
1866        // "a" has a target handler.
1867        {
1868            let log = visit_log.clone();
1869            root.children_mut()[0].add_target_handler(move |_ev, ctx| {
1870                log.borrow_mut()
1871                    .push((ctx.current_tag.unwrap_or("?"), "target"));
1872                false
1873            });
1874        }
1875
1876        let ev = DispatchInput::PointerObject {
1877            x: 5,
1878            y: 5,
1879            event: ObjectEvent::Clicked { x: 5, y: 5 },
1880        };
1881        let result = dispatch_object_event(&mut root, ev);
1882
1883        assert_eq!(result, Disposition::Unconsumed);
1884        // Expected visit order: root/trickle → a/target → root/bubble
1885        let log = visit_log.borrow();
1886        assert_eq!(
1887            *log,
1888            vec![("root", "trickle"), ("a", "target"), ("root", "bubble"),]
1889        );
1890    }
1891
1892    // -----------------------------------------------------------------------
1893    // LPAR-04: stop-on-consume halts remaining phases
1894    // -----------------------------------------------------------------------
1895
1896    #[test]
1897    fn stop_on_consume_at_target_prevents_bubble() {
1898        let bubble_called: Rc<RefCell<bool>> = Rc::new(RefCell::new(false));
1899
1900        let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
1901        {
1902            let called = bubble_called.clone();
1903            root.add_bubble_handler(move |_ev, _ctx| {
1904                *called.borrow_mut() = true;
1905                false
1906            });
1907        }
1908
1909        let mut a = clickable("a", 0, 0);
1910        // Consuming target handler.
1911        a.add_target_handler(|_ev, _ctx| true);
1912        a.set_flag(ObjectFlags::EVENT_BUBBLE, true);
1913        root.append_child(a);
1914
1915        let result = dispatch_object_event(
1916            &mut root,
1917            DispatchInput::PointerObject {
1918                x: 5,
1919                y: 5,
1920                event: ObjectEvent::Clicked { x: 5, y: 5 },
1921            },
1922        );
1923
1924        assert_eq!(result, Disposition::Consumed);
1925        assert!(
1926            !*bubble_called.borrow(),
1927            "bubble must not run after consume"
1928        );
1929    }
1930
1931    #[test]
1932    fn stop_on_consume_at_trickle_prevents_target_and_bubble() {
1933        let target_called: Rc<RefCell<bool>> = Rc::new(RefCell::new(false));
1934        let bubble_called: Rc<RefCell<bool>> = Rc::new(RefCell::new(false));
1935
1936        let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
1937
1938        // Append child first, then register handlers so lifecycle emission
1939        // during append_child does not set the flags prematurely.
1940        let mut a = clickable("a", 0, 0);
1941        a.set_flag(ObjectFlags::EVENT_BUBBLE, true);
1942        root.append_child(a);
1943
1944        // Consuming trickle handler at root (registered after child is in tree).
1945        root.add_trickle_handler(|_ev, _ctx| true);
1946
1947        {
1948            let tc = target_called.clone();
1949            root.children_mut()[0].add_target_handler(move |_ev, _ctx| {
1950                *tc.borrow_mut() = true;
1951                false
1952            });
1953        }
1954        {
1955            let bc = bubble_called.clone();
1956            root.add_bubble_handler(move |_ev, _ctx| {
1957                *bc.borrow_mut() = true;
1958                false
1959            });
1960        }
1961
1962        let result = dispatch_object_event(
1963            &mut root,
1964            DispatchInput::PointerObject {
1965                x: 5,
1966                y: 5,
1967                event: ObjectEvent::Clicked { x: 5, y: 5 },
1968            },
1969        );
1970
1971        assert_eq!(result, Disposition::Consumed);
1972        assert!(!*target_called.borrow());
1973        assert!(!*bubble_called.borrow());
1974    }
1975
1976    // -----------------------------------------------------------------------
1977    // LPAR-04: EVENT_BUBBLE gating
1978    // -----------------------------------------------------------------------
1979
1980    #[test]
1981    fn bubble_only_when_event_bubble_flag_set() {
1982        let bubble_called: Rc<RefCell<bool>> = Rc::new(RefCell::new(false));
1983
1984        let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
1985        {
1986            let called = bubble_called.clone();
1987            root.add_bubble_handler(move |_ev, _ctx| {
1988                *called.borrow_mut() = true;
1989                false
1990            });
1991        }
1992
1993        // "a" does NOT have EVENT_BUBBLE.
1994        let a = clickable("a", 0, 0);
1995        root.append_child(a);
1996
1997        dispatch_object_event(
1998            &mut root,
1999            DispatchInput::PointerObject {
2000                x: 5,
2001                y: 5,
2002                event: ObjectEvent::Clicked { x: 5, y: 5 },
2003            },
2004        );
2005
2006        assert!(
2007            !*bubble_called.borrow(),
2008            "bubble must not run without EVENT_BUBBLE"
2009        );
2010    }
2011
2012    #[test]
2013    fn bubble_runs_when_event_bubble_flag_set() {
2014        let bubble_called: Rc<RefCell<bool>> = Rc::new(RefCell::new(false));
2015
2016        let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
2017        {
2018            let called = bubble_called.clone();
2019            root.add_bubble_handler(move |_ev, _ctx| {
2020                *called.borrow_mut() = true;
2021                false
2022            });
2023        }
2024
2025        let mut a = clickable("a", 0, 0);
2026        a.set_flag(ObjectFlags::EVENT_BUBBLE, true);
2027        root.append_child(a);
2028
2029        dispatch_object_event(
2030            &mut root,
2031            DispatchInput::PointerObject {
2032                x: 5,
2033                y: 5,
2034                event: ObjectEvent::Clicked { x: 5, y: 5 },
2035            },
2036        );
2037
2038        assert!(*bubble_called.borrow(), "bubble must run with EVENT_BUBBLE");
2039    }
2040
2041    #[test]
2042    fn bubble_stops_at_ancestor_without_event_bubble_flag() {
2043        // root → b → a(target). a opts into bubbling, b does NOT. The event
2044        // must reach b's bubble handler but stop there — never reaching root.
2045        let b_called: Rc<RefCell<bool>> = Rc::new(RefCell::new(false));
2046        let root_called: Rc<RefCell<bool>> = Rc::new(RefCell::new(false));
2047
2048        let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
2049        let mut b = TestWidget::node("b", rect(0, 0, 20, 20));
2050        let mut a = clickable("a", 0, 0);
2051        a.set_flag(ObjectFlags::EVENT_BUBBLE, true);
2052        b.append_child(a);
2053        // b deliberately has NO EVENT_BUBBLE flag.
2054        root.append_child(b);
2055
2056        // Register handlers after the tree is built so lifecycle emission
2057        // during append does not run them.
2058        {
2059            let called = b_called.clone();
2060            root.children_mut()[0].add_bubble_handler(move |_ev, _ctx| {
2061                *called.borrow_mut() = true;
2062                false
2063            });
2064        }
2065        {
2066            let called = root_called.clone();
2067            root.add_bubble_handler(move |_ev, _ctx| {
2068                *called.borrow_mut() = true;
2069                false
2070            });
2071        }
2072
2073        dispatch_object_event(
2074            &mut root,
2075            DispatchInput::PointerObject {
2076                x: 5,
2077                y: 5,
2078                event: ObjectEvent::Clicked { x: 5, y: 5 },
2079            },
2080        );
2081
2082        assert!(
2083            *b_called.borrow(),
2084            "bubble must reach the flagged target's parent"
2085        );
2086        assert!(
2087            !*root_called.borrow(),
2088            "bubble must stop at an ancestor that lacks EVENT_BUBBLE"
2089        );
2090    }
2091
2092    #[test]
2093    fn reorder_emits_child_changed_to_parent() {
2094        let count: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
2095
2096        let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
2097        root.append_child(TestWidget::node("a", rect(0, 0, 10, 10)));
2098        root.append_child(TestWidget::node("b", rect(0, 0, 10, 10)));
2099        root.append_child(TestWidget::node("c", rect(0, 0, 10, 10)));
2100
2101        // Registered after appends, so only reorders are counted.
2102        {
2103            let c = count.clone();
2104            root.add_target_handler(move |ev, _ctx| {
2105                if matches!(ev, ObjectEvent::ChildChanged) {
2106                    *c.borrow_mut() += 1;
2107                }
2108                false
2109            });
2110        }
2111
2112        assert!(root.raise_child(0));
2113        assert!(root.lower_child(1));
2114        assert!(root.move_child_before(2, 0));
2115        assert!(root.move_child_after(0, 2));
2116        assert_eq!(
2117            *count.borrow(),
2118            4,
2119            "each effective reorder emits ChildChanged"
2120        );
2121
2122        // A no-op move emits nothing.
2123        assert!(root.move_child_before(1, 1));
2124        assert_eq!(
2125            *count.borrow(),
2126            4,
2127            "a no-op move must not emit ChildChanged"
2128        );
2129    }
2130
2131    // -----------------------------------------------------------------------
2132    // LPAR-04: pointer target resolution via hit_test
2133    // -----------------------------------------------------------------------
2134
2135    #[test]
2136    fn pointer_target_resolved_via_hit_test() {
2137        let target_seen: Rc<RefCell<Option<&'static str>>> = Rc::new(RefCell::new(None));
2138
2139        let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
2140
2141        let mut a = clickable("a", 0, 0);
2142        {
2143            let ts = target_seen.clone();
2144            a.add_target_handler(move |_ev, ctx| {
2145                *ts.borrow_mut() = ctx.target_tag;
2146                false
2147            });
2148        }
2149        root.append_child(a);
2150
2151        dispatch_object_event(
2152            &mut root,
2153            DispatchInput::PointerObject {
2154                x: 5,
2155                y: 5,
2156                event: ObjectEvent::Clicked { x: 5, y: 5 },
2157            },
2158        );
2159
2160        assert_eq!(*target_seen.borrow(), Some("a"));
2161    }
2162
2163    #[test]
2164    fn pointer_no_target_returns_no_target() {
2165        let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
2166        // root has no CLICKABLE children; nothing to hit at (50,50).
2167        let result = dispatch_object_event(
2168            &mut root,
2169            DispatchInput::PointerObject {
2170                x: 50,
2171                y: 50,
2172                event: ObjectEvent::Clicked { x: 50, y: 50 },
2173            },
2174        );
2175        assert_eq!(result, Disposition::NoTarget);
2176    }
2177
2178    // -----------------------------------------------------------------------
2179    // LPAR-04: key/encoder via focus
2180    // -----------------------------------------------------------------------
2181
2182    #[test]
2183    fn key_event_resolves_to_focused_node() {
2184        let key_seen: Rc<RefCell<Option<&'static str>>> = Rc::new(RefCell::new(None));
2185
2186        let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
2187        let mut a = TestWidget::node("a", rect(0, 0, 10, 10));
2188        a.set_flag(ObjectFlags::FOCUSABLE, true);
2189        {
2190            let ks = key_seen.clone();
2191            a.add_target_handler(move |_ev, ctx| {
2192                *ks.borrow_mut() = ctx.target_tag;
2193                false
2194            });
2195        }
2196        root.append_child(a);
2197
2198        // Manually give "a" focus.
2199        root.children_mut()[0].set_state(ObjectStates::FOCUSED, true);
2200
2201        let result = dispatch_object_event(
2202            &mut root,
2203            DispatchInput::Focused {
2204                event: ObjectEvent::Key(crate::event::Key::Enter),
2205            },
2206        );
2207
2208        assert_eq!(result, Disposition::Unconsumed);
2209        assert_eq!(*key_seen.borrow(), Some("a"));
2210    }
2211
2212    #[test]
2213    fn key_event_no_focused_returns_no_target() {
2214        let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
2215        root.append_child(TestWidget::node("a", rect(0, 0, 10, 10)));
2216
2217        let result = dispatch_object_event(
2218            &mut root,
2219            DispatchInput::Focused {
2220                event: ObjectEvent::Key(crate::event::Key::Enter),
2221            },
2222        );
2223        assert_eq!(result, Disposition::NoTarget);
2224    }
2225
2226    // -----------------------------------------------------------------------
2227    // LPAR-04: Pointer stream event mapping
2228    // -----------------------------------------------------------------------
2229
2230    #[test]
2231    fn stream_press_release_maps_to_clicked() {
2232        let ev_seen: Rc<RefCell<Option<ObjectEvent>>> = Rc::new(RefCell::new(None));
2233
2234        let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
2235        let mut a = clickable("a", 0, 0);
2236        {
2237            let es = ev_seen.clone();
2238            a.add_target_handler(move |ev, _ctx| {
2239                *es.borrow_mut() = Some(ev.clone());
2240                false
2241            });
2242        }
2243        root.append_child(a);
2244
2245        dispatch_object_event(
2246            &mut root,
2247            DispatchInput::Pointer {
2248                x: 5,
2249                y: 5,
2250                event: Event::PressRelease { x: 5, y: 5 },
2251            },
2252        );
2253
2254        assert_eq!(*ev_seen.borrow(), Some(ObjectEvent::Clicked { x: 5, y: 5 }));
2255    }
2256
2257    #[test]
2258    fn stream_tick_produces_no_target() {
2259        let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
2260        root.append_child(clickable("a", 0, 0));
2261
2262        // Tick has no pointer-coordinate mapping; should return NoTarget.
2263        let result = dispatch_object_event(
2264            &mut root,
2265            DispatchInput::Pointer {
2266                x: 5,
2267                y: 5,
2268                event: Event::Tick,
2269            },
2270        );
2271        assert_eq!(result, Disposition::NoTarget);
2272    }
2273
2274    // -----------------------------------------------------------------------
2275    // LPAR-04: Widget::handle_event at target phase
2276    // -----------------------------------------------------------------------
2277
2278    #[test]
2279    fn widget_handle_event_at_target_phase_can_consume() {
2280        let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
2281        // Build a node whose widget returns true for PressRelease.
2282        let widget = Rc::new(RefCell::new(TestWidget {
2283            tag: "a",
2284            bounds: rect(0, 0, 10, 10),
2285            consume_clicks: true,
2286        }));
2287        let mut a = ObjectNode::new(widget).with_tag("a");
2288        a.set_flag(ObjectFlags::CLICKABLE, true);
2289        root.append_child(a);
2290
2291        let result = dispatch_object_event(
2292            &mut root,
2293            DispatchInput::Pointer {
2294                x: 5,
2295                y: 5,
2296                event: Event::PressRelease { x: 5, y: 5 },
2297            },
2298        );
2299        assert_eq!(result, Disposition::Consumed);
2300    }
2301
2302    // -----------------------------------------------------------------------
2303    // LPAR-04: Lifecycle events
2304    // -----------------------------------------------------------------------
2305
2306    #[test]
2307    fn append_child_emits_attached_and_child_changed() {
2308        let log: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
2309
2310        let mut parent = TestWidget::node("parent", rect(0, 0, 100, 100));
2311        {
2312            let l = log.clone();
2313            parent.add_target_handler(move |ev, _ctx| {
2314                if matches!(ev, ObjectEvent::ChildChanged) {
2315                    l.borrow_mut().push("parent:child_changed");
2316                }
2317                false
2318            });
2319        }
2320
2321        let mut child = TestWidget::node("child", rect(0, 0, 10, 10));
2322        {
2323            let l = log.clone();
2324            child.add_target_handler(move |ev, _ctx| {
2325                if matches!(ev, ObjectEvent::Attached) {
2326                    l.borrow_mut().push("child:attached");
2327                }
2328                false
2329            });
2330        }
2331
2332        parent.append_child(child);
2333
2334        assert_eq!(
2335            *log.borrow(),
2336            vec!["child:attached", "parent:child_changed"]
2337        );
2338    }
2339
2340    #[test]
2341    fn insert_child_emits_attached_and_child_changed() {
2342        let log: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
2343
2344        let mut parent = TestWidget::node("parent", rect(0, 0, 100, 100));
2345        {
2346            let l = log.clone();
2347            parent.add_target_handler(move |ev, _ctx| {
2348                if matches!(ev, ObjectEvent::ChildChanged) {
2349                    l.borrow_mut().push("parent:child_changed");
2350                }
2351                false
2352            });
2353        }
2354
2355        let mut child = TestWidget::node("child", rect(0, 0, 10, 10));
2356        {
2357            let l = log.clone();
2358            child.add_target_handler(move |ev, _ctx| {
2359                if matches!(ev, ObjectEvent::Attached) {
2360                    l.borrow_mut().push("child:attached");
2361                }
2362                false
2363            });
2364        }
2365
2366        let ok = parent.insert_child(0, child);
2367        assert!(ok);
2368        assert_eq!(
2369            *log.borrow(),
2370            vec!["child:attached", "parent:child_changed"]
2371        );
2372    }
2373
2374    #[test]
2375    fn detach_child_emits_detached_and_child_changed() {
2376        let log: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
2377        let mut parent = TestWidget::node("parent", rect(0, 0, 100, 100));
2378        {
2379            let l = log.clone();
2380            parent.add_target_handler(move |ev, _ctx| {
2381                if matches!(ev, ObjectEvent::ChildChanged) {
2382                    l.borrow_mut().push("parent:child_changed");
2383                }
2384                false
2385            });
2386        }
2387
2388        let mut child = TestWidget::node("child", rect(0, 0, 10, 10));
2389        {
2390            let l = log.clone();
2391            child.add_target_handler(move |ev, _ctx| {
2392                if matches!(ev, ObjectEvent::Detached) {
2393                    l.borrow_mut().push("child:detached");
2394                }
2395                false
2396            });
2397        }
2398
2399        parent.append_child(child);
2400        log.borrow_mut().clear(); // clear the Attached/ChildChanged from append.
2401
2402        let detached = parent.detach_child(0).expect("has child");
2403        drop(detached);
2404
2405        assert_eq!(
2406            *log.borrow(),
2407            vec!["child:detached", "parent:child_changed"]
2408        );
2409    }
2410
2411    #[test]
2412    fn detach_child_still_marks_subtree_detached() {
2413        let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
2414        let mut child = TestWidget::node("a", rect(0, 0, 10, 10));
2415        child.append_child(TestWidget::node("grand", rect(0, 0, 10, 10)));
2416        root.append_child(child);
2417
2418        let detached = root.detach_child(0).expect("child is present");
2419
2420        assert!(root.children().is_empty());
2421        assert!(detached.is_detached());
2422        assert!(detached.children()[0].is_detached());
2423    }
2424
2425    // -----------------------------------------------------------------------
2426    // LPAR-04: tag-based focus targeting must not exist
2427    // -----------------------------------------------------------------------
2428
2429    #[test]
2430    fn focus_path_not_tag_based() {
2431        // Verify that focus_path takes a &[usize] structural path, not a tag.
2432        // This test documents the API shape; it would fail to compile if the
2433        // wrong API were provided.
2434        use crate::focus::FocusGroup;
2435
2436        let mut root = TestWidget::node("root", rect(0, 0, 100, 100));
2437        let mut a = TestWidget::node("a", rect(0, 0, 10, 10));
2438        a.set_flag(ObjectFlags::FOCUSABLE, true);
2439        root.append_child(a);
2440
2441        let fg = FocusGroup::new();
2442        // Structural path [0] → child 0 of root ("a").
2443        let ok = fg.focus_path(&mut root, &[0usize]);
2444        assert!(ok);
2445        assert!(root.children()[0].states().contains(ObjectStates::FOCUSED));
2446    }
2447
2448    fn child_tags(root: &ObjectNode) -> Vec<&'static str> {
2449        root.children().iter().filter_map(ObjectNode::tag).collect()
2450    }
2451}