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