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