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>>,
237 deferred_post_dispatch: DeferredPostDispatch,
238}
239
240impl PointerEvent {
241 pub fn new(kind: PointerEventKind, position: Point, global_position: Point) -> Self {
242 Self {
243 id: 0,
244 kind,
245 phase: match kind {
246 PointerEventKind::Down => PointerPhase::Start,
247 PointerEventKind::Move | PointerEventKind::Enter | PointerEventKind::Exit => {
248 PointerPhase::Move
249 }
250 PointerEventKind::Up => PointerPhase::End,
251 PointerEventKind::Cancel => PointerPhase::Cancel,
252 PointerEventKind::Scroll
253 | PointerEventKind::Zoom
254 | PointerEventKind::RotaryScrollPre
255 | PointerEventKind::RotaryScroll => PointerPhase::Move,
256 },
257 position,
258 global_position,
259 scroll_delta: Point { x: 0.0, y: 0.0 },
260 buttons: PointerButtons::NONE,
261 time_ms: None,
262 animation_time_nanos: None,
263 zoom_delta: 1.0,
264 source: PointerSource::Unknown,
265 modifiers: None,
266 consumed: Rc::new(Cell::new(false)),
267 deferred_post_dispatch: DeferredPostDispatch {
268 action: Rc::new(RefCell::new(None)),
269 },
270 }
271 }
272
273 pub fn with_id(mut self, id: PointerId) -> Self {
275 self.id = id;
276 self
277 }
278
279 pub fn with_zoom_delta(mut self, zoom_delta: f32) -> Self {
281 self.zoom_delta = zoom_delta;
282 self
283 }
284
285 pub fn with_scroll_delta(mut self, scroll_delta: Point) -> Self {
287 self.scroll_delta = scroll_delta;
288 self
289 }
290
291 pub fn with_time_ms(mut self, time_ms: Option<i64>) -> Self {
293 self.time_ms = time_ms;
294 self
295 }
296
297 pub fn with_animation_time_nanos(mut self, time_nanos: u64) -> Self {
299 self.animation_time_nanos = Some(time_nanos);
300 self
301 }
302
303 pub fn with_buttons(mut self, buttons: PointerButtons) -> Self {
305 self.buttons = buttons;
306 self
307 }
308
309 pub fn with_source(mut self, source: PointerSource) -> Self {
311 self.source = source;
312 self
313 }
314
315 pub fn with_modifiers(mut self, modifiers: Modifiers) -> Self {
321 self.modifiers = Some(modifiers);
322 self
323 }
324
325 pub fn rotary(kind: PointerEventKind, rotary: RotaryScrollEvent, position: Point) -> Self {
332 debug_assert!(
333 kind.is_rotary(),
334 "PointerEvent::rotary requires a rotary event kind"
335 );
336 Self::new(kind, position, position)
337 .with_scroll_delta(Point {
338 x: rotary.horizontal_scroll_pixels,
339 y: rotary.vertical_scroll_pixels,
340 })
341 .with_time_ms(Some(rotary.uptime_millis as i64))
342 }
343
344 pub fn rotary_scroll_event(&self) -> Option<RotaryScrollEvent> {
349 if !self.kind.is_rotary() {
350 return None;
351 }
352 Some(RotaryScrollEvent {
353 vertical_scroll_pixels: self.scroll_delta.y,
354 horizontal_scroll_pixels: self.scroll_delta.x,
355 uptime_millis: self.time_ms.unwrap_or(0).max(0) as u64,
356 })
357 }
358
359 pub fn consume(&self) {
364 self.consumed.set(true);
365 }
366
367 pub fn is_consumed(&self) -> bool {
372 self.consumed.get()
373 }
374
375 pub fn defer_post_dispatch_action<F>(&self, action: F)
376 where
377 F: FnOnce() -> bool + 'static,
378 {
379 *self.deferred_post_dispatch.action.borrow_mut() = Some(Box::new(action));
380 }
381
382 pub fn finish_post_dispatch(&self) {
383 if self.is_consumed() {
384 self.deferred_post_dispatch.action.borrow_mut().take();
385 return;
386 }
387
388 let Some(action) = self.deferred_post_dispatch.action.borrow_mut().take() else {
389 return;
390 };
391 if action() {
392 self.consume();
393 }
394 }
395
396 pub fn copy_with_local_position(&self, position: Point) -> Self {
398 Self {
399 id: self.id,
400 kind: self.kind,
401 phase: self.phase,
402 position,
403 global_position: self.global_position,
404 scroll_delta: self.scroll_delta,
405 buttons: self.buttons,
406 time_ms: self.time_ms,
407 animation_time_nanos: self.animation_time_nanos,
408 zoom_delta: self.zoom_delta,
409 source: self.source,
410 modifiers: self.modifiers,
411 consumed: self.consumed.clone(),
412 deferred_post_dispatch: self.deferred_post_dispatch.clone(),
413 }
414 }
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420
421 fn point(x: f32, y: f32) -> Point {
422 Point { x, y }
423 }
424
425 #[test]
426 fn pointer_event_clones_share_consumed_state() {
427 let event = PointerEvent::new(PointerEventKind::Move, point(1.0, 2.0), point(3.0, 4.0));
428 let cloned = event.clone();
429 assert!(!event.is_consumed());
430 assert!(!cloned.is_consumed());
431
432 cloned.consume();
433
434 assert!(event.is_consumed());
435 assert!(cloned.is_consumed());
436 }
437
438 #[test]
439 fn pointer_event_source_defaults_unknown_and_threads_through_copy() {
440 let event = PointerEvent::new(PointerEventKind::Down, point(1.0, 1.0), point(1.0, 1.0));
441 assert_eq!(event.source, PointerSource::Unknown);
442 assert!(!PointerSource::Unknown.is_touch_like());
443
444 let touch = event.with_source(PointerSource::Touch);
445 assert_eq!(touch.source, PointerSource::Touch);
446 assert!(PointerSource::Touch.is_touch_like());
447 assert!(PointerSource::Stylus.is_touch_like());
448 assert!(!PointerSource::Mouse.is_touch_like());
449
450 let local = touch.copy_with_local_position(point(5.0, 5.0));
451 assert_eq!(local.source, PointerSource::Touch);
452 }
453
454 #[test]
455 fn modifiers_any_is_true_when_any_field_is_set() {
456 assert!(!Modifiers::NONE.any());
457 assert!(!Modifiers::default().any());
458 assert!(
459 Modifiers {
460 shift: true,
461 ..Modifiers::NONE
462 }
463 .any()
464 );
465 }
466
467 #[test]
468 fn modifiers_command_or_ctrl_reads_the_platform_appropriate_key() {
469 let ctrl_only = Modifiers {
470 ctrl: true,
471 ..Modifiers::NONE
472 };
473 let meta_only = Modifiers {
474 meta: true,
475 ..Modifiers::NONE
476 };
477
478 #[cfg(target_os = "macos")]
479 {
480 assert!(!ctrl_only.command_or_ctrl());
481 assert!(meta_only.command_or_ctrl());
482 }
483 #[cfg(not(target_os = "macos"))]
484 {
485 assert!(ctrl_only.command_or_ctrl());
486 assert!(!meta_only.command_or_ctrl());
487 }
488 assert!(!Modifiers::NONE.command_or_ctrl());
489 }
490
491 #[test]
492 fn pointer_event_modifiers_default_to_unreported_and_thread_through_copy() {
493 let event = PointerEvent::new(PointerEventKind::Down, point(1.0, 1.0), point(1.0, 1.0));
494 assert_eq!(event.modifiers, None);
495
496 let shift = event.with_modifiers(Modifiers {
497 shift: true,
498 ..Modifiers::NONE
499 });
500 assert_eq!(
501 shift.modifiers,
502 Some(Modifiers {
503 shift: true,
504 ..Modifiers::NONE
505 })
506 );
507
508 let local = shift.copy_with_local_position(point(5.0, 5.0));
509 assert_eq!(local.modifiers, shift.modifiers);
510 }
511
512 #[test]
513 fn rotary_payload_round_trips_through_pointer_event() {
514 let rotary = RotaryScrollEvent::new(-64.0, 12.0, 1_234);
515 let event = PointerEvent::rotary(PointerEventKind::RotaryScroll, rotary, point(5.0, 6.0));
516
517 assert_eq!(event.phase, PointerPhase::Move);
518 assert_eq!(event.scroll_delta, point(12.0, -64.0));
519 assert_eq!(event.time_ms, Some(1_234));
520 assert_eq!(event.rotary_scroll_event(), Some(rotary));
521 }
522
523 #[test]
524 fn rotary_payload_survives_local_position_copies() {
525 let rotary = RotaryScrollEvent::new(-8.0, 0.0, 7);
526 let event =
527 PointerEvent::rotary(PointerEventKind::RotaryScrollPre, rotary, point(0.0, 0.0));
528
529 let local = event.copy_with_local_position(point(3.0, 4.0));
530
531 assert_eq!(local.rotary_scroll_event(), Some(rotary));
532 }
533
534 #[test]
535 fn non_rotary_events_have_no_rotary_payload() {
536 let scroll = PointerEvent::new(PointerEventKind::Scroll, point(0.0, 0.0), point(0.0, 0.0))
537 .with_scroll_delta(point(1.0, 2.0));
538
539 assert_eq!(scroll.rotary_scroll_event(), None);
540 assert!(!PointerEventKind::Scroll.is_rotary());
541 assert!(PointerEventKind::RotaryScroll.is_rotary());
542 assert!(PointerEventKind::RotaryScrollPre.is_rotary());
543 }
544
545 #[test]
546 fn pointer_event_copy_with_local_position_preserves_consumption_state() {
547 let event = PointerEvent::new(PointerEventKind::Down, point(4.0, 5.0), point(4.0, 5.0))
548 .with_time_ms(Some(123))
549 .with_animation_time_nanos(456_000_000);
550 let local = event.copy_with_local_position(point(1.0, 1.0));
551
552 assert_eq!(local.position, point(1.0, 1.0));
553 assert_eq!(local.global_position, event.global_position);
554 assert_eq!(local.time_ms, Some(123));
555 assert_eq!(local.animation_time_nanos, Some(456_000_000));
556 assert!(!local.is_consumed());
557
558 event.consume();
559
560 assert!(local.is_consumed());
561 }
562
563 #[test]
564 fn deferred_post_dispatch_action_consumes_when_it_applies() {
565 let event = PointerEvent::new(PointerEventKind::Move, point(0.0, 0.0), point(0.0, 0.0));
566 event.defer_post_dispatch_action(|| true);
567
568 event.finish_post_dispatch();
569
570 assert!(event.is_consumed());
571 }
572
573 #[test]
574 fn deferred_post_dispatch_action_is_replaced_and_discarded_when_consumed() {
575 let event = PointerEvent::new(PointerEventKind::Move, point(0.0, 0.0), point(0.0, 0.0));
576 let first_called = Rc::new(Cell::new(false));
577 let second_called = Rc::new(Cell::new(false));
578 let first_called_in_action = first_called.clone();
579 let second_called_in_action = second_called.clone();
580 event.defer_post_dispatch_action(move || {
581 first_called_in_action.set(true);
582 true
583 });
584 event.defer_post_dispatch_action(move || {
585 second_called_in_action.set(true);
586 true
587 });
588 event.consume();
589
590 event.finish_post_dispatch();
591
592 assert!(!first_called.get());
593 assert!(!second_called.get());
594 assert!(event.is_consumed());
595 }
596}