i_slint_core/properties/
change_tracker.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::{
5    single_linked_list_pin::SingleLinkedListPinHead, BindingHolder, BindingResult, BindingVTable,
6    DependencyListHead, DependencyNode,
7};
8use alloc::boxed::Box;
9use core::cell::Cell;
10use core::marker::PhantomPinned;
11use core::pin::Pin;
12use core::ptr::addr_of;
13
14// TODO a pinned thread local key?
15crate::thread_local! {static CHANGED_NODES : Pin<Box<DependencyListHead>> = Box::pin(DependencyListHead::default()) }
16
17struct ChangeTrackerInner<T, EvalFn, NotifyFn, Data> {
18    eval_fn: EvalFn,
19    notify_fn: NotifyFn,
20    value: T,
21    data: Data,
22    /// When true, we are currently running eval_fn or notify_fn and we shouldn't be dropped
23    evaluating: bool,
24}
25
26/// A change tracker is used to run a callback when a property value changes.
27///
28/// The Change Tracker must be initialized with the [`Self::init`] method.
29///
30/// When the property changes, the ChangeTracker is added to a thread local list, and the notify
31/// callback is called when the [`Self::run_change_handlers()`] method is called
32#[derive(Debug)]
33pub struct ChangeTracker {
34    /// (Actually a `BindingHolder<ChangeTrackerInner>`)
35    inner: Cell<*mut BindingHolder>,
36}
37
38impl Default for ChangeTracker {
39    fn default() -> Self {
40        Self { inner: Cell::new(core::ptr::null_mut()) }
41    }
42}
43
44impl Drop for ChangeTracker {
45    fn drop(&mut self) {
46        self.clear();
47    }
48}
49
50impl ChangeTracker {
51    /// Initialize the change tracker with the given data and callbacks.
52    ///
53    /// The `data` is any struct that is going to be passed to the functor.
54    /// The `eval_fn` is a function that queries and return the property.
55    /// And the `notify_fn` is the callback run if the property is changed
56    pub fn init<Data, T: Default + PartialEq, EF: Fn(&Data) -> T, NF: Fn(&Data, &T)>(
57        &self,
58        data: Data,
59        eval_fn: EF,
60        notify_fn: NF,
61    ) {
62        self.init_impl(data, eval_fn, notify_fn, false);
63    }
64
65    /// Initialize the change tracker with the given data and callbacks.
66    ///
67    /// Same as [`Self::init`], but the first eval function is called in a future evaluation of the event loop.
68    /// This means that the change tracker will consider the value as default initialized, and the eval function will
69    /// be called the firs ttime if the initial value is not equal to the default constructed value.
70    pub fn init_delayed<Data, T: Default + PartialEq, EF: Fn(&Data) -> T, NF: Fn(&Data, &T)>(
71        &self,
72        data: Data,
73        eval_fn: EF,
74        notify_fn: NF,
75    ) {
76        self.init_impl(data, eval_fn, notify_fn, true);
77    }
78
79    fn init_impl<Data, T: Default + PartialEq, EF: Fn(&Data) -> T, NF: Fn(&Data, &T)>(
80        &self,
81        data: Data,
82        eval_fn: EF,
83        notify_fn: NF,
84        delayed: bool,
85    ) {
86        self.clear();
87        let inner =
88            ChangeTrackerInner { eval_fn, notify_fn, value: T::default(), data, evaluating: false };
89
90        unsafe fn evaluate<T: PartialEq, EF: Fn(&Data) -> T, NF: Fn(&Data, &T), Data>(
91            _self: *mut BindingHolder,
92            _value: *mut (),
93        ) -> BindingResult {
94            let pinned_holder = Pin::new_unchecked(&*_self);
95            let _self = _self as *mut BindingHolder<ChangeTrackerInner<T, EF, NF, Data>>;
96            let inner = core::ptr::addr_of_mut!((*_self).binding).as_mut().unwrap();
97            (*core::ptr::addr_of_mut!((*_self).dep_nodes)).take();
98            assert!(!inner.evaluating);
99            inner.evaluating = true;
100            let new_value =
101                super::CURRENT_BINDING.set(Some(pinned_holder), || (inner.eval_fn)(&inner.data));
102            if new_value != inner.value {
103                inner.value = new_value;
104                (inner.notify_fn)(&inner.data, &inner.value);
105            }
106            if !core::mem::replace(&mut inner.evaluating, false) {
107                // `drop` from the vtable was called while evaluating. Do it now.
108                core::mem::drop(Box::from_raw(_self));
109            }
110            BindingResult::KeepBinding
111        }
112
113        unsafe fn drop<T, EF, NF, Data>(_self: *mut BindingHolder) {
114            let _self = _self as *mut BindingHolder<ChangeTrackerInner<T, EF, NF, Data>>;
115            let evaluating = core::mem::replace(
116                &mut core::ptr::addr_of_mut!((*_self).binding).as_mut().unwrap().evaluating,
117                false,
118            );
119            if !evaluating {
120                core::mem::drop(Box::from_raw(_self));
121            }
122        }
123
124        trait HasBindingVTable {
125            const VT: &'static BindingVTable;
126        }
127        impl<T: PartialEq, EF: Fn(&Data) -> T, NF: Fn(&Data, &T), Data> HasBindingVTable
128            for ChangeTrackerInner<T, EF, NF, Data>
129        {
130            const VT: &'static BindingVTable = &BindingVTable {
131                drop: drop::<T, EF, NF, Data>,
132                evaluate: evaluate::<T, EF, NF, Data>,
133                mark_dirty: ChangeTracker::mark_dirty,
134                intercept_set: |_, _| false,
135                intercept_set_binding: |_, _| false,
136            };
137        }
138        let holder = BindingHolder {
139            dependencies: Cell::new(0),
140            dep_nodes: Default::default(),
141            vtable: <ChangeTrackerInner<T, EF, NF, Data> as HasBindingVTable>::VT,
142            dirty: Cell::new(false),
143            is_two_way_binding: false,
144            pinned: PhantomPinned,
145            binding: inner,
146            #[cfg(slint_debug_property)]
147            debug_name: "<ChangeTracker>".into(),
148        };
149
150        let raw = Box::into_raw(Box::new(holder));
151        unsafe { self.set_internal(raw as *mut BindingHolder) };
152        if delayed {
153            let mut dep_nodes = SingleLinkedListPinHead::default();
154            let node = dep_nodes.push_front(DependencyNode::new(raw as *const BindingHolder));
155            CHANGED_NODES.with(|changed_nodes| {
156                changed_nodes.append(node);
157            });
158            unsafe { (*core::ptr::addr_of_mut!((*raw).dep_nodes)).set(dep_nodes) };
159            return;
160        }
161        let value = unsafe {
162            let pinned_holder = Pin::new_unchecked((raw as *mut BindingHolder).as_ref().unwrap());
163            let inner = core::ptr::addr_of!((*raw).binding).as_ref().unwrap();
164            super::CURRENT_BINDING.set(Some(pinned_holder), || (inner.eval_fn)(&inner.data))
165        };
166        unsafe { core::ptr::addr_of_mut!((*raw).binding).as_mut().unwrap().value = value };
167    }
168
169    /// Clear the change tracker.
170    /// No notify function will be called after this.
171    pub fn clear(&self) {
172        let inner = self.inner.get();
173        if !inner.is_null() {
174            unsafe {
175                let drop = (*core::ptr::addr_of!((*inner).vtable)).drop;
176                drop(inner);
177            }
178            self.inner.set(core::ptr::null_mut());
179        }
180    }
181
182    /// Run all the change handler that were queued.
183    pub fn run_change_handlers() {
184        CHANGED_NODES.with(|list| {
185            let old_list = DependencyListHead::default();
186            let old_list = core::pin::pin!(old_list);
187            let mut count = 0;
188            while !list.is_empty() {
189                count += 1;
190                if count > 9 {
191                    crate::debug_log!("Slint: long changed callback chain detected");
192                    return;
193                }
194                DependencyListHead::swap(list.as_ref(), old_list.as_ref());
195                while let Some(node) = old_list.take_head() {
196                    unsafe {
197                        ((*addr_of!((*node).vtable)).evaluate)(
198                            node as *mut BindingHolder,
199                            core::ptr::null_mut(),
200                        );
201                    }
202                }
203            }
204        });
205    }
206
207    pub(super) unsafe fn mark_dirty(_self: *const BindingHolder, _was_dirty: bool) {
208        let _self = _self.as_ref().unwrap();
209        let node_head = _self.dep_nodes.take();
210        if let Some(node) = node_head.iter().next() {
211            node.remove();
212            CHANGED_NODES.with(|changed_nodes| {
213                changed_nodes.append(node);
214            });
215        }
216        let other = _self.dep_nodes.replace(node_head);
217        debug_assert!(other.iter().next().is_none());
218    }
219
220    pub(super) unsafe fn set_internal(&self, raw: *mut BindingHolder) {
221        self.inner.set(raw);
222    }
223}
224
225#[test]
226fn change_tracker() {
227    use super::Property;
228    use std::rc::Rc;
229    let prop1 = Rc::pin(Property::new(42));
230    let prop2 = Rc::pin(Property::<i32>::default());
231    prop2.as_ref().set_binding({
232        let prop1 = prop1.clone();
233        move || prop1.as_ref().get() * 2
234    });
235
236    let change1 = ChangeTracker::default();
237    let change2 = ChangeTracker::default();
238
239    let state = Rc::new(core::cell::RefCell::new(std::string::String::new()));
240
241    change1.init(
242        (state.clone(), prop1.clone()),
243        |(_, prop1)| prop1.as_ref().get(),
244        |(state, _), val| {
245            *state.borrow_mut() += &std::format!(":1({val})");
246        },
247    );
248    change2.init(
249        (state.clone(), prop2.clone()),
250        |(_, prop2)| prop2.as_ref().get(),
251        |(state, _), val| {
252            *state.borrow_mut() += &std::format!(":2({val})");
253        },
254    );
255
256    assert_eq!(state.borrow().as_str(), "");
257    prop1.as_ref().set(10);
258    assert_eq!(state.borrow().as_str(), "");
259    prop1.as_ref().set(30);
260    assert_eq!(state.borrow().as_str(), "");
261
262    ChangeTracker::run_change_handlers();
263    assert_eq!(state.borrow().as_str(), ":1(30):2(60)");
264    ChangeTracker::run_change_handlers();
265    assert_eq!(state.borrow().as_str(), ":1(30):2(60)");
266    prop1.as_ref().set(1);
267    assert_eq!(state.borrow().as_str(), ":1(30):2(60)");
268    ChangeTracker::run_change_handlers();
269    assert_eq!(state.borrow().as_str(), ":1(30):2(60):1(1):2(2)");
270}
271
272/// test for issue #8741
273#[test]
274fn delete_from_eval_fn() {
275    use std::cell::RefCell;
276    use std::rc::Rc;
277    use std::string::String;
278
279    let change = Rc::<RefCell<Option<ChangeTracker>>>::new(Some(ChangeTracker::default()).into());
280    let xyz = RefCell::new(String::from("*"));
281    let result = Rc::new(RefCell::new(String::new()));
282    let result2 = result.clone();
283    // The change event are run in reverse order as they are created, so this one shouldn't be ever called as it is being detroyed from `change`
284    let another = Rc::<RefCell<Option<ChangeTracker>>>::new(Some(ChangeTracker::default()).into());
285    another.borrow().as_ref().unwrap().init_delayed(
286        (),
287        |()| unreachable!(),
288        move |(), &()| unreachable!(),
289    );
290    change.borrow().as_ref().unwrap().init_delayed(
291        change.clone(),
292        |x| {
293            x.borrow_mut().take().unwrap();
294            String::from("hi")
295        },
296        move |x, val| {
297            assert!(x.borrow().is_none());
298            assert_eq!(val, "hi");
299            xyz.borrow_mut().push_str("+");
300            assert!(xyz.borrow().as_str().starts_with("*+"));
301            result2.replace(xyz.borrow().clone());
302            another.borrow_mut().take().unwrap();
303        },
304    );
305
306    assert_eq!(result.borrow().as_str(), "");
307    ChangeTracker::run_change_handlers();
308    assert_eq!(result.borrow().as_str(), "*+");
309    ChangeTracker::run_change_handlers();
310    assert_eq!(result.borrow().as_str(), "*+");
311}
312
313#[test]
314fn change_mutliple_dependencies() {
315    use super::Property;
316    use std::cell::RefCell;
317    use std::rc::Rc;
318    use std::string::String;
319    let prop1 = Rc::pin(Property::new(1));
320    let prop2 = Rc::pin(Property::new(2));
321    let prop3 = Rc::pin(Property::new(3));
322    let prop4 = Rc::pin(Property::new(4));
323    let prop_with_deps = Rc::pin(Property::new(5));
324    let result = Rc::new(RefCell::new(String::new()));
325
326    let change_tracker = ChangeTracker::default();
327    change_tracker.init(
328        result.clone(),
329        {
330            let prop1 = prop1.clone();
331            let prop2 = prop2.clone();
332            let prop3 = prop3.clone();
333            let prop4 = prop4.clone();
334            let prop_with_deps = prop_with_deps.clone();
335            move |_| {
336                prop1.as_ref().get()
337                    + prop2.as_ref().get()
338                    + prop3.as_ref().get()
339                    + prop4.as_ref().get()
340                    + prop_with_deps.as_ref().get()
341            }
342        },
343        move |result, val| {
344            *result.borrow_mut() += &std::format!("[{val}]");
345        },
346    );
347
348    assert_eq!(result.borrow().as_str(), "");
349    ChangeTracker::run_change_handlers();
350    assert_eq!(result.borrow().as_str(), "");
351
352    prop_with_deps.as_ref().set_binding({
353        let prop1 = prop1.clone();
354        let prop2 = prop2.clone();
355        move || prop1.as_ref().get() + prop2.as_ref().get()
356    });
357
358    assert_eq!(result.borrow().as_str(), "");
359    ChangeTracker::run_change_handlers();
360    assert_eq!(prop_with_deps.as_ref().get(), 3);
361    assert_eq!(result.borrow().as_str(), "[13]"); // 1 + 2 + 3 + 4 + 3
362
363    ChangeTracker::run_change_handlers();
364    assert_eq!(result.borrow().as_str(), "[13]");
365
366    prop1.as_ref().set(10);
367    assert_eq!(result.borrow().as_str(), "[13]");
368    ChangeTracker::run_change_handlers();
369    assert_eq!(result.borrow().as_str(), "[13][31]"); // 10 + 2 + 3 + 4 + 12
370
371    prop2.as_ref().set(20);
372    prop3.as_ref().set(30);
373    assert_eq!(result.borrow().as_str(), "[13][31]");
374    ChangeTracker::run_change_handlers();
375    assert_eq!(result.borrow().as_str(), "[13][31][94]"); // 10 + 20 + 30 + 4 + 30
376
377    ChangeTracker::run_change_handlers();
378    assert_eq!(result.borrow().as_str(), "[13][31][94]");
379
380    // just swap prop1 and prop2, doesn't change the outcome
381    prop1.as_ref().set(20);
382    prop2.as_ref().set(10);
383    ChangeTracker::run_change_handlers();
384    assert_eq!(result.borrow().as_str(), "[13][31][94]");
385}