1use std::{
2 cell::{Cell, RefCell},
3 rc::Rc,
4};
5
6use cranpose_ui_graphics::Point;
7
8use super::rotary::RotaryScrollEvent;
9
10pub type PointerId = u64;
11
12type PostDispatchAction = Box<dyn FnOnce() -> bool>;
13
14#[derive(Clone)]
15struct DeferredPostDispatch {
16 action: Rc<RefCell<Option<PostDispatchAction>>>,
17}
18
19impl std::fmt::Debug for DeferredPostDispatch {
20 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 formatter
22 .debug_struct("DeferredPostDispatch")
23 .field("is_pending", &self.action.borrow().is_some())
24 .finish()
25 }
26}
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum PointerPhase {
30 Start,
31 Move,
32 End,
33 Cancel,
34}
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum PointerEventKind {
38 Down,
39 Move,
40 Up,
41 Cancel,
42 Scroll,
43 Zoom,
46 RotaryScrollPre,
56 RotaryScroll,
59 Enter,
60 Exit,
61}
62
63impl PointerEventKind {
64 pub fn is_rotary(self) -> bool {
66 matches!(self, Self::RotaryScrollPre | Self::RotaryScroll)
67 }
68}
69
70#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
77pub enum PointerSource {
78 Mouse,
80 Touch,
82 Stylus,
84 #[default]
86 Unknown,
87}
88
89impl PointerSource {
90 pub fn is_touch_like(self) -> bool {
93 matches!(self, PointerSource::Touch | PointerSource::Stylus)
94 }
95}
96
97#[repr(u8)]
98#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
99pub enum PointerButton {
100 Primary = 0,
101 Secondary = 1,
102 Middle = 2,
103 Back = 3,
104 Forward = 4,
105}
106
107#[derive(Clone, Copy, Debug, PartialEq, Eq)]
108pub struct PointerButtons(u8);
109
110impl PointerButtons {
111 pub const NONE: Self = Self(0);
112
113 pub fn new() -> Self {
114 Self::NONE
115 }
116
117 pub fn with(mut self, button: PointerButton) -> Self {
118 self.insert(button);
119 self
120 }
121
122 pub fn insert(&mut self, button: PointerButton) {
123 self.0 |= 1 << (button as u8);
124 }
125
126 pub fn remove(&mut self, button: PointerButton) {
127 self.0 &= !(1 << (button as u8));
128 }
129
130 pub fn contains(&self, button: PointerButton) -> bool {
131 (self.0 & (1 << (button as u8))) != 0
132 }
133}
134
135impl Default for PointerButtons {
136 fn default() -> Self {
137 Self::NONE
138 }
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
150pub struct Modifiers {
151 pub shift: bool,
153 pub ctrl: bool,
155 pub alt: bool,
157 pub meta: bool,
159}
160
161impl Modifiers {
162 pub const NONE: Modifiers = Modifiers {
164 shift: false,
165 ctrl: false,
166 alt: false,
167 meta: false,
168 };
169
170 pub fn any(&self) -> bool {
172 self.shift || self.ctrl || self.alt || self.meta
173 }
174
175 pub fn command_or_ctrl(&self) -> bool {
177 #[cfg(target_os = "macos")]
178 {
179 self.meta
180 }
181 #[cfg(not(target_os = "macos"))]
182 {
183 self.ctrl
184 }
185 }
186}
187
188#[derive(Clone, Debug)]
194pub struct PointerEvent {
195 pub id: PointerId,
196 pub kind: PointerEventKind,
197 pub phase: PointerPhase,
198 pub position: Point,
199 pub global_position: Point,
200 pub scroll_delta: Point,
205 pub buttons: PointerButtons,
206 pub time_ms: Option<i64>,
215 pub animation_time_nanos: Option<u64>,
219 pub zoom_delta: f32,
222 pub source: PointerSource,
225 pub modifiers: Option<Modifiers>,
236 consumed: Rc<Cell<bool>>,
239 deferred_post_dispatch: DeferredPostDispatch,
240}
241
242impl PointerEvent {
243 pub fn new(kind: PointerEventKind, position: Point, global_position: Point) -> Self {
244 Self {
245 id: 0,
246 kind,
247 phase: match kind {
248 PointerEventKind::Down => PointerPhase::Start,
249 PointerEventKind::Move | PointerEventKind::Enter | PointerEventKind::Exit => {
250 PointerPhase::Move
251 }
252 PointerEventKind::Up => PointerPhase::End,
253 PointerEventKind::Cancel => PointerPhase::Cancel,
254 PointerEventKind::Scroll
255 | PointerEventKind::Zoom
256 | PointerEventKind::RotaryScrollPre
257 | PointerEventKind::RotaryScroll => PointerPhase::Move,
258 },
259 position,
260 global_position,
261 scroll_delta: Point { x: 0.0, y: 0.0 },
262 buttons: PointerButtons::NONE,
263 time_ms: None,
264 animation_time_nanos: None,
265 zoom_delta: 1.0,
266 source: PointerSource::Unknown,
267 modifiers: None,
268 consumed: Rc::new(Cell::new(false)),
269 deferred_post_dispatch: DeferredPostDispatch {
270 action: Rc::new(RefCell::new(None)),
271 },
272 }
273 }
274
275 pub fn with_id(mut self, id: PointerId) -> Self {
277 self.id = id;
278 self
279 }
280
281 pub fn with_zoom_delta(mut self, zoom_delta: f32) -> Self {
283 self.zoom_delta = zoom_delta;
284 self
285 }
286
287 pub fn with_scroll_delta(mut self, scroll_delta: Point) -> Self {
289 self.scroll_delta = scroll_delta;
290 self
291 }
292
293 pub fn with_time_ms(mut self, time_ms: Option<i64>) -> Self {
295 self.time_ms = time_ms;
296 self
297 }
298
299 pub fn with_animation_time_nanos(mut self, time_nanos: u64) -> Self {
301 self.animation_time_nanos = Some(time_nanos);
302 self
303 }
304
305 pub fn with_buttons(mut self, buttons: PointerButtons) -> Self {
307 self.buttons = buttons;
308 self
309 }
310
311 pub fn with_source(mut self, source: PointerSource) -> Self {
313 self.source = source;
314 self
315 }
316
317 pub fn with_modifiers(mut self, modifiers: Modifiers) -> Self {
323 self.modifiers = Some(modifiers);
324 self
325 }
326
327 pub fn rotary(kind: PointerEventKind, rotary: RotaryScrollEvent, position: Point) -> Self {
334 debug_assert!(
335 kind.is_rotary(),
336 "PointerEvent::rotary requires a rotary event kind"
337 );
338 Self::new(kind, position, position)
339 .with_scroll_delta(Point {
340 x: rotary.horizontal_scroll_pixels,
341 y: rotary.vertical_scroll_pixels,
342 })
343 .with_time_ms(Some(rotary.uptime_millis as i64))
344 }
345
346 pub fn rotary_scroll_event(&self) -> Option<RotaryScrollEvent> {
351 if !self.kind.is_rotary() {
352 return None;
353 }
354 Some(RotaryScrollEvent {
355 vertical_scroll_pixels: self.scroll_delta.y,
356 horizontal_scroll_pixels: self.scroll_delta.x,
357 uptime_millis: self.time_ms.unwrap_or(0).max(0) as u64,
358 })
359 }
360
361 pub fn consume(&self) {
366 self.consumed.set(true);
367 }
368
369 pub fn is_consumed(&self) -> bool {
374 self.consumed.get()
375 }
376
377 pub fn defer_post_dispatch_action<F>(&self, action: F)
378 where
379 F: FnOnce() -> bool + 'static,
380 {
381 *self.deferred_post_dispatch.action.borrow_mut() = Some(Box::new(action));
382 }
383
384 pub fn finish_post_dispatch(&self) {
385 if self.is_consumed() {
386 self.deferred_post_dispatch.action.borrow_mut().take();
387 return;
388 }
389
390 let Some(action) = self.deferred_post_dispatch.action.borrow_mut().take() else {
391 return;
392 };
393 if action() {
394 self.consume();
395 }
396 }
397
398 pub fn copy_with_local_position(&self, position: Point) -> Self {
400 Self {
401 id: self.id,
402 kind: self.kind,
403 phase: self.phase,
404 position,
405 global_position: self.global_position,
406 scroll_delta: self.scroll_delta,
407 buttons: self.buttons,
408 time_ms: self.time_ms,
409 animation_time_nanos: self.animation_time_nanos,
410 zoom_delta: self.zoom_delta,
411 source: self.source,
412 modifiers: self.modifiers,
413 consumed: self.consumed.clone(),
414 deferred_post_dispatch: self.deferred_post_dispatch.clone(),
415 }
416 }
417}
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422
423 fn point(x: f32, y: f32) -> Point {
424 Point { x, y }
425 }
426
427 #[test]
428 fn pointer_event_clones_share_consumed_state() {
429 let event = PointerEvent::new(PointerEventKind::Move, point(1.0, 2.0), point(3.0, 4.0));
430 let cloned = event.clone();
431 assert!(!event.is_consumed());
432 assert!(!cloned.is_consumed());
433
434 cloned.consume();
435
436 assert!(event.is_consumed());
437 assert!(cloned.is_consumed());
438 }
439
440 #[test]
441 fn pointer_event_source_defaults_unknown_and_threads_through_copy() {
442 let event = PointerEvent::new(PointerEventKind::Down, point(1.0, 1.0), point(1.0, 1.0));
443 assert_eq!(event.source, PointerSource::Unknown);
444 assert!(!PointerSource::Unknown.is_touch_like());
445
446 let touch = event.with_source(PointerSource::Touch);
447 assert_eq!(touch.source, PointerSource::Touch);
448 assert!(PointerSource::Touch.is_touch_like());
449 assert!(PointerSource::Stylus.is_touch_like());
450 assert!(!PointerSource::Mouse.is_touch_like());
451
452 let local = touch.copy_with_local_position(point(5.0, 5.0));
454 assert_eq!(local.source, PointerSource::Touch);
455 }
456
457 #[test]
458 fn modifiers_any_is_true_when_any_field_is_set() {
459 assert!(!Modifiers::NONE.any());
460 assert!(!Modifiers::default().any());
461 assert!(Modifiers {
462 shift: true,
463 ..Modifiers::NONE
464 }
465 .any());
466 }
467
468 #[test]
469 fn modifiers_command_or_ctrl_reads_the_platform_appropriate_key() {
470 let ctrl_only = Modifiers {
471 ctrl: true,
472 ..Modifiers::NONE
473 };
474 let meta_only = Modifiers {
475 meta: true,
476 ..Modifiers::NONE
477 };
478
479 #[cfg(target_os = "macos")]
480 {
481 assert!(!ctrl_only.command_or_ctrl());
482 assert!(meta_only.command_or_ctrl());
483 }
484 #[cfg(not(target_os = "macos"))]
485 {
486 assert!(ctrl_only.command_or_ctrl());
487 assert!(!meta_only.command_or_ctrl());
488 }
489 assert!(!Modifiers::NONE.command_or_ctrl());
490 }
491
492 #[test]
493 fn pointer_event_modifiers_default_to_unreported_and_thread_through_copy() {
494 let event = PointerEvent::new(PointerEventKind::Down, point(1.0, 1.0), point(1.0, 1.0));
495 assert_eq!(event.modifiers, None);
498
499 let shift = event.with_modifiers(Modifiers {
500 shift: true,
501 ..Modifiers::NONE
502 });
503 assert_eq!(
504 shift.modifiers,
505 Some(Modifiers {
506 shift: true,
507 ..Modifiers::NONE
508 })
509 );
510
511 let local = shift.copy_with_local_position(point(5.0, 5.0));
514 assert_eq!(local.modifiers, shift.modifiers);
515 }
516
517 #[test]
518 fn rotary_payload_round_trips_through_pointer_event() {
519 let rotary = RotaryScrollEvent::new(-64.0, 12.0, 1_234);
520 let event = PointerEvent::rotary(PointerEventKind::RotaryScroll, rotary, point(5.0, 6.0));
521
522 assert_eq!(event.phase, PointerPhase::Move);
523 assert_eq!(event.scroll_delta, point(12.0, -64.0));
524 assert_eq!(event.time_ms, Some(1_234));
525 assert_eq!(event.rotary_scroll_event(), Some(rotary));
526 }
527
528 #[test]
529 fn rotary_payload_survives_local_position_copies() {
530 let rotary = RotaryScrollEvent::new(-8.0, 0.0, 7);
533 let event =
534 PointerEvent::rotary(PointerEventKind::RotaryScrollPre, rotary, point(0.0, 0.0));
535
536 let local = event.copy_with_local_position(point(3.0, 4.0));
537
538 assert_eq!(local.rotary_scroll_event(), Some(rotary));
539 }
540
541 #[test]
542 fn non_rotary_events_have_no_rotary_payload() {
543 let scroll = PointerEvent::new(PointerEventKind::Scroll, point(0.0, 0.0), point(0.0, 0.0))
544 .with_scroll_delta(point(1.0, 2.0));
545
546 assert_eq!(scroll.rotary_scroll_event(), None);
547 assert!(!PointerEventKind::Scroll.is_rotary());
548 assert!(PointerEventKind::RotaryScroll.is_rotary());
549 assert!(PointerEventKind::RotaryScrollPre.is_rotary());
550 }
551
552 #[test]
553 fn pointer_event_copy_with_local_position_preserves_consumption_state() {
554 let event = PointerEvent::new(PointerEventKind::Down, point(4.0, 5.0), point(4.0, 5.0))
555 .with_time_ms(Some(123))
556 .with_animation_time_nanos(456_000_000);
557 let local = event.copy_with_local_position(point(1.0, 1.0));
558
559 assert_eq!(local.position, point(1.0, 1.0));
560 assert_eq!(local.global_position, event.global_position);
561 assert_eq!(local.time_ms, Some(123));
562 assert_eq!(local.animation_time_nanos, Some(456_000_000));
563 assert!(!local.is_consumed());
564
565 event.consume();
566
567 assert!(local.is_consumed());
568 }
569
570 #[test]
571 fn deferred_post_dispatch_action_consumes_when_it_applies() {
572 let event = PointerEvent::new(PointerEventKind::Move, point(0.0, 0.0), point(0.0, 0.0));
573 event.defer_post_dispatch_action(|| true);
574
575 event.finish_post_dispatch();
576
577 assert!(event.is_consumed());
578 }
579
580 #[test]
581 fn deferred_post_dispatch_action_is_replaced_and_discarded_when_consumed() {
582 let event = PointerEvent::new(PointerEventKind::Move, point(0.0, 0.0), point(0.0, 0.0));
583 let first_called = Rc::new(Cell::new(false));
584 let second_called = Rc::new(Cell::new(false));
585 let first_called_in_action = first_called.clone();
586 let second_called_in_action = second_called.clone();
587 event.defer_post_dispatch_action(move || {
588 first_called_in_action.set(true);
589 true
590 });
591 event.defer_post_dispatch_action(move || {
592 second_called_in_action.set(true);
593 true
594 });
595 event.consume();
596
597 event.finish_post_dispatch();
598
599 assert!(!first_called.get());
600 assert!(!second_called.get());
601 assert!(event.is_consumed());
602 }
603}