1use alloc::boxed::Box;
22use alloc::vec::Vec;
23
24use crate::animation::loop_progress;
25pub use crate::animation::{Easing, LoopMode};
26use crate::widget::{Color, Rect};
27
28pub const ANIM_SCALE: i32 = 256;
31
32#[derive(Debug, Clone, Copy, PartialEq)]
47pub struct Tween {
48 from: i32,
49 to: i32,
50 duration: u32,
52 elapsed: u32,
54 easing: Easing,
55 loop_mode: LoopMode,
56}
57
58impl Tween {
59 pub fn new(from: i32, to: i32, duration: u32) -> Self {
64 Self {
65 from,
66 to,
67 duration,
68 elapsed: 0,
69 easing: Easing::Linear,
70 loop_mode: LoopMode::Once,
71 }
72 }
73
74 pub fn with_easing(mut self, easing: Easing) -> Self {
76 self.easing = easing;
77 self
78 }
79
80 pub fn with_loop(mut self, loop_mode: LoopMode) -> Self {
82 self.loop_mode = loop_mode;
83 self
84 }
85
86 pub fn value_at(&self, tick: u32) -> i32 {
91 let (raw_t, _) = loop_progress(tick, self.duration, self.loop_mode);
92 let t = self.easing.apply(raw_t);
93 (self.from as f32 + (self.to as f32 - self.from as f32) * t) as i32
94 }
95
96 pub fn step(&mut self) -> i32 {
101 self.elapsed = self.elapsed.saturating_add(1);
102 self.value_at(self.elapsed)
103 }
104
105 pub fn value(&self) -> i32 {
107 self.value_at(self.elapsed)
108 }
109
110 pub fn elapsed(&self) -> u32 {
112 self.elapsed
113 }
114
115 pub fn finished(&self) -> bool {
119 let (_, done) = loop_progress(self.elapsed, self.duration, self.loop_mode);
120 done
121 }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub struct AnimId(u32);
132
133pub type ApplyFn = Box<dyn FnMut(i32) -> Option<Rect>>;
137
138struct Entry {
139 id: AnimId,
140 tween: Tween,
141 apply: ApplyFn,
142}
143
144#[derive(Default)]
153pub struct Animations {
154 entries: Vec<Entry>,
155 next_id: u32,
156 dirty: Vec<Rect>,
158}
159
160impl Animations {
161 pub fn new() -> Self {
163 Self {
164 entries: Vec::new(),
165 next_id: 0,
166 dirty: Vec::new(),
167 }
168 }
169
170 pub fn register(&mut self, tween: Tween, apply: ApplyFn) -> AnimId {
173 let id = AnimId(self.next_id);
174 self.next_id = self.next_id.wrapping_add(1);
175 self.entries.push(Entry { id, tween, apply });
176 id
177 }
178
179 pub fn tick(&mut self) -> bool {
187 self.dirty.clear();
188 for entry in &mut self.entries {
189 let value = entry.tween.step();
190 if let Some(rect) = (entry.apply)(value) {
191 self.dirty.push(rect);
192 }
193 }
194 self.entries.retain(|entry| !entry.tween.finished());
195 !self.entries.is_empty()
196 }
197
198 pub fn any_active(&self) -> bool {
201 !self.entries.is_empty()
202 }
203
204 pub fn len(&self) -> usize {
206 self.entries.len()
207 }
208
209 pub fn is_empty(&self) -> bool {
211 self.entries.is_empty()
212 }
213
214 pub fn dirty_rects(&self) -> &[Rect] {
218 &self.dirty
219 }
220
221 pub fn dirty_union(&self) -> Option<Rect> {
224 let mut union: Option<Rect> = None;
225 for &rect in &self.dirty {
226 union = Some(match union {
227 None => rect,
228 Some(u) => u.union(rect),
229 });
230 }
231 union
232 }
233
234 pub fn cancel(&mut self, id: AnimId) -> bool {
237 let before = self.entries.len();
238 self.entries.retain(|entry| entry.id != id);
239 self.entries.len() != before
240 }
241
242 pub fn pulse_color(
252 &mut self,
253 from: Color,
254 to: Color,
255 half_period: u32,
256 easing: Easing,
257 mut apply: Box<dyn FnMut(Color) -> Option<Rect>>,
258 ) -> AnimId {
259 let tween = Tween::new(0, ANIM_SCALE, half_period)
260 .with_easing(easing)
261 .with_loop(LoopMode::PingPong(0));
262 self.register(
263 tween,
264 Box::new(move |v| apply(from.lerp(to, v, ANIM_SCALE))),
265 )
266 }
267
268 pub fn slide_rect(
276 &mut self,
277 from: Rect,
278 to: Rect,
279 duration: u32,
280 easing: Easing,
281 mut apply: Box<dyn FnMut(Rect) -> Option<Rect>>,
282 ) -> AnimId {
283 let tween = Tween::new(0, ANIM_SCALE, duration)
284 .with_easing(easing)
285 .with_loop(LoopMode::Once);
286 self.register(
287 tween,
288 Box::new(move |v| apply(from.lerp(to, v, ANIM_SCALE))),
289 )
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296 use alloc::rc::Rc;
297 use alloc::vec;
298 use core::cell::RefCell;
299
300 #[test]
304 fn value_tables_linear_and_easeout_once() {
305 let linear = Tween::new(0, 100, 10);
306 let expected = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 100];
307 for (tick, want) in expected.iter().enumerate() {
308 assert_eq!(linear.value_at(tick as u32), *want, "linear tick {tick}");
309 }
310
311 let easeout = Tween::new(0, 100, 10).with_easing(Easing::EaseOut);
312 let expected = [0, 19, 35, 51, 64, 75, 84, 91, 96, 99, 100, 100];
313 for (tick, want) in expected.iter().enumerate() {
314 assert_eq!(easeout.value_at(tick as u32), *want, "easeout tick {tick}");
315 }
316
317 let negative = Tween::new(-120, 20, 8).with_easing(Easing::EaseOut);
318 let expected = [-120, -87, -58, -34, -15, 0, 11, 17, 20, 20];
319 for (tick, want) in expected.iter().enumerate() {
320 assert_eq!(
321 negative.value_at(tick as u32),
322 *want,
323 "negative tick {tick}"
324 );
325 }
326 }
327
328 #[test]
329 fn value_tables_pingpong_infinite() {
330 let linear = Tween::new(0, 256, 4).with_loop(LoopMode::PingPong(0));
331 let expected = [
332 0, 64, 128, 192, 256, 192, 128, 64, 0, 64, 128, 192, 256, 192, 128, 64, 0, 64,
333 ];
334 for (tick, want) in expected.iter().enumerate() {
335 assert_eq!(linear.value_at(tick as u32), *want, "pp linear tick {tick}");
336 }
337
338 let easeout = Tween::new(0, 256, 4)
339 .with_easing(Easing::EaseOut)
340 .with_loop(LoopMode::PingPong(0));
341 let expected = [
342 0, 112, 192, 240, 256, 240, 192, 112, 0, 112, 192, 240, 256, 240, 192, 112, 0, 112,
343 ];
344 for (tick, want) in expected.iter().enumerate() {
345 assert_eq!(
346 easeout.value_at(tick as u32),
347 *want,
348 "pp easeout tick {tick}"
349 );
350 }
351 }
352
353 #[test]
354 fn value_tables_repeat() {
355 let infinite = Tween::new(0, 100, 5).with_loop(LoopMode::Repeat(0));
356 let expected = [0, 20, 40, 60, 80, 0, 20, 40, 60, 80, 0, 20, 40];
357 for (tick, want) in expected.iter().enumerate() {
358 assert_eq!(infinite.value_at(tick as u32), *want, "rep inf tick {tick}");
359 }
360
361 let twice = Tween::new(0, 100, 5).with_loop(LoopMode::Repeat(2));
362 let expected = [0, 20, 40, 60, 80, 0, 20, 40, 60, 80, 100, 100, 100];
363 for (tick, want) in expected.iter().enumerate() {
364 assert_eq!(twice.value_at(tick as u32), *want, "rep 2 tick {tick}");
365 }
366 }
367
368 #[test]
369 fn step_matches_value_at_across_loop_cycles() {
370 let pure = Tween::new(-50, 999, 7)
371 .with_easing(Easing::EaseOut)
372 .with_loop(LoopMode::PingPong(0));
373 let mut stateful = pure;
374 for tick in 1..100u32 {
375 assert_eq!(stateful.step(), pure.value_at(tick), "tick {tick}");
376 }
377 }
378
379 #[test]
380 fn sequences_are_identical_across_runs() {
381 let sample = || -> Vec<i32> {
382 let tween = Tween::new(3, 977, 13)
383 .with_easing(Easing::EaseOut)
384 .with_loop(LoopMode::PingPong(0));
385 (0..200).map(|t| tween.value_at(t)).collect()
386 };
387 assert_eq!(sample(), sample());
388 }
389
390 #[test]
391 fn finished_semantics() {
392 let mut once = Tween::new(0, 10, 3);
393 assert!(!once.finished());
394 once.step();
395 once.step();
396 assert!(!once.finished());
397 once.step();
398 assert!(once.finished());
399
400 let mut twice = Tween::new(0, 10, 3).with_loop(LoopMode::Repeat(2));
401 for _ in 0..5 {
402 twice.step();
403 }
404 assert!(!twice.finished());
405 twice.step();
406 assert!(twice.finished());
407
408 let mut infinite = Tween::new(0, 10, 3).with_loop(LoopMode::Repeat(0));
409 let mut pingpong = Tween::new(0, 10, 3).with_loop(LoopMode::PingPong(0));
410 for _ in 0..10_000 {
411 infinite.step();
412 pingpong.step();
413 }
414 assert!(!infinite.finished());
415 assert!(!pingpong.finished());
416 }
417
418 #[test]
419 fn zero_duration_is_born_finished_at_end_value() {
420 let tween = Tween::new(7, 42, 0);
421 assert!(tween.finished());
422 assert_eq!(tween.value_at(0), 42);
423 assert_eq!(tween.value(), 42);
424 }
425
426 #[test]
427 fn registry_applies_terminal_value_then_removes() {
428 let seen = Rc::new(RefCell::new(Vec::new()));
429 let mut anims = Animations::new();
430 let sink = seen.clone();
431 anims.register(
432 Tween::new(0, 100, 2),
433 Box::new(move |v| {
434 sink.borrow_mut().push(v);
435 None
436 }),
437 );
438 assert!(anims.any_active());
439 assert!(anims.tick());
440 assert!(!anims.tick(), "completed entry retained");
441 assert!(!anims.any_active());
442 assert_eq!(*seen.borrow(), vec![50, 100], "terminal value applied");
443 assert!(!anims.tick(), "empty registry stays inactive");
444 assert_eq!(seen.borrow().len(), 2, "no application after removal");
445 }
446
447 #[test]
448 fn registry_cancel() {
449 let mut anims = Animations::new();
450 let id_a = anims.register(Tween::new(0, 1, 10), Box::new(|_| None));
451 let id_b = anims.register(Tween::new(0, 1, 10), Box::new(|_| None));
452 assert_eq!(anims.len(), 2);
453 assert!(anims.cancel(id_a));
454 assert!(!anims.cancel(id_a), "double-cancel reports false");
455 assert_eq!(anims.len(), 1);
456 assert!(anims.cancel(id_b));
457 assert!(anims.is_empty());
458 }
459
460 #[test]
461 fn twenty_five_concurrent_pulses_stay_within_dirty_budget() {
462 let mut anims = Animations::new();
465 for i in 0..25i32 {
466 let rect = Rect {
467 x: 10 + (i % 5) * 40,
468 y: 10 + (i / 5) * 40,
469 width: 20,
470 height: 20,
471 };
472 anims.pulse_color(
473 Color(255, 255, 255, 255),
474 Color(40, 40, 40, 255),
475 32,
476 Easing::EaseOut,
477 Box::new(move |_| Some(rect)),
478 );
479 }
480 for _ in 0..100 {
481 assert!(anims.tick(), "infinite pulses stay active");
482 assert_eq!(anims.dirty_rects().len(), 25, "one rect per pulse");
483 }
484 let union = anims.dirty_union().expect("dirty union");
485 assert_eq!(
486 union,
487 Rect {
488 x: 10,
489 y: 10,
490 width: 180,
491 height: 180
492 },
493 "union bounded by the pulsing widgets, not the full frame"
494 );
495 }
496
497 #[test]
498 fn pulse_color_hits_endpoints_at_half_period_multiples() {
499 let last = Rc::new(RefCell::new(Color(0, 0, 0, 0)));
500 let mut anims = Animations::new();
501 let sink = last.clone();
502 let from = Color(200, 100, 50, 255);
503 let to = Color(20, 40, 60, 255);
504 anims.pulse_color(
505 from,
506 to,
507 8,
508 Easing::Linear,
509 Box::new(move |c| {
510 *sink.borrow_mut() = c;
511 None
512 }),
513 );
514 for tick in 1..=32u32 {
515 anims.tick();
516 match tick % 16 {
517 8 => assert_eq!(*last.borrow(), to, "peak at tick {tick}"),
518 0 => assert_eq!(*last.borrow(), from, "trough at tick {tick}"),
519 _ => {}
520 }
521 }
522 }
523
524 #[test]
525 fn slide_rect_lands_exactly_on_rest_position() {
526 let last = Rc::new(RefCell::new(None));
527 let mut anims = Animations::new();
528 let sink = last.clone();
529 let from = Rect {
530 x: -200,
531 y: 30,
532 width: 180,
533 height: 120,
534 };
535 let to = Rect {
536 x: 16,
537 y: 30,
538 width: 180,
539 height: 120,
540 };
541 anims.slide_rect(
542 from,
543 to,
544 24,
545 Easing::EaseOut,
546 Box::new(move |rect| {
547 let prev: Option<Rect> = sink.borrow_mut().replace(rect);
548 Some(prev.map_or(rect, |p| p.union(rect)))
549 }),
550 );
551 let mut ticks = 0;
552 while anims.tick() {
553 ticks += 1;
554 assert!(ticks <= 24, "slide overran its duration");
555 }
556 assert_eq!(ticks, 23, "auto-removed on the terminal tick");
557 assert_eq!(last.borrow().unwrap(), to, "final frame at rest position");
558 assert!(anims.dirty_union().is_some());
560 assert!(anims.is_empty());
561 }
562}