Skip to main content

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