use crate::compat::{HashMap, MiniToString, Vec};
use crate::event::types::{mouse_button, Event, TouchId};
pub const TOUCH_CANCEL_EVENT_NAME: &str = "touch_cancel";
pub fn touch_cancel(pos: crate::core::Point, touch_id: TouchId) -> Event {
let mut payload = Vec::with_capacity(16);
payload.extend_from_slice(&touch_id.to_le_bytes());
payload.extend_from_slice(&pos.x.to_le_bytes());
payload.extend_from_slice(&pos.y.to_le_bytes());
Event::Custom { name: TOUCH_CANCEL_EVENT_NAME.to_string(), payload }
}
pub fn is_touch_cancel(event: &Event) -> bool {
matches!(event, Event::Custom { name, .. } if name == TOUCH_CANCEL_EVENT_NAME)
}
pub fn touch_cancel_payload(event: &Event) -> Option<(crate::core::Point, TouchId)> {
let Event::Custom { name, payload } = event else {
return None;
};
if name != TOUCH_CANCEL_EVENT_NAME {
return None;
}
if payload.len() < 16 {
return None;
}
let touch_id = TouchId::from_le_bytes(payload[0..8].try_into().ok()?);
let x = i32::from_le_bytes(payload[8..12].try_into().ok()?);
let y = i32::from_le_bytes(payload[12..16].try_into().ok()?);
Some((crate::core::Point::new(x, y), touch_id))
}
#[derive(Debug, Default)]
pub struct TouchEventTranslator {
active_touches: HashMap<TouchId, (/* x */ i32, /* y */ i32)>,
enabled: bool,
}
impl TouchEventTranslator {
pub fn new() -> Self {
Self { active_touches: HashMap::new(), enabled: true }
}
pub fn disabled() -> Self {
Self { active_touches: HashMap::new(), enabled: false }
}
pub fn set_enabled(&mut self, enabled: bool) {
if !enabled {
self.active_touches.clear();
}
self.enabled = enabled;
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn translate(&mut self, event: &Event) -> Vec<Event> {
if !self.enabled {
return Vec::new();
}
match *event {
Event::TouchBegin { pos, touch_id } => {
self.active_touches.insert(touch_id, (pos.x, pos.y));
vec![
Event::MousePress { pos, button: mouse_button::PRIMARY, modifiers: 0 },
Event::MouseEnter { pos },
]
}
Event::TouchMove { pos, touch_id } => {
if let Some(pos_ref) = self.active_touches.get_mut(&touch_id) {
*pos_ref = (pos.x, pos.y);
vec![Event::MouseMove { pos }]
} else {
Vec::new()
}
}
Event::TouchEnd { pos, touch_id } => {
if self.active_touches.remove(&touch_id).is_none() {
return Vec::new();
}
vec![
Event::MouseRelease { pos, button: mouse_button::PRIMARY },
Event::MouseLeave { pos },
]
}
_ if is_touch_cancel(event) => {
if let Some((_, touch_id)) = touch_cancel_payload(event) {
self.active_touches.remove(&touch_id);
} else {
self.active_touches.clear();
}
Vec::new()
}
_ => Vec::new(),
}
}
pub fn clear(&mut self) {
self.active_touches.clear();
}
pub fn active_touch_count(&self) -> usize {
self.active_touches.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Point;
#[test]
fn touch_begin_generates_mouse_press_and_enter() {
let mut t = TouchEventTranslator::new();
let ev = Event::TouchBegin { pos: Point::new(10, 20), touch_id: 1 };
let result = t.translate(&ev);
assert_eq!(result.len(), 2);
assert!(matches!(result[0], Event::MousePress { .. }));
assert!(matches!(result[1], Event::MouseEnter { .. }));
assert_eq!(t.active_touch_count(), 1);
}
#[test]
fn touch_begin_press_carries_the_primary_button() {
let mut t = TouchEventTranslator::new();
let ev = Event::TouchBegin { pos: Point::new(3, 4), touch_id: 1 };
let result = t.translate(&ev);
match result[0] {
Event::MousePress { pos, button, .. } => {
assert_eq!(pos, Point::new(3, 4));
assert_eq!(button, crate::event::types::mouse_button::PRIMARY);
}
_ => panic!("expected a synthesized MousePress"),
}
}
#[test]
fn touch_end_release_carries_the_primary_button() {
let mut t = TouchEventTranslator::new();
t.translate(&Event::TouchBegin { pos: Point::new(0, 0), touch_id: 1 });
let result = t.translate(&Event::TouchEnd { pos: Point::new(9, 8), touch_id: 1 });
match result[0] {
Event::MouseRelease { pos, button } => {
assert_eq!(pos, Point::new(9, 8));
assert_eq!(button, crate::event::types::mouse_button::PRIMARY);
}
_ => panic!("expected a synthesized MouseRelease"),
}
}
#[test]
fn touch_end_without_begin_is_ignored() {
let mut t = TouchEventTranslator::new();
let ev = Event::TouchEnd { pos: Point::new(10, 20), touch_id: 42 };
assert!(
t.translate(&ev).is_empty(),
"an end without a matching begin must not synthesize a release"
);
assert_eq!(t.active_touch_count(), 0);
}
#[test]
fn repeated_touch_end_is_ignored() {
let mut t = TouchEventTranslator::new();
t.translate(&Event::TouchBegin { pos: Point::new(0, 0), touch_id: 1 });
let first = t.translate(&Event::TouchEnd { pos: Point::new(5, 5), touch_id: 1 });
assert_eq!(first.len(), 2, "the first end is the real one");
let second = t.translate(&Event::TouchEnd { pos: Point::new(5, 5), touch_id: 1 });
assert!(second.is_empty(), "a repeated end must not synthesize a second release");
}
#[test]
fn normal_begin_end_pairing_is_unchanged() {
let mut t = TouchEventTranslator::new();
let begin = t.translate(&Event::TouchBegin { pos: Point::new(1, 1), touch_id: 7 });
assert_eq!(begin.len(), 2);
let end = t.translate(&Event::TouchEnd { pos: Point::new(2, 2), touch_id: 7 });
assert_eq!(end.len(), 2);
assert_eq!(t.active_touch_count(), 0);
}
#[test]
fn multi_touch_ends_are_independent() {
let mut t = TouchEventTranslator::new();
t.translate(&Event::TouchBegin { pos: Point::new(1, 1), touch_id: 1 });
t.translate(&Event::TouchBegin { pos: Point::new(2, 2), touch_id: 2 });
let end_one = t.translate(&Event::TouchEnd { pos: Point::new(1, 1), touch_id: 1 });
assert_eq!(end_one.len(), 2, "touch 1's end is a real end");
assert_eq!(t.active_touch_count(), 1, "touch 2 is still active");
assert!(t.translate(&Event::TouchEnd { pos: Point::new(1, 1), touch_id: 1 }).is_empty());
let end_two = t.translate(&Event::TouchEnd { pos: Point::new(2, 2), touch_id: 2 });
assert_eq!(end_two.len(), 2);
assert_eq!(t.active_touch_count(), 0);
}
#[test]
fn disabled_then_reenabled_translation() {
let mut t = TouchEventTranslator::disabled();
assert!(t.translate(&Event::TouchBegin { pos: Point::new(0, 0), touch_id: 1 }).is_empty());
assert_eq!(t.active_touch_count(), 0);
t.set_enabled(true);
assert!(t.is_enabled());
let begin = t.translate(&Event::TouchBegin { pos: Point::new(0, 0), touch_id: 1 });
assert_eq!(begin.len(), 2);
let end = t.translate(&Event::TouchEnd { pos: Point::new(1, 1), touch_id: 1 });
assert_eq!(end.len(), 2);
}
#[test]
fn touch_move_generates_mouse_move() {
let mut t = TouchEventTranslator::new();
t.translate(&Event::TouchBegin { pos: Point::new(0, 0), touch_id: 1 });
let ev = Event::TouchMove { pos: Point::new(10, 20), touch_id: 1 };
let result = t.translate(&ev);
assert_eq!(result.len(), 1);
assert!(matches!(result[0], Event::MouseMove { .. }));
}
#[test]
fn touch_end_generates_mouse_release_and_leave() {
let mut t = TouchEventTranslator::new();
t.translate(&Event::TouchBegin { pos: Point::new(0, 0), touch_id: 1 });
let ev = Event::TouchEnd { pos: Point::new(10, 20), touch_id: 1 };
let result = t.translate(&ev);
assert_eq!(result.len(), 2);
assert!(matches!(result[0], Event::MouseRelease { .. }));
assert!(matches!(result[1], Event::MouseLeave { .. }));
assert_eq!(t.active_touch_count(), 0);
}
#[test]
fn unknown_touch_id_ignored() {
let mut t = TouchEventTranslator::new();
let ev = Event::TouchMove { pos: Point::new(10, 20), touch_id: 999 };
let result = t.translate(&ev);
assert!(result.is_empty());
}
#[test]
fn disabled_translator_produces_no_events() {
let mut t = TouchEventTranslator::disabled();
let ev = Event::TouchBegin { pos: Point::new(10, 20), touch_id: 1 };
let result = t.translate(&ev);
assert!(result.is_empty());
}
#[test]
fn touch_cancel_drops_the_touch_without_synthesizing_a_release() {
let mut t = TouchEventTranslator::new();
t.translate(&Event::TouchBegin { pos: Point::new(10, 10), touch_id: 1 });
assert_eq!(t.active_touch_count(), 1);
let cancel = touch_cancel(Point::new(10, 10), 1);
assert!(is_touch_cancel(&cancel));
let out = t.translate(&cancel);
assert!(out.is_empty(), "a cancel must not synthesize an activating mouse release");
assert_eq!(t.active_touch_count(), 0, "the cancelled touch must be dropped");
assert!(
t.translate(&Event::TouchEnd { pos: Point::new(10, 10), touch_id: 1 }).is_empty(),
"an end after a cancel must not synthesize a release"
);
}
#[test]
fn touch_cancel_payload_round_trips() {
let pos = Point::new(-12, 345);
let cancel = touch_cancel(pos, 42);
assert_eq!(touch_cancel_payload(&cancel), Some((pos, 42)));
assert_eq!(touch_cancel_payload(&Event::TouchBegin { pos, touch_id: 1 }), None);
assert_eq!(
touch_cancel_payload(&Event::Custom {
name: TOUCH_CANCEL_EVENT_NAME.to_string(),
payload: alloc::vec![1, 2, 3],
}),
None
);
}
#[test]
fn touch_cancel_of_one_id_keeps_the_others() {
let mut t = TouchEventTranslator::new();
t.translate(&Event::TouchBegin { pos: Point::new(1, 1), touch_id: 1 });
t.translate(&Event::TouchBegin { pos: Point::new(2, 2), touch_id: 2 });
assert_eq!(t.active_touch_count(), 2);
let out = t.translate(&touch_cancel(Point::new(1, 1), 1));
assert!(out.is_empty());
assert_eq!(t.active_touch_count(), 1, "only the cancelled touch is dropped");
let end = t.translate(&Event::TouchEnd { pos: Point::new(2, 2), touch_id: 2 });
assert_eq!(end.len(), 2);
assert_eq!(t.active_touch_count(), 0);
}
#[test]
fn non_touch_events_produce_no_translation() {
let mut t = TouchEventTranslator::new();
let ev = Event::MousePress { pos: Point::new(10, 20), button: 0, modifiers: 0 };
let result = t.translate(&ev);
assert!(result.is_empty());
}
#[test]
fn clear_removes_all_tracked_touches() {
let mut t = TouchEventTranslator::new();
t.translate(&Event::TouchBegin { pos: Point::new(0, 0), touch_id: 1 });
t.translate(&Event::TouchBegin { pos: Point::new(0, 0), touch_id: 2 });
assert_eq!(t.active_touch_count(), 2);
t.clear();
assert_eq!(t.active_touch_count(), 0);
}
#[test]
fn multiple_touches_tracked_independently() {
let mut t = TouchEventTranslator::new();
t.translate(&Event::TouchBegin { pos: Point::new(10, 20), touch_id: 1 });
t.translate(&Event::TouchBegin { pos: Point::new(30, 40), touch_id: 2 });
assert_eq!(t.active_touch_count(), 2);
let r1 = t.translate(&Event::TouchMove { pos: Point::new(15, 25), touch_id: 1 });
assert_eq!(r1.len(), 1);
let r2 = t.translate(&Event::TouchEnd { pos: Point::new(35, 45), touch_id: 2 });
assert_eq!(r2.len(), 2);
assert_eq!(t.active_touch_count(), 1);
}
#[test]
fn disabling_clears_active_touches_so_reenable_produces_no_orphan_release() {
let mut t = TouchEventTranslator::new();
t.translate(&Event::TouchBegin { pos: Point::new(5, 5), touch_id: 1 });
assert_eq!(t.active_touch_count(), 1);
t.set_enabled(false);
assert_eq!(
t.active_touch_count(),
0,
"disabling must drop the tracked touch, or a stale end can pair with it later"
);
assert!(t.translate(&Event::TouchEnd { pos: Point::new(5, 5), touch_id: 1 }).is_empty());
t.set_enabled(true);
let late = t.translate(&Event::TouchEnd { pos: Point::new(5, 5), touch_id: 1 });
assert!(
late.is_empty(),
"a repeated end after re-enable must not produce an unmatched release"
);
assert_eq!(t.active_touch_count(), 0);
assert_eq!(t.translate(&Event::TouchBegin { pos: Point::new(7, 7), touch_id: 1 }).len(), 2);
assert_eq!(t.translate(&Event::TouchEnd { pos: Point::new(8, 8), touch_id: 1 }).len(), 2);
}
#[test]
fn disabling_clears_all_active_touches_and_keeps_ids_reusable() {
let mut t = TouchEventTranslator::new();
t.translate(&Event::TouchBegin { pos: Point::new(1, 1), touch_id: 1 });
t.translate(&Event::TouchBegin { pos: Point::new(2, 2), touch_id: 2 });
t.translate(&Event::TouchBegin { pos: Point::new(3, 3), touch_id: 3 });
assert_eq!(t.active_touch_count(), 3);
t.set_enabled(false);
assert_eq!(t.active_touch_count(), 0, "every tracked touch is cleared on disable");
t.set_enabled(true);
for id in 1..=3 {
assert!(
t.translate(&Event::TouchEnd { pos: Point::new(9, 9), touch_id: id }).is_empty(),
"id {id} had no begin in this enabled cycle, so its end must be ignored"
);
}
for id in 1..=3 {
assert_eq!(
t.translate(&Event::TouchBegin { pos: Point::new(0, 0), touch_id: id }).len(),
2
);
}
for id in 1..=3 {
assert_eq!(
t.translate(&Event::TouchEnd { pos: Point::new(4, 4), touch_id: id }).len(),
2
);
}
assert_eq!(t.active_touch_count(), 0);
}
#[test]
fn repeated_disable_is_idempotent_and_never_revives_stale_touches() {
let mut t = TouchEventTranslator::new();
t.translate(&Event::TouchBegin { pos: Point::new(1, 1), touch_id: 9 });
t.set_enabled(false);
t.set_enabled(false);
assert_eq!(t.active_touch_count(), 0);
t.set_enabled(true);
assert_eq!(t.active_touch_count(), 0, "re-enabling must not resurrect cleared touches");
assert!(
t.translate(&Event::TouchEnd { pos: Point::new(1, 1), touch_id: 9 }).is_empty(),
"an end for a pre-disable begin must stay unmatched"
);
}
}