// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 use super::*; use crate::{items::PropertyAnimation, lengths::LogicalLength}; #[cfg(not(feature = "std"))] use num_traits::Float; enum AnimationState { Delaying, Animating { current_iteration: u64 }, Done, } pub(super) struct PropertyValueAnimationData { from_value: T, to_value: T, details: PropertyAnimation, start_time: crate::animations::Instant, state: AnimationState, } impl PropertyValueAnimationData { pub fn new(from_value: T, to_value: T, details: PropertyAnimation) -> Self { let start_time = crate::animations::current_tick(); Self { from_value, to_value, details, start_time, state: AnimationState::Delaying } } pub fn compute_interpolated_value(&mut self) -> (T, bool) { let new_tick = crate::animations::current_tick(); let mut time_progress = new_tick.duration_since(self.start_time).as_millis() as u64; match self.state { AnimationState::Delaying => { if self.details.delay <= 0 { self.state = AnimationState::Animating { current_iteration: 0 }; return self.compute_interpolated_value(); } let delay = self.details.delay as u64; if time_progress < delay { (self.from_value.clone(), false) } else { self.start_time = new_tick - core::time::Duration::from_millis(time_progress - delay); // Decide on next state: self.state = AnimationState::Animating { current_iteration: 0 }; self.compute_interpolated_value() } } AnimationState::Animating { mut current_iteration } => { if self.details.duration <= 0 || self.details.iteration_count == 0. { self.state = AnimationState::Done; return self.compute_interpolated_value(); } let duration = self.details.duration as u64; if time_progress >= duration { // wrap around current_iteration += time_progress / duration; time_progress %= duration; self.start_time = new_tick - core::time::Duration::from_millis(time_progress); } if (self.details.iteration_count < 0.) || (((current_iteration * duration) + time_progress) as f64) < ((self.details.iteration_count as f64) * (duration as f64)) { self.state = AnimationState::Animating { current_iteration }; let progress = (time_progress as f32 / self.details.duration as f32).clamp(0., 1.); let t = crate::animations::easing_curve(&self.details.easing, progress); let val = self.from_value.interpolate(&self.to_value, t); (val, false) } else { self.state = AnimationState::Done; self.compute_interpolated_value() } } AnimationState::Done => (self.to_value.clone(), true), } } fn reset(&mut self) { self.state = AnimationState::Delaying; self.start_time = crate::animations::current_tick(); } } #[derive(Clone, Copy, Eq, PartialEq, Debug)] pub(super) enum AnimatedBindingState { Animating, NotAnimating, ShouldStart, } pub(super) struct AnimatedBindingCallable { pub(super) original_binding: PropertyHandle, pub(super) state: Cell, pub(super) animation_data: RefCell>, pub(super) compute_animation_details: A, } pub(super) type AnimationDetail = Option<(PropertyAnimation, crate::animations::Instant)>; unsafe impl AnimationDetail> BindingCallable for AnimatedBindingCallable { unsafe fn evaluate(self: Pin<&Self>, value: *mut ()) -> BindingResult { let original_binding = Pin::new_unchecked(&self.original_binding); original_binding.register_as_dependency_to_current_binding( #[cfg(slint_debug_property)] "", ); match self.state.get() { AnimatedBindingState::Animating => { let (val, finished) = self.animation_data.borrow_mut().compute_interpolated_value(); *(value as *mut T) = val; if finished { self.state.set(AnimatedBindingState::NotAnimating) } else { crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.set_has_active_animations()); } } AnimatedBindingState::NotAnimating => { self.original_binding.update(value); } AnimatedBindingState::ShouldStart => { let value = &mut *(value as *mut T); self.state.set(AnimatedBindingState::Animating); let mut animation_data = self.animation_data.borrow_mut(); // animation_data.details.iteration_count = 1.; animation_data.from_value = value.clone(); self.original_binding.update((&mut animation_data.to_value) as *mut T as *mut ()); if let Some((details, start_time)) = (self.compute_animation_details)() { animation_data.start_time = start_time; animation_data.details = details; } let (val, finished) = animation_data.compute_interpolated_value(); *value = val; if finished { self.state.set(AnimatedBindingState::NotAnimating) } else { crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.set_has_active_animations()); } } }; BindingResult::KeepBinding } fn mark_dirty(self: Pin<&Self>) { if self.state.get() == AnimatedBindingState::ShouldStart { return; } let original_dirty = self.original_binding.access(|b| b.unwrap().dirty.get()); if original_dirty { self.state.set(AnimatedBindingState::ShouldStart); self.animation_data.borrow_mut().reset(); } } } /// InterpolatedPropertyValue is a trait used to enable properties to be used with /// animations that interpolate values. The basic requirement is the ability to apply /// a progress that's typically between 0 and 1 to a range. pub trait InterpolatedPropertyValue: PartialEq + Default + 'static { /// Returns the interpolated value between self and target_value according to the /// progress parameter t that's usually between 0 and 1. With certain animation /// easing curves it may over- or undershoot though. #[must_use] fn interpolate(&self, target_value: &Self, t: f32) -> Self; } impl InterpolatedPropertyValue for f32 { fn interpolate(&self, target_value: &Self, t: f32) -> Self { self + t * (target_value - self) } } impl InterpolatedPropertyValue for i32 { fn interpolate(&self, target_value: &Self, t: f32) -> Self { self + (t * (target_value - self) as f32).round() as i32 } } impl InterpolatedPropertyValue for i64 { fn interpolate(&self, target_value: &Self, t: f32) -> Self { self + (t * (target_value - self) as f32).round() as Self } } impl InterpolatedPropertyValue for u8 { fn interpolate(&self, target_value: &Self, t: f32) -> Self { ((*self as f32) + (t * ((*target_value as f32) - (*self as f32)))).round().clamp(0., 255.) as u8 } } impl InterpolatedPropertyValue for LogicalLength { fn interpolate(&self, target_value: &Self, t: f32) -> Self { LogicalLength::new(self.get().interpolate(&target_value.get(), t)) } } impl Property { /// Change the value of this property, by animating (interpolating) from the current property's value /// to the specified parameter value. The animation is done according to the parameters described by /// the PropertyAnimation object. /// /// If other properties have binding depending of this property, these properties will /// be marked as dirty. pub fn set_animated_value(&self, value: T, animation_data: PropertyAnimation) { // FIXME if the current value is a dirty binding, we must run it, but we do not have the context let d = RefCell::new(properties_animations::PropertyValueAnimationData::new( self.get_internal(), value, animation_data, )); // Safety: the BindingCallable will cast its argument to T unsafe { self.handle.set_binding( move |val: *mut ()| { let (value, finished) = d.borrow_mut().compute_interpolated_value(); *(val as *mut T) = value; if finished { BindingResult::RemoveBinding } else { crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.set_has_active_animations()); BindingResult::KeepBinding } }, #[cfg(slint_debug_property)] self.debug_name.borrow().as_str(), ); } self.handle.mark_dirty( #[cfg(slint_debug_property)] self.debug_name.borrow().as_str(), ); } /// Set a binding to this property. /// pub fn set_animated_binding( &self, binding: impl Binding + 'static, animation_data: PropertyAnimation, ) { let binding_callable = properties_animations::AnimatedBindingCallable:: { original_binding: PropertyHandle { handle: Cell::new( (alloc_binding_holder(move |val: *mut ()| unsafe { let val = &mut *(val as *mut T); *(val as *mut T) = binding.evaluate(val); BindingResult::KeepBinding }) as usize) | 0b10, ), }, state: Cell::new(properties_animations::AnimatedBindingState::NotAnimating), animation_data: RefCell::new(properties_animations::PropertyValueAnimationData::new( T::default(), T::default(), animation_data, )), compute_animation_details: || -> properties_animations::AnimationDetail { None }, }; // Safety: the `AnimatedBindingCallable`'s type match the property type unsafe { self.handle.set_binding( binding_callable, #[cfg(slint_debug_property)] self.debug_name.borrow().as_str(), ) }; self.handle.mark_dirty( #[cfg(slint_debug_property)] self.debug_name.borrow().as_str(), ); } /// Set a binding to this property, providing a callback for the transition animation /// pub fn set_animated_binding_for_transition( &self, binding: impl Binding + 'static, compute_animation_details: impl Fn() -> (PropertyAnimation, crate::animations::Instant) + 'static, ) { let binding_callable = properties_animations::AnimatedBindingCallable:: { original_binding: PropertyHandle { handle: Cell::new( (alloc_binding_holder(move |val: *mut ()| unsafe { let val = &mut *(val as *mut T); *(val as *mut T) = binding.evaluate(val); BindingResult::KeepBinding }) as usize) | 0b10, ), }, state: Cell::new(properties_animations::AnimatedBindingState::NotAnimating), animation_data: RefCell::new(properties_animations::PropertyValueAnimationData::new( T::default(), T::default(), PropertyAnimation::default(), )), compute_animation_details: move || Some(compute_animation_details()), }; // Safety: the `AnimatedBindingCallable`'s type match the property type unsafe { self.handle.set_binding( binding_callable, #[cfg(slint_debug_property)] self.debug_name.borrow().as_str(), ) }; self.handle.mark_dirty( #[cfg(slint_debug_property)] self.debug_name.borrow().as_str(), ); } } #[cfg(test)] mod animation_tests { use super::*; #[derive(Default)] struct Component { width: Property, width_times_two: Property, feed_property: Property, // used by binding to feed values into width } impl Component { fn new_test_component() -> Rc { let compo = Rc::new(Component::default()); let w = Rc::downgrade(&compo); compo.width_times_two.set_binding(move || { let compo = w.upgrade().unwrap(); get_prop_value(&compo.width) * 2 }); compo } } const DURATION: std::time::Duration = std::time::Duration::from_millis(10000); const DELAY: std::time::Duration = std::time::Duration::from_millis(800); // Helper just for testing fn get_prop_value(prop: &Property) -> T { unsafe { Pin::new_unchecked(prop).get() } } #[test] fn properties_test_animation_negative_delay_triggered_by_set() { let compo = Component::new_test_component(); let animation_details = PropertyAnimation { delay: -25, duration: DURATION.as_millis() as _, iteration_count: 1., ..PropertyAnimation::default() }; compo.width.set(100); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); let start_time = crate::animations::current_tick(); compo.width.set_animated_value(200, animation_details); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DURATION / 2)); assert_eq!(get_prop_value(&compo.width), 150); assert_eq!(get_prop_value(&compo.width_times_two), 300); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DURATION)); assert_eq!(get_prop_value(&compo.width), 200); assert_eq!(get_prop_value(&compo.width_times_two), 400); // Overshoot: Always to_value. crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DURATION + DURATION / 2)); assert_eq!(get_prop_value(&compo.width), 200); assert_eq!(get_prop_value(&compo.width_times_two), 400); // the binding should be removed compo.width.handle.access(|binding| assert!(binding.is_none())); } #[test] fn properties_test_animation_triggered_by_set() { let compo = Component::new_test_component(); let animation_details = PropertyAnimation { duration: DURATION.as_millis() as _, iteration_count: 1., ..PropertyAnimation::default() }; compo.width.set(100); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); let start_time = crate::animations::current_tick(); compo.width.set_animated_value(200, animation_details); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DURATION / 2)); assert_eq!(get_prop_value(&compo.width), 150); assert_eq!(get_prop_value(&compo.width_times_two), 300); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DURATION)); assert_eq!(get_prop_value(&compo.width), 200); assert_eq!(get_prop_value(&compo.width_times_two), 400); // Overshoot: Always to_value. crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DURATION + DURATION / 2)); assert_eq!(get_prop_value(&compo.width), 200); assert_eq!(get_prop_value(&compo.width_times_two), 400); // the binding should be removed compo.width.handle.access(|binding| assert!(binding.is_none())); } #[test] fn properties_test_delayed_animation_triggered_by_set() { let compo = Component::new_test_component(); let animation_details = PropertyAnimation { delay: DELAY.as_millis() as _, iteration_count: 1., duration: DURATION.as_millis() as _, ..PropertyAnimation::default() }; compo.width.set(100); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); let start_time = crate::animations::current_tick(); compo.width.set_animated_value(200, animation_details); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); // In delay: crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY / 2)); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); // In animation: crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY)); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY + DURATION / 2)); assert_eq!(get_prop_value(&compo.width), 150); assert_eq!(get_prop_value(&compo.width_times_two), 300); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY + DURATION)); assert_eq!(get_prop_value(&compo.width), 200); assert_eq!(get_prop_value(&compo.width_times_two), 400); // Overshoot: Always to_value. crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 2)); assert_eq!(get_prop_value(&compo.width), 200); assert_eq!(get_prop_value(&compo.width_times_two), 400); // the binding should be removed compo.width.handle.access(|binding| assert!(binding.is_none())); } #[test] fn properties_test_delayed_animation_fractual_interation_triggered_by_set() { let compo = Component::new_test_component(); let animation_details = PropertyAnimation { delay: DELAY.as_millis() as _, iteration_count: 1.5, duration: DURATION.as_millis() as _, ..PropertyAnimation::default() }; compo.width.set(100); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); let start_time = crate::animations::current_tick(); compo.width.set_animated_value(200, animation_details); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); // In delay: crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY / 2)); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); // In animation: crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY)); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY + DURATION / 2)); assert_eq!(get_prop_value(&compo.width), 150); assert_eq!(get_prop_value(&compo.width_times_two), 300); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY + DURATION)); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); // (fractual) end of animation crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 4)); assert_eq!(get_prop_value(&compo.width), 125); assert_eq!(get_prop_value(&compo.width_times_two), 250); // End of animation: crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 2)); assert_eq!(get_prop_value(&compo.width), 200); assert_eq!(get_prop_value(&compo.width_times_two), 400); // the binding should be removed compo.width.handle.access(|binding| assert!(binding.is_none())); } #[test] fn properties_test_delayed_animation_null_duration_triggered_by_set() { let compo = Component::new_test_component(); let animation_details = PropertyAnimation { delay: DELAY.as_millis() as _, iteration_count: 1.0, duration: 0, ..PropertyAnimation::default() }; compo.width.set(100); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); let start_time = crate::animations::current_tick(); compo.width.set_animated_value(200, animation_details); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); // In delay: crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY / 2)); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); // No animation: crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY)); assert_eq!(get_prop_value(&compo.width), 200); assert_eq!(get_prop_value(&compo.width_times_two), 400); // Overshoot: Always to_value. crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 2)); assert_eq!(get_prop_value(&compo.width), 200); assert_eq!(get_prop_value(&compo.width_times_two), 400); // the binding should be removed compo.width.handle.access(|binding| assert!(binding.is_none())); } #[test] fn properties_test_delayed_animation_negative_duration_triggered_by_set() { let compo = Component::new_test_component(); let animation_details = PropertyAnimation { delay: DELAY.as_millis() as _, iteration_count: 1.0, duration: -25, ..PropertyAnimation::default() }; compo.width.set(100); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); let start_time = crate::animations::current_tick(); compo.width.set_animated_value(200, animation_details); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); // In delay: crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY / 2)); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); // No animation: crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY)); assert_eq!(get_prop_value(&compo.width), 200); assert_eq!(get_prop_value(&compo.width_times_two), 400); // Overshoot: Always to_value. crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 2)); assert_eq!(get_prop_value(&compo.width), 200); assert_eq!(get_prop_value(&compo.width_times_two), 400); // the binding should be removed compo.width.handle.access(|binding| assert!(binding.is_none())); } #[test] fn properties_test_delayed_animation_no_iteration_triggered_by_set() { let compo = Component::new_test_component(); let animation_details = PropertyAnimation { delay: DELAY.as_millis() as _, iteration_count: 0.0, duration: DURATION.as_millis() as _, ..PropertyAnimation::default() }; compo.width.set(100); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); let start_time = crate::animations::current_tick(); compo.width.set_animated_value(200, animation_details); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); // In delay: crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY / 2)); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); // No animation: crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY)); assert_eq!(get_prop_value(&compo.width), 200); assert_eq!(get_prop_value(&compo.width_times_two), 400); // Overshoot: Always to_value. crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 2)); assert_eq!(get_prop_value(&compo.width), 200); assert_eq!(get_prop_value(&compo.width_times_two), 400); // the binding should be removed compo.width.handle.access(|binding| assert!(binding.is_none())); } #[test] fn properties_test_delayed_animation_negative_iteration_triggered_by_set() { let compo = Component::new_test_component(); let animation_details = PropertyAnimation { delay: DELAY.as_millis() as _, iteration_count: -42., // loop forever! duration: DURATION.as_millis() as _, ..PropertyAnimation::default() }; compo.width.set(100); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); let start_time = crate::animations::current_tick(); compo.width.set_animated_value(200, animation_details); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); // In delay: crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY / 2)); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); // In animation: crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY)); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY + DURATION / 2)); assert_eq!(get_prop_value(&compo.width), 150); assert_eq!(get_prop_value(&compo.width_times_two), 300); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY + DURATION)); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); // In animation (again): crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY + 500 * DURATION)); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| { driver.update_animations(start_time + DELAY + 50000 * DURATION + DURATION / 2) }); assert_eq!(get_prop_value(&compo.width), 150); assert_eq!(get_prop_value(&compo.width_times_two), 300); // the binding should not be removed as it is still animating! compo.width.handle.access(|binding| assert!(binding.is_some())); } #[test] fn properties_test_animation_triggered_by_binding() { let compo = Component::new_test_component(); let start_time = crate::animations::current_tick(); let animation_details = PropertyAnimation { duration: DURATION.as_millis() as _, iteration_count: 1., ..PropertyAnimation::default() }; let w = Rc::downgrade(&compo); compo.width.set_animated_binding( move || { let compo = w.upgrade().unwrap(); get_prop_value(&compo.feed_property) }, animation_details, ); compo.feed_property.set(100); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); compo.feed_property.set(200); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DURATION / 2)); assert_eq!(get_prop_value(&compo.width), 150); assert_eq!(get_prop_value(&compo.width_times_two), 300); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DURATION)); assert_eq!(get_prop_value(&compo.width), 200); assert_eq!(get_prop_value(&compo.width_times_two), 400); } #[test] fn properties_test_delayed_animation_triggered_by_binding() { let compo = Component::new_test_component(); let start_time = crate::animations::current_tick(); let animation_details = PropertyAnimation { delay: DELAY.as_millis() as _, duration: DURATION.as_millis() as _, iteration_count: 1.0, ..PropertyAnimation::default() }; let w = Rc::downgrade(&compo); compo.width.set_animated_binding( move || { let compo = w.upgrade().unwrap(); get_prop_value(&compo.feed_property) }, animation_details, ); compo.feed_property.set(100); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); compo.feed_property.set(200); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); // In delay: crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY / 2)); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); // In animation: crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY)); assert_eq!(get_prop_value(&compo.width), 100); assert_eq!(get_prop_value(&compo.width_times_two), 200); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY + DURATION / 2)); assert_eq!(get_prop_value(&compo.width), 150); assert_eq!(get_prop_value(&compo.width_times_two), 300); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY + DURATION)); assert_eq!(get_prop_value(&compo.width), 200); assert_eq!(get_prop_value(&compo.width_times_two), 400); // Overshoot: Always to_value. crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 2)); assert_eq!(get_prop_value(&compo.width), 200); assert_eq!(get_prop_value(&compo.width_times_two), 400); } #[test] fn test_loop() { let compo = Component::new_test_component(); let animation_details = PropertyAnimation { duration: DURATION.as_millis() as _, iteration_count: 2., ..PropertyAnimation::default() }; compo.width.set(100); let start_time = crate::animations::current_tick(); compo.width.set_animated_value(200, animation_details); assert_eq!(get_prop_value(&compo.width), 100); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DURATION / 2)); assert_eq!(get_prop_value(&compo.width), 150); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DURATION)); assert_eq!(get_prop_value(&compo.width), 100); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DURATION + DURATION / 2)); assert_eq!(get_prop_value(&compo.width), 150); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DURATION * 2)); assert_eq!(get_prop_value(&compo.width), 200); // the binding should be removed compo.width.handle.access(|binding| assert!(binding.is_none())); } #[test] fn test_loop_via_binding() { // Loop twice, restart the animation and still loop twice. let compo = Component::new_test_component(); let start_time = crate::animations::current_tick(); let animation_details = PropertyAnimation { duration: DURATION.as_millis() as _, iteration_count: 2., ..PropertyAnimation::default() }; let w = Rc::downgrade(&compo); compo.width.set_animated_binding( move || { let compo = w.upgrade().unwrap(); get_prop_value(&compo.feed_property) }, animation_details, ); compo.feed_property.set(100); assert_eq!(get_prop_value(&compo.width), 100); compo.feed_property.set(200); assert_eq!(get_prop_value(&compo.width), 100); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DURATION / 2)); assert_eq!(get_prop_value(&compo.width), 150); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DURATION)); assert_eq!(get_prop_value(&compo.width), 100); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DURATION + DURATION / 2)); assert_eq!(get_prop_value(&compo.width), 150); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + 2 * DURATION)); assert_eq!(get_prop_value(&compo.width), 200); // Overshoot a bit: crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + 2 * DURATION + DURATION / 2)); assert_eq!(get_prop_value(&compo.width), 200); // Restart the animation by setting a new value. let start_time = crate::animations::current_tick(); compo.feed_property.set(300); assert_eq!(get_prop_value(&compo.width), 200); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DURATION / 2)); assert_eq!(get_prop_value(&compo.width), 250); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DURATION)); assert_eq!(get_prop_value(&compo.width), 200); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DURATION + DURATION / 2)); assert_eq!(get_prop_value(&compo.width), 250); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + 2 * DURATION)); assert_eq!(get_prop_value(&compo.width), 300); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + 2 * DURATION + DURATION / 2)); assert_eq!(get_prop_value(&compo.width), 300); } }