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.
531    ///
532    /// The event loop needs to know, because it decides whether draining once
533    /// per turn is enough. A shim that reads on the **winit thread** — the
534    /// Windows `WM_POINTER` subclass — has already filled its buffer by the
535    /// time the turn that carried the message reaches the pump, so one drain
536    /// per turn sees everything. A shim that reads on its **own** thread — the
537    /// Wayland tablet listener — has not: the compositor event that woke the
538    /// loop and the packet the shim will make of it are up to one of that
539    /// listener's dispatch intervals apart, so the loop has to look again. The
540    /// catch-up look is armed at [`PEN_POLL_INTERVAL`], which is the interval
541    /// that applies once a tool has been announced; a session with none is on
542    /// a slower tier, and cannot deliver a packet at all until `tool_added`
543    /// has moved it to the fast one.
544    ///
545    /// Defaults to `false`, which is the answer for a source with no thread.
546    fn polls_off_thread(&self) -> bool {
547        false
548    }
549
550    /// The contact patch most recently reported for an OS touch contact id, in
551    /// logical pixels.
552    ///
553    /// This is not pen data, and it is here for one reason: on Windows the
554    /// `WM_POINTER` family carries `POINTER_TOUCH_INFO::rcContact`, which
555    /// `WM_TOUCH` — the path winit 0.30 takes — does not. The same subclass
556    /// that reads pen packets can read it, and the palm heuristic and the
557    /// finger-avoiding overlay placement both want it. Every other source
558    /// returns `None`.
559    ///
560    /// Keyed by the raw OS contact id (Windows' `pointerId`), because that is
561    /// what winit puts in `Touch::id` on the `WM_POINTER` path.
562    fn touch_contact(&self, _os_contact_id: u64) -> Option<Size> {
563        None
564    }
565}
566
567/// The pen source for a window, or [`null::NullPenSource`] where the platform
568/// has none.
569///
570/// Never fails: a window whose tablet manager is missing, whose subclass would
571/// not install, or which runs on a platform with no pen path at all, gets the
572/// null source and reports its absence through [`PenCaps::NONE`].
573pub fn create_pen_source(parent: &ParentHandle) -> Box<dyn PenSource> {
574    #[cfg(all(unix, not(target_os = "macos")))]
575    {
576        if let Some(source) = wayland::WaylandPenSource::attach(parent) {
577            return Box::new(source);
578        }
579    }
580    #[cfg(target_os = "windows")]
581    {
582        if let Some(source) = windows::WindowsPenSource::attach(parent) {
583            return Box::new(source);
584        }
585    }
586    let _ = parent;
587    Box::new(null::NullPenSource::new())
588}
589
590/// How often a pen shim that reads on its own thread looks at the digitizer.
591///
592/// The Wayland shim's own sleep interval, published so the event loop can pace
593/// its catch-up look to it rather than guessing. The bound that makes a value
594/// wrong is the velocity tracker's
595/// [`STOP_GAP`](teksilo_core::kinetic::velocity::STOP_GAP): a gap that long
596/// between samples is read as the stroke having paused and clears the history,
597/// so a shim looking that rarely would turn one continuous stroke into a
598/// sequence of standing starts. That relation is asserted in this module's
599/// tests rather than left to this sentence.
600pub const PEN_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(4);
601
602#[cfg(test)]
603mod tests {
604    use super::*;
605    use crate::pointer_backend::{PlatformKind, PointerBackend};
606    use crate::window_system::WindowSystem;
607
608    #[test]
609    fn buttons_are_a_set() {
610        let held = PenButtons::NONE.with(PenButtons::BARREL, true);
611        assert!(held.contains(PenButtons::BARREL));
612        assert!(!held.contains(PenButtons::SECONDARY_BARREL));
613        assert!(!held.is_empty());
614        assert!(held.without(PenButtons::BARREL).is_empty());
615        // Removing something that was never held is not an error.
616        assert_eq!(held.without(PenButtons::ERASER), held);
617    }
618
619    #[test]
620    fn caps_only_raise_never_lower() {
621        // macOS reports no pressure; a source that does must not be able to
622        // *unset* a capability winit already claimed either.
623        let mut caps = BackendCaps::for_platform(PlatformKind::Windows, WindowSystem::Unknown);
624        assert!(caps.reports_pressure);
625        PenCaps::NONE.apply_to(&mut caps);
626        assert!(caps.reports_pressure, "NONE must not clear a set flag");
627        assert!(!caps.reports_tilt);
628        PenCaps::FULL_PEN.apply_to(&mut caps);
629        assert!(caps.reports_tilt && caps.reports_twist && caps.reports_pen_kind);
630    }
631
632    /// The one property that makes [`PEN_POLL_INTERVAL`] right or wrong.
633    ///
634    /// A shim reading on its own thread hands the translator samples no fresher
635    /// than one interval. If that interval reached the velocity tracker's
636    /// `STOP_GAP`, every sample would look to the tracker like the resumption
637    /// of a stroke that had stopped, and a pen fling would be estimated from
638    /// standing starts. Well under it is the requirement; the exact figure is
639    /// the Wayland shim's sleep.
640    #[test]
641    fn the_poll_interval_stays_under_the_velocity_stop_gap() {
642        use teksilo_core::kinetic::velocity::STOP_GAP;
643        assert!(
644            PEN_POLL_INTERVAL < STOP_GAP,
645            "a poll interval at or past the {STOP_GAP:?} stop gap clears the \
646             velocity history between samples"
647        );
648    }
649
650    // -----------------------------------------------------------------------
651    // back_date
652    // -----------------------------------------------------------------------
653
654    /// The invariant the old behaviour got right and a back-dating design must
655    /// not break: one packet is the poll's clock, exactly.
656    #[test]
657    fn a_batch_of_one_is_the_polls_own_clock() {
658        let now = EventTime::from_millis(1234);
659        assert_eq!(back_date(now, &[Some(99_000)]), vec![now]);
660        assert_eq!(back_date(now, &[None]), vec![now]);
661        assert!(back_date(now, &[]).is_empty());
662    }
663
664    /// The defect this exists to kill: every packet in a drain claiming one
665    /// instant. With the device's own stamps the batch is strictly increasing
666    /// and carries the digitizer's spacing, not the poll's.
667    #[test]
668    fn a_batch_keeps_the_devices_own_spacing() {
669        let now = EventTime::from_millis(1_000);
670        let times = back_date(now, &[Some(40), Some(44), Some(52), Some(53)]);
671        assert_eq!(
672            times,
673            vec![
674                EventTime::from_millis(987),
675                EventTime::from_millis(991),
676                EventTime::from_millis(999),
677                now,
678            ]
679        );
680        for pair in times.windows(2) {
681            assert!(pair[1] > pair[0], "{times:?} must strictly increase");
682        }
683    }
684
685    /// A device whose counter rolls over mid-batch is still a forward stroke.
686    /// `wrapping_sub` is what makes the `u32` safe to subtract.
687    #[test]
688    fn a_wrapping_counter_is_still_a_forward_delta() {
689        let now = EventTime::from_millis(500);
690        let times = back_date(now, &[Some(u32::MAX - 3), Some(u32::MAX), Some(4)]);
691        assert_eq!(
692            times,
693            vec![
694                EventTime::from_millis(492),
695                EventTime::from_millis(495),
696                now,
697            ],
698            "MAX-3 → MAX is 3 ms and MAX → 4 is 5 ms across the wrap"
699        );
700    }
701
702    /// A stamp that goes *backwards* is not a 49-day pause. It falls through
703    /// to the even step rather than back-dating the batch into the last
704    /// century.
705    #[test]
706    fn a_backwards_stamp_falls_back_to_the_even_step() {
707        let now = EventTime::from_millis(100);
708        let times = back_date(now, &[Some(900), Some(100)]);
709        let step = PEN_POLL_INTERVAL / 2;
710        assert_eq!(
711            times,
712            vec![EventTime::from_duration(now.as_duration() - step), now]
713        );
714    }
715
716    /// No device clock at all: the batch is still spread, because the packets
717    /// are separate digitizer frames and calling them simultaneous is the
718    /// original bug in miniature.
719    #[test]
720    fn a_batch_with_no_device_clock_divides_the_poll_interval() {
721        let now = EventTime::from_millis(100);
722        let times = back_date(now, &[None, None, None]);
723        let step = PEN_POLL_INTERVAL / 3;
724        assert_eq!(
725            times,
726            vec![
727                EventTime::from_duration(now.as_duration() - step * 2),
728                EventTime::from_duration(now.as_duration() - step),
729                now,
730            ]
731        );
732        // The whole batch stays inside the window it was buffered in.
733        assert!(now.saturating_since(times[0]) < PEN_POLL_INTERVAL);
734    }
735
736    /// A source that stamps some packets and not others is pathological, not
737    /// impossible. It must still come out ordered.
738    #[test]
739    fn a_partly_stamped_batch_stays_monotone() {
740        let now = EventTime::from_millis(1_000);
741        let times = back_date(now, &[Some(10), None, Some(30), Some(31)]);
742        assert_eq!(times.len(), 4);
743        for pair in times.windows(2) {
744            assert!(pair[0] <= pair[1], "{times:?} must not go backwards");
745        }
746        assert_eq!(*times.last().unwrap(), now);
747    }
748
749    /// Equal device stamps stay equal. Two packets the digitizer stamped in
750    /// the same millisecond really were in the same millisecond, and
751    /// manufacturing a gap would be manufacturing precision.
752    #[test]
753    fn equal_device_stamps_stay_equal() {
754        let now = EventTime::from_millis(50);
755        assert_eq!(back_date(now, &[Some(7), Some(7)]), vec![now, now]);
756    }
757
758    /// Nothing is ever placed in the future, and nothing underflows the epoch.
759    #[test]
760    fn the_batch_is_clamped_to_the_epoch_and_to_now() {
761        let now = EventTime::from_millis(2);
762        let times = back_date(now, &[Some(0), Some(500), Some(1_000)]);
763        assert_eq!(times, vec![EventTime::ZERO, EventTime::ZERO, now]);
764        assert!(times.iter().all(|&t| t <= now));
765    }
766
767    /// `back_date_into` is the allocation-free twin, and answers identically.
768    #[test]
769    fn the_into_form_agrees_with_the_allocating_one() {
770        let now = EventTime::from_millis(777);
771        let stamps = [Some(1), Some(3), None, Some(9)];
772        let mut buffer = vec![EventTime::from_millis(42); 9];
773        back_date_into(now, &stamps, &mut buffer);
774        assert_eq!(buffer, back_date(now, &stamps));
775        back_date_into(now, &[], &mut buffer);
776        assert!(buffer.is_empty(), "an empty drain clears the buffer");
777    }
778
779    #[test]
780    fn the_null_source_reports_no_pen_and_yields_nothing() {
781        let mut source = null::NullPenSource::new();
782        let mut out = Vec::new();
783        source.poll(&mut out);
784        assert!(out.is_empty(), "the null source must yield no packets");
785        assert_eq!(source.capabilities(), PenCaps::NONE);
786        assert_eq!(source.touch_contact(1), None);
787
788        // And a translator carrying it advertises no pen either.
789        let mut state = crate::event_translation::TranslationState::new();
790        state.set_pen_source(Box::new(null::NullPenSource::new()));
791        let caps = state.capabilities();
792        assert!(!caps.reports_pen_kind);
793        assert!(!caps.reports_tilt);
794        assert!(!caps.reports_twist);
795        assert!(
796            state.poll_pen(EventTime::from_millis(10)).is_empty(),
797            "no packets in, no samples out"
798        );
799    }
800}