Skip to main content

cranpose_foundation/nodes/input/
types.rs

1use std::{
2    cell::{Cell, RefCell},
3    rc::Rc,
4};
5
6use cranpose_ui_graphics::{Point, space::Ray};
7
8use super::rotary::RotaryScrollEvent;
9
10/// Identifies one pointer of a gesture, such as one finger, as Compose's `PointerId`. The
11/// primary pointer is [`PointerId::PRIMARY`].
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub struct PointerId(pub u64);
14
15impl PointerId {
16    /// The mouse, or the first finger of a touch.
17    pub const PRIMARY: Self = Self(0);
18
19    /// Whether this is [`PointerId::PRIMARY`].
20    pub const fn is_primary(self) -> bool {
21        self.0 == Self::PRIMARY.0
22    }
23}
24
25impl std::fmt::Display for PointerId {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        self.0.fmt(f)
28    }
29}
30
31type PostDispatchAction = Box<dyn FnOnce() -> bool>;
32
33#[derive(Clone)]
34struct DeferredPostDispatch {
35    action: Rc<RefCell<Option<PostDispatchAction>>>,
36}
37
38impl std::fmt::Debug for DeferredPostDispatch {
39    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        formatter
41            .debug_struct("DeferredPostDispatch")
42            .field("is_pending", &self.action.borrow().is_some())
43            .finish()
44    }
45}
46
47/// The phase of a pointer gesture that an event belongs to.
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub enum PointerPhase {
50    /// The pointer went down.
51    Start,
52    /// The pointer moved, scrolled or hovered.
53    Move,
54    /// The pointer went up.
55    End,
56    /// The system canceled the gesture.
57    Cancel,
58}
59
60/// What a pointer event reports.
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub enum PointerEventKind {
63    /// A pointer went down: the primary mouse button, a finger or a pen
64    /// touched. Other mouse buttons arrive as
65    /// [`ButtonDown`](Self::ButtonDown).
66    Down,
67    /// A pointer moved.
68    Move,
69    /// A pointer went up: the primary mouse button, a finger or a pen
70    /// lifted.
71    Up,
72    /// A mouse button other than the primary one went down: the right, the
73    /// middle or a side button. A click, a drag or a scroll starts only from
74    /// [`Down`](Self::Down); a handler that takes another button, such as one
75    /// that opens a menu or turns a camera, matches this. The nodes under the
76    /// pointer receive it, or, while another button is down, the nodes that
77    /// took that button's press. Those nodes receive the moves until the
78    /// last button goes up, with the buttons held in
79    /// [`PointerEvent::buttons`].
80    ButtonDown(PointerButton),
81    /// A mouse button other than the primary one went up; see
82    /// [`ButtonDown`](Self::ButtonDown).
83    ButtonUp(PointerButton),
84    /// The system canceled the gesture of the pointer.
85    Cancel,
86    /// A wheel or trackpad scroll. The delta is in [`PointerEvent::scroll_delta`].
87    Scroll,
88    /// Discrete zoom step (desktop ctrl+wheel, browser pinch-trackpad).
89    /// The multiplicative factor is carried in [`PointerEvent::zoom_delta`].
90    Zoom,
91    /// Rotary scroll (Wear OS crown / rotating bezel) during the **capture**
92    /// pass, which runs root-to-focused so ancestors can intercept the event
93    /// before the focused node sees it.
94    ///
95    /// The scroll amounts are carried in [`PointerEvent::scroll_delta`] (`y` =
96    /// vertical pixels, `x` = horizontal pixels) and the rotary uptime in
97    /// [`PointerEvent::time_ms`]; use
98    /// [`PointerEvent::rotary_scroll_event`] to read them back as a
99    /// [`RotaryScrollEvent`]. Mirrors Compose's `onPreRotaryScrollEvent`.
100    RotaryScrollPre,
101    /// Rotary scroll during the **bubble** pass, which runs focused-to-root.
102    /// Mirrors Compose's `onRotaryScrollEvent`.
103    RotaryScroll,
104    /// A pointer began to hover over the receiving node.
105    Enter,
106    /// A pointer stopped hovering over the receiving node.
107    Exit,
108}
109
110impl PointerEventKind {
111    /// Returns true for the two rotary passes.
112    pub fn is_rotary(self) -> bool {
113        matches!(self, Self::RotaryScrollPre | Self::RotaryScroll)
114    }
115}
116
117/// The kind of physical device that produced a pointer event.
118///
119/// Threaded from the platform layer (Android `MotionEvent` tool type, winit
120/// `PointerSource`/`ButtonSource`, web `PointerEvent.pointerType`) so input
121/// consumers can preserve device-specific gesture details while keeping shared
122/// direct-manipulation behavior source-independent.
123#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
124pub enum PointerSource {
125    /// A mouse or other indirect precise pointer (desktop, web `"mouse"`).
126    Mouse,
127    /// A finger on a touchscreen (Android finger, winit touch, web `"touch"`).
128    Touch,
129    /// A stylus/pen (Android stylus/eraser, winit tablet tool, web `"pen"`).
130    Stylus,
131    /// The platform did not report a device type.
132    #[default]
133    Unknown,
134}
135
136impl PointerSource {
137    /// Whether this source is a direct-touch device (finger or stylus), used
138    /// for contact-specific release and velocity semantics.
139    pub fn is_touch_like(self) -> bool {
140        matches!(self, Self::Touch | Self::Stylus)
141    }
142}
143
144/// A button of a mouse or of a similar pointing device.
145#[repr(u8)]
146#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
147pub enum PointerButton {
148    /// The main button, usually the left mouse button.
149    Primary = 0,
150    /// The secondary button, usually the right mouse button.
151    Secondary = 1,
152    /// The middle button, usually a press on the wheel.
153    Middle = 2,
154    /// The back button of a mouse.
155    Back = 3,
156    /// The forward button of a mouse.
157    Forward = 4,
158}
159
160/// A set of pointer buttons that are pressed together.
161#[derive(Clone, Copy, Debug, PartialEq, Eq)]
162pub struct PointerButtons(u8);
163
164impl PointerButtons {
165    /// The set with no button pressed.
166    pub const NONE: Self = Self(0);
167
168    /// Creates a set with no button pressed.
169    pub fn new() -> Self {
170        Self::NONE
171    }
172
173    /// Returns the set with `button` added.
174    pub fn with(mut self, button: PointerButton) -> Self {
175        self.insert(button);
176        self
177    }
178
179    /// Adds `button` to the set.
180    pub fn insert(&mut self, button: PointerButton) {
181        self.0 |= 1 << (button as u8);
182    }
183
184    /// Removes `button` from the set.
185    pub fn remove(&mut self, button: PointerButton) {
186        self.0 &= !(1 << (button as u8));
187    }
188
189    /// Whether `button` is in the set.
190    pub fn contains(&self, button: PointerButton) -> bool {
191        (self.0 & (1 << (button as u8))) != 0
192    }
193}
194
195impl Default for PointerButtons {
196    fn default() -> Self {
197        Self::NONE
198    }
199}
200
201/// Keyboard modifier keys held during an input sample.
202///
203/// Lives here (rather than up in `cranpose-ui`, where the keyboard `KeyEvent`
204/// type lives) because [`PointerEvent`] needs it too and `cranpose-foundation`
205/// sits below `cranpose-ui` in the dependency graph — this is the one crate
206/// both a key event and a pointer event can share it from. `cranpose-ui`
207/// re-exports this type rather than defining its own, so there is exactly one
208/// `Modifiers` in the framework.
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
210pub struct Modifiers {
211    /// Shift key is pressed.
212    pub shift: bool,
213    /// Control key is pressed (Cmd on macOS).
214    pub ctrl: bool,
215    /// Alt key is pressed (Option on macOS).
216    pub alt: bool,
217    /// Meta/Super key is pressed (Windows key, Cmd on macOS).
218    pub meta: bool,
219}
220
221impl Modifiers {
222    /// No modifiers pressed.
223    pub const NONE: Self = Self {
224        shift: false,
225        ctrl: false,
226        alt: false,
227        meta: false,
228    };
229
230    /// Shift alone.
231    pub const SHIFT: Self = Self {
232        shift: true,
233        ..Self::NONE
234    };
235
236    /// Control alone.
237    pub const CTRL: Self = Self {
238        ctrl: true,
239        ..Self::NONE
240    };
241
242    /// The platform's command key alone: Cmd on macOS, Control elsewhere,
243    /// the key [`Modifiers::command_or_ctrl`] reads.
244    pub const COMMAND: Self = Self {
245        ctrl: cfg!(not(target_os = "macos")),
246        meta: cfg!(target_os = "macos"),
247        ..Self::NONE
248    };
249
250    /// The keys held in `self` or in `other`.
251    pub const fn union(self, other: Self) -> Self {
252        Self {
253            shift: self.shift || other.shift,
254            ctrl: self.ctrl || other.ctrl,
255            alt: self.alt || other.alt,
256            meta: self.meta || other.meta,
257        }
258    }
259
260    /// Returns true if any modifier is pressed.
261    pub fn any(&self) -> bool {
262        self.shift || self.ctrl || self.alt || self.meta
263    }
264
265    /// Returns true if Ctrl (or Cmd on macOS) is pressed.
266    pub fn command_or_ctrl(&self) -> bool {
267        #[cfg(target_os = "macos")]
268        {
269            self.meta
270        }
271        #[cfg(not(target_os = "macos"))]
272        {
273            self.ctrl
274        }
275    }
276}
277
278/// Pointer event with consumption tracking for gesture disambiguation.
279///
280/// Events can be consumed by handlers (e.g., scroll) to prevent other handlers
281/// (e.g., clicks) from receiving them. This enables proper gesture disambiguation
282/// matching Jetpack Compose's event consumption pattern.
283#[derive(Clone, Debug)]
284pub struct PointerEvent {
285    /// The pointer this event belongs to. `0` is the primary pointer.
286    pub id: PointerId,
287    /// What happened to the pointer.
288    pub kind: PointerEventKind,
289    /// The phase of the gesture that the event belongs to, which follows from `kind`.
290    pub phase: PointerPhase,
291    /// Where the pointer is, in logical pixels, in the coordinates of the node that receives the
292    /// event.
293    pub position: Point,
294    /// Where the pointer is, in logical pixels, in the coordinates of the composition root.
295    pub global_position: Point,
296    /// Where the pointer is on the screen, in logical pixels, when the
297    /// platform told the shell where the window drawing this event's root
298    /// sits. `None` on platforms without window positions and for events a
299    /// test dispatches without one. A gesture that crosses windows, such as
300    /// a tab torn out of one window and dropped on another, compares this
301    /// with the windows' positions instead of translating `global_position`
302    /// itself.
303    pub screen_position: Option<Point>,
304    /// Scroll delta in logical pixels.
305    ///
306    /// This is non-zero for [`PointerEventKind::Scroll`] events and zero for
307    /// button/move events.
308    pub scroll_delta: Point,
309    /// The buttons that are pressed at the time of this event.
310    pub buttons: PointerButtons,
311    /// Platform timestamp of the input sample in milliseconds, when available.
312    ///
313    /// The time base is platform specific (e.g. Android's uptime clock); only
314    /// differences between events of the same gesture are meaningful. Gesture
315    /// velocity trackers must prefer this over the delivery time because
316    /// platforms like Android deliver input batched/frame-aligned: several
317    /// samples arrive back-to-back and delivery-time stamping makes computed
318    /// velocities wildly wrong.
319    pub time_ms: Option<i64>,
320    /// Timestamp in the animation frame-clock domain at input dispatch.
321    /// Unlike `time_ms`, this has the same origin as frame callbacks and can
322    /// anchor input-driven animations without a wall/platform clock conversion.
323    pub animation_time_nanos: Option<u64>,
324    /// Multiplicative zoom factor for [`PointerEventKind::Zoom`] events
325    /// (`> 1.0` zooms in, `< 1.0` zooms out). `1.0` for all other events.
326    pub zoom_delta: f32,
327    /// The kind of device that produced this event (touch, mouse, stylus), when
328    /// the platform reports it. Defaults to [`PointerSource::Unknown`].
329    pub source: PointerSource,
330    /// Keyboard modifiers held at the time of this sample, when the platform
331    /// can report them.
332    ///
333    /// `None` means the platform never told the shell what the keyboard state
334    /// was — touch-only Android/iOS input has no channel for it today — and is
335    /// deliberately distinct from `Some(Modifiers::NONE)`, which means the
336    /// platform looked and nothing was held. An app that wants shift/ctrl-click
337    /// multi-select reads this field directly; it must not treat `None` as
338    /// "nothing held" or it silently drops the gesture on the platforms that
339    /// cannot yet report it instead of visibly doing nothing.
340    pub modifiers: Option<Modifiers>,
341    /// For a pointer that points along a ray in a 3D scene from somewhere
342    /// other than the window's eye (a controller, a hand, a gaze), the ray,
343    /// in the space the scene's eye stands in. A target on a mesh surface
344    /// finds the event's position where the ray meets the surface, in place
345    /// of where `global_position` shows. `None` for a pointer on the window.
346    pub ray: Option<Ray>,
347    consumed: Rc<Cell<bool>>,
348    deferred_post_dispatch: DeferredPostDispatch,
349}
350
351impl PointerEvent {
352    /// Creates an event of `kind` at `position`, in the coordinates of the receiving node, and at
353    /// `global_position`, in the coordinates of the composition root. The pointer is
354    /// [`PointerId::PRIMARY`] and the other fields have neutral values.
355    pub fn new(kind: PointerEventKind, position: Point, global_position: Point) -> Self {
356        Self {
357            id: PointerId::PRIMARY,
358            kind,
359            phase: match kind {
360                PointerEventKind::Down => PointerPhase::Start,
361                PointerEventKind::Move
362                | PointerEventKind::Enter
363                | PointerEventKind::Exit
364                | PointerEventKind::ButtonDown(_)
365                | PointerEventKind::ButtonUp(_) => PointerPhase::Move,
366                PointerEventKind::Up => PointerPhase::End,
367                PointerEventKind::Cancel => PointerPhase::Cancel,
368                PointerEventKind::Scroll
369                | PointerEventKind::Zoom
370                | PointerEventKind::RotaryScrollPre
371                | PointerEventKind::RotaryScroll => PointerPhase::Move,
372            },
373            position,
374            global_position,
375            screen_position: None,
376            scroll_delta: Point { x: 0.0, y: 0.0 },
377            buttons: PointerButtons::NONE,
378            time_ms: None,
379            animation_time_nanos: None,
380            zoom_delta: 1.0,
381            source: PointerSource::Unknown,
382            modifiers: None,
383            ray: None,
384            consumed: Rc::new(Cell::new(false)),
385            deferred_post_dispatch: DeferredPostDispatch {
386                action: Rc::new(RefCell::new(None)),
387            },
388        }
389    }
390
391    /// Set the pointer id for this event (`0` is the primary pointer).
392    pub fn with_id(mut self, id: PointerId) -> Self {
393        self.id = id;
394        self
395    }
396
397    /// Set the ray a pointer in a 3D scene points along (see
398    /// [`PointerEvent::ray`]).
399    pub fn with_ray(mut self, ray: Ray) -> Self {
400        self.ray = Some(ray);
401        self
402    }
403
404    /// Set the multiplicative zoom factor for a [`PointerEventKind::Zoom`] event.
405    pub fn with_zoom_delta(mut self, zoom_delta: f32) -> Self {
406        self.zoom_delta = zoom_delta;
407        self
408    }
409
410    /// Set the scroll delta for this event.
411    pub fn with_scroll_delta(mut self, scroll_delta: Point) -> Self {
412        self.scroll_delta = scroll_delta;
413        self
414    }
415
416    /// Set the platform timestamp (milliseconds) for this event.
417    pub fn with_time_ms(mut self, time_ms: Option<i64>) -> Self {
418        self.time_ms = time_ms;
419        self
420    }
421
422    /// Set the timestamp in the animation frame-clock domain.
423    pub fn with_animation_time_nanos(mut self, time_nanos: u64) -> Self {
424        self.animation_time_nanos = Some(time_nanos);
425        self
426    }
427
428    /// Set where the pointer is on the screen, when the platform knows.
429    pub fn with_screen_position(mut self, screen_position: Option<Point>) -> Self {
430        self.screen_position = screen_position;
431        self
432    }
433
434    /// Where the pointer is in the steadiest frame the platform offers: on
435    /// the screen where it reports window positions, and in the composition
436    /// where it does not.
437    ///
438    /// Two events compared this way say how far the hand travelled even when
439    /// the window under it travelled too, which is what a drag threshold has
440    /// to measure: a press that drags a borderless window never moves within
441    /// that window, because the window follows it.
442    pub fn travelled_to(&self) -> Point {
443        self.screen_position.unwrap_or(self.global_position)
444    }
445
446    /// Set the buttons state for this event
447    pub fn with_buttons(mut self, buttons: PointerButtons) -> Self {
448        self.buttons = buttons;
449        self
450    }
451
452    /// Set the device source (touch/mouse/stylus) for this event.
453    pub fn with_source(mut self, source: PointerSource) -> Self {
454        self.source = source;
455        self
456    }
457
458    /// Set the keyboard modifiers held during this event, when the platform
459    /// can report them. See the [`modifiers`](Self::modifiers) field docs for
460    /// why this takes a concrete [`Modifiers`] rather than an `Option`: the
461    /// `None` case is the *absence* of a call to this builder, not a value it
462    /// produces.
463    pub fn with_modifiers(mut self, modifiers: Modifiers) -> Self {
464        self.modifiers = Some(modifiers);
465        self
466    }
467
468    /// Builds a rotary pointer event for one dispatch pass.
469    ///
470    /// `kind` must be [`PointerEventKind::RotaryScrollPre`] (capture) or
471    /// [`PointerEventKind::RotaryScroll`] (bubble). The rotary payload rides on
472    /// the existing `scroll_delta`/`time_ms` fields so rotary reuses the
473    /// pointer dispatch path without widening [`PointerEvent`].
474    pub fn rotary(kind: PointerEventKind, rotary: RotaryScrollEvent, position: Point) -> Self {
475        debug_assert!(
476            kind.is_rotary(),
477            "PointerEvent::rotary requires a rotary event kind"
478        );
479        Self::new(kind, position, position)
480            .with_scroll_delta(Point {
481                x: rotary.horizontal_scroll_pixels,
482                y: rotary.vertical_scroll_pixels,
483            })
484            .with_time_ms(Some(rotary.uptime_millis as i64))
485    }
486
487    /// Reads this event back as a [`RotaryScrollEvent`], or `None` when it is
488    /// not a rotary event.
489    ///
490    /// Copies three scalars out of the event; it never allocates.
491    pub fn rotary_scroll_event(&self) -> Option<RotaryScrollEvent> {
492        if !self.kind.is_rotary() {
493            return None;
494        }
495        Some(RotaryScrollEvent {
496            vertical_scroll_pixels: self.scroll_delta.y,
497            horizontal_scroll_pixels: self.scroll_delta.x,
498            uptime_millis: self.time_ms.unwrap_or(0).max(0) as u64,
499        })
500    }
501
502    /// Mark this event as consumed, preventing other handlers from processing it.
503    ///
504    /// Example: Scroll gestures consume events once dragging starts to prevent
505    /// child buttons from firing clicks.
506    pub fn consume(&self) {
507        self.consumed.set(true);
508    }
509
510    /// Check if this event has been consumed by another handler.
511    ///
512    /// Handlers should check this before processing events. For example,
513    /// clickable should not fire if the event was consumed by a scroll gesture.
514    pub fn is_consumed(&self) -> bool {
515        self.consumed.get()
516    }
517
518    /// Stores `action` to run once after every handler saw the event. It runs only when no handler
519    /// consumed the event, and when it returns `true` the event becomes consumed. A later call
520    /// replaces the stored action.
521    pub fn defer_post_dispatch_action<F>(&self, action: F)
522    where
523        F: FnOnce() -> bool + 'static,
524    {
525        *self.deferred_post_dispatch.action.borrow_mut() = Some(Box::new(action));
526    }
527
528    /// Runs the action stored by `defer_post_dispatch_action`, unless a handler consumed the event;
529    /// then the action is dropped. The shell calls this after it dispatched the event to all
530    /// targets.
531    pub fn finish_post_dispatch(&self) {
532        if self.is_consumed() {
533            self.deferred_post_dispatch.action.borrow_mut().take();
534            return;
535        }
536
537        let Some(action) = self.deferred_post_dispatch.action.borrow_mut().take() else {
538            return;
539        };
540        if action() {
541            self.consume();
542        }
543    }
544
545    /// Creates a copy of this event with a new local position, sharing the consumption state.
546    pub fn copy_with_local_position(&self, position: Point) -> Self {
547        Self {
548            id: self.id,
549            kind: self.kind,
550            phase: self.phase,
551            position,
552            global_position: self.global_position,
553            screen_position: self.screen_position,
554            scroll_delta: self.scroll_delta,
555            buttons: self.buttons,
556            time_ms: self.time_ms,
557            animation_time_nanos: self.animation_time_nanos,
558            zoom_delta: self.zoom_delta,
559            source: self.source,
560            modifiers: self.modifiers,
561            ray: self.ray,
562            consumed: Rc::clone(&self.consumed),
563            deferred_post_dispatch: self.deferred_post_dispatch.clone(),
564        }
565    }
566}
567
568#[cfg(test)]
569#[path = "tests/types_tests.rs"]
570mod tests;