diff --git a/internal/core/properties.rs b/internal/core/properties.rs index 9a44380f0c..9be0989ca5 100644 --- a/internal/core/properties.rs +++ b/internal/core/properties.rs @@ -267,7 +267,7 @@ type DependencyNode = dependency_tracker::DependencyNode<*const BindingHolder>; use alloc::boxed::Box; use alloc::rc::Rc; use core::cell::{Cell, RefCell, UnsafeCell}; -use core::marker::PhantomPinned; +use core::marker::{PhantomData, PhantomPinned}; use core::pin::Pin; /// if a DependencyListHead points to that value, it is because the property is actually @@ -1021,81 +1021,85 @@ fn properties_simple_test() { assert_eq!(g(&compo.area), 8 * 8 * 2); } +struct TwoWayBinding { + common_property: Pin>>, +} +unsafe impl BindingCallable for TwoWayBinding { + unsafe fn evaluate(self: Pin<&Self>, value: *mut ()) -> BindingResult { + *(value as *mut T) = self.common_property.as_ref().get(); + BindingResult::KeepBinding + } + + unsafe fn intercept_set(self: Pin<&Self>, value: *const ()) -> bool { + self.common_property.as_ref().set((*(value as *const T)).clone()); + true + } + + unsafe fn intercept_set_binding(self: Pin<&Self>, new_binding: *mut BindingHolder) -> bool { + self.common_property.handle.set_binding_impl(new_binding); + true + } + + const IS_TWO_WAY_BINDING: bool = true; +} + impl Property { + /// If the property is a two way binding, return the common property + fn check_common_property(self: Pin<&Self>) -> Option>>> { + let handle_val = self.handle.handle.get(); + if handle_val & 0b10 == 0b10 { + let holder = (handle_val & !0b11) as *const BindingHolder; + // Safety: the handle is a pointer to a binding + if unsafe { *&raw const (*holder).is_two_way_binding } { + // Safety: the handle is a pointer to a binding whose B is a TwoWayBinding + return Some(unsafe { + (*(holder as *const BindingHolder>)) + .binding + .common_property + .clone() + }); + } + } + None + } + /// Link two property such that any change to one property is affecting the other property as if they /// where, in fact, a single property. /// The value or binding of prop2 is kept. pub fn link_two_way(prop1: Pin<&Self>, prop2: Pin<&Self>) { - struct TwoWayBinding { - common_property: Pin>>, - } - unsafe impl BindingCallable for TwoWayBinding { - unsafe fn evaluate(self: Pin<&Self>, value: *mut ()) -> BindingResult { - *(value as *mut T) = self.common_property.as_ref().get(); - BindingResult::KeepBinding - } - - unsafe fn intercept_set(self: Pin<&Self>, value: *const ()) -> bool { - self.common_property.as_ref().set((*(value as *const T)).clone()); - true - } - - unsafe fn intercept_set_binding( - self: Pin<&Self>, - new_binding: *mut BindingHolder, - ) -> bool { - self.common_property.handle.set_binding_impl(new_binding); - true - } - - const IS_TWO_WAY_BINDING: bool = true; - } - #[cfg(slint_debug_property)] let debug_name = alloc::format!("<{}<=>{}>", prop1.debug_name.borrow(), prop2.debug_name.borrow()); let value = prop2.get_internal(); - let prop1_handle_val = prop1.handle.handle.get(); - if prop1_handle_val & 0b10 == 0b10 { - // Safety: the handle is a pointer to a binding - let holder = unsafe { &*((prop1_handle_val & !0b11) as *const BindingHolder) }; - if holder.is_two_way_binding { - unsafe { - // Safety: the handle is a pointer to a binding whose B is a TwoWayBinding - let holder = - &*((prop1_handle_val & !0b11) as *const BindingHolder>); - // Safety: TwoWayBinding's T is the same as the type for both properties - prop2.handle.set_binding( - TwoWayBinding { common_property: holder.binding.common_property.clone() }, - #[cfg(slint_debug_property)] - debug_name.as_str(), - ); - } - prop2.set(value); - return; + if let Some(common_property) = prop1.check_common_property() { + // Safety: TwoWayBinding is a BindingCallable for type T + unsafe { + prop2.handle.set_binding( + TwoWayBinding:: { common_property }, + #[cfg(slint_debug_property)] + debug_name.as_str(), + ); } - }; + prop2.set(value); + return; + } + + if let Some(common_property) = prop2.check_common_property() { + // Safety: TwoWayBinding is a BindingCallable for type T + unsafe { + prop1.handle.set_binding( + TwoWayBinding:: { common_property }, + #[cfg(slint_debug_property)] + debug_name.as_str(), + ); + } + return; + } let prop2_handle_val = prop2.handle.handle.get(); let handle = if prop2_handle_val & 0b10 == 0b10 { - // Safety: the handle is a pointer to a binding - let holder = unsafe { &*((prop2_handle_val & !0b11) as *const BindingHolder) }; - if holder.is_two_way_binding { - unsafe { - // Safety: the handle is a pointer to a binding whose B is a TwoWayBinding - let holder = - &*((prop2_handle_val & !0b11) as *const BindingHolder>); - // Safety: TwoWayBinding's T is the same as the type for both properties - prop1.handle.set_binding( - TwoWayBinding { common_property: holder.binding.common_property.clone() }, - #[cfg(slint_debug_property)] - debug_name.as_str(), - ); - } - return; - } // If prop2 is a binding, just "steal it" prop2.handle.handle.set(0); PropertyHandle { handle: Cell::new(prop2_handle_val) } @@ -1124,6 +1128,131 @@ impl Property { ); } } + + /// Link a property to another property of a different type, with mapping function to go between them. + /// + /// the value of the `prop1` (of type `T`) is kept. (This is the opposite of [`Self::link_two_way`]) + /// `T2` must be able to be derived from `T` using the `map_to` function. + /// `T` may contain more information than `T2` and the value of prop1 will be updated with the `map_from` function when `prop2` changes + pub fn link_two_way_with_map( + prop1: Pin<&Self>, + prop2: Pin<&Property>, + map_to: impl Fn(&T) -> T2 + Clone + 'static, // Rename map_to_t2 + map_from: impl Fn(&mut T, &T2) + Clone + 'static, + ) { + struct TwoWayBindingWithMap { + common_property: Pin>>, + map_to: M1, + map_from: M2, + marker: PhantomData<(T, T2)>, + } + unsafe impl< + T: PartialEq + Clone + 'static, + T2: PartialEq + Clone + 'static, + M1: Fn(&T) -> T2 + Clone + 'static, + M2: Fn(&mut T, &T2) + Clone + 'static, + > BindingCallable for TwoWayBindingWithMap + { + unsafe fn evaluate(self: Pin<&Self>, value: *mut ()) -> BindingResult { + *(value as *mut T2) = (self.map_to)(&self.common_property.as_ref().get()); + BindingResult::KeepBinding + } + + unsafe fn intercept_set(self: Pin<&Self>, value: *const ()) -> bool { + let mut old = self.common_property.as_ref().get(); + (self.map_from)(&mut old, &*(value as *const T2)); + self.common_property.as_ref().set(old); + true + } + + unsafe fn intercept_set_binding( + self: Pin<&Self>, + new_binding: *mut BindingHolder, + ) -> bool { + let new_new_binding = alloc_binding_holder(BindingMapper:: { + b: new_binding, + map_to: self.map_to.clone(), + map_from: self.map_from.clone(), + marker: PhantomData, + }); + self.common_property.handle.set_binding_impl(new_new_binding); + true + } + } + + /// Given a binding for T2, maps to a binding for T + struct BindingMapper { + /// Binding that returns a `T` + b: *mut BindingHolder, + map_to: M1, + map_from: M2, + marker: PhantomData<(T, T2)>, + } + unsafe impl< + T: PartialEq + Clone + 'static, + T2: PartialEq + Clone + 'static, + M1: Fn(&T) -> T2 + 'static, + M2: Fn(&mut T, &T2) + 'static, + > BindingCallable for BindingMapper + { + unsafe fn evaluate(self: Pin<&Self>, value: *mut ()) -> BindingResult { + let value = &mut *(value as *mut T); + let mut sub_value = (self.map_to)(value); + ((*self.b).vtable.evaluate)(self.b, &mut sub_value as *mut T2 as *mut ()); + (self.map_from)(value, &sub_value); + BindingResult::KeepBinding + } + } + impl Drop for BindingMapper { + fn drop(&mut self) { + unsafe { + ((*self.b).vtable.drop)(self.b); + } + } + } + + #[cfg(slint_debug_property)] + let debug_name = + alloc::format!("<{}<=>{}>", prop1.debug_name.borrow(), prop2.debug_name.borrow()); + + let common_property = if let Some(common_property) = prop1.check_common_property() { + common_property + } else { + let prop1_handle_val = prop1.handle.handle.get(); + let handle = if prop1_handle_val & 0b10 == 0b10 { + // If prop1 is a binding, just "steal it" + prop1.handle.handle.set(0); + PropertyHandle { handle: Cell::new(prop1_handle_val) } + } else { + PropertyHandle::default() + }; + + let common_property = Rc::pin(Property { + handle, + value: UnsafeCell::new(prop1.get_internal()), + pinned: PhantomPinned, + #[cfg(slint_debug_property)] + debug_name: debug_name.clone().into(), + }); + // Safety: TwoWayBinding's T is the same as the type for both properties + unsafe { + prop1.handle.set_binding( + TwoWayBinding:: { common_property: common_property.clone() }, + #[cfg(slint_debug_property)] + debug_name.as_str(), + ); + } + common_property + }; + + unsafe { + prop2.handle.set_binding( + TwoWayBindingWithMap { common_property, map_to, map_from, marker: PhantomData }, + #[cfg(slint_debug_property)] + debug_name.as_str(), + ) + }; + } } #[test] @@ -1328,6 +1457,70 @@ fn property_two_ways_binding_of_two_two_way_bindings() { assert_eq!(p2_2.as_ref().get(), 9); } +#[test] +fn test_two_way_with_map() { + #[derive(PartialEq, Clone, Default, Debug)] + struct Struct { + foo: i32, + bar: alloc::string::String, + } + let p1 = Rc::pin(Property::new(Struct { foo: 42, bar: "hello".into() })); + let p2 = Rc::pin(Property::new(88)); + let p3 = Rc::pin(Property::new(alloc::string::String::from("xyz"))); + Property::link_two_way_with_map(p1.as_ref(), p2.as_ref(), |s| s.foo, |s, foo| s.foo = *foo); + assert_eq!(p1.as_ref().get(), Struct { foo: 42, bar: "hello".into() }); + assert_eq!(p2.as_ref().get(), 42); + + p2.as_ref().set(81); + assert_eq!(p1.as_ref().get(), Struct { foo: 81, bar: "hello".into() }); + assert_eq!(p2.as_ref().get(), 81); + + p1.as_ref().set(Struct { foo: 78, bar: "world".into() }); + assert_eq!(p1.as_ref().get(), Struct { foo: 78, bar: "world".into() }); + assert_eq!(p2.as_ref().get(), 78); + + Property::link_two_way_with_map( + p1.as_ref(), + p3.as_ref(), + |s| s.bar.clone(), + |s, bar| s.bar = bar.clone(), + ); + assert_eq!(p1.as_ref().get(), Struct { foo: 78, bar: "world".into() }); + assert_eq!(p2.as_ref().get(), 78); + assert_eq!(p3.as_ref().get(), "world"); + + p3.as_ref().set("abc".into()); + assert_eq!(p1.as_ref().get(), Struct { foo: 78, bar: "abc".into() }); + assert_eq!(p2.as_ref().get(), 78); + assert_eq!(p3.as_ref().get(), "abc"); + + let p4 = Rc::pin(Property::new(123)); + p2.set_binding({ + let p4 = p4.clone(); + move || p4.as_ref().get() + 1 + }); + + assert_eq!(p1.as_ref().get(), Struct { foo: 124, bar: "abc".into() }); + assert_eq!(p2.as_ref().get(), 124); + assert_eq!(p3.as_ref().get(), "abc"); + + p4.as_ref().set(456); + assert_eq!(p1.as_ref().get(), Struct { foo: 457, bar: "abc".into() }); + assert_eq!(p2.as_ref().get(), 457); + assert_eq!(p3.as_ref().get(), "abc"); + + p3.as_ref().set("def".into()); + assert_eq!(p1.as_ref().get(), Struct { foo: 457, bar: "def".into() }); + assert_eq!(p2.as_ref().get(), 457); + assert_eq!(p3.as_ref().get(), "def"); + + p4.as_ref().set(789); + // Note that the binding with `p2 : p4+1` is broken + assert_eq!(p1.as_ref().get(), Struct { foo: 457, bar: "def".into() }); + assert_eq!(p2.as_ref().get(), 457); + assert_eq!(p3.as_ref().get(), "def"); +} + mod change_tracker; pub use change_tracker::*; mod properties_animations;