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