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)]
42pub struct Animation {
43 pub duration: Duration,
45 pub oneshot: bool,
47 pub easing: Rc<dyn Fn(f32) -> f32>,
50 pub max_fps: Option<f32>,
53}
54
55impl Animation {
56 pub fn new(duration: Duration) -> Self {
59 Self {
60 duration,
61 oneshot: true,
62 easing: Rc::new(linear),
63 max_fps: None,
64 }
65 }
66
67 pub fn repeat(mut self) -> Self {
69 self.oneshot = false;
70 self
71 }
72
73 pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
78 self.easing = Rc::new(easing);
79 self
80 }
81
82 pub fn with_max_fps(mut self, max_fps: f32) -> Self {
87 self.max_fps = Some(max_fps);
88 self
89 }
90}
91
92pub trait AnimationExt {
94 fn with_animation(
96 self,
97 id: impl Into<ElementId>,
98 animation: Animation,
99 animator: impl Fn(Self, f32) -> Self + 'static,
100 ) -> AnimationElement<Self>
101 where
102 Self: Sized,
103 {
104 AnimationElement {
105 id: id.into(),
106 element: Some(self),
107 animator: Box::new(move |this, _, value| animator(this, value)),
108 animations: smallvec::smallvec![animation],
109 cancel_handle: None,
110 }
111 }
112
113 fn with_animations(
115 self,
116 id: impl Into<ElementId>,
117 animations: Vec<Animation>,
118 animator: impl Fn(Self, usize, f32) -> Self + 'static,
119 ) -> AnimationElement<Self>
120 where
121 Self: Sized,
122 {
123 AnimationElement {
124 id: id.into(),
125 element: Some(self),
126 animator: Box::new(animator),
127 animations: animations.into(),
128 cancel_handle: None,
129 }
130 }
131
132 fn with_cancellable_animation(
135 self,
136 id: impl Into<ElementId>,
137 animation: Animation,
138 animator: impl Fn(Self, f32) -> Self + 'static,
139 ) -> (AnimationElement<Self>, AnimationHandle)
140 where
141 Self: Sized,
142 {
143 let handle = AnimationHandle::new();
144 let element = AnimationElement {
145 id: id.into(),
146 element: Some(self),
147 animator: Box::new(move |this, _, value| animator(this, value)),
148 animations: smallvec::smallvec![animation],
149 cancel_handle: Some(handle.cancelled.clone()),
150 };
151 (element, handle)
152 }
153
154 fn with_spring<T>(
160 self,
161 id: impl Into<ElementId>,
162 animation: SpringAnimation<T>,
163 animator: impl FnOnce(Self, T::Output) -> Self + 'static,
164 ) -> SpringAnimationElement<Self>
165 where
166 Self: Sized,
167 T: SpringTarget,
168 T::Output: 'static,
169 {
170 let SpringAnimation {
171 config,
172 target,
173 epsilon,
174 initial,
175 playback,
176 } = animation;
177 let scalar_target = target.target();
178 SpringAnimationElement {
179 id: id.into(),
180 element: Some(self),
181 config,
182 target: scalar_target,
183 epsilon,
184 initial,
185 playback,
186 animator: Some(Box::new(move |this, value| {
187 animator(this, target.resolve(value))
188 })),
189 }
190 }
191}
192
193impl<E: IntoElement + 'static> AnimationExt for E {}
194
195pub struct AnimationElement<E> {
197 id: ElementId,
198 element: Option<E>,
199 animations: SmallVec<[Animation; 1]>,
200 animator: Box<dyn Fn(E, usize, f32) -> E + 'static>,
201 cancel_handle: Option<Rc<Cell<bool>>>,
202}
203
204pub struct SpringAnimationElement<E> {
206 id: ElementId,
207 element: Option<E>,
208 config: SpringConfig,
209 target: f32,
210 epsilon: f32,
211 initial: Option<f32>,
212 playback: SpringPlayback,
213 animator: Option<Box<dyn FnOnce(E, f32) -> E + 'static>>,
214}
215
216impl<E: ParentElement> ParentElement for SpringAnimationElement<E> {
217 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
218 let Some(element) = &mut self.element else {
219 return;
220 };
221
222 element.extend(elements);
223 }
224}
225
226impl<E> SpringAnimationElement<E> {
227 pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> SpringAnimationElement<E> {
230 self.element = self.element.map(f);
231 self
232 }
233}
234
235impl<E: IntoElement + 'static> IntoElement for SpringAnimationElement<E> {
236 type Element = SpringAnimationElement<E>;
237
238 fn into_element(self) -> Self::Element {
239 self
240 }
241}
242
243impl<E: ParentElement> ParentElement for AnimationElement<E> {
244 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
245 let Some(element) = &mut self.element else {
246 return;
247 };
248
249 element.extend(elements);
250 }
251}
252
253impl<E> AnimationElement<E> {
254 pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> AnimationElement<E> {
257 self.element = self.element.map(f);
258 self
259 }
260}
261
262impl<E: IntoElement + 'static> IntoElement for AnimationElement<E> {
263 type Element = AnimationElement<E>;
264
265 fn into_element(self) -> Self::Element {
266 self
267 }
268}
269
270struct AnimationState {
271 start: Instant,
272 animation_ix: usize,
273 delayed_frame_pending: Rc<Cell<bool>>,
276}
277
278struct SpringElementState {
279 spring: SpringState,
280 target: f32,
281 config: SpringConfig,
282 initial: f32,
283 playback: SpringPlayback,
284 updated_at: Instant,
285}
286
287impl<E: IntoElement + 'static> Element for SpringAnimationElement<E> {
288 type RequestLayoutState = AnyElement;
289 type PrepaintState = ();
290
291 fn id(&self) -> Option<ElementId> {
292 Some(self.id.clone())
293 }
294
295 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
296 None
297 }
298
299 fn request_layout(
300 &mut self,
301 global_id: Option<&GlobalElementId>,
302 _inspector_id: Option<&InspectorElementId>,
303 window: &mut Window,
304 cx: &mut App,
305 ) -> (crate::LayoutId, Self::RequestLayoutState) {
306 window.with_element_state(global_id.unwrap(), |state, window| {
307 let now = Instant::now();
308 let initial = self.initial.unwrap_or(self.target);
309 let mut state = state.unwrap_or_else(|| SpringElementState {
310 spring: SpringState {
311 position: initial,
312 velocity: 0.0,
313 },
314 target: self.target,
315 config: self.config,
316 initial,
317 playback: self.playback,
318 updated_at: now,
319 });
320
321 let elapsed = now.duration_since(state.updated_at).as_secs_f32();
322 match state.playback {
323 SpringPlayback::Running => {
324 state.spring = state.config.step(state.spring, state.target, elapsed);
325 }
326 SpringPlayback::Paused
327 | SpringPlayback::Stopped
328 | SpringPlayback::Completed
329 | SpringPlayback::Cancelled => {}
330 }
331
332 state.config = self.config;
333 state.target = self.target;
334
335 let done = match self.playback {
336 SpringPlayback::Running => {
337 let done = state
338 .config
339 .is_settled(state.spring, state.target, self.epsilon);
340 if done {
341 state.spring = SpringState {
342 position: state.target,
343 velocity: 0.0,
344 };
345 }
346 done
347 }
348 SpringPlayback::Paused => true,
349 SpringPlayback::Stopped => {
350 state.spring.velocity = 0.0;
351 true
352 }
353 SpringPlayback::Completed => {
354 state.spring = SpringState {
355 position: state.target,
356 velocity: 0.0,
357 };
358 true
359 }
360 SpringPlayback::Cancelled => {
361 state.spring = SpringState {
362 position: state.initial,
363 velocity: 0.0,
364 };
365 true
366 }
367 };
368 state.playback = self.playback;
369 state.updated_at = now;
370
371 let element = self.element.take().expect("should only be called once");
372 let animator = self.animator.take().expect("should only be called once");
373 let mut element = animator(element, state.spring.position).into_any_element();
374
375 if !done {
376 window.request_animation_frame();
377 }
378
379 ((element.request_layout(window, cx), element), state)
380 })
381 }
382
383 fn prepaint(
384 &mut self,
385 _id: Option<&GlobalElementId>,
386 _inspector_id: Option<&InspectorElementId>,
387 _bounds: crate::Bounds<crate::Pixels>,
388 element: &mut Self::RequestLayoutState,
389 window: &mut Window,
390 cx: &mut App,
391 ) -> Self::PrepaintState {
392 element.prepaint(window, cx);
393 }
394
395 fn paint(
396 &mut self,
397 _id: Option<&GlobalElementId>,
398 _inspector_id: Option<&InspectorElementId>,
399 _bounds: crate::Bounds<crate::Pixels>,
400 element: &mut Self::RequestLayoutState,
401 _: &mut Self::PrepaintState,
402 window: &mut Window,
403 cx: &mut App,
404 ) {
405 element.paint(window, cx);
406 }
407}
408
409impl<E: IntoElement + 'static> Element for AnimationElement<E> {
410 type RequestLayoutState = AnyElement;
411 type PrepaintState = ();
412
413 fn id(&self) -> Option<ElementId> {
414 Some(self.id.clone())
415 }
416
417 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
418 None
419 }
420
421 fn request_layout(
422 &mut self,
423 global_id: Option<&GlobalElementId>,
424 _inspector_id: Option<&InspectorElementId>,
425 window: &mut Window,
426 cx: &mut App,
427 ) -> (crate::LayoutId, Self::RequestLayoutState) {
428 window.with_element_state(global_id.unwrap(), |state, window| {
429 let mut state = state.unwrap_or_else(|| AnimationState {
430 start: Instant::now(),
431 animation_ix: 0,
432 delayed_frame_pending: Rc::new(Cell::new(false)),
433 });
434
435 let cancelled = self.cancel_handle.as_ref().map_or(false, |h| h.get());
436
437 let animation_ix = state.animation_ix;
438
439 let (delta, done) = if cancelled {
440 (1.0_f32, true)
441 } else {
442 let mut delta = state.start.elapsed().as_secs_f32()
443 / self.animations[animation_ix].duration.as_secs_f32();
444
445 let mut done = false;
446 if delta > 1.0 {
447 if self.animations[animation_ix].oneshot {
448 if animation_ix >= self.animations.len() - 1 {
449 done = true;
450 } else {
451 state.start = Instant::now();
452 state.animation_ix += 1;
453 }
454 delta = 1.0;
455 } else {
456 delta %= 1.0;
457 }
458 }
459 let delta = (self.animations[animation_ix].easing)(delta);
460 (delta, done)
461 };
462
463 debug_assert!(delta.is_finite(), "animated value should be finite");
464
465 let element = self.element.take().expect("should only be called once");
466 let mut element = (self.animator)(element, animation_ix, delta).into_any_element();
467
468 if !done {
469 match self.animations[animation_ix].max_fps {
470 Some(max_fps) if max_fps.is_finite() && max_fps > 0.0 => {
471 if !state.delayed_frame_pending.get() {
472 state.delayed_frame_pending.set(true);
473 let delayed_frame_pending = state.delayed_frame_pending.clone();
474 let view = window.current_view();
475 let interval = Duration::from_secs_f32(1.0 / max_fps);
476 window
477 .spawn(cx, async move |cx| {
478 cx.background_executor().timer(interval).await;
479 delayed_frame_pending.set(false);
480 cx.update(move |_, cx| cx.notify(view)).ok();
481 })
482 .detach();
483 }
484 }
485 _ => window.request_animation_frame(),
486 }
487 }
488
489 ((element.request_layout(window, cx), element), state)
490 })
491 }
492
493 fn prepaint(
494 &mut self,
495 _id: Option<&GlobalElementId>,
496 _inspector_id: Option<&InspectorElementId>,
497 _bounds: crate::Bounds<crate::Pixels>,
498 element: &mut Self::RequestLayoutState,
499 window: &mut Window,
500 cx: &mut App,
501 ) -> Self::PrepaintState {
502 element.prepaint(window, cx);
503 }
504
505 fn paint(
506 &mut self,
507 _id: Option<&GlobalElementId>,
508 _inspector_id: Option<&InspectorElementId>,
509 _bounds: crate::Bounds<crate::Pixels>,
510 element: &mut Self::RequestLayoutState,
511 _: &mut Self::PrepaintState,
512 window: &mut Window,
513 cx: &mut App,
514 ) {
515 element.paint(window, cx);
516 }
517}
518
519mod easing {
520 use std::f32::consts::PI;
521
522 pub fn linear(delta: f32) -> f32 {
524 delta
525 }
526
527 pub fn quadratic(delta: f32) -> f32 {
529 delta * delta
530 }
531
532 pub fn ease_in_out(delta: f32) -> f32 {
534 if delta < 0.5 {
535 2.0 * delta * delta
536 } else {
537 let x = -2.0 * delta + 2.0;
538 1.0 - x * x / 2.0
539 }
540 }
541
542 pub fn ease_out_quint() -> impl Fn(f32) -> f32 {
544 move |delta| 1.0 - (1.0 - delta).powi(5)
545 }
546
547 pub fn bounce(easing: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 {
549 move |delta| {
550 if delta < 0.5 {
551 easing(delta * 2.0)
552 } else {
553 easing((1.0 - delta) * 2.0)
554 }
555 }
556 }
557
558 pub fn pulsating_between(min: f32, max: f32) -> impl Fn(f32) -> f32 {
560 let range = max - min;
561
562 move |delta| {
563 let t = (delta * 2.0 * PI).sin();
565 let breath = (t * t * t + t) / 2.0;
566
567 let normalized_alpha = (breath + 1.0) / 2.0;
569
570 min + (normalized_alpha * range)
571 }
572 }
573}
574
575#[cfg(test)]
576mod tests {
577 use crate::{InteractiveElement, Styled, div, px};
578
579 use super::*;
580
581 #[test]
582 fn test_animation_parent() {
583 div()
584 .id("id")
585 .with_animation(
586 "animation",
587 Animation::new(Duration::from_secs(1)),
588 |el, _t| el,
589 )
590 .child(div());
591 }
592
593 #[test]
594 fn test_spring_animation_parent() {
595 div()
596 .id("id")
597 .with_spring(
598 "spring-animation",
599 SpringAnimation::new(SpringConfig::new(100.0, 10.0, 1.0))
600 .to(px(10.0))
601 .from(px(0.0)),
602 |element, value| element.left(value),
603 )
604 .child(div());
605 }
606
607 #[test]
608 fn test_with_max_fps_stores_positive_and_invalid_rates() {
609 let capped = Animation::new(Duration::from_secs(1)).with_max_fps(12.0);
610 assert_eq!(capped.max_fps, Some(12.0));
611
612 let invalid = Animation::new(Duration::from_secs(1)).with_max_fps(0.0);
613 assert_eq!(invalid.max_fps, Some(0.0));
614 }
615}