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