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; /// An animation that can be applied to an element. #[derive(Clone)] pub struct Animation { /// The amount of time for which this animation should run pub duration: Duration, /// Whether to repeat this animation when it finishes pub oneshot: bool, /// A function that takes a delta between 0 and 1 and returns a new delta /// between 0 and 1 based on the given easing function. pub easing: Rc f32>, } impl Animation { /// Create a new animation with the given duration. /// By default the animation will only run once and will use a linear easing function. pub fn new(duration: Duration) -> Self { Self { duration, oneshot: true, easing: Rc::new(linear), } } /// Set the animation to loop when it finishes. pub fn repeat(mut self) -> Self { self.oneshot = false; self } /// Set the easing function to use for this animation. /// The easing function will take a time delta between 0 and 1 and return a new delta /// between 0 and 1 pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self { self.easing = Rc::new(easing); self } } /// An extension trait for adding the animation wrapper to both Elements and Components /// /// Animations rendered through this trait automatically respect /// [`App::reduce_motion`](crate::App::reduce_motion): when it is set, /// the element is rendered in a static state (the end state for oneshot /// animations, the start state for repeating ones) and no animation frames are /// scheduled. pub trait AnimationExt { /// Render this component or element with an animation fn with_animation( self, id: impl Into, animation: Animation, animator: impl Fn(Self, f32) -> Self + 'static, ) -> AnimationElement where Self: Sized, { AnimationElement { id: id.into(), element: Some(self), animator: Box::new(move |this, _, value| animator(this, value)), animations: smallvec::smallvec![animation], } } /// Render this component or element with a chain of animations fn with_animations( self, id: impl Into, animations: Vec, animator: impl Fn(Self, usize, f32) -> Self + 'static, ) -> AnimationElement where Self: Sized, { AnimationElement { id: id.into(), element: Some(self), animator: Box::new(animator), animations: animations.into(), } } } impl AnimationExt for E {} /// A GPUI element that applies an animation to another element pub struct AnimationElement { id: ElementId, element: Option, animations: SmallVec<[Animation; 1]>, animator: Box E + 'static>, } impl ParentElement for AnimationElement { fn extend(&mut self, elements: impl IntoIterator) { let Some(element) = &mut self.element else { return; }; element.extend(elements); } } impl AnimationElement { /// Returns a new [`AnimationElement`] after applying the given function /// to the element being animated. pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> AnimationElement { self.element = self.element.map(f); self } } impl IntoElement for AnimationElement { type Element = AnimationElement; fn into_element(self) -> Self::Element { self } } struct AnimationState { start: Instant, animation_ix: usize, } impl Element for AnimationElement { type RequestLayoutState = AnyElement; type PrepaintState = (); fn id(&self) -> Option { 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, 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, element: &mut Self::RequestLayoutState, _: &mut Self::PrepaintState, window: &mut Window, cx: &mut App, ) { element.paint(window, cx); } } mod easing { use std::f32::consts::PI; /// The linear easing function, or delta itself pub fn linear(delta: f32) -> f32 { delta } /// The quadratic easing function, delta * delta pub fn quadratic(delta: f32) -> f32 { delta * delta } /// The quadratic ease-in-out function, which starts and ends slowly but speeds up in the middle 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 } } /// The Quint ease-out function, which starts quickly and decelerates to a stop pub fn ease_out_quint() -> impl Fn(f32) -> f32 { move |delta| 1.0 - (1.0 - delta).powi(5) } /// Apply the given easing function, first in the forward direction and then in the reverse direction 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) } } } /// A custom easing function for pulsating alpha that slows down as it approaches 0.1 pub fn pulsating_between(min: f32, max: f32) -> impl Fn(f32) -> f32 { let range = max - min; move |delta| { // Use a combination of sine and cubic functions for a more natural breathing rhythm let t = (delta * 2.0 * PI).sin(); let breath = (t * t * t + t) / 2.0; // Map the breath to our desired alpha range 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>>, } impl Render for AnimationTestView { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> 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>>, WindowHandle) { 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, cx: &mut TestAppContext, ) -> usize { let callback_count = window .update(cx, |_, window, cx| window.simulate_next_frame(cx)) .unwrap(); cx.run_until_parked(); callback_count } // Before parent-animation-element, using .with_animation // would not allow chaining .parent after. This is just a // build check that we can call div().id().with_animation().child() #[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]); } }