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 // propagate bindings immediately, services operating on hooks expect
474 // binding effects to propagate fully before next hook
475 let mut vars = VARS_SV.write();
476 let updates = mem::take(vars.updates.get_mut());
477 if !updates.is_empty() {
478 drop(vars);
479 update_each_and_bindings(updates, depth + 1);
480 }
481 }
482 }
483 }
484
485 fn animate_impl(&self, mut animation: SmallBox<dyn FnMut(&Animation) + Send + 'static, smallbox::space::S4>) -> AnimationHandle {
486 let mut vars = VARS_SV.write();
487
488 // # Modify Importance
489 //
490 // Variables only accept modifications from an importance (IMP) >= the previous IM that modified it.
491 //
492 // Direct modifications always overwrite previous animations, so we advance the IMP for each call to
493 // this method **and then** advance the IMP again for all subsequent direct modifications.
494 //
495 // Example sequence of events:
496 //
497 // |IM| Modification | Accepted
498 // |--|---------------|----------
499 // | 1| Var::set | YES
500 // | 2| Var::ease | YES
501 // | 2| ease update | YES
502 // | 3| Var::set | YES
503 // | 3| Var::set | YES
504 // | 2| ease update | NO
505 // | 4| Var::ease | YES
506 // | 2| ease update | NO
507 // | 4| ease update | YES
508 // | 5| Var::set | YES
509 // | 2| ease update | NO
510 // | 4| ease update | NO
511
512 // ensure that all animations started in this update have the same exact time, we update then with the same `now`
513 // timestamp also, this ensures that synchronized animations match perfectly.
514 let start_time = if let Some(t) = vars.ans_animation_start_time {
515 t
516 } else {
517 let t = INSTANT.now();
518 vars.ans_animation_start_time = Some(t);
519 t
520 };
521
522 let mut anim_imp = None;
523 if let Some(c) = VARS_MODIFY_CTX.get_clone()
524 && c.is_animating()
525 {
526 // nested animation uses parent importance.
527 anim_imp = Some(c.importance);
528 }
529 let anim_imp = match anim_imp {
530 Some(i) => i,
531 None => {
532 // not nested, advance base imp
533 let mut imp = vars.ans_animation_imp.wrapping_add(1);
534 if imp == 0 {
535 imp = 1;
536 }
537
538 let mut next_imp = imp.wrapping_add(1);
539 if next_imp == 0 {
540 next_imp = 1;
541 }
542
543 vars.ans_animation_imp = next_imp;
544 vars.ans_current_modify.importance = next_imp;
545
546 imp
547 }
548 };
549
550 let (handle_owner, handle) = AnimationHandle::new();
551 let weak_handle = handle.downgrade();
552
553 let controller = VARS_ANIMATION_CTRL_CTX.get();
554
555 let anim = Animation::new(vars.animations_enabled.get(), start_time, vars.animation_time_scale.get());
556
557 drop(vars);
558
559 controller.on_start(&anim);
560 let mut controller = Some(controller);
561 let mut anim_modify_info = Some(Arc::new(Some(ModifyInfo {
562 handle: Some(weak_handle.clone()),
563 importance: anim_imp,
564 })));
565
566 let mut vars = VARS_SV.write();
567
568 vars.ans_animations.get_mut().push(smallbox!(move |info: AnimationUpdateInfo| {
569 let _handle_owner = &handle_owner; // capture and own the handle owner.
570
571 if weak_handle.upgrade().is_some() {
572 if anim.stop_requested() {
573 // drop
574 controller.as_ref().unwrap().on_stop(&anim);
575 return None;
576 }
577
578 if let Some(sleep) = anim.sleep_deadline() {
579 if sleep > info.next_frame {
580 // retain sleep
581 return Some(sleep);
582 } else if sleep.0 > info.now {
583 // sync-up to frame rate after sleep
584 anim.reset_sleep();
585 return Some(info.next_frame);
586 }
587 }
588
589 anim.reset_state(info.animations_enabled, info.now, info.time_scale);
590
591 VARS_ANIMATION_CTRL_CTX.with_context(&mut controller, || {
592 VARS_MODIFY_CTX.with_context(&mut anim_modify_info, || animation(&anim))
593 });
594
595 // retain until next frame
596 //
597 // stop or sleep may be requested after this (during modify apply),
598 // these updates are applied on the next frame.
599 Some(info.next_frame)
600 } else {
601 // drop
602 controller.as_ref().unwrap().on_stop(&anim);
603 None
604 }
605 }));
606
607 vars.ans_next_frame = Some(Deadline(INSTANT.now()));
608
609 vars.wake_app();
610
611 handle
612 }
613
614 fn update_animations_impl(&self, timer: &mut dyn AnimationTimer) {
615 let mut vars = VARS_SV.write();
616 if let Some(next_frame) = vars.ans_next_frame
617 && timer.elapsed(next_frame)
618 {
619 let mut animations = mem::take(vars.ans_animations.get_mut());
620 debug_assert!(!animations.is_empty());
621
622 let info = AnimationUpdateInfo {
623 animations_enabled: vars.animations_enabled.get(),
624 time_scale: vars.animation_time_scale.get(),
625 now: timer.now(),
626 next_frame: next_frame + vars.frame_duration.get(),
627 };
628
629 let mut min_sleep = Deadline(info.now + Duration::from_secs(60 * 60));
630
631 drop(vars);
632
633 animations.retain_mut(|animate| {
634 if let Some(sleep) = animate(info) {
635 min_sleep = min_sleep.min(sleep);
636 true
637 } else {
638 false
639 }
640 });
641
642 let mut vars = VARS_SV.write();
643
644 let self_animations = vars.ans_animations.get_mut();
645 if !self_animations.is_empty() {
646 min_sleep = Deadline(info.now);
647 }
648 animations.append(self_animations);
649 *self_animations = animations;
650
651 if !self_animations.is_empty() {
652 vars.ans_next_frame = Some(min_sleep);
653 timer.register(min_sleep);
654 } else {
655 vars.ans_next_frame = None;
656 }
657 }
658 }
659
660 fn next_deadline_impl(&self, timer: &mut dyn AnimationTimer) {
661 if let Some(next_frame) = VARS_SV.read().ans_next_frame {
662 timer.register(next_frame);
663 }
664 }
665}
666
667#[derive(Clone, Copy)]
668struct AnimationUpdateInfo {
669 animations_enabled: bool,
670 now: DInstant,
671 time_scale: Factor,
672 next_frame: Deadline,
673}