slint/internal/core/properties/properties_animations.rs
Simon Hausmann c16253d29f Use Property<LogicalLength> instead of Property<Coord>
This removes the special code for the generated property getters and
ensures type safety in the run-time library for property value setting.

In the Rust generated code we continue to do arithmetic on the scalar
values, that means we immediately extract the scalar, do arithmetic and
rely on the compiler to only allow compatible units.

Danger zone alert: In the interpreter Value::Number can now be converted
to LogicalLength as-is.
2022-10-24 12:49:37 +02:00

968 lines
38 KiB
Rust

// Copyright © SixtyFPS GmbH <info@slint-ui.com>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
use super::*;
use crate::{items::PropertyAnimation, lengths::LogicalLength};
enum AnimationState {
Delaying,
Animating { current_iteration: u64 },
Done,
}
pub(super) struct PropertyValueAnimationData<T> {
from_value: T,
to_value: T,
details: PropertyAnimation,
start_time: crate::animations::Instant,
state: AnimationState,
}
impl<T: InterpolatedPropertyValue + Clone> PropertyValueAnimationData<T> {
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 as u64);
}
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<T, A> {
pub(super) original_binding: PropertyHandle,
pub(super) state: Cell<AnimatedBindingState>,
pub(super) animation_data: RefCell<PropertyValueAnimationData<T>>,
pub(super) compute_animation_details: A,
}
pub(super) type AnimationDetail = Option<(PropertyAnimation, crate::animations::Instant)>;
unsafe impl<T: InterpolatedPropertyValue + Clone, A: Fn() -> AnimationDetail> BindingCallable
for AnimatedBindingCallable<T, A>
{
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)]
"<AnimatedBindingCallable>",
);
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) as i32
}
}
impl InterpolatedPropertyValue for i64 {
fn interpolate(&self, target_value: &Self, t: f32) -> Self {
self + (t * (target_value - self) as f32) 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)))).min(255.).max(0.) 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<T: Clone + InterpolatedPropertyValue + 'static> Property<T> {
/// 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<T> + 'static,
animation_data: PropertyAnimation,
) {
let binding_callable = properties_animations::AnimatedBindingCallable::<T, _> {
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<T> + 'static,
compute_animation_details: impl Fn() -> (PropertyAnimation, crate::animations::Instant)
+ 'static,
) {
let binding_callable = properties_animations::AnimatedBindingCallable::<T, _> {
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::*;
use crate::items::PropertyAnimation;
use std::rc::Rc;
#[derive(Default)]
struct Component {
width: Property<i32>,
width_times_two: Property<i32>,
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 DELAY: instant::Duration = instant::Duration::from_millis(800);
// Helper just for testing
fn get_prop_value<T: Clone>(prop: &Property<T>) -> 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);
}
}