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