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().saturating_sub(old_cursor_byte);
1166 return snap(new_text.len().saturating_sub(offset_from_end));
1167 }
1168
1169 snap(new_suffix_start)
1172}
1173
1174#[must_use] pub fn get_node_text_content(node: &NodeData) -> Option<&str> {
1178 if let NodeType::Text(ref text) = node.get_node_type() {
1179 Some(text.as_str())
1180 } else {
1181 None
1182 }
1183}
1184
1185#[derive(Debug, Clone, PartialEq, Eq)]
1191pub struct TextChange {
1192 pub old_text: String,
1194 pub new_text: String,
1196}
1197
1198#[derive(Debug, Clone, Default)]
1200pub struct NodeChangeReport {
1201 pub change_set: NodeChangeSet,
1203
1204 pub relayout_scope: RelayoutScope,
1212
1213 pub changed_css_properties: Vec<CssPropertyType>,
1216
1217 pub text_change: Option<TextChange>,
1219}
1220
1221impl NodeChangeReport {
1222 #[must_use] pub fn needs_layout(&self) -> bool {
1225 self.change_set.needs_layout() || self.relayout_scope > RelayoutScope::None
1226 }
1227
1228 #[must_use] pub const fn needs_paint(&self) -> bool {
1229 self.change_set.needs_paint()
1230 }
1231
1232 #[must_use] pub fn is_visually_unchanged(&self) -> bool {
1233 self.change_set.is_visually_unchanged() && self.relayout_scope == RelayoutScope::None
1234 }
1235}
1236
1237#[derive(Debug, Clone, Default)]
1245pub struct ChangeAccumulator {
1246 pub per_node: BTreeMap<NodeId, NodeChangeReport>,
1248
1249 pub max_scope: RelayoutScope,
1252
1253 pub mounted_nodes: Vec<NodeId>,
1256
1257 pub unmounted_nodes: Vec<NodeId>,
1260}
1261
1262impl ChangeAccumulator {
1263 #[must_use] pub fn new() -> Self {
1264 Self::default()
1265 }
1266
1267 #[must_use] pub fn is_empty(&self) -> bool {
1269 self.per_node.is_empty() && self.mounted_nodes.is_empty() && self.unmounted_nodes.is_empty()
1270 }
1271
1272 #[must_use] pub fn needs_layout(&self) -> bool {
1274 self.max_scope > RelayoutScope::None
1275 || !self.mounted_nodes.is_empty()
1276 || self.per_node.values().any(NodeChangeReport::needs_layout)
1277 }
1278
1279 #[must_use] pub fn needs_paint_only(&self) -> bool {
1281 !self.needs_layout() && self.per_node.values().any(NodeChangeReport::needs_paint)
1282 }
1283
1284 #[must_use] pub fn is_visually_unchanged(&self) -> bool {
1286 self.mounted_nodes.is_empty()
1287 && self.unmounted_nodes.is_empty()
1288 && self.max_scope == RelayoutScope::None
1289 && self.per_node.values().all(NodeChangeReport::is_visually_unchanged)
1290 }
1291
1292 pub fn add_dom_change(
1294 &mut self,
1295 new_node_id: NodeId,
1296 change_set: NodeChangeSet,
1297 relayout_scope: RelayoutScope,
1298 text_change: Option<TextChange>,
1299 changed_css_properties: Vec<CssPropertyType>,
1300 ) {
1301 if relayout_scope > self.max_scope {
1302 self.max_scope = relayout_scope;
1303 }
1304
1305 let report = self.per_node.entry(new_node_id).or_default();
1306 report.change_set |= change_set;
1307 if relayout_scope > report.relayout_scope {
1308 report.relayout_scope = relayout_scope;
1309 }
1310 if text_change.is_some() {
1311 report.text_change = text_change;
1312 }
1313 report.changed_css_properties.extend(changed_css_properties);
1314 }
1315
1316 pub fn add_text_change(
1318 &mut self,
1319 node_id: NodeId,
1320 old_text: String,
1321 new_text: String,
1322 ) {
1323 let scope = RelayoutScope::IfcOnly;
1324 if scope > self.max_scope {
1325 self.max_scope = scope;
1326 }
1327
1328 let report = self.per_node.entry(node_id).or_default();
1329 report.change_set.insert(NodeChangeSet::TEXT_CONTENT);
1330 if scope > report.relayout_scope {
1331 report.relayout_scope = scope;
1332 }
1333 report.text_change = Some(TextChange { old_text, new_text });
1334 }
1335
1336 pub fn add_css_change(
1338 &mut self,
1339 node_id: NodeId,
1340 prop_type: CssPropertyType,
1341 scope: RelayoutScope,
1342 ) {
1343 if scope > self.max_scope {
1344 self.max_scope = scope;
1345 }
1346
1347 let report = self.per_node.entry(node_id).or_default();
1348 if scope > RelayoutScope::None {
1349 report.change_set.insert(NodeChangeSet::INLINE_STYLE_LAYOUT);
1350 } else {
1351 report.change_set.insert(NodeChangeSet::INLINE_STYLE_PAINT);
1352 }
1353 if scope > report.relayout_scope {
1354 report.relayout_scope = scope;
1355 }
1356 report.changed_css_properties.push(prop_type);
1357 }
1358
1359 pub fn add_image_change(
1361 &mut self,
1362 node_id: NodeId,
1363 scope: RelayoutScope,
1364 ) {
1365 if scope > self.max_scope {
1366 self.max_scope = scope;
1367 }
1368
1369 let report = self.per_node.entry(node_id).or_default();
1370 report.change_set.insert(NodeChangeSet::IMAGE_CHANGED);
1371 if scope > report.relayout_scope {
1372 report.relayout_scope = scope;
1373 }
1374 }
1375
1376 pub fn add_mount(&mut self, node_id: NodeId) {
1378 self.mounted_nodes.push(node_id);
1379 }
1380
1381 pub fn add_unmount(&mut self, node_id: NodeId) {
1383 self.unmounted_nodes.push(node_id);
1384 }
1385
1386 pub fn merge_restyle_result(&mut self, restyle: &crate::styled_dom::RestyleResult) {
1392 for (node_id, changed_props) in &restyle.changed_nodes {
1393 for changed in changed_props {
1394 let prop_type = changed.current_prop.get_type();
1395 let scope = prop_type.relayout_scope(true); self.add_css_change(*node_id, prop_type, scope);
1397 }
1398 }
1399 }
1400
1401 pub fn merge_extended_diff(
1406 &mut self,
1407 extended: &ExtendedDiffResult,
1408 old_node_data: &[NodeData],
1409 new_node_data: &[NodeData],
1410 ) {
1411 for &(old_id, new_id, ref change_set) in &extended.node_changes {
1412 if change_set.is_empty() {
1413 continue;
1414 }
1415
1416 let scope = Self::classify_change_scope(*change_set, new_node_data, new_id);
1418
1419 let text_change = if change_set.contains(NodeChangeSet::TEXT_CONTENT) {
1421 let old_text = get_node_text_content(&old_node_data[old_id.index()])
1422 .unwrap_or("")
1423 .to_string();
1424 let new_text = get_node_text_content(&new_node_data[new_id.index()])
1425 .unwrap_or("")
1426 .to_string();
1427 Some(TextChange { old_text, new_text })
1428 } else {
1429 None
1430 };
1431
1432 self.add_dom_change(new_id, *change_set, scope, text_change, Vec::new());
1433 }
1434
1435 let matched_new: alloc::collections::BTreeSet<usize> = extended
1437 .diff
1438 .node_moves
1439 .iter()
1440 .map(|m| m.new_node_id.index())
1441 .collect();
1442
1443 for idx in 0..new_node_data.len() {
1444 if !matched_new.contains(&idx) {
1445 self.add_mount(NodeId::new(idx));
1446 }
1447 }
1448
1449 let matched_old: alloc::collections::BTreeSet<usize> = extended
1451 .diff
1452 .node_moves
1453 .iter()
1454 .map(|m| m.old_node_id.index())
1455 .collect();
1456
1457 for idx in 0..old_node_data.len() {
1458 if !matched_old.contains(&idx) {
1459 self.add_unmount(NodeId::new(idx));
1460 }
1461 }
1462 }
1463
1464 fn classify_change_scope(
1466 change_set: NodeChangeSet,
1467 new_node_data: &[NodeData],
1468 new_node_id: NodeId,
1469 ) -> RelayoutScope {
1470 if change_set.contains(NodeChangeSet::NODE_TYPE_CHANGED)
1472 || change_set.contains(NodeChangeSet::CHILDREN_CHANGED)
1473 {
1474 return RelayoutScope::Full;
1475 }
1476
1477 if change_set.contains(NodeChangeSet::IDS_AND_CLASSES) {
1479 return RelayoutScope::Full;
1480 }
1481
1482 if change_set.contains(NodeChangeSet::INLINE_STYLE_LAYOUT) {
1487 let new_node = &new_node_data[new_node_id.index()];
1489 let mut max_scope = RelayoutScope::None;
1490 for (prop, _conds) in new_node.style.iter_inline_properties() {
1491 let scope = prop.get_type().relayout_scope(true);
1492 if scope > max_scope {
1493 max_scope = scope;
1494 }
1495 }
1496 return if max_scope == RelayoutScope::None {
1497 RelayoutScope::SizingOnly } else {
1499 max_scope
1500 };
1501 }
1502
1503 if change_set.contains(NodeChangeSet::TEXT_CONTENT) {
1505 return RelayoutScope::IfcOnly;
1506 }
1507
1508 if change_set.contains(NodeChangeSet::IMAGE_CHANGED) {
1510 return RelayoutScope::SizingOnly;
1511 }
1512
1513 if change_set.contains(NodeChangeSet::CONTENTEDITABLE) {
1515 return RelayoutScope::SizingOnly;
1516 }
1517
1518 if change_set.intersects(NodeChangeSet::AFFECTS_PAINT) {
1520 return RelayoutScope::None;
1521 }
1522
1523 RelayoutScope::None
1524 }
1525}
1526
1527#[must_use] pub fn reconcile_dom_with_changes(
1535 old_node_data: &[NodeData],
1536 new_node_data: &[NodeData],
1537 old_hierarchy: &[NodeHierarchyItem],
1538 new_hierarchy: &[NodeHierarchyItem],
1539 old_styled_nodes: Option<&[StyledNodeState]>,
1540 new_styled_nodes: Option<&[StyledNodeState]>,
1541 old_layout: &OrderedMap<NodeId, LogicalRect>,
1542 new_layout: &OrderedMap<NodeId, LogicalRect>,
1543 dom_id: DomId,
1544 timestamp: Instant,
1545) -> ExtendedDiffResult {
1546 let diff = reconcile_dom(
1548 old_node_data,
1549 new_node_data,
1550 old_hierarchy,
1551 new_hierarchy,
1552 old_layout,
1553 new_layout,
1554 dom_id,
1555 timestamp,
1556 );
1557
1558 let mut node_changes = Vec::new();
1560 for node_move in &diff.node_moves {
1561 let old_nd = &old_node_data[node_move.old_node_id.index()];
1562 let new_nd = &new_node_data[node_move.new_node_id.index()];
1563
1564 let old_state = old_styled_nodes.and_then(|s| s.get(node_move.old_node_id.index()));
1565 let new_state = new_styled_nodes.and_then(|s| s.get(node_move.new_node_id.index()));
1566
1567 let changes = compute_node_changes(old_nd, new_nd, old_state, new_state);
1568 node_changes.push((node_move.old_node_id, node_move.new_node_id, changes));
1569 }
1570
1571 ExtendedDiffResult { diff, node_changes }
1572}
1573
1574#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1590#[derive(Default)]
1591pub struct NodeDataFingerprint {
1592 pub content_hash: u64,
1594 pub state_hash: u64,
1596 pub inline_css_hash: u64,
1598 pub ids_classes_hash: u64,
1600 pub callbacks_hash: u64,
1602 pub attrs_hash: u64,
1604}
1605
1606
1607impl NodeDataFingerprint {
1608 #[must_use] pub fn compute(node: &NodeData, styled_state: Option<&StyledNodeState>) -> Self {
1610 use core::hash::Hasher;
1611 use core::hash::Hash;
1612
1613 let content_hash = {
1615 let mut h = crate::hash::DefaultHasher::new();
1616 node.get_node_type().hash(&mut h);
1617 h.finish()
1618 };
1619
1620 let state_hash = {
1622 let mut h = crate::hash::DefaultHasher::new();
1623 if let Some(state) = styled_state {
1624 state.hash(&mut h);
1625 }
1626 h.finish()
1627 };
1628
1629 let inline_css_hash = {
1633 let mut h = crate::hash::DefaultHasher::new();
1634 for (prop, conds) in node.style.iter_inline_properties() {
1635 prop.hash(&mut h);
1636 conds.as_slice().len().hash(&mut h);
1637 }
1638 h.finish()
1639 };
1640
1641 let ids_classes_hash = {
1643 let mut h = crate::hash::DefaultHasher::new();
1644 for attr in node.attributes().as_ref() {
1645 match attr {
1646 crate::dom::AttributeType::Id(s) => {
1647 crate::dom::IdOrClass::Id(s.clone()).hash(&mut h);
1648 }
1649 crate::dom::AttributeType::Class(s) => {
1650 crate::dom::IdOrClass::Class(s.clone()).hash(&mut h);
1651 }
1652 _ => {}
1653 }
1654 }
1655 h.finish()
1656 };
1657
1658 let callbacks_hash = {
1660 let mut h = crate::hash::DefaultHasher::new();
1661 for cb in node.callbacks.as_ref() {
1662 cb.event.hash(&mut h);
1663 cb.callback.hash(&mut h);
1664 }
1665 h.finish()
1666 };
1667
1668 let attrs_hash = {
1670 let mut h = crate::hash::DefaultHasher::new();
1671 node.is_contenteditable().hash(&mut h);
1672 node.flags.hash(&mut h);
1673 node.get_dataset().hash(&mut h);
1674 h.finish()
1675 };
1676
1677 Self {
1678 content_hash,
1679 state_hash,
1680 inline_css_hash,
1681 ids_classes_hash,
1682 callbacks_hash,
1683 attrs_hash,
1684 }
1685 }
1686
1687 #[must_use] pub const fn diff(&self, other: &Self) -> NodeChangeSet {
1695 let mut changes = NodeChangeSet::empty();
1696
1697 if self.content_hash != other.content_hash {
1698 changes.insert(NodeChangeSet::TEXT_CONTENT);
1702 changes.insert(NodeChangeSet::IMAGE_CHANGED);
1703 }
1704
1705 if self.state_hash != other.state_hash {
1706 changes.insert(NodeChangeSet::STYLED_STATE);
1707 }
1708
1709 if self.inline_css_hash != other.inline_css_hash {
1710 changes.insert(NodeChangeSet::INLINE_STYLE_LAYOUT);
1713 }
1714
1715 if self.ids_classes_hash != other.ids_classes_hash {
1716 changes.insert(NodeChangeSet::IDS_AND_CLASSES);
1717 }
1718
1719 if self.callbacks_hash != other.callbacks_hash {
1720 changes.insert(NodeChangeSet::CALLBACKS);
1721 }
1722
1723 if self.attrs_hash != other.attrs_hash {
1724 changes.insert(NodeChangeSet::TAB_INDEX);
1725 changes.insert(NodeChangeSet::CONTENTEDITABLE);
1726 }
1727
1728 changes
1729 }
1730
1731 #[must_use] pub fn is_identical(&self, other: &Self) -> bool {
1733 self == other
1734 }
1735
1736 #[must_use] pub const fn might_affect_layout(&self, other: &Self) -> bool {
1738 self.content_hash != other.content_hash
1739 || self.inline_css_hash != other.inline_css_hash
1740 || self.ids_classes_hash != other.ids_classes_hash
1741 || self.attrs_hash != other.attrs_hash
1742 }
1743
1744 #[must_use] pub const fn might_affect_visuals(&self, other: &Self) -> bool {
1746 self.content_hash != other.content_hash
1747 || self.state_hash != other.state_hash
1748 || self.inline_css_hash != other.inline_css_hash
1749 || self.ids_classes_hash != other.ids_classes_hash
1750 }
1751}
1752
1753#[cfg(test)]
1754mod audit_tests {
1755 use super::*;
1756 use crate::dom::NodeData;
1757 use crate::styled_dom::NodeHierarchyItem;
1758
1759 fn hitem(
1761 parent: Option<usize>,
1762 prev: Option<usize>,
1763 next: Option<usize>,
1764 last_child: Option<usize>,
1765 ) -> NodeHierarchyItem {
1766 NodeHierarchyItem {
1767 parent: parent.map_or(0, |p| p + 1),
1768 previous_sibling: prev.map_or(0, |p| p + 1),
1769 next_sibling: next.map_or(0, |p| p + 1),
1770 last_child: last_child.map_or(0, |p| p + 1),
1771 }
1772 }
1773
1774 #[test]
1776 fn reconciliation_key_deep_chain_no_overflow() {
1777 let build = |n: usize| -> (Vec<NodeData>, Vec<NodeHierarchyItem>) {
1778 let node_data = (0..n).map(|_| NodeData::create_div()).collect();
1779 let hierarchy = (0..n)
1780 .map(|i| {
1781 hitem(
1782 if i == 0 { None } else { Some(i - 1) },
1783 None,
1784 None,
1785 if i + 1 < n { Some(i + 1) } else { None },
1786 )
1787 })
1788 .collect();
1789 (node_data, hierarchy)
1790 };
1791
1792 let n = 100_000usize;
1795 let (node_data, hierarchy) = build(n);
1796 let _ = calculate_reconciliation_key(&node_data, &hierarchy, NodeId::new(n - 1));
1797 let _ = calculate_contenteditable_key(&node_data, &hierarchy, NodeId::new(n - 1));
1798
1799 let m = 2_000usize;
1805 let (nd, hi) = build(m);
1806 let keys = precompute_reconciliation_keys(&nd, &hi);
1807 assert_eq!(keys.len(), m);
1808 }
1809
1810 #[test]
1812 fn reconciliation_key_cycle_terminates() {
1813 let node_data = vec![
1814 NodeData::create_div(),
1815 NodeData::create_div(),
1816 NodeData::create_div(),
1817 ];
1818 let hierarchy = vec![
1820 hitem(None, None, None, None),
1821 hitem(Some(2), None, None, None),
1822 hitem(Some(1), None, None, None),
1823 ];
1824 let _ = calculate_reconciliation_key(&node_data, &hierarchy, NodeId::new(1));
1825 let _ = calculate_contenteditable_key(&node_data, &hierarchy, NodeId::new(1));
1826 }
1827
1828 #[test]
1829 fn reconciliation_key_single_node() {
1830 let node_data = vec![NodeData::create_div()];
1831 let hierarchy = vec![hitem(None, None, None, None)];
1832 let direct = calculate_reconciliation_key(&node_data, &hierarchy, NodeId::new(0));
1833 let pre = precompute_reconciliation_keys(&node_data, &hierarchy)[0];
1834 assert_eq!(direct, pre);
1835 }
1836
1837 #[test]
1838 fn reconciliation_key_distinguishes_siblings() {
1839 let node_data = vec![NodeData::create_div(); 3];
1841 let hierarchy = vec![
1842 hitem(None, None, None, Some(2)), hitem(Some(0), None, Some(2), None), hitem(Some(0), Some(1), None, None), ];
1846 let k1 = calculate_reconciliation_key(&node_data, &hierarchy, NodeId::new(1));
1847 let k2 = calculate_reconciliation_key(&node_data, &hierarchy, NodeId::new(2));
1848 assert_ne!(k1, k2);
1849 }
1850
1851 #[test]
1852 fn cursor_offsets_are_always_char_boundaries() {
1853 let old = "héllo";
1855 let new = "héllo wörld"; for c in 0..=old.len() {
1857 let r = reconcile_cursor_position(old, new, c);
1858 assert!(
1859 new.is_char_boundary(r),
1860 "cursor {c} mapped to non-char-boundary offset {r} in {new:?}",
1861 );
1862 assert!(r <= new.len());
1863 }
1864 let r = reconcile_cursor_position("aömega", "bömega", 3);
1866 assert!("bömega".is_char_boundary(r));
1867 }
1868
1869 #[test]
1870 fn cursor_prefix_unchanged_stays_put() {
1871 assert_eq!(reconcile_cursor_position("Hello", "Hello World", 5), 5);
1872 }
1873
1874 #[test]
1875 fn cursor_empty_cases() {
1876 assert_eq!(reconcile_cursor_position("", "abc", 0), 3);
1877 assert_eq!(reconcile_cursor_position("abc", "", 2), 0);
1878 assert_eq!(reconcile_cursor_position("abc", "abc", 2), 2);
1879 }
1880}
1881
1882#[cfg(test)]
1901mod autotest_generated {
1902 use super::*;
1903
1904 use azul_css::{
1905 css::CssPropertyValue,
1906 props::{layout::LayoutWidth, property::CssProperty},
1907 };
1908
1909 use crate::{
1910 callbacks::CoreCallback,
1911 dom::{DatasetMergeCallbackType, TabIndex},
1912 geom::{LogicalPosition, LogicalSize},
1913 refany::{OptionRefAny, RefAny},
1914 resources::{ImageRef, RawImageFormat},
1915 };
1916
1917 fn noop_callback() -> CoreCallback {
1924 CoreCallback {
1925 cb: 0usize,
1926 ctx: OptionRefAny::None,
1927 }
1928 }
1929
1930 fn with_cb(mut nd: NodeData, filter: ComponentEventFilter) -> NodeData {
1931 nd.add_callback(
1932 EventFilter::Component(filter),
1933 RefAny::new(0u32),
1934 noop_callback(),
1935 );
1936 nd
1937 }
1938
1939 fn hitem(
1941 parent: Option<usize>,
1942 prev: Option<usize>,
1943 next: Option<usize>,
1944 last_child: Option<usize>,
1945 ) -> NodeHierarchyItem {
1946 NodeHierarchyItem {
1947 parent: parent.map_or(0, |p| p + 1),
1948 previous_sibling: prev.map_or(0, |p| p + 1),
1949 next_sibling: next.map_or(0, |p| p + 1),
1950 last_child: last_child.map_or(0, |p| p + 1),
1951 }
1952 }
1953
1954 fn rect(w: f32, h: f32) -> LogicalRect {
1955 LogicalRect::new(LogicalPosition::new(0.0, 0.0), LogicalSize::new(w, h))
1956 }
1957
1958 fn layout_of(entries: &[(usize, LogicalRect)]) -> OrderedMap<NodeId, LogicalRect> {
1959 let mut m = OrderedMap::default();
1960 for (idx, r) in entries {
1961 m.insert(NodeId::new(*idx), *r);
1962 }
1963 m
1964 }
1965
1966 fn no_layout() -> OrderedMap<NodeId, LogicalRect> {
1967 OrderedMap::default()
1968 }
1969
1970 fn diff_flat(old: &[NodeData], new: &[NodeData]) -> DiffResult {
1973 reconcile_dom(
1974 old,
1975 new,
1976 &[],
1977 &[],
1978 &no_layout(),
1979 &no_layout(),
1980 DomId::ROOT_ID,
1981 Instant::now(),
1982 )
1983 }
1984
1985 fn count_events(r: &DiffResult, t: EventType) -> usize {
1986 r.events.iter().filter(|e| e.event_type == t).count()
1987 }
1988
1989 fn id_node(id: &str) -> NodeData {
1990 NodeData::create_div().with_ids_and_classes(vec![IdOrClass::Id(id.into())].into())
1991 }
1992
1993 fn class_node(class: &str) -> NodeData {
1994 NodeData::create_div().with_ids_and_classes(vec![IdOrClass::Class(class.into())].into())
1995 }
1996
1997 const UNICODE_SAMPLES: &[&str] = &[
2000 "",
2001 "a",
2002 "héllo",
2003 "e\u{0301}galite\u{0301}", "مرحبا بالعالم", "👨👩👧👦 family", "日本語のテキスト",
2007 "\u{feff}bom-prefixed",
2008 "🇩🇪🇫🇷🇯🇵", "mixed 漢字 and ascii ✅",
2010 ];
2011
2012 #[test]
2017 fn autotest_changeset_empty_is_a_neutral_element() {
2018 let e = NodeChangeSet::empty();
2019 assert_eq!(e.bits, 0);
2020 assert!(e.is_empty());
2021 assert!(!e.needs_layout());
2022 assert!(!e.needs_paint());
2023 assert!(e.is_visually_unchanged());
2024 assert_eq!(NodeChangeSet::default(), e);
2026 let mut some = NodeChangeSet::empty();
2028 some.insert(NodeChangeSet::TEXT_CONTENT);
2029 assert_eq!((some | e).bits, some.bits);
2030 assert_eq!((e | some).bits, some.bits);
2031 }
2032
2033 #[test]
2034 fn autotest_changeset_contains_zero_is_vacuously_true() {
2035 assert!(NodeChangeSet::empty().contains(0));
2039 let mut s = NodeChangeSet::empty();
2040 s.insert(NodeChangeSet::CALLBACKS);
2041 assert!(s.contains(0));
2042 assert!(NodeChangeSet { bits: u32::MAX }.contains(0));
2043 }
2044
2045 #[test]
2046 fn autotest_changeset_intersects_zero_is_always_false() {
2047 assert!(!NodeChangeSet::empty().intersects(0));
2049 assert!(!NodeChangeSet { bits: u32::MAX }.intersects(0));
2050 }
2051
2052 #[test]
2053 fn autotest_changeset_contains_is_all_bits_intersects_is_any_bits() {
2054 let mut s = NodeChangeSet::empty();
2055 s.insert(NodeChangeSet::TEXT_CONTENT);
2056
2057 let both = NodeChangeSet::TEXT_CONTENT | NodeChangeSet::IMAGE_CHANGED;
2058 assert!(!s.contains(both), "contains() must require ALL bits");
2059 assert!(s.intersects(both), "intersects() must require ANY bit");
2060 assert!(s.contains(NodeChangeSet::TEXT_CONTENT));
2061 }
2062
2063 #[test]
2064 fn autotest_changeset_insert_min_max_and_idempotent() {
2065 let mut s = NodeChangeSet::empty();
2067 s.insert(0);
2068 assert!(s.is_empty());
2069
2070 let mut s = NodeChangeSet::empty();
2072 s.insert(u32::MAX);
2073 assert_eq!(s.bits, u32::MAX);
2074 s.insert(u32::MAX);
2076 assert_eq!(s.bits, u32::MAX);
2077 s.insert(NodeChangeSet::TEXT_CONTENT);
2078 assert_eq!(s.bits, u32::MAX);
2079
2080 assert!(s.contains(NodeChangeSet::NODE_TYPE_CHANGED));
2082 assert!(s.contains(NodeChangeSet::AFFECTS_LAYOUT));
2083 assert!(s.contains(NodeChangeSet::AFFECTS_PAINT));
2084 assert!(s.needs_layout());
2085 assert!(s.needs_paint());
2086 assert!(!s.is_visually_unchanged());
2087 assert!(!s.is_empty());
2088 }
2089
2090 #[test]
2091 fn autotest_changeset_undefined_high_bits_trigger_no_work() {
2092 let s = NodeChangeSet {
2095 bits: 0b1000_0000_0000_0000_0000_0000_0000_0000,
2096 };
2097 assert!(!s.is_empty());
2098 assert!(!s.needs_layout());
2099 assert!(!s.needs_paint());
2100 assert!(s.is_visually_unchanged());
2101 }
2102
2103 #[test]
2104 fn autotest_changeset_layout_and_paint_masks_are_disjoint() {
2105 assert_eq!(
2108 NodeChangeSet::AFFECTS_LAYOUT & NodeChangeSet::AFFECTS_PAINT,
2109 0,
2110 "AFFECTS_LAYOUT and AFFECTS_PAINT must not overlap",
2111 );
2112
2113 for flag in [
2114 NodeChangeSet::NODE_TYPE_CHANGED,
2115 NodeChangeSet::TEXT_CONTENT,
2116 NodeChangeSet::IDS_AND_CLASSES,
2117 NodeChangeSet::INLINE_STYLE_LAYOUT,
2118 NodeChangeSet::CHILDREN_CHANGED,
2119 NodeChangeSet::IMAGE_CHANGED,
2120 NodeChangeSet::CONTENTEDITABLE,
2121 NodeChangeSet::INLINE_STYLE_PAINT,
2122 NodeChangeSet::STYLED_STATE,
2123 NodeChangeSet::CALLBACKS,
2124 NodeChangeSet::DATASET,
2125 NodeChangeSet::ACCESSIBILITY,
2126 ] {
2127 let mut s = NodeChangeSet::empty();
2128 s.insert(flag);
2129 assert!(!(s.needs_layout() && s.needs_paint()), "flag {flag:#b} is both");
2130 assert_eq!(
2132 s.is_visually_unchanged(),
2133 !s.needs_layout() && !s.needs_paint(),
2134 "is_visually_unchanged() disagrees with needs_layout/needs_paint for {flag:#b}",
2135 );
2136 }
2137 }
2138
2139 #[test]
2140 fn autotest_changeset_nonvisual_flags_are_visually_unchanged() {
2141 let mut s = NodeChangeSet::empty();
2142 s.insert(NodeChangeSet::CALLBACKS);
2143 s.insert(NodeChangeSet::DATASET);
2144 s.insert(NodeChangeSet::ACCESSIBILITY);
2145 s.insert(NodeChangeSet::TAB_INDEX); assert!(!s.is_empty());
2147 assert!(s.is_visually_unchanged());
2148 assert!(!s.needs_layout());
2149 assert!(!s.needs_paint());
2150 }
2151
2152 #[test]
2153 fn autotest_changeset_bitor_matches_bitorassign() {
2154 for (a, b) in [
2157 (0u32, 0u32),
2158 (0, u32::MAX),
2159 (u32::MAX, u32::MAX),
2160 (NodeChangeSet::TEXT_CONTENT, NodeChangeSet::STYLED_STATE),
2161 (0xDEAD_BEEF, 0x0BAD_F00D),
2162 ] {
2163 let (sa, sb) = (NodeChangeSet { bits: a }, NodeChangeSet { bits: b });
2164
2165 let by_operator = sa | sb;
2166 assert_eq!(by_operator.bits, a | b);
2167
2168 let mut by_assign = sa;
2169 by_assign |= sb;
2170 assert_eq!(by_assign, by_operator);
2171
2172 assert_eq!((sb | sa).bits, by_operator.bits, "BitOr must commute");
2173 assert_eq!((by_operator | by_operator).bits, by_operator.bits);
2174 }
2175 }
2176
2177 #[test]
2182 fn autotest_change_report_default_is_inert() {
2183 let r = NodeChangeReport::default();
2184 assert!(!r.needs_layout());
2185 assert!(!r.needs_paint());
2186 assert!(r.is_visually_unchanged());
2187 assert_eq!(r.relayout_scope, RelayoutScope::None);
2188 assert!(r.changed_css_properties.is_empty());
2189 assert!(r.text_change.is_none());
2190 }
2191
2192 #[test]
2193 fn autotest_change_report_scope_alone_forces_layout() {
2194 let r = NodeChangeReport { relayout_scope: RelayoutScope::IfcOnly, ..Default::default() };
2197 assert!(r.needs_layout());
2198 assert!(!r.needs_paint());
2199 assert!(!r.is_visually_unchanged());
2200 }
2201
2202 #[test]
2203 fn autotest_change_report_paint_flag_does_not_force_layout() {
2204 let mut r = NodeChangeReport::default();
2205 r.change_set.insert(NodeChangeSet::STYLED_STATE);
2206 assert!(!r.needs_layout());
2207 assert!(r.needs_paint());
2208 assert!(!r.is_visually_unchanged());
2209 }
2210
2211 #[test]
2216 fn autotest_cursor_result_is_always_a_valid_slice_index() {
2217 for old in UNICODE_SAMPLES {
2221 for new in UNICODE_SAMPLES {
2222 for cursor in 0..=old.len() {
2223 let r = reconcile_cursor_position(old, new, cursor);
2224 assert!(
2225 r <= new.len(),
2226 "cursor {cursor} in {old:?} -> {r} exceeds len of {new:?}",
2227 );
2228 assert!(
2229 new.is_char_boundary(r),
2230 "cursor {cursor} in {old:?} -> {r} splits a codepoint in {new:?}",
2231 );
2232 let _ = &new[..r];
2234 }
2235 }
2236 }
2237 }
2238
2239 #[test]
2240 fn autotest_cursor_is_deterministic() {
2241 for old in UNICODE_SAMPLES {
2244 for new in UNICODE_SAMPLES {
2245 let a = reconcile_cursor_position(old, new, old.len() / 2);
2246 let b = reconcile_cursor_position(old, new, old.len() / 2);
2247 assert_eq!(a, b);
2248 }
2249 }
2250 }
2251
2252 #[test]
2253 fn autotest_cursor_zero_stays_zero_when_texts_differ_at_byte_zero() {
2254 assert_eq!(reconcile_cursor_position("abc", "xyz", 0), 0);
2256 assert_eq!(reconcile_cursor_position("日本", "中国", 0), 0);
2257 }
2258
2259 #[test]
2260 fn autotest_cursor_identical_text_clamps_to_len() {
2261 assert_eq!(reconcile_cursor_position("abc", "abc", usize::MAX), 3);
2264 assert_eq!(reconcile_cursor_position("héllo", "héllo", usize::MAX), 6);
2265 assert_eq!(reconcile_cursor_position("héllo", "héllo", 2), 1);
2267 }
2268
2269 #[test]
2270 fn autotest_cursor_empty_sides_are_documented_constants() {
2271 for new in UNICODE_SAMPLES {
2273 assert_eq!(reconcile_cursor_position("", new, 0), new.len());
2274 assert_eq!(reconcile_cursor_position("", new, usize::MAX), new.len());
2275 }
2276 for old in UNICODE_SAMPLES {
2277 if old.is_empty() {
2278 continue; }
2280 assert_eq!(reconcile_cursor_position(old, "", 0), 0);
2281 assert_eq!(reconcile_cursor_position(old, "", old.len()), 0);
2282 }
2283 }
2284
2285 #[test]
2286 fn autotest_cursor_appended_text_keeps_prefix_cursor() {
2287 let old = "Hello";
2289 let new = "Hello, World";
2290 for cursor in 0..=old.len() {
2291 assert_eq!(reconcile_cursor_position(old, new, cursor), cursor);
2292 }
2293 }
2294
2295 #[test]
2296 fn autotest_cursor_deleted_tail_clamps_into_new_text() {
2297 let old = "Hello, World";
2300 let new = "Hello";
2301 assert_eq!(reconcile_cursor_position(old, new, old.len()), new.len());
2302 assert_eq!(reconcile_cursor_position(old, new, 5), 5);
2303 }
2304
2305 #[test]
2306 fn autotest_cursor_multibyte_insert_before_cursor_shifts_by_suffix_rule() {
2307 let old = "mega";
2310 let new = "ömega";
2311 let r = reconcile_cursor_position(old, new, 4); assert_eq!(r, new.len());
2313 assert!(new.is_char_boundary(r));
2314 }
2315
2316 #[test]
2317 fn autotest_cursor_huge_inputs_do_not_hang_or_panic() {
2318 let old: String = std::iter::repeat_n('a', 200_000).collect();
2321 let mut new = old.clone();
2322 new.push_str("tail");
2323
2324 let r = reconcile_cursor_position(&old, &new, old.len());
2325 assert!(r <= new.len());
2326 assert!(new.is_char_boundary(r));
2327
2328 let old_u: String = std::iter::repeat_n('é', 50_000).collect();
2330 let new_u: String = std::iter::repeat_n('é', 49_999).collect();
2331 let r = reconcile_cursor_position(&old_u, &new_u, old_u.len());
2332 assert!(r <= new_u.len());
2333 assert!(new_u.is_char_boundary(r));
2334 }
2335
2336 #[test]
2345 fn autotest_cursor_out_of_range_cursor_must_saturate_not_underflow() {
2346 assert_eq!(reconcile_cursor_position("abc", "abd", usize::MAX), 3);
2348 assert_eq!(reconcile_cursor_position("abc", "abd", 99), 3);
2349 assert_eq!(reconcile_cursor_position("héllo", "héllx", usize::MAX), 6);
2350 }
2351
2352 #[test]
2357 fn autotest_text_content_round_trips_unicode() {
2358 for s in UNICODE_SAMPLES {
2359 let node = NodeData::create_text(*s);
2360 assert_eq!(
2361 get_node_text_content(&node),
2362 Some(*s),
2363 "create_text -> get_node_text_content must round-trip {s:?}",
2364 );
2365 }
2366 }
2367
2368 #[test]
2369 fn autotest_text_content_is_none_for_non_text_nodes() {
2370 assert_eq!(get_node_text_content(&NodeData::create_div()), None);
2371 assert_eq!(get_node_text_content(&NodeData::create_body()), None);
2372 assert_eq!(get_node_text_content(&NodeData::create_br()), None);
2373 let img = NodeData::create_image(ImageRef::null_image(
2374 1,
2375 1,
2376 RawImageFormat::RGBA8,
2377 Vec::new(),
2378 ));
2379 assert_eq!(get_node_text_content(&img), None);
2380 }
2381
2382 #[test]
2387 fn autotest_callback_predicates_all_false_without_callbacks() {
2388 let n = NodeData::create_div();
2389 assert!(!has_mount_callback(&n));
2390 assert!(!has_unmount_callback(&n));
2391 assert!(!has_resize_callback(&n));
2392 assert!(!has_update_callback(&n));
2393 }
2394
2395 #[test]
2396 fn autotest_callback_predicates_are_mutually_exclusive_per_filter() {
2397 let cases = [
2399 (ComponentEventFilter::AfterMount, [true, false, false, false]),
2400 (ComponentEventFilter::BeforeUnmount, [false, true, false, false]),
2401 (ComponentEventFilter::NodeResized, [false, false, true, false]),
2402 (ComponentEventFilter::Updated, [false, false, false, true]),
2403 (ComponentEventFilter::Selected, [false, false, false, false]),
2405 (ComponentEventFilter::DefaultAction, [false, false, false, false]),
2406 ];
2407
2408 for (filter, expected) in cases {
2409 let n = with_cb(NodeData::create_div(), filter);
2410 let got = [
2411 has_mount_callback(&n),
2412 has_unmount_callback(&n),
2413 has_resize_callback(&n),
2414 has_update_callback(&n),
2415 ];
2416 assert_eq!(got, expected, "predicate mismatch for {filter:?}");
2417 }
2418 }
2419
2420 #[test]
2421 fn autotest_callback_predicates_find_target_among_many() {
2422 let mut n = NodeData::create_div();
2424 for f in [
2425 ComponentEventFilter::Selected,
2426 ComponentEventFilter::DefaultAction,
2427 ComponentEventFilter::NodeResized,
2428 ] {
2429 n = with_cb(n, f);
2430 }
2431 assert!(has_resize_callback(&n));
2432 assert!(!has_mount_callback(&n));
2433 }
2434
2435 #[test]
2440 fn autotest_lifecycle_event_fields_are_wired_consistently() {
2441 let ts = Instant::now();
2442 let ev = create_lifecycle_event(
2443 EventType::Mount,
2444 NodeId::new(1_000_000),
2445 DomId::ROOT_ID,
2446 &ts,
2447 LifecycleEventData {
2448 reason: LifecycleReason::InitialMount,
2449 previous_bounds: None,
2450 current_bounds: rect(1.0, 2.0),
2451 },
2452 );
2453
2454 assert_eq!(ev.event_type, EventType::Mount);
2455 assert_eq!(ev.source, EventSource::Lifecycle);
2456 assert_eq!(ev.phase, EventPhase::Target);
2457 assert_eq!(ev.target, ev.current_target);
2459 assert_eq!(
2460 ev.target.node.into_crate_internal(),
2461 Some(NodeId::new(1_000_000)),
2462 "NodeId must survive the 1-based NodeHierarchyItemId encoding",
2463 );
2464 assert!(!ev.stopped);
2465 assert!(!ev.stopped_immediate);
2466 assert!(!ev.prevented_default);
2467
2468 let EventData::Lifecycle(data) = &ev.data else {
2469 panic!("expected EventData::Lifecycle, got {:?}", ev.data);
2470 };
2471 assert!(data.previous_bounds.is_none());
2472 assert_eq!(data.current_bounds, rect(1.0, 2.0));
2473 }
2474
2475 #[test]
2480 fn autotest_compute_changes_identical_nodes_report_nothing() {
2481 let a = NodeData::create_text("same");
2482 let b = NodeData::create_text("same");
2483 let changes = compute_node_changes(&a, &b, None, None);
2484 assert!(
2485 changes.is_empty(),
2486 "identical nodes must produce no change flags, got {:#b}",
2487 changes.bits,
2488 );
2489 assert!(changes.is_visually_unchanged());
2490 }
2491
2492 #[test]
2493 fn autotest_compute_changes_node_type_change_short_circuits_everything() {
2494 let old = NodeData::create_div();
2499 let new = with_cb(
2500 NodeData::create_text("now a text node")
2501 .with_ids_and_classes(vec![IdOrClass::Class("brand-new".into())].into())
2502 .with_css("width: 10px")
2503 .with_tab_index(TabIndex::NoKeyboardFocus)
2504 .with_contenteditable(true),
2505 ComponentEventFilter::AfterMount,
2506 );
2507
2508 let hovered = StyledNodeState {
2509 hover: true,
2510 ..StyledNodeState::default()
2511 };
2512 let changes = compute_node_changes(
2513 &old,
2514 &new,
2515 Some(&StyledNodeState::default()),
2516 Some(&hovered),
2517 );
2518 assert_eq!(
2519 changes.bits,
2520 NodeChangeSet::NODE_TYPE_CHANGED,
2521 "a node-type change must be reported alone (early return)",
2522 );
2523 }
2524
2525 #[test]
2526 fn autotest_compute_changes_text_content_unicode() {
2527 for (i, s) in UNICODE_SAMPLES.iter().enumerate() {
2528 let old = NodeData::create_text(*s);
2529
2530 let same = NodeData::create_text(*s);
2532 assert!(
2533 !compute_node_changes(&old, &same, None, None)
2534 .contains(NodeChangeSet::TEXT_CONTENT),
2535 "identical text {s:?} must not report TEXT_CONTENT",
2536 );
2537
2538 let other = UNICODE_SAMPLES[(i + 1) % UNICODE_SAMPLES.len()];
2540 if other == *s {
2541 continue;
2542 }
2543 let changed = NodeData::create_text(other);
2544 assert!(
2545 compute_node_changes(&old, &changed, None, None)
2546 .contains(NodeChangeSet::TEXT_CONTENT),
2547 "{s:?} -> {other:?} must report TEXT_CONTENT",
2548 );
2549 }
2550 }
2551
2552 #[test]
2553 fn autotest_compute_changes_paint_only_css_never_sets_layout() {
2554 let old = NodeData::create_div().with_css("color: red");
2556 let new = NodeData::create_div().with_css("color: blue");
2557 let changes = compute_node_changes(&old, &new, None, None);
2558
2559 assert!(changes.contains(NodeChangeSet::INLINE_STYLE_PAINT));
2560 assert!(
2561 !changes.contains(NodeChangeSet::INLINE_STYLE_LAYOUT),
2562 "a paint-only property must not request relayout",
2563 );
2564 assert!(changes.needs_paint());
2565 assert!(!changes.needs_layout());
2566 }
2567
2568 #[test]
2569 fn autotest_compute_changes_sizing_css_sets_layout_not_paint() {
2570 let old = NodeData::create_div().with_css("width: 10px");
2572 let new = NodeData::create_div().with_css("width: 20px");
2573 let changes = compute_node_changes(&old, &new, None, None);
2574
2575 assert!(changes.contains(NodeChangeSet::INLINE_STYLE_LAYOUT));
2576 assert!(!changes.contains(NodeChangeSet::INLINE_STYLE_PAINT));
2577 assert!(changes.needs_layout());
2578 }
2579
2580 #[test]
2581 fn autotest_compute_changes_detects_removed_property() {
2582 let old = NodeData::create_div().with_css("color: red");
2585 let new = NodeData::create_div();
2586 let changes = compute_node_changes(&old, &new, None, None);
2587 assert!(
2588 changes.contains(NodeChangeSet::INLINE_STYLE_PAINT),
2589 "removing an inline property must be reported, got {:#b}",
2590 changes.bits,
2591 );
2592 }
2593
2594 #[test]
2595 fn autotest_compute_changes_detects_added_property() {
2596 let old = NodeData::create_div();
2597 let new = NodeData::create_div().with_css("width: 5px");
2598 let changes = compute_node_changes(&old, &new, None, None);
2599 assert!(changes.contains(NodeChangeSet::INLINE_STYLE_LAYOUT));
2600 }
2601
2602 #[test]
2603 fn autotest_compute_changes_ids_and_classes() {
2604 let changes = compute_node_changes(&class_node("a"), &class_node("b"), None, None);
2605 assert!(changes.contains(NodeChangeSet::IDS_AND_CLASSES));
2606
2607 let changes = compute_node_changes(&class_node("a"), &class_node("a"), None, None);
2609 assert!(!changes.contains(NodeChangeSet::IDS_AND_CLASSES));
2610 }
2611
2612 #[test]
2613 fn autotest_compute_changes_styled_state() {
2614 let n = NodeData::create_div();
2615 let calm = StyledNodeState::default();
2616 let hovered = StyledNodeState {
2617 hover: true,
2618 ..StyledNodeState::default()
2619 };
2620
2621 let changes = compute_node_changes(&n, &n, Some(&calm), Some(&hovered));
2622 assert!(changes.contains(NodeChangeSet::STYLED_STATE));
2623 assert!(changes.needs_paint());
2624 assert!(!changes.needs_layout());
2625
2626 assert!(!compute_node_changes(&n, &n, Some(&calm), Some(&calm))
2628 .contains(NodeChangeSet::STYLED_STATE));
2629 assert!(!compute_node_changes(&n, &n, None, None).contains(NodeChangeSet::STYLED_STATE));
2630
2631 assert!(compute_node_changes(&n, &n, None, Some(&calm))
2633 .contains(NodeChangeSet::STYLED_STATE));
2634 }
2635
2636 #[test]
2637 fn autotest_compute_changes_tab_index_and_contenteditable() {
2638 let plain = NodeData::create_div();
2639
2640 let editable = NodeData::create_div().with_contenteditable(true);
2641 let changes = compute_node_changes(&plain, &editable, None, None);
2642 assert!(changes.contains(NodeChangeSet::CONTENTEDITABLE));
2643 assert!(changes.needs_layout(), "CONTENTEDITABLE is in AFFECTS_LAYOUT");
2644
2645 let tabbed = NodeData::create_div().with_tab_index(TabIndex::OverrideInParent(3));
2646 let changes = compute_node_changes(&plain, &tabbed, None, None);
2647 assert!(changes.contains(NodeChangeSet::TAB_INDEX));
2648 assert!(changes.is_visually_unchanged());
2650 }
2651
2652 #[test]
2653 fn autotest_compute_changes_callbacks_count_and_identity() {
2654 let plain = NodeData::create_div();
2655 let one = with_cb(NodeData::create_div(), ComponentEventFilter::AfterMount);
2656
2657 let changes = compute_node_changes(&plain, &one, None, None);
2659 assert!(changes.contains(NodeChangeSet::CALLBACKS));
2660 assert!(changes.is_visually_unchanged(), "callbacks are not a visual change");
2661
2662 let other = with_cb(NodeData::create_div(), ComponentEventFilter::BeforeUnmount);
2664 let changes = compute_node_changes(&one, &other, None, None);
2665 assert!(changes.contains(NodeChangeSet::CALLBACKS));
2666
2667 let same = with_cb(NodeData::create_div(), ComponentEventFilter::AfterMount);
2669 let changes = compute_node_changes(&one, &same, None, None);
2670 assert!(!changes.contains(NodeChangeSet::CALLBACKS));
2671 }
2672
2673 #[test]
2674 fn autotest_compute_changes_image_identity_is_by_image_id() {
2675 let img = ImageRef::null_image(4, 4, RawImageFormat::RGBA8, Vec::new());
2678 let same = NodeData::create_image(img.clone());
2679 let also_same = NodeData::create_image(img.clone());
2680 assert!(
2681 !compute_node_changes(&same, &also_same, None, None)
2682 .contains(NodeChangeSet::IMAGE_CHANGED),
2683 "two nodes holding clones of the SAME ImageRef must not report a change",
2684 );
2685
2686 let other = NodeData::create_image(ImageRef::null_image(
2689 4,
2690 4,
2691 RawImageFormat::RGBA8,
2692 Vec::new(),
2693 ));
2694 let changes = compute_node_changes(&same, &other, None, None);
2695 assert!(changes.contains(NodeChangeSet::IMAGE_CHANGED));
2696 assert!(changes.needs_layout(), "IMAGE_CHANGED is in AFFECTS_LAYOUT");
2697 }
2698
2699 #[test]
2704 fn autotest_rec_key_empty_node_data_is_safe() {
2705 assert!(precompute_reconciliation_keys(&[], &[]).is_empty());
2706 }
2707
2708 #[test]
2709 fn autotest_rec_key_precompute_matches_per_node_calculation() {
2710 let node_data = vec![
2713 NodeData::create_div(),
2714 class_node("row"),
2715 NodeData::create_text("leaf"),
2716 id_node("footer"),
2717 ];
2718 let hierarchy = vec![
2719 hitem(None, None, None, Some(3)),
2720 hitem(Some(0), None, Some(2), None),
2721 hitem(Some(0), Some(1), Some(3), None),
2722 hitem(Some(0), Some(2), None, None),
2723 ];
2724
2725 let keys = precompute_reconciliation_keys(&node_data, &hierarchy);
2726 assert_eq!(keys.len(), node_data.len());
2727 for (i, k) in keys.iter().enumerate() {
2728 assert_eq!(
2729 *k,
2730 calculate_reconciliation_key(&node_data, &hierarchy, NodeId::new(i)),
2731 "precomputed key for node {i} disagrees with the direct call",
2732 );
2733 }
2734 }
2735
2736 #[test]
2737 fn autotest_rec_key_explicit_key_beats_css_id_and_node_type() {
2738 let bare = NodeData::create_div().with_key(7u32);
2741 let decorated = NodeData::create_text("totally different")
2742 .with_key(7u32)
2743 .with_ids_and_classes(
2744 vec![IdOrClass::Id("hero".into()), IdOrClass::Class("x".into())].into(),
2745 );
2746
2747 let a = calculate_reconciliation_key(&[bare], &[], NodeId::new(0));
2748 let b = calculate_reconciliation_key(&[decorated], &[], NodeId::new(0));
2749 assert_eq!(a, b, "an explicit .with_key() must dominate every other input");
2750 }
2751
2752 #[test]
2753 fn autotest_rec_key_css_id_used_when_no_explicit_key() {
2754 let same_a = calculate_reconciliation_key(&[id_node("hero")], &[], NodeId::new(0));
2755 let same_b = calculate_reconciliation_key(&[id_node("hero")], &[], NodeId::new(0));
2756 let other = calculate_reconciliation_key(&[id_node("footer")], &[], NodeId::new(0));
2757
2758 assert_eq!(same_a, same_b, "the CSS-ID key must be stable");
2759 assert_ne!(same_a, other, "different CSS IDs must produce different keys");
2760 }
2761
2762 #[test]
2763 fn autotest_rec_key_classes_participate_in_the_structural_key() {
2764 let a = calculate_reconciliation_key(&[class_node("alpha")], &[], NodeId::new(0));
2765 let b = calculate_reconciliation_key(&[class_node("beta")], &[], NodeId::new(0));
2766 assert_ne!(a, b, "classes must feed the structural key");
2767 }
2768
2769 #[test]
2770 fn autotest_rec_key_node_type_participates_in_the_structural_key() {
2771 let div = calculate_reconciliation_key(&[NodeData::create_div()], &[], NodeId::new(0));
2772 let txt =
2773 calculate_reconciliation_key(&[NodeData::create_text("x")], &[], NodeId::new(0));
2774 assert_ne!(div, txt, "the node-type discriminant must feed the structural key");
2775 }
2776
2777 #[test]
2778 fn autotest_rec_key_hierarchy_shorter_than_node_data_is_safe() {
2779 let node_data = vec![NodeData::create_div(), class_node("a"), id_node("b")];
2782
2783 let with_none = precompute_reconciliation_keys(&node_data, &[]);
2784 let with_short = precompute_reconciliation_keys(&node_data, &[hitem(None, None, None, None)]);
2785
2786 assert_eq!(with_none.len(), 3);
2787 assert_eq!(with_short.len(), 3);
2788 assert_eq!(with_none[0], with_short[0]);
2790 }
2791
2792 #[test]
2793 fn autotest_rec_key_identical_leaves_under_different_parents_differ() {
2794 let node_data = vec![
2801 NodeData::create_div(),
2802 id_node("left"),
2803 id_node("right"),
2804 NodeData::create_div(),
2805 NodeData::create_div(),
2806 ];
2807 let hierarchy = vec![
2808 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), ];
2814
2815 let k3 = calculate_reconciliation_key(&node_data, &hierarchy, NodeId::new(3));
2816 let k4 = calculate_reconciliation_key(&node_data, &hierarchy, NodeId::new(4));
2817 assert_ne!(k3, k4, "identical leaves under different parents must not share a key");
2818 }
2819
2820 #[test]
2825 fn autotest_contenteditable_key_is_deterministic_and_honours_explicit_keys() {
2826 let node_data = vec![NodeData::create_div().with_key(99u64)];
2827 let a = calculate_contenteditable_key(&node_data, &[], NodeId::new(0));
2828 let b = calculate_contenteditable_key(&node_data, &[], NodeId::new(0));
2829 assert_eq!(a, b, "must be deterministic");
2830
2831 assert_eq!(
2834 a,
2835 calculate_reconciliation_key(&node_data, &[], NodeId::new(0)),
2836 "explicit keys must be identical across both key functions",
2837 );
2838 }
2839
2840 #[test]
2841 fn autotest_contenteditable_key_distinguishes_nth_of_type() {
2842 let node_data = vec![
2845 NodeData::create_div(),
2846 NodeData::create_text("A"),
2847 NodeData::create_text("B"),
2848 ];
2849 let hierarchy = vec![
2850 hitem(None, None, None, Some(2)),
2851 hitem(Some(0), None, Some(2), None),
2852 hitem(Some(0), Some(1), None, None),
2853 ];
2854
2855 let k1 = calculate_contenteditable_key(&node_data, &hierarchy, NodeId::new(1));
2856 let k2 = calculate_contenteditable_key(&node_data, &hierarchy, NodeId::new(2));
2857 assert_ne!(k1, k2, "same-type siblings must differ by nth-of-type");
2858 }
2859
2860 #[test]
2861 fn autotest_contenteditable_key_empty_hierarchy_is_safe() {
2862 let node_data = vec![NodeData::create_div(), class_node("editor")];
2863 for i in 0..node_data.len() {
2864 let k = calculate_contenteditable_key(&node_data, &[], NodeId::new(i));
2865 assert_eq!(k, calculate_contenteditable_key(&node_data, &[], NodeId::new(i)));
2866 }
2867 }
2868
2869 #[test]
2874 fn autotest_reconcile_empty_to_empty_is_a_no_op() {
2875 let r = diff_flat(&[], &[]);
2876 assert!(r.events.is_empty());
2877 assert!(r.node_moves.is_empty());
2878 }
2879
2880 #[test]
2881 fn autotest_reconcile_mount_and_unmount_need_a_callback_to_fire() {
2882 let silent_new = vec![NodeData::create_div()];
2885 let r = diff_flat(&[], &silent_new);
2886 assert!(r.events.is_empty(), "no callback -> no event");
2887 assert!(r.node_moves.is_empty());
2888
2889 let loud_new = vec![with_cb(NodeData::create_div(), ComponentEventFilter::AfterMount)];
2890 let r = diff_flat(&[], &loud_new);
2891 assert_eq!(count_events(&r, EventType::Mount), 1);
2892
2893 let loud_old = vec![with_cb(
2894 NodeData::create_div(),
2895 ComponentEventFilter::BeforeUnmount,
2896 )];
2897 let r = diff_flat(&loud_old, &[]);
2898 assert_eq!(count_events(&r, EventType::Unmount), 1);
2899 assert!(r.node_moves.is_empty());
2900 }
2901
2902 #[test]
2903 fn autotest_reconcile_node_moves_are_a_bijection() {
2904 let old: Vec<NodeData> = (0..50).map(|_| NodeData::create_div()).collect();
2908 let new: Vec<NodeData> = (0..50).map(|_| NodeData::create_div()).collect();
2909
2910 let r = diff_flat(&old, &new);
2911 assert_eq!(r.node_moves.len(), 50);
2912
2913 let mut seen_old = [false; 50];
2914 let mut seen_new = [false; 50];
2915 for m in &r.node_moves {
2916 assert!(!seen_old[m.old_node_id.index()], "old node claimed twice");
2917 assert!(!seen_new[m.new_node_id.index()], "new node matched twice");
2918 seen_old[m.old_node_id.index()] = true;
2919 seen_new[m.new_node_id.index()] = true;
2920 }
2921 assert!(seen_old.iter().all(|b| *b), "every old node must be claimed");
2922 assert!(seen_new.iter().all(|b| *b), "every new node must be matched");
2923 assert!(r.events.is_empty(), "no lifecycle callbacks -> no events");
2924 }
2925
2926 #[test]
2927 fn autotest_reconcile_surplus_new_nodes_mount_and_surplus_old_unmount() {
2928 let old: Vec<NodeData> = (0..50).map(|_| NodeData::create_div()).collect();
2930 let new: Vec<NodeData> = (0..60)
2931 .map(|_| with_cb(NodeData::create_div(), ComponentEventFilter::AfterMount))
2932 .collect();
2933
2934 let r = diff_flat(&old, &new);
2935 assert_eq!(r.node_moves.len(), 50);
2936 assert_eq!(count_events(&r, EventType::Mount), 10);
2937 assert_eq!(count_events(&r, EventType::Unmount), 0);
2938
2939 let old: Vec<NodeData> = (0..50)
2941 .map(|_| with_cb(NodeData::create_div(), ComponentEventFilter::BeforeUnmount))
2942 .collect();
2943 let new: Vec<NodeData> = (0..40).map(|_| NodeData::create_div()).collect();
2944
2945 let r = diff_flat(&old, &new);
2946 assert_eq!(r.node_moves.len(), 40);
2947 assert_eq!(count_events(&r, EventType::Unmount), 10);
2948 assert_eq!(count_events(&r, EventType::Mount), 0);
2949 }
2950
2951 #[test]
2952 fn autotest_reconcile_explicit_key_mismatch_mounts_instead_of_guessing() {
2953 let old = vec![with_cb(
2957 NodeData::create_text("same content").with_key(1u32),
2958 ComponentEventFilter::BeforeUnmount,
2959 )];
2960 let new = vec![with_cb(
2961 NodeData::create_text("same content").with_key(2u32),
2962 ComponentEventFilter::AfterMount,
2963 )];
2964
2965 let r = diff_flat(&old, &new);
2966 assert!(
2967 r.node_moves.is_empty(),
2968 "keys 1 and 2 must not match, got {:?}",
2969 r.node_moves,
2970 );
2971 assert_eq!(count_events(&r, EventType::Mount), 1);
2972 assert_eq!(count_events(&r, EventType::Unmount), 1);
2973 }
2974
2975 #[test]
2976 fn autotest_reconcile_update_fires_only_on_rec_key_match_with_changed_content() {
2977 let old = vec![NodeData::create_text("v1").with_key(1u32)];
2979 let new = vec![with_cb(
2980 NodeData::create_text("v2").with_key(1u32),
2981 ComponentEventFilter::Updated,
2982 )];
2983
2984 let r = diff_flat(&old, &new);
2985 assert_eq!(r.node_moves.len(), 1, "the key must match across frames");
2986 assert_eq!(count_events(&r, EventType::Update), 1);
2987
2988 let stable = with_cb(
2993 NodeData::create_text("v1").with_key(1u32),
2994 ComponentEventFilter::Updated,
2995 );
2996 let old = vec![stable.clone()];
2997 let new = vec![stable];
2998
2999 let r = diff_flat(&old, &new);
3000 assert_eq!(r.node_moves.len(), 1);
3001 assert_eq!(
3002 count_events(&r, EventType::Update),
3003 0,
3004 "unchanged content must not fire Update",
3005 );
3006 }
3007
3008 #[test]
3009 fn autotest_reconcile_update_requires_the_callback() {
3010 let old = vec![NodeData::create_text("v1").with_key(1u32)];
3012 let new = vec![NodeData::create_text("v2").with_key(1u32)];
3013 let r = diff_flat(&old, &new);
3014 assert_eq!(r.node_moves.len(), 1);
3015 assert!(r.events.is_empty());
3016 }
3017
3018 #[test]
3019 fn autotest_reconcile_missing_layout_entries_default_to_zero_rect() {
3020 let old = vec![NodeData::create_div()];
3023 let new = vec![with_cb(
3024 NodeData::create_div(),
3025 ComponentEventFilter::NodeResized,
3026 )];
3027
3028 let r = diff_flat(&old, &new);
3029 assert_eq!(r.node_moves.len(), 1);
3030 assert_eq!(
3031 count_events(&r, EventType::Resize),
3032 0,
3033 "zero-vs-zero bounds must not be treated as a resize",
3034 );
3035 }
3036
3037 #[test]
3038 fn autotest_reconcile_resize_fires_with_previous_and_current_bounds() {
3039 let old = vec![NodeData::create_div()];
3040 let new = vec![with_cb(
3041 NodeData::create_div(),
3042 ComponentEventFilter::NodeResized,
3043 )];
3044
3045 let r = reconcile_dom(
3046 &old,
3047 &new,
3048 &[],
3049 &[],
3050 &layout_of(&[(0, rect(100.0, 50.0))]),
3051 &layout_of(&[(0, rect(100.0, 80.0))]),
3052 DomId::ROOT_ID,
3053 Instant::now(),
3054 );
3055
3056 assert_eq!(count_events(&r, EventType::Resize), 1);
3057 let EventData::Lifecycle(data) = &r.events[0].data else {
3058 panic!("resize event must carry EventData::Lifecycle");
3059 };
3060 assert_eq!(data.reason, LifecycleReason::Resize);
3061 assert_eq!(data.previous_bounds, Some(rect(100.0, 50.0)));
3062 assert_eq!(data.current_bounds, rect(100.0, 80.0));
3063 }
3064
3065 #[test]
3066 fn autotest_reconcile_resize_ignores_pure_translation() {
3067 let old = vec![NodeData::create_div()];
3069 let new = vec![with_cb(
3070 NodeData::create_div(),
3071 ComponentEventFilter::NodeResized,
3072 )];
3073
3074 let moved = LogicalRect::new(LogicalPosition::new(999.0, 999.0), LogicalSize::new(10.0, 10.0));
3075 let r = reconcile_dom(
3076 &old,
3077 &new,
3078 &[],
3079 &[],
3080 &layout_of(&[(0, rect(10.0, 10.0))]),
3081 &layout_of(&[(0, moved)]),
3082 DomId::ROOT_ID,
3083 Instant::now(),
3084 );
3085 assert_eq!(count_events(&r, EventType::Resize), 0);
3086 }
3087
3088 #[test]
3089 fn autotest_reconcile_nan_bounds_do_not_fire_a_resize_every_frame() {
3090 let old = vec![NodeData::create_div()];
3104 let new = vec![with_cb(
3105 NodeData::create_div(),
3106 ComponentEventFilter::NodeResized,
3107 )];
3108
3109 let nan = rect(f32::NAN, f32::NAN);
3110 let r = reconcile_dom(
3111 &old,
3112 &new,
3113 &[],
3114 &[],
3115 &layout_of(&[(0, nan)]),
3116 &layout_of(&[(0, nan)]),
3117 DomId::ROOT_ID,
3118 Instant::now(),
3119 );
3120 assert_eq!(r.node_moves.len(), 1);
3121 assert_eq!(
3122 count_events(&r, EventType::Resize),
3123 0,
3124 "an unchanged NaN size must not be reported as a resize",
3125 );
3126
3127 let inf = rect(f32::INFINITY, f32::NEG_INFINITY);
3130 let r = reconcile_dom(
3131 &old,
3132 &new,
3133 &[],
3134 &[],
3135 &layout_of(&[(0, inf)]),
3136 &layout_of(&[(0, inf)]),
3137 DomId::ROOT_ID,
3138 Instant::now(),
3139 );
3140 assert_eq!(
3141 count_events(&r, EventType::Resize),
3142 0,
3143 "infinite-but-equal bounds must not be treated as a resize",
3144 );
3145
3146 let r = reconcile_dom(
3148 &old,
3149 &new,
3150 &[],
3151 &[],
3152 &layout_of(&[(0, nan)]),
3153 &layout_of(&[(0, rect(10.0, 20.0))]),
3154 DomId::ROOT_ID,
3155 Instant::now(),
3156 );
3157 assert_eq!(
3158 count_events(&r, EventType::Resize),
3159 1,
3160 "NaN -> a real size is a real resize",
3161 );
3162 }
3163
3164 #[test]
3165 fn autotest_reconcile_extreme_bounds_do_not_panic() {
3166 let old = vec![NodeData::create_div()];
3169 let new = vec![with_cb(
3170 NodeData::create_div(),
3171 ComponentEventFilter::NodeResized,
3172 )];
3173
3174 for (a, b) in [
3175 (rect(f32::MIN, f32::MAX), rect(f32::MAX, f32::MIN)),
3176 (rect(f32::MIN_POSITIVE, 0.0), rect(0.0, f32::MIN_POSITIVE)),
3177 (rect(-0.0, 0.0), rect(0.0, -0.0)), ] {
3179 let r = reconcile_dom(
3180 &old,
3181 &new,
3182 &[],
3183 &[],
3184 &layout_of(&[(0, a)]),
3185 &layout_of(&[(0, b)]),
3186 DomId::ROOT_ID,
3187 Instant::now(),
3188 );
3189 assert_eq!(r.node_moves.len(), 1);
3190 }
3191 }
3192
3193 #[test]
3194 fn autotest_reconcile_keyless_tiers_respect_the_parent_key_gate() {
3195 let old_nd = vec![
3203 NodeData::create_div(),
3204 id_node("left"),
3205 NodeData::create_text("leaf"),
3206 ];
3207 let old_hier = vec![
3208 hitem(None, None, None, Some(1)),
3209 hitem(Some(0), None, None, Some(2)),
3210 hitem(Some(1), None, None, None),
3211 ];
3212
3213 let new_nd = vec![
3214 NodeData::create_div(),
3215 id_node("right"),
3216 NodeData::create_text("leaf"),
3217 ];
3218 let new_hier = old_hier.clone();
3219
3220 let r = reconcile_dom(
3221 &old_nd,
3222 &new_nd,
3223 &old_hier,
3224 &new_hier,
3225 &no_layout(),
3226 &no_layout(),
3227 DomId::ROOT_ID,
3228 Instant::now(),
3229 );
3230
3231 let leaf_matched = r
3234 .node_moves
3235 .iter()
3236 .any(|m| m.new_node_id.index() == 2 && m.old_node_id.index() == 2);
3237 assert!(
3238 !leaf_matched,
3239 "a leaf must not migrate across parents; moves = {:?}",
3240 r.node_moves,
3241 );
3242 }
3243
3244 #[test]
3249 fn autotest_migration_map_empty_and_large() {
3250 assert!(create_migration_map(&[]).is_empty());
3251
3252 let moves: Vec<NodeMove> = (0..1000)
3253 .map(|i| NodeMove {
3254 old_node_id: NodeId::new(i),
3255 new_node_id: NodeId::new(i * 2),
3256 })
3257 .collect();
3258 let map = create_migration_map(&moves);
3259 assert_eq!(map.len(), 1000);
3260 assert_eq!(map.get(&NodeId::new(999)), Some(&NodeId::new(1998)));
3261 }
3262
3263 #[test]
3264 fn autotest_migration_map_duplicate_old_id_keeps_the_last_write() {
3265 let moves = vec![
3268 NodeMove {
3269 old_node_id: NodeId::new(0),
3270 new_node_id: NodeId::new(5),
3271 },
3272 NodeMove {
3273 old_node_id: NodeId::new(0),
3274 new_node_id: NodeId::new(9),
3275 },
3276 ];
3277 let map = create_migration_map(&moves);
3278 assert_eq!(map.len(), 1);
3279 assert_eq!(map.get(&NodeId::new(0)), Some(&NodeId::new(9)));
3280 }
3281
3282 #[test]
3283 fn autotest_migration_map_round_trips_a_real_diff() {
3284 let old: Vec<NodeData> = (0..8).map(|_| NodeData::create_div()).collect();
3285 let new: Vec<NodeData> = (0..8).map(|_| NodeData::create_div()).collect();
3286 let r = diff_flat(&old, &new);
3287
3288 let map = create_migration_map(&r.node_moves);
3289 assert_eq!(map.len(), r.node_moves.len());
3290 for m in &r.node_moves {
3291 assert_eq!(map.get(&m.old_node_id), Some(&m.new_node_id));
3292 }
3293 }
3294
3295 #[allow(dead_code)]
3300 struct TestState(u32);
3301
3302 extern "C" fn merge_keep_old(_new_data: RefAny, old_data: RefAny) -> RefAny {
3305 old_data
3306 }
3307
3308 #[test]
3309 fn autotest_transfer_states_out_of_range_moves_are_skipped() {
3310 let mut old = vec![NodeData::create_div()];
3313 let mut new = vec![NodeData::create_div()];
3314
3315 let moves = vec![
3316 NodeMove {
3317 old_node_id: NodeId::new(5), new_node_id: NodeId::new(0),
3319 },
3320 NodeMove {
3321 old_node_id: NodeId::new(0),
3322 new_node_id: NodeId::new(7), },
3324 NodeMove {
3325 old_node_id: NodeId::new(usize::MAX),
3326 new_node_id: NodeId::new(usize::MAX),
3327 },
3328 ];
3329
3330 transfer_states(&mut old, &mut new, &moves); assert!(new[0].get_dataset().is_none());
3332 }
3333
3334 #[test]
3335 fn autotest_transfer_states_without_merge_callback_leaves_datasets_intact() {
3336 let mut old = vec![NodeData::create_div()];
3337 old[0].set_dataset(OptionRefAny::Some(RefAny::new(TestState(1))));
3338
3339 let mut new = vec![NodeData::create_div()];
3340 new[0].set_dataset(OptionRefAny::Some(RefAny::new(TestState(2))));
3341
3342 let old_ptr = old[0].get_dataset().unwrap().sharing_info.ptr as usize;
3343 let new_ptr = new[0].get_dataset().unwrap().sharing_info.ptr as usize;
3344
3345 transfer_states(
3346 &mut old,
3347 &mut new,
3348 &[NodeMove {
3349 old_node_id: NodeId::new(0),
3350 new_node_id: NodeId::new(0),
3351 }],
3352 );
3353
3354 assert_eq!(
3356 old[0].get_dataset().unwrap().sharing_info.ptr as usize,
3357 old_ptr,
3358 );
3359 assert_eq!(
3360 new[0].get_dataset().unwrap().sharing_info.ptr as usize,
3361 new_ptr,
3362 );
3363 }
3364
3365 #[test]
3366 fn autotest_transfer_states_with_one_missing_dataset_restores_both_sides() {
3367 let mut old = vec![NodeData::create_div()];
3370 let mut new = vec![NodeData::create_div()];
3371 new[0].set_merge_callback(merge_keep_old as DatasetMergeCallbackType);
3372 new[0].set_dataset(OptionRefAny::Some(RefAny::new(TestState(2))));
3373
3374 let new_ptr = new[0].get_dataset().unwrap().sharing_info.ptr as usize;
3375
3376 transfer_states(
3377 &mut old,
3378 &mut new,
3379 &[NodeMove {
3380 old_node_id: NodeId::new(0),
3381 new_node_id: NodeId::new(0),
3382 }],
3383 );
3384
3385 assert!(old[0].get_dataset().is_none());
3386 assert_eq!(
3387 new[0].get_dataset().unwrap().sharing_info.ptr as usize,
3388 new_ptr,
3389 "the fresh dataset must be restored, not dropped",
3390 );
3391 }
3392
3393 #[test]
3394 fn autotest_transfer_states_repoints_orphaned_callback_refanys() {
3395 let fresh = RefAny::new(TestState(1));
3401 let fresh_ptr = fresh.sharing_info.ptr as usize;
3402
3403 let mut new0 = NodeData::create_div();
3404 new0.set_merge_callback(merge_keep_old as DatasetMergeCallbackType);
3405 new0.set_dataset(OptionRefAny::Some(fresh.clone()));
3406 new0.add_callback(
3408 EventFilter::Component(ComponentEventFilter::Selected),
3409 fresh.clone(),
3410 noop_callback(),
3411 );
3412
3413 let mut new1 = NodeData::create_div();
3416 new1.add_callback(
3417 EventFilter::Component(ComponentEventFilter::Selected),
3418 fresh.clone(),
3419 noop_callback(),
3420 );
3421
3422 let persistent = RefAny::new(TestState(2));
3423 let persistent_ptr = persistent.sharing_info.ptr as usize;
3424 assert_ne!(fresh_ptr, persistent_ptr, "test setup: allocations must differ");
3425
3426 let mut old = vec![NodeData::create_div()];
3427 old[0].set_dataset(OptionRefAny::Some(persistent));
3428
3429 let mut new = vec![new0, new1];
3430
3431 transfer_states(
3432 &mut old,
3433 &mut new,
3434 &[NodeMove {
3435 old_node_id: NodeId::new(0),
3436 new_node_id: NodeId::new(0),
3437 }],
3438 );
3439
3440 assert_eq!(
3442 new[0].get_dataset().unwrap().sharing_info.ptr as usize,
3443 persistent_ptr,
3444 "the merge must keep the persistent allocation",
3445 );
3446 assert!(old[0].get_dataset().is_none());
3448
3449 for (i, nd) in new.iter().enumerate() {
3452 for cb in nd.callbacks.as_ref() {
3453 assert_eq!(
3454 cb.refany.sharing_info.ptr as usize,
3455 persistent_ptr,
3456 "node {i}: an orphaned callback refany was not re-pointed",
3457 );
3458 }
3459 }
3460 }
3461
3462 #[test]
3467 fn autotest_accumulator_new_is_empty_and_inert() {
3468 let a = ChangeAccumulator::new();
3469 assert!(a.is_empty());
3470 assert!(!a.needs_layout());
3471 assert!(!a.needs_paint_only());
3472 assert!(a.is_visually_unchanged());
3473 assert_eq!(a.max_scope, RelayoutScope::None);
3474 let d = ChangeAccumulator::default();
3476 assert_eq!(a.is_empty(), d.is_empty());
3477 assert_eq!(a.max_scope, d.max_scope);
3478 }
3479
3480 #[test]
3481 fn autotest_accumulator_mount_forces_layout_unmount_does_not() {
3482 let mut a = ChangeAccumulator::new();
3483 a.add_mount(NodeId::new(0));
3484 assert!(!a.is_empty());
3485 assert!(a.needs_layout(), "a mounted node always needs layout");
3486 assert!(!a.needs_paint_only());
3487 assert!(!a.is_visually_unchanged());
3488
3489 let mut a = ChangeAccumulator::new();
3492 a.add_unmount(NodeId::new(0));
3493 assert!(!a.is_empty());
3494 assert!(!a.needs_layout());
3495 assert!(!a.is_visually_unchanged());
3496 }
3497
3498 #[test]
3499 fn autotest_accumulator_css_change_routes_paint_vs_layout_by_scope() {
3500 let mut a = ChangeAccumulator::new();
3502 a.add_css_change(NodeId::new(0), CssPropertyType::TextColor, RelayoutScope::None);
3503 assert!(!a.needs_layout());
3504 assert!(a.needs_paint_only());
3505 assert!(!a.is_visually_unchanged());
3506 assert_eq!(a.max_scope, RelayoutScope::None);
3507 assert!(a.per_node[&NodeId::new(0)]
3508 .change_set
3509 .contains(NodeChangeSet::INLINE_STYLE_PAINT));
3510
3511 let mut a = ChangeAccumulator::new();
3513 a.add_css_change(NodeId::new(0), CssPropertyType::Width, RelayoutScope::SizingOnly);
3514 assert!(a.needs_layout());
3515 assert!(!a.needs_paint_only(), "layout work subsumes paint-only");
3516 assert_eq!(a.max_scope, RelayoutScope::SizingOnly);
3517 assert!(a.per_node[&NodeId::new(0)]
3518 .change_set
3519 .contains(NodeChangeSet::INLINE_STYLE_LAYOUT));
3520 }
3521
3522 #[test]
3523 fn autotest_accumulator_max_scope_is_monotone() {
3524 let mut a = ChangeAccumulator::new();
3527 a.add_css_change(NodeId::new(0), CssPropertyType::Display, RelayoutScope::Full);
3528 assert_eq!(a.max_scope, RelayoutScope::Full);
3529
3530 a.add_css_change(NodeId::new(0), CssPropertyType::TextColor, RelayoutScope::None);
3531 assert_eq!(a.max_scope, RelayoutScope::Full, "max_scope must not regress");
3532 assert_eq!(
3533 a.per_node[&NodeId::new(0)].relayout_scope,
3534 RelayoutScope::Full,
3535 "per-node scope must not regress either",
3536 );
3537
3538 a.add_css_change(NodeId::new(1), CssPropertyType::Width, RelayoutScope::SizingOnly);
3539 assert_eq!(a.max_scope, RelayoutScope::Full);
3540 assert_eq!(
3541 a.per_node[&NodeId::new(1)].relayout_scope,
3542 RelayoutScope::SizingOnly,
3543 "a different node keeps its own, lower scope",
3544 );
3545 }
3546
3547 #[test]
3548 fn autotest_accumulator_text_change_is_ifc_scoped_and_unicode_safe() {
3549 for s in UNICODE_SAMPLES {
3550 let mut a = ChangeAccumulator::new();
3551 a.add_text_change(NodeId::new(0), String::new(), (*s).to_string());
3552
3553 let report = &a.per_node[&NodeId::new(0)];
3554 assert!(report.change_set.contains(NodeChangeSet::TEXT_CONTENT));
3555 assert_eq!(report.relayout_scope, RelayoutScope::IfcOnly);
3556 assert_eq!(
3557 report.text_change,
3558 Some(TextChange {
3559 old_text: String::new(),
3560 new_text: (*s).to_string(),
3561 }),
3562 );
3563 assert!(a.needs_layout());
3564 assert_eq!(a.max_scope, RelayoutScope::IfcOnly);
3565 }
3566 }
3567
3568 #[test]
3569 fn autotest_accumulator_add_dom_change_accumulates_and_never_clears_text() {
3570 let node = NodeId::new(0);
3571 let mut a = ChangeAccumulator::new();
3572
3573 a.add_dom_change(
3574 node,
3575 NodeChangeSet {
3576 bits: NodeChangeSet::TEXT_CONTENT,
3577 },
3578 RelayoutScope::IfcOnly,
3579 Some(TextChange {
3580 old_text: "a".to_string(),
3581 new_text: "b".to_string(),
3582 }),
3583 vec![CssPropertyType::Width],
3584 );
3585
3586 a.add_dom_change(
3588 node,
3589 NodeChangeSet {
3590 bits: NodeChangeSet::STYLED_STATE,
3591 },
3592 RelayoutScope::None,
3593 None,
3594 vec![CssPropertyType::TextColor],
3595 );
3596
3597 let report = &a.per_node[&node];
3598 assert!(report.change_set.contains(NodeChangeSet::TEXT_CONTENT));
3599 assert!(
3600 report.change_set.contains(NodeChangeSet::STYLED_STATE),
3601 "flags must be OR-accumulated across calls",
3602 );
3603 assert_eq!(report.relayout_scope, RelayoutScope::IfcOnly);
3604 assert!(
3605 report.text_change.is_some(),
3606 "a None text_change must not erase a previously recorded one",
3607 );
3608 assert_eq!(
3609 report.changed_css_properties,
3610 vec![CssPropertyType::Width, CssPropertyType::TextColor],
3611 "changed properties must be appended, not replaced",
3612 );
3613 }
3614
3615 #[test]
3616 fn autotest_accumulator_image_change() {
3617 let mut a = ChangeAccumulator::new();
3618 a.add_image_change(NodeId::new(0), RelayoutScope::SizingOnly);
3619 assert!(a.per_node[&NodeId::new(0)]
3620 .change_set
3621 .contains(NodeChangeSet::IMAGE_CHANGED));
3622 assert!(a.needs_layout());
3623 assert_eq!(a.max_scope, RelayoutScope::SizingOnly);
3624 }
3625
3626 #[test]
3627 fn autotest_accumulator_merge_empty_restyle_result_is_a_no_op() {
3628 let mut a = ChangeAccumulator::new();
3629 a.merge_restyle_result(&RestyleResult::default());
3630 assert!(a.is_empty());
3631 assert!(a.is_visually_unchanged());
3632 }
3633
3634 #[test]
3635 fn autotest_accumulator_merge_restyle_result_classifies_by_property() {
3636 let prop = CssProperty::Width(CssPropertyValue::Exact(LayoutWidth::const_px(100)));
3637 let changed = ChangedCssProperty {
3638 previous_state: StyledNodeState::default(),
3639 previous_prop: prop.clone(),
3640 current_state: StyledNodeState::default(),
3641 current_prop: prop,
3642 };
3643
3644 let mut restyle = RestyleResult::default();
3645 restyle
3646 .changed_nodes
3647 .insert(NodeId::new(3), vec![changed]);
3648
3649 let mut a = ChangeAccumulator::new();
3650 a.merge_restyle_result(&restyle);
3651
3652 assert!(!a.is_empty());
3654 assert!(a.needs_layout());
3655 assert_eq!(a.max_scope, RelayoutScope::SizingOnly);
3656 let report = &a.per_node[&NodeId::new(3)];
3657 assert!(report.change_set.contains(NodeChangeSet::INLINE_STYLE_LAYOUT));
3658 assert_eq!(report.changed_css_properties, vec![CssPropertyType::Width]);
3659 }
3660
3661 #[test]
3662 fn autotest_accumulator_merge_extended_diff_counts_mounts_and_unmounts() {
3663 let old_nd = vec![NodeData::create_div(), NodeData::create_div()];
3665 let new_nd = vec![
3666 NodeData::create_div(),
3667 NodeData::create_div(),
3668 NodeData::create_div(),
3669 ];
3670
3671 let mut a = ChangeAccumulator::new();
3672 a.merge_extended_diff(&ExtendedDiffResult::default(), &old_nd, &new_nd);
3673
3674 assert_eq!(a.mounted_nodes.len(), 3);
3675 assert_eq!(a.unmounted_nodes.len(), 2);
3676 assert!(!a.is_empty());
3677 assert!(a.needs_layout(), "mounted nodes always need layout");
3678 assert!(!a.is_visually_unchanged());
3679 }
3680
3681 #[test]
3682 fn autotest_accumulator_merge_extended_diff_on_empty_doms_is_empty() {
3683 let mut a = ChangeAccumulator::new();
3684 a.merge_extended_diff(&ExtendedDiffResult::default(), &[], &[]);
3685 assert!(a.is_empty());
3686 }
3687
3688 #[test]
3689 fn autotest_accumulator_merge_extended_diff_skips_empty_change_sets() {
3690 let old_nd = vec![NodeData::create_div()];
3692 let new_nd = vec![NodeData::create_div()];
3693
3694 let extended = ExtendedDiffResult {
3695 diff: DiffResult {
3696 events: Vec::new(),
3697 node_moves: vec![NodeMove {
3698 old_node_id: NodeId::new(0),
3699 new_node_id: NodeId::new(0),
3700 }],
3701 },
3702 node_changes: vec![(NodeId::new(0), NodeId::new(0), NodeChangeSet::empty())],
3703 };
3704
3705 let mut a = ChangeAccumulator::new();
3706 a.merge_extended_diff(&extended, &old_nd, &new_nd);
3707
3708 assert!(a.per_node.is_empty(), "an empty change set must be skipped");
3709 assert!(a.mounted_nodes.is_empty());
3710 assert!(a.unmounted_nodes.is_empty());
3711 assert!(a.is_empty());
3712 }
3713
3714 #[test]
3715 fn autotest_accumulator_merge_extended_diff_extracts_text_change() {
3716 let old_nd = vec![NodeData::create_text("héllo")];
3717 let new_nd = vec![NodeData::create_text("héllo wörld")];
3718
3719 let extended = ExtendedDiffResult {
3720 diff: DiffResult {
3721 events: Vec::new(),
3722 node_moves: vec![NodeMove {
3723 old_node_id: NodeId::new(0),
3724 new_node_id: NodeId::new(0),
3725 }],
3726 },
3727 node_changes: vec![(
3728 NodeId::new(0),
3729 NodeId::new(0),
3730 NodeChangeSet {
3731 bits: NodeChangeSet::TEXT_CONTENT,
3732 },
3733 )],
3734 };
3735
3736 let mut a = ChangeAccumulator::new();
3737 a.merge_extended_diff(&extended, &old_nd, &new_nd);
3738
3739 let report = &a.per_node[&NodeId::new(0)];
3740 assert_eq!(
3741 report.text_change,
3742 Some(TextChange {
3743 old_text: "héllo".to_string(),
3744 new_text: "héllo wörld".to_string(),
3745 }),
3746 "TEXT_CONTENT must carry the old/new text for cursor reconciliation",
3747 );
3748 assert_eq!(report.relayout_scope, RelayoutScope::IfcOnly);
3749 }
3750
3751 #[test]
3756 fn autotest_classify_scope_maps_each_flag_to_its_documented_scope() {
3757 let nodes = vec![NodeData::create_div()];
3758 let id = NodeId::new(0);
3759
3760 let classify = |bits: u32| {
3761 ChangeAccumulator::classify_change_scope(NodeChangeSet { bits }, &nodes, id)
3762 };
3763
3764 assert_eq!(classify(0), RelayoutScope::None, "empty -> no work");
3765 assert_eq!(classify(NodeChangeSet::NODE_TYPE_CHANGED), RelayoutScope::Full);
3766 assert_eq!(classify(NodeChangeSet::CHILDREN_CHANGED), RelayoutScope::Full);
3767 assert_eq!(classify(NodeChangeSet::IDS_AND_CLASSES), RelayoutScope::Full);
3768 assert_eq!(classify(NodeChangeSet::TEXT_CONTENT), RelayoutScope::IfcOnly);
3769 assert_eq!(classify(NodeChangeSet::IMAGE_CHANGED), RelayoutScope::SizingOnly);
3770 assert_eq!(classify(NodeChangeSet::CONTENTEDITABLE), RelayoutScope::SizingOnly);
3771 assert_eq!(classify(NodeChangeSet::STYLED_STATE), RelayoutScope::None);
3772 assert_eq!(classify(NodeChangeSet::INLINE_STYLE_PAINT), RelayoutScope::None);
3773 assert_eq!(classify(NodeChangeSet::CALLBACKS), RelayoutScope::None);
3775 assert_eq!(classify(NodeChangeSet::DATASET), RelayoutScope::None);
3776 assert_eq!(classify(NodeChangeSet::TAB_INDEX), RelayoutScope::None);
3777 }
3778
3779 #[test]
3780 fn autotest_classify_scope_precedence_is_widest_first() {
3781 let nodes = vec![NodeData::create_div()];
3782 let id = NodeId::new(0);
3783
3784 let bits = NodeChangeSet::NODE_TYPE_CHANGED
3786 | NodeChangeSet::TEXT_CONTENT
3787 | NodeChangeSet::IMAGE_CHANGED
3788 | NodeChangeSet::STYLED_STATE;
3789 assert_eq!(
3790 ChangeAccumulator::classify_change_scope(NodeChangeSet { bits }, &nodes, id),
3791 RelayoutScope::Full,
3792 );
3793
3794 let bits = NodeChangeSet::TEXT_CONTENT | NodeChangeSet::IMAGE_CHANGED;
3797 assert_eq!(
3798 ChangeAccumulator::classify_change_scope(NodeChangeSet { bits }, &nodes, id),
3799 RelayoutScope::IfcOnly,
3800 );
3801 }
3802
3803 #[test]
3804 fn autotest_classify_scope_inline_layout_walks_the_nodes_own_css() {
3805 let nodes = vec![NodeData::create_div().with_css("width: 100px")];
3807 assert_eq!(
3808 ChangeAccumulator::classify_change_scope(
3809 NodeChangeSet {
3810 bits: NodeChangeSet::INLINE_STYLE_LAYOUT,
3811 },
3812 &nodes,
3813 NodeId::new(0),
3814 ),
3815 RelayoutScope::SizingOnly,
3816 );
3817
3818 let nodes = vec![NodeData::create_div().with_css("display: flex")];
3820 assert_eq!(
3821 ChangeAccumulator::classify_change_scope(
3822 NodeChangeSet {
3823 bits: NodeChangeSet::INLINE_STYLE_LAYOUT,
3824 },
3825 &nodes,
3826 NodeId::new(0),
3827 ),
3828 RelayoutScope::Full,
3829 );
3830
3831 let nodes = vec![NodeData::create_div()];
3835 assert_eq!(
3836 ChangeAccumulator::classify_change_scope(
3837 NodeChangeSet {
3838 bits: NodeChangeSet::INLINE_STYLE_LAYOUT,
3839 },
3840 &nodes,
3841 NodeId::new(0),
3842 ),
3843 RelayoutScope::SizingOnly,
3844 "an INLINE_STYLE_LAYOUT change must never classify as 'no layout'",
3845 );
3846 }
3847
3848 #[test]
3853 fn autotest_reconcile_with_changes_on_empty_doms() {
3854 let r = reconcile_dom_with_changes(
3855 &[],
3856 &[],
3857 &[],
3858 &[],
3859 None,
3860 None,
3861 &no_layout(),
3862 &no_layout(),
3863 DomId::ROOT_ID,
3864 Instant::now(),
3865 );
3866 assert!(r.diff.events.is_empty());
3867 assert!(r.diff.node_moves.is_empty());
3868 assert!(r.node_changes.is_empty());
3869 }
3870
3871 #[test]
3872 fn autotest_reconcile_with_changes_reports_one_entry_per_move() {
3873 let old = vec![NodeData::create_text("v1").with_key(1u32)];
3874 let new = vec![NodeData::create_text("v2").with_key(1u32)];
3875
3876 let r = reconcile_dom_with_changes(
3877 &old,
3878 &new,
3879 &[],
3880 &[],
3881 None,
3882 None,
3883 &no_layout(),
3884 &no_layout(),
3885 DomId::ROOT_ID,
3886 Instant::now(),
3887 );
3888
3889 assert_eq!(r.diff.node_moves.len(), 1);
3890 assert_eq!(
3891 r.node_changes.len(),
3892 r.diff.node_moves.len(),
3893 "there must be exactly one change entry per matched pair",
3894 );
3895
3896 let (old_id, new_id, changes) = &r.node_changes[0];
3897 assert_eq!(*old_id, NodeId::new(0));
3898 assert_eq!(*new_id, NodeId::new(0));
3899 assert!(changes.contains(NodeChangeSet::TEXT_CONTENT));
3900 }
3901
3902 #[test]
3903 fn autotest_reconcile_with_changes_tolerates_short_styled_state_slices() {
3904 let old = vec![NodeData::create_div(), NodeData::create_div()];
3907 let new = vec![NodeData::create_div(), NodeData::create_div()];
3908 let short = [StyledNodeState::default()]; let r = reconcile_dom_with_changes(
3911 &old,
3912 &new,
3913 &[],
3914 &[],
3915 Some(&short[..]),
3916 Some(&short[..]),
3917 &no_layout(),
3918 &no_layout(),
3919 DomId::ROOT_ID,
3920 Instant::now(),
3921 );
3922 assert_eq!(r.node_changes.len(), 2);
3923 for (_, _, changes) in &r.node_changes {
3925 assert!(!changes.contains(NodeChangeSet::STYLED_STATE));
3926 }
3927 }
3928
3929 #[test]
3930 fn autotest_reconcile_with_changes_feeds_the_accumulator() {
3931 let old = vec![NodeData::create_text("before").with_key(1u32)];
3933 let new = vec![NodeData::create_text("after").with_key(1u32)];
3934
3935 let extended = reconcile_dom_with_changes(
3936 &old,
3937 &new,
3938 &[],
3939 &[],
3940 None,
3941 None,
3942 &no_layout(),
3943 &no_layout(),
3944 DomId::ROOT_ID,
3945 Instant::now(),
3946 );
3947
3948 let mut acc = ChangeAccumulator::new();
3949 acc.merge_extended_diff(&extended, &old, &new);
3950
3951 assert!(!acc.is_empty());
3952 assert!(acc.needs_layout(), "a text edit needs (IFC) layout");
3953 assert!(!acc.is_visually_unchanged());
3954 assert!(acc.mounted_nodes.is_empty());
3955 assert!(acc.unmounted_nodes.is_empty());
3956
3957 let report = &acc.per_node[&NodeId::new(0)];
3958 assert_eq!(report.relayout_scope, RelayoutScope::IfcOnly);
3959 assert_eq!(
3960 report.text_change,
3961 Some(TextChange {
3962 old_text: "before".to_string(),
3963 new_text: "after".to_string(),
3964 }),
3965 );
3966 }
3967
3968 #[test]
3973 fn autotest_fingerprint_default_and_self_comparison_are_inert() {
3974 let d = NodeDataFingerprint::default();
3975 assert!(d.is_identical(&d));
3976 assert!(d.diff(&d).is_empty());
3977 assert!(!d.might_affect_layout(&d));
3978 assert!(!d.might_affect_visuals(&d));
3979 assert_eq!(d, NodeDataFingerprint::default());
3980 }
3981
3982 #[test]
3983 fn autotest_fingerprint_is_a_pure_function_of_its_inputs() {
3984 let state = StyledNodeState::default();
3987 for s in UNICODE_SAMPLES {
3988 let a = NodeDataFingerprint::compute(&NodeData::create_text(*s), Some(&state));
3989 let b = NodeDataFingerprint::compute(&NodeData::create_text(*s), Some(&state));
3990 assert_eq!(a, b, "fingerprint of {s:?} is not deterministic");
3991 assert!(a.is_identical(&b));
3992 assert!(a.diff(&b).is_empty());
3993 }
3994 }
3995
3996 #[test]
3997 fn autotest_fingerprint_diff_is_symmetric() {
3998 let a = NodeDataFingerprint::compute(&NodeData::create_text("a"), None);
3999 let b = NodeDataFingerprint::compute(&class_node("x").with_css("width: 1px"), None);
4000
4001 assert_eq!(a.diff(&b), b.diff(&a), "diff must be symmetric");
4002 assert_eq!(
4003 a.might_affect_layout(&b),
4004 b.might_affect_layout(&a),
4005 "might_affect_layout must be symmetric",
4006 );
4007 assert_eq!(a.might_affect_visuals(&b), b.might_affect_visuals(&a));
4008 }
4009
4010 #[test]
4011 fn autotest_fingerprint_text_change_is_layout_and_visual() {
4012 let a = NodeDataFingerprint::compute(&NodeData::create_text("one"), None);
4013 let b = NodeDataFingerprint::compute(&NodeData::create_text("two"), None);
4014
4015 assert!(!a.is_identical(&b));
4016 let changes = a.diff(&b);
4017 assert!(changes.contains(NodeChangeSet::TEXT_CONTENT));
4019 assert!(changes.contains(NodeChangeSet::IMAGE_CHANGED));
4020 assert!(a.might_affect_layout(&b));
4021 assert!(a.might_affect_visuals(&b));
4022 }
4023
4024 #[test]
4025 fn autotest_fingerprint_styled_state_is_visual_but_not_layout() {
4026 let node = NodeData::create_div();
4029 let calm = StyledNodeState::default();
4030 let hovered = StyledNodeState {
4031 hover: true,
4032 ..StyledNodeState::default()
4033 };
4034
4035 let a = NodeDataFingerprint::compute(&node, Some(&calm));
4036 let b = NodeDataFingerprint::compute(&node, Some(&hovered));
4037
4038 assert!(!a.is_identical(&b));
4039 assert!(a.diff(&b).contains(NodeChangeSet::STYLED_STATE));
4040 assert!(
4041 !a.might_affect_layout(&b),
4042 "a styled-state change must not be able to request layout",
4043 );
4044 assert!(a.might_affect_visuals(&b));
4045 }
4046
4047 #[test]
4048 fn autotest_fingerprint_callback_change_is_neither_layout_nor_visual() {
4049 let plain = NodeData::create_div();
4050 let with_handler = with_cb(NodeData::create_div(), ComponentEventFilter::AfterMount);
4051
4052 let a = NodeDataFingerprint::compute(&plain, None);
4053 let b = NodeDataFingerprint::compute(&with_handler, None);
4054
4055 assert!(!a.is_identical(&b), "the callback list must be fingerprinted");
4056 assert!(a.diff(&b).contains(NodeChangeSet::CALLBACKS));
4057 assert!(
4058 !a.might_affect_layout(&b),
4059 "swapping an event handler must not trigger relayout",
4060 );
4061 assert!(
4062 !a.might_affect_visuals(&b),
4063 "swapping an event handler must not trigger a repaint",
4064 );
4065 }
4066
4067 #[test]
4068 fn autotest_fingerprint_ids_classes_and_inline_css_are_layout_relevant() {
4069 let base = NodeDataFingerprint::compute(&NodeData::create_div(), None);
4070
4071 let classes = NodeDataFingerprint::compute(&class_node("banner"), None);
4072 assert!(base.diff(&classes).contains(NodeChangeSet::IDS_AND_CLASSES));
4073 assert!(base.might_affect_layout(&classes));
4074 assert!(base.might_affect_visuals(&classes));
4075
4076 let styled =
4077 NodeDataFingerprint::compute(&NodeData::create_div().with_css("width: 3px"), None);
4078 assert!(base.diff(&styled).contains(NodeChangeSet::INLINE_STYLE_LAYOUT));
4079 assert!(base.might_affect_layout(&styled));
4080 assert!(base.might_affect_visuals(&styled));
4081 }
4082
4083 #[test]
4084 fn autotest_fingerprint_attrs_change_flags_tab_index_and_contenteditable() {
4085 let base = NodeDataFingerprint::compute(&NodeData::create_div(), None);
4086 let editable =
4087 NodeDataFingerprint::compute(&NodeData::create_div().with_contenteditable(true), None);
4088
4089 let changes = base.diff(&editable);
4090 assert!(changes.contains(NodeChangeSet::TAB_INDEX));
4091 assert!(changes.contains(NodeChangeSet::CONTENTEDITABLE));
4092 assert!(
4093 base.might_affect_layout(&editable),
4094 "attrs_hash feeds might_affect_layout",
4095 );
4096 assert!(
4097 !base.might_affect_visuals(&editable),
4098 "attrs_hash is deliberately NOT part of might_affect_visuals",
4099 );
4100 }
4101
4102 #[test]
4103 fn autotest_fingerprint_agrees_with_compute_node_changes_on_unchanged_nodes() {
4104 let state = StyledNodeState::default();
4108 let samples = vec![
4109 NodeData::create_div(),
4110 NodeData::create_text("hello 🌍"),
4111 class_node("row"),
4112 id_node("main"),
4113 NodeData::create_div().with_css("color: red"),
4114 NodeData::create_div().with_contenteditable(true),
4115 with_cb(NodeData::create_div(), ComponentEventFilter::AfterMount),
4116 ];
4117
4118 for node in &samples {
4119 let clone = node.clone();
4120
4121 let fp_a = NodeDataFingerprint::compute(node, Some(&state));
4122 let fp_b = NodeDataFingerprint::compute(&clone, Some(&state));
4123 assert!(
4124 fp_a.is_identical(&fp_b),
4125 "a cloned node must fingerprint identically",
4126 );
4127 assert!(fp_a.diff(&fp_b).is_empty());
4128
4129 let tier2 = compute_node_changes(node, &clone, Some(&state), Some(&state));
4130 assert!(
4131 tier2.is_empty(),
4132 "compute_node_changes must agree that a clone is unchanged, got {:#b}",
4133 tier2.bits,
4134 );
4135 }
4136 }
4137}