pub mod clock;
pub mod hit_slop;
pub mod table;
pub mod touch_action;
pub mod trace;
use std::collections::HashMap;
use std::num::NonZeroU64;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Mutex, OnceLock};
use std::time::Duration;
use teksilo_canvas::{Point, Size};
use teksilo_tokens::PointerKind;
use crate::event::{ButtonMask, Modifiers, PointerButton, ScrollDelta};
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct PointerId(NonZeroU64);
impl PointerId {
pub const MOUSE: Self = Self(NonZeroU64::new(1).unwrap());
pub const fn get(self) -> u64 {
self.0.get()
}
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct BackendDeviceKey(u64);
impl BackendDeviceKey {
pub const DEFAULT: Self = Self(0);
pub const fn new(raw: u64) -> Self {
Self(raw)
}
pub const fn get(self) -> u64 {
self.0
}
}
#[derive(Debug)]
pub struct PointerIdAllocator {
next: AtomicU64,
live: Mutex<HashMap<(BackendDeviceKey, u64), PointerId>>,
}
static GLOBAL_ALLOCATOR: OnceLock<PointerIdAllocator> = OnceLock::new();
impl PointerIdAllocator {
pub fn global() -> &'static Self {
GLOBAL_ALLOCATOR.get_or_init(|| Self {
next: AtomicU64::new(2),
live: Mutex::new(HashMap::new()),
})
}
pub fn begin(&self, device: BackendDeviceKey, os_id: u64) -> PointerId {
let raw = self.next.fetch_add(1, Ordering::Relaxed);
let id = PointerId(NonZeroU64::new(raw).expect("allocator starts at 2 and only grows"));
if let Ok(mut live) = self.live.lock() {
live.insert((device, os_id), id);
}
id
}
pub fn get(&self, device: BackendDeviceKey, os_id: u64) -> Option<PointerId> {
self.live
.lock()
.ok()
.and_then(|live| live.get(&(device, os_id)).copied())
}
pub fn end(&self, device: BackendDeviceKey, os_id: u64) -> Option<PointerId> {
self.live
.lock()
.ok()
.and_then(|mut live| live.remove(&(device, os_id)))
}
pub fn live_count(&self) -> usize {
self.live.lock().map(|live| live.len()).unwrap_or(0)
}
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct EventTime(Duration);
impl EventTime {
pub const ZERO: Self = Self(Duration::ZERO);
pub const fn from_duration(d: Duration) -> Self {
Self(d)
}
pub const fn from_millis(ms: u64) -> Self {
Self(Duration::from_millis(ms))
}
pub const fn as_duration(self) -> Duration {
self.0
}
pub const fn saturating_since(self, earlier: Self) -> Duration {
self.0.saturating_sub(earlier.0)
}
pub fn checked_add(self, d: Duration) -> Option<Self> {
self.0.checked_add(d).map(Self)
}
}
impl std::ops::Add<Duration> for EventTime {
type Output = Self;
fn add(self, d: Duration) -> Self {
Self(self.0.saturating_add(d))
}
}
#[non_exhaustive]
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub struct PointerAxes {
pub pressure: Option<f32>,
pub tangential_pressure: Option<f32>,
pub tilt: Option<(f32, f32)>,
pub twist: Option<f32>,
pub contact: Option<Size>,
}
#[non_exhaustive]
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct PointerInfo {
pub id: PointerId,
pub kind: PointerKind,
pub primary: bool,
pub buttons: ButtonMask,
pub axes: PointerAxes,
pub time: EventTime,
pub palm: bool,
}
impl PointerInfo {
pub const fn mouse(time: EventTime) -> Self {
Self {
id: PointerId::MOUSE,
kind: PointerKind::Mouse,
primary: true,
buttons: ButtonMask::NONE,
axes: PointerAxes {
pressure: None,
tangential_pressure: None,
tilt: None,
twist: None,
contact: None,
},
time,
palm: false,
}
}
pub const fn touch(id: PointerId, time: EventTime) -> Self {
Self {
id,
kind: PointerKind::Touch,
primary: false,
buttons: ButtonMask::NONE,
axes: PointerAxes {
pressure: None,
tangential_pressure: None,
tilt: None,
twist: None,
contact: None,
},
time,
palm: false,
}
}
pub const fn is_direct(&self) -> bool {
self.kind.is_direct()
}
pub const fn is_coarse(&self) -> bool {
self.kind.is_coarse()
}
pub const fn is_precise(&self) -> bool {
self.kind.is_precise()
}
pub fn effective_pressure(&self) -> f32 {
match self.axes.pressure {
Some(p) => p,
None if !self.buttons.is_empty() => 0.5,
None => 0.0,
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub enum PointerPhase {
Down,
Move,
Up,
Cancel,
}
#[derive(Copy, Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct CoalescedSample {
pub time: EventTime,
pub window_position: Point,
pub axes: PointerAxes,
}
impl CoalescedSample {
pub fn new(time: EventTime, window_position: Point) -> Self {
Self {
time,
window_position,
axes: PointerAxes::default(),
}
}
pub fn with_axes(mut self, axes: PointerAxes) -> Self {
self.axes = axes;
self
}
}
#[derive(Clone, Debug)]
pub struct PointerSample {
pub pointer: PointerInfo,
pub phase: PointerPhase,
pub position: Point,
pub button: Option<PointerButton>,
pub modifiers: Modifiers,
pub coalesced: Vec<CoalescedSample>,
}
impl PointerSample {
pub fn mouse(phase: PointerPhase, position: Point, time: EventTime) -> Self {
Self {
pointer: PointerInfo::mouse(time),
phase,
position,
button: None,
modifiers: Modifiers::NONE,
coalesced: Vec::new(),
}
}
pub fn with_button(mut self, button: PointerButton) -> Self {
self.button = Some(button);
self
}
pub fn with_modifiers(mut self, modifiers: Modifiers) -> Self {
self.modifiers = modifiers;
self
}
}
#[non_exhaustive]
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Default)]
pub enum ScrollPhase {
#[default]
Discrete,
Began,
Changed,
Ended,
Momentum,
MomentumEnded,
Fling,
Cancelled,
}
#[non_exhaustive]
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Default)]
pub enum ScrollSource {
#[default]
Wheel,
Trackpad,
TouchPan,
Programmatic,
}
#[derive(Clone, Debug)]
pub struct ScrollSample {
pub delta: ScrollDelta,
pub position: Option<Point>,
pub phase: ScrollPhase,
pub source: ScrollSource,
pub pointer: PointerInfo,
pub modifiers: Modifiers,
}
impl ScrollSample {
pub fn wheel(delta: ScrollDelta, modifiers: Modifiers, time: EventTime) -> Self {
Self {
delta,
position: None,
phase: ScrollPhase::Discrete,
source: ScrollSource::Wheel,
pointer: PointerInfo::mouse(time),
modifiers,
}
}
pub fn at(mut self, position: Point) -> Self {
self.position = Some(position);
self
}
}
#[non_exhaustive]
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum CancelReason {
Platform,
WindowDeactivated,
Occluded,
ModalOpened,
SubtreeParked,
WidgetDestroyed,
CaptureOrphaned,
OsDragStarted,
ExternalDndTakeover,
PeerClaimed,
OverlayDismissed,
MultiContactIgnored,
ContactCapExceeded,
PalmRejected,
Deactivated,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct InputSnapshot {
pub(crate) pointer: PointerInfo,
pub(crate) position: Option<Point>,
pub(crate) scroll_phase: ScrollPhase,
pub(crate) scroll_source: ScrollSource,
pub(crate) coalesced: Vec<CoalescedSample>,
}
impl Default for InputSnapshot {
fn default() -> Self {
Self {
pointer: PointerInfo::mouse(EventTime::ZERO),
position: None,
scroll_phase: ScrollPhase::Discrete,
scroll_source: ScrollSource::Wheel,
coalesced: Vec::new(),
}
}
}
impl InputSnapshot {
pub(crate) fn from_pointer_sample(sample: &PointerSample) -> Self {
Self {
pointer: sample.pointer,
position: Some(sample.position),
coalesced: sample.coalesced.clone(),
..Self::default()
}
}
pub(crate) fn for_recognized_gesture(pointer: PointerInfo) -> Self {
Self {
pointer,
coalesced: Vec::new(),
..Self::default()
}
}
pub(crate) fn for_drag_session(pointer: PointerInfo) -> Self {
Self {
pointer,
coalesced: Vec::new(),
..Self::default()
}
}
pub(crate) fn from_scroll_sample(sample: &ScrollSample) -> Self {
Self {
pointer: sample.pointer,
position: sample.position,
scroll_phase: sample.phase,
scroll_source: sample.source,
coalesced: Vec::new(),
}
}
pub(crate) fn from_event(event: &crate::event::WidgetEvent) -> Self {
use crate::event::WidgetEvent;
match event {
WidgetEvent::PointerDown {
position, pointer, ..
}
| WidgetEvent::PointerUp {
position, pointer, ..
}
| WidgetEvent::PointerMove {
position, pointer, ..
} => Self {
pointer: *pointer,
position: Some(*position),
..Self::default()
},
WidgetEvent::PointerEnter { pointer } | WidgetEvent::PointerLeave { pointer } => Self {
pointer: *pointer,
..Self::default()
},
WidgetEvent::Scroll {
window_position,
phase,
pointer,
..
} => Self {
pointer: *pointer,
position: *window_position,
scroll_phase: *phase,
scroll_source: ScrollSource::Wheel,
coalesced: Vec::new(),
},
WidgetEvent::PointerCancel {
window_position,
pointer,
..
} => Self {
pointer: *pointer,
position: *window_position,
..Self::default()
},
_ => Self::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn event_time_measures_from_the_epoch() {
let t = EventTime::from_millis(250);
assert_eq!(t.as_duration(), Duration::from_millis(250));
assert_eq!(EventTime::ZERO.as_duration(), Duration::ZERO);
assert_eq!(EventTime::default(), EventTime::ZERO);
}
#[test]
fn saturating_since_measures_forward() {
let a = EventTime::from_millis(100);
let b = EventTime::from_millis(350);
assert_eq!(b.saturating_since(a), Duration::from_millis(250));
assert_eq!(a.saturating_since(a), Duration::ZERO);
}
#[test]
fn saturating_since_clamps_an_inverted_pair() {
let early = EventTime::from_millis(10);
let late = EventTime::from_millis(900);
assert_eq!(early.saturating_since(late), Duration::ZERO);
}
#[test]
fn checked_add_reports_overflow() {
let t = EventTime::from_millis(5);
assert_eq!(
t.checked_add(Duration::from_millis(15)),
Some(EventTime::from_millis(20))
);
assert_eq!(t.checked_add(Duration::MAX), None);
}
#[test]
fn event_times_order_by_their_offset() {
let mut times = [
EventTime::from_millis(30),
EventTime::ZERO,
EventTime::from_millis(7),
];
times.sort();
assert_eq!(
times,
[
EventTime::ZERO,
EventTime::from_millis(7),
EventTime::from_millis(30)
]
);
}
#[test]
fn a_reused_os_id_mints_a_fresh_pointer_id() {
let alloc = PointerIdAllocator::global();
let device = BackendDeviceKey::new(0xFEED);
let first = alloc.begin(device, 7);
assert_eq!(alloc.get(device, 7), Some(first));
assert_eq!(alloc.end(device, 7), Some(first));
assert_eq!(alloc.get(device, 7), None);
let second = alloc.begin(device, 7);
assert_ne!(first, second, "a reused OS id must not reuse the PointerId");
assert!(second > first, "ids are monotonic");
alloc.end(device, 7);
}
#[test]
fn the_same_os_id_on_two_devices_is_two_pointers() {
let alloc = PointerIdAllocator::global();
let screen = BackendDeviceKey::new(0xA1);
let tablet = BackendDeviceKey::new(0xB2);
let a = alloc.begin(screen, 1);
let b = alloc.begin(tablet, 1);
assert_ne!(a, b);
assert_eq!(alloc.get(screen, 1), Some(a));
assert_eq!(alloc.get(tablet, 1), Some(b));
alloc.end(screen, 1);
assert_eq!(alloc.get(tablet, 1), Some(b), "ending one leaves the other");
alloc.end(tablet, 1);
}
#[test]
fn a_second_begin_replaces_a_stranded_mapping() {
let alloc = PointerIdAllocator::global();
let device = BackendDeviceKey::new(0xC3);
let first = alloc.begin(device, 42);
let second = alloc.begin(device, 42);
assert_ne!(first, second);
assert_eq!(alloc.get(device, 42), Some(second));
alloc.end(device, 42);
}
#[test]
fn ending_an_unknown_contact_is_a_no_op() {
let alloc = PointerIdAllocator::global();
assert_eq!(alloc.end(BackendDeviceKey::new(0xD4), 999), None);
}
#[test]
fn the_mouse_id_is_never_minted() {
let alloc = PointerIdAllocator::global();
let device = BackendDeviceKey::new(0xE5);
let id = alloc.begin(device, 3);
assert_ne!(id, PointerId::MOUSE);
assert_eq!(PointerId::MOUSE.get(), 1);
alloc.end(device, 3);
}
#[test]
fn the_mouse_constructor_is_the_legacy_pointer() {
let m = PointerInfo::mouse(EventTime::ZERO);
assert_eq!(m.id, PointerId::MOUSE);
assert_eq!(m.kind, PointerKind::Mouse);
assert!(m.primary);
assert!(m.buttons.is_empty());
assert_eq!(m.axes, PointerAxes::default());
assert!(!m.is_direct() && !m.is_coarse() && m.is_precise());
}
#[test]
fn a_touch_contact_is_direct_and_coarse() {
let t = PointerInfo::touch(PointerId::MOUSE, EventTime::ZERO);
assert_eq!(t.kind, PointerKind::Touch);
assert!(t.is_direct() && t.is_coarse() && !t.is_precise());
assert!(
!t.primary,
"primacy is the pointer table's decision, not the constructor's"
);
}
#[test]
fn effective_pressure_follows_the_w3c_rule() {
let mut m = PointerInfo::mouse(EventTime::ZERO);
assert_eq!(m.effective_pressure(), 0.0);
m.buttons = ButtonMask::PRIMARY;
assert_eq!(m.effective_pressure(), 0.5);
m.axes.pressure = Some(0.75);
assert_eq!(m.effective_pressure(), 0.75);
m.buttons = ButtonMask::NONE;
assert_eq!(m.effective_pressure(), 0.75, "a reported value always wins");
}
#[test]
fn a_mouse_sample_carries_no_coalesced_history() {
let s = PointerSample::mouse(PointerPhase::Down, Point::new(3.0, 4.0), EventTime::ZERO)
.with_button(PointerButton::Primary)
.with_modifiers(Modifiers::SHIFT);
assert!(s.coalesced.is_empty());
assert_eq!(s.button, Some(PointerButton::Primary));
assert_eq!(s.modifiers, Modifiers::SHIFT);
assert_eq!(s.pointer.id, PointerId::MOUSE);
}
#[test]
fn a_wheel_sample_is_discrete_and_positionless() {
let s = ScrollSample::wheel(
ScrollDelta::Lines { x: 0.0, y: -1.0 },
Modifiers::NONE,
EventTime::ZERO,
);
assert_eq!(s.phase, ScrollPhase::Discrete);
assert_eq!(s.source, ScrollSource::Wheel);
assert_eq!(s.position, None);
let at = s.at(Point::new(10.0, 20.0));
assert_eq!(at.position, Some(Point::new(10.0, 20.0)));
}
#[test]
fn scroll_defaults_are_todays_wheel() {
assert_eq!(ScrollPhase::default(), ScrollPhase::Discrete);
assert_eq!(ScrollSource::default(), ScrollSource::Wheel);
}
#[test]
fn the_default_snapshot_is_a_mouse_at_the_epoch() {
let s = InputSnapshot::default();
assert_eq!(s.pointer.id, PointerId::MOUSE);
assert_eq!(s.pointer.time, EventTime::ZERO);
assert_eq!(s.position, None);
assert_eq!(s.scroll_phase, ScrollPhase::Discrete);
assert_eq!(s.scroll_source, ScrollSource::Wheel);
}
#[test]
fn a_scroll_sample_snapshot_keeps_its_phase_and_source() {
let sample = ScrollSample {
delta: ScrollDelta::Pixels { x: 0.0, y: 12.0 },
position: Some(Point::new(5.0, 5.0)),
phase: ScrollPhase::Momentum,
source: ScrollSource::TouchPan,
pointer: PointerInfo::mouse(EventTime::from_millis(9)),
modifiers: Modifiers::NONE,
};
let snap = InputSnapshot::from_scroll_sample(&sample);
assert_eq!(snap.scroll_phase, ScrollPhase::Momentum);
assert_eq!(snap.scroll_source, ScrollSource::TouchPan);
assert_eq!(snap.position, Some(Point::new(5.0, 5.0)));
}
}