#![cfg_attr(
not(all(unix, not(target_os = "macos"))),
allow(rustdoc::broken_intra_doc_links)
)]
pub mod null;
#[cfg(all(unix, not(target_os = "macos")))]
pub mod wayland;
pub mod windows;
use teksilo_canvas::{Point, Size};
use teksilo_core::pointer::EventTime;
use teksilo_core::raw_handle::ParentHandle;
use teksilo_tokens::PenKind;
use crate::pointer_backend::BackendCaps;
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Default)]
pub struct PenButtons(u8);
impl PenButtons {
pub const NONE: Self = Self(0);
pub const BARREL: Self = Self(1 << 0);
pub const SECONDARY_BARREL: Self = Self(1 << 1);
pub const ERASER: Self = Self(1 << 2);
pub const fn bits(self) -> u8 {
self.0
}
pub const fn contains(self, other: Self) -> bool {
self.0 & other.0 == other.0
}
pub const fn is_empty(self) -> bool {
self.0 == 0
}
pub const fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
pub const fn without(self, other: Self) -> Self {
Self(self.0 & !other.0)
}
pub const fn with(self, other: Self, held: bool) -> Self {
if held {
self.union(other)
} else {
self.without(other)
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Default)]
pub struct PenCaps {
pub tool_kind: bool,
pub pressure: bool,
pub tilt: bool,
pub twist: bool,
pub touch_contact: bool,
}
impl PenCaps {
pub const NONE: Self = Self {
tool_kind: false,
pressure: false,
tilt: false,
twist: false,
touch_contact: false,
};
pub const FULL_PEN: Self = Self {
tool_kind: true,
pressure: true,
tilt: true,
twist: true,
touch_contact: false,
};
pub fn apply_to(self, caps: &mut BackendCaps) {
caps.reports_pen_kind |= self.tool_kind;
caps.reports_pressure |= self.pressure;
caps.reports_tilt |= self.tilt;
caps.reports_twist |= self.twist;
}
}
#[derive(Copy, Clone, PartialEq, Debug)]
pub struct PenPacket {
pub tool: PenKind,
pub position: Point,
pub pressure: f32,
pub tilt: Option<(f32, f32)>,
pub twist: Option<f32>,
pub buttons: PenButtons,
pub in_proximity: bool,
pub down: bool,
pub device_time_ms: Option<u32>,
}
impl PenPacket {
pub fn hovering(tool: PenKind, position: Point) -> Self {
Self {
tool,
position,
pressure: 0.0,
tilt: None,
twist: None,
buttons: PenButtons::NONE,
in_proximity: true,
down: false,
device_time_ms: None,
}
}
pub fn out_of_proximity(tool: PenKind, position: Point) -> Self {
Self {
in_proximity: false,
..Self::hovering(tool, position)
}
}
pub fn down_at(mut self, pressure: f32) -> Self {
self.down = true;
self.pressure = pressure.clamp(0.0, 1.0);
self
}
pub const fn at_device_ms(mut self, ms: u32) -> Self {
self.device_time_ms = Some(ms);
self
}
}
pub const MAX_DEVICE_GAP_MS: u64 = 10_000;
pub fn back_date(now: EventTime, device_ms: &[Option<u32>]) -> Vec<EventTime> {
let mut out = Vec::with_capacity(device_ms.len());
back_date_into(now, device_ms, &mut out);
out
}
pub fn back_date_into(now: EventTime, device_ms: &[Option<u32>], out: &mut Vec<EventTime>) {
out.clear();
let count = device_ms.len();
if count == 0 {
return;
}
let step = PEN_POLL_INTERVAL / count as u32;
let mut elapsed = std::time::Duration::ZERO;
out.push(EventTime::from_duration(elapsed));
for index in 1..count {
let advance = match (device_ms[index - 1], device_ms[index]) {
(Some(previous), Some(current)) => {
let delta = u64::from(current.wrapping_sub(previous));
if delta <= MAX_DEVICE_GAP_MS {
std::time::Duration::from_millis(delta)
} else {
step
}
}
_ => step,
};
elapsed = elapsed.saturating_add(advance);
out.push(EventTime::from_duration(elapsed));
}
let span = elapsed;
for slot in out.iter_mut() {
let before_now = span - slot.as_duration();
*slot = EventTime::from_duration(now.as_duration().saturating_sub(before_now));
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
#[non_exhaustive]
pub enum PenBatching {
#[default]
PerPacket,
Coalesce,
}
pub trait PenSource: std::fmt::Debug {
fn poll(&mut self, out: &mut Vec<PenPacket>);
fn capabilities(&self) -> PenCaps {
PenCaps::NONE
}
fn polls_off_thread(&self) -> bool {
false
}
fn touch_contact(&self, _os_contact_id: u64) -> Option<Size> {
None
}
}
pub fn create_pen_source(parent: &ParentHandle) -> Box<dyn PenSource> {
#[cfg(all(unix, not(target_os = "macos")))]
{
if let Some(source) = wayland::WaylandPenSource::attach(parent) {
return Box::new(source);
}
}
#[cfg(target_os = "windows")]
{
if let Some(source) = windows::WindowsPenSource::attach(parent) {
return Box::new(source);
}
}
let _ = parent;
Box::new(null::NullPenSource::new())
}
pub const PEN_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(4);
#[cfg(test)]
mod tests {
use super::*;
use crate::pointer_backend::{PlatformKind, PointerBackend};
use crate::window_system::WindowSystem;
#[test]
fn buttons_are_a_set() {
let held = PenButtons::NONE.with(PenButtons::BARREL, true);
assert!(held.contains(PenButtons::BARREL));
assert!(!held.contains(PenButtons::SECONDARY_BARREL));
assert!(!held.is_empty());
assert!(held.without(PenButtons::BARREL).is_empty());
assert_eq!(held.without(PenButtons::ERASER), held);
}
#[test]
fn caps_only_raise_never_lower() {
let mut caps = BackendCaps::for_platform(PlatformKind::Windows, WindowSystem::Unknown);
assert!(caps.reports_pressure);
PenCaps::NONE.apply_to(&mut caps);
assert!(caps.reports_pressure, "NONE must not clear a set flag");
assert!(!caps.reports_tilt);
PenCaps::FULL_PEN.apply_to(&mut caps);
assert!(caps.reports_tilt && caps.reports_twist && caps.reports_pen_kind);
}
#[test]
fn the_poll_interval_stays_under_the_velocity_stop_gap() {
use teksilo_core::kinetic::velocity::STOP_GAP;
assert!(
PEN_POLL_INTERVAL < STOP_GAP,
"a poll interval at or past the {STOP_GAP:?} stop gap clears the \
velocity history between samples"
);
}
#[test]
fn a_batch_of_one_is_the_polls_own_clock() {
let now = EventTime::from_millis(1234);
assert_eq!(back_date(now, &[Some(99_000)]), vec![now]);
assert_eq!(back_date(now, &[None]), vec![now]);
assert!(back_date(now, &[]).is_empty());
}
#[test]
fn a_batch_keeps_the_devices_own_spacing() {
let now = EventTime::from_millis(1_000);
let times = back_date(now, &[Some(40), Some(44), Some(52), Some(53)]);
assert_eq!(
times,
vec![
EventTime::from_millis(987),
EventTime::from_millis(991),
EventTime::from_millis(999),
now,
]
);
for pair in times.windows(2) {
assert!(pair[1] > pair[0], "{times:?} must strictly increase");
}
}
#[test]
fn a_wrapping_counter_is_still_a_forward_delta() {
let now = EventTime::from_millis(500);
let times = back_date(now, &[Some(u32::MAX - 3), Some(u32::MAX), Some(4)]);
assert_eq!(
times,
vec![
EventTime::from_millis(492),
EventTime::from_millis(495),
now,
],
"MAX-3 → MAX is 3 ms and MAX → 4 is 5 ms across the wrap"
);
}
#[test]
fn a_backwards_stamp_falls_back_to_the_even_step() {
let now = EventTime::from_millis(100);
let times = back_date(now, &[Some(900), Some(100)]);
let step = PEN_POLL_INTERVAL / 2;
assert_eq!(
times,
vec![EventTime::from_duration(now.as_duration() - step), now]
);
}
#[test]
fn a_batch_with_no_device_clock_divides_the_poll_interval() {
let now = EventTime::from_millis(100);
let times = back_date(now, &[None, None, None]);
let step = PEN_POLL_INTERVAL / 3;
assert_eq!(
times,
vec![
EventTime::from_duration(now.as_duration() - step * 2),
EventTime::from_duration(now.as_duration() - step),
now,
]
);
assert!(now.saturating_since(times[0]) < PEN_POLL_INTERVAL);
}
#[test]
fn a_partly_stamped_batch_stays_monotone() {
let now = EventTime::from_millis(1_000);
let times = back_date(now, &[Some(10), None, Some(30), Some(31)]);
assert_eq!(times.len(), 4);
for pair in times.windows(2) {
assert!(pair[0] <= pair[1], "{times:?} must not go backwards");
}
assert_eq!(*times.last().unwrap(), now);
}
#[test]
fn equal_device_stamps_stay_equal() {
let now = EventTime::from_millis(50);
assert_eq!(back_date(now, &[Some(7), Some(7)]), vec![now, now]);
}
#[test]
fn the_batch_is_clamped_to_the_epoch_and_to_now() {
let now = EventTime::from_millis(2);
let times = back_date(now, &[Some(0), Some(500), Some(1_000)]);
assert_eq!(times, vec![EventTime::ZERO, EventTime::ZERO, now]);
assert!(times.iter().all(|&t| t <= now));
}
#[test]
fn the_into_form_agrees_with_the_allocating_one() {
let now = EventTime::from_millis(777);
let stamps = [Some(1), Some(3), None, Some(9)];
let mut buffer = vec![EventTime::from_millis(42); 9];
back_date_into(now, &stamps, &mut buffer);
assert_eq!(buffer, back_date(now, &stamps));
back_date_into(now, &[], &mut buffer);
assert!(buffer.is_empty(), "an empty drain clears the buffer");
}
#[test]
fn the_null_source_reports_no_pen_and_yields_nothing() {
let mut source = null::NullPenSource::new();
let mut out = Vec::new();
source.poll(&mut out);
assert!(out.is_empty(), "the null source must yield no packets");
assert_eq!(source.capabilities(), PenCaps::NONE);
assert_eq!(source.touch_contact(1), None);
let mut state = crate::event_translation::TranslationState::new();
state.set_pen_source(Box::new(null::NullPenSource::new()));
let caps = state.capabilities();
assert!(!caps.reports_pen_kind);
assert!(!caps.reports_tilt);
assert!(!caps.reports_twist);
assert!(
state.poll_pen(EventTime::from_millis(10)).is_empty(),
"no packets in, no samples out"
);
}
}