diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index 988d3b4b3..8dc3bff40 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -5,7 +5,7 @@ use crate::mutations::Mutation::*; use crate::nodes::VNode; use crate::nodes::{DynamicNode, TemplateNode}; use crate::virtual_dom::VirtualDom; -use crate::{AttributeValue, ScopeId, SuspenseContext, TemplateAttribute}; +use crate::{AttributeValue, ElementId, ScopeId, SuspenseContext, TemplateAttribute}; impl<'b: 'static> VirtualDom { /// Create a new template [`VNode`] and write it to the [`Mutations`] buffer. @@ -36,120 +36,140 @@ impl<'b: 'static> VirtualDom { let mut on_stack = 0; for (root_idx, root) in template.template.roots.iter().enumerate() { + // We might need to generate an ID for the root node on_stack += match root { - TemplateNode::Element { .. } | TemplateNode::Text(_) => { - self.mutations.push(LoadTemplate { - name: template.template.id, - index: root_idx, - }); - 1 - } - TemplateNode::DynamicText(id) | TemplateNode::Dynamic(id) => { match &template.dynamic_nodes[*id] { - DynamicNode::Fragment(frag) => match frag { - VFragment::Empty(slot) => { - let id = - self.next_element(template, template.template.node_paths[*id]); - slot.set(id); - self.mutations.push(CreatePlaceholder { id }); - 1 - } - VFragment::NonEmpty(_) => self.create_dynamic_node( - template, - &template.dynamic_nodes[*id], - *id, - ), - }, - DynamicNode::Component { .. } => { - self.create_dynamic_node(template, &template.dynamic_nodes[*id], *id) - } + // a dynamic text node doesn't replace a template node, instead we create it on the fly DynamicNode::Text(VText { id: slot, value }) => { let id = self.next_element(template, template.template.node_paths[*id]); slot.set(id); self.mutations.push(CreateTextNode { value, id }); 1 } + + DynamicNode::Fragment(VFragment::Empty(slot)) => { + let id = self.next_element(template, template.template.node_paths[*id]); + slot.set(id); + self.mutations.push(CreatePlaceholder { id }); + 1 + } + + DynamicNode::Fragment(VFragment::NonEmpty(_)) + | DynamicNode::Component { .. } => { + self.create_dynamic_node(template, &template.dynamic_nodes[*id], *id) + } } } + + TemplateNode::Element { .. } | TemplateNode::Text(_) => { + let this_id = self.next_element(template, &[]); + template.root_ids[root_idx].set(this_id); + self.mutations.push(LoadTemplate { + name: template.template.id, + index: root_idx, + id: this_id, + }); + + // we're on top of a node that has a dynamic attribute for a descendant + // Set that attribute now before the stack gets in a weird state + while let Some((mut attr_id, path)) = + dynamic_attrs.next_if(|(_, p)| p[0] == root_idx as u8) + { + // if attribute is on a root node, then we've already created the element + // Else, it's deep in the template and we should create a new id for it + let id = match path.len() { + 1 => this_id, + _ => self.next_element(template, template.template.attr_paths[attr_id]), + }; + + loop { + let attribute = template.dynamic_attrs.get(attr_id).unwrap(); + attribute.mounted_element.set(id); + + match &attribute.value { + AttributeValue::Text(value) => self.mutations.push(SetAttribute { + name: attribute.name, + value: *value, + ns: attribute.namespace, + id, + }), + AttributeValue::Bool(value) => { + self.mutations.push(SetBoolAttribute { + name: attribute.name, + value: *value, + id, + }) + } + AttributeValue::Listener(_) => { + self.mutations.push(NewEventListener { + // all listeners start with "on" + event_name: &attribute.name[2..], + scope: cur_scope, + id, + }) + } + AttributeValue::Float(_) => todo!(), + AttributeValue::Int(_) => todo!(), + AttributeValue::Any(_) => todo!(), + AttributeValue::None => todo!(), + } + + // Only push the dynamic attributes forward if they match the current path (same element) + match dynamic_attrs.next_if(|(_, p)| *p == path) { + Some((next_attr_id, _)) => attr_id = next_attr_id, + None => break, + } + } + } + + // We're on top of a node that has a dynamic child for a descendant + // Skip any node that's a root + let mut start = None; + let mut end = None; + + // Collect all the dynamic nodes below this root + // We assign the start and end of the range of dynamic nodes since they area ordered in terms of tree path + // + // [0] + // [1, 1] <---| + // [1, 1, 1] <---| these are the range of dynamic nodes below root 1 + // [1, 1, 2] <---| + // [2] + // + // We collect each range and then create them and replace the placeholder in the template + while let Some((idx, p)) = + dynamic_nodes.next_if(|(_, p)| p[0] == root_idx as u8) + { + if p.len() == 1 { + continue; + } + + if start.is_none() { + start = Some(idx); + } + + end = Some(idx); + } + + // + if let (Some(start), Some(end)) = (start, end) { + for idx in start..=end { + let node = &template.dynamic_nodes[idx]; + let m = self.create_dynamic_node(template, node, idx); + if m > 0 { + self.mutations.push(ReplacePlaceholder { + m, + path: &template.template.node_paths[idx][1..], + }); + } + } + } + + // elements create only one node :-) + 1 + } }; - - // we're on top of a node that has a dynamic attribute for a descendant - // Set that attribute now before the stack gets in a weird state - while let Some((mut attr_id, path)) = - dynamic_attrs.next_if(|(_, p)| p[0] == root_idx as u8) - { - let id = self.next_element(template, template.template.attr_paths[attr_id]); - self.mutations.push(AssignId { - path: &path[1..], - id, - }); - - loop { - let attribute = template.dynamic_attrs.get(attr_id).unwrap(); - attribute.mounted_element.set(id); - - match &attribute.value { - AttributeValue::Text(value) => self.mutations.push(SetAttribute { - name: attribute.name, - value: *value, - ns: attribute.namespace, - id, - }), - AttributeValue::Bool(value) => self.mutations.push(SetBoolAttribute { - name: attribute.name, - value: *value, - id, - }), - AttributeValue::Listener(_) => self.mutations.push(NewEventListener { - // all listeners start with "on" - event_name: &attribute.name[2..], - scope: cur_scope, - id, - }), - AttributeValue::Float(_) => todo!(), - AttributeValue::Int(_) => todo!(), - AttributeValue::Any(_) => todo!(), - AttributeValue::None => todo!(), - } - - // Only push the dynamic attributes forward if they match the current path (same element) - match dynamic_attrs.next_if(|(_, p)| *p == path) { - Some((next_attr_id, _)) => attr_id = next_attr_id, - None => break, - } - } - } - - // We're on top of a node that has a dynamic child for a descendant - // Skip any node that's a root - let mut start = None; - let mut end = None; - - while let Some((idx, p)) = dynamic_nodes.next_if(|(_, p)| p[0] == root_idx as u8) { - if p.len() == 1 { - continue; - } - - if start.is_none() { - start = Some(idx); - } - - end = Some(idx); - } - - if let (Some(start), Some(end)) = (start, end) { - for idx in start..=end { - let node = &template.dynamic_nodes[idx]; - let m = self.create_dynamic_node(template, node, idx); - if m > 0 { - self.mutations.push(ReplacePlaceholder { - m, - path: &template.template.node_paths[idx][1..], - }); - } - } - } } on_stack @@ -196,8 +216,8 @@ impl<'b: 'static> VirtualDom { ) { match *node { // Todo: create the children's template - TemplateNode::Dynamic(idx) => { - let id = self.next_element(template, template.template.node_paths[idx]); + TemplateNode::Dynamic(_) => { + let id = ElementId(0); self.mutations .template_mutations .push(CreatePlaceholder { id }) @@ -218,7 +238,7 @@ impl<'b: 'static> VirtualDom { tag, inner_opt, } => { - let id = self.next_element(template, &[]); // never gets referenced, empty path is fine, I think? + let id = ElementId(0); self.mutations.template_mutations.push(CreateElement { name: tag, @@ -344,19 +364,28 @@ impl<'b: 'static> VirtualDom { use RenderReturn::*; match return_nodes { - Sync(Ok(t)) => self.mount_component(scope, t, idx), + Sync(Ok(t)) => { + self.mount_component(scope, template, t, idx) + // self.create(t) + } Sync(Err(_e)) => todo!("Propogate error upwards"), Async(_) => self.mount_component_placeholder(template, idx, scope), } } - fn mount_component(&mut self, scope: ScopeId, template: &'b VNode<'b>, idx: usize) -> usize { + fn mount_component( + &mut self, + scope: ScopeId, + parent: &'b VNode<'b>, + new: &'b VNode<'b>, + idx: usize, + ) -> usize { // Keep track of how many mutations are in the buffer in case we need to split them out if a suspense boundary // is encountered let mutations_to_this_point = self.mutations.len(); // Create the component's root element - let created = self.create_scope(scope, template); + let created = self.create_scope(scope, new); // If there are no suspense leaves below us, then just don't bother checking anything suspense related if self.collected_leaves.is_empty() { @@ -370,7 +399,7 @@ impl<'b: 'static> VirtualDom { }; // Since this is a boundary, use its placeholder within the template as the placeholder for the suspense tree - let new_id = self.next_element(template, template.template.node_paths[idx]); + let new_id = self.next_element(new, parent.template.node_paths[idx]); // Now connect everything to the boundary self.scopes[scope.0].placeholder.set(Some(new_id)); @@ -392,7 +421,7 @@ impl<'b: 'static> VirtualDom { // Now assign the placeholder in the DOM self.mutations.push(AssignId { id: new_id, - path: &template.template.node_paths[idx][1..], + path: &parent.template.node_paths[idx][1..], }); 0 @@ -412,6 +441,11 @@ impl<'b: 'static> VirtualDom { // Set the placeholder of the scope self.scopes[scope.0].placeholder.set(Some(new_id)); + println!( + "assigning id {:?} to path {:?}, template: {:?}", + new_id, &template.template.node_paths, template.template + ); + // Since the placeholder is already in the DOM, we don't create any new nodes self.mutations.push(AssignId { id: new_id, diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 38cd517b9..4b98ac978 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -1,26 +1,17 @@ -use std::any::Any; -use std::cell::Cell; - -use crate::factory::RenderReturn; -use crate::innerlude::{Mutations, VComponent, VFragment, VText}; -use crate::virtual_dom::VirtualDom; -use crate::{Attribute, AttributeValue, TemplateNode}; - -use crate::any_props::VProps; -use DynamicNode::*; - -use crate::mutations::Mutation; -use crate::nodes::{DynamicNode, Template, TemplateId}; -use crate::scopes::Scope; use crate::{ - any_props::AnyProps, arena::ElementId, - bump_frame::BumpFrame, - nodes::VNode, - scopes::{ScopeId, ScopeState}, + factory::RenderReturn, + innerlude::{DirtyScope, VComponent, VFragment, VText}, + mutations::Mutation, + nodes::{DynamicNode, VNode}, + scopes::ScopeId, + virtual_dom::VirtualDom, + AttributeValue, TemplateNode, }; + use fxhash::{FxHashMap, FxHashSet}; -use slab::Slab; +use std::cell::Cell; +use DynamicNode::*; impl<'b: 'static> VirtualDom { pub fn diff_scope(&mut self, scope: ScopeId) { @@ -123,6 +114,15 @@ impl<'b: 'static> VirtualDom { _ => self.replace(left_template, right_template, left_node, right_node), }; } + + // Make sure the roots get transferred over + for (left, right) in left_template + .root_ids + .iter() + .zip(right_template.root_ids.iter()) + { + right.set(left.get()); + } } fn diff_vcomponent(&mut self, left: &'b VComponent<'b>, right: &'b VComponent<'b>) { @@ -228,8 +228,7 @@ impl<'b: 'static> VirtualDom { } fn replace_placeholder_with_nodes(&mut self, l: &'b Cell, r: &'b [VNode<'b>]) { - let created = r.iter().fold(0, |acc, child| acc + self.create(child)); - + let created = self.create_children(r); self.mutations.push(Mutation::ReplaceWith { id: l.get(), m: created, @@ -238,9 +237,10 @@ impl<'b: 'static> VirtualDom { fn replace_nodes_with_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b Cell) { // - // Remove the old nodes, except for one self.remove_nodes(&l[1..]); + let id = self.next_element(&l[0], &[]); + // self.replace(left_template, right_template, left, right) } fn diff_non_empty_fragment(&mut self, new: &'b [VNode<'b>], old: &'b [VNode<'b>]) { @@ -613,8 +613,12 @@ impl<'b: 'static> VirtualDom { } } - fn insert_after(&mut self, node: &'b VNode<'b>, nodes_created: usize) {} - fn insert_before(&mut self, node: &'b VNode<'b>, nodes_created: usize) {} + fn insert_after(&mut self, node: &'b VNode<'b>, nodes_created: usize) { + todo!() + } + fn insert_before(&mut self, node: &'b VNode<'b>, nodes_created: usize) { + todo!() + } fn replace_node_with_on_stack(&mut self, old: &'b VNode<'b>, m: usize) { todo!() @@ -623,7 +627,39 @@ impl<'b: 'static> VirtualDom { /// Remove these nodes from the dom /// Wont generate mutations for the inner nodes fn remove_nodes(&mut self, nodes: &'b [VNode<'b>]) { - // + nodes.into_iter().for_each(|node| self.remove_node(node)); + } + + fn remove_node(&mut self, node: &'b VNode<'b>) { + for (idx, root) in node.template.roots.iter().enumerate() { + let id = match root { + TemplateNode::Text(_) | TemplateNode::Element { .. } => node.root_ids[idx].get(), + TemplateNode::DynamicText(id) | TemplateNode::Dynamic(id) => { + match &node.dynamic_nodes[*id] { + Text(t) => t.id.get(), + Fragment(VFragment::Empty(t)) => t.get(), + Fragment(VFragment::NonEmpty(t)) => return self.remove_nodes(t), + Component(comp) => return self.remove_component(comp.scope.get().unwrap()), + } + } + }; + + self.mutations.push(Mutation::Remove { id }) + } + } + + fn remove_component(&mut self, scope_id: ScopeId) { + let height = self.scopes[scope_id.0].height; + self.dirty_scopes.remove(&DirtyScope { + height, + id: scope_id, + }); + + // I promise, since we're descending down the tree, this is safe + match unsafe { self.scopes[scope_id.0].root_node().extend_lifetime_ref() } { + RenderReturn::Sync(Ok(t)) => self.remove_node(t), + _ => todo!("cannot handle nonstandard nodes"), + } } /// Push all the real nodes on the stack @@ -632,26 +668,60 @@ impl<'b: 'static> VirtualDom { } fn create_children(&mut self, nodes: &'b [VNode<'b>]) -> usize { - todo!() + nodes.iter().fold(0, |acc, child| acc + self.create(child)) } - pub(crate) fn create_and_insert_before(&self, new: &[VNode], after: &VNode) { - let id = self.find_last_element(after); + fn create_and_insert_before(&mut self, new: &'b [VNode<'b>], before: &'b VNode<'b>) { + let m = self.create_children(new); + let id = self.find_first_element(before); + self.mutations.push(Mutation::InsertBefore { id, m }) } - pub(crate) fn create_and_insert_after(&self, new: &[VNode], after: &VNode) { + + fn create_and_insert_after(&mut self, new: &'b [VNode<'b>], after: &'b VNode<'b>) { + let m = self.create_children(new); let id = self.find_last_element(after); + self.mutations.push(Mutation::InsertAfter { id, m }) } fn find_first_element(&self, node: &VNode) -> ElementId { - todo!() + match &node.template.roots[0] { + TemplateNode::Element { .. } | TemplateNode::Text(_) => node.root_ids[0].get(), + TemplateNode::DynamicText(id) | TemplateNode::Dynamic(id) => { + match &node.dynamic_nodes[*id] { + Text(t) => t.id.get(), + Fragment(VFragment::NonEmpty(t)) => self.find_first_element(&t[0]), + Fragment(VFragment::Empty(t)) => t.get(), + Component(comp) => { + let scope = comp.scope.get().unwrap(); + match self.scopes[scope.0].root_node() { + RenderReturn::Sync(Ok(t)) => self.find_first_element(t), + _ => todo!("cannot handle nonstandard nodes"), + } + } + } + } + } } fn find_last_element(&self, node: &VNode) -> ElementId { match node.template.roots.last().unwrap() { - TemplateNode::Element { .. } => todo!(), - TemplateNode::Text(t) => todo!(), - TemplateNode::Dynamic(_) => todo!(), - TemplateNode::DynamicText(_) => todo!(), + TemplateNode::Element { .. } | TemplateNode::Text(_) => { + node.root_ids.last().unwrap().get() + } + TemplateNode::DynamicText(id) | TemplateNode::Dynamic(id) => { + match &node.dynamic_nodes[*id] { + Text(t) => t.id.get(), + Fragment(VFragment::NonEmpty(t)) => self.find_last_element(t.last().unwrap()), + Fragment(VFragment::Empty(t)) => t.get(), + Component(comp) => { + let scope = comp.scope.get().unwrap(); + match self.scopes[scope.0].root_node() { + RenderReturn::Sync(Ok(t)) => self.find_last_element(t), + _ => todo!("cannot handle nonstandard nodes"), + } + } + } + } } } @@ -666,6 +736,15 @@ impl<'b: 'static> VirtualDom { left: &'b DynamicNode<'b>, right: &'b DynamicNode<'b>, ) { + // Remove all but the first root + for root in &left_template.template.roots[1..] { + match root { + TemplateNode::Element { .. } => todo!(), + TemplateNode::Text(_) => todo!(), + TemplateNode::Dynamic(_) => todo!(), + TemplateNode::DynamicText(_) => todo!(), + } + } } } diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 66d95cfd0..94cd00d70 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -94,6 +94,7 @@ pub enum Mutation<'a> { LoadTemplate { name: &'static str, index: usize, + id: ElementId, }, // Take the current element and replace it with the element with the given id. @@ -103,8 +104,8 @@ pub enum Mutation<'a> { }, ReplacePlaceholder { - m: usize, path: &'static [u8], + m: usize, }, /// Insert a number of nodes after a given node. @@ -176,4 +177,7 @@ pub enum Mutation<'a> { /// The name of the event to remove. event: &'a str, }, + Remove { + id: ElementId, + }, } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 54cb47df6..1719388ad 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -38,15 +38,6 @@ pub struct VNode<'a> { } impl<'a> VNode<'a> { - pub fn placeholder_template(cx: &'a ScopeState) -> Self { - Self::template_from_dynamic_node( - cx, - DynamicNode::Fragment(VFragment::Empty(Cell::new(ElementId(0)))), - "dioxus-placeholder", - ) - .unwrap() - } - pub fn empty() -> Element<'a> { Ok(VNode { node_id: Cell::new(ElementId(0)), @@ -63,48 +54,6 @@ impl<'a> VNode<'a> { }, }) } - - pub fn template_from_dynamic_node( - cx: &'a ScopeState, - node: DynamicNode<'a>, - id: &'static str, - ) -> anyhow::Result { - Ok(VNode { - node_id: Cell::new(ElementId(0)), - key: None, - parent: None, - root_ids: &[], - dynamic_nodes: cx.bump().alloc([node]), - dynamic_attrs: &[], - template: Template { - id, - roots: &[TemplateNode::Dynamic(0)], - node_paths: &[&[0]], - attr_paths: &[], - }, - }) - } - - pub fn single_text( - _cx: &'a ScopeState, - text: &'static [TemplateNode<'static>], - id: &'static str, - ) -> anyhow::Result { - Ok(VNode { - node_id: Cell::new(ElementId(0)), - key: None, - parent: None, - root_ids: &[], - dynamic_nodes: &[], - dynamic_attrs: &[], - template: Template { - id, - roots: text, - node_paths: &[&[0]], - attr_paths: &[], - }, - }) - } } #[derive(Debug, Clone, Copy)] diff --git a/packages/core/src/scheduler/suspense.rs b/packages/core/src/scheduler/suspense.rs index 665458bd3..0b2952425 100644 --- a/packages/core/src/scheduler/suspense.rs +++ b/packages/core/src/scheduler/suspense.rs @@ -37,8 +37,8 @@ pub struct SuspenseBoundary { } impl SuspenseBoundary { - pub fn new(id: ScopeId) -> Rc { - Rc::new(Self { + pub fn new(id: ScopeId) -> Self { + Self { id, waiting_on: Default::default(), mutations: RefCell::new(Mutations::new()), @@ -46,7 +46,7 @@ impl SuspenseBoundary { created_on_stack: Cell::new(0), onresolve: None, onstart: None, - }) + } } } diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 14bea6ccc..0fdccd820 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -103,6 +103,9 @@ impl ScopeState { } } + /// Get the current render since the inception of this component + /// + /// This can be used as a helpful diagnostic when debugging hooks/renders, etc pub fn generation(&self) -> usize { self.render_cnt.get() } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 7c051c824..1a94d04e5 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -252,7 +252,7 @@ impl VirtualDom { // This could be unexpected, so we might rethink this behavior later // // We *could* just panic if the suspense boundary is not found - root.provide_context(SuspenseBoundary::new(ScopeId(0))); + root.provide_context(Rc::new(SuspenseBoundary::new(ScopeId(0)))); // the root element is always given element ID 0 since it's the container for the entire tree dom.elements.insert(ElementRef::null()); diff --git a/packages/core/tests/create_lists.rs b/packages/core/tests/create_lists.rs index 2c9f3ad4c..8297081be 100644 --- a/packages/core/tests/create_lists.rs +++ b/packages/core/tests/create_lists.rs @@ -6,6 +6,8 @@ use dioxus_core::ElementId; // In react, this would be a lot of node creation. // // In Dioxus, we memoize the rsx! body and simplify it down to a few template loads +// +// Also note that the IDs increase linearly. This lets us drive a vec on the renderer for O(1) re-indexing fn app(cx: Scope) -> Element { cx.render(rsx! { div { @@ -29,41 +31,41 @@ fn list_renders() { edits.template_mutations, [ // Create the outer div - CreateElement { name: "div", namespace: None, id: ElementId(1) }, + CreateElement { name: "div", namespace: None, id: ElementId(0) }, // todo: since this is the only child, we should just use // append when modify the values (IE no need for a placeholder) - CreatePlaceholder { id: ElementId(2) }, + CreatePlaceholder { id: ElementId(0) }, AppendChildren { m: 1 }, SaveTemplate { name: "template", m: 1 }, // Create the inner template div - CreateElement { name: "div", namespace: None, id: ElementId(3) }, - CreateElement { name: "h1", namespace: None, id: ElementId(4) }, + CreateElement { name: "div", namespace: None, id: ElementId(0) }, + CreateElement { name: "h1", namespace: None, id: ElementId(0) }, CreateStaticText { value: "hello world! " }, AppendChildren { m: 1 }, - CreateElement { name: "p", namespace: None, id: ElementId(5) }, + CreateElement { name: "p", namespace: None, id: ElementId(0) }, CreateStaticText { value: "d" }, AppendChildren { m: 1 }, AppendChildren { m: 2 }, SaveTemplate { name: "template", m: 1 } - ] + ], ); assert_eq!( edits.edits, [ // Load the outer div - LoadTemplate { name: "template", index: 0 }, + LoadTemplate { name: "template", index: 0, id: ElementId(1) }, // Load each template one-by-one, rehydrating it - LoadTemplate { name: "template", index: 0 }, - HydrateText { path: &[1, 0], value: "0", id: ElementId(6) }, - LoadTemplate { name: "template", index: 0 }, - HydrateText { path: &[1, 0], value: "1", id: ElementId(7) }, - LoadTemplate { name: "template", index: 0 }, - HydrateText { path: &[1, 0], value: "2", id: ElementId(8) }, + LoadTemplate { name: "template", index: 0, id: ElementId(2) }, + HydrateText { path: &[1, 0], value: "0", id: ElementId(3) }, + LoadTemplate { name: "template", index: 0, id: ElementId(4) }, + HydrateText { path: &[1, 0], value: "1", id: ElementId(5) }, + LoadTemplate { name: "template", index: 0, id: ElementId(6) }, + HydrateText { path: &[1, 0], value: "2", id: ElementId(7) }, // Replace the 0th childn on the div with the 3 templates on the stack ReplacePlaceholder { m: 3, path: &[0] }, // Append the container div to the dom AppendChildren { m: 1 } - ] + ], ) } diff --git a/packages/core/tests/create_passthru.rs b/packages/core/tests/create_passthru.rs index a46249695..b8916156b 100644 --- a/packages/core/tests/create_passthru.rs +++ b/packages/core/tests/create_passthru.rs @@ -28,13 +28,15 @@ fn nested_passthru_creates() { assert_eq!( edits.edits, [ - LoadTemplate { name: "template", index: 0 }, + LoadTemplate { name: "template", index: 0, id: ElementId(1) }, AppendChildren { m: 1 }, ] ) } /// Should load all the templates and append them +/// +/// Take note on how we don't spit out the template for child_comp since it's entirely dynamic #[test] fn nested_passthru_creates_add() { fn app(cx: Scope) -> Element { @@ -64,15 +66,20 @@ fn nested_passthru_creates_add() { assert_eq!( dom.rebuild().santize().edits, [ - LoadTemplate { name: "template", index: 0 }, - LoadTemplate { name: "template", index: 0 }, - LoadTemplate { name: "template", index: 0 }, - LoadTemplate { name: "template", index: 1 }, + // load 1 + LoadTemplate { name: "template", index: 0, id: ElementId(1) }, + // load 2 + LoadTemplate { name: "template", index: 0, id: ElementId(2) }, + // load 3 + LoadTemplate { name: "template", index: 0, id: ElementId(3) }, + // load div that contains 4 + LoadTemplate { name: "template", index: 1, id: ElementId(4) }, AppendChildren { m: 4 }, ] ); } +/// note that the template is all dynamic roots - so it doesn't actually get cached as a template #[test] fn dynamic_node_as_root() { fn app(cx: Scope) -> Element { diff --git a/packages/core/tests/diff_unkeyed_list.rs b/packages/core/tests/diff_unkeyed_list.rs new file mode 100644 index 000000000..aabd38510 --- /dev/null +++ b/packages/core/tests/diff_unkeyed_list.rs @@ -0,0 +1,134 @@ +use dioxus::core::{ElementId, Mutation::*}; +use dioxus::prelude::*; + +#[test] +fn list_creates_one_by_one() { + let mut dom = VirtualDom::new(|cx| { + let gen = cx.generation(); + + cx.render(rsx! { + div { + (0..gen).map(|i| rsx! { + div { "{i}" } + }) + } + }) + }); + + // load the div and then assign the empty fragment as a placeholder + assert_eq!( + dom.rebuild().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, + AssignId { path: &[0], id: ElementId(2,) }, + AppendChildren { m: 1 }, + ] + ); + + // Rendering the first item should replace the placeholder with an element + dom.mark_dirty_scope(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(3,) }, + HydrateText { path: &[0], value: "0", id: ElementId(4,) }, + ReplaceWith { id: ElementId(2,), m: 1 }, + ] + ); + + // Rendering the next item should insert after the previous + dom.mark_dirty_scope(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(5,) }, + HydrateText { path: &[0], value: "1", id: ElementId(6,) }, + InsertAfter { id: ElementId(3,), m: 1 }, + ] + ); + + // ... and again! + dom.mark_dirty_scope(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(7,) }, + HydrateText { path: &[0], value: "2", id: ElementId(8,) }, + InsertAfter { id: ElementId(5,), m: 1 }, + ] + ); + + // once more + dom.mark_dirty_scope(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(9,) }, + HydrateText { path: &[0], value: "3", id: ElementId(10,) }, + InsertAfter { id: ElementId(7,), m: 1 }, + ] + ); +} + +#[test] +fn removes_one_by_one() { + let mut dom = VirtualDom::new(|cx| { + let gen = 3 - cx.generation(); + + println!("gen: {:?}", gen); + println!("range: {:?}", 0..gen); + + cx.render(rsx! { + div { + (0..gen).map(|i| rsx! { + div { "{i}" } + }) + } + }) + }); + + // load the div and then assign the empty fragment as a placeholder + assert_eq!( + dom.rebuild().santize().edits, + [ + // The container + LoadTemplate { name: "template", index: 0, id: ElementId(1) }, + // each list item + LoadTemplate { name: "template", index: 0, id: ElementId(2) }, + HydrateText { path: &[0], value: "0", id: ElementId(3) }, + LoadTemplate { name: "template", index: 0, id: ElementId(4) }, + HydrateText { path: &[0], value: "1", id: ElementId(5) }, + LoadTemplate { name: "template", index: 0, id: ElementId(6) }, + HydrateText { path: &[0], value: "2", id: ElementId(7) }, + // replace the placeholder in the template with the 3 templates on the stack + ReplacePlaceholder { m: 3, path: &[0] }, + // Mount the div + AppendChildren { m: 1 } + ] + ); + + // Remove div(3) + // Rendering the first item should replace the placeholder with an element + dom.mark_dirty_scope(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [Remove { id: ElementId(6) }] + ); + + // Remove div(2) + dom.mark_dirty_scope(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [Remove { id: ElementId(4) }] + ); + + // Remove div(1) and replace with a placeholder + dom.mark_dirty_scope(ScopeId(0)); + assert_eq!(dom.render_immediate().santize().edits, []); + + // dom.mark_dirty_scope(ScopeId(0)); + // assert_eq!( + // dom.render_immediate().santize().edits, + // [Remove { id: ElementId(4) }] + // ); +} diff --git a/packages/core/tests/suspend.rs b/packages/core/tests/suspend.rs index 18743c26a..21af95771 100644 --- a/packages/core/tests/suspend.rs +++ b/packages/core/tests/suspend.rs @@ -1,55 +1,67 @@ -use dioxus_core::*; -use std::{cell::RefCell, rc::Rc, time::Duration}; +use dioxus::core::ElementId; +use dioxus::core::{Mutation::*, SuspenseBoundary}; +use dioxus::prelude::*; +use dioxus_core::SuspenseContext; +use std::{rc::Rc, time::Duration}; #[tokio::test] async fn it_works() { let mut dom = VirtualDom::new(app); - let mutations = dom.rebuild(); + let mutations = dom.rebuild().santize(); - println!("mutations: {:?}", mutations); + // We should at least get the top-level template in + assert_eq!( + mutations.template_mutations, + [ + CreateElement { name: "div", namespace: None, id: ElementId(0) }, + CreateStaticText { value: "Waiting for child..." }, + CreatePlaceholder { id: ElementId(0) }, + AppendChildren { m: 2 }, + SaveTemplate { name: "template", m: 1 } + ] + ); + + // And we should load it in and assign the placeholder properly + assert_eq!( + mutations.edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1) }, + // hmmmmmmmmm.... with suspense how do we guarantee that IDs increase linearly? + // can we even? + AssignId { path: &[1], id: ElementId(3) }, + AppendChildren { m: 1 }, + ] + ); + + // wait just a moment, not enough time for the boundary to resolve dom.wait_for_work().await; } fn app(cx: Scope) -> Element { - println!("running root app"); - - VNode::template_from_dynamic_node( - cx, - cx.component(suspense_boundary, (), "suspense_boundary"), - "app", - ) + cx.render(rsx!( + div { + "Waiting for child..." + suspense_boundary {} + } + )) } fn suspense_boundary(cx: Scope) -> Element { - println!("running boundary"); + cx.use_hook(|| cx.provide_context(Rc::new(SuspenseBoundary::new(cx.scope_id())))); - let _ = cx.use_hook(|| { - cx.provide_context(Rc::new(RefCell::new(SuspenseBoundary::new(cx.scope_id())))) - }); + // Ensure the right types are found + cx.has_context::().unwrap(); - VNode::template_from_dynamic_node(cx, cx.component(async_child, (), "async_child"), "app") + cx.render(rsx!(async_child {})) } async fn async_child(cx: Scope<'_>) -> Element { - println!("rendering async child"); - - let fut = cx.use_hook(|| { - Box::pin(async { - println!("Starting sleep"); - tokio::time::sleep(Duration::from_secs(1)).await; - println!("Sleep ended"); - }) - }); - - fut.await; - - println!("Future awaited and complete"); - - VNode::template_from_dynamic_node(cx, cx.component(async_text, (), "async_text"), "app") + use_future!(cx, || tokio::time::sleep(Duration::from_millis(10))).await; + cx.render(rsx!(async_text {})) } async fn async_text(cx: Scope<'_>) -> Element { - VNode::single_text(&cx, &[TemplateNode::Text("it works!")], "beauty") + cx.render(rsx!("async_text")) } diff --git a/packages/core/tests/task.rs b/packages/core/tests/task.rs index 83c61d0b0..be7c969d3 100644 --- a/packages/core/tests/task.rs +++ b/packages/core/tests/task.rs @@ -3,6 +3,8 @@ use dioxus::prelude::*; use std::time::Duration; +static mut POLL_COUNT: usize = 0; + #[tokio::test] async fn it_works() { let mut dom = VirtualDom::new(app); @@ -11,23 +13,27 @@ async fn it_works() { tokio::select! { _ = dom.wait_for_work() => {} - _ = tokio::time::sleep(Duration::from_millis(600)) => {} + _ = tokio::time::sleep(Duration::from_millis(10)) => {} }; + + // By the time the tasks are finished, we should've accumulated ticks from two tasks + // Be warned that by setting the delay to too short, tokio might not schedule in the tasks + assert_eq!(unsafe { POLL_COUNT }, 135); } fn app(cx: Scope) -> Element { cx.use_hook(|| { cx.spawn(async { for x in 0..10 { - tokio::time::sleep(Duration::from_millis(50)).await; - println!("Hello, world! {x}"); + tokio::time::sleep(Duration::from_micros(50)).await; + unsafe { POLL_COUNT += x } } }); cx.spawn(async { for x in 0..10 { - tokio::time::sleep(Duration::from_millis(25)).await; - println!("Hello, world from second thread! {x}"); + tokio::time::sleep(Duration::from_micros(25)).await; + unsafe { POLL_COUNT += x * 2 } } }); }); diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index 825f7bda7..15e9c00a9 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -121,6 +121,7 @@ impl<'a> ToTokens for TemplateRenderer<'a> { }); // Render and release the mutable borrow on context + let num_roots = self.roots.len(); let roots = quote! { #( #root_printer ),* }; let node_printer = &context.dynamic_nodes; let dyn_attr_printer = &context.dynamic_attributes; @@ -147,7 +148,7 @@ impl<'a> ToTokens for TemplateRenderer<'a> { parent: None, key: #key_tokens, template: TEMPLATE, - root_ids: __cx.bump().alloc([]), + root_ids: std::cell::Cell::from_mut( __cx.bump().alloc([::dioxus::core::ElementId(0); #num_roots]) as &mut [::dioxus::core::ElementId]).as_slice_of_cells(), dynamic_nodes: __cx.bump().alloc([ #( #node_printer ),* ]), dynamic_attrs: __cx.bump().alloc([ #( #dyn_attr_printer ),* ]), }