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