1pub mod a11y;
29pub mod biometric;
30pub mod changeset;
31pub mod clipboard;
32pub mod drag_drop;
33pub mod file_drop;
34pub mod focus_cursor;
35pub mod gamepad;
36pub mod geolocation;
37pub mod gesture;
38pub mod gpu_state;
39pub mod hover;
40pub mod keyring;
41pub mod permission;
42pub mod virtual_view;
43pub mod scroll_into_view;
44pub mod scroll_state;
45pub mod selection;
46pub mod sensors;
47pub mod text_edit;
48pub mod text_input;
49pub mod undo_redo;
50
51use alloc::collections::BTreeMap;
52
53use azul_core::dom::{DomId, DomNodeId, NodeId};
54use azul_core::styled_dom::NodeHierarchyItemId;
55
56#[derive(Debug, Clone, Default, PartialEq, Eq)]
72pub struct NodeIdMap {
73 moves: BTreeMap<NodeId, NodeId>,
74}
75
76impl NodeIdMap {
77 #[must_use]
79 pub fn from_node_moves(node_moves: &[azul_core::diff::NodeMove]) -> Self {
80 Self {
81 moves: node_moves
82 .iter()
83 .map(|m| (m.old_node_id, m.new_node_id))
84 .collect(),
85 }
86 }
87
88 #[must_use]
91 pub fn from_pairs<I: IntoIterator<Item = (NodeId, NodeId)>>(pairs: I) -> Self {
92 Self {
93 moves: pairs.into_iter().collect(),
94 }
95 }
96
97 #[must_use]
99 pub fn resolve(&self, old: NodeId) -> Option<NodeId> {
100 self.moves.get(&old).copied()
101 }
102
103 #[must_use]
105 pub fn is_unmounted(&self, old: NodeId) -> bool {
106 !self.moves.contains_key(&old)
107 }
108
109 #[must_use]
112 pub fn resolve_dom_node_id(&self, dom: DomId, id: DomNodeId) -> Option<DomNodeId> {
113 if id.dom != dom {
114 return Some(id);
115 }
116 let old = id.node.into_crate_internal()?;
117 let new = self.resolve(old)?;
118 Some(DomNodeId {
119 dom,
120 node: NodeHierarchyItemId::from_crate_internal(Some(new)),
121 })
122 }
123
124 #[must_use]
127 pub const fn as_btree_map(&self) -> &BTreeMap<NodeId, NodeId> {
128 &self.moves
129 }
130
131 #[must_use]
133 pub fn is_empty(&self) -> bool {
134 self.moves.is_empty()
135 }
136}
137
138pub trait NodeIdRemap {
149 fn remap_node_ids(&mut self, dom: DomId, map: &NodeIdMap);
151}
152
153pub(crate) fn remap_keys<V>(map: &mut BTreeMap<NodeId, V>, node_map: &NodeIdMap) {
155 let old = core::mem::take(map);
156 for (old_id, v) in old {
157 if let Some(new_id) = node_map.resolve(old_id) {
158 map.insert(new_id, v);
159 }
160 }
161}
162
163pub(crate) fn remap_dom_keys<V>(
167 map: &mut BTreeMap<(DomId, NodeId), V>,
168 dom: DomId,
169 node_map: &NodeIdMap,
170) {
171 let old = core::mem::take(map);
172 for ((d, old_id), v) in old {
173 if d != dom {
174 map.insert((d, old_id), v);
175 } else if let Some(new_id) = node_map.resolve(old_id) {
176 map.insert((d, new_id), v);
177 }
178 }
179}
180
181#[cfg(all(test, feature = "std"))]
203mod preceding_sibling_remap_tests {
204 use alloc::collections::BTreeMap;
205
206 use azul_core::{
207 dom::{DomId, DomNodeId, NodeId},
208 drag::{DragContext, DragData},
209 geom::LogicalPosition,
210 hit_test::{FullHitTest, HitTest, HitTestItem},
211 selection::{CursorAffinity, GraphemeClusterId, MultiCursorState, TextCursor},
212 styled_dom::NodeHierarchyItemId,
213 task::{Instant, SystemTick},
214 };
215
216 use super::{
217 changeset::{TextChangeset, TextOpInsertText, TextOperation},
218 focus_cursor::FocusManager,
219 gesture::GestureAndDragManager,
220 gpu_state::GpuStateManager,
221 hover::{HoverManager, InputPointId},
222 scroll_state::ScrollManager,
223 text_edit::TextEditManager,
224 text_input::{TextInputManager, TextInputSource},
225 undo_redo::{NodeStateSnapshot, UndoRedoManager},
226 virtual_view::VirtualViewManager,
227 NodeIdMap, NodeIdRemap,
228 };
229
230 const ROOT: DomId = DomId { inner: 0 };
231 const A: NodeId = NodeId::new(1);
233 const B_OLD: NodeId = NodeId::new(2);
235 const B_NEW: NodeId = NodeId::new(1);
236 const C_OLD: NodeId = NodeId::new(3);
238 const C_NEW: NodeId = NodeId::new(2);
239
240 fn delete_a() -> NodeIdMap {
243 NodeIdMap::from_pairs([
244 (NodeId::new(0), NodeId::new(0)),
245 (B_OLD, B_NEW),
246 (C_OLD, C_NEW),
247 ])
248 }
249
250 fn now() -> Instant {
251 Instant::Tick(SystemTick { tick_counter: 0 })
252 }
253
254 fn dom_node(node: NodeId) -> DomNodeId {
255 DomNodeId {
256 dom: ROOT,
257 node: NodeHierarchyItemId::from_crate_internal(Some(node)),
258 }
259 }
260
261 #[test]
264 fn scroll_offsets_follow_their_node_across_a_preceding_sibling_delete() {
265 let mut m = ScrollManager::new();
266 m.set_scroll_position_unclamped(ROOT, A, LogicalPosition::new(0.0, 10.0), now());
268 m.set_scroll_position_unclamped(ROOT, B_OLD, LogicalPosition::new(0.0, 20.0), now());
269 m.set_scroll_position_unclamped(ROOT, C_OLD, LogicalPosition::new(0.0, 30.0), now());
270
271 m.remap_node_ids(ROOT, &delete_a());
272
273 assert_eq!(
276 m.get_scroll_state(ROOT, C_NEW).map(|s| s.current_offset.y),
277 Some(30.0),
278 "C's scroll offset must follow C to its new NodeId"
279 );
280 assert_eq!(
281 m.get_scroll_state(ROOT, B_NEW).map(|s| s.current_offset.y),
282 Some(20.0),
283 "B's scroll offset must follow B to its new NodeId"
284 );
285 assert!(
287 m.get_scroll_state(ROOT, NodeId::new(3)).is_none(),
288 "no state may remain at a NodeId that no longer exists"
289 );
290 assert_eq!(m.get_scroll_states_for_dom(ROOT).len(), 2, "A's state must be GC'd");
291 }
292
293 fn undo_op(changeset_id: usize, node: NodeId, text: &str) -> super::undo_redo::UndoableOperation {
296 super::undo_redo::UndoableOperation {
297 changeset: TextChangeset {
298 id: changeset_id,
299 target: dom_node(node),
300 operation: TextOperation::InsertText(TextOpInsertText {
301 text: text.into(),
302 position: azul_core::window::CursorPosition::Uninitialized,
303 new_cursor: azul_core::window::CursorPosition::Uninitialized,
304 }),
305 timestamp: now(),
306 },
307 pre_state: NodeStateSnapshot {
308 node_id: node,
309 text_content: text.into(),
310 cursor_position: None.into(),
311 selection_range: None.into(),
312 timestamp: now(),
313 },
314 }
315 }
316
317 #[test]
318 fn undo_history_stays_attached_to_the_same_element() {
319 let mut m = UndoRedoManager::new();
320 let a = undo_op(1, A, "typed-into-A");
321 let b = undo_op(2, B_OLD, "typed-into-B");
322 let c = undo_op(3, C_OLD, "typed-into-C");
323 m.record_operation(a.changeset.clone(), a.pre_state.clone());
324 m.record_operation(b.changeset.clone(), b.pre_state.clone());
325 m.record_operation(c.changeset.clone(), c.pre_state.clone());
326
327 m.remap_node_ids(ROOT, &delete_a());
328
329 let undo_on_c = m.peek_undo(C_NEW).expect("C must still have undo history");
331 assert_eq!(
332 undo_on_c.pre_state.text_content.as_str(),
333 "typed-into-C",
334 "undo on C must revert C's edit — an unremapped Vec re-attaches B's history here"
335 );
336 let undo_on_b = m.peek_undo(B_NEW).expect("B must still have undo history");
337 assert_eq!(undo_on_b.pre_state.text_content.as_str(), "typed-into-B");
338
339 assert_eq!(
341 undo_on_c.changeset.target.node.into_crate_internal(),
342 Some(C_NEW)
343 );
344 assert_eq!(undo_on_c.pre_state.node_id, C_NEW);
345
346 assert_eq!(m.node_stacks.len(), 2, "the deleted node's undo stack must be GC'd");
348 assert!(!m.can_undo(NodeId::new(3)), "no history at a NodeId that no longer exists");
349 }
350
351 #[test]
354 fn virtual_view_nested_doms_stay_with_their_host_node() {
355 let mut m = VirtualViewManager::new();
356 let dom_a = m.get_or_create_nested_dom_id(ROOT, A);
357 let dom_b = m.get_or_create_nested_dom_id(ROOT, B_OLD);
358 let dom_c = m.get_or_create_nested_dom_id(ROOT, C_OLD);
359 assert_ne!(dom_b, dom_c);
360
361 m.remap_node_ids(ROOT, &delete_a());
362
363 assert_eq!(
364 m.get_nested_dom_id(ROOT, C_NEW),
365 Some(dom_c),
366 "C's nested DOM must follow C — otherwise C renders B's virtual view"
367 );
368 assert_eq!(m.get_nested_dom_id(ROOT, B_NEW), Some(dom_b));
369 assert_eq!(m.debug_counts(), 2, "the deleted view's state must be GC'd");
370 assert!(!m.all_view_keys().iter().any(|(_, n)| *n == C_OLD));
371 assert_ne!(m.get_nested_dom_id(ROOT, B_NEW), Some(dom_a));
372 }
373
374 #[test]
377 fn gpu_transform_keys_stay_with_their_node() {
378 use azul_core::resources::{OpacityKey, TransformKey};
379 let mut m = GpuStateManager::default();
380 {
381 let cache = m.get_or_create_cache(ROOT);
382 cache.opacity_keys.insert(A, OpacityKey::unique());
383 cache.current_opacity_values.insert(A, 0.1);
384 cache.current_opacity_values.insert(B_OLD, 0.2);
385 cache.current_opacity_values.insert(C_OLD, 0.3);
386 cache.css_transform_keys.insert(C_OLD, TransformKey::unique());
387 }
388 let c_key = m.get_cache(ROOT).unwrap().css_transform_keys[&C_OLD];
389
390 m.remap_node_ids(ROOT, &delete_a());
391
392 let cache = m.get_cache(ROOT).unwrap();
393 assert_eq!(
394 cache.current_opacity_values.get(&C_NEW).copied(),
395 Some(0.3),
396 "C's opacity must follow C, not be inherited from B"
397 );
398 assert_eq!(cache.current_opacity_values.get(&B_NEW).copied(), Some(0.2));
399 assert_eq!(
400 cache.css_transform_keys.get(&C_NEW).copied(),
401 Some(c_key),
402 "C's GPU transform key must follow C"
403 );
404 assert!(cache.opacity_keys.is_empty(), "the deleted node's GPU keys must be GC'd");
405 assert_eq!(cache.current_opacity_values.len(), 2);
406 }
407
408 #[test]
411 fn focus_follows_its_node_and_is_cleared_when_the_node_dies() {
412 let mut m = FocusManager::new();
413 m.set_focused_node(Some(dom_node(C_OLD)));
414 m.remap_node_ids(ROOT, &delete_a());
415 assert_eq!(
416 m.get_focused_node().and_then(|f| f.node.into_crate_internal()),
417 Some(C_NEW),
418 "focus must follow the focused element, not stay on a recycled index"
419 );
420
421 let mut m = FocusManager::new();
422 m.set_focused_node(Some(dom_node(A)));
423 m.remap_node_ids(ROOT, &delete_a());
424 assert!(
425 m.get_focused_node().is_none(),
426 "focus on an unmounted node must be cleared, never retargeted"
427 );
428 }
429
430 #[test]
433 fn a_live_selection_stays_on_the_edited_element() {
434 let cursor = TextCursor {
435 cluster_id: GraphemeClusterId {
436 source_run: 0,
437 start_byte_in_run: 0,
438 },
439 affinity: CursorAffinity::Leading,
440 };
441 let mut m = TextEditManager::new();
442 m.multi_cursor = Some(MultiCursorState::new_with_cursor(cursor, dom_node(C_OLD), 0));
443
444 m.remap_node_ids(ROOT, &delete_a());
445
446 let mc = m.multi_cursor.as_ref().expect("the editing session survives");
447 assert_eq!(
448 mc.node_id.node.into_crate_internal(),
449 Some(C_NEW),
450 "the caret must stay in the element the user is editing"
451 );
452 assert_eq!(mc.selections.len(), 1, "surviving node keeps its selections");
453
454 let mut m = TextEditManager::new();
456 m.multi_cursor = Some(MultiCursorState::new_with_cursor(cursor, dom_node(A), 0));
457 m.remap_node_ids(ROOT, &delete_a());
458 assert!(m.multi_cursor.is_none(), "editing an unmounted node must end the session");
459 }
460
461 fn node_drag(node: NodeId) -> DragContext {
464 DragContext::node_drag(ROOT, node, LogicalPosition::zero(), DragData::default(), 1)
465 }
466
467 #[test]
468 fn a_live_drag_keeps_dragging_the_same_element() {
469 let mut m = GestureAndDragManager::new();
470 m.active_drag = Some(node_drag(C_OLD));
471
472 m.remap_node_ids(ROOT, &delete_a());
473
474 assert!(
475 m.is_node_dragging(ROOT, C_NEW),
476 "the dragged element must still be the dragged element after the rebuild"
477 );
478 assert!(
479 !m.is_node_dragging(ROOT, B_NEW),
480 "the drag must NOT jump onto the sibling that inherited the old index"
481 );
482
483 let mut m = GestureAndDragManager::new();
485 m.active_drag = Some(node_drag(A));
486 m.remap_node_ids(ROOT, &delete_a());
487 assert!(m.get_drag_context().is_none(), "a drag whose source vanished is cancelled");
488 }
489
490 #[test]
493 fn a_pending_text_edit_is_not_applied_to_the_wrong_node() {
494 let mut m = TextInputManager::new();
495 m.record_input(dom_node(C_OLD), "x".into(), String::new(), TextInputSource::Keyboard);
496 m.remap_node_ids(ROOT, &delete_a());
497 assert_eq!(
498 m.get_pending_changeset()
499 .and_then(|p| p.node.node.into_crate_internal()),
500 Some(C_NEW),
501 "the recorded edit must apply to the node it was recorded on"
502 );
503
504 let mut m = TextInputManager::new();
505 m.record_input(dom_node(A), "x".into(), String::new(), TextInputSource::Keyboard);
506 m.remap_node_ids(ROOT, &delete_a());
507 assert!(
508 m.get_pending_changeset().is_none(),
509 "an edit recorded on an unmounted node must be dropped, not applied elsewhere"
510 );
511 }
512
513 #[test]
516 fn hover_history_hits_follow_their_nodes() {
517 fn hit(depth: u32) -> HitTestItem {
518 HitTestItem {
519 point_in_viewport: LogicalPosition::zero(),
520 point_relative_to_item: LogicalPosition::zero(),
521 is_focusable: false,
522 is_virtual_view_hit: None,
523 hit_depth: depth,
524 }
525 }
526 let mut ht = HitTest::empty();
527 ht.regular_hit_test_nodes.insert(A, hit(1));
528 ht.regular_hit_test_nodes.insert(B_OLD, hit(2));
529 ht.regular_hit_test_nodes.insert(C_OLD, hit(3));
530 let mut full = FullHitTest::empty(None);
531 full.hovered_nodes.insert(ROOT, ht);
532
533 let mut m = HoverManager::new();
534 m.push_hit_test(InputPointId::Mouse, full);
535
536 m.remap_node_ids(ROOT, &delete_a());
537
538 let nodes = &m
539 .get_current(&InputPointId::Mouse)
540 .unwrap()
541 .hovered_nodes[&ROOT]
542 .regular_hit_test_nodes;
543 assert_eq!(
544 nodes.get(&C_NEW).map(|h| h.hit_depth),
545 Some(3),
546 "C's hit must follow C (an unremapped history hands B's hit back for C)"
547 );
548 assert_eq!(nodes.get(&B_NEW).map(|h| h.hit_depth), Some(2));
549 assert_eq!(nodes.len(), 2, "the deleted node's hit must be GC'd");
550 }
551
552 #[test]
555 fn state_belonging_to_another_dom_is_never_touched() {
556 let other = DomId { inner: 7 };
557 let mut m = ScrollManager::new();
558 m.set_scroll_position_unclamped(other, C_OLD, LogicalPosition::new(0.0, 99.0), now());
559 m.remap_node_ids(ROOT, &delete_a());
560 assert_eq!(
561 m.get_scroll_state(other, C_OLD).map(|s| s.current_offset.y),
562 Some(99.0),
563 "a reconciliation of DOM 0 says nothing about DOM 7"
564 );
565
566 let mut vv = VirtualViewManager::new();
567 let nested = vv.get_or_create_nested_dom_id(other, C_OLD);
568 vv.remap_node_ids(ROOT, &delete_a());
569 assert_eq!(vv.get_nested_dom_id(other, C_OLD), Some(nested));
570 }
571
572 #[test]
575 fn node_id_map_semantics() {
576 let map = delete_a();
577 assert_eq!(map.resolve(NodeId::new(0)), Some(NodeId::new(0)));
578 assert_eq!(map.resolve(C_OLD), Some(C_NEW));
579 assert!(map.is_unmounted(A));
580 assert!(map.resolve(A).is_none());
581 let _unused: &BTreeMap<NodeId, NodeId> = map.as_btree_map();
582 }
583}
584
585#[cfg(test)]
589mod autotest_generated {
590 use azul_core::diff::NodeMove;
591
592 use super::*;
593
594 const DOM0: DomId = DomId { inner: 0 };
595 const DOM1: DomId = DomId { inner: 1 };
596 const DOM_MAX: DomId = DomId { inner: usize::MAX };
598
599 fn nid(i: usize) -> NodeId {
600 NodeId::new(i)
601 }
602
603 fn mv(old: usize, new: usize) -> NodeMove {
604 NodeMove {
605 old_node_id: nid(old),
606 new_node_id: nid(new),
607 }
608 }
609
610 const MAX_ENCODABLE: usize = usize::MAX - 1;
615
616 fn dom_node_at(dom: DomId, node: NodeId) -> DomNodeId {
617 DomNodeId {
618 dom,
619 node: NodeHierarchyItemId::from_crate_internal(Some(node)),
620 }
621 }
622
623 #[test]
626 fn from_node_moves_on_an_empty_slice_yields_an_empty_map() {
627 let map = NodeIdMap::from_node_moves(&[]);
628 assert!(map.is_empty());
629 assert!(map.as_btree_map().is_empty());
630 assert_eq!(map, NodeIdMap::default());
631 }
632
633 #[test]
634 fn from_node_moves_agrees_with_from_pairs_on_the_same_data() {
635 let moves = [mv(0, 0), mv(5, 3), mv(9, 9)];
636 let from_moves = NodeIdMap::from_node_moves(&moves);
637 let from_pairs =
638 NodeIdMap::from_pairs([(nid(0), nid(0)), (nid(5), nid(3)), (nid(9), nid(9))]);
639 assert_eq!(
640 from_moves, from_pairs,
641 "the two constructors must produce identical maps for identical data"
642 );
643 }
644
645 #[test]
649 fn from_node_moves_with_a_duplicated_old_id_keeps_the_last_entry() {
650 let map = NodeIdMap::from_node_moves(&[mv(1, 10), mv(1, 20), mv(1, 30)]);
651 assert_eq!(map.as_btree_map().len(), 1, "duplicates collapse to one key");
652 assert_eq!(
653 map.resolve(nid(1)),
654 Some(nid(30)),
655 "the last NodeMove for an old id wins"
656 );
657 }
658
659 #[test]
662 fn from_node_moves_with_a_non_injective_mapping_does_not_panic() {
663 let map = NodeIdMap::from_node_moves(&[mv(1, 7), mv(2, 7)]);
664 assert_eq!(map.as_btree_map().len(), 2, "both old ids are retained as keys");
665 assert_eq!(map.resolve(nid(1)), Some(nid(7)));
666 assert_eq!(map.resolve(nid(2)), Some(nid(7)));
667 }
668
669 #[test]
672 fn from_node_moves_handles_extreme_node_ids() {
673 let map = NodeIdMap::from_node_moves(&[
674 mv(usize::MAX, usize::MAX),
675 mv(usize::MAX - 1, 0),
676 mv(0, usize::MAX),
677 ]);
678 assert_eq!(map.resolve(nid(usize::MAX)), Some(nid(usize::MAX)));
679 assert_eq!(map.resolve(nid(usize::MAX - 1)), Some(nid(0)));
680 assert_eq!(map.resolve(nid(0)), Some(nid(usize::MAX)));
681 assert!(!map.is_empty());
682 }
683
684 #[test]
685 fn from_pairs_on_an_empty_iterator_yields_an_empty_map() {
686 let map = NodeIdMap::from_pairs(Vec::new());
687 assert!(map.is_empty());
688 assert!(map.resolve(NodeId::ZERO).is_none());
689 assert!(map.is_unmounted(NodeId::ZERO));
690 }
691
692 #[test]
693 fn from_pairs_with_a_duplicated_old_id_keeps_the_last_entry() {
694 let map = NodeIdMap::from_pairs([(nid(4), nid(1)), (nid(4), nid(2))]);
695 assert_eq!(map.as_btree_map().len(), 1);
696 assert_eq!(map.resolve(nid(4)), Some(nid(2)));
697 }
698
699 #[test]
703 fn from_pairs_invariants_hold_at_volume() {
704 let n = 2048usize;
705 let pairs: Vec<(NodeId, NodeId)> = (0..n).map(|i| (nid(i), nid(n - 1 - i))).collect();
706 let map = NodeIdMap::from_pairs(pairs);
707
708 assert_eq!(map.as_btree_map().len(), n);
709 assert!(!map.is_empty());
710 for i in 0..n {
711 assert_eq!(map.resolve(nid(i)), Some(nid(n - 1 - i)));
712 assert!(!map.is_unmounted(nid(i)));
713 }
714 assert!(map.resolve(nid(n)).is_none(), "an id never inserted is unmounted");
715 assert!(map.is_unmounted(nid(n)));
716 }
717
718 #[test]
721 fn as_btree_map_round_trips_through_from_pairs() {
722 let original = NodeIdMap::from_pairs([
723 (nid(0), nid(0)),
724 (nid(2), nid(1)),
725 (nid(3), nid(2)),
726 (nid(usize::MAX), nid(4)),
727 ]);
728 let decoded = NodeIdMap::from_pairs(
729 original
730 .as_btree_map()
731 .iter()
732 .map(|(old, new)| (*old, *new))
733 .collect::<Vec<_>>(),
734 );
735 assert_eq!(decoded, original, "encode == decode");
736 assert_eq!(decoded.as_btree_map(), original.as_btree_map());
737 }
738
739 #[test]
740 fn a_default_map_unmounts_everything() {
741 let map = NodeIdMap::default();
742 assert!(map.is_empty());
743 assert!(map.as_btree_map().is_empty());
744 for i in [0usize, 1, 2, 1024, usize::MAX - 1, usize::MAX] {
745 assert!(map.resolve(nid(i)).is_none());
746 assert!(
747 map.is_unmounted(nid(i)),
748 "an empty reconciliation means every old node was unmounted"
749 );
750 }
751 }
752
753 #[test]
758 fn resolve_and_is_unmounted_never_disagree() {
759 let map = NodeIdMap::from_pairs([(nid(0), nid(0)), (nid(2), nid(1)), (nid(3), nid(2))]);
760 for i in [0usize, 1, 2, 3, 4, 100, usize::MAX - 1, usize::MAX] {
761 assert_eq!(
762 map.resolve(nid(i)).is_none(),
763 map.is_unmounted(nid(i)),
764 "is_unmounted({i}) must be exactly !resolve({i}).is_some()"
765 );
766 }
767 }
768
769 #[test]
770 fn resolve_is_pure_repeated_calls_return_the_same_answer() {
771 let map = NodeIdMap::from_pairs([(nid(3), nid(2))]);
772 let first = map.resolve(nid(3));
773 assert_eq!(first, map.resolve(nid(3)));
774 assert_eq!(first, map.resolve(nid(3)));
775 assert_eq!(first, Some(nid(2)));
776 }
777
778 #[test]
782 fn resolve_does_not_chase_chains() {
783 let map = NodeIdMap::from_pairs([(nid(1), nid(2)), (nid(2), nid(3))]);
784 assert_eq!(
785 map.resolve(nid(1)),
786 Some(nid(2)),
787 "resolve must apply exactly one hop"
788 );
789 assert_eq!(map.resolve(nid(2)), Some(nid(3)));
790 }
791
792 #[test]
795 fn an_identity_entry_means_matched_not_unmounted() {
796 let map = NodeIdMap::from_pairs([(nid(7), nid(7))]);
797 assert!(!map.is_unmounted(nid(7)));
798 assert_eq!(map.resolve(nid(7)), Some(nid(7)));
799 assert!(map.is_unmounted(nid(6)));
800 assert!(map.is_unmounted(nid(8)));
801 }
802
803 #[test]
804 fn is_empty_is_exactly_the_btree_maps_emptiness() {
805 let empty = NodeIdMap::from_pairs(Vec::new());
806 assert_eq!(empty.is_empty(), empty.as_btree_map().is_empty());
807 assert!(empty.is_empty());
808
809 let full = NodeIdMap::from_pairs([(nid(0), nid(0))]);
810 assert_eq!(full.is_empty(), full.as_btree_map().is_empty());
811 assert!(!full.is_empty());
812 }
813
814 #[test]
820 fn a_foreign_dom_node_id_is_passed_through_untouched() {
821 let map = NodeIdMap::from_pairs([(nid(2), nid(1))]);
822 let foreign = dom_node_at(DOM1, nid(5));
824 assert_eq!(
825 map.resolve_dom_node_id(DOM0, foreign),
826 Some(foreign),
827 "an id in another DOM must never be dropped by this DOM's reconciliation"
828 );
829 let foreign_colliding = dom_node_at(DOM1, nid(2));
831 assert_eq!(
832 map.resolve_dom_node_id(DOM0, foreign_colliding),
833 Some(foreign_colliding),
834 "a foreign id must not be remapped just because its index appears in the map"
835 );
836 }
837
838 #[test]
839 fn resolve_dom_node_id_rewrites_matched_nodes_and_drops_unmounted_ones() {
840 let map = NodeIdMap::from_pairs([(nid(2), nid(1))]);
841 assert_eq!(
842 map.resolve_dom_node_id(DOM0, dom_node_at(DOM0, nid(2))),
843 Some(dom_node_at(DOM0, nid(1)))
844 );
845 assert_eq!(
846 map.resolve_dom_node_id(DOM0, dom_node_at(DOM0, nid(1))),
847 None,
848 "a node absent from the map is unmounted, so the id must be dropped"
849 );
850 }
851
852 #[test]
856 fn a_none_node_id_is_handled_without_panicking() {
857 let map = NodeIdMap::from_pairs([(nid(0), nid(0))]);
858 let none_here = DomNodeId {
859 dom: DOM0,
860 node: NodeHierarchyItemId::NONE,
861 };
862 assert_eq!(map.resolve_dom_node_id(DOM0, none_here), None);
863
864 let none_elsewhere = DomNodeId {
865 dom: DOM1,
866 node: NodeHierarchyItemId::NONE,
867 };
868 assert_eq!(
869 map.resolve_dom_node_id(DOM0, none_elsewhere),
870 Some(none_elsewhere),
871 "the foreign-DOM pass-through happens before the node is decoded"
872 );
873 }
874
875 #[test]
879 fn resolve_dom_node_id_survives_boundary_ids() {
880 let map = NodeIdMap::from_pairs([
881 (nid(MAX_ENCODABLE), nid(0)),
882 (nid(0), nid(MAX_ENCODABLE)),
883 ]);
884
885 let big_old = dom_node_at(DOM0, nid(MAX_ENCODABLE));
887 assert_eq!(big_old.node.into_raw(), usize::MAX, "1-based encoding is saturated");
888 assert_eq!(
889 map.resolve_dom_node_id(DOM0, big_old),
890 Some(dom_node_at(DOM0, nid(0)))
891 );
892
893 assert_eq!(
895 map.resolve_dom_node_id(DOM0, dom_node_at(DOM0, nid(0))),
896 Some(dom_node_at(DOM0, nid(MAX_ENCODABLE)))
897 );
898
899 let far_dom = dom_node_at(DOM_MAX, nid(0));
901 assert_eq!(
902 map.resolve_dom_node_id(DOM0, far_dom),
903 Some(far_dom),
904 "DomId::MAX is foreign to DOM 0 and passes through"
905 );
906 assert_eq!(
907 map.resolve_dom_node_id(DOM_MAX, far_dom),
908 Some(dom_node_at(DOM_MAX, nid(MAX_ENCODABLE))),
909 "when DomId::MAX *is* the reconciled DOM, its nodes are remapped"
910 );
911 }
912
913 #[test]
914 fn resolve_dom_node_id_on_an_empty_map_drops_own_dom_and_keeps_foreign() {
915 let map = NodeIdMap::default();
916 assert_eq!(map.resolve_dom_node_id(DOM0, dom_node_at(DOM0, nid(0))), None);
917 let foreign = dom_node_at(DOM1, nid(0));
918 assert_eq!(map.resolve_dom_node_id(DOM0, foreign), Some(foreign));
919 }
920
921 #[test]
928 fn remap_keys_handles_a_swap_without_clobbering_payloads() {
929 let mut map: BTreeMap<NodeId, &str> = BTreeMap::new();
930 map.insert(nid(1), "one");
931 map.insert(nid(2), "two");
932 let node_map = NodeIdMap::from_pairs([(nid(1), nid(2)), (nid(2), nid(1))]);
933
934 remap_keys(&mut map, &node_map);
935
936 assert_eq!(map.len(), 2, "a swap must not lose an entry");
937 assert_eq!(map.get(&nid(1)).copied(), Some("two"));
938 assert_eq!(map.get(&nid(2)).copied(), Some("one"));
939 }
940
941 #[test]
944 fn remap_keys_drops_state_for_unmounted_nodes() {
945 let mut map: BTreeMap<NodeId, u32> = BTreeMap::new();
946 map.insert(nid(1), 10);
947 map.insert(nid(2), 20);
948 map.insert(nid(3), 30);
949 let node_map = NodeIdMap::from_pairs([(nid(2), nid(1)), (nid(3), nid(2))]);
950
951 remap_keys(&mut map, &node_map);
952
953 assert_eq!(map.len(), 2, "node 1's state must be GC'd");
954 assert_eq!(map.get(&nid(1)).copied(), Some(20));
955 assert_eq!(map.get(&nid(2)).copied(), Some(30));
956 assert!(!map.contains_key(&nid(3)), "no state may remain at a dead index");
957 }
958
959 #[test]
960 fn remap_keys_with_an_empty_node_map_clears_all_state() {
961 let mut map: BTreeMap<NodeId, u32> = BTreeMap::new();
962 map.insert(nid(0), 1);
963 map.insert(nid(9), 2);
964
965 remap_keys(&mut map, &NodeIdMap::default());
966
967 assert!(
968 map.is_empty(),
969 "an empty reconciliation unmounts everything, so all state is dropped"
970 );
971 }
972
973 #[test]
974 fn remap_keys_with_an_identity_map_is_a_no_op() {
975 let mut map: BTreeMap<NodeId, u32> = (0..64).map(|i| (nid(i), i as u32)).collect();
976 let before = map.clone();
977 let node_map = NodeIdMap::from_pairs((0..64).map(|i| (nid(i), nid(i))));
978
979 remap_keys(&mut map, &node_map);
980
981 assert_eq!(map, before);
982 }
983
984 #[test]
985 fn remap_keys_on_an_empty_map_does_not_panic() {
986 let mut map: BTreeMap<NodeId, u32> = BTreeMap::new();
987 remap_keys(&mut map, &NodeIdMap::from_pairs([(nid(1), nid(0))]));
988 assert!(map.is_empty());
989 }
990
991 #[test]
996 fn remap_keys_collision_is_lossy_but_deterministic() {
997 let mut map: BTreeMap<NodeId, &str> = BTreeMap::new();
998 map.insert(nid(1), "from-1");
999 map.insert(nid(2), "from-2");
1000 let node_map = NodeIdMap::from_pairs([(nid(1), nid(5)), (nid(2), nid(5))]);
1001
1002 remap_keys(&mut map, &node_map);
1003
1004 assert_eq!(map.len(), 1, "two old keys collapsing onto one new key lose one entry");
1005 assert_eq!(
1006 map.get(&nid(5)).copied(),
1007 Some("from-2"),
1008 "the last-visited (highest) old id wins — deterministic, not arbitrary"
1009 );
1010 }
1011
1012 #[test]
1017 fn remap_keys_preserves_payload_identity_under_a_shift_down() {
1018 let n = 256usize;
1019 let mut map: BTreeMap<NodeId, usize> = (0..n).map(|i| (nid(i), i * 1000)).collect();
1020 let node_map = NodeIdMap::from_pairs(
1022 core::iter::once((nid(0), nid(0))).chain((2..n).map(|i| (nid(i), nid(i - 1)))),
1023 );
1024
1025 remap_keys(&mut map, &node_map);
1026
1027 assert_eq!(map.len(), n - 1, "exactly the deleted node's state is GC'd");
1028 assert_eq!(map.get(&nid(0)).copied(), Some(0));
1029 for i in 2..n {
1030 assert_eq!(
1031 map.get(&nid(i - 1)).copied(),
1032 Some(i * 1000),
1033 "node {i}'s payload must follow it to index {}",
1034 i - 1
1035 );
1036 }
1037 assert!(!map.contains_key(&nid(n - 1)), "the vacated tail index is empty");
1038 }
1039
1040 #[test]
1044 fn remap_keys_survivor_moving_onto_a_dead_index_is_not_dropped() {
1045 let mut map: BTreeMap<NodeId, &str> = BTreeMap::new();
1046 map.insert(nid(1), "doomed");
1047 map.insert(nid(2), "survivor");
1048 let node_map = NodeIdMap::from_pairs([(nid(2), nid(1))]);
1050
1051 remap_keys(&mut map, &node_map);
1052
1053 assert_eq!(map.len(), 1);
1054 assert_eq!(
1055 map.get(&nid(1)).copied(),
1056 Some("survivor"),
1057 "the surviving payload occupies the recycled index — the dead one is gone"
1058 );
1059 }
1060
1061 #[test]
1064 fn remap_dom_keys_never_touches_another_dom() {
1065 let mut map: BTreeMap<(DomId, NodeId), u32> = BTreeMap::new();
1066 map.insert((DOM0, nid(2)), 20);
1067 map.insert((DOM1, nid(2)), 99);
1069 map.insert((DOM1, nid(1)), 98);
1071 let node_map = NodeIdMap::from_pairs([(nid(2), nid(1))]);
1072
1073 remap_dom_keys(&mut map, DOM0, &node_map);
1074
1075 assert_eq!(map.get(&(DOM0, nid(1))).copied(), Some(20), "DOM 0's entry is remapped");
1076 assert!(!map.contains_key(&(DOM0, nid(2))), "the old DOM 0 key is gone");
1077 assert_eq!(
1078 map.get(&(DOM1, nid(2))).copied(),
1079 Some(99),
1080 "DOM 1 is untouched by a DOM 0 reconciliation"
1081 );
1082 assert_eq!(map.get(&(DOM1, nid(1))).copied(), Some(98));
1083 assert_eq!(map.len(), 3);
1084 }
1085
1086 #[test]
1087 fn remap_dom_keys_drops_unmounted_entries_of_the_target_dom_only() {
1088 let mut map: BTreeMap<(DomId, NodeId), u32> = BTreeMap::new();
1089 map.insert((DOM0, nid(1)), 10); map.insert((DOM1, nid(1)), 11); let node_map = NodeIdMap::from_pairs([(nid(0), nid(0))]);
1092
1093 remap_dom_keys(&mut map, DOM0, &node_map);
1094
1095 assert!(!map.contains_key(&(DOM0, nid(1))));
1096 assert_eq!(map.get(&(DOM1, nid(1))).copied(), Some(11));
1097 assert_eq!(map.len(), 1);
1098 }
1099
1100 #[test]
1101 fn remap_dom_keys_handles_a_swap_within_the_target_dom() {
1102 let mut map: BTreeMap<(DomId, NodeId), &str> = BTreeMap::new();
1103 map.insert((DOM0, nid(1)), "one");
1104 map.insert((DOM0, nid(2)), "two");
1105 map.insert((DOM1, nid(1)), "other-dom");
1106 let node_map = NodeIdMap::from_pairs([(nid(1), nid(2)), (nid(2), nid(1))]);
1107
1108 remap_dom_keys(&mut map, DOM0, &node_map);
1109
1110 assert_eq!(map.get(&(DOM0, nid(1))).copied(), Some("two"));
1111 assert_eq!(map.get(&(DOM0, nid(2))).copied(), Some("one"));
1112 assert_eq!(map.get(&(DOM1, nid(1))).copied(), Some("other-dom"));
1113 assert_eq!(map.len(), 3, "a swap plus a bystander DOM loses nothing");
1114 }
1115
1116 #[test]
1117 fn remap_dom_keys_with_an_empty_node_map_clears_only_the_target_dom() {
1118 let mut map: BTreeMap<(DomId, NodeId), u32> = BTreeMap::new();
1119 map.insert((DOM0, nid(0)), 1);
1120 map.insert((DOM0, nid(5)), 2);
1121 map.insert((DOM1, nid(0)), 3);
1122
1123 remap_dom_keys(&mut map, DOM0, &NodeIdMap::default());
1124
1125 assert_eq!(map.len(), 1, "every DOM 0 node was unmounted");
1126 assert_eq!(map.get(&(DOM1, nid(0))).copied(), Some(3));
1127 }
1128
1129 #[test]
1130 fn remap_dom_keys_on_an_empty_map_does_not_panic() {
1131 let mut map: BTreeMap<(DomId, NodeId), u32> = BTreeMap::new();
1132 remap_dom_keys(&mut map, DOM0, &NodeIdMap::from_pairs([(nid(1), nid(0))]));
1133 assert!(map.is_empty());
1134 }
1135
1136 #[test]
1137 fn remap_dom_keys_with_a_target_dom_that_has_no_entries_is_a_no_op() {
1138 let mut map: BTreeMap<(DomId, NodeId), u32> = BTreeMap::new();
1139 map.insert((DOM1, nid(1)), 1);
1140 map.insert((DOM_MAX, nid(2)), 2);
1141 let before = map.clone();
1142
1143 remap_dom_keys(&mut map, DOM0, &NodeIdMap::from_pairs([(nid(1), nid(9))]));
1144
1145 assert_eq!(map, before);
1146 }
1147
1148 #[test]
1149 fn remap_dom_keys_handles_boundary_dom_and_node_ids() {
1150 let mut map: BTreeMap<(DomId, NodeId), u32> = BTreeMap::new();
1151 map.insert((DOM_MAX, nid(usize::MAX)), 1);
1152 map.insert((DOM_MAX, nid(usize::MAX - 1)), 2);
1153 map.insert((DOM0, nid(usize::MAX)), 3);
1154 let node_map = NodeIdMap::from_pairs([(nid(usize::MAX), nid(0))]);
1155
1156 remap_dom_keys(&mut map, DOM_MAX, &node_map);
1157
1158 assert_eq!(
1159 map.get(&(DOM_MAX, nid(0))).copied(),
1160 Some(1),
1161 "usize::MAX remaps like any other index"
1162 );
1163 assert!(
1164 !map.contains_key(&(DOM_MAX, nid(usize::MAX - 1))),
1165 "unmounted in DOM_MAX -> dropped"
1166 );
1167 assert_eq!(
1168 map.get(&(DOM0, nid(usize::MAX))).copied(),
1169 Some(3),
1170 "DOM 0 is a bystander here"
1171 );
1172 assert_eq!(map.len(), 2);
1173 }
1174
1175 #[test]
1180 fn remap_dom_keys_with_an_identity_map_is_a_no_op_even_when_repeated() {
1181 let mut map: BTreeMap<(DomId, NodeId), u32> =
1182 (0..32).map(|i| ((DOM0, nid(i)), i as u32)).collect();
1183 let before = map.clone();
1184 let node_map = NodeIdMap::from_pairs((0..32).map(|i| (nid(i), nid(i))));
1185
1186 remap_dom_keys(&mut map, DOM0, &node_map);
1187 remap_dom_keys(&mut map, DOM0, &node_map);
1188
1189 assert_eq!(map, before);
1190 }
1191}