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