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;
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 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
}
}
#[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 { .. })));
}
}