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_internal();
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(0);
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(0);
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(common_property, prop2, map_to, map_from);
163    }
164
165    /// Make a two way binding between the common property and the binding prop2.
166    /// if prop2 has a binding, it will be preserved
167    pub(crate) fn link_two_way_with_map_to_common_property<T2: PartialEq + Clone + 'static>(
168        common_property: Pin<Rc<Self>>,
169        prop2: Pin<&Property<T2>>,
170        map_to: impl Fn(&T) -> T2 + Clone + 'static,
171        map_from: impl Fn(&mut T, &T2) + Clone + 'static,
172    ) {
173        struct TwoWayBindingWithMap<T, T2, M1, M2> {
174            common_property: Pin<Rc<Property<T>>>,
175            map_to: M1,
176            map_from: M2,
177            marker: PhantomData<(T, T2)>,
178        }
179        unsafe impl<
180            T: PartialEq + Clone + 'static,
181            T2: PartialEq + Clone + 'static,
182            M1: Fn(&T) -> T2 + Clone + 'static,
183            M2: Fn(&mut T, &T2) + Clone + 'static,
184        > BindingCallable<T2> for TwoWayBindingWithMap<T, T2, M1, M2>
185        {
186            fn evaluate(self: Pin<&Self>, value: &mut T2) -> BindingResult {
187                *value = (self.map_to)(&self.common_property.as_ref().get());
188                BindingResult::KeepBinding
189            }
190
191            fn intercept_set(self: Pin<&Self>, value: &T2) -> bool {
192                let mut old = self.common_property.as_ref().get();
193                (self.map_from)(&mut old, value);
194                self.common_property.as_ref().set(old);
195                true
196            }
197
198            unsafe fn intercept_set_binding(
199                self: Pin<&Self>,
200                new_binding: *mut BindingHolder,
201            ) -> bool {
202                let new_new_binding = alloc_binding_holder(BindingMapper::<T, T2, M1, M2> {
203                    b: new_binding,
204                    map_to: self.map_to.clone(),
205                    map_from: self.map_from.clone(),
206                    marker: PhantomData,
207                });
208                self.common_property.handle.set_binding_impl(new_new_binding);
209                true
210            }
211        }
212
213        /// Given a binding for T2, maps to a binding for T
214        struct BindingMapper<T, T2, M1, M2> {
215            /// Binding that returns a `T2`
216            b: *mut BindingHolder,
217            map_to: M1,
218            map_from: M2,
219            marker: PhantomData<(T, T2)>,
220        }
221        unsafe impl<
222            T: PartialEq + Clone + 'static,
223            T2: PartialEq + Clone + 'static,
224            M1: Fn(&T) -> T2 + 'static,
225            M2: Fn(&mut T, &T2) + 'static,
226        > BindingCallable<T> for BindingMapper<T, T2, M1, M2>
227        {
228            fn evaluate(self: Pin<&Self>, value: &mut T) -> BindingResult {
229                let mut sub_value = (self.map_to)(value);
230                // Safety: `self.b` is a BindingHolder that expects a `T2`
231                unsafe {
232                    ((*self.b).vtable.evaluate)(self.b, &mut sub_value as *mut T2 as *mut ());
233                }
234                (self.map_from)(value, &sub_value);
235                BindingResult::KeepBinding
236            }
237
238            fn intercept_set(self: Pin<&Self>, value: &T) -> bool {
239                let sub_value = (self.map_to)(value);
240                // Safety: `self.b` is a BindingHolder that expects a `T2`
241                unsafe {
242                    ((*self.b).vtable.intercept_set)(self.b, &sub_value as *const T2 as *const ())
243                }
244            }
245        }
246        impl<T, T2, M1, M2> Drop for BindingMapper<T, T2, M1, M2> {
247            fn drop(&mut self) {
248                unsafe {
249                    ((*self.b).vtable.drop)(self.b);
250                }
251            }
252        }
253
254        #[cfg(slint_debug_property)]
255        let debug_name = alloc::format!(
256            "<{}<=>{}>",
257            common_property.debug_name.borrow(),
258            prop2.debug_name.borrow()
259        );
260
261        // Detach the old binding (if any) from prop2, transferring its
262        // dependency list back to the handle so that set_binding() will
263        // properly move it into the new TwoWayBindingWithMap binding.
264        // Without this, the old binding's dependency list would be orphaned
265        // when it is later freed via BindingMapper::drop, leaving
266        // DependencyNodes with dangling prev pointers.
267        let old_binding = prop2.handle.detach_binding();
268
269        unsafe {
270            prop2.handle.set_binding(
271                TwoWayBindingWithMap { common_property, map_to, map_from, marker: PhantomData },
272                #[cfg(slint_debug_property)]
273                debug_name.as_str(),
274            );
275
276            if let Some(binding) = old_binding {
277                prop2.handle.set_binding_impl(binding);
278            }
279        };
280    }
281}
282
283#[test]
284fn property_two_ways_test() {
285    let p1 = Rc::pin(Property::new(42));
286    let p2 = Rc::pin(Property::new(88));
287
288    let depends = Box::pin(Property::new(0));
289    depends.as_ref().set_binding({
290        let p1 = p1.clone();
291        move || p1.as_ref().get() + 8
292    });
293    assert_eq!(depends.as_ref().get(), 42 + 8);
294    Property::link_two_way(p1.as_ref(), p2.as_ref());
295    assert_eq!(p1.as_ref().get(), 88);
296    assert_eq!(p2.as_ref().get(), 88);
297    assert_eq!(depends.as_ref().get(), 88 + 8);
298    p2.as_ref().set(5);
299    assert_eq!(p1.as_ref().get(), 5);
300    assert_eq!(p2.as_ref().get(), 5);
301    assert_eq!(depends.as_ref().get(), 5 + 8);
302    p1.as_ref().set(22);
303    assert_eq!(p1.as_ref().get(), 22);
304    assert_eq!(p2.as_ref().get(), 22);
305    assert_eq!(depends.as_ref().get(), 22 + 8);
306}
307
308#[test]
309fn property_two_ways_test_binding() {
310    let p1 = Rc::pin(Property::new(42));
311    let p2 = Rc::pin(Property::new(88));
312    let global = Rc::pin(Property::new(23));
313    p2.as_ref().set_binding({
314        let global = global.clone();
315        move || global.as_ref().get() + 9
316    });
317
318    let depends = Box::pin(Property::new(0));
319    depends.as_ref().set_binding({
320        let p1 = p1.clone();
321        move || p1.as_ref().get() + 8
322    });
323
324    Property::link_two_way(p1.as_ref(), p2.as_ref());
325    assert_eq!(p1.as_ref().get(), 23 + 9);
326    assert_eq!(p2.as_ref().get(), 23 + 9);
327    assert_eq!(depends.as_ref().get(), 23 + 9 + 8);
328    global.as_ref().set(55);
329    assert_eq!(p1.as_ref().get(), 55 + 9);
330    assert_eq!(p2.as_ref().get(), 55 + 9);
331    assert_eq!(depends.as_ref().get(), 55 + 9 + 8);
332}
333
334#[test]
335fn property_two_ways_recurse_from_binding() {
336    let xx = Rc::pin(Property::new(0));
337
338    let p1 = Rc::pin(Property::new(42));
339    let p2 = Rc::pin(Property::new(88));
340    let global = Rc::pin(Property::new(23));
341
342    let done = Rc::new(Cell::new(false));
343    xx.set_binding({
344        let p1 = p1.clone();
345        let p2 = p2.clone();
346        let global = global.clone();
347        let xx_weak = pin_weak::rc::PinWeak::downgrade(xx.clone());
348        move || {
349            if !done.get() {
350                done.set(true);
351                Property::link_two_way(p1.as_ref(), p2.as_ref());
352                let xx_weak = xx_weak.clone();
353                p1.as_ref().set_binding(move || xx_weak.upgrade().unwrap().as_ref().get() + 9);
354            }
355            global.as_ref().get() + 2
356        }
357    });
358    assert_eq!(xx.as_ref().get(), 23 + 2);
359    assert_eq!(p1.as_ref().get(), 23 + 2 + 9);
360    assert_eq!(p2.as_ref().get(), 23 + 2 + 9);
361
362    global.as_ref().set(55);
363    assert_eq!(p1.as_ref().get(), 55 + 2 + 9);
364    assert_eq!(p2.as_ref().get(), 55 + 2 + 9);
365    assert_eq!(xx.as_ref().get(), 55 + 2);
366}
367
368#[test]
369fn property_two_ways_binding_of_two_way_binding_first() {
370    let p1_1 = Rc::pin(Property::new(2));
371    let p1_2 = Rc::pin(Property::new(4));
372    Property::link_two_way(p1_1.as_ref(), p1_2.as_ref());
373
374    assert_eq!(p1_1.as_ref().get(), 4);
375    assert_eq!(p1_2.as_ref().get(), 4);
376
377    let p2 = Rc::pin(Property::new(3));
378    Property::link_two_way(p1_1.as_ref(), p2.as_ref());
379
380    assert_eq!(p1_1.as_ref().get(), 3);
381    assert_eq!(p1_2.as_ref().get(), 3);
382    assert_eq!(p2.as_ref().get(), 3);
383
384    p1_1.set(6);
385
386    assert_eq!(p1_1.as_ref().get(), 6);
387    assert_eq!(p1_2.as_ref().get(), 6);
388    assert_eq!(p2.as_ref().get(), 6);
389
390    p1_2.set(8);
391
392    assert_eq!(p1_1.as_ref().get(), 8);
393    assert_eq!(p1_2.as_ref().get(), 8);
394    assert_eq!(p2.as_ref().get(), 8);
395
396    p2.set(7);
397
398    assert_eq!(p1_1.as_ref().get(), 7);
399    assert_eq!(p1_2.as_ref().get(), 7);
400    assert_eq!(p2.as_ref().get(), 7);
401}
402
403#[test]
404fn property_two_ways_binding_of_two_way_binding_second() {
405    let p1 = Rc::pin(Property::new(2));
406    let p2_1 = Rc::pin(Property::new(3));
407    let p2_2 = Rc::pin(Property::new(5));
408    Property::link_two_way(p2_1.as_ref(), p2_2.as_ref());
409
410    assert_eq!(p2_1.as_ref().get(), 5);
411    assert_eq!(p2_2.as_ref().get(), 5);
412
413    Property::link_two_way(p1.as_ref(), p2_2.as_ref());
414
415    assert_eq!(p1.as_ref().get(), 5);
416    assert_eq!(p2_1.as_ref().get(), 5);
417    assert_eq!(p2_2.as_ref().get(), 5);
418
419    p1.set(6);
420
421    assert_eq!(p1.as_ref().get(), 6);
422    assert_eq!(p2_1.as_ref().get(), 6);
423    assert_eq!(p2_2.as_ref().get(), 6);
424
425    p2_1.set(7);
426
427    assert_eq!(p1.as_ref().get(), 7);
428    assert_eq!(p2_1.as_ref().get(), 7);
429    assert_eq!(p2_2.as_ref().get(), 7);
430
431    p2_2.set(9);
432
433    assert_eq!(p1.as_ref().get(), 9);
434    assert_eq!(p2_1.as_ref().get(), 9);
435    assert_eq!(p2_2.as_ref().get(), 9);
436}
437
438#[test]
439fn property_two_ways_binding_of_two_two_way_bindings() {
440    let p1_1 = Rc::pin(Property::new(2));
441    let p1_2 = Rc::pin(Property::new(4));
442    Property::link_two_way(p1_1.as_ref(), p1_2.as_ref());
443    assert_eq!(p1_1.as_ref().get(), 4);
444    assert_eq!(p1_2.as_ref().get(), 4);
445
446    let p2_1 = Rc::pin(Property::new(3));
447    let p2_2 = Rc::pin(Property::new(5));
448    Property::link_two_way(p2_1.as_ref(), p2_2.as_ref());
449
450    assert_eq!(p2_1.as_ref().get(), 5);
451    assert_eq!(p2_2.as_ref().get(), 5);
452
453    Property::link_two_way(p1_1.as_ref(), p2_2.as_ref());
454
455    assert_eq!(p1_1.as_ref().get(), 5);
456    assert_eq!(p1_2.as_ref().get(), 5);
457    assert_eq!(p2_1.as_ref().get(), 5);
458    assert_eq!(p2_2.as_ref().get(), 5);
459
460    p1_1.set(6);
461    assert_eq!(p1_1.as_ref().get(), 6);
462    assert_eq!(p1_2.as_ref().get(), 6);
463    assert_eq!(p2_1.as_ref().get(), 6);
464    assert_eq!(p2_2.as_ref().get(), 6);
465
466    p1_2.set(8);
467    assert_eq!(p1_1.as_ref().get(), 8);
468    assert_eq!(p1_2.as_ref().get(), 8);
469    assert_eq!(p2_1.as_ref().get(), 8);
470    assert_eq!(p2_2.as_ref().get(), 8);
471
472    p2_1.set(7);
473    assert_eq!(p1_1.as_ref().get(), 7);
474    assert_eq!(p1_2.as_ref().get(), 7);
475    assert_eq!(p2_1.as_ref().get(), 7);
476    assert_eq!(p2_2.as_ref().get(), 7);
477
478    p2_2.set(9);
479    assert_eq!(p1_1.as_ref().get(), 9);
480    assert_eq!(p1_2.as_ref().get(), 9);
481    assert_eq!(p2_1.as_ref().get(), 9);
482    assert_eq!(p2_2.as_ref().get(), 9);
483}
484
485#[test]
486fn test_two_way_with_map() {
487    #[derive(PartialEq, Clone, Default, Debug)]
488    struct Struct {
489        foo: i32,
490        bar: alloc::string::String,
491    }
492    let p1 = Rc::pin(Property::new(Struct { foo: 42, bar: "hello".into() }));
493    let p2 = Rc::pin(Property::new(88));
494    let p3 = Rc::pin(Property::new(alloc::string::String::from("xyz")));
495    Property::link_two_way_with_map(p1.as_ref(), p2.as_ref(), |s| s.foo, |s, foo| s.foo = *foo);
496    assert_eq!(p1.as_ref().get(), Struct { foo: 42, bar: "hello".into() });
497    assert_eq!(p2.as_ref().get(), 42);
498
499    p2.as_ref().set(81);
500    assert_eq!(p1.as_ref().get(), Struct { foo: 81, bar: "hello".into() });
501    assert_eq!(p2.as_ref().get(), 81);
502
503    p1.as_ref().set(Struct { foo: 78, bar: "world".into() });
504    assert_eq!(p1.as_ref().get(), Struct { foo: 78, bar: "world".into() });
505    assert_eq!(p2.as_ref().get(), 78);
506
507    Property::link_two_way_with_map(
508        p1.as_ref(),
509        p3.as_ref(),
510        |s| s.bar.clone(),
511        |s, bar| s.bar = bar.clone(),
512    );
513    assert_eq!(p1.as_ref().get(), Struct { foo: 78, bar: "world".into() });
514    assert_eq!(p2.as_ref().get(), 78);
515    assert_eq!(p3.as_ref().get(), "world");
516
517    p3.as_ref().set("abc".into());
518    assert_eq!(p1.as_ref().get(), Struct { foo: 78, bar: "abc".into() });
519    assert_eq!(p2.as_ref().get(), 78);
520    assert_eq!(p3.as_ref().get(), "abc");
521
522    let p4 = Rc::pin(Property::new(123));
523    p2.set_binding({
524        let p4 = p4.clone();
525        move || p4.as_ref().get() + 1
526    });
527
528    assert_eq!(p1.as_ref().get(), Struct { foo: 124, bar: "abc".into() });
529    assert_eq!(p2.as_ref().get(), 124);
530    assert_eq!(p3.as_ref().get(), "abc");
531
532    p4.as_ref().set(456);
533    assert_eq!(p1.as_ref().get(), Struct { foo: 457, bar: "abc".into() });
534    assert_eq!(p2.as_ref().get(), 457);
535    assert_eq!(p3.as_ref().get(), "abc");
536
537    p3.as_ref().set("def".into());
538    assert_eq!(p1.as_ref().get(), Struct { foo: 457, bar: "def".into() });
539    assert_eq!(p2.as_ref().get(), 457);
540    assert_eq!(p3.as_ref().get(), "def");
541
542    p4.as_ref().set(789);
543    // Note that the binding with `p2 : p4+1` is broken
544    assert_eq!(p1.as_ref().get(), Struct { foo: 457, bar: "def".into() });
545    assert_eq!(p2.as_ref().get(), 457);
546    assert_eq!(p3.as_ref().get(), "def");
547}
548
549/// Regression test for use-after-free in `link_two_way_with_map_to_common_property`.
550///
551/// When a property already has a binding with dependants and is then linked
552/// via `link_two_way_with_map`, the old binding's dependency list must be
553/// transferred to the new `TwoWayBindingWithMap` binding. Without this
554/// transfer, the old binding is later freed (via `BindingMapper::drop`) while
555/// dependency nodes still point into its `dependencies` field, causing
556/// panics in `DependencyNode::debug_assert_valid` on drop.
557///
558/// The drop order is arranged so that the tracker (which owns the
559/// dependency node) outlives the properties, forcing the node to be
560/// removed after the old binding would have been freed.
561#[test]
562fn test_two_way_with_map_dependency_list_transfer() {
563    #[derive(PartialEq, Clone, Default, Debug)]
564    struct Wrapper {
565        value: i32,
566    }
567
568    let source = Rc::pin(Property::new(10));
569
570    // Declare the tracker before the properties so it is dropped *after*
571    // them (Rust drops locals in reverse declaration order). This ensures
572    // the dependency node outlives the old binding.
573    let tracker = Box::pin(<PropertyTracker>::default());
574
575    let p_field = Rc::pin(Property::new(0i32));
576    p_field.as_ref().set_binding({
577        let source = source.clone();
578        move || source.as_ref().get() * 2
579    });
580    assert_eq!(p_field.as_ref().get(), 20);
581
582    // Evaluate the tracker, which reads p_field and registers a dependency
583    // node on p_field's binding's dependency list.
584    let val = tracker.as_ref().evaluate({
585        let p_field = p_field.clone();
586        move || p_field.as_ref().get()
587    });
588    assert_eq!(val, 20);
589    assert!(!tracker.as_ref().is_dirty());
590
591    // link_two_way_with_map replaces p_field's binding with a
592    // TwoWayBindingWithMap and wraps the old binding in a BindingMapper
593    // on the common property. The dependency list must be transferred.
594    let p_struct = Rc::pin(Property::new(Wrapper { value: 0 }));
595    Property::link_two_way_with_map(
596        p_struct.as_ref(),
597        p_field.as_ref(),
598        |s| s.value,
599        |s, v| s.value = *v,
600    );
601
602    assert_eq!(p_field.as_ref().get(), 20);
603    assert_eq!(p_struct.as_ref().get(), Wrapper { value: 20 });
604
605    // The tracker's dependency should still fire when the source changes.
606    source.as_ref().set(5);
607    assert!(tracker.as_ref().is_dirty());
608
609    // Implicit drop order: p_struct, p_field, tracker, source.
610    // p_field's drop frees the common property chain (including the old
611    // binding via BindingMapper::drop). tracker is dropped afterwards —
612    // its DependencyNode::remove would panic in debug_assert_valid if
613    // the dependency list was not properly transferred.
614}