Skip to main content

pax_runtime_api/properties/
mod.rs

1use serde::{Deserialize, Serialize};
2use std::{marker::PhantomData, rc::Rc};
3
4mod graph_operations;
5mod properties_table;
6#[cfg(test)]
7mod tests;
8mod untyped_property;
9
10use crate::{Duration, EasingCurve, Interpolatable, TransitionQueueEntry};
11
12use self::properties_table::{PropertyType, PROPERTY_MILLIS, PROPERTY_TIME};
13pub use properties_table::EffectDrainReport;
14use properties_table::PROPERTY_TABLE;
15pub use untyped_property::UntypedProperty;
16
17/// Sealed PropertyId needed for slotmap (strictly internal)
18mod private {
19    slotmap::new_key_type!(
20        pub struct PropertyId;
21    );
22}
23
24/// Bound for values that can live inside Pax `Property<T>`.
25///
26/// Values must be cloneable for `.get()`, interpolatable for transitions, and
27/// `'static` because properties are stored in the runtime graph.
28pub trait PropertyValue: Default + Clone + Interpolatable + 'static {}
29impl<T: Default + Clone + Interpolatable + 'static> PropertyValue for T {}
30
31impl<T: PropertyValue> Interpolatable for Property<T> {
32    fn interpolate(&self, other: &Self, t: f64) -> Self {
33        let cp_self = self.clone();
34        let cp_other = other.clone();
35        Property::computed(
36            move || cp_self.get().interpolate(&cp_other.get(), t),
37            &[self.untyped(), other.untyped()],
38        )
39    }
40}
41/// A reactive value node in Pax's property graph.
42///
43/// `Property<T>` is the primary state and binding primitive used by generated
44/// components, PAXEL expressions, and Rust component logic.
45#[derive(Clone)]
46pub struct Property<T> {
47    untyped: UntypedProperty,
48    _phantom: PhantomData<T>,
49}
50
51impl<T: PropertyValue + std::fmt::Debug> std::fmt::Debug for Property<T> {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        write!(f, "Property ({:?})", self.get())
54    }
55}
56
57impl<T: PropertyValue> Property<T> {
58    /// Creates a literal property with an initial value.
59    pub fn new(val: T) -> Self {
60        Self::new_optional_name(val, None)
61    }
62
63    // Rewraps an untyped property with a typed view.
64    pub fn new_from_untyped(untyped: UntypedProperty) -> Self {
65        Self {
66            untyped,
67            _phantom: PhantomData {},
68        }
69    }
70
71    /// Creates a computed property from an evaluator and dependency list.
72    pub fn computed(evaluator: impl Fn() -> T + 'static, dependents: &[UntypedProperty]) -> Self {
73        Self::computed_with_config(evaluator, dependents, None, None)
74    }
75
76    /// Creates a computed property with a propagation cutoff.
77    ///
78    /// The predicate receives the last accepted value and the newly evaluated
79    /// candidate. Returning `true` discards the candidate and stops outbound
80    /// invalidation at this property; returning `false` accepts and propagates
81    /// it. The first evaluation is always accepted.
82    pub fn computed_with_cutoff(
83        evaluator: impl Fn() -> T + 'static,
84        dependents: &[UntypedProperty],
85        cutoff: impl Fn(&T, &T) -> bool + 'static,
86    ) -> Self {
87        Self::computed_with_config(evaluator, dependents, None, Some(Rc::new(cutoff)))
88    }
89
90    /// Creates a named literal property, useful for diagnostics.
91    pub fn new_with_name(val: T, name: &str) -> Self {
92        Self::new_optional_name(val, Some(name))
93    }
94
95    /// Creates a named computed property, useful for diagnostics.
96    pub fn computed_with_name(
97        evaluator: impl Fn() -> T + 'static,
98        dependents: &[UntypedProperty],
99        name: &str,
100    ) -> Self {
101        Self::computed_with_config(evaluator, dependents, Some(name), None)
102    }
103
104    /// Creates a named cutoff computed property, useful for diagnostics.
105    pub fn computed_with_cutoff_and_name(
106        evaluator: impl Fn() -> T + 'static,
107        dependents: &[UntypedProperty],
108        cutoff: impl Fn(&T, &T) -> bool + 'static,
109        name: &str,
110    ) -> Self {
111        Self::computed_with_config(evaluator, dependents, Some(name), Some(Rc::new(cutoff)))
112    }
113
114    fn new_optional_name(val: T, name: Option<&str>) -> Self {
115        Self {
116            untyped: UntypedProperty::new(val, Vec::with_capacity(0), PropertyType::Literal, name),
117            _phantom: PhantomData {},
118        }
119    }
120
121    fn computed_with_config(
122        evaluator: impl Fn() -> T + 'static,
123        dependents: &[UntypedProperty],
124        name: Option<&str>,
125        cutoff: Option<Rc<dyn Fn(&T, &T) -> bool>>,
126    ) -> Self {
127        let inbound: Vec<_> = dependents.iter().map(|v| v.get_id()).collect();
128        let start_val = T::default();
129        let evaluator = Rc::new(evaluator);
130        Self {
131            untyped: UntypedProperty::new(
132                start_val,
133                inbound,
134                PropertyType::Computed {
135                    evaluator,
136                    cutoff: cutoff.map(|predicate| properties_table::Cutoff {
137                        predicate,
138                        initialized: false,
139                    }),
140                },
141                name,
142            ),
143            _phantom: PhantomData {},
144        }
145    }
146
147    /// Immediately starts an ease transition from the current value to end_val, over a duration, following curve.
148    ///
149    /// Numeric arguments preserve the historical frame-based behavior. Use
150    /// `Duration::Milliseconds`, `Duration::Seconds`, or `Duration::Frames` to
151    /// select an explicit unit.
152    pub fn ease_to<D: Into<Duration>>(&self, end_val: T, duration: D, curve: EasingCurve) {
153        self.ease_to_value(end_val, duration.into(), curve, true);
154    }
155
156    /// Enqueues an ease transition from the current value to end_val, over a duration, following
157    /// curve, which will start after all currently enqueued transitions finish.
158    pub fn ease_to_later<D: Into<Duration>>(&self, end_val: T, duration: D, curve: EasingCurve) {
159        self.ease_to_value(end_val, duration.into(), curve, false);
160    }
161
162    /// Shared logic for easing operations
163    fn ease_to_value(&self, end_val: T, duration: Duration, curve: EasingCurve, overwrite: bool) {
164        PROPERTY_TABLE.with(|t| {
165            t.transition(
166                self.untyped.id,
167                TransitionQueueEntry {
168                    duration,
169                    curve,
170                    ending_value: end_val,
171                },
172                overwrite,
173            )
174        })
175    }
176
177    /// Stops the active transition and clears every queued transition segment.
178    ///
179    /// The property is left at its current eased value. A subsequent [`Property::set`]
180    /// can therefore take immediate ownership without the cancelled transition
181    /// overwriting it on the next runtime tick.
182    pub fn cancel_transitions(&self) {
183        PROPERTY_TABLE.with(|t| t.cancel_transitions::<T>(self.untyped.id));
184    }
185
186    /// Gets the currently stored value. Might be computationally
187    /// expensive in a large reactivity network since this triggers
188    /// re-evaluation of dirty property chains
189    pub fn get(&self) -> T {
190        PROPERTY_TABLE.with(|t| t.get_value(self.untyped.id))
191    }
192
193    /// Sets this properties value and sets the dirty bit recursively of all of
194    /// its dependencies if not already set
195    pub fn set(&self, val: T) {
196        PROPERTY_TABLE.with(|t| t.set_value(self.untyped.id, val));
197    }
198
199    /// Marks this property for re-evaluation without replacing its value or evaluator.
200    ///
201    /// This is useful for computed properties whose evaluator observes runtime state
202    /// outside the reactive dependency graph.
203    #[doc(hidden)]
204    pub fn invalidate(&self) {
205        PROPERTY_TABLE.with(|t| t.invalidate(self.untyped.id));
206    }
207
208    /// Sets the value only when it differs from the current one.
209    ///
210    /// Returns `true` when the write changed the property and dirtied dependents.
211    pub fn set_if_neq(&self, val: T) -> bool
212    where
213        T: PartialEq,
214    {
215        let should_set =
216            PROPERTY_TABLE.with(|t| t.read_value(self.untyped.id, |current: &T| current != &val));
217        if should_set {
218            self.set(val);
219        }
220        should_set
221    }
222
223    /// Get access to a mutable reference to the inner value T.
224    /// Will trigger updates for dependents of this property, regardless
225    /// of if the value actually changed
226    pub fn update(&self, f: impl FnOnce(&mut T)) {
227        // This is a temporary impl of the update method.
228        // (very bad perf comparatively, but very safe).
229        let mut val = self.get();
230        f(&mut val);
231        self.set(val);
232    }
233
234    /// Reads the inner value by reference.
235    ///
236    /// Panics if this property is already borrowed, which can happen if `read`
237    /// is called inside a read of the same property.
238    pub fn read<V>(&self, f: impl FnOnce(&T) -> V) -> V {
239        PROPERTY_TABLE.with(|t| t.read_value(self.untyped.id, f))
240    }
241
242    /// Replaces this property's evaluator, dependencies, and value with `target`, while keeping dependents.
243    ///
244    /// This can introduce circular dependencies if used carelessly. It is
245    /// intended for changing a property from literal to computed (or vice
246    /// versa) without severing existing outbound links.
247    pub fn replace_with(&self, target: Property<T>) {
248        PROPERTY_TABLE.with(|t| {
249            // we know self contains T, and that target contains T, so this should never panic
250            t.replace_property_keep_outbound_connections::<T>(self.untyped.id, target.untyped.id)
251        })
252    }
253
254    /// Casts this property to its untyped version.
255    pub fn untyped(&self) -> UntypedProperty {
256        self.untyped.clone()
257    }
258}
259
260impl<T: PropertyValue> Default for Property<T> {
261    fn default() -> Self {
262        Property::new(T::default())
263    }
264}
265
266// Serialization and deserialization fully disconnects properties,
267// and only loads them back in as literal values.
268impl<'de, T: PropertyValue + Deserialize<'de>> Deserialize<'de> for Property<T> {
269    fn deserialize<D>(deserializer: D) -> Result<Property<T>, D::Error>
270    where
271        D: serde::Deserializer<'de>,
272    {
273        let value = T::deserialize(deserializer)?;
274        Ok(Property::new(value))
275    }
276}
277
278impl<T: PropertyValue + Serialize> Serialize for Property<T> {
279    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
280    where
281        S: serde::Serializer,
282    {
283        // TODO check if literal or computed, error on computed?
284        self.get().serialize(serializer)
285    }
286}
287
288// Utility method to inspect total entry count in property table.
289pub fn property_table_total_properties_count() -> usize {
290    PROPERTY_TABLE.with(|t| t.total_properties_count())
291}
292
293#[doc(hidden)]
294pub fn register_effect_property(prop: &Property<()>) {
295    PROPERTY_TABLE.with(|t| t.register_effect(prop.untyped.id));
296}
297
298#[doc(hidden)]
299pub fn register_effect_property_with_name(prop: &Property<()>, debug_name: &str) {
300    PROPERTY_TABLE.with(|t| t.register_effect_with_name(prop.untyped.id, Some(debug_name)));
301}
302
303#[doc(hidden)]
304pub fn drain_effects(max_iterations: usize) -> usize {
305    PROPERTY_TABLE.with(|t| t.drain_effects(max_iterations))
306}
307
308#[doc(hidden)]
309pub fn drain_effects_with_report(max_iterations: usize) -> EffectDrainReport {
310    PROPERTY_TABLE.with(|t| t.drain_effects_with_report(max_iterations))
311}
312
313#[doc(hidden)]
314pub fn property_outbound_debug_names(prop: &UntypedProperty) -> Vec<String> {
315    PROPERTY_TABLE.with(|t| t.outbound_debug_names(prop.id))
316}
317
318#[doc(hidden)]
319pub fn property_has_direct_outbound(source: &UntypedProperty, outbound: &UntypedProperty) -> bool {
320    PROPERTY_TABLE.with(|t| t.has_direct_outbound(source.id, outbound.id))
321}
322
323// Registers the runtime clock property used by transition/easing machinery.
324pub fn register_time(prop: &Property<u64>) {
325    PROPERTY_TIME.with_borrow_mut(|time| *time = prop.clone());
326}
327
328// Registers the runtime wall clock property used by time-based transition/easing machinery.
329pub fn register_millis(prop: &Property<u64>) {
330    PROPERTY_MILLIS.with_borrow_mut(|time| *time = prop.clone());
331}