Skip to main content

i_slint_core/model/
model_peer.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
4//! This module contains the implementation of the model change tracking.
5
6// Safety: we use pointer to ModelChangeListenerContainer in the DependencyList,
7// but the Drop of the ModelChangeListenerContainer will remove them from the list
8// so it will not be accessed after it is dropped
9#![allow(unsafe_code)]
10
11use super::*;
12use crate::properties::dependency_tracker::DependencyNode;
13
14type DependencyListHead =
15    crate::properties::dependency_tracker::DependencyListHead<*const dyn ModelChangeListener>;
16
17/// Represent a handle to a view that listens to changes to a model.
18///
19/// One should normally not use this class directly, it is just
20/// used internally by via [`ModelTracker::attach_peer`] and [`ModelNotify`]
21#[derive(Clone)]
22pub struct ModelPeer<'a> {
23    inner: Pin<&'a DependencyNode<*const dyn ModelChangeListener>>,
24}
25
26/// Which rows [`ModelTracker::track_row_data_changes`] and [`ModelTracker::track_any_change`]
27/// have registered a dependency on.
28enum TrackedRows {
29    /// Sorted list of individually tracked rows.
30    Rows(Vec<usize>),
31    /// track_any_change() was called, making every row implicitly tracked.
32    All,
33}
34
35impl Default for TrackedRows {
36    fn default() -> Self {
37        TrackedRows::Rows(Vec::new())
38    }
39}
40
41impl TrackedRows {
42    fn is_tracked(&self, row: usize) -> bool {
43        match self {
44            TrackedRows::Rows(rows) => rows.binary_search(&row).is_ok(),
45            TrackedRows::All => true,
46        }
47    }
48}
49
50#[pin_project]
51#[derive(Default)]
52struct ModelNotifyInner {
53    #[pin]
54    model_row_count_dirty_property: Property<()>,
55    #[pin]
56    model_row_data_dirty_property: Property<()>,
57    #[pin]
58    peers: DependencyListHead,
59    tracked_rows: RefCell<TrackedRows>,
60}
61
62/// Dispatch notifications from a [`Model`] to one or several [`ModelPeer`].
63/// Typically, you would want to put this in the implementation of the Model
64#[derive(Default)]
65pub struct ModelNotify {
66    inner: Pin<Box<ModelNotifyInner>>,
67}
68
69impl ModelNotify {
70    fn inner(&self) -> Pin<&ModelNotifyInner> {
71        self.inner.as_ref()
72    }
73
74    /// Notify the peers that a specific row was changed
75    pub fn row_changed(&self, row: usize) {
76        let inner = &self.inner;
77        if inner.tracked_rows.borrow().is_tracked(row) {
78            inner.model_row_data_dirty_property.mark_dirty();
79        }
80        inner.as_ref().project_ref().peers.for_each(|p| {
81            // Safety: The peers contain a list of pinned ModelChangedListener
82            unsafe { Pin::new_unchecked(&**p) }.row_changed(row)
83        })
84    }
85    /// Notify the peers that rows were added
86    pub fn row_added(&self, index: usize, count: usize) {
87        let inner = &self.inner;
88        inner.model_row_count_dirty_property.mark_dirty();
89        *inner.tracked_rows.borrow_mut() = TrackedRows::default();
90        inner.model_row_data_dirty_property.mark_dirty();
91        inner.as_ref().project_ref().peers.for_each(|p| {
92            // Safety: The peers contain a list of pinned ModelChangedListener
93            unsafe { Pin::new_unchecked(&**p) }.row_added(index, count)
94        })
95    }
96    /// Notify the peers that rows were removed
97    pub fn row_removed(&self, index: usize, count: usize) {
98        let inner = &self.inner;
99        inner.model_row_count_dirty_property.mark_dirty();
100        *inner.tracked_rows.borrow_mut() = TrackedRows::default();
101        inner.model_row_data_dirty_property.mark_dirty();
102        inner.as_ref().project_ref().peers.for_each(|p| {
103            // Safety: The peers contain a list of pinned ModelChangedListener
104            unsafe { Pin::new_unchecked(&**p) }.row_removed(index, count)
105        })
106    }
107
108    /// Notify the peer that the model has been changed in some way and
109    /// everything needs to be reloaded
110    pub fn reset(&self) {
111        let inner = &self.inner;
112        inner.model_row_count_dirty_property.mark_dirty();
113        *inner.tracked_rows.borrow_mut() = TrackedRows::default();
114        inner.model_row_data_dirty_property.mark_dirty();
115        inner.as_ref().project_ref().peers.for_each(|p| {
116            // Safety: The peers contain a list of pinned ModelChangedListener
117            unsafe { Pin::new_unchecked(&**p) }.reset()
118        })
119    }
120}
121
122impl ModelTracker for ModelNotify {
123    /// Attach one peer. The peer will be notified when the model changes
124    fn attach_peer(&self, peer: ModelPeer) {
125        self.inner().project_ref().peers.append(peer.inner)
126    }
127
128    fn track_row_count_changes(&self) {
129        self.inner().project_ref().model_row_count_dirty_property.get();
130    }
131
132    fn track_row_data_changes(&self, row: usize) {
133        if crate::properties::is_currently_tracking() {
134            let inner = self.inner().project_ref();
135
136            // Recording the row individually is redundant once every row is tracked.
137            if let TrackedRows::Rows(rows) = &mut *inner.tracked_rows.borrow_mut()
138                && let Err(insertion_point) = rows.binary_search(&row)
139            {
140                rows.insert(insertion_point, row);
141            }
142
143            inner.model_row_data_dirty_property.get();
144        }
145    }
146
147    fn track_any_change(&self, _row_count: usize, _: crate::InternalToken) {
148        self.track_row_count_changes();
149        if crate::properties::is_currently_tracking() {
150            let inner = self.inner().project_ref();
151            // Any individually tracked rows are now subsumed by the whole-model dependency.
152            *inner.tracked_rows.borrow_mut() = TrackedRows::All;
153            inner.model_row_data_dirty_property.get();
154        }
155    }
156}
157
158pub trait ModelChangeListener {
159    fn row_changed(self: Pin<&Self>, row: usize);
160    fn row_added(self: Pin<&Self>, index: usize, count: usize);
161    fn row_removed(self: Pin<&Self>, index: usize, count: usize);
162    fn reset(self: Pin<&Self>);
163}
164
165#[pin_project(PinnedDrop)]
166#[derive(Default, derive_more::Deref)]
167/// This is a structure that contains a T which implements [`ModelChangeListener`]
168/// and can provide a [`ModelPeer`] for it when pinned.
169pub struct ModelChangeListenerContainer<T: ModelChangeListener> {
170    /// Will be initialized when the ModelPeer is initialized.
171    /// The DependencyNode points to data.
172    peer: OnceCell<DependencyNode<*const dyn ModelChangeListener>>,
173
174    #[pin]
175    #[deref]
176    data: T,
177}
178
179#[pin_project::pinned_drop]
180impl<T: ModelChangeListener> PinnedDrop for ModelChangeListenerContainer<T> {
181    fn drop(self: Pin<&mut Self>) {
182        if let Some(peer) = self.peer.get() {
183            peer.remove();
184        }
185    }
186}
187
188impl<T: ModelChangeListener + 'static> ModelChangeListenerContainer<T> {
189    pub fn new(data: T) -> Self {
190        Self { peer: Default::default(), data }
191    }
192
193    pub fn model_peer(self: Pin<&Self>) -> ModelPeer<'_> {
194        let peer = self.get_ref().peer.get_or_init(|| {
195            //Safety: self.data and self.peer have the same lifetime, so the pointer stays valid
196            DependencyNode::new(
197                (&self.data) as &dyn ModelChangeListener as *const dyn ModelChangeListener,
198            )
199        });
200
201        // Safety: `peer` is pinned because `self` is pinned and it is a projection, but pin_project don't go through the OnceCell
202        let peer = unsafe { Pin::new_unchecked(peer) };
203
204        ModelPeer { inner: peer }
205    }
206
207    pub fn get(self: Pin<&Self>) -> Pin<&T> {
208        self.project_ref().data
209    }
210}
211
212/// A pinned `ModelChangeListenerContainer` using `NonNull` instead of `Box`
213/// to avoid aliasing issues when the struct is moved into `Rc::new()`.
214pub struct ModelChangeListenerBox<T: ModelChangeListener + 'static> {
215    ptr: core::ptr::NonNull<ModelChangeListenerContainer<T>>,
216}
217
218impl<T: ModelChangeListener + 'static> ModelChangeListenerBox<T> {
219    pub fn new(data: T) -> Self {
220        let container = ModelChangeListenerContainer::new(data);
221        // Safety: Box::into_raw returns a non-null pointer
222        let ptr = unsafe { core::ptr::NonNull::new_unchecked(Box::into_raw(Box::new(container))) };
223        Self { ptr }
224    }
225
226    pub fn as_ref(&self) -> Pin<&ModelChangeListenerContainer<T>> {
227        // Safety: the data is pinned because we never move it or expose &mut to it
228        unsafe { Pin::new_unchecked(self.ptr.as_ref()) }
229    }
230}
231
232impl<T: ModelChangeListener + 'static> core::ops::Deref for ModelChangeListenerBox<T> {
233    type Target = T;
234    fn deref(&self) -> &T {
235        // Safety: ptr is valid for the lifetime of self
236        unsafe { &self.ptr.as_ref().data }
237    }
238}
239
240impl<T: ModelChangeListener + 'static> Drop for ModelChangeListenerBox<T> {
241    fn drop(&mut self) {
242        // Safety: we own the allocation and it was created by Box::new.
243        // Box::from_raw runs PinnedDrop which calls peer.remove().
244        unsafe { drop(Box::from_raw(self.ptr.as_ptr())) }
245    }
246}