ui_events/pointer/mod.rs
1// Copyright 2025 the UI Events Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! # Pointer event types
5//!
6//! This module contains the core types for representing pointer input across
7//! mice, pens, and touch. These types are transport-agnostic and can be
8//! produced from platform backends (e.g. winit, web) and consumed in UI code.
9//!
10//! ## Key ideas:
11//!
12//! - [`PointerId`] and [`PersistentDeviceId`] help correlate states over time.
13//! - [`PointerState`] carries position, pressure, tilt, modifiers and more.
14//! - [`PointerEvent`] is the main event enum: down/up/move/enter/leave/scroll/gesture.
15//! - [`PointerInfo::is_primary_pointer`] is a convenience for primary interactions.
16//!
17//! ## Example: checking for primary pointer and using logical coordinates
18//!
19//! ```
20//! use ui_events::pointer::{PointerEvent, PointerUpdate};
21//!
22//! fn handle(ev: PointerEvent) {
23//! if let PointerEvent::Move(PointerUpdate { pointer, current, .. }) = ev {
24//! if pointer.is_primary_pointer() {
25//! let lp = current.logical_position();
26//! let _ = (lp.x, lp.y);
27//! }
28//! }
29//! }
30//! ```
31
32mod buttons;
33
34pub use buttons::{PointerButton, PointerButtons};
35
36extern crate alloc;
37use alloc::vec::Vec;
38
39use core::num::NonZeroU64;
40
41use dpi::{LogicalPosition, PhysicalPosition, PhysicalSize};
42use keyboard_types::Modifiers;
43
44use crate::ScrollDelta;
45
46/// A unique identifier for the pointer.
47///
48/// PointerId(1) is reserved for the primary pointer.
49#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
50pub struct PointerId(NonZeroU64);
51
52impl PointerId {
53 /// The id of the primary pointer.
54 pub const PRIMARY: Self = Self(NonZeroU64::MIN);
55
56 /// Make a new `PointerId` from a `u64`.
57 #[inline(always)]
58 pub fn new(n: u64) -> Option<Self> {
59 NonZeroU64::new(n).map(PointerId)
60 }
61
62 /// Return `true` if this is the primary `PointerId`.
63 #[inline(always)]
64 pub fn is_primary_pointer(self) -> bool {
65 self == Self::PRIMARY
66 }
67
68 /// Returns the inner `NonZeroU64` value.
69 #[inline(always)]
70 pub fn get_inner(self) -> NonZeroU64 {
71 self.0
72 }
73}
74
75/// An identifier for the pointing device that is stable across the session.
76///
77/// PointerId(1) is reserved for the primary pointer.
78#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
79pub struct PersistentDeviceId(NonZeroU64);
80
81impl PersistentDeviceId {
82 /// Make a new `PersistentDeviceId` from a `u64`.
83 #[inline(always)]
84 pub fn new(n: u64) -> Option<Self> {
85 NonZeroU64::new(n).map(PersistentDeviceId)
86 }
87}
88
89/// The type of device that has generated a pointer event.
90#[non_exhaustive]
91#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
92#[repr(u8)]
93pub enum PointerType {
94 /// The type of device could not be determined.
95 #[default]
96 Unknown,
97 /// A mouse.
98 Mouse,
99 /// A pen or stylus.
100 Pen,
101 /// A touch contact.
102 Touch,
103}
104
105/// Identifying information about a pointer, stable across states.
106#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
107pub struct PointerInfo {
108 /// Pointer ID.
109 ///
110 /// [`PointerId::PRIMARY`] is reserved for the primary pointer,
111 /// so when converting platform pointer IDs on a platform that
112 /// does not reserve this value, add an offset to avoid collision.
113 ///
114 /// `None` is for events not originating from a pointing device.
115 pub pointer_id: Option<PointerId>,
116 /// Persistent device ID.
117 ///
118 /// This should be set when the platform can identify a given pointing
119 /// device during the whole session, and associate it with events.
120 /// If this is not possible for the given event, it should be `None`.
121 pub persistent_device_id: Option<PersistentDeviceId>,
122 /// Pointer type.
123 pub pointer_type: PointerType,
124}
125
126impl PointerInfo {
127 /// Returns `true` if this is the primary pointer.
128 #[inline(always)]
129 pub fn is_primary_pointer(&self) -> bool {
130 self.pointer_id.is_some_and(PointerId::is_primary_pointer)
131 }
132}
133
134/// Orientation of a pointer.
135#[derive(Clone, Copy, Debug, PartialEq)]
136pub struct PointerOrientation {
137 /// Spherical altitude.
138 ///
139 /// 0 is parallel to the surface, π/2 is perpendicular.
140 pub altitude: f32,
141 /// Spherical azimuth.
142 ///
143 /// 0 is the positive x axis, π/2 is positive y.
144 pub azimuth: f32,
145}
146
147impl Default for PointerOrientation {
148 fn default() -> Self {
149 Self {
150 altitude: core::f32::consts::FRAC_PI_2,
151 azimuth: core::f32::consts::FRAC_PI_2,
152 }
153 }
154}
155
156/// The size of an input, usually touch.
157///
158/// If this is not provided by the underlying API, platform, or device,
159/// then it should be a single pixel.
160pub type ContactGeometry = PhysicalSize<f64>;
161
162/// A single pointer state.
163#[derive(Clone, Debug, PartialEq)]
164pub struct PointerState {
165 /// `u64` nanoseconds real time.
166 ///
167 /// The base time is not important, except by convention, and should
168 /// generally be the same at least for states originating from the
169 /// same device.
170 pub time: u64,
171 /// Position.
172 pub position: PhysicalPosition<f64>,
173 /// Pressed buttons.
174 pub buttons: PointerButtons,
175 /// Modifiers state.
176 pub modifiers: Modifiers,
177 /// Click or tap count associated with the pointer.
178 pub count: u8,
179 /// The size of an input, usually touch.
180 ///
181 /// If this is not provided by the underlying API, platform, or device,
182 /// then it should be a single pixel.
183 pub contact_geometry: ContactGeometry,
184 /// Orientation.
185 pub orientation: PointerOrientation,
186 /// Normalized pressure in range 0..1.
187 ///
188 /// Where pressure is not reported by the platform, it
189 /// is always 0.5 when activated and 0.0 when not.
190 pub pressure: f32,
191 /// Normalized ‘tangential pressure’ in range -1..1.
192 ///
193 /// This is an arbitrary parameter and, despite its name,
194 /// it may not originate from a pressure sensitive control.
195 /// This is often controlled by something like a wheel on the
196 /// barrel of an ‘airbrush’ style pen.
197 pub tangential_pressure: f32,
198 /// The scale factor of the window/screen where this pointer event occurred.
199 pub scale_factor: f64,
200}
201
202impl PointerState {
203 /// Returns the pointer position as a logical `kurbo::Point`.
204 ///
205 /// The position is converted from physical pixels to logical coordinates
206 /// using the state's scale factor.
207 #[cfg(feature = "kurbo")]
208 pub fn logical_point(&self) -> kurbo::Point {
209 let log = self.position.to_logical(self.scale_factor);
210 kurbo::Point { x: log.x, y: log.y }
211 }
212
213 /// Returns the pointer position as a physical `kurbo::Point`.
214 #[cfg(feature = "kurbo")]
215 pub fn physical_point(&self) -> kurbo::Point {
216 kurbo::Point {
217 x: self.position.x,
218 y: self.position.y,
219 }
220 }
221
222 /// Returns the pointer position in logical coordinates.
223 ///
224 /// This converts the physical position to logical coordinates using
225 /// the state's scale factor, providing DPI-independent positioning.
226 pub fn logical_position(&self) -> LogicalPosition<f64> {
227 self.position.to_logical(self.scale_factor)
228 }
229}
230
231impl Default for PointerState {
232 fn default() -> Self {
233 Self {
234 time: 0,
235 position: PhysicalPosition::<f64>::default(),
236 buttons: PointerButtons::default(),
237 modifiers: Modifiers::default(),
238 count: 0,
239 contact_geometry: ContactGeometry {
240 width: 1.0,
241 height: 1.0,
242 },
243 orientation: PointerOrientation::default(),
244 // No buttons pressed, therefore no pressure.
245 pressure: 0.0,
246 tangential_pressure: 0.0,
247 scale_factor: 1.,
248 }
249 }
250}
251
252/// A pointer update, along with coalesced and predicted states.
253#[derive(Clone, Debug, PartialEq)]
254pub struct PointerUpdate {
255 /// Identifying information about pointer.
256 pub pointer: PointerInfo,
257 /// Current state.
258 pub current: PointerState,
259 /// Coalesced states, ordered by `time`.
260 ///
261 /// Coalescing is application-specific.
262 /// On the web, the browser does its own coalescing, whereas
263 /// on other platforms you may do your own, or forego it
264 /// altogether, delivering every state.
265 pub coalesced: Vec<PointerState>,
266 /// Predicted states, ordered by `time`.
267 ///
268 /// Some platforms provide predicted states directly,
269 /// and you may choose to add your own predictor.
270 pub predicted: Vec<PointerState>,
271}
272
273impl PointerUpdate {
274 /// Returns `true` if this is the primary pointer.
275 #[inline(always)]
276 pub fn is_primary_pointer(&self) -> bool {
277 self.pointer.is_primary_pointer()
278 }
279}
280
281/// An event representing a [`PointerButton`] that was pressed or released.
282#[derive(Clone, Debug)]
283pub struct PointerButtonEvent {
284 /// The [`PointerButton`] that was pressed.
285 pub button: Option<PointerButton>,
286 /// Identity of the pointer.
287 pub pointer: PointerInfo,
288 /// The state of the pointer (i.e. position, pressure, etc.).
289 pub state: PointerState,
290}
291
292/// An event representing a scroll
293#[derive(Clone, Debug)]
294pub struct PointerScrollEvent {
295 /// Identity of the pointer.
296 pub pointer: PointerInfo,
297 /// The delta of the scroll.
298 pub delta: ScrollDelta,
299 /// The state of the pointer (i.e. position, pressure, etc.).
300 pub state: PointerState,
301}
302
303/// A touchpad gesture for pointer.
304#[derive(Clone, Debug)]
305pub enum PointerGesture {
306 /// Pinch delta.
307 ///
308 /// This is a signed quantity as a fraction of the current scale.
309 ///
310 /// For example, `0.1` means “increase scale by 10%”, and `-0.1` means
311 /// “decrease scale by 10%”.
312 ///
313 /// A common update rule is `new_scale = current_scale * (1.0 + delta)`.
314 Pinch(f32),
315 /// Clockwise rotation in radians.
316 ///
317 /// This is a delta for this update, not an absolute angle.
318 Rotate(f32),
319}
320
321/// An event representing a gesture
322#[derive(Clone, Debug)]
323pub struct PointerGestureEvent {
324 /// Identity of the pointer.
325 pub pointer: PointerInfo,
326 /// The gesture being performed.
327 pub gesture: PointerGesture,
328 /// The state of the pointer (i.e. position, pressure, etc.).
329 pub state: PointerState,
330}
331
332/// A standard `PointerEvent`.
333///
334/// This is intentionally limited to standard pointer events,
335/// and it is expected that applications and frameworks that
336/// support more event types will use this as a base and add
337/// what they need in a conversion.
338#[derive(Clone, Debug)]
339pub enum PointerEvent {
340 /// A [`PointerButton`] was pressed.
341 Down(PointerButtonEvent),
342 /// A [`PointerButton`] was released.
343 Up(PointerButtonEvent),
344 /// Pointer moved.
345 Move(PointerUpdate),
346 /// Pointer motion was cancelled.
347 ///
348 /// Usually this is a touch which was taken over somewhere else.
349 /// You should try to undo the effect of the gesture when you receive this.
350 Cancel(PointerInfo),
351 /// Pointer entered the area that receives this event.
352 Enter(PointerInfo),
353 /// Pointer left the area that receives these events.
354 Leave(PointerInfo),
355 /// A scroll was requested at the pointer location.
356 ///
357 /// Usually this is caused by a mouse wheel or a touchpad.
358 Scroll(PointerScrollEvent),
359 /// Gesture at pointer.
360 Gesture(PointerGestureEvent),
361}
362
363impl PointerEvent {
364 /// Returns `true` if this event is for the primary pointer.
365 #[inline(always)]
366 pub fn is_primary_pointer(&self) -> bool {
367 match self {
368 Self::Down(PointerButtonEvent { pointer, .. })
369 | Self::Up(PointerButtonEvent { pointer, .. })
370 | Self::Move(PointerUpdate { pointer, .. })
371 | Self::Cancel(pointer)
372 | Self::Enter(pointer)
373 | Self::Leave(pointer)
374 | Self::Scroll(PointerScrollEvent { pointer, .. })
375 | Self::Gesture(PointerGestureEvent { pointer, .. }) => pointer.is_primary_pointer(),
376 }
377 }
378}