From cb74d70f831b5510f1ee191d91eaff621ffa6256 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 3 Mar 2021 02:27:26 -0500 Subject: [PATCH] wip: broken, but solved --- notes/log.md | 70 ++ packages/core-macro/src/htm.rs | 6 +- packages/core-macro/src/rsxt.rs | 5 +- packages/core/Cargo.toml | 2 +- packages/core/examples/rsx_usage.rs | 31 +- packages/core/old/patch.rs | 80 ++ packages/core/old/percydiff.rs | 329 ++++++ packages/core/{src => old}/validation.rs | 0 packages/core/src/changelist.rs | 753 ------------ packages/core/src/component.rs | 6 +- packages/core/src/context.rs | 21 +- packages/core/src/diff.rs | 1374 +++++++++++++++++----- packages/core/src/dodriodiff.rs | 1165 ------------------ packages/core/src/events.rs | 18 +- packages/core/src/lib.rs | 25 +- packages/core/src/nodebuilder.rs | 2 +- packages/core/src/nodes.rs | 8 +- packages/core/src/patch.rs | 782 +++++++++++- packages/core/src/scope.rs | 20 +- packages/core/src/virtual_dom.rs | 36 +- packages/web/src/interpreter.rs | 6 +- 21 files changed, 2398 insertions(+), 2341 deletions(-) create mode 100644 notes/log.md create mode 100644 packages/core/old/patch.rs create mode 100644 packages/core/old/percydiff.rs rename packages/core/{src => old}/validation.rs (100%) delete mode 100644 packages/core/src/changelist.rs delete mode 100644 packages/core/src/dodriodiff.rs diff --git a/notes/log.md b/notes/log.md new file mode 100644 index 000000000..e3f4e18ea --- /dev/null +++ b/notes/log.md @@ -0,0 +1,70 @@ +# March 3, 2021 + +Still TODO: +- Wire up Nodebuilder to track listeners as they are added. (easyish) +- Wire up attrs on nodes to track listeners properly + - Could be done in the nodebuilder where the attrs are added automatically (easyish) + - Could just inject context into the diffing algorithm (hardish) +- Wire up component syntax (easy) +- Wire up component calling approach (easyish) +- Wire up component diffing (hardish) + + +Approach: +- move listeners out of vnode diffing +- move listeners onto scope via nodebuilder +- instead of a listeners list, store a list of listeners and their IDs + - this way means the diffing algorithm doesn't need to know that context +- This should fix our listener approach +- The only thing from here is child component + + +Thoughts: +- the macros should generate a static set of attrs into a [attr] array (faster, more predictable, no allocs) +- children should be generated as a static set if no parans are detected + - More complex in the macro sized, unfortunately, not *too* hard +- Listeners should also be a static set (dynamic listeners don't make too much sense) + - use the builder syntax if you're doing something wild and need this granular control +- Tags should also be &'static str - no reason to generate them on the fly + +Major milestones going forward: +- Scheduled updates +- String renderer (and methods for accessing vdom directly as a tree of nodes) + - good existing work on this in some places +- Suspense +- Child support, nested diffing +- State management +- Tests tests tests + +Done so far: +- websys +- webview +- rsx! macro +- html! macro +- lifecycles +- scopes +- hooks +- context API +- bump + + +## Solutions from today's thinking session... + +### To solve children: + +- maintain a map of `ScopeIdx` to `Node` in the renderer +- Add new patch commands + - traverse_to_known (idx) + - Pop known component onto stack (super easy) + - add_known (idx) + - Save top of stack as root associated with idx + - remove_known (idx) + - Remove node on top of stack from known roots + - ... Something like this +- Continue with BFS exploration of child components, DFS of VNodes + - Easier to write, easier to reason about + +### To solve listeners: + +- Map listeners directly as attrs before diffing via a listenerhandle +- Evaluation of nodes is now stateful where we track listeners as they are added diff --git a/packages/core-macro/src/htm.rs b/packages/core-macro/src/htm.rs index cf83eba32..55c849c6b 100644 --- a/packages/core-macro/src/htm.rs +++ b/packages/core-macro/src/htm.rs @@ -58,7 +58,11 @@ impl ToTokens for HtmlRender { // create a lazy tree that accepts a bump allocator let final_tokens = quote! { - move |bump| { #new_toks } + move |ctx| { + let bump = ctx.bump(); + + #new_toks + } }; final_tokens.to_tokens(out_tokens); diff --git a/packages/core-macro/src/rsxt.rs b/packages/core-macro/src/rsxt.rs index cad534d58..4b3c9eb16 100644 --- a/packages/core-macro/src/rsxt.rs +++ b/packages/core-macro/src/rsxt.rs @@ -72,7 +72,10 @@ impl ToTokens for RsxRender { // create a lazy tree that accepts a bump allocator let final_tokens = quote! { - move |bump: &Bump| { #new_toks } + move |ctx| { + let bump = ctx.bump(); + #new_toks + } }; final_tokens.to_tokens(out_tokens); diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 78e077560..52b41edce 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -18,7 +18,7 @@ dioxus-core-macro = { path = "../core-macro", version = "0.1.1" } once_cell = "1.5.2" # Backs the scope creation and reutilization -generational-arena = "0.2.8" +generational-arena = { version = "0.2.8", features = ["serde"] } # Bumpalo backs the VNode creation bumpalo = { version = "3.6.0", features = ["collections"] } diff --git a/packages/core/examples/rsx_usage.rs b/packages/core/examples/rsx_usage.rs index 1ab197bf8..7ed471a69 100644 --- a/packages/core/examples/rsx_usage.rs +++ b/packages/core/examples/rsx_usage.rs @@ -1,3 +1,4 @@ +use bumpalo::Bump; use dioxus_core::prelude::*; fn main() {} @@ -12,21 +13,21 @@ struct ButtonProps<'a> { fn CustomButton(ctx: Context, props: ButtonProps) -> DomTree { let onfocus = move |evt: ()| log::debug!("Focused"); - todo!() - // ctx.render(rsx! { - // // button { - // // // ..props.attrs, - // // class: "abc123", - // // // style: { a: 2, b: 3, c: 4 }, - // // onclick: {move |evt| { - // // log::info("hello world"); - // // }}, - // // // Custom1 { a: 123 } - // // // Custom2 { a: 456, "abc", h1 {"1"}, h2 {"2"} } - // // // Custom3 { a: "sometext goes here" } - // // // Custom4 { onclick: |evt| log::info("click") } - // // } - // }) + // todo!() + ctx.render(rsx! { + button { + // ..props.attrs, + class: "abc123", + // style: { a: 2, b: 3, c: 4 }, + onclick: move |evt| { + // log::info("hello world"); + }, + // Custom1 { a: 123 } + // Custom2 { a: 456, "abc", h1 {"1"}, h2 {"2"} } + // Custom3 { a: "sometext goes here" } + // Custom4 { onclick: |evt| log::info("click") } + } + }) } // h1 { diff --git a/packages/core/old/patch.rs b/packages/core/old/patch.rs new file mode 100644 index 000000000..e91e4435d --- /dev/null +++ b/packages/core/old/patch.rs @@ -0,0 +1,80 @@ +use fxhash::FxHashMap; + +use crate::innerlude::{VNode, VText}; + +/// A Patch encodes an operation that modifies a real DOM element. +/// +/// To update the real DOM that a user sees you'll want to first diff your +/// old virtual dom and new virtual dom. +/// +/// This diff operation will generate `Vec` with zero or more patches that, when +/// applied to your real DOM, will make your real DOM look like your new virtual dom. +/// +/// Each Patch has a u32 node index that helps us identify the real DOM node that it applies to. +/// +/// Our old virtual dom's nodes are indexed depth first, as shown in this illustration +/// (0 being the root node, 1 being it's first child, 2 being it's first child's first child). +/// +/// ```text +/// .─. +/// ( 0 ) +/// `┬' +/// ┌────┴──────┐ +/// │ │ +/// ▼ ▼ +/// .─. .─. +/// ( 1 ) ( 4 ) +/// `┬' `─' +/// ┌────┴───┐ ├─────┬─────┐ +/// │ │ │ │ │ +/// ▼ ▼ ▼ ▼ ▼ +/// .─. .─. .─. .─. .─. +/// ( 2 ) ( 3 ) ( 5 ) ( 6 ) ( 7 ) +/// `─' `─' `─' `─' `─' +/// ``` +/// +/// The patching process is tested in a real browser in crates/virtual-dom-rs/tests/diff_patch.rs + +// #[derive(serde::Serialize, serde::Deserialize)] +pub enum Patch<'a> { + /// Append a vector of child nodes to a parent node id. + AppendChildren(NodeIdx, Vec<&'a VNode<'a>>), + + /// For a `node_i32`, remove all children besides the first `len` + TruncateChildren(NodeIdx, usize), + + /// Replace a node with another node. This typically happens when a node's tag changes. + /// ex:
becomes + Replace(NodeIdx, &'a VNode<'a>), + + /// Add attributes that the new node has that the old node does not + AddAttributes(NodeIdx, FxHashMap<&'a str, &'a str>), + + /// Remove attributes that the old node had that the new node doesn't + RemoveAttributes(NodeIdx, Vec<&'a str>), + + /// Change the text of a Text node. + ChangeText(NodeIdx, &'a VText<'a>), +} + +type NodeIdx = usize; + +impl<'a> Patch<'a> { + /// Every Patch is meant to be applied to a specific node within the DOM. Get the + /// index of the DOM node that this patch should apply to. DOM nodes are indexed + /// depth first with the root node in the tree having index 0. + pub fn node_idx(&self) -> usize { + match self { + Patch::AppendChildren(node_idx, _) => *node_idx, + Patch::TruncateChildren(node_idx, _) => *node_idx, + Patch::Replace(node_idx, _) => *node_idx, + Patch::AddAttributes(node_idx, _) => *node_idx, + Patch::RemoveAttributes(node_idx, _) => *node_idx, + Patch::ChangeText(node_idx, _) => *node_idx, + } + } +} + +pub struct PatchList<'a> { + patches: Vec>, +} diff --git a/packages/core/old/percydiff.rs b/packages/core/old/percydiff.rs new file mode 100644 index 000000000..c5ae7dd9d --- /dev/null +++ b/packages/core/old/percydiff.rs @@ -0,0 +1,329 @@ +//! 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, + diffed: FxHashSet, + need_to_diff: FxHashSet, + marked_for_removal: Vec, +} + +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> { + 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> { + 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::>(); + + let new_attrs = old_element + .attributes + .iter() + .map(|f| (f.name, f.value)) + .collect::>(); + + let mut add_attributes = FxHashMap::<&'static str, &str>::default(); + // [("blah", "blah")] + // .into_iter() + // .map(|f| (f.0, f.1)) + // .collect::>(); + + // 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, + 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! {
}, + html! {
"Hello world!"
}, + "demo the difference between two simple dom tree", + ); + + printdiff( + html! { +
+ "Hello world!" +
+ }, + html! { +
+
+ "Hello world!" + "Hello world!" + "Hello world!" + "Hello world!" + "Hello world!" +
+
+ }, + "demo the difference between two simple dom tree", + ); + } +} diff --git a/packages/core/src/validation.rs b/packages/core/old/validation.rs similarity index 100% rename from packages/core/src/validation.rs rename to packages/core/old/validation.rs diff --git a/packages/core/src/changelist.rs b/packages/core/src/changelist.rs deleted file mode 100644 index 3bb936cef..000000000 --- a/packages/core/src/changelist.rs +++ /dev/null @@ -1,753 +0,0 @@ -//! Changelist -//! ---------- -//! -//! This module exposes the "changelist" object which allows 3rd party implementors to handle diffs to the virtual dom. -//! -//! # Design -//! --- -//! In essence, the changelist object connects a diff of two vdoms to the actual edits required to update the output renderer. -//! -//! This abstraction relies on the assumption that the final renderer accepts a tree of elements. For most target platforms, -//! this is an appropriate abstraction . -//! -//! During the diff phase, the change list is built. Once the diff phase is over, the change list is finished and returned back -//! to the renderer. The renderer is responsible for propogating the updates (a stream of u32) to the final display. -//! -//! Because the change list references data internal to the vdom, it needs to be consumed by the renderer before the vdom -//! can continue to work. This means once a change list is generated, it should be consumed as fast as possible, otherwise the -//! dom will be blocked from progressing. This is enforced by lifetimes on the returend changelist object. -//! -//! - -use bumpalo::Bump; - -use crate::innerlude::Listener; -use serde::{Deserialize, Serialize}; -/// The `Edit` represents a single modifcation of the renderer tree. -/// -/// -/// -/// -/// -/// -/// -/// -/// todo@ jon: allow serde to be optional -#[derive(Debug, Serialize, Deserialize)] -#[serde(tag = "type")] -pub enum Edit<'d> { - SetText { text: &'d str }, - RemoveSelfAndNextSiblings {}, - ReplaceWith, - SetAttribute { name: &'d str, value: &'d str }, - RemoveAttribute { name: &'d str }, - PushReverseChild { n: u32 }, - PopPushChild { n: u32 }, - Pop, - AppendChild, - CreateTextNode { text: &'d str }, - CreateElement { tag_name: &'d str }, - NewEventListener { event_type: &'d str, idx: CbIdx }, - UpdateEventListener { event_type: &'d str, idx: CbIdx }, - RemoveEventListener { event_type: &'d str }, - CreateElementNs { tag_name: &'d str, ns: &'d str }, - SaveChildrenToTemporaries { temp: u32, start: u32, end: u32 }, - PushChild { n: u32 }, - PushTemporary { temp: u32 }, - InsertBefore, - PopPushReverseChild { n: u32 }, - RemoveChild { n: u32 }, - SetClass { class_name: &'d str }, -} - -/// Re-export a cover over generational ID for libraries that don't need it -/// We can go back and forth between the two via methods on GI -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -pub struct CbIdx { - pub gi_id: usize, - pub gi_gen: u64, - pub listener_idx: usize, -} - -impl CbIdx { - pub fn from_gi_index(index: generational_arena::Index, listener_idx: usize) -> Self { - let (gi_id, gi_gen) = index.into_raw_parts(); - Self { - gi_id, - gi_gen, - listener_idx, - } - } -} - -pub type EditList<'src> = Vec>; - -pub struct EditMachine<'src> { - pub traversal: Traversal, - next_temporary: u32, - forcing_new_listeners: bool, - - pub emitter: EditList<'src>, -} - -impl<'b> EditMachine<'b> { - pub fn new(_bump: &'b Bump) -> Self { - Self { - traversal: Traversal::new(), - next_temporary: 0, - forcing_new_listeners: false, - emitter: EditList::default(), - } - } - - /// Traversal methods. - pub fn go_down(&mut self) { - self.traversal.down(); - } - - pub fn go_down_to_child(&mut self, index: usize) { - self.traversal.down(); - self.traversal.sibling(index); - } - - pub fn go_down_to_reverse_child(&mut self, index: usize) { - self.traversal.down(); - self.traversal.reverse_sibling(index); - } - - pub fn go_up(&mut self) { - self.traversal.up(); - } - - pub fn go_to_sibling(&mut self, index: usize) { - self.traversal.sibling(index); - } - - pub fn go_to_temp_sibling(&mut self, temp: u32) { - self.traversal.up(); - self.traversal.down_to_temp(temp); - } - - pub fn go_down_to_temp_child(&mut self, temp: u32) { - self.traversal.down_to_temp(temp); - } - - pub fn commit_traversal(&mut self) { - if self.traversal.is_committed() { - log::debug!("Traversal already committed"); - return; - } - - for mv in self.traversal.commit() { - match mv { - MoveTo::Parent => { - log::debug!("emit: pop"); - self.emitter.push(Edit::Pop {}); - // self.emitter.pop(); - } - MoveTo::Child(n) => { - log::debug!("emit: push_child({})", n); - self.emitter.push(Edit::PushChild { n }); - } - MoveTo::ReverseChild(n) => { - log::debug!("emit: push_reverse_child({})", n); - self.emitter.push(Edit::PushReverseChild { n }); - // self.emitter.push_reverse_child(n); - } - MoveTo::Sibling(n) => { - log::debug!("emit: pop_push_child({})", n); - self.emitter.push(Edit::PopPushChild { n }); - // self.emitter.pop_push_child(n); - } - MoveTo::ReverseSibling(n) => { - log::debug!("emit: pop_push_reverse_child({})", n); - self.emitter.push(Edit::PopPushReverseChild { n }); - } - MoveTo::TempChild(temp) => { - log::debug!("emit: push_temporary({})", temp); - self.emitter.push(Edit::PushTemporary { temp }); - // self.emitter.push_temporary(temp); - } - } - } - } - - pub fn traversal_is_committed(&self) -> bool { - self.traversal.is_committed() - } -} - -impl<'a> EditMachine<'a> { - pub fn next_temporary(&self) -> u32 { - self.next_temporary - } - - pub fn set_next_temporary(&mut self, next_temporary: u32) { - self.next_temporary = next_temporary; - } - - pub fn save_children_to_temporaries(&mut self, start: usize, end: usize) -> u32 { - debug_assert!(self.traversal_is_committed()); - debug_assert!(start < end); - let temp_base = self.next_temporary; - // debug!( - // "emit: save_children_to_temporaries({}, {}, {})", - // temp_base, start, end - // ); - self.next_temporary = temp_base + (end - start) as u32; - self.emitter.push(Edit::SaveChildrenToTemporaries { - temp: temp_base, - start: start as u32, - end: end as u32, - }); - temp_base - } - - pub fn push_temporary(&mut self, temp: u32) { - debug_assert!(self.traversal_is_committed()); - // debug!("emit: push_temporary({})", temp); - self.emitter.push(Edit::PushTemporary { temp }); - // self.emitter.push_temporary(temp); - } - - pub fn remove_child(&mut self, child: usize) { - debug_assert!(self.traversal_is_committed()); - // debug!("emit: remove_child({})", child); - // self.emitter.remove_child(child as u32); - self.emitter.push(Edit::RemoveChild { n: child as u32 }) - } - - pub fn insert_before(&mut self) { - debug_assert!(self.traversal_is_committed()); - // debug!("emit: insert_before()"); - // self.emitter.insert_before(); - self.emitter.push(Edit::InsertBefore {}) - } - - pub fn ensure_string(&mut self, _string: &str) -> StringKey { - todo!() - // self.strings.ensure_string(string, &self.emitter) - } - - pub fn set_text(&mut self, text: &'a str) { - debug_assert!(self.traversal_is_committed()); - // debug!("emit: set_text({:?})", text); - // self.emitter.set_text(text); - self.emitter.push(Edit::SetText { text }); - // .set_text(text.as_ptr() as u32, text.len() as u32); - } - - pub fn remove_self_and_next_siblings(&mut self) { - debug_assert!(self.traversal_is_committed()); - // debug!("emit: remove_self_and_next_siblings()"); - self.emitter.push(Edit::RemoveSelfAndNextSiblings {}); - // self.emitter.remove_self_and_next_siblings(); - } - - pub fn replace_with(&mut self) { - debug_assert!(self.traversal_is_committed()); - // debug!("emit: replace_with()"); - self.emitter.push(Edit::ReplaceWith {}); - // self.emitter.replace_with(); - } - - pub fn set_attribute(&mut self, name: &'a str, value: &'a str, is_namespaced: bool) { - debug_assert!(self.traversal_is_committed()); - // todo!() - if name == "class" && !is_namespaced { - // let class_id = self.ensure_string(value); - // let class_id = self.ensure_string(value); - // debug!("emit: set_class({:?})", value); - // self.emitter.set_class(class_id.into()); - self.emitter.push(Edit::SetClass { class_name: value }); - } else { - self.emitter.push(Edit::SetAttribute { name, value }); - // let name_id = self.ensure_string(name); - // let value_id = self.ensure_string(value); - // debug!("emit: set_attribute({:?}, {:?})", name, value); - // self.state - // .emitter - // .set_attribute(name_id.into(), value_id.into()); - } - } - - pub fn remove_attribute(&mut self, name: &'a str) { - // todo!("figure out how to get this working with ensure string"); - self.emitter.push(Edit::RemoveAttribute { name }); - // self.emitter.remove_attribute(name); - // debug_assert!(self.traversal_is_committed()); - // // debug!("emit: remove_attribute({:?})", name); - // let name_id = self.ensure_string(name); - // self.emitter.remove_attribute(name_id.into()); - } - - pub fn append_child(&mut self) { - debug_assert!(self.traversal_is_committed()); - // debug!("emit: append_child()"); - self.emitter.push(Edit::AppendChild {}); - // self.emitter.append_child(); - } - - pub fn create_text_node(&mut self, text: &'a str) { - debug_assert!(self.traversal_is_committed()); - // debug!("emit: create_text_node({:?})", text); - // self.emitter.create_text_node(text); - self.emitter.push(Edit::CreateTextNode { text }); - } - - pub fn create_element(&mut self, tag_name: &'a str) { - // debug_assert!(self.traversal_is_committed()); - // debug!("emit: create_element({:?})", tag_name); - // let tag_name_id = self.ensure_string(tag_name); - self.emitter.push(Edit::CreateElement { tag_name }); - // self.emitter.create_element(tag_name); - // self.emitter.create_element(tag_name_id.into()); - } - - pub fn create_element_ns(&mut self, tag_name: &'a str, ns: &'a str) { - debug_assert!(self.traversal_is_committed()); - // debug!("emit: create_element_ns({:?}, {:?})", tag_name, ns); - // let tag_name_id = self.ensure_string(tag_name); - // let ns_id = self.ensure_string(ns); - // self.emitter.create_element_ns(tag_name, ns); - self.emitter.push(Edit::CreateElementNs { tag_name, ns }); - // self.emitter - // .create_element_ns(tag_name_id.into(), ns_id.into()); - } - - pub fn push_force_new_listeners(&mut self) -> bool { - let old = self.forcing_new_listeners; - self.forcing_new_listeners = true; - old - } - - pub fn pop_force_new_listeners(&mut self, previous: bool) { - debug_assert!(self.forcing_new_listeners); - self.forcing_new_listeners = previous; - } - - pub fn new_event_listener(&mut self, event: &'a str, idx: CbIdx) { - debug_assert!(self.traversal_is_committed()); - self.emitter.push(Edit::NewEventListener { - event_type: event, - idx, - }); - // todo!("Event listener not wired up yet"); - // log::debug!("emit: new_event_listener({:?})", listener); - // let (a, b) = listener.get_callback_parts(); - // debug_assert!(a != 0); - // // let event_id = self.ensure_string(listener.event); - // self.emitter.new_event_listener(listener.event.into(), a, b); - } - - pub fn update_event_listener(&mut self, event: &'a str, idx: CbIdx) { - debug_assert!(self.traversal_is_committed()); - if self.forcing_new_listeners { - self.new_event_listener(event, idx); - return; - } - - self.emitter.push(Edit::NewEventListener { - event_type: event, - idx, - }); - - // log::debug!("emit: update_event_listener({:?})", listener); - // // todo!("Event listener not wired up yet"); - // let (a, b) = listener.get_callback_parts(); - // debug_assert!(a != 0); - // self.emitter.push(Edit::UpdateEventListener { - // event_type: listener.event.into(), - // a, - // b, - // }); - // self.emitter.update_event_listener(event_id.into(), a, b); - } - - pub fn remove_event_listener(&mut self, event: &'a str) { - debug_assert!(self.traversal_is_committed()); - self.emitter - .push(Edit::RemoveEventListener { event_type: event }); - // debug!("emit: remove_event_listener({:?})", event); - // let _event_id = self.ensure_string(event); - // todo!("Event listener not wired up yet"); - // self.emitter.remove_event_listener(event_id.into()); - } - - // #[inline] - // pub fn has_template(&mut self, id: CacheId) -> bool { - // self.templates.contains(&id) - // } - - // pub fn save_template(&mut self, id: CacheId) { - // debug_assert!(self.traversal_is_committed()); - // debug_assert!(!self.has_template(id)); - // // debug!("emit: save_template({:?})", id); - // self.templates.insert(id); - // self.emitter.save_template(id.into()); - // } - - // pub fn push_template(&mut self, id: CacheId) { - // debug_assert!(self.traversal_is_committed()); - // debug_assert!(self.has_template(id)); - // // debug!("emit: push_template({:?})", id); - // self.emitter.push_template(id.into()); - // } -} - -// Keeps track of where we are moving in a DOM tree, and shortens traversal -// paths between mutations to their minimal number of operations. - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum MoveTo { - /// Move from the current node up to its parent. - Parent, - - /// Move to the current node's n^th child. - Child(u32), - - /// Move to the current node's n^th from last child. - ReverseChild(u32), - - /// Move to the n^th sibling. Not relative from the current - /// location. Absolute indexed within all of the current siblings. - Sibling(u32), - - /// Move to the n^th from last sibling. Not relative from the current - /// location. Absolute indexed within all of the current siblings. - ReverseSibling(u32), - - /// Move down to the given saved temporary child. - TempChild(u32), -} - -#[derive(Debug)] -pub struct Traversal { - uncommitted: Vec, -} - -impl Traversal { - /// Construct a new `Traversal` with its internal storage backed by the - /// given bump arena. - pub fn new() -> Traversal { - Traversal { - uncommitted: Vec::with_capacity(32), - } - } - - /// Move the traversal up in the tree. - pub fn up(&mut self) { - match self.uncommitted.last() { - Some(MoveTo::Sibling(_)) | Some(MoveTo::ReverseSibling(_)) => { - self.uncommitted.pop(); - self.uncommitted.push(MoveTo::Parent); - } - Some(MoveTo::TempChild(_)) | Some(MoveTo::Child(_)) | Some(MoveTo::ReverseChild(_)) => { - self.uncommitted.pop(); - // And we're back at the parent. - } - _ => { - self.uncommitted.push(MoveTo::Parent); - } - } - } - - /// Move the traversal down in the tree to the first child of the current - /// node. - pub fn down(&mut self) { - if let Some(&MoveTo::Parent) = self.uncommitted.last() { - self.uncommitted.pop(); - self.sibling(0); - } else { - self.uncommitted.push(MoveTo::Child(0)); - } - } - - /// Move the traversal to the n^th sibling. - pub fn sibling(&mut self, index: usize) { - let index = index as u32; - match self.uncommitted.last_mut() { - Some(MoveTo::Sibling(ref mut n)) | Some(MoveTo::Child(ref mut n)) => { - *n = index; - } - Some(MoveTo::ReverseSibling(_)) => { - self.uncommitted.pop(); - self.uncommitted.push(MoveTo::Sibling(index)); - } - Some(MoveTo::TempChild(_)) | Some(MoveTo::ReverseChild(_)) => { - self.uncommitted.pop(); - self.uncommitted.push(MoveTo::Child(index)) - } - _ => { - self.uncommitted.push(MoveTo::Sibling(index)); - } - } - } - - /// Move the the n^th from last sibling. - pub fn reverse_sibling(&mut self, index: usize) { - let index = index as u32; - match self.uncommitted.last_mut() { - Some(MoveTo::ReverseSibling(ref mut n)) | Some(MoveTo::ReverseChild(ref mut n)) => { - *n = index; - } - Some(MoveTo::Sibling(_)) => { - self.uncommitted.pop(); - self.uncommitted.push(MoveTo::ReverseSibling(index)); - } - Some(MoveTo::TempChild(_)) | Some(MoveTo::Child(_)) => { - self.uncommitted.pop(); - self.uncommitted.push(MoveTo::ReverseChild(index)) - } - _ => { - self.uncommitted.push(MoveTo::ReverseSibling(index)); - } - } - } - - /// Go to the given saved temporary. - pub fn down_to_temp(&mut self, temp: u32) { - match self.uncommitted.last() { - Some(MoveTo::Sibling(_)) | Some(MoveTo::ReverseSibling(_)) => { - self.uncommitted.pop(); - } - Some(MoveTo::Parent) - | Some(MoveTo::TempChild(_)) - | Some(MoveTo::Child(_)) - | Some(MoveTo::ReverseChild(_)) - | None => { - // Can't remove moves to parents since we rely on their stack - // pops. - } - } - self.uncommitted.push(MoveTo::TempChild(temp)); - } - - /// Are all the traversal's moves committed? That is, are there no moves - /// that have *not* been committed yet? - #[inline] - pub fn is_committed(&self) -> bool { - // is_empty is not inlined? - self.uncommitted.is_empty() - // self.uncommitted.len() == 0 - } - - /// Commit this traversals moves and return the optimized path from the last - /// commit. - #[inline] - pub fn commit(&mut self) -> Moves { - Moves { - inner: self.uncommitted.drain(..), - } - } - - #[inline] - pub fn reset(&mut self) { - self.uncommitted.clear(); - } -} - -pub struct Moves<'a> { - inner: std::vec::Drain<'a, MoveTo>, -} - -impl Iterator for Moves<'_> { - type Item = MoveTo; - - #[inline] - fn next(&mut self) -> Option { - self.inner.next() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_traversal() { - fn t(f: F) -> Box - where - F: 'static + FnMut(&mut Traversal), - { - Box::new(f) as _ - } - - for (mut traverse, expected_moves) in vec![ - ( - t(|t| { - t.down(); - }), - vec![MoveTo::Child(0)], - ), - ( - t(|t| { - t.up(); - }), - vec![MoveTo::Parent], - ), - ( - t(|t| { - t.sibling(42); - }), - vec![MoveTo::Sibling(42)], - ), - ( - t(|t| { - t.down(); - t.up(); - }), - vec![], - ), - ( - t(|t| { - t.down(); - t.sibling(2); - t.up(); - }), - vec![], - ), - ( - t(|t| { - t.down(); - t.sibling(3); - }), - vec![MoveTo::Child(3)], - ), - ( - t(|t| { - t.down(); - t.sibling(4); - t.sibling(8); - }), - vec![MoveTo::Child(8)], - ), - ( - t(|t| { - t.sibling(1); - t.sibling(1); - }), - vec![MoveTo::Sibling(1)], - ), - ( - t(|t| { - t.reverse_sibling(3); - }), - vec![MoveTo::ReverseSibling(3)], - ), - ( - t(|t| { - t.down(); - t.reverse_sibling(3); - }), - vec![MoveTo::ReverseChild(3)], - ), - ( - t(|t| { - t.down(); - t.reverse_sibling(3); - t.up(); - }), - vec![], - ), - ( - t(|t| { - t.down(); - t.reverse_sibling(3); - t.reverse_sibling(6); - }), - vec![MoveTo::ReverseChild(6)], - ), - ( - t(|t| { - t.up(); - t.reverse_sibling(3); - t.reverse_sibling(6); - }), - vec![MoveTo::Parent, MoveTo::ReverseSibling(6)], - ), - ( - t(|t| { - t.up(); - t.sibling(3); - t.sibling(6); - }), - vec![MoveTo::Parent, MoveTo::Sibling(6)], - ), - ( - t(|t| { - t.sibling(3); - t.sibling(6); - t.up(); - }), - vec![MoveTo::Parent], - ), - ( - t(|t| { - t.reverse_sibling(3); - t.reverse_sibling(6); - t.up(); - }), - vec![MoveTo::Parent], - ), - ( - t(|t| { - t.down(); - t.down_to_temp(3); - }), - vec![MoveTo::Child(0), MoveTo::TempChild(3)], - ), - ( - t(|t| { - t.down_to_temp(3); - t.sibling(5); - }), - vec![MoveTo::Child(5)], - ), - ( - t(|t| { - t.down_to_temp(3); - t.reverse_sibling(5); - }), - vec![MoveTo::ReverseChild(5)], - ), - ( - t(|t| { - t.down_to_temp(3); - t.up(); - }), - vec![], - ), - ( - t(|t| { - t.sibling(2); - t.up(); - t.down_to_temp(3); - }), - vec![MoveTo::Parent, MoveTo::TempChild(3)], - ), - ( - t(|t| { - t.up(); - t.down_to_temp(3); - }), - vec![MoveTo::Parent, MoveTo::TempChild(3)], - ), - ] { - let mut traversal = Traversal::new(); - traverse(&mut traversal); - let actual_moves: Vec<_> = traversal.commit().collect(); - assert_eq!(actual_moves, expected_moves); - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct StringKey(u32); - -impl From for u32 { - #[inline] - fn from(key: StringKey) -> u32 { - key.0 - } -} diff --git a/packages/core/src/component.rs b/packages/core/src/component.rs index e4641506d..be3e990df 100644 --- a/packages/core/src/component.rs +++ b/packages/core/src/component.rs @@ -1,6 +1,7 @@ //! This file handles the supporting infrastructure for the `Component` trait and `Properties` which makes it possible //! for components to be used within Nodes. -//! + +pub type ScopeIdx = generational_arena::Index; /// The `Component` trait refers to any struct or funciton that can be used as a component /// We automatically implement Component for FC @@ -44,6 +45,7 @@ mod tests { static TestComponent: FC<()> = |ctx, props| { // + ctx.render(html! {
@@ -52,7 +54,7 @@ mod tests { static TestComponent2: FC<()> = |ctx, props| { // - ctx.render(|bump: &Bump| VNode::text("blah")) + ctx.render(|ctx| VNode::text("blah")) }; #[test] diff --git a/packages/core/src/context.rs b/packages/core/src/context.rs index b8b48fd81..c828728ed 100644 --- a/packages/core/src/context.rs +++ b/packages/core/src/context.rs @@ -76,8 +76,10 @@ impl<'a> Context<'a> { /// ctx.render(lazy_tree) /// } ///``` - pub fn render(self, lazy_nodes: impl FnOnce(&'a Bump) -> VNode<'a> + 'a) -> DomTree { - let safe_nodes = lazy_nodes(self.bump); + pub fn render(self, lazy_nodes: impl FnOnce(NodeCtx<'a>) -> VNode<'a> + 'a) -> DomTree { + let ctx = NodeCtx { bump: self.bump }; + let safe_nodes = lazy_nodes(ctx); + let unsafe_nodes = unsafe { std::mem::transmute::, VNode<'static>>(safe_nodes) }; self.final_nodes.deref().borrow_mut().replace(unsafe_nodes); DomTree {} @@ -90,12 +92,25 @@ impl<'a> Context<'a> { /// When the future completes, the component will be renderered pub fn suspend( &self, - _fut: impl Future VNode<'a>>, + _fut: impl Future) -> VNode<'a>>, ) -> VNode<'a> { todo!() } } +// NodeCtx is used to build VNodes in the component's memory space. +// This struct adds metadata to the final DomTree about listeners, attributes, and children +pub struct NodeCtx<'a> { + bump: &'a Bump, +} + +impl NodeCtx<'_> { + #[inline] + pub fn bump(&self) -> &Bump { + self.bump + } +} + pub mod hooks { //! This module provides internal state management functionality for Dioxus components //! diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index c5ae7dd9d..ec3da7f86 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -1,329 +1,1155 @@ -//! A primitive diffing algorithm +//! Diff the `old` node with the `new` node. Emits instructions to modify a +//! physical DOM node that reflects `old` into something that reflects `new`. //! +//! Upon entry to this function, the physical DOM node must be on the top of the +//! change list stack: //! +//! [... node] //! +//! The change list stack is in the same state when this function exits. //! +//! ---- //! - -use std::{collections::HashMap, mem}; - +//! There are more ways of increasing diff performance here that are currently not implemented. +//! Additionally, the caching mechanism has also been tweaked. +//! +//! Instead of having "cached" nodes, each component is, by default, a cached node. This leads to increased +//! memory overhead for large numbers of small components, but we can optimize this by tracking alloc size over time +//! and shrinking bumps down if possible. +//! +//! Additionally, clean up of these components is not done at diff time (though it should), but rather, the diffing +//! proprogates removal lifecycle events for affected components into the event queue. It's not imperative that these +//! are ran immediately, but it should be noted that cleanup of components might be able to emit changes. +//! +//! This diffing only ever occurs on a component-by-component basis (not entire trees at once). +//! +//! Currently, the listener situation is a bit broken. +//! We aren't removing listeners (choosing to leak them instead) :( +//! Eventually, we'll set things up so add/remove listener is an instruction again +//! +//! A major assumption of this diff algorithm when combined with the ChangeList is that the Changelist will be +//! fresh and the event queue is clean. This lets us continue to batch edits together under the same ChangeList +//! +//! More info on how to improve this diffing algorithm: +//! - https://hacks.mozilla.org/2019/03/fast-bump-allocated-virtual-doms-with-rust-and-wasm/ use crate::innerlude::*; -use crate::patch::Patch; -use fxhash::{FxBuildHasher, FxHashMap, FxHashSet}; -use generational_arena::Index; +use bumpalo::Bump; +use fxhash::{FxHashMap, FxHashSet}; +use std::cmp::Ordering; -pub struct DiffMachine { - immediate_queue: Vec, - diffed: FxHashSet, - need_to_diff: FxHashSet, - marked_for_removal: Vec, +/// The DiffState is a cursor internal to the VirtualDOM's diffing algorithm that allows persistence of state while +/// diffing trees of components. This means we can "re-enter" a subtree of a component by queuing a "NeedToDiff" event. +/// +/// By re-entering via NodeDiff, we can connect disparate edits together into a single EditList. This batching of edits +/// leads to very fast re-renders (all done in a single animation frame). +/// +/// It also means diffing two trees is only ever complex as diffing a single smaller tree, and then re-entering at a +/// different cursor position. +/// +/// The order of these re-entrances is stored in the DiffState itself. The DiffState comes pre-loaded with a set of components +/// that were modified by the eventtrigger. This prevents doubly evaluating components if they wereboth updated via +/// subscriptions and props changes. +pub struct DiffMachine<'a> { + pub change_list: EditMachine<'a>, + immediate_queue: Vec, + diffed: FxHashSet, + need_to_diff: FxHashSet, } -impl DiffMachine { - pub fn new() -> Self { +impl<'a> DiffMachine<'a> { + pub fn new(bump: &'a Bump) -> Self { + // log::debug!("starting diff machine"); Self { - immediate_queue: vec![], + change_list: EditMachine::new(bump), + immediate_queue: Vec::new(), diffed: FxHashSet::default(), need_to_diff: FxHashSet::default(), - marked_for_removal: vec![], + // current_idx: None, } } - /// 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> { - self.diff_recursive(&old, &new, &mut 0) + pub fn consume(self) -> EditList<'a> { + self.change_list.emitter } - pub fn diff_recursive<'a, 'b>( + pub fn diff_node( &mut self, - old: &'a VNode, - new: &'a VNode, - cur_node_idx: &'b mut usize, - ) -> Vec> { - let mut patches = vec![]; - let mut replace = false; + old: &VNode<'a>, + new: &VNode<'a>, + // scope: Option, + ) { + log::debug!("old {:#?}", old); + log::debug!("new {:#?}", new); - // 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. + // Set it while diffing + // Reset it when finished diffing + // self.current_idx = scope; + /* + For each valid case, we "commit traversal", meaning we save this current position in the tree. + Then, we diff and queue an edit event (via chagelist). s single trees - when components show up, we save that traversal and then re-enter later. + When re-entering, we reuse the EditList in DiffState + */ match (old, new) { - // We're comparing two text nodes - (VNode::Text(old_text), VNode::Text(new_text)) => { + // This case occurs when two text nodes are generation + (VNode::Text(VText { text: old_text }), VNode::Text(VText { text: new_text })) => { if old_text != new_text { - patches.push(Patch::ChangeText(*cur_node_idx, &new_text)); + self.change_list.commit_traversal(); + self.change_list.set_text(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::>(); + // Definitely different, need to commit update + (VNode::Text(_), VNode::Element(_)) => { + // TODO: Hook up the events properly + // todo!("Hook up events registry"); + self.change_list.commit_traversal(); + // diff_support::create(cached_set, self.change_list, registry, new, cached_roots); + self.create(new); + // registry.remove_subtree(&old); + self.change_list.replace_with(); + } - let new_attrs = old_element - .attributes - .iter() - .map(|f| (f.name, f.value)) - .collect::>(); + // Definitely different, need to commit update + (VNode::Element(_), VNode::Text(_)) => { + self.change_list.commit_traversal(); + self.create(new); - let mut add_attributes = FxHashMap::<&'static str, &str>::default(); - // [("blah", "blah")] - // .into_iter() - // .map(|f| (f.0, f.1)) - // .collect::>(); + // create(cached_set, self.change_list, registry, new, cached_roots); + // Note: text nodes cannot have event listeners, so we don't need to + // remove the old node's listeners from our registry her. + self.change_list.replace_with(); + } + // compare elements + // if different, schedule different types of update + (VNode::Element(eold), VNode::Element(enew)) => { + // log::debug!("elements are different"); + // If the element type is completely different, the element needs to be re-rendered completely + if enew.tag_name != eold.tag_name || enew.namespace != eold.namespace { + self.change_list.commit_traversal(); + // create(cached_set, self.change_list, registry, new, cached_roots); + // registry.remove_subtree(&old); + self.change_list.replace_with(); + return; + } - // let mut add_attribute = HashMap::<&str, &str, FxBuildHasher>::new(); - let mut remove_attributes: Vec<&str> = vec![]; + self.diff_listeners(eold.listeners, enew.listeners); - // 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); - } + self.diff_attr(eold.attributes, enew.attributes, enew.namespace.is_some()); + + self.diff_children(eold.children, enew.children); + } + // No immediate change to dom. If props changed though, queue a "props changed" update + // However, mark these for a + (VNode::Component(_), VNode::Component(_)) => { + todo!("Usage of component VNode not currently supported"); + // // Both the new and old nodes are cached. + // (&NodeKind::Cached(ref new), &NodeKind::Cached(ref old)) => { + // cached_roots.insert(new.id); + // if new.id == old.id { + // // This is the same cached node, so nothing has changed! + // return; + // } + // let (new, new_template) = cached_set.get(new.id); + // let (old, old_template) = cached_set.get(old.id); + // if new_template == old_template { + // // If they are both using the same template, then just diff the + // // subtrees. + // diff(cached_set, change_list, registry, old, new, cached_roots); + // } else { + // // Otherwise, they are probably different enough that + // // re-constructing the subtree from scratch should be faster. + // // This doubly holds true if we have a new template. + // change_list.commit_traversal(); + // create_and_replace( + // cached_set, + // change_list, + // registry, + // new_template, + // old, + // new, + // cached_roots, + // ); + // } + // } + // queue a lifecycle event. + // no change + } + + // A component has been birthed! + // Queue its arrival + (_, VNode::Component(_)) => { + todo!("Usage of component VNode not currently supported"); + // // Old cached node and new non-cached node. Again, assume that they are + // // probably pretty different and create the new non-cached node afresh. + // (_, &NodeKind::Cached(_)) => { + // change_list.commit_traversal(); + // create(cached_set, change_list, registry, new, cached_roots); + // registry.remove_subtree(&old); + // change_list.replace_with(); + // } + // } + } + + // A component was removed :( + // Queue its removal + (VNode::Component(_), _) => { + // // New cached node when the old node was not cached. In this scenario, + // // we assume that they are pretty different, and it isn't worth diffing + // // the subtrees, so we just create the new cached node afresh. + // (&NodeKind::Cached(ref c), _) => { + // change_list.commit_traversal(); + // cached_roots.insert(c.id); + // let (new, new_template) = cached_set.get(c.id); + // create_and_replace( + // cached_set, + // change_list, + // registry, + // new_template, + // old, + // new, + // cached_roots, + // ); + // } + todo!("Usage of component VNode not currently supported"); + } + + // A suspended component appeared! + // Don't do much, just wait + (VNode::Suspended, _) | (_, VNode::Suspended) => { + // (VNode::Element(_), VNode::Suspended) => {} + // (VNode::Text(_), VNode::Suspended) => {} + // (VNode::Component(_), VNode::Suspended) => {} + // (VNode::Suspended, VNode::Element(_)) => {} + // (VNode::Suspended, VNode::Text(_)) => {} + // (VNode::Suspended, VNode::Suspended) => {} + // (VNode::Suspended, VNode::Component(_)) => {} + todo!("Suspended components not currently available") + } + } + // self.current_idx = None; + } + + // Diff event listeners between `old` and `new`. + // + // The listeners' node must be on top of the change list stack: + // + // [... node] + // + // The change list stack is left unchanged. + fn diff_listeners(&mut self, old: &[Listener<'a>], new: &[Listener<'a>]) { + if !old.is_empty() || !new.is_empty() { + self.change_list.commit_traversal(); + } + + 'outer1: for (l_idx, new_l) in new.iter().enumerate() { + // unsafe { + // Safety relies on removing `new_l` from the registry manually at + // the end of its lifetime. This happens below in the `'outer2` + // loop, and elsewhere in diffing when removing old dom trees. + // registry.add(new_l); + // } + let event_type = new_l.event; + + for old_l in old { + if new_l.event == old_l.event { + // if let Some(scope) = self.current_idx { + // let cb = CbIdx::from_gi_index(scope, l_idx); + self.change_list.update_event_listener(event_type, cb); + // } + + continue 'outer1; + } + } + + if let Some(scope) = self.current_idx { + let cb = CbIdx::from_gi_index(scope, l_idx); + self.change_list.new_event_listener(event_type, cb); + } + } + + 'outer2: for old_l in old { + // registry.remove(old_l); + + for new_l in new { + if new_l.event == old_l.event { + continue 'outer2; + } + } + self.change_list.remove_event_listener(old_l.event); + } + } + + // Diff a node's attributes. + // + // The attributes' node must be on top of the change list stack: + // + // [... node] + // + // The change list stack is left unchanged. + fn diff_attr( + &mut self, + old: &'a [Attribute<'a>], + new: &'a [Attribute<'a>], + is_namespaced: bool, + ) { + // Do O(n^2) passes to add/update and remove attributes, since + // there are almost always very few attributes. + 'outer: for new_attr in new { + if new_attr.is_volatile() { + self.change_list.commit_traversal(); + self.change_list + .set_attribute(new_attr.name, new_attr.value, is_namespaced); + } else { + for old_attr in old { + if old_attr.name == new_attr.name { + if old_attr.value != new_attr.value { + self.change_list.commit_traversal(); + self.change_list.set_attribute( + new_attr.name, + new_attr.value, + is_namespaced, + ); } - 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); + continue 'outer; } } + + self.change_list.commit_traversal(); + self.change_list + .set_attribute(new_attr.name, new_attr.value, is_namespaced); + } + } + + 'outer2: for old_attr in old { + for new_attr in new { + if old_attr.name == new_attr.name { + continue 'outer2; + } } - (VNode::Suspended, _) - | (_, VNode::Suspended) - | (VNode::Component(_), _) - | (_, VNode::Component(_)) => { - todo!("cant yet handle these two") - } + self.change_list.commit_traversal(); + self.change_list.remove_attribute(old_attr.name); + } + } - (VNode::Text(_), VNode::Element(_)) - | (VirtualNode::Element(_), VirtualNode::Text(_)) => { - unreachable!("Unequal variant discriminants should already have been handled"); + // Diff the given set of old and new children. + // + // The parent must be on top of the change list stack when this function is + // entered: + // + // [... parent] + // + // the change list stack is in the same state when this function returns. + fn diff_children(&mut self, old: &'a [VNode<'a>], new: &'a [VNode<'a>]) { + if new.is_empty() { + if !old.is_empty() { + self.change_list.commit_traversal(); + self.remove_all_children(old); } + return; + } + + if new.len() == 1 { + match (old.first(), &new[0]) { + ( + Some(&VNode::Text(VText { text: old_text })), + &VNode::Text(VText { text: new_text }), + ) if old_text == new_text => { + // Don't take this fast path... + } + + (_, &VNode::Text(VText { text })) => { + self.change_list.commit_traversal(); + self.change_list.set_text(text); + // for o in old { + // registry.remove_subtree(o); + // } + return; + } + + (_, _) => {} + } + } + + if old.is_empty() { + if !new.is_empty() { + self.change_list.commit_traversal(); + self.create_and_append_children(new); + } + return; + } + + let new_is_keyed = new[0].key().is_some(); + let old_is_keyed = old[0].key().is_some(); + + debug_assert!( + new.iter().all(|n| n.key().is_some() == new_is_keyed), + "all siblings must be keyed or all siblings must be non-keyed" + ); + debug_assert!( + old.iter().all(|o| o.key().is_some() == old_is_keyed), + "all siblings must be keyed or all siblings must be non-keyed" + ); + + if new_is_keyed && old_is_keyed { + let t = self.change_list.next_temporary(); + // diff_keyed_children(self.change_list, old, new); + // diff_keyed_children(self.change_list, old, new, cached_roots); + // diff_keyed_children(cached_set, self.change_list, registry, old, new, cached_roots); + self.change_list.set_next_temporary(t); + } else { + self.diff_non_keyed_children(old, new); + // diff_non_keyed_children(cached_set, change_list, registry, old, new, cached_roots); + } + } + + // Diffing "keyed" children. + // + // With keyed children, we care about whether we delete, move, or create nodes + // versus mutate existing nodes in place. Presumably there is some sort of CSS + // transition animation that makes the virtual DOM diffing algorithm + // observable. By specifying keys for nodes, we know which virtual DOM nodes + // must reuse (or not reuse) the same physical DOM nodes. + // + // This is loosely based on Inferno's keyed patching implementation. However, we + // have to modify the algorithm since we are compiling the diff down into change + // list instructions that will be executed later, rather than applying the + // changes to the DOM directly as we compare virtual DOMs. + // + // https://github.com/infernojs/inferno/blob/36fd96/packages/inferno/src/DOM/patching.ts#L530-L739 + // + // When entering this function, the parent must be on top of the change list + // stack: + // + // [... parent] + // + // Upon exiting, the change list stack is in the same state. + fn diff_keyed_children(&mut self, old: &[VNode<'a>], new: &[VNode<'a>]) { + // let DiffState { change_list, queue } = &*state; + + if cfg!(debug_assertions) { + let mut keys = fxhash::FxHashSet::default(); + let mut assert_unique_keys = |children: &[VNode]| { + keys.clear(); + for child in children { + let key = child.key(); + debug_assert!( + key.is_some(), + "if any sibling is keyed, all siblings must be keyed" + ); + keys.insert(key); + } + debug_assert_eq!( + children.len(), + keys.len(), + "keyed siblings must each have a unique key" + ); + }; + assert_unique_keys(old); + assert_unique_keys(new); + } + + // First up, we diff all the nodes with the same key at the beginning of the + // children. + // + // `shared_prefix_count` is the count of how many nodes at the start of + // `new` and `old` share the same keys. + let shared_prefix_count = match self.diff_keyed_prefix(old, new) { + KeyedPrefixResult::Finished => return, + KeyedPrefixResult::MoreWorkToDo(count) => count, }; - // new_root.create_element() - patches - } -} + match self.diff_keyed_prefix(old, new) { + KeyedPrefixResult::Finished => return, + KeyedPrefixResult::MoreWorkToDo(count) => count, + }; -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, - 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 + // Next, we find out how many of the nodes at the end of the children have + // the same key. We do _not_ diff them yet, since we want to emit the change + // list instructions such that they can be applied in a single pass over the + // DOM. Instead, we just save this information for later. + // + // `shared_suffix_count` is the count of how many nodes at the end of `new` + // and `old` share the same keys. + let shared_suffix_count = old[shared_prefix_count..] .iter() - .zip(expected_patches.iter()) - .for_each(|f| assert_eq!(compare_patch(f.0, f.1), true, "{}", description)); - } + .rev() + .zip(new[shared_prefix_count..].iter().rev()) + .take_while(|&(old, new)| old.key() == new.key()) + .count(); - // 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, + let old_shared_suffix_start = old.len() - shared_suffix_count; + let new_shared_suffix_start = new.len() - shared_suffix_count; - (Patch::TruncateChildren(_, _), Patch::TruncateChildren(_, _)) => true, - (Patch::TruncateChildren(_, _), _) => false, + // Ok, we now hopefully have a smaller range of children in the middle + // within which to re-order nodes with the same keys, remove old nodes with + // now-unused keys, and create new nodes with fresh keys. + self.diff_keyed_middle( + &old[shared_prefix_count..old_shared_suffix_start], + &new[shared_prefix_count..new_shared_suffix_start], + shared_prefix_count, + shared_suffix_count, + old_shared_suffix_start, + ); - (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, + // Finally, diff the nodes at the end of `old` and `new` that share keys. + let old_suffix = &old[old_shared_suffix_start..]; + let new_suffix = &new[new_shared_suffix_start..]; + debug_assert_eq!(old_suffix.len(), new_suffix.len()); + if !old_suffix.is_empty() { + self.diff_keyed_suffix(old_suffix, new_suffix, new_shared_suffix_start) } } - 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(); + // Diff the prefix of children in `new` and `old` that share the same keys in + // the same order. + // + // Upon entry of this function, the change list stack must be: + // + // [... parent] + // + // Upon exit, the change list stack is the same. + fn diff_keyed_prefix(&mut self, old: &[VNode<'a>], new: &[VNode<'a>]) -> KeyedPrefixResult { + self.change_list.go_down(); + let mut shared_prefix_count = 0; - let nodes1 = tree1(&bump); - let nodes2 = tree2(&bump); + for (i, (old, new)) in old.iter().zip(new.iter()).enumerate() { + if old.key() != new.key() { + break; + } - let mut machine = DiffMachine::new(); + self.change_list.go_to_sibling(i); - let patches = machine.diff(&nodes1, &nodes2); + self.diff_node(old, new); - 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"); - } - }); + shared_prefix_count += 1; + } + + // If that was all of the old children, then create and append the remaining + // new children and we're finished. + if shared_prefix_count == old.len() { + self.change_list.go_up(); + self.change_list.commit_traversal(); + self.create_and_append_children(&new[shared_prefix_count..]); + return KeyedPrefixResult::Finished; + } + + // And if that was all of the new children, then remove all of the remaining + // old children and we're finished. + if shared_prefix_count == new.len() { + self.change_list.go_to_sibling(shared_prefix_count); + self.change_list.commit_traversal(); + self.remove_self_and_next_siblings(&old[shared_prefix_count..]); + return KeyedPrefixResult::Finished; + } + + self.change_list.go_up(); + KeyedPrefixResult::MoreWorkToDo(shared_prefix_count) } - #[test] - fn example_diff() { - printdiff( - html! {
}, - html! {
"Hello world!"
}, - "demo the difference between two simple dom tree", + // The most-general, expensive code path for keyed children diffing. + // + // We find the longest subsequence within `old` of children that are relatively + // ordered the same way in `new` (via finding a longest-increasing-subsequence + // of the old child's index within `new`). The children that are elements of + // this subsequence will remain in place, minimizing the number of DOM moves we + // will have to do. + // + // Upon entry to this function, the change list stack must be: + // + // [... parent] + // + // Upon exit from this function, it will be restored to that same state. + fn diff_keyed_middle( + &mut self, + old: &[VNode<'a>], + mut new: &[VNode<'a>], + shared_prefix_count: usize, + shared_suffix_count: usize, + old_shared_suffix_start: usize, + ) { + // Should have already diffed the shared-key prefixes and suffixes. + debug_assert_ne!(new.first().map(|n| n.key()), old.first().map(|o| o.key())); + debug_assert_ne!(new.last().map(|n| n.key()), old.last().map(|o| o.key())); + + // The algorithm below relies upon using `u32::MAX` as a sentinel + // value, so if we have that many new nodes, it won't work. This + // check is a bit academic (hence only enabled in debug), since + // wasm32 doesn't have enough address space to hold that many nodes + // in memory. + debug_assert!(new.len() < u32::MAX as usize); + + // Map from each `old` node's key to its index within `old`. + let mut old_key_to_old_index = FxHashMap::default(); + old_key_to_old_index.reserve(old.len()); + old_key_to_old_index.extend(old.iter().enumerate().map(|(i, o)| (o.key(), i))); + + // The set of shared keys between `new` and `old`. + let mut shared_keys = FxHashSet::default(); + // Map from each index in `new` to the index of the node in `old` that + // has the same key. + let mut new_index_to_old_index = Vec::with_capacity(new.len()); + new_index_to_old_index.extend(new.iter().map(|n| { + let key = n.key(); + if let Some(&i) = old_key_to_old_index.get(&key) { + shared_keys.insert(key); + i + } else { + u32::MAX as usize + } + })); + + // If none of the old keys are reused by the new children, then we + // remove all the remaining old children and create the new children + // afresh. + if shared_suffix_count == 0 && shared_keys.is_empty() { + if shared_prefix_count == 0 { + self.change_list.commit_traversal(); + self.remove_all_children(old); + } else { + self.change_list.go_down_to_child(shared_prefix_count); + self.change_list.commit_traversal(); + self.remove_self_and_next_siblings(&old[shared_prefix_count..]); + } + + self.create_and_append_children(new); + + return; + } + + // Save each of the old children whose keys are reused in the new + // children. + let mut old_index_to_temp = vec![u32::MAX; old.len()]; + let mut start = 0; + loop { + let end = (start..old.len()) + .find(|&i| { + let key = old[i].key(); + !shared_keys.contains(&key) + }) + .unwrap_or(old.len()); + + if end - start > 0 { + self.change_list.commit_traversal(); + let mut t = self.change_list.save_children_to_temporaries( + shared_prefix_count + start, + shared_prefix_count + end, + ); + for i in start..end { + old_index_to_temp[i] = t; + t += 1; + } + } + + debug_assert!(end <= old.len()); + if end == old.len() { + break; + } else { + start = end + 1; + } + } + + // Remove any old children whose keys were not reused in the new + // children. Remove from the end first so that we don't mess up indices. + let mut removed_count = 0; + for (i, old_child) in old.iter().enumerate().rev() { + if !shared_keys.contains(&old_child.key()) { + // registry.remove_subtree(old_child); + // todo + self.change_list.commit_traversal(); + self.change_list.remove_child(i + shared_prefix_count); + removed_count += 1; + } + } + + // If there aren't any more new children, then we are done! + if new.is_empty() { + return; + } + + // The longest increasing subsequence within `new_index_to_old_index`. This + // is the longest sequence on DOM nodes in `old` that are relatively ordered + // correctly within `new`. We will leave these nodes in place in the DOM, + // and only move nodes that are not part of the LIS. This results in the + // maximum number of DOM nodes left in place, AKA the minimum number of DOM + // nodes moved. + let mut new_index_is_in_lis = FxHashSet::default(); + new_index_is_in_lis.reserve(new_index_to_old_index.len()); + let mut predecessors = vec![0; new_index_to_old_index.len()]; + let mut starts = vec![0; new_index_to_old_index.len()]; + longest_increasing_subsequence::lis_with( + &new_index_to_old_index, + &mut new_index_is_in_lis, + |a, b| a < b, + &mut predecessors, + &mut starts, ); - printdiff( - html! { -
- "Hello world!" -
- }, - html! { -
-
- "Hello world!" - "Hello world!" - "Hello world!" - "Hello world!" - "Hello world!" -
-
- }, - "demo the difference between two simple dom tree", - ); + // Now we will iterate from the end of the new children back to the + // beginning, diffing old children we are reusing and if they aren't in the + // LIS moving them to their new destination, or creating new children. Note + // that iterating in reverse order lets us use `Node.prototype.insertBefore` + // to move/insert children. + // + // But first, we ensure that we have a child on the change list stack that + // we can `insertBefore`. We handle this once before looping over `new` + // children, so that we don't have to keep checking on every loop iteration. + if shared_suffix_count > 0 { + // There is a shared suffix after these middle children. We will be + // inserting before that shared suffix, so add the first child of that + // shared suffix to the change list stack. + // + // [... parent] + self.change_list + .go_down_to_child(old_shared_suffix_start - removed_count); + // [... parent first_child_of_shared_suffix] + } else { + // There is no shared suffix coming after these middle children. + // Therefore we have to process the last child in `new` and move it to + // the end of the parent's children if it isn't already there. + let last_index = new.len() - 1; + // uhhhh why an unwrap? + let last = new.last().unwrap(); + // let last = new.last().unwrap_throw(); + new = &new[..new.len() - 1]; + if shared_keys.contains(&last.key()) { + let old_index = new_index_to_old_index[last_index]; + let temp = old_index_to_temp[old_index]; + // [... parent] + self.change_list.go_down_to_temp_child(temp); + // [... parent last] + self.diff_node(&old[old_index], last); + + if new_index_is_in_lis.contains(&last_index) { + // Don't move it, since it is already where it needs to be. + } else { + self.change_list.commit_traversal(); + // [... parent last] + self.change_list.append_child(); + // [... parent] + self.change_list.go_down_to_temp_child(temp); + // [... parent last] + } + } else { + self.change_list.commit_traversal(); + // [... parent] + self.create(last); + + // [... parent last] + self.change_list.append_child(); + // [... parent] + self.change_list.go_down_to_reverse_child(0); + // [... parent last] + } + } + + for (new_index, new_child) in new.iter().enumerate().rev() { + let old_index = new_index_to_old_index[new_index]; + if old_index == u32::MAX as usize { + debug_assert!(!shared_keys.contains(&new_child.key())); + self.change_list.commit_traversal(); + // [... parent successor] + self.create(new_child); + // [... parent successor new_child] + self.change_list.insert_before(); + // [... parent new_child] + } else { + debug_assert!(shared_keys.contains(&new_child.key())); + let temp = old_index_to_temp[old_index]; + debug_assert_ne!(temp, u32::MAX); + + if new_index_is_in_lis.contains(&new_index) { + // [... parent successor] + self.change_list.go_to_temp_sibling(temp); + // [... parent new_child] + } else { + self.change_list.commit_traversal(); + // [... parent successor] + self.change_list.push_temporary(temp); + // [... parent successor new_child] + self.change_list.insert_before(); + // [... parent new_child] + } + + self.diff_node(&old[old_index], new_child); + } + } + + // [... parent child] + self.change_list.go_up(); + // [... parent] + } + + // Diff the suffix of keyed children that share the same keys in the same order. + // + // The parent must be on the change list stack when we enter this function: + // + // [... parent] + // + // When this function exits, the change list stack remains the same. + fn diff_keyed_suffix( + &mut self, + old: &[VNode<'a>], + new: &[VNode<'a>], + new_shared_suffix_start: usize, + ) { + debug_assert_eq!(old.len(), new.len()); + debug_assert!(!old.is_empty()); + + // [... parent] + self.change_list.go_down(); + // [... parent new_child] + + for (i, (old_child, new_child)) in old.iter().zip(new.iter()).enumerate() { + self.change_list.go_to_sibling(new_shared_suffix_start + i); + self.diff_node(old_child, new_child); + } + + // [... parent] + self.change_list.go_up(); + } + + // Diff children that are not keyed. + // + // The parent must be on the top of the change list stack when entering this + // function: + // + // [... parent] + // + // the change list stack is in the same state when this function returns. + fn diff_non_keyed_children(&mut self, old: &'a [VNode<'a>], new: &'a [VNode<'a>]) { + // Handled these cases in `diff_children` before calling this function. + debug_assert!(!new.is_empty()); + debug_assert!(!old.is_empty()); + + // [... parent] + self.change_list.go_down(); + // [... parent child] + + for (i, (new_child, old_child)) in new.iter().zip(old.iter()).enumerate() { + // [... parent prev_child] + self.change_list.go_to_sibling(i); + // [... parent this_child] + self.diff_node(old_child, new_child); + } + + match old.len().cmp(&new.len()) { + Ordering::Greater => { + // [... parent prev_child] + self.change_list.go_to_sibling(new.len()); + // [... parent first_child_to_remove] + self.change_list.commit_traversal(); + // support::remove_self_and_next_siblings(state, &old[new.len()..]); + self.remove_self_and_next_siblings(&old[new.len()..]); + // [... parent] + } + Ordering::Less => { + // [... parent last_child] + self.change_list.go_up(); + // [... parent] + self.change_list.commit_traversal(); + self.create_and_append_children(&new[old.len()..]); + } + Ordering::Equal => { + // [... parent child] + self.change_list.go_up(); + // [... parent] + } + } + } + + // ====================== + // Support methods + // ====================== + + // Emit instructions to create the given virtual node. + // + // The change list stack may have any shape upon entering this function: + // + // [...] + // + // When this function returns, the new node is on top of the change list stack: + // + // [... node] + fn create(&mut self, node: &VNode<'a>) { + debug_assert!(self.change_list.traversal_is_committed()); + match node { + VNode::Text(VText { text }) => { + self.change_list.create_text_node(text); + } + VNode::Element(&VElement { + key: _, + tag_name, + listeners, + attributes, + children, + namespace, + }) => { + // log::info!("Creating {:#?}", node); + if let Some(namespace) = namespace { + self.change_list.create_element_ns(tag_name, namespace); + } else { + self.change_list.create_element(tag_name); + } + + listeners.iter().enumerate().for_each(|(id, listener)| { + if let Some(index) = self.current_idx { + self.change_list + .new_event_listener(listener.event, CbIdx::from_gi_index(index, id)); + } else { + // Don't panic + // Used for testing + log::trace!("Failed to set listener, create was not called in the context of the virtual dom"); + } + }); + // for l in listeners { + // unsafe { + // registry.add(l); + // } + // } + + for attr in attributes { + self.change_list + .set_attribute(&attr.name, &attr.value, namespace.is_some()); + } + + // Fast path: if there is a single text child, it is faster to + // create-and-append the text node all at once via setting the + // parent's `textContent` in a single change list instruction than + // to emit three instructions to (1) create a text node, (2) set its + // text content, and finally (3) append the text node to this + // parent. + if children.len() == 1 { + if let VNode::Text(VText { text }) = children[0] { + self.change_list.set_text(text); + return; + } + } + + for child in children { + self.create(child); + self.change_list.append_child(); + } + } + + /* + todo: integrate re-entrace + */ + // NodeKind::Cached(ref c) => { + // cached_roots.insert(c.id); + // let (node, template) = cached_set.get(c.id); + // if let Some(template) = template { + // create_with_template( + // cached_set, + // self.change_list, + // registry, + // template, + // node, + // cached_roots, + // ); + // } else { + // create(cached_set, change_list, registry, node, cached_roots); + // } + // } + VNode::Suspended => { + todo!("Creation of VNode::Suspended not yet supported") + } + VNode::Component(_) => { + todo!("Creation of VNode::Component not yet supported") + } + } + } + + // Remove all of a node's children. + // + // The change list stack must have this shape upon entry to this function: + // + // [... parent] + // + // When this function returns, the change list stack is in the same state. + pub fn remove_all_children(&mut self, old: &[VNode<'a>]) { + debug_assert!(self.change_list.traversal_is_committed()); + for _child in old { + // registry.remove_subtree(child); + } + // Fast way to remove all children: set the node's textContent to an empty + // string. + self.change_list.set_text(""); + } + + // Create the given children and append them to the parent node. + // + // The parent node must currently be on top of the change list stack: + // + // [... parent] + // + // When this function returns, the change list stack is in the same state. + pub fn create_and_append_children(&mut self, new: &[VNode<'a>]) { + debug_assert!(self.change_list.traversal_is_committed()); + for child in new { + self.create(child); + self.change_list.append_child(); + } + } + + // Remove the current child and all of its following siblings. + // + // The change list stack must have this shape upon entry to this function: + // + // [... parent child] + // + // After the function returns, the child is no longer on the change list stack: + // + // [... parent] + pub fn remove_self_and_next_siblings(&mut self, old: &[VNode<'a>]) { + debug_assert!(self.change_list.traversal_is_committed()); + for _child in old { + // registry.remove_subtree(child); + } + self.change_list.remove_self_and_next_siblings(); } } + +enum KeyedPrefixResult { + // Fast path: we finished diffing all the children just by looking at the + // prefix of shared keys! + Finished, + // There is more diffing work to do. Here is a count of how many children at + // the beginning of `new` and `old` we already processed. + MoreWorkToDo(usize), +} + +mod support { + + // // Get or create the template. + // // + // // Upon entering this function the change list stack may be in any shape: + // // + // // [...] + // // + // // When this function returns, it leaves a freshly cloned copy of the template + // // on the top of the change list stack: + // // + // // [... template] + // #[inline] + // pub fn get_or_create_template<'a>(// cached_set: &'a CachedSet, + // // change_list: &mut ChangeListBuilder, + // // registry: &mut EventsRegistry, + // // cached_roots: &mut FxHashSet, + // // template_id: CacheId, + // ) -> (&'a Node<'a>, bool) { + // let (template, template_template) = cached_set.get(template_id); + // debug_assert!( + // template_template.is_none(), + // "templates should not be templated themselves" + // ); + + // // If we haven't already created and saved the physical DOM subtree for this + // // template, do that now. + // if change_list.has_template(template_id) { + // // Clone the template and push it onto the stack. + // // + // // [...] + // change_list.push_template(template_id); + // // [... template] + + // (template, true) + // } else { + // // [...] + // create(cached_set, change_list, registry, template, cached_roots); + // // [... template] + // change_list.save_template(template_id); + // // [... template] + + // (template, false) + // } + // } + + // pub fn create_and_replace( + // cached_set: &CachedSet, + // change_list: &mut ChangeListBuilder, + // registry: &mut EventsRegistry, + // new_template: Option, + // old: &Node, + // new: &Node, + // cached_roots: &mut FxHashSet, + // ) { + // debug_assert!(change_list.traversal_is_committed()); + + // if let Some(template_id) = new_template { + // let (template, needs_listeners) = get_or_create_template( + // cached_set, + // change_list, + // registry, + // cached_roots, + // template_id, + // ); + // change_list.replace_with(); + + // let mut old_forcing = None; + // if needs_listeners { + // old_forcing = Some(change_list.push_force_new_listeners()); + // } + + // diff( + // cached_set, + // change_list, + // registry, + // template, + // new, + // cached_roots, + // ); + + // if let Some(old) = old_forcing { + // change_list.pop_force_new_listeners(old); + // } + + // change_list.commit_traversal(); + // } else { + // create(cached_set, change_list, registry, new, cached_roots); + // change_list.replace_with(); + // } + // registry.remove_subtree(old); + // } + + // pub fn create_with_template( + // cached_set: &CachedSet, + // change_list: &mut ChangeListBuilder, + // registry: &mut EventsRegistry, + // template_id: CacheId, + // node: &Node, + // cached_roots: &mut FxHashSet, + // ) { + // debug_assert!(change_list.traversal_is_committed()); + + // // [...] + // let (template, needs_listeners) = + // get_or_create_template(cached_set, change_list, registry, cached_roots, template_id); + // // [... template] + + // // Now diff the node with its template. + // // + // // We must force adding new listeners instead of updating existing ones, + // // since listeners don't get cloned in `cloneNode`. + // let mut old_forcing = None; + // if needs_listeners { + // old_forcing = Some(change_list.push_force_new_listeners()); + // } + + // diff( + // cached_set, + // change_list, + // registry, + // template, + // node, + // cached_roots, + // ); + + // if let Some(old) = old_forcing { + // change_list.pop_force_new_listeners(old); + // } + + // // Make sure that we come back up to the level we were at originally. + // change_list.commit_traversal(); + // } +} diff --git a/packages/core/src/dodriodiff.rs b/packages/core/src/dodriodiff.rs deleted file mode 100644 index 749a577b0..000000000 --- a/packages/core/src/dodriodiff.rs +++ /dev/null @@ -1,1165 +0,0 @@ -use bumpalo::Bump; -/// Diff the `old` node with the `new` node. Emits instructions to modify a -/// physical DOM node that reflects `old` into something that reflects `new`. -/// -/// Upon entry to this function, the physical DOM node must be on the top of the -/// change list stack: -/// -/// [... node] -/// -/// The change list stack is in the same state when this function exits. -/// -/// ---- -/// -/// There are more ways of increasing diff performance here that are currently not implemented. -/// Additionally, the caching mechanism has also been tweaked. -/// -/// Instead of having "cached" nodes, each component is, by default, a cached node. This leads to increased -/// memory overhead for large numbers of small components, but we can optimize this by tracking alloc size over time -/// and shrinking bumps down if possible. -/// -/// Additionally, clean up of these components is not done at diff time (though it should), but rather, the diffing -/// proprogates removal lifecycle events for affected components into the event queue. It's not imperative that these -/// are ran immediately, but it should be noted that cleanup of components might be able to emit changes. -/// -/// This diffing only ever occurs on a component-by-component basis (not entire trees at once). -/// -/// Currently, the listener situation is a bit broken. -/// We aren't removing listeners (choosing to leak them instead) :( -/// Eventually, we'll set things up so add/remove listener is an instruction again -/// -/// A major assumption of this diff algorithm when combined with the ChangeList is that the Changelist will be -/// fresh and the event queue is clean. This lets us continue to batch edits together under the same ChangeList -/// -/// More info on how to improve this diffing algorithm: -/// - https://hacks.mozilla.org/2019/03/fast-bump-allocated-virtual-doms-with-rust-and-wasm/ -use fxhash::{FxHashMap, FxHashSet}; -use generational_arena::Index; - -use crate::{ - changelist::{CbIdx, Edit, EditList, EditMachine}, - innerlude::{Attribute, Listener, Scope, VElement, VNode, VText}, - virtual_dom::LifecycleEvent, -}; - -use std::cmp::Ordering; - -/// The DiffState is a cursor internal to the VirtualDOM's diffing algorithm that allows persistence of state while -/// diffing trees of components. This means we can "re-enter" a subtree of a component by queuing a "NeedToDiff" event. -/// -/// By re-entering via NodeDiff, we can connect disparate edits together into a single EditList. This batching of edits -/// leads to very fast re-renders (all done in a single animation frame). -/// -/// It also means diffing two trees is only ever complex as diffing a single smaller tree, and then re-entering at a -/// different cursor position. -/// -/// The order of these re-entrances is stored in the DiffState itself. The DiffState comes pre-loaded with a set of components -/// that were modified by the eventtrigger. This prevents doubly evaluating components if they wereboth updated via -/// subscriptions and props changes. -pub struct DiffMachine<'a> { - pub change_list: EditMachine<'a>, - immediate_queue: Vec, - diffed: FxHashSet, - need_to_diff: FxHashSet, - - // Current scopes we're comparing - current_idx: Option, -} - -impl<'a> DiffMachine<'a> { - pub fn new(bump: &'a Bump) -> Self { - // log::debug!("starting diff machine"); - Self { - change_list: EditMachine::new(bump), - immediate_queue: Vec::new(), - diffed: FxHashSet::default(), - need_to_diff: FxHashSet::default(), - current_idx: None, - } - } - - pub fn consume(self) -> EditList<'a> { - self.change_list.emitter - } - - pub fn diff_node( - &mut self, - old: &VNode<'a>, - new: &VNode<'a>, - scope: Option, - ) { - log::debug!("old {:#?}", old); - log::debug!("new {:#?}", new); - - // Set it while diffing - // Reset it when finished diffing - self.current_idx = scope; - /* - For each valid case, we "commit traversal", meaning we save this current position in the tree. - Then, we diff and queue an edit event (via chagelist). s single trees - when components show up, we save that traversal and then re-enter later. - When re-entering, we reuse the EditList in DiffState - */ - match (old, new) { - // This case occurs when two text nodes are generation - (VNode::Text(VText { text: old_text }), VNode::Text(VText { text: new_text })) => { - if old_text != new_text { - self.change_list.commit_traversal(); - self.change_list.set_text(new_text); - } - } - - // Definitely different, need to commit update - (VNode::Text(_), VNode::Element(_)) => { - // TODO: Hook up the events properly - // todo!("Hook up events registry"); - self.change_list.commit_traversal(); - // diff_support::create(cached_set, self.change_list, registry, new, cached_roots); - self.create(new); - // registry.remove_subtree(&old); - self.change_list.replace_with(); - } - - // Definitely different, need to commit update - (VNode::Element(_), VNode::Text(_)) => { - self.change_list.commit_traversal(); - self.create(new); - - // create(cached_set, self.change_list, registry, new, cached_roots); - // Note: text nodes cannot have event listeners, so we don't need to - // remove the old node's listeners from our registry her. - self.change_list.replace_with(); - } - // compare elements - // if different, schedule different types of update - (VNode::Element(eold), VNode::Element(enew)) => { - // log::debug!("elements are different"); - // If the element type is completely different, the element needs to be re-rendered completely - if enew.tag_name != eold.tag_name || enew.namespace != eold.namespace { - self.change_list.commit_traversal(); - // create(cached_set, self.change_list, registry, new, cached_roots); - // registry.remove_subtree(&old); - self.change_list.replace_with(); - return; - } - - self.diff_listeners(eold.listeners, enew.listeners); - - self.diff_attr(eold.attributes, enew.attributes, enew.namespace.is_some()); - - self.diff_children(eold.children, enew.children); - } - // No immediate change to dom. If props changed though, queue a "props changed" update - // However, mark these for a - (VNode::Component(_), VNode::Component(_)) => { - todo!("Usage of component VNode not currently supported"); - // // Both the new and old nodes are cached. - // (&NodeKind::Cached(ref new), &NodeKind::Cached(ref old)) => { - // cached_roots.insert(new.id); - // if new.id == old.id { - // // This is the same cached node, so nothing has changed! - // return; - // } - // let (new, new_template) = cached_set.get(new.id); - // let (old, old_template) = cached_set.get(old.id); - // if new_template == old_template { - // // If they are both using the same template, then just diff the - // // subtrees. - // diff(cached_set, change_list, registry, old, new, cached_roots); - // } else { - // // Otherwise, they are probably different enough that - // // re-constructing the subtree from scratch should be faster. - // // This doubly holds true if we have a new template. - // change_list.commit_traversal(); - // create_and_replace( - // cached_set, - // change_list, - // registry, - // new_template, - // old, - // new, - // cached_roots, - // ); - // } - // } - // queue a lifecycle event. - // no change - } - - // A component has been birthed! - // Queue its arrival - (_, VNode::Component(_)) => { - todo!("Usage of component VNode not currently supported"); - // // Old cached node and new non-cached node. Again, assume that they are - // // probably pretty different and create the new non-cached node afresh. - // (_, &NodeKind::Cached(_)) => { - // change_list.commit_traversal(); - // create(cached_set, change_list, registry, new, cached_roots); - // registry.remove_subtree(&old); - // change_list.replace_with(); - // } - // } - } - - // A component was removed :( - // Queue its removal - (VNode::Component(_), _) => { - // // New cached node when the old node was not cached. In this scenario, - // // we assume that they are pretty different, and it isn't worth diffing - // // the subtrees, so we just create the new cached node afresh. - // (&NodeKind::Cached(ref c), _) => { - // change_list.commit_traversal(); - // cached_roots.insert(c.id); - // let (new, new_template) = cached_set.get(c.id); - // create_and_replace( - // cached_set, - // change_list, - // registry, - // new_template, - // old, - // new, - // cached_roots, - // ); - // } - todo!("Usage of component VNode not currently supported"); - } - - // A suspended component appeared! - // Don't do much, just wait - (VNode::Suspended, _) | (_, VNode::Suspended) => { - // (VNode::Element(_), VNode::Suspended) => {} - // (VNode::Text(_), VNode::Suspended) => {} - // (VNode::Component(_), VNode::Suspended) => {} - // (VNode::Suspended, VNode::Element(_)) => {} - // (VNode::Suspended, VNode::Text(_)) => {} - // (VNode::Suspended, VNode::Suspended) => {} - // (VNode::Suspended, VNode::Component(_)) => {} - todo!("Suspended components not currently available") - } - } - self.current_idx = None; - } - - // Diff event listeners between `old` and `new`. - // - // The listeners' node must be on top of the change list stack: - // - // [... node] - // - // The change list stack is left unchanged. - fn diff_listeners(&mut self, old: &[Listener<'a>], new: &[Listener<'a>]) { - if !old.is_empty() || !new.is_empty() { - self.change_list.commit_traversal(); - } - - 'outer1: for (l_idx, new_l) in new.iter().enumerate() { - // unsafe { - // Safety relies on removing `new_l` from the registry manually at - // the end of its lifetime. This happens below in the `'outer2` - // loop, and elsewhere in diffing when removing old dom trees. - // registry.add(new_l); - // } - let event_type = new_l.event; - - for old_l in old { - if new_l.event == old_l.event { - if let Some(scope) = self.current_idx { - let cb = CbIdx::from_gi_index(scope, l_idx); - self.change_list.update_event_listener(event_type, cb); - } - - continue 'outer1; - } - } - - if let Some(scope) = self.current_idx { - let cb = CbIdx::from_gi_index(scope, l_idx); - self.change_list.new_event_listener(event_type, cb); - } - } - - 'outer2: for old_l in old { - // registry.remove(old_l); - - for new_l in new { - if new_l.event == old_l.event { - continue 'outer2; - } - } - self.change_list.remove_event_listener(old_l.event); - } - } - - // Diff a node's attributes. - // - // The attributes' node must be on top of the change list stack: - // - // [... node] - // - // The change list stack is left unchanged. - fn diff_attr( - &mut self, - old: &'a [Attribute<'a>], - new: &'a [Attribute<'a>], - is_namespaced: bool, - ) { - // Do O(n^2) passes to add/update and remove attributes, since - // there are almost always very few attributes. - 'outer: for new_attr in new { - if new_attr.is_volatile() { - self.change_list.commit_traversal(); - self.change_list - .set_attribute(new_attr.name, new_attr.value, is_namespaced); - } else { - for old_attr in old { - if old_attr.name == new_attr.name { - if old_attr.value != new_attr.value { - self.change_list.commit_traversal(); - self.change_list.set_attribute( - new_attr.name, - new_attr.value, - is_namespaced, - ); - } - continue 'outer; - } - } - - self.change_list.commit_traversal(); - self.change_list - .set_attribute(new_attr.name, new_attr.value, is_namespaced); - } - } - - 'outer2: for old_attr in old { - for new_attr in new { - if old_attr.name == new_attr.name { - continue 'outer2; - } - } - - self.change_list.commit_traversal(); - self.change_list.remove_attribute(old_attr.name); - } - } - - // Diff the given set of old and new children. - // - // The parent must be on top of the change list stack when this function is - // entered: - // - // [... parent] - // - // the change list stack is in the same state when this function returns. - fn diff_children(&mut self, old: &'a [VNode<'a>], new: &'a [VNode<'a>]) { - if new.is_empty() { - if !old.is_empty() { - self.change_list.commit_traversal(); - self.remove_all_children(old); - } - return; - } - - if new.len() == 1 { - match (old.first(), &new[0]) { - ( - Some(&VNode::Text(VText { text: old_text })), - &VNode::Text(VText { text: new_text }), - ) if old_text == new_text => { - // Don't take this fast path... - } - - (_, &VNode::Text(VText { text })) => { - self.change_list.commit_traversal(); - self.change_list.set_text(text); - // for o in old { - // registry.remove_subtree(o); - // } - return; - } - - (_, _) => {} - } - } - - if old.is_empty() { - if !new.is_empty() { - self.change_list.commit_traversal(); - self.create_and_append_children(new); - } - return; - } - - let new_is_keyed = new[0].key().is_some(); - let old_is_keyed = old[0].key().is_some(); - - debug_assert!( - new.iter().all(|n| n.key().is_some() == new_is_keyed), - "all siblings must be keyed or all siblings must be non-keyed" - ); - debug_assert!( - old.iter().all(|o| o.key().is_some() == old_is_keyed), - "all siblings must be keyed or all siblings must be non-keyed" - ); - - if new_is_keyed && old_is_keyed { - let t = self.change_list.next_temporary(); - // diff_keyed_children(self.change_list, old, new); - // diff_keyed_children(self.change_list, old, new, cached_roots); - // diff_keyed_children(cached_set, self.change_list, registry, old, new, cached_roots); - self.change_list.set_next_temporary(t); - } else { - self.diff_non_keyed_children(old, new); - // diff_non_keyed_children(cached_set, change_list, registry, old, new, cached_roots); - } - } - - // Diffing "keyed" children. - // - // With keyed children, we care about whether we delete, move, or create nodes - // versus mutate existing nodes in place. Presumably there is some sort of CSS - // transition animation that makes the virtual DOM diffing algorithm - // observable. By specifying keys for nodes, we know which virtual DOM nodes - // must reuse (or not reuse) the same physical DOM nodes. - // - // This is loosely based on Inferno's keyed patching implementation. However, we - // have to modify the algorithm since we are compiling the diff down into change - // list instructions that will be executed later, rather than applying the - // changes to the DOM directly as we compare virtual DOMs. - // - // https://github.com/infernojs/inferno/blob/36fd96/packages/inferno/src/DOM/patching.ts#L530-L739 - // - // When entering this function, the parent must be on top of the change list - // stack: - // - // [... parent] - // - // Upon exiting, the change list stack is in the same state. - fn diff_keyed_children(&mut self, old: &[VNode<'a>], new: &[VNode<'a>]) { - // let DiffState { change_list, queue } = &*state; - - if cfg!(debug_assertions) { - let mut keys = fxhash::FxHashSet::default(); - let mut assert_unique_keys = |children: &[VNode]| { - keys.clear(); - for child in children { - let key = child.key(); - debug_assert!( - key.is_some(), - "if any sibling is keyed, all siblings must be keyed" - ); - keys.insert(key); - } - debug_assert_eq!( - children.len(), - keys.len(), - "keyed siblings must each have a unique key" - ); - }; - assert_unique_keys(old); - assert_unique_keys(new); - } - - // First up, we diff all the nodes with the same key at the beginning of the - // children. - // - // `shared_prefix_count` is the count of how many nodes at the start of - // `new` and `old` share the same keys. - let shared_prefix_count = match self.diff_keyed_prefix(old, new) { - KeyedPrefixResult::Finished => return, - KeyedPrefixResult::MoreWorkToDo(count) => count, - }; - - match self.diff_keyed_prefix(old, new) { - KeyedPrefixResult::Finished => return, - KeyedPrefixResult::MoreWorkToDo(count) => count, - }; - - // Next, we find out how many of the nodes at the end of the children have - // the same key. We do _not_ diff them yet, since we want to emit the change - // list instructions such that they can be applied in a single pass over the - // DOM. Instead, we just save this information for later. - // - // `shared_suffix_count` is the count of how many nodes at the end of `new` - // and `old` share the same keys. - let shared_suffix_count = old[shared_prefix_count..] - .iter() - .rev() - .zip(new[shared_prefix_count..].iter().rev()) - .take_while(|&(old, new)| old.key() == new.key()) - .count(); - - let old_shared_suffix_start = old.len() - shared_suffix_count; - let new_shared_suffix_start = new.len() - shared_suffix_count; - - // Ok, we now hopefully have a smaller range of children in the middle - // within which to re-order nodes with the same keys, remove old nodes with - // now-unused keys, and create new nodes with fresh keys. - self.diff_keyed_middle( - &old[shared_prefix_count..old_shared_suffix_start], - &new[shared_prefix_count..new_shared_suffix_start], - shared_prefix_count, - shared_suffix_count, - old_shared_suffix_start, - ); - - // Finally, diff the nodes at the end of `old` and `new` that share keys. - let old_suffix = &old[old_shared_suffix_start..]; - let new_suffix = &new[new_shared_suffix_start..]; - debug_assert_eq!(old_suffix.len(), new_suffix.len()); - if !old_suffix.is_empty() { - self.diff_keyed_suffix(old_suffix, new_suffix, new_shared_suffix_start) - } - } - - // Diff the prefix of children in `new` and `old` that share the same keys in - // the same order. - // - // Upon entry of this function, the change list stack must be: - // - // [... parent] - // - // Upon exit, the change list stack is the same. - fn diff_keyed_prefix(&mut self, old: &[VNode<'a>], new: &[VNode<'a>]) -> KeyedPrefixResult { - self.change_list.go_down(); - let mut shared_prefix_count = 0; - - for (i, (old, new)) in old.iter().zip(new.iter()).enumerate() { - if old.key() != new.key() { - break; - } - - self.change_list.go_to_sibling(i); - - self.diff_node(old, new, self.current_idx); - - shared_prefix_count += 1; - } - - // If that was all of the old children, then create and append the remaining - // new children and we're finished. - if shared_prefix_count == old.len() { - self.change_list.go_up(); - self.change_list.commit_traversal(); - self.create_and_append_children(&new[shared_prefix_count..]); - return KeyedPrefixResult::Finished; - } - - // And if that was all of the new children, then remove all of the remaining - // old children and we're finished. - if shared_prefix_count == new.len() { - self.change_list.go_to_sibling(shared_prefix_count); - self.change_list.commit_traversal(); - self.remove_self_and_next_siblings(&old[shared_prefix_count..]); - return KeyedPrefixResult::Finished; - } - - self.change_list.go_up(); - KeyedPrefixResult::MoreWorkToDo(shared_prefix_count) - } - - // The most-general, expensive code path for keyed children diffing. - // - // We find the longest subsequence within `old` of children that are relatively - // ordered the same way in `new` (via finding a longest-increasing-subsequence - // of the old child's index within `new`). The children that are elements of - // this subsequence will remain in place, minimizing the number of DOM moves we - // will have to do. - // - // Upon entry to this function, the change list stack must be: - // - // [... parent] - // - // Upon exit from this function, it will be restored to that same state. - fn diff_keyed_middle( - &mut self, - old: &[VNode<'a>], - mut new: &[VNode<'a>], - shared_prefix_count: usize, - shared_suffix_count: usize, - old_shared_suffix_start: usize, - ) { - // Should have already diffed the shared-key prefixes and suffixes. - debug_assert_ne!(new.first().map(|n| n.key()), old.first().map(|o| o.key())); - debug_assert_ne!(new.last().map(|n| n.key()), old.last().map(|o| o.key())); - - // The algorithm below relies upon using `u32::MAX` as a sentinel - // value, so if we have that many new nodes, it won't work. This - // check is a bit academic (hence only enabled in debug), since - // wasm32 doesn't have enough address space to hold that many nodes - // in memory. - debug_assert!(new.len() < u32::MAX as usize); - - // Map from each `old` node's key to its index within `old`. - let mut old_key_to_old_index = FxHashMap::default(); - old_key_to_old_index.reserve(old.len()); - old_key_to_old_index.extend(old.iter().enumerate().map(|(i, o)| (o.key(), i))); - - // The set of shared keys between `new` and `old`. - let mut shared_keys = FxHashSet::default(); - // Map from each index in `new` to the index of the node in `old` that - // has the same key. - let mut new_index_to_old_index = Vec::with_capacity(new.len()); - new_index_to_old_index.extend(new.iter().map(|n| { - let key = n.key(); - if let Some(&i) = old_key_to_old_index.get(&key) { - shared_keys.insert(key); - i - } else { - u32::MAX as usize - } - })); - - // If none of the old keys are reused by the new children, then we - // remove all the remaining old children and create the new children - // afresh. - if shared_suffix_count == 0 && shared_keys.is_empty() { - if shared_prefix_count == 0 { - self.change_list.commit_traversal(); - self.remove_all_children(old); - } else { - self.change_list.go_down_to_child(shared_prefix_count); - self.change_list.commit_traversal(); - self.remove_self_and_next_siblings(&old[shared_prefix_count..]); - } - - self.create_and_append_children(new); - - return; - } - - // Save each of the old children whose keys are reused in the new - // children. - let mut old_index_to_temp = vec![u32::MAX; old.len()]; - let mut start = 0; - loop { - let end = (start..old.len()) - .find(|&i| { - let key = old[i].key(); - !shared_keys.contains(&key) - }) - .unwrap_or(old.len()); - - if end - start > 0 { - self.change_list.commit_traversal(); - let mut t = self.change_list.save_children_to_temporaries( - shared_prefix_count + start, - shared_prefix_count + end, - ); - for i in start..end { - old_index_to_temp[i] = t; - t += 1; - } - } - - debug_assert!(end <= old.len()); - if end == old.len() { - break; - } else { - start = end + 1; - } - } - - // Remove any old children whose keys were not reused in the new - // children. Remove from the end first so that we don't mess up indices. - let mut removed_count = 0; - for (i, old_child) in old.iter().enumerate().rev() { - if !shared_keys.contains(&old_child.key()) { - // registry.remove_subtree(old_child); - // todo - self.change_list.commit_traversal(); - self.change_list.remove_child(i + shared_prefix_count); - removed_count += 1; - } - } - - // If there aren't any more new children, then we are done! - if new.is_empty() { - return; - } - - // The longest increasing subsequence within `new_index_to_old_index`. This - // is the longest sequence on DOM nodes in `old` that are relatively ordered - // correctly within `new`. We will leave these nodes in place in the DOM, - // and only move nodes that are not part of the LIS. This results in the - // maximum number of DOM nodes left in place, AKA the minimum number of DOM - // nodes moved. - let mut new_index_is_in_lis = FxHashSet::default(); - new_index_is_in_lis.reserve(new_index_to_old_index.len()); - let mut predecessors = vec![0; new_index_to_old_index.len()]; - let mut starts = vec![0; new_index_to_old_index.len()]; - longest_increasing_subsequence::lis_with( - &new_index_to_old_index, - &mut new_index_is_in_lis, - |a, b| a < b, - &mut predecessors, - &mut starts, - ); - - // Now we will iterate from the end of the new children back to the - // beginning, diffing old children we are reusing and if they aren't in the - // LIS moving them to their new destination, or creating new children. Note - // that iterating in reverse order lets us use `Node.prototype.insertBefore` - // to move/insert children. - // - // But first, we ensure that we have a child on the change list stack that - // we can `insertBefore`. We handle this once before looping over `new` - // children, so that we don't have to keep checking on every loop iteration. - if shared_suffix_count > 0 { - // There is a shared suffix after these middle children. We will be - // inserting before that shared suffix, so add the first child of that - // shared suffix to the change list stack. - // - // [... parent] - self.change_list - .go_down_to_child(old_shared_suffix_start - removed_count); - // [... parent first_child_of_shared_suffix] - } else { - // There is no shared suffix coming after these middle children. - // Therefore we have to process the last child in `new` and move it to - // the end of the parent's children if it isn't already there. - let last_index = new.len() - 1; - // uhhhh why an unwrap? - let last = new.last().unwrap(); - // let last = new.last().unwrap_throw(); - new = &new[..new.len() - 1]; - if shared_keys.contains(&last.key()) { - let old_index = new_index_to_old_index[last_index]; - let temp = old_index_to_temp[old_index]; - // [... parent] - self.change_list.go_down_to_temp_child(temp); - // [... parent last] - self.diff_node(&old[old_index], last, self.current_idx); - - if new_index_is_in_lis.contains(&last_index) { - // Don't move it, since it is already where it needs to be. - } else { - self.change_list.commit_traversal(); - // [... parent last] - self.change_list.append_child(); - // [... parent] - self.change_list.go_down_to_temp_child(temp); - // [... parent last] - } - } else { - self.change_list.commit_traversal(); - // [... parent] - self.create(last); - - // [... parent last] - self.change_list.append_child(); - // [... parent] - self.change_list.go_down_to_reverse_child(0); - // [... parent last] - } - } - - for (new_index, new_child) in new.iter().enumerate().rev() { - let old_index = new_index_to_old_index[new_index]; - if old_index == u32::MAX as usize { - debug_assert!(!shared_keys.contains(&new_child.key())); - self.change_list.commit_traversal(); - // [... parent successor] - self.create(new_child); - // [... parent successor new_child] - self.change_list.insert_before(); - // [... parent new_child] - } else { - debug_assert!(shared_keys.contains(&new_child.key())); - let temp = old_index_to_temp[old_index]; - debug_assert_ne!(temp, u32::MAX); - - if new_index_is_in_lis.contains(&new_index) { - // [... parent successor] - self.change_list.go_to_temp_sibling(temp); - // [... parent new_child] - } else { - self.change_list.commit_traversal(); - // [... parent successor] - self.change_list.push_temporary(temp); - // [... parent successor new_child] - self.change_list.insert_before(); - // [... parent new_child] - } - - self.diff_node(&old[old_index], new_child, self.current_idx); - } - } - - // [... parent child] - self.change_list.go_up(); - // [... parent] - } - - // Diff the suffix of keyed children that share the same keys in the same order. - // - // The parent must be on the change list stack when we enter this function: - // - // [... parent] - // - // When this function exits, the change list stack remains the same. - fn diff_keyed_suffix( - &mut self, - old: &[VNode<'a>], - new: &[VNode<'a>], - new_shared_suffix_start: usize, - ) { - debug_assert_eq!(old.len(), new.len()); - debug_assert!(!old.is_empty()); - - // [... parent] - self.change_list.go_down(); - // [... parent new_child] - - for (i, (old_child, new_child)) in old.iter().zip(new.iter()).enumerate() { - self.change_list.go_to_sibling(new_shared_suffix_start + i); - self.diff_node(old_child, new_child, self.current_idx); - } - - // [... parent] - self.change_list.go_up(); - } - - // Diff children that are not keyed. - // - // The parent must be on the top of the change list stack when entering this - // function: - // - // [... parent] - // - // the change list stack is in the same state when this function returns. - fn diff_non_keyed_children(&mut self, old: &'a [VNode<'a>], new: &'a [VNode<'a>]) { - // Handled these cases in `diff_children` before calling this function. - debug_assert!(!new.is_empty()); - debug_assert!(!old.is_empty()); - - // [... parent] - self.change_list.go_down(); - // [... parent child] - - for (i, (new_child, old_child)) in new.iter().zip(old.iter()).enumerate() { - // [... parent prev_child] - self.change_list.go_to_sibling(i); - // [... parent this_child] - self.diff_node(old_child, new_child, self.current_idx); - } - - match old.len().cmp(&new.len()) { - Ordering::Greater => { - // [... parent prev_child] - self.change_list.go_to_sibling(new.len()); - // [... parent first_child_to_remove] - self.change_list.commit_traversal(); - // support::remove_self_and_next_siblings(state, &old[new.len()..]); - self.remove_self_and_next_siblings(&old[new.len()..]); - // [... parent] - } - Ordering::Less => { - // [... parent last_child] - self.change_list.go_up(); - // [... parent] - self.change_list.commit_traversal(); - self.create_and_append_children(&new[old.len()..]); - } - Ordering::Equal => { - // [... parent child] - self.change_list.go_up(); - // [... parent] - } - } - } - - // ====================== - // Support methods - // ====================== - - // Emit instructions to create the given virtual node. - // - // The change list stack may have any shape upon entering this function: - // - // [...] - // - // When this function returns, the new node is on top of the change list stack: - // - // [... node] - fn create(&mut self, node: &VNode<'a>) { - debug_assert!(self.change_list.traversal_is_committed()); - match node { - VNode::Text(VText { text }) => { - self.change_list.create_text_node(text); - } - VNode::Element(&VElement { - key: _, - tag_name, - listeners, - attributes, - children, - namespace, - }) => { - // log::info!("Creating {:#?}", node); - if let Some(namespace) = namespace { - self.change_list.create_element_ns(tag_name, namespace); - } else { - self.change_list.create_element(tag_name); - } - - listeners.iter().enumerate().for_each(|(id, listener)| { - if let Some(index) = self.current_idx { - self.change_list - .new_event_listener(listener.event, CbIdx::from_gi_index(index, id)); - } else { - // Don't panic - // Used for testing - log::trace!("Failed to set listener, create was not called in the context of the virtual dom"); - } - }); - // for l in listeners { - // unsafe { - // registry.add(l); - // } - // } - - for attr in attributes { - self.change_list - .set_attribute(&attr.name, &attr.value, namespace.is_some()); - } - - // Fast path: if there is a single text child, it is faster to - // create-and-append the text node all at once via setting the - // parent's `textContent` in a single change list instruction than - // to emit three instructions to (1) create a text node, (2) set its - // text content, and finally (3) append the text node to this - // parent. - if children.len() == 1 { - if let VNode::Text(VText { text }) = children[0] { - self.change_list.set_text(text); - return; - } - } - - for child in children { - self.create(child); - self.change_list.append_child(); - } - } - - /* - todo: integrate re-entrace - */ - // NodeKind::Cached(ref c) => { - // cached_roots.insert(c.id); - // let (node, template) = cached_set.get(c.id); - // if let Some(template) = template { - // create_with_template( - // cached_set, - // self.change_list, - // registry, - // template, - // node, - // cached_roots, - // ); - // } else { - // create(cached_set, change_list, registry, node, cached_roots); - // } - // } - VNode::Suspended => { - todo!("Creation of VNode::Suspended not yet supported") - } - VNode::Component(_) => { - todo!("Creation of VNode::Component not yet supported") - } - } - } - - // Remove all of a node's children. - // - // The change list stack must have this shape upon entry to this function: - // - // [... parent] - // - // When this function returns, the change list stack is in the same state. - pub fn remove_all_children(&mut self, old: &[VNode<'a>]) { - debug_assert!(self.change_list.traversal_is_committed()); - for _child in old { - // registry.remove_subtree(child); - } - // Fast way to remove all children: set the node's textContent to an empty - // string. - self.change_list.set_text(""); - } - - // Create the given children and append them to the parent node. - // - // The parent node must currently be on top of the change list stack: - // - // [... parent] - // - // When this function returns, the change list stack is in the same state. - pub fn create_and_append_children(&mut self, new: &[VNode<'a>]) { - debug_assert!(self.change_list.traversal_is_committed()); - for child in new { - self.create(child); - self.change_list.append_child(); - } - } - - // Remove the current child and all of its following siblings. - // - // The change list stack must have this shape upon entry to this function: - // - // [... parent child] - // - // After the function returns, the child is no longer on the change list stack: - // - // [... parent] - pub fn remove_self_and_next_siblings(&mut self, old: &[VNode<'a>]) { - debug_assert!(self.change_list.traversal_is_committed()); - for _child in old { - // registry.remove_subtree(child); - } - self.change_list.remove_self_and_next_siblings(); - } -} - -enum KeyedPrefixResult { - // Fast path: we finished diffing all the children just by looking at the - // prefix of shared keys! - Finished, - // There is more diffing work to do. Here is a count of how many children at - // the beginning of `new` and `old` we already processed. - MoreWorkToDo(usize), -} - -mod support { - - // // Get or create the template. - // // - // // Upon entering this function the change list stack may be in any shape: - // // - // // [...] - // // - // // When this function returns, it leaves a freshly cloned copy of the template - // // on the top of the change list stack: - // // - // // [... template] - // #[inline] - // pub fn get_or_create_template<'a>(// cached_set: &'a CachedSet, - // // change_list: &mut ChangeListBuilder, - // // registry: &mut EventsRegistry, - // // cached_roots: &mut FxHashSet, - // // template_id: CacheId, - // ) -> (&'a Node<'a>, bool) { - // let (template, template_template) = cached_set.get(template_id); - // debug_assert!( - // template_template.is_none(), - // "templates should not be templated themselves" - // ); - - // // If we haven't already created and saved the physical DOM subtree for this - // // template, do that now. - // if change_list.has_template(template_id) { - // // Clone the template and push it onto the stack. - // // - // // [...] - // change_list.push_template(template_id); - // // [... template] - - // (template, true) - // } else { - // // [...] - // create(cached_set, change_list, registry, template, cached_roots); - // // [... template] - // change_list.save_template(template_id); - // // [... template] - - // (template, false) - // } - // } - - // pub fn create_and_replace( - // cached_set: &CachedSet, - // change_list: &mut ChangeListBuilder, - // registry: &mut EventsRegistry, - // new_template: Option, - // old: &Node, - // new: &Node, - // cached_roots: &mut FxHashSet, - // ) { - // debug_assert!(change_list.traversal_is_committed()); - - // if let Some(template_id) = new_template { - // let (template, needs_listeners) = get_or_create_template( - // cached_set, - // change_list, - // registry, - // cached_roots, - // template_id, - // ); - // change_list.replace_with(); - - // let mut old_forcing = None; - // if needs_listeners { - // old_forcing = Some(change_list.push_force_new_listeners()); - // } - - // diff( - // cached_set, - // change_list, - // registry, - // template, - // new, - // cached_roots, - // ); - - // if let Some(old) = old_forcing { - // change_list.pop_force_new_listeners(old); - // } - - // change_list.commit_traversal(); - // } else { - // create(cached_set, change_list, registry, new, cached_roots); - // change_list.replace_with(); - // } - // registry.remove_subtree(old); - // } - - // pub fn create_with_template( - // cached_set: &CachedSet, - // change_list: &mut ChangeListBuilder, - // registry: &mut EventsRegistry, - // template_id: CacheId, - // node: &Node, - // cached_roots: &mut FxHashSet, - // ) { - // debug_assert!(change_list.traversal_is_committed()); - - // // [...] - // let (template, needs_listeners) = - // get_or_create_template(cached_set, change_list, registry, cached_roots, template_id); - // // [... template] - - // // Now diff the node with its template. - // // - // // We must force adding new listeners instead of updating existing ones, - // // since listeners don't get cloned in `cloneNode`. - // let mut old_forcing = None; - // if needs_listeners { - // old_forcing = Some(change_list.push_force_new_listeners()); - // } - - // diff( - // cached_set, - // change_list, - // registry, - // template, - // node, - // cached_roots, - // ); - - // if let Some(old) = old_forcing { - // change_list.pop_force_new_listeners(old); - // } - - // // Make sure that we come back up to the level we were at originally. - // change_list.commit_traversal(); - // } -} diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 32547ddd1..37b6b08e2 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -4,29 +4,21 @@ //! 3rd party renderers are responsible for forming this virtual events from events //! //! The goal here is to provide a consistent event interface across all renderer types -use generational_arena::Index; -use crate::innerlude::CbIdx; +use crate::innerlude::ScopeIdx; #[derive(Debug)] pub struct EventTrigger { - pub component_id: Index, + pub component_id: ScopeIdx, pub listener_id: usize, pub event: VirtualEvent, } impl EventTrigger { - pub fn new(event: VirtualEvent, cb: CbIdx) -> Self { - let CbIdx { - gi_id, - gi_gen, - listener_idx, - } = cb; - - let component_id = Index::from_raw_parts(gi_id, gi_gen); + pub fn new(event: VirtualEvent, scope: ScopeIdx, id: usize) -> Self { Self { - component_id, - listener_id: listener_idx, + component_id: scope, + listener_id: id, event, } } diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index ff1faf16a..8748a0146 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -65,20 +65,21 @@ //! - dioxus-liveview (SSR + StringRenderer) //! -pub mod changelist; // An "edit phase" described by transitions and edit operations pub mod component; // Logic for extending FC pub mod context; // Logic for providing hook + context functionality to user components -pub mod debug_renderer; // Test harness for validating that lifecycles and diffs work appropriately - // pub mod diff; - // pub mod patch; // The diffing algorithm that builds the ChangeList -pub mod dodriodiff; // The diffing algorithm that builds the ChangeList +pub mod debug_renderer; +pub mod patch; // An "edit phase" described by transitions and edit operations // Test harness for validating that lifecycles and diffs work appropriately + // pub mod diff; + // pub mod patch; // The diffing algorithm that builds the ChangeList +pub mod diff; +// the diffing algorithm that builds the ChangeList pub mod error; // Error type we expose to the renderers pub mod events; // Manages the synthetic event API pub mod hooks; // Built-in hooks pub mod nodebuilder; // Logic for building VNodes with a direct syntax pub mod nodes; // Logic for the VNodes pub mod scope; // Logic for single components -pub mod validation; // Logic for validating trees + // pub mod validation; // Logic for validating trees pub mod virtual_dom; // Most fun logic starts here, manages the lifecycle and suspense pub mod builder { @@ -96,7 +97,12 @@ pub(crate) mod innerlude { pub(crate) use crate::virtual_dom::VirtualDom; pub(crate) use nodes::*; - pub use crate::changelist::CbIdx; + pub use crate::component::ScopeIdx; + pub use crate::diff::DiffMachine; + pub use crate::events::EventTrigger; + pub use crate::patch::{EditList, EditMachine}; + // pub use crate::patchdx; + // pub use crate::patchtList; // pub use nodes::iterables::IterableNodes; /// This type alias is an internal way of abstracting over the static functions that represent components. @@ -138,6 +144,7 @@ pub mod prelude { // expose our bumpalo type pub use bumpalo; + pub use bumpalo::Bump; // Re-export the FC macro pub use crate as dioxus; @@ -146,8 +153,8 @@ pub mod prelude { pub use dioxus_core_macro::format_args_f; pub use dioxus_core_macro::{fc, html, rsx}; - // pub use crate::diff::DiffMachine; - pub use crate::dodriodiff::DiffMachine; + pub use crate::component::ScopeIdx; + pub use crate::diff::DiffMachine; pub use crate::hooks::*; } diff --git a/packages/core/src/nodebuilder.rs b/packages/core/src/nodebuilder.rs index e6b023e08..c6228d9f8 100644 --- a/packages/core/src/nodebuilder.rs +++ b/packages/core/src/nodebuilder.rs @@ -333,7 +333,7 @@ where /// /// // A button that does something when clicked! /// let my_button = button(&b) - /// .on("click", |root, vdom, event| { + /// .on("click", |event| { /// // ... /// }) /// .finish(); diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 1e6cd21c0..3b44837c1 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -94,7 +94,8 @@ mod vnode { } mod velement { - use crate::events::VirtualEvent; + + // use crate::{events::VirtualEvent, innerlude::CbIdx}; use super::*; use std::fmt::Debug; @@ -179,6 +180,11 @@ mod velement { } } + pub struct ListenerHandle { + pub event: &'static str, + pub idx: CbIdx, + } + /// An event listener. pub struct Listener<'bump> { /// The type of event to listen for. diff --git a/packages/core/src/patch.rs b/packages/core/src/patch.rs index e91e4435d..da4620e0c 100644 --- a/packages/core/src/patch.rs +++ b/packages/core/src/patch.rs @@ -1,80 +1,734 @@ -use fxhash::FxHashMap; +//! Changelist +//! ---------- +//! +//! This module exposes the "changelist" object which allows 3rd party implementors to handle diffs to the virtual dom. +//! +//! # Design +//! --- +//! In essence, the changelist object connects a diff of two vdoms to the actual edits required to update the output renderer. +//! +//! This abstraction relies on the assumption that the final renderer accepts a tree of elements. For most target platforms, +//! this is an appropriate abstraction . +//! +//! During the diff phase, the change list is built. Once the diff phase is over, the change list is finished and returned back +//! to the renderer. The renderer is responsible for propogating the updates to the final display. +//! +//! Because the change list references data internal to the vdom, it needs to be consumed by the renderer before the vdom +//! can continue to work. This means once a change list is generated, it should be consumed as fast as possible, otherwise the +//! dom will be blocked from progressing. This is enforced by lifetimes on the returend changelist object. +//! +//! -use crate::innerlude::{VNode, VText}; +use bumpalo::Bump; -/// A Patch encodes an operation that modifies a real DOM element. +use crate::innerlude::{Listener, ScopeIdx}; +use serde::{Deserialize, Serialize}; +/// The `Edit` represents a single modifcation of the renderer tree. /// -/// To update the real DOM that a user sees you'll want to first diff your -/// old virtual dom and new virtual dom. /// -/// This diff operation will generate `Vec` with zero or more patches that, when -/// applied to your real DOM, will make your real DOM look like your new virtual dom. /// -/// Each Patch has a u32 node index that helps us identify the real DOM node that it applies to. /// -/// Our old virtual dom's nodes are indexed depth first, as shown in this illustration -/// (0 being the root node, 1 being it's first child, 2 being it's first child's first child). /// -/// ```text -/// .─. -/// ( 0 ) -/// `┬' -/// ┌────┴──────┐ -/// │ │ -/// ▼ ▼ -/// .─. .─. -/// ( 1 ) ( 4 ) -/// `┬' `─' -/// ┌────┴───┐ ├─────┬─────┐ -/// │ │ │ │ │ -/// ▼ ▼ ▼ ▼ ▼ -/// .─. .─. .─. .─. .─. -/// ( 2 ) ( 3 ) ( 5 ) ( 6 ) ( 7 ) -/// `─' `─' `─' `─' `─' -/// ``` /// -/// The patching process is tested in a real browser in crates/virtual-dom-rs/tests/diff_patch.rs - -// #[derive(serde::Serialize, serde::Deserialize)] -pub enum Patch<'a> { - /// Append a vector of child nodes to a parent node id. - AppendChildren(NodeIdx, Vec<&'a VNode<'a>>), - - /// For a `node_i32`, remove all children besides the first `len` - TruncateChildren(NodeIdx, usize), - - /// Replace a node with another node. This typically happens when a node's tag changes. - /// ex:
becomes - Replace(NodeIdx, &'a VNode<'a>), - - /// Add attributes that the new node has that the old node does not - AddAttributes(NodeIdx, FxHashMap<&'a str, &'a str>), - - /// Remove attributes that the old node had that the new node doesn't - RemoveAttributes(NodeIdx, Vec<&'a str>), - - /// Change the text of a Text node. - ChangeText(NodeIdx, &'a VText<'a>), +/// +/// +/// todo@ jon: allow serde to be optional +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum Edit<'d> { + SetText { text: &'d str }, + RemoveSelfAndNextSiblings {}, + ReplaceWith, + SetAttribute { name: &'d str, value: &'d str }, + RemoveAttribute { name: &'d str }, + PushReverseChild { n: u32 }, + PopPushChild { n: u32 }, + Pop, + AppendChild, + CreateTextNode { text: &'d str }, + CreateElement { tag_name: &'d str }, + NewEventListener { event_type: &'d str, s: ScopeIdx }, + UpdateEventListener { event_type: &'d str, s: ScopeIdx }, + RemoveEventListener { event_type: &'d str }, + CreateElementNs { tag_name: &'d str, ns: &'d str }, + SaveChildrenToTemporaries { temp: u32, start: u32, end: u32 }, + PushChild { n: u32 }, + PushTemporary { temp: u32 }, + InsertBefore, + PopPushReverseChild { n: u32 }, + RemoveChild { n: u32 }, + SetClass { class_name: &'d str }, + PushKnown { node: ScopeIdx }, } -type NodeIdx = usize; +pub type EditList<'src> = Vec>; -impl<'a> Patch<'a> { - /// Every Patch is meant to be applied to a specific node within the DOM. Get the - /// index of the DOM node that this patch should apply to. DOM nodes are indexed - /// depth first with the root node in the tree having index 0. - pub fn node_idx(&self) -> usize { - match self { - Patch::AppendChildren(node_idx, _) => *node_idx, - Patch::TruncateChildren(node_idx, _) => *node_idx, - Patch::Replace(node_idx, _) => *node_idx, - Patch::AddAttributes(node_idx, _) => *node_idx, - Patch::RemoveAttributes(node_idx, _) => *node_idx, - Patch::ChangeText(node_idx, _) => *node_idx, +pub struct EditMachine<'src> { + pub traversal: Traversal, + next_temporary: u32, + forcing_new_listeners: bool, + + pub emitter: EditList<'src>, +} + +impl<'b> EditMachine<'b> { + pub fn new(_bump: &'b Bump) -> Self { + Self { + traversal: Traversal::new(), + next_temporary: 0, + forcing_new_listeners: false, + emitter: EditList::default(), + } + } + + /// Traversal methods. + pub fn go_down(&mut self) { + self.traversal.down(); + } + + pub fn go_down_to_child(&mut self, index: usize) { + self.traversal.down(); + self.traversal.sibling(index); + } + + pub fn go_down_to_reverse_child(&mut self, index: usize) { + self.traversal.down(); + self.traversal.reverse_sibling(index); + } + + pub fn go_up(&mut self) { + self.traversal.up(); + } + + pub fn go_to_sibling(&mut self, index: usize) { + self.traversal.sibling(index); + } + + pub fn go_to_temp_sibling(&mut self, temp: u32) { + self.traversal.up(); + self.traversal.down_to_temp(temp); + } + + pub fn go_down_to_temp_child(&mut self, temp: u32) { + self.traversal.down_to_temp(temp); + } + + pub fn commit_traversal(&mut self) { + if self.traversal.is_committed() { + log::debug!("Traversal already committed"); + return; + } + + for mv in self.traversal.commit() { + match mv { + MoveTo::Parent => { + log::debug!("emit: pop"); + self.emitter.push(Edit::Pop {}); + // self.emitter.pop(); + } + MoveTo::Child(n) => { + log::debug!("emit: push_child({})", n); + self.emitter.push(Edit::PushChild { n }); + } + MoveTo::ReverseChild(n) => { + log::debug!("emit: push_reverse_child({})", n); + self.emitter.push(Edit::PushReverseChild { n }); + // self.emitter.push_reverse_child(n); + } + MoveTo::Sibling(n) => { + log::debug!("emit: pop_push_child({})", n); + self.emitter.push(Edit::PopPushChild { n }); + // self.emitter.pop_push_child(n); + } + MoveTo::ReverseSibling(n) => { + log::debug!("emit: pop_push_reverse_child({})", n); + self.emitter.push(Edit::PopPushReverseChild { n }); + } + MoveTo::TempChild(temp) => { + log::debug!("emit: push_temporary({})", temp); + self.emitter.push(Edit::PushTemporary { temp }); + // self.emitter.push_temporary(temp); + } + } + } + } + + pub fn traversal_is_committed(&self) -> bool { + self.traversal.is_committed() + } +} + +impl<'a> EditMachine<'a> { + pub fn next_temporary(&self) -> u32 { + self.next_temporary + } + + pub fn set_next_temporary(&mut self, next_temporary: u32) { + self.next_temporary = next_temporary; + } + + pub fn save_children_to_temporaries(&mut self, start: usize, end: usize) -> u32 { + debug_assert!(self.traversal_is_committed()); + debug_assert!(start < end); + let temp_base = self.next_temporary; + // debug!( + // "emit: save_children_to_temporaries({}, {}, {})", + // temp_base, start, end + // ); + self.next_temporary = temp_base + (end - start) as u32; + self.emitter.push(Edit::SaveChildrenToTemporaries { + temp: temp_base, + start: start as u32, + end: end as u32, + }); + temp_base + } + + pub fn push_temporary(&mut self, temp: u32) { + debug_assert!(self.traversal_is_committed()); + // debug!("emit: push_temporary({})", temp); + self.emitter.push(Edit::PushTemporary { temp }); + // self.emitter.push_temporary(temp); + } + + pub fn remove_child(&mut self, child: usize) { + debug_assert!(self.traversal_is_committed()); + // debug!("emit: remove_child({})", child); + // self.emitter.remove_child(child as u32); + self.emitter.push(Edit::RemoveChild { n: child as u32 }) + } + + pub fn insert_before(&mut self) { + debug_assert!(self.traversal_is_committed()); + // debug!("emit: insert_before()"); + // self.emitter.insert_before(); + self.emitter.push(Edit::InsertBefore {}) + } + + pub fn ensure_string(&mut self, _string: &str) -> StringKey { + todo!() + // self.strings.ensure_string(string, &self.emitter) + } + + pub fn set_text(&mut self, text: &'a str) { + debug_assert!(self.traversal_is_committed()); + // debug!("emit: set_text({:?})", text); + // self.emitter.set_text(text); + self.emitter.push(Edit::SetText { text }); + // .set_text(text.as_ptr() as u32, text.len() as u32); + } + + pub fn remove_self_and_next_siblings(&mut self) { + debug_assert!(self.traversal_is_committed()); + // debug!("emit: remove_self_and_next_siblings()"); + self.emitter.push(Edit::RemoveSelfAndNextSiblings {}); + // self.emitter.remove_self_and_next_siblings(); + } + + pub fn replace_with(&mut self) { + debug_assert!(self.traversal_is_committed()); + // debug!("emit: replace_with()"); + self.emitter.push(Edit::ReplaceWith {}); + // self.emitter.replace_with(); + } + + pub fn set_attribute(&mut self, name: &'a str, value: &'a str, is_namespaced: bool) { + debug_assert!(self.traversal_is_committed()); + // todo!() + if name == "class" && !is_namespaced { + // let class_id = self.ensure_string(value); + // let class_id = self.ensure_string(value); + // debug!("emit: set_class({:?})", value); + // self.emitter.set_class(class_id.into()); + self.emitter.push(Edit::SetClass { class_name: value }); + } else { + self.emitter.push(Edit::SetAttribute { name, value }); + // let name_id = self.ensure_string(name); + // let value_id = self.ensure_string(value); + // debug!("emit: set_attribute({:?}, {:?})", name, value); + // self.state + // .emitter + // .set_attribute(name_id.into(), value_id.into()); + } + } + + pub fn remove_attribute(&mut self, name: &'a str) { + // todo!("figure out how to get this working with ensure string"); + self.emitter.push(Edit::RemoveAttribute { name }); + // self.emitter.remove_attribute(name); + // debug_assert!(self.traversal_is_committed()); + // // debug!("emit: remove_attribute({:?})", name); + // let name_id = self.ensure_string(name); + // self.emitter.remove_attribute(name_id.into()); + } + + pub fn append_child(&mut self) { + debug_assert!(self.traversal_is_committed()); + // debug!("emit: append_child()"); + self.emitter.push(Edit::AppendChild {}); + // self.emitter.append_child(); + } + + pub fn create_text_node(&mut self, text: &'a str) { + debug_assert!(self.traversal_is_committed()); + // debug!("emit: create_text_node({:?})", text); + // self.emitter.create_text_node(text); + self.emitter.push(Edit::CreateTextNode { text }); + } + + pub fn create_element(&mut self, tag_name: &'a str) { + // debug_assert!(self.traversal_is_committed()); + // debug!("emit: create_element({:?})", tag_name); + // let tag_name_id = self.ensure_string(tag_name); + self.emitter.push(Edit::CreateElement { tag_name }); + // self.emitter.create_element(tag_name); + // self.emitter.create_element(tag_name_id.into()); + } + + pub fn create_element_ns(&mut self, tag_name: &'a str, ns: &'a str) { + debug_assert!(self.traversal_is_committed()); + // debug!("emit: create_element_ns({:?}, {:?})", tag_name, ns); + // let tag_name_id = self.ensure_string(tag_name); + // let ns_id = self.ensure_string(ns); + // self.emitter.create_element_ns(tag_name, ns); + self.emitter.push(Edit::CreateElementNs { tag_name, ns }); + // self.emitter + // .create_element_ns(tag_name_id.into(), ns_id.into()); + } + + pub fn push_force_new_listeners(&mut self) -> bool { + let old = self.forcing_new_listeners; + self.forcing_new_listeners = true; + old + } + + pub fn pop_force_new_listeners(&mut self, previous: bool) { + debug_assert!(self.forcing_new_listeners); + self.forcing_new_listeners = previous; + } + + pub fn new_event_listener(&mut self, event: &'a str, idx: CbIdx) { + debug_assert!(self.traversal_is_committed()); + self.emitter.push(Edit::NewEventListener { + event_type: event, + s: idx, + }); + // todo!("Event listener not wired up yet"); + // log::debug!("emit: new_event_listener({:?})", listener); + // let (a, b) = listener.get_callback_parts(); + // debug_assert!(a != 0); + // // let event_id = self.ensure_string(listener.event); + // self.emitter.new_event_listener(listener.event.into(), a, b); + } + + pub fn update_event_listener(&mut self, event: &'a str, idx: CbIdx) { + debug_assert!(self.traversal_is_committed()); + if self.forcing_new_listeners { + self.new_event_listener(event, idx); + return; + } + + self.emitter.push(Edit::NewEventListener { + event_type: event, + s: idx, + }); + + // log::debug!("emit: update_event_listener({:?})", listener); + // // todo!("Event listener not wired up yet"); + // let (a, b) = listener.get_callback_parts(); + // debug_assert!(a != 0); + // self.emitter.push(Edit::UpdateEventListener { + // event_type: listener.event.into(), + // a, + // b, + // }); + // self.emitter.update_event_listener(event_id.into(), a, b); + } + + pub fn remove_event_listener(&mut self, event: &'a str) { + debug_assert!(self.traversal_is_committed()); + self.emitter + .push(Edit::RemoveEventListener { event_type: event }); + // debug!("emit: remove_event_listener({:?})", event); + // let _event_id = self.ensure_string(event); + // todo!("Event listener not wired up yet"); + // self.emitter.remove_event_listener(event_id.into()); + } + + // #[inline] + // pub fn has_template(&mut self, id: CacheId) -> bool { + // self.templates.contains(&id) + // } + + // pub fn save_template(&mut self, id: CacheId) { + // debug_assert!(self.traversal_is_committed()); + // debug_assert!(!self.has_template(id)); + // // debug!("emit: save_template({:?})", id); + // self.templates.insert(id); + // self.emitter.save_template(id.into()); + // } + + // pub fn push_template(&mut self, id: CacheId) { + // debug_assert!(self.traversal_is_committed()); + // debug_assert!(self.has_template(id)); + // // debug!("emit: push_template({:?})", id); + // self.emitter.push_template(id.into()); + // } +} + +// Keeps track of where we are moving in a DOM tree, and shortens traversal +// paths between mutations to their minimal number of operations. + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MoveTo { + /// Move from the current node up to its parent. + Parent, + + /// Move to the current node's n^th child. + Child(u32), + + /// Move to the current node's n^th from last child. + ReverseChild(u32), + + /// Move to the n^th sibling. Not relative from the current + /// location. Absolute indexed within all of the current siblings. + Sibling(u32), + + /// Move to the n^th from last sibling. Not relative from the current + /// location. Absolute indexed within all of the current siblings. + ReverseSibling(u32), + + /// Move down to the given saved temporary child. + TempChild(u32), +} + +#[derive(Debug)] +pub struct Traversal { + uncommitted: Vec, +} + +impl Traversal { + /// Construct a new `Traversal` with its internal storage backed by the + /// given bump arena. + pub fn new() -> Traversal { + Traversal { + uncommitted: Vec::with_capacity(32), + } + } + + /// Move the traversal up in the tree. + pub fn up(&mut self) { + match self.uncommitted.last() { + Some(MoveTo::Sibling(_)) | Some(MoveTo::ReverseSibling(_)) => { + self.uncommitted.pop(); + self.uncommitted.push(MoveTo::Parent); + } + Some(MoveTo::TempChild(_)) | Some(MoveTo::Child(_)) | Some(MoveTo::ReverseChild(_)) => { + self.uncommitted.pop(); + // And we're back at the parent. + } + _ => { + self.uncommitted.push(MoveTo::Parent); + } + } + } + + /// Move the traversal down in the tree to the first child of the current + /// node. + pub fn down(&mut self) { + if let Some(&MoveTo::Parent) = self.uncommitted.last() { + self.uncommitted.pop(); + self.sibling(0); + } else { + self.uncommitted.push(MoveTo::Child(0)); + } + } + + /// Move the traversal to the n^th sibling. + pub fn sibling(&mut self, index: usize) { + let index = index as u32; + match self.uncommitted.last_mut() { + Some(MoveTo::Sibling(ref mut n)) | Some(MoveTo::Child(ref mut n)) => { + *n = index; + } + Some(MoveTo::ReverseSibling(_)) => { + self.uncommitted.pop(); + self.uncommitted.push(MoveTo::Sibling(index)); + } + Some(MoveTo::TempChild(_)) | Some(MoveTo::ReverseChild(_)) => { + self.uncommitted.pop(); + self.uncommitted.push(MoveTo::Child(index)) + } + _ => { + self.uncommitted.push(MoveTo::Sibling(index)); + } + } + } + + /// Move the the n^th from last sibling. + pub fn reverse_sibling(&mut self, index: usize) { + let index = index as u32; + match self.uncommitted.last_mut() { + Some(MoveTo::ReverseSibling(ref mut n)) | Some(MoveTo::ReverseChild(ref mut n)) => { + *n = index; + } + Some(MoveTo::Sibling(_)) => { + self.uncommitted.pop(); + self.uncommitted.push(MoveTo::ReverseSibling(index)); + } + Some(MoveTo::TempChild(_)) | Some(MoveTo::Child(_)) => { + self.uncommitted.pop(); + self.uncommitted.push(MoveTo::ReverseChild(index)) + } + _ => { + self.uncommitted.push(MoveTo::ReverseSibling(index)); + } + } + } + + /// Go to the given saved temporary. + pub fn down_to_temp(&mut self, temp: u32) { + match self.uncommitted.last() { + Some(MoveTo::Sibling(_)) | Some(MoveTo::ReverseSibling(_)) => { + self.uncommitted.pop(); + } + Some(MoveTo::Parent) + | Some(MoveTo::TempChild(_)) + | Some(MoveTo::Child(_)) + | Some(MoveTo::ReverseChild(_)) + | None => { + // Can't remove moves to parents since we rely on their stack + // pops. + } + } + self.uncommitted.push(MoveTo::TempChild(temp)); + } + + /// Are all the traversal's moves committed? That is, are there no moves + /// that have *not* been committed yet? + #[inline] + pub fn is_committed(&self) -> bool { + // is_empty is not inlined? + self.uncommitted.is_empty() + // self.uncommitted.len() == 0 + } + + /// Commit this traversals moves and return the optimized path from the last + /// commit. + #[inline] + pub fn commit(&mut self) -> Moves { + Moves { + inner: self.uncommitted.drain(..), + } + } + + #[inline] + pub fn reset(&mut self) { + self.uncommitted.clear(); + } +} + +pub struct Moves<'a> { + inner: std::vec::Drain<'a, MoveTo>, +} + +impl Iterator for Moves<'_> { + type Item = MoveTo; + + #[inline] + fn next(&mut self) -> Option { + self.inner.next() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_traversal() { + fn t(f: F) -> Box + where + F: 'static + FnMut(&mut Traversal), + { + Box::new(f) as _ + } + + for (mut traverse, expected_moves) in vec![ + ( + t(|t| { + t.down(); + }), + vec![MoveTo::Child(0)], + ), + ( + t(|t| { + t.up(); + }), + vec![MoveTo::Parent], + ), + ( + t(|t| { + t.sibling(42); + }), + vec![MoveTo::Sibling(42)], + ), + ( + t(|t| { + t.down(); + t.up(); + }), + vec![], + ), + ( + t(|t| { + t.down(); + t.sibling(2); + t.up(); + }), + vec![], + ), + ( + t(|t| { + t.down(); + t.sibling(3); + }), + vec![MoveTo::Child(3)], + ), + ( + t(|t| { + t.down(); + t.sibling(4); + t.sibling(8); + }), + vec![MoveTo::Child(8)], + ), + ( + t(|t| { + t.sibling(1); + t.sibling(1); + }), + vec![MoveTo::Sibling(1)], + ), + ( + t(|t| { + t.reverse_sibling(3); + }), + vec![MoveTo::ReverseSibling(3)], + ), + ( + t(|t| { + t.down(); + t.reverse_sibling(3); + }), + vec![MoveTo::ReverseChild(3)], + ), + ( + t(|t| { + t.down(); + t.reverse_sibling(3); + t.up(); + }), + vec![], + ), + ( + t(|t| { + t.down(); + t.reverse_sibling(3); + t.reverse_sibling(6); + }), + vec![MoveTo::ReverseChild(6)], + ), + ( + t(|t| { + t.up(); + t.reverse_sibling(3); + t.reverse_sibling(6); + }), + vec![MoveTo::Parent, MoveTo::ReverseSibling(6)], + ), + ( + t(|t| { + t.up(); + t.sibling(3); + t.sibling(6); + }), + vec![MoveTo::Parent, MoveTo::Sibling(6)], + ), + ( + t(|t| { + t.sibling(3); + t.sibling(6); + t.up(); + }), + vec![MoveTo::Parent], + ), + ( + t(|t| { + t.reverse_sibling(3); + t.reverse_sibling(6); + t.up(); + }), + vec![MoveTo::Parent], + ), + ( + t(|t| { + t.down(); + t.down_to_temp(3); + }), + vec![MoveTo::Child(0), MoveTo::TempChild(3)], + ), + ( + t(|t| { + t.down_to_temp(3); + t.sibling(5); + }), + vec![MoveTo::Child(5)], + ), + ( + t(|t| { + t.down_to_temp(3); + t.reverse_sibling(5); + }), + vec![MoveTo::ReverseChild(5)], + ), + ( + t(|t| { + t.down_to_temp(3); + t.up(); + }), + vec![], + ), + ( + t(|t| { + t.sibling(2); + t.up(); + t.down_to_temp(3); + }), + vec![MoveTo::Parent, MoveTo::TempChild(3)], + ), + ( + t(|t| { + t.up(); + t.down_to_temp(3); + }), + vec![MoveTo::Parent, MoveTo::TempChild(3)], + ), + ] { + let mut traversal = Traversal::new(); + traverse(&mut traversal); + let actual_moves: Vec<_> = traversal.commit().collect(); + assert_eq!(actual_moves, expected_moves); } } } -pub struct PatchList<'a> { - patches: Vec>, +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct StringKey(u32); + +impl From for u32 { + #[inline] + fn from(key: StringKey) -> u32 { + key.0 + } } diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index e5fe49acc..b45435aff 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -1,8 +1,9 @@ +use crate::component::ScopeIdx; use crate::context::hooks::Hook; use crate::innerlude::*; use crate::nodes::VNode; use bumpalo::Bump; -use generational_arena::Index; +// use generational_arena::ScopeIdx; use std::{ any::TypeId, @@ -32,7 +33,7 @@ pub struct Scope { pub hook_arena: typed_arena::Arena, // Map to the parent - pub parent: Option, + pub parent: Option, pub frames: ActiveFrame, @@ -50,7 +51,7 @@ pub struct Scope { impl Scope { // create a new scope from a function - pub fn new<'a, P1, P2: 'static>(f: FC, props: P1, parent: Option) -> Self { + pub fn new<'a, P1, P2: 'static>(f: FC, props: P1, parent: Option) -> Self { let hook_arena = typed_arena::Arena::new(); let hooks = RefCell::new(Vec::new()); @@ -288,7 +289,7 @@ mod tests { fn test_scope() { let example: FC<()> = |ctx, props| { use crate::builder::*; - ctx.render(|b| div(b).child(text("a")).finish()) + ctx.render(|ctx| div(ctx.bump()).child(text("a")).finish()) }; let props = (); @@ -318,7 +319,8 @@ mod tests { let childprops: ExampleProps<'a> = ExampleProps { name: content }; // let childprops: ExampleProps<'a> = ExampleProps { name: content }; - ctx.render(move |b: &'a Bump| { + ctx.render(move |ctx| { + let b = ctx.bump(); div(b) .child(text(props.name)) // .child(text(props.name)) @@ -336,8 +338,8 @@ mod tests { } fn child_example<'b>(ctx: Context<'b>, props: &'b ExampleProps) -> DomTree { - ctx.render(move |b| { - div(b) + ctx.render(move |ctx| { + div(ctx.bump()) .child(text(props.name)) // .finish() @@ -346,8 +348,8 @@ mod tests { static CHILD: FC = |ctx, props: &'_ ExampleProps| { // todo!() - ctx.render(move |b| { - div(b) + ctx.render(move |ctx| { + div(ctx.bump()) .child(text(props.name)) // .finish() diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 866218351..4c41dd384 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -1,15 +1,10 @@ // use crate::{changelist::EditList, nodes::VNode}; -use crate::{ - changelist::{self, EditList}, - dodriodiff::DiffMachine, -}; -use crate::{events::EventTrigger, innerlude::*}; +use crate::innerlude::*; use bumpalo::Bump; - -use generational_arena::{Arena, Index}; +use generational_arena::Arena; use std::{ - any::{self, TypeId}, + any::TypeId, borrow::BorrowMut, cell::{RefCell, UnsafeCell}, collections::{vec_deque, VecDeque}, @@ -28,7 +23,7 @@ pub struct VirtualDom { /// The index of the root component. /// Will not be ready if the dom is fresh - base_scope: Index, + base_scope: ScopeIdx, event_queue: Rc>>, @@ -98,11 +93,7 @@ impl VirtualDom { component.run::<()>(); - diff_machine.diff_node( - component.old_frame(), - component.new_frame(), - Some(self.base_scope), - ); + diff_machine.diff_node(component.old_frame(), component.new_frame()); Ok(diff_machine.consume()) } @@ -173,11 +164,7 @@ impl VirtualDom { component.run::<()>(); - diff_machine.diff_node( - component.old_frame(), - component.new_frame(), - Some(self.base_scope), - ); + diff_machine.diff_node(component.old_frame(), component.new_frame()); // diff_machine.diff_node( // component.old_frame(), // component.new_frame(), @@ -252,7 +239,7 @@ pub struct LifecycleEvent { pub enum LifecycleType { // Component needs to be mounted, but its scope doesn't exist yet Mount { - to: Index, + to: ScopeIdx, under: usize, props: Box, }, @@ -260,17 +247,17 @@ pub enum LifecycleType { // Parent was evalauted causing new props to generate PropsChanged { props: Box, - component: Index, + component: ScopeIdx, }, // Hook for the subscription API Callback { - component: Index, + component: ScopeIdx, }, } impl LifecycleEvent { - fn index(&self) -> Option { + fn index(&self) -> Option { match &self.event_type { LifecycleType::Mount { to: _, @@ -290,8 +277,9 @@ mod tests { #[test] fn start_dom() { let mut dom = VirtualDom::new(|ctx, props| { - ctx.render(|bump| { + ctx.render(|ctx| { use crate::builder::*; + let bump = ctx.bump(); div(bump).child(text("hello, world")).finish() }) }); diff --git a/packages/web/src/interpreter.rs b/packages/web/src/interpreter.rs index 85c83a2cd..bd9cb7f49 100644 --- a/packages/web/src/interpreter.rs +++ b/packages/web/src/interpreter.rs @@ -1,9 +1,6 @@ use std::{borrow::Borrow, fmt::Debug, sync::Arc}; -use dioxus_core::{ - changelist::{CbIdx, Edit}, - events::{EventTrigger, MouseEvent, VirtualEvent}, -}; +use dioxus_core::events::{EventTrigger, MouseEvent, VirtualEvent}; use fxhash::FxHashMap; use log::debug; use wasm_bindgen::{closure::Closure, JsCast}; @@ -91,7 +88,6 @@ impl EventDelegater { .and_then(|v| v.parse().ok()); if let (Some(gi_id), Some(gi_gen), Some(li_idx)) = (gi_id, gi_gen, li_idx) { - // Call the trigger trigger.0.as_ref()(EventTrigger::new( virtual_event_from_websys_event(event),