use ferroui_base::collections::{FerroList, NotifyCollectionChangedAction, NotifyCollectionChangedEventArgs};
use ferroui_base::data::core::ValueTypes;
use ferroui_base::data::model::BindableList;
use ferroui_base::{AnyValue, BoxedValue, PropertyValue};
use std::any::Any;
use std::rc::Rc;
#[derive(Clone, Copy)]
pub struct ItemsView<'a>(ItemsViewSource<'a>);
#[derive(Clone, Copy)]
enum ItemsViewSource<'a> {
Slice(&'a [Option<BoxedValue>]),
Window { list: &'a dyn IItemsList, start: usize, count: usize },
Typed(&'a dyn TypedItemsSource),
}
pub struct TypedItems<'a, T>(pub &'a [T]);
trait TypedItemsSource {
fn len(&self) -> usize;
fn get(&self, index: usize) -> Option<BoxedValue>;
}
impl<T: PropertyValue> TypedItemsSource for TypedItems<'_, T> {
#[inline]
fn len(&self) -> usize {
self.0.len()
}
#[inline]
fn get(&self, index: usize) -> Option<BoxedValue> {
box_item(&self.0[index])
}
}
impl<'a> ItemsView<'a> {
pub const EMPTY: ItemsView<'static> = ItemsView(ItemsViewSource::Slice(&[]));
#[inline]
pub const fn from_slice(items: &'a [Option<BoxedValue>]) -> Self {
Self(ItemsViewSource::Slice(items))
}
#[inline]
pub fn window(list: &'a dyn IItemsList, start: usize, count: usize) -> Self {
Self(ItemsViewSource::Window { list, start, count })
}
#[inline]
pub fn of_source(source: &'a ItemsSource) -> Self {
Self::window(&**source.list(), 0, source.count())
}
#[inline]
pub fn of_source_range(source: &'a ItemsSource, start: usize, count: usize) -> Self {
Self::window(&**source.list(), start, count)
}
#[inline]
pub fn from_typed<T: PropertyValue>(items: &'a TypedItems<'a, T>) -> Self {
Self(ItemsViewSource::Typed(items))
}
#[inline]
pub fn len(&self) -> usize {
match self.0 {
ItemsViewSource::Slice(items) => items.len(),
ItemsViewSource::Window { count, .. } => count,
ItemsViewSource::Typed(items) => items.len(),
}
}
#[inline]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[inline]
pub fn get(&self, index: usize) -> Option<BoxedValue> {
match self.0 {
ItemsViewSource::Slice(items) => items[index].clone(),
ItemsViewSource::Window { list, start, count } => {
assert!(index < count, "index out of range");
list.get_at(start + index)
}
ItemsViewSource::Typed(items) => items.get(index),
}
}
#[inline]
pub fn iter(&self) -> ItemsViewIter<'a> {
ItemsViewIter { view: *self, index: 0, end: self.len() }
}
pub fn to_vec(&self) -> Vec<Option<BoxedValue>> {
match self.0 {
ItemsViewSource::Slice(items) => items.to_vec(),
_ => self.iter().collect(),
}
}
}
impl Default for ItemsView<'_> {
fn default() -> Self {
ItemsView::EMPTY
}
}
impl std::fmt::Debug for ItemsView<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ItemsView(len = {})", self.len())
}
}
impl<'a> From<&'a [Option<BoxedValue>]> for ItemsView<'a> {
#[inline]
fn from(items: &'a [Option<BoxedValue>]) -> Self {
Self::from_slice(items)
}
}
impl<'a> From<&'a Vec<Option<BoxedValue>>> for ItemsView<'a> {
#[inline]
fn from(items: &'a Vec<Option<BoxedValue>>) -> Self {
Self::from_slice(items)
}
}
impl<'a, const N: usize> From<&'a [Option<BoxedValue>; N]> for ItemsView<'a> {
#[inline]
fn from(items: &'a [Option<BoxedValue>; N]) -> Self {
Self::from_slice(items)
}
}
impl<'a> From<&'a ItemsSource> for ItemsView<'a> {
#[inline]
fn from(source: &'a ItemsSource) -> Self {
Self::of_source(source)
}
}
#[derive(Clone)]
pub struct ItemsViewIter<'a> {
view: ItemsView<'a>,
index: usize,
end: usize,
}
impl Iterator for ItemsViewIter<'_> {
type Item = Option<BoxedValue>;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
if self.index < self.end {
self.index += 1;
Some(self.view.get(self.index - 1))
} else {
None
}
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = self.end - self.index;
(remaining, Some(remaining))
}
}
impl DoubleEndedIterator for ItemsViewIter<'_> {
#[inline]
fn next_back(&mut self) -> Option<Self::Item> {
if self.index < self.end {
self.end -= 1;
Some(self.view.get(self.end))
} else {
None
}
}
}
impl ExactSizeIterator for ItemsViewIter<'_> {}
impl<'a> IntoIterator for ItemsView<'a> {
type Item = Option<BoxedValue>;
type IntoIter = ItemsViewIter<'a>;
#[inline]
fn into_iter(self) -> ItemsViewIter<'a> {
self.iter()
}
}
impl<'a> IntoIterator for &ItemsView<'a> {
type Item = Option<BoxedValue>;
type IntoIter = ItemsViewIter<'a>;
#[inline]
fn into_iter(self) -> ItemsViewIter<'a> {
self.iter()
}
}
#[derive(Clone, Copy, Debug)]
pub struct ItemsChangedEventArgs<'a> {
pub action: NotifyCollectionChangedAction,
pub new_items: ItemsView<'a>,
pub old_items: ItemsView<'a>,
pub new_starting_index: i32,
pub old_starting_index: i32,
}
impl ItemsChangedEventArgs<'static> {
pub const RESET: ItemsChangedEventArgs<'static> = ItemsChangedEventArgs {
action: NotifyCollectionChangedAction::Reset,
new_items: ItemsView::EMPTY,
old_items: ItemsView::EMPTY,
new_starting_index: -1,
old_starting_index: -1,
};
}
impl<'a> ItemsChangedEventArgs<'a> {
#[inline]
pub fn add(new_items: impl Into<ItemsView<'a>>, new_starting_index: i32) -> Self {
Self {
action: NotifyCollectionChangedAction::Add,
new_items: new_items.into(),
old_items: ItemsView::EMPTY,
new_starting_index,
old_starting_index: -1,
}
}
#[inline]
pub fn remove(old_items: impl Into<ItemsView<'a>>, old_starting_index: i32) -> Self {
Self {
action: NotifyCollectionChangedAction::Remove,
new_items: ItemsView::EMPTY,
old_items: old_items.into(),
new_starting_index: -1,
old_starting_index,
}
}
}
impl<'a> From<&NotifyCollectionChangedEventArgs<'a, Option<BoxedValue>>> for ItemsChangedEventArgs<'a> {
#[inline]
fn from(e: &NotifyCollectionChangedEventArgs<'a, Option<BoxedValue>>) -> Self {
Self {
action: e.action,
new_items: ItemsView::from_slice(e.new_items),
old_items: ItemsView::from_slice(e.old_items),
new_starting_index: e.new_starting_index,
old_starting_index: e.old_starting_index,
}
}
}
pub type ItemsChangedHandler = dyn for<'a> Fn(&ItemsChangedEventArgs<'a>);
#[inline]
pub fn items_equal(a: &Option<BoxedValue>, b: &Option<BoxedValue>) -> bool {
match (a, b) {
(None, None) => true,
(Some(a), Some(b)) => Rc::ptr_eq(a, b) || (**a).any_value_eq(&**b),
_ => false,
}
}
pub fn reference_equals(a: &Option<BoxedValue>, b: &Option<BoxedValue>) -> bool {
match (a, b) {
(None, None) => true,
(Some(a), Some(b)) => boxed_reference_equals(a, b),
_ => false,
}
}
pub fn boxed_reference_equals(a: &BoxedValue, b: &BoxedValue) -> bool {
if std::ptr::addr_eq(Rc::as_ptr(a), Rc::as_ptr(b)) {
return true;
}
fn as_str(value: &dyn AnyValue) -> Option<&str> {
if let Some(text) = value.downcast_ref::<String>() {
return Some(text.as_str());
}
value.downcast_ref::<&'static str>().copied()
}
let (a, b): (&dyn AnyValue, &dyn AnyValue) = (&**a, &**b);
if let (Some(a), Some(b)) = (as_str(a), as_str(b)) {
return a == b;
}
match (ValueTypes::as_object(a), ValueTypes::as_object(b)) {
(Some(a), Some(b)) => a.ptr_eq(&b),
_ => false,
}
}
#[inline]
pub fn box_item<T: PropertyValue>(value: &T) -> Option<BoxedValue> {
let any: &dyn Any = value;
if let Some(boxed) = any.downcast_ref::<BoxedValue>() {
return Some(boxed.clone());
}
if let Some(boxed) = any.downcast_ref::<Option<BoxedValue>>() {
return boxed.clone();
}
Some(Rc::new(value.clone()))
}
#[inline]
pub fn unbox_item<T: PropertyValue>(item: &Option<BoxedValue>) -> Option<T> {
let boxed = item.as_ref()?;
let any: &dyn Any = boxed;
if let Some(value) = any.downcast_ref::<T>() {
return Some(value.clone());
}
let value: &dyn AnyValue = &**boxed;
value.downcast_ref::<T>().cloned()
}
#[inline]
fn item_equals_typed<T: PropertyValue>(item: &Option<BoxedValue>, value: &T) -> bool {
let any: &dyn Any = value;
if let Some(boxed) = any.downcast_ref::<BoxedValue>() {
return item.as_ref().is_some_and(|item| Rc::ptr_eq(item, boxed) || (**item).any_value_eq(&**boxed));
}
if let Some(boxed) = any.downcast_ref::<Option<BoxedValue>>() {
return items_equal(item, boxed);
}
match item {
Some(item) => {
let item: &dyn AnyValue = &**item;
item.downcast_ref::<T>().is_some_and(|item| item == value)
}
None => false,
}
}
pub trait IItemsList {
fn count(&self) -> usize;
fn get_at(&self, index: usize) -> Option<BoxedValue>;
fn index_of(&self, item: &Option<BoxedValue>) -> i32 {
for i in 0..self.count() {
if items_equal(&self.get_at(i), item) {
return i as i32;
}
}
-1
}
fn is_notifying(&self) -> bool {
false
}
fn add_collection_changed(&self, _handler: Rc<ItemsChangedHandler>) -> Option<u64> {
None
}
fn remove_collection_changed(&self, _token: u64) {}
fn identity(&self) -> Option<usize> {
None
}
fn as_any(&self) -> &dyn Any;
}
impl IItemsList for Vec<Option<BoxedValue>> {
fn count(&self) -> usize {
self.len()
}
fn get_at(&self, index: usize) -> Option<BoxedValue> {
self[index].clone()
}
fn index_of(&self, item: &Option<BoxedValue>) -> i32 {
self.iter().position(|i| items_equal(i, item)).map_or(-1, |i| i as i32)
}
fn as_any(&self) -> &dyn Any {
self
}
}
impl<T: PropertyValue> IItemsList for FerroList<T> {
fn count(&self) -> usize {
FerroList::count(self)
}
fn get_at(&self, index: usize) -> Option<BoxedValue> {
let snapshot = self.snapshot();
box_item(&snapshot[index])
}
fn index_of(&self, item: &Option<BoxedValue>) -> i32 {
let snapshot = self.snapshot();
if let Some(index) = snapshot.iter().position(|i| item_equals_typed(item, i)) {
return index as i32;
}
let is_typed = item.as_ref().is_some_and(|boxed| (**boxed).downcast_ref::<T>().is_some());
if !is_typed {
if let Some(typed) = ferroui_base::metadata::from_markup_value::<T>(item) {
return snapshot.iter().position(|i| *i == typed).map_or(-1, |i| i as i32);
}
}
-1
}
fn is_notifying(&self) -> bool {
true
}
fn add_collection_changed(&self, handler: Rc<ItemsChangedHandler>) -> Option<u64> {
Some(FerroList::add_collection_changed(
self,
Rc::new(move |e: &NotifyCollectionChangedEventArgs<'_, T>| {
let new_items = TypedItems(e.new_items);
let old_items = TypedItems(e.old_items);
handler(&ItemsChangedEventArgs {
action: e.action,
new_items: ItemsView::from_typed(&new_items),
old_items: ItemsView::from_typed(&old_items),
new_starting_index: e.new_starting_index,
old_starting_index: e.old_starting_index,
});
}),
))
}
fn remove_collection_changed(&self, token: u64) {
FerroList::remove_collection_changed(self, token);
}
fn identity(&self) -> Option<usize> {
Some(ferroui_base::utilities::WeakEventSender::sender_address(self))
}
fn as_any(&self) -> &dyn Any {
self
}
}
impl<T: PropertyValue> IItemsList for BindableList<T> {
fn count(&self) -> usize {
self.items().count()
}
fn get_at(&self, index: usize) -> Option<BoxedValue> {
IItemsList::get_at(self.items(), index)
}
fn index_of(&self, item: &Option<BoxedValue>) -> i32 {
IItemsList::index_of(self.items(), item)
}
fn is_notifying(&self) -> bool {
true
}
fn add_collection_changed(&self, handler: Rc<ItemsChangedHandler>) -> Option<u64> {
IItemsList::add_collection_changed(self.items(), handler)
}
fn remove_collection_changed(&self, token: u64) {
IItemsList::remove_collection_changed(self.items(), token);
}
fn as_any(&self) -> &dyn Any {
self
}
}
impl<T: PropertyValue> IItemsList for ferroui_system::collections::object_model::ObservableCollection<T> {
fn count(&self) -> usize {
ferroui_system::collections::object_model::ObservableCollection::count(self)
}
fn get_at(&self, index: usize) -> Option<BoxedValue> {
box_item(&self.get(index))
}
fn index_of(&self, item: &Option<BoxedValue>) -> i32 {
(0..ferroui_system::collections::object_model::ObservableCollection::count(self))
.position(|index| item_equals_typed(item, &self.get(index)))
.map_or(-1, |index| index as i32)
}
fn is_notifying(&self) -> bool {
true
}
fn add_collection_changed(&self, handler: Rc<ItemsChangedHandler>) -> Option<u64> {
Some(ferroui_system::collections::object_model::ObservableCollection::add_collection_changed(
self,
Rc::new(move |e: &ferroui_system::collections::specialized::NotifyCollectionChangedEventArgs<'_, T>| {
let new_items = TypedItems(e.new_items);
let old_items = TypedItems(e.old_items);
handler(&ItemsChangedEventArgs {
action: e.action,
new_items: ItemsView::from_typed(&new_items),
old_items: ItemsView::from_typed(&old_items),
new_starting_index: e.new_starting_index,
old_starting_index: e.old_starting_index,
});
}),
))
}
fn remove_collection_changed(&self, token: u64) {
let _ = ferroui_system::collections::object_model::ObservableCollection::remove_collection_changed(self, token);
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[derive(Clone)]
pub struct ItemsSource(Rc<dyn IItemsList>);
impl ItemsSource {
pub fn new(list: Rc<dyn IItemsList>) -> Self {
Self(list)
}
pub fn from_items(items: impl IntoIterator<Item = Option<BoxedValue>>) -> Self {
Self(Rc::new(items.into_iter().collect::<Vec<_>>()))
}
pub fn from_values<T: PropertyValue>(values: impl IntoIterator<Item = T>) -> Self {
Self(Rc::new(values.into_iter().map(|v| box_item(&v)).collect::<Vec<_>>()))
}
pub fn from_strs<'a>(values: impl IntoIterator<Item = &'a str>) -> Self {
Self::from_values(values.into_iter().map(str::to_string))
}
#[inline]
pub fn list(&self) -> &Rc<dyn IItemsList> {
&self.0
}
#[inline]
pub fn count(&self) -> usize {
self.0.count()
}
#[inline]
pub fn get_at(&self, index: usize) -> Option<BoxedValue> {
self.0.get_at(index)
}
#[inline]
pub fn index_of(&self, item: &Option<BoxedValue>) -> i32 {
self.0.index_of(item)
}
#[inline]
pub fn contains(&self, item: &Option<BoxedValue>) -> bool {
self.0.index_of(item) >= 0
}
#[inline]
pub fn is_notifying(&self) -> bool {
self.0.is_notifying()
}
pub fn downcast_ref<T: 'static>(&self) -> Option<&T> {
self.0.as_any().downcast_ref::<T>()
}
#[inline]
pub fn identity(&self) -> usize {
self.0.identity().unwrap_or(Rc::as_ptr(&self.0) as *const () as usize)
}
#[inline]
pub fn ptr_eq(&self, other: &ItemsSource) -> bool {
self.identity() == other.identity()
}
pub fn iter(&self) -> impl Iterator<Item = Option<BoxedValue>> + '_ {
let mut index = 0;
std::iter::from_fn(move || {
if index < self.0.count() {
index += 1;
Some(self.0.get_at(index - 1))
} else {
None
}
})
}
pub fn to_vec(&self) -> Vec<Option<BoxedValue>> {
self.iter().collect()
}
pub fn register_binding_conversion_with<S: 'static>(convert: fn(&S) -> ItemsSource) {
ValueTypes::register_conversion::<S, ItemsSource>(move |s| Some(convert(s)));
ValueTypes::register_conversion::<S, Option<ItemsSource>>(move |s| Some(Some(convert(s))));
}
pub fn register_binding_conversion<S: Clone + Into<ItemsSource> + 'static>() {
ValueTypes::register_conversion::<S, ItemsSource>(|s| Some(s.clone().into()));
ValueTypes::register_conversion::<S, Option<ItemsSource>>(|s| Some(Some(s.clone().into())));
}
}
impl PartialEq for ItemsSource {
fn eq(&self, other: &Self) -> bool {
self.ptr_eq(other)
}
}
impl std::fmt::Debug for ItemsSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ItemsSource(count = {})", self.count())
}
}
impl From<Rc<dyn IItemsList>> for ItemsSource {
fn from(value: Rc<dyn IItemsList>) -> Self {
Self(value)
}
}
impl<T: PropertyValue> From<Rc<FerroList<T>>> for ItemsSource {
fn from(value: Rc<FerroList<T>>) -> Self {
Self(value)
}
}
impl<T: PropertyValue> From<Rc<BindableList<T>>> for ItemsSource {
fn from(value: Rc<BindableList<T>>) -> Self {
Self(value)
}
}
impl From<Vec<Option<BoxedValue>>> for ItemsSource {
fn from(value: Vec<Option<BoxedValue>>) -> Self {
Self(Rc::new(value))
}
}
impl From<Vec<BoxedValue>> for ItemsSource {
fn from(value: Vec<BoxedValue>) -> Self {
Self::from_items(value.into_iter().map(Some))
}
}
impl From<Rc<Vec<Option<BoxedValue>>>> for ItemsSource {
fn from(value: Rc<Vec<Option<BoxedValue>>>) -> Self {
Self(value)
}
}
#[cfg(test)]
mod reference_equals_tests {
use super::{boxed_reference_equals, items_equal, reference_equals};
use crate::{Border, Control};
use ferroui_base::{BoxedValue, Ref, StyledElement};
use std::rc::Rc;
fn boxed<T: PartialEq + 'static>(value: T) -> Option<BoxedValue> {
Some(Rc::new(value))
}
#[test]
fn nulls_and_the_same_box_are_the_same_reference() {
let value = boxed(42_i32);
assert!(reference_equals(&None, &None));
assert!(reference_equals(&value, &value.clone()));
assert!(!reference_equals(&value, &None));
assert!(!reference_equals(&None, &value));
}
#[test]
fn equal_strings_in_different_boxes_are_the_same_reference() {
assert!(reference_equals(&boxed("able".to_string()), &boxed("able".to_string())));
assert!(reference_equals(&boxed("able".to_string()), &boxed("able")));
assert!(reference_equals(&boxed(String::new()), &boxed(String::new())));
assert!(!reference_equals(&boxed("able".to_string()), &boxed("abide".to_string())));
assert!(!reference_equals(&boxed("able".to_string()), &boxed("Able".to_string())));
assert!(!reference_equals(&boxed("1".to_string()), &boxed(1_i32)));
}
#[test]
fn equal_values_in_different_boxes_are_different_references() {
assert!(!reference_equals(&boxed(42_i32), &boxed(42_i32)));
assert!(!reference_equals(&boxed(1.5_f64), &boxed(1.5_f64)));
assert!(!reference_equals(&boxed(true), &boxed(true)));
assert!(items_equal(&boxed(42_i32), &boxed(42_i32)));
}
#[test]
fn the_same_control_in_different_boxes_is_the_same_reference() {
let border = Border::new();
let as_control: Ref<Control> = border.clone().upcast();
let as_element: Ref<StyledElement> = border.clone().upcast();
assert!(reference_equals(&Some(Control::boxed(border.clone())), &Some(Control::boxed(border.clone()))));
assert!(reference_equals(&boxed(border.clone()), &boxed(as_control)));
assert!(reference_equals(&boxed(as_element), &boxed(border.clone())));
assert!(boxed_reference_equals(&Control::boxed(border.clone()), &Control::boxed(border)));
}
#[test]
fn different_controls_are_different_references() {
let (first, second) = (Border::new(), Border::new());
assert!(!reference_equals(&Some(Control::boxed(first.clone())), &Some(Control::boxed(second))));
assert!(!reference_equals(&Some(Control::boxed(first)), &boxed("able".to_string())));
}
}
#[cfg(test)]
mod reference_item_tests {
use super::*;
#[derive(PartialEq)]
struct Model(i32);
#[test]
fn a_shared_object_is_found_in_a_list_of_its_handles() {
ValueTypes::register_reference::<Model>();
let (first, second) = (Rc::new(Model(1)), Rc::new(Model(2)));
let list: FerroList<Rc<Model>> = FerroList::new();
list.add(first.clone());
list.add(second.clone());
assert_eq!(1, IItemsList::index_of(&list, &IItemsList::get_at(&list, 1)));
assert_eq!(1, IItemsList::index_of(&list, &Some(second as BoxedValue)));
assert_eq!(-1, IItemsList::index_of(&list, &Some(Rc::new(7i32) as BoxedValue)));
}
}
#[cfg(test)]
mod identity_tests {
use super::ItemsSource;
use ferroui_base::collections::FerroList;
use std::rc::Rc;
#[test]
fn two_handles_to_one_list_are_the_same_collection() {
let list = FerroList::from_items([1, 2, 3]);
let first = ItemsSource::from(Rc::new(list.clone()));
let second = ItemsSource::from(Rc::new(list.clone()));
assert!(first.ptr_eq(&second));
assert_eq!(first, second);
let other = ItemsSource::from(Rc::new(FerroList::from_items([1, 2, 3])));
assert!(!first.ptr_eq(&other));
let vector = ItemsSource::from(vec![None, None]);
assert!(vector.ptr_eq(&vector.clone()));
assert!(!vector.ptr_eq(&ItemsSource::from(vec![None, None])));
}
}