1use super::{AnimValue, Easing, Frame, Prop};
13use crate::transition::TransitionKind;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub enum Loop {
18 #[default]
19 Once,
20 Times(u32),
22 Forever,
23}
24
25impl Loop {
26 pub fn count(self) -> Option<u32> {
28 match self {
29 Loop::Once => Some(1),
30 Loop::Times(n) => Some(n.max(1)),
31 Loop::Forever => None,
32 }
33 }
34}
35
36#[derive(Debug, Clone, Copy, PartialEq)]
39pub struct Keyframe {
40 pub value: AnimValue,
41 pub duration: Option<f32>,
44 pub delay: f32,
46 pub ease: Option<Easing>,
48}
49
50impl Keyframe {
51 pub fn to(value: impl Into<AnimValue>) -> Self {
52 Keyframe {
53 value: value.into(),
54 duration: None,
55 delay: 0.0,
56 ease: None,
57 }
58 }
59
60 pub fn duration(mut self, ms: f32) -> Self {
61 self.duration = Some(ms.max(0.0));
62 self
63 }
64
65 pub fn delay(mut self, ms: f32) -> Self {
66 self.delay = ms.max(0.0);
67 self
68 }
69
70 pub fn ease(mut self, easing: Easing) -> Self {
71 self.ease = Some(easing);
72 self
73 }
74}
75
76pub trait IntoKeyframe {
83 fn into_keyframe(self) -> Keyframe;
84}
85
86impl IntoKeyframe for Keyframe {
87 fn into_keyframe(self) -> Keyframe {
88 self
89 }
90}
91
92macro_rules! keyframe_from_value {
93 ($($ty:ty),* $(,)?) => {
94 $(
95 impl IntoKeyframe for $ty {
96 fn into_keyframe(self) -> Keyframe {
97 Keyframe::to(self)
98 }
99 }
100 )*
101 };
102}
103
104keyframe_from_value!(
105 f32,
106 f64,
107 i32,
108 gpui::Pixels,
109 gpui::Hsla,
110 gpui::Rgba,
111 AnimValue
112);
113
114#[derive(Debug, Clone, PartialEq)]
116pub struct Track {
117 pub prop: Prop,
118 pub from: AnimValue,
121 pub frames: Vec<Keyframe>,
122}
123
124impl Track {
125 pub fn new(prop: Prop, from: impl Into<AnimValue>) -> Self {
126 Track {
127 prop,
128 from: from.into(),
129 frames: Vec::new(),
130 }
131 }
132
133 pub fn to(mut self, value: impl Into<AnimValue>) -> Self {
134 self.frames.push(Keyframe::to(value));
135 self
136 }
137
138 pub fn keyframe(mut self, frame: Keyframe) -> Self {
139 self.frames.push(frame);
140 self
141 }
142
143 fn leg_duration(&self, duration: f32) -> f32 {
145 let mut fixed = 0.0;
146 let mut flexible = 0usize;
147 for frame in &self.frames {
148 fixed += frame.delay;
149 match frame.duration {
150 Some(d) => fixed += d,
151 None => flexible += 1,
152 }
153 }
154 if flexible == 0 {
155 0.0
156 } else {
157 ((duration - fixed) / flexible as f32).max(0.0)
158 }
159 }
160
161 pub fn span(&self, duration: f32) -> f32 {
165 let each = self.leg_duration(duration);
166 self
167 .frames
168 .iter()
169 .map(|frame| frame.delay + frame.duration.unwrap_or(each))
170 .sum()
171 }
172
173 pub fn sample(&self, t: f32, duration: f32, ease: Easing) -> AnimValue {
176 let each = self.leg_duration(duration);
177 let mut value = self.from;
178 let mut cursor = 0.0;
179 for frame in &self.frames {
180 let leg = frame.duration.unwrap_or(each);
181 let start = cursor + frame.delay;
182 if t <= start {
183 return value;
184 }
185 let end = start + leg;
186 if t < end {
187 let local = if leg <= 0.0 { 1.0 } else { (t - start) / leg };
188 let curve = frame.ease.unwrap_or(ease);
189 return value.lerp(frame.value, curve.apply(local));
190 }
191 value = frame.value;
192 cursor = end;
193 }
194 value
195 }
196}
197
198#[derive(Debug, Clone, PartialEq)]
200pub struct Motion {
201 pub tracks: Vec<Track>,
202 pub duration: f32,
204 pub delay: f32,
208 pub end_delay: f32,
211 pub ease: Easing,
212 pub loops: Loop,
213 pub alternate: bool,
215 pub reversed: bool,
217}
218
219impl Default for Motion {
220 fn default() -> Self {
221 Motion {
222 tracks: Vec::new(),
223 duration: 300.0,
224 delay: 0.0,
225 end_delay: 0.0,
226 ease: Easing::default(),
227 loops: Loop::Once,
228 alternate: false,
229 reversed: false,
230 }
231 }
232}
233
234impl Motion {
235 pub fn new() -> Self {
236 Motion::default()
237 }
238
239 pub fn tween(mut self, prop: Prop, from: impl Into<AnimValue>, to: impl Into<AnimValue>) -> Self {
241 self.tracks.push(Track::new(prop, from).to(to));
242 self
243 }
244
245 pub fn keyframes<K: IntoKeyframe>(
248 mut self,
249 prop: Prop,
250 from: impl Into<AnimValue>,
251 frames: impl IntoIterator<Item = K>,
252 ) -> Self {
253 let mut track = Track::new(prop, from);
254 track
255 .frames
256 .extend(frames.into_iter().map(IntoKeyframe::into_keyframe));
257 self.tracks.push(track);
258 self
259 }
260
261 pub fn track(mut self, track: Track) -> Self {
262 self.tracks.push(track);
263 self
264 }
265
266 pub fn duration(mut self, ms: f32) -> Self {
268 self.duration = ms.max(0.0);
269 self
270 }
271
272 pub fn delay(mut self, ms: f32) -> Self {
273 self.delay = ms.max(0.0);
274 self
275 }
276
277 pub fn end_delay(mut self, ms: f32) -> Self {
278 self.end_delay = ms.max(0.0);
279 self
280 }
281
282 pub fn ease(mut self, easing: Easing) -> Self {
283 self.ease = easing;
284 self
285 }
286
287 pub fn loops(mut self, loops: Loop) -> Self {
288 self.loops = loops;
289 self
290 }
291
292 pub fn repeat(mut self, times: u32) -> Self {
293 self.loops = Loop::Times(times);
294 self
295 }
296
297 pub fn repeat_forever(mut self) -> Self {
298 self.loops = Loop::Forever;
299 self
300 }
301
302 pub fn alternate(mut self, alternate: bool) -> Self {
303 self.alternate = alternate;
304 self
305 }
306
307 pub fn reversed(mut self, reversed: bool) -> Self {
308 self.reversed = reversed;
309 self
310 }
311
312 pub fn iteration_ms(&self) -> f32 {
314 let span = self
315 .tracks
316 .iter()
317 .map(|track| track.span(self.duration))
318 .fold(0.0_f32, f32::max);
319 self.delay + span + self.end_delay
320 }
321
322 pub fn total_ms(&self) -> f32 {
325 match self.loops.count() {
326 Some(n) => self.iteration_ms() * n as f32,
327 None => f32::INFINITY,
328 }
329 }
330
331 pub fn sample(&self, t: f32) -> Frame {
333 let mut frame = Frame::new();
334 self.sample_into(t, &mut frame);
335 frame
336 }
337
338 pub fn sample_into(&self, t: f32, frame: &mut Frame) {
341 let (local, progress, finished) = fold_time(
344 t,
345 self.iteration_ms(),
346 self.loops,
347 self.alternate,
348 self.reversed,
349 );
350 let track_time = local - self.delay;
354 for track in &self.tracks {
355 frame.set(
356 track.prop,
357 track.sample(track_time, self.duration, self.ease),
358 );
359 }
360 frame.progress = progress;
361 frame.finished = finished;
362 }
363}
364
365pub(crate) fn fold_time(
369 t: f32,
370 iteration: f32,
371 loops: Loop,
372 alternate: bool,
373 reversed: bool,
374) -> (f32, f32, bool) {
375 let total = match loops.count() {
376 Some(n) => iteration * n as f32,
377 None => f32::INFINITY,
378 };
379 let t = t.max(0.0);
380 let finished = total.is_finite() && t >= total;
381 let (index, mut local) = if iteration <= 0.0 {
382 (0u32, 0.0)
383 } else if finished {
384 (loops.count().unwrap_or(1).saturating_sub(1), iteration)
386 } else {
387 ((t / iteration) as u32, t % iteration)
388 };
389
390 if alternate && index % 2 == 1 {
391 local = iteration - local;
392 }
393 if reversed {
394 local = iteration - local;
395 }
396
397 let progress = if total.is_finite() {
398 if total <= 0.0 {
399 1.0
400 } else {
401 (t / total).clamp(0.0, 1.0)
402 }
403 } else if iteration <= 0.0 {
404 0.0
405 } else {
406 (t % iteration) / iteration
407 };
408
409 (local, progress, finished)
410}
411
412pub const SLIDE_DISTANCE: f32 = 8.0;
414
415impl Motion {
416 pub fn enter(kind: TransitionKind) -> Self {
419 Motion::enter_from(kind, SLIDE_DISTANCE)
420 }
421
422 pub fn enter_from(kind: TransitionKind, distance: f32) -> Self {
424 let motion = Motion::new().duration(200.0).tween(Prop::Opacity, 0.0, 1.0);
425 match kind {
426 TransitionKind::Fade => motion,
427 TransitionKind::SlideUp => motion.tween(Prop::Y, distance, 0.0),
428 TransitionKind::SlideDown => motion.tween(Prop::Y, -distance, 0.0),
429 TransitionKind::SlideLeft => motion.tween(Prop::X, distance, 0.0),
430 TransitionKind::SlideRight => motion.tween(Prop::X, -distance, 0.0),
431 }
432 }
433
434 pub fn exit(kind: TransitionKind) -> Self {
436 Motion::exit_to(kind, SLIDE_DISTANCE)
437 }
438
439 pub fn exit_to(kind: TransitionKind, distance: f32) -> Self {
440 let motion = Motion::new().duration(160.0).tween(Prop::Opacity, 1.0, 0.0);
441 match kind {
442 TransitionKind::Fade => motion,
443 TransitionKind::SlideUp => motion.tween(Prop::Y, 0.0, -distance),
444 TransitionKind::SlideDown => motion.tween(Prop::Y, 0.0, distance),
445 TransitionKind::SlideLeft => motion.tween(Prop::X, 0.0, -distance),
446 TransitionKind::SlideRight => motion.tween(Prop::X, 0.0, distance),
447 }
448 }
449
450 pub fn as_margins(mut self) -> Self {
459 for track in &mut self.tracks {
460 track.prop = match track.prop {
461 Prop::X => Prop::MarginLeft,
462 Prop::Y => Prop::MarginTop,
463 other => other,
464 };
465 }
466 self
467 }
468
469 pub fn pulse() -> Self {
472 Motion::new()
473 .duration(900.0)
474 .ease(Easing::InOut(super::ease::Curve::Sine))
475 .alternate(true)
476 .repeat_forever()
477 .tween(Prop::Opacity, 1.0, 0.35)
478 }
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484
485 fn opacity(motion: &Motion, t: f32) -> f32 {
486 motion.sample(t).number(Prop::Opacity).unwrap()
487 }
488
489 #[test]
490 fn a_tween_runs_from_end_to_end() {
491 let motion = Motion::new()
492 .duration(100.0)
493 .ease(Easing::Linear)
494 .tween(Prop::Opacity, 0.0, 1.0);
495 assert_eq!(motion.iteration_ms(), 100.0);
496 assert_eq!(opacity(&motion, 0.0), 0.0);
497 assert!((opacity(&motion, 50.0) - 0.5).abs() < 1e-5);
498 assert_eq!(opacity(&motion, 100.0), 1.0);
499 assert_eq!(opacity(&motion, 500.0), 1.0);
500 }
501
502 #[test]
503 fn a_delay_holds_the_starting_value() {
504 let motion = Motion::new()
505 .duration(100.0)
506 .delay(50.0)
507 .ease(Easing::Linear)
508 .tween(Prop::Opacity, 0.0, 1.0);
509 assert_eq!(motion.iteration_ms(), 150.0);
510 assert_eq!(opacity(&motion, 0.0), 0.0);
511 assert_eq!(opacity(&motion, 49.0), 0.0);
512 assert!((opacity(&motion, 100.0) - 0.5).abs() < 1e-5);
513 assert_eq!(opacity(&motion, 150.0), 1.0);
514 }
515
516 #[test]
517 fn unsized_keyframes_split_the_duration_evenly() {
518 let motion = Motion::new()
519 .duration(300.0)
520 .ease(Easing::Linear)
521 .keyframes(
522 Prop::X,
523 0.0,
524 [Keyframe::to(10.0), Keyframe::to(20.0), Keyframe::to(30.0)],
525 );
526 assert_eq!(motion.iteration_ms(), 300.0);
527 assert!((motion.sample(100.0).number(Prop::X).unwrap() - 10.0).abs() < 1e-4);
528 assert!((motion.sample(200.0).number(Prop::X).unwrap() - 20.0).abs() < 1e-4);
529 assert!((motion.sample(300.0).number(Prop::X).unwrap() - 30.0).abs() < 1e-4);
530 }
531
532 #[test]
533 fn a_fixed_leg_takes_its_time_and_the_rest_share() {
534 let motion = Motion::new()
535 .duration(300.0)
536 .ease(Easing::Linear)
537 .keyframes(
538 Prop::X,
539 0.0,
540 [Keyframe::to(10.0).duration(200.0), Keyframe::to(20.0)],
541 );
542 assert_eq!(motion.iteration_ms(), 300.0);
543 assert!((motion.sample(100.0).number(Prop::X).unwrap() - 5.0).abs() < 1e-4);
544 assert!((motion.sample(250.0).number(Prop::X).unwrap() - 15.0).abs() < 1e-4);
545 }
546
547 #[test]
548 fn over_long_legs_stretch_the_motion() {
549 let motion =
550 Motion::new()
551 .duration(100.0)
552 .keyframes(Prop::X, 0.0, [Keyframe::to(1.0).duration(400.0)]);
553 assert_eq!(motion.iteration_ms(), 400.0);
554 }
555
556 #[test]
557 fn repeats_replay_the_iteration() {
558 let motion = Motion::new()
559 .duration(100.0)
560 .ease(Easing::Linear)
561 .repeat(3)
562 .tween(Prop::Opacity, 0.0, 1.0);
563 assert_eq!(motion.total_ms(), 300.0);
564 assert!((opacity(&motion, 150.0) - 0.5).abs() < 1e-5);
565 assert!(!motion.sample(299.0).finished);
566 assert!(motion.sample(300.0).finished);
567 }
568
569 #[test]
570 fn alternate_runs_odd_iterations_backwards() {
571 let motion = Motion::new()
572 .duration(100.0)
573 .ease(Easing::Linear)
574 .repeat(2)
575 .alternate(true)
576 .tween(Prop::Opacity, 0.0, 1.0);
577 assert!((opacity(&motion, 50.0) - 0.5).abs() < 1e-5);
578 assert!((opacity(&motion, 150.0) - 0.5).abs() < 1e-5);
579 assert!(opacity(&motion, 120.0) > 0.75);
580 }
581
582 #[test]
583 fn reversed_plays_from_the_far_end() {
584 let motion = Motion::new()
585 .duration(100.0)
586 .ease(Easing::Linear)
587 .reversed(true)
588 .tween(Prop::Opacity, 0.0, 1.0);
589 assert_eq!(opacity(&motion, 0.0), 1.0);
590 assert_eq!(opacity(&motion, 100.0), 0.0);
591 }
592
593 #[test]
594 fn forever_never_finishes() {
595 let motion = Motion::pulse();
596 assert!(motion.total_ms().is_infinite());
597 let frame = motion.sample(10_000.0);
598 assert!(!frame.finished);
599 assert!((0.0..=1.0).contains(&frame.progress));
600 }
601
602 #[test]
603 fn end_delay_holds_the_final_value_inside_the_iteration() {
604 let motion = Motion::new()
605 .duration(100.0)
606 .end_delay(100.0)
607 .ease(Easing::Linear)
608 .tween(Prop::Opacity, 0.0, 1.0);
609 assert_eq!(motion.iteration_ms(), 200.0);
610 assert_eq!(opacity(&motion, 150.0), 1.0);
611 }
612
613 #[test]
614 fn as_margins_moves_the_offsets_off_the_inset() {
615 let motion = Motion::enter(TransitionKind::SlideUp).as_margins();
616 let start = motion.sample(0.0);
617 assert_eq!(start.number(Prop::Y), None);
618 assert_eq!(start.number(Prop::MarginTop), Some(SLIDE_DISTANCE));
619 assert_eq!(start.number(Prop::Opacity), Some(0.0));
621 }
622
623 #[test]
624 fn presets_start_hidden_and_end_settled() {
625 for kind in [
626 TransitionKind::Fade,
627 TransitionKind::SlideUp,
628 TransitionKind::SlideDown,
629 TransitionKind::SlideLeft,
630 TransitionKind::SlideRight,
631 ] {
632 let motion = Motion::enter(kind);
633 let start = motion.sample(0.0);
634 let end = motion.sample(motion.total_ms());
635 assert_eq!(start.number(Prop::Opacity), Some(0.0));
636 assert_eq!(end.number(Prop::Opacity), Some(1.0));
637 if kind != TransitionKind::Fade {
638 let moved = end.number(Prop::X).or(end.number(Prop::Y)).unwrap();
639 assert_eq!(moved, 0.0);
640 }
641 }
642 }
643}
644
645#[cfg(test)]
646mod bench {
647 use super::*;
648 use std::time::Instant;
649
650 #[test]
657 #[ignore = "a measurement, not a test"]
658 fn sampling_cost() {
659 let simple = Motion::enter(crate::TransitionKind::SlideUp);
660 let heavy = Motion::new().duration(900.0).keyframes(
661 Prop::Y,
662 0.0,
663 [
664 Keyframe::to(10.0),
665 Keyframe::to(20.0),
666 Keyframe::to(30.0),
667 Keyframe::to(40.0),
668 ],
669 );
670 let sequence = crate::Sequence::new()
671 .add(simple.clone())
672 .add(heavy.clone())
673 .add(simple.clone());
674
675 type Case = (&'static str, Box<dyn Fn(f32)>);
676 let cases: [Case; 3] = [
677 (
678 "motion(2 tracks)",
679 Box::new(move |t| {
680 simple.sample(t);
681 }),
682 ),
683 (
684 "motion(4 legs)",
685 Box::new(move |t| {
686 heavy.sample(t);
687 }),
688 ),
689 (
690 "sequence(3)",
691 Box::new(move |t| {
692 sequence.sample(t);
693 }),
694 ),
695 ];
696 for (name, run) in cases {
697 let start = Instant::now();
698 for i in 0..100_000 {
699 run(i as f32 * 0.01);
700 }
701 let each = start.elapsed().as_secs_f64() * 1e9 / 100_000.0;
702 println!("{name:20} {each:8.1} ns/sample");
703 }
704 }
705}