use core::ops::Range;
use super::{TouchEvent, list_velocity::VelocityTracker};
const MIN_VELOCITY: f32 = 12.0;
const MAX_VELOCITY: f32 = 9_600.0;
const FLING_GAIN: f32 = 1.75;
const DECELERATION_RATE: f32 = 0.9990;
const SNAP_BACK_RATE: f32 = 16.0;
const MAX_DT_SECONDS: f32 = 1.0 / 30.0;
const RUBBER_BAND_COEFFICIENT: f32 = 0.65;
const MAX_OVERSCROLL_PX: f32 = 72.0;
const SETTLE_DISTANCE_PX: f32 = 0.5;
const DRAG_SLOP_PX: f32 = 8.0;
#[derive(Clone, Copy, Debug, Default)]
pub struct ScrollViewState {
position_px: f32,
drag_anchor_y: f32,
drag_anchor_position_px: f32,
velocity_px_per_s: f32,
touch_active: bool,
scrolling: bool,
tracker: VelocityTracker,
}
impl ScrollViewState {
pub const fn new() -> Self {
Self {
position_px: 0.0,
drag_anchor_y: 0.0,
drag_anchor_position_px: 0.0,
velocity_px_per_s: 0.0,
touch_active: false,
scrolling: false,
tracker: VelocityTracker::new(),
}
}
pub const fn is_dragging(&self) -> bool {
self.touch_active
}
pub const fn is_scrolling(&self) -> bool {
self.scrolling
}
pub fn content_offset_y(&self) -> i32 {
-to_pixels(self.position_px)
}
pub fn content_offset(&self) -> f32 {
self.position_px
}
pub fn begin_drag(&mut self, touch: TouchEvent) {
self.touch_active = true;
self.scrolling = false;
self.velocity_px_per_s = 0.0;
self.drag_anchor_y = touch.point.y as f32;
self.drag_anchor_position_px = self.position_px;
self.tracker.reset(touch);
}
pub fn drag(&mut self, touch: TouchEvent, content_height: u32, viewport_height: u32) -> bool {
if !self.touch_active {
self.begin_drag(touch);
return false;
}
let before = self.content_offset_y();
let max = max_position_px(content_height, viewport_height);
let delta_y = touch.point.y as f32 - self.drag_anchor_y;
let delta_abs = delta_y.abs();
if !self.scrolling {
if delta_abs < DRAG_SLOP_PX {
return false;
}
self.scrolling = true;
}
self.tracker.add(touch);
let effective_delta_y = delta_y.signum() * (delta_abs - DRAG_SLOP_PX).max(0.0);
let raw_position = self.drag_anchor_position_px - effective_delta_y;
self.position_px = apply_rubber_band(raw_position, max, viewport_height as f32);
before != self.content_offset_y()
}
pub fn end_drag(
&mut self,
touch: TouchEvent,
content_height: u32,
viewport_height: u32,
) -> bool {
if !self.touch_active {
return false;
}
let changed = self.drag(touch, content_height, viewport_height);
self.touch_active = false;
let max = max_position_px(content_height, viewport_height);
self.velocity_px_per_s = if !self.scrolling || outside_bounds(self.position_px, max) {
0.0
} else {
release_velocity(self.tracker.velocity())
};
self.scrolling = false;
changed
}
pub fn tick(&mut self, dt_ms: u32, content_height: u32, viewport_height: u32) -> bool {
if self.touch_active || dt_ms == 0 {
return false;
}
let before = self.content_offset_y();
let dt = (dt_ms as f32 / 1000.0).min(MAX_DT_SECONDS);
let max = max_position_px(content_height, viewport_height);
if outside_bounds(self.position_px, max) {
self.snap_toward_bounds(dt, max);
} else if self.velocity_px_per_s.abs() >= MIN_VELOCITY {
let next = self.position_px + (self.velocity_px_per_s * dt);
if next < 0.0 {
self.position_px = 0.0;
self.velocity_px_per_s = 0.0;
} else if next > max {
self.position_px = max;
self.velocity_px_per_s = 0.0;
} else {
self.position_px = next;
self.velocity_px_per_s = apply_decay(self.velocity_px_per_s, dt_ms);
}
}
if self.velocity_px_per_s.abs() < MIN_VELOCITY {
self.velocity_px_per_s = 0.0;
}
before != self.content_offset_y()
}
pub fn is_animating(&self, content_height: u32, viewport_height: u32) -> bool {
let max = max_position_px(content_height, viewport_height);
outside_bounds(self.position_px, max) || self.velocity_px_per_s.abs() >= MIN_VELOCITY
}
pub fn visible_range(
&self,
item_height: u32,
item_count: usize,
viewport_height: u32,
) -> Range<usize> {
if item_height == 0 || item_count == 0 {
return 0..0;
}
let top = self.position_px.max(0.0) as u32;
let bottom = top.saturating_add(viewport_height);
let start = (top / item_height) as usize;
let end = bottom.div_ceil(item_height) as usize + 1;
start.min(item_count)..end.min(item_count)
}
fn snap_toward_bounds(&mut self, dt: f32, max: f32) {
let target = self.position_px.clamp(0.0, max);
self.position_px += (target - self.position_px) * (SNAP_BACK_RATE * dt).min(1.0);
self.velocity_px_per_s = 0.0;
if (target - self.position_px).abs() <= SETTLE_DISTANCE_PX {
self.position_px = target;
}
}
}
pub type ScrollListState = ScrollViewState;
fn apply_rubber_band(position: f32, max: f32, viewport: f32) -> f32 {
if position < 0.0 {
-rubber_band_distance(-position, viewport)
} else if position > max {
max + rubber_band_distance(position - max, viewport)
} else {
position
}
}
fn apply_decay(mut velocity: f32, dt_ms: u32) -> f32 {
for _ in 0..dt_ms.min(32) {
velocity *= DECELERATION_RATE;
}
velocity
}
fn release_velocity(velocity: f32) -> f32 {
let boosted = (velocity * FLING_GAIN).clamp(-MAX_VELOCITY, MAX_VELOCITY);
if boosted.abs() < MIN_VELOCITY {
0.0
} else {
boosted
}
}
fn rubber_band_distance(distance: f32, viewport: f32) -> f32 {
let scaled = distance * RUBBER_BAND_COEFFICIENT / viewport.max(1.0);
((1.0 - (1.0 / (scaled + 1.0))) * viewport).min(MAX_OVERSCROLL_PX)
}
fn outside_bounds(position: f32, max: f32) -> bool {
position < 0.0 || position > max
}
fn max_position_px(content_height: u32, viewport_height: u32) -> f32 {
content_height.saturating_sub(viewport_height) as f32
}
fn to_pixels(position: f32) -> i32 {
if position >= 0.0 {
(position + 0.5) as i32
} else {
-((-position + 0.5) as i32)
}
}