1#![warn(missing_docs)]
4
5use std::collections::HashMap;
6
7#[cfg(test)]
9mod scroll_integration_tests;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ScrollDirection {
14 Up,
16 Down,
18 Left,
20 Right,
22}
23
24#[derive(Debug, Clone)]
26pub struct ScrollTrigger {
27 pub threshold: f64,
29 pub repeat: bool,
31 pub element_id: String,
33}
34
35impl Default for ScrollTrigger {
36 fn default() -> Self {
37 Self {
38 threshold: 0.5,
39 repeat: false,
40 element_id: String::new(),
41 }
42 }
43}
44
45impl ScrollTrigger {
46 pub fn new(element_id: String) -> Self {
48 Self {
49 element_id,
50 ..Self::default()
51 }
52 }
53
54 pub fn with_threshold(mut self, threshold: f64) -> Self {
56 self.threshold = threshold.clamp(0.0, 1.0);
57 self
58 }
59
60 pub fn with_repeat(mut self, repeat: bool) -> Self {
62 self.repeat = repeat;
63 self
64 }
65}
66
67#[derive(Debug, Clone)]
69pub struct ScrollAnimationState {
70 pub scroll_y: f64,
72 pub previous_scroll_y: f64,
74 pub direction: ScrollDirection,
76 pub velocity: f64,
78}
79
80impl Default for ScrollAnimationState {
81 fn default() -> Self {
82 Self {
83 scroll_y: 0.0,
84 previous_scroll_y: 0.0,
85 direction: ScrollDirection::Down,
86 velocity: 0.0,
87 }
88 }
89}
90
91impl ScrollAnimationState {
92 pub fn new() -> Self {
94 Self::default()
95 }
96
97 pub fn update_position(&mut self, new_y: f64) {
99 self.previous_scroll_y = self.scroll_y;
100 self.scroll_y = new_y;
101
102 let delta = new_y - self.previous_scroll_y;
103 self.velocity = delta;
104
105 self.direction = if delta > 0.0 {
106 ScrollDirection::Down
107 } else if delta < 0.0 {
108 ScrollDirection::Up
109 } else {
110 self.direction };
112 }
113
114 pub fn get_progress(&self, viewport_height: f64, document_height: f64) -> f64 {
116 if document_height <= viewport_height {
117 return 0.0;
118 }
119 let max_scroll = document_height - viewport_height;
120 (self.scroll_y / max_scroll).clamp(0.0, 1.0)
121 }
122
123 pub fn is_scrolling(&self, direction: ScrollDirection) -> bool {
125 self.direction == direction && self.velocity.abs() > 0.1
126 }
127}
128
129#[derive(Default)]
131pub struct ScrollAnimator {
132 pub active: bool,
134 triggers: HashMap<String, ScrollTrigger>,
136 scroll_state: ScrollAnimationState,
138 triggered: HashMap<String, bool>,
140}
141
142impl ScrollAnimator {
143 pub fn new() -> Self {
145 Self {
146 active: false,
147 triggers: HashMap::new(),
148 scroll_state: ScrollAnimationState::new(),
149 triggered: HashMap::new(),
150 }
151 }
152
153 pub fn add_trigger(&mut self, trigger: ScrollTrigger) {
155 let id = trigger.element_id.clone();
156 self.triggers.insert(id.clone(), trigger);
157 self.triggered.insert(id, false);
158 }
159
160 pub fn remove_trigger(&mut self, element_id: &str) -> bool {
162 let removed = self.triggers.remove(element_id).is_some();
163 self.triggered.remove(element_id);
164 removed
165 }
166
167 pub fn update_scroll(&mut self, scroll_y: f64) {
169 self.scroll_state.update_position(scroll_y);
170 self.active = self.scroll_state.velocity.abs() > 0.1;
171 }
172
173 pub fn check_triggers(&mut self, viewport_height: f64, document_height: f64) -> Vec<String> {
175 let mut activated = Vec::new();
176 let progress = self
177 .scroll_state
178 .get_progress(viewport_height, document_height);
179
180 for (id, trigger) in &self.triggers {
181 let was_triggered = self.triggered.get(id).copied().unwrap_or(false);
182 let should_trigger = progress >= trigger.threshold;
183
184 if should_trigger && (!was_triggered || trigger.repeat) {
185 activated.push(id.clone());
186 self.triggered.insert(id.clone(), true);
187 } else if !should_trigger && trigger.repeat {
188 self.triggered.insert(id.clone(), false);
189 }
190 }
191
192 activated
193 }
194
195 pub fn get_scroll_state(&self) -> &ScrollAnimationState {
197 &self.scroll_state
198 }
199
200 pub fn trigger_count(&self) -> usize {
202 self.triggers.len()
203 }
204
205 pub fn clear_triggers(&mut self) {
207 self.triggers.clear();
208 self.triggered.clear();
209 }
210
211 pub fn was_triggered(&self, element_id: &str) -> bool {
213 self.triggered.get(element_id).copied().unwrap_or(false)
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn test_scroll_direction_values() {
223 assert_eq!(ScrollDirection::Up, ScrollDirection::Up);
224 assert_eq!(ScrollDirection::Down, ScrollDirection::Down);
225 assert_eq!(ScrollDirection::Left, ScrollDirection::Left);
226 assert_eq!(ScrollDirection::Right, ScrollDirection::Right);
227 }
228
229 #[test]
230 fn test_scroll_trigger_new() {
231 let trigger = ScrollTrigger::new("element1".to_string());
232 assert_eq!(trigger.element_id, "element1");
233 assert_eq!(trigger.threshold, 0.5);
234 assert!(!trigger.repeat);
235 }
236
237 #[test]
238 fn test_scroll_trigger_default() {
239 let trigger = ScrollTrigger::default();
240 assert_eq!(trigger.element_id, "");
241 assert_eq!(trigger.threshold, 0.5);
242 assert!(!trigger.repeat);
243 }
244
245 #[test]
246 fn test_scroll_trigger_builder() {
247 let trigger = ScrollTrigger::new("element1".to_string())
248 .with_threshold(0.8)
249 .with_repeat(true);
250
251 assert_eq!(trigger.element_id, "element1");
252 assert_eq!(trigger.threshold, 0.8);
253 assert!(trigger.repeat);
254 }
255
256 #[test]
257 fn test_scroll_trigger_threshold_clamping() {
258 let trigger1 = ScrollTrigger::new("element1".to_string()).with_threshold(-0.5);
259 assert_eq!(trigger1.threshold, 0.0);
260
261 let trigger2 = ScrollTrigger::new("element2".to_string()).with_threshold(1.5);
262 assert_eq!(trigger2.threshold, 1.0);
263 }
264
265 #[test]
266 fn test_scroll_animation_state_new() {
267 let state = ScrollAnimationState::new();
268 assert_eq!(state.scroll_y, 0.0);
269 assert_eq!(state.previous_scroll_y, 0.0);
270 assert_eq!(state.direction, ScrollDirection::Down);
271 assert_eq!(state.velocity, 0.0);
272 }
273
274 #[test]
275 fn test_scroll_animation_state_default() {
276 let state = ScrollAnimationState::default();
277 assert_eq!(state.scroll_y, 0.0);
278 assert_eq!(state.previous_scroll_y, 0.0);
279 assert_eq!(state.direction, ScrollDirection::Down);
280 assert_eq!(state.velocity, 0.0);
281 }
282
283 #[test]
284 fn test_scroll_animation_state_update_position_down() {
285 let mut state = ScrollAnimationState::new();
286 state.update_position(100.0);
287
288 assert_eq!(state.scroll_y, 100.0);
289 assert_eq!(state.previous_scroll_y, 0.0);
290 assert_eq!(state.direction, ScrollDirection::Down);
291 assert_eq!(state.velocity, 100.0);
292 }
293
294 #[test]
295 fn test_scroll_animation_state_update_position_up() {
296 let mut state = ScrollAnimationState::new();
297 state.update_position(100.0);
298 state.update_position(50.0);
299
300 assert_eq!(state.scroll_y, 50.0);
301 assert_eq!(state.previous_scroll_y, 100.0);
302 assert_eq!(state.direction, ScrollDirection::Up);
303 assert_eq!(state.velocity, -50.0);
304 }
305
306 #[test]
307 fn test_scroll_animation_state_get_progress() {
308 let mut state = ScrollAnimationState::new();
309 state.update_position(200.0);
310
311 let progress = state.get_progress(600.0, 1200.0);
315 assert!((progress - 0.3333333333333333).abs() < 0.0001);
316 }
317
318 #[test]
319 fn test_scroll_animation_state_get_progress_no_scroll() {
320 let state = ScrollAnimationState::new();
321
322 let progress = state.get_progress(1200.0, 800.0);
324 assert_eq!(progress, 0.0);
325 }
326
327 #[test]
328 fn test_scroll_animation_state_is_scrolling() {
329 let mut state = ScrollAnimationState::new();
330
331 assert!(!state.is_scrolling(ScrollDirection::Down));
333 assert!(!state.is_scrolling(ScrollDirection::Up));
334
335 state.update_position(10.0);
337 assert!(state.is_scrolling(ScrollDirection::Down));
338 assert!(!state.is_scrolling(ScrollDirection::Up));
339
340 state.update_position(0.0);
342 assert!(!state.is_scrolling(ScrollDirection::Down));
343 assert!(state.is_scrolling(ScrollDirection::Up));
344 }
345
346 #[test]
347 fn test_scroll_animator_new() {
348 let animator = ScrollAnimator::new();
349 assert!(!animator.active);
350 assert_eq!(animator.trigger_count(), 0);
351 }
352
353 #[test]
354 fn test_scroll_animator_default() {
355 let animator = ScrollAnimator::default();
356 assert!(!animator.active);
357 assert_eq!(animator.trigger_count(), 0);
358 }
359
360 #[test]
361 fn test_scroll_animator_add_trigger() {
362 let mut animator = ScrollAnimator::new();
363 let trigger = ScrollTrigger::new("element1".to_string());
364
365 animator.add_trigger(trigger);
366 assert_eq!(animator.trigger_count(), 1);
367 assert!(!animator.was_triggered("element1"));
368 }
369
370 #[test]
371 fn test_scroll_animator_remove_trigger() {
372 let mut animator = ScrollAnimator::new();
373 let trigger = ScrollTrigger::new("element1".to_string());
374
375 animator.add_trigger(trigger);
376 assert_eq!(animator.trigger_count(), 1);
377
378 let removed = animator.remove_trigger("element1");
379 assert!(removed);
380 assert_eq!(animator.trigger_count(), 0);
381
382 let not_removed = animator.remove_trigger("nonexistent");
383 assert!(!not_removed);
384 }
385
386 #[test]
387 fn test_scroll_animator_update_scroll() {
388 let mut animator = ScrollAnimator::new();
389
390 animator.update_scroll(100.0);
391 assert!(animator.active); assert_eq!(animator.get_scroll_state().scroll_y, 100.0);
393 }
394
395 #[test]
396 fn test_scroll_animator_check_triggers() {
397 let mut animator = ScrollAnimator::new();
398
399 let trigger = ScrollTrigger::new("element1".to_string()).with_threshold(0.5);
401 animator.add_trigger(trigger);
402
403 animator.update_scroll(180.0); let activated = animator.check_triggers(600.0, 1200.0);
406 assert!(activated.is_empty());
407 assert!(!animator.was_triggered("element1"));
408
409 animator.update_scroll(360.0); let activated = animator.check_triggers(600.0, 1200.0);
412 assert_eq!(activated.len(), 1);
413 assert_eq!(activated[0], "element1");
414 assert!(animator.was_triggered("element1"));
415 }
416
417 #[test]
418 fn test_scroll_animator_check_triggers_repeat() {
419 let mut animator = ScrollAnimator::new();
420
421 let trigger = ScrollTrigger::new("element1".to_string())
423 .with_threshold(0.5)
424 .with_repeat(true);
425 animator.add_trigger(trigger);
426
427 animator.update_scroll(360.0); let activated = animator.check_triggers(600.0, 1200.0);
430 assert_eq!(activated.len(), 1);
431
432 animator.update_scroll(240.0); let activated = animator.check_triggers(600.0, 1200.0);
435 assert!(activated.is_empty());
436
437 animator.update_scroll(360.0); let activated = animator.check_triggers(600.0, 1200.0);
440 assert_eq!(activated.len(), 1);
441 }
442
443 #[test]
444 fn test_scroll_animator_clear_triggers() {
445 let mut animator = ScrollAnimator::new();
446
447 let trigger1 = ScrollTrigger::new("element1".to_string());
448 let trigger2 = ScrollTrigger::new("element2".to_string());
449
450 animator.add_trigger(trigger1);
451 animator.add_trigger(trigger2);
452 assert_eq!(animator.trigger_count(), 2);
453
454 animator.clear_triggers();
455 assert_eq!(animator.trigger_count(), 0);
456 }
457
458 #[test]
459 fn test_scroll_animator_get_scroll_state() {
460 let mut animator = ScrollAnimator::new();
461 animator.update_scroll(150.0);
462
463 let state = animator.get_scroll_state();
464 assert_eq!(state.scroll_y, 150.0);
465 assert_eq!(state.direction, ScrollDirection::Down);
466 }
467}