Skip to main content

ferroui_base/collections/
ferro_list_extensions.rs

1use super::{CollectionChangedHandler, IFerroReadOnlyList, NotifyCollectionChangedAction, NotifyCollectionChangedEventArgs};
2use crate::data::core::ValueTypes;
3use crate::data::model::ModelTypes;
4use crate::reactive::{Disposable, IDisposable};
5use crate::utilities::WeakEventSender;
6use crate::BoxedValue;
7use std::any::Any;
8use std::cell::{Cell, RefCell};
9use std::rc::{Rc, Weak};
10
11/// Defines extension methods for working with [`FerroList`](super::FerroList)s.
12pub struct FerroListExtensions;
13
14impl FerroListExtensions {
15    /// Invokes an action for each item in a collection and subsequently each
16    /// item added or removed from the collection.
17    ///
18    /// `added` is called initially for each item in the collection and
19    /// subsequently for each item added to the collection; `removed` for
20    /// each item removed from the collection; `reset` when the collection is
21    /// reset. `weak_subscription` indicates if a weak subscription should be
22    /// used to track changes to the collection. Returns a disposable used to
23    /// terminate the subscription.
24    pub fn for_each_item<T, L>(
25        collection: &L,
26        added: impl Fn(&T) + 'static,
27        removed: impl Fn(&T) + 'static,
28        reset: impl Fn() + 'static,
29        weak_subscription: bool,
30    ) -> Rc<dyn IDisposable>
31    where
32        T: Clone + 'static,
33        L: IFerroReadOnlyList<T> + WeakEventSender,
34    {
35        Self::for_each_item_indexed(collection, move |_, i| added(i), move |_, i| removed(i), reset, weak_subscription)
36    }
37
38    /// Invokes an action for each item in a collection and subsequently each
39    /// item added or removed from the collection.
40    ///
41    /// `added` is called initially for each item in the collection and
42    /// subsequently for each item added to the collection, with the index in
43    /// the collection and the item; `removed` for each item removed from the
44    /// collection, with the index in the collection and the item; `reset`
45    /// when the collection is reset, which is followed by calls to `added`
46    /// for each item present in the collection after the reset.
47    /// `weak_subscription` indicates if a weak subscription should be used to
48    /// track changes to the collection. Returns a disposable used to
49    /// terminate the subscription.
50    ///
51    /// A weak subscription is held by the returned disposable: the collection
52    /// holds the handler weakly, as the weak collection changed subscription
53    /// of the original does, and the handler goes when the disposable is
54    /// disposed or dropped.
55    pub fn for_each_item_indexed<T, L>(
56        collection: &L,
57        added: impl Fn(usize, &T) + 'static,
58        removed: impl Fn(usize, &T) + 'static,
59        reset: impl Fn() + 'static,
60        weak_subscription: bool,
61    ) -> Rc<dyn IDisposable>
62    where
63        T: Clone + 'static,
64        L: IFerroReadOnlyList<T> + WeakEventSender,
65    {
66        let add = Rc::new(move |index: usize, items: &[T]| {
67            let mut index = index;
68            for item in items {
69                added(index, item);
70                index += 1;
71            }
72        });
73
74        let remove = move |index: usize, items: &[T]| {
75            for i in (0..items.len()).rev() {
76                removed(index + i, &items[i]);
77            }
78        };
79
80        // The handler belongs to the collection, so it reads the collection
81        // through a weak reference.
82        let source = collection.downgrade_sender();
83        let add_items = add.clone();
84        let handler: Rc<CollectionChangedHandler<T>> = Rc::new(move |e: &NotifyCollectionChangedEventArgs<'_, T>| {
85            let reset_collection = || {
86                reset();
87                if let Some(collection) = L::upgrade_sender(&source) {
88                    add_items(0, &collection.snapshot());
89                }
90            };
91
92            match e.action {
93                NotifyCollectionChangedAction::Add => add_items(e.new_starting_index as usize, e.new_items),
94
95                NotifyCollectionChangedAction::Move => {
96                    if e.old_starting_index < 0 {
97                        return reset_collection();
98                    }
99
100                    remove(e.old_starting_index as usize, e.old_items);
101                    let mut new_index = e.new_starting_index;
102
103                    if new_index > e.old_starting_index {
104                        new_index -= e.old_items.len() as i32 - 1;
105                    }
106
107                    add_items(new_index as usize, e.new_items);
108                }
109                NotifyCollectionChangedAction::Replace => {
110                    if e.old_starting_index < 0 {
111                        return reset_collection();
112                    }
113
114                    remove(e.old_starting_index as usize, e.old_items);
115                    add_items(e.new_starting_index as usize, e.new_items);
116                }
117
118                NotifyCollectionChangedAction::Remove => remove(e.old_starting_index as usize, e.old_items),
119
120                NotifyCollectionChangedAction::Reset => reset_collection(),
121            }
122        });
123
124        add(0, &collection.snapshot());
125
126        if weak_subscription {
127            weak_subscribe(collection, handler, L::add_collection_changed, L::remove_collection_changed)
128        } else {
129            let token = collection.add_collection_changed(handler);
130
131            let collection = collection.clone();
132            Disposable::create(move || {
133                collection.remove_collection_changed(token);
134            })
135        }
136    }
137
138    /// Listens for property changed events from all items in a collection.
139    ///
140    /// `callback` is called for each property changed event with the item
141    /// that raised it and the name of the property. An item raises property
142    /// changed events if its model type declares them
143    /// ([`ModelTypes`]); an item that is a shared model object (`Rc<M>` of a
144    /// registered model type `M`, or a boxed value) is the object itself.
145    /// Returns a disposable used to terminate the subscription.
146    ///
147    /// # Panics
148    /// The subscription panics when the collection is reset, as the original
149    /// throws.
150    pub fn track_item_property_changed<T, L>(
151        collection: &L,
152        callback: impl Fn((Option<BoxedValue>, &str)) + 'static,
153    ) -> Rc<dyn IDisposable>
154    where
155        T: Clone + PartialEq + 'static,
156        L: IFerroReadOnlyList<T> + WeakEventSender,
157    {
158        let tracked: Rc<RefCell<Vec<(BoxedValue, u64)>>> = Rc::new(RefCell::new(Vec::new()));
159        let callback = Rc::new(callback);
160
161        let handler = move |inpc: &BoxedValue| -> Rc<dyn Fn(&str)> {
162            let sender: Weak<dyn crate::AnyValue> = Rc::downgrade(inpc);
163            let callback = callback.clone();
164            Rc::new(move |e: &str| callback((sender.upgrade(), e)))
165        };
166
167        let tracked_added = tracked.clone();
168        let tracked_removed = tracked.clone();
169        let _ = Self::for_each_item(
170            collection,
171            move |x: &T| {
172                let inpc = as_object(x);
173
174                if let Some(model) = ModelTypes::find(&*inpc) {
175                    if let Some(notify) = model.as_notify_property_changed(&*inpc) {
176                        let token = notify.property_changed().add(handler(&inpc));
177                        tracked_added.borrow_mut().push((inpc.clone(), token));
178                    }
179                }
180            },
181            move |x: &T| {
182                let inpc = as_object(x);
183
184                if let Some(model) = ModelTypes::find(&*inpc) {
185                    if let Some(notify) = model.as_notify_property_changed(&*inpc) {
186                        let mut tracked = tracked_removed.borrow_mut();
187                        if let Some(index) = tracked.iter().position(|(i, _)| **i == *inpc) {
188                            let (_, token) = tracked.remove(index);
189                            drop(tracked);
190                            notify.property_changed().remove(token);
191                        }
192                    }
193                }
194            },
195            || panic!("Collection reset not supported."),
196            false,
197        );
198
199        Disposable::create(move || {
200            for (i, token) in tracked.borrow().iter() {
201                if let Some(notify) = ModelTypes::find(&**i).and_then(|model| model.as_notify_property_changed(&**i)) {
202                    notify.property_changed().remove(*token);
203                }
204            }
205        })
206    }
207}
208
209/// An item as an object: a boxed item is its content, the handle of a shared
210/// model object is the object.
211fn as_object<T: Clone + PartialEq + 'static>(x: &T) -> BoxedValue {
212    if let Some(boxed) = (x as &dyn Any).downcast_ref::<BoxedValue>() {
213        return boxed.clone();
214    }
215    let boxed: BoxedValue = Rc::new(x.clone());
216    ValueTypes::reference_object(&boxed).unwrap_or(boxed)
217}
218
219/// Subscribes `handler` to a typed collection (through its `add` and
220/// `remove` functions) so that the collection holds it weakly: the returned
221/// disposable holds it, and disposing or dropping the disposable ends the
222/// subscription (a handler that is gone removes its forwarder the next time
223/// the collection changes).
224pub(crate) fn weak_subscribe<I, C>(
225    collection: &C,
226    handler: Rc<CollectionChangedHandler<I>>,
227    add: fn(&C, Rc<CollectionChangedHandler<I>>) -> u64,
228    remove: fn(&C, u64) -> bool,
229) -> Rc<dyn IDisposable>
230where
231    I: 'static,
232    C: WeakEventSender,
233{
234    let weak_handler = Rc::downgrade(&handler);
235    let source = collection.downgrade_sender();
236    let token = Rc::new(Cell::new(None::<u64>));
237    let forwarder_token = token.clone();
238    token.set(Some(add(
239        collection,
240        Rc::new(move |e: &NotifyCollectionChangedEventArgs<'_, I>| match weak_handler.upgrade() {
241            Some(handler) => handler(e),
242            None => {
243                if let (Some(collection), Some(token)) = (C::upgrade_sender(&source), forwarder_token.take()) {
244                    remove(&collection, token);
245                }
246            }
247        }),
248    )));
249
250    let source = collection.downgrade_sender();
251    Disposable::create(move || {
252        if let (Some(collection), Some(token)) = (C::upgrade_sender(&source), token.take()) {
253            remove(&collection, token);
254        }
255        drop(handler);
256    })
257}
258
259#[cfg(test)]
260mod tests {
261    //! Not from upstream: the managed test suite has no tests for these
262    //! extensions.
263
264    use super::*;
265    use crate::collections::FerroList;
266    use crate::data::model::{Event, INotifyPropertyChanged, Model, ModelTypeBuilder};
267
268    fn tracked(list: &FerroList<i32>, weak: bool) -> (Rc<RefCell<Vec<String>>>, Rc<dyn IDisposable>) {
269        let log = Rc::new(RefCell::new(Vec::new()));
270        let (a, r, z) = (log.clone(), log.clone(), log.clone());
271        let subscription = FerroListExtensions::for_each_item_indexed(
272            list,
273            move |index, item: &i32| a.borrow_mut().push(format!("+{index}:{item}")),
274            move |index, item: &i32| r.borrow_mut().push(format!("-{index}:{item}")),
275            move || z.borrow_mut().push("reset".to_string()),
276            weak,
277        );
278        (log, subscription)
279    }
280
281    #[test]
282    fn for_each_item_reports_initial_added_removed_moved_and_reset_items() {
283        let list = FerroList::from_items([1, 2, 3]);
284        let (log, subscription) = tracked(&list, false);
285
286        list.add(4);
287        list.remove_at(0);
288        list.move_item(0, 2);
289        list.clear();
290        list.add(5);
291        subscription.dispose();
292        list.add(6);
293
294        assert_eq!(
295            *log.borrow(),
296            ["+0:1", "+1:2", "+2:3", "+3:4", "-0:1", "-0:2", "+2:2", "reset", "+0:5"].map(String::from).to_vec()
297        );
298    }
299
300    #[test]
301    fn weak_for_each_item_ends_when_the_subscription_is_dropped() {
302        let list = FerroList::from_items([1]);
303        let (log, subscription) = tracked(&list, true);
304
305        list.add(2);
306        drop(subscription);
307        list.add(3);
308
309        assert_eq!(*log.borrow(), ["+0:1", "+1:2"].map(String::from).to_vec());
310        assert!(!list.has_collection_changed_subscribers());
311    }
312
313    struct Item {
314        property_changed: Event<str>,
315    }
316
317    impl PartialEq for Item {
318        fn eq(&self, other: &Self) -> bool {
319            std::ptr::eq(self, other)
320        }
321    }
322
323    impl INotifyPropertyChanged for Item {
324        fn property_changed(&self) -> &Event<str> {
325            &self.property_changed
326        }
327    }
328
329    impl Model for Item {
330        fn describe(builder: ModelTypeBuilder<Self>) -> ModelTypeBuilder<Self> {
331            builder.notify_property_changed()
332        }
333    }
334
335    #[test]
336    fn track_item_property_changed_reports_changes_of_tracked_items() {
337        let first = Item::new_model(Item { property_changed: Event::new() });
338        let second = Item::new_model(Item { property_changed: Event::new() });
339        let list = FerroList::from_items([first.clone()]);
340        let log = Rc::new(RefCell::new(Vec::new()));
341        let l = log.clone();
342
343        let subscription = FerroListExtensions::track_item_property_changed(&list, move |(sender, name)| {
344            let is_first = sender.is_some_and(|s| s.downcast_ref::<Item>().is_some_and(|s| std::ptr::eq(s, &*first)));
345            l.borrow_mut().push((is_first, name.to_string()));
346        });
347        list.add(second.clone());
348        list.get(0).property_changed.raise("Foo");
349        second.property_changed.raise("Bar");
350        list.remove(&second);
351        second.property_changed.raise("Baz");
352        subscription.dispose();
353        list.get(0).property_changed.raise("Qux");
354
355        assert_eq!(*log.borrow(), vec![(true, "Foo".to_string()), (false, "Bar".to_string())]);
356    }
357}