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 = Instant::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 mut state = state.unwrap_or_else(|| AnimationState {
569 start: Instant::now(),
570 animation_ix: 0,
571 repaint_pending: Rc::new(Cell::new(false)),
572 repaint_generation: Rc::new(Cell::new(0)),
573 });
574
575 let cancelled = self.cancel_handle.as_ref().map_or(false, |h| h.get());
576
577 let animation_ix = state.animation_ix;
578 let animation = self.animations[animation_ix].clone();
579
580 let (delta, done) = if cancelled {
581 (1.0_f32, true)
582 } else {
583 let elapsed = state.start.elapsed();
584 match animation_pass(&animation, elapsed) {
585 AnimationPass::Delayed => ((animation.easing)(0.0), false),
586 AnimationPass::Running { cycle_delta } => {
587 ((animation.easing)(cycle_delta), false)
588 }
589 AnimationPass::Finished => {
590 if animation_ix >= self.animations.len() - 1 {
591 ((animation.easing)(1.0), true)
592 } else {
593 state.start = Instant::now();
594 state.animation_ix += 1;
595 ((animation.easing)(1.0), false)
596 }
597 }
598 }
599 };
600
601 debug_assert!(delta.is_finite(), "animated value should be finite");
602
603 let element = self.element.take().expect("should only be called once");
604 let mut element = (self.animator)(element, animation_ix, delta).into_any_element();
605
606 let animation_changed = state.animation_ix != animation_ix;
607 if animation_changed || done {
608 invalidate_scheduled_repaint(&state);
612 }
613
614 if !done {
615 let animation = &self.animations[state.animation_ix];
616 let elapsed = state.start.elapsed();
617 if elapsed < animation.delay {
618 let remaining = animation.delay - elapsed;
619 schedule_repaint(
620 window,
621 cx,
622 remaining,
623 &state.repaint_pending,
624 &state.repaint_generation,
625 );
626 } else {
627 match animation.max_fps {
628 Some(max_fps) if max_fps.is_finite() && max_fps > 0.0 => {
629 let interval = Duration::from_secs_f32(1.0 / max_fps);
630 schedule_repaint(
631 window,
632 cx,
633 interval,
634 &state.repaint_pending,
635 &state.repaint_generation,
636 );
637 }
638 _ => window.request_animation_frame(),
639 }
640 }
641 }
642
643 ((element.request_layout(window, cx), element), state)
644 })
645 }
646
647 fn prepaint(
648 &mut self,
649 _id: Option<&GlobalElementId>,
650 _inspector_id: Option<&InspectorElementId>,
651 _bounds: crate::Bounds<crate::Pixels>,
652 element: &mut Self::RequestLayoutState,
653 window: &mut Window,
654 cx: &mut App,
655 ) -> Self::PrepaintState {
656 element.prepaint(window, cx);
657 }
658
659 fn paint(
660 &mut self,
661 _id: Option<&GlobalElementId>,
662 _inspector_id: Option<&InspectorElementId>,
663 _bounds: crate::Bounds<crate::Pixels>,
664 element: &mut Self::RequestLayoutState,
665 _: &mut Self::PrepaintState,
666 window: &mut Window,
667 cx: &mut App,
668 ) {
669 element.paint(window, cx);
670 }
671}
672
673mod easing {
674 use std::f32::consts::PI;
675
676 pub fn linear(delta: f32) -> f32 {
678 delta
679 }
680
681 pub fn quadratic(delta: f32) -> f32 {
683 delta * delta
684 }
685
686 pub fn ease_in_out(delta: f32) -> f32 {
688 if delta < 0.5 {
689 2.0 * delta * delta
690 } else {
691 let x = -2.0 * delta + 2.0;
692 1.0 - x * x / 2.0
693 }
694 }
695
696 pub fn ease_out_quint() -> impl Fn(f32) -> f32 {
698 move |delta| 1.0 - (1.0 - delta).powi(5)
699 }
700
701 pub fn bounce(easing: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 {
703 move |delta| {
704 if delta < 0.5 {
705 easing(delta * 2.0)
706 } else {
707 easing((1.0 - delta) * 2.0)
708 }
709 }
710 }
711
712 pub fn pulsating_between(min: f32, max: f32) -> impl Fn(f32) -> f32 {
714 let range = max - min;
715
716 move |delta| {
717 let t = (delta * 2.0 * PI).sin();
719 let breath = (t * t * t + t) / 2.0;
720
721 let normalized_alpha = (breath + 1.0) / 2.0;
723
724 min + (normalized_alpha * range)
725 }
726 }
727}
728
729#[cfg(test)]
730mod tests {
731 use crate::{InteractiveElement, Styled, div, px};
732
733 use super::*;
734
735 #[test]
736 fn test_animation_parent() {
737 div()
738 .id("id")
739 .with_animation(
740 "animation",
741 Animation::new(Duration::from_secs(1)),
742 |el, _t| el,
743 )
744 .child(div());
745 }
746
747 #[test]
748 fn test_spring_animation_parent() {
749 div()
750 .id("id")
751 .with_spring(
752 "spring-animation",
753 SpringAnimation::new(SpringConfig::new(100.0, 10.0, 1.0))
754 .to(px(10.0))
755 .from(px(0.0)),
756 |element, value| element.left(value),
757 )
758 .child(div());
759 }
760
761 #[test]
762 fn test_with_max_fps_stores_positive_and_invalid_rates() {
763 let capped = Animation::new(Duration::from_secs(1)).with_max_fps(12.0);
764 assert_eq!(capped.max_fps, Some(12.0));
765
766 let invalid = Animation::new(Duration::from_secs(1)).with_max_fps(0.0);
767 assert_eq!(invalid.max_fps, Some(0.0));
768 }
769
770 #[test]
771 fn test_with_delay_stores_delay() {
772 let delayed = Animation::new(Duration::from_secs(1)).with_delay(Duration::from_millis(250));
773 assert_eq!(delayed.delay, Duration::from_millis(250));
774
775 let immediate = Animation::new(Duration::from_secs(1));
776 assert_eq!(immediate.delay, Duration::ZERO);
777 }
778
779 #[test]
780 fn test_repeat_builders() {
781 assert_eq!(
782 Animation::new(Duration::from_secs(1)).repeat,
783 AnimationRepeat::Once
784 );
785 assert_eq!(
786 Animation::new(Duration::from_secs(1)).repeat().repeat,
787 AnimationRepeat::Forever
788 );
789 assert_eq!(
790 Animation::new(Duration::from_secs(1)).repeat_n(3).repeat,
791 AnimationRepeat::Times(3)
792 );
793 assert_eq!(
794 Animation::new(Duration::from_secs(1))
795 .with_repeat(AnimationRepeat::Times(2))
796 .repeat,
797 AnimationRepeat::Times(2)
798 );
799 }
800
801 #[test]
802 fn test_animation_pass_once() {
803 let animation = Animation::new(Duration::from_secs(2));
804 assert_eq!(
805 animation_pass(&animation, Duration::ZERO),
806 AnimationPass::Running { cycle_delta: 0.0 }
807 );
808 assert_eq!(
809 animation_pass(&animation, Duration::from_secs(1)),
810 AnimationPass::Running { cycle_delta: 0.5 }
811 );
812 assert_eq!(
813 animation_pass(&animation, Duration::from_secs(2)),
814 AnimationPass::Finished
815 );
816 assert_eq!(
817 animation_pass(&animation, Duration::from_secs(3)),
818 AnimationPass::Finished
819 );
820 }
821
822 #[test]
823 fn test_animation_pass_delay_holds_zero_then_runs() {
824 let animation =
825 Animation::new(Duration::from_secs(2)).with_delay(Duration::from_millis(500));
826 assert_eq!(
827 animation_pass(&animation, Duration::from_millis(499)),
828 AnimationPass::Delayed
829 );
830 assert_eq!(
831 animation_pass(&animation, Duration::from_millis(500)),
832 AnimationPass::Running { cycle_delta: 0.0 }
833 );
834 assert_eq!(
835 animation_pass(&animation, Duration::from_millis(1500)),
836 AnimationPass::Running { cycle_delta: 0.5 }
837 );
838 assert_eq!(
839 animation_pass(&animation, Duration::from_millis(2600)),
840 AnimationPass::Finished
841 );
842 }
843
844 #[test]
845 fn test_animation_pass_forever_wraps() {
846 let animation = Animation::new(Duration::from_secs(1)).repeat();
847 assert_eq!(
848 animation_pass(&animation, Duration::from_millis(1500)),
849 AnimationPass::Running { cycle_delta: 0.5 }
850 );
851 assert_eq!(
852 animation_pass(&animation, Duration::from_secs(3)),
853 AnimationPass::Running { cycle_delta: 0.0 }
854 );
855 }
856
857 #[test]
858 fn test_animation_pass_finite_times() {
859 let animation = Animation::new(Duration::from_secs(1)).repeat_n(3);
860 assert_eq!(
861 animation_pass(&animation, Duration::from_millis(1500)),
862 AnimationPass::Running { cycle_delta: 0.5 }
863 );
864 assert_eq!(
866 animation_pass(&animation, Duration::from_millis(2500)),
867 AnimationPass::Running { cycle_delta: 0.5 }
868 );
869 assert_eq!(
871 animation_pass(&animation, Duration::from_secs(3)),
872 AnimationPass::Finished
873 );
874 assert_eq!(
875 animation_pass(&animation, Duration::from_millis(3100)),
876 AnimationPass::Finished
877 );
878 }
879
880 #[test]
881 fn test_animation_pass_times_zero_clamps_to_once() {
882 let animation = Animation::new(Duration::from_secs(1)).repeat_n(0);
883 assert_eq!(
884 animation_pass(&animation, Duration::from_millis(500)),
885 AnimationPass::Running { cycle_delta: 0.5 }
886 );
887 assert_eq!(
888 animation_pass(&animation, Duration::from_millis(1500)),
889 AnimationPass::Finished
890 );
891 }
892
893 #[test]
894 fn test_animation_pass_zero_duration_finishes_immediately() {
895 let animation = Animation::new(Duration::ZERO);
896
897 assert_eq!(
898 animation_pass(&animation, Duration::ZERO),
899 AnimationPass::Finished
900 );
901 assert_eq!(
902 animation_pass(&animation.repeat(), Duration::from_secs(1)),
903 AnimationPass::Finished
904 );
905 }
906}