Skip to main content

ferroui_base/media/
media_collection.rs

1//! Shared handles to observable lists, as used by the media object model.
2//!
3//! [`MediaCollection`] is a reference-counted handle to a
4//! [`FerroList`](crate::collections::FerroList) that compares by identity, so
5//! a list can be the value of a property (property values must be
6//! `Clone + PartialEq`). The module also holds the list helpers the media
7//! classes rely on: [`MediaCollection::for_each_item`] and
8//! [`track_item_property_changed`].
9
10use crate::collections::{FerroList, NotifyCollectionChangedAction, NotifyCollectionChangedEventArgs};
11use crate::reactive::{Disposable, IDisposable};
12use crate::{FerroObject, ObjectType, Ref};
13use std::cell::RefCell;
14use std::fmt;
15use std::rc::Rc;
16
17pub use crate::collections::ResetBehavior;
18
19/// A shared, observable list. Cloning the handle shares the list; equality is
20/// by identity.
21pub struct MediaCollection<T: Clone + 'static> {
22    list: Rc<FerroList<T>>,
23}
24
25impl<T: Clone + 'static> Clone for MediaCollection<T> {
26    #[inline]
27    fn clone(&self) -> Self {
28        Self { list: self.list.clone() }
29    }
30}
31
32impl<T: Clone + 'static> PartialEq for MediaCollection<T> {
33    #[inline]
34    fn eq(&self, other: &Self) -> bool {
35        Rc::ptr_eq(&self.list, &other.list)
36    }
37}
38
39impl<T: Clone + 'static> Default for MediaCollection<T> {
40    fn default() -> Self {
41        Self::new()
42    }
43}
44
45impl<T: Clone + fmt::Debug + 'static> fmt::Debug for MediaCollection<T> {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        f.debug_list().entries(self.list.snapshot().iter()).finish()
48    }
49}
50
51impl<T: Clone + 'static> FromIterator<T> for MediaCollection<T> {
52    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
53        Self::from_items(iter)
54    }
55}
56
57impl<T: Clone + 'static> MediaCollection<T> {
58    /// Creates an empty list.
59    pub fn new() -> Self {
60        Self { list: Rc::new(FerroList::new()) }
61    }
62
63    /// Creates a list holding `items`.
64    pub fn from_items(items: impl IntoIterator<Item = T>) -> Self {
65        Self { list: Rc::new(FerroList::from_items(items)) }
66    }
67
68    /// Wraps an existing list.
69    pub fn from_list(list: Rc<FerroList<T>>) -> Self {
70        Self { list }
71    }
72
73    /// The underlying list.
74    #[inline]
75    pub fn list(&self) -> &Rc<FerroList<T>> {
76        &self.list
77    }
78
79    /// Whether both handles refer to the same list.
80    #[inline]
81    pub fn ptr_eq(&self, other: &Self) -> bool {
82        Rc::ptr_eq(&self.list, &other.list)
83    }
84
85    /// The action notified when the list is cleared.
86    #[inline]
87    pub fn reset_behavior(&self) -> ResetBehavior {
88        self.list.reset_behavior()
89    }
90
91    /// Sets the action notified when the list is cleared.
92    #[inline]
93    pub fn set_reset_behavior(&self, value: ResetBehavior) {
94        self.list.set_reset_behavior(value)
95    }
96
97    /// The number of items.
98    #[inline]
99    pub fn len(&self) -> usize {
100        self.list.count()
101    }
102
103    #[inline]
104    pub fn is_empty(&self) -> bool {
105        self.list.is_empty()
106    }
107
108    /// The item at `index`. Panics if the index is out of range.
109    #[inline]
110    pub fn get(&self, index: usize) -> T {
111        self.list.get(index)
112    }
113
114    /// A copy of the items.
115    #[inline]
116    pub fn to_vec(&self) -> Vec<T> {
117        self.list.to_vec()
118    }
119
120    /// Iterates over a snapshot of the items.
121    #[inline]
122    pub fn iter(&self) -> std::vec::IntoIter<T> {
123        self.to_vec().into_iter()
124    }
125
126    /// Appends an item.
127    #[inline]
128    pub fn add(&self, item: T) {
129        self.list.add(item)
130    }
131
132    /// Appends several items with a single notification.
133    #[inline]
134    pub fn add_range(&self, items: impl IntoIterator<Item = T>) {
135        self.list.add_range(items)
136    }
137
138    /// Inserts an item at `index`.
139    #[inline]
140    pub fn insert(&self, index: usize, item: T) {
141        self.list.insert(index, item)
142    }
143
144    /// Removes the item at `index`.
145    #[inline]
146    pub fn remove_at(&self, index: usize) {
147        self.list.remove_at(index)
148    }
149
150    /// Removes all items.
151    #[inline]
152    pub fn clear(&self) {
153        self.list.clear()
154    }
155
156    /// Subscribes to changes of the list. Disposing the returned handle
157    /// unsubscribes.
158    pub fn collection_changed(
159        &self,
160        handler: impl for<'a, 'b> Fn(&'a NotifyCollectionChangedEventArgs<'b, T>) + 'static,
161    ) -> Rc<dyn IDisposable> {
162        let token = self.list.add_collection_changed(Rc::new(handler));
163        let weak = Rc::downgrade(&self.list);
164        Disposable::create(move || {
165            if let Some(list) = weak.upgrade() {
166                list.remove_collection_changed(token);
167            }
168        })
169    }
170
171    /// Invokes `added` for every item currently in the list and for every item
172    /// added later, `removed` for every item removed and `reset` when the list
173    /// is reset.
174    pub fn for_each_item(
175        &self,
176        added: impl Fn(usize, &T) + 'static,
177        removed: impl Fn(usize, &T) + 'static,
178        reset: impl Fn() + 'static,
179    ) -> Rc<dyn IDisposable> {
180        let add = move |index: usize, items: &[T]| {
181            for (i, item) in items.iter().enumerate() {
182                added(index + i, item);
183            }
184        };
185        let remove = move |index: usize, items: &[T]| {
186            for (i, item) in items.iter().enumerate().rev() {
187                removed(index + i, item);
188            }
189        };
190
191        add(0, &self.list.snapshot());
192
193        self.collection_changed(move |e| match e.action {
194            NotifyCollectionChangedAction::Add => add(e.new_starting_index as usize, e.new_items),
195            NotifyCollectionChangedAction::Move => {
196                if e.old_starting_index < 0 {
197                    return reset();
198                }
199
200                remove(e.old_starting_index as usize, e.old_items);
201                let mut new_index = e.new_starting_index;
202
203                if new_index > e.old_starting_index {
204                    new_index -= e.old_items.len() as i32 - 1;
205                }
206
207                add(new_index as usize, e.new_items);
208            }
209            NotifyCollectionChangedAction::Replace => {
210                if e.old_starting_index < 0 {
211                    return reset();
212                }
213
214                remove(e.old_starting_index as usize, e.old_items);
215                add(e.new_starting_index as usize, e.new_items);
216            }
217            NotifyCollectionChangedAction::Remove => remove(e.old_starting_index as usize, e.old_items),
218            NotifyCollectionChangedAction::Reset => reset(),
219        })
220    }
221}
222
223impl<T: Clone + PartialEq + 'static> MediaCollection<T> {
224    /// Replaces the item at `index`.
225    #[inline]
226    pub fn set(&self, index: usize, value: T) {
227        self.list.set(index, value)
228    }
229
230    /// Whether the list contains `item`.
231    #[inline]
232    pub fn contains(&self, item: &T) -> bool {
233        self.list.contains(item)
234    }
235
236    /// The index of the first occurrence of `item`.
237    #[inline]
238    pub fn index_of(&self, item: &T) -> Option<usize> {
239        self.list.index_of(item)
240    }
241
242    /// Removes the first occurrence of `item`; returns whether it was found.
243    #[inline]
244    pub fn remove(&self, item: &T) -> bool {
245        self.list.remove(item)
246    }
247}
248
249/// Invokes `callback` whenever a property changes on any item of the list.
250///
251/// A reset of the list is not supported and panics.
252pub fn track_item_property_changed<T: ObjectType>(
253    collection: &MediaCollection<Ref<T>>,
254    callback: impl Fn() + 'static,
255) -> Rc<dyn IDisposable> {
256    type Tracked = Rc<RefCell<Vec<(Ref<FerroObject>, Rc<dyn IDisposable>)>>>;
257    let tracked: Tracked = Rc::new(RefCell::new(Vec::new()));
258    let callback: Rc<dyn Fn()> = Rc::new(callback);
259
260    let tracked_add = tracked.clone();
261    let tracked_remove = tracked.clone();
262    // The subscription to the list itself is intentionally kept, as in the
263    // reference implementation; disposing only detaches the tracked items.
264    let _ = collection.for_each_item(
265        move |_, item: &Ref<T>| {
266            let callback = callback.clone();
267            let object: &FerroObject = (**item).upcast();
268            let subscription = object.property_changed(move |_| callback());
269            tracked_add.borrow_mut().push((object.to_ref(), subscription));
270        },
271        move |_, item: &Ref<T>| {
272            let object: &FerroObject = (**item).upcast();
273            let object = object.to_ref();
274            let position = tracked_remove.borrow().iter().position(|(o, _)| o.ptr_eq(&object));
275            if let Some(position) = position {
276                let (_, subscription) = tracked_remove.borrow_mut().remove(position);
277                subscription.dispose();
278            }
279        },
280        || panic!("Collection reset not supported."),
281    );
282
283    Disposable::create(move || {
284        let items = std::mem::take(&mut *tracked.borrow_mut());
285        for (_, subscription) in items {
286            subscription.dispose();
287        }
288    })
289}
290
291/// Declares a named list type wrapping a [`MediaCollection`].
292macro_rules! media_collection_type {
293    ($(#[$meta:meta])* $name:ident, $item:ty) => {
294        $(#[$meta])*
295        #[derive(Clone, PartialEq)]
296        pub struct $name($crate::media::MediaCollection<$item>);
297
298        impl ::std::ops::Deref for $name {
299            type Target = $crate::media::MediaCollection<$item>;
300            #[inline]
301            fn deref(&self) -> &Self::Target {
302                &self.0
303            }
304        }
305
306        impl ::std::default::Default for $name {
307            fn default() -> Self {
308                Self::new()
309            }
310        }
311
312        impl ::std::iter::FromIterator<$item> for $name {
313            fn from_iter<I: IntoIterator<Item = $item>>(iter: I) -> Self {
314                Self::from_items(iter)
315            }
316        }
317    };
318}
319
320pub(crate) use media_collection_type;