Skip to main content

ferroui_base/animation/
animation.rs

1use crate::animation::cancellation::register_local;
2use crate::animation::easings::Easing;
3use crate::animation::i_page_transition::continuation_context;
4use crate::animation::key_frame::KeyFrameTimingMode;
5use crate::animation::{
6    Animatable, AnimationError, AnimationTask, AnimatorFactory, AnimatorKeyFrame, Cue, FillMode,
7    IAnimator, IClock, IterationCount, KeyFrames, PlaybackBehavior, PlaybackDirection, TimeSpan,
8};
9use crate::data::{BindingMode, BindingPriority};
10use crate::reactive::{
11    CompositeDisposable, Disposable, IDisposable, IObservable, Observable, SerialDisposable,
12};
13use crate::threading::CancellationToken;
14use crate::{
15    ferro_class, ferro_impl_classes, ferro_property, instantiate, DirectProperty,
16    DirectPropertyMetadata, FerroObject, FerroObjectImpl, FerroProperty, Ref, Visual, WeakRef,
17};
18use std::any::TypeId;
19use std::cell::{Cell, RefCell};
20use std::rc::{Rc, Weak};
21
22struct PreviewTransform {
23    target: WeakRef<Visual>,
24    transform: Rc<dyn crate::media::ITransform>,
25    value: SerialDisposable,
26}
27impl Drop for PreviewTransform {
28    fn drop(&mut self) {
29        self.value.dispose();
30    }
31}
32crate::ferro_thread_local! {
33    // Weak entries allow separately applied preview programs to share a generated
34    // transform without extending the life of the target or any completed preview.
35    static PREVIEW_TRANSFORMS: RefCell<Vec<Weak<PreviewTransform>>> = RefCell::new(Vec::new());
36}
37
38/// Tracks the progress of an animation.
39#[repr(C)]
40pub struct Animation {
41    base: FerroObject,
42    duration: Cell<TimeSpan>,
43    iteration_count: Cell<IterationCount>,
44    playback_direction: Cell<PlaybackDirection>,
45    playback_behavior: Cell<PlaybackBehavior>,
46    fill_mode: Cell<FillMode>,
47    easing: RefCell<Easing>,
48    delay: Cell<TimeSpan>,
49    delay_between_iterations: Cell<TimeSpan>,
50    speed_ratio: Cell<f64>,
51    children: KeyFrames,
52    preview_values: RefCell<Option<Rc<CompositeDisposable>>>,
53}
54
55ferro_class!(Animation: FerroObject);
56crate::ferro_class_info!(Animation { new: Animation::new, interfaces: [Rc<dyn crate::animation::IAnimation>] });
57ferro_impl_classes!(Animation: FerroObjectImpl);
58
59macro_rules! direct_cell_property {
60    ($(#[$meta:meta])* $accessor:ident, $name:literal, $ty:ty, $getter:ident, $setter:ident, $field:ident, $unset:expr) => {
61        ferro_property!(for Animation;
62            $(#[$meta])*
63            pub fn $accessor() -> DirectProperty<Animation, $ty> {
64                FerroProperty::register_direct::<Animation, _>($name, |o| o.$getter(), Some(|o, v| o.$setter(v)), $unset)
65            }
66        );
67
68        pub fn $getter(&self) -> $ty {
69            self.$field.get()
70        }
71
72        pub fn $setter(&self, value: $ty) {
73            self.set_and_raise_cell(Self::$accessor(), &self.$field, value);
74        }
75    };
76}
77
78crate::ferro_properties! {
79    impl Animation, also [
80        Animation::duration_property,
81        Animation::iteration_count_property,
82        Animation::playback_direction_property,
83        Animation::playback_behavior_property,
84        Animation::fill_mode_property,
85        Animation::delay_property,
86        Animation::delay_between_iterations_property,
87        Animation::easing_property,
88        Animation::speed_ratio_property,
89    ] {}
90}
91
92impl Animation {
93    direct_cell_property!(
94        /// Defines the `Duration` property: the active time of one iteration.
95        duration_property, "Duration", TimeSpan, duration, set_duration, duration, TimeSpan::ZERO
96    );
97
98    direct_cell_property!(
99        /// Defines the `IterationCount` property: the repeat count.
100        iteration_count_property, "IterationCount", IterationCount, iteration_count, set_iteration_count,
101        iteration_count, IterationCount::new(1)
102    );
103
104    direct_cell_property!(
105        /// Defines the `PlaybackDirection` property.
106        playback_direction_property, "PlaybackDirection", PlaybackDirection, playback_direction,
107        set_playback_direction, playback_direction, PlaybackDirection::Normal
108    );
109
110    direct_cell_property!(
111        /// Defines the `PlaybackBehavior` property: whether the animation
112        /// pauses when its target is not effectively visible.
113        playback_behavior_property, "PlaybackBehavior", PlaybackBehavior, playback_behavior,
114        set_playback_behavior, playback_behavior, PlaybackBehavior::Auto
115    );
116
117    direct_cell_property!(
118        /// Defines the `FillMode` property: the value fill mode.
119        fill_mode_property, "FillMode", FillMode, fill_mode, set_fill_mode, fill_mode, FillMode::None
120    );
121
122    direct_cell_property!(
123        /// Defines the `Delay` property: the initial delay time.
124        delay_property, "Delay", TimeSpan, delay, set_delay, delay, TimeSpan::ZERO
125    );
126
127    direct_cell_property!(
128        /// Defines the `DelayBetweenIterations` property.
129        delay_between_iterations_property, "DelayBetweenIterations", TimeSpan, delay_between_iterations,
130        set_delay_between_iterations, delay_between_iterations, TimeSpan::ZERO
131    );
132
133    ferro_property!(for Animation;
134        /// Defines the `Easing` property: the easing function to be used.
135        pub fn easing_property() -> DirectProperty<Animation, Easing> {
136            FerroProperty::register_direct::<Animation, _>(
137                "Easing",
138                |o| o.easing(),
139                Some(|o, v| o.set_easing(v)),
140                Easing::default(),
141            )
142        }
143    );
144
145    ferro_property!(for Animation;
146        /// Defines the `SpeedRatio` property: the speed multiple.
147        pub fn speed_ratio_property() -> DirectProperty<Animation, f64> {
148            FerroProperty::register_direct_with::<Animation, _>(
149                "SpeedRatio",
150                |o| o.speed_ratio(),
151                Some(|o, v| o.set_speed_ratio(v)),
152                DirectPropertyMetadata::new(Some(1.0)).with_default_binding_mode(BindingMode::TwoWay),
153            )
154        }
155    );
156
157    /// Creates the class data; see [`FerroObject::construct`].
158    pub fn construct() -> Self {
159        Self {
160            base: FerroObject::construct(),
161            duration: Cell::new(TimeSpan::ZERO),
162            iteration_count: Cell::new(IterationCount::new(1)),
163            playback_direction: Cell::new(PlaybackDirection::Normal),
164            playback_behavior: Cell::new(PlaybackBehavior::Auto),
165            fill_mode: Cell::new(FillMode::None),
166            easing: RefCell::new(Easing::default()),
167            delay: Cell::new(TimeSpan::ZERO),
168            delay_between_iterations: Cell::new(TimeSpan::ZERO),
169            speed_ratio: Cell::new(1.0),
170            children: KeyFrames::new(),
171            preview_values: RefCell::new(None),
172        }
173    }
174
175    pub fn new() -> Ref<Self> {
176        instantiate(Self::construct())
177    }
178
179    /// The easing function to be used for this animation.
180    pub fn easing(&self) -> Easing {
181        self.easing.borrow().clone()
182    }
183
184    pub fn set_easing(&self, value: impl Into<Easing>) {
185        self.set_and_raise(Self::easing_property(), &self.easing, value.into());
186    }
187
188    /// The speed multiple for this animation.
189    pub fn speed_ratio(&self) -> f64 {
190        self.speed_ratio.get()
191    }
192
193    pub fn set_speed_ratio(&self, value: f64) {
194        self.set_and_raise_cell(Self::speed_ratio_property(), &self.speed_ratio, value);
195    }
196
197    /// The children of the animation: its key frames.
198    pub fn children(&self) -> KeyFrames {
199        self.children.clone()
200    }
201
202    #[allow(clippy::type_complexity)]
203    fn interpret_keyframes(
204        &self,
205        control: &Animatable,
206    ) -> (Vec<Rc<dyn IAnimator>>, Vec<Rc<dyn IDisposable>>, bool) {
207        let mut handler_list: Vec<((TypeId, &'static FerroProperty), AnimatorFactory)> = Vec::new();
208        let mut animator_key_frames: Vec<Ref<AnimatorKeyFrame>> = Vec::new();
209        let mut subscriptions: Vec<Rc<dyn IDisposable>> = Vec::new();
210        let mut animates_visibility = false;
211
212        for keyframe in self.children.snapshot().iter() {
213            for setter in keyframe.setters().snapshot().iter() {
214                let Some(property) = setter.property() else {
215                    panic!("No Setter property assigned.");
216                };
217
218                if property == Visual::is_visible_property().as_property() {
219                    animates_visibility = true;
220                }
221
222                let handler = Animation::get_animator(setter)
223                    .or_else(|| Animation::get_animator_type(property));
224
225                let Some((type_, factory)) = handler else {
226                    panic!(
227                        "No animator registered for the property {property}. Add an animator to the Animation.Animators collection that matches this property to animate it."
228                    );
229                };
230
231                if !handler_list
232                    .iter()
233                    .any(|((t, p), _)| *t == type_ && *p == property)
234                {
235                    handler_list.push(((type_, property), factory.clone()));
236                }
237
238                let mut cue = keyframe.cue();
239
240                if keyframe.timing_mode() == KeyFrameTimingMode::TimeSpan {
241                    cue = Cue::new(
242                        keyframe.key_time().total_seconds() / self.duration().total_seconds(),
243                    );
244                }
245
246                let new_kf =
247                    AnimatorKeyFrame::new(Some(type_), Some(factory), cue, keyframe.key_spline());
248
249                let subscription = new_kf.bind_setter(&**setter, control);
250                self.retain_preview_subscription(&subscription);
251                subscriptions.push(subscription);
252
253                animator_key_frames.push(new_kf);
254            }
255        }
256
257        animator_key_frames.sort_by(|x, y| x.cue().cue_value().total_cmp(&y.cue().cue_value()));
258
259        let mut new_animator_instances: Vec<Rc<dyn IAnimator>> = Vec::new();
260
261        for ((_, property), factory) in &handler_list {
262            let new_instance = factory();
263            new_instance.set_property(Some(property));
264            new_animator_instances.push(new_instance);
265        }
266
267        let fill_mode = self.fill_mode();
268        for keyframe in animator_key_frames {
269            let animator = new_animator_instances
270                .iter()
271                .find(|a| {
272                    Some(a.animator_type()) == keyframe.animator_type()
273                        && a.property() == keyframe.property()
274                })
275                .expect("Sequence contains no matching element");
276
277            if animator.count() == 0 && matches!(fill_mode, FillMode::Backward | FillMode::Both) {
278                keyframe.set_fill_before(true);
279            }
280
281            animator.add(keyframe);
282        }
283
284        if matches!(fill_mode, FillMode::Forward | FillMode::Both) {
285            for new_animator_instance in &new_animator_instances {
286                let count = new_animator_instance.count();
287                if count > 0 {
288                    new_animator_instance.get(count - 1).set_fill_after(true);
289                }
290            }
291        }
292
293        (new_animator_instances, subscriptions, animates_visibility)
294    }
295
296    /// Applies the animation to `control`: it runs while `match_` is true,
297    /// on `clock` (or on the control's clock, or on the global clock).
298    /// `on_complete` runs when the animation ends by itself.
299    /// `is_manually_started` tells an animation started by code from one
300    /// started by a style. Disposing the result removes the animation.
301    pub fn apply(
302        &self,
303        control: &Animatable,
304        clock: Option<Rc<dyn IClock>>,
305        match_: Rc<dyn IObservable<bool>>,
306        on_complete: Option<Rc<dyn Fn()>>,
307        is_manually_started: bool,
308    ) -> Rc<dyn IDisposable> {
309        let (animators, mut subscriptions, animates_visibility) = self.interpret_keyframes(control);
310
311        let should_pause_on_invisible = match self.playback_behavior.get() {
312            PlaybackBehavior::Auto => !(animates_visibility || is_manually_started),
313            PlaybackBehavior::Always => false,
314            PlaybackBehavior::OnlyIfVisible => true,
315        };
316
317        if animators.len() == 1 {
318            let subscription = animators[0].clone().apply(
319                self,
320                control,
321                clock,
322                match_,
323                on_complete,
324                should_pause_on_invisible,
325            );
326
327            if let Some(subscription) = subscription {
328                self.retain_preview_subscription(&subscription);
329                subscriptions.push(subscription);
330            }
331        } else {
332            // The completion action runs once every animator has completed,
333            // through the synchronization context that is current now.
334            let remaining = Rc::new(Cell::new(animators.len()));
335            let schedule_on_complete = on_complete.map(|on_complete| {
336                let context = continuation_context();
337                Rc::new(move || {
338                    let on_complete = on_complete.clone();
339                    context.post_local(move || on_complete());
340                })
341            });
342
343            for animator in &animators {
344                let animator_on_complete: Option<Rc<dyn Fn()>> =
345                    schedule_on_complete.as_ref().map(|schedule| {
346                        let schedule = schedule.clone();
347                        let remaining = remaining.clone();
348                        let completed = Cell::new(false);
349                        let action: Rc<dyn Fn()> = Rc::new(move || {
350                            if completed.replace(true) {
351                                return;
352                            }
353                            remaining.set(remaining.get() - 1);
354                            if remaining.get() == 0 {
355                                schedule();
356                            }
357                        });
358                        action
359                    });
360
361                let subscription = animator.clone().apply(
362                    self,
363                    control,
364                    clock.clone(),
365                    match_.clone(),
366                    animator_on_complete,
367                    should_pause_on_invisible,
368                );
369
370                if let Some(subscription) = subscription {
371                    self.retain_preview_subscription(&subscription);
372                    subscriptions.push(subscription);
373                }
374            }
375
376            if animators.is_empty() {
377                if let Some(schedule) = &schedule_on_complete {
378                    schedule();
379                }
380            }
381        }
382
383        Rc::new(CompositeDisposable::from_disposables(subscriptions))
384    }
385
386    /// Applies an isolated snapshot using the framework animators, retaining final fill
387    /// and automatically created transforms at animation priority. Disposing the result
388    /// restores the control's underlying values and bindings, including after completion.
389    /// This is intended for interactive authoring and inspection; ordinary `apply` retains
390    /// its application semantics, including committing forward fill to a local value.
391    pub fn apply_for_preview(
392        &self,
393        control: &Animatable,
394        clock: Option<Rc<dyn IClock>>,
395        on_complete: Option<Rc<dyn Fn()>>,
396    ) -> Rc<dyn IDisposable> {
397        let preview = Animation::new();
398        preview.set_duration(self.duration());
399        preview.set_iteration_count(self.iteration_count());
400        preview.set_playback_direction(self.playback_direction());
401        preview.set_playback_behavior(self.playback_behavior());
402        preview.set_fill_mode(self.fill_mode());
403        preview.set_easing(self.easing());
404        preview.set_delay(self.delay());
405        preview.set_delay_between_iterations(self.delay_between_iterations());
406        preview.set_speed_ratio(self.speed_ratio());
407        preview
408            .children()
409            .add_range(self.children().snapshot().iter().cloned());
410        let values = Rc::new(CompositeDisposable::new());
411        *preview.preview_values.borrow_mut() = Some(values.clone());
412        let applied = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
413            // The application transform animator currently returns an empty subscription
414            // for CSS operation lists. A designer must surface that capability boundary
415            // instead of presenting an apparently successful but motionless preview.
416            if let Some(visual) = control.downcast_ref::<Visual>() {
417                if visual.render_transform().is_some_and(|value| {
418                    value
419                        .as_any()
420                        .is::<crate::media::transformation::TransformOperations>()
421                }) {
422                    for keyframe in preview.children().snapshot().iter() {
423                        for setter in keyframe.setters().snapshot().iter() {
424                            let handler = Animation::get_animator(setter).or_else(|| {
425                                setter.property().and_then(Animation::get_animator_type)
426                            });
427                            if handler.is_some_and(|(kind, _)| kind == TypeId::of::<crate::animation::animators::TransformAnimator>()) {
428                                panic!("The native component animator cannot target an existing CSS TransformOperations value; use a native TransformGroup or a registered transform animator.");
429                            }
430                        }
431                    }
432                }
433            }
434            preview.apply(
435                control,
436                clock,
437                Observable::single_value(true),
438                on_complete,
439                true,
440            )
441        }));
442        let applied = match applied {
443            Ok(applied) => applied,
444            Err(error) => {
445                // A redirected transform can be created before a later animator validates
446                // its timing or target. Remove every completed setup step on failure.
447                values.dispose();
448                std::panic::resume_unwind(error);
449            }
450        };
451        Disposable::create(move || {
452            // Mark the scope disposed before unsubscribing: ordinary forward-fill teardown
453            // must not author a final value while a preview is being removed.
454            values.dispose();
455            applied.dispose();
456            drop(preview);
457        })
458    }
459
460    pub(crate) fn retain_preview_subscription(&self, subscription: &Rc<dyn IDisposable>) {
461        if let Some(values) = self.preview_values.borrow().clone() {
462            values.add(subscription.clone());
463        }
464    }
465
466    pub(crate) fn is_preview(&self) -> bool {
467        self.preview_values.borrow().is_some()
468    }
469
470    pub(crate) fn create_preview_transform(
471        &self,
472        target: &Visual,
473        transform: Rc<dyn crate::media::ITransform>,
474    ) -> bool {
475        let values = self.preview_values.borrow().clone();
476        if let Some(values) = values {
477            if !values.is_disposed() {
478                let lease = Rc::new(PreviewTransform {
479                    target: target.to_ref().downgrade(),
480                    transform: transform.clone(),
481                    value: SerialDisposable::new(),
482                });
483                PREVIEW_TRANSFORMS.with(|entries| {
484                    let mut entries = entries.borrow_mut();
485                    entries.retain(|entry| entry.strong_count() > 0);
486                    entries.push(Rc::downgrade(&lease));
487                });
488                // Publishing the lease precedes the property notification: a callback
489                // may synchronously start another preview that reuses this transform.
490                let retained = lease.clone();
491                values.add(Disposable::create(move || drop(retained)));
492                lease.value.set(target.set_value_with_priority(
493                    Visual::render_transform_property(),
494                    Some(transform),
495                    BindingPriority::Animation,
496                ));
497            }
498            true
499        } else {
500            false
501        }
502    }
503
504    pub(crate) fn retain_preview_transform(
505        &self,
506        target: &Visual,
507        transform: &Rc<dyn crate::media::ITransform>,
508    ) {
509        let Some(values) = self.preview_values.borrow().clone() else {
510            return;
511        };
512        let lease = PREVIEW_TRANSFORMS.with(|entries| {
513            let mut entries = entries.borrow_mut();
514            entries.retain(|entry| entry.strong_count() > 0);
515            entries.iter().filter_map(Weak::upgrade).find(|entry| {
516                Rc::ptr_eq(&entry.transform, transform)
517                    && entry
518                        .target
519                        .upgrade()
520                        .is_some_and(|owner| std::ptr::eq(&*owner, target))
521            })
522        });
523        if let Some(lease) = lease {
524            values.add(Disposable::create(move || drop(lease)));
525        }
526    }
527
528    /// Starts the animation on `control` and returns a task that completes
529    /// when it has ended or `cancellation_token` was cancelled.
530    pub fn run_async(
531        &self,
532        control: &Animatable,
533        cancellation_token: CancellationToken,
534    ) -> AnimationTask {
535        self.run_async_with_clock(control, None, cancellation_token)
536    }
537
538    /// [`run_async`](Self::run_async) on an explicit clock.
539    pub fn run_async_with_clock(
540        &self,
541        control: &Animatable,
542        clock: Option<Rc<dyn IClock>>,
543        cancellation_token: CancellationToken,
544    ) -> AnimationTask {
545        if cancellation_token.is_cancellation_requested() {
546            return AnimationTask::completed();
547        }
548
549        let run = AnimationTask::pending();
550
551        if self.iteration_count() == IterationCount::INFINITE {
552            run.try_set(Err(AnimationError(
553                "Looping animations must not use the Run method.".to_string(),
554            )));
555        }
556
557        let subscriptions: Rc<RefCell<Option<Rc<dyn IDisposable>>>> = Rc::new(RefCell::new(None));
558        let cancellation = Rc::new(RefCell::new(None));
559
560        let finish: Rc<dyn Fn()> = {
561            let run = run.clone();
562            let subscriptions = subscriptions.clone();
563            let cancellation = cancellation.clone();
564            Rc::new(move || {
565                run.try_set_result();
566                let subscriptions = subscriptions.borrow_mut().take();
567                if let Some(subscriptions) = subscriptions {
568                    subscriptions.dispose();
569                }
570                let cancellation: Option<
571                    crate::animation::cancellation::LocalCancellationRegistration,
572                > = cancellation.borrow_mut().take();
573                if let Some(cancellation) = cancellation {
574                    cancellation.dispose();
575                }
576            })
577        };
578
579        let applied = self.apply(
580            control,
581            clock,
582            Observable::return_(true),
583            Some(finish.clone()),
584            true,
585        );
586        // The animation may have completed while it was being applied, in
587        // which case there is nothing left to dispose on cancellation.
588        *subscriptions.borrow_mut() = Some(applied);
589
590        let registration = register_local(&cancellation_token, move || finish());
591        if !run.is_completed() || !cancellation_token.is_cancellation_requested() {
592            *cancellation.borrow_mut() = Some(registration);
593        }
594
595        run
596    }
597}