Skip to main content

ferroui_base/rendering/composition/animations/
animation_controller.rs

1//! WinUI's `AnimationController`: pausing, resuming, scrubbing and speeding up
2//! running animations, and the global playback rate of the compositor
3//! (`Compositor.GlobalPlaybackRate`).
4//!
5//! # Design
6//!
7//! A controller is a live [`CompositionPropertySet`] with the entries
8//! `Progress`, `PlaybackRate`, `IsPaused`, `ProgressBehavior` and
9//! `ProgressRevision`. The UI-thread members write the entries; the animation
10//! instances the controller controls hold the id of the set and read the
11//! entries on the server every frame through an [`AnimationControllerClock`],
12//! which maps the time of the compositor to the time of the animation:
13//!
14//! - while not paused, the animation time advances by the elapsed compositor
15//!   time multiplied by `PlaybackRate` (a negative rate plays backwards, down
16//!   to the start);
17//! - when `Progress` changes (set by the application, or animated: an
18//!   expression animation of `Progress` scrubs the animation), the animation
19//!   time jumps to that point of the animation;
20//! - a paused animation does not end, so a scrubbed animation stays alive.
21//!
22//! Because the controller is a property set, its `Progress` and
23//! `PlaybackRate` can be animated with `start_animation`, and an expression
24//! can refer to the controller as a reference parameter.
25//!
26//! The global playback rate scales the clock of all the animations of the
27//! compositor on the server ([`ServerCompositorAnimations::animation_time`](crate::rendering::composition::server::ServerCompositorAnimations::animation_time)).
28
29use super::{AnimationInstanceFactory, ICompositionAnimation, ICompositionAnimationBase};
30use crate::rendering::composition::expressions::ExpressionVariant;
31use crate::rendering::composition::server::{
32    CompositionProperty, ServerCompositionPropertySet, ServerCompositor, ServerObjectId,
33};
34use crate::rendering::composition::{
35    AsCompositionObject, CompositionObject, CompositionPropertySet, Compositor, ICompositionObjectAnimations,
36};
37use std::cell::{Cell, RefCell};
38use std::rc::{Rc, Weak};
39
40/// The entry of the progress (0..1).
41const PROGRESS: &str = "Progress";
42/// The entry of the playback rate.
43const PLAYBACK_RATE: &str = "PlaybackRate";
44/// The entry that tells whether the controlled animations are paused.
45const IS_PAUSED: &str = "IsPaused";
46/// The entry of the progress behavior: 0 for `Default`, 1 for
47/// `IncludesDelayTime`.
48const PROGRESS_BEHAVIOR: &str = "ProgressBehavior";
49/// The entry counting the assignments of `Progress`, so that assigning the
50/// same value again moves the animation back to it.
51const PROGRESS_REVISION: &str = "ProgressRevision";
52
53/// What the progress of an [`AnimationController`] covers (WinUI
54/// `AnimationControllerProgressBehavior`).
55#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
56pub enum AnimationControllerProgressBehavior {
57    /// The progress covers the animation after its delay.
58    #[default]
59    Default,
60    /// The progress covers the delay and the animation.
61    IncludesDelayTime,
62}
63
64/// Controls running animations: pauses and resumes them, moves them to a
65/// point (`Progress`) and changes their speed (`PlaybackRate`) (WinUI
66/// `AnimationController`).
67///
68/// An animation comes under a controller when it is started with
69/// [`start_animation_with_controller`](ICompositionObjectAnimations::start_animation_with_controller),
70/// or through [`try_get_animation_controller`](ICompositionObjectAnimations::try_get_animation_controller)
71/// of the object it animates. Key frame and natural motion animations can be
72/// controlled; expression animations are not affected. One controller can
73/// control several animations.
74///
75/// `Progress` and `PlaybackRate` can be animated
76/// (`controller.start_animation("Progress", expression)`), and the controller
77/// can be a reference parameter of an expression (`controller.Progress`).
78pub struct AnimationController {
79    properties: Rc<CompositionPropertySet>,
80    progress_revision: Cell<f32>,
81}
82
83impl AnimationController {
84    /// The highest playback rate (WinUI `MaxPlaybackRate`).
85    pub const MAX_PLAYBACK_RATE: f32 = 1000.0;
86    /// The lowest playback rate (WinUI `MinPlaybackRate`); a negative rate
87    /// plays backwards.
88    pub const MIN_PLAYBACK_RATE: f32 = -1000.0;
89
90    pub(crate) fn new(compositor: &Rc<Compositor>) -> Rc<AnimationController> {
91        let properties = CompositionPropertySet::new(compositor);
92        properties.insert_scalar(PROGRESS, 0.0);
93        properties.insert_scalar(PLAYBACK_RATE, 1.0);
94        properties.insert_boolean(IS_PAUSED, false);
95        properties.insert_scalar(PROGRESS_BEHAVIOR, 0.0);
96        properties.insert_scalar(PROGRESS_REVISION, 0.0);
97        Rc::new(AnimationController { properties, progress_revision: Cell::new(0.0) })
98    }
99
100    /// The compositor of the controller.
101    pub fn compositor(&self) -> &Rc<Compositor> {
102        self.properties.compositor()
103    }
104
105    /// Pauses the controlled animations: their time stops, and they do not end
106    /// while paused.
107    pub fn pause(&self) {
108        self.properties.insert_boolean(IS_PAUSED, true)
109    }
110
111    /// Resumes the controlled animations from where they were paused.
112    pub fn resume(&self) {
113        self.properties.insert_boolean(IS_PAUSED, false)
114    }
115
116    /// Whether the controller is paused (not a WinUI member).
117    pub fn is_paused(&self) -> bool {
118        self.properties.try_get_boolean(IS_PAUSED).1
119    }
120
121    /// The speed of the controlled animations: 1 is normal speed, 2 twice as
122    /// fast, a negative rate plays backwards.
123    pub fn playback_rate(&self) -> f32 {
124        self.properties.try_get_scalar(PLAYBACK_RATE).1
125    }
126
127    /// Sets the playback rate, clamped to
128    /// [`MIN_PLAYBACK_RATE`](Self::MIN_PLAYBACK_RATE)..[`MAX_PLAYBACK_RATE`](Self::MAX_PLAYBACK_RATE).
129    pub fn set_playback_rate(&self, value: f32) {
130        let value = if value.is_nan() { 1.0 } else { value.clamp(Self::MIN_PLAYBACK_RATE, Self::MAX_PLAYBACK_RATE) };
131        self.properties.insert_scalar(PLAYBACK_RATE, value)
132    }
133
134    /// The progress last assigned (0..1).
135    //
136    // Deviation (winui-composition.md): WinUI reads the progress of the
137    // running animation; the UI thread cannot read the server, so this is the
138    // value last assigned.
139    pub fn progress(&self) -> f32 {
140        self.properties.try_get_scalar(PROGRESS).1
141    }
142
143    /// Moves the controlled animations to a point between their start (0)
144    /// and their end (1), clamped to that range. A playing animation goes on
145    /// from there; a paused one stays there.
146    pub fn set_progress(&self, value: f32) {
147        let value = if value.is_nan() { 0.0 } else { value.clamp(0.0, 1.0) };
148        self.properties.insert_scalar(PROGRESS, value);
149        self.progress_revision.set(self.progress_revision.get() + 1.0);
150        self.properties.insert_scalar(PROGRESS_REVISION, self.progress_revision.get());
151    }
152
153    /// What the progress covers.
154    pub fn progress_behavior(&self) -> AnimationControllerProgressBehavior {
155        match self.properties.try_get_scalar(PROGRESS_BEHAVIOR).1 {
156            v if v >= 0.5 => AnimationControllerProgressBehavior::IncludesDelayTime,
157            _ => AnimationControllerProgressBehavior::Default,
158        }
159    }
160
161    pub fn set_progress_behavior(&self, value: AnimationControllerProgressBehavior) {
162        let value = match value {
163            AnimationControllerProgressBehavior::Default => 0.0,
164            AnimationControllerProgressBehavior::IncludesDelayTime => 1.0,
165        };
166        self.properties.insert_scalar(PROGRESS_BEHAVIOR, value)
167    }
168
169    /// The id of the server counterpart (the property set of the controller).
170    pub(crate) fn server(&self) -> ServerObjectId {
171        self.properties.as_composition_object().required_server()
172    }
173
174    /// Wraps the factory of an animation instance so that the instance comes
175    /// under this controller before it starts.
176    pub(crate) fn wrap(&self, factory: AnimationInstanceFactory) -> AnimationInstanceFactory {
177        let controller = self.server();
178        AnimationInstanceFactory::new(move || {
179            let instance = factory.create();
180            instance.attach_controller(controller);
181            instance
182        })
183    }
184}
185
186impl AsCompositionObject for AnimationController {
187    fn as_composition_object(&self) -> &CompositionObject {
188        self.properties.as_composition_object()
189    }
190
191    fn composition_type_name(&self) -> &'static str {
192        "AnimationController"
193    }
194
195    /// An expression refers to the controller through its property set
196    /// (`controller.Progress`).
197    fn as_property_set(self: Rc<Self>) -> Option<Rc<CompositionPropertySet>> {
198        Some(self.properties.clone())
199    }
200}
201
202impl ICompositionObjectAnimations for AnimationController {
203    /// `Progress` and `PlaybackRate` can be animated.
204    fn try_start_animation(
205        &self,
206        property_name: &str,
207        animation: &dyn ICompositionAnimation,
208        final_value: Option<ExpressionVariant>,
209    ) -> bool {
210        if property_name != PROGRESS && property_name != PLAYBACK_RATE {
211            return false;
212        }
213        self.properties.try_start_animation(property_name, animation, final_value)
214    }
215
216    fn get_composition_property(&self, property_name: &str) -> Option<&'static CompositionProperty> {
217        if property_name != PROGRESS && property_name != PLAYBACK_RATE {
218            return None;
219        }
220        self.properties.get_composition_property(property_name)
221    }
222
223    fn composition_object(&self) -> &CompositionObject {
224        self.properties.as_composition_object()
225    }
226}
227
228/// An animation whose instance factory is made already: what the start path
229/// of a property is given for an animation started with a controller (the
230/// factory of the animation wrapped by [`AnimationController::wrap`]).
231pub(crate) struct PreparedAnimation {
232    factory: RefCell<Option<AnimationInstanceFactory>>,
233}
234
235impl PreparedAnimation {
236    pub(crate) fn new(factory: AnimationInstanceFactory) -> PreparedAnimation {
237        PreparedAnimation { factory: RefCell::new(Some(factory)) }
238    }
239}
240
241impl ICompositionAnimationBase for PreparedAnimation {
242    fn as_composition_animation(&self) -> Option<&dyn ICompositionAnimation> {
243        Some(self)
244    }
245}
246
247impl ICompositionAnimation for PreparedAnimation {
248    fn target(&self) -> Option<String> {
249        None
250    }
251
252    /// The prepared factory; the arguments were applied when it was made.
253    /// Called once by the start path.
254    fn create_instance(&self, _target_object: ServerObjectId, _final_value: Option<ExpressionVariant>) -> AnimationInstanceFactory {
255        match self.factory.borrow_mut().take() {
256            Some(factory) => factory,
257            None => panic!("the instance of a prepared animation is created once"),
258        }
259    }
260}
261
262/// The span of an animation that the progress of a controller covers, in
263/// ticks (100 ns): the delay before the animation and the length of the
264/// animation after it.
265#[derive(Clone, Copy, Debug)]
266pub(crate) struct ProgressSpan {
267    pub(crate) delay: i64,
268    pub(crate) active: i64,
269}
270
271/// The time of a controlled animation instance, on the server: maps the time
272/// of the compositor to the time of the animation through the controller the
273/// instance is attached to. Without a controller the time of the animation is
274/// the time since it started.
275#[derive(Default)]
276pub(crate) struct AnimationControllerClock {
277    /// The property set of the controller.
278    controller: Cell<Option<ServerObjectId>>,
279    resolved: RefCell<Option<Weak<ServerCompositionPropertySet>>>,
280    /// The compositor time (ticks) of the last evaluation under the
281    /// controller.
282    last_now: Cell<Option<i64>>,
283    /// The time of the animation (ticks since it started).
284    time: Cell<f64>,
285    /// The progress and its revision seen last.
286    progress_seen: Cell<(f64, f64)>,
287    paused: Cell<bool>,
288}
289
290impl AnimationControllerClock {
291    /// Attaches the controller whose property set has the id `controller`.
292    pub(crate) fn attach(&self, controller: ServerObjectId) {
293        self.controller.set(Some(controller));
294        *self.resolved.borrow_mut() = None;
295        self.last_now.set(None);
296    }
297
298    /// Whether the controller paused the animation at the last evaluation.
299    pub(crate) fn is_paused(&self) -> bool {
300        self.paused.get()
301    }
302
303    fn controller_set(&self, compositor: Option<&Rc<ServerCompositor>>) -> Option<Rc<ServerCompositionPropertySet>> {
304        let id = self.controller.get()?;
305        if let Some(set) = self.resolved.borrow().as_ref().and_then(Weak::upgrade) {
306            return Some(set);
307        }
308        let set = compositor?.get::<ServerCompositionPropertySet>(id)?;
309        *self.resolved.borrow_mut() = Some(Rc::downgrade(&set));
310        Some(set)
311    }
312
313    /// The time of the animation at the compositor time `now` (ticks), for an
314    /// animation started at `started_at` (ticks): the ticks since the start of
315    /// the animation, negative before it. `span` is what the progress of the
316    /// controller covers, if the animation can be moved to a progress.
317    pub(crate) fn elapsed(
318        &self,
319        compositor: Option<&Rc<ServerCompositor>>,
320        now: i64,
321        started_at: i64,
322        span: Option<ProgressSpan>,
323    ) -> i64 {
324        let raw = now - started_at;
325        if self.controller.get().is_none() {
326            return raw;
327        }
328        let Some(set) = self.controller_set(compositor) else {
329            // The controller is gone: the animation plays on at normal speed
330            // from where it was.
331            self.paused.set(false);
332            return match self.last_now.get() {
333                Some(last) => {
334                    self.time.set(self.time.get() + (now - last) as f64);
335                    self.last_now.set(Some(now));
336                    self.time.get() as i64
337                }
338                None => raw,
339            };
340        };
341        let number = |name: &str, default: f64| set.get_value(name).try_cast::<f64>().unwrap_or(default);
342        let paused = set.get_value(IS_PAUSED).try_cast::<bool>().unwrap_or(false);
343        let rate = number(PLAYBACK_RATE, 1.0);
344        let progress = number(PROGRESS, 0.0);
345        let revision = number(PROGRESS_REVISION, 0.0);
346        let includes_delay = number(PROGRESS_BEHAVIOR, 0.0) >= 0.5;
347
348        match self.last_now.get() {
349            None => {
350                // The first evaluation under the controller: the animation
351                // time goes on from the time since the start, and moves to
352                // the progress of the controller if it was assigned.
353                self.time.set(raw as f64);
354                self.progress_seen.set((0.0, 0.0));
355            }
356            Some(last) => {
357                if !paused && rate.is_finite() {
358                    self.time.set(self.time.get() + (now - last) as f64 * rate);
359                }
360            }
361        }
362        self.last_now.set(Some(now));
363        self.paused.set(paused);
364
365        if self.progress_seen.get() != (progress, revision) {
366            self.progress_seen.set((progress, revision));
367            if let Some(span) = span {
368                let (offset, length) =
369                    if includes_delay { (0, span.delay + span.active) } else { (span.delay, span.active) };
370                self.time.set(offset as f64 + progress.clamp(0.0, 1.0) * length as f64);
371            }
372        }
373        let time = self.time.get();
374        if time.is_nan() || time < 0.0 {
375            self.time.set(0.0);
376        }
377        self.time.get() as i64
378    }
379}
380
381crate::ferro_thread_local! {
382    /// The global playback rates of the compositors of this thread, as the
383    /// UI thread last set them (the server keeps its own copy).
384    static GLOBAL_PLAYBACK_RATES: RefCell<Vec<(Weak<Compositor>, f32)>> = RefCell::new(Vec::new());
385}
386
387impl Compositor {
388    /// The highest global playback rate (WinUI `MaxGlobalPlaybackRate`).
389    pub const MAX_GLOBAL_PLAYBACK_RATE: f32 = 1000.0;
390    /// The lowest global playback rate (WinUI `MinGlobalPlaybackRate`).
391    pub const MIN_GLOBAL_PLAYBACK_RATE: f32 = 0.0;
392
393    /// Creates an animation controller (WinUI `CreateAnimationController`).
394    pub fn create_animation_controller(&self) -> Rc<AnimationController> {
395        AnimationController::new(&self.this_handle())
396    }
397
398    /// The rate at which the time of every animation of the compositor runs
399    /// (WinUI `GlobalPlaybackRate`): 1 by default, 0.5 for half speed.
400    pub fn global_playback_rate(&self) -> f32 {
401        let this = self as *const Compositor;
402        GLOBAL_PLAYBACK_RATES
403            .with(|rates| rates.borrow().iter().find(|(c, _)| std::ptr::eq(c.as_ptr(), this)).map(|(_, r)| *r))
404            .unwrap_or(1.0)
405    }
406
407    /// Sets the global playback rate, clamped to
408    /// [`MIN_GLOBAL_PLAYBACK_RATE`](Self::MIN_GLOBAL_PLAYBACK_RATE)..[`MAX_GLOBAL_PLAYBACK_RATE`](Self::MAX_GLOBAL_PLAYBACK_RATE).
409    /// It applies from the next frame to running animations and to those
410    /// started later: their time from then on runs at the new rate.
411    //
412    // Deviation (winui-composition.md): the range of WinUI is not documented
413    // here; the port does not run time backwards globally (minimum 0).
414    pub fn set_global_playback_rate(&self, value: f32) {
415        let value = if value.is_nan() {
416            1.0
417        } else {
418            value.clamp(Self::MIN_GLOBAL_PLAYBACK_RATE, Self::MAX_GLOBAL_PLAYBACK_RATE)
419        };
420        let this = self.this_handle();
421        GLOBAL_PLAYBACK_RATES.with(|rates| {
422            let mut rates = rates.borrow_mut();
423            rates.retain(|(c, _)| c.strong_count() > 0);
424            match rates.iter_mut().find(|(c, _)| std::ptr::eq(c.as_ptr(), Rc::as_ptr(&this))) {
425                Some(entry) => entry.1 = value,
426                None => rates.push((Rc::downgrade(&this), value)),
427            }
428        });
429        self.post_server_job(
430            move |server| server.animations().set_global_playback_rate(value as f64, server.server_now()),
431            false,
432        );
433    }
434}