Skip to main content

i_slint_core/properties/
properties_animations.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4use super::*;
5use crate::{
6    animations::simulations::{
7        Parameter, Simulation,
8        spring::{SpringDurationBounceParameters, SpringParameters, SpringRegime},
9    },
10    items::{AnimationDirection, PropertyAnimation},
11    lengths::LogicalLength,
12};
13use euclid::Length;
14#[cfg(not(feature = "std"))]
15use num_traits::Float;
16
17enum AnimationState {
18    /// The animation will start after the delay is finished
19    Delaying,
20    /// Actual animation
21    Animating {
22        current_iteration: u64,
23    },
24    Done {
25        iteration_count: u64,
26    },
27}
28
29pub(super) struct PropertyPhysicsAnimationData<S> {
30    simulation: S,
31    state: AnimationState,
32}
33
34impl<S> PropertyPhysicsAnimationData<S>
35where
36    S: Simulation,
37{
38    pub fn new(simulation: S) -> PropertyPhysicsAnimationData<S> {
39        PropertyPhysicsAnimationData { simulation, state: AnimationState::Delaying }
40    }
41
42    /// Single iteration of the animation
43    pub fn update_value(&mut self, target: &mut crate::Coord) -> bool {
44        match self.state {
45            AnimationState::Delaying => {
46                // Decide on next state:
47                self.state = AnimationState::Animating { current_iteration: 0 };
48                self.update_value(target)
49            }
50            AnimationState::Animating { current_iteration: _ } => {
51                // TODO: Pass in Coord directly?
52                let mut value: f32 = *target as f32;
53                let finished = self.simulation.step(&mut value, crate::animations::current_tick());
54                *target = value as crate::Coord;
55                if finished {
56                    self.state = AnimationState::Done { iteration_count: 0 };
57                    true
58                } else {
59                    false
60                }
61            }
62            AnimationState::Done { iteration_count: _ } => true,
63        }
64    }
65}
66
67pub(super) struct PropertyValueAnimationData<T> {
68    from_value: T,
69    to_value: Option<T>,
70    details: PropertyAnimation,
71    start_time: crate::animations::Instant,
72    state: AnimationState,
73    /// Applied to every interpolated value before it is stored. Lets a
74    /// type-erased property (the interpreter's `Property<Value>`) reproduce
75    /// the interpolation of the erased type, e.g. rounding for `int`.
76    map: Option<fn(T) -> T>,
77    spring: Option<SpringRegime>,
78    /// Whether the final iteration's spring has already been re-damped
79    spring_settle_clamped: bool,
80}
81
82impl<T: InterpolatedPropertyValue + Clone> PropertyValueAnimationData<T> {
83    pub fn new(from_value: T, to_value: Option<T>, details: PropertyAnimation) -> Self {
84        Self::new_with_velocity(from_value, to_value, details, 0.0)
85    }
86
87    /// Used to carry velocity over across a retarget.
88    pub fn new_with_velocity(
89        from_value: T,
90        to_value: Option<T>,
91        details: PropertyAnimation,
92        initial_velocity: f32,
93    ) -> Self {
94        let start_time = crate::animations::current_tick();
95        let spring = Self::compute_spring(&details, &from_value, &to_value, initial_velocity);
96        Self {
97            from_value,
98            to_value,
99            details,
100            start_time,
101            state: AnimationState::Delaying,
102            map: None,
103            spring,
104            spring_settle_clamped: false,
105        }
106    }
107
108    /// A spring with duration <= 0 (and no mass/stiffness/damping override) can't be simulated
109    fn compute_spring(
110        details: &PropertyAnimation,
111        from_value: &T,
112        to_value: &Option<T>,
113        initial_velocity: f32,
114    ) -> Option<SpringRegime> {
115        matches!(details.easing, crate::animations::EasingCurve::Spring(_))
116            .then(|| {
117                let crate::animations::EasingCurve::Spring(bounce) = details.easing else {
118                    return None;
119                };
120                let (w_n, zeta) = if details.duration > 0 {
121                    Some(
122                        SpringDurationBounceParameters::new(
123                            details.duration as f32 / 1000.0,
124                            bounce,
125                        )
126                        .to_natural_frequency_and_damping_ratio(),
127                    )
128                } else {
129                    None
130                }?;
131
132                // -1 so that the spring knows to go to 0; re-express the carried-over velocity
133                // (in property units/sec) in the spring's -1..=0-relative units.
134                let delta = to_value.as_ref().map_or(0.0, |tv| from_value.scalar_delta(tv));
135                let v0 = if delta != 0.0 { initial_velocity / delta } else { 0.0 };
136                Some(SpringRegime::new(-1.0, v0, w_n, zeta))
137            })
138            .flatten()
139    }
140
141    pub fn with_map(mut self, map: fn(T) -> T) -> Self {
142        self.map = Some(map);
143        self
144    }
145
146    fn apply_map(&self, value: T) -> T {
147        match self.map {
148            Some(map) => map(value),
149            None => value,
150        }
151    }
152
153    /// The current velocity (in property units per second) of a live spring animation
154    fn current_velocity(&self) -> Option<f32> {
155        if !matches!(self.state, AnimationState::Animating { .. }) {
156            return None;
157        }
158        let spring = self.spring.as_ref()?;
159        let elapsed_secs =
160            crate::animations::current_tick().duration_since(self.start_time).as_millis() as f32
161                / 1000.0;
162        let (_, rel_vel) = spring.evaluate(elapsed_secs);
163        let to_value = self.to_value.as_ref().expect("The animation should have a to_value");
164        Some(rel_vel * self.from_value.scalar_delta(to_value))
165    }
166
167    /// Single iteration of the animation
168    pub fn compute_interpolated_value(&mut self) -> (T, bool) {
169        // If animation is disabled, immediately return the target value
170        let to_value = self.to_value.clone().expect("The animation should have a to_value");
171        if !self.details.enabled {
172            return (self.apply_map(to_value), true);
173        }
174
175        let new_tick = crate::animations::current_tick();
176        let mut time_progress = new_tick.duration_since(self.start_time).as_millis() as u64;
177        let reversed = |iteration: u64| -> bool {
178            #[allow(clippy::manual_is_multiple_of)] // keep symmetry
179            match self.details.direction {
180                AnimationDirection::Normal => false,
181                AnimationDirection::Reverse => true,
182                AnimationDirection::Alternate => iteration % 2 == 1,
183                AnimationDirection::AlternateReverse => iteration % 2 == 0,
184            }
185        };
186
187        match self.state {
188            AnimationState::Delaying => {
189                if self.details.delay <= 0 {
190                    self.state = AnimationState::Animating { current_iteration: 0 };
191                    return self.compute_interpolated_value();
192                }
193
194                let delay = self.details.delay as u64;
195
196                if time_progress < delay {
197                    if reversed(0) {
198                        (self.apply_map(to_value), false)
199                    } else {
200                        (self.apply_map(self.from_value.clone()), false)
201                    }
202                } else {
203                    self.start_time =
204                        new_tick - core::time::Duration::from_millis(time_progress - delay);
205
206                    // Decide on next state:
207                    self.state = AnimationState::Animating { current_iteration: 0 };
208                    self.compute_interpolated_value()
209                }
210            }
211            AnimationState::Animating { current_iteration } => {
212                // A spring runs in real time and ends only once it settles.
213                if matches!(self.details.easing, crate::animations::EasingCurve::Spring(_)) {
214                    if self.details.iteration_count == 0. {
215                        self.state = AnimationState::Done { iteration_count: 0 };
216                        return self.compute_interpolated_value();
217                    }
218                    return if let Some(spring) = self.spring.as_ref() {
219                        let next_iteration = current_iteration + 1;
220                        let has_more_iterations = self.details.iteration_count < 0.
221                            || (next_iteration as f64) < self.details.iteration_count as f64;
222                        let duration_ms = self.details.duration as u64;
223
224                        if has_more_iterations && time_progress >= duration_ms {
225                            // Bounce into the next iteration at `duration` rather than waiting to
226                            // settle. Velocity always carries over; position only carries over on
227                            // a direction flip (e.g. `alternate`), to stay continuous instead of
228                            // snapping. Otherwise it resets to the start, like a repeating easing
229                            // curve.
230                            let duration_secs = duration_ms as f32 / 1000.0;
231                            let (rel_pos, rel_vel) = spring.evaluate(duration_secs);
232                            let crate::animations::EasingCurve::Spring(bounce) =
233                                self.details.easing
234                            else {
235                                unreachable!()
236                            };
237                            let (w_n, zeta) =
238                                SpringDurationBounceParameters::new(duration_secs, bounce)
239                                    .to_natural_frequency_and_damping_ratio();
240                            let x0 = if reversed(current_iteration) != reversed(next_iteration) {
241                                -(1.0 + rel_pos)
242                            } else {
243                                -1.0
244                            };
245                            self.spring = Some(SpringRegime::new(x0, rel_vel, w_n, zeta));
246                            self.start_time += core::time::Duration::from_millis(duration_ms);
247                            self.state =
248                                AnimationState::Animating { current_iteration: next_iteration };
249                            self.compute_interpolated_value()
250                        } else {
251                            let elapsed_secs = time_progress as f32 / 1000.0;
252                            let (t, settled) =
253                                crate::animations::spring_settle_progress(spring, elapsed_secs);
254                            if !settled
255                                && !self.spring_settle_clamped
256                                && time_progress >= duration_ms
257                            {
258                                // Hasn't settled by the end of `duration`: re-damp the tail (see
259                                // `spring_settle_within`) so it's guaranteed to settle within a
260                                // further 9 multiples of `duration`, carrying over position/velocity.
261                                self.spring_settle_clamped = true;
262                                let duration_secs = duration_ms as f32 / 1000.0;
263                                let w_n = 2.0 * core::f32::consts::PI / duration_secs;
264                                let settled_regime = crate::animations::spring_settle_within(
265                                    spring,
266                                    duration_secs,
267                                    w_n,
268                                );
269                                self.spring = Some(settled_regime);
270                                self.start_time += core::time::Duration::from_millis(duration_ms);
271                                return self.compute_interpolated_value();
272                            }
273                            if settled {
274                                self.state = if has_more_iterations {
275                                    self.start_time = new_tick;
276                                    AnimationState::Animating { current_iteration: next_iteration }
277                                } else {
278                                    AnimationState::Done { iteration_count: current_iteration }
279                                };
280                                self.compute_interpolated_value()
281                            } else {
282                                let progress = if reversed(current_iteration) { 1. - t } else { t };
283                                let val = self.from_value.interpolate(&to_value, progress);
284                                (self.apply_map(val), false)
285                            }
286                        }
287                    } else {
288                        self.state = AnimationState::Done { iteration_count: 0 };
289                        self.compute_interpolated_value()
290                    };
291                }
292                let mut current_iteration = current_iteration;
293
294                if self.details.duration <= 0 || self.details.iteration_count == 0. {
295                    self.state = AnimationState::Done { iteration_count: 0 };
296                    return self.compute_interpolated_value();
297                }
298
299                let duration = self.details.duration as u64;
300                if time_progress >= duration {
301                    // wrap around
302                    current_iteration += time_progress / duration;
303                    time_progress %= duration;
304                    self.start_time = new_tick - core::time::Duration::from_millis(time_progress);
305                }
306
307                if (self.details.iteration_count < 0.)
308                    || (((current_iteration * duration) + time_progress) as f64)
309                        < ((self.details.iteration_count as f64) * (duration as f64))
310                {
311                    self.state = AnimationState::Animating { current_iteration };
312
313                    let progress = {
314                        let progress =
315                            (time_progress as f32 / self.details.duration as f32).clamp(0., 1.);
316                        if reversed(current_iteration) { 1. - progress } else { progress }
317                    };
318                    let t = crate::animations::easing_curve(&self.details.easing, progress);
319                    let val = self.from_value.interpolate(&to_value, t);
320
321                    (self.apply_map(val), false)
322                } else {
323                    self.state =
324                        AnimationState::Done { iteration_count: current_iteration.max(1) - 1 };
325                    self.compute_interpolated_value()
326                }
327            }
328            AnimationState::Done { iteration_count } => {
329                if reversed(iteration_count) {
330                    (self.apply_map(self.from_value.clone()), true)
331                } else {
332                    (self.apply_map(to_value), true)
333                }
334            }
335        }
336    }
337}
338
339#[derive(Clone, Copy, Eq, PartialEq, Debug)]
340pub(super) enum AnimatedBindingState {
341    Animating,
342    NotAnimating,
343    ShouldStart,
344}
345
346#[pin_project::pin_project]
347pub(super) struct AnimatedBindingCallable<T, A> {
348    #[pin]
349    pub(super) original_binding: PropertyHandle,
350    pub(super) state: Cell<AnimatedBindingState>,
351    pub(super) animation_data: RefCell<PropertyValueAnimationData<T>>,
352    pub(super) compute_animation_details: A,
353    /// Tick captured by `mark_dirty`
354    pub(super) dirty_time: Cell<crate::animations::Instant>,
355    pub(crate) carried_velocity: Cell<f32>,
356}
357
358pub(super) type AnimationDetail = (PropertyAnimation, Option<crate::animations::Instant>);
359
360unsafe impl<T: InterpolatedPropertyValue + Clone, A: Fn() -> AnimationDetail> BindingCallable<T>
361    for AnimatedBindingCallable<T, A>
362{
363    fn evaluate(self: Pin<&Self>, value: &mut T) -> BindingResult {
364        let original_binding = self.project_ref().original_binding;
365        original_binding.register_as_dependency_to_current_binding(
366            #[cfg(slint_debug_property)]
367            "<AnimatedBindingCallable>",
368        );
369        match self.state.get() {
370            AnimatedBindingState::Animating => {
371                let (val, finished) = self.animation_data.borrow_mut().compute_interpolated_value();
372                *value = val;
373                if finished {
374                    self.state.set(AnimatedBindingState::NotAnimating)
375                } else {
376                    crate::animations::CURRENT_ANIMATION_DRIVER
377                        .with(|driver| driver.set_has_active_animations());
378                }
379            }
380            AnimatedBindingState::NotAnimating => {
381                // Safety: `value` is a valid mutable reference
382                unsafe { self.original_binding.update(value as *mut T) };
383            }
384            AnimatedBindingState::ShouldStart => {
385                let mut animation_data = self.animation_data.borrow_mut();
386
387                // Since `mark_dirty` fires when dependencies of `original_binding` changes
388                // if the change doesn't actually affect the computed value, it shouldn't restart
389                // the animation
390                let previous_to_value = animation_data.to_value.clone();
391                let mut new_to_value = T::default();
392                // Safety: `new_to_value` is a valid mutable reference matching the
393                // original binding's value type
394                unsafe { self.original_binding.update(&mut new_to_value as *mut T) };
395                animation_data.to_value = Some(new_to_value);
396
397                if animation_data.to_value != previous_to_value {
398                    animation_data.state = AnimationState::Delaying;
399                    // Anchor timing to when the change was first signalled
400                    animation_data.start_time = self.dirty_time.get();
401                    // animation_data.details.iteration_count = 1.;
402                    animation_data.from_value = value.clone();
403                    let (details, start_time) = (self.compute_animation_details)();
404                    if let Some(start_time) = start_time {
405                        animation_data.start_time = start_time;
406                    }
407                    animation_data.details = details;
408                    animation_data.spring = PropertyValueAnimationData::<T>::compute_spring(
409                        &animation_data.details,
410                        &animation_data.from_value,
411                        &animation_data.to_value,
412                        self.carried_velocity.get(),
413                    );
414                    animation_data.spring_settle_clamped = false;
415                }
416
417                self.state.set(AnimatedBindingState::Animating);
418                let (val, finished) = animation_data.compute_interpolated_value();
419                *value = val;
420                if finished {
421                    self.state.set(AnimatedBindingState::NotAnimating)
422                } else {
423                    crate::animations::CURRENT_ANIMATION_DRIVER
424                        .with(|driver| driver.set_has_active_animations());
425                }
426            }
427        };
428        BindingResult::KeepBinding
429    }
430    fn mark_dirty(self: Pin<&Self>) {
431        if self.state.get() == AnimatedBindingState::ShouldStart {
432            return;
433        }
434        let original_dirty = self.original_binding.access(|b| b.unwrap().dirty.get());
435        if original_dirty {
436            self.carried_velocity
437                .set(self.animation_data.borrow().current_velocity().unwrap_or(0.0));
438            self.state.set(AnimatedBindingState::ShouldStart);
439            self.dirty_time.set(crate::animations::current_tick());
440        }
441    }
442
443    fn velocity(self: Pin<&Self>) -> Option<f32> {
444        self.animation_data.borrow().current_velocity()
445    }
446}
447
448/// InterpolatedPropertyValue is a trait used to enable properties to be used with
449/// animations that interpolate values. The basic requirement is the ability to apply
450/// a progress that's typically between 0 and 1 to a range.
451pub trait InterpolatedPropertyValue: PartialEq + Default + 'static {
452    /// Returns the interpolated value between self and target_value according to the
453    /// progress parameter t that's usually between 0 and 1. With certain animation
454    /// easing curves it may over- or undershoot though.
455    #[must_use]
456    fn interpolate(&self, target_value: &Self, t: f32) -> Self;
457
458    /// Returns `target_value - self` as a scalar.
459    /// Types with no natural single-scalar notion of velocity keep the default `0.0`
460    fn scalar_delta(&self, _target_value: &Self) -> f32 {
461        0.0
462    }
463}
464
465impl InterpolatedPropertyValue for f32 {
466    fn interpolate(&self, target_value: &Self, t: f32) -> Self {
467        self + t * (target_value - self)
468    }
469
470    fn scalar_delta(&self, target_value: &Self) -> f32 {
471        target_value - self
472    }
473}
474
475impl InterpolatedPropertyValue for i32 {
476    fn interpolate(&self, target_value: &Self, t: f32) -> Self {
477        self + (t * (target_value - self) as f32).round() as i32
478    }
479
480    fn scalar_delta(&self, target_value: &Self) -> f32 {
481        (target_value - self) as f32
482    }
483}
484
485impl InterpolatedPropertyValue for i64 {
486    fn interpolate(&self, target_value: &Self, t: f32) -> Self {
487        self + (t * (target_value - self) as f32).round() as Self
488    }
489
490    fn scalar_delta(&self, target_value: &Self) -> f32 {
491        (target_value - self) as f32
492    }
493}
494
495impl InterpolatedPropertyValue for u8 {
496    fn interpolate(&self, target_value: &Self, t: f32) -> Self {
497        ((*self as f32) + (t * ((*target_value as f32) - (*self as f32)))).round().clamp(0., 255.)
498            as u8
499    }
500
501    fn scalar_delta(&self, target_value: &Self) -> f32 {
502        (*target_value as f32) - (*self as f32)
503    }
504}
505
506impl InterpolatedPropertyValue for LogicalLength {
507    fn interpolate(&self, target_value: &Self, t: f32) -> Self {
508        LogicalLength::new(self.get().interpolate(&target_value.get(), t))
509    }
510
511    fn scalar_delta(&self, target_value: &Self) -> f32 {
512        (target_value.get() - self.get()) as f32
513    }
514}
515
516/// Binding installed by `Property::set_animated_value`.
517/// A type so a retarget can report the current velocity
518struct AnimatedValueBinding<T> {
519    animation_data: RefCell<PropertyValueAnimationData<T>>,
520}
521
522unsafe impl<T: InterpolatedPropertyValue + Clone + 'static> BindingCallable<T>
523    for AnimatedValueBinding<T>
524{
525    fn evaluate(self: Pin<&Self>, value: &mut T) -> BindingResult {
526        let (val, finished) = self.animation_data.borrow_mut().compute_interpolated_value();
527        *value = val;
528        if finished {
529            BindingResult::RemoveBinding
530        } else {
531            crate::animations::CURRENT_ANIMATION_DRIVER
532                .with(|driver| driver.set_has_active_animations());
533            BindingResult::KeepBinding
534        }
535    }
536
537    fn velocity(self: Pin<&Self>) -> Option<f32> {
538        self.animation_data.borrow().current_velocity()
539    }
540}
541
542impl<T: Clone + InterpolatedPropertyValue + 'static> Property<T> {
543    /// Evaluate the property and remove the (animation) binding of this property.
544    ///
545    /// Note that a binding can intercept this via intercept_set_binding and still remain on the property.
546    /// (e.g. two-way-bindings will not be removed with this call!)
547    pub fn remove_binding(self: Pin<&Self>) {
548        // FIXME: This is a bit of a hack, set_animated_value will call set_binding on the internal handle,
549        // which will call intercept_set_binding, which will check if the binding should be removed or not.
550        // In the case of two-way bindings, we want to keep the binding, but reset the value to the current one,
551        // so that any animation binding is removed, but the two-way-binding is kept.
552        self.set_animated_value(self.get(), PropertyAnimation::default());
553    }
554
555    /// Change the value of this property, by animating (interpolating) from the current property's value
556    /// to the specified parameter value. The animation is done according to the parameters described by
557    /// the PropertyAnimation object.
558    ///
559    /// If other properties have binding depending of this property, these properties will
560    /// be marked as dirty.
561    pub fn set_animated_value(self: Pin<&Self>, value: T, animation_data: PropertyAnimation) {
562        self.set_animated_value_impl(value, animation_data, None)
563    }
564
565    /// Like [`Self::set_animated_value`], but passes every interpolated value through `map`
566    /// before storing it, so a type-erased property can reproduce the interpolation
567    /// of the erased type (e.g. rounding for `int` properties).
568    pub fn set_animated_value_with_map(
569        self: Pin<&Self>,
570        value: T,
571        animation_data: PropertyAnimation,
572        map: fn(T) -> T,
573    ) {
574        self.set_animated_value_impl(value, animation_data, Some(map))
575    }
576
577    fn set_animated_value_impl(
578        self: Pin<&Self>,
579        value: T,
580        animation_data: PropertyAnimation,
581        map: Option<fn(T) -> T>,
582    ) {
583        // Carry over the outgoing binding's velocity
584        let carried_velocity = self.handle.current_velocity().unwrap_or(0.0);
585        let mut d = properties_animations::PropertyValueAnimationData::new_with_velocity(
586            self.get(),
587            Some(value),
588            animation_data,
589            carried_velocity,
590        );
591        if let Some(map) = map {
592            d = d.with_map(map);
593        }
594        let binding =
595            properties_animations::AnimatedValueBinding { animation_data: RefCell::new(d) };
596        // Safety: the BindingCallable will cast its argument to T
597        unsafe {
598            self.handle.set_binding(
599                binding,
600                #[cfg(slint_debug_property)]
601                self.debug_name.borrow().as_str(),
602            );
603        }
604        self.handle.mark_dirty(
605            #[cfg(slint_debug_property)]
606            self.debug_name.borrow().as_str(),
607        );
608    }
609
610    /// Set a binding to this property, providing a callback for the animation and an optional
611    /// start_time (relevant for state transitions).
612    pub fn set_animated_binding(
613        &self,
614        binding: impl Binding<T> + 'static,
615        compute_animation_details: impl Fn() -> (PropertyAnimation, Option<crate::animations::Instant>)
616        + 'static,
617    ) {
618        self.set_animated_binding_impl(binding, compute_animation_details, None)
619    }
620
621    /// Like [`Self::set_animated_binding`], but passes every interpolated value through `map`
622    /// before storing it, so a type-erased property can reproduce the interpolation
623    /// of the erased type (e.g. rounding for `int` properties).
624    pub fn set_animated_binding_with_map(
625        &self,
626        binding: impl Binding<T> + 'static,
627        compute_animation_details: impl Fn() -> (PropertyAnimation, Option<crate::animations::Instant>)
628        + 'static,
629        map: fn(T) -> T,
630    ) {
631        self.set_animated_binding_impl(binding, compute_animation_details, Some(map))
632    }
633
634    fn set_animated_binding_impl(
635        &self,
636        binding: impl Binding<T> + 'static,
637        compute_animation_details: impl Fn() -> (PropertyAnimation, Option<crate::animations::Instant>)
638        + 'static,
639        map: Option<fn(T) -> T>,
640    ) {
641        let mut animation_data = properties_animations::PropertyValueAnimationData::new(
642            T::default(),
643            None,
644            PropertyAnimation::default(),
645        );
646        if let Some(map) = map {
647            animation_data = animation_data.with_map(map);
648        }
649        let binding_callable = properties_animations::AnimatedBindingCallable::<T, _> {
650            original_binding: PropertyHandle {
651                handle: Cell::new(
652                    (alloc_binding_holder(move |val: &mut T| {
653                        *val = binding.evaluate(val);
654                        BindingResult::KeepBinding
655                    }) as *mut ())
656                        .map_addr(|a| a | 0b10),
657                ),
658            },
659            state: Cell::new(properties_animations::AnimatedBindingState::NotAnimating),
660            animation_data: RefCell::new(animation_data),
661            compute_animation_details,
662            dirty_time: Cell::new(crate::animations::current_tick()),
663            carried_velocity: Cell::new(0.0),
664        };
665
666        // Safety: the `AnimatedBindingCallable`'s type match the property type
667        unsafe {
668            self.handle.set_binding(
669                binding_callable,
670                #[cfg(slint_debug_property)]
671                self.debug_name.borrow().as_str(),
672            )
673        };
674        self.handle.mark_dirty(
675            #[cfg(slint_debug_property)]
676            self.debug_name.borrow().as_str(),
677        );
678    }
679}
680
681unsafe impl<Unit, S: Simulation> BindingCallable<Length<crate::Coord, Unit>>
682    for RefCell<PropertyPhysicsAnimationData<S>>
683{
684    fn evaluate(self: Pin<&Self>, value: &mut Length<crate::Coord, Unit>) -> BindingResult {
685        let finished = self.borrow_mut().update_value(&mut value.0);
686        if finished {
687            BindingResult::RemoveBinding
688        } else {
689            crate::animations::CURRENT_ANIMATION_DRIVER
690                .with(|driver| driver.set_has_active_animations());
691            BindingResult::KeepBinding
692        }
693    }
694
695    // This binding should not be removed if the value is updated externally.
696    fn intercept_set(self: Pin<&Self>, _value: &Length<crate::Coord, Unit>) -> bool {
697        true
698    }
699}
700
701impl<Unit> Property<Length<crate::Coord, Unit>> {
702    /// Change the value by using a physics animation
703    pub fn set_physic_animation_value<S: Simulation + 'static, AD: Parameter<Output = S>>(
704        &self,
705        limit_value: Pin<Box<Property<f32>>>,
706        simulation_data: AD,
707    ) {
708        // Safety: the BindingCallable will cast its argument to T
709        unsafe {
710            self.handle.set_binding::<Length<crate::Coord, Unit>, core::cell::RefCell<PropertyPhysicsAnimationData<S>>>(RefCell::new(PropertyPhysicsAnimationData::new(
711                    simulation_data.simulation(self.get_internal().0 as f32, limit_value),
712                )),
713                #[cfg(slint_debug_property)]
714                self.debug_name.borrow().as_str()
715            );
716        }
717        self.handle.mark_dirty(
718            #[cfg(slint_debug_property)]
719            self.debug_name.borrow().as_str(),
720        );
721    }
722}
723
724#[cfg(test)]
725mod animation_tests {
726    use super::*;
727    use pin_weak::rc::PinWeak;
728    use std::rc::Rc;
729
730    #[derive(Default)]
731    struct Component {
732        width: Property<i32>,
733        width_times_two: Property<i32>,
734        feed_property: Property<i32>, // used by binding to feed values into width
735    }
736
737    impl Component {
738        fn new_test_component() -> Pin<Rc<Self>> {
739            let compo = Rc::pin(Component::default());
740            let w = PinWeak::downgrade(compo.clone());
741            compo.width_times_two.set_binding(move || {
742                let compo = w.upgrade().unwrap();
743                get_prop_value(&compo.width) * 2
744            });
745
746            compo
747        }
748    }
749
750    const DURATION: std::time::Duration = std::time::Duration::from_millis(10000);
751    const DELAY: std::time::Duration = std::time::Duration::from_millis(800);
752
753    // Helper just for testing
754    fn get_prop_value<T: Clone>(prop: &Property<T>) -> T {
755        unsafe { Pin::new_unchecked(prop).get() }
756    }
757
758    // Helper just for testing: the property lives in a pinned `Rc<Component>`.
759    fn set_animated_value<T: Clone + InterpolatedPropertyValue + 'static>(
760        prop: &Property<T>,
761        value: T,
762        animation_data: PropertyAnimation,
763    ) {
764        unsafe { Pin::new_unchecked(prop) }.set_animated_value(value, animation_data);
765    }
766
767    #[test]
768    fn properties_test_animation_negative_delay_triggered_by_set() {
769        let compo = Component::new_test_component();
770
771        let animation_details = PropertyAnimation {
772            delay: -25,
773            duration: DURATION.as_millis() as _,
774            iteration_count: 1.,
775            ..PropertyAnimation::default()
776        };
777
778        compo.width.set(100);
779        assert_eq!(get_prop_value(&compo.width), 100);
780        assert_eq!(get_prop_value(&compo.width_times_two), 200);
781
782        let start_time = crate::animations::current_tick();
783
784        set_animated_value(&compo.width, 200, animation_details);
785        assert_eq!(get_prop_value(&compo.width), 100);
786        assert_eq!(get_prop_value(&compo.width_times_two), 200);
787
788        crate::animations::CURRENT_ANIMATION_DRIVER
789            .with(|driver| driver.update_animations(start_time + DURATION / 2));
790        assert_eq!(get_prop_value(&compo.width), 150);
791        assert_eq!(get_prop_value(&compo.width_times_two), 300);
792
793        crate::animations::CURRENT_ANIMATION_DRIVER
794            .with(|driver| driver.update_animations(start_time + DURATION));
795        assert_eq!(get_prop_value(&compo.width), 200);
796        assert_eq!(get_prop_value(&compo.width_times_two), 400);
797
798        // Overshoot: Always to_value.
799        crate::animations::CURRENT_ANIMATION_DRIVER
800            .with(|driver| driver.update_animations(start_time + DURATION + DURATION / 2));
801        assert_eq!(get_prop_value(&compo.width), 200);
802        assert_eq!(get_prop_value(&compo.width_times_two), 400);
803
804        // the binding should be removed
805        compo.width.handle.access(|binding| assert!(binding.is_none()));
806    }
807
808    #[test]
809    fn properties_test_animation_triggered_by_set() {
810        let compo = Component::new_test_component();
811
812        let animation_details = PropertyAnimation {
813            duration: DURATION.as_millis() as _,
814            iteration_count: 1.,
815            ..PropertyAnimation::default()
816        };
817
818        compo.width.set(100);
819        assert_eq!(get_prop_value(&compo.width), 100);
820        assert_eq!(get_prop_value(&compo.width_times_two), 200);
821
822        let start_time = crate::animations::current_tick();
823
824        set_animated_value(&compo.width, 200, animation_details);
825        assert_eq!(get_prop_value(&compo.width), 100);
826        assert_eq!(get_prop_value(&compo.width_times_two), 200);
827
828        crate::animations::CURRENT_ANIMATION_DRIVER
829            .with(|driver| driver.update_animations(start_time + DURATION / 2));
830        assert_eq!(get_prop_value(&compo.width), 150);
831        assert_eq!(get_prop_value(&compo.width_times_two), 300);
832
833        crate::animations::CURRENT_ANIMATION_DRIVER
834            .with(|driver| driver.update_animations(start_time + DURATION));
835        assert_eq!(get_prop_value(&compo.width), 200);
836        assert_eq!(get_prop_value(&compo.width_times_two), 400);
837
838        // Overshoot: Always to_value.
839        crate::animations::CURRENT_ANIMATION_DRIVER
840            .with(|driver| driver.update_animations(start_time + DURATION + DURATION / 2));
841        assert_eq!(get_prop_value(&compo.width), 200);
842        assert_eq!(get_prop_value(&compo.width_times_two), 400);
843
844        // the binding should be removed
845        compo.width.handle.access(|binding| assert!(binding.is_none()));
846    }
847
848    #[test]
849    fn properties_test_delayed_animation_triggered_by_set() {
850        let compo = Component::new_test_component();
851
852        let animation_details = PropertyAnimation {
853            delay: DELAY.as_millis() as _,
854            iteration_count: 1.,
855            duration: DURATION.as_millis() as _,
856            ..PropertyAnimation::default()
857        };
858
859        compo.width.set(100);
860        assert_eq!(get_prop_value(&compo.width), 100);
861        assert_eq!(get_prop_value(&compo.width_times_two), 200);
862
863        let start_time = crate::animations::current_tick();
864
865        set_animated_value(&compo.width, 200, animation_details);
866        assert_eq!(get_prop_value(&compo.width), 100);
867        assert_eq!(get_prop_value(&compo.width_times_two), 200);
868
869        // In delay:
870        crate::animations::CURRENT_ANIMATION_DRIVER
871            .with(|driver| driver.update_animations(start_time + DELAY / 2));
872        assert_eq!(get_prop_value(&compo.width), 100);
873        assert_eq!(get_prop_value(&compo.width_times_two), 200);
874
875        // In animation:
876        crate::animations::CURRENT_ANIMATION_DRIVER
877            .with(|driver| driver.update_animations(start_time + DELAY));
878        assert_eq!(get_prop_value(&compo.width), 100);
879        assert_eq!(get_prop_value(&compo.width_times_two), 200);
880
881        crate::animations::CURRENT_ANIMATION_DRIVER
882            .with(|driver| driver.update_animations(start_time + DELAY + DURATION / 2));
883        assert_eq!(get_prop_value(&compo.width), 150);
884        assert_eq!(get_prop_value(&compo.width_times_two), 300);
885
886        crate::animations::CURRENT_ANIMATION_DRIVER
887            .with(|driver| driver.update_animations(start_time + DELAY + DURATION));
888        assert_eq!(get_prop_value(&compo.width), 200);
889        assert_eq!(get_prop_value(&compo.width_times_two), 400);
890
891        // Overshoot: Always to_value.
892        crate::animations::CURRENT_ANIMATION_DRIVER
893            .with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 2));
894        assert_eq!(get_prop_value(&compo.width), 200);
895        assert_eq!(get_prop_value(&compo.width_times_two), 400);
896
897        // the binding should be removed
898        compo.width.handle.access(|binding| assert!(binding.is_none()));
899    }
900
901    #[test]
902    fn properties_test_delayed_animation_fractal_iteration_triggered_by_set() {
903        let compo = Component::new_test_component();
904
905        let animation_details = PropertyAnimation {
906            delay: DELAY.as_millis() as _,
907            iteration_count: 1.5,
908            duration: DURATION.as_millis() as _,
909            ..PropertyAnimation::default()
910        };
911
912        compo.width.set(100);
913        assert_eq!(get_prop_value(&compo.width), 100);
914        assert_eq!(get_prop_value(&compo.width_times_two), 200);
915
916        let start_time = crate::animations::current_tick();
917
918        set_animated_value(&compo.width, 200, animation_details);
919        assert_eq!(get_prop_value(&compo.width), 100);
920        assert_eq!(get_prop_value(&compo.width_times_two), 200);
921
922        // In delay:
923        crate::animations::CURRENT_ANIMATION_DRIVER
924            .with(|driver| driver.update_animations(start_time + DELAY / 2));
925        assert_eq!(get_prop_value(&compo.width), 100);
926        assert_eq!(get_prop_value(&compo.width_times_two), 200);
927
928        // In animation:
929        crate::animations::CURRENT_ANIMATION_DRIVER
930            .with(|driver| driver.update_animations(start_time + DELAY));
931        assert_eq!(get_prop_value(&compo.width), 100);
932        assert_eq!(get_prop_value(&compo.width_times_two), 200);
933
934        crate::animations::CURRENT_ANIMATION_DRIVER
935            .with(|driver| driver.update_animations(start_time + DELAY + DURATION / 2));
936        assert_eq!(get_prop_value(&compo.width), 150);
937        assert_eq!(get_prop_value(&compo.width_times_two), 300);
938
939        crate::animations::CURRENT_ANIMATION_DRIVER
940            .with(|driver| driver.update_animations(start_time + DELAY + DURATION));
941        assert_eq!(get_prop_value(&compo.width), 100);
942        assert_eq!(get_prop_value(&compo.width_times_two), 200);
943
944        // (fractal) end of animation
945        crate::animations::CURRENT_ANIMATION_DRIVER
946            .with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 4));
947        assert_eq!(get_prop_value(&compo.width), 125);
948        assert_eq!(get_prop_value(&compo.width_times_two), 250);
949
950        // End of animation:
951        crate::animations::CURRENT_ANIMATION_DRIVER
952            .with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 2));
953        assert_eq!(get_prop_value(&compo.width), 200);
954        assert_eq!(get_prop_value(&compo.width_times_two), 400);
955
956        // the binding should be removed
957        compo.width.handle.access(|binding| assert!(binding.is_none()));
958    }
959    #[test]
960    fn properties_test_delayed_animation_null_duration_triggered_by_set() {
961        let compo = Component::new_test_component();
962
963        let animation_details = PropertyAnimation {
964            delay: DELAY.as_millis() as _,
965            iteration_count: 1.0,
966            duration: 0,
967            ..PropertyAnimation::default()
968        };
969
970        compo.width.set(100);
971        assert_eq!(get_prop_value(&compo.width), 100);
972        assert_eq!(get_prop_value(&compo.width_times_two), 200);
973
974        let start_time = crate::animations::current_tick();
975
976        set_animated_value(&compo.width, 200, animation_details);
977        assert_eq!(get_prop_value(&compo.width), 100);
978        assert_eq!(get_prop_value(&compo.width_times_two), 200);
979
980        // In delay:
981        crate::animations::CURRENT_ANIMATION_DRIVER
982            .with(|driver| driver.update_animations(start_time + DELAY / 2));
983        assert_eq!(get_prop_value(&compo.width), 100);
984        assert_eq!(get_prop_value(&compo.width_times_two), 200);
985
986        // No animation:
987        crate::animations::CURRENT_ANIMATION_DRIVER
988            .with(|driver| driver.update_animations(start_time + DELAY));
989        assert_eq!(get_prop_value(&compo.width), 200);
990        assert_eq!(get_prop_value(&compo.width_times_two), 400);
991
992        // Overshoot: Always to_value.
993        crate::animations::CURRENT_ANIMATION_DRIVER
994            .with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 2));
995        assert_eq!(get_prop_value(&compo.width), 200);
996        assert_eq!(get_prop_value(&compo.width_times_two), 400);
997
998        // the binding should be removed
999        compo.width.handle.access(|binding| assert!(binding.is_none()));
1000    }
1001
1002    #[test]
1003    fn properties_test_delayed_animation_negative_duration_triggered_by_set() {
1004        let compo = Component::new_test_component();
1005
1006        let animation_details = PropertyAnimation {
1007            delay: DELAY.as_millis() as _,
1008            iteration_count: 1.0,
1009            duration: -25,
1010            ..PropertyAnimation::default()
1011        };
1012
1013        compo.width.set(100);
1014        assert_eq!(get_prop_value(&compo.width), 100);
1015        assert_eq!(get_prop_value(&compo.width_times_two), 200);
1016
1017        let start_time = crate::animations::current_tick();
1018
1019        set_animated_value(&compo.width, 200, animation_details);
1020        assert_eq!(get_prop_value(&compo.width), 100);
1021        assert_eq!(get_prop_value(&compo.width_times_two), 200);
1022
1023        // In delay:
1024        crate::animations::CURRENT_ANIMATION_DRIVER
1025            .with(|driver| driver.update_animations(start_time + DELAY / 2));
1026        assert_eq!(get_prop_value(&compo.width), 100);
1027        assert_eq!(get_prop_value(&compo.width_times_two), 200);
1028
1029        // No animation:
1030        crate::animations::CURRENT_ANIMATION_DRIVER
1031            .with(|driver| driver.update_animations(start_time + DELAY));
1032        assert_eq!(get_prop_value(&compo.width), 200);
1033        assert_eq!(get_prop_value(&compo.width_times_two), 400);
1034
1035        // Overshoot: Always to_value.
1036        crate::animations::CURRENT_ANIMATION_DRIVER
1037            .with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 2));
1038        assert_eq!(get_prop_value(&compo.width), 200);
1039        assert_eq!(get_prop_value(&compo.width_times_two), 400);
1040
1041        // the binding should be removed
1042        compo.width.handle.access(|binding| assert!(binding.is_none()));
1043    }
1044
1045    #[test]
1046    fn properties_test_delayed_animation_no_iteration_triggered_by_set() {
1047        let compo = Component::new_test_component();
1048
1049        let animation_details = PropertyAnimation {
1050            delay: DELAY.as_millis() as _,
1051            iteration_count: 0.0,
1052            duration: DURATION.as_millis() as _,
1053            ..PropertyAnimation::default()
1054        };
1055
1056        compo.width.set(100);
1057        assert_eq!(get_prop_value(&compo.width), 100);
1058        assert_eq!(get_prop_value(&compo.width_times_two), 200);
1059
1060        let start_time = crate::animations::current_tick();
1061
1062        set_animated_value(&compo.width, 200, animation_details);
1063        assert_eq!(get_prop_value(&compo.width), 100);
1064        assert_eq!(get_prop_value(&compo.width_times_two), 200);
1065
1066        // In delay:
1067        crate::animations::CURRENT_ANIMATION_DRIVER
1068            .with(|driver| driver.update_animations(start_time + DELAY / 2));
1069        assert_eq!(get_prop_value(&compo.width), 100);
1070        assert_eq!(get_prop_value(&compo.width_times_two), 200);
1071
1072        // No animation:
1073        crate::animations::CURRENT_ANIMATION_DRIVER
1074            .with(|driver| driver.update_animations(start_time + DELAY));
1075        assert_eq!(get_prop_value(&compo.width), 200);
1076        assert_eq!(get_prop_value(&compo.width_times_two), 400);
1077
1078        // Overshoot: Always to_value.
1079        crate::animations::CURRENT_ANIMATION_DRIVER
1080            .with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 2));
1081        assert_eq!(get_prop_value(&compo.width), 200);
1082        assert_eq!(get_prop_value(&compo.width_times_two), 400);
1083
1084        // the binding should be removed
1085        compo.width.handle.access(|binding| assert!(binding.is_none()));
1086    }
1087
1088    #[test]
1089    fn properties_test_delayed_animation_negative_iteration_triggered_by_set() {
1090        let compo = Component::new_test_component();
1091
1092        let animation_details = PropertyAnimation {
1093            delay: DELAY.as_millis() as _,
1094            iteration_count: -42., // loop forever!
1095            duration: DURATION.as_millis() as _,
1096            ..PropertyAnimation::default()
1097        };
1098
1099        compo.width.set(100);
1100        assert_eq!(get_prop_value(&compo.width), 100);
1101        assert_eq!(get_prop_value(&compo.width_times_two), 200);
1102
1103        let start_time = crate::animations::current_tick();
1104
1105        set_animated_value(&compo.width, 200, animation_details);
1106        assert_eq!(get_prop_value(&compo.width), 100);
1107        assert_eq!(get_prop_value(&compo.width_times_two), 200);
1108
1109        // In delay:
1110        crate::animations::CURRENT_ANIMATION_DRIVER
1111            .with(|driver| driver.update_animations(start_time + DELAY / 2));
1112        assert_eq!(get_prop_value(&compo.width), 100);
1113        assert_eq!(get_prop_value(&compo.width_times_two), 200);
1114
1115        // In animation:
1116        crate::animations::CURRENT_ANIMATION_DRIVER
1117            .with(|driver| driver.update_animations(start_time + DELAY));
1118        assert_eq!(get_prop_value(&compo.width), 100);
1119        assert_eq!(get_prop_value(&compo.width_times_two), 200);
1120
1121        crate::animations::CURRENT_ANIMATION_DRIVER
1122            .with(|driver| driver.update_animations(start_time + DELAY + DURATION / 2));
1123        assert_eq!(get_prop_value(&compo.width), 150);
1124        assert_eq!(get_prop_value(&compo.width_times_two), 300);
1125
1126        crate::animations::CURRENT_ANIMATION_DRIVER
1127            .with(|driver| driver.update_animations(start_time + DELAY + DURATION));
1128        assert_eq!(get_prop_value(&compo.width), 100);
1129        assert_eq!(get_prop_value(&compo.width_times_two), 200);
1130
1131        // In animation (again):
1132        crate::animations::CURRENT_ANIMATION_DRIVER
1133            .with(|driver| driver.update_animations(start_time + DELAY + 500 * DURATION));
1134        assert_eq!(get_prop_value(&compo.width), 100);
1135        assert_eq!(get_prop_value(&compo.width_times_two), 200);
1136
1137        crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| {
1138            driver.update_animations(start_time + DELAY + 50000 * DURATION + DURATION / 2)
1139        });
1140        assert_eq!(get_prop_value(&compo.width), 150);
1141        assert_eq!(get_prop_value(&compo.width_times_two), 300);
1142
1143        // the binding should not be removed as it is still animating!
1144        compo.width.handle.access(|binding| assert!(binding.is_some()));
1145    }
1146
1147    #[test]
1148    fn properties_test_animation_direction_triggered_by_set() {
1149        let compo = Component::new_test_component();
1150
1151        let animation_details = PropertyAnimation {
1152            delay: -25,
1153            duration: DURATION.as_millis() as _,
1154            direction: AnimationDirection::AlternateReverse,
1155            iteration_count: 1.,
1156            ..PropertyAnimation::default()
1157        };
1158
1159        compo.width.set(100);
1160        assert_eq!(get_prop_value(&compo.width), 100);
1161        assert_eq!(get_prop_value(&compo.width_times_two), 200);
1162
1163        let start_time = crate::animations::current_tick();
1164
1165        set_animated_value(&compo.width, 200, animation_details);
1166        assert_eq!(get_prop_value(&compo.width), 200);
1167        assert_eq!(get_prop_value(&compo.width_times_two), 400);
1168
1169        crate::animations::CURRENT_ANIMATION_DRIVER
1170            .with(|driver| driver.update_animations(start_time + DURATION / 2));
1171        assert_eq!(get_prop_value(&compo.width), 150);
1172        assert_eq!(get_prop_value(&compo.width_times_two), 300);
1173
1174        crate::animations::CURRENT_ANIMATION_DRIVER
1175            .with(|driver| driver.update_animations(start_time + DURATION));
1176        assert_eq!(get_prop_value(&compo.width), 100);
1177        assert_eq!(get_prop_value(&compo.width_times_two), 200);
1178
1179        // Overshoot: Always from_value.
1180        crate::animations::CURRENT_ANIMATION_DRIVER
1181            .with(|driver| driver.update_animations(start_time + DURATION + DURATION / 2));
1182        assert_eq!(get_prop_value(&compo.width), 100);
1183        assert_eq!(get_prop_value(&compo.width_times_two), 200);
1184
1185        // the binding should be removed
1186        compo.width.handle.access(|binding| assert!(binding.is_none()));
1187    }
1188
1189    #[test]
1190    fn properties_test_animation_triggered_by_binding() {
1191        let compo = Component::new_test_component();
1192
1193        let start_time = crate::animations::current_tick();
1194
1195        let animation_details = PropertyAnimation {
1196            duration: DURATION.as_millis() as _,
1197            iteration_count: 1.,
1198            ..PropertyAnimation::default()
1199        };
1200
1201        let w = PinWeak::downgrade(compo.clone());
1202        compo.width.set_animated_binding(
1203            move || {
1204                let compo = w.upgrade().unwrap();
1205                get_prop_value(&compo.feed_property)
1206            },
1207            move || (animation_details.clone(), None),
1208        );
1209
1210        compo.feed_property.set(100);
1211        assert_eq!(get_prop_value(&compo.width), 100);
1212        assert_eq!(get_prop_value(&compo.width_times_two), 200);
1213
1214        compo.feed_property.set(200);
1215        assert_eq!(get_prop_value(&compo.width), 100);
1216        assert_eq!(get_prop_value(&compo.width_times_two), 200);
1217
1218        crate::animations::CURRENT_ANIMATION_DRIVER
1219            .with(|driver| driver.update_animations(start_time + DURATION / 2));
1220        assert_eq!(get_prop_value(&compo.width), 150);
1221        assert_eq!(get_prop_value(&compo.width_times_two), 300);
1222
1223        crate::animations::CURRENT_ANIMATION_DRIVER
1224            .with(|driver| driver.update_animations(start_time + DURATION));
1225        assert_eq!(get_prop_value(&compo.width), 200);
1226        assert_eq!(get_prop_value(&compo.width_times_two), 400);
1227    }
1228
1229    #[test]
1230    fn properties_test_delayed_animation_triggered_by_binding() {
1231        let compo = Component::new_test_component();
1232
1233        let start_time = crate::animations::current_tick();
1234
1235        let animation_details = PropertyAnimation {
1236            delay: DELAY.as_millis() as _,
1237            duration: DURATION.as_millis() as _,
1238            iteration_count: 1.0,
1239            ..PropertyAnimation::default()
1240        };
1241
1242        let w = PinWeak::downgrade(compo.clone());
1243        compo.width.set_animated_binding(
1244            move || {
1245                let compo = w.upgrade().unwrap();
1246                get_prop_value(&compo.feed_property)
1247            },
1248            move || (animation_details.clone(), None),
1249        );
1250
1251        compo.feed_property.set(100);
1252        assert_eq!(get_prop_value(&compo.width), 100);
1253        assert_eq!(get_prop_value(&compo.width_times_two), 200);
1254
1255        compo.feed_property.set(200);
1256        assert_eq!(get_prop_value(&compo.width), 100);
1257        assert_eq!(get_prop_value(&compo.width_times_two), 200);
1258
1259        // In delay:
1260        crate::animations::CURRENT_ANIMATION_DRIVER
1261            .with(|driver| driver.update_animations(start_time + DELAY / 2));
1262        assert_eq!(get_prop_value(&compo.width), 100);
1263        assert_eq!(get_prop_value(&compo.width_times_two), 200);
1264
1265        // In animation:
1266        crate::animations::CURRENT_ANIMATION_DRIVER
1267            .with(|driver| driver.update_animations(start_time + DELAY));
1268        assert_eq!(get_prop_value(&compo.width), 100);
1269        assert_eq!(get_prop_value(&compo.width_times_two), 200);
1270
1271        crate::animations::CURRENT_ANIMATION_DRIVER
1272            .with(|driver| driver.update_animations(start_time + DELAY + DURATION / 2));
1273        assert_eq!(get_prop_value(&compo.width), 150);
1274        assert_eq!(get_prop_value(&compo.width_times_two), 300);
1275
1276        crate::animations::CURRENT_ANIMATION_DRIVER
1277            .with(|driver| driver.update_animations(start_time + DELAY + DURATION));
1278        assert_eq!(get_prop_value(&compo.width), 200);
1279        assert_eq!(get_prop_value(&compo.width_times_two), 400);
1280
1281        // Overshoot: Always to_value.
1282        crate::animations::CURRENT_ANIMATION_DRIVER
1283            .with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 2));
1284        assert_eq!(get_prop_value(&compo.width), 200);
1285        assert_eq!(get_prop_value(&compo.width_times_two), 400);
1286    }
1287
1288    #[test]
1289    fn properties_test_animation_triggered_by_binding_with_unrelated_dirty() {
1290        // Reproduces the dependency not changing target bug: the binding driving the
1291        // animated property (`row.1`) never changes, but an *unrelated* field of the
1292        // same source value (`row.0`) is rewritten every frame, like a repeated item's
1293        // model row being touched by `VecModel::set_row_data` every tick.
1294        #[derive(Default)]
1295        struct Component {
1296            width: Property<i32>,
1297            row: Property<(i32, bool)>,
1298        }
1299
1300        let compo = Rc::pin(Component::default());
1301
1302        let animation_details = PropertyAnimation {
1303            duration: DURATION.as_millis() as _,
1304            iteration_count: 1.,
1305            ..PropertyAnimation::default()
1306        };
1307
1308        let w = PinWeak::downgrade(compo.clone());
1309        compo.width.set_animated_binding(
1310            move || {
1311                let compo = w.upgrade().unwrap();
1312                if get_prop_value(&compo.row).1 { 200 } else { 40 }
1313            },
1314            move || (animation_details.clone(), None),
1315        );
1316
1317        compo.row.set((0, false));
1318        assert_eq!(get_prop_value(&compo.width), 40);
1319
1320        let start_time = crate::animations::current_tick();
1321
1322        // Flip the field the animation depends on: this should kick off a 40 -> 200
1323        // animation over DURATION.
1324        compo.row.set((0, true));
1325        assert_eq!(get_prop_value(&compo.width), 40);
1326
1327        // Simulate ~700 real frames (16ms each -- more than DURATION worth of real time
1328        // in total)
1329        let tick = core::time::Duration::from_millis(16);
1330        for i in 1..=700u32 {
1331            compo.row.set((i as i32, true));
1332            crate::animations::CURRENT_ANIMATION_DRIVER
1333                .with(|driver| driver.update_animations(start_time + tick * i));
1334            // Poll every frame like a real renderer repainting
1335            let _ = get_prop_value(&compo.width);
1336        }
1337
1338        // After more than DURATION worth of real time has elapsed, the animation should
1339        // have completed regardless of the unrelated per-frame writes to `row.0`.
1340        assert_eq!(get_prop_value(&compo.width), 200);
1341    }
1342
1343    #[test]
1344    fn test_loop() {
1345        let compo = Component::new_test_component();
1346
1347        let animation_details = PropertyAnimation {
1348            duration: DURATION.as_millis() as _,
1349            iteration_count: 2.,
1350            ..PropertyAnimation::default()
1351        };
1352
1353        compo.width.set(100);
1354
1355        let start_time = crate::animations::current_tick();
1356
1357        set_animated_value(&compo.width, 200, animation_details);
1358        assert_eq!(get_prop_value(&compo.width), 100);
1359
1360        crate::animations::CURRENT_ANIMATION_DRIVER
1361            .with(|driver| driver.update_animations(start_time + DURATION / 2));
1362        assert_eq!(get_prop_value(&compo.width), 150);
1363
1364        crate::animations::CURRENT_ANIMATION_DRIVER
1365            .with(|driver| driver.update_animations(start_time + DURATION));
1366        assert_eq!(get_prop_value(&compo.width), 100);
1367
1368        crate::animations::CURRENT_ANIMATION_DRIVER
1369            .with(|driver| driver.update_animations(start_time + DURATION + DURATION / 2));
1370        assert_eq!(get_prop_value(&compo.width), 150);
1371
1372        crate::animations::CURRENT_ANIMATION_DRIVER
1373            .with(|driver| driver.update_animations(start_time + DURATION * 2));
1374        assert_eq!(get_prop_value(&compo.width), 200);
1375
1376        // the binding should be removed
1377        compo.width.handle.access(|binding| assert!(binding.is_none()));
1378    }
1379
1380    #[test]
1381    fn test_loop_via_binding() {
1382        // Loop twice, restart the animation and still loop twice.
1383
1384        let compo = Component::new_test_component();
1385
1386        let start_time = crate::animations::current_tick();
1387
1388        let animation_details = PropertyAnimation {
1389            duration: DURATION.as_millis() as _,
1390            iteration_count: 2.,
1391            ..PropertyAnimation::default()
1392        };
1393
1394        let w = PinWeak::downgrade(compo.clone());
1395        compo.width.set_animated_binding(
1396            move || {
1397                let compo = w.upgrade().unwrap();
1398                get_prop_value(&compo.feed_property)
1399            },
1400            move || (animation_details.clone(), None),
1401        );
1402
1403        compo.feed_property.set(100);
1404        assert_eq!(get_prop_value(&compo.width), 100);
1405
1406        compo.feed_property.set(200);
1407        assert_eq!(get_prop_value(&compo.width), 100);
1408
1409        crate::animations::CURRENT_ANIMATION_DRIVER
1410            .with(|driver| driver.update_animations(start_time + DURATION / 2));
1411
1412        assert_eq!(get_prop_value(&compo.width), 150);
1413
1414        crate::animations::CURRENT_ANIMATION_DRIVER
1415            .with(|driver| driver.update_animations(start_time + DURATION));
1416
1417        assert_eq!(get_prop_value(&compo.width), 100);
1418
1419        crate::animations::CURRENT_ANIMATION_DRIVER
1420            .with(|driver| driver.update_animations(start_time + DURATION + DURATION / 2));
1421
1422        assert_eq!(get_prop_value(&compo.width), 150);
1423
1424        crate::animations::CURRENT_ANIMATION_DRIVER
1425            .with(|driver| driver.update_animations(start_time + 2 * DURATION));
1426
1427        assert_eq!(get_prop_value(&compo.width), 200);
1428
1429        // Overshoot a bit:
1430        crate::animations::CURRENT_ANIMATION_DRIVER
1431            .with(|driver| driver.update_animations(start_time + 2 * DURATION + DURATION / 2));
1432
1433        assert_eq!(get_prop_value(&compo.width), 200);
1434
1435        // Restart the animation by setting a new value.
1436
1437        let start_time = crate::animations::current_tick();
1438
1439        compo.feed_property.set(300);
1440        assert_eq!(get_prop_value(&compo.width), 200);
1441
1442        crate::animations::CURRENT_ANIMATION_DRIVER
1443            .with(|driver| driver.update_animations(start_time + DURATION / 2));
1444
1445        assert_eq!(get_prop_value(&compo.width), 250);
1446
1447        crate::animations::CURRENT_ANIMATION_DRIVER
1448            .with(|driver| driver.update_animations(start_time + DURATION));
1449
1450        assert_eq!(get_prop_value(&compo.width), 200);
1451
1452        crate::animations::CURRENT_ANIMATION_DRIVER
1453            .with(|driver| driver.update_animations(start_time + DURATION + DURATION / 2));
1454
1455        assert_eq!(get_prop_value(&compo.width), 250);
1456
1457        crate::animations::CURRENT_ANIMATION_DRIVER
1458            .with(|driver| driver.update_animations(start_time + 2 * DURATION));
1459
1460        assert_eq!(get_prop_value(&compo.width), 300);
1461
1462        crate::animations::CURRENT_ANIMATION_DRIVER
1463            .with(|driver| driver.update_animations(start_time + 2 * DURATION + DURATION / 2));
1464
1465        assert_eq!(get_prop_value(&compo.width), 300);
1466    }
1467
1468    #[test]
1469    fn spring_retarget_carries_velocity() {
1470        // A retarget mid-flight must carry the outgoing spring's velocity into the new one,
1471        // instead of restarting it at rest (which would produce a visible "pop").
1472        let compo = Component::new_test_component();
1473
1474        let spring_details = PropertyAnimation {
1475            duration: 1000,
1476            easing: crate::animations::EasingCurve::Spring(0.0),
1477            ..PropertyAnimation::default()
1478        };
1479
1480        compo.width.set(0);
1481        let start_time = crate::animations::current_tick();
1482        set_animated_value(&compo.width, 1000, spring_details.clone());
1483
1484        // Let the spring run for a while so it picks up meaningful velocity.
1485        crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| {
1486            driver.update_animations(start_time + core::time::Duration::from_millis(300))
1487        });
1488        let before_a = get_prop_value(&compo.width) as f32;
1489        crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| {
1490            driver.update_animations(start_time + core::time::Duration::from_millis(310))
1491        });
1492        let before_b = get_prop_value(&compo.width) as f32;
1493        let slope_before = before_b - before_a; // units per 10ms, just prior to the retarget
1494
1495        // Retarget to a new value while the spring is still moving.
1496        set_animated_value(&compo.width, 2000, spring_details);
1497        assert_eq!(
1498            get_prop_value(&compo.width) as f32,
1499            before_b,
1500            "retarget must not snap the value"
1501        );
1502
1503        crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| {
1504            driver.update_animations(start_time + core::time::Duration::from_millis(320))
1505        });
1506        let after = get_prop_value(&compo.width) as f32;
1507        let slope_after = after - before_b; // units per 10ms, just after the retarget
1508
1509        // With velocity carried over, the slope right after the retarget should be close to the
1510        // slope right before it (same order of magnitude, same direction). Without the fix, the
1511        // new spring starts at rest (v0 == 0), so `slope_after` would be near zero here.
1512        assert!(slope_before > 0.5, "sanity check: spring should be moving before retarget");
1513        assert!(
1514            slope_after > slope_before * 0.5,
1515            "velocity was not carried over: slope_before={slope_before}, slope_after={slope_after}"
1516        );
1517    }
1518
1519    #[test]
1520    fn spring_retarget_via_binding_carries_velocity() {
1521        let compo = Component::new_test_component();
1522
1523        let spring_details = PropertyAnimation {
1524            duration: 1000,
1525            easing: crate::animations::EasingCurve::Spring(0.0),
1526            ..PropertyAnimation::default()
1527        };
1528
1529        let w = PinWeak::downgrade(compo.clone());
1530        let details = spring_details.clone();
1531        compo.width.set_animated_binding(
1532            move || {
1533                let compo = w.upgrade().unwrap();
1534                get_prop_value(&compo.feed_property)
1535            },
1536            move || (details.clone(), None),
1537        );
1538
1539        // Establish the dependency and a baseline value (the very first read never animates).
1540        compo.feed_property.set(0);
1541        assert_eq!(get_prop_value(&compo.width), 0);
1542
1543        let start_time = crate::animations::current_tick();
1544        compo.feed_property.set(1000);
1545        assert_eq!(get_prop_value(&compo.width), 0);
1546
1547        // Let the spring run for a while so it picks up meaningful velocity.
1548        crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| {
1549            driver.update_animations(start_time + core::time::Duration::from_millis(300))
1550        });
1551        let before_a = get_prop_value(&compo.width) as f32;
1552        crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| {
1553            driver.update_animations(start_time + core::time::Duration::from_millis(310))
1554        });
1555        let before_b = get_prop_value(&compo.width) as f32;
1556        let slope_before = before_b - before_a; // units per 10ms, just prior to the retarget
1557
1558        // Retarget mid-flight by changing the value the animated binding reads.
1559        compo.feed_property.set(2000);
1560        assert_eq!(
1561            get_prop_value(&compo.width) as f32,
1562            before_b,
1563            "retarget must not snap the value"
1564        );
1565
1566        crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| {
1567            driver.update_animations(start_time + core::time::Duration::from_millis(320))
1568        });
1569        let after = get_prop_value(&compo.width) as f32;
1570        let slope_after = after - before_b; // units per 10ms, just after the retarget
1571
1572        assert!(slope_before > 0.5, "sanity check: spring should be moving before retarget");
1573        assert!(
1574            slope_after > slope_before * 0.5,
1575            "velocity was not carried over through the binding-triggered retarget path: slope_before={slope_before}, slope_after={slope_after}"
1576        );
1577    }
1578
1579    #[test]
1580    fn spring_continuous_retarget_keeps_advancing() {
1581        let compo = Component::new_test_component();
1582
1583        let spring_details = PropertyAnimation {
1584            duration: 1000,
1585            easing: crate::animations::EasingCurve::Spring(0.7),
1586            ..PropertyAnimation::default()
1587        };
1588
1589        let w = PinWeak::downgrade(compo.clone());
1590        let details = spring_details.clone();
1591        compo.width.set_animated_binding(
1592            move || {
1593                let compo = w.upgrade().unwrap();
1594                get_prop_value(&compo.feed_property)
1595            },
1596            move || (details.clone(), None),
1597        );
1598
1599        compo.feed_property.set(0);
1600        assert_eq!(get_prop_value(&compo.width), 0);
1601
1602        let start_time = crate::animations::current_tick();
1603        let mut mouse_x = 0i32;
1604        let mut final_width = 0f32;
1605        for frame in 1..=200 {
1606            mouse_x += 5; // simulate a steady mouse drag, 5px per frame
1607            compo.feed_property.set(mouse_x);
1608            let t = start_time + core::time::Duration::from_millis(frame * 16);
1609            crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| driver.update_animations(t));
1610            // Read the property every frame, like a renderer would when painting each frame --
1611            // this is what actually drives `evaluate()` (property evaluation is lazy).
1612            final_width = get_prop_value(&compo.width) as f32;
1613        }
1614
1615        assert!(
1616            final_width > 500.0,
1617            "spring should have tracked the continuously-moving target by now, got {final_width}"
1618        );
1619    }
1620
1621    #[test]
1622    fn spring_respects_reverse_direction() {
1623        let compo = Component::new_test_component();
1624
1625        let spring_details = PropertyAnimation {
1626            duration: 200,
1627            easing: crate::animations::EasingCurve::Spring(0.0),
1628            direction: AnimationDirection::Reverse,
1629            ..PropertyAnimation::default()
1630        };
1631
1632        compo.width.set(0);
1633        let start_time = crate::animations::current_tick();
1634        set_animated_value(&compo.width, 100, spring_details);
1635
1636        // Reverse: the animation starts at the target and settles back at the origin.
1637        assert_eq!(get_prop_value(&compo.width), 100);
1638
1639        crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| {
1640            driver.update_animations(start_time + core::time::Duration::from_millis(2000))
1641        });
1642        assert_eq!(get_prop_value(&compo.width), 0);
1643
1644        // the binding should be removed once settled
1645        compo.width.handle.access(|binding| assert!(binding.is_none()));
1646    }
1647
1648    #[test]
1649    fn spring_respects_iteration_count_bounce() {
1650        let compo = Component::new_test_component();
1651
1652        let spring_details = PropertyAnimation {
1653            duration: 200,
1654            easing: crate::animations::EasingCurve::Spring(0.0),
1655            direction: AnimationDirection::Alternate,
1656            iteration_count: 2.,
1657            ..PropertyAnimation::default()
1658        };
1659
1660        compo.width.set(0);
1661        let start_time = crate::animations::current_tick();
1662        set_animated_value(&compo.width, 100, spring_details);
1663        assert_eq!(get_prop_value(&compo.width), 0);
1664
1665        // The first leg (forward) switches into the second (reversed) leg at exactly
1666        // `duration`, carrying the spring's velocity into the bounce-back.
1667        crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| {
1668            driver.update_animations(start_time + core::time::Duration::from_millis(200))
1669        });
1670        let mid = get_prop_value(&compo.width);
1671        assert!(mid > 90, "expected the first leg to have reached the target, got {mid}");
1672        compo.width.handle.access(|binding| assert!(binding.is_some()));
1673
1674        // Second leg settles back at the origin, and the binding is then removed.
1675        crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| {
1676            driver.update_animations(start_time + core::time::Duration::from_millis(600))
1677        });
1678        assert_eq!(get_prop_value(&compo.width), 0);
1679        compo.width.handle.access(|binding| assert!(binding.is_none()));
1680    }
1681
1682    #[test]
1683    fn spring_never_settling_on_its_own_settles_within_budget() {
1684        // bounce: 1.0 is undamped -- on its own it oscillates forever. With a finite
1685        // iteration-count (the default, here), it must still settle within
1686        // 10 multiples of `duration` (see `spring_settle_within`).
1687        let compo = Component::new_test_component();
1688
1689        let spring_details = PropertyAnimation {
1690            duration: 200,
1691            easing: crate::animations::EasingCurve::Spring(1.0),
1692            ..PropertyAnimation::default()
1693        };
1694
1695        compo.width.set(0);
1696        let start_time = crate::animations::current_tick();
1697        set_animated_value(&compo.width, 100, spring_details);
1698
1699        // An undamped spring's period equals `duration`, so right at `duration` it's back near
1700        // its start -- nowhere near settled, and still running at full, unclamped bounce.
1701        crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| {
1702            driver.update_animations(start_time + core::time::Duration::from_millis(200))
1703        });
1704        assert!(get_prop_value(&compo.width) < 20, "should still be near the start at duration");
1705        compo.width.handle.access(|binding| assert!(binding.is_some()));
1706
1707        // Comfortably within the settle budget (10x duration), it must have settled.
1708        crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| {
1709            driver.update_animations(start_time + core::time::Duration::from_millis(2000))
1710        });
1711        assert_eq!(get_prop_value(&compo.width), 100);
1712        compo.width.handle.access(|binding| assert!(binding.is_none()));
1713    }
1714
1715    #[test]
1716    fn spring_never_settling_stays_infinite_with_iteration_count_minus_one() {
1717        // The same never-settling bounce, but with `iteration-count: -1`: it must keep
1718        // oscillating indefinitely instead of ever being re-damped.
1719        let compo = Component::new_test_component();
1720
1721        let spring_details = PropertyAnimation {
1722            duration: 200,
1723            easing: crate::animations::EasingCurve::Spring(1.0),
1724            iteration_count: -1.,
1725            direction: AnimationDirection::Alternate,
1726            ..PropertyAnimation::default()
1727        };
1728
1729        compo.width.set(0);
1730        let start_time = crate::animations::current_tick();
1731        set_animated_value(&compo.width, 100, spring_details);
1732
1733        // Well past what would be the settle budget for a finite iteration-count: still running.
1734        crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| {
1735            driver.update_animations(start_time + core::time::Duration::from_millis(3000))
1736        });
1737        compo.width.handle.access(|binding| assert!(binding.is_some()));
1738    }
1739}