cranpose_animation/
transition.rs1#![allow(non_snake_case)]
22
23use std::{cell::RefCell, rc::Rc};
24
25use cranpose_core::{DisposableEffectResult, Owned, State, with_current_composer};
26use cranpose_ui_graphics::{Color, Dp};
27
28use crate::animation::{Animatable, AnimationType, SpringScalar};
29
30trait TransitionChild {
31 fn is_running(&self) -> bool;
32}
33
34impl<T: SpringScalar + 'static> TransitionChild for Animatable<T> {
35 fn is_running(&self) -> bool {
36 Animatable::is_running(self)
37 }
38}
39
40struct TransitionInner<S> {
41 target_state: RefCell<S>,
42 children: RefCell<Vec<Rc<dyn TransitionChild>>>,
43}
44
45impl<S> TransitionInner<S> {
46 fn add_child(&self, child: Rc<dyn TransitionChild>) {
47 self.children.borrow_mut().push(child);
48 }
49
50 fn remove_child(&self, child: &Rc<dyn TransitionChild>) {
51 let mut children = self.children.borrow_mut();
52 if let Some(index) = children
53 .iter()
54 .position(|existing| Rc::ptr_eq(existing, child))
55 {
56 children.remove(index);
57 }
58 }
59
60 fn is_running(&self) -> bool {
61 self.children
62 .borrow()
63 .iter()
64 .any(|child| child.is_running())
65 }
66}
67
68pub struct Transition<S: 'static> {
73 inner: Rc<TransitionInner<S>>,
74}
75
76impl<S> Clone for Transition<S> {
77 fn clone(&self) -> Self {
78 Self {
79 inner: Rc::clone(&self.inner),
80 }
81 }
82}
83
84impl<S: Clone + 'static> Transition<S> {
85 fn new(target_state: S) -> Self {
86 Self {
87 inner: Rc::new(TransitionInner {
88 target_state: RefCell::new(target_state),
89 children: RefCell::new(Vec::new()),
90 }),
91 }
92 }
93
94 fn set_target_state(&self, target_state: S) {
95 *self.inner.target_state.borrow_mut() = target_state;
96 }
97
98 pub fn target_state(&self) -> S {
100 self.inner.target_state.borrow().clone()
101 }
102
103 pub fn is_running(&self) -> bool {
107 self.inner.is_running()
108 }
109
110 #[track_caller]
116 pub fn animateValue<T: SpringScalar + PartialEq + 'static>(
117 &self,
118 target_value: T,
119 animation: AnimationType,
120 label: &str,
121 ) -> State<T> {
122 let _ = label;
123 let caller = cranpose_core::caller_location_key();
124 with_current_composer(|composer| {
125 let runtime = composer.runtime_handle();
126 let anim: Owned<Animatable<T>> = composer.remember_at(caller, || {
127 Animatable::new_with_animation(target_value.clone(), animation, runtime)
128 });
129 anim.update(|animatable| {
130 let is_new_target = animatable.target() != target_value;
131 let is_new_animation = animatable.animation_type() != animation;
132 if is_new_target || is_new_animation {
133 animatable.animateTo(target_value.clone(), animation);
134 }
135 });
136
137 let animatable_clone = anim.with(|animatable| animatable.clone());
138 let identity = animatable_clone.identity();
139 let transition_inner = Rc::clone(&self.inner);
140 cranpose_core::__disposable_effect_impl(
141 caller ^ cranpose_core::location_key(file!(), line!(), column!()),
142 identity,
143 move |_scope| {
144 let child: Rc<dyn TransitionChild> = Rc::new(animatable_clone);
145 transition_inner.add_child(Rc::clone(&child));
146 let transition_inner = Rc::clone(&transition_inner);
147 DisposableEffectResult::new(move || {
148 transition_inner.remove_child(&child);
149 })
150 },
151 );
152
153 anim.with(|animatable| animatable.state())
154 })
155 }
156
157 #[track_caller]
159 pub fn animateFloat(
160 &self,
161 target_value: f32,
162 animation: AnimationType,
163 label: &str,
164 ) -> State<f32> {
165 self.animateValue(target_value, animation, label)
166 }
167
168 #[track_caller]
171 pub fn animateDp(&self, target_value: Dp, animation: AnimationType, label: &str) -> State<Dp> {
172 self.animateValue(target_value, animation, label)
173 }
174
175 #[track_caller]
177 pub fn animateColor(
178 &self,
179 target_value: Color,
180 animation: AnimationType,
181 label: &str,
182 ) -> State<Color> {
183 self.animateValue(target_value, animation, label)
184 }
185}
186
187#[track_caller]
194pub fn updateTransition<S: Clone + 'static>(target_state: S, label: &str) -> Transition<S> {
195 let _ = label;
196 let caller = cranpose_core::caller_location_key();
197 with_current_composer(|composer| {
198 let transition: Owned<Transition<S>> =
199 composer.remember_at(caller, || Transition::new(target_state.clone()));
200 transition.with(|transition| transition.set_target_state(target_state.clone()));
201 transition.with(|transition| transition.clone())
202 })
203}
204
205#[cfg(test)]
206#[path = "tests/transition_tests.rs"]
207mod tests;