#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ScrollPhysics {
pub page_fraction: f32,
pub flick_velocity_px_per_sec: f32,
pub drag_min_fraction: f32,
pub snaps_to_page: bool,
}
impl Default for ScrollPhysics {
fn default() -> Self {
Self {
page_fraction: 0.18,
flick_velocity_px_per_sec: 400.0,
drag_min_fraction: 0.02,
snaps_to_page: true,
}
}
}
impl ScrollPhysics {
pub const fn pager() -> Self {
Self {
page_fraction: 0.18,
flick_velocity_px_per_sec: 400.0,
drag_min_fraction: 0.02,
snaps_to_page: true,
}
}
pub const fn scroller() -> Self {
Self {
page_fraction: 1.0,
flick_velocity_px_per_sec: f32::INFINITY,
drag_min_fraction: 0.0,
snaps_to_page: false,
}
}
pub const fn deliberate() -> Self {
Self {
page_fraction: 0.5,
flick_velocity_px_per_sec: 1200.0,
drag_min_fraction: 0.1,
snaps_to_page: true,
}
}
pub fn release_direction(
&self,
travel: f32,
extent: f32,
velocity_px_per_sec: f32,
) -> Option<i8> {
if !self.snaps_to_page || !travel.is_finite() || !extent.is_finite() || extent <= 0.0 {
return None;
}
let speed = if velocity_px_per_sec.is_nan() { 0.0 } else { velocity_px_per_sec.abs() };
let fraction = (travel / extent).abs();
if fraction < self.drag_min_fraction {
return None;
}
if fraction >= self.page_fraction || speed >= self.flick_velocity_px_per_sec {
return Some(if travel > 0.0 { 1 } else { -1 });
}
None
}
pub fn would_page(&self, travel: f32, extent: f32, velocity_px_per_sec: f32) -> bool {
self.release_direction(travel, extent, velocity_px_per_sec).is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
const EXTENT: f32 = 400.0;
#[test]
fn the_default_is_the_crates_existing_feel() {
let physics = ScrollPhysics::default();
assert_eq!(physics.page_fraction, 0.18);
assert_eq!(physics.flick_velocity_px_per_sec, 400.0);
assert_eq!(physics.drag_min_fraction, 0.02);
assert!(physics.snaps_to_page);
assert_eq!(ScrollPhysics::pager(), ScrollPhysics::default());
}
#[test]
fn a_long_drag_pages_on_distance() {
let physics = ScrollPhysics::default();
assert_eq!(physics.release_direction(160.0, EXTENT, 0.0), Some(1));
assert_eq!(physics.release_direction(-160.0, EXTENT, 0.0), Some(-1));
}
#[test]
fn a_short_fast_flick_pages_on_speed() {
let physics = ScrollPhysics::default();
assert_eq!(physics.release_direction(60.0, EXTENT, 900.0), Some(1));
assert_eq!(physics.release_direction(60.0, EXTENT, 100.0), None);
}
#[test]
fn a_jittering_pointer_does_not_page_however_fast() {
let physics = ScrollPhysics::default();
assert_eq!(physics.release_direction(4.0, EXTENT, 500.0), None);
assert!(!physics.would_page(4.0, EXTENT, 500.0));
}
#[test]
fn a_scroller_never_pages() {
let physics = ScrollPhysics::scroller();
assert!(!physics.would_page(1_000.0, EXTENT, 5_000.0), "a scroller stops where it is");
assert!(!physics.snaps_to_page);
}
#[test]
fn the_deliberate_feel_is_harder_to_trigger() {
let physics = ScrollPhysics::deliberate();
assert!(!physics.would_page(100.0, EXTENT, 0.0));
assert!(ScrollPhysics::default().would_page(100.0, EXTENT, 0.0));
assert!(ScrollPhysics::default().would_page(80.0, EXTENT, 600.0));
assert!(!physics.would_page(80.0, EXTENT, 600.0));
}
#[test]
fn a_degenerate_extent_does_not_page_or_panic() {
let physics = ScrollPhysics::default();
assert_eq!(
physics.release_direction(50.0, 0.0, 1_000.0),
None,
"a zero extent pages nothing"
);
assert_eq!(physics.release_direction(50.0, f32::NAN, 1_000.0), None);
assert_eq!(physics.release_direction(50.0, -10.0, 1_000.0), None);
}
#[test]
fn a_nan_speed_is_treated_as_no_speed() {
let physics = ScrollPhysics::default();
assert_eq!(physics.release_direction(40.0, EXTENT, f32::NAN), None);
assert_eq!(physics.release_direction(40.0, EXTENT, f32::INFINITY), Some(1));
assert_eq!(physics.release_direction(-40.0, EXTENT, f32::INFINITY), Some(-1));
}
}