Skip to main content

bevy_react/protocol/
animatable.rs

1//! [`Animatable<T>`] — the `{ animated }` wrapper every animatable style/attr
2//! field decodes through — and the [`AnimatableField`] read helpers.
3
4use serde::Deserialize;
5use serde::de::{self, Deserializer};
6
7use super::decode_warn;
8
9#[derive(Debug, Clone, PartialEq)]
10pub enum Animatable<T> {
11    Static(T),
12    Animated {
13        binding: crate::animations::protocol::Binding,
14        /// The wrapper's sibling `seed`, decoded as `T` (a malformed seed
15        /// warns `styleBinding` and drops to `None`).
16        ///
17        /// While an animation driver runs, the seed carries the **last driven
18        /// value**: the apply stage (`crate::animations`' shape-attr stage)
19        /// writes each frame's resolved value into this slot — never
20        /// replacing the variant with `Static`, which would destroy the
21        /// binding — so seed-rendering read sites (`static_or_seed`) see the
22        /// live value while the binding survives re-derivation.
23        seed: Option<T>,
24    },
25}
26
27impl<T> Animatable<T> {
28    /// The static value; `None` while animated (the seed is NOT a static
29    /// value — see [`Self::seed`]).
30    pub fn value(&self) -> Option<&T> {
31        match self {
32            Animatable::Static(v) => Some(v),
33            Animatable::Animated { .. } => None,
34        }
35    }
36
37    /// The binding; `None` when static.
38    pub fn binding(&self) -> Option<&crate::animations::protocol::Binding> {
39        match self {
40            Animatable::Static(_) => None,
41            Animatable::Animated { binding, .. } => Some(binding),
42        }
43    }
44
45    /// The animated wrapper's `seed`; `None` when static or seed-less.
46    pub fn seed(&self) -> Option<&T> {
47        match self {
48            Animatable::Static(_) => None,
49            Animatable::Animated { seed, .. } => seed.as_ref(),
50        }
51    }
52}
53
54/// Read helpers for the `Option<Animatable<T>>` style fields, so read sites
55/// stay as terse as the plain `Option<T>` they replaced.
56pub trait AnimatableField<T> {
57    /// The static value by copy; `None` when absent **or** animated.
58    fn static_val(&self) -> Option<T>
59    where
60        T: Copy;
61    /// The static value by reference; `None` when absent or animated.
62    fn static_ref(&self) -> Option<&T>;
63    /// The static value — or, while animated, the wrapper's `seed`; `None`
64    /// when absent or animated seed-less. The read helper for fields whose
65    /// consumers should *render* the seed until a driver writes (SVG shape
66    /// attrs); style read sites use [`Self::static_val`] instead (their
67    /// animated fields read as absent by design).
68    fn static_or_seed(&self) -> Option<T>
69    where
70        T: Copy;
71    /// The binding; `None` when absent or static.
72    fn binding(&self) -> Option<&crate::animations::protocol::Binding>;
73}
74
75impl<T> AnimatableField<T> for Option<Animatable<T>> {
76    fn static_val(&self) -> Option<T>
77    where
78        T: Copy,
79    {
80        self.static_ref().copied()
81    }
82    fn static_ref(&self) -> Option<&T> {
83        self.as_ref().and_then(Animatable::value)
84    }
85    fn static_or_seed(&self) -> Option<T>
86    where
87        T: Copy,
88    {
89        self.as_ref()
90            .and_then(|a| a.value().or_else(|| a.seed()))
91            .copied()
92    }
93    fn binding(&self) -> Option<&crate::animations::protocol::Binding> {
94        self.as_ref().and_then(Animatable::binding)
95    }
96}
97
98impl<'de, T: de::DeserializeOwned> Deserialize<'de> for Animatable<T> {
99    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
100        let v = serde_json::Value::deserialize(d)?;
101        if let Some(map) = v.as_object()
102            && let Some(inner) = map.get("animated")
103        {
104            let seed = map.get("seed").and_then(|s| match T::deserialize(s) {
105                Ok(seed) => Some(seed),
106                Err(e) => {
107                    decode_warn(
108                        "styleBinding",
109                        &s.to_string(),
110                        &format!("invalid seed: {e}"),
111                    );
112                    None
113                }
114            });
115            return Ok(Animatable::Animated {
116                binding: binding_from_wrapper(inner),
117                seed,
118            });
119        }
120        T::deserialize(v)
121            .map(Animatable::Static)
122            .map_err(de::Error::custom)
123    }
124}
125
126/// Decode the payload of an `{ animated: … }` wrapper: a descriptor object
127/// (tagged by `type`) decodes as a [`Binding`](crate::animations::protocol::Binding);
128/// a bare shared value is recognized by its numeric `id` (every other
129/// enumerable field of the JS handle is ignored). Malformed → warn + inert.
130pub(crate) fn binding_from_wrapper(
131    inner: &serde_json::Value,
132) -> crate::animations::protocol::Binding {
133    use crate::animations::protocol::Binding;
134    let inert = Binding::Shared { id: 0 };
135    let Some(map) = inner.as_object() else {
136        decode_warn(
137            "styleBinding",
138            &inner.to_string(),
139            "animated must be a shared value or an interpolate/interpolateColor descriptor",
140        );
141        return inert;
142    };
143    if map.contains_key("type") {
144        match Binding::deserialize(inner) {
145            Ok(b) => b,
146            Err(e) => {
147                decode_warn("styleBinding", &inner.to_string(), &e.to_string());
148                inert
149            }
150        }
151    } else if let Some(id) = map.get("id").and_then(serde_json::Value::as_u64) {
152        Binding::Shared { id: id as u32 }
153    } else {
154        decode_warn(
155            "styleBinding",
156            &inner.to_string(),
157            "animated needs a shared value ({id}) or a descriptor ({type, id, …})",
158        );
159        inert
160    }
161}