WIP: core: two way bindings with mapping

This commit is contained in:
Olivier Goffart 2025-10-06 16:54:15 +02:00
parent cad0a8ea83
commit 6845255b68

View file

@ -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<T> {
common_property: Pin<Rc<Property<T>>>,
}
unsafe impl<T: PartialEq + Clone + 'static> BindingCallable for TwoWayBinding<T> {
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<T: PartialEq + Clone + 'static> Property<T> {
/// If the property is a two way binding, return the common property
fn check_common_property(self: Pin<&Self>) -> Option<Pin<Rc<Property<T>>>> {
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<T>
return Some(unsafe {
(*(holder as *const BindingHolder<TwoWayBinding<T>>))
.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<T> {
common_property: Pin<Rc<Property<T>>>,
}
unsafe impl<T: PartialEq + Clone + 'static> BindingCallable for TwoWayBinding<T> {
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<T>
let holder =
&*((prop1_handle_val & !0b11) as *const BindingHolder<TwoWayBinding<T>>);
// 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::<T> { 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::<T> { 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<T>
let holder =
&*((prop2_handle_val & !0b11) as *const BindingHolder<TwoWayBinding<T>>);
// 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<T: PartialEq + Clone + 'static> Property<T> {
);
}
}
/// 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<T2: PartialEq + Clone + 'static>(
prop1: Pin<&Self>,
prop2: Pin<&Property<T2>>,
map_to: impl Fn(&T) -> T2 + Clone + 'static, // Rename map_to_t2
map_from: impl Fn(&mut T, &T2) + Clone + 'static,
) {
struct TwoWayBindingWithMap<T, T2, M1, M2> {
common_property: Pin<Rc<Property<T>>>,
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<T, T2, M1, M2>
{
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::<T, T2, M1, M2> {
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<T, T2, M1, M2> {
/// 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<T, T2, M1, M2>
{
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<T, T2, M1, M2> Drop for BindingMapper<T, T2, M1, M2> {
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::<T> { 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;