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}