From 5713e13ff28ad6362e20f60f560a5ede29217086 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 24 Nov 2022 09:11:27 -0500 Subject: [PATCH 01/18] feat: implement ID cycling --- packages/core/src/diff.rs | 149 ++++++++++++++---------- packages/core/src/garbage.rs | 9 +- packages/core/src/scheduler/wait.rs | 27 +++-- packages/core/tests/bubble_error.rs | 83 ++++--------- packages/core/tests/create_fragments.rs | 2 +- packages/core/tests/create_lists.rs | 2 +- packages/core/tests/cycle.rs | 51 ++++++++ packages/core/tests/diff_element.rs | 32 ++++- packages/core/tests/suspend.rs | 2 +- packages/rsx/src/lib.rs | 7 +- 10 files changed, 216 insertions(+), 148 deletions(-) create mode 100644 packages/core/tests/cycle.rs diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 98d7dc56b..541167486 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -46,30 +46,29 @@ impl<'b: 'static> VirtualDom { fn diff_maybe_node(&mut self, left: &'b RenderReturn<'b>, right: &'b RenderReturn<'b>) { use RenderReturn::{Async, Sync}; match (left, right) { - // diff (Sync(Ok(l)), Sync(Ok(r))) => self.diff_node(l, r), - _ => todo!("handle diffing nonstandard nodes"), - // // remove old with placeholder - // (Sync(Ok(l)), Sync(None)) | (Sync(Ok(l)), Async(_)) => { - // // - // let id = self.next_element(l, &[]); // todo! - // m.push(Mutation::CreatePlaceholder { id }); - // self.drop_template(m, l, true); - // } + // Err cases + (Sync(Ok(l)), Sync(Err(e))) => self.diff_ok_to_err(l, e), + (Sync(Err(e)), Sync(Ok(r))) => self.diff_err_to_ok(e, r), + (Sync(Err(_eo)), Sync(Err(_en))) => { /* nothing */ } - // // remove placeholder with nodes - // (Sync(None), Sync(Ok(_))) => {} - // (Async(_), Sync(Ok(v))) => {} - - // // nothing... just transfer the placeholders over - // (Async(_), Async(_)) - // | (Sync(None), Sync(None)) - // | (Sync(None), Async(_)) - // | (Async(_), Sync(None)) => {} + // Async + (Sync(Ok(_l)), Async(_)) => todo!(), + (Sync(Err(_e)), Async(_)) => todo!(), + (Async(_), Sync(Ok(_r))) => todo!(), + (Async(_), Sync(Err(_e))) => { /* nothing */ } + (Async(_), Async(_)) => { /* nothing */ } } } + fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, e: &anyhow::Error) { + todo!("Not yet handling error rollover") + } + fn diff_err_to_ok(&mut self, e: &anyhow::Error, l: &'b VNode<'b>) { + todo!("Not yet handling error rollover") + } + pub fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) { if left_template.template.id != right_template.template.id { return self.light_diff_templates(left_template, right_template); @@ -97,7 +96,7 @@ impl<'b: 'static> VirtualDom { }); } // todo: more types of attribute values - _ => (), + _ => todo!("other attribute types"), } } } @@ -111,7 +110,7 @@ impl<'b: 'static> VirtualDom { (Text(left), Text(right)) => self.diff_vtext(left, right), (Fragment(left), Fragment(right)) => self.diff_vfragment(left, right), (Component(left), Component(right)) => self.diff_vcomponent(left, right), - _ => self.replace(left_template, right_template, left_node, right_node), + _ => self.replace(left_template, right_template), }; } @@ -196,7 +195,7 @@ impl<'b: 'static> VirtualDom { /// ``` fn light_diff_templates(&mut self, left: &'b VNode<'b>, right: &'b VNode<'b>) { match matching_components(left, right) { - None => self.replace_template(left, right), + None => self.replace(left, right), Some(components) => components .into_iter() .for_each(|(l, r)| self.diff_vcomponent(l, r)), @@ -236,63 +235,98 @@ 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 + // Remove the old nodes, except for onea self.remove_nodes(&l[1..]); - let first = self.find_first_element(&l[0]); - self.remove_all_but_first_node(&l[0]); + let first = self.replace_inner(&l[0]); let placeholder = self.next_element(&l[0], &[]); r.set(placeholder); + self.mutations .push(Mutation::CreatePlaceholder { id: placeholder }); self.mutations - .push(Mutation::ReplaceWith { id: first, m: 1 }) + .push(Mutation::ReplaceWith { id: first, m: 1 }); + + self.reclaim(first); } - // Remove all the top-level nodes, returning the - fn remove_all_but_first_node(&mut self, node: &'b VNode<'b>) { - match &node.template.roots[0] { - TemplateNode::Text(_) | TemplateNode::Element { .. } => {} + // Remove all the top-level nodes, returning the firstmost root ElementId + fn replace_inner(&mut self, node: &'b VNode<'b>) -> ElementId { + let id = match &node.template.roots[0] { + TemplateNode::Text(_) | TemplateNode::Element { .. } => node.root_ids[0].get(), TemplateNode::DynamicText(id) | TemplateNode::Dynamic(id) => { match &node.dynamic_nodes[*id] { - Text(_) => {} - Fragment(VFragment::Empty(_)) => {} + Text(t) => t.id.get(), + Fragment(VFragment::Empty(e)) => e.get(), Fragment(VFragment::NonEmpty(nodes)) => { - self.remove_all_but_first_node(&nodes[0]); + let id = self.replace_inner(&nodes[0]); self.remove_nodes(&nodes[1..]); + id } Component(comp) => { let scope = comp.scope.get().unwrap(); match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { - RenderReturn::Sync(Ok(t)) => self.remove_all_but_first_node(t), + RenderReturn::Sync(Ok(t)) => self.replace_inner(t), _ => todo!("cannot handle nonstandard nodes"), - }; + } } - }; + } } - } + }; - // Just remove the rest + // Just remove the rest from the dom for (idx, _) in node.template.roots.iter().enumerate().skip(1) { self.remove_root_node(node, idx); } + + // Garabge collect all of the nodes since this gets used in replace + self.clean_up_node(node); + + id + } + + /// Clean up the node, not generating mutations + /// + /// Simply walks through the dynamic nodes + fn clean_up_node(&mut self, node: &'b VNode<'b>) { + for node in node.dynamic_nodes { + match node { + Component(comp) => { + let scope = comp.scope.get().unwrap(); + match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { + RenderReturn::Sync(Ok(t)) => self.clean_up_node(t), + _ => todo!("cannot handle nonstandard nodes"), + }; + } + Text(t) => self.reclaim(t.id.get()), + Fragment(VFragment::Empty(t)) => self.reclaim(t.get()), + Fragment(VFragment::NonEmpty(nodes)) => { + nodes.into_iter().for_each(|node| self.clean_up_node(node)) + } + }; + } } fn remove_root_node(&mut self, node: &'b VNode<'b>, idx: usize) { let root = node.template.roots[idx]; match root { TemplateNode::Element { .. } | TemplateNode::Text(_) => { - self.mutations.push(Mutation::Remove { - id: node.root_ids[idx].get(), - }) + let id = node.root_ids[idx].get(); + self.mutations.push(Mutation::Remove { id }); + self.reclaim(id); } TemplateNode::DynamicText(id) | TemplateNode::Dynamic(id) => { match &node.dynamic_nodes[id] { - Text(i) => self.mutations.push(Mutation::Remove { id: i.id.get() }), + Text(i) => { + let id = i.id.get(); + self.mutations.push(Mutation::Remove { id }); + self.reclaim(id); + } Fragment(VFragment::Empty(e)) => { - self.mutations.push(Mutation::Remove { id: e.get() }) + let id = e.get(); + self.mutations.push(Mutation::Remove { id }); + self.reclaim(id); } Fragment(VFragment::NonEmpty(nodes)) => self.remove_nodes(nodes), Component(comp) => { @@ -789,26 +823,15 @@ impl<'b: 'static> VirtualDom { } } - fn replace_template(&mut self, left: &'b VNode<'b>, right: &'b VNode<'b>) { - todo!("replacing should work!") - } - - fn replace( - &mut self, - left_template: &'b VNode<'b>, - right_template: &'b VNode<'b>, - 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!(), - } - } + fn replace(&mut self, left: &'b VNode<'b>, right: &'b VNode<'b>) { + let first = self.find_first_element(left); + let id = self.replace_inner(left); + let created = self.create(right); + self.mutations.push(Mutation::ReplaceWith { + id: first, + m: created, + }); + self.reclaim(id); } } diff --git a/packages/core/src/garbage.rs b/packages/core/src/garbage.rs index 056b14481..2d9e1d6d7 100644 --- a/packages/core/src/garbage.rs +++ b/packages/core/src/garbage.rs @@ -1,4 +1,6 @@ -use crate::{nodes::VNode, scopes::ScopeId, virtual_dom::VirtualDom, DynamicNode, Mutations}; +use crate::{ + nodes::VNode, scopes::ScopeId, virtual_dom::VirtualDom, DynamicNode, ElementId, Mutations, +}; impl<'b> VirtualDom { pub fn drop_scope(&mut self, id: ScopeId) { @@ -11,6 +13,11 @@ impl<'b> VirtualDom { todo!() } + pub fn reclaim(&mut self, el: ElementId) { + assert_ne!(el, ElementId(0)); + self.elements.remove(el.0); + } + pub fn drop_template( &mut self, mutations: &mut Mutations, diff --git a/packages/core/src/scheduler/wait.rs b/packages/core/src/scheduler/wait.rs index feb781b17..794abedc2 100644 --- a/packages/core/src/scheduler/wait.rs +++ b/packages/core/src/scheduler/wait.rs @@ -78,19 +78,22 @@ impl VirtualDom { let template: &VNode = unsafe { std::mem::transmute(template) }; let mutations: &mut Mutations = unsafe { std::mem::transmute(mutations) }; - todo!(); - // let place_holder_id = scope.placeholder.get().unwrap(); - // self.scope_stack.push(scope_id); - // let created = self.create(mutations, template); - // self.scope_stack.pop(); - // mutations.push(Mutation::ReplaceWith { - // id: place_holder_id, - // m: created, - // }); + std::mem::swap(&mut self.mutations, mutations); - // for leaf in self.collected_leaves.drain(..) { - // fiber.waiting_on.borrow_mut().insert(leaf); - // } + let place_holder_id = scope.placeholder.get().unwrap(); + self.scope_stack.push(scope_id); + let created = self.create(template); + self.scope_stack.pop(); + mutations.push(Mutation::ReplaceWith { + id: place_holder_id, + m: created, + }); + + for leaf in self.collected_leaves.drain(..) { + fiber.waiting_on.borrow_mut().insert(leaf); + } + + std::mem::swap(&mut self.mutations, mutations); if fiber.waiting_on.borrow().is_empty() { println!("fiber is finished!"); diff --git a/packages/core/tests/bubble_error.rs b/packages/core/tests/bubble_error.rs index 46a1a3579..310f5118d 100644 --- a/packages/core/tests/bubble_error.rs +++ b/packages/core/tests/bubble_error.rs @@ -1,69 +1,30 @@ //! we should properly bubble up errors from components -use std::{error::Error as StdError, marker::PhantomData, string::ParseError}; - -use anyhow::{anyhow, bail}; use dioxus::prelude::*; -// todo: add these to dioxus -pub trait Reject: Sized { - fn reject_err(self, t: impl FnOnce(E) -> anyhow::Error) -> Result { - todo!() - } - fn reject_because(self, t: impl Into) -> Result { - todo!() - } - - fn reject(self) -> Result { - todo!() - } -} - -impl Reject for &Result { - fn reject_err(self, t: impl FnOnce(E) -> anyhow::Error) -> Result { - todo!() - } -} - -/// Call "clone" on the underlying error so it can be propogated out -pub trait CloneErr { - fn clone_err(&self) -> Result<&T, E::Owned> - where - Self: Sized; -} - -impl CloneErr for Result { - fn clone_err(&self) -> Result<&T, E::Owned> - where - Self: Sized, - { - match self { - Ok(s) => Ok(s), - Err(e) => Err(e.to_owned()), - } - } -} - fn app(cx: Scope) -> Element { - // propgates error upwards, does not give a reason, lets Dioxus figure it out - let value = cx.use_hook(|| "123123123.123".parse::()).reject()?; + let raw = match cx.generation() % 2 { + 0 => "123.123", + 1 => "123.123.123", + _ => unreachable!(), + }; - // propgates error upwards, gives a reason - let value = cx - .use_hook(|| "123123123.123".parse::()) - .reject_because("Parsing float failed")?; + let value = raw.parse::()?; - let value = cx.use_hook(|| "123123123.123".parse::()).clone_err()?; - - let value = cx - .use_hook(|| "123123123.123".parse::()) - .as_ref() - .map_err(Clone::clone)?; - - let value = cx - .use_hook(|| "123123123.123".parse::()) - .as_ref() - .map_err(|_| anyhow!("Parsing float failed"))?; - - todo!() + cx.render(rsx! { + div { "hello {value}" } + }) +} + +#[test] +fn it_goes() { + let mut dom = VirtualDom::new(app); + + let edits = dom.rebuild().santize(); + + dbg!(edits); + + dom.mark_dirty_scope(ScopeId(0)); + + dom.render_immediate(); } diff --git a/packages/core/tests/create_fragments.rs b/packages/core/tests/create_fragments.rs index 6ec19372d..5b67711a8 100644 --- a/packages/core/tests/create_fragments.rs +++ b/packages/core/tests/create_fragments.rs @@ -16,7 +16,7 @@ fn empty_fragment_creates_nothing() { assert_eq!( edits.edits, [ - CreatePlaceholder { id: ElementId(2) }, + CreatePlaceholder { id: ElementId(1) }, AppendChildren { m: 1 } ] ); diff --git a/packages/core/tests/create_lists.rs b/packages/core/tests/create_lists.rs index 5848bc788..30d6abc2f 100644 --- a/packages/core/tests/create_lists.rs +++ b/packages/core/tests/create_lists.rs @@ -34,7 +34,7 @@ fn list_renders() { CreateElement { name: "div" }, // 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(0) }, + CreateStaticPlaceholder, AppendChildren { m: 1 }, SaveTemplate { name: "template", m: 1 }, // Create the inner template div diff --git a/packages/core/tests/cycle.rs b/packages/core/tests/cycle.rs new file mode 100644 index 000000000..803edede7 --- /dev/null +++ b/packages/core/tests/cycle.rs @@ -0,0 +1,51 @@ +use dioxus::core::{ElementId, Mutation::*}; +use dioxus::prelude::*; + +/// As we clean up old templates, the ID for the node should cycle +#[test] +fn cycling_elements() { + let mut dom = VirtualDom::new(|cx| { + cx.render(match cx.generation() % 2 { + 0 => rsx! { div { "wasd" } }, + 1 => rsx! { div { "abcd" } }, + _ => unreachable!(), + }) + }); + + let edits = dom.rebuild().santize(); + assert_eq!( + edits.edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, + AppendChildren { m: 1 }, + ] + ); + + dom.mark_dirty_scope(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, + ReplaceWith { id: ElementId(1,), m: 1 }, + ] + ); + + // notice that the IDs cycle back to ElementId(1), preserving a minimal memory footprint + dom.mark_dirty_scope(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, + ReplaceWith { id: ElementId(2,), m: 1 }, + ] + ); + + dom.mark_dirty_scope(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, + ReplaceWith { id: ElementId(1,), m: 1 }, + ] + ); +} diff --git a/packages/core/tests/diff_element.rs b/packages/core/tests/diff_element.rs index 241adabe8..f4e5f108d 100644 --- a/packages/core/tests/diff_element.rs +++ b/packages/core/tests/diff_element.rs @@ -47,14 +47,38 @@ fn element_swap() { vdom.rebuild(); vdom.mark_dirty_scope(ScopeId(0)); - dbg!(vdom.render_immediate()); + assert_eq!( + vdom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, + ReplaceWith { id: ElementId(1,), m: 1 }, + ] + ); vdom.mark_dirty_scope(ScopeId(0)); - dbg!(vdom.render_immediate()); + assert_eq!( + vdom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(3,) }, + ReplaceWith { id: ElementId(2,), m: 1 }, + ] + ); vdom.mark_dirty_scope(ScopeId(0)); - dbg!(vdom.render_immediate()); + assert_eq!( + vdom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(4,) }, + ReplaceWith { id: ElementId(3,), m: 1 }, + ] + ); vdom.mark_dirty_scope(ScopeId(0)); - dbg!(vdom.render_immediate()); + assert_eq!( + vdom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(5,) }, + ReplaceWith { id: ElementId(4,), m: 1 }, + ] + ); } diff --git a/packages/core/tests/suspend.rs b/packages/core/tests/suspend.rs index 13e4359d3..559c4e4ce 100644 --- a/packages/core/tests/suspend.rs +++ b/packages/core/tests/suspend.rs @@ -16,7 +16,7 @@ async fn it_works() { [ CreateElement { name: "div" }, CreateStaticText { value: "Waiting for child..." }, - CreatePlaceholder { id: ElementId(0) }, + CreateStaticPlaceholder, AppendChildren { m: 2 }, SaveTemplate { name: "template", m: 1 } ] diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index 15e9c00a9..f1fe0cafb 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -255,10 +255,9 @@ impl<'a> DynamicContext<'a> { self.dynamic_nodes.push(root); self.node_paths.push(self.current_path.clone()); - if let BodyNode::Text(_) = root { - quote! { ::dioxus::core::TemplateNode::DynamicText(#ct) } - } else { - quote! { ::dioxus::core::TemplateNode::Dynamic(#ct) } + match root { + BodyNode::Text(_) => quote! { ::dioxus::core::TemplateNode::DynamicText(#ct) }, + _ => quote! { ::dioxus::core::TemplateNode::Dynamic(#ct) }, } } } From bffb2644a377d5365b524cd1b676cb79478c94f1 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 25 Nov 2022 02:12:29 -0500 Subject: [PATCH 02/18] fix: node reclaimation --- packages/core/src/arena.rs | 2 + packages/core/src/diff.rs | 177 +++++++++++------------ packages/core/src/garbage.rs | 1 + packages/core/src/nodes.rs | 9 ++ packages/core/tests/diff_element.rs | 10 +- packages/core/tests/diff_unkeyed_list.rs | 55 ++++--- 6 files changed, 129 insertions(+), 125 deletions(-) diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index c4fbe2b23..98fe478e6 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -31,6 +31,8 @@ impl VirtualDom { path, }); + println!("Claiming {}", id); + ElementId(id) } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 541167486..21091f961 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -227,17 +227,16 @@ impl<'b: 'static> VirtualDom { } fn replace_placeholder_with_nodes(&mut self, l: &'b Cell, r: &'b [VNode<'b>]) { - let created = self.create_children(r); - self.mutations.push(Mutation::ReplaceWith { - id: l.get(), - m: created, - }) + let m = self.create_children(r); + let id = l.get(); + self.mutations.push(Mutation::ReplaceWith { id, m }); + self.reclaim(id); } fn replace_nodes_with_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b Cell) { // Remove the old nodes, except for onea - self.remove_nodes(&l[1..]); let first = self.replace_inner(&l[0]); + self.remove_nodes(&l[1..]); let placeholder = self.next_element(&l[0], &[]); r.set(placeholder); @@ -252,24 +251,20 @@ impl<'b: 'static> VirtualDom { // Remove all the top-level nodes, returning the firstmost root ElementId fn replace_inner(&mut self, node: &'b VNode<'b>) -> ElementId { - let id = match &node.template.roots[0] { - TemplateNode::Text(_) | TemplateNode::Element { .. } => node.root_ids[0].get(), - TemplateNode::DynamicText(id) | TemplateNode::Dynamic(id) => { - match &node.dynamic_nodes[*id] { - Text(t) => t.id.get(), - Fragment(VFragment::Empty(e)) => e.get(), - Fragment(VFragment::NonEmpty(nodes)) => { - let id = self.replace_inner(&nodes[0]); - self.remove_nodes(&nodes[1..]); - id - } - Component(comp) => { - let scope = comp.scope.get().unwrap(); - match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { - RenderReturn::Sync(Ok(t)) => self.replace_inner(t), - _ => todo!("cannot handle nonstandard nodes"), - } - } + let id = match node.dynamic_root(0) { + None => node.root_ids[0].get(), + Some(Text(t)) => t.id.get(), + Some(Fragment(VFragment::Empty(e))) => e.get(), + Some(Fragment(VFragment::NonEmpty(nodes))) => { + let id = self.replace_inner(&nodes[0]); + self.remove_nodes(&nodes[1..]); + id + } + Some(Component(comp)) => { + let scope = comp.scope.get().unwrap(); + match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { + RenderReturn::Sync(Ok(t)) => self.replace_inner(t), + _ => todo!("cannot handle nonstandard nodes"), } } }; @@ -305,40 +300,43 @@ impl<'b: 'static> VirtualDom { } }; } + + // we also need to clean up dynamic attribute roots + // let last_node = None; + // for attr in node.dynamic_attrs { + // match last_node { + // Some(node) => todo!(), + // None => todo!(), + // } + // } } fn remove_root_node(&mut self, node: &'b VNode<'b>, idx: usize) { - let root = node.template.roots[idx]; - match root { - TemplateNode::Element { .. } | TemplateNode::Text(_) => { + match node.dynamic_root(idx) { + Some(Text(i)) => { + let id = i.id.get(); + self.mutations.push(Mutation::Remove { id }); + self.reclaim(id); + } + Some(Fragment(VFragment::Empty(e))) => { + let id = e.get(); + self.mutations.push(Mutation::Remove { id }); + self.reclaim(id); + } + Some(Fragment(VFragment::NonEmpty(nodes))) => self.remove_nodes(nodes), + Some(Component(comp)) => { + let scope = comp.scope.get().unwrap(); + match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { + RenderReturn::Sync(Ok(t)) => self.remove_node(t), + _ => todo!("cannot handle nonstandard nodes"), + }; + } + None => { let id = node.root_ids[idx].get(); self.mutations.push(Mutation::Remove { id }); self.reclaim(id); } - - TemplateNode::DynamicText(id) | TemplateNode::Dynamic(id) => { - match &node.dynamic_nodes[id] { - Text(i) => { - let id = i.id.get(); - self.mutations.push(Mutation::Remove { id }); - self.reclaim(id); - } - Fragment(VFragment::Empty(e)) => { - let id = e.get(); - self.mutations.push(Mutation::Remove { id }); - self.reclaim(id); - } - Fragment(VFragment::NonEmpty(nodes)) => self.remove_nodes(nodes), - Component(comp) => { - let scope = comp.scope.get().unwrap(); - match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { - RenderReturn::Sync(Ok(t)) => self.remove_node(t), - _ => todo!("cannot handle nonstandard nodes"), - }; - } - }; - } - } + }; } fn diff_non_empty_fragment(&mut self, new: &'b [VNode<'b>], old: &'b [VNode<'b>]) { @@ -729,21 +727,26 @@ impl<'b: 'static> VirtualDom { } 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()), - } - } + for (idx, _) in node.template.roots.iter().enumerate() { + let id = match node.dynamic_root(idx) { + Some(Text(t)) => t.id.get(), + Some(Fragment(VFragment::Empty(t))) => t.get(), + Some(Fragment(VFragment::NonEmpty(t))) => return self.remove_nodes(t), + Some(Component(comp)) => return self.remove_component(comp.scope.get().unwrap()), + None => node.root_ids[idx].get(), }; self.mutations.push(Mutation::Remove { id }) } + + self.clean_up_node(node); + + for root in node.root_ids { + let id = root.get(); + if id.0 != 0 { + self.reclaim(id); + } + } } fn remove_component(&mut self, scope_id: ScopeId) { @@ -782,42 +785,32 @@ impl<'b: 'static> VirtualDom { } fn find_first_element(&self, node: &VNode) -> ElementId { - 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"), - } - } + match node.dynamic_root(0) { + None => node.root_ids[0].get(), + Some(Text(t)) => t.id.get(), + Some(Fragment(VFragment::NonEmpty(t))) => self.find_first_element(&t[0]), + Some(Fragment(VFragment::Empty(t))) => t.get(), + Some(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 { .. } | 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"), - } - } + match node.dynamic_root(node.template.roots.len() - 1) { + None => node.root_ids.last().unwrap().get(), + Some(Text(t)) => t.id.get(), + Some(Fragment(VFragment::NonEmpty(t))) => self.find_last_element(t.last().unwrap()), + Some(Fragment(VFragment::Empty(t))) => t.get(), + Some(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"), } } } diff --git a/packages/core/src/garbage.rs b/packages/core/src/garbage.rs index 2d9e1d6d7..66a54c2d9 100644 --- a/packages/core/src/garbage.rs +++ b/packages/core/src/garbage.rs @@ -15,6 +15,7 @@ impl<'b> VirtualDom { pub fn reclaim(&mut self, el: ElementId) { assert_ne!(el, ElementId(0)); + println!("reclaiming {}", el.0); self.elements.remove(el.0); } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 1719388ad..1750d4276 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -54,6 +54,15 @@ impl<'a> VNode<'a> { }, }) } + + pub fn dynamic_root(&self, idx: usize) -> Option<&'a DynamicNode<'a>> { + match &self.template.roots[idx] { + TemplateNode::Element { .. } | TemplateNode::Text(_) => None, + TemplateNode::Dynamic(id) | TemplateNode::DynamicText(id) => { + Some(&self.dynamic_nodes[*id]) + } + } + } } #[derive(Debug, Clone, Copy)] diff --git a/packages/core/tests/diff_element.rs b/packages/core/tests/diff_element.rs index f4e5f108d..5dff0fbc8 100644 --- a/packages/core/tests/diff_element.rs +++ b/packages/core/tests/diff_element.rs @@ -59,7 +59,7 @@ fn element_swap() { assert_eq!( vdom.render_immediate().santize().edits, [ - LoadTemplate { name: "template", index: 0, id: ElementId(3,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, ReplaceWith { id: ElementId(2,), m: 1 }, ] ); @@ -68,8 +68,8 @@ fn element_swap() { assert_eq!( vdom.render_immediate().santize().edits, [ - LoadTemplate { name: "template", index: 0, id: ElementId(4,) }, - ReplaceWith { id: ElementId(3,), m: 1 }, + LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, + ReplaceWith { id: ElementId(1,), m: 1 }, ] ); @@ -77,8 +77,8 @@ fn element_swap() { assert_eq!( vdom.render_immediate().santize().edits, [ - LoadTemplate { name: "template", index: 0, id: ElementId(5,) }, - ReplaceWith { id: ElementId(4,), m: 1 }, + LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, + ReplaceWith { id: ElementId(2,), m: 1 }, ] ); } diff --git a/packages/core/tests/diff_unkeyed_list.rs b/packages/core/tests/diff_unkeyed_list.rs index de7252f4e..6d8f0a236 100644 --- a/packages/core/tests/diff_unkeyed_list.rs +++ b/packages/core/tests/diff_unkeyed_list.rs @@ -41,8 +41,8 @@ fn list_creates_one_by_one() { assert_eq!( dom.render_immediate().santize().edits, [ - LoadTemplate { name: "template", index: 0, id: ElementId(5,) }, - HydrateText { path: &[0], value: "1", id: ElementId(6,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, + HydrateText { path: &[0], value: "1", id: ElementId(5,) }, InsertAfter { id: ElementId(3,), m: 1 }, ] ); @@ -52,9 +52,9 @@ fn list_creates_one_by_one() { 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 }, + LoadTemplate { name: "template", index: 0, id: ElementId(6,) }, + HydrateText { path: &[0], value: "2", id: ElementId(7,) }, + InsertAfter { id: ElementId(2,), m: 1 }, ] ); @@ -63,9 +63,9 @@ fn list_creates_one_by_one() { 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 }, + LoadTemplate { name: "template", index: 0, id: ElementId(8,) }, + HydrateText { path: &[0], value: "3", id: ElementId(9,) }, + InsertAfter { id: ElementId(6,), m: 1 }, ] ); } @@ -125,7 +125,7 @@ fn removes_one_by_one() { assert_eq!( dom.render_immediate().santize().edits, [ - CreatePlaceholder { id: ElementId(8) }, + CreatePlaceholder { id: ElementId(3) }, ReplaceWith { id: ElementId(2), m: 1 } ] ); @@ -136,13 +136,13 @@ fn removes_one_by_one() { assert_eq!( dom.render_immediate().santize().edits, [ - LoadTemplate { name: "template", index: 0, id: ElementId(9) }, - HydrateText { path: &[0], value: "0", id: ElementId(10) }, - LoadTemplate { name: "template", index: 0, id: ElementId(11) }, - HydrateText { path: &[0], value: "1", id: ElementId(12) }, - LoadTemplate { name: "template", index: 0, id: ElementId(13) }, - HydrateText { path: &[0], value: "2", id: ElementId(14) }, - ReplaceWith { id: ElementId(8), m: 3 } + LoadTemplate { name: "template", index: 0, id: ElementId(2) }, + HydrateText { path: &[0], value: "0", id: ElementId(4) }, + LoadTemplate { name: "template", index: 0, id: ElementId(5) }, + HydrateText { path: &[0], value: "1", id: ElementId(6) }, + LoadTemplate { name: "template", index: 0, id: ElementId(7) }, + HydrateText { path: &[0], value: "2", id: ElementId(8) }, + ReplaceWith { id: ElementId(3), m: 3 } ] ); } @@ -150,10 +150,9 @@ fn removes_one_by_one() { #[test] fn list_shrink_multiroot() { let mut dom = VirtualDom::new(|cx| { - let gen = cx.generation(); cx.render(rsx! { div { - (0..gen).map(|i| rsx! { + (0..cx.generation()).map(|i| rsx! { div { "{i}" } div { "{i}" } }) @@ -186,10 +185,10 @@ fn list_shrink_multiroot() { assert_eq!( dom.render_immediate().santize().edits, [ - LoadTemplate { name: "template", index: 0, id: ElementId(7) }, - HydrateText { path: &[0], value: "1", id: ElementId(8) }, - LoadTemplate { name: "template", index: 1, id: ElementId(9) }, - HydrateText { path: &[0], value: "1", id: ElementId(10) }, + LoadTemplate { name: "template", index: 0, id: ElementId(2) }, + HydrateText { path: &[0], value: "1", id: ElementId(7) }, + LoadTemplate { name: "template", index: 1, id: ElementId(8) }, + HydrateText { path: &[0], value: "1", id: ElementId(9) }, InsertAfter { id: ElementId(5), m: 2 } ] ); @@ -198,11 +197,11 @@ fn list_shrink_multiroot() { assert_eq!( dom.render_immediate().santize().edits, [ - LoadTemplate { name: "template", index: 0, id: ElementId(11) }, - HydrateText { path: &[0], value: "2", id: ElementId(12) }, - LoadTemplate { name: "template", index: 1, id: ElementId(13) }, - HydrateText { path: &[0], value: "2", id: ElementId(14) }, - InsertAfter { id: ElementId(9), m: 2 } + LoadTemplate { name: "template", index: 0, id: ElementId(10) }, + HydrateText { path: &[0], value: "2", id: ElementId(11) }, + LoadTemplate { name: "template", index: 1, id: ElementId(12) }, + HydrateText { path: &[0], value: "2", id: ElementId(13) }, + InsertAfter { id: ElementId(8), m: 2 } ] ); } @@ -266,7 +265,7 @@ fn removes_one_by_one_multiroot() { dom.render_immediate().santize().edits, [ Remove { id: ElementId(4) }, - CreatePlaceholder { id: ElementId(14) }, + CreatePlaceholder { id: ElementId(5) }, ReplaceWith { id: ElementId(2), m: 1 } ] ); From 399169800db7bbc6e58c3e4b02fe22048057e26e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 27 Nov 2022 00:22:39 -0500 Subject: [PATCH 03/18] chore: dynamic attributes cleanup --- packages/core/src/arena.rs | 47 +++++++++++++++++---- packages/core/src/create.rs | 10 ++++- packages/core/src/diff.rs | 26 ++++++++---- packages/core/src/garbage.rs | 42 ------------------- packages/core/src/lib.rs | 1 - packages/core/tests/attr_cleanup.rs | 65 +++++++++++++++++++++++++++++ 6 files changed, 130 insertions(+), 61 deletions(-) delete mode 100644 packages/core/src/garbage.rs create mode 100644 packages/core/tests/attr_cleanup.rs diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 98fe478e6..ec4ff862c 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -1,4 +1,4 @@ -use crate::{nodes::VNode, virtual_dom::VirtualDom}; +use crate::{nodes::VNode, virtual_dom::VirtualDom, Mutations, ScopeId}; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] @@ -21,7 +21,7 @@ impl ElementRef { } } -impl VirtualDom { +impl<'b> VirtualDom { pub fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId { let entry = self.elements.vacant_entry(); let id = entry.key(); @@ -39,11 +39,40 @@ impl VirtualDom { pub fn cleanup_element(&mut self, id: ElementId) { self.elements.remove(id.0); } + + pub fn drop_scope(&mut self, id: ScopeId) { + // let scope = self.scopes.get(id.0).unwrap(); + + // let root = scope.root_node(); + // let root = unsafe { std::mem::transmute(root) }; + + // self.drop_template(root, false); + todo!() + } + + pub fn reclaim(&mut self, el: ElementId) { + assert_ne!(el, ElementId(0)); + self.elements.remove(el.0); + } + + pub fn drop_template( + &mut self, + mutations: &mut Mutations, + template: &'b VNode<'b>, + gen_roots: bool, + ) { + // for node in template.dynamic_nodes.iter() { + // match node { + // DynamicNode::Text { id, .. } => {} + + // DynamicNode::Component { .. } => { + // todo!() + // } + + // DynamicNode::Fragment { inner, nodes } => {} + // DynamicNode::Placeholder(_) => todo!(), + // _ => todo!(), + // } + // } + } } - -/* -now...... - -an ID is mostly a pointer to a node in the real dom. -We need to -*/ diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index b0c1a10a9..ccc95887a 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -80,7 +80,15 @@ impl<'b: 'static> VirtualDom { // 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]), + _ => { + let id = self + .next_element(template, template.template.attr_paths[attr_id]); + self.mutations.push(Mutation::AssignId { + path: &path[1..], + id, + }); + id + } }; loop { diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 21091f961..bb6a66efc 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -301,14 +301,24 @@ impl<'b: 'static> VirtualDom { }; } - // we also need to clean up dynamic attribute roots - // let last_node = None; - // for attr in node.dynamic_attrs { - // match last_node { - // Some(node) => todo!(), - // None => todo!(), - // } - // } + // we clean up nodes with dynamic attributes, provided the node is unique and not a root node + let mut id = None; + for (idx, attr) in node.dynamic_attrs.into_iter().enumerate() { + // We'll clean up the root nodes either way, so don't worry + if node.template.attr_paths[idx].len() == 1 { + continue; + } + + let next_id = attr.mounted_element.get(); + + if id == Some(next_id) { + continue; + } + + id = Some(next_id); + + self.reclaim(next_id); + } } fn remove_root_node(&mut self, node: &'b VNode<'b>, idx: usize) { diff --git a/packages/core/src/garbage.rs b/packages/core/src/garbage.rs deleted file mode 100644 index 66a54c2d9..000000000 --- a/packages/core/src/garbage.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::{ - nodes::VNode, scopes::ScopeId, virtual_dom::VirtualDom, DynamicNode, ElementId, Mutations, -}; - -impl<'b> VirtualDom { - pub fn drop_scope(&mut self, id: ScopeId) { - // let scope = self.scopes.get(id.0).unwrap(); - - // let root = scope.root_node(); - // let root = unsafe { std::mem::transmute(root) }; - - // self.drop_template(root, false); - todo!() - } - - pub fn reclaim(&mut self, el: ElementId) { - assert_ne!(el, ElementId(0)); - println!("reclaiming {}", el.0); - self.elements.remove(el.0); - } - - pub fn drop_template( - &mut self, - mutations: &mut Mutations, - template: &'b VNode<'b>, - gen_roots: bool, - ) { - // for node in template.dynamic_nodes.iter() { - // match node { - // DynamicNode::Text { id, .. } => {} - - // DynamicNode::Component { .. } => { - // todo!() - // } - - // DynamicNode::Fragment { inner, nodes } => {} - // DynamicNode::Placeholder(_) => todo!(), - // _ => todo!(), - // } - // } - } -} diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index d88e73986..ac9b4e10f 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -8,7 +8,6 @@ mod error_boundary; mod events; mod factory; mod fragment; -mod garbage; mod lazynodes; mod mutations; mod nodes; diff --git a/packages/core/tests/attr_cleanup.rs b/packages/core/tests/attr_cleanup.rs new file mode 100644 index 000000000..f8aa39835 --- /dev/null +++ b/packages/core/tests/attr_cleanup.rs @@ -0,0 +1,65 @@ +//! dynamic attributes in dioxus necessitate an allocated node ID. +//! +//! This tests to ensure we clean it up + +use dioxus::core::{ElementId, Mutation::*}; +use dioxus::prelude::*; + +#[test] +fn attrs_cycle() { + let mut dom = VirtualDom::new(|cx| { + let id = cx.generation(); + match cx.generation() % 2 { + 0 => cx.render(rsx! { + div {} + }), + 1 => cx.render(rsx! { + div { + h1 { class: "{id}", id: "{id}" } + } + }), + _ => unreachable!(), + } + }); + + assert_eq!( + dom.rebuild().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, + AppendChildren { m: 1 }, + ] + ); + + dom.mark_dirty_scope(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, + AssignId { path: &[0,], id: ElementId(3,) }, + SetAttribute { name: "class", value: "1", id: ElementId(3,), ns: None }, + SetAttribute { name: "id", value: "1", id: ElementId(3,), ns: None }, + ReplaceWith { id: ElementId(1,), m: 1 }, + ] + ); + + dom.mark_dirty_scope(ScopeId(0)); + dbg!( + dom.render_immediate().santize(), + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, + ReplaceWith { id: ElementId(2,), m: 1 }, + ] + ); + + dom.mark_dirty_scope(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(2) }, + AssignId { path: &[0], id: ElementId(1) }, + SetAttribute { name: "class", value: "3", id: ElementId(1), ns: None }, + SetAttribute { name: "id", value: "3", id: ElementId(1), ns: None }, + ReplaceWith { id: ElementId(3), m: 1 } + ] + ); +} From 04296bb88d172f43f8c6db98d5190bd627a0e1f4 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 27 Nov 2022 00:25:35 -0500 Subject: [PATCH 04/18] chore: fix tests --- packages/core/tests/attr_cleanup.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/core/tests/attr_cleanup.rs b/packages/core/tests/attr_cleanup.rs index f8aa39835..2f70ca527 100644 --- a/packages/core/tests/attr_cleanup.rs +++ b/packages/core/tests/attr_cleanup.rs @@ -43,11 +43,11 @@ fn attrs_cycle() { ); dom.mark_dirty_scope(ScopeId(0)); - dbg!( - dom.render_immediate().santize(), + assert_eq!( + dom.render_immediate().santize().edits, [ - LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, - ReplaceWith { id: ElementId(2,), m: 1 }, + LoadTemplate { name: "template", index: 0, id: ElementId(3) }, + ReplaceWith { id: ElementId(2), m: 1 } ] ); @@ -62,4 +62,14 @@ fn attrs_cycle() { ReplaceWith { id: ElementId(3), m: 1 } ] ); + + // we take the node taken by attributes since we reused it + dom.mark_dirty_scope(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1) }, + ReplaceWith { id: ElementId(2), m: 1 } + ] + ); } From d0554b9ed68a09ff74c15eb44f1d01c19f4e93b8 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 27 Nov 2022 00:38:40 -0500 Subject: [PATCH 05/18] chore: add tests for context api --- packages/core/src/virtual_dom.rs | 5 + packages/core/tests/context_api.rs | 51 ++++++ packages/dioxus/tests/create_dom.rs | 248 -------------------------- packages/dioxus/tests/earlyabort.rs | 67 ------- packages/dioxus/tests/vdom_rebuild.rs | 104 ----------- 5 files changed, 56 insertions(+), 419 deletions(-) create mode 100644 packages/core/tests/context_api.rs delete mode 100644 packages/dioxus/tests/create_dom.rs delete mode 100644 packages/dioxus/tests/earlyabort.rs delete mode 100644 packages/dioxus/tests/vdom_rebuild.rs diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 1a94d04e5..cbb1f7250 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -290,6 +290,11 @@ impl VirtualDom { self.dirty_scopes.insert(DirtyScope { height, id }); } + /// Mark the entire tree as dirty. + /// + /// Will force a re-render of every component + pub fn mark_dirty_all(&mut self) {} + /// Determine whether or not a scope is currently in a suspended state /// /// This does not mean the scope is waiting on its own futures, just that the tree that the scope exists in is diff --git a/packages/core/tests/context_api.rs b/packages/core/tests/context_api.rs new file mode 100644 index 000000000..71069d2fc --- /dev/null +++ b/packages/core/tests/context_api.rs @@ -0,0 +1,51 @@ +use dioxus::core::{ElementId, Mutation::*}; +use dioxus::prelude::*; + +#[test] +fn state_shares() { + fn app(cx: Scope) -> Element { + cx.provide_context(cx.generation() as i32); + + cx.render(rsx!(child_1 {})) + } + + fn child_1(cx: Scope) -> Element { + cx.render(rsx!(child_2 {})) + } + + fn child_2(cx: Scope) -> Element { + let value = cx.consume_context::().unwrap(); + cx.render(rsx!("Value is {value}")) + } + + let mut dom = VirtualDom::new(app); + assert_eq!( + dom.rebuild().santize().edits, + [ + CreateTextNode { value: "Value is 0", id: ElementId(1,) }, + AppendChildren { m: 1 }, + ] + ); + + dom.mark_dirty_scope(ScopeId(0)); + dom.render_immediate(); + assert_eq!(dom.base_scope().consume_context::().unwrap(), 1); + + dom.mark_dirty_scope(ScopeId(0)); + dom.render_immediate(); + assert_eq!(dom.base_scope().consume_context::().unwrap(), 2); + + dom.mark_dirty_scope(ScopeId(2)); + assert_eq!( + dom.render_immediate().santize().edits, + [SetText { value: "Value is 2", id: ElementId(1,) },] + ); + + dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty_scope(ScopeId(2)); + let edits = dom.render_immediate(); + assert_eq!( + edits.santize().edits, + [SetText { value: "Value is 3", id: ElementId(1,) },] + ); +} diff --git a/packages/dioxus/tests/create_dom.rs b/packages/dioxus/tests/create_dom.rs deleted file mode 100644 index a49efb1f7..000000000 --- a/packages/dioxus/tests/create_dom.rs +++ /dev/null @@ -1,248 +0,0 @@ -#![allow(unused, non_upper_case_globals, non_snake_case)] - -//! Prove that the dom works normally through virtualdom methods. -//! -//! This methods all use "rebuild" which completely bypasses the scheduler. -//! Hard rebuilds don't consume any events from the event queue. - -use dioxus::prelude::*; - -use dioxus_edit_stream::DomEdit::*; - -fn new_dom(app: Component

, props: P) -> VirtualDom { - VirtualDom::new_with_props(app, props) -} - -#[test] -fn test_original_diff() { - static APP: Component = |cx| { - cx.render(rsx! { - div { - div { - "Hello, world!" - } - } - }) - }; - - let mut dom = new_dom(APP, ()); - let mutations = dom.rebuild(); - assert_eq!( - mutations.edits, - [ - // create template - CreateElement { root: Some(1), tag: "template", children: 1 }, - CreateElement { root: None, tag: "div", children: 1 }, - CreateElement { root: None, tag: "div", children: 1 }, - CreateTextNode { root: None, text: "Hello, world!" }, - // clone template - CloneNodeChildren { id: Some(1), new_ids: vec![2] }, - // add to root - AppendChildren { root: Some(0), children: vec![2] }, - ] - ); -} - -#[test] -fn create() { - static APP: Component = |cx| { - cx.render(rsx! { - div { - div { - "Hello, world!" - div { - div { - Fragment { - "hello" - "world" - } - } - } - } - } - }) - }; - - let mut dom = new_dom(APP, ()); - let mutations = dom.rebuild(); - - assert_eq!( - mutations.edits, - [ - // create template - CreateElement { root: Some(1), tag: "template", children: 1 }, - CreateElement { root: None, tag: "div", children: 1 }, - CreateElement { root: None, tag: "div", children: 2 }, - CreateTextNode { root: None, text: "Hello, world!" }, - CreateElement { root: None, tag: "div", children: 1 }, - CreateElement { root: None, tag: "div", children: 0 }, - // clone template - CloneNodeChildren { id: Some(1), new_ids: vec![2] }, - SetLastNode { id: 2 }, - FirstChild {}, - StoreWithId { id: 3 }, - FirstChild {}, - NextSibling {}, - StoreWithId { id: 4 }, - FirstChild {}, - StoreWithId { id: 5 }, - CreateTextNode { root: Some(6), text: "hello" }, - CreateTextNode { root: Some(7), text: "world" }, - SetLastNode { id: 5 }, - AppendChildren { root: None, children: vec![6, 7] }, - AppendChildren { root: Some(0), children: vec![2] } - ] - ); -} - -#[test] -fn create_list() { - static APP: Component = |cx| { - cx.render(rsx! { - {(0..3).map(|f| rsx!{ div { - "hello" - }})} - }) - }; - - let mut dom = new_dom(APP, ()); - let mutations = dom.rebuild(); - - assert_eq!( - mutations.edits, - [ - // create template - CreateElement { root: Some(1), tag: "template", children: 1 }, - CreateElement { root: None, tag: "div", children: 1 }, - CreateTextNode { root: None, text: "hello" }, - // clone template - CloneNodeChildren { id: Some(1), new_ids: vec![2] }, - CloneNodeChildren { id: Some(1), new_ids: vec![3] }, - CloneNodeChildren { id: Some(1), new_ids: vec![4] }, - // add to root - AppendChildren { root: Some(0), children: vec![2, 3, 4] }, - ] - ); -} - -#[test] -fn create_simple() { - static APP: Component = |cx| { - cx.render(rsx! { - div {} - div {} - div {} - div {} - }) - }; - - let mut dom = new_dom(APP, ()); - let mutations = dom.rebuild(); - - assert_eq!( - mutations.edits, - [ - // create template - CreateElement { root: Some(1), tag: "template", children: 4 }, - CreateElement { root: None, tag: "div", children: 0 }, - CreateElement { root: None, tag: "div", children: 0 }, - CreateElement { root: None, tag: "div", children: 0 }, - CreateElement { root: None, tag: "div", children: 0 }, - // clone template - CloneNodeChildren { id: Some(1), new_ids: vec![2, 3, 4, 5] }, - // add to root - AppendChildren { root: Some(0), children: vec![2, 3, 4, 5] }, - ] - ); -} -#[test] -fn create_components() { - static App: Component = |cx| { - cx.render(rsx! { - Child { "abc1" } - Child { "abc2" } - Child { "abc3" } - }) - }; - - #[derive(Props)] - struct ChildProps<'a> { - children: Element<'a>, - } - - fn Child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element { - cx.render(rsx! { - h1 {} - div { &cx.props.children } - p {} - }) - } - - let mut dom = new_dom(App, ()); - let mutations = dom.rebuild(); - - assert_eq!( - mutations.edits, - [ - // create template - CreateElement { root: Some(1), tag: "template", children: 3 }, - CreateElement { root: None, tag: "h1", children: 0 }, - CreateElement { root: None, tag: "div", children: 0 }, - CreateElement { root: None, tag: "p", children: 0 }, - // clone template - CloneNodeChildren { id: Some(1), new_ids: vec![2, 3, 4] }, - // update template - SetLastNode { id: 2 }, - NextSibling {}, - CreateTextNode { root: Some(5), text: "abc1" }, - SetLastNode { id: 3 }, - AppendChildren { root: None, children: vec![5] }, - // clone template - CloneNodeChildren { id: Some(1), new_ids: vec![6, 7, 8] }, - SetLastNode { id: 6 }, - NextSibling {}, - // update template - CreateTextNode { root: Some(9), text: "abc2" }, - SetLastNode { id: 7 }, - AppendChildren { root: None, children: vec![9] }, - // clone template - CloneNodeChildren { id: Some(1), new_ids: vec![10, 11, 12] }, - // update template - SetLastNode { id: 10 }, - NextSibling {}, - CreateTextNode { root: Some(13), text: "abc3" }, - SetLastNode { id: 11 }, - AppendChildren { root: None, children: vec![13] }, - // add to root - AppendChildren { root: Some(0), children: vec![2, 3, 4, 6, 7, 8, 10, 11, 12] } - ] - ); -} - -#[test] -fn anchors() { - static App: Component = |cx| { - cx.render(rsx! { - {true.then(|| rsx!{ div { "hello" } })} - {false.then(|| rsx!{ div { "goodbye" } })} - }) - }; - - let mut dom = new_dom(App, ()); - let mutations = dom.rebuild(); - - assert_eq!( - mutations.edits, - [ - // create template - CreateElement { root: Some(1), tag: "template", children: 1 }, - CreateElement { root: None, tag: "div", children: 1 }, - CreateTextNode { root: None, text: "hello" }, - // clone template - CloneNodeChildren { id: Some(1), new_ids: vec![2] }, - CreatePlaceholder { root: Some(3) }, - // add to root - AppendChildren { root: Some(0), children: vec![2, 3] }, - ] - ); -} diff --git a/packages/dioxus/tests/earlyabort.rs b/packages/dioxus/tests/earlyabort.rs deleted file mode 100644 index 11bf3e2ca..000000000 --- a/packages/dioxus/tests/earlyabort.rs +++ /dev/null @@ -1,67 +0,0 @@ -#![allow(unused, non_upper_case_globals, non_snake_case)] - -//! Prove that the dom works normally through virtualdom methods. -//! -//! This methods all use "rebuild" which completely bypasses the scheduler. -//! Hard rebuilds don't consume any events from the event queue. - -use dioxus::prelude::*; - -use dioxus_core::{DomEdit::*, ScopeId}; - -const IS_LOGGING_ENABLED: bool = false; - -fn new_dom(app: Component

, props: P) -> VirtualDom { - VirtualDom::new_with_props(app, props) -} - -/// This test ensures that if a component aborts early, it is replaced with a placeholder. -/// In debug, this should also toss a warning. -#[test] -fn test_early_abort() { - const app: Component = |cx| { - let val = cx.use_hook(|| 0); - - *val += 1; - - if *val == 2 { - return None; - } - - render!(div { "Hello, world!" }) - }; - - let mut dom = new_dom(app, ()); - - let edits = dom.rebuild(); - assert_eq!( - edits.edits, - [ - // create template - CreateElement { root: Some(1), tag: "template", children: 1 }, - CreateElement { root: None, tag: "div", children: 1 }, - CreateTextNode { root: None, text: "Hello, world!" }, - // clone template - CloneNodeChildren { id: Some(1), new_ids: vec![2] }, - AppendChildren { root: Some(0), children: vec![2] } - ] - ); - - let edits = dom.hard_diff(ScopeId(0)); - assert_eq!( - edits.edits, - [ - CreatePlaceholder { root: Some(3) }, - ReplaceWith { root: Some(2), nodes: vec![3] } - ] - ); - - let edits = dom.hard_diff(ScopeId(0)); - assert_eq!( - edits.edits, - [ - CloneNodeChildren { id: Some(1), new_ids: vec![2] }, - ReplaceWith { root: Some(3), nodes: vec![2] } - ] - ); -} diff --git a/packages/dioxus/tests/vdom_rebuild.rs b/packages/dioxus/tests/vdom_rebuild.rs deleted file mode 100644 index 5d19de48e..000000000 --- a/packages/dioxus/tests/vdom_rebuild.rs +++ /dev/null @@ -1,104 +0,0 @@ -#![allow(unused, non_upper_case_globals)] - -//! Rebuilding tests -//! ---------------- -//! -//! This tests module ensures that the initial build of the virtualdom is correct. -//! This does not include dynamic tests or the diffing algorithm itself. -//! -//! It does prove that mounting works properly and the correct edit streams are generated. -//! -//! Don't have a good way to validate, everything is done manually ATM - -use dioxus::prelude::*; -use dioxus_core::DomEdit::*; - -#[test] -fn app_runs() { - static App: Component = |cx| render!(div{"hello"} ); - - let mut vdom = VirtualDom::new(App); - let edits = vdom.rebuild(); -} - -#[test] -fn fragments_work() { - static App: Component = |cx| { - cx.render(rsx!( - div{"hello"} - div{"goodbye"} - )) - }; - let mut vdom = VirtualDom::new(App); - let edits = vdom.rebuild(); - // should result in a final "appendchildren n=2" - dbg!(edits); -} - -#[test] -fn lists_work() { - static App: Component = |cx| { - cx.render(rsx!( - h1 {"hello"} - (0..6).map(|f| rsx!(span{ "{f}" })) - )) - }; - let mut vdom = VirtualDom::new(App); - let edits = vdom.rebuild(); - dbg!(edits); -} - -#[test] -#[ignore] -fn conditional_rendering() { - static App: Component = |cx| { - cx.render(rsx!( - h1 {"hello"} - {true.then(|| rsx!(span{ "a" }))} - {false.then(|| rsx!(span{ "b" }))} - )) - }; - let mut vdom = VirtualDom::new(App); - - let mutations = vdom.rebuild(); - assert_eq!( - mutations.edits, - [ - CreateElement { root: Some(1), tag: "template", children: 3 }, - CreateElement { root: None, tag: "h1", children: 1 }, - CreateTextNode { root: None, text: "hello" }, - CreatePlaceholder { root: None }, - CreatePlaceholder { root: None }, - CloneNodeChildren { id: Some(1), new_ids: vec![2, 3, 4] }, - CreateElement { root: Some(5), tag: "template", children: 1 }, - CreateElement { root: None, tag: "span", children: 1 }, - CreateTextNode { root: None, text: "a" }, - CloneNodeChildren { id: Some(5), new_ids: vec![6] }, - SetLastNode { id: 3 }, - ReplaceWith { root: None, nodes: vec![6] }, - CreatePlaceholder { root: Some(7) }, - SetLastNode { id: 4 }, - ReplaceWith { root: None, nodes: vec![7] }, - AppendChildren { root: Some(0), children: vec![2, 3, 4] } - ] - ) -} - -#[test] -fn child_components() { - static App: Component = |cx| { - cx.render(rsx!( - {true.then(|| rsx!(Child { }))} - {false.then(|| rsx!(Child { }))} - )) - }; - static Child: Component = |cx| { - cx.render(rsx!( - h1 {"hello"} - h1 {"goodbye"} - )) - }; - let mut vdom = VirtualDom::new(App); - let edits = vdom.rebuild(); - dbg!(edits); -} From f614cbb401c3111235752cabeb9edfde865a3290 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 27 Nov 2022 00:56:49 -0500 Subject: [PATCH 06/18] wip: migrate all tests to core --- packages/core/src/create.rs | 2 +- packages/core/src/mutations.rs | 1 + packages/{dioxus => core}/tests/README.md | 0 .../tests/diff_keyed_list.rs} | 0 packages/core/tests/kitchen_sink.rs | 78 +++++++++++ packages/{dioxus => core}/tests/lifecycle.rs | 0 .../{dioxus => core}/tests/miri_stress.rs | 0 .../core/tests/{suspend.rs => suspense.rs} | 25 +++- packages/dioxus/tests/.rustfmt.toml | 1 - packages/dioxus/tests/rsx_syntax.rs | 83 ------------ packages/dioxus/tests/sharedstate.rs | 126 ------------------ packages/dioxus/tests/suspense.rs | 61 --------- 12 files changed, 104 insertions(+), 273 deletions(-) rename packages/{dioxus => core}/tests/README.md (100%) rename packages/{dioxus/tests/diffing.rs => core/tests/diff_keyed_list.rs} (100%) create mode 100644 packages/core/tests/kitchen_sink.rs rename packages/{dioxus => core}/tests/lifecycle.rs (100%) rename packages/{dioxus => core}/tests/miri_stress.rs (100%) rename packages/core/tests/{suspend.rs => suspense.rs} (69%) delete mode 100644 packages/dioxus/tests/.rustfmt.toml delete mode 100644 packages/dioxus/tests/rsx_syntax.rs delete mode 100644 packages/dioxus/tests/sharedstate.rs delete mode 100644 packages/dioxus/tests/suspense.rs diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index ccc95887a..81be55199 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -235,7 +235,7 @@ impl<'b: 'static> VirtualDom { TemplateNode::DynamicText { .. } => self .mutations .template_mutations - .push(CreateStaticText { value: "d" }), + .push(CreateTextPlaceholder), TemplateNode::Element { attrs, diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 3627836df..fa404fafe 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -81,6 +81,7 @@ pub enum Mutation<'a> { id: ElementId, }, CreateStaticPlaceholder, + CreateTextPlaceholder, CreateStaticText { value: &'a str, }, diff --git a/packages/dioxus/tests/README.md b/packages/core/tests/README.md similarity index 100% rename from packages/dioxus/tests/README.md rename to packages/core/tests/README.md diff --git a/packages/dioxus/tests/diffing.rs b/packages/core/tests/diff_keyed_list.rs similarity index 100% rename from packages/dioxus/tests/diffing.rs rename to packages/core/tests/diff_keyed_list.rs diff --git a/packages/core/tests/kitchen_sink.rs b/packages/core/tests/kitchen_sink.rs new file mode 100644 index 000000000..7071cb120 --- /dev/null +++ b/packages/core/tests/kitchen_sink.rs @@ -0,0 +1,78 @@ +use dioxus::core::{ElementId, Mutation}; +use dioxus::prelude::*; + +fn basic_syntax_is_a_template(cx: Scope) -> Element { + let asd = 123; + let var = 123; + + cx.render(rsx! { + div { key: "12345", + class: "asd", + class: "{asd}", + onclick: move |_| {}, + div { "{var}" } + div { + h1 { "var" } + p { "you're great!" } + div { background_color: "red", + h1 { "var" } + div { b { "asd" } "not great" } + } + p { "you're great!" } + } + } + }) +} +#[test] +fn dual_stream() { + let mut dom = VirtualDom::new(basic_syntax_is_a_template); + let edits = dom.rebuild().santize(); + + use Mutation::*; + assert_eq!( + edits.template_mutations, + [ + CreateElement { name: "div" }, + SetStaticAttribute { name: "class", value: "asd", ns: None }, + CreateElement { name: "div" }, + CreateTextPlaceholder, + AppendChildren { m: 1 }, + CreateElement { name: "div" }, + CreateElement { name: "h1" }, + CreateStaticText { value: "var" }, + AppendChildren { m: 1 }, + CreateElement { name: "p" }, + CreateStaticText { value: "you're great!" }, + AppendChildren { m: 1 }, + CreateElement { name: "div" }, + SetStaticAttribute { name: "background-color", value: "red", ns: Some("style") }, + CreateElement { name: "h1" }, + CreateStaticText { value: "var" }, + AppendChildren { m: 1 }, + CreateElement { name: "div" }, + CreateElement { name: "b" }, + CreateStaticText { value: "asd" }, + AppendChildren { m: 1 }, + CreateStaticText { value: "not great" }, + AppendChildren { m: 2 }, + AppendChildren { m: 2 }, + CreateElement { name: "p" }, + CreateStaticText { value: "you're great!" }, + AppendChildren { m: 1 }, + AppendChildren { m: 4 }, + AppendChildren { m: 2 }, + SaveTemplate { name: "template", m: 1 } + ], + ); + + assert_eq!( + edits.edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1) }, + SetAttribute { name: "class", value: "123", id: ElementId(1), ns: None }, + NewEventListener { event_name: "click", scope: ScopeId(0), id: ElementId(1) }, + HydrateText { path: &[0, 0], value: "123", id: ElementId(2) }, + AppendChildren { m: 1 } + ], + ); +} diff --git a/packages/dioxus/tests/lifecycle.rs b/packages/core/tests/lifecycle.rs similarity index 100% rename from packages/dioxus/tests/lifecycle.rs rename to packages/core/tests/lifecycle.rs diff --git a/packages/dioxus/tests/miri_stress.rs b/packages/core/tests/miri_stress.rs similarity index 100% rename from packages/dioxus/tests/miri_stress.rs rename to packages/core/tests/miri_stress.rs diff --git a/packages/core/tests/suspend.rs b/packages/core/tests/suspense.rs similarity index 69% rename from packages/core/tests/suspend.rs rename to packages/core/tests/suspense.rs index 559c4e4ce..c0d89a10f 100644 --- a/packages/core/tests/suspend.rs +++ b/packages/core/tests/suspense.rs @@ -2,6 +2,7 @@ use dioxus::core::ElementId; use dioxus::core::{Mutation::*, SuspenseBoundary}; use dioxus::prelude::*; use dioxus_core::SuspenseContext; +use std::future::IntoFuture; use std::{rc::Rc, time::Duration}; #[tokio::test] @@ -63,5 +64,27 @@ async fn async_child(cx: Scope<'_>) -> Element { } async fn async_text(cx: Scope<'_>) -> Element { - cx.render(rsx!("async_text")) + let username = use_future!(cx, || async { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + "async child 1" + }); + + let age = use_future!(cx, || async { + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + println!("long future completed"); + 1234 + }); + + let (_user, _age) = use_future!(cx, || async { + tokio::join!( + tokio::time::sleep(std::time::Duration::from_secs(1)), + tokio::time::sleep(std::time::Duration::from_secs(2)) + ); + ("async child 1", 1234) + }) + .await; + + let (username, age) = tokio::join!(username.into_future(), age.into_future()); + + cx.render(rsx!( div { "Hello! {username}, you are {age}, {_user} {_age}" } )) } diff --git a/packages/dioxus/tests/.rustfmt.toml b/packages/dioxus/tests/.rustfmt.toml deleted file mode 100644 index 70a9483a8..000000000 --- a/packages/dioxus/tests/.rustfmt.toml +++ /dev/null @@ -1 +0,0 @@ -struct_lit_width = 80 diff --git a/packages/dioxus/tests/rsx_syntax.rs b/packages/dioxus/tests/rsx_syntax.rs deleted file mode 100644 index 4401e1ed0..000000000 --- a/packages/dioxus/tests/rsx_syntax.rs +++ /dev/null @@ -1,83 +0,0 @@ -fn basic_syntax_is_a_template(cx: Scope) -> Element { - let asd = 123; - let var = 123; - - cx.render(rsx! { - div { key: "12345", - class: "asd", - class: "{asd}", - onclick: move |_| {}, - div { "{var}" } - div { - h1 { "var" } - p { "you're great!" } - div { background_color: "red", - h1 { "var" } - div { b { "asd" } "not great" } - } - p { "you're great!" } - } - } - }) -} - -// imports come after the test since the rsx! macro is sensitive to its location in the file -// (the byte index is used to differentiate sub templates) -use dioxus::core::{ElementId, Mutation}; -use dioxus::prelude::*; - -#[test] -fn dual_stream() { - let mut dom = VirtualDom::new(basic_syntax_is_a_template); - let edits = dom.rebuild(); - - use Mutation::*; - assert_eq!( - edits.template_mutations, - vec![ - CreateElement { name: "div", namespace: None, id: ElementId(1) }, - SetAttribute { name: "class", value: "asd", id: ElementId(1) }, - CreateElement { name: "div", namespace: None, id: ElementId(2) }, - CreatePlaceholder { id: ElementId(3) }, - AppendChildren { m: 1 }, - CreateElement { name: "div", namespace: None, id: ElementId(4) }, - CreateElement { name: "h1", namespace: None, id: ElementId(5) }, - CreateTextNode { value: "var" }, - AppendChildren { m: 1 }, - CreateElement { name: "p", namespace: None, id: ElementId(6) }, - CreateTextNode { value: "you're great!" }, - AppendChildren { m: 1 }, - CreateElement { name: "div", namespace: None, id: ElementId(7) }, - SetAttribute { name: "background-color", value: "red", id: ElementId(7) }, - CreateElement { name: "h1", namespace: None, id: ElementId(8) }, - CreateTextNode { value: "var" }, - AppendChildren { m: 1 }, - CreateElement { name: "div", namespace: None, id: ElementId(9) }, - CreateElement { name: "b", namespace: None, id: ElementId(10) }, - CreateTextNode { value: "asd" }, - AppendChildren { m: 1 }, - CreateTextNode { value: "not great" }, - AppendChildren { m: 2 }, - AppendChildren { m: 2 }, - CreateElement { name: "p", namespace: None, id: ElementId(11) }, - CreateTextNode { value: "you're great!" }, - AppendChildren { m: 1 }, - AppendChildren { m: 4 }, - AppendChildren { m: 2 }, - SaveTemplate { name: "packages/dioxus/tests/rsx_syntax.rs:5:15:122", m: 1 } - ] - ); - - assert_eq!( - edits.edits, - vec![ - LoadTemplate { name: "packages/dioxus/tests/rsx_syntax.rs:5:15:122", index: 0 }, - AssignId { path: &[], id: ElementId(12) }, - SetAttribute { name: "class", value: "123", id: ElementId(12) }, - SetAttribute { name: "onclick", value: "asd", id: ElementId(12) }, // ---- todo: listeners - HydrateText { path: &[0, 0], value: "123", id: ElementId(13) }, - ReplacePlaceholder { m: 1, path: &[0, 0] }, - AppendChildren { m: 1 } - ] - ); -} diff --git a/packages/dioxus/tests/sharedstate.rs b/packages/dioxus/tests/sharedstate.rs deleted file mode 100644 index 3b6f3559c..000000000 --- a/packages/dioxus/tests/sharedstate.rs +++ /dev/null @@ -1,126 +0,0 @@ -#![allow(unused, non_upper_case_globals, non_snake_case)] - -use dioxus::{core_macro::rsx_without_templates, prelude::*}; -use dioxus_core::{DomEdit, Mutations, SchedulerMsg, ScopeId}; -use std::rc::Rc; -use DomEdit::*; - -#[test] -fn shared_state_test() { - struct MySharedState(&'static str); - - static App: Component = |cx| { - cx.provide_context(Rc::new(MySharedState("world!"))); - cx.render(rsx_without_templates!(Child {})) - }; - - static Child: Component = |cx| { - let shared = cx.consume_context::>()?; - cx.render(rsx_without_templates!("Hello, {shared.0}")) - }; - - let mut dom = VirtualDom::new(App); - let Mutations { edits, .. } = dom.rebuild(); - - assert_eq!( - edits, - [ - CreateTextNode { root: Some(1), text: "Hello, world!" }, - AppendChildren { root: Some(0), children: vec![1] }, - ] - ); -} - -#[test] -fn swap_test() { - struct MySharedState(&'static str); - - fn app(cx: Scope) -> Element { - let val = cx.use_hook(|| 0); - *val += 1; - - cx.provide_context(Rc::new(MySharedState("world!"))); - - let child = match *val % 2 { - 0 => rsx_without_templates!( - Child1 { - Child1 { } - Child2 { } - } - ), - _ => rsx_without_templates!( - Child2 { - Child2 { } - Child2 { } - } - ), - }; - - cx.render(rsx_without_templates!( - Router { - div { child } - } - )) - } - - #[inline_props] - fn Router<'a>(cx: Scope, children: Element<'a>) -> Element<'a> { - cx.render(rsx_without_templates!(div { children })) - } - - #[inline_props] - fn Child1<'a>(cx: Scope, children: Element<'a>) -> Element { - let shared = cx.consume_context::>().unwrap(); - println!("Child1: {}", shared.0); - cx.render(rsx_without_templates! { - div { - "{shared.0}", - children - } - }) - } - - #[inline_props] - fn Child2<'a>(cx: Scope, children: Element<'a>) -> Element { - let shared = cx.consume_context::>().unwrap(); - println!("Child2: {}", shared.0); - cx.render(rsx_without_templates! { - h1 { - "{shared.0}", - children - } - }) - } - - let mut dom = VirtualDom::new(app); - let Mutations { edits, .. } = dom.rebuild(); - - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); - - // dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); - // dom.work_with_deadline(|| false); - - // dom.handle_message(SchedulerMsg::Immediate(ScopeId(2))); - // dom.work_with_deadline(|| false); - - // dom.handle_message(SchedulerMsg::Immediate(ScopeId(3))); - // dom.work_with_deadline(|| false); - - // dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - // dom.work_with_deadline(|| false); - // dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - // dom.work_with_deadline(|| false); - // dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - // dom.work_with_deadline(|| false); -} diff --git a/packages/dioxus/tests/suspense.rs b/packages/dioxus/tests/suspense.rs deleted file mode 100644 index 7e539c423..000000000 --- a/packages/dioxus/tests/suspense.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::{future::IntoFuture, rc::Rc}; - -use dioxus::prelude::*; -use dioxus_core::SuspenseBoundary; - -#[tokio::test] -async fn basic_prints() { - let mut dom = VirtualDom::new(|cx| { - cx.render(rsx! { - div { - h1 { "var" } - suspense_boundary { - basic_child { } - async_child { } - } - } - }) - }); - - dbg!(dom.rebuild()); - - dom.wait_for_work().await; - - dbg!(dom.rebuild()); -} - -#[inline_props] -fn suspense_boundary<'a>(cx: Scope<'a>, children: Element<'a>) -> Element { - cx.use_hook(|| cx.provide_context(Rc::new(SuspenseBoundary::new(cx.scope_id())))); - cx.render(rsx! { children }) -} - -fn basic_child(cx: Scope) -> Element { - cx.render(rsx!( div { "basic child 1" } )) -} - -async fn async_child(cx: Scope<'_>) -> Element { - let username = use_future!(cx, || async { - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - "async child 1" - }); - - let age = use_future!(cx, || async { - tokio::time::sleep(std::time::Duration::from_secs(2)).await; - println!("long future completed"); - 1234 - }); - - let (_user, _age) = use_future!(cx, || async { - tokio::join!( - tokio::time::sleep(std::time::Duration::from_secs(1)), - tokio::time::sleep(std::time::Duration::from_secs(2)) - ); - ("async child 1", 1234) - }) - .await; - - let (username, age) = tokio::join!(username.into_future(), age.into_future()); - - cx.render(rsx!( div { "Hello! {username}, you are {age}, {_user} {_age}" } )) -} From 565df11f7bd397cfd7482a89052a7c2c956b0178 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 27 Nov 2022 02:06:04 -0500 Subject: [PATCH 07/18] chore: more tests passing --- packages/core/src/diff.rs | 43 +- packages/core/src/mutations.rs | 4 + packages/core/src/virtual_dom.rs | 2 + packages/core/tests/create_element.rs | 29 + packages/core/tests/diff_keyed_list.rs | 873 +++++------------------ packages/core/tests/diff_unkeyed_list.rs | 107 +++ packages/core/tests/lifecycle.rs | 4 +- packages/core/tests/miri_stress.rs | 9 +- 8 files changed, 364 insertions(+), 707 deletions(-) create mode 100644 packages/core/tests/create_element.rs diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index bb6a66efc..75141e002 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -605,7 +605,7 @@ impl<'b: 'static> VirtualDom { // only valid of the if there are no trailing elements // self.create_and_append_children(new); - todo!() + todo!("we should never be appending - just creating N"); } return; } @@ -615,8 +615,7 @@ impl<'b: 'static> VirtualDom { for child in old { let key = child.key.unwrap(); if !shared_keys.contains(&key) { - todo!("remove node"); - // self.remove_nodes( [child]); + self.remove_node(child); } } @@ -719,13 +718,6 @@ impl<'b: 'static> VirtualDom { } } - 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!() } @@ -775,7 +767,36 @@ impl<'b: 'static> VirtualDom { /// Push all the real nodes on the stack fn push_all_real_nodes(&mut self, node: &VNode) -> usize { - todo!() + let mut onstack = 0; + + for (idx, _) in node.template.roots.iter().enumerate() { + match node.dynamic_root(idx) { + Some(Text(t)) => { + self.mutations.push(Mutation::PushRoot { id: t.id.get() }); + onstack += 1; + } + Some(Fragment(VFragment::Empty(t))) => { + self.mutations.push(Mutation::PushRoot { id: t.get() }); + onstack += 1; + } + Some(Fragment(VFragment::NonEmpty(t))) => { + for node in *t { + onstack += self.push_all_real_nodes(node); + } + } + Some(Component(comp)) => { + todo!() + } + None => { + self.mutations.push(Mutation::PushRoot { + id: node.root_ids[idx].get(), + }); + onstack += 1; + } + }; + } + + onstack } fn create_children(&mut self, nodes: &'b [VNode<'b>]) -> usize { diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index fa404fafe..7e8e10f88 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -189,4 +189,8 @@ pub enum Mutation<'a> { Remove { id: ElementId, }, + + PushRoot { + id: ElementId, + }, } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index cbb1f7250..d77e1e37c 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -483,6 +483,7 @@ impl VirtualDom { /// /// apply_edits(edits); /// ``` + #[must_use] pub fn rebuild<'a>(&'a mut self) -> Mutations<'a> { match unsafe { self.run_scope(ScopeId(0)).extend_lifetime_ref() } { // Rebuilding implies we append the created elements to the root @@ -500,6 +501,7 @@ impl VirtualDom { /// Render whatever the VirtualDom has ready as fast as possible without requiring an executor to progress /// suspended subtrees. + #[must_use] pub fn render_immediate(&mut self) -> Mutations { // Build a waker that won't wake up since our deadline is already expired when it's polled let waker = futures_util::task::noop_waker(); diff --git a/packages/core/tests/create_element.rs b/packages/core/tests/create_element.rs new file mode 100644 index 000000000..ff9d485b6 --- /dev/null +++ b/packages/core/tests/create_element.rs @@ -0,0 +1,29 @@ +use dioxus::core::Mutation::*; +use dioxus::prelude::*; + +#[test] +fn multiroot() { + let mut dom = VirtualDom::new(|cx| { + cx.render(rsx! { + div { "Hello a" } + div { "Hello b" } + div { "Hello c" } + }) + }); + + assert_eq!( + dom.rebuild().santize().template_mutations, + [ + CreateElement { name: "div" }, + CreateStaticText { value: "Hello a" }, + AppendChildren { m: 1 }, + CreateElement { name: "div" }, + CreateStaticText { value: "Hello b" }, + AppendChildren { m: 1 }, + CreateElement { name: "div" }, + CreateStaticText { value: "Hello c" }, + AppendChildren { m: 1 }, + SaveTemplate { name: "template", m: 3 } + ] + ) +} diff --git a/packages/core/tests/diff_keyed_list.rs b/packages/core/tests/diff_keyed_list.rs index 0f807151f..521d9385a 100644 --- a/packages/core/tests/diff_keyed_list.rs +++ b/packages/core/tests/diff_keyed_list.rs @@ -6,661 +6,298 @@ //! //! It does not validated that component lifecycles work properly. This is done in another test file. -use dioxus::{core_macro::rsx_without_templates, prelude::*}; - -fn new_dom() -> VirtualDom { - VirtualDom::new(|cx| cx.render(rsx_without_templates!("hi"))) -} - -use dioxus_core::DomEdit::*; - -/// Should push the text node onto the stack and modify it -#[test] -fn html_and_rsx_generate_the_same_output() { - let dom = new_dom(); - let (create, change) = dom.diff_lazynodes( - rsx_without_templates! ( div { "Hello world" } ), - rsx_without_templates! ( div { "Goodbye world" } ), - ); - assert_eq!( - create.edits, - [ - CreateElement { root: Some(1,), tag: "div", children: 0 }, - CreateTextNode { root: Some(2,), text: "Hello world" }, - AppendChildren { root: Some(1,), children: vec![2] }, - AppendChildren { root: Some(0,), children: vec![1] }, - ] - ); - - assert_eq!( - change.edits, - [SetText { root: Some(2,), text: "Goodbye world" },] - ); -} - -/// Should result in 3 elements on the stack -#[test] -fn fragments_create_properly() { - let dom = new_dom(); - - let create = dom.create_vnodes(rsx_without_templates! { - div { "Hello a" } - div { "Hello b" } - div { "Hello c" } - }); - - assert_eq!( - create.edits, - [ - CreateElement { root: Some(1,), tag: "div", children: 0 }, - CreateTextNode { root: Some(2,), text: "Hello a" }, - AppendChildren { root: Some(1,), children: vec![2,] }, - CreateElement { root: Some(3,), tag: "div", children: 0 }, - CreateTextNode { root: Some(4,), text: "Hello b" }, - AppendChildren { root: Some(3,), children: vec![4,] }, - CreateElement { root: Some(5,), tag: "div", children: 0 }, - CreateTextNode { root: Some(6,), text: "Hello c" }, - AppendChildren { root: Some(5,), children: vec![6,] }, - AppendChildren { root: Some(0,), children: vec![1, 3, 5,] }, - ] - ); -} - -/// Should result in the creation of an anchor (placeholder) and then a replacewith -#[test] -fn empty_fragments_create_anchors() { - let dom = new_dom(); - - let left = rsx_without_templates!({ (0..0).map(|_f| rsx_without_templates! { div {}}) }); - let right = rsx_without_templates!({ (0..1).map(|_f| rsx_without_templates! { div {}}) }); - - let (create, change) = dom.diff_lazynodes(left, right); - - assert_eq!( - create.edits, - [ - CreatePlaceholder { root: Some(1,) }, - AppendChildren { root: Some(0,), children: vec![1,] }, - ] - ); - assert_eq!( - change.edits, - [ - CreateElement { root: Some(2,), tag: "div", children: 0 }, - ReplaceWith { root: Some(1,), nodes: vec![2,] }, - ] - ); -} - -/// Should result in the creation of an anchor (placeholder) and then a replacewith m=5 -#[test] -fn empty_fragments_create_many_anchors() { - let dom = new_dom(); - - let left = rsx_without_templates!({ (0..0).map(|_f| rsx_without_templates! { div {}}) }); - let right = rsx_without_templates!({ (0..5).map(|_f| rsx_without_templates! { div {}}) }); - - let (create, change) = dom.diff_lazynodes(left, right); - - assert_eq!( - create.edits, - [ - CreatePlaceholder { root: Some(1,) }, - AppendChildren { root: Some(0,), children: vec![1,] }, - ] - ); - - assert_eq!( - change.edits, - [ - CreateElement { root: Some(2,), tag: "div", children: 0 }, - CreateElement { root: Some(3,), tag: "div", children: 0 }, - CreateElement { root: Some(4,), tag: "div", children: 0 }, - CreateElement { root: Some(5,), tag: "div", children: 0 }, - CreateElement { root: Some(6,), tag: "div", children: 0 }, - ReplaceWith { root: Some(1,), nodes: vec![2, 3, 4, 5, 6,] }, - ] - ); -} - -/// Should result in the creation of an anchor (placeholder) and then a replacewith -/// Includes child nodes inside the fragment -#[test] -fn empty_fragments_create_anchors_with_many_children() { - let dom = new_dom(); - - let left = rsx_without_templates!({ (0..0).map(|_| rsx_without_templates! { div {} }) }); - let right = rsx_without_templates!({ - (0..3).map(|f| { - rsx_without_templates! { div { "hello: {f}" }} - }) - }); - - let (create, change) = dom.diff_lazynodes(left, right); - - assert_eq!( - create.edits, - [ - CreatePlaceholder { root: Some(1,) }, - AppendChildren { root: Some(0,), children: vec![1,] }, - ] - ); - - assert_eq!( - change.edits, - [ - CreateElement { root: Some(2,), tag: "div", children: 0 }, - CreateTextNode { root: Some(3,), text: "hello: 0" }, - AppendChildren { root: Some(2,), children: vec![3,] }, - CreateElement { root: Some(4,), tag: "div", children: 0 }, - CreateTextNode { root: Some(5,), text: "hello: 1" }, - AppendChildren { root: Some(4,), children: vec![5,] }, - CreateElement { root: Some(6,), tag: "div", children: 0 }, - CreateTextNode { root: Some(7,), text: "hello: 2" }, - AppendChildren { root: Some(6,), children: vec![7,] }, - ReplaceWith { root: Some(1,), nodes: vec![2, 4, 6,] }, - ] - ); -} - -/// Should result in every node being pushed and then replaced with an anchor -#[test] -fn many_items_become_fragment() { - let dom = new_dom(); - - let left = rsx_without_templates!({ - (0..2).map(|_| { - rsx_without_templates! { div { "hello" }} - }) - }); - let right = rsx_without_templates!({ (0..0).map(|_| rsx_without_templates! { div {} }) }); - - let (create, change) = dom.diff_lazynodes(left, right); - - assert_eq!( - create.edits, - [ - CreateElement { root: Some(1,), tag: "div", children: 0 }, - CreateTextNode { root: Some(2,), text: "hello" }, - AppendChildren { root: Some(1,), children: vec![2,] }, - CreateElement { root: Some(3,), tag: "div", children: 0 }, - CreateTextNode { root: Some(4,), text: "hello" }, - AppendChildren { root: Some(3,), children: vec![4,] }, - AppendChildren { root: Some(0,), children: vec![1, 3,] }, - ] - ); - - assert_eq!( - change.edits, - [ - CreatePlaceholder { root: Some(5,) }, - ReplaceWith { root: Some(1,), nodes: vec![5,] }, - Remove { root: Some(3,) }, - ] - ); -} - -/// Should result in no edits -#[test] -fn two_equal_fragments_are_equal() { - let dom = new_dom(); - - let left = rsx_without_templates!({ - (0..2).map(|_| { - rsx_without_templates! { div { "hello" }} - }) - }); - let right = rsx_without_templates!({ - (0..2).map(|_| { - rsx_without_templates! { div { "hello" }} - }) - }); - - let (_create, change) = dom.diff_lazynodes(left, right); - assert!(change.edits.is_empty()); -} - -/// Should result the creation of more nodes appended after the old last node -#[test] -fn two_fragments_with_differrent_elements_are_differet() { - let dom = new_dom(); - - let left = rsx_without_templates!( - { (0..2).map(|_| rsx_without_templates! { div { }} ) } - p {} - ); - let right = rsx_without_templates!( - { (0..5).map(|_| rsx_without_templates! (h1 { }) ) } - p {} - ); - - let (_create, changes) = dom.diff_lazynodes(left, right); - assert_eq!( - changes.edits, - [ - CreateElement { root: Some(4,), tag: "h1", children: 0 }, - CreateElement { root: Some(5,), tag: "h1", children: 0 }, - CreateElement { root: Some(6,), tag: "h1", children: 0 }, - InsertAfter { root: Some(2,), nodes: vec![4, 5, 6,] }, - CreateElement { root: Some(7,), tag: "h1", children: 0 }, - ReplaceWith { root: Some(1,), nodes: vec![7,] }, // notice how 1 gets re-used - CreateElement { root: Some(1,), tag: "h1", children: 0 }, - ReplaceWith { root: Some(2,), nodes: vec![1,] }, - ] - ); -} - -/// Should result in multiple nodes destroyed - with changes to the first nodes -#[test] -fn two_fragments_with_differrent_elements_are_differet_shorter() { - let dom = new_dom(); - - let left = rsx_without_templates!( - {(0..5).map(|f| {rsx_without_templates! { div { }}})} - p {} - ); - let right = rsx_without_templates!( - {(0..2).map(|f| {rsx_without_templates! { h1 { }}})} - p {} - ); - - let (create, change) = dom.diff_lazynodes(left, right); - - assert_eq!( - create.edits, - [ - CreateElement { root: Some(1,), tag: "div", children: 0 }, - CreateElement { root: Some(2,), tag: "div", children: 0 }, - CreateElement { root: Some(3,), tag: "div", children: 0 }, - CreateElement { root: Some(4,), tag: "div", children: 0 }, - CreateElement { root: Some(5,), tag: "div", children: 0 }, - CreateElement { root: Some(6,), tag: "p", children: 0 }, - AppendChildren { root: Some(0,), children: vec![1, 2, 3, 4, 5, 6,] }, - ] - ); - - // note: key reuse is always the last node that got used - // slab maintains a linked list, essentially - assert_eq!( - change.edits, - [ - Remove { root: Some(3,) }, - Remove { root: Some(4,) }, - Remove { root: Some(5,) }, - CreateElement { root: Some(5,), tag: "h1", children: 0 }, // 5 gets reused - ReplaceWith { root: Some(1,), nodes: vec![5,] }, // 1 gets deleted - CreateElement { root: Some(1,), tag: "h1", children: 0 }, // 1 gets reused - ReplaceWith { root: Some(2,), nodes: vec![1,] }, - ] - ); -} - -/// Should result in multiple nodes destroyed - with no changes -#[test] -fn two_fragments_with_same_elements_are_differet() { - let dom = new_dom(); - - let left = rsx_without_templates!( - {(0..2).map(|f| rsx_without_templates! { div { }})} - p {} - ); - let right = rsx_without_templates!( - {(0..5).map(|f| rsx_without_templates! { div { }})} - p {} - ); - - let (create, change) = dom.diff_lazynodes(left, right); - - assert_eq!( - create.edits, - [ - CreateElement { root: Some(1,), tag: "div", children: 0 }, - CreateElement { root: Some(2,), tag: "div", children: 0 }, - CreateElement { root: Some(3,), tag: "p", children: 0 }, - AppendChildren { root: Some(0,), children: vec![1, 2, 3,] }, - ] - ); - assert_eq!( - change.edits, - [ - CreateElement { root: Some(4,), tag: "div", children: 0 }, - CreateElement { root: Some(5,), tag: "div", children: 0 }, - CreateElement { root: Some(6,), tag: "div", children: 0 }, - InsertAfter { root: Some(2,), nodes: vec![4, 5, 6,] }, - ] - ); -} - -/// should result in the removal of elements -#[test] -fn keyed_diffing_order() { - let dom = new_dom(); - - let left = rsx_without_templates!( - {(0..5).map(|f| {rsx_without_templates! { div { key: "{f}" }}})} - p {"e"} - ); - let right = rsx_without_templates!( - {(0..2).map(|f| rsx_without_templates! { div { key: "{f}" }})} - p {"e"} - ); - - let (create, change) = dom.diff_lazynodes(left, right); - - assert_eq!( - change.edits, - [ - Remove { root: Some(3,) }, - Remove { root: Some(4,) }, - Remove { root: Some(5,) }, - ] - ); -} +use dioxus::core::{ElementId, Mutation::*}; +use dioxus::prelude::*; /// Should result in moves, but not removals or additions #[test] fn keyed_diffing_out_of_order() { - let dom = new_dom(); + let mut dom = VirtualDom::new(|cx| { + let order = match cx.generation() % 2 { + 0 => &[0, 1, 2, 3, /**/ 4, 5, 6, /**/ 7, 8, 9], + 1 => &[0, 1, 2, 3, /**/ 6, 4, 5, /**/ 7, 8, 9], + _ => unreachable!(), + }; - let left = rsx_without_templates!({ - [0, 1, 2, 3, /**/ 4, 5, 6, /**/ 7, 8, 9].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) + cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) }); - let right = rsx_without_templates!({ - [0, 1, 2, 3, /**/ 6, 4, 5, /**/ 7, 8, 9].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - let (_, changes) = dom.diff_lazynodes(left, right); - assert_eq!( - changes.edits, - [InsertBefore { root: Some(5,), nodes: vec![7,] },] + dom.rebuild().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(3,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(4,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(5,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(6,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(7,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(8,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(9,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(10,) }, + AppendChildren { m: 10 }, + ] ); + + dom.mark_dirty_scope(ScopeId(0)); + assert_eq!( + dom.render_immediate().edits, + [ + PushRoot { id: ElementId(7,) }, + InsertBefore { id: ElementId(5,), m: 1 }, + ] + ) } /// Should result in moves only #[test] fn keyed_diffing_out_of_order_adds() { - let dom = new_dom(); + let mut dom = VirtualDom::new(|cx| { + let order = match cx.generation() % 2 { + 0 => &[/**/ 4, 5, 6, 7, 8 /**/], + 1 => &[/**/ 8, 7, 4, 5, 6 /**/], + _ => unreachable!(), + }; - let left = rsx_without_templates!({ - [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) + cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) }); - let right = rsx_without_templates!({ - [/**/ 8, 7, 4, 5, 6 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - let (_, change) = dom.diff_lazynodes(left, right); + _ = dom.rebuild(); + dom.mark_dirty_scope(ScopeId(0)); assert_eq!( - change.edits, - [InsertBefore { root: Some(1,), nodes: vec![5, 4,] },] - ); + dom.render_immediate().edits, + [ + PushRoot { id: ElementId(5,) }, + PushRoot { id: ElementId(4,) }, + InsertBefore { id: ElementId(1,), m: 2 }, + ] + ) } + /// Should result in moves only #[test] -fn keyed_diffing_out_of_order_adds_2() { - let dom = new_dom(); - - let left = rsx_without_templates!({ - [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - let right = rsx_without_templates!({ - [/**/ 7, 8, 4, 5, 6 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - let (_, change) = dom.diff_lazynodes(left, right); - - assert_eq!( - change.edits, - [InsertBefore { root: Some(1,), nodes: vec![4, 5,] },] - ); -} - -/// Should result in moves onl -#[test] fn keyed_diffing_out_of_order_adds_3() { - let dom = new_dom(); + let mut dom = VirtualDom::new(|cx| { + let order = match cx.generation() % 2 { + 0 => &[/**/ 4, 5, 6, 7, 8 /**/], + 1 => &[/**/ 4, 8, 7, 5, 6 /**/], + _ => unreachable!(), + }; - let left = rsx_without_templates!({ - [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) + cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) }); - let right = rsx_without_templates!({ - [/**/ 4, 8, 7, 5, 6 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - let (_, change) = dom.diff_lazynodes(left, right); + _ = dom.rebuild(); + dom.mark_dirty_scope(ScopeId(0)); assert_eq!( - change.edits, - [InsertBefore { root: Some(2,), nodes: vec![5, 4,] },] + dom.render_immediate().edits, + [ + PushRoot { id: ElementId(5,) }, + PushRoot { id: ElementId(4,) }, + InsertBefore { id: ElementId(2,), m: 2 }, + ] ); } /// Should result in moves onl #[test] fn keyed_diffing_out_of_order_adds_4() { - let dom = new_dom(); + let mut dom = VirtualDom::new(|cx| { + let order = match cx.generation() % 2 { + 0 => &[/**/ 4, 5, 6, 7, 8 /**/], + 1 => &[/**/ 4, 5, 8, 7, 6 /**/], + _ => unreachable!(), + }; - let left = rsx_without_templates!({ - [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) + cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) }); - let right = rsx_without_templates!({ - [/**/ 4, 5, 8, 7, 6 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - let (_, change) = dom.diff_lazynodes(left, right); + _ = dom.rebuild(); + dom.mark_dirty_scope(ScopeId(0)); assert_eq!( - change.edits, - [InsertBefore { root: Some(3), nodes: vec![5, 4,] },] + dom.render_immediate().edits, + [ + PushRoot { id: ElementId(5,) }, + PushRoot { id: ElementId(4,) }, + InsertBefore { id: ElementId(3,), m: 2 }, + ] ); } /// Should result in moves onl #[test] fn keyed_diffing_out_of_order_adds_5() { - let dom = new_dom(); + let mut dom = VirtualDom::new(|cx| { + let order = match cx.generation() % 2 { + 0 => &[/**/ 4, 5, 6, 7, 8 /**/], + 1 => &[/**/ 4, 5, 6, 8, 7 /**/], + _ => unreachable!(), + }; - let left = rsx_without_templates!({ - [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) + cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) }); - let right = rsx_without_templates!({ - [/**/ 4, 5, 6, 8, 7 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); + _ = dom.rebuild(); - let (_, change) = dom.diff_lazynodes(left, right); + dom.mark_dirty_scope(ScopeId(0)); assert_eq!( - change.edits, - [InsertBefore { root: Some(4), nodes: vec![5] }] + dom.render_immediate().edits, + [ + PushRoot { id: ElementId(5,) }, + InsertBefore { id: ElementId(4,), m: 1 }, + ] ); } +/// Should result in moves onl #[test] fn keyed_diffing_additions() { - let dom = new_dom(); + let mut dom = VirtualDom::new(|cx| { + let order: &[_] = match cx.generation() % 2 { + 0 => &[/**/ 4, 5, 6, 7, 8 /**/], + 1 => &[/**/ 4, 5, 6, 7, 8, 9, 10 /**/], + _ => unreachable!(), + }; - let left = rsx_without_templates!({ - [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) + cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) }); - let right = rsx_without_templates!({ - [/**/ 4, 5, 6, 7, 8, 9, 10 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - let (_, change) = dom.diff_lazynodes(left, right); + _ = dom.rebuild(); + dom.mark_dirty_scope(ScopeId(0)); assert_eq!( - change.edits, + dom.render_immediate().santize().edits, [ - CreateElement { root: Some(6,), tag: "div", children: 0 }, - CreateElement { root: Some(7,), tag: "div", children: 0 }, - InsertAfter { root: Some(5,), nodes: vec![6, 7,] }, + LoadTemplate { name: "template", index: 0, id: ElementId(6) }, + LoadTemplate { name: "template", index: 0, id: ElementId(7) }, + InsertAfter { id: ElementId(5), m: 2 } ] ); } #[test] fn keyed_diffing_additions_and_moves_on_ends() { - let dom = new_dom(); + let mut dom = VirtualDom::new(|cx| { + let order: &[_] = match cx.generation() % 2 { + 0 => &[/**/ 4, 5, 6, 7 /**/], + 1 => &[/**/ 7, 4, 5, 6, 11, 12 /**/], + _ => unreachable!(), + }; - let left = rsx_without_templates!({ - [/**/ 4, 5, 6, 7 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) + cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) }); - let right = rsx_without_templates!({ - [/**/ 7, 4, 5, 6, 11, 12 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); + _ = dom.rebuild(); - let (_, change) = dom.diff_lazynodes(left, right); - println!("{:?}", change); + dom.mark_dirty_scope(ScopeId(0)); assert_eq!( - change.edits, + dom.render_immediate().santize().edits, [ // create 11, 12 - CreateElement { root: Some(5), tag: "div", children: 0 }, - CreateElement { root: Some(6), tag: "div", children: 0 }, - InsertAfter { root: Some(3), nodes: vec![5, 6] }, - // // move 7 to the front - InsertBefore { root: Some(1), nodes: vec![4] } + LoadTemplate { name: "template", index: 0, id: ElementId(5) }, + LoadTemplate { name: "template", index: 0, id: ElementId(6) }, + InsertAfter { id: ElementId(3), m: 2 }, + // move 7 to the front + PushRoot { id: ElementId(4) }, + InsertBefore { id: ElementId(1), m: 1 } ] ); } #[test] fn keyed_diffing_additions_and_moves_in_middle() { - let dom = new_dom(); + let mut dom = VirtualDom::new(|cx| { + let order: &[_] = match cx.generation() % 2 { + 0 => &[/**/ 1, 2, 3, 4 /**/], + 1 => &[/**/ 4, 1, 7, 8, 2, 5, 6, 3 /**/], + _ => unreachable!(), + }; - let left = rsx_without_templates!({ - [/**/ 1, 2, 3, 4 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) + cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) }); - let right = rsx_without_templates!({ - [/**/ 4, 1, 7, 8, 2, 5, 6, 3 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); + _ = dom.rebuild(); // LIS: 4, 5, 6 - let (_, change) = dom.diff_lazynodes(left, right); - println!("{:#?}", change); + dom.mark_dirty_scope(ScopeId(0)); assert_eq!( - change.edits, + dom.render_immediate().santize().edits, [ // create 5, 6 - CreateElement { root: Some(5,), tag: "div", children: 0 }, - CreateElement { root: Some(6,), tag: "div", children: 0 }, - InsertBefore { root: Some(3,), nodes: vec![5, 6,] }, + LoadTemplate { name: "template", index: 0, id: ElementId(5) }, + LoadTemplate { name: "template", index: 0, id: ElementId(6) }, + InsertBefore { id: ElementId(3), m: 2 }, // create 7, 8 - CreateElement { root: Some(7,), tag: "div", children: 0 }, - CreateElement { root: Some(8,), tag: "div", children: 0 }, - InsertBefore { root: Some(2,), nodes: vec![7, 8,] }, + LoadTemplate { name: "template", index: 0, id: ElementId(7) }, + LoadTemplate { name: "template", index: 0, id: ElementId(8) }, + InsertBefore { id: ElementId(2), m: 2 }, // move 7 - InsertBefore { root: Some(1,), nodes: vec![4,] }, + PushRoot { id: ElementId(4) }, + InsertBefore { id: ElementId(1), m: 1 } ] ); } #[test] fn controlled_keyed_diffing_out_of_order() { - let dom = new_dom(); + let mut dom = VirtualDom::new(|cx| { + let order: &[_] = match cx.generation() % 2 { + 0 => &[4, 5, 6, 7], + 1 => &[0, 5, 9, 6, 4], + _ => unreachable!(), + }; - let left = rsx_without_templates!({ - [4, 5, 6, 7].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) + cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) }); - let right = rsx_without_templates!({ - [0, 5, 9, 6, 4].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); + _ = dom.rebuild(); // LIS: 5, 6 - let (_, changes) = dom.diff_lazynodes(left, right); - println!("{:#?}", &changes); + dom.mark_dirty_scope(ScopeId(0)); assert_eq!( - changes.edits, + dom.render_immediate().santize().edits, [ // remove 7 - Remove { root: Some(4,) }, + Remove { id: ElementId(4,) }, // move 4 to after 6 - InsertAfter { root: Some(3,), nodes: vec![1,] }, + PushRoot { id: ElementId(1) }, + InsertAfter { id: ElementId(3,), m: 1 }, // create 9 and insert before 6 - CreateElement { root: Some(4,), tag: "div", children: 0 }, - InsertBefore { root: Some(3,), nodes: vec![4,] }, + LoadTemplate { name: "template", index: 0, id: ElementId(4) }, + InsertBefore { id: ElementId(3,), m: 1 }, // create 0 and insert before 5 - CreateElement { root: Some(5,), tag: "div", children: 0 }, - InsertBefore { root: Some(2,), nodes: vec![5,] }, + LoadTemplate { name: "template", index: 0, id: ElementId(5) }, + InsertBefore { id: ElementId(2,), m: 1 }, ] ); } #[test] fn controlled_keyed_diffing_out_of_order_max_test() { - let dom = new_dom(); + let mut dom = VirtualDom::new(|cx| { + let order: &[_] = match cx.generation() % 2 { + 0 => &[0, 1, 2, 3, 4], + 1 => &[3, 0, 1, 10, 2], + _ => unreachable!(), + }; - let left = rsx_without_templates!({ - [0, 1, 2, 3, 4].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) + cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) }); - let right = rsx_without_templates!({ - [3, 0, 1, 10, 2].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); + _ = dom.rebuild(); - let (_, changes) = dom.diff_lazynodes(left, right); - println!("{:#?}", &changes); + dom.mark_dirty_scope(ScopeId(0)); assert_eq!( - changes.edits, + dom.render_immediate().santize().edits, [ - Remove { root: Some(5,) }, - CreateElement { root: Some(5,), tag: "div", children: 0 }, - InsertBefore { root: Some(3,), nodes: vec![5,] }, - InsertBefore { root: Some(1,), nodes: vec![4,] }, + Remove { id: ElementId(5,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(5) }, + InsertBefore { id: ElementId(3,), m: 1 }, + PushRoot { id: ElementId(4) }, + InsertBefore { id: ElementId(1,), m: 1 }, ] ); } @@ -669,163 +306,25 @@ fn controlled_keyed_diffing_out_of_order_max_test() { // just making sure it doesnt happen in the core implementation #[test] fn remove_list() { - let dom = new_dom(); + let mut dom = VirtualDom::new(|cx| { + let order: &[_] = match cx.generation() % 2 { + 0 => &[9, 8, 7, 6, 5], + 1 => &[9, 8], + _ => unreachable!(), + }; - let left = rsx_without_templates!({ - (0..10).rev().take(5).map(|i| { - rsx_without_templates! { Fragment { key: "{i}", "{i}" }} - }) + cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) }); - let right = rsx_without_templates!({ - (0..10).rev().take(2).map(|i| { - rsx_without_templates! { Fragment { key: "{i}", "{i}" }} - }) - }); - - let (create, changes) = dom.diff_lazynodes(left, right); - - // dbg!(create); - // dbg!(changes); + _ = dom.rebuild(); + dom.mark_dirty_scope(ScopeId(0)); assert_eq!( - changes.edits, - // remove 5, 4, 3 + dom.render_immediate().santize().edits, [ - Remove { root: Some(3) }, - Remove { root: Some(4) }, - Remove { root: Some(5) } - ] - ); -} - -// noticed some weird behavior in the desktop interpreter -// just making sure it doesnt happen in the core implementation -#[test] -fn remove_list_nokeyed() { - let dom = new_dom(); - - let left = rsx_without_templates!({ - (0..10).rev().take(5).map(|i| { - rsx_without_templates! { Fragment { "{i}" }} - }) - }); - - let right = rsx_without_templates!({ - (0..10).rev().take(2).map(|i| { - rsx_without_templates! { Fragment { "{i}" }} - }) - }); - - let (create, changes) = dom.diff_lazynodes(left, right); - - assert_eq!( - changes.edits, - [ - // remove 5, 4, 3 - Remove { root: Some(3) }, - Remove { root: Some(4) }, - Remove { root: Some(5) }, - ] - ); -} - -#[test] -fn add_nested_elements() { - let vdom = new_dom(); - - let (_create, change) = vdom.diff_lazynodes( - rsx_without_templates! { - div{} - }, - rsx_without_templates! { - div{ - div{} - } - }, - ); - - assert_eq!( - change.edits, - [ - CreateElement { root: Some(2), tag: "div", children: 0 }, - AppendChildren { root: Some(1), children: vec![2] }, - ] - ); -} - -#[test] -fn add_listeners() { - let vdom = new_dom(); - - let (_create, change) = vdom.diff_lazynodes( - rsx_without_templates! { - div{} - }, - rsx_without_templates! { - div{ - onkeyup: |_| {}, - onkeydown: |_| {}, - } - }, - ); - - assert_eq!( - change.edits, - [ - NewEventListener { event_name: "keyup", scope: ScopeId(0), root: Some(1) }, - NewEventListener { event_name: "keydown", scope: ScopeId(0), root: Some(1) }, - ] - ); -} - -#[test] -fn remove_listeners() { - let vdom = new_dom(); - - let (_create, change) = vdom.diff_lazynodes( - rsx_without_templates! { - div{ - onkeyup: |_| {}, - onkeydown: |_| {}, - } - }, - rsx_without_templates! { - div{} - }, - ); - - assert_eq!( - change.edits, - [ - RemoveEventListener { event: "keyup", root: Some(1) }, - RemoveEventListener { event: "keydown", root: Some(1) }, - ] - ); -} - -#[test] -fn diff_listeners() { - let vdom = new_dom(); - - let (_create, change) = vdom.diff_lazynodes( - rsx_without_templates! { - div{ - onkeydown: |_| {}, - } - }, - rsx_without_templates! { - div{ - onkeyup: |_| {}, - } - }, - ); - - assert_eq!( - change.edits, - [ - RemoveEventListener { root: Some(1), event: "keydown" }, - NewEventListener { event_name: "keyup", scope: ScopeId(0), root: Some(1) } + Remove { id: ElementId(3) }, + Remove { id: ElementId(4) }, + Remove { id: ElementId(5) } ] ); } diff --git a/packages/core/tests/diff_unkeyed_list.rs b/packages/core/tests/diff_unkeyed_list.rs index 6d8f0a236..96de44cc3 100644 --- a/packages/core/tests/diff_unkeyed_list.rs +++ b/packages/core/tests/diff_unkeyed_list.rs @@ -270,3 +270,110 @@ fn removes_one_by_one_multiroot() { ] ); } + +#[test] +fn two_equal_fragments_are_equal_static() { + let mut dom = VirtualDom::new(|cx| { + cx.render(rsx! { + (0..5).map(|_| rsx! { + div { "hello" } + }) + }) + }); + + _ = dom.rebuild(); + assert!(dom.render_immediate().edits.is_empty()); +} + +#[test] +fn two_equal_fragments_are_equal() { + let mut dom = VirtualDom::new(|cx| { + cx.render(rsx! { + (0..5).map(|i| rsx! { + div { "hello {i}" } + }) + }) + }); + + _ = dom.rebuild(); + assert!(dom.render_immediate().edits.is_empty()); +} + +#[test] +fn remove_many() { + let mut dom = VirtualDom::new(|cx| { + let num = match cx.generation() % 3 { + 0 => 0, + 1 => 1, + 2 => 5, + _ => unreachable!(), + }; + + cx.render(rsx! { + (0..num).map(|i| rsx! { div { "hello {i}" } }) + }) + }); + + let edits = dom.rebuild().santize(); + assert!(edits.template_mutations.is_empty()); + assert_eq!( + edits.edits, + [ + CreatePlaceholder { id: ElementId(1,) }, + AppendChildren { m: 1 }, + ] + ); + + dom.mark_dirty_scope(ScopeId(0)); + let edits = dom.render_immediate().santize(); + assert_eq!( + edits.edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, + HydrateText { path: &[0,], value: "hello 0", id: ElementId(3,) }, + ReplaceWith { id: ElementId(1,), m: 1 }, + ] + ); + + dom.mark_dirty_scope(ScopeId(0)); + let edits = dom.render_immediate().santize(); + assert_eq!( + edits.edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, + HydrateText { path: &[0,], value: "hello 1", id: ElementId(4,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(5,) }, + HydrateText { path: &[0,], value: "hello 2", id: ElementId(6,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(7,) }, + HydrateText { path: &[0,], value: "hello 3", id: ElementId(8,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(9,) }, + HydrateText { path: &[0,], value: "hello 4", id: ElementId(10,) }, + InsertAfter { id: ElementId(2,), m: 4 }, + ] + ); + + dom.mark_dirty_scope(ScopeId(0)); + let edits = dom.render_immediate().santize(); + assert_eq!( + edits.edits, + [ + Remove { id: ElementId(1,) }, + Remove { id: ElementId(5,) }, + Remove { id: ElementId(7,) }, + Remove { id: ElementId(9,) }, + CreatePlaceholder { id: ElementId(9,) }, + ReplaceWith { id: ElementId(2,), m: 1 }, + ] + ); + + dom.mark_dirty_scope(ScopeId(0)); + let edits = dom.render_immediate().santize(); + assert_eq!( + edits.edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, + HydrateText { path: &[0,], value: "hello 0", id: ElementId(10,) }, + ReplaceWith { id: ElementId(9,), m: 1 }, + ] + ) +} diff --git a/packages/core/tests/lifecycle.rs b/packages/core/tests/lifecycle.rs index 17d198597..0c9850345 100644 --- a/packages/core/tests/lifecycle.rs +++ b/packages/core/tests/lifecycle.rs @@ -2,8 +2,8 @@ #![allow(non_snake_case)] //! Tests for the lifecycle of components. -use dioxus::{core_macro::rsx_without_templates, prelude::*}; -use dioxus_core::DomEdit::*; +use dioxus::core::{ElementId, Mutation::*}; +use dioxus::prelude::*; use std::sync::{Arc, Mutex}; type Shared = Arc>; diff --git a/packages/core/tests/miri_stress.rs b/packages/core/tests/miri_stress.rs index 11f7d06ad..20a29b30c 100644 --- a/packages/core/tests/miri_stress.rs +++ b/packages/core/tests/miri_stress.rs @@ -11,11 +11,6 @@ Specifically: */ use dioxus::prelude::*; -use dioxus_core::SchedulerMsg; - -fn new_dom(app: Component

, props: P) -> VirtualDom { - VirtualDom::new_with_props(app, props) -} /// This test ensures that if a component aborts early, it is replaced with a placeholder. /// In debug, this should also toss a warning. @@ -28,7 +23,7 @@ fn test_memory_leak() { *val += 1; if *val == 2 || *val == 4 { - return None; + return cx.render(rsx!(())); } let name = cx.use_hook(|| String::from("asd")); @@ -66,7 +61,7 @@ fn test_memory_leak() { render!(div { "goodbye world" }) } - let mut dom = new_dom(app, ()); + let mut dom = VirtualDom::new(app); dom.rebuild(); dom.hard_diff(ScopeId(0)); From 0027cdd9388409b29ddb20717646537addb88b1e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 27 Nov 2022 09:38:40 -0500 Subject: [PATCH 08/18] chore: get event test working --- packages/core/src/arena.rs | 53 +++- packages/core/src/create.rs | 3 +- packages/core/src/virtual_dom.rs | 10 +- packages/core/tests/lifecycle.rs | 409 +++++++++++++++--------------- packages/html/src/events/mouse.rs | 2 +- 5 files changed, 263 insertions(+), 214 deletions(-) diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index ec4ff862c..a0da5b6ef 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -6,17 +6,23 @@ pub struct ElementId(pub usize); pub struct ElementRef { // the pathway of the real element inside the template - pub path: &'static [u8], + pub path: ElementPath, // The actual template pub template: *mut VNode<'static>, } +#[derive(Clone, Copy)] +pub enum ElementPath { + Deep(&'static [u8]), + Root(usize), +} + impl ElementRef { pub fn null() -> Self { Self { template: std::ptr::null_mut(), - path: &[], + path: ElementPath::Root(0), } } } @@ -28,7 +34,21 @@ impl<'b> VirtualDom { entry.insert(ElementRef { template: template as *const _ as *mut _, - path, + path: ElementPath::Deep(path), + }); + + println!("Claiming {}", id); + + ElementId(id) + } + + pub fn next_root(&mut self, template: &VNode, path: usize) -> ElementId { + let entry = self.elements.vacant_entry(); + let id = entry.key(); + + entry.insert(ElementRef { + template: template as *const _ as *mut _, + path: ElementPath::Root(path), }); println!("Claiming {}", id); @@ -76,3 +96,30 @@ impl<'b> VirtualDom { // } } } +impl ElementPath { + pub(crate) fn is_ascendant(&self, big: &&[u8]) -> bool { + match *self { + ElementPath::Deep(small) => small.len() <= big.len() && small == &big[..small.len()], + ElementPath::Root(r) => big.len() == 1 && big[0] == r as u8, + } + } +} + +#[test] +fn path_ascendant() { + // assert!(&ElementPath::Deep(&[]).is_ascendant(&&[0_u8])); + // assert!(&ElementPath::Deep(&[1, 2]), &[1, 2, 3]); + // assert!(!is_path_ascendant( + // &ElementPath::Deep(&[1, 2, 3, 4]), + // &[1, 2, 3] + // )); +} + +impl PartialEq<&[u8]> for ElementPath { + fn eq(&self, other: &&[u8]) -> bool { + match *self { + ElementPath::Deep(deep) => deep.eq(*other), + ElementPath::Root(r) => other.len() == 1 && other[0] == r as u8, + } + } +} diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index 81be55199..fc7ab37c7 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -63,7 +63,8 @@ impl<'b: 'static> VirtualDom { } TemplateNode::Element { .. } | TemplateNode::Text(_) => { - let this_id = self.next_element(template, &[]); + let this_id = self.next_root(template, root_idx); + template.root_ids[root_idx].set(this_id); self.mutations.push(LoadTemplate { name: template.template.id, diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index d77e1e37c..3faf732d9 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -6,7 +6,7 @@ use crate::{ any_props::VProps, arena::{ElementId, ElementRef}, factory::RenderReturn, - innerlude::{DirtyScope, Mutations, Scheduler, SchedulerMsg}, + innerlude::{DirtyScope, ElementPath, Mutations, Scheduler, SchedulerMsg}, mutations::Mutation, nodes::{Template, TemplateId}, scheduler::{SuspenseBoundary, SuspenseId}, @@ -368,15 +368,11 @@ impl VirtualDom { let target_path = el_ref.path; for (idx, attr) in template.dynamic_attrs.iter().enumerate() { - fn is_path_ascendant(small: &[u8], big: &[u8]) -> bool { - small.len() >= big.len() && small == &big[..small.len()] - } - let this_path = template.template.attr_paths[idx]; // listeners are required to be prefixed with "on", but they come back to the virtualdom with that missing // we should fix this so that we look for "onclick" instead of "click" - if &attr.name[2..] == name && is_path_ascendant(&target_path, &this_path) { + if &attr.name[2..] == name && target_path.is_ascendant(&this_path) { listeners.push(&attr.value); // Break if the event doesn't bubble anyways @@ -387,7 +383,7 @@ impl VirtualDom { // Break if this is the exact target element. // This means we won't call two listeners with the same name on the same element. This should be // documented, or be rejected from the rsx! macro outright - if this_path == target_path { + if target_path == this_path { break; } } diff --git a/packages/core/tests/lifecycle.rs b/packages/core/tests/lifecycle.rs index 0c9850345..06baf32a0 100644 --- a/packages/core/tests/lifecycle.rs +++ b/packages/core/tests/lifecycle.rs @@ -4,6 +4,7 @@ //! Tests for the lifecycle of components. use dioxus::core::{ElementId, Mutation::*}; use dioxus::prelude::*; +use std::rc::Rc; use std::sync::{Arc, Mutex}; type Shared = Arc>; @@ -14,21 +15,26 @@ fn manual_diffing() { value: Shared<&'static str>, } - static App: Component = |cx| { + fn app(cx: Scope) -> Element { let val = cx.props.value.lock().unwrap(); - cx.render(rsx_without_templates! { div { "{val}" } }) + cx.render(rsx! { div { "{val}" } }) }; let value = Arc::new(Mutex::new("Hello")); - let mut dom = VirtualDom::new_with_props(App, AppProps { value: value.clone() }); + let mut dom = VirtualDom::new_with_props(app, AppProps { value: value.clone() }); let _ = dom.rebuild(); *value.lock().unwrap() = "goodbye"; - let edits = dom.rebuild(); - - println!("edits: {:?}", edits); + assert_eq!( + dom.rebuild().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(3) }, + HydrateText { path: &[0], value: "goodbye", id: ElementId(4) }, + AppendChildren { m: 1 } + ] + ); } #[test] @@ -36,229 +42,228 @@ fn events_generate() { fn app(cx: Scope) -> Element { let count = cx.use_hook(|| 0); - let inner = match *count { - 0 => { - rsx_without_templates! { - div { - onclick: move |_| *count += 1, - div { - "nested" - } - "Click me!" - } + match *count { + 0 => cx.render(rsx! { + div { + onclick: move |_| { + *count += 1 + }, + div { "nested" } + "Click me!" } - } - _ => todo!(), - }; - - cx.render(inner) + }), + _ => cx.render(rsx!(())), + } }; let mut dom = VirtualDom::new(app); - let mut channel = dom.get_scheduler_channel(); - assert!(dom.has_work()); + _ = dom.rebuild(); + + dom.handle_event( + "click", + Rc::new(MouseData::default()), + ElementId(1), + true, + EventPriority::Immediate, + ); + + dom.mark_dirty_scope(ScopeId(0)); + let edits = dom.render_immediate(); - let edits = dom.rebuild(); assert_eq!( edits.edits, [ - CreateElement { root: Some(1), tag: "div", children: 0 }, - NewEventListener { event_name: "click", scope: ScopeId(0), root: Some(1) }, - CreateElement { root: Some(2), tag: "div", children: 0 }, - CreateTextNode { root: Some(3), text: "nested" }, - AppendChildren { root: Some(2), children: vec![3] }, - CreateTextNode { root: Some(4), text: "Click me!" }, - AppendChildren { root: Some(1), children: vec![2, 4] }, - AppendChildren { root: Some(0), children: vec![1] } + CreatePlaceholder { id: ElementId(2) }, + ReplaceWith { id: ElementId(1), m: 1 } ] ) } -#[test] -fn components_generate() { - fn app(cx: Scope) -> Element { - let render_phase = cx.use_hook(|| 0); - *render_phase += 1; +// #[test] +// fn components_generate() { +// fn app(cx: Scope) -> Element { +// let render_phase = cx.use_hook(|| 0); +// *render_phase += 1; - cx.render(match *render_phase { - 1 => rsx_without_templates!("Text0"), - 2 => rsx_without_templates!(div {}), - 3 => rsx_without_templates!("Text2"), - 4 => rsx_without_templates!(Child {}), - 5 => rsx_without_templates!({ None as Option<()> }), - 6 => rsx_without_templates!("text 3"), - 7 => rsx_without_templates!({ (0..2).map(|f| rsx_without_templates!("text {f}")) }), - 8 => rsx_without_templates!(Child {}), - _ => todo!(), - }) - }; +// cx.render(match *render_phase { +// 1 => rsx_without_templates!("Text0"), +// 2 => rsx_without_templates!(div {}), +// 3 => rsx_without_templates!("Text2"), +// 4 => rsx_without_templates!(Child {}), +// 5 => rsx_without_templates!({ None as Option<()> }), +// 6 => rsx_without_templates!("text 3"), +// 7 => rsx_without_templates!({ (0..2).map(|f| rsx_without_templates!("text {f}")) }), +// 8 => rsx_without_templates!(Child {}), +// _ => todo!(), +// }) +// }; - fn Child(cx: Scope) -> Element { - println!("Running child"); - cx.render(rsx_without_templates! { - h1 {} - }) - } +// fn Child(cx: Scope) -> Element { +// println!("Running child"); +// cx.render(rsx_without_templates! { +// h1 {} +// }) +// } - let mut dom = VirtualDom::new(app); - let edits = dom.rebuild(); - assert_eq!( - edits.edits, - [ - CreateTextNode { root: Some(1), text: "Text0" }, - AppendChildren { root: Some(0), children: vec![1] } - ] - ); +// let mut dom = VirtualDom::new(app); +// let edits = dom.rebuild(); +// assert_eq!( +// edits.edits, +// [ +// CreateTextNode { root: Some(1), text: "Text0" }, +// AppendChildren { root: Some(0), children: vec![1] } +// ] +// ); - assert_eq!( - dom.hard_diff(ScopeId(0)).edits, - [ - CreateElement { root: Some(2), tag: "div", children: 0 }, - ReplaceWith { root: Some(1), nodes: vec![2] } - ] - ); +// assert_eq!( +// dom.hard_diff(ScopeId(0)).edits, +// [ +// CreateElement { root: Some(2), tag: "div", children: 0 }, +// ReplaceWith { root: Some(1), nodes: vec![2] } +// ] +// ); - assert_eq!( - dom.hard_diff(ScopeId(0)).edits, - [ - CreateTextNode { root: Some(1), text: "Text2" }, - ReplaceWith { root: Some(2), nodes: vec![1] } - ] - ); +// assert_eq!( +// dom.hard_diff(ScopeId(0)).edits, +// [ +// CreateTextNode { root: Some(1), text: "Text2" }, +// ReplaceWith { root: Some(2), nodes: vec![1] } +// ] +// ); - // child {} - assert_eq!( - dom.hard_diff(ScopeId(0)).edits, - [ - CreateElement { root: Some(2), tag: "h1", children: 0 }, - ReplaceWith { root: Some(1), nodes: vec![2] } - ] - ); +// // child {} +// assert_eq!( +// dom.hard_diff(ScopeId(0)).edits, +// [ +// CreateElement { root: Some(2), tag: "h1", children: 0 }, +// ReplaceWith { root: Some(1), nodes: vec![2] } +// ] +// ); - // placeholder - assert_eq!( - dom.hard_diff(ScopeId(0)).edits, - [ - CreatePlaceholder { root: Some(1) }, - ReplaceWith { root: Some(2), nodes: vec![1] } - ] - ); +// // placeholder +// assert_eq!( +// dom.hard_diff(ScopeId(0)).edits, +// [ +// CreatePlaceholder { root: Some(1) }, +// ReplaceWith { root: Some(2), nodes: vec![1] } +// ] +// ); - assert_eq!( - dom.hard_diff(ScopeId(0)).edits, - [ - CreateTextNode { root: Some(2), text: "text 3" }, - ReplaceWith { root: Some(1), nodes: vec![2] } - ] - ); +// assert_eq!( +// dom.hard_diff(ScopeId(0)).edits, +// [ +// CreateTextNode { root: Some(2), text: "text 3" }, +// ReplaceWith { root: Some(1), nodes: vec![2] } +// ] +// ); - assert_eq!( - dom.hard_diff(ScopeId(0)).edits, - [ - CreateTextNode { text: "text 0", root: Some(1) }, - CreateTextNode { text: "text 1", root: Some(3) }, - ReplaceWith { root: Some(2), nodes: vec![1, 3] }, - ] - ); +// assert_eq!( +// dom.hard_diff(ScopeId(0)).edits, +// [ +// CreateTextNode { text: "text 0", root: Some(1) }, +// CreateTextNode { text: "text 1", root: Some(3) }, +// ReplaceWith { root: Some(2), nodes: vec![1, 3] }, +// ] +// ); - assert_eq!( - dom.hard_diff(ScopeId(0)).edits, - [ - CreateElement { tag: "h1", root: Some(2), children: 0 }, - ReplaceWith { root: Some(1), nodes: vec![2] }, - Remove { root: Some(3) }, - ] - ); -} +// assert_eq!( +// dom.hard_diff(ScopeId(0)).edits, +// [ +// CreateElement { tag: "h1", root: Some(2), children: 0 }, +// ReplaceWith { root: Some(1), nodes: vec![2] }, +// Remove { root: Some(3) }, +// ] +// ); +// } -#[test] -fn component_swap() { - fn app(cx: Scope) -> Element { - let render_phase = cx.use_hook(|| 0); - *render_phase += 1; +// #[test] +// fn component_swap() { +// fn app(cx: Scope) -> Element { +// let render_phase = cx.use_hook(|| 0); +// *render_phase += 1; - cx.render(match *render_phase { - 0 => rsx_without_templates!( - div { - NavBar {} - Dashboard {} - } - ), - 1 => rsx_without_templates!( - div { - NavBar {} - Results {} - } - ), - 2 => rsx_without_templates!( - div { - NavBar {} - Dashboard {} - } - ), - 3 => rsx_without_templates!( - div { - NavBar {} - Results {} - } - ), - 4 => rsx_without_templates!( - div { - NavBar {} - Dashboard {} - } - ), - _ => rsx_without_templates!("blah"), - }) - }; +// cx.render(match *render_phase { +// 0 => rsx_without_templates!( +// div { +// NavBar {} +// Dashboard {} +// } +// ), +// 1 => rsx_without_templates!( +// div { +// NavBar {} +// Results {} +// } +// ), +// 2 => rsx_without_templates!( +// div { +// NavBar {} +// Dashboard {} +// } +// ), +// 3 => rsx_without_templates!( +// div { +// NavBar {} +// Results {} +// } +// ), +// 4 => rsx_without_templates!( +// div { +// NavBar {} +// Dashboard {} +// } +// ), +// _ => rsx_without_templates!("blah"), +// }) +// }; - static NavBar: Component = |cx| { - println!("running navbar"); - cx.render(rsx_without_templates! { - h1 { - "NavBar" - {(0..3).map(|f| rsx_without_templates!(NavLink {}))} - } - }) - }; +// static NavBar: Component = |cx| { +// println!("running navbar"); +// cx.render(rsx_without_templates! { +// h1 { +// "NavBar" +// {(0..3).map(|f| rsx_without_templates!(NavLink {}))} +// } +// }) +// }; - static NavLink: Component = |cx| { - println!("running navlink"); - cx.render(rsx_without_templates! { - h1 { - "NavLink" - } - }) - }; +// static NavLink: Component = |cx| { +// println!("running navlink"); +// cx.render(rsx_without_templates! { +// h1 { +// "NavLink" +// } +// }) +// }; - static Dashboard: Component = |cx| { - println!("running dashboard"); - cx.render(rsx_without_templates! { - div { - "dashboard" - } - }) - }; +// static Dashboard: Component = |cx| { +// println!("running dashboard"); +// cx.render(rsx_without_templates! { +// div { +// "dashboard" +// } +// }) +// }; - static Results: Component = |cx| { - println!("running results"); - cx.render(rsx_without_templates! { - div { - "results" - } - }) - }; +// static Results: Component = |cx| { +// println!("running results"); +// cx.render(rsx_without_templates! { +// div { +// "results" +// } +// }) +// }; - let mut dom = VirtualDom::new(app); - let edits = dom.rebuild(); - dbg!(&edits); +// let mut dom = VirtualDom::new(app); +// let edits = dom.rebuild(); +// dbg!(&edits); - let edits = dom.work_with_deadline(|| false); - dbg!(&edits); - let edits = dom.work_with_deadline(|| false); - dbg!(&edits); - let edits = dom.work_with_deadline(|| false); - dbg!(&edits); - let edits = dom.work_with_deadline(|| false); - dbg!(&edits); -} +// let edits = dom.work_with_deadline(|| false); +// dbg!(&edits); +// let edits = dom.work_with_deadline(|| false); +// dbg!(&edits); +// let edits = dom.work_with_deadline(|| false); +// dbg!(&edits); +// let edits = dom.work_with_deadline(|| false); +// dbg!(&edits); +// } diff --git a/packages/html/src/events/mouse.rs b/packages/html/src/events/mouse.rs index e7a7a362f..9738fb80f 100644 --- a/packages/html/src/events/mouse.rs +++ b/packages/html/src/events/mouse.rs @@ -10,7 +10,7 @@ pub type MouseEvent = UiEvent; /// A synthetic event that wraps a web-style [`MouseEvent`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone)] +#[derive(Clone, Default)] /// Data associated with a mouse event /// /// Do not use the deprecated fields; they may change or become private in the future. From 7c3d308ab5f822bc24344d1b7f28d2f78f0c73ca Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 29 Nov 2022 16:31:04 -0500 Subject: [PATCH 09/18] chore: make warnings go away --- packages/core/Cargo.toml | 1 + packages/core/src/any_props.rs | 4 +- packages/core/src/arena.rs | 106 +++-- packages/core/src/bump_frame.rs | 21 +- packages/core/src/create.rs | 59 ++- packages/core/src/diff.rs | 166 ++++--- packages/core/src/error_boundary.rs | 34 +- packages/core/src/events.rs | 52 --- packages/core/src/factory.rs | 6 +- packages/core/src/fragment.rs | 24 +- packages/core/src/future_container.rs | 63 --- packages/core/src/lib.rs | 9 +- packages/core/src/mutations.rs | 1 + packages/core/src/nodes.rs | 25 +- packages/core/src/scope_arena.rs | 58 ++- packages/core/src/scopes.rs | 17 +- packages/core/src/virtual_dom.rs | 34 +- packages/core/tests/attr_cleanup.rs | 8 +- packages/core/tests/bubble_error.rs | 4 +- packages/core/tests/context_api.rs | 14 +- packages/core/tests/create_lists.rs | 2 +- packages/core/tests/cycle.rs | 6 +- packages/core/tests/diff_component.rs | 102 +++++ packages/core/tests/diff_element.rs | 18 +- packages/core/tests/diff_keyed_list.rs | 52 ++- packages/core/tests/diff_unkeyed_list.rs | 36 +- packages/core/tests/hotreloading.rs | 1 + packages/core/tests/lifecycle.rs | 107 +---- packages/core/tests/miri_simple.rs | 127 ++++++ packages/core/tests/miri_stress.rs | 545 ++++++++++------------- packages/core/tests/safety.rs | 4 - packages/core/tests/suspense.rs | 2 +- packages/core/tests/task.rs | 2 +- packages/rsx/src/lib.rs | 1 - 34 files changed, 901 insertions(+), 810 deletions(-) delete mode 100644 packages/core/src/future_container.rs create mode 100644 packages/core/tests/diff_component.rs create mode 100644 packages/core/tests/hotreloading.rs create mode 100644 packages/core/tests/miri_simple.rs diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 55e0ff30a..9479bfc83 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -34,6 +34,7 @@ indexmap = "1.7" # Serialize the Edits for use in Webview/Liveview instances serde = { version = "1", features = ["derive"], optional = true } anyhow = "1.0.66" +bumpslab = "0.1.0" [dev-dependencies] tokio = { version = "*", features = ["full"] } diff --git a/packages/core/src/any_props.rs b/packages/core/src/any_props.rs index 532fe02ca..797fb8ddd 100644 --- a/packages/core/src/any_props.rs +++ b/packages/core/src/any_props.rs @@ -18,7 +18,6 @@ pub(crate) struct VProps<'a, P, A, F: ComponentReturn<'a, A> = Element<'a>> { pub render_fn: fn(Scope<'a, P>) -> F, pub memo: unsafe fn(&P, &P) -> bool, pub props: P, - // pub props: PropsAllocation

, _marker: PhantomData, } @@ -35,7 +34,6 @@ where render_fn, memo, props, - // props: PropsAllocation::Borrowed(props), _marker: PhantomData, } } @@ -60,7 +58,7 @@ where } fn render(&'a self, cx: &'a ScopeState) -> RenderReturn<'a> { - let scope = cx.bump().alloc(Scoped { + let scope: &mut Scoped

= cx.bump().alloc(Scoped { props: &self.props, scope: cx, }); diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index a0da5b6ef..6add899e8 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -1,10 +1,14 @@ -use crate::{nodes::VNode, virtual_dom::VirtualDom, Mutations, ScopeId}; +use crate::{ + factory::RenderReturn, nodes::VNode, virtual_dom::VirtualDom, AttributeValue, DynamicNode, + ScopeId, VFragment, +}; +use bumpalo::boxed::Box as BumpBox; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct ElementId(pub usize); -pub struct ElementRef { +pub(crate) struct ElementRef { // the pathway of the real element inside the template pub path: ElementPath, @@ -19,7 +23,7 @@ pub enum ElementPath { } impl ElementRef { - pub fn null() -> Self { + pub(crate) fn null() -> Self { Self { template: std::ptr::null_mut(), path: ElementPath::Root(0), @@ -28,74 +32,88 @@ impl ElementRef { } impl<'b> VirtualDom { - pub fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId { + pub(crate) fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId { let entry = self.elements.vacant_entry(); let id = entry.key(); - entry.insert(ElementRef { template: template as *const _ as *mut _, path: ElementPath::Deep(path), }); - - println!("Claiming {}", id); - ElementId(id) } - pub fn next_root(&mut self, template: &VNode, path: usize) -> ElementId { + pub(crate) fn next_root(&mut self, template: &VNode, path: usize) -> ElementId { let entry = self.elements.vacant_entry(); let id = entry.key(); - entry.insert(ElementRef { template: template as *const _ as *mut _, path: ElementPath::Root(path), }); - - println!("Claiming {}", id); - ElementId(id) } - pub fn cleanup_element(&mut self, id: ElementId) { - self.elements.remove(id.0); + pub(crate) fn reclaim(&mut self, el: ElementId) { + self.try_reclaim(el) + .unwrap_or_else(|| panic!("cannot reclaim {:?}", el)); } - pub fn drop_scope(&mut self, id: ScopeId) { - // let scope = self.scopes.get(id.0).unwrap(); - - // let root = scope.root_node(); - // let root = unsafe { std::mem::transmute(root) }; - - // self.drop_template(root, false); - todo!() - } - - pub fn reclaim(&mut self, el: ElementId) { + pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option { assert_ne!(el, ElementId(0)); - self.elements.remove(el.0); + self.elements.try_remove(el.0) } - pub fn drop_template( - &mut self, - mutations: &mut Mutations, - template: &'b VNode<'b>, - gen_roots: bool, - ) { - // for node in template.dynamic_nodes.iter() { - // match node { - // DynamicNode::Text { id, .. } => {} + // Drop a scope and all its children + pub(crate) fn drop_scope(&mut self, id: ScopeId) { + let scope = self.scopes.get(id.0).unwrap(); - // DynamicNode::Component { .. } => { - // todo!() - // } + if let Some(root) = scope.as_ref().try_root_node() { + let root = unsafe { root.extend_lifetime_ref() }; + match root { + RenderReturn::Sync(Ok(node)) => self.drop_scope_inner(node), + _ => {} + } + } - // DynamicNode::Fragment { inner, nodes } => {} - // DynamicNode::Placeholder(_) => todo!(), - // _ => todo!(), - // } - // } + let scope = self.scopes.get(id.0).unwrap(); + + if let Some(root) = unsafe { scope.as_ref().previous_frame().try_load_node() } { + let root = unsafe { root.extend_lifetime_ref() }; + match root { + RenderReturn::Sync(Ok(node)) => self.drop_scope_inner(node), + _ => {} + } + } + + let scope = self.scopes.get(id.0).unwrap().as_ref(); + + // Drop all the hooks once the children are dropped + // this means we'll drop hooks bottom-up + for hook in scope.hook_list.borrow_mut().drain(..) { + drop(unsafe { BumpBox::from_raw(hook) }); + } + } + + fn drop_scope_inner(&mut self, node: &VNode) { + for attr in node.dynamic_attrs { + if let AttributeValue::Listener(l) = &attr.value { + l.borrow_mut().take(); + } + } + + for (idx, _) in node.template.roots.iter().enumerate() { + match node.dynamic_root(idx) { + Some(DynamicNode::Component(c)) => self.drop_scope(c.scope.get().unwrap()), + Some(DynamicNode::Fragment(VFragment::NonEmpty(nodes))) => { + for node in *nodes { + self.drop_scope_inner(node); + } + } + _ => {} + } + } } } + impl ElementPath { pub(crate) fn is_ascendant(&self, big: &&[u8]) -> bool { match *self { diff --git a/packages/core/src/bump_frame.rs b/packages/core/src/bump_frame.rs index ca7ff66f6..581452836 100644 --- a/packages/core/src/bump_frame.rs +++ b/packages/core/src/bump_frame.rs @@ -2,9 +2,9 @@ use crate::factory::RenderReturn; use bumpalo::Bump; use std::cell::Cell; -pub struct BumpFrame { +pub(crate) struct BumpFrame { pub bump: Bump, - pub node: Cell<*mut RenderReturn<'static>>, + pub node: Cell<*const RenderReturn<'static>>, } impl BumpFrame { @@ -12,17 +12,18 @@ impl BumpFrame { let bump = Bump::with_capacity(capacity); Self { bump, - node: Cell::new(std::ptr::null_mut()), + node: Cell::new(std::ptr::null()), } } - pub fn reset(&mut self) { - self.bump.reset(); - self.node.set(std::ptr::null_mut()); - } - /// Creates a new lifetime out of thin air - pub unsafe fn load_node<'b>(&self) -> &'b RenderReturn<'b> { - unsafe { std::mem::transmute(&*self.node.get()) } + pub unsafe fn try_load_node<'b>(&self) -> Option<&'b RenderReturn<'b>> { + let node = self.node.get(); + + if node.is_null() { + return None; + } + + unsafe { std::mem::transmute(&*node) } } } diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index fc7ab37c7..1a308a577 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -5,9 +5,9 @@ use crate::mutations::Mutation::*; use crate::nodes::VNode; use crate::nodes::{DynamicNode, TemplateNode}; use crate::virtual_dom::VirtualDom; -use crate::{AttributeValue, ElementId, ScopeId, SuspenseContext, TemplateAttribute}; +use crate::{AttributeValue, ScopeId, SuspenseContext, TemplateAttribute}; -impl<'b: 'static> VirtualDom { +impl<'b> VirtualDom { /// Create a new template [`VNode`] and write it to the [`Mutations`] buffer. /// /// This method pushes the ScopeID to the internal scopestack and returns the number of nodes created. @@ -44,7 +44,14 @@ impl<'b: 'static> VirtualDom { 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 }); + + // Safety: we promise not to re-alias this text later on after committing it to the mutation + let unbounded_text = unsafe { std::mem::transmute(*value) }; + self.mutations.push(CreateTextNode { + value: unbounded_text, + id, + }); + 1 } @@ -96,16 +103,24 @@ impl<'b: 'static> VirtualDom { let attribute = template.dynamic_attrs.get(attr_id).unwrap(); attribute.mounted_element.set(id); + // Safety: we promise not to re-alias this text later on after committing it to the mutation + let unbounded_name = unsafe { std::mem::transmute(attribute.name) }; + match &attribute.value { - AttributeValue::Text(value) => self.mutations.push(SetAttribute { - name: attribute.name, - value: *value, - ns: attribute.namespace, - id, - }), + AttributeValue::Text(value) => { + // Safety: we promise not to re-alias this text later on after committing it to the mutation + let unbounded_value = unsafe { std::mem::transmute(*value) }; + + self.mutations.push(SetAttribute { + name: unbounded_name, + value: unbounded_value, + ns: attribute.namespace, + id, + }) + } AttributeValue::Bool(value) => { self.mutations.push(SetBoolAttribute { - name: attribute.name, + name: unbounded_name, value: *value, id, }) @@ -113,7 +128,7 @@ impl<'b: 'static> VirtualDom { AttributeValue::Listener(_) => { self.mutations.push(NewEventListener { // all listeners start with "on" - event_name: &attribute.name[2..], + event_name: &unbounded_name[2..], scope: cur_scope, id, }) @@ -315,11 +330,14 @@ impl<'b: 'static> VirtualDom { // Make sure the text node is assigned to the correct element text.id.set(new_id); + // Safety: we promise not to re-alias this text later on after committing it to the mutation + let value = unsafe { std::mem::transmute(text.value) }; + // Add the mutation to the list self.mutations.push(HydrateText { id: new_id, path: &template.template.node_paths[idx][1..], - value: text.value, + value, }); // Since we're hydrating an existing node, we don't create any new nodes @@ -356,18 +374,20 @@ impl<'b: 'static> VirtualDom { } } - fn create_component_node( + pub(super) fn create_component_node( &mut self, template: &'b VNode<'b>, component: &'b VComponent<'b>, idx: usize, ) -> usize { - let props = component.props.replace(None).unwrap(); + let props = component + .props + .replace(None) + .expect("Props to always exist when a component is being created"); - let prop_ptr = unsafe { std::mem::transmute(props.as_ref()) }; - let scope = self.new_scope(prop_ptr).id; + let unbounded_props = unsafe { std::mem::transmute(props) }; - component.props.replace(Some(props)); + let scope = self.new_scope(unbounded_props).id; component.scope.set(Some(scope)); let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() }; @@ -375,10 +395,7 @@ impl<'b: 'static> VirtualDom { use RenderReturn::*; match return_nodes { - Sync(Ok(t)) => { - self.mount_component(scope, template, t, idx) - // self.create(t) - } + Sync(Ok(t)) => self.mount_component(scope, template, t, idx), Sync(Err(_e)) => todo!("Propogate error upwards"), Async(_) => self.mount_component_placeholder(template, idx, scope), } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 75141e002..669288ca7 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -13,39 +13,29 @@ use fxhash::{FxHashMap, FxHashSet}; use std::cell::Cell; use DynamicNode::*; -impl<'b: 'static> VirtualDom { +impl<'b> VirtualDom { pub fn diff_scope(&mut self, scope: ScopeId) { let scope_state = &mut self.scopes[scope.0]; - // Load the old and new bump arenas - let cur_arena = scope_state.current_frame(); - let prev_arena = scope_state.previous_frame(); - - // Make sure the nodes arent null (they've been set properly) - // This is a rough check to make sure we're not entering any UB - assert_ne!( - cur_arena.node.get(), - std::ptr::null_mut(), - "Call rebuild before diffing" - ); - assert_ne!( - prev_arena.node.get(), - std::ptr::null_mut(), - "Call rebuild before diffing" - ); - self.scope_stack.push(scope); unsafe { - let cur_arena = cur_arena.load_node(); - let prev_arena = prev_arena.load_node(); - self.diff_maybe_node(prev_arena, cur_arena); + // Load the old and new bump arenas + let old = scope_state + .previous_frame() + .try_load_node() + .expect("Call rebuild before diffing"); + let new = scope_state + .current_frame() + .try_load_node() + .expect("Call rebuild before diffing"); + self.diff_maybe_node(old, new); } self.scope_stack.pop(); } - fn diff_maybe_node(&mut self, left: &'b RenderReturn<'b>, right: &'b RenderReturn<'b>) { + fn diff_maybe_node(&mut self, old: &'b RenderReturn<'b>, new: &'b RenderReturn<'b>) { use RenderReturn::{Async, Sync}; - match (left, right) { + match (old, new) { (Sync(Ok(l)), Sync(Ok(r))) => self.diff_node(l, r), // Err cases @@ -62,10 +52,10 @@ impl<'b: 'static> VirtualDom { } } - fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, e: &anyhow::Error) { + fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>, _e: &anyhow::Error) { todo!("Not yet handling error rollover") } - fn diff_err_to_ok(&mut self, e: &anyhow::Error, l: &'b VNode<'b>) { + fn diff_err_to_ok(&mut self, _e: &anyhow::Error, _l: &'b VNode<'b>) { todo!("Not yet handling error rollover") } @@ -88,11 +78,13 @@ impl<'b: 'static> VirtualDom { // todo: add more types of attribute values match right_attr.value { AttributeValue::Text(text) => { + let name = unsafe { std::mem::transmute(left_attr.name) }; + let value = unsafe { std::mem::transmute(text) }; self.mutations.push(Mutation::SetAttribute { id: left_attr.mounted_element.get(), - name: left_attr.name, - value: text, ns: right_attr.namespace, + name, + value, }); } // todo: more types of attribute values @@ -101,15 +93,18 @@ impl<'b: 'static> VirtualDom { } } - for (left_node, right_node) in left_template + for (idx, (left_node, right_node)) in left_template .dynamic_nodes .iter() .zip(right_template.dynamic_nodes.iter()) + .enumerate() { match (left_node, right_node) { (Text(left), Text(right)) => self.diff_vtext(left, right), (Fragment(left), Fragment(right)) => self.diff_vfragment(left, right), - (Component(left), Component(right)) => self.diff_vcomponent(left, right), + (Component(left), Component(right)) => { + self.diff_vcomponent(left, right, right_template, idx) + } _ => self.replace(left_template, right_template), }; } @@ -124,24 +119,44 @@ impl<'b: 'static> VirtualDom { } } - fn diff_vcomponent(&mut self, left: &'b VComponent<'b>, right: &'b VComponent<'b>) { - // Due to how templates work, we should never get two different components. The only way we could enter - // this codepath is through "light_diff", but we check there that the pointers are the same - assert_eq!(left.render_fn, right.render_fn); + fn diff_vcomponent( + &mut self, + left: &'b VComponent<'b>, + right: &'b VComponent<'b>, + right_template: &'b VNode<'b>, + idx: usize, + ) { + // Replace components that have different render fns + if left.render_fn != right.render_fn { + dbg!(&left, &right); + let created = self.create_component_node(right_template, right, idx); + let head = unsafe { + self.scopes[left.scope.get().unwrap().0] + .root_node() + .extend_lifetime_ref() + }; + let id = match head { + RenderReturn::Sync(Ok(node)) => self.replace_inner(node), + _ => todo!(), + }; + self.mutations + .push(Mutation::ReplaceWith { id, m: created }); + return; + } // Make sure the new vcomponent has the right scopeid associated to it let scope_id = left.scope.get().unwrap(); right.scope.set(Some(scope_id)); // copy out the box for both - let old = left.props.replace(None).unwrap(); + let old = self.scopes[scope_id.0].props.as_ref(); let new = right.props.replace(None).unwrap(); // If the props are static, then we try to memoize by setting the new with the old // The target scopestate still has the reference to the old props, so there's no need to update anything // This also implicitly drops the new props since they're not used if left.static_props && unsafe { old.memoize(new.as_ref()) } { - return right.props.set(Some(old)); + return; } // If the props are dynamic *or* the memoization failed, then we need to diff the props @@ -198,7 +213,8 @@ impl<'b: 'static> VirtualDom { None => self.replace(left, right), Some(components) => components .into_iter() - .for_each(|(l, r)| self.diff_vcomponent(l, r)), + .enumerate() + .for_each(|(idx, (l, r))| self.diff_vcomponent(l, r, right, idx)), } } @@ -207,12 +223,12 @@ impl<'b: 'static> VirtualDom { /// This just moves the ID of the old node over to the new node, and then sets the text of the new node if it's /// different. fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>) { - right.id.set(left.id.get()); + let id = left.id.get(); + + right.id.set(id); if left.value != right.value { - self.mutations.push(Mutation::SetText { - id: left.id.get(), - value: right.value, - }); + let value = unsafe { std::mem::transmute(right.value) }; + self.mutations.push(Mutation::SetText { id, value }); } } @@ -234,22 +250,25 @@ impl<'b: 'static> VirtualDom { } fn replace_nodes_with_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b Cell) { + // Create the placeholder first, ensuring we get a dedicated ID for the placeholder + let placeholder = self.next_element(&l[0], &[]); + r.set(placeholder); + self.mutations + .push(Mutation::CreatePlaceholder { id: placeholder }); + // Remove the old nodes, except for onea let first = self.replace_inner(&l[0]); self.remove_nodes(&l[1..]); - let placeholder = self.next_element(&l[0], &[]); - r.set(placeholder); - - self.mutations - .push(Mutation::CreatePlaceholder { id: placeholder }); self.mutations .push(Mutation::ReplaceWith { id: first, m: 1 }); - self.reclaim(first); + self.try_reclaim(first); } - // Remove all the top-level nodes, returning the firstmost root ElementId + /// Remove all the top-level nodes, returning the firstmost root ElementId + /// + /// All IDs will be garbage collected fn replace_inner(&mut self, node: &'b VNode<'b>) -> ElementId { let id = match node.dynamic_root(0) { None => node.root_ids[0].get(), @@ -284,8 +303,13 @@ impl<'b: 'static> VirtualDom { /// /// Simply walks through the dynamic nodes fn clean_up_node(&mut self, node: &'b VNode<'b>) { - for node in node.dynamic_nodes { - match node { + for (idx, dyn_node) in node.dynamic_nodes.iter().enumerate() { + // Roots are cleaned up automatically? + if node.template.node_paths[idx].len() == 1 { + continue; + } + + match dyn_node { Component(comp) => { let scope = comp.scope.get().unwrap(); match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { @@ -596,10 +620,9 @@ impl<'b: 'static> VirtualDom { // 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_keys.is_empty() { - if let Some(first_old) = old.get(0) { + if let Some(_) = old.get(0) { self.remove_nodes(&old[1..]); - let nodes_created = self.create_children(new); - self.replace_node_with_on_stack(first_old, nodes_created); + self.replace_many(&old[0], new); } else { // I think this is wrong - why are we appending? // only valid of the if there are no trailing elements @@ -718,10 +741,6 @@ impl<'b: 'static> VirtualDom { } } - fn replace_node_with_on_stack(&mut self, old: &'b VNode<'b>, m: usize) { - todo!() - } - /// Remove these nodes from the dom /// Wont generate mutations for the inner nodes fn remove_nodes(&mut self, nodes: &'b [VNode<'b>]) { @@ -766,7 +785,7 @@ impl<'b: 'static> VirtualDom { } /// Push all the real nodes on the stack - fn push_all_real_nodes(&mut self, node: &VNode) -> usize { + fn push_all_real_nodes(&mut self, node: &'b VNode<'b>) -> usize { let mut onstack = 0; for (idx, _) in node.template.roots.iter().enumerate() { @@ -785,7 +804,12 @@ impl<'b: 'static> VirtualDom { } } Some(Component(comp)) => { - todo!() + let scope = comp.scope.get().unwrap(); + onstack += + match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { + RenderReturn::Sync(Ok(node)) => self.push_all_real_nodes(node), + _ => todo!(), + } } None => { self.mutations.push(Mutation::PushRoot { @@ -815,7 +839,7 @@ impl<'b: 'static> VirtualDom { self.mutations.push(Mutation::InsertAfter { id, m }) } - fn find_first_element(&self, node: &VNode) -> ElementId { + fn find_first_element(&self, node: &'b VNode<'b>) -> ElementId { match node.dynamic_root(0) { None => node.root_ids[0].get(), Some(Text(t)) => t.id.get(), @@ -823,7 +847,7 @@ impl<'b: 'static> VirtualDom { Some(Fragment(VFragment::Empty(t))) => t.get(), Some(Component(comp)) => { let scope = comp.scope.get().unwrap(); - match self.scopes[scope.0].root_node() { + match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { RenderReturn::Sync(Ok(t)) => self.find_first_element(t), _ => todo!("cannot handle nonstandard nodes"), } @@ -831,7 +855,7 @@ impl<'b: 'static> VirtualDom { } } - fn find_last_element(&self, node: &VNode) -> ElementId { + fn find_last_element(&self, node: &'b VNode<'b>) -> ElementId { match node.dynamic_root(node.template.roots.len() - 1) { None => node.root_ids.last().unwrap().get(), Some(Text(t)) => t.id.get(), @@ -839,7 +863,7 @@ impl<'b: 'static> VirtualDom { Some(Fragment(VFragment::Empty(t))) => t.get(), Some(Component(comp)) => { let scope = comp.scope.get().unwrap(); - match self.scopes[scope.0].root_node() { + match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { RenderReturn::Sync(Ok(t)) => self.find_last_element(t), _ => todo!("cannot handle nonstandard nodes"), } @@ -855,7 +879,18 @@ impl<'b: 'static> VirtualDom { id: first, m: created, }); - self.reclaim(id); + self.try_reclaim(id); + } + + fn replace_many(&mut self, left: &'b VNode<'b>, right: &'b [VNode<'b>]) { + let first = self.find_first_element(left); + let id = self.replace_inner(left); + let created = self.create_children(right); + self.mutations.push(Mutation::ReplaceWith { + id: first, + m: created, + }); + self.try_reclaim(id); } } @@ -883,7 +918,7 @@ fn matching_components<'a>( _ => return None, }; - (l.render_fn == r.render_fn).then(|| (l, r)) + Some((l, r)) }) .collect() } @@ -894,6 +929,7 @@ fn matching_components<'a>( /// - for text - we can use SetTextContent /// - for clearning children we can use RemoveChildren /// - for appending children we can use AppendChildren +#[allow(dead_code)] fn is_dyn_node_only_child(node: &VNode, idx: usize) -> bool { let path = node.template.node_paths[idx]; diff --git a/packages/core/src/error_boundary.rs b/packages/core/src/error_boundary.rs index c2a1947da..6f40c2484 100644 --- a/packages/core/src/error_boundary.rs +++ b/packages/core/src/error_boundary.rs @@ -1,29 +1,19 @@ -use std::{cell::RefCell, rc::Rc}; +use std::cell::RefCell; -use crate::{ScopeId, ScopeState}; +use crate::ScopeId; -pub struct ErrorContext { +/// A boundary that will capture any errors from child components +#[allow(dead_code)] +pub struct ErrorBoundary { error: RefCell>, + id: ScopeId, } -/// Catch all errors from the children and bubble them up to this component -/// -/// Returns the error and scope that caused the error -pub fn use_catch_error(cx: &ScopeState) -> Option<&(anyhow::Error, ScopeId)> { - let err_ctx = use_error_context(cx); - - let out = cx.use_hook(|| None); - - if let Some(error) = err_ctx.error.take() { - *out = Some(error); +impl ErrorBoundary { + pub fn new(id: ScopeId) -> Self { + Self { + error: RefCell::new(None), + id, + } } - - out.as_ref() -} - -/// Create a new error context at this component. -/// -/// This component will start to catch any errors that occur in its children. -pub fn use_error_context(cx: &ScopeState) -> &ErrorContext { - cx.use_hook(|| cx.provide_context(Rc::new(ErrorContext { error: None.into() }))) } diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index cb14734d2..f511e73f9 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -50,58 +50,6 @@ impl std::fmt::Debug for UiEvent { } } -/// Priority of Event Triggers. -/// -/// Internally, Dioxus will abort work that's taking too long if new, more important work arrives. Unlike React, Dioxus -/// won't be afraid to pause work or flush changes to the Real Dom. This is called "cooperative scheduling". Some Renderers -/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well. -/// -/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes. -/// -/// React has a 5-tier priority system. However, they break things into "Continuous" and "Discrete" priority. For now, -/// we keep it simple, and just use a 3-tier priority system. -/// -/// - `NoPriority` = 0 -/// - `LowPriority` = 1 -/// - `NormalPriority` = 2 -/// - `UserBlocking` = 3 -/// - `HighPriority` = 4 -/// - `ImmediatePriority` = 5 -/// -/// We still have a concept of discrete vs continuous though - discrete events won't be batched, but continuous events will. -/// This means that multiple "scroll" events will be processed in a single frame, but multiple "click" events will be -/// flushed before proceeding. Multiple discrete events is highly unlikely, though. -#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] -pub enum EventPriority { - /// Work that must be completed during the EventHandler phase. - /// - /// Currently this is reserved for controlled inputs. - Immediate = 3, - - /// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work. - /// - /// This is typically reserved for things like user interaction. - /// - /// React calls these "discrete" events, but with an extra category of "user-blocking" (Immediate). - High = 2, - - /// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important - /// than "High Priority" events and will take precedence over low priority events. - /// - /// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input. - /// - /// React calls these "continuous" events (e.g. mouse move, mouse wheel, touch move, etc). - Medium = 1, - - /// "Low Priority" work will always be preempted unless the work is significantly delayed, in which case it will be - /// advanced to the front of the work queue until completed. - /// - /// The primary user of Low Priority work is the asynchronous work system (Suspense). - /// - /// This is considered "idle" work or "background" work. - Low = 0, -} - type ExternalListenerCallback<'bump, T> = BumpBox<'bump, dyn FnMut(T) + 'bump>; /// The callback type generated by the `rsx!` macro when an `on` field is specified for components. diff --git a/packages/core/src/factory.rs b/packages/core/src/factory.rs index d92899f3d..a529d38bc 100644 --- a/packages/core/src/factory.rs +++ b/packages/core/src/factory.rs @@ -82,9 +82,9 @@ impl ScopeState { // let as_dyn: &dyn AnyProps = self.bump().alloc(vcomp); // todo: clean up borrowed props // if !P::IS_STATIC { - // let vcomp = &*vcomp; - // let vcomp = unsafe { std::mem::transmute(vcomp) }; - // self.scope.items.borrow_mut().borrowed_props.push(vcomp); + // let vcomp = ex; + // let vcomp = unsafe { std::mem::transmute(vcomp) }; + // self.items.borrow_mut().borrowed_props.push(vcomp); // } DynamicNode::Component(VComponent { diff --git a/packages/core/src/fragment.rs b/packages/core/src/fragment.rs index f2e539d17..ebe9104e1 100644 --- a/packages/core/src/fragment.rs +++ b/packages/core/src/fragment.rs @@ -26,17 +26,17 @@ use crate::innerlude::*; /// /// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it. #[allow(non_upper_case_globals, non_snake_case)] -pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element { - let children = cx.props.0.as_ref().unwrap(); - Ok(VNode { - node_id: children.node_id.clone(), - key: children.key.clone(), - parent: children.parent.clone(), - template: children.template, - root_ids: children.root_ids, - dynamic_nodes: children.dynamic_nodes, - dynamic_attrs: children.dynamic_attrs, - }) +pub fn Fragment<'a>(_cx: Scope<'a, FragmentProps<'a>>) -> Element { + // let children = cx.props.0.as_ref().unwrap(); + todo!() + // Ok(VNode { + // key: children.key.clone(), + // parent: children.parent.clone(), + // template: children.template, + // root_ids: children.root_ids, + // dynamic_nodes: children.dynamic_nodes, + // dynamic_attrs: children.dynamic_attrs, + // }) } pub struct FragmentProps<'a>(Element<'a>); @@ -96,7 +96,7 @@ impl<'a> Properties for FragmentProps<'a> { type Builder = FragmentBuilder<'a, false>; const IS_STATIC: bool = false; fn builder() -> Self::Builder { - FragmentBuilder(VNode::empty()) + todo!() } unsafe fn memoize(&self, _other: &Self) -> bool { false diff --git a/packages/core/src/future_container.rs b/packages/core/src/future_container.rs deleted file mode 100644 index 0d2f5b549..000000000 --- a/packages/core/src/future_container.rs +++ /dev/null @@ -1,63 +0,0 @@ -use futures_channel::mpsc::UnboundedSender; -use slab::Slab; -use std::future::Future; -use std::{cell::RefCell, rc::Rc, sync::Arc}; - -use crate::innerlude::ScopeId; -/// The type of message that can be sent to the scheduler. -/// -/// These messages control how the scheduler will process updates to the UI. -#[derive(Debug)] -pub enum SchedulerMsg { - /// Events from athe Renderer - Event, - - /// Immediate updates from Components that mark them as dirty - Immediate(ScopeId), - - /// Mark all components as dirty and update them - DirtyAll, - - /// New tasks from components that should be polled when the next poll is ready - NewTask(ScopeId), -} - -// todo extract this so spawning doesn't require refcell and rc doesnt need to be tracked -#[derive(Clone)] -pub struct FutureQueue { - pub sender: UnboundedSender, - pub queue: RefCell>>>, -} - -impl FutureQueue { - pub fn new(sender: UnboundedSender) -> Self { - Self { - sender, - queue: Default::default(), - } - } - - pub fn spawn(&self, scope: ScopeId, fut: impl Future + 'static) -> TaskId { - let id = self.queue.borrow_mut().insert(Arc::new(fut)); - - TaskId { id, scope } - } - - pub fn remove(&self, id: TaskId) { - todo!() - } -} - -/// A task's unique identifier. -/// -/// `TaskId` is a `usize` that is unique across the entire [`VirtualDom`] and across time. [`TaskID`]s will never be reused -/// once a Task has been completed. -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub struct TaskId { - /// The global ID of the task - pub id: usize, - - /// The original scope that this task was scheduled in - pub scope: ScopeId, -} diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index ac9b4e10f..1754f2766 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -20,6 +20,7 @@ mod virtual_dom; pub(crate) mod innerlude { pub use crate::arena::*; pub use crate::dirty_scope::*; + pub use crate::error_boundary::*; pub use crate::events::*; pub use crate::fragment::*; pub use crate::lazynodes::*; @@ -78,8 +79,6 @@ pub use crate::innerlude::{ DynamicNode, Element, ElementId, - ElementRef, - EventPriority, Fragment, LazyNodes, Mutation, @@ -109,9 +108,9 @@ pub use crate::innerlude::{ /// This includes types like [`Scope`], [`Element`], and [`Component`]. pub mod prelude { pub use crate::innerlude::{ - fc_to_builder, Element, EventHandler, EventPriority, Fragment, LazyNodes, NodeFactory, - Properties, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, - TemplateNode, UiEvent, VNode, VirtualDom, + fc_to_builder, Element, EventHandler, Fragment, LazyNodes, NodeFactory, Properties, Scope, + ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, UiEvent, + VNode, VirtualDom, }; } diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 7e8e10f88..b1dff4011 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -1,6 +1,7 @@ use crate::{arena::ElementId, ScopeId}; #[derive(Debug)] +#[must_use = "not handling edits can lead to visual inconsistencies in UI"] pub struct Mutations<'a> { pub subtree: usize, pub template_mutations: Vec>, diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 1750d4276..40aefbee8 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -1,4 +1,5 @@ use crate::{any_props::AnyProps, arena::ElementId, Element, ScopeId, ScopeState, UiEvent}; +use bumpalo::boxed::Box as BumpBox; use std::{ any::{Any, TypeId}, cell::{Cell, RefCell}, @@ -12,9 +13,6 @@ pub type TemplateId = &'static str; /// static parts of the template. #[derive(Debug, Clone)] pub struct VNode<'a> { - // The ID assigned for the root of this template - pub node_id: Cell, - /// The key given to the root of this template. /// /// In fragments, this is the key of the first child. In other cases, it is the key of the root. @@ -40,7 +38,6 @@ pub struct VNode<'a> { impl<'a> VNode<'a> { pub fn empty() -> Element<'a> { Ok(VNode { - node_id: Cell::new(ElementId(0)), key: None, parent: None, root_ids: &[], @@ -156,26 +153,28 @@ pub enum AttributeValue<'a> { Float(f32), Int(i32), Bool(bool), - Listener(RefCell<&'a mut dyn FnMut(UiEvent)>), - Any(&'a dyn AnyValue), + Listener(RefCell) + 'a>>>), + Any(BumpBox<'a, dyn AnyValue>), None, } impl<'a> AttributeValue<'a> { pub fn new_listener( cx: &'a ScopeState, - mut f: impl FnMut(UiEvent) + 'a, + mut callback: impl FnMut(UiEvent) + 'a, ) -> AttributeValue<'a> { - AttributeValue::Listener(RefCell::new(cx.bump().alloc( - move |event: UiEvent| { + let boxed: BumpBox<'a, dyn FnMut(_) + 'a> = unsafe { + BumpBox::from_raw(cx.bump().alloc(move |event: UiEvent| { if let Ok(data) = event.data.downcast::() { - f(UiEvent { + callback(UiEvent { bubbles: event.bubbles, data, }) } - }, - ))) + })) + }; + + AttributeValue::Listener(RefCell::new(Some(boxed))) } } @@ -201,7 +200,7 @@ impl<'a> PartialEq for AttributeValue<'a> { (Self::Int(l0), Self::Int(r0)) => l0 == r0, (Self::Bool(l0), Self::Bool(r0)) => l0 == r0, (Self::Listener(_), Self::Listener(_)) => true, - (Self::Any(l0), Self::Any(r0)) => l0.any_cmp(*r0), + (Self::Any(l0), Self::Any(r0)) => l0.any_cmp(r0.as_ref()), _ => core::mem::discriminant(self) == core::mem::discriminant(other), } } diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 9259a9f56..2419c52a0 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -7,6 +7,7 @@ use crate::{ scheduler::RcWake, scopes::{ScopeId, ScopeState}, virtual_dom::VirtualDom, + AttributeValue, DynamicNode, VFragment, VNode, }; use futures_util::FutureExt; use std::{ @@ -17,13 +18,13 @@ use std::{ }; impl VirtualDom { - pub(super) fn new_scope(&mut self, props: *const dyn AnyProps<'static>) -> &mut ScopeState { + pub(super) fn new_scope(&mut self, props: Box>) -> &mut ScopeState { let parent = self.acquire_current_scope_raw(); let entry = self.scopes.vacant_entry(); let height = unsafe { parent.map(|f| (*f).height + 1).unwrap_or(0) }; let id = ScopeId(entry.key()); - entry.insert(ScopeState { + entry.insert(Box::new(ScopeState { parent, id, height, @@ -38,19 +39,64 @@ impl VirtualDom { hook_idx: Default::default(), shared_contexts: Default::default(), tasks: self.scheduler.clone(), - }) + })) } fn acquire_current_scope_raw(&mut self) -> Option<*mut ScopeState> { self.scope_stack .last() .copied() - .and_then(|id| self.scopes.get_mut(id.0).map(|f| f as *mut ScopeState)) + .and_then(|id| self.scopes.get_mut(id.0).map(|f| f.as_mut() as *mut _)) + } + + pub fn ensure_drop_safety(&self, scope: ScopeId) { + let scope = &self.scopes[scope.0]; + let node = unsafe { scope.previous_frame().try_load_node() }; + + // And now we want to make sure the previous frame has dropped anything that borrows self + if let Some(RenderReturn::Sync(Ok(node))) = node { + self.ensure_drop_safety_inner(node); + } + } + + fn ensure_drop_safety_inner(&self, node: &VNode) { + for attr in node.dynamic_attrs { + if let AttributeValue::Listener(l) = &attr.value { + l.borrow_mut().take(); + } + } + + for child in node.dynamic_nodes { + match child { + DynamicNode::Component(c) => { + // Only descend if the props are borrowed + if !c.static_props { + self.ensure_drop_safety(c.scope.get().unwrap()); + c.props.set(None); + } + } + DynamicNode::Fragment(VFragment::NonEmpty(f)) => { + for node in *f { + self.ensure_drop_safety_inner(node); + } + } + _ => {} + } + } } pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn { + // Cycle to the next frame and then reset it + // This breaks any latent references, invalidating every pointer referencing into it. + // Remove all the outdated listeners + self.ensure_drop_safety(scope_id); + let mut new_nodes = unsafe { - let scope = &mut self.scopes[scope_id.0]; + let scope = self.scopes[scope_id.0].as_mut(); + + scope.previous_frame_mut().bump.reset(); + + // Make sure to reset the hook counter so we give out hooks in the right order scope.hook_idx.set(0); // safety: due to how we traverse the tree, we know that the scope is not currently aliased @@ -113,7 +159,7 @@ impl VirtualDom { let frame = scope.previous_frame(); // set the new head of the bump frame - let alloced = frame.bump.alloc(new_nodes); + let alloced = &*frame.bump.alloc(new_nodes); frame.node.set(alloced); // And move the render generation forward by one diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 0fdccd820..946ae90dc 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -82,12 +82,12 @@ pub struct ScopeState { pub(crate) tasks: Rc, pub(crate) spawned_tasks: HashSet, - pub(crate) props: *const dyn AnyProps<'static>, + pub(crate) props: Box>, pub(crate) placeholder: Cell>, } impl ScopeState { - pub fn current_frame(&self) -> &BumpFrame { + pub(crate) fn current_frame(&self) -> &BumpFrame { match self.render_cnt.get() % 2 { 0 => &self.node_arena_1, 1 => &self.node_arena_2, @@ -95,7 +95,7 @@ impl ScopeState { } } - pub fn previous_frame(&self) -> &BumpFrame { + pub(crate) fn previous_frame(&self) -> &BumpFrame { match self.render_cnt.get() % 2 { 1 => &self.node_arena_1, 0 => &self.node_arena_2, @@ -103,6 +103,14 @@ impl ScopeState { } } + pub(crate) fn previous_frame_mut(&mut self) -> &mut BumpFrame { + match self.render_cnt.get() % 2 { + 1 => &mut self.node_arena_1, + 0 => &mut self.node_arena_2, + _ => unreachable!(), + } + } + /// Get the current render since the inception of this component /// /// This can be used as a helpful diagnostic when debugging hooks/renders, etc @@ -117,7 +125,8 @@ impl ScopeState { /// /// If you need to allocate items that need to be dropped, use bumpalo's box. pub fn bump(&self) -> &Bump { - &self.current_frame().bump + // note that this is actually the previous frame since we use that as scratch space while the component is rendering + &self.previous_frame().bump } /// Get a handle to the currently active head node arena for this Scope diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 3faf732d9..374559e8f 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -6,12 +6,12 @@ use crate::{ any_props::VProps, arena::{ElementId, ElementRef}, factory::RenderReturn, - innerlude::{DirtyScope, ElementPath, Mutations, Scheduler, SchedulerMsg}, + innerlude::{DirtyScope, Mutations, Scheduler, SchedulerMsg, ErrorBoundary}, mutations::Mutation, nodes::{Template, TemplateId}, scheduler::{SuspenseBoundary, SuspenseId}, scopes::{ScopeId, ScopeState}, - AttributeValue, Element, EventPriority, Scope, SuspenseContext, UiEvent, + AttributeValue, Element, Scope, SuspenseContext, UiEvent, }; use futures_util::{pin_mut, StreamExt}; use slab::Slab; @@ -149,7 +149,7 @@ use std::{ /// ``` pub struct VirtualDom { pub(crate) templates: HashMap>, - pub(crate) scopes: Slab, + pub(crate) scopes: Slab>, pub(crate) dirty_scopes: BTreeSet, pub(crate) scheduler: Rc, @@ -224,10 +224,7 @@ impl VirtualDom { /// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" }); /// let mutations = dom.rebuild(); /// ``` - pub fn new_with_props

(root: fn(Scope

) -> Element, root_props: P) -> Self - where - P: 'static, - { + pub fn new_with_props(root: fn(Scope

) -> Element, root_props: P) -> Self { let (tx, rx) = futures_channel::mpsc::unbounded(); let mut dom = Self { rx, @@ -242,11 +239,11 @@ impl VirtualDom { mutations: Mutations::new(), }; - let root = dom.new_scope(Box::leak(Box::new(VProps::new( + let root = dom.new_scope(Box::new(VProps::new( root, |_, _| unreachable!(), root_props, - )))); + ))); // The root component is always a suspense boundary for any async children // This could be unexpected, so we might rethink this behavior later @@ -254,6 +251,9 @@ impl VirtualDom { // We *could* just panic if the suspense boundary is not found root.provide_context(Rc::new(SuspenseBoundary::new(ScopeId(0)))); + // Unlike react, we provide a default error boundary that just renders the error as a string + root.provide_context(Rc::new(ErrorBoundary::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()); @@ -264,7 +264,7 @@ impl VirtualDom { /// /// This is useful for inserting or removing contexts from a scope, or rendering out its root node pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> { - self.scopes.get(id.0) + self.scopes.get(id.0).map(|f| f.as_ref()) } /// Get the single scope at the top of the VirtualDom tree that will always be around @@ -285,7 +285,7 @@ impl VirtualDom { /// Manually mark a scope as requiring a re-render /// /// Whenever the VirtualDom "works", it will re-render this scope - pub fn mark_dirty_scope(&mut self, id: ScopeId) { + pub fn mark_dirty(&mut self, id: ScopeId) { let height = self.scopes[id.0].height; self.dirty_scopes.insert(DirtyScope { height, id }); } @@ -329,7 +329,6 @@ impl VirtualDom { data: Rc, element: ElementId, bubbles: bool, - _priority: EventPriority, ) { /* ------------------------ @@ -393,7 +392,10 @@ impl VirtualDom { // We check the bubble state between each call to see if the event has been stopped from bubbling for listener in listeners.drain(..).rev() { if let AttributeValue::Listener(listener) = listener { - listener.borrow_mut()(uievent.clone()); + if let Some(cb) = listener.borrow_mut().as_deref_mut() { + cb(uievent.clone()); + } + if !uievent.bubbles.get() { return; } @@ -427,7 +429,7 @@ impl VirtualDom { match some_msg.take() { // If a bunch of messages are ready in a sequence, try to pop them off synchronously Some(msg) => match msg { - SchedulerMsg::Immediate(id) => self.mark_dirty_scope(id), + SchedulerMsg::Immediate(id) => self.mark_dirty(id), SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task), SchedulerMsg::SuspenseNotified(id) => self.handle_suspense_wakeup(id), }, @@ -479,7 +481,6 @@ impl VirtualDom { /// /// apply_edits(edits); /// ``` - #[must_use] pub fn rebuild<'a>(&'a mut self) -> Mutations<'a> { match unsafe { self.run_scope(ScopeId(0)).extend_lifetime_ref() } { // Rebuilding implies we append the created elements to the root @@ -497,7 +498,6 @@ impl VirtualDom { /// Render whatever the VirtualDom has ready as fast as possible without requiring an executor to progress /// suspended subtrees. - #[must_use] pub fn render_immediate(&mut self) -> Mutations { // Build a waker that won't wake up since our deadline is already expired when it's polled let waker = futures_util::task::noop_waker(); @@ -612,6 +612,6 @@ impl VirtualDom { impl Drop for VirtualDom { fn drop(&mut self) { - // self.drop_scope(ScopeId(0)); + self.drop_scope(ScopeId(0)); } } diff --git a/packages/core/tests/attr_cleanup.rs b/packages/core/tests/attr_cleanup.rs index 2f70ca527..99ce9a01b 100644 --- a/packages/core/tests/attr_cleanup.rs +++ b/packages/core/tests/attr_cleanup.rs @@ -30,7 +30,7 @@ fn attrs_cycle() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -42,7 +42,7 @@ fn attrs_cycle() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -51,7 +51,7 @@ fn attrs_cycle() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -64,7 +64,7 @@ fn attrs_cycle() { ); // we take the node taken by attributes since we reused it - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ diff --git a/packages/core/tests/bubble_error.rs b/packages/core/tests/bubble_error.rs index 310f5118d..6dbd0f3f7 100644 --- a/packages/core/tests/bubble_error.rs +++ b/packages/core/tests/bubble_error.rs @@ -22,9 +22,7 @@ fn it_goes() { let edits = dom.rebuild().santize(); - dbg!(edits); - - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); dom.render_immediate(); } diff --git a/packages/core/tests/context_api.rs b/packages/core/tests/context_api.rs index 71069d2fc..2ec1ea18b 100644 --- a/packages/core/tests/context_api.rs +++ b/packages/core/tests/context_api.rs @@ -27,22 +27,22 @@ fn state_shares() { ] ); - dom.mark_dirty_scope(ScopeId(0)); - dom.render_immediate(); + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); assert_eq!(dom.base_scope().consume_context::().unwrap(), 1); - dom.mark_dirty_scope(ScopeId(0)); - dom.render_immediate(); + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); assert_eq!(dom.base_scope().consume_context::().unwrap(), 2); - dom.mark_dirty_scope(ScopeId(2)); + dom.mark_dirty(ScopeId(2)); assert_eq!( dom.render_immediate().santize().edits, [SetText { value: "Value is 2", id: ElementId(1,) },] ); - dom.mark_dirty_scope(ScopeId(0)); - dom.mark_dirty_scope(ScopeId(2)); + dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId(2)); let edits = dom.render_immediate(); assert_eq!( edits.santize().edits, diff --git a/packages/core/tests/create_lists.rs b/packages/core/tests/create_lists.rs index 30d6abc2f..27540d9f0 100644 --- a/packages/core/tests/create_lists.rs +++ b/packages/core/tests/create_lists.rs @@ -43,7 +43,7 @@ fn list_renders() { CreateStaticText { value: "hello world! " }, AppendChildren { m: 1 }, CreateElement { name: "p" }, - CreateStaticText { value: "d" }, + CreateTextPlaceholder, AppendChildren { m: 1 }, AppendChildren { m: 2 }, SaveTemplate { name: "template", m: 1 } diff --git a/packages/core/tests/cycle.rs b/packages/core/tests/cycle.rs index 803edede7..72e1a028d 100644 --- a/packages/core/tests/cycle.rs +++ b/packages/core/tests/cycle.rs @@ -21,7 +21,7 @@ fn cycling_elements() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -31,7 +31,7 @@ fn cycling_elements() { ); // notice that the IDs cycle back to ElementId(1), preserving a minimal memory footprint - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -40,7 +40,7 @@ fn cycling_elements() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ diff --git a/packages/core/tests/diff_component.rs b/packages/core/tests/diff_component.rs new file mode 100644 index 000000000..6e72f04cd --- /dev/null +++ b/packages/core/tests/diff_component.rs @@ -0,0 +1,102 @@ +use dioxus::core::{ElementId, Mutation::*}; +use dioxus::prelude::*; + +/// When returning sets of components, we do a light diff of the contents to preserve some react-like functionality +/// +/// This means that nav_bar should never get re-created and that we should only be swapping out +/// different pointers +#[test] +fn component_swap() { + fn app(cx: Scope) -> Element { + let render_phase = cx.use_hook(|| 0); + + *render_phase += 1; + + cx.render(match *render_phase { + 0 => rsx! { + nav_bar {} + dash_board {} + }, + 1 => rsx! { + nav_bar {} + dash_results {} + }, + 2 => rsx! { + nav_bar {} + dash_board {} + }, + 3 => rsx! { + nav_bar {} + dash_results {} + }, + 4 => rsx! { + nav_bar {} + dash_board {} + }, + _ => rsx!("blah"), + }) + } + + fn nav_bar(cx: Scope) -> Element { + cx.render(rsx! { + h1 { + "NavBar" + (0..3).map(|_| rsx!(nav_link {})) + } + }) + } + + fn nav_link(cx: Scope) -> Element { + cx.render(rsx!( h1 { "nav_link" } )) + } + + fn dash_board(cx: Scope) -> Element { + cx.render(rsx!( div { "dashboard" } )) + } + + fn dash_results(cx: Scope) -> Element { + cx.render(rsx!( div { "results" } )) + } + + let mut dom = VirtualDom::new(app); + let edits = dom.rebuild().santize(); + assert_eq!( + edits.edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1) }, + LoadTemplate { name: "template", index: 0, id: ElementId(2) }, + LoadTemplate { name: "template", index: 0, id: ElementId(3) }, + LoadTemplate { name: "template", index: 0, id: ElementId(4) }, + ReplacePlaceholder { path: &[1], m: 3 }, + LoadTemplate { name: "template", index: 0, id: ElementId(5) }, + AppendChildren { m: 2 } + ] + ); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(6) }, + ReplaceWith { id: ElementId(5), m: 1 } + ] + ); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(5) }, + ReplaceWith { id: ElementId(6), m: 1 } + ] + ); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(6) }, + ReplaceWith { id: ElementId(5), m: 1 } + ] + ); +} diff --git a/packages/core/tests/diff_element.rs b/packages/core/tests/diff_element.rs index 5dff0fbc8..3ab9b284a 100644 --- a/packages/core/tests/diff_element.rs +++ b/packages/core/tests/diff_element.rs @@ -10,21 +10,21 @@ fn text_diff() { } let mut vdom = VirtualDom::new(app); - vdom.rebuild(); + _ = vdom.rebuild(); - vdom.mark_dirty_scope(ScopeId(0)); + vdom.mark_dirty(ScopeId(0)); assert_eq!( vdom.render_immediate().edits, [SetText { value: "hello 1", id: ElementId(2) }] ); - vdom.mark_dirty_scope(ScopeId(0)); + vdom.mark_dirty(ScopeId(0)); assert_eq!( vdom.render_immediate().edits, [SetText { value: "hello 2", id: ElementId(2) }] ); - vdom.mark_dirty_scope(ScopeId(0)); + vdom.mark_dirty(ScopeId(0)); assert_eq!( vdom.render_immediate().edits, [SetText { value: "hello 3", id: ElementId(2) }] @@ -44,9 +44,9 @@ fn element_swap() { } let mut vdom = VirtualDom::new(app); - vdom.rebuild(); + _ = vdom.rebuild(); - vdom.mark_dirty_scope(ScopeId(0)); + vdom.mark_dirty(ScopeId(0)); assert_eq!( vdom.render_immediate().santize().edits, [ @@ -55,7 +55,7 @@ fn element_swap() { ] ); - vdom.mark_dirty_scope(ScopeId(0)); + vdom.mark_dirty(ScopeId(0)); assert_eq!( vdom.render_immediate().santize().edits, [ @@ -64,7 +64,7 @@ fn element_swap() { ] ); - vdom.mark_dirty_scope(ScopeId(0)); + vdom.mark_dirty(ScopeId(0)); assert_eq!( vdom.render_immediate().santize().edits, [ @@ -73,7 +73,7 @@ fn element_swap() { ] ); - vdom.mark_dirty_scope(ScopeId(0)); + vdom.mark_dirty(ScopeId(0)); assert_eq!( vdom.render_immediate().santize().edits, [ diff --git a/packages/core/tests/diff_keyed_list.rs b/packages/core/tests/diff_keyed_list.rs index 521d9385a..6acbaebbb 100644 --- a/packages/core/tests/diff_keyed_list.rs +++ b/packages/core/tests/diff_keyed_list.rs @@ -1,5 +1,3 @@ -#![allow(unused, non_upper_case_globals)] - //! Diffing Tests //! //! These tests only verify that the diffing algorithm works properly for single components. @@ -39,7 +37,7 @@ fn keyed_diffing_out_of_order() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().edits, [ @@ -64,7 +62,7 @@ fn keyed_diffing_out_of_order_adds() { _ = dom.rebuild(); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().edits, [ @@ -90,7 +88,7 @@ fn keyed_diffing_out_of_order_adds_3() { _ = dom.rebuild(); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().edits, [ @@ -116,7 +114,7 @@ fn keyed_diffing_out_of_order_adds_4() { _ = dom.rebuild(); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().edits, [ @@ -142,7 +140,7 @@ fn keyed_diffing_out_of_order_adds_5() { _ = dom.rebuild(); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().edits, [ @@ -167,7 +165,7 @@ fn keyed_diffing_additions() { _ = dom.rebuild(); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -192,7 +190,7 @@ fn keyed_diffing_additions_and_moves_on_ends() { _ = dom.rebuild(); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -222,7 +220,7 @@ fn keyed_diffing_additions_and_moves_in_middle() { _ = dom.rebuild(); // LIS: 4, 5, 6 - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -256,7 +254,7 @@ fn controlled_keyed_diffing_out_of_order() { _ = dom.rebuild(); // LIS: 5, 6 - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -289,7 +287,7 @@ fn controlled_keyed_diffing_out_of_order_max_test() { _ = dom.rebuild(); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -318,7 +316,7 @@ fn remove_list() { _ = dom.rebuild(); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -328,3 +326,31 @@ fn remove_list() { ] ); } + +#[test] +fn no_common_keys() { + let mut dom = VirtualDom::new(|cx| { + let order: &[_] = match cx.generation() % 2 { + 0 => &[1, 2, 3], + 1 => &[4, 5, 6], + _ => unreachable!(), + }; + + cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) + }); + + _ = dom.rebuild(); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + Remove { id: ElementId(2) }, + Remove { id: ElementId(3) }, + LoadTemplate { name: "template", index: 0, id: ElementId(3) }, + LoadTemplate { name: "template", index: 0, id: ElementId(2) }, + LoadTemplate { name: "template", index: 0, id: ElementId(4) }, + ReplaceWith { id: ElementId(1), m: 3 } + ] + ); +} diff --git a/packages/core/tests/diff_unkeyed_list.rs b/packages/core/tests/diff_unkeyed_list.rs index 96de44cc3..5252d1ece 100644 --- a/packages/core/tests/diff_unkeyed_list.rs +++ b/packages/core/tests/diff_unkeyed_list.rs @@ -26,7 +26,7 @@ fn list_creates_one_by_one() { ); // Rendering the first item should replace the placeholder with an element - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -37,7 +37,7 @@ fn list_creates_one_by_one() { ); // Rendering the next item should insert after the previous - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -48,7 +48,7 @@ fn list_creates_one_by_one() { ); // ... and again! - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -59,7 +59,7 @@ fn list_creates_one_by_one() { ); // once more - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -106,14 +106,14 @@ fn removes_one_by_one() { // Remove div(3) // Rendering the first item should replace the placeholder with an element - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [Remove { id: ElementId(6) }] ); // Remove div(2) - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [Remove { id: ElementId(4) }] @@ -121,7 +121,7 @@ fn removes_one_by_one() { // Remove div(1) and replace with a placeholder // todo: this should just be a remove with no placeholder - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -132,7 +132,7 @@ fn removes_one_by_one() { // load the 3 and replace the placeholder // todo: this should actually be append to, but replace placeholder is fine for now - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -169,7 +169,7 @@ fn list_shrink_multiroot() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -181,7 +181,7 @@ fn list_shrink_multiroot() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -193,7 +193,7 @@ fn list_shrink_multiroot() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -248,19 +248,19 @@ fn removes_one_by_one_multiroot() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [Remove { id: ElementId(10) }, Remove { id: ElementId(12) }] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [Remove { id: ElementId(6) }, Remove { id: ElementId(8) }] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -324,7 +324,7 @@ fn remove_many() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); let edits = dom.render_immediate().santize(); assert_eq!( edits.edits, @@ -335,7 +335,7 @@ fn remove_many() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); let edits = dom.render_immediate().santize(); assert_eq!( edits.edits, @@ -352,7 +352,7 @@ fn remove_many() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); let edits = dom.render_immediate().santize(); assert_eq!( edits.edits, @@ -366,7 +366,7 @@ fn remove_many() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); let edits = dom.render_immediate().santize(); assert_eq!( edits.edits, diff --git a/packages/core/tests/hotreloading.rs b/packages/core/tests/hotreloading.rs new file mode 100644 index 000000000..8e9f3400b --- /dev/null +++ b/packages/core/tests/hotreloading.rs @@ -0,0 +1 @@ +//! It should be possible to swap out templates at runtime, enabling hotreloading diff --git a/packages/core/tests/lifecycle.rs b/packages/core/tests/lifecycle.rs index 06baf32a0..ab7be8793 100644 --- a/packages/core/tests/lifecycle.rs +++ b/packages/core/tests/lifecycle.rs @@ -44,10 +44,7 @@ fn events_generate() { match *count { 0 => cx.render(rsx! { - div { - onclick: move |_| { - *count += 1 - }, + div { onclick: move |_| *count += 1, div { "nested" } "Click me!" } @@ -59,15 +56,9 @@ fn events_generate() { let mut dom = VirtualDom::new(app); _ = dom.rebuild(); - dom.handle_event( - "click", - Rc::new(MouseData::default()), - ElementId(1), - true, - EventPriority::Immediate, - ); + dom.handle_event("click", Rc::new(MouseData::default()), ElementId(1), true); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); let edits = dom.render_immediate(); assert_eq!( @@ -175,95 +166,3 @@ fn events_generate() { // ] // ); // } - -// #[test] -// fn component_swap() { -// fn app(cx: Scope) -> Element { -// let render_phase = cx.use_hook(|| 0); -// *render_phase += 1; - -// cx.render(match *render_phase { -// 0 => rsx_without_templates!( -// div { -// NavBar {} -// Dashboard {} -// } -// ), -// 1 => rsx_without_templates!( -// div { -// NavBar {} -// Results {} -// } -// ), -// 2 => rsx_without_templates!( -// div { -// NavBar {} -// Dashboard {} -// } -// ), -// 3 => rsx_without_templates!( -// div { -// NavBar {} -// Results {} -// } -// ), -// 4 => rsx_without_templates!( -// div { -// NavBar {} -// Dashboard {} -// } -// ), -// _ => rsx_without_templates!("blah"), -// }) -// }; - -// static NavBar: Component = |cx| { -// println!("running navbar"); -// cx.render(rsx_without_templates! { -// h1 { -// "NavBar" -// {(0..3).map(|f| rsx_without_templates!(NavLink {}))} -// } -// }) -// }; - -// static NavLink: Component = |cx| { -// println!("running navlink"); -// cx.render(rsx_without_templates! { -// h1 { -// "NavLink" -// } -// }) -// }; - -// static Dashboard: Component = |cx| { -// println!("running dashboard"); -// cx.render(rsx_without_templates! { -// div { -// "dashboard" -// } -// }) -// }; - -// static Results: Component = |cx| { -// println!("running results"); -// cx.render(rsx_without_templates! { -// div { -// "results" -// } -// }) -// }; - -// let mut dom = VirtualDom::new(app); -// let edits = dom.rebuild(); -// dbg!(&edits); - -// let edits = dom.work_with_deadline(|| false); -// dbg!(&edits); -// let edits = dom.work_with_deadline(|| false); -// dbg!(&edits); -// let edits = dom.work_with_deadline(|| false); -// dbg!(&edits); -// let edits = dom.work_with_deadline(|| false); -// dbg!(&edits); -// } diff --git a/packages/core/tests/miri_simple.rs b/packages/core/tests/miri_simple.rs new file mode 100644 index 000000000..19d3a44a0 --- /dev/null +++ b/packages/core/tests/miri_simple.rs @@ -0,0 +1,127 @@ +use dioxus::prelude::*; + +#[test] +fn app_drops() { + fn app(cx: Scope) -> Element { + cx.render(rsx! { + div {} + }) + } + + let mut dom = VirtualDom::new(app); + + _ = dom.rebuild(); + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); +} + +#[test] +fn hooks_drop() { + fn app(cx: Scope) -> Element { + cx.use_hook(|| String::from("asd")); + cx.use_hook(|| String::from("asd")); + cx.use_hook(|| String::from("asd")); + cx.use_hook(|| String::from("asd")); + + cx.render(rsx! { + div {} + }) + } + + let mut dom = VirtualDom::new(app); + + _ = dom.rebuild(); + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); +} + +#[test] +fn contexts_drop() { + fn app(cx: Scope) -> Element { + cx.provide_context(String::from("asd")); + + cx.render(rsx! { + div { + child_comp {} + } + }) + } + + fn child_comp(cx: Scope) -> Element { + let el = cx.consume_context::().unwrap(); + + cx.render(rsx! { + div { "hello {el}" } + }) + } + + let mut dom = VirtualDom::new(app); + + _ = dom.rebuild(); + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); +} + +#[test] +fn tasks_drop() { + fn app(cx: Scope) -> Element { + cx.spawn(async { + tokio::time::sleep(std::time::Duration::from_millis(100000)).await; + }); + + cx.render(rsx! { + div { } + }) + } + + let mut dom = VirtualDom::new(app); + + _ = dom.rebuild(); + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); +} + +#[test] +fn root_props_drop() { + struct RootProps(String); + + let mut dom = VirtualDom::new_with_props( + |cx| cx.render(rsx!( div { "{cx.props.0}" } )), + RootProps("asdasd".to_string()), + ); + + _ = dom.rebuild(); + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); +} + +#[test] +fn diffing_drops_old() { + fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + match cx.generation() % 2 { + 0 => rsx!( child_comp1 { name: "asdasd".to_string() }), + 1 => rsx!( child_comp2 { name: "asdasd".to_string() }), + _ => todo!() + } + } + }) + } + + #[inline_props] + fn child_comp1(cx: Scope, name: String) -> Element { + cx.render(rsx! { "Hello {name}" }) + } + + #[inline_props] + fn child_comp2(cx: Scope, name: String) -> Element { + cx.render(rsx! { "Goodbye {name}" }) + } + + let mut dom = VirtualDom::new(app); + _ = dom.rebuild(); + dom.mark_dirty(ScopeId(0)); + + _ = dom.render_immediate(); +} diff --git a/packages/core/tests/miri_stress.rs b/packages/core/tests/miri_stress.rs index 20a29b30c..3ebd5c504 100644 --- a/packages/core/tests/miri_stress.rs +++ b/packages/core/tests/miri_stress.rs @@ -1,32 +1,28 @@ #![allow(non_snake_case)] -/* -Stress Miri as much as possible. -Prove that we don't leak memory and that our methods are safe. - -Specifically: -- [ ] VirtualDom drops memory safely -- [ ] Borrowed components don't expose invalid pointers -- [ ] Async isn't busted -*/ +use std::rc::Rc; use dioxus::prelude::*; -/// This test ensures that if a component aborts early, it is replaced with a placeholder. -/// In debug, this should also toss a warning. +/// This test checks that we should release all memory used by the virtualdom when it exits. +/// +/// When miri runs, it'll let us know if we leaked or aliased. #[test] -#[ignore] fn test_memory_leak() { fn app(cx: Scope) -> Element { - let val = cx.use_hook(|| 0); + let val = cx.generation(); - *val += 1; + cx.spawn(async { + tokio::time::sleep(std::time::Duration::from_millis(100000)).await; + }); - if *val == 2 || *val == 4 { + if val == 2 || val == 4 { return cx.render(rsx!(())); } - let name = cx.use_hook(|| String::from("asd")); + let name = cx.use_hook(|| String::from("numbers: ")); + + name.push_str("123 "); cx.render(rsx!( div { "Hello, world!" } @@ -36,24 +32,26 @@ fn test_memory_leak() { Child {} Child {} Child {} - BorrowedChild { na: name } - BorrowedChild { na: name } - BorrowedChild { na: name } - BorrowedChild { na: name } - BorrowedChild { na: name } + BorrowedChild { name: name } + BorrowedChild { name: name } + BorrowedChild { name: name } + BorrowedChild { name: name } + BorrowedChild { name: name } )) } #[derive(Props)] struct BorrowedProps<'a> { - na: &'a str, + name: &'a str, } fn BorrowedChild<'a>(cx: Scope<'a, BorrowedProps<'a>>) -> Element { - render!(div { - "goodbye {cx.props.na}" - Child {} - Child {} + cx.render(rsx! { + div { + "goodbye {cx.props.name}" + Child {} + Child {} + } }) } @@ -63,24 +61,21 @@ fn test_memory_leak() { let mut dom = VirtualDom::new(app); - dom.rebuild(); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); + _ = dom.rebuild(); + + for _ in 0..5 { + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); + } } #[test] fn memo_works_properly() { fn app(cx: Scope) -> Element { - let val = cx.use_hook(|| 0); + let val = cx.generation(); - *val += 1; - - if *val == 2 || *val == 4 { - return None; + if val == 2 || val == 4 { + return cx.render(rsx!(())); } let name = cx.use_hook(|| String::from("asd")); @@ -100,309 +95,257 @@ fn memo_works_properly() { render!(div { "goodbye world" }) } - let mut dom = new_dom(app, ()); + let mut dom = VirtualDom::new(app); dom.rebuild(); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); -} - -#[test] -fn free_works_on_root_props() { - fn app(cx: Scope) -> Element { - cx.render(rsx! { - Child { a: "alpha"} - Child { a: "beta"} - Child { a: "gamma"} - Child { a: "delta"} - }) - } - - #[derive(Props, PartialEq)] - struct ChildProps { - a: &'static str, - } - - fn Child(cx: Scope) -> Element { - render!("child {cx.props.a}") - } - - struct Custom { - val: String, - } - - impl Drop for Custom { - fn drop(&mut self) { - dbg!("dropped! {}", &self.val); - } - } - - let mut dom = new_dom(app, Custom { val: String::from("asd") }); - dom.rebuild(); -} - -#[test] -fn free_works_on_borrowed() { - fn app(cx: Scope) -> Element { - cx.render(rsx! { - Child { a: "alpha", b: "asd".to_string() } - }) - } - #[derive(Props)] - struct ChildProps<'a> { - a: &'a str, - b: String, - } - - fn Child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element { - dbg!("rendering child"); - render!("child {cx.props.a}, {cx.props.b}") - } - - impl Drop for ChildProps<'_> { - fn drop(&mut self) { - dbg!("dropped child!"); - } - } - - let mut dom = new_dom(app, ()); - let _ = dom.rebuild(); + todo!() + // dom.hard_diff(ScopeId(0)); + // dom.hard_diff(ScopeId(0)); + // dom.hard_diff(ScopeId(0)); + // dom.hard_diff(ScopeId(0)); + // dom.hard_diff(ScopeId(0)); + // dom.hard_diff(ScopeId(0)); + // dom.hard_diff(ScopeId(0)); } #[test] fn free_works_on_root_hooks() { /* - On Drop, scopearena drops all the hook contents. + On Drop, scopearena drops all the hook contents. and props */ - - struct Droppable(T); - impl Drop for Droppable { - fn drop(&mut self) { - dbg!("dropping droppable"); - } + #[derive(PartialEq, Clone, Props)] + struct AppProps { + inner: Rc, } - fn app(cx: Scope) -> Element { - let name = cx.use_hook(|| Droppable(String::from("asd"))); - render!(div { "{name.0}" }) + fn app(cx: Scope) -> Element { + let name: &AppProps = cx.use_hook(|| cx.props.clone()); + render!(child_component { inner: name.inner.clone() }) } - let mut dom = new_dom(app, ()); - let _ = dom.rebuild(); -} - -#[test] -fn old_props_arent_stale() { - fn app(cx: Scope) -> Element { - dbg!("rendering parent"); - let cnt = cx.use_hook(|| 0); - *cnt += 1; - - if *cnt == 1 { - render!(div { Child { a: "abcdef".to_string() } }) - } else { - render!(div { Child { a: "abcdef".to_string() } }) - } + fn child_component(cx: Scope) -> Element { + render!(div { "{cx.props.inner}" }) } - #[derive(Props, PartialEq)] - struct ChildProps { - a: String, - } - fn Child(cx: Scope) -> Element { - dbg!("rendering child", &cx.props.a); - render!(div { "child {cx.props.a}" }) - } - - let mut dom = new_dom(app, ()); + let ptr = Rc::new("asdasd".to_string()); + let mut dom = VirtualDom::new_with_props(app, AppProps { inner: ptr.clone() }); let _ = dom.rebuild(); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); + // ptr gets cloned into props and then into the hook + assert_eq!(Rc::strong_count(&ptr), 4); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); + drop(dom); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); - - dbg!("forcing update to child"); - - dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); - dom.work_with_deadline(|| false); - - dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); - dom.work_with_deadline(|| false); - - dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); - dom.work_with_deadline(|| false); + assert_eq!(Rc::strong_count(&ptr), 1); } -#[test] -fn basic() { - fn app(cx: Scope) -> Element { - render!(div { - Child { a: "abcdef".to_string() } - }) - } +// #[test] +// fn old_props_arent_stale() { +// fn app(cx: Scope) -> Element { +// dbg!("rendering parent"); +// let cnt = cx.use_hook(|| 0); +// *cnt += 1; - #[derive(Props, PartialEq)] - struct ChildProps { - a: String, - } +// if *cnt == 1 { +// render!(div { Child { a: "abcdef".to_string() } }) +// } else { +// render!(div { Child { a: "abcdef".to_string() } }) +// } +// } - fn Child(cx: Scope) -> Element { - dbg!("rendering child", &cx.props.a); - render!(div { "child {cx.props.a}" }) - } +// #[derive(Props, PartialEq)] +// struct ChildProps { +// a: String, +// } +// fn Child(cx: Scope) -> Element { +// dbg!("rendering child", &cx.props.a); +// render!(div { "child {cx.props.a}" }) +// } - let mut dom = new_dom(app, ()); - let _ = dom.rebuild(); +// let mut dom = new_dom(app, ()); +// let _ = dom.rebuild(); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); +// dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); -} +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); +// dom.work_with_deadline(|| false); -#[test] -fn leak_thru_children() { - fn app(cx: Scope) -> Element { - cx.render(rsx! { - Child { - name: "asd".to_string(), - } - }); - cx.render(rsx! { - div {} - }) - } +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); +// dom.work_with_deadline(|| false); - #[inline_props] - fn Child(cx: Scope, name: String) -> Element { - render!(div { "child {name}" }) - } +// dbg!("forcing update to child"); - let mut dom = new_dom(app, ()); - let _ = dom.rebuild(); +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); +// dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); +// dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); -} +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); +// dom.work_with_deadline(|| false); +// } -#[test] -fn test_pass_thru() { - #[inline_props] - fn NavContainer<'a>(cx: Scope, children: Element<'a>) -> Element { - cx.render(rsx! { - header { - nav { children } - } - }) - } +// #[test] +// fn basic() { +// fn app(cx: Scope) -> Element { +// render!(div { +// Child { a: "abcdef".to_string() } +// }) +// } - fn NavMenu(cx: Scope) -> Element { - render!( NavBrand {} - div { - NavStart {} - NavEnd {} - } - ) - } +// #[derive(Props, PartialEq)] +// struct ChildProps { +// a: String, +// } - fn NavBrand(cx: Scope) -> Element { - render!(div {}) - } +// fn Child(cx: Scope) -> Element { +// dbg!("rendering child", &cx.props.a); +// render!(div { "child {cx.props.a}" }) +// } - fn NavStart(cx: Scope) -> Element { - render!(div {}) - } +// let mut dom = new_dom(app, ()); +// let _ = dom.rebuild(); - fn NavEnd(cx: Scope) -> Element { - render!(div {}) - } +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); +// dom.work_with_deadline(|| false); - #[inline_props] - fn MainContainer<'a>( - cx: Scope, - nav: Element<'a>, - body: Element<'a>, - footer: Element<'a>, - ) -> Element { - cx.render(rsx! { - div { - class: "columns is-mobile", - div { - class: "column is-full", - nav, - body, - footer, - } - } - }) - } +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); +// dom.work_with_deadline(|| false); +// } - fn app(cx: Scope) -> Element { - let nav = cx.render(rsx! { - NavContainer { - NavMenu {} - } - }); - let body = cx.render(rsx! { - div {} - }); - let footer = cx.render(rsx! { - div {} - }); +// #[test] +// fn leak_thru_children() { +// fn app(cx: Scope) -> Element { +// cx.render(rsx! { +// Child { +// name: "asd".to_string(), +// } +// }); +// cx.render(rsx! { +// div {} +// }) +// } - cx.render(rsx! { - MainContainer { - nav: nav, - body: body, - footer: footer, - } - }) - } +// #[inline_props] +// fn Child(cx: Scope, name: String) -> Element { +// render!(div { "child {name}" }) +// } - let mut dom = new_dom(app, ()); - let _ = dom.rebuild(); +// let mut dom = new_dom(app, ()); +// let _ = dom.rebuild(); - for _ in 0..40 { - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); +// dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); - dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); - dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); - dom.work_with_deadline(|| false); +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); +// dom.work_with_deadline(|| false); +// } - dom.handle_message(SchedulerMsg::Immediate(ScopeId(2))); - dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(2))); - dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(2))); - dom.work_with_deadline(|| false); +// #[test] +// fn test_pass_thru() { +// #[inline_props] +// fn NavContainer<'a>(cx: Scope, children: Element<'a>) -> Element { +// cx.render(rsx! { +// header { +// nav { children } +// } +// }) +// } - dom.handle_message(SchedulerMsg::Immediate(ScopeId(3))); - dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(3))); - dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(3))); - dom.work_with_deadline(|| false); - } -} +// fn NavMenu(cx: Scope) -> Element { +// render!( NavBrand {} +// div { +// NavStart {} +// NavEnd {} +// } +// ) +// } + +// fn NavBrand(cx: Scope) -> Element { +// render!(div {}) +// } + +// fn NavStart(cx: Scope) -> Element { +// render!(div {}) +// } + +// fn NavEnd(cx: Scope) -> Element { +// render!(div {}) +// } + +// #[inline_props] +// fn MainContainer<'a>( +// cx: Scope, +// nav: Element<'a>, +// body: Element<'a>, +// footer: Element<'a>, +// ) -> Element { +// cx.render(rsx! { +// div { +// class: "columns is-mobile", +// div { +// class: "column is-full", +// nav, +// body, +// footer, +// } +// } +// }) +// } + +// fn app(cx: Scope) -> Element { +// let nav = cx.render(rsx! { +// NavContainer { +// NavMenu {} +// } +// }); +// let body = cx.render(rsx! { +// div {} +// }); +// let footer = cx.render(rsx! { +// div {} +// }); + +// cx.render(rsx! { +// MainContainer { +// nav: nav, +// body: body, +// footer: footer, +// } +// }) +// } + +// let mut dom = new_dom(app, ()); +// let _ = dom.rebuild(); + +// for _ in 0..40 { +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); +// dom.work_with_deadline(|| false); +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); +// dom.work_with_deadline(|| false); +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); +// dom.work_with_deadline(|| false); + +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); +// dom.work_with_deadline(|| false); +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); +// dom.work_with_deadline(|| false); +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); +// dom.work_with_deadline(|| false); + +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(2))); +// dom.work_with_deadline(|| false); +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(2))); +// dom.work_with_deadline(|| false); +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(2))); +// dom.work_with_deadline(|| false); + +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(3))); +// dom.work_with_deadline(|| false); +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(3))); +// dom.work_with_deadline(|| false); +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(3))); +// dom.work_with_deadline(|| false); +// } +// } diff --git a/packages/core/tests/safety.rs b/packages/core/tests/safety.rs index 318e1088d..564f21eb6 100644 --- a/packages/core/tests/safety.rs +++ b/packages/core/tests/safety.rs @@ -13,10 +13,6 @@ fn root_node_isnt_null() { // We haven't built the tree, so trying to get out the root node should fail assert!(scope.try_root_node().is_none()); - // There should be no way to gain an invalid pointer - assert!(scope.current_frame().node.get().is_null()); - assert!(scope.previous_frame().node.get().is_null()); - // The height should be 0 assert_eq!(scope.height(), 0); diff --git a/packages/core/tests/suspense.rs b/packages/core/tests/suspense.rs index c0d89a10f..481ade2e3 100644 --- a/packages/core/tests/suspense.rs +++ b/packages/core/tests/suspense.rs @@ -11,7 +11,7 @@ async fn it_works() { let mutations = dom.rebuild().santize(); - // We should at least get the top-level template in + // We should at least get the top-level template in before pausing for the children assert_eq!( mutations.template_mutations, [ diff --git a/packages/core/tests/task.rs b/packages/core/tests/task.rs index be7c969d3..2704c5eb2 100644 --- a/packages/core/tests/task.rs +++ b/packages/core/tests/task.rs @@ -13,7 +13,7 @@ async fn it_works() { tokio::select! { _ = dom.wait_for_work() => {} - _ = tokio::time::sleep(Duration::from_millis(10)) => {} + _ = tokio::time::sleep(Duration::from_millis(500)) => {} }; // By the time the tasks are finished, we should've accumulated ticks from two tasks diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index f1fe0cafb..8d5a9da59 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -144,7 +144,6 @@ impl<'a> ToTokens for TemplateRenderer<'a> { attr_paths: &[ #(#attr_paths),* ], }; ::dioxus::core::VNode { - node_id: Default::default(), parent: None, key: #key_tokens, template: TEMPLATE, From 03aea885cfd1952d36f55e49cdb14beb73cde782 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 29 Nov 2022 16:46:25 -0500 Subject: [PATCH 10/18] chore: make clippy happy --- packages/core/src/any_props.rs | 8 +++-- packages/core/src/arena.rs | 22 ++++---------- packages/core/src/create.rs | 14 ++++----- packages/core/src/diff.rs | 8 ++--- packages/core/src/factory.rs | 40 +++++++++++-------------- packages/core/src/mutations.rs | 10 +------ packages/core/src/nodes.rs | 24 ++++++++------- packages/core/src/scheduler/suspense.rs | 2 +- packages/core/src/scheduler/task.rs | 18 ++--------- packages/core/src/scheduler/wait.rs | 5 +++- packages/core/src/scopes.rs | 4 +-- packages/core/src/virtual_dom.rs | 15 ++++------ 12 files changed, 67 insertions(+), 103 deletions(-) diff --git a/packages/core/src/any_props.rs b/packages/core/src/any_props.rs index 797fb8ddd..d83b41b0c 100644 --- a/packages/core/src/any_props.rs +++ b/packages/core/src/any_props.rs @@ -8,7 +8,11 @@ use crate::{ }; /// A trait that essentially allows VComponentProps to be used generically -pub unsafe trait AnyProps<'a> { +/// +/// # Safety +/// +/// This should not be implemented outside this module +pub(crate) unsafe trait AnyProps<'a> { fn props_ptr(&self) -> *const (); fn render(&'a self, bump: &'a ScopeState) -> RenderReturn<'a>; unsafe fn memoize(&self, other: &dyn AnyProps) -> bool; @@ -64,6 +68,6 @@ where }); // Call the render function directly - (self.render_fn)(scope).as_return(cx) + (self.render_fn)(scope).into_return(cx) } } diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 6add899e8..c6796ed30 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -31,7 +31,7 @@ impl ElementRef { } } -impl<'b> VirtualDom { +impl VirtualDom { pub(crate) fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId { let entry = self.elements.vacant_entry(); let id = entry.key(); @@ -68,9 +68,8 @@ impl<'b> VirtualDom { if let Some(root) = scope.as_ref().try_root_node() { let root = unsafe { root.extend_lifetime_ref() }; - match root { - RenderReturn::Sync(Ok(node)) => self.drop_scope_inner(node), - _ => {} + if let RenderReturn::Sync(Ok(node)) = root { + self.drop_scope_inner(node) } } @@ -78,9 +77,8 @@ impl<'b> VirtualDom { if let Some(root) = unsafe { scope.as_ref().previous_frame().try_load_node() } { let root = unsafe { root.extend_lifetime_ref() }; - match root { - RenderReturn::Sync(Ok(node)) => self.drop_scope_inner(node), - _ => {} + if let RenderReturn::Sync(Ok(node)) = root { + self.drop_scope_inner(node) } } @@ -123,16 +121,6 @@ impl ElementPath { } } -#[test] -fn path_ascendant() { - // assert!(&ElementPath::Deep(&[]).is_ascendant(&&[0_u8])); - // assert!(&ElementPath::Deep(&[1, 2]), &[1, 2, 3]); - // assert!(!is_path_ascendant( - // &ElementPath::Deep(&[1, 2, 3, 4]), - // &[1, 2, 3] - // )); -} - impl PartialEq<&[u8]> for ElementPath { fn eq(&self, other: &&[u8]) -> bool { match *self { diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index 1a308a577..de309221f 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -224,7 +224,7 @@ impl<'b> VirtualDom { } for node in template.template.roots { - self.create_static_node(template, node); + self.create_static_node(node); } self.mutations.template_mutations.push(SaveTemplate { @@ -233,11 +233,7 @@ impl<'b> VirtualDom { }); } - pub(crate) fn create_static_node( - &mut self, - template: &'b VNode<'b>, - node: &'b TemplateNode<'static>, - ) { + pub(crate) fn create_static_node(&mut self, node: &'b TemplateNode<'static>) { match *node { // Todo: create the children's template TemplateNode::Dynamic(_) => self @@ -275,7 +271,7 @@ impl<'b> VirtualDom { self.mutations .template_mutations - .extend(attrs.into_iter().filter_map(|attr| match attr { + .extend(attrs.iter().filter_map(|attr| match attr { TemplateAttribute::Static { name, value, @@ -294,8 +290,8 @@ impl<'b> VirtualDom { } children - .into_iter() - .for_each(|child| self.create_static_node(template, child)); + .iter() + .for_each(|child| self.create_static_node(child)); self.mutations .template_mutations diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 669288ca7..21fd9b207 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -320,14 +320,14 @@ impl<'b> VirtualDom { Text(t) => self.reclaim(t.id.get()), Fragment(VFragment::Empty(t)) => self.reclaim(t.get()), Fragment(VFragment::NonEmpty(nodes)) => { - nodes.into_iter().for_each(|node| self.clean_up_node(node)) + nodes.iter().for_each(|node| self.clean_up_node(node)) } }; } // we clean up nodes with dynamic attributes, provided the node is unique and not a root node let mut id = None; - for (idx, attr) in node.dynamic_attrs.into_iter().enumerate() { + for (idx, attr) in node.dynamic_attrs.iter().enumerate() { // We'll clean up the root nodes either way, so don't worry if node.template.attr_paths[idx].len() == 1 { continue; @@ -620,7 +620,7 @@ impl<'b> VirtualDom { // 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_keys.is_empty() { - if let Some(_) = old.get(0) { + if old.get(0).is_some() { self.remove_nodes(&old[1..]); self.replace_many(&old[0], new); } else { @@ -744,7 +744,7 @@ impl<'b> 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)); + nodes.iter().for_each(|node| self.remove_node(node)); } fn remove_node(&mut self, node: &'b VNode<'b>) { diff --git a/packages/core/src/factory.rs b/packages/core/src/factory.rs index a529d38bc..20a98edea 100644 --- a/packages/core/src/factory.rs +++ b/packages/core/src/factory.rs @@ -106,10 +106,10 @@ impl ScopeState { } pub trait ComponentReturn<'a, A = ()> { - fn as_return(self, cx: &'a ScopeState) -> RenderReturn<'a>; + fn into_return(self, cx: &'a ScopeState) -> RenderReturn<'a>; } impl<'a> ComponentReturn<'a> for Element<'a> { - fn as_return(self, _cx: &ScopeState) -> RenderReturn<'a> { + fn into_return(self, _cx: &ScopeState) -> RenderReturn<'a> { RenderReturn::Sync(self) } } @@ -119,11 +119,9 @@ impl<'a, F> ComponentReturn<'a, AsyncMarker> for F where F: Future> + 'a, { - fn as_return(self, cx: &'a ScopeState) -> RenderReturn<'a> { + fn into_return(self, cx: &'a ScopeState) -> RenderReturn<'a> { let f: &mut dyn Future> = cx.bump().alloc(self); - let boxed = unsafe { BumpBox::from_raw(f) }; - let pined: BumpBox<_> = boxed.into(); - RenderReturn::Async(pined) + RenderReturn::Async(unsafe { BumpBox::from_raw(f) }) } } @@ -148,18 +146,18 @@ pub trait IntoDynNode<'a, A = ()> { fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a>; } -impl<'a, 'b> IntoDynNode<'a> for () { +impl<'a> IntoDynNode<'a> for () { fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { DynamicNode::Fragment(VFragment::Empty(Cell::new(ElementId(0)))) } } -impl<'a, 'b> IntoDynNode<'a> for VNode<'a> { +impl<'a> IntoDynNode<'a> for VNode<'a> { fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { DynamicNode::Fragment(VFragment::NonEmpty(_cx.bump().alloc([self]))) } } -impl<'a, 'b, T: IntoDynNode<'a>> IntoDynNode<'a> for Option { +impl<'a, T: IntoDynNode<'a>> IntoDynNode<'a> for Option { fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { match self { Some(val) => val.into_vnode(_cx), @@ -183,7 +181,7 @@ impl<'a, 'b> IntoDynNode<'a> for LazyNodes<'a, 'b> { } } -impl<'b> IntoDynNode<'_> for &'b str { +impl<'a> IntoDynNode<'_> for &'a str { fn into_vnode(self, cx: &ScopeState) -> DynamicNode { cx.text(format_args!("{}", self)) } @@ -201,25 +199,23 @@ impl<'b> IntoDynNode<'b> for Arguments<'_> { } } -impl<'a, 'b> IntoDynNode<'a> for &VNode<'a> { +impl<'a> IntoDynNode<'a> for &'a VNode<'a> { fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { - todo!() - // VNode { - // node_id: self.node_id.clone(), - // parent: self.parent, - // template: self.template, - // root_ids: self.root_ids, - // key: self.key, - // dynamic_nodes: self.dynamic_nodes, - // dynamic_attrs: self.dynamic_attrs, - // } + DynamicNode::Fragment(VFragment::NonEmpty(_cx.bump().alloc([VNode { + parent: self.parent, + template: self.template, + root_ids: self.root_ids, + key: self.key, + dynamic_nodes: self.dynamic_nodes, + dynamic_attrs: self.dynamic_attrs, + }]))) } } pub trait IntoTemplate<'a> { fn into_template(self, _cx: &'a ScopeState) -> VNode<'a>; } -impl<'a, 'b> IntoTemplate<'a> for VNode<'a> { +impl<'a> IntoTemplate<'a> for VNode<'a> { fn into_template(self, _cx: &'a ScopeState) -> VNode<'a> { self } diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index b1dff4011..894dc28c8 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -1,6 +1,6 @@ use crate::{arena::ElementId, ScopeId}; -#[derive(Debug)] +#[derive(Debug, Default)] #[must_use = "not handling edits can lead to visual inconsistencies in UI"] pub struct Mutations<'a> { pub subtree: usize, @@ -9,14 +9,6 @@ pub struct Mutations<'a> { } impl<'a> Mutations<'a> { - pub fn new() -> Self { - Self { - subtree: 0, - edits: Vec::new(), - template_mutations: Vec::new(), - } - } - /// A useful tool for testing mutations /// /// Rewrites IDs to just be "template", so you can compare the mutations diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 40aefbee8..e6dbcab2e 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -102,8 +102,8 @@ pub struct VComponent<'a> { pub name: &'static str, pub static_props: bool, pub scope: Cell>, - pub props: Cell + 'a>>>, pub render_fn: *const (), + pub(crate) props: Cell + 'a>>>, } impl<'a> std::fmt::Debug for VComponent<'a> { @@ -153,11 +153,13 @@ pub enum AttributeValue<'a> { Float(f32), Int(i32), Bool(bool), - Listener(RefCell) + 'a>>>), + Listener(RefCell>>), Any(BumpBox<'a, dyn AnyValue>), None, } +type ListenerCb<'a> = BumpBox<'a, dyn FnMut(UiEvent) + 'a>; + impl<'a> AttributeValue<'a> { pub fn new_listener( cx: &'a ScopeState, @@ -208,15 +210,15 @@ impl<'a> PartialEq for AttributeValue<'a> { impl<'a> AttributeValue<'a> { pub fn matches_type(&self, other: &'a AttributeValue<'a>) -> bool { - match (self, other) { - (Self::Text(_), Self::Text(_)) => true, - (Self::Float(_), Self::Float(_)) => true, - (Self::Int(_), Self::Int(_)) => true, - (Self::Bool(_), Self::Bool(_)) => true, - (Self::Listener(_), Self::Listener(_)) => true, - (Self::Any(_), Self::Any(_)) => true, - _ => return false, - } + matches!( + (self, other), + (Self::Text(_), Self::Text(_)) + | (Self::Float(_), Self::Float(_)) + | (Self::Int(_), Self::Int(_)) + | (Self::Bool(_), Self::Bool(_)) + | (Self::Listener(_), Self::Listener(_)) + | (Self::Any(_), Self::Any(_)) + ) } } diff --git a/packages/core/src/scheduler/suspense.rs b/packages/core/src/scheduler/suspense.rs index 0b2952425..e73be438a 100644 --- a/packages/core/src/scheduler/suspense.rs +++ b/packages/core/src/scheduler/suspense.rs @@ -41,7 +41,7 @@ impl SuspenseBoundary { Self { id, waiting_on: Default::default(), - mutations: RefCell::new(Mutations::new()), + mutations: RefCell::new(Mutations::default()), placeholder: Cell::new(None), created_on_stack: Cell::new(0), onresolve: None, diff --git a/packages/core/src/scheduler/task.rs b/packages/core/src/scheduler/task.rs index 5ff831216..c8d7b6b10 100644 --- a/packages/core/src/scheduler/task.rs +++ b/packages/core/src/scheduler/task.rs @@ -2,8 +2,7 @@ use super::{waker::RcWake, Scheduler, SchedulerMsg}; use crate::ScopeId; use std::cell::RefCell; use std::future::Future; -use std::task::Context; -use std::{pin::Pin, rc::Rc, task::Poll}; +use std::{pin::Pin, rc::Rc}; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] @@ -12,22 +11,9 @@ pub struct TaskId(pub usize); /// the task itself is the waker pub(crate) struct LocalTask { pub scope: ScopeId, + pub(super) task: RefCell + 'static>>>, id: TaskId, tx: futures_channel::mpsc::UnboundedSender, - task: RefCell + 'static>>>, -} - -impl LocalTask { - /// Poll this task and return whether or not it is complete - pub(super) fn progress(self: &Rc) -> bool { - let waker = self.waker(); - let mut cx = Context::from_waker(&waker); - - match self.task.borrow_mut().as_mut().poll(&mut cx) { - Poll::Ready(_) => true, - _ => false, - } - } } impl Scheduler { diff --git a/packages/core/src/scheduler/wait.rs b/packages/core/src/scheduler/wait.rs index 794abedc2..36a8818e3 100644 --- a/packages/core/src/scheduler/wait.rs +++ b/packages/core/src/scheduler/wait.rs @@ -18,8 +18,11 @@ impl VirtualDom { let mut tasks = self.scheduler.tasks.borrow_mut(); let task = &tasks[id.0]; + let waker = task.waker(); + let mut cx = Context::from_waker(&waker); + // If the task completes... - if task.progress() { + if task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() { // Remove it from the scope so we dont try to double drop it when the scope dropes self.scopes[task.scope.0].spawned_tasks.remove(&id); diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 946ae90dc..ae944792b 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -134,7 +134,7 @@ impl ScopeState { /// This is useful for traversing the tree outside of the VirtualDom, such as in a custom renderer or in SSR. /// /// Panics if the tree has not been built yet. - pub fn root_node<'a>(&'a self) -> &'a RenderReturn<'a> { + pub fn root_node(&self) -> &RenderReturn { self.try_root_node() .expect("The tree has not been built yet. Make sure to call rebuild on the tree before accessing its nodes.") } @@ -144,7 +144,7 @@ impl ScopeState { /// This is useful for traversing the tree outside of the VirtualDom, such as in a custom renderer or in SSR. /// /// Returns [`None`] if the tree has not been built yet. - pub fn try_root_node<'a>(&'a self) -> Option<&'a RenderReturn<'a>> { + pub fn try_root_node(&self) -> Option<&RenderReturn> { let ptr = self.current_frame().node.get(); if ptr.is_null() { diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 374559e8f..38227a69d 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -6,7 +6,7 @@ use crate::{ any_props::VProps, arena::{ElementId, ElementRef}, factory::RenderReturn, - innerlude::{DirtyScope, Mutations, Scheduler, SchedulerMsg, ErrorBoundary}, + innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg}, mutations::Mutation, nodes::{Template, TemplateId}, scheduler::{SuspenseBoundary, SuspenseId}, @@ -236,7 +236,7 @@ impl VirtualDom { dirty_scopes: BTreeSet::new(), collected_leaves: Vec::new(), finished_fibers: Vec::new(), - mutations: Mutations::new(), + mutations: Mutations::default(), }; let root = dom.new_scope(Box::new(VProps::new( @@ -454,9 +454,9 @@ impl VirtualDom { } /// Swap the current mutations with a new - fn finalize<'a>(&'a mut self) -> Mutations<'a> { + fn finalize(&mut self) -> Mutations { // todo: make this a routine - let mut out = Mutations::new(); + let mut out = Mutations::default(); std::mem::swap(&mut self.mutations, &mut out); out } @@ -481,7 +481,7 @@ impl VirtualDom { /// /// apply_edits(edits); /// ``` - pub fn rebuild<'a>(&'a mut self) -> Mutations<'a> { + pub fn rebuild(&mut self) -> Mutations { match unsafe { self.run_scope(ScopeId(0)).extend_lifetime_ref() } { // Rebuilding implies we append the created elements to the root RenderReturn::Sync(Ok(node)) => { @@ -519,10 +519,7 @@ impl VirtualDom { /// It's generally a good idea to put some sort of limit on the suspense process in case a future is having issues. /// /// If no suspense trees are present - pub async fn render_with_deadline<'a>( - &'a mut self, - deadline: impl Future, - ) -> Mutations<'a> { + pub async fn render_with_deadline(&mut self, deadline: impl Future) -> Mutations { pin_mut!(deadline); loop { From 16a521a601596df614dd50f9dde8dff873b03bc1 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 30 Nov 2022 10:31:44 -0500 Subject: [PATCH 11/18] chore: update docs --- packages/core/Cargo.toml | 2 - packages/core/README.md | 110 ++++++++++++++++++------------- packages/core/src/diff.rs | 9 ++- packages/core/src/lib.rs | 2 + packages/core/src/virtual_dom.rs | 14 ++-- 5 files changed, 75 insertions(+), 62 deletions(-) diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 9479bfc83..f779e6ba2 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -34,7 +34,6 @@ indexmap = "1.7" # Serialize the Edits for use in Webview/Liveview instances serde = { version = "1", features = ["derive"], optional = true } anyhow = "1.0.66" -bumpslab = "0.1.0" [dev-dependencies] tokio = { version = "*", features = ["full"] } @@ -43,4 +42,3 @@ dioxus = { path = "../dioxus" } [features] default = [] serialize = ["serde"] -debug_vdom = [] diff --git a/packages/core/README.md b/packages/core/README.md index 9b0b14fff..cf3467030 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -1,52 +1,84 @@ -# Dioxus-core +# dioxus-core -This is the core crate for the Dioxus Virtual DOM. This README will focus on the technical design and layout of this Virtual DOM implementation. If you want to read more about using Dioxus, then check out the Dioxus crate, documentation, and website. +dioxus-core is a fast and featureful VirtualDom implementation written in and for Rust. -To build new apps with Dioxus or to extend the ecosystem with new hooks or components, use the higher-level `dioxus` crate with the appropriate feature flags. +# Features +- Functions as components +- Hooks for local state +- Task pool for spawning futures +- Template-based architecture +- Asynchronous components +- Suspense boundaries +- Error boundaries through the `anyhow` crate +- Customizable memoization + +If just starting out, check out the Guides first. + +# General Theory + +The dioxus-core `VirtualDom` object is built around the concept of a `Template`. Templates describe a layout tree known at compile time with dynamic parts filled at runtime. + +Each component in the VirtualDom works as a dedicated render loop where re-renders are triggered by events external to the VirtualDom, or from the components themselves. + +When each component re-renders, it must return an `Element`. In Dioxus, the `Element` type is an alias for `Result`. Between two renders, Dioxus compares the inner `VNode` object, and calculates the differences of the dynamic portions of each internal `Template`. If any attributes or elements are different between the old layout and new layout, Dioxus will write modifications to the `Mutations` object. + +Dioxus expects the target renderer to save its nodes in a list. Each element is given a numerical ID which can be used to directly index into that list for O(1) lookups. + +# Usage + +All Dioxus apps start as just a function that takes the [`Scope`] object and returns an [`Element`]. + +The `dioxus` crate exports the `rsx` macro which transforms a helpful, simpler syntax of Rust into the logic required to build Templates. + +First, start with your app: ```rust, ignore fn app(cx: Scope) -> Element { - render!(div { "hello world" }) + cx.render(rsx!( div { "hello world" } )) } +``` -fn main() { - let mut renderer = SomeRenderer::new(); +Then, we'll want to create a new VirtualDom using this app as the root component. - // Creating a new virtualdom from a component - let mut dom = VirtualDom::new(app); +```rust, ingore +let mut dom = VirtualDom::new(app); +``` - // Patching the renderer with the changes to draw the screen - let edits = dom.rebuild(); - renderer.apply(edits); +To build the app into a stream of mutations, we'll use [`VirtualDom::rebuild`]: - // Injecting events - dom.handle_message(SchedulerMsg::Event(UserEvent { - scope_id: None, - priority: EventPriority::High, - element: ElementId(0), - name: "onclick", - data: Arc::new(()), - })); +```rust, ignore +let mutations = dom.rebuild(); - // polling asynchronously - dom.wait_for_work().await; +apply_edits_to_real_dom(mutations); +``` - // working with a deadline - if let Some(edits) = dom.work_with_deadline(|| false) { - renderer.apply(edits); +We can then wait for any asynchronous components or pending futures using the `wait_for_work()` method. If we have a deadline, then we can use render_with_deadline instead: + +```rust, ignore +// Wait for the dom to be marked dirty internally +dom.wait_for_work().await; + +// Or wait for a deadline and then collect edits +dom.render_with_deadline(tokio::time::sleep(Duration::from_millis(16))); +``` + +If an event occurs from outside the virtualdom while waiting for work, then we can cancel the wait using a `select!` block and inject the event. + +```rust +loop { + tokio::select! { + evt = real_dom.event() => dom.handle_event("click", evt.data, evt.element, evt.bubbles), + _ = dom.wait_for_work() => {} } - // getting state of scopes - let scope = dom.get_scope(ScopeId(0)).unwrap(); + // Render any work without blocking the main thread for too long + let mutations = dom.render_with_deadline(tokio::time::sleep(Duration::from_millis(10))); - // iterating through the tree - match scope.root_node() { - VNodes::Text(vtext) => dbg!(vtext), - VNodes::Element(vel) => dbg!(vel), - _ => todo!() - } + // And then apply the edits + real_dom.apply(mutations); } + ``` ## Internals @@ -82,17 +114,3 @@ The final implementation of Dioxus must: - Support server-side-rendering (SSR). VNodes should render to a string that can be served via a web server. - Be "live". Components should be able to be both server rendered and client rendered without needing frontend APIs. - Be modular. Components and hooks should be work anywhere without worrying about target platform. - - -## Safety - -Dioxus uses unsafe. The design of Dioxus *requires* unsafe (self-referential trees). - -All of our test suite passes MIRI without errors. - -Dioxus deals with arenas, lifetimes, asynchronous tasks, custom allocators, pinning, and a lot more foundational low-level work that is very difficult to implement with 0 unsafe. - -If you don't want to use a crate that uses unsafe, then this crate is not for you. - -However, we are always interested in decreasing the scope of the core VirtualDom to make it easier to review. We'd be happy to welcome PRs that can eliminate unsafe code while still upholding the numerous invariants required to execute certain features. - diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 21fd9b207..656df0fd1 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -14,7 +14,7 @@ use std::cell::Cell; use DynamicNode::*; impl<'b> VirtualDom { - pub fn diff_scope(&mut self, scope: ScopeId) { + pub(super) fn diff_scope(&mut self, scope: ScopeId) { let scope_state = &mut self.scopes[scope.0]; self.scope_stack.push(scope); @@ -59,7 +59,7 @@ impl<'b> VirtualDom { todo!("Not yet handling error rollover") } - pub fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) { + fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) { if left_template.template.id != right_template.template.id { return self.light_diff_templates(left_template, right_template); } @@ -185,7 +185,7 @@ impl<'b> VirtualDom { /// This is mostly implemented to help solve the issue where the same component is rendered under two different /// conditions: /// - /// ```rust + /// ```rust, ignore /// if enabled { /// rsx!{ Component { enabled_sign: "abc" } } /// } else { @@ -196,8 +196,7 @@ impl<'b> VirtualDom { /// However, we should not that it's explicit in the docs that this is not a guarantee. If you need to preserve state, /// then you should be passing in separate props instead. /// - /// ``` - /// + /// ```rust, ignore /// let props = if enabled { /// ComponentProps { enabled_sign: "abc" } /// } else { diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 1754f2766..b81d8e9ed 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -1,3 +1,5 @@ +#![doc = include_str!("../README.md")] + mod any_props; mod arena; mod bump_frame; diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 38227a69d..31a693b9b 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -94,9 +94,9 @@ use std::{ /// ## Building an event loop around Dioxus: /// /// Putting everything together, you can build an event loop around Dioxus by using the methods outlined above. -/// ```rust +/// ```rust, ignore /// fn app(cx: Scope) -> Element { -/// cx.render(rsx!{ +/// cx.render(rsx! { /// div { "Hello World" } /// }) /// } @@ -121,7 +121,7 @@ use std::{ /// where waiting on portions of the UI to finish rendering is important. To wait for suspense, use the /// [`VirtualDom::render_with_deadline`] method: /// -/// ```rust +/// ```rust, ignore /// let dom = VirtualDom::new(app); /// /// let deadline = tokio::time::sleep(Duration::from_millis(100)); @@ -134,7 +134,7 @@ use std::{ /// suggest rendering with a deadline, and then looping between [`VirtualDom::wait_for_work`] and render_immediate until /// no suspended work is left. /// -/// ``` +/// ```rust, ignore /// let dom = VirtualDom::new(app); /// /// let deadline = tokio::time::sleep(Duration::from_millis(20)); @@ -290,11 +290,6 @@ impl VirtualDom { self.dirty_scopes.insert(DirtyScope { height, id }); } - /// Mark the entire tree as dirty. - /// - /// Will force a re-render of every component - pub fn mark_dirty_all(&mut self) {} - /// Determine whether or not a scope is currently in a suspended state /// /// This does not mean the scope is waiting on its own futures, just that the tree that the scope exists in is @@ -609,6 +604,7 @@ impl VirtualDom { impl Drop for VirtualDom { fn drop(&mut self) { + // Simply drop this scope which drops all of its children self.drop_scope(ScopeId(0)); } } From 3c19def5500c2dae56d90ca8e4f05caf9e39ac8e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 30 Nov 2022 11:24:13 -0500 Subject: [PATCH 12/18] chore: get create working and simplify dynamic nodes --- examples/router.rs | 4 +- examples/rsx_usage.rs | 4 +- packages/core/src/arena.rs | 4 +- packages/core/src/create.rs | 50 +++++++-------- packages/core/src/diff.rs | 71 +++++--------------- packages/core/src/factory.rs | 28 +++++--- packages/core/src/lib.rs | 3 +- packages/core/src/mutations.rs | 4 -- packages/core/src/nodes.rs | 12 ++-- packages/core/src/scope_arena.rs | 4 +- packages/desktop/src/controller.rs | 2 +- packages/interpreter/src/interpreter.js | 75 ++++++++++++++-------- packages/router/src/components/redirect.rs | 2 +- packages/router/src/components/route.rs | 5 +- packages/ssr/src/cache.rs | 2 + packages/ssr/src/template.rs | 71 ++++++++++---------- 16 files changed, 168 insertions(+), 173 deletions(-) diff --git a/examples/router.rs b/examples/router.rs index 20b168670..a4118111a 100644 --- a/examples/router.rs +++ b/examples/router.rs @@ -30,7 +30,7 @@ fn app(cx: Scope) -> Element { } fn BlogPost(cx: Scope) -> Element { - let post = dioxus_router::use_route(&cx).last_segment()?; + let post = dioxus_router::use_route(&cx).last_segment().unwrap(); cx.render(rsx! { div { @@ -46,7 +46,7 @@ struct Query { } fn User(cx: Scope) -> Element { - let post = dioxus_router::use_route(&cx).last_segment()?; + let post = dioxus_router::use_route(&cx).last_segment().unwrap(); let query = dioxus_router::use_route(&cx) .query::() diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs index 597c6ab79..99598479f 100644 --- a/examples/rsx_usage.rs +++ b/examples/rsx_usage.rs @@ -165,13 +165,13 @@ fn app(cx: Scope) -> Element { // Can pass in props directly as an expression { - let props = TallerProps {a: "hello", children: Default::default()}; + let props = TallerProps {a: "hello", children: cx.render(rsx!(()))}; rsx!(Taller { ..props }) } // Spreading can also be overridden manually Taller { - ..TallerProps { a: "ballin!", children: Default::default() }, + ..TallerProps { a: "ballin!", children: cx.render(rsx!(()) )}, a: "not ballin!" } diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index c6796ed30..c27dca5ed 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -1,6 +1,6 @@ use crate::{ factory::RenderReturn, nodes::VNode, virtual_dom::VirtualDom, AttributeValue, DynamicNode, - ScopeId, VFragment, + ScopeId, }; use bumpalo::boxed::Box as BumpBox; @@ -101,7 +101,7 @@ impl VirtualDom { for (idx, _) in node.template.roots.iter().enumerate() { match node.dynamic_root(idx) { Some(DynamicNode::Component(c)) => self.drop_scope(c.scope.get().unwrap()), - Some(DynamicNode::Fragment(VFragment::NonEmpty(nodes))) => { + Some(DynamicNode::Fragment(nodes)) => { for node in *nodes { self.drop_scope_inner(node); } diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index de309221f..771b1dfa6 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -1,11 +1,13 @@ +use std::cell::Cell; + use crate::factory::RenderReturn; -use crate::innerlude::{VComponent, VFragment, VText}; +use crate::innerlude::{VComponent, VText}; use crate::mutations::Mutation; 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> VirtualDom { /// Create a new template [`VNode`] and write it to the [`Mutations`] buffer. @@ -55,15 +57,14 @@ impl<'b> VirtualDom { 1 } - DynamicNode::Fragment(VFragment::Empty(slot)) => { + DynamicNode::Placeholder(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 { .. } => { + DynamicNode::Fragment(_) | DynamicNode::Component { .. } => { self.create_dynamic_node(template, &template.dynamic_nodes[*id], *id) } } @@ -309,7 +310,8 @@ impl<'b> VirtualDom { use DynamicNode::*; match node { Text(text) => self.create_dynamic_text(template, text, idx), - Fragment(frag) => self.create_fragment(frag, template, idx), + Fragment(frag) => self.create_fragment(frag), + Placeholder(frag) => self.create_placeholder(frag, template, idx), Component(component) => self.create_component_node(template, component, idx), } } @@ -340,34 +342,30 @@ impl<'b> VirtualDom { 0 } - pub(crate) fn create_fragment( + pub(crate) fn create_placeholder( &mut self, - frag: &'b VFragment<'b>, + slot: &Cell, template: &'b VNode<'b>, idx: usize, ) -> usize { - match frag { - VFragment::NonEmpty(nodes) => { - nodes.iter().fold(0, |acc, child| acc + self.create(child)) - } + // Allocate a dynamic element reference for this text node + let id = self.next_element(template, template.template.node_paths[idx]); - VFragment::Empty(slot) => { - // Allocate a dynamic element reference for this text node - let id = self.next_element(template, template.template.node_paths[idx]); + // Make sure the text node is assigned to the correct element + slot.set(id); - // Make sure the text node is assigned to the correct element - slot.set(id); + // Assign the ID to the existing node in the template + self.mutations.push(AssignId { + path: &template.template.node_paths[idx][1..], + id, + }); - // Assign the ID to the existing node in the template - self.mutations.push(AssignId { - path: &template.template.node_paths[idx][1..], - id, - }); + // Since the placeholder is already in the DOM, we don't create any new nodes + 0 + } - // Since the placeholder is already in the DOM, we don't create any new nodes - 0 - } - } + pub(crate) fn create_fragment(&mut self, nodes: &'b [VNode<'b>]) -> usize { + nodes.iter().fold(0, |acc, child| acc + self.create(child)) } pub(super) fn create_component_node( diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 656df0fd1..5ed82fbcd 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -1,7 +1,7 @@ use crate::{ arena::ElementId, factory::RenderReturn, - innerlude::{DirtyScope, VComponent, VFragment, VText}, + innerlude::{DirtyScope, VComponent, VText}, mutations::Mutation, nodes::{DynamicNode, VNode}, scopes::ScopeId, @@ -10,7 +10,6 @@ use crate::{ }; use fxhash::{FxHashMap, FxHashSet}; -use std::cell::Cell; use DynamicNode::*; impl<'b> VirtualDom { @@ -101,7 +100,7 @@ impl<'b> VirtualDom { { match (left_node, right_node) { (Text(left), Text(right)) => self.diff_vtext(left, right), - (Fragment(left), Fragment(right)) => self.diff_vfragment(left, right), + (Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right), (Component(left), Component(right)) => { self.diff_vcomponent(left, right, right_template, idx) } @@ -231,40 +230,6 @@ impl<'b> VirtualDom { } } - fn diff_vfragment(&mut self, left: &'b VFragment<'b>, right: &'b VFragment<'b>) { - use VFragment::*; - match (left, right) { - (Empty(l), Empty(r)) => r.set(l.get()), - (Empty(l), NonEmpty(r)) => self.replace_placeholder_with_nodes(l, r), - (NonEmpty(l), Empty(r)) => self.replace_nodes_with_placeholder(l, r), - (NonEmpty(old), NonEmpty(new)) => self.diff_non_empty_fragment(new, old), - } - } - - fn replace_placeholder_with_nodes(&mut self, l: &'b Cell, r: &'b [VNode<'b>]) { - let m = self.create_children(r); - let id = l.get(); - self.mutations.push(Mutation::ReplaceWith { id, m }); - self.reclaim(id); - } - - fn replace_nodes_with_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b Cell) { - // Create the placeholder first, ensuring we get a dedicated ID for the placeholder - let placeholder = self.next_element(&l[0], &[]); - r.set(placeholder); - self.mutations - .push(Mutation::CreatePlaceholder { id: placeholder }); - - // Remove the old nodes, except for onea - let first = self.replace_inner(&l[0]); - self.remove_nodes(&l[1..]); - - self.mutations - .push(Mutation::ReplaceWith { id: first, m: 1 }); - - self.try_reclaim(first); - } - /// Remove all the top-level nodes, returning the firstmost root ElementId /// /// All IDs will be garbage collected @@ -272,8 +237,8 @@ impl<'b> VirtualDom { let id = match node.dynamic_root(0) { None => node.root_ids[0].get(), Some(Text(t)) => t.id.get(), - Some(Fragment(VFragment::Empty(e))) => e.get(), - Some(Fragment(VFragment::NonEmpty(nodes))) => { + Some(Placeholder(e)) => e.get(), + Some(Fragment(nodes)) => { let id = self.replace_inner(&nodes[0]); self.remove_nodes(&nodes[1..]); id @@ -317,10 +282,8 @@ impl<'b> VirtualDom { }; } Text(t) => self.reclaim(t.id.get()), - Fragment(VFragment::Empty(t)) => self.reclaim(t.get()), - Fragment(VFragment::NonEmpty(nodes)) => { - nodes.iter().for_each(|node| self.clean_up_node(node)) - } + Placeholder(t) => self.reclaim(t.get()), + Fragment(nodes) => nodes.iter().for_each(|node| self.clean_up_node(node)), }; } @@ -351,12 +314,12 @@ impl<'b> VirtualDom { self.mutations.push(Mutation::Remove { id }); self.reclaim(id); } - Some(Fragment(VFragment::Empty(e))) => { + Some(Placeholder(e)) => { let id = e.get(); self.mutations.push(Mutation::Remove { id }); self.reclaim(id); } - Some(Fragment(VFragment::NonEmpty(nodes))) => self.remove_nodes(nodes), + Some(Fragment(nodes)) => self.remove_nodes(nodes), Some(Component(comp)) => { let scope = comp.scope.get().unwrap(); match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { @@ -750,8 +713,8 @@ impl<'b> VirtualDom { for (idx, _) in node.template.roots.iter().enumerate() { let id = match node.dynamic_root(idx) { Some(Text(t)) => t.id.get(), - Some(Fragment(VFragment::Empty(t))) => t.get(), - Some(Fragment(VFragment::NonEmpty(t))) => return self.remove_nodes(t), + Some(Placeholder(t)) => t.get(), + Some(Fragment(t)) => return self.remove_nodes(t), Some(Component(comp)) => return self.remove_component(comp.scope.get().unwrap()), None => node.root_ids[idx].get(), }; @@ -793,12 +756,12 @@ impl<'b> VirtualDom { self.mutations.push(Mutation::PushRoot { id: t.id.get() }); onstack += 1; } - Some(Fragment(VFragment::Empty(t))) => { + Some(Placeholder(t)) => { self.mutations.push(Mutation::PushRoot { id: t.get() }); onstack += 1; } - Some(Fragment(VFragment::NonEmpty(t))) => { - for node in *t { + Some(Fragment(nodes)) => { + for node in *nodes { onstack += self.push_all_real_nodes(node); } } @@ -842,8 +805,8 @@ impl<'b> VirtualDom { match node.dynamic_root(0) { None => node.root_ids[0].get(), Some(Text(t)) => t.id.get(), - Some(Fragment(VFragment::NonEmpty(t))) => self.find_first_element(&t[0]), - Some(Fragment(VFragment::Empty(t))) => t.get(), + Some(Fragment(t)) => self.find_first_element(&t[0]), + Some(Placeholder(t)) => t.get(), Some(Component(comp)) => { let scope = comp.scope.get().unwrap(); match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { @@ -858,8 +821,8 @@ impl<'b> VirtualDom { match node.dynamic_root(node.template.roots.len() - 1) { None => node.root_ids.last().unwrap().get(), Some(Text(t)) => t.id.get(), - Some(Fragment(VFragment::NonEmpty(t))) => self.find_last_element(t.last().unwrap()), - Some(Fragment(VFragment::Empty(t))) => t.get(), + Some(Fragment(t)) => self.find_last_element(t.last().unwrap()), + Some(Placeholder(t)) => t.get(), Some(Component(comp)) => { let scope = comp.scope.get().unwrap(); match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { diff --git a/packages/core/src/factory.rs b/packages/core/src/factory.rs index 20a98edea..0423d521e 100644 --- a/packages/core/src/factory.rs +++ b/packages/core/src/factory.rs @@ -10,7 +10,7 @@ use std::future::Future; use crate::{ any_props::{AnyProps, VProps}, arena::ElementId, - innerlude::{DynamicNode, EventHandler, VComponent, VFragment, VText}, + innerlude::{DynamicNode, EventHandler, VComponent, VText}, Attribute, AttributeValue, Element, LazyNodes, Properties, Scope, ScopeState, VNode, }; @@ -148,12 +148,20 @@ pub trait IntoDynNode<'a, A = ()> { impl<'a> IntoDynNode<'a> for () { fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { - DynamicNode::Fragment(VFragment::Empty(Cell::new(ElementId(0)))) + DynamicNode::placeholder() } } impl<'a> IntoDynNode<'a> for VNode<'a> { fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { - DynamicNode::Fragment(VFragment::NonEmpty(_cx.bump().alloc([self]))) + DynamicNode::Fragment(_cx.bump().alloc([self])) + } +} +impl<'a> IntoDynNode<'a> for Element<'a> { + fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + match self { + Ok(val) => val.into_vnode(_cx), + _ => DynamicNode::placeholder(), + } } } @@ -161,7 +169,7 @@ impl<'a, T: IntoDynNode<'a>> IntoDynNode<'a> for Option { fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { match self { Some(val) => val.into_vnode(_cx), - None => DynamicNode::Fragment(VFragment::Empty(Cell::new(ElementId(0)))), + None => DynamicNode::placeholder(), } } } @@ -170,14 +178,14 @@ impl<'a> IntoDynNode<'a> for &Element<'a> { fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { match self.as_ref() { Ok(val) => val.clone().into_vnode(_cx), - _ => DynamicNode::Fragment(VFragment::Empty(Cell::new(ElementId(0)))), + _ => DynamicNode::placeholder(), } } } impl<'a, 'b> IntoDynNode<'a> for LazyNodes<'a, 'b> { fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a> { - DynamicNode::Fragment(VFragment::NonEmpty(cx.bump().alloc([self.call(cx)]))) + DynamicNode::Fragment(cx.bump().alloc([self.call(cx)])) } } @@ -201,14 +209,14 @@ impl<'b> IntoDynNode<'b> for Arguments<'_> { impl<'a> IntoDynNode<'a> for &'a VNode<'a> { fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { - DynamicNode::Fragment(VFragment::NonEmpty(_cx.bump().alloc([VNode { + DynamicNode::Fragment(_cx.bump().alloc([VNode { parent: self.parent, template: self.template, root_ids: self.root_ids, key: self.key, dynamic_nodes: self.dynamic_nodes, dynamic_attrs: self.dynamic_attrs, - }]))) + }])) } } @@ -243,8 +251,8 @@ where let children = nodes.into_bump_slice(); match children.len() { - 0 => DynamicNode::Fragment(VFragment::Empty(Cell::new(ElementId(0)))), - _ => DynamicNode::Fragment(VFragment::NonEmpty(children)), + 0 => DynamicNode::placeholder(), + _ => DynamicNode::Fragment(children), } } } diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index b81d8e9ed..8f9aa8f33 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -24,6 +24,7 @@ pub(crate) mod innerlude { pub use crate::dirty_scope::*; pub use crate::error_boundary::*; pub use crate::events::*; + pub use crate::factory::RenderReturn; pub use crate::fragment::*; pub use crate::lazynodes::*; pub use crate::mutations::*; @@ -87,6 +88,7 @@ pub use crate::innerlude::{ Mutations, NodeFactory, Properties, + RenderReturn, Scope, ScopeId, ScopeState, @@ -99,7 +101,6 @@ pub use crate::innerlude::{ TemplateNode, UiEvent, VComponent, - VFragment, VNode, VText, VirtualDom, diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 894dc28c8..70ed3684c 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -150,10 +150,6 @@ pub enum Mutation<'a> { id: ElementId, }, - SetInnerText { - value: &'a str, - }, - SetText { value: &'a str, id: ElementId, diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index e6dbcab2e..19955be64 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -89,13 +89,17 @@ pub enum TemplateNode<'a> { pub enum DynamicNode<'a> { Component(VComponent<'a>), Text(VText<'a>), - Fragment(VFragment<'a>), + Placeholder(Cell), + Fragment(&'a [VNode<'a>]), } impl<'a> DynamicNode<'a> { pub fn is_component(&self) -> bool { matches!(self, DynamicNode::Component(_)) } + pub fn placeholder() -> Self { + Self::Placeholder(Cell::new(ElementId(0))) + } } pub struct VComponent<'a> { @@ -122,12 +126,6 @@ pub struct VText<'a> { pub value: &'a str, } -#[derive(Debug)] -pub enum VFragment<'a> { - Empty(Cell), - NonEmpty(&'a [VNode<'a>]), -} - #[derive(Debug)] pub enum TemplateAttribute<'a> { Static { diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 2419c52a0..62f6afa33 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -7,7 +7,7 @@ use crate::{ scheduler::RcWake, scopes::{ScopeId, ScopeState}, virtual_dom::VirtualDom, - AttributeValue, DynamicNode, VFragment, VNode, + AttributeValue, DynamicNode, VNode, }; use futures_util::FutureExt; use std::{ @@ -75,7 +75,7 @@ impl VirtualDom { c.props.set(None); } } - DynamicNode::Fragment(VFragment::NonEmpty(f)) => { + DynamicNode::Fragment(f) => { for node in *f { self.ensure_drop_safety_inner(node); } diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs index 0f8492c58..3aff11352 100644 --- a/packages/desktop/src/controller.rs +++ b/packages/desktop/src/controller.rs @@ -67,7 +67,7 @@ impl DesktopController { let name = value.event.clone(); let el_id = ElementId(value.mounted_dom_id); if let Some(evt) = decode_event(value) { - dom.handle_event(&name, evt, el_id, true, EventPriority::Medium); + dom.handle_event(&name, evt, el_id, true); } } } diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index df9bed8db..bcfd77865 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -132,16 +132,13 @@ export class Interpreter { this.nodes[root] = node; this.stack.push(node); } - CreateElement(tag, root) { + CreateElement(tag) { const el = document.createElement(tag); - this.nodes[root] = el; this.stack.push(el); } - CreateElementNs(tag, root, ns) { - console.log("creating element", tag, root, ns); + CreateElementNs(tag, ns) { let el = document.createElementNS(ns, tag); this.stack.push(el); - this.nodes[root] = el; } CreatePlaceholder(root) { let el = document.createElement("pre"); @@ -149,6 +146,11 @@ export class Interpreter { this.stack.push(el); this.nodes[root] = el; } + CreateStaticPlaceholder() { + let el = document.createElement("pre"); + el.hidden = true; + this.stack.push(el); + } NewEventListener(event_name, root, handler, bubbles) { const element = this.nodes[root]; element.setAttribute("data-dioxus-id", `${root}`); @@ -162,9 +164,16 @@ export class Interpreter { SetText(root, text) { this.nodes[root].textContent = text; } - SetAttribute(root, field, value, ns) { + SetAttribute(id, field, value, ns) { + const node = this.nodes[id]; + this.SetAttributeInner(node, field, value, ns); + } + SetStaticAttribute(field, value, ns) { + const node = this.top(); + this.SetAttributeInner(node, field, value, ns); + } + SetAttributeInner(node, field, value, ns) { const name = field; - const node = this.nodes[root]; if (ns === "style") { // ????? why do we need to do this if (node.style === undefined) { @@ -248,9 +257,9 @@ export class Interpreter { let node = this.LoadChild(path); node.replaceWith(...els); } - LoadTemplate(name, index) { - console.log("loading template", name, index); + LoadTemplate(name, index, id) { let node = this.templates[name][index].cloneNode(true); + this.nodes[id] = node; this.stack.push(node); } SaveTemplate(name, m) { @@ -265,24 +274,38 @@ export class Interpreter { this.AssignId(edit.path, edit.id); break; case "CreateElement": - if (edit.namespace !== null && edit.namespace !== undefined) { - this.CreateElementNs(edit.name, edit.id, edit.namespace); - } else { - this.CreateElement(edit.name, edit.id); - } + this.CreateElement(edit.name); + break; + case "CreateElementNs": + this.CreateElementNs(edit.name, edit.namespace); break; case "CreatePlaceholder": this.CreatePlaceholder(edit.id); break; - case "CreateTextNode": + case "CreateStaticText": this.CreateTextNode(edit.value); break; + case "CreateStaticPlaceholder": + this.CreateStaticPlaceholder(); + break; + case "CreateTextPlaceholder": + this.CreateRawText("placeholder"); + break; case "CreateStaticText": + this.CreateRawText(edit.value); + break; + case "CreateTextNode": this.CreateTextNode(edit.value); break; case "HydrateText": this.HydrateText(edit.path, edit.value, edit.id); break; + case "LoadTemplate": + this.LoadTemplate(edit.name, edit.index); + break; + case "SaveTemplate": + this.SaveTemplate(edit.name, edit.m); + break; case "PushRoot": this.PushRoot(edit.id); break; @@ -293,29 +316,29 @@ export class Interpreter { this.ReplacePlaceholder(edit.path, edit.m); break; case "InsertAfter": - this.InsertAfter(edit.id, edit.n); + this.InsertAfter(edit.id, edit.m); break; case "InsertBefore": - this.InsertBefore(edit.id, edit.n); + this.InsertBefore(edit.id, edit.m); break; case "Remove": this.Remove(edit.id); break; - case "LoadTemplate": - this.LoadTemplate(edit.name, edit.index); - break; - case "SaveTemplate": - this.SaveTemplate(edit.name, edit.m); - break; - case "CreateElementNs": - this.CreateElementNs(edit.name, edit.id, edit.ns); - break; case "SetText": this.SetText(edit.id, edit.value); break; case "SetAttribute": this.SetAttribute(edit.id, edit.name, edit.value, edit.ns); break; + case "SetStaticAttribute": + this.SetStaticAttribute(edit.name, edit.value, edit.ns); + break; + case "SetBoolAttribute": + this.SetAttribute(edit.id, edit.name, edit.value, edit.ns); + break; + case "SetInnerText": + console.log("Set inner text?"); + break; case "RemoveAttribute": this.RemoveAttribute(edit.id, edit.name, edit.ns); break; diff --git a/packages/router/src/components/redirect.rs b/packages/router/src/components/redirect.rs index 561dec7b1..5bb0650e2 100644 --- a/packages/router/src/components/redirect.rs +++ b/packages/router/src/components/redirect.rs @@ -47,5 +47,5 @@ pub fn Redirect<'a>(cx: Scope<'a, RedirectProps<'a>>) -> Element { router.replace_route(cx.props.to, None, None); } - None + cx.render(rsx!(())) } diff --git a/packages/router/src/components/route.rs b/packages/router/src/components/route.rs index b613a4d5c..04b8944e1 100644 --- a/packages/router/src/components/route.rs +++ b/packages/router/src/components/route.rs @@ -29,7 +29,8 @@ pub struct RouteProps<'a> { pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element { let router_root = cx .use_hook(|| cx.consume_context::>()) - .as_ref()?; + .as_ref() + .unwrap(); cx.use_hook(|| { // create a bigger, better, longer route if one above us exists @@ -55,6 +56,6 @@ pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element { cx.render(rsx!(&cx.props.children)) } else { log::trace!("Route should *not* render: {:?}", cx.scope_id()); - None + cx.render(rsx!(())) } } diff --git a/packages/ssr/src/cache.rs b/packages/ssr/src/cache.rs index 0e7d585ff..d7c2d9ca0 100644 --- a/packages/ssr/src/cache.rs +++ b/packages/ssr/src/cache.rs @@ -3,6 +3,7 @@ use std::fmt::Write; pub struct StringCache { pub segments: Vec, + pub template: Template<'static>, } #[derive(Default)] @@ -40,6 +41,7 @@ impl StringCache { Ok(Self { segments: chain.segments, + template: template.template, }) } diff --git a/packages/ssr/src/template.rs b/packages/ssr/src/template.rs index 1df8c1581..d082a2f4a 100644 --- a/packages/ssr/src/template.rs +++ b/packages/ssr/src/template.rs @@ -1,5 +1,5 @@ use super::cache::Segment; -use dioxus_core::{prelude::*, AttributeValue, DynamicNode, VText}; +use dioxus_core::{prelude::*, AttributeValue, DynamicNode, RenderReturn, VText}; use std::collections::HashMap; use std::fmt::Write; use std::rc::Rc; @@ -9,7 +9,7 @@ use crate::cache::StringCache; /// A virtualdom renderer that caches the templates it has seen for faster rendering #[derive(Default)] pub struct SsrRender { - template_cache: HashMap, Rc>, + template_cache: HashMap<&'static str, Rc>, } impl SsrRender { @@ -18,7 +18,11 @@ impl SsrRender { let root = scope.root_node(); let mut out = String::new(); - // self.render_template(&mut out, dom, root).unwrap(); + + match root { + RenderReturn::Sync(Ok(node)) => self.render_template(&mut out, dom, node).unwrap(), + _ => {} + }; out } @@ -31,7 +35,7 @@ impl SsrRender { ) -> std::fmt::Result { let entry = self .template_cache - .entry(template.template) + .entry(template.template.id) .or_insert_with(|| Rc::new(StringCache::from_template(template).unwrap())) .clone(); @@ -46,37 +50,38 @@ impl SsrRender { }; } Segment::Node(idx) => match &template.dynamic_nodes[*idx] { - DynamicNode::Component(_) => todo!(), - DynamicNode::Text(_) => todo!(), - DynamicNode::Fragment(_) => todo!(), - DynamicNode::Placeholder(_) => todo!(), - // todo!() - // DynamicNode::Text(VText { id, value }) => { - // // in SSR, we are concerned that we can't hunt down the right text node since they might get merged - // // if !*inner { - // write!(buf, "")?; - // // } + DynamicNode::Component(node) => { + let id = node.scope.get().unwrap(); + let scope = dom.get_scope(id).unwrap(); + let node = scope.root_node(); + match node { + RenderReturn::Sync(Ok(node)) => self.render_template(buf, dom, node)?, + _ => todo!(), + } + } + DynamicNode::Text(text) => { + // in SSR, we are concerned that we can't hunt down the right text node since they might get merged + // if !*inner { + // write!(buf, "")?; + // } - // // todo: escape the text - // write!(buf, "{}", value)?; + // todo: escape the text + write!(buf, "{}", text.value)?; - // // if !*inner { - // write!(buf, "")?; - // // } - // } - // DynamicNode::Fragment { nodes, .. } => { - // for child in *nodes { - // self.render_template(buf, dom, child)?; - // } - // } - // DynamicNode::Component { scope, .. } => { - // let id = scope.get().unwrap(); - // let scope = dom.get_scope(id).unwrap(); - // self.render_template(buf, dom, scope.root_node())?; - // } - // DynamicNode::Placeholder(_el) => { - // write!(buf, "")?; - // } + // if !*inner { + // write!(buf, "")?; + // } + } + DynamicNode::Fragment(nodes) => { + for child in *nodes { + self.render_template(buf, dom, child)?; + } + } + + DynamicNode::Placeholder(_el) => { + // todo write a placeholder if in pre-render mode + // write!(buf, "")?; + } }, Segment::PreRendered(contents) => buf.push_str(contents), From 18d6b1ad6fedf96fd80b507fcab0adda3c726db2 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 30 Nov 2022 17:21:10 -0500 Subject: [PATCH 13/18] feat: get web working properly --- Cargo.toml | 6 +- examples/simple_todo.rs | 32 + examples/todomvc.rs | 7 +- packages/core/src/arena.rs | 32 +- packages/core/src/create.rs | 2 +- packages/core/src/diff.rs | 70 +- packages/core/src/mutations.rs | 8 +- packages/core/src/scope_arena.rs | 7 +- packages/core/src/scopes.rs | 2 +- packages/core/src/virtual_dom.rs | 30 +- packages/core/tests/kitchen_sink.rs | 2 +- packages/core/tests/miri_full_app.rs | 49 ++ packages/desktop/src/controller.rs | 8 +- packages/desktop/src/desktop_context.rs | 5 +- packages/desktop/src/lib.rs | 2 +- packages/html/src/events.rs | 174 ++--- packages/html/src/lib.rs | 4 +- packages/html/src/web_sys_bind/events.rs | 6 +- packages/interpreter/src/bindings.rs | 95 ++- packages/interpreter/src/interpreter.js | 13 +- packages/web/Cargo.toml | 2 +- packages/web/src/dom.rs | 475 +++++-------- packages/web/src/lib.rs | 186 +++-- packages/web/src/olddom.rs | 856 ----------------------- packages/web/src/rehydrate.rs | 21 +- packages/web/tests/hydrate.rs | 2 +- 26 files changed, 674 insertions(+), 1422 deletions(-) create mode 100644 examples/simple_todo.rs create mode 100644 packages/core/tests/miri_full_app.rs delete mode 100644 packages/web/src/olddom.rs diff --git a/Cargo.toml b/Cargo.toml index a523e1fca..ae0a7490c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,13 +12,13 @@ members = [ "packages/desktop", "packages/mobile", "packages/interpreter", + # "packages/tui", "packages/fermi", - "packages/tui", "packages/liveview", "packages/autofmt", "packages/rsx", - "packages/native-core", - "packages/native-core-macro", + # "packages/native-core", + # "packages/native-core-macro", # "packages/edit-stream", "docs/guide", ] diff --git a/examples/simple_todo.rs b/examples/simple_todo.rs new file mode 100644 index 000000000..454e4b508 --- /dev/null +++ b/examples/simple_todo.rs @@ -0,0 +1,32 @@ +use dioxus::prelude::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let mut idx = use_state(cx, || 0); + let onhover = |h| println!("go!"); + + cx.render(rsx! { + div { + button { onclick: move |_| idx += 1, "+" } + button { onclick: move |_| idx -= 1, "-" } + ul { + (0..**idx).map(|i| rsx! { + Child { i: i, onhover: onhover } + }) + } + } + }) +} + +#[inline_props] +fn Child<'a>(cx: Scope<'a>, i: i32, onhover: EventHandler<'a, MouseEvent>) -> Element { + cx.render(rsx! { + li { + onmouseover: move |e| onhover.call(e), + "{i}" + } + }) +} diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 71ceee8e1..f74f56063 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -57,7 +57,10 @@ pub fn app(cx: Scope<()>) -> Element { placeholder: "What needs to be done?", value: "{draft}", autofocus: "true", - oninput: move |evt| draft.set(evt.value.clone()), + oninput: move |evt| { + println!("calling oninput"); + draft.set(evt.value.clone()); + }, onkeydown: move |evt| { if evt.key() == Key::Enter && !draft.is_empty() { todos.make_mut().insert( @@ -121,6 +124,8 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { let completed = if todo.checked { "completed" } else { "" }; let editing = if **is_editing { "editing" } else { "" }; + println!("rendering todo entry"); + cx.render(rsx!{ li { class: "{completed} {editing}", diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index c27dca5ed..efc947ff3 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -13,7 +13,7 @@ pub(crate) struct ElementRef { pub path: ElementPath, // The actual template - pub template: *mut VNode<'static>, + pub template: *const VNode<'static>, } #[derive(Clone, Copy)] @@ -58,10 +58,22 @@ impl VirtualDom { } pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option { - assert_ne!(el, ElementId(0)); + if el.0 == 0 { + panic!( + "Invalid element set to 0 - {:#?}", + std::backtrace::Backtrace::force_capture() + ) + } + + println!("reclaiming {:?}", el); self.elements.try_remove(el.0) } + pub(crate) fn update_template(&mut self, el: ElementId, node: &VNode) { + let node: *const VNode = node as *const _; + self.elements[el.0].template = unsafe { std::mem::transmute(node) }; + } + // Drop a scope and all its children pub(crate) fn drop_scope(&mut self, id: ScopeId) { let scope = self.scopes.get(id.0).unwrap(); @@ -73,21 +85,15 @@ impl VirtualDom { } } - let scope = self.scopes.get(id.0).unwrap(); - - if let Some(root) = unsafe { scope.as_ref().previous_frame().try_load_node() } { - let root = unsafe { root.extend_lifetime_ref() }; - if let RenderReturn::Sync(Ok(node)) = root { - self.drop_scope_inner(node) - } - } - - let scope = self.scopes.get(id.0).unwrap().as_ref(); + let scope = self.scopes.get_mut(id.0).unwrap(); + scope.props.take(); // Drop all the hooks once the children are dropped // this means we'll drop hooks bottom-up - for hook in scope.hook_list.borrow_mut().drain(..) { + for hook in scope.hook_list.get_mut().drain(..) { + println!("dropping hook !"); drop(unsafe { BumpBox::from_raw(hook) }); + println!("hook dropped !"); } } diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index 771b1dfa6..2f11eff1f 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -129,7 +129,7 @@ impl<'b> VirtualDom { AttributeValue::Listener(_) => { self.mutations.push(NewEventListener { // all listeners start with "on" - event_name: &unbounded_name[2..], + name: &unbounded_name[2..], scope: cur_scope, id, }) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 5ed82fbcd..4ac5553b2 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -1,3 +1,5 @@ +use std::cell::Cell; + use crate::{ arena::ElementId, factory::RenderReturn, @@ -59,10 +61,17 @@ impl<'b> VirtualDom { } fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) { + println!( + "diffing node {:#?},\n\n{:#?}", + left_template.template.id, right_template.template.id + ); + if left_template.template.id != right_template.template.id { return self.light_diff_templates(left_template, right_template); } + println!("diffing attributs..."); + for (left_attr, right_attr) in left_template .dynamic_attrs .iter() @@ -73,6 +82,11 @@ impl<'b> VirtualDom { .mounted_element .set(left_attr.mounted_element.get()); + // We want to make sure anything listener that gets pulled is valid + if let AttributeValue::Listener(_) = right_attr.value { + self.update_template(left_attr.mounted_element.get(), right_template); + } + if left_attr.value != right_attr.value || left_attr.volatile { // todo: add more types of attribute values match right_attr.value { @@ -92,6 +106,8 @@ impl<'b> VirtualDom { } } + println!("diffing dyn nodes..."); + for (idx, (left_node, right_node)) in left_template .dynamic_nodes .iter() @@ -101,13 +117,24 @@ impl<'b> VirtualDom { match (left_node, right_node) { (Text(left), Text(right)) => self.diff_vtext(left, right), (Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right), + (Placeholder(left), Placeholder(right)) => { + right.set(left.get()); + } (Component(left), Component(right)) => { self.diff_vcomponent(left, right, right_template, idx) } - _ => self.replace(left_template, right_template), + (Placeholder(left), Fragment(right)) => { + self.replace_placeholder_with_nodes(left, right) + } + (Fragment(left), Placeholder(right)) => { + self.replace_nodes_with_placeholder(left, right) + } + _ => todo!(), }; } + println!("applying roots..."); + // Make sure the roots get transferred over for (left, right) in left_template .root_ids @@ -118,6 +145,30 @@ impl<'b> VirtualDom { } } + fn replace_placeholder_with_nodes(&mut self, l: &'b Cell, r: &'b [VNode<'b>]) { + let m = self.create_children(r); + let id = l.get(); + self.mutations.push(Mutation::ReplaceWith { id, m }); + self.reclaim(id); + } + + fn replace_nodes_with_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b Cell) { + // Create the placeholder first, ensuring we get a dedicated ID for the placeholder + let placeholder = self.next_element(&l[0], &[]); + r.set(placeholder); + self.mutations + .push(Mutation::CreatePlaceholder { id: placeholder }); + + // Remove the old nodes, except for onea + let first = self.replace_inner(&l[0]); + self.remove_nodes(&l[1..]); + + self.mutations + .push(Mutation::ReplaceWith { id: first, m: 1 }); + + self.try_reclaim(first); + } + fn diff_vcomponent( &mut self, left: &'b VComponent<'b>, @@ -140,6 +191,7 @@ impl<'b> VirtualDom { }; self.mutations .push(Mutation::ReplaceWith { id, m: created }); + self.drop_scope(left.scope.get().unwrap()); return; } @@ -154,15 +206,12 @@ impl<'b> VirtualDom { // If the props are static, then we try to memoize by setting the new with the old // The target scopestate still has the reference to the old props, so there's no need to update anything // This also implicitly drops the new props since they're not used - if left.static_props && unsafe { old.memoize(new.as_ref()) } { + if left.static_props && unsafe { old.as_ref().unwrap().memoize(new.as_ref()) } { return; } - // If the props are dynamic *or* the memoization failed, then we need to diff the props - // First, move over the props from the old to the new, dropping old props in the process - self.scopes[scope_id.0].props = unsafe { std::mem::transmute(new.as_ref()) }; - right.props.set(Some(new)); + self.scopes[scope_id.0].props = unsafe { std::mem::transmute(new) }; // Now run the component and diff it self.run_scope(scope_id); @@ -335,7 +384,7 @@ impl<'b> VirtualDom { }; } - fn diff_non_empty_fragment(&mut self, new: &'b [VNode<'b>], old: &'b [VNode<'b>]) { + fn diff_non_empty_fragment(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { let new_is_keyed = new[0].key.is_some(); let old_is_keyed = old[0].key.is_some(); debug_assert!( @@ -346,6 +395,9 @@ impl<'b> VirtualDom { old.iter().all(|o| o.key.is_some() == old_is_keyed), "all siblings must be keyed or all siblings must be non-keyed" ); + + println!("Diffing fragment {:?}, {:?}", old.len(), new.len()); + if new_is_keyed && old_is_keyed { self.diff_keyed_children(old, new); } else { @@ -368,7 +420,9 @@ impl<'b> VirtualDom { debug_assert!(!new.is_empty()); debug_assert!(!old.is_empty()); - match old.len().cmp(&new.len()) { + println!("Diffing non keyed children"); + + match dbg!(old.len().cmp(&new.len())) { Ordering::Greater => self.remove_nodes(&old[new.len()..]), Ordering::Less => self.create_and_insert_after(&new[old.len()..], old.last().unwrap()), Ordering::Equal => {} diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 70ed3684c..750013703 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -158,7 +158,7 @@ pub enum Mutation<'a> { /// Create a new Event Listener. NewEventListener { /// The name of the event to listen for. - event_name: &'a str, + name: &'a str, /// The ID of the node to attach the listener to. scope: ScopeId, @@ -169,11 +169,11 @@ pub enum Mutation<'a> { /// Remove an existing Event Listener. RemoveEventListener { + /// The name of the event to remove. + name: &'a str, + /// The ID of the node to remove. id: ElementId, - - /// The name of the event to remove. - event: &'a str, }, Remove { id: ElementId, diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 62f6afa33..722621ec4 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -28,7 +28,7 @@ impl VirtualDom { parent, id, height, - props, + props: Some(props), placeholder: Default::default(), node_arena_1: BumpFrame::new(50), node_arena_2: BumpFrame::new(50), @@ -86,6 +86,8 @@ impl VirtualDom { } pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn { + println!("Running scope {:?}", scope_id); + // Cycle to the next frame and then reset it // This breaks any latent references, invalidating every pointer referencing into it. // Remove all the outdated listeners @@ -100,7 +102,8 @@ impl VirtualDom { scope.hook_idx.set(0); // safety: due to how we traverse the tree, we know that the scope is not currently aliased - let props: &dyn AnyProps = mem::transmute(&*scope.props); + let props = scope.props.as_ref().unwrap().as_ref(); + let props: &dyn AnyProps = mem::transmute(props); props.render(scope).extend_lifetime() }; diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index ae944792b..776414563 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -82,7 +82,7 @@ pub struct ScopeState { pub(crate) tasks: Rc, pub(crate) spawned_tasks: HashSet, - pub(crate) props: Box>, + pub(crate) props: Option>>, pub(crate) placeholder: Cell>, } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 31a693b9b..77619e674 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -362,6 +362,8 @@ impl VirtualDom { let target_path = el_ref.path; for (idx, attr) in template.dynamic_attrs.iter().enumerate() { + println!("{:?} \n {:?} \n {:?}", attr, name, element); + let this_path = template.template.attr_paths[idx]; // listeners are required to be prefixed with "on", but they come back to the virtualdom with that missing @@ -383,6 +385,8 @@ impl VirtualDom { } } + println!("calling listeners: {:?}", listeners); + // Now that we've accumulated all the parent attributes for the target element, call them in reverse order // We check the bubble state between each call to see if the event has been stopped from bubbling for listener in listeners.drain(..).rev() { @@ -399,6 +403,8 @@ impl VirtualDom { parent_path = template.parent.and_then(|id| self.elements.get(id.0)); } + + println!("all listeners exhausted"); } /// Wait for the scheduler to have any work. @@ -448,12 +454,15 @@ impl VirtualDom { } } - /// Swap the current mutations with a new - fn finalize(&mut self) -> Mutations { - // todo: make this a routine - let mut out = Mutations::default(); - std::mem::swap(&mut self.mutations, &mut out); - out + /// Process all events in the queue until there are no more left + pub fn process_events(&mut self) { + while let Ok(Some(msg)) = self.rx.try_next() { + match msg { + SchedulerMsg::Immediate(id) => self.mark_dirty(id), + SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task), + SchedulerMsg::SuspenseNotified(id) => self.handle_suspense_wakeup(id), + } + } } /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom from scratch. @@ -515,6 +524,7 @@ impl VirtualDom { /// /// If no suspense trees are present pub async fn render_with_deadline(&mut self, deadline: impl Future) -> Mutations { + println!("render with deadline"); pin_mut!(deadline); loop { @@ -600,6 +610,14 @@ impl VirtualDom { } } } + + /// Swap the current mutations with a new + fn finalize(&mut self) -> Mutations { + // todo: make this a routine + let mut out = Mutations::default(); + std::mem::swap(&mut self.mutations, &mut out); + out + } } impl Drop for VirtualDom { diff --git a/packages/core/tests/kitchen_sink.rs b/packages/core/tests/kitchen_sink.rs index 7071cb120..7f719f772 100644 --- a/packages/core/tests/kitchen_sink.rs +++ b/packages/core/tests/kitchen_sink.rs @@ -70,7 +70,7 @@ fn dual_stream() { [ LoadTemplate { name: "template", index: 0, id: ElementId(1) }, SetAttribute { name: "class", value: "123", id: ElementId(1), ns: None }, - NewEventListener { event_name: "click", scope: ScopeId(0), id: ElementId(1) }, + NewEventListener { name: "click", scope: ScopeId(0), id: ElementId(1) }, HydrateText { path: &[0, 0], value: "123", id: ElementId(2) }, AppendChildren { m: 1 } ], diff --git a/packages/core/tests/miri_full_app.rs b/packages/core/tests/miri_full_app.rs new file mode 100644 index 000000000..6cd3d2bd8 --- /dev/null +++ b/packages/core/tests/miri_full_app.rs @@ -0,0 +1,49 @@ +use dioxus::prelude::*; +use dioxus_core::ElementId; +use std::rc::Rc; + +#[test] +fn miri_rollover() { + let mut dom = VirtualDom::new(app); + + _ = dom.rebuild(); + + for x in 0..3 { + dom.handle_event("click", Rc::new(MouseData::default()), ElementId(2), true); + dom.process_events(); + dom.render_immediate(); + } +} + +fn app(cx: Scope) -> Element { + let mut idx = use_state(cx, || 0); + let onhover = |h| println!("go!"); + + cx.render(rsx! { + div { + button { + onclick: move |_| { + idx += 1; + println!("Clicked"); + }, + "+" + } + button { onclick: move |_| idx -= 1, "-" } + ul { + (0..**idx).map(|i| rsx! { + Child { i: i, onhover: onhover } + }) + } + } + }) +} + +#[inline_props] +fn Child<'a>(cx: Scope<'a>, i: i32, onhover: EventHandler<'a, MouseEvent>) -> Element { + cx.render(rsx! { + li { + onmouseover: move |e| onhover.call(e), + "{i}" + } + }) +} diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs index 3aff11352..19ac205b4 100644 --- a/packages/desktop/src/controller.rs +++ b/packages/desktop/src/controller.rs @@ -56,7 +56,7 @@ impl DesktopController { let mut queue = edit_queue.lock().unwrap(); queue.push(serde_json::to_string(&edits.template_mutations).unwrap()); queue.push(serde_json::to_string(&edits.edits).unwrap()); - proxy.send_event(UserWindowEvent::Update).unwrap(); + proxy.send_event(UserWindowEvent::EditsReady).unwrap(); } loop { @@ -67,7 +67,7 @@ impl DesktopController { let name = value.event.clone(); let el_id = ElementId(value.mounted_dom_id); if let Some(evt) = decode_event(value) { - dom.handle_event(&name, evt, el_id, true); + dom.handle_event(&name, evt, el_id, dioxus_html::events::event_bubbles(&name)); } } } @@ -81,7 +81,7 @@ impl DesktopController { let mut queue = edit_queue.lock().unwrap(); queue.push(serde_json::to_string(&muts.template_mutations).unwrap()); queue.push(serde_json::to_string(&muts.edits).unwrap()); - let _ = proxy.send_event(UserWindowEvent::Update); + let _ = proxy.send_event(UserWindowEvent::EditsReady); } } }) @@ -116,6 +116,8 @@ impl DesktopController { let (_id, view) = self.webviews.iter_mut().next().unwrap(); + println!("processing pending edits {:?}", new_queue.len()); + for edit in new_queue.drain(..) { view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit)) .unwrap(); diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 757a35360..71357e0ab 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -161,7 +161,8 @@ impl DesktopContext { #[derive(Debug)] pub enum UserWindowEvent { - Update, + EditsReady, + Initialize, CloseWindow, DragWindow, @@ -213,7 +214,7 @@ pub(super) fn handler( println!("user_event: {:?}", user_event); match user_event { - Update => desktop.try_load_ready_webviews(), + Initialize | EditsReady => desktop.try_load_ready_webviews(), CloseWindow => *control_flow = ControlFlow::Exit, DragWindow => { // if the drag_window has any errors, we don't do anything diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index cbdf3a4c4..a729a2f6f 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -154,7 +154,7 @@ pub fn launch_with_props(root: Component

, props: P, mut cf "initialize" => { is_ready.store(true, std::sync::atomic::Ordering::Relaxed); println!("initializing..."); - let _ = proxy.send_event(UserWindowEvent::Update); + let _ = proxy.send_event(UserWindowEvent::EditsReady); } "browser_open" => { let data = message.params(); diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index 7e903b99e..5107e2cda 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -60,90 +60,90 @@ pub use touch::*; pub use transition::*; pub use wheel::*; -// pub fn event_bubbles(evt: &str) -> bool { -// match evt { -// "copy" => true, -// "cut" => true, -// "paste" => true, -// "compositionend" => true, -// "compositionstart" => true, -// "compositionupdate" => true, -// "keydown" => true, -// "keypress" => true, -// "keyup" => true, -// "focus" => false, -// "focusout" => true, -// "focusin" => true, -// "blur" => false, -// "change" => true, -// "input" => true, -// "invalid" => true, -// "reset" => true, -// "submit" => true, -// "click" => true, -// "contextmenu" => true, -// "doubleclick" => true, -// "dblclick" => true, -// "drag" => true, -// "dragend" => true, -// "dragenter" => false, -// "dragexit" => false, -// "dragleave" => true, -// "dragover" => true, -// "dragstart" => true, -// "drop" => true, -// "mousedown" => true, -// "mouseenter" => false, -// "mouseleave" => false, -// "mousemove" => true, -// "mouseout" => true, -// "scroll" => false, -// "mouseover" => true, -// "mouseup" => true, -// "pointerdown" => true, -// "pointermove" => true, -// "pointerup" => true, -// "pointercancel" => true, -// "gotpointercapture" => true, -// "lostpointercapture" => true, -// "pointerenter" => false, -// "pointerleave" => false, -// "pointerover" => true, -// "pointerout" => true, -// "select" => true, -// "touchcancel" => true, -// "touchend" => true, -// "touchmove" => true, -// "touchstart" => true, -// "wheel" => true, -// "abort" => false, -// "canplay" => false, -// "canplaythrough" => false, -// "durationchange" => false, -// "emptied" => false, -// "encrypted" => true, -// "ended" => false, -// "error" => false, -// "loadeddata" => false, -// "loadedmetadata" => false, -// "loadstart" => false, -// "pause" => false, -// "play" => false, -// "playing" => false, -// "progress" => false, -// "ratechange" => false, -// "seeked" => false, -// "seeking" => false, -// "stalled" => false, -// "suspend" => false, -// "timeupdate" => false, -// "volumechange" => false, -// "waiting" => false, -// "animationstart" => true, -// "animationend" => true, -// "animationiteration" => true, -// "transitionend" => true, -// "toggle" => true, -// _ => panic!("unsupported event type {:?}", evt), -// } -// } +pub fn event_bubbles(evt: &str) -> bool { + match evt { + "copy" => true, + "cut" => true, + "paste" => true, + "compositionend" => true, + "compositionstart" => true, + "compositionupdate" => true, + "keydown" => true, + "keypress" => true, + "keyup" => true, + "focus" => false, + "focusout" => true, + "focusin" => true, + "blur" => false, + "change" => true, + "input" => true, + "invalid" => true, + "reset" => true, + "submit" => true, + "click" => true, + "contextmenu" => true, + "doubleclick" => true, + "dblclick" => true, + "drag" => true, + "dragend" => true, + "dragenter" => false, + "dragexit" => false, + "dragleave" => true, + "dragover" => true, + "dragstart" => true, + "drop" => true, + "mousedown" => true, + "mouseenter" => false, + "mouseleave" => false, + "mousemove" => true, + "mouseout" => true, + "scroll" => false, + "mouseover" => true, + "mouseup" => true, + "pointerdown" => true, + "pointermove" => true, + "pointerup" => true, + "pointercancel" => true, + "gotpointercapture" => true, + "lostpointercapture" => true, + "pointerenter" => false, + "pointerleave" => false, + "pointerover" => true, + "pointerout" => true, + "select" => true, + "touchcancel" => true, + "touchend" => true, + "touchmove" => true, + "touchstart" => true, + "wheel" => true, + "abort" => false, + "canplay" => false, + "canplaythrough" => false, + "durationchange" => false, + "emptied" => false, + "encrypted" => true, + "ended" => false, + "error" => false, + "loadeddata" => false, + "loadedmetadata" => false, + "loadstart" => false, + "pause" => false, + "play" => false, + "playing" => false, + "progress" => false, + "ratechange" => false, + "seeked" => false, + "seeking" => false, + "stalled" => false, + "suspend" => false, + "timeupdate" => false, + "volumechange" => false, + "waiting" => false, + "animationstart" => true, + "animationend" => true, + "animationiteration" => true, + "transitionend" => true, + "toggle" => true, + _ => true, + } +} diff --git a/packages/html/src/lib.rs b/packages/html/src/lib.rs index 871d78e3f..325116a34 100644 --- a/packages/html/src/lib.rs +++ b/packages/html/src/lib.rs @@ -18,8 +18,8 @@ pub mod events; pub mod geometry; mod global_attributes; pub mod input_data; -// #[cfg(feature = "wasm-bind")] -// mod web_sys_bind; +#[cfg(feature = "wasm-bind")] +mod web_sys_bind; pub use elements::*; pub use events::*; diff --git a/packages/html/src/web_sys_bind/events.rs b/packages/html/src/web_sys_bind/events.rs index 197eed737..5f0e5b2a3 100644 --- a/packages/html/src/web_sys_bind/events.rs +++ b/packages/html/src/web_sys_bind/events.rs @@ -1,9 +1,9 @@ -use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint}; -use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton}; -use crate::on::{ +use crate::events::{ AnimationData, CompositionData, KeyboardData, MouseData, PointerData, TouchData, TransitionData, WheelData, }; +use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint}; +use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton}; use keyboard_types::{Code, Key, Modifiers}; use std::convert::TryInto; use std::str::FromStr; diff --git a/packages/interpreter/src/bindings.rs b/packages/interpreter/src/bindings.rs index 0587368b5..708842e23 100644 --- a/packages/interpreter/src/bindings.rs +++ b/packages/interpreter/src/bindings.rs @@ -2,7 +2,7 @@ use js_sys::Function; use wasm_bindgen::prelude::*; -use web_sys::{Element, Node}; +use web_sys::Element; #[wasm_bindgen(module = "/src/interpreter.js")] extern "C" { @@ -12,86 +12,83 @@ extern "C" { pub fn new(arg: Element) -> Interpreter; #[wasm_bindgen(method)] - pub fn SetNode(this: &Interpreter, id: usize, node: Node); + pub fn AppendChildren(this: &Interpreter, many: u32); #[wasm_bindgen(method)] - pub fn AppendChildren(this: &Interpreter, root: Option, children: Vec); + pub fn AssignId(this: &Interpreter, path: &[u8], id: u32); #[wasm_bindgen(method)] - pub fn ReplaceWith(this: &Interpreter, root: Option, nodes: Vec); + pub fn CreateElement(this: &Interpreter, tag: &str); #[wasm_bindgen(method)] - pub fn InsertAfter(this: &Interpreter, root: Option, nodes: Vec); + pub fn CreateElementNs(this: &Interpreter, tag: &str, ns: &str); #[wasm_bindgen(method)] - pub fn InsertBefore(this: &Interpreter, root: Option, nodes: Vec); + pub fn CreatePlaceholder(this: &Interpreter, id: u32); #[wasm_bindgen(method)] - pub fn Remove(this: &Interpreter, root: Option); + pub fn CreateStaticPlaceholder(this: &Interpreter); #[wasm_bindgen(method)] - pub fn CreateTextNode(this: &Interpreter, text: JsValue, root: Option); + pub fn CreateTextPlaceholder(this: &Interpreter); #[wasm_bindgen(method)] - pub fn CreateElement(this: &Interpreter, tag: &str, root: Option, children: u32); + pub fn CreateStaticText(this: &Interpreter, value: &str); #[wasm_bindgen(method)] - pub fn CreateElementNs( - this: &Interpreter, - tag: &str, - root: Option, - ns: &str, - children: u32, - ); + pub fn CreateTextNode(this: &Interpreter, value: JsValue, id: u32); #[wasm_bindgen(method)] - pub fn CreatePlaceholder(this: &Interpreter, root: Option); + pub fn HydrateText(this: &Interpreter, path: &[u8], value: &str, id: u32); + + #[wasm_bindgen(method)] + pub fn LoadTemplate(this: &Interpreter, name: &str, index: u32, id: u32); + + #[wasm_bindgen(method)] + pub fn ReplaceWith(this: &Interpreter, id: u32, m: u32); + + #[wasm_bindgen(method)] + pub fn ReplacePlaceholder(this: &Interpreter, path: &[u8], m: u32); + + #[wasm_bindgen(method)] + pub fn InsertAfter(this: &Interpreter, id: u32, n: u32); + + #[wasm_bindgen(method)] + pub fn InsertBefore(this: &Interpreter, id: u32, n: u32); + + #[wasm_bindgen(method)] + pub fn SaveTemplate(this: &Interpreter, name: &str, m: u32); + + #[wasm_bindgen(method)] + pub fn SetAttribute(this: &Interpreter, id: u32, name: &str, value: JsValue, ns: Option<&str>); + + #[wasm_bindgen(method)] + pub fn SetStaticAttribute(this: &Interpreter, name: &str, value: JsValue, ns: Option<&str>); + + #[wasm_bindgen(method)] + pub fn SetBoolAttribute(this: &Interpreter, id: u32, name: &str, value: bool); + + #[wasm_bindgen(method)] + pub fn SetText(this: &Interpreter, id: u32, text: JsValue); #[wasm_bindgen(method)] pub fn NewEventListener( this: &Interpreter, name: &str, - root: Option, + id: u32, handler: &Function, bubbles: bool, ); #[wasm_bindgen(method)] - pub fn RemoveEventListener(this: &Interpreter, root: Option, name: &str, bubbles: bool); + pub fn RemoveEventListener(this: &Interpreter, name: &str, id: u32); #[wasm_bindgen(method)] - pub fn SetText(this: &Interpreter, root: Option, text: JsValue); + pub fn RemoveAttribute(this: &Interpreter, id: u32, field: &str, ns: Option<&str>); #[wasm_bindgen(method)] - pub fn SetAttribute( - this: &Interpreter, - root: Option, - field: &str, - value: JsValue, - ns: Option<&str>, - ); + pub fn Remove(this: &Interpreter, id: u32); #[wasm_bindgen(method)] - pub fn RemoveAttribute(this: &Interpreter, root: Option, field: &str, ns: Option<&str>); - - #[wasm_bindgen(method)] - pub fn CloneNode(this: &Interpreter, root: Option, new_id: u64); - - #[wasm_bindgen(method)] - pub fn CloneNodeChildren(this: &Interpreter, root: Option, new_ids: Vec); - - #[wasm_bindgen(method)] - pub fn FirstChild(this: &Interpreter); - - #[wasm_bindgen(method)] - pub fn NextSibling(this: &Interpreter); - - #[wasm_bindgen(method)] - pub fn ParentNode(this: &Interpreter); - - #[wasm_bindgen(method)] - pub fn StoreWithId(this: &Interpreter, id: u64); - - #[wasm_bindgen(method)] - pub fn SetLastNode(this: &Interpreter, id: u64); + pub fn PushRoot(this: &Interpreter, id: u32); } diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index bcfd77865..9bede888c 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -265,7 +265,14 @@ export class Interpreter { SaveTemplate(name, m) { this.templates[name] = this.stack.splice(this.stack.length - m); } + CreateStaticText(text) { + this.CreateTextNode(text); + } + CreateTextPlaceholder() { + this.CreateRawText("placeholder"); + } handleEdit(edit) { + console.log(edit); switch (edit.type) { case "AppendChildren": this.AppendChildren(edit.m); @@ -283,13 +290,13 @@ export class Interpreter { this.CreatePlaceholder(edit.id); break; case "CreateStaticText": - this.CreateTextNode(edit.value); + this.CreateStaticText(edit.value) break; case "CreateStaticPlaceholder": this.CreateStaticPlaceholder(); break; case "CreateTextPlaceholder": - this.CreateRawText("placeholder"); + this.CreateTextPlaceholder(); break; case "CreateStaticText": this.CreateRawText(edit.value); @@ -301,7 +308,7 @@ export class Interpreter { this.HydrateText(edit.path, edit.value, edit.id); break; case "LoadTemplate": - this.LoadTemplate(edit.name, edit.index); + this.LoadTemplate(edit.name, edit.index, edit.id); break; case "SaveTemplate": this.SaveTemplate(edit.name, edit.m); diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 7d8e6a3cc..5be66b63d 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -77,7 +77,7 @@ features = [ ] [features] -default = ["panic_hook", "hydrate"] +default = ["panic_hook"] panic_hook = ["console_error_panic_hook"] hydrate = [] diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 98e89ee47..20e50c0fd 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -7,11 +7,12 @@ //! - tests to ensure dyn_into works for various event types. //! - Partial delegation?> -use dioxus_core::{DomEdit, ElementId, SchedulerMsg, UserEvent}; -use dioxus_html::event_bubbles; +use dioxus_core::{ElementId, Mutation, Mutations}; +use dioxus_html::{event_bubbles, CompositionData, FormData}; use dioxus_interpreter_js::Interpreter; +use futures_channel::mpsc; use js_sys::Function; -use std::{any::Any, rc::Rc, sync::Arc}; +use std::{any::Any, rc::Rc}; use wasm_bindgen::{closure::Closure, JsCast, JsValue}; use web_sys::{Document, Element, Event, HtmlElement}; @@ -26,68 +27,26 @@ pub struct WebsysDom { } impl WebsysDom { - pub fn new(cfg: Config, sender_callback: Rc) -> Self { + pub fn new(cfg: Config, event_channel: mpsc::UnboundedSender) -> Self { // eventually, we just want to let the interpreter do all the work of decoding events into our event type let callback: Box = Box::new(move |event: &web_sys::Event| { - let mut target = event - .target() - .expect("missing target") - .dyn_into::() - .expect("not a valid element"); + _ = event_channel.unbounded_send(event.clone()); - let typ = event.type_(); + // if let Ok(synthetic_event) = decoded { + // // Try to prevent default if the attribute is set + // if let Some(node) = target.dyn_ref::() { + // if let Some(name) = node.get_attribute("dioxus-prevent-default") { + // if name == synthetic_event.name + // || name.trim_start_matches("on") == synthetic_event.name + // { + // log::trace!("Preventing default"); + // event.prevent_default(); + // } + // } + // } - let decoded: anyhow::Result = loop { - match target.get_attribute("data-dioxus-id").map(|f| f.parse()) { - Some(Ok(id)) => { - break Ok(UserEvent { - name: event_name_from_typ(&typ), - data: virtual_event_from_websys_event(event.clone(), target.clone()), - element: Some(ElementId(id)), - scope_id: None, - priority: dioxus_core::EventPriority::Medium, - bubbles: event.bubbles(), - }); - } - Some(Err(e)) => { - break Err(e.into()); - } - None => { - // walk the tree upwards until we actually find an event target - if let Some(parent) = target.parent_element() { - target = parent; - } else { - break Ok(UserEvent { - name: event_name_from_typ(&typ), - data: virtual_event_from_websys_event( - event.clone(), - target.clone(), - ), - element: None, - scope_id: None, - priority: dioxus_core::EventPriority::Low, - bubbles: event.bubbles(), - }); - } - } - } - }; - - if let Ok(synthetic_event) = decoded { - // Try to prevent default if the attribute is set - if let Some(node) = target.dyn_ref::() { - if let Some(name) = node.get_attribute("dioxus-prevent-default") { - if name == synthetic_event.name - || name.trim_start_matches("on") == synthetic_event.name - { - log::trace!("Preventing default"); - event.prevent_default(); - } - } - } - - sender_callback.as_ref()(SchedulerMsg::Event(synthetic_event)) - } + // sender_callback.as_ref()(SchedulerMsg::Event(synthetic_event)) + // } }); // a match here in order to avoid some error during runtime browser test @@ -104,75 +63,56 @@ impl WebsysDom { } } - pub fn apply_edits(&mut self, mut edits: Vec) { + pub fn apply_edits(&mut self, mut edits: Vec) { + use Mutation::*; + let i = &self.interpreter; + for edit in edits.drain(..) { match edit { - DomEdit::AppendChildren { root, children } => { - self.interpreter.AppendChildren(root, children); - } - DomEdit::ReplaceWith { root, nodes } => self.interpreter.ReplaceWith(root, nodes), - DomEdit::InsertAfter { root, nodes } => self.interpreter.InsertAfter(root, nodes), - DomEdit::InsertBefore { root, nodes } => self.interpreter.InsertBefore(root, nodes), - DomEdit::Remove { root } => self.interpreter.Remove(root), - - DomEdit::CreateElement { - root, - tag, - children, - } => self.interpreter.CreateElement(tag, root, children), - DomEdit::CreateElementNs { - root, - tag, + AppendChildren { m } => i.AppendChildren(m as u32), + AssignId { path, id } => i.AssignId(path, id.0 as u32), + CreateElement { name } => i.CreateElement(name), + CreateElementNamespace { name, namespace } => i.CreateElementNs(name, namespace), + CreatePlaceholder { id } => i.CreatePlaceholder(id.0 as u32), + CreateStaticPlaceholder => i.CreateStaticPlaceholder(), + CreateTextPlaceholder => i.CreateTextPlaceholder(), + CreateStaticText { value } => i.CreateStaticText(value), + CreateTextNode { value, id } => i.CreateTextNode(value.into(), id.0 as u32), + HydrateText { path, value, id } => i.HydrateText(path, value, id.0 as u32), + LoadTemplate { name, index, id } => i.LoadTemplate(name, index as u32, id.0 as u32), + ReplaceWith { id, m } => i.ReplaceWith(id.0 as u32, m as u32), + ReplacePlaceholder { path, m } => i.ReplacePlaceholder(path, m as u32), + InsertAfter { id, m } => i.InsertAfter(id.0 as u32, m as u32), + InsertBefore { id, m } => i.InsertBefore(id.0 as u32, m as u32), + SaveTemplate { name, m } => i.SaveTemplate(name, m as u32), + SetAttribute { + name, + value, + id, ns, - children, - } => self.interpreter.CreateElementNs(tag, root, ns, children), - DomEdit::CreatePlaceholder { root } => self.interpreter.CreatePlaceholder(root), - DomEdit::NewEventListener { - event_name, root, .. - } => { + } => i.SetAttribute(id.0 as u32, name, value.into(), ns), + SetStaticAttribute { name, value, ns } => { + i.SetStaticAttribute(name, value.into(), ns) + } + SetBoolAttribute { name, value, id } => { + i.SetBoolAttribute(id.0 as u32, name, value) + } + SetText { value, id } => i.SetText(id.0 as u32, value.into()), + NewEventListener { name, scope, id } => { let handler: &Function = self.handler.as_ref().unchecked_ref(); self.interpreter.NewEventListener( - event_name, - root, + name, + id.0 as u32, handler, - event_bubbles(event_name), + event_bubbles(&name[2..]), ); } - - DomEdit::RemoveEventListener { root, event } => self - .interpreter - .RemoveEventListener(root, event, event_bubbles(event)), - - DomEdit::RemoveAttribute { root, name, ns } => { - self.interpreter.RemoveAttribute(root, name, ns) - } - - DomEdit::CreateTextNode { text, root } => { - let text = JsValue::from_str(text); - self.interpreter.CreateTextNode(text, root) - } - DomEdit::SetText { root, text } => { - let text = JsValue::from_str(text); - self.interpreter.SetText(root, text) - } - DomEdit::SetAttribute { - root, - field, - value, - ns, - } => { - let value = JsValue::from_str(&value.to_string()); - self.interpreter.SetAttribute(root, field, value, ns) - } - DomEdit::CloneNode { id, new_id } => self.interpreter.CloneNode(id, new_id), - DomEdit::CloneNodeChildren { id, new_ids } => { - self.interpreter.CloneNodeChildren(id, new_ids) - } - DomEdit::FirstChild {} => self.interpreter.FirstChild(), - DomEdit::NextSibling {} => self.interpreter.NextSibling(), - DomEdit::ParentNode {} => self.interpreter.ParentNode(), - DomEdit::StoreWithId { id } => self.interpreter.StoreWithId(id), - DomEdit::SetLastNode { id } => self.interpreter.SetLastNode(id), + RemoveEventListener { name, id } => i.RemoveEventListener(name, id.0 as u32), + Remove { id } => i.Remove(id.0 as u32), + PushRoot { id } => i.PushRoot(id.0 as u32), + // Mutation::RemoveEventListener { root, name: event } => self + // .interpreter + // .RemoveEventListener(root, event, event_bubbles(event)), } } } @@ -187,129 +127,54 @@ unsafe impl Sync for DioxusWebsysEvent {} // todo: some of these events are being casted to the wrong event type. // We need tests that simulate clicks/etc and make sure every event type works. -fn virtual_event_from_websys_event( - event: web_sys::Event, - target: Element, -) -> Arc { - use dioxus_html::on::*; +pub fn virtual_event_from_websys_event(event: web_sys::Event, target: Element) -> Rc { + use dioxus_html::events::*; match event.type_().as_str() { - "copy" | "cut" | "paste" => Arc::new(ClipboardData {}), + "copy" | "cut" | "paste" => Rc::new(ClipboardData {}), "compositionend" | "compositionstart" | "compositionupdate" => { - let evt: &web_sys::CompositionEvent = event.dyn_ref().unwrap(); - Arc::new(CompositionData { - data: evt.data().unwrap_or_default(), - }) + make_composition_event(&event) } - "keydown" | "keypress" | "keyup" => Arc::new(KeyboardData::from(event)), - "focus" | "blur" | "focusout" | "focusin" => Arc::new(FocusData {}), + "keydown" | "keypress" | "keyup" => Rc::new(KeyboardData::from(event)), + "focus" | "blur" | "focusout" | "focusin" => Rc::new(FocusData {}), - // todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy - // don't have a good solution with the serialized event problem - "change" | "input" | "invalid" | "reset" | "submit" => { - let value: String = target - .dyn_ref() - .map(|input: &web_sys::HtmlInputElement| { - // todo: special case more input types - match input.type_().as_str() { - "checkbox" => { - match input.checked() { - true => "true".to_string(), - false => "false".to_string(), - } - }, - _ => { - input.value() - } - } - }) - .or_else(|| { - target - .dyn_ref() - .map(|input: &web_sys::HtmlTextAreaElement| input.value()) - }) - // select elements are NOT input events - because - why woudn't they be?? - .or_else(|| { - target - .dyn_ref() - .map(|input: &web_sys::HtmlSelectElement| input.value()) - }) - .or_else(|| { - target - .dyn_ref::() - .unwrap() - .text_content() - }) - .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); + "change" | "input" | "invalid" | "reset" | "submit" => read_input_to_data(target), - let mut values = std::collections::HashMap::new(); - - // try to fill in form values - if let Some(form) = target.dyn_ref::() { - let elements = form.elements(); - for x in 0..elements.length() { - let element = elements.item(x).unwrap(); - if let Some(name) = element.get_attribute("name") { - let value: Option = element - .dyn_ref() - .map(|input: &web_sys::HtmlInputElement| { - match input.type_().as_str() { - "checkbox" => { - match input.checked() { - true => Some("true".to_string()), - false => Some("false".to_string()), - } - }, - "radio" => { - match input.checked() { - true => Some(input.value()), - false => None, - } - } - _ => Some(input.value()) - } - }) - .or_else(|| element.dyn_ref().map(|input: &web_sys::HtmlTextAreaElement| Some(input.value()))) - .or_else(|| element.dyn_ref().map(|input: &web_sys::HtmlSelectElement| Some(input.value()))) - .or_else(|| Some(element.dyn_ref::().unwrap().text_content())) - .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); - if let Some(value) = value { - values.insert(name, value); - } - } - } - } - - Arc::new(FormData { value, values }) - } "click" | "contextmenu" | "dblclick" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit" | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter" | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => { - Arc::new(MouseData::from(event)) + Rc::new(MouseData::from(event)) } "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { - Arc::new(PointerData::from(event)) + Rc::new(PointerData::from(event)) } - "select" => Arc::new(SelectionData {}), - "touchcancel" | "touchend" | "touchmove" | "touchstart" => Arc::new(TouchData::from(event)), + "select" => Rc::new(SelectionData {}), + "touchcancel" | "touchend" | "touchmove" | "touchstart" => Rc::new(TouchData::from(event)), - "scroll" => Arc::new(()), - "wheel" => Arc::new(WheelData::from(event)), + "scroll" => Rc::new(()), + "wheel" => Rc::new(WheelData::from(event)), "animationstart" | "animationend" | "animationiteration" => { - Arc::new(AnimationData::from(event)) + Rc::new(AnimationData::from(event)) } - "transitionend" => Arc::new(TransitionData::from(event)), + "transitionend" => Rc::new(TransitionData::from(event)), "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted" | "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play" | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" - | "timeupdate" | "volumechange" | "waiting" => Arc::new(MediaData {}), - "toggle" => Arc::new(ToggleData {}), + | "timeupdate" | "volumechange" | "waiting" => Rc::new(MediaData {}), + "toggle" => Rc::new(ToggleData {}), - _ => Arc::new(()), + _ => Rc::new(()), } } +fn make_composition_event(event: &Event) -> Rc { + let evt: &web_sys::CompositionEvent = event.dyn_ref().unwrap(); + Rc::new(CompositionData { + data: evt.data().unwrap_or_default(), + }) +} + pub(crate) fn load_document() -> Document { web_sys::window() .expect("should have access to the Window") @@ -317,92 +182,86 @@ pub(crate) fn load_document() -> Document { .expect("should have access to the Document") } -fn event_name_from_typ(typ: &str) -> &'static str { - match typ { - "copy" => "copy", - "cut" => "cut", - "paste" => "paste", - "compositionend" => "compositionend", - "compositionstart" => "compositionstart", - "compositionupdate" => "compositionupdate", - "keydown" => "keydown", - "keypress" => "keypress", - "keyup" => "keyup", - "focus" => "focus", - "focusout" => "focusout", - "focusin" => "focusin", - "blur" => "blur", - "change" => "change", - "input" => "input", - "invalid" => "invalid", - "reset" => "reset", - "submit" => "submit", - "click" => "click", - "contextmenu" => "contextmenu", - "doubleclick" => "doubleclick", - "dblclick" => "dblclick", - "drag" => "drag", - "dragend" => "dragend", - "dragenter" => "dragenter", - "dragexit" => "dragexit", - "dragleave" => "dragleave", - "dragover" => "dragover", - "dragstart" => "dragstart", - "drop" => "drop", - "mousedown" => "mousedown", - "mouseenter" => "mouseenter", - "mouseleave" => "mouseleave", - "mousemove" => "mousemove", - "mouseout" => "mouseout", - "mouseover" => "mouseover", - "mouseup" => "mouseup", - "pointerdown" => "pointerdown", - "pointermove" => "pointermove", - "pointerup" => "pointerup", - "pointercancel" => "pointercancel", - "gotpointercapture" => "gotpointercapture", - "lostpointercapture" => "lostpointercapture", - "pointerenter" => "pointerenter", - "pointerleave" => "pointerleave", - "pointerover" => "pointerover", - "pointerout" => "pointerout", - "select" => "select", - "touchcancel" => "touchcancel", - "touchend" => "touchend", - "touchmove" => "touchmove", - "touchstart" => "touchstart", - "scroll" => "scroll", - "wheel" => "wheel", - "animationstart" => "animationstart", - "animationend" => "animationend", - "animationiteration" => "animationiteration", - "transitionend" => "transitionend", - "abort" => "abort", - "canplay" => "canplay", - "canplaythrough" => "canplaythrough", - "durationchange" => "durationchange", - "emptied" => "emptied", - "encrypted" => "encrypted", - "ended" => "ended", - "error" => "error", - "loadeddata" => "loadeddata", - "loadedmetadata" => "loadedmetadata", - "loadstart" => "loadstart", - "pause" => "pause", - "play" => "play", - "playing" => "playing", - "progress" => "progress", - "ratechange" => "ratechange", - "seeked" => "seeked", - "seeking" => "seeking", - "stalled" => "stalled", - "suspend" => "suspend", - "timeupdate" => "timeupdate", - "volumechange" => "volumechange", - "waiting" => "waiting", - "toggle" => "toggle", - a => { - panic!("unsupported event type {:?}", a); +fn read_input_to_data(target: Element) -> Rc { + // todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy + // don't have a good solution with the serialized event problem + + let value: String = target + .dyn_ref() + .map(|input: &web_sys::HtmlInputElement| { + // todo: special case more input types + match input.type_().as_str() { + "checkbox" => { + match input.checked() { + true => "true".to_string(), + false => "false".to_string(), + } + }, + _ => { + input.value() + } + } + }) + .or_else(|| { + target + .dyn_ref() + .map(|input: &web_sys::HtmlTextAreaElement| input.value()) + }) + // select elements are NOT input events - because - why woudn't they be?? + .or_else(|| { + target + .dyn_ref() + .map(|input: &web_sys::HtmlSelectElement| input.value()) + }) + .or_else(|| { + target + .dyn_ref::() + .unwrap() + .text_content() + }) + .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); + + let mut values = std::collections::HashMap::new(); + + // try to fill in form values + if let Some(form) = target.dyn_ref::() { + let elements = form.elements(); + for x in 0..elements.length() { + let element = elements.item(x).unwrap(); + if let Some(name) = element.get_attribute("name") { + let value: Option = element + .dyn_ref() + .map(|input: &web_sys::HtmlInputElement| { + match input.type_().as_str() { + "checkbox" => { + match input.checked() { + true => Some("true".to_string()), + false => Some("false".to_string()), + } + }, + "radio" => { + match input.checked() { + true => Some(input.value()), + false => None, + } + } + _ => Some(input.value()) + } + }) + .or_else(|| element.dyn_ref().map(|input: &web_sys::HtmlTextAreaElement| Some(input.value()))) + .or_else(|| element.dyn_ref().map(|input: &web_sys::HtmlSelectElement| Some(input.value()))) + .or_else(|| Some(element.dyn_ref::().unwrap().text_content())) + .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); + if let Some(value) = value { + values.insert(name, value); + } + } } } + + Rc::new(FormData { + value, + values, + files: None, + }) } diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 4c0831ef5..5b33bb9a4 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -54,21 +54,23 @@ // - Do the VDOM work during the idlecallback // - Do DOM work in the next requestAnimationFrame callback -use std::rc::Rc; +use std::{rc::Rc, time::Duration}; pub use crate::cfg::Config; +use crate::dom::virtual_event_from_websys_event; pub use crate::util::use_eval; -use dioxus_core::prelude::Component; -use dioxus_core::SchedulerMsg; -use dioxus_core::VirtualDom; +use dioxus_core::{Element, ElementId, Scope, VirtualDom}; +use futures_util::{pin_mut, FutureExt, StreamExt}; +use gloo_timers::future::sleep; +use web_sys::Event; mod cache; mod cfg; mod dom; -#[cfg(any(feature = "hot-reload", debug_assertions))] -mod hot_reload; -#[cfg(feature = "hydrate")] -mod rehydrate; +// #[cfg(any(feature = "hot-reload", debug_assertions))] +// mod hot_reload; +// #[cfg(feature = "hydrate")] +// mod rehydrate; // mod ric_raf; mod util; @@ -92,7 +94,7 @@ mod util; /// render!(div {"hello world"}) /// } /// ``` -pub fn launch(root_component: Component) { +pub fn launch(root_component: fn(Scope) -> Element) { launch_with_props(root_component, (), Config::default()); } @@ -115,7 +117,7 @@ pub fn launch(root_component: Component) { /// }) /// } /// ``` -pub fn launch_cfg(root: Component, config: Config) { +pub fn launch_cfg(root: fn(Scope) -> Element, config: Config) { launch_with_props(root, (), config) } @@ -143,10 +145,11 @@ pub fn launch_cfg(root: Component, config: Config) { /// render!(div {"hello {cx.props.name}"}) /// } /// ``` -pub fn launch_with_props(root_component: Component, root_properties: T, config: Config) -where - T: Send + 'static, -{ +pub fn launch_with_props( + root_component: fn(Scope) -> Element, + root_properties: T, + config: Config, +) { wasm_bindgen_futures::spawn_local(run_with_props(root_component, root_properties, config)); } @@ -162,7 +165,9 @@ where /// wasm_bindgen_futures::spawn_local(app_fut); /// } /// ``` -pub async fn run_with_props(root: Component, root_props: T, cfg: Config) { +pub async fn run_with_props(root: fn(Scope) -> Element, root_props: T, cfg: Config) { + log::info!("Starting up"); + let mut dom = VirtualDom::new_with_props(root, root_props); #[cfg(feature = "panic_hook")] @@ -170,8 +175,8 @@ pub async fn run_with_props(root: Component, root_props: T console_error_panic_hook::set_once(); } - #[cfg(any(feature = "hot-reload", debug_assertions))] - hot_reload::init(&dom); + // #[cfg(any(feature = "hot-reload", debug_assertions))] + // hot_reload::init(&dom); for s in crate::cache::BUILTIN_INTERNED_STRINGS { wasm_bindgen::intern(s); @@ -180,50 +185,48 @@ pub async fn run_with_props(root: Component, root_props: T wasm_bindgen::intern(s); } - let tasks = dom.get_scheduler_channel(); + // a let should_hydrate = cfg.hydrate; - let sender_callback: Rc = - Rc::new(move |event| tasks.unbounded_send(event).unwrap()); + let (tx, mut rx) = futures_channel::mpsc::unbounded(); - let should_hydrate = cfg.hydrate; + let mut websys_dom = dom::WebsysDom::new(cfg, tx); - let mut websys_dom = dom::WebsysDom::new(cfg, sender_callback); + log::info!("rebuilding app"); - log::trace!("rebuilding app"); - - if should_hydrate { - // todo: we need to split rebuild and initialize into two phases - // it's a waste to produce edits just to get the vdom loaded - let _ = dom.rebuild(); - - #[cfg(feature = "hydrate")] - #[allow(unused_variables)] - if let Err(err) = websys_dom.rehydrate(&dom) { - log::error!( - "Rehydration failed {:?}. Rebuild DOM into element from scratch", - &err - ); - - websys_dom.root.set_text_content(None); - - // errrrr we should split rebuild into two phases - // one that initializes things and one that produces edits - let edits = dom.rebuild(); - - websys_dom.apply_edits(edits.edits); - } - } else { - let edits = dom.rebuild(); - websys_dom.apply_edits(edits.edits); - } - - // let mut work_loop = ric_raf::RafLoop::new(); + let edits = dom.rebuild(); + websys_dom.apply_edits(edits.template_mutations); + websys_dom.apply_edits(edits.edits); loop { log::trace!("waiting for work"); // if virtualdom has nothing, wait for it to have something before requesting idle time // if there is work then this future resolves immediately. - dom.wait_for_work().await; + + let mut res = { + let work = dom.wait_for_work().fuse(); + pin_mut!(work); + + futures_util::select! { + _ = work => None, + evt = rx.next() => evt + } + }; + + while let Some(evt) = res { + let name = evt.type_(); + let element = walk_event_for_id(&evt); + let bubbles = dioxus_html::event_bubbles(name.as_str()); + + if let Some((element, target)) = element { + let data = virtual_event_from_websys_event(evt, target); + dom.handle_event(name.as_str(), data, element, bubbles); + } + res = rx.try_next().transpose().unwrap().ok(); + } + + let deadline = sleep(Duration::from_millis(50)); + + let edits = dom.render_with_deadline(deadline).await; log::trace!("working.."); @@ -232,14 +235,85 @@ pub async fn run_with_props(root: Component, root_props: T // run the virtualdom work phase until the frame deadline is reached // let mutations = dom.work_with_deadline(|| (&mut deadline).now_or_never().is_some()); - let mutations = dom.work_with_deadline(|| false); // wait for the animation frame to fire so we can apply our changes // work_loop.wait_for_raf().await; - for edit in mutations { - // actually apply our changes during the animation frame - websys_dom.apply_edits(edit.edits); + // for edit in mutations { + // // actually apply our changes during the animation frame + websys_dom.apply_edits(edits.template_mutations); + websys_dom.apply_edits(edits.edits); + // } + } +} + +fn walk_event_for_id(event: &Event) -> Option<(ElementId, web_sys::Element)> { + use wasm_bindgen::{closure::Closure, JsCast, JsValue}; + use web_sys::{Document, Element, Event, HtmlElement}; + + let mut target = event + .target() + .expect("missing target") + .dyn_into::() + .expect("not a valid element"); + + // break Ok(UserEvent { + // name: event_name_from_typ(&typ), + // data: virtual_event_from_websys_event(event.clone(), target.clone()), + // element: Some(ElementId(id)), + // scope_id: None, + // priority: dioxus_core::EventPriority::Medium, + // bubbles: event.bubbles(), + // }); + + // break Ok(UserEvent { + // name: event_name_from_typ(&typ), + // data: virtual_event_from_websys_event(event.clone(), target.clone()), + // element: None, + // scope_id: None, + // priority: dioxus_core::EventPriority::Low, + // bubbles: event.bubbles(), + // }); + + loop { + match target.get_attribute("data-dioxus-id").map(|f| f.parse()) { + Some(Ok(id)) => return Some((ElementId(id), target)), + Some(Err(_)) => return None, + + // walk the tree upwards until we actually find an event target + None => match target.parent_element() { + Some(parent) => target = parent, + None => return None, + }, } } } + +// if should_hydrate { +// // todo: we need to split rebuild and initialize into two phases +// // it's a waste to produce edits just to get the vdom loaded +// let _ = dom.rebuild(); + +// #[cfg(feature = "hydrate")] +// #[allow(unused_variables)] +// if let Err(err) = websys_dom.rehydrate(&dom) { +// log::error!( +// "Rehydration failed {:?}. Rebuild DOM into element from scratch", +// &err +// ); + +// websys_dom.root.set_text_content(None); + +// // errrrr we should split rebuild into two phases +// // one that initializes things and one that produces edits +// let edits = dom.rebuild(); + +// websys_dom.apply_edits(edits.edits); +// } +// } else { +// let edits = dom.rebuild(); +// websys_dom.apply_edits(edits.template_mutations); +// websys_dom.apply_edits(edits.edits); +// } + +// let mut work_loop = ric_raf::RafLoop::new(); diff --git a/packages/web/src/olddom.rs b/packages/web/src/olddom.rs deleted file mode 100644 index e07d7478f..000000000 --- a/packages/web/src/olddom.rs +++ /dev/null @@ -1,856 +0,0 @@ -//! Implementation of a renderer for Dioxus on the web. -//! -//! Oustanding todos: -//! - Removing event listeners (delegation) -//! - Passive event listeners -//! - no-op event listener patch for safari -//! - tests to ensure dyn_into works for various event types. -//! - Partial delegation?> - -use dioxus_core::{DomEdit, ElementId, SchedulerMsg, ScopeId, UserEvent}; -use rustc_hash::FxHashMap; -use std::{any::Any, fmt::Debug, rc::Rc, sync::Arc}; -use wasm_bindgen::{closure::Closure, JsCast}; -use web_sys::{ - CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement, - HtmlOptionElement, HtmlTextAreaElement, Node, -}; - -use crate::{nodeslab::NodeSlab, Config}; - -pub struct WebsysDom { - stack: Stack, - - /// A map from ElementID (index) to Node - pub(crate) nodes: NodeSlab, - - document: Document, - - pub(crate) root: Element, - - sender_callback: Rc, - - // map of listener types to number of those listeners - // This is roughly a delegater - // TODO: check how infero delegates its events - some are more performant - listeners: FxHashMap<&'static str, ListenerEntry>, -} - -type ListenerEntry = (usize, Closure); - -impl WebsysDom { - pub fn new(cfg: Config, sender_callback: Rc) -> Self { - let document = load_document(); - - let nodes = NodeSlab::new(2000); - let listeners = FxHashMap::default(); - - let mut stack = Stack::with_capacity(10); - - let root = load_document().get_element_by_id(&cfg.rootname).unwrap(); - let root_node = root.clone().dyn_into::().unwrap(); - stack.push(root_node); - - Self { - stack, - nodes, - listeners, - document, - sender_callback, - root, - } - } - - pub fn apply_edits(&mut self, mut edits: Vec) { - for edit in edits.drain(..) { - match edit { - DomEdit::PushRoot { root } => self.push(root), - DomEdit::AppendChildren { many } => self.append_children(many), - DomEdit::ReplaceWith { m, root } => self.replace_with(m, root), - DomEdit::Remove { root } => self.remove(root), - DomEdit::CreateTextNode { text, root: id } => self.create_text_node(text, id), - DomEdit::CreateElement { tag, root: id } => self.create_element(tag, None, id), - DomEdit::CreateElementNs { tag, root: id, ns } => { - self.create_element(tag, Some(ns), id) - } - DomEdit::CreatePlaceholder { root: id } => self.create_placeholder(id), - DomEdit::NewEventListener { - event_name, - scope, - root: mounted_node_id, - } => self.new_event_listener(event_name, scope, mounted_node_id), - - DomEdit::RemoveEventListener { event, root } => { - self.remove_event_listener(event, root) - } - - DomEdit::SetText { text, root } => self.set_text(text, root), - DomEdit::SetAttribute { - field, - value, - ns, - root, - } => self.set_attribute(field, value, ns, root), - DomEdit::RemoveAttribute { name, root } => self.remove_attribute(name, root), - - DomEdit::InsertAfter { n, root } => self.insert_after(n, root), - DomEdit::InsertBefore { n, root } => self.insert_before(n, root), - } - } - } - fn push(&mut self, root: u64) { - let key = root as usize; - let domnode = &self.nodes[key]; - - let real_node: Node = match domnode { - Some(n) => n.clone(), - None => todo!(), - }; - - self.stack.push(real_node); - } - - fn append_children(&mut self, many: u32) { - let root: Node = self - .stack - .list - .get(self.stack.list.len() - (1 + many as usize)) - .unwrap() - .clone(); - - // We need to make sure to add comments between text nodes - // We ensure that the text siblings are patched by preventing the browser from merging - // neighboring text nodes. Originally inspired by some of React's work from 2016. - // -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes - // -> https://github.com/facebook/react/pull/5753 - /* - todo: we need to track this for replacing/insert after/etc - */ - let mut last_node_was_text = false; - - for child in self - .stack - .list - .drain((self.stack.list.len() - many as usize)..) - { - if child.dyn_ref::().is_some() { - if last_node_was_text { - let comment_node = self - .document - .create_comment("dioxus") - .dyn_into::() - .unwrap(); - root.append_child(&comment_node).unwrap(); - } - last_node_was_text = true; - } else { - last_node_was_text = false; - } - root.append_child(&child).unwrap(); - } - } - - fn replace_with(&mut self, m: u32, root: u64) { - let old = self.nodes[root as usize].as_ref().unwrap(); - - let arr: js_sys::Array = self - .stack - .list - .drain((self.stack.list.len() - m as usize)..) - .collect(); - - if let Some(el) = old.dyn_ref::() { - el.replace_with_with_node(&arr).unwrap(); - } else if let Some(el) = old.dyn_ref::() { - el.replace_with_with_node(&arr).unwrap(); - } else if let Some(el) = old.dyn_ref::() { - el.replace_with_with_node(&arr).unwrap(); - } - } - - fn remove(&mut self, root: u64) { - let node = self.nodes[root as usize].as_ref().unwrap(); - if let Some(element) = node.dyn_ref::() { - element.remove(); - } else { - if let Some(parent) = node.parent_node() { - parent.remove_child(&node).unwrap(); - } - } - } - - fn create_placeholder(&mut self, id: u64) { - self.create_element("pre", None, id); - self.set_attribute("hidden", "", None, id); - } - - fn create_text_node(&mut self, text: &str, id: u64) { - let textnode = self - .document - .create_text_node(text) - .dyn_into::() - .unwrap(); - - self.stack.push(textnode.clone()); - - self.nodes[(id as usize)] = Some(textnode); - } - - fn create_element(&mut self, tag: &str, ns: Option<&'static str>, id: u64) { - let tag = wasm_bindgen::intern(tag); - - let el = match ns { - Some(ns) => self - .document - .create_element_ns(Some(ns), tag) - .unwrap() - .dyn_into::() - .unwrap(), - None => self - .document - .create_element(tag) - .unwrap() - .dyn_into::() - .unwrap(), - }; - - use smallstr::SmallString; - use std::fmt::Write; - - let mut s: SmallString<[u8; 8]> = smallstr::SmallString::new(); - write!(s, "{}", id).unwrap(); - - let el2 = el.dyn_ref::().unwrap(); - el2.set_attribute("dioxus-id", s.as_str()).unwrap(); - - self.stack.push(el.clone()); - self.nodes[(id as usize)] = Some(el); - } - - fn new_event_listener(&mut self, event: &'static str, _scope: ScopeId, _real_id: u64) { - let event = wasm_bindgen::intern(event); - - // attach the correct attributes to the element - // these will be used by accessing the event's target - // This ensures we only ever have one handler attached to the root, but decide - // dynamically when we want to call a listener. - - let el = self.stack.top(); - - let el = el.dyn_ref::().unwrap(); - - el.set_attribute("dioxus-event", event).unwrap(); - - // Register the callback to decode - - if let Some(entry) = self.listeners.get_mut(event) { - entry.0 += 1; - } else { - let trigger = self.sender_callback.clone(); - - let c: Box = Box::new(move |event: &web_sys::Event| { - // "Result" cannot be received from JS - // Instead, we just build and immediately execute a closure that returns result - match decode_trigger(event) { - Ok(synthetic_event) => { - let target = event.target().unwrap(); - if let Some(node) = target.dyn_ref::() { - if let Some(name) = node.get_attribute("dioxus-prevent-default") { - if name == synthetic_event.name - || name.trim_start_matches("on") == synthetic_event.name - { - log::trace!("Preventing default"); - event.prevent_default(); - } - } - } - - trigger.as_ref()(SchedulerMsg::Event(synthetic_event)) - } - Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e), - }; - }); - - let handler = Closure::wrap(c); - - self.root - .add_event_listener_with_callback(event, (&handler).as_ref().unchecked_ref()) - .unwrap(); - - // Increment the listeners - self.listeners.insert(event.into(), (1, handler)); - } - } - - fn remove_event_listener(&mut self, _event: &str, _root: u64) { - todo!() - } - - fn set_text(&mut self, text: &str, root: u64) { - let el = self.nodes[root as usize].as_ref().unwrap(); - el.set_text_content(Some(text)) - } - - fn set_attribute(&mut self, name: &str, value: &str, ns: Option<&str>, root: u64) { - let node = self.nodes[root as usize].as_ref().unwrap(); - if ns == Some("style") { - if let Some(el) = node.dyn_ref::() { - let el = el.dyn_ref::().unwrap(); - let style_dc: CssStyleDeclaration = el.style(); - style_dc.set_property(name, value).unwrap(); - } - } else { - let fallback = || { - let el = node.dyn_ref::().unwrap(); - el.set_attribute(name, value).unwrap() - }; - match name { - "dangerous_inner_html" => { - if let Some(el) = node.dyn_ref::() { - el.set_inner_html(value); - } - } - "value" => { - if let Some(input) = node.dyn_ref::() { - /* - if the attribute being set is the same as the value of the input, then don't bother setting it. - This is used in controlled components to keep the cursor in the right spot. - - this logic should be moved into the virtualdom since we have the notion of "volatile" - */ - if input.value() != value { - input.set_value(value); - } - } else if let Some(node) = node.dyn_ref::() { - if name == "value" { - node.set_value(value); - } - } else { - fallback(); - } - } - "checked" => { - if let Some(input) = node.dyn_ref::() { - match value { - "true" => input.set_checked(true), - "false" => input.set_checked(false), - _ => fallback(), - } - } else { - fallback(); - } - } - "selected" => { - if let Some(node) = node.dyn_ref::() { - node.set_selected(true); - } else { - fallback(); - } - } - _ => { - // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if value == "false" { - if let Some(el) = node.dyn_ref::() { - match name { - "allowfullscreen" - | "allowpaymentrequest" - | "async" - | "autofocus" - | "autoplay" - | "checked" - | "controls" - | "default" - | "defer" - | "disabled" - | "formnovalidate" - | "hidden" - | "ismap" - | "itemscope" - | "loop" - | "multiple" - | "muted" - | "nomodule" - | "novalidate" - | "open" - | "playsinline" - | "readonly" - | "required" - | "reversed" - | "selected" - | "truespeed" => { - let _ = el.remove_attribute(name); - } - _ => { - let _ = el.set_attribute(name, value); - } - }; - } - } else { - fallback(); - } - } - } - } - } - - fn remove_attribute(&mut self, name: &str, root: u64) { - let node = self.nodes[root as usize].as_ref().unwrap(); - if let Some(node) = node.dyn_ref::() { - node.remove_attribute(name).unwrap(); - } - if let Some(node) = node.dyn_ref::() { - // Some attributes are "volatile" and don't work through `removeAttribute`. - if name == "value" { - node.set_value(""); - } - if name == "checked" { - node.set_checked(false); - } - } - - if let Some(node) = node.dyn_ref::() { - if name == "selected" { - node.set_selected(true); - } - } - } - - fn insert_after(&mut self, n: u32, root: u64) { - let old = self.nodes[root as usize].as_ref().unwrap(); - - let arr: js_sys::Array = self - .stack - .list - .drain((self.stack.list.len() - n as usize)..) - .collect(); - - if let Some(el) = old.dyn_ref::() { - el.after_with_node(&arr).unwrap(); - } else if let Some(el) = old.dyn_ref::() { - el.after_with_node(&arr).unwrap(); - } else if let Some(el) = old.dyn_ref::() { - el.after_with_node(&arr).unwrap(); - } - } - - fn insert_before(&mut self, n: u32, root: u64) { - let anchor = self.nodes[root as usize].as_ref().unwrap(); - - if n == 1 { - let before = self.stack.pop(); - - anchor - .parent_node() - .unwrap() - .insert_before(&before, Some(&anchor)) - .unwrap(); - } else { - let arr: js_sys::Array = self - .stack - .list - .drain((self.stack.list.len() - n as usize)..) - .collect(); - - if let Some(el) = anchor.dyn_ref::() { - el.before_with_node(&arr).unwrap(); - } else if let Some(el) = anchor.dyn_ref::() { - el.before_with_node(&arr).unwrap(); - } else if let Some(el) = anchor.dyn_ref::() { - el.before_with_node(&arr).unwrap(); - } - } - } -} - -#[derive(Debug, Default)] -struct Stack { - list: Vec, -} - -impl Stack { - #[inline] - fn with_capacity(cap: usize) -> Self { - Stack { - list: Vec::with_capacity(cap), - } - } - - #[inline] - fn push(&mut self, node: Node) { - self.list.push(node); - } - - #[inline] - fn pop(&mut self) -> Node { - self.list.pop().unwrap() - } - - fn top(&self) -> &Node { - match self.list.last() { - Some(a) => a, - None => panic!("Called 'top' of an empty stack, make sure to push the root first"), - } - } -} - -pub struct DioxusWebsysEvent(web_sys::Event); - -// safety: currently the web is not multithreaded and our VirtualDom exists on the same thread -unsafe impl Send for DioxusWebsysEvent {} -unsafe impl Sync for DioxusWebsysEvent {} - -// todo: some of these events are being casted to the wrong event type. -// We need tests that simulate clicks/etc and make sure every event type works. -fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc { - use dioxus_html::on::*; - use dioxus_html::KeyCode; - - match event.type_().as_str() { - "copy" | "cut" | "paste" => Arc::new(ClipboardData {}), - "compositionend" | "compositionstart" | "compositionupdate" => { - let evt: &web_sys::CompositionEvent = event.dyn_ref().unwrap(); - Arc::new(CompositionData { - data: evt.data().unwrap_or_default(), - }) - } - "keydown" | "keypress" | "keyup" => { - let evt: &web_sys::KeyboardEvent = event.dyn_ref().unwrap(); - Arc::new(KeyboardData { - alt_key: evt.alt_key(), - char_code: evt.char_code(), - key: evt.key(), - key_code: KeyCode::from_raw_code(evt.key_code() as u8), - ctrl_key: evt.ctrl_key(), - location: evt.location() as usize, - meta_key: evt.meta_key(), - repeat: evt.repeat(), - shift_key: evt.shift_key(), - which: evt.which() as usize, - }) - } - "focus" | "blur" => Arc::new(FocusData {}), - - // todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy - // don't have a good solution with the serialized event problem - "change" | "input" | "invalid" | "reset" | "submit" => { - let evt: &web_sys::Event = event.dyn_ref().unwrap(); - - let target: web_sys::EventTarget = evt.target().unwrap(); - let value: String = (&target) - .dyn_ref() - .map(|input: &web_sys::HtmlInputElement| { - // todo: special case more input types - match input.type_().as_str() { - "checkbox" => { - match input.checked() { - true => "true".to_string(), - false => "false".to_string(), - } - }, - _ => { - input.value() - } - } - }) - .or_else(|| { - target - .dyn_ref() - .map(|input: &web_sys::HtmlTextAreaElement| input.value()) - }) - // select elements are NOT input events - because - why woudn't they be?? - .or_else(|| { - target - .dyn_ref() - .map(|input: &web_sys::HtmlSelectElement| input.value()) - }) - .or_else(|| { - target - .dyn_ref::() - .unwrap() - .text_content() - }) - .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); - - Arc::new(FormData { value }) - } - "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit" - | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter" - | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => { - let evt: &web_sys::MouseEvent = event.dyn_ref().unwrap(); - Arc::new(MouseData { - alt_key: evt.alt_key(), - button: evt.button(), - buttons: evt.buttons(), - client_x: evt.client_x(), - client_y: evt.client_y(), - ctrl_key: evt.ctrl_key(), - meta_key: evt.meta_key(), - screen_x: evt.screen_x(), - screen_y: evt.screen_y(), - shift_key: evt.shift_key(), - page_x: evt.page_x(), - page_y: evt.page_y(), - }) - } - "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" - | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { - let evt: &web_sys::PointerEvent = event.dyn_ref().unwrap(); - Arc::new(PointerData { - alt_key: evt.alt_key(), - button: evt.button(), - buttons: evt.buttons(), - client_x: evt.client_x(), - client_y: evt.client_y(), - ctrl_key: evt.ctrl_key(), - meta_key: evt.meta_key(), - page_x: evt.page_x(), - page_y: evt.page_y(), - screen_x: evt.screen_x(), - screen_y: evt.screen_y(), - shift_key: evt.shift_key(), - pointer_id: evt.pointer_id(), - width: evt.width(), - height: evt.height(), - pressure: evt.pressure(), - tangential_pressure: evt.tangential_pressure(), - tilt_x: evt.tilt_x(), - tilt_y: evt.tilt_y(), - twist: evt.twist(), - pointer_type: evt.pointer_type(), - is_primary: evt.is_primary(), - // get_modifier_state: evt.get_modifier_state(), - }) - } - "select" => Arc::new(SelectionData {}), - "touchcancel" | "touchend" | "touchmove" | "touchstart" => { - let evt: &web_sys::TouchEvent = event.dyn_ref().unwrap(); - Arc::new(TouchData { - alt_key: evt.alt_key(), - ctrl_key: evt.ctrl_key(), - meta_key: evt.meta_key(), - shift_key: evt.shift_key(), - }) - } - "scroll" => Arc::new(()), - "wheel" => { - let evt: &web_sys::WheelEvent = event.dyn_ref().unwrap(); - Arc::new(WheelData { - delta_x: evt.delta_x(), - delta_y: evt.delta_y(), - delta_z: evt.delta_z(), - delta_mode: evt.delta_mode(), - }) - } - "animationstart" | "animationend" | "animationiteration" => { - let evt: &web_sys::AnimationEvent = event.dyn_ref().unwrap(); - Arc::new(AnimationData { - elapsed_time: evt.elapsed_time(), - animation_name: evt.animation_name(), - pseudo_element: evt.pseudo_element(), - }) - } - "transitionend" => { - let evt: &web_sys::TransitionEvent = event.dyn_ref().unwrap(); - Arc::new(TransitionData { - elapsed_time: evt.elapsed_time(), - property_name: evt.property_name(), - pseudo_element: evt.pseudo_element(), - }) - } - "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted" - | "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play" - | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" - | "timeupdate" | "volumechange" | "waiting" => Arc::new(MediaData {}), - "toggle" => Arc::new(ToggleData {}), - _ => Arc::new(()), - } -} - -/// This function decodes a websys event and produces an EventTrigger -/// With the websys implementation, we attach a unique key to the nodes -fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { - use anyhow::Context; - - let target = event - .target() - .expect("missing target") - .dyn_into::() - .expect("not a valid element"); - - let typ = event.type_(); - - let element_id = target - .get_attribute("dioxus-id") - .context("Could not find element id on event target")? - .parse()?; - - Ok(UserEvent { - name: event_name_from_typ(&typ), - data: virtual_event_from_websys_event(event.clone()), - element: Some(ElementId(element_id)), - scope_id: None, - priority: dioxus_core::EventPriority::Medium, - }) -} - -pub(crate) fn load_document() -> Document { - web_sys::window() - .expect("should have access to the Window") - .document() - .expect("should have access to the Document") -} - -fn event_name_from_typ(typ: &str) -> &'static str { - match typ { - "copy" => "copy", - "cut" => "cut", - "paste" => "paste", - "compositionend" => "compositionend", - "compositionstart" => "compositionstart", - "compositionupdate" => "compositionupdate", - "keydown" => "keydown", - "keypress" => "keypress", - "keyup" => "keyup", - "focus" => "focus", - "blur" => "blur", - "change" => "change", - "input" => "input", - "invalid" => "invalid", - "reset" => "reset", - "submit" => "submit", - "click" => "click", - "contextmenu" => "contextmenu", - "doubleclick" => "doubleclick", - "drag" => "drag", - "dragend" => "dragend", - "dragenter" => "dragenter", - "dragexit" => "dragexit", - "dragleave" => "dragleave", - "dragover" => "dragover", - "dragstart" => "dragstart", - "drop" => "drop", - "mousedown" => "mousedown", - "mouseenter" => "mouseenter", - "mouseleave" => "mouseleave", - "mousemove" => "mousemove", - "mouseout" => "mouseout", - "mouseover" => "mouseover", - "mouseup" => "mouseup", - "pointerdown" => "pointerdown", - "pointermove" => "pointermove", - "pointerup" => "pointerup", - "pointercancel" => "pointercancel", - "gotpointercapture" => "gotpointercapture", - "lostpointercapture" => "lostpointercapture", - "pointerenter" => "pointerenter", - "pointerleave" => "pointerleave", - "pointerover" => "pointerover", - "pointerout" => "pointerout", - "select" => "select", - "touchcancel" => "touchcancel", - "touchend" => "touchend", - "touchmove" => "touchmove", - "touchstart" => "touchstart", - "scroll" => "scroll", - "wheel" => "wheel", - "animationstart" => "animationstart", - "animationend" => "animationend", - "animationiteration" => "animationiteration", - "transitionend" => "transitionend", - "abort" => "abort", - "canplay" => "canplay", - "canplaythrough" => "canplaythrough", - "durationchange" => "durationchange", - "emptied" => "emptied", - "encrypted" => "encrypted", - "ended" => "ended", - "error" => "error", - "loadeddata" => "loadeddata", - "loadedmetadata" => "loadedmetadata", - "loadstart" => "loadstart", - "pause" => "pause", - "play" => "play", - "playing" => "playing", - "progress" => "progress", - "ratechange" => "ratechange", - "seeked" => "seeked", - "seeking" => "seeking", - "stalled" => "stalled", - "suspend" => "suspend", - "timeupdate" => "timeupdate", - "volumechange" => "volumechange", - "waiting" => "waiting", - "toggle" => "toggle", - _ => { - panic!("unsupported event type") - } - } -} - - -//! This module provides a mirror of the VirtualDOM Element Slab using a Vector. - -use std::ops::{Index, IndexMut}; -use web_sys::Node; - -pub(crate) struct NodeSlab { - nodes: Vec>, -} - -impl NodeSlab { - pub fn new(capacity: usize) -> NodeSlab { - let nodes = Vec::with_capacity(capacity); - NodeSlab { nodes } - } -} -impl Index for NodeSlab { - type Output = Option; - fn index(&self, index: usize) -> &Self::Output { - &self.nodes[index] - } -} - -impl IndexMut for NodeSlab { - fn index_mut(&mut self, index: usize) -> &mut Self::Output { - if index >= self.nodes.capacity() * 3 { - panic!("Trying to mutate an element way too far out of bounds"); - } - - if index + 1 > self.nodes.len() { - self.nodes.resize_with(index + 1, || None); - } - &mut self.nodes[index] - } -} - - -#[derive(Debug, Default)] -struct Stack { - list: Vec, -} - -impl Stack { - #[inline] - fn with_capacity(cap: usize) -> Self { - Stack { - list: Vec::with_capacity(cap), - } - } - - #[inline] - fn push(&mut self, node: Node) { - self.list.push(node); - } - - #[inline] - fn pop(&mut self) -> Node { - self.list.pop().unwrap() - } - - fn top(&self) -> &Node { - match self.list.last() { - Some(a) => a, - None => panic!("Called 'top' of an empty stack, make sure to push the root first"), - } - } -} diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index cab43bacd..bf327176d 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -1,6 +1,5 @@ use crate::dom::WebsysDom; use dioxus_core::{VNode, VirtualDom}; -use dioxus_html::event_bubbles; use wasm_bindgen::JsCast; use web_sys::{Comment, Element, Node, Text}; @@ -30,14 +29,15 @@ impl WebsysDom { let mut last_node_was_text = false; - // Recursively rehydrate the dom from the VirtualDom - self.rehydrate_single( - &mut nodes, - &mut counter, - dom, - root_node, - &mut last_node_was_text, - ) + todo!() + // // Recursively rehydrate the dom from the VirtualDom + // self.rehydrate_single( + // &mut nodes, + // &mut counter, + // dom, + // root_node, + // &mut last_node_was_text, + // ) } fn rehydrate_single( @@ -164,7 +164,8 @@ impl WebsysDom { VNode::Component(el) => { let scope = dom.get_scope(el.scope.get().unwrap()).unwrap(); let node = scope.root_node(); - self.rehydrate_single(nodes, place, dom, node, last_node_was_text)?; + todo!() + // self.rehydrate_single(nodes, place, dom, node, last_node_was_text)?; } VNode::TemplateRef(_) => todo!(), } diff --git a/packages/web/tests/hydrate.rs b/packages/web/tests/hydrate.rs index af6e49cc2..d0b5acb9c 100644 --- a/packages/web/tests/hydrate.rs +++ b/packages/web/tests/hydrate.rs @@ -50,7 +50,7 @@ fn rehydrates() { let mut dom = VirtualDom::new(app); let _ = dom.rebuild(); - let out = dioxus_ssr::render_vdom_cfg(&dom, |c| c.pre_render(true)); + let out = dioxus_ssr::render_vdom_cfg(&dom, Default::default()); window() .unwrap() From ba26b1001a4bbd414193c3ff1eab09f32e91ab0c Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 30 Nov 2022 17:44:00 -0500 Subject: [PATCH 14/18] chore: clean up web impl --- packages/web/src/dom.rs | 7 --- packages/web/src/hot_reload.rs | 33 +++++++++---- packages/web/src/lib.rs | 88 ++++++++++++++-------------------- 3 files changed, 60 insertions(+), 68 deletions(-) diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 20e50c0fd..e211188c9 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -118,13 +118,6 @@ impl WebsysDom { } } -pub struct DioxusWebsysEvent(web_sys::Event); - -// safety: currently the web is not multithreaded and our VirtualDom exists on the same thread -#[allow(clippy::non_send_fields_in_send_ty)] -unsafe impl Send for DioxusWebsysEvent {} -unsafe impl Sync for DioxusWebsysEvent {} - // todo: some of these events are being casted to the wrong event type. // We need tests that simulate clicks/etc and make sure every event type works. pub fn virtual_event_from_websys_event(event: web_sys::Event, target: Element) -> Rc { diff --git a/packages/web/src/hot_reload.rs b/packages/web/src/hot_reload.rs index 7305cb0cb..ac474370a 100644 --- a/packages/web/src/hot_reload.rs +++ b/packages/web/src/hot_reload.rs @@ -1,11 +1,24 @@ -use dioxus_core::SchedulerMsg; -use dioxus_core::SetTemplateMsg; -use dioxus_core::VirtualDom; +#![allow(dead_code)] + +use futures_channel::mpsc::UnboundedReceiver; + use wasm_bindgen::closure::Closure; use wasm_bindgen::JsCast; use web_sys::{MessageEvent, WebSocket}; -pub(crate) fn init(dom: &VirtualDom) { +#[cfg(not(debug_assertions))] +pub(crate) fn init() -> UnboundedReceiver { + let (tx, rx) = futures_channel::mpsc::unbounded(); + + std::mem::forget(tx); + + rx +} + +#[cfg(debug_assertions)] +pub(crate) fn init() -> UnboundedReceiver { + use std::convert::TryInto; + let window = web_sys::window().unwrap(); let protocol = if window.location().protocol().unwrap() == "https:" { @@ -20,18 +33,20 @@ pub(crate) fn init(dom: &VirtualDom) { ); let ws = WebSocket::new(&url).unwrap(); - let mut channel = dom.get_scheduler_channel(); + + let (tx, rx) = futures_channel::mpsc::unbounded(); // change the rsx when new data is received let cl = Closure::wrap(Box::new(move |e: MessageEvent| { if let Ok(text) = e.data().dyn_into::() { - let msg: SetTemplateMsg = serde_json::from_str(&format!("{text}")).unwrap(); - channel - .start_send(SchedulerMsg::SetTemplate(Box::new(msg))) - .unwrap(); + if let Ok(val) = text.try_into() { + _ = tx.unbounded_send(val); + } } }) as Box); ws.set_onmessage(Some(cl.as_ref().unchecked_ref())); cl.forget(); + + rx } diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 5b33bb9a4..68ccf3cf5 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -9,9 +9,8 @@ //! - idle work //! - animations //! - jank-free rendering -//! - noderefs //! - controlled components -//! - re-hydration +//! - hydration //! - and more. //! //! The actual implementation is farily thin, with the heavy lifting happening inside the Dioxus Core crate. @@ -54,24 +53,22 @@ // - Do the VDOM work during the idlecallback // - Do DOM work in the next requestAnimationFrame callback -use std::{rc::Rc, time::Duration}; +use std::time::Duration; pub use crate::cfg::Config; use crate::dom::virtual_event_from_websys_event; pub use crate::util::use_eval; use dioxus_core::{Element, ElementId, Scope, VirtualDom}; +use futures_channel::mpsc::unbounded; use futures_util::{pin_mut, FutureExt, StreamExt}; use gloo_timers::future::sleep; -use web_sys::Event; mod cache; mod cfg; mod dom; -// #[cfg(any(feature = "hot-reload", debug_assertions))] -// mod hot_reload; -// #[cfg(feature = "hydrate")] +mod hot_reload; // mod rehydrate; -// mod ric_raf; +mod ric_raf; mod util; /// Launch the VirtualDOM given a root component and a configuration. @@ -175,8 +172,7 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop console_error_panic_hook::set_once(); } - // #[cfg(any(feature = "hot-reload", debug_assertions))] - // hot_reload::init(&dom); + let mut hotreload_rx = hot_reload::init(); for s in crate::cache::BUILTIN_INTERNED_STRINGS { wasm_bindgen::intern(s); @@ -185,7 +181,7 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop wasm_bindgen::intern(s); } - // a let should_hydrate = cfg.hydrate; + let should_hydrate = cfg.hydrate; let (tx, mut rx) = futures_channel::mpsc::unbounded(); @@ -193,30 +189,39 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop log::info!("rebuilding app"); - let edits = dom.rebuild(); - websys_dom.apply_edits(edits.template_mutations); - websys_dom.apply_edits(edits.edits); + if should_hydrate { + } else { + let edits = dom.rebuild(); + websys_dom.apply_edits(edits.template_mutations); + websys_dom.apply_edits(edits.edits); + } + + let mut work_loop = ric_raf::RafLoop::new(); loop { log::trace!("waiting for work"); + // if virtualdom has nothing, wait for it to have something before requesting idle time // if there is work then this future resolves immediately. - let mut res = { let work = dom.wait_for_work().fuse(); pin_mut!(work); - futures_util::select! { _ = work => None, + new_template = hotreload_rx.next() => { + todo!("Implement hot reload"); + None + } evt = rx.next() => evt } }; + // Dequeue all of the events from the channel in send order + // todo: we should re-order these if possible while let Some(evt) = res { let name = evt.type_(); let element = walk_event_for_id(&evt); let bubbles = dioxus_html::event_bubbles(name.as_str()); - if let Some((element, target)) = element { let data = virtual_event_from_websys_event(evt, target); dom.handle_event(name.as_str(), data, element, bubbles); @@ -224,57 +229,38 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop res = rx.try_next().transpose().unwrap().ok(); } - let deadline = sleep(Duration::from_millis(50)); - - let edits = dom.render_with_deadline(deadline).await; - - log::trace!("working.."); + // Jank free rendering + // + // 1. wait for the browser to give us "idle" time + // 2. During idle time, diff the dom + // 3. Stop diffing if the deadline is exceded + // 4. Wait for the animation frame to patch the dom // wait for the mainthread to schedule us in - // let mut deadline = work_loop.wait_for_idle_time().await; + let deadline = work_loop.wait_for_idle_time().await; // run the virtualdom work phase until the frame deadline is reached - // let mutations = dom.work_with_deadline(|| (&mut deadline).now_or_never().is_some()); + let edits = dom.render_with_deadline(deadline).await; // wait for the animation frame to fire so we can apply our changes - // work_loop.wait_for_raf().await; + work_loop.wait_for_raf().await; + + log::debug!("edits {:#?}", edits); - // for edit in mutations { - // // actually apply our changes during the animation frame websys_dom.apply_edits(edits.template_mutations); websys_dom.apply_edits(edits.edits); - // } } } -fn walk_event_for_id(event: &Event) -> Option<(ElementId, web_sys::Element)> { - use wasm_bindgen::{closure::Closure, JsCast, JsValue}; - use web_sys::{Document, Element, Event, HtmlElement}; +fn walk_event_for_id(event: &web_sys::Event) -> Option<(ElementId, web_sys::Element)> { + use wasm_bindgen::JsCast; let mut target = event .target() .expect("missing target") - .dyn_into::() + .dyn_into::() .expect("not a valid element"); - // break Ok(UserEvent { - // name: event_name_from_typ(&typ), - // data: virtual_event_from_websys_event(event.clone(), target.clone()), - // element: Some(ElementId(id)), - // scope_id: None, - // priority: dioxus_core::EventPriority::Medium, - // bubbles: event.bubbles(), - // }); - - // break Ok(UserEvent { - // name: event_name_from_typ(&typ), - // data: virtual_event_from_websys_event(event.clone(), target.clone()), - // element: None, - // scope_id: None, - // priority: dioxus_core::EventPriority::Low, - // bubbles: event.bubbles(), - // }); - loop { match target.get_attribute("data-dioxus-id").map(|f| f.parse()) { Some(Ok(id)) => return Some((ElementId(id), target)), @@ -315,5 +301,3 @@ fn walk_event_for_id(event: &Event) -> Option<(ElementId, web_sys::Element)> { // websys_dom.apply_edits(edits.template_mutations); // websys_dom.apply_edits(edits.edits); // } - -// let mut work_loop = ric_raf::RafLoop::new(); From 3b166c9eddd667a09250d59b2fe6908a5ab85ba8 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 30 Nov 2022 23:54:30 -0500 Subject: [PATCH 15/18] wip: clean up some things --- Cargo.toml | 12 ++- packages/autofmt/src/buffer.rs | 2 +- packages/autofmt/src/element.rs | 2 +- packages/core/src/create.rs | 46 ++++----- packages/core/src/events.rs | 121 ++++++++++++++++------- packages/core/src/factory.rs | 116 ++-------------------- packages/core/src/lazynodes.rs | 108 ++++---------------- packages/core/src/lib.rs | 69 ++----------- packages/core/src/mutations.rs | 87 +++++++++++----- packages/core/src/nodes.rs | 15 ++- packages/core/src/scheduler/suspense.rs | 26 +---- packages/core/src/scope_arena.rs | 2 +- packages/core/src/scopes.rs | 110 +++++++++++++++++++-- packages/core/src/virtual_dom.rs | 20 ++-- packages/core/tests/README.md | 9 +- packages/core/tests/attr_cleanup.rs | 10 +- packages/core/tests/boolattrs.rs | 2 +- packages/core/tests/borrowedstate.rs | 2 +- packages/core/tests/context_api.rs | 6 +- packages/core/tests/create_dom.rs | 16 +-- packages/core/tests/create_element.rs | 2 +- packages/core/tests/create_fragments.rs | 10 +- packages/core/tests/create_lists.rs | 4 +- packages/core/tests/create_passthru.rs | 8 +- packages/core/tests/cycle.rs | 8 +- packages/core/tests/diff_component.rs | 8 +- packages/core/tests/diff_element.rs | 14 +-- packages/core/tests/diff_keyed_list.rs | 26 ++--- packages/core/tests/diff_unkeyed_list.rs | 52 +++++----- packages/core/tests/kitchen_sink.rs | 4 +- packages/core/tests/lifecycle.rs | 4 +- packages/core/tests/suspense.rs | 4 +- packages/desktop/src/controller.rs | 8 +- packages/dioxus/benches/jsframework.rs | 2 +- packages/hooks/src/lib.rs | 24 ++++- packages/hooks/src/usestate.rs | 2 +- packages/html/src/events.rs | 2 +- packages/html/src/events/animation.rs | 4 +- packages/html/src/events/clipboard.rs | 4 +- packages/html/src/events/composition.rs | 4 +- packages/html/src/events/drag.rs | 4 +- packages/html/src/events/focus.rs | 4 +- packages/html/src/events/form.rs | 4 +- packages/html/src/events/image.rs | 4 +- packages/html/src/events/keyboard.rs | 4 +- packages/html/src/events/media.rs | 4 +- packages/html/src/events/mouse.rs | 4 +- packages/html/src/events/pointer.rs | 4 +- packages/html/src/events/scroll.rs | 4 +- packages/html/src/events/selection.rs | 4 +- packages/html/src/events/toggle.rs | 4 +- packages/html/src/events/touch.rs | 4 +- packages/html/src/events/transition.rs | 4 +- packages/html/src/events/wheel.rs | 4 +- packages/liveview/src/events.rs | 39 ++++---- packages/native-core/src/real_dom.rs | 2 +- packages/native-core/src/utils.rs | 4 +- packages/rsx/src/component.rs | 2 +- packages/rsx/src/lib.rs | 4 +- packages/rsx/src/node.rs | 10 +- packages/tui/src/focus.rs | 2 +- packages/web/src/dom.rs | 35 ++----- packages/web/src/lib.rs | 8 +- packages/web/tests/hydrate.rs | 2 +- 64 files changed, 540 insertions(+), 599 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ae0a7490c..50a0bfbf6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,3 @@ - [workspace] members = [ "packages/dioxus", @@ -12,17 +11,20 @@ members = [ "packages/desktop", "packages/mobile", "packages/interpreter", - # "packages/tui", "packages/fermi", "packages/liveview", "packages/autofmt", "packages/rsx", + "docs/guide", + # "packages/tui", # "packages/native-core", # "packages/native-core-macro", - # "packages/edit-stream", - "docs/guide", ] - +exclude = [ + "packages/tui", + "packages/native-core", + "packages/native-core-macro", +] # This is a "virtual package" # It is not meant to be published, but is used so "cargo run --example XYZ" works properly diff --git a/packages/autofmt/src/buffer.rs b/packages/autofmt/src/buffer.rs index 720802346..45799ca62 100644 --- a/packages/autofmt/src/buffer.rs +++ b/packages/autofmt/src/buffer.rs @@ -67,7 +67,7 @@ impl Buffer { match node { BodyNode::Element(el) => self.write_element(el), BodyNode::Component(component) => self.write_component(component), - BodyNode::DynamicText(text) => self.write_text(text), + BodyNode::Text(text) => self.write_text(text), BodyNode::RawExpr(exp) => self.write_raw_expr(exp), _ => Ok(()), } diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs index 4f14249c9..fd4529766 100644 --- a/packages/autofmt/src/element.rs +++ b/packages/autofmt/src/element.rs @@ -280,7 +280,7 @@ impl Buffer { } match children { - [BodyNode::DynamicText(ref text)] => Some(text.source.as_ref().unwrap().value().len()), + [BodyNode::Text(ref text)] => Some(text.source.as_ref().unwrap().value().len()), [BodyNode::Component(ref comp)] => { let attr_len = self.field_len(&comp.fields, &comp.manual_props); diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index 2f11eff1f..c64763c5b 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -228,27 +228,29 @@ impl<'b> VirtualDom { self.create_static_node(node); } - self.mutations.template_mutations.push(SaveTemplate { + self.mutations.template_edits.push(SaveTemplate { name: template.template.id, m: template.template.roots.len(), }); } + // todo: we shouldn't have any of instructions for building templates - renderers should be able to work with the + // template type directly, right? pub(crate) fn create_static_node(&mut self, node: &'b TemplateNode<'static>) { match *node { // Todo: create the children's template TemplateNode::Dynamic(_) => self .mutations - .template_mutations + .template_edits .push(CreateStaticPlaceholder {}), TemplateNode::Text(value) => self .mutations - .template_mutations + .template_edits .push(CreateStaticText { value }), - TemplateNode::DynamicText { .. } => self - .mutations - .template_mutations - .push(CreateTextPlaceholder), + + TemplateNode::DynamicText { .. } => { + self.mutations.template_edits.push(CreateTextPlaceholder) + } TemplateNode::Element { attrs, @@ -257,21 +259,19 @@ impl<'b> VirtualDom { tag, .. } => { - if let Some(namespace) = namespace { - self.mutations - .template_mutations - .push(CreateElementNamespace { - name: tag, - namespace, - }); - } else { - self.mutations - .template_mutations - .push(CreateElement { name: tag }); + match namespace { + Some(namespace) => self.mutations.template_edits.push(CreateElementNamespace { + name: tag, + namespace, + }), + None => self + .mutations + .template_edits + .push(CreateElement { name: tag }), } self.mutations - .template_mutations + .template_edits .extend(attrs.iter().filter_map(|attr| match attr { TemplateAttribute::Static { name, @@ -295,7 +295,7 @@ impl<'b> VirtualDom { .for_each(|child| self.create_static_node(child)); self.mutations - .template_mutations + .template_edits .push(AppendChildren { m: children.len() }) } } @@ -404,7 +404,7 @@ impl<'b> VirtualDom { ) -> 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(); + let mutations_to_this_point = self.mutations.dom_edits.len(); // Create the component's root element let created = self.create_scope(scope, new); @@ -430,10 +430,10 @@ impl<'b> VirtualDom { // Note that we break off dynamic mutations only - since static mutations aren't rendered immediately let split_off = unsafe { std::mem::transmute::, Vec>( - self.mutations.split_off(mutations_to_this_point), + self.mutations.dom_edits.split_off(mutations_to_this_point), ) }; - boundary.mutations.borrow_mut().edits.extend(split_off); + boundary.mutations.borrow_mut().dom_edits.extend(split_off); boundary.created_on_stack.set(created); boundary .waiting_on diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index f511e73f9..c79cee549 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -1,57 +1,110 @@ -use bumpalo::boxed::Box as BumpBox; use std::{ - any::Any, cell::{Cell, RefCell}, - fmt::Debug, rc::Rc, }; -pub struct UiEvent { - pub(crate) bubbles: Rc>, +/// A wrapper around some generic data that handles the event's state +/// +/// +/// Prevent this event from continuing to bubble up the tree to parent elements. +/// +/// # Example +/// +/// ```rust, ignore +/// rsx! { +/// button { +/// onclick: move |evt: Event| { +/// evt.cancel_bubble(); +/// +/// } +/// } +/// } +/// ``` +pub struct Event { pub(crate) data: Rc, + pub(crate) propogates: Rc>, } -impl UiEvent { - pub fn downcast(self) -> Option> { - Some(UiEvent { - bubbles: self.bubbles, - data: self.data.downcast().ok()?, - }) +impl Event { + /// Prevent this event from continuing to bubble up the tree to parent elements. + /// + /// # Example + /// + /// ```rust, ignore + /// rsx! { + /// button { + /// onclick: move |evt: Event| { + /// evt.cancel_bubble(); + /// } + /// } + /// } + /// ``` + #[deprecated = "use stop_propogation instead"] + pub fn cancel_bubble(&self) { + self.propogates.set(false); + } + + /// Prevent this event from continuing to bubble up the tree to parent elements. + /// + /// # Example + /// + /// ```rust, ignore + /// rsx! { + /// button { + /// onclick: move |evt: Event| { + /// evt.cancel_bubble(); + /// } + /// } + /// } + /// ``` + pub fn stop_propogation(&self) { + self.propogates.set(false); + } + + /// Get a reference to the inner data from this event + /// + /// ```rust, ignore + /// rsx! { + /// button { + /// onclick: move |evt: Event| { + /// let data = evt.inner.clone(); + /// cx.spawn(async move { + /// println!("{:?}", data); + /// }); + /// } + /// } + /// } + /// ``` + pub fn inner(&self) -> &Rc { + &self.data } } -impl Clone for UiEvent { + +impl Clone for Event { fn clone(&self) -> Self { Self { - bubbles: self.bubbles.clone(), + propogates: self.propogates.clone(), data: self.data.clone(), } } } -impl UiEvent { - pub fn cancel_bubble(&self) { - self.bubbles.set(false); - } -} -impl std::ops::Deref for UiEvent { +impl std::ops::Deref for Event { type Target = Rc; - fn deref(&self) -> &Self::Target { &self.data } } -impl std::fmt::Debug for UiEvent { +impl std::fmt::Debug for Event { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("UiEvent") - .field("bubble_state", &self.bubbles) + .field("bubble_state", &self.propogates) .field("data", &self.data) .finish() } } -type ExternalListenerCallback<'bump, T> = BumpBox<'bump, dyn FnMut(T) + 'bump>; - /// The callback type generated by the `rsx!` macro when an `on` field is specified for components. /// /// This makes it possible to pass `move |evt| {}` style closures into components as property fields. @@ -60,9 +113,8 @@ type ExternalListenerCallback<'bump, T> = BumpBox<'bump, dyn FnMut(T) + 'bump>; /// # Example /// /// ```rust, ignore -/// /// rsx!{ -/// MyComponent { onclick: move |evt| log::info!("clicked"), } +/// MyComponent { onclick: move |evt| log::info!("clicked") } /// } /// /// #[derive(Props)] @@ -79,22 +131,17 @@ type ExternalListenerCallback<'bump, T> = BumpBox<'bump, dyn FnMut(T) + 'bump>; /// } /// /// ``` +#[derive(Default)] pub struct EventHandler<'bump, T = ()> { - /// The (optional) callback that the user specified - /// Uses a `RefCell` to allow for interior mutability, and FnMut closures. - pub callback: RefCell>>, + pub(super) callback: RefCell>>, } -impl<'a, T> Default for EventHandler<'a, T> { - fn default() -> Self { - Self { - callback: RefCell::new(None), - } - } -} +type ExternalListenerCallback<'bump, T> = bumpalo::boxed::Box<'bump, dyn FnMut(T) + 'bump>; impl EventHandler<'_, T> { /// Call this event handler with the appropriate event type + /// + /// This borrows the event using a RefCell. Recursively calling a listener will cause a panic. pub fn call(&self, event: T) { if let Some(callback) = self.callback.borrow_mut().as_mut() { callback(event); @@ -102,6 +149,8 @@ impl EventHandler<'_, T> { } /// Forcibly drop the internal handler callback, releasing memory + /// + /// This will force any future calls to "call" to not doing anything pub fn release(&self) { self.callback.replace(None); } diff --git a/packages/core/src/factory.rs b/packages/core/src/factory.rs index 0423d521e..b4217184d 100644 --- a/packages/core/src/factory.rs +++ b/packages/core/src/factory.rs @@ -1,119 +1,21 @@ -use std::{ - cell::{Cell, RefCell}, - fmt::Arguments, -}; - +use crate::{innerlude::DynamicNode, AttributeValue, Element, LazyNodes, ScopeState, VNode}; use bumpalo::boxed::Box as BumpBox; use bumpalo::Bump; +use std::fmt::Arguments; use std::future::Future; -use crate::{ - any_props::{AnyProps, VProps}, - arena::ElementId, - innerlude::{DynamicNode, EventHandler, VComponent, VText}, - Attribute, AttributeValue, Element, LazyNodes, Properties, Scope, ScopeState, VNode, -}; - -impl ScopeState { - /// Create some text that's allocated along with the other vnodes - pub fn text<'a>(&'a self, args: Arguments) -> DynamicNode<'a> { - let (text, _) = self.raw_text(args); - DynamicNode::Text(VText { - id: Cell::new(ElementId(0)), - value: text, - }) - } - - pub fn raw_text_inline<'a>(&'a self, args: Arguments) -> &'a str { - self.raw_text(args).0 - } - - pub fn raw_text<'a>(&'a self, args: Arguments) -> (&'a str, bool) { - match args.as_str() { - Some(static_str) => (static_str, true), - None => { - use bumpalo::core_alloc::fmt::Write; - let mut str_buf = bumpalo::collections::String::new_in(self.bump()); - str_buf.write_fmt(args).unwrap(); - (str_buf.into_bump_str(), false) - } - } - } - - pub fn fragment_from_iter<'a, 'c, I>( - &'a self, - node_iter: impl IntoDynNode<'a, I> + 'c, - ) -> DynamicNode { - node_iter.into_vnode(self) - } - - /// Create a new [`Attribute`] - pub fn attr<'a>( - &'a self, - name: &'static str, - value: impl IntoAttributeValue<'a>, - namespace: Option<&'static str>, - volatile: bool, - ) -> Attribute<'a> { - Attribute { - name, - namespace, - volatile, - value: value.into_value(self.bump()), - mounted_element: Cell::new(ElementId(0)), - } - } - - /// Create a new [`VNode::Component`] - pub fn component<'a, P, A, F: ComponentReturn<'a, A>>( - &'a self, - component: fn(Scope<'a, P>) -> F, - props: P, - fn_name: &'static str, - ) -> DynamicNode<'a> - where - P: Properties + 'a, - { - let as_component = component; - let vcomp = VProps::new(as_component, P::memoize, props); - let as_dyn: Box> = Box::new(vcomp); - let extended: Box = unsafe { std::mem::transmute(as_dyn) }; - - // let as_dyn: &dyn AnyProps = self.bump().alloc(vcomp); - // todo: clean up borrowed props - // if !P::IS_STATIC { - // let vcomp = ex; - // let vcomp = unsafe { std::mem::transmute(vcomp) }; - // self.items.borrow_mut().borrowed_props.push(vcomp); - // } - - DynamicNode::Component(VComponent { - name: fn_name, - render_fn: component as *const (), - static_props: P::IS_STATIC, - props: Cell::new(Some(extended)), - scope: Cell::new(None), - }) - } - - /// Create a new [`EventHandler`] from an [`FnMut`] - pub fn event_handler<'a, T>(&'a self, f: impl FnMut(T) + 'a) -> EventHandler<'a, T> { - let handler: &mut dyn FnMut(T) = self.bump().alloc(f); - let caller = unsafe { BumpBox::from_raw(handler as *mut dyn FnMut(T)) }; - let callback = RefCell::new(Some(caller)); - EventHandler { callback } - } -} - +#[doc(hidden)] pub trait ComponentReturn<'a, A = ()> { fn into_return(self, cx: &'a ScopeState) -> RenderReturn<'a>; } + impl<'a> ComponentReturn<'a> for Element<'a> { fn into_return(self, _cx: &ScopeState) -> RenderReturn<'a> { RenderReturn::Sync(self) } } +#[doc(hidden)] pub struct AsyncMarker; impl<'a, F> ComponentReturn<'a, AsyncMarker> for F where @@ -142,6 +44,7 @@ impl<'a> RenderReturn<'a> { } } +#[doc(hidden)] pub trait IntoDynNode<'a, A = ()> { fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a>; } @@ -191,19 +94,19 @@ impl<'a, 'b> IntoDynNode<'a> for LazyNodes<'a, 'b> { impl<'a> IntoDynNode<'_> for &'a str { fn into_vnode(self, cx: &ScopeState) -> DynamicNode { - cx.text(format_args!("{}", self)) + cx.text_node(format_args!("{}", self)) } } impl IntoDynNode<'_> for String { fn into_vnode(self, cx: &ScopeState) -> DynamicNode { - cx.text(format_args!("{}", self)) + cx.text_node(format_args!("{}", self)) } } impl<'b> IntoDynNode<'b> for Arguments<'_> { fn into_vnode(self, cx: &'b ScopeState) -> DynamicNode<'b> { - cx.text(self) + cx.text_node(self) } } @@ -235,6 +138,7 @@ impl<'a, 'b> IntoTemplate<'a> for LazyNodes<'a, 'b> { } // Note that we're using the E as a generic but this is never crafted anyways. +#[doc(hidden)] pub struct FromNodeIterator; impl<'a, T, I> IntoDynNode<'a, FromNodeIterator> for T where diff --git a/packages/core/src/lazynodes.rs b/packages/core/src/lazynodes.rs index 76c25641f..35333ca40 100644 --- a/packages/core/src/lazynodes.rs +++ b/packages/core/src/lazynodes.rs @@ -3,7 +3,7 @@ //! This module provides support for a type called `LazyNodes` which is a micro-heap located on the stack to make calls //! to `rsx!` more efficient. //! -//! To support returning rsx! from branches in match statements, we need to use dynamic dispatch on [`NodeFactory`] closures. +//! To support returning rsx! from branches in match statements, we need to use dynamic dispatch on [`ScopeState`] closures. //! //! This can be done either through boxing directly, or by using dynamic-sized-types and a custom allocator. In our case, //! we build a tiny alloactor in the stack and allocate the closure into that. @@ -27,13 +27,11 @@ pub struct LazyNodes<'a, 'b> { inner: StackNodeStorage<'a, 'b>, } -pub type NodeFactory<'a> = &'a ScopeState; - type StackHeapSize = [usize; 16]; enum StackNodeStorage<'a, 'b> { Stack(LazyStack), - Heap(Box>) -> Option> + 'b>), + Heap(Box) -> Option> + 'b>), } impl<'a, 'b> LazyNodes<'a, 'b> { @@ -42,11 +40,11 @@ impl<'a, 'b> LazyNodes<'a, 'b> { /// If the closure cannot fit into the stack allocation (16 bytes), then it /// is placed on the heap. Most closures will fit into the stack, and is /// the most optimal way to use the creation function. - pub fn new(val: impl FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b) -> Self { + pub fn new(val: impl FnOnce(&'a ScopeState) -> VNode<'a> + 'b) -> Self { // there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch let mut slot = Some(val); - let val = move |fac: Option>| { + let val = move |fac: Option<&'a ScopeState>| { fac.map( slot.take() .expect("LazyNodes closure to be called only once"), @@ -67,13 +65,13 @@ impl<'a, 'b> LazyNodes<'a, 'b> { /// Create a new [`LazyNodes`] closure, but force it onto the heap. pub fn new_boxed(inner: F) -> Self where - F: FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b, + F: FnOnce(&'a ScopeState) -> VNode<'a> + 'b, { // there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch let mut slot = Some(inner); Self { - inner: StackNodeStorage::Heap(Box::new(move |fac: Option>| { + inner: StackNodeStorage::Heap(Box::new(move |fac: Option<&'a ScopeState>| { fac.map( slot.take() .expect("LazyNodes closure to be called only once"), @@ -84,9 +82,9 @@ impl<'a, 'b> LazyNodes<'a, 'b> { unsafe fn new_inner(val: F) -> Self where - F: FnMut(Option>) -> Option> + 'b, + F: FnMut(Option<&'a ScopeState>) -> Option> + 'b, { - let mut ptr: *const _ = &val as &dyn FnMut(Option>) -> Option>; + let mut ptr: *const _ = &val as &dyn FnMut(Option<&'a ScopeState>) -> Option>; assert_eq!( ptr as *const u8, &val as *const _ as *const u8, @@ -162,12 +160,10 @@ impl<'a, 'b> LazyNodes<'a, 'b> { /// ```rust, ignore /// let f = LazyNodes::new(move |f| f.element("div", [], [], [] None)); /// - /// let fac = NodeFactory::new(&cx); - /// /// let node = f.call(cac); /// ``` #[must_use] - pub fn call(self, f: NodeFactory<'a>) -> VNode<'a> { + pub fn call(self, f: &'a ScopeState) -> VNode<'a> { match self.inner { StackNodeStorage::Heap(mut lazy) => { lazy(Some(f)).expect("Closure should not be called twice") @@ -184,18 +180,18 @@ struct LazyStack { } impl LazyStack { - fn call<'a>(&mut self, f: NodeFactory<'a>) -> VNode<'a> { + fn call<'a>(&mut self, f: &'a ScopeState) -> VNode<'a> { let LazyStack { buf, .. } = self; let data = buf.as_ref(); let info_size = - mem::size_of::<*mut dyn FnMut(Option>) -> Option>>() + mem::size_of::<*mut dyn FnMut(Option<&'a ScopeState>) -> Option>>() / mem::size_of::() - 1; let info_ofs = data.len() - info_size; - let g: *mut dyn FnMut(Option>) -> Option> = + let g: *mut dyn FnMut(Option<&'a ScopeState>) -> Option> = unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) }; self.dropped = true; @@ -210,14 +206,14 @@ impl Drop for LazyStack { let LazyStack { buf, .. } = self; let data = buf.as_ref(); - let info_size = mem::size_of::< - *mut dyn FnMut(Option>) -> Option>, - >() / mem::size_of::() - - 1; + let info_size = + mem::size_of::<*mut dyn FnMut(Option<&ScopeState>) -> Option>>() + / mem::size_of::() + - 1; let info_ofs = data.len() - info_size; - let g: *mut dyn FnMut(Option>) -> Option> = + let g: *mut dyn FnMut(Option<&ScopeState>) -> Option> = unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) }; self.dropped = true; @@ -252,73 +248,3 @@ unsafe fn make_fat_ptr(data_ptr: usize, meta_vals: &[usize]) -> *mut fn round_to_words(len: usize) -> usize { (len + mem::size_of::() - 1) / mem::size_of::() } - -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::innerlude::{Element, Scope, VirtualDom}; - -// #[test] -// fn it_works() { -// fn app(cx: Scope<()>) -> Element { -// cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!")))) -// } - -// let mut dom = VirtualDom::new(app); -// dom.rebuild(); - -// let g = dom.base_scope().root_node(); -// dbg!(g); -// } - -// #[test] -// fn it_drops() { -// use std::rc::Rc; - -// struct AppProps { -// inner: Rc, -// } - -// fn app(cx: Scope) -> Element { -// struct DropInner { -// id: i32, -// } -// impl Drop for DropInner { -// fn drop(&mut self) { -// eprintln!("dropping inner"); -// } -// } - -// let caller = { -// let it = (0..10).map(|i| { -// let val = cx.props.inner.clone(); -// LazyNodes::new(move |f| { -// eprintln!("hell closure"); -// let inner = DropInner { id: i }; -// f.text(format_args!("hello world {:?}, {:?}", inner.id, val)) -// }) -// }); - -// LazyNodes::new(|f| { -// eprintln!("main closure"); -// f.fragment_from_iter(it) -// }) -// }; - -// cx.render(caller) -// } - -// let inner = Rc::new(0); -// let mut dom = VirtualDom::new_with_props( -// app, -// AppProps { -// inner: inner.clone(), -// }, -// ); -// dom.rebuild(); - -// drop(dom); - -// assert_eq!(Rc::strong_count(&inner), 1); -// } -// } diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 8f9aa8f33..c4d10493c 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -1,4 +1,5 @@ #![doc = include_str!("../README.md")] +#![warn(missing_docs)] mod any_props; mod arena; @@ -67,43 +68,13 @@ pub(crate) mod innerlude { /// ) /// ``` pub type Component

= fn(Scope

) -> Element; - - /// A list of attributes - pub type Attributes<'a> = Option<&'a [Attribute<'a>]>; } pub use crate::innerlude::{ - // AnyAttributeValue, AnyEvent, - fc_to_builder, - Attribute, - AttributeValue, - Attributes, - Component, - DynamicNode, - Element, - ElementId, - Fragment, - LazyNodes, - Mutation, - Mutations, - NodeFactory, - Properties, - RenderReturn, - Scope, - ScopeId, - ScopeState, - Scoped, - SuspenseBoundary, - SuspenseContext, - TaskId, - Template, - TemplateAttribute, - TemplateNode, - UiEvent, - VComponent, - VNode, - VText, - VirtualDom, + fc_to_builder, Attribute, AttributeValue, Component, DynamicNode, Element, ElementId, Event, + Fragment, LazyNodes, Mutation, Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, + Scoped, SuspenseBoundary, SuspenseContext, TaskId, Template, TemplateAttribute, TemplateNode, + VComponent, VNode, VText, VirtualDom, }; /// The purpose of this module is to alleviate imports of many common types @@ -111,9 +82,9 @@ pub use crate::innerlude::{ /// This includes types like [`Scope`], [`Element`], and [`Component`]. pub mod prelude { pub use crate::innerlude::{ - fc_to_builder, Element, EventHandler, Fragment, LazyNodes, NodeFactory, Properties, Scope, - ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, UiEvent, - VNode, VirtualDom, + fc_to_builder, Element, Event, EventHandler, Fragment, LazyNodes, Properties, Scope, + ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, VNode, + VirtualDom, }; } @@ -121,28 +92,4 @@ pub mod exports { //! Important dependencies that are used by the rest of the library //! Feel free to just add the dependencies in your own Crates.toml pub use bumpalo; - pub use futures_channel; -} - -#[macro_export] -/// A helper macro for using hooks in async environements. -/// -/// # Usage -/// -/// -/// ```ignore -/// let (data) = use_ref(&cx, || {}); -/// -/// let handle_thing = move |_| { -/// to_owned![data] -/// cx.spawn(async move { -/// // do stuff -/// }); -/// } -/// ``` -macro_rules! to_owned { - ($($es:ident),+) => {$( - #[allow(unused_mut)] - let mut $es = $es.to_owned(); - )*} } diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 750013703..208ec65ab 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -1,45 +1,50 @@ +use fxhash::FxHashSet; + use crate::{arena::ElementId, ScopeId}; +/// A container for all the relevant steps to modify the Real DOM +/// +/// This method returns "mutations" - IE the necessary changes to get the RealDOM to match the VirtualDOM. It also +/// includes a list of NodeRefs that need to be applied and effects that need to be triggered after the RealDOM has +/// applied the edits. +/// +/// Mutations are the only link between the RealDOM and the VirtualDOM. #[derive(Debug, Default)] #[must_use = "not handling edits can lead to visual inconsistencies in UI"] pub struct Mutations<'a> { + /// The ID of the subtree that these edits are targetting pub subtree: usize, - pub template_mutations: Vec>, - pub edits: Vec>, + + /// The list of Scopes that were diffed, created, and removed during the Diff process. + pub dirty_scopes: FxHashSet, + + /// Any mutations required to build the templates using [`Mutations`] + pub template_edits: Vec>, + + /// Any mutations required to patch the renderer to match the layout of the VirtualDom + pub dom_edits: Vec>, } impl<'a> Mutations<'a> { - /// A useful tool for testing mutations - /// /// Rewrites IDs to just be "template", so you can compare the mutations + /// + /// Used really only for testing pub fn santize(mut self) -> Self { - for edit in self - .template_mutations + self.template_edits .iter_mut() - .chain(self.edits.iter_mut()) - { - match edit { + .chain(self.dom_edits.iter_mut()) + .for_each(|edit| match edit { Mutation::LoadTemplate { name, .. } => *name = "template", Mutation::SaveTemplate { name, .. } => *name = "template", _ => {} - } - } + }); self } -} -impl<'a> std::ops::Deref for Mutations<'a> { - type Target = Vec>; - - fn deref(&self) -> &Self::Target { - &self.edits - } -} - -impl std::ops::DerefMut for Mutations<'_> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.edits + /// Push a new mutation into the dom_edits list + pub(crate) fn push(&mut self, mutation: Mutation<'static>) { + self.dom_edits.push(mutation) } } @@ -54,7 +59,11 @@ each subtree has its own numbering scheme )] #[derive(Debug, PartialEq, Eq)] pub enum Mutation<'a> { + /// Pop the topmost node from our stack and append them to the node + /// at the top of the stack. AppendChildren { + /// How many nodes should be popped from the stack. + /// The node remaining on the stack will be the target for the append. m: usize, }, @@ -122,36 +131,61 @@ pub enum Mutation<'a> { m: usize, }, + /// Save the top m nodes as a placeholder SaveTemplate { + /// The name of the template that we're saving name: &'static str, + + /// How many nodes are being saved into this template m: usize, }, + /// Set the value of a node's attribute. SetAttribute { + /// The name of the attribute to set. name: &'a str, + /// The value of the attribute. value: &'a str, + + /// The ID of the node to set the attribute of. id: ElementId, - // value: &'bump str, /// The (optional) namespace of the attribute. /// For instance, "style" is in the "style" namespace. ns: Option<&'a str>, }, + /// Set the value of a node's attribute. SetStaticAttribute { + /// The name of the attribute to set. name: &'a str, + + /// The value of the attribute. value: &'a str, + + /// The (optional) namespace of the attribute. + /// For instance, "style" is in the "style" namespace. ns: Option<&'a str>, }, + /// Set the value of a node's attribute. SetBoolAttribute { + /// The name of the attribute to set. name: &'a str, + + /// The value of the attribute. value: bool, + + /// The ID of the node to set the attribute of. id: ElementId, }, + /// Set the textcontent of a node. SetText { + /// The textcontent of the node value: &'a str, + + /// The ID of the node to set the textcontent of. id: ElementId, }, @@ -175,11 +209,16 @@ pub enum Mutation<'a> { /// The ID of the node to remove. id: ElementId, }, + + /// Remove a particular node from the DOM Remove { + /// The ID of the node to remove. id: ElementId, }, + /// Push the given root node onto our stack. PushRoot { + /// The ID of the root node to push. id: ElementId, }, } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 19955be64..9c6eb6cd9 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -1,4 +1,4 @@ -use crate::{any_props::AnyProps, arena::ElementId, Element, ScopeId, ScopeState, UiEvent}; +use crate::{any_props::AnyProps, arena::ElementId, Element, Event, ScopeId, ScopeState}; use bumpalo::boxed::Box as BumpBox; use std::{ any::{Any, TypeId}, @@ -70,7 +70,6 @@ pub struct Template<'a> { pub attr_paths: &'a [&'a [u8]], } -/// A weird-ish variant of VNodes with way more limited types #[derive(Debug, Clone, Copy)] pub enum TemplateNode<'a> { Element { @@ -98,7 +97,7 @@ impl<'a> DynamicNode<'a> { matches!(self, DynamicNode::Component(_)) } pub fn placeholder() -> Self { - Self::Placeholder(Cell::new(ElementId(0))) + Self::Placeholder(Default::default()) } } @@ -156,18 +155,18 @@ pub enum AttributeValue<'a> { None, } -type ListenerCb<'a> = BumpBox<'a, dyn FnMut(UiEvent) + 'a>; +type ListenerCb<'a> = BumpBox<'a, dyn FnMut(Event) + 'a>; impl<'a> AttributeValue<'a> { pub fn new_listener( cx: &'a ScopeState, - mut callback: impl FnMut(UiEvent) + 'a, + mut callback: impl FnMut(Event) + 'a, ) -> AttributeValue<'a> { let boxed: BumpBox<'a, dyn FnMut(_) + 'a> = unsafe { - BumpBox::from_raw(cx.bump().alloc(move |event: UiEvent| { + BumpBox::from_raw(cx.bump().alloc(move |event: Event| { if let Ok(data) = event.data.downcast::() { - callback(UiEvent { - bubbles: event.bubbles, + callback(Event { + propogates: event.propogates, data, }) } diff --git a/packages/core/src/scheduler/suspense.rs b/packages/core/src/scheduler/suspense.rs index e73be438a..dde0acb63 100644 --- a/packages/core/src/scheduler/suspense.rs +++ b/packages/core/src/scheduler/suspense.rs @@ -15,25 +15,11 @@ pub type SuspenseContext = Rc; /// Essentially a fiber in React pub struct SuspenseBoundary { - pub id: ScopeId, - pub waiting_on: RefCell>, - pub mutations: RefCell>, - pub placeholder: Cell>, - - pub created_on_stack: Cell, - - // whenever the suspense resolves, we call this onresolve function - // this lets us do things like putting up a loading spinner - // - // todo: we need a way of controlling whether or not a component hides itself but still processes changes - // If we run into suspense, we perform a diff, so its important that the old elements are still around. - // - // When the timer expires, I imagine a container could hide the elements and show the spinner. This, however, - // can not be - pub onresolve: Option>, - - /// Called when - pub onstart: Option>, + pub(crate) id: ScopeId, + pub(crate) waiting_on: RefCell>, + pub(crate) mutations: RefCell>, + pub(crate) placeholder: Cell>, + pub(crate) created_on_stack: Cell, } impl SuspenseBoundary { @@ -44,8 +30,6 @@ impl SuspenseBoundary { mutations: RefCell::new(Mutations::default()), placeholder: Cell::new(None), created_on_stack: Cell::new(0), - onresolve: None, - onstart: None, } } } diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 722621ec4..91fb0b8b2 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -49,7 +49,7 @@ impl VirtualDom { .and_then(|id| self.scopes.get_mut(id.0).map(|f| f.as_mut() as *mut _)) } - pub fn ensure_drop_safety(&self, scope: ScopeId) { + fn ensure_drop_safety(&self, scope: ScopeId) { let scope = &self.scopes[scope.0]; let node = unsafe { scope.previous_frame().try_load_node() }; diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 776414563..f78fc7bf1 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -1,18 +1,21 @@ use crate::{ any_props::AnyProps, + any_props::VProps, arena::ElementId, bump_frame::BumpFrame, - factory::RenderReturn, + factory::{ComponentReturn, IntoAttributeValue, IntoDynNode, RenderReturn}, + innerlude::{DynamicNode, EventHandler, VComponent, VText}, innerlude::{Scheduler, SchedulerMsg}, lazynodes::LazyNodes, - Element, TaskId, + Attribute, Element, Properties, TaskId, }; -use bumpalo::Bump; -use std::future::Future; +use bumpalo::{boxed::Box as BumpBox, Bump}; use std::{ any::{Any, TypeId}, cell::{Cell, RefCell}, collections::{HashMap, HashSet}, + fmt::Arguments, + future::Future, rc::Rc, sync::Arc, }; @@ -59,9 +62,9 @@ impl<'a, T> std::ops::Deref for Scoped<'a, T> { #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)] pub struct ScopeId(pub usize); -/// A component's state. +/// A component's state separate from its props. /// -/// This struct stores all the important information about a component's state without the props. +/// This struct exists to provide a common interface for all scopes without relying on generics. pub struct ScopeState { pub(crate) render_cnt: Cell, @@ -86,7 +89,7 @@ pub struct ScopeState { pub(crate) placeholder: Cell>, } -impl ScopeState { +impl<'src> ScopeState { pub(crate) fn current_frame(&self) -> &BumpFrame { match self.render_cnt.get() % 2 { 0 => &self.node_arena_1, @@ -359,10 +362,101 @@ impl ScopeState { /// cx.render(lazy_tree) /// } ///``` - pub fn render<'src>(&'src self, rsx: LazyNodes<'src, '_>) -> Element<'src> { + pub fn render(&'src self, rsx: LazyNodes<'src, '_>) -> Element<'src> { Ok(rsx.call(self)) } + /// Create a dynamic text node using [`Arguments`] and the [`ScopeState`]'s internal [`Bump`] allocator + pub fn text_node(&'src self, args: Arguments) -> DynamicNode<'src> { + DynamicNode::Text(VText { + value: self.raw_text(args), + id: Default::default(), + }) + } + + /// Allocate some text inside the [`ScopeState`] from [`Arguments`] + /// + /// Uses the currently active [`Bump`] allocator + pub fn raw_text(&'src self, args: Arguments) -> &'src str { + args.as_str().unwrap_or_else(|| { + use bumpalo::core_alloc::fmt::Write; + let mut str_buf = bumpalo::collections::String::new_in(self.bump()); + str_buf.write_fmt(args).unwrap(); + str_buf.into_bump_str() + }) + } + + /// Convert any item that implements [`IntoDynNode`] into a [`DynamicNode`] using the internal [`Bump`] allocator + pub fn make_node<'c, I>(&'src self, into: impl IntoDynNode<'src, I> + 'c) -> DynamicNode { + into.into_vnode(self) + } + + /// Create a new [`Attribute`] from a name, value, namespace, and volatile bool + /// + /// "Volatile" referes to whether or not Dioxus should always override the value. This helps prevent the UI in + /// some renderers stay in sync with the VirtualDom's understanding of the world + pub fn attr( + &'src self, + name: &'static str, + value: impl IntoAttributeValue<'src>, + namespace: Option<&'static str>, + volatile: bool, + ) -> Attribute<'src> { + Attribute { + name, + namespace, + volatile, + mounted_element: Default::default(), + value: value.into_value(self.bump()), + } + } + + /// Create a new [`DynamicNode::Component`] variant + /// + /// + /// The given component can be any of four signatures. Remember that an [`Element`] is really a [`Result`]. + /// + /// ```rust, ignore + /// // Without explicit props + /// fn(Scope) -> Element; + /// async fn(Scope<'_>) -> Element; + /// + /// // With explicit props + /// fn(Scope) -> Element; + /// async fn(Scope>) -> Element; + /// ``` + pub fn component>( + &'src self, + component: fn(Scope<'src, P>) -> F, + props: P, + fn_name: &'static str, + ) -> DynamicNode<'src> + where + P: Properties + 'src, + { + let vcomp = VProps::new(component, P::memoize, props); + + // cast off the lifetime of the render return + let as_dyn: Box + '_> = Box::new(vcomp); + let extended: Box + 'src> = unsafe { std::mem::transmute(as_dyn) }; + + DynamicNode::Component(VComponent { + name: fn_name, + render_fn: component as *const (), + static_props: P::IS_STATIC, + props: Cell::new(Some(extended)), + scope: Cell::new(None), + }) + } + + /// Create a new [`EventHandler`] from an [`FnMut`] + pub fn event_handler(&'src self, f: impl FnMut(T) + 'src) -> EventHandler<'src, T> { + let handler: &mut dyn FnMut(T) = self.bump().alloc(f); + let caller = unsafe { BumpBox::from_raw(handler as *mut dyn FnMut(T)) }; + let callback = RefCell::new(Some(caller)); + EventHandler { callback } + } + /// Store a value between renders. The foundational hook for all other hooks. /// /// Accepts an `initializer` closure, which is run on the first use of the hook (typically the initial render). The return value of this closure is stored for the lifetime of the component, and a mutable reference to it is provided on every render as the return value of `use_hook`. diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 77619e674..41a54d391 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -11,7 +11,7 @@ use crate::{ nodes::{Template, TemplateId}, scheduler::{SuspenseBoundary, SuspenseId}, scopes::{ScopeId, ScopeState}, - AttributeValue, Element, Scope, SuspenseContext, UiEvent, + AttributeValue, Element, Event, Scope, SuspenseContext, }; use futures_util::{pin_mut, StreamExt}; use slab::Slab; @@ -350,8 +350,8 @@ impl VirtualDom { let mut listeners = vec![]; // We will clone this later. The data itself is wrapped in RC to be used in callbacks if required - let uievent = UiEvent { - bubbles: Rc::new(Cell::new(bubbles)), + let uievent = Event { + propogates: Rc::new(Cell::new(bubbles)), data, }; @@ -395,7 +395,7 @@ impl VirtualDom { cb(uievent.clone()); } - if !uievent.bubbles.get() { + if !uievent.propogates.get() { return; } } @@ -534,9 +534,12 @@ impl VirtualDom { let context = scope.has_context::().unwrap(); self.mutations - .extend(context.mutations.borrow_mut().template_mutations.drain(..)); + .template_edits + .extend(context.mutations.borrow_mut().template_edits.drain(..)); + self.mutations - .extend(context.mutations.borrow_mut().drain(..)); + .dom_edits + .extend(context.mutations.borrow_mut().dom_edits.drain(..)); // TODO: count how many nodes are on the stack? self.mutations.push(Mutation::ReplaceWith { @@ -556,7 +559,7 @@ impl VirtualDom { } // Save the current mutations length so we can split them into boundary - let mutations_to_this_point = self.mutations.len(); + let mutations_to_this_point = self.mutations.dom_edits.len(); // Run the scope and get the mutations self.run_scope(dirty.id); @@ -575,7 +578,8 @@ impl VirtualDom { boundary_mut .mutations .borrow_mut() - .extend(self.mutations.split_off(mutations_to_this_point)); + .dom_edits + .extend(self.mutations.dom_edits.split_off(mutations_to_this_point)); // Attach suspended leaves boundary diff --git a/packages/core/tests/README.md b/packages/core/tests/README.md index 63293c741..eea7cec3a 100644 --- a/packages/core/tests/README.md +++ b/packages/core/tests/README.md @@ -1,9 +1,6 @@ # Testing of Dioxus core -NodeFactory -- [] rsx, html, NodeFactory generate the same structures - Diffing - [x] create elements - [x] create text @@ -19,15 +16,13 @@ Diffing - [x] keyed diffing - [x] keyed diffing out of order - [x] keyed diffing with prefix/suffix -- [x] suspended nodes work +- [x] suspended nodes work Lifecycle - [] Components mount properly - [] Components create new child components - [] Replaced components unmount old components and mount new - [] Post-render effects are called -- [] - Shared Context - [] Shared context propagates downwards @@ -37,7 +32,7 @@ Suspense - [] use_suspense generates suspended nodes -Hooks +Hooks - [] Drop order is maintained - [] Shared hook state is okay - [] use_hook works diff --git a/packages/core/tests/attr_cleanup.rs b/packages/core/tests/attr_cleanup.rs index 99ce9a01b..d3e98769a 100644 --- a/packages/core/tests/attr_cleanup.rs +++ b/packages/core/tests/attr_cleanup.rs @@ -23,7 +23,7 @@ fn attrs_cycle() { }); assert_eq!( - dom.rebuild().santize().edits, + dom.rebuild().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, AppendChildren { m: 1 }, @@ -32,7 +32,7 @@ fn attrs_cycle() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, AssignId { path: &[0,], id: ElementId(3,) }, @@ -44,7 +44,7 @@ fn attrs_cycle() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(3) }, ReplaceWith { id: ElementId(2), m: 1 } @@ -53,7 +53,7 @@ fn attrs_cycle() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(2) }, AssignId { path: &[0], id: ElementId(1) }, @@ -66,7 +66,7 @@ fn attrs_cycle() { // we take the node taken by attributes since we reused it dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1) }, ReplaceWith { id: ElementId(2), m: 1 } diff --git a/packages/core/tests/boolattrs.rs b/packages/core/tests/boolattrs.rs index 5187e00f9..7688e5a49 100644 --- a/packages/core/tests/boolattrs.rs +++ b/packages/core/tests/boolattrs.rs @@ -5,7 +5,7 @@ use dioxus::prelude::*; fn bool_test() { let mut app = VirtualDom::new(|cx| cx.render(rsx!(div { hidden: false }))); assert_eq!( - app.rebuild().santize().edits, + app.rebuild().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1) }, SetBoolAttribute { name: "hidden", value: false, id: ElementId(1,) }, diff --git a/packages/core/tests/borrowedstate.rs b/packages/core/tests/borrowedstate.rs index 3da5c8d94..1f1214189 100644 --- a/packages/core/tests/borrowedstate.rs +++ b/packages/core/tests/borrowedstate.rs @@ -8,7 +8,7 @@ fn test_borrowed_state() { let mut dom = VirtualDom::new(Parent); assert_eq!( - dom.rebuild().santize().edits, + dom.rebuild().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, diff --git a/packages/core/tests/context_api.rs b/packages/core/tests/context_api.rs index 2ec1ea18b..b16b2433d 100644 --- a/packages/core/tests/context_api.rs +++ b/packages/core/tests/context_api.rs @@ -20,7 +20,7 @@ fn state_shares() { let mut dom = VirtualDom::new(app); assert_eq!( - dom.rebuild().santize().edits, + dom.rebuild().santize().dom_edits, [ CreateTextNode { value: "Value is 0", id: ElementId(1,) }, AppendChildren { m: 1 }, @@ -37,7 +37,7 @@ fn state_shares() { dom.mark_dirty(ScopeId(2)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [SetText { value: "Value is 2", id: ElementId(1,) },] ); @@ -45,7 +45,7 @@ fn state_shares() { dom.mark_dirty(ScopeId(2)); let edits = dom.render_immediate(); assert_eq!( - edits.santize().edits, + edits.santize().dom_edits, [SetText { value: "Value is 3", id: ElementId(1,) },] ); } diff --git a/packages/core/tests/create_dom.rs b/packages/core/tests/create_dom.rs index 98e7706bd..8eda5783b 100644 --- a/packages/core/tests/create_dom.rs +++ b/packages/core/tests/create_dom.rs @@ -23,7 +23,7 @@ fn test_original_diff() { let edits = dom.rebuild().santize(); assert_eq!( - edits.template_mutations, + edits.template_edits, [ // create template CreateElement { name: "div" }, @@ -36,7 +36,7 @@ fn test_original_diff() { ); assert_eq!( - edits.edits, + edits.dom_edits, [ // add to root LoadTemplate { name: "template", index: 0, id: ElementId(1) }, @@ -67,7 +67,7 @@ fn create() { let edits = dom.rebuild().santize(); assert_eq!( - edits.template_mutations, + edits.template_edits, [ // create template CreateElement { name: "div" }, @@ -99,7 +99,7 @@ fn create_list() { let edits = dom.rebuild().santize(); assert_eq!( - edits.template_mutations, + edits.template_edits, [ // create template CreateElement { name: "div" }, @@ -123,7 +123,7 @@ fn create_simple() { let edits = dom.rebuild().santize(); assert_eq!( - edits.template_mutations, + edits.template_edits, [ // create template CreateElement { name: "div" }, @@ -160,7 +160,7 @@ fn create_components() { let edits = dom.rebuild().santize(); assert_eq!( - edits.template_mutations, + edits.template_edits, [ // The "child" template CreateElement { name: "h1" }, @@ -196,7 +196,7 @@ fn anchors() { // note that the template under "false" doesn't show up since it's not loaded let edits = dom.rebuild().santize(); assert_eq!( - edits.template_mutations, + edits.template_edits, [ // create each template CreateElement { name: "div" }, @@ -207,7 +207,7 @@ fn anchors() { ); assert_eq!( - edits.edits, + edits.dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1) }, CreatePlaceholder { id: ElementId(2) }, diff --git a/packages/core/tests/create_element.rs b/packages/core/tests/create_element.rs index ff9d485b6..e6331f9ac 100644 --- a/packages/core/tests/create_element.rs +++ b/packages/core/tests/create_element.rs @@ -12,7 +12,7 @@ fn multiroot() { }); assert_eq!( - dom.rebuild().santize().template_mutations, + dom.rebuild().santize().template_edits, [ CreateElement { name: "div" }, CreateStaticText { value: "Hello a" }, diff --git a/packages/core/tests/create_fragments.rs b/packages/core/tests/create_fragments.rs index 5b67711a8..33bba1eaa 100644 --- a/packages/core/tests/create_fragments.rs +++ b/packages/core/tests/create_fragments.rs @@ -14,7 +14,7 @@ fn empty_fragment_creates_nothing() { let edits = vdom.rebuild(); assert_eq!( - edits.edits, + edits.dom_edits, [ CreatePlaceholder { id: ElementId(1) }, AppendChildren { m: 1 } @@ -32,7 +32,7 @@ fn root_fragments_work() { }); assert_eq!( - vdom.rebuild().edits.last().unwrap(), + vdom.rebuild().dom_edits.last().unwrap(), &AppendChildren { m: 2 } ); } @@ -59,7 +59,7 @@ fn fragments_nested() { }); assert_eq!( - vdom.rebuild().edits.last().unwrap(), + vdom.rebuild().dom_edits.last().unwrap(), &AppendChildren { m: 8 } ); } @@ -84,7 +84,7 @@ fn fragments_across_components() { } assert_eq!( - VirtualDom::new(app).rebuild().edits.last().unwrap(), + VirtualDom::new(app).rebuild().dom_edits.last().unwrap(), &AppendChildren { m: 8 } ); } @@ -98,7 +98,7 @@ fn list_fragments() { )) } assert_eq!( - VirtualDom::new(app).rebuild().edits.last().unwrap(), + VirtualDom::new(app).rebuild().dom_edits.last().unwrap(), &AppendChildren { m: 7 } ); } diff --git a/packages/core/tests/create_lists.rs b/packages/core/tests/create_lists.rs index 27540d9f0..e29044514 100644 --- a/packages/core/tests/create_lists.rs +++ b/packages/core/tests/create_lists.rs @@ -28,7 +28,7 @@ fn list_renders() { let edits = dom.rebuild().santize(); assert_eq!( - edits.template_mutations, + edits.template_edits, [ // Create the outer div CreateElement { name: "div" }, @@ -51,7 +51,7 @@ fn list_renders() { ); assert_eq!( - edits.edits, + edits.dom_edits, [ // Load the outer div LoadTemplate { name: "template", index: 0, id: ElementId(1) }, diff --git a/packages/core/tests/create_passthru.rs b/packages/core/tests/create_passthru.rs index b8916156b..2ffdf0065 100644 --- a/packages/core/tests/create_passthru.rs +++ b/packages/core/tests/create_passthru.rs @@ -26,7 +26,7 @@ fn nested_passthru_creates() { let edits = dom.rebuild().santize(); assert_eq!( - edits.edits, + edits.dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1) }, AppendChildren { m: 1 }, @@ -64,7 +64,7 @@ fn nested_passthru_creates_add() { let mut dom = VirtualDom::new(app); assert_eq!( - dom.rebuild().santize().edits, + dom.rebuild().santize().dom_edits, [ // load 1 LoadTemplate { name: "template", index: 0, id: ElementId(1) }, @@ -92,11 +92,11 @@ fn dynamic_node_as_root() { let edits = dom.rebuild().santize(); // Since the roots were all dynamic, they should not cause any template muations - assert_eq!(edits.template_mutations, []); + assert_eq!(edits.template_edits, []); // The root node is text, so we just create it on the spot assert_eq!( - edits.edits, + edits.dom_edits, [ CreateTextNode { value: "123", id: ElementId(1) }, CreateTextNode { value: "456", id: ElementId(2) }, diff --git a/packages/core/tests/cycle.rs b/packages/core/tests/cycle.rs index 72e1a028d..87ede2271 100644 --- a/packages/core/tests/cycle.rs +++ b/packages/core/tests/cycle.rs @@ -14,7 +14,7 @@ fn cycling_elements() { let edits = dom.rebuild().santize(); assert_eq!( - edits.edits, + edits.dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, AppendChildren { m: 1 }, @@ -23,7 +23,7 @@ fn cycling_elements() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, ReplaceWith { id: ElementId(1,), m: 1 }, @@ -33,7 +33,7 @@ fn cycling_elements() { // notice that the IDs cycle back to ElementId(1), preserving a minimal memory footprint dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, ReplaceWith { id: ElementId(2,), m: 1 }, @@ -42,7 +42,7 @@ fn cycling_elements() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, ReplaceWith { id: ElementId(1,), m: 1 }, diff --git a/packages/core/tests/diff_component.rs b/packages/core/tests/diff_component.rs index 6e72f04cd..0f6ec615c 100644 --- a/packages/core/tests/diff_component.rs +++ b/packages/core/tests/diff_component.rs @@ -61,7 +61,7 @@ fn component_swap() { let mut dom = VirtualDom::new(app); let edits = dom.rebuild().santize(); assert_eq!( - edits.edits, + edits.dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1) }, LoadTemplate { name: "template", index: 0, id: ElementId(2) }, @@ -75,7 +75,7 @@ fn component_swap() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(6) }, ReplaceWith { id: ElementId(5), m: 1 } @@ -84,7 +84,7 @@ fn component_swap() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(5) }, ReplaceWith { id: ElementId(6), m: 1 } @@ -93,7 +93,7 @@ fn component_swap() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(6) }, ReplaceWith { id: ElementId(5), m: 1 } diff --git a/packages/core/tests/diff_element.rs b/packages/core/tests/diff_element.rs index 3ab9b284a..4ca97dcdb 100644 --- a/packages/core/tests/diff_element.rs +++ b/packages/core/tests/diff_element.rs @@ -14,19 +14,19 @@ fn text_diff() { vdom.mark_dirty(ScopeId(0)); assert_eq!( - vdom.render_immediate().edits, + vdom.render_immediate().dom_edits, [SetText { value: "hello 1", id: ElementId(2) }] ); vdom.mark_dirty(ScopeId(0)); assert_eq!( - vdom.render_immediate().edits, + vdom.render_immediate().dom_edits, [SetText { value: "hello 2", id: ElementId(2) }] ); vdom.mark_dirty(ScopeId(0)); assert_eq!( - vdom.render_immediate().edits, + vdom.render_immediate().dom_edits, [SetText { value: "hello 3", id: ElementId(2) }] ); } @@ -48,7 +48,7 @@ fn element_swap() { vdom.mark_dirty(ScopeId(0)); assert_eq!( - vdom.render_immediate().santize().edits, + vdom.render_immediate().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, ReplaceWith { id: ElementId(1,), m: 1 }, @@ -57,7 +57,7 @@ fn element_swap() { vdom.mark_dirty(ScopeId(0)); assert_eq!( - vdom.render_immediate().santize().edits, + vdom.render_immediate().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, ReplaceWith { id: ElementId(2,), m: 1 }, @@ -66,7 +66,7 @@ fn element_swap() { vdom.mark_dirty(ScopeId(0)); assert_eq!( - vdom.render_immediate().santize().edits, + vdom.render_immediate().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, ReplaceWith { id: ElementId(1,), m: 1 }, @@ -75,7 +75,7 @@ fn element_swap() { vdom.mark_dirty(ScopeId(0)); assert_eq!( - vdom.render_immediate().santize().edits, + vdom.render_immediate().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, ReplaceWith { id: ElementId(2,), m: 1 }, diff --git a/packages/core/tests/diff_keyed_list.rs b/packages/core/tests/diff_keyed_list.rs index 6acbaebbb..a2c00e439 100644 --- a/packages/core/tests/diff_keyed_list.rs +++ b/packages/core/tests/diff_keyed_list.rs @@ -21,7 +21,7 @@ fn keyed_diffing_out_of_order() { }); assert_eq!( - dom.rebuild().santize().edits, + dom.rebuild().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, @@ -39,7 +39,7 @@ fn keyed_diffing_out_of_order() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().edits, + dom.render_immediate().dom_edits, [ PushRoot { id: ElementId(7,) }, InsertBefore { id: ElementId(5,), m: 1 }, @@ -64,7 +64,7 @@ fn keyed_diffing_out_of_order_adds() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().edits, + dom.render_immediate().dom_edits, [ PushRoot { id: ElementId(5,) }, PushRoot { id: ElementId(4,) }, @@ -90,7 +90,7 @@ fn keyed_diffing_out_of_order_adds_3() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().edits, + dom.render_immediate().dom_edits, [ PushRoot { id: ElementId(5,) }, PushRoot { id: ElementId(4,) }, @@ -116,7 +116,7 @@ fn keyed_diffing_out_of_order_adds_4() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().edits, + dom.render_immediate().dom_edits, [ PushRoot { id: ElementId(5,) }, PushRoot { id: ElementId(4,) }, @@ -142,7 +142,7 @@ fn keyed_diffing_out_of_order_adds_5() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().edits, + dom.render_immediate().dom_edits, [ PushRoot { id: ElementId(5,) }, InsertBefore { id: ElementId(4,), m: 1 }, @@ -167,7 +167,7 @@ fn keyed_diffing_additions() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(6) }, LoadTemplate { name: "template", index: 0, id: ElementId(7) }, @@ -192,7 +192,7 @@ fn keyed_diffing_additions_and_moves_on_ends() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [ // create 11, 12 LoadTemplate { name: "template", index: 0, id: ElementId(5) }, @@ -222,7 +222,7 @@ fn keyed_diffing_additions_and_moves_in_middle() { // LIS: 4, 5, 6 dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [ // create 5, 6 LoadTemplate { name: "template", index: 0, id: ElementId(5) }, @@ -256,7 +256,7 @@ fn controlled_keyed_diffing_out_of_order() { // LIS: 5, 6 dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [ // remove 7 Remove { id: ElementId(4,) }, @@ -289,7 +289,7 @@ fn controlled_keyed_diffing_out_of_order_max_test() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [ Remove { id: ElementId(5,) }, LoadTemplate { name: "template", index: 0, id: ElementId(5) }, @@ -318,7 +318,7 @@ fn remove_list() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [ Remove { id: ElementId(3) }, Remove { id: ElementId(4) }, @@ -343,7 +343,7 @@ fn no_common_keys() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [ Remove { id: ElementId(2) }, Remove { id: ElementId(3) }, diff --git a/packages/core/tests/diff_unkeyed_list.rs b/packages/core/tests/diff_unkeyed_list.rs index 5252d1ece..75c12aec0 100644 --- a/packages/core/tests/diff_unkeyed_list.rs +++ b/packages/core/tests/diff_unkeyed_list.rs @@ -17,7 +17,7 @@ fn list_creates_one_by_one() { // load the div and then assign the empty fragment as a placeholder assert_eq!( - dom.rebuild().santize().edits, + dom.rebuild().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, AssignId { path: &[0], id: ElementId(2,) }, @@ -28,7 +28,7 @@ fn list_creates_one_by_one() { // Rendering the first item should replace the placeholder with an element dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(3,) }, HydrateText { path: &[0], value: "0", id: ElementId(4,) }, @@ -39,7 +39,7 @@ fn list_creates_one_by_one() { // Rendering the next item should insert after the previous dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, HydrateText { path: &[0], value: "1", id: ElementId(5,) }, @@ -50,7 +50,7 @@ fn list_creates_one_by_one() { // ... and again! dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(6,) }, HydrateText { path: &[0], value: "2", id: ElementId(7,) }, @@ -61,7 +61,7 @@ fn list_creates_one_by_one() { // once more dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(8,) }, HydrateText { path: &[0], value: "3", id: ElementId(9,) }, @@ -86,7 +86,7 @@ fn removes_one_by_one() { // load the div and then assign the empty fragment as a placeholder assert_eq!( - dom.rebuild().santize().edits, + dom.rebuild().santize().dom_edits, [ // The container LoadTemplate { name: "template", index: 0, id: ElementId(1) }, @@ -108,14 +108,14 @@ fn removes_one_by_one() { // Rendering the first item should replace the placeholder with an element dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [Remove { id: ElementId(6) }] ); // Remove div(2) dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [Remove { id: ElementId(4) }] ); @@ -123,7 +123,7 @@ fn removes_one_by_one() { // todo: this should just be a remove with no placeholder dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [ CreatePlaceholder { id: ElementId(3) }, ReplaceWith { id: ElementId(2), m: 1 } @@ -134,7 +134,7 @@ fn removes_one_by_one() { // todo: this should actually be append to, but replace placeholder is fine for now dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(2) }, HydrateText { path: &[0], value: "0", id: ElementId(4) }, @@ -161,7 +161,7 @@ fn list_shrink_multiroot() { }); assert_eq!( - dom.rebuild().santize().edits, + dom.rebuild().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, AssignId { path: &[0,], id: ElementId(2,) }, @@ -171,7 +171,7 @@ fn list_shrink_multiroot() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(3) }, HydrateText { path: &[0], value: "0", id: ElementId(4) }, @@ -183,7 +183,7 @@ fn list_shrink_multiroot() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(2) }, HydrateText { path: &[0], value: "1", id: ElementId(7) }, @@ -195,7 +195,7 @@ fn list_shrink_multiroot() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(10) }, HydrateText { path: &[0], value: "2", id: ElementId(11) }, @@ -223,7 +223,7 @@ fn removes_one_by_one_multiroot() { // load the div and then assign the empty fragment as a placeholder assert_eq!( - dom.rebuild().santize().edits, + dom.rebuild().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1) }, // @@ -250,19 +250,19 @@ fn removes_one_by_one_multiroot() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [Remove { id: ElementId(10) }, Remove { id: ElementId(12) }] ); dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [Remove { id: ElementId(6) }, Remove { id: ElementId(8) }] ); dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().edits, + dom.render_immediate().santize().dom_edits, [ Remove { id: ElementId(4) }, CreatePlaceholder { id: ElementId(5) }, @@ -282,7 +282,7 @@ fn two_equal_fragments_are_equal_static() { }); _ = dom.rebuild(); - assert!(dom.render_immediate().edits.is_empty()); + assert!(dom.render_immediate().dom_edits.is_empty()); } #[test] @@ -296,7 +296,7 @@ fn two_equal_fragments_are_equal() { }); _ = dom.rebuild(); - assert!(dom.render_immediate().edits.is_empty()); + assert!(dom.render_immediate().dom_edits.is_empty()); } #[test] @@ -315,9 +315,9 @@ fn remove_many() { }); let edits = dom.rebuild().santize(); - assert!(edits.template_mutations.is_empty()); + assert!(edits.template_edits.is_empty()); assert_eq!( - edits.edits, + edits.dom_edits, [ CreatePlaceholder { id: ElementId(1,) }, AppendChildren { m: 1 }, @@ -327,7 +327,7 @@ fn remove_many() { dom.mark_dirty(ScopeId(0)); let edits = dom.render_immediate().santize(); assert_eq!( - edits.edits, + edits.dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, HydrateText { path: &[0,], value: "hello 0", id: ElementId(3,) }, @@ -338,7 +338,7 @@ fn remove_many() { dom.mark_dirty(ScopeId(0)); let edits = dom.render_immediate().santize(); assert_eq!( - edits.edits, + edits.dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, HydrateText { path: &[0,], value: "hello 1", id: ElementId(4,) }, @@ -355,7 +355,7 @@ fn remove_many() { dom.mark_dirty(ScopeId(0)); let edits = dom.render_immediate().santize(); assert_eq!( - edits.edits, + edits.dom_edits, [ Remove { id: ElementId(1,) }, Remove { id: ElementId(5,) }, @@ -369,7 +369,7 @@ fn remove_many() { dom.mark_dirty(ScopeId(0)); let edits = dom.render_immediate().santize(); assert_eq!( - edits.edits, + edits.dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, HydrateText { path: &[0,], value: "hello 0", id: ElementId(10,) }, diff --git a/packages/core/tests/kitchen_sink.rs b/packages/core/tests/kitchen_sink.rs index 7f719f772..737980938 100644 --- a/packages/core/tests/kitchen_sink.rs +++ b/packages/core/tests/kitchen_sink.rs @@ -30,7 +30,7 @@ fn dual_stream() { use Mutation::*; assert_eq!( - edits.template_mutations, + edits.template_edits, [ CreateElement { name: "div" }, SetStaticAttribute { name: "class", value: "asd", ns: None }, @@ -66,7 +66,7 @@ fn dual_stream() { ); assert_eq!( - edits.edits, + edits.dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1) }, SetAttribute { name: "class", value: "123", id: ElementId(1), ns: None }, diff --git a/packages/core/tests/lifecycle.rs b/packages/core/tests/lifecycle.rs index ab7be8793..bdceb2332 100644 --- a/packages/core/tests/lifecycle.rs +++ b/packages/core/tests/lifecycle.rs @@ -28,7 +28,7 @@ fn manual_diffing() { *value.lock().unwrap() = "goodbye"; assert_eq!( - dom.rebuild().santize().edits, + dom.rebuild().santize().dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(3) }, HydrateText { path: &[0], value: "goodbye", id: ElementId(4) }, @@ -62,7 +62,7 @@ fn events_generate() { let edits = dom.render_immediate(); assert_eq!( - edits.edits, + edits.dom_edits, [ CreatePlaceholder { id: ElementId(2) }, ReplaceWith { id: ElementId(1), m: 1 } diff --git a/packages/core/tests/suspense.rs b/packages/core/tests/suspense.rs index 481ade2e3..4e70ec807 100644 --- a/packages/core/tests/suspense.rs +++ b/packages/core/tests/suspense.rs @@ -13,7 +13,7 @@ async fn it_works() { // We should at least get the top-level template in before pausing for the children assert_eq!( - mutations.template_mutations, + mutations.template_edits, [ CreateElement { name: "div" }, CreateStaticText { value: "Waiting for child..." }, @@ -25,7 +25,7 @@ async fn it_works() { // And we should load it in and assign the placeholder properly assert_eq!( - mutations.edits, + mutations.dom_edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1) }, // hmmmmmmmmm.... with suspense how do we guarantee that IDs increase linearly? diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs index 19ac205b4..4eca36db1 100644 --- a/packages/desktop/src/controller.rs +++ b/packages/desktop/src/controller.rs @@ -54,8 +54,8 @@ impl DesktopController { { let edits = dom.rebuild(); let mut queue = edit_queue.lock().unwrap(); - queue.push(serde_json::to_string(&edits.template_mutations).unwrap()); - queue.push(serde_json::to_string(&edits.edits).unwrap()); + queue.push(serde_json::to_string(&edits.template_edits).unwrap()); + queue.push(serde_json::to_string(&edits.dom_edits).unwrap()); proxy.send_event(UserWindowEvent::EditsReady).unwrap(); } @@ -79,8 +79,8 @@ impl DesktopController { { let mut queue = edit_queue.lock().unwrap(); - queue.push(serde_json::to_string(&muts.template_mutations).unwrap()); - queue.push(serde_json::to_string(&muts.edits).unwrap()); + queue.push(serde_json::to_string(&muts.template_edits).unwrap()); + queue.push(serde_json::to_string(&muts.dom_edits).unwrap()); let _ = proxy.send_event(UserWindowEvent::EditsReady); } } diff --git a/packages/dioxus/benches/jsframework.rs b/packages/dioxus/benches/jsframework.rs index 24ca2fe69..281d36904 100644 --- a/packages/dioxus/benches/jsframework.rs +++ b/packages/dioxus/benches/jsframework.rs @@ -46,7 +46,7 @@ fn create_rows(c: &mut Criterion) { b.iter(|| { let g = dom.rebuild(); - assert!(g.edits.len() > 1); + assert!(g.dom_edits.len() > 1); }) }); } diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index 1332975ea..4eadea1fd 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -1,5 +1,25 @@ -// #![deny(missing_docs)] -//! Useful foundational hooks for Dioxus +#[macro_export] +/// A helper macro for using hooks in async environements. +/// +/// # Usage +/// +/// +/// ```ignore +/// let (data) = use_ref(&cx, || {}); +/// +/// let handle_thing = move |_| { +/// to_owned![data] +/// cx.spawn(async move { +/// // do stuff +/// }); +/// } +/// ``` +macro_rules! to_owned { + ($($es:ident),+) => {$( + #[allow(unused_mut)] + let mut $es = $es.to_owned(); + )*} +} mod usestate; pub use usestate::{use_state, UseState}; diff --git a/packages/hooks/src/usestate.rs b/packages/hooks/src/usestate.rs index 15ba5fd3d..a2d0242d8 100644 --- a/packages/hooks/src/usestate.rs +++ b/packages/hooks/src/usestate.rs @@ -39,7 +39,7 @@ pub fn use_state( let update_callback = cx.schedule_update(); let slot = Rc::new(RefCell::new(current_val.clone())); let setter = Rc::new({ - dioxus_core::to_owned![update_callback, slot]; + to_owned![update_callback, slot]; move |new| { { let mut slot = slot.borrow_mut(); diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index 5107e2cda..c5b8af9e1 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -11,7 +11,7 @@ macro_rules! impl_event { ) => { $( $( #[$attr] )* - pub fn $name<'a>(_cx: &'a ::dioxus_core::ScopeState, _f: impl FnMut(::dioxus_core::UiEvent<$data>) + 'a) -> ::dioxus_core::Attribute<'a> { + pub fn $name<'a>(_cx: &'a ::dioxus_core::ScopeState, _f: impl FnMut(::dioxus_core::Event<$data>) + 'a) -> ::dioxus_core::Attribute<'a> { ::dioxus_core::Attribute { name: stringify!($name), value: ::dioxus_core::AttributeValue::new_listener(_cx, _f), diff --git a/packages/html/src/events/animation.rs b/packages/html/src/events/animation.rs index 2e576e578..fb6dc64ca 100644 --- a/packages/html/src/events/animation.rs +++ b/packages/html/src/events/animation.rs @@ -1,6 +1,6 @@ -use dioxus_core::UiEvent; +use dioxus_core::Event; -pub type AnimationEvent = UiEvent; +pub type AnimationEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone)] diff --git a/packages/html/src/events/clipboard.rs b/packages/html/src/events/clipboard.rs index 9b8365401..e7031d061 100644 --- a/packages/html/src/events/clipboard.rs +++ b/packages/html/src/events/clipboard.rs @@ -1,6 +1,6 @@ -use dioxus_core::UiEvent; +use dioxus_core::Event; -pub type ClipboardEvent = UiEvent; +pub type ClipboardEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone)] pub struct ClipboardData { diff --git a/packages/html/src/events/composition.rs b/packages/html/src/events/composition.rs index d29171787..59009c1f0 100644 --- a/packages/html/src/events/composition.rs +++ b/packages/html/src/events/composition.rs @@ -1,6 +1,6 @@ -use dioxus_core::UiEvent; +use dioxus_core::Event; -pub type CompositionEvent = UiEvent; +pub type CompositionEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone)] pub struct CompositionData { diff --git a/packages/html/src/events/drag.rs b/packages/html/src/events/drag.rs index f0fc8774a..5dd7f5c71 100644 --- a/packages/html/src/events/drag.rs +++ b/packages/html/src/events/drag.rs @@ -1,10 +1,10 @@ use std::any::Any; -use dioxus_core::UiEvent; +use dioxus_core::Event; use crate::MouseData; -pub type DragEvent = UiEvent; +pub type DragEvent = Event; /// The DragEvent interface is a DOM event that represents a drag and drop interaction. The user initiates a drag by /// placing a pointer device (such as a mouse) on the touch surface and then dragging the pointer to a new location diff --git a/packages/html/src/events/focus.rs b/packages/html/src/events/focus.rs index d402684fd..b2a4f5205 100644 --- a/packages/html/src/events/focus.rs +++ b/packages/html/src/events/focus.rs @@ -1,6 +1,6 @@ -use dioxus_core::UiEvent; +use dioxus_core::Event; -pub type FocusEvent = UiEvent; +pub type FocusEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone)] diff --git a/packages/html/src/events/form.rs b/packages/html/src/events/form.rs index b003e24c7..5e0b6a17e 100644 --- a/packages/html/src/events/form.rs +++ b/packages/html/src/events/form.rs @@ -1,8 +1,8 @@ use std::{collections::HashMap, fmt::Debug, sync::Arc}; -use dioxus_core::UiEvent; +use dioxus_core::Event; -pub type FormEvent = UiEvent; +pub type FormEvent = Event; /* DOMEvent: Send + SyncTarget relatedTarget */ #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] diff --git a/packages/html/src/events/image.rs b/packages/html/src/events/image.rs index deb6ae48c..06430c4c2 100644 --- a/packages/html/src/events/image.rs +++ b/packages/html/src/events/image.rs @@ -1,6 +1,6 @@ -use dioxus_core::UiEvent; +use dioxus_core::Event; -pub type ImageEvent = UiEvent; +pub type ImageEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone)] pub struct ImageData { diff --git a/packages/html/src/events/keyboard.rs b/packages/html/src/events/keyboard.rs index 040b2658e..ddf720770 100644 --- a/packages/html/src/events/keyboard.rs +++ b/packages/html/src/events/keyboard.rs @@ -1,11 +1,11 @@ use crate::input_data::{decode_key_location, encode_key_location}; -use dioxus_core::UiEvent; +use dioxus_core::Event; use keyboard_types::{Code, Key, Location, Modifiers}; use std::convert::TryInto; use std::fmt::{Debug, Formatter}; use std::str::FromStr; -pub type KeyboardEvent = UiEvent; +pub type KeyboardEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone)] pub struct KeyboardData { diff --git a/packages/html/src/events/media.rs b/packages/html/src/events/media.rs index a3433131f..0b2fe70e0 100644 --- a/packages/html/src/events/media.rs +++ b/packages/html/src/events/media.rs @@ -1,6 +1,6 @@ -use dioxus_core::UiEvent; +use dioxus_core::Event; -pub type MediaEvent = UiEvent; +pub type MediaEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone)] pub struct MediaData {} diff --git a/packages/html/src/events/mouse.rs b/packages/html/src/events/mouse.rs index 9738fb80f..a0fda7be6 100644 --- a/packages/html/src/events/mouse.rs +++ b/packages/html/src/events/mouse.rs @@ -2,11 +2,11 @@ use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenP use crate::input_data::{ decode_mouse_button_set, encode_mouse_button_set, MouseButton, MouseButtonSet, }; -use dioxus_core::UiEvent; +use dioxus_core::Event; use keyboard_types::Modifiers; use std::fmt::{Debug, Formatter}; -pub type MouseEvent = UiEvent; +pub type MouseEvent = Event; /// A synthetic event that wraps a web-style [`MouseEvent`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] diff --git a/packages/html/src/events/pointer.rs b/packages/html/src/events/pointer.rs index a87dbe9a5..23b0aba71 100644 --- a/packages/html/src/events/pointer.rs +++ b/packages/html/src/events/pointer.rs @@ -1,6 +1,6 @@ -use dioxus_core::UiEvent; +use dioxus_core::Event; -pub type PointerEvent = UiEvent; +pub type PointerEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone)] pub struct PointerData { diff --git a/packages/html/src/events/scroll.rs b/packages/html/src/events/scroll.rs index e4b62e50a..0c2dd3143 100644 --- a/packages/html/src/events/scroll.rs +++ b/packages/html/src/events/scroll.rs @@ -1,6 +1,6 @@ -use dioxus_core::UiEvent; +use dioxus_core::Event; -pub type ScrollEvent = UiEvent; +pub type ScrollEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone)] pub struct ScrollData {} diff --git a/packages/html/src/events/selection.rs b/packages/html/src/events/selection.rs index e28eefcf9..ff1496c96 100644 --- a/packages/html/src/events/selection.rs +++ b/packages/html/src/events/selection.rs @@ -1,6 +1,6 @@ -use dioxus_core::UiEvent; +use dioxus_core::Event; -pub type SelectionEvent = UiEvent; +pub type SelectionEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone)] pub struct SelectionData {} diff --git a/packages/html/src/events/toggle.rs b/packages/html/src/events/toggle.rs index 448df05d1..1f0d3f6e7 100644 --- a/packages/html/src/events/toggle.rs +++ b/packages/html/src/events/toggle.rs @@ -1,6 +1,6 @@ -use dioxus_core::UiEvent; +use dioxus_core::Event; -pub type ToggleEvent = UiEvent; +pub type ToggleEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone)] pub struct ToggleData {} diff --git a/packages/html/src/events/touch.rs b/packages/html/src/events/touch.rs index 30855814a..1f219b22f 100644 --- a/packages/html/src/events/touch.rs +++ b/packages/html/src/events/touch.rs @@ -1,6 +1,6 @@ -use dioxus_core::UiEvent; +use dioxus_core::Event; -pub type TouchEvent = UiEvent; +pub type TouchEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone)] pub struct TouchData { diff --git a/packages/html/src/events/transition.rs b/packages/html/src/events/transition.rs index fe574c1ab..095496664 100644 --- a/packages/html/src/events/transition.rs +++ b/packages/html/src/events/transition.rs @@ -1,6 +1,6 @@ -use dioxus_core::UiEvent; +use dioxus_core::Event; -pub type TransitionEvent = UiEvent; +pub type TransitionEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone)] pub struct TransitionData { diff --git a/packages/html/src/events/wheel.rs b/packages/html/src/events/wheel.rs index 344d9179c..e6e06501b 100644 --- a/packages/html/src/events/wheel.rs +++ b/packages/html/src/events/wheel.rs @@ -1,10 +1,10 @@ -use dioxus_core::UiEvent; +use dioxus_core::Event; use euclid::UnknownUnit; use std::fmt::{Debug, Formatter}; use crate::geometry::{LinesVector, PagesVector, PixelsVector, WheelDelta}; -pub type WheelEvent = UiEvent; +pub type WheelEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone)] pub struct WheelData { diff --git a/packages/liveview/src/events.rs b/packages/liveview/src/events.rs index 841bdeeff..874d8081a 100644 --- a/packages/liveview/src/events.rs +++ b/packages/liveview/src/events.rs @@ -6,9 +6,8 @@ use std::any::Any; use std::sync::Arc; use dioxus_core::ElementId; -use dioxus_core::{EventPriority, UserEvent}; use dioxus_html::event_bubbles; -use dioxus_html::on::*; +use dioxus_html::events::*; #[derive(serde::Serialize, serde::Deserialize)] pub(crate) struct IpcMessage { @@ -30,29 +29,29 @@ struct ImEvent { contents: serde_json::Value, } -pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent { - let ImEvent { - event, - mounted_dom_id, - contents, - } = serde_json::from_value(val).unwrap(); +pub fn trigger_from_serialized(val: serde_json::Value) -> () { + todo!() + // let ImEvent { + // event, + // mounted_dom_id, + // contents, + // } = serde_json::from_value(val).unwrap(); - let mounted_dom_id = Some(mounted_dom_id); + // let mounted_dom_id = Some(mounted_dom_id); - let name = event_name_from_type(&event); - let event = make_synthetic_event(&event, contents); + // let name = event_name_from_type(&event); + // let event = make_synthetic_event(&event, contents); - UserEvent { - name, - priority: EventPriority::Low, - scope_id: None, - element: mounted_dom_id, - data: event, - bubbles: event_bubbles(name), - } + // UserEvent { + // name, + // scope_id: None, + // element: mounted_dom_id, + // data: event, + // bubbles: event_bubbles(name), + // } } -fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc { +fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc { match name { "copy" | "cut" | "paste" => { // diff --git a/packages/native-core/src/real_dom.rs b/packages/native-core/src/real_dom.rs index 20a055778..a0f071a15 100644 --- a/packages/native-core/src/real_dom.rs +++ b/packages/native-core/src/real_dom.rs @@ -67,7 +67,7 @@ impl RealDom { let mut nodes_updated = Vec::new(); nodes_updated.push((RealNodeId::ElementId(ElementId(0)), NodeMask::ALL)); for mutations in mutations_vec { - for e in mutations.edits { + for e in mutations.dom_edits { use dioxus_core::DomEdit::*; match e { AppendChildren { root, children } => { diff --git a/packages/native-core/src/utils.rs b/packages/native-core/src/utils.rs index aa61807a4..66e9906a2 100644 --- a/packages/native-core/src/utils.rs +++ b/packages/native-core/src/utils.rs @@ -72,7 +72,7 @@ impl PersistantElementIter { pub fn prune(&mut self, mutations: &Mutations, rdom: &RealDom) -> bool { let mut changed = false; let ids_removed: Vec<_> = mutations - .edits + .dom_edits .iter() .filter_map(|e| { // nodes within templates will never be removed @@ -97,7 +97,7 @@ impl PersistantElementIter { for (el_id, child_idx) in self.stack.iter_mut() { if let NodePosition::InChild(child_idx) = child_idx { if let NodeType::Element { children, .. } = &rdom[*el_id].node_data.node_type { - for m in &mutations.edits { + for m in &mutations.dom_edits { match m { DomEdit::Remove { root } => { let id = rdom.resolve_maybe_id(*root); diff --git a/packages/rsx/src/component.rs b/packages/rsx/src/component.rs index d1132fdae..090f7382d 100644 --- a/packages/rsx/src/component.rs +++ b/packages/rsx/src/component.rs @@ -219,7 +219,7 @@ impl ToTokens for ContentField { match self { ContentField::ManExpr(e) => e.to_tokens(tokens), ContentField::Formatted(s) => tokens.append_all(quote! { - __cx.raw_text(#s).0 + __cx.raw_text(#s) }), ContentField::OnHandlerRaw(e) => tokens.append_all(quote! { __cx.event_handler(#e) diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index 8d5a9da59..6cd5102de 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -77,7 +77,7 @@ impl ToTokens for CallBody { }) } else { out_tokens.append_all(quote! { - ::dioxus::core::LazyNodes::new( move | __cx: ::dioxus::core::NodeFactory| -> ::dioxus::core::VNode { + ::dioxus::core::LazyNodes::new( move | __cx: &::dioxus::core::ScopeState| -> ::dioxus::core::VNode { #body }) }) @@ -106,7 +106,7 @@ impl<'a> ToTokens for TemplateRenderer<'a> { }; let key_tokens = match key { - Some(tok) => quote! { Some( __cx.raw_text_inline(#tok) ) }, + Some(tok) => quote! { Some( __cx.raw_text(#tok) ) }, None => quote! { None }, }; diff --git a/packages/rsx/src/node.rs b/packages/rsx/src/node.rs index 8788e52d2..11ec1a0b2 100644 --- a/packages/rsx/src/node.rs +++ b/packages/rsx/src/node.rs @@ -131,10 +131,10 @@ impl ToTokens for BodyNode { BodyNode::Element(el) => el.to_tokens(tokens), BodyNode::Component(comp) => comp.to_tokens(tokens), BodyNode::Text(txt) => tokens.append_all(quote! { - __cx.text(#txt) + __cx.text_node(#txt) }), BodyNode::RawExpr(exp) => tokens.append_all(quote! { - __cx.fragment_from_iter(#exp) + __cx.make_node(#exp) }), BodyNode::ForLoop(exp) => { let ForLoop { @@ -144,7 +144,7 @@ impl ToTokens for BodyNode { let renderer = TemplateRenderer { roots: &body }; tokens.append_all(quote! { - __cx.fragment_from_iter( + __cx.make_node( (#expr).into_iter().map(|#pat| { #renderer }) ) }) @@ -152,7 +152,7 @@ impl ToTokens for BodyNode { BodyNode::IfChain(chain) => { if is_if_chain_terminated(chain) { tokens.append_all(quote! { - __cx.fragment_from_iter(#chain) + __cx.make_node(#chain) }); } else { let ExprIf { @@ -206,7 +206,7 @@ impl ToTokens for BodyNode { }); tokens.append_all(quote! { - __cx.fragment_from_iter(#body) + __cx.make_node(#body) }); } } diff --git a/packages/tui/src/focus.rs b/packages/tui/src/focus.rs index 9c070bc00..4a21df431 100644 --- a/packages/tui/src/focus.rs +++ b/packages/tui/src/focus.rs @@ -256,7 +256,7 @@ impl FocusState { if self.focus_iter.prune(mutations, rdom) { self.dirty = true; } - for m in &mutations.edits { + for m in &mutations.dom_edits { match m { dioxus_core::DomEdit::ReplaceWith { root, .. } => remove_children( &mut [&mut self.last_focused_id], diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index e211188c9..17b7978a2 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -19,36 +19,14 @@ use web_sys::{Document, Element, Event, HtmlElement}; use crate::Config; pub struct WebsysDom { - pub interpreter: Interpreter, - - pub(crate) root: Element, - - pub handler: Closure, + interpreter: Interpreter, + handler: Closure, + root: Element, } impl WebsysDom { pub fn new(cfg: Config, event_channel: mpsc::UnboundedSender) -> Self { // eventually, we just want to let the interpreter do all the work of decoding events into our event type - let callback: Box = Box::new(move |event: &web_sys::Event| { - _ = event_channel.unbounded_send(event.clone()); - - // if let Ok(synthetic_event) = decoded { - // // Try to prevent default if the attribute is set - // if let Some(node) = target.dyn_ref::() { - // if let Some(name) = node.get_attribute("dioxus-prevent-default") { - // if name == synthetic_event.name - // || name.trim_start_matches("on") == synthetic_event.name - // { - // log::trace!("Preventing default"); - // event.prevent_default(); - // } - // } - // } - - // sender_callback.as_ref()(SchedulerMsg::Event(synthetic_event)) - // } - }); - // a match here in order to avoid some error during runtime browser test let document = load_document(); let root = match document.get_element_by_id(&cfg.rootname) { @@ -58,8 +36,10 @@ impl WebsysDom { Self { interpreter: Interpreter::new(root.clone()), - handler: Closure::wrap(callback), root, + handler: Closure::wrap(Box::new(move |event: &web_sys::Event| { + let _ = event_channel.unbounded_send(event.clone()); + })), } } @@ -99,11 +79,10 @@ impl WebsysDom { } SetText { value, id } => i.SetText(id.0 as u32, value.into()), NewEventListener { name, scope, id } => { - let handler: &Function = self.handler.as_ref().unchecked_ref(); self.interpreter.NewEventListener( name, id.0 as u32, - handler, + self.handler.as_ref().unchecked_ref(), event_bubbles(&name[2..]), ); } diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 68ccf3cf5..443791ec1 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -192,8 +192,8 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop if should_hydrate { } else { let edits = dom.rebuild(); - websys_dom.apply_edits(edits.template_mutations); - websys_dom.apply_edits(edits.edits); + websys_dom.apply_edits(edits.template_edits); + websys_dom.apply_edits(edits.dom_edits); } let mut work_loop = ric_raf::RafLoop::new(); @@ -247,8 +247,8 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop log::debug!("edits {:#?}", edits); - websys_dom.apply_edits(edits.template_mutations); - websys_dom.apply_edits(edits.edits); + websys_dom.apply_edits(edits.template_edits); + websys_dom.apply_edits(edits.dom_edits); } } diff --git a/packages/web/tests/hydrate.rs b/packages/web/tests/hydrate.rs index d0b5acb9c..f295037bb 100644 --- a/packages/web/tests/hydrate.rs +++ b/packages/web/tests/hydrate.rs @@ -23,7 +23,7 @@ fn makes_tree() { let mut dom = VirtualDom::new(app); let muts = dom.rebuild(); - dbg!(muts.edits); + dbg!(muts.dom_edits); } #[wasm_bindgen_test] From 85657d3906c0ad5689e7414d676feb5544adae22 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 1 Dec 2022 00:46:15 -0500 Subject: [PATCH 16/18] feat: rip out mutations for templates --- packages/core/src/create.rs | 84 ++---------------------- packages/core/src/mutations.rs | 73 +++++--------------- packages/core/src/nodes.rs | 7 +- packages/core/src/virtual_dom.rs | 16 ++--- packages/core/tests/attr_cleanup.rs | 10 +-- packages/core/tests/boolattrs.rs | 2 +- packages/core/tests/borrowedstate.rs | 2 +- packages/core/tests/context_api.rs | 6 +- packages/core/tests/create_dom.rs | 16 ++--- packages/core/tests/create_element.rs | 2 +- packages/core/tests/create_fragments.rs | 10 +-- packages/core/tests/create_lists.rs | 4 +- packages/core/tests/create_passthru.rs | 8 +-- packages/core/tests/cycle.rs | 8 +-- packages/core/tests/diff_component.rs | 8 +-- packages/core/tests/diff_element.rs | 14 ++-- packages/core/tests/diff_keyed_list.rs | 26 ++++---- packages/core/tests/diff_unkeyed_list.rs | 52 +++++++-------- packages/core/tests/kitchen_sink.rs | 4 +- packages/core/tests/lifecycle.rs | 4 +- packages/core/tests/suspense.rs | 4 +- packages/desktop/src/controller.rs | 8 +-- packages/dioxus/benches/jsframework.rs | 2 +- packages/html/src/lib.rs | 2 + packages/html/src/render_template.rs | 41 ++++++++++++ packages/interpreter/src/bindings.rs | 28 ++------ packages/interpreter/src/interpreter.js | 33 ++-------- packages/native-core/src/real_dom.rs | 2 +- packages/native-core/src/utils.rs | 4 +- packages/tui/src/focus.rs | 2 +- packages/web/src/dom.rs | 78 ++++++++++++++++++---- packages/web/src/lib.rs | 22 ++++--- packages/web/tests/hydrate.rs | 2 +- 33 files changed, 270 insertions(+), 314 deletions(-) create mode 100644 packages/html/src/render_template.rs diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index c64763c5b..0559c3473 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -7,7 +7,7 @@ use crate::mutations::Mutation::*; use crate::nodes::VNode; use crate::nodes::{DynamicNode, TemplateNode}; use crate::virtual_dom::VirtualDom; -use crate::{AttributeValue, ElementId, ScopeId, SuspenseContext, TemplateAttribute}; +use crate::{AttributeValue, ElementId, ScopeId, SuspenseContext}; impl<'b> VirtualDom { /// Create a new template [`VNode`] and write it to the [`Mutations`] buffer. @@ -224,81 +224,7 @@ impl<'b> VirtualDom { return; } - for node in template.template.roots { - self.create_static_node(node); - } - - self.mutations.template_edits.push(SaveTemplate { - name: template.template.id, - m: template.template.roots.len(), - }); - } - - // todo: we shouldn't have any of instructions for building templates - renderers should be able to work with the - // template type directly, right? - pub(crate) fn create_static_node(&mut self, node: &'b TemplateNode<'static>) { - match *node { - // Todo: create the children's template - TemplateNode::Dynamic(_) => self - .mutations - .template_edits - .push(CreateStaticPlaceholder {}), - TemplateNode::Text(value) => self - .mutations - .template_edits - .push(CreateStaticText { value }), - - TemplateNode::DynamicText { .. } => { - self.mutations.template_edits.push(CreateTextPlaceholder) - } - - TemplateNode::Element { - attrs, - children, - namespace, - tag, - .. - } => { - match namespace { - Some(namespace) => self.mutations.template_edits.push(CreateElementNamespace { - name: tag, - namespace, - }), - None => self - .mutations - .template_edits - .push(CreateElement { name: tag }), - } - - self.mutations - .template_edits - .extend(attrs.iter().filter_map(|attr| match attr { - TemplateAttribute::Static { - name, - value, - namespace, - .. - } => Some(SetStaticAttribute { - name, - value, - ns: *namespace, - }), - _ => None, - })); - - if children.is_empty() { - return; - } - - children - .iter() - .for_each(|child| self.create_static_node(child)); - - self.mutations - .template_edits - .push(AppendChildren { m: children.len() }) - } - } + self.mutations.templates.push(template.template); } pub(crate) fn create_dynamic_node( @@ -404,7 +330,7 @@ impl<'b> VirtualDom { ) -> 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.dom_edits.len(); + let mutations_to_this_point = self.mutations.edits.len(); // Create the component's root element let created = self.create_scope(scope, new); @@ -430,10 +356,10 @@ impl<'b> VirtualDom { // Note that we break off dynamic mutations only - since static mutations aren't rendered immediately let split_off = unsafe { std::mem::transmute::, Vec>( - self.mutations.dom_edits.split_off(mutations_to_this_point), + self.mutations.edits.split_off(mutations_to_this_point), ) }; - boundary.mutations.borrow_mut().dom_edits.extend(split_off); + boundary.mutations.borrow_mut().edits.extend(split_off); boundary.created_on_stack.set(created); boundary .waiting_on diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 208ec65ab..0369c2da8 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -1,6 +1,6 @@ use fxhash::FxHashSet; -use crate::{arena::ElementId, ScopeId}; +use crate::{arena::ElementId, ScopeId, Template}; /// A container for all the relevant steps to modify the Real DOM /// @@ -18,11 +18,13 @@ pub struct Mutations<'a> { /// The list of Scopes that were diffed, created, and removed during the Diff process. pub dirty_scopes: FxHashSet, - /// Any mutations required to build the templates using [`Mutations`] - pub template_edits: Vec>, + /// Any templates encountered while diffing the DOM. + /// + /// These must be loaded into a cache before applying the edits + pub templates: Vec>, /// Any mutations required to patch the renderer to match the layout of the VirtualDom - pub dom_edits: Vec>, + pub edits: Vec>, } impl<'a> Mutations<'a> { @@ -30,21 +32,22 @@ impl<'a> Mutations<'a> { /// /// Used really only for testing pub fn santize(mut self) -> Self { - self.template_edits - .iter_mut() - .chain(self.dom_edits.iter_mut()) - .for_each(|edit| match edit { - Mutation::LoadTemplate { name, .. } => *name = "template", - Mutation::SaveTemplate { name, .. } => *name = "template", - _ => {} - }); + todo!() + // self.templates + // .iter_mut() + // .chain(self.dom_edits.iter_mut()) + // .for_each(|edit| match edit { + // Mutation::LoadTemplate { name, .. } => *name = "template", + // Mutation::SaveTemplate { name, .. } => *name = "template", + // _ => {} + // }); - self + // self } /// Push a new mutation into the dom_edits list pub(crate) fn push(&mut self, mutation: Mutation<'static>) { - self.dom_edits.push(mutation) + self.edits.push(mutation) } } @@ -59,34 +62,14 @@ each subtree has its own numbering scheme )] #[derive(Debug, PartialEq, Eq)] pub enum Mutation<'a> { - /// Pop the topmost node from our stack and append them to the node - /// at the top of the stack. - AppendChildren { - /// How many nodes should be popped from the stack. - /// The node remaining on the stack will be the target for the append. - m: usize, - }, - AssignId { path: &'static [u8], id: ElementId, }, - CreateElement { - name: &'a str, - }, - CreateElementNamespace { - name: &'a str, - namespace: &'a str, - }, CreatePlaceholder { id: ElementId, }, - CreateStaticPlaceholder, - CreateTextPlaceholder, - CreateStaticText { - value: &'a str, - }, CreateTextNode { value: &'a str, id: ElementId, @@ -131,15 +114,6 @@ pub enum Mutation<'a> { m: usize, }, - /// Save the top m nodes as a placeholder - SaveTemplate { - /// The name of the template that we're saving - name: &'static str, - - /// How many nodes are being saved into this template - m: usize, - }, - /// Set the value of a node's attribute. SetAttribute { /// The name of the attribute to set. @@ -155,19 +129,6 @@ pub enum Mutation<'a> { ns: Option<&'a str>, }, - /// Set the value of a node's attribute. - SetStaticAttribute { - /// The name of the attribute to set. - name: &'a str, - - /// The value of the attribute. - value: &'a str, - - /// The (optional) namespace of the attribute. - /// For instance, "style" is in the "style" namespace. - ns: Option<&'a str>, - }, - /// Set the value of a node's attribute. SetBoolAttribute { /// The name of the attribute to set. diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 9c6eb6cd9..df8c96def 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -62,6 +62,7 @@ impl<'a> VNode<'a> { } } +#[cfg_attr(feature = "serialize", derive(serde::Serialize))] #[derive(Debug, Clone, Copy)] pub struct Template<'a> { pub id: &'a str, @@ -71,6 +72,7 @@ pub struct Template<'a> { } #[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize))] pub enum TemplateNode<'a> { Element { tag: &'a str, @@ -126,11 +128,12 @@ pub struct VText<'a> { } #[derive(Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub enum TemplateAttribute<'a> { Static { - name: &'static str, + name: &'a str, value: &'a str, - namespace: Option<&'static str>, + namespace: Option<&'a str>, volatile: bool, }, Dynamic(usize), diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 41a54d391..7954413ca 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -490,7 +490,7 @@ impl VirtualDom { // Rebuilding implies we append the created elements to the root RenderReturn::Sync(Ok(node)) => { let m = self.create_scope(ScopeId(0), node); - self.mutations.push(Mutation::AppendChildren { m }); + // self.mutations.push(Mutation::AppendChildren { m }); } // If an error occurs, we should try to render the default error component and context where the error occured RenderReturn::Sync(Err(e)) => panic!("Cannot catch errors during rebuild {:?}", e), @@ -534,12 +534,12 @@ impl VirtualDom { let context = scope.has_context::().unwrap(); self.mutations - .template_edits - .extend(context.mutations.borrow_mut().template_edits.drain(..)); + .templates + .extend(context.mutations.borrow_mut().templates.drain(..)); self.mutations - .dom_edits - .extend(context.mutations.borrow_mut().dom_edits.drain(..)); + .edits + .extend(context.mutations.borrow_mut().edits.drain(..)); // TODO: count how many nodes are on the stack? self.mutations.push(Mutation::ReplaceWith { @@ -559,7 +559,7 @@ impl VirtualDom { } // Save the current mutations length so we can split them into boundary - let mutations_to_this_point = self.mutations.dom_edits.len(); + let mutations_to_this_point = self.mutations.edits.len(); // Run the scope and get the mutations self.run_scope(dirty.id); @@ -578,8 +578,8 @@ impl VirtualDom { boundary_mut .mutations .borrow_mut() - .dom_edits - .extend(self.mutations.dom_edits.split_off(mutations_to_this_point)); + .edits + .extend(self.mutations.edits.split_off(mutations_to_this_point)); // Attach suspended leaves boundary diff --git a/packages/core/tests/attr_cleanup.rs b/packages/core/tests/attr_cleanup.rs index d3e98769a..99ce9a01b 100644 --- a/packages/core/tests/attr_cleanup.rs +++ b/packages/core/tests/attr_cleanup.rs @@ -23,7 +23,7 @@ fn attrs_cycle() { }); assert_eq!( - dom.rebuild().santize().dom_edits, + dom.rebuild().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, AppendChildren { m: 1 }, @@ -32,7 +32,7 @@ fn attrs_cycle() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, AssignId { path: &[0,], id: ElementId(3,) }, @@ -44,7 +44,7 @@ fn attrs_cycle() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(3) }, ReplaceWith { id: ElementId(2), m: 1 } @@ -53,7 +53,7 @@ fn attrs_cycle() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(2) }, AssignId { path: &[0], id: ElementId(1) }, @@ -66,7 +66,7 @@ fn attrs_cycle() { // we take the node taken by attributes since we reused it dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1) }, ReplaceWith { id: ElementId(2), m: 1 } diff --git a/packages/core/tests/boolattrs.rs b/packages/core/tests/boolattrs.rs index 7688e5a49..5187e00f9 100644 --- a/packages/core/tests/boolattrs.rs +++ b/packages/core/tests/boolattrs.rs @@ -5,7 +5,7 @@ use dioxus::prelude::*; fn bool_test() { let mut app = VirtualDom::new(|cx| cx.render(rsx!(div { hidden: false }))); assert_eq!( - app.rebuild().santize().dom_edits, + app.rebuild().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1) }, SetBoolAttribute { name: "hidden", value: false, id: ElementId(1,) }, diff --git a/packages/core/tests/borrowedstate.rs b/packages/core/tests/borrowedstate.rs index 1f1214189..3da5c8d94 100644 --- a/packages/core/tests/borrowedstate.rs +++ b/packages/core/tests/borrowedstate.rs @@ -8,7 +8,7 @@ fn test_borrowed_state() { let mut dom = VirtualDom::new(Parent); assert_eq!( - dom.rebuild().santize().dom_edits, + dom.rebuild().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, diff --git a/packages/core/tests/context_api.rs b/packages/core/tests/context_api.rs index b16b2433d..2ec1ea18b 100644 --- a/packages/core/tests/context_api.rs +++ b/packages/core/tests/context_api.rs @@ -20,7 +20,7 @@ fn state_shares() { let mut dom = VirtualDom::new(app); assert_eq!( - dom.rebuild().santize().dom_edits, + dom.rebuild().santize().edits, [ CreateTextNode { value: "Value is 0", id: ElementId(1,) }, AppendChildren { m: 1 }, @@ -37,7 +37,7 @@ fn state_shares() { dom.mark_dirty(ScopeId(2)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [SetText { value: "Value is 2", id: ElementId(1,) },] ); @@ -45,7 +45,7 @@ fn state_shares() { dom.mark_dirty(ScopeId(2)); let edits = dom.render_immediate(); assert_eq!( - edits.santize().dom_edits, + edits.santize().edits, [SetText { value: "Value is 3", id: ElementId(1,) },] ); } diff --git a/packages/core/tests/create_dom.rs b/packages/core/tests/create_dom.rs index 8eda5783b..594cea580 100644 --- a/packages/core/tests/create_dom.rs +++ b/packages/core/tests/create_dom.rs @@ -23,7 +23,7 @@ fn test_original_diff() { let edits = dom.rebuild().santize(); assert_eq!( - edits.template_edits, + edits.templates, [ // create template CreateElement { name: "div" }, @@ -36,7 +36,7 @@ fn test_original_diff() { ); assert_eq!( - edits.dom_edits, + edits.edits, [ // add to root LoadTemplate { name: "template", index: 0, id: ElementId(1) }, @@ -67,7 +67,7 @@ fn create() { let edits = dom.rebuild().santize(); assert_eq!( - edits.template_edits, + edits.templates, [ // create template CreateElement { name: "div" }, @@ -99,7 +99,7 @@ fn create_list() { let edits = dom.rebuild().santize(); assert_eq!( - edits.template_edits, + edits.templates, [ // create template CreateElement { name: "div" }, @@ -123,7 +123,7 @@ fn create_simple() { let edits = dom.rebuild().santize(); assert_eq!( - edits.template_edits, + edits.templates, [ // create template CreateElement { name: "div" }, @@ -160,7 +160,7 @@ fn create_components() { let edits = dom.rebuild().santize(); assert_eq!( - edits.template_edits, + edits.templates, [ // The "child" template CreateElement { name: "h1" }, @@ -196,7 +196,7 @@ fn anchors() { // note that the template under "false" doesn't show up since it's not loaded let edits = dom.rebuild().santize(); assert_eq!( - edits.template_edits, + edits.templates, [ // create each template CreateElement { name: "div" }, @@ -207,7 +207,7 @@ fn anchors() { ); assert_eq!( - edits.dom_edits, + edits.edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1) }, CreatePlaceholder { id: ElementId(2) }, diff --git a/packages/core/tests/create_element.rs b/packages/core/tests/create_element.rs index e6331f9ac..42da01656 100644 --- a/packages/core/tests/create_element.rs +++ b/packages/core/tests/create_element.rs @@ -12,7 +12,7 @@ fn multiroot() { }); assert_eq!( - dom.rebuild().santize().template_edits, + dom.rebuild().santize().templates, [ CreateElement { name: "div" }, CreateStaticText { value: "Hello a" }, diff --git a/packages/core/tests/create_fragments.rs b/packages/core/tests/create_fragments.rs index 33bba1eaa..5b67711a8 100644 --- a/packages/core/tests/create_fragments.rs +++ b/packages/core/tests/create_fragments.rs @@ -14,7 +14,7 @@ fn empty_fragment_creates_nothing() { let edits = vdom.rebuild(); assert_eq!( - edits.dom_edits, + edits.edits, [ CreatePlaceholder { id: ElementId(1) }, AppendChildren { m: 1 } @@ -32,7 +32,7 @@ fn root_fragments_work() { }); assert_eq!( - vdom.rebuild().dom_edits.last().unwrap(), + vdom.rebuild().edits.last().unwrap(), &AppendChildren { m: 2 } ); } @@ -59,7 +59,7 @@ fn fragments_nested() { }); assert_eq!( - vdom.rebuild().dom_edits.last().unwrap(), + vdom.rebuild().edits.last().unwrap(), &AppendChildren { m: 8 } ); } @@ -84,7 +84,7 @@ fn fragments_across_components() { } assert_eq!( - VirtualDom::new(app).rebuild().dom_edits.last().unwrap(), + VirtualDom::new(app).rebuild().edits.last().unwrap(), &AppendChildren { m: 8 } ); } @@ -98,7 +98,7 @@ fn list_fragments() { )) } assert_eq!( - VirtualDom::new(app).rebuild().dom_edits.last().unwrap(), + VirtualDom::new(app).rebuild().edits.last().unwrap(), &AppendChildren { m: 7 } ); } diff --git a/packages/core/tests/create_lists.rs b/packages/core/tests/create_lists.rs index e29044514..163eb577f 100644 --- a/packages/core/tests/create_lists.rs +++ b/packages/core/tests/create_lists.rs @@ -28,7 +28,7 @@ fn list_renders() { let edits = dom.rebuild().santize(); assert_eq!( - edits.template_edits, + edits.templates, [ // Create the outer div CreateElement { name: "div" }, @@ -51,7 +51,7 @@ fn list_renders() { ); assert_eq!( - edits.dom_edits, + edits.edits, [ // Load the outer div LoadTemplate { name: "template", index: 0, id: ElementId(1) }, diff --git a/packages/core/tests/create_passthru.rs b/packages/core/tests/create_passthru.rs index 2ffdf0065..c13101320 100644 --- a/packages/core/tests/create_passthru.rs +++ b/packages/core/tests/create_passthru.rs @@ -26,7 +26,7 @@ fn nested_passthru_creates() { let edits = dom.rebuild().santize(); assert_eq!( - edits.dom_edits, + edits.edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1) }, AppendChildren { m: 1 }, @@ -64,7 +64,7 @@ fn nested_passthru_creates_add() { let mut dom = VirtualDom::new(app); assert_eq!( - dom.rebuild().santize().dom_edits, + dom.rebuild().santize().edits, [ // load 1 LoadTemplate { name: "template", index: 0, id: ElementId(1) }, @@ -92,11 +92,11 @@ fn dynamic_node_as_root() { let edits = dom.rebuild().santize(); // Since the roots were all dynamic, they should not cause any template muations - assert_eq!(edits.template_edits, []); + assert_eq!(edits.templates, []); // The root node is text, so we just create it on the spot assert_eq!( - edits.dom_edits, + edits.edits, [ CreateTextNode { value: "123", id: ElementId(1) }, CreateTextNode { value: "456", id: ElementId(2) }, diff --git a/packages/core/tests/cycle.rs b/packages/core/tests/cycle.rs index 87ede2271..72e1a028d 100644 --- a/packages/core/tests/cycle.rs +++ b/packages/core/tests/cycle.rs @@ -14,7 +14,7 @@ fn cycling_elements() { let edits = dom.rebuild().santize(); assert_eq!( - edits.dom_edits, + edits.edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, AppendChildren { m: 1 }, @@ -23,7 +23,7 @@ fn cycling_elements() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, ReplaceWith { id: ElementId(1,), m: 1 }, @@ -33,7 +33,7 @@ fn cycling_elements() { // notice that the IDs cycle back to ElementId(1), preserving a minimal memory footprint dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, ReplaceWith { id: ElementId(2,), m: 1 }, @@ -42,7 +42,7 @@ fn cycling_elements() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, ReplaceWith { id: ElementId(1,), m: 1 }, diff --git a/packages/core/tests/diff_component.rs b/packages/core/tests/diff_component.rs index 0f6ec615c..6e72f04cd 100644 --- a/packages/core/tests/diff_component.rs +++ b/packages/core/tests/diff_component.rs @@ -61,7 +61,7 @@ fn component_swap() { let mut dom = VirtualDom::new(app); let edits = dom.rebuild().santize(); assert_eq!( - edits.dom_edits, + edits.edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1) }, LoadTemplate { name: "template", index: 0, id: ElementId(2) }, @@ -75,7 +75,7 @@ fn component_swap() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(6) }, ReplaceWith { id: ElementId(5), m: 1 } @@ -84,7 +84,7 @@ fn component_swap() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(5) }, ReplaceWith { id: ElementId(6), m: 1 } @@ -93,7 +93,7 @@ fn component_swap() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(6) }, ReplaceWith { id: ElementId(5), m: 1 } diff --git a/packages/core/tests/diff_element.rs b/packages/core/tests/diff_element.rs index 4ca97dcdb..3ab9b284a 100644 --- a/packages/core/tests/diff_element.rs +++ b/packages/core/tests/diff_element.rs @@ -14,19 +14,19 @@ fn text_diff() { vdom.mark_dirty(ScopeId(0)); assert_eq!( - vdom.render_immediate().dom_edits, + vdom.render_immediate().edits, [SetText { value: "hello 1", id: ElementId(2) }] ); vdom.mark_dirty(ScopeId(0)); assert_eq!( - vdom.render_immediate().dom_edits, + vdom.render_immediate().edits, [SetText { value: "hello 2", id: ElementId(2) }] ); vdom.mark_dirty(ScopeId(0)); assert_eq!( - vdom.render_immediate().dom_edits, + vdom.render_immediate().edits, [SetText { value: "hello 3", id: ElementId(2) }] ); } @@ -48,7 +48,7 @@ fn element_swap() { vdom.mark_dirty(ScopeId(0)); assert_eq!( - vdom.render_immediate().santize().dom_edits, + vdom.render_immediate().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, ReplaceWith { id: ElementId(1,), m: 1 }, @@ -57,7 +57,7 @@ fn element_swap() { vdom.mark_dirty(ScopeId(0)); assert_eq!( - vdom.render_immediate().santize().dom_edits, + vdom.render_immediate().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, ReplaceWith { id: ElementId(2,), m: 1 }, @@ -66,7 +66,7 @@ fn element_swap() { vdom.mark_dirty(ScopeId(0)); assert_eq!( - vdom.render_immediate().santize().dom_edits, + vdom.render_immediate().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, ReplaceWith { id: ElementId(1,), m: 1 }, @@ -75,7 +75,7 @@ fn element_swap() { vdom.mark_dirty(ScopeId(0)); assert_eq!( - vdom.render_immediate().santize().dom_edits, + vdom.render_immediate().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, ReplaceWith { id: ElementId(2,), m: 1 }, diff --git a/packages/core/tests/diff_keyed_list.rs b/packages/core/tests/diff_keyed_list.rs index a2c00e439..6acbaebbb 100644 --- a/packages/core/tests/diff_keyed_list.rs +++ b/packages/core/tests/diff_keyed_list.rs @@ -21,7 +21,7 @@ fn keyed_diffing_out_of_order() { }); assert_eq!( - dom.rebuild().santize().dom_edits, + dom.rebuild().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, @@ -39,7 +39,7 @@ fn keyed_diffing_out_of_order() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().dom_edits, + dom.render_immediate().edits, [ PushRoot { id: ElementId(7,) }, InsertBefore { id: ElementId(5,), m: 1 }, @@ -64,7 +64,7 @@ fn keyed_diffing_out_of_order_adds() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().dom_edits, + dom.render_immediate().edits, [ PushRoot { id: ElementId(5,) }, PushRoot { id: ElementId(4,) }, @@ -90,7 +90,7 @@ fn keyed_diffing_out_of_order_adds_3() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().dom_edits, + dom.render_immediate().edits, [ PushRoot { id: ElementId(5,) }, PushRoot { id: ElementId(4,) }, @@ -116,7 +116,7 @@ fn keyed_diffing_out_of_order_adds_4() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().dom_edits, + dom.render_immediate().edits, [ PushRoot { id: ElementId(5,) }, PushRoot { id: ElementId(4,) }, @@ -142,7 +142,7 @@ fn keyed_diffing_out_of_order_adds_5() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().dom_edits, + dom.render_immediate().edits, [ PushRoot { id: ElementId(5,) }, InsertBefore { id: ElementId(4,), m: 1 }, @@ -167,7 +167,7 @@ fn keyed_diffing_additions() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(6) }, LoadTemplate { name: "template", index: 0, id: ElementId(7) }, @@ -192,7 +192,7 @@ fn keyed_diffing_additions_and_moves_on_ends() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [ // create 11, 12 LoadTemplate { name: "template", index: 0, id: ElementId(5) }, @@ -222,7 +222,7 @@ fn keyed_diffing_additions_and_moves_in_middle() { // LIS: 4, 5, 6 dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [ // create 5, 6 LoadTemplate { name: "template", index: 0, id: ElementId(5) }, @@ -256,7 +256,7 @@ fn controlled_keyed_diffing_out_of_order() { // LIS: 5, 6 dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [ // remove 7 Remove { id: ElementId(4,) }, @@ -289,7 +289,7 @@ fn controlled_keyed_diffing_out_of_order_max_test() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [ Remove { id: ElementId(5,) }, LoadTemplate { name: "template", index: 0, id: ElementId(5) }, @@ -318,7 +318,7 @@ fn remove_list() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [ Remove { id: ElementId(3) }, Remove { id: ElementId(4) }, @@ -343,7 +343,7 @@ fn no_common_keys() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [ Remove { id: ElementId(2) }, Remove { id: ElementId(3) }, diff --git a/packages/core/tests/diff_unkeyed_list.rs b/packages/core/tests/diff_unkeyed_list.rs index 75c12aec0..b32218327 100644 --- a/packages/core/tests/diff_unkeyed_list.rs +++ b/packages/core/tests/diff_unkeyed_list.rs @@ -17,7 +17,7 @@ fn list_creates_one_by_one() { // load the div and then assign the empty fragment as a placeholder assert_eq!( - dom.rebuild().santize().dom_edits, + dom.rebuild().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, AssignId { path: &[0], id: ElementId(2,) }, @@ -28,7 +28,7 @@ fn list_creates_one_by_one() { // Rendering the first item should replace the placeholder with an element dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(3,) }, HydrateText { path: &[0], value: "0", id: ElementId(4,) }, @@ -39,7 +39,7 @@ fn list_creates_one_by_one() { // Rendering the next item should insert after the previous dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, HydrateText { path: &[0], value: "1", id: ElementId(5,) }, @@ -50,7 +50,7 @@ fn list_creates_one_by_one() { // ... and again! dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(6,) }, HydrateText { path: &[0], value: "2", id: ElementId(7,) }, @@ -61,7 +61,7 @@ fn list_creates_one_by_one() { // once more dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(8,) }, HydrateText { path: &[0], value: "3", id: ElementId(9,) }, @@ -86,7 +86,7 @@ fn removes_one_by_one() { // load the div and then assign the empty fragment as a placeholder assert_eq!( - dom.rebuild().santize().dom_edits, + dom.rebuild().santize().edits, [ // The container LoadTemplate { name: "template", index: 0, id: ElementId(1) }, @@ -108,14 +108,14 @@ fn removes_one_by_one() { // Rendering the first item should replace the placeholder with an element dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [Remove { id: ElementId(6) }] ); // Remove div(2) dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [Remove { id: ElementId(4) }] ); @@ -123,7 +123,7 @@ fn removes_one_by_one() { // todo: this should just be a remove with no placeholder dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [ CreatePlaceholder { id: ElementId(3) }, ReplaceWith { id: ElementId(2), m: 1 } @@ -134,7 +134,7 @@ fn removes_one_by_one() { // todo: this should actually be append to, but replace placeholder is fine for now dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(2) }, HydrateText { path: &[0], value: "0", id: ElementId(4) }, @@ -161,7 +161,7 @@ fn list_shrink_multiroot() { }); assert_eq!( - dom.rebuild().santize().dom_edits, + dom.rebuild().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, AssignId { path: &[0,], id: ElementId(2,) }, @@ -171,7 +171,7 @@ fn list_shrink_multiroot() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(3) }, HydrateText { path: &[0], value: "0", id: ElementId(4) }, @@ -183,7 +183,7 @@ fn list_shrink_multiroot() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(2) }, HydrateText { path: &[0], value: "1", id: ElementId(7) }, @@ -195,7 +195,7 @@ fn list_shrink_multiroot() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(10) }, HydrateText { path: &[0], value: "2", id: ElementId(11) }, @@ -223,7 +223,7 @@ fn removes_one_by_one_multiroot() { // load the div and then assign the empty fragment as a placeholder assert_eq!( - dom.rebuild().santize().dom_edits, + dom.rebuild().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1) }, // @@ -250,19 +250,19 @@ fn removes_one_by_one_multiroot() { dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [Remove { id: ElementId(10) }, Remove { id: ElementId(12) }] ); dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [Remove { id: ElementId(6) }, Remove { id: ElementId(8) }] ); dom.mark_dirty(ScopeId(0)); assert_eq!( - dom.render_immediate().santize().dom_edits, + dom.render_immediate().santize().edits, [ Remove { id: ElementId(4) }, CreatePlaceholder { id: ElementId(5) }, @@ -282,7 +282,7 @@ fn two_equal_fragments_are_equal_static() { }); _ = dom.rebuild(); - assert!(dom.render_immediate().dom_edits.is_empty()); + assert!(dom.render_immediate().edits.is_empty()); } #[test] @@ -296,7 +296,7 @@ fn two_equal_fragments_are_equal() { }); _ = dom.rebuild(); - assert!(dom.render_immediate().dom_edits.is_empty()); + assert!(dom.render_immediate().edits.is_empty()); } #[test] @@ -315,9 +315,9 @@ fn remove_many() { }); let edits = dom.rebuild().santize(); - assert!(edits.template_edits.is_empty()); + assert!(edits.templates.is_empty()); assert_eq!( - edits.dom_edits, + edits.edits, [ CreatePlaceholder { id: ElementId(1,) }, AppendChildren { m: 1 }, @@ -327,7 +327,7 @@ fn remove_many() { dom.mark_dirty(ScopeId(0)); let edits = dom.render_immediate().santize(); assert_eq!( - edits.dom_edits, + edits.edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, HydrateText { path: &[0,], value: "hello 0", id: ElementId(3,) }, @@ -338,7 +338,7 @@ fn remove_many() { dom.mark_dirty(ScopeId(0)); let edits = dom.render_immediate().santize(); assert_eq!( - edits.dom_edits, + edits.edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, HydrateText { path: &[0,], value: "hello 1", id: ElementId(4,) }, @@ -355,7 +355,7 @@ fn remove_many() { dom.mark_dirty(ScopeId(0)); let edits = dom.render_immediate().santize(); assert_eq!( - edits.dom_edits, + edits.edits, [ Remove { id: ElementId(1,) }, Remove { id: ElementId(5,) }, @@ -369,7 +369,7 @@ fn remove_many() { dom.mark_dirty(ScopeId(0)); let edits = dom.render_immediate().santize(); assert_eq!( - edits.dom_edits, + edits.edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, HydrateText { path: &[0,], value: "hello 0", id: ElementId(10,) }, diff --git a/packages/core/tests/kitchen_sink.rs b/packages/core/tests/kitchen_sink.rs index 737980938..afa1ed64f 100644 --- a/packages/core/tests/kitchen_sink.rs +++ b/packages/core/tests/kitchen_sink.rs @@ -30,7 +30,7 @@ fn dual_stream() { use Mutation::*; assert_eq!( - edits.template_edits, + edits.templates, [ CreateElement { name: "div" }, SetStaticAttribute { name: "class", value: "asd", ns: None }, @@ -66,7 +66,7 @@ fn dual_stream() { ); assert_eq!( - edits.dom_edits, + edits.edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1) }, SetAttribute { name: "class", value: "123", id: ElementId(1), ns: None }, diff --git a/packages/core/tests/lifecycle.rs b/packages/core/tests/lifecycle.rs index bdceb2332..ab7be8793 100644 --- a/packages/core/tests/lifecycle.rs +++ b/packages/core/tests/lifecycle.rs @@ -28,7 +28,7 @@ fn manual_diffing() { *value.lock().unwrap() = "goodbye"; assert_eq!( - dom.rebuild().santize().dom_edits, + dom.rebuild().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(3) }, HydrateText { path: &[0], value: "goodbye", id: ElementId(4) }, @@ -62,7 +62,7 @@ fn events_generate() { let edits = dom.render_immediate(); assert_eq!( - edits.dom_edits, + edits.edits, [ CreatePlaceholder { id: ElementId(2) }, ReplaceWith { id: ElementId(1), m: 1 } diff --git a/packages/core/tests/suspense.rs b/packages/core/tests/suspense.rs index 4e70ec807..ae69acab5 100644 --- a/packages/core/tests/suspense.rs +++ b/packages/core/tests/suspense.rs @@ -13,7 +13,7 @@ async fn it_works() { // We should at least get the top-level template in before pausing for the children assert_eq!( - mutations.template_edits, + mutations.templates, [ CreateElement { name: "div" }, CreateStaticText { value: "Waiting for child..." }, @@ -25,7 +25,7 @@ async fn it_works() { // And we should load it in and assign the placeholder properly assert_eq!( - mutations.dom_edits, + mutations.edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1) }, // hmmmmmmmmm.... with suspense how do we guarantee that IDs increase linearly? diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs index 4eca36db1..d01b6545f 100644 --- a/packages/desktop/src/controller.rs +++ b/packages/desktop/src/controller.rs @@ -54,8 +54,8 @@ impl DesktopController { { let edits = dom.rebuild(); let mut queue = edit_queue.lock().unwrap(); - queue.push(serde_json::to_string(&edits.template_edits).unwrap()); - queue.push(serde_json::to_string(&edits.dom_edits).unwrap()); + queue.push(serde_json::to_string(&edits.templates).unwrap()); + queue.push(serde_json::to_string(&edits.edits).unwrap()); proxy.send_event(UserWindowEvent::EditsReady).unwrap(); } @@ -79,8 +79,8 @@ impl DesktopController { { let mut queue = edit_queue.lock().unwrap(); - queue.push(serde_json::to_string(&muts.template_edits).unwrap()); - queue.push(serde_json::to_string(&muts.dom_edits).unwrap()); + queue.push(serde_json::to_string(&muts.templates).unwrap()); + queue.push(serde_json::to_string(&muts.edits).unwrap()); let _ = proxy.send_event(UserWindowEvent::EditsReady); } } diff --git a/packages/dioxus/benches/jsframework.rs b/packages/dioxus/benches/jsframework.rs index 281d36904..24ca2fe69 100644 --- a/packages/dioxus/benches/jsframework.rs +++ b/packages/dioxus/benches/jsframework.rs @@ -46,7 +46,7 @@ fn create_rows(c: &mut Criterion) { b.iter(|| { let g = dom.rebuild(); - assert!(g.dom_edits.len() > 1); + assert!(g.edits.len() > 1); }) }); } diff --git a/packages/html/src/lib.rs b/packages/html/src/lib.rs index 325116a34..ab2e49f1e 100644 --- a/packages/html/src/lib.rs +++ b/packages/html/src/lib.rs @@ -18,12 +18,14 @@ pub mod events; pub mod geometry; mod global_attributes; pub mod input_data; +mod render_template; #[cfg(feature = "wasm-bind")] mod web_sys_bind; pub use elements::*; pub use events::*; pub use global_attributes::*; +pub use render_template::*; pub mod prelude { pub use crate::events::*; diff --git a/packages/html/src/render_template.rs b/packages/html/src/render_template.rs new file mode 100644 index 000000000..5fbff5ff0 --- /dev/null +++ b/packages/html/src/render_template.rs @@ -0,0 +1,41 @@ +use dioxus_core::{Template, TemplateAttribute, TemplateNode}; +use std::fmt::Write; + +/// Render a template to an HTML string +/// +/// Useful for sending over the wire. Can be used to with innerHtml to create templates with little work +pub fn render_template_to_html(template: &Template) -> String { + let mut out = String::new(); + + for root in template.roots { + render_template_node(root, &mut out).unwrap(); + } + + out +} + +fn render_template_node(node: &TemplateNode, out: &mut String) -> std::fmt::Result { + match node { + TemplateNode::Element { + tag, + attrs, + children, + .. + } => { + write!(out, "<{tag}")?; + for attr in *attrs { + if let TemplateAttribute::Static { name, value, .. } = attr { + write!(out, "{}=\"{}\"", name, value)?; + } + } + for child in *children { + render_template_node(child, out)?; + } + write!(out, "")?; + } + TemplateNode::Text(t) => write!(out, "{t}")?, + TemplateNode::Dynamic(_) => write!(out, "