1use std::num::NonZeroU32;
12use std::ops::Deref;
13
14use serde::{Deserialize, Serialize};
15
16use super::icons::IconData;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct EmptyFrameListError;
21
22impl std::fmt::Display for EmptyFrameListError {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 f.write_str("frame list must not be empty")
25 }
26}
27
28impl std::error::Error for EmptyFrameListError {}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
50pub struct FrameList(Vec<IconData>);
51
52impl FrameList {
53 pub fn new(frames: Vec<IconData>) -> Result<Self, EmptyFrameListError> {
55 if frames.is_empty() {
56 return Err(EmptyFrameListError);
57 }
58 Ok(Self(frames))
59 }
60
61 pub fn first(&self) -> &IconData {
63 debug_assert!(
68 !self.0.is_empty(),
69 "FrameList invariant violated: empty list"
70 );
71 #[allow(clippy::indexing_slicing)]
72 {
73 &self.0[0]
74 }
75 }
76
77 #[must_use]
82 #[allow(clippy::len_without_is_empty)]
83 pub fn len(&self) -> usize {
84 self.0.len()
85 }
86}
87
88impl Deref for FrameList {
89 type Target = [IconData];
90 fn deref(&self) -> &[IconData] {
91 &self.0
92 }
93}
94
95impl<'de> serde::Deserialize<'de> for FrameList {
98 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
99 where
100 D: serde::Deserializer<'de>,
101 {
102 let frames = Vec::<IconData>::deserialize(deserializer)?;
103 FrameList::new(frames).map_err(|_| serde::de::Error::custom("frame list must not be empty"))
104 }
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
120#[non_exhaustive]
121pub enum TransformAnimation {
122 Spin {
124 duration_ms: NonZeroU32,
126 },
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
131pub struct FramesData {
132 frames: FrameList,
133 frame_duration_ms: NonZeroU32,
134}
135
136impl FramesData {
137 #[must_use]
139 pub fn frames(&self) -> &FrameList {
140 &self.frames
141 }
142
143 #[must_use]
145 pub fn frame_duration_ms(&self) -> NonZeroU32 {
146 self.frame_duration_ms
147 }
148}
149
150#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
152pub struct TransformData {
153 icon: IconData,
154 animation: TransformAnimation,
155}
156
157impl TransformData {
158 pub fn icon(&self) -> &IconData {
160 &self.icon
161 }
162
163 #[must_use]
165 pub fn animation(&self) -> &TransformAnimation {
166 &self.animation
167 }
168}
169
170#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
220#[non_exhaustive]
221pub enum AnimatedIcon {
222 Frames(FramesData),
224 Transform(TransformData),
226}
227
228impl AnimatedIcon {
229 pub fn frames(
252 frames: Vec<IconData>,
253 frame_duration_ms: NonZeroU32,
254 ) -> Result<Self, EmptyFrameListError> {
255 Ok(AnimatedIcon::Frames(FramesData {
256 frames: FrameList::new(frames)?,
257 frame_duration_ms,
258 }))
259 }
260
261 #[must_use]
276 pub fn transform(icon: IconData, animation: TransformAnimation) -> Self {
277 AnimatedIcon::Transform(TransformData { icon, animation })
278 }
279
280 pub fn first_frame(&self) -> &IconData {
307 match self {
308 AnimatedIcon::Frames(data) => data.frames.first(),
309 AnimatedIcon::Transform(data) => &data.icon,
310 }
311 }
312
313 #[must_use]
315 pub fn frame_list(&self) -> Option<&FrameList> {
316 match self {
317 AnimatedIcon::Frames(data) => Some(&data.frames),
318 AnimatedIcon::Transform(_) => None,
319 }
320 }
321
322 #[must_use]
324 pub fn frame_duration_ms(&self) -> Option<NonZeroU32> {
325 match self {
326 AnimatedIcon::Frames(data) => Some(data.frame_duration_ms),
327 AnimatedIcon::Transform(_) => None,
328 }
329 }
330
331 #[must_use]
333 pub fn icon(&self) -> Option<&IconData> {
334 match self {
335 AnimatedIcon::Transform(data) => Some(&data.icon),
336 AnimatedIcon::Frames(_) => None,
337 }
338 }
339
340 #[must_use]
342 pub fn animation(&self) -> Option<&TransformAnimation> {
343 match self {
344 AnimatedIcon::Transform(data) => Some(&data.animation),
345 AnimatedIcon::Frames(_) => None,
346 }
347 }
348
349 #[deprecated(
351 since = "0.5.7",
352 note = "use AnimatedIcon::frames() which returns Result"
353 )]
354 #[must_use]
355 pub fn new_frames(frames: Vec<IconData>, frame_duration_ms: u32) -> Option<Self> {
356 let dur = NonZeroU32::new(frame_duration_ms)?;
357 Self::frames(frames, dur).ok()
358 }
359}
360
361#[cfg(test)]
362#[allow(clippy::unwrap_used, clippy::expect_used)]
363mod tests {
364 use super::*;
365 use std::borrow::Cow;
366
367 fn test_icon(label: &'static str) -> IconData {
368 IconData::Svg(Cow::Owned(format!("<svg>{label}</svg>").into_bytes()))
369 }
370
371 fn test_icon_borrowed() -> IconData {
372 IconData::Svg(Cow::Borrowed(b"<svg>test</svg>"))
373 }
374
375 fn nz(val: u32) -> NonZeroU32 {
376 NonZeroU32::new(val).unwrap()
377 }
378
379 #[test]
382 fn frame_list_new_with_one_frame() {
383 let fl = FrameList::new(vec![test_icon("f1")]);
384 assert!(fl.is_ok());
385 assert_eq!(fl.unwrap().len(), 1);
386 }
387
388 #[test]
389 fn frame_list_new_with_multiple_frames() {
390 let fl = FrameList::new(vec![test_icon("f1"), test_icon("f2"), test_icon("f3")]).unwrap();
391 assert_eq!(fl.len(), 3);
392 }
393
394 #[test]
395 fn frame_list_new_rejects_empty() {
396 let fl = FrameList::new(vec![]);
397 assert!(fl.is_err());
398 assert_eq!(fl.unwrap_err(), EmptyFrameListError);
399 }
400
401 #[test]
402 fn frame_list_first_returns_first_frame() {
403 let icon = test_icon("first");
404 let fl = FrameList::new(vec![icon.clone(), test_icon("second")]).unwrap();
405 assert_eq!(fl.first(), &icon);
406 }
407
408 #[test]
409 fn frame_list_deref_to_slice() {
410 let fl = FrameList::new(vec![test_icon("a"), test_icon("b")]).unwrap();
411 let slice: &[IconData] = &fl;
412 assert_eq!(slice.len(), 2);
413 assert_eq!(fl.iter().count(), 2);
415 }
416
417 #[test]
418 fn empty_frame_list_error_display() {
419 let err = EmptyFrameListError;
420 assert_eq!(err.to_string(), "frame list must not be empty");
421 }
422
423 #[test]
424 fn empty_frame_list_error_is_std_error() {
425 let err: Box<dyn std::error::Error> = Box::new(EmptyFrameListError);
426 assert_eq!(err.to_string(), "frame list must not be empty");
427 }
428
429 #[test]
432 fn frame_list_deserialize_rejects_empty_json() {
433 let json = "[]";
434 let result: Result<FrameList, _> = serde_json::from_str(json);
435 assert!(result.is_err());
436 let err = result.unwrap_err().to_string();
437 assert!(
438 err.contains("frame list must not be empty"),
439 "expected custom error message, got: {err}"
440 );
441 }
442
443 #[test]
444 fn frame_list_deserialize_non_empty_json_succeeds() {
445 let fl = FrameList::new(vec![test_icon_borrowed()]).unwrap();
447 let json = serde_json::to_string(&fl).unwrap();
448 let deserialized: FrameList = serde_json::from_str(&json).unwrap();
449 assert_eq!(deserialized.len(), 1);
450 }
451
452 #[test]
453 fn frame_list_round_trips_through_json() {
454 let fl = FrameList::new(vec![test_icon("a"), test_icon("b")]).unwrap();
455 let json = serde_json::to_string(&fl).unwrap();
456 let back: FrameList = serde_json::from_str(&json).unwrap();
457 assert_eq!(fl, back);
458 }
459
460 #[test]
463 fn frames_variant_constructs() {
464 let icon = AnimatedIcon::frames(vec![test_icon("f1")], nz(83)).unwrap();
465 assert!(matches!(icon, AnimatedIcon::Frames(_)));
466 if let AnimatedIcon::Frames(data) = &icon {
467 assert_eq!(data.frames().len(), 1);
468 assert_eq!(data.frame_duration_ms().get(), 83);
469 }
470 }
471
472 #[test]
473 fn frames_constructor_rejects_empty() {
474 let result = AnimatedIcon::frames(vec![], nz(83));
475 assert!(result.is_err());
476 }
477
478 #[test]
479 fn transform_variant_constructs() {
480 let icon = AnimatedIcon::transform(
481 test_icon("spinner"),
482 TransformAnimation::Spin {
483 duration_ms: nz(1000),
484 },
485 );
486 assert!(matches!(icon, AnimatedIcon::Transform(_)));
487 if let AnimatedIcon::Transform(data) = &icon {
488 assert_eq!(
489 *data.animation(),
490 TransformAnimation::Spin {
491 duration_ms: nz(1000)
492 }
493 );
494 }
495 }
496
497 #[test]
498 #[allow(clippy::clone_on_copy)]
499 fn transform_animation_is_copy_clone_debug_eq_hash() {
500 let a = TransformAnimation::Spin {
501 duration_ms: nz(500),
502 };
503 let a2 = a; let a3 = a.clone(); assert_eq!(a2, a3); use std::hash::Hash;
507 let mut hasher = std::collections::hash_map::DefaultHasher::new();
508 a.hash(&mut hasher);
509 let _ = format!("{a:?}"); }
511
512 #[test]
513 fn animated_icon_is_clone_debug_eq_not_copy() {
514 let icon = AnimatedIcon::frames(vec![test_icon("f1")], nz(100)).unwrap();
515 let cloned = icon.clone(); assert_eq!(icon, cloned); let _ = format!("{icon:?}"); }
519
520 #[test]
523 fn first_frame_frames_with_items() {
524 let f0 = test_icon("frame0");
525 let icon = AnimatedIcon::frames(vec![f0.clone(), test_icon("frame1")], nz(100)).unwrap();
526 assert_eq!(icon.first_frame(), &f0);
528 }
529
530 #[test]
531 fn first_frame_transform() {
532 let data = test_icon("spin");
533 let icon = AnimatedIcon::transform(
534 data.clone(),
535 TransformAnimation::Spin {
536 duration_ms: nz(1000),
537 },
538 );
539 assert_eq!(icon.first_frame(), &data);
540 }
541
542 #[test]
545 fn frame_list_accessor_returns_some_for_frames() {
546 let icon = AnimatedIcon::frames(vec![test_icon("f1")], nz(83)).unwrap();
547 assert!(icon.frame_list().is_some());
548 assert_eq!(icon.frame_list().unwrap().len(), 1);
549 }
550
551 #[test]
552 fn frame_list_accessor_returns_none_for_transform() {
553 let icon = AnimatedIcon::transform(
554 test_icon("spin"),
555 TransformAnimation::Spin {
556 duration_ms: nz(1000),
557 },
558 );
559 assert!(icon.frame_list().is_none());
560 }
561
562 #[test]
563 fn frame_duration_ms_accessor() {
564 let icon = AnimatedIcon::frames(vec![test_icon("f1")], nz(42)).unwrap();
565 assert_eq!(icon.frame_duration_ms().unwrap().get(), 42);
566 }
567
568 #[test]
569 fn icon_accessor_returns_some_for_transform() {
570 let data = test_icon("spin");
571 let icon = AnimatedIcon::transform(
572 data.clone(),
573 TransformAnimation::Spin {
574 duration_ms: nz(1000),
575 },
576 );
577 assert_eq!(icon.icon(), Some(&data));
578 }
579
580 #[test]
581 fn icon_accessor_returns_none_for_frames() {
582 let icon = AnimatedIcon::frames(vec![test_icon("f1")], nz(83)).unwrap();
583 assert!(icon.icon().is_none());
584 }
585
586 #[test]
587 fn animation_accessor_returns_some_for_transform() {
588 let anim = TransformAnimation::Spin {
589 duration_ms: nz(1000),
590 };
591 let icon = AnimatedIcon::transform(test_icon("spin"), anim);
592 assert_eq!(icon.animation(), Some(&anim));
593 }
594
595 #[test]
596 fn animation_accessor_returns_none_for_frames() {
597 let icon = AnimatedIcon::frames(vec![test_icon("f1")], nz(83)).unwrap();
598 assert!(icon.animation().is_none());
599 }
600
601 #[test]
604 #[allow(deprecated)]
605 fn new_frames_valid() {
606 let anim = AnimatedIcon::new_frames(vec![test_icon("f1")], 83);
607 assert!(anim.is_some());
608 }
609
610 #[test]
611 #[allow(deprecated)]
612 fn new_frames_rejects_empty() {
613 assert!(AnimatedIcon::new_frames(vec![], 83).is_none());
614 }
615
616 #[test]
617 #[allow(deprecated)]
618 fn new_frames_rejects_zero_duration() {
619 let frames = vec![test_icon("f1")];
620 assert!(AnimatedIcon::new_frames(frames, 0).is_none());
621 }
622
623 #[test]
626 fn animated_icon_frames_round_trips_json() {
627 let icon = AnimatedIcon::frames(vec![test_icon("f1"), test_icon("f2")], nz(83)).unwrap();
628 let json = serde_json::to_string(&icon).unwrap();
629 let back: AnimatedIcon = serde_json::from_str(&json).unwrap();
630 assert_eq!(icon, back);
631 }
632
633 #[test]
634 fn animated_icon_transform_round_trips_json() {
635 let icon = AnimatedIcon::transform(
636 test_icon("spinner"),
637 TransformAnimation::Spin {
638 duration_ms: nz(1000),
639 },
640 );
641 let json = serde_json::to_string(&icon).unwrap();
642 let back: AnimatedIcon = serde_json::from_str(&json).unwrap();
643 assert_eq!(icon, back);
644 }
645
646 #[test]
647 fn transform_animation_spin_uses_non_zero_u32() {
648 assert!(NonZeroU32::new(0).is_none());
650 let anim = TransformAnimation::Spin {
652 duration_ms: nz(500),
653 };
654 let TransformAnimation::Spin { duration_ms } = anim;
655 assert_eq!(duration_ms.get(), 500);
656 }
657
658 #[test]
661 fn frames_data_accessors() {
662 let icon = AnimatedIcon::frames(vec![test_icon("f1")], nz(83)).unwrap();
663 if let AnimatedIcon::Frames(data) = &icon {
664 assert_eq!(data.frames().len(), 1);
665 assert_eq!(data.frame_duration_ms().get(), 83);
666 } else {
667 panic!("expected Frames variant");
668 }
669 }
670
671 #[test]
672 fn transform_data_accessors() {
673 let icon_data = test_icon("spin");
674 let anim = TransformAnimation::Spin {
675 duration_ms: nz(1000),
676 };
677 let icon = AnimatedIcon::transform(icon_data.clone(), anim);
678 if let AnimatedIcon::Transform(data) = &icon {
679 assert_eq!(data.icon(), &icon_data);
680 assert_eq!(*data.animation(), anim);
681 } else {
682 panic!("expected Transform variant");
683 }
684 }
685}