1use std::{rc::Rc, time::Duration};
2
3use gpui::{
4 Animation, AnimationExt, ElementId, Hsla, IntoElement, Pixels, Point, Styled, point,
5 prelude::FluentBuilder, px,
6};
7use smallvec::SmallVec;
8
9pub fn cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32) -> impl Fn(f32) -> f32 {
15 let (cx, cy) = (3.0 * x1, 3.0 * y1);
17 let (bx, by) = (3.0 * (x2 - x1) - cx, 3.0 * (y2 - y1) - cy);
18 let (ax, ay) = (1.0 - cx - bx, 1.0 - cy - by);
19 let sample_x = move |t: f32| ((ax * t + bx) * t + cx) * t;
20 let sample_y = move |t: f32| ((ay * t + by) * t + cy) * t;
21 let slope_x = move |t: f32| (3.0 * ax * t + 2.0 * bx) * t + cx;
22
23 let solve_s = move |t: f32| {
25 let mut s = t;
26 for _ in 0..8 {
27 let error = sample_x(s) - t;
28 if error.abs() < 1e-6 {
29 return s;
30 }
31 let slope = slope_x(s);
32 if slope.abs() < 1e-6 {
33 break;
34 }
35 s = (s - error / slope).clamp(0.0, 1.0);
36 }
37
38 let (mut low, mut high) = (0.0, 1.0);
39 let mut s = t;
40 for _ in 0..32 {
41 let x = sample_x(s);
42 if (x - t).abs() < 1e-6 {
43 break;
44 }
45 if x < t {
46 low = s;
47 } else {
48 high = s;
49 }
50 s = (low + high) / 2.0;
51 }
52 s
53 };
54
55 move |t: f32| {
56 let t = t.clamp(0.0, 1.0);
57 sample_y(solve_s(t)).clamp(0.0, 1.0)
62 }
63}
64
65pub fn ease_out_cubic(t: f32) -> f32 {
69 let t = t.clamp(0.0, 1.0);
70 1.0 - (1.0 - t).powi(3)
71}
72
73pub fn ease_in_cubic(t: f32) -> f32 {
75 let t = t.clamp(0.0, 1.0);
76 t * t * t
77}
78
79pub fn ease_in_out_cubic(t: f32) -> f32 {
81 let t = t.clamp(0.0, 1.0);
82 if t < 0.5 {
83 4.0 * t * t * t
84 } else {
85 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
86 }
87}
88
89pub trait Lerp: Clone {
93 fn lerp(&self, target: &Self, t: f32) -> Self;
94}
95
96impl Lerp for f32 {
97 fn lerp(&self, target: &Self, t: f32) -> Self {
98 self + (target - self) * t
99 }
100}
101
102impl Lerp for Pixels {
103 fn lerp(&self, target: &Self, t: f32) -> Self {
104 let a: f32 = (*self).into();
105 let b: f32 = (*target).into();
106 px(a + (b - a) * t)
107 }
108}
109
110impl Lerp for Point<Pixels> {
111 fn lerp(&self, target: &Self, t: f32) -> Self {
112 point(
113 Lerp::lerp(&self.x, &target.x, t),
114 Lerp::lerp(&self.y, &target.y, t),
115 )
116 }
117}
118
119impl Lerp for Hsla {
120 fn lerp(&self, target: &Self, t: f32) -> Self {
124 Hsla {
125 h: self.h.lerp(&target.h, t),
126 s: self.s.lerp(&target.s, t),
127 l: self.l.lerp(&target.l, t),
128 a: self.a.lerp(&target.a, t),
129 }
130 }
131}
132
133#[derive(Clone)]
152pub struct EffectTransition {
153 pub duration: Duration,
154 easing: Rc<dyn Fn(f32) -> f32>,
155 effects: SmallVec<[TransitionEffect; 2]>,
156}
157
158#[derive(Clone, Copy)]
159enum TransitionEffect {
160 SlideY(Pixels, Pixels),
161 SlideX(Pixels, Pixels),
162 Fade(f32, f32),
163 Width(Pixels, Pixels),
164 Height(Pixels, Pixels),
165}
166
167impl EffectTransition {
168 pub fn new(duration: Duration) -> Self {
169 Self {
170 duration,
171 easing: Rc::new(ease_out_cubic),
172 effects: SmallVec::new(),
173 }
174 }
175
176 pub fn ease(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
178 self.easing = Rc::new(easing);
179 self
180 }
181
182 pub fn slide_y(mut self, from: Pixels, to: Pixels) -> Self {
184 self.effects.push(TransitionEffect::SlideY(from, to));
185 self
186 }
187
188 pub fn slide_x(mut self, from: Pixels, to: Pixels) -> Self {
190 self.effects.push(TransitionEffect::SlideX(from, to));
191 self
192 }
193
194 pub fn fade(mut self, from: f32, to: f32) -> Self {
196 self.effects.push(TransitionEffect::Fade(from, to));
197 self
198 }
199
200 pub fn width(mut self, from: Pixels, to: Pixels) -> Self {
202 self.effects.push(TransitionEffect::Width(from, to));
203 self
204 }
205
206 pub fn height(mut self, from: Pixels, to: Pixels) -> Self {
208 self.effects.push(TransitionEffect::Height(from, to));
209 self
210 }
211
212 pub fn apply<E: IntoElement + Styled + 'static>(
214 self,
215 element: E,
216 id: impl Into<ElementId>,
217 ) -> gpui::AnimationElement<E> {
218 let animation = Animation::new(self.duration).with_easing({
219 let easing = self.easing.clone();
220 move |t| easing(t)
221 });
222 let effects = self.effects;
223 element.with_animation(id, animation, move |el, delta| {
224 let mut el = el;
225 for effect in &effects {
226 match effect {
227 TransitionEffect::SlideY(from, to) => {
228 el = el.top(Lerp::lerp(from, to, delta));
229 }
230 TransitionEffect::SlideX(from, to) => {
231 el = el.left(Lerp::lerp(from, to, delta));
232 }
233 TransitionEffect::Fade(from, to) => {
234 el = el.opacity(Lerp::lerp(from, to, delta));
235 }
236 TransitionEffect::Width(from, to) => {
237 el = el.w(Lerp::lerp(from, to, delta));
238 }
239 TransitionEffect::Height(from, to) => {
240 el = el.h(Lerp::lerp(from, to, delta));
241 }
242 }
243 }
244 el
245 })
246 }
247}
248
249impl FluentBuilder for EffectTransition {}
250
251#[deprecated(since = "0.5.2", note = "renamed to `EffectTransition`")]
256pub type Transition = EffectTransition;
257
258#[cfg(test)]
259mod tests {
260 use super::cubic_bezier;
261
262 #[test]
263 fn cubic_bezier_matches_engine_published_values() {
264 let ease = cubic_bezier(0.25, 0.1, 0.25, 1.);
266 for (t, expected) in [
267 (0.0, 0.0),
268 (0.2, 0.295),
269 (0.5, 0.802),
270 (0.8, 0.976),
271 (1.0, 1.0),
272 ] {
273 assert!(
274 (ease(t) - expected).abs() < 1e-3,
275 "ease({t}) = {}, expected {expected}",
276 ease(t)
277 );
278 }
279
280 let curve = cubic_bezier(0.25, 0., 0.75, 1.);
284 for (t, expected) in [
285 (0.05, 0.01136),
286 (0.1, 0.03978),
287 (0.15, 0.07978),
288 (0.2, 0.12803),
289 (0.25, 0.18235),
290 (0.3, 0.24115),
291 (0.35, 0.30323),
292 (0.4, 0.36761),
293 (0.45, 0.43345),
294 (0.5, 0.5),
295 (0.6, 0.63238),
296 (0.65, 0.69676),
297 (0.7, 0.75884),
298 (0.75, 0.81764),
299 (0.8, 0.87196),
300 (0.85, 0.92021),
301 (0.9, 0.96021),
302 (0.95, 0.98863),
303 ] {
304 assert!(
305 (curve(t) - expected).abs() < 3e-4,
306 "curve({t}) = {}, Chromium says {expected}",
307 curve(t)
308 );
309 }
310 }
311
312 #[test]
313 fn cubic_bezier_with_thirds_x_maps_time_identically() {
314 let ease = cubic_bezier(1. / 3., 0.72, 2. / 3., 1.);
318 for step in 0..=100 {
319 let t = step as f32 / 100.;
320 let one_t = 1. - t;
321 let expected = 3. * 0.72 * one_t * one_t * t + 3. * one_t * t * t + t * t * t;
322 assert!(
323 (ease(t) - expected).abs() < 1e-4,
324 "ease({t}) = {}, expected {expected}",
325 ease(t)
326 );
327 }
328 }
329
330 #[test]
331 fn cubic_bezier_matches_the_css_definition() {
332 fn css_reference(x1: f64, y1: f64, x2: f64, y2: f64, t: f64) -> f64 {
336 let sample = |p1: f64, p2: f64, s: f64| {
337 3. * p1 * (1. - s) * (1. - s) * s + 3. * p2 * (1. - s) * s * s + s * s * s
338 };
339 let (mut low, mut high) = (0f64, 1f64);
340 for _ in 0..64 {
341 let mid = (low + high) / 2.;
342 if sample(x1, x2, mid) < t {
343 low = mid;
344 } else {
345 high = mid;
346 }
347 }
348 sample(y1, y2, (low + high) / 2.)
349 }
350
351 let curves = [
352 (0.25, 0.1, 0.25, 1.),
354 (1. / 3., 0.72, 2. / 3., 1.),
355 (0.42, 0., 1., 1.),
357 (0., 0., 0.58, 1.),
358 (0.42, 0., 0.58, 1.),
359 (0., 0., 1., 1.),
360 (1., 0., 0., 1.),
363 (0., 0., 0., 1.),
364 (1., 0.5, 1., 0.5),
365 ];
366 for (x1, y1, x2, y2) in curves {
367 let ease = cubic_bezier(x1, y1, x2, y2);
368 for step in 0..=1000 {
369 let t = step as f32 / 1000.;
370 let y = ease(t);
371 let expected =
372 css_reference(x1 as f64, y1 as f64, x2 as f64, y2 as f64, t as f64) as f32;
373 assert!(
374 (y - expected).abs() < 5e-4,
375 "cubic_bezier({x1}, {y1}, {x2}, {y2})({t}) = {y}, CSS = {expected}"
376 );
377 assert!((0.0..=1.0).contains(&y), "ease({t}) = {y} out of range");
379 }
380 }
381 }
382
383 #[test]
384 fn cubic_bezier_is_monotonic_and_clamped() {
385 let ease = cubic_bezier(0.32, 0.72, 0., 1.);
386 assert_eq!(ease(-1.), 0.);
387 assert_eq!(ease(2.), 1.);
388
389 let mut previous = 0.;
390 for step in 0..=100 {
391 let current = ease(step as f32 / 100.);
392 assert!(current >= previous - 1e-4, "not monotonic at {step}");
393 previous = current;
394 }
395 }
396}