diff --git a/api/sixtyfps-cpp/include/sixtyfps_properties.h b/api/sixtyfps-cpp/include/sixtyfps_properties.h index 741e088a5..fdf7e3d66 100644 --- a/api/sixtyfps-cpp/include/sixtyfps_properties.h +++ b/api/sixtyfps-cpp/include/sixtyfps_properties.h @@ -228,6 +228,22 @@ struct PropertyTracker return result; } + template + auto evaluate_as_dependency_root(const F &f) const -> std::enable_if_t> + { + cbindgen_private::sixtyfps_property_tracker_evaluate_as_dependency_root( + &inner, [](void *f) { (*reinterpret_cast(f))(); }, const_cast(&f)); + } + + template + auto evaluate_as_dependency_root(const F &f) const + -> std::enable_if_t, decltype(f())> + { + decltype(f()) result; + this->evaluate_as_dependency_root([&] { result = f(); }); + return result; + } + private: cbindgen_private::PropertyTrackerOpaque inner; }; diff --git a/api/sixtyfps-cpp/tests/datastructures.cpp b/api/sixtyfps-cpp/tests/datastructures.cpp index e5edaba2a..2a5023897 100644 --- a/api/sixtyfps-cpp/tests/datastructures.cpp +++ b/api/sixtyfps-cpp/tests/datastructures.cpp @@ -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 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()); +} \ No newline at end of file diff --git a/sixtyfps_runtime/corelib/properties.rs b/sixtyfps_runtime/corelib/properties.rs index f8627666d..69f0f6624 100644 --- a/sixtyfps_runtime/corelib/properties.rs +++ b/sixtyfps_runtime/corelib/properties.rs @@ -167,6 +167,18 @@ struct BindingHolder { 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(binding: B) -> *mut BindingHolder { /// Safety: _self must be a pointer that comes from a `Box>::into_raw()` unsafe fn binding_drop(_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(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(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( diff --git a/sixtyfps_runtime/rendering_backends/gl/graphics_window.rs b/sixtyfps_runtime/rendering_backends/gl/graphics_window.rs index 62d0dbbbc..801ce70e8 100644 --- a/sixtyfps_runtime/rendering_backends/gl/graphics_window.rs +++ b/sixtyfps_runtime/rendering_backends/gl/graphics_window.rs @@ -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()); diff --git a/sixtyfps_runtime/rendering_backends/qt/qt_window.rs b/sixtyfps_runtime/rendering_backends/qt/qt_window.rs index 3d86bff02..907b58691 100644 --- a/sixtyfps_runtime/rendering_backends/qt/qt_window.rs +++ b/sixtyfps_runtime/rendering_backends/qt/qt_window.rs @@ -780,18 +780,20 @@ 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(|| { - self.apply_geometry_constraint(component.as_ref().layout_info()); - component.as_ref().apply_layout(Default::default()); + 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()); - let root_item = component.as_ref().get_item_ref(0); - if let Some(window_item) = ItemRef::downcast_pin(root_item) { - self.apply_window_properties(window_item); - } - }); + let root_item = component.as_ref().get_item_ref(0); + if let Some(window_item) = ItemRef::downcast_pin(root_item) { + 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,