#![allow(non_snake_case)]
use std::{cell::RefCell, rc::Rc};
use cranpose_core::{DisposableEffectResult, Owned, State, with_current_composer};
use cranpose_ui_graphics::{Color, Dp};
use crate::animation::{Animatable, AnimationType, SpringScalar};
trait TransitionChild {
fn is_running(&self) -> bool;
}
impl<T: SpringScalar + 'static> TransitionChild for Animatable<T> {
fn is_running(&self) -> bool {
Animatable::is_running(self)
}
}
struct TransitionInner<S> {
target_state: RefCell<S>,
children: RefCell<Vec<Rc<dyn TransitionChild>>>,
}
impl<S> TransitionInner<S> {
fn add_child(&self, child: Rc<dyn TransitionChild>) {
self.children.borrow_mut().push(child);
}
fn remove_child(&self, child: &Rc<dyn TransitionChild>) {
let mut children = self.children.borrow_mut();
if let Some(index) = children
.iter()
.position(|existing| Rc::ptr_eq(existing, child))
{
children.remove(index);
}
}
fn is_running(&self) -> bool {
self.children
.borrow()
.iter()
.any(|child| child.is_running())
}
}
pub struct Transition<S: 'static> {
inner: Rc<TransitionInner<S>>,
}
impl<S> Clone for Transition<S> {
fn clone(&self) -> Self {
Self {
inner: Rc::clone(&self.inner),
}
}
}
impl<S: Clone + 'static> Transition<S> {
fn new(target_state: S) -> Self {
Self {
inner: Rc::new(TransitionInner {
target_state: RefCell::new(target_state),
children: RefCell::new(Vec::new()),
}),
}
}
fn set_target_state(&self, target_state: S) {
*self.inner.target_state.borrow_mut() = target_state;
}
pub fn target_state(&self) -> S {
self.inner.target_state.borrow().clone()
}
pub fn is_running(&self) -> bool {
self.inner.is_running()
}
#[track_caller]
pub fn animateValue<T: SpringScalar + PartialEq + 'static>(
&self,
target_value: T,
animation: AnimationType,
label: &str,
) -> State<T> {
let _ = label;
let caller = cranpose_core::caller_location_key();
with_current_composer(|composer| {
let runtime = composer.runtime_handle();
let anim: Owned<Animatable<T>> = composer.remember_at(caller, || {
Animatable::new_with_animation(target_value.clone(), animation, runtime)
});
anim.update(|animatable| {
let is_new_target = animatable.target() != target_value;
let is_new_animation = animatable.animation_type() != animation;
if is_new_target || is_new_animation {
animatable.animateTo(target_value.clone(), animation);
}
});
let animatable_clone = anim.with(|animatable| animatable.clone());
let identity = animatable_clone.identity();
let transition_inner = Rc::clone(&self.inner);
cranpose_core::__disposable_effect_impl(
caller ^ cranpose_core::location_key(file!(), line!(), column!()),
identity,
move |_scope| {
let child: Rc<dyn TransitionChild> = Rc::new(animatable_clone);
transition_inner.add_child(Rc::clone(&child));
let transition_inner = Rc::clone(&transition_inner);
DisposableEffectResult::new(move || {
transition_inner.remove_child(&child);
})
},
);
anim.with(|animatable| animatable.state())
})
}
#[track_caller]
pub fn animateFloat(
&self,
target_value: f32,
animation: AnimationType,
label: &str,
) -> State<f32> {
self.animateValue(target_value, animation, label)
}
#[track_caller]
pub fn animateDp(&self, target_value: Dp, animation: AnimationType, label: &str) -> State<Dp> {
self.animateValue(target_value, animation, label)
}
#[track_caller]
pub fn animateColor(
&self,
target_value: Color,
animation: AnimationType,
label: &str,
) -> State<Color> {
self.animateValue(target_value, animation, label)
}
}
#[track_caller]
pub fn updateTransition<S: Clone + 'static>(target_state: S, label: &str) -> Transition<S> {
let _ = label;
let caller = cranpose_core::caller_location_key();
with_current_composer(|composer| {
let transition: Owned<Transition<S>> =
composer.remember_at(caller, || Transition::new(target_state.clone()));
transition.with(|transition| transition.set_target_state(target_state.clone()));
transition.with(|transition| transition.clone())
})
}
#[cfg(test)]
#[path = "tests/transition_tests.rs"]
mod tests;