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