pub mod a11y;
pub mod biometric;
pub mod changeset;
pub mod clipboard;
pub mod drag_drop;
pub mod file_drop;
pub mod focus_cursor;
pub mod gamepad;
pub mod geolocation;
pub mod gesture;
pub mod gpu_state;
pub mod hover;
pub mod keyring;
pub mod permission;
pub mod virtual_view;
pub mod scroll_into_view;
pub mod scroll_state;
pub mod selection;
pub mod sensors;
pub mod text_edit;
pub mod text_input;
pub mod undo_redo;
use alloc::collections::BTreeMap;
use azul_core::dom::{DomId, DomNodeId, NodeId};
use azul_core::styled_dom::NodeHierarchyItemId;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct NodeIdMap {
moves: BTreeMap<NodeId, NodeId>,
}
impl NodeIdMap {
#[must_use]
pub fn from_node_moves(node_moves: &[azul_core::diff::NodeMove]) -> Self {
Self {
moves: node_moves
.iter()
.map(|m| (m.old_node_id, m.new_node_id))
.collect(),
}
}
#[must_use]
pub fn from_pairs<I: IntoIterator<Item = (NodeId, NodeId)>>(pairs: I) -> Self {
Self {
moves: pairs.into_iter().collect(),
}
}
#[must_use]
pub fn resolve(&self, old: NodeId) -> Option<NodeId> {
self.moves.get(&old).copied()
}
#[must_use]
pub fn is_unmounted(&self, old: NodeId) -> bool {
!self.moves.contains_key(&old)
}
#[must_use]
pub fn resolve_dom_node_id(&self, dom: DomId, id: DomNodeId) -> Option<DomNodeId> {
if id.dom != dom {
return Some(id);
}
let old = id.node.into_crate_internal()?;
let new = self.resolve(old)?;
Some(DomNodeId {
dom,
node: NodeHierarchyItemId::from_crate_internal(Some(new)),
})
}
#[must_use]
pub const fn as_btree_map(&self) -> &BTreeMap<NodeId, NodeId> {
&self.moves
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.moves.is_empty()
}
}
pub trait NodeIdRemap {
fn remap_node_ids(&mut self, dom: DomId, map: &NodeIdMap);
}
pub(crate) fn remap_keys<V>(map: &mut BTreeMap<NodeId, V>, node_map: &NodeIdMap) {
let old = core::mem::take(map);
for (old_id, v) in old {
if let Some(new_id) = node_map.resolve(old_id) {
map.insert(new_id, v);
}
}
}
pub(crate) fn remap_dom_keys<V>(
map: &mut BTreeMap<(DomId, NodeId), V>,
dom: DomId,
node_map: &NodeIdMap,
) {
let old = core::mem::take(map);
for ((d, old_id), v) in old {
if d != dom {
map.insert((d, old_id), v);
} else if let Some(new_id) = node_map.resolve(old_id) {
map.insert((d, new_id), v);
}
}
}
#[cfg(all(test, feature = "std"))]
mod preceding_sibling_remap_tests {
use alloc::collections::BTreeMap;
use azul_core::{
dom::{DomId, DomNodeId, NodeId},
drag::{DragContext, DragData},
geom::LogicalPosition,
hit_test::{FullHitTest, HitTest, HitTestItem},
selection::{CursorAffinity, GraphemeClusterId, MultiCursorState, TextCursor},
styled_dom::NodeHierarchyItemId,
task::{Instant, SystemTick},
};
use super::{
changeset::{TextChangeset, TextOpInsertText, TextOperation},
focus_cursor::FocusManager,
gesture::GestureAndDragManager,
gpu_state::GpuStateManager,
hover::{HoverManager, InputPointId},
scroll_state::ScrollManager,
text_edit::TextEditManager,
text_input::{TextInputManager, TextInputSource},
undo_redo::{NodeStateSnapshot, UndoRedoManager},
virtual_view::VirtualViewManager,
NodeIdMap, NodeIdRemap,
};
const ROOT: DomId = DomId { inner: 0 };
const A: NodeId = NodeId::new(1);
const B_OLD: NodeId = NodeId::new(2);
const B_NEW: NodeId = NodeId::new(1);
const C_OLD: NodeId = NodeId::new(3);
const C_NEW: NodeId = NodeId::new(2);
fn delete_a() -> NodeIdMap {
NodeIdMap::from_pairs([
(NodeId::new(0), NodeId::new(0)),
(B_OLD, B_NEW),
(C_OLD, C_NEW),
])
}
fn now() -> Instant {
Instant::Tick(SystemTick { tick_counter: 0 })
}
fn dom_node(node: NodeId) -> DomNodeId {
DomNodeId {
dom: ROOT,
node: NodeHierarchyItemId::from_crate_internal(Some(node)),
}
}
#[test]
fn scroll_offsets_follow_their_node_across_a_preceding_sibling_delete() {
let mut m = ScrollManager::new();
m.set_scroll_position_unclamped(ROOT, A, LogicalPosition::new(0.0, 10.0), now());
m.set_scroll_position_unclamped(ROOT, B_OLD, LogicalPosition::new(0.0, 20.0), now());
m.set_scroll_position_unclamped(ROOT, C_OLD, LogicalPosition::new(0.0, 30.0), now());
m.remap_node_ids(ROOT, &delete_a());
assert_eq!(
m.get_scroll_state(ROOT, C_NEW).map(|s| s.current_offset.y),
Some(30.0),
"C's scroll offset must follow C to its new NodeId"
);
assert_eq!(
m.get_scroll_state(ROOT, B_NEW).map(|s| s.current_offset.y),
Some(20.0),
"B's scroll offset must follow B to its new NodeId"
);
assert!(
m.get_scroll_state(ROOT, NodeId::new(3)).is_none(),
"no state may remain at a NodeId that no longer exists"
);
assert_eq!(m.get_scroll_states_for_dom(ROOT).len(), 2, "A's state must be GC'd");
}
fn undo_op(changeset_id: usize, node: NodeId, text: &str) -> super::undo_redo::UndoableOperation {
super::undo_redo::UndoableOperation {
changeset: TextChangeset {
id: changeset_id,
target: dom_node(node),
operation: TextOperation::InsertText(TextOpInsertText {
text: text.into(),
position: azul_core::window::CursorPosition::Uninitialized,
new_cursor: azul_core::window::CursorPosition::Uninitialized,
}),
timestamp: now(),
},
pre_state: NodeStateSnapshot {
node_id: node,
text_content: text.into(),
cursor_position: None.into(),
selection_range: None.into(),
timestamp: now(),
},
}
}
#[test]
fn undo_history_stays_attached_to_the_same_element() {
let mut m = UndoRedoManager::new();
let a = undo_op(1, A, "typed-into-A");
let b = undo_op(2, B_OLD, "typed-into-B");
let c = undo_op(3, C_OLD, "typed-into-C");
m.record_operation(a.changeset.clone(), a.pre_state.clone());
m.record_operation(b.changeset.clone(), b.pre_state.clone());
m.record_operation(c.changeset.clone(), c.pre_state.clone());
m.remap_node_ids(ROOT, &delete_a());
let undo_on_c = m.peek_undo(C_NEW).expect("C must still have undo history");
assert_eq!(
undo_on_c.pre_state.text_content.as_str(),
"typed-into-C",
"undo on C must revert C's edit — an unremapped Vec re-attaches B's history here"
);
let undo_on_b = m.peek_undo(B_NEW).expect("B must still have undo history");
assert_eq!(undo_on_b.pre_state.text_content.as_str(), "typed-into-B");
assert_eq!(
undo_on_c.changeset.target.node.into_crate_internal(),
Some(C_NEW)
);
assert_eq!(undo_on_c.pre_state.node_id, C_NEW);
assert_eq!(m.node_stacks.len(), 2, "the deleted node's undo stack must be GC'd");
assert!(!m.can_undo(NodeId::new(3)), "no history at a NodeId that no longer exists");
}
#[test]
fn virtual_view_nested_doms_stay_with_their_host_node() {
let mut m = VirtualViewManager::new();
let dom_a = m.get_or_create_nested_dom_id(ROOT, A);
let dom_b = m.get_or_create_nested_dom_id(ROOT, B_OLD);
let dom_c = m.get_or_create_nested_dom_id(ROOT, C_OLD);
assert_ne!(dom_b, dom_c);
m.remap_node_ids(ROOT, &delete_a());
assert_eq!(
m.get_nested_dom_id(ROOT, C_NEW),
Some(dom_c),
"C's nested DOM must follow C — otherwise C renders B's virtual view"
);
assert_eq!(m.get_nested_dom_id(ROOT, B_NEW), Some(dom_b));
assert_eq!(m.debug_counts(), 2, "the deleted view's state must be GC'd");
assert!(!m.all_view_keys().iter().any(|(_, n)| *n == C_OLD));
assert_ne!(m.get_nested_dom_id(ROOT, B_NEW), Some(dom_a));
}
#[test]
fn gpu_transform_keys_stay_with_their_node() {
use azul_core::resources::{OpacityKey, TransformKey};
let mut m = GpuStateManager::default();
{
let cache = m.get_or_create_cache(ROOT);
cache.opacity_keys.insert(A, OpacityKey::unique());
cache.current_opacity_values.insert(A, 0.1);
cache.current_opacity_values.insert(B_OLD, 0.2);
cache.current_opacity_values.insert(C_OLD, 0.3);
cache.css_transform_keys.insert(C_OLD, TransformKey::unique());
}
let c_key = m.get_cache(ROOT).unwrap().css_transform_keys[&C_OLD];
m.remap_node_ids(ROOT, &delete_a());
let cache = m.get_cache(ROOT).unwrap();
assert_eq!(
cache.current_opacity_values.get(&C_NEW).copied(),
Some(0.3),
"C's opacity must follow C, not be inherited from B"
);
assert_eq!(cache.current_opacity_values.get(&B_NEW).copied(), Some(0.2));
assert_eq!(
cache.css_transform_keys.get(&C_NEW).copied(),
Some(c_key),
"C's GPU transform key must follow C"
);
assert!(cache.opacity_keys.is_empty(), "the deleted node's GPU keys must be GC'd");
assert_eq!(cache.current_opacity_values.len(), 2);
}
#[test]
fn focus_follows_its_node_and_is_cleared_when_the_node_dies() {
let mut m = FocusManager::new();
m.set_focused_node(Some(dom_node(C_OLD)));
m.remap_node_ids(ROOT, &delete_a());
assert_eq!(
m.get_focused_node().and_then(|f| f.node.into_crate_internal()),
Some(C_NEW),
"focus must follow the focused element, not stay on a recycled index"
);
let mut m = FocusManager::new();
m.set_focused_node(Some(dom_node(A)));
m.remap_node_ids(ROOT, &delete_a());
assert!(
m.get_focused_node().is_none(),
"focus on an unmounted node must be cleared, never retargeted"
);
}
#[test]
fn a_live_selection_stays_on_the_edited_element() {
let cursor = TextCursor {
cluster_id: GraphemeClusterId {
source_run: 0,
start_byte_in_run: 0,
},
affinity: CursorAffinity::Leading,
};
let mut m = TextEditManager::new();
m.multi_cursor = Some(MultiCursorState::new_with_cursor(cursor, dom_node(C_OLD), 0));
m.remap_node_ids(ROOT, &delete_a());
let mc = m.multi_cursor.as_ref().expect("the editing session survives");
assert_eq!(
mc.node_id.node.into_crate_internal(),
Some(C_NEW),
"the caret must stay in the element the user is editing"
);
assert_eq!(mc.selections.len(), 1, "surviving node keeps its selections");
let mut m = TextEditManager::new();
m.multi_cursor = Some(MultiCursorState::new_with_cursor(cursor, dom_node(A), 0));
m.remap_node_ids(ROOT, &delete_a());
assert!(m.multi_cursor.is_none(), "editing an unmounted node must end the session");
}
fn node_drag(node: NodeId) -> DragContext {
DragContext::node_drag(ROOT, node, LogicalPosition::zero(), DragData::default(), 1)
}
#[test]
fn a_live_drag_keeps_dragging_the_same_element() {
let mut m = GestureAndDragManager::new();
m.active_drag = Some(node_drag(C_OLD));
m.remap_node_ids(ROOT, &delete_a());
assert!(
m.is_node_dragging(ROOT, C_NEW),
"the dragged element must still be the dragged element after the rebuild"
);
assert!(
!m.is_node_dragging(ROOT, B_NEW),
"the drag must NOT jump onto the sibling that inherited the old index"
);
let mut m = GestureAndDragManager::new();
m.active_drag = Some(node_drag(A));
m.remap_node_ids(ROOT, &delete_a());
assert!(m.get_drag_context().is_none(), "a drag whose source vanished is cancelled");
}
#[test]
fn a_pending_text_edit_is_not_applied_to_the_wrong_node() {
let mut m = TextInputManager::new();
m.record_input(dom_node(C_OLD), "x".into(), String::new(), TextInputSource::Keyboard);
m.remap_node_ids(ROOT, &delete_a());
assert_eq!(
m.get_pending_changeset()
.and_then(|p| p.node.node.into_crate_internal()),
Some(C_NEW),
"the recorded edit must apply to the node it was recorded on"
);
let mut m = TextInputManager::new();
m.record_input(dom_node(A), "x".into(), String::new(), TextInputSource::Keyboard);
m.remap_node_ids(ROOT, &delete_a());
assert!(
m.get_pending_changeset().is_none(),
"an edit recorded on an unmounted node must be dropped, not applied elsewhere"
);
}
#[test]
fn hover_history_hits_follow_their_nodes() {
fn hit(depth: u32) -> HitTestItem {
HitTestItem {
point_in_viewport: LogicalPosition::zero(),
point_relative_to_item: LogicalPosition::zero(),
is_focusable: false,
is_virtual_view_hit: None,
hit_depth: depth,
}
}
let mut ht = HitTest::empty();
ht.regular_hit_test_nodes.insert(A, hit(1));
ht.regular_hit_test_nodes.insert(B_OLD, hit(2));
ht.regular_hit_test_nodes.insert(C_OLD, hit(3));
let mut full = FullHitTest::empty(None);
full.hovered_nodes.insert(ROOT, ht);
let mut m = HoverManager::new();
m.push_hit_test(InputPointId::Mouse, full);
m.remap_node_ids(ROOT, &delete_a());
let nodes = &m
.get_current(&InputPointId::Mouse)
.unwrap()
.hovered_nodes[&ROOT]
.regular_hit_test_nodes;
assert_eq!(
nodes.get(&C_NEW).map(|h| h.hit_depth),
Some(3),
"C's hit must follow C (an unremapped history hands B's hit back for C)"
);
assert_eq!(nodes.get(&B_NEW).map(|h| h.hit_depth), Some(2));
assert_eq!(nodes.len(), 2, "the deleted node's hit must be GC'd");
}
#[test]
fn state_belonging_to_another_dom_is_never_touched() {
let other = DomId { inner: 7 };
let mut m = ScrollManager::new();
m.set_scroll_position_unclamped(other, C_OLD, LogicalPosition::new(0.0, 99.0), now());
m.remap_node_ids(ROOT, &delete_a());
assert_eq!(
m.get_scroll_state(other, C_OLD).map(|s| s.current_offset.y),
Some(99.0),
"a reconciliation of DOM 0 says nothing about DOM 7"
);
let mut vv = VirtualViewManager::new();
let nested = vv.get_or_create_nested_dom_id(other, C_OLD);
vv.remap_node_ids(ROOT, &delete_a());
assert_eq!(vv.get_nested_dom_id(other, C_OLD), Some(nested));
}
#[test]
fn node_id_map_semantics() {
let map = delete_a();
assert_eq!(map.resolve(NodeId::new(0)), Some(NodeId::new(0)));
assert_eq!(map.resolve(C_OLD), Some(C_NEW));
assert!(map.is_unmounted(A));
assert!(map.resolve(A).is_none());
let _unused: &BTreeMap<NodeId, NodeId> = map.as_btree_map();
}
}
#[cfg(test)]
mod autotest_generated {
use azul_core::diff::NodeMove;
use super::*;
const DOM0: DomId = DomId { inner: 0 };
const DOM1: DomId = DomId { inner: 1 };
const DOM_MAX: DomId = DomId { inner: usize::MAX };
fn nid(i: usize) -> NodeId {
NodeId::new(i)
}
fn mv(old: usize, new: usize) -> NodeMove {
NodeMove {
old_node_id: nid(old),
new_node_id: nid(new),
}
}
const MAX_ENCODABLE: usize = usize::MAX - 1;
fn dom_node_at(dom: DomId, node: NodeId) -> DomNodeId {
DomNodeId {
dom,
node: NodeHierarchyItemId::from_crate_internal(Some(node)),
}
}
#[test]
fn from_node_moves_on_an_empty_slice_yields_an_empty_map() {
let map = NodeIdMap::from_node_moves(&[]);
assert!(map.is_empty());
assert!(map.as_btree_map().is_empty());
assert_eq!(map, NodeIdMap::default());
}
#[test]
fn from_node_moves_agrees_with_from_pairs_on_the_same_data() {
let moves = [mv(0, 0), mv(5, 3), mv(9, 9)];
let from_moves = NodeIdMap::from_node_moves(&moves);
let from_pairs =
NodeIdMap::from_pairs([(nid(0), nid(0)), (nid(5), nid(3)), (nid(9), nid(9))]);
assert_eq!(
from_moves, from_pairs,
"the two constructors must produce identical maps for identical data"
);
}
#[test]
fn from_node_moves_with_a_duplicated_old_id_keeps_the_last_entry() {
let map = NodeIdMap::from_node_moves(&[mv(1, 10), mv(1, 20), mv(1, 30)]);
assert_eq!(map.as_btree_map().len(), 1, "duplicates collapse to one key");
assert_eq!(
map.resolve(nid(1)),
Some(nid(30)),
"the last NodeMove for an old id wins"
);
}
#[test]
fn from_node_moves_with_a_non_injective_mapping_does_not_panic() {
let map = NodeIdMap::from_node_moves(&[mv(1, 7), mv(2, 7)]);
assert_eq!(map.as_btree_map().len(), 2, "both old ids are retained as keys");
assert_eq!(map.resolve(nid(1)), Some(nid(7)));
assert_eq!(map.resolve(nid(2)), Some(nid(7)));
}
#[test]
fn from_node_moves_handles_extreme_node_ids() {
let map = NodeIdMap::from_node_moves(&[
mv(usize::MAX, usize::MAX),
mv(usize::MAX - 1, 0),
mv(0, usize::MAX),
]);
assert_eq!(map.resolve(nid(usize::MAX)), Some(nid(usize::MAX)));
assert_eq!(map.resolve(nid(usize::MAX - 1)), Some(nid(0)));
assert_eq!(map.resolve(nid(0)), Some(nid(usize::MAX)));
assert!(!map.is_empty());
}
#[test]
fn from_pairs_on_an_empty_iterator_yields_an_empty_map() {
let map = NodeIdMap::from_pairs(Vec::new());
assert!(map.is_empty());
assert!(map.resolve(NodeId::ZERO).is_none());
assert!(map.is_unmounted(NodeId::ZERO));
}
#[test]
fn from_pairs_with_a_duplicated_old_id_keeps_the_last_entry() {
let map = NodeIdMap::from_pairs([(nid(4), nid(1)), (nid(4), nid(2))]);
assert_eq!(map.as_btree_map().len(), 1);
assert_eq!(map.resolve(nid(4)), Some(nid(2)));
}
#[test]
fn from_pairs_invariants_hold_at_volume() {
let n = 2048usize;
let pairs: Vec<(NodeId, NodeId)> = (0..n).map(|i| (nid(i), nid(n - 1 - i))).collect();
let map = NodeIdMap::from_pairs(pairs);
assert_eq!(map.as_btree_map().len(), n);
assert!(!map.is_empty());
for i in 0..n {
assert_eq!(map.resolve(nid(i)), Some(nid(n - 1 - i)));
assert!(!map.is_unmounted(nid(i)));
}
assert!(map.resolve(nid(n)).is_none(), "an id never inserted is unmounted");
assert!(map.is_unmounted(nid(n)));
}
#[test]
fn as_btree_map_round_trips_through_from_pairs() {
let original = NodeIdMap::from_pairs([
(nid(0), nid(0)),
(nid(2), nid(1)),
(nid(3), nid(2)),
(nid(usize::MAX), nid(4)),
]);
let decoded = NodeIdMap::from_pairs(
original
.as_btree_map()
.iter()
.map(|(old, new)| (*old, *new))
.collect::<Vec<_>>(),
);
assert_eq!(decoded, original, "encode == decode");
assert_eq!(decoded.as_btree_map(), original.as_btree_map());
}
#[test]
fn a_default_map_unmounts_everything() {
let map = NodeIdMap::default();
assert!(map.is_empty());
assert!(map.as_btree_map().is_empty());
for i in [0usize, 1, 2, 1024, usize::MAX - 1, usize::MAX] {
assert!(map.resolve(nid(i)).is_none());
assert!(
map.is_unmounted(nid(i)),
"an empty reconciliation means every old node was unmounted"
);
}
}
#[test]
fn resolve_and_is_unmounted_never_disagree() {
let map = NodeIdMap::from_pairs([(nid(0), nid(0)), (nid(2), nid(1)), (nid(3), nid(2))]);
for i in [0usize, 1, 2, 3, 4, 100, usize::MAX - 1, usize::MAX] {
assert_eq!(
map.resolve(nid(i)).is_none(),
map.is_unmounted(nid(i)),
"is_unmounted({i}) must be exactly !resolve({i}).is_some()"
);
}
}
#[test]
fn resolve_is_pure_repeated_calls_return_the_same_answer() {
let map = NodeIdMap::from_pairs([(nid(3), nid(2))]);
let first = map.resolve(nid(3));
assert_eq!(first, map.resolve(nid(3)));
assert_eq!(first, map.resolve(nid(3)));
assert_eq!(first, Some(nid(2)));
}
#[test]
fn resolve_does_not_chase_chains() {
let map = NodeIdMap::from_pairs([(nid(1), nid(2)), (nid(2), nid(3))]);
assert_eq!(
map.resolve(nid(1)),
Some(nid(2)),
"resolve must apply exactly one hop"
);
assert_eq!(map.resolve(nid(2)), Some(nid(3)));
}
#[test]
fn an_identity_entry_means_matched_not_unmounted() {
let map = NodeIdMap::from_pairs([(nid(7), nid(7))]);
assert!(!map.is_unmounted(nid(7)));
assert_eq!(map.resolve(nid(7)), Some(nid(7)));
assert!(map.is_unmounted(nid(6)));
assert!(map.is_unmounted(nid(8)));
}
#[test]
fn is_empty_is_exactly_the_btree_maps_emptiness() {
let empty = NodeIdMap::from_pairs(Vec::new());
assert_eq!(empty.is_empty(), empty.as_btree_map().is_empty());
assert!(empty.is_empty());
let full = NodeIdMap::from_pairs([(nid(0), nid(0))]);
assert_eq!(full.is_empty(), full.as_btree_map().is_empty());
assert!(!full.is_empty());
}
#[test]
fn a_foreign_dom_node_id_is_passed_through_untouched() {
let map = NodeIdMap::from_pairs([(nid(2), nid(1))]);
let foreign = dom_node_at(DOM1, nid(5));
assert_eq!(
map.resolve_dom_node_id(DOM0, foreign),
Some(foreign),
"an id in another DOM must never be dropped by this DOM's reconciliation"
);
let foreign_colliding = dom_node_at(DOM1, nid(2));
assert_eq!(
map.resolve_dom_node_id(DOM0, foreign_colliding),
Some(foreign_colliding),
"a foreign id must not be remapped just because its index appears in the map"
);
}
#[test]
fn resolve_dom_node_id_rewrites_matched_nodes_and_drops_unmounted_ones() {
let map = NodeIdMap::from_pairs([(nid(2), nid(1))]);
assert_eq!(
map.resolve_dom_node_id(DOM0, dom_node_at(DOM0, nid(2))),
Some(dom_node_at(DOM0, nid(1)))
);
assert_eq!(
map.resolve_dom_node_id(DOM0, dom_node_at(DOM0, nid(1))),
None,
"a node absent from the map is unmounted, so the id must be dropped"
);
}
#[test]
fn a_none_node_id_is_handled_without_panicking() {
let map = NodeIdMap::from_pairs([(nid(0), nid(0))]);
let none_here = DomNodeId {
dom: DOM0,
node: NodeHierarchyItemId::NONE,
};
assert_eq!(map.resolve_dom_node_id(DOM0, none_here), None);
let none_elsewhere = DomNodeId {
dom: DOM1,
node: NodeHierarchyItemId::NONE,
};
assert_eq!(
map.resolve_dom_node_id(DOM0, none_elsewhere),
Some(none_elsewhere),
"the foreign-DOM pass-through happens before the node is decoded"
);
}
#[test]
fn resolve_dom_node_id_survives_boundary_ids() {
let map = NodeIdMap::from_pairs([
(nid(MAX_ENCODABLE), nid(0)),
(nid(0), nid(MAX_ENCODABLE)),
]);
let big_old = dom_node_at(DOM0, nid(MAX_ENCODABLE));
assert_eq!(big_old.node.into_raw(), usize::MAX, "1-based encoding is saturated");
assert_eq!(
map.resolve_dom_node_id(DOM0, big_old),
Some(dom_node_at(DOM0, nid(0)))
);
assert_eq!(
map.resolve_dom_node_id(DOM0, dom_node_at(DOM0, nid(0))),
Some(dom_node_at(DOM0, nid(MAX_ENCODABLE)))
);
let far_dom = dom_node_at(DOM_MAX, nid(0));
assert_eq!(
map.resolve_dom_node_id(DOM0, far_dom),
Some(far_dom),
"DomId::MAX is foreign to DOM 0 and passes through"
);
assert_eq!(
map.resolve_dom_node_id(DOM_MAX, far_dom),
Some(dom_node_at(DOM_MAX, nid(MAX_ENCODABLE))),
"when DomId::MAX *is* the reconciled DOM, its nodes are remapped"
);
}
#[test]
fn resolve_dom_node_id_on_an_empty_map_drops_own_dom_and_keeps_foreign() {
let map = NodeIdMap::default();
assert_eq!(map.resolve_dom_node_id(DOM0, dom_node_at(DOM0, nid(0))), None);
let foreign = dom_node_at(DOM1, nid(0));
assert_eq!(map.resolve_dom_node_id(DOM0, foreign), Some(foreign));
}
#[test]
fn remap_keys_handles_a_swap_without_clobbering_payloads() {
let mut map: BTreeMap<NodeId, &str> = BTreeMap::new();
map.insert(nid(1), "one");
map.insert(nid(2), "two");
let node_map = NodeIdMap::from_pairs([(nid(1), nid(2)), (nid(2), nid(1))]);
remap_keys(&mut map, &node_map);
assert_eq!(map.len(), 2, "a swap must not lose an entry");
assert_eq!(map.get(&nid(1)).copied(), Some("two"));
assert_eq!(map.get(&nid(2)).copied(), Some("one"));
}
#[test]
fn remap_keys_drops_state_for_unmounted_nodes() {
let mut map: BTreeMap<NodeId, u32> = BTreeMap::new();
map.insert(nid(1), 10);
map.insert(nid(2), 20);
map.insert(nid(3), 30);
let node_map = NodeIdMap::from_pairs([(nid(2), nid(1)), (nid(3), nid(2))]);
remap_keys(&mut map, &node_map);
assert_eq!(map.len(), 2, "node 1's state must be GC'd");
assert_eq!(map.get(&nid(1)).copied(), Some(20));
assert_eq!(map.get(&nid(2)).copied(), Some(30));
assert!(!map.contains_key(&nid(3)), "no state may remain at a dead index");
}
#[test]
fn remap_keys_with_an_empty_node_map_clears_all_state() {
let mut map: BTreeMap<NodeId, u32> = BTreeMap::new();
map.insert(nid(0), 1);
map.insert(nid(9), 2);
remap_keys(&mut map, &NodeIdMap::default());
assert!(
map.is_empty(),
"an empty reconciliation unmounts everything, so all state is dropped"
);
}
#[test]
fn remap_keys_with_an_identity_map_is_a_no_op() {
let mut map: BTreeMap<NodeId, u32> = (0..64).map(|i| (nid(i), i as u32)).collect();
let before = map.clone();
let node_map = NodeIdMap::from_pairs((0..64).map(|i| (nid(i), nid(i))));
remap_keys(&mut map, &node_map);
assert_eq!(map, before);
}
#[test]
fn remap_keys_on_an_empty_map_does_not_panic() {
let mut map: BTreeMap<NodeId, u32> = BTreeMap::new();
remap_keys(&mut map, &NodeIdMap::from_pairs([(nid(1), nid(0))]));
assert!(map.is_empty());
}
#[test]
fn remap_keys_collision_is_lossy_but_deterministic() {
let mut map: BTreeMap<NodeId, &str> = BTreeMap::new();
map.insert(nid(1), "from-1");
map.insert(nid(2), "from-2");
let node_map = NodeIdMap::from_pairs([(nid(1), nid(5)), (nid(2), nid(5))]);
remap_keys(&mut map, &node_map);
assert_eq!(map.len(), 1, "two old keys collapsing onto one new key lose one entry");
assert_eq!(
map.get(&nid(5)).copied(),
Some("from-2"),
"the last-visited (highest) old id wins — deterministic, not arbitrary"
);
}
#[test]
fn remap_keys_preserves_payload_identity_under_a_shift_down() {
let n = 256usize;
let mut map: BTreeMap<NodeId, usize> = (0..n).map(|i| (nid(i), i * 1000)).collect();
let node_map = NodeIdMap::from_pairs(
core::iter::once((nid(0), nid(0))).chain((2..n).map(|i| (nid(i), nid(i - 1)))),
);
remap_keys(&mut map, &node_map);
assert_eq!(map.len(), n - 1, "exactly the deleted node's state is GC'd");
assert_eq!(map.get(&nid(0)).copied(), Some(0));
for i in 2..n {
assert_eq!(
map.get(&nid(i - 1)).copied(),
Some(i * 1000),
"node {i}'s payload must follow it to index {}",
i - 1
);
}
assert!(!map.contains_key(&nid(n - 1)), "the vacated tail index is empty");
}
#[test]
fn remap_keys_survivor_moving_onto_a_dead_index_is_not_dropped() {
let mut map: BTreeMap<NodeId, &str> = BTreeMap::new();
map.insert(nid(1), "doomed");
map.insert(nid(2), "survivor");
let node_map = NodeIdMap::from_pairs([(nid(2), nid(1))]);
remap_keys(&mut map, &node_map);
assert_eq!(map.len(), 1);
assert_eq!(
map.get(&nid(1)).copied(),
Some("survivor"),
"the surviving payload occupies the recycled index — the dead one is gone"
);
}
#[test]
fn remap_dom_keys_never_touches_another_dom() {
let mut map: BTreeMap<(DomId, NodeId), u32> = BTreeMap::new();
map.insert((DOM0, nid(2)), 20);
map.insert((DOM1, nid(2)), 99);
map.insert((DOM1, nid(1)), 98);
let node_map = NodeIdMap::from_pairs([(nid(2), nid(1))]);
remap_dom_keys(&mut map, DOM0, &node_map);
assert_eq!(map.get(&(DOM0, nid(1))).copied(), Some(20), "DOM 0's entry is remapped");
assert!(!map.contains_key(&(DOM0, nid(2))), "the old DOM 0 key is gone");
assert_eq!(
map.get(&(DOM1, nid(2))).copied(),
Some(99),
"DOM 1 is untouched by a DOM 0 reconciliation"
);
assert_eq!(map.get(&(DOM1, nid(1))).copied(), Some(98));
assert_eq!(map.len(), 3);
}
#[test]
fn remap_dom_keys_drops_unmounted_entries_of_the_target_dom_only() {
let mut map: BTreeMap<(DomId, NodeId), u32> = BTreeMap::new();
map.insert((DOM0, nid(1)), 10); map.insert((DOM1, nid(1)), 11); let node_map = NodeIdMap::from_pairs([(nid(0), nid(0))]);
remap_dom_keys(&mut map, DOM0, &node_map);
assert!(!map.contains_key(&(DOM0, nid(1))));
assert_eq!(map.get(&(DOM1, nid(1))).copied(), Some(11));
assert_eq!(map.len(), 1);
}
#[test]
fn remap_dom_keys_handles_a_swap_within_the_target_dom() {
let mut map: BTreeMap<(DomId, NodeId), &str> = BTreeMap::new();
map.insert((DOM0, nid(1)), "one");
map.insert((DOM0, nid(2)), "two");
map.insert((DOM1, nid(1)), "other-dom");
let node_map = NodeIdMap::from_pairs([(nid(1), nid(2)), (nid(2), nid(1))]);
remap_dom_keys(&mut map, DOM0, &node_map);
assert_eq!(map.get(&(DOM0, nid(1))).copied(), Some("two"));
assert_eq!(map.get(&(DOM0, nid(2))).copied(), Some("one"));
assert_eq!(map.get(&(DOM1, nid(1))).copied(), Some("other-dom"));
assert_eq!(map.len(), 3, "a swap plus a bystander DOM loses nothing");
}
#[test]
fn remap_dom_keys_with_an_empty_node_map_clears_only_the_target_dom() {
let mut map: BTreeMap<(DomId, NodeId), u32> = BTreeMap::new();
map.insert((DOM0, nid(0)), 1);
map.insert((DOM0, nid(5)), 2);
map.insert((DOM1, nid(0)), 3);
remap_dom_keys(&mut map, DOM0, &NodeIdMap::default());
assert_eq!(map.len(), 1, "every DOM 0 node was unmounted");
assert_eq!(map.get(&(DOM1, nid(0))).copied(), Some(3));
}
#[test]
fn remap_dom_keys_on_an_empty_map_does_not_panic() {
let mut map: BTreeMap<(DomId, NodeId), u32> = BTreeMap::new();
remap_dom_keys(&mut map, DOM0, &NodeIdMap::from_pairs([(nid(1), nid(0))]));
assert!(map.is_empty());
}
#[test]
fn remap_dom_keys_with_a_target_dom_that_has_no_entries_is_a_no_op() {
let mut map: BTreeMap<(DomId, NodeId), u32> = BTreeMap::new();
map.insert((DOM1, nid(1)), 1);
map.insert((DOM_MAX, nid(2)), 2);
let before = map.clone();
remap_dom_keys(&mut map, DOM0, &NodeIdMap::from_pairs([(nid(1), nid(9))]));
assert_eq!(map, before);
}
#[test]
fn remap_dom_keys_handles_boundary_dom_and_node_ids() {
let mut map: BTreeMap<(DomId, NodeId), u32> = BTreeMap::new();
map.insert((DOM_MAX, nid(usize::MAX)), 1);
map.insert((DOM_MAX, nid(usize::MAX - 1)), 2);
map.insert((DOM0, nid(usize::MAX)), 3);
let node_map = NodeIdMap::from_pairs([(nid(usize::MAX), nid(0))]);
remap_dom_keys(&mut map, DOM_MAX, &node_map);
assert_eq!(
map.get(&(DOM_MAX, nid(0))).copied(),
Some(1),
"usize::MAX remaps like any other index"
);
assert!(
!map.contains_key(&(DOM_MAX, nid(usize::MAX - 1))),
"unmounted in DOM_MAX -> dropped"
);
assert_eq!(
map.get(&(DOM0, nid(usize::MAX))).copied(),
Some(3),
"DOM 0 is a bystander here"
);
assert_eq!(map.len(), 2);
}
#[test]
fn remap_dom_keys_with_an_identity_map_is_a_no_op_even_when_repeated() {
let mut map: BTreeMap<(DomId, NodeId), u32> =
(0..32).map(|i| ((DOM0, nid(i)), i as u32)).collect();
let before = map.clone();
let node_map = NodeIdMap::from_pairs((0..32).map(|i| (nid(i), nid(i))));
remap_dom_keys(&mut map, DOM0, &node_map);
remap_dom_keys(&mut map, DOM0, &node_map);
assert_eq!(map, before);
}
}