1use std::cell::RefCell;
47use std::collections::HashSet;
48use std::hash::Hash;
49use std::ops::Range;
50use std::rc::Rc;
51
52use teksilo_core::ObserverHandle;
53
54use crate::data_change::DataChange;
55use crate::dnd_types::{
56 DragEligibility, DragSource, DropCommit, DropPosition, DropQuery, DropResponse,
57};
58use crate::list_data_source::ListDataSource;
59
60struct ObserverEntry {
61 id: u64,
62 callback: Rc<dyn Fn(&DataChange)>,
63}
64
65struct ListModelInner<T> {
66 items: Vec<T>,
67 observers: Vec<ObserverEntry>,
68 next_observer_id: u64,
69 #[cfg(debug_assertions)]
75 debug_adapter: Option<Rc<dyn crate::debug_registry::ModelDebug>>,
76}
77
78pub struct ListModel<T: 'static> {
87 inner: Rc<RefCell<ListModelInner<T>>>,
88}
89
90impl<T: 'static> ListModel<T> {
91 pub fn new() -> Self {
93 Self {
94 inner: Rc::new(RefCell::new(ListModelInner {
95 items: Vec::new(),
96 observers: Vec::new(),
97 next_observer_id: 1,
98 #[cfg(debug_assertions)]
99 debug_adapter: None,
100 })),
101 }
102 }
103
104 pub fn from_vec(items: Vec<T>) -> Self {
106 Self {
107 inner: Rc::new(RefCell::new(ListModelInner {
108 items,
109 observers: Vec::new(),
110 next_observer_id: 1,
111 #[cfg(debug_assertions)]
112 debug_adapter: None,
113 })),
114 }
115 }
116
117 pub fn len(&self) -> usize {
119 self.inner.borrow().items.len()
120 }
121
122 pub fn is_empty(&self) -> bool {
124 self.inner.borrow().items.is_empty()
125 }
126
127 pub fn with_item<R>(&self, index: usize, f: impl FnOnce(&T) -> R) -> Option<R> {
132 let guard = self.inner.borrow();
133 guard.items.get(index).map(f)
134 }
135
136 pub fn push(&self, item: T) {
138 let index = {
139 let mut guard = self.inner.borrow_mut();
140 let index = guard.items.len();
141 guard.items.push(item);
142 index
143 };
144 self.notify(DataChange::ItemsInserted {
145 range: index..index + 1,
146 });
147 }
148
149 pub fn insert(&self, index: usize, item: T) {
154 {
155 let mut guard = self.inner.borrow_mut();
156 guard.items.insert(index, item);
157 }
158 self.notify(DataChange::ItemsInserted {
159 range: index..index + 1,
160 });
161 }
162
163 pub fn remove(&self, index: usize) -> T {
168 let item = {
169 let mut guard = self.inner.borrow_mut();
170 guard.items.remove(index)
171 };
172 self.notify(DataChange::ItemsRemoved {
173 range: index..index + 1,
174 });
175 item
176 }
177
178 pub fn set(&self, index: usize, item: T) {
183 {
184 let mut guard = self.inner.borrow_mut();
185 guard.items[index] = item;
186 }
187 self.notify(DataChange::ItemUpdated { index });
188 }
189
190 pub fn move_item(&self, from: usize, to: usize) {
197 if from == to {
198 return;
199 }
200 {
201 let mut guard = self.inner.borrow_mut();
202 let item = guard.items.remove(from);
203 guard.items.insert(to, item);
204 }
205 self.notify(DataChange::ItemsMoved { from, to, count: 1 });
206 }
207
208 pub fn move_items(&self, indices: &[usize], insert_gap: usize) -> bool {
222 let len = self.len();
223 let mut idx: Vec<usize> = indices.iter().copied().filter(|&i| i < len).collect();
224 idx.sort_unstable();
225 idx.dedup();
226 if idx.is_empty() {
227 return false;
228 }
229 let contiguous = idx.windows(2).all(|w| w[1] == w[0] + 1);
230 let from0 = idx[0];
231 let count = idx.len();
232 let at;
233 {
234 let mut guard = self.inner.borrow_mut();
235 let mut block: Vec<T> = idx.iter().rev().map(|&i| guard.items.remove(i)).collect();
238 block.reverse();
239 let removed_before = idx.iter().filter(|&&i| i < insert_gap).count();
240 at = insert_gap
241 .saturating_sub(removed_before)
242 .min(guard.items.len());
243 for (off, item) in block.into_iter().enumerate() {
244 guard.items.insert(at + off, item);
245 }
246 }
247 if contiguous && from0 != at {
248 self.notify(DataChange::ItemsMoved {
249 from: from0,
250 to: at,
251 count,
252 });
253 } else if contiguous {
254 } else {
256 self.notify(DataChange::Reset);
257 }
258 true
259 }
260
261 pub fn replace_all(&self, items: Vec<T>) {
263 {
264 let mut guard = self.inner.borrow_mut();
265 guard.items = items;
266 }
267 self.notify(DataChange::Reset);
268 }
269
270 pub fn clear(&self) {
272 {
273 let mut guard = self.inner.borrow_mut();
274 guard.items.clear();
275 }
276 self.notify(DataChange::Reset);
277 }
278
279 pub fn observe_changes(&self, f: impl Fn(&DataChange) + 'static) -> ObserverHandle {
282 let mut guard = self.inner.borrow_mut();
283 let id = guard.next_observer_id;
284 guard.next_observer_id += 1;
285 guard.observers.push(ObserverEntry {
286 id,
287 callback: Rc::new(f),
288 });
289 let inner = self.inner.clone();
290 ObserverHandle::new(
291 self.inner.clone(),
292 id,
293 Rc::new(move |observer_id| {
294 inner.borrow_mut().observers.retain(|e| e.id != observer_id);
295 }),
296 )
297 }
298
299 fn notify(&self, change: DataChange) {
300 let callbacks: Vec<Rc<dyn Fn(&DataChange)>> = self
301 .inner
302 .borrow()
303 .observers
304 .iter()
305 .map(|e| e.callback.clone())
306 .collect();
307 for cb in &callbacks {
308 cb(&change);
309 }
310 }
311}
312
313impl<T: PartialEq + 'static> ListModel<T> {
314 pub fn reconcile_by_key<K: Eq + Hash>(&self, new_items: Vec<T>, key_fn: impl Fn(&T) -> K) {
396 let mut changes: Vec<DataChange> = Vec::new();
397 {
398 let mut guard = self.inner.borrow_mut();
399 reconcile_vec(&mut guard.items, new_items, &key_fn, &mut changes);
400 }
401 for change in changes {
402 self.notify(change);
403 }
404 }
405}
406
407fn reconcile_vec<T: PartialEq, K: Eq + Hash>(
414 items: &mut Vec<T>,
415 new_items: Vec<T>,
416 key_fn: &impl Fn(&T) -> K,
417 out: &mut Vec<DataChange>,
418) {
419 let new_pairs: Vec<(K, T)> = new_items.into_iter().map(|it| (key_fn(&it), it)).collect();
423 let new_key_set: HashSet<&K> = new_pairs.iter().map(|(k, _)| k).collect();
424
425 let old_keys: Vec<K> = items.iter().map(key_fn).collect();
430 let remove_idxs: Vec<usize> = (0..items.len())
431 .filter(|&i| !new_key_set.contains(&old_keys[i]))
432 .collect();
433 for range in coalesce_ranges(&remove_idxs).into_iter().rev() {
434 items.drain(range.clone());
435 out.push(DataChange::ItemsRemoved { range });
436 }
437
438 let remaining_keys: HashSet<K> = items.iter().map(key_fn).collect();
441
442 let mut cursor = 0usize;
448 let mut pending_inserts: Vec<T> = Vec::new();
449
450 for (key, new_item) in new_pairs {
451 if remaining_keys.contains(&key) {
452 flush_inserts(items, &mut cursor, &mut pending_inserts, out);
453
454 let pos = items
458 .iter()
459 .skip(cursor)
460 .position(|it| key_fn(it) == key)
461 .expect(
462 "reconcile_by_key: key reported common but not found — \
463 key_fn must be stable and keys unique",
464 );
465 let actual = cursor + pos;
466 if actual != cursor {
467 let val = items.remove(actual);
468 items.insert(cursor, val);
469 out.push(DataChange::ItemsMoved {
470 from: actual,
471 to: cursor,
472 count: 1,
473 });
474 }
475 if items[cursor] != new_item {
476 items[cursor] = new_item;
477 out.push(DataChange::ItemUpdated { index: cursor });
478 }
479 cursor += 1;
480 } else {
481 pending_inserts.push(new_item);
482 }
483 }
484 flush_inserts(items, &mut cursor, &mut pending_inserts, out);
485}
486
487fn flush_inserts<T>(
491 items: &mut Vec<T>,
492 cursor: &mut usize,
493 pending: &mut Vec<T>,
494 out: &mut Vec<DataChange>,
495) {
496 if pending.is_empty() {
497 return;
498 }
499 let start = *cursor;
500 let n = pending.len();
501 items.splice(start..start, pending.drain(..));
502 out.push(DataChange::ItemsInserted {
503 range: start..start + n,
504 });
505 *cursor += n;
506}
507
508fn coalesce_ranges(sorted_idxs: &[usize]) -> Vec<Range<usize>> {
511 let mut ranges = Vec::new();
512 let mut iter = sorted_idxs.iter().peekable();
513 while let Some(&start) = iter.next() {
514 let mut end = start + 1;
515 while iter.peek().is_some_and(|&&n| n == end) {
516 end += 1;
517 iter.next();
518 }
519 ranges.push(start..end);
520 }
521 ranges
522}
523
524impl<T: std::fmt::Debug + 'static> ListModel<T> {
525 pub fn debug_named(self, _name: impl Into<String>) -> Self {
533 #[cfg(debug_assertions)]
534 {
535 let weak = Rc::downgrade(&self.inner);
536 let adapter: Rc<dyn crate::debug_registry::ModelDebug> =
537 Rc::new(ListModelDebug::<T> { weak });
538 let name = _name.into();
539 crate::debug_registry::register(name, Rc::downgrade(&adapter));
540 self.inner.borrow_mut().debug_adapter = Some(adapter);
541 }
542 self
543 }
544}
545
546#[cfg(debug_assertions)]
547struct ListModelDebug<T> {
548 weak: std::rc::Weak<RefCell<ListModelInner<T>>>,
549}
550
551#[cfg(debug_assertions)]
552impl<T: std::fmt::Debug + 'static> crate::debug_registry::ModelDebug for ListModelDebug<T> {
553 fn kind(&self) -> &'static str {
554 "ListModel"
555 }
556 fn len(&self) -> usize {
557 self.weak
558 .upgrade()
559 .map(|inner| inner.borrow().items.len())
560 .unwrap_or(0)
561 }
562 fn debug_dump(&self, out: &mut dyn std::fmt::Write) {
563 let Some(inner) = self.weak.upgrade() else {
564 return;
565 };
566 let guard = inner.borrow();
567 for (i, item) in guard.items.iter().enumerate() {
568 let _ = writeln!(out, "[{}] {:?}", i, item);
569 }
570 }
571}
572
573impl<T: 'static> Default for ListModel<T> {
574 fn default() -> Self {
575 Self::new()
576 }
577}
578
579impl<T: 'static> Clone for ListModel<T> {
580 fn clone(&self) -> Self {
581 Self {
582 inner: self.inner.clone(),
583 }
584 }
585}
586
587impl<T: std::fmt::Debug + 'static> std::fmt::Debug for ListModel<T> {
588 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
589 f.debug_struct("ListModel")
590 .field("len", &self.inner.borrow().items.len())
591 .finish()
592 }
593}
594
595impl<T: 'static> ListDataSource for ListModel<T> {
600 type Item = T;
601 type Key = usize;
602
603 fn len(&self) -> usize {
604 ListModel::len(self)
605 }
606
607 fn with_item<R>(&self, index: usize, f: impl FnOnce(&T) -> R) -> Option<R> {
608 ListModel::with_item(self, index, f)
609 }
610
611 fn key_at(&self, index: usize) -> Option<usize> {
612 (index < ListModel::len(self)).then_some(index)
613 }
614
615 fn index_of(&self, key: &usize) -> Option<usize> {
616 (*key < ListModel::len(self)).then_some(*key)
617 }
618
619 fn observe_changes(&self, f: impl Fn(&DataChange) + 'static) -> ObserverHandle {
620 ListModel::observe_changes(self, f)
621 }
622
623 fn drag(&self, _key: &usize) -> DragEligibility {
624 DragEligibility::CanDrag
625 }
626
627 fn can_accept(&self, query: &DropQuery<'_, usize>) -> DropResponse {
628 match &query.source {
629 DragSource::SameView { .. } => match query.position {
630 DropPosition::Into => DropResponse::Reject,
631 DropPosition::Before | DropPosition::After => DropResponse::Accept,
632 },
633 DragSource::Foreign { .. } => DropResponse::Reject,
634 }
635 }
636
637 fn accept_drop(&self, commit: DropCommit<'_, usize>) -> bool {
638 let DragSource::SameView { key: from } = commit.source else {
639 return false;
640 };
641 let len = ListModel::len(self);
642 if from >= len {
643 return false;
644 }
645 let target = commit.target;
646 let shift = if from < target { 1 } else { 0 };
649 let to = match commit.position {
650 DropPosition::Before => target.saturating_sub(shift),
651 DropPosition::After => (target + 1).saturating_sub(shift),
652 DropPosition::Into => return false,
653 };
654 let to = to.min(len.saturating_sub(1));
655 self.move_item(from, to);
656 true
657 }
658
659 fn reorder_within(&self, sources: &[usize], target: &usize, position: DropPosition) -> bool {
660 if sources.len() <= 1 {
665 let Some(&from) = sources.first() else {
666 return false;
667 };
668 return self.accept_drop(DropCommit {
669 source: DragSource::SameView { key: from },
670 target: *target,
671 position,
672 });
673 }
674 let gap = match position {
675 DropPosition::Before => *target,
676 DropPosition::After => *target + 1,
677 DropPosition::Into => return false,
678 };
679 self.move_items(sources, gap)
680 }
681
682 fn on_drag_out(&self, key: &usize) {
683 if *key < ListModel::len(self) {
687 let _ = self.remove(*key);
688 }
689 }
690}
691
692#[cfg(test)]
693mod tests {
694 use std::cell::Cell;
695
696 use super::*;
697
698 #[test]
699 fn new_is_empty() {
700 let model: ListModel<String> = ListModel::new();
701 assert!(model.is_empty());
702 assert_eq!(model.len(), 0);
703 }
704
705 #[test]
706 fn from_vec() {
707 let model = ListModel::from_vec(vec![10, 20, 30]);
708 assert_eq!(model.len(), 3);
709 assert_eq!(model.with_item(1, |v| *v), Some(20));
710 }
711
712 #[test]
713 fn push_emits_inserted() {
714 let model = ListModel::new();
715 let changes: Rc<RefCell<Vec<DataChange>>> = Rc::new(RefCell::new(Vec::new()));
716 let c = changes.clone();
717 let _handle = model.observe_changes(move |change| {
718 c.borrow_mut().push(change.clone());
719 });
720
721 model.push("a");
722 model.push("b");
723
724 let log = changes.borrow();
725 assert_eq!(log.len(), 2);
726 assert_eq!(log[0], DataChange::ItemsInserted { range: 0..1 });
727 assert_eq!(log[1], DataChange::ItemsInserted { range: 1..2 });
728 }
729
730 #[test]
731 fn insert_emits_inserted() {
732 let model = ListModel::from_vec(vec![1, 2, 3]);
733 let changes: Rc<RefCell<Vec<DataChange>>> = Rc::new(RefCell::new(Vec::new()));
734 let c = changes.clone();
735 let _handle = model.observe_changes(move |change| {
736 c.borrow_mut().push(change.clone());
737 });
738
739 model.insert(1, 99);
740
741 assert_eq!(model.len(), 4);
742 assert_eq!(model.with_item(1, |v| *v), Some(99));
743 let log = changes.borrow();
744 assert_eq!(log.len(), 1, "insert should emit exactly one change");
745 assert_eq!(log[0], DataChange::ItemsInserted { range: 1..2 });
746 }
747
748 #[test]
749 fn remove_emits_removed() {
750 let model = ListModel::from_vec(vec!["a", "b", "c"]);
751 let changes: Rc<RefCell<Vec<DataChange>>> = Rc::new(RefCell::new(Vec::new()));
752 let c = changes.clone();
753 let _handle = model.observe_changes(move |change| {
754 c.borrow_mut().push(change.clone());
755 });
756
757 let removed = model.remove(1);
758 assert_eq!(removed, "b");
759 assert_eq!(model.len(), 2);
760 let log = changes.borrow();
761 assert_eq!(log.len(), 1, "remove should emit exactly one change");
762 assert_eq!(log[0], DataChange::ItemsRemoved { range: 1..2 });
763 }
764
765 #[test]
766 fn set_emits_updated() {
767 let model = ListModel::from_vec(vec![10, 20, 30]);
768 let changes: Rc<RefCell<Vec<DataChange>>> = Rc::new(RefCell::new(Vec::new()));
769 let c = changes.clone();
770 let _handle = model.observe_changes(move |change| {
771 c.borrow_mut().push(change.clone());
772 });
773
774 model.set(2, 99);
775 assert_eq!(model.with_item(2, |v| *v), Some(99));
776 let log = changes.borrow();
777 assert_eq!(log.len(), 1, "set should emit exactly one change");
778 assert_eq!(log[0], DataChange::ItemUpdated { index: 2 });
779 }
780
781 #[test]
782 fn move_item_emits_moved() {
783 let model = ListModel::from_vec(vec!["a", "b", "c", "d"]);
784 let changes: Rc<RefCell<Vec<DataChange>>> = Rc::new(RefCell::new(Vec::new()));
785 let c = changes.clone();
786 let _handle = model.observe_changes(move |change| {
787 c.borrow_mut().push(change.clone());
788 });
789
790 model.move_item(0, 2);
791 assert_eq!(model.with_item(0, |v| *v), Some("b"));
793 assert_eq!(model.with_item(2, |v| *v), Some("a"));
794 let log = changes.borrow();
795 assert_eq!(log.len(), 1, "move_item should emit exactly one change");
796 assert_eq!(
797 log[0],
798 DataChange::ItemsMoved {
799 from: 0,
800 to: 2,
801 count: 1
802 }
803 );
804 }
805
806 #[test]
807 fn move_item_same_index_is_noop() {
808 let model = ListModel::from_vec(vec![1, 2, 3]);
809 let count = Rc::new(Cell::new(0));
810 let c = count.clone();
811 let _handle = model.observe_changes(move |_| {
812 c.set(c.get() + 1);
813 });
814
815 model.move_item(1, 1);
816 assert_eq!(count.get(), 0);
817 }
818
819 #[test]
820 fn replace_all_emits_reset() {
821 let model = ListModel::from_vec(vec![1, 2]);
822 let changes: Rc<RefCell<Vec<DataChange>>> = Rc::new(RefCell::new(Vec::new()));
823 let c = changes.clone();
824 let _handle = model.observe_changes(move |change| {
825 c.borrow_mut().push(change.clone());
826 });
827
828 model.replace_all(vec![10, 20, 30]);
829 assert_eq!(model.len(), 3);
830 let log = changes.borrow();
831 assert_eq!(log[0], DataChange::Reset);
832 }
833
834 #[test]
835 fn clear_emits_reset() {
836 let model = ListModel::from_vec(vec![1, 2, 3]);
837 let changes: Rc<RefCell<Vec<DataChange>>> = Rc::new(RefCell::new(Vec::new()));
838 let c = changes.clone();
839 let _handle = model.observe_changes(move |change| {
840 c.borrow_mut().push(change.clone());
841 });
842
843 model.clear();
844 assert!(model.is_empty());
845 let log = changes.borrow();
846 assert_eq!(log[0], DataChange::Reset);
847 }
848
849 #[test]
850 fn observer_removed_on_handle_drop() {
851 let model = ListModel::new();
852 let count = Rc::new(Cell::new(0));
853 let c = count.clone();
854 let handle = model.observe_changes(move |_| {
855 c.set(c.get() + 1);
856 });
857
858 model.push(1);
859 assert_eq!(count.get(), 1);
860
861 drop(handle);
862 model.push(2);
863 assert_eq!(count.get(), 1); }
865
866 #[test]
867 fn multiple_observers() {
868 let model = ListModel::new();
869 let count = Rc::new(Cell::new(0));
870 let c1 = count.clone();
871 let c2 = count.clone();
872 let _h1 = model.observe_changes(move |_| c1.set(c1.get() + 1));
873 let _h2 = model.observe_changes(move |_| c2.set(c2.get() + 1));
874
875 model.push(42);
876 assert_eq!(count.get(), 2);
877 }
878
879 #[test]
880 fn clone_shares_data() {
881 let model = ListModel::from_vec(vec![1, 2, 3]);
882 let clone = model.clone();
883
884 model.push(4);
885 assert_eq!(clone.len(), 4);
886 assert_eq!(clone.with_item(3, |v| *v), Some(4));
887 }
888
889 #[test]
890 fn clone_shares_observers() {
891 let model = ListModel::from_vec(vec![1, 2]);
892 let count = Rc::new(Cell::new(0));
893 let c = count.clone();
894 let _handle = model.observe_changes(move |_| c.set(c.get() + 1));
895
896 let clone = model.clone();
897 clone.push(3); assert_eq!(count.get(), 1);
899 }
900
901 #[test]
902 fn with_item_out_of_bounds_returns_none() {
903 let model = ListModel::from_vec(vec![1, 2]);
904 assert_eq!(model.with_item(5, |v| *v), None);
905 }
906
907 #[test]
908 #[should_panic]
909 fn remove_out_of_bounds_panics() {
910 let model = ListModel::from_vec(vec![1]);
911 model.remove(5);
912 }
913
914 fn order<T: Clone>(model: &ListModel<T>) -> Vec<T> {
917 (0..model.len())
918 .map(|i| model.with_item(i, |v| v.clone()).unwrap())
919 .collect()
920 }
921
922 #[test]
923 fn list_source_accept_drop_after_reorders() {
924 let model = ListModel::from_vec(vec!["a", "b", "c", "d"]);
926 assert!(model.accept_drop(DropCommit {
927 source: DragSource::SameView { key: 0 },
928 target: 2,
929 position: DropPosition::After,
930 }));
931 assert_eq!(order(&model), vec!["b", "c", "a", "d"]);
932 }
933
934 #[test]
935 fn list_source_accept_drop_before_reorders() {
936 let model = ListModel::from_vec(vec!["a", "b", "c", "d"]);
938 assert!(model.accept_drop(DropCommit {
939 source: DragSource::SameView { key: 3 },
940 target: 1,
941 position: DropPosition::Before,
942 }));
943 assert_eq!(order(&model), vec!["a", "d", "b", "c"]);
944 }
945
946 #[test]
947 fn list_source_can_accept_rejects_into_accepts_sibling() {
948 let model = ListModel::from_vec(vec![1, 2, 3]);
949 assert_eq!(
951 model.can_accept(&DropQuery {
952 source: DragSource::SameView { key: 0 },
953 target: 1,
954 position: DropPosition::Into,
955 }),
956 DropResponse::Reject
957 );
958 assert_eq!(
960 model.can_accept(&DropQuery {
961 source: DragSource::SameView { key: 0 },
962 target: 1,
963 position: DropPosition::After,
964 }),
965 DropResponse::Accept
966 );
967 }
968
969 #[test]
970 fn list_source_key_is_positional_identity() {
971 let model = ListModel::from_vec(vec![10, 20, 30]);
972 assert_eq!(model.key_at(1), Some(1));
973 assert_eq!(model.index_of(&2), Some(2));
974 assert_eq!(model.key_at(5), None);
975 assert_eq!(model.index_of(&9), None);
976 }
977
978 fn snapshot<T: Clone>(model: &ListModel<T>) -> Vec<T> {
979 (0..model.len())
980 .filter_map(|i| model.with_item(i, |v| v.clone()))
981 .collect()
982 }
983
984 #[test]
985 fn move_items_block_move_contiguous_and_ordered() {
986 let model = ListModel::from_vec(vec!['A', 'B', 'C', 'D', 'E', 'F']);
989 model.move_items(&[0, 2, 4], 3);
990 assert_eq!(snapshot(&model), vec!['B', 'A', 'C', 'E', 'D', 'F']);
993 }
994
995 #[test]
996 fn move_items_to_end() {
997 let model = ListModel::from_vec(vec![1, 2, 3, 4]);
998 model.move_items(&[0, 1], 4);
999 assert_eq!(snapshot(&model), vec![3, 4, 1, 2]);
1000 }
1001
1002 #[test]
1003 fn move_items_ignores_out_of_range_and_dedups() {
1004 let model = ListModel::from_vec(vec![1, 2, 3]);
1005 model.move_items(&[1, 1, 9], 3);
1006 assert_eq!(snapshot(&model), vec![1, 3, 2]);
1007 }
1008
1009 #[test]
1010 fn move_items_single_matches_move_item() {
1011 let a = ListModel::from_vec(vec![1, 2, 3, 4, 5]);
1012 let b = ListModel::from_vec(vec![1, 2, 3, 4, 5]);
1013 a.move_items(&[1], 4);
1015 b.move_item(1, 3);
1017 assert_eq!(snapshot(&a), snapshot(&b));
1018 }
1019
1020 #[test]
1021 fn reorder_within_multi_row_lands_contiguously() {
1022 let model = ListModel::from_vec(vec![0, 1, 2, 3, 4, 5]);
1024 assert!(model.reorder_within(&[0, 1], &4, DropPosition::After));
1026 assert_eq!(snapshot(&model), vec![2, 3, 4, 0, 1, 5]);
1027 }
1028
1029 #[test]
1030 fn reorder_within_single_row_uses_accept_drop() {
1031 let model = ListModel::from_vec(vec![10, 20, 30]);
1032 assert!(model.reorder_within(&[0], &2, DropPosition::After));
1033 assert_eq!(snapshot(&model), vec![20, 30, 10]);
1034 }
1035
1036 #[test]
1037 fn reorder_within_into_is_rejected_for_flat_list() {
1038 let model = ListModel::from_vec(vec![1, 2, 3]);
1039 assert!(!model.reorder_within(&[0, 1], &2, DropPosition::Into));
1040 assert_eq!(snapshot(&model), vec![1, 2, 3]);
1041 }
1042
1043 #[test]
1044 fn on_drag_out_removes_the_moved_row() {
1045 let model = ListModel::from_vec(vec!['a', 'b', 'c']);
1046 model.on_drag_out(&1);
1047 assert_eq!(snapshot(&model), vec!['a', 'c']);
1048 }
1049
1050 #[test]
1051 fn move_items_contiguous_emits_moved_non_contiguous_resets() {
1052 let model = ListModel::from_vec(vec![0, 1, 2, 3, 4]);
1053 let log: Rc<RefCell<Vec<DataChange>>> = Rc::new(RefCell::new(Vec::new()));
1054 let l = log.clone();
1055 let _h = model.observe_changes(move |c| l.borrow_mut().push(c.clone()));
1056 assert!(model.move_items(&[1, 2], 5));
1058 assert!(matches!(
1059 log.borrow().last(),
1060 Some(DataChange::ItemsMoved { .. })
1061 ));
1062 log.borrow_mut().clear();
1063 assert!(model.move_items(&[0, 2], 0));
1065 assert!(matches!(log.borrow().last(), Some(DataChange::Reset)));
1066 }
1067
1068 #[test]
1069 fn reorder_within_and_move_items_report_no_move_for_out_of_range() {
1070 let model = ListModel::from_vec(vec![1, 2, 3]);
1071 assert!(!model.reorder_within(&[99, 100], &0, DropPosition::Before));
1072 assert!(!model.move_items(&[99, 100], 0));
1073 assert_eq!(snapshot(&model), vec![1, 2, 3]);
1074 }
1075
1076 #[derive(Debug, Clone, PartialEq, Eq)]
1079 struct Row {
1080 id: u64,
1081 val: &'static str,
1082 }
1083
1084 fn row(id: u64, val: &'static str) -> Row {
1085 Row { id, val }
1086 }
1087
1088 fn key(r: &Row) -> u64 {
1089 r.id
1090 }
1091
1092 fn ids(model: &ListModel<Row>) -> Vec<u64> {
1093 (0..model.len())
1094 .map(|i| model.with_item(i, |r| r.id).unwrap())
1095 .collect()
1096 }
1097
1098 fn vals(model: &ListModel<Row>) -> Vec<&'static str> {
1099 (0..model.len())
1100 .map(|i| model.with_item(i, |r| r.val).unwrap())
1101 .collect()
1102 }
1103
1104 fn record_changes(model: &ListModel<Row>) -> (Rc<RefCell<Vec<DataChange>>>, ObserverHandle) {
1105 let log: Rc<RefCell<Vec<DataChange>>> = Rc::new(RefCell::new(Vec::new()));
1106 let l = log.clone();
1107 let handle = model.observe_changes(move |c| l.borrow_mut().push(c.clone()));
1108 (log, handle)
1109 }
1110
1111 #[test]
1112 fn reconcile_pure_insert_emits_one_coalesced_range() {
1113 let model = ListModel::from_vec(vec![row(1, "a"), row(2, "b")]);
1114 let (log, _h) = record_changes(&model);
1115
1116 model.reconcile_by_key(
1117 vec![row(1, "a"), row(2, "b"), row(3, "c"), row(4, "d")],
1118 key,
1119 );
1120
1121 assert_eq!(ids(&model), vec![1, 2, 3, 4]);
1122 assert_eq!(vals(&model), vec!["a", "b", "c", "d"]);
1123 let entries = log.borrow();
1124 assert_eq!(entries.len(), 1, "contiguous inserts coalesce: {entries:?}");
1125 assert_eq!(entries[0], DataChange::ItemsInserted { range: 2..4 });
1126 }
1127
1128 #[test]
1129 fn reconcile_insert_at_front_and_middle() {
1130 let model = ListModel::from_vec(vec![row(1, "a"), row(2, "b")]);
1131 let (log, _h) = record_changes(&model);
1132
1133 model.reconcile_by_key(
1135 vec![row(0, "z"), row(1, "a"), row(5, "m"), row(2, "b")],
1136 key,
1137 );
1138
1139 assert_eq!(ids(&model), vec![0, 1, 5, 2]);
1140 let entries = log.borrow();
1141 assert_eq!(entries.len(), 2, "{entries:?}");
1144 assert_eq!(entries[0], DataChange::ItemsInserted { range: 0..1 });
1145 assert_eq!(entries[1], DataChange::ItemsInserted { range: 2..3 });
1146 }
1147
1148 #[test]
1149 fn reconcile_pure_remove_emits_one_coalesced_range() {
1150 let model = ListModel::from_vec(vec![row(1, "a"), row(2, "b"), row(3, "c"), row(4, "d")]);
1151 let (log, _h) = record_changes(&model);
1152
1153 model.reconcile_by_key(vec![row(1, "a"), row(4, "d")], key);
1154
1155 assert_eq!(ids(&model), vec![1, 4]);
1156 let entries = log.borrow();
1157 assert_eq!(entries.len(), 1, "contiguous removes coalesce: {entries:?}");
1158 assert_eq!(entries[0], DataChange::ItemsRemoved { range: 1..3 });
1159 }
1160
1161 #[test]
1162 fn reconcile_remove_scattered_emits_separate_ranges() {
1163 let model = ListModel::from_vec(vec![row(1, "a"), row(2, "b"), row(3, "c"), row(4, "d")]);
1164 let (log, _h) = record_changes(&model);
1165
1166 model.reconcile_by_key(vec![row(1, "a"), row(3, "c")], key);
1167
1168 assert_eq!(ids(&model), vec![1, 3]);
1169 let entries = log.borrow();
1170 assert_eq!(entries.len(), 2, "{entries:?}");
1171 assert_eq!(entries[0], DataChange::ItemsRemoved { range: 3..4 });
1173 assert_eq!(entries[1], DataChange::ItemsRemoved { range: 1..2 });
1174 }
1175
1176 #[test]
1177 fn reconcile_reorder_emits_only_moves() {
1178 let model = ListModel::from_vec(vec![row(1, "a"), row(2, "b"), row(3, "c")]);
1179 let (log, _h) = record_changes(&model);
1180
1181 model.reconcile_by_key(vec![row(3, "c"), row(2, "b"), row(1, "a")], key);
1182
1183 assert_eq!(ids(&model), vec![3, 2, 1]);
1184 let entries = log.borrow();
1185 assert!(
1186 entries
1187 .iter()
1188 .all(|c| matches!(c, DataChange::ItemsMoved { .. })),
1189 "a pure reorder must only emit moves: {entries:?}"
1190 );
1191 assert!(!entries.is_empty());
1192 }
1193
1194 #[test]
1195 fn reconcile_no_reorder_when_relative_order_already_matches() {
1196 let model = ListModel::from_vec(vec![row(1, "a"), row(2, "b"), row(3, "c"), row(4, "d")]);
1199 let (log, _h) = record_changes(&model);
1200
1201 model.reconcile_by_key(vec![row(1, "a"), row(3, "c"), row(4, "d")], key);
1202
1203 assert_eq!(ids(&model), vec![1, 3, 4]);
1204 let entries = log.borrow();
1205 assert!(
1206 entries
1207 .iter()
1208 .all(|c| !matches!(c, DataChange::ItemsMoved { .. })),
1209 "untouched relative order must not emit moves: {entries:?}"
1210 );
1211 }
1212
1213 #[test]
1214 fn reconcile_in_place_update_emits_item_updated() {
1215 let model = ListModel::from_vec(vec![row(1, "a"), row(2, "b"), row(3, "c")]);
1216 let (log, _h) = record_changes(&model);
1217
1218 model.reconcile_by_key(vec![row(1, "a"), row(2, "CHANGED"), row(3, "c")], key);
1219
1220 assert_eq!(vals(&model), vec!["a", "CHANGED", "c"]);
1221 let entries = log.borrow();
1222 assert_eq!(entries.len(), 1, "{entries:?}");
1223 assert_eq!(entries[0], DataChange::ItemUpdated { index: 1 });
1224 }
1225
1226 #[test]
1227 fn reconcile_combined_insert_remove_reorder_update() {
1228 let model = ListModel::from_vec(vec![row(1, "a"), row(2, "b"), row(3, "c"), row(4, "d")]);
1235 let (log, _h) = record_changes(&model);
1236
1237 model.reconcile_by_key(
1238 vec![row(4, "d"), row(2, "B!"), row(5, "e"), row(1, "a")],
1239 key,
1240 );
1241
1242 assert_eq!(ids(&model), vec![4, 2, 5, 1]);
1243 assert_eq!(vals(&model), vec!["d", "B!", "e", "a"]);
1244 let entries = log.borrow();
1245 assert!(!entries.is_empty());
1246 assert!(
1247 entries
1248 .iter()
1249 .any(|c| matches!(c, DataChange::ItemsMoved { .. })),
1250 "{entries:?}"
1251 );
1252 assert!(
1253 entries
1254 .iter()
1255 .any(|c| matches!(c, DataChange::ItemsInserted { .. })),
1256 "{entries:?}"
1257 );
1258 assert!(
1259 entries
1260 .iter()
1261 .any(|c| matches!(c, DataChange::ItemsRemoved { .. })),
1262 "{entries:?}"
1263 );
1264 assert!(
1265 entries
1266 .iter()
1267 .any(|c| matches!(c, DataChange::ItemUpdated { .. })),
1268 "{entries:?}"
1269 );
1270 assert!(
1271 !entries.iter().any(|c| matches!(c, DataChange::Reset)),
1272 "{entries:?}"
1273 );
1274 }
1275
1276 #[test]
1277 fn reconcile_emits_in_walk_order_with_sequential_indices() {
1278 let model = ListModel::from_vec(vec![row(1, "a"), row(2, "b")]);
1280 let (log, _h) = record_changes(&model);
1281 model.reconcile_by_key(vec![row(1, "a"), row(2, "B"), row(3, "c")], key);
1282 assert_eq!(
1283 *log.borrow(),
1284 vec![
1285 DataChange::ItemUpdated { index: 1 },
1286 DataChange::ItemsInserted { range: 2..3 },
1287 ]
1288 );
1289
1290 let model = ListModel::from_vec(vec![row(1, "a"), row(2, "b"), row(3, "c"), row(4, "d")]);
1294 let (log, _h) = record_changes(&model);
1295 model.reconcile_by_key(
1296 vec![row(4, "d"), row(9, "n"), row(2, "B"), row(1, "a")],
1297 key,
1298 );
1299 assert_eq!(ids(&model), vec![4, 9, 2, 1]);
1300 assert_eq!(
1301 *log.borrow(),
1302 vec![
1303 DataChange::ItemsRemoved { range: 2..3 },
1305 DataChange::ItemsMoved {
1307 from: 2,
1308 to: 0,
1309 count: 1
1310 },
1311 DataChange::ItemsInserted { range: 1..2 },
1313 DataChange::ItemsMoved {
1315 from: 3,
1316 to: 2,
1317 count: 1
1318 },
1319 DataChange::ItemUpdated { index: 2 },
1320 ]
1321 );
1322 }
1323
1324 #[test]
1325 fn reconcile_empty_to_full() {
1326 let model: ListModel<Row> = ListModel::new();
1327 let (log, _h) = record_changes(&model);
1328
1329 model.reconcile_by_key(vec![row(1, "a"), row(2, "b"), row(3, "c")], key);
1330
1331 assert_eq!(ids(&model), vec![1, 2, 3]);
1332 let entries = log.borrow();
1333 assert_eq!(entries.len(), 1, "{entries:?}");
1334 assert_eq!(entries[0], DataChange::ItemsInserted { range: 0..3 });
1335 }
1336
1337 #[test]
1338 fn reconcile_full_to_empty() {
1339 let model = ListModel::from_vec(vec![row(1, "a"), row(2, "b"), row(3, "c")]);
1340 let (log, _h) = record_changes(&model);
1341
1342 model.reconcile_by_key(vec![], key);
1343
1344 assert!(model.is_empty());
1345 let entries = log.borrow();
1346 assert_eq!(entries.len(), 1, "{entries:?}");
1347 assert_eq!(entries[0], DataChange::ItemsRemoved { range: 0..3 });
1348 }
1349
1350 #[test]
1351 fn reconcile_identical_input_emits_nothing() {
1352 let model = ListModel::from_vec(vec![row(1, "a"), row(2, "b"), row(3, "c")]);
1353 let (log, _h) = record_changes(&model);
1354
1355 model.reconcile_by_key(vec![row(1, "a"), row(2, "b"), row(3, "c")], key);
1356
1357 assert_eq!(ids(&model), vec![1, 2, 3]);
1358 assert!(
1359 log.borrow().is_empty(),
1360 "identical reconcile must not notify: {:?}",
1361 log.borrow()
1362 );
1363 }
1364
1365 #[test]
1366 fn reconcile_never_emits_reset() {
1367 let scenarios: Vec<(Vec<Row>, Vec<Row>)> = vec![
1371 (
1372 vec![row(1, "a"), row(2, "b")],
1373 vec![row(1, "a"), row(2, "b"), row(3, "c")],
1374 ),
1375 (
1376 vec![row(1, "a"), row(2, "b"), row(3, "c")],
1377 vec![row(1, "a")],
1378 ),
1379 (
1380 vec![row(1, "a"), row(2, "b"), row(3, "c")],
1381 vec![row(3, "c"), row(1, "a"), row(2, "b")],
1382 ),
1383 (vec![], vec![row(1, "a"), row(2, "b")]),
1384 (vec![row(1, "a"), row(2, "b")], vec![]),
1385 (
1386 vec![row(1, "a"), row(2, "b"), row(3, "c"), row(4, "d")],
1387 vec![row(9, "x"), row(8, "y"), row(7, "z")],
1388 ),
1389 ];
1390 for (before, after) in scenarios {
1391 let model = ListModel::from_vec(before.clone());
1392 let (log, _h) = record_changes(&model);
1393 model.reconcile_by_key(after.clone(), key);
1394 assert!(
1395 !log.borrow().iter().any(|c| matches!(c, DataChange::Reset)),
1396 "reconcile must never emit Reset — before {before:?}, after {after:?}, got {:?}",
1397 log.borrow()
1398 );
1399 }
1400 }
1401
1402 #[test]
1403 #[should_panic(expected = "reconcile_by_key")]
1404 fn reconcile_duplicate_key_in_new_items_panics() {
1405 let model = ListModel::from_vec(vec![row(1, "a"), row(2, "b")]);
1406 model.reconcile_by_key(vec![row(1, "a"), row(1, "a-dup"), row(2, "b")], key);
1410 }
1411}