Skip to main content

rlvgl_platform/
input_device.rs

1//! LPAR-04 §8 input-device adapters.
2//!
3//! Four thin typed wrappers that turn platform input into core [`Event`]
4//! streams plus the routing rules defined in LPAR-04 §8.  They are *adapters*,
5//! not a driver framework — existing raw-event producers (`PD`/`PM`/`PU`/`KD`/
6//! `KU`) remain valid without wrapping; this layer is additive.
7//!
8//! # Device classes (LPAR-04 §8.1)
9//!
10//! | Type | Produces | Routing rule |
11//! |---|---|---|
12//! | [`PointerDevice`] | `PointerDown/Move/Up`, optionally `Touch` | Hit-test target (§6.2), canonical recognizer chain (§8.3) |
13//! | [`KeypadDevice`] | `KeyDown`/`KeyUp`, tick-driven auto-repeat | Focused object as `ObjectEvent::Key` |
14//! | [`EncoderDevice`] | `Encoder { diff }`, Enter press | Navigate → focus movement; editing → `ObjectEvent::Rotary`/`Key` |
15//! | [`ButtonDevice`] | Synthesized `PointerDown/Up` at a configured screen point | Identical to Pointer after synthesis |
16//!
17//! # Per-instance state
18//!
19//! Two [`PointerDevice`]s do not share recognizer state (§8.4).  Keypad and
20//! encoder devices address focus through the single-`FOCUSED` tree invariant
21//! (§7.3); multiple simultaneous keypad/encoder devices targeting different
22//! focus groups in one tree are out of v1 scope.
23//!
24//! # Tick-domain durations
25//!
26//! All durations are Tick counts (LPAR-04 §9.1).  No wall-clock API appears
27//! anywhere in this module.  Default constants are documented against LVGL's
28//! nominal timings for a 30 Hz tick loop.
29
30use rlvgl_core::event::{Event, Key};
31use rlvgl_core::focus::FocusGroup;
32use rlvgl_core::object::{DispatchInput, Disposition, ObjectEvent, ObjectNode};
33use rlvgl_core::scroll::{ScrollConfig, ScrollController};
34use rlvgl_core::widget::Rect;
35
36use crate::gesture::{
37    DoubleTapRecognizer, DragRecognizer, LongPressConfig, LongPressRecognizer, TapRecognizer,
38};
39
40// ---------------------------------------------------------------------------
41// Keypad auto-repeat constants (§9.7)
42// ---------------------------------------------------------------------------
43
44/// Number of ticks a key must be held before auto-repeat begins.
45///
46/// At 30 Hz this is ≈ 400 ms, matching LVGL's nominal `lv_indev_get_act_obj`
47/// long-press delay and the [`crate::gesture::LONG_PRESS_TICKS`] precedent.
48pub const KEY_REPEAT_DELAY_TICKS: u32 = 12;
49
50/// Number of ticks between successive auto-repeat deliveries after the initial
51/// repeat fires.
52///
53/// At 30 Hz this is ≈ 100 ms, matching the
54/// [`crate::gesture::LONG_PRESS_REPEAT_TICKS`] precedent.
55pub const KEY_REPEAT_PERIOD_TICKS: u32 = 3;
56
57// ---------------------------------------------------------------------------
58// PointerDevice
59// ---------------------------------------------------------------------------
60
61/// LPAR-04 §8 Pointer input device adapter.
62///
63/// Owns the canonical recognizer chain:
64///
65/// ```text
66/// raw → DragRecognizer → TapRecognizer → LongPressRecognizer → DoubleTapRecognizer → dispatch
67/// ```
68///
69/// **Note on chain order vs. spec:** LPAR-04 §8.3 lists the chain as
70/// `raw → Drag → LongPress → Tap → DoubleTap`.  In practice, the
71/// `LongPressRecognizer` implementation arms on `PressDown` (the output of
72/// `TapRecognizer`), not on raw `PointerDown`.  The chain is therefore
73/// corrected to `Drag → Tap → LongPress → DoubleTap` so that:
74///
75/// 1. `PressDown` (debounced) arms the long-press timer.
76/// 2. `DragStart` cancels `TapRecognizer` (no `PressDown` emitted) **and**
77///    also explicitly cancels `LongPressRecognizer` (safety — e.g. if a
78///    contact moved past the drag threshold after the long-press fired).
79///
80/// This preserves the INPUT-00 §6.2 / LPAR-04 §9.4 cancellation contract.
81///
82/// # Scroll integration (LPAR-05 §7)
83///
84/// When constructed with [`with_scroll`](Self::with_scroll), an owned
85/// [`ScrollController`] sits above the recognizer chain.  On every
86/// `DragStart`/`DragMove`/`DragEnd` the controller is offered the event
87/// first.  If the controller activates a scroll session (it found a
88/// `SCROLLABLE` ancestor), the drag event is **not** re-dispatched to the
89/// object tree as a normal gesture — scrolling and gesture dispatch are
90/// mutually exclusive for the same contact (LPAR-05 §7.2).  Dirty rects
91/// are accumulated in an internal `Vec<Rect>` and can be drained with
92/// [`take_dirty`](Self::take_dirty).
93///
94/// Feed raw hardware events with [`process`](Self::process) and call
95/// [`tick`](Self::tick) once per frame tick to drive the long-press,
96/// double-tap timers, and (if enabled) the scroll throw tween.  Each
97/// resulting stream event is dispatched into the tree via
98/// `dispatch_object_event(root, DispatchInput::Pointer { x, y, event })`.
99pub struct PointerDevice {
100    drag: DragRecognizer,
101    tap: TapRecognizer,
102    long_press: LongPressRecognizer,
103    dtap: DoubleTapRecognizer,
104    /// Optional LPAR-05 scroll controller.  `None` when scroll wiring is not
105    /// enabled (the default constructors do not attach one).
106    scroll: Option<ScrollController>,
107    /// Accumulated viewport dirty rects from scroll offset changes.
108    dirty: alloc::vec::Vec<Rect>,
109}
110
111impl PointerDevice {
112    /// Create a pointer device with the default recognizer thresholds at the
113    /// given frame rate.
114    ///
115    /// `frame_hz` is used to convert the duration constants in
116    /// [`crate::gesture`] from milliseconds to ticks.
117    ///
118    /// Scroll integration is **disabled** by this constructor.  Use
119    /// [`with_scroll`](Self::with_scroll) to enable it.
120    pub fn new(frame_hz: u32) -> Self {
121        Self {
122            drag: DragRecognizer::new(),
123            tap: TapRecognizer::new(frame_hz),
124            long_press: LongPressRecognizer::new(),
125            dtap: DoubleTapRecognizer::new(frame_hz),
126            scroll: None,
127            dirty: alloc::vec::Vec::new(),
128        }
129    }
130
131    /// Create a pointer device with custom long-press thresholds.
132    ///
133    /// Use this to override the default 400 ms / 100 ms timings.
134    ///
135    /// Scroll integration is **disabled** by this constructor.  Use
136    /// [`with_scroll`](Self::with_scroll) on the returned instance to enable
137    /// it.
138    pub fn with_long_press_config(frame_hz: u32, lp_config: LongPressConfig) -> Self {
139        Self {
140            drag: DragRecognizer::new(),
141            tap: TapRecognizer::new(frame_hz),
142            long_press: LongPressRecognizer::with_config(lp_config),
143            dtap: DoubleTapRecognizer::new(frame_hz),
144            scroll: None,
145            dirty: alloc::vec::Vec::new(),
146        }
147    }
148
149    /// Attach a [`ScrollController`] to this device, enabling LPAR-05
150    /// drag→scroll composition.
151    ///
152    /// Can be called with a custom [`ScrollConfig`] or with the default via
153    /// [`ScrollConfig::default()`].
154    ///
155    /// # Composition rule (LPAR-05 §7.2)
156    ///
157    /// A drag event that activates a scroll session is consumed by the
158    /// controller and **MUST NOT** also be dispatched as a normal drag gesture
159    /// to the object tree.  Use [`take_dirty`](Self::take_dirty) after each
160    /// [`process`](Self::process) / [`tick`](Self::tick) call to drain the
161    /// accumulated viewport dirty rects and forward them to the LPAR-03
162    /// invalidation planner.
163    pub fn with_scroll(mut self, config: ScrollConfig) -> Self {
164        self.scroll = Some(ScrollController::new(config));
165        self
166    }
167
168    /// Drain and return all viewport dirty [`Rect`]s accumulated since the
169    /// last call.
170    ///
171    /// Each rect corresponds to the viewport of a scroll container whose
172    /// offset changed during the last [`process`](Self::process) or
173    /// [`tick`](Self::tick) call.  Forward these to the LPAR-03 invalidation
174    /// planner so the display driver repaints the affected areas.
175    ///
176    /// Returns an empty `Vec` when no scroll activity occurred or when scroll
177    /// integration is disabled.
178    pub fn take_dirty(&mut self) -> alloc::vec::Vec<Rect> {
179        core::mem::take(&mut self.dirty)
180    }
181
182    /// Feed one raw hardware event through the recognizer chain and dispatch
183    /// any resulting stream events to the tree.
184    ///
185    /// Raw events (`PointerDown`/`Move`/`Up`) traverse the chain in order.
186    /// Returns the [`Disposition`] of the *last* dispatch that was made for
187    /// this event, or [`Disposition::NoTarget`] when the chain produced no
188    /// dispatchable output.
189    ///
190    /// When scroll integration is enabled (see [`with_scroll`](Self::with_scroll)):
191    /// drag events (`DragStart`/`DragMove`/`DragEnd`) are offered to the
192    /// [`ScrollController`] first.  If the controller activates or continues a
193    /// scroll session, the drag is **suppressed** from normal object-tree
194    /// dispatch (LPAR-05 §7.2).  Dirty rects are accumulated; drain them with
195    /// [`take_dirty`](Self::take_dirty).
196    pub fn process(&mut self, root: &mut ObjectNode, raw: Event) -> Disposition {
197        let mut last = Disposition::NoTarget;
198
199        // ── Stage 1: DragRecognizer ───────────────────────────────────────
200        let after_drag = match self.drag.process(&raw) {
201            None => return Disposition::NoTarget,
202            Some(ev) => ev,
203        };
204
205        // DragStart: cancel tap AND long-press (INPUT-00 §6.2, LPAR-04 §9.4).
206        // Cancelling tap prevents a stale PressRelease.  Cancelling long-press
207        // is a safety measure in case it was armed before the drag threshold
208        // was crossed.
209        if matches!(after_drag, Event::DragStart { .. }) {
210            self.tap.cancel();
211            self.long_press.cancel();
212        }
213
214        // ── Scroll controller intercept (LPAR-05 §7.1–§7.2) ──────────────
215        // Offer DragStart/DragMove/DragEnd to the scroll controller before the
216        // tap/long-press stages.  Suppress the drag from normal object-tree
217        // dispatch if the controller owns this contact either *before* the call
218        // (an in-progress session — including the terminating `DragEnd` that a
219        // below-threshold release ends with `session = None`) or *after* it (a
220        // `DragStart` that just activated a session). Checking only the
221        // after-state would leak a scroll contact's final `DragEnd` into normal
222        // dispatch.
223        if matches!(
224            after_drag,
225            Event::DragStart { .. } | Event::DragMove { .. } | Event::DragEnd { .. }
226        ) && let Some(ref mut ctrl) = self.scroll
227        {
228            let was_active = ctrl.is_active();
229            let dirty = &mut self.dirty;
230            ctrl.process(root, &after_drag, &mut |r| dirty.push(r));
231            if was_active || ctrl.is_active() {
232                // The scroll controller owns this contact: drag consumed by
233                // scrolling. Do NOT fall through to normal dispatch.
234                return Disposition::Unconsumed;
235            }
236            // No session before or after (no SCROLLABLE ancestor, or wrong
237            // axis): fall through to normal drag dispatch as today.
238        }
239
240        // ── Stage 2: TapRecognizer ────────────────────────────────────────
241        // Converts PointerDown → PressDown (arms long-press downstream).
242        // PointerUp starts the settle timer (output deferred to tap.tick).
243        let after_tap = match self.tap.process(&after_drag) {
244            None => return last,
245            Some(ev) => ev,
246        };
247
248        // ── Stage 3: LongPressRecognizer (pass-through, additive) ─────────
249        // Arms on PressDown, disarms on DragStart/PressRelease/PointerUp.
250        let after_lp = match self.long_press.process(&after_tap) {
251            None => return last,
252            Some(ev) => ev,
253        };
254
255        // ── Stage 4: DoubleTapRecognizer → dispatch ───────────────────────
256        let (ev_a, ev_b) = self.dtap.process(&after_lp);
257        if let Some(ev) = ev_a {
258            last = dispatch_pointer_event(root, ev);
259        }
260        if let Some(ev) = ev_b {
261            last = dispatch_pointer_event(root, ev);
262        }
263
264        last
265    }
266
267    /// Advance all time-based recognizers one tick and dispatch any emitted
268    /// stream events to the tree.
269    ///
270    /// Call this once per [`Event::Tick`].  Drives the long-press recognizer
271    /// (for `LongPress`/`LongPressRepeat` delivery), the double-tap window
272    /// timer (for deferred single-tap delivery), and — when scroll integration
273    /// is enabled — the scroll throw/snap-settle tween.  Dirty rects from
274    /// throw ticks are accumulated; drain them with
275    /// [`take_dirty`](Self::take_dirty).
276    ///
277    /// Returns the [`Disposition`] of the last dispatch that occurred during
278    /// this tick, or [`Disposition::NoTarget`] if the tick produced no output.
279    pub fn tick(&mut self, root: &mut ObjectNode) -> Disposition {
280        let mut last = Disposition::NoTarget;
281
282        // Advance scroll throw/snap-settle tween (LPAR-05 §8.4, §9.4).
283        if let Some(ref mut ctrl) = self.scroll {
284            let dirty = &mut self.dirty;
285            ctrl.tick(root, &mut |r| dirty.push(r));
286        }
287
288        // Tap settle timer: may emit a deferred PressRelease.
289        // Feed through long_press (disarms it) then dtap.
290        if let Some(tap_ev) = self.tap.tick()
291            && let Some(after_lp) = self.long_press.process(&tap_ev)
292        {
293            let (ev_a, ev_b) = self.dtap.process(&after_lp);
294            if let Some(ev) = ev_a {
295                last = dispatch_pointer_event(root, ev);
296            }
297            if let Some(ev) = ev_b {
298                last = dispatch_pointer_event(root, ev);
299            }
300        }
301
302        // Long-press timer: may emit LongPress / LongPressRepeat.
303        // These bypass the tap stage (tap has no state change here) and go
304        // directly into dtap (which passes non-tap events through unchanged).
305        if let Some(lp_ev) = self.long_press.tick() {
306            let (ev_a, ev_b) = self.dtap.process(&lp_ev);
307            if let Some(ev) = ev_a {
308                last = dispatch_pointer_event(root, ev);
309            }
310            if let Some(ev) = ev_b {
311                last = dispatch_pointer_event(root, ev);
312            }
313        }
314
315        // Double-tap window timer: may emit a buffered PressRelease.
316        if let Some(dt_ev) = self.dtap.tick() {
317            last = dispatch_pointer_event(root, dt_ev);
318        }
319
320        last
321    }
322}
323
324/// Dispatch a single pointer-family stream event to the tree.
325///
326/// Extracts coordinates from the event and routes via
327/// `DispatchInput::Pointer`.  Events with no spatial coordinates (e.g.
328/// `Tick`, `KeyDown`) are silently dropped.
329fn dispatch_pointer_event(root: &mut ObjectNode, ev: Event) -> Disposition {
330    let (x, y) = match pointer_coords(&ev) {
331        Some(c) => c,
332        None => return Disposition::NoTarget,
333    };
334    rlvgl_core::object::dispatch_object_event(root, DispatchInput::Pointer { x, y, event: ev })
335}
336
337/// Extract `(x, y)` from a pointer-family stream event, or `None` for
338/// non-spatial events.
339fn pointer_coords(ev: &Event) -> Option<(i32, i32)> {
340    match ev {
341        Event::PointerDown { x, y }
342        | Event::PointerUp { x, y }
343        | Event::PointerMove { x, y }
344        | Event::PressDown { x, y }
345        | Event::PressRelease { x, y }
346        | Event::DoubleTap { x, y }
347        | Event::LongPress { x, y }
348        | Event::LongPressRepeat { x, y } => Some((*x, *y)),
349        Event::DragStart { x, y, .. } | Event::DragMove { x, y } | Event::DragEnd { x, y } => {
350            Some((*x, *y))
351        }
352        _ => None,
353    }
354}
355
356// ---------------------------------------------------------------------------
357// KeypadDevice
358// ---------------------------------------------------------------------------
359
360/// LPAR-04 §8 Keypad input device adapter with §9.7 tick-counted auto-repeat.
361///
362/// Routes `KeyDown`/`KeyUp` to the focused object via
363/// `dispatch_object_event(root, DispatchInput::Focused { event: ObjectEvent::Key(key) })`.
364///
365/// # Auto-repeat (§9.7)
366///
367/// While a key is held (a `KeyDown` received without a matching `KeyUp`),
368/// [`tick`](Self::tick) re-delivers `ObjectEvent::Key` to the focused object
369/// after [`KEY_REPEAT_DELAY_TICKS`] ticks, then every
370/// [`KEY_REPEAT_PERIOD_TICKS`] ticks thereafter.  These are synthetic
371/// `ObjectEvent::Key` re-deliveries — the stream never sees extra `KeyDown`
372/// events for repeats.
373///
374/// # Unconsumed keys
375///
376/// When dispatch returns [`Disposition::Unconsumed`] or
377/// [`Disposition::NoTarget`], the key was not consumed by the focused object.
378/// The caller's existing app-level post-dispatch path receives it through the
379/// normal application pump — `KeypadDevice` does **not** swallow unconsumed
380/// keys.
381pub struct KeypadDevice {
382    /// Key currently held, if any.
383    held_key: Option<Key>,
384    /// Tick counter since key-down (used for auto-repeat).
385    held_ticks: u32,
386    /// Ticks before auto-repeat begins.
387    repeat_delay: u32,
388    /// Ticks between successive auto-repeat deliveries after the first.
389    repeat_period: u32,
390}
391
392impl KeypadDevice {
393    /// Create a keypad device with the default auto-repeat constants
394    /// ([`KEY_REPEAT_DELAY_TICKS`] / [`KEY_REPEAT_PERIOD_TICKS`]).
395    pub fn new() -> Self {
396        Self {
397            held_key: None,
398            held_ticks: 0,
399            repeat_delay: KEY_REPEAT_DELAY_TICKS,
400            repeat_period: KEY_REPEAT_PERIOD_TICKS,
401        }
402    }
403
404    /// Create a keypad device with custom auto-repeat timing.
405    ///
406    /// `delay_ticks` is the hold duration before the first repeat.
407    /// `period_ticks` is the interval between subsequent repeats.
408    /// `period_ticks` is clamped to at least 1 to prevent infinite loops.
409    pub fn with_repeat(delay_ticks: u32, period_ticks: u32) -> Self {
410        Self {
411            held_key: None,
412            held_ticks: 0,
413            repeat_delay: delay_ticks,
414            repeat_period: period_ticks.max(1),
415        }
416    }
417
418    /// Process a `KeyDown` event: route to the focused object and arm
419    /// auto-repeat.
420    ///
421    /// Returns the [`Disposition`] from `dispatch_object_event`.
422    pub fn key_down(&mut self, root: &mut ObjectNode, key: Key) -> Disposition {
423        self.held_key = Some(key.clone());
424        self.held_ticks = 0;
425        dispatch_key(root, key)
426    }
427
428    /// Process a `KeyUp` event: disarm auto-repeat.
429    ///
430    /// Returns [`Disposition::Unconsumed`] — key-up has no `ObjectEvent`
431    /// counterpart in v1; the caller's app-level handler receives it through
432    /// the normal pump.
433    pub fn key_up(&mut self, _root: &mut ObjectNode, _key: Key) -> Disposition {
434        self.held_key = None;
435        self.held_ticks = 0;
436        Disposition::Unconsumed
437    }
438
439    /// Advance the auto-repeat timer one tick.
440    ///
441    /// If a key is currently held and the repeat schedule fires, re-delivers
442    /// `ObjectEvent::Key` to the focused object.  Returns the
443    /// [`Disposition`] of the re-delivery, or [`Disposition::NoTarget`] when
444    /// no repeat occurred this tick.
445    pub fn tick(&mut self, root: &mut ObjectNode) -> Disposition {
446        let key = match &self.held_key {
447            Some(k) => k.clone(),
448            None => return Disposition::NoTarget,
449        };
450
451        self.held_ticks += 1;
452
453        // Has auto-repeat started?
454        if self.held_ticks < self.repeat_delay {
455            return Disposition::NoTarget;
456        }
457
458        // Ticks past the initial delay.
459        let after_delay = self.held_ticks - self.repeat_delay;
460
461        // First repeat fires exactly when `after_delay == 0`.
462        // Subsequent repeats fire every `repeat_period` ticks.
463        let fires = after_delay == 0 || after_delay.is_multiple_of(self.repeat_period);
464
465        if fires {
466            dispatch_key(root, key)
467        } else {
468            Disposition::NoTarget
469        }
470    }
471}
472
473impl Default for KeypadDevice {
474    fn default() -> Self {
475        Self::new()
476    }
477}
478
479/// Deliver `ObjectEvent::Key(key)` to the focused node.
480fn dispatch_key(root: &mut ObjectNode, key: Key) -> Disposition {
481    rlvgl_core::object::dispatch_object_event(
482        root,
483        DispatchInput::Focused {
484            event: ObjectEvent::Key(key),
485        },
486    )
487}
488
489// ---------------------------------------------------------------------------
490// EncoderDevice
491// ---------------------------------------------------------------------------
492
493/// LPAR-04 §8 Encoder input device adapter.
494///
495/// Holds a [`FocusGroup`] and routes encoder events according to the group's
496/// navigate / editing mode:
497///
498/// - **Navigate mode** (`policy.editing == false`): positive `diff` calls
499///   `focus_next` once per detent; negative `diff` calls `focus_prev` once per
500///   detent.  A `diff` of `±N` steps focus `N` times in the corresponding
501///   direction (one step per detent — the simplest correct rule for a discrete
502///   encoder).
503///
504/// - **Editing mode** (`policy.editing == true`): delivers
505///   `ObjectEvent::Rotary { diff }` to the focused object for any non-zero
506///   `diff`, and `ObjectEvent::Key(Key::Enter)` for encoder presses.
507///
508/// # Mode toggle
509///
510/// An encoder press in either mode inverts `policy.editing` (LVGL encoder
511/// convention: Enter toggles between navigate and edit).  The press is
512/// delivered as `ObjectEvent::Key(Key::Enter)` to the focused object *before*
513/// the toggle in editing mode.
514///
515/// # Focus group ownership
516///
517/// Each `EncoderDevice` owns its own [`FocusGroup`], keeping the per-device
518/// state isolated (LPAR-04 §8.4).
519pub struct EncoderDevice {
520    /// Focus traversal and editing policy.
521    pub focus_group: FocusGroup,
522}
523
524impl EncoderDevice {
525    /// Create an encoder device with default focus policy (wrap enabled,
526    /// navigate mode).
527    pub fn new() -> Self {
528        Self {
529            focus_group: FocusGroup::new(),
530        }
531    }
532
533    /// Process a rotation event.
534    ///
535    /// - Navigate mode: steps focus `diff.abs()` times in the direction of
536    ///   the sign (positive → next, negative → prev).
537    /// - Editing mode: delivers `ObjectEvent::Rotary { diff }` to the focused
538    ///   object.  Returns [`Disposition::NoTarget`] when `diff == 0`.
539    pub fn rotate(&mut self, root: &mut ObjectNode, diff: i32) -> Disposition {
540        if diff == 0 {
541            return Disposition::NoTarget;
542        }
543
544        if self.focus_group.policy.editing {
545            // Editing mode: deliver rotation to the focused object.
546            rlvgl_core::object::dispatch_object_event(
547                root,
548                DispatchInput::Focused {
549                    event: ObjectEvent::Rotary { diff },
550                },
551            )
552        } else {
553            // Navigate mode: step focus by |diff| in the sign's direction.
554            let steps = diff.unsigned_abs();
555            let mut moved = false;
556            for _ in 0..steps {
557                if diff > 0 {
558                    moved |= self.focus_group.focus_next(root);
559                } else {
560                    moved |= self.focus_group.focus_prev(root);
561                }
562            }
563            if moved {
564                Disposition::Unconsumed
565            } else {
566                Disposition::NoTarget
567            }
568        }
569    }
570
571    /// Process an encoder press (the physical click of the encoder shaft).
572    ///
573    /// Toggles editing mode and:
574    /// - **Before toggle from editing → navigate**: delivers
575    ///   `ObjectEvent::Key(Key::Enter)` to the focused object.
576    /// - **Toggle from navigate → editing**: delivers
577    ///   `ObjectEvent::Key(Key::Enter)` to the focused object *after* entering
578    ///   editing mode (so the widget sees Enter while in edit mode).
579    ///
580    /// Returns the [`Disposition`] of the key delivery, or
581    /// [`Disposition::NoTarget`] if no focused object exists.
582    pub fn press(&mut self, root: &mut ObjectNode) -> Disposition {
583        let was_editing = self.focus_group.policy.editing;
584
585        if was_editing {
586            // Deliver Enter first (in editing mode), then exit editing.
587            let d = dispatch_key(root, Key::Enter);
588            self.focus_group.set_editing(root, false);
589            d
590        } else {
591            // Enter editing mode first, then deliver Enter to the (now editing) target.
592            self.focus_group.set_editing(root, true);
593            dispatch_key(root, Key::Enter)
594        }
595    }
596
597    /// Return whether the encoder is currently in editing mode.
598    pub fn is_editing(&self) -> bool {
599        self.focus_group.policy.editing
600    }
601
602    /// Directly set editing mode without delivering a key event.
603    ///
604    /// Sets or clears [`rlvgl_core::object::ObjectStates::EDITED`] on the
605    /// currently focused node in `root`.
606    pub fn set_editing(&mut self, root: &mut ObjectNode, editing: bool) {
607        self.focus_group.set_editing(root, editing);
608    }
609}
610
611impl Default for EncoderDevice {
612    fn default() -> Self {
613        Self::new()
614    }
615}
616
617// ---------------------------------------------------------------------------
618// ButtonDevice
619// ---------------------------------------------------------------------------
620
621/// A hardware button mapped to a screen point.
622///
623/// When the button is pressed, synthesizes a `PointerDown` at `(x, y)`.
624/// When released, synthesizes a `PointerUp` at `(x, y)`.  These are fed
625/// through the same [`PointerDevice`] path, so all recognizer logic (tap,
626/// drag, long-press) applies identically (LPAR-04 §8.1: "Identical to Pointer
627/// after synthesis").
628#[derive(Debug, Clone, Copy)]
629pub struct ButtonMapping {
630    /// Hardware button index (application-defined).
631    pub button_id: u32,
632    /// Screen x coordinate to synthesize pointer events at.
633    pub x: i32,
634    /// Screen y coordinate to synthesize pointer events at.
635    pub y: i32,
636}
637
638/// LPAR-04 §8 Button input device adapter.
639///
640/// Maps each configured hardware button to a screen point.  A button press
641/// synthesizes `PointerDown` at the mapped point; a button release synthesizes
642/// `PointerUp`.  Both are fed through an owned [`PointerDevice`] so the full
643/// recognizer chain applies — no new `Event` variants are introduced.
644///
645/// Two [`ButtonDevice`]s do not share recognizer state (per-device-instance
646/// rule, §8.4).
647pub struct ButtonDevice {
648    pointer: PointerDevice,
649    mappings: alloc::vec::Vec<ButtonMapping>,
650}
651
652impl ButtonDevice {
653    /// Create a button device with the given button→point mappings and frame
654    /// rate.
655    ///
656    /// `frame_hz` is forwarded to the owned [`PointerDevice`] for threshold
657    /// conversion.
658    pub fn new(frame_hz: u32, mappings: alloc::vec::Vec<ButtonMapping>) -> Self {
659        Self {
660            pointer: PointerDevice::new(frame_hz),
661            mappings,
662        }
663    }
664
665    /// Find the mapping for `button_id`.
666    fn mapping_for(&self, button_id: u32) -> Option<&ButtonMapping> {
667        self.mappings.iter().find(|m| m.button_id == button_id)
668    }
669
670    /// Process a button press: synthesize `PointerDown` at the mapped point
671    /// and feed it through the pointer device.
672    ///
673    /// Returns [`Disposition::NoTarget`] if `button_id` is not registered.
674    pub fn button_down(&mut self, root: &mut ObjectNode, button_id: u32) -> Disposition {
675        let (x, y) = match self.mapping_for(button_id) {
676            Some(m) => (m.x, m.y),
677            None => return Disposition::NoTarget,
678        };
679        self.pointer.process(root, Event::PointerDown { x, y })
680    }
681
682    /// Process a button release: synthesize `PointerUp` at the mapped point
683    /// and feed it through the pointer device.
684    ///
685    /// Returns [`Disposition::NoTarget`] if `button_id` is not registered.
686    pub fn button_up(&mut self, root: &mut ObjectNode, button_id: u32) -> Disposition {
687        let (x, y) = match self.mapping_for(button_id) {
688            Some(m) => (m.x, m.y),
689            None => return Disposition::NoTarget,
690        };
691        self.pointer.process(root, Event::PointerUp { x, y })
692    }
693
694    /// Advance the owned pointer device's timers one tick.
695    ///
696    /// Drives long-press and double-tap timers for button contacts.
697    pub fn tick(&mut self, root: &mut ObjectNode) -> Disposition {
698        self.pointer.tick(root)
699    }
700}
701
702// ---------------------------------------------------------------------------
703// Tests
704// ---------------------------------------------------------------------------
705
706#[cfg(test)]
707mod tests {
708    use alloc::boxed::Box;
709    use alloc::rc::Rc;
710    use alloc::vec;
711    use alloc::vec::Vec;
712    use core::cell::RefCell;
713
714    use rlvgl_core::event::{Event, Key};
715    use rlvgl_core::focus::FocusGroup;
716    use rlvgl_core::object::{Disposition, ObjectEvent, ObjectFlags, ObjectNode, ObjectStates};
717    use rlvgl_core::renderer::Renderer;
718    use rlvgl_core::widget::{Rect, Widget};
719
720    // Renderer is used by the Widget impl, Color by draw_text signature.
721    // They must remain in scope even though they appear "unused" in test bodies.
722
723    use super::*;
724
725    // -----------------------------------------------------------------------
726    // Minimal test widget
727    // -----------------------------------------------------------------------
728
729    struct TW {
730        bounds: Rect,
731        consume_clicks: bool,
732        // Stored so the widget keeps a reference alive alongside the handlers
733        // that capture a clone; not read directly by Widget methods.
734        #[allow(dead_code)]
735        events: Rc<RefCell<Vec<ObjectEvent>>>,
736    }
737
738    impl TW {
739        fn node(
740            tag: &'static str,
741            bounds: Rect,
742            events: Rc<RefCell<Vec<ObjectEvent>>>,
743        ) -> ObjectNode {
744            let widget = Rc::new(RefCell::new(TW {
745                bounds,
746                consume_clicks: false,
747                events,
748            }));
749            ObjectNode::new(widget).with_tag(tag)
750        }
751    }
752
753    impl Widget for TW {
754        fn bounds(&self) -> Rect {
755            self.bounds
756        }
757        fn draw(&self, _renderer: &mut dyn Renderer) {}
758        fn handle_event(&mut self, event: &Event) -> bool {
759            self.consume_clicks && matches!(event, Event::PressRelease { .. })
760        }
761    }
762
763    fn r(x: i32, y: i32, w: i32, h: i32) -> Rect {
764        Rect {
765            x,
766            y,
767            width: w,
768            height: h,
769        }
770    }
771
772    /// Make a clickable node at the given bounds.
773    fn clickable(
774        tag: &'static str,
775        bounds: Rect,
776        events: Rc<RefCell<Vec<ObjectEvent>>>,
777    ) -> ObjectNode {
778        let mut n = TW::node(tag, bounds, events.clone());
779        n.set_flag(ObjectFlags::CLICKABLE, true);
780        // Register a target handler that records all ObjectEvents.
781        let ev = events.clone();
782        n.add_target_handler(move |event, _ctx| {
783            ev.borrow_mut().push(event.clone());
784            false
785        });
786        n
787    }
788
789    /// Make a focusable node (not necessarily clickable).
790    fn focusable(
791        tag: &'static str,
792        bounds: Rect,
793        events: Rc<RefCell<Vec<ObjectEvent>>>,
794    ) -> ObjectNode {
795        let mut n = TW::node(tag, bounds, events.clone());
796        n.set_flag(ObjectFlags::FOCUSABLE, true);
797        let ev = events.clone();
798        n.add_target_handler(move |event, _ctx| {
799            ev.borrow_mut().push(event.clone());
800            false
801        });
802        n
803    }
804
805    // -----------------------------------------------------------------------
806    // PointerDevice tests
807    // -----------------------------------------------------------------------
808
809    /// A clean press-release over a clickable node dispatches Pressed then
810    /// Clicked to the node's handler.
811    #[test]
812    fn pointer_device_click_dispatches_pressed_and_clicked() {
813        let events = Rc::new(RefCell::new(Vec::new()));
814        let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
815            bounds: r(0, 0, 100, 100),
816            consume_clicks: false,
817            events: events.clone(),
818        })));
819        root.append_child(clickable("btn", r(10, 10, 30, 30), events.clone()));
820
821        let mut dev = PointerDevice::new(30);
822
823        // Down at (20, 20) — inside "btn".
824        dev.process(&mut root, Event::PointerDown { x: 20, y: 20 });
825        dev.process(&mut root, Event::PointerUp { x: 20, y: 20 });
826
827        // Settle the tap recognizer (SETTLE_MS / 30Hz = 6 ticks).
828        for _ in 0..10 {
829            dev.tick(&mut root);
830        }
831
832        // Wait for double-tap window to expire.
833        for _ in 0..20 {
834            dev.tick(&mut root);
835        }
836
837        let ev = events.borrow();
838        assert!(
839            ev.iter().any(|e| matches!(e, ObjectEvent::Pressed { .. })),
840            "expected Pressed: {ev:?}"
841        );
842        assert!(
843            ev.iter().any(|e| matches!(e, ObjectEvent::Clicked { .. })),
844            "expected Clicked: {ev:?}"
845        );
846    }
847
848    /// A drag-crossing contact produces no `ObjectEvent::Clicked`.
849    #[test]
850    fn pointer_device_drag_suppresses_clicked() {
851        let events = Rc::new(RefCell::new(Vec::new()));
852        let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
853            bounds: r(0, 0, 200, 200),
854            consume_clicks: false,
855            events: events.clone(),
856        })));
857        root.append_child(clickable("btn", r(0, 0, 200, 200), events.clone()));
858
859        let mut dev = PointerDevice::new(30);
860
861        dev.process(&mut root, Event::PointerDown { x: 10, y: 10 });
862        // Move far enough to cross the drag threshold (>10 px Euclidean).
863        dev.process(&mut root, Event::PointerMove { x: 30, y: 10 });
864        dev.process(&mut root, Event::PointerUp { x: 30, y: 10 });
865
866        // Drain settle + double-tap window.
867        for _ in 0..40 {
868            dev.tick(&mut root);
869        }
870
871        let ev = events.borrow();
872        assert!(
873            !ev.iter().any(|e| matches!(e, ObjectEvent::Clicked { .. })),
874            "drag-crossing contact must not produce Clicked: {ev:?}"
875        );
876    }
877
878    /// `tick()` drives a held contact to `LongPressed` delivery at the target.
879    #[test]
880    fn pointer_device_tick_drives_long_press() {
881        // Use a short threshold for this test.
882        let lp_config = LongPressConfig {
883            long_press_ticks: 5,
884            repeat_ticks: 3,
885        };
886
887        let events = Rc::new(RefCell::new(Vec::new()));
888        let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
889            bounds: r(0, 0, 100, 100),
890            consume_clicks: false,
891            events: events.clone(),
892        })));
893        root.append_child(clickable("btn", r(10, 10, 50, 50), events.clone()));
894
895        let mut dev = PointerDevice::with_long_press_config(30, lp_config);
896
897        // Press down and hold.
898        dev.process(&mut root, Event::PointerDown { x: 20, y: 20 });
899
900        // Tick past the long-press threshold.
901        for _ in 0..6 {
902            dev.tick(&mut root);
903        }
904
905        let ev = events.borrow();
906        assert!(
907            ev.iter()
908                .any(|e| matches!(e, ObjectEvent::LongPressed { .. })),
909            "expected LongPressed after threshold: {ev:?}"
910        );
911    }
912
913    // -----------------------------------------------------------------------
914    // KeypadDevice tests
915    // -----------------------------------------------------------------------
916
917    /// `KeyDown` to a focused node delivers `ObjectEvent::Key`.
918    #[test]
919    fn keypad_delivers_key_to_focused_node() {
920        let events = Rc::new(RefCell::new(Vec::new()));
921        let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
922            bounds: r(0, 0, 100, 100),
923            consume_clicks: false,
924            events: events.clone(),
925        })));
926        let node = focusable("input", r(0, 0, 50, 50), events.clone());
927        root.append_child(node);
928
929        // Focus the child.
930        let fg = FocusGroup::new();
931        fg.focus_next(&mut root);
932
933        let mut dev = KeypadDevice::new();
934        dev.key_down(&mut root, Key::Character('a'));
935
936        let ev = events.borrow();
937        assert!(
938            ev.iter()
939                .any(|e| matches!(e, ObjectEvent::Key(Key::Character('a')))),
940            "expected Key(Character('a')): {ev:?}"
941        );
942    }
943
944    /// Auto-repeat fires after delay, then at each period; exact tick counts
945    /// are asserted.
946    #[test]
947    fn keypad_auto_repeat_fires_at_exact_ticks() {
948        let events = Rc::new(RefCell::new(Vec::new()));
949        let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
950            bounds: r(0, 0, 100, 100),
951            consume_clicks: false,
952            events: events.clone(),
953        })));
954        let node = focusable("input", r(0, 0, 100, 100), events.clone());
955        root.append_child(node);
956
957        let fg = FocusGroup::new();
958        fg.focus_next(&mut root);
959
960        // Use short thresholds: delay=4, period=2.
961        let delay = 4u32;
962        let period = 2u32;
963        let mut dev = KeypadDevice::with_repeat(delay, period);
964
965        // Initial key delivery.
966        dev.key_down(&mut root, Key::Enter);
967
968        let mut repeat_ticks = Vec::new();
969
970        // Tick through delay + several periods.  Track which ticks produce a repeat.
971        for tick in 1..=(delay + period * 4) {
972            let d = dev.tick(&mut root);
973            if d != Disposition::NoTarget {
974                repeat_ticks.push(tick);
975            }
976        }
977
978        // First repeat at tick == delay; subsequent at delay+period, delay+2*period, ...
979        let expected: Vec<u32> = (0..=4).map(|i| delay + i * period).collect();
980        assert_eq!(repeat_ticks, expected, "auto-repeat tick schedule mismatch");
981
982        // The event log should contain one initial Key + 5 repeat Keys.
983        let ev = events.borrow();
984        let key_count = ev
985            .iter()
986            .filter(|e| matches!(e, ObjectEvent::Key(Key::Enter)))
987            .count();
988        assert_eq!(key_count, 1 + 5, "1 initial + 5 repeats expected");
989    }
990
991    /// Unconsumed key: dispatch returns `Unconsumed` or `NoTarget` when no
992    /// focused node exists.
993    #[test]
994    fn keypad_unconsumed_key_returns_no_target_when_no_focus() {
995        let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
996            bounds: r(0, 0, 100, 100),
997            consume_clicks: false,
998            events: Rc::new(RefCell::new(Vec::new())),
999        })));
1000
1001        let mut dev = KeypadDevice::new();
1002        let d = dev.key_down(&mut root, Key::Escape);
1003        // No focused node → NoTarget.
1004        assert_eq!(d, Disposition::NoTarget);
1005    }
1006
1007    // -----------------------------------------------------------------------
1008    // EncoderDevice tests
1009    // -----------------------------------------------------------------------
1010
1011    /// Navigate-mode diff moves focus next/prev.
1012    #[test]
1013    fn encoder_navigate_mode_diff_moves_focus() {
1014        let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
1015            bounds: r(0, 0, 200, 100),
1016            consume_clicks: false,
1017            events: Rc::new(RefCell::new(Vec::new())),
1018        })));
1019
1020        // Three focusable nodes side by side.
1021        for (tag, x) in [("a", 0), ("b", 50), ("c", 100)] {
1022            let mut n = TW::node(tag, r(x, 0, 40, 40), Rc::new(RefCell::new(Vec::new())));
1023            n.set_flag(ObjectFlags::FOCUSABLE, true);
1024            root.append_child(n);
1025        }
1026
1027        let mut enc = EncoderDevice::new();
1028        assert!(!enc.is_editing());
1029
1030        // +1 → focus "a"
1031        enc.rotate(&mut root, 1);
1032        assert_eq!(focused_tag(&root), Some("a"));
1033
1034        // +1 → focus "b"
1035        enc.rotate(&mut root, 1);
1036        assert_eq!(focused_tag(&root), Some("b"));
1037
1038        // -1 → focus "a"
1039        enc.rotate(&mut root, -1);
1040        assert_eq!(focused_tag(&root), Some("a"));
1041
1042        // +2 (multi-step) → focus "c" (a→b→c)
1043        enc.rotate(&mut root, 2);
1044        assert_eq!(focused_tag(&root), Some("c"));
1045    }
1046
1047    /// Editing-mode diff delivers `Rotary` to the focused node.
1048    #[test]
1049    fn encoder_editing_mode_delivers_rotary_to_focused() {
1050        let rotary_events = Rc::new(RefCell::new(Vec::new()));
1051        let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
1052            bounds: r(0, 0, 100, 100),
1053            consume_clicks: false,
1054            events: Rc::new(RefCell::new(Vec::new())),
1055        })));
1056
1057        let node = focusable("ctrl", r(0, 0, 100, 100), rotary_events.clone());
1058        root.append_child(node);
1059
1060        let mut enc = EncoderDevice::new();
1061
1062        // Focus the node first.
1063        enc.focus_group.focus_next(&mut root);
1064
1065        // Enter editing mode.
1066        enc.set_editing(&mut root, true);
1067        assert!(enc.is_editing());
1068
1069        enc.rotate(&mut root, 3);
1070
1071        let ev = rotary_events.borrow();
1072        assert!(
1073            ev.iter()
1074                .any(|e| matches!(e, ObjectEvent::Rotary { diff: 3 })),
1075            "expected Rotary {{ diff: 3 }}: {ev:?}"
1076        );
1077    }
1078
1079    /// Enter press toggles editing mode.
1080    #[test]
1081    fn encoder_enter_press_toggles_editing() {
1082        let key_events = Rc::new(RefCell::new(Vec::new()));
1083        let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
1084            bounds: r(0, 0, 100, 100),
1085            consume_clicks: false,
1086            events: Rc::new(RefCell::new(Vec::new())),
1087        })));
1088
1089        let node = focusable("ctrl", r(0, 0, 100, 100), key_events.clone());
1090        root.append_child(node);
1091
1092        let mut enc = EncoderDevice::new();
1093        enc.focus_group.focus_next(&mut root);
1094
1095        // Start in navigate mode; press → enter editing + deliver Enter.
1096        assert!(!enc.is_editing());
1097        enc.press(&mut root);
1098        assert!(enc.is_editing(), "first press should enter editing mode");
1099
1100        let ev = key_events.borrow();
1101        assert!(
1102            ev.iter().any(|e| matches!(e, ObjectEvent::Key(Key::Enter))),
1103            "expected Key(Enter): {ev:?}"
1104        );
1105        drop(ev);
1106
1107        // Second press → leave editing + deliver Enter.
1108        enc.press(&mut root);
1109        assert!(!enc.is_editing(), "second press should exit editing mode");
1110    }
1111
1112    // -----------------------------------------------------------------------
1113    // ButtonDevice tests
1114    // -----------------------------------------------------------------------
1115
1116    /// A button press synthesizes a pointer event at the mapped point and
1117    /// hits the right node.
1118    #[test]
1119    fn button_device_press_hits_mapped_node() {
1120        let events_a = Rc::new(RefCell::new(Vec::new()));
1121        let events_b = Rc::new(RefCell::new(Vec::new()));
1122
1123        let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
1124            bounds: r(0, 0, 200, 100),
1125            consume_clicks: false,
1126            events: Rc::new(RefCell::new(Vec::new())),
1127        })));
1128        root.append_child(clickable("a", r(0, 0, 50, 50), events_a.clone()));
1129        root.append_child(clickable("b", r(100, 0, 50, 50), events_b.clone()));
1130
1131        let mut dev = ButtonDevice::new(
1132            30,
1133            vec![
1134                ButtonMapping {
1135                    button_id: 0,
1136                    x: 10,
1137                    y: 10,
1138                }, // hits "a"
1139                ButtonMapping {
1140                    button_id: 1,
1141                    x: 110,
1142                    y: 10,
1143                }, // hits "b"
1144            ],
1145        );
1146
1147        // Press button 1 — should hit node "b".
1148        dev.button_down(&mut root, 1);
1149        dev.button_up(&mut root, 1);
1150
1151        // Settle.
1152        for _ in 0..30 {
1153            dev.tick(&mut root);
1154        }
1155
1156        let ea = events_a.borrow();
1157        let eb = events_b.borrow();
1158        assert!(
1159            ea.iter().all(|e| !matches!(e, ObjectEvent::Pressed { .. })),
1160            "node 'a' should not have been pressed: {ea:?}"
1161        );
1162        assert!(
1163            eb.iter().any(|e| matches!(e, ObjectEvent::Pressed { .. })),
1164            "node 'b' should have been pressed: {eb:?}"
1165        );
1166    }
1167
1168    // -----------------------------------------------------------------------
1169    // §7.6 WID adapter demonstration — focus → set_active
1170    // -----------------------------------------------------------------------
1171
1172    /// Demonstrates the §7.6 documented composition pattern: a
1173    /// `Focused`/`Defocused` handler on a node calls a stand-in
1174    /// `set_active(bool)` (a simple bool cell) WITHOUT the framework
1175    /// auto-calling it.  This shows that focus changes drive activation
1176    /// correctly via the application-level adapter pattern.
1177    ///
1178    /// The framework does NOT auto-call `set_active`; only the handler
1179    /// wired by the application does.
1180    #[test]
1181    fn focus_to_set_active_adapter_demonstration() {
1182        // Stand-in for a WID `set_active` field.
1183        let active = Rc::new(RefCell::new(false));
1184        let active2 = active.clone();
1185
1186        let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
1187            bounds: r(0, 0, 100, 100),
1188            consume_clicks: false,
1189            events: Rc::new(RefCell::new(Vec::new())),
1190        })));
1191
1192        let mut field = TW::node(
1193            "field",
1194            r(0, 0, 100, 100),
1195            Rc::new(RefCell::new(Vec::new())),
1196        );
1197        field.set_flag(ObjectFlags::FOCUSABLE, true);
1198
1199        // Application-level adapter: Focused → set_active(true),
1200        // Defocused → set_active(false).
1201        field.add_target_handler(move |event, _ctx| {
1202            match event {
1203                ObjectEvent::Focused => *active2.borrow_mut() = true,
1204                ObjectEvent::Defocused => *active2.borrow_mut() = false,
1205                _ => {}
1206            }
1207            false
1208        });
1209
1210        root.append_child(field);
1211
1212        // Second focusable node so focus can move away.
1213        let mut other = TW::node("other", r(0, 50, 50, 50), Rc::new(RefCell::new(Vec::new())));
1214        other.set_flag(ObjectFlags::FOCUSABLE, true);
1215        root.append_child(other);
1216
1217        let fg = FocusGroup::new();
1218
1219        // Initially not active.
1220        assert!(!*active.borrow(), "should start inactive");
1221
1222        // Move focus to "field" → adapter sets active = true.
1223        fg.focus_next(&mut root);
1224        assert!(
1225            *active.borrow(),
1226            "Focused event should activate via adapter"
1227        );
1228        // Confirm the focused node is actually "field".
1229        assert_eq!(focused_tag(&root), Some("field"));
1230
1231        // Move focus to "other" → adapter sets active = false.
1232        fg.focus_next(&mut root);
1233        assert!(
1234            !*active.borrow(),
1235            "Defocused event should deactivate via adapter"
1236        );
1237    }
1238
1239    // -----------------------------------------------------------------------
1240    // Helper
1241    // -----------------------------------------------------------------------
1242
1243    fn focused_tag(root: &ObjectNode) -> Option<&'static str> {
1244        if root.states().contains(ObjectStates::FOCUSED) {
1245            return root.tag();
1246        }
1247        for child in root.children() {
1248            if let Some(t) = focused_tag(child) {
1249                return Some(t);
1250            }
1251        }
1252        None
1253    }
1254
1255    // -----------------------------------------------------------------------
1256    // PointerDevice scroll integration tests (LPAR-05 §7)
1257    // -----------------------------------------------------------------------
1258
1259    use rlvgl_core::scroll::{ScrollConfig, ScrollState};
1260
1261    /// Build a minimal tree with a SCROLLABLE container for scroll tests.
1262    ///
1263    /// Layout: root(400×600) → container(400×600, SCROLLABLE, content_h=1200) → child(400×1200)
1264    fn scroll_tree_platform() -> ObjectNode {
1265        let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
1266            bounds: r(0, 0, 400, 600),
1267            consume_clicks: false,
1268            events: Rc::new(RefCell::new(Vec::new())),
1269        })));
1270
1271        let mut container = ObjectNode::new(Rc::new(RefCell::new(TW {
1272            bounds: r(0, 0, 400, 600),
1273            consume_clicks: false,
1274            events: Rc::new(RefCell::new(Vec::new())),
1275        })));
1276        let mut ss = ScrollState::new();
1277        ss.content_h = 1200;
1278        container.set_scroll_state(Box::new(ss));
1279
1280        let child = ObjectNode::new(Rc::new(RefCell::new(TW {
1281            bounds: r(0, 0, 400, 1200),
1282            consume_clicks: false,
1283            events: Rc::new(RefCell::new(Vec::new())),
1284        })));
1285        container.append_child(child);
1286        root.append_child(container);
1287        root
1288    }
1289
1290    /// A drag over a SCROLLABLE container changes the scroll offset and does
1291    /// NOT dispatch a normal drag gesture to the object tree.
1292    #[test]
1293    fn scroll_drag_moves_offset_and_suppresses_gesture_dispatch() {
1294        let gesture_events: Rc<RefCell<Vec<ObjectEvent>>> = Rc::new(RefCell::new(Vec::new()));
1295        let mut root = scroll_tree_platform();
1296
1297        // Register a target handler on the scrollable container to catch any
1298        // gesture ObjectEvents that leak through.
1299        {
1300            let ev = gesture_events.clone();
1301            // path [0] = container
1302            use rlvgl_core::object::ObjectNode;
1303            fn node_mut<'a>(root: &'a mut ObjectNode, path: &[usize]) -> &'a mut ObjectNode {
1304                let mut cur = root;
1305                for &i in path {
1306                    cur = &mut cur.children_mut()[i];
1307                }
1308                cur
1309            }
1310            node_mut(&mut root, &[0]).add_target_handler(move |event, _ctx| {
1311                match event {
1312                    ObjectEvent::Gesture { .. } | ObjectEvent::Pressed { .. } => {
1313                        ev.borrow_mut().push(event.clone());
1314                    }
1315                    _ => {}
1316                }
1317                false
1318            });
1319        }
1320
1321        let mut dev = PointerDevice::new(30).with_scroll(ScrollConfig::default());
1322
1323        // PointerDown + big move (crosses drag threshold) + PointerUp
1324        dev.process(&mut root, Event::PointerDown { x: 200, y: 300 });
1325        dev.process(&mut root, Event::PointerMove { x: 200, y: 270 }); // crosses drag threshold
1326        dev.process(&mut root, Event::PointerMove { x: 200, y: 250 });
1327        dev.process(&mut root, Event::PointerUp { x: 200, y: 250 });
1328        for _ in 0..30 {
1329            dev.tick(&mut root);
1330        }
1331
1332        // Offset must have changed.
1333        let offset = root.children()[0]
1334            .scroll_state()
1335            .expect("container must have scroll state")
1336            .offset_y;
1337        assert!(
1338            offset > 0,
1339            "scroll offset must increase after drag-down: {offset}"
1340        );
1341
1342        // Dirty rects must have been generated.
1343        let dirty = dev.take_dirty();
1344        // Some ticks may have already drained; check cumulatively by verifying
1345        // offset changed (dirty was pushed when offset changed).
1346        // The take here just verifies no panic.
1347        let _ = dirty;
1348
1349        // No normal drag gesture events should have been dispatched.
1350        let gestures = gesture_events.borrow();
1351        assert!(
1352            gestures.is_empty(),
1353            "drag over SCROLLABLE must not dispatch Gesture/Pressed: {gestures:?}"
1354        );
1355    }
1356
1357    /// A scroll contact that ends *below* the throw threshold (no snap, so the
1358    /// controller clears its session on the terminating `DragEnd`) must still
1359    /// not leak any drag-stream event into the object tree: the scroll
1360    /// controller owns the whole contact (LPAR-05 §7.1). Regression for
1361    /// checking `is_active()` only *after* `process()`, which misses the
1362    /// session-ending `DragEnd` (whose `process()` sets `session = None`).
1363    #[test]
1364    fn below_threshold_scroll_suppresses_terminating_drag_from_widgets() {
1365        struct RecW {
1366            bounds: Rect,
1367            seen: Rc<RefCell<Vec<Event>>>,
1368        }
1369        impl Widget for RecW {
1370            fn bounds(&self) -> Rect {
1371                self.bounds
1372            }
1373            fn draw(&self, _r: &mut dyn Renderer) {}
1374            fn handle_event(&mut self, ev: &Event) -> bool {
1375                self.seen.borrow_mut().push(ev.clone());
1376                false
1377            }
1378        }
1379
1380        let seen: Rc<RefCell<Vec<Event>>> = Rc::new(RefCell::new(Vec::new()));
1381        let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
1382            bounds: r(0, 0, 400, 600),
1383            consume_clicks: false,
1384            events: Rc::new(RefCell::new(Vec::new())),
1385        })));
1386        let mut container = ObjectNode::new(Rc::new(RefCell::new(TW {
1387            bounds: r(0, 0, 400, 600),
1388            consume_clicks: false,
1389            events: Rc::new(RefCell::new(Vec::new())),
1390        })));
1391        let mut ss = ScrollState::new();
1392        ss.content_h = 1200;
1393        container.set_scroll_state(Box::new(ss));
1394        let mut child = ObjectNode::new(Rc::new(RefCell::new(RecW {
1395            bounds: r(0, 0, 400, 1200),
1396            seen: seen.clone(),
1397        })));
1398        child.set_flag(ObjectFlags::CLICKABLE, true);
1399        container.append_child(child);
1400        root.append_child(container);
1401
1402        let mut dev = PointerDevice::new(30).with_scroll(ScrollConfig::default());
1403        // Press, cross the drag threshold (activates scroll), release — all in
1404        // the same tick so the release velocity is below the throw threshold.
1405        dev.process(&mut root, Event::PointerDown { x: 200, y: 300 });
1406        dev.process(&mut root, Event::PointerMove { x: 200, y: 270 });
1407        dev.process(&mut root, Event::PointerMove { x: 200, y: 250 });
1408        dev.process(&mut root, Event::PointerUp { x: 200, y: 250 });
1409
1410        // The scroll happened.
1411        assert!(
1412            root.children()[0]
1413                .scroll_state()
1414                .expect("container has scroll state")
1415                .offset_y
1416                > 0,
1417            "scroll offset must have advanced"
1418        );
1419        // No drag-stream event leaked into the child widget.
1420        let evs = seen.borrow();
1421        assert!(
1422            !evs.iter().any(|e| matches!(
1423                e,
1424                Event::DragStart { .. } | Event::DragMove { .. } | Event::DragEnd { .. }
1425            )),
1426            "a scroll contact must not leak drag events into widgets: {evs:?}"
1427        );
1428    }
1429
1430    /// A drag with no scrollable ancestor falls through to normal dispatch
1431    /// (the existing LPAR-04 behaviour is unaffected).
1432    #[test]
1433    fn scroll_no_scrollable_ancestor_falls_through_to_normal_dispatch() {
1434        // Tree with only a CLICKABLE node, no SCROLLABLE.
1435        let click_events: Rc<RefCell<Vec<ObjectEvent>>> = Rc::new(RefCell::new(Vec::new()));
1436        let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
1437            bounds: r(0, 0, 200, 200),
1438            consume_clicks: false,
1439            events: click_events.clone(),
1440        })));
1441        let child = clickable("btn", r(0, 0, 200, 200), click_events.clone());
1442        root.append_child(child);
1443
1444        let mut dev = PointerDevice::new(30).with_scroll(ScrollConfig::default());
1445
1446        dev.process(&mut root, Event::PointerDown { x: 10, y: 10 });
1447        // Move far enough to trigger a drag.
1448        dev.process(&mut root, Event::PointerMove { x: 30, y: 10 });
1449        dev.process(&mut root, Event::PointerUp { x: 30, y: 10 });
1450        for _ in 0..40 {
1451            dev.tick(&mut root);
1452        }
1453
1454        // The drag has no SCROLLABLE ancestor, so it must NOT have been
1455        // suppressed by the scroll controller — scroll stays inactive and
1456        // the normal gesture path runs.  Dirty rects must be empty.
1457        let dirty = dev.take_dirty();
1458        assert!(
1459            dirty.is_empty(),
1460            "no dirty rects without a scrollable ancestor"
1461        );
1462    }
1463
1464    /// The dirty sink receives the viewport rect when the scroll offset changes.
1465    #[test]
1466    fn scroll_dirty_sink_receives_viewport_rect_on_effective_scroll() {
1467        let mut root = scroll_tree_platform();
1468        let mut dev = PointerDevice::new(30).with_scroll(ScrollConfig::default());
1469
1470        // Accumulate dirty rects across the full gesture.
1471        let mut all_dirty: Vec<Rect> = Vec::new();
1472
1473        dev.process(&mut root, Event::PointerDown { x: 200, y: 400 });
1474        all_dirty.extend(dev.take_dirty());
1475
1476        dev.process(&mut root, Event::PointerMove { x: 200, y: 380 });
1477        all_dirty.extend(dev.take_dirty());
1478
1479        dev.process(&mut root, Event::PointerMove { x: 200, y: 350 });
1480        all_dirty.extend(dev.take_dirty());
1481
1482        dev.process(&mut root, Event::PointerUp { x: 200, y: 350 });
1483        all_dirty.extend(dev.take_dirty());
1484
1485        // At least one dirty rect must have been produced.
1486        assert!(
1487            !all_dirty.is_empty(),
1488            "dirty sink must receive rects on effective scroll"
1489        );
1490        // All dirty rects should correspond to the container viewport (400×600).
1491        for d in &all_dirty {
1492            assert_eq!(d.width, 400, "dirty rect width must match viewport");
1493            assert_eq!(d.height, 600, "dirty rect height must match viewport");
1494        }
1495    }
1496}