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                velocity: |_| None,
174            };
175        }
176        let holder = BindingHolder {
177            dependencies: Cell::new(core::ptr::null_mut()),
178            dep_nodes: Default::default(),
179            vtable: <ChangeTrackerInner<T, EF, NF, Data> as HasBindingVTable>::VT,
180            dirty: Cell::new(false),
181            is_two_way_binding: false,
182            pinned: PhantomPinned,
183            binding: inner,
184            #[cfg(slint_debug_property)]
185            debug_name: "<ChangeTracker>".into(),
186        };
187
188        let raw = Box::into_raw(Box::new(holder));
189        unsafe { self.set_internal(raw as *mut BindingHolder) };
190        if delayed {
191            // Safety: raw is valid and we own it
192            unsafe {
193                let dep_nodes = &mut *(*core::ptr::addr_of!((*raw).dep_nodes)).get();
194                let node = dep_nodes.push_front(DependencyNode::new(raw as *const BindingHolder));
195                CHANGED_NODES.with(|changed_nodes| {
196                    changed_nodes.append(node);
197                });
198            }
199            return;
200        }
201        let value = unsafe {
202            let inner = core::ptr::addr_of!((*raw).binding).as_ref().unwrap();
203            super::current_binding_storage::set(Some(raw as *const BindingHolder), || {
204                (inner.eval_fn)(&inner.data)
205            })
206        };
207        unsafe {
208            *core::ptr::addr_of_mut!((*raw).binding).as_mut().unwrap().value.get_mut() = value
209        };
210    }
211
212    /// Test helper: the number of dependency nodes the bound holder currently owns.
213    #[cfg(test)]
214    pub(crate) fn test_dep_node_count(&self) -> usize {
215        let inner = self.inner.get();
216        if inner.is_null() {
217            return 0;
218        }
219        unsafe { (*(*core::ptr::addr_of!((*inner).dep_nodes)).get()).iter().count() }
220    }
221
222    /// Clear the change tracker.
223    /// No notify function will be called after this.
224    pub fn clear(&self) {
225        let inner = self.inner.get();
226        if !inner.is_null() {
227            unsafe {
228                let drop = (*core::ptr::addr_of!((*inner).vtable)).drop;
229                drop(inner);
230            }
231            self.inner.set(core::ptr::null_mut());
232        }
233    }
234
235    /// Run all the change handlers that were queued, looping until no more
236    /// handlers are pending (up to an internal limit).
237    pub fn run_change_handlers() {
238        for _ in 0..10 {
239            if !Self::run_change_handlers_once() {
240                return;
241            }
242        }
243        crate::debug_log!("Slint: long changed callback chain detected");
244    }
245
246    /// Run one round of pending change handlers.
247    /// Returns `true` if any handler was executed.
248    pub fn run_change_handlers_once() -> bool {
249        CHANGED_NODES.with(|list| {
250            if list.is_empty() {
251                return false;
252            }
253            let old_list = DependencyListHead::default();
254            let old_list = core::pin::pin!(old_list);
255            DependencyListHead::swap(list.as_ref(), old_list.as_ref());
256            while let Some(node) = old_list.take_head() {
257                unsafe {
258                    ((*addr_of!((*node).vtable)).evaluate)(
259                        node as *mut BindingHolder,
260                        core::ptr::null_mut(),
261                    );
262                }
263            }
264            true
265        })
266    }
267
268    pub(super) unsafe fn mark_dirty(_self: *const BindingHolder, _was_dirty: bool) {
269        unsafe {
270            // Take dep_nodes out so we can iterate without alias conflicts.
271            let dep_nodes = &mut *(*_self).dep_nodes.get();
272            let node_head = core::mem::take(dep_nodes);
273            if let Some(node) = node_head.iter().next() {
274                node.remove();
275                CHANGED_NODES.with(|changed_nodes| {
276                    changed_nodes.append(node);
277                });
278            }
279            // Restore the list.
280            let other = core::mem::replace(dep_nodes, node_head);
281            debug_assert!(other.iter().next().is_none());
282        }
283    }
284
285    pub(super) unsafe fn set_internal(&self, raw: *mut BindingHolder) {
286        self.inner.set(raw);
287    }
288}
289
290#[test]
291fn change_tracker() {
292    use super::Property;
293    use std::rc::Rc;
294    let prop1 = Rc::pin(Property::new(42));
295    let prop2 = Rc::pin(Property::<i32>::default());
296    prop2.as_ref().set_binding({
297        let prop1 = prop1.clone();
298        move || prop1.as_ref().get() * 2
299    });
300
301    let change1 = ChangeTracker::default();
302    let change2 = ChangeTracker::default();
303
304    let state = Rc::new(core::cell::RefCell::new(std::string::String::new()));
305
306    change1.init(
307        (state.clone(), prop1.clone()),
308        |(_, prop1)| prop1.as_ref().get(),
309        |(state, _), val| {
310            *state.borrow_mut() += &std::format!(":1({val})");
311        },
312    );
313    change2.init(
314        (state.clone(), prop2.clone()),
315        |(_, prop2)| prop2.as_ref().get(),
316        |(state, _), val| {
317            *state.borrow_mut() += &std::format!(":2({val})");
318        },
319    );
320
321    assert_eq!(state.borrow().as_str(), "");
322    prop1.as_ref().set(10);
323    assert_eq!(state.borrow().as_str(), "");
324    prop1.as_ref().set(30);
325    assert_eq!(state.borrow().as_str(), "");
326
327    ChangeTracker::run_change_handlers();
328    assert_eq!(state.borrow().as_str(), ":1(30):2(60)");
329    ChangeTracker::run_change_handlers();
330    assert_eq!(state.borrow().as_str(), ":1(30):2(60)");
331    prop1.as_ref().set(1);
332    assert_eq!(state.borrow().as_str(), ":1(30):2(60)");
333    ChangeTracker::run_change_handlers();
334    assert_eq!(state.borrow().as_str(), ":1(30):2(60):1(1):2(2)");
335}
336
337/// test for issue #8741
338#[test]
339fn delete_from_eval_fn() {
340    use std::cell::RefCell;
341    use std::rc::Rc;
342    use std::string::String;
343
344    let change = Rc::<RefCell<Option<ChangeTracker>>>::new(Some(ChangeTracker::default()).into());
345    let xyz = RefCell::new(String::from("*"));
346    let result = Rc::new(RefCell::new(String::new()));
347    let result2 = result.clone();
348    // 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`
349    let another = Rc::<RefCell<Option<ChangeTracker>>>::new(Some(ChangeTracker::default()).into());
350    another.borrow().as_ref().unwrap().init_delayed(
351        (),
352        |()| unreachable!(),
353        move |(), &()| unreachable!(),
354    );
355    change.borrow().as_ref().unwrap().init_delayed(
356        change.clone(),
357        |x| {
358            x.borrow_mut().take().unwrap();
359            String::from("hi")
360        },
361        move |x, val| {
362            assert!(x.borrow().is_none());
363            assert_eq!(val, "hi");
364            xyz.borrow_mut().push_str("+");
365            assert!(xyz.borrow().as_str().starts_with("*+"));
366            result2.replace(xyz.borrow().clone());
367            another.borrow_mut().take().unwrap();
368        },
369    );
370
371    assert_eq!(result.borrow().as_str(), "");
372    ChangeTracker::run_change_handlers();
373    assert_eq!(result.borrow().as_str(), "*+");
374    ChangeTracker::run_change_handlers();
375    assert_eq!(result.borrow().as_str(), "*+");
376}
377
378#[test]
379fn change_multiple_dependencies() {
380    use super::Property;
381    use std::cell::RefCell;
382    use std::rc::Rc;
383    use std::string::String;
384    let prop1 = Rc::pin(Property::new(1));
385    let prop2 = Rc::pin(Property::new(2));
386    let prop3 = Rc::pin(Property::new(3));
387    let prop4 = Rc::pin(Property::new(4));
388    let prop_with_deps = Rc::pin(Property::new(5));
389    let result = Rc::new(RefCell::new(String::new()));
390
391    let change_tracker = ChangeTracker::default();
392    change_tracker.init(
393        result.clone(),
394        {
395            let prop1 = prop1.clone();
396            let prop2 = prop2.clone();
397            let prop3 = prop3.clone();
398            let prop4 = prop4.clone();
399            let prop_with_deps = prop_with_deps.clone();
400            move |_| {
401                prop1.as_ref().get()
402                    + prop2.as_ref().get()
403                    + prop3.as_ref().get()
404                    + prop4.as_ref().get()
405                    + prop_with_deps.as_ref().get()
406            }
407        },
408        move |result, val| {
409            *result.borrow_mut() += &std::format!("[{val}]");
410        },
411    );
412
413    assert_eq!(result.borrow().as_str(), "");
414    ChangeTracker::run_change_handlers();
415    assert_eq!(result.borrow().as_str(), "");
416
417    prop_with_deps.as_ref().set_binding({
418        let prop1 = prop1.clone();
419        let prop2 = prop2.clone();
420        move || prop1.as_ref().get() + prop2.as_ref().get()
421    });
422
423    assert_eq!(result.borrow().as_str(), "");
424    ChangeTracker::run_change_handlers();
425    assert_eq!(prop_with_deps.as_ref().get(), 3);
426    assert_eq!(result.borrow().as_str(), "[13]"); // 1 + 2 + 3 + 4 + 3
427
428    ChangeTracker::run_change_handlers();
429    assert_eq!(result.borrow().as_str(), "[13]");
430
431    prop1.as_ref().set(10);
432    assert_eq!(result.borrow().as_str(), "[13]");
433    ChangeTracker::run_change_handlers();
434    assert_eq!(result.borrow().as_str(), "[13][31]"); // 10 + 2 + 3 + 4 + 12
435
436    prop2.as_ref().set(20);
437    prop3.as_ref().set(30);
438    assert_eq!(result.borrow().as_str(), "[13][31]");
439    ChangeTracker::run_change_handlers();
440    assert_eq!(result.borrow().as_str(), "[13][31][94]"); // 10 + 20 + 30 + 4 + 30
441
442    ChangeTracker::run_change_handlers();
443    assert_eq!(result.borrow().as_str(), "[13][31][94]");
444
445    // just swap prop1 and prop2, doesn't change the outcome
446    prop1.as_ref().set(20);
447    prop2.as_ref().set(10);
448    ChangeTracker::run_change_handlers();
449    assert_eq!(result.borrow().as_str(), "[13][31][94]");
450}