mirror of
https://github.com/slint-ui/slint.git
synced 2025-09-08 03:20:34 +00:00

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.
968 lines
38 KiB
Rust
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);
|
|
}
|
|
}
|