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
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
use std::{any::Any, cell::RefCell, rc::Rc};
use slotmap::{SlotMap, SparseSecondaryMap};
use crate::{Property, TransitionManager, TransitionQueueEntry};
use super::{private::PropertyId, PropertyValue};
thread_local! {
/// Global property table used to store data backing dirty-dag
pub(crate) static PROPERTY_TABLE: PropertyTable = PropertyTable::default();
/// Property time variable, to be used by
pub(crate) static PROPERTY_TIME: RefCell<Property<u64>> = RefCell::new(Property::new(0));
}
/// The main collection of data associated with a specific property id
pub struct PropertyData {
// typed data for this property,
// can always be downcast to TypedPropertyData<T>
// where T matches the property type
typed_data: Box<dyn Any>,
// List of properties that this property depends on
pub inbound: Vec<PropertyId>,
// List of properties that depend on this value
pub outbound: Vec<PropertyId>,
// Dirty bit set if a dependency further up in the dirty-dag tree
// has been changed. For computed this can be any other props,
// for literals, only time variable
pub dirty: bool,
}
impl PropertyData {
fn typed_data<T: 'static>(&mut self) -> &mut TypedPropertyData<T> {
self.typed_data
.downcast_mut::<TypedPropertyData<T>>()
.unwrap_or_else(|| panic!("Failed to downcast to TypedPropertyData<{}>. The actual type does not match the expected type.", std::any::type_name::<T>()))
}
}
pub struct TypedPropertyData<T> {
value: T,
transition_manager: Option<TransitionManager<T>>,
// Specialization data (computed/literal etc)
property_type: PropertyType<T>,
}
/// Specialization data only needed for different kinds of properties
#[derive(Clone)]
pub(crate) enum PropertyType<T> {
Literal,
Computed {
// Information needed to recompute on change
evaluator: Rc<dyn Fn() -> T>,
},
}
/// Main propertytable, containing data associated with each property
#[derive(Default)]
pub(crate) struct PropertyTable {
// Main property table containing property data
// Box<dyn Any> is of type Box<Entry<T>> where T is the proptype
pub(crate) property_map: RefCell<SlotMap<PropertyId, Entry>>,
debug_names: RefCell<SparseSecondaryMap<PropertyId, String>>,
}
pub struct Entry {
ref_count: usize,
data: Option<PropertyData>,
}
impl PropertyTable {
/// Main function to get access to a value inside of a property.
/// Makes sure the value is up to date before returning in the case
/// of computed properties.
pub fn get_value<T: PropertyValue>(&self, id: PropertyId) -> T {
self.update_value::<T>(id);
self.with_property_data_mut(id, |property_data| {
property_data.typed_data::<T>().value.clone()
})
}
pub fn read_value<T: PropertyValue, V>(&self, id: PropertyId, f: impl FnOnce(&T) -> V) -> V {
self.update_value::<T>(id);
self.with_property_data_mut(id, |property_data| {
f(&property_data.typed_data::<T>().value)
})
}
// Main function to set a value of a property.
// NOTE: This always assumes the underlying data was changed, and marks
// it and its dependents as dirty irrespective of actual modification
pub fn set_value<T: PropertyValue>(&self, id: PropertyId, new_val: T) {
self.with_property_data_mut(id, |property_data: &mut PropertyData| {
let typed_data = property_data.typed_data();
typed_data.value = new_val;
property_data.dirty = false;
});
self.dirtify_outbound(id);
}
/// Adds a new untyped property entry
pub fn add_entry<T: PropertyValue>(
&self,
start_val: T,
inbound: Vec<PropertyId>,
data: PropertyType<T>,
debug_name: Option<&str>,
) -> PropertyId {
let id = {
let Ok(mut sm) = self.property_map.try_borrow_mut() else {
panic!(
"couldn't create new property \"{}\"- table already borrowed",
debug_name.unwrap_or("<no name>")
);
};
let entry = Entry {
ref_count: 1,
data: Some(PropertyData {
inbound,
dirty: true,
typed_data: Box::new(TypedPropertyData {
value: start_val,
property_type: data,
transition_manager: None,
}),
outbound: Vec::with_capacity(0),
}),
};
sm.insert(entry)
};
if let Some(name) = debug_name {
self.debug_names.borrow_mut().insert(id, name.to_owned());
}
self.connect_inbound(id);
id
}
// Add a transition to the transitionmanager, making the value slowly change over time
// Currently this only transitions the literal value of the property (and updates dependents accordingly)
// This has no special interactions with computed properties
pub fn transition<T: PropertyValue>(
&self,
id: PropertyId,
transition: TransitionQueueEntry<T>,
overwrite: bool,
) {
let mut should_connect_to_time = false;
let (time_id, curr_time) = PROPERTY_TIME.with_borrow(|time| (time.untyped.id, time.get()));
self.with_property_data_mut(id, |property_data: &mut PropertyData| {
let typed_data = property_data.typed_data::<T>();
let transition_manager = typed_data
.transition_manager
.get_or_insert_with(|| TransitionManager::new(typed_data.value.clone(), curr_time));
if overwrite {
transition_manager.reset_transitions(curr_time);
}
transition_manager.push_transition(transition);
if !property_data.inbound.contains(&time_id) {
should_connect_to_time = true;
property_data.inbound.push(time_id);
}
});
if should_connect_to_time {
self.connect_inbound(id);
}
}
/// Gives mutable access to a entry in the property table
/// WARNING: this function is dangerous, f can not drop, create, set, get
/// or in any other way modify the global property table or this will panic
/// with multiple mutable borrows. Letting f contain any form of userland
/// code is NOT a good idea.
fn with_entry_mut<V>(&self, id: PropertyId, f: impl FnOnce(&mut Entry) -> V) -> V {
let mut sm = self.property_map.borrow_mut();
let data = sm.get_mut(id).unwrap();
let return_value = f(data);
return_value
}
/// Allows mutable access to the data associated with a property id.
/// WARNING while this method is being run, the entry corresponding to id
/// is not present in the table, and methods such as .get(), .set(), and
/// possibly others on the property with the id parameter bellow will panic.
pub fn with_property_data_mut<V>(
&self,
id: PropertyId,
f: impl FnOnce(&mut PropertyData) -> V,
) -> V {
// take the value out of the table
let mut property_data = self.with_entry_mut(id, |entry| {
entry
.data
.take()
.expect("property data should not have already been taken")
});
// run f, without table being borrowed
// NOTE: need to make sure with_property_data_mut is not run recursively
// with the same id
let res = f(&mut property_data);
// return value to the table that was taken
self.with_entry_mut(id, |entry| {
entry.data = Some(property_data);
});
res
}
/// Allows access to the data associated with a property id.
/// WARNING while this method is being run, the entry corresponding to id
/// is not present in the table, and methods such as .get(), .set(), and
/// possibly others on the property with the id parameter bellow will panic.
pub fn with_property_data<V>(&self, id: PropertyId, f: impl FnOnce(&PropertyData) -> V) -> V {
self.with_property_data_mut(id, |property_data| f(&*property_data))
}
/// Increase the ref count of a property
pub fn increase_ref_count(&self, id: PropertyId) -> usize {
self.with_entry_mut(id, |entry| {
entry.ref_count += 1;
entry.ref_count
})
}
/// Decrease the ref count of a property
pub fn decrease_ref_count(&self, id: PropertyId) -> usize {
self.with_entry_mut(id, |entry| {
entry.ref_count -= 1;
entry.ref_count
})
}
/// Replaces the way the source parameters property is being
/// computed / its value to the way target does.
/// NOTE: source_id and target_id need to both contain
/// the type T, or else this panics
pub fn replace_property_keep_outbound_connections<T: Clone + 'static>(
&self,
source_id: PropertyId,
target_id: PropertyId,
) {
// disconnect self from its dependents, in preparation of overwriting
// with targets inbound. (only does something for computed values)
self.disconnect_inbound(source_id);
// copy necessary internal state from target to source
self.with_property_data_mut(source_id, |source_property_data| {
self.with_property_data_mut(target_id, |target_property_data| {
// Copy over inbound, dirty state, and current value to source
source_property_data.inbound = target_property_data.inbound.clone();
source_property_data.dirty = target_property_data.dirty;
let source_typed = source_property_data.typed_data::<T>();
let target_typed = target_property_data.typed_data::<T>();
source_typed.value = target_typed.value.clone();
source_typed.property_type = target_typed.property_type.clone();
});
});
// connect self to its new dependents (found in property_types Expr
// type as inbound) (only does something for computed values)
self.connect_inbound(source_id);
// make sure dependencies of self
// know that something has changed
self.dirtify_outbound(source_id);
// overwrite with more descriptive name
let target_name = self.debug_name(target_id);
let mut names = self.debug_names.borrow_mut();
names.insert(source_id, format!("{}", target_name));
}
// re-computes the value if dirty
pub fn update_value<T: PropertyValue>(&self, id: PropertyId) {
let mut remove_dep_from_literal = false;
let evaluator = self.with_property_data_mut(id, |property_data| {
//short circuit if the value is still up to date
if property_data.dirty == false {
return None;
}
property_data.dirty = false;
let typed_data = property_data.typed_data::<T>();
match &mut typed_data.property_type {
PropertyType::Computed { evaluator, .. } => Some(Rc::clone(&evaluator)),
PropertyType::Literal => {
let tm = typed_data.transition_manager.as_mut()?;
let curr_time = PROPERTY_TIME.with_borrow(|time| time.get());
let value = tm.compute_eased_value(curr_time);
if let Some(interp_value) = value {
typed_data.value = interp_value;
} else {
//transition must be over, let's remove dependencies
remove_dep_from_literal = true;
typed_data.transition_manager = None;
}
None
}
}
});
if remove_dep_from_literal {
self.disconnect_inbound(id);
self.with_property_data_mut(id, |property_data| {
property_data.inbound.clear();
});
}
if let Some(evaluator) = evaluator {
// WARNING: the evaluator should not be run while the table is in
// an invalid state (borrowed, in with_property_data closure, etc.)
// as this function is provided by a user of the property system and
// can do arbitrary sets/ gets/drops etc (that need the prop data)
let new_value = { evaluator() };
self.with_property_data_mut(id, |property_data| {
let typed_data = property_data.typed_data();
typed_data.value = new_value;
})
}
}
/// drop a properties underlying data, making any subsequent calls invalid by panic
pub fn remove_entry(&self, id: PropertyId) {
let res = {
self.disconnect_outbound(id);
self.disconnect_inbound(id);
let Ok(mut sm) = self.property_map.try_borrow_mut() else {
panic!(
"failed to remove property \"{}\" - propertytable already borrowed",
self.debug_name(id),
);
};
sm.remove(id).expect("tried to remove non-existent prop")
};
drop(res);
}
pub fn debug_name(&self, id: PropertyId) -> String {
self.debug_names
.borrow()
.get(id)
.as_ref()
.map(|s| s.as_str())
.unwrap_or("<NO DEBUG NAME>")
.to_owned()
}
pub(crate) fn total_properties_count(&self) -> usize {
self.property_map.borrow().len()
}
}