Add support for recursive dependency tracking across PropertyTracker instances

By default PropertyTracker::evaluate() registers the currently
evaluating binding/tracker as a dependency. This should help with
repeaters and other scenarios where in the run-time we use property
trackers but want to track the overall "dirtyness" in the window with
regards to whether a redraw is needed or not.

The new evaluate_as_dependency_root() function allows skipping this
mechanism and is used for the two trackers in the window.
This commit is contained in:
Simon Hausmann 2021-03-24 13:51:23 +01:00
parent e63baa5766
commit 3bd5b0eadf
5 changed files with 115 additions and 16 deletions

View file

@ -228,6 +228,22 @@ struct PropertyTracker
return result;
}
template<typename F>
auto evaluate_as_dependency_root(const F &f) const -> std::enable_if_t<std::is_same_v<decltype(f()), void>>
{
cbindgen_private::sixtyfps_property_tracker_evaluate_as_dependency_root(
&inner, [](void *f) { (*reinterpret_cast<const F *>(f))(); }, const_cast<F *>(&f));
}
template<typename F>
auto evaluate_as_dependency_root(const F &f) const
-> std::enable_if_t<!std::is_same_v<decltype(f()), void>, decltype(f())>
{
decltype(f()) result;
this->evaluate_as_dependency_root([&] { result = f(); });
return result;
}
private:
cbindgen_private::PropertyTrackerOpaque inner;
};

View file

@ -42,3 +42,25 @@ TEST_CASE("Basic SharedVector API", "[vector]")
REQUIRE(vec[2] == 10);
}
}
TEST_CASE("Property Tracker")
{
using namespace sixtyfps;
PropertyTracker tracker1;
PropertyTracker tracker2;
Property<int> prop(42);
auto r = tracker1.evaluate([&]() { return tracker2.evaluate([&]() { return prop.get(); }); });
REQUIRE(r == 42);
prop.set(1);
REQUIRE(tracker2.is_dirty());
REQUIRE(tracker1.is_dirty());
r = tracker1.evaluate(
[&]() { return tracker2.evaluate_as_dependency_root([&]() { return prop.get(); }); });
REQUIRE(r == 1);
prop.set(100);
REQUIRE(tracker2.is_dirty());
REQUIRE(!tracker1.is_dirty());
}

View file

@ -167,6 +167,18 @@ struct BindingHolder<B = ()> {
binding: B,
}
impl BindingHolder {
fn register_self_as_dependency(
self: Pin<&Self>,
property_that_will_notify: *mut DependencyListHead,
) {
let node = DependencyNode::for_binding(self);
let mut dep_nodes = self.dep_nodes.borrow_mut();
let node = dep_nodes.push_front(node);
unsafe { DependencyListHead::append(property_that_will_notify, node.get_ref() as *const _) }
}
}
fn alloc_binding_holder<B: BindingCallable + 'static>(binding: B) -> *mut BindingHolder {
/// Safety: _self must be a pointer that comes from a `Box<BindingHolder<B>>::into_raw()`
unsafe fn binding_drop<B>(_self: *mut BindingHolder) {
@ -447,12 +459,7 @@ impl PropertyHandle {
fn register_as_dependency_to_current_binding(&self) {
if CURRENT_BINDING.is_set() {
CURRENT_BINDING.with(|cur_binding| {
let node = DependencyNode::for_binding(cur_binding);
let mut dep_nodes = cur_binding.dep_nodes.borrow_mut();
let node = dep_nodes.push_front(node);
unsafe {
DependencyListHead::append(self.dependencies(), node.get_ref() as *const _)
}
cur_binding.register_self_as_dependency(self.dependencies());
});
}
}
@ -1302,9 +1309,27 @@ impl PropertyTracker {
}
/// Evaluate the function, and record dependencies of properties accessed whithin this function.
/// If this is called during the evaluation of another property binding or property tracker, then
/// any changes to accessed properties will also mark the other binding/tracker dirty.
pub fn evaluate<R>(self: Pin<&Self>, f: impl FnOnce() -> R) -> R {
if CURRENT_BINDING.is_set() {
CURRENT_BINDING.with(|cur_binding| {
cur_binding.register_self_as_dependency(
self.holder.dependencies.as_ptr() as *mut DependencyListHead
)
});
}
self.evaluate_as_dependency_root(f)
}
/// Evaluate the function, and record dependencies of properties accessed whithin this function.
/// If this is called during the evaluation of another property binding or property tracker, then
/// any changes to accessed properties will not propagate to the other tracker.
pub fn evaluate_as_dependency_root<R>(self: Pin<&Self>, f: impl FnOnce() -> R) -> R {
// clear all the nodes so that we can start from scratch
*self.holder.dep_nodes.borrow_mut() = Default::default();
// Safety: it is safe to project the holder as we don't implement drop or unpin
let pinned_holder = unsafe { self.map_unchecked(|s| &s.holder) };
let r = CURRENT_BINDING.set(pinned_holder, f);
@ -1351,6 +1376,28 @@ fn test_property_listener_scope() {
assert!(ok);
}
#[test]
fn test_nested_property_trackers() {
let tracker1 = Box::pin(PropertyTracker::default());
let tracker2 = Box::pin(PropertyTracker::default());
let prop = Box::pin(Property::new(42));
let r = tracker1.as_ref().evaluate(|| tracker2.as_ref().evaluate(|| prop.as_ref().get()));
assert_eq!(r, 42);
prop.as_ref().set(1);
assert!(tracker2.as_ref().is_dirty());
assert!(tracker1.as_ref().is_dirty());
let r = tracker1
.as_ref()
.evaluate(|| tracker2.as_ref().evaluate_as_dependency_root(|| prop.as_ref().get()));
assert_eq!(r, 1);
prop.as_ref().set(100);
assert!(tracker2.as_ref().is_dirty());
assert!(!tracker1.as_ref().is_dirty());
}
#[cfg(feature = "ffi")]
pub(crate) mod ffi {
use super::*;
@ -1758,6 +1805,7 @@ pub(crate) mod ffi {
}
/// Call the callback with the user data. Any properties access within the callback will be registered.
/// Any currently evaluated bindings or property trackers will be notified if accessed properties are changed.
#[no_mangle]
pub unsafe extern "C" fn sixtyfps_property_tracker_evaluate(
handle: *const PropertyTrackerOpaque,
@ -1767,6 +1815,17 @@ pub(crate) mod ffi {
Pin::new_unchecked(&*(handle as *const PropertyTracker)).evaluate(|| callback(user_data))
}
/// Call the callback with the user data. Any properties access within the callback will be registered.
/// Any currently evaluated bindings or property trackers will be not notified if accessed properties are changed.
#[no_mangle]
pub unsafe extern "C" fn sixtyfps_property_tracker_evaluate_as_dependency_root(
handle: *const PropertyTrackerOpaque,
callback: extern "C" fn(user_data: *mut c_void),
user_data: *mut c_void,
) {
Pin::new_unchecked(&*(handle as *const PropertyTracker))
.evaluate_as_dependency_root(|| callback(user_data))
}
/// Query if the property tracker is dirty
#[no_mangle]
pub unsafe extern "C" fn sixtyfps_property_tracker_is_dirty(

View file

@ -285,7 +285,7 @@ impl GraphicsWindow {
{
if self.meta_property_listener.as_ref().is_dirty() {
self.meta_property_listener.as_ref().evaluate(|| {
self.meta_property_listener.as_ref().evaluate_as_dependency_root(|| {
self.apply_geometry_constraint(component.as_ref().layout_info());
component.as_ref().apply_layout(self.get_geometry());

View file

@ -780,7 +780,8 @@ impl QtWindow {
let component_rc = self.self_weak.upgrade().unwrap().component();
let component = ComponentRc::borrow_pin(&component_rc);
self.meta_property_listener.as_ref().evaluate_if_dirty(|| {
if self.meta_property_listener.as_ref().is_dirty() {
self.meta_property_listener.as_ref().evaluate_as_dependency_root(|| {
self.apply_geometry_constraint(component.as_ref().layout_info());
component.as_ref().apply_layout(Default::default());
@ -789,9 +790,10 @@ impl QtWindow {
self.apply_window_properties(window_item);
}
});
}
let cache = self.cache.clone();
self.redraw_listener.as_ref().evaluate(|| {
self.redraw_listener.as_ref().evaluate_as_dependency_root(|| {
let mut renderer = QtItemRenderer {
painter,
cache,