1use core::time::Duration;
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum ButtonEdge {
49 Press,
51 Release,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum ButtonEvent {
58 Single,
59 Double,
60 Triple,
61 Quad,
62 Long,
63 VeryLong,
64}
65
66#[derive(Debug, Clone, Copy)]
68pub struct ButtonTimings {
69 pub max_click_hold: Duration,
72 pub inter_click_gap: Duration,
75 pub long_press: Duration,
77 pub very_long_press: Option<Duration>,
81}
82
83impl Default for ButtonTimings {
84 fn default() -> Self {
85 Self {
86 max_click_hold: Duration::from_millis(500),
87 inter_click_gap: Duration::from_millis(400),
88 long_press: Duration::from_secs(3),
89 very_long_press: None,
90 }
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95enum State {
96 Idle,
98 Pressed { pressed_at: u64, prior_clicks: u8 },
101 WaitingForNext { released_at: u64, clicks: u8 },
104 LongFired,
106}
107
108#[derive(Debug)]
110pub struct ButtonFsm {
111 timings: ButtonTimings,
112 state: State,
113}
114
115impl ButtonFsm {
116 pub fn new(timings: ButtonTimings) -> Self {
117 Self {
118 timings,
119 state: State::Idle,
120 }
121 }
122
123 pub fn on_edge(&mut self, edge: ButtonEdge, now_ms: u64) -> Option<ButtonEvent> {
126 match (self.state, edge) {
127 (State::Idle, ButtonEdge::Press) => {
129 self.state = State::Pressed {
130 pressed_at: now_ms,
131 prior_clicks: 0,
132 };
133 None
134 }
135
136 (State::WaitingForNext { clicks, .. }, ButtonEdge::Press) => {
138 self.state = State::Pressed {
139 pressed_at: now_ms,
140 prior_clicks: clicks,
141 };
142 None
143 }
144
145 (
147 State::Pressed {
148 pressed_at,
149 prior_clicks,
150 },
151 ButtonEdge::Release,
152 ) => self.classify_release(pressed_at, prior_clicks, now_ms),
153
154 (State::LongFired, ButtonEdge::Release) => {
156 self.state = State::Idle;
157 None
158 }
159
160 (State::Pressed { .. }, ButtonEdge::Press) => None,
162 (State::WaitingForNext { .. }, ButtonEdge::Release) => None,
163 (State::Idle, ButtonEdge::Release) => None,
164 (State::LongFired, ButtonEdge::Press) => None,
165 }
166 }
167
168 pub fn poll(&mut self, now_ms: u64) -> Option<ButtonEvent> {
172 match self.state {
173 State::Pressed { pressed_at, .. }
174 if elapsed(pressed_at, now_ms)
175 >= self
176 .timings
177 .very_long_press
178 .unwrap_or(self.timings.long_press) =>
179 {
180 self.state = State::LongFired;
181 Some(if self.timings.very_long_press.is_some() {
182 ButtonEvent::VeryLong
183 } else {
184 ButtonEvent::Long
185 })
186 }
187
188 State::WaitingForNext {
189 released_at,
190 clicks,
191 } if elapsed(released_at, now_ms) >= self.timings.inter_click_gap => {
192 self.state = State::Idle;
193 click_count_to_event(clicks)
194 }
195
196 _ => None,
197 }
198 }
199
200 pub fn next_deadline(&self) -> Option<u64> {
203 match self.state {
204 State::Pressed { pressed_at, .. } => {
205 let deadline = self
206 .timings
207 .very_long_press
208 .unwrap_or(self.timings.long_press);
209 Some(pressed_at + deadline.as_millis() as u64)
210 }
211 State::WaitingForNext { released_at, .. } => {
212 Some(released_at + self.timings.inter_click_gap.as_millis() as u64)
213 }
214 State::Idle | State::LongFired => None,
215 }
216 }
217
218 fn classify_release(
219 &mut self,
220 pressed_at: u64,
221 prior_clicks: u8,
222 now_ms: u64,
223 ) -> Option<ButtonEvent> {
224 let hold = elapsed(pressed_at, now_ms);
225
226 if let Some(very_long) = self.timings.very_long_press {
227 if hold >= very_long {
228 self.state = State::Idle;
231 return Some(ButtonEvent::VeryLong);
232 }
233 if hold >= self.timings.long_press {
234 self.state = State::Idle;
235 return Some(ButtonEvent::Long);
236 }
237 }
238
239 if self.timings.very_long_press.is_none() && hold >= self.timings.long_press {
240 self.state = State::Idle;
243 return Some(ButtonEvent::Long);
244 }
245
246 if hold > self.timings.max_click_hold {
247 self.state = State::Idle;
250 return click_count_to_event(prior_clicks);
251 }
252
253 let clicks = prior_clicks.saturating_add(1);
255 if clicks >= 4 {
256 self.state = State::Idle;
257 return Some(ButtonEvent::Quad);
258 }
259 self.state = State::WaitingForNext {
260 released_at: now_ms,
261 clicks,
262 };
263 None
264 }
265}
266
267fn elapsed(since_ms: u64, now_ms: u64) -> Duration {
268 Duration::from_millis(now_ms.saturating_sub(since_ms))
269}
270
271fn click_count_to_event(clicks: u8) -> Option<ButtonEvent> {
272 match clicks {
273 1 => Some(ButtonEvent::Single),
274 2 => Some(ButtonEvent::Double),
275 3 => Some(ButtonEvent::Triple),
276 4 => Some(ButtonEvent::Quad),
277 _ => None,
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 fn fsm() -> ButtonFsm {
294 ButtonFsm::new(ButtonTimings {
295 max_click_hold: Duration::from_millis(500),
296 inter_click_gap: Duration::from_millis(400),
297 long_press: Duration::from_secs(5),
298 very_long_press: None,
299 })
300 }
301
302 fn click(
307 fsm: &mut ButtonFsm,
308 down_ms: u64,
309 up_ms: u64,
310 ) -> (Option<ButtonEvent>, Option<ButtonEvent>) {
311 let on_press = fsm.on_edge(ButtonEdge::Press, down_ms);
312 let on_release = fsm.on_edge(ButtonEdge::Release, up_ms);
313 (on_press, on_release)
314 }
315
316 #[test]
317 fn idle_release_is_ignored() {
318 let mut fsm = fsm();
319 assert_eq!(fsm.on_edge(ButtonEdge::Release, 0), None);
320 }
321
322 #[test]
323 fn single_click_fires_after_gap() {
324 let mut fsm = fsm();
325 let (p, r) = click(&mut fsm, 0, 100);
326 assert_eq!(p, None);
327 assert_eq!(r, None);
328
329 assert_eq!(fsm.poll(300), None);
331
332 assert_eq!(fsm.poll(600), Some(ButtonEvent::Single));
334 assert_eq!(fsm.poll(700), None);
335 }
336
337 #[test]
338 fn double_click_fires_after_gap() {
339 let mut fsm = fsm();
340 click(&mut fsm, 0, 100);
341 click(&mut fsm, 200, 300);
342
343 assert_eq!(fsm.poll(400), None);
344 assert_eq!(fsm.poll(800), Some(ButtonEvent::Double));
345 }
346
347 #[test]
348 fn triple_click_fires_after_gap() {
349 let mut fsm = fsm();
353 click(&mut fsm, 0, 100);
354 click(&mut fsm, 200, 300);
355 let (_, third_release) = click(&mut fsm, 400, 500);
356 assert_eq!(third_release, None);
357
358 assert_eq!(fsm.poll(700), None);
360
361 assert_eq!(fsm.poll(900), Some(ButtonEvent::Triple));
363 assert_eq!(fsm.poll(1_000), None);
364 }
365
366 #[test]
367 fn long_press_fires_while_held() {
368 let mut fsm = fsm();
369 assert_eq!(fsm.on_edge(ButtonEdge::Press, 0), None);
370
371 assert_eq!(fsm.poll(4_999), None);
373
374 assert_eq!(fsm.poll(5_000), Some(ButtonEvent::Long));
376
377 assert_eq!(fsm.poll(6_000), None);
379
380 assert_eq!(fsm.on_edge(ButtonEdge::Release, 7_000), None);
382 assert_eq!(fsm.poll(8_000), None);
383 }
384
385 #[test]
386 fn long_press_consumes_prior_clicks() {
387 let mut fsm = fsm();
388 click(&mut fsm, 0, 100); assert_eq!(fsm.on_edge(ButtonEdge::Press, 200), None);
390
391 assert_eq!(fsm.poll(5_200), Some(ButtonEvent::Long));
393 assert_eq!(fsm.on_edge(ButtonEdge::Release, 5_300), None);
394 }
395
396 #[test]
397 fn long_press_fires_on_release_if_poll_was_missed() {
398 let mut fsm = fsm();
399 fsm.on_edge(ButtonEdge::Press, 0);
400 assert_eq!(
402 fsm.on_edge(ButtonEdge::Release, 6_000),
403 Some(ButtonEvent::Long)
404 );
405 }
406
407 #[test]
408 fn two_stage_hold_emits_long_on_release_without_firing_early() {
409 let mut fsm = ButtonFsm::new(ButtonTimings {
410 max_click_hold: Duration::from_millis(500),
411 inter_click_gap: Duration::from_millis(400),
412 long_press: Duration::from_secs(1),
413 very_long_press: Some(Duration::from_secs(4)),
414 });
415 fsm.on_edge(ButtonEdge::Press, 0);
416
417 assert_eq!(fsm.poll(1_000), None);
420 assert_eq!(fsm.poll(2_500), None);
421 assert_eq!(
422 fsm.on_edge(ButtonEdge::Release, 2_500),
423 Some(ButtonEvent::Long)
424 );
425 }
426
427 #[test]
428 fn two_stage_hold_emits_only_very_long_at_second_deadline() {
429 let mut fsm = ButtonFsm::new(ButtonTimings {
430 max_click_hold: Duration::from_millis(500),
431 inter_click_gap: Duration::from_millis(400),
432 long_press: Duration::from_secs(1),
433 very_long_press: Some(Duration::from_secs(4)),
434 });
435 fsm.on_edge(ButtonEdge::Press, 0);
436
437 assert_eq!(fsm.next_deadline(), Some(4_000));
438 assert_eq!(fsm.poll(3_999), None);
439 assert_eq!(fsm.poll(4_000), Some(ButtonEvent::VeryLong));
440 assert_eq!(fsm.poll(5_000), None);
441 assert_eq!(fsm.on_edge(ButtonEdge::Release, 5_100), None);
442 }
443
444 #[test]
445 fn two_stage_very_long_survives_a_missed_poll() {
446 let mut fsm = ButtonFsm::new(ButtonTimings {
447 max_click_hold: Duration::from_millis(500),
448 inter_click_gap: Duration::from_millis(400),
449 long_press: Duration::from_secs(1),
450 very_long_press: Some(Duration::from_secs(4)),
451 });
452 fsm.on_edge(ButtonEdge::Press, 0);
453 assert_eq!(
454 fsm.on_edge(ButtonEdge::Release, 4_500),
455 Some(ButtonEvent::VeryLong)
456 );
457 }
458
459 #[test]
460 fn hold_between_click_and_long_press_discards_click_with_no_priors() {
461 let mut fsm = fsm();
462 fsm.on_edge(ButtonEdge::Press, 0);
464 assert_eq!(fsm.on_edge(ButtonEdge::Release, 1_000), None);
465 assert_eq!(fsm.poll(2_000), None);
466 }
467
468 #[test]
469 fn hold_between_click_and_long_press_emits_prior_clicks() {
470 let mut fsm = fsm();
471 click(&mut fsm, 0, 100);
473 fsm.on_edge(ButtonEdge::Press, 200);
475 let release = fsm.on_edge(ButtonEdge::Release, 1_500);
476 assert_eq!(release, Some(ButtonEvent::Single));
477 }
478
479 #[test]
480 fn next_deadline_tracks_long_press_while_held() {
481 let mut fsm = fsm();
482 fsm.on_edge(ButtonEdge::Press, 1_000);
483 assert_eq!(fsm.next_deadline(), Some(6_000)); }
485
486 #[test]
487 fn next_deadline_tracks_gap_while_waiting() {
488 let mut fsm = fsm();
489 click(&mut fsm, 0, 100);
490 assert_eq!(fsm.next_deadline(), Some(500)); }
492
493 #[test]
494 fn next_deadline_is_none_when_idle() {
495 let fsm = fsm();
496 assert_eq!(fsm.next_deadline(), None);
497 }
498
499 #[test]
500 fn duplicate_press_edges_are_ignored() {
501 let mut fsm = fsm();
502 assert_eq!(fsm.on_edge(ButtonEdge::Press, 0), None);
503 assert_eq!(fsm.on_edge(ButtonEdge::Press, 50), None);
504 assert_eq!(fsm.on_edge(ButtonEdge::Release, 100), None);
506 assert_eq!(fsm.poll(600), Some(ButtonEvent::Single));
507 }
508
509 #[test]
510 fn glitch_release_in_idle_is_ignored() {
511 let mut fsm = fsm();
512 assert_eq!(fsm.on_edge(ButtonEdge::Release, 0), None);
513 assert_eq!(fsm.poll(1_000), None);
514 }
515
516 #[test]
517 fn four_quick_clicks_resolve_as_quad_immediately() {
518 let mut fsm = fsm();
521 click(&mut fsm, 0, 50);
522 click(&mut fsm, 100, 150);
523 let (_, third) = click(&mut fsm, 200, 250);
524 assert_eq!(third, None);
525 let (_, quad) = click(&mut fsm, 300, 350);
526 assert_eq!(quad, Some(ButtonEvent::Quad));
527
528 assert_eq!(fsm.poll(800), None);
530 assert_eq!(fsm.next_deadline(), None);
531 }
532}