use scheduler::Instant;
use std::{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 easing: Rc<dyn Fn(f32) -> f32>,
}
impl Animation {
pub fn new(duration: Duration) -> Self {
Self {
duration,
oneshot: true,
easing: Rc::new(linear),
}
}
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 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,
}
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,
});
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 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;
}
}
(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 {
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>>>,
}
impl Render for AnimationTestView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let rendered_deltas = self.rendered_deltas.clone();
div().size_full().child(div().with_animation(
"repeating-animation",
Animation::new(Duration::from_secs(1)).repeat(),
move |this, delta| {
rendered_deltas.borrow_mut().push(delta);
this
},
))
}
}
fn open_test_window(
cx: &mut TestAppContext,
) -> (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 }
});
cx.run_until_parked();
(rendered_deltas, window)
}
fn simulate_next_frame(
window: &WindowHandle<AnimationTestView>,
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(),
);
}
#[gpui::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);
}
}
#[gpui::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]);
}
}