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