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#[repr(u8)]
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31pub enum PointerButton {
32    Primary = 0,
33    Secondary = 1,
34    Middle = 2,
35    Back = 3,
36    Forward = 4,
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub struct PointerButtons(u8);
41
42impl PointerButtons {
43    pub const NONE: Self = Self(0);
44
45    pub fn new() -> Self {
46        Self::NONE
47    }
48
49    pub fn with(mut self, button: PointerButton) -> Self {
50        self.insert(button);
51        self
52    }
53
54    pub fn insert(&mut self, button: PointerButton) {
55        self.0 |= 1 << (button as u8);
56    }
57
58    pub fn remove(&mut self, button: PointerButton) {
59        self.0 &= !(1 << (button as u8));
60    }
61
62    pub fn contains(&self, button: PointerButton) -> bool {
63        (self.0 & (1 << (button as u8))) != 0
64    }
65}
66
67impl Default for PointerButtons {
68    fn default() -> Self {
69        Self::NONE
70    }
71}
72
73/// Pointer event with consumption tracking for gesture disambiguation.
74///
75/// Events can be consumed by handlers (e.g., scroll) to prevent other handlers
76/// (e.g., clicks) from receiving them. This enables proper gesture disambiguation
77/// matching Jetpack Compose's event consumption pattern.
78#[derive(Clone, Debug)]
79pub struct PointerEvent {
80    pub id: PointerId,
81    pub kind: PointerEventKind,
82    pub phase: PointerPhase,
83    pub position: Point,
84    pub global_position: Point,
85    /// Scroll delta in logical pixels.
86    ///
87    /// This is non-zero for [`PointerEventKind::Scroll`] events and zero for
88    /// button/move events.
89    pub scroll_delta: Point,
90    pub buttons: PointerButtons,
91    /// Platform timestamp of the input sample in milliseconds, when available.
92    ///
93    /// The time base is platform specific (e.g. Android's uptime clock); only
94    /// differences between events of the same gesture are meaningful. Gesture
95    /// velocity trackers must prefer this over the delivery time because
96    /// platforms like Android deliver input batched/frame-aligned: several
97    /// samples arrive back-to-back and delivery-time stamping makes computed
98    /// velocities wildly wrong.
99    pub time_ms: Option<i64>,
100    /// Multiplicative zoom factor for [`PointerEventKind::Zoom`] events
101    /// (`> 1.0` zooms in, `< 1.0` zooms out). `1.0` for all other events.
102    pub zoom_delta: f32,
103    /// Tracks whether this event has been consumed by a handler.
104    /// Shared via Rc<Cell> so consumption can be tracked across copies.
105    consumed: Rc<Cell<bool>>,
106}
107
108impl PointerEvent {
109    pub fn new(kind: PointerEventKind, position: Point, global_position: Point) -> Self {
110        Self {
111            id: 0,
112            kind,
113            phase: match kind {
114                PointerEventKind::Down => PointerPhase::Start,
115                PointerEventKind::Move | PointerEventKind::Enter | PointerEventKind::Exit => {
116                    PointerPhase::Move
117                }
118                PointerEventKind::Up => PointerPhase::End,
119                PointerEventKind::Cancel => PointerPhase::Cancel,
120                PointerEventKind::Scroll | PointerEventKind::Zoom => PointerPhase::Move,
121            },
122            position,
123            global_position,
124            scroll_delta: Point { x: 0.0, y: 0.0 },
125            buttons: PointerButtons::NONE,
126            time_ms: None,
127            zoom_delta: 1.0,
128            consumed: Rc::new(Cell::new(false)),
129        }
130    }
131
132    /// Set the pointer id for this event (`0` is the primary pointer).
133    pub fn with_id(mut self, id: PointerId) -> Self {
134        self.id = id;
135        self
136    }
137
138    /// Set the multiplicative zoom factor for a [`PointerEventKind::Zoom`] event.
139    pub fn with_zoom_delta(mut self, zoom_delta: f32) -> Self {
140        self.zoom_delta = zoom_delta;
141        self
142    }
143
144    /// Set the scroll delta for this event.
145    pub fn with_scroll_delta(mut self, scroll_delta: Point) -> Self {
146        self.scroll_delta = scroll_delta;
147        self
148    }
149
150    /// Set the platform timestamp (milliseconds) for this event.
151    pub fn with_time_ms(mut self, time_ms: Option<i64>) -> Self {
152        self.time_ms = time_ms;
153        self
154    }
155
156    /// Set the buttons state for this event
157    pub fn with_buttons(mut self, buttons: PointerButtons) -> Self {
158        self.buttons = buttons;
159        self
160    }
161
162    /// Mark this event as consumed, preventing other handlers from processing it.
163    ///
164    /// Example: Scroll gestures consume events once dragging starts to prevent
165    /// child buttons from firing clicks.
166    pub fn consume(&self) {
167        self.consumed.set(true);
168    }
169
170    /// Check if this event has been consumed by another handler.
171    ///
172    /// Handlers should check this before processing events. For example,
173    /// clickable should not fire if the event was consumed by a scroll gesture.
174    pub fn is_consumed(&self) -> bool {
175        self.consumed.get()
176    }
177
178    /// Creates a copy of this event with a new local position, sharing the consumption state.
179    pub fn copy_with_local_position(&self, position: Point) -> Self {
180        Self {
181            id: self.id,
182            kind: self.kind,
183            phase: self.phase,
184            position,
185            global_position: self.global_position,
186            scroll_delta: self.scroll_delta,
187            buttons: self.buttons,
188            time_ms: self.time_ms,
189            zoom_delta: self.zoom_delta,
190            consumed: self.consumed.clone(),
191        }
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    fn point(x: f32, y: f32) -> Point {
200        Point { x, y }
201    }
202
203    #[test]
204    fn pointer_event_clones_share_consumed_state() {
205        let event = PointerEvent::new(PointerEventKind::Move, point(1.0, 2.0), point(3.0, 4.0));
206        let cloned = event.clone();
207        assert!(!event.is_consumed());
208        assert!(!cloned.is_consumed());
209
210        cloned.consume();
211
212        assert!(event.is_consumed());
213        assert!(cloned.is_consumed());
214    }
215
216    #[test]
217    fn pointer_event_copy_with_local_position_preserves_consumption_state() {
218        let event = PointerEvent::new(PointerEventKind::Down, point(4.0, 5.0), point(4.0, 5.0));
219        let local = event.copy_with_local_position(point(1.0, 1.0));
220
221        assert_eq!(local.position, point(1.0, 1.0));
222        assert_eq!(local.global_position, event.global_position);
223        assert!(!local.is_consumed());
224
225        event.consume();
226
227        assert!(local.is_consumed());
228    }
229}