diff --git a/sixtyfps_compiler/typeregister.rs b/sixtyfps_compiler/typeregister.rs index 364ca9f56..23230962b 100644 --- a/sixtyfps_compiler/typeregister.rs +++ b/sixtyfps_compiler/typeregister.rs @@ -337,6 +337,7 @@ impl TypeRegister { let mut property_animation = BuiltinElement { class_name: "PropertyAnimation".into(), ..Default::default() }; property_animation.properties.insert("duration".to_owned(), Type::Int32); + property_animation.properties.insert("loop_count".to_owned(), Type::Int32); r.property_animation_type = Type::Builtin(Rc::new(property_animation)); r.supported_property_animation_types.insert(Type::Float32.to_string()); r.supported_property_animation_types.insert(Type::Int32.to_string()); diff --git a/sixtyfps_runtime/corelib/abi/primitives.rs b/sixtyfps_runtime/corelib/abi/primitives.rs index 11bf9cdfb..42a7b05f6 100644 --- a/sixtyfps_runtime/corelib/abi/primitives.rs +++ b/sixtyfps_runtime/corelib/abi/primitives.rs @@ -289,4 +289,6 @@ pub use crate::abi::datastructures::PathVTable; pub struct PropertyAnimation { #[rtti_field] pub duration: i32, + #[rtti_field] + pub loop_count: i32, } diff --git a/sixtyfps_runtime/corelib/abi/properties.rs b/sixtyfps_runtime/corelib/abi/properties.rs index 98e97f243..c50508d37 100644 --- a/sixtyfps_runtime/corelib/abi/properties.rs +++ b/sixtyfps_runtime/corelib/abi/properties.rs @@ -487,9 +487,13 @@ impl Property { /// 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 = PropertyValueAnimationData::new(self.get_internal(), value, animation_data.clone()); + let d = RefCell::new(PropertyValueAnimationData::new( + self.get_internal(), + value, + animation_data.clone(), + )); self.handle.set_binding(move |val: *mut ()| unsafe { - let (value, finished) = d.compute_interpolated_value(); + let (value, finished) = d.borrow_mut().compute_interpolated_value(); *(val as *mut T) = value; if finished { BindingResult::RemoveBinding @@ -534,6 +538,7 @@ struct PropertyValueAnimationData { to_value: T, details: crate::abi::primitives::PropertyAnimation, start_time: instant::Instant, + loop_iteration: i32, } impl PropertyValueAnimationData { @@ -541,15 +546,22 @@ impl PropertyValueAnimationData { let start_time = crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| driver.current_tick()); - Self { from_value, to_value, details, start_time } + Self { from_value, to_value, details, start_time, loop_iteration: 0 } } - fn compute_interpolated_value(&self) -> (T, bool) { + fn compute_interpolated_value(&mut self) -> (T, bool) { + let duration = self.details.duration as u128; let new_tick = crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| driver.current_tick()); - let time_progress = new_tick.duration_since(self.start_time).as_millis(); - if time_progress >= self.details.duration as _ { - return (self.to_value.clone(), true); + let mut time_progress = new_tick.duration_since(self.start_time).as_millis(); + if time_progress >= duration { + if self.loop_iteration < self.details.loop_count || self.details.loop_count < 0 { + self.loop_iteration += (time_progress / duration) as i32; + time_progress = time_progress % duration; + self.start_time = new_tick - std::time::Duration::from_millis(time_progress as u64); + } else { + return (self.to_value.clone(), true); + } } let progress = time_progress as f32 / self.details.duration as f32; assert!(progress <= 1.); @@ -576,7 +588,7 @@ impl BindingCallable for AnimatedBindingCallable { - let (val, finished) = self.animation_data.borrow().compute_interpolated_value(); + let (val, finished) = self.animation_data.borrow_mut().compute_interpolated_value(); *(value as *mut T) = val; if finished { self.state.set(AnimatedBindingState::NotAnimating) @@ -771,9 +783,9 @@ fn c_set_animated_value( to: T, animation_data: &crate::abi::primitives::PropertyAnimation, ) { - let d = PropertyValueAnimationData::new(from, to, animation_data.clone()); + let d = RefCell::new(PropertyValueAnimationData::new(from, to, animation_data.clone())); handle.0.set_binding(move |val: *mut ()| { - let (value, finished) = d.compute_interpolated_value(); + let (value, finished) = d.borrow_mut().compute_interpolated_value(); unsafe { *(val as *mut T) = value; } @@ -898,48 +910,57 @@ mod animation_tests { 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: instant::Duration = instant::Duration::from_millis(10000); + // Helper just for testing + fn get_prop_value(prop: &Property) -> T { + unsafe { Pin::new_unchecked(prop).get() } + } + #[test] fn properties_test_animation_triggered_by_set() { - fn g(prop: &Property) -> i32 { - unsafe { Pin::new_unchecked(prop).get() } - } + let compo = Component::new_test_component(); - let compo = Rc::new(Component::default()); - - let w = Rc::downgrade(&compo); - compo.width_times_two.set_binding(move || { - let compo = w.upgrade().unwrap(); - g(&compo.width) * 2 - }); - - let animation_details = PropertyAnimation { duration: DURATION.as_millis() as _ }; + let animation_details = + PropertyAnimation { duration: DURATION.as_millis() as _, loop_count: 0 }; compo.width.set(100); - assert_eq!(g(&compo.width), 100); - assert_eq!(g(&compo.width_times_two), 200); + assert_eq!(get_prop_value(&compo.width), 100); + assert_eq!(get_prop_value(&compo.width_times_two), 200); let start_time = crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| driver.current_tick()); compo.width.set_animated_value(200, &animation_details); - assert_eq!(g(&compo.width), 100); - assert_eq!(g(&compo.width_times_two), 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!(g(&compo.width), 150); - assert_eq!(g(&compo.width_times_two), 300); + 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!(g(&compo.width), 200); - assert_eq!(g(&compo.width_times_two), 400); + assert_eq!(get_prop_value(&compo.width), 200); + assert_eq!(get_prop_value(&compo.width_times_two), 400); crate::animations::CURRENT_ANIMATION_DRIVER .with(|driver| driver.update_animations(start_time + DURATION * 2)); - assert_eq!(g(&compo.width), 200); - assert_eq!(g(&compo.width_times_two), 400); + 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())); @@ -947,50 +968,116 @@ mod animation_tests { #[test] fn properties_test_animation_triggered_by_binding() { - fn g(prop: &Property) -> i32 { - unsafe { Pin::new_unchecked(prop).get() } - } - let compo = Rc::new(Component::default()); - - let w = Rc::downgrade(&compo); - compo.width_times_two.set_binding(move || { - let compo = w.upgrade().unwrap(); - g(&compo.width) * 2 - }); + let compo = Component::new_test_component(); let start_time = crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| driver.current_tick()); - let animation_details = PropertyAnimation { duration: DURATION.as_millis() as _ }; + let animation_details = + PropertyAnimation { duration: DURATION.as_millis() as _, loop_count: 0 }; let w = Rc::downgrade(&compo); compo.width.set_animated_binding( move || { let compo = w.upgrade().unwrap(); - g(&compo.feed_property) + get_prop_value(&compo.feed_property) }, &animation_details, ); compo.feed_property.set(100); - assert_eq!(g(&compo.width), 100); - assert_eq!(g(&compo.width_times_two), 200); + 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!(g(&compo.width), 100); - assert_eq!(g(&compo.width_times_two), 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!(g(&compo.width), 150); - assert_eq!(g(&compo.width_times_two), 300); + 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!(g(&compo.width), 200); - assert_eq!(g(&compo.width_times_two), 400); + 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 _, loop_count: 2 }; + + compo.width.set(100); + + let start_time = + crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| driver.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), 100); + + crate::animations::CURRENT_ANIMATION_DRIVER + .with(|driver| driver.update_animations(start_time + DURATION * 2 + DURATION / 2)); + assert_eq!(get_prop_value(&compo.width), 150); + + crate::animations::CURRENT_ANIMATION_DRIVER + .with(|driver| driver.update_animations(start_time + DURATION * 3)); + 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_overshoot() { + let compo = Component::new_test_component(); + + let animation_details = + PropertyAnimation { duration: DURATION.as_millis() as _, loop_count: 2 }; + + compo.width.set(100); + + let start_time = + crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| driver.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 * 2 + DURATION / 2)); + assert_eq!(get_prop_value(&compo.width), 150); + + crate::animations::CURRENT_ANIMATION_DRIVER + .with(|driver| driver.update_animations(start_time + DURATION * 3)); + assert_eq!(get_prop_value(&compo.width), 200); + + // the binding should be removed + compo.width.handle.access(|binding| assert!(binding.is_none())); } }