1#![forbid(unsafe_code)]
2
3use std::time::Duration;
39
40use super::Animation;
41
42struct GroupMember {
48 label: String,
49 animation: Box<dyn Animation>,
50}
51
52impl std::fmt::Debug for GroupMember {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 f.debug_struct("GroupMember")
55 .field("label", &self.label)
56 .field("value", &self.animation.value())
57 .field("complete", &self.animation.is_complete())
58 .finish()
59 }
60}
61
62pub struct AnimationGroup {
67 members: Vec<GroupMember>,
68}
69
70impl std::fmt::Debug for AnimationGroup {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 f.debug_struct("AnimationGroup")
73 .field("count", &self.members.len())
74 .field("progress", &self.overall_progress())
75 .field("complete", &self.all_complete())
76 .finish()
77 }
78}
79
80impl AnimationGroup {
85 #[must_use]
87 pub fn new() -> Self {
88 Self {
89 members: Vec::new(),
90 }
91 }
92
93 #[must_use]
97 pub fn add(mut self, label: &str, animation: impl Animation + 'static) -> Self {
98 self.insert(label, Box::new(animation));
99 self
100 }
101
102 pub fn insert(&mut self, label: &str, animation: Box<dyn Animation>) {
106 if let Some(existing) = self.members.iter_mut().find(|m| m.label == label) {
107 existing.animation = animation;
108 } else {
109 self.members.push(GroupMember {
110 label: label.to_string(),
111 animation,
112 });
113 }
114 }
115
116 pub fn remove(&mut self, label: &str) -> bool {
118 let len_before = self.members.len();
119 self.members.retain(|m| m.label != label);
120 self.members.len() < len_before
121 }
122}
123
124impl Default for AnimationGroup {
125 fn default() -> Self {
126 Self::new()
127 }
128}
129
130impl AnimationGroup {
135 pub fn start_all(&mut self) {
137 for member in &mut self.members {
138 member.animation.reset();
139 }
140 }
141
142 pub fn cancel_all(&mut self) {
144 self.start_all();
145 }
146
147 #[inline]
149 #[must_use]
150 pub fn len(&self) -> usize {
151 self.members.len()
152 }
153
154 #[inline]
156 #[must_use]
157 pub fn is_empty(&self) -> bool {
158 self.members.is_empty()
159 }
160
161 #[inline]
163 #[must_use]
164 pub fn all_complete(&self) -> bool {
165 self.members.is_empty() || self.members.iter().all(|m| m.animation.is_complete())
166 }
167
168 #[inline]
172 #[must_use]
173 pub fn overall_progress(&self) -> f32 {
174 if self.members.is_empty() {
175 return 0.0;
176 }
177 let sum: f32 = self.members.iter().map(|m| m.animation.value()).sum();
178 sum / self.members.len() as f32
179 }
180
181 #[inline]
183 #[must_use]
184 pub fn get(&self, label: &str) -> Option<&dyn Animation> {
185 self.members
186 .iter()
187 .find(|m| m.label == label)
188 .map(|m| &*m.animation)
189 }
190
191 pub fn get_mut(&mut self, label: &str) -> Option<&mut Box<dyn Animation>> {
193 for member in &mut self.members {
194 if member.label == label {
195 return Some(&mut member.animation);
196 }
197 }
198 None
199 }
200
201 #[inline]
203 #[must_use]
204 pub fn get_at(&self, index: usize) -> Option<&dyn Animation> {
205 self.members.get(index).map(|m| &*m.animation)
206 }
207
208 pub fn iter(&self) -> impl Iterator<Item = (&str, &dyn Animation)> {
210 self.members
211 .iter()
212 .map(|m| (m.label.as_str(), &*m.animation))
213 }
214
215 pub fn labels(&self) -> impl Iterator<Item = &str> {
217 self.members.iter().map(|m| m.label.as_str())
218 }
219}
220
221impl Animation for AnimationGroup {
226 fn tick(&mut self, dt: Duration) {
227 for member in &mut self.members {
228 if !member.animation.is_complete() {
229 member.animation.tick(dt);
230 }
231 }
232 }
233
234 fn is_complete(&self) -> bool {
235 self.all_complete()
236 }
237
238 fn value(&self) -> f32 {
239 self.overall_progress()
240 }
241
242 fn reset(&mut self) {
243 for member in &mut self.members {
244 member.animation.reset();
245 }
246 }
247}
248
249#[cfg(test)]
254mod tests {
255 use super::*;
256 use crate::animation::Fade;
257
258 const MS_100: Duration = Duration::from_millis(100);
259 const MS_200: Duration = Duration::from_millis(200);
260 const MS_300: Duration = Duration::from_millis(300);
261 const MS_500: Duration = Duration::from_millis(500);
262 const SEC_1: Duration = Duration::from_secs(1);
263
264 #[test]
265 fn empty_group() {
266 let group = AnimationGroup::new();
267 assert!(group.is_empty());
268 assert_eq!(group.len(), 0);
269 assert!(group.all_complete());
270 assert_eq!(group.overall_progress(), 0.0);
271 }
272
273 #[test]
274 fn add_and_tick() {
275 let mut group = AnimationGroup::new()
276 .add("a", Fade::new(MS_500))
277 .add("b", Fade::new(SEC_1));
278
279 assert_eq!(group.len(), 2);
280 assert!(!group.all_complete());
281
282 group.tick(MS_500);
283 assert!(group.get("a").unwrap().is_complete());
285 assert!(!group.get("b").unwrap().is_complete());
286 assert!((group.get("b").unwrap().value() - 0.5).abs() < 0.02);
287 }
288
289 #[test]
290 fn overall_progress() {
291 let mut group = AnimationGroup::new()
292 .add("short", Fade::new(MS_200))
293 .add("long", Fade::new(SEC_1));
294
295 group.tick(MS_200);
296 assert!((group.overall_progress() - 0.6).abs() < 0.02);
298 }
299
300 #[test]
301 fn all_complete_when_all_done() {
302 let mut group = AnimationGroup::new()
303 .add("a", Fade::new(MS_100))
304 .add("b", Fade::new(MS_200));
305
306 group.tick(MS_200);
307 assert!(group.all_complete());
308 assert!(group.is_complete());
309 }
310
311 #[test]
312 fn start_all_resets_everything() {
313 let mut group = AnimationGroup::new().add("a", Fade::new(MS_100));
314
315 group.tick(MS_100);
316 assert!(group.all_complete());
317
318 group.start_all();
319 assert!(!group.all_complete());
320 assert!((group.get("a").unwrap().value() - 0.0).abs() < f32::EPSILON);
321 }
322
323 #[test]
324 fn cancel_all_resets() {
325 let mut group = AnimationGroup::new().add("a", Fade::new(MS_100));
326
327 group.tick(MS_100);
328 group.cancel_all();
329 assert!(!group.all_complete());
330 }
331
332 #[test]
333 fn duplicate_label_replaces() {
334 let group = AnimationGroup::new()
335 .add("x", Fade::new(MS_100))
336 .add("x", Fade::new(SEC_1));
337
338 assert_eq!(group.len(), 1);
339 }
341
342 #[test]
343 fn remove_animation() {
344 let mut group = AnimationGroup::new()
345 .add("a", Fade::new(MS_100))
346 .add("b", Fade::new(MS_200));
347
348 assert!(group.remove("a"));
349 assert_eq!(group.len(), 1);
350 assert!(group.get("a").is_none());
351 assert!(group.get("b").is_some());
352
353 assert!(!group.remove("nonexistent"));
354 }
355
356 #[test]
357 fn get_at_index() {
358 let group = AnimationGroup::new()
359 .add("a", Fade::new(MS_100))
360 .add("b", Fade::new(MS_200));
361
362 assert!(group.get_at(0).is_some());
363 assert!(group.get_at(1).is_some());
364 assert!(group.get_at(2).is_none());
365 }
366
367 #[test]
368 fn get_mut_allows_individual_tick() {
369 let mut group = AnimationGroup::new()
370 .add("a", Fade::new(SEC_1))
371 .add("b", Fade::new(SEC_1));
372
373 if let Some(a) = group.get_mut("a") {
375 a.tick(MS_500);
376 }
377 assert!((group.get("a").unwrap().value() - 0.5).abs() < 0.02);
378 assert!((group.get("b").unwrap().value() - 0.0).abs() < f32::EPSILON);
379 }
380
381 #[test]
382 fn labels_iterator() {
383 let group = AnimationGroup::new()
384 .add("alpha", Fade::new(MS_100))
385 .add("beta", Fade::new(MS_100));
386
387 let labels: Vec<&str> = group.labels().collect();
388 assert_eq!(labels, vec!["alpha", "beta"]);
389 }
390
391 #[test]
392 fn iter_pairs() {
393 let group = AnimationGroup::new()
394 .add("a", Fade::new(MS_100))
395 .add("b", Fade::new(MS_100));
396
397 let pairs: Vec<_> = group.iter().collect();
398 assert_eq!(pairs.len(), 2);
399 assert_eq!(pairs[0].0, "a");
400 assert_eq!(pairs[1].0, "b");
401 }
402
403 #[test]
404 fn animation_trait_reset() {
405 let mut group = AnimationGroup::new().add("a", Fade::new(MS_100));
406
407 group.tick(MS_100);
408 assert!(group.is_complete());
409
410 group.reset();
411 assert!(!group.is_complete());
412 }
413
414 #[test]
415 fn animation_trait_value_matches_overall() {
416 let mut group = AnimationGroup::new()
417 .add("a", Fade::new(MS_300))
418 .add("b", Fade::new(SEC_1));
419
420 group.tick(MS_300);
421 assert!((group.value() - group.overall_progress()).abs() < f32::EPSILON);
422 }
423
424 #[test]
425 fn skips_completed_on_tick() {
426 let mut group = AnimationGroup::new()
427 .add("a", Fade::new(MS_100))
428 .add("b", Fade::new(SEC_1));
429
430 group.tick(MS_200);
431 let a_val = group.get("a").unwrap().value();
433 group.tick(MS_100);
434 assert!((group.get("a").unwrap().value() - a_val).abs() < f32::EPSILON);
436 }
437
438 #[test]
439 fn debug_format() {
440 let group = AnimationGroup::new().add("a", Fade::new(MS_100));
441
442 let dbg = format!("{:?}", group);
443 assert!(dbg.contains("AnimationGroup"));
444 assert!(dbg.contains("count"));
445 }
446
447 #[test]
448 fn insert_mutating() {
449 let mut group = AnimationGroup::new();
450 group.insert("x", Box::new(Fade::new(MS_100)));
451 assert_eq!(group.len(), 1);
452 assert!(group.get("x").is_some());
453 }
454
455 #[test]
458 fn default_trait() {
459 let group = AnimationGroup::default();
460 assert!(group.is_empty());
461 assert_eq!(group.len(), 0);
462 assert!(group.all_complete());
463 }
464
465 #[test]
466 fn get_unknown_label_returns_none() {
467 let group = AnimationGroup::new().add("a", Fade::new(MS_100));
468 assert!(group.get("nonexistent").is_none());
469 }
470
471 #[test]
472 fn get_mut_unknown_label_returns_none() {
473 let mut group = AnimationGroup::new().add("a", Fade::new(MS_100));
474 assert!(group.get_mut("nonexistent").is_none());
475 }
476
477 #[test]
478 fn insert_replaces_existing() {
479 let mut group = AnimationGroup::new();
480 group.insert("x", Box::new(Fade::new(MS_100)));
481 group.insert("x", Box::new(Fade::new(SEC_1)));
482 assert_eq!(group.len(), 1);
483 group.tick(MS_100);
486 assert!(!group.all_complete());
487 }
488
489 #[test]
490 fn remove_from_empty_group() {
491 let mut group = AnimationGroup::new();
492 assert!(!group.remove("anything"));
493 assert_eq!(group.len(), 0);
494 }
495
496 #[test]
497 fn tick_on_empty_group_no_panic() {
498 let mut group = AnimationGroup::new();
499 group.tick(MS_500);
500 assert!(group.is_complete());
501 }
502
503 #[test]
504 fn reset_on_empty_group_no_panic() {
505 let mut group = AnimationGroup::new();
506 group.reset();
507 assert!(group.is_complete());
508 }
509
510 #[test]
511 fn single_member_progress() {
512 let mut group = AnimationGroup::new().add("only", Fade::new(MS_200));
513 group.tick(MS_100);
514 assert!((group.overall_progress() - 0.5).abs() < 0.02);
515 }
516
517 #[test]
518 fn three_members_progress() {
519 let mut group = AnimationGroup::new()
520 .add("a", Fade::new(MS_100))
521 .add("b", Fade::new(MS_200))
522 .add("c", Fade::new(MS_300));
523
524 assert_eq!(group.len(), 3);
525 group.tick(MS_300);
526 assert!(group.all_complete());
527 assert!((group.overall_progress() - 1.0).abs() < f32::EPSILON);
528 }
529
530 #[test]
531 fn add_remove_add_same_label() {
532 let mut group = AnimationGroup::new().add("x", Fade::new(MS_100));
533 assert!(group.remove("x"));
534 assert_eq!(group.len(), 0);
535 group.insert("x", Box::new(Fade::new(MS_200)));
536 assert_eq!(group.len(), 1);
537 }
538
539 #[test]
540 fn start_all_on_empty_no_panic() {
541 let mut group = AnimationGroup::new();
542 group.start_all();
543 assert!(group.is_empty());
544 }
545
546 #[test]
547 fn cancel_all_on_empty_no_panic() {
548 let mut group = AnimationGroup::new();
549 group.cancel_all();
550 assert!(group.is_empty());
551 }
552
553 #[test]
554 fn progress_mixed_complete_and_incomplete() {
555 let mut group = AnimationGroup::new()
556 .add("done", Fade::new(MS_100))
557 .add("half", Fade::new(MS_500));
558
559 group.tick(MS_200);
560 let progress = group.overall_progress();
562 assert!(progress > 0.5 && progress < 0.9, "progress: {progress}");
563 assert!(!group.all_complete());
564 }
565
566 #[test]
567 fn iter_empty_group() {
568 let group = AnimationGroup::new();
569 assert_eq!(group.iter().count(), 0);
570 }
571
572 #[test]
573 fn labels_empty_group() {
574 let group = AnimationGroup::new();
575 assert_eq!(group.labels().count(), 0);
576 }
577
578 #[test]
579 fn get_at_after_removal() {
580 let mut group = AnimationGroup::new()
581 .add("a", Fade::new(MS_100))
582 .add("b", Fade::new(MS_200));
583
584 group.remove("a");
585 assert!(group.get_at(0).is_some());
587 assert!(group.get_at(1).is_none());
588 }
589
590 #[test]
591 fn animation_value_empty_is_zero() {
592 let group = AnimationGroup::new();
593 assert!((group.value() - 0.0).abs() < f32::EPSILON);
594 }
595
596 #[test]
597 fn debug_format_includes_progress_and_complete() {
598 let group = AnimationGroup::new().add("a", Fade::new(MS_100));
599 let dbg = format!("{group:?}");
600 assert!(dbg.contains("progress"));
601 assert!(dbg.contains("complete"));
602 }
603}