Skip to main content

cranpose_foundation/nodes/input/
types.rs

1use cranpose_ui_graphics::Point;
2use std::cell::Cell;
3use std::rc::Rc;
4
5pub type PointerId = u64;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum PointerPhase {
9    Start,
10    Move,
11    End,
12    Cancel,
13}
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum PointerEventKind {
17    Down,
18    Move,
19    Up,
20    Cancel,
21    Scroll,
22    /// Discrete zoom step (desktop ctrl+wheel, browser pinch-trackpad).
23    /// The multiplicative factor is carried in [`PointerEvent::zoom_delta`].
24    Zoom,
25    Enter,
26    Exit,
27}
28
29/// The kind of physical device that produced a pointer event.
30///
31/// Threaded from the platform layer (Android `MotionEvent` tool type, winit
32/// `PointerSource`/`ButtonSource`, web `PointerEvent.pointerType`) so input
33/// consumers can preserve device-specific gesture details while keeping shared
34/// direct-manipulation behavior source-independent.
35#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
36pub enum PointerSource {
37    /// A mouse or other indirect precise pointer (desktop, web `"mouse"`).
38    Mouse,
39    /// A finger on a touchscreen (Android finger, winit touch, web `"touch"`).
40    Touch,
41    /// A stylus/pen (Android stylus/eraser, winit tablet tool, web `"pen"`).
42    Stylus,
43    /// The platform did not report a device type.
44    #[default]
45    Unknown,
46}
47
48impl PointerSource {
49    /// Whether this source is a direct-touch device (finger or stylus), used
50    /// for contact-specific release and velocity semantics.
51    pub fn is_touch_like(self) -> bool {
52        matches!(self, PointerSource::Touch | PointerSource::Stylus)
53    }
54}
55
56#[repr(u8)]
57#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
58pub enum PointerButton {
59    Primary = 0,
60    Secondary = 1,
61    Middle = 2,
62    Back = 3,
63    Forward = 4,
64}
65
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
67pub struct PointerButtons(u8);
68
69impl PointerButtons {
70    pub const NONE: Self = Self(0);
71
72    pub fn new() -> Self {
73        Self::NONE
74    }
75
76    pub fn with(mut self, button: PointerButton) -> Self {
77        self.insert(button);
78        self
79    }
80
81    pub fn insert(&mut self, button: PointerButton) {
82        self.0 |= 1 << (button as u8);
83    }
84
85    pub fn remove(&mut self, button: PointerButton) {
86        self.0 &= !(1 << (button as u8));
87    }
88
89    pub fn contains(&self, button: PointerButton) -> bool {
90        (self.0 & (1 << (button as u8))) != 0
91    }
92}
93
94impl Default for PointerButtons {
95    fn default() -> Self {
96        Self::NONE
97    }
98}
99
100/// Pointer event with consumption tracking for gesture disambiguation.
101///
102/// Events can be consumed by handlers (e.g., scroll) to prevent other handlers
103/// (e.g., clicks) from receiving them. This enables proper gesture disambiguation
104/// matching Jetpack Compose's event consumption pattern.
105#[derive(Clone, Debug)]
106pub struct PointerEvent {
107    pub id: PointerId,
108    pub kind: PointerEventKind,
109    pub phase: PointerPhase,
110    pub position: Point,
111    pub global_position: Point,
112    /// Scroll delta in logical pixels.
113    ///
114    /// This is non-zero for [`PointerEventKind::Scroll`] events and zero for
115    /// button/move events.
116    pub scroll_delta: Point,
117    pub buttons: PointerButtons,
118    /// Platform timestamp of the input sample in milliseconds, when available.
119    ///
120    /// The time base is platform specific (e.g. Android's uptime clock); only
121    /// differences between events of the same gesture are meaningful. Gesture
122    /// velocity trackers must prefer this over the delivery time because
123    /// platforms like Android deliver input batched/frame-aligned: several
124    /// samples arrive back-to-back and delivery-time stamping makes computed
125    /// velocities wildly wrong.
126    pub time_ms: Option<i64>,
127    /// Timestamp in the animation frame-clock domain at input dispatch.
128    /// Unlike `time_ms`, this has the same origin as frame callbacks and can
129    /// anchor input-driven animations without a wall/platform clock conversion.
130    pub animation_time_nanos: Option<u64>,
131    /// Multiplicative zoom factor for [`PointerEventKind::Zoom`] events
132    /// (`> 1.0` zooms in, `< 1.0` zooms out). `1.0` for all other events.
133    pub zoom_delta: f32,
134    /// The kind of device that produced this event (touch, mouse, stylus), when
135    /// the platform reports it. Defaults to [`PointerSource::Unknown`].
136    pub source: PointerSource,
137    /// Tracks whether this event has been consumed by a handler.
138    /// Shared via Rc<Cell> so consumption can be tracked across copies.
139    consumed: Rc<Cell<bool>>,
140}
141
142impl PointerEvent {
143    pub fn new(kind: PointerEventKind, position: Point, global_position: Point) -> Self {
144        Self {
145            id: 0,
146            kind,
147            phase: match kind {
148                PointerEventKind::Down => PointerPhase::Start,
149                PointerEventKind::Move | PointerEventKind::Enter | PointerEventKind::Exit => {
150                    PointerPhase::Move
151                }
152                PointerEventKind::Up => PointerPhase::End,
153                PointerEventKind::Cancel => PointerPhase::Cancel,
154                PointerEventKind::Scroll | PointerEventKind::Zoom => PointerPhase::Move,
155            },
156            position,
157            global_position,
158            scroll_delta: Point { x: 0.0, y: 0.0 },
159            buttons: PointerButtons::NONE,
160            time_ms: None,
161            animation_time_nanos: None,
162            zoom_delta: 1.0,
163            source: PointerSource::Unknown,
164            consumed: Rc::new(Cell::new(false)),
165        }
166    }
167
168    /// Set the pointer id for this event (`0` is the primary pointer).
169    pub fn with_id(mut self, id: PointerId) -> Self {
170        self.id = id;
171        self
172    }
173
174    /// Set the multiplicative zoom factor for a [`PointerEventKind::Zoom`] event.
175    pub fn with_zoom_delta(mut self, zoom_delta: f32) -> Self {
176        self.zoom_delta = zoom_delta;
177        self
178    }
179
180    /// Set the scroll delta for this event.
181    pub fn with_scroll_delta(mut self, scroll_delta: Point) -> Self {
182        self.scroll_delta = scroll_delta;
183        self
184    }
185
186    /// Set the platform timestamp (milliseconds) for this event.
187    pub fn with_time_ms(mut self, time_ms: Option<i64>) -> Self {
188        self.time_ms = time_ms;
189        self
190    }
191
192    /// Set the timestamp in the animation frame-clock domain.
193    pub fn with_animation_time_nanos(mut self, time_nanos: u64) -> Self {
194        self.animation_time_nanos = Some(time_nanos);
195        self
196    }
197
198    /// Set the buttons state for this event
199    pub fn with_buttons(mut self, buttons: PointerButtons) -> Self {
200        self.buttons = buttons;
201        self
202    }
203
204    /// Set the device source (touch/mouse/stylus) for this event.
205    pub fn with_source(mut self, source: PointerSource) -> Self {
206        self.source = source;
207        self
208    }
209
210    /// Mark this event as consumed, preventing other handlers from processing it.
211    ///
212    /// Example: Scroll gestures consume events once dragging starts to prevent
213    /// child buttons from firing clicks.
214    pub fn consume(&self) {
215        self.consumed.set(true);
216    }
217
218    /// Check if this event has been consumed by another handler.
219    ///
220    /// Handlers should check this before processing events. For example,
221    /// clickable should not fire if the event was consumed by a scroll gesture.
222    pub fn is_consumed(&self) -> bool {
223        self.consumed.get()
224    }
225
226    /// Creates a copy of this event with a new local position, sharing the consumption state.
227    pub fn copy_with_local_position(&self, position: Point) -> Self {
228        Self {
229            id: self.id,
230            kind: self.kind,
231            phase: self.phase,
232            position,
233            global_position: self.global_position,
234            scroll_delta: self.scroll_delta,
235            buttons: self.buttons,
236            time_ms: self.time_ms,
237            animation_time_nanos: self.animation_time_nanos,
238            zoom_delta: self.zoom_delta,
239            source: self.source,
240            consumed: self.consumed.clone(),
241        }
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    fn point(x: f32, y: f32) -> Point {
250        Point { x, y }
251    }
252
253    #[test]
254    fn pointer_event_clones_share_consumed_state() {
255        let event = PointerEvent::new(PointerEventKind::Move, point(1.0, 2.0), point(3.0, 4.0));
256        let cloned = event.clone();
257        assert!(!event.is_consumed());
258        assert!(!cloned.is_consumed());
259
260        cloned.consume();
261
262        assert!(event.is_consumed());
263        assert!(cloned.is_consumed());
264    }
265
266    #[test]
267    fn pointer_event_source_defaults_unknown_and_threads_through_copy() {
268        let event = PointerEvent::new(PointerEventKind::Down, point(1.0, 1.0), point(1.0, 1.0));
269        assert_eq!(event.source, PointerSource::Unknown);
270        assert!(!PointerSource::Unknown.is_touch_like());
271
272        let touch = event.with_source(PointerSource::Touch);
273        assert_eq!(touch.source, PointerSource::Touch);
274        assert!(PointerSource::Touch.is_touch_like());
275        assert!(PointerSource::Stylus.is_touch_like());
276        assert!(!PointerSource::Mouse.is_touch_like());
277
278        // Local-position copies (used during hit-test dispatch) keep the source.
279        let local = touch.copy_with_local_position(point(5.0, 5.0));
280        assert_eq!(local.source, PointerSource::Touch);
281    }
282
283    #[test]
284    fn pointer_event_copy_with_local_position_preserves_consumption_state() {
285        let event = PointerEvent::new(PointerEventKind::Down, point(4.0, 5.0), point(4.0, 5.0))
286            .with_time_ms(Some(123))
287            .with_animation_time_nanos(456_000_000);
288        let local = event.copy_with_local_position(point(1.0, 1.0));
289
290        assert_eq!(local.position, point(1.0, 1.0));
291        assert_eq!(local.global_position, event.global_position);
292        assert_eq!(local.time_ms, Some(123));
293        assert_eq!(local.animation_time_nanos, Some(456_000_000));
294        assert!(!local.is_consumed());
295
296        event.consume();
297
298        assert!(local.is_consumed());
299    }
300}