1use std::time::{Duration, Instant};
2
3use crate::{AnyElement, App, Element, ElementId, GlobalElementId, IntoElement, Window};
4
5pub use easing::*;
6use smallvec::SmallVec;
7
8/// An animation that can be applied to an element.
9pub struct Animation {
10 /// The amount of time for which this animation should run
11 pub duration: Duration,
12 /// Whether to repeat this animation when it finishes
13 pub oneshot: bool,
14 /// A function that takes a delta between 0 and 1 and returns a new delta
15 /// between 0 and 1 based on the given easing function.
16 pub easing: Box<dyn Fn(f32) -> f32>,
17}
18
19impl Animation {
20 /// Create a new animation with the given duration.
21 /// By default the animation will only run once and will use a linear easing function.
22 pub fn new(duration: Duration) -> Self {
23 Self {
24 duration,
25 oneshot: true,
26 easing: Box::new(linear),
27 }
28 }
29
30 /// Set the animation to loop when it finishes.
31 pub fn repeat(mut self) -> Self {
32 self.oneshot = false;
33 self
34 }
35
36 /// Set the easing function to use for this animation.
37 /// The easing function will take a time delta between 0 and 1 and return a new delta
38 /// between 0 and 1
39 pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
40 self.easing = Box::new(easing);
41 self
42 }
43}
44
45/// An extension trait for adding the animation wrapper to both Elements and Components
46pub trait AnimationExt {
47 /// Render this component or element with an animation
48 fn with_animation(
49 self,
50 id: impl Into<ElementId>,
51 animation: Animation,
52 animator: impl Fn(Self, f32) -> Self + 'static,
53 ) -> AnimationElement<Self>
54 where
55 Self: Sized,
56 {
57 AnimationElement {
58 id: id.into(),
59 element: Some(self),
60 animator: Box::new(move |this, _, value| animator(this, value)),
61 animations: smallvec::smallvec![animation],
62 }
63 }
64
65 /// Render this component or element with a chain of animations
66 fn with_animations(
67 self,
68 id: impl Into<ElementId>,
69 animations: Vec<Animation>,
70 animator: impl Fn(Self, usize, f32) -> Self + 'static,
71 ) -> AnimationElement<Self>
72 where
73 Self: Sized,
74 {
75 AnimationElement {
76 id: id.into(),
77 element: Some(self),
78 animator: Box::new(animator),
79 animations: animations.into(),
80 }
81 }
82}
83
84impl<E> AnimationExt for E {}
85
86/// A GPUI element that applies an animation to another element
87pub struct AnimationElement<E> {
88 id: ElementId,
89 element: Option<E>,
90 animations: SmallVec<[Animation; 1]>,
91 animator: Box<dyn Fn(E, usize, f32) -> E + 'static>,
92}
93
94impl<E> AnimationElement<E> {
95 /// Returns a new [`AnimationElement<E>`] after applying the given function
96 /// to the element being animated.
97 pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> AnimationElement<E> {
98 self.element = self.element.map(f);
99 self
100 }
101}
102
103impl<E: IntoElement + 'static> IntoElement for AnimationElement<E> {
104 type Element = AnimationElement<E>;
105
106 fn into_element(self) -> Self::Element {
107 self
108 }
109}
110
111struct AnimationState {
112 start: Instant,
113 animation_ix: usize,
114}
115
116impl<E: IntoElement + 'static> Element for AnimationElement<E> {
117 type RequestLayoutState = AnyElement;
118 type PrepaintState = ();
119
120 fn id(&self) -> Option<ElementId> {
121 Some(self.id.clone())
122 }
123
124 fn request_layout(
125 &mut self,
126 global_id: Option<&GlobalElementId>,
127 window: &mut Window,
128 cx: &mut App,
129 ) -> (crate::LayoutId, Self::RequestLayoutState) {
130 window.with_element_state(global_id.unwrap(), |state, window| {
131 let mut state = state.unwrap_or_else(|| AnimationState {
132 start: Instant::now(),
133 animation_ix: 0,
134 });
135 let animation_ix = state.animation_ix;
136
137 let mut delta = state.start.elapsed().as_secs_f32()
138 / self.animations[animation_ix].duration.as_secs_f32();
139
140 let mut done = false;
141 if delta > 1.0 {
142 if self.animations[animation_ix].oneshot {
143 if animation_ix >= self.animations.len() - 1 {
144 done = true;
145 } else {
146 state.start = Instant::now();
147 state.animation_ix += 1;
148 }
149 delta = 1.0;
150 } else {
151 delta %= 1.0;
152 }
153 }
154 let delta = (self.animations[animation_ix].easing)(delta);
155
156 debug_assert!(
157 (0.0..=1.0).contains(&delta),
158 "delta should always be between 0 and 1"
159 );
160
161 let element = self.element.take().expect("should only be called once");
162 let mut element = (self.animator)(element, animation_ix, delta).into_any_element();
163
164 if !done {
165 window.request_animation_frame();
166 }
167
168 ((element.request_layout(window, cx), element), state)
169 })
170 }
171
172 fn prepaint(
173 &mut self,
174 _id: Option<&GlobalElementId>,
175 _bounds: crate::Bounds<crate::Pixels>,
176 element: &mut Self::RequestLayoutState,
177 window: &mut Window,
178 cx: &mut App,
179 ) -> Self::PrepaintState {
180 element.prepaint(window, cx);
181 }
182
183 fn paint(
184 &mut self,
185 _id: Option<&GlobalElementId>,
186 _bounds: crate::Bounds<crate::Pixels>,
187 element: &mut Self::RequestLayoutState,
188 _: &mut Self::PrepaintState,
189 window: &mut Window,
190 cx: &mut App,
191 ) {
192 element.paint(window, cx);
193 }
194}
195
196mod easing {
197 use std::f32::consts::PI;
198
199 /// The linear easing function, or delta itself
200 pub fn linear(delta: f32) -> f32 {
201 delta
202 }
203
204 /// The quadratic easing function, delta * delta
205 pub fn quadratic(delta: f32) -> f32 {
206 delta * delta
207 }
208
209 /// The quadratic ease-in-out function, which starts and ends slowly but speeds up in the middle
210 pub fn ease_in_out(delta: f32) -> f32 {
211 if delta < 0.5 {
212 2.0 * delta * delta
213 } else {
214 let x = -2.0 * delta + 2.0;
215 1.0 - x * x / 2.0
216 }
217 }
218
219 /// The Quint ease-out function, which starts quickly and decelerates to a stop
220 pub fn ease_out_quint() -> impl Fn(f32) -> f32 {
221 move |delta| 1.0 - (1.0 - delta).powi(5)
222 }
223
224 /// Apply the given easing function, first in the forward direction and then in the reverse direction
225 pub fn bounce(easing: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 {
226 move |delta| {
227 if delta < 0.5 {
228 easing(delta * 2.0)
229 } else {
230 easing((1.0 - delta) * 2.0)
231 }
232 }
233 }
234
235 /// A custom easing function for pulsating alpha that slows down as it approaches 0.1
236 pub fn pulsating_between(min: f32, max: f32) -> impl Fn(f32) -> f32 {
237 let range = max - min;
238
239 move |delta| {
240 // Use a combination of sine and cubic functions for a more natural breathing rhythm
241 let t = (delta * 2.0 * PI).sin();
242 let breath = (t * t * t + t) / 2.0;
243
244 // Map the breath to our desired alpha range
245 let normalized_alpha = (breath + 1.0) / 2.0;
246
247 min + (normalized_alpha * range)
248 }
249 }
250}