use alloc::{
collections::BTreeMap,
collections::VecDeque,
string::{String, ToString},
vec::Vec,
};
use core::hash::Hash;
use azul_css::props::property::{CssPropertyType, RelayoutScope};
use crate::{
dom::{DomId, DomNodeHash, DomNodeId, IdOrClass, NodeData, NodeType},
events::{
ComponentEventFilter, EventData, EventFilter, EventPhase, EventSource, EventType,
LifecycleEventData, LifecycleReason, SyntheticEvent,
},
geom::LogicalRect,
id::NodeId,
refany::RefAny,
styled_dom::{
ChangedCssProperty, NodeHierarchyItem, NodeHierarchyItemId, RestyleResult, StyledNodeState,
},
task::Instant,
OrderedMap,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct NodeChangeSet {
pub bits: u32,
}
impl NodeChangeSet {
pub const NODE_TYPE_CHANGED: u32 = 0b0000_0000_0000_0001;
pub const TEXT_CONTENT: u32 = 0b0000_0000_0000_0010;
pub const IDS_AND_CLASSES: u32 = 0b0000_0000_0000_0100;
pub const INLINE_STYLE_LAYOUT: u32 = 0b0000_0000_0000_1000;
pub const CHILDREN_CHANGED: u32 = 0b0000_0000_0001_0000;
pub const IMAGE_CHANGED: u32 = 0b0000_0000_0010_0000;
pub const CONTENTEDITABLE: u32 = 0b0000_0000_0100_0000;
pub const TAB_INDEX: u32 = 0b0000_0000_1000_0000;
pub const INLINE_STYLE_PAINT: u32 = 0b0000_0001_0000_0000;
pub const STYLED_STATE: u32 = 0b0000_0010_0000_0000;
pub const CALLBACKS: u32 = 0b0000_0100_0000_0000;
pub const DATASET: u32 = 0b0000_1000_0000_0000;
pub const ACCESSIBILITY: u32 = 0b0001_0000_0000_0000;
pub const AFFECTS_LAYOUT: u32 = Self::NODE_TYPE_CHANGED
| Self::TEXT_CONTENT
| Self::IDS_AND_CLASSES
| Self::INLINE_STYLE_LAYOUT
| Self::CHILDREN_CHANGED
| Self::IMAGE_CHANGED
| Self::CONTENTEDITABLE;
pub const AFFECTS_PAINT: u32 = Self::INLINE_STYLE_PAINT | Self::STYLED_STATE;
#[must_use]
pub const fn empty() -> Self {
Self { bits: 0 }
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.bits == 0
}
#[must_use]
pub const fn contains(&self, flag: u32) -> bool {
(self.bits & flag) == flag
}
#[must_use]
pub const fn intersects(&self, mask: u32) -> bool {
(self.bits & mask) != 0
}
pub const fn insert(&mut self, flag: u32) {
self.bits |= flag;
}
#[must_use]
pub const fn is_visually_unchanged(&self) -> bool {
!self.intersects(Self::AFFECTS_LAYOUT) && !self.intersects(Self::AFFECTS_PAINT)
}
#[must_use]
pub const fn needs_layout(&self) -> bool {
self.intersects(Self::AFFECTS_LAYOUT)
}
#[must_use]
pub const fn needs_paint(&self) -> bool {
self.intersects(Self::AFFECTS_PAINT)
}
}
impl core::ops::BitOrAssign for NodeChangeSet {
fn bitor_assign(&mut self, rhs: Self) {
self.bits |= rhs.bits;
}
}
impl core::ops::BitOr for NodeChangeSet {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
Self {
bits: self.bits | rhs.bits,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ExtendedDiffResult {
pub diff: DiffResult,
pub node_changes: Vec<(NodeId, NodeId, NodeChangeSet)>,
}
#[allow(clippy::too_many_lines)] #[must_use]
pub fn compute_node_changes(
old_node: &NodeData,
new_node: &NodeData,
old_styled_state: Option<&StyledNodeState>,
new_styled_state: Option<&StyledNodeState>,
) -> NodeChangeSet {
let mut changes = NodeChangeSet::empty();
if core::mem::discriminant(old_node.get_node_type())
!= core::mem::discriminant(new_node.get_node_type())
{
changes.insert(NodeChangeSet::NODE_TYPE_CHANGED);
return changes; }
match (old_node.get_node_type(), new_node.get_node_type()) {
(NodeType::Text(old_text), NodeType::Text(new_text)) => {
if old_text.as_str() != new_text.as_str() {
changes.insert(NodeChangeSet::TEXT_CONTENT);
}
}
(NodeType::Image(old_img), NodeType::Image(new_img)) => {
use core::hash::Hasher;
let hash_img = |img: &crate::resources::ImageRef| -> u64 {
let mut h = crate::hash::DefaultHasher::new();
img.hash(&mut h);
h.finish()
};
if hash_img(old_img) != hash_img(new_img) {
changes.insert(NodeChangeSet::IMAGE_CHANGED);
}
}
_ => {} }
{
use crate::dom::AttributeType;
let old_ids_classes: Vec<_> = old_node
.attributes()
.as_ref()
.iter()
.filter(|a| matches!(a, AttributeType::Id(_) | AttributeType::Class(_)))
.collect();
let new_ids_classes: Vec<_> = new_node
.attributes()
.as_ref()
.iter()
.filter(|a| matches!(a, AttributeType::Id(_) | AttributeType::Class(_)))
.collect();
if old_ids_classes != new_ids_classes {
changes.insert(NodeChangeSet::IDS_AND_CLASSES);
}
}
if old_node.style != new_node.style {
let mut has_layout = false;
let mut has_paint = false;
#[allow(clippy::items_after_statements)]
fn mark(prop_type: CssPropertyType, has_layout: &mut bool, has_paint: &mut bool) {
if prop_type.relayout_scope(true) == RelayoutScope::None {
*has_paint = true;
} else {
*has_layout = true;
}
}
let old_props: Vec<(CssPropertyType, _, _)> = old_node
.style
.iter_inline_properties()
.map(|(prop, conds)| (prop.get_type(), prop, conds))
.collect();
let mut old_matched = vec![false; old_props.len()];
for (prop, conds) in new_node.style.iter_inline_properties() {
let prop_type = prop.get_type();
let mut found_unchanged = false;
for (i, (old_type, old_prop, old_conds)) in old_props.iter().enumerate() {
if old_matched[i]
|| *old_type != prop_type
|| old_conds.as_slice() != conds.as_slice()
{
continue;
}
old_matched[i] = true;
if *old_prop == prop {
found_unchanged = true;
}
break;
}
if !found_unchanged {
mark(prop_type, &mut has_layout, &mut has_paint);
}
}
for (i, (old_type, _, _)) in old_props.iter().enumerate() {
if !old_matched[i] {
mark(*old_type, &mut has_layout, &mut has_paint);
}
}
if has_layout {
changes.insert(NodeChangeSet::INLINE_STYLE_LAYOUT);
}
if has_paint {
changes.insert(NodeChangeSet::INLINE_STYLE_PAINT);
}
}
{
let old_cbs = old_node.callbacks.as_ref();
let new_cbs = new_node.callbacks.as_ref();
if old_cbs.len() == new_cbs.len() {
for (o, n) in old_cbs.iter().zip(new_cbs.iter()) {
if o.event != n.event || o.callback != n.callback {
changes.insert(NodeChangeSet::CALLBACKS);
break;
}
}
} else {
changes.insert(NodeChangeSet::CALLBACKS);
}
}
if old_node.get_dataset() != new_node.get_dataset() {
changes.insert(NodeChangeSet::DATASET);
}
if old_node.is_contenteditable() != new_node.is_contenteditable() {
changes.insert(NodeChangeSet::CONTENTEDITABLE);
}
if old_node.get_tab_index() != new_node.get_tab_index() {
changes.insert(NodeChangeSet::TAB_INDEX);
}
if old_styled_state != new_styled_state {
changes.insert(NodeChangeSet::STYLED_STATE);
}
changes
}
#[must_use]
pub fn calculate_reconciliation_key(
node_data: &[NodeData],
hierarchy: &[NodeHierarchyItem],
node_id: NodeId,
) -> u64 {
use core::hash::Hasher;
let n = node_data.len();
let terminal_key = |nid: NodeId| -> Option<u64> {
let node = &node_data[nid.index()];
if let Some(key) = node.get_key() {
return Some(key);
}
for attr in node.attributes().as_ref() {
if let Some(id) = attr.as_id() {
let mut hasher = crate::hash::DefaultHasher::new();
id.hash(&mut hasher);
return Some(hasher.finish());
}
}
None
};
if let Some(key) = terminal_key(node_id) {
return key;
}
let mut chain: Vec<NodeId> = Vec::new();
let mut seed_parent_key: Option<u64> = None;
let mut cur = node_id;
for _ in 0..n {
if cur.index() >= n {
break;
}
chain.push(cur);
match hierarchy
.get(cur.index())
.and_then(NodeHierarchyItem::parent_id)
{
None => break,
Some(parent) => {
if let Some(k) = terminal_key(parent) {
seed_parent_key = Some(k);
break;
}
cur = parent;
}
}
}
let mut parent_key: Option<u64> = seed_parent_key;
for &nid in chain.iter().rev() {
let node = &node_data[nid.index()];
let mut hasher = crate::hash::DefaultHasher::new();
core::mem::discriminant(node.get_node_type()).hash(&mut hasher);
for attr in node.attributes().as_ref() {
if let Some(class) = attr.as_class() {
class.hash(&mut hasher);
}
}
if let Some(parent_id) = hierarchy
.get(nid.index())
.and_then(NodeHierarchyItem::parent_id)
{
let mut sibling_index: usize = 0;
let mut current = hierarchy
.get(parent_id.index())
.and_then(|h| h.first_child_id(parent_id));
while let Some(sibling_id) = current {
if sibling_id == nid {
break;
}
let sibling = &node_data[sibling_id.index()];
if core::mem::discriminant(sibling.get_node_type())
== core::mem::discriminant(node.get_node_type())
{
sibling_index += 1;
}
current = hierarchy
.get(sibling_id.index())
.and_then(NodeHierarchyItem::next_sibling_id);
}
sibling_index.hash(&mut hasher);
parent_key.unwrap_or(0).hash(&mut hasher);
}
parent_key = Some(hasher.finish());
}
parent_key.unwrap_or(0)
}
#[must_use]
pub fn precompute_reconciliation_keys(
node_data: &[NodeData],
hierarchy: &[NodeHierarchyItem],
) -> Vec<u64> {
(0..node_data.len())
.map(|idx| calculate_reconciliation_key(node_data, hierarchy, NodeId::new(idx)))
.collect()
}
#[derive(Debug, Clone, Copy)]
pub struct NodeMove {
pub old_node_id: NodeId,
pub new_node_id: NodeId,
}
#[derive(Debug, Clone, Default)]
pub struct DiffResult {
pub events: Vec<SyntheticEvent>,
pub node_moves: Vec<NodeMove>,
}
fn compute_subtree_hashes(node_data: &[NodeData], hierarchy: &[NodeHierarchyItem]) -> Vec<u64> {
use core::hash::{Hash, Hasher};
let mut hashes = vec![0u64; node_data.len()];
for idx in (0..node_data.len()).rev() {
let mut h = crate::hash::DefaultHasher::new();
node_data[idx].calculate_node_data_hash().hash(&mut h);
let mut child = hierarchy
.get(idx)
.and_then(|item| item.first_child_id(NodeId::new(idx)));
while let Some(c) = child {
if c.index() >= hashes.len() {
break;
}
hashes[c.index()].hash(&mut h);
child = hierarchy
.get(c.index())
.and_then(NodeHierarchyItem::next_sibling_id);
}
hashes[idx] = h.finish();
}
hashes
}
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] #[must_use]
pub fn reconcile_dom(
old_node_data: &[NodeData],
new_node_data: &[NodeData],
old_hierarchy: &[NodeHierarchyItem],
new_hierarchy: &[NodeHierarchyItem],
old_layout: &OrderedMap<NodeId, LogicalRect>,
new_layout: &OrderedMap<NodeId, LogicalRect>,
dom_id: DomId,
timestamp: Instant,
) -> DiffResult {
fn pop_first_unconsumed(queue: &mut VecDeque<NodeId>, consumed: &[bool]) -> Option<NodeId> {
while let Some(&old_id) = queue.front() {
queue.pop_front();
if !consumed[old_id.index()] {
return Some(old_id);
}
}
None
}
let mut result = DiffResult::default();
let old_rec_keys = precompute_reconciliation_keys(old_node_data, old_hierarchy);
let new_rec_keys = precompute_reconciliation_keys(new_node_data, new_hierarchy);
let old_parent_key = |old_id: NodeId| -> Option<u64> {
old_hierarchy
.get(old_id.index())
.and_then(NodeHierarchyItem::parent_id)
.map(|p| old_rec_keys[p.index()])
};
let mut old_by_rec_key: OrderedMap<u64, VecDeque<NodeId>> = OrderedMap::default();
let mut old_hashed: OrderedMap<DomNodeHash, VecDeque<NodeId>> = OrderedMap::default();
let mut old_structural: OrderedMap<DomNodeHash, VecDeque<NodeId>> = OrderedMap::default();
let mut old_nodes_consumed = vec![false; old_node_data.len()];
for (idx, node) in old_node_data.iter().enumerate() {
let id = NodeId::new(idx);
old_by_rec_key
.entry(old_rec_keys[idx])
.or_default()
.push_back(id);
let hash = node.calculate_node_data_hash();
old_hashed.entry(hash).or_default().push_back(id);
let structural_hash = node.calculate_structural_hash();
old_structural
.entry(structural_hash)
.or_default()
.push_back(id);
}
let old_subtree_hashes = compute_subtree_hashes(old_node_data, old_hierarchy);
let new_subtree_hashes = compute_subtree_hashes(new_node_data, new_hierarchy);
let mut old_by_subtree: OrderedMap<u64, VecDeque<NodeId>> = OrderedMap::default();
for (idx, h) in old_subtree_hashes.iter().enumerate() {
old_by_subtree
.entry(*h)
.or_default()
.push_back(NodeId::new(idx));
}
let has_terminal_identity = |node: &NodeData| -> bool {
node.get_key().is_some()
|| node
.attributes()
.as_ref()
.iter()
.any(|attr| attr.as_id().is_some())
};
let n_new = new_node_data.len();
let mut matched: Vec<Option<NodeId>> = vec![None; n_new];
let mut matched_by_rec_key: Vec<bool> = vec![false; n_new];
for (new_idx, new_node) in new_node_data.iter().enumerate() {
if !has_terminal_identity(new_node) {
continue;
}
if let Some(queue) = old_by_rec_key.get_mut(&new_rec_keys[new_idx]) {
if let Some(old_id) = pop_first_unconsumed(queue, &old_nodes_consumed) {
old_nodes_consumed[old_id.index()] = true;
matched[new_idx] = Some(old_id);
matched_by_rec_key[new_idx] = true;
}
}
}
let terminal_key_of = |node: &NodeData| -> Option<u64> {
use core::hash::{Hash, Hasher};
if let Some(key) = node.get_key() {
return Some(key);
}
for attr in node.attributes().as_ref() {
if let Some(id) = attr.as_id() {
let mut hasher = crate::hash::DefaultHasher::new();
id.hash(&mut hasher);
return Some(hasher.finish());
}
}
None
};
let old_parent_terminal = |old_id: NodeId| -> Option<u64> {
old_hierarchy
.get(old_id.index())
.and_then(NodeHierarchyItem::parent_id)
.and_then(|p| terminal_key_of(&old_node_data[p.index()]))
};
for new_idx in 0..n_new {
if matched[new_idx].is_some() || new_node_data[new_idx].get_key().is_some() {
continue;
}
let new_parent_terminal: Option<u64> = new_hierarchy
.get(new_idx)
.and_then(NodeHierarchyItem::parent_id)
.and_then(|p| terminal_key_of(&new_node_data[p.index()]));
if let Some(queue) = old_by_subtree.get_mut(&new_subtree_hashes[new_idx]) {
if let Some(pos) = queue.iter().position(|&old_id| {
!old_nodes_consumed[old_id.index()]
&& old_parent_terminal(old_id) == new_parent_terminal
}) {
if let Some(old_id) = queue.remove(pos) {
old_nodes_consumed[old_id.index()] = true;
matched[new_idx] = Some(old_id);
}
}
}
}
for (new_idx, new_node) in new_node_data.iter().enumerate() {
if matched[new_idx].is_some() || new_node.get_key().is_some() {
continue;
}
if let Some(queue) = old_by_rec_key.get_mut(&new_rec_keys[new_idx]) {
if let Some(old_id) = pop_first_unconsumed(queue, &old_nodes_consumed) {
old_nodes_consumed[old_id.index()] = true;
matched[new_idx] = Some(old_id);
matched_by_rec_key[new_idx] = true;
}
}
}
for (new_idx, new_node) in new_node_data.iter().enumerate() {
if matched[new_idx].is_some() || new_node.get_key().is_some() {
continue;
}
let new_parent_key: Option<u64> = new_hierarchy
.get(new_idx)
.and_then(NodeHierarchyItem::parent_id)
.map(|p| new_rec_keys[p.index()]);
let hash = new_node.calculate_node_data_hash();
if let Some(queue) = old_hashed.get_mut(&hash) {
if let Some(pos) = queue.iter().position(|&old_id| {
!old_nodes_consumed[old_id.index()] && old_parent_key(old_id) == new_parent_key
}) {
if let Some(old_id) = queue.remove(pos) {
old_nodes_consumed[old_id.index()] = true;
matched[new_idx] = Some(old_id);
continue;
}
}
}
let structural_hash = new_node.calculate_structural_hash();
if let Some(queue) = old_structural.get_mut(&structural_hash) {
if let Some(pos) = queue.iter().position(|&old_id| {
!old_nodes_consumed[old_id.index()] && old_parent_key(old_id) == new_parent_key
}) {
if let Some(old_id) = queue.remove(pos) {
old_nodes_consumed[old_id.index()] = true;
matched[new_idx] = Some(old_id);
}
}
}
}
for (new_idx, new_node) in new_node_data.iter().enumerate() {
let new_id = NodeId::new(new_idx);
let matched_old_id = matched[new_idx];
let matched_by_rec_key = matched_by_rec_key[new_idx];
if let Some(old_id) = matched_old_id {
result.node_moves.push(NodeMove {
old_node_id: old_id,
new_node_id: new_id,
});
let old_rect = old_layout
.get(&old_id)
.copied()
.unwrap_or(LogicalRect::zero());
let new_rect = new_layout
.get(&new_id)
.copied()
.unwrap_or(LogicalRect::zero());
if old_rect.size != new_rect.size {
if has_resize_callback(new_node) {
result.events.push(create_lifecycle_event(
EventType::Resize,
new_id,
dom_id,
×tamp,
LifecycleEventData {
reason: LifecycleReason::Resize,
previous_bounds: Some(old_rect),
current_bounds: new_rect,
},
));
}
}
if matched_by_rec_key {
let old_hash = old_node_data[old_id.index()].calculate_node_data_hash();
let new_hash = new_node.calculate_node_data_hash();
if old_hash != new_hash && has_update_callback(new_node) {
result.events.push(create_lifecycle_event(
EventType::Update,
new_id,
dom_id,
×tamp,
LifecycleEventData {
reason: LifecycleReason::Update,
previous_bounds: Some(old_rect),
current_bounds: new_rect,
},
));
}
}
} else {
if has_mount_callback(new_node) {
let bounds = new_layout
.get(&new_id)
.copied()
.unwrap_or(LogicalRect::zero());
result.events.push(create_lifecycle_event(
EventType::Mount,
new_id,
dom_id,
×tamp,
LifecycleEventData {
reason: LifecycleReason::InitialMount,
previous_bounds: None,
current_bounds: bounds,
},
));
}
}
}
for (old_idx, consumed) in old_nodes_consumed.iter().enumerate() {
if !consumed {
let old_id = NodeId::new(old_idx);
let old_node = &old_node_data[old_idx];
if has_unmount_callback(old_node) {
let bounds = old_layout
.get(&old_id)
.copied()
.unwrap_or(LogicalRect::zero());
result.events.push(create_lifecycle_event(
EventType::Unmount,
old_id,
dom_id,
×tamp,
LifecycleEventData {
reason: LifecycleReason::Unmount,
previous_bounds: Some(bounds),
current_bounds: LogicalRect::zero(),
},
));
}
}
}
result
}
fn create_lifecycle_event(
event_type: EventType,
node_id: NodeId,
dom_id: DomId,
timestamp: &Instant,
data: LifecycleEventData,
) -> SyntheticEvent {
let dom_node_id = DomNodeId {
dom: dom_id,
node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
};
SyntheticEvent {
event_type,
source: EventSource::Lifecycle,
phase: EventPhase::Target,
target: dom_node_id,
current_target: dom_node_id,
timestamp: timestamp.clone(),
data: EventData::Lifecycle(data),
stopped: false,
stopped_immediate: false,
prevented_default: false,
at_target_only: false,
}
}
#[must_use]
pub fn create_dismiss_event(
node_id: NodeId,
dom_id: DomId,
timestamp: &Instant,
bounds: LogicalRect,
) -> SyntheticEvent {
create_lifecycle_event(
EventType::Dismiss,
node_id,
dom_id,
timestamp,
LifecycleEventData {
reason: LifecycleReason::Dismiss,
previous_bounds: None,
current_bounds: bounds,
},
)
}
#[must_use]
pub fn create_tearoff_event(
node_id: NodeId,
dom_id: DomId,
timestamp: &Instant,
torn: bool,
bounds: LogicalRect,
) -> SyntheticEvent {
let (ty, reason) = if torn {
(EventType::TearOff, LifecycleReason::TearOff)
} else {
(EventType::Dock, LifecycleReason::Dock)
};
create_lifecycle_event(
ty,
node_id,
dom_id,
timestamp,
LifecycleEventData {
reason,
previous_bounds: None,
current_bounds: bounds,
},
)
}
fn has_mount_callback(node: &NodeData) -> bool {
node.get_callbacks().iter().any(|cb| {
matches!(
cb.event,
EventFilter::Component(ComponentEventFilter::AfterMount)
)
})
}
fn has_unmount_callback(node: &NodeData) -> bool {
node.get_callbacks().iter().any(|cb| {
matches!(
cb.event,
EventFilter::Component(ComponentEventFilter::BeforeUnmount)
)
})
}
fn has_resize_callback(node: &NodeData) -> bool {
node.get_callbacks().iter().any(|cb| {
matches!(
cb.event,
EventFilter::Component(ComponentEventFilter::NodeResized)
)
})
}
fn has_update_callback(node: &NodeData) -> bool {
node.get_callbacks().iter().any(|cb| {
matches!(
cb.event,
EventFilter::Component(ComponentEventFilter::Updated)
)
})
}
#[must_use]
pub fn create_migration_map(node_moves: &[NodeMove]) -> OrderedMap<NodeId, NodeId> {
let mut map = OrderedMap::default();
for m in node_moves {
map.insert(m.old_node_id, m.new_node_id);
}
map
}
pub const IMAGE_CHURN_SUPPRESS_TAG: &str = "image_churn";
const IMAGE_CHURN_PER_SEC: u32 = 10;
#[cfg(feature = "std")]
type ImageChurnMap = BTreeMap<usize, (u32, std::time::Instant, bool)>;
#[cfg(feature = "std")]
fn image_churn_state() -> &'static std::sync::Mutex<ImageChurnMap> {
use std::sync::{Mutex, OnceLock};
static CHURN: OnceLock<Mutex<ImageChurnMap>> = OnceLock::new();
CHURN.get_or_init(|| Mutex::new(ImageChurnMap::new()))
}
#[cfg(all(feature = "std", test))]
pub(crate) fn image_churn_count(node_index: usize) -> u32 {
image_churn_state()
.lock()
.ok()
.and_then(|m| m.get(&node_index).map(|e| e.0))
.unwrap_or(0)
}
#[cfg(feature = "std")]
fn note_image_reinitialised(node_index: usize, carried: bool) {
use std::{
collections::BTreeMap,
sync::{Mutex, OnceLock},
time::Instant,
};
static SUPPRESSED: OnceLock<bool> = OnceLock::new();
if *SUPPRESSED.get_or_init(|| {
let v = std::env::var("AZ_SUPPRESS")
.or_else(|_| std::env::var("AZ_SUPRESS"))
.unwrap_or_default();
v.split(',')
.any(|t| t.trim().eq_ignore_ascii_case(IMAGE_CHURN_SUPPRESS_TAG))
}) {
return;
}
let churn = image_churn_state();
let Ok(mut map) = churn.lock() else {
return; };
let now = Instant::now();
let entry = map.entry(node_index).or_insert((0, now, false));
if now.duration_since(entry.1).as_secs_f32() >= 1.0 {
*entry = (1, now, entry.2);
return;
}
entry.0 += 1;
if entry.0 < IMAGE_CHURN_PER_SEC || entry.2 {
return;
}
entry.2 = true;
let rate = entry.0;
if carried {
crate::diagnostics::emit(format!(
"[azul][image-churn] node {node_index} rebuilt its image as a \
PLACEHOLDER {rate}x in one second. The previous frame was carried \
forward each time, so nothing flickers — but a live image node is \
being reconstructed every frame. If this is not a capture widget, \
build the node once and update it through the image cache. \
(suppress with AZ_SUPPRESS={IMAGE_CHURN_SUPPRESS_TAG})"
));
} else {
crate::diagnostics::emit(format!(
"[azul][image-churn] node {node_index} rebuilt its image as a \
PLACEHOLDER {rate}x in one second and the previous frame could NOT \
be carried forward: this node has NO DATASET + merge callback, so \
the reconciler cannot tell the rebuilt node is the same widget. The \
live image is discarded every frame and the node falls back to its \
placeholder until the next one arrives — a continuous flicker. If \
this is a video or camera node, it is almost certainly missing its \
dataset: attach one with a DatasetMergeCallback (see MapWidget / \
ScreenCaptureWidget). \
(suppress with AZ_SUPPRESS={IMAGE_CHURN_SUPPRESS_TAG})"
));
}
}
#[cfg(not(feature = "std"))]
fn note_image_reinitialised(_node_index: usize, _carried: bool) {}
pub fn transfer_states(
old_node_data: &mut [NodeData],
new_node_data: &mut [NodeData],
node_moves: &[NodeMove],
) {
use crate::refany::OptionRefAny;
for movement in node_moves {
let old_idx = movement.old_node_id.index();
let new_idx = movement.new_node_id.index();
if old_idx >= old_node_data.len() || new_idx >= new_node_data.len() {
continue;
}
let Some(merge_callback) = new_node_data[new_idx].get_merge_callback() else {
if new_node_data[new_idx].image_is_placeholder()
&& !old_node_data[old_idx].image_is_placeholder()
{
note_image_reinitialised(new_idx, false);
}
continue; };
let old_dataset = old_node_data[old_idx].take_dataset();
let new_dataset = new_node_data[new_idx].take_dataset();
match (new_dataset, old_dataset) {
(Some(new_data), Some(old_data)) => {
let orphan_alloc = new_data.sharing_info.ptr as usize;
let merged = (merge_callback.cb)(new_data, old_data);
if new_node_data[new_idx].image_is_placeholder()
&& !old_node_data[old_idx].image_is_placeholder()
{
if let Some(prev) = old_node_data[old_idx].get_image_ref_cloned() {
new_node_data[new_idx].set_image_ref(prev);
}
note_image_reinitialised(new_idx, true);
}
new_node_data[new_idx].set_dataset(OptionRefAny::Some(merged.clone()));
repoint_orphaned_refanys(new_node_data, orphan_alloc, &merged);
}
(new_ds, old_ds) => {
if let Some(ds) = new_ds {
new_node_data[new_idx].set_dataset(OptionRefAny::Some(ds));
}
if let Some(ds) = old_ds {
old_node_data[old_idx].set_dataset(OptionRefAny::Some(ds));
}
}
}
}
}
fn repoint_orphaned_refanys(node_data: &mut [NodeData], orphan_alloc: usize, merged: &RefAny) {
use crate::refany::OptionRefAny;
if merged.sharing_info.ptr as usize == orphan_alloc {
return; }
for nd in node_data.iter_mut() {
if let Some(vv) = nd.get_virtual_view_node() {
if vv.refany.sharing_info.ptr as usize == orphan_alloc {
vv.refany = merged.clone();
}
}
for cb in nd.callbacks.as_mut().iter_mut() {
if cb.refany.sharing_info.ptr as usize == orphan_alloc {
cb.refany = merged.clone();
}
}
let ds_is_orphan = nd
.get_dataset()
.is_some_and(|ds| ds.sharing_info.ptr as usize == orphan_alloc);
if ds_is_orphan {
nd.set_dataset(OptionRefAny::Some(merged.clone()));
}
}
}
pub fn merge_fresh_dataset(node_data: &mut [NodeData], idx: usize, fresh: RefAny) {
use crate::refany::OptionRefAny;
let Some(nd) = node_data.get_mut(idx) else {
return;
};
let orphan_alloc = fresh.sharing_info.ptr as usize;
let merge_callback = nd.get_merge_callback();
let retained = nd.take_dataset();
let result = match (merge_callback, retained) {
(Some(cb), Some(old)) => (cb.cb)(fresh, old),
_ => fresh,
};
nd.set_dataset(OptionRefAny::Some(result.clone()));
repoint_orphaned_refanys(node_data, orphan_alloc, &result);
}
#[must_use]
pub fn calculate_contenteditable_key(
node_data: &[NodeData],
hierarchy: &[NodeHierarchyItem],
node_id: NodeId,
) -> u64 {
use core::hash::Hasher;
let n = node_data.len();
let terminal_key = |nid: NodeId| -> Option<u64> {
let node = &node_data[nid.index()];
if let Some(explicit_key) = node.get_key() {
return Some(explicit_key);
}
for attr in node.attributes().as_ref() {
if let Some(id) = attr.as_id() {
let mut hasher = crate::hash::DefaultHasher::new(); hasher.write(id.as_bytes());
return Some(hasher.finish());
}
}
None
};
if let Some(key) = terminal_key(node_id) {
return key;
}
let mut chain: Vec<NodeId> = Vec::new();
let mut seed_parent_key: Option<u64> = None;
let mut cur = node_id;
for _ in 0..n {
if cur.index() >= n {
break;
}
chain.push(cur);
match hierarchy
.get(cur.index())
.and_then(NodeHierarchyItem::parent_id)
{
None => break,
Some(parent) => {
if let Some(k) = terminal_key(parent) {
seed_parent_key = Some(k);
break;
}
cur = parent;
}
}
}
let mut parent_key: u64 = seed_parent_key.unwrap_or(0);
for &nid in chain.iter().rev() {
let node = &node_data[nid.index()];
let mut hasher = crate::hash::DefaultHasher::new();
let node_parent = hierarchy
.get(nid.index())
.and_then(NodeHierarchyItem::parent_id);
let level_parent_key = if node_parent.is_some() { parent_key } else { 0 };
hasher.write(&level_parent_key.to_le_bytes());
let node_discriminant = core::mem::discriminant(node.get_node_type());
let nth_of_type = node_parent.map_or(0u32, |parent_id| {
let mut count = 0u32;
let mut sibling_id = hierarchy
.get(parent_id.index())
.and_then(|h| h.first_child_id(parent_id));
while let Some(sib_id) = sibling_id {
if sib_id == nid {
break;
}
let sibling_discriminant =
core::mem::discriminant(node_data[sib_id.index()].get_node_type());
if sibling_discriminant == node_discriminant {
count += 1;
}
sibling_id = hierarchy
.get(sib_id.index())
.and_then(NodeHierarchyItem::next_sibling_id);
}
count
});
hasher.write(&nth_of_type.to_le_bytes());
node_discriminant.hash(&mut hasher);
for attr in node.attributes().as_ref() {
if let Some(class) = attr.as_class() {
hasher.write(class.as_bytes());
}
}
parent_key = hasher.finish();
}
parent_key
}
#[must_use]
pub fn reconcile_cursor_position(old_text: &str, new_text: &str, old_cursor_byte: usize) -> usize {
let snap = |offset: usize| -> usize {
let mut o = offset.min(new_text.len());
while o > 0 && !new_text.is_char_boundary(o) {
o -= 1;
}
o
};
if old_text == new_text {
return snap(old_cursor_byte);
}
if old_text.is_empty() {
return new_text.len();
}
if new_text.is_empty() {
return 0;
}
let common_prefix_bytes = old_text
.bytes()
.zip(new_text.bytes())
.take_while(|(a, b)| a == b)
.count();
if old_cursor_byte <= common_prefix_bytes {
return snap(old_cursor_byte);
}
let common_suffix_bytes = old_text
.bytes()
.rev()
.zip(new_text.bytes().rev())
.take_while(|(a, b)| a == b)
.count();
let old_suffix_start = old_text.len().saturating_sub(common_suffix_bytes);
let new_suffix_start = new_text.len().saturating_sub(common_suffix_bytes);
if old_cursor_byte >= old_suffix_start {
let offset_from_end = old_text.len().saturating_sub(old_cursor_byte);
return snap(new_text.len().saturating_sub(offset_from_end));
}
snap(new_suffix_start)
}
#[must_use]
pub fn get_node_text_content(node: &NodeData) -> Option<&str> {
if let NodeType::Text(ref text) = node.get_node_type() {
Some(text.as_str())
} else {
None
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TextChange {
pub old_text: String,
pub new_text: String,
}
#[derive(Debug, Clone, Default)]
pub struct NodeChangeReport {
pub change_set: NodeChangeSet,
pub relayout_scope: RelayoutScope,
pub changed_css_properties: Vec<CssPropertyType>,
pub text_change: Option<TextChange>,
}
impl NodeChangeReport {
#[must_use]
pub fn needs_layout(&self) -> bool {
self.change_set.needs_layout() || self.relayout_scope > RelayoutScope::None
}
#[must_use]
pub const fn needs_paint(&self) -> bool {
self.change_set.needs_paint()
}
#[must_use]
pub fn is_visually_unchanged(&self) -> bool {
self.change_set.is_visually_unchanged() && self.relayout_scope == RelayoutScope::None
}
}
#[derive(Debug, Clone, Default)]
pub struct ChangeAccumulator {
pub per_node: BTreeMap<NodeId, NodeChangeReport>,
pub max_scope: RelayoutScope,
pub mounted_nodes: Vec<NodeId>,
pub unmounted_nodes: Vec<NodeId>,
}
impl ChangeAccumulator {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.per_node.is_empty() && self.mounted_nodes.is_empty() && self.unmounted_nodes.is_empty()
}
#[must_use]
pub fn needs_layout(&self) -> bool {
self.max_scope > RelayoutScope::None
|| !self.mounted_nodes.is_empty()
|| self.per_node.values().any(NodeChangeReport::needs_layout)
}
#[must_use]
pub fn needs_paint_only(&self) -> bool {
!self.needs_layout() && self.per_node.values().any(NodeChangeReport::needs_paint)
}
#[must_use]
pub fn is_visually_unchanged(&self) -> bool {
self.mounted_nodes.is_empty()
&& self.unmounted_nodes.is_empty()
&& self.max_scope == RelayoutScope::None
&& self
.per_node
.values()
.all(NodeChangeReport::is_visually_unchanged)
}
pub fn add_dom_change(
&mut self,
new_node_id: NodeId,
change_set: NodeChangeSet,
relayout_scope: RelayoutScope,
text_change: Option<TextChange>,
changed_css_properties: Vec<CssPropertyType>,
) {
if relayout_scope > self.max_scope {
self.max_scope = relayout_scope;
}
let report = self.per_node.entry(new_node_id).or_default();
report.change_set |= change_set;
if relayout_scope > report.relayout_scope {
report.relayout_scope = relayout_scope;
}
if text_change.is_some() {
report.text_change = text_change;
}
report.changed_css_properties.extend(changed_css_properties);
}
pub fn add_text_change(&mut self, node_id: NodeId, old_text: String, new_text: String) {
let scope = RelayoutScope::IfcOnly;
if scope > self.max_scope {
self.max_scope = scope;
}
let report = self.per_node.entry(node_id).or_default();
report.change_set.insert(NodeChangeSet::TEXT_CONTENT);
if scope > report.relayout_scope {
report.relayout_scope = scope;
}
report.text_change = Some(TextChange { old_text, new_text });
}
pub fn add_css_change(
&mut self,
node_id: NodeId,
prop_type: CssPropertyType,
scope: RelayoutScope,
) {
if scope > self.max_scope {
self.max_scope = scope;
}
let report = self.per_node.entry(node_id).or_default();
if scope > RelayoutScope::None {
report.change_set.insert(NodeChangeSet::INLINE_STYLE_LAYOUT);
} else {
report.change_set.insert(NodeChangeSet::INLINE_STYLE_PAINT);
}
if scope > report.relayout_scope {
report.relayout_scope = scope;
}
report.changed_css_properties.push(prop_type);
}
pub fn add_image_change(&mut self, node_id: NodeId, scope: RelayoutScope) {
if scope > self.max_scope {
self.max_scope = scope;
}
let report = self.per_node.entry(node_id).or_default();
report.change_set.insert(NodeChangeSet::IMAGE_CHANGED);
if scope > report.relayout_scope {
report.relayout_scope = scope;
}
}
pub fn add_mount(&mut self, node_id: NodeId) {
self.mounted_nodes.push(node_id);
}
pub fn add_unmount(&mut self, node_id: NodeId) {
self.unmounted_nodes.push(node_id);
}
pub fn merge_restyle_result(&mut self, restyle: &crate::styled_dom::RestyleResult) {
for (node_id, changed_props) in &restyle.changed_nodes {
for changed in changed_props {
let prop_type = changed.current_prop.get_type();
let scope = prop_type.relayout_scope(true); self.add_css_change(*node_id, prop_type, scope);
}
}
}
pub fn merge_extended_diff(
&mut self,
extended: &ExtendedDiffResult,
old_node_data: &[NodeData],
new_node_data: &[NodeData],
) {
for &(old_id, new_id, ref change_set) in &extended.node_changes {
if change_set.is_empty() {
continue;
}
let scope = Self::classify_change_scope(*change_set, new_node_data, new_id);
let text_change = if change_set.contains(NodeChangeSet::TEXT_CONTENT) {
let old_text = get_node_text_content(&old_node_data[old_id.index()])
.unwrap_or("")
.to_string();
let new_text = get_node_text_content(&new_node_data[new_id.index()])
.unwrap_or("")
.to_string();
Some(TextChange { old_text, new_text })
} else {
None
};
self.add_dom_change(new_id, *change_set, scope, text_change, Vec::new());
}
let matched_new: alloc::collections::BTreeSet<usize> = extended
.diff
.node_moves
.iter()
.map(|m| m.new_node_id.index())
.collect();
for idx in 0..new_node_data.len() {
if !matched_new.contains(&idx) {
self.add_mount(NodeId::new(idx));
}
}
let matched_old: alloc::collections::BTreeSet<usize> = extended
.diff
.node_moves
.iter()
.map(|m| m.old_node_id.index())
.collect();
for idx in 0..old_node_data.len() {
if !matched_old.contains(&idx) {
self.add_unmount(NodeId::new(idx));
}
}
}
fn classify_change_scope(
change_set: NodeChangeSet,
new_node_data: &[NodeData],
new_node_id: NodeId,
) -> RelayoutScope {
if change_set.contains(NodeChangeSet::NODE_TYPE_CHANGED)
|| change_set.contains(NodeChangeSet::CHILDREN_CHANGED)
{
return RelayoutScope::Full;
}
if change_set.contains(NodeChangeSet::IDS_AND_CLASSES) {
return RelayoutScope::Full;
}
if change_set.contains(NodeChangeSet::INLINE_STYLE_LAYOUT) {
let new_node = &new_node_data[new_node_id.index()];
let mut max_scope = RelayoutScope::None;
for (prop, _conds) in new_node.style.iter_inline_properties() {
let scope = prop.get_type().relayout_scope(true);
if scope > max_scope {
max_scope = scope;
}
}
return if max_scope == RelayoutScope::None {
RelayoutScope::SizingOnly } else {
max_scope
};
}
if change_set.contains(NodeChangeSet::TEXT_CONTENT) {
return RelayoutScope::IfcOnly;
}
if change_set.contains(NodeChangeSet::IMAGE_CHANGED) {
return RelayoutScope::SizingOnly;
}
if change_set.contains(NodeChangeSet::CONTENTEDITABLE) {
return RelayoutScope::SizingOnly;
}
if change_set.intersects(NodeChangeSet::AFFECTS_PAINT) {
return RelayoutScope::None;
}
RelayoutScope::None
}
}
#[must_use]
pub fn reconcile_dom_with_changes(
old_node_data: &[NodeData],
new_node_data: &[NodeData],
old_hierarchy: &[NodeHierarchyItem],
new_hierarchy: &[NodeHierarchyItem],
old_styled_nodes: Option<&[StyledNodeState]>,
new_styled_nodes: Option<&[StyledNodeState]>,
old_layout: &OrderedMap<NodeId, LogicalRect>,
new_layout: &OrderedMap<NodeId, LogicalRect>,
dom_id: DomId,
timestamp: Instant,
) -> ExtendedDiffResult {
let diff = reconcile_dom(
old_node_data,
new_node_data,
old_hierarchy,
new_hierarchy,
old_layout,
new_layout,
dom_id,
timestamp,
);
let mut node_changes = Vec::new();
for node_move in &diff.node_moves {
let old_nd = &old_node_data[node_move.old_node_id.index()];
let new_nd = &new_node_data[node_move.new_node_id.index()];
let old_state = old_styled_nodes.and_then(|s| s.get(node_move.old_node_id.index()));
let new_state = new_styled_nodes.and_then(|s| s.get(node_move.new_node_id.index()));
let changes = compute_node_changes(old_nd, new_nd, old_state, new_state);
node_changes.push((node_move.old_node_id, node_move.new_node_id, changes));
}
ExtendedDiffResult { diff, node_changes }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct NodeDataFingerprint {
pub content_hash: u64,
pub state_hash: u64,
pub inline_css_hash: u64,
pub ids_classes_hash: u64,
pub callbacks_hash: u64,
pub attrs_hash: u64,
pub dataset_hash: u64,
}
impl NodeDataFingerprint {
#[must_use]
pub fn compute(node: &NodeData, styled_state: Option<&StyledNodeState>) -> Self {
use core::hash::Hash;
use core::hash::Hasher;
let content_hash = {
let mut h = crate::hash::DefaultHasher::new();
node.get_node_type().hash(&mut h);
h.finish()
};
let state_hash = {
let mut h = crate::hash::DefaultHasher::new();
if let Some(state) = styled_state {
state.hash(&mut h);
}
h.finish()
};
let inline_css_hash = {
let mut h = crate::hash::DefaultHasher::new();
for (prop, conds) in node.style.iter_inline_properties() {
prop.hash(&mut h);
conds.as_slice().len().hash(&mut h);
}
h.finish()
};
let ids_classes_hash = {
let mut h = crate::hash::DefaultHasher::new();
for attr in node.attributes().as_ref() {
match attr {
crate::dom::AttributeType::Id(s) => {
crate::dom::IdOrClass::Id(s.clone()).hash(&mut h);
}
crate::dom::AttributeType::Class(s) => {
crate::dom::IdOrClass::Class(s.clone()).hash(&mut h);
}
_ => {}
}
}
h.finish()
};
let callbacks_hash = {
let mut h = crate::hash::DefaultHasher::new();
for cb in node.callbacks.as_ref() {
cb.event.hash(&mut h);
cb.callback.hash(&mut h);
}
h.finish()
};
let attrs_hash = {
let mut h = crate::hash::DefaultHasher::new();
node.is_contenteditable().hash(&mut h);
node.flags.hash(&mut h);
h.finish()
};
let dataset_hash = {
let mut h = crate::hash::DefaultHasher::new();
match node.get_dataset() {
Some(ds) => {
true.hash(&mut h);
ds.get_type_id().hash(&mut h);
}
None => false.hash(&mut h),
}
h.finish()
};
Self {
content_hash,
state_hash,
inline_css_hash,
ids_classes_hash,
callbacks_hash,
attrs_hash,
dataset_hash,
}
}
#[must_use]
pub const fn diff(&self, other: &Self) -> NodeChangeSet {
let mut changes = NodeChangeSet::empty();
if self.content_hash != other.content_hash {
changes.insert(NodeChangeSet::TEXT_CONTENT);
changes.insert(NodeChangeSet::IMAGE_CHANGED);
}
if self.state_hash != other.state_hash {
changes.insert(NodeChangeSet::STYLED_STATE);
}
if self.inline_css_hash != other.inline_css_hash {
changes.insert(NodeChangeSet::INLINE_STYLE_LAYOUT);
}
if self.ids_classes_hash != other.ids_classes_hash {
changes.insert(NodeChangeSet::IDS_AND_CLASSES);
}
if self.callbacks_hash != other.callbacks_hash {
changes.insert(NodeChangeSet::CALLBACKS);
}
if self.attrs_hash != other.attrs_hash {
changes.insert(NodeChangeSet::TAB_INDEX);
changes.insert(NodeChangeSet::CONTENTEDITABLE);
}
if self.dataset_hash != other.dataset_hash {
changes.insert(NodeChangeSet::DATASET);
}
changes
}
#[must_use]
pub fn is_identical(&self, other: &Self) -> bool {
self == other
}
#[must_use]
pub const fn might_affect_layout(&self, other: &Self) -> bool {
self.content_hash != other.content_hash
|| self.inline_css_hash != other.inline_css_hash
|| self.ids_classes_hash != other.ids_classes_hash
|| self.attrs_hash != other.attrs_hash
}
#[must_use]
pub const fn might_affect_visuals(&self, other: &Self) -> bool {
self.content_hash != other.content_hash
|| self.state_hash != other.state_hash
|| self.inline_css_hash != other.inline_css_hash
|| self.ids_classes_hash != other.ids_classes_hash
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DomFingerprints {
pub structure: Vec<u64>,
pub style: Vec<u64>,
pub structure_root: u64,
pub style_root: u64,
}
#[derive(Debug, Default, Clone)]
pub struct PreCascadeTransfers {
pub image_callbacks: Vec<(usize, crate::callbacks::CoreImageCallback)>,
pub callbacks: Vec<(usize, crate::callbacks::CoreCallbackDataVec)>,
pub datasets: Vec<(usize, RefAny)>,
}
#[allow(clippy::too_many_lines)] #[must_use]
pub fn fingerprint_dom(dom: &crate::dom::Dom) -> (DomFingerprints, PreCascadeTransfers) {
use core::hash::{Hash, Hasher};
fn node_structure_hash(node: &NodeData, child_count: usize) -> u64 {
use crate::dom::NodeType;
use crate::resources::DecodedImage;
use core::hash::{Hash, Hasher};
let mut h = crate::hash::DefaultHasher::new();
match node.get_node_type() {
NodeType::Image(img) => {
match img.get_data() {
DecodedImage::Callback(cb) => {
0xB0DE_CA11u32.hash(&mut h);
cb.callback.cb.hash(&mut h);
cb.refany.get_type_id().hash(&mut h);
}
_ => {
node.get_node_type().hash(&mut h);
}
}
}
other => other.hash(&mut h),
}
for attr in node.attributes().as_ref() {
match attr {
crate::dom::AttributeType::Id(s) => {
1u8.hash(&mut h);
s.hash(&mut h);
}
crate::dom::AttributeType::Class(s) => {
2u8.hash(&mut h);
s.hash(&mut h);
}
other => {
3u8.hash(&mut h);
other.hash(&mut h);
}
}
}
node.callbacks.as_ref().len().hash(&mut h);
for cb in node.callbacks.as_ref() {
cb.event.hash(&mut h);
}
node.is_contenteditable().hash(&mut h);
node.flags.hash(&mut h);
child_count.hash(&mut h);
h.finish()
}
fn node_style_hash(dom: &crate::dom::Dom) -> u64 {
use core::hash::{Hash, Hasher};
let mut h = crate::hash::DefaultHasher::new();
for (prop, conds) in dom.root.style.iter_inline_properties() {
prop.hash(&mut h);
conds.as_slice().len().hash(&mut h);
}
dom.css.as_ref().len().hash(&mut h);
for css in dom.css.as_ref() {
for rule in css.rules.as_ref() {
rule.path.hash(&mut h);
for decl in rule.declarations.as_ref() {
decl.hash(&mut h);
}
for cond in rule.conditions.as_ref() {
alloc::format!("{cond:?}").hash(&mut h);
}
rule.priority.hash(&mut h);
}
}
h.finish()
}
fn walk(dom: &crate::dom::Dom, fp: &mut DomFingerprints, transfers: &mut PreCascadeTransfers) {
use crate::dom::NodeType;
use crate::resources::DecodedImage;
let idx = fp.structure.len();
fp.structure
.push(node_structure_hash(&dom.root, dom.children.as_ref().len()));
fp.style.push(node_style_hash(dom));
if let NodeType::Image(img) = dom.root.get_node_type() {
if let DecodedImage::Callback(cb) = img.get_data() {
transfers.image_callbacks.push((idx, cb.clone()));
}
}
if !dom.root.callbacks.as_ref().is_empty() {
transfers.callbacks.push((idx, dom.root.callbacks.clone()));
}
if let Some(ds) = dom.root.get_dataset() {
transfers.datasets.push((idx, ds.clone()));
}
for child in dom.children.as_ref() {
walk(child, fp, transfers);
}
}
let mut fp = DomFingerprints {
structure: Vec::new(),
style: Vec::new(),
structure_root: 0,
style_root: 0,
};
let mut transfers = PreCascadeTransfers::default();
walk(dom, &mut fp, &mut transfers);
let mut hs = crate::hash::DefaultHasher::new();
for v in &fp.structure {
v.hash(&mut hs);
}
fp.structure_root = hs.finish();
let mut hy = crate::hash::DefaultHasher::new();
for v in &fp.style {
v.hash(&mut hy);
}
fp.style_root = hy.finish();
(fp, transfers)
}
#[cfg(test)]
#[path = "diff_test.rs"]
mod diff_test;