use crate::scheduler::Instant;
use std::{cell::Cell, rc::Rc, time::Duration};
use crate::{
AnyElement, App, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement,
ParentElement, Window,
};
pub use easing::*;
use smallvec::SmallVec;
#[derive(Clone)]
pub struct Animation {
pub duration: Duration,
pub oneshot: bool,
pub synced: 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,
synced: false,
easing: Rc::new(linear),
max_fps: None,
}
}
pub fn repeat(mut self) -> Self {
self.oneshot = false;
self
}
pub fn repeat_synced(mut self) -> Self {
self.oneshot = false;
self.synced = true;
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],
}
}
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(),
}
}
}
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>,
}
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>>,
}
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 (animation_ix, delta, done) = if cx.reduce_motion() {
let animation_ix = self.animations.len() - 1;
let delta = if self.animations[animation_ix].oneshot {
1.0
} else {
0.0
};
(animation_ix, delta, true)
} else {
let animation_ix = state.animation_ix;
let duration = self.animations[animation_ix].duration;
let elapsed = if self.animations[animation_ix].synced && !duration.is_zero() {
let elapsed = cx.background_executor().now() - cx.synced_animation_epoch;
Duration::from_nanos((elapsed.as_nanos() % duration.as_nanos()) as u64)
} else {
state.start.elapsed()
};
let mut delta = elapsed.as_secs_f32() / 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;
}
}
(animation_ix, delta, done)
};
let delta = (self.animations[animation_ix].easing)(delta);
debug_assert!(
(0.0..=1.0).contains(&delta),
"delta should always be between 0 and 1"
);
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 std::{cell::RefCell, rc::Rc, time::Duration};
use crate::{
Animation, Context, InteractiveElement, Render, TestAppContext, WindowHandle, div,
prelude::*, px, size,
};
use super::*;
struct AnimationTestView {
rendered_deltas: Rc<RefCell<Vec<f32>>>,
max_fps: Option<f32>,
}
struct SyncedAnimationTestView {
show_second: bool,
first_deltas: Rc<RefCell<Vec<f32>>>,
second_deltas: Rc<RefCell<Vec<f32>>>,
}
impl Render for SyncedAnimationTestView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let record_deltas = |deltas: Rc<RefCell<Vec<f32>>>| {
move |this, delta| {
deltas.borrow_mut().push(delta);
this
}
};
div()
.size_full()
.child(div().with_animation(
"first-synced-animation",
Animation::new(Duration::from_secs(1)).repeat_synced(),
record_deltas(self.first_deltas.clone()),
))
.when(self.show_second, |this| {
this.child(div().with_animation(
"second-synced-animation",
Animation::new(Duration::from_secs(1)).repeat_synced(),
record_deltas(self.second_deltas.clone()),
))
})
}
}
impl Render for AnimationTestView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let rendered_deltas = self.rendered_deltas.clone();
let mut animation = Animation::new(Duration::from_secs(1));
if let Some(max_fps) = self.max_fps {
animation = animation.repeat_synced().with_max_fps(max_fps);
} else {
animation = animation.repeat();
}
div().size_full().child(div().with_animation(
"repeating-animation",
animation,
move |this, delta| {
rendered_deltas.borrow_mut().push(delta);
this
},
))
}
}
fn open_test_window(
cx: &mut TestAppContext,
) -> (Rc<RefCell<Vec<f32>>>, WindowHandle<AnimationTestView>) {
open_test_window_with_max_fps(cx, None)
}
fn open_test_window_with_max_fps(
cx: &mut TestAppContext,
max_fps: Option<f32>,
) -> (Rc<RefCell<Vec<f32>>>, WindowHandle<AnimationTestView>) {
let rendered_deltas = Rc::new(RefCell::new(Vec::new()));
let window = cx.open_window(size(px(100.), px(100.)), {
let rendered_deltas = rendered_deltas.clone();
move |_, _| AnimationTestView {
rendered_deltas,
max_fps,
}
});
cx.run_until_parked();
(rendered_deltas, window)
}
fn simulate_next_frame<V: Render>(window: &WindowHandle<V>, cx: &mut TestAppContext) -> usize {
let callback_count = window
.update(cx, |_, window, cx| window.simulate_next_frame(cx))
.unwrap();
cx.run_until_parked();
callback_count
}
#[test]
fn test_animation_parent() {
div()
.id("id")
.with_animation(
"animation",
Animation::new(Duration::from_secs(1)),
|el, _t| {
el
},
)
.child(
div(),
);
}
#[rgpui::test]
fn test_repeating_animation_schedules_animation_frames(cx: &mut TestAppContext) {
let (rendered_deltas, window) = open_test_window(cx);
assert_eq!(rendered_deltas.borrow().len(), 1);
for expected_frames in 2..=3 {
assert_eq!(simulate_next_frame(&window, cx), 1);
assert_eq!(rendered_deltas.borrow().len(), expected_frames);
}
}
#[rgpui::test]
fn test_max_fps_schedules_timer_driven_frames(cx: &mut TestAppContext) {
let (rendered_deltas, window) = open_test_window_with_max_fps(cx, Some(10.0));
let assert_deltas_approx_eq = |expected: &[f32]| {
let actual = rendered_deltas.borrow();
assert_eq!(actual.len(), expected.len(), "deltas: {actual:?}");
for (actual, expected) in actual.iter().zip(expected) {
assert!(
(actual - expected).abs() < 1e-2,
"expected {expected}, got {actual}"
);
}
};
assert_deltas_approx_eq(&[0.0]);
assert_eq!(simulate_next_frame(&window, cx), 0);
assert_deltas_approx_eq(&[0.0]);
cx.executor().advance_clock(Duration::from_millis(105));
cx.run_until_parked();
assert_deltas_approx_eq(&[0.0, 0.105]);
cx.executor().advance_clock(Duration::from_millis(105));
cx.run_until_parked();
assert_deltas_approx_eq(&[0.0, 0.105, 0.21]);
}
#[rgpui::test]
fn test_synced_animations_share_phase_across_elements(cx: &mut TestAppContext) {
let first_deltas = Rc::new(RefCell::new(Vec::new()));
let second_deltas = Rc::new(RefCell::new(Vec::new()));
let window = cx.open_window(size(px(100.), px(100.)), {
let first_deltas = first_deltas.clone();
let second_deltas = second_deltas.clone();
move |_, _| SyncedAnimationTestView {
show_second: false,
first_deltas,
second_deltas,
}
});
cx.run_until_parked();
assert_eq!(*first_deltas.borrow(), vec![0.0]);
cx.executor().advance_clock(Duration::from_millis(250));
simulate_next_frame(&window, cx);
assert_eq!(*first_deltas.borrow(), vec![0.0, 0.25]);
window
.update(cx, |view, _, cx| {
view.show_second = true;
cx.notify();
})
.unwrap();
cx.run_until_parked();
cx.executor().advance_clock(Duration::from_millis(250));
simulate_next_frame(&window, cx);
assert_eq!(*second_deltas.borrow().last().unwrap(), 0.5);
assert_eq!(
*first_deltas.borrow().last().unwrap(),
*second_deltas.borrow().last().unwrap()
);
assert!(second_deltas.borrow().iter().all(|delta| *delta > 0.0));
cx.executor().advance_clock(Duration::from_millis(2250));
simulate_next_frame(&window, cx);
assert_eq!(*first_deltas.borrow().last().unwrap(), 0.75);
cx.executor()
.advance_clock(Duration::from_secs(300 * 24 * 60 * 60) + Duration::from_millis(500));
simulate_next_frame(&window, cx);
assert_eq!(*first_deltas.borrow().last().unwrap(), 0.25);
}
#[rgpui::test]
fn test_reduce_motion_renders_single_static_frame(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_reduce_motion(true));
let (rendered_deltas, window) = open_test_window(cx);
assert_eq!(*rendered_deltas.borrow(), vec![0.0]);
assert_eq!(simulate_next_frame(&window, cx), 0);
assert_eq!(*rendered_deltas.borrow(), vec![0.0]);
}
}