cranpose_foundation/nodes/input/
types.rs1use super::rotary::RotaryScrollEvent;
2use cranpose_ui_graphics::Point;
3use std::cell::Cell;
4use std::rc::Rc;
5
6pub type PointerId = u64;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum PointerPhase {
10 Start,
11 Move,
12 End,
13 Cancel,
14}
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum PointerEventKind {
18 Down,
19 Move,
20 Up,
21 Cancel,
22 Scroll,
23 Zoom,
26 RotaryScrollPre,
36 RotaryScroll,
39 Enter,
40 Exit,
41}
42
43impl PointerEventKind {
44 pub fn is_rotary(self) -> bool {
46 matches!(self, Self::RotaryScrollPre | Self::RotaryScroll)
47 }
48}
49
50#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
57pub enum PointerSource {
58 Mouse,
60 Touch,
62 Stylus,
64 #[default]
66 Unknown,
67}
68
69impl PointerSource {
70 pub fn is_touch_like(self) -> bool {
73 matches!(self, PointerSource::Touch | PointerSource::Stylus)
74 }
75}
76
77#[repr(u8)]
78#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
79pub enum PointerButton {
80 Primary = 0,
81 Secondary = 1,
82 Middle = 2,
83 Back = 3,
84 Forward = 4,
85}
86
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88pub struct PointerButtons(u8);
89
90impl PointerButtons {
91 pub const NONE: Self = Self(0);
92
93 pub fn new() -> Self {
94 Self::NONE
95 }
96
97 pub fn with(mut self, button: PointerButton) -> Self {
98 self.insert(button);
99 self
100 }
101
102 pub fn insert(&mut self, button: PointerButton) {
103 self.0 |= 1 << (button as u8);
104 }
105
106 pub fn remove(&mut self, button: PointerButton) {
107 self.0 &= !(1 << (button as u8));
108 }
109
110 pub fn contains(&self, button: PointerButton) -> bool {
111 (self.0 & (1 << (button as u8))) != 0
112 }
113}
114
115impl Default for PointerButtons {
116 fn default() -> Self {
117 Self::NONE
118 }
119}
120
121#[derive(Clone, Debug)]
127pub struct PointerEvent {
128 pub id: PointerId,
129 pub kind: PointerEventKind,
130 pub phase: PointerPhase,
131 pub position: Point,
132 pub global_position: Point,
133 pub scroll_delta: Point,
138 pub buttons: PointerButtons,
139 pub time_ms: Option<i64>,
148 pub animation_time_nanos: Option<u64>,
152 pub zoom_delta: f32,
155 pub source: PointerSource,
158 consumed: Rc<Cell<bool>>,
161}
162
163impl PointerEvent {
164 pub fn new(kind: PointerEventKind, position: Point, global_position: Point) -> Self {
165 Self {
166 id: 0,
167 kind,
168 phase: match kind {
169 PointerEventKind::Down => PointerPhase::Start,
170 PointerEventKind::Move | PointerEventKind::Enter | PointerEventKind::Exit => {
171 PointerPhase::Move
172 }
173 PointerEventKind::Up => PointerPhase::End,
174 PointerEventKind::Cancel => PointerPhase::Cancel,
175 PointerEventKind::Scroll
176 | PointerEventKind::Zoom
177 | PointerEventKind::RotaryScrollPre
178 | PointerEventKind::RotaryScroll => PointerPhase::Move,
179 },
180 position,
181 global_position,
182 scroll_delta: Point { x: 0.0, y: 0.0 },
183 buttons: PointerButtons::NONE,
184 time_ms: None,
185 animation_time_nanos: None,
186 zoom_delta: 1.0,
187 source: PointerSource::Unknown,
188 consumed: Rc::new(Cell::new(false)),
189 }
190 }
191
192 pub fn with_id(mut self, id: PointerId) -> Self {
194 self.id = id;
195 self
196 }
197
198 pub fn with_zoom_delta(mut self, zoom_delta: f32) -> Self {
200 self.zoom_delta = zoom_delta;
201 self
202 }
203
204 pub fn with_scroll_delta(mut self, scroll_delta: Point) -> Self {
206 self.scroll_delta = scroll_delta;
207 self
208 }
209
210 pub fn with_time_ms(mut self, time_ms: Option<i64>) -> Self {
212 self.time_ms = time_ms;
213 self
214 }
215
216 pub fn with_animation_time_nanos(mut self, time_nanos: u64) -> Self {
218 self.animation_time_nanos = Some(time_nanos);
219 self
220 }
221
222 pub fn with_buttons(mut self, buttons: PointerButtons) -> Self {
224 self.buttons = buttons;
225 self
226 }
227
228 pub fn with_source(mut self, source: PointerSource) -> Self {
230 self.source = source;
231 self
232 }
233
234 pub fn rotary(kind: PointerEventKind, rotary: RotaryScrollEvent, position: Point) -> Self {
241 debug_assert!(
242 kind.is_rotary(),
243 "PointerEvent::rotary requires a rotary event kind"
244 );
245 Self::new(kind, position, position)
246 .with_scroll_delta(Point {
247 x: rotary.horizontal_scroll_pixels,
248 y: rotary.vertical_scroll_pixels,
249 })
250 .with_time_ms(Some(rotary.uptime_millis as i64))
251 }
252
253 pub fn rotary_scroll_event(&self) -> Option<RotaryScrollEvent> {
258 if !self.kind.is_rotary() {
259 return None;
260 }
261 Some(RotaryScrollEvent {
262 vertical_scroll_pixels: self.scroll_delta.y,
263 horizontal_scroll_pixels: self.scroll_delta.x,
264 uptime_millis: self.time_ms.unwrap_or(0).max(0) as u64,
265 })
266 }
267
268 pub fn consume(&self) {
273 self.consumed.set(true);
274 }
275
276 pub fn is_consumed(&self) -> bool {
281 self.consumed.get()
282 }
283
284 pub fn copy_with_local_position(&self, position: Point) -> Self {
286 Self {
287 id: self.id,
288 kind: self.kind,
289 phase: self.phase,
290 position,
291 global_position: self.global_position,
292 scroll_delta: self.scroll_delta,
293 buttons: self.buttons,
294 time_ms: self.time_ms,
295 animation_time_nanos: self.animation_time_nanos,
296 zoom_delta: self.zoom_delta,
297 source: self.source,
298 consumed: self.consumed.clone(),
299 }
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306
307 fn point(x: f32, y: f32) -> Point {
308 Point { x, y }
309 }
310
311 #[test]
312 fn pointer_event_clones_share_consumed_state() {
313 let event = PointerEvent::new(PointerEventKind::Move, point(1.0, 2.0), point(3.0, 4.0));
314 let cloned = event.clone();
315 assert!(!event.is_consumed());
316 assert!(!cloned.is_consumed());
317
318 cloned.consume();
319
320 assert!(event.is_consumed());
321 assert!(cloned.is_consumed());
322 }
323
324 #[test]
325 fn pointer_event_source_defaults_unknown_and_threads_through_copy() {
326 let event = PointerEvent::new(PointerEventKind::Down, point(1.0, 1.0), point(1.0, 1.0));
327 assert_eq!(event.source, PointerSource::Unknown);
328 assert!(!PointerSource::Unknown.is_touch_like());
329
330 let touch = event.with_source(PointerSource::Touch);
331 assert_eq!(touch.source, PointerSource::Touch);
332 assert!(PointerSource::Touch.is_touch_like());
333 assert!(PointerSource::Stylus.is_touch_like());
334 assert!(!PointerSource::Mouse.is_touch_like());
335
336 let local = touch.copy_with_local_position(point(5.0, 5.0));
338 assert_eq!(local.source, PointerSource::Touch);
339 }
340
341 #[test]
342 fn rotary_payload_round_trips_through_pointer_event() {
343 let rotary = RotaryScrollEvent::new(-64.0, 12.0, 1_234);
344 let event = PointerEvent::rotary(PointerEventKind::RotaryScroll, rotary, point(5.0, 6.0));
345
346 assert_eq!(event.phase, PointerPhase::Move);
347 assert_eq!(event.scroll_delta, point(12.0, -64.0));
348 assert_eq!(event.time_ms, Some(1_234));
349 assert_eq!(event.rotary_scroll_event(), Some(rotary));
350 }
351
352 #[test]
353 fn rotary_payload_survives_local_position_copies() {
354 let rotary = RotaryScrollEvent::new(-8.0, 0.0, 7);
357 let event =
358 PointerEvent::rotary(PointerEventKind::RotaryScrollPre, rotary, point(0.0, 0.0));
359
360 let local = event.copy_with_local_position(point(3.0, 4.0));
361
362 assert_eq!(local.rotary_scroll_event(), Some(rotary));
363 }
364
365 #[test]
366 fn non_rotary_events_have_no_rotary_payload() {
367 let scroll = PointerEvent::new(PointerEventKind::Scroll, point(0.0, 0.0), point(0.0, 0.0))
368 .with_scroll_delta(point(1.0, 2.0));
369
370 assert_eq!(scroll.rotary_scroll_event(), None);
371 assert!(!PointerEventKind::Scroll.is_rotary());
372 assert!(PointerEventKind::RotaryScroll.is_rotary());
373 assert!(PointerEventKind::RotaryScrollPre.is_rotary());
374 }
375
376 #[test]
377 fn pointer_event_copy_with_local_position_preserves_consumption_state() {
378 let event = PointerEvent::new(PointerEventKind::Down, point(4.0, 5.0), point(4.0, 5.0))
379 .with_time_ms(Some(123))
380 .with_animation_time_nanos(456_000_000);
381 let local = event.copy_with_local_position(point(1.0, 1.0));
382
383 assert_eq!(local.position, point(1.0, 1.0));
384 assert_eq!(local.global_position, event.global_position);
385 assert_eq!(local.time_ms, Some(123));
386 assert_eq!(local.animation_time_nanos, Some(456_000_000));
387 assert!(!local.is_consumed());
388
389 event.consume();
390
391 assert!(local.is_consumed());
392 }
393}