1use alloc::{collections::VecDeque, vec::Vec};
16use core::hash::Hash;
17
18use crate::{
19 dom::{DomId, DomNodeHash, DomNodeId, NodeData, IdOrClass},
20 events::{
21 ComponentEventFilter, EventData, EventFilter, EventPhase, EventSource, EventType,
22 LifecycleEventData, LifecycleReason, SyntheticEvent,
23 },
24 geom::LogicalRect,
25 id::NodeId,
26 styled_dom::{NodeHierarchyItemId, NodeHierarchyItem},
27 task::Instant,
28 FastHashMap,
29};
30
31#[derive(Debug, Clone, Copy)]
33pub struct NodeMove {
34 pub old_node_id: NodeId,
36 pub new_node_id: NodeId,
38}
39
40#[derive(Debug, Clone)]
42pub struct DiffResult {
43 pub events: Vec<SyntheticEvent>,
45 pub node_moves: Vec<NodeMove>,
47}
48
49impl Default for DiffResult {
50 fn default() -> Self {
51 Self {
52 events: Vec::new(),
53 node_moves: Vec::new(),
54 }
55 }
56}
57
58pub fn calculate_reconciliation_key(
74 node_data: &[NodeData],
75 hierarchy: &[NodeHierarchyItem],
76 node_id: NodeId,
77) -> u64 {
78 use highway::{HighwayHash, HighwayHasher, Key};
79
80 let node = &node_data[node_id.index()];
81
82 if let Some(key) = node.get_key() {
84 return key;
85 }
86
87 for id_or_class in node.ids_and_classes.as_ref().iter() {
89 if let IdOrClass::Id(id) = id_or_class {
90 let mut hasher = HighwayHasher::new(Key([0; 4]));
91 id.as_str().hash(&mut hasher);
92 return hasher.finalize64();
93 }
94 }
95
96 let mut hasher = HighwayHasher::new(Key([0; 4]));
98
99 core::mem::discriminant(node.get_node_type()).hash(&mut hasher);
101 for id_or_class in node.ids_and_classes.as_ref().iter() {
102 if let IdOrClass::Class(class) = id_or_class {
103 class.as_str().hash(&mut hasher);
104 }
105 }
106
107 if let Some(hierarchy_item) = hierarchy.get(node_id.index()) {
109 if let Some(parent_id) = hierarchy_item.parent_id() {
110 let mut sibling_index: usize = 0;
112 let parent_hierarchy = &hierarchy[parent_id.index()];
113
114 let mut current = parent_hierarchy.first_child_id(parent_id);
116 while let Some(sibling_id) = current {
117 if sibling_id == node_id {
118 break;
119 }
120 let sibling = &node_data[sibling_id.index()];
122 if core::mem::discriminant(sibling.get_node_type())
123 == core::mem::discriminant(node.get_node_type())
124 {
125 sibling_index += 1;
126 }
127 current = hierarchy[sibling_id.index()].next_sibling_id();
128 }
129
130 sibling_index.hash(&mut hasher);
131
132 let parent_key = calculate_reconciliation_key(node_data, hierarchy, parent_id);
134 parent_key.hash(&mut hasher);
135 }
136 }
137
138 hasher.finalize64()
139}
140
141pub fn precompute_reconciliation_keys(
150 node_data: &[NodeData],
151 hierarchy: &[NodeHierarchyItem],
152) -> FastHashMap<NodeId, u64> {
153 let mut keys = FastHashMap::default();
154 for idx in 0..node_data.len() {
155 let node_id = NodeId::new(idx);
156 let key = calculate_reconciliation_key(node_data, hierarchy, node_id);
157 keys.insert(node_id, key);
158 }
159 keys
160}
161
162pub fn reconcile_dom(
179 old_node_data: &[NodeData],
180 new_node_data: &[NodeData],
181 old_layout: &FastHashMap<NodeId, LogicalRect>,
182 new_layout: &FastHashMap<NodeId, LogicalRect>,
183 dom_id: DomId,
184 timestamp: Instant,
185) -> DiffResult {
186 let mut result = DiffResult::default();
187
188 let mut old_keyed: FastHashMap<u64, NodeId> = FastHashMap::default();
199 let mut old_hashed: FastHashMap<DomNodeHash, VecDeque<NodeId>> = FastHashMap::default();
200 let mut old_structural: FastHashMap<DomNodeHash, VecDeque<NodeId>> = FastHashMap::default();
201 let mut old_nodes_consumed = vec![false; old_node_data.len()];
202
203 for (idx, node) in old_node_data.iter().enumerate() {
204 let id = NodeId::new(idx);
205
206 if let Some(key) = node.get_key() {
207 old_keyed.insert(key, id);
209 } else {
210 let hash = node.calculate_node_data_hash();
212 old_hashed.entry(hash).or_default().push_back(id);
213
214 let structural_hash = node.calculate_structural_hash();
216 old_structural.entry(structural_hash).or_default().push_back(id);
217 }
218 }
219
220 for (new_idx, new_node) in new_node_data.iter().enumerate() {
223 let new_id = NodeId::new(new_idx);
224 let mut matched_old_id = None;
225
226 if let Some(key) = new_node.get_key() {
228 if let Some(&old_id) = old_keyed.get(&key) {
229 if !old_nodes_consumed[old_id.index()] {
230 matched_old_id = Some(old_id);
231 }
232 }
233 }
234 else {
236 let hash = new_node.calculate_node_data_hash();
237
238 if let Some(queue) = old_hashed.get_mut(&hash) {
240 while let Some(old_id) = queue.front() {
242 if !old_nodes_consumed[old_id.index()] {
243 matched_old_id = Some(*old_id);
244 queue.pop_front();
245 break;
246 } else {
247 queue.pop_front();
248 }
249 }
250 }
251
252 if matched_old_id.is_none() {
254 let structural_hash = new_node.calculate_structural_hash();
255 if let Some(queue) = old_structural.get_mut(&structural_hash) {
256 while let Some(old_id) = queue.front() {
257 if !old_nodes_consumed[old_id.index()] {
258 matched_old_id = Some(*old_id);
259 queue.pop_front();
260 break;
261 } else {
262 queue.pop_front();
263 }
264 }
265 }
266 }
267 }
268
269 if let Some(old_id) = matched_old_id {
272 old_nodes_consumed[old_id.index()] = true;
275 result.node_moves.push(NodeMove {
276 old_node_id: old_id,
277 new_node_id: new_id,
278 });
279
280 let old_rect = old_layout.get(&old_id).copied().unwrap_or(LogicalRect::zero());
282 let new_rect = new_layout.get(&new_id).copied().unwrap_or(LogicalRect::zero());
283
284 if old_rect.size != new_rect.size {
285 if has_resize_callback(new_node) {
287 result.events.push(create_lifecycle_event(
288 EventType::Resize,
289 new_id,
290 dom_id,
291 ×tamp,
292 LifecycleEventData {
293 reason: LifecycleReason::Resize,
294 previous_bounds: Some(old_rect),
295 current_bounds: new_rect,
296 },
297 ));
298 }
299 }
300
301 if new_node.get_key().is_some() {
303 let old_hash = old_node_data[old_id.index()].calculate_node_data_hash();
304 let new_hash = new_node.calculate_node_data_hash();
305
306 if old_hash != new_hash && has_update_callback(new_node) {
307 result.events.push(create_lifecycle_event(
308 EventType::Update,
309 new_id,
310 dom_id,
311 ×tamp,
312 LifecycleEventData {
313 reason: LifecycleReason::Update,
314 previous_bounds: Some(old_rect),
315 current_bounds: new_rect,
316 },
317 ));
318 }
319 }
320 } else {
321 if has_mount_callback(new_node) {
323 let bounds = new_layout.get(&new_id).copied().unwrap_or(LogicalRect::zero());
324 result.events.push(create_lifecycle_event(
325 EventType::Mount,
326 new_id,
327 dom_id,
328 ×tamp,
329 LifecycleEventData {
330 reason: LifecycleReason::InitialMount,
331 previous_bounds: None,
332 current_bounds: bounds,
333 },
334 ));
335 }
336 }
337 }
338
339 for (old_idx, consumed) in old_nodes_consumed.iter().enumerate() {
343 if !consumed {
344 let old_id = NodeId::new(old_idx);
345 let old_node = &old_node_data[old_idx];
346
347 if has_unmount_callback(old_node) {
348 let bounds = old_layout.get(&old_id).copied().unwrap_or(LogicalRect::zero());
349 result.events.push(create_lifecycle_event(
350 EventType::Unmount,
351 old_id,
352 dom_id,
353 ×tamp,
354 LifecycleEventData {
355 reason: LifecycleReason::InitialMount, previous_bounds: Some(bounds),
357 current_bounds: LogicalRect::zero(),
358 },
359 ));
360 }
361 }
362 }
363
364 result
365}
366
367fn create_lifecycle_event(
369 event_type: EventType,
370 node_id: NodeId,
371 dom_id: DomId,
372 timestamp: &Instant,
373 data: LifecycleEventData,
374) -> SyntheticEvent {
375 let dom_node_id = DomNodeId {
376 dom: dom_id,
377 node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
378 };
379 SyntheticEvent {
380 event_type,
381 source: EventSource::Lifecycle,
382 phase: EventPhase::Target,
383 target: dom_node_id,
384 current_target: dom_node_id,
385 timestamp: timestamp.clone(),
386 data: EventData::Lifecycle(data),
387 stopped: false,
388 stopped_immediate: false,
389 prevented_default: false,
390 }
391}
392
393fn has_mount_callback(node: &NodeData) -> bool {
395 node.get_callbacks().iter().any(|cb| {
396 matches!(
397 cb.event,
398 EventFilter::Component(ComponentEventFilter::AfterMount)
399 )
400 })
401}
402
403fn has_unmount_callback(node: &NodeData) -> bool {
405 node.get_callbacks().iter().any(|cb| {
406 matches!(
407 cb.event,
408 EventFilter::Component(ComponentEventFilter::BeforeUnmount)
409 )
410 })
411}
412
413fn has_resize_callback(node: &NodeData) -> bool {
415 node.get_callbacks().iter().any(|cb| {
416 matches!(
417 cb.event,
418 EventFilter::Component(ComponentEventFilter::NodeResized)
419 )
420 })
421}
422
423fn has_update_callback(node: &NodeData) -> bool {
425 node.get_callbacks().iter().any(|cb| {
428 matches!(
429 cb.event,
430 EventFilter::Component(ComponentEventFilter::Selected)
431 )
432 })
433}
434
435pub fn create_migration_map(node_moves: &[NodeMove]) -> FastHashMap<NodeId, NodeId> {
456 let mut map = FastHashMap::default();
457 for m in node_moves {
458 map.insert(m.old_node_id, m.new_node_id);
459 }
460 map
461}
462
463pub fn transfer_states(
486 old_node_data: &mut [NodeData],
487 new_node_data: &mut [NodeData],
488 node_moves: &[NodeMove],
489) {
490 use crate::refany::OptionRefAny;
491
492 for movement in node_moves {
493 let old_idx = movement.old_node_id.index();
494 let new_idx = movement.new_node_id.index();
495
496 if old_idx >= old_node_data.len() || new_idx >= new_node_data.len() {
498 continue;
499 }
500
501 let merge_callback = match new_node_data[new_idx].get_merge_callback() {
503 Some(cb) => cb,
504 None => continue, };
506
507 let old_dataset = core::mem::replace(
510 &mut old_node_data[old_idx].dataset,
511 OptionRefAny::None
512 );
513 let new_dataset = core::mem::replace(
514 &mut new_node_data[new_idx].dataset,
515 OptionRefAny::None
516 );
517
518 match (new_dataset, old_dataset) {
519 (OptionRefAny::Some(new_data), OptionRefAny::Some(old_data)) => {
520 let merged = (merge_callback.cb)(new_data, old_data);
523
524 new_node_data[new_idx].dataset = OptionRefAny::Some(merged);
526 }
527 (new_ds, old_ds) => {
528 new_node_data[new_idx].dataset = new_ds;
530 old_node_data[old_idx].dataset = old_ds;
531 }
532 }
533 }
534}
535
536pub fn calculate_contenteditable_key(
555 node_data: &[NodeData],
556 hierarchy: &[crate::styled_dom::NodeHierarchyItem],
557 node_id: NodeId,
558) -> u64 {
559 use highway::{HighwayHash, HighwayHasher, Key};
560 use crate::dom::IdOrClass;
561
562 let node = &node_data[node_id.index()];
563
564 if let Some(explicit_key) = node.get_key() {
566 return explicit_key;
567 }
568
569 for id_or_class in node.get_ids_and_classes().as_ref().iter() {
571 if let IdOrClass::Id(id) = id_or_class {
572 let mut hasher = HighwayHasher::new(Key([1; 4])); hasher.append(id.as_str().as_bytes());
574 return hasher.finalize64();
575 }
576 }
577
578 let mut hasher = HighwayHasher::new(Key([2; 4])); let parent_key = if let Some(parent_id) = hierarchy.get(node_id.index()).and_then(|h| h.parent_id()) {
583 calculate_contenteditable_key(node_data, hierarchy, parent_id)
584 } else {
585 0u64 };
587 hasher.append(&parent_key.to_le_bytes());
588
589 let node_discriminant = core::mem::discriminant(node.get_node_type());
592 let nth_of_type = if let Some(parent_id) = hierarchy.get(node_id.index()).and_then(|h| h.parent_id()) {
593 let mut count = 0u32;
595 let mut sibling_id = hierarchy.get(parent_id.index()).and_then(|h| h.first_child_id(parent_id));
596 while let Some(sib_id) = sibling_id {
597 if sib_id == node_id {
598 break;
599 }
600 let sibling_discriminant = core::mem::discriminant(node_data[sib_id.index()].get_node_type());
601 if sibling_discriminant == node_discriminant {
602 count += 1;
603 }
604 sibling_id = hierarchy.get(sib_id.index()).and_then(|h| h.next_sibling_id());
605 }
606 count
607 } else {
608 0
609 };
610
611 hasher.append(&nth_of_type.to_le_bytes());
612
613 #[cfg(feature = "std")]
616 {
617 let type_str = format!("{:?}", node_discriminant);
618 hasher.append(type_str.as_bytes());
619 }
620 #[cfg(not(feature = "std"))]
621 {
622 let discriminant_bytes: [u8; core::mem::size_of::<core::mem::Discriminant<crate::dom::NodeType>>()] =
625 unsafe { core::mem::transmute(node_discriminant) };
626 hasher.append(&discriminant_bytes);
627 }
628
629 for id_or_class in node.get_ids_and_classes().as_ref().iter() {
631 if let IdOrClass::Class(class) = id_or_class {
632 hasher.append(class.as_str().as_bytes());
633 }
634 }
635
636 hasher.finalize64()
637}
638
639pub fn reconcile_cursor_position(
665 old_text: &str,
666 new_text: &str,
667 old_cursor_byte: usize,
668) -> usize {
669 if old_text == new_text {
671 return old_cursor_byte;
672 }
673
674 if old_text.is_empty() {
676 return new_text.len();
677 }
678
679 if new_text.is_empty() {
681 return 0;
682 }
683
684 let common_prefix_bytes = old_text
686 .bytes()
687 .zip(new_text.bytes())
688 .take_while(|(a, b)| a == b)
689 .count();
690
691 if old_cursor_byte <= common_prefix_bytes {
693 return old_cursor_byte.min(new_text.len());
694 }
695
696 let common_suffix_bytes = old_text
698 .bytes()
699 .rev()
700 .zip(new_text.bytes().rev())
701 .take_while(|(a, b)| a == b)
702 .count();
703
704 let old_suffix_start = old_text.len().saturating_sub(common_suffix_bytes);
706 let new_suffix_start = new_text.len().saturating_sub(common_suffix_bytes);
707
708 if old_cursor_byte >= old_suffix_start {
710 let offset_from_end = old_text.len() - old_cursor_byte;
711 return new_text.len().saturating_sub(offset_from_end);
712 }
713
714 new_suffix_start
717}
718
719pub fn get_node_text_content(node: &NodeData) -> Option<&str> {
723 if let crate::dom::NodeType::Text(ref text) = node.get_node_type() {
724 Some(text.as_str())
725 } else {
726 None
727 }
728}
729
730#[cfg(test)]
731mod tests {
732 use super::*;
733 use crate::dom::NodeData;
734
735 #[test]
736 fn test_simple_mount() {
737 let old_data: Vec<NodeData> = vec![];
738 let new_data = vec![NodeData::create_div()];
739
740 let old_layout = FastHashMap::default();
741 let mut new_layout = FastHashMap::default();
742 new_layout.insert(NodeId::new(0), LogicalRect::zero());
743
744 let result = reconcile_dom(
745 &old_data,
746 &new_data,
747 &old_layout,
748 &new_layout,
749 DomId { inner: 0 },
750 Instant::now(),
751 );
752
753 assert!(result.events.is_empty());
755 assert!(result.node_moves.is_empty());
756 }
757
758 #[test]
759 fn test_identical_nodes_match() {
760 let div = NodeData::create_div();
761 let old_data = vec![div.clone()];
762 let new_data = vec![div.clone()];
763
764 let mut old_layout = FastHashMap::default();
765 old_layout.insert(NodeId::new(0), LogicalRect::zero());
766 let mut new_layout = FastHashMap::default();
767 new_layout.insert(NodeId::new(0), LogicalRect::zero());
768
769 let result = reconcile_dom(
770 &old_data,
771 &new_data,
772 &old_layout,
773 &new_layout,
774 DomId { inner: 0 },
775 Instant::now(),
776 );
777
778 assert!(result.events.is_empty());
780 assert_eq!(result.node_moves.len(), 1);
781 assert_eq!(result.node_moves[0].old_node_id, NodeId::new(0));
782 assert_eq!(result.node_moves[0].new_node_id, NodeId::new(0));
783 }
784
785 #[test]
786 fn test_reorder_by_hash() {
787 use azul_css::AzString;
788
789 let mut div_a = NodeData::create_div();
790 div_a.add_class(AzString::from("a"));
791 let mut div_b = NodeData::create_div();
792 div_b.add_class(AzString::from("b"));
793
794 let old_data = vec![div_a.clone(), div_b.clone()];
796 let new_data = vec![div_b.clone(), div_a.clone()];
797
798 let mut old_layout = FastHashMap::default();
799 old_layout.insert(NodeId::new(0), LogicalRect::zero());
800 old_layout.insert(NodeId::new(1), LogicalRect::zero());
801
802 let mut new_layout = FastHashMap::default();
803 new_layout.insert(NodeId::new(0), LogicalRect::zero());
804 new_layout.insert(NodeId::new(1), LogicalRect::zero());
805
806 let result = reconcile_dom(
807 &old_data,
808 &new_data,
809 &old_layout,
810 &new_layout,
811 DomId { inner: 0 },
812 Instant::now(),
813 );
814
815 assert!(result.events.is_empty());
817 assert_eq!(result.node_moves.len(), 2);
818
819 assert!(result.node_moves.iter().any(|m|
821 m.old_node_id == NodeId::new(1) && m.new_node_id == NodeId::new(0)
822 ));
823 assert!(result.node_moves.iter().any(|m|
825 m.old_node_id == NodeId::new(0) && m.new_node_id == NodeId::new(1)
826 ));
827 }
828
829 #[test]
832 fn test_empty_to_empty() {
833 let old_data: Vec<NodeData> = vec![];
834 let new_data: Vec<NodeData> = vec![];
835
836 let result = reconcile_dom(
837 &old_data,
838 &new_data,
839 &FastHashMap::default(),
840 &FastHashMap::default(),
841 DomId { inner: 0 },
842 Instant::now(),
843 );
844
845 assert!(result.events.is_empty());
846 assert!(result.node_moves.is_empty());
847 }
848
849 #[test]
850 fn test_all_nodes_removed() {
851 let old_data = vec![
852 NodeData::create_div(),
853 NodeData::create_div(),
854 NodeData::create_div(),
855 ];
856 let new_data: Vec<NodeData> = vec![];
857
858 let mut old_layout = FastHashMap::default();
859 for i in 0..3 {
860 old_layout.insert(NodeId::new(i), LogicalRect::zero());
861 }
862
863 let result = reconcile_dom(
864 &old_data,
865 &new_data,
866 &old_layout,
867 &FastHashMap::default(),
868 DomId { inner: 0 },
869 Instant::now(),
870 );
871
872 assert!(result.events.is_empty());
874 assert!(result.node_moves.is_empty());
875 }
876
877 #[test]
878 fn test_all_nodes_added() {
879 let old_data: Vec<NodeData> = vec![];
880 let new_data = vec![
881 NodeData::create_div(),
882 NodeData::create_div(),
883 NodeData::create_div(),
884 ];
885
886 let mut new_layout = FastHashMap::default();
887 for i in 0..3 {
888 new_layout.insert(NodeId::new(i), LogicalRect::zero());
889 }
890
891 let result = reconcile_dom(
892 &old_data,
893 &new_data,
894 &FastHashMap::default(),
895 &new_layout,
896 DomId { inner: 0 },
897 Instant::now(),
898 );
899
900 assert!(result.events.is_empty());
902 assert!(result.node_moves.is_empty());
903 }
904
905 #[test]
906 fn test_keyed_node_match() {
907 let mut old_node = NodeData::create_div();
909 old_node.set_key("my-key");
910
911 let mut new_node = NodeData::create_div();
912 new_node.set_key("my-key");
913 new_node.add_class(azul_css::AzString::from("updated"));
914
915 let old_data = vec![old_node];
916 let new_data = vec![new_node];
917
918 let mut old_layout = FastHashMap::default();
919 old_layout.insert(NodeId::new(0), LogicalRect::zero());
920 let mut new_layout = FastHashMap::default();
921 new_layout.insert(NodeId::new(0), LogicalRect::zero());
922
923 let result = reconcile_dom(
924 &old_data,
925 &new_data,
926 &old_layout,
927 &new_layout,
928 DomId { inner: 0 },
929 Instant::now(),
930 );
931
932 assert_eq!(result.node_moves.len(), 1);
934 assert_eq!(result.node_moves[0].old_node_id, NodeId::new(0));
935 assert_eq!(result.node_moves[0].new_node_id, NodeId::new(0));
936 }
937
938 #[test]
939 fn test_keyed_reorder() {
940 let mut node_a = NodeData::create_div();
941 node_a.set_key("key-a");
942 let mut node_b = NodeData::create_div();
943 node_b.set_key("key-b");
944 let mut node_c = NodeData::create_div();
945 node_c.set_key("key-c");
946
947 let old_data = vec![node_a.clone(), node_b.clone(), node_c.clone()];
949 let new_data = vec![node_c.clone(), node_b.clone(), node_a.clone()];
950
951 let mut old_layout = FastHashMap::default();
952 let mut new_layout = FastHashMap::default();
953 for i in 0..3 {
954 old_layout.insert(NodeId::new(i), LogicalRect::zero());
955 new_layout.insert(NodeId::new(i), LogicalRect::zero());
956 }
957
958 let result = reconcile_dom(
959 &old_data,
960 &new_data,
961 &old_layout,
962 &new_layout,
963 DomId { inner: 0 },
964 Instant::now(),
965 );
966
967 assert_eq!(result.node_moves.len(), 3);
968
969 assert!(result.node_moves.iter().any(|m|
971 m.old_node_id == NodeId::new(2) && m.new_node_id == NodeId::new(0)
972 ));
973 assert!(result.node_moves.iter().any(|m|
975 m.old_node_id == NodeId::new(1) && m.new_node_id == NodeId::new(1)
976 ));
977 assert!(result.node_moves.iter().any(|m|
979 m.old_node_id == NodeId::new(0) && m.new_node_id == NodeId::new(2)
980 ));
981 }
982
983 #[test]
984 fn test_identical_nodes_fifo() {
985 let div = NodeData::create_div();
987 let old_data = vec![div.clone(), div.clone(), div.clone()];
988 let new_data = vec![div.clone(), div.clone()]; let mut old_layout = FastHashMap::default();
991 let mut new_layout = FastHashMap::default();
992 for i in 0..3 {
993 old_layout.insert(NodeId::new(i), LogicalRect::zero());
994 }
995 for i in 0..2 {
996 new_layout.insert(NodeId::new(i), LogicalRect::zero());
997 }
998
999 let result = reconcile_dom(
1000 &old_data,
1001 &new_data,
1002 &old_layout,
1003 &new_layout,
1004 DomId { inner: 0 },
1005 Instant::now(),
1006 );
1007
1008 assert_eq!(result.node_moves.len(), 2);
1010 assert!(result.node_moves.iter().any(|m|
1011 m.old_node_id == NodeId::new(0) && m.new_node_id == NodeId::new(0)
1012 ));
1013 assert!(result.node_moves.iter().any(|m|
1014 m.old_node_id == NodeId::new(1) && m.new_node_id == NodeId::new(1)
1015 ));
1016 }
1017
1018 #[test]
1019 fn test_insert_at_beginning() {
1020 use azul_css::AzString;
1021
1022 let mut div_a = NodeData::create_div();
1023 div_a.add_class(AzString::from("a"));
1024 let mut div_b = NodeData::create_div();
1025 div_b.add_class(AzString::from("b"));
1026 let mut div_new = NodeData::create_div();
1027 div_new.add_class(AzString::from("new"));
1028
1029 let old_data = vec![div_a.clone(), div_b.clone()];
1031 let new_data = vec![div_new.clone(), div_a.clone(), div_b.clone()];
1032
1033 let mut old_layout = FastHashMap::default();
1034 let mut new_layout = FastHashMap::default();
1035 for i in 0..2 {
1036 old_layout.insert(NodeId::new(i), LogicalRect::zero());
1037 }
1038 for i in 0..3 {
1039 new_layout.insert(NodeId::new(i), LogicalRect::zero());
1040 }
1041
1042 let result = reconcile_dom(
1043 &old_data,
1044 &new_data,
1045 &old_layout,
1046 &new_layout,
1047 DomId { inner: 0 },
1048 Instant::now(),
1049 );
1050
1051 assert_eq!(result.node_moves.len(), 2);
1053
1054 assert!(result.node_moves.iter().any(|m|
1056 m.old_node_id == NodeId::new(0) && m.new_node_id == NodeId::new(1)
1057 ));
1058 assert!(result.node_moves.iter().any(|m|
1060 m.old_node_id == NodeId::new(1) && m.new_node_id == NodeId::new(2)
1061 ));
1062 }
1063
1064 #[test]
1065 fn test_insert_in_middle() {
1066 use azul_css::AzString;
1067
1068 let mut div_a = NodeData::create_div();
1069 div_a.add_class(AzString::from("a"));
1070 let mut div_b = NodeData::create_div();
1071 div_b.add_class(AzString::from("b"));
1072 let mut div_new = NodeData::create_div();
1073 div_new.add_class(AzString::from("new"));
1074
1075 let old_data = vec![div_a.clone(), div_b.clone()];
1077 let new_data = vec![div_a.clone(), div_new.clone(), div_b.clone()];
1078
1079 let mut old_layout = FastHashMap::default();
1080 let mut new_layout = FastHashMap::default();
1081 for i in 0..2 {
1082 old_layout.insert(NodeId::new(i), LogicalRect::zero());
1083 }
1084 for i in 0..3 {
1085 new_layout.insert(NodeId::new(i), LogicalRect::zero());
1086 }
1087
1088 let result = reconcile_dom(
1089 &old_data,
1090 &new_data,
1091 &old_layout,
1092 &new_layout,
1093 DomId { inner: 0 },
1094 Instant::now(),
1095 );
1096
1097 assert_eq!(result.node_moves.len(), 2);
1098
1099 assert!(result.node_moves.iter().any(|m|
1101 m.old_node_id == NodeId::new(0) && m.new_node_id == NodeId::new(0)
1102 ));
1103 assert!(result.node_moves.iter().any(|m|
1105 m.old_node_id == NodeId::new(1) && m.new_node_id == NodeId::new(2)
1106 ));
1107 }
1108
1109 #[test]
1110 fn test_remove_from_middle() {
1111 use azul_css::AzString;
1112
1113 let mut div_a = NodeData::create_div();
1114 div_a.add_class(AzString::from("a"));
1115 let mut div_b = NodeData::create_div();
1116 div_b.add_class(AzString::from("b"));
1117 let mut div_c = NodeData::create_div();
1118 div_c.add_class(AzString::from("c"));
1119
1120 let old_data = vec![div_a.clone(), div_b.clone(), div_c.clone()];
1122 let new_data = vec![div_a.clone(), div_c.clone()];
1123
1124 let mut old_layout = FastHashMap::default();
1125 let mut new_layout = FastHashMap::default();
1126 for i in 0..3 {
1127 old_layout.insert(NodeId::new(i), LogicalRect::zero());
1128 }
1129 for i in 0..2 {
1130 new_layout.insert(NodeId::new(i), LogicalRect::zero());
1131 }
1132
1133 let result = reconcile_dom(
1134 &old_data,
1135 &new_data,
1136 &old_layout,
1137 &new_layout,
1138 DomId { inner: 0 },
1139 Instant::now(),
1140 );
1141
1142 assert_eq!(result.node_moves.len(), 2);
1144
1145 assert!(result.node_moves.iter().any(|m|
1147 m.old_node_id == NodeId::new(0) && m.new_node_id == NodeId::new(0)
1148 ));
1149 assert!(result.node_moves.iter().any(|m|
1151 m.old_node_id == NodeId::new(2) && m.new_node_id == NodeId::new(1)
1152 ));
1153 }
1154
1155 #[test]
1156 fn test_mixed_keyed_and_unkeyed() {
1157 use azul_css::AzString;
1158
1159 let mut keyed = NodeData::create_div();
1160 keyed.set_key("my-key");
1161 keyed.add_class(AzString::from("keyed"));
1162
1163 let mut unkeyed = NodeData::create_div();
1164 unkeyed.add_class(AzString::from("unkeyed"));
1165
1166 let old_data = vec![keyed.clone(), unkeyed.clone()];
1168 let new_data = vec![unkeyed.clone(), keyed.clone()];
1169
1170 let mut old_layout = FastHashMap::default();
1171 let mut new_layout = FastHashMap::default();
1172 for i in 0..2 {
1173 old_layout.insert(NodeId::new(i), LogicalRect::zero());
1174 new_layout.insert(NodeId::new(i), LogicalRect::zero());
1175 }
1176
1177 let result = reconcile_dom(
1178 &old_data,
1179 &new_data,
1180 &old_layout,
1181 &new_layout,
1182 DomId { inner: 0 },
1183 Instant::now(),
1184 );
1185
1186 assert_eq!(result.node_moves.len(), 2);
1187
1188 assert!(result.node_moves.iter().any(|m|
1190 m.old_node_id == NodeId::new(0) && m.new_node_id == NodeId::new(1)
1191 ));
1192 assert!(result.node_moves.iter().any(|m|
1194 m.old_node_id == NodeId::new(1) && m.new_node_id == NodeId::new(0)
1195 ));
1196 }
1197
1198 #[test]
1199 fn test_duplicate_keys() {
1200 let mut node1 = NodeData::create_div();
1202 node1.set_key("duplicate");
1203 node1.add_class(azul_css::AzString::from("first"));
1204
1205 let mut node2 = NodeData::create_div();
1206 node2.set_key("duplicate");
1207 node2.add_class(azul_css::AzString::from("second"));
1208
1209 let old_data = vec![node1.clone()];
1210 let new_data = vec![node2.clone()];
1211
1212 let mut old_layout = FastHashMap::default();
1213 old_layout.insert(NodeId::new(0), LogicalRect::zero());
1214 let mut new_layout = FastHashMap::default();
1215 new_layout.insert(NodeId::new(0), LogicalRect::zero());
1216
1217 let result = reconcile_dom(
1218 &old_data,
1219 &new_data,
1220 &old_layout,
1221 &new_layout,
1222 DomId { inner: 0 },
1223 Instant::now(),
1224 );
1225
1226 assert_eq!(result.node_moves.len(), 1);
1228 }
1229
1230 #[test]
1231 fn test_key_not_in_old() {
1232 let old_div = NodeData::create_div();
1234
1235 let mut new_div = NodeData::create_div();
1236 new_div.set_key("new-key");
1237
1238 let old_data = vec![old_div];
1239 let new_data = vec![new_div];
1240
1241 let mut old_layout = FastHashMap::default();
1242 old_layout.insert(NodeId::new(0), LogicalRect::zero());
1243 let mut new_layout = FastHashMap::default();
1244 new_layout.insert(NodeId::new(0), LogicalRect::zero());
1245
1246 let result = reconcile_dom(
1247 &old_data,
1248 &new_data,
1249 &old_layout,
1250 &new_layout,
1251 DomId { inner: 0 },
1252 Instant::now(),
1253 );
1254
1255 assert!(result.node_moves.is_empty()); }
1259
1260 #[test]
1261 fn test_large_list_reorder() {
1262 use azul_css::AzString;
1263
1264 let nodes: Vec<NodeData> = (0..100).map(|i| {
1266 let mut node = NodeData::create_div();
1267 node.add_class(AzString::from(format!("item-{}", i)));
1268 node
1269 }).collect();
1270
1271 let old_data = nodes.clone();
1273 let new_data: Vec<NodeData> = nodes.into_iter().rev().collect();
1274
1275 let mut old_layout = FastHashMap::default();
1276 let mut new_layout = FastHashMap::default();
1277 for i in 0..100 {
1278 old_layout.insert(NodeId::new(i), LogicalRect::zero());
1279 new_layout.insert(NodeId::new(i), LogicalRect::zero());
1280 }
1281
1282 let result = reconcile_dom(
1283 &old_data,
1284 &new_data,
1285 &old_layout,
1286 &new_layout,
1287 DomId { inner: 0 },
1288 Instant::now(),
1289 );
1290
1291 assert_eq!(result.node_moves.len(), 100);
1293 assert!(result.events.is_empty());
1294 }
1295
1296 #[test]
1297 fn test_migration_map() {
1298 use azul_css::AzString;
1299
1300 let mut div_a = NodeData::create_div();
1301 div_a.add_class(AzString::from("a"));
1302 let mut div_b = NodeData::create_div();
1303 div_b.add_class(AzString::from("b"));
1304
1305 let old_data = vec![div_a.clone(), div_b.clone()];
1306 let new_data = vec![div_b.clone(), div_a.clone()];
1307
1308 let mut old_layout = FastHashMap::default();
1309 let mut new_layout = FastHashMap::default();
1310 for i in 0..2 {
1311 old_layout.insert(NodeId::new(i), LogicalRect::zero());
1312 new_layout.insert(NodeId::new(i), LogicalRect::zero());
1313 }
1314
1315 let result = reconcile_dom(
1316 &old_data,
1317 &new_data,
1318 &old_layout,
1319 &new_layout,
1320 DomId { inner: 0 },
1321 Instant::now(),
1322 );
1323
1324 let migration = create_migration_map(&result.node_moves);
1325
1326 assert_eq!(migration.get(&NodeId::new(0)), Some(&NodeId::new(1)));
1328 assert_eq!(migration.get(&NodeId::new(1)), Some(&NodeId::new(0)));
1330 }
1331
1332 #[test]
1333 fn test_different_node_types() {
1334 let div = NodeData::create_div();
1336 let span = NodeData::create_node(crate::dom::NodeType::Span);
1337
1338 let old_data = vec![div];
1339 let new_data = vec![span];
1340
1341 let mut old_layout = FastHashMap::default();
1342 old_layout.insert(NodeId::new(0), LogicalRect::zero());
1343 let mut new_layout = FastHashMap::default();
1344 new_layout.insert(NodeId::new(0), LogicalRect::zero());
1345
1346 let result = reconcile_dom(
1347 &old_data,
1348 &new_data,
1349 &old_layout,
1350 &new_layout,
1351 DomId { inner: 0 },
1352 Instant::now(),
1353 );
1354
1355 assert!(result.node_moves.is_empty());
1357 }
1358
1359 #[test]
1360 fn test_text_nodes() {
1361 use azul_css::AzString;
1362
1363 let text_a = NodeData::create_text(AzString::from("Hello"));
1364 let text_b = NodeData::create_text(AzString::from("World"));
1365 let text_a_copy = NodeData::create_text(AzString::from("Hello"));
1366
1367 let old_data = vec![text_a.clone(), text_b.clone()];
1369 let new_data = vec![text_b.clone(), text_a_copy.clone()];
1370
1371 let mut old_layout = FastHashMap::default();
1372 let mut new_layout = FastHashMap::default();
1373 for i in 0..2 {
1374 old_layout.insert(NodeId::new(i), LogicalRect::zero());
1375 new_layout.insert(NodeId::new(i), LogicalRect::zero());
1376 }
1377
1378 let result = reconcile_dom(
1379 &old_data,
1380 &new_data,
1381 &old_layout,
1382 &new_layout,
1383 DomId { inner: 0 },
1384 Instant::now(),
1385 );
1386
1387 assert_eq!(result.node_moves.len(), 2);
1389 }
1390
1391 #[test]
1392 fn test_shuffle_three() {
1393 use azul_css::AzString;
1394
1395 let mut a = NodeData::create_div();
1396 a.add_class(AzString::from("a"));
1397 let mut b = NodeData::create_div();
1398 b.add_class(AzString::from("b"));
1399 let mut c = NodeData::create_div();
1400 c.add_class(AzString::from("c"));
1401
1402 let old_data = vec![a.clone(), b.clone(), c.clone()];
1404 let new_data = vec![b.clone(), c.clone(), a.clone()];
1405
1406 let mut old_layout = FastHashMap::default();
1407 let mut new_layout = FastHashMap::default();
1408 for i in 0..3 {
1409 old_layout.insert(NodeId::new(i), LogicalRect::zero());
1410 new_layout.insert(NodeId::new(i), LogicalRect::zero());
1411 }
1412
1413 let result = reconcile_dom(
1414 &old_data,
1415 &new_data,
1416 &old_layout,
1417 &new_layout,
1418 DomId { inner: 0 },
1419 Instant::now(),
1420 );
1421
1422 assert_eq!(result.node_moves.len(), 3);
1423
1424 assert!(result.node_moves.iter().any(|m|
1426 m.old_node_id == NodeId::new(0) && m.new_node_id == NodeId::new(2)
1427 ));
1428 assert!(result.node_moves.iter().any(|m|
1430 m.old_node_id == NodeId::new(1) && m.new_node_id == NodeId::new(0)
1431 ));
1432 assert!(result.node_moves.iter().any(|m|
1434 m.old_node_id == NodeId::new(2) && m.new_node_id == NodeId::new(1)
1435 ));
1436 }
1437
1438 use crate::refany::{RefAny, OptionRefAny};
1443 use crate::dom::DatasetMergeCallbackType;
1444 use alloc::sync::Arc;
1445 use core::cell::RefCell;
1446
1447 struct VideoPlayerState {
1449 url: alloc::string::String,
1450 decoder_handle: Option<u64>, }
1452
1453 extern "C" fn merge_video_state(mut new_data: RefAny, mut old_data: RefAny) -> RefAny {
1455 if let Some(mut new_guard) = new_data.downcast_mut::<VideoPlayerState>() {
1457 if let Some(old_guard) = old_data.downcast_ref::<VideoPlayerState>() {
1458 new_guard.decoder_handle = old_guard.decoder_handle;
1460 }
1461 }
1462 new_data
1463 }
1464
1465 #[test]
1466 fn test_transfer_states_basic() {
1467 let old_state = VideoPlayerState {
1471 url: "movie.mp4".into(),
1472 decoder_handle: Some(12345),
1473 };
1474 let new_state = VideoPlayerState {
1475 url: "movie.mp4".into(),
1476 decoder_handle: None, };
1478
1479 let mut old_node = NodeData::create_div();
1480 old_node.dataset = OptionRefAny::Some(RefAny::new(old_state));
1481
1482 let mut new_node = NodeData::create_div();
1483 new_node.dataset = OptionRefAny::Some(RefAny::new(new_state));
1484 new_node.set_merge_callback(merge_video_state as DatasetMergeCallbackType);
1485
1486 let mut old_data = vec![old_node];
1487 let mut new_data = vec![new_node];
1488
1489 let moves = vec![NodeMove {
1490 old_node_id: NodeId::new(0),
1491 new_node_id: NodeId::new(0),
1492 }];
1493
1494 transfer_states(&mut old_data, &mut new_data, &moves);
1496
1497 if let OptionRefAny::Some(ref mut dataset) = new_data[0].dataset {
1499 let guard = dataset.downcast_ref::<VideoPlayerState>().unwrap();
1500 assert_eq!(guard.decoder_handle, Some(12345));
1501 } else {
1502 panic!("Dataset should exist");
1503 }
1504 }
1505
1506 #[test]
1507 fn test_transfer_states_no_callback_no_transfer() {
1508 let old_state = VideoPlayerState {
1511 url: "movie.mp4".into(),
1512 decoder_handle: Some(99999),
1513 };
1514 let new_state = VideoPlayerState {
1515 url: "movie.mp4".into(),
1516 decoder_handle: None,
1517 };
1518
1519 let mut old_node = NodeData::create_div();
1520 old_node.dataset = OptionRefAny::Some(RefAny::new(old_state));
1521
1522 let mut new_node = NodeData::create_div();
1523 new_node.dataset = OptionRefAny::Some(RefAny::new(new_state));
1524 let mut old_data = vec![old_node];
1527 let mut new_data = vec![new_node];
1528
1529 let moves = vec![NodeMove {
1530 old_node_id: NodeId::new(0),
1531 new_node_id: NodeId::new(0),
1532 }];
1533
1534 transfer_states(&mut old_data, &mut new_data, &moves);
1535
1536 if let OptionRefAny::Some(ref mut dataset) = new_data[0].dataset {
1538 let guard = dataset.downcast_ref::<VideoPlayerState>().unwrap();
1539 assert_eq!(guard.decoder_handle, None); } else {
1541 panic!("Dataset should exist");
1542 }
1543 }
1544
1545 #[test]
1546 fn test_transfer_states_no_old_dataset() {
1547 let new_state = VideoPlayerState {
1550 url: "movie.mp4".into(),
1551 decoder_handle: None,
1552 };
1553
1554 let old_node = NodeData::create_div(); let mut new_node = NodeData::create_div();
1557 new_node.dataset = OptionRefAny::Some(RefAny::new(new_state));
1558 new_node.set_merge_callback(merge_video_state as DatasetMergeCallbackType);
1559
1560 let mut old_data = vec![old_node];
1561 let mut new_data = vec![new_node];
1562
1563 let moves = vec![NodeMove {
1564 old_node_id: NodeId::new(0),
1565 new_node_id: NodeId::new(0),
1566 }];
1567
1568 transfer_states(&mut old_data, &mut new_data, &moves);
1570
1571 if let OptionRefAny::Some(ref mut dataset) = new_data[0].dataset {
1573 let guard = dataset.downcast_ref::<VideoPlayerState>().unwrap();
1574 assert_eq!(guard.decoder_handle, None);
1575 } else {
1576 panic!("Dataset should still exist");
1577 }
1578 }
1579
1580 #[test]
1581 fn test_transfer_states_no_new_dataset() {
1582 let old_state = VideoPlayerState {
1585 url: "movie.mp4".into(),
1586 decoder_handle: Some(77777),
1587 };
1588
1589 let mut old_node = NodeData::create_div();
1590 old_node.dataset = OptionRefAny::Some(RefAny::new(old_state));
1591
1592 let mut new_node = NodeData::create_div();
1593 new_node.set_merge_callback(merge_video_state as DatasetMergeCallbackType);
1595
1596 let mut old_data = vec![old_node];
1597 let mut new_data = vec![new_node];
1598
1599 let moves = vec![NodeMove {
1600 old_node_id: NodeId::new(0),
1601 new_node_id: NodeId::new(0),
1602 }];
1603
1604 transfer_states(&mut old_data, &mut new_data, &moves);
1606
1607 assert!(matches!(new_data[0].dataset, OptionRefAny::None));
1609 }
1610
1611 #[test]
1612 fn test_transfer_states_reorder_preserves_handles() {
1613 let state_a = VideoPlayerState { url: "a.mp4".into(), decoder_handle: Some(111) };
1617 let state_b = VideoPlayerState { url: "b.mp4".into(), decoder_handle: Some(222) };
1618
1619 let mut old_a = NodeData::create_div();
1620 old_a.set_key("player-a");
1621 old_a.dataset = OptionRefAny::Some(RefAny::new(state_a));
1622
1623 let mut old_b = NodeData::create_div();
1624 old_b.set_key("player-b");
1625 old_b.dataset = OptionRefAny::Some(RefAny::new(state_b));
1626
1627 let new_state_b = VideoPlayerState { url: "b.mp4".into(), decoder_handle: None };
1629 let new_state_a = VideoPlayerState { url: "a.mp4".into(), decoder_handle: None };
1630
1631 let mut new_b = NodeData::create_div();
1632 new_b.set_key("player-b");
1633 new_b.dataset = OptionRefAny::Some(RefAny::new(new_state_b));
1634 new_b.set_merge_callback(merge_video_state as DatasetMergeCallbackType);
1635
1636 let mut new_a = NodeData::create_div();
1637 new_a.set_key("player-a");
1638 new_a.dataset = OptionRefAny::Some(RefAny::new(new_state_a));
1639 new_a.set_merge_callback(merge_video_state as DatasetMergeCallbackType);
1640
1641 let mut old_data = vec![old_a, old_b]; let mut new_data = vec![new_b, new_a]; let moves = vec![
1646 NodeMove { old_node_id: NodeId::new(0), new_node_id: NodeId::new(1) }, NodeMove { old_node_id: NodeId::new(1), new_node_id: NodeId::new(0) }, ];
1649
1650 transfer_states(&mut old_data, &mut new_data, &moves);
1651
1652 if let OptionRefAny::Some(ref mut ds) = new_data[0].dataset {
1654 let guard = ds.downcast_ref::<VideoPlayerState>().unwrap();
1655 assert_eq!(guard.url, "b.mp4");
1656 assert_eq!(guard.decoder_handle, Some(222));
1657 } else {
1658 panic!("B dataset missing");
1659 }
1660
1661 if let OptionRefAny::Some(ref mut ds) = new_data[1].dataset {
1663 let guard = ds.downcast_ref::<VideoPlayerState>().unwrap();
1664 assert_eq!(guard.url, "a.mp4");
1665 assert_eq!(guard.decoder_handle, Some(111));
1666 } else {
1667 panic!("A dataset missing");
1668 }
1669 }
1670
1671 #[test]
1672 fn test_transfer_states_out_of_bounds() {
1673 let state = VideoPlayerState { url: "test.mp4".into(), decoder_handle: Some(123) };
1676
1677 let mut old_node = NodeData::create_div();
1678 old_node.dataset = OptionRefAny::Some(RefAny::new(state));
1679
1680 let mut old_data = vec![old_node];
1681 let mut new_data: Vec<NodeData> = vec![]; let moves = vec![NodeMove {
1684 old_node_id: NodeId::new(0),
1685 new_node_id: NodeId::new(999), }];
1687
1688 transfer_states(&mut old_data, &mut new_data, &moves);
1690 }
1691
1692 struct WebGLContext {
1694 texture_ids: alloc::vec::Vec<u32>,
1695 shader_program: Option<u32>,
1696 }
1697
1698 extern "C" fn merge_webgl_context(mut new_data: RefAny, mut old_data: RefAny) -> RefAny {
1699 if let Some(mut new_guard) = new_data.downcast_mut::<WebGLContext>() {
1700 if let Some(old_guard) = old_data.downcast_ref::<WebGLContext>() {
1701 new_guard.texture_ids = old_guard.texture_ids.clone();
1703 new_guard.shader_program = old_guard.shader_program;
1704 }
1705 }
1706 new_data
1707 }
1708
1709 #[test]
1710 fn test_transfer_states_complex_type() {
1711 let old_ctx = WebGLContext {
1712 texture_ids: vec![1, 2, 3, 4, 5],
1713 shader_program: Some(42),
1714 };
1715 let new_ctx = WebGLContext {
1716 texture_ids: vec![],
1717 shader_program: None,
1718 };
1719
1720 let mut old_node = NodeData::create_div();
1721 old_node.dataset = OptionRefAny::Some(RefAny::new(old_ctx));
1722
1723 let mut new_node = NodeData::create_div();
1724 new_node.dataset = OptionRefAny::Some(RefAny::new(new_ctx));
1725 new_node.set_merge_callback(merge_webgl_context as DatasetMergeCallbackType);
1726
1727 let mut old_data = vec![old_node];
1728 let mut new_data = vec![new_node];
1729
1730 let moves = vec![NodeMove {
1731 old_node_id: NodeId::new(0),
1732 new_node_id: NodeId::new(0),
1733 }];
1734
1735 transfer_states(&mut old_data, &mut new_data, &moves);
1736
1737 if let OptionRefAny::Some(ref mut ds) = new_data[0].dataset {
1738 let guard = ds.downcast_ref::<WebGLContext>().unwrap();
1739 assert_eq!(guard.texture_ids, vec![1, 2, 3, 4, 5]);
1740 assert_eq!(guard.shader_program, Some(42));
1741 } else {
1742 panic!("Dataset missing");
1743 }
1744 }
1745
1746 #[test]
1747 fn test_transfer_states_callback_returns_old_data() {
1748 struct Counter {
1751 value: u32,
1752 }
1753
1754 extern "C" fn prefer_old(_new_data: RefAny, old_data: RefAny) -> RefAny {
1755 old_data
1757 }
1758
1759 let old_counter = Counter { value: 100 };
1760 let new_counter = Counter { value: 0 };
1761
1762 let mut old_node = NodeData::create_div();
1763 old_node.dataset = OptionRefAny::Some(RefAny::new(old_counter));
1764
1765 let mut new_node = NodeData::create_div();
1766 new_node.dataset = OptionRefAny::Some(RefAny::new(new_counter));
1767 new_node.set_merge_callback(prefer_old as DatasetMergeCallbackType);
1768
1769 let mut old_data = vec![old_node];
1770 let mut new_data = vec![new_node];
1771
1772 let moves = vec![NodeMove {
1773 old_node_id: NodeId::new(0),
1774 new_node_id: NodeId::new(0),
1775 }];
1776
1777 transfer_states(&mut old_data, &mut new_data, &moves);
1778
1779 if let OptionRefAny::Some(ref mut ds) = new_data[0].dataset {
1781 let guard = ds.downcast_ref::<Counter>().unwrap();
1782 assert_eq!(guard.value, 100);
1783 } else {
1784 panic!("Dataset missing");
1785 }
1786 }
1787
1788 #[test]
1789 fn test_transfer_states_multiple_nodes_partial_callbacks() {
1790 struct Simple { val: u32 }
1793
1794 extern "C" fn merge_simple(mut new_data: RefAny, mut old_data: RefAny) -> RefAny {
1795 if let Some(mut new_g) = new_data.downcast_mut::<Simple>() {
1796 if let Some(old_g) = old_data.downcast_ref::<Simple>() {
1797 new_g.val = old_g.val;
1798 }
1799 }
1800 new_data
1801 }
1802
1803 let mut old_nodes = vec![
1804 {
1805 let mut n = NodeData::create_div();
1806 n.dataset = OptionRefAny::Some(RefAny::new(Simple { val: 1 }));
1807 n
1808 },
1809 {
1810 let mut n = NodeData::create_div();
1811 n.dataset = OptionRefAny::Some(RefAny::new(Simple { val: 2 }));
1812 n
1813 },
1814 {
1815 let mut n = NodeData::create_div();
1816 n.dataset = OptionRefAny::Some(RefAny::new(Simple { val: 3 }));
1817 n
1818 },
1819 ];
1820
1821 let mut new_nodes = vec![
1822 {
1823 let mut n = NodeData::create_div();
1824 n.dataset = OptionRefAny::Some(RefAny::new(Simple { val: 10 }));
1825 n
1827 },
1828 {
1829 let mut n = NodeData::create_div();
1830 n.dataset = OptionRefAny::Some(RefAny::new(Simple { val: 20 }));
1831 n.set_merge_callback(merge_simple as DatasetMergeCallbackType); n
1833 },
1834 {
1835 let mut n = NodeData::create_div();
1836 n.dataset = OptionRefAny::Some(RefAny::new(Simple { val: 30 }));
1837 n
1839 },
1840 ];
1841
1842 let moves = vec![
1843 NodeMove { old_node_id: NodeId::new(0), new_node_id: NodeId::new(0) },
1844 NodeMove { old_node_id: NodeId::new(1), new_node_id: NodeId::new(1) },
1845 NodeMove { old_node_id: NodeId::new(2), new_node_id: NodeId::new(2) },
1846 ];
1847
1848 transfer_states(&mut old_nodes, &mut new_nodes, &moves);
1849
1850 if let OptionRefAny::Some(ref mut ds) = new_nodes[0].dataset {
1852 let g = ds.downcast_ref::<Simple>().unwrap();
1853 assert_eq!(g.val, 10);
1854 }
1855
1856 if let OptionRefAny::Some(ref mut ds) = new_nodes[1].dataset {
1858 let g = ds.downcast_ref::<Simple>().unwrap();
1859 assert_eq!(g.val, 2);
1860 }
1861
1862 if let OptionRefAny::Some(ref mut ds) = new_nodes[2].dataset {
1864 let g = ds.downcast_ref::<Simple>().unwrap();
1865 assert_eq!(g.val, 30);
1866 }
1867 }
1868
1869 #[test]
1870 fn test_transfer_states_empty_moves() {
1871 let mut old_data: Vec<NodeData> = vec![];
1873 let mut new_data: Vec<NodeData> = vec![];
1874 let moves: Vec<NodeMove> = vec![];
1875
1876 transfer_states(&mut old_data, &mut new_data, &moves);
1878 }
1879
1880 #[test]
1881 fn test_reconcile_then_transfer_integration() {
1882 struct AppState {
1885 name: alloc::string::String,
1886 heavy_handle: Option<u64>
1887 }
1888
1889 extern "C" fn merge_app(mut new_data: RefAny, mut old_data: RefAny) -> RefAny {
1890 if let Some(mut new_g) = new_data.downcast_mut::<AppState>() {
1891 if let Some(old_g) = old_data.downcast_ref::<AppState>() {
1892 new_g.heavy_handle = old_g.heavy_handle;
1893 }
1894 }
1895 new_data
1896 }
1897
1898 let old_state = AppState { name: "old".into(), heavy_handle: Some(999) };
1900 let mut old_node = NodeData::create_div();
1901 old_node.set_key("main");
1902 old_node.dataset = OptionRefAny::Some(RefAny::new(old_state));
1903
1904 let new_state = AppState { name: "new".into(), heavy_handle: None };
1906 let mut new_node = NodeData::create_div();
1907 new_node.set_key("main");
1908 new_node.dataset = OptionRefAny::Some(RefAny::new(new_state));
1909 new_node.set_merge_callback(merge_app as DatasetMergeCallbackType);
1910
1911 let mut old_data = vec![old_node];
1912 let mut new_data = vec![new_node];
1913
1914 let mut old_layout = FastHashMap::default();
1915 old_layout.insert(NodeId::new(0), LogicalRect::zero());
1916 let mut new_layout = FastHashMap::default();
1917 new_layout.insert(NodeId::new(0), LogicalRect::zero());
1918
1919 let diff = reconcile_dom(
1921 &old_data,
1922 &new_data,
1923 &old_layout,
1924 &new_layout,
1925 DomId { inner: 0 },
1926 Instant::now(),
1927 );
1928
1929 assert_eq!(diff.node_moves.len(), 1);
1931 assert_eq!(diff.node_moves[0].old_node_id, NodeId::new(0));
1932 assert_eq!(diff.node_moves[0].new_node_id, NodeId::new(0));
1933
1934 transfer_states(&mut old_data, &mut new_data, &diff.node_moves);
1936
1937 if let OptionRefAny::Some(ref mut ds) = new_data[0].dataset {
1939 let g = ds.downcast_ref::<AppState>().unwrap();
1940 assert_eq!(g.name, "new");
1941 assert_eq!(g.heavy_handle, Some(999));
1942 } else {
1943 panic!("Dataset missing");
1944 }
1945 }
1946
1947 #[test]
1950 fn test_cursor_reconcile_identical_text() {
1951 let result = reconcile_cursor_position("Hello", "Hello", 3);
1952 assert_eq!(result, 3);
1953 }
1954
1955 #[test]
1956 fn test_cursor_reconcile_text_appended() {
1957 let result = reconcile_cursor_position("Hello", "Hello World", 5);
1959 assert_eq!(result, 5); }
1961
1962 #[test]
1963 fn test_cursor_reconcile_text_prepended() {
1964 let result = reconcile_cursor_position("Hello", "Say Hello", 1);
1967 assert_eq!(result, 4); }
1969
1970 #[test]
1971 fn test_cursor_reconcile_suffix_preserved() {
1972 let result = reconcile_cursor_position("Hello World", "Hi World", 6);
1975 assert_eq!(result, 3);
1981 }
1982
1983 #[test]
1984 fn test_cursor_reconcile_empty_to_text() {
1985 let result = reconcile_cursor_position("", "Hello", 0);
1986 assert_eq!(result, 5); }
1988
1989 #[test]
1990 fn test_cursor_reconcile_text_to_empty() {
1991 let result = reconcile_cursor_position("Hello", "", 3);
1992 assert_eq!(result, 0); }
1994
1995 #[test]
1996 fn test_cursor_reconcile_insert_at_cursor() {
1997 let result = reconcile_cursor_position("Hello", "HelXlo", 3);
1999 assert_eq!(result, 3);
2002 }
2003
2004 #[test]
2005 fn test_structural_hash_text_nodes_match() {
2006 use azul_css::AzString;
2007
2008 let text_a = NodeData::create_text(AzString::from("Hello"));
2010 let text_b = NodeData::create_text(AzString::from("Hello World"));
2011
2012 assert_ne!(text_a.calculate_node_data_hash(), text_b.calculate_node_data_hash());
2014
2015 assert_eq!(text_a.calculate_structural_hash(), text_b.calculate_structural_hash());
2017 }
2018
2019 #[test]
2020 fn test_structural_hash_different_types() {
2021 use azul_css::AzString;
2022
2023 let div = NodeData::create_div();
2025 let text = NodeData::create_text(AzString::from("Hello"));
2026
2027 assert_ne!(div.calculate_structural_hash(), text.calculate_structural_hash());
2028 }
2029
2030 #[test]
2031 fn test_text_nodes_match_by_structural_hash() {
2032 use azul_css::AzString;
2033
2034 let old_data = vec![NodeData::create_text(AzString::from("Hello"))];
2037 let new_data = vec![NodeData::create_text(AzString::from("Hello World"))];
2038
2039 let mut old_layout = FastHashMap::default();
2040 old_layout.insert(NodeId::new(0), LogicalRect::zero());
2041 let mut new_layout = FastHashMap::default();
2042 new_layout.insert(NodeId::new(0), LogicalRect::zero());
2043
2044 let result = reconcile_dom(
2045 &old_data,
2046 &new_data,
2047 &old_layout,
2048 &new_layout,
2049 DomId { inner: 0 },
2050 Instant::now(),
2051 );
2052
2053 assert_eq!(result.node_moves.len(), 1);
2055 assert_eq!(result.node_moves[0].old_node_id, NodeId::new(0));
2056 assert_eq!(result.node_moves[0].new_node_id, NodeId::new(0));
2057 }
2058}