use std::time::{Duration, Instant}; use crate::{AnyElement, Element, ElementId, GlobalElementId, IntoElement}; pub use easing::*; /// An animation that can be applied to an element.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: Box<dyn Fn(f32) -> 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: Box::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 = Box::new(easing); self }} /// An extension trait for adding the animation wrapper to both Elements and Componentspub trait AnimationExt { /// Render this component or element with an animation 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(animator), animation, } }} impl<E> AnimationExt for E {} /// A GPUI element that applies an animation to another elementpub struct AnimationElement<E> { id: ElementId, element: Option<E>, animation: Animation, animator: Box<dyn Fn(E, f32) -> E + 'static>,} impl<E> AnimationElement<E> { /// Returns a new [`AnimationElement<E>`] after applying the given function /// to the element being animated. 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,} impl<E: IntoElement + 'static> Element for AnimationElement<E> { type RequestLayoutState = AnyElement; type PrepaintState = (); fn id(&self) -> Option<ElementId> { Some(self.id.clone()) } fn request_layout( &mut self, global_id: Option<&GlobalElementId>, cx: &mut crate::WindowContext, ) -> (crate::LayoutId, Self::RequestLayoutState) { cx.with_element_state(global_id.unwrap(), |state, cx| { let state = state.unwrap_or_else(|| AnimationState { start: Instant::now(), }); let mut delta = state.start.elapsed().as_secs_f32() / self.animation.duration.as_secs_f32(); let mut done = false; if delta > 1.0 { if self.animation.oneshot { done = true; delta = 1.0; } else { delta %= 1.0; } } let delta = (self.animation.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, delta).into_any_element(); if !done { cx.request_animation_frame(); } ((element.request_layout(cx), element), state) }) } fn prepaint( &mut self, _id: Option<&GlobalElementId>, _bounds: crate::Bounds<crate::Pixels>, element: &mut Self::RequestLayoutState, cx: &mut crate::WindowContext, ) -> Self::PrepaintState { element.prepaint(cx); } fn paint( &mut self, _id: Option<&GlobalElementId>, _bounds: crate::Bounds<crate::Pixels>, element: &mut Self::RequestLayoutState, _: &mut Self::PrepaintState, cx: &mut crate::WindowContext, ) { element.paint(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 } } /// 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) } }}
use std::time::{Duration, Instant};
use crate::{AnyElement, Element, ElementId, GlobalElementId, IntoElement};
pub use easing::*;
/// An animation that can be applied to an element.
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: Box<dyn Fn(f32) -> 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: Box::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 = Box::new(easing);
self
}
}
/// An extension trait for adding the animation wrapper to both Elements and Components
pub trait AnimationExt {
/// Render this component or element with an animation
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(animator),
animation,
}
}
}
impl<E> AnimationExt for E {}
/// A GPUI element that applies an animation to another element
pub struct AnimationElement<E> {
id: ElementId,
element: Option<E>,
animation: Animation,
animator: Box<dyn Fn(E, f32) -> E + 'static>,
}
impl<E> AnimationElement<E> {
/// Returns a new [`AnimationElement<E>`] after applying the given function
/// to the element being animated.
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,
}
impl<E: IntoElement + 'static> Element for AnimationElement<E> {
type RequestLayoutState = AnyElement;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
cx: &mut crate::WindowContext,
) -> (crate::LayoutId, Self::RequestLayoutState) {
cx.with_element_state(global_id.unwrap(), |state, cx| {
let state = state.unwrap_or_else(|| AnimationState {
start: Instant::now(),
});
let mut delta =
state.start.elapsed().as_secs_f32() / self.animation.duration.as_secs_f32();
let mut done = false;
if delta > 1.0 {
if self.animation.oneshot {
done = true;
delta = 1.0;
} else {
delta %= 1.0;
}
}
let delta = (self.animation.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, delta).into_any_element();
if !done {
cx.request_animation_frame();
}
((element.request_layout(cx), element), state)
})
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_bounds: crate::Bounds<crate::Pixels>,
element: &mut Self::RequestLayoutState,
cx: &mut crate::WindowContext,
) -> Self::PrepaintState {
element.prepaint(cx);
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_bounds: crate::Bounds<crate::Pixels>,
element: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
cx: &mut crate::WindowContext,
) {
element.paint(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
}
}
/// 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)
}
}
}