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