use std::collections::HashMap;
use std::hash::Hash;
use std::time::{Duration, Instant};
pub const DEFAULT_MIN_INTERVAL: Duration = Duration::from_millis(80);
#[derive(Debug)]
pub struct KeyRepeatGate<K: Eq + Hash + Copy> {
last_pass: HashMap<K, Instant>,
min_interval: Duration,
}
impl<K: Eq + Hash + Copy> Default for KeyRepeatGate<K> {
fn default() -> Self {
Self::new()
}
}
impl<K: Eq + Hash + Copy> KeyRepeatGate<K> {
#[must_use]
pub fn new() -> Self {
Self::with_interval(DEFAULT_MIN_INTERVAL)
}
#[must_use]
pub fn with_interval(min_interval: Duration) -> Self {
Self {
last_pass: HashMap::new(),
min_interval,
}
}
pub fn try_pass(&mut self, key: K) -> bool {
self.try_pass_at(key, Instant::now())
}
pub fn try_pass_at(&mut self, key: K, now: Instant) -> bool {
match self.last_pass.get(&key) {
Some(prev) if now.duration_since(*prev) < self.min_interval => false,
_ => {
self.last_pass.insert(key, now);
true
}
}
}
pub fn clear(&mut self) {
self.last_pass.clear();
}
pub fn clear_key(&mut self, key: K) {
self.last_pass.remove(&key);
}
#[must_use]
pub fn min_interval(&self) -> Duration {
self.min_interval
}
pub fn set_min_interval(&mut self, interval: Duration) {
self.min_interval = interval;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum TestAction {
FontIncrease,
FontDecrease,
Copy,
}
#[test]
fn first_pass_always_succeeds() {
let mut gate = KeyRepeatGate::new();
assert!(gate.try_pass(TestAction::FontIncrease));
}
#[test]
fn rapid_repeats_within_window_are_dropped() {
let mut gate = KeyRepeatGate::with_interval(Duration::from_millis(80));
let t0 = Instant::now();
assert!(gate.try_pass_at(TestAction::FontIncrease, t0));
assert!(!gate.try_pass_at(TestAction::FontIncrease, t0 + Duration::from_millis(10)));
assert!(!gate.try_pass_at(TestAction::FontIncrease, t0 + Duration::from_millis(50)));
assert!(!gate.try_pass_at(TestAction::FontIncrease, t0 + Duration::from_millis(79)));
}
#[test]
fn pass_after_window_succeeds() {
let mut gate = KeyRepeatGate::with_interval(Duration::from_millis(80));
let t0 = Instant::now();
assert!(gate.try_pass_at(TestAction::FontIncrease, t0));
assert!(gate.try_pass_at(TestAction::FontIncrease, t0 + Duration::from_millis(80)));
assert!(gate.try_pass_at(TestAction::FontIncrease, t0 + Duration::from_millis(160)));
}
#[test]
fn different_keys_have_independent_clocks() {
let mut gate = KeyRepeatGate::with_interval(Duration::from_millis(80));
let t0 = Instant::now();
assert!(gate.try_pass_at(TestAction::FontIncrease, t0));
assert!(!gate.try_pass_at(TestAction::FontIncrease, t0 + Duration::from_millis(10)));
assert!(gate.try_pass_at(TestAction::Copy, t0 + Duration::from_millis(11)));
assert!(gate.try_pass_at(TestAction::FontDecrease, t0 + Duration::from_millis(12)));
}
#[test]
fn runaway_font_storm_drops_to_one_per_window() {
let mut gate = KeyRepeatGate::with_interval(Duration::from_millis(80));
let t0 = Instant::now();
let mut passes = 0;
for i in 0..25 {
let when = t0 + Duration::from_millis(i * 60);
if gate.try_pass_at(TestAction::FontIncrease, when) {
passes += 1;
}
}
assert!(
(12..=14).contains(&passes),
"expected ~13 passes for 25 events @60ms with 80ms gate, got {passes}"
);
}
#[test]
fn clear_resets_all_clocks() {
let mut gate = KeyRepeatGate::with_interval(Duration::from_millis(80));
let t0 = Instant::now();
assert!(gate.try_pass_at(TestAction::FontIncrease, t0));
assert!(!gate.try_pass_at(TestAction::FontIncrease, t0 + Duration::from_millis(10)));
gate.clear();
assert!(gate.try_pass_at(TestAction::FontIncrease, t0 + Duration::from_millis(11)));
}
#[test]
fn clear_key_resets_only_one_clock() {
let mut gate = KeyRepeatGate::with_interval(Duration::from_millis(80));
let t0 = Instant::now();
assert!(gate.try_pass_at(TestAction::FontIncrease, t0));
assert!(gate.try_pass_at(TestAction::Copy, t0));
gate.clear_key(TestAction::FontIncrease);
assert!(gate.try_pass_at(TestAction::FontIncrease, t0 + Duration::from_millis(1)));
assert!(!gate.try_pass_at(TestAction::Copy, t0 + Duration::from_millis(1)));
}
#[test]
fn min_interval_zero_is_a_passthrough() {
let mut gate = KeyRepeatGate::with_interval(Duration::ZERO);
let t0 = Instant::now();
for i in 0..100 {
assert!(gate.try_pass_at(TestAction::FontIncrease, t0 + Duration::from_nanos(i)));
}
}
#[test]
fn set_min_interval_takes_effect_immediately() {
let mut gate = KeyRepeatGate::with_interval(Duration::from_millis(80));
let t0 = Instant::now();
assert!(gate.try_pass_at(TestAction::FontIncrease, t0));
assert!(!gate.try_pass_at(TestAction::FontIncrease, t0 + Duration::from_millis(50)));
gate.set_min_interval(Duration::from_millis(20));
assert!(gate.try_pass_at(TestAction::FontIncrease, t0 + Duration::from_millis(51)));
}
#[test]
fn default_constructor_uses_default_min_interval() {
let gate: KeyRepeatGate<TestAction> = KeyRepeatGate::default();
assert_eq!(gate.min_interval(), DEFAULT_MIN_INTERVAL);
}
}