Skip to main content

gpui/
transition.rs

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/// An animated transition between values of type `T`.
11///
12/// `Transition` manages the interpolation of a value from a start state to a goal
13/// state over a specified duration. It supports customizable easing functions and
14/// can operate in continuous or non-continuous mode.
15///
16/// # Type Parameters
17///
18/// * `T` - The type of value being transitioned. Must implement [`Lerp`], [`Clone`],
19///   and [`PartialEq`].
20///
21/// # Continuous vs Non-Continuous Mode
22///
23/// By default, transitions operate in continuous mode. When the goal is updated:
24/// - **Continuous mode** (`continuous = true`): The transition smoothly continues
25///   from the current interpolated value to the new goal.
26/// - **Non-continuous mode** (`continuous = false`): The transition restarts from
27///   the initial value to the new goal.
28///
29/// # Example
30///
31/// ```ignore
32/// let transition = window.use_transition(cx, Duration::from_millis(300), |_, _| 0.0_f32)
33///     .with_easing(ease_in_out);
34///
35/// // Get the current interpolated value
36/// let value = transition.evaluate(window, cx);
37///
38/// // Update the goal
39/// transition.update(cx, |val, cx| {
40///     *val = 1.0;
41///     cx.notify();
42/// });
43/// ```
44#[derive(Clone)]
45pub struct Transition<T: Lerp + Clone + PartialEq + 'static> {
46    /// The amount of time for which this transition should run.
47    duration_secs: f32,
48
49    /// A function that takes a delta between 0 and 1 and returns a new delta
50    /// between 0 and 1 based on the given easing function.
51    easing: Rc<dyn Fn(f32) -> f32>,
52
53    state: Entity<TransitionState<T>>,
54
55    /// A cached version of the transition's value.
56    cached_value: RefCell<Option<T>>,
57
58    /// Whether to continue the transition from the current value when the goal changes.
59    /// If true, transitions smoothly from current animated value to new goal.
60    /// If false, restarts from the original start value.
61    continuous: bool,
62}
63
64impl<T: Lerp + Clone + PartialEq + 'static> Transition<T> {
65    /// Create a new transition with the given duration using the specified state.
66    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    /// Set the easing function to use for this transition.
77    /// The easing function will take a time delta between 0 and 1 and return a new delta
78    /// between 0 and 1
79    pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
80        self.easing = Rc::new(easing);
81        self
82    }
83
84    /// Sets whether the transition should be continuous.
85    ///
86    /// On goal updates, transitions continue from the current value by default.
87    /// If `continuous` is set to false, the transition will restart from its initial value.
88    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    /// Evaluates the value of the transition without using the cache.
98    /// Returns if the transition is finished (bool) and the evaluated value (T).
99    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    /// Evaluates and returns the current interpolated value of the transition.
123    ///
124    /// This method calculates the value based on the elapsed time since the last
125    /// goal update, applies the easing function, and caches the result. If the
126    /// transition is still in progress, it automatically requests an animation
127    /// frame to continue the animation.
128    ///
129    /// The returned value is cached for the duration of the current frame to avoid
130    /// redundant calculations when called multiple times.
131    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    /// Reads the end goal of the transitions.
146    pub fn read_goal<'b>(&'b self, cx: &'b mut App) -> &'b T {
147        &self.state.read(cx).end_goal
148    }
149
150    /// Reads the current value of the cached transition, if it exists.
151    pub fn read_cache(&self) -> Ref<'_, Option<T>> {
152        self.cached_value.borrow()
153    }
154
155    /// Evaluates and returns the current progress delta of the transition.
156    ///
157    /// Returns a value between 0.0 and 1.0 representing how far the transition
158    /// has progressed, after applying the easing function. A value of 0.0 means
159    /// the transition just started, and 1.0 means it has completed.
160    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    /// Updates the goal value for the transition.
172    ///
173    /// The provided closure receives a mutable reference to the current goal value
174    /// and can modify it. If the goal changes (and continuous mode is enabled),
175    /// a new animation will begin from the current interpolated value toward the
176    /// new goal.
177    ///
178    /// Returns `true` if the goal was actually updated (i.e., the new value differs
179    /// from the previous goal), `false` otherwise.
180    ///
181    /// Note: This method does not automatically notify GPUI of changes. You should
182    /// call `cx.notify()` within the closure if you want to trigger a re-render.
183    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    /// Instantly set the transition to the given target value without animation.
212    ///
213    /// Sets both the start and end goals to `target` so that subsequent evaluations
214    /// return `target` immediately. This is useful for transitions that require a
215    /// different start value on each update.
216    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    /// Scale the transition's start and end goals by the given ratio.
226    ///
227    /// This preserves the relative progress of an in-flight animation when the
228    /// coordinate space changes (e.g. on window resize).  Both the start and
229    /// end goal are multiplied by `ratio` so the interpolated value remains
230    /// proportionally correct.
231    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    /// Returns the entity ID associated with this transition's state.
243    ///
244    /// This can be useful for tracking or comparing transitions.
245    pub fn entity_id(&self) -> EntityId {
246        self.state.entity_id()
247    }
248
249    /// Resets the transition to its initial state.
250    ///
251    /// This clears all progress and sets both the start and end goals back to
252    /// the initial value that was provided when the transition was created.
253    /// The cache is also cleared.
254    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// Internal state container for a [`Transition`](crate::Transition).
266///
267/// This struct holds the data necessary to track a transition's progress,
268/// including the start and end goals, timing information, and the last
269/// computed delta value.
270#[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    /// Creates a new transition state with the given initial goal.
281    ///
282    /// The start goal, end goal, and initial goal are all set to the provided value.
283    /// The transition begins in a "completed" state (delta = 1.0) until the goal
284    /// is updated.
285    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    /// Helper to create a Transition directly without using window hooks.
305    /// This bypasses the render-phase restriction of use_transition/use_keyed_transition.
306    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            // Read the goal - should be the initial value
321            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            // Update to the same value
355            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            // Different transitions should have different entity IDs
384            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            // Update the goal
394            transition.update(cx, |val, _cx| {
395                *val = 100.0;
396            });
397
398            assert_eq!(*transition.read_goal(cx), 100.0);
399
400            // Reset the transition
401            transition.reset(cx);
402
403            // Goal should be back to initial value
404            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            // Manually populate the cache using raw_evaluate
414            let (_, value) = transition.raw_evaluate(cx);
415            *transition.cached_value.borrow_mut() = Some(value);
416            assert!(transition.read_cache().is_some());
417
418            // Reset
419            transition.reset(cx);
420
421            // Cache should be cleared
422            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            // Custom easing that always returns 0.5
430            let transition =
431                create_transition(cx, Duration::from_millis(300), 0.0_f32).with_easing(|_| 0.5);
432
433            // Update goal to trigger animation
434            transition.update(cx, |val, _cx| {
435                *val = 100.0;
436            });
437
438            // With our custom easing, the delta should be 0.5
439            // So the value should be lerp(0, 100, 0.5) = 50
440            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            // By default, transitions are continuous
483            let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32);
484
485            // First update
486            transition.update(cx, |val, _cx| {
487                *val = 50.0;
488            });
489
490            // Second update (in continuous mode, this should work from current interpolated position)
491            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            // Update the goal
507            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            // Without any update, the transition should be "complete" (delta = 1.0)
521            // because it starts in completed state
522            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            // Immediately after update, delta should be close to 0
537            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            // Both should reference the same entity
553            assert_eq!(transition.entity_id(), cloned.entity_id());
554
555            // Updating one should affect the other
556            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); // Always return 0.5 for deterministic testing
569
570            transition.update(cx, |val, _cx| {
571                *val = 100.0;
572            });
573
574            // First evaluation using raw_evaluate
575            let (_, value1) = transition.raw_evaluate(cx);
576
577            // Second evaluation should return the same value
578            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            // Update only transition_a
591            transition_a.update(cx, |val, _cx| {
592                *val = 50.0;
593            });
594
595            // transition_b should remain unchanged
596            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            // Rapidly update multiple times
624            for i in 1..=10 {
625                transition.update(cx, |val, _cx| {
626                    *val = i as f32 * 10.0;
627                });
628            }
629
630            // Final goal should be 100.0
631            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            // Immediately after update, the transition should be in progress
645            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            // Without any update, the transition starts as "complete"
659            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            // Test with different easing values
672            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        // last_delta should be 1.0 indicating the transition is "complete"
745        assert_eq!(state.last_delta, 1.0);
746    }
747}