Skip to main content

pax_runtime_api/properties/
local_property.rs

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