use std::time::Duration;
use frust::{AnimationController, Curve};
use super::layout::PRECISION_TOLERANCE;
use crate::tokens::MaterialMotion;
pub(crate) const SNAP_VELOCITY_TOLERANCE: f64 = 20.0;
pub(crate) const SETTLE_DURATION: Duration = MaterialMotion::LONG_2;
pub(crate) const SETTLE_CURVE: Curve = Curve::Cubic(0.25, 0.1, 0.25, 1.0);
pub(crate) fn settle_controller() -> AnimationController {
AnimationController::new(SETTLE_DURATION).with_curve(SETTLE_CURVE)
}
pub(crate) fn continuous_item(offset: f64, stride: f64) -> f64 {
if stride <= 0.0 || !stride.is_finite() || !offset.is_finite() {
return 0.0;
}
let actual = offset.max(0.0) / stride;
if !actual.is_finite() {
return 0.0;
}
let round = actual.round();
if (actual - round).abs() < PRECISION_TOLERANCE {
round
} else {
actual
}
}
pub(crate) fn nearest_item(offset: f64, stride: f64) -> usize {
let rounded = continuous_item(offset, stride).round();
if rounded <= 0.0 { 0 } else { rounded as usize }
}
pub(crate) fn snap_offset(offset: f64, stride: f64, scroll_velocity: f64, max_offset: f64) -> f64 {
if stride <= 0.0 || !stride.is_finite() {
return 0.0;
}
let mut item = continuous_item(offset, stride);
if scroll_velocity.is_finite() {
if scroll_velocity < -SNAP_VELOCITY_TOLERANCE {
item -= 0.5;
} else if scroll_velocity > SNAP_VELOCITY_TOLERANCE {
item += 0.5;
}
}
let target = item.round() * stride;
let upper = if max_offset.is_finite() {
max_offset.max(0.0)
} else {
0.0
};
target.max(0.0).min(upper)
}
#[cfg(test)]
mod tests {
use super::*;
const STRIDE: f64 = 200.0;
const MAX: f64 = 1200.0;
#[test]
fn a_slow_release_settles_onto_the_nearest_boundary() {
assert_eq!(snap_offset(70.0, STRIDE, 0.0, MAX), 0.0);
assert_eq!(snap_offset(130.0, STRIDE, 0.0, MAX), STRIDE);
assert_eq!(snap_offset(400.0, STRIDE, 0.0, MAX), 400.0);
}
#[test]
fn a_release_below_the_velocity_tolerance_is_still_a_settle() {
let creeping = SNAP_VELOCITY_TOLERANCE - 1.0;
assert_eq!(snap_offset(70.0, STRIDE, creeping, MAX), 0.0);
assert_eq!(snap_offset(70.0, STRIDE, -creeping, MAX), 0.0);
}
#[test]
fn a_forward_fling_advances_one_item_however_short_the_drag() {
let fast = SNAP_VELOCITY_TOLERANCE * 40.0;
assert_eq!(snap_offset(10.0, STRIDE, fast, MAX), STRIDE);
assert_eq!(snap_offset(210.0, STRIDE, fast, MAX), STRIDE * 2.0);
}
#[test]
fn a_backward_fling_biases_toward_the_previous_boundary() {
let fast = -SNAP_VELOCITY_TOLERANCE * 40.0;
assert_eq!(snap_offset(390.0, STRIDE, fast, MAX), STRIDE);
assert_eq!(snap_offset(410.0, STRIDE, fast, MAX), STRIDE * 2.0);
}
#[test]
fn a_fling_never_leaves_the_scrollable_range() {
let fast = SNAP_VELOCITY_TOLERANCE * 40.0;
assert_eq!(snap_offset(MAX, STRIDE, fast, MAX), MAX);
assert_eq!(snap_offset(0.0, STRIDE, -fast, MAX), 0.0);
assert_eq!(snap_offset(500.0, STRIDE, fast, 0.0), 0.0);
}
#[test]
fn a_degenerate_stride_or_bound_degrades_to_zero_instead_of_dividing() {
assert_eq!(snap_offset(300.0, 0.0, 500.0, MAX), 0.0);
assert_eq!(snap_offset(300.0, -5.0, 500.0, MAX), 0.0);
assert_eq!(snap_offset(300.0, f64::NAN, 500.0, MAX), 0.0);
assert_eq!(snap_offset(300.0, STRIDE, f64::NAN, MAX), 400.0);
assert_eq!(snap_offset(300.0, STRIDE, 0.0, f64::NAN), 0.0);
}
#[test]
fn the_continuous_item_pins_to_a_boundary_within_tolerance() {
assert_eq!(continuous_item(STRIDE, STRIDE), 1.0);
assert_eq!(continuous_item(STRIDE - 1e-12, STRIDE), 1.0);
assert!((continuous_item(STRIDE * 1.5, STRIDE) - 1.5).abs() < 1e-12);
assert_eq!(continuous_item(-50.0, STRIDE), 0.0, "never negative");
assert_eq!(continuous_item(50.0, 0.0), 0.0);
}
#[test]
fn the_nearest_item_crosses_at_the_halfway_mark_in_both_directions() {
assert_eq!(nearest_item(0.0, STRIDE), 0);
assert_eq!(nearest_item(99.0, STRIDE), 0);
assert_eq!(nearest_item(101.0, STRIDE), 1);
assert_eq!(nearest_item(299.0, STRIDE), 1);
assert_eq!(nearest_item(301.0, STRIDE), 2);
assert_eq!(nearest_item(-500.0, STRIDE), 0);
}
#[test]
fn the_settle_ramp_is_the_references_own_duration_and_curve() {
assert_eq!(SETTLE_DURATION, Duration::from_millis(500));
assert_eq!(SETTLE_CURVE, Curve::Cubic(0.25, 0.1, 0.25, 1.0));
let anim = settle_controller();
assert!(!anim.is_animating(), "a fresh controller is idle");
assert_eq!(anim.value(), 0.0);
}
}