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