use rlvgl_core::event::Event;
pub const SETTLE_MS: u32 = 200;
pub const SHORT_PRESS_MAX_MS: u32 = 250;
pub const DOUBLE_TAP_WINDOW_MS: u32 = 400;
pub const DOUBLE_TAP_MAX_DISTANCE: i32 = 20;
pub const DRAG_START_THRESHOLD_PX: i32 = 10;
fn ms_to_ticks(ms: u32, frame_hz: u32) -> u8 {
(ms * frame_hz).div_ceil(1000) as u8
}
pub struct TapRecognizer {
state: TapState,
pos: (i32, i32),
settle: u8,
max_settle: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TapState {
Idle,
Down,
PendingRelease,
}
impl TapRecognizer {
pub fn new(frame_hz: u32) -> Self {
Self {
state: TapState::Idle,
pos: (0, 0),
settle: 0,
max_settle: ms_to_ticks(SETTLE_MS, frame_hz),
}
}
pub fn process(&mut self, event: &Event) -> Option<Event> {
match event {
Event::PointerDown { x, y } => {
self.pos = (*x, *y);
match self.state {
TapState::Idle => {
self.state = TapState::Down;
Some(Event::PressDown { x: *x, y: *y })
}
TapState::Down => {
None
}
TapState::PendingRelease => {
self.state = TapState::Down;
self.settle = 0;
None }
}
}
Event::PointerUp { x, y } => {
self.pos = (*x, *y);
match self.state {
TapState::Down => {
self.state = TapState::PendingRelease;
self.settle = self.max_settle;
None
}
TapState::PendingRelease => {
self.settle = self.max_settle;
None
}
TapState::Idle => {
None
}
}
}
Event::PointerMove { x, y } => {
if self.state == TapState::Down {
self.pos = (*x, *y);
}
Some(event.clone())
}
_ => Some(event.clone()),
}
}
pub fn tick(&mut self) -> Option<Event> {
if self.state == TapState::PendingRelease {
if self.settle > 0 {
self.settle -= 1;
}
if self.settle == 0 {
self.state = TapState::Idle;
let (x, y) = self.pos;
return Some(Event::PressRelease { x, y });
}
}
None
}
pub fn cancel(&mut self) {
self.state = TapState::Idle;
self.settle = 0;
}
}
pub struct DragRecognizer {
state: DragState,
origin: (i32, i32),
threshold_sq: i64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DragState {
Idle,
Armed,
Dragging,
}
impl DragRecognizer {
pub fn new() -> Self {
Self::with_threshold(DRAG_START_THRESHOLD_PX)
}
pub fn with_threshold(threshold_px: i32) -> Self {
let t = threshold_px.max(0) as i64;
Self {
state: DragState::Idle,
origin: (0, 0),
threshold_sq: t * t,
}
}
pub fn is_dragging(&self) -> bool {
self.state == DragState::Dragging
}
pub fn process(&mut self, event: &Event) -> Option<Event> {
match event {
Event::PointerDown { x, y } => {
self.state = DragState::Armed;
self.origin = (*x, *y);
Some(event.clone())
}
Event::PointerMove { x, y } => match self.state {
DragState::Armed => {
let dx = (*x - self.origin.0) as i64;
let dy = (*y - self.origin.1) as i64;
if dx * dx + dy * dy >= self.threshold_sq {
self.state = DragState::Dragging;
Some(Event::DragStart {
x: *x,
y: *y,
origin_x: self.origin.0,
origin_y: self.origin.1,
})
} else {
Some(event.clone())
}
}
DragState::Dragging => Some(Event::DragMove { x: *x, y: *y }),
DragState::Idle => Some(event.clone()),
},
Event::PointerUp { x, y } => match self.state {
DragState::Dragging => {
self.state = DragState::Idle;
Some(Event::DragEnd { x: *x, y: *y })
}
_ => {
self.state = DragState::Idle;
Some(event.clone())
}
},
_ => Some(event.clone()),
}
}
pub fn tick(&mut self) -> Option<Event> {
None
}
}
impl Default for DragRecognizer {
fn default() -> Self {
Self::new()
}
}
pub struct DoubleTapRecognizer {
state: DtState,
armed_pos: (i32, i32),
countdown: u8,
down_tick: u8,
tick_counter: u8,
short_press_max_ticks: u8,
window_ticks: u8,
max_distance: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DtState {
Idle,
Armed,
}
impl DoubleTapRecognizer {
pub fn new(frame_hz: u32) -> Self {
Self {
state: DtState::Idle,
armed_pos: (0, 0),
countdown: 0,
down_tick: 0,
tick_counter: 0,
short_press_max_ticks: ms_to_ticks(SHORT_PRESS_MAX_MS, frame_hz),
window_ticks: ms_to_ticks(DOUBLE_TAP_WINDOW_MS, frame_hz),
max_distance: DOUBLE_TAP_MAX_DISTANCE,
}
}
pub fn process(&mut self, event: &Event) -> (Option<Event>, Option<Event>) {
match event {
Event::PressDown { .. } => {
self.down_tick = self.tick_counter;
(Some(event.clone()), None)
}
Event::PressRelease { x, y } => {
let hold = self.tick_counter.wrapping_sub(self.down_tick);
let is_short = hold <= self.short_press_max_ticks;
match self.state {
DtState::Idle => {
if is_short {
self.state = DtState::Armed;
self.armed_pos = (*x, *y);
self.countdown = self.window_ticks;
(None, None) } else {
(Some(event.clone()), None)
}
}
DtState::Armed => {
let (ax, ay) = self.armed_pos;
let dist = (ax - *x).abs() + (ay - *y).abs();
if is_short && dist <= self.max_distance {
self.state = DtState::Idle;
self.countdown = 0;
(Some(Event::DoubleTap { x: *x, y: *y }), None)
} else {
let first = Event::PressRelease { x: ax, y: ay };
if is_short {
self.armed_pos = (*x, *y);
self.countdown = self.window_ticks;
(Some(first), None)
} else {
self.state = DtState::Idle;
self.countdown = 0;
(Some(first), Some(event.clone()))
}
}
}
}
}
_ => (Some(event.clone()), None),
}
}
pub fn tick(&mut self) -> Option<Event> {
self.tick_counter = self.tick_counter.wrapping_add(1);
if self.state == DtState::Armed {
if self.countdown > 0 {
self.countdown -= 1;
}
if self.countdown == 0 {
self.state = DtState::Idle;
let (x, y) = self.armed_pos;
return Some(Event::PressRelease { x, y });
}
}
None
}
}
pub const LONG_PRESS_TICKS: u32 = 12;
pub const LONG_PRESS_REPEAT_TICKS: u32 = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LongPressConfig {
pub long_press_ticks: u32,
pub repeat_ticks: u32,
}
impl Default for LongPressConfig {
fn default() -> Self {
Self {
long_press_ticks: LONG_PRESS_TICKS,
repeat_ticks: LONG_PRESS_REPEAT_TICKS,
}
}
}
pub struct LongPressRecognizer {
state: LpState,
pos: (i32, i32),
counter: u32,
config: LongPressConfig,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LpState {
Idle,
Armed,
Fired,
}
impl LongPressRecognizer {
pub fn new() -> Self {
Self::with_config(LongPressConfig::default())
}
pub fn with_config(config: LongPressConfig) -> Self {
let config = LongPressConfig {
repeat_ticks: config.repeat_ticks.max(1),
..config
};
Self {
state: LpState::Idle,
pos: (0, 0),
counter: 0,
config,
}
}
pub fn process(&mut self, event: &Event) -> Option<Event> {
match event {
Event::PressDown { x, y } => {
self.state = LpState::Armed;
self.pos = (*x, *y);
self.counter = 0;
}
Event::PointerMove { x, y } | Event::DragMove { x, y }
if self.state != LpState::Idle =>
{
self.pos = (*x, *y);
}
Event::DragStart { .. }
| Event::PointerUp { .. }
| Event::PressRelease { .. }
| Event::DragEnd { .. } => {
self.cancel();
}
_ => {}
}
Some(event.clone())
}
pub fn tick(&mut self) -> Option<Event> {
match self.state {
LpState::Idle => None,
LpState::Armed => {
self.counter += 1;
if self.counter >= self.config.long_press_ticks {
self.state = LpState::Fired;
self.counter = 0;
let (x, y) = self.pos;
Some(Event::LongPress { x, y })
} else {
None
}
}
LpState::Fired => {
self.counter += 1;
if self.counter >= self.config.repeat_ticks {
self.counter = 0;
let (x, y) = self.pos;
Some(Event::LongPressRepeat { x, y })
} else {
None
}
}
}
}
pub fn cancel(&mut self) {
self.state = LpState::Idle;
self.counter = 0;
}
}
impl Default for LongPressRecognizer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
use alloc::vec::Vec;
#[test]
fn tap_produces_press_down_then_release() {
let mut tap = TapRecognizer::new(30);
let result = tap.process(&Event::PointerDown { x: 100, y: 200 });
assert_eq!(result, Some(Event::PressDown { x: 100, y: 200 }));
let result = tap.process(&Event::PointerUp { x: 100, y: 200 });
assert_eq!(result, None);
for _ in 0..5 {
assert_eq!(tap.tick(), None);
}
let result = tap.tick();
assert_eq!(result, Some(Event::PressRelease { x: 100, y: 200 }));
assert_eq!(tap.tick(), None);
}
#[test]
fn bounce_suppressed() {
let mut tap = TapRecognizer::new(30);
tap.process(&Event::PointerDown { x: 10, y: 20 });
tap.process(&Event::PointerUp { x: 10, y: 20 });
let result = tap.process(&Event::PointerDown { x: 10, y: 20 });
assert_eq!(result, None);
tap.process(&Event::PointerUp { x: 10, y: 20 });
let settle_ticks = ms_to_ticks(SETTLE_MS, 30);
for _ in 0..(settle_ticks - 1) {
assert_eq!(tap.tick(), None);
}
assert_eq!(tap.tick(), Some(Event::PressRelease { x: 10, y: 20 }));
}
#[test]
fn non_pointer_events_pass_through() {
let mut tap = TapRecognizer::new(30);
assert_eq!(tap.process(&Event::Tick), Some(Event::Tick));
}
#[test]
fn settle_ticks_scale_with_frame_rate() {
assert_eq!(ms_to_ticks(SETTLE_MS, 6), 2);
assert_eq!(ms_to_ticks(SETTLE_MS, 30), 6);
assert_eq!(ms_to_ticks(SETTLE_MS, 60), 12);
}
fn short_tap(dtap: &mut DoubleTapRecognizer, x: i32, y: i32, hold_ticks: u8) -> Vec<Event> {
let mut out = Vec::new();
let (e1, e2) = dtap.process(&Event::PressDown { x, y });
if let Some(e) = e1 {
out.push(e);
}
if let Some(e) = e2 {
out.push(e);
}
for _ in 0..hold_ticks {
if let Some(e) = dtap.tick() {
out.push(e);
}
}
let (e1, e2) = dtap.process(&Event::PressRelease { x, y });
if let Some(e) = e1 {
out.push(e);
}
if let Some(e) = e2 {
out.push(e);
}
out
}
#[test]
fn double_tap_emits_double_tap_event() {
let mut dtap = DoubleTapRecognizer::new(30);
let events = short_tap(&mut dtap, 100, 200, 2);
assert_eq!(events, vec![Event::PressDown { x: 100, y: 200 }]);
for _ in 0..3 {
assert_eq!(dtap.tick(), None);
}
let events = short_tap(&mut dtap, 100, 200, 2);
assert!(events.contains(&Event::DoubleTap { x: 100, y: 200 }));
}
#[test]
fn single_tap_emits_after_timeout() {
let mut dtap = DoubleTapRecognizer::new(30);
let events = short_tap(&mut dtap, 50, 60, 1);
assert_eq!(events, vec![Event::PressDown { x: 50, y: 60 }]);
let window = ms_to_ticks(DOUBLE_TAP_WINDOW_MS, 30);
let mut released = false;
for _ in 0..window {
if let Some(e) = dtap.tick() {
assert_eq!(e, Event::PressRelease { x: 50, y: 60 });
released = true;
}
}
assert!(released, "buffered PressRelease should emit on timeout");
}
#[test]
fn long_press_passes_through_immediately() {
let mut dtap = DoubleTapRecognizer::new(30);
let long_hold = ms_to_ticks(SHORT_PRESS_MAX_MS, 30) + 5;
let events = short_tap(&mut dtap, 100, 200, long_hold);
assert!(events.contains(&Event::PressDown { x: 100, y: 200 }));
assert!(events.contains(&Event::PressRelease { x: 100, y: 200 }));
}
#[test]
fn distance_rejection() {
let mut dtap = DoubleTapRecognizer::new(30);
short_tap(&mut dtap, 10, 10, 1);
let far = DOUBLE_TAP_MAX_DISTANCE + 10;
let events = short_tap(&mut dtap, 10 + far, 10, 1);
assert!(!events.iter().any(|e| matches!(e, Event::DoubleTap { .. })));
}
struct DragTapChain {
drag: DragRecognizer,
tap: TapRecognizer,
}
impl DragTapChain {
fn new() -> Self {
Self {
drag: DragRecognizer::new(),
tap: TapRecognizer::new(30),
}
}
fn feed(&mut self, event: &Event) -> Vec<Event> {
let mut out = Vec::new();
if let Some(stage) = self.drag.process(event) {
if matches!(stage, Event::DragStart { .. }) {
self.tap.cancel();
}
if let Some(gesture) = self.tap.process(&stage) {
out.push(gesture);
}
}
out
}
fn tick(&mut self) -> Vec<Event> {
let mut out = Vec::new();
if let Some(e) = self.tap.tick() {
out.push(e);
}
if let Some(e) = self.drag.tick() {
out.push(e);
}
out
}
}
fn is_drag(e: &Event) -> bool {
matches!(
e,
Event::DragStart { .. } | Event::DragMove { .. } | Event::DragEnd { .. }
)
}
#[test]
fn sub_threshold_wander_yields_press_release_and_no_drag() {
let mut chain = DragTapChain::new();
let mut events = Vec::new();
events.extend(chain.feed(&Event::PointerDown { x: 100, y: 100 }));
for (x, y) in [(103, 102), (106, 104), (109, 100), (104, 103)] {
events.extend(chain.feed(&Event::PointerMove { x, y }));
}
events.extend(chain.feed(&Event::PointerUp { x: 104, y: 103 }));
for _ in 0..ms_to_ticks(SETTLE_MS, 30) {
events.extend(chain.tick());
}
assert!(
events
.iter()
.any(|e| matches!(e, Event::PressRelease { .. })),
"wandering tap still releases: {events:?}"
);
assert!(
!events.iter().any(is_drag),
"no drag events below threshold: {events:?}"
);
}
#[test]
fn threshold_crossing_emits_drag_with_origin_and_no_press_release() {
let mut chain = DragTapChain::new();
let mut events = Vec::new();
events.extend(chain.feed(&Event::PointerDown { x: 100, y: 100 }));
events.extend(chain.feed(&Event::PointerMove { x: 105, y: 103 })); events.extend(chain.feed(&Event::PointerMove { x: 108, y: 106 })); events.extend(chain.feed(&Event::PointerMove { x: 140, y: 90 }));
events.extend(chain.feed(&Event::PointerUp { x: 150, y: 80 }));
for _ in 0..ms_to_ticks(SETTLE_MS, 30) + 2 {
events.extend(chain.tick());
}
let drags: Vec<&Event> = events.iter().filter(|e| is_drag(e)).collect();
assert_eq!(
drags,
vec![
&Event::DragStart {
x: 108,
y: 106,
origin_x: 100,
origin_y: 100
},
&Event::DragMove { x: 140, y: 90 },
&Event::DragEnd { x: 150, y: 80 },
],
"start-at-origin / move / end sequencing"
);
assert!(
!events
.iter()
.any(|e| matches!(e, Event::PressRelease { .. })),
"click-vs-drag suppression: no PressRelease: {events:?}"
);
assert!(
events.iter().any(|e| matches!(e, Event::PressDown { .. })),
"PressDown still emitted at contact"
);
}
#[test]
fn threshold_metric_is_euclidean_not_manhattan() {
let mut drag = DragRecognizer::new();
drag.process(&Event::PointerDown { x: 0, y: 0 });
let out = drag.process(&Event::PointerMove { x: 7, y: 7 });
assert_eq!(out, Some(Event::PointerMove { x: 7, y: 7 }));
assert!(!drag.is_dragging());
let out = drag.process(&Event::PointerMove { x: 8, y: 7 });
assert_eq!(
out,
Some(Event::DragStart {
x: 8,
y: 7,
origin_x: 0,
origin_y: 0
})
);
assert!(drag.is_dragging());
}
#[test]
fn custom_threshold_and_inclusive_boundary() {
let mut drag = DragRecognizer::with_threshold(5);
drag.process(&Event::PointerDown { x: 10, y: 10 });
let out = drag.process(&Event::PointerMove { x: 13, y: 14 }); assert!(matches!(out, Some(Event::DragStart { .. })), "{out:?}");
}
#[test]
fn pointer_up_while_armed_passes_through_to_tap_path() {
let mut drag = DragRecognizer::new();
drag.process(&Event::PointerDown { x: 5, y: 5 });
let out = drag.process(&Event::PointerUp { x: 6, y: 6 });
assert_eq!(out, Some(Event::PointerUp { x: 6, y: 6 }));
assert!(!drag.is_dragging());
}
#[test]
fn pointer_down_while_dragging_rearms_defensively() {
let mut drag = DragRecognizer::new();
drag.process(&Event::PointerDown { x: 0, y: 0 });
drag.process(&Event::PointerMove { x: 20, y: 0 });
assert!(drag.is_dragging());
let out = drag.process(&Event::PointerDown { x: 50, y: 50 });
assert_eq!(out, Some(Event::PointerDown { x: 50, y: 50 }));
assert!(!drag.is_dragging());
let out = drag.process(&Event::PointerMove { x: 56, y: 58 }); assert_eq!(
out,
Some(Event::DragStart {
x: 56,
y: 58,
origin_x: 50,
origin_y: 50
})
);
}
#[test]
fn non_pointer_events_pass_through_drag_recognizer() {
let mut drag = DragRecognizer::new();
assert_eq!(drag.process(&Event::Tick), Some(Event::Tick));
assert_eq!(drag.tick(), None, "no timers in v1");
}
#[test]
fn multi_recognizer_coexistence_full_chain() {
let mut drag = DragRecognizer::new();
let mut tap = TapRecognizer::new(30);
let mut dtap = DoubleTapRecognizer::new(30);
let feed = |drag: &mut DragRecognizer,
tap: &mut TapRecognizer,
dtap: &mut DoubleTapRecognizer,
event: &Event|
-> Vec<Event> {
let mut out = Vec::new();
if let Some(stage) = drag.process(event) {
if matches!(stage, Event::DragStart { .. }) {
tap.cancel();
}
if let Some(gesture) = tap.process(&stage) {
let (a, b) = dtap.process(&gesture);
out.extend(a);
out.extend(b);
}
}
out
};
let tick = |tap: &mut TapRecognizer, dtap: &mut DoubleTapRecognizer| -> Vec<Event> {
let mut out = Vec::new();
if let Some(gesture) = tap.tick() {
let (a, b) = dtap.process(&gesture);
out.extend(a);
out.extend(b);
}
out.extend(dtap.tick());
out
};
let mut events = Vec::new();
events.extend(feed(
&mut drag,
&mut tap,
&mut dtap,
&Event::PointerDown { x: 10, y: 10 },
));
events.extend(feed(
&mut drag,
&mut tap,
&mut dtap,
&Event::PointerMove { x: 40, y: 10 },
));
events.extend(feed(
&mut drag,
&mut tap,
&mut dtap,
&Event::PointerUp { x: 60, y: 10 },
));
for _ in 0..20 {
events.extend(tick(&mut tap, &mut dtap));
}
assert!(events.iter().any(|e| matches!(e, Event::DragEnd { .. })));
assert!(
!events
.iter()
.any(|e| matches!(e, Event::PressRelease { .. } | Event::DoubleTap { .. })),
"drag emitted tap-family events: {events:?}"
);
events.clear();
for _ in 0..2 {
events.extend(feed(
&mut drag,
&mut tap,
&mut dtap,
&Event::PointerDown { x: 100, y: 100 },
));
events.extend(feed(
&mut drag,
&mut tap,
&mut dtap,
&Event::PointerUp { x: 100, y: 100 },
));
for _ in 0..ms_to_ticks(SETTLE_MS, 30) {
events.extend(tick(&mut tap, &mut dtap));
}
}
assert!(
events
.iter()
.any(|e| matches!(e, Event::DoubleTap { x: 100, y: 100 })),
"double-tap survives drag coexistence: {events:?}"
);
assert!(
!events.iter().any(is_drag),
"stationary taps emitted drag events"
);
}
fn hold_contact(lp: &mut LongPressRecognizer, x: i32, y: i32, hold_ticks: u32) -> Vec<Event> {
let mut out = Vec::new();
lp.process(&Event::PressDown { x, y });
for _ in 0..hold_ticks {
if let Some(e) = lp.tick() {
out.push(e);
}
}
out
}
#[test]
fn long_press_fires_once_then_repeats() {
let mut lp = LongPressRecognizer::with_config(LongPressConfig {
long_press_ticks: 5,
repeat_ticks: 3,
});
let events = hold_contact(&mut lp, 10, 20, 11);
assert_eq!(
events,
vec![
Event::LongPress { x: 10, y: 20 },
Event::LongPressRepeat { x: 10, y: 20 },
Event::LongPressRepeat { x: 10, y: 20 },
],
"exact emission ticks: LongPress at threshold, repeats at each interval"
);
}
#[test]
fn release_before_threshold_emits_no_long_press() {
let mut lp = LongPressRecognizer::with_config(LongPressConfig {
long_press_ticks: 10,
repeat_ticks: 3,
});
let events = hold_contact(&mut lp, 5, 5, 9);
lp.process(&Event::PressRelease { x: 5, y: 5 });
assert!(
events.is_empty(),
"release before threshold must not emit LongPress: {events:?}"
);
for _ in 0..5 {
assert_eq!(lp.tick(), None, "nothing after release");
}
}
#[test]
fn drag_start_cancels_long_press() {
let mut lp = LongPressRecognizer::with_config(LongPressConfig {
long_press_ticks: 5,
repeat_ticks: 3,
});
lp.process(&Event::PressDown { x: 0, y: 0 });
for _ in 0..3 {
assert_eq!(lp.tick(), None);
}
lp.process(&Event::DragStart {
x: 20,
y: 0,
origin_x: 0,
origin_y: 0,
});
for _ in 0..10 {
assert_eq!(
lp.tick(),
None,
"DragStart must disarm the long-press recognizer"
);
}
}
#[test]
fn determinism_identical_scripts_produce_identical_output() {
let config = LongPressConfig {
long_press_ticks: 4,
repeat_ticks: 2,
};
let run = || {
let mut lp = LongPressRecognizer::with_config(config);
let mut out = Vec::new();
lp.process(&Event::PressDown { x: 7, y: 8 });
for _ in 0..8 {
if let Some(e) = lp.tick() {
out.push(e);
}
}
lp.process(&Event::PressRelease { x: 7, y: 8 });
for _ in 0..3 {
assert_eq!(lp.tick(), None);
}
out
};
let first = run();
let second = run();
assert_eq!(
first, second,
"identical input+tick sequences must produce identical output"
);
}
#[test]
fn long_press_does_not_emit_tap_events() {
let mut lp = LongPressRecognizer::with_config(LongPressConfig {
long_press_ticks: 3,
repeat_ticks: 2,
});
let events = hold_contact(&mut lp, 50, 60, 10);
lp.process(&Event::PointerUp { x: 50, y: 60 });
let pass = lp.process(&Event::PressRelease { x: 50, y: 60 });
for _ in 0..5 {
assert_eq!(lp.tick(), None);
}
for e in &events {
assert!(
matches!(e, Event::LongPress { .. } | Event::LongPressRepeat { .. }),
"recognizer emitted unexpected event from tick: {e:?}"
);
}
assert_eq!(
pass,
Some(Event::PressRelease { x: 50, y: 60 }),
"process() must always pass events through"
);
}
#[test]
fn position_tracks_drag_move_while_armed() {
let mut lp = LongPressRecognizer::with_config(LongPressConfig {
long_press_ticks: 4,
repeat_ticks: 2,
});
lp.process(&Event::PressDown { x: 0, y: 0 });
lp.process(&Event::DragMove { x: 3, y: 4 });
let mut out = Vec::new();
for _ in 0..4 {
if let Some(e) = lp.tick() {
out.push(e);
}
}
assert_eq!(
out,
vec![Event::LongPress { x: 3, y: 4 }],
"LongPress coordinates must reflect last DragMove position"
);
}
#[test]
fn pointer_up_disarms_recognizer() {
let mut lp = LongPressRecognizer::with_config(LongPressConfig {
long_press_ticks: 5,
repeat_ticks: 2,
});
lp.process(&Event::PressDown { x: 1, y: 2 });
for _ in 0..3 {
lp.tick();
}
lp.process(&Event::PointerUp { x: 1, y: 2 });
for _ in 0..10 {
assert_eq!(lp.tick(), None, "PointerUp must disarm");
}
}
#[test]
fn non_pointer_events_pass_through_long_press_recognizer() {
let mut lp = LongPressRecognizer::new();
assert_eq!(lp.process(&Event::Tick), Some(Event::Tick));
}
}