Skip to main content

midi_controller/
long_press.rs

1//! Long-press detection: distinguishes short press from long press using timestamps.
2//!
3//! Call `update(edge, now_ms)` on each input poll. Returns a `Gesture` when detected:
4//! - `ShortPress` on release before threshold
5//! - `LongPress` on hold ≥ threshold
6//!
7//! Platform-agnostic: firmware provides `Mono::now()`, simulator provides `Instant::now()`.
8
9/// Default long-press threshold in milliseconds.
10pub const LONG_PRESS_MS: u32 = 500;
11
12/// Input edge from a debounced button.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum Edge {
15    Activate,
16    Deactivate,
17}
18
19/// Detected gesture after timing analysis.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum Gesture {
22    ShortPress,
23    LongPress,
24}
25
26/// Detects long-press gestures using absolute timestamps.
27///
28/// Call `update()` on each poll cycle with the current edge (if any) and the
29/// current monotonic time in milliseconds.
30#[derive(Debug, Clone)]
31pub struct LongPressDetector {
32    press_time: u32,
33    active: bool,
34    fired: bool,
35    threshold_ms: u32,
36}
37
38impl Default for LongPressDetector {
39    fn default() -> Self {
40        Self::new()
41    }
42}
43
44impl LongPressDetector {
45    pub fn new() -> Self {
46        Self {
47            press_time: 0,
48            active: false,
49            fired: false,
50            threshold_ms: LONG_PRESS_MS,
51        }
52    }
53
54    /// Create a detector with a custom threshold.
55    pub fn with_threshold(threshold_ms: u32) -> Self {
56        Self {
57            press_time: 0,
58            active: false,
59            fired: false,
60            threshold_ms,
61        }
62    }
63
64    /// Create a detector in "fired" state — a subsequent Deactivate will be suppressed.
65    /// Used after preset switch to ignore stale button releases.
66    pub fn new_fired() -> Self {
67        Self {
68            press_time: 0,
69            active: false,
70            fired: true,
71            threshold_ms: LONG_PRESS_MS,
72        }
73    }
74
75    /// Update with the current edge and timestamp.
76    /// Returns a gesture when detected.
77    pub fn update(&mut self, edge: Option<Edge>, now_ms: u32) -> Option<Gesture> {
78        match edge {
79            Some(Edge::Activate) => {
80                self.active = true;
81                self.press_time = now_ms;
82                self.fired = false;
83                None
84            }
85            Some(Edge::Deactivate) => {
86                self.active = false;
87                if self.fired {
88                    // Long press already handled, suppress short press
89                    None
90                } else {
91                    Some(Gesture::ShortPress)
92                }
93            }
94            None if self.active && !self.fired => {
95                let held_ms = now_ms.wrapping_sub(self.press_time);
96                if held_ms >= self.threshold_ms {
97                    self.fired = true;
98                    Some(Gesture::LongPress)
99                } else {
100                    None
101                }
102            }
103            _ => None,
104        }
105    }
106
107    /// Returns true while the button is held (before or after firing).
108    pub fn is_active(&self) -> bool {
109        self.active
110    }
111
112    /// Returns true if the long press gesture has already fired.
113    pub fn has_fired(&self) -> bool {
114        self.fired
115    }
116
117    /// Returns ms elapsed since press, or 0 if not active.
118    pub fn held_ms(&self, now_ms: u32) -> u32 {
119        if self.active {
120            now_ms.wrapping_sub(self.press_time)
121        } else {
122            0
123        }
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn short_press_on_release_before_threshold() {
133        let mut det = LongPressDetector::new();
134        assert_eq!(det.update(Some(Edge::Activate), 0), None);
135        // Poll for 100ms — no edge
136        assert_eq!(det.update(None, 100), None);
137        // Release at 200ms (< 500ms threshold)
138        assert_eq!(
139            det.update(Some(Edge::Deactivate), 200),
140            Some(Gesture::ShortPress)
141        );
142    }
143
144    #[test]
145    fn long_press_fires_at_threshold() {
146        let mut det = LongPressDetector::new();
147        det.update(Some(Edge::Activate), 1000);
148        // Still under threshold
149        assert_eq!(det.update(None, 1400), None);
150        assert_eq!(det.update(None, 1499), None);
151        // At threshold (500ms after press at t=1000)
152        assert_eq!(det.update(None, 1500), Some(Gesture::LongPress));
153    }
154
155    #[test]
156    fn release_after_long_press_suppressed() {
157        let mut det = LongPressDetector::new();
158        det.update(Some(Edge::Activate), 0);
159        assert_eq!(det.update(None, 500), Some(Gesture::LongPress));
160        // Release should NOT produce ShortPress
161        assert_eq!(det.update(Some(Edge::Deactivate), 600), None);
162    }
163
164    #[test]
165    fn no_double_fire_on_continued_hold() {
166        let mut det = LongPressDetector::new();
167        det.update(Some(Edge::Activate), 0);
168        assert_eq!(det.update(None, 500), Some(Gesture::LongPress));
169        // Continue holding — should not fire again
170        assert_eq!(det.update(None, 600), None);
171        assert_eq!(det.update(None, 1000), None);
172        assert_eq!(det.update(None, 5000), None);
173    }
174
175    #[test]
176    fn new_fired_suppresses_stale_release() {
177        let mut det = LongPressDetector::new_fired();
178        // Stale release after preset switch
179        assert_eq!(det.update(Some(Edge::Deactivate), 100), None);
180        // Next press works normally
181        assert_eq!(det.update(Some(Edge::Activate), 200), None);
182        assert_eq!(
183            det.update(Some(Edge::Deactivate), 300),
184            Some(Gesture::ShortPress)
185        );
186    }
187
188    #[test]
189    fn custom_threshold() {
190        let mut det = LongPressDetector::with_threshold(1000);
191        det.update(Some(Edge::Activate), 0);
192        assert_eq!(det.update(None, 500), None); // would fire at default 500ms
193        assert_eq!(det.update(None, 999), None);
194        assert_eq!(det.update(None, 1000), Some(Gesture::LongPress));
195    }
196
197    #[test]
198    fn held_ms_reports_duration() {
199        let mut det = LongPressDetector::new();
200        assert_eq!(det.held_ms(0), 0); // not active
201        det.update(Some(Edge::Activate), 100);
202        assert_eq!(det.held_ms(250), 150);
203        assert_eq!(det.held_ms(600), 500);
204    }
205
206    #[test]
207    fn wrapping_timestamp() {
208        let mut det = LongPressDetector::new();
209        // Press near u32::MAX
210        det.update(Some(Edge::Activate), u32::MAX - 100);
211        // Time wraps around
212        assert_eq!(det.update(None, u32::MAX), None); // 100ms held
213                                                      // After wrap: 400ms total (still under threshold)
214        assert_eq!(det.update(None, 299), None); // wrapping_sub gives 400ms
215                                                 // At threshold: 500ms total
216        assert_eq!(det.update(None, 399), Some(Gesture::LongPress));
217    }
218}