1use serde::{Deserialize, Serialize};
13
14pub const HOLD_SLOP: f32 = 24.0;
16
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20#[serde(tag = "do", rename_all = "camelCase")]
21pub enum HoldInput {
22 #[serde(rename = "pointerdown")]
23 PointerDown { x: f32, y: f32 },
24 #[serde(rename = "pointermove")]
25 PointerMove { x: f32, y: f32 },
26 #[serde(rename = "pointerup")]
27 PointerUp { x: f32, y: f32 },
28 #[serde(rename = "pointercancel")]
29 PointerCancel,
30 #[serde(rename = "keydown")]
31 KeyDown {
32 key: String,
33 #[serde(default)]
34 repeat: bool,
35 },
36 #[serde(rename = "keyup")]
37 KeyUp { key: String },
38 #[serde(rename = "blur")]
39 Blur,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45pub enum HoldEvent {
46 HoldStart,
47 HoldEnd,
48 HoldCancel,
49}
50
51impl HoldEvent {
52 pub fn name(self) -> &'static str {
54 match self {
55 Self::HoldStart => "holdStart",
56 Self::HoldEnd => "holdEnd",
57 Self::HoldCancel => "holdCancel",
58 }
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Default)]
63enum Source {
64 #[default]
65 None,
66 Pointer,
67 Key(String),
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Default)]
72pub struct HoldTracker {
73 source: Source,
74}
75
76fn within_slop(x: f32, y: f32, width: f32, height: f32) -> bool {
77 x >= -HOLD_SLOP && y >= -HOLD_SLOP && x <= width + HOLD_SLOP && y <= height + HOLD_SLOP
78}
79
80fn hold_key(key: &str) -> bool {
81 matches!(key, " " | "Space" | "Spacebar" | "Enter")
82}
83
84impl HoldTracker {
85 pub fn active(&self) -> bool {
87 self.source != Source::None
88 }
89
90 pub fn input(&mut self, input: &HoldInput, width: f32, height: f32) -> Option<HoldEvent> {
92 let active = self.active();
93 let cancel = |s: &mut Self| {
94 s.source = Source::None;
95 Some(HoldEvent::HoldCancel)
96 };
97 match input {
98 HoldInput::PointerDown { .. } if !active => {
99 self.source = Source::Pointer;
100 Some(HoldEvent::HoldStart)
101 }
102 HoldInput::PointerMove { x, y } if self.source == Source::Pointer && !within_slop(*x, *y, width, height) => cancel(self),
103 HoldInput::PointerUp { x, y } if self.source == Source::Pointer => {
104 self.source = Source::None;
105 Some(if within_slop(*x, *y, width, height) { HoldEvent::HoldEnd } else { HoldEvent::HoldCancel })
106 }
107 HoldInput::PointerCancel if self.source == Source::Pointer => cancel(self),
108 HoldInput::KeyDown { key, .. } if active && key == "Escape" => cancel(self),
109 HoldInput::KeyDown { key, repeat } if !active && !repeat && hold_key(key) => {
110 self.source = Source::Key(key.clone());
111 Some(HoldEvent::HoldStart)
112 }
113 HoldInput::KeyUp { key } if self.source == Source::Key(key.clone()) => {
114 self.source = Source::None;
115 Some(HoldEvent::HoldEnd)
116 }
117 HoldInput::Blur if active => cancel(self),
118 _ => None,
119 }
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn a_slide_away_cancels_and_the_release_fires_nothing() {
129 let mut t = HoldTracker::default();
130 assert_eq!(t.input(&HoldInput::PointerDown { x: 60.0, y: 20.0 }, 120.0, 40.0), Some(HoldEvent::HoldStart));
131 assert_eq!(t.input(&HoldInput::PointerMove { x: -24.0, y: 20.0 }, 120.0, 40.0), None, "the slop is inclusive");
132 assert_eq!(t.input(&HoldInput::PointerMove { x: -30.0, y: 20.0 }, 120.0, 40.0), Some(HoldEvent::HoldCancel));
133 assert_eq!(t.input(&HoldInput::PointerUp { x: 60.0, y: 20.0 }, 120.0, 40.0), None);
134 assert!(!t.active());
135 }
136}