1use scheduler::Instant;
2use std::{cell::Cell, rc::Rc, time::Duration};
3
4use crate::{
5 AnyElement, App, Element, ElementId, GlobalElementId, IntoElement, ParentElement,
6 SpringAnimation, SpringConfig, SpringPlayback, SpringState, SpringTarget, Window,
7};
8
9pub use easing::*;
10use smallvec::SmallVec;
11
12#[derive(Clone)]
14pub struct Animation {
15 pub duration: Duration,
17 pub oneshot: bool,
19 pub synced: bool,
21 pub easing: Rc<dyn Fn(f32) -> f32>,
24 pub max_fps: Option<f32>,
27}
28
29impl Animation {
30 pub fn new(duration: Duration) -> Self {
33 Self {
34 duration,
35 oneshot: true,
36 synced: false,
37 easing: Rc::new(linear),
38 max_fps: None,
39 }
40 }
41
42 pub fn repeat(mut self) -> Self {
44 self.oneshot = false;
45 self
46 }
47
48 pub fn repeat_synced(mut self) -> Self {
50 self.oneshot = false;
51 self.synced = true;
52 self
53 }
54
55 pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
60 self.easing = Rc::new(easing);
61 self
62 }
63
64 pub fn with_max_fps(mut self, max_fps: f32) -> Self {
69 self.max_fps = Some(max_fps);
70 self
71 }
72}
73
74pub trait AnimationExt {
82 fn with_animation(
84 self,
85 id: impl Into<ElementId>,
86 animation: Animation,
87 animator: impl Fn(Self, f32) -> Self + 'static,
88 ) -> AnimationElement<Self>
89 where
90 Self: Sized,
91 {
92 AnimationElement {
93 id: id.into(),
94 element: Some(self),
95 animator: Box::new(move |this, _, value| animator(this, value)),
96 animations: smallvec::smallvec![animation],
97 }
98 }
99
100 fn with_animations(
102 self,
103 id: impl Into<ElementId>,
104 animations: Vec<Animation>,
105 animator: impl Fn(Self, usize, f32) -> Self + 'static,
106 ) -> AnimationElement<Self>
107 where
108 Self: Sized,
109 {
110 AnimationElement {
111 id: id.into(),
112 element: Some(self),
113 animator: Box::new(animator),
114 animations: animations.into(),
115 }
116 }
117
118 fn with_spring<T>(
124 self,
125 id: impl Into<ElementId>,
126 animation: SpringAnimation<T>,
127 animator: impl FnOnce(Self, T::Output) -> Self + 'static,
128 ) -> SpringAnimationElement<Self>
129 where
130 Self: Sized,
131 T: SpringTarget,
132 T::Output: 'static,
133 {
134 let SpringAnimation {
135 config,
136 target,
137 epsilon,
138 initial,
139 playback,
140 } = animation;
141 let scalar_target = target.target();
142 SpringAnimationElement {
143 id: id.into(),
144 element: Some(self),
145 config,
146 target: scalar_target,
147 epsilon,
148 initial,
149 playback,
150 animator: Some(Box::new(move |this, value| {
151 animator(this, target.resolve(value))
152 })),
153 }
154 }
155}
156
157impl<E: IntoElement + 'static> AnimationExt for E {}
158
159pub struct AnimationElement<E> {
161 id: ElementId,
162 element: Option<E>,
163 animations: SmallVec<[Animation; 1]>,
164 animator: Box<dyn Fn(E, usize, f32) -> E + 'static>,
165}
166
167pub struct SpringAnimationElement<E> {
169 id: ElementId,
170 element: Option<E>,
171 config: SpringConfig,
172 target: f32,
173 epsilon: f32,
174 initial: Option<f32>,
175 playback: SpringPlayback,
176 animator: Option<Box<dyn FnOnce(E, f32) -> E + 'static>>,
177}
178
179impl<E: ParentElement> ParentElement for SpringAnimationElement<E> {
180 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
181 let Some(element) = &mut self.element else {
182 return;
183 };
184
185 element.extend(elements);
186 }
187}
188
189impl<E> SpringAnimationElement<E> {
190 pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> SpringAnimationElement<E> {
193 self.element = self.element.map(f);
194 self
195 }
196}
197
198impl<E: IntoElement + 'static> IntoElement for SpringAnimationElement<E> {
199 type Element = SpringAnimationElement<E>;
200
201 fn into_element(self) -> Self::Element {
202 self
203 }
204}
205
206impl<E: ParentElement> ParentElement for AnimationElement<E> {
207 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
208 let Some(element) = &mut self.element else {
209 return;
210 };
211
212 element.extend(elements);
213 }
214}
215
216impl<E> AnimationElement<E> {
217 pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> AnimationElement<E> {
220 self.element = self.element.map(f);
221 self
222 }
223}
224
225impl<E: IntoElement + 'static> IntoElement for AnimationElement<E> {
226 type Element = AnimationElement<E>;
227
228 fn into_element(self) -> Self::Element {
229 self
230 }
231}
232
233struct AnimationState {
234 start: Instant,
235 animation_ix: usize,
236 delayed_frame_pending: Rc<Cell<bool>>,
239}
240
241struct SpringElementState {
242 spring: SpringState,
243 target: f32,
244 config: SpringConfig,
245 initial: f32,
246 playback: SpringPlayback,
247 updated_at: Instant,
248}
249
250impl<E: IntoElement + 'static> Element for SpringAnimationElement<E> {
251 type RequestLayoutState = AnyElement;
252 type PrepaintState = ();
253
254 fn id(&self) -> Option<ElementId> {
255 Some(self.id.clone())
256 }
257
258 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
259 None
260 }
261
262 fn request_layout(
263 &mut self,
264 global_id: Option<&GlobalElementId>,
265 window: &mut Window,
266 cx: &mut App,
267 ) -> (crate::LayoutId, Self::RequestLayoutState) {
268 window.with_element_state(global_id.unwrap(), |state, window| {
269 let now = Instant::now();
270 let initial = self.initial.unwrap_or(self.target);
271 let mut state = state.unwrap_or_else(|| SpringElementState {
272 spring: SpringState {
273 position: initial,
274 velocity: 0.0,
275 },
276 target: self.target,
277 config: self.config,
278 initial,
279 playback: self.playback,
280 updated_at: now,
281 });
282
283 let elapsed = now.duration_since(state.updated_at).as_secs_f32();
284 match state.playback {
285 SpringPlayback::Running => {
286 state.spring = state.config.step(state.spring, state.target, elapsed);
287 }
288 SpringPlayback::Paused
289 | SpringPlayback::Stopped
290 | SpringPlayback::Completed
291 | SpringPlayback::Cancelled => {}
292 }
293
294 state.config = self.config;
295 state.target = self.target;
296
297 let done = match self.playback {
298 SpringPlayback::Running => {
299 if cx.reduce_motion() {
300 state.spring = SpringState {
301 position: state.target,
302 velocity: 0.0,
303 };
304 true
305 } else {
306 let done =
307 state
308 .config
309 .is_settled(state.spring, state.target, self.epsilon);
310 if done {
311 state.spring = SpringState {
312 position: state.target,
313 velocity: 0.0,
314 };
315 }
316 done
317 }
318 }
319 SpringPlayback::Paused => true,
320 SpringPlayback::Stopped => {
321 state.spring.velocity = 0.0;
322 true
323 }
324 SpringPlayback::Completed => {
325 state.spring = SpringState {
326 position: state.target,
327 velocity: 0.0,
328 };
329 true
330 }
331 SpringPlayback::Cancelled => {
332 state.spring = SpringState {
333 position: state.initial,
334 velocity: 0.0,
335 };
336 true
337 }
338 };
339 state.playback = self.playback;
340 state.updated_at = now;
341
342 let element = self.element.take().expect("should only be called once");
343 let animator = self.animator.take().expect("should only be called once");
344 let mut element = animator(element, state.spring.position).into_any_element();
345
346 if !done {
347 window.request_animation_frame();
348 }
349
350 ((element.request_layout(window, cx), element), state)
351 })
352 }
353
354 fn prepaint(
355 &mut self,
356 _id: Option<&GlobalElementId>,
357 _bounds: crate::Bounds<crate::Pixels>,
358 element: &mut Self::RequestLayoutState,
359 window: &mut Window,
360 cx: &mut App,
361 ) -> Self::PrepaintState {
362 element.prepaint(window, cx);
363 }
364
365 fn paint(
366 &mut self,
367 _id: Option<&GlobalElementId>,
368 _bounds: crate::Bounds<crate::Pixels>,
369 element: &mut Self::RequestLayoutState,
370 _: &mut Self::PrepaintState,
371 window: &mut Window,
372 cx: &mut App,
373 ) {
374 element.paint(window, cx);
375 }
376}
377
378impl<E: IntoElement + 'static> Element for AnimationElement<E> {
379 type RequestLayoutState = AnyElement;
380 type PrepaintState = ();
381
382 fn id(&self) -> Option<ElementId> {
383 Some(self.id.clone())
384 }
385
386 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
387 None
388 }
389
390 fn request_layout(
391 &mut self,
392 global_id: Option<&GlobalElementId>,
393 window: &mut Window,
394 cx: &mut App,
395 ) -> (crate::LayoutId, Self::RequestLayoutState) {
396 window.with_element_state(global_id.unwrap(), |state, window| {
397 let mut state = state.unwrap_or_else(|| AnimationState {
398 start: Instant::now(),
399 animation_ix: 0,
400 delayed_frame_pending: Rc::new(Cell::new(false)),
401 });
402 let (animation_ix, delta, done) = if cx.reduce_motion() {
403 let animation_ix = self.animations.len() - 1;
404 let delta = if self.animations[animation_ix].oneshot {
405 1.0
406 } else {
407 0.0
408 };
409 (animation_ix, delta, true)
410 } else {
411 let animation_ix = state.animation_ix;
412 let duration = self.animations[animation_ix].duration;
413
414 let elapsed = if self.animations[animation_ix].synced && !duration.is_zero() {
415 let elapsed = cx.background_executor().now() - cx.synced_animation_epoch;
416 Duration::from_nanos((elapsed.as_nanos() % duration.as_nanos()) as u64)
418 } else {
419 state.start.elapsed()
420 };
421 let mut delta = elapsed.as_secs_f32() / duration.as_secs_f32();
422
423 let mut done = false;
424 if delta > 1.0 {
425 if self.animations[animation_ix].oneshot {
426 if animation_ix >= self.animations.len() - 1 {
427 done = true;
428 } else {
429 state.start = Instant::now();
430 state.animation_ix += 1;
431 }
432 delta = 1.0;
433 } else {
434 delta %= 1.0;
435 }
436 }
437 (animation_ix, delta, done)
438 };
439 let delta = (self.animations[animation_ix].easing)(delta);
440
441 debug_assert!(delta.is_finite(), "animated value should be finite");
442
443 let element = self.element.take().expect("should only be called once");
444 let mut element = (self.animator)(element, animation_ix, delta).into_any_element();
445
446 if !done {
447 match self.animations[animation_ix].max_fps {
448 Some(max_fps) if max_fps.is_finite() && max_fps > 0.0 => {
449 if !state.delayed_frame_pending.get() {
450 state.delayed_frame_pending.set(true);
451 let delayed_frame_pending = state.delayed_frame_pending.clone();
452 let view = window.current_view();
453 let interval = Duration::from_secs_f32(1.0 / max_fps);
454 window
455 .spawn(cx, async move |cx| {
456 cx.background_executor().timer(interval).await;
457 delayed_frame_pending.set(false);
458 cx.update(move |_, cx| cx.notify(view)).ok();
459 })
460 .detach();
461 }
462 }
463 _ => window.request_animation_frame(),
464 }
465 }
466
467 ((element.request_layout(window, cx), element), state)
468 })
469 }
470
471 fn prepaint(
472 &mut self,
473 _id: Option<&GlobalElementId>,
474 _bounds: crate::Bounds<crate::Pixels>,
475 element: &mut Self::RequestLayoutState,
476 window: &mut Window,
477 cx: &mut App,
478 ) -> Self::PrepaintState {
479 element.prepaint(window, cx);
480 }
481
482 fn paint(
483 &mut self,
484 _id: Option<&GlobalElementId>,
485 _bounds: crate::Bounds<crate::Pixels>,
486 element: &mut Self::RequestLayoutState,
487 _: &mut Self::PrepaintState,
488 window: &mut Window,
489 cx: &mut App,
490 ) {
491 element.paint(window, cx);
492 }
493}
494
495mod easing {
496 use std::f32::consts::PI;
497
498 pub fn linear(delta: f32) -> f32 {
500 delta
501 }
502
503 pub fn quadratic(delta: f32) -> f32 {
505 delta * delta
506 }
507
508 pub fn ease_in_out(delta: f32) -> f32 {
510 if delta < 0.5 {
511 2.0 * delta * delta
512 } else {
513 let x = -2.0 * delta + 2.0;
514 1.0 - x * x / 2.0
515 }
516 }
517
518 pub fn ease_out_quint() -> impl Fn(f32) -> f32 {
520 move |delta| 1.0 - (1.0 - delta).powi(5)
521 }
522
523 pub fn bounce(easing: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 {
525 move |delta| {
526 if delta < 0.5 {
527 easing(delta * 2.0)
528 } else {
529 easing((1.0 - delta) * 2.0)
530 }
531 }
532 }
533
534 pub fn pulsating_between(min: f32, max: f32) -> impl Fn(f32) -> f32 {
536 let range = max - min;
537
538 move |delta| {
539 let t = (delta * 2.0 * PI).sin();
541 let breath = (t * t * t + t) / 2.0;
542
543 let normalized_alpha = (breath + 1.0) / 2.0;
545
546 min + (normalized_alpha * range)
547 }
548 }
549}
550
551#[cfg(test)]
552mod tests {
553 use std::{cell::RefCell, rc::Rc, time::Duration};
554
555 use crate::{
556 Animation, Context, InteractiveElement, Pixels, Render, SpringAnimation, SpringConfig,
557 TestAppContext, WindowHandle, div, prelude::*, px, size,
558 };
559
560 use super::*;
561
562 struct AnimationTestView {
563 rendered_deltas: Rc<RefCell<Vec<f32>>>,
564 max_fps: Option<f32>,
565 }
566
567 struct SyncedAnimationTestView {
568 show_second: bool,
569 first_deltas: Rc<RefCell<Vec<f32>>>,
570 second_deltas: Rc<RefCell<Vec<f32>>>,
571 }
572
573 struct SpringAnimationTestView {
574 target: Pixels,
575 initial: Option<Pixels>,
576 playback: SpringPlayback,
577 rendered_values: Rc<RefCell<Vec<Pixels>>>,
578 }
579
580 impl Render for SpringAnimationTestView {
581 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
582 let rendered_values = self.rendered_values.clone();
583 let mut animation = SpringAnimation::new(SpringConfig::new(100.0, 2.0, 1.0))
584 .to(self.target)
585 .with_epsilon(0.01)
586 .playback(self.playback);
587 if let Some(initial) = self.initial {
588 animation = animation.from(initial);
589 }
590 div().with_spring("spring-animation", animation, move |this, value| {
591 rendered_values.borrow_mut().push(value);
592 this.left(value)
593 })
594 }
595 }
596
597 impl Render for SyncedAnimationTestView {
598 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
599 let record_deltas = |deltas: Rc<RefCell<Vec<f32>>>| {
600 move |this, delta| {
601 deltas.borrow_mut().push(delta);
602 this
603 }
604 };
605 div()
606 .size_full()
607 .child(div().with_animation(
608 "first-synced-animation",
609 Animation::new(Duration::from_secs(1)).repeat_synced(),
610 record_deltas(self.first_deltas.clone()),
611 ))
612 .when(self.show_second, |this| {
613 this.child(div().with_animation(
614 "second-synced-animation",
615 Animation::new(Duration::from_secs(1)).repeat_synced(),
616 record_deltas(self.second_deltas.clone()),
617 ))
618 })
619 }
620 }
621
622 impl Render for AnimationTestView {
623 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
624 let rendered_deltas = self.rendered_deltas.clone();
625 let mut animation = Animation::new(Duration::from_secs(1));
628 if let Some(max_fps) = self.max_fps {
629 animation = animation.repeat_synced().with_max_fps(max_fps);
630 } else {
631 animation = animation.repeat();
632 }
633 div().size_full().child(div().with_animation(
634 "repeating-animation",
635 animation,
636 move |this, delta| {
637 rendered_deltas.borrow_mut().push(delta);
638 this
639 },
640 ))
641 }
642 }
643
644 fn open_test_window(
645 cx: &mut TestAppContext,
646 ) -> (Rc<RefCell<Vec<f32>>>, WindowHandle<AnimationTestView>) {
647 open_test_window_with_max_fps(cx, None)
648 }
649
650 fn open_test_window_with_max_fps(
651 cx: &mut TestAppContext,
652 max_fps: Option<f32>,
653 ) -> (Rc<RefCell<Vec<f32>>>, WindowHandle<AnimationTestView>) {
654 let rendered_deltas = Rc::new(RefCell::new(Vec::new()));
655 let window = cx.open_window(size(px(100.), px(100.)), {
656 let rendered_deltas = rendered_deltas.clone();
657 move |_, _| AnimationTestView {
658 rendered_deltas,
659 max_fps,
660 }
661 });
662 cx.run_until_parked();
663 (rendered_deltas, window)
664 }
665
666 fn simulate_next_frame<V: Render>(window: &WindowHandle<V>, cx: &mut TestAppContext) -> usize {
667 let callback_count = window
668 .update(cx, |_, window, cx| window.simulate_next_frame(cx))
669 .unwrap();
670 cx.run_until_parked();
671 callback_count
672 }
673 #[test]
677 fn test_animation_parent() {
678 div()
679 .id("id")
680 .with_animation(
682 "animation",
683 Animation::new(Duration::from_secs(1)),
684 |el, _t| {
685 el
687 },
688 )
689 .child(
690 div(),
692 );
693 }
694
695 #[test]
696 fn test_spring_animation_parent() {
697 div()
698 .id("id")
699 .with_spring(
700 "spring-animation",
701 SpringAnimation::new(SpringConfig::new(100.0, 10.0, 1.0))
702 .to(px(10.0))
703 .from(px(0.0)),
704 |element, value| element.left(value),
705 )
706 .child(div());
707 }
708
709 #[gpui::test]
710 fn test_spring_animation_preserves_velocity_when_retargeted(cx: &mut TestAppContext) {
711 let rendered_values = Rc::new(RefCell::new(Vec::new()));
712 let window = cx.open_window(size(px(100.0), px(100.0)), {
713 let rendered_values = rendered_values.clone();
714 move |_, _| SpringAnimationTestView {
715 target: px(0.0),
716 initial: None,
717 playback: SpringPlayback::Running,
718 rendered_values,
719 }
720 });
721 cx.run_until_parked();
722 assert_eq!(*rendered_values.borrow(), vec![px(0.0)]);
723
724 window
725 .update(cx, |view, _, cx| {
726 view.target = px(100.0);
727 cx.notify();
728 })
729 .unwrap();
730 cx.run_until_parked();
731
732 cx.executor().advance_clock(Duration::from_millis(50));
733 assert!(simulate_next_frame(&window, cx) > 0);
734 let value_before_retargeting = *rendered_values.borrow().last().unwrap();
735 assert!(value_before_retargeting > px(0.0));
736 assert!(value_before_retargeting < px(100.0));
737
738 window
739 .update(cx, |view, _, cx| {
740 view.target = px(0.0);
741 cx.notify();
742 })
743 .unwrap();
744 cx.run_until_parked();
745
746 cx.executor().advance_clock(Duration::from_millis(5));
747 assert!(simulate_next_frame(&window, cx) > 0);
748 let value_after_retargeting = *rendered_values.borrow().last().unwrap();
749 assert!(value_after_retargeting > value_before_retargeting);
750 }
751
752 #[gpui::test]
753 fn test_paused_spring_resumes_with_its_velocity(cx: &mut TestAppContext) {
754 let rendered_values = Rc::new(RefCell::new(Vec::new()));
755 let window = cx.open_window(size(px(100.0), px(100.0)), {
756 let rendered_values = rendered_values.clone();
757 move |_, _| SpringAnimationTestView {
758 target: px(0.0),
759 initial: None,
760 playback: SpringPlayback::Running,
761 rendered_values,
762 }
763 });
764 cx.run_until_parked();
765
766 window
767 .update(cx, |view, _, cx| {
768 view.target = px(100.0);
769 cx.notify();
770 })
771 .unwrap();
772 cx.run_until_parked();
773 cx.executor().advance_clock(Duration::from_millis(50));
774 assert!(simulate_next_frame(&window, cx) > 0);
775
776 window
777 .update(cx, |view, _, cx| {
778 view.target = px(0.0);
779 view.playback = SpringPlayback::Paused;
780 cx.notify();
781 })
782 .unwrap();
783 cx.run_until_parked();
784 let paused_value = *rendered_values.borrow().last().unwrap();
785
786 cx.executor().advance_clock(Duration::from_millis(500));
787 assert!(simulate_next_frame(&window, cx) > 0);
788 assert_eq!(*rendered_values.borrow().last().unwrap(), paused_value);
789 assert_eq!(simulate_next_frame(&window, cx), 0);
790
791 window
792 .update(cx, |view, _, cx| {
793 view.playback = SpringPlayback::Running;
794 cx.notify();
795 })
796 .unwrap();
797 cx.run_until_parked();
798 cx.executor().advance_clock(Duration::from_millis(5));
799 assert!(simulate_next_frame(&window, cx) > 0);
800 assert!(*rendered_values.borrow().last().unwrap() > paused_value);
801 }
802
803 #[gpui::test]
804 fn test_stopped_spring_resumes_without_velocity(cx: &mut TestAppContext) {
805 let rendered_values = Rc::new(RefCell::new(Vec::new()));
806 let window = cx.open_window(size(px(100.0), px(100.0)), {
807 let rendered_values = rendered_values.clone();
808 move |_, _| SpringAnimationTestView {
809 target: px(0.0),
810 initial: None,
811 playback: SpringPlayback::Running,
812 rendered_values,
813 }
814 });
815 cx.run_until_parked();
816
817 window
818 .update(cx, |view, _, cx| {
819 view.target = px(1_000_000.0);
820 cx.notify();
821 })
822 .unwrap();
823 cx.run_until_parked();
824 cx.executor().advance_clock(Duration::from_millis(50));
825 assert!(simulate_next_frame(&window, cx) > 0);
826
827 window
828 .update(cx, |view, _, cx| {
829 view.target = px(0.0);
830 view.playback = SpringPlayback::Stopped;
831 cx.notify();
832 })
833 .unwrap();
834 cx.run_until_parked();
835 let stopped_value = *rendered_values.borrow().last().unwrap();
836
837 cx.executor().advance_clock(Duration::from_millis(500));
838 assert!(simulate_next_frame(&window, cx) > 0);
839 assert_eq!(*rendered_values.borrow().last().unwrap(), stopped_value);
840 assert_eq!(simulate_next_frame(&window, cx), 0);
841
842 window
843 .update(cx, |view, _, cx| {
844 view.target = stopped_value;
845 view.playback = SpringPlayback::Running;
846 cx.notify();
847 })
848 .unwrap();
849 cx.run_until_parked();
850 assert_eq!(*rendered_values.borrow().last().unwrap(), stopped_value);
851 assert_eq!(simulate_next_frame(&window, cx), 0);
852 }
853
854 #[gpui::test]
855 fn test_cancelled_and_completed_springs_resolve_their_endpoints(cx: &mut TestAppContext) {
856 let rendered_values = Rc::new(RefCell::new(Vec::new()));
857 let window = cx.open_window(size(px(100.0), px(100.0)), {
858 let rendered_values = rendered_values.clone();
859 move |_, _| SpringAnimationTestView {
860 target: px(100.0),
861 initial: Some(px(20.0)),
862 playback: SpringPlayback::Running,
863 rendered_values,
864 }
865 });
866 cx.run_until_parked();
867 assert_eq!(*rendered_values.borrow(), vec![px(20.0)]);
868
869 cx.executor().advance_clock(Duration::from_millis(50));
870 assert!(simulate_next_frame(&window, cx) > 0);
871 assert!(*rendered_values.borrow().last().unwrap() > px(20.0));
872
873 window
874 .update(cx, |view, _, cx| {
875 view.playback = SpringPlayback::Cancelled;
876 cx.notify();
877 })
878 .unwrap();
879 cx.run_until_parked();
880 assert_eq!(*rendered_values.borrow().last().unwrap(), px(20.0));
881 assert!(simulate_next_frame(&window, cx) > 0);
882 assert_eq!(simulate_next_frame(&window, cx), 0);
883
884 window
885 .update(cx, |view, _, cx| {
886 view.playback = SpringPlayback::Completed;
887 cx.notify();
888 })
889 .unwrap();
890 cx.run_until_parked();
891 assert_eq!(*rendered_values.borrow().last().unwrap(), px(100.0));
892 assert_eq!(simulate_next_frame(&window, cx), 0);
893 }
894
895 #[gpui::test]
896 fn test_spring_animation_respects_reduced_motion(cx: &mut TestAppContext) {
897 cx.update(|cx| cx.set_reduce_motion(true));
898 let rendered_values = Rc::new(RefCell::new(Vec::new()));
899 let window = cx.open_window(size(px(100.0), px(100.0)), {
900 let rendered_values = rendered_values.clone();
901 move |_, _| SpringAnimationTestView {
902 target: px(100.0),
903 initial: None,
904 playback: SpringPlayback::Running,
905 rendered_values,
906 }
907 });
908 cx.run_until_parked();
909
910 assert_eq!(*rendered_values.borrow(), vec![px(100.0)]);
911 assert_eq!(simulate_next_frame(&window, cx), 0);
912 }
913
914 #[gpui::test]
915 fn test_repeating_animation_schedules_animation_frames(cx: &mut TestAppContext) {
916 let (rendered_deltas, window) = open_test_window(cx);
917
918 assert_eq!(rendered_deltas.borrow().len(), 1);
919
920 for expected_frames in 2..=3 {
921 assert_eq!(simulate_next_frame(&window, cx), 1);
922 assert_eq!(rendered_deltas.borrow().len(), expected_frames);
923 }
924 }
925
926 #[gpui::test]
927 fn test_max_fps_schedules_timer_driven_frames(cx: &mut TestAppContext) {
928 let (rendered_deltas, window) = open_test_window_with_max_fps(cx, Some(10.0));
929
930 let assert_deltas_approx_eq = |expected: &[f32]| {
933 let actual = rendered_deltas.borrow();
934 assert_eq!(actual.len(), expected.len(), "deltas: {actual:?}");
935 for (actual, expected) in actual.iter().zip(expected) {
936 assert!(
937 (actual - expected).abs() < 1e-2,
938 "expected {expected}, got {actual}"
939 );
940 }
941 };
942
943 assert_deltas_approx_eq(&[0.0]);
944
945 assert_eq!(simulate_next_frame(&window, cx), 0);
947 assert_deltas_approx_eq(&[0.0]);
948
949 cx.executor().advance_clock(Duration::from_millis(105));
950 cx.run_until_parked();
951 assert_deltas_approx_eq(&[0.0, 0.105]);
952
953 cx.executor().advance_clock(Duration::from_millis(105));
954 cx.run_until_parked();
955 assert_deltas_approx_eq(&[0.0, 0.105, 0.21]);
956 }
957
958 #[gpui::test]
959 fn test_synced_animations_share_phase_across_elements(cx: &mut TestAppContext) {
960 let first_deltas = Rc::new(RefCell::new(Vec::new()));
961 let second_deltas = Rc::new(RefCell::new(Vec::new()));
962 let window = cx.open_window(size(px(100.), px(100.)), {
963 let first_deltas = first_deltas.clone();
964 let second_deltas = second_deltas.clone();
965 move |_, _| SyncedAnimationTestView {
966 show_second: false,
967 first_deltas,
968 second_deltas,
969 }
970 });
971 cx.run_until_parked();
972
973 assert_eq!(*first_deltas.borrow(), vec![0.0]);
974
975 cx.executor().advance_clock(Duration::from_millis(250));
976 simulate_next_frame(&window, cx);
977 assert_eq!(*first_deltas.borrow(), vec![0.0, 0.25]);
978
979 window
982 .update(cx, |view, _, cx| {
983 view.show_second = true;
984 cx.notify();
985 })
986 .unwrap();
987 cx.run_until_parked();
988 cx.executor().advance_clock(Duration::from_millis(250));
989 simulate_next_frame(&window, cx);
990
991 assert_eq!(*second_deltas.borrow().last().unwrap(), 0.5);
992 assert_eq!(
993 *first_deltas.borrow().last().unwrap(),
994 *second_deltas.borrow().last().unwrap()
995 );
996 assert!(second_deltas.borrow().iter().all(|delta| *delta > 0.0));
997
998 cx.executor().advance_clock(Duration::from_millis(2250));
1000 simulate_next_frame(&window, cx);
1001 assert_eq!(*first_deltas.borrow().last().unwrap(), 0.75);
1002
1003 cx.executor()
1006 .advance_clock(Duration::from_secs(300 * 24 * 60 * 60) + Duration::from_millis(500));
1007 simulate_next_frame(&window, cx);
1008 assert_eq!(*first_deltas.borrow().last().unwrap(), 0.25);
1009 }
1010
1011 #[gpui::test]
1012 fn test_reduce_motion_renders_single_static_frame(cx: &mut TestAppContext) {
1013 cx.update(|cx| cx.set_reduce_motion(true));
1014 let (rendered_deltas, window) = open_test_window(cx);
1015
1016 assert_eq!(*rendered_deltas.borrow(), vec![0.0]);
1017
1018 assert_eq!(simulate_next_frame(&window, cx), 0);
1019 assert_eq!(*rendered_deltas.borrow(), vec![0.0]);
1020 }
1021}