1use alloc::vec;
7use alloc::vec::Vec;
8
9use dpi::{PhysicalPosition, PhysicalSize};
10use js_sys::{Array, Function, Reflect};
11use ui_events::ScrollDelta;
12use ui_events::keyboard::Modifiers;
13use ui_events::pointer::{
14 PointerButton, PointerButtonEvent, PointerButtons, PointerEvent, PointerId, PointerInfo,
15 PointerOrientation, PointerState, PointerType, PointerUpdate,
16};
17use web_sys::wasm_bindgen::{JsCast, JsValue};
18use web_sys::{
19 Element, Event, MouseEvent, PointerEvent as WebPointerEvent, Touch, TouchEvent, TouchList,
20 WheelEvent,
21};
22
23#[inline]
24#[expect(
25 clippy::cast_possible_truncation,
26 reason = "DOM timestamp is f64 ms; convert to integer ns intentionally"
27)]
28fn ms_to_ns_u64(ms: f64) -> u64 {
29 (ms * 1_000_000.0) as u64
30}
31
32#[inline]
33#[expect(
34 clippy::cast_possible_truncation,
35 reason = "DOM wheel line/page deltas are f64; ui-events stores f32"
36)]
37fn f64_to_f32_delta(v: f64) -> f32 {
38 v as f32
39}
40
41pub fn try_from_web_button(b: i16) -> Option<PointerButton> {
48 Some(match b {
49 0 => PointerButton::Primary,
50 1 => PointerButton::Auxiliary,
53 2 => PointerButton::Secondary,
54 3 => PointerButton::X1,
55 4 => PointerButton::X2,
56 5 => PointerButton::PenEraser,
57 6 => PointerButton::B7,
58 7 => PointerButton::B8,
59 8 => PointerButton::B9,
60 9 => PointerButton::B10,
61 10 => PointerButton::B11,
62 11 => PointerButton::B12,
63 12 => PointerButton::B13,
64 13 => PointerButton::B14,
65 14 => PointerButton::B15,
66 15 => PointerButton::B16,
67 16 => PointerButton::B17,
68 17 => PointerButton::B18,
69 18 => PointerButton::B19,
70 19 => PointerButton::B20,
71 20 => PointerButton::B21,
72 21 => PointerButton::B22,
73 22 => PointerButton::B23,
74 23 => PointerButton::B24,
75 24 => PointerButton::B25,
76 25 => PointerButton::B26,
77 26 => PointerButton::B27,
78 27 => PointerButton::B28,
79 28 => PointerButton::B29,
80 29 => PointerButton::B30,
81 30 => PointerButton::B31,
82 31 => PointerButton::B32,
83 _ => {
84 return None;
85 }
86 })
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use ui_events::pointer::PointerButton;
93
94 #[test]
95 fn web_mouse_button_mapping_matches_dom_spec() {
96 assert_eq!(try_from_web_button(0), Some(PointerButton::Primary));
97 assert_eq!(try_from_web_button(1), Some(PointerButton::Auxiliary));
98 assert_eq!(try_from_web_button(2), Some(PointerButton::Secondary));
99 assert_eq!(try_from_web_button(3), Some(PointerButton::X1));
100 assert_eq!(try_from_web_button(4), Some(PointerButton::X2));
101 assert_eq!(try_from_web_button(-1), None);
102 assert_eq!(try_from_web_button(32), None);
103 }
104}
105
106pub fn from_web_buttons_mask(mask: u16) -> PointerButtons {
108 let mask32 = mask as u32;
110 let mut out = PointerButtons::default();
111 for (i, btn) in NONZERO_VARIANTS.iter().enumerate() {
112 if (mask32 & (1_u32 << i)) != 0 {
113 out.insert(*btn);
114 }
115 }
116 out
117}
118
119const NONZERO_VARIANTS: [PointerButton; 32] = [
120 PointerButton::Primary,
121 PointerButton::Secondary,
122 PointerButton::Auxiliary,
123 PointerButton::X1,
124 PointerButton::X2,
125 PointerButton::PenEraser,
126 PointerButton::B7,
127 PointerButton::B8,
128 PointerButton::B9,
129 PointerButton::B10,
130 PointerButton::B11,
131 PointerButton::B12,
132 PointerButton::B13,
133 PointerButton::B14,
134 PointerButton::B15,
135 PointerButton::B16,
136 PointerButton::B17,
137 PointerButton::B18,
138 PointerButton::B19,
139 PointerButton::B20,
140 PointerButton::B21,
141 PointerButton::B22,
142 PointerButton::B23,
143 PointerButton::B24,
144 PointerButton::B25,
145 PointerButton::B26,
146 PointerButton::B27,
147 PointerButton::B28,
148 PointerButton::B29,
149 PointerButton::B30,
150 PointerButton::B31,
151 PointerButton::B32,
152];
153
154pub fn state_from_mouse_event(e: &MouseEvent, scale_factor: f64) -> PointerState {
162 let css_x = e.client_x() as f64;
163 let css_y = e.client_y() as f64;
164 let buttons = from_web_buttons_mask(e.buttons());
165 let pressure = if buttons.is_empty() { 0.0 } else { 0.5 };
166 let time_ns = ms_to_ns_u64(e.time_stamp());
167 PointerState {
168 time: time_ns, position: PhysicalPosition {
170 x: css_x * scale_factor,
171 y: css_y * scale_factor,
172 },
173 buttons,
174 modifiers: modifiers_from_mouse(e),
175 count: e.detail().clamp(0, 255) as u8,
176 contact_geometry: PhysicalSize {
177 width: 1.0,
178 height: 1.0,
179 },
180 orientation: Default::default(),
181 pressure,
182 tangential_pressure: 0.0,
183 scale_factor,
184 }
185}
186
187fn modifiers_from_mouse(e: &MouseEvent) -> Modifiers {
188 let mut m = Modifiers::default();
189 if e.ctrl_key() {
190 m.insert(Modifiers::CONTROL);
191 }
192 if e.alt_key() {
193 m.insert(Modifiers::ALT);
194 }
195 if e.shift_key() {
196 m.insert(Modifiers::SHIFT);
197 }
198 if e.meta_key() {
199 m.insert(Modifiers::META);
200 }
201 m
202}
203
204fn pointer_info_mouse() -> PointerInfo {
205 PointerInfo {
206 pointer_id: Some(PointerId::PRIMARY),
207 persistent_device_id: None,
208 pointer_type: PointerType::Mouse,
209 }
210}
211
212pub fn down_from_mouse_event(e: &MouseEvent, scale_factor: f64) -> PointerEvent {
216 PointerEvent::Down(PointerButtonEvent {
217 button: try_from_web_button(e.button()),
218 pointer: pointer_info_mouse(),
219 state: state_from_mouse_event(e, scale_factor),
220 })
221}
222
223pub fn up_from_mouse_event(e: &MouseEvent, scale_factor: f64) -> PointerEvent {
227 PointerEvent::Up(PointerButtonEvent {
228 button: try_from_web_button(e.button()),
229 pointer: pointer_info_mouse(),
230 state: state_from_mouse_event(e, scale_factor),
231 })
232}
233
234pub fn move_from_mouse_event(e: &MouseEvent, scale_factor: f64) -> PointerEvent {
238 PointerEvent::Move(PointerUpdate {
239 pointer: pointer_info_mouse(),
240 current: state_from_mouse_event(e, scale_factor),
241 coalesced: Vec::new(),
242 predicted: Vec::new(),
243 })
244}
245
246pub fn enter_from_mouse_event(_e: &MouseEvent) -> PointerEvent {
250 PointerEvent::Enter(pointer_info_mouse())
251}
252
253pub fn leave_from_mouse_event(_e: &MouseEvent) -> PointerEvent {
257 PointerEvent::Leave(pointer_info_mouse())
258}
259
260pub fn scroll_from_wheel_event(e: &WheelEvent, scale_factor: f64) -> PointerEvent {
264 let delta = match e.delta_mode() {
265 WheelEvent::DOM_DELTA_PIXEL => ScrollDelta::PixelDelta(PhysicalPosition {
266 x: e.delta_x() * scale_factor,
267 y: e.delta_y() * scale_factor,
268 }),
269 WheelEvent::DOM_DELTA_LINE => {
270 ScrollDelta::LineDelta(f64_to_f32_delta(e.delta_x()), f64_to_f32_delta(e.delta_y()))
271 }
272 WheelEvent::DOM_DELTA_PAGE => {
273 ScrollDelta::PageDelta(f64_to_f32_delta(e.delta_x()), f64_to_f32_delta(e.delta_y()))
274 }
275 _ => ScrollDelta::PixelDelta(PhysicalPosition { x: 0.0, y: 0.0 }),
276 };
277
278 let me: &MouseEvent = e;
279 PointerEvent::Scroll(ui_events::pointer::PointerScrollEvent {
280 pointer: pointer_info_mouse(),
281 delta,
282 state: state_from_mouse_event(me, scale_factor),
283 })
284}
285
286fn pointer_type_from_str(s: &str) -> PointerType {
289 match s {
290 "mouse" => PointerType::Mouse,
291 "pen" => PointerType::Pen,
292 "touch" => PointerType::Touch,
293 _ => PointerType::Unknown,
294 }
295}
296
297fn pointer_info_from_web_pointer(e: &WebPointerEvent) -> PointerInfo {
298 let id = if e.is_primary() {
299 Some(PointerId::PRIMARY)
300 } else {
301 let raw = e.pointer_id() as u64;
302 PointerId::new(raw.saturating_add(1))
304 };
305 PointerInfo {
306 pointer_id: id,
307 persistent_device_id: None,
308 pointer_type: pointer_type_from_str(&e.pointer_type()),
309 }
310}
311
312fn modifiers_from_pointer(e: &WebPointerEvent) -> Modifiers {
313 let mut m = Modifiers::default();
314 if e.ctrl_key() {
315 m.insert(Modifiers::CONTROL);
316 }
317 if e.alt_key() {
318 m.insert(Modifiers::ALT);
319 }
320 if e.shift_key() {
321 m.insert(Modifiers::SHIFT);
322 }
323 if e.meta_key() {
324 m.insert(Modifiers::META);
325 }
326 m
327}
328
329fn orientation_from_pointer_event(e: &WebPointerEvent) -> PointerOrientation {
330 let obj = e.as_ref();
332 if let (Ok(alt), Ok(azi)) = (
333 Reflect::get(obj, &JsValue::from_str("altitudeAngle")),
334 Reflect::get(obj, &JsValue::from_str("azimuthAngle")),
335 ) {
336 if let (Some(alt), Some(azi)) = (alt.as_f64(), azi.as_f64()) {
337 #[expect(
338 clippy::cast_possible_truncation,
339 reason = "DOM provides f64 radians; ui-events stores orientation as f32"
340 )]
341 return PointerOrientation {
342 altitude: alt as f32,
343 azimuth: azi as f32,
344 };
345 }
346 }
347
348 let tilt_x = (e.tilt_x() as f32).clamp(-89.9, 89.9);
351 let tilt_y = (e.tilt_y() as f32).clamp(-89.9, 89.9);
352 pointer_orientation_from_tilt_degrees(tilt_x, tilt_y)
353}
354
355fn pointer_orientation_from_tilt_degrees(tilt_x_deg: f32, tilt_y_deg: f32) -> PointerOrientation {
356 let tx = tilt_x_deg.to_radians();
357 let ty = tilt_y_deg.to_radians();
358 let x = tx.tan();
359 let y = ty.tan();
360
361 let inv_norm = 1.0 / (x.mul_add(x, y * y) + 1.0).sqrt();
364 let z = inv_norm;
365
366 let altitude = z.asin();
367 let azimuth = if x == 0.0 && y == 0.0 {
368 core::f32::consts::FRAC_PI_2
369 } else {
370 y.atan2(x)
371 };
372
373 PointerOrientation { altitude, azimuth }
374}
375
376pub fn state_from_pointer_event(e: &WebPointerEvent, scale_factor: f64) -> PointerState {
386 let css_x = e.client_x() as f64;
387 let css_y = e.client_y() as f64;
388 let buttons = from_web_buttons_mask(e.buttons());
389 let pressure = e.pressure();
390 let tangential_pressure = e.tangential_pressure();
391 let width = e.width() as f64 * scale_factor;
392 let height = e.height() as f64 * scale_factor;
393 let time_ns = ms_to_ns_u64(e.time_stamp());
394 PointerState {
395 time: time_ns,
396 position: PhysicalPosition {
397 x: css_x * scale_factor,
398 y: css_y * scale_factor,
399 },
400 buttons,
401 modifiers: modifiers_from_pointer(e),
402 count: e.detail().clamp(0, 255) as u8,
403 contact_geometry: PhysicalSize { width, height },
404 orientation: orientation_from_pointer_event(e),
405 pressure,
406 tangential_pressure,
407 scale_factor,
408 }
409}
410
411pub fn down_from_pointer_event(e: &WebPointerEvent, scale_factor: f64) -> PointerEvent {
413 PointerEvent::Down(PointerButtonEvent {
414 button: try_from_web_button(e.button()),
415 pointer: pointer_info_from_web_pointer(e),
416 state: state_from_pointer_event(e, scale_factor),
417 })
418}
419
420pub fn up_from_pointer_event(e: &WebPointerEvent, scale_factor: f64) -> PointerEvent {
422 PointerEvent::Up(PointerButtonEvent {
423 button: try_from_web_button(e.button()),
424 pointer: pointer_info_from_web_pointer(e),
425 state: state_from_pointer_event(e, scale_factor),
426 })
427}
428
429#[derive(Clone, Copy, Debug)]
431pub struct Options {
432 pub scale_factor: f64,
434 pub collect_coalesced: bool,
436 pub collect_predicted: bool,
438}
439
440impl Default for Options {
441 fn default() -> Self {
442 Self {
444 scale_factor: 1.0,
445 collect_coalesced: false,
446 collect_predicted: false,
447 }
448 }
449}
450
451impl Options {
452 pub fn with_scale(mut self, scale: f64) -> Self {
454 self.scale_factor = scale;
455 self
456 }
457 pub fn with_coalesced(mut self, enabled: bool) -> Self {
459 self.collect_coalesced = enabled;
460 self
461 }
462 pub fn with_predicted(mut self, enabled: bool) -> Self {
464 self.collect_predicted = enabled;
465 self
466 }
467}
468
469pub fn move_from_pointer_event(e: &WebPointerEvent, opts: &Options) -> PointerEvent {
471 let pointer = pointer_info_from_web_pointer(e);
472 let current = state_from_pointer_event(e, opts.scale_factor);
473
474 let coalesced_states = if opts.collect_coalesced {
475 get_coalesced_events_safe(e, opts.scale_factor)
476 } else {
477 Vec::new()
478 };
479
480 let predicted_states = if opts.collect_predicted {
481 get_predicted_events_safe(e, opts.scale_factor)
482 } else {
483 Vec::new()
484 };
485
486 PointerEvent::Move(PointerUpdate {
487 pointer,
488 current,
489 coalesced: coalesced_states,
490 predicted: predicted_states,
491 })
492}
493
494fn collect_states_from_array(arr: &Array, scale_factor: f64) -> Vec<PointerState> {
495 let mut out = Vec::new();
496 let len = arr.length();
497 for i in 0..len {
498 let v = arr.get(i);
499 if let Ok(pe) = v.dyn_into::<WebPointerEvent>() {
500 out.push(state_from_pointer_event(&pe, scale_factor));
501 }
502 }
503 out
504}
505
506fn get_coalesced_events_safe(e: &WebPointerEvent, scale_factor: f64) -> Vec<PointerState> {
507 let obj = e.as_ref();
508 let Ok(v) = Reflect::get(
509 obj,
510 &web_sys::wasm_bindgen::JsValue::from_str("getCoalescedEvents"),
511 ) else {
512 return Vec::new();
513 };
514 if !v.is_function() {
515 return Vec::new();
516 }
517 let f: Function = v.unchecked_into();
518 let Ok(jsarr) = f.call0(obj) else {
519 return Vec::new();
520 };
521 let Ok(arr) = jsarr.dyn_into::<Array>() else {
522 return Vec::new();
523 };
524 collect_states_from_array(&arr, scale_factor)
525}
526
527fn get_predicted_events_safe(e: &WebPointerEvent, scale_factor: f64) -> Vec<PointerState> {
528 let obj = e.as_ref();
529 let Ok(v) = Reflect::get(
530 obj,
531 &web_sys::wasm_bindgen::JsValue::from_str("getPredictedEvents"),
532 ) else {
533 return Vec::new();
534 };
535 if !v.is_function() {
536 return Vec::new();
537 }
538 let f: Function = v.unchecked_into();
539 let Ok(jsarr) = f.call0(obj) else {
540 return Vec::new();
541 };
542 let Ok(arr) = jsarr.dyn_into::<Array>() else {
543 return Vec::new();
544 };
545 collect_states_from_array(&arr, scale_factor)
546}
547
548pub fn enter_from_pointer_event(e: &WebPointerEvent) -> PointerEvent {
550 PointerEvent::Enter(pointer_info_from_web_pointer(e))
551}
552
553pub fn leave_from_pointer_event(e: &WebPointerEvent) -> PointerEvent {
555 PointerEvent::Leave(pointer_info_from_web_pointer(e))
556}
557
558pub fn cancel_from_pointer_event(e: &WebPointerEvent) -> PointerEvent {
560 PointerEvent::Cancel(pointer_info_from_web_pointer(e))
561}
562
563pub fn pointer_events_from_touch_event(ev: &TouchEvent, opts: &Options) -> Vec<PointerEvent> {
572 let time_ns = ms_to_ns_u64(ev.time_stamp());
573 let modifiers = modifiers_from_touch(ev);
574
575 let touch_count = pointer_attach_count_from_active_touches(ev.touches().length());
576 let primary_identifier = min_touch_identifier_from_event(ev);
577
578 let type_ = ev.type_();
579 let changed = ev.changed_touches();
580
581 let mut out = Vec::new();
582 let len = changed.length();
583 for i in 0..len {
584 let Some(touch) = changed.item(i) else {
585 continue;
586 };
587 let pointer = pointer_info_from_touch(&touch, primary_identifier);
588 match type_.as_str() {
589 "touchstart" => out.push(PointerEvent::Down(PointerButtonEvent {
590 button: None,
591 pointer,
592 state: state_from_touch(&touch, time_ns, modifiers, touch_count, opts.scale_factor),
593 })),
594 "touchmove" => out.push(PointerEvent::Move(PointerUpdate {
595 pointer,
596 current: state_from_touch(
597 &touch,
598 time_ns,
599 modifiers,
600 touch_count,
601 opts.scale_factor,
602 ),
603 coalesced: Vec::new(),
604 predicted: Vec::new(),
605 })),
606 "touchend" => out.push(PointerEvent::Up(PointerButtonEvent {
607 button: None,
608 pointer,
609 state: state_from_touch_end(
610 &touch,
611 time_ns,
612 modifiers,
613 touch_count,
614 opts.scale_factor,
615 ),
616 })),
617 "touchcancel" => out.push(PointerEvent::Cancel(pointer)),
618 _ => {}
619 }
620 }
621 out
622}
623
624fn modifiers_from_touch(e: &TouchEvent) -> Modifiers {
625 let mut m = Modifiers::default();
626 if e.ctrl_key() {
627 m.insert(Modifiers::CONTROL);
628 }
629 if e.alt_key() {
630 m.insert(Modifiers::ALT);
631 }
632 if e.shift_key() {
633 m.insert(Modifiers::SHIFT);
634 }
635 if e.meta_key() {
636 m.insert(Modifiers::META);
637 }
638 m
639}
640
641fn min_touch_identifier_from_event(ev: &TouchEvent) -> Option<u64> {
642 let mut min = min_touch_identifier(&ev.touches())?;
643 if let Some(changed_min) = min_touch_identifier(&ev.changed_touches()) {
644 min = min.min(changed_min);
645 }
646 Some(min)
647}
648
649fn min_touch_identifier(list: &TouchList) -> Option<u64> {
650 let mut min: Option<u64> = None;
651 let len = list.length();
652 for i in 0..len {
653 let Some(t) = list.item(i) else {
654 continue;
655 };
656 let id = touch_identifier_u64(&t)?;
657 min = Some(min.map_or(id, |m| m.min(id)));
658 }
659 min
660}
661
662fn touch_identifier_u64(touch: &Touch) -> Option<u64> {
663 let id = touch.identifier();
664 if id < 0 {
665 return None;
666 }
667 Some(id as u64)
668}
669
670fn pointer_id_from_touch_identifier(id: i32, primary_identifier: Option<u64>) -> Option<PointerId> {
671 if id < 0 {
672 return None;
673 }
674 let id_u64 = id as u64;
675 if primary_identifier.is_some_and(|p| p == id_u64) {
676 return Some(PointerId::PRIMARY);
677 }
678 PointerId::new(id_u64.saturating_add(2))
679}
680
681fn pointer_attach_count_from_active_touches(active_touches: u32) -> u8 {
682 active_touches.min(255) as u8
683}
684
685fn pointer_info_from_touch(touch: &Touch, primary_identifier: Option<u64>) -> PointerInfo {
686 PointerInfo {
687 pointer_id: pointer_id_from_touch_identifier(touch.identifier(), primary_identifier),
688 persistent_device_id: None,
689 pointer_type: PointerType::Touch,
690 }
691}
692
693fn state_from_touch(
694 touch: &Touch,
695 time_ns: u64,
696 modifiers: Modifiers,
697 touch_count: u8,
698 scale_factor: f64,
699) -> PointerState {
700 let css_x = touch.client_x() as f64;
701 let css_y = touch.client_y() as f64;
702
703 let width_css = (touch.radius_x() as f64 * 2.0).max(1.0);
705 let height_css = (touch.radius_y() as f64 * 2.0).max(1.0);
706
707 let pressure = {
708 let f = touch.force();
709 if f > 0.0 { f } else { 0.5 }
710 };
711
712 PointerState {
713 time: time_ns,
714 position: PhysicalPosition {
715 x: css_x * scale_factor,
716 y: css_y * scale_factor,
717 },
718 buttons: PointerButtons::default(),
719 modifiers,
720 count: touch_count,
721 contact_geometry: PhysicalSize {
722 width: width_css * scale_factor,
723 height: height_css * scale_factor,
724 },
725 orientation: Default::default(),
726 pressure,
727 tangential_pressure: 0.0,
728 scale_factor,
729 }
730}
731
732fn state_from_touch_end(
733 touch: &Touch,
734 time_ns: u64,
735 modifiers: Modifiers,
736 touch_count: u8,
737 scale_factor: f64,
738) -> PointerState {
739 let mut s = state_from_touch(touch, time_ns, modifiers, touch_count, scale_factor);
740 s.pressure = 0.0;
741 s
742}
743
744pub fn pointer_events_from_dom_event(ev: &Event, opts: &Options) -> Vec<PointerEvent> {
747 if let Some(te) = ev.dyn_ref::<TouchEvent>() {
748 let out = pointer_events_from_touch_event(te, opts);
749 if !out.is_empty() {
750 return out;
751 }
752 }
753
754 if let Some(wheel) = ev.dyn_ref::<WheelEvent>() {
755 return vec![scroll_from_wheel_event(wheel, opts.scale_factor)];
756 }
757 if let Some(pe) = ev.dyn_ref::<WebPointerEvent>() {
758 let Some(out) = (match pe.type_().as_str() {
759 "pointerdown" => Some(down_from_pointer_event(pe, opts.scale_factor)),
760 "pointerup" => Some(up_from_pointer_event(pe, opts.scale_factor)),
761 "pointermove" => Some(move_from_pointer_event(pe, opts)),
762 "pointerenter" => Some(enter_from_pointer_event(pe)),
763 "pointerleave" => Some(leave_from_pointer_event(pe)),
764 "pointercancel" => Some(cancel_from_pointer_event(pe)),
765 _ => None,
766 }) else {
767 return Vec::new();
768 };
769 return vec![out];
770 }
771 if let Some(me) = ev.dyn_ref::<MouseEvent>() {
772 let Some(out) = (match me.type_().as_str() {
773 "mousedown" => Some(down_from_mouse_event(me, opts.scale_factor)),
774 "mouseup" => Some(up_from_mouse_event(me, opts.scale_factor)),
775 "mousemove" => Some(move_from_mouse_event(me, opts.scale_factor)),
776 "mouseenter" => Some(enter_from_mouse_event(me)),
777 "mouseleave" => Some(leave_from_mouse_event(me)),
778 _ => None,
779 }) else {
780 return Vec::new();
781 };
782 return vec![out];
783 }
784 Vec::new()
785}
786
787pub fn pointer_event_from_dom_event(ev: &Event, opts: &Options) -> Option<PointerEvent> {
793 let mut events = pointer_events_from_dom_event(ev, opts);
794 if events.is_empty() {
795 return None;
796 }
797 if let Some(primary_idx) = events.iter().position(PointerEvent::is_primary_pointer) {
798 return Some(events.swap_remove(primary_idx));
799 }
800 events.into_iter().next()
801}
802
803pub fn set_pointer_capture(
805 el: &Element,
806 e: &WebPointerEvent,
807) -> Result<(), web_sys::js_sys::JsString> {
808 Ok(el.set_pointer_capture(e.pointer_id())?)
809}
810
811pub fn release_pointer_capture(
813 el: &Element,
814 e: &WebPointerEvent,
815) -> Result<(), web_sys::js_sys::JsString> {
816 Ok(el.release_pointer_capture(e.pointer_id())?)
817}
818
819pub fn has_pointer_capture(el: &Element, e: &WebPointerEvent) -> bool {
821 el.has_pointer_capture(e.pointer_id())
822}
823
824#[cfg(test)]
825mod touch_tests {
826 use super::*;
827
828 #[test]
829 fn touch_identifier_to_pointer_id_mapping() {
830 assert_eq!(
831 pointer_id_from_touch_identifier(0, Some(0)),
832 Some(PointerId::PRIMARY)
833 );
834 assert_eq!(
835 pointer_id_from_touch_identifier(0, Some(1)),
836 PointerId::new(2)
837 );
838 assert_eq!(
839 pointer_id_from_touch_identifier(1, Some(1)),
840 Some(PointerId::PRIMARY)
841 );
842 assert_eq!(
843 pointer_id_from_touch_identifier(1, Some(0)),
844 PointerId::new(3)
845 );
846 assert_eq!(pointer_id_from_touch_identifier(-1, Some(0)), None);
847 }
848
849 #[test]
850 fn touch_count_clamps_to_u8() {
851 assert_eq!(pointer_attach_count_from_active_touches(0), 0);
852 assert_eq!(pointer_attach_count_from_active_touches(1), 1);
853 assert_eq!(pointer_attach_count_from_active_touches(255), 255);
854 assert_eq!(pointer_attach_count_from_active_touches(256), 255);
855 assert_eq!(pointer_attach_count_from_active_touches(u32::MAX), 255);
856 }
857}
858
859#[cfg(test)]
860mod stylus_orientation_tests {
861 use super::*;
862
863 fn assert_approx(a: f32, b: f32, eps: f32) {
864 assert!((a - b).abs() <= eps, "expected {a} ~= {b} (eps={eps})");
865 }
866
867 fn angle_wrap_pi(mut a: f32) -> f32 {
868 const TWO_PI: f32 = core::f32::consts::PI * 2.0;
870 a = (a + core::f32::consts::PI).rem_euclid(TWO_PI) - core::f32::consts::PI;
871 if a <= -core::f32::consts::PI {
872 a += TWO_PI;
873 }
874 a
875 }
876
877 fn assert_azimuth_approx(a: f32, b: f32, eps: f32) {
878 let da = angle_wrap_pi(a - b).abs();
879 assert!(
880 da <= eps,
881 "expected azimuth {a} ~= {b} (|Δ|={da}, eps={eps})"
882 );
883 }
884
885 #[test]
886 fn perpendicular_tilt_maps_to_perpendicular_altitude() {
887 let o = pointer_orientation_from_tilt_degrees(0.0, 0.0);
888 assert!((o.altitude - core::f32::consts::FRAC_PI_2).abs() < 1e-6);
889 }
890
891 #[test]
892 fn azimuth_matches_axes() {
893 let o = pointer_orientation_from_tilt_degrees(30.0, 0.0);
895 assert_azimuth_approx(o.azimuth, 0.0, 1e-6);
896
897 let o = pointer_orientation_from_tilt_degrees(-30.0, 0.0);
899 assert_azimuth_approx(o.azimuth, core::f32::consts::PI, 1e-6);
900
901 let o = pointer_orientation_from_tilt_degrees(0.0, 30.0);
903 assert_azimuth_approx(o.azimuth, core::f32::consts::FRAC_PI_2, 1e-6);
904
905 let o = pointer_orientation_from_tilt_degrees(0.0, -30.0);
907 assert_azimuth_approx(o.azimuth, -core::f32::consts::FRAC_PI_2, 1e-6);
908 }
909
910 #[test]
911 fn increasing_tilt_reduces_altitude() {
912 let o0 = pointer_orientation_from_tilt_degrees(0.0, 0.0);
913 let o1 = pointer_orientation_from_tilt_degrees(30.0, 0.0);
914 let o2 = pointer_orientation_from_tilt_degrees(60.0, 0.0);
915 assert!(o1.altitude < o0.altitude);
916 assert!(o2.altitude < o1.altitude);
917 }
918
919 #[test]
920 fn symmetry_negating_tilt_flips_azimuth_by_pi() {
921 let o = pointer_orientation_from_tilt_degrees(25.0, -10.0);
922 let o_neg = pointer_orientation_from_tilt_degrees(-25.0, 10.0);
923
924 assert_approx(o.altitude, o_neg.altitude, 1e-6);
925 assert_azimuth_approx(o_neg.azimuth, o.azimuth + core::f32::consts::PI, 1e-6);
926 }
927
928 #[test]
929 fn near_ninety_degree_tilt_is_finite_and_near_parallel() {
930 let o = pointer_orientation_from_tilt_degrees(89.9, 0.0);
931 assert!(o.altitude.is_finite());
932 assert!(o.azimuth.is_finite());
933 assert!(o.altitude < 0.01);
934
935 let o = pointer_orientation_from_tilt_degrees(-89.9, 0.0);
936 assert!(o.altitude.is_finite());
937 assert!(o.azimuth.is_finite());
938 assert!(o.altitude < 0.01);
939
940 let o = pointer_orientation_from_tilt_degrees(0.0, 89.9);
941 assert!(o.altitude.is_finite());
942 assert!(o.azimuth.is_finite());
943 assert!(o.altitude < 0.01);
944 }
945}