Skip to main content

xen_animation/
manager.rs

1// SPDX-License-Identifier: Apache-2.0
2use super::{ AnimValue, Transition };
3use std::collections::HashMap;
4use std::hash::Hash;
5use std::time::Duration;
6
7// A single in-flight transition: where it started from, where it's headed,
8// the timing function driving it, and how much time has elapsed since it began.
9struct Anim {
10    from: AnimValue,
11    to: AnimValue,
12    transition: Transition,
13    elapsed: Duration,
14    premultiplied: bool,
15}
16
17impl Anim {
18    // Current interpolated value given elapsed time, transition delay,
19    // duration, and easing curve.
20    fn value_at(&self) -> AnimValue {
21        let past_delay = self.elapsed.saturating_sub(self.transition.delay);
22        let t = if self.transition.duration.is_zero() {
23            1.0
24        } else {
25            past_delay.as_secs_f32() / self.transition.duration.as_secs_f32()
26        };
27        let eased = self.transition.easing.apply(t);
28        if self.premultiplied {
29            self.from.lerp_premultiplied(self.to, eased)
30        } else {
31            self.from.lerp(self.to, eased)
32        }
33    }
34
35    // Whether delay + duration has fully elapsed.
36    fn finished(&self) -> bool {
37        self.elapsed >= self.transition.delay + self.transition.duration
38    }
39}
40
41/// Central, Qt-style animation driver, generic over any hashable key type
42/// so it can be reused outside of any specific GUI framework. Callers never
43/// own a timer themselves; they only report their current target value and
44/// the manager owns the entire lifecycle (starting, easing, retargeting,
45/// finishing).
46pub struct AnimationManager<K: Eq + Hash + Copy> {
47    active: HashMap<K, Anim>,
48    // Last settled value per key, kept around after an animation finishes
49    // and is dropped from `active` - without this, the next retarget would
50    // have no baseline to interpolate from and would snap instantly.
51    resting: HashMap<K, AnimValue>,
52}
53
54impl<K: Eq + Hash + Copy> Default for AnimationManager<K> {
55    fn default() -> Self {
56        Self { active: HashMap::new(), resting: HashMap::new() }
57    }
58}
59
60impl<K: Eq + Hash + Copy> AnimationManager<K> {
61    /// Creates an empty manager with no active or resting animations.
62    pub fn new() -> Self {
63        Self::default()
64    }
65
66    pub fn set_target(&mut self, key: K, target: AnimValue, transition: Option<Transition>) {
67        self.set_target_impl(key, target, transition, false);
68    }
69
70    /// Like `set_target`, but blends the transition using premultiplied
71    /// alpha - use this when `target` packs an RGBA color, so a
72    /// transparent endpoint's meaningless RGB doesn't leak into the blend
73    /// as a visible flash partway through the fade.
74    pub fn set_color_target(&mut self, key: K, target: AnimValue, transition: Option<Transition>) {
75        self.set_target_impl(key, target, transition, true);
76    }
77
78    fn set_target_impl(
79        &mut self,
80        key: K,
81        target: AnimValue,
82        transition: Option<Transition>,
83        premultiplied: bool
84    ) {
85        let Some(transition) = transition else {
86            self.active.remove(&key);
87            self.resting.insert(key, target);
88            return;
89        };
90
91        match self.active.get_mut(&key) {
92            Some(anim) if anim.to == target => {}
93            Some(anim) => {
94                anim.from = anim.value_at();
95                anim.to = target;
96                anim.transition = transition;
97                anim.elapsed = Duration::ZERO;
98                anim.premultiplied = premultiplied;
99            }
100            None => {
101                let from = self.resting.get(&key).copied().unwrap_or(target);
102                self.resting.insert(key, target);
103
104                if from == target {
105                    return;
106                }
107
108                self.active.insert(key, Anim {
109                    from,
110                    to: target,
111                    transition,
112                    elapsed: Duration::ZERO,
113                    premultiplied,
114                });
115            }
116        }
117    }
118
119    /// Advances every active transition by one frame's delta time. Call
120    /// exactly once per frame, before layout/paint.
121    pub fn tick(&mut self, dt: Duration) {
122        for anim in self.active.values_mut() {
123            anim.elapsed += dt;
124        }
125
126        for (key, anim) in self.active.iter() {
127            if anim.finished() {
128                self.resting.insert(*key, anim.to);
129            }
130        }
131
132        self.active.retain(|_, anim| !anim.finished());
133    }
134
135    /// Current (possibly mid-transition) value for `key`, or `None` once
136    /// the transition has settled - callers should fall back to their
137    /// own resolved target value in that case.
138    pub fn value(&self, key: K) -> Option<AnimValue> {
139        self.active.get(&key).map(Anim::value_at)
140    }
141
142    /// Iterates the keys of every animation currently mid-transition,
143    /// letting callers check *what* is animating instead of only *whether*
144    /// anything is - e.g. to skip layout work for animations that only
145    /// affect paint (colors, opacity) and not the box model.
146    pub fn active_keys(&self) -> impl Iterator<Item = &K> {
147        self.active.keys()
148    }
149
150    /// Whether any animation is currently in flight across all keys. Useful
151    /// to decide whether the render loop needs to keep polling for frames.
152    pub fn is_animating(&self) -> bool {
153        !self.active.is_empty()
154    }
155}