1use std::ops::Range;
8use std::time::Duration;
9
10use rand::Rng;
11
12use crate::sample::log_normal;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum WheelStyle {
16 Notched,
18 MacAccelerated,
20}
21
22#[derive(Debug, Clone, PartialEq)]
24pub struct WheelParams {
25 pub notch_px: i32,
27 pub notch_jitter_px: i32,
28 pub notch_gap_ms: Range<u64>,
29 pub accel_first_px: i32,
32 pub accel_second_px: Range<i32>,
33 pub accel_third_px: Range<i32>,
34 pub accel_median_px: f64,
35 pub accel_sigma: f64,
36 pub accel_max_px: f64,
37 pub accel_gap_ms: Range<u64>,
38 pub accel_distance_jitter: f64,
41}
42
43impl Default for WheelParams {
44 fn default() -> Self {
45 Self {
46 notch_px: 100,
48 notch_jitter_px: 0,
49 notch_gap_ms: 30..120,
50 accel_first_px: 2,
51 accel_second_px: 20..60,
52 accel_third_px: 80..200,
53 accel_median_px: 300.0,
54 accel_sigma: 0.4,
55 accel_max_px: 800.0,
56 accel_gap_ms: 10..25,
57 accel_distance_jitter: 0.05,
58 }
59 }
60}
61
62pub fn wheel_events(
65 style: WheelStyle,
66 amount: i32,
67 p: &WheelParams,
68 rng: &mut impl Rng,
69) -> Vec<(i32, Duration)> {
70 match style {
71 WheelStyle::Notched => notches(amount, p, rng),
72 WheelStyle::MacAccelerated => flick(amount, p, rng),
73 }
74}
75
76pub fn scroll_distance(style: WheelStyle, amount: i32, p: &WheelParams, rng: &mut impl Rng) -> i32 {
79 match style {
80 WheelStyle::Notched => amount,
81 WheelStyle::MacAccelerated => {
82 let j = p.accel_distance_jitter;
83 let f = rng.gen_range(1.0 - j..=1.0 + j);
84 ((f64::from(amount) * f).round() as i32).max(1)
85 }
86 }
87}
88
89fn flick(amount: i32, p: &WheelParams, rng: &mut impl Rng) -> Vec<(i32, Duration)> {
90 let mut left = amount.max(0);
91 let mut out = Vec::new();
92 for i in 0.. {
93 if left == 0 {
94 break;
95 }
96 let want = match i {
97 0 => p.accel_first_px,
98 1 => rng.gen_range(p.accel_second_px.clone()),
99 2 => rng.gen_range(p.accel_third_px.clone()),
100 _ => log_normal(p.accel_median_px, p.accel_sigma, rng).min(p.accel_max_px) as i32,
101 };
102 let step = want.max(1).min(left);
103 left -= step;
104 out.push((
105 step,
106 Duration::from_millis(rng.gen_range(p.accel_gap_ms.clone())),
107 ));
108 }
109 out
110}
111
112fn notches(amount: i32, p: &WheelParams, rng: &mut impl Rng) -> Vec<(i32, Duration)> {
113 let mut left = amount.max(0);
114 let mut out = Vec::new();
115 while left > 0 {
116 let j = p.notch_jitter_px;
117 let notch = (p.notch_px + rng.gen_range(-j..=j)).max(1);
118 let step = notch.min(left);
119 left -= step;
120 out.push((
121 step,
122 Duration::from_millis(rng.gen_range(p.notch_gap_ms.clone())),
123 ));
124 }
125 out
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use rand::rngs::StdRng;
132 use rand::SeedableRng;
133
134 fn rng(seed: u64) -> StdRng {
135 StdRng::seed_from_u64(seed)
136 }
137
138 #[test]
139 fn notches_add_up_to_amount() {
140 let p = WheelParams {
141 notch_jitter_px: 15,
142 ..WheelParams::default()
143 };
144 for (seed, amount) in [(1, 300), (2, 1), (3, 2_000), (4, 99)] {
145 let steps = wheel_events(WheelStyle::Notched, amount, &p, &mut rng(seed));
146 assert_eq!(
147 steps.iter().map(|s| s.0).sum::<i32>(),
148 amount,
149 "seed {seed}"
150 );
151 assert!(steps.iter().all(|s| s.0 > 0 && s.0 <= p.notch_px + 15));
152 }
153 let big = wheel_events(WheelStyle::Notched, 2_000, &p, &mut rng(9));
154 let sizes: std::collections::HashSet<i32> = big.iter().map(|s| s.0).collect();
155 assert!(sizes.len() > 3, "every notch the same: {sizes:?}");
156 }
157
158 #[test]
159 fn default_notches_are_exact() {
160 let steps = wheel_events(
161 WheelStyle::Notched,
162 450,
163 &WheelParams::default(),
164 &mut rng(1),
165 );
166 let sizes: Vec<i32> = steps.iter().map(|s| s.0).collect();
167 assert_eq!(sizes, vec![100, 100, 100, 100, 50]);
168 }
169
170 #[test]
171 fn mac_flick_ramps_up_and_adds_up() {
172 let p = WheelParams::default();
173 for (seed, amount) in [(1, 2_000), (2, 300), (3, 5_000), (4, 1)] {
174 let ev = wheel_events(WheelStyle::MacAccelerated, amount, &p, &mut rng(seed));
175 assert_eq!(ev.iter().map(|e| e.0).sum::<i32>(), amount, "seed {seed}");
176 assert_eq!(ev[0].0, p.accel_first_px.min(amount));
177 }
178 let ev = wheel_events(WheelStyle::MacAccelerated, 5_000, &p, &mut rng(9));
179 assert!(
180 ev.iter().any(|e| e.0 > 200),
181 "no large delta — no acceleration"
182 );
183 assert!(ev
184 .iter()
185 .all(|e| p.accel_gap_ms.contains(&(e.1.as_millis() as u64))));
186 }
187
188 #[test]
189 fn mac_distance_varies_notched_does_not() {
190 let p = WheelParams::default();
191 let mut r = rng(11);
192 let d: Vec<i32> = (0..50)
193 .map(|_| scroll_distance(WheelStyle::MacAccelerated, 600, &p, &mut r))
194 .collect();
195 assert!(d.iter().all(|x| (570..=630).contains(x)), "{d:?}");
196 assert!(d.iter().any(|&x| x != 600));
197 assert_eq!(scroll_distance(WheelStyle::Notched, 600, &p, &mut r), 600);
198 }
199}