1use std::time::Instant;
2
3#[derive(Debug)]
5pub struct ScrollState {
6 pub offset: usize,
8 pub target_offset: usize,
10 pub animated_offset: f64,
12 pub animation_start: Option<Instant>,
14 pub dragging: bool,
16 pub drag_offset: f32,
18 pub last_activity: Instant,
20}
21
22impl Default for ScrollState {
23 fn default() -> Self {
24 Self::new()
25 }
26}
27
28impl ScrollState {
29 pub fn new() -> Self {
31 Self {
32 offset: 0,
33 target_offset: 0,
34 animated_offset: 0.0,
35 animation_start: None,
36 dragging: false,
37 drag_offset: 0.0,
38 last_activity: Instant::now(),
39 }
40 }
41
42 pub fn set_target(&mut self, new_offset: usize) -> bool {
45 if new_offset != self.target_offset {
46 self.target_offset = new_offset;
47 self.animation_start = Some(Instant::now());
48 self.last_activity = Instant::now();
49 true
50 } else {
51 false
52 }
53 }
54
55 pub fn update_animation(&mut self) -> bool {
58 if let Some(start_time) = self.animation_start {
59 const ANIMATION_DURATION: f64 = 0.15; let elapsed = start_time.elapsed().as_secs_f64();
61
62 if elapsed >= ANIMATION_DURATION {
63 self.animated_offset = self.target_offset as f64;
65 self.offset = self.target_offset;
66 self.animation_start = None;
67 return false;
68 }
69
70 let t = elapsed / ANIMATION_DURATION;
73 let eased = 1.0 - (1.0 - t).powi(3);
74
75 let start = self.offset as f64;
76 let target = self.target_offset as f64;
77 self.animated_offset = start + (target - start) * eased;
78
79 self.offset = self.animated_offset.round() as usize;
81
82 return true;
83 }
84
85 false
86 }
87
88 pub fn clamp_to_scrollback(&mut self, max_scroll: usize) {
90 if self.offset > max_scroll {
91 self.offset = max_scroll;
92 }
93 if self.target_offset > max_scroll {
95 self.target_offset = max_scroll;
96 self.animated_offset = max_scroll as f64;
97 self.animation_start = None;
98 }
99 }
100
101 pub fn apply_scroll(&mut self, lines: i32, max_scroll: usize) -> usize {
104 if lines > 0 {
105 let new_offset = self.target_offset.saturating_add(lines as usize);
107 new_offset.min(max_scroll)
108 } else if lines < 0 {
109 self.target_offset
111 .saturating_sub(lines.unsigned_abs() as usize)
112 } else {
113 self.target_offset
114 }
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn test_scroll_state_defaults() {
124 let state = ScrollState::new();
125 assert_eq!(state.offset, 0);
126 assert_eq!(state.target_offset, 0);
127 assert!(!state.dragging);
128 }
129
130 #[test]
131 fn test_set_target() {
132 let mut state = ScrollState::new();
133 assert!(state.set_target(10));
134 assert_eq!(state.target_offset, 10);
135 assert!(state.animation_start.is_some());
136
137 assert!(!state.set_target(10));
139 }
140
141 #[test]
142 fn test_clamp_to_scrollback() {
143 let mut state = ScrollState::new();
144 state.offset = 100;
145 state.target_offset = 100;
146
147 state.clamp_to_scrollback(50);
148
149 assert_eq!(state.offset, 50);
150 assert_eq!(state.target_offset, 50);
151 assert!(state.animation_start.is_none());
152 }
153
154 #[test]
155 fn test_apply_scroll() {
156 let mut state = ScrollState::new();
157 let max_scroll = 100;
158
159 assert_eq!(state.apply_scroll(10, max_scroll), 10);
161 state.target_offset = 10;
162
163 assert_eq!(state.apply_scroll(-5, max_scroll), 5);
165 state.target_offset = 5;
166
167 state.target_offset = 95;
169 assert_eq!(state.apply_scroll(10, max_scroll), 100);
170
171 state.target_offset = 5;
173 assert_eq!(state.apply_scroll(-10, max_scroll), 0);
174 }
175}