1use super::rotary::RotaryScrollEvent;
2use cranpose_ui_graphics::Point;
3use std::cell::{Cell, RefCell};
4use std::rc::Rc;
5
6pub type PointerId = u64;
7
8type PostDispatchAction = Box<dyn FnOnce() -> bool>;
9
10#[derive(Clone)]
11struct DeferredPostDispatch {
12 action: Rc<RefCell<Option<PostDispatchAction>>>,
13}
14
15impl std::fmt::Debug for DeferredPostDispatch {
16 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17 formatter
18 .debug_struct("DeferredPostDispatch")
19 .field("is_pending", &self.action.borrow().is_some())
20 .finish()
21 }
22}
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub enum PointerPhase {
26 Start,
27 Move,
28 End,
29 Cancel,
30}
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum PointerEventKind {
34 Down,
35 Move,
36 Up,
37 Cancel,
38 Scroll,
39 Zoom,
42 RotaryScrollPre,
52 RotaryScroll,
55 Enter,
56 Exit,
57}
58
59impl PointerEventKind {
60 pub fn is_rotary(self) -> bool {
62 matches!(self, Self::RotaryScrollPre | Self::RotaryScroll)
63 }
64}
65
66#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
73pub enum PointerSource {
74 Mouse,
76 Touch,
78 Stylus,
80 #[default]
82 Unknown,
83}
84
85impl PointerSource {
86 pub fn is_touch_like(self) -> bool {
89 matches!(self, PointerSource::Touch | PointerSource::Stylus)
90 }
91}
92
93#[repr(u8)]
94#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
95pub enum PointerButton {
96 Primary = 0,
97 Secondary = 1,
98 Middle = 2,
99 Back = 3,
100 Forward = 4,
101}
102
103#[derive(Clone, Copy, Debug, PartialEq, Eq)]
104pub struct PointerButtons(u8);
105
106impl PointerButtons {
107 pub const NONE: Self = Self(0);
108
109 pub fn new() -> Self {
110 Self::NONE
111 }
112
113 pub fn with(mut self, button: PointerButton) -> Self {
114 self.insert(button);
115 self
116 }
117
118 pub fn insert(&mut self, button: PointerButton) {
119 self.0 |= 1 << (button as u8);
120 }
121
122 pub fn remove(&mut self, button: PointerButton) {
123 self.0 &= !(1 << (button as u8));
124 }
125
126 pub fn contains(&self, button: PointerButton) -> bool {
127 (self.0 & (1 << (button as u8))) != 0
128 }
129}
130
131impl Default for PointerButtons {
132 fn default() -> Self {
133 Self::NONE
134 }
135}
136
137#[derive(Clone, Debug)]
143pub struct PointerEvent {
144 pub id: PointerId,
145 pub kind: PointerEventKind,
146 pub phase: PointerPhase,
147 pub position: Point,
148 pub global_position: Point,
149 pub scroll_delta: Point,
154 pub buttons: PointerButtons,
155 pub time_ms: Option<i64>,
164 pub animation_time_nanos: Option<u64>,
168 pub zoom_delta: f32,
171 pub source: PointerSource,
174 consumed: Rc<Cell<bool>>,
177 deferred_post_dispatch: DeferredPostDispatch,
178}
179
180impl PointerEvent {
181 pub fn new(kind: PointerEventKind, position: Point, global_position: Point) -> Self {
182 Self {
183 id: 0,
184 kind,
185 phase: match kind {
186 PointerEventKind::Down => PointerPhase::Start,
187 PointerEventKind::Move | PointerEventKind::Enter | PointerEventKind::Exit => {
188 PointerPhase::Move
189 }
190 PointerEventKind::Up => PointerPhase::End,
191 PointerEventKind::Cancel => PointerPhase::Cancel,
192 PointerEventKind::Scroll
193 | PointerEventKind::Zoom
194 | PointerEventKind::RotaryScrollPre
195 | PointerEventKind::RotaryScroll => PointerPhase::Move,
196 },
197 position,
198 global_position,
199 scroll_delta: Point { x: 0.0, y: 0.0 },
200 buttons: PointerButtons::NONE,
201 time_ms: None,
202 animation_time_nanos: None,
203 zoom_delta: 1.0,
204 source: PointerSource::Unknown,
205 consumed: Rc::new(Cell::new(false)),
206 deferred_post_dispatch: DeferredPostDispatch {
207 action: Rc::new(RefCell::new(None)),
208 },
209 }
210 }
211
212 pub fn with_id(mut self, id: PointerId) -> Self {
214 self.id = id;
215 self
216 }
217
218 pub fn with_zoom_delta(mut self, zoom_delta: f32) -> Self {
220 self.zoom_delta = zoom_delta;
221 self
222 }
223
224 pub fn with_scroll_delta(mut self, scroll_delta: Point) -> Self {
226 self.scroll_delta = scroll_delta;
227 self
228 }
229
230 pub fn with_time_ms(mut self, time_ms: Option<i64>) -> Self {
232 self.time_ms = time_ms;
233 self
234 }
235
236 pub fn with_animation_time_nanos(mut self, time_nanos: u64) -> Self {
238 self.animation_time_nanos = Some(time_nanos);
239 self
240 }
241
242 pub fn with_buttons(mut self, buttons: PointerButtons) -> Self {
244 self.buttons = buttons;
245 self
246 }
247
248 pub fn with_source(mut self, source: PointerSource) -> Self {
250 self.source = source;
251 self
252 }
253
254 pub fn rotary(kind: PointerEventKind, rotary: RotaryScrollEvent, position: Point) -> Self {
261 debug_assert!(
262 kind.is_rotary(),
263 "PointerEvent::rotary requires a rotary event kind"
264 );
265 Self::new(kind, position, position)
266 .with_scroll_delta(Point {
267 x: rotary.horizontal_scroll_pixels,
268 y: rotary.vertical_scroll_pixels,
269 })
270 .with_time_ms(Some(rotary.uptime_millis as i64))
271 }
272
273 pub fn rotary_scroll_event(&self) -> Option<RotaryScrollEvent> {
278 if !self.kind.is_rotary() {
279 return None;
280 }
281 Some(RotaryScrollEvent {
282 vertical_scroll_pixels: self.scroll_delta.y,
283 horizontal_scroll_pixels: self.scroll_delta.x,
284 uptime_millis: self.time_ms.unwrap_or(0).max(0) as u64,
285 })
286 }
287
288 pub fn consume(&self) {
293 self.consumed.set(true);
294 }
295
296 pub fn is_consumed(&self) -> bool {
301 self.consumed.get()
302 }
303
304 pub fn defer_post_dispatch_action<F>(&self, action: F)
305 where
306 F: FnOnce() -> bool + 'static,
307 {
308 *self.deferred_post_dispatch.action.borrow_mut() = Some(Box::new(action));
309 }
310
311 pub fn finish_post_dispatch(&self) {
312 if self.is_consumed() {
313 self.deferred_post_dispatch.action.borrow_mut().take();
314 return;
315 }
316
317 let Some(action) = self.deferred_post_dispatch.action.borrow_mut().take() else {
318 return;
319 };
320 if action() {
321 self.consume();
322 }
323 }
324
325 pub fn copy_with_local_position(&self, position: Point) -> Self {
327 Self {
328 id: self.id,
329 kind: self.kind,
330 phase: self.phase,
331 position,
332 global_position: self.global_position,
333 scroll_delta: self.scroll_delta,
334 buttons: self.buttons,
335 time_ms: self.time_ms,
336 animation_time_nanos: self.animation_time_nanos,
337 zoom_delta: self.zoom_delta,
338 source: self.source,
339 consumed: self.consumed.clone(),
340 deferred_post_dispatch: self.deferred_post_dispatch.clone(),
341 }
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348
349 fn point(x: f32, y: f32) -> Point {
350 Point { x, y }
351 }
352
353 #[test]
354 fn pointer_event_clones_share_consumed_state() {
355 let event = PointerEvent::new(PointerEventKind::Move, point(1.0, 2.0), point(3.0, 4.0));
356 let cloned = event.clone();
357 assert!(!event.is_consumed());
358 assert!(!cloned.is_consumed());
359
360 cloned.consume();
361
362 assert!(event.is_consumed());
363 assert!(cloned.is_consumed());
364 }
365
366 #[test]
367 fn pointer_event_source_defaults_unknown_and_threads_through_copy() {
368 let event = PointerEvent::new(PointerEventKind::Down, point(1.0, 1.0), point(1.0, 1.0));
369 assert_eq!(event.source, PointerSource::Unknown);
370 assert!(!PointerSource::Unknown.is_touch_like());
371
372 let touch = event.with_source(PointerSource::Touch);
373 assert_eq!(touch.source, PointerSource::Touch);
374 assert!(PointerSource::Touch.is_touch_like());
375 assert!(PointerSource::Stylus.is_touch_like());
376 assert!(!PointerSource::Mouse.is_touch_like());
377
378 let local = touch.copy_with_local_position(point(5.0, 5.0));
380 assert_eq!(local.source, PointerSource::Touch);
381 }
382
383 #[test]
384 fn rotary_payload_round_trips_through_pointer_event() {
385 let rotary = RotaryScrollEvent::new(-64.0, 12.0, 1_234);
386 let event = PointerEvent::rotary(PointerEventKind::RotaryScroll, rotary, point(5.0, 6.0));
387
388 assert_eq!(event.phase, PointerPhase::Move);
389 assert_eq!(event.scroll_delta, point(12.0, -64.0));
390 assert_eq!(event.time_ms, Some(1_234));
391 assert_eq!(event.rotary_scroll_event(), Some(rotary));
392 }
393
394 #[test]
395 fn rotary_payload_survives_local_position_copies() {
396 let rotary = RotaryScrollEvent::new(-8.0, 0.0, 7);
399 let event =
400 PointerEvent::rotary(PointerEventKind::RotaryScrollPre, rotary, point(0.0, 0.0));
401
402 let local = event.copy_with_local_position(point(3.0, 4.0));
403
404 assert_eq!(local.rotary_scroll_event(), Some(rotary));
405 }
406
407 #[test]
408 fn non_rotary_events_have_no_rotary_payload() {
409 let scroll = PointerEvent::new(PointerEventKind::Scroll, point(0.0, 0.0), point(0.0, 0.0))
410 .with_scroll_delta(point(1.0, 2.0));
411
412 assert_eq!(scroll.rotary_scroll_event(), None);
413 assert!(!PointerEventKind::Scroll.is_rotary());
414 assert!(PointerEventKind::RotaryScroll.is_rotary());
415 assert!(PointerEventKind::RotaryScrollPre.is_rotary());
416 }
417
418 #[test]
419 fn pointer_event_copy_with_local_position_preserves_consumption_state() {
420 let event = PointerEvent::new(PointerEventKind::Down, point(4.0, 5.0), point(4.0, 5.0))
421 .with_time_ms(Some(123))
422 .with_animation_time_nanos(456_000_000);
423 let local = event.copy_with_local_position(point(1.0, 1.0));
424
425 assert_eq!(local.position, point(1.0, 1.0));
426 assert_eq!(local.global_position, event.global_position);
427 assert_eq!(local.time_ms, Some(123));
428 assert_eq!(local.animation_time_nanos, Some(456_000_000));
429 assert!(!local.is_consumed());
430
431 event.consume();
432
433 assert!(local.is_consumed());
434 }
435
436 #[test]
437 fn deferred_post_dispatch_action_consumes_when_it_applies() {
438 let event = PointerEvent::new(PointerEventKind::Move, point(0.0, 0.0), point(0.0, 0.0));
439 event.defer_post_dispatch_action(|| true);
440
441 event.finish_post_dispatch();
442
443 assert!(event.is_consumed());
444 }
445
446 #[test]
447 fn deferred_post_dispatch_action_is_replaced_and_discarded_when_consumed() {
448 let event = PointerEvent::new(PointerEventKind::Move, point(0.0, 0.0), point(0.0, 0.0));
449 let first_called = Rc::new(Cell::new(false));
450 let second_called = Rc::new(Cell::new(false));
451 let first_called_in_action = first_called.clone();
452 let second_called_in_action = second_called.clone();
453 event.defer_post_dispatch_action(move || {
454 first_called_in_action.set(true);
455 true
456 });
457 event.defer_post_dispatch_action(move || {
458 second_called_in_action.set(true);
459 true
460 });
461 event.consume();
462
463 event.finish_post_dispatch();
464
465 assert!(!first_called.get());
466 assert!(!second_called.get());
467 assert!(event.is_consumed());
468 }
469}