1#![forbid(unsafe_code)]
2
3use std::time::Duration;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
49pub enum ModalAnimationPhase {
50 #[default]
52 Closed,
53 Opening,
55 Open,
57 Closing,
59}
60
61impl ModalAnimationPhase {
62 #[inline]
64 pub fn is_visible(self) -> bool {
65 !matches!(self, Self::Closed)
66 }
67
68 #[inline]
70 pub fn is_animating(self) -> bool {
71 matches!(self, Self::Opening | Self::Closing)
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
81pub enum ModalEntranceAnimation {
82 #[default]
84 ScaleIn,
85 FadeIn,
87 SlideDown,
89 SlideUp,
91 None,
93}
94
95impl ModalEntranceAnimation {
96 pub fn initial_scale(self, config: &ModalAnimationConfig) -> f64 {
100 match self {
101 Self::ScaleIn => config.min_scale,
102 Self::FadeIn | Self::SlideDown | Self::SlideUp | Self::None => 1.0,
103 }
104 }
105
106 pub fn initial_opacity(self) -> f64 {
108 match self {
109 Self::ScaleIn | Self::FadeIn | Self::SlideDown | Self::SlideUp => 0.0,
110 Self::None => 1.0,
111 }
112 }
113
114 pub fn initial_y_offset(self, modal_height: u16) -> i16 {
116 match self {
117 Self::SlideDown => -(modal_height as i16).min(8),
118 Self::SlideUp => (modal_height as i16).min(8),
119 Self::ScaleIn | Self::FadeIn | Self::None => 0,
120 }
121 }
122
123 pub fn scale_at_progress(self, progress: f64, config: &ModalAnimationConfig) -> f64 {
125 let initial = self.initial_scale(config);
126 let p = progress.clamp(0.0, 1.0);
127 initial + (1.0 - initial) * p
128 }
129
130 pub fn opacity_at_progress(self, progress: f64) -> f64 {
132 let initial = self.initial_opacity();
133 let p = progress.clamp(0.0, 1.0);
134 initial + (1.0 - initial) * p
135 }
136
137 pub fn y_offset_at_progress(self, progress: f64, modal_height: u16) -> i16 {
139 let initial = self.initial_y_offset(modal_height);
140 let p = progress.clamp(0.0, 1.0);
141 let inv = 1.0 - p;
142 (initial as f64 * inv).round() as i16
143 }
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
152pub enum ModalExitAnimation {
153 #[default]
155 ScaleOut,
156 FadeOut,
158 SlideUp,
160 SlideDown,
162 None,
164}
165
166impl ModalExitAnimation {
167 pub fn final_scale(self, config: &ModalAnimationConfig) -> f64 {
169 match self {
170 Self::ScaleOut => config.min_scale,
171 Self::FadeOut | Self::SlideUp | Self::SlideDown | Self::None => 1.0,
172 }
173 }
174
175 pub fn final_opacity(self) -> f64 {
177 match self {
178 Self::ScaleOut | Self::FadeOut | Self::SlideUp | Self::SlideDown => 0.0,
179 Self::None => 0.0, }
181 }
182
183 pub fn final_y_offset(self, modal_height: u16) -> i16 {
185 match self {
186 Self::SlideUp => -(modal_height as i16).min(8),
187 Self::SlideDown => (modal_height as i16).min(8),
188 Self::ScaleOut | Self::FadeOut | Self::None => 0,
189 }
190 }
191
192 pub fn scale_at_progress(self, progress: f64, config: &ModalAnimationConfig) -> f64 {
196 let final_scale = self.final_scale(config);
197 let p = progress.clamp(0.0, 1.0);
198 1.0 - (1.0 - final_scale) * p
199 }
200
201 pub fn opacity_at_progress(self, progress: f64) -> f64 {
203 let p = progress.clamp(0.0, 1.0);
204 1.0 - p
205 }
206
207 pub fn y_offset_at_progress(self, progress: f64, modal_height: u16) -> i16 {
209 let final_offset = self.final_y_offset(modal_height);
210 let p = progress.clamp(0.0, 1.0);
211 (final_offset as f64 * p).round() as i16
212 }
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Default)]
224pub enum ModalEasing {
225 Linear,
227 #[default]
229 EaseOut,
230 EaseIn,
232 EaseInOut,
234 Back,
236}
237
238impl ModalEasing {
239 pub fn apply(self, t: f64) -> f64 {
241 let t = t.clamp(0.0, 1.0);
242 match self {
243 Self::Linear => t,
244 Self::EaseOut => {
245 let inv = 1.0 - t;
246 1.0 - inv * inv * inv
247 }
248 Self::EaseIn => t * t * t,
249 Self::EaseInOut => {
250 if t < 0.5 {
251 4.0 * t * t * t
252 } else {
253 let inv = -2.0 * t + 2.0;
254 1.0 - inv * inv * inv / 2.0
255 }
256 }
257 Self::Back => {
258 let c1 = 1.70158;
260 let c3 = c1 + 1.0;
261 let t_minus_1 = t - 1.0;
262 1.0 + c3 * t_minus_1 * t_minus_1 * t_minus_1 + c1 * t_minus_1 * t_minus_1
263 }
264 }
265 }
266
267 pub fn can_overshoot(self) -> bool {
269 matches!(self, Self::Back)
270 }
271}
272
273#[derive(Debug, Clone)]
279pub struct ModalAnimationConfig {
280 pub entrance: ModalEntranceAnimation,
282 pub exit: ModalExitAnimation,
284 pub entrance_duration: Duration,
286 pub exit_duration: Duration,
288 pub entrance_easing: ModalEasing,
290 pub exit_easing: ModalEasing,
292 pub min_scale: f64,
294 pub animate_backdrop: bool,
296 pub backdrop_duration: Duration,
298 pub respect_reduced_motion: bool,
300}
301
302impl Default for ModalAnimationConfig {
303 fn default() -> Self {
304 Self {
305 entrance: ModalEntranceAnimation::ScaleIn,
306 exit: ModalExitAnimation::ScaleOut,
307 entrance_duration: Duration::from_millis(200),
308 exit_duration: Duration::from_millis(150),
309 entrance_easing: ModalEasing::EaseOut,
310 exit_easing: ModalEasing::EaseIn,
311 min_scale: 0.92,
312 animate_backdrop: true,
313 backdrop_duration: Duration::from_millis(150),
314 respect_reduced_motion: true,
315 }
316 }
317}
318
319impl ModalAnimationConfig {
320 pub fn new() -> Self {
322 Self::default()
323 }
324
325 pub fn none() -> Self {
327 Self {
328 entrance: ModalEntranceAnimation::None,
329 exit: ModalExitAnimation::None,
330 entrance_duration: Duration::ZERO,
331 exit_duration: Duration::ZERO,
332 backdrop_duration: Duration::ZERO,
333 ..Default::default()
334 }
335 }
336
337 pub fn reduced_motion() -> Self {
341 Self {
342 entrance: ModalEntranceAnimation::FadeIn,
343 exit: ModalExitAnimation::FadeOut,
344 entrance_duration: Duration::from_millis(100),
345 exit_duration: Duration::from_millis(100),
346 entrance_easing: ModalEasing::Linear,
347 exit_easing: ModalEasing::Linear,
348 min_scale: 1.0,
349 animate_backdrop: true,
350 backdrop_duration: Duration::from_millis(100),
351 respect_reduced_motion: true,
352 }
353 }
354
355 #[must_use]
357 pub fn entrance(mut self, anim: ModalEntranceAnimation) -> Self {
358 self.entrance = anim;
359 self
360 }
361
362 #[must_use]
364 pub fn exit(mut self, anim: ModalExitAnimation) -> Self {
365 self.exit = anim;
366 self
367 }
368
369 #[must_use]
371 pub fn entrance_duration(mut self, duration: Duration) -> Self {
372 self.entrance_duration = duration;
373 self
374 }
375
376 #[must_use]
378 pub fn exit_duration(mut self, duration: Duration) -> Self {
379 self.exit_duration = duration;
380 self
381 }
382
383 #[must_use]
385 pub fn entrance_easing(mut self, easing: ModalEasing) -> Self {
386 self.entrance_easing = easing;
387 self
388 }
389
390 #[must_use]
392 pub fn exit_easing(mut self, easing: ModalEasing) -> Self {
393 self.exit_easing = easing;
394 self
395 }
396
397 #[must_use]
399 pub fn min_scale(mut self, scale: f64) -> Self {
400 self.min_scale = scale.clamp(0.5, 1.0);
401 self
402 }
403
404 #[must_use]
406 pub fn animate_backdrop(mut self, animate: bool) -> Self {
407 self.animate_backdrop = animate;
408 self
409 }
410
411 #[must_use]
413 pub fn backdrop_duration(mut self, duration: Duration) -> Self {
414 self.backdrop_duration = duration;
415 self
416 }
417
418 #[must_use]
420 pub fn respect_reduced_motion(mut self, respect: bool) -> Self {
421 self.respect_reduced_motion = respect;
422 self
423 }
424
425 pub fn is_disabled(&self) -> bool {
427 matches!(self.entrance, ModalEntranceAnimation::None)
428 && matches!(self.exit, ModalExitAnimation::None)
429 }
430
431 pub fn effective(&self, reduced_motion: bool) -> Self {
433 if reduced_motion && self.respect_reduced_motion {
434 Self::reduced_motion()
435 } else {
436 self.clone()
437 }
438 }
439}
440
441#[derive(Debug, Clone)]
450pub struct ModalAnimationState {
451 phase: ModalAnimationPhase,
453 progress: f64,
455 backdrop_progress: f64,
457 reduced_motion: bool,
459}
460
461impl Default for ModalAnimationState {
462 fn default() -> Self {
463 Self::new()
464 }
465}
466
467impl ModalAnimationState {
468 pub fn new() -> Self {
470 Self {
471 phase: ModalAnimationPhase::Closed,
472 progress: 0.0,
473 backdrop_progress: 0.0,
474 reduced_motion: false,
475 }
476 }
477
478 pub fn open() -> Self {
480 Self {
481 phase: ModalAnimationPhase::Open,
482 progress: 1.0,
483 backdrop_progress: 1.0,
484 reduced_motion: false,
485 }
486 }
487
488 pub fn phase(&self) -> ModalAnimationPhase {
490 self.phase
491 }
492
493 pub fn progress(&self) -> f64 {
495 self.progress
496 }
497
498 pub fn backdrop_progress(&self) -> f64 {
500 self.backdrop_progress
501 }
502
503 #[inline]
505 pub fn is_visible(&self) -> bool {
506 self.phase.is_visible()
507 }
508
509 #[inline]
511 pub fn is_animating(&self) -> bool {
512 self.phase.is_animating()
513 }
514
515 #[inline]
517 pub fn is_open(&self) -> bool {
518 matches!(self.phase, ModalAnimationPhase::Open)
519 }
520
521 #[inline]
523 pub fn is_closed(&self) -> bool {
524 matches!(self.phase, ModalAnimationPhase::Closed)
525 }
526
527 pub fn set_reduced_motion(&mut self, enabled: bool) {
529 self.reduced_motion = enabled;
530 }
531
532 pub fn start_opening(&mut self) {
537 match self.phase {
538 ModalAnimationPhase::Closed => {
539 self.phase = ModalAnimationPhase::Opening;
540 self.progress = 0.0;
541 self.backdrop_progress = 0.0;
542 }
543 ModalAnimationPhase::Closing => {
544 self.phase = ModalAnimationPhase::Opening;
546 self.progress = 1.0 - self.progress;
548 self.backdrop_progress = 1.0 - self.backdrop_progress;
549 }
550 ModalAnimationPhase::Opening | ModalAnimationPhase::Open => {
551 }
553 }
554 }
555
556 pub fn start_closing(&mut self) {
561 match self.phase {
562 ModalAnimationPhase::Open => {
563 self.phase = ModalAnimationPhase::Closing;
564 self.progress = 0.0;
565 self.backdrop_progress = 0.0;
566 }
567 ModalAnimationPhase::Opening => {
568 self.phase = ModalAnimationPhase::Closing;
570 self.progress = 1.0 - self.progress;
572 self.backdrop_progress = 1.0 - self.backdrop_progress;
573 }
574 ModalAnimationPhase::Closing | ModalAnimationPhase::Closed => {
575 }
577 }
578 }
579
580 pub fn force_open(&mut self) {
582 self.phase = ModalAnimationPhase::Open;
583 self.progress = 1.0;
584 self.backdrop_progress = 1.0;
585 }
586
587 pub fn force_close(&mut self) {
589 self.phase = ModalAnimationPhase::Closed;
590 self.progress = 0.0;
591 self.backdrop_progress = 0.0;
592 }
593
594 pub fn tick(&mut self, delta: Duration, config: &ModalAnimationConfig) -> bool {
598 let delta_secs = delta.as_secs_f64().max(0.0);
599 let config = config.effective(self.reduced_motion);
600
601 match self.phase {
602 ModalAnimationPhase::Opening => {
603 let content_duration = config.entrance_duration.as_secs_f64();
604 let backdrop_duration = if config.animate_backdrop {
605 config.backdrop_duration.as_secs_f64()
606 } else {
607 0.0
608 };
609
610 if content_duration > 0.0 {
612 self.progress += delta_secs / content_duration;
613 } else {
614 self.progress = 1.0;
615 }
616
617 if backdrop_duration > 0.0 {
619 self.backdrop_progress += delta_secs / backdrop_duration;
620 } else {
621 self.backdrop_progress = 1.0;
622 }
623
624 self.progress = self.progress.min(1.0);
626 self.backdrop_progress = self.backdrop_progress.min(1.0);
627
628 if self.progress >= 1.0 && self.backdrop_progress >= 1.0 {
629 self.phase = ModalAnimationPhase::Open;
630 self.progress = 1.0;
631 self.backdrop_progress = 1.0;
632 return true;
633 }
634 }
635 ModalAnimationPhase::Closing => {
636 let content_duration = config.exit_duration.as_secs_f64();
637 let backdrop_duration = if config.animate_backdrop {
638 config.backdrop_duration.as_secs_f64()
639 } else {
640 0.0
641 };
642
643 if content_duration > 0.0 {
645 self.progress += delta_secs / content_duration;
646 } else {
647 self.progress = 1.0;
648 }
649
650 if backdrop_duration > 0.0 {
652 self.backdrop_progress += delta_secs / backdrop_duration;
653 } else {
654 self.backdrop_progress = 1.0;
655 }
656
657 self.progress = self.progress.min(1.0);
659 self.backdrop_progress = self.backdrop_progress.min(1.0);
660
661 if self.progress >= 1.0 && self.backdrop_progress >= 1.0 {
662 self.phase = ModalAnimationPhase::Closed;
663 self.progress = 0.0;
664 self.backdrop_progress = 0.0;
665 return true;
666 }
667 }
668 ModalAnimationPhase::Open | ModalAnimationPhase::Closed => {
669 }
671 }
672
673 false
674 }
675
676 pub fn eased_progress(&self, config: &ModalAnimationConfig) -> f64 {
678 let config = config.effective(self.reduced_motion);
679 match self.phase {
680 ModalAnimationPhase::Opening => config.entrance_easing.apply(self.progress),
681 ModalAnimationPhase::Closing => config.exit_easing.apply(self.progress),
682 ModalAnimationPhase::Open => 1.0,
683 ModalAnimationPhase::Closed => 0.0,
684 }
685 }
686
687 pub fn eased_backdrop_progress(&self, config: &ModalAnimationConfig) -> f64 {
689 let _config = config.effective(self.reduced_motion);
690 match self.phase {
692 ModalAnimationPhase::Opening => ModalEasing::EaseOut.apply(self.backdrop_progress),
693 ModalAnimationPhase::Closing => ModalEasing::EaseIn.apply(self.backdrop_progress),
694 ModalAnimationPhase::Open => 1.0,
695 ModalAnimationPhase::Closed => 0.0,
696 }
697 }
698
699 pub fn current_scale(&self, config: &ModalAnimationConfig) -> f64 {
703 let config = config.effective(self.reduced_motion);
704 let eased = self.eased_progress(&config);
705
706 match self.phase {
707 ModalAnimationPhase::Opening => config.entrance.scale_at_progress(eased, &config),
708 ModalAnimationPhase::Closing => config.exit.scale_at_progress(eased, &config),
709 ModalAnimationPhase::Open => 1.0,
710 ModalAnimationPhase::Closed => config.entrance.initial_scale(&config),
711 }
712 }
713
714 pub fn current_opacity(&self, config: &ModalAnimationConfig) -> f64 {
718 let config = config.effective(self.reduced_motion);
719 let eased = self.eased_progress(&config);
720
721 match self.phase {
722 ModalAnimationPhase::Opening => config.entrance.opacity_at_progress(eased),
723 ModalAnimationPhase::Closing => config.exit.opacity_at_progress(eased),
724 ModalAnimationPhase::Open => 1.0,
725 ModalAnimationPhase::Closed => 0.0,
726 }
727 }
728
729 pub fn current_backdrop_opacity(&self, config: &ModalAnimationConfig) -> f64 {
733 let config = config.effective(self.reduced_motion);
734
735 if !config.animate_backdrop {
736 return match self.phase {
737 ModalAnimationPhase::Open | ModalAnimationPhase::Opening => 1.0,
738 ModalAnimationPhase::Closed | ModalAnimationPhase::Closing => 0.0,
739 };
740 }
741
742 let eased = self.eased_backdrop_progress(&config);
743
744 match self.phase {
745 ModalAnimationPhase::Opening => eased,
746 ModalAnimationPhase::Closing => 1.0 - eased,
747 ModalAnimationPhase::Open => 1.0,
748 ModalAnimationPhase::Closed => 0.0,
749 }
750 }
751
752 pub fn current_y_offset(&self, config: &ModalAnimationConfig, modal_height: u16) -> i16 {
756 let config = config.effective(self.reduced_motion);
757 let eased = self.eased_progress(&config);
758
759 match self.phase {
760 ModalAnimationPhase::Opening => {
761 config.entrance.y_offset_at_progress(eased, modal_height)
762 }
763 ModalAnimationPhase::Closing => config.exit.y_offset_at_progress(eased, modal_height),
764 ModalAnimationPhase::Open | ModalAnimationPhase::Closed => 0,
765 }
766 }
767
768 pub fn current_values(
772 &self,
773 config: &ModalAnimationConfig,
774 modal_height: u16,
775 ) -> (f64, f64, f64, i16) {
776 (
777 self.current_scale(config),
778 self.current_opacity(config),
779 self.current_backdrop_opacity(config),
780 self.current_y_offset(config, modal_height),
781 )
782 }
783}
784
785#[cfg(test)]
790mod tests {
791 use super::*;
792
793 #[test]
798 fn test_phase_visibility() {
799 assert!(!ModalAnimationPhase::Closed.is_visible());
800 assert!(ModalAnimationPhase::Opening.is_visible());
801 assert!(ModalAnimationPhase::Open.is_visible());
802 assert!(ModalAnimationPhase::Closing.is_visible());
803 }
804
805 #[test]
806 fn test_phase_animating() {
807 assert!(!ModalAnimationPhase::Closed.is_animating());
808 assert!(ModalAnimationPhase::Opening.is_animating());
809 assert!(!ModalAnimationPhase::Open.is_animating());
810 assert!(ModalAnimationPhase::Closing.is_animating());
811 }
812
813 #[test]
814 fn test_start_opening_from_closed() {
815 let mut state = ModalAnimationState::new();
816 assert_eq!(state.phase(), ModalAnimationPhase::Closed);
817
818 state.start_opening();
819 assert_eq!(state.phase(), ModalAnimationPhase::Opening);
820 assert_eq!(state.progress(), 0.0);
821 }
822
823 #[test]
824 fn test_start_closing_from_open() {
825 let mut state = ModalAnimationState::open();
826 assert_eq!(state.phase(), ModalAnimationPhase::Open);
827
828 state.start_closing();
829 assert_eq!(state.phase(), ModalAnimationPhase::Closing);
830 assert_eq!(state.progress(), 0.0);
831 }
832
833 #[test]
834 fn test_rapid_toggle_reverses_animation() {
835 let mut state = ModalAnimationState::new();
836 let config = ModalAnimationConfig::default();
837
838 state.start_opening();
840 state.tick(Duration::from_millis(100), &config); let opening_progress = state.progress();
843 assert!(opening_progress > 0.0);
844 assert!(opening_progress < 1.0);
845
846 state.start_closing();
848 assert_eq!(state.phase(), ModalAnimationPhase::Closing);
849
850 let closing_progress = state.progress();
852 assert!((opening_progress + closing_progress - 1.0).abs() < 0.001);
853 }
854
855 #[test]
856 fn test_opening_noop_when_already_opening() {
857 let mut state = ModalAnimationState::new();
858 state.start_opening();
859 let progress1 = state.progress();
860
861 state.start_opening(); assert_eq!(state.progress(), progress1);
863 assert_eq!(state.phase(), ModalAnimationPhase::Opening);
864 }
865
866 #[test]
871 fn test_tick_advances_progress() {
872 let mut state = ModalAnimationState::new();
873 let config = ModalAnimationConfig::default();
874
875 state.start_opening();
876 assert_eq!(state.progress(), 0.0);
877
878 state.tick(Duration::from_millis(100), &config);
879 assert!(state.progress() > 0.0);
880 assert!(state.progress() < 1.0);
881 }
882
883 #[test]
884 fn test_tick_completes_animation() {
885 let mut state = ModalAnimationState::new();
886 let config = ModalAnimationConfig::default();
887
888 state.start_opening();
889 let changed = state.tick(Duration::from_millis(500), &config);
890
891 assert!(changed);
892 assert_eq!(state.phase(), ModalAnimationPhase::Open);
893 assert_eq!(state.progress(), 1.0);
894 }
895
896 #[test]
897 fn test_zero_duration_completes_instantly() {
898 let mut state = ModalAnimationState::new();
899 let config = ModalAnimationConfig::none();
900
901 state.start_opening();
902 let changed = state.tick(Duration::from_millis(1), &config);
903
904 assert!(changed);
905 assert_eq!(state.phase(), ModalAnimationPhase::Open);
906 }
907
908 #[test]
913 fn test_easing_linear() {
914 assert_eq!(ModalEasing::Linear.apply(0.0), 0.0);
915 assert_eq!(ModalEasing::Linear.apply(0.5), 0.5);
916 assert_eq!(ModalEasing::Linear.apply(1.0), 1.0);
917 }
918
919 #[test]
920 fn test_easing_clamps_input() {
921 assert_eq!(ModalEasing::Linear.apply(-0.5), 0.0);
922 assert_eq!(ModalEasing::Linear.apply(1.5), 1.0);
923 }
924
925 #[test]
926 fn test_easing_ease_out_decelerates() {
927 let linear = ModalEasing::Linear.apply(0.5);
929 let ease_out = ModalEasing::EaseOut.apply(0.5);
930 assert!(ease_out > linear);
931 }
932
933 #[test]
934 fn test_easing_ease_in_accelerates() {
935 let linear = ModalEasing::Linear.apply(0.5);
937 let ease_in = ModalEasing::EaseIn.apply(0.5);
938 assert!(ease_in < linear);
939 }
940
941 #[test]
946 fn test_scale_during_opening() {
947 let mut state = ModalAnimationState::new();
948 let config = ModalAnimationConfig::default();
949
950 let scale = state.current_scale(&config);
952 assert!((scale - config.min_scale).abs() < 0.001);
953
954 state.start_opening();
956 state.tick(Duration::from_millis(100), &config);
957 let mid_scale = state.current_scale(&config);
958 assert!(mid_scale > config.min_scale);
959 assert!(mid_scale < 1.0);
960
961 state.tick(Duration::from_millis(500), &config);
963 let final_scale = state.current_scale(&config);
964 assert!((final_scale - 1.0).abs() < 0.001);
965 }
966
967 #[test]
968 fn test_opacity_during_closing() {
969 let mut state = ModalAnimationState::open();
970 let config = ModalAnimationConfig::default();
971
972 assert!((state.current_opacity(&config) - 1.0).abs() < 0.001);
974
975 state.start_closing();
977 state.tick(Duration::from_millis(75), &config);
978 let mid_opacity = state.current_opacity(&config);
979 assert!(mid_opacity > 0.0);
980 assert!(mid_opacity < 1.0);
981
982 state.tick(Duration::from_millis(500), &config);
984 let final_opacity = state.current_opacity(&config);
985 assert!((final_opacity - 0.0).abs() < 0.001);
986 }
987
988 #[test]
989 fn test_backdrop_opacity_independent() {
990 let mut state = ModalAnimationState::new();
991 let config = ModalAnimationConfig::default()
992 .entrance_duration(Duration::from_millis(200))
993 .backdrop_duration(Duration::from_millis(100));
994
995 state.start_opening();
996
997 state.tick(Duration::from_millis(100), &config);
999
1000 let content_opacity = state.current_opacity(&config);
1001 let backdrop_opacity = state.current_backdrop_opacity(&config);
1002
1003 assert!(backdrop_opacity > content_opacity);
1005 }
1006
1007 #[test]
1012 fn test_reduced_motion_config() {
1013 let config = ModalAnimationConfig::reduced_motion();
1014
1015 assert!(matches!(config.entrance, ModalEntranceAnimation::FadeIn));
1016 assert!(matches!(config.exit, ModalExitAnimation::FadeOut));
1017 assert!((config.min_scale - 1.0).abs() < 0.001); }
1019
1020 #[test]
1021 fn test_reduced_motion_applies_effective_config() {
1022 let mut state = ModalAnimationState::new();
1023 state.set_reduced_motion(true);
1024
1025 let config = ModalAnimationConfig::default();
1026
1027 state.start_opening();
1028 let scale = state.current_scale(&config);
1029
1030 assert!((scale - 1.0).abs() < 0.001);
1032 }
1033
1034 #[test]
1039 fn test_force_open() {
1040 let mut state = ModalAnimationState::new();
1041 state.force_open();
1042
1043 assert_eq!(state.phase(), ModalAnimationPhase::Open);
1044 assert_eq!(state.progress(), 1.0);
1045 assert_eq!(state.backdrop_progress(), 1.0);
1046 }
1047
1048 #[test]
1049 fn test_force_close() {
1050 let mut state = ModalAnimationState::open();
1051 state.force_close();
1052
1053 assert_eq!(state.phase(), ModalAnimationPhase::Closed);
1054 assert_eq!(state.progress(), 0.0);
1055 assert_eq!(state.backdrop_progress(), 0.0);
1056 }
1057
1058 #[test]
1063 fn test_scale_in_initial_scale() {
1064 let config = ModalAnimationConfig::default();
1065 let initial = ModalEntranceAnimation::ScaleIn.initial_scale(&config);
1066 assert!((initial - config.min_scale).abs() < 0.001);
1067 }
1068
1069 #[test]
1070 fn test_fade_in_no_scale() {
1071 let config = ModalAnimationConfig::default();
1072 let initial = ModalEntranceAnimation::FadeIn.initial_scale(&config);
1073 assert!((initial - 1.0).abs() < 0.001);
1074 }
1075
1076 #[test]
1077 fn test_slide_down_y_offset() {
1078 let initial = ModalEntranceAnimation::SlideDown.initial_y_offset(20);
1079 assert!(initial < 0); }
1081
1082 #[test]
1083 fn test_slide_up_y_offset() {
1084 let initial = ModalEntranceAnimation::SlideUp.initial_y_offset(20);
1085 assert!(initial > 0); }
1087
1088 #[test]
1093 fn test_progress_always_in_bounds() {
1094 let mut state = ModalAnimationState::new();
1095 let config = ModalAnimationConfig::default();
1096
1097 state.start_opening();
1098
1099 for _ in 0..100 {
1101 state.tick(Duration::from_millis(100), &config);
1102 assert!(state.progress() >= 0.0);
1103 assert!(state.progress() <= 1.0);
1104 assert!(state.backdrop_progress() >= 0.0);
1105 assert!(state.backdrop_progress() <= 1.0);
1106 }
1107 }
1108
1109 #[test]
1110 fn test_scale_always_in_bounds() {
1111 let mut state = ModalAnimationState::new();
1112 let config = ModalAnimationConfig::default();
1113
1114 state.start_opening();
1115
1116 for i in 0..20 {
1117 state.tick(Duration::from_millis(20), &config);
1118 let scale = state.current_scale(&config);
1119 assert!(
1120 scale >= config.min_scale,
1121 "scale {} < min {} at step {}",
1122 scale,
1123 config.min_scale,
1124 i
1125 );
1126 assert!(scale <= 1.0, "scale {} > 1.0 at step {}", scale, i);
1127 }
1128 }
1129
1130 #[test]
1131 fn test_opacity_always_in_bounds() {
1132 let mut state = ModalAnimationState::new();
1133 let config = ModalAnimationConfig::default();
1134
1135 state.start_opening();
1136
1137 for i in 0..20 {
1138 state.tick(Duration::from_millis(20), &config);
1139 let opacity = state.current_opacity(&config);
1140 assert!(opacity >= 0.0, "opacity {} < 0 at step {}", opacity, i);
1141 assert!(opacity <= 1.0, "opacity {} > 1.0 at step {}", opacity, i);
1142 }
1143 }
1144
1145 #[test]
1148 fn edge_easing_ease_in_out_at_boundary() {
1149 let at_half = ModalEasing::EaseInOut.apply(0.5);
1151 assert!(
1152 (at_half - 0.5).abs() < 0.001,
1153 "EaseInOut at 0.5 should be ~0.5, got {at_half}"
1154 );
1155 assert_eq!(ModalEasing::EaseInOut.apply(0.0), 0.0);
1157 assert!((ModalEasing::EaseInOut.apply(1.0) - 1.0).abs() < 1e-10);
1158 }
1159
1160 #[test]
1161 fn edge_easing_back_overshoots() {
1162 let mid = ModalEasing::Back.apply(0.5);
1165 assert!((ModalEasing::Back.apply(0.0)).abs() < 1e-10);
1167 assert!((ModalEasing::Back.apply(1.0) - 1.0).abs() < 1e-10);
1168 let mut found_overshoot = false;
1170 for i in 1..100 {
1171 let t = i as f64 / 100.0;
1172 let v = ModalEasing::Back.apply(t);
1173 if v > 1.0 {
1174 found_overshoot = true;
1175 break;
1176 }
1177 }
1178 assert!(
1179 found_overshoot,
1180 "Back easing should overshoot 1.0 at some point, mid={mid}"
1181 );
1182 }
1183
1184 #[test]
1185 fn edge_can_overshoot_only_back() {
1186 assert!(!ModalEasing::Linear.can_overshoot());
1187 assert!(!ModalEasing::EaseOut.can_overshoot());
1188 assert!(!ModalEasing::EaseIn.can_overshoot());
1189 assert!(!ModalEasing::EaseInOut.can_overshoot());
1190 assert!(ModalEasing::Back.can_overshoot());
1191 }
1192
1193 #[test]
1194 fn edge_easing_ease_in_endpoints() {
1195 assert_eq!(ModalEasing::EaseIn.apply(0.0), 0.0);
1196 assert!((ModalEasing::EaseIn.apply(1.0) - 1.0).abs() < 1e-10);
1197 }
1198
1199 #[test]
1200 fn edge_easing_ease_out_endpoints() {
1201 assert_eq!(ModalEasing::EaseOut.apply(0.0), 0.0);
1202 assert!((ModalEasing::EaseOut.apply(1.0) - 1.0).abs() < 1e-10);
1203 }
1204
1205 #[test]
1206 fn edge_exit_final_scale_variants() {
1207 let config = ModalAnimationConfig::default();
1208 assert!(
1209 (ModalExitAnimation::ScaleOut.final_scale(&config) - config.min_scale).abs() < 1e-10
1210 );
1211 assert!((ModalExitAnimation::FadeOut.final_scale(&config) - 1.0).abs() < 1e-10);
1212 assert!((ModalExitAnimation::SlideUp.final_scale(&config) - 1.0).abs() < 1e-10);
1213 assert!((ModalExitAnimation::SlideDown.final_scale(&config) - 1.0).abs() < 1e-10);
1214 assert!((ModalExitAnimation::None.final_scale(&config) - 1.0).abs() < 1e-10);
1215 }
1216
1217 #[test]
1218 fn edge_exit_final_opacity_all_zero() {
1219 assert_eq!(ModalExitAnimation::ScaleOut.final_opacity(), 0.0);
1221 assert_eq!(ModalExitAnimation::FadeOut.final_opacity(), 0.0);
1222 assert_eq!(ModalExitAnimation::SlideUp.final_opacity(), 0.0);
1223 assert_eq!(ModalExitAnimation::SlideDown.final_opacity(), 0.0);
1224 assert_eq!(ModalExitAnimation::None.final_opacity(), 0.0);
1225 }
1226
1227 #[test]
1228 fn edge_exit_final_y_offset() {
1229 assert!(ModalExitAnimation::SlideUp.final_y_offset(20) < 0);
1230 assert!(ModalExitAnimation::SlideDown.final_y_offset(20) > 0);
1231 assert_eq!(ModalExitAnimation::ScaleOut.final_y_offset(20), 0);
1232 assert_eq!(ModalExitAnimation::FadeOut.final_y_offset(20), 0);
1233 assert_eq!(ModalExitAnimation::None.final_y_offset(20), 0);
1234 }
1235
1236 #[test]
1237 fn edge_exit_scale_at_progress() {
1238 let config = ModalAnimationConfig::default();
1239 let s0 = ModalExitAnimation::ScaleOut.scale_at_progress(0.0, &config);
1241 assert!((s0 - 1.0).abs() < 1e-10);
1242 let s1 = ModalExitAnimation::ScaleOut.scale_at_progress(1.0, &config);
1244 assert!((s1 - config.min_scale).abs() < 1e-10);
1245 }
1246
1247 #[test]
1248 fn edge_exit_opacity_at_progress() {
1249 assert!((ModalExitAnimation::FadeOut.opacity_at_progress(0.0) - 1.0).abs() < 1e-10);
1250 assert!((ModalExitAnimation::FadeOut.opacity_at_progress(1.0)).abs() < 1e-10);
1251 assert!((ModalExitAnimation::FadeOut.opacity_at_progress(0.5) - 0.5).abs() < 1e-10);
1252 }
1253
1254 #[test]
1255 fn edge_exit_y_offset_at_progress() {
1256 assert_eq!(ModalExitAnimation::SlideUp.y_offset_at_progress(0.0, 20), 0);
1257 let final_offset = ModalExitAnimation::SlideUp.y_offset_at_progress(1.0, 20);
1258 assert_eq!(final_offset, ModalExitAnimation::SlideUp.final_y_offset(20));
1259 }
1260
1261 #[test]
1262 fn edge_entrance_none_instant() {
1263 let config = ModalAnimationConfig::default();
1264 assert!((ModalEntranceAnimation::None.initial_scale(&config) - 1.0).abs() < 1e-10);
1265 assert!((ModalEntranceAnimation::None.initial_opacity() - 1.0).abs() < 1e-10);
1266 assert_eq!(ModalEntranceAnimation::None.initial_y_offset(20), 0);
1267 }
1268
1269 #[test]
1270 fn edge_slide_height_clamped_at_8() {
1271 let down = ModalEntranceAnimation::SlideDown.initial_y_offset(100);
1273 assert_eq!(down, -8);
1274 let up = ModalEntranceAnimation::SlideUp.initial_y_offset(100);
1275 assert_eq!(up, 8);
1276
1277 let exit_up = ModalExitAnimation::SlideUp.final_y_offset(100);
1279 assert_eq!(exit_up, -8);
1280 let exit_down = ModalExitAnimation::SlideDown.final_y_offset(100);
1281 assert_eq!(exit_down, 8);
1282 }
1283
1284 #[test]
1285 fn edge_zero_modal_height_y_offset() {
1286 assert_eq!(ModalEntranceAnimation::SlideDown.initial_y_offset(0), 0);
1287 assert_eq!(ModalEntranceAnimation::SlideUp.initial_y_offset(0), 0);
1288 assert_eq!(ModalExitAnimation::SlideUp.final_y_offset(0), 0);
1289 assert_eq!(ModalExitAnimation::SlideDown.final_y_offset(0), 0);
1290 }
1291
1292 #[test]
1293 fn edge_config_builder_methods() {
1294 let config = ModalAnimationConfig::new()
1295 .entrance(ModalEntranceAnimation::SlideDown)
1296 .exit(ModalExitAnimation::SlideUp)
1297 .entrance_duration(Duration::from_millis(300))
1298 .exit_duration(Duration::from_millis(200))
1299 .entrance_easing(ModalEasing::Back)
1300 .exit_easing(ModalEasing::EaseInOut)
1301 .min_scale(0.8)
1302 .animate_backdrop(false)
1303 .backdrop_duration(Duration::from_millis(50))
1304 .respect_reduced_motion(false);
1305
1306 assert_eq!(config.entrance, ModalEntranceAnimation::SlideDown);
1307 assert_eq!(config.exit, ModalExitAnimation::SlideUp);
1308 assert_eq!(config.entrance_duration, Duration::from_millis(300));
1309 assert_eq!(config.exit_duration, Duration::from_millis(200));
1310 assert_eq!(config.entrance_easing, ModalEasing::Back);
1311 assert_eq!(config.exit_easing, ModalEasing::EaseInOut);
1312 assert!((config.min_scale - 0.8).abs() < 1e-10);
1313 assert!(!config.animate_backdrop);
1314 assert_eq!(config.backdrop_duration, Duration::from_millis(50));
1315 assert!(!config.respect_reduced_motion);
1316 }
1317
1318 #[test]
1319 fn edge_min_scale_clamped() {
1320 let config = ModalAnimationConfig::new().min_scale(0.1);
1322 assert!((config.min_scale - 0.5).abs() < 1e-10);
1323
1324 let config = ModalAnimationConfig::new().min_scale(1.5);
1326 assert!((config.min_scale - 1.0).abs() < 1e-10);
1327
1328 let config = ModalAnimationConfig::new().min_scale(0.75);
1330 assert!((config.min_scale - 0.75).abs() < 1e-10);
1331 }
1332
1333 #[test]
1334 fn edge_is_disabled() {
1335 let config = ModalAnimationConfig::none();
1336 assert!(config.is_disabled());
1337
1338 let config = ModalAnimationConfig::default();
1339 assert!(!config.is_disabled());
1340
1341 let config = ModalAnimationConfig::new()
1343 .entrance(ModalEntranceAnimation::None)
1344 .exit(ModalExitAnimation::FadeOut);
1345 assert!(!config.is_disabled());
1346 }
1347
1348 #[test]
1349 fn edge_effective_without_reduced_motion() {
1350 let config = ModalAnimationConfig::default();
1351 let eff = config.effective(false);
1352 assert_eq!(eff.entrance, ModalEntranceAnimation::ScaleIn);
1354 assert_eq!(eff.exit, ModalExitAnimation::ScaleOut);
1355 }
1356
1357 #[test]
1358 fn edge_effective_with_reduced_motion_but_not_respected() {
1359 let config = ModalAnimationConfig::default().respect_reduced_motion(false);
1360 let eff = config.effective(true);
1361 assert_eq!(eff.entrance, ModalEntranceAnimation::ScaleIn);
1363 }
1364
1365 #[test]
1366 fn edge_current_values_helper() {
1367 let state = ModalAnimationState::open();
1368 let config = ModalAnimationConfig::default();
1369 let (scale, opacity, backdrop, y_offset) = state.current_values(&config, 20);
1370 assert!((scale - 1.0).abs() < 1e-10);
1371 assert!((opacity - 1.0).abs() < 1e-10);
1372 assert!((backdrop - 1.0).abs() < 1e-10);
1373 assert_eq!(y_offset, 0);
1374 }
1375
1376 #[test]
1377 fn edge_current_values_closed() {
1378 let state = ModalAnimationState::new();
1379 let config = ModalAnimationConfig::default();
1380 let (scale, opacity, backdrop, y_offset) = state.current_values(&config, 20);
1381 assert!((scale - config.min_scale).abs() < 1e-10);
1382 assert!(opacity.abs() < 1e-10);
1383 assert!(backdrop.abs() < 1e-10);
1384 assert_eq!(y_offset, 0);
1385 }
1386
1387 #[test]
1388 fn edge_tick_noop_on_open() {
1389 let mut state = ModalAnimationState::open();
1390 let config = ModalAnimationConfig::default();
1391 let changed = state.tick(Duration::from_millis(100), &config);
1392 assert!(!changed);
1393 assert_eq!(state.phase(), ModalAnimationPhase::Open);
1394 }
1395
1396 #[test]
1397 fn edge_tick_noop_on_closed() {
1398 let mut state = ModalAnimationState::new();
1399 let config = ModalAnimationConfig::default();
1400 let changed = state.tick(Duration::from_millis(100), &config);
1401 assert!(!changed);
1402 assert_eq!(state.phase(), ModalAnimationPhase::Closed);
1403 }
1404
1405 #[test]
1406 fn edge_tick_returns_false_mid_animation() {
1407 let mut state = ModalAnimationState::new();
1408 let config = ModalAnimationConfig::default();
1409 state.start_opening();
1410 let changed = state.tick(Duration::from_millis(50), &config);
1412 assert!(!changed);
1413 assert_eq!(state.phase(), ModalAnimationPhase::Opening);
1414 }
1415
1416 #[test]
1417 fn edge_closing_animation_completes_to_closed() {
1418 let mut state = ModalAnimationState::open();
1419 let config = ModalAnimationConfig::default();
1420 state.start_closing();
1421 let changed = state.tick(Duration::from_secs(1), &config);
1422 assert!(changed);
1423 assert_eq!(state.phase(), ModalAnimationPhase::Closed);
1424 assert_eq!(state.progress(), 0.0);
1425 assert_eq!(state.backdrop_progress(), 0.0);
1426 }
1427
1428 #[test]
1429 fn edge_start_opening_when_open_is_noop() {
1430 let mut state = ModalAnimationState::open();
1431 state.start_opening();
1432 assert_eq!(state.phase(), ModalAnimationPhase::Open);
1433 assert_eq!(state.progress(), 1.0);
1434 }
1435
1436 #[test]
1437 fn edge_start_closing_when_closed_is_noop() {
1438 let mut state = ModalAnimationState::new();
1439 state.start_closing();
1440 assert_eq!(state.phase(), ModalAnimationPhase::Closed);
1441 assert_eq!(state.progress(), 0.0);
1442 }
1443
1444 #[test]
1445 fn edge_default_state_equals_new() {
1446 let default = ModalAnimationState::default();
1447 let new = ModalAnimationState::new();
1448 assert_eq!(default.phase(), new.phase());
1449 assert_eq!(default.progress(), new.progress());
1450 assert_eq!(default.backdrop_progress(), new.backdrop_progress());
1451 }
1452
1453 #[test]
1454 fn edge_backdrop_no_animation() {
1455 let mut state = ModalAnimationState::new();
1456 let config = ModalAnimationConfig::default().animate_backdrop(false);
1457 state.start_opening();
1458
1459 let backdrop = state.current_backdrop_opacity(&config);
1461 assert!((backdrop - 1.0).abs() < 1e-10);
1462
1463 state.force_open();
1465 state.start_closing();
1466 let backdrop = state.current_backdrop_opacity(&config);
1467 assert!(backdrop.abs() < 1e-10);
1468 }
1469
1470 #[test]
1471 fn edge_entrance_scale_at_progress_clamped() {
1472 let config = ModalAnimationConfig::default();
1473 let s = ModalEntranceAnimation::ScaleIn.scale_at_progress(-0.5, &config);
1475 assert!((s - config.min_scale).abs() < 1e-10);
1476 let s = ModalEntranceAnimation::ScaleIn.scale_at_progress(2.0, &config);
1477 assert!((s - 1.0).abs() < 1e-10);
1478 }
1479
1480 #[test]
1481 fn edge_entrance_opacity_at_progress_clamped() {
1482 let o = ModalEntranceAnimation::FadeIn.opacity_at_progress(-1.0);
1483 assert!(o.abs() < 1e-10);
1484 let o = ModalEntranceAnimation::FadeIn.opacity_at_progress(5.0);
1485 assert!((o - 1.0).abs() < 1e-10);
1486 }
1487
1488 #[test]
1489 fn edge_entrance_y_offset_at_progress_clamped() {
1490 let y = ModalEntranceAnimation::SlideDown.y_offset_at_progress(-1.0, 20);
1492 assert_eq!(y, ModalEntranceAnimation::SlideDown.initial_y_offset(20));
1493 let y = ModalEntranceAnimation::SlideDown.y_offset_at_progress(5.0, 20);
1495 assert_eq!(y, 0);
1496 }
1497
1498 #[test]
1499 fn edge_phase_default_is_closed() {
1500 assert_eq!(ModalAnimationPhase::default(), ModalAnimationPhase::Closed);
1501 }
1502
1503 #[test]
1504 fn edge_entrance_default_is_scale_in() {
1505 assert_eq!(
1506 ModalEntranceAnimation::default(),
1507 ModalEntranceAnimation::ScaleIn
1508 );
1509 }
1510
1511 #[test]
1512 fn edge_exit_default_is_scale_out() {
1513 assert_eq!(ModalExitAnimation::default(), ModalExitAnimation::ScaleOut);
1514 }
1515
1516 #[test]
1517 fn edge_easing_default_is_ease_out() {
1518 assert_eq!(ModalEasing::default(), ModalEasing::EaseOut);
1519 }
1520
1521 #[test]
1522 fn edge_config_none_fields() {
1523 let config = ModalAnimationConfig::none();
1524 assert_eq!(config.entrance, ModalEntranceAnimation::None);
1525 assert_eq!(config.exit, ModalExitAnimation::None);
1526 assert_eq!(config.entrance_duration, Duration::ZERO);
1527 assert_eq!(config.exit_duration, Duration::ZERO);
1528 assert_eq!(config.backdrop_duration, Duration::ZERO);
1529 }
1530
1531 #[test]
1532 fn edge_state_is_visible_is_closed_is_open() {
1533 let mut state = ModalAnimationState::new();
1534 assert!(!state.is_visible());
1535 assert!(state.is_closed());
1536 assert!(!state.is_open());
1537 assert!(!state.is_animating());
1538
1539 state.start_opening();
1540 assert!(state.is_visible());
1541 assert!(!state.is_closed());
1542 assert!(!state.is_open());
1543 assert!(state.is_animating());
1544
1545 state.force_open();
1546 assert!(state.is_visible());
1547 assert!(!state.is_closed());
1548 assert!(state.is_open());
1549 assert!(!state.is_animating());
1550 }
1551
1552 #[test]
1553 fn edge_force_open_during_closing() {
1554 let mut state = ModalAnimationState::open();
1555 state.start_closing();
1556 let config = ModalAnimationConfig::default();
1557 state.tick(Duration::from_millis(50), &config);
1558 assert_eq!(state.phase(), ModalAnimationPhase::Closing);
1559
1560 state.force_open();
1561 assert_eq!(state.phase(), ModalAnimationPhase::Open);
1562 assert_eq!(state.progress(), 1.0);
1563 }
1564
1565 #[test]
1566 fn edge_force_close_during_opening() {
1567 let mut state = ModalAnimationState::new();
1568 state.start_opening();
1569 let config = ModalAnimationConfig::default();
1570 state.tick(Duration::from_millis(50), &config);
1571
1572 state.force_close();
1573 assert_eq!(state.phase(), ModalAnimationPhase::Closed);
1574 assert_eq!(state.progress(), 0.0);
1575 }
1576
1577 #[test]
1578 fn edge_eased_progress_open_closed() {
1579 let config = ModalAnimationConfig::default();
1580 let state_open = ModalAnimationState::open();
1581 assert!((state_open.eased_progress(&config) - 1.0).abs() < 1e-10);
1582
1583 let state_closed = ModalAnimationState::new();
1584 assert!(state_closed.eased_progress(&config).abs() < 1e-10);
1585 }
1586
1587 #[test]
1588 fn edge_eased_backdrop_progress_open_closed() {
1589 let config = ModalAnimationConfig::default();
1590 let state_open = ModalAnimationState::open();
1591 assert!((state_open.eased_backdrop_progress(&config) - 1.0).abs() < 1e-10);
1592
1593 let state_closed = ModalAnimationState::new();
1594 assert!(state_closed.eased_backdrop_progress(&config).abs() < 1e-10);
1595 }
1596
1597 #[test]
1598 fn edge_clone_debug_phase() {
1599 let phase = ModalAnimationPhase::Opening;
1600 let cloned = phase;
1601 assert_eq!(cloned, ModalAnimationPhase::Opening);
1602 let _ = format!("{phase:?}");
1603 }
1604
1605 #[test]
1606 fn edge_clone_debug_entrance() {
1607 let anim = ModalEntranceAnimation::SlideDown;
1608 let cloned = anim;
1609 assert_eq!(cloned, ModalEntranceAnimation::SlideDown);
1610 let _ = format!("{anim:?}");
1611 }
1612
1613 #[test]
1614 fn edge_clone_debug_exit() {
1615 let anim = ModalExitAnimation::SlideUp;
1616 let cloned = anim;
1617 assert_eq!(cloned, ModalExitAnimation::SlideUp);
1618 let _ = format!("{anim:?}");
1619 }
1620
1621 #[test]
1622 fn edge_clone_debug_easing() {
1623 let easing = ModalEasing::Back;
1624 let _ = format!("{easing:?}");
1625 assert_eq!(easing, ModalEasing::Back);
1627 assert_ne!(easing, ModalEasing::Linear);
1628 }
1629
1630 #[test]
1631 fn edge_clone_debug_config() {
1632 let config = ModalAnimationConfig::default();
1633 let cloned = config.clone();
1634 assert_eq!(cloned.entrance, config.entrance);
1635 assert_eq!(cloned.exit, config.exit);
1636 let _ = format!("{config:?}");
1637 }
1638
1639 #[test]
1640 fn edge_clone_debug_state() {
1641 let mut state = ModalAnimationState::new();
1642 state.start_opening();
1643 let cloned = state.clone();
1644 assert_eq!(cloned.phase(), state.phase());
1645 assert_eq!(cloned.progress(), state.progress());
1646 let _ = format!("{state:?}");
1647 }
1648}