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) {
377 let mut changes: Vec<DataChange> = Vec::new();
378 {
379 let mut guard = self.inner.borrow_mut();
380 reconcile_vec(&mut guard.items, new_items, &key_fn, &mut changes);
381 }
382 for change in changes {
383 self.notify(change);
384 }
385 }
386}
387
388fn reconcile_vec<T: PartialEq, K: Eq + Hash>(
395 items: &mut Vec<T>,
396 new_items: Vec<T>,
397 key_fn: &impl Fn(&T) -> K,
398 out: &mut Vec<DataChange>,
399) {
400 let new_pairs: Vec<(K, T)> = new_items.into_iter().map(|it| (key_fn(&it), it)).collect();
404 let new_key_set: HashSet<&K> = new_pairs.iter().map(|(k, _)| k).collect();
405
406 let old_keys: Vec<K> = items.iter().map(key_fn).collect();
411 let remove_idxs: Vec<usize> = (0..items.len())
412 .filter(|&i| !new_key_set.contains(&old_keys[i]))
413 .collect();
414 for range in coalesce_ranges(&remove_idxs).into_iter().rev() {
415 items.drain(range.clone());
416 out.push(DataChange::ItemsRemoved { range });
417 }
418
419 let remaining_keys: HashSet<K> = items.iter().map(key_fn).collect();
422
423 let mut cursor = 0usize;
429 let mut pending_inserts: Vec<T> = Vec::new();
430
431 for (key, new_item) in new_pairs {
432 if remaining_keys.contains(&key) {
433 flush_inserts(items, &mut cursor, &mut pending_inserts, out);
434
435 let pos = items
439 .iter()
440 .skip(cursor)
441 .position(|it| key_fn(it) == key)
442 .expect(
443 "reconcile_by_key: key reported common but not found — \
444 key_fn must be stable and keys unique",
445 );
446 let actual = cursor + pos;
447 if actual != cursor {
448 let val = items.remove(actual);
449 items.insert(cursor, val);
450 out.push(DataChange::ItemsMoved {
451 from: actual,
452 to: cursor,
453 count: 1,
454 });
455 }
456 if items[cursor] != new_item {
457 items[cursor] = new_item;
458 out.push(DataChange::ItemUpdated { index: cursor });
459 }
460 cursor += 1;
461 } else {
462 pending_inserts.push(new_item);
463 }
464 }
465 flush_inserts(items, &mut cursor, &mut pending_inserts, out);
466}
467
468fn flush_inserts<T>(
472 items: &mut Vec<T>,
473 cursor: &mut usize,
474 pending: &mut Vec<T>,
475 out: &mut Vec<DataChange>,
476) {
477 if pending.is_empty() {
478 return;
479 }
480 let start = *cursor;
481 let n = pending.len();
482 items.splice(start..start, pending.drain(..));
483 out.push(DataChange::ItemsInserted {
484 range: start..start + n,
485 });
486 *cursor += n;
487}
488
489fn coalesce_ranges(sorted_idxs: &[usize]) -> Vec<Range<usize>> {
492 let mut ranges = Vec::new();
493 let mut iter = sorted_idxs.iter().peekable();
494 while let Some(&start) = iter.next() {
495 let mut end = start + 1;
496 while iter.peek().is_some_and(|&&n| n == end) {
497 end += 1;
498 iter.next();
499 }
500 ranges.push(start..end);
501 }
502 ranges
503}
504
505impl<T: std::fmt::Debug + 'static> ListModel<T> {
506 pub fn debug_named(self, _name: impl Into<String>) -> Self {
514 #[cfg(debug_assertions)]
515 {
516 let weak = Rc::downgrade(&self.inner);
517 let adapter: Rc<dyn crate::debug_registry::ModelDebug> =
518 Rc::new(ListModelDebug::<T> { weak });
519 let name = _name.into();
520 crate::debug_registry::register(name, Rc::downgrade(&adapter));
521 self.inner.borrow_mut().debug_adapter = Some(adapter);
522 }
523 self
524 }
525}
526
527#[cfg(debug_assertions)]
528struct ListModelDebug<T> {
529 weak: std::rc::Weak<RefCell<ListModelInner<T>>>,
530}
531
532#[cfg(debug_assertions)]
533impl<T: std::fmt::Debug + 'static> crate::debug_registry::ModelDebug for ListModelDebug<T> {
534 fn kind(&self) -> &'static str {
535 "ListModel"
536 }
537 fn len(&self) -> usize {
538 self.weak
539 .upgrade()
540 .map(|inner| inner.borrow().items.len())
541 .unwrap_or(0)
542 }
543 fn debug_dump(&self, out: &mut dyn std::fmt::Write) {
544 let Some(inner) = self.weak.upgrade() else {
545 return;
546 };
547 let guard = inner.borrow();
548 for (i, item) in guard.items.iter().enumerate() {
549 let _ = writeln!(out, "[{}] {:?}", i, item);
550 }
551 }
552}
553
554impl<T: 'static> Default for ListModel<T> {
555 fn default() -> Self {
556 Self::new()
557 }
558}
559
560impl<T: 'static> Clone for ListModel<T> {
561 fn clone(&self) -> Self {
562 Self {
563 inner: self.inner.clone(),
564 }
565 }
566}
567
568impl<T: std::fmt::Debug + 'static> std::fmt::Debug for ListModel<T> {
569 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
570 f.debug_struct("ListModel")
571 .field("len", &self.inner.borrow().items.len())
572 .finish()
573 }
574}
575
576impl<T: 'static> ListDataSource for ListModel<T> {
581 type Item = T;
582 type Key = usize;
583
584 fn len(&self) -> usize {
585 ListModel::len(self)
586 }
587
588 fn with_item<R>(&self, index: usize, f: impl FnOnce(&T) -> R) -> Option<R> {
589 ListModel::with_item(self, index, f)
590 }
591
592 fn key_at(&self, index: usize) -> Option<usize> {
593 (index < ListModel::len(self)).then_some(index)
594 }
595
596 fn index_of(&self, key: &usize) -> Option<usize> {
597 (*key < ListModel::len(self)).then_some(*key)
598 }
599
600 fn observe_changes(&self, f: impl Fn(&DataChange) + 'static) -> ObserverHandle {
601 ListModel::observe_changes(self, f)
602 }
603
604 fn drag(&self, _key: &usize) -> DragEligibility {
605 DragEligibility::CanDrag
606 }
607
608 fn can_accept(&self, query: &DropQuery<'_, usize>) -> DropResponse {
609 match &query.source {
610 DragSource::SameView { .. } => match query.position {
611 DropPosition::Into => DropResponse::Reject,
612 DropPosition::Before | DropPosition::After => DropResponse::Accept,
613 },
614 DragSource::Foreign { .. } => DropResponse::Reject,
615 }
616 }
617
618 fn accept_drop(&self, commit: DropCommit<'_, usize>) -> bool {
619 let DragSource::SameView { key: from } = commit.source else {
620 return false;
621 };
622 let len = ListModel::len(self);
623 if from >= len {
624 return false;
625 }
626 let target = commit.target;
627 let shift = if from < target { 1 } else { 0 };
630 let to = match commit.position {
631 DropPosition::Before => target.saturating_sub(shift),
632 DropPosition::After => (target + 1).saturating_sub(shift),
633 DropPosition::Into => return false,
634 };
635 let to = to.min(len.saturating_sub(1));
636 self.move_item(from, to);
637 true
638 }
639
640 fn reorder_within(&self, sources: &[usize], target: &usize, position: DropPosition) -> bool {
641 if sources.len() <= 1 {
646 let Some(&from) = sources.first() else {
647 return false;
648 };
649 return self.accept_drop(DropCommit {
650 source: DragSource::SameView { key: from },
651 target: *target,
652 position,
653 });
654 }
655 let gap = match position {
656 DropPosition::Before => *target,
657 DropPosition::After => *target + 1,
658 DropPosition::Into => return false,
659 };
660 self.move_items(sources, gap)
661 }
662
663 fn on_drag_out(&self, key: &usize) {
664 if *key < ListModel::len(self) {
668 let _ = self.remove(*key);
669 }
670 }
671}
672
673#[cfg(test)]
674mod tests {
675 use std::cell::Cell;
676
677 use super::*;
678
679 #[test]
680 fn new_is_empty() {
681 let model: ListModel<String> = ListModel::new();
682 assert!(model.is_empty());
683 assert_eq!(model.len(), 0);
684 }
685
686 #[test]
687 fn from_vec() {
688 let model = ListModel::from_vec(vec![10, 20, 30]);
689 assert_eq!(model.len(), 3);
690 assert_eq!(model.with_item(1, |v| *v), Some(20));
691 }
692
693 #[test]
694 fn push_emits_inserted() {
695 let model = ListModel::new();
696 let changes: Rc<RefCell<Vec<DataChange>>> = Rc::new(RefCell::new(Vec::new()));
697 let c = changes.clone();
698 let _handle = model.observe_changes(move |change| {
699 c.borrow_mut().push(change.clone());
700 });
701
702 model.push("a");
703 model.push("b");
704
705 let log = changes.borrow();
706 assert_eq!(log.len(), 2);
707 assert_eq!(log[0], DataChange::ItemsInserted { range: 0..1 });
708 assert_eq!(log[1], DataChange::ItemsInserted { range: 1..2 });
709 }
710
711 #[test]
712 fn insert_emits_inserted() {
713 let model = ListModel::from_vec(vec![1, 2, 3]);
714 let changes: Rc<RefCell<Vec<DataChange>>> = Rc::new(RefCell::new(Vec::new()));
715 let c = changes.clone();
716 let _handle = model.observe_changes(move |change| {
717 c.borrow_mut().push(change.clone());
718 });
719
720 model.insert(1, 99);
721
722 assert_eq!(model.len(), 4);
723 assert_eq!(model.with_item(1, |v| *v), Some(99));
724 let log = changes.borrow();
725 assert_eq!(log.len(), 1, "insert should emit exactly one change");
726 assert_eq!(log[0], DataChange::ItemsInserted { range: 1..2 });
727 }
728
729 #[test]
730 fn remove_emits_removed() {
731 let model = ListModel::from_vec(vec!["a", "b", "c"]);
732 let changes: Rc<RefCell<Vec<DataChange>>> = Rc::new(RefCell::new(Vec::new()));
733 let c = changes.clone();
734 let _handle = model.observe_changes(move |change| {
735 c.borrow_mut().push(change.clone());
736 });
737
738 let removed = model.remove(1);
739 assert_eq!(removed, "b");
740 assert_eq!(model.len(), 2);
741 let log = changes.borrow();
742 assert_eq!(log.len(), 1, "remove should emit exactly one change");
743 assert_eq!(log[0], DataChange::ItemsRemoved { range: 1..2 });
744 }
745
746 #[test]
747 fn set_emits_updated() {
748 let model = ListModel::from_vec(vec![10, 20, 30]);
749 let changes: Rc<RefCell<Vec<DataChange>>> = Rc::new(RefCell::new(Vec::new()));
750 let c = changes.clone();
751 let _handle = model.observe_changes(move |change| {
752 c.borrow_mut().push(change.clone());
753 });
754
755 model.set(2, 99);
756 assert_eq!(model.with_item(2, |v| *v), Some(99));
757 let log = changes.borrow();
758 assert_eq!(log.len(), 1, "set should emit exactly one change");
759 assert_eq!(log[0], DataChange::ItemUpdated { index: 2 });
760 }
761
762 #[test]
763 fn move_item_emits_moved() {
764 let model = ListModel::from_vec(vec!["a", "b", "c", "d"]);
765 let changes: Rc<RefCell<Vec<DataChange>>> = Rc::new(RefCell::new(Vec::new()));
766 let c = changes.clone();
767 let _handle = model.observe_changes(move |change| {
768 c.borrow_mut().push(change.clone());
769 });
770
771 model.move_item(0, 2);
772 assert_eq!(model.with_item(0, |v| *v), Some("b"));
774 assert_eq!(model.with_item(2, |v| *v), Some("a"));
775 let log = changes.borrow();
776 assert_eq!(log.len(), 1, "move_item should emit exactly one change");
777 assert_eq!(
778 log[0],
779 DataChange::ItemsMoved {
780 from: 0,
781 to: 2,
782 count: 1
783 }
784 );
785 }
786
787 #[test]
788 fn move_item_same_index_is_noop() {
789 let model = ListModel::from_vec(vec![1, 2, 3]);
790 let count = Rc::new(Cell::new(0));
791 let c = count.clone();
792 let _handle = model.observe_changes(move |_| {
793 c.set(c.get() + 1);
794 });
795
796 model.move_item(1, 1);
797 assert_eq!(count.get(), 0);
798 }
799
800 #[test]
801 fn replace_all_emits_reset() {
802 let model = ListModel::from_vec(vec![1, 2]);
803 let changes: Rc<RefCell<Vec<DataChange>>> = Rc::new(RefCell::new(Vec::new()));
804 let c = changes.clone();
805 let _handle = model.observe_changes(move |change| {
806 c.borrow_mut().push(change.clone());
807 });
808
809 model.replace_all(vec![10, 20, 30]);
810 assert_eq!(model.len(), 3);
811 let log = changes.borrow();
812 assert_eq!(log[0], DataChange::Reset);
813 }
814
815 #[test]
816 fn clear_emits_reset() {
817 let model = ListModel::from_vec(vec![1, 2, 3]);
818 let changes: Rc<RefCell<Vec<DataChange>>> = Rc::new(RefCell::new(Vec::new()));
819 let c = changes.clone();
820 let _handle = model.observe_changes(move |change| {
821 c.borrow_mut().push(change.clone());
822 });
823
824 model.clear();
825 assert!(model.is_empty());
826 let log = changes.borrow();
827 assert_eq!(log[0], DataChange::Reset);
828 }
829
830 #[test]
831 fn observer_removed_on_handle_drop() {
832 let model = ListModel::new();
833 let count = Rc::new(Cell::new(0));
834 let c = count.clone();
835 let handle = model.observe_changes(move |_| {
836 c.set(c.get() + 1);
837 });
838
839 model.push(1);
840 assert_eq!(count.get(), 1);
841
842 drop(handle);
843 model.push(2);
844 assert_eq!(count.get(), 1); }
846
847 #[test]
848 fn multiple_observers() {
849 let model = ListModel::new();
850 let count = Rc::new(Cell::new(0));
851 let c1 = count.clone();
852 let c2 = count.clone();
853 let _h1 = model.observe_changes(move |_| c1.set(c1.get() + 1));
854 let _h2 = model.observe_changes(move |_| c2.set(c2.get() + 1));
855
856 model.push(42);
857 assert_eq!(count.get(), 2);
858 }
859
860 #[test]
861 fn clone_shares_data() {
862 let model = ListModel::from_vec(vec![1, 2, 3]);
863 let clone = model.clone();
864
865 model.push(4);
866 assert_eq!(clone.len(), 4);
867 assert_eq!(clone.with_item(3, |v| *v), Some(4));
868 }
869
870 #[test]
871 fn clone_shares_observers() {
872 let model = ListModel::from_vec(vec![1, 2]);
873 let count = Rc::new(Cell::new(0));
874 let c = count.clone();
875 let _handle = model.observe_changes(move |_| c.set(c.get() + 1));
876
877 let clone = model.clone();
878 clone.push(3); assert_eq!(count.get(), 1);
880 }
881
882 #[test]
883 fn with_item_out_of_bounds_returns_none() {
884 let model = ListModel::from_vec(vec![1, 2]);
885 assert_eq!(model.with_item(5, |v| *v), None);
886 }
887
888 #[test]
889 #[should_panic]
890 fn remove_out_of_bounds_panics() {
891 let model = ListModel::from_vec(vec![1]);
892 model.remove(5);
893 }
894
895 fn order<T: Clone>(model: &ListModel<T>) -> Vec<T> {
898 (0..model.len())
899 .map(|i| model.with_item(i, |v| v.clone()).unwrap())
900 .collect()
901 }
902
903 #[test]
904 fn list_source_accept_drop_after_reorders() {
905 let model = ListModel::from_vec(vec!["a", "b", "c", "d"]);
907 assert!(model.accept_drop(DropCommit {
908 source: DragSource::SameView { key: 0 },
909 target: 2,
910 position: DropPosition::After,
911 }));
912 assert_eq!(order(&model), vec!["b", "c", "a", "d"]);
913 }
914
915 #[test]
916 fn list_source_accept_drop_before_reorders() {
917 let model = ListModel::from_vec(vec!["a", "b", "c", "d"]);
919 assert!(model.accept_drop(DropCommit {
920 source: DragSource::SameView { key: 3 },
921 target: 1,
922 position: DropPosition::Before,
923 }));
924 assert_eq!(order(&model), vec!["a", "d", "b", "c"]);
925 }
926
927 #[test]
928 fn list_source_can_accept_rejects_into_accepts_sibling() {
929 let model = ListModel::from_vec(vec![1, 2, 3]);
930 assert_eq!(
932 model.can_accept(&DropQuery {
933 source: DragSource::SameView { key: 0 },
934 target: 1,
935 position: DropPosition::Into,
936 }),
937 DropResponse::Reject
938 );
939 assert_eq!(
941 model.can_accept(&DropQuery {
942 source: DragSource::SameView { key: 0 },
943 target: 1,
944 position: DropPosition::After,
945 }),
946 DropResponse::Accept
947 );
948 }
949
950 #[test]
951 fn list_source_key_is_positional_identity() {
952 let model = ListModel::from_vec(vec![10, 20, 30]);
953 assert_eq!(model.key_at(1), Some(1));
954 assert_eq!(model.index_of(&2), Some(2));
955 assert_eq!(model.key_at(5), None);
956 assert_eq!(model.index_of(&9), None);
957 }
958
959 fn snapshot<T: Clone>(model: &ListModel<T>) -> Vec<T> {
960 (0..model.len())
961 .filter_map(|i| model.with_item(i, |v| v.clone()))
962 .collect()
963 }
964
965 #[test]
966 fn move_items_block_move_contiguous_and_ordered() {
967 let model = ListModel::from_vec(vec!['A', 'B', 'C', 'D', 'E', 'F']);
970 model.move_items(&[0, 2, 4], 3);
971 assert_eq!(snapshot(&model), vec!['B', 'A', 'C', 'E', 'D', 'F']);
974 }
975
976 #[test]
977 fn move_items_to_end() {
978 let model = ListModel::from_vec(vec![1, 2, 3, 4]);
979 model.move_items(&[0, 1], 4);
980 assert_eq!(snapshot(&model), vec![3, 4, 1, 2]);
981 }
982
983 #[test]
984 fn move_items_ignores_out_of_range_and_dedups() {
985 let model = ListModel::from_vec(vec![1, 2, 3]);
986 model.move_items(&[1, 1, 9], 3);
987 assert_eq!(snapshot(&model), vec![1, 3, 2]);
988 }
989
990 #[test]
991 fn move_items_single_matches_move_item() {
992 let a = ListModel::from_vec(vec![1, 2, 3, 4, 5]);
993 let b = ListModel::from_vec(vec![1, 2, 3, 4, 5]);
994 a.move_items(&[1], 4);
996 b.move_item(1, 3);
998 assert_eq!(snapshot(&a), snapshot(&b));
999 }
1000
1001 #[test]
1002 fn reorder_within_multi_row_lands_contiguously() {
1003 let model = ListModel::from_vec(vec![0, 1, 2, 3, 4, 5]);
1005 assert!(model.reorder_within(&[0, 1], &4, DropPosition::After));
1007 assert_eq!(snapshot(&model), vec![2, 3, 4, 0, 1, 5]);
1008 }
1009
1010 #[test]
1011 fn reorder_within_single_row_uses_accept_drop() {
1012 let model = ListModel::from_vec(vec![10, 20, 30]);
1013 assert!(model.reorder_within(&[0], &2, DropPosition::After));
1014 assert_eq!(snapshot(&model), vec![20, 30, 10]);
1015 }
1016
1017 #[test]
1018 fn reorder_within_into_is_rejected_for_flat_list() {
1019 let model = ListModel::from_vec(vec![1, 2, 3]);
1020 assert!(!model.reorder_within(&[0, 1], &2, DropPosition::Into));
1021 assert_eq!(snapshot(&model), vec![1, 2, 3]);
1022 }
1023
1024 #[test]
1025 fn on_drag_out_removes_the_moved_row() {
1026 let model = ListModel::from_vec(vec!['a', 'b', 'c']);
1027 model.on_drag_out(&1);
1028 assert_eq!(snapshot(&model), vec!['a', 'c']);
1029 }
1030
1031 #[test]
1032 fn move_items_contiguous_emits_moved_non_contiguous_resets() {
1033 let model = ListModel::from_vec(vec![0, 1, 2, 3, 4]);
1034 let log: Rc<RefCell<Vec<DataChange>>> = Rc::new(RefCell::new(Vec::new()));
1035 let l = log.clone();
1036 let _h = model.observe_changes(move |c| l.borrow_mut().push(c.clone()));
1037 assert!(model.move_items(&[1, 2], 5));
1039 assert!(matches!(
1040 log.borrow().last(),
1041 Some(DataChange::ItemsMoved { .. })
1042 ));
1043 log.borrow_mut().clear();
1044 assert!(model.move_items(&[0, 2], 0));
1046 assert!(matches!(log.borrow().last(), Some(DataChange::Reset)));
1047 }
1048
1049 #[test]
1050 fn reorder_within_and_move_items_report_no_move_for_out_of_range() {
1051 let model = ListModel::from_vec(vec![1, 2, 3]);
1052 assert!(!model.reorder_within(&[99, 100], &0, DropPosition::Before));
1053 assert!(!model.move_items(&[99, 100], 0));
1054 assert_eq!(snapshot(&model), vec![1, 2, 3]);
1055 }
1056
1057 #[derive(Debug, Clone, PartialEq, Eq)]
1060 struct Row {
1061 id: u64,
1062 val: &'static str,
1063 }
1064
1065 fn row(id: u64, val: &'static str) -> Row {
1066 Row { id, val }
1067 }
1068
1069 fn key(r: &Row) -> u64 {
1070 r.id
1071 }
1072
1073 fn ids(model: &ListModel<Row>) -> Vec<u64> {
1074 (0..model.len())
1075 .map(|i| model.with_item(i, |r| r.id).unwrap())
1076 .collect()
1077 }
1078
1079 fn vals(model: &ListModel<Row>) -> Vec<&'static str> {
1080 (0..model.len())
1081 .map(|i| model.with_item(i, |r| r.val).unwrap())
1082 .collect()
1083 }
1084
1085 fn record_changes(model: &ListModel<Row>) -> (Rc<RefCell<Vec<DataChange>>>, ObserverHandle) {
1086 let log: Rc<RefCell<Vec<DataChange>>> = Rc::new(RefCell::new(Vec::new()));
1087 let l = log.clone();
1088 let handle = model.observe_changes(move |c| l.borrow_mut().push(c.clone()));
1089 (log, handle)
1090 }
1091
1092 #[test]
1093 fn reconcile_pure_insert_emits_one_coalesced_range() {
1094 let model = ListModel::from_vec(vec![row(1, "a"), row(2, "b")]);
1095 let (log, _h) = record_changes(&model);
1096
1097 model.reconcile_by_key(
1098 vec![row(1, "a"), row(2, "b"), row(3, "c"), row(4, "d")],
1099 key,
1100 );
1101
1102 assert_eq!(ids(&model), vec![1, 2, 3, 4]);
1103 assert_eq!(vals(&model), vec!["a", "b", "c", "d"]);
1104 let entries = log.borrow();
1105 assert_eq!(entries.len(), 1, "contiguous inserts coalesce: {entries:?}");
1106 assert_eq!(entries[0], DataChange::ItemsInserted { range: 2..4 });
1107 }
1108
1109 #[test]
1110 fn reconcile_insert_at_front_and_middle() {
1111 let model = ListModel::from_vec(vec![row(1, "a"), row(2, "b")]);
1112 let (log, _h) = record_changes(&model);
1113
1114 model.reconcile_by_key(
1116 vec![row(0, "z"), row(1, "a"), row(5, "m"), row(2, "b")],
1117 key,
1118 );
1119
1120 assert_eq!(ids(&model), vec![0, 1, 5, 2]);
1121 let entries = log.borrow();
1122 assert_eq!(entries.len(), 2, "{entries:?}");
1125 assert_eq!(entries[0], DataChange::ItemsInserted { range: 0..1 });
1126 assert_eq!(entries[1], DataChange::ItemsInserted { range: 2..3 });
1127 }
1128
1129 #[test]
1130 fn reconcile_pure_remove_emits_one_coalesced_range() {
1131 let model = ListModel::from_vec(vec![row(1, "a"), row(2, "b"), row(3, "c"), row(4, "d")]);
1132 let (log, _h) = record_changes(&model);
1133
1134 model.reconcile_by_key(vec![row(1, "a"), row(4, "d")], key);
1135
1136 assert_eq!(ids(&model), vec![1, 4]);
1137 let entries = log.borrow();
1138 assert_eq!(entries.len(), 1, "contiguous removes coalesce: {entries:?}");
1139 assert_eq!(entries[0], DataChange::ItemsRemoved { range: 1..3 });
1140 }
1141
1142 #[test]
1143 fn reconcile_remove_scattered_emits_separate_ranges() {
1144 let model = ListModel::from_vec(vec![row(1, "a"), row(2, "b"), row(3, "c"), row(4, "d")]);
1145 let (log, _h) = record_changes(&model);
1146
1147 model.reconcile_by_key(vec![row(1, "a"), row(3, "c")], key);
1148
1149 assert_eq!(ids(&model), vec![1, 3]);
1150 let entries = log.borrow();
1151 assert_eq!(entries.len(), 2, "{entries:?}");
1152 assert_eq!(entries[0], DataChange::ItemsRemoved { range: 3..4 });
1154 assert_eq!(entries[1], DataChange::ItemsRemoved { range: 1..2 });
1155 }
1156
1157 #[test]
1158 fn reconcile_reorder_emits_only_moves() {
1159 let model = ListModel::from_vec(vec![row(1, "a"), row(2, "b"), row(3, "c")]);
1160 let (log, _h) = record_changes(&model);
1161
1162 model.reconcile_by_key(vec![row(3, "c"), row(2, "b"), row(1, "a")], key);
1163
1164 assert_eq!(ids(&model), vec![3, 2, 1]);
1165 let entries = log.borrow();
1166 assert!(
1167 entries
1168 .iter()
1169 .all(|c| matches!(c, DataChange::ItemsMoved { .. })),
1170 "a pure reorder must only emit moves: {entries:?}"
1171 );
1172 assert!(!entries.is_empty());
1173 }
1174
1175 #[test]
1176 fn reconcile_no_reorder_when_relative_order_already_matches() {
1177 let model = ListModel::from_vec(vec![row(1, "a"), row(2, "b"), row(3, "c"), row(4, "d")]);
1180 let (log, _h) = record_changes(&model);
1181
1182 model.reconcile_by_key(vec![row(1, "a"), row(3, "c"), row(4, "d")], key);
1183
1184 assert_eq!(ids(&model), vec![1, 3, 4]);
1185 let entries = log.borrow();
1186 assert!(
1187 entries
1188 .iter()
1189 .all(|c| !matches!(c, DataChange::ItemsMoved { .. })),
1190 "untouched relative order must not emit moves: {entries:?}"
1191 );
1192 }
1193
1194 #[test]
1195 fn reconcile_in_place_update_emits_item_updated() {
1196 let model = ListModel::from_vec(vec![row(1, "a"), row(2, "b"), row(3, "c")]);
1197 let (log, _h) = record_changes(&model);
1198
1199 model.reconcile_by_key(vec![row(1, "a"), row(2, "CHANGED"), row(3, "c")], key);
1200
1201 assert_eq!(vals(&model), vec!["a", "CHANGED", "c"]);
1202 let entries = log.borrow();
1203 assert_eq!(entries.len(), 1, "{entries:?}");
1204 assert_eq!(entries[0], DataChange::ItemUpdated { index: 1 });
1205 }
1206
1207 #[test]
1208 fn reconcile_combined_insert_remove_reorder_update() {
1209 let model = ListModel::from_vec(vec![row(1, "a"), row(2, "b"), row(3, "c"), row(4, "d")]);
1216 let (log, _h) = record_changes(&model);
1217
1218 model.reconcile_by_key(
1219 vec![row(4, "d"), row(2, "B!"), row(5, "e"), row(1, "a")],
1220 key,
1221 );
1222
1223 assert_eq!(ids(&model), vec![4, 2, 5, 1]);
1224 assert_eq!(vals(&model), vec!["d", "B!", "e", "a"]);
1225 let entries = log.borrow();
1226 assert!(!entries.is_empty());
1227 assert!(
1228 entries
1229 .iter()
1230 .any(|c| matches!(c, DataChange::ItemsMoved { .. })),
1231 "{entries:?}"
1232 );
1233 assert!(
1234 entries
1235 .iter()
1236 .any(|c| matches!(c, DataChange::ItemsInserted { .. })),
1237 "{entries:?}"
1238 );
1239 assert!(
1240 entries
1241 .iter()
1242 .any(|c| matches!(c, DataChange::ItemsRemoved { .. })),
1243 "{entries:?}"
1244 );
1245 assert!(
1246 entries
1247 .iter()
1248 .any(|c| matches!(c, DataChange::ItemUpdated { .. })),
1249 "{entries:?}"
1250 );
1251 assert!(
1252 !entries.iter().any(|c| matches!(c, DataChange::Reset)),
1253 "{entries:?}"
1254 );
1255 }
1256
1257 #[test]
1258 fn reconcile_empty_to_full() {
1259 let model: ListModel<Row> = ListModel::new();
1260 let (log, _h) = record_changes(&model);
1261
1262 model.reconcile_by_key(vec![row(1, "a"), row(2, "b"), row(3, "c")], key);
1263
1264 assert_eq!(ids(&model), vec![1, 2, 3]);
1265 let entries = log.borrow();
1266 assert_eq!(entries.len(), 1, "{entries:?}");
1267 assert_eq!(entries[0], DataChange::ItemsInserted { range: 0..3 });
1268 }
1269
1270 #[test]
1271 fn reconcile_full_to_empty() {
1272 let model = ListModel::from_vec(vec![row(1, "a"), row(2, "b"), row(3, "c")]);
1273 let (log, _h) = record_changes(&model);
1274
1275 model.reconcile_by_key(vec![], key);
1276
1277 assert!(model.is_empty());
1278 let entries = log.borrow();
1279 assert_eq!(entries.len(), 1, "{entries:?}");
1280 assert_eq!(entries[0], DataChange::ItemsRemoved { range: 0..3 });
1281 }
1282
1283 #[test]
1284 fn reconcile_identical_input_emits_nothing() {
1285 let model = ListModel::from_vec(vec![row(1, "a"), row(2, "b"), row(3, "c")]);
1286 let (log, _h) = record_changes(&model);
1287
1288 model.reconcile_by_key(vec![row(1, "a"), row(2, "b"), row(3, "c")], key);
1289
1290 assert_eq!(ids(&model), vec![1, 2, 3]);
1291 assert!(
1292 log.borrow().is_empty(),
1293 "identical reconcile must not notify: {:?}",
1294 log.borrow()
1295 );
1296 }
1297
1298 #[test]
1299 fn reconcile_never_emits_reset() {
1300 let scenarios: Vec<(Vec<Row>, Vec<Row>)> = vec![
1304 (
1305 vec![row(1, "a"), row(2, "b")],
1306 vec![row(1, "a"), row(2, "b"), row(3, "c")],
1307 ),
1308 (
1309 vec![row(1, "a"), row(2, "b"), row(3, "c")],
1310 vec![row(1, "a")],
1311 ),
1312 (
1313 vec![row(1, "a"), row(2, "b"), row(3, "c")],
1314 vec![row(3, "c"), row(1, "a"), row(2, "b")],
1315 ),
1316 (vec![], vec![row(1, "a"), row(2, "b")]),
1317 (vec![row(1, "a"), row(2, "b")], vec![]),
1318 (
1319 vec![row(1, "a"), row(2, "b"), row(3, "c"), row(4, "d")],
1320 vec![row(9, "x"), row(8, "y"), row(7, "z")],
1321 ),
1322 ];
1323 for (before, after) in scenarios {
1324 let model = ListModel::from_vec(before.clone());
1325 let (log, _h) = record_changes(&model);
1326 model.reconcile_by_key(after.clone(), key);
1327 assert!(
1328 !log.borrow().iter().any(|c| matches!(c, DataChange::Reset)),
1329 "reconcile must never emit Reset — before {before:?}, after {after:?}, got {:?}",
1330 log.borrow()
1331 );
1332 }
1333 }
1334
1335 #[test]
1336 #[should_panic(expected = "reconcile_by_key")]
1337 fn reconcile_duplicate_key_in_new_items_panics() {
1338 let model = ListModel::from_vec(vec![row(1, "a"), row(2, "b")]);
1339 model.reconcile_by_key(vec![row(1, "a"), row(1, "a-dup"), row(2, "b")], key);
1343 }
1344}