use emath::Rangef;
use crate::{MouseWheelSource, style::KineticScrollStyle};
const TOUCH_RUBBER_BAND_SLOPE: f32 = 0.55;
const TOUCH_MAX_STRETCH: f32 = 1.0;
const TRACKPAD_RUBBER_BAND_SLOPE: f32 = 0.3;
const TRACKPAD_MAX_STRETCH: f32 = 1.0 / 3.0;
const RUBBER_BAND_TIME: f32 = 0.1;
pub fn scroll_bounds(max_offset: f32) -> Rangef {
Rangef::new(0.0, max_offset.max(0.0))
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct WheelScroll {
pub delta: f32,
pub source: Option<MouseWheelSource>,
pub can_chain_to_parent: bool,
pub dt: f32,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AxisPhysics {
pub bounds: Rangef,
pub viewport_extent: f32,
pub style: KineticScrollStyle,
}
impl AxisPhysics {
pub fn clamp(&self, offset: f32) -> f32 {
self.bounds.clamp(offset)
}
pub fn overscroll(&self, offset: f32) -> f32 {
offset - self.clamp(offset)
}
pub fn is_overscrolled(&self, offset: f32) -> bool {
self.overscroll(offset) != 0.0
}
pub fn is_rubber_banding(&self, offset: f32) -> bool {
self.style.rubber_band && self.is_overscrolled(offset)
}
pub fn drag(&self, offset: f32, delta: f32) -> f32 {
if self.style.rubber_band {
rubber_band_drag(
offset,
delta,
self.bounds,
self.viewport_extent * TOUCH_MAX_STRETCH,
TOUCH_RUBBER_BAND_SLOPE,
)
} else {
offset + delta
}
}
pub fn step(&self, offset: &mut f32, vel: &mut f32, dt: f32, hold: bool) -> bool {
let KineticScrollStyle {
decay_time,
stop_distance,
rubber_band,
} = self.style;
let edge = self.clamp(*offset);
let overscroll = *offset - edge;
if rubber_band && overscroll != 0.0 {
if hold {
return false;
}
let (new_overscroll, new_vel) =
critically_damped_spring_step(overscroll, *vel, dt, RUBBER_BAND_TIME);
let settled = new_overscroll.abs() < stop_distance
&& (new_vel * RUBBER_BAND_TIME).abs() < stop_distance;
if settled {
*offset = edge;
*vel = 0.0;
false
} else {
*offset = edge + new_overscroll;
*vel = new_vel;
true
}
} else {
let remaining_distance = vel.abs() * decay_time;
if remaining_distance < stop_distance || !remaining_distance.is_finite() {
*vel = 0.0;
false
} else {
let new_vel = *vel * (-dt / decay_time).exp();
*offset += decay_time * (*vel - new_vel);
*vel = new_vel;
true
}
}
}
pub fn wheel(
&self,
offset: &mut f32,
vel: &mut f32,
scroll: WheelScroll,
bounced_this_momentum: &mut bool,
) -> bool {
let WheelScroll {
delta,
source,
can_chain_to_parent,
dt,
} = scroll;
let is_momentum = source == Some(MouseWheelSource::Momentum);
let is_trackpad = matches!(
source,
Some(MouseWheelSource::Trackpad | MouseWheelSource::Momentum)
);
if !is_momentum {
*bounced_this_momentum = false;
}
if delta == 0.0 {
return false;
}
let can_scroll = (delta < 0.0 && self.bounds.min < *offset)
|| (0.0 < delta && *offset < self.bounds.max);
let rubber_band = self.style.rubber_band
&& is_trackpad
&& (can_scroll
|| self.is_overscrolled(*offset)
|| !can_chain_to_parent
|| *bounced_this_momentum);
if !(rubber_band || can_scroll) {
return false;
}
if rubber_band && self.overscroll(*offset) * delta < 0.0 {
*offset += delta;
*vel = 0.0;
} else if rubber_band && is_momentum {
let unclamped = *offset + delta;
if *bounced_this_momentum {
} else if self.bounds.contains(unclamped) {
*offset = unclamped;
} else {
if !self.is_overscrolled(*offset) {
*offset = self.clamp(unclamped);
}
*vel = self.bounce_velocity(*offset, delta, dt);
*bounced_this_momentum = true;
}
} else if rubber_band {
*offset = rubber_band_drag(
*offset,
delta,
self.bounds,
self.viewport_extent * TRACKPAD_MAX_STRETCH,
TRACKPAD_RUBBER_BAND_SLOPE,
);
*vel = 0.0;
} else {
*offset = self.clamp(*offset + delta);
}
true
}
fn bounce_velocity(&self, offset: f32, delta: f32, dt: f32) -> f32 {
let room =
(self.viewport_extent * TRACKPAD_MAX_STRETCH - self.overscroll(offset).abs()).max(0.0);
let max_speed = room * core::f32::consts::E / RUBBER_BAND_TIME;
let speed = if 0.0 < dt { delta.abs() / dt } else { 0.0 };
speed.min(max_speed).copysign(delta)
}
}
fn rubber_band_drag(
mut offset: f32,
mut delta: f32,
bounds: Rangef,
viewport_extent: f32,
slope: f32,
) -> f32 {
if bounds.contains(offset) {
let unclamped = offset + delta;
offset = bounds.clamp(unclamped);
delta = unclamped - offset;
}
if delta == 0.0 {
return offset;
}
let edge = bounds.clamp(offset);
let overscroll = offset - edge;
let fraction = if 0.0 < viewport_extent {
(overscroll.abs() / viewport_extent).min(1.0)
} else {
1.0
};
let factor = slope * (1.0 - fraction).powi(2);
let new_overscroll = overscroll + delta * factor;
if overscroll != 0.0 && new_overscroll.signum() != overscroll.signum() {
let delta_to_reach_edge = -overscroll / factor;
let remaining = delta - delta_to_reach_edge;
bounds.clamp(edge + remaining)
} else {
edge + new_overscroll
}
}
fn critically_damped_spring_step(x0: f32, v0: f32, dt: f32, time_constant: f32) -> (f32, f32) {
if time_constant <= 0.0 {
return (0.0, 0.0);
}
let w = 1.0 / time_constant;
let e = (-w * dt).exp();
let a = v0 + w * x0;
((x0 + a * dt) * e, (v0 - a * w * dt) * e)
}
#[cfg(test)]
mod tests {
use super::*;
const DT: f32 = 1.0 / 60.0;
fn physics(rubber_band: bool) -> AxisPhysics {
AxisPhysics {
bounds: Rangef::new(0.0, 100.0),
viewport_extent: 200.0,
style: KineticScrollStyle {
rubber_band,
..Default::default()
},
}
}
fn wheel(
delta: f32,
source: Option<MouseWheelSource>,
can_chain_to_parent: bool,
) -> WheelScroll {
WheelScroll {
delta,
source,
can_chain_to_parent,
dt: DT,
}
}
fn settle(p: &AxisPhysics, offset: &mut f32, vel: &mut f32) -> f32 {
let mut min_offset = *offset;
for _ in 0..1000 {
if !p.step(offset, vel, DT, false) {
return min_offset;
}
min_offset = min_offset.min(*offset);
}
panic!("Should settle eventually");
}
#[test]
fn drag_resists_past_the_edge() {
let p = physics(true);
let offset = p.drag(0.0, -10.0);
assert!(
(offset - -10.0 * TOUCH_RUBBER_BAND_SLOPE).abs() < 1e-4,
"{offset}"
);
assert!(p.is_rubber_banding(offset));
let mut offset = 0.0;
let mut prev_step = f32::INFINITY;
for _ in 0..1000 {
let new_offset = p.drag(offset, -10.0);
let step = offset - new_offset;
assert!(0.0 <= step && step <= prev_step, "{step} > {prev_step}");
prev_step = step;
offset = new_offset;
}
assert!(
-p.viewport_extent < offset && offset < -p.viewport_extent * 0.9,
"{offset}"
);
let offset = p.drag(5.0, -15.0);
assert!(
(offset - -10.0 * TOUCH_RUBBER_BAND_SLOPE).abs() < 1e-4,
"{offset}"
);
}
#[test]
fn drag_back_into_bounds() {
let p = physics(true);
let offset = p.drag(-5.5, 20.0);
assert!((offset - 9.43).abs() < 0.05, "{offset}");
}
#[test]
fn step_coasts_and_stops() {
let p = physics(true);
let (mut offset, mut vel) = (10.0, 100.0);
settle(&p, &mut offset, &mut vel);
let expected = 10.0 + 100.0 * p.style.decay_time;
assert!((offset - expected).abs() < 1.0, "{offset}");
assert_eq!(vel, 0.0);
}
#[test]
fn step_springs_back_without_overshoot() {
let p = physics(true);
let (mut offset, mut vel) = (-50.0, 0.0);
let mut prev = offset;
while p.step(&mut offset, &mut vel, DT, false) {
assert!(prev <= offset && offset <= 0.0, "{prev} -> {offset}");
prev = offset;
}
assert_eq!((offset, vel), (0.0, 0.0));
}
#[test]
fn spring_is_frame_rate_independent() {
let tau = 0.1;
let (x0, v0) = (50.0, -200.0);
let (mut x, mut v) = (x0, v0);
for _ in 0..60 {
(x, v) = critically_damped_spring_step(x, v, DT, tau);
}
let (x_coarse, v_coarse) = critically_damped_spring_step(x0, v0, 1.0, tau);
assert!((x - x_coarse).abs() < 1e-3, "{x} vs {x_coarse}");
assert!((v - v_coarse).abs() < 1e-2, "{v} vs {v_coarse}");
}
#[test]
fn trackpad_chains_to_parent_when_at_the_edge_at_gesture_start() {
let p = physics(true);
let (mut offset, mut vel) = (0.0, 0.0);
let mut bounced = false;
let fingers = Some(MouseWheelSource::Trackpad);
assert!(!p.wheel(
&mut offset,
&mut vel,
wheel(-10.0, fingers, true),
&mut bounced
));
assert_eq!(offset, 0.0);
offset = 5.0;
assert!(p.wheel(
&mut offset,
&mut vel,
wheel(-10.0, fingers, true),
&mut bounced
));
assert!(offset < 0.0, "{offset}");
assert!(p.wheel(
&mut offset,
&mut vel,
wheel(-10.0, fingers, true),
&mut bounced
));
}
#[test]
fn momentum_bounces_once_then_is_ignored() {
let p = physics(true);
let (mut offset, mut vel) = (30.0, 0.0);
let mut bounced = false;
let momentum = Some(MouseWheelSource::Momentum);
for _ in 0..3 {
assert!(p.wheel(
&mut offset,
&mut vel,
wheel(-10.0, momentum, false),
&mut bounced
));
}
assert_eq!((offset, vel, bounced), (0.0, 0.0, false));
assert!(p.wheel(
&mut offset,
&mut vel,
wheel(-10.0, momentum, false),
&mut bounced
));
assert!(bounced);
assert!(vel < 0.0, "{vel}");
let before = (offset, vel);
assert!(p.wheel(
&mut offset,
&mut vel,
wheel(-10.0, momentum, false),
&mut bounced
));
assert_eq!((offset, vel), before);
let min_offset = settle(&p, &mut offset, &mut vel);
assert!(min_offset < -5.0, "{min_offset}");
assert_eq!((offset, vel), (0.0, 0.0));
assert!(p.wheel(
&mut offset,
&mut vel,
wheel(-1.0, Some(MouseWheelSource::Trackpad), false),
&mut bounced
));
assert!(!bounced);
}
#[test]
fn momentum_after_rubber_banding_bounces_once() {
let p = physics(true);
let mut bounced = false;
let (mut offset, mut vel) = (-40.0, 0.0);
let momentum = wheel(-30.0, Some(MouseWheelSource::Momentum), false);
assert!(p.wheel(&mut offset, &mut vel, momentum, &mut bounced));
assert!(bounced);
assert_eq!(offset, -40.0);
assert!(vel < 0.0, "{vel}");
let before = (offset, vel);
for _ in 0..5 {
assert!(p.wheel(&mut offset, &mut vel, momentum, &mut bounced));
assert_eq!((offset, vel), before);
}
}
}