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#[pin_project]
27#[derive(Default)]
28struct ModelNotifyInner {
29    #[pin]
30    model_row_count_dirty_property: Property<()>,
31    #[pin]
32    model_row_data_dirty_property: Property<()>,
33    #[pin]
34    peers: DependencyListHead,
35    // Sorted list of rows that track_row_data_changes() was called for
36    tracked_rows: RefCell<Vec<usize>>,
37}
38
39/// Dispatch notifications from a [`Model`] to one or several [`ModelPeer`].
40/// Typically, you would want to put this in the implementation of the Model
41#[derive(Default)]
42pub struct ModelNotify {
43    inner: Pin<Box<ModelNotifyInner>>,
44}
45
46impl ModelNotify {
47    fn inner(&self) -> Pin<&ModelNotifyInner> {
48        self.inner.as_ref()
49    }
50
51    /// Notify the peers that a specific row was changed
52    pub fn row_changed(&self, row: usize) {
53        let inner = &self.inner;
54        if inner.tracked_rows.borrow().binary_search(&row).is_ok() {
55            inner.model_row_data_dirty_property.mark_dirty();
56        }
57        inner.as_ref().project_ref().peers.for_each(|p| {
58            // Safety: The peers contain a list of pinned ModelChangedListener
59            unsafe { Pin::new_unchecked(&**p) }.row_changed(row)
60        })
61    }
62    /// Notify the peers that rows were added
63    pub fn row_added(&self, index: usize, count: usize) {
64        let inner = &self.inner;
65        inner.model_row_count_dirty_property.mark_dirty();
66        inner.tracked_rows.borrow_mut().clear();
67        inner.model_row_data_dirty_property.mark_dirty();
68        inner.as_ref().project_ref().peers.for_each(|p| {
69            // Safety: The peers contain a list of pinned ModelChangedListener
70            unsafe { Pin::new_unchecked(&**p) }.row_added(index, count)
71        })
72    }
73    /// Notify the peers that rows were removed
74    pub fn row_removed(&self, index: usize, count: usize) {
75        let inner = &self.inner;
76        inner.model_row_count_dirty_property.mark_dirty();
77        inner.tracked_rows.borrow_mut().clear();
78        inner.model_row_data_dirty_property.mark_dirty();
79        inner.as_ref().project_ref().peers.for_each(|p| {
80            // Safety: The peers contain a list of pinned ModelChangedListener
81            unsafe { Pin::new_unchecked(&**p) }.row_removed(index, count)
82        })
83    }
84
85    /// Notify the peer that the model has been changed in some way and
86    /// everything needs to be reloaded
87    pub fn reset(&self) {
88        let inner = &self.inner;
89        inner.model_row_count_dirty_property.mark_dirty();
90        inner.tracked_rows.borrow_mut().clear();
91        inner.model_row_data_dirty_property.mark_dirty();
92        inner.as_ref().project_ref().peers.for_each(|p| {
93            // Safety: The peers contain a list of pinned ModelChangedListener
94            unsafe { Pin::new_unchecked(&**p) }.reset()
95        })
96    }
97}
98
99impl ModelTracker for ModelNotify {
100    /// Attach one peer. The peer will be notified when the model changes
101    fn attach_peer(&self, peer: ModelPeer) {
102        self.inner().project_ref().peers.append(peer.inner)
103    }
104
105    fn track_row_count_changes(&self) {
106        self.inner().project_ref().model_row_count_dirty_property.get();
107    }
108
109    fn track_row_data_changes(&self, row: usize) {
110        if crate::properties::is_currently_tracking() {
111            let inner = self.inner().project_ref();
112
113            let mut tracked_rows = inner.tracked_rows.borrow_mut();
114            if let Err(insertion_point) = tracked_rows.binary_search(&row) {
115                tracked_rows.insert(insertion_point, row);
116            }
117
118            inner.model_row_data_dirty_property.get();
119        }
120    }
121}
122
123pub trait ModelChangeListener {
124    fn row_changed(self: Pin<&Self>, row: usize);
125    fn row_added(self: Pin<&Self>, index: usize, count: usize);
126    fn row_removed(self: Pin<&Self>, index: usize, count: usize);
127    fn reset(self: Pin<&Self>);
128}
129
130#[pin_project(PinnedDrop)]
131#[derive(Default, derive_more::Deref)]
132/// This is a structure that contains a T which implements [`ModelChangeListener`]
133/// and can provide a [`ModelPeer`] for it when pinned.
134pub struct ModelChangeListenerContainer<T: ModelChangeListener> {
135    /// Will be initialized when the ModelPeer is initialized.
136    /// The DependencyNode points to data.
137    peer: OnceCell<DependencyNode<*const dyn ModelChangeListener>>,
138
139    #[pin]
140    #[deref]
141    data: T,
142}
143
144#[pin_project::pinned_drop]
145impl<T: ModelChangeListener> PinnedDrop for ModelChangeListenerContainer<T> {
146    fn drop(self: Pin<&mut Self>) {
147        if let Some(peer) = self.peer.get() {
148            peer.remove();
149        }
150    }
151}
152
153impl<T: ModelChangeListener + 'static> ModelChangeListenerContainer<T> {
154    pub fn new(data: T) -> Self {
155        Self { peer: Default::default(), data }
156    }
157
158    pub fn model_peer(self: Pin<&Self>) -> ModelPeer<'_> {
159        let peer = self.get_ref().peer.get_or_init(|| {
160            //Safety: self.data and self.peer have the same lifetime, so the pointer stays valid
161            DependencyNode::new(
162                (&self.data) as &dyn ModelChangeListener as *const dyn ModelChangeListener,
163            )
164        });
165
166        // Safety: `peer` is pinned because `self` is pinned and it is a projection, but pin_project don't go through the OnceCell
167        let peer = unsafe { Pin::new_unchecked(peer) };
168
169        ModelPeer { inner: peer }
170    }
171
172    pub fn get(self: Pin<&Self>) -> Pin<&T> {
173        self.project_ref().data
174    }
175}
176
177/// A pinned `ModelChangeListenerContainer` using `NonNull` instead of `Box`
178/// to avoid aliasing issues when the struct is moved into `Rc::new()`.
179pub struct ModelChangeListenerBox<T: ModelChangeListener + 'static> {
180    ptr: core::ptr::NonNull<ModelChangeListenerContainer<T>>,
181}
182
183impl<T: ModelChangeListener + 'static> ModelChangeListenerBox<T> {
184    pub fn new(data: T) -> Self {
185        let container = ModelChangeListenerContainer::new(data);
186        // Safety: Box::into_raw returns a non-null pointer
187        let ptr = unsafe { core::ptr::NonNull::new_unchecked(Box::into_raw(Box::new(container))) };
188        Self { ptr }
189    }
190
191    pub fn as_ref(&self) -> Pin<&ModelChangeListenerContainer<T>> {
192        // Safety: the data is pinned because we never move it or expose &mut to it
193        unsafe { Pin::new_unchecked(self.ptr.as_ref()) }
194    }
195}
196
197impl<T: ModelChangeListener + 'static> core::ops::Deref for ModelChangeListenerBox<T> {
198    type Target = T;
199    fn deref(&self) -> &T {
200        // Safety: ptr is valid for the lifetime of self
201        unsafe { &self.ptr.as_ref().data }
202    }
203}
204
205impl<T: ModelChangeListener + 'static> Drop for ModelChangeListenerBox<T> {
206    fn drop(&mut self) {
207        // Safety: we own the allocation and it was created by Box::new.
208        // Box::from_raw runs PinnedDrop which calls peer.remove().
209        unsafe { drop(Box::from_raw(self.ptr.as_ptr())) }
210    }
211}