Skip to main content

ferroui_controls/
items_source.rs

1//! The untyped collection contract of the items controls.
2//!
3//! The managed framework types `ItemsSource`, selection sources and the
4//! like as "any enumerable" and discovers at run time whether the object is
5//! an indexable list and whether it notifies of changes. Here that contract
6//! is explicit:
7//!
8//! - an item is an `Option<BoxedValue>` (an untyped, nullable value);
9//! - [`IItemsList`] is the untyped, indexable, optionally notifying list;
10//! - [`ItemsSource`] is the shared handle stored in properties. It compares
11//!   by the identity of the collection, so assigning the same collection
12//!   twice is not a change.
13//!
14//! [`IItemsList`] is implemented for the collection abstractions of the
15//! port, so each of them converts into an [`ItemsSource`] without copying:
16//!
17//! | Collection | Notifying | Items |
18//! |---|---|---|
19//! | `Rc<FerroList<T>>` | yes | each `T` boxed on access (no allocation when `T` is `BoxedValue` or `Option<BoxedValue>`) |
20//! | `Rc<BindableList<T>>` | yes | as its inner `FerroList<T>` |
21//! | `Vec<Option<BoxedValue>>`, `Vec<BoxedValue>` | no | as stored |
22//! | any iterator of property values ([`ItemsSource::from_values`]) | no | boxed once, up front |
23//!
24//! A source that only enumerates is copied into a list when it is assigned,
25//! exactly as the managed implementation copies a non-list enumerable.
26//!
27//! # Change notifications
28//!
29//! A notifying list reports changes with [`ItemsChangedEventArgs`], the
30//! untyped counterpart of the typed `NotifyCollectionChangedEventArgs` of
31//! `FerroList<T>`. The managed framework hands the changed list itself to
32//! the handlers, which mostly ask it for its count only. Likewise
33//! `new_items` and `old_items` here are an [`ItemsView`]: a `Copy` view,
34//! two or three words wide, whose `len()` never reads an item and whose
35//! `get(i)`/`iter()`/`to_vec()` read exactly the items asked for. A view is
36//! one of:
37//!
38//! - a borrowed slice of untyped items ([`ItemsView::from_slice`]);
39//! - a window of a list, read through [`IItemsList::get_at`] on demand
40//!   ([`ItemsView::window`], [`ItemsView::of_source`]): replacing the source
41//!   of an items control notifies with windows over the old and the new
42//!   source, so a data-virtualised source is never enumerated;
43//! - the typed items of a `FerroList<T>` change ([`ItemsView::from_typed`]),
44//!   boxed one by one when read: adapting a typed notification allocates
45//!   nothing, whatever the number of items.
46//!
47//! Handlers on virtualizing paths use the counts only. A handler that keeps
48//! items beyond the notification copies them with `to_vec()`.
49//!
50//! Controls are items too: a control item is boxed as exactly `Ref<Control>`
51//! (see `Control::boxed`).
52//!
53//! Bindings deliver untyped values of the view model's property type. To
54//! bind a view model property holding one of the collection types above to
55//! an items source property, register the conversion once with
56//! [`ItemsSource::register_binding_conversion`].
57
58use ferroui_base::collections::{FerroList, NotifyCollectionChangedAction, NotifyCollectionChangedEventArgs};
59use ferroui_base::data::core::ValueTypes;
60use ferroui_base::data::model::BindableList;
61use ferroui_base::{AnyValue, BoxedValue, PropertyValue};
62use std::any::Any;
63use std::rc::Rc;
64
65/// The items carried by a change notification of an untyped items list: a
66/// cheap, copyable view that produces an item only when asked for it.
67///
68/// See the module documentation ("Change notifications").
69#[derive(Clone, Copy)]
70pub struct ItemsView<'a>(ItemsViewSource<'a>);
71
72#[derive(Clone, Copy)]
73enum ItemsViewSource<'a> {
74    /// Items that already exist as untyped values.
75    Slice(&'a [Option<BoxedValue>]),
76    /// `count` items of `list` starting at `start`, read on demand.
77    Window { list: &'a dyn IItemsList, start: usize, count: usize },
78    /// The items of a typed collection, boxed on demand.
79    Typed(&'a dyn TypedItemsSource),
80}
81
82/// The typed items of a change, viewed as untyped items by
83/// [`ItemsView::from_typed`]. Each item is boxed when it is read (no
84/// allocation when `T` is `BoxedValue` or `Option<BoxedValue>`).
85pub struct TypedItems<'a, T>(pub &'a [T]);
86
87trait TypedItemsSource {
88    fn len(&self) -> usize;
89    fn get(&self, index: usize) -> Option<BoxedValue>;
90}
91
92impl<T: PropertyValue> TypedItemsSource for TypedItems<'_, T> {
93    #[inline]
94    fn len(&self) -> usize {
95        self.0.len()
96    }
97
98    #[inline]
99    fn get(&self, index: usize) -> Option<BoxedValue> {
100        box_item(&self.0[index])
101    }
102}
103
104impl<'a> ItemsView<'a> {
105    /// A view of no items.
106    pub const EMPTY: ItemsView<'static> = ItemsView(ItemsViewSource::Slice(&[]));
107
108    /// A view of existing untyped items.
109    #[inline]
110    pub const fn from_slice(items: &'a [Option<BoxedValue>]) -> Self {
111        Self(ItemsViewSource::Slice(items))
112    }
113
114    /// A view of `count` items of `list` starting at `start`. Nothing is
115    /// read from the list until an item is asked for.
116    #[inline]
117    pub fn window(list: &'a dyn IItemsList, start: usize, count: usize) -> Self {
118        Self(ItemsViewSource::Window { list, start, count })
119    }
120
121    /// A view of all the items of `source`. Only the count is read.
122    #[inline]
123    pub fn of_source(source: &'a ItemsSource) -> Self {
124        Self::window(&**source.list(), 0, source.count())
125    }
126
127    /// A view of `count` items of `source` starting at `start`.
128    #[inline]
129    pub fn of_source_range(source: &'a ItemsSource, start: usize, count: usize) -> Self {
130        Self::window(&**source.list(), start, count)
131    }
132
133    /// A view of typed items, each boxed when it is read.
134    #[inline]
135    pub fn from_typed<T: PropertyValue>(items: &'a TypedItems<'a, T>) -> Self {
136        Self(ItemsViewSource::Typed(items))
137    }
138
139    /// The number of items. Never reads an item.
140    #[inline]
141    pub fn len(&self) -> usize {
142        match self.0 {
143            ItemsViewSource::Slice(items) => items.len(),
144            ItemsViewSource::Window { count, .. } => count,
145            ItemsViewSource::Typed(items) => items.len(),
146        }
147    }
148
149    /// Whether the view has no items. Never reads an item.
150    #[inline]
151    pub fn is_empty(&self) -> bool {
152        self.len() == 0
153    }
154
155    /// The item at `index`. Panics if out of range.
156    #[inline]
157    pub fn get(&self, index: usize) -> Option<BoxedValue> {
158        match self.0 {
159            ItemsViewSource::Slice(items) => items[index].clone(),
160            ItemsViewSource::Window { list, start, count } => {
161                assert!(index < count, "index out of range");
162                list.get_at(start + index)
163            }
164            ItemsViewSource::Typed(items) => items.get(index),
165        }
166    }
167
168    /// The items, each read when the iterator reaches it.
169    #[inline]
170    pub fn iter(&self) -> ItemsViewIter<'a> {
171        ItemsViewIter { view: *self, index: 0, end: self.len() }
172    }
173
174    /// A copy of the items. Reads every item.
175    pub fn to_vec(&self) -> Vec<Option<BoxedValue>> {
176        match self.0 {
177            ItemsViewSource::Slice(items) => items.to_vec(),
178            _ => self.iter().collect(),
179        }
180    }
181}
182
183impl Default for ItemsView<'_> {
184    fn default() -> Self {
185        ItemsView::EMPTY
186    }
187}
188
189impl std::fmt::Debug for ItemsView<'_> {
190    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191        write!(f, "ItemsView(len = {})", self.len())
192    }
193}
194
195impl<'a> From<&'a [Option<BoxedValue>]> for ItemsView<'a> {
196    #[inline]
197    fn from(items: &'a [Option<BoxedValue>]) -> Self {
198        Self::from_slice(items)
199    }
200}
201
202impl<'a> From<&'a Vec<Option<BoxedValue>>> for ItemsView<'a> {
203    #[inline]
204    fn from(items: &'a Vec<Option<BoxedValue>>) -> Self {
205        Self::from_slice(items)
206    }
207}
208
209impl<'a, const N: usize> From<&'a [Option<BoxedValue>; N]> for ItemsView<'a> {
210    #[inline]
211    fn from(items: &'a [Option<BoxedValue>; N]) -> Self {
212        Self::from_slice(items)
213    }
214}
215
216impl<'a> From<&'a ItemsSource> for ItemsView<'a> {
217    #[inline]
218    fn from(source: &'a ItemsSource) -> Self {
219        Self::of_source(source)
220    }
221}
222
223/// The iterator of an [`ItemsView`].
224#[derive(Clone)]
225pub struct ItemsViewIter<'a> {
226    view: ItemsView<'a>,
227    index: usize,
228    end: usize,
229}
230
231impl Iterator for ItemsViewIter<'_> {
232    type Item = Option<BoxedValue>;
233
234    #[inline]
235    fn next(&mut self) -> Option<Self::Item> {
236        if self.index < self.end {
237            self.index += 1;
238            Some(self.view.get(self.index - 1))
239        } else {
240            None
241        }
242    }
243
244    #[inline]
245    fn size_hint(&self) -> (usize, Option<usize>) {
246        let remaining = self.end - self.index;
247        (remaining, Some(remaining))
248    }
249}
250
251impl DoubleEndedIterator for ItemsViewIter<'_> {
252    #[inline]
253    fn next_back(&mut self) -> Option<Self::Item> {
254        if self.index < self.end {
255            self.end -= 1;
256            Some(self.view.get(self.end))
257        } else {
258            None
259        }
260    }
261}
262
263impl ExactSizeIterator for ItemsViewIter<'_> {}
264
265impl<'a> IntoIterator for ItemsView<'a> {
266    type Item = Option<BoxedValue>;
267    type IntoIter = ItemsViewIter<'a>;
268
269    #[inline]
270    fn into_iter(self) -> ItemsViewIter<'a> {
271        self.iter()
272    }
273}
274
275impl<'a> IntoIterator for &ItemsView<'a> {
276    type Item = Option<BoxedValue>;
277    type IntoIter = ItemsViewIter<'a>;
278
279    #[inline]
280    fn into_iter(self) -> ItemsViewIter<'a> {
281        self.iter()
282    }
283}
284
285/// A change to an untyped items list.
286#[derive(Clone, Copy, Debug)]
287pub struct ItemsChangedEventArgs<'a> {
288    /// The action that caused the notification.
289    pub action: NotifyCollectionChangedAction,
290    /// The items involved in the change (added, or the replacements).
291    pub new_items: ItemsView<'a>,
292    /// The items affected by a replace, remove or move.
293    pub old_items: ItemsView<'a>,
294    /// The index at which the change occurred, or `-1`.
295    pub new_starting_index: i32,
296    /// The index at which a move, remove or replace occurred, or `-1`.
297    pub old_starting_index: i32,
298}
299
300impl ItemsChangedEventArgs<'static> {
301    /// The arguments of a reset notification.
302    pub const RESET: ItemsChangedEventArgs<'static> = ItemsChangedEventArgs {
303        action: NotifyCollectionChangedAction::Reset,
304        new_items: ItemsView::EMPTY,
305        old_items: ItemsView::EMPTY,
306        new_starting_index: -1,
307        old_starting_index: -1,
308    };
309}
310
311impl<'a> ItemsChangedEventArgs<'a> {
312    /// The arguments of an add of `new_items` at `new_starting_index`.
313    #[inline]
314    pub fn add(new_items: impl Into<ItemsView<'a>>, new_starting_index: i32) -> Self {
315        Self {
316            action: NotifyCollectionChangedAction::Add,
317            new_items: new_items.into(),
318            old_items: ItemsView::EMPTY,
319            new_starting_index,
320            old_starting_index: -1,
321        }
322    }
323
324    /// The arguments of a remove of `old_items` from `old_starting_index`.
325    #[inline]
326    pub fn remove(old_items: impl Into<ItemsView<'a>>, old_starting_index: i32) -> Self {
327        Self {
328            action: NotifyCollectionChangedAction::Remove,
329            new_items: ItemsView::EMPTY,
330            old_items: old_items.into(),
331            new_starting_index: -1,
332            old_starting_index,
333        }
334    }
335}
336
337/// The change of a list of untyped items, as it is: the items are viewed in
338/// place.
339impl<'a> From<&NotifyCollectionChangedEventArgs<'a, Option<BoxedValue>>> for ItemsChangedEventArgs<'a> {
340    #[inline]
341    fn from(e: &NotifyCollectionChangedEventArgs<'a, Option<BoxedValue>>) -> Self {
342        Self {
343            action: e.action,
344            new_items: ItemsView::from_slice(e.new_items),
345            old_items: ItemsView::from_slice(e.old_items),
346            new_starting_index: e.new_starting_index,
347            old_starting_index: e.old_starting_index,
348        }
349    }
350}
351
352/// The signature of a handler of changes to an untyped items list.
353pub type ItemsChangedHandler = dyn for<'a> Fn(&ItemsChangedEventArgs<'a>);
354
355/// Compares two items: both null, or both of the same type and equal.
356#[inline]
357pub fn items_equal(a: &Option<BoxedValue>, b: &Option<BoxedValue>) -> bool {
358    match (a, b) {
359        (None, None) => true,
360        (Some(a), Some(b)) => Rc::ptr_eq(a, b) || (**a).any_value_eq(&**b),
361        _ => false,
362    }
363}
364
365/// Whether two untyped values are the same object: C# `ReferenceEquals` (and
366/// `==` / `!=` on `object`) for values that travel in boxes.
367///
368/// The rule:
369/// * both null, or the same box: the same reference;
370/// * boxes that hold handles of the same object of the class hierarchy (a
371///   control boxed twice, or boxed once as `Ref<Control>` and once as
372///   `Ref<Border>`): the same reference;
373/// * boxes that both hold a string: the same reference if the strings are
374///   equal. A string is the immutable value-like type that the lists of the
375///   managed original hand out as the same instance on every read, whereas a
376///   typed list of strings here boxes each string anew on every read; without
377///   this, an unchanged item read twice would look like another object;
378/// * anything else in two boxes (two separately boxed equal numbers, two
379///   equal structures) is two references, as two boxings are in the managed
380///   original.
381///
382/// Use [`items_equal`] where the original calls `Equals`.
383pub fn reference_equals(a: &Option<BoxedValue>, b: &Option<BoxedValue>) -> bool {
384    match (a, b) {
385        (None, None) => true,
386        (Some(a), Some(b)) => boxed_reference_equals(a, b),
387        _ => false,
388    }
389}
390
391/// [`reference_equals`] for two values that are not null.
392pub fn boxed_reference_equals(a: &BoxedValue, b: &BoxedValue) -> bool {
393    // The addresses of the boxes; the comparison of the handles themselves
394    // would also compare their metadata.
395    if std::ptr::addr_eq(Rc::as_ptr(a), Rc::as_ptr(b)) {
396        return true;
397    }
398
399    fn as_str(value: &dyn AnyValue) -> Option<&str> {
400        if let Some(text) = value.downcast_ref::<String>() {
401            return Some(text.as_str());
402        }
403        value.downcast_ref::<&'static str>().copied()
404    }
405
406    let (a, b): (&dyn AnyValue, &dyn AnyValue) = (&**a, &**b);
407    if let (Some(a), Some(b)) = (as_str(a), as_str(b)) {
408        return a == b;
409    }
410
411    match (ValueTypes::as_object(a), ValueTypes::as_object(b)) {
412        (Some(a), Some(b)) => a.ptr_eq(&b),
413        _ => false,
414    }
415}
416
417/// Boxes a typed value as an item. A value that already is an untyped value
418/// is passed through.
419#[inline]
420pub fn box_item<T: PropertyValue>(value: &T) -> Option<BoxedValue> {
421    let any: &dyn Any = value;
422    if let Some(boxed) = any.downcast_ref::<BoxedValue>() {
423        return Some(boxed.clone());
424    }
425    if let Some(boxed) = any.downcast_ref::<Option<BoxedValue>>() {
426        return boxed.clone();
427    }
428    Some(Rc::new(value.clone()))
429}
430
431/// The typed value held by an item: the item itself when `T` is the untyped
432/// value type, otherwise the contained `T`. `None` for a null item or an
433/// item of another type.
434#[inline]
435pub fn unbox_item<T: PropertyValue>(item: &Option<BoxedValue>) -> Option<T> {
436    let boxed = item.as_ref()?;
437    let any: &dyn Any = boxed;
438    if let Some(value) = any.downcast_ref::<T>() {
439        // `T` is `BoxedValue`.
440        return Some(value.clone());
441    }
442    let value: &dyn AnyValue = &**boxed;
443    value.downcast_ref::<T>().cloned()
444}
445
446/// Whether an item holds a value equal to `value`, without boxing `value`.
447#[inline]
448fn item_equals_typed<T: PropertyValue>(item: &Option<BoxedValue>, value: &T) -> bool {
449    let any: &dyn Any = value;
450    if let Some(boxed) = any.downcast_ref::<BoxedValue>() {
451        return item.as_ref().is_some_and(|item| Rc::ptr_eq(item, boxed) || (**item).any_value_eq(&**boxed));
452    }
453    if let Some(boxed) = any.downcast_ref::<Option<BoxedValue>>() {
454        return items_equal(item, boxed);
455    }
456    match item {
457        Some(item) => {
458            let item: &dyn AnyValue = &**item;
459            item.downcast_ref::<T>().is_some_and(|item| item == value)
460        }
461        None => false,
462    }
463}
464
465/// An untyped, indexable list of items that may notify of changes.
466pub trait IItemsList {
467    /// The number of items.
468    fn count(&self) -> usize;
469
470    /// The item at `index`. Panics if out of range.
471    fn get_at(&self, index: usize) -> Option<BoxedValue>;
472
473    /// The index of the first item equal to `item`, or `-1`.
474    fn index_of(&self, item: &Option<BoxedValue>) -> i32 {
475        for i in 0..self.count() {
476            if items_equal(&self.get_at(i), item) {
477                return i as i32;
478            }
479        }
480        -1
481    }
482
483    /// Whether the list notifies of changes.
484    fn is_notifying(&self) -> bool {
485        false
486    }
487
488    /// Subscribes to changes. Returns the token for
489    /// [`remove_collection_changed`](Self::remove_collection_changed), or
490    /// `None` if the list does not notify.
491    fn add_collection_changed(&self, _handler: Rc<ItemsChangedHandler>) -> Option<u64> {
492        None
493    }
494
495    /// Unsubscribes a handler.
496    fn remove_collection_changed(&self, _token: u64) {}
497
498    /// The identity of the collection, for a list that is a handle to a collection
499    /// (a [`FerroList`]): two handles to one collection are the same collection. `None`
500    /// for a list that is the collection itself, whose identity is its address.
501    fn identity(&self) -> Option<usize> {
502        None
503    }
504
505    /// The collection as [`Any`], to get back to the typed collection.
506    fn as_any(&self) -> &dyn Any;
507}
508
509impl IItemsList for Vec<Option<BoxedValue>> {
510    fn count(&self) -> usize {
511        self.len()
512    }
513
514    fn get_at(&self, index: usize) -> Option<BoxedValue> {
515        self[index].clone()
516    }
517
518    fn index_of(&self, item: &Option<BoxedValue>) -> i32 {
519        self.iter().position(|i| items_equal(i, item)).map_or(-1, |i| i as i32)
520    }
521
522    fn as_any(&self) -> &dyn Any {
523        self
524    }
525}
526
527impl<T: PropertyValue> IItemsList for FerroList<T> {
528    fn count(&self) -> usize {
529        FerroList::count(self)
530    }
531
532    fn get_at(&self, index: usize) -> Option<BoxedValue> {
533        let snapshot = self.snapshot();
534        box_item(&snapshot[index])
535    }
536
537    fn index_of(&self, item: &Option<BoxedValue>) -> i32 {
538        let snapshot = self.snapshot();
539        if let Some(index) = snapshot.iter().position(|i| item_equals_typed(item, i)) {
540            return index as i32;
541        }
542        // The item in another form than the list holds it in: a shared object of a list of
543        // handles (`Rc<T>`) is delivered by a binding as the object itself. Upstream's
544        // `IList.IndexOf(object)` finds the reference whatever the static type of the list.
545        let is_typed = item.as_ref().is_some_and(|boxed| (**boxed).downcast_ref::<T>().is_some());
546        if !is_typed {
547            if let Some(typed) = ferroui_base::metadata::from_markup_value::<T>(item) {
548                return snapshot.iter().position(|i| *i == typed).map_or(-1, |i| i as i32);
549            }
550        }
551        -1
552    }
553
554    fn is_notifying(&self) -> bool {
555        true
556    }
557
558    fn add_collection_changed(&self, handler: Rc<ItemsChangedHandler>) -> Option<u64> {
559        Some(FerroList::add_collection_changed(
560            self,
561            Rc::new(move |e: &NotifyCollectionChangedEventArgs<'_, T>| {
562                // Nothing is boxed or copied here: the handler boxes the
563                // items it reads.
564                let new_items = TypedItems(e.new_items);
565                let old_items = TypedItems(e.old_items);
566                handler(&ItemsChangedEventArgs {
567                    action: e.action,
568                    new_items: ItemsView::from_typed(&new_items),
569                    old_items: ItemsView::from_typed(&old_items),
570                    new_starting_index: e.new_starting_index,
571                    old_starting_index: e.old_starting_index,
572                });
573            }),
574        ))
575    }
576
577    fn remove_collection_changed(&self, token: u64) {
578        FerroList::remove_collection_changed(self, token);
579    }
580
581    fn identity(&self) -> Option<usize> {
582        Some(ferroui_base::utilities::WeakEventSender::sender_address(self))
583    }
584
585    fn as_any(&self) -> &dyn Any {
586        self
587    }
588}
589
590impl<T: PropertyValue> IItemsList for BindableList<T> {
591    fn count(&self) -> usize {
592        self.items().count()
593    }
594
595    fn get_at(&self, index: usize) -> Option<BoxedValue> {
596        IItemsList::get_at(self.items(), index)
597    }
598
599    fn index_of(&self, item: &Option<BoxedValue>) -> i32 {
600        IItemsList::index_of(self.items(), item)
601    }
602
603    fn is_notifying(&self) -> bool {
604        true
605    }
606
607    fn add_collection_changed(&self, handler: Rc<ItemsChangedHandler>) -> Option<u64> {
608        IItemsList::add_collection_changed(self.items(), handler)
609    }
610
611    fn remove_collection_changed(&self, token: u64) {
612        IItemsList::remove_collection_changed(self.items(), token);
613    }
614
615    fn as_any(&self) -> &dyn Any {
616        self
617    }
618}
619
620/// The `ObservableCollection<T>` of `ferroui-system` as the items of an items control, as an
621/// `ObservableCollection<T>` is an `IList` with `INotifyCollectionChanged` that an items control
622/// of .NET reads directly: the collections that the reactive libraries fill (DynamicData's
623/// `ObservableCollectionExtended::collection()`, ReactiveUI's routing stack) are items sources.
624impl<T: PropertyValue> IItemsList for ferroui_system::collections::object_model::ObservableCollection<T> {
625    fn count(&self) -> usize {
626        ferroui_system::collections::object_model::ObservableCollection::count(self)
627    }
628
629    fn get_at(&self, index: usize) -> Option<BoxedValue> {
630        box_item(&self.get(index))
631    }
632
633    fn index_of(&self, item: &Option<BoxedValue>) -> i32 {
634        (0..ferroui_system::collections::object_model::ObservableCollection::count(self))
635            .position(|index| item_equals_typed(item, &self.get(index)))
636            .map_or(-1, |index| index as i32)
637    }
638
639    fn is_notifying(&self) -> bool {
640        true
641    }
642
643    fn add_collection_changed(&self, handler: Rc<ItemsChangedHandler>) -> Option<u64> {
644        Some(ferroui_system::collections::object_model::ObservableCollection::add_collection_changed(
645            self,
646            Rc::new(move |e: &ferroui_system::collections::specialized::NotifyCollectionChangedEventArgs<'_, T>| {
647                let new_items = TypedItems(e.new_items);
648                let old_items = TypedItems(e.old_items);
649                handler(&ItemsChangedEventArgs {
650                    action: e.action,
651                    new_items: ItemsView::from_typed(&new_items),
652                    old_items: ItemsView::from_typed(&old_items),
653                    new_starting_index: e.new_starting_index,
654                    old_starting_index: e.old_starting_index,
655                });
656            }),
657        ))
658    }
659
660    fn remove_collection_changed(&self, token: u64) {
661        let _ = ferroui_system::collections::object_model::ObservableCollection::remove_collection_changed(self, token);
662    }
663
664    fn as_any(&self) -> &dyn Any {
665        self
666    }
667}
668
669/// A shared handle to an untyped items list: the value of an items source
670/// property.
671///
672/// Equality is the identity of the underlying collection.
673#[derive(Clone)]
674pub struct ItemsSource(Rc<dyn IItemsList>);
675
676impl ItemsSource {
677    /// Wraps a list.
678    pub fn new(list: Rc<dyn IItemsList>) -> Self {
679        Self(list)
680    }
681
682    /// Creates a non-notifying source holding the given items.
683    pub fn from_items(items: impl IntoIterator<Item = Option<BoxedValue>>) -> Self {
684        Self(Rc::new(items.into_iter().collect::<Vec<_>>()))
685    }
686
687    /// Creates a non-notifying source by boxing each of the given values.
688    pub fn from_values<T: PropertyValue>(values: impl IntoIterator<Item = T>) -> Self {
689        Self(Rc::new(values.into_iter().map(|v| box_item(&v)).collect::<Vec<_>>()))
690    }
691
692    /// Creates a non-notifying source of strings.
693    pub fn from_strs<'a>(values: impl IntoIterator<Item = &'a str>) -> Self {
694        Self::from_values(values.into_iter().map(str::to_string))
695    }
696
697    /// The list.
698    #[inline]
699    pub fn list(&self) -> &Rc<dyn IItemsList> {
700        &self.0
701    }
702
703    /// The number of items.
704    #[inline]
705    pub fn count(&self) -> usize {
706        self.0.count()
707    }
708
709    /// The item at `index`. Panics if out of range.
710    #[inline]
711    pub fn get_at(&self, index: usize) -> Option<BoxedValue> {
712        self.0.get_at(index)
713    }
714
715    /// The index of the first item equal to `item`, or `-1`.
716    #[inline]
717    pub fn index_of(&self, item: &Option<BoxedValue>) -> i32 {
718        self.0.index_of(item)
719    }
720
721    /// Whether the list contains an item equal to `item`.
722    #[inline]
723    pub fn contains(&self, item: &Option<BoxedValue>) -> bool {
724        self.0.index_of(item) >= 0
725    }
726
727    /// Whether the list notifies of changes.
728    #[inline]
729    pub fn is_notifying(&self) -> bool {
730        self.0.is_notifying()
731    }
732
733    /// The typed collection behind the source, if it is a `T`.
734    pub fn downcast_ref<T: 'static>(&self) -> Option<&T> {
735        self.0.as_any().downcast_ref::<T>()
736    }
737
738    /// The identity of the underlying collection.
739    #[inline]
740    pub fn identity(&self) -> usize {
741        self.0.identity().unwrap_or(Rc::as_ptr(&self.0) as *const () as usize)
742    }
743
744    /// Whether two handles refer to the same collection.
745    #[inline]
746    pub fn ptr_eq(&self, other: &ItemsSource) -> bool {
747        self.identity() == other.identity()
748    }
749
750    /// The items, enumerated by index over the live list.
751    pub fn iter(&self) -> impl Iterator<Item = Option<BoxedValue>> + '_ {
752        let mut index = 0;
753        std::iter::from_fn(move || {
754            if index < self.0.count() {
755                index += 1;
756                Some(self.0.get_at(index - 1))
757            } else {
758                None
759            }
760        })
761    }
762
763    /// A copy of the items.
764    pub fn to_vec(&self) -> Vec<Option<BoxedValue>> {
765        self.iter().collect()
766    }
767
768    /// Lets bindings deliver a value of the collection type `S` to a
769    /// property of type `Option<ItemsSource>` (or `ItemsSource`) on the
770    /// current thread.
771    /// [`register_binding_conversion`](Self::register_binding_conversion) for a
772    /// collection type that converts with `convert` (a list handle that is delivered as
773    /// the shared list).
774    pub fn register_binding_conversion_with<S: 'static>(convert: fn(&S) -> ItemsSource) {
775        ValueTypes::register_conversion::<S, ItemsSource>(move |s| Some(convert(s)));
776        ValueTypes::register_conversion::<S, Option<ItemsSource>>(move |s| Some(Some(convert(s))));
777    }
778
779    pub fn register_binding_conversion<S: Clone + Into<ItemsSource> + 'static>() {
780        ValueTypes::register_conversion::<S, ItemsSource>(|s| Some(s.clone().into()));
781        ValueTypes::register_conversion::<S, Option<ItemsSource>>(|s| Some(Some(s.clone().into())));
782    }
783}
784
785impl PartialEq for ItemsSource {
786    fn eq(&self, other: &Self) -> bool {
787        self.ptr_eq(other)
788    }
789}
790
791impl std::fmt::Debug for ItemsSource {
792    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
793        write!(f, "ItemsSource(count = {})", self.count())
794    }
795}
796
797impl From<Rc<dyn IItemsList>> for ItemsSource {
798    fn from(value: Rc<dyn IItemsList>) -> Self {
799        Self(value)
800    }
801}
802
803impl<T: PropertyValue> From<Rc<FerroList<T>>> for ItemsSource {
804    fn from(value: Rc<FerroList<T>>) -> Self {
805        Self(value)
806    }
807}
808
809impl<T: PropertyValue> From<Rc<BindableList<T>>> for ItemsSource {
810    fn from(value: Rc<BindableList<T>>) -> Self {
811        Self(value)
812    }
813}
814
815impl From<Vec<Option<BoxedValue>>> for ItemsSource {
816    fn from(value: Vec<Option<BoxedValue>>) -> Self {
817        Self(Rc::new(value))
818    }
819}
820
821impl From<Vec<BoxedValue>> for ItemsSource {
822    fn from(value: Vec<BoxedValue>) -> Self {
823        Self::from_items(value.into_iter().map(Some))
824    }
825}
826
827impl From<Rc<Vec<Option<BoxedValue>>>> for ItemsSource {
828    fn from(value: Rc<Vec<Option<BoxedValue>>>) -> Self {
829        Self(value)
830    }
831}
832
833#[cfg(test)]
834mod reference_equals_tests {
835    use super::{boxed_reference_equals, items_equal, reference_equals};
836    use crate::{Border, Control};
837    use ferroui_base::{BoxedValue, Ref, StyledElement};
838    use std::rc::Rc;
839
840    fn boxed<T: PartialEq + 'static>(value: T) -> Option<BoxedValue> {
841        Some(Rc::new(value))
842    }
843
844    #[test]
845    fn nulls_and_the_same_box_are_the_same_reference() {
846        let value = boxed(42_i32);
847
848        assert!(reference_equals(&None, &None));
849        assert!(reference_equals(&value, &value.clone()));
850        assert!(!reference_equals(&value, &None));
851        assert!(!reference_equals(&None, &value));
852    }
853
854    #[test]
855    fn equal_strings_in_different_boxes_are_the_same_reference() {
856        assert!(reference_equals(&boxed("able".to_string()), &boxed("able".to_string())));
857        assert!(reference_equals(&boxed("able".to_string()), &boxed("able")));
858        assert!(reference_equals(&boxed(String::new()), &boxed(String::new())));
859        assert!(!reference_equals(&boxed("able".to_string()), &boxed("abide".to_string())));
860        assert!(!reference_equals(&boxed("able".to_string()), &boxed("Able".to_string())));
861        assert!(!reference_equals(&boxed("1".to_string()), &boxed(1_i32)));
862    }
863
864    #[test]
865    fn equal_values_in_different_boxes_are_different_references() {
866        assert!(!reference_equals(&boxed(42_i32), &boxed(42_i32)));
867        assert!(!reference_equals(&boxed(1.5_f64), &boxed(1.5_f64)));
868        assert!(!reference_equals(&boxed(true), &boxed(true)));
869        // They are equal values all the same.
870        assert!(items_equal(&boxed(42_i32), &boxed(42_i32)));
871    }
872
873    #[test]
874    fn the_same_control_in_different_boxes_is_the_same_reference() {
875        let border = Border::new();
876        let as_control: Ref<Control> = border.clone().upcast();
877        let as_element: Ref<StyledElement> = border.clone().upcast();
878
879        assert!(reference_equals(&Some(Control::boxed(border.clone())), &Some(Control::boxed(border.clone()))));
880        // Whatever the type of the handle in the box is.
881        assert!(reference_equals(&boxed(border.clone()), &boxed(as_control)));
882        assert!(reference_equals(&boxed(as_element), &boxed(border.clone())));
883        assert!(boxed_reference_equals(&Control::boxed(border.clone()), &Control::boxed(border)));
884    }
885
886    #[test]
887    fn different_controls_are_different_references() {
888        let (first, second) = (Border::new(), Border::new());
889
890        assert!(!reference_equals(&Some(Control::boxed(first.clone())), &Some(Control::boxed(second))));
891        assert!(!reference_equals(&Some(Control::boxed(first)), &boxed("able".to_string())));
892    }
893}
894
895#[cfg(test)]
896mod reference_item_tests {
897    // Not from upstream: an item of a list of handles is found in the form a binding
898    // delivers it in (found by the IntegrationTestApp sample, I008).
899    use super::*;
900
901    #[derive(PartialEq)]
902    struct Model(i32);
903
904    #[test]
905    fn a_shared_object_is_found_in_a_list_of_its_handles() {
906        ValueTypes::register_reference::<Model>();
907        let (first, second) = (Rc::new(Model(1)), Rc::new(Model(2)));
908        let list: FerroList<Rc<Model>> = FerroList::new();
909        list.add(first.clone());
910        list.add(second.clone());
911        // The form the list boxes its items in.
912        assert_eq!(1, IItemsList::index_of(&list, &IItemsList::get_at(&list, 1)));
913        // The object itself, as a binding of a property of the handle type delivers it.
914        assert_eq!(1, IItemsList::index_of(&list, &Some(second as BoxedValue)));
915        assert_eq!(-1, IItemsList::index_of(&list, &Some(Rc::new(7i32) as BoxedValue)));
916    }
917}
918
919#[cfg(test)]
920mod identity_tests {
921    // Not from upstream: the identity of an items source over a handle to a collection.
922    use super::ItemsSource;
923    use ferroui_base::collections::FerroList;
924    use std::rc::Rc;
925
926    #[test]
927    fn two_handles_to_one_list_are_the_same_collection() {
928        let list = FerroList::from_items([1, 2, 3]);
929        let first = ItemsSource::from(Rc::new(list.clone()));
930        let second = ItemsSource::from(Rc::new(list.clone()));
931        assert!(first.ptr_eq(&second));
932        assert_eq!(first, second);
933
934        let other = ItemsSource::from(Rc::new(FerroList::from_items([1, 2, 3])));
935        assert!(!first.ptr_eq(&other));
936
937        // A list that is the collection itself is identified by its address.
938        let vector = ItemsSource::from(vec![None, None]);
939        assert!(vector.ptr_eq(&vector.clone()));
940        assert!(!vector.ptr_eq(&ItemsSource::from(vec![None, None])));
941    }
942}