zng_var/
vars.rs

1//!: Vars service.
2
3use std::{mem, sync::Arc, thread::ThreadId, time::Duration};
4
5use parking_lot::Mutex;
6use smallbox::SmallBox;
7use zng_app_context::{app_local, context_local};
8use zng_time::{DInstant, Deadline, INSTANT, INSTANT_APP};
9use zng_unit::{Factor, FactorUnits as _, TimeUnits as _};
10
11use smallbox::smallbox;
12
13use crate::{
14    AnyVar, Var,
15    animation::{Animation, AnimationController, AnimationHandle, AnimationTimer, ModifyInfo},
16    var,
17};
18
19/// Represents the last time a variable was mutated or the current update cycle.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, bytemuck::NoUninit)]
21#[repr(transparent)]
22pub struct VarUpdateId(pub(crate) u32);
23impl VarUpdateId {
24    /// ID that is never new.
25    pub const fn never() -> Self {
26        VarUpdateId(0)
27    }
28
29    fn next(&mut self) {
30        if self.0 == u32::MAX {
31            self.0 = 1;
32        } else {
33            self.0 += 1;
34        }
35    }
36}
37impl Default for VarUpdateId {
38    fn default() -> Self {
39        Self::never()
40    }
41}
42
43pub(super) type VarUpdateFn = SmallBox<dyn FnMut() + Send + 'static, smallbox::space::S8>;
44
45app_local! {
46    pub(crate) static VARS_SV: VarsService = VarsService::new();
47}
48context_local! {
49    pub(crate) static VARS_MODIFY_CTX: Option<ModifyInfo> = None;
50    pub(crate) static VARS_ANIMATION_CTRL_CTX: Box<dyn AnimationController> = {
51        let r: Box<dyn AnimationController> = Box::new(());
52        r
53    };
54}
55
56type AnimationFn = SmallBox<dyn FnMut(AnimationUpdateInfo) -> Option<Deadline> + Send, smallbox::space::S8>;
57
58pub(crate) struct VarsService {
59    // animation config
60    animations_enabled: Var<bool>,
61    sys_animations_enabled: Var<bool>,
62    frame_duration: Var<Duration>,
63    animation_time_scale: Var<Factor>,
64
65    // VARS_APP stuff
66    app_waker: Option<SmallBox<dyn Fn() + Send + Sync + 'static, smallbox::space::S2>>,
67    modify_trace: Option<SmallBox<dyn Fn(&'static str) + Send + Sync + 'static, smallbox::space::S2>>,
68
69    // AnyVar::perm storage
70    perm: Mutex<Vec<AnyVar>>,
71
72    // update state
73    update_id: VarUpdateId,
74    updates: Mutex<Vec<(ModifyInfo, VarUpdateFn)>>,
75    updating_thread: Option<ThreadId>,
76    updates_after: Mutex<Vec<(ModifyInfo, VarUpdateFn)>>,
77
78    // animations state
79    ans_animations: Mutex<Vec<AnimationFn>>,
80    ans_animation_imp: usize,
81    ans_current_modify: ModifyInfo,
82    ans_animation_start_time: Option<DInstant>,
83    ans_next_frame: Option<Deadline>,
84}
85impl VarsService {
86    pub(crate) fn new() -> Self {
87        let sys_animations_enabled = var(true);
88        Self {
89            animations_enabled: sys_animations_enabled.cow(),
90            sys_animations_enabled,
91            frame_duration: var((1.0 / 60.0).secs()),
92            animation_time_scale: var(1.fct()),
93
94            app_waker: None,
95            modify_trace: None,
96
97            perm: Mutex::new(vec![]),
98
99            update_id: VarUpdateId::never(),
100            updates: Mutex::new(vec![]),
101            updating_thread: None,
102            updates_after: Mutex::new(vec![]),
103
104            ans_animations: Mutex::new(vec![]),
105            ans_animation_imp: 0,
106            ans_current_modify: ModifyInfo::never(),
107            ans_animation_start_time: None,
108            ans_next_frame: None,
109        }
110    }
111
112    fn wake_app(&self) {
113        if let Some(w) = &self.app_waker {
114            w();
115        }
116    }
117}
118
119/// Variable updates and animation service.
120pub struct VARS;
121impl VARS {
122    /// Id of the current vars update in the app scope.
123    ///
124    /// Variable with [`AnyVar::last_update`] equal to this are *new*.
125    pub fn update_id(&self) -> VarUpdateId {
126        VARS_SV.read().update_id
127    }
128
129    /// Read-write that defines if animations are enabled on the app.
130    ///
131    /// The value is the same as [`sys_animations_enabled`], if set the variable disconnects from system config.
132    ///
133    /// [`sys_animations_enabled`]: Self::sys_animations_enabled
134    pub fn animations_enabled(&self) -> Var<bool> {
135        VARS_SV.read().animations_enabled.clone()
136    }
137
138    /// Read-only that tracks if animations are enabled in the operating system.
139    ///
140    /// This is `true` by default, it updates when the operating system config changes.
141    pub fn sys_animations_enabled(&self) -> Var<bool> {
142        VARS_SV.read().sys_animations_enabled.read_only()
143    }
144
145    /// Variable that defines the global frame duration, the default is 60fps `(1.0 / 60.0).secs()`.
146    pub fn frame_duration(&self) -> Var<Duration> {
147        VARS_SV.read().frame_duration.clone()
148    }
149
150    /// Variable that defines a global scale for the elapsed time of animations.
151    pub fn animation_time_scale(&self) -> Var<Factor> {
152        VARS_SV.read().animation_time_scale.clone()
153    }
154
155    /// Info about the current context when requesting variable modification.
156    ///
157    /// If is currently inside a [`VARS.animate`] closure, or inside a [`Var::modify`] closure requested by an animation, or inside
158    /// an [`AnimationController`], returns the info that was collected at the moment the animation was requested. Outside of animations
159    /// gets an info with [`importance`] guaranteed to override the [`modify_importance`].
160    ///
161    /// [`importance`]: ModifyInfo::importance
162    /// [`modify_importance`]: AnyVar::modify_importance
163    /// [`AnimationController`]: crate::animation::AnimationController
164    /// [`VARS.animate`]: VARS::animate
165    pub fn current_modify(&self) -> ModifyInfo {
166        match VARS_MODIFY_CTX.get_clone() {
167            Some(current) => current, // override set by modify and animation closures.
168            None => VARS_SV.read().ans_current_modify.clone(),
169        }
170    }
171
172    /// Adds an `animation` closure that is called every frame to update captured variables, starting after next frame.
173    ///
174    /// This is used to implement all [`Var<T>`] animations, it enables any kind of variable animation,
175    /// including multiple variables.
176    ///
177    /// Returns an [`AnimationHandle`] that can be used to monitor the animation status and to [`stop`] or to
178    /// make the animation [`perm`].
179    ///
180    /// # Variable Control
181    ///
182    /// Animations assume *control* of a variable on the first time they cause its value to be new, after this
183    /// moment the [`AnyVar::is_animating`] value is `true` and [`AnyVar::modify_importance`] is the animation's importance,
184    /// until the animation stops. Only one animation can control a variable at a time, if an animation loses control of a
185    /// variable all attempts to modify it from inside the animation are ignored.
186    ///
187    /// Later started animations steal control from previous animations, update, modify or set calls also remove the variable
188    /// from being affected by a running animation, even if just set to an equal value, that is, not actually updated.
189    ///
190    /// # Nested Animations
191    ///
192    /// Other animations can be started from inside the animation closure, these *nested* animations have the same importance
193    /// as the *parent* animation, the animation handle is different and [`AnyVar::is_animating`] is `false` if the nested animation
194    /// is dropped before the *parent* animation. But because the animations share the same importance the parent animation can
195    /// set the variable again.
196    ///
197    /// You can also use the [`EasingTime::seg`] method to get a normalized time factor for a given segment or slice of the overall time,
198    /// complex animations targeting multiple variables can be coordinated with precision using this method.
199    ///
200    /// # Examples
201    ///
202    /// The example animates a `text` variable from `"Animation at 0%"` to `"Animation at 100%"`, when the animation
203    /// stops the `completed` variable is set to `true`.
204    ///
205    /// ```
206    /// # use zng_var::{*, animation::easing};
207    /// # use zng_txt::*;
208    /// # use zng_unit::*;
209    /// # use zng_clone_move::*;
210    /// #
211    /// fn animate_text(text: &Var<Txt>, completed: &Var<bool>) {
212    ///     let transition = animation::Transition::new(0u8, 100);
213    ///     let mut prev_value = 101;
214    ///     VARS.animate(clmv!(text, completed, |animation| {
215    ///         let step = easing::expo(animation.elapsed_stop(1.secs()));
216    ///         let value = transition.sample(step);
217    ///         if value != prev_value {
218    ///             if value == 100 {
219    ///                 animation.stop();
220    ///                 let _ = completed.set(true);
221    ///             }
222    ///             let _ = text.set(formatx!("Animation at {value}%"));
223    ///             prev_value = value;
224    ///         }
225    ///     }))
226    ///     .perm()
227    /// }
228    /// ```
229    ///
230    /// Note that the animation can be stopped from the inside, the closure parameter is an [`Animation`]. In
231    /// the example this is the only way to stop the animation, because [`perm`] was called. Animations hold a clone
232    /// of the variables they affect and exist for the duration of the app if not stopped, causing the app to wake and call the
233    /// animation closure for every frame.
234    ///
235    /// This method is the most basic animation interface, used to build all other animations, its rare that you
236    /// will need to use it directly, most of the time animation effects can be composted using the [`Var`] easing and mapping
237    /// methods.
238    ///
239    /// ```
240    /// # use zng_var::{*, animation::easing};
241    /// # use zng_txt::*;
242    /// # use zng_unit::*;
243    /// # fn demo() {
244    /// let value = var(0u8);
245    /// let text = value.map(|v| formatx!("Animation at {v}%"));
246    /// value.ease(100, 1.secs(), easing::expo);
247    /// # }
248    /// ```
249    ///
250    /// # Optimization Tips
251    ///
252    /// When no animation is running the app *sleeps* awaiting for an external event, update request or timer elapse, when at least one
253    /// animation is running the app awakes every [`VARS.frame_duration`]. You can use [`Animation::sleep`] to *pause* the animation
254    /// for a duration, if all animations are sleeping the app is also sleeping.
255    ///
256    /// Animations lose control over a variable permanently when a newer animation modifies the var or
257    /// the var is modified directly, but even if the animation can't control any variables **it keeps running**.
258    /// This happens because the system has no insight of all side effects caused by the `animation` closure. You
259    /// can use the [`VARS.current_modify`] and [`AnyVar::modify_importance`] to detect when the animation no longer affects
260    /// any variables and stop the animation to avoid awaking the app for no reason.
261    ///
262    /// These optimizations are already implemented by the animations provided as methods of [`Var<T>`].
263    ///
264    /// # External Controller
265    ///
266    /// The animation can be controlled from the inside using the [`Animation`] reference, it can be stopped using the returned
267    /// [`AnimationHandle`], and it can also be controlled by a registered [`AnimationController`] that can manage multiple
268    /// animations at the same time, see [`with_animation_controller`] for more details.
269    ///
270    /// [`Animation::sleep`]: Animation::sleep
271    /// [`stop`]: AnimationHandle::stop
272    /// [`perm`]: AnimationHandle::perm
273    /// [`with_animation_controller`]: VARS::with_animation_controller
274    /// [`VARS.frame_duration`]: VARS::frame_duration
275    /// [`VARS.current_modify`]: VARS::current_modify
276    /// [`EasingTime::seg`]: crate::animation::easing::EasingTime::seg
277    pub fn animate<A>(&self, animation: A) -> AnimationHandle
278    where
279        A: FnMut(&Animation) + Send + 'static,
280    {
281        VARS.animate_impl(smallbox!(animation))
282    }
283
284    /// Calls `animate` while `controller` is registered as the animation controller.
285    ///
286    /// The `controller` is notified of animation events for each animation spawned by `animate` and can affect then with the same
287    /// level of access as [`VARS.animate`]. Only one controller can affect animations at a time.
288    ///
289    /// This can be used to manage multiple animations at the same time, or to get [`VARS.animate`] level of access to an animation
290    /// that is not implemented to allow such access. Note that animation implementers are not required to support the full
291    /// [`Animation`] API, for example, there is no guarantee that a restart requested by the controller will repeat the same animation.
292    ///
293    /// The controller can start new animations, these animations will have the same controller if not overridden, you can
294    /// use this method and the `()` controller to avoid this behavior.
295    ///
296    /// [`Animation`]: crate::animation::Animation
297    /// [`VARS.animate`]: VARS::animate
298    pub fn with_animation_controller<R>(&self, controller: impl AnimationController, animate: impl FnOnce() -> R) -> R {
299        let controller: Box<dyn AnimationController> = Box::new(controller);
300        let mut opt = Some(Arc::new(controller));
301        VARS_ANIMATION_CTRL_CTX.with_context(&mut opt, animate)
302    }
303
304    /// Schedule a custom closure to run as a variable modify callback.
305    ///
306    /// The closure `m` will run after the current update, any variable it modifies will be new in the next update, like a binding.
307    pub fn modify(&self, debug_name: &'static str, m: impl FnOnce() + Send + 'static) {
308        self.schedule_update(debug_name, m);
309    }
310
311    pub(crate) fn schedule_update(&self, value_type_name: &'static str, apply_update: impl FnOnce() + Send + 'static) {
312        let mut once = Some(apply_update);
313        self.schedule_update_impl(
314            value_type_name,
315            smallbox!(move || {
316                let once = once.take().unwrap();
317                once();
318            }),
319        );
320    }
321
322    pub(crate) fn perm(&self, var: AnyVar) {
323        VARS_SV.read().perm.lock().push(var);
324    }
325}
326
327/// VARS APP integration.
328#[expect(non_camel_case_types)]
329pub struct VARS_APP;
330impl VARS_APP {
331    /// Register a closure called when [`apply_updates`] should be called because there are changes pending.
332    ///
333    /// # Panics
334    ///
335    /// Panics if already called for the current app. This must be called by app framework implementers only.
336    ///
337    /// [`apply_updates`]: Self::apply_updates
338    pub fn init_app_waker(&self, waker: impl Fn() + Send + Sync + 'static) {
339        let mut vars = VARS_SV.write();
340        assert!(vars.app_waker.is_none());
341        vars.app_waker = Some(smallbox!(waker));
342    }
343
344    /// Register a closure called when a variable modify is about to be scheduled. The
345    /// closure parameter is the type name of the variable type.
346    ///
347    /// # Panics
348    ///
349    /// Panics if already called for the current app. This must be called by app framework implementers only.
350    pub fn init_modify_trace(&self, trace: impl Fn(&'static str) + Send + Sync + 'static) {
351        let mut vars = VARS_SV.write();
352        assert!(vars.modify_trace.is_none());
353        vars.modify_trace = Some(smallbox!(trace));
354    }
355
356    /// If [`apply_updates`] will do anything.
357    ///
358    /// [`apply_updates`]: Self::apply_updates
359    pub fn has_pending_updates(&self) -> bool {
360        !VARS_SV.write().updates.get_mut().is_empty()
361    }
362
363    /// Sets the `sys_animations_enabled` read-only variable.
364    pub fn set_sys_animations_enabled(&self, enabled: bool) {
365        VARS_SV.read().sys_animations_enabled.set(enabled);
366    }
367
368    /// Apply all pending updates, call hooks and update bindings.
369    ///
370    /// This must be called by app framework implementers only.
371    pub fn apply_updates(&self) {
372        let _s = tracing::trace_span!("VARS").entered();
373        let _t = INSTANT_APP.pause_for_update();
374        VARS.apply_updates_and_after(0)
375    }
376
377    /// Does one animation frame if the frame duration has elapsed.
378    ///
379    /// This must be called by app framework implementers only.
380    pub fn update_animations(&self, timer: &mut impl AnimationTimer) {
381        VARS.update_animations_impl(timer);
382    }
383
384    /// Register the next animation frame, if there are any active animations.
385    ///
386    /// This must be called by app framework implementers only.
387    pub fn next_deadline(&self, timer: &mut impl AnimationTimer) {
388        VARS.next_deadline_impl(timer)
389    }
390}
391
392impl VARS {
393    fn schedule_update_impl(&self, value_type_name: &'static str, update: VarUpdateFn) {
394        let vars = VARS_SV.read();
395        if let Some(trace) = &vars.modify_trace {
396            trace(value_type_name);
397        }
398        let cur_modify = match VARS_MODIFY_CTX.get_clone() {
399            Some(current) => current, // override set by modify and animation closures.
400            None => vars.ans_current_modify.clone(),
401        };
402
403        if let Some(id) = vars.updating_thread {
404            if std::thread::current().id() == id {
405                // is binding request, enqueue for immediate exec.
406                vars.updates.lock().push((cur_modify, update));
407            } else {
408                // is request from app task thread when we are already updating, enqueue for exec after current update.
409                vars.updates_after.lock().push((cur_modify, update));
410            }
411        } else {
412            // request from any app thread,
413            vars.updates.lock().push((cur_modify, update));
414            vars.wake_app();
415        }
416    }
417
418    fn apply_updates_and_after(&self, depth: u8) {
419        let mut vars = VARS_SV.write();
420
421        match depth {
422            0 => {
423                vars.update_id.next();
424                vars.ans_animation_start_time = None;
425            }
426            10 => {
427                // high-pressure from worker threads, skip
428                return;
429            }
430            _ => {}
431        }
432
433        // updates requested by other threads while was applying updates
434        let mut updates = mem::take(vars.updates_after.get_mut());
435        // normal updates
436        if updates.is_empty() {
437            updates = mem::take(vars.updates.get_mut());
438        } else {
439            updates.append(vars.updates.get_mut());
440        }
441        // apply pending updates
442        if !updates.is_empty() {
443            debug_assert!(vars.updating_thread.is_none());
444            vars.updating_thread = Some(std::thread::current().id());
445
446            drop(vars);
447            update_each_and_bindings(updates, 0);
448
449            vars = VARS_SV.write();
450            vars.updating_thread = None;
451
452            if !vars.updates_after.get_mut().is_empty() {
453                drop(vars);
454                VARS.apply_updates_and_after(depth + 1)
455            }
456        }
457
458        fn update_each_and_bindings(updates: Vec<(ModifyInfo, VarUpdateFn)>, depth: u16) {
459            if depth == 1000 {
460                tracing::error!(
461                    "updated variable bindings 1000 times, probably stuck in an infinite loop\n\
462                    will skip next updates"
463                );
464                return;
465            }
466
467            for (info, mut update) in updates {
468                #[allow(clippy::redundant_closure)] // false positive
469                VARS_MODIFY_CTX.with_context(&mut Some(Arc::new(Some(info))), || (update)());
470
471                let mut vars = VARS_SV.write();
472                let updates = mem::take(vars.updates.get_mut());
473                if !updates.is_empty() {
474                    drop(vars);
475                    update_each_and_bindings(updates, depth + 1);
476                }
477            }
478        }
479    }
480
481    fn animate_impl(&self, mut animation: SmallBox<dyn FnMut(&Animation) + Send + 'static, smallbox::space::S4>) -> AnimationHandle {
482        let mut vars = VARS_SV.write();
483
484        // # Modify Importance
485        //
486        // Variables only accept modifications from an importance (IMP) >= the previous IM that modified it.
487        //
488        // Direct modifications always overwrite previous animations, so we advance the IMP for each call to
489        // this method **and then** advance the IMP again for all subsequent direct modifications.
490        //
491        // Example sequence of events:
492        //
493        // |IM| Modification  | Accepted
494        // |--|---------------|----------
495        // | 1| Var::set      | YES
496        // | 2| Var::ease     | YES
497        // | 2| ease update   | YES
498        // | 3| Var::set      | YES
499        // | 3| Var::set      | YES
500        // | 2| ease update   | NO
501        // | 4| Var::ease     | YES
502        // | 2| ease update   | NO
503        // | 4| ease update   | YES
504        // | 5| Var::set      | YES
505        // | 2| ease update   | NO
506        // | 4| ease update   | NO
507
508        // ensure that all animations started in this update have the same exact time, we update then with the same `now`
509        // timestamp also, this ensures that synchronized animations match perfectly.
510        let start_time = if let Some(t) = vars.ans_animation_start_time {
511            t
512        } else {
513            let t = INSTANT.now();
514            vars.ans_animation_start_time = Some(t);
515            t
516        };
517
518        let mut anim_imp = None;
519        if let Some(c) = VARS_MODIFY_CTX.get_clone()
520            && c.is_animating()
521        {
522            // nested animation uses parent importance.
523            anim_imp = Some(c.importance);
524        }
525        let anim_imp = match anim_imp {
526            Some(i) => i,
527            None => {
528                // not nested, advance base imp
529                let mut imp = vars.ans_animation_imp.wrapping_add(1);
530                if imp == 0 {
531                    imp = 1;
532                }
533
534                let mut next_imp = imp.wrapping_add(1);
535                if next_imp == 0 {
536                    next_imp = 1;
537                }
538
539                vars.ans_animation_imp = next_imp;
540                vars.ans_current_modify.importance = next_imp;
541
542                imp
543            }
544        };
545
546        let (handle_owner, handle) = AnimationHandle::new();
547        let weak_handle = handle.downgrade();
548
549        let controller = VARS_ANIMATION_CTRL_CTX.get();
550
551        let anim = Animation::new(vars.animations_enabled.get(), start_time, vars.animation_time_scale.get());
552
553        drop(vars);
554
555        controller.on_start(&anim);
556        let mut controller = Some(controller);
557        let mut anim_modify_info = Some(Arc::new(Some(ModifyInfo {
558            handle: Some(weak_handle.clone()),
559            importance: anim_imp,
560        })));
561
562        let mut vars = VARS_SV.write();
563
564        vars.ans_animations.get_mut().push(smallbox!(move |info: AnimationUpdateInfo| {
565            let _handle_owner = &handle_owner; // capture and own the handle owner.
566
567            if weak_handle.upgrade().is_some() {
568                if anim.stop_requested() {
569                    // drop
570                    controller.as_ref().unwrap().on_stop(&anim);
571                    return None;
572                }
573
574                if let Some(sleep) = anim.sleep_deadline() {
575                    if sleep > info.next_frame {
576                        // retain sleep
577                        return Some(sleep);
578                    } else if sleep.0 > info.now {
579                        // sync-up to frame rate after sleep
580                        anim.reset_sleep();
581                        return Some(info.next_frame);
582                    }
583                }
584
585                anim.reset_state(info.animations_enabled, info.now, info.time_scale);
586
587                VARS_ANIMATION_CTRL_CTX.with_context(&mut controller, || {
588                    VARS_MODIFY_CTX.with_context(&mut anim_modify_info, || animation(&anim))
589                });
590
591                // retain until next frame
592                //
593                // stop or sleep may be requested after this (during modify apply),
594                // these updates are applied on the next frame.
595                Some(info.next_frame)
596            } else {
597                // drop
598                controller.as_ref().unwrap().on_stop(&anim);
599                None
600            }
601        }));
602
603        vars.ans_next_frame = Some(Deadline(INSTANT.now()));
604
605        vars.wake_app();
606
607        handle
608    }
609
610    fn update_animations_impl(&self, timer: &mut dyn AnimationTimer) {
611        let mut vars = VARS_SV.write();
612        if let Some(next_frame) = vars.ans_next_frame
613            && timer.elapsed(next_frame)
614        {
615            let mut animations = mem::take(vars.ans_animations.get_mut());
616            debug_assert!(!animations.is_empty());
617
618            let info = AnimationUpdateInfo {
619                animations_enabled: vars.animations_enabled.get(),
620                time_scale: vars.animation_time_scale.get(),
621                now: timer.now(),
622                next_frame: next_frame + vars.frame_duration.get(),
623            };
624
625            let mut min_sleep = Deadline(info.now + Duration::from_secs(60 * 60));
626
627            drop(vars);
628
629            animations.retain_mut(|animate| {
630                if let Some(sleep) = animate(info) {
631                    min_sleep = min_sleep.min(sleep);
632                    true
633                } else {
634                    false
635                }
636            });
637
638            let mut vars = VARS_SV.write();
639
640            let self_animations = vars.ans_animations.get_mut();
641            if !self_animations.is_empty() {
642                min_sleep = Deadline(info.now);
643            }
644            animations.append(self_animations);
645            *self_animations = animations;
646
647            if !self_animations.is_empty() {
648                vars.ans_next_frame = Some(min_sleep);
649                timer.register(min_sleep);
650            } else {
651                vars.ans_next_frame = None;
652            }
653        }
654    }
655
656    fn next_deadline_impl(&self, timer: &mut dyn AnimationTimer) {
657        if let Some(next_frame) = VARS_SV.read().ans_next_frame {
658            timer.register(next_frame);
659        }
660    }
661}
662
663#[derive(Clone, Copy)]
664struct AnimationUpdateInfo {
665    animations_enabled: bool,
666    now: DInstant,
667    time_scale: Factor,
668    next_frame: Deadline,
669}