use teksilo_canvas::Point;
use teksilo_tokens::GestureProfile;
use crate::pointer::PointerAxes;
pub const PALM_CONTACT_THRESHOLD: f32 = 40.0;
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct PalmWatch {
origin: Point,
largest: f32,
travelled: bool,
}
impl PalmWatch {
pub fn press(origin: Point, axes: &PointerAxes) -> Self {
Self {
origin,
largest: extent(axes),
travelled: false,
}
}
pub fn sample(&mut self, position: Point, axes: &PointerAxes, profile: &GestureProfile) {
self.largest = self.largest.max(extent(axes));
if !self.travelled {
let dx = position.x - self.origin.x;
let dy = position.y - self.origin.y;
self.travelled = (dx * dx + dy * dy).sqrt() > profile.tap_slop;
}
}
pub fn is_palm(&self) -> bool {
self.largest > PALM_CONTACT_THRESHOLD && !self.travelled
}
pub fn travelled(&self) -> bool {
self.travelled
}
pub fn largest_extent(&self) -> f32 {
self.largest
}
}
fn extent(axes: &PointerAxes) -> f32 {
axes.contact
.map(|size| size.width.max(size.height))
.unwrap_or(0.0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gesture::default_profile;
use teksilo_canvas::Size;
use teksilo_tokens::PointerKind;
fn touch() -> GestureProfile {
default_profile(PointerKind::Touch)
}
fn contact(extent: f32) -> PointerAxes {
PointerAxes {
pressure: None,
tangential_pressure: None,
tilt: None,
twist: None,
contact: Some(Size::new(extent, extent)),
}
}
fn no_size() -> PointerAxes {
PointerAxes {
pressure: None,
tangential_pressure: None,
tilt: None,
twist: None,
contact: None,
}
}
#[test]
fn a_large_stationary_contact_is_a_palm() {
let profile = touch();
let mut watch = PalmWatch::press(Point::new(50.0, 50.0), &contact(60.0));
watch.sample(Point::new(51.0, 50.5), &contact(60.0), &profile);
assert!(watch.is_palm());
}
#[test]
fn a_large_contact_that_travels_is_left_alone() {
let profile = touch();
let mut watch = PalmWatch::press(Point::new(50.0, 50.0), &contact(60.0));
watch.sample(
Point::new(50.0, 50.0 + profile.tap_slop + 1.0),
&contact(60.0),
&profile,
);
assert!(watch.travelled());
assert!(
!watch.is_palm(),
"a deliberate drag from a large contact must keep working"
);
}
#[test]
fn travel_is_latched_so_coming_back_does_not_undo_it() {
let profile = touch();
let mut watch = PalmWatch::press(Point::ZERO, &contact(60.0));
watch.sample(
Point::new(0.0, profile.tap_slop + 10.0),
&contact(60.0),
&profile,
);
watch.sample(Point::ZERO, &contact(60.0), &profile);
assert!(watch.travelled());
assert!(!watch.is_palm());
}
#[test]
fn a_fingertip_is_never_a_palm() {
let profile = touch();
let mut watch = PalmWatch::press(Point::ZERO, &contact(12.0));
watch.sample(Point::new(0.5, 0.5), &contact(14.0), &profile);
assert!(!watch.is_palm());
}
#[test]
fn a_contact_that_grows_into_a_palm_is_caught() {
let profile = touch();
let mut watch = PalmWatch::press(Point::ZERO, &contact(10.0));
watch.sample(Point::new(0.5, 0.0), &contact(35.0), &profile);
watch.sample(Point::new(0.5, 0.5), &contact(70.0), &profile);
assert_eq!(watch.largest_extent(), 70.0, "the maximum is what counts");
assert!(watch.is_palm());
}
#[test]
fn a_backend_that_reports_no_contact_size_makes_the_watch_inert() {
let profile = touch();
let mut watch = PalmWatch::press(Point::ZERO, &no_size());
watch.sample(Point::new(0.2, 0.2), &no_size(), &profile);
assert_eq!(watch.largest_extent(), 0.0);
assert!(
!watch.is_palm(),
"with no size reported the heuristic must never fire"
);
}
}