Skip to main content

teksilo_platform/
pen.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Pen and stylus input, ahead of winit.
5//!
6//! winit 0.30 exposes **no pen API at all**. On Windows its `WM_POINTER` arm
7//! already decodes pen packets and hands the app nothing; on Wayland
8//! `zwp_tablet_v2` is simply never bound. A pen therefore reaches a winit 0.30
9//! client either as a mouse (Windows, via the OS's own promotion) or as
10//! nothing (Wayland). Neither carries pressure, tilt, twist, the eraser end, or
11//! the fact that the tool is *hovering*.
12//!
13//! This module is the shim that fills that gap without waiting for the winit
14//! 0.31 upgrade, behind one seam:
15//!
16//! ```text
17//!   OS                    PenSource::poll        TranslationState
18//!   zwp_tablet_tool_v2 ─┐
19//!   WM_POINTER* ────────┼─▶  Vec<PenPacket>  ─▶  Vec<PointerSample>
20//!   (nothing) ──────────┘                        PointerKind::Pen(tool)
21//! ```
22//!
23//! [`PenSource`] is *pulled*, not pushed: both backends buffer packets off the
24//! event path (a Wayland dispatch thread, a Win32 subclass proc) and the caller
25//! drains them once per event-loop turn through
26//! [`TranslationState::poll_pen`](crate::event_translation::TranslationState::poll_pen).
27//! That keeps the OS callbacks free of Teksilo state and gives the translator
28//! the caller's clock, as the one-clock rule requires.
29//!
30//! # Support matrix
31//!
32//! | | pen at all | tool kind | pressure | tilt | twist | contact patch |
33//! | --- | --- | --- | --- | --- | --- | --- |
34//! | Wayland | [`wayland`] | yes | yes | yes | yes | — |
35//! | Windows | [`windows`] | yes | yes | yes | yes | yes (touch) |
36//! | X11 | [`null`] | no | no | no | no | no |
37//! | macOS | [`null`] | no | no | no | no | no |
38//!
39//! X11 and macOS report their absence through [`BackendCaps`] rather than
40//! pretending: `reports_pen_kind` and friends stay `false`, and a consumer that
41//! must know asks instead of guessing from `cfg!(target_os = ...)`.
42//!
43//! # A drained batch has its own timeline
44//!
45//! A poll drains everything buffered since the last one, and those packets did
46//! **not** all happen at the instant the poll ran. Each carries the device's
47//! own millisecond counter in [`PenPacket::device_time_ms`]; [`back_date`]
48//! turns that batch into one [`EventTime`] per packet — newest at the poll's
49//! `now`, earlier ones at the device's own deltas before it — so a stroke's
50//! velocity, its smoothing and its per-sample time offsets are computed from
51//! when the digitizer says the samples happened rather than from when the
52//! event loop got round to asking. A batch of one is stamped `now` exactly.
53//!
54//! The device counters themselves never escape the platform layer: their epoch
55//! is unknown, so only their *differences* are ever read.
56//!
57//! # Proximity is a first-class state
58//!
59//! A pen in proximity with no contact is a **hovering pointer**: it moves,
60//! drives hover visuals, tooltips and the cursor exactly as a mouse does, and
61//! it does so with `down: false`. That is why [`PointerKind::hovers`] is true
62//! for `Pen` and false for `Touch`. The proximity → contact → proximity-out
63//! machine lives in the translator (`event_translation.rs`); a source's job is
64//! only to report `in_proximity` and `down` truthfully per packet.
65//!
66//! # Buttons, normatively
67//!
68//! - Pen **contact** is [`PointerButton::Primary`](teksilo_core::event::PointerButton::Primary).
69//! - The **barrel** button is
70//!   [`Secondary`](teksilo_core::event::PointerButton::Secondary), matching
71//!   W3C Pointer Events (pen barrel → `button` 2, `buttons` bit 2).
72//! - A second barrel button, where the hardware has one, is
73//!   [`Middle`](teksilo_core::event::PointerButton::Middle).
74//! - The **eraser is a tool kind** ([`PenKind::Eraser`]), never a button. A
75//!   digitizer that reports the eraser as a flag has that flag folded into the
76//!   tool before it leaves the source; [`PenButtons::ERASER`] exists only so a
77//!   backend can carry the raw bit faithfully.
78//!
79//! # This module has an expiry date
80//!
81//! winit 0.31 supersedes both shims with its own `TabletTool*` events and
82//! `PointerSource::Tablet`. At that upgrade [`wayland`] and [`windows`] are
83//! **deleted**, `create_pen_source` returns [`null::NullPenSource`] everywhere,
84//! and the translator reads the pen off winit like every other device. Nothing
85//! above this seam changes.
86//!
87//! Reference: `docs/touch-and-pen.md`, "Pen and stylus".
88//!
89//! [`BackendCaps`]: crate::pointer_backend::BackendCaps
90//! [`PointerKind::hovers`]: teksilo_tokens::PointerKind::hovers
91
92// The support matrix above links `wayland`, which is `#[cfg]`-ed away off Unix
93// — so on Windows and macOS that link cannot resolve and a local
94// `RUSTDOCFLAGS="-D warnings"` run fails on a link that is correct by
95// construction. The docs that ship are built on Linux (both `ci.yml`'s doc gate
96// and `docs.yml` are `runs-on: ubuntu-latest`), where the module exists, the
97// link resolves, and every link in this module is still checked under
98// `-D warnings`. Scoped to the hosts that cannot have the module, so a
99// genuinely broken link here still fails the gate on the host that enforces it.
100#![cfg_attr(
101    not(all(unix, not(target_os = "macos"))),
102    allow(rustdoc::broken_intra_doc_links)
103)]
104
105pub mod null;
106#[cfg(all(unix, not(target_os = "macos")))]
107pub mod wayland;
108// Compiled on **every** target on purpose: the `POINTER_PEN_INFO` /
109// `POINTER_TOUCH_INFO` decoder inside is pure byte-slice arithmetic, and it is
110// tested from recorded layouts on hosts that have no Windows. Only the
111// subclass shim that feeds it is `#[cfg(target_os = "windows")]`.
112pub mod windows;
113
114use teksilo_canvas::{Point, Size};
115use teksilo_core::pointer::EventTime;
116use teksilo_core::raw_handle::ParentHandle;
117use teksilo_tokens::PenKind;
118
119use crate::pointer_backend::BackendCaps;
120
121// ---------------------------------------------------------------------------
122// Buttons
123// ---------------------------------------------------------------------------
124
125/// The stylus buttons a packet reports as held.
126///
127/// A bitset rather than a `Vec` because the set is small, fixed and copied on
128/// every packet. See the module docs for the normative mapping onto
129/// [`PointerButton`](teksilo_core::event::PointerButton) — in particular, the
130/// tip is **not** in here (it is `down`), and [`ERASER`](Self::ERASER) is a
131/// carried flag rather than a button Teksilo dispatches.
132#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Default)]
133pub struct PenButtons(u8);
134
135impl PenButtons {
136    /// No stylus button held.
137    pub const NONE: Self = Self(0);
138    /// The barrel button — the one every stylus has. Dispatched as
139    /// `PointerButton::Secondary`.
140    pub const BARREL: Self = Self(1 << 0);
141    /// A second barrel button, where the hardware has one (Wayland's
142    /// `BTN_STYLUS2`). Dispatched as `PointerButton::Middle`.
143    pub const SECONDARY_BARREL: Self = Self(1 << 1);
144    /// The digitizer's "eraser" flag. Carried for fidelity and folded into
145    /// [`PenKind::Eraser`] by the source; never dispatched as a button.
146    pub const ERASER: Self = Self(1 << 2);
147
148    /// The raw bits, for a backend that must store the set compactly.
149    pub const fn bits(self) -> u8 {
150        self.0
151    }
152
153    /// Whether every button in `other` is held.
154    pub const fn contains(self, other: Self) -> bool {
155        self.0 & other.0 == other.0
156    }
157
158    /// Nothing held.
159    pub const fn is_empty(self) -> bool {
160        self.0 == 0
161    }
162
163    /// The union of two sets.
164    pub const fn union(self, other: Self) -> Self {
165        Self(self.0 | other.0)
166    }
167
168    /// `other` removed from this set.
169    pub const fn without(self, other: Self) -> Self {
170        Self(self.0 & !other.0)
171    }
172
173    /// `other` added to or removed from this set.
174    pub const fn with(self, other: Self, held: bool) -> Self {
175        if held {
176            self.union(other)
177        } else {
178            self.without(other)
179        }
180    }
181}
182
183// ---------------------------------------------------------------------------
184// Capabilities
185// ---------------------------------------------------------------------------
186
187/// What a [`PenSource`] can actually report.
188///
189/// Folded into the window's [`BackendCaps`] by
190/// [`apply_to`](Self::apply_to), so a consumer keeps asking one question
191/// ("does this window report tilt?") whether the answer comes from winit or
192/// from a shim.
193#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Default)]
194pub struct PenCaps {
195    /// The source distinguishes a pen tip from an eraser (and the other
196    /// [`PenKind`]s).
197    pub tool_kind: bool,
198    /// Tip pressure is reported.
199    pub pressure: bool,
200    /// Tilt is reported.
201    pub tilt: bool,
202    /// Barrel rotation is reported.
203    pub twist: bool,
204    /// The source reports a touch contact patch (Windows `rcContact`).
205    pub touch_contact: bool,
206}
207
208impl PenCaps {
209    /// A source that reports nothing — the honest answer on X11 and macOS.
210    pub const NONE: Self = Self {
211        tool_kind: false,
212        pressure: false,
213        tilt: false,
214        twist: false,
215        touch_contact: false,
216    };
217
218    /// Everything a full digitizer reports, minus the contact patch.
219    pub const FULL_PEN: Self = Self {
220        tool_kind: true,
221        pressure: true,
222        tilt: true,
223        twist: true,
224        touch_contact: false,
225    };
226
227    /// Raise the matching flags on a window's platform capabilities.
228    ///
229    /// Only ever *raises* them: a shim adds a capability winit lacks, it never
230    /// takes one away.
231    pub fn apply_to(self, caps: &mut BackendCaps) {
232        caps.reports_pen_kind |= self.tool_kind;
233        caps.reports_pressure |= self.pressure;
234        caps.reports_tilt |= self.tilt;
235        caps.reports_twist |= self.twist;
236    }
237}
238
239// ---------------------------------------------------------------------------
240// Packets
241// ---------------------------------------------------------------------------
242
243/// One digitizer packet, normalised.
244///
245/// A packet is a *level*, not an edge: it describes the tool's complete state
246/// at one instant, and the translator derives the transitions (enter, down, up,
247/// leave, button changes) by comparing consecutive packets. That is the shape
248/// both backends produce naturally — Wayland accumulates axes and commits them
249/// on `frame`, Win32 fills one `POINTER_PEN_INFO` per message — and it means a
250/// dropped packet costs a sample, never a stuck button.
251#[derive(Copy, Clone, PartialEq, Debug)]
252pub struct PenPacket {
253    /// The tool the digitizer says is in use. The eraser end is a tool, not a
254    /// button.
255    pub tool: PenKind,
256    /// Window-logical position, already divided by the window's scale factor.
257    pub position: Point,
258    /// Normalised tip pressure, `0.0..=1.0`. `0.0` while hovering.
259    pub pressure: f32,
260    /// `(tilt_x, tilt_y)` in degrees, each `-90.0..=90.0`, or `None` when the
261    /// tool has no tilt axis.
262    pub tilt: Option<(f32, f32)>,
263    /// Barrel rotation in degrees, `0.0..=359.0`, or `None` when the tool has
264    /// no rotation axis.
265    pub twist: Option<f32>,
266    /// The stylus buttons held.
267    pub buttons: PenButtons,
268    /// Whether the tool is within the digitizer's detection range. A packet
269    /// with `in_proximity: false` ends the hover session.
270    pub in_proximity: bool,
271    /// Whether the tip is touching the surface.
272    pub down: bool,
273    /// The device's own millisecond counter, on whatever clock the OS uses,
274    /// or `None` from a source that has no clock at all.
275    ///
276    /// Deliberately **not** an [`EventTime`]: the epoch is unknown. Wayland's
277    /// `frame` time is the compositor's, Win32's `dwTime` is
278    /// `GetTickCount`'s, and neither has a known offset from the tree's epoch,
279    /// so as an absolute this number is a lie dressed as precision.
280    ///
281    /// **Within one drained batch it is exact as a relative**, and that is the
282    /// only way it is ever read: [`back_date`] places a batch on the tree's
283    /// timeline by anchoring the newest packet at the poll's `now` and walking
284    /// backwards through these deltas. That is what stops twenty packets
285    /// spanning one drain from all claiming a single instant, which is what
286    /// they did while this field did not exist.
287    ///
288    /// A `u32` because both platforms report one, wrap included: `back_date`
289    /// reads the deltas with `wrapping_sub`, so a counter rolling over inside
290    /// a batch costs nothing.
291    pub device_time_ms: Option<u32>,
292}
293
294impl PenPacket {
295    /// A hovering packet: in proximity, tip up, no pressure, no buttons.
296    pub fn hovering(tool: PenKind, position: Point) -> Self {
297        Self {
298            tool,
299            position,
300            pressure: 0.0,
301            tilt: None,
302            twist: None,
303            buttons: PenButtons::NONE,
304            in_proximity: true,
305            down: false,
306            device_time_ms: None,
307        }
308    }
309
310    /// The packet a tool leaving the digitizer's range produces. Position is
311    /// the last known one — the tool did not move, it stopped being seen.
312    pub fn out_of_proximity(tool: PenKind, position: Point) -> Self {
313        Self {
314            in_proximity: false,
315            ..Self::hovering(tool, position)
316        }
317    }
318
319    /// This packet with the tip in contact at `pressure`.
320    pub fn down_at(mut self, pressure: f32) -> Self {
321        self.down = true;
322        self.pressure = pressure.clamp(0.0, 1.0);
323        self
324    }
325
326    /// This packet stamped with the device's own millisecond counter.
327    ///
328    /// The value a shim reads off the digitizer — Wayland's `frame` time,
329    /// Win32's `dwTime`. See [`device_time_ms`](Self::device_time_ms) for why
330    /// it stays a raw counter rather than becoming an [`EventTime`].
331    pub const fn at_device_ms(mut self, ms: u32) -> Self {
332        self.device_time_ms = Some(ms);
333        self
334    }
335}
336
337// ---------------------------------------------------------------------------
338// Placing a drained batch on the tree's timeline
339// ---------------------------------------------------------------------------
340
341/// A device delta longer than this is read as the counter having run
342/// *backwards* — an out-of-order stamp, or a counter reset — rather than as a
343/// real pause inside one drained batch.
344///
345/// The number only has to separate a plausible forward delta from the ~4.29
346/// billion a `wrapping_sub` yields when `cur < prev`, so it is set generously.
347/// A drained batch spans one [`PEN_POLL_INTERVAL`] in the steady state; ten
348/// seconds inside one drain is already nonsense.
349pub const MAX_DEVICE_GAP_MS: u64 = 10_000;
350
351/// Place a drained batch of packets on the tree's timeline, newest at `now`.
352///
353/// `device_ms` is each packet's [`PenPacket::device_time_ms`], **oldest
354/// first**, exactly as [`PenSource::poll`] appends them. The result is the
355/// same length and the same order.
356///
357/// The rule:
358///
359/// - The newest packet is `now`. It is the one the poll's clock actually
360///   describes, and a batch of one is therefore stamped `now` exactly — which
361///   is what every packet was stamped before this function existed.
362/// - Each earlier packet sits at the device's own delta before the one after
363///   it, read with `wrapping_sub` so a `u32` counter rolling over inside the
364///   batch costs nothing. A delta past [`MAX_DEVICE_GAP_MS`] is the counter
365///   running backwards and falls through to the step below.
366/// - Where either neighbour reports no device clock, the step is one
367///   [`PEN_POLL_INTERVAL`] divided evenly across the batch, so the whole batch
368///   still fits inside the window it was buffered in.
369/// - Times saturate at [`EventTime::ZERO`] and never exceed `now`.
370///
371/// Monotone non-decreasing by construction, and **strictly** increasing
372/// whenever the device's own stamps strictly increase. Two packets the
373/// digitizer stamped in the same millisecond stay equal: that is what the
374/// device said, and inventing a gap would be inventing precision.
375///
376/// Pure, so the rule is testable with no digitizer:
377///
378/// ```
379/// use teksilo_platform::pen::back_date;
380/// use teksilo_core::pointer::EventTime;
381///
382/// let now = EventTime::from_millis(1_000);
383/// assert_eq!(
384///     back_date(now, &[Some(40), Some(44), Some(52)]),
385///     vec![
386///         EventTime::from_millis(988),
387///         EventTime::from_millis(992),
388///         now,
389///     ],
390/// );
391/// // A batch of one is `now`.
392/// assert_eq!(back_date(now, &[Some(7)]), vec![now]);
393/// ```
394pub fn back_date(now: EventTime, device_ms: &[Option<u32>]) -> Vec<EventTime> {
395    let mut out = Vec::with_capacity(device_ms.len());
396    back_date_into(now, device_ms, &mut out);
397    out
398}
399
400/// [`back_date`] into a caller-owned buffer, so the pen pump allocates nothing
401/// per drain.
402///
403/// `out` is cleared first and left the same length as `device_ms`.
404pub fn back_date_into(now: EventTime, device_ms: &[Option<u32>], out: &mut Vec<EventTime>) {
405    out.clear();
406    let count = device_ms.len();
407    if count == 0 {
408        return;
409    }
410    // One poll interval divided evenly is the step wherever the device says
411    // nothing — `count`, not `count - 1`, so even the oldest packet stays
412    // strictly inside the interval it was buffered in.
413    let step = PEN_POLL_INTERVAL / count as u32;
414
415    // Pass one: a relative timeline, `EventTime` standing in for a `Duration`
416    // since the batch's own start so no second buffer is needed.
417    let mut elapsed = std::time::Duration::ZERO;
418    out.push(EventTime::from_duration(elapsed));
419    for index in 1..count {
420        let advance = match (device_ms[index - 1], device_ms[index]) {
421            (Some(previous), Some(current)) => {
422                let delta = u64::from(current.wrapping_sub(previous));
423                if delta <= MAX_DEVICE_GAP_MS {
424                    std::time::Duration::from_millis(delta)
425                } else {
426                    step
427                }
428            }
429            _ => step,
430        };
431        elapsed = elapsed.saturating_add(advance);
432        out.push(EventTime::from_duration(elapsed));
433    }
434
435    // Pass two: slide the timeline so its newest entry lands on `now`.
436    let span = elapsed;
437    for slot in out.iter_mut() {
438        // `span >= slot` always: the timeline is non-decreasing and `span` is
439        // its last entry.
440        let before_now = span - slot.as_duration();
441        *slot = EventTime::from_duration(now.as_duration().saturating_sub(before_now));
442    }
443}
444
445// ---------------------------------------------------------------------------
446// The source
447// ---------------------------------------------------------------------------
448
449/// How a drained pen batch reaches the tree.
450///
451/// A digitizer runs at 200-360 Hz and a window's message rate does not, so one
452/// `poll_pen` drain routinely holds several packets. Both answers to "what does
453/// the tree see?" are defensible and the trade is real, which is why this is a
454/// knob rather than a decision baked in:
455///
456/// - [`PerPacket`](Self::PerPacket) spends a whole tree dispatch — hit test,
457///   arbitration turn, handler walk — on every packet. Nothing is lost and
458///   nothing is coalesced; it is what the pen path has always done.
459/// - [`Coalesce`](Self::Coalesce) spends one dispatch per drain and hands the
460///   intermediate positions over as
461///   [`PointerSample::coalesced`](teksilo_core::PointerSample::coalesced),
462///   each keeping its own time and its own axes. A surface that reads
463///   `EventContext::coalesced` sees exactly the same positions; one that does
464///   not sees fewer moves.
465///
466/// **Transitions are never folded.** Down, Up, a button change and proximity
467/// enter/leave each keep their own sample under either mode, so no recognizer
468/// sees a different *sequence* — only the number of `PointerMove`s between two
469/// transitions changes.
470///
471/// Four of those five the fold can *see* for itself, because all it tests is
472/// the phase and the button: Down and Up are not `Move`, a button change reports
473/// a button, and proximity **leave** is a
474/// [`PointerPhase::Cancel`](teksilo_core::PointerPhase::Cancel), so it breaks a
475/// run for free. The fifth it cannot, and that one is handled by name rather
476/// than by luck: proximity **enter** has no phase of its own — it is carried as
477/// a move with nothing held — so `poll_pen` pins it, because folding it away
478/// would not cost a `PointerMove` but a `PointerEnter`: the tree derives the
479/// hover owner, the cursor and the tooltip dwell from a sample's position and
480/// never from its batched list, and a tool that came into range over one widget
481/// and hovered onto another inside one drain would otherwise never enter the
482/// first.
483#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
484#[non_exhaustive]
485pub enum PenBatching {
486    /// One [`PointerSample`](teksilo_core::PointerSample) per packet. The
487    /// default, and what the pen path did before this existed.
488    #[default]
489    PerPacket,
490    /// One sample per *transition*; the pure-motion packets between two
491    /// transitions ride in
492    /// [`PointerSample::coalesced`](teksilo_core::PointerSample::coalesced).
493    Coalesce,
494}
495
496/// A buffered supply of [`PenPacket`]s.
497///
498/// One instance per window. Implementations are expected to be cheap to poll
499/// and to return promptly with nothing when the user is not holding a stylus,
500/// because the caller polls once per event-loop turn.
501///
502/// `Debug` is a supertrait so that a `TranslationState` holding one stays
503/// `Debug` — the whole per-window translator is dumped in traces and in the
504/// inspector.
505pub trait PenSource: std::fmt::Debug {
506    /// Append every packet buffered since the last poll to `out`, oldest
507    /// first, and clear the buffer.
508    ///
509    /// Appending rather than returning a `Vec` lets the caller reuse one
510    /// scratch buffer for the life of the window.
511    ///
512    /// **Oldest first is load-bearing**, not a convenience: the caller reads
513    /// the run as one timeline and back-dates it from the last entry (see
514    /// [`back_date`]). A source whose OS hands it the newest entry first —
515    /// Win32's `GetPointerPenInfoHistory` does exactly that — reverses before
516    /// appending.
517    ///
518    /// Each packet should carry the device's own stamp in
519    /// [`PenPacket::device_time_ms`] where the platform reports one. A source
520    /// that leaves it `None` is not wrong; its batch is simply spread evenly
521    /// over one [`PEN_POLL_INTERVAL`] instead of by the device's deltas.
522    fn poll(&mut self, out: &mut Vec<PenPacket>);
523
524    /// What this source reports. Defaults to [`PenCaps::NONE`], which is the
525    /// correct answer for a source that yields no packets.
526    fn capabilities(&self) -> PenCaps {
527        PenCaps::NONE
528    }
529
530    /// Whether this source fills its buffer from a thread of its own, so a
531    /// packet may still be on its way after the event that woke the loop.
532    ///
533    /// The event loop needs to know, because it decides whether draining once
534    /// per turn is enough. A shim that reads on the **winit thread** — the
535    /// Windows `WM_POINTER` subclass — has already filled its buffer by the
536    /// time the turn that carried the message reaches the pump, so one drain
537    /// per turn sees everything. A shim that reads on its **own** thread — the
538    /// Wayland tablet listener — has not: the compositor event that woke the
539    /// loop and the packet the shim will make of it are up to one of that
540    /// listener's dispatch intervals apart, so the loop has to look again. The
541    /// catch-up look is armed at [`PEN_POLL_INTERVAL`], which is the interval
542    /// that applies once a tool has been announced; a session with none is on
543    /// a slower tier, and cannot deliver a packet at all until `tool_added`
544    /// has moved it to the fast one. Such a source answers `false` until then:
545    /// a look armed at every wake would find nothing, and cost the loop a
546    /// second turn per wake on every window of a session with no tablet.
547    ///
548    /// Defaults to `false`, which is the answer for a source with no thread.
549    fn polls_off_thread(&self) -> bool {
550        false
551    }
552
553    /// The contact patch most recently reported for an OS touch contact id, in
554    /// logical pixels.
555    ///
556    /// This is not pen data, and it is here for one reason: on Windows the
557    /// `WM_POINTER` family carries `POINTER_TOUCH_INFO::rcContact`, which
558    /// `WM_TOUCH` — the path winit 0.30 takes — does not. The same subclass
559    /// that reads pen packets can read it, and the palm heuristic and the
560    /// finger-avoiding overlay placement both want it. Every other source
561    /// returns `None`.
562    ///
563    /// Keyed by the raw OS contact id (Windows' `pointerId`), because that is
564    /// what winit puts in `Touch::id` on the `WM_POINTER` path.
565    fn touch_contact(&self, _os_contact_id: u64) -> Option<Size> {
566        None
567    }
568}
569
570/// The pen source for a window, or [`null::NullPenSource`] where the platform
571/// has none.
572///
573/// Never fails: a window whose tablet manager is missing, whose subclass would
574/// not install, or which runs on a platform with no pen path at all, gets the
575/// null source and reports its absence through [`PenCaps::NONE`].
576pub fn create_pen_source(parent: &ParentHandle) -> Box<dyn PenSource> {
577    #[cfg(all(unix, not(target_os = "macos")))]
578    {
579        if let Some(source) = wayland::WaylandPenSource::attach(parent) {
580            return Box::new(source);
581        }
582    }
583    #[cfg(target_os = "windows")]
584    {
585        if let Some(source) = windows::WindowsPenSource::attach(parent) {
586            return Box::new(source);
587        }
588    }
589    let _ = parent;
590    Box::new(null::NullPenSource::new())
591}
592
593/// How often a pen shim that reads on its own thread looks at the digitizer.
594///
595/// The Wayland shim's own sleep interval, published so the event loop can pace
596/// its catch-up look to it rather than guessing. The bound that makes a value
597/// wrong is the velocity tracker's
598/// [`STOP_GAP`](teksilo_core::kinetic::velocity::STOP_GAP): a gap that long
599/// between samples is read as the stroke having paused and clears the history,
600/// so a shim looking that rarely would turn one continuous stroke into a
601/// sequence of standing starts. That relation is asserted in this module's
602/// tests rather than left to this sentence.
603pub const PEN_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(4);
604
605#[cfg(test)]
606mod tests {
607    use super::*;
608    use crate::pointer_backend::{PlatformKind, PointerBackend};
609    use crate::window_system::WindowSystem;
610
611    #[test]
612    fn buttons_are_a_set() {
613        let held = PenButtons::NONE.with(PenButtons::BARREL, true);
614        assert!(held.contains(PenButtons::BARREL));
615        assert!(!held.contains(PenButtons::SECONDARY_BARREL));
616        assert!(!held.is_empty());
617        assert!(held.without(PenButtons::BARREL).is_empty());
618        // Removing something that was never held is not an error.
619        assert_eq!(held.without(PenButtons::ERASER), held);
620    }
621
622    #[test]
623    fn caps_only_raise_never_lower() {
624        // macOS reports no pressure; a source that does must not be able to
625        // *unset* a capability winit already claimed either.
626        let mut caps = BackendCaps::for_platform(PlatformKind::Windows, WindowSystem::Unknown);
627        assert!(caps.reports_pressure);
628        PenCaps::NONE.apply_to(&mut caps);
629        assert!(caps.reports_pressure, "NONE must not clear a set flag");
630        assert!(!caps.reports_tilt);
631        PenCaps::FULL_PEN.apply_to(&mut caps);
632        assert!(caps.reports_tilt && caps.reports_twist && caps.reports_pen_kind);
633    }
634
635    /// The one property that makes [`PEN_POLL_INTERVAL`] right or wrong.
636    ///
637    /// A shim reading on its own thread hands the translator samples no fresher
638    /// than one interval. If that interval reached the velocity tracker's
639    /// `STOP_GAP`, every sample would look to the tracker like the resumption
640    /// of a stroke that had stopped, and a pen fling would be estimated from
641    /// standing starts. Well under it is the requirement; the exact figure is
642    /// the Wayland shim's sleep.
643    #[test]
644    fn the_poll_interval_stays_under_the_velocity_stop_gap() {
645        use teksilo_core::kinetic::velocity::STOP_GAP;
646        assert!(
647            PEN_POLL_INTERVAL < STOP_GAP,
648            "a poll interval at or past the {STOP_GAP:?} stop gap clears the \
649             velocity history between samples"
650        );
651    }
652
653    // -----------------------------------------------------------------------
654    // back_date
655    // -----------------------------------------------------------------------
656
657    /// The invariant the old behaviour got right and a back-dating design must
658    /// not break: one packet is the poll's clock, exactly.
659    #[test]
660    fn a_batch_of_one_is_the_polls_own_clock() {
661        let now = EventTime::from_millis(1234);
662        assert_eq!(back_date(now, &[Some(99_000)]), vec![now]);
663        assert_eq!(back_date(now, &[None]), vec![now]);
664        assert!(back_date(now, &[]).is_empty());
665    }
666
667    /// The defect this exists to kill: every packet in a drain claiming one
668    /// instant. With the device's own stamps the batch is strictly increasing
669    /// and carries the digitizer's spacing, not the poll's.
670    #[test]
671    fn a_batch_keeps_the_devices_own_spacing() {
672        let now = EventTime::from_millis(1_000);
673        let times = back_date(now, &[Some(40), Some(44), Some(52), Some(53)]);
674        assert_eq!(
675            times,
676            vec![
677                EventTime::from_millis(987),
678                EventTime::from_millis(991),
679                EventTime::from_millis(999),
680                now,
681            ]
682        );
683        for pair in times.windows(2) {
684            assert!(pair[1] > pair[0], "{times:?} must strictly increase");
685        }
686    }
687
688    /// A device whose counter rolls over mid-batch is still a forward stroke.
689    /// `wrapping_sub` is what makes the `u32` safe to subtract.
690    #[test]
691    fn a_wrapping_counter_is_still_a_forward_delta() {
692        let now = EventTime::from_millis(500);
693        let times = back_date(now, &[Some(u32::MAX - 3), Some(u32::MAX), Some(4)]);
694        assert_eq!(
695            times,
696            vec![
697                EventTime::from_millis(492),
698                EventTime::from_millis(495),
699                now,
700            ],
701            "MAX-3 → MAX is 3 ms and MAX → 4 is 5 ms across the wrap"
702        );
703    }
704
705    /// A stamp that goes *backwards* is not a 49-day pause. It falls through
706    /// to the even step rather than back-dating the batch into the last
707    /// century.
708    #[test]
709    fn a_backwards_stamp_falls_back_to_the_even_step() {
710        let now = EventTime::from_millis(100);
711        let times = back_date(now, &[Some(900), Some(100)]);
712        let step = PEN_POLL_INTERVAL / 2;
713        assert_eq!(
714            times,
715            vec![EventTime::from_duration(now.as_duration() - step), now]
716        );
717    }
718
719    /// No device clock at all: the batch is still spread, because the packets
720    /// are separate digitizer frames and calling them simultaneous is the
721    /// original bug in miniature.
722    #[test]
723    fn a_batch_with_no_device_clock_divides_the_poll_interval() {
724        let now = EventTime::from_millis(100);
725        let times = back_date(now, &[None, None, None]);
726        let step = PEN_POLL_INTERVAL / 3;
727        assert_eq!(
728            times,
729            vec![
730                EventTime::from_duration(now.as_duration() - step * 2),
731                EventTime::from_duration(now.as_duration() - step),
732                now,
733            ]
734        );
735        // The whole batch stays inside the window it was buffered in.
736        assert!(now.saturating_since(times[0]) < PEN_POLL_INTERVAL);
737    }
738
739    /// A source that stamps some packets and not others is pathological, not
740    /// impossible. It must still come out ordered.
741    #[test]
742    fn a_partly_stamped_batch_stays_monotone() {
743        let now = EventTime::from_millis(1_000);
744        let times = back_date(now, &[Some(10), None, Some(30), Some(31)]);
745        assert_eq!(times.len(), 4);
746        for pair in times.windows(2) {
747            assert!(pair[0] <= pair[1], "{times:?} must not go backwards");
748        }
749        assert_eq!(*times.last().unwrap(), now);
750    }
751
752    /// Equal device stamps stay equal. Two packets the digitizer stamped in
753    /// the same millisecond really were in the same millisecond, and
754    /// manufacturing a gap would be manufacturing precision.
755    #[test]
756    fn equal_device_stamps_stay_equal() {
757        let now = EventTime::from_millis(50);
758        assert_eq!(back_date(now, &[Some(7), Some(7)]), vec![now, now]);
759    }
760
761    /// Nothing is ever placed in the future, and nothing underflows the epoch.
762    #[test]
763    fn the_batch_is_clamped_to_the_epoch_and_to_now() {
764        let now = EventTime::from_millis(2);
765        let times = back_date(now, &[Some(0), Some(500), Some(1_000)]);
766        assert_eq!(times, vec![EventTime::ZERO, EventTime::ZERO, now]);
767        assert!(times.iter().all(|&t| t <= now));
768    }
769
770    /// `back_date_into` is the allocation-free twin, and answers identically.
771    #[test]
772    fn the_into_form_agrees_with_the_allocating_one() {
773        let now = EventTime::from_millis(777);
774        let stamps = [Some(1), Some(3), None, Some(9)];
775        let mut buffer = vec![EventTime::from_millis(42); 9];
776        back_date_into(now, &stamps, &mut buffer);
777        assert_eq!(buffer, back_date(now, &stamps));
778        back_date_into(now, &[], &mut buffer);
779        assert!(buffer.is_empty(), "an empty drain clears the buffer");
780    }
781
782    #[test]
783    fn the_null_source_reports_no_pen_and_yields_nothing() {
784        let mut source = null::NullPenSource::new();
785        let mut out = Vec::new();
786        source.poll(&mut out);
787        assert!(out.is_empty(), "the null source must yield no packets");
788        assert_eq!(source.capabilities(), PenCaps::NONE);
789        assert_eq!(source.touch_contact(1), None);
790
791        // And a translator carrying it advertises no pen either.
792        let mut state = crate::event_translation::TranslationState::new();
793        state.set_pen_source(Box::new(null::NullPenSource::new()));
794        let caps = state.capabilities();
795        assert!(!caps.reports_pen_kind);
796        assert!(!caps.reports_tilt);
797        assert!(!caps.reports_twist);
798        assert!(
799            state.poll_pen(EventTime::from_millis(10)).is_empty(),
800            "no packets in, no samples out"
801        );
802    }
803}