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}