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