mirror of
https://github.com/slint-ui/slint.git
synced 2025-12-23 09:19:32 +00:00
WIP: core: two way bindings with mapping
This commit is contained in:
parent
cad0a8ea83
commit
6845255b68
1 changed files with 254 additions and 61 deletions
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue