use super::{ScrollMetrics, ScrollPhysics, Simulation};
use crate::scroll::OVERSCROLL_RESISTANCE;
#[derive(Debug, Default)]
pub struct RubberBand {
parent: Option<Box<dyn ScrollPhysics>>,
}
impl RubberBand {
pub fn new() -> Self {
Self { parent: None }
}
pub fn chain(self, parent: impl ScrollPhysics + 'static) -> Self {
Self {
parent: Some(Box::new(parent)),
}
}
}
impl ScrollPhysics for RubberBand {
fn parent(&self) -> Option<&dyn ScrollPhysics> {
self.parent.as_deref()
}
fn apply_physics_to_user_offset(&self, metrics: &ScrollMetrics, offset: f64) -> f64 {
let min = metrics.min_scroll_extent;
let max = metrics.max_scroll_extent;
let start = metrics.pixels.clamp(min, max);
let end = (metrics.pixels + offset).clamp(min, max);
let in_range = end - start;
let past_edge = offset - in_range;
in_range + past_edge * OVERSCROLL_RESISTANCE
}
fn apply_boundary_conditions(&self, _metrics: &ScrollMetrics, _value: f64) -> f64 {
0.0
}
fn create_ballistic_simulation(
&self,
_metrics: &ScrollMetrics,
_velocity: f64,
) -> Option<Box<dyn Simulation>> {
None
}
fn carried_momentum(&self, _existing_velocity: f64) -> f64 {
0.0
}
fn should_accept_user_offset(&self, _metrics: &ScrollMetrics) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
fn metrics(pixels: f64, max: f64) -> ScrollMetrics {
ScrollMetrics {
pixels,
min_scroll_extent: 0.0,
max_scroll_extent: max,
viewport_dimension: 100.0,
device_pixel_ratio: 1.0,
}
}
#[test]
fn rubber_band_drag_mapping_matches_legacy_math() {
let physics = RubberBand::new();
assert_eq!(
physics.apply_physics_to_user_offset(&metrics(0.0, 900.0), 30.0),
30.0
);
assert_eq!(
physics.apply_physics_to_user_offset(&metrics(500.0, 900.0), -120.0),
-120.0
);
assert_eq!(
physics.apply_physics_to_user_offset(&metrics(0.0, 900.0), -20.0),
-10.0
);
assert_eq!(
physics.apply_physics_to_user_offset(&metrics(900.0, 900.0), 60.0),
30.0
);
assert_eq!(
physics.apply_physics_to_user_offset(&metrics(5.0, 900.0), -25.0),
-15.0
);
assert_eq!(
physics.apply_physics_to_user_offset(&metrics(0.0, 0.0), 40.0),
20.0
);
}
#[test]
fn per_move_deltas_telescope_to_the_pre_seam_pull() {
let physics = RubberBand::new();
let mut position = 0.0;
for _ in 0..4 {
position += physics.apply_physics_to_user_offset(&metrics(position, 900.0), -10.0);
}
assert_eq!(position, -20.0);
assert_eq!(
physics.apply_physics_to_user_offset(&metrics(0.0, 900.0), -40.0),
-20.0,
"…the same number the whole pull in one move gives"
);
let returned =
position + physics.apply_physics_to_user_offset(&metrics(position, 900.0), 40.0);
assert_eq!(returned, 10.0);
}
#[test]
fn rubber_band_rejects_nothing_and_carries_no_momentum() {
let physics = RubberBand::new();
let m = metrics(0.0, 900.0);
assert_eq!(physics.apply_boundary_conditions(&m, -500.0), 0.0);
assert_eq!(physics.apply_boundary_conditions(&m, 1400.0), 0.0);
assert_eq!(physics.carried_momentum(2000.0), 0.0);
assert!(physics.create_ballistic_simulation(&m, 2000.0).is_none());
}
#[test]
fn rubber_band_accepts_a_drag_even_when_content_fits() {
assert!(RubberBand::new().should_accept_user_offset(&metrics(0.0, 0.0)));
}
#[derive(Debug)]
struct Slow;
impl ScrollPhysics for Slow {
fn min_fling_velocity(&self) -> f64 {
5.0
}
}
#[test]
fn chaining_delegates_unoverridden_methods_to_the_parent() {
let chained = RubberBand::new().chain(Slow);
assert_eq!(chained.min_fling_velocity(), 5.0);
assert_eq!(
chained.apply_physics_to_user_offset(&metrics(0.0, 900.0), -20.0),
-10.0
);
assert_eq!(
RubberBand::new().min_fling_velocity(),
crate::physics::MIN_FLING_VELOCITY
);
}
}