use std::time::Instant;
#[derive(Debug)]
pub struct ScrollState {
pub offset: usize,
pub target_offset: usize,
pub animated_offset: f64,
pub animation_start: Option<Instant>,
pub dragging: bool,
pub drag_offset: f32,
pub last_activity: Instant,
}
impl Default for ScrollState {
fn default() -> Self {
Self::new()
}
}
impl ScrollState {
pub fn new() -> Self {
Self {
offset: 0,
target_offset: 0,
animated_offset: 0.0,
animation_start: None,
dragging: false,
drag_offset: 0.0,
last_activity: Instant::now(),
}
}
pub fn set_target(&mut self, new_offset: usize) -> bool {
if new_offset != self.target_offset {
self.target_offset = new_offset;
self.animation_start = Some(Instant::now());
self.last_activity = Instant::now();
true
} else {
false
}
}
pub fn update_animation(&mut self) -> bool {
if let Some(start_time) = self.animation_start {
const ANIMATION_DURATION: f64 = 0.15; let elapsed = start_time.elapsed().as_secs_f64();
if elapsed >= ANIMATION_DURATION {
self.animated_offset = self.target_offset as f64;
self.offset = self.target_offset;
self.animation_start = None;
return false;
}
let t = elapsed / ANIMATION_DURATION;
let eased = 1.0 - (1.0 - t).powi(3);
let start = self.offset as f64;
let target = self.target_offset as f64;
self.animated_offset = start + (target - start) * eased;
self.offset = self.animated_offset.round() as usize;
return true;
}
false
}
pub fn clamp_to_scrollback(&mut self, max_scroll: usize) {
if self.offset > max_scroll {
self.offset = max_scroll;
}
if self.target_offset > max_scroll {
self.target_offset = max_scroll;
self.animated_offset = max_scroll as f64;
self.animation_start = None;
}
}
pub fn apply_scroll(&mut self, lines: i32, max_scroll: usize) -> usize {
if lines > 0 {
let new_offset = self.target_offset.saturating_add(lines as usize);
new_offset.min(max_scroll)
} else if lines < 0 {
self.target_offset
.saturating_sub(lines.unsigned_abs() as usize)
} else {
self.target_offset
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scroll_state_defaults() {
let state = ScrollState::new();
assert_eq!(state.offset, 0);
assert_eq!(state.target_offset, 0);
assert!(!state.dragging);
}
#[test]
fn test_set_target() {
let mut state = ScrollState::new();
assert!(state.set_target(10));
assert_eq!(state.target_offset, 10);
assert!(state.animation_start.is_some());
assert!(!state.set_target(10));
}
#[test]
fn test_clamp_to_scrollback() {
let mut state = ScrollState::new();
state.offset = 100;
state.target_offset = 100;
state.clamp_to_scrollback(50);
assert_eq!(state.offset, 50);
assert_eq!(state.target_offset, 50);
assert!(state.animation_start.is_none());
}
#[test]
fn test_apply_scroll() {
let mut state = ScrollState::new();
let max_scroll = 100;
assert_eq!(state.apply_scroll(10, max_scroll), 10);
state.target_offset = 10;
assert_eq!(state.apply_scroll(-5, max_scroll), 5);
state.target_offset = 5;
state.target_offset = 95;
assert_eq!(state.apply_scroll(10, max_scroll), 100);
state.target_offset = 5;
assert_eq!(state.apply_scroll(-10, max_scroll), 0);
}
}