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