Skip to main content

aura_anim_iced/
property.rs

1//! Visual property identifiers, value containers, and composition helpers.
2
3mod kind;
4mod spec;
5#[cfg(test)]
6mod tests;
7mod value;
8
9pub use kind::{Color, PropertyValueKind, Rectangle, Scalar, Shadow, Size, Transform, Vector2};
10pub use spec::{PropertySpec, RawPropertySpec};
11pub use value::{PropertyValue, TransformValue};
12
13/// Built-in opacity property.
14pub const OPACITY: PropertySpec<Scalar> =
15    PropertySpec::new(PropertyKey::new("aura", "opacity"), 10);
16/// Built-in transform property.
17pub const TRANSFORM: PropertySpec<Transform> =
18    PropertySpec::new(PropertyKey::new("aura", "transform"), 18);
19/// Built-in translate property.
20pub const TRANSLATE: PropertySpec<Vector2> =
21    PropertySpec::new(PropertyKey::new("aura", "translate"), 19);
22/// Built-in uniform scale property.
23pub const SCALE: PropertySpec<Scalar> = PropertySpec::new(PropertyKey::new("aura", "scale"), 20);
24/// Built-in rotate property.
25pub const ROTATE: PropertySpec<Scalar> = PropertySpec::new(PropertyKey::new("aura", "rotate"), 21);
26/// Built-in width property.
27pub const WIDTH: PropertySpec<Scalar> = PropertySpec::new(PropertyKey::new("aura", "width"), 30);
28/// Built-in height property.
29pub const HEIGHT: PropertySpec<Scalar> = PropertySpec::new(PropertyKey::new("aura", "height"), 31);
30/// Built-in padding property.
31pub const PADDING: PropertySpec<Scalar> =
32    PropertySpec::new(PropertyKey::new("aura", "padding"), 40);
33/// Built-in border radius property.
34pub const RADIUS: PropertySpec<Scalar> = PropertySpec::new(PropertyKey::new("aura", "radius"), 50);
35/// Built-in background color property.
36pub const BACKGROUND: PropertySpec<Color> =
37    PropertySpec::new(PropertyKey::new("aura", "background"), 60);
38/// Built-in border color property.
39pub const BORDER_COLOR: PropertySpec<Color> =
40    PropertySpec::new(PropertyKey::new("aura", "border-color"), 70);
41/// Built-in text color property.
42pub const TEXT_COLOR: PropertySpec<Color> =
43    PropertySpec::new(PropertyKey::new("aura", "text-color"), 80);
44/// Built-in shadow property.
45pub const SHADOW: PropertySpec<Shadow> = PropertySpec::new(PropertyKey::new("aura", "shadow"), 90);
46
47/// A type-erased collection of sampled properties for one animation target.
48///
49/// Later entries with the same [`RawPropertySpec`] replace earlier entries when
50/// snapshots are merged. The runtime uses this as the value passed from
51/// keyframes/timelines into target-scoped ticks.
52///
53/// # Example
54///
55/// ```
56/// use aura_anim_iced::property::{self, PropertySnapshot, PropertyValue};
57///
58/// let mut base = PropertySnapshot::from((property::OPACITY, 0.25));
59/// base.merge(PropertySnapshot::from(vec![
60///     (property::OPACITY, 1.0),
61///     (property::SCALE, 1.05),
62/// ]));
63///
64/// let opacity = base
65///     .find_property(&property::OPACITY.raw())
66///     .expect("opacity sample");
67///
68/// assert_eq!(opacity.value(), &PropertyValue::Scalar(1.0));
69/// ```
70#[derive(Debug, Clone, PartialEq)]
71pub struct PropertySnapshot {
72    entries: Vec<PropertyEntry>,
73}
74
75impl<K: PropertyValueKind> From<Vec<(PropertySpec<K>, K::Inner)>> for PropertySnapshot {
76    fn from(value: Vec<(PropertySpec<K>, K::Inner)>) -> Self {
77        let mut s = Self {
78            entries: value
79                .into_iter()
80                .map(|(spec, value)| PropertyEntry::new(spec, value))
81                .collect(),
82        };
83        s.sort_by_composition_key();
84
85        s
86    }
87}
88
89impl<K: PropertyValueKind> From<(PropertySpec<K>, K::Inner)> for PropertySnapshot {
90    fn from(value: (PropertySpec<K>, K::Inner)) -> Self {
91        Self {
92            entries: vec![PropertyEntry::new(value.0, value.1)],
93        }
94    }
95}
96
97impl From<Vec<PropertyEntry>> for PropertySnapshot {
98    fn from(entries: Vec<PropertyEntry>) -> Self {
99        let mut s = Self { entries };
100        s.sort_by_composition_key();
101
102        s
103    }
104}
105
106impl PropertySnapshot {
107    /// Creates an empty property snapshot.
108    #[must_use]
109    pub const fn new() -> Self {
110        Self {
111            entries: Vec::new(),
112        }
113    }
114
115    /// Returns whether the snapshot contains no properties.
116    #[must_use]
117    pub const fn is_empty(&self) -> bool {
118        self.entries.is_empty()
119    }
120
121    /// Returns the sampled properties in composition order when normalized.
122    #[must_use]
123    pub fn entries(&self) -> &[PropertyEntry] {
124        &self.entries
125    }
126
127    pub(crate) fn sort_by_composition_key(&mut self) {
128        self.entries
129            .sort_by_key(|entry| entry.spec.composition_order());
130    }
131
132    /// Merges another snapshot into this one.
133    ///
134    /// Existing properties with the same raw spec are replaced; new properties
135    /// are appended and the result is sorted by composition order.
136    pub fn merge(&mut self, other: Self) {
137        self.merge_unsorted(other);
138        self.sort_by_composition_key();
139    }
140
141    /// Returns the entry for a raw property spec, if present.
142    #[must_use]
143    pub fn find_property(&self, property: &RawPropertySpec) -> Option<&PropertyEntry> {
144        self.entries.iter().find(|entry| entry.spec == *property)
145    }
146
147    pub(crate) fn push(&mut self, entry: PropertyEntry) {
148        self.entries.push(entry);
149    }
150
151    pub(crate) fn clear(&mut self) {
152        self.entries.clear();
153    }
154
155    pub(crate) fn replace_from(&mut self, other: &Self) {
156        self.entries.clear();
157        self.entries.extend_from_slice(other.entries());
158    }
159
160    pub(crate) fn with_capacity(capacity: usize) -> Self {
161        Self {
162            entries: Vec::with_capacity(capacity),
163        }
164    }
165
166    pub(crate) fn merge_unsorted(&mut self, other: Self) {
167        other.entries.into_iter().for_each(|snapshot| {
168            if let Some(entry) = self.find_property_mut(&snapshot.spec) {
169                entry.value = snapshot.value;
170            } else {
171                self.entries.push(snapshot);
172            }
173        });
174    }
175
176    pub(crate) fn merge_entries_unsorted(&mut self, entries: &[PropertyEntry]) {
177        for snapshot in entries {
178            if let Some(entry) = self.find_property_mut(snapshot.spec()) {
179                entry.value = *snapshot.value();
180            } else {
181                self.entries.push(*snapshot);
182            }
183        }
184    }
185
186    fn find_property_mut(&mut self, property: &RawPropertySpec) -> Option<&mut PropertyEntry> {
187        self.entries
188            .iter_mut()
189            .find(|entry| entry.spec == *property)
190    }
191}
192
193impl Default for PropertySnapshot {
194    fn default() -> Self {
195        Self::new()
196    }
197}
198
199/// One sampled property value after type erasure.
200#[derive(Debug, Clone, Copy, PartialEq)]
201pub struct PropertyEntry {
202    spec: RawPropertySpec,
203    value: PropertyValue,
204}
205
206impl PropertyEntry {
207    /// Creates a sampled property entry from a typed property spec.
208    #[must_use]
209    pub fn new<K: PropertyValueKind>(spec: PropertySpec<K>, value: K::Inner) -> Self {
210        Self {
211            spec: spec.raw(),
212            value: K::wrap(value),
213        }
214    }
215
216    /// Returns the erased property spec.
217    #[must_use]
218    pub fn spec(&self) -> &RawPropertySpec {
219        &self.spec
220    }
221
222    /// Returns the erased property value.
223    #[must_use]
224    pub fn value(&self) -> &PropertyValue {
225        &self.value
226    }
227
228    pub(crate) fn new_unchecked(spec: RawPropertySpec, value: PropertyValue) -> Self {
229        Self { spec, value }
230    }
231}
232
233/// Stable property identity.
234///
235/// The namespace/name pair is used instead of a central enum so downstream code
236/// can declare additional properties without modifying this crate.
237///
238/// # Example
239///
240/// ```
241/// use aura_anim_iced::property::{PropertyKey, PropertySpec, Scalar};
242///
243/// const TOAST_Y: PropertySpec<Scalar> =
244///     PropertySpec::new(PropertyKey::new("app", "toast-y"), 21);
245///
246/// assert_eq!(TOAST_Y.raw().key().name(), "toast-y");
247/// ```
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
249pub struct PropertyKey {
250    namespace: &'static str,
251    name: &'static str,
252}
253
254impl PropertyKey {
255    /// Creates a property key.
256    #[must_use]
257    pub const fn new(namespace: &'static str, name: &'static str) -> Self {
258        Self { namespace, name }
259    }
260
261    /// Returns the key namespace.
262    #[must_use]
263    pub const fn namespace(&self) -> &'static str {
264        self.namespace
265    }
266
267    /// Returns the key name inside its namespace.
268    #[must_use]
269    pub const fn name(&self) -> &'static str {
270        self.name
271    }
272}