mirror of
https://github.com/slint-ui/slint.git
synced 2025-09-29 13:24:48 +00:00
Add support for looping in animations
This commit is contained in:
parent
c30aa370f3
commit
f8024e6658
3 changed files with 142 additions and 52 deletions
|
@ -337,6 +337,7 @@ impl TypeRegister {
|
||||||
let mut property_animation =
|
let mut property_animation =
|
||||||
BuiltinElement { class_name: "PropertyAnimation".into(), ..Default::default() };
|
BuiltinElement { class_name: "PropertyAnimation".into(), ..Default::default() };
|
||||||
property_animation.properties.insert("duration".to_owned(), Type::Int32);
|
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.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::Float32.to_string());
|
||||||
r.supported_property_animation_types.insert(Type::Int32.to_string());
|
r.supported_property_animation_types.insert(Type::Int32.to_string());
|
||||||
|
|
|
@ -289,4 +289,6 @@ pub use crate::abi::datastructures::PathVTable;
|
||||||
pub struct PropertyAnimation {
|
pub struct PropertyAnimation {
|
||||||
#[rtti_field]
|
#[rtti_field]
|
||||||
pub duration: i32,
|
pub duration: i32,
|
||||||
|
#[rtti_field]
|
||||||
|
pub loop_count: i32,
|
||||||
}
|
}
|
||||||
|
|
|
@ -487,9 +487,13 @@ impl<T: Clone + InterpolatedPropertyValue + 'static> Property<T> {
|
||||||
/// be marked as dirty.
|
/// be marked as dirty.
|
||||||
pub fn set_animated_value(&self, value: T, animation_data: &PropertyAnimation) {
|
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
|
// 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 {
|
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;
|
*(val as *mut T) = value;
|
||||||
if finished {
|
if finished {
|
||||||
BindingResult::RemoveBinding
|
BindingResult::RemoveBinding
|
||||||
|
@ -534,6 +538,7 @@ struct PropertyValueAnimationData<T> {
|
||||||
to_value: T,
|
to_value: T,
|
||||||
details: crate::abi::primitives::PropertyAnimation,
|
details: crate::abi::primitives::PropertyAnimation,
|
||||||
start_time: instant::Instant,
|
start_time: instant::Instant,
|
||||||
|
loop_iteration: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: InterpolatedPropertyValue> PropertyValueAnimationData<T> {
|
impl<T: InterpolatedPropertyValue> PropertyValueAnimationData<T> {
|
||||||
|
@ -541,15 +546,22 @@ impl<T: InterpolatedPropertyValue> PropertyValueAnimationData<T> {
|
||||||
let start_time =
|
let start_time =
|
||||||
crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| driver.current_tick());
|
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 =
|
let new_tick =
|
||||||
crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| driver.current_tick());
|
crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| driver.current_tick());
|
||||||
let time_progress = new_tick.duration_since(self.start_time).as_millis();
|
let mut time_progress = new_tick.duration_since(self.start_time).as_millis();
|
||||||
if time_progress >= self.details.duration as _ {
|
if time_progress >= duration {
|
||||||
return (self.to_value.clone(), true);
|
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;
|
let progress = time_progress as f32 / self.details.duration as f32;
|
||||||
assert!(progress <= 1.);
|
assert!(progress <= 1.);
|
||||||
|
@ -576,7 +588,7 @@ impl<T: InterpolatedPropertyValue> BindingCallable for AnimatedBindingCallable<T
|
||||||
self.original_binding.register_as_dependency_to_current_binding();
|
self.original_binding.register_as_dependency_to_current_binding();
|
||||||
match self.state.get() {
|
match self.state.get() {
|
||||||
AnimatedBindingState::Animating => {
|
AnimatedBindingState::Animating => {
|
||||||
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;
|
*(value as *mut T) = val;
|
||||||
if finished {
|
if finished {
|
||||||
self.state.set(AnimatedBindingState::NotAnimating)
|
self.state.set(AnimatedBindingState::NotAnimating)
|
||||||
|
@ -771,9 +783,9 @@ fn c_set_animated_value<T: InterpolatedPropertyValue>(
|
||||||
to: T,
|
to: T,
|
||||||
animation_data: &crate::abi::primitives::PropertyAnimation,
|
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 ()| {
|
handle.0.set_binding(move |val: *mut ()| {
|
||||||
let (value, finished) = d.compute_interpolated_value();
|
let (value, finished) = d.borrow_mut().compute_interpolated_value();
|
||||||
unsafe {
|
unsafe {
|
||||||
*(val as *mut T) = value;
|
*(val as *mut T) = value;
|
||||||
}
|
}
|
||||||
|
@ -898,48 +910,57 @@ mod animation_tests {
|
||||||
feed_property: Property<i32>, // used by binding to feed values into width
|
feed_property: Property<i32>, // used by binding to feed values into width
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Component {
|
||||||
|
fn new_test_component() -> Rc<Self> {
|
||||||
|
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);
|
const DURATION: instant::Duration = instant::Duration::from_millis(10000);
|
||||||
|
|
||||||
|
// Helper just for testing
|
||||||
|
fn get_prop_value<T: Clone>(prop: &Property<T>) -> T {
|
||||||
|
unsafe { Pin::new_unchecked(prop).get() }
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn properties_test_animation_triggered_by_set() {
|
fn properties_test_animation_triggered_by_set() {
|
||||||
fn g(prop: &Property<i32>) -> i32 {
|
let compo = Component::new_test_component();
|
||||||
unsafe { Pin::new_unchecked(prop).get() }
|
|
||||||
}
|
|
||||||
|
|
||||||
let compo = Rc::new(Component::default());
|
let animation_details =
|
||||||
|
PropertyAnimation { duration: DURATION.as_millis() as _, loop_count: 0 };
|
||||||
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 _ };
|
|
||||||
|
|
||||||
compo.width.set(100);
|
compo.width.set(100);
|
||||||
assert_eq!(g(&compo.width), 100);
|
assert_eq!(get_prop_value(&compo.width), 100);
|
||||||
assert_eq!(g(&compo.width_times_two), 200);
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
||||||
|
|
||||||
let start_time =
|
let start_time =
|
||||||
crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| driver.current_tick());
|
crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| driver.current_tick());
|
||||||
|
|
||||||
compo.width.set_animated_value(200, &animation_details);
|
compo.width.set_animated_value(200, &animation_details);
|
||||||
assert_eq!(g(&compo.width), 100);
|
assert_eq!(get_prop_value(&compo.width), 100);
|
||||||
assert_eq!(g(&compo.width_times_two), 200);
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
||||||
|
|
||||||
crate::animations::CURRENT_ANIMATION_DRIVER
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
||||||
.with(|driver| driver.update_animations(start_time + DURATION / 2));
|
.with(|driver| driver.update_animations(start_time + DURATION / 2));
|
||||||
assert_eq!(g(&compo.width), 150);
|
assert_eq!(get_prop_value(&compo.width), 150);
|
||||||
assert_eq!(g(&compo.width_times_two), 300);
|
assert_eq!(get_prop_value(&compo.width_times_two), 300);
|
||||||
|
|
||||||
crate::animations::CURRENT_ANIMATION_DRIVER
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
||||||
.with(|driver| driver.update_animations(start_time + DURATION));
|
.with(|driver| driver.update_animations(start_time + DURATION));
|
||||||
assert_eq!(g(&compo.width), 200);
|
assert_eq!(get_prop_value(&compo.width), 200);
|
||||||
assert_eq!(g(&compo.width_times_two), 400);
|
assert_eq!(get_prop_value(&compo.width_times_two), 400);
|
||||||
crate::animations::CURRENT_ANIMATION_DRIVER
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
||||||
.with(|driver| driver.update_animations(start_time + DURATION * 2));
|
.with(|driver| driver.update_animations(start_time + DURATION * 2));
|
||||||
assert_eq!(g(&compo.width), 200);
|
assert_eq!(get_prop_value(&compo.width), 200);
|
||||||
assert_eq!(g(&compo.width_times_two), 400);
|
assert_eq!(get_prop_value(&compo.width_times_two), 400);
|
||||||
|
|
||||||
// the binding should be removed
|
// the binding should be removed
|
||||||
compo.width.handle.access(|binding| assert!(binding.is_none()));
|
compo.width.handle.access(|binding| assert!(binding.is_none()));
|
||||||
|
@ -947,50 +968,116 @@ mod animation_tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn properties_test_animation_triggered_by_binding() {
|
fn properties_test_animation_triggered_by_binding() {
|
||||||
fn g(prop: &Property<i32>) -> i32 {
|
let compo = Component::new_test_component();
|
||||||
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 start_time =
|
let start_time =
|
||||||
crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| driver.current_tick());
|
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);
|
let w = Rc::downgrade(&compo);
|
||||||
compo.width.set_animated_binding(
|
compo.width.set_animated_binding(
|
||||||
move || {
|
move || {
|
||||||
let compo = w.upgrade().unwrap();
|
let compo = w.upgrade().unwrap();
|
||||||
g(&compo.feed_property)
|
get_prop_value(&compo.feed_property)
|
||||||
},
|
},
|
||||||
&animation_details,
|
&animation_details,
|
||||||
);
|
);
|
||||||
|
|
||||||
compo.feed_property.set(100);
|
compo.feed_property.set(100);
|
||||||
assert_eq!(g(&compo.width), 100);
|
assert_eq!(get_prop_value(&compo.width), 100);
|
||||||
assert_eq!(g(&compo.width_times_two), 200);
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
||||||
|
|
||||||
compo.feed_property.set(200);
|
compo.feed_property.set(200);
|
||||||
assert_eq!(g(&compo.width), 100);
|
assert_eq!(get_prop_value(&compo.width), 100);
|
||||||
assert_eq!(g(&compo.width_times_two), 200);
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
||||||
|
|
||||||
crate::animations::CURRENT_ANIMATION_DRIVER
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
||||||
.with(|driver| driver.update_animations(start_time + DURATION / 2));
|
.with(|driver| driver.update_animations(start_time + DURATION / 2));
|
||||||
|
|
||||||
assert_eq!(g(&compo.width), 150);
|
assert_eq!(get_prop_value(&compo.width), 150);
|
||||||
assert_eq!(g(&compo.width_times_two), 300);
|
assert_eq!(get_prop_value(&compo.width_times_two), 300);
|
||||||
|
|
||||||
crate::animations::CURRENT_ANIMATION_DRIVER
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
||||||
.with(|driver| driver.update_animations(start_time + DURATION));
|
.with(|driver| driver.update_animations(start_time + DURATION));
|
||||||
|
|
||||||
assert_eq!(g(&compo.width), 200);
|
assert_eq!(get_prop_value(&compo.width), 200);
|
||||||
assert_eq!(g(&compo.width_times_two), 400);
|
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()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue