ftui_core/animation/
group.rs1#![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 #[must_use]
149 pub fn len(&self) -> usize {
150 self.members.len()
151 }
152
153 #[must_use]
155 pub fn is_empty(&self) -> bool {
156 self.members.is_empty()
157 }
158
159 #[must_use]
161 pub fn all_complete(&self) -> bool {
162 self.members.is_empty() || self.members.iter().all(|m| m.animation.is_complete())
163 }
164
165 #[must_use]
169 pub fn overall_progress(&self) -> f32 {
170 if self.members.is_empty() {
171 return 0.0;
172 }
173 let sum: f32 = self.members.iter().map(|m| m.animation.value()).sum();
174 sum / self.members.len() as f32
175 }
176
177 #[must_use]
179 pub fn get(&self, label: &str) -> Option<&dyn Animation> {
180 self.members
181 .iter()
182 .find(|m| m.label == label)
183 .map(|m| &*m.animation)
184 }
185
186 pub fn get_mut(&mut self, label: &str) -> Option<&mut Box<dyn Animation>> {
188 for member in &mut self.members {
189 if member.label == label {
190 return Some(&mut member.animation);
191 }
192 }
193 None
194 }
195
196 #[must_use]
198 pub fn get_at(&self, index: usize) -> Option<&dyn Animation> {
199 self.members.get(index).map(|m| &*m.animation)
200 }
201
202 pub fn iter(&self) -> impl Iterator<Item = (&str, &dyn Animation)> {
204 self.members
205 .iter()
206 .map(|m| (m.label.as_str(), &*m.animation))
207 }
208
209 pub fn labels(&self) -> impl Iterator<Item = &str> {
211 self.members.iter().map(|m| m.label.as_str())
212 }
213}
214
215impl Animation for AnimationGroup {
220 fn tick(&mut self, dt: Duration) {
221 for member in &mut self.members {
222 if !member.animation.is_complete() {
223 member.animation.tick(dt);
224 }
225 }
226 }
227
228 fn is_complete(&self) -> bool {
229 self.all_complete()
230 }
231
232 fn value(&self) -> f32 {
233 self.overall_progress()
234 }
235
236 fn reset(&mut self) {
237 for member in &mut self.members {
238 member.animation.reset();
239 }
240 }
241}
242
243#[cfg(test)]
248mod tests {
249 use super::*;
250 use crate::animation::Fade;
251
252 const MS_100: Duration = Duration::from_millis(100);
253 const MS_200: Duration = Duration::from_millis(200);
254 const MS_300: Duration = Duration::from_millis(300);
255 const MS_500: Duration = Duration::from_millis(500);
256 const SEC_1: Duration = Duration::from_secs(1);
257
258 #[test]
259 fn empty_group() {
260 let group = AnimationGroup::new();
261 assert!(group.is_empty());
262 assert_eq!(group.len(), 0);
263 assert!(group.all_complete());
264 assert_eq!(group.overall_progress(), 0.0);
265 }
266
267 #[test]
268 fn add_and_tick() {
269 let mut group = AnimationGroup::new()
270 .add("a", Fade::new(MS_500))
271 .add("b", Fade::new(SEC_1));
272
273 assert_eq!(group.len(), 2);
274 assert!(!group.all_complete());
275
276 group.tick(MS_500);
277 assert!(group.get("a").unwrap().is_complete());
279 assert!(!group.get("b").unwrap().is_complete());
280 assert!((group.get("b").unwrap().value() - 0.5).abs() < 0.02);
281 }
282
283 #[test]
284 fn overall_progress() {
285 let mut group = AnimationGroup::new()
286 .add("short", Fade::new(MS_200))
287 .add("long", Fade::new(SEC_1));
288
289 group.tick(MS_200);
290 assert!((group.overall_progress() - 0.6).abs() < 0.02);
292 }
293
294 #[test]
295 fn all_complete_when_all_done() {
296 let mut group = AnimationGroup::new()
297 .add("a", Fade::new(MS_100))
298 .add("b", Fade::new(MS_200));
299
300 group.tick(MS_200);
301 assert!(group.all_complete());
302 assert!(group.is_complete());
303 }
304
305 #[test]
306 fn start_all_resets_everything() {
307 let mut group = AnimationGroup::new().add("a", Fade::new(MS_100));
308
309 group.tick(MS_100);
310 assert!(group.all_complete());
311
312 group.start_all();
313 assert!(!group.all_complete());
314 assert!((group.get("a").unwrap().value() - 0.0).abs() < f32::EPSILON);
315 }
316
317 #[test]
318 fn cancel_all_resets() {
319 let mut group = AnimationGroup::new().add("a", Fade::new(MS_100));
320
321 group.tick(MS_100);
322 group.cancel_all();
323 assert!(!group.all_complete());
324 }
325
326 #[test]
327 fn duplicate_label_replaces() {
328 let group = AnimationGroup::new()
329 .add("x", Fade::new(MS_100))
330 .add("x", Fade::new(SEC_1));
331
332 assert_eq!(group.len(), 1);
333 }
335
336 #[test]
337 fn remove_animation() {
338 let mut group = AnimationGroup::new()
339 .add("a", Fade::new(MS_100))
340 .add("b", Fade::new(MS_200));
341
342 assert!(group.remove("a"));
343 assert_eq!(group.len(), 1);
344 assert!(group.get("a").is_none());
345 assert!(group.get("b").is_some());
346
347 assert!(!group.remove("nonexistent"));
348 }
349
350 #[test]
351 fn get_at_index() {
352 let group = AnimationGroup::new()
353 .add("a", Fade::new(MS_100))
354 .add("b", Fade::new(MS_200));
355
356 assert!(group.get_at(0).is_some());
357 assert!(group.get_at(1).is_some());
358 assert!(group.get_at(2).is_none());
359 }
360
361 #[test]
362 fn get_mut_allows_individual_tick() {
363 let mut group = AnimationGroup::new()
364 .add("a", Fade::new(SEC_1))
365 .add("b", Fade::new(SEC_1));
366
367 if let Some(a) = group.get_mut("a") {
369 a.tick(MS_500);
370 }
371 assert!((group.get("a").unwrap().value() - 0.5).abs() < 0.02);
372 assert!((group.get("b").unwrap().value() - 0.0).abs() < f32::EPSILON);
373 }
374
375 #[test]
376 fn labels_iterator() {
377 let group = AnimationGroup::new()
378 .add("alpha", Fade::new(MS_100))
379 .add("beta", Fade::new(MS_100));
380
381 let labels: Vec<&str> = group.labels().collect();
382 assert_eq!(labels, vec!["alpha", "beta"]);
383 }
384
385 #[test]
386 fn iter_pairs() {
387 let group = AnimationGroup::new()
388 .add("a", Fade::new(MS_100))
389 .add("b", Fade::new(MS_100));
390
391 let pairs: Vec<_> = group.iter().collect();
392 assert_eq!(pairs.len(), 2);
393 assert_eq!(pairs[0].0, "a");
394 assert_eq!(pairs[1].0, "b");
395 }
396
397 #[test]
398 fn animation_trait_reset() {
399 let mut group = AnimationGroup::new().add("a", Fade::new(MS_100));
400
401 group.tick(MS_100);
402 assert!(group.is_complete());
403
404 group.reset();
405 assert!(!group.is_complete());
406 }
407
408 #[test]
409 fn animation_trait_value_matches_overall() {
410 let mut group = AnimationGroup::new()
411 .add("a", Fade::new(MS_300))
412 .add("b", Fade::new(SEC_1));
413
414 group.tick(MS_300);
415 assert!((group.value() - group.overall_progress()).abs() < f32::EPSILON);
416 }
417
418 #[test]
419 fn skips_completed_on_tick() {
420 let mut group = AnimationGroup::new()
421 .add("a", Fade::new(MS_100))
422 .add("b", Fade::new(SEC_1));
423
424 group.tick(MS_200);
425 let a_val = group.get("a").unwrap().value();
427 group.tick(MS_100);
428 assert!((group.get("a").unwrap().value() - a_val).abs() < f32::EPSILON);
430 }
431
432 #[test]
433 fn debug_format() {
434 let group = AnimationGroup::new().add("a", Fade::new(MS_100));
435
436 let dbg = format!("{:?}", group);
437 assert!(dbg.contains("AnimationGroup"));
438 assert!(dbg.contains("count"));
439 }
440
441 #[test]
442 fn insert_mutating() {
443 let mut group = AnimationGroup::new();
444 group.insert("x", Box::new(Fade::new(MS_100)));
445 assert_eq!(group.len(), 1);
446 assert!(group.get("x").is_some());
447 }
448}