mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-30 16:09:12 +00:00
329 lines
12 KiB
Rust
329 lines
12 KiB
Rust
//! A primitive diffing algorithm
|
|
//!
|
|
//!
|
|
//!
|
|
//!
|
|
//!
|
|
|
|
use std::{collections::HashMap, mem};
|
|
|
|
use crate::innerlude::*;
|
|
use crate::patch::Patch;
|
|
use fxhash::{FxBuildHasher, FxHashMap, FxHashSet};
|
|
use generational_arena::Index;
|
|
|
|
pub struct DiffMachine {
|
|
immediate_queue: Vec<Index>,
|
|
diffed: FxHashSet<Index>,
|
|
need_to_diff: FxHashSet<Index>,
|
|
marked_for_removal: Vec<Index>,
|
|
}
|
|
|
|
impl DiffMachine {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
immediate_queue: vec![],
|
|
diffed: FxHashSet::default(),
|
|
need_to_diff: FxHashSet::default(),
|
|
marked_for_removal: vec![],
|
|
}
|
|
}
|
|
|
|
/// Given two VirtualNode's generate Patch's that would turn the old virtual node's
|
|
/// real DOM node equivalent into the new VirtualNode's real DOM node equivalent.
|
|
pub fn diff<'a>(&mut self, old: &'a VNode, new: &'a VNode) -> Vec<Patch<'a>> {
|
|
self.diff_recursive(&old, &new, &mut 0)
|
|
}
|
|
|
|
pub fn diff_recursive<'a, 'b>(
|
|
&mut self,
|
|
old: &'a VNode,
|
|
new: &'a VNode,
|
|
cur_node_idx: &'b mut usize,
|
|
) -> Vec<Patch<'a>> {
|
|
let mut patches = vec![];
|
|
let mut replace = false;
|
|
|
|
// Different enum variants, replace!
|
|
if mem::discriminant(old) != mem::discriminant(new) {
|
|
replace = true;
|
|
}
|
|
|
|
if let (VNode::Element(old_element), VNode::Element(new_element)) = (old, new) {
|
|
// Replace if there are different element tags
|
|
if old_element.tag_name != new_element.tag_name {
|
|
// if old_element.tag != new_element.tag {
|
|
replace = true;
|
|
}
|
|
|
|
// Replace if two elements have different keys
|
|
// TODO: More robust key support. This is just an early stopgap to allow you to force replace
|
|
// an element... say if it's event changed. Just change the key name for now.
|
|
// In the future we want keys to be used to create a Patch::ReOrder to re-order siblings
|
|
// todo!
|
|
// if old_element.attributes.get("key").is_some()
|
|
// && old_element.attrs.get("key") != new_element.attrs.get("key")
|
|
// {
|
|
// replace = true;
|
|
// }
|
|
}
|
|
|
|
// Handle replacing of a node
|
|
if replace {
|
|
patches.push(Patch::Replace(*cur_node_idx, &new));
|
|
if let VNode::Element(old_element_node) = old {
|
|
for child in old_element_node.children.iter() {
|
|
increment_node_idx_for_children(child, cur_node_idx);
|
|
}
|
|
}
|
|
return patches;
|
|
}
|
|
|
|
// The following comparison can only contain identical variants, other
|
|
// cases have already been handled above by comparing variant
|
|
// discriminants.
|
|
match (old, new) {
|
|
// We're comparing two text nodes
|
|
(VNode::Text(old_text), VNode::Text(new_text)) => {
|
|
if old_text != new_text {
|
|
patches.push(Patch::ChangeText(*cur_node_idx, &new_text));
|
|
}
|
|
}
|
|
|
|
// We're comparing two element nodes
|
|
(VNode::Element(old_element), VNode::Element(new_element)) => {
|
|
// let b: HashMap<&str, &str, FxBuildHasher> = HashMap::new()
|
|
let old_attrs = old_element
|
|
.attributes
|
|
.iter()
|
|
.map(|f| (f.name, f.value))
|
|
.collect::<HashMap<&'static str, &str, FxBuildHasher>>();
|
|
|
|
let new_attrs = old_element
|
|
.attributes
|
|
.iter()
|
|
.map(|f| (f.name, f.value))
|
|
.collect::<HashMap<&'static str, &str, FxBuildHasher>>();
|
|
|
|
let mut add_attributes = FxHashMap::<&'static str, &str>::default();
|
|
// [("blah", "blah")]
|
|
// .into_iter()
|
|
// .map(|f| (f.0, f.1))
|
|
// .collect::<HashMap<&'static str, &str, FxBuildHasher>>();
|
|
|
|
// let mut add_attribute = HashMap::<&str, &str, FxBuildHasher>::new();
|
|
let mut remove_attributes: Vec<&str> = vec![];
|
|
|
|
// TODO: -> split out into func
|
|
for (new_attr_name, new_attr_val) in new_attrs.iter() {
|
|
// for (new_attr_name, new_attr_val) in new_element.attrs.iter() {
|
|
match old_attrs.get(new_attr_name) {
|
|
// match old_element.attrs.get(new_attr_name) {
|
|
Some(ref old_attr_val) => {
|
|
if old_attr_val != &new_attr_val {
|
|
add_attributes.insert(new_attr_name, new_attr_val);
|
|
}
|
|
}
|
|
None => {
|
|
add_attributes.insert(new_attr_name, new_attr_val);
|
|
}
|
|
};
|
|
}
|
|
|
|
// TODO: -> split out into func
|
|
for (old_attr_name, old_attr_val) in old_attrs.iter() {
|
|
// for (old_attr_name, old_attr_val) in old_element.attrs.iter() {
|
|
if add_attributes.get(&old_attr_name[..]).is_some() {
|
|
continue;
|
|
};
|
|
|
|
match new_attrs.get(old_attr_name) {
|
|
// match new_element.attrs.get(old_attr_name) {
|
|
Some(ref new_attr_val) => {
|
|
if new_attr_val != &old_attr_val {
|
|
remove_attributes.push(old_attr_name);
|
|
}
|
|
}
|
|
None => {
|
|
remove_attributes.push(old_attr_name);
|
|
}
|
|
};
|
|
}
|
|
|
|
if add_attributes.len() > 0 {
|
|
patches.push(Patch::AddAttributes(*cur_node_idx, add_attributes));
|
|
}
|
|
if remove_attributes.len() > 0 {
|
|
patches.push(Patch::RemoveAttributes(*cur_node_idx, remove_attributes));
|
|
}
|
|
|
|
let old_child_count = old_element.children.len();
|
|
let new_child_count = new_element.children.len();
|
|
|
|
if new_child_count > old_child_count {
|
|
let append_patch: Vec<&'a VNode> =
|
|
new_element.children[old_child_count..].iter().collect();
|
|
patches.push(Patch::AppendChildren(*cur_node_idx, append_patch))
|
|
}
|
|
|
|
if new_child_count < old_child_count {
|
|
patches.push(Patch::TruncateChildren(*cur_node_idx, new_child_count))
|
|
}
|
|
|
|
let min_count = std::cmp::min(old_child_count, new_child_count);
|
|
for index in 0..min_count {
|
|
*cur_node_idx = *cur_node_idx + 1;
|
|
let old_child = &old_element.children[index];
|
|
let new_child = &new_element.children[index];
|
|
patches.append(&mut self.diff_recursive(&old_child, &new_child, cur_node_idx))
|
|
}
|
|
if new_child_count < old_child_count {
|
|
for child in old_element.children[min_count..].iter() {
|
|
increment_node_idx_for_children(child, cur_node_idx);
|
|
}
|
|
}
|
|
}
|
|
|
|
(VNode::Suspended, _)
|
|
| (_, VNode::Suspended)
|
|
| (VNode::Component(_), _)
|
|
| (_, VNode::Component(_)) => {
|
|
todo!("cant yet handle these two")
|
|
}
|
|
|
|
(VNode::Text(_), VNode::Element(_))
|
|
| (VirtualNode::Element(_), VirtualNode::Text(_)) => {
|
|
unreachable!("Unequal variant discriminants should already have been handled");
|
|
}
|
|
};
|
|
|
|
// new_root.create_element()
|
|
patches
|
|
}
|
|
}
|
|
|
|
fn increment_node_idx_for_children<'a, 'b>(old: &'a VirtualNode, cur_node_idx: &'b mut usize) {
|
|
*cur_node_idx += 1;
|
|
if let VirtualNode::Element(element_node) = old {
|
|
for child in element_node.children.iter() {
|
|
increment_node_idx_for_children(&child, cur_node_idx);
|
|
}
|
|
}
|
|
}
|
|
|
|
// #[cfg(test)]
|
|
mod tests {
|
|
use bumpalo::Bump;
|
|
|
|
use super::*;
|
|
|
|
fn test_diff(
|
|
tree1: impl Fn(&Bump) -> VNode<'_>,
|
|
tree2: impl Fn(&Bump) -> VNode<'_>,
|
|
expected_patches: Vec<Patch>,
|
|
description: &'static str,
|
|
) {
|
|
let bump = Bump::new();
|
|
|
|
let nodes1 = tree1(&bump);
|
|
let nodes2 = tree1(&bump);
|
|
|
|
let mut machine = DiffMachine::new();
|
|
|
|
let patches = machine.diff(&nodes1, &nodes2);
|
|
|
|
patches
|
|
.iter()
|
|
.zip(expected_patches.iter())
|
|
.for_each(|f| assert_eq!(compare_patch(f.0, f.1), true, "{}", description));
|
|
}
|
|
|
|
// todo: make this actually perform real comparisons
|
|
// by default, nothing is derived for vnodes or patches
|
|
fn compare_patch(patch1: &Patch, patch2: &Patch) -> bool {
|
|
match (patch1, patch2) {
|
|
(Patch::AppendChildren(_, _), Patch::AppendChildren(_, _)) => true,
|
|
(Patch::AppendChildren(_, _), _) => false,
|
|
|
|
(Patch::TruncateChildren(_, _), Patch::TruncateChildren(_, _)) => true,
|
|
(Patch::TruncateChildren(_, _), _) => false,
|
|
|
|
(Patch::Replace(_, _), Patch::Replace(_, _)) => true,
|
|
(Patch::Replace(_, _), _) => false,
|
|
|
|
(Patch::AddAttributes(_, _), Patch::AddAttributes(_, _)) => true,
|
|
(Patch::AddAttributes(_, _), _) => false,
|
|
|
|
(Patch::RemoveAttributes(_, _), Patch::RemoveAttributes(_, _)) => true,
|
|
(Patch::RemoveAttributes(_, _), _) => false,
|
|
|
|
(Patch::ChangeText(_, _), Patch::ChangeText(_, _)) => true,
|
|
(Patch::ChangeText(_, _), _) => false,
|
|
}
|
|
}
|
|
|
|
fn printdiff(
|
|
tree1: impl for<'a> Fn(&'a Bump) -> VNode<'a>,
|
|
tree2: impl for<'a> Fn(&'a Bump) -> VNode<'a>,
|
|
desc: &'static str,
|
|
) {
|
|
let bump = Bump::new();
|
|
|
|
let nodes1 = tree1(&bump);
|
|
let nodes2 = tree2(&bump);
|
|
|
|
let mut machine = DiffMachine::new();
|
|
|
|
let patches = machine.diff(&nodes1, &nodes2);
|
|
|
|
patches.iter().for_each(|f| match f {
|
|
Patch::AppendChildren(idx, a) => {
|
|
println!("AppendChildren");
|
|
}
|
|
Patch::TruncateChildren(idx, a) => {
|
|
println!("TruncateChildren");
|
|
}
|
|
Patch::Replace(idx, a) => {
|
|
println!("Replace");
|
|
}
|
|
Patch::AddAttributes(idx, a) => {
|
|
println!("AddAttributes");
|
|
}
|
|
Patch::RemoveAttributes(idx, a) => {
|
|
println!("RemoveAttributes");
|
|
}
|
|
Patch::ChangeText(idx, a) => {
|
|
println!("ChangeText");
|
|
}
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn example_diff() {
|
|
printdiff(
|
|
html! { <div> </div> },
|
|
html! { <div>"Hello world!" </div> },
|
|
"demo the difference between two simple dom tree",
|
|
);
|
|
|
|
printdiff(
|
|
html! {
|
|
<div>
|
|
"Hello world!"
|
|
</div>
|
|
},
|
|
html! {
|
|
<div>
|
|
<div>
|
|
"Hello world!"
|
|
"Hello world!"
|
|
"Hello world!"
|
|
"Hello world!"
|
|
"Hello world!"
|
|
</div>
|
|
</div>
|
|
},
|
|
"demo the difference between two simple dom tree",
|
|
);
|
|
}
|
|
}
|