1use std::{
2 borrow::BorrowMut,
3 cell::{Ref, RefCell},
4 rc::Rc,
5 time::{Duration, Instant},
6};
7
8use crate::{App, Entity, EntityId, Window, lerp::Lerp, linear};
9
10#[derive(Clone)]
45pub struct Transition<T: Lerp + Clone + PartialEq + 'static> {
46 duration_secs: f32,
48
49 easing: Rc<dyn Fn(f32) -> f32>,
52
53 state: Entity<TransitionState<T>>,
54
55 cached_value: RefCell<Option<T>>,
57
58 continuous: bool,
62}
63
64impl<T: Lerp + Clone + PartialEq + 'static> Transition<T> {
65 pub fn new(state: Entity<TransitionState<T>>, duration: Duration) -> Self {
67 Self {
68 duration_secs: duration.as_secs_f32(),
69 easing: Rc::new(linear),
70 state,
71 cached_value: RefCell::new(None),
72 continuous: true,
73 }
74 }
75
76 pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
80 self.easing = Rc::new(easing);
81 self
82 }
83
84 pub fn continuous(mut self, continuous: bool) -> Self {
89 self.continuous = continuous;
90 self
91 }
92
93 fn default_goal_updated_at(&self) -> Instant {
94 Instant::now() - Duration::from_secs_f32(self.duration_secs)
95 }
96
97 fn raw_evaluate(&self, cx: &mut App) -> (bool, T) {
100 let mut state_entity = self.state.as_mut(cx);
101 let state: &mut TransitionState<T> = state_entity.borrow_mut();
102
103 let elapsed_secs = state
104 .goal_last_updated_at
105 .unwrap_or_else(|| self.default_goal_updated_at())
106 .elapsed()
107 .as_secs_f32();
108 let delta = (self.easing)((elapsed_secs / self.duration_secs).min(1.));
109
110 debug_assert!(
111 (0.0..=1.0).contains(&delta),
112 "delta should always be between 0 and 1"
113 );
114
115 state.last_delta = delta;
116
117 let evaluated_value = state.start_goal.lerp(&state.end_goal, delta);
118
119 (delta != 1., evaluated_value)
120 }
121
122 pub fn evaluate(&self, window: &mut Window, cx: &mut App) -> Ref<'_, T> {
132 if self.cached_value.borrow().is_none() {
133 let (in_progress, evaluated_value) = self.raw_evaluate(cx);
134
135 if in_progress {
136 window.request_animation_frame();
137 }
138
139 *self.cached_value.borrow_mut() = Some(evaluated_value);
140 }
141
142 Ref::map(self.cached_value.borrow(), |opt| opt.as_ref().unwrap())
143 }
144
145 pub fn read_goal<'b>(&'b self, cx: &'b mut App) -> &'b T {
147 &self.state.read(cx).end_goal
148 }
149
150 pub fn read_cache(&self) -> Ref<'_, Option<T>> {
152 self.cached_value.borrow()
153 }
154
155 pub fn evaluate_delta<'b>(&'b self, cx: &'b App) -> f32 {
161 let goal_last_updated_at = self
162 .state
163 .read(cx)
164 .goal_last_updated_at
165 .unwrap_or_else(|| self.default_goal_updated_at());
166
167 let elapsed_secs = goal_last_updated_at.elapsed().as_secs_f32();
168 (self.easing)((elapsed_secs / self.duration_secs).min(1.))
169 }
170
171 pub fn update<R>(
184 &self,
185 cx: &mut App,
186 update: impl FnOnce(&mut T, &mut crate::Context<TransitionState<T>>) -> R,
187 ) -> bool {
188 let mut was_updated = false;
189
190 self.state.update(cx, |state, cx| {
191 let last_end_goal = state.end_goal.clone();
192
193 update(&mut state.end_goal, cx);
194
195 if self.continuous && state.end_goal == last_end_goal {
196 return;
197 };
198
199 state.goal_last_updated_at = Some(Instant::now());
200
201 if self.continuous {
202 state.start_goal = state.start_goal.lerp(&last_end_goal, state.last_delta);
203 }
204
205 was_updated = true;
206 });
207
208 was_updated
209 }
210
211 pub fn jump_to(&self, target: T, cx: &mut App) {
217 self.state.update(cx, |state, _cx| {
218 state.start_goal = target.clone();
219 state.end_goal = target;
220 state.goal_last_updated_at = Some(Instant::now());
221 });
222 self.cached_value.borrow_mut().take();
223 }
224
225 pub fn scale_by(&self, ratio: f32, cx: &mut App)
232 where
233 T: std::ops::Mul<f32, Output = T>,
234 {
235 self.state.update(cx, |state, _cx| {
236 state.start_goal = state.start_goal.clone() * ratio;
237 state.end_goal = state.end_goal.clone() * ratio;
238 });
239 self.cached_value.borrow_mut().take();
240 }
241
242 pub fn entity_id(&self) -> EntityId {
246 self.state.entity_id()
247 }
248
249 pub fn reset(&self, cx: &mut App) {
255 self.state.update(cx, |state, _cx| {
256 state.goal_last_updated_at = None;
257 state.start_goal = state.initial_goal.clone();
258 state.end_goal = state.initial_goal.clone();
259 state.last_delta = 0.0;
260 });
261 *self.cached_value.borrow_mut() = None;
262 }
263}
264
265#[derive(Clone)]
271pub struct TransitionState<T: Lerp + Clone + PartialEq + 'static> {
272 pub(crate) goal_last_updated_at: Option<Instant>,
273 pub(crate) initial_goal: T,
274 pub(crate) start_goal: T,
275 pub(crate) end_goal: T,
276 pub(crate) last_delta: f32,
277}
278
279impl<T: Lerp + Clone + PartialEq + 'static> TransitionState<T> {
280 pub fn new(initial_goal: T) -> Self {
286 Self {
287 goal_last_updated_at: None,
288 initial_goal: initial_goal.clone(),
289 start_goal: initial_goal.clone(),
290 end_goal: initial_goal,
291 last_delta: 1.,
292 }
293 }
294}
295
296#[cfg(all(test, feature = "test-support"))]
297mod tests {
298 use crate::AppContext;
299
300 use super::*;
301 use gpui::{Point, TestAppContext, px};
302 use palette::rgb::Rgba;
303
304 fn create_transition<T: Lerp + Clone + PartialEq + 'static>(
307 cx: &mut App,
308 duration: Duration,
309 initial: T,
310 ) -> Transition<T> {
311 let state = cx.new(|_| TransitionState::new(initial));
312 Transition::new(state, duration)
313 }
314
315 #[gpui::test]
316 fn test_transition_creation(cx: &mut TestAppContext) {
317 cx.update(|cx| {
318 let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32);
319
320 let goal = transition.read_goal(cx);
322 assert_eq!(*goal, 0.0);
323 });
324 }
325
326 #[gpui::test]
327 fn test_transition_read_goal(cx: &mut TestAppContext) {
328 cx.update(|cx| {
329 let transition = create_transition(cx, Duration::from_millis(300), 42.0_f32);
330
331 let goal = transition.read_goal(cx);
332 assert_eq!(*goal, 42.0);
333 });
334 }
335
336 #[gpui::test]
337 fn test_transition_update_returns_true_on_change(cx: &mut TestAppContext) {
338 cx.update(|cx| {
339 let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32);
340
341 let was_updated = transition.update(cx, |val, _cx| {
342 *val = 100.0;
343 });
344
345 assert!(was_updated);
346 });
347 }
348
349 #[gpui::test]
350 fn test_transition_update_returns_false_on_no_change(cx: &mut TestAppContext) {
351 cx.update(|cx| {
352 let transition = create_transition(cx, Duration::from_millis(300), 50.0_f32);
353
354 let was_updated = transition.update(cx, |val, _cx| {
356 *val = 50.0;
357 });
358
359 assert!(!was_updated);
360 });
361 }
362
363 #[gpui::test]
364 fn test_transition_goal_updated_after_update(cx: &mut TestAppContext) {
365 cx.update(|cx| {
366 let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32);
367
368 transition.update(cx, |val, _cx| {
369 *val = 200.0;
370 });
371
372 let goal = transition.read_goal(cx);
373 assert_eq!(*goal, 200.0);
374 });
375 }
376
377 #[gpui::test]
378 fn test_transition_entity_id(cx: &mut TestAppContext) {
379 cx.update(|cx| {
380 let transition1 = create_transition(cx, Duration::from_millis(300), 0.0_f32);
381 let transition2 = create_transition(cx, Duration::from_millis(300), 0.0_f32);
382
383 assert_ne!(transition1.entity_id(), transition2.entity_id());
385 });
386 }
387
388 #[gpui::test]
389 fn test_transition_reset(cx: &mut TestAppContext) {
390 cx.update(|cx| {
391 let transition = create_transition(cx, Duration::from_millis(300), 10.0_f32);
392
393 transition.update(cx, |val, _cx| {
395 *val = 100.0;
396 });
397
398 assert_eq!(*transition.read_goal(cx), 100.0);
399
400 transition.reset(cx);
402
403 assert_eq!(*transition.read_goal(cx), 10.0);
405 });
406 }
407
408 #[gpui::test]
409 fn test_transition_cache_cleared_on_reset(cx: &mut TestAppContext) {
410 cx.update(|cx| {
411 let transition = create_transition(cx, Duration::from_millis(300), 25.0_f32);
412
413 let (_, value) = transition.raw_evaluate(cx);
415 *transition.cached_value.borrow_mut() = Some(value);
416 assert!(transition.read_cache().is_some());
417
418 transition.reset(cx);
420
421 assert!(transition.read_cache().is_none());
423 });
424 }
425
426 #[gpui::test]
427 fn test_transition_with_custom_easing(cx: &mut TestAppContext) {
428 cx.update(|cx| {
429 let transition =
431 create_transition(cx, Duration::from_millis(300), 0.0_f32).with_easing(|_| 0.5);
432
433 transition.update(cx, |val, _cx| {
435 *val = 100.0;
436 });
437
438 let (_, value) = transition.raw_evaluate(cx);
441 assert_eq!(value, 50.0);
442 });
443 }
444
445 #[gpui::test]
446 fn test_transition_with_point(cx: &mut TestAppContext) {
447 cx.update(|cx| {
448 let initial: Point<f32> = Point { x: 0.0, y: 0.0 };
449 let transition = create_transition(cx, Duration::from_millis(300), initial);
450
451 let goal = transition.read_goal(cx);
452 assert_eq!(goal.x, 0.0);
453 assert_eq!(goal.y, 0.0);
454
455 transition.update(cx, |point, _cx| {
456 point.x = 100.0;
457 point.y = 200.0;
458 });
459
460 let goal = transition.read_goal(cx);
461 assert_eq!(goal.x, 100.0);
462 assert_eq!(goal.y, 200.0);
463 });
464 }
465
466 #[gpui::test]
467 fn test_transition_with_rgba(cx: &mut TestAppContext) {
468 cx.update(|cx| {
469 let initial = Rgba::new(1., 0., 0., 1.);
470 let transition = create_transition(cx, Duration::from_millis(300), initial);
471
472 let goal = transition.read_goal(cx);
473 assert_eq!(goal.red, 1.0);
474 assert_eq!(goal.green, 0.0);
475 assert_eq!(goal.blue, 0.0);
476 });
477 }
478
479 #[gpui::test]
480 fn test_transition_continuous_mode_default(cx: &mut TestAppContext) {
481 cx.update(|cx| {
482 let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32);
484
485 transition.update(cx, |val, _cx| {
487 *val = 50.0;
488 });
489
490 let was_updated = transition.update(cx, |val, _cx| {
492 *val = 100.0;
493 });
494
495 assert!(was_updated);
496 assert_eq!(*transition.read_goal(cx), 100.0);
497 });
498 }
499
500 #[gpui::test]
501 fn test_transition_non_continuous_mode(cx: &mut TestAppContext) {
502 cx.update(|cx| {
503 let transition =
504 create_transition(cx, Duration::from_millis(300), 0.0_f32).continuous(false);
505
506 transition.update(cx, |val, _cx| {
508 *val = 100.0;
509 });
510
511 assert_eq!(*transition.read_goal(cx), 100.0);
512 });
513 }
514
515 #[gpui::test]
516 fn test_evaluate_delta_initial(cx: &mut TestAppContext) {
517 cx.update(|cx| {
518 let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32);
519
520 let delta = transition.evaluate_delta(cx);
523 assert_eq!(delta, 1.0);
524 });
525 }
526
527 #[gpui::test]
528 fn test_evaluate_delta_after_update(cx: &mut TestAppContext) {
529 cx.update(|cx| {
530 let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32);
531
532 transition.update(cx, |val, _cx| {
533 *val = 100.0;
534 });
535
536 let delta = transition.evaluate_delta(cx);
538 assert!(
539 delta < 0.1,
540 "delta should be small immediately after update"
541 );
542 });
543 }
544
545 #[gpui::test]
546 fn test_transition_clone(cx: &mut TestAppContext) {
547 cx.update(|cx| {
548 let transition = create_transition(cx, Duration::from_millis(300), 42.0_f32);
549
550 let cloned = transition.clone();
551
552 assert_eq!(transition.entity_id(), cloned.entity_id());
554
555 transition.update(cx, |val, _cx| {
557 *val = 100.0;
558 });
559
560 assert_eq!(*cloned.read_goal(cx), 100.0);
561 });
562 }
563
564 #[gpui::test]
565 fn test_transition_cache_consistency(cx: &mut TestAppContext) {
566 cx.update(|cx| {
567 let transition =
568 create_transition(cx, Duration::from_millis(300), 0.0_f32).with_easing(|_| 0.5); transition.update(cx, |val, _cx| {
571 *val = 100.0;
572 });
573
574 let (_, value1) = transition.raw_evaluate(cx);
576
577 let (_, value2) = transition.raw_evaluate(cx);
579
580 assert_eq!(value1, value2);
581 });
582 }
583
584 #[gpui::test]
585 fn test_multiple_transitions_independent(cx: &mut TestAppContext) {
586 cx.update(|cx| {
587 let transition_a = create_transition(cx, Duration::from_millis(300), 0.0_f32);
588 let transition_b = create_transition(cx, Duration::from_millis(300), 100.0_f32);
589
590 transition_a.update(cx, |val, _cx| {
592 *val = 50.0;
593 });
594
595 assert_eq!(*transition_a.read_goal(cx), 50.0);
597 assert_eq!(*transition_b.read_goal(cx), 100.0);
598 });
599 }
600
601 #[gpui::test]
602 fn test_transition_with_pixels(cx: &mut TestAppContext) {
603 cx.update(|cx| {
604 let transition = create_transition(cx, Duration::from_millis(300), px(0.0));
605
606 let goal = transition.read_goal(cx);
607 assert_eq!(*goal, px(0.0));
608
609 transition.update(cx, |val, _cx| {
610 *val = px(100.0);
611 });
612
613 let goal = transition.read_goal(cx);
614 assert_eq!(*goal, px(100.0));
615 });
616 }
617
618 #[gpui::test]
619 fn test_transition_rapid_updates(cx: &mut TestAppContext) {
620 cx.update(|cx| {
621 let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32);
622
623 for i in 1..=10 {
625 transition.update(cx, |val, _cx| {
626 *val = i as f32 * 10.0;
627 });
628 }
629
630 assert_eq!(*transition.read_goal(cx), 100.0);
632 });
633 }
634
635 #[gpui::test]
636 fn test_raw_evaluate_in_progress(cx: &mut TestAppContext) {
637 cx.update(|cx| {
638 let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32);
639
640 transition.update(cx, |val, _cx| {
641 *val = 100.0;
642 });
643
644 let (in_progress, _value) = transition.raw_evaluate(cx);
646 assert!(
647 in_progress,
648 "transition should be in progress immediately after update"
649 );
650 });
651 }
652
653 #[gpui::test]
654 fn test_raw_evaluate_completed(cx: &mut TestAppContext) {
655 cx.update(|cx| {
656 let transition = create_transition(cx, Duration::from_millis(300), 50.0_f32);
657
658 let (in_progress, value) = transition.raw_evaluate(cx);
660 assert!(
661 !in_progress,
662 "transition should be complete without updates"
663 );
664 assert_eq!(value, 50.0);
665 });
666 }
667
668 #[gpui::test]
669 fn test_transition_interpolation_with_easing(cx: &mut TestAppContext) {
670 cx.update(|cx| {
671 for expected_delta in [0.0, 0.25, 0.5, 0.75, 1.0] {
673 let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32)
674 .with_easing(move |_| expected_delta);
675
676 transition.update(cx, |val, _cx| {
677 *val = 100.0;
678 });
679
680 let (_, value) = transition.raw_evaluate(cx);
681 let expected_value = 0.0_f32.lerp(&100.0_f32, expected_delta);
682 assert_eq!(value, expected_value);
683 }
684 });
685 }
686
687 #[test]
688 fn test_new_state_initialization() {
689 let state = TransitionState::new(42.0_f32);
690
691 assert_eq!(state.initial_goal, 42.0);
692 assert_eq!(state.start_goal, 42.0);
693 assert_eq!(state.end_goal, 42.0);
694 assert_eq!(state.last_delta, 1.0);
695 assert!(state.goal_last_updated_at.is_none());
696 }
697
698 #[test]
699 fn test_state_with_point() {
700 let initial: Point<f32> = Point { x: 10.0, y: 20.0 };
701 let state = TransitionState::new(initial);
702
703 assert_eq!(state.initial_goal.x, 10.0);
704 assert_eq!(state.initial_goal.y, 20.0);
705 assert_eq!(state.start_goal.x, 10.0);
706 assert_eq!(state.end_goal.x, 10.0);
707 }
708
709 #[test]
710 fn test_state_with_rgba() {
711 let initial = Rgba::new(1., 0.5, 0., 1.);
712 let state = TransitionState::new(initial);
713
714 assert_eq!(state.initial_goal.red, 1.0);
715 assert_eq!(state.initial_goal.green, 0.5);
716 assert_eq!(state.initial_goal.blue, 0.0);
717 assert_eq!(state.initial_goal.alpha, 1.0);
718 }
719
720 #[test]
721 fn test_state_clone() {
722 let state = TransitionState::new(100.0_f32);
723 let cloned = state.clone();
724
725 assert_eq!(state.initial_goal, cloned.initial_goal);
726 assert_eq!(state.start_goal, cloned.start_goal);
727 assert_eq!(state.end_goal, cloned.end_goal);
728 assert_eq!(state.last_delta, cloned.last_delta);
729 }
730
731 #[test]
732 fn test_state_with_integer() {
733 let state = TransitionState::new(50_i32);
734
735 assert_eq!(state.initial_goal, 50);
736 assert_eq!(state.start_goal, 50);
737 assert_eq!(state.end_goal, 50);
738 }
739
740 #[test]
741 fn test_state_starts_completed() {
742 let state = TransitionState::new(0.0_f32);
743
744 assert_eq!(state.last_delta, 1.0);
746 }
747}