Skip to main content

i_slint_core/properties/
two_way_binding.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4use super::*;
5use alloc::rc::Rc;
6use core::cell::Cell;
7use core::cell::UnsafeCell;
8use core::marker::PhantomData;
9use core::marker::PhantomPinned;
10use core::pin::Pin;
11
12struct TwoWayBinding<T> {
13    common_property: Pin<Rc<Property<T>>>,
14}
15unsafe impl<T: PartialEq + Clone + 'static> BindingCallable<T> for TwoWayBinding<T> {
16    fn evaluate(self: Pin<&Self>, value: &mut T) -> BindingResult {
17        *value = self.common_property.as_ref().get();
18        BindingResult::KeepBinding
19    }
20
21    fn intercept_set(self: Pin<&Self>, value: &T) -> bool {
22        self.common_property.as_ref().set(value.clone());
23        true
24    }
25
26    unsafe fn intercept_set_binding(self: Pin<&Self>, new_binding: *mut BindingHolder) -> bool {
27        self.common_property.handle.set_binding_impl(new_binding);
28        true
29    }
30
31    const IS_TWO_WAY_BINDING: bool = true;
32}
33
34impl<T: PartialEq + Clone + 'static> Property<T> {
35    /// If the property is a two way binding, return the common property
36    pub(crate) fn check_common_property(self: Pin<&Self>) -> Option<Pin<Rc<Property<T>>>> {
37        let handle_val = self.handle.handle.get();
38        if let Some(holder) = PropertyHandle::pointer_to_binding(handle_val) {
39            // Safety: the handle is a pointer to a binding
40            if unsafe { (*holder).is_two_way_binding } {
41                // Safety: the handle is a pointer to a binding whose B is a TwoWayBinding<T>
42                return Some(unsafe {
43                    (*(holder as *const BindingHolder<TwoWayBinding<T>>))
44                        .binding
45                        .common_property
46                        .clone()
47                });
48            }
49        }
50        None
51    }
52
53    /// Link two property such that any change to one property is affecting the other property as if they
54    /// where, in fact, a single property.
55    /// The value or binding of prop2 is kept.
56    pub fn link_two_way(prop1: Pin<&Self>, prop2: Pin<&Self>) {
57        #[cfg(slint_debug_property)]
58        let debug_name =
59            alloc::format!("<{}<=>{}>", prop1.debug_name.borrow(), prop2.debug_name.borrow());
60
61        let value = prop2.get_untracked();
62
63        if let Some(common_property) = prop1.check_common_property() {
64            // Safety: TwoWayBinding is a BindingCallable for type T
65            unsafe {
66                prop2.handle.set_binding(
67                    TwoWayBinding::<T> { common_property },
68                    #[cfg(slint_debug_property)]
69                    debug_name.as_str(),
70                );
71            }
72            prop2.set(value);
73            return;
74        }
75
76        if let Some(common_property) = prop2.check_common_property() {
77            // Safety: TwoWayBinding is a BindingCallable for type T
78            unsafe {
79                prop1.handle.set_binding(
80                    TwoWayBinding::<T> { common_property },
81                    #[cfg(slint_debug_property)]
82                    debug_name.as_str(),
83                );
84            }
85            return;
86        }
87
88        let prop2_handle_val = prop2.handle.handle.get();
89        let handle = if PropertyHandle::is_pointer_to_binding(prop2_handle_val) {
90            // If prop2 is a binding, just "steal it"
91            prop2.handle.handle.set(core::ptr::null_mut());
92            PropertyHandle { handle: Cell::new(prop2_handle_val) }
93        } else {
94            PropertyHandle::default()
95        };
96
97        let common_property = Rc::pin(Property {
98            handle,
99            value: UnsafeCell::new(value),
100            pinned: PhantomPinned,
101            #[cfg(slint_debug_property)]
102            debug_name: debug_name.clone().into(),
103        });
104        // Safety: TwoWayBinding's T is the same as the type for both properties
105        unsafe {
106            prop1.handle.set_binding(
107                TwoWayBinding { common_property: common_property.clone() },
108                #[cfg(slint_debug_property)]
109                debug_name.as_str(),
110            );
111            prop2.handle.set_binding(
112                TwoWayBinding { common_property },
113                #[cfg(slint_debug_property)]
114                debug_name.as_str(),
115            );
116        }
117    }
118
119    /// Link a property to another property of a different type, with mapping function to go between them.
120    ///
121    /// the value of the `prop1` (of type `T`) is kept. (This is the opposite of [`Self::link_two_way`])
122    /// `T2` must be able to be derived from `T` using the `map_to` function.
123    /// `T` may contain more information than `T2` and the value of prop1 will be updated with the `map_from` function when `prop2` changes
124    pub fn link_two_way_with_map<T2: PartialEq + Clone + 'static>(
125        prop1: Pin<&Self>,
126        prop2: Pin<&Property<T2>>,
127        map_to: impl Fn(&T) -> T2 + Clone + 'static, // Rename map_to_t2
128        map_from: impl Fn(&mut T, &T2) + Clone + 'static,
129    ) {
130        let common_property = if let Some(common_property) = prop1.check_common_property() {
131            common_property
132        } else {
133            let prop1_handle_val = prop1.handle.handle.get();
134            let handle = if PropertyHandle::is_pointer_to_binding(prop1_handle_val) {
135                // If prop1 is a binding, just "steal it"
136                prop1.handle.handle.set(core::ptr::null_mut());
137                PropertyHandle { handle: Cell::new(prop1_handle_val) }
138            } else {
139                PropertyHandle::default()
140            };
141
142            #[cfg(slint_debug_property)]
143            let debug_name = alloc::format!("{}*", prop1.debug_name.borrow());
144
145            let common_property = Rc::pin(Property {
146                handle,
147                value: UnsafeCell::new(prop1.get_internal()),
148                pinned: PhantomPinned,
149                #[cfg(slint_debug_property)]
150                debug_name: debug_name.clone().into(),
151            });
152            // Safety: TwoWayBinding's T is the same as the type for both properties
153            unsafe {
154                prop1.handle.set_binding(
155                    TwoWayBinding::<T> { common_property: common_property.clone() },
156                    #[cfg(slint_debug_property)]
157                    debug_name.as_str(),
158                );
159            }
160            common_property
161        };
162        Self::link_two_way_with_map_to_common_property(
163            common_property,
164            prop2,
165            map_to,
166            map_from,
167            false,
168        );
169    }
170
171    /// Make a two way binding between the common property and the binding prop2.
172    /// Two-way bindings on prop2 are always forwarded through the chain.
173    /// Regular closure bindings on prop2 are preserved when
174    /// `preserve_prop2_binding` is true, or dropped (so the common
175    /// property's value wins) when false.
176    pub(crate) fn link_two_way_with_map_to_common_property<T2: PartialEq + Clone + 'static>(
177        common_property: Pin<Rc<Self>>,
178        prop2: Pin<&Property<T2>>,
179        map_to: impl Fn(&T) -> T2 + Clone + 'static,
180        map_from: impl Fn(&mut T, &T2) + Clone + 'static,
181        preserve_prop2_binding: bool,
182    ) {
183        struct TwoWayBindingWithMap<T, T2, M1, M2> {
184            common_property: Pin<Rc<Property<T>>>,
185            map_to: M1,
186            map_from: M2,
187            marker: PhantomData<(T, T2)>,
188        }
189        unsafe impl<
190            T: PartialEq + Clone + 'static,
191            T2: PartialEq + Clone + 'static,
192            M1: Fn(&T) -> T2 + Clone + 'static,
193            M2: Fn(&mut T, &T2) + Clone + 'static,
194        > BindingCallable<T2> for TwoWayBindingWithMap<T, T2, M1, M2>
195        {
196            fn evaluate(self: Pin<&Self>, value: &mut T2) -> BindingResult {
197                *value = (self.map_to)(&self.common_property.as_ref().get());
198                BindingResult::KeepBinding
199            }
200
201            fn intercept_set(self: Pin<&Self>, value: &T2) -> bool {
202                let mut old = self.common_property.as_ref().get();
203                (self.map_from)(&mut old, value);
204                self.common_property.as_ref().set(old);
205                true
206            }
207
208            unsafe fn intercept_set_binding(
209                self: Pin<&Self>,
210                new_binding: *mut BindingHolder,
211            ) -> bool {
212                let new_new_binding = alloc_binding_holder(BindingMapper::<T, T2, M1, M2> {
213                    b: new_binding,
214                    map_to: self.map_to.clone(),
215                    map_from: self.map_from.clone(),
216                    marker: PhantomData,
217                });
218                self.common_property.handle.set_binding_impl(new_new_binding);
219                true
220            }
221        }
222
223        /// Given a binding for T2, maps to a binding for T
224        struct BindingMapper<T, T2, M1, M2> {
225            /// Binding that returns a `T2`
226            b: *mut BindingHolder,
227            map_to: M1,
228            map_from: M2,
229            marker: PhantomData<(T, T2)>,
230        }
231        unsafe impl<
232            T: PartialEq + Clone + 'static,
233            T2: PartialEq + Clone + 'static,
234            M1: Fn(&T) -> T2 + 'static,
235            M2: Fn(&mut T, &T2) + 'static,
236        > BindingCallable<T> for BindingMapper<T, T2, M1, M2>
237        {
238            fn evaluate(self: Pin<&Self>, value: &mut T) -> BindingResult {
239                let mut sub_value = (self.map_to)(value);
240                // Safety: `self.b` is a BindingHolder that expects a `T2`
241                unsafe {
242                    ((*self.b).vtable.evaluate)(
243                        self.b,
244                        (&mut sub_value as *mut T2).cast::<c_void>(),
245                    );
246                }
247                (self.map_from)(value, &sub_value);
248                BindingResult::KeepBinding
249            }
250
251            fn intercept_set(self: Pin<&Self>, value: &T) -> bool {
252                let sub_value = (self.map_to)(value);
253                // Safety: `self.b` is a BindingHolder that expects a `T2`
254                unsafe {
255                    ((*self.b).vtable.intercept_set)(
256                        self.b,
257                        (&sub_value as *const T2).cast::<c_void>(),
258                    )
259                }
260            }
261        }
262        impl<T, T2, M1, M2> Drop for BindingMapper<T, T2, M1, M2> {
263            fn drop(&mut self) {
264                unsafe {
265                    ((*self.b).vtable.drop)(self.b);
266                }
267            }
268        }
269
270        #[cfg(slint_debug_property)]
271        let debug_name = alloc::format!(
272            "<{}<=>{}>",
273            common_property.debug_name.borrow(),
274            prop2.debug_name.borrow()
275        );
276
277        let old_binding = prop2.handle.detach_binding();
278
279        unsafe {
280            if let Some(old) = old_binding {
281                let new_binding = alloc_binding_holder(TwoWayBindingWithMap {
282                    common_property,
283                    map_to,
284                    map_from,
285                    marker: PhantomData,
286                });
287                if ((*old).vtable.intercept_set_binding)(old, new_binding) {
288                    prop2.handle.set_binding_impl(old);
289                } else {
290                    prop2.handle.set_binding_impl(new_binding);
291                    if preserve_prop2_binding {
292                        // Re-attach so TwoWayBindingWithMap wraps it
293                        // as a BindingMapper for reactivity.
294                        prop2.handle.set_binding_impl(old);
295                    } else {
296                        ((*old).vtable.drop)(old);
297                    }
298                }
299            } else {
300                prop2.handle.set_binding(
301                    TwoWayBindingWithMap { common_property, map_to, map_from, marker: PhantomData },
302                    #[cfg(slint_debug_property)]
303                    debug_name.as_str(),
304                );
305            }
306        }
307    }
308}
309
310struct TwoWayBindingModel<T, ItemTree, Getter, Setter> {
311    phantom: PhantomData<fn(T) -> T>,
312    item_tree: ItemTree,
313    getter: Getter,
314    setter: Setter,
315}
316
317// Safety: IS_TWO_WAY_BINDING is false
318unsafe impl<T, ItemTree, Getter, Setter> BindingCallable<T>
319    for TwoWayBindingModel<T, ItemTree, Getter, Setter>
320where
321    Getter: Fn(&ItemTree) -> Option<T>,
322    Setter: Fn(&ItemTree, &T),
323{
324    fn evaluate(self: Pin<&Self>, value: &mut T) -> BindingResult {
325        if let Some(v) = (self.getter)(&self.item_tree) {
326            *value = v;
327        }
328        BindingResult::KeepBinding
329    }
330
331    unsafe fn intercept_set_binding(self: Pin<&Self>, _new_binding: *mut BindingHolder) -> bool {
332        false
333    }
334
335    fn intercept_set(self: Pin<&Self>, value: &T) -> bool {
336        (self.setter)(&self.item_tree, value);
337        true
338    }
339}
340
341impl<T: 'static> Property<T> {
342    /// Bind this property two-way to a value stored in a model row.
343    /// `getter` reads the current row value (and registers a dependency on
344    /// it); `setter` writes a new value back into the row.
345    pub fn link_two_way_to_model_data<ItemTree: 'static>(
346        self: Pin<&Self>,
347        item_tree: ItemTree,
348        getter: impl Fn(&ItemTree) -> Option<T> + 'static,
349        setter: impl Fn(&ItemTree, &T) + 'static,
350    ) {
351        let binding = TwoWayBindingModel { phantom: PhantomData, item_tree, getter, setter };
352        // Safety: TwoWayBindingModel implements BindingCallable<T> for the same T as `Self`.
353        unsafe {
354            self.handle.set_binding(
355                binding,
356                #[cfg(slint_debug_property)]
357                &alloc::format!("{}<=>[model]", self.debug_name.borrow()),
358            )
359        };
360    }
361}
362
363#[test]
364fn property_two_ways_test() {
365    let p1 = Rc::pin(Property::new(42));
366    let p2 = Rc::pin(Property::new(88));
367
368    let depends = Box::pin(Property::new(0));
369    depends.as_ref().set_binding({
370        let p1 = p1.clone();
371        move || p1.as_ref().get() + 8
372    });
373    assert_eq!(depends.as_ref().get(), 42 + 8);
374    Property::link_two_way(p1.as_ref(), p2.as_ref());
375    assert_eq!(p1.as_ref().get(), 88);
376    assert_eq!(p2.as_ref().get(), 88);
377    assert_eq!(depends.as_ref().get(), 88 + 8);
378    p2.as_ref().set(5);
379    assert_eq!(p1.as_ref().get(), 5);
380    assert_eq!(p2.as_ref().get(), 5);
381    assert_eq!(depends.as_ref().get(), 5 + 8);
382    p1.as_ref().set(22);
383    assert_eq!(p1.as_ref().get(), 22);
384    assert_eq!(p2.as_ref().get(), 22);
385    assert_eq!(depends.as_ref().get(), 22 + 8);
386}
387
388#[test]
389fn property_two_ways_test_binding() {
390    let p1 = Rc::pin(Property::new(42));
391    let p2 = Rc::pin(Property::new(88));
392    let global = Rc::pin(Property::new(23));
393    p2.as_ref().set_binding({
394        let global = global.clone();
395        move || global.as_ref().get() + 9
396    });
397
398    let depends = Box::pin(Property::new(0));
399    depends.as_ref().set_binding({
400        let p1 = p1.clone();
401        move || p1.as_ref().get() + 8
402    });
403
404    Property::link_two_way(p1.as_ref(), p2.as_ref());
405    assert_eq!(p1.as_ref().get(), 23 + 9);
406    assert_eq!(p2.as_ref().get(), 23 + 9);
407    assert_eq!(depends.as_ref().get(), 23 + 9 + 8);
408    global.as_ref().set(55);
409    assert_eq!(p1.as_ref().get(), 55 + 9);
410    assert_eq!(p2.as_ref().get(), 55 + 9);
411    assert_eq!(depends.as_ref().get(), 55 + 9 + 8);
412}
413
414#[test]
415fn property_two_ways_recurse_from_binding() {
416    let xx = Rc::pin(Property::new(0));
417
418    let p1 = Rc::pin(Property::new(42));
419    let p2 = Rc::pin(Property::new(88));
420    let global = Rc::pin(Property::new(23));
421
422    let done = Rc::new(Cell::new(false));
423    xx.set_binding({
424        let p1 = p1.clone();
425        let p2 = p2.clone();
426        let global = global.clone();
427        let xx_weak = pin_weak::rc::PinWeak::downgrade(xx.clone());
428        move || {
429            if !done.get() {
430                done.set(true);
431                Property::link_two_way(p1.as_ref(), p2.as_ref());
432                let xx_weak = xx_weak.clone();
433                p1.as_ref().set_binding(move || xx_weak.upgrade().unwrap().as_ref().get() + 9);
434            }
435            global.as_ref().get() + 2
436        }
437    });
438    assert_eq!(xx.as_ref().get(), 23 + 2);
439    assert_eq!(p1.as_ref().get(), 23 + 2 + 9);
440    assert_eq!(p2.as_ref().get(), 23 + 2 + 9);
441
442    global.as_ref().set(55);
443    assert_eq!(p1.as_ref().get(), 55 + 2 + 9);
444    assert_eq!(p2.as_ref().get(), 55 + 2 + 9);
445    assert_eq!(xx.as_ref().get(), 55 + 2);
446}
447
448#[test]
449fn property_two_ways_binding_of_two_way_binding_first() {
450    let p1_1 = Rc::pin(Property::new(2));
451    let p1_2 = Rc::pin(Property::new(4));
452    Property::link_two_way(p1_1.as_ref(), p1_2.as_ref());
453
454    assert_eq!(p1_1.as_ref().get(), 4);
455    assert_eq!(p1_2.as_ref().get(), 4);
456
457    let p2 = Rc::pin(Property::new(3));
458    Property::link_two_way(p1_1.as_ref(), p2.as_ref());
459
460    assert_eq!(p1_1.as_ref().get(), 3);
461    assert_eq!(p1_2.as_ref().get(), 3);
462    assert_eq!(p2.as_ref().get(), 3);
463
464    p1_1.set(6);
465
466    assert_eq!(p1_1.as_ref().get(), 6);
467    assert_eq!(p1_2.as_ref().get(), 6);
468    assert_eq!(p2.as_ref().get(), 6);
469
470    p1_2.set(8);
471
472    assert_eq!(p1_1.as_ref().get(), 8);
473    assert_eq!(p1_2.as_ref().get(), 8);
474    assert_eq!(p2.as_ref().get(), 8);
475
476    p2.set(7);
477
478    assert_eq!(p1_1.as_ref().get(), 7);
479    assert_eq!(p1_2.as_ref().get(), 7);
480    assert_eq!(p2.as_ref().get(), 7);
481}
482
483#[test]
484fn property_two_ways_binding_of_two_way_binding_second() {
485    let p1 = Rc::pin(Property::new(2));
486    let p2_1 = Rc::pin(Property::new(3));
487    let p2_2 = Rc::pin(Property::new(5));
488    Property::link_two_way(p2_1.as_ref(), p2_2.as_ref());
489
490    assert_eq!(p2_1.as_ref().get(), 5);
491    assert_eq!(p2_2.as_ref().get(), 5);
492
493    Property::link_two_way(p1.as_ref(), p2_2.as_ref());
494
495    assert_eq!(p1.as_ref().get(), 5);
496    assert_eq!(p2_1.as_ref().get(), 5);
497    assert_eq!(p2_2.as_ref().get(), 5);
498
499    p1.set(6);
500
501    assert_eq!(p1.as_ref().get(), 6);
502    assert_eq!(p2_1.as_ref().get(), 6);
503    assert_eq!(p2_2.as_ref().get(), 6);
504
505    p2_1.set(7);
506
507    assert_eq!(p1.as_ref().get(), 7);
508    assert_eq!(p2_1.as_ref().get(), 7);
509    assert_eq!(p2_2.as_ref().get(), 7);
510
511    p2_2.set(9);
512
513    assert_eq!(p1.as_ref().get(), 9);
514    assert_eq!(p2_1.as_ref().get(), 9);
515    assert_eq!(p2_2.as_ref().get(), 9);
516}
517
518#[test]
519fn property_two_ways_binding_of_two_two_way_bindings() {
520    let p1_1 = Rc::pin(Property::new(2));
521    let p1_2 = Rc::pin(Property::new(4));
522    Property::link_two_way(p1_1.as_ref(), p1_2.as_ref());
523    assert_eq!(p1_1.as_ref().get(), 4);
524    assert_eq!(p1_2.as_ref().get(), 4);
525
526    let p2_1 = Rc::pin(Property::new(3));
527    let p2_2 = Rc::pin(Property::new(5));
528    Property::link_two_way(p2_1.as_ref(), p2_2.as_ref());
529
530    assert_eq!(p2_1.as_ref().get(), 5);
531    assert_eq!(p2_2.as_ref().get(), 5);
532
533    Property::link_two_way(p1_1.as_ref(), p2_2.as_ref());
534
535    assert_eq!(p1_1.as_ref().get(), 5);
536    assert_eq!(p1_2.as_ref().get(), 5);
537    assert_eq!(p2_1.as_ref().get(), 5);
538    assert_eq!(p2_2.as_ref().get(), 5);
539
540    p1_1.set(6);
541    assert_eq!(p1_1.as_ref().get(), 6);
542    assert_eq!(p1_2.as_ref().get(), 6);
543    assert_eq!(p2_1.as_ref().get(), 6);
544    assert_eq!(p2_2.as_ref().get(), 6);
545
546    p1_2.set(8);
547    assert_eq!(p1_1.as_ref().get(), 8);
548    assert_eq!(p1_2.as_ref().get(), 8);
549    assert_eq!(p2_1.as_ref().get(), 8);
550    assert_eq!(p2_2.as_ref().get(), 8);
551
552    p2_1.set(7);
553    assert_eq!(p1_1.as_ref().get(), 7);
554    assert_eq!(p1_2.as_ref().get(), 7);
555    assert_eq!(p2_1.as_ref().get(), 7);
556    assert_eq!(p2_2.as_ref().get(), 7);
557
558    p2_2.set(9);
559    assert_eq!(p1_1.as_ref().get(), 9);
560    assert_eq!(p1_2.as_ref().get(), 9);
561    assert_eq!(p2_1.as_ref().get(), 9);
562    assert_eq!(p2_2.as_ref().get(), 9);
563}
564
565#[test]
566fn test_two_way_with_map() {
567    #[derive(PartialEq, Clone, Default, Debug)]
568    struct Struct {
569        foo: i32,
570        bar: alloc::string::String,
571    }
572    let p1 = Rc::pin(Property::new(Struct { foo: 42, bar: "hello".into() }));
573    let p2 = Rc::pin(Property::new(88));
574    let p3 = Rc::pin(Property::new(alloc::string::String::from("xyz")));
575    Property::link_two_way_with_map(p1.as_ref(), p2.as_ref(), |s| s.foo, |s, foo| s.foo = *foo);
576    assert_eq!(p1.as_ref().get(), Struct { foo: 42, bar: "hello".into() });
577    assert_eq!(p2.as_ref().get(), 42);
578
579    p2.as_ref().set(81);
580    assert_eq!(p1.as_ref().get(), Struct { foo: 81, bar: "hello".into() });
581    assert_eq!(p2.as_ref().get(), 81);
582
583    p1.as_ref().set(Struct { foo: 78, bar: "world".into() });
584    assert_eq!(p1.as_ref().get(), Struct { foo: 78, bar: "world".into() });
585    assert_eq!(p2.as_ref().get(), 78);
586
587    Property::link_two_way_with_map(
588        p1.as_ref(),
589        p3.as_ref(),
590        |s| s.bar.clone(),
591        |s, bar| s.bar = bar.clone(),
592    );
593    assert_eq!(p1.as_ref().get(), Struct { foo: 78, bar: "world".into() });
594    assert_eq!(p2.as_ref().get(), 78);
595    assert_eq!(p3.as_ref().get(), "world");
596
597    p3.as_ref().set("abc".into());
598    assert_eq!(p1.as_ref().get(), Struct { foo: 78, bar: "abc".into() });
599    assert_eq!(p2.as_ref().get(), 78);
600    assert_eq!(p3.as_ref().get(), "abc");
601
602    let p4 = Rc::pin(Property::new(123));
603    p2.set_binding({
604        let p4 = p4.clone();
605        move || p4.as_ref().get() + 1
606    });
607
608    assert_eq!(p1.as_ref().get(), Struct { foo: 124, bar: "abc".into() });
609    assert_eq!(p2.as_ref().get(), 124);
610    assert_eq!(p3.as_ref().get(), "abc");
611
612    p4.as_ref().set(456);
613    assert_eq!(p1.as_ref().get(), Struct { foo: 457, bar: "abc".into() });
614    assert_eq!(p2.as_ref().get(), 457);
615    assert_eq!(p3.as_ref().get(), "abc");
616
617    p3.as_ref().set("def".into());
618    assert_eq!(p1.as_ref().get(), Struct { foo: 457, bar: "def".into() });
619    assert_eq!(p2.as_ref().get(), 457);
620    assert_eq!(p3.as_ref().get(), "def");
621
622    p4.as_ref().set(789);
623    // Note that the binding with `p2 : p4+1` is broken
624    assert_eq!(p1.as_ref().get(), Struct { foo: 457, bar: "def".into() });
625    assert_eq!(p2.as_ref().get(), 457);
626    assert_eq!(p3.as_ref().get(), "def");
627}
628
629/// Regression test for use-after-free in `link_two_way_with_map_to_common_property`.
630///
631/// When a property already has a binding with dependant properties and is then linked
632/// via `link_two_way_with_map`, the old binding's dependency list must be
633/// transferred to the new `TwoWayBindingWithMap` binding. Without this
634/// transfer, dependency nodes would point into freed memory, causing
635/// panics in `DependencyNode::debug_assert_valid` on drop.
636///
637/// The drop order is arranged so that the tracker (which owns the
638/// dependency node) outlives the properties, forcing the node to be
639/// removed after the old binding would have been freed.
640#[test]
641fn test_two_way_with_map_dependency_list_transfer() {
642    #[derive(PartialEq, Clone, Default, Debug)]
643    struct Wrapper {
644        value: i32,
645    }
646
647    let source = Rc::pin(Property::new(10));
648
649    // Declare the tracker before the properties so it is dropped *after*
650    // them (Rust drops locals in reverse declaration order). This ensures
651    // the dependency node outlives the old binding.
652    let tracker = Box::pin(<PropertyTracker>::default());
653
654    let p_field = Rc::pin(Property::new(0i32));
655    p_field.as_ref().set_binding({
656        let source = source.clone();
657        move || source.as_ref().get() * 2
658    });
659    assert_eq!(p_field.as_ref().get(), 20);
660
661    // Evaluate the tracker, which reads p_field and registers a dependency
662    // node on p_field's binding's dependency list.
663    let val = tracker.as_ref().evaluate({
664        let p_field = p_field.clone();
665        move || p_field.as_ref().get()
666    });
667    assert_eq!(val, 20);
668    assert!(!tracker.as_ref().is_dirty());
669
670    // link_two_way_with_map replaces p_field's binding with a
671    // TwoWayBindingWithMap. The old closure binding is dropped
672    // (prop1's value wins), so p_field now reads from the common
673    // property initialized from p_struct.
674    let p_struct = Rc::pin(Property::new(Wrapper { value: 0 }));
675    Property::link_two_way_with_map(
676        p_struct.as_ref(),
677        p_field.as_ref(),
678        |s| s.value,
679        |s, v| s.value = *v,
680    );
681
682    assert_eq!(p_field.as_ref().get(), 0);
683    assert_eq!(p_struct.as_ref().get(), Wrapper { value: 0 });
684
685    // The binding replacement dirtied the tracker via the transferred
686    // dependency list.
687    assert!(tracker.as_ref().is_dirty());
688
689    // Implicit drop order: p_struct, p_field, tracker, source.
690    // p_field's drop frees the TwoWayBindingWithMap binding.
691    // tracker is dropped afterwards — its DependencyNode::remove
692    // would panic in debug_assert_valid if the dependency list was
693    // not properly transferred.
694}