1use alloc::{collections::BTreeMap, collections::VecDeque, string::String, 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 pub const fn empty() -> Self {
97 Self { bits: 0 }
98 }
99
100 pub const fn is_empty(&self) -> bool {
101 self.bits == 0
102 }
103
104 pub const fn contains(&self, flag: u32) -> bool {
105 (self.bits & flag) == flag
106 }
107
108 pub const fn intersects(&self, mask: u32) -> bool {
109 (self.bits & mask) != 0
110 }
111
112 pub fn insert(&mut self, flag: u32) {
113 self.bits |= flag;
114 }
115
116 pub const fn is_visually_unchanged(&self) -> bool {
118 !self.intersects(Self::AFFECTS_LAYOUT) && !self.intersects(Self::AFFECTS_PAINT)
119 }
120
121 pub const fn needs_layout(&self) -> bool {
123 self.intersects(Self::AFFECTS_LAYOUT)
124 }
125
126 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)]
147pub struct ExtendedDiffResult {
148 pub diff: DiffResult,
150 pub node_changes: Vec<(NodeId, NodeId, NodeChangeSet)>,
154}
155
156impl Default for ExtendedDiffResult {
157 fn default() -> Self {
158 Self {
159 diff: DiffResult::default(),
160 node_changes: Vec::new(),
161 }
162 }
163}
164
165pub fn compute_node_changes(
168 old_node: &NodeData,
169 new_node: &NodeData,
170 old_styled_state: Option<&StyledNodeState>,
171 new_styled_state: Option<&StyledNodeState>,
172) -> NodeChangeSet {
173 let mut changes = NodeChangeSet::empty();
174
175 if core::mem::discriminant(old_node.get_node_type())
177 != core::mem::discriminant(new_node.get_node_type())
178 {
179 changes.insert(NodeChangeSet::NODE_TYPE_CHANGED);
180 return changes; }
182
183 match (old_node.get_node_type(), new_node.get_node_type()) {
185 (NodeType::Text(old_text), NodeType::Text(new_text)) => {
186 if old_text.as_str() != new_text.as_str() {
187 changes.insert(NodeChangeSet::TEXT_CONTENT);
188 }
189 }
190 (NodeType::Image(old_img), NodeType::Image(new_img)) => {
191 use std::hash::Hasher;
194 let hash_img = |img: &crate::resources::ImageRef| -> u64 {
195 let mut h = std::hash::DefaultHasher::new();
196 img.hash(&mut h);
197 h.finish()
198 };
199 if hash_img(old_img) != hash_img(new_img) {
200 changes.insert(NodeChangeSet::IMAGE_CHANGED);
201 }
202 }
203 _ => {} }
205
206 {
208 use crate::dom::AttributeType;
209 let old_ids_classes: Vec<_> = old_node.attributes().as_ref().iter()
210 .filter(|a| matches!(a, AttributeType::Id(_) | AttributeType::Class(_)))
211 .collect();
212 let new_ids_classes: Vec<_> = new_node.attributes().as_ref().iter()
213 .filter(|a| matches!(a, AttributeType::Id(_) | AttributeType::Class(_)))
214 .collect();
215 if old_ids_classes != new_ids_classes {
216 changes.insert(NodeChangeSet::IDS_AND_CLASSES);
217 }
218 }
219
220 if old_node.style != new_node.style {
225 let mut has_layout = false;
226 let mut has_paint = false;
227
228 let mut old_map = OrderedMap::default();
230 for (prop, conds) in old_node.style.iter_inline_properties() {
231 old_map.insert(prop.get_type(), (prop, conds));
232 }
233
234 let mut seen_types = OrderedMap::default();
236 for (prop, conds) in new_node.style.iter_inline_properties() {
237 let prop_type = prop.get_type();
238 seen_types.insert(prop_type, ());
239 match old_map.get(&prop_type) {
240 Some(&(old_prop, old_conds))
241 if old_prop == prop
242 && old_conds.as_slice() == conds.as_slice() => {} _ => {
244 let scope = prop_type.relayout_scope(true);
245 if scope != RelayoutScope::None {
246 has_layout = true;
247 } else {
248 has_paint = true;
249 }
250 }
251 }
252 }
253
254 for (prop_type, _) in old_map.iter() {
256 if !seen_types.contains_key(prop_type) {
257 let scope = prop_type.relayout_scope(true);
258 if scope != RelayoutScope::None {
259 has_layout = true;
260 } else {
261 has_paint = true;
262 }
263 }
264 }
265
266 if has_layout {
267 changes.insert(NodeChangeSet::INLINE_STYLE_LAYOUT);
268 }
269 if has_paint {
270 changes.insert(NodeChangeSet::INLINE_STYLE_PAINT);
271 }
272 }
273
274 {
276 let old_cbs = old_node.callbacks.as_ref();
277 let new_cbs = new_node.callbacks.as_ref();
278 if old_cbs.len() != new_cbs.len() {
279 changes.insert(NodeChangeSet::CALLBACKS);
280 } else {
281 for (o, n) in old_cbs.iter().zip(new_cbs.iter()) {
282 if o.event != n.event || o.callback != n.callback {
283 changes.insert(NodeChangeSet::CALLBACKS);
284 break;
285 }
286 }
287 }
288 }
289
290 if old_node.get_dataset() != new_node.get_dataset() {
292 changes.insert(NodeChangeSet::DATASET);
293 }
294
295 if old_node.is_contenteditable() != new_node.is_contenteditable() {
297 changes.insert(NodeChangeSet::CONTENTEDITABLE);
298 }
299
300 if old_node.get_tab_index() != new_node.get_tab_index() {
302 changes.insert(NodeChangeSet::TAB_INDEX);
303 }
304
305 if old_styled_state != new_styled_state {
307 changes.insert(NodeChangeSet::STYLED_STATE);
308 }
309
310 changes
311}
312
313pub fn calculate_reconciliation_key(
330 node_data: &[NodeData],
331 hierarchy: &[NodeHierarchyItem],
332 node_id: NodeId,
333) -> u64 {
334 use core::hash::Hasher;
335
336 let node = &node_data[node_id.index()];
337
338 if let Some(key) = node.get_key() {
340 return key;
341 }
342
343 for attr in node.attributes().as_ref().iter() {
345 if let Some(id) = attr.as_id() {
346 let mut hasher = std::hash::DefaultHasher::new();
347 id.hash(&mut hasher);
348 return hasher.finish();
349 }
350 }
351
352 let mut hasher = std::hash::DefaultHasher::new();
354
355 core::mem::discriminant(node.get_node_type()).hash(&mut hasher);
356 for attr in node.attributes().as_ref().iter() {
357 if let Some(class) = attr.as_class() {
358 class.hash(&mut hasher);
359 }
360 }
361
362 if let Some(hierarchy_item) = hierarchy.get(node_id.index()) {
363 if let Some(parent_id) = hierarchy_item.parent_id() {
364 let mut sibling_index: usize = 0;
365 let parent_hierarchy = &hierarchy[parent_id.index()];
366
367 let mut current = parent_hierarchy.first_child_id(parent_id);
368 while let Some(sibling_id) = current {
369 if sibling_id == node_id {
370 break;
371 }
372 let sibling = &node_data[sibling_id.index()];
373 if core::mem::discriminant(sibling.get_node_type())
374 == core::mem::discriminant(node.get_node_type())
375 {
376 sibling_index += 1;
377 }
378 current = hierarchy[sibling_id.index()].next_sibling_id();
379 }
380
381 sibling_index.hash(&mut hasher);
382
383 let parent_key = calculate_reconciliation_key(node_data, hierarchy, parent_id);
384 parent_key.hash(&mut hasher);
385 }
386 }
387
388 hasher.finish()
389}
390
391pub fn precompute_reconciliation_keys(
397 node_data: &[NodeData],
398 hierarchy: &[NodeHierarchyItem],
399) -> Vec<u64> {
400 (0..node_data.len())
401 .map(|idx| calculate_reconciliation_key(node_data, hierarchy, NodeId::new(idx)))
402 .collect()
403}
404
405#[derive(Debug, Clone, Copy)]
407pub struct NodeMove {
408 pub old_node_id: NodeId,
410 pub new_node_id: NodeId,
412}
413
414#[derive(Debug, Clone)]
416pub struct DiffResult {
417 pub events: Vec<SyntheticEvent>,
419 pub node_moves: Vec<NodeMove>,
421}
422
423impl Default for DiffResult {
424 fn default() -> Self {
425 Self {
426 events: Vec::new(),
427 node_moves: Vec::new(),
428 }
429 }
430}
431
432pub fn reconcile_dom(
458 old_node_data: &[NodeData],
459 new_node_data: &[NodeData],
460 old_hierarchy: &[NodeHierarchyItem],
461 new_hierarchy: &[NodeHierarchyItem],
462 old_layout: &OrderedMap<NodeId, LogicalRect>,
463 new_layout: &OrderedMap<NodeId, LogicalRect>,
464 dom_id: DomId,
465 timestamp: Instant,
466) -> DiffResult {
467 let mut result = DiffResult::default();
468
469 let old_rec_keys = precompute_reconciliation_keys(old_node_data, old_hierarchy);
481
482 let mut old_by_rec_key: OrderedMap<u64, VecDeque<NodeId>> = OrderedMap::default();
483 let mut old_hashed: OrderedMap<DomNodeHash, VecDeque<NodeId>> = OrderedMap::default();
484 let mut old_structural: OrderedMap<DomNodeHash, VecDeque<NodeId>> = OrderedMap::default();
485 let mut old_nodes_consumed = vec![false; old_node_data.len()];
486
487 for (idx, node) in old_node_data.iter().enumerate() {
488 let id = NodeId::new(idx);
489 old_by_rec_key.entry(old_rec_keys[idx]).or_default().push_back(id);
490
491 let hash = node.calculate_node_data_hash();
492 old_hashed.entry(hash).or_default().push_back(id);
493
494 let structural_hash = node.calculate_structural_hash();
495 old_structural.entry(structural_hash).or_default().push_back(id);
496 }
497
498 fn pop_first_unconsumed(
502 queue: &mut VecDeque<NodeId>,
503 consumed: &[bool],
504 ) -> Option<NodeId> {
505 while let Some(&old_id) = queue.front() {
506 if consumed[old_id.index()] {
507 queue.pop_front();
508 } else {
509 queue.pop_front();
510 return Some(old_id);
511 }
512 }
513 None
514 }
515
516 for (new_idx, new_node) in new_node_data.iter().enumerate() {
517 let new_id = NodeId::new(new_idx);
518 let mut matched_old_id = None;
519 let mut matched_by_rec_key = false;
520 let has_explicit_key = new_node.get_key().is_some();
521
522 let new_rec_key =
524 calculate_reconciliation_key(new_node_data, new_hierarchy, new_id);
525 if let Some(queue) = old_by_rec_key.get_mut(&new_rec_key) {
526 if let Some(old_id) = pop_first_unconsumed(queue, &old_nodes_consumed) {
527 matched_old_id = Some(old_id);
528 matched_by_rec_key = true;
529 }
530 }
531
532 if !has_explicit_key && matched_old_id.is_none() {
537 let hash = new_node.calculate_node_data_hash();
539 if let Some(queue) = old_hashed.get_mut(&hash) {
540 if let Some(old_id) = pop_first_unconsumed(queue, &old_nodes_consumed) {
541 matched_old_id = Some(old_id);
542 }
543 }
544
545 if matched_old_id.is_none() {
547 let structural_hash = new_node.calculate_structural_hash();
548 if let Some(queue) = old_structural.get_mut(&structural_hash) {
549 if let Some(old_id) = pop_first_unconsumed(queue, &old_nodes_consumed) {
550 matched_old_id = Some(old_id);
551 }
552 }
553 }
554 }
555
556 if let Some(old_id) = matched_old_id {
559 old_nodes_consumed[old_id.index()] = true;
562 result.node_moves.push(NodeMove {
563 old_node_id: old_id,
564 new_node_id: new_id,
565 });
566
567 let old_rect = old_layout.get(&old_id).copied().unwrap_or(LogicalRect::zero());
569 let new_rect = new_layout.get(&new_id).copied().unwrap_or(LogicalRect::zero());
570
571 if old_rect.size != new_rect.size {
572 if has_resize_callback(new_node) {
574 result.events.push(create_lifecycle_event(
575 EventType::Resize,
576 new_id,
577 dom_id,
578 ×tamp,
579 LifecycleEventData {
580 reason: LifecycleReason::Resize,
581 previous_bounds: Some(old_rect),
582 current_bounds: new_rect,
583 },
584 ));
585 }
586 }
587
588 if matched_by_rec_key {
594 let old_hash = old_node_data[old_id.index()].calculate_node_data_hash();
595 let new_hash = new_node.calculate_node_data_hash();
596
597 if old_hash != new_hash && has_update_callback(new_node) {
598 result.events.push(create_lifecycle_event(
599 EventType::Update,
600 new_id,
601 dom_id,
602 ×tamp,
603 LifecycleEventData {
604 reason: LifecycleReason::Update,
605 previous_bounds: Some(old_rect),
606 current_bounds: new_rect,
607 },
608 ));
609 }
610 }
611 } else {
612 if has_mount_callback(new_node) {
614 let bounds = new_layout.get(&new_id).copied().unwrap_or(LogicalRect::zero());
615 result.events.push(create_lifecycle_event(
616 EventType::Mount,
617 new_id,
618 dom_id,
619 ×tamp,
620 LifecycleEventData {
621 reason: LifecycleReason::InitialMount,
622 previous_bounds: None,
623 current_bounds: bounds,
624 },
625 ));
626 }
627 }
628 }
629
630 for (old_idx, consumed) in old_nodes_consumed.iter().enumerate() {
634 if !consumed {
635 let old_id = NodeId::new(old_idx);
636 let old_node = &old_node_data[old_idx];
637
638 if has_unmount_callback(old_node) {
639 let bounds = old_layout.get(&old_id).copied().unwrap_or(LogicalRect::zero());
640 result.events.push(create_lifecycle_event(
641 EventType::Unmount,
642 old_id,
643 dom_id,
644 ×tamp,
645 LifecycleEventData {
646 reason: LifecycleReason::Unmount,
647 previous_bounds: Some(bounds),
648 current_bounds: LogicalRect::zero(),
649 },
650 ));
651 }
652 }
653 }
654
655 result
656}
657
658fn create_lifecycle_event(
660 event_type: EventType,
661 node_id: NodeId,
662 dom_id: DomId,
663 timestamp: &Instant,
664 data: LifecycleEventData,
665) -> SyntheticEvent {
666 let dom_node_id = DomNodeId {
667 dom: dom_id,
668 node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
669 };
670 SyntheticEvent {
671 event_type,
672 source: EventSource::Lifecycle,
673 phase: EventPhase::Target,
674 target: dom_node_id,
675 current_target: dom_node_id,
676 timestamp: timestamp.clone(),
677 data: EventData::Lifecycle(data),
678 stopped: false,
679 stopped_immediate: false,
680 prevented_default: false,
681 }
682}
683
684fn has_mount_callback(node: &NodeData) -> bool {
686 node.get_callbacks().iter().any(|cb| {
687 matches!(
688 cb.event,
689 EventFilter::Component(ComponentEventFilter::AfterMount)
690 )
691 })
692}
693
694fn has_unmount_callback(node: &NodeData) -> bool {
696 node.get_callbacks().iter().any(|cb| {
697 matches!(
698 cb.event,
699 EventFilter::Component(ComponentEventFilter::BeforeUnmount)
700 )
701 })
702}
703
704fn has_resize_callback(node: &NodeData) -> bool {
706 node.get_callbacks().iter().any(|cb| {
707 matches!(
708 cb.event,
709 EventFilter::Component(ComponentEventFilter::NodeResized)
710 )
711 })
712}
713
714fn has_update_callback(node: &NodeData) -> bool {
716 node.get_callbacks().iter().any(|cb| {
717 matches!(
718 cb.event,
719 EventFilter::Component(ComponentEventFilter::Updated)
720 )
721 })
722}
723
724pub fn create_migration_map(node_moves: &[NodeMove]) -> OrderedMap<NodeId, NodeId> {
745 let mut map = OrderedMap::default();
746 for m in node_moves {
747 map.insert(m.old_node_id, m.new_node_id);
748 }
749 map
750}
751
752pub fn transfer_states(
775 old_node_data: &mut [NodeData],
776 new_node_data: &mut [NodeData],
777 node_moves: &[NodeMove],
778) {
779 use crate::refany::OptionRefAny;
780
781 for movement in node_moves {
782 let old_idx = movement.old_node_id.index();
783 let new_idx = movement.new_node_id.index();
784
785 if old_idx >= old_node_data.len() || new_idx >= new_node_data.len() {
787 continue;
788 }
789
790 let merge_callback = match new_node_data[new_idx].get_merge_callback() {
792 Some(cb) => cb,
793 None => continue, };
795
796 let old_dataset = old_node_data[old_idx].take_dataset();
799 let new_dataset = new_node_data[new_idx].take_dataset();
800
801 match (new_dataset, old_dataset) {
802 (Some(new_data), Some(old_data)) => {
803 let merged = (merge_callback.cb)(new_data, old_data);
806
807 new_node_data[new_idx].set_dataset(OptionRefAny::Some(merged));
809 }
810 (new_ds, old_ds) => {
811 if let Some(ds) = new_ds {
813 new_node_data[new_idx].set_dataset(OptionRefAny::Some(ds));
814 }
815 if let Some(ds) = old_ds {
816 old_node_data[old_idx].set_dataset(OptionRefAny::Some(ds));
817 }
818 }
819 }
820 }
821}
822
823pub fn calculate_contenteditable_key(
842 node_data: &[NodeData],
843 hierarchy: &[crate::styled_dom::NodeHierarchyItem],
844 node_id: NodeId,
845) -> u64 {
846 use std::hash::Hasher;
847
848 let node = &node_data[node_id.index()];
849
850 if let Some(explicit_key) = node.get_key() {
852 return explicit_key;
853 }
854
855 for attr in node.attributes().as_ref().iter() {
857 if let Some(id) = attr.as_id() {
858 let mut hasher = std::hash::DefaultHasher::new(); hasher.write(id.as_bytes());
860 return hasher.finish();
861 }
862 }
863
864 let mut hasher = std::hash::DefaultHasher::new(); let parent_key = if let Some(parent_id) = hierarchy.get(node_id.index()).and_then(|h| h.parent_id()) {
869 calculate_contenteditable_key(node_data, hierarchy, parent_id)
870 } else {
871 0u64 };
873 hasher.write(&parent_key.to_le_bytes());
874
875 let node_discriminant = core::mem::discriminant(node.get_node_type());
878 let nth_of_type = if let Some(parent_id) = hierarchy.get(node_id.index()).and_then(|h| h.parent_id()) {
879 let mut count = 0u32;
881 let mut sibling_id = hierarchy.get(parent_id.index()).and_then(|h| h.first_child_id(parent_id));
882 while let Some(sib_id) = sibling_id {
883 if sib_id == node_id {
884 break;
885 }
886 let sibling_discriminant = core::mem::discriminant(node_data[sib_id.index()].get_node_type());
887 if sibling_discriminant == node_discriminant {
888 count += 1;
889 }
890 sibling_id = hierarchy.get(sib_id.index()).and_then(|h| h.next_sibling_id());
891 }
892 count
893 } else {
894 0
895 };
896
897 hasher.write(&nth_of_type.to_le_bytes());
898
899 node_discriminant.hash(&mut hasher);
901
902 for attr in node.attributes().as_ref().iter() {
904 if let Some(class) = attr.as_class() {
905 hasher.write(class.as_bytes());
906 }
907 }
908
909 hasher.finish()
910}
911
912pub fn reconcile_cursor_position(
938 old_text: &str,
939 new_text: &str,
940 old_cursor_byte: usize,
941) -> usize {
942 if old_text == new_text {
944 return old_cursor_byte;
945 }
946
947 if old_text.is_empty() {
949 return new_text.len();
950 }
951
952 if new_text.is_empty() {
954 return 0;
955 }
956
957 let common_prefix_bytes = old_text
959 .bytes()
960 .zip(new_text.bytes())
961 .take_while(|(a, b)| a == b)
962 .count();
963
964 if old_cursor_byte <= common_prefix_bytes {
966 return old_cursor_byte.min(new_text.len());
967 }
968
969 let common_suffix_bytes = old_text
971 .bytes()
972 .rev()
973 .zip(new_text.bytes().rev())
974 .take_while(|(a, b)| a == b)
975 .count();
976
977 let old_suffix_start = old_text.len().saturating_sub(common_suffix_bytes);
979 let new_suffix_start = new_text.len().saturating_sub(common_suffix_bytes);
980
981 if old_cursor_byte >= old_suffix_start {
983 let offset_from_end = old_text.len() - old_cursor_byte;
984 return new_text.len().saturating_sub(offset_from_end);
985 }
986
987 new_suffix_start
990}
991
992pub fn get_node_text_content(node: &NodeData) -> Option<&str> {
996 if let crate::dom::NodeType::Text(ref text) = node.get_node_type() {
997 Some(text.as_str())
998 } else {
999 None
1000 }
1001}
1002
1003#[derive(Debug, Clone, PartialEq, Eq)]
1009pub struct TextChange {
1010 pub old_text: String,
1012 pub new_text: String,
1014}
1015
1016#[derive(Debug, Clone, Default)]
1018pub struct NodeChangeReport {
1019 pub change_set: NodeChangeSet,
1021
1022 pub relayout_scope: RelayoutScope,
1030
1031 pub changed_css_properties: Vec<CssPropertyType>,
1034
1035 pub text_change: Option<TextChange>,
1037}
1038
1039impl NodeChangeReport {
1040 pub fn needs_layout(&self) -> bool {
1043 self.change_set.needs_layout() || self.relayout_scope > RelayoutScope::None
1044 }
1045
1046 pub fn needs_paint(&self) -> bool {
1047 self.change_set.needs_paint()
1048 }
1049
1050 pub fn is_visually_unchanged(&self) -> bool {
1051 self.change_set.is_visually_unchanged() && self.relayout_scope == RelayoutScope::None
1052 }
1053}
1054
1055#[derive(Debug, Clone, Default)]
1063pub struct ChangeAccumulator {
1064 pub per_node: BTreeMap<NodeId, NodeChangeReport>,
1066
1067 pub max_scope: RelayoutScope,
1070
1071 pub mounted_nodes: Vec<NodeId>,
1074
1075 pub unmounted_nodes: Vec<NodeId>,
1078}
1079
1080impl ChangeAccumulator {
1081 pub fn new() -> Self {
1082 Self::default()
1083 }
1084
1085 pub fn is_empty(&self) -> bool {
1087 self.per_node.is_empty() && self.mounted_nodes.is_empty() && self.unmounted_nodes.is_empty()
1088 }
1089
1090 pub fn needs_layout(&self) -> bool {
1092 self.max_scope > RelayoutScope::None
1093 || !self.mounted_nodes.is_empty()
1094 || self.per_node.values().any(|r| r.needs_layout())
1095 }
1096
1097 pub fn needs_paint_only(&self) -> bool {
1099 !self.needs_layout() && self.per_node.values().any(|r| r.needs_paint())
1100 }
1101
1102 pub fn is_visually_unchanged(&self) -> bool {
1104 self.mounted_nodes.is_empty()
1105 && self.unmounted_nodes.is_empty()
1106 && self.max_scope == RelayoutScope::None
1107 && self.per_node.values().all(|r| r.is_visually_unchanged())
1108 }
1109
1110 pub fn add_dom_change(
1112 &mut self,
1113 new_node_id: NodeId,
1114 change_set: NodeChangeSet,
1115 relayout_scope: RelayoutScope,
1116 text_change: Option<TextChange>,
1117 changed_css_properties: Vec<CssPropertyType>,
1118 ) {
1119 if relayout_scope > self.max_scope {
1120 self.max_scope = relayout_scope;
1121 }
1122
1123 let report = self.per_node.entry(new_node_id).or_default();
1124 report.change_set |= change_set;
1125 if relayout_scope > report.relayout_scope {
1126 report.relayout_scope = relayout_scope;
1127 }
1128 if text_change.is_some() {
1129 report.text_change = text_change;
1130 }
1131 report.changed_css_properties.extend(changed_css_properties);
1132 }
1133
1134 pub fn add_text_change(
1136 &mut self,
1137 node_id: NodeId,
1138 old_text: String,
1139 new_text: String,
1140 ) {
1141 let scope = RelayoutScope::IfcOnly;
1142 if scope > self.max_scope {
1143 self.max_scope = scope;
1144 }
1145
1146 let report = self.per_node.entry(node_id).or_default();
1147 report.change_set.insert(NodeChangeSet::TEXT_CONTENT);
1148 if scope > report.relayout_scope {
1149 report.relayout_scope = scope;
1150 }
1151 report.text_change = Some(TextChange { old_text, new_text });
1152 }
1153
1154 pub fn add_css_change(
1156 &mut self,
1157 node_id: NodeId,
1158 prop_type: CssPropertyType,
1159 scope: RelayoutScope,
1160 ) {
1161 if scope > self.max_scope {
1162 self.max_scope = scope;
1163 }
1164
1165 let report = self.per_node.entry(node_id).or_default();
1166 if scope > RelayoutScope::None {
1167 report.change_set.insert(NodeChangeSet::INLINE_STYLE_LAYOUT);
1168 } else {
1169 report.change_set.insert(NodeChangeSet::INLINE_STYLE_PAINT);
1170 }
1171 if scope > report.relayout_scope {
1172 report.relayout_scope = scope;
1173 }
1174 report.changed_css_properties.push(prop_type);
1175 }
1176
1177 pub fn add_image_change(
1179 &mut self,
1180 node_id: NodeId,
1181 scope: RelayoutScope,
1182 ) {
1183 if scope > self.max_scope {
1184 self.max_scope = scope;
1185 }
1186
1187 let report = self.per_node.entry(node_id).or_default();
1188 report.change_set.insert(NodeChangeSet::IMAGE_CHANGED);
1189 if scope > report.relayout_scope {
1190 report.relayout_scope = scope;
1191 }
1192 }
1193
1194 pub fn add_mount(&mut self, node_id: NodeId) {
1196 self.mounted_nodes.push(node_id);
1197 }
1198
1199 pub fn add_unmount(&mut self, node_id: NodeId) {
1201 self.unmounted_nodes.push(node_id);
1202 }
1203
1204 pub fn merge_restyle_result(&mut self, restyle: &crate::styled_dom::RestyleResult) {
1210 for (node_id, changed_props) in &restyle.changed_nodes {
1211 for changed in changed_props {
1212 let prop_type = changed.current_prop.get_type();
1213 let scope = prop_type.relayout_scope(true); self.add_css_change(*node_id, prop_type, scope);
1215 }
1216 }
1217 }
1218
1219 pub fn merge_extended_diff(
1224 &mut self,
1225 extended: &ExtendedDiffResult,
1226 old_node_data: &[NodeData],
1227 new_node_data: &[NodeData],
1228 ) {
1229 for &(old_id, new_id, ref change_set) in &extended.node_changes {
1230 if change_set.is_empty() {
1231 continue;
1232 }
1233
1234 let scope = self.classify_change_scope(change_set, new_node_data, new_id);
1236
1237 let text_change = if change_set.contains(NodeChangeSet::TEXT_CONTENT) {
1239 let old_text = get_node_text_content(&old_node_data[old_id.index()])
1240 .unwrap_or("")
1241 .to_string();
1242 let new_text = get_node_text_content(&new_node_data[new_id.index()])
1243 .unwrap_or("")
1244 .to_string();
1245 Some(TextChange { old_text, new_text })
1246 } else {
1247 None
1248 };
1249
1250 self.add_dom_change(new_id, *change_set, scope, text_change, Vec::new());
1251 }
1252
1253 let matched_new: alloc::collections::BTreeSet<usize> = extended
1255 .diff
1256 .node_moves
1257 .iter()
1258 .map(|m| m.new_node_id.index())
1259 .collect();
1260
1261 for idx in 0..new_node_data.len() {
1262 if !matched_new.contains(&idx) {
1263 self.add_mount(NodeId::new(idx));
1264 }
1265 }
1266
1267 let matched_old: alloc::collections::BTreeSet<usize> = extended
1269 .diff
1270 .node_moves
1271 .iter()
1272 .map(|m| m.old_node_id.index())
1273 .collect();
1274
1275 for idx in 0..old_node_data.len() {
1276 if !matched_old.contains(&idx) {
1277 self.add_unmount(NodeId::new(idx));
1278 }
1279 }
1280 }
1281
1282 fn classify_change_scope(
1284 &self,
1285 change_set: &NodeChangeSet,
1286 new_node_data: &[NodeData],
1287 new_node_id: NodeId,
1288 ) -> RelayoutScope {
1289 if change_set.contains(NodeChangeSet::NODE_TYPE_CHANGED)
1291 || change_set.contains(NodeChangeSet::CHILDREN_CHANGED)
1292 {
1293 return RelayoutScope::Full;
1294 }
1295
1296 if change_set.contains(NodeChangeSet::IDS_AND_CLASSES) {
1298 return RelayoutScope::Full;
1299 }
1300
1301 if change_set.contains(NodeChangeSet::INLINE_STYLE_LAYOUT) {
1306 let new_node = &new_node_data[new_node_id.index()];
1308 let mut max_scope = RelayoutScope::None;
1309 for (prop, _conds) in new_node.style.iter_inline_properties() {
1310 let scope = prop.get_type().relayout_scope(true);
1311 if scope > max_scope {
1312 max_scope = scope;
1313 }
1314 }
1315 return if max_scope == RelayoutScope::None {
1316 RelayoutScope::SizingOnly } else {
1318 max_scope
1319 };
1320 }
1321
1322 if change_set.contains(NodeChangeSet::TEXT_CONTENT) {
1324 return RelayoutScope::IfcOnly;
1325 }
1326
1327 if change_set.contains(NodeChangeSet::IMAGE_CHANGED) {
1329 return RelayoutScope::SizingOnly;
1330 }
1331
1332 if change_set.contains(NodeChangeSet::CONTENTEDITABLE) {
1334 return RelayoutScope::SizingOnly;
1335 }
1336
1337 if change_set.intersects(NodeChangeSet::AFFECTS_PAINT) {
1339 return RelayoutScope::None;
1340 }
1341
1342 RelayoutScope::None
1343 }
1344}
1345
1346pub fn reconcile_dom_with_changes(
1354 old_node_data: &[NodeData],
1355 new_node_data: &[NodeData],
1356 old_hierarchy: &[NodeHierarchyItem],
1357 new_hierarchy: &[NodeHierarchyItem],
1358 old_styled_nodes: Option<&[StyledNodeState]>,
1359 new_styled_nodes: Option<&[StyledNodeState]>,
1360 old_layout: &OrderedMap<NodeId, LogicalRect>,
1361 new_layout: &OrderedMap<NodeId, LogicalRect>,
1362 dom_id: DomId,
1363 timestamp: Instant,
1364) -> ExtendedDiffResult {
1365 let diff = reconcile_dom(
1367 old_node_data,
1368 new_node_data,
1369 old_hierarchy,
1370 new_hierarchy,
1371 old_layout,
1372 new_layout,
1373 dom_id,
1374 timestamp,
1375 );
1376
1377 let mut node_changes = Vec::new();
1379 for node_move in &diff.node_moves {
1380 let old_nd = &old_node_data[node_move.old_node_id.index()];
1381 let new_nd = &new_node_data[node_move.new_node_id.index()];
1382
1383 let old_state = old_styled_nodes.and_then(|s| s.get(node_move.old_node_id.index()));
1384 let new_state = new_styled_nodes.and_then(|s| s.get(node_move.new_node_id.index()));
1385
1386 let changes = compute_node_changes(old_nd, new_nd, old_state, new_state);
1387 node_changes.push((node_move.old_node_id, node_move.new_node_id, changes));
1388 }
1389
1390 ExtendedDiffResult { diff, node_changes }
1391}
1392
1393#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1409pub struct NodeDataFingerprint {
1410 pub content_hash: u64,
1412 pub state_hash: u64,
1414 pub inline_css_hash: u64,
1416 pub ids_classes_hash: u64,
1418 pub callbacks_hash: u64,
1420 pub attrs_hash: u64,
1422}
1423
1424impl Default for NodeDataFingerprint {
1425 fn default() -> Self {
1426 Self {
1427 content_hash: 0,
1428 state_hash: 0,
1429 inline_css_hash: 0,
1430 ids_classes_hash: 0,
1431 callbacks_hash: 0,
1432 attrs_hash: 0,
1433 }
1434 }
1435}
1436
1437impl NodeDataFingerprint {
1438 pub fn compute(node: &NodeData, styled_state: Option<&StyledNodeState>) -> Self {
1440 use std::hash::Hasher;
1441 use core::hash::Hash;
1442
1443 let content_hash = {
1445 let mut h = std::hash::DefaultHasher::new();
1446 node.get_node_type().hash(&mut h);
1447 h.finish()
1448 };
1449
1450 let state_hash = {
1452 let mut h = std::hash::DefaultHasher::new();
1453 if let Some(state) = styled_state {
1454 state.hash(&mut h);
1455 }
1456 h.finish()
1457 };
1458
1459 let inline_css_hash = {
1463 let mut h = std::hash::DefaultHasher::new();
1464 for (prop, conds) in node.style.iter_inline_properties() {
1465 prop.hash(&mut h);
1466 conds.as_slice().len().hash(&mut h);
1467 }
1468 h.finish()
1469 };
1470
1471 let ids_classes_hash = {
1473 let mut h = std::hash::DefaultHasher::new();
1474 for attr in node.attributes().as_ref().iter() {
1475 match attr {
1476 crate::dom::AttributeType::Id(s) => {
1477 crate::dom::IdOrClass::Id(s.clone()).hash(&mut h);
1478 }
1479 crate::dom::AttributeType::Class(s) => {
1480 crate::dom::IdOrClass::Class(s.clone()).hash(&mut h);
1481 }
1482 _ => {}
1483 }
1484 }
1485 h.finish()
1486 };
1487
1488 let callbacks_hash = {
1490 let mut h = std::hash::DefaultHasher::new();
1491 for cb in node.callbacks.as_ref().iter() {
1492 cb.event.hash(&mut h);
1493 cb.callback.hash(&mut h);
1494 }
1495 h.finish()
1496 };
1497
1498 let attrs_hash = {
1500 let mut h = std::hash::DefaultHasher::new();
1501 node.is_contenteditable().hash(&mut h);
1502 node.flags.hash(&mut h);
1503 node.get_dataset().hash(&mut h);
1504 h.finish()
1505 };
1506
1507 Self {
1508 content_hash,
1509 state_hash,
1510 inline_css_hash,
1511 ids_classes_hash,
1512 callbacks_hash,
1513 attrs_hash,
1514 }
1515 }
1516
1517 pub fn diff(&self, other: &NodeDataFingerprint) -> NodeChangeSet {
1525 let mut changes = NodeChangeSet::empty();
1526
1527 if self.content_hash != other.content_hash {
1528 changes.insert(NodeChangeSet::TEXT_CONTENT);
1532 changes.insert(NodeChangeSet::IMAGE_CHANGED);
1533 }
1534
1535 if self.state_hash != other.state_hash {
1536 changes.insert(NodeChangeSet::STYLED_STATE);
1537 }
1538
1539 if self.inline_css_hash != other.inline_css_hash {
1540 changes.insert(NodeChangeSet::INLINE_STYLE_LAYOUT);
1543 }
1544
1545 if self.ids_classes_hash != other.ids_classes_hash {
1546 changes.insert(NodeChangeSet::IDS_AND_CLASSES);
1547 }
1548
1549 if self.callbacks_hash != other.callbacks_hash {
1550 changes.insert(NodeChangeSet::CALLBACKS);
1551 }
1552
1553 if self.attrs_hash != other.attrs_hash {
1554 changes.insert(NodeChangeSet::TAB_INDEX);
1555 changes.insert(NodeChangeSet::CONTENTEDITABLE);
1556 }
1557
1558 changes
1559 }
1560
1561 pub fn is_identical(&self, other: &NodeDataFingerprint) -> bool {
1563 self == other
1564 }
1565
1566 pub fn might_affect_layout(&self, other: &NodeDataFingerprint) -> bool {
1568 self.content_hash != other.content_hash
1569 || self.inline_css_hash != other.inline_css_hash
1570 || self.ids_classes_hash != other.ids_classes_hash
1571 || self.attrs_hash != other.attrs_hash
1572 }
1573
1574 pub fn might_affect_visuals(&self, other: &NodeDataFingerprint) -> bool {
1576 self.content_hash != other.content_hash
1577 || self.state_hash != other.state_hash
1578 || self.inline_css_hash != other.inline_css_hash
1579 || self.ids_classes_hash != other.ids_classes_hash
1580 }
1581}