use std::ops::Range;
use std::time::Duration;
use rand::Rng;
use crate::sample::log_normal;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WheelStyle {
Notched,
MacAccelerated,
}
#[derive(Debug, Clone, PartialEq)]
pub struct WheelParams {
pub notch_px: i32,
pub notch_jitter_px: i32,
pub notch_gap_ms: Range<u64>,
pub accel_first_px: i32,
pub accel_second_px: Range<i32>,
pub accel_third_px: Range<i32>,
pub accel_median_px: f64,
pub accel_sigma: f64,
pub accel_max_px: f64,
pub accel_gap_ms: Range<u64>,
pub accel_distance_jitter: f64,
}
impl Default for WheelParams {
fn default() -> Self {
Self {
notch_px: 100,
notch_jitter_px: 0,
notch_gap_ms: 30..120,
accel_first_px: 2,
accel_second_px: 20..60,
accel_third_px: 80..200,
accel_median_px: 300.0,
accel_sigma: 0.4,
accel_max_px: 800.0,
accel_gap_ms: 10..25,
accel_distance_jitter: 0.05,
}
}
}
pub fn wheel_events(
style: WheelStyle,
amount: i32,
p: &WheelParams,
rng: &mut impl Rng,
) -> Vec<(i32, Duration)> {
match style {
WheelStyle::Notched => notches(amount, p, rng),
WheelStyle::MacAccelerated => flick(amount, p, rng),
}
}
pub fn scroll_distance(style: WheelStyle, amount: i32, p: &WheelParams, rng: &mut impl Rng) -> i32 {
match style {
WheelStyle::Notched => amount,
WheelStyle::MacAccelerated => {
let j = p.accel_distance_jitter;
let f = rng.gen_range(1.0 - j..=1.0 + j);
((f64::from(amount) * f).round() as i32).max(1)
}
}
}
fn flick(amount: i32, p: &WheelParams, rng: &mut impl Rng) -> Vec<(i32, Duration)> {
let mut left = amount.max(0);
let mut out = Vec::new();
for i in 0.. {
if left == 0 {
break;
}
let want = match i {
0 => p.accel_first_px,
1 => rng.gen_range(p.accel_second_px.clone()),
2 => rng.gen_range(p.accel_third_px.clone()),
_ => log_normal(p.accel_median_px, p.accel_sigma, rng).min(p.accel_max_px) as i32,
};
let step = want.max(1).min(left);
left -= step;
out.push((
step,
Duration::from_millis(rng.gen_range(p.accel_gap_ms.clone())),
));
}
out
}
fn notches(amount: i32, p: &WheelParams, rng: &mut impl Rng) -> Vec<(i32, Duration)> {
let mut left = amount.max(0);
let mut out = Vec::new();
while left > 0 {
let j = p.notch_jitter_px;
let notch = (p.notch_px + rng.gen_range(-j..=j)).max(1);
let step = notch.min(left);
left -= step;
out.push((
step,
Duration::from_millis(rng.gen_range(p.notch_gap_ms.clone())),
));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use rand::rngs::StdRng;
use rand::SeedableRng;
fn rng(seed: u64) -> StdRng {
StdRng::seed_from_u64(seed)
}
#[test]
fn notches_add_up_to_amount() {
let p = WheelParams {
notch_jitter_px: 15,
..WheelParams::default()
};
for (seed, amount) in [(1, 300), (2, 1), (3, 2_000), (4, 99)] {
let steps = wheel_events(WheelStyle::Notched, amount, &p, &mut rng(seed));
assert_eq!(
steps.iter().map(|s| s.0).sum::<i32>(),
amount,
"seed {seed}"
);
assert!(steps.iter().all(|s| s.0 > 0 && s.0 <= p.notch_px + 15));
}
let big = wheel_events(WheelStyle::Notched, 2_000, &p, &mut rng(9));
let sizes: std::collections::HashSet<i32> = big.iter().map(|s| s.0).collect();
assert!(sizes.len() > 3, "every notch the same: {sizes:?}");
}
#[test]
fn default_notches_are_exact() {
let steps = wheel_events(
WheelStyle::Notched,
450,
&WheelParams::default(),
&mut rng(1),
);
let sizes: Vec<i32> = steps.iter().map(|s| s.0).collect();
assert_eq!(sizes, vec![100, 100, 100, 100, 50]);
}
#[test]
fn mac_flick_ramps_up_and_adds_up() {
let p = WheelParams::default();
for (seed, amount) in [(1, 2_000), (2, 300), (3, 5_000), (4, 1)] {
let ev = wheel_events(WheelStyle::MacAccelerated, amount, &p, &mut rng(seed));
assert_eq!(ev.iter().map(|e| e.0).sum::<i32>(), amount, "seed {seed}");
assert_eq!(ev[0].0, p.accel_first_px.min(amount));
}
let ev = wheel_events(WheelStyle::MacAccelerated, 5_000, &p, &mut rng(9));
assert!(
ev.iter().any(|e| e.0 > 200),
"no large delta — no acceleration"
);
assert!(ev
.iter()
.all(|e| p.accel_gap_ms.contains(&(e.1.as_millis() as u64))));
}
#[test]
fn mac_distance_varies_notched_does_not() {
let p = WheelParams::default();
let mut r = rng(11);
let d: Vec<i32> = (0..50)
.map(|_| scroll_distance(WheelStyle::MacAccelerated, 600, &p, &mut r))
.collect();
assert!(d.iter().all(|x| (570..=630).contains(x)), "{d:?}");
assert!(d.iter().any(|&x| x != 600));
assert_eq!(scroll_distance(WheelStyle::Notched, 600, &p, &mut r), 600);
}
}