Skip to main content

aura_anim_iced/behavior/
transition.rs

1use crate::{
2    ActivePropertyTransition, AnimationHandle, AnimationRuntime, AnimationTargetId, BehaviorRule,
3    Duration, PropertyTransitionProgress, PropertyTransitionRegistration, Timing,
4    behavior::TransitionValueKind, property::PropertySpec, runtime::AnimationClock,
5};
6
7/// Tracks one visual property and starts a transition when its target value changes.
8///
9/// The first observed value becomes the stable target baseline and does not
10/// start an animation. Later different values register a two-keyframe animation
11/// from the previous visual result to the new target value.
12///
13/// ```
14/// use aura_anim_iced::{
15///     AnimationRuntime, AnimationTargetId, Duration, OPACITY, PropertyTransition,
16///     PropertyValue, Timing,
17/// };
18///
19/// let mut runtime = AnimationRuntime::testing();
20/// let target = AnimationTargetId::new();
21/// let mut opacity = PropertyTransition::new(target, OPACITY)
22///     .with_timing(Timing::new(100.0));
23///
24/// opacity.transition_to(&mut runtime, 0.0);
25/// let first = opacity.transition_to(&mut runtime, 1.0).unwrap();
26///
27/// runtime.clock_mut().set_now(Duration::from_millis(40.0));
28/// runtime.tick();
29///
30/// // Retargeting starts from the active animation's sampled visual value.
31/// let retargeted = opacity.retarget_to(&mut runtime, 0.25).unwrap();
32/// assert_eq!(retargeted.replaced(), Some(first.handle()));
33///
34/// let start = retargeted.registration().properties().unwrap();
35/// let entry = start.find_property(&OPACITY.raw()).unwrap();
36/// assert!(matches!(entry.value(), PropertyValue::Scalar(value) if (*value - 0.4).abs() < 0.001));
37/// ```
38#[derive(Debug, Clone, PartialEq)]
39pub struct PropertyTransition<K: TransitionValueKind>
40where
41    K::Inner: Copy + PartialEq,
42{
43    target: AnimationTargetId,
44    property: PropertySpec<K>,
45    timing: Timing,
46    current: Option<K::Inner>,
47    active: Option<ActivePropertyTransition<K>>,
48}
49
50// TODO: may need optimization
51impl<K> PropertyTransition<K>
52where
53    K: TransitionValueKind,
54    K::Inner: Copy + PartialEq,
55{
56    /// Creates a property transition tracker with default timing.
57    #[must_use]
58    pub fn new(target: AnimationTargetId, property: PropertySpec<K>) -> Self {
59        Self::from_rule(target, &BehaviorRule::new(property))
60    }
61
62    /// Creates a property transition tracker from a reusable behavior rule.
63    #[must_use]
64    pub const fn from_rule(target: AnimationTargetId, rule: &BehaviorRule<K>) -> Self {
65        Self {
66            target,
67            property: rule.property(),
68            timing: rule.timing(),
69            current: None,
70            active: None,
71        }
72    }
73
74    /// Replaces the timing used for newly registered transitions.
75    #[must_use]
76    pub const fn with_timing(mut self, timing: Timing) -> Self {
77        self.timing = timing;
78        self
79    }
80
81    /// Returns the target that receives transition animations.
82    #[must_use]
83    pub const fn target(&self) -> AnimationTargetId {
84        self.target
85    }
86
87    /// Returns the tracked property.
88    #[must_use]
89    pub const fn property(&self) -> PropertySpec<K> {
90        self.property
91    }
92
93    /// Returns the timing used for newly registered transitions.
94    #[must_use]
95    pub const fn timing(&self) -> Timing {
96        self.timing
97    }
98
99    /// Returns the last target value observed by this tracker.
100    #[must_use]
101    pub const fn current_value(&self) -> Option<K::Inner> {
102        self.current
103    }
104
105    /// Returns the active runtime handle created by this tracker, if any.
106    #[must_use]
107    pub const fn active_handle(&self) -> Option<AnimationHandle> {
108        match &self.active {
109            Some(active) => Some(active.handle()),
110            None => None,
111        }
112    }
113
114    /// Returns whether this tracker currently owns a runtime animation handle.
115    #[must_use]
116    pub fn is_active<C: AnimationClock>(&self, runtime: &AnimationRuntime<C>) -> bool {
117        match &self.active {
118            Some(active) => runtime.contains(self.target, active.handle()),
119            None => false,
120        }
121    }
122
123    /// Clears this tracker's active handle when it appears in completed output.
124    ///
125    /// Returns `true` when this transition handled its own completion.
126    pub fn handle_completion<C: AnimationClock>(&mut self, runtime: &AnimationRuntime<C>) -> bool {
127        let Some(active) = &self.active else {
128            return false;
129        };
130
131        if runtime.contains(self.target, active.handle()) {
132            return false;
133        }
134
135        self.active = None;
136        true
137    }
138
139    /// Returns metadata for the active property transition, if any.
140    #[must_use]
141    pub const fn active_transition(&self) -> Option<&ActivePropertyTransition<K>> {
142        self.active.as_ref()
143    }
144
145    /// Returns sampled progress for the active transition at `timestamp`.
146    #[must_use]
147    pub fn active_progress_at(&self, timestamp: Duration) -> Option<PropertyTransitionProgress<K>> {
148        self.active
149            .as_ref()
150            .map(|active| active.progress_at(timestamp))
151    }
152
153    /// Observes a new target value and registers an animation when it changed.
154    ///
155    /// Returns `None` when the value only seeded the baseline or did not change.
156    /// If a previous transition is still running, the replacement starts from
157    /// that transition's last sampled visual value.
158    pub fn transition_to<C: AnimationClock>(
159        &mut self,
160        runtime: &mut AnimationRuntime<C>,
161        value: K::Inner,
162    ) -> Option<PropertyTransitionRegistration> {
163        self.invalidate_if_stale(runtime);
164
165        let Some(previous) = self.current else {
166            self.current = Some(value);
167            return None;
168        };
169
170        if previous == value {
171            return None;
172        }
173
174        let from = self.current_visual_value(runtime).unwrap_or(previous);
175
176        Some(self.register_from(runtime, from, value))
177    }
178
179    /// Observes a new target value with an explicit current visual value.
180    ///
181    /// This is useful when application code already stores the rendered value
182    /// from the latest tick. The registered animation starts from `visual`
183    /// even if the previous target or runtime handle no longer represents what
184    /// is currently on screen.
185    pub fn transition_from_visual<C: AnimationClock>(
186        &mut self,
187        runtime: &mut AnimationRuntime<C>,
188        visual: K::Inner,
189        value: K::Inner,
190    ) -> Option<PropertyTransitionRegistration> {
191        self.invalidate_if_stale(runtime);
192
193        if self.current == Some(value) {
194            return None;
195        }
196
197        self.current = Some(value);
198
199        if visual == value {
200            return None;
201        }
202
203        Some(self.register_from(runtime, visual, value))
204    }
205
206    /// Retargets a currently running transition to a new destination.
207    ///
208    /// Returns `None` when there is no active runtime animation, the active
209    /// handle is stale, or the new destination is already the current target.
210    /// The replacement animation starts from the active animation's last
211    /// sampled visual value.
212    pub fn retarget_to<C: AnimationClock>(
213        &mut self,
214        runtime: &mut AnimationRuntime<C>,
215        value: K::Inner,
216    ) -> Option<PropertyTransitionRegistration> {
217        self.invalidate_if_stale(runtime);
218
219        if self.current == Some(value) {
220            return None;
221        }
222
223        let from = self.current_visual_value(runtime)?;
224
225        Some(self.register_from(runtime, from, value))
226    }
227
228    /// Interrupts the current transition and continues from an explicit visual value.
229    ///
230    /// Unlike [`transition_from_visual`](Self::transition_from_visual), this
231    /// method can replace an active animation even when `value` is already the
232    /// current target. This is useful when an external interaction interrupts
233    /// playback and application code wants the replacement animation to start
234    /// from the exact value currently rendered on screen.
235    pub fn interrupt_from_visual<C: AnimationClock>(
236        &mut self,
237        runtime: &mut AnimationRuntime<C>,
238        visual: K::Inner,
239        value: K::Inner,
240    ) -> Option<PropertyTransitionRegistration> {
241        self.invalidate_if_stale(runtime);
242
243        if self.active.is_none() && self.current == Some(value) {
244            return None;
245        }
246
247        self.current = Some(value);
248
249        if visual == value {
250            if let Some(active) = self.active.take() {
251                runtime.cancel(self.target, active.handle());
252            }
253
254            return None;
255        }
256
257        Some(self.register_from(runtime, visual, value))
258    }
259
260    fn register_from<C: AnimationClock>(
261        &mut self,
262        runtime: &mut AnimationRuntime<C>,
263        from: K::Inner,
264        value: K::Inner,
265    ) -> PropertyTransitionRegistration {
266        let replaced = self.active.take();
267        let replaced_handle = replaced.as_ref().map(ActivePropertyTransition::handle);
268
269        self.current = Some(value);
270
271        let registration = runtime.register_property_transition(
272            self.target,
273            self.property,
274            self.timing,
275            from,
276            value,
277        );
278
279        self.active = Some(ActivePropertyTransition::new(
280            registration.handle(),
281            from,
282            value,
283            runtime.clock().now(),
284            self.timing.total_duration(),
285        ));
286
287        self.cleanup_replaced(runtime, replaced);
288
289        PropertyTransitionRegistration::new(registration, replaced_handle)
290    }
291
292    fn cleanup_replaced<C: AnimationClock>(
293        &self,
294        runtime: &mut AnimationRuntime<C>,
295        replaced: Option<ActivePropertyTransition<K>>,
296    ) {
297        if let Some(active) = replaced {
298            runtime.cancel(self.target, active.handle());
299        }
300    }
301
302    fn current_visual_value<C: AnimationClock>(
303        &self,
304        runtime: &AnimationRuntime<C>,
305    ) -> Option<K::Inner> {
306        let active = self.active.as_ref()?;
307        let snapshot = runtime.last_properties(self.target, active.handle())?;
308        let entry = snapshot.find_property(&self.property.raw())?;
309
310        K::unwrap_transition_value(entry.value())
311    }
312
313    fn invalidate_if_stale<C: AnimationClock>(&mut self, runtime: &AnimationRuntime<C>) {
314        if let Some(active) = &self.active
315            && !runtime.contains(self.target, active.handle())
316        {
317            self.active = None;
318        }
319    }
320}