1use scheduler::Instant;
2use std::{cell::Cell, rc::Rc, time::Duration};
3
4use crate::{
5 AnyElement, App, AppContext, Element, ElementId, GlobalElementId, InspectorElementId,
6 IntoElement, Motion, ParentElement, SpringAnimation, SpringConfig, SpringDescription,
7 SpringPlayback, SpringState, SpringTarget, Window,
8};
9
10pub use easing::*;
11use smallvec::SmallVec;
12
13#[derive(Clone)]
38pub struct Animation {
39 pub motion: Motion,
41 pub synced: bool,
44 pub max_fps: Option<f32>,
47}
48
49impl Animation {
50 pub fn new(motion: impl Into<Motion>) -> Self {
54 Self {
55 motion: motion.into(),
56 synced: false,
57 max_fps: None,
58 }
59 }
60
61 pub fn repeat(mut self) -> Self {
66 self.motion = self.motion.repeat_forever();
67 self
68 }
69
70 pub fn repeat_synced(mut self) -> Self {
75 self.motion = self.motion.repeat_forever();
76 self.synced = true;
77 self
78 }
79
80 pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
86 self.motion = self.motion.with_easing(easing);
87 self
88 }
89
90 pub fn with_max_fps(mut self, max_fps: f32) -> Self {
95 self.max_fps = Some(max_fps);
96 self
97 }
98}
99
100pub trait AnimationExt {
110 fn with_animation(
153 self,
154 id: impl Into<ElementId>,
155 animation: Animation,
156 animator: impl Fn(Self, f32) -> Self + 'static,
157 ) -> AnimationElement<Self>
158 where
159 Self: Sized,
160 {
161 AnimationElement {
162 id: id.into(),
163 element: Some(self),
164 animator: Box::new(move |this, _, value| animator(this, value)),
165 animations: smallvec::smallvec![animation],
166 }
167 }
168
169 fn with_animations(
223 self,
224 id: impl Into<ElementId>,
225 animations: Vec<Animation>,
226 animator: impl Fn(Self, usize, f32) -> Self + 'static,
227 ) -> AnimationElement<Self>
228 where
229 Self: Sized,
230 {
231 debug_assert!(!animations.is_empty(), "animations must not be empty");
232
233 AnimationElement {
234 id: id.into(),
235 element: Some(self),
236 animator: Box::new(animator),
237 animations: animations.into(),
238 }
239 }
240
241 fn with_spring<T>(
248 self,
249 id: impl Into<ElementId>,
250 animation: SpringAnimation<T>,
251 animator: impl FnOnce(Self, T::Output) -> Self + 'static,
252 ) -> SpringAnimationElement<Self>
253 where
254 Self: Sized,
255 T: SpringTarget,
256 T::Output: 'static,
257 {
258 let SpringAnimation {
259 motion,
260 target,
261 initial,
262 playback,
263 } = animation;
264 let scalar_target = target.target();
265 SpringAnimationElement {
266 id: id.into(),
267 element: Some(self),
268 motion,
269 target: scalar_target,
270 initial,
271 playback,
272 animator: Some(Box::new(move |this, value| {
273 animator(this, target.resolve(value))
274 })),
275 }
276 }
277}
278
279impl<E: IntoElement + 'static> AnimationExt for E {}
280
281pub struct AnimationElement<E> {
285 id: ElementId,
286 element: Option<E>,
287 animations: SmallVec<[Animation; 1]>,
288 animator: Box<dyn Fn(E, usize, f32) -> E + 'static>,
289}
290
291pub struct SpringAnimationElement<E> {
295 id: ElementId,
296 element: Option<E>,
297 motion: Motion<SpringDescription>,
298 target: f32,
299 initial: Option<f32>,
300 playback: SpringPlayback,
301 animator: Option<Box<dyn FnOnce(E, f32) -> E + 'static>>,
302}
303
304impl<E: ParentElement> ParentElement for SpringAnimationElement<E> {
305 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
306 let Some(element) = &mut self.element else {
307 return;
308 };
309
310 element.extend(elements);
311 }
312}
313
314impl<E> SpringAnimationElement<E> {
315 pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> SpringAnimationElement<E> {
319 self.element = self.element.map(f);
320 self
321 }
322}
323
324impl<E: IntoElement + 'static> IntoElement for SpringAnimationElement<E> {
325 type Element = SpringAnimationElement<E>;
326
327 fn into_element(self) -> Self::Element {
328 self
329 }
330}
331
332impl<E: ParentElement> ParentElement for AnimationElement<E> {
333 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
334 let Some(element) = &mut self.element else {
335 return;
336 };
337
338 element.extend(elements);
339 }
340}
341
342impl<E> AnimationElement<E> {
343 pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> AnimationElement<E> {
364 self.element = self.element.map(f);
365 self
366 }
367}
368
369impl<E: IntoElement + 'static> IntoElement for AnimationElement<E> {
370 type Element = AnimationElement<E>;
371
372 fn into_element(self) -> Self::Element {
373 self
374 }
375}
376
377struct AnimationState {
378 start: Instant,
379 animation_ix: usize,
380 delayed_frame_pending: Rc<Cell<bool>>,
383}
384
385struct SpringElementState {
386 spring: SpringState,
387 target: f32,
388 config: SpringConfig,
389 initial: f32,
390 playback: SpringPlayback,
391 updated_at: Instant,
392}
393
394impl<E: IntoElement + 'static> Element for SpringAnimationElement<E> {
395 type RequestLayoutState = AnyElement;
396 type PrepaintState = ();
397
398 fn id(&self) -> Option<ElementId> {
399 Some(self.id.clone())
400 }
401
402 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
403 None
404 }
405
406 fn request_layout(
407 &mut self,
408 global_id: Option<&GlobalElementId>,
409 _inspector_id: Option<&InspectorElementId>,
410 window: &mut Window,
411 cx: &mut App,
412 ) -> (crate::LayoutId, Self::RequestLayoutState) {
413 window.with_element_state(global_id.unwrap(), |state, window| {
414 let now = cx.background_executor().now();
417 let initial = self.initial.unwrap_or(self.target);
418 let mut state = state.unwrap_or_else(|| SpringElementState {
419 spring: SpringState {
420 position: initial,
421 velocity: 0.0,
422 },
423 target: self.target,
424 config: self.motion.config,
425 initial,
426 playback: self.playback,
427 updated_at: now,
428 });
429
430 let elapsed = now.duration_since(state.updated_at).as_secs_f32();
431 match state.playback {
432 SpringPlayback::Running => {
433 state.spring = state.config.step(state.spring, state.target, elapsed);
434 }
435 SpringPlayback::Paused
436 | SpringPlayback::Stopped
437 | SpringPlayback::Completed
438 | SpringPlayback::Cancelled => {}
439 }
440
441 state.config = self.motion.config;
442 state.target = self.target;
443
444 let done = match self.playback {
445 SpringPlayback::Running => {
446 if cx.reduce_motion() {
447 state.spring = SpringState {
448 position: state.target,
449 velocity: 0.0,
450 };
451 true
452 } else {
453 let done = state.config.is_settled(
454 state.spring,
455 state.target,
456 self.motion.epsilon,
457 );
458 if done {
459 state.spring = SpringState {
460 position: state.target,
461 velocity: 0.0,
462 };
463 }
464 done
465 }
466 }
467 SpringPlayback::Paused => true,
468 SpringPlayback::Stopped => {
469 state.spring.velocity = 0.0;
470 true
471 }
472 SpringPlayback::Completed => {
473 state.spring = SpringState {
474 position: state.target,
475 velocity: 0.0,
476 };
477 true
478 }
479 SpringPlayback::Cancelled => {
480 state.spring = SpringState {
481 position: state.initial,
482 velocity: 0.0,
483 };
484 true
485 }
486 };
487 state.playback = self.playback;
488 state.updated_at = now;
489
490 let element = self.element.take().expect("should only be called once");
491 let animator = self.animator.take().expect("should only be called once");
492 let mut element = animator(element, state.spring.position).into_any_element();
493
494 if !done {
495 window.request_animation_frame();
496 }
497
498 ((element.request_layout(window, cx), element), state)
499 })
500 }
501
502 fn prepaint(
503 &mut self,
504 _id: Option<&GlobalElementId>,
505 _inspector_id: Option<&InspectorElementId>,
506 _bounds: crate::Bounds<crate::Pixels>,
507 element: &mut Self::RequestLayoutState,
508 window: &mut Window,
509 cx: &mut App,
510 ) -> Self::PrepaintState {
511 element.prepaint(window, cx);
512 }
513
514 fn paint(
515 &mut self,
516 _id: Option<&GlobalElementId>,
517 _inspector_id: Option<&InspectorElementId>,
518 _bounds: crate::Bounds<crate::Pixels>,
519 element: &mut Self::RequestLayoutState,
520 _: &mut Self::PrepaintState,
521 window: &mut Window,
522 cx: &mut App,
523 ) {
524 element.paint(window, cx);
525 }
526}
527
528impl<E: IntoElement + 'static> Element for AnimationElement<E> {
529 type RequestLayoutState = AnyElement;
530 type PrepaintState = ();
531
532 fn id(&self) -> Option<ElementId> {
533 Some(self.id.clone())
534 }
535
536 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
537 None
538 }
539
540 fn request_layout(
541 &mut self,
542 global_id: Option<&GlobalElementId>,
543 _inspector_id: Option<&InspectorElementId>,
544 window: &mut Window,
545 cx: &mut App,
546 ) -> (crate::LayoutId, Self::RequestLayoutState) {
547 window.with_element_state(global_id.unwrap(), |state, window| {
548 let now = cx.background_executor().now();
549 let mut state = state.unwrap_or_else(|| AnimationState {
550 start: now,
551 animation_ix: 0,
552 delayed_frame_pending: Rc::new(Cell::new(false)),
553 });
554 let (animation_ix, delta, done) = if cx.reduce_motion() {
555 let animation_ix = self.animations.len() - 1;
556 let delta = self.animations[animation_ix]
557 .motion
558 .resting_progress()
559 .get();
560 (animation_ix, delta, true)
561 } else {
562 let animation_ix = state.animation_ix;
563 let animation = &self.animations[animation_ix];
564 let elapsed = if animation.synced {
565 now - cx.synced_animation_epoch
566 } else {
567 now - state.start
568 };
569 let sample = animation.motion.sample(elapsed);
570 let mut done = !sample.is_active;
571 if done && animation_ix < self.animations.len() - 1 {
572 state.start = now;
573 state.animation_ix += 1;
574 done = false;
575 }
576 (animation_ix, sample.progress.get(), done)
577 };
578
579 debug_assert!(delta.is_finite(), "animated value should be finite");
580
581 let element = self.element.take().expect("should only be called once");
582 let mut element = (self.animator)(element, animation_ix, delta).into_any_element();
583
584 if !done {
585 match self.animations[animation_ix].max_fps {
586 Some(max_fps) if max_fps.is_finite() && max_fps > 0.0 => {
587 if !state.delayed_frame_pending.get() {
588 state.delayed_frame_pending.set(true);
589 let delayed_frame_pending = state.delayed_frame_pending.clone();
590 let view = window.current_view();
591 let interval = Duration::from_secs_f32(1.0 / max_fps);
592 window
593 .spawn(cx, async move |cx| {
594 cx.background_executor().timer(interval).await;
595 delayed_frame_pending.set(false);
596 cx.update(move |_, cx| cx.notify(view)).ok();
597 })
598 .detach();
599 }
600 }
601 _ => window.request_animation_frame(),
602 }
603 }
604
605 ((element.request_layout(window, cx), element), state)
606 })
607 }
608
609 fn prepaint(
610 &mut self,
611 _id: Option<&GlobalElementId>,
612 _inspector_id: Option<&InspectorElementId>,
613 _bounds: crate::Bounds<crate::Pixels>,
614 element: &mut Self::RequestLayoutState,
615 window: &mut Window,
616 cx: &mut App,
617 ) -> Self::PrepaintState {
618 element.prepaint(window, cx);
619 }
620
621 fn paint(
622 &mut self,
623 _id: Option<&GlobalElementId>,
624 _inspector_id: Option<&InspectorElementId>,
625 _bounds: crate::Bounds<crate::Pixels>,
626 element: &mut Self::RequestLayoutState,
627 _: &mut Self::PrepaintState,
628 window: &mut Window,
629 cx: &mut App,
630 ) {
631 element.paint(window, cx);
632 }
633}
634
635mod easing {
636 use std::f32::consts::PI;
637
638 pub fn linear(delta: f32) -> f32 {
648 delta
649 }
650
651 pub fn quadratic(delta: f32) -> f32 {
661 delta * delta
662 }
663
664 pub fn ease_in_out(delta: f32) -> f32 {
676 if delta < 0.5 {
677 2.0 * delta * delta
678 } else {
679 let x = -2.0 * delta + 2.0;
680 1.0 - x * x / 2.0
681 }
682 }
683
684 pub fn ease_out_quint() -> impl Fn(f32) -> f32 {
703 move |delta| 1.0 - (1.0 - delta).powi(5)
704 }
705
706 pub fn bounce(easing: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 {
723 move |delta| {
724 if delta < 0.5 {
725 easing(delta * 2.0)
726 } else {
727 easing((1.0 - delta) * 2.0)
728 }
729 }
730 }
731
732 pub fn pulsating_between(min: f32, max: f32) -> impl Fn(f32) -> f32 {
750 let range = max - min;
751
752 move |delta| {
753 let t = (delta * 2.0 * PI).sin();
755 let breath = (t * t * t + t) / 2.0;
756
757 let normalized_alpha = (breath + 1.0) / 2.0;
759
760 min + (normalized_alpha * range)
761 }
762 }
763}
764
765#[cfg(test)]
766mod tests {
767 use std::{cell::RefCell, rc::Rc, time::Duration};
768
769 use crate::{
770 Animation, Context, InteractiveElement, Pixels, Render, SpringAnimation, SpringConfig,
771 TestAppContext, WindowHandle, div, prelude::*, px, size,
772 };
773
774 use super::*;
775
776 struct AnimationTestView {
777 rendered_deltas: Rc<RefCell<Vec<f32>>>,
778 max_fps: Option<f32>,
779 }
780
781 struct AnimationSequenceTestView {
782 rendered_samples: Rc<RefCell<Vec<(usize, f32)>>>,
783 }
784
785 struct SyncedAnimationTestView {
786 show_second: bool,
787 first_deltas: Rc<RefCell<Vec<f32>>>,
788 second_deltas: Rc<RefCell<Vec<f32>>>,
789 }
790
791 struct SpringAnimationTestView {
792 target: Pixels,
793 initial: Option<Pixels>,
794 playback: SpringPlayback,
795 rendered_values: Rc<RefCell<Vec<Pixels>>>,
796 }
797
798 impl Render for SpringAnimationTestView {
799 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
800 let rendered_values = self.rendered_values.clone();
801 let mut animation = Motion::spring(SpringConfig::new(100.0, 2.0, 1.0))
802 .with_epsilon(0.01)
803 .to(self.target)
804 .playback(self.playback);
805 if let Some(initial) = self.initial {
806 animation = animation.from(initial);
807 }
808 div().with_spring("spring-animation", animation, move |this, value| {
809 rendered_values.borrow_mut().push(value);
810 this.left(value)
811 })
812 }
813 }
814
815 impl Render for SyncedAnimationTestView {
816 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
817 let record_deltas = |deltas: Rc<RefCell<Vec<f32>>>| {
818 move |this, delta| {
819 deltas.borrow_mut().push(delta);
820 this
821 }
822 };
823 div()
824 .size_full()
825 .child(div().with_animation(
826 "first-synced-animation",
827 Animation::new(Duration::from_secs(1)).repeat_synced(),
828 record_deltas(self.first_deltas.clone()),
829 ))
830 .when(self.show_second, |this| {
831 this.child(div().with_animation(
832 "second-synced-animation",
833 Animation::new(Duration::from_secs(1)).repeat_synced(),
834 record_deltas(self.second_deltas.clone()),
835 ))
836 })
837 }
838 }
839
840 impl Render for AnimationTestView {
841 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
842 let rendered_deltas = self.rendered_deltas.clone();
843 let mut animation = Animation::new(Motion::new(Duration::from_secs(1)));
846 if let Some(max_fps) = self.max_fps {
847 animation = animation.repeat_synced().with_max_fps(max_fps);
848 } else {
849 animation = animation.repeat();
850 }
851 div().size_full().child(div().with_animation(
852 "repeating-animation",
853 animation,
854 move |this, delta| {
855 rendered_deltas.borrow_mut().push(delta);
856 this
857 },
858 ))
859 }
860 }
861
862 impl Render for AnimationSequenceTestView {
863 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
864 let rendered_samples = self.rendered_samples.clone();
865 div().with_animations(
866 "animation-sequence",
867 vec![
868 Animation::new(Duration::ZERO),
869 Animation::new(Duration::ZERO),
870 ],
871 move |this, animation_ix, delta| {
872 rendered_samples.borrow_mut().push((animation_ix, delta));
873 this
874 },
875 )
876 }
877 }
878
879 fn open_test_window(
880 cx: &mut TestAppContext,
881 ) -> (Rc<RefCell<Vec<f32>>>, WindowHandle<AnimationTestView>) {
882 open_test_window_with_max_fps(cx, None)
883 }
884
885 fn open_test_window_with_max_fps(
886 cx: &mut TestAppContext,
887 max_fps: Option<f32>,
888 ) -> (Rc<RefCell<Vec<f32>>>, WindowHandle<AnimationTestView>) {
889 let rendered_deltas = Rc::new(RefCell::new(Vec::new()));
890 let window = cx.open_window(size(px(100.), px(100.)), {
891 let rendered_deltas = rendered_deltas.clone();
892 move |_, _| AnimationTestView {
893 rendered_deltas,
894 max_fps,
895 }
896 });
897 cx.run_until_parked();
898 (rendered_deltas, window)
899 }
900
901 fn simulate_next_frame<V: Render>(window: &WindowHandle<V>, cx: &mut TestAppContext) -> usize {
902 let callback_count = window
903 .update(cx, |_, window, cx| window.simulate_next_frame(cx))
904 .unwrap();
905 cx.run_until_parked();
906 callback_count
907 }
908
909 #[test]
910 fn test_animation_wrappers_accept_children() {
911 div()
912 .id("id")
913 .with_animation(
914 "animation",
915 Animation::new(Duration::from_secs(1)),
916 |element, _progress| element,
917 )
918 .child(div());
919
920 div()
921 .id("id")
922 .with_spring(
923 "spring-animation",
924 SpringAnimation::new(SpringConfig::new(100.0, 10.0, 1.0))
925 .to(px(10.0))
926 .from(px(0.0)),
927 |element, value| element.left(value),
928 )
929 .child(div());
930 }
931
932 #[gpui::test]
933 fn test_spring_animation_preserves_velocity_when_retargeted(cx: &mut TestAppContext) {
934 let rendered_values = Rc::new(RefCell::new(Vec::new()));
935 let window = cx.open_window(size(px(100.0), px(100.0)), {
936 let rendered_values = rendered_values.clone();
937 move |_, _| SpringAnimationTestView {
938 target: px(0.0),
939 initial: None,
940 playback: SpringPlayback::Running,
941 rendered_values,
942 }
943 });
944 cx.run_until_parked();
945 assert_eq!(*rendered_values.borrow(), vec![px(0.0)]);
946
947 window
948 .update(cx, |view, _, cx| {
949 view.target = px(100.0);
950 cx.notify();
951 })
952 .unwrap();
953 cx.run_until_parked();
954
955 cx.executor().advance_clock(Duration::from_millis(50));
956 assert!(simulate_next_frame(&window, cx) > 0);
957 cx.refresh().unwrap();
961 cx.run_until_parked();
962 let value_before_retargeting = *rendered_values.borrow().last().unwrap();
963 assert!(value_before_retargeting > px(0.0));
964 assert!(value_before_retargeting < px(100.0));
965
966 window
967 .update(cx, |view, _, cx| {
968 view.target = px(0.0);
969 cx.notify();
970 })
971 .unwrap();
972 cx.run_until_parked();
973
974 cx.executor().advance_clock(Duration::from_millis(5));
975 assert!(simulate_next_frame(&window, cx) > 0);
976 cx.refresh().unwrap();
977 cx.run_until_parked();
978 let value_after_retargeting = *rendered_values.borrow().last().unwrap();
979 assert!(value_after_retargeting > value_before_retargeting);
980 }
981
982 #[gpui::test]
983 fn test_paused_spring_resumes_with_its_velocity(cx: &mut TestAppContext) {
984 let rendered_values = Rc::new(RefCell::new(Vec::new()));
985 let window = cx.open_window(size(px(100.0), px(100.0)), {
986 let rendered_values = rendered_values.clone();
987 move |_, _| SpringAnimationTestView {
988 target: px(0.0),
989 initial: None,
990 playback: SpringPlayback::Running,
991 rendered_values,
992 }
993 });
994 cx.run_until_parked();
995
996 window
997 .update(cx, |view, _, cx| {
998 view.target = px(100.0);
999 cx.notify();
1000 })
1001 .unwrap();
1002 cx.run_until_parked();
1003 cx.executor().advance_clock(Duration::from_millis(50));
1004 assert!(simulate_next_frame(&window, cx) > 0);
1005
1006 window
1007 .update(cx, |view, _, cx| {
1008 view.target = px(0.0);
1009 view.playback = SpringPlayback::Paused;
1010 cx.notify();
1011 })
1012 .unwrap();
1013 cx.run_until_parked();
1014 let paused_value = *rendered_values.borrow().last().unwrap();
1015
1016 cx.executor().advance_clock(Duration::from_millis(500));
1017 assert!(simulate_next_frame(&window, cx) > 0);
1018 assert_eq!(*rendered_values.borrow().last().unwrap(), paused_value);
1019 assert_eq!(simulate_next_frame(&window, cx), 0);
1020
1021 window
1022 .update(cx, |view, _, cx| {
1023 view.playback = SpringPlayback::Running;
1024 cx.notify();
1025 })
1026 .unwrap();
1027 cx.run_until_parked();
1028 cx.executor().advance_clock(Duration::from_millis(5));
1029 assert!(simulate_next_frame(&window, cx) > 0);
1030 assert!(*rendered_values.borrow().last().unwrap() > paused_value);
1031 }
1032
1033 #[gpui::test]
1034 fn test_stopped_spring_resumes_without_velocity(cx: &mut TestAppContext) {
1035 let rendered_values = Rc::new(RefCell::new(Vec::new()));
1036 let window = cx.open_window(size(px(100.0), px(100.0)), {
1037 let rendered_values = rendered_values.clone();
1038 move |_, _| SpringAnimationTestView {
1039 target: px(0.0),
1040 initial: None,
1041 playback: SpringPlayback::Running,
1042 rendered_values,
1043 }
1044 });
1045 cx.run_until_parked();
1046
1047 window
1048 .update(cx, |view, _, cx| {
1049 view.target = px(1_000_000.0);
1050 cx.notify();
1051 })
1052 .unwrap();
1053 cx.run_until_parked();
1054 cx.executor().advance_clock(Duration::from_millis(50));
1055 assert!(simulate_next_frame(&window, cx) > 0);
1056
1057 window
1058 .update(cx, |view, _, cx| {
1059 view.target = px(0.0);
1060 view.playback = SpringPlayback::Stopped;
1061 cx.notify();
1062 })
1063 .unwrap();
1064 cx.run_until_parked();
1065 let stopped_value = *rendered_values.borrow().last().unwrap();
1066
1067 cx.executor().advance_clock(Duration::from_millis(500));
1068 assert!(simulate_next_frame(&window, cx) > 0);
1069 assert_eq!(*rendered_values.borrow().last().unwrap(), stopped_value);
1070 assert_eq!(simulate_next_frame(&window, cx), 0);
1071
1072 window
1073 .update(cx, |view, _, cx| {
1074 view.target = stopped_value;
1075 view.playback = SpringPlayback::Running;
1076 cx.notify();
1077 })
1078 .unwrap();
1079 cx.run_until_parked();
1080 assert_eq!(*rendered_values.borrow().last().unwrap(), stopped_value);
1081 assert_eq!(simulate_next_frame(&window, cx), 0);
1082 }
1083
1084 #[gpui::test]
1085 fn test_cancelled_and_completed_springs_resolve_their_endpoints(cx: &mut TestAppContext) {
1086 let rendered_values = Rc::new(RefCell::new(Vec::new()));
1087 let window = cx.open_window(size(px(100.0), px(100.0)), {
1088 let rendered_values = rendered_values.clone();
1089 move |_, _| SpringAnimationTestView {
1090 target: px(100.0),
1091 initial: Some(px(20.0)),
1092 playback: SpringPlayback::Running,
1093 rendered_values,
1094 }
1095 });
1096 cx.run_until_parked();
1097 assert_eq!(*rendered_values.borrow(), vec![px(20.0)]);
1098
1099 cx.executor().advance_clock(Duration::from_millis(50));
1100 assert!(simulate_next_frame(&window, cx) > 0);
1101 assert!(*rendered_values.borrow().last().unwrap() > px(20.0));
1102
1103 window
1104 .update(cx, |view, _, cx| {
1105 view.playback = SpringPlayback::Cancelled;
1106 cx.notify();
1107 })
1108 .unwrap();
1109 cx.run_until_parked();
1110 assert_eq!(*rendered_values.borrow().last().unwrap(), px(20.0));
1111 assert!(simulate_next_frame(&window, cx) > 0);
1112 assert_eq!(simulate_next_frame(&window, cx), 0);
1113
1114 window
1115 .update(cx, |view, _, cx| {
1116 view.playback = SpringPlayback::Completed;
1117 cx.notify();
1118 })
1119 .unwrap();
1120 cx.run_until_parked();
1121 assert_eq!(*rendered_values.borrow().last().unwrap(), px(100.0));
1122 assert_eq!(simulate_next_frame(&window, cx), 0);
1123 }
1124
1125 #[gpui::test]
1126 fn test_animations_respect_reduced_motion(cx: &mut TestAppContext) {
1127 cx.update(|cx| cx.set_reduce_motion(true));
1128 let rendered_values = Rc::new(RefCell::new(Vec::new()));
1129 let window = cx.open_window(size(px(100.0), px(100.0)), {
1130 let rendered_values = rendered_values.clone();
1131 move |_, _| SpringAnimationTestView {
1132 target: px(100.0),
1133 initial: None,
1134 playback: SpringPlayback::Running,
1135 rendered_values,
1136 }
1137 });
1138 cx.run_until_parked();
1139
1140 assert_eq!(*rendered_values.borrow(), vec![px(100.0)]);
1141 assert_eq!(simulate_next_frame(&window, cx), 0);
1142
1143 let (rendered_deltas, window) = open_test_window(cx);
1144 assert_eq!(*rendered_deltas.borrow(), vec![0.0]);
1145 assert_eq!(simulate_next_frame(&window, cx), 0);
1146 }
1147
1148 #[gpui::test]
1149 fn test_zero_duration_animation_sequence_advances_without_nan(cx: &mut TestAppContext) {
1150 let rendered_samples = Rc::new(RefCell::new(Vec::new()));
1151 let window = cx.open_window(size(px(100.0), px(100.0)), {
1152 let rendered_samples = rendered_samples.clone();
1153 move |_, _| AnimationSequenceTestView { rendered_samples }
1154 });
1155 cx.run_until_parked();
1156
1157 assert_eq!(*rendered_samples.borrow(), vec![(0, 1.0)]);
1158 assert_eq!(simulate_next_frame(&window, cx), 1);
1159 assert_eq!(*rendered_samples.borrow(), vec![(0, 1.0), (1, 1.0)]);
1160 assert_eq!(simulate_next_frame(&window, cx), 0);
1161 }
1162
1163 #[gpui::test]
1164 fn test_repeating_animation_schedules_animation_frames(cx: &mut TestAppContext) {
1165 let (rendered_deltas, window) = open_test_window(cx);
1166
1167 assert_eq!(rendered_deltas.borrow().len(), 1);
1168
1169 for expected_frames in 2..=3 {
1170 assert_eq!(simulate_next_frame(&window, cx), 1);
1171 assert_eq!(rendered_deltas.borrow().len(), expected_frames);
1172 }
1173 }
1174
1175 #[gpui::test]
1176 fn test_max_fps_schedules_timer_driven_frames(cx: &mut TestAppContext) {
1177 let (rendered_deltas, window) = open_test_window_with_max_fps(cx, Some(10.0));
1178
1179 let assert_deltas_approx_eq = |expected: &[f32]| {
1182 let actual = rendered_deltas.borrow();
1183 assert_eq!(actual.len(), expected.len(), "deltas: {actual:?}");
1184 for (actual, expected) in actual.iter().zip(expected) {
1185 assert!(
1186 (actual - expected).abs() < 1e-2,
1187 "expected {expected}, got {actual}"
1188 );
1189 }
1190 };
1191
1192 assert_deltas_approx_eq(&[0.0]);
1193
1194 assert_eq!(simulate_next_frame(&window, cx), 0);
1196 assert_deltas_approx_eq(&[0.0]);
1197
1198 cx.executor().advance_clock(Duration::from_millis(105));
1199 cx.run_until_parked();
1200 assert_deltas_approx_eq(&[0.0, 0.105]);
1201
1202 cx.executor().advance_clock(Duration::from_millis(105));
1203 cx.run_until_parked();
1204 assert_deltas_approx_eq(&[0.0, 0.105, 0.21]);
1205 }
1206
1207 #[gpui::test]
1208 fn test_synced_animations_share_phase_across_elements(cx: &mut TestAppContext) {
1209 let first_deltas = Rc::new(RefCell::new(Vec::new()));
1210 let second_deltas = Rc::new(RefCell::new(Vec::new()));
1211 let window = cx.open_window(size(px(100.), px(100.)), {
1212 let first_deltas = first_deltas.clone();
1213 let second_deltas = second_deltas.clone();
1214 move |_, _| SyncedAnimationTestView {
1215 show_second: false,
1216 first_deltas,
1217 second_deltas,
1218 }
1219 });
1220 cx.run_until_parked();
1221
1222 assert_eq!(*first_deltas.borrow(), vec![0.0]);
1223
1224 cx.executor().advance_clock(Duration::from_millis(250));
1225 simulate_next_frame(&window, cx);
1226 assert_eq!(*first_deltas.borrow(), vec![0.0, 0.25]);
1227
1228 window
1231 .update(cx, |view, _, cx| {
1232 view.show_second = true;
1233 cx.notify();
1234 })
1235 .unwrap();
1236 cx.run_until_parked();
1237 cx.executor().advance_clock(Duration::from_millis(250));
1238 simulate_next_frame(&window, cx);
1239
1240 assert_eq!(*second_deltas.borrow().last().unwrap(), 0.5);
1241 assert_eq!(
1242 *first_deltas.borrow().last().unwrap(),
1243 *second_deltas.borrow().last().unwrap()
1244 );
1245 assert!(second_deltas.borrow().iter().all(|delta| *delta > 0.0));
1246
1247 cx.executor().advance_clock(Duration::from_millis(2250));
1249 simulate_next_frame(&window, cx);
1250 assert_eq!(*first_deltas.borrow().last().unwrap(), 0.75);
1251
1252 cx.executor()
1255 .advance_clock(Duration::from_secs(300 * 24 * 60 * 60) + Duration::from_millis(500));
1256 simulate_next_frame(&window, cx);
1257 assert_eq!(*first_deltas.borrow().last().unwrap(), 0.25);
1258 }
1259}