use std::time::{Duration, Instant};
const FLOOD: Duration = Duration::from_millis(6);
const NEW_GESTURE: Duration = Duration::from_millis(250);
const HALF_LIFE: Duration = Duration::from_millis(150);
const RAMP: f32 = 0.7;
const FASTEST: f32 = 6.0;
#[derive(Debug, Default)]
pub struct Wheel {
last: Option<(Instant, bool)>,
speed: f32,
carried: f32,
}
impl Wheel {
pub fn lines(&mut self, down: bool, now: Instant) -> usize {
let last = self.last.replace((now, down));
let gap = last
.filter(|(_, was_down)| *was_down == down)
.map(|(at, _)| now.duration_since(at))
.filter(|gap| *gap <= NEW_GESTURE);
let Some(gap) = gap else {
self.speed = 1.0;
self.carried = 0.0;
return 1;
};
if gap < FLOOD {
return 1;
}
let fade = 0.5_f32.powf(gap.as_secs_f32() / HALF_LIFE.as_secs_f32());
self.speed = (1.0 + (self.speed - 1.0 + RAMP) * fade).min(FASTEST);
let lines = self.speed + self.carried;
self.carried = lines.fract();
lines as usize
}
}
#[cfg(test)]
mod tests {
use super::*;
fn run(wheel: &mut Wheel, gap: Duration, count: u32) -> Vec<usize> {
let start = Instant::now();
(0..count)
.map(|i| wheel.lines(true, start + gap * i))
.collect()
}
#[test]
fn a_trackpads_flood_moves_a_line_an_event() {
let lines = run(&mut Wheel::default(), Duration::from_millis(2), 30);
assert!(lines.iter().all(|line| *line == 1), "{lines:?}");
}
#[test]
fn the_first_event_never_jumps() {
assert_eq!(Wheel::default().lines(true, Instant::now()), 1);
}
#[test]
fn a_wheel_spun_quickly_gathers_speed() {
let lines = run(&mut Wheel::default(), Duration::from_millis(30), 40);
assert!(lines[lines.len() - 1] > 1, "{lines:?}");
assert!(
lines.iter().all(|line| *line <= FASTEST as usize + 1),
"{lines:?}"
);
}
#[test]
fn a_slow_wheel_stays_slow() {
let lines = run(&mut Wheel::default(), Duration::from_millis(300), 8);
assert!(lines.iter().all(|line| *line == 1), "{lines:?}");
}
#[test]
fn turning_round_starts_again() {
let mut wheel = Wheel::default();
let start = Instant::now();
let gap = Duration::from_millis(30);
for i in 0..30 {
wheel.lines(true, start + gap * i);
}
assert_eq!(wheel.lines(false, start + gap * 30), 1);
}
}