use rlvgl_core::event::{Event, Key};
use rlvgl_core::focus::FocusGroup;
use rlvgl_core::object::{DispatchInput, Disposition, ObjectEvent, ObjectNode};
use rlvgl_core::scroll::{ScrollConfig, ScrollController};
use rlvgl_core::widget::Rect;
use crate::gesture::{
DoubleTapRecognizer, DragRecognizer, LongPressConfig, LongPressRecognizer, TapRecognizer,
};
pub const KEY_REPEAT_DELAY_TICKS: u32 = 12;
pub const KEY_REPEAT_PERIOD_TICKS: u32 = 3;
pub struct PointerDevice {
drag: DragRecognizer,
tap: TapRecognizer,
long_press: LongPressRecognizer,
dtap: DoubleTapRecognizer,
scroll: Option<ScrollController>,
dirty: alloc::vec::Vec<Rect>,
}
impl PointerDevice {
pub fn new(frame_hz: u32) -> Self {
Self {
drag: DragRecognizer::new(),
tap: TapRecognizer::new(frame_hz),
long_press: LongPressRecognizer::new(),
dtap: DoubleTapRecognizer::new(frame_hz),
scroll: None,
dirty: alloc::vec::Vec::new(),
}
}
pub fn with_long_press_config(frame_hz: u32, lp_config: LongPressConfig) -> Self {
Self {
drag: DragRecognizer::new(),
tap: TapRecognizer::new(frame_hz),
long_press: LongPressRecognizer::with_config(lp_config),
dtap: DoubleTapRecognizer::new(frame_hz),
scroll: None,
dirty: alloc::vec::Vec::new(),
}
}
pub fn with_scroll(mut self, config: ScrollConfig) -> Self {
self.scroll = Some(ScrollController::new(config));
self
}
pub fn take_dirty(&mut self) -> alloc::vec::Vec<Rect> {
core::mem::take(&mut self.dirty)
}
pub fn process(&mut self, root: &mut ObjectNode, raw: Event) -> Disposition {
let mut last = Disposition::NoTarget;
let after_drag = match self.drag.process(&raw) {
None => return Disposition::NoTarget,
Some(ev) => ev,
};
if matches!(after_drag, Event::DragStart { .. }) {
self.tap.cancel();
self.long_press.cancel();
}
if matches!(
after_drag,
Event::DragStart { .. } | Event::DragMove { .. } | Event::DragEnd { .. }
) && let Some(ref mut ctrl) = self.scroll
{
let was_active = ctrl.is_active();
let dirty = &mut self.dirty;
ctrl.process(root, &after_drag, &mut |r| dirty.push(r));
if was_active || ctrl.is_active() {
return Disposition::Unconsumed;
}
}
let after_tap = match self.tap.process(&after_drag) {
None => return last,
Some(ev) => ev,
};
let after_lp = match self.long_press.process(&after_tap) {
None => return last,
Some(ev) => ev,
};
let (ev_a, ev_b) = self.dtap.process(&after_lp);
if let Some(ev) = ev_a {
last = dispatch_pointer_event(root, ev);
}
if let Some(ev) = ev_b {
last = dispatch_pointer_event(root, ev);
}
last
}
pub fn tick(&mut self, root: &mut ObjectNode) -> Disposition {
let mut last = Disposition::NoTarget;
if let Some(ref mut ctrl) = self.scroll {
let dirty = &mut self.dirty;
ctrl.tick(root, &mut |r| dirty.push(r));
}
if let Some(tap_ev) = self.tap.tick()
&& let Some(after_lp) = self.long_press.process(&tap_ev)
{
let (ev_a, ev_b) = self.dtap.process(&after_lp);
if let Some(ev) = ev_a {
last = dispatch_pointer_event(root, ev);
}
if let Some(ev) = ev_b {
last = dispatch_pointer_event(root, ev);
}
}
if let Some(lp_ev) = self.long_press.tick() {
let (ev_a, ev_b) = self.dtap.process(&lp_ev);
if let Some(ev) = ev_a {
last = dispatch_pointer_event(root, ev);
}
if let Some(ev) = ev_b {
last = dispatch_pointer_event(root, ev);
}
}
if let Some(dt_ev) = self.dtap.tick() {
last = dispatch_pointer_event(root, dt_ev);
}
last
}
}
fn dispatch_pointer_event(root: &mut ObjectNode, ev: Event) -> Disposition {
let (x, y) = match pointer_coords(&ev) {
Some(c) => c,
None => return Disposition::NoTarget,
};
rlvgl_core::object::dispatch_object_event(root, DispatchInput::Pointer { x, y, event: ev })
}
fn pointer_coords(ev: &Event) -> Option<(i32, i32)> {
match ev {
Event::PointerDown { x, y }
| Event::PointerUp { x, y }
| Event::PointerMove { x, y }
| Event::PressDown { x, y }
| Event::PressRelease { x, y }
| Event::DoubleTap { x, y }
| Event::LongPress { x, y }
| Event::LongPressRepeat { x, y } => Some((*x, *y)),
Event::DragStart { x, y, .. } | Event::DragMove { x, y } | Event::DragEnd { x, y } => {
Some((*x, *y))
}
_ => None,
}
}
pub struct KeypadDevice {
held_key: Option<Key>,
held_ticks: u32,
repeat_delay: u32,
repeat_period: u32,
}
impl KeypadDevice {
pub fn new() -> Self {
Self {
held_key: None,
held_ticks: 0,
repeat_delay: KEY_REPEAT_DELAY_TICKS,
repeat_period: KEY_REPEAT_PERIOD_TICKS,
}
}
pub fn with_repeat(delay_ticks: u32, period_ticks: u32) -> Self {
Self {
held_key: None,
held_ticks: 0,
repeat_delay: delay_ticks,
repeat_period: period_ticks.max(1),
}
}
pub fn key_down(&mut self, root: &mut ObjectNode, key: Key) -> Disposition {
self.held_key = Some(key.clone());
self.held_ticks = 0;
dispatch_key(root, key)
}
pub fn key_up(&mut self, _root: &mut ObjectNode, _key: Key) -> Disposition {
self.held_key = None;
self.held_ticks = 0;
Disposition::Unconsumed
}
pub fn tick(&mut self, root: &mut ObjectNode) -> Disposition {
let key = match &self.held_key {
Some(k) => k.clone(),
None => return Disposition::NoTarget,
};
self.held_ticks += 1;
if self.held_ticks < self.repeat_delay {
return Disposition::NoTarget;
}
let after_delay = self.held_ticks - self.repeat_delay;
let fires = after_delay == 0 || after_delay.is_multiple_of(self.repeat_period);
if fires {
dispatch_key(root, key)
} else {
Disposition::NoTarget
}
}
}
impl Default for KeypadDevice {
fn default() -> Self {
Self::new()
}
}
fn dispatch_key(root: &mut ObjectNode, key: Key) -> Disposition {
rlvgl_core::object::dispatch_object_event(
root,
DispatchInput::Focused {
event: ObjectEvent::Key(key),
},
)
}
pub struct EncoderDevice {
pub focus_group: FocusGroup,
}
impl EncoderDevice {
pub fn new() -> Self {
Self {
focus_group: FocusGroup::new(),
}
}
pub fn rotate(&mut self, root: &mut ObjectNode, diff: i32) -> Disposition {
if diff == 0 {
return Disposition::NoTarget;
}
if self.focus_group.policy.editing {
rlvgl_core::object::dispatch_object_event(
root,
DispatchInput::Focused {
event: ObjectEvent::Rotary { diff },
},
)
} else {
let steps = diff.unsigned_abs();
let mut moved = false;
for _ in 0..steps {
if diff > 0 {
moved |= self.focus_group.focus_next(root);
} else {
moved |= self.focus_group.focus_prev(root);
}
}
if moved {
Disposition::Unconsumed
} else {
Disposition::NoTarget
}
}
}
pub fn press(&mut self, root: &mut ObjectNode) -> Disposition {
let was_editing = self.focus_group.policy.editing;
if was_editing {
let d = dispatch_key(root, Key::Enter);
self.focus_group.set_editing(root, false);
d
} else {
self.focus_group.set_editing(root, true);
dispatch_key(root, Key::Enter)
}
}
pub fn is_editing(&self) -> bool {
self.focus_group.policy.editing
}
pub fn set_editing(&mut self, root: &mut ObjectNode, editing: bool) {
self.focus_group.set_editing(root, editing);
}
}
impl Default for EncoderDevice {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy)]
pub struct ButtonMapping {
pub button_id: u32,
pub x: i32,
pub y: i32,
}
pub struct ButtonDevice {
pointer: PointerDevice,
mappings: alloc::vec::Vec<ButtonMapping>,
}
impl ButtonDevice {
pub fn new(frame_hz: u32, mappings: alloc::vec::Vec<ButtonMapping>) -> Self {
Self {
pointer: PointerDevice::new(frame_hz),
mappings,
}
}
fn mapping_for(&self, button_id: u32) -> Option<&ButtonMapping> {
self.mappings.iter().find(|m| m.button_id == button_id)
}
pub fn button_down(&mut self, root: &mut ObjectNode, button_id: u32) -> Disposition {
let (x, y) = match self.mapping_for(button_id) {
Some(m) => (m.x, m.y),
None => return Disposition::NoTarget,
};
self.pointer.process(root, Event::PointerDown { x, y })
}
pub fn button_up(&mut self, root: &mut ObjectNode, button_id: u32) -> Disposition {
let (x, y) = match self.mapping_for(button_id) {
Some(m) => (m.x, m.y),
None => return Disposition::NoTarget,
};
self.pointer.process(root, Event::PointerUp { x, y })
}
pub fn tick(&mut self, root: &mut ObjectNode) -> Disposition {
self.pointer.tick(root)
}
}
#[cfg(test)]
mod tests {
use alloc::boxed::Box;
use alloc::rc::Rc;
use alloc::vec;
use alloc::vec::Vec;
use core::cell::RefCell;
use rlvgl_core::event::{Event, Key};
use rlvgl_core::focus::FocusGroup;
use rlvgl_core::object::{Disposition, ObjectEvent, ObjectFlags, ObjectNode, ObjectStates};
use rlvgl_core::renderer::Renderer;
use rlvgl_core::widget::{Rect, Widget};
use super::*;
struct TW {
bounds: Rect,
consume_clicks: bool,
#[allow(dead_code)]
events: Rc<RefCell<Vec<ObjectEvent>>>,
}
impl TW {
fn node(
tag: &'static str,
bounds: Rect,
events: Rc<RefCell<Vec<ObjectEvent>>>,
) -> ObjectNode {
let widget = Rc::new(RefCell::new(TW {
bounds,
consume_clicks: false,
events,
}));
ObjectNode::new(widget).with_tag(tag)
}
}
impl Widget for TW {
fn bounds(&self) -> Rect {
self.bounds
}
fn draw(&self, _renderer: &mut dyn Renderer) {}
fn handle_event(&mut self, event: &Event) -> bool {
self.consume_clicks && matches!(event, Event::PressRelease { .. })
}
}
fn r(x: i32, y: i32, w: i32, h: i32) -> Rect {
Rect {
x,
y,
width: w,
height: h,
}
}
fn clickable(
tag: &'static str,
bounds: Rect,
events: Rc<RefCell<Vec<ObjectEvent>>>,
) -> ObjectNode {
let mut n = TW::node(tag, bounds, events.clone());
n.set_flag(ObjectFlags::CLICKABLE, true);
let ev = events.clone();
n.add_target_handler(move |event, _ctx| {
ev.borrow_mut().push(event.clone());
false
});
n
}
fn focusable(
tag: &'static str,
bounds: Rect,
events: Rc<RefCell<Vec<ObjectEvent>>>,
) -> ObjectNode {
let mut n = TW::node(tag, bounds, events.clone());
n.set_flag(ObjectFlags::FOCUSABLE, true);
let ev = events.clone();
n.add_target_handler(move |event, _ctx| {
ev.borrow_mut().push(event.clone());
false
});
n
}
#[test]
fn pointer_device_click_dispatches_pressed_and_clicked() {
let events = Rc::new(RefCell::new(Vec::new()));
let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
bounds: r(0, 0, 100, 100),
consume_clicks: false,
events: events.clone(),
})));
root.append_child(clickable("btn", r(10, 10, 30, 30), events.clone()));
let mut dev = PointerDevice::new(30);
dev.process(&mut root, Event::PointerDown { x: 20, y: 20 });
dev.process(&mut root, Event::PointerUp { x: 20, y: 20 });
for _ in 0..10 {
dev.tick(&mut root);
}
for _ in 0..20 {
dev.tick(&mut root);
}
let ev = events.borrow();
assert!(
ev.iter().any(|e| matches!(e, ObjectEvent::Pressed { .. })),
"expected Pressed: {ev:?}"
);
assert!(
ev.iter().any(|e| matches!(e, ObjectEvent::Clicked { .. })),
"expected Clicked: {ev:?}"
);
}
#[test]
fn pointer_device_drag_suppresses_clicked() {
let events = Rc::new(RefCell::new(Vec::new()));
let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
bounds: r(0, 0, 200, 200),
consume_clicks: false,
events: events.clone(),
})));
root.append_child(clickable("btn", r(0, 0, 200, 200), events.clone()));
let mut dev = PointerDevice::new(30);
dev.process(&mut root, Event::PointerDown { x: 10, y: 10 });
dev.process(&mut root, Event::PointerMove { x: 30, y: 10 });
dev.process(&mut root, Event::PointerUp { x: 30, y: 10 });
for _ in 0..40 {
dev.tick(&mut root);
}
let ev = events.borrow();
assert!(
!ev.iter().any(|e| matches!(e, ObjectEvent::Clicked { .. })),
"drag-crossing contact must not produce Clicked: {ev:?}"
);
}
#[test]
fn pointer_device_tick_drives_long_press() {
let lp_config = LongPressConfig {
long_press_ticks: 5,
repeat_ticks: 3,
};
let events = Rc::new(RefCell::new(Vec::new()));
let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
bounds: r(0, 0, 100, 100),
consume_clicks: false,
events: events.clone(),
})));
root.append_child(clickable("btn", r(10, 10, 50, 50), events.clone()));
let mut dev = PointerDevice::with_long_press_config(30, lp_config);
dev.process(&mut root, Event::PointerDown { x: 20, y: 20 });
for _ in 0..6 {
dev.tick(&mut root);
}
let ev = events.borrow();
assert!(
ev.iter()
.any(|e| matches!(e, ObjectEvent::LongPressed { .. })),
"expected LongPressed after threshold: {ev:?}"
);
}
#[test]
fn keypad_delivers_key_to_focused_node() {
let events = Rc::new(RefCell::new(Vec::new()));
let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
bounds: r(0, 0, 100, 100),
consume_clicks: false,
events: events.clone(),
})));
let node = focusable("input", r(0, 0, 50, 50), events.clone());
root.append_child(node);
let fg = FocusGroup::new();
fg.focus_next(&mut root);
let mut dev = KeypadDevice::new();
dev.key_down(&mut root, Key::Character('a'));
let ev = events.borrow();
assert!(
ev.iter()
.any(|e| matches!(e, ObjectEvent::Key(Key::Character('a')))),
"expected Key(Character('a')): {ev:?}"
);
}
#[test]
fn keypad_auto_repeat_fires_at_exact_ticks() {
let events = Rc::new(RefCell::new(Vec::new()));
let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
bounds: r(0, 0, 100, 100),
consume_clicks: false,
events: events.clone(),
})));
let node = focusable("input", r(0, 0, 100, 100), events.clone());
root.append_child(node);
let fg = FocusGroup::new();
fg.focus_next(&mut root);
let delay = 4u32;
let period = 2u32;
let mut dev = KeypadDevice::with_repeat(delay, period);
dev.key_down(&mut root, Key::Enter);
let mut repeat_ticks = Vec::new();
for tick in 1..=(delay + period * 4) {
let d = dev.tick(&mut root);
if d != Disposition::NoTarget {
repeat_ticks.push(tick);
}
}
let expected: Vec<u32> = (0..=4).map(|i| delay + i * period).collect();
assert_eq!(repeat_ticks, expected, "auto-repeat tick schedule mismatch");
let ev = events.borrow();
let key_count = ev
.iter()
.filter(|e| matches!(e, ObjectEvent::Key(Key::Enter)))
.count();
assert_eq!(key_count, 1 + 5, "1 initial + 5 repeats expected");
}
#[test]
fn keypad_unconsumed_key_returns_no_target_when_no_focus() {
let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
bounds: r(0, 0, 100, 100),
consume_clicks: false,
events: Rc::new(RefCell::new(Vec::new())),
})));
let mut dev = KeypadDevice::new();
let d = dev.key_down(&mut root, Key::Escape);
assert_eq!(d, Disposition::NoTarget);
}
#[test]
fn encoder_navigate_mode_diff_moves_focus() {
let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
bounds: r(0, 0, 200, 100),
consume_clicks: false,
events: Rc::new(RefCell::new(Vec::new())),
})));
for (tag, x) in [("a", 0), ("b", 50), ("c", 100)] {
let mut n = TW::node(tag, r(x, 0, 40, 40), Rc::new(RefCell::new(Vec::new())));
n.set_flag(ObjectFlags::FOCUSABLE, true);
root.append_child(n);
}
let mut enc = EncoderDevice::new();
assert!(!enc.is_editing());
enc.rotate(&mut root, 1);
assert_eq!(focused_tag(&root), Some("a"));
enc.rotate(&mut root, 1);
assert_eq!(focused_tag(&root), Some("b"));
enc.rotate(&mut root, -1);
assert_eq!(focused_tag(&root), Some("a"));
enc.rotate(&mut root, 2);
assert_eq!(focused_tag(&root), Some("c"));
}
#[test]
fn encoder_editing_mode_delivers_rotary_to_focused() {
let rotary_events = Rc::new(RefCell::new(Vec::new()));
let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
bounds: r(0, 0, 100, 100),
consume_clicks: false,
events: Rc::new(RefCell::new(Vec::new())),
})));
let node = focusable("ctrl", r(0, 0, 100, 100), rotary_events.clone());
root.append_child(node);
let mut enc = EncoderDevice::new();
enc.focus_group.focus_next(&mut root);
enc.set_editing(&mut root, true);
assert!(enc.is_editing());
enc.rotate(&mut root, 3);
let ev = rotary_events.borrow();
assert!(
ev.iter()
.any(|e| matches!(e, ObjectEvent::Rotary { diff: 3 })),
"expected Rotary {{ diff: 3 }}: {ev:?}"
);
}
#[test]
fn encoder_enter_press_toggles_editing() {
let key_events = Rc::new(RefCell::new(Vec::new()));
let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
bounds: r(0, 0, 100, 100),
consume_clicks: false,
events: Rc::new(RefCell::new(Vec::new())),
})));
let node = focusable("ctrl", r(0, 0, 100, 100), key_events.clone());
root.append_child(node);
let mut enc = EncoderDevice::new();
enc.focus_group.focus_next(&mut root);
assert!(!enc.is_editing());
enc.press(&mut root);
assert!(enc.is_editing(), "first press should enter editing mode");
let ev = key_events.borrow();
assert!(
ev.iter().any(|e| matches!(e, ObjectEvent::Key(Key::Enter))),
"expected Key(Enter): {ev:?}"
);
drop(ev);
enc.press(&mut root);
assert!(!enc.is_editing(), "second press should exit editing mode");
}
#[test]
fn button_device_press_hits_mapped_node() {
let events_a = Rc::new(RefCell::new(Vec::new()));
let events_b = Rc::new(RefCell::new(Vec::new()));
let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
bounds: r(0, 0, 200, 100),
consume_clicks: false,
events: Rc::new(RefCell::new(Vec::new())),
})));
root.append_child(clickable("a", r(0, 0, 50, 50), events_a.clone()));
root.append_child(clickable("b", r(100, 0, 50, 50), events_b.clone()));
let mut dev = ButtonDevice::new(
30,
vec![
ButtonMapping {
button_id: 0,
x: 10,
y: 10,
}, ButtonMapping {
button_id: 1,
x: 110,
y: 10,
}, ],
);
dev.button_down(&mut root, 1);
dev.button_up(&mut root, 1);
for _ in 0..30 {
dev.tick(&mut root);
}
let ea = events_a.borrow();
let eb = events_b.borrow();
assert!(
ea.iter().all(|e| !matches!(e, ObjectEvent::Pressed { .. })),
"node 'a' should not have been pressed: {ea:?}"
);
assert!(
eb.iter().any(|e| matches!(e, ObjectEvent::Pressed { .. })),
"node 'b' should have been pressed: {eb:?}"
);
}
#[test]
fn focus_to_set_active_adapter_demonstration() {
let active = Rc::new(RefCell::new(false));
let active2 = active.clone();
let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
bounds: r(0, 0, 100, 100),
consume_clicks: false,
events: Rc::new(RefCell::new(Vec::new())),
})));
let mut field = TW::node(
"field",
r(0, 0, 100, 100),
Rc::new(RefCell::new(Vec::new())),
);
field.set_flag(ObjectFlags::FOCUSABLE, true);
field.add_target_handler(move |event, _ctx| {
match event {
ObjectEvent::Focused => *active2.borrow_mut() = true,
ObjectEvent::Defocused => *active2.borrow_mut() = false,
_ => {}
}
false
});
root.append_child(field);
let mut other = TW::node("other", r(0, 50, 50, 50), Rc::new(RefCell::new(Vec::new())));
other.set_flag(ObjectFlags::FOCUSABLE, true);
root.append_child(other);
let fg = FocusGroup::new();
assert!(!*active.borrow(), "should start inactive");
fg.focus_next(&mut root);
assert!(
*active.borrow(),
"Focused event should activate via adapter"
);
assert_eq!(focused_tag(&root), Some("field"));
fg.focus_next(&mut root);
assert!(
!*active.borrow(),
"Defocused event should deactivate via adapter"
);
}
fn focused_tag(root: &ObjectNode) -> Option<&'static str> {
if root.states().contains(ObjectStates::FOCUSED) {
return root.tag();
}
for child in root.children() {
if let Some(t) = focused_tag(child) {
return Some(t);
}
}
None
}
use rlvgl_core::scroll::{ScrollConfig, ScrollState};
fn scroll_tree_platform() -> ObjectNode {
let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
bounds: r(0, 0, 400, 600),
consume_clicks: false,
events: Rc::new(RefCell::new(Vec::new())),
})));
let mut container = ObjectNode::new(Rc::new(RefCell::new(TW {
bounds: r(0, 0, 400, 600),
consume_clicks: false,
events: Rc::new(RefCell::new(Vec::new())),
})));
let mut ss = ScrollState::new();
ss.content_h = 1200;
container.set_scroll_state(Box::new(ss));
let child = ObjectNode::new(Rc::new(RefCell::new(TW {
bounds: r(0, 0, 400, 1200),
consume_clicks: false,
events: Rc::new(RefCell::new(Vec::new())),
})));
container.append_child(child);
root.append_child(container);
root
}
#[test]
fn scroll_drag_moves_offset_and_suppresses_gesture_dispatch() {
let gesture_events: Rc<RefCell<Vec<ObjectEvent>>> = Rc::new(RefCell::new(Vec::new()));
let mut root = scroll_tree_platform();
{
let ev = gesture_events.clone();
use rlvgl_core::object::ObjectNode;
fn node_mut<'a>(root: &'a mut ObjectNode, path: &[usize]) -> &'a mut ObjectNode {
let mut cur = root;
for &i in path {
cur = &mut cur.children_mut()[i];
}
cur
}
node_mut(&mut root, &[0]).add_target_handler(move |event, _ctx| {
match event {
ObjectEvent::Gesture { .. } | ObjectEvent::Pressed { .. } => {
ev.borrow_mut().push(event.clone());
}
_ => {}
}
false
});
}
let mut dev = PointerDevice::new(30).with_scroll(ScrollConfig::default());
dev.process(&mut root, Event::PointerDown { x: 200, y: 300 });
dev.process(&mut root, Event::PointerMove { x: 200, y: 270 }); dev.process(&mut root, Event::PointerMove { x: 200, y: 250 });
dev.process(&mut root, Event::PointerUp { x: 200, y: 250 });
for _ in 0..30 {
dev.tick(&mut root);
}
let offset = root.children()[0]
.scroll_state()
.expect("container must have scroll state")
.offset_y;
assert!(
offset > 0,
"scroll offset must increase after drag-down: {offset}"
);
let dirty = dev.take_dirty();
let _ = dirty;
let gestures = gesture_events.borrow();
assert!(
gestures.is_empty(),
"drag over SCROLLABLE must not dispatch Gesture/Pressed: {gestures:?}"
);
}
#[test]
fn below_threshold_scroll_suppresses_terminating_drag_from_widgets() {
struct RecW {
bounds: Rect,
seen: Rc<RefCell<Vec<Event>>>,
}
impl Widget for RecW {
fn bounds(&self) -> Rect {
self.bounds
}
fn draw(&self, _r: &mut dyn Renderer) {}
fn handle_event(&mut self, ev: &Event) -> bool {
self.seen.borrow_mut().push(ev.clone());
false
}
}
let seen: Rc<RefCell<Vec<Event>>> = Rc::new(RefCell::new(Vec::new()));
let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
bounds: r(0, 0, 400, 600),
consume_clicks: false,
events: Rc::new(RefCell::new(Vec::new())),
})));
let mut container = ObjectNode::new(Rc::new(RefCell::new(TW {
bounds: r(0, 0, 400, 600),
consume_clicks: false,
events: Rc::new(RefCell::new(Vec::new())),
})));
let mut ss = ScrollState::new();
ss.content_h = 1200;
container.set_scroll_state(Box::new(ss));
let mut child = ObjectNode::new(Rc::new(RefCell::new(RecW {
bounds: r(0, 0, 400, 1200),
seen: seen.clone(),
})));
child.set_flag(ObjectFlags::CLICKABLE, true);
container.append_child(child);
root.append_child(container);
let mut dev = PointerDevice::new(30).with_scroll(ScrollConfig::default());
dev.process(&mut root, Event::PointerDown { x: 200, y: 300 });
dev.process(&mut root, Event::PointerMove { x: 200, y: 270 });
dev.process(&mut root, Event::PointerMove { x: 200, y: 250 });
dev.process(&mut root, Event::PointerUp { x: 200, y: 250 });
assert!(
root.children()[0]
.scroll_state()
.expect("container has scroll state")
.offset_y
> 0,
"scroll offset must have advanced"
);
let evs = seen.borrow();
assert!(
!evs.iter().any(|e| matches!(
e,
Event::DragStart { .. } | Event::DragMove { .. } | Event::DragEnd { .. }
)),
"a scroll contact must not leak drag events into widgets: {evs:?}"
);
}
#[test]
fn scroll_no_scrollable_ancestor_falls_through_to_normal_dispatch() {
let click_events: Rc<RefCell<Vec<ObjectEvent>>> = Rc::new(RefCell::new(Vec::new()));
let mut root = ObjectNode::new(Rc::new(RefCell::new(TW {
bounds: r(0, 0, 200, 200),
consume_clicks: false,
events: click_events.clone(),
})));
let child = clickable("btn", r(0, 0, 200, 200), click_events.clone());
root.append_child(child);
let mut dev = PointerDevice::new(30).with_scroll(ScrollConfig::default());
dev.process(&mut root, Event::PointerDown { x: 10, y: 10 });
dev.process(&mut root, Event::PointerMove { x: 30, y: 10 });
dev.process(&mut root, Event::PointerUp { x: 30, y: 10 });
for _ in 0..40 {
dev.tick(&mut root);
}
let dirty = dev.take_dirty();
assert!(
dirty.is_empty(),
"no dirty rects without a scrollable ancestor"
);
}
#[test]
fn scroll_dirty_sink_receives_viewport_rect_on_effective_scroll() {
let mut root = scroll_tree_platform();
let mut dev = PointerDevice::new(30).with_scroll(ScrollConfig::default());
let mut all_dirty: Vec<Rect> = Vec::new();
dev.process(&mut root, Event::PointerDown { x: 200, y: 400 });
all_dirty.extend(dev.take_dirty());
dev.process(&mut root, Event::PointerMove { x: 200, y: 380 });
all_dirty.extend(dev.take_dirty());
dev.process(&mut root, Event::PointerMove { x: 200, y: 350 });
all_dirty.extend(dev.take_dirty());
dev.process(&mut root, Event::PointerUp { x: 200, y: 350 });
all_dirty.extend(dev.take_dirty());
assert!(
!all_dirty.is_empty(),
"dirty sink must receive rects on effective scroll"
);
for d in &all_dirty {
assert_eq!(d.width, 400, "dirty rect width must match viewport");
assert_eq!(d.height, 600, "dirty rect height must match viewport");
}
}
}