1use alloc::{collections::BTreeMap, collections::VecDeque, string::{String, ToString}, vec::Vec};
16use core::hash::Hash;
17
18use azul_css::props::property::{CssPropertyType, RelayoutScope};
19
20use crate::{
21 dom::{DomId, DomNodeHash, DomNodeId, NodeData, NodeType, IdOrClass},
22 events::{
23 ComponentEventFilter, EventData, EventFilter, EventPhase, EventSource, EventType,
24 LifecycleEventData, LifecycleReason, SyntheticEvent,
25 },
26 geom::LogicalRect,
27 id::NodeId,
28 styled_dom::{ChangedCssProperty, NodeHierarchyItemId, NodeHierarchyItem, RestyleResult, StyledNodeState},
29 task::Instant,
30 OrderedMap,
31};
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41pub struct NodeChangeSet {
42 pub bits: u32,
43}
44
45impl NodeChangeSet {
46 pub const NODE_TYPE_CHANGED: u32 = 0b0000_0000_0000_0001;
50 pub const TEXT_CONTENT: u32 = 0b0000_0000_0000_0010;
52 pub const IDS_AND_CLASSES: u32 = 0b0000_0000_0000_0100;
54 pub const INLINE_STYLE_LAYOUT: u32 = 0b0000_0000_0000_1000;
56 pub const CHILDREN_CHANGED: u32 = 0b0000_0000_0001_0000;
58 pub const IMAGE_CHANGED: u32 = 0b0000_0000_0010_0000;
60 pub const CONTENTEDITABLE: u32 = 0b0000_0000_0100_0000;
62 pub const TAB_INDEX: u32 = 0b0000_0000_1000_0000;
64
65 pub const INLINE_STYLE_PAINT: u32 = 0b0000_0001_0000_0000;
69 pub const STYLED_STATE: u32 = 0b0000_0010_0000_0000;
71
72 pub const CALLBACKS: u32 = 0b0000_0100_0000_0000;
76 pub const DATASET: u32 = 0b0000_1000_0000_0000;
78 pub const ACCESSIBILITY: u32 = 0b0001_0000_0000_0000;
80
81 pub const AFFECTS_LAYOUT: u32 = Self::NODE_TYPE_CHANGED
85 | Self::TEXT_CONTENT
86 | Self::IDS_AND_CLASSES
87 | Self::INLINE_STYLE_LAYOUT
88 | Self::CHILDREN_CHANGED
89 | Self::IMAGE_CHANGED
90 | Self::CONTENTEDITABLE;
91
92 pub const AFFECTS_PAINT: u32 = Self::INLINE_STYLE_PAINT
94 | Self::STYLED_STATE;
95
96 #[must_use] pub const fn empty() -> Self {
97 Self { bits: 0 }
98 }
99
100 #[must_use] pub const fn is_empty(&self) -> bool {
101 self.bits == 0
102 }
103
104 #[must_use] pub const fn contains(&self, flag: u32) -> bool {
105 (self.bits & flag) == flag
106 }
107
108 #[must_use] pub const fn intersects(&self, mask: u32) -> bool {
109 (self.bits & mask) != 0
110 }
111
112 pub const fn insert(&mut self, flag: u32) {
113 self.bits |= flag;
114 }
115
116 #[must_use] pub const fn is_visually_unchanged(&self) -> bool {
118 !self.intersects(Self::AFFECTS_LAYOUT) && !self.intersects(Self::AFFECTS_PAINT)
119 }
120
121 #[must_use] pub const fn needs_layout(&self) -> bool {
123 self.intersects(Self::AFFECTS_LAYOUT)
124 }
125
126 #[must_use] pub const fn needs_paint(&self) -> bool {
128 self.intersects(Self::AFFECTS_PAINT)
129 }
130}
131
132impl core::ops::BitOrAssign for NodeChangeSet {
133 fn bitor_assign(&mut self, rhs: Self) {
134 self.bits |= rhs.bits;
135 }
136}
137
138impl core::ops::BitOr for NodeChangeSet {
139 type Output = Self;
140 fn bitor(self, rhs: Self) -> Self {
141 Self { bits: self.bits | rhs.bits }
142 }
143}
144
145#[derive(Debug, Clone)]
147#[derive(Default)]
148pub struct ExtendedDiffResult {
149 pub diff: DiffResult,
151 pub node_changes: Vec<(NodeId, NodeId, NodeChangeSet)>,
155}
156
157
158#[allow(clippy::too_many_lines)] #[must_use] pub fn compute_node_changes(
162 old_node: &NodeData,
163 new_node: &NodeData,
164 old_styled_state: Option<&StyledNodeState>,
165 new_styled_state: Option<&StyledNodeState>,
166) -> NodeChangeSet {
167 let mut changes = NodeChangeSet::empty();
168
169 if core::mem::discriminant(old_node.get_node_type())
171 != core::mem::discriminant(new_node.get_node_type())
172 {
173 changes.insert(NodeChangeSet::NODE_TYPE_CHANGED);
174 return changes; }
176
177 match (old_node.get_node_type(), new_node.get_node_type()) {
179 (NodeType::Text(old_text), NodeType::Text(new_text)) => {
180 if old_text.as_str() != new_text.as_str() {
181 changes.insert(NodeChangeSet::TEXT_CONTENT);
182 }
183 }
184 (NodeType::Image(old_img), NodeType::Image(new_img)) => {
185 use core::hash::Hasher;
188 let hash_img = |img: &crate::resources::ImageRef| -> u64 {
189 let mut h = crate::hash::DefaultHasher::new();
190 img.hash(&mut h);
191 h.finish()
192 };
193 if hash_img(old_img) != hash_img(new_img) {
194 changes.insert(NodeChangeSet::IMAGE_CHANGED);
195 }
196 }
197 _ => {} }
199
200 {
202 use crate::dom::AttributeType;
203 let old_ids_classes: Vec<_> = old_node.attributes().as_ref().iter()
204 .filter(|a| matches!(a, AttributeType::Id(_) | AttributeType::Class(_)))
205 .collect();
206 let new_ids_classes: Vec<_> = new_node.attributes().as_ref().iter()
207 .filter(|a| matches!(a, AttributeType::Id(_) | AttributeType::Class(_)))
208 .collect();
209 if old_ids_classes != new_ids_classes {
210 changes.insert(NodeChangeSet::IDS_AND_CLASSES);
211 }
212 }
213
214 if old_node.style != new_node.style {
219 let mut has_layout = false;
220 let mut has_paint = false;
221
222 #[allow(clippy::items_after_statements)]
224 fn mark(prop_type: CssPropertyType, has_layout: &mut bool, has_paint: &mut bool) {
225 if prop_type.relayout_scope(true) == RelayoutScope::None {
226 *has_paint = true;
227 } else {
228 *has_layout = true;
229 }
230 }
231
232 let old_props: Vec<(CssPropertyType, _, _)> = old_node
240 .style
241 .iter_inline_properties()
242 .map(|(prop, conds)| (prop.get_type(), prop, conds))
243 .collect();
244 let mut old_matched = vec![false; old_props.len()];
245
246 for (prop, conds) in new_node.style.iter_inline_properties() {
247 let prop_type = prop.get_type();
248 let mut found_unchanged = false;
250 for (i, (old_type, old_prop, old_conds)) in old_props.iter().enumerate() {
251 if old_matched[i]
252 || *old_type != prop_type
253 || old_conds.as_slice() != conds.as_slice()
254 {
255 continue;
256 }
257 old_matched[i] = true;
258 if *old_prop == prop {
259 found_unchanged = true;
260 }
261 break;
262 }
263 if !found_unchanged {
266 mark(prop_type, &mut has_layout, &mut has_paint);
267 }
268 }
269
270 for (i, (old_type, _, _)) in old_props.iter().enumerate() {
272 if !old_matched[i] {
273 mark(*old_type, &mut has_layout, &mut has_paint);
274 }
275 }
276
277 if has_layout {
278 changes.insert(NodeChangeSet::INLINE_STYLE_LAYOUT);
279 }
280 if has_paint {
281 changes.insert(NodeChangeSet::INLINE_STYLE_PAINT);
282 }
283 }
284
285 {
287 let old_cbs = old_node.callbacks.as_ref();
288 let new_cbs = new_node.callbacks.as_ref();
289 if old_cbs.len() == new_cbs.len() {
290 for (o, n) in old_cbs.iter().zip(new_cbs.iter()) {
291 if o.event != n.event || o.callback != n.callback {
292 changes.insert(NodeChangeSet::CALLBACKS);
293 break;
294 }
295 }
296 } else {
297 changes.insert(NodeChangeSet::CALLBACKS);
298 }
299 }
300
301 if old_node.get_dataset() != new_node.get_dataset() {
303 changes.insert(NodeChangeSet::DATASET);
304 }
305
306 if old_node.is_contenteditable() != new_node.is_contenteditable() {
308 changes.insert(NodeChangeSet::CONTENTEDITABLE);
309 }
310
311 if old_node.get_tab_index() != new_node.get_tab_index() {
313 changes.insert(NodeChangeSet::TAB_INDEX);
314 }
315
316 if old_styled_state != new_styled_state {
318 changes.insert(NodeChangeSet::STYLED_STATE);
319 }
320
321 changes
322}
323
324#[must_use] pub fn calculate_reconciliation_key(
341 node_data: &[NodeData],
342 hierarchy: &[NodeHierarchyItem],
343 node_id: NodeId,
344) -> u64 {
345 use core::hash::Hasher;
346
347 let n = node_data.len();
348
349 let terminal_key = |nid: NodeId| -> Option<u64> {
352 let node = &node_data[nid.index()];
353 if let Some(key) = node.get_key() {
355 return Some(key);
356 }
357 for attr in node.attributes().as_ref() {
359 if let Some(id) = attr.as_id() {
360 let mut hasher = crate::hash::DefaultHasher::new();
361 id.hash(&mut hasher);
362 return Some(hasher.finish());
363 }
364 }
365 None
366 };
367
368 if let Some(key) = terminal_key(node_id) {
370 return key;
371 }
372
373 let mut chain: Vec<NodeId> = Vec::new();
387 let mut seed_parent_key: Option<u64> = None;
388 let mut cur = node_id;
389 for _ in 0..n {
390 if cur.index() >= n {
391 break;
392 }
393 chain.push(cur);
394 match hierarchy.get(cur.index()).and_then(NodeHierarchyItem::parent_id) {
395 None => break,
396 Some(parent) => {
397 if let Some(k) = terminal_key(parent) {
398 seed_parent_key = Some(k);
399 break;
400 }
401 cur = parent;
402 }
403 }
404 }
405
406 let mut parent_key: Option<u64> = seed_parent_key;
410 for &nid in chain.iter().rev() {
411 let node = &node_data[nid.index()];
412 let mut hasher = crate::hash::DefaultHasher::new();
413
414 core::mem::discriminant(node.get_node_type()).hash(&mut hasher);
415 for attr in node.attributes().as_ref() {
416 if let Some(class) = attr.as_class() {
417 class.hash(&mut hasher);
418 }
419 }
420
421 if let Some(parent_id) =
422 hierarchy.get(nid.index()).and_then(NodeHierarchyItem::parent_id)
423 {
424 let mut sibling_index: usize = 0;
426 let mut current = hierarchy
427 .get(parent_id.index())
428 .and_then(|h| h.first_child_id(parent_id));
429 while let Some(sibling_id) = current {
430 if sibling_id == nid {
431 break;
432 }
433 let sibling = &node_data[sibling_id.index()];
434 if core::mem::discriminant(sibling.get_node_type())
435 == core::mem::discriminant(node.get_node_type())
436 {
437 sibling_index += 1;
438 }
439 current = hierarchy
440 .get(sibling_id.index())
441 .and_then(NodeHierarchyItem::next_sibling_id);
442 }
443
444 sibling_index.hash(&mut hasher);
445 parent_key.unwrap_or(0).hash(&mut hasher);
446 }
447
448 parent_key = Some(hasher.finish());
449 }
450
451 parent_key.unwrap_or(0)
452}
453
454#[must_use] pub fn precompute_reconciliation_keys(
460 node_data: &[NodeData],
461 hierarchy: &[NodeHierarchyItem],
462) -> Vec<u64> {
463 (0..node_data.len())
464 .map(|idx| calculate_reconciliation_key(node_data, hierarchy, NodeId::new(idx)))
465 .collect()
466}
467
468#[derive(Debug, Clone, Copy)]
470pub struct NodeMove {
471 pub old_node_id: NodeId,
473 pub new_node_id: NodeId,
475}
476
477#[derive(Debug, Clone)]
479#[derive(Default)]
480pub struct DiffResult {
481 pub events: Vec<SyntheticEvent>,
483 pub node_moves: Vec<NodeMove>,
485}
486
487
488#[allow(clippy::needless_pass_by_value)] #[allow(clippy::too_many_lines)] #[must_use] pub fn reconcile_dom(
516 old_node_data: &[NodeData],
517 new_node_data: &[NodeData],
518 old_hierarchy: &[NodeHierarchyItem],
519 new_hierarchy: &[NodeHierarchyItem],
520 old_layout: &OrderedMap<NodeId, LogicalRect>,
521 new_layout: &OrderedMap<NodeId, LogicalRect>,
522 dom_id: DomId,
523 timestamp: Instant,
524) -> DiffResult {
525 fn pop_first_unconsumed(
527 queue: &mut VecDeque<NodeId>,
528 consumed: &[bool],
529 ) -> Option<NodeId> {
530 while let Some(&old_id) = queue.front() {
531 queue.pop_front();
532 if !consumed[old_id.index()] {
533 return Some(old_id);
534 }
535 }
536 None
537 }
538
539 let mut result = DiffResult::default();
540
541 let old_rec_keys = precompute_reconciliation_keys(old_node_data, old_hierarchy);
553 let new_rec_keys = precompute_reconciliation_keys(new_node_data, new_hierarchy);
557
558 let old_parent_key = |old_id: NodeId| -> Option<u64> {
562 old_hierarchy
563 .get(old_id.index())
564 .and_then(NodeHierarchyItem::parent_id)
565 .map(|p| old_rec_keys[p.index()])
566 };
567
568 let mut old_by_rec_key: OrderedMap<u64, VecDeque<NodeId>> = OrderedMap::default();
569 let mut old_hashed: OrderedMap<DomNodeHash, VecDeque<NodeId>> = OrderedMap::default();
570 let mut old_structural: OrderedMap<DomNodeHash, VecDeque<NodeId>> = OrderedMap::default();
571 let mut old_nodes_consumed = vec![false; old_node_data.len()];
572
573 for (idx, node) in old_node_data.iter().enumerate() {
574 let id = NodeId::new(idx);
575 old_by_rec_key.entry(old_rec_keys[idx]).or_default().push_back(id);
576
577 let hash = node.calculate_node_data_hash();
578 old_hashed.entry(hash).or_default().push_back(id);
579
580 let structural_hash = node.calculate_structural_hash();
581 old_structural.entry(structural_hash).or_default().push_back(id);
582 }
583
584 for (new_idx, new_node) in new_node_data.iter().enumerate() {
587 let new_id = NodeId::new(new_idx);
588 let mut matched_old_id = None;
589 let mut matched_by_rec_key = false;
590 let has_explicit_key = new_node.get_key().is_some();
591
592 let new_rec_key = new_rec_keys[new_idx];
594 if let Some(queue) = old_by_rec_key.get_mut(&new_rec_key) {
595 if let Some(old_id) = pop_first_unconsumed(queue, &old_nodes_consumed) {
596 matched_old_id = Some(old_id);
597 matched_by_rec_key = true;
598 }
599 }
600
601 let new_parent_key: Option<u64> = new_hierarchy
608 .get(new_idx)
609 .and_then(NodeHierarchyItem::parent_id)
610 .map(|p| new_rec_keys[p.index()]);
611
612 if !has_explicit_key && matched_old_id.is_none() {
617 let hash = new_node.calculate_node_data_hash();
619 if let Some(queue) = old_hashed.get_mut(&hash) {
620 if let Some(pos) = queue.iter().position(|&old_id| {
621 !old_nodes_consumed[old_id.index()]
622 && old_parent_key(old_id) == new_parent_key
623 }) {
624 matched_old_id = queue.remove(pos);
625 }
626 }
627
628 if matched_old_id.is_none() {
630 let structural_hash = new_node.calculate_structural_hash();
631 if let Some(queue) = old_structural.get_mut(&structural_hash) {
632 if let Some(pos) = queue.iter().position(|&old_id| {
633 !old_nodes_consumed[old_id.index()]
634 && old_parent_key(old_id) == new_parent_key
635 }) {
636 matched_old_id = queue.remove(pos);
637 }
638 }
639 }
640 }
641
642 if let Some(old_id) = matched_old_id {
645 old_nodes_consumed[old_id.index()] = true;
648 result.node_moves.push(NodeMove {
649 old_node_id: old_id,
650 new_node_id: new_id,
651 });
652
653 let old_rect = old_layout.get(&old_id).copied().unwrap_or(LogicalRect::zero());
655 let new_rect = new_layout.get(&new_id).copied().unwrap_or(LogicalRect::zero());
656
657 if old_rect.size != new_rect.size {
658 if has_resize_callback(new_node) {
660 result.events.push(create_lifecycle_event(
661 EventType::Resize,
662 new_id,
663 dom_id,
664 ×tamp,
665 LifecycleEventData {
666 reason: LifecycleReason::Resize,
667 previous_bounds: Some(old_rect),
668 current_bounds: new_rect,
669 },
670 ));
671 }
672 }
673
674 if matched_by_rec_key {
680 let old_hash = old_node_data[old_id.index()].calculate_node_data_hash();
681 let new_hash = new_node.calculate_node_data_hash();
682
683 if old_hash != new_hash && has_update_callback(new_node) {
684 result.events.push(create_lifecycle_event(
685 EventType::Update,
686 new_id,
687 dom_id,
688 ×tamp,
689 LifecycleEventData {
690 reason: LifecycleReason::Update,
691 previous_bounds: Some(old_rect),
692 current_bounds: new_rect,
693 },
694 ));
695 }
696 }
697 } else {
698 if has_mount_callback(new_node) {
700 let bounds = new_layout.get(&new_id).copied().unwrap_or(LogicalRect::zero());
701 result.events.push(create_lifecycle_event(
702 EventType::Mount,
703 new_id,
704 dom_id,
705 ×tamp,
706 LifecycleEventData {
707 reason: LifecycleReason::InitialMount,
708 previous_bounds: None,
709 current_bounds: bounds,
710 },
711 ));
712 }
713 }
714 }
715
716 for (old_idx, consumed) in old_nodes_consumed.iter().enumerate() {
720 if !consumed {
721 let old_id = NodeId::new(old_idx);
722 let old_node = &old_node_data[old_idx];
723
724 if has_unmount_callback(old_node) {
725 let bounds = old_layout.get(&old_id).copied().unwrap_or(LogicalRect::zero());
726 result.events.push(create_lifecycle_event(
727 EventType::Unmount,
728 old_id,
729 dom_id,
730 ×tamp,
731 LifecycleEventData {
732 reason: LifecycleReason::Unmount,
733 previous_bounds: Some(bounds),
734 current_bounds: LogicalRect::zero(),
735 },
736 ));
737 }
738 }
739 }
740
741 result
742}
743
744fn create_lifecycle_event(
746 event_type: EventType,
747 node_id: NodeId,
748 dom_id: DomId,
749 timestamp: &Instant,
750 data: LifecycleEventData,
751) -> SyntheticEvent {
752 let dom_node_id = DomNodeId {
753 dom: dom_id,
754 node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
755 };
756 SyntheticEvent {
757 event_type,
758 source: EventSource::Lifecycle,
759 phase: EventPhase::Target,
760 target: dom_node_id,
761 current_target: dom_node_id,
762 timestamp: timestamp.clone(),
763 data: EventData::Lifecycle(data),
764 stopped: false,
765 stopped_immediate: false,
766 prevented_default: false,
767 }
768}
769
770fn has_mount_callback(node: &NodeData) -> bool {
772 node.get_callbacks().iter().any(|cb| {
773 matches!(
774 cb.event,
775 EventFilter::Component(ComponentEventFilter::AfterMount)
776 )
777 })
778}
779
780fn has_unmount_callback(node: &NodeData) -> bool {
782 node.get_callbacks().iter().any(|cb| {
783 matches!(
784 cb.event,
785 EventFilter::Component(ComponentEventFilter::BeforeUnmount)
786 )
787 })
788}
789
790fn has_resize_callback(node: &NodeData) -> bool {
792 node.get_callbacks().iter().any(|cb| {
793 matches!(
794 cb.event,
795 EventFilter::Component(ComponentEventFilter::NodeResized)
796 )
797 })
798}
799
800fn has_update_callback(node: &NodeData) -> bool {
802 node.get_callbacks().iter().any(|cb| {
803 matches!(
804 cb.event,
805 EventFilter::Component(ComponentEventFilter::Updated)
806 )
807 })
808}
809
810#[must_use] pub fn create_migration_map(node_moves: &[NodeMove]) -> OrderedMap<NodeId, NodeId> {
831 let mut map = OrderedMap::default();
832 for m in node_moves {
833 map.insert(m.old_node_id, m.new_node_id);
834 }
835 map
836}
837
838pub fn transfer_states(
861 old_node_data: &mut [NodeData],
862 new_node_data: &mut [NodeData],
863 node_moves: &[NodeMove],
864) {
865 use crate::refany::OptionRefAny;
866
867 for movement in node_moves {
868 let old_idx = movement.old_node_id.index();
869 let new_idx = movement.new_node_id.index();
870
871 if old_idx >= old_node_data.len() || new_idx >= new_node_data.len() {
873 continue;
874 }
875
876 let Some(merge_callback) = new_node_data[new_idx].get_merge_callback() else {
878 continue; };
880
881 let old_dataset = old_node_data[old_idx].take_dataset();
884 let new_dataset = new_node_data[new_idx].take_dataset();
885
886 match (new_dataset, old_dataset) {
887 (Some(new_data), Some(old_data)) => {
888 let orphan_alloc = new_data.sharing_info.ptr as usize;
901
902 let merged = (merge_callback.cb)(new_data, old_data);
905
906 new_node_data[new_idx].set_dataset(OptionRefAny::Some(merged.clone()));
908
909 for nd in new_node_data.iter_mut() {
918 if let Some(vv) = nd.get_virtual_view_node() {
919 if vv.refany.sharing_info.ptr as usize == orphan_alloc {
920 vv.refany = merged.clone();
921 }
922 }
923 for cb in nd.callbacks.as_mut().iter_mut() {
924 if cb.refany.sharing_info.ptr as usize == orphan_alloc {
925 cb.refany = merged.clone();
926 }
927 }
928 let ds_is_orphan = nd
929 .get_dataset()
930 .is_some_and(|ds| ds.sharing_info.ptr as usize == orphan_alloc);
931 if ds_is_orphan {
932 nd.set_dataset(OptionRefAny::Some(merged.clone()));
933 }
934 }
935 }
936 (new_ds, old_ds) => {
937 if let Some(ds) = new_ds {
939 new_node_data[new_idx].set_dataset(OptionRefAny::Some(ds));
940 }
941 if let Some(ds) = old_ds {
942 old_node_data[old_idx].set_dataset(OptionRefAny::Some(ds));
943 }
944 }
945 }
946 }
947}
948
949#[must_use] pub fn calculate_contenteditable_key(
968 node_data: &[NodeData],
969 hierarchy: &[NodeHierarchyItem],
970 node_id: NodeId,
971) -> u64 {
972 use core::hash::Hasher;
973
974 let n = node_data.len();
975
976 let terminal_key = |nid: NodeId| -> Option<u64> {
979 let node = &node_data[nid.index()];
980 if let Some(explicit_key) = node.get_key() {
982 return Some(explicit_key);
983 }
984 for attr in node.attributes().as_ref() {
986 if let Some(id) = attr.as_id() {
987 let mut hasher = crate::hash::DefaultHasher::new(); hasher.write(id.as_bytes());
989 return Some(hasher.finish());
990 }
991 }
992 None
993 };
994
995 if let Some(key) = terminal_key(node_id) {
997 return key;
998 }
999
1000 let mut chain: Vec<NodeId> = Vec::new();
1006 let mut seed_parent_key: Option<u64> = None;
1007 let mut cur = node_id;
1008 for _ in 0..n {
1009 if cur.index() >= n {
1010 break;
1011 }
1012 chain.push(cur);
1013 match hierarchy.get(cur.index()).and_then(NodeHierarchyItem::parent_id) {
1014 None => break,
1015 Some(parent) => {
1016 if let Some(k) = terminal_key(parent) {
1017 seed_parent_key = Some(k);
1018 break;
1019 }
1020 cur = parent;
1021 }
1022 }
1023 }
1024
1025 let mut parent_key: u64 = seed_parent_key.unwrap_or(0);
1030 for &nid in chain.iter().rev() {
1031 let node = &node_data[nid.index()];
1032 let mut hasher = crate::hash::DefaultHasher::new(); let node_parent = hierarchy.get(nid.index()).and_then(NodeHierarchyItem::parent_id);
1035
1036 let level_parent_key = if node_parent.is_some() { parent_key } else { 0 };
1038 hasher.write(&level_parent_key.to_le_bytes());
1039
1040 let node_discriminant = core::mem::discriminant(node.get_node_type());
1042 let nth_of_type = node_parent.map_or(0u32, |parent_id| {
1043 let mut count = 0u32;
1044 let mut sibling_id = hierarchy
1045 .get(parent_id.index())
1046 .and_then(|h| h.first_child_id(parent_id));
1047 while let Some(sib_id) = sibling_id {
1048 if sib_id == nid {
1049 break;
1050 }
1051 let sibling_discriminant =
1052 core::mem::discriminant(node_data[sib_id.index()].get_node_type());
1053 if sibling_discriminant == node_discriminant {
1054 count += 1;
1055 }
1056 sibling_id = hierarchy
1057 .get(sib_id.index())
1058 .and_then(NodeHierarchyItem::next_sibling_id);
1059 }
1060 count
1061 });
1062 hasher.write(&nth_of_type.to_le_bytes());
1063
1064 node_discriminant.hash(&mut hasher);
1066
1067 for attr in node.attributes().as_ref() {
1069 if let Some(class) = attr.as_class() {
1070 hasher.write(class.as_bytes());
1071 }
1072 }
1073
1074 parent_key = hasher.finish();
1075 }
1076
1077 parent_key
1078}
1079
1080#[must_use] pub fn reconcile_cursor_position(
1106 old_text: &str,
1107 new_text: &str,
1108 old_cursor_byte: usize,
1109) -> usize {
1110 let snap = |offset: usize| -> usize {
1115 let mut o = offset.min(new_text.len());
1116 while o > 0 && !new_text.is_char_boundary(o) {
1117 o -= 1;
1118 }
1119 o
1120 };
1121
1122 if old_text == new_text {
1124 return snap(old_cursor_byte);
1125 }
1126
1127 if old_text.is_empty() {
1129 return new_text.len();
1130 }
1131
1132 if new_text.is_empty() {
1134 return 0;
1135 }
1136
1137 let common_prefix_bytes = old_text
1139 .bytes()
1140 .zip(new_text.bytes())
1141 .take_while(|(a, b)| a == b)
1142 .count();
1143
1144 if old_cursor_byte <= common_prefix_bytes {
1146 return snap(old_cursor_byte);
1147 }
1148
1149 let common_suffix_bytes = old_text
1151 .bytes()
1152 .rev()
1153 .zip(new_text.bytes().rev())
1154 .take_while(|(a, b)| a == b)
1155 .count();
1156
1157 let old_suffix_start = old_text.len().saturating_sub(common_suffix_bytes);
1159 let new_suffix_start = new_text.len().saturating_sub(common_suffix_bytes);
1160
1161 if old_cursor_byte >= old_suffix_start {
1163 let offset_from_end = old_text.len() - old_cursor_byte;
1164 return snap(new_text.len().saturating_sub(offset_from_end));
1165 }
1166
1167 snap(new_suffix_start)
1170}
1171
1172#[must_use] pub fn get_node_text_content(node: &NodeData) -> Option<&str> {
1176 if let NodeType::Text(ref text) = node.get_node_type() {
1177 Some(text.as_str())
1178 } else {
1179 None
1180 }
1181}
1182
1183#[derive(Debug, Clone, PartialEq, Eq)]
1189pub struct TextChange {
1190 pub old_text: String,
1192 pub new_text: String,
1194}
1195
1196#[derive(Debug, Clone, Default)]
1198pub struct NodeChangeReport {
1199 pub change_set: NodeChangeSet,
1201
1202 pub relayout_scope: RelayoutScope,
1210
1211 pub changed_css_properties: Vec<CssPropertyType>,
1214
1215 pub text_change: Option<TextChange>,
1217}
1218
1219impl NodeChangeReport {
1220 #[must_use] pub fn needs_layout(&self) -> bool {
1223 self.change_set.needs_layout() || self.relayout_scope > RelayoutScope::None
1224 }
1225
1226 #[must_use] pub const fn needs_paint(&self) -> bool {
1227 self.change_set.needs_paint()
1228 }
1229
1230 #[must_use] pub fn is_visually_unchanged(&self) -> bool {
1231 self.change_set.is_visually_unchanged() && self.relayout_scope == RelayoutScope::None
1232 }
1233}
1234
1235#[derive(Debug, Clone, Default)]
1243pub struct ChangeAccumulator {
1244 pub per_node: BTreeMap<NodeId, NodeChangeReport>,
1246
1247 pub max_scope: RelayoutScope,
1250
1251 pub mounted_nodes: Vec<NodeId>,
1254
1255 pub unmounted_nodes: Vec<NodeId>,
1258}
1259
1260impl ChangeAccumulator {
1261 #[must_use] pub fn new() -> Self {
1262 Self::default()
1263 }
1264
1265 #[must_use] pub fn is_empty(&self) -> bool {
1267 self.per_node.is_empty() && self.mounted_nodes.is_empty() && self.unmounted_nodes.is_empty()
1268 }
1269
1270 #[must_use] pub fn needs_layout(&self) -> bool {
1272 self.max_scope > RelayoutScope::None
1273 || !self.mounted_nodes.is_empty()
1274 || self.per_node.values().any(NodeChangeReport::needs_layout)
1275 }
1276
1277 #[must_use] pub fn needs_paint_only(&self) -> bool {
1279 !self.needs_layout() && self.per_node.values().any(NodeChangeReport::needs_paint)
1280 }
1281
1282 #[must_use] pub fn is_visually_unchanged(&self) -> bool {
1284 self.mounted_nodes.is_empty()
1285 && self.unmounted_nodes.is_empty()
1286 && self.max_scope == RelayoutScope::None
1287 && self.per_node.values().all(NodeChangeReport::is_visually_unchanged)
1288 }
1289
1290 pub fn add_dom_change(
1292 &mut self,
1293 new_node_id: NodeId,
1294 change_set: NodeChangeSet,
1295 relayout_scope: RelayoutScope,
1296 text_change: Option<TextChange>,
1297 changed_css_properties: Vec<CssPropertyType>,
1298 ) {
1299 if relayout_scope > self.max_scope {
1300 self.max_scope = relayout_scope;
1301 }
1302
1303 let report = self.per_node.entry(new_node_id).or_default();
1304 report.change_set |= change_set;
1305 if relayout_scope > report.relayout_scope {
1306 report.relayout_scope = relayout_scope;
1307 }
1308 if text_change.is_some() {
1309 report.text_change = text_change;
1310 }
1311 report.changed_css_properties.extend(changed_css_properties);
1312 }
1313
1314 pub fn add_text_change(
1316 &mut self,
1317 node_id: NodeId,
1318 old_text: String,
1319 new_text: String,
1320 ) {
1321 let scope = RelayoutScope::IfcOnly;
1322 if scope > self.max_scope {
1323 self.max_scope = scope;
1324 }
1325
1326 let report = self.per_node.entry(node_id).or_default();
1327 report.change_set.insert(NodeChangeSet::TEXT_CONTENT);
1328 if scope > report.relayout_scope {
1329 report.relayout_scope = scope;
1330 }
1331 report.text_change = Some(TextChange { old_text, new_text });
1332 }
1333
1334 pub fn add_css_change(
1336 &mut self,
1337 node_id: NodeId,
1338 prop_type: CssPropertyType,
1339 scope: RelayoutScope,
1340 ) {
1341 if scope > self.max_scope {
1342 self.max_scope = scope;
1343 }
1344
1345 let report = self.per_node.entry(node_id).or_default();
1346 if scope > RelayoutScope::None {
1347 report.change_set.insert(NodeChangeSet::INLINE_STYLE_LAYOUT);
1348 } else {
1349 report.change_set.insert(NodeChangeSet::INLINE_STYLE_PAINT);
1350 }
1351 if scope > report.relayout_scope {
1352 report.relayout_scope = scope;
1353 }
1354 report.changed_css_properties.push(prop_type);
1355 }
1356
1357 pub fn add_image_change(
1359 &mut self,
1360 node_id: NodeId,
1361 scope: RelayoutScope,
1362 ) {
1363 if scope > self.max_scope {
1364 self.max_scope = scope;
1365 }
1366
1367 let report = self.per_node.entry(node_id).or_default();
1368 report.change_set.insert(NodeChangeSet::IMAGE_CHANGED);
1369 if scope > report.relayout_scope {
1370 report.relayout_scope = scope;
1371 }
1372 }
1373
1374 pub fn add_mount(&mut self, node_id: NodeId) {
1376 self.mounted_nodes.push(node_id);
1377 }
1378
1379 pub fn add_unmount(&mut self, node_id: NodeId) {
1381 self.unmounted_nodes.push(node_id);
1382 }
1383
1384 pub fn merge_restyle_result(&mut self, restyle: &crate::styled_dom::RestyleResult) {
1390 for (node_id, changed_props) in &restyle.changed_nodes {
1391 for changed in changed_props {
1392 let prop_type = changed.current_prop.get_type();
1393 let scope = prop_type.relayout_scope(true); self.add_css_change(*node_id, prop_type, scope);
1395 }
1396 }
1397 }
1398
1399 pub fn merge_extended_diff(
1404 &mut self,
1405 extended: &ExtendedDiffResult,
1406 old_node_data: &[NodeData],
1407 new_node_data: &[NodeData],
1408 ) {
1409 for &(old_id, new_id, ref change_set) in &extended.node_changes {
1410 if change_set.is_empty() {
1411 continue;
1412 }
1413
1414 let scope = Self::classify_change_scope(*change_set, new_node_data, new_id);
1416
1417 let text_change = if change_set.contains(NodeChangeSet::TEXT_CONTENT) {
1419 let old_text = get_node_text_content(&old_node_data[old_id.index()])
1420 .unwrap_or("")
1421 .to_string();
1422 let new_text = get_node_text_content(&new_node_data[new_id.index()])
1423 .unwrap_or("")
1424 .to_string();
1425 Some(TextChange { old_text, new_text })
1426 } else {
1427 None
1428 };
1429
1430 self.add_dom_change(new_id, *change_set, scope, text_change, Vec::new());
1431 }
1432
1433 let matched_new: alloc::collections::BTreeSet<usize> = extended
1435 .diff
1436 .node_moves
1437 .iter()
1438 .map(|m| m.new_node_id.index())
1439 .collect();
1440
1441 for idx in 0..new_node_data.len() {
1442 if !matched_new.contains(&idx) {
1443 self.add_mount(NodeId::new(idx));
1444 }
1445 }
1446
1447 let matched_old: alloc::collections::BTreeSet<usize> = extended
1449 .diff
1450 .node_moves
1451 .iter()
1452 .map(|m| m.old_node_id.index())
1453 .collect();
1454
1455 for idx in 0..old_node_data.len() {
1456 if !matched_old.contains(&idx) {
1457 self.add_unmount(NodeId::new(idx));
1458 }
1459 }
1460 }
1461
1462 fn classify_change_scope(
1464 change_set: NodeChangeSet,
1465 new_node_data: &[NodeData],
1466 new_node_id: NodeId,
1467 ) -> RelayoutScope {
1468 if change_set.contains(NodeChangeSet::NODE_TYPE_CHANGED)
1470 || change_set.contains(NodeChangeSet::CHILDREN_CHANGED)
1471 {
1472 return RelayoutScope::Full;
1473 }
1474
1475 if change_set.contains(NodeChangeSet::IDS_AND_CLASSES) {
1477 return RelayoutScope::Full;
1478 }
1479
1480 if change_set.contains(NodeChangeSet::INLINE_STYLE_LAYOUT) {
1485 let new_node = &new_node_data[new_node_id.index()];
1487 let mut max_scope = RelayoutScope::None;
1488 for (prop, _conds) in new_node.style.iter_inline_properties() {
1489 let scope = prop.get_type().relayout_scope(true);
1490 if scope > max_scope {
1491 max_scope = scope;
1492 }
1493 }
1494 return if max_scope == RelayoutScope::None {
1495 RelayoutScope::SizingOnly } else {
1497 max_scope
1498 };
1499 }
1500
1501 if change_set.contains(NodeChangeSet::TEXT_CONTENT) {
1503 return RelayoutScope::IfcOnly;
1504 }
1505
1506 if change_set.contains(NodeChangeSet::IMAGE_CHANGED) {
1508 return RelayoutScope::SizingOnly;
1509 }
1510
1511 if change_set.contains(NodeChangeSet::CONTENTEDITABLE) {
1513 return RelayoutScope::SizingOnly;
1514 }
1515
1516 if change_set.intersects(NodeChangeSet::AFFECTS_PAINT) {
1518 return RelayoutScope::None;
1519 }
1520
1521 RelayoutScope::None
1522 }
1523}
1524
1525#[must_use] pub fn reconcile_dom_with_changes(
1533 old_node_data: &[NodeData],
1534 new_node_data: &[NodeData],
1535 old_hierarchy: &[NodeHierarchyItem],
1536 new_hierarchy: &[NodeHierarchyItem],
1537 old_styled_nodes: Option<&[StyledNodeState]>,
1538 new_styled_nodes: Option<&[StyledNodeState]>,
1539 old_layout: &OrderedMap<NodeId, LogicalRect>,
1540 new_layout: &OrderedMap<NodeId, LogicalRect>,
1541 dom_id: DomId,
1542 timestamp: Instant,
1543) -> ExtendedDiffResult {
1544 let diff = reconcile_dom(
1546 old_node_data,
1547 new_node_data,
1548 old_hierarchy,
1549 new_hierarchy,
1550 old_layout,
1551 new_layout,
1552 dom_id,
1553 timestamp,
1554 );
1555
1556 let mut node_changes = Vec::new();
1558 for node_move in &diff.node_moves {
1559 let old_nd = &old_node_data[node_move.old_node_id.index()];
1560 let new_nd = &new_node_data[node_move.new_node_id.index()];
1561
1562 let old_state = old_styled_nodes.and_then(|s| s.get(node_move.old_node_id.index()));
1563 let new_state = new_styled_nodes.and_then(|s| s.get(node_move.new_node_id.index()));
1564
1565 let changes = compute_node_changes(old_nd, new_nd, old_state, new_state);
1566 node_changes.push((node_move.old_node_id, node_move.new_node_id, changes));
1567 }
1568
1569 ExtendedDiffResult { diff, node_changes }
1570}
1571
1572#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1588#[derive(Default)]
1589pub struct NodeDataFingerprint {
1590 pub content_hash: u64,
1592 pub state_hash: u64,
1594 pub inline_css_hash: u64,
1596 pub ids_classes_hash: u64,
1598 pub callbacks_hash: u64,
1600 pub attrs_hash: u64,
1602}
1603
1604
1605impl NodeDataFingerprint {
1606 #[must_use] pub fn compute(node: &NodeData, styled_state: Option<&StyledNodeState>) -> Self {
1608 use core::hash::Hasher;
1609 use core::hash::Hash;
1610
1611 let content_hash = {
1613 let mut h = crate::hash::DefaultHasher::new();
1614 node.get_node_type().hash(&mut h);
1615 h.finish()
1616 };
1617
1618 let state_hash = {
1620 let mut h = crate::hash::DefaultHasher::new();
1621 if let Some(state) = styled_state {
1622 state.hash(&mut h);
1623 }
1624 h.finish()
1625 };
1626
1627 let inline_css_hash = {
1631 let mut h = crate::hash::DefaultHasher::new();
1632 for (prop, conds) in node.style.iter_inline_properties() {
1633 prop.hash(&mut h);
1634 conds.as_slice().len().hash(&mut h);
1635 }
1636 h.finish()
1637 };
1638
1639 let ids_classes_hash = {
1641 let mut h = crate::hash::DefaultHasher::new();
1642 for attr in node.attributes().as_ref() {
1643 match attr {
1644 crate::dom::AttributeType::Id(s) => {
1645 crate::dom::IdOrClass::Id(s.clone()).hash(&mut h);
1646 }
1647 crate::dom::AttributeType::Class(s) => {
1648 crate::dom::IdOrClass::Class(s.clone()).hash(&mut h);
1649 }
1650 _ => {}
1651 }
1652 }
1653 h.finish()
1654 };
1655
1656 let callbacks_hash = {
1658 let mut h = crate::hash::DefaultHasher::new();
1659 for cb in node.callbacks.as_ref() {
1660 cb.event.hash(&mut h);
1661 cb.callback.hash(&mut h);
1662 }
1663 h.finish()
1664 };
1665
1666 let attrs_hash = {
1668 let mut h = crate::hash::DefaultHasher::new();
1669 node.is_contenteditable().hash(&mut h);
1670 node.flags.hash(&mut h);
1671 node.get_dataset().hash(&mut h);
1672 h.finish()
1673 };
1674
1675 Self {
1676 content_hash,
1677 state_hash,
1678 inline_css_hash,
1679 ids_classes_hash,
1680 callbacks_hash,
1681 attrs_hash,
1682 }
1683 }
1684
1685 #[must_use] pub const fn diff(&self, other: &Self) -> NodeChangeSet {
1693 let mut changes = NodeChangeSet::empty();
1694
1695 if self.content_hash != other.content_hash {
1696 changes.insert(NodeChangeSet::TEXT_CONTENT);
1700 changes.insert(NodeChangeSet::IMAGE_CHANGED);
1701 }
1702
1703 if self.state_hash != other.state_hash {
1704 changes.insert(NodeChangeSet::STYLED_STATE);
1705 }
1706
1707 if self.inline_css_hash != other.inline_css_hash {
1708 changes.insert(NodeChangeSet::INLINE_STYLE_LAYOUT);
1711 }
1712
1713 if self.ids_classes_hash != other.ids_classes_hash {
1714 changes.insert(NodeChangeSet::IDS_AND_CLASSES);
1715 }
1716
1717 if self.callbacks_hash != other.callbacks_hash {
1718 changes.insert(NodeChangeSet::CALLBACKS);
1719 }
1720
1721 if self.attrs_hash != other.attrs_hash {
1722 changes.insert(NodeChangeSet::TAB_INDEX);
1723 changes.insert(NodeChangeSet::CONTENTEDITABLE);
1724 }
1725
1726 changes
1727 }
1728
1729 #[must_use] pub fn is_identical(&self, other: &Self) -> bool {
1731 self == other
1732 }
1733
1734 #[must_use] pub const fn might_affect_layout(&self, other: &Self) -> bool {
1736 self.content_hash != other.content_hash
1737 || self.inline_css_hash != other.inline_css_hash
1738 || self.ids_classes_hash != other.ids_classes_hash
1739 || self.attrs_hash != other.attrs_hash
1740 }
1741
1742 #[must_use] pub const fn might_affect_visuals(&self, other: &Self) -> bool {
1744 self.content_hash != other.content_hash
1745 || self.state_hash != other.state_hash
1746 || self.inline_css_hash != other.inline_css_hash
1747 || self.ids_classes_hash != other.ids_classes_hash
1748 }
1749}
1750
1751#[cfg(test)]
1752mod audit_tests {
1753 use super::*;
1754 use crate::dom::NodeData;
1755 use crate::styled_dom::NodeHierarchyItem;
1756
1757 fn hitem(
1759 parent: Option<usize>,
1760 prev: Option<usize>,
1761 next: Option<usize>,
1762 last_child: Option<usize>,
1763 ) -> NodeHierarchyItem {
1764 NodeHierarchyItem {
1765 parent: parent.map_or(0, |p| p + 1),
1766 previous_sibling: prev.map_or(0, |p| p + 1),
1767 next_sibling: next.map_or(0, |p| p + 1),
1768 last_child: last_child.map_or(0, |p| p + 1),
1769 }
1770 }
1771
1772 #[test]
1774 fn reconciliation_key_deep_chain_no_overflow() {
1775 let build = |n: usize| -> (Vec<NodeData>, Vec<NodeHierarchyItem>) {
1776 let node_data = (0..n).map(|_| NodeData::create_div()).collect();
1777 let hierarchy = (0..n)
1778 .map(|i| {
1779 hitem(
1780 if i == 0 { None } else { Some(i - 1) },
1781 None,
1782 None,
1783 if i + 1 < n { Some(i + 1) } else { None },
1784 )
1785 })
1786 .collect();
1787 (node_data, hierarchy)
1788 };
1789
1790 let n = 100_000usize;
1793 let (node_data, hierarchy) = build(n);
1794 let _ = calculate_reconciliation_key(&node_data, &hierarchy, NodeId::new(n - 1));
1795 let _ = calculate_contenteditable_key(&node_data, &hierarchy, NodeId::new(n - 1));
1796
1797 let m = 2_000usize;
1803 let (nd, hi) = build(m);
1804 let keys = precompute_reconciliation_keys(&nd, &hi);
1805 assert_eq!(keys.len(), m);
1806 }
1807
1808 #[test]
1810 fn reconciliation_key_cycle_terminates() {
1811 let node_data = vec![
1812 NodeData::create_div(),
1813 NodeData::create_div(),
1814 NodeData::create_div(),
1815 ];
1816 let hierarchy = vec![
1818 hitem(None, None, None, None),
1819 hitem(Some(2), None, None, None),
1820 hitem(Some(1), None, None, None),
1821 ];
1822 let _ = calculate_reconciliation_key(&node_data, &hierarchy, NodeId::new(1));
1823 let _ = calculate_contenteditable_key(&node_data, &hierarchy, NodeId::new(1));
1824 }
1825
1826 #[test]
1827 fn reconciliation_key_single_node() {
1828 let node_data = vec![NodeData::create_div()];
1829 let hierarchy = vec![hitem(None, None, None, None)];
1830 let direct = calculate_reconciliation_key(&node_data, &hierarchy, NodeId::new(0));
1831 let pre = precompute_reconciliation_keys(&node_data, &hierarchy)[0];
1832 assert_eq!(direct, pre);
1833 }
1834
1835 #[test]
1836 fn reconciliation_key_distinguishes_siblings() {
1837 let node_data = vec![NodeData::create_div(); 3];
1839 let hierarchy = vec![
1840 hitem(None, None, None, Some(2)), hitem(Some(0), None, Some(2), None), hitem(Some(0), Some(1), None, None), ];
1844 let k1 = calculate_reconciliation_key(&node_data, &hierarchy, NodeId::new(1));
1845 let k2 = calculate_reconciliation_key(&node_data, &hierarchy, NodeId::new(2));
1846 assert_ne!(k1, k2);
1847 }
1848
1849 #[test]
1850 fn cursor_offsets_are_always_char_boundaries() {
1851 let old = "héllo";
1853 let new = "héllo wörld"; for c in 0..=old.len() {
1855 let r = reconcile_cursor_position(old, new, c);
1856 assert!(
1857 new.is_char_boundary(r),
1858 "cursor {c} mapped to non-char-boundary offset {r} in {new:?}",
1859 );
1860 assert!(r <= new.len());
1861 }
1862 let r = reconcile_cursor_position("aömega", "bömega", 3);
1864 assert!("bömega".is_char_boundary(r));
1865 }
1866
1867 #[test]
1868 fn cursor_prefix_unchanged_stays_put() {
1869 assert_eq!(reconcile_cursor_position("Hello", "Hello World", 5), 5);
1870 }
1871
1872 #[test]
1873 fn cursor_empty_cases() {
1874 assert_eq!(reconcile_cursor_position("", "abc", 0), 3);
1875 assert_eq!(reconcile_cursor_position("abc", "", 2), 0);
1876 assert_eq!(reconcile_cursor_position("abc", "abc", 2), 2);
1877 }
1878}
1879
1880#[cfg(test)]
1899mod autotest_generated {
1900 use super::*;
1901
1902 use azul_css::{
1903 css::CssPropertyValue,
1904 props::{layout::LayoutWidth, property::CssProperty},
1905 };
1906
1907 use crate::{
1908 callbacks::CoreCallback,
1909 dom::{DatasetMergeCallbackType, TabIndex},
1910 geom::{LogicalPosition, LogicalSize},
1911 refany::{OptionRefAny, RefAny},
1912 resources::{ImageRef, RawImageFormat},
1913 };
1914
1915 fn noop_callback() -> CoreCallback {
1922 CoreCallback {
1923 cb: 0usize,
1924 ctx: OptionRefAny::None,
1925 }
1926 }
1927
1928 fn with_cb(mut nd: NodeData, filter: ComponentEventFilter) -> NodeData {
1929 nd.add_callback(
1930 EventFilter::Component(filter),
1931 RefAny::new(0u32),
1932 noop_callback(),
1933 );
1934 nd
1935 }
1936
1937 fn hitem(
1939 parent: Option<usize>,
1940 prev: Option<usize>,
1941 next: Option<usize>,
1942 last_child: Option<usize>,
1943 ) -> NodeHierarchyItem {
1944 NodeHierarchyItem {
1945 parent: parent.map_or(0, |p| p + 1),
1946 previous_sibling: prev.map_or(0, |p| p + 1),
1947 next_sibling: next.map_or(0, |p| p + 1),
1948 last_child: last_child.map_or(0, |p| p + 1),
1949 }
1950 }
1951
1952 fn rect(w: f32, h: f32) -> LogicalRect {
1953 LogicalRect::new(LogicalPosition::new(0.0, 0.0), LogicalSize::new(w, h))
1954 }
1955
1956 fn layout_of(entries: &[(usize, LogicalRect)]) -> OrderedMap<NodeId, LogicalRect> {
1957 let mut m = OrderedMap::default();
1958 for (idx, r) in entries {
1959 m.insert(NodeId::new(*idx), *r);
1960 }
1961 m
1962 }
1963
1964 fn no_layout() -> OrderedMap<NodeId, LogicalRect> {
1965 OrderedMap::default()
1966 }
1967
1968 fn diff_flat(old: &[NodeData], new: &[NodeData]) -> DiffResult {
1971 reconcile_dom(
1972 old,
1973 new,
1974 &[],
1975 &[],
1976 &no_layout(),
1977 &no_layout(),
1978 DomId::ROOT_ID,
1979 Instant::now(),
1980 )
1981 }
1982
1983 fn count_events(r: &DiffResult, t: EventType) -> usize {
1984 r.events.iter().filter(|e| e.event_type == t).count()
1985 }
1986
1987 fn id_node(id: &str) -> NodeData {
1988 NodeData::create_div().with_ids_and_classes(vec![IdOrClass::Id(id.into())].into())
1989 }
1990
1991 fn class_node(class: &str) -> NodeData {
1992 NodeData::create_div().with_ids_and_classes(vec![IdOrClass::Class(class.into())].into())
1993 }
1994
1995 const UNICODE_SAMPLES: &[&str] = &[
1998 "",
1999 "a",
2000 "héllo",
2001 "e\u{0301}galite\u{0301}", "مرحبا بالعالم", "👨👩👧👦 family", "日本語のテキスト",
2005 "\u{feff}bom-prefixed",
2006 "🇩🇪🇫🇷🇯🇵", "mixed 漢字 and ascii ✅",
2008 ];
2009
2010 #[test]
2015 fn autotest_changeset_empty_is_a_neutral_element() {
2016 let e = NodeChangeSet::empty();
2017 assert_eq!(e.bits, 0);
2018 assert!(e.is_empty());
2019 assert!(!e.needs_layout());
2020 assert!(!e.needs_paint());
2021 assert!(e.is_visually_unchanged());
2022 assert_eq!(NodeChangeSet::default(), e);
2024 let mut some = NodeChangeSet::empty();
2026 some.insert(NodeChangeSet::TEXT_CONTENT);
2027 assert_eq!((some | e).bits, some.bits);
2028 assert_eq!((e | some).bits, some.bits);
2029 }
2030
2031 #[test]
2032 fn autotest_changeset_contains_zero_is_vacuously_true() {
2033 assert!(NodeChangeSet::empty().contains(0));
2037 let mut s = NodeChangeSet::empty();
2038 s.insert(NodeChangeSet::CALLBACKS);
2039 assert!(s.contains(0));
2040 assert!(NodeChangeSet { bits: u32::MAX }.contains(0));
2041 }
2042
2043 #[test]
2044 fn autotest_changeset_intersects_zero_is_always_false() {
2045 assert!(!NodeChangeSet::empty().intersects(0));
2047 assert!(!NodeChangeSet { bits: u32::MAX }.intersects(0));
2048 }
2049
2050 #[test]
2051 fn autotest_changeset_contains_is_all_bits_intersects_is_any_bits() {
2052 let mut s = NodeChangeSet::empty();
2053 s.insert(NodeChangeSet::TEXT_CONTENT);
2054
2055 let both = NodeChangeSet::TEXT_CONTENT | NodeChangeSet::IMAGE_CHANGED;
2056 assert!(!s.contains(both), "contains() must require ALL bits");
2057 assert!(s.intersects(both), "intersects() must require ANY bit");
2058 assert!(s.contains(NodeChangeSet::TEXT_CONTENT));
2059 }
2060
2061 #[test]
2062 fn autotest_changeset_insert_min_max_and_idempotent() {
2063 let mut s = NodeChangeSet::empty();
2065 s.insert(0);
2066 assert!(s.is_empty());
2067
2068 let mut s = NodeChangeSet::empty();
2070 s.insert(u32::MAX);
2071 assert_eq!(s.bits, u32::MAX);
2072 s.insert(u32::MAX);
2074 assert_eq!(s.bits, u32::MAX);
2075 s.insert(NodeChangeSet::TEXT_CONTENT);
2076 assert_eq!(s.bits, u32::MAX);
2077
2078 assert!(s.contains(NodeChangeSet::NODE_TYPE_CHANGED));
2080 assert!(s.contains(NodeChangeSet::AFFECTS_LAYOUT));
2081 assert!(s.contains(NodeChangeSet::AFFECTS_PAINT));
2082 assert!(s.needs_layout());
2083 assert!(s.needs_paint());
2084 assert!(!s.is_visually_unchanged());
2085 assert!(!s.is_empty());
2086 }
2087
2088 #[test]
2089 fn autotest_changeset_undefined_high_bits_trigger_no_work() {
2090 let s = NodeChangeSet {
2093 bits: 0b1000_0000_0000_0000_0000_0000_0000_0000,
2094 };
2095 assert!(!s.is_empty());
2096 assert!(!s.needs_layout());
2097 assert!(!s.needs_paint());
2098 assert!(s.is_visually_unchanged());
2099 }
2100
2101 #[test]
2102 fn autotest_changeset_layout_and_paint_masks_are_disjoint() {
2103 assert_eq!(
2106 NodeChangeSet::AFFECTS_LAYOUT & NodeChangeSet::AFFECTS_PAINT,
2107 0,
2108 "AFFECTS_LAYOUT and AFFECTS_PAINT must not overlap",
2109 );
2110
2111 for flag in [
2112 NodeChangeSet::NODE_TYPE_CHANGED,
2113 NodeChangeSet::TEXT_CONTENT,
2114 NodeChangeSet::IDS_AND_CLASSES,
2115 NodeChangeSet::INLINE_STYLE_LAYOUT,
2116 NodeChangeSet::CHILDREN_CHANGED,
2117 NodeChangeSet::IMAGE_CHANGED,
2118 NodeChangeSet::CONTENTEDITABLE,
2119 NodeChangeSet::INLINE_STYLE_PAINT,
2120 NodeChangeSet::STYLED_STATE,
2121 NodeChangeSet::CALLBACKS,
2122 NodeChangeSet::DATASET,
2123 NodeChangeSet::ACCESSIBILITY,
2124 ] {
2125 let mut s = NodeChangeSet::empty();
2126 s.insert(flag);
2127 assert!(!(s.needs_layout() && s.needs_paint()), "flag {flag:#b} is both");
2128 assert_eq!(
2130 s.is_visually_unchanged(),
2131 !s.needs_layout() && !s.needs_paint(),
2132 "is_visually_unchanged() disagrees with needs_layout/needs_paint for {flag:#b}",
2133 );
2134 }
2135 }
2136
2137 #[test]
2138 fn autotest_changeset_nonvisual_flags_are_visually_unchanged() {
2139 let mut s = NodeChangeSet::empty();
2140 s.insert(NodeChangeSet::CALLBACKS);
2141 s.insert(NodeChangeSet::DATASET);
2142 s.insert(NodeChangeSet::ACCESSIBILITY);
2143 s.insert(NodeChangeSet::TAB_INDEX); assert!(!s.is_empty());
2145 assert!(s.is_visually_unchanged());
2146 assert!(!s.needs_layout());
2147 assert!(!s.needs_paint());
2148 }
2149
2150 #[test]
2151 fn autotest_changeset_bitor_matches_bitorassign() {
2152 for (a, b) in [
2155 (0u32, 0u32),
2156 (0, u32::MAX),
2157 (u32::MAX, u32::MAX),
2158 (NodeChangeSet::TEXT_CONTENT, NodeChangeSet::STYLED_STATE),
2159 (0xDEAD_BEEF, 0x0BAD_F00D),
2160 ] {
2161 let (sa, sb) = (NodeChangeSet { bits: a }, NodeChangeSet { bits: b });
2162
2163 let by_operator = sa | sb;
2164 assert_eq!(by_operator.bits, a | b);
2165
2166 let mut by_assign = sa;
2167 by_assign |= sb;
2168 assert_eq!(by_assign, by_operator);
2169
2170 assert_eq!((sb | sa).bits, by_operator.bits, "BitOr must commute");
2171 assert_eq!((by_operator | by_operator).bits, by_operator.bits);
2172 }
2173 }
2174
2175 #[test]
2180 fn autotest_change_report_default_is_inert() {
2181 let r = NodeChangeReport::default();
2182 assert!(!r.needs_layout());
2183 assert!(!r.needs_paint());
2184 assert!(r.is_visually_unchanged());
2185 assert_eq!(r.relayout_scope, RelayoutScope::None);
2186 assert!(r.changed_css_properties.is_empty());
2187 assert!(r.text_change.is_none());
2188 }
2189
2190 #[test]
2191 fn autotest_change_report_scope_alone_forces_layout() {
2192 let mut r = NodeChangeReport::default();
2195 r.relayout_scope = RelayoutScope::IfcOnly;
2196 assert!(r.needs_layout());
2197 assert!(!r.needs_paint());
2198 assert!(!r.is_visually_unchanged());
2199 }
2200
2201 #[test]
2202 fn autotest_change_report_paint_flag_does_not_force_layout() {
2203 let mut r = NodeChangeReport::default();
2204 r.change_set.insert(NodeChangeSet::STYLED_STATE);
2205 assert!(!r.needs_layout());
2206 assert!(r.needs_paint());
2207 assert!(!r.is_visually_unchanged());
2208 }
2209
2210 #[test]
2215 fn autotest_cursor_result_is_always_a_valid_slice_index() {
2216 for old in UNICODE_SAMPLES {
2220 for new in UNICODE_SAMPLES {
2221 for cursor in 0..=old.len() {
2222 let r = reconcile_cursor_position(old, new, cursor);
2223 assert!(
2224 r <= new.len(),
2225 "cursor {cursor} in {old:?} -> {r} exceeds len of {new:?}",
2226 );
2227 assert!(
2228 new.is_char_boundary(r),
2229 "cursor {cursor} in {old:?} -> {r} splits a codepoint in {new:?}",
2230 );
2231 let _ = &new[..r];
2233 }
2234 }
2235 }
2236 }
2237
2238 #[test]
2239 fn autotest_cursor_is_deterministic() {
2240 for old in UNICODE_SAMPLES {
2243 for new in UNICODE_SAMPLES {
2244 let a = reconcile_cursor_position(old, new, old.len() / 2);
2245 let b = reconcile_cursor_position(old, new, old.len() / 2);
2246 assert_eq!(a, b);
2247 }
2248 }
2249 }
2250
2251 #[test]
2252 fn autotest_cursor_zero_stays_zero_when_texts_differ_at_byte_zero() {
2253 assert_eq!(reconcile_cursor_position("abc", "xyz", 0), 0);
2255 assert_eq!(reconcile_cursor_position("日本", "中国", 0), 0);
2256 }
2257
2258 #[test]
2259 fn autotest_cursor_identical_text_clamps_to_len() {
2260 assert_eq!(reconcile_cursor_position("abc", "abc", usize::MAX), 3);
2263 assert_eq!(reconcile_cursor_position("héllo", "héllo", usize::MAX), 6);
2264 assert_eq!(reconcile_cursor_position("héllo", "héllo", 2), 1);
2266 }
2267
2268 #[test]
2269 fn autotest_cursor_empty_sides_are_documented_constants() {
2270 for new in UNICODE_SAMPLES {
2272 assert_eq!(reconcile_cursor_position("", new, 0), new.len());
2273 assert_eq!(reconcile_cursor_position("", new, usize::MAX), new.len());
2274 }
2275 for old in UNICODE_SAMPLES {
2276 if old.is_empty() {
2277 continue; }
2279 assert_eq!(reconcile_cursor_position(old, "", 0), 0);
2280 assert_eq!(reconcile_cursor_position(old, "", old.len()), 0);
2281 }
2282 }
2283
2284 #[test]
2285 fn autotest_cursor_appended_text_keeps_prefix_cursor() {
2286 let old = "Hello";
2288 let new = "Hello, World";
2289 for cursor in 0..=old.len() {
2290 assert_eq!(reconcile_cursor_position(old, new, cursor), cursor);
2291 }
2292 }
2293
2294 #[test]
2295 fn autotest_cursor_deleted_tail_clamps_into_new_text() {
2296 let old = "Hello, World";
2299 let new = "Hello";
2300 assert_eq!(reconcile_cursor_position(old, new, old.len()), new.len());
2301 assert_eq!(reconcile_cursor_position(old, new, 5), 5);
2302 }
2303
2304 #[test]
2305 fn autotest_cursor_multibyte_insert_before_cursor_shifts_by_suffix_rule() {
2306 let old = "mega";
2309 let new = "ömega";
2310 let r = reconcile_cursor_position(old, new, 4); assert_eq!(r, new.len());
2312 assert!(new.is_char_boundary(r));
2313 }
2314
2315 #[test]
2316 fn autotest_cursor_huge_inputs_do_not_hang_or_panic() {
2317 let old: String = core::iter::repeat('a').take(200_000).collect();
2320 let mut new = old.clone();
2321 new.push_str("tail");
2322
2323 let r = reconcile_cursor_position(&old, &new, old.len());
2324 assert!(r <= new.len());
2325 assert!(new.is_char_boundary(r));
2326
2327 let old_u: String = core::iter::repeat('é').take(50_000).collect();
2329 let new_u: String = core::iter::repeat('é').take(49_999).collect();
2330 let r = reconcile_cursor_position(&old_u, &new_u, old_u.len());
2331 assert!(r <= new_u.len());
2332 assert!(new_u.is_char_boundary(r));
2333 }
2334
2335 #[test]
2353 #[ignore = "known bug: subtract-with-overflow on out-of-range cursor; see comment above"]
2354 fn autotest_cursor_out_of_range_cursor_must_saturate_not_underflow() {
2355 assert_eq!(reconcile_cursor_position("abc", "abd", usize::MAX), 3);
2357 assert_eq!(reconcile_cursor_position("abc", "abd", 99), 3);
2358 assert_eq!(reconcile_cursor_position("héllo", "héllx", usize::MAX), 6);
2359 }
2360
2361 #[test]
2366 fn autotest_text_content_round_trips_unicode() {
2367 for s in UNICODE_SAMPLES {
2368 let node = NodeData::create_text(*s);
2369 assert_eq!(
2370 get_node_text_content(&node),
2371 Some(*s),
2372 "create_text -> get_node_text_content must round-trip {s:?}",
2373 );
2374 }
2375 }
2376
2377 #[test]
2378 fn autotest_text_content_is_none_for_non_text_nodes() {
2379 assert_eq!(get_node_text_content(&NodeData::create_div()), None);
2380 assert_eq!(get_node_text_content(&NodeData::create_body()), None);
2381 assert_eq!(get_node_text_content(&NodeData::create_br()), None);
2382 let img = NodeData::create_image(ImageRef::null_image(
2383 1,
2384 1,
2385 RawImageFormat::RGBA8,
2386 Vec::new(),
2387 ));
2388 assert_eq!(get_node_text_content(&img), None);
2389 }
2390
2391 #[test]
2396 fn autotest_callback_predicates_all_false_without_callbacks() {
2397 let n = NodeData::create_div();
2398 assert!(!has_mount_callback(&n));
2399 assert!(!has_unmount_callback(&n));
2400 assert!(!has_resize_callback(&n));
2401 assert!(!has_update_callback(&n));
2402 }
2403
2404 #[test]
2405 fn autotest_callback_predicates_are_mutually_exclusive_per_filter() {
2406 let cases = [
2408 (ComponentEventFilter::AfterMount, [true, false, false, false]),
2409 (ComponentEventFilter::BeforeUnmount, [false, true, false, false]),
2410 (ComponentEventFilter::NodeResized, [false, false, true, false]),
2411 (ComponentEventFilter::Updated, [false, false, false, true]),
2412 (ComponentEventFilter::Selected, [false, false, false, false]),
2414 (ComponentEventFilter::DefaultAction, [false, false, false, false]),
2415 ];
2416
2417 for (filter, expected) in cases {
2418 let n = with_cb(NodeData::create_div(), filter);
2419 let got = [
2420 has_mount_callback(&n),
2421 has_unmount_callback(&n),
2422 has_resize_callback(&n),
2423 has_update_callback(&n),
2424 ];
2425 assert_eq!(got, expected, "predicate mismatch for {filter:?}");
2426 }
2427 }
2428
2429 #[test]
2430 fn autotest_callback_predicates_find_target_among_many() {
2431 let mut n = NodeData::create_div();
2433 for f in [
2434 ComponentEventFilter::Selected,
2435 ComponentEventFilter::DefaultAction,
2436 ComponentEventFilter::NodeResized,
2437 ] {
2438 n = with_cb(n, f);
2439 }
2440 assert!(has_resize_callback(&n));
2441 assert!(!has_mount_callback(&n));
2442 }
2443
2444 #[test]
2449 fn autotest_lifecycle_event_fields_are_wired_consistently() {
2450 let ts = Instant::now();
2451 let ev = create_lifecycle_event(
2452 EventType::Mount,
2453 NodeId::new(1_000_000),
2454 DomId::ROOT_ID,
2455 &ts,
2456 LifecycleEventData {
2457 reason: LifecycleReason::InitialMount,
2458 previous_bounds: None,
2459 current_bounds: rect(1.0, 2.0),
2460 },
2461 );
2462
2463 assert_eq!(ev.event_type, EventType::Mount);
2464 assert_eq!(ev.source, EventSource::Lifecycle);
2465 assert_eq!(ev.phase, EventPhase::Target);
2466 assert_eq!(ev.target, ev.current_target);
2468 assert_eq!(
2469 ev.target.node.into_crate_internal(),
2470 Some(NodeId::new(1_000_000)),
2471 "NodeId must survive the 1-based NodeHierarchyItemId encoding",
2472 );
2473 assert!(!ev.stopped);
2474 assert!(!ev.stopped_immediate);
2475 assert!(!ev.prevented_default);
2476
2477 let EventData::Lifecycle(data) = &ev.data else {
2478 panic!("expected EventData::Lifecycle, got {:?}", ev.data);
2479 };
2480 assert!(data.previous_bounds.is_none());
2481 assert_eq!(data.current_bounds, rect(1.0, 2.0));
2482 }
2483
2484 #[test]
2489 fn autotest_compute_changes_identical_nodes_report_nothing() {
2490 let a = NodeData::create_text("same");
2491 let b = NodeData::create_text("same");
2492 let changes = compute_node_changes(&a, &b, None, None);
2493 assert!(
2494 changes.is_empty(),
2495 "identical nodes must produce no change flags, got {:#b}",
2496 changes.bits,
2497 );
2498 assert!(changes.is_visually_unchanged());
2499 }
2500
2501 #[test]
2502 fn autotest_compute_changes_node_type_change_short_circuits_everything() {
2503 let old = NodeData::create_div();
2508 let new = with_cb(
2509 NodeData::create_text("now a text node")
2510 .with_ids_and_classes(vec![IdOrClass::Class("brand-new".into())].into())
2511 .with_css("width: 10px")
2512 .with_tab_index(TabIndex::NoKeyboardFocus)
2513 .with_contenteditable(true),
2514 ComponentEventFilter::AfterMount,
2515 );
2516
2517 let hovered = StyledNodeState {
2518 hover: true,
2519 ..StyledNodeState::default()
2520 };
2521 let changes = compute_node_changes(
2522 &old,
2523 &new,
2524 Some(&StyledNodeState::default()),
2525 Some(&hovered),
2526 );
2527 assert_eq!(
2528 changes.bits,
2529 NodeChangeSet::NODE_TYPE_CHANGED,
2530 "a node-type change must be reported alone (early return)",
2531 );
2532 }
2533
2534 #[test]
2535 fn autotest_compute_changes_text_content_unicode() {
2536 for (i, s) in UNICODE_SAMPLES.iter().enumerate() {
2537 let old = NodeData::create_text(*s);
2538
2539 let same = NodeData::create_text(*s);
2541 assert!(
2542 !compute_node_changes(&old, &same, None, None)
2543 .contains(NodeChangeSet::TEXT_CONTENT),
2544 "identical text {s:?} must not report TEXT_CONTENT",
2545 );
2546
2547 let other = UNICODE_SAMPLES[(i + 1) % UNICODE_SAMPLES.len()];
2549 if other == *s {
2550 continue;
2551 }
2552 let changed = NodeData::create_text(other);
2553 assert!(
2554 compute_node_changes(&old, &changed, None, None)
2555 .contains(NodeChangeSet::TEXT_CONTENT),
2556 "{s:?} -> {other:?} must report TEXT_CONTENT",
2557 );
2558 }
2559 }
2560
2561 #[test]
2562 fn autotest_compute_changes_paint_only_css_never_sets_layout() {
2563 let old = NodeData::create_div().with_css("color: red");
2565 let new = NodeData::create_div().with_css("color: blue");
2566 let changes = compute_node_changes(&old, &new, None, None);
2567
2568 assert!(changes.contains(NodeChangeSet::INLINE_STYLE_PAINT));
2569 assert!(
2570 !changes.contains(NodeChangeSet::INLINE_STYLE_LAYOUT),
2571 "a paint-only property must not request relayout",
2572 );
2573 assert!(changes.needs_paint());
2574 assert!(!changes.needs_layout());
2575 }
2576
2577 #[test]
2578 fn autotest_compute_changes_sizing_css_sets_layout_not_paint() {
2579 let old = NodeData::create_div().with_css("width: 10px");
2581 let new = NodeData::create_div().with_css("width: 20px");
2582 let changes = compute_node_changes(&old, &new, None, None);
2583
2584 assert!(changes.contains(NodeChangeSet::INLINE_STYLE_LAYOUT));
2585 assert!(!changes.contains(NodeChangeSet::INLINE_STYLE_PAINT));
2586 assert!(changes.needs_layout());
2587 }
2588
2589 #[test]
2590 fn autotest_compute_changes_detects_removed_property() {
2591 let old = NodeData::create_div().with_css("color: red");
2594 let new = NodeData::create_div();
2595 let changes = compute_node_changes(&old, &new, None, None);
2596 assert!(
2597 changes.contains(NodeChangeSet::INLINE_STYLE_PAINT),
2598 "removing an inline property must be reported, got {:#b}",
2599 changes.bits,
2600 );
2601 }
2602
2603 #[test]
2604 fn autotest_compute_changes_detects_added_property() {
2605 let old = NodeData::create_div();
2606 let new = NodeData::create_div().with_css("width: 5px");
2607 let changes = compute_node_changes(&old, &new, None, None);
2608 assert!(changes.contains(NodeChangeSet::INLINE_STYLE_LAYOUT));
2609 }
2610
2611 #[test]
2612 fn autotest_compute_changes_ids_and_classes() {
2613 let changes = compute_node_changes(&class_node("a"), &class_node("b"), None, None);
2614 assert!(changes.contains(NodeChangeSet::IDS_AND_CLASSES));
2615
2616 let changes = compute_node_changes(&class_node("a"), &class_node("a"), None, None);
2618 assert!(!changes.contains(NodeChangeSet::IDS_AND_CLASSES));
2619 }
2620
2621 #[test]
2622 fn autotest_compute_changes_styled_state() {
2623 let n = NodeData::create_div();
2624 let calm = StyledNodeState::default();
2625 let hovered = StyledNodeState {
2626 hover: true,
2627 ..StyledNodeState::default()
2628 };
2629
2630 let changes = compute_node_changes(&n, &n, Some(&calm), Some(&hovered));
2631 assert!(changes.contains(NodeChangeSet::STYLED_STATE));
2632 assert!(changes.needs_paint());
2633 assert!(!changes.needs_layout());
2634
2635 assert!(!compute_node_changes(&n, &n, Some(&calm), Some(&calm))
2637 .contains(NodeChangeSet::STYLED_STATE));
2638 assert!(!compute_node_changes(&n, &n, None, None).contains(NodeChangeSet::STYLED_STATE));
2639
2640 assert!(compute_node_changes(&n, &n, None, Some(&calm))
2642 .contains(NodeChangeSet::STYLED_STATE));
2643 }
2644
2645 #[test]
2646 fn autotest_compute_changes_tab_index_and_contenteditable() {
2647 let plain = NodeData::create_div();
2648
2649 let editable = NodeData::create_div().with_contenteditable(true);
2650 let changes = compute_node_changes(&plain, &editable, None, None);
2651 assert!(changes.contains(NodeChangeSet::CONTENTEDITABLE));
2652 assert!(changes.needs_layout(), "CONTENTEDITABLE is in AFFECTS_LAYOUT");
2653
2654 let tabbed = NodeData::create_div().with_tab_index(TabIndex::OverrideInParent(3));
2655 let changes = compute_node_changes(&plain, &tabbed, None, None);
2656 assert!(changes.contains(NodeChangeSet::TAB_INDEX));
2657 assert!(changes.is_visually_unchanged());
2659 }
2660
2661 #[test]
2662 fn autotest_compute_changes_callbacks_count_and_identity() {
2663 let plain = NodeData::create_div();
2664 let one = with_cb(NodeData::create_div(), ComponentEventFilter::AfterMount);
2665
2666 let changes = compute_node_changes(&plain, &one, None, None);
2668 assert!(changes.contains(NodeChangeSet::CALLBACKS));
2669 assert!(changes.is_visually_unchanged(), "callbacks are not a visual change");
2670
2671 let other = with_cb(NodeData::create_div(), ComponentEventFilter::BeforeUnmount);
2673 let changes = compute_node_changes(&one, &other, None, None);
2674 assert!(changes.contains(NodeChangeSet::CALLBACKS));
2675
2676 let same = with_cb(NodeData::create_div(), ComponentEventFilter::AfterMount);
2678 let changes = compute_node_changes(&one, &same, None, None);
2679 assert!(!changes.contains(NodeChangeSet::CALLBACKS));
2680 }
2681
2682 #[test]
2683 fn autotest_compute_changes_image_identity_is_by_image_id() {
2684 let img = ImageRef::null_image(4, 4, RawImageFormat::RGBA8, Vec::new());
2687 let same = NodeData::create_image(img.clone());
2688 let also_same = NodeData::create_image(img.clone());
2689 assert!(
2690 !compute_node_changes(&same, &also_same, None, None)
2691 .contains(NodeChangeSet::IMAGE_CHANGED),
2692 "two nodes holding clones of the SAME ImageRef must not report a change",
2693 );
2694
2695 let other = NodeData::create_image(ImageRef::null_image(
2698 4,
2699 4,
2700 RawImageFormat::RGBA8,
2701 Vec::new(),
2702 ));
2703 let changes = compute_node_changes(&same, &other, None, None);
2704 assert!(changes.contains(NodeChangeSet::IMAGE_CHANGED));
2705 assert!(changes.needs_layout(), "IMAGE_CHANGED is in AFFECTS_LAYOUT");
2706 }
2707
2708 #[test]
2713 fn autotest_rec_key_empty_node_data_is_safe() {
2714 assert!(precompute_reconciliation_keys(&[], &[]).is_empty());
2715 }
2716
2717 #[test]
2718 fn autotest_rec_key_precompute_matches_per_node_calculation() {
2719 let node_data = vec![
2722 NodeData::create_div(),
2723 class_node("row"),
2724 NodeData::create_text("leaf"),
2725 id_node("footer"),
2726 ];
2727 let hierarchy = vec![
2728 hitem(None, None, None, Some(3)),
2729 hitem(Some(0), None, Some(2), None),
2730 hitem(Some(0), Some(1), Some(3), None),
2731 hitem(Some(0), Some(2), None, None),
2732 ];
2733
2734 let keys = precompute_reconciliation_keys(&node_data, &hierarchy);
2735 assert_eq!(keys.len(), node_data.len());
2736 for (i, k) in keys.iter().enumerate() {
2737 assert_eq!(
2738 *k,
2739 calculate_reconciliation_key(&node_data, &hierarchy, NodeId::new(i)),
2740 "precomputed key for node {i} disagrees with the direct call",
2741 );
2742 }
2743 }
2744
2745 #[test]
2746 fn autotest_rec_key_explicit_key_beats_css_id_and_node_type() {
2747 let bare = NodeData::create_div().with_key(7u32);
2750 let decorated = NodeData::create_text("totally different")
2751 .with_key(7u32)
2752 .with_ids_and_classes(
2753 vec![IdOrClass::Id("hero".into()), IdOrClass::Class("x".into())].into(),
2754 );
2755
2756 let a = calculate_reconciliation_key(&[bare], &[], NodeId::new(0));
2757 let b = calculate_reconciliation_key(&[decorated], &[], NodeId::new(0));
2758 assert_eq!(a, b, "an explicit .with_key() must dominate every other input");
2759 }
2760
2761 #[test]
2762 fn autotest_rec_key_css_id_used_when_no_explicit_key() {
2763 let same_a = calculate_reconciliation_key(&[id_node("hero")], &[], NodeId::new(0));
2764 let same_b = calculate_reconciliation_key(&[id_node("hero")], &[], NodeId::new(0));
2765 let other = calculate_reconciliation_key(&[id_node("footer")], &[], NodeId::new(0));
2766
2767 assert_eq!(same_a, same_b, "the CSS-ID key must be stable");
2768 assert_ne!(same_a, other, "different CSS IDs must produce different keys");
2769 }
2770
2771 #[test]
2772 fn autotest_rec_key_classes_participate_in_the_structural_key() {
2773 let a = calculate_reconciliation_key(&[class_node("alpha")], &[], NodeId::new(0));
2774 let b = calculate_reconciliation_key(&[class_node("beta")], &[], NodeId::new(0));
2775 assert_ne!(a, b, "classes must feed the structural key");
2776 }
2777
2778 #[test]
2779 fn autotest_rec_key_node_type_participates_in_the_structural_key() {
2780 let div = calculate_reconciliation_key(&[NodeData::create_div()], &[], NodeId::new(0));
2781 let txt =
2782 calculate_reconciliation_key(&[NodeData::create_text("x")], &[], NodeId::new(0));
2783 assert_ne!(div, txt, "the node-type discriminant must feed the structural key");
2784 }
2785
2786 #[test]
2787 fn autotest_rec_key_hierarchy_shorter_than_node_data_is_safe() {
2788 let node_data = vec![NodeData::create_div(), class_node("a"), id_node("b")];
2791
2792 let with_none = precompute_reconciliation_keys(&node_data, &[]);
2793 let with_short = precompute_reconciliation_keys(&node_data, &[hitem(None, None, None, None)]);
2794
2795 assert_eq!(with_none.len(), 3);
2796 assert_eq!(with_short.len(), 3);
2797 assert_eq!(with_none[0], with_short[0]);
2799 }
2800
2801 #[test]
2802 fn autotest_rec_key_identical_leaves_under_different_parents_differ() {
2803 let node_data = vec![
2810 NodeData::create_div(),
2811 id_node("left"),
2812 id_node("right"),
2813 NodeData::create_div(),
2814 NodeData::create_div(),
2815 ];
2816 let hierarchy = vec![
2817 hitem(None, None, None, Some(2)), hitem(Some(0), None, Some(2), Some(3)), hitem(Some(0), Some(1), None, Some(4)), hitem(Some(1), None, None, None), hitem(Some(2), None, None, None), ];
2823
2824 let k3 = calculate_reconciliation_key(&node_data, &hierarchy, NodeId::new(3));
2825 let k4 = calculate_reconciliation_key(&node_data, &hierarchy, NodeId::new(4));
2826 assert_ne!(k3, k4, "identical leaves under different parents must not share a key");
2827 }
2828
2829 #[test]
2834 fn autotest_contenteditable_key_is_deterministic_and_honours_explicit_keys() {
2835 let node_data = vec![NodeData::create_div().with_key(99u64)];
2836 let a = calculate_contenteditable_key(&node_data, &[], NodeId::new(0));
2837 let b = calculate_contenteditable_key(&node_data, &[], NodeId::new(0));
2838 assert_eq!(a, b, "must be deterministic");
2839
2840 assert_eq!(
2843 a,
2844 calculate_reconciliation_key(&node_data, &[], NodeId::new(0)),
2845 "explicit keys must be identical across both key functions",
2846 );
2847 }
2848
2849 #[test]
2850 fn autotest_contenteditable_key_distinguishes_nth_of_type() {
2851 let node_data = vec![
2854 NodeData::create_div(),
2855 NodeData::create_text("A"),
2856 NodeData::create_text("B"),
2857 ];
2858 let hierarchy = vec![
2859 hitem(None, None, None, Some(2)),
2860 hitem(Some(0), None, Some(2), None),
2861 hitem(Some(0), Some(1), None, None),
2862 ];
2863
2864 let k1 = calculate_contenteditable_key(&node_data, &hierarchy, NodeId::new(1));
2865 let k2 = calculate_contenteditable_key(&node_data, &hierarchy, NodeId::new(2));
2866 assert_ne!(k1, k2, "same-type siblings must differ by nth-of-type");
2867 }
2868
2869 #[test]
2870 fn autotest_contenteditable_key_empty_hierarchy_is_safe() {
2871 let node_data = vec![NodeData::create_div(), class_node("editor")];
2872 for i in 0..node_data.len() {
2873 let k = calculate_contenteditable_key(&node_data, &[], NodeId::new(i));
2874 assert_eq!(k, calculate_contenteditable_key(&node_data, &[], NodeId::new(i)));
2875 }
2876 }
2877
2878 #[test]
2883 fn autotest_reconcile_empty_to_empty_is_a_no_op() {
2884 let r = diff_flat(&[], &[]);
2885 assert!(r.events.is_empty());
2886 assert!(r.node_moves.is_empty());
2887 }
2888
2889 #[test]
2890 fn autotest_reconcile_mount_and_unmount_need_a_callback_to_fire() {
2891 let silent_new = vec![NodeData::create_div()];
2894 let r = diff_flat(&[], &silent_new);
2895 assert!(r.events.is_empty(), "no callback -> no event");
2896 assert!(r.node_moves.is_empty());
2897
2898 let loud_new = vec![with_cb(NodeData::create_div(), ComponentEventFilter::AfterMount)];
2899 let r = diff_flat(&[], &loud_new);
2900 assert_eq!(count_events(&r, EventType::Mount), 1);
2901
2902 let loud_old = vec![with_cb(
2903 NodeData::create_div(),
2904 ComponentEventFilter::BeforeUnmount,
2905 )];
2906 let r = diff_flat(&loud_old, &[]);
2907 assert_eq!(count_events(&r, EventType::Unmount), 1);
2908 assert!(r.node_moves.is_empty());
2909 }
2910
2911 #[test]
2912 fn autotest_reconcile_node_moves_are_a_bijection() {
2913 let old: Vec<NodeData> = (0..50).map(|_| NodeData::create_div()).collect();
2917 let new: Vec<NodeData> = (0..50).map(|_| NodeData::create_div()).collect();
2918
2919 let r = diff_flat(&old, &new);
2920 assert_eq!(r.node_moves.len(), 50);
2921
2922 let mut seen_old = vec![false; 50];
2923 let mut seen_new = vec![false; 50];
2924 for m in &r.node_moves {
2925 assert!(!seen_old[m.old_node_id.index()], "old node claimed twice");
2926 assert!(!seen_new[m.new_node_id.index()], "new node matched twice");
2927 seen_old[m.old_node_id.index()] = true;
2928 seen_new[m.new_node_id.index()] = true;
2929 }
2930 assert!(seen_old.iter().all(|b| *b), "every old node must be claimed");
2931 assert!(seen_new.iter().all(|b| *b), "every new node must be matched");
2932 assert!(r.events.is_empty(), "no lifecycle callbacks -> no events");
2933 }
2934
2935 #[test]
2936 fn autotest_reconcile_surplus_new_nodes_mount_and_surplus_old_unmount() {
2937 let old: Vec<NodeData> = (0..50).map(|_| NodeData::create_div()).collect();
2939 let new: Vec<NodeData> = (0..60)
2940 .map(|_| with_cb(NodeData::create_div(), ComponentEventFilter::AfterMount))
2941 .collect();
2942
2943 let r = diff_flat(&old, &new);
2944 assert_eq!(r.node_moves.len(), 50);
2945 assert_eq!(count_events(&r, EventType::Mount), 10);
2946 assert_eq!(count_events(&r, EventType::Unmount), 0);
2947
2948 let old: Vec<NodeData> = (0..50)
2950 .map(|_| with_cb(NodeData::create_div(), ComponentEventFilter::BeforeUnmount))
2951 .collect();
2952 let new: Vec<NodeData> = (0..40).map(|_| NodeData::create_div()).collect();
2953
2954 let r = diff_flat(&old, &new);
2955 assert_eq!(r.node_moves.len(), 40);
2956 assert_eq!(count_events(&r, EventType::Unmount), 10);
2957 assert_eq!(count_events(&r, EventType::Mount), 0);
2958 }
2959
2960 #[test]
2961 fn autotest_reconcile_explicit_key_mismatch_mounts_instead_of_guessing() {
2962 let old = vec![with_cb(
2966 NodeData::create_text("same content").with_key(1u32),
2967 ComponentEventFilter::BeforeUnmount,
2968 )];
2969 let new = vec![with_cb(
2970 NodeData::create_text("same content").with_key(2u32),
2971 ComponentEventFilter::AfterMount,
2972 )];
2973
2974 let r = diff_flat(&old, &new);
2975 assert!(
2976 r.node_moves.is_empty(),
2977 "keys 1 and 2 must not match, got {:?}",
2978 r.node_moves,
2979 );
2980 assert_eq!(count_events(&r, EventType::Mount), 1);
2981 assert_eq!(count_events(&r, EventType::Unmount), 1);
2982 }
2983
2984 #[test]
2985 fn autotest_reconcile_update_fires_only_on_rec_key_match_with_changed_content() {
2986 let old = vec![NodeData::create_text("v1").with_key(1u32)];
2988 let new = vec![with_cb(
2989 NodeData::create_text("v2").with_key(1u32),
2990 ComponentEventFilter::Updated,
2991 )];
2992
2993 let r = diff_flat(&old, &new);
2994 assert_eq!(r.node_moves.len(), 1, "the key must match across frames");
2995 assert_eq!(count_events(&r, EventType::Update), 1);
2996
2997 let stable = with_cb(
3002 NodeData::create_text("v1").with_key(1u32),
3003 ComponentEventFilter::Updated,
3004 );
3005 let old = vec![stable.clone()];
3006 let new = vec![stable];
3007
3008 let r = diff_flat(&old, &new);
3009 assert_eq!(r.node_moves.len(), 1);
3010 assert_eq!(
3011 count_events(&r, EventType::Update),
3012 0,
3013 "unchanged content must not fire Update",
3014 );
3015 }
3016
3017 #[test]
3018 fn autotest_reconcile_update_requires_the_callback() {
3019 let old = vec![NodeData::create_text("v1").with_key(1u32)];
3021 let new = vec![NodeData::create_text("v2").with_key(1u32)];
3022 let r = diff_flat(&old, &new);
3023 assert_eq!(r.node_moves.len(), 1);
3024 assert!(r.events.is_empty());
3025 }
3026
3027 #[test]
3028 fn autotest_reconcile_missing_layout_entries_default_to_zero_rect() {
3029 let old = vec![NodeData::create_div()];
3032 let new = vec![with_cb(
3033 NodeData::create_div(),
3034 ComponentEventFilter::NodeResized,
3035 )];
3036
3037 let r = diff_flat(&old, &new);
3038 assert_eq!(r.node_moves.len(), 1);
3039 assert_eq!(
3040 count_events(&r, EventType::Resize),
3041 0,
3042 "zero-vs-zero bounds must not be treated as a resize",
3043 );
3044 }
3045
3046 #[test]
3047 fn autotest_reconcile_resize_fires_with_previous_and_current_bounds() {
3048 let old = vec![NodeData::create_div()];
3049 let new = vec![with_cb(
3050 NodeData::create_div(),
3051 ComponentEventFilter::NodeResized,
3052 )];
3053
3054 let r = reconcile_dom(
3055 &old,
3056 &new,
3057 &[],
3058 &[],
3059 &layout_of(&[(0, rect(100.0, 50.0))]),
3060 &layout_of(&[(0, rect(100.0, 80.0))]),
3061 DomId::ROOT_ID,
3062 Instant::now(),
3063 );
3064
3065 assert_eq!(count_events(&r, EventType::Resize), 1);
3066 let EventData::Lifecycle(data) = &r.events[0].data else {
3067 panic!("resize event must carry EventData::Lifecycle");
3068 };
3069 assert_eq!(data.reason, LifecycleReason::Resize);
3070 assert_eq!(data.previous_bounds, Some(rect(100.0, 50.0)));
3071 assert_eq!(data.current_bounds, rect(100.0, 80.0));
3072 }
3073
3074 #[test]
3075 fn autotest_reconcile_resize_ignores_pure_translation() {
3076 let old = vec![NodeData::create_div()];
3078 let new = vec![with_cb(
3079 NodeData::create_div(),
3080 ComponentEventFilter::NodeResized,
3081 )];
3082
3083 let moved = LogicalRect::new(LogicalPosition::new(999.0, 999.0), LogicalSize::new(10.0, 10.0));
3084 let r = reconcile_dom(
3085 &old,
3086 &new,
3087 &[],
3088 &[],
3089 &layout_of(&[(0, rect(10.0, 10.0))]),
3090 &layout_of(&[(0, moved)]),
3091 DomId::ROOT_ID,
3092 Instant::now(),
3093 );
3094 assert_eq!(count_events(&r, EventType::Resize), 0);
3095 }
3096
3097 #[test]
3098 fn autotest_reconcile_nan_bounds_do_not_fire_a_resize_every_frame() {
3099 let old = vec![NodeData::create_div()];
3113 let new = vec![with_cb(
3114 NodeData::create_div(),
3115 ComponentEventFilter::NodeResized,
3116 )];
3117
3118 let nan = rect(f32::NAN, f32::NAN);
3119 let r = reconcile_dom(
3120 &old,
3121 &new,
3122 &[],
3123 &[],
3124 &layout_of(&[(0, nan)]),
3125 &layout_of(&[(0, nan)]),
3126 DomId::ROOT_ID,
3127 Instant::now(),
3128 );
3129 assert_eq!(r.node_moves.len(), 1);
3130 assert_eq!(
3131 count_events(&r, EventType::Resize),
3132 0,
3133 "an unchanged NaN size must not be reported as a resize",
3134 );
3135
3136 let inf = rect(f32::INFINITY, f32::NEG_INFINITY);
3139 let r = reconcile_dom(
3140 &old,
3141 &new,
3142 &[],
3143 &[],
3144 &layout_of(&[(0, inf)]),
3145 &layout_of(&[(0, inf)]),
3146 DomId::ROOT_ID,
3147 Instant::now(),
3148 );
3149 assert_eq!(
3150 count_events(&r, EventType::Resize),
3151 0,
3152 "infinite-but-equal bounds must not be treated as a resize",
3153 );
3154
3155 let r = reconcile_dom(
3157 &old,
3158 &new,
3159 &[],
3160 &[],
3161 &layout_of(&[(0, nan)]),
3162 &layout_of(&[(0, rect(10.0, 20.0))]),
3163 DomId::ROOT_ID,
3164 Instant::now(),
3165 );
3166 assert_eq!(
3167 count_events(&r, EventType::Resize),
3168 1,
3169 "NaN -> a real size is a real resize",
3170 );
3171 }
3172
3173 #[test]
3174 fn autotest_reconcile_extreme_bounds_do_not_panic() {
3175 let old = vec![NodeData::create_div()];
3178 let new = vec![with_cb(
3179 NodeData::create_div(),
3180 ComponentEventFilter::NodeResized,
3181 )];
3182
3183 for (a, b) in [
3184 (rect(f32::MIN, f32::MAX), rect(f32::MAX, f32::MIN)),
3185 (rect(f32::MIN_POSITIVE, 0.0), rect(0.0, f32::MIN_POSITIVE)),
3186 (rect(-0.0, 0.0), rect(0.0, -0.0)), ] {
3188 let r = reconcile_dom(
3189 &old,
3190 &new,
3191 &[],
3192 &[],
3193 &layout_of(&[(0, a)]),
3194 &layout_of(&[(0, b)]),
3195 DomId::ROOT_ID,
3196 Instant::now(),
3197 );
3198 assert_eq!(r.node_moves.len(), 1);
3199 }
3200 }
3201
3202 #[test]
3203 fn autotest_reconcile_keyless_tiers_respect_the_parent_key_gate() {
3204 let old_nd = vec![
3212 NodeData::create_div(),
3213 id_node("left"),
3214 NodeData::create_text("leaf"),
3215 ];
3216 let old_hier = vec![
3217 hitem(None, None, None, Some(1)),
3218 hitem(Some(0), None, None, Some(2)),
3219 hitem(Some(1), None, None, None),
3220 ];
3221
3222 let new_nd = vec![
3223 NodeData::create_div(),
3224 id_node("right"),
3225 NodeData::create_text("leaf"),
3226 ];
3227 let new_hier = old_hier.clone();
3228
3229 let r = reconcile_dom(
3230 &old_nd,
3231 &new_nd,
3232 &old_hier,
3233 &new_hier,
3234 &no_layout(),
3235 &no_layout(),
3236 DomId::ROOT_ID,
3237 Instant::now(),
3238 );
3239
3240 let leaf_matched = r
3243 .node_moves
3244 .iter()
3245 .any(|m| m.new_node_id.index() == 2 && m.old_node_id.index() == 2);
3246 assert!(
3247 !leaf_matched,
3248 "a leaf must not migrate across parents; moves = {:?}",
3249 r.node_moves,
3250 );
3251 }
3252
3253 #[test]
3258 fn autotest_migration_map_empty_and_large() {
3259 assert!(create_migration_map(&[]).is_empty());
3260
3261 let moves: Vec<NodeMove> = (0..1000)
3262 .map(|i| NodeMove {
3263 old_node_id: NodeId::new(i),
3264 new_node_id: NodeId::new(i * 2),
3265 })
3266 .collect();
3267 let map = create_migration_map(&moves);
3268 assert_eq!(map.len(), 1000);
3269 assert_eq!(map.get(&NodeId::new(999)), Some(&NodeId::new(1998)));
3270 }
3271
3272 #[test]
3273 fn autotest_migration_map_duplicate_old_id_keeps_the_last_write() {
3274 let moves = vec![
3277 NodeMove {
3278 old_node_id: NodeId::new(0),
3279 new_node_id: NodeId::new(5),
3280 },
3281 NodeMove {
3282 old_node_id: NodeId::new(0),
3283 new_node_id: NodeId::new(9),
3284 },
3285 ];
3286 let map = create_migration_map(&moves);
3287 assert_eq!(map.len(), 1);
3288 assert_eq!(map.get(&NodeId::new(0)), Some(&NodeId::new(9)));
3289 }
3290
3291 #[test]
3292 fn autotest_migration_map_round_trips_a_real_diff() {
3293 let old: Vec<NodeData> = (0..8).map(|_| NodeData::create_div()).collect();
3294 let new: Vec<NodeData> = (0..8).map(|_| NodeData::create_div()).collect();
3295 let r = diff_flat(&old, &new);
3296
3297 let map = create_migration_map(&r.node_moves);
3298 assert_eq!(map.len(), r.node_moves.len());
3299 for m in &r.node_moves {
3300 assert_eq!(map.get(&m.old_node_id), Some(&m.new_node_id));
3301 }
3302 }
3303
3304 #[allow(dead_code)]
3309 struct TestState(u32);
3310
3311 extern "C" fn merge_keep_old(_new_data: RefAny, old_data: RefAny) -> RefAny {
3314 old_data
3315 }
3316
3317 #[test]
3318 fn autotest_transfer_states_out_of_range_moves_are_skipped() {
3319 let mut old = vec![NodeData::create_div()];
3322 let mut new = vec![NodeData::create_div()];
3323
3324 let moves = vec![
3325 NodeMove {
3326 old_node_id: NodeId::new(5), new_node_id: NodeId::new(0),
3328 },
3329 NodeMove {
3330 old_node_id: NodeId::new(0),
3331 new_node_id: NodeId::new(7), },
3333 NodeMove {
3334 old_node_id: NodeId::new(usize::MAX),
3335 new_node_id: NodeId::new(usize::MAX),
3336 },
3337 ];
3338
3339 transfer_states(&mut old, &mut new, &moves); assert!(new[0].get_dataset().is_none());
3341 }
3342
3343 #[test]
3344 fn autotest_transfer_states_without_merge_callback_leaves_datasets_intact() {
3345 let mut old = vec![NodeData::create_div()];
3346 old[0].set_dataset(OptionRefAny::Some(RefAny::new(TestState(1))));
3347
3348 let mut new = vec![NodeData::create_div()];
3349 new[0].set_dataset(OptionRefAny::Some(RefAny::new(TestState(2))));
3350
3351 let old_ptr = old[0].get_dataset().unwrap().sharing_info.ptr as usize;
3352 let new_ptr = new[0].get_dataset().unwrap().sharing_info.ptr as usize;
3353
3354 transfer_states(
3355 &mut old,
3356 &mut new,
3357 &[NodeMove {
3358 old_node_id: NodeId::new(0),
3359 new_node_id: NodeId::new(0),
3360 }],
3361 );
3362
3363 assert_eq!(
3365 old[0].get_dataset().unwrap().sharing_info.ptr as usize,
3366 old_ptr,
3367 );
3368 assert_eq!(
3369 new[0].get_dataset().unwrap().sharing_info.ptr as usize,
3370 new_ptr,
3371 );
3372 }
3373
3374 #[test]
3375 fn autotest_transfer_states_with_one_missing_dataset_restores_both_sides() {
3376 let mut old = vec![NodeData::create_div()];
3379 let mut new = vec![NodeData::create_div()];
3380 new[0].set_merge_callback(merge_keep_old as DatasetMergeCallbackType);
3381 new[0].set_dataset(OptionRefAny::Some(RefAny::new(TestState(2))));
3382
3383 let new_ptr = new[0].get_dataset().unwrap().sharing_info.ptr as usize;
3384
3385 transfer_states(
3386 &mut old,
3387 &mut new,
3388 &[NodeMove {
3389 old_node_id: NodeId::new(0),
3390 new_node_id: NodeId::new(0),
3391 }],
3392 );
3393
3394 assert!(old[0].get_dataset().is_none());
3395 assert_eq!(
3396 new[0].get_dataset().unwrap().sharing_info.ptr as usize,
3397 new_ptr,
3398 "the fresh dataset must be restored, not dropped",
3399 );
3400 }
3401
3402 #[test]
3403 fn autotest_transfer_states_repoints_orphaned_callback_refanys() {
3404 let fresh = RefAny::new(TestState(1));
3410 let fresh_ptr = fresh.sharing_info.ptr as usize;
3411
3412 let mut new0 = NodeData::create_div();
3413 new0.set_merge_callback(merge_keep_old as DatasetMergeCallbackType);
3414 new0.set_dataset(OptionRefAny::Some(fresh.clone()));
3415 new0.add_callback(
3417 EventFilter::Component(ComponentEventFilter::Selected),
3418 fresh.clone(),
3419 noop_callback(),
3420 );
3421
3422 let mut new1 = NodeData::create_div();
3425 new1.add_callback(
3426 EventFilter::Component(ComponentEventFilter::Selected),
3427 fresh.clone(),
3428 noop_callback(),
3429 );
3430
3431 let persistent = RefAny::new(TestState(2));
3432 let persistent_ptr = persistent.sharing_info.ptr as usize;
3433 assert_ne!(fresh_ptr, persistent_ptr, "test setup: allocations must differ");
3434
3435 let mut old = vec![NodeData::create_div()];
3436 old[0].set_dataset(OptionRefAny::Some(persistent));
3437
3438 let mut new = vec![new0, new1];
3439
3440 transfer_states(
3441 &mut old,
3442 &mut new,
3443 &[NodeMove {
3444 old_node_id: NodeId::new(0),
3445 new_node_id: NodeId::new(0),
3446 }],
3447 );
3448
3449 assert_eq!(
3451 new[0].get_dataset().unwrap().sharing_info.ptr as usize,
3452 persistent_ptr,
3453 "the merge must keep the persistent allocation",
3454 );
3455 assert!(old[0].get_dataset().is_none());
3457
3458 for (i, nd) in new.iter().enumerate() {
3461 for cb in nd.callbacks.as_ref() {
3462 assert_eq!(
3463 cb.refany.sharing_info.ptr as usize,
3464 persistent_ptr,
3465 "node {i}: an orphaned callback refany was not re-pointed",
3466 );
3467 }
3468 }
3469 }
3470
3471 #[test]
3476 fn autotest_accumulator_new_is_empty_and_inert() {
3477 let a = ChangeAccumulator::new();
3478 assert!(a.is_empty());
3479 assert!(!a.needs_layout());
3480 assert!(!a.needs_paint_only());
3481 assert!(a.is_visually_unchanged());
3482 assert_eq!(a.max_scope, RelayoutScope::None);
3483 let d = ChangeAccumulator::default();
3485 assert_eq!(a.is_empty(), d.is_empty());
3486 assert_eq!(a.max_scope, d.max_scope);
3487 }
3488
3489 #[test]
3490 fn autotest_accumulator_mount_forces_layout_unmount_does_not() {
3491 let mut a = ChangeAccumulator::new();
3492 a.add_mount(NodeId::new(0));
3493 assert!(!a.is_empty());
3494 assert!(a.needs_layout(), "a mounted node always needs layout");
3495 assert!(!a.needs_paint_only());
3496 assert!(!a.is_visually_unchanged());
3497
3498 let mut a = ChangeAccumulator::new();
3501 a.add_unmount(NodeId::new(0));
3502 assert!(!a.is_empty());
3503 assert!(!a.needs_layout());
3504 assert!(!a.is_visually_unchanged());
3505 }
3506
3507 #[test]
3508 fn autotest_accumulator_css_change_routes_paint_vs_layout_by_scope() {
3509 let mut a = ChangeAccumulator::new();
3511 a.add_css_change(NodeId::new(0), CssPropertyType::TextColor, RelayoutScope::None);
3512 assert!(!a.needs_layout());
3513 assert!(a.needs_paint_only());
3514 assert!(!a.is_visually_unchanged());
3515 assert_eq!(a.max_scope, RelayoutScope::None);
3516 assert!(a.per_node[&NodeId::new(0)]
3517 .change_set
3518 .contains(NodeChangeSet::INLINE_STYLE_PAINT));
3519
3520 let mut a = ChangeAccumulator::new();
3522 a.add_css_change(NodeId::new(0), CssPropertyType::Width, RelayoutScope::SizingOnly);
3523 assert!(a.needs_layout());
3524 assert!(!a.needs_paint_only(), "layout work subsumes paint-only");
3525 assert_eq!(a.max_scope, RelayoutScope::SizingOnly);
3526 assert!(a.per_node[&NodeId::new(0)]
3527 .change_set
3528 .contains(NodeChangeSet::INLINE_STYLE_LAYOUT));
3529 }
3530
3531 #[test]
3532 fn autotest_accumulator_max_scope_is_monotone() {
3533 let mut a = ChangeAccumulator::new();
3536 a.add_css_change(NodeId::new(0), CssPropertyType::Display, RelayoutScope::Full);
3537 assert_eq!(a.max_scope, RelayoutScope::Full);
3538
3539 a.add_css_change(NodeId::new(0), CssPropertyType::TextColor, RelayoutScope::None);
3540 assert_eq!(a.max_scope, RelayoutScope::Full, "max_scope must not regress");
3541 assert_eq!(
3542 a.per_node[&NodeId::new(0)].relayout_scope,
3543 RelayoutScope::Full,
3544 "per-node scope must not regress either",
3545 );
3546
3547 a.add_css_change(NodeId::new(1), CssPropertyType::Width, RelayoutScope::SizingOnly);
3548 assert_eq!(a.max_scope, RelayoutScope::Full);
3549 assert_eq!(
3550 a.per_node[&NodeId::new(1)].relayout_scope,
3551 RelayoutScope::SizingOnly,
3552 "a different node keeps its own, lower scope",
3553 );
3554 }
3555
3556 #[test]
3557 fn autotest_accumulator_text_change_is_ifc_scoped_and_unicode_safe() {
3558 for s in UNICODE_SAMPLES {
3559 let mut a = ChangeAccumulator::new();
3560 a.add_text_change(NodeId::new(0), String::new(), (*s).to_string());
3561
3562 let report = &a.per_node[&NodeId::new(0)];
3563 assert!(report.change_set.contains(NodeChangeSet::TEXT_CONTENT));
3564 assert_eq!(report.relayout_scope, RelayoutScope::IfcOnly);
3565 assert_eq!(
3566 report.text_change,
3567 Some(TextChange {
3568 old_text: String::new(),
3569 new_text: (*s).to_string(),
3570 }),
3571 );
3572 assert!(a.needs_layout());
3573 assert_eq!(a.max_scope, RelayoutScope::IfcOnly);
3574 }
3575 }
3576
3577 #[test]
3578 fn autotest_accumulator_add_dom_change_accumulates_and_never_clears_text() {
3579 let node = NodeId::new(0);
3580 let mut a = ChangeAccumulator::new();
3581
3582 a.add_dom_change(
3583 node,
3584 NodeChangeSet {
3585 bits: NodeChangeSet::TEXT_CONTENT,
3586 },
3587 RelayoutScope::IfcOnly,
3588 Some(TextChange {
3589 old_text: "a".to_string(),
3590 new_text: "b".to_string(),
3591 }),
3592 vec![CssPropertyType::Width],
3593 );
3594
3595 a.add_dom_change(
3597 node,
3598 NodeChangeSet {
3599 bits: NodeChangeSet::STYLED_STATE,
3600 },
3601 RelayoutScope::None,
3602 None,
3603 vec![CssPropertyType::TextColor],
3604 );
3605
3606 let report = &a.per_node[&node];
3607 assert!(report.change_set.contains(NodeChangeSet::TEXT_CONTENT));
3608 assert!(
3609 report.change_set.contains(NodeChangeSet::STYLED_STATE),
3610 "flags must be OR-accumulated across calls",
3611 );
3612 assert_eq!(report.relayout_scope, RelayoutScope::IfcOnly);
3613 assert!(
3614 report.text_change.is_some(),
3615 "a None text_change must not erase a previously recorded one",
3616 );
3617 assert_eq!(
3618 report.changed_css_properties,
3619 vec![CssPropertyType::Width, CssPropertyType::TextColor],
3620 "changed properties must be appended, not replaced",
3621 );
3622 }
3623
3624 #[test]
3625 fn autotest_accumulator_image_change() {
3626 let mut a = ChangeAccumulator::new();
3627 a.add_image_change(NodeId::new(0), RelayoutScope::SizingOnly);
3628 assert!(a.per_node[&NodeId::new(0)]
3629 .change_set
3630 .contains(NodeChangeSet::IMAGE_CHANGED));
3631 assert!(a.needs_layout());
3632 assert_eq!(a.max_scope, RelayoutScope::SizingOnly);
3633 }
3634
3635 #[test]
3636 fn autotest_accumulator_merge_empty_restyle_result_is_a_no_op() {
3637 let mut a = ChangeAccumulator::new();
3638 a.merge_restyle_result(&RestyleResult::default());
3639 assert!(a.is_empty());
3640 assert!(a.is_visually_unchanged());
3641 }
3642
3643 #[test]
3644 fn autotest_accumulator_merge_restyle_result_classifies_by_property() {
3645 let prop = CssProperty::Width(CssPropertyValue::Exact(LayoutWidth::const_px(100)));
3646 let changed = ChangedCssProperty {
3647 previous_state: StyledNodeState::default(),
3648 previous_prop: prop.clone(),
3649 current_state: StyledNodeState::default(),
3650 current_prop: prop,
3651 };
3652
3653 let mut restyle = RestyleResult::default();
3654 restyle
3655 .changed_nodes
3656 .insert(NodeId::new(3), vec![changed]);
3657
3658 let mut a = ChangeAccumulator::new();
3659 a.merge_restyle_result(&restyle);
3660
3661 assert!(!a.is_empty());
3663 assert!(a.needs_layout());
3664 assert_eq!(a.max_scope, RelayoutScope::SizingOnly);
3665 let report = &a.per_node[&NodeId::new(3)];
3666 assert!(report.change_set.contains(NodeChangeSet::INLINE_STYLE_LAYOUT));
3667 assert_eq!(report.changed_css_properties, vec![CssPropertyType::Width]);
3668 }
3669
3670 #[test]
3671 fn autotest_accumulator_merge_extended_diff_counts_mounts_and_unmounts() {
3672 let old_nd = vec![NodeData::create_div(), NodeData::create_div()];
3674 let new_nd = vec![
3675 NodeData::create_div(),
3676 NodeData::create_div(),
3677 NodeData::create_div(),
3678 ];
3679
3680 let mut a = ChangeAccumulator::new();
3681 a.merge_extended_diff(&ExtendedDiffResult::default(), &old_nd, &new_nd);
3682
3683 assert_eq!(a.mounted_nodes.len(), 3);
3684 assert_eq!(a.unmounted_nodes.len(), 2);
3685 assert!(!a.is_empty());
3686 assert!(a.needs_layout(), "mounted nodes always need layout");
3687 assert!(!a.is_visually_unchanged());
3688 }
3689
3690 #[test]
3691 fn autotest_accumulator_merge_extended_diff_on_empty_doms_is_empty() {
3692 let mut a = ChangeAccumulator::new();
3693 a.merge_extended_diff(&ExtendedDiffResult::default(), &[], &[]);
3694 assert!(a.is_empty());
3695 }
3696
3697 #[test]
3698 fn autotest_accumulator_merge_extended_diff_skips_empty_change_sets() {
3699 let old_nd = vec![NodeData::create_div()];
3701 let new_nd = vec![NodeData::create_div()];
3702
3703 let extended = ExtendedDiffResult {
3704 diff: DiffResult {
3705 events: Vec::new(),
3706 node_moves: vec![NodeMove {
3707 old_node_id: NodeId::new(0),
3708 new_node_id: NodeId::new(0),
3709 }],
3710 },
3711 node_changes: vec![(NodeId::new(0), NodeId::new(0), NodeChangeSet::empty())],
3712 };
3713
3714 let mut a = ChangeAccumulator::new();
3715 a.merge_extended_diff(&extended, &old_nd, &new_nd);
3716
3717 assert!(a.per_node.is_empty(), "an empty change set must be skipped");
3718 assert!(a.mounted_nodes.is_empty());
3719 assert!(a.unmounted_nodes.is_empty());
3720 assert!(a.is_empty());
3721 }
3722
3723 #[test]
3724 fn autotest_accumulator_merge_extended_diff_extracts_text_change() {
3725 let old_nd = vec![NodeData::create_text("héllo")];
3726 let new_nd = vec![NodeData::create_text("héllo wörld")];
3727
3728 let extended = ExtendedDiffResult {
3729 diff: DiffResult {
3730 events: Vec::new(),
3731 node_moves: vec![NodeMove {
3732 old_node_id: NodeId::new(0),
3733 new_node_id: NodeId::new(0),
3734 }],
3735 },
3736 node_changes: vec![(
3737 NodeId::new(0),
3738 NodeId::new(0),
3739 NodeChangeSet {
3740 bits: NodeChangeSet::TEXT_CONTENT,
3741 },
3742 )],
3743 };
3744
3745 let mut a = ChangeAccumulator::new();
3746 a.merge_extended_diff(&extended, &old_nd, &new_nd);
3747
3748 let report = &a.per_node[&NodeId::new(0)];
3749 assert_eq!(
3750 report.text_change,
3751 Some(TextChange {
3752 old_text: "héllo".to_string(),
3753 new_text: "héllo wörld".to_string(),
3754 }),
3755 "TEXT_CONTENT must carry the old/new text for cursor reconciliation",
3756 );
3757 assert_eq!(report.relayout_scope, RelayoutScope::IfcOnly);
3758 }
3759
3760 #[test]
3765 fn autotest_classify_scope_maps_each_flag_to_its_documented_scope() {
3766 let nodes = vec![NodeData::create_div()];
3767 let id = NodeId::new(0);
3768
3769 let classify = |bits: u32| {
3770 ChangeAccumulator::classify_change_scope(NodeChangeSet { bits }, &nodes, id)
3771 };
3772
3773 assert_eq!(classify(0), RelayoutScope::None, "empty -> no work");
3774 assert_eq!(classify(NodeChangeSet::NODE_TYPE_CHANGED), RelayoutScope::Full);
3775 assert_eq!(classify(NodeChangeSet::CHILDREN_CHANGED), RelayoutScope::Full);
3776 assert_eq!(classify(NodeChangeSet::IDS_AND_CLASSES), RelayoutScope::Full);
3777 assert_eq!(classify(NodeChangeSet::TEXT_CONTENT), RelayoutScope::IfcOnly);
3778 assert_eq!(classify(NodeChangeSet::IMAGE_CHANGED), RelayoutScope::SizingOnly);
3779 assert_eq!(classify(NodeChangeSet::CONTENTEDITABLE), RelayoutScope::SizingOnly);
3780 assert_eq!(classify(NodeChangeSet::STYLED_STATE), RelayoutScope::None);
3781 assert_eq!(classify(NodeChangeSet::INLINE_STYLE_PAINT), RelayoutScope::None);
3782 assert_eq!(classify(NodeChangeSet::CALLBACKS), RelayoutScope::None);
3784 assert_eq!(classify(NodeChangeSet::DATASET), RelayoutScope::None);
3785 assert_eq!(classify(NodeChangeSet::TAB_INDEX), RelayoutScope::None);
3786 }
3787
3788 #[test]
3789 fn autotest_classify_scope_precedence_is_widest_first() {
3790 let nodes = vec![NodeData::create_div()];
3791 let id = NodeId::new(0);
3792
3793 let bits = NodeChangeSet::NODE_TYPE_CHANGED
3795 | NodeChangeSet::TEXT_CONTENT
3796 | NodeChangeSet::IMAGE_CHANGED
3797 | NodeChangeSet::STYLED_STATE;
3798 assert_eq!(
3799 ChangeAccumulator::classify_change_scope(NodeChangeSet { bits }, &nodes, id),
3800 RelayoutScope::Full,
3801 );
3802
3803 let bits = NodeChangeSet::TEXT_CONTENT | NodeChangeSet::IMAGE_CHANGED;
3806 assert_eq!(
3807 ChangeAccumulator::classify_change_scope(NodeChangeSet { bits }, &nodes, id),
3808 RelayoutScope::IfcOnly,
3809 );
3810 }
3811
3812 #[test]
3813 fn autotest_classify_scope_inline_layout_walks_the_nodes_own_css() {
3814 let nodes = vec![NodeData::create_div().with_css("width: 100px")];
3816 assert_eq!(
3817 ChangeAccumulator::classify_change_scope(
3818 NodeChangeSet {
3819 bits: NodeChangeSet::INLINE_STYLE_LAYOUT,
3820 },
3821 &nodes,
3822 NodeId::new(0),
3823 ),
3824 RelayoutScope::SizingOnly,
3825 );
3826
3827 let nodes = vec![NodeData::create_div().with_css("display: flex")];
3829 assert_eq!(
3830 ChangeAccumulator::classify_change_scope(
3831 NodeChangeSet {
3832 bits: NodeChangeSet::INLINE_STYLE_LAYOUT,
3833 },
3834 &nodes,
3835 NodeId::new(0),
3836 ),
3837 RelayoutScope::Full,
3838 );
3839
3840 let nodes = vec![NodeData::create_div()];
3844 assert_eq!(
3845 ChangeAccumulator::classify_change_scope(
3846 NodeChangeSet {
3847 bits: NodeChangeSet::INLINE_STYLE_LAYOUT,
3848 },
3849 &nodes,
3850 NodeId::new(0),
3851 ),
3852 RelayoutScope::SizingOnly,
3853 "an INLINE_STYLE_LAYOUT change must never classify as 'no layout'",
3854 );
3855 }
3856
3857 #[test]
3862 fn autotest_reconcile_with_changes_on_empty_doms() {
3863 let r = reconcile_dom_with_changes(
3864 &[],
3865 &[],
3866 &[],
3867 &[],
3868 None,
3869 None,
3870 &no_layout(),
3871 &no_layout(),
3872 DomId::ROOT_ID,
3873 Instant::now(),
3874 );
3875 assert!(r.diff.events.is_empty());
3876 assert!(r.diff.node_moves.is_empty());
3877 assert!(r.node_changes.is_empty());
3878 }
3879
3880 #[test]
3881 fn autotest_reconcile_with_changes_reports_one_entry_per_move() {
3882 let old = vec![NodeData::create_text("v1").with_key(1u32)];
3883 let new = vec![NodeData::create_text("v2").with_key(1u32)];
3884
3885 let r = reconcile_dom_with_changes(
3886 &old,
3887 &new,
3888 &[],
3889 &[],
3890 None,
3891 None,
3892 &no_layout(),
3893 &no_layout(),
3894 DomId::ROOT_ID,
3895 Instant::now(),
3896 );
3897
3898 assert_eq!(r.diff.node_moves.len(), 1);
3899 assert_eq!(
3900 r.node_changes.len(),
3901 r.diff.node_moves.len(),
3902 "there must be exactly one change entry per matched pair",
3903 );
3904
3905 let (old_id, new_id, changes) = &r.node_changes[0];
3906 assert_eq!(*old_id, NodeId::new(0));
3907 assert_eq!(*new_id, NodeId::new(0));
3908 assert!(changes.contains(NodeChangeSet::TEXT_CONTENT));
3909 }
3910
3911 #[test]
3912 fn autotest_reconcile_with_changes_tolerates_short_styled_state_slices() {
3913 let old = vec![NodeData::create_div(), NodeData::create_div()];
3916 let new = vec![NodeData::create_div(), NodeData::create_div()];
3917 let short = [StyledNodeState::default()]; let r = reconcile_dom_with_changes(
3920 &old,
3921 &new,
3922 &[],
3923 &[],
3924 Some(&short[..]),
3925 Some(&short[..]),
3926 &no_layout(),
3927 &no_layout(),
3928 DomId::ROOT_ID,
3929 Instant::now(),
3930 );
3931 assert_eq!(r.node_changes.len(), 2);
3932 for (_, _, changes) in &r.node_changes {
3934 assert!(!changes.contains(NodeChangeSet::STYLED_STATE));
3935 }
3936 }
3937
3938 #[test]
3939 fn autotest_reconcile_with_changes_feeds_the_accumulator() {
3940 let old = vec![NodeData::create_text("before").with_key(1u32)];
3942 let new = vec![NodeData::create_text("after").with_key(1u32)];
3943
3944 let extended = reconcile_dom_with_changes(
3945 &old,
3946 &new,
3947 &[],
3948 &[],
3949 None,
3950 None,
3951 &no_layout(),
3952 &no_layout(),
3953 DomId::ROOT_ID,
3954 Instant::now(),
3955 );
3956
3957 let mut acc = ChangeAccumulator::new();
3958 acc.merge_extended_diff(&extended, &old, &new);
3959
3960 assert!(!acc.is_empty());
3961 assert!(acc.needs_layout(), "a text edit needs (IFC) layout");
3962 assert!(!acc.is_visually_unchanged());
3963 assert!(acc.mounted_nodes.is_empty());
3964 assert!(acc.unmounted_nodes.is_empty());
3965
3966 let report = &acc.per_node[&NodeId::new(0)];
3967 assert_eq!(report.relayout_scope, RelayoutScope::IfcOnly);
3968 assert_eq!(
3969 report.text_change,
3970 Some(TextChange {
3971 old_text: "before".to_string(),
3972 new_text: "after".to_string(),
3973 }),
3974 );
3975 }
3976
3977 #[test]
3982 fn autotest_fingerprint_default_and_self_comparison_are_inert() {
3983 let d = NodeDataFingerprint::default();
3984 assert!(d.is_identical(&d));
3985 assert!(d.diff(&d).is_empty());
3986 assert!(!d.might_affect_layout(&d));
3987 assert!(!d.might_affect_visuals(&d));
3988 assert_eq!(d, NodeDataFingerprint::default());
3989 }
3990
3991 #[test]
3992 fn autotest_fingerprint_is_a_pure_function_of_its_inputs() {
3993 let state = StyledNodeState::default();
3996 for s in UNICODE_SAMPLES {
3997 let a = NodeDataFingerprint::compute(&NodeData::create_text(*s), Some(&state));
3998 let b = NodeDataFingerprint::compute(&NodeData::create_text(*s), Some(&state));
3999 assert_eq!(a, b, "fingerprint of {s:?} is not deterministic");
4000 assert!(a.is_identical(&b));
4001 assert!(a.diff(&b).is_empty());
4002 }
4003 }
4004
4005 #[test]
4006 fn autotest_fingerprint_diff_is_symmetric() {
4007 let a = NodeDataFingerprint::compute(&NodeData::create_text("a"), None);
4008 let b = NodeDataFingerprint::compute(&class_node("x").with_css("width: 1px"), None);
4009
4010 assert_eq!(a.diff(&b), b.diff(&a), "diff must be symmetric");
4011 assert_eq!(
4012 a.might_affect_layout(&b),
4013 b.might_affect_layout(&a),
4014 "might_affect_layout must be symmetric",
4015 );
4016 assert_eq!(a.might_affect_visuals(&b), b.might_affect_visuals(&a));
4017 }
4018
4019 #[test]
4020 fn autotest_fingerprint_text_change_is_layout_and_visual() {
4021 let a = NodeDataFingerprint::compute(&NodeData::create_text("one"), None);
4022 let b = NodeDataFingerprint::compute(&NodeData::create_text("two"), None);
4023
4024 assert!(!a.is_identical(&b));
4025 let changes = a.diff(&b);
4026 assert!(changes.contains(NodeChangeSet::TEXT_CONTENT));
4028 assert!(changes.contains(NodeChangeSet::IMAGE_CHANGED));
4029 assert!(a.might_affect_layout(&b));
4030 assert!(a.might_affect_visuals(&b));
4031 }
4032
4033 #[test]
4034 fn autotest_fingerprint_styled_state_is_visual_but_not_layout() {
4035 let node = NodeData::create_div();
4038 let calm = StyledNodeState::default();
4039 let hovered = StyledNodeState {
4040 hover: true,
4041 ..StyledNodeState::default()
4042 };
4043
4044 let a = NodeDataFingerprint::compute(&node, Some(&calm));
4045 let b = NodeDataFingerprint::compute(&node, Some(&hovered));
4046
4047 assert!(!a.is_identical(&b));
4048 assert!(a.diff(&b).contains(NodeChangeSet::STYLED_STATE));
4049 assert!(
4050 !a.might_affect_layout(&b),
4051 "a styled-state change must not be able to request layout",
4052 );
4053 assert!(a.might_affect_visuals(&b));
4054 }
4055
4056 #[test]
4057 fn autotest_fingerprint_callback_change_is_neither_layout_nor_visual() {
4058 let plain = NodeData::create_div();
4059 let with_handler = with_cb(NodeData::create_div(), ComponentEventFilter::AfterMount);
4060
4061 let a = NodeDataFingerprint::compute(&plain, None);
4062 let b = NodeDataFingerprint::compute(&with_handler, None);
4063
4064 assert!(!a.is_identical(&b), "the callback list must be fingerprinted");
4065 assert!(a.diff(&b).contains(NodeChangeSet::CALLBACKS));
4066 assert!(
4067 !a.might_affect_layout(&b),
4068 "swapping an event handler must not trigger relayout",
4069 );
4070 assert!(
4071 !a.might_affect_visuals(&b),
4072 "swapping an event handler must not trigger a repaint",
4073 );
4074 }
4075
4076 #[test]
4077 fn autotest_fingerprint_ids_classes_and_inline_css_are_layout_relevant() {
4078 let base = NodeDataFingerprint::compute(&NodeData::create_div(), None);
4079
4080 let classes = NodeDataFingerprint::compute(&class_node("banner"), None);
4081 assert!(base.diff(&classes).contains(NodeChangeSet::IDS_AND_CLASSES));
4082 assert!(base.might_affect_layout(&classes));
4083 assert!(base.might_affect_visuals(&classes));
4084
4085 let styled =
4086 NodeDataFingerprint::compute(&NodeData::create_div().with_css("width: 3px"), None);
4087 assert!(base.diff(&styled).contains(NodeChangeSet::INLINE_STYLE_LAYOUT));
4088 assert!(base.might_affect_layout(&styled));
4089 assert!(base.might_affect_visuals(&styled));
4090 }
4091
4092 #[test]
4093 fn autotest_fingerprint_attrs_change_flags_tab_index_and_contenteditable() {
4094 let base = NodeDataFingerprint::compute(&NodeData::create_div(), None);
4095 let editable =
4096 NodeDataFingerprint::compute(&NodeData::create_div().with_contenteditable(true), None);
4097
4098 let changes = base.diff(&editable);
4099 assert!(changes.contains(NodeChangeSet::TAB_INDEX));
4100 assert!(changes.contains(NodeChangeSet::CONTENTEDITABLE));
4101 assert!(
4102 base.might_affect_layout(&editable),
4103 "attrs_hash feeds might_affect_layout",
4104 );
4105 assert!(
4106 !base.might_affect_visuals(&editable),
4107 "attrs_hash is deliberately NOT part of might_affect_visuals",
4108 );
4109 }
4110
4111 #[test]
4112 fn autotest_fingerprint_agrees_with_compute_node_changes_on_unchanged_nodes() {
4113 let state = StyledNodeState::default();
4117 let samples = vec![
4118 NodeData::create_div(),
4119 NodeData::create_text("hello 🌍"),
4120 class_node("row"),
4121 id_node("main"),
4122 NodeData::create_div().with_css("color: red"),
4123 NodeData::create_div().with_contenteditable(true),
4124 with_cb(NodeData::create_div(), ComponentEventFilter::AfterMount),
4125 ];
4126
4127 for node in &samples {
4128 let clone = node.clone();
4129
4130 let fp_a = NodeDataFingerprint::compute(node, Some(&state));
4131 let fp_b = NodeDataFingerprint::compute(&clone, Some(&state));
4132 assert!(
4133 fp_a.is_identical(&fp_b),
4134 "a cloned node must fingerprint identically",
4135 );
4136 assert!(fp_a.diff(&fp_b).is_empty());
4137
4138 let tier2 = compute_node_changes(node, &clone, Some(&state), Some(&state));
4139 assert!(
4140 tier2.is_empty(),
4141 "compute_node_changes must agree that a clone is unchanged, got {:#b}",
4142 tier2.bits,
4143 );
4144 }
4145 }
4146}