repame_view2d/pick.rs
1//! Pointer picks in world space: the event vocabulary and the click rule.
2//!
3//! Reuse this vocabulary for any surface that reports picks; the emitters
4//! (`actors_view_world_with_events`, a game's own sprite viewport) and the
5//! consumers then agree on what the pointer did.
6
7use glam::Vec2;
8
9pub enum PickEvent {
10 Press {
11 world: Vec2,
12 screen: [f32; 2],
13 button: repose_core::input::PointerButton,
14 },
15 Click {
16 world: Vec2,
17 screen: [f32; 2],
18 button: repose_core::input::PointerButton,
19 },
20 /// Pointer released, always. [`PickEvent::Click`] is gated behind
21 /// [`click_within_slop_dp`], so a drag that ends far from where it started
22 /// emits no click at all; a game that drags something has no other way to
23 /// learn that the drag ended. Emitted before [`PickEvent::Click`], and
24 /// whether or not a click follows.
25 Release {
26 world: Vec2,
27 screen: [f32; 2],
28 button: repose_core::input::PointerButton,
29 },
30 Hover {
31 world: Vec2,
32 /// Window-physical px (y-down), same space as `Press.screen`.
33 /// Games stage this raw and unproject through the live camera
34 /// each frame (screen-anchored aim); the `world` point is baked
35 /// through the camera at event time and goes stale as the
36 /// camera moves.
37 screen: [f32; 2],
38 },
39 /// Touch/pen contact began: pointer id + window-physical px.
40 /// Mouse does not emit these; taps still emit `Click` too.
41 TouchDown { id: u64, screen: [f32; 2] },
42 /// Touch/pen contact moved (same coordinate space as `TouchDown`).
43 TouchMove { id: u64, screen: [f32; 2] },
44 /// Touch/pen contact ended (up / leave).
45 TouchUp { id: u64 },
46}
47
48/// Press slop in dp: pointer-up farther than this from pointer-down
49/// cancels the `Click` (drag-off-cancel). Density-independent: compared
50/// after dividing physical-px deltas by the painted density, so a tap
51/// cancels identically on density 1 and density 3.
52pub const CLICK_SLOP_DP: f32 = 12.0;
53
54/// Dp-space slop check: physical-px points divided by `density` (falling
55/// back to 1.0) before comparing against [`CLICK_SLOP_DP`].
56pub fn click_within_slop_dp(a: [f32; 2], b: [f32; 2], density: f32) -> bool {
57 let d = if density.is_finite() && density > 1e-6 {
58 density
59 } else {
60 1.0
61 };
62 let dx = (a[0] - b[0]) / d;
63 let dy = (a[1] - b[1]) / d;
64 dx * dx + dy * dy <= CLICK_SLOP_DP * CLICK_SLOP_DP
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70
71 #[test]
72 fn click_slop_cancels_drags() {
73 assert!(click_within_slop_dp([100.0, 100.0], [105.0, 105.0], 1.0));
74 assert!(click_within_slop_dp([100.0, 100.0], [105.0, 105.0], 1.0));
75 assert!(!click_within_slop_dp(
76 [100.0, 100.0],
77 [100.0 + CLICK_SLOP_DP + 1.0, 100.0],
78 1.0
79 ));
80 }
81
82 #[test]
83 fn click_slop_is_density_independent() {
84 // 12 dp at density 3 is 36 px: same tap cancels on both densities.
85 assert!(click_within_slop_dp([0.0, 0.0], [36.0, 0.0], 3.0));
86 assert!(!click_within_slop_dp([0.0, 0.0], [37.0, 0.0], 3.0));
87 assert!(click_within_slop_dp([0.0, 0.0], [12.0, 0.0], 1.0));
88 assert!(!click_within_slop_dp([0.0, 0.0], [13.0, 0.0], 1.0));
89 }
90}