use super::input_history::{InputHistory, ScrollDirection};
use log::debug;
use std::time::Duration;
pub(super) struct ScrollPolicy;
const ADAPTIVE_BASE_CELLS: u32 = 2;
const DENSITY_WINDOW: Duration = Duration::from_millis(300);
const HIGH_WINDOW: Duration = Duration::from_millis(600);
const SUSTAIN_WINDOW: Duration = Duration::from_millis(800);
const MID_THRESHOLD: usize = 2; const HIGH_FAST_THRESHOLD: usize = 6; const HIGH_SUSTAIN_THRESHOLD: usize = 10;
const MULTIPLIER_NORMAL: f32 = 1.0;
const MULTIPLIER_MID: f32 = 1.6;
const MULTIPLIER_HIGH: f32 = 1.8;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ScrollState {
Normal,
Mid,
High,
}
impl ScrollState {
fn multiplier(self) -> f32 {
match self {
Self::Normal => MULTIPLIER_NORMAL,
Self::Mid => MULTIPLIER_MID,
Self::High => MULTIPLIER_HIGH,
}
}
}
impl ScrollPolicy {
pub(super) const fn new() -> Self {
Self
}
pub(super) fn effective_step(
&self,
cell_h: u32,
dir: ScrollDirection,
history: &InputHistory,
) -> u32 {
let state = classify(history, dir);
let base = ADAPTIVE_BASE_CELLS * cell_h;
let effective = ((base as f32) * state.multiplier()).round() as u32;
let density = history.count_in_window(dir, DENSITY_WINDOW) + 1;
let fast = history.count_in_window(dir, HIGH_WINDOW) + 1;
let sustain = history.count_in_window(dir, SUSTAIN_WINDOW) + 1;
let gap_ms = history
.last_gap(dir)
.map(|d| d.as_millis() as i64)
.unwrap_or(-1);
debug!(
"scroll_policy: dir={dir:?} state={state:?} mult={:.2} cell_h={cell_h} base={base} eff={effective} density={density} fast={fast} sustain={sustain} last_gap_ms={gap_ms}",
state.multiplier()
);
effective
}
}
fn classify(history: &InputHistory, dir: ScrollDirection) -> ScrollState {
let density = history.count_in_window(dir, DENSITY_WINDOW) + 1;
if density < MID_THRESHOLD {
return ScrollState::Normal;
}
let fast = history.count_in_window(dir, HIGH_WINDOW) + 1;
let sustain = history.count_in_window(dir, SUSTAIN_WINDOW) + 1;
if fast >= HIGH_FAST_THRESHOLD && sustain >= HIGH_SUSTAIN_THRESHOLD {
ScrollState::High
} else {
ScrollState::Mid
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
const CELL_H: u32 = 24;
#[test]
fn empty_history_is_normal() {
let policy = ScrollPolicy::new();
let h = InputHistory::new(Duration::from_secs(5), 128);
assert_eq!(policy.effective_step(CELL_H, ScrollDirection::Down, &h), 48);
}
#[test]
fn single_prior_press_promotes_to_mid() {
let policy = ScrollPolicy::new();
let mut h = InputHistory::new(Duration::from_secs(5), 128);
let _ = h.record(ScrollDirection::Down, 0);
assert_eq!(policy.effective_step(CELL_H, ScrollDirection::Down, &h), 77);
}
#[test]
fn prior_pair_stays_mid() {
let policy = ScrollPolicy::new();
let mut h = InputHistory::new(Duration::from_secs(5), 128);
let _ = h.record(ScrollDirection::Down, 0);
let _ = h.record(ScrollDirection::Down, 0);
assert_eq!(policy.effective_step(CELL_H, ScrollDirection::Down, &h), 77);
}
#[test]
fn high_requires_both_fast_and_sustain() {
let policy = ScrollPolicy::new();
let mut h = InputHistory::new(Duration::from_secs(5), 128);
for _ in 0..12 {
let _ = h.record(ScrollDirection::Down, 0);
}
assert_eq!(policy.effective_step(CELL_H, ScrollDirection::Down, &h), 86);
}
#[test]
fn high_fast_but_not_sustained_stays_mid() {
let policy = ScrollPolicy::new();
let mut h = InputHistory::new(Duration::from_secs(5), 128);
for _ in 0..6 {
let _ = h.record(ScrollDirection::Down, 0);
}
assert_eq!(policy.effective_step(CELL_H, ScrollDirection::Down, &h), 77);
}
#[test]
fn idle_past_density_window_returns_to_normal() {
let policy = ScrollPolicy::new();
let mut h = InputHistory::new(Duration::from_secs(5), 128);
for _ in 0..12 {
let _ = h.record(ScrollDirection::Down, 0);
}
thread::sleep(DENSITY_WINDOW + Duration::from_millis(50));
assert_eq!(policy.effective_step(CELL_H, ScrollDirection::Down, &h), 48);
}
#[test]
fn opposite_direction_not_counted() {
let policy = ScrollPolicy::new();
let mut h = InputHistory::new(Duration::from_secs(5), 128);
for _ in 0..12 {
let _ = h.record(ScrollDirection::Up, 0);
}
assert_eq!(policy.effective_step(CELL_H, ScrollDirection::Down, &h), 48);
}
#[test]
fn scales_with_cell_height() {
let policy = ScrollPolicy::new();
let h = InputHistory::new(Duration::from_secs(5), 128);
assert_eq!(policy.effective_step(30, ScrollDirection::Down, &h), 60);
}
}