ferroui_base/collections/
ferro_list_extensions.rs1use 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
11pub struct FerroListExtensions;
13
14impl FerroListExtensions {
15 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 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 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 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
209fn 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
219pub(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 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}