use std::{
cell::Cell,
rc::Rc,
time::{Duration, Instant},
};
use crate::{
AnyElement, App, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement,
ParentElement, SpringAnimation, SpringConfig, SpringPlayback, SpringState, SpringTarget,
Window,
};
pub use easing::*;
use smallvec::SmallVec;
#[derive(Clone)]
pub struct AnimationHandle {
cancelled: Rc<Cell<bool>>,
}
impl AnimationHandle {
fn new() -> Self {
Self {
cancelled: Rc::new(Cell::new(false)),
}
}
pub fn cancel(&self) {
self.cancelled.set(true);
}
pub fn is_cancelled(&self) -> bool {
self.cancelled.get()
}
}
#[derive(Clone)]
pub struct Animation {
pub duration: Duration,
pub oneshot: bool,
pub easing: Rc<dyn Fn(f32) -> f32>,
pub max_fps: Option<f32>,
}
impl Animation {
pub fn new(duration: Duration) -> Self {
Self {
duration,
oneshot: true,
easing: Rc::new(linear),
max_fps: None,
}
}
pub fn repeat(mut self) -> Self {
self.oneshot = false;
self
}
pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
self.easing = Rc::new(easing);
self
}
pub fn with_max_fps(mut self, max_fps: f32) -> Self {
self.max_fps = Some(max_fps);
self
}
}
pub trait AnimationExt {
fn with_animation(
self,
id: impl Into<ElementId>,
animation: Animation,
animator: impl Fn(Self, f32) -> Self + 'static,
) -> AnimationElement<Self>
where
Self: Sized,
{
AnimationElement {
id: id.into(),
element: Some(self),
animator: Box::new(move |this, _, value| animator(this, value)),
animations: smallvec::smallvec![animation],
cancel_handle: None,
}
}
fn with_animations(
self,
id: impl Into<ElementId>,
animations: Vec<Animation>,
animator: impl Fn(Self, usize, f32) -> Self + 'static,
) -> AnimationElement<Self>
where
Self: Sized,
{
AnimationElement {
id: id.into(),
element: Some(self),
animator: Box::new(animator),
animations: animations.into(),
cancel_handle: None,
}
}
fn with_cancellable_animation(
self,
id: impl Into<ElementId>,
animation: Animation,
animator: impl Fn(Self, f32) -> Self + 'static,
) -> (AnimationElement<Self>, AnimationHandle)
where
Self: Sized,
{
let handle = AnimationHandle::new();
let element = AnimationElement {
id: id.into(),
element: Some(self),
animator: Box::new(move |this, _, value| animator(this, value)),
animations: smallvec::smallvec![animation],
cancel_handle: Some(handle.cancelled.clone()),
};
(element, handle)
}
fn with_spring<T>(
self,
id: impl Into<ElementId>,
animation: SpringAnimation<T>,
animator: impl FnOnce(Self, T::Output) -> Self + 'static,
) -> SpringAnimationElement<Self>
where
Self: Sized,
T: SpringTarget,
T::Output: 'static,
{
let SpringAnimation {
config,
target,
epsilon,
initial,
playback,
} = animation;
let scalar_target = target.target();
SpringAnimationElement {
id: id.into(),
element: Some(self),
config,
target: scalar_target,
epsilon,
initial,
playback,
animator: Some(Box::new(move |this, value| {
animator(this, target.resolve(value))
})),
}
}
}
impl<E: IntoElement + 'static> AnimationExt for E {}
pub struct AnimationElement<E> {
id: ElementId,
element: Option<E>,
animations: SmallVec<[Animation; 1]>,
animator: Box<dyn Fn(E, usize, f32) -> E + 'static>,
cancel_handle: Option<Rc<Cell<bool>>>,
}
pub struct SpringAnimationElement<E> {
id: ElementId,
element: Option<E>,
config: SpringConfig,
target: f32,
epsilon: f32,
initial: Option<f32>,
playback: SpringPlayback,
animator: Option<Box<dyn FnOnce(E, f32) -> E + 'static>>,
}
impl<E: ParentElement> ParentElement for SpringAnimationElement<E> {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
let Some(element) = &mut self.element else {
return;
};
element.extend(elements);
}
}
impl<E> SpringAnimationElement<E> {
pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> SpringAnimationElement<E> {
self.element = self.element.map(f);
self
}
}
impl<E: IntoElement + 'static> IntoElement for SpringAnimationElement<E> {
type Element = SpringAnimationElement<E>;
fn into_element(self) -> Self::Element {
self
}
}
impl<E: ParentElement> ParentElement for AnimationElement<E> {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
let Some(element) = &mut self.element else {
return;
};
element.extend(elements);
}
}
impl<E> AnimationElement<E> {
pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> AnimationElement<E> {
self.element = self.element.map(f);
self
}
}
impl<E: IntoElement + 'static> IntoElement for AnimationElement<E> {
type Element = AnimationElement<E>;
fn into_element(self) -> Self::Element {
self
}
}
struct AnimationState {
start: Instant,
animation_ix: usize,
delayed_frame_pending: Rc<Cell<bool>>,
}
struct SpringElementState {
spring: SpringState,
target: f32,
config: SpringConfig,
initial: f32,
playback: SpringPlayback,
updated_at: Instant,
}
impl<E: IntoElement + 'static> Element for SpringAnimationElement<E> {
type RequestLayoutState = AnyElement;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (crate::LayoutId, Self::RequestLayoutState) {
window.with_element_state(global_id.unwrap(), |state, window| {
let now = Instant::now();
let initial = self.initial.unwrap_or(self.target);
let mut state = state.unwrap_or_else(|| SpringElementState {
spring: SpringState {
position: initial,
velocity: 0.0,
},
target: self.target,
config: self.config,
initial,
playback: self.playback,
updated_at: now,
});
let elapsed = now.duration_since(state.updated_at).as_secs_f32();
match state.playback {
SpringPlayback::Running => {
state.spring = state.config.step(state.spring, state.target, elapsed);
}
SpringPlayback::Paused
| SpringPlayback::Stopped
| SpringPlayback::Completed
| SpringPlayback::Cancelled => {}
}
state.config = self.config;
state.target = self.target;
let done = match self.playback {
SpringPlayback::Running => {
let done = state
.config
.is_settled(state.spring, state.target, self.epsilon);
if done {
state.spring = SpringState {
position: state.target,
velocity: 0.0,
};
}
done
}
SpringPlayback::Paused => true,
SpringPlayback::Stopped => {
state.spring.velocity = 0.0;
true
}
SpringPlayback::Completed => {
state.spring = SpringState {
position: state.target,
velocity: 0.0,
};
true
}
SpringPlayback::Cancelled => {
state.spring = SpringState {
position: state.initial,
velocity: 0.0,
};
true
}
};
state.playback = self.playback;
state.updated_at = now;
let element = self.element.take().expect("should only be called once");
let animator = self.animator.take().expect("should only be called once");
let mut element = animator(element, state.spring.position).into_any_element();
if !done {
window.request_animation_frame();
}
((element.request_layout(window, cx), element), state)
})
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: crate::Bounds<crate::Pixels>,
element: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
element.prepaint(window, cx);
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: crate::Bounds<crate::Pixels>,
element: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
element.paint(window, cx);
}
}
impl<E: IntoElement + 'static> Element for AnimationElement<E> {
type RequestLayoutState = AnyElement;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (crate::LayoutId, Self::RequestLayoutState) {
window.with_element_state(global_id.unwrap(), |state, window| {
let mut state = state.unwrap_or_else(|| AnimationState {
start: Instant::now(),
animation_ix: 0,
delayed_frame_pending: Rc::new(Cell::new(false)),
});
let cancelled = self.cancel_handle.as_ref().map_or(false, |h| h.get());
let animation_ix = state.animation_ix;
let (delta, done) = if cancelled {
(1.0_f32, true)
} else {
let mut delta = state.start.elapsed().as_secs_f32()
/ self.animations[animation_ix].duration.as_secs_f32();
let mut done = false;
if delta > 1.0 {
if self.animations[animation_ix].oneshot {
if animation_ix >= self.animations.len() - 1 {
done = true;
} else {
state.start = Instant::now();
state.animation_ix += 1;
}
delta = 1.0;
} else {
delta %= 1.0;
}
}
let delta = (self.animations[animation_ix].easing)(delta);
(delta, done)
};
debug_assert!(delta.is_finite(), "animated value should be finite");
let element = self.element.take().expect("should only be called once");
let mut element = (self.animator)(element, animation_ix, delta).into_any_element();
if !done {
match self.animations[animation_ix].max_fps {
Some(max_fps) if max_fps.is_finite() && max_fps > 0.0 => {
if !state.delayed_frame_pending.get() {
state.delayed_frame_pending.set(true);
let delayed_frame_pending = state.delayed_frame_pending.clone();
let view = window.current_view();
let interval = Duration::from_secs_f32(1.0 / max_fps);
window
.spawn(cx, async move |cx| {
cx.background_executor().timer(interval).await;
delayed_frame_pending.set(false);
cx.update(move |_, cx| cx.notify(view)).ok();
})
.detach();
}
}
_ => window.request_animation_frame(),
}
}
((element.request_layout(window, cx), element), state)
})
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: crate::Bounds<crate::Pixels>,
element: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
element.prepaint(window, cx);
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: crate::Bounds<crate::Pixels>,
element: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
element.paint(window, cx);
}
}
mod easing {
use std::f32::consts::PI;
pub fn linear(delta: f32) -> f32 {
delta
}
pub fn quadratic(delta: f32) -> f32 {
delta * delta
}
pub fn ease_in_out(delta: f32) -> f32 {
if delta < 0.5 {
2.0 * delta * delta
} else {
let x = -2.0 * delta + 2.0;
1.0 - x * x / 2.0
}
}
pub fn ease_out_quint() -> impl Fn(f32) -> f32 {
move |delta| 1.0 - (1.0 - delta).powi(5)
}
pub fn bounce(easing: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 {
move |delta| {
if delta < 0.5 {
easing(delta * 2.0)
} else {
easing((1.0 - delta) * 2.0)
}
}
}
pub fn pulsating_between(min: f32, max: f32) -> impl Fn(f32) -> f32 {
let range = max - min;
move |delta| {
let t = (delta * 2.0 * PI).sin();
let breath = (t * t * t + t) / 2.0;
let normalized_alpha = (breath + 1.0) / 2.0;
min + (normalized_alpha * range)
}
}
}
#[cfg(test)]
mod tests {
use crate::{InteractiveElement, Styled, div, px};
use super::*;
#[test]
fn test_animation_parent() {
div()
.id("id")
.with_animation(
"animation",
Animation::new(Duration::from_secs(1)),
|el, _t| el,
)
.child(div());
}
#[test]
fn test_spring_animation_parent() {
div()
.id("id")
.with_spring(
"spring-animation",
SpringAnimation::new(SpringConfig::new(100.0, 10.0, 1.0))
.to(px(10.0))
.from(px(0.0)),
|element, value| element.left(value),
)
.child(div());
}
#[test]
fn test_with_max_fps_stores_positive_and_invalid_rates() {
let capped = Animation::new(Duration::from_secs(1)).with_max_fps(12.0);
assert_eq!(capped.max_fps, Some(12.0));
let invalid = Animation::new(Duration::from_secs(1)).with_max_fps(0.0);
assert_eq!(invalid.max_fps, Some(0.0));
}
}