From 160d86abbe1b325e3123202aef29025dcd96f4eb Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 21 Jul 2021 22:55:04 -0400 Subject: [PATCH 01/21] wip: serious refactor with const generics --- packages/core-macro/src/html/mod.rs | 27 -- packages/core-macro/src/lib.rs | 69 +---- packages/core-macro/src/rsx/ambiguous.rs | 59 ++++- packages/core-macro/src/rsx/body.rs | 131 ++++++++++ packages/core-macro/src/rsx/component.rs | 236 +++++++++++++----- packages/core-macro/src/rsx/element.rs | 97 +++++-- packages/core-macro/src/rsx/fragment.rs | 33 ++- packages/core-macro/src/rsx/mod.rs | 94 +------ packages/core-macro/src/rsx/node.rs | 32 ++- packages/core-macro/{src => }/styles/calc.rs | 0 .../core-macro/{src => }/styles/codegen.rs | 0 packages/core-macro/{src => }/styles/color.rs | 0 packages/core-macro/{src => }/styles/mod.rs | 0 .../{src => }/styles/string/lexer.rs | 0 .../core-macro/{src => }/styles/string/mod.rs | 0 .../core-macro/{src => }/styles/syn_parse.rs | 0 16 files changed, 492 insertions(+), 286 deletions(-) delete mode 100644 packages/core-macro/src/html/mod.rs create mode 100644 packages/core-macro/src/rsx/body.rs rename packages/core-macro/{src => }/styles/calc.rs (100%) rename packages/core-macro/{src => }/styles/codegen.rs (100%) rename packages/core-macro/{src => }/styles/color.rs (100%) rename packages/core-macro/{src => }/styles/mod.rs (100%) rename packages/core-macro/{src => }/styles/string/lexer.rs (100%) rename packages/core-macro/{src => }/styles/string/mod.rs (100%) rename packages/core-macro/{src => }/styles/syn_parse.rs (100%) diff --git a/packages/core-macro/src/html/mod.rs b/packages/core-macro/src/html/mod.rs deleted file mode 100644 index 6ad20acad..000000000 --- a/packages/core-macro/src/html/mod.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! Html body -//! ------- -//! -//! -//! Since both HTML and RSX serialize to the same node structure, the HTML parser uses the same types as RSX, -//! but has a different Parse implementation. - -use crate::rsx::*; -use quote::ToTokens; -use syn::parse::Parse; - -pub struct HtmlBody(RsxBody); - -impl Parse for HtmlBody { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - todo!() - } -} -impl ToTokens for HtmlBody { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - self.0.to_tokens(tokens) - } -} - -pub struct HtmlNode(BodyNode); -pub struct HtmlAmbigiousElement(AmbiguousElement); -pub struct HtmlComponent(Component); diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 6cee65f8d..8c3c00bf1 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -1,69 +1,14 @@ use proc_macro::TokenStream; use quote::ToTokens; +use rsx::{AS_HTML, AS_RSX}; use syn::parse_macro_input; -pub(crate) mod fc; pub(crate) mod htm; -pub(crate) mod html; pub(crate) mod ifmt; pub(crate) mod props; pub(crate) mod rsx; -pub(crate) mod rsxtemplate; pub(crate) mod util; -/// The html! macro makes it easy for developers to write jsx-style markup in their components. -/// We aim to keep functional parity with html templates. -#[proc_macro] -pub fn html(s: TokenStream) -> TokenStream { - match syn::parse::(s) { - Err(e) => e.to_compile_error().into(), - Ok(s) => s.to_token_stream().into(), - } -} - -/// The html! macro makes it easy for developers to write jsx-style markup in their components. -/// We aim to keep functional parity with html templates. -#[proc_macro] -pub fn rsx_template(s: TokenStream) -> TokenStream { - match syn::parse::(s) { - Err(e) => e.to_compile_error().into(), - Ok(s) => s.to_token_stream().into(), - } -} - -/// The html! macro makes it easy for developers to write jsx-style markup in their components. -/// We aim to keep functional parity with html templates. -#[proc_macro] -pub fn html_template(s: TokenStream) -> TokenStream { - match syn::parse::(s) { - Err(e) => e.to_compile_error().into(), - Ok(s) => s.to_token_stream().into(), - } -} - -// #[proc_macro_attribute] -// pub fn fc(attr: TokenStream, item: TokenStream) -> TokenStream { - -/// Label a function or static closure as a functional component. -/// This macro reduces the need to create a separate properties struct. -/// -/// Using this macro is fun and simple -/// -/// ```ignore -/// -/// #[fc] -/// fn Example(cx: Context, name: &str) -> DomTree { -/// cx.render(rsx! { h1 {"hello {name}"} }) -/// } -/// ``` -#[proc_macro_attribute] -pub fn fc(_attr: TokenStream, item: TokenStream) -> TokenStream { - match syn::parse::(item) { - Err(e) => e.to_compile_error().into(), - Ok(s) => s.to_token_stream().into(), - } -} - #[proc_macro] pub fn format_args_f(input: TokenStream) -> TokenStream { use ifmt::*; @@ -238,7 +183,17 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token /// ``` #[proc_macro] pub fn rsx(s: TokenStream) -> TokenStream { - match syn::parse::(s) { + match syn::parse::>(s) { + Err(e) => e.to_compile_error().into(), + Ok(s) => s.to_token_stream().into(), + } +} + +/// The html! macro makes it easy for developers to write jsx-style markup in their components. +/// We aim to keep functional parity with html templates. +#[proc_macro] +pub fn html(s: TokenStream) -> TokenStream { + match syn::parse::>(s) { Err(e) => e.to_compile_error().into(), Ok(s) => s.to_token_stream().into(), } diff --git a/packages/core-macro/src/rsx/ambiguous.rs b/packages/core-macro/src/rsx/ambiguous.rs index 7b894c6e2..d75b07fc5 100644 --- a/packages/core-macro/src/rsx/ambiguous.rs +++ b/packages/core-macro/src/rsx/ambiguous.rs @@ -15,17 +15,17 @@ use syn::{ Error, Ident, LitStr, Result, Token, }; -pub enum AmbiguousElement { - Element(Element), - Component(Component), +pub enum AmbiguousElement { + Element(Element), + Component(Component), } -impl Parse for AmbiguousElement { +impl Parse for AmbiguousElement { fn parse(input: ParseStream) -> Result { // Try to parse as an absolute path and immediately defer to the componetn if input.peek(Token![::]) { return input - .parse::() + .parse::>() .map(|c| AmbiguousElement::Component(c)); } @@ -34,7 +34,7 @@ impl Parse for AmbiguousElement { if let Ok(pat) = input.fork().parse::() { if pat.segments.len() > 1 { return input - .parse::() + .parse::>() .map(|c| AmbiguousElement::Component(c)); } } @@ -45,11 +45,11 @@ impl Parse for AmbiguousElement { let first_char = name_str.chars().next().unwrap(); if first_char.is_ascii_uppercase() { input - .parse::() + .parse::>() .map(|c| AmbiguousElement::Component(c)) } else { input - .parse::() + .parse::>() .map(|c| AmbiguousElement::Element(c)) } } else { @@ -61,7 +61,48 @@ impl Parse for AmbiguousElement { } } -impl ToTokens for AmbiguousElement { +impl Parse for AmbiguousElement { + fn parse(input: ParseStream) -> Result { + // Try to parse as an absolute path and immediately defer to the componetn + if input.peek(Token![::]) { + return input + .parse::>() + .map(|c| AmbiguousElement::Component(c)); + } + + // If not an absolute path, then parse the ident and check if it's a valid tag + + if let Ok(pat) = input.fork().parse::() { + if pat.segments.len() > 1 { + return input + .parse::>() + .map(|c| AmbiguousElement::Component(c)); + } + } + + if let Ok(name) = input.fork().parse::() { + let name_str = name.to_string(); + + let first_char = name_str.chars().next().unwrap(); + if first_char.is_ascii_uppercase() { + input + .parse::>() + .map(|c| AmbiguousElement::Component(c)) + } else { + input + .parse::>() + .map(|c| AmbiguousElement::Element(c)) + } + } else { + if input.peek(LitStr) { + panic!("it's actually a litstr"); + } + Err(Error::new(input.span(), "Not a valid Html tag")) + } + } +} + +impl ToTokens for AmbiguousElement { fn to_tokens(&self, tokens: &mut TokenStream2) { match self { AmbiguousElement::Element(el) => el.to_tokens(tokens), diff --git a/packages/core-macro/src/rsx/body.rs b/packages/core-macro/src/rsx/body.rs new file mode 100644 index 000000000..cfbc405d2 --- /dev/null +++ b/packages/core-macro/src/rsx/body.rs @@ -0,0 +1,131 @@ +use crate::util::is_valid_tag; +use proc_macro2::TokenStream as TokenStream2; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{ + parse::{Parse, ParseStream}, + Error, Ident, Result, Token, +}; + +use super::*; + +pub struct RsxBody { + custom_context: Option, + roots: Vec>, +} + +/// The custom rusty variant of parsing rsx! +impl Parse for RsxBody { + fn parse(input: ParseStream) -> Result { + // if input.peek(LitStr) { + // return input.parse::()?.parse::(); + // } + + // try to parse the first ident and comma + let custom_context = + if input.peek(Token![in]) && input.peek2(Ident) && input.peek3(Token![,]) { + let _ = input.parse::()?; + let name = input.parse::()?; + if is_valid_tag(&name.to_string()) { + return Err(Error::new( + input.span(), + "Custom context cannot be an html element name", + )); + } else { + input.parse::().unwrap(); + Some(name) + } + } else { + None + }; + + let mut body = Vec::new(); + let mut children = Vec::new(); + let mut manual_props = None; + let cfg: BodyParseConfig = BodyParseConfig { + allow_children: true, + allow_fields: false, + allow_manual_props: false, + }; + cfg.parse_component_body(input, &mut body, &mut children, &mut manual_props)?; + + Ok(Self { + roots: children, + custom_context, + }) + } +} + +/// The HTML variant of parsing rsx! +impl Parse for RsxBody { + fn parse(input: ParseStream) -> Result { + // if input.peek(LitStr) { + // return input.parse::()?.parse::(); + // } + + // try to parse the first ident and comma + let custom_context = + if input.peek(Token![in]) && input.peek2(Ident) && input.peek3(Token![,]) { + let _ = input.parse::()?; + let name = input.parse::()?; + if is_valid_tag(&name.to_string()) { + return Err(Error::new( + input.span(), + "Custom context cannot be an html element name", + )); + } else { + input.parse::().unwrap(); + Some(name) + } + } else { + None + }; + + let mut body = Vec::new(); + let mut children = Vec::new(); + let mut manual_props = None; + + let cfg: BodyParseConfig = BodyParseConfig { + allow_children: true, + allow_fields: false, + allow_manual_props: false, + }; + cfg.parse_component_body(input, &mut body, &mut children, &mut manual_props)?; + + Ok(Self { + roots: children, + custom_context, + }) + } +} + +/// Serialize the same way, regardless of flavor +impl ToTokens for RsxBody { + fn to_tokens(&self, out_tokens: &mut TokenStream2) { + let inner = if self.roots.len() == 1 { + let inner = &self.roots[0]; + quote! {#inner} + } else { + let childs = &self.roots; + quote! { __cx.fragment_from_iter([ #(#childs),* ]) } + }; + + match &self.custom_context { + // The `in cx` pattern allows directly rendering + Some(ident) => out_tokens.append_all(quote! { + #ident.render(dioxus::prelude::LazyNodes::new(move |__cx: NodeFactory|{ + use dioxus_elements::GlobalAttributes; + + #inner + })) + }), + // Otherwise we just build the LazyNode wrapper + None => out_tokens.append_all(quote! { + dioxus::prelude::LazyNodes::new(move |__cx: NodeFactory|{ + use dioxus_elements::GlobalAttributes; + + #inner + }) + }), + }; + } +} diff --git a/packages/core-macro/src/rsx/component.rs b/packages/core-macro/src/rsx/component.rs index c3a5c143b..79a4d26c4 100644 --- a/packages/core-macro/src/rsx/component.rs +++ b/packages/core-macro/src/rsx/component.rs @@ -19,18 +19,18 @@ use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ ext::IdentExt, parse::{Parse, ParseBuffer, ParseStream}, - token, Expr, ExprClosure, Ident, Result, Token, + token, Error, Expr, ExprClosure, Ident, Result, Token, }; -pub struct Component { +pub struct Component { // accept any path-like argument name: syn::Path, - body: Vec, - children: Vec, + body: Vec>, + children: Vec>, manual_props: Option, } -impl Parse for Component { +impl Parse for Component { fn parse(stream: ParseStream) -> Result { // let name = s.parse::()?; // todo: look into somehow getting the crate/super/etc @@ -41,21 +41,45 @@ impl Parse for Component { let content: ParseBuffer; syn::braced!(content in stream); - let mut body: Vec = Vec::new(); - let mut children: Vec = Vec::new(); + let mut body: Vec> = Vec::new(); + let mut children: Vec> = Vec::new(); let mut manual_props = None; - parse_component_body( - &content, - &BodyParseConfig { - allow_children: true, - allow_fields: true, - allow_manual_props: true, - }, - &mut body, - &mut children, - &mut manual_props, - )?; + let cfg: BodyParseConfig = BodyParseConfig { + allow_children: true, + allow_fields: true, + allow_manual_props: true, + }; + + cfg.parse_component_body(&content, &mut body, &mut children, &mut manual_props)?; + + Ok(Self { + name, + body, + children, + manual_props, + }) + } +} +impl Parse for Component { + fn parse(stream: ParseStream) -> Result { + let name = syn::Path::parse_mod_style(stream)?; + + // parse the guts + let content: ParseBuffer; + syn::braced!(content in stream); + + let mut body: Vec> = Vec::new(); + let mut children: Vec> = Vec::new(); + let mut manual_props = None; + + let cfg: BodyParseConfig = BodyParseConfig { + allow_children: true, + allow_fields: true, + allow_manual_props: true, + }; + + cfg.parse_component_body(&content, &mut body, &mut children, &mut manual_props)?; Ok(Self { name, @@ -66,64 +90,117 @@ impl Parse for Component { } } -pub struct BodyParseConfig { +pub struct BodyParseConfig { pub allow_fields: bool, pub allow_children: bool, pub allow_manual_props: bool, } - -// todo: unify this body parsing for both elements and components -// both are style rather ad-hoc, though components are currently more configured -pub fn parse_component_body( - content: &ParseBuffer, - cfg: &BodyParseConfig, - body: &mut Vec, - children: &mut Vec, - manual_props: &mut Option, -) -> Result<()> { - 'parsing: loop { - // [1] Break if empty - if content.is_empty() { - break 'parsing; - } - - if content.peek(Token![..]) { - if !cfg.allow_manual_props { - return Err(Error::new( - content.span(), - "Props spread syntax is not allowed in this context. \nMake to only use the elipsis `..` in Components.", - )); +impl BodyParseConfig { + // todo: unify this body parsing for both elements and components + // both are style rather ad-hoc, though components are currently more configured + pub fn parse_component_body( + &self, + content: &ParseBuffer, + body: &mut Vec>, + children: &mut Vec>, + manual_props: &mut Option, + ) -> Result<()> { + 'parsing: loop { + // [1] Break if empty + if content.is_empty() { + break 'parsing; } - content.parse::()?; - *manual_props = Some(content.parse::()?); - } else if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { - if !cfg.allow_fields { - return Err(Error::new( - content.span(), - "Property fields is not allowed in this context. \nMake to only use fields in Components or Elements.", - )); - } - body.push(content.parse::()?); - } else { - if !cfg.allow_children { - return Err(Error::new( - content.span(), - "This item is not allowed to accept children.", - )); - } - children.push(content.parse::()?); - } - // consume comma if it exists - // we don't actually care if there *are* commas between attrs - if content.peek(Token![,]) { - let _ = content.parse::(); + if content.peek(Token![..]) { + if !self.allow_manual_props { + return Err(Error::new( + content.span(), + "Props spread syntax is not allowed in this context. \nMake to only use the elipsis `..` in Components.", + )); + } + content.parse::()?; + *manual_props = Some(content.parse::()?); + } else if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { + if !self.allow_fields { + return Err(Error::new( + content.span(), + "Property fields is not allowed in this context. \nMake to only use fields in Components or Elements.", + )); + } + body.push(content.parse::>()?); + } else { + if !self.allow_children { + return Err(Error::new( + content.span(), + "This item is not allowed to accept children.", + )); + } + children.push(content.parse::>()?); + } + + // consume comma if it exists + // we don't actually care if there *are* commas between attrs + if content.peek(Token![,]) { + let _ = content.parse::(); + } } + Ok(()) + } +} +impl BodyParseConfig { + // todo: unify this body parsing for both elements and components + // both are style rather ad-hoc, though components are currently more configured + pub fn parse_component_body( + &self, + content: &ParseBuffer, + body: &mut Vec>, + children: &mut Vec>, + manual_props: &mut Option, + ) -> Result<()> { + 'parsing: loop { + // [1] Break if empty + if content.is_empty() { + break 'parsing; + } + + if content.peek(Token![..]) { + if !self.allow_manual_props { + return Err(Error::new( + content.span(), + "Props spread syntax is not allowed in this context. \nMake to only use the elipsis `..` in Components.", + )); + } + content.parse::()?; + *manual_props = Some(content.parse::()?); + } else if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { + if !self.allow_fields { + return Err(Error::new( + content.span(), + "Property fields is not allowed in this context. \nMake to only use fields in Components or Elements.", + )); + } + body.push(content.parse::>()?); + } else { + if !self.allow_children { + return Err(Error::new( + content.span(), + "This item is not allowed to accept children.", + )); + } + children.push(content.parse::>()?); + } + + // consume comma if it exists + // we don't actually care if there *are* commas between attrs + if content.peek(Token![,]) { + let _ = content.parse::(); + } + } + Ok(()) } - Ok(()) } -impl ToTokens for Component { +impl ToTokens for Component { fn to_tokens(&self, tokens: &mut TokenStream2) { let name = &self.name; @@ -195,7 +272,7 @@ impl ToTokens for Component { } // the struct's fields info -pub struct ComponentField { +pub struct ComponentField { name: Ident, content: ContentField, } @@ -222,7 +299,28 @@ impl ToTokens for ContentField { } } -impl Parse for ComponentField { +impl Parse for ComponentField { + fn parse(input: ParseStream) -> Result { + let name = Ident::parse_any(input)?; + input.parse::()?; + + let name_str = name.to_string(); + let content = if name_str.starts_with("on") { + if input.peek(token::Brace) { + let content; + syn::braced!(content in input); + ContentField::OnHandlerRaw(content.parse()?) + } else { + ContentField::OnHandler(input.parse()?) + } + } else { + ContentField::ManExpr(input.parse::()?) + }; + + Ok(Self { name, content }) + } +} +impl Parse for ComponentField { fn parse(input: ParseStream) -> Result { let name = Ident::parse_any(input)?; input.parse::()?; @@ -244,7 +342,7 @@ impl Parse for ComponentField { } } -impl ToTokens for ComponentField { +impl ToTokens for ComponentField { fn to_tokens(&self, tokens: &mut TokenStream2) { let ComponentField { name, content, .. } = self; tokens.append_all(quote! { diff --git a/packages/core-macro/src/rsx/element.rs b/packages/core-macro/src/rsx/element.rs index 41d39fe9d..af0e6daa3 100644 --- a/packages/core-macro/src/rsx/element.rs +++ b/packages/core-macro/src/rsx/element.rs @@ -11,35 +11,16 @@ use syn::{ // ======================================= // Parse the VNode::Element type // ======================================= -pub struct Element { +pub struct Element { name: Ident, key: Option, attributes: Vec, listeners: Vec, - children: Vec, + children: Vec>, is_static: bool, } -impl ToTokens for Element { - fn to_tokens(&self, tokens: &mut TokenStream2) { - let name = &self.name; - let attr = &self.attributes; - let childs = &self.children; - let listeners = &self.listeners; - - tokens.append_all(quote! { - __cx.element( - dioxus_elements::#name, - __cx.bump().alloc([ #(#listeners),* ]), - __cx.bump().alloc([ #(#attr),* ]), - __cx.bump().alloc([ #(#childs),* ]), - None, - ) - }); - } -} - -impl Parse for Element { +impl Parse for Element { fn parse(stream: ParseStream) -> Result { let name = Ident::parse(stream)?; @@ -49,7 +30,7 @@ impl Parse for Element { let mut attributes: Vec = vec![]; let mut listeners: Vec = vec![]; - let mut children: Vec = vec![]; + let mut children: Vec> = vec![]; let mut key = None; 'parsing: loop { @@ -67,7 +48,7 @@ impl Parse for Element { name.clone(), )?; } else { - children.push(content.parse::()?); + children.push(content.parse::>()?); } // consume comma if it exists @@ -88,6 +69,74 @@ impl Parse for Element { } } +impl Parse for Element { + fn parse(stream: ParseStream) -> Result { + let name = Ident::parse(stream)?; + + // parse the guts + let content: ParseBuffer; + syn::braced!(content in stream); + + let mut attributes: Vec = vec![]; + let mut listeners: Vec = vec![]; + let mut children: Vec> = vec![]; + let mut key = None; + + 'parsing: loop { + // [1] Break if empty + if content.is_empty() { + break 'parsing; + } + + if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { + parse_element_body( + &content, + &mut attributes, + &mut listeners, + &mut key, + name.clone(), + )?; + } else { + children.push(content.parse::>()?); + } + + // consume comma if it exists + // we don't actually care if there *are* commas after elements/text + if content.peek(Token![,]) { + let _ = content.parse::(); + } + } + + Ok(Self { + key, + name, + attributes, + children, + listeners, + is_static: false, + }) + } +} + +impl ToTokens for Element { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let name = &self.name; + let attr = &self.attributes; + let childs = &self.children; + let listeners = &self.listeners; + + tokens.append_all(quote! { + __cx.element( + dioxus_elements::#name, + __cx.bump().alloc([ #(#listeners),* ]), + __cx.bump().alloc([ #(#attr),* ]), + __cx.bump().alloc([ #(#childs),* ]), + None, + ) + }); + } +} + /// ======================================= /// Parse a VElement's Attributes /// ======================================= diff --git a/packages/core-macro/src/rsx/fragment.rs b/packages/core-macro/src/rsx/fragment.rs index 812252a25..e1b57b2d4 100644 --- a/packages/core-macro/src/rsx/fragment.rs +++ b/packages/core-macro/src/rsx/fragment.rs @@ -10,7 +10,7 @@ use syn::parse::ParseBuffer; -use super::AmbiguousElement; +use super::{AmbiguousElement, AS_HTML, AS_RSX, HTML_OR_RSX}; use { proc_macro::TokenStream, @@ -23,10 +23,11 @@ use { }, }; -pub struct Fragment { - children: Vec, +pub struct Fragment { + children: Vec>, } -impl Parse for Fragment { + +impl Parse for Fragment { fn parse(input: ParseStream) -> Result { input.parse::()?; @@ -36,7 +37,7 @@ impl Parse for Fragment { let content: ParseBuffer; syn::braced!(content in input); while !content.is_empty() { - content.parse::()?; + content.parse::>()?; if content.peek(Token![,]) { let _ = content.parse::(); @@ -46,7 +47,27 @@ impl Parse for Fragment { } } -impl ToTokens for Fragment { +impl Parse for Fragment { + fn parse(input: ParseStream) -> Result { + input.parse::()?; + + let children = Vec::new(); + + // parse the guts + let content: ParseBuffer; + syn::braced!(content in input); + while !content.is_empty() { + content.parse::>()?; + + if content.peek(Token![,]) { + let _ = content.parse::(); + } + } + Ok(Self { children }) + } +} + +impl ToTokens for Fragment { fn to_tokens(&self, tokens: &mut TokenStream2) { let childs = &self.children; let children = quote! { diff --git a/packages/core-macro/src/rsx/mod.rs b/packages/core-macro/src/rsx/mod.rs index c30a4d0d3..ce8bef36e 100644 --- a/packages/core-macro/src/rsx/mod.rs +++ b/packages/core-macro/src/rsx/mod.rs @@ -12,6 +12,7 @@ //! Any errors in using rsx! will likely occur when people start using it, so the first errors must be really helpful. mod ambiguous; +mod body; mod component; mod element; mod fragment; @@ -19,97 +20,12 @@ mod node; // Re-export the namespaces into each other pub use ambiguous::*; +pub use body::*; pub use component::*; pub use element::*; pub use fragment::*; pub use node::*; -use crate::util::is_valid_tag; -use proc_macro2::TokenStream as TokenStream2; -use quote::{quote, ToTokens, TokenStreamExt}; -use syn::{ - parse::{Parse, ParseStream}, - Error, Ident, LitStr, Result, Token, -}; - -pub struct RsxBody { - custom_context: Option, - roots: Vec, -} - -impl Parse for RsxBody { - fn parse(input: ParseStream) -> Result { - // if input.peek(LitStr) { - // return input.parse::()?.parse::(); - // } - - // try to parse the first ident and comma - let custom_context = - if input.peek(Token![in]) && input.peek2(Ident) && input.peek3(Token![,]) { - let _ = input.parse::()?; - let name = input.parse::()?; - if is_valid_tag(&name.to_string()) { - return Err(Error::new( - input.span(), - "Custom context cannot be an html element name", - )); - } else { - input.parse::().unwrap(); - Some(name) - } - } else { - None - }; - - let mut body = Vec::new(); - let mut children = Vec::new(); - let mut manual_props = None; - parse_component_body( - input, - &BodyParseConfig { - allow_children: true, - allow_fields: false, - allow_manual_props: false, - }, - &mut body, - &mut children, - &mut manual_props, - )?; - - Ok(Self { - roots: children, - custom_context, - }) - } -} - -impl ToTokens for RsxBody { - fn to_tokens(&self, out_tokens: &mut TokenStream2) { - let inner = if self.roots.len() == 1 { - let inner = &self.roots[0]; - quote! {#inner} - } else { - let childs = &self.roots; - quote! { __cx.fragment_from_iter([ #(#childs),* ]) } - }; - - match &self.custom_context { - // The `in cx` pattern allows directly rendering - Some(ident) => out_tokens.append_all(quote! { - #ident.render(dioxus::prelude::LazyNodes::new(move |__cx: NodeFactory|{ - use dioxus_elements::GlobalAttributes; - - #inner - })) - }), - // Otherwise we just build the LazyNode wrapper - None => out_tokens.append_all(quote! { - dioxus::prelude::LazyNodes::new(move |__cx: NodeFactory|{ - use dioxus_elements::GlobalAttributes; - - #inner - }) - }), - }; - } -} +pub type HTML_OR_RSX = bool; +pub const AS_HTML: bool = true; +pub const AS_RSX: bool = false; diff --git a/packages/core-macro/src/rsx/node.rs b/packages/core-macro/src/rsx/node.rs index e027711e6..33bb44e65 100644 --- a/packages/core-macro/src/rsx/node.rs +++ b/packages/core-macro/src/rsx/node.rs @@ -10,13 +10,13 @@ use syn::{ // ============================================== // Parse any div {} as a VElement // ============================================== -pub enum BodyNode { - Element(AmbiguousElement), +pub enum BodyNode { + Element(AmbiguousElement), Text(TextNode), RawExpr(Expr), } -impl Parse for BodyNode { +impl Parse for BodyNode { fn parse(stream: ParseStream) -> Result { // Supposedly this approach is discouraged due to inability to return proper errors // TODO: Rework this to provide more informative errors @@ -31,11 +31,33 @@ impl Parse for BodyNode { return Ok(BodyNode::Text(stream.parse::()?)); } - Ok(BodyNode::Element(stream.parse::()?)) + Ok(BodyNode::Element( + stream.parse::>()?, + )) + } +} +impl Parse for BodyNode { + fn parse(stream: ParseStream) -> Result { + // Supposedly this approach is discouraged due to inability to return proper errors + // TODO: Rework this to provide more informative errors + + if stream.peek(token::Brace) { + let content; + syn::braced!(content in stream); + return Ok(BodyNode::RawExpr(content.parse::()?)); + } + + if stream.peek(LitStr) { + return Ok(BodyNode::Text(stream.parse::()?)); + } + + Ok(BodyNode::Element( + stream.parse::>()?, + )) } } -impl ToTokens for BodyNode { +impl ToTokens for BodyNode { fn to_tokens(&self, tokens: &mut TokenStream2) { match &self { BodyNode::Element(el) => el.to_tokens(tokens), diff --git a/packages/core-macro/src/styles/calc.rs b/packages/core-macro/styles/calc.rs similarity index 100% rename from packages/core-macro/src/styles/calc.rs rename to packages/core-macro/styles/calc.rs diff --git a/packages/core-macro/src/styles/codegen.rs b/packages/core-macro/styles/codegen.rs similarity index 100% rename from packages/core-macro/src/styles/codegen.rs rename to packages/core-macro/styles/codegen.rs diff --git a/packages/core-macro/src/styles/color.rs b/packages/core-macro/styles/color.rs similarity index 100% rename from packages/core-macro/src/styles/color.rs rename to packages/core-macro/styles/color.rs diff --git a/packages/core-macro/src/styles/mod.rs b/packages/core-macro/styles/mod.rs similarity index 100% rename from packages/core-macro/src/styles/mod.rs rename to packages/core-macro/styles/mod.rs diff --git a/packages/core-macro/src/styles/string/lexer.rs b/packages/core-macro/styles/string/lexer.rs similarity index 100% rename from packages/core-macro/src/styles/string/lexer.rs rename to packages/core-macro/styles/string/lexer.rs diff --git a/packages/core-macro/src/styles/string/mod.rs b/packages/core-macro/styles/string/mod.rs similarity index 100% rename from packages/core-macro/src/styles/string/mod.rs rename to packages/core-macro/styles/string/mod.rs diff --git a/packages/core-macro/src/styles/syn_parse.rs b/packages/core-macro/styles/syn_parse.rs similarity index 100% rename from packages/core-macro/src/styles/syn_parse.rs rename to packages/core-macro/styles/syn_parse.rs From 22f894e6b98073bffa39f08b890071ffc00b8d49 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 22 Jul 2021 02:06:49 -0400 Subject: [PATCH 02/21] wip: more upgades to html parser --- packages/core-macro/src/rsx/body.rs | 91 ++++---------- packages/core-macro/src/rsx/component.rs | 58 +++++---- packages/core-macro/src/rsx/element.rs | 144 +++++++++++++++++------ 3 files changed, 165 insertions(+), 128 deletions(-) diff --git a/packages/core-macro/src/rsx/body.rs b/packages/core-macro/src/rsx/body.rs index cfbc405d2..9ea37585d 100644 --- a/packages/core-macro/src/rsx/body.rs +++ b/packages/core-macro/src/rsx/body.rs @@ -16,41 +16,12 @@ pub struct RsxBody { /// The custom rusty variant of parsing rsx! impl Parse for RsxBody { fn parse(input: ParseStream) -> Result { - // if input.peek(LitStr) { - // return input.parse::()?.parse::(); - // } - - // try to parse the first ident and comma - let custom_context = - if input.peek(Token![in]) && input.peek2(Ident) && input.peek3(Token![,]) { - let _ = input.parse::()?; - let name = input.parse::()?; - if is_valid_tag(&name.to_string()) { - return Err(Error::new( - input.span(), - "Custom context cannot be an html element name", - )); - } else { - input.parse::().unwrap(); - Some(name) - } - } else { - None - }; - - let mut body = Vec::new(); - let mut children = Vec::new(); - let mut manual_props = None; - let cfg: BodyParseConfig = BodyParseConfig { - allow_children: true, - allow_fields: false, - allow_manual_props: false, - }; - cfg.parse_component_body(input, &mut body, &mut children, &mut manual_props)?; - + let custom_context = try_parse_custom_context(input)?; + let (_, roots, _) = + BodyParseConfig::::new_as_body().parse_component_body(&input)?; Ok(Self { - roots: children, custom_context, + roots, }) } } @@ -58,46 +29,28 @@ impl Parse for RsxBody { /// The HTML variant of parsing rsx! impl Parse for RsxBody { fn parse(input: ParseStream) -> Result { - // if input.peek(LitStr) { - // return input.parse::()?.parse::(); - // } - - // try to parse the first ident and comma - let custom_context = - if input.peek(Token![in]) && input.peek2(Ident) && input.peek3(Token![,]) { - let _ = input.parse::()?; - let name = input.parse::()?; - if is_valid_tag(&name.to_string()) { - return Err(Error::new( - input.span(), - "Custom context cannot be an html element name", - )); - } else { - input.parse::().unwrap(); - Some(name) - } - } else { - None - }; - - let mut body = Vec::new(); - let mut children = Vec::new(); - let mut manual_props = None; - - let cfg: BodyParseConfig = BodyParseConfig { - allow_children: true, - allow_fields: false, - allow_manual_props: false, - }; - cfg.parse_component_body(input, &mut body, &mut children, &mut manual_props)?; - + let custom_context = try_parse_custom_context(input)?; + let (_, roots, _) = + BodyParseConfig::::new_as_body().parse_component_body(&input)?; Ok(Self { - roots: children, custom_context, + roots, }) } } +fn try_parse_custom_context(input: ParseStream) -> Result> { + let res = if input.peek(Token![in]) && input.peek2(Ident) && input.peek3(Token![,]) { + let _ = input.parse::()?; + let name = input.parse::()?; + input.parse::()?; + Some(name) + } else { + None + }; + Ok(res) +} + /// Serialize the same way, regardless of flavor impl ToTokens for RsxBody { fn to_tokens(&self, out_tokens: &mut TokenStream2) { @@ -113,7 +66,7 @@ impl ToTokens for RsxBody { // The `in cx` pattern allows directly rendering Some(ident) => out_tokens.append_all(quote! { #ident.render(dioxus::prelude::LazyNodes::new(move |__cx: NodeFactory|{ - use dioxus_elements::GlobalAttributes; + use dioxus_elements::{GlobalAttributes, SvgAttributes}; #inner })) @@ -121,7 +74,7 @@ impl ToTokens for RsxBody { // Otherwise we just build the LazyNode wrapper None => out_tokens.append_all(quote! { dioxus::prelude::LazyNodes::new(move |__cx: NodeFactory|{ - use dioxus_elements::GlobalAttributes; + use dioxus_elements::{GlobalAttributes, SvgAttributes}; #inner }) diff --git a/packages/core-macro/src/rsx/component.rs b/packages/core-macro/src/rsx/component.rs index 79a4d26c4..f493cbe3b 100644 --- a/packages/core-macro/src/rsx/component.rs +++ b/packages/core-macro/src/rsx/component.rs @@ -41,17 +41,13 @@ impl Parse for Component { let content: ParseBuffer; syn::braced!(content in stream); - let mut body: Vec> = Vec::new(); - let mut children: Vec> = Vec::new(); - let mut manual_props = None; - let cfg: BodyParseConfig = BodyParseConfig { allow_children: true, allow_fields: true, allow_manual_props: true, }; - cfg.parse_component_body(&content, &mut body, &mut children, &mut manual_props)?; + let (body, children, manual_props) = cfg.parse_component_body(&content)?; Ok(Self { name, @@ -69,17 +65,13 @@ impl Parse for Component { let content: ParseBuffer; syn::braced!(content in stream); - let mut body: Vec> = Vec::new(); - let mut children: Vec> = Vec::new(); - let mut manual_props = None; - let cfg: BodyParseConfig = BodyParseConfig { allow_children: true, allow_fields: true, allow_manual_props: true, }; - cfg.parse_component_body(&content, &mut body, &mut children, &mut manual_props)?; + let (body, children, manual_props) = cfg.parse_component_body(&content)?; Ok(Self { name, @@ -95,16 +87,33 @@ pub struct BodyParseConfig { pub allow_children: bool, pub allow_manual_props: bool, } + +impl BodyParseConfig { + /// The configuration to parse the root + pub fn new_as_body() -> Self { + Self { + allow_children: true, + allow_fields: false, + allow_manual_props: false, + } + } +} + impl BodyParseConfig { // todo: unify this body parsing for both elements and components // both are style rather ad-hoc, though components are currently more configured pub fn parse_component_body( &self, content: &ParseBuffer, - body: &mut Vec>, - children: &mut Vec>, - manual_props: &mut Option, - ) -> Result<()> { + ) -> Result<( + Vec>, + Vec>, + Option, + )> { + let mut body = Vec::new(); + let mut children = Vec::new(); + let mut manual_props = None; + 'parsing: loop { // [1] Break if empty if content.is_empty() { @@ -119,7 +128,7 @@ impl BodyParseConfig { )); } content.parse::()?; - *manual_props = Some(content.parse::()?); + manual_props = Some(content.parse::()?); } else if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { if !self.allow_fields { return Err(Error::new( @@ -144,7 +153,7 @@ impl BodyParseConfig { let _ = content.parse::(); } } - Ok(()) + Ok((body, children, manual_props)) } } impl BodyParseConfig { @@ -153,10 +162,15 @@ impl BodyParseConfig { pub fn parse_component_body( &self, content: &ParseBuffer, - body: &mut Vec>, - children: &mut Vec>, - manual_props: &mut Option, - ) -> Result<()> { + ) -> Result<( + Vec>, + Vec>, + Option, + )> { + let mut body = Vec::new(); + let mut children = Vec::new(); + let mut manual_props = None; + 'parsing: loop { // [1] Break if empty if content.is_empty() { @@ -171,7 +185,7 @@ impl BodyParseConfig { )); } content.parse::()?; - *manual_props = Some(content.parse::()?); + manual_props = Some(content.parse::()?); } else if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { if !self.allow_fields { return Err(Error::new( @@ -196,7 +210,7 @@ impl BodyParseConfig { let _ = content.parse::(); } } - Ok(()) + Ok((body, children, manual_props)) } } diff --git a/packages/core-macro/src/rsx/element.rs b/packages/core-macro/src/rsx/element.rs index af0e6daa3..e5973d263 100644 --- a/packages/core-macro/src/rsx/element.rs +++ b/packages/core-macro/src/rsx/element.rs @@ -14,8 +14,8 @@ use syn::{ pub struct Element { name: Ident, key: Option, - attributes: Vec, - listeners: Vec, + attributes: Vec>, + listeners: Vec>, children: Vec>, is_static: bool, } @@ -28,8 +28,8 @@ impl Parse for Element { let content: ParseBuffer; syn::braced!(content in stream); - let mut attributes: Vec = vec![]; - let mut listeners: Vec = vec![]; + let mut attributes: Vec> = vec![]; + let mut listeners: Vec> = vec![]; let mut children: Vec> = vec![]; let mut key = None; @@ -40,7 +40,7 @@ impl Parse for Element { } if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { - parse_element_body( + parse_rsx_element_field( &content, &mut attributes, &mut listeners, @@ -71,45 +71,114 @@ impl Parse for Element { impl Parse for Element { fn parse(stream: ParseStream) -> Result { - let name = Ident::parse(stream)?; + let l_tok = stream.parse::()?; + let el_name = Ident::parse(stream)?; // parse the guts - let content: ParseBuffer; - syn::braced!(content in stream); + // let content: ParseBuffer; + // syn::braced!(content in stream); - let mut attributes: Vec = vec![]; - let mut listeners: Vec = vec![]; + let mut attributes: Vec> = vec![]; + let mut listeners: Vec> = vec![]; let mut children: Vec> = vec![]; let mut key = None; - 'parsing: loop { - // [1] Break if empty - if content.is_empty() { - break 'parsing; + // loop { + // if stream.peek(Token![>]) { + // break; + // } else { + // } + // } + while !stream.peek(Token![>]) { + // self-closing + if stream.peek(Token![/]) { + stream.parse::()?; + stream.parse::]>()?; + + return Ok(Self { + name: el_name, + key: None, + attributes, + is_static: false, + listeners, + children, + }); } - if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { - parse_element_body( - &content, - &mut attributes, - &mut listeners, - &mut key, - name.clone(), - )?; + let name = Ident::parse_any(stream)?; + let name_str = name.to_string(); + stream.parse::()?; + if name_str.starts_with("on") { + todo!() } else { - children.push(content.parse::>()?); - } + match name_str.as_str() { + "style" => todo!(), + "key" => todo!(), + "classes" => todo!(), + "namespace" => todo!(), + "ref" => todo!(), + _ => { + let ty = if stream.peek(LitStr) { + let rawtext = stream.parse::().unwrap(); + AttrType::BumpText(rawtext) + } else { + let toks = stream.parse::()?; + AttrType::FieldTokens(toks) + }; + attributes.push(ElementAttr { + element_name: el_name.clone(), + name, + value: ty, + namespace: None, + }) + } + } + }; + // if stream.peek(LitStr) { - // consume comma if it exists - // we don't actually care if there *are* commas after elements/text - if content.peek(Token![,]) { - let _ = content.parse::(); - } + // } else { + // } + // if name_str.starts_with("on") {} + + // attributes.push(stream.parse()?); } + stream.parse::]>()?; + + // closing element + stream.parse::()?; + stream.parse::()?; + let close = Ident::parse_any(stream)?; + if close.to_string() != el_name.to_string() { + return Err(Error::new_spanned( + close, + "closing element does not match opening", + )); + } + stream.parse::]>()?; + // 'parsing: loop { + // // if stream.peek(Token![>]) {} + + // // // [1] Break if empty + // // if content.is_empty() { + // // break 'parsing; + // // } + + // if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { + // parse_element_body( + // &content, + // &mut attributes, + // &mut listeners, + // &mut key, + // name.clone(), + // )?; + // } else { + // children.push(stream.parse::>()?); + // } + // } Ok(Self { key, - name, + name: el_name, attributes, children, listeners, @@ -140,7 +209,7 @@ impl ToTokens for Element { /// ======================================= /// Parse a VElement's Attributes /// ======================================= -struct ElementAttr { +struct ElementAttr { element_name: Ident, name: Ident, value: AttrType, @@ -157,14 +226,14 @@ enum AttrType { // We parse attributes and dump them into the attribute vec // This is because some tags might be namespaced (IE style) // These dedicated tags produce multiple name-spaced attributes -fn parse_element_body( +fn parse_rsx_element_field( stream: ParseStream, - attrs: &mut Vec, - listeners: &mut Vec, + attrs: &mut Vec>, + listeners: &mut Vec>, key: &mut Option, element_name: Ident, ) -> Result<()> { - let mut name = Ident::parse_any(stream)?; + let name = Ident::parse_any(stream)?; let name_str = name.to_string(); stream.parse::()?; @@ -199,7 +268,8 @@ fn parse_element_body( let ty: AttrType = match name_str.as_str() { // short circuit early if style is using the special syntax - "style" if stream.peek(token::Brace) => { + "style" if stream.peek(Token![:]) => { + stream.parse::().unwrap(); let inner; syn::braced!(inner in stream); @@ -267,7 +337,7 @@ fn parse_element_body( Ok(()) } -impl ToTokens for ElementAttr { +impl ToTokens for ElementAttr { fn to_tokens(&self, tokens: &mut TokenStream2) { let el_name = &self.element_name; let name_str = self.name.to_string(); From f644d7c44159eef091552dcc90acbb151ea76b21 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 23 Jul 2021 10:27:43 -0400 Subject: [PATCH 03/21] wip: move things into a "shared" object --- packages/core-macro/src/rsx/element.rs | 32 ++--- packages/core/.vscode/settings.json | 2 +- packages/core/src/arena.rs | 148 ++++++++++++++++------ packages/core/src/bumpframe.rs | 120 ++++++++++-------- packages/core/src/childiter.rs | 91 -------------- packages/core/src/component.rs | 44 ++++++- packages/core/src/context.rs | 55 ++++---- packages/core/src/diff.rs | 166 ++++++++++++++----------- packages/core/src/editor.rs | 12 +- packages/core/src/events.rs | 19 +-- packages/core/src/heuristics.rs | 19 ++- packages/core/src/lib.rs | 6 +- packages/core/src/nodes.rs | 67 ++++++---- packages/core/src/scope.rs | 85 +++++-------- packages/core/src/tasks.rs | 137 -------------------- packages/core/src/util.rs | 74 ++--------- packages/core/src/virtual_dom.rs | 132 ++++++++------------ packages/ssr/src/lib.rs | 4 +- packages/web/src/new.rs | 27 ++-- 19 files changed, 542 insertions(+), 698 deletions(-) delete mode 100644 packages/core/src/childiter.rs delete mode 100644 packages/core/src/tasks.rs diff --git a/packages/core-macro/src/rsx/element.rs b/packages/core-macro/src/rsx/element.rs index e5973d263..c4fa949e8 100644 --- a/packages/core-macro/src/rsx/element.rs +++ b/packages/core-macro/src/rsx/element.rs @@ -109,20 +109,29 @@ impl Parse for Element { let name_str = name.to_string(); stream.parse::()?; if name_str.starts_with("on") { - todo!() + let inner; + syn::braced!(inner in stream); + let toks = inner.parse::()?; + let ty = AttrType::EventTokens(toks); + listeners.push(ElementAttr { + element_name: el_name.clone(), + name, + value: ty, + namespace: None, + }) } else { match name_str.as_str() { - "style" => todo!(), - "key" => todo!(), - "classes" => todo!(), - "namespace" => todo!(), - "ref" => todo!(), - _ => { + "style" => {} + "key" => {} + "classes" | "namespace" | "ref" | _ => { let ty = if stream.peek(LitStr) { let rawtext = stream.parse::().unwrap(); AttrType::BumpText(rawtext) } else { - let toks = stream.parse::()?; + // like JSX, we expect raw expressions + let inner; + syn::braced!(inner in stream); + let toks = inner.parse::()?; AttrType::FieldTokens(toks) }; attributes.push(ElementAttr { @@ -134,13 +143,6 @@ impl Parse for Element { } } }; - // if stream.peek(LitStr) { - - // } else { - // } - // if name_str.starts_with("on") {} - - // attributes.push(stream.parse()?); } stream.parse::]>()?; diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json index 2b58ae251..1c68f97cc 100644 --- a/packages/core/.vscode/settings.json +++ b/packages/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": false + "rust-analyzer.inlayHints.enable": true } diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 6d24afee0..0e8be0eaf 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -1,55 +1,99 @@ +use std::cell::{RefCell, RefMut}; use std::{cell::UnsafeCell, rc::Rc}; +use crate::heuristics::*; use crate::innerlude::*; +use futures_util::stream::FuturesUnordered; use slotmap::SlotMap; +slotmap::new_key_type! { + // A dedicated key type for the all the scopes + pub struct ScopeId; +} + +slotmap::new_key_type! { + // A dedicated key type for every real element that the virtualdom creates. + // + // This is a slotmap key because we expect the "mirror" realdom to also maintain a slotmap mapping + // of virtual element to real element. + pub struct ElementId; +} + +impl ElementId { + pub fn as_u64(self) -> u64 { + self.0.as_ffi() + } +} + +type Shared = Rc>; + +/// These are resources shared among all the components and the virtualdom itself #[derive(Clone)] -pub struct SharedArena { - pub components: Rc>, -} -pub type ScopeMap = SlotMap; +pub struct SharedResources { + pub components: Rc>>, -enum MutStatus { - Immut, - Mut, + pub event_queue: Shared>, + + pub events: Shared>, + + pub(crate) heuristics: Shared, + + pub(crate) tasks: Shared>, + + /// We use a SlotSet to keep track of the keys that are currently being used. + /// However, we don't store any specific data since the "mirror" + pub raw_elements: Shared>, + + pub task_setter: Rc, } -impl SharedArena { - pub fn new(arena: ScopeMap) -> Self { - let components = Rc::new(UnsafeCell::new(arena)); - SharedArena { components } +impl SharedResources { + pub fn new() -> Self { + // preallocate 1000 elements and 20 scopes to avoid dynamic allocation + let components = Rc::new(UnsafeCell::new( + SlotMap::::with_capacity_and_key(20), + )); + let raw_elements = SlotMap::::with_capacity_and_key(1000); + + let event_queue = Rc::new(RefCell::new(Vec::new())); + let tasks = Vec::new(); + let heuristics = HeuristicsEngine::new(); + + let queue = event_queue.clone(); + let _components = components.clone(); + let task_setter = Rc::new(move |idx| { + let comps = unsafe { &*_components.get() }; + if let Some(scope) = comps.get(idx) { + queue.borrow_mut().push(HeightMarker { + height: scope.height, + idx, + }) + } + }); + + Self { + event_queue, + components, + tasks: Rc::new(RefCell::new(FuturesUnordered::new())), + events: Rc::new(RefCell::new(tasks)), + heuristics: Rc::new(RefCell::new(heuristics)), + raw_elements: Rc::new(RefCell::new(raw_elements)), + task_setter, + } } - /// THIS METHOD IS CURRENTLY UNSAFE - /// THERE ARE NO CHECKS TO VERIFY THAT WE ARE ALLOWED TO DO THIS - pub fn get(&self, idx: ScopeId) -> Option<&Scope> { - let inner = unsafe { &*self.components.get() }; + /// this is unsafe because the caller needs to track which other scopes it's already using + pub unsafe fn get_scope(&self, idx: ScopeId) -> Option<&Scope> { + let inner = &*self.components.get(); inner.get(idx) } - /// THIS METHOD IS CURRENTLY UNSAFE - /// THERE ARE NO CHECKS TO VERIFY THAT WE ARE ALLOWED TO DO THIS - pub fn get_mut(&self, idx: ScopeId) -> Option<&mut Scope> { - let inner = unsafe { &mut *self.components.get() }; + /// this is unsafe because the caller needs to track which other scopes it's already using + pub unsafe fn get_sope_mut(&self, idx: ScopeId) -> Option<&mut Scope> { + let inner = &mut *self.components.get(); inner.get_mut(idx) } - fn inner(&self) -> &ScopeMap { - todo!() - } - - fn inner_mut(&mut self) -> &mut ScopeMap { - todo!() - } - - /// THIS METHOD IS CURRENTLY UNSAFE - /// THERE ARE NO CHECKS TO VERIFY THAT WE ARE ALLOWED TO DO THIS - pub fn with(&self, f: impl FnOnce(&mut ScopeMap) -> T) -> Result { - let inner = unsafe { &mut *self.components.get() }; - Ok(f(inner)) - // todo!() - } - pub fn with_scope<'b, O: 'static>( &'b self, _id: ScopeId, @@ -75,7 +119,37 @@ impl SharedArena { .ok_or_else(|| Error::FatalInternal("Scope not found")) } - unsafe fn inner_unchecked<'s>() -> &'s mut ScopeMap { - todo!() + pub fn reserve_node(&self) -> ElementId { + self.raw_elements.borrow_mut().insert(()) + } + + /// return the id, freeing the space of the original node + pub fn collect_garbage(&self, id: ElementId) {} + + pub fn borrow_queue(&self) -> RefMut> { + self.event_queue.borrow_mut() + } + + pub fn insert_scope_with_key(&self, f: impl FnOnce(ScopeId) -> Scope) -> ScopeId { + let g = unsafe { &mut *self.components.get() }; + g.insert_with_key(f) + } + + pub fn schedule_update(&self) -> Rc { + self.task_setter.clone() + } + + pub fn submit_task(&self, task: FiberTask) -> TaskHandle { + self.tasks.borrow_mut().push(task); + TaskHandle {} } } + +pub struct TaskHandle {} + +impl TaskHandle { + pub fn toggle(&self) {} + pub fn start(&self) {} + pub fn stop(&self) {} + pub fn restart(&self) {} +} diff --git a/packages/core/src/bumpframe.rs b/packages/core/src/bumpframe.rs index 62d5035dd..19c545679 100644 --- a/packages/core/src/bumpframe.rs +++ b/packages/core/src/bumpframe.rs @@ -1,10 +1,10 @@ use crate::innerlude::*; use bumpalo::Bump; +use std::cell::Cell; -use std::cell::RefCell; pub struct ActiveFrame { // We use a "generation" for users of contents in the bump frames to ensure their data isn't broken - pub generation: RefCell, + pub generation: Cell, // The double-buffering situation that we will use pub frames: [BumpFrame; 2], @@ -13,78 +13,100 @@ pub struct ActiveFrame { pub struct BumpFrame { pub bump: Bump, pub head_node: VNode<'static>, + + #[cfg(test)] + name: &'static str, } impl ActiveFrame { pub fn new() -> Self { - Self::from_frames( - BumpFrame { - bump: Bump::new(), - head_node: NodeFactory::static_text(""), - }, - BumpFrame { - bump: Bump::new(), - head_node: NodeFactory::static_text(""), - }, - ) - } + let frame_a = BumpFrame { + bump: Bump::new(), + head_node: NodeFactory::unstable_place_holder(), - pub fn from_frames(a: BumpFrame, b: BumpFrame) -> Self { + #[cfg(test)] + name: "old", + }; + let frame_b = BumpFrame { + bump: Bump::new(), + head_node: NodeFactory::unstable_place_holder(), + + #[cfg(test)] + name: "new", + }; Self { generation: 0.into(), - frames: [a, b], + frames: [frame_a, frame_b], } } - pub fn cur_frame(&self) -> &BumpFrame { - match *self.generation.borrow() & 1 == 0 { + pub fn prev_frame(&self) -> &BumpFrame { + match self.generation.get() & 1 == 0 { true => &self.frames[0], false => &self.frames[1], } } - pub fn cur_frame_mut(&mut self) -> &mut BumpFrame { - match *self.generation.borrow() & 1 == 0 { + + pub fn prev_frame_mut(&mut self) -> &mut BumpFrame { + match self.generation.get() & 1 == 0 { true => &mut self.frames[0], false => &mut self.frames[1], } } + pub fn cur_frame(&self) -> &BumpFrame { + match self.generation.get() & 1 == 1 { + true => &self.frames[0], + false => &self.frames[1], + } + } + + pub fn cur_frame_mut(&mut self) -> &mut BumpFrame { + match self.generation.get() & 1 == 1 { + true => &mut self.frames[0], + false => &mut self.frames[1], + } + } + /// Give out our self-referential item with our own borrowed lifetime pub fn current_head_node<'b>(&'b self) -> &'b VNode<'b> { - let raw_node = match *self.generation.borrow() & 1 == 0 { - true => &self.frames[0], - false => &self.frames[1], - }; - - // Give out our self-referential item with our own borrowed lifetime - unsafe { - let unsafe_head = &raw_node.head_node; - let safe_node = std::mem::transmute::<&VNode<'static>, &VNode<'b>>(unsafe_head); - safe_node - } + let cur_head = &self.cur_frame().head_node; + unsafe { std::mem::transmute::<&VNode<'static>, &VNode<'b>>(cur_head) } } + /// Give out our self-referential item with our own borrowed lifetime pub fn prev_head_node<'b>(&'b self) -> &'b VNode<'b> { - let raw_node = match *self.generation.borrow() & 1 != 0 { - true => &self.frames[0], - false => &self.frames[1], - }; - - // Give out our self-referential item with our own borrowed lifetime - unsafe { - let unsafe_head = &raw_node.head_node; - let safe_node = std::mem::transmute::<&VNode<'static>, &VNode<'b>>(unsafe_head); - safe_node - } - } - - pub fn old_frame_mut(&mut self) -> &mut BumpFrame { - match *self.generation.borrow() & 1 == 0 { - true => &mut self.frames[1], - false => &mut self.frames[0], - } + let cur_head = &self.prev_frame().head_node; + unsafe { std::mem::transmute::<&VNode<'static>, &VNode<'b>>(cur_head) } } pub fn cycle_frame(&mut self) { - *self.generation.borrow_mut() += 1; + self.generation.set(self.generation.get() + 1); + } +} + +#[cfg(test)] +mod tests { + //! These tests are bad. I don't have a good way of properly testing the ActiveFrame stuff + use super::*; + + #[test] + fn test_bump_frame() { + let mut frames = ActiveFrame::new(); + + // just cycle a few times and make sure we get the right frames out + for _ in 0..5 { + let old = frames.prev_frame(); + let new = frames.cur_frame(); + assert_eq!(old.name, "old"); + assert_eq!(new.name, "new"); + frames.cycle_frame(); + + let old = frames.prev_frame(); + let new = frames.cur_frame(); + assert_eq!(old.name, "new"); + assert_eq!(new.name, "old"); + frames.cycle_frame(); + } + assert_eq!(frames.generation.get(), 10); } } diff --git a/packages/core/src/childiter.rs b/packages/core/src/childiter.rs deleted file mode 100644 index 875bf6ec6..000000000 --- a/packages/core/src/childiter.rs +++ /dev/null @@ -1,91 +0,0 @@ -/// This iterator iterates through a list of virtual children and only returns real children (Elements or Text). -/// -/// This iterator is useful when it's important to load the next real root onto the top of the stack for operations like -/// "InsertBefore". -struct RealChildIterator<'a> { - scopes: &'a SharedArena, - - // Heuristcally we should never bleed into 5 completely nested fragments/components - // Smallvec lets us stack allocate our little stack machine so the vast majority of cases are sane - stack: smallvec::SmallVec<[(u16, &'a VNode<'a>); 5]>, -} - -impl<'a> RealChildIterator<'a> { - fn new(starter: &'a VNode<'a>, scopes: &'a SharedArena) -> Self { - Self { - scopes, - stack: smallvec::smallvec![(0, starter)], - } - } -} - -impl<'a> Iterator for RealChildIterator<'a> { - type Item = &'a VNode<'a>; - - fn next(&mut self) -> Option<&'a VNode<'a>> { - let mut should_pop = false; - let mut returned_node = None; - let mut should_push = None; - - while returned_node.is_none() { - if let Some((count, node)) = self.stack.last_mut() { - match node { - // We can only exit our looping when we get "real" nodes - // This includes fragments and components when they're empty (have a single root) - VNode::Element(_) | VNode::Text(_) => { - // We've recursed INTO an element/text - // We need to recurse *out* of it and move forward to the next - should_pop = true; - returned_node = Some(&**node); - } - - // If we get a fragment we push the next child - VNode::Fragment(frag) => { - let subcount = *count as usize; - - if frag.children.len() == 0 { - should_pop = true; - returned_node = Some(&**node); - } - - if subcount >= frag.children.len() { - should_pop = true; - } else { - should_push = Some(&frag.children[subcount]); - } - } - - // Immediately abort suspended nodes - can't do anything with them yet - // VNode::Suspended => should_pop = true, - VNode::Suspended { real } => todo!(), - - // For components, we load their root and push them onto the stack - VNode::Component(sc) => { - let scope = self.scopes.try_get(sc.ass_scope.get().unwrap()).unwrap(); - - // Simply swap the current node on the stack with the root of the component - *node = scope.root(); - } - } - } else { - // If there's no more items on the stack, we're done! - return None; - } - - if should_pop { - self.stack.pop(); - if let Some((id, _)) = self.stack.last_mut() { - *id += 1; - } - should_pop = false; - } - - if let Some(push) = should_push { - self.stack.push((0, push)); - should_push = None; - } - } - - returned_node - } -} diff --git a/packages/core/src/component.rs b/packages/core/src/component.rs index c6acdb721..754fa5498 100644 --- a/packages/core/src/component.rs +++ b/packages/core/src/component.rs @@ -5,8 +5,40 @@ //! if the type suppports PartialEq. The Properties trait is used by the rsx! and html! macros to generate the type-safe builder //! that ensures compile-time required and optional fields on cx. -use crate::innerlude::{Context, DomTree, LazyNodes, VNode, FC}; +use crate::innerlude::{Context, DomTree, LazyNodes, FC}; +/// Every "Props" used for a component must implement the `Properties` trait. This trait gives some hints to Dioxus +/// on how to memoize the props and some additional optimizations that can be made. We strongly encourage using the +/// derive macro to implement the `Properties` trait automatically as guarantee that your memoization strategy is safe. +/// +/// If your props are 'static, then Dioxus will require that they also be PartialEq for the derived memoize strategy. However, +/// if your props borrow data, then the memoization strategy will simply default to "false" and the PartialEq will be ignored. +/// This tends to be useful when props borrow something that simply cannot be compared (IE a reference to a closure); +/// +/// By default, the memoization strategy is very conservative, but can be tuned to be more aggressive manually. However, +/// this is only safe if the props are 'static - otherwise you might borrow references after-free. +/// +/// We strongly suggest that any changes to memoization be done at the "PartialEq" level for 'static props. Additionally, +/// we advise the use of smart pointers in cases where memoization is important. +/// +/// ## Example +/// +/// For props that are 'static: +/// ```rust ignore +/// #[derive(Props, PartialEq)] +/// struct MyProps { +/// data: String +/// } +/// ``` +/// +/// For props that borrow: +/// +/// ```rust ignore +/// #[derive(Props)] +/// struct MyProps<'a >{ +/// data: &'a str +/// } +/// ``` pub trait Properties: Sized { type Builder; const IS_STATIC: bool = false; @@ -50,6 +82,16 @@ pub fn fc_to_builder(_: FC) -> T::Builder { /// /// Fragments are incredibly useful when necessary, but *do* add cost in the diffing phase. /// Try to avoid nesting fragments if you can. Infinitely nested Fragments *will* cause diffing to crash. +/// +/// This function defines a dedicated `Fragment` component that can be used to create inline fragments in the RSX macro. +/// +/// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it. +/// +/// ```rust +/// rsx!{ +/// Fragment { key: "abc" } +/// } +/// ``` #[allow(non_upper_case_globals, non_snake_case)] pub fn Fragment<'a>(cx: Context<'a, ()>) -> DomTree<'a> { cx.render(LazyNodes::new(move |f| f.fragment_from_iter(cx.children()))) diff --git a/packages/core/src/context.rs b/packages/core/src/context.rs index 0c8879eae..efc7a5613 100644 --- a/packages/core/src/context.rs +++ b/packages/core/src/context.rs @@ -99,12 +99,21 @@ impl<'src, P> Context<'src, P> { /// } /// ``` pub fn children(&self) -> &'src [VNode<'src>] { - self.scope.child_nodes + self.scope.child_nodes() } /// Create a subscription that schedules a future render for the reference component + /// + /// ## Notice: you should prefer using prepare_update and get_scope_id + /// pub fn schedule_update(&self) -> Rc { - self.scope.event_channel.clone() + let cb = self.scope.vdom.schedule_update(); + let id = self.get_scope_id(); + Rc::new(move || cb(id)) + } + + fn prepare_update(&self) -> Rc { + self.scope.vdom.schedule_update() } pub fn schedule_effect(&self) -> Rc { @@ -142,7 +151,7 @@ impl<'src, P> Context<'src, P> { let scope_ref = self.scope; let listener_id = &scope_ref.listener_idx; Some(lazy_nodes.into_vnode(NodeFactory { - scope_ref, + scope: scope_ref, listener_id, })) } @@ -297,7 +306,7 @@ Any function prefixed with "use" should not be called conditionally. } else { match inner.parent_idx { Some(parent_id) => { - scope = inner.arena_link.get(parent_id); + scope = unsafe { inner.vdom.get_scope(parent_id) }; } None => break, } @@ -329,8 +338,7 @@ Any function prefixed with "use" should not be called conditionally. /// /// pub fn submit_task(&self, task: FiberTask) -> TaskHandle { - (self.scope.task_submitter)(task); - TaskHandle { _p: PhantomData {} } + self.scope.vdom.submit_task(task) } /// Awaits the given task, forcing the component to re-render when the value is ready. @@ -341,13 +349,14 @@ Any function prefixed with "use" should not be called conditionally. pub fn use_task( self, task_initializer: Init, - ) -> (TaskHandle<'src>, &'src Option) + ) -> (&'src TaskHandle, &'src Option) where Out: 'static, Fut: Future + 'static, Init: FnOnce() -> Fut + 'src, { struct TaskHook { + handle: TaskHandle, task_dump: Rc>>, value: Option, } @@ -360,12 +369,15 @@ Any function prefixed with "use" should not be called conditionally. let task_dump = Rc::new(RefCell::new(None)); let slot = task_dump.clone(); - let update = self.schedule_update(); + + let updater = self.prepare_update(); + let update_id = self.get_scope_id(); + let originator = self.scope.our_arena_idx.clone(); - self.submit_task(Box::pin(task_fut.then(move |output| async move { + let handle = self.submit_task(Box::pin(task_fut.then(move |output| async move { *slot.as_ref().borrow_mut() = Some(output); - update(); + updater(update_id); EventTrigger { event: VirtualEvent::AsyncEvent { hook_idx }, originator, @@ -377,13 +389,14 @@ Any function prefixed with "use" should not be called conditionally. TaskHook { task_dump, value: None, + handle, } }, |hook| { if let Some(val) = hook.task_dump.as_ref().borrow_mut().take() { hook.value = Some(val); } - (TaskHandle { _p: PhantomData }, &hook.value) + (&hook.handle, &hook.value) }, |_| {}, ) @@ -393,7 +406,7 @@ Any function prefixed with "use" should not be called conditionally. pub(crate) struct SuspenseHook { pub value: Rc>>>, pub callback: SuspendedCallback, - pub dom_node_id: Rc>, + pub dom_node_id: Rc>>, } type SuspendedCallback = Box Fn(SuspendedContext<'a>) -> DomTree<'a>>; @@ -422,7 +435,7 @@ impl<'src, P> Context<'src, P> { move |hook_idx| { let value = Rc::new(RefCell::new(None)); - let dom_node_id = Rc::new(RealDomNode::empty_cell()); + let dom_node_id = Rc::new(empty_cell()); let domnode = dom_node_id.clone(); let slot = value.clone(); @@ -438,7 +451,7 @@ impl<'src, P> Context<'src, P> { None => { // Some(VNode { - dom_id: RealDomNode::empty_cell(), + dom_id: empty_cell(), key: None, kind: VNodeKind::Suspended { node: domnode.clone(), @@ -495,20 +508,8 @@ impl<'src> SuspendedContext<'src> { let scope_ref = self.inner.scope; let listener_id = &scope_ref.listener_idx; Some(lazy_nodes.into_vnode(NodeFactory { - scope_ref, + scope: scope_ref, listener_id, })) } } - -#[derive(Clone, Copy)] -pub struct TaskHandle<'src> { - _p: PhantomData<&'src ()>, -} - -impl<'src> TaskHandle<'src> { - pub fn toggle(&self) {} - pub fn start(&self) {} - pub fn stop(&self) {} - pub fn restart(&self) {} -} diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index a91a81cdc..f29c1fc84 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -15,7 +15,7 @@ //! whose keys can be converted to u64 on FFI boundaries. //! //! When new nodes are created through `render`, they won't know which real node they correspond to. During diffing, we -//! always make sure to copy over the ID. If we don't do this properly, the realdomnode will be populated incorrectly and +//! always make sure to copy over the ID. If we don't do this properly, the ElementId will be populated incorrectly and //! brick the user's page. //! //! ## Subtree Memoization @@ -48,11 +48,11 @@ //! More info on how to improve this diffing algorithm: //! - https://hacks.mozilla.org/2019/03/fast-bump-allocated-virtual-doms-with-rust-and-wasm/ -use crate::{arena::SharedArena, innerlude::*, tasks::TaskQueue}; -use fxhash::FxHashSet; +use crate::{arena::SharedResources, innerlude::*}; +use fxhash::{FxHashMap, FxHashSet}; use smallvec::{smallvec, SmallVec}; -use std::any::Any; +use std::{any::Any, borrow::Borrow}; /// The accompanying "real dom" exposes an imperative API for controlling the UI layout /// @@ -66,21 +66,35 @@ use std::any::Any; /// any modifications that follow. This technique enables the diffing algorithm to avoid directly handling or storing any /// target-specific Node type as well as easily serializing the edits to be sent over a network or IPC connection. pub trait RealDom<'a> { - fn request_available_node(&mut self) -> RealDomNode; fn raw_node_as_any(&self) -> &mut dyn Any; } pub struct DiffMachine<'real, 'bump, Dom: RealDom<'bump>> { - pub dom: &'real mut Dom, + pub real_dom: &'real mut Dom, + pub vdom: &'bump SharedResources, pub edits: DomEditor<'real, 'bump>, - pub components: &'bump SharedArena, - pub task_queue: &'bump TaskQueue, + pub cur_idxs: SmallVec<[ScopeId; 5]>, pub diffed: FxHashSet, - pub event_queue: EventQueue, pub seen_nodes: FxHashSet, } +impl<'r, 'b, D: RealDom<'b>> DiffMachine<'r, 'b, D> { + pub fn get_scope_mut(&mut self, id: &ScopeId) -> Option<&'b mut Scope> { + // ensure we haven't seen this scope before + // if we have, then we're trying to alias it, which is not allowed + debug_assert!(!self.seen_nodes.contains(id)); + let compon = unsafe { &mut *self.vdom.components.get() }; + compon.get_mut(*id) + } + pub fn get_scope(&mut self, id: &ScopeId) -> Option<&'b Scope> { + // ensure we haven't seen this scope before + // if we have, then we're trying to alias it, which is not allowed + let compon = unsafe { &*self.vdom.components.get() }; + compon.get(*id) + } +} + impl<'real, 'bump, Dom> DiffMachine<'real, 'bump, Dom> where Dom: RealDom<'bump>, @@ -88,18 +102,14 @@ where pub fn new( edits: &'real mut Vec>, dom: &'real mut Dom, - components: &'bump SharedArena, cur_idx: ScopeId, - event_queue: EventQueue, - task_queue: &'bump TaskQueue, + shared: &'bump SharedResources, ) -> Self { Self { + real_dom: dom, edits: DomEditor::new(edits), - components, - dom, cur_idxs: smallvec![cur_idx], - event_queue, - task_queue, + vdom: shared, diffed: FxHashSet::default(), seen_nodes: FxHashSet::default(), } @@ -112,20 +122,24 @@ where // // each function call assumes the stack is fresh (empty). pub fn diff_node(&mut self, old_node: &'bump VNode<'bump>, new_node: &'bump VNode<'bump>) { + let root = old_node + .dom_id + .get() + .expect("Should not be diffing old nodes that were never assigned"); + match (&old_node.kind, &new_node.kind) { // Handle the "sane" cases first. // The rsx and html macros strongly discourage dynamic lists not encapsulated by a "Fragment". // So the sane (and fast!) cases are where the virtual structure stays the same and is easily diffable. (VNodeKind::Text(old), VNodeKind::Text(new)) => { - let root = old_node.dom_id.get(); if old.text != new.text { - self.edits.push(root); + self.edits.push_root(root); log::debug!("Text has changed {}, {}", old.text, new.text); self.edits.set_text(new.text); self.edits.pop(); } - new_node.dom_id.set(root); + new_node.dom_id.set(Some(root)); } (VNodeKind::Element(old), VNodeKind::Element(new)) => { @@ -133,20 +147,19 @@ where // This is an optimization React makes due to how users structure their code // // In Dioxus, this is less likely to occur unless through a fragment - let root = old_node.dom_id.get(); if new.tag_name != old.tag_name || new.namespace != old.namespace { - self.edits.push(root); + self.edits.push_root(root); let meta = self.create(new_node); self.edits.replace_with(meta.added_to_stack); self.edits.pop(); return; } - new_node.dom_id.set(root); + new_node.dom_id.set(Some(root)); // push it just in case // TODO: remove this - it clogs up things and is inefficient - self.edits.push(root); + self.edits.push_root(root); self.diff_listeners(old.listeners, new.listeners); self.diff_attr(old.attributes, new.attributes, new.namespace); self.diff_children(old.children, new.children); @@ -160,11 +173,11 @@ where self.cur_idxs.push(old.ass_scope.get().unwrap()); // Make sure the new component vnode is referencing the right scope id - let scope_id = old.ass_scope.get(); - new.ass_scope.set(scope_id); + let scope_addr = old.ass_scope.get().unwrap(); + new.ass_scope.set(Some(scope_addr)); // make sure the component's caller function is up to date - let scope = self.components.get_mut(scope_id.unwrap()).unwrap(); + let scope = self.get_scope_mut(&scope_addr).unwrap(); scope.caller = new.caller.clone(); @@ -185,22 +198,22 @@ where } self.cur_idxs.pop(); - self.seen_nodes.insert(scope_id.unwrap()); + self.seen_nodes.insert(scope_addr); } else { // this seems to be a fairy common code path that we could - let mut old_iter = RealChildIterator::new(old_node, &self.components); + let mut old_iter = RealChildIterator::new(old_node, &self.vdom); let first = old_iter .next() .expect("Components should generate a placeholder root"); // remove any leftovers for to_remove in old_iter { - self.edits.push(to_remove); + self.edits.push_root(to_remove); self.edits.remove(); } // seems like we could combine this into a single instruction.... - self.edits.push(first); + self.edits.push_root(first); let meta = self.create(new_node); self.edits.replace_with(meta.added_to_stack); self.edits.pop(); @@ -229,8 +242,12 @@ where } // The strategy here is to pick the first possible node from the previous set and use that as our replace with root + // // We also walk the "real node" list to make sure all latent roots are claened up // This covers the case any time a fragment or component shows up with pretty much anything else + // + // This likely isn't the fastest way to go about replacing one node with a virtual node, but the "insane" cases + // are pretty rare. IE replacing a list (component or fragment) with a single node. ( VNodeKind::Component(_) | VNodeKind::Fragment(_) @@ -242,12 +259,12 @@ where | VNodeKind::Element(_), ) => { // Choose the node to use as the placeholder for replacewith - let back_node = match old_node.kind { + let back_node_id = match old_node.kind { // We special case these two types to avoid allocating the small-vecs - VNodeKind::Element(_) | VNodeKind::Text(_) => old_node.dom_id.get(), + VNodeKind::Element(_) | VNodeKind::Text(_) => root, _ => { - let mut old_iter = RealChildIterator::new(old_node, &self.components); + let mut old_iter = RealChildIterator::new(old_node, &self.vdom); let back_node = old_iter .next() @@ -255,7 +272,7 @@ where // remove any leftovers for to_remove in old_iter { - self.edits.push(to_remove); + self.edits.push_root(to_remove); self.edits.remove(); } @@ -264,7 +281,7 @@ where }; // replace the placeholder or first node with the nodes generated from the "new" - self.edits.push(back_node); + self.edits.push_root(back_node_id); let meta = self.create(new_node); self.edits.replace_with(meta.added_to_stack); @@ -314,9 +331,10 @@ where log::warn!("Creating node! ... {:#?}", node); match &node.kind { VNodeKind::Text(text) => { - let real_id = self.dom.request_available_node(); + let real_id = self.vdom.reserve_node(); self.edits.create_text_node(text.text, real_id); - node.dom_id.set(real_id); + node.dom_id.set(Some(real_id)); + CreateMeta::new(text.is_static, 1) } VNodeKind::Element(el) => { @@ -338,18 +356,18 @@ where static_listeners: _, } = el; - let real_id = self.dom.request_available_node(); + let real_id = self.vdom.reserve_node(); if let Some(namespace) = namespace { self.edits .create_element(tag_name, Some(namespace), real_id) } else { self.edits.create_element(tag_name, None, real_id) }; - node.dom_id.set(real_id); + node.dom_id.set(Some(real_id)); listeners.iter().enumerate().for_each(|(idx, listener)| { log::info!("setting listener id to {:#?}", real_id); - listener.mounted_node.set(real_id); + listener.mounted_node.set(Some(real_id)); self.edits .new_event_listener(listener.event, listener.scope, idx, real_id); @@ -405,25 +423,18 @@ where let parent_idx = self.cur_idxs.last().unwrap().clone(); // Insert a new scope into our component list - let new_idx = self - .components - .with(|components| { - components.insert_with_key(|new_idx| { - let parent_scope = self.components.get(parent_idx).unwrap(); - let height = parent_scope.height + 1; - Scope::new( - caller, - new_idx, - Some(parent_idx), - height, - self.event_queue.new_channel(height, new_idx), - self.components.clone(), - vcomponent.children, - self.task_queue.new_submitter(), - ) - }) - }) - .unwrap(); + let new_idx = self.vdom.insert_scope_with_key(|new_idx| { + let parent_scope = self.get_scope(&parent_idx).unwrap(); + let height = parent_scope.height + 1; + Scope::new( + caller, + new_idx, + Some(parent_idx), + height, + vcomponent.children, + self.vdom.clone(), + ) + }); // This code is supposed to insert the new idx into the parent's descendent list, but it doesn't really work. // This is mostly used for cleanup - to remove old scopes when components are destroyed. @@ -437,7 +448,7 @@ where // .insert(idx); // TODO: abstract this unsafe into the arena abstraction - let inner: &'bump mut _ = unsafe { &mut *self.components.components.get() }; + let inner: &'bump mut _ = unsafe { &mut *self.vdom.components.get() }; let new_component = inner.get_mut(new_idx).unwrap(); // Actually initialize the caller's slot with the right address @@ -477,10 +488,10 @@ where } VNodeKind::Suspended { node: real_node } => { - let id = self.dom.request_available_node(); + let id = self.vdom.reserve_node(); self.edits.create_placeholder(id); - node.dom_id.set(id); - real_node.set(id); + node.dom_id.set(Some(id)); + real_node.set(Some(id)); CreateMeta::new(false, 1) } } @@ -500,7 +511,7 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { while let Some(scope_id) = scopes_to_explore.pop() { // If we're planning on deleting this node, then we don't need to both rendering it self.seen_nodes.insert(scope_id); - let scope = self.components.get(scope_id).unwrap(); + let scope = self.get_scope(&scope_id).unwrap(); for child in scope.descendents.borrow().iter() { // Add this node to be explored scopes_to_explore.push(child.clone()); @@ -513,7 +524,7 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // Delete all scopes that we found as part of this subtree for node in nodes_to_delete { log::debug!("Removing scope {:#?}", node); - let _scope = self.components.try_remove(node).unwrap(); + let _scope = self.vdom.try_remove(node).unwrap(); // do anything we need to do to delete the scope // I think we need to run the destructors on the hooks // TODO @@ -527,7 +538,7 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // [... node] // // The change list stack is left unchanged. - fn diff_listeners(&mut self, old: &[Listener<'_>], new: &[Listener<'_>]) { + fn diff_listeners(&mut self, old: &[&mut Listener<'_>], new: &[&mut Listener<'_>]) { if !old.is_empty() || !new.is_empty() { // self.edits.commit_traversal(); } @@ -547,7 +558,7 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { new_l.mounted_node.set(old_l.mounted_node.get()); // if new_l.id != old_l.id { // self.edits.remove_event_listener(event_type); - // // TODO! we need to mess with events and assign them by RealDomNode + // // TODO! we need to mess with events and assign them by ElementId // // self.edits // // .update_event_listener(event_type, new_l.scope, new_l.id) // } @@ -1270,7 +1281,7 @@ enum KeyedPrefixResult { /// This iterator is useful when it's important to load the next real root onto the top of the stack for operations like /// "InsertBefore". struct RealChildIterator<'a> { - scopes: &'a SharedArena, + scopes: &'a SharedResources, // Heuristcally we should never bleed into 4 completely nested fragments/components // Smallvec lets us stack allocate our little stack machine so the vast majority of cases are sane @@ -1279,7 +1290,7 @@ struct RealChildIterator<'a> { } impl<'a> RealChildIterator<'a> { - fn new(starter: &'a VNode<'a>, scopes: &'a SharedArena) -> Self { + fn new(starter: &'a VNode<'a>, scopes: &'a SharedResources) -> Self { Self { scopes, stack: smallvec::smallvec![(0, starter)], @@ -1288,9 +1299,9 @@ impl<'a> RealChildIterator<'a> { } impl<'a> Iterator for RealChildIterator<'a> { - type Item = RealDomNode; + type Item = ElementId; - fn next(&mut self) -> Option { + fn next(&mut self) -> Option { let mut should_pop = false; let mut returned_node = None; let mut should_push = None; @@ -1304,7 +1315,7 @@ impl<'a> Iterator for RealChildIterator<'a> { // We've recursed INTO an element/text // We need to recurse *out* of it and move forward to the next should_pop = true; - returned_node = Some(node.dom_id.get()); + returned_node = node.dom_id.get(); } // If we get a fragment we push the next child @@ -1313,7 +1324,7 @@ impl<'a> Iterator for RealChildIterator<'a> { if frag.children.len() == 0 { should_pop = true; - returned_node = Some(node.dom_id.get()); + returned_node = node.dom_id.get(); } if subcount >= frag.children.len() { @@ -1325,11 +1336,16 @@ impl<'a> Iterator for RealChildIterator<'a> { // Immediately abort suspended nodes - can't do anything with them yet // VNodeKind::Suspended => should_pop = true, - VNodeKind::Suspended { .. } => todo!(), + VNodeKind::Suspended { node, .. } => { + todo!() + // *node = node.as_ref().borrow().get().expect("msg"); + } // For components, we load their root and push them onto the stack VNodeKind::Component(sc) => { - let scope = self.scopes.get(sc.ass_scope.get().unwrap()).unwrap(); + let scope = + unsafe { self.scopes.get_scope(sc.ass_scope.get().unwrap()) }.unwrap(); + // let scope = self.scopes.get(sc.ass_scope.get().unwrap()).unwrap(); // Simply swap the current node on the stack with the root of the component *node = scope.root(); diff --git a/packages/core/src/editor.rs b/packages/core/src/editor.rs index c443f884c..5e8a685c8 100644 --- a/packages/core/src/editor.rs +++ b/packages/core/src/editor.rs @@ -5,7 +5,7 @@ //! //! -use crate::{innerlude::ScopeId, RealDomNode}; +use crate::{innerlude::ScopeId, ElementId}; /// The `DomEditor` provides an imperative interface for the Diffing algorithm to plan out its changes. /// @@ -24,7 +24,7 @@ impl<'real, 'bump> DomEditor<'real, 'bump> { } // Navigation - pub(crate) fn push(&mut self, root: RealDomNode) { + pub(crate) fn push_root(&mut self, root: ElementId) { let id = root.as_u64(); self.edits.push(PushRoot { id }); } @@ -61,7 +61,7 @@ impl<'real, 'bump> DomEditor<'real, 'bump> { // Create #[inline] - pub(crate) fn create_text_node(&mut self, text: &'bump str, id: RealDomNode) { + pub(crate) fn create_text_node(&mut self, text: &'bump str, id: ElementId) { let id = id.as_u64(); self.edits.push(CreateTextNode { text, id }); } @@ -70,7 +70,7 @@ impl<'real, 'bump> DomEditor<'real, 'bump> { &mut self, tag: &'static str, ns: Option<&'static str>, - id: RealDomNode, + id: ElementId, ) { let id = id.as_u64(); match ns { @@ -80,7 +80,7 @@ impl<'real, 'bump> DomEditor<'real, 'bump> { } // placeholders are nodes that don't get rendered but still exist as an "anchor" in the real dom - pub(crate) fn create_placeholder(&mut self, id: RealDomNode) { + pub(crate) fn create_placeholder(&mut self, id: ElementId) { let id = id.as_u64(); self.edits.push(CreatePlaceholder { id }); } @@ -91,7 +91,7 @@ impl<'real, 'bump> DomEditor<'real, 'bump> { event: &'static str, scope: ScopeId, element_id: usize, - realnode: RealDomNode, + realnode: ElementId, ) { self.edits.push(NewEventListener { scope, diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 66cc0bf5e..9cd6d2c0b 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -6,7 +6,7 @@ use std::{cell::Cell, rc::Rc}; -use crate::innerlude::{RealDomNode, ScopeId}; +use crate::innerlude::{ElementId, HeightMarker, ScopeId}; #[derive(Debug)] pub struct EventTrigger { @@ -14,7 +14,7 @@ pub struct EventTrigger { pub originator: ScopeId, /// The optional real node associated with the trigger - pub real_node_id: Option, + pub real_node_id: Option, /// The type of event pub event: VirtualEvent, @@ -72,7 +72,7 @@ impl EventTrigger { pub fn new( event: VirtualEvent, scope: ScopeId, - mounted_dom_id: Option, + mounted_dom_id: Option, priority: EventPriority, ) -> Self { Self { @@ -105,6 +105,9 @@ pub enum VirtualEvent { // image event has conflicting method types // ImageEvent(event_data::ImageEvent), + SetStateEvent { + height: HeightMarker, + }, // Whenever a task is ready (complete) Dioxus produces this "AsyncEvent" // @@ -117,10 +120,8 @@ pub enum VirtualEvent { // These are more intrusive than the rest SuspenseEvent { hook_idx: usize, - domnode: Rc>, + domnode: Rc>>, }, - - OtherEvent, } pub mod on { @@ -138,7 +139,7 @@ pub mod on { use crate::{ innerlude::NodeFactory, - innerlude::{Attribute, Listener, RealDomNode, VNode}, + innerlude::{Attribute, ElementId, Listener, VNode}, }; use std::cell::Cell; @@ -180,8 +181,8 @@ pub mod on { let bump = &c.bump(); Listener { event: stringify!($name), - mounted_node: bump.alloc(Cell::new(RealDomNode::empty())), - scope: c.scope_ref.our_arena_idx, + mounted_node: Cell::new(None), + scope: c.scope.our_arena_idx, callback: bump.alloc(move |evt: VirtualEvent| match evt { VirtualEvent::$wrapper(event) => callback(event), _ => unreachable!("Downcasted VirtualEvent to wrong event type - this is an internal bug!") diff --git a/packages/core/src/heuristics.rs b/packages/core/src/heuristics.rs index 80c788359..366c29d85 100644 --- a/packages/core/src/heuristics.rs +++ b/packages/core/src/heuristics.rs @@ -1,14 +1,23 @@ use std::collections::HashMap; +use fxhash::FxHashMap; + use crate::FC; -pub(crate) struct HeuristicsEngine { - heuristics: HashMap, +/// Provides heuristics to the "SharedResources" object for improving allocation performance. +/// +/// This heueristic engine records the memory footprint of bump arenas and hook lists for each component. These records are +/// then used later on to optimize the initial allocation for future components. This helps save large allocations later on +/// that would slow down the diffing and initializion process. +/// +/// +pub struct HeuristicsEngine { + heuristics: FxHashMap, } -pub(crate) type FcSlot = *const (); +pub type FcSlot = *const (); -pub(crate) struct Heuristic { +pub struct Heuristic { hooks: u32, bump_size: u64, } @@ -16,7 +25,7 @@ pub(crate) struct Heuristic { impl HeuristicsEngine { pub(crate) fn new() -> Self { Self { - heuristics: HashMap::new(), + heuristics: FxHashMap::default(), } } diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 6a2fab33f..5d36787e2 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -10,8 +10,8 @@ //! pub use crate::innerlude::{ - format_args_f, html, rsx, Context, DioxusElement, DomEdit, DomTree, EventTrigger, LazyNodes, - NodeFactory, Properties, RealDom, RealDomNode, ScopeId, VNode, VNodeKind, VirtualDom, + format_args_f, html, rsx, Context, DioxusElement, DomEdit, DomTree, ElementId, EventTrigger, + LazyNodes, NodeFactory, Properties, RealDom, ScopeId, VNode, VNodeKind, VirtualDom, VirtualEvent, FC, }; @@ -38,7 +38,6 @@ pub(crate) mod innerlude { pub use crate::hooklist::*; pub use crate::nodes::*; pub use crate::scope::*; - pub use crate::tasks::*; pub use crate::util::*; pub use crate::virtual_dom::*; @@ -66,6 +65,5 @@ pub mod hooklist; pub mod nodes; pub mod scope; pub mod signals; -pub mod tasks; pub mod util; pub mod virtual_dom; diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index d6db0a637..a9e2ea9cc 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -5,7 +5,7 @@ //! These VNodes should be *very* cheap and *very* fast to construct - building a full tree should be insanely quick. use crate::{ events::VirtualEvent, - innerlude::{Context, DomTree, Properties, RealDomNode, Scope, ScopeId, FC}, + innerlude::{empty_cell, Context, DomTree, ElementId, Properties, Scope, ScopeId, FC}, }; use std::{ cell::Cell, @@ -16,7 +16,12 @@ use std::{ pub struct VNode<'src> { pub kind: VNodeKind<'src>, - pub(crate) dom_id: Cell, + + /// + /// ElementId supports NonZero32 and Cell is zero cost, so the size of this field is unaffected + /// + /// + pub(crate) dom_id: Cell>, pub(crate) key: Option<&'src str>, } impl VNode<'_> { @@ -39,7 +44,7 @@ pub enum VNodeKind<'src> { Component(&'src VComponent<'src>), - Suspended { node: Rc> }, + Suspended { node: Rc>> }, } pub struct VText<'src> { @@ -50,7 +55,6 @@ pub struct VText<'src> { pub struct VFragment<'src> { pub children: &'src [VNode<'src>], pub is_static: bool, - pub is_error: bool, } pub trait DioxusElement { @@ -72,7 +76,7 @@ pub struct VElement<'a> { pub namespace: Option<&'static str>, pub static_listeners: bool, - pub listeners: &'a [Listener<'a>], + pub listeners: &'a [&'a mut Listener<'a>], pub static_attrs: bool, pub attributes: &'a [Attribute<'a>], @@ -105,9 +109,9 @@ pub struct Listener<'bump> { pub scope: ScopeId, - pub mounted_node: &'bump mut Cell, + pub mounted_node: Cell>, - pub(crate) callback: &'bump dyn FnMut(VirtualEvent), + pub(crate) callback: &'bump mut dyn FnMut(VirtualEvent), } /// Virtual Components for custom user-defined components @@ -136,20 +140,31 @@ pub struct VComponent<'src> { /// This struct adds metadata to the final VNode about listeners, attributes, and children #[derive(Copy, Clone)] pub struct NodeFactory<'a> { - pub scope_ref: &'a Scope, + pub scope: &'a Scope, pub listener_id: &'a Cell, } impl<'a> NodeFactory<'a> { #[inline] pub fn bump(&self) -> &'a bumpalo::Bump { - &self.scope_ref.cur_frame().bump + &self.scope.cur_frame().bump + } + + pub fn unstable_place_holder() -> VNode<'static> { + VNode { + dom_id: empty_cell(), + key: None, + kind: VNodeKind::Text(VText { + text: "", + is_static: true, + }), + } } /// Used in a place or two to make it easier to build vnodes from dummy text - pub fn static_text(text: &'static str) -> VNode { + pub fn static_text(&self, text: &'static str) -> VNode<'a> { VNode { - dom_id: RealDomNode::empty_cell(), + dom_id: empty_cell(), key: None, kind: VNodeKind::Text(VText { text, @@ -178,7 +193,7 @@ impl<'a> NodeFactory<'a> { pub fn text(&self, args: Arguments) -> VNode<'a> { let (text, is_static) = self.raw_text(args); VNode { - dom_id: RealDomNode::empty_cell(), + dom_id: empty_cell(), key: None, kind: VNodeKind::Text(VText { text, is_static }), } @@ -187,7 +202,7 @@ impl<'a> NodeFactory<'a> { pub fn element( &self, el: impl DioxusElement, - listeners: &'a mut [Listener<'a>], + listeners: &'a mut [&'a mut Listener<'a>], attributes: &'a [Attribute<'a>], children: &'a [VNode<'a>], key: Option<&'a str>, @@ -206,7 +221,7 @@ impl<'a> NodeFactory<'a> { &self, tag: &'static str, namespace: Option<&'static str>, - listeners: &'a mut [Listener], + listeners: &'a mut [&'a mut Listener<'a>], attributes: &'a [Attribute], children: &'a [VNode<'a>], key: Option<&'a str>, @@ -215,15 +230,17 @@ impl<'a> NodeFactory<'a> { // TODO: this code shouldn't necessarily be here of all places // It would make more sense to do this in diffing - let mut queue = self.scope_ref.listeners.borrow_mut(); + let mut queue = self.scope.listeners.borrow_mut(); for listener in listeners.iter_mut() { - let mounted = listener.mounted_node as *mut _; - let callback = listener.callback as *const _ as *mut _; - queue.push((mounted, callback)) + let raw_listener = &mut **listener; + + let long_listener: &mut Listener<'static> = + unsafe { std::mem::transmute(raw_listener) }; + queue.push(long_listener as *mut _) } VNode { - dom_id: RealDomNode::empty_cell(), + dom_id: empty_cell(), key, kind: VNodeKind::Element(self.bump().alloc(VElement { tag_name: tag, @@ -242,10 +259,10 @@ impl<'a> NodeFactory<'a> { pub fn suspended() -> VNode<'static> { VNode { - dom_id: RealDomNode::empty_cell(), + dom_id: empty_cell(), key: None, kind: VNodeKind::Suspended { - node: Rc::new(RealDomNode::empty_cell()), + node: Rc::new(empty_cell()), }, } } @@ -322,7 +339,7 @@ impl<'a> NodeFactory<'a> { VNode { key, - dom_id: Cell::new(RealDomNode::empty()), + dom_id: empty_cell(), kind: VNodeKind::Component(self.bump().alloc_with(|| VComponent { user_fc, comparator, @@ -383,12 +400,11 @@ To help you identify where this error is coming from, we've generated a backtrac } } VNode { - dom_id: RealDomNode::empty_cell(), + dom_id: empty_cell(), key: None, kind: VNodeKind::Fragment(VFragment { children: nodes.into_bump_slice(), is_static: false, - is_error: false, }), } } @@ -444,7 +460,6 @@ impl<'a> IntoVNode<'a> for &VNode<'a> { VNodeKind::Fragment(fragment) => VNodeKind::Fragment(VFragment { children: fragment.children, is_static: fragment.is_static, - is_error: false, }), VNodeKind::Component(component) => VNodeKind::Component(component), @@ -535,7 +550,7 @@ impl<'a> IntoVNode<'a> for Option> { impl IntoVNode<'_> for &'static str { fn into_vnode<'a>(self, cx: NodeFactory<'a>) -> VNode<'a> { - NodeFactory::static_text(self) + cx.static_text(self) } } impl IntoVNode<'_> for Arguments<'_> { diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index 1c6dd4244..01a52fc7b 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -18,40 +18,30 @@ use std::{ /// We expose the `Scope` type so downstream users can traverse the Dioxus VirtualDOM for whatever /// usecase they might have. pub struct Scope { - // Book-keeping about the arena + // Book-keeping about our spot in the arena pub(crate) parent_idx: Option, - pub(crate) descendents: RefCell>, pub(crate) our_arena_idx: ScopeId, pub(crate) height: u32, + pub(crate) descendents: RefCell>, // Nodes // an internal, highly efficient storage of vnodes pub(crate) frames: ActiveFrame, - pub(crate) child_nodes: &'static [VNode<'static>], pub(crate) caller: Rc, + pub(crate) child_nodes: &'static [VNode<'static>], // Listeners - pub(crate) listeners: RefCell, *mut dyn FnMut(VirtualEvent))>>, + pub(crate) listeners: RefCell>>, pub(crate) listener_idx: Cell, // State pub(crate) hooks: HookList, pub(crate) shared_contexts: RefCell>>, - // Events - pub(crate) event_channel: Rc, - - // Tasks - pub(crate) task_submitter: TaskSubmitter, - - // A reference to the list of components. - // This lets us traverse the component list whenever we need to access our parent or children. - pub(crate) arena_link: SharedArena, + // A reference to the resources shared by all the comonents + pub(crate) vdom: SharedResources, } -// The type of the channel function -type EventChannel = Rc; - // The type of closure that wraps calling components pub type WrappedCaller = dyn for<'b> Fn(&'b Scope) -> DomTree<'b>; @@ -68,13 +58,16 @@ impl Scope { // Therefore, their lifetimes are connected exclusively to the virtual dom pub fn new<'creator_node>( caller: Rc, + arena_idx: ScopeId, + parent: Option, + height: u32, - event_channel: EventChannel, - arena_link: SharedArena, + child_nodes: &'creator_node [VNode<'creator_node>], - task_submitter: TaskSubmitter, + + vdom: SharedResources, ) -> Self { let child_nodes = unsafe { std::mem::transmute(child_nodes) }; Self { @@ -83,9 +76,7 @@ impl Scope { parent_idx: parent, our_arena_idx: arena_idx, height, - event_channel, - arena_link, - task_submitter, + vdom, listener_idx: Default::default(), frames: ActiveFrame::new(), hooks: Default::default(), @@ -113,7 +104,7 @@ impl Scope { // Remove all the outdated listeners // This is a very dangerous operation - let next_frame = self.frames.old_frame_mut(); + let next_frame = self.frames.prev_frame_mut(); next_frame.bump.reset(); self.listeners.borrow_mut().clear(); @@ -132,7 +123,7 @@ impl Scope { } Some(new_head) => { // the user's component succeeded. We can safely cycle to the next frame - self.frames.old_frame_mut().head_node = unsafe { std::mem::transmute(new_head) }; + self.frames.prev_frame_mut().head_node = unsafe { std::mem::transmute(new_head) }; self.frames.cycle_frame(); Ok(()) } @@ -162,33 +153,27 @@ impl Scope { let listners = self.listeners.borrow_mut(); - let raw_listener = listners.iter().find(|(domptr, _)| { - let search = unsafe { &**domptr }; - let search_id = search.get(); + let raw_listener = listners.iter().find(|lis| { + let search = unsafe { &mut ***lis }; + let search_id = search.mounted_node.get(); log::info!("searching listener {:#?}", search_id); - match real_node_id { - Some(e) => search_id == e, - None => false, + + match (real_node_id, search_id) { + (Some(e), Some(search_id)) => search_id == e, + _ => false, } }); - match raw_listener { - Some((_node, listener)) => unsafe { - // TODO: Don'tdo a linear scan! Do a hashmap lookup! It'll be faster! - let listener_fn = &mut **listener; - listener_fn(event); - }, - None => todo!(), + if let Some(raw_listener) = raw_listener { + let listener = unsafe { &mut **raw_listener }; + (listener.callback)(event); + } else { + log::warn!("An event was triggered but there was no listener to handle it"); } Ok(()) } - pub(crate) fn submit_task(&self, task: FiberTask) { - log::debug!("Task submitted into scope"); - (self.task_submitter)(task); - } - #[inline] pub(crate) fn next_frame<'bump>(&'bump self) -> &'bump VNode<'bump> { self.frames.current_head_node() @@ -207,18 +192,10 @@ impl Scope { /// Get the root VNode of this component #[inline] pub fn root<'a>(&'a self) -> &'a VNode<'a> { - &self.frames.cur_frame().head_node + &self.frames.current_head_node() } -} - -pub fn errored_fragment() -> VNode<'static> { - VNode { - dom_id: RealDomNode::empty_cell(), - key: None, - kind: VNodeKind::Fragment(VFragment { - children: &[], - is_static: false, - is_error: true, - }), + #[inline] + pub fn child_nodes<'a>(&'a self) -> &'a [VNode<'a>] { + unsafe { std::mem::transmute(self.child_nodes) } } } diff --git a/packages/core/src/tasks.rs b/packages/core/src/tasks.rs deleted file mode 100644 index 875dc99ee..000000000 --- a/packages/core/src/tasks.rs +++ /dev/null @@ -1,137 +0,0 @@ -//! The TaskQueue serves as a centralized async store for all tasks in Dioxus. -//! When a component renders, it may submit an async task to the queue. -//! -//! Then the task complete, it is emitted from the virtual dom in the event loop, which is then fed back into the virtualdom -//! as an event trigger. -//! -//! When a component is scheduled to re-render, the awaing task must be dumped from the queue. -//! -//! This is all pretty unsafe stuff. -//! The major invariant here is that tasks that enter the queue may be invalidated during transitions. - -use std::{ - cell::Cell, - sync::{Arc, RwLock}, -}; - -use futures_util::{stream::FuturesUnordered, Future, Stream, StreamExt}; -use slotmap::{DefaultKey, SlotMap}; - -use crate::innerlude::{EventTrigger, FiberTask, ScopeId}; - -pub type TaskSubmitter = Arc; - -pub struct TaskQueue { - slots: Arc>>, - // slots: Arc>>, - submitter: TaskSubmitter, -} - -impl TaskQueue { - pub fn new() -> Self { - let slots = Arc::new(RwLock::new(FuturesUnordered::new())); - let slots2 = slots.clone(); - - let submitter = Arc::new(move |task| { - let mut slots = slots2.write().unwrap(); - log::debug!("Task submitted into global task queue"); - slots.push(task); - }); - Self { slots, submitter } - } - - pub fn new_submitter(&self) -> TaskSubmitter { - self.submitter.clone() - } - - pub fn submit_task(&mut self, task: FiberTask) { - self.slots.write().unwrap().push(task); - // TaskHandle { key } - } - - pub fn is_empty(&self) -> bool { - self.slots.read().unwrap().is_empty() - } - pub fn len(&self) -> usize { - self.slots.read().unwrap().len() - } - - pub async fn next(&mut self) -> Option { - let mut slots = self.slots.write().unwrap(); - slots.next().await - } -} - -// impl Stream for TaskQueue { -// type Item = EventTrigger; - -// /// We can never be finished polling -// fn poll_next( -// self: Pin<&mut Self>, -// cx: &mut std::task::Context<'_>, -// ) -> std::task::Poll> { -// // let yield_every = self.len(); -// // let mut polled = 0; - -// let mut slots = self.slots.write().unwrap(); -// for (_key, slot) in slots.iter_mut() { -// if slot.dead.get() { -// continue; -// } -// let r = slot.fut; -// // let fut = unsafe { &mut *r }; -// // use futures::{future::Future, poll, FutureExt}; - -// let f2 = fut.as_mut(); -// let w = cx.waker(); -// let mut cx = Context::from_waker(&w); - -// // Pin::new_unchecked(pointer) -// // use std::future::Future; -// match f2.poll(&mut cx) { -// Poll::Ready(_) => { -// let trigger = EventTrigger::new_from_task(slot.originator); -// slot.dead.set(true); -// return Poll::Ready(Some(trigger)); -// } -// Poll::Pending => continue, -// } -// } - -// // we tried polling every active task. -// // give up and relinquish controlto the parent - -// // We have polled a large number of futures in a row without yielding. -// // To ensure we do not starve other tasks waiting on the executor, -// // we yield here, but immediately wake ourselves up to continue. -// // cx.waker().wake_by_ref(); -// return Poll::Pending; -// } -// } - -pub struct TaskHandle { - key: DefaultKey, -} - -pub struct DTask { - fut: FiberTask, - originator: ScopeId, - dead: Cell, -} -impl DTask { - pub fn new(fut: FiberTask, originator: ScopeId) -> Self { - Self { - fut, - originator, - dead: Cell::new(false), - } - } - pub fn debug_new(fut: FiberTask) -> Self { - let originator = ScopeId::default(); - Self { - fut, - originator, - dead: Cell::new(false), - } - } -} diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs index c357651c4..b4409a813 100644 --- a/packages/core/src/util.rs +++ b/packages/core/src/util.rs @@ -1,35 +1,11 @@ -use std::{ - cell::{Cell, RefCell, RefMut}, - rc::Rc, -}; - -use futures_util::StreamExt; -use slotmap::{DefaultKey, Key, KeyData}; +use std::cell::Cell; use crate::innerlude::*; -#[derive(PartialEq, Debug, Clone, Default)] -pub struct EventQueue { - pub queue: Rc>>, -} - -impl EventQueue { - pub fn new_channel(&self, height: u32, idx: ScopeId) -> Rc { - let inner = self.clone(); - let marker = HeightMarker { height, idx }; - Rc::new(move || { - log::debug!("channel updated {:#?}", marker); - inner.queue.as_ref().borrow_mut().push(marker) - }) - } - - pub fn sort_unstable(&self) { - self.queue.borrow_mut().sort_unstable() - } - - pub fn borrow_mut(&self) -> RefMut> { - self.queue.borrow_mut() - } +// create a cell with a "none" value +#[inline] +pub fn empty_cell() -> Cell> { + Cell::new(None as Option) } /// A helper type that lets scopes be ordered by their height @@ -51,41 +27,10 @@ impl PartialOrd for HeightMarker { } } -/// The `RealDomNode` is an ID handle that corresponds to a foreign DOM node. -/// -/// "u64" was chosen for two reasons -/// - 0 cost hashing -/// - use with slotmap and other versioned slot arenas - -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct RealDomNode(pub u64); -impl RealDomNode { - #[inline] - pub fn empty() -> Self { - Self(u64::MIN) - } - #[inline] - pub fn empty_cell() -> Cell { - Cell::new(Self::empty()) - } - #[inline] - pub fn from_u64(id: u64) -> Self { - Self(id) - } - - #[inline] - pub fn as_u64(&self) -> u64 { - self.0 - } -} - -pub struct DebugDom { - counter: u64, -} +pub struct DebugDom {} impl DebugDom { pub fn new() -> Self { - Self { counter: 0 } + Self {} } } @@ -93,9 +38,4 @@ impl<'a> RealDom<'a> for DebugDom { fn raw_node_as_any(&self) -> &mut dyn std::any::Any { todo!() } - - fn request_available_node(&mut self) -> RealDomNode { - self.counter += 1; - RealDomNode::from_u64(self.counter) - } } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index db7a26a92..a59705547 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -19,8 +19,7 @@ //! This module includes just the barebones for a complete VirtualDOM API. //! Additional functionality is defined in the respective files. -use crate::tasks::TaskQueue; -use crate::{arena::SharedArena, innerlude::*}; +use crate::{arena::SharedResources, innerlude::*}; use slotmap::DefaultKey; use slotmap::SlotMap; @@ -30,10 +29,6 @@ use std::any::TypeId; use std::cell::RefCell; use std::pin::Pin; -slotmap::new_key_type! { - pub struct ScopeId; -} - /// An integrated virtual node system that progresses events and diffs UI trees. /// Differences are converted into patches which a renderer can use to draw the UI. /// @@ -49,7 +44,7 @@ pub struct VirtualDom { /// /// This is wrapped in an UnsafeCell because we will need to get mutable access to unique values in unique bump arenas /// and rusts's guartnees cannot prove that this is safe. We will need to maintain the safety guarantees manually. - pub components: SharedArena, + pub shared: SharedResources, /// The index of the root component /// Should always be the first (gen=0, id=0) @@ -57,21 +52,12 @@ pub struct VirtualDom { pub triggers: RefCell>, - /// All components dump their updates into a queue to be processed - pub event_queue: EventQueue, - - pub tasks: TaskQueue, - - heuristics: HeuristicsEngine, - - root_props: std::pin::Pin>, - - /// Type of the original props. This is stored as TypeId so VirtualDom does not need to be generic. - /// - /// Whenver props need to be updated, an Error will be thrown if the new props do not - /// match the props used to create the VirtualDom. + // for managing the props that were used to create the dom #[doc(hidden)] _root_prop_type: std::any::TypeId, + + #[doc(hidden)] + _root_props: std::pin::Pin>, } // ====================================== @@ -142,37 +128,22 @@ impl VirtualDom { /// let dom = VirtualDom::new(Example); /// ``` pub fn new_with_props(root: FC

, root_props: P) -> Self { - let components = SharedArena::new(SlotMap::::with_key()); + let components = SharedResources::new(); let root_props: Pin> = Box::pin(root_props); let props_ptr = root_props.as_ref().downcast_ref::

().unwrap() as *const P; - // Build a funnel for hooks to send their updates into. The `use_hook` method will call into the update funnel. - let event_queue = EventQueue::default(); - let _event_queue = event_queue.clone(); - let link = components.clone(); - let tasks = TaskQueue::new(); - let submitter = tasks.new_submitter(); - - let base_scope = components - .with(|arena| { - arena.insert_with_key(move |myidx| { - let event_channel = _event_queue.new_channel(0, myidx); - let caller = NodeFactory::create_component_caller(root, props_ptr as *const _); - Scope::new(caller, myidx, None, 0, event_channel, link, &[], submitter) - }) - }) - .unwrap(); + let base_scope = components.insert_scope_with_key(move |myidx| { + let caller = NodeFactory::create_component_caller(root, props_ptr as *const _); + Scope::new(caller, myidx, None, 0, &[], link) + }); Self { base_scope, - event_queue, - components, - root_props, - tasks, - heuristics: HeuristicsEngine::new(), + _root_props: root_props, + shared: components, triggers: Default::default(), _root_prop_type: TypeId::of::

(), } @@ -209,29 +180,28 @@ impl VirtualDom { /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom rom scratch /// - /// Currently this doesn't do what we want it to do - /// /// The diff machine expects the RealDom's stack to be the root of the application pub fn rebuild<'s, Dom: RealDom<'s>>( &'s mut self, realdom: &mut Dom, edits: &mut Vec>, ) -> Result<()> { - let mut diff_machine = DiffMachine::new( - edits, - realdom, - &self.components, - self.base_scope, - self.event_queue.clone(), - &self.tasks, - ); + let mut diff_machine = DiffMachine::new(edits, realdom, self.base_scope, &self.shared); - let cur_component = self.components.get_mut(self.base_scope).unwrap(); + let cur_component = diff_machine + .get_scope_mut(&self.base_scope) + .expect("The base scope should never be moved"); // We run the component. If it succeeds, then we can diff it and add the changes to the dom. if cur_component.run_scope().is_ok() { let meta = diff_machine.create(cur_component.next_frame()); diff_machine.edits.append_children(meta.added_to_stack); + } else { + // todo: should this be a hard error? + log::warn!( + "Component failed to run succesfully during rebuild. + This does not result in a failed rebuild, but indicates a logic failure within your app." + ); } Ok(()) @@ -242,10 +212,9 @@ impl VirtualDom { /// /// /// - pub fn queue_event(&self, trigger: EventTrigger) -> Result<()> { + pub fn queue_event(&self, trigger: EventTrigger) { let mut triggers = self.triggers.borrow_mut(); triggers.push(trigger); - Ok(()) } /// This method is the most sophisticated way of updating the virtual dom after an external event has been triggered. @@ -298,26 +267,18 @@ impl VirtualDom { ) -> Result<()> { let trigger = self.triggers.borrow_mut().pop().expect("failed"); - let mut diff_machine = DiffMachine::new( - edits, - realdom, - &self.components, - trigger.originator, - self.event_queue.clone(), - &self.tasks, - ); + let mut diff_machine = DiffMachine::new(edits, realdom, trigger.originator, &self.shared); match &trigger.event { - VirtualEvent::OtherEvent => todo!(), - // Nothing yet VirtualEvent::AsyncEvent { .. } => {} // Suspense Events! A component's suspended node is updated VirtualEvent::SuspenseEvent { hook_idx, domnode } => { - let scope = self.components.get_mut(trigger.originator).unwrap(); + // Safety: this handler is the only thing that can mutate shared items at this moment in tim + let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); - // safety: we are sure that there are no other references to the inner content of this hook + // safety: we are sure that there are no other references to the inner content of suspense hooks let hook = unsafe { scope.hooks.get_mut::(*hook_idx) }.unwrap(); let cx = Context { scope, props: &() }; @@ -325,17 +286,24 @@ impl VirtualDom { // generate the new node! let nodes: Option> = (&hook.callback)(scx); - let nodes = nodes.unwrap_or_else(|| errored_fragment()); - let nodes = scope.cur_frame().bump.alloc(nodes); + match nodes { + None => { + log::warn!("Suspense event came through, but there was no mounted node to update >:("); + } + Some(nodes) => { + let nodes = scope.cur_frame().bump.alloc(nodes); - // push the old node's root onto the stack - diff_machine.edits.push(domnode.get()); + // push the old node's root onto the stack + let real_id = domnode.get().ok_or(Error::NotMounted)?; + diff_machine.edits.push_root(real_id); - // push these new nodes onto the diff machines stack - let meta = diff_machine.create(&*nodes); + // push these new nodes onto the diff machines stack + let meta = diff_machine.create(&*nodes); - // replace the placeholder with the new nodes we just pushed on the stack - diff_machine.edits.replace_with(meta.added_to_stack); + // replace the placeholder with the new nodes we just pushed on the stack + diff_machine.edits.replace_with(meta.added_to_stack); + } + } } // This is the "meat" of our cooperative scheduler @@ -343,12 +311,12 @@ impl VirtualDom { // // We use the reconciler to request new IDs and then commit/uncommit the IDs when the scheduler is finished _ => { - self.components - .get_mut(trigger.originator) + diff_machine + .get_scope_mut(&trigger.originator) .map(|f| f.call_listener(trigger)); // Now, there are events in the queue - let mut updates = self.event_queue.queue.as_ref().borrow_mut(); + let mut updates = self.shared.borrow_queue(); // Order the nodes by their height, we want the nodes with the smallest depth on top // This prevents us from running the same component multiple times @@ -370,8 +338,10 @@ impl VirtualDom { diff_machine.seen_nodes.insert(update.idx.clone()); // Start a new mutable borrow to components - // We are guaranteeed that this scope is unique because we are tracking which nodes have modified - let cur_component = self.components.get_mut(update.idx).unwrap(); + // We are guaranteeed that this scope is unique because we are tracking which nodes have modified in the diff machine + let cur_component = diff_machine + .get_scope_mut(&update.idx) + .expect("Failed to find scope or borrow would be aliasing"); if cur_component.run_scope().is_ok() { let (old, new) = (cur_component.old_frame(), cur_component.next_frame()); @@ -385,7 +355,7 @@ impl VirtualDom { } pub fn base_scope(&self) -> &Scope { - self.components.get(self.base_scope).unwrap() + unsafe { self.shared.get_scope(self.base_scope).unwrap() } } } diff --git a/packages/ssr/src/lib.rs b/packages/ssr/src/lib.rs index dbae758e6..6a9636364 100644 --- a/packages/ssr/src/lib.rs +++ b/packages/ssr/src/lib.rs @@ -21,7 +21,7 @@ pub fn render_vdom_scope(vdom: &VirtualDom, scope: ScopeId) -> Option { "{:}", TextRenderer { cfg: SsrConfig::default(), - root: vdom.components.get(scope).unwrap().root(), + root: vdom.shared.get_scope(scope).unwrap().root(), vdom: Some(vdom) } )) @@ -163,7 +163,7 @@ impl<'a> TextRenderer<'a> { VNodeKind::Component(vcomp) => { let idx = vcomp.ass_scope.get().unwrap(); if let Some(vdom) = self.vdom { - let new_node = vdom.components.get(idx).unwrap().root(); + let new_node = vdom.shared.get_scope(idx).unwrap().root(); self.html_render(new_node, f, il + 1)?; } } diff --git a/packages/web/src/new.rs b/packages/web/src/new.rs index e1138206a..129ffb702 100644 --- a/packages/web/src/new.rs +++ b/packages/web/src/new.rs @@ -13,7 +13,7 @@ use web_sys::{ pub struct WebsysDom { pub stack: Stack, - nodes: slotmap::SlotMap>, + nodes: slotmap::SlotMap, document: Document, root: Element, @@ -51,7 +51,7 @@ impl WebsysDom { let mut nodes = slotmap::SlotMap::with_capacity(1000); - let root_id = nodes.insert(Some(root.clone().dyn_into::().unwrap())); + let root_id = nodes.insert(root.clone().dyn_into::().unwrap()); Self { stack: Stack::with_capacity(10), @@ -101,11 +101,16 @@ impl WebsysDom { let key = DefaultKey::from(KeyData::from_ffi(root)); let domnode = self.nodes.get_mut(key); - let domnode = domnode.unwrap().as_mut().unwrap(); + let real_node: Node = match domnode { + Some(n) => n.clone(), + None => todo!(), + }; + + // let domnode = domnode.unwrap().as_mut().unwrap(); // .expect(&format!("Failed to pop know root: {:#?}", key)) // .unwrap(); - self.stack.push(domnode.clone()); + self.stack.push(real_node); } // drop the node off the stack fn pop(&mut self) { @@ -202,7 +207,7 @@ impl WebsysDom { *self .nodes .get_mut(DefaultKey::from(KeyData::from_ffi(id))) - .unwrap() = Some(textnode); + .unwrap() = textnode; } fn create_element(&mut self, tag: &str, ns: Option<&'static str>, id: u64) { @@ -224,7 +229,7 @@ impl WebsysDom { let id = DefaultKey::from(KeyData::from_ffi(id)); self.stack.push(el.clone()); - *self.nodes.get_mut(id).unwrap() = Some(el); + *self.nodes.get_mut(id).unwrap() = el; // let nid = self.node_counter.?next(); // let nid = self.nodes.insert(el).data().as_ffi(); // log::debug!("Called [`create_element`]: {}, {:?}", tag, nid); @@ -344,11 +349,11 @@ impl WebsysDom { } impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom { - fn request_available_node(&mut self) -> RealDomNode { - let key = self.nodes.insert(None); - log::debug!("making new key: {:#?}", key); - RealDomNode(key.data().as_ffi()) - } + // fn request_available_node(&mut self) -> RealDomNode { + // let key = self.nodes.insert(None); + // log::debug!("making new key: {:#?}", key); + // RealDomNode(key.data().as_ffi()) + // } fn raw_node_as_any(&self) -> &mut dyn std::any::Any { todo!() From 6084fbcd11d1669db135098d920488e2c01a7014 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 23 Jul 2021 17:03:51 -0400 Subject: [PATCH 04/21] wip: move to slab --- packages/core/Cargo.toml | 8 +- packages/core/README.md | 2 + packages/core/src/arena.rs | 85 +++++++------ packages/core/src/diff.rs | 206 ++++++++++++++----------------- packages/core/src/nodes.rs | 13 +- packages/core/src/virtual_dom.rs | 12 +- packages/web/src/new.rs | 3 +- 7 files changed, 159 insertions(+), 170 deletions(-) diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index c662c6732..7febd1fac 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -31,15 +31,15 @@ log = "0.4" # # Serialize the Edits for use in Webview/Liveview instances serde = { version = "1", features = ["derive"], optional = true } -# Backs scopes and unique keys -slotmap = "1.0.3" - appendlist = "1.4.0" futures-util = "0.3.15" + smallvec = "1.6.1" +slab = "0.4.3" + [features] default = ["serialize"] -serialize = ["slotmap/serde", "serde"] +serialize = ["serde"] diff --git a/packages/core/README.md b/packages/core/README.md index b88a595ec..2d6c19658 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -21,6 +21,8 @@ Dioxus-core leverages some really cool techniques and hits a very high level of - suspended nodes (task/fiber endpoints) for asynchronous vnodes - custom memory allocator for vnodes and all text content - support for fragments w/ lazy normalization +- slab allocator for scopes +- mirrored-slab approach for remote vdoms There's certainly more to the story, but these optimizations make Dioxus memory use and allocation count extremely minimal. For an average application, it is likely that zero allocations will need to be performed once the app has been mounted. Only when new components are added to the dom will allocations occur - and only en mass. The space of old VNodes is dynamically recycled as new nodes are added. Additionally, Dioxus tracks the average memory footprint of previous components to estimate how much memory allocate for future components. diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 0e8be0eaf..1ea90f93b 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -4,24 +4,24 @@ use std::{cell::UnsafeCell, rc::Rc}; use crate::heuristics::*; use crate::innerlude::*; use futures_util::stream::FuturesUnordered; -use slotmap::SlotMap; +use slab::Slab; -slotmap::new_key_type! { - // A dedicated key type for the all the scopes - pub struct ScopeId; -} +// slotmap::new_key_type! { +// // A dedicated key type for the all the scopes +// pub struct ScopeId; +// } +// #[cfg(feature = "serialize", serde::Serialize)] +// #[cfg(feature = "serialize", serde::Serialize)] +#[derive(serde::Serialize, serde::Deserialize, Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct ScopeId(usize); -slotmap::new_key_type! { - // A dedicated key type for every real element that the virtualdom creates. - // - // This is a slotmap key because we expect the "mirror" realdom to also maintain a slotmap mapping - // of virtual element to real element. - pub struct ElementId; -} +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct ElementId(usize); impl ElementId { pub fn as_u64(self) -> u64 { - self.0.as_ffi() + todo!() + // self.0.as_ffi() } } @@ -30,7 +30,7 @@ type Shared = Rc>; /// These are resources shared among all the components and the virtualdom itself #[derive(Clone)] pub struct SharedResources { - pub components: Rc>>, + pub components: Rc>>, pub event_queue: Shared>, @@ -42,34 +42,38 @@ pub struct SharedResources { /// We use a SlotSet to keep track of the keys that are currently being used. /// However, we don't store any specific data since the "mirror" - pub raw_elements: Shared>, + pub raw_elements: Shared>, pub task_setter: Rc, } impl SharedResources { pub fn new() -> Self { - // preallocate 1000 elements and 20 scopes to avoid dynamic allocation - let components = Rc::new(UnsafeCell::new( - SlotMap::::with_capacity_and_key(20), - )); - let raw_elements = SlotMap::::with_capacity_and_key(1000); + // preallocate 2000 elements and 20 scopes to avoid dynamic allocation + let components: Rc>> = + Rc::new(UnsafeCell::new(Slab::with_capacity(100))); + + // elements are super cheap - the value takes no space + let raw_elements = Slab::with_capacity(2000); let event_queue = Rc::new(RefCell::new(Vec::new())); let tasks = Vec::new(); let heuristics = HeuristicsEngine::new(); - let queue = event_queue.clone(); - let _components = components.clone(); - let task_setter = Rc::new(move |idx| { - let comps = unsafe { &*_components.get() }; - if let Some(scope) = comps.get(idx) { - queue.borrow_mut().push(HeightMarker { - height: scope.height, - idx, - }) - } - }); + let task_setter = { + let queue = event_queue.clone(); + let components = components.clone(); + Rc::new(move |idx: ScopeId| { + let comps = unsafe { &*components.get() }; + + if let Some(scope) = comps.get(idx.0) { + queue.borrow_mut().push(HeightMarker { + height: scope.height, + idx, + }) + } + }) + }; Self { event_queue, @@ -85,13 +89,13 @@ impl SharedResources { /// this is unsafe because the caller needs to track which other scopes it's already using pub unsafe fn get_scope(&self, idx: ScopeId) -> Option<&Scope> { let inner = &*self.components.get(); - inner.get(idx) + inner.get(idx.0) } /// this is unsafe because the caller needs to track which other scopes it's already using - pub unsafe fn get_sope_mut(&self, idx: ScopeId) -> Option<&mut Scope> { + pub unsafe fn get_scope_mut(&self, idx: ScopeId) -> Option<&mut Scope> { let inner = &mut *self.components.get(); - inner.get_mut(idx) + inner.get_mut(idx.0) } pub fn with_scope<'b, O: 'static>( @@ -114,13 +118,13 @@ impl SharedResources { pub fn try_remove(&self, id: ScopeId) -> Result { let inner = unsafe { &mut *self.components.get() }; - inner - .remove(id) - .ok_or_else(|| Error::FatalInternal("Scope not found")) + Ok(inner.remove(id.0)) + // .try_remove(id.0) + // .ok_or_else(|| Error::FatalInternal("Scope not found")) } pub fn reserve_node(&self) -> ElementId { - self.raw_elements.borrow_mut().insert(()) + ElementId(self.raw_elements.borrow_mut().insert(())) } /// return the id, freeing the space of the original node @@ -132,7 +136,10 @@ impl SharedResources { pub fn insert_scope_with_key(&self, f: impl FnOnce(ScopeId) -> Scope) -> ScopeId { let g = unsafe { &mut *self.components.get() }; - g.insert_with_key(f) + let entry = g.vacant_entry(); + let id = ScopeId(entry.key()); + entry.insert(f(id)); + id } pub fn schedule_update(&self) -> Rc { diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index f29c1fc84..f28b9052a 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -36,10 +36,9 @@ //! //! ## Garbage Collection //! --------------------- -//! We roughly place the role of garbage collection onto the reconciler. Dioxus needs to manage the lifecycle of components -//! but will not spend any time cleaning up old elements. It's the Reconciler's duty to understand which elements need to -//! be cleaned up *after* the diffing is completed. The reconciler should schedule this garbage collection as the absolute -//! lowest priority task, after all edits have been applied. +//! Dioxus uses a passive garbage collection system to clean up old nodes once the work has been completed. This garabge +//! collection is done internally once the main diffing work is complete. After the "garbage" is collected, Dioxus will then +//! start to re-use old keys for new nodes. This results in a passive memory management system that is very efficient. //! //! //! Further Reading and Thoughts @@ -54,8 +53,6 @@ use smallvec::{smallvec, SmallVec}; use std::{any::Any, borrow::Borrow}; -/// The accompanying "real dom" exposes an imperative API for controlling the UI layout -/// /// Instead of having handles directly over nodes, Dioxus uses simple u64s as node IDs. /// The expectation is that the underlying renderer will mainain their Nodes in something like slotmap or an ECS memory /// where indexing is very fast. For reference, the slotmap in the WebSys renderer takes about 3ns to randomly access any @@ -69,39 +66,36 @@ pub trait RealDom<'a> { fn raw_node_as_any(&self) -> &mut dyn Any; } -pub struct DiffMachine<'real, 'bump, Dom: RealDom<'bump>> { - pub real_dom: &'real mut Dom, +pub struct DiffMachine<'real, 'bump> { + pub real_dom: &'real dyn RealDom<'bump>, pub vdom: &'bump SharedResources, pub edits: DomEditor<'real, 'bump>, + pub scheduled_garbage: Vec<&'bump VNode<'bump>>, + pub cur_idxs: SmallVec<[ScopeId; 5]>, pub diffed: FxHashSet, pub seen_nodes: FxHashSet, } -impl<'r, 'b, D: RealDom<'b>> DiffMachine<'r, 'b, D> { +impl<'r, 'b> DiffMachine<'r, 'b> { pub fn get_scope_mut(&mut self, id: &ScopeId) -> Option<&'b mut Scope> { // ensure we haven't seen this scope before // if we have, then we're trying to alias it, which is not allowed debug_assert!(!self.seen_nodes.contains(id)); - let compon = unsafe { &mut *self.vdom.components.get() }; - compon.get_mut(*id) + unsafe { self.vdom.get_scope_mut(*id) } } pub fn get_scope(&mut self, id: &ScopeId) -> Option<&'b Scope> { // ensure we haven't seen this scope before // if we have, then we're trying to alias it, which is not allowed - let compon = unsafe { &*self.vdom.components.get() }; - compon.get(*id) + unsafe { self.vdom.get_scope(*id) } } } -impl<'real, 'bump, Dom> DiffMachine<'real, 'bump, Dom> -where - Dom: RealDom<'bump>, -{ +impl<'real, 'bump> DiffMachine<'real, 'bump> { pub fn new( edits: &'real mut Vec>, - dom: &'real mut Dom, + dom: &'real dyn RealDom<'bump>, cur_idx: ScopeId, shared: &'bump SharedResources, ) -> Self { @@ -110,6 +104,7 @@ where edits: DomEditor::new(edits), cur_idxs: smallvec![cur_idx], vdom: shared, + scheduled_garbage: vec![], diffed: FxHashSet::default(), seen_nodes: FxHashSet::default(), } @@ -151,6 +146,7 @@ where self.edits.push_root(root); let meta = self.create(new_node); self.edits.replace_with(meta.added_to_stack); + self.scheduled_garbage.push(old_node); self.edits.pop(); return; } @@ -208,14 +204,15 @@ where // remove any leftovers for to_remove in old_iter { - self.edits.push_root(to_remove); + self.edits.push_root(to_remove.element_id().unwrap()); self.edits.remove(); } // seems like we could combine this into a single instruction.... - self.edits.push_root(first); + self.edits.push_root(first.element_id().unwrap()); let meta = self.create(new_node); self.edits.replace_with(meta.added_to_stack); + self.scheduled_garbage.push(old_node); self.edits.pop(); // Wipe the old one and plant the new one @@ -272,11 +269,11 @@ where // remove any leftovers for to_remove in old_iter { - self.edits.push_root(to_remove); + self.edits.push_root(to_remove.element_id().unwrap()); self.edits.remove(); } - back_node + back_node.element_id().unwrap() } }; @@ -314,10 +311,7 @@ impl CreateMeta { } } -impl<'real, 'bump, Dom> DiffMachine<'real, 'bump, Dom> -where - Dom: RealDom<'bump>, -{ +impl<'real, 'bump> DiffMachine<'real, 'bump> { // Emit instructions to create the given virtual node. // // The change list stack may have any shape upon entering this function: @@ -448,8 +442,7 @@ where // .insert(idx); // TODO: abstract this unsafe into the arena abstraction - let inner: &'bump mut _ = unsafe { &mut *self.vdom.components.get() }; - let new_component = inner.get_mut(new_idx).unwrap(); + let new_component = self.get_scope_mut(&new_idx).unwrap(); // Actually initialize the caller's slot with the right address vcomponent.ass_scope.set(Some(new_idx)); @@ -498,7 +491,7 @@ where } } -impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { +impl<'a, 'bump> DiffMachine<'a, 'bump> { /// Destroy a scope and all of its descendents. /// /// Calling this will run the destuctors on all hooks in the tree. @@ -727,7 +720,7 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // [... parent] // // Upon exiting, the change list stack is in the same state. - fn diff_keyed_children(&self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { + fn diff_keyed_children(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { // todo!(); if cfg!(debug_assertions) { let mut keys = fxhash::FxHashSet::default(); @@ -761,11 +754,6 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { KeyedPrefixResult::MoreWorkToDo(count) => count, }; - match self.diff_keyed_prefix(old, new) { - KeyedPrefixResult::Finished => return, - KeyedPrefixResult::MoreWorkToDo(count) => count, - }; - // Next, we find out how many of the nodes at the end of the children have // the same key. We do _not_ diff them yet, since we want to emit the change // list instructions such that they can be applied in a single pass over the @@ -812,46 +800,47 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // // Upon exit, the change list stack is the same. fn diff_keyed_prefix( - &self, - _old: &'bump [VNode<'bump>], - _new: &'bump [VNode<'bump>], + &mut self, + old: &'bump [VNode<'bump>], + new: &'bump [VNode<'bump>], ) -> KeyedPrefixResult { - todo!() // self.edits.go_down(); - // let mut shared_prefix_count = 0; - // for (i, (old, new)) in old.iter().zip(new.iter()).enumerate() { - // if old.key() != new.key() { - // break; - // } + let mut shared_prefix_count = 0; - // self.edits.go_to_sibling(i); + for (i, (old, new)) in old.iter().zip(new.iter()).enumerate() { + // abort early if we finally run into nodes with different keys + if old.key() != new.key() { + break; + } - // self.diff_node(old, new); + // self.edits.go_to_sibling(i); - // shared_prefix_count += 1; - // } + self.diff_node(old, new); - // // If that was all of the old children, then create and append the remaining - // // new children and we're finished. - // if shared_prefix_count == old.len() { - // self.edits.go_up(); - // // self.edits.commit_traversal(); - // self.create_and_append_children(&new[shared_prefix_count..]); - // return KeyedPrefixResult::Finished; - // } + shared_prefix_count += 1; + } - // // And if that was all of the new children, then remove all of the remaining - // // old children and we're finished. - // if shared_prefix_count == new.len() { - // self.edits.go_to_sibling(shared_prefix_count); - // // self.edits.commit_traversal(); - // self.remove_self_and_next_siblings(&old[shared_prefix_count..]); - // return KeyedPrefixResult::Finished; - // } + // If that was all of the old children, then create and append the remaining + // new children and we're finished. + if shared_prefix_count == old.len() { + // self.edits.go_up(); + // self.edits.commit_traversal(); + self.create_and_append_children(&new[shared_prefix_count..]); + return KeyedPrefixResult::Finished; + } + // And if that was all of the new children, then remove all of the remaining + // old children and we're finished. + if shared_prefix_count == new.len() { + // self.edits.go_to_sibling(shared_prefix_count); + // self.edits.commit_traversal(); + self.remove_self_and_next_siblings(&old[shared_prefix_count..]); + return KeyedPrefixResult::Finished; + } + // // self.edits.go_up(); - // KeyedPrefixResult::MoreWorkToDo(shared_prefix_count) + KeyedPrefixResult::MoreWorkToDo(shared_prefix_count) } // Remove all of a node's children. @@ -902,13 +891,12 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // Upon exit from this function, it will be restored to that same state. fn diff_keyed_middle( &self, - _old: &[VNode<'bump>], - _new: &[VNode<'bump>], - _shared_prefix_count: usize, - _shared_suffix_count: usize, - _old_shared_suffix_start: usize, + old: &[VNode<'bump>], + new: &[VNode<'bump>], + shared_prefix_count: usize, + shared_suffix_count: usize, + old_shared_suffix_start: usize, ) { - todo!() // // Should have already diffed the shared-key prefixes and suffixes. // debug_assert_ne!(new.first().map(|n| n.key()), old.first().map(|o| o.key())); // debug_assert_ne!(new.last().map(|n| n.key()), old.last().map(|o| o.key())); @@ -921,24 +909,30 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // debug_assert!(new.len() < u32::MAX as usize); // // Map from each `old` node's key to its index within `old`. - // let mut old_key_to_old_index = FxHashMap::default(); - // old_key_to_old_index.reserve(old.len()); - // old_key_to_old_index.extend(old.iter().enumerate().map(|(i, o)| (o.key(), i))); + // // IE if the keys were A B C, then we would have (A, 1) (B, 2) (C, 3). + // let mut old_key_to_old_index = old + // .iter() + // .enumerate() + // .map(|(i, o)| (o.key(), i)) + // .collect::>(); // // The set of shared keys between `new` and `old`. // let mut shared_keys = FxHashSet::default(); + // // Map from each index in `new` to the index of the node in `old` that // // has the same key. - // let mut new_index_to_old_index = Vec::with_capacity(new.len()); - // new_index_to_old_index.extend(new.iter().map(|n| { - // let key = n.key(); - // if let Some(&i) = old_key_to_old_index.get(&key) { - // shared_keys.insert(key); - // i - // } else { - // u32::MAX as usize - // } - // })); + // let mut new_index_to_old_index = new + // .iter() + // .map(|n| { + // let key = n.key(); + // if let Some(&i) = old_key_to_old_index.get(&key) { + // shared_keys.insert(key); + // i + // } else { + // u32::MAX as usize + // } + // }) + // .collect::>(); // // 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 @@ -948,7 +942,7 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // // self.edits.commit_traversal(); // self.remove_all_children(old); // } else { - // self.edits.go_down_to_child(shared_prefix_count); + // // self.edits.go_down_to_child(shared_prefix_count); // // self.edits.commit_traversal(); // self.remove_self_and_next_siblings(&old[shared_prefix_count..]); // } @@ -998,6 +992,7 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // // registry.remove_subtree(old_child); // // todo // // self.edits.commit_traversal(); + // self.edits.remove(old_child.dom_id.get()); // self.edits.remove_child(i + shared_prefix_count); // removed_count += 1; // } @@ -1115,10 +1110,6 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // self.diff_node(&old[old_index], new_child); // } // } - - // // [... parent child] - // self.edits.go_up(); - // [... parent] } // Diff the suffix of keyed children that share the same keys in the same order. @@ -1129,26 +1120,17 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // // When this function exits, the change list stack remains the same. fn diff_keyed_suffix( - &self, - _old: &[VNode<'bump>], - _new: &[VNode<'bump>], - _new_shared_suffix_start: usize, + &mut self, + old: &'bump [VNode<'bump>], + new: &'bump [VNode<'bump>], + new_shared_suffix_start: usize, ) { - todo!() - // debug_assert_eq!(old.len(), new.len()); - // debug_assert!(!old.is_empty()); + debug_assert_eq!(old.len(), new.len()); + debug_assert!(!old.is_empty()); - // // [... parent] - // self.edits.go_down(); - // // [... parent new_child] - - // for (i, (old_child, new_child)) in old.iter().zip(new.iter()).enumerate() { - // self.edits.go_to_sibling(new_shared_suffix_start + i); - // self.diff_node(old_child, new_child); - // } - - // // [... parent] - // self.edits.go_up(); + for (i, (old_child, new_child)) in old.iter().zip(new.iter()).enumerate() { + self.diff_node(old_child, new_child); + } } // Diff children that are not keyed. @@ -1299,11 +1281,11 @@ impl<'a> RealChildIterator<'a> { } impl<'a> Iterator for RealChildIterator<'a> { - type Item = ElementId; + type Item = &'a VNode<'a>; - fn next(&mut self) -> Option { + fn next(&mut self) -> Option<&'a VNode<'a>> { let mut should_pop = false; - let mut returned_node = None; + let mut returned_node: Option<&'a VNode<'a>> = None; let mut should_push = None; while returned_node.is_none() { @@ -1315,7 +1297,7 @@ impl<'a> Iterator for RealChildIterator<'a> { // We've recursed INTO an element/text // We need to recurse *out* of it and move forward to the next should_pop = true; - returned_node = node.dom_id.get(); + returned_node = Some(&*node); } // If we get a fragment we push the next child @@ -1324,7 +1306,7 @@ impl<'a> Iterator for RealChildIterator<'a> { if frag.children.len() == 0 { should_pop = true; - returned_node = node.dom_id.get(); + returned_node = Some(&*node); } if subcount >= frag.children.len() { diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index a9e2ea9cc..844e4e66e 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -17,17 +17,18 @@ use std::{ pub struct VNode<'src> { pub kind: VNodeKind<'src>, - /// - /// ElementId supports NonZero32 and Cell is zero cost, so the size of this field is unaffected - /// - /// - pub(crate) dom_id: Cell>, pub(crate) key: Option<&'src str>, + + /// ElementId supports NonZero32 and Cell is zero cost, so the size of this field is unaffected + pub(crate) dom_id: Cell>, } impl VNode<'_> { - fn key(&self) -> Option<&str> { + pub fn key(&self) -> Option<&str> { self.key } + pub fn element_id(&self) -> Option { + self.dom_id.get() + } } /// Tools for the base unit of the virtual dom - the VNode diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index a59705547..1dd6ccfd4 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -21,8 +21,6 @@ use crate::{arena::SharedResources, innerlude::*}; -use slotmap::DefaultKey; -use slotmap::SlotMap; use std::any::Any; use std::any::TypeId; @@ -181,10 +179,10 @@ impl VirtualDom { /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom rom scratch /// /// The diff machine expects the RealDom's stack to be the root of the application - pub fn rebuild<'s, Dom: RealDom<'s>>( + pub fn rebuild<'s>( &'s mut self, - realdom: &mut Dom, - edits: &mut Vec>, + realdom: &'s mut dyn RealDom<'s>, + edits: &'s mut Vec>, ) -> Result<()> { let mut diff_machine = DiffMachine::new(edits, realdom, self.base_scope, &self.shared); @@ -260,9 +258,9 @@ impl VirtualDom { // but the guarantees provide a safe, fast, and efficient abstraction for the VirtualDOM updating framework. // // A good project would be to remove all unsafe from this crate and move the unsafety into safer abstractions. - pub async fn progress_with_event<'s, Dom: RealDom<'s>>( + pub async fn progress_with_event<'s>( &'s mut self, - realdom: &'_ mut Dom, + realdom: &'s mut dyn RealDom<'s>, edits: &mut Vec>, ) -> Result<()> { let trigger = self.triggers.borrow_mut().pop().expect("failed"); diff --git a/packages/web/src/new.rs b/packages/web/src/new.rs index 129ffb702..b196f726b 100644 --- a/packages/web/src/new.rs +++ b/packages/web/src/new.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, rc::Rc, sync::Arc}; use dioxus_core::{ events::{EventTrigger, VirtualEvent}, - DomEdit, RealDomNode, ScopeId, + DomEdit, ElementId, ScopeId, }; use fxhash::FxHashMap; use slotmap::{DefaultKey, Key, KeyData}; @@ -588,7 +588,6 @@ fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent { // let evt: web_sys::ToggleEvent = event.clone().dyn_into().unwrap(); todo!() } - _ => VirtualEvent::OtherEvent, } } From dfaf5adee164f44a679ab21d730caaab3610e01f Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 24 Jul 2021 00:29:23 -0400 Subject: [PATCH 05/21] Feat: solve some issues regarding listeners --- docs/advanced-guides/custom-renderer.md | 2 +- examples/basic.rs | 15 +++++++ examples/testbed.rs | 3 +- packages/core-macro/src/props/mod.rs | 8 ---- packages/core-macro/src/rsx/element.rs | 9 ++-- packages/core/src/arena.rs | 4 +- packages/core/src/diff.rs | 14 ++++--- packages/core/src/events.rs | 11 +++-- packages/core/src/nodes.rs | 56 ++++++++++++++++--------- packages/core/src/scope.rs | 10 +++-- packages/core/src/virtual_dom.rs | 8 +++- packages/desktop/Cargo.toml | 1 + packages/desktop/examples/test.rs | 11 +---- packages/desktop/src/dom.rs | 8 +--- packages/desktop/src/index.html | 4 +- packages/html/src/attrval.s | 30 +++++++++++++ packages/html/src/lib.rs | 31 +------------- packages/mobile/src/dom.rs | 7 +--- packages/ssr/src/lib.rs | 38 ++++++++--------- packages/web/Cargo.toml | 1 - packages/web/src/lib.rs | 51 +++++++++++----------- packages/web/src/new.rs | 38 +++++++++-------- 22 files changed, 192 insertions(+), 168 deletions(-) create mode 100644 examples/basic.rs create mode 100644 packages/html/src/attrval.s diff --git a/docs/advanced-guides/custom-renderer.md b/docs/advanced-guides/custom-renderer.md index ac141e92a..5e1f8d878 100644 --- a/docs/advanced-guides/custom-renderer.md +++ b/docs/advanced-guides/custom-renderer.md @@ -25,7 +25,7 @@ The current `RealDom` trait lives in `dioxus_core/diff`. A version of it is prov ```rust pub trait RealDom<'a> { fn handle_edit(&mut self, edit: DomEdit); - fn request_available_node(&mut self) -> RealDomNode; + fn request_available_node(&mut self) -> ElementId; fn raw_node_as_any(&self) -> &mut dyn Any; } ``` diff --git a/examples/basic.rs b/examples/basic.rs new file mode 100644 index 000000000..63ab7d3fc --- /dev/null +++ b/examples/basic.rs @@ -0,0 +1,15 @@ +use dioxus::prelude::*; + +fn main() { + let g = dioxus::prelude::LazyNodes::new(move |__cx: NodeFactory| { + use dioxus_elements::{GlobalAttributes, SvgAttributes}; + __cx.element( + dioxus_elements::button, + __cx.bump() + .alloc([dioxus::events::on::onclick(__cx, move |_| {})]), + __cx.bump().alloc([]), + __cx.bump().alloc([]), + None, + ) + }); +} diff --git a/examples/testbed.rs b/examples/testbed.rs index 240c75a76..1eadcac53 100644 --- a/examples/testbed.rs +++ b/examples/testbed.rs @@ -3,7 +3,7 @@ use std::cell::Cell; use dioxus::prelude::*; use dioxus_core::{ nodes::{VElement, VText}, - RealDomNode, + ElementId, }; fn main() { @@ -33,6 +33,7 @@ const Example: FC<()> = |cx| { } "h4" div { "h5" } + button { } Child {} }) }; diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index 9291dd6de..f91eebbda 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -502,7 +502,6 @@ mod field_info { mod struct_info { use proc_macro2::TokenStream; - use quote::__private::ext::RepToTokensExt; use quote::quote; use syn::parse::Error; @@ -1095,13 +1094,6 @@ Finally, call `.build()` to create the instance of `{name}`. } ) } - - pub fn build_props_impl(&self) -> TokenStream { - // SomeProps: #name - // #builder_name - // #generics_with_empty - quote! {} - } } #[derive(Debug, Default)] diff --git a/packages/core-macro/src/rsx/element.rs b/packages/core-macro/src/rsx/element.rs index c4fa949e8..45c391831 100644 --- a/packages/core-macro/src/rsx/element.rs +++ b/packages/core-macro/src/rsx/element.rs @@ -199,9 +199,12 @@ impl ToTokens for Element { tokens.append_all(quote! { __cx.element( dioxus_elements::#name, - __cx.bump().alloc([ #(#listeners),* ]), - __cx.bump().alloc([ #(#attr),* ]), - __cx.bump().alloc([ #(#childs),* ]), + [ #(#listeners),* ], + [ #(#attr),* ], + [ #(#childs),* ], + // __cx.bump().alloc([ #(#listeners),* ]), + // __cx.bump().alloc([ #(#attr),* ]), + // __cx.bump().alloc([ #(#childs),* ]), None, ) }); diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 1ea90f93b..2fd1f1d2f 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -13,10 +13,10 @@ use slab::Slab; // #[cfg(feature = "serialize", serde::Serialize)] // #[cfg(feature = "serialize", serde::Serialize)] #[derive(serde::Serialize, serde::Deserialize, Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub struct ScopeId(usize); +pub struct ScopeId(pub usize); #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub struct ElementId(usize); +pub struct ElementId(pub usize); impl ElementId { pub fn as_u64(self) -> u64 { diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index f28b9052a..1ce705e4d 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -34,12 +34,16 @@ //! For all components, we employ some basic heuristics to speed up allocations and pre-size bump arenas. The heuristics are //! currently very rough, but will get better as time goes on. For FFI, we recommend using a bloom filter to cache strings. //! +//! //! ## Garbage Collection //! --------------------- //! Dioxus uses a passive garbage collection system to clean up old nodes once the work has been completed. This garabge //! collection is done internally once the main diffing work is complete. After the "garbage" is collected, Dioxus will then //! start to re-use old keys for new nodes. This results in a passive memory management system that is very efficient. //! +//! The IDs used by the key/map are just an index into a vec. This means that Dioxus will drive the key allocation strategy +//! so the client only needs to maintain a simple list of nodes. By default, Dioxus will not manually clean up old nodes +//! for the client. As new nodes are created, old nodes will be over-written. //! //! Further Reading and Thoughts //! ---------------------------- @@ -53,10 +57,9 @@ use smallvec::{smallvec, SmallVec}; use std::{any::Any, borrow::Borrow}; -/// Instead of having handles directly over nodes, Dioxus uses simple u64s as node IDs. -/// The expectation is that the underlying renderer will mainain their Nodes in something like slotmap or an ECS memory -/// where indexing is very fast. For reference, the slotmap in the WebSys renderer takes about 3ns to randomly access any -/// node. +/// Instead of having handles directly over nodes, Dioxus uses simple u32 as node IDs. +/// The expectation is that the underlying renderer will mainain their Nodes in vec where the ids are the index. This allows +/// for a form of passive garbage collection where nodes aren't immedately cleaned up. /// /// The "RealDom" abstracts over the... real dom. The RealDom trait assumes that the renderer maintains a stack of real /// nodes as the diffing algorithm descenes through the tree. This means that whatever is on top of the stack will receive @@ -531,7 +534,7 @@ impl<'a, 'bump> DiffMachine<'a, 'bump> { // [... node] // // The change list stack is left unchanged. - fn diff_listeners(&mut self, old: &[&mut Listener<'_>], new: &[&mut Listener<'_>]) { + fn diff_listeners(&mut self, old: &[Listener<'_>], new: &[Listener<'_>]) { if !old.is_empty() || !new.is_empty() { // self.edits.commit_traversal(); } @@ -721,7 +724,6 @@ impl<'a, 'bump> DiffMachine<'a, 'bump> { // // Upon exiting, the change list stack is in the same state. fn diff_keyed_children(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { - // todo!(); if cfg!(debug_assertions) { let mut keys = fxhash::FxHashSet::default(); let mut assert_unique_keys = |children: &'bump [VNode<'bump>]| { diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 9cd6d2c0b..74dca1dab 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -4,7 +4,10 @@ //! 3rd party renderers are responsible for converting their native events into these virtual event types. Events might //! be heavy or need to interact through FFI, so the events themselves are designed to be lazy. -use std::{cell::Cell, rc::Rc}; +use std::{ + cell::{Cell, RefCell}, + rc::Rc, +}; use crate::innerlude::{ElementId, HeightMarker, ScopeId}; @@ -135,7 +138,7 @@ pub mod on { //! #![allow(unused)] - use std::{fmt::Debug, ops::Deref, rc::Rc}; + use std::{cell::RefCell, fmt::Debug, ops::Deref, rc::Rc}; use crate::{ innerlude::NodeFactory, @@ -183,10 +186,10 @@ pub mod on { event: stringify!($name), mounted_node: Cell::new(None), scope: c.scope.our_arena_idx, - callback: bump.alloc(move |evt: VirtualEvent| match evt { + callback: RefCell::new(bump.alloc(move |evt: VirtualEvent| match evt { VirtualEvent::$wrapper(event) => callback(event), _ => unreachable!("Downcasted VirtualEvent to wrong event type - this is an internal bug!") - }), + })), } } )* diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 844e4e66e..d549aa1dd 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -8,7 +8,7 @@ use crate::{ innerlude::{empty_cell, Context, DomTree, ElementId, Properties, Scope, ScopeId, FC}, }; use std::{ - cell::Cell, + cell::{Cell, RefCell}, fmt::{Arguments, Debug, Formatter}, marker::PhantomData, rc::Rc, @@ -77,7 +77,7 @@ pub struct VElement<'a> { pub namespace: Option<&'static str>, pub static_listeners: bool, - pub listeners: &'a [&'a mut Listener<'a>], + pub listeners: &'a [Listener<'a>], pub static_attrs: bool, pub attributes: &'a [Attribute<'a>], @@ -112,7 +112,7 @@ pub struct Listener<'bump> { pub mounted_node: Cell>, - pub(crate) callback: &'bump mut dyn FnMut(VirtualEvent), + pub(crate) callback: RefCell<&'bump mut dyn FnMut(VirtualEvent)>, } /// Virtual Components for custom user-defined components @@ -200,14 +200,19 @@ impl<'a> NodeFactory<'a> { } } - pub fn element( + pub fn element( &self, el: impl DioxusElement, - listeners: &'a mut [&'a mut Listener<'a>], - attributes: &'a [Attribute<'a>], - children: &'a [VNode<'a>], + listeners: L, + attributes: A, + children: V, key: Option<&'a str>, - ) -> VNode<'a> { + ) -> VNode<'a> + where + L: 'a + AsRef<[Listener<'a>]>, + A: 'a + AsRef<[Attribute<'a>]>, + V: 'a + AsRef<[VNode<'a>]>, + { self.raw_element( el.tag_name(), el.namespace(), @@ -218,26 +223,39 @@ impl<'a> NodeFactory<'a> { ) } - pub fn raw_element( + pub fn raw_element( &self, tag: &'static str, namespace: Option<&'static str>, - listeners: &'a mut [&'a mut Listener<'a>], - attributes: &'a [Attribute], - children: &'a [VNode<'a>], + listeners: L, + attributes: A, + children: V, key: Option<&'a str>, - ) -> VNode<'a> { + ) -> VNode<'a> + where + L: 'a + AsRef<[Listener<'a>]>, + A: 'a + AsRef<[Attribute<'a>]>, + V: 'a + AsRef<[VNode<'a>]>, + { + let listeners: &'a L = self.bump().alloc(listeners); + let listeners = listeners.as_ref(); + + let attributes: &'a A = self.bump().alloc(attributes); + let attributes = attributes.as_ref(); + + let children: &'a V = self.bump().alloc(children); + let children = children.as_ref(); + // We take the references directly from the bump arena + // + // // TODO: this code shouldn't necessarily be here of all places // It would make more sense to do this in diffing let mut queue = self.scope.listeners.borrow_mut(); - for listener in listeners.iter_mut() { - let raw_listener = &mut **listener; - - let long_listener: &mut Listener<'static> = - unsafe { std::mem::transmute(raw_listener) }; - queue.push(long_listener as *mut _) + for listener in listeners.iter() { + let long_listener: &Listener<'static> = unsafe { std::mem::transmute(listener) }; + queue.push(long_listener as *const _) } VNode { diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index 01a52fc7b..1a70fc13f 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -1,4 +1,5 @@ use crate::innerlude::*; +use bumpalo::boxed::Box as BumpBox; use std::{ any::{Any, TypeId}, cell::{Cell, RefCell}, @@ -31,7 +32,7 @@ pub struct Scope { pub(crate) child_nodes: &'static [VNode<'static>], // Listeners - pub(crate) listeners: RefCell>>, + pub(crate) listeners: RefCell>>, pub(crate) listener_idx: Cell, // State @@ -154,7 +155,7 @@ impl Scope { let listners = self.listeners.borrow_mut(); let raw_listener = listners.iter().find(|lis| { - let search = unsafe { &mut ***lis }; + let search = unsafe { &***lis }; let search_id = search.mounted_node.get(); log::info!("searching listener {:#?}", search_id); @@ -165,8 +166,9 @@ impl Scope { }); if let Some(raw_listener) = raw_listener { - let listener = unsafe { &mut **raw_listener }; - (listener.callback)(event); + let listener = unsafe { &**raw_listener }; + let mut cb = listener.callback.borrow_mut(); + (cb)(event); } else { log::warn!("An event was triggered but there was no listener to handle it"); } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 1dd6ccfd4..f82026a8d 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -181,8 +181,8 @@ impl VirtualDom { /// The diff machine expects the RealDom's stack to be the root of the application pub fn rebuild<'s>( &'s mut self, - realdom: &'s mut dyn RealDom<'s>, - edits: &'s mut Vec>, + realdom: &'_ mut dyn RealDom<'s>, + edits: &'_ mut Vec>, ) -> Result<()> { let mut diff_machine = DiffMachine::new(edits, realdom, self.base_scope, &self.shared); @@ -355,6 +355,10 @@ impl VirtualDom { pub fn base_scope(&self) -> &Scope { unsafe { self.shared.get_scope(self.base_scope).unwrap() } } + + pub fn get_scope(&self, id: ScopeId) -> Option<&Scope> { + unsafe { self.shared.get_scope(id) } + } } // TODO! diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 4249d4826..4c44aecb1 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -23,6 +23,7 @@ wry = "0.11.0" [dev-dependencies] +dioxus-html = { path = "../html" } tide = "0.15.0" tide-websockets = "0.3.0" async-std = { version = "1.9.0", features = ["attributes"] } diff --git a/packages/desktop/examples/test.rs b/packages/desktop/examples/test.rs index 51eca643f..f18103c71 100644 --- a/packages/desktop/examples/test.rs +++ b/packages/desktop/examples/test.rs @@ -1,5 +1,6 @@ use dioxus_core as dioxus; use dioxus_core::prelude::*; +use dioxus_html as dioxus_elements; fn main() { dioxus_desktop::launch(App, |f| f.with_maximized(true)).expect("Failed"); @@ -13,13 +14,3 @@ static App: FC<()> = |cx| { } )) }; - -mod dioxus_elements { - use super::*; - pub struct div; - impl DioxusElement for div { - const TAG_NAME: &'static str = "div"; - const NAME_SPACE: Option<&'static str> = None; - } - pub trait GlobalAttributes {} -} diff --git a/packages/desktop/src/dom.rs b/packages/desktop/src/dom.rs index 72aa87d68..59debac46 100644 --- a/packages/desktop/src/dom.rs +++ b/packages/desktop/src/dom.rs @@ -1,7 +1,6 @@ //! webview dom -use dioxus_core::{DomEdit, RealDom, RealDomNode, ScopeId}; -use DomEdit::*; +use dioxus_core::{DomEdit, RealDom}; pub struct WebviewRegistry {} @@ -35,9 +34,4 @@ impl<'bump> RealDom<'bump> for WebviewDom<'bump> { todo!() // self.edits.push(PushRoot { root }); } - - fn request_available_node(&mut self) -> RealDomNode { - self.node_counter += 1; - RealDomNode::from_u64(self.node_counter) - } } diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html index e67dee623..584685419 100644 --- a/packages/desktop/src/index.html +++ b/packages/desktop/src/index.html @@ -8,9 +8,7 @@ this.stack = [root]; this.listeners = {}; this.lastNodeWasText = false; - this.nodes = { - 0: root - }; + this.nodes = []; } top() { diff --git a/packages/html/src/attrval.s b/packages/html/src/attrval.s new file mode 100644 index 000000000..cb081f4cb --- /dev/null +++ b/packages/html/src/attrval.s @@ -0,0 +1,30 @@ + +trait AsAttributeValue: Sized { + fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a>; +} +enum AttributeValue<'a> { + Int(i32), + Float(f32), + Str(&'a str), + Bool(bool), +} +impl<'b> AsAttributeValue for Arguments<'b> { + fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a> { + todo!() + } +} +impl AsAttributeValue for &'static str { + fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a> { + todo!() + } +} +impl AsAttributeValue for f32 { + fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a> { + todo!() + } +} +impl AsAttributeValue for i32 { + fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a> { + todo!() + } +} diff --git a/packages/html/src/lib.rs b/packages/html/src/lib.rs index 22709da86..b726d34a5 100644 --- a/packages/html/src/lib.rs +++ b/packages/html/src/lib.rs @@ -1,3 +1,4 @@ +#![allow(non_snake_case)] //! # Dioxus Namespace for HTML //! //! This crate provides a set of compile-time correct HTML elements that can be used with the Rsx and Html macros. @@ -2100,33 +2101,3 @@ summary slot template */ - -trait AsAttributeValue: Sized { - fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a>; -} -enum AttributeValue<'a> { - Int(i32), - Float(f32), - Str(&'a str), - Bool(bool), -} -impl<'b> AsAttributeValue for Arguments<'b> { - fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a> { - todo!() - } -} -impl AsAttributeValue for &'static str { - fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a> { - todo!() - } -} -impl AsAttributeValue for f32 { - fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a> { - todo!() - } -} -impl AsAttributeValue for i32 { - fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a> { - todo!() - } -} diff --git a/packages/mobile/src/dom.rs b/packages/mobile/src/dom.rs index 72aa87d68..cea4f530d 100644 --- a/packages/mobile/src/dom.rs +++ b/packages/mobile/src/dom.rs @@ -1,6 +1,6 @@ //! webview dom -use dioxus_core::{DomEdit, RealDom, RealDomNode, ScopeId}; +use dioxus_core::{DomEdit, ElementId, RealDom, ScopeId}; use DomEdit::*; pub struct WebviewRegistry {} @@ -35,9 +35,4 @@ impl<'bump> RealDom<'bump> for WebviewDom<'bump> { todo!() // self.edits.push(PushRoot { root }); } - - fn request_available_node(&mut self) -> RealDomNode { - self.node_counter += 1; - RealDomNode::from_u64(self.node_counter) - } } diff --git a/packages/ssr/src/lib.rs b/packages/ssr/src/lib.rs index 6a9636364..3d81c33b6 100644 --- a/packages/ssr/src/lib.rs +++ b/packages/ssr/src/lib.rs @@ -21,7 +21,7 @@ pub fn render_vdom_scope(vdom: &VirtualDom, scope: ScopeId) -> Option { "{:}", TextRenderer { cfg: SsrConfig::default(), - root: vdom.shared.get_scope(scope).unwrap().root(), + root: vdom.get_scope(scope).unwrap().root(), vdom: Some(vdom) } )) @@ -163,7 +163,7 @@ impl<'a> TextRenderer<'a> { VNodeKind::Component(vcomp) => { let idx = vcomp.ass_scope.get().unwrap(); if let Some(vdom) = self.vdom { - let new_node = vdom.shared.get_scope(idx).unwrap().root(); + let new_node = vdom.get_scope(idx).unwrap().root(); self.html_render(new_node, f, il + 1)?; } } @@ -260,22 +260,22 @@ mod tests { .unwrap(); } - #[test] - fn styles() { - const STLYE_APP: FC<()> = |cx| { - // - cx.render(rsx! { - div { - style: { - color: "blue", - font_size: "46px" - } - } - }) - }; + // #[test] + // fn styles() { + // const STLYE_APP: FC<()> = |cx| { + // // + // cx.render(rsx! { + // div { + // style: { + // color: "blue", + // font_size: "46px" + // } + // } + // }) + // }; - let mut dom = VirtualDom::new(STLYE_APP); - dom.rebuild_in_place().expect("failed to run virtualdom"); - dbg!(render_vdom(&dom)); - } + // let mut dom = VirtualDom::new(STLYE_APP); + // dom.rebuild_in_place().expect("failed to run virtualdom"); + // dbg!(render_vdom(&dom)); + // } } diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 9de645a68..e23d4c76d 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -23,7 +23,6 @@ wasm-bindgen-test = "0.3.21" once_cell = "1.8" async-channel = "1.6.1" anyhow = "1.0.41" -slotmap = "1.0.3" futures-util = "0.3.15" diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index d517b4744..1a41b930b 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -68,36 +68,37 @@ pub async fn event_loop(mut internal_dom: VirtualDom) -> dioxus_core::error::Res log::info!("Going into event loop"); loop { - let trigger = { - let real_queue = websys_dom.wait_for_event(); - if internal_dom.tasks.is_empty() { - log::info!("tasks is empty, waiting for dom event to trigger soemthing"); - real_queue.await - } else { - log::info!("tasks is not empty, waiting for either tasks or event system"); - let task_queue = (&mut internal_dom.tasks).next(); + todo!(); + // let trigger = { + // let real_queue = websys_dom.wait_for_event(); + // if internal_dom.tasks.is_empty() { + // log::info!("tasks is empty, waiting for dom event to trigger soemthing"); + // real_queue.await + // } else { + // log::info!("tasks is not empty, waiting for either tasks or event system"); + // let task_queue = (&mut internal_dom.tasks).next(); - pin_mut!(real_queue); - pin_mut!(task_queue); + // pin_mut!(real_queue); + // pin_mut!(task_queue); - match futures_util::future::select(real_queue, task_queue).await { - futures_util::future::Either::Left((trigger, _)) => trigger, - futures_util::future::Either::Right((trigger, _)) => trigger, - } - } - }; + // match futures_util::future::select(real_queue, task_queue).await { + // futures_util::future::Either::Left((trigger, _)) => trigger, + // futures_util::future::Either::Right((trigger, _)) => trigger, + // } + // } + // }; - if let Some(real_trigger) = trigger { - log::info!("event received"); + // if let Some(real_trigger) = trigger { + // log::info!("event received"); - internal_dom.queue_event(real_trigger)?; + // internal_dom.queue_event(real_trigger); - let mut edits = Vec::new(); - internal_dom - .progress_with_event(&mut websys_dom, &mut edits) - .await?; - websys_dom.process_edits(&mut edits); - } + // let mut edits = Vec::new(); + // internal_dom + // .progress_with_event(&mut websys_dom, &mut edits) + // .await?; + // websys_dom.process_edits(&mut edits); + // } } // should actually never return from this, should be an error, rustc just cant see it diff --git a/packages/web/src/new.rs b/packages/web/src/new.rs index b196f726b..51110cd11 100644 --- a/packages/web/src/new.rs +++ b/packages/web/src/new.rs @@ -5,7 +5,6 @@ use dioxus_core::{ DomEdit, ElementId, ScopeId, }; use fxhash::FxHashMap; -use slotmap::{DefaultKey, Key, KeyData}; use wasm_bindgen::{closure::Closure, JsCast}; use web_sys::{ window, Document, Element, Event, HtmlElement, HtmlInputElement, HtmlOptionElement, Node, @@ -13,7 +12,7 @@ use web_sys::{ pub struct WebsysDom { pub stack: Stack, - nodes: slotmap::SlotMap, + nodes: Vec, document: Document, root: Element, @@ -49,9 +48,9 @@ impl WebsysDom { }); }); - let mut nodes = slotmap::SlotMap::with_capacity(1000); + let mut nodes = Vec::with_capacity(1000); - let root_id = nodes.insert(root.clone().dyn_into::().unwrap()); + // let root_id = nodes.insert(root.clone().dyn_into::().unwrap()); Self { stack: Stack::with_capacity(10), @@ -98,7 +97,8 @@ impl WebsysDom { } } fn push(&mut self, root: u64) { - let key = DefaultKey::from(KeyData::from_ffi(root)); + // let key = DefaultKey::from(KeyData::from_ffi(root)); + let key = root as usize; let domnode = self.nodes.get_mut(key); let real_node: Node = match domnode { @@ -203,11 +203,9 @@ impl WebsysDom { .dyn_into::() .unwrap(); + let id = id as usize; self.stack.push(textnode.clone()); - *self - .nodes - .get_mut(DefaultKey::from(KeyData::from_ffi(id))) - .unwrap() = textnode; + *self.nodes.get_mut(id).unwrap() = textnode; } fn create_element(&mut self, tag: &str, ns: Option<&'static str>, id: u64) { @@ -226,14 +224,14 @@ impl WebsysDom { .dyn_into::() .unwrap(), }; - let id = DefaultKey::from(KeyData::from_ffi(id)); + let id = id as usize; self.stack.push(el.clone()); *self.nodes.get_mut(id).unwrap() = el; // let nid = self.node_counter.?next(); // let nid = self.nodes.insert(el).data().as_ffi(); // log::debug!("Called [`create_element`]: {}, {:?}", tag, nid); - // RealDomNode::new(nid) + // ElementId::new(nid) } fn new_event_listener( @@ -257,7 +255,9 @@ impl WebsysDom { .dyn_ref::() .expect(&format!("not an element: {:?}", el)); - let scope_id = scope.data().as_ffi(); + // let scope_id = scope.data().as_ffi(); + let scope_id = scope.0 as u64; + el.set_attribute( &format!("dioxus-event-{}", event), &format!("{}.{}", scope_id, real_id), @@ -349,10 +349,10 @@ impl WebsysDom { } impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom { - // fn request_available_node(&mut self) -> RealDomNode { + // fn request_available_node(&mut self) -> ElementId { // let key = self.nodes.insert(None); // log::debug!("making new key: {:#?}", key); - // RealDomNode(key.data().as_ffi()) + // ElementId(key.data().as_ffi()) // } fn raw_node_as_any(&self) -> &mut dyn std::any::Any { @@ -588,6 +588,9 @@ fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent { // let evt: web_sys::ToggleEvent = event.clone().dyn_into().unwrap(); todo!() } + _ => { + todo!() + } } } @@ -632,12 +635,13 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { // Call the trigger log::debug!("decoded scope_id: {}, node_id: {:#?}", gi_id, real_id); - let triggered_scope: ScopeId = KeyData::from_ffi(gi_id).into(); + let triggered_scope = gi_id; + // let triggered_scope: ScopeId = KeyData::from_ffi(gi_id).into(); log::debug!("Triggered scope is {:#?}", triggered_scope); Ok(EventTrigger::new( virtual_event_from_websys_event(event), - triggered_scope, - Some(RealDomNode::from_u64(real_id)), + ScopeId(triggered_scope as usize), + Some(ElementId(real_id as usize)), dioxus_core::events::EventPriority::High, )) } From 0a907b35c647b67172080023462770b761364d51 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 24 Jul 2021 00:40:34 -0400 Subject: [PATCH 06/21] feat: fix some lifetime issues --- packages/core/src/events.rs | 13 +++++++++---- packages/core/src/nodes.rs | 3 ++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 74dca1dab..45ceabf53 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -138,6 +138,7 @@ pub mod on { //! #![allow(unused)] + use bumpalo::boxed::Box as BumpBox; use std::{cell::RefCell, fmt::Debug, ops::Deref, rc::Rc}; use crate::{ @@ -182,14 +183,18 @@ pub mod on { where F: FnMut($wrapper) + 'a { let bump = &c.bump(); + let cb: &mut dyn FnMut(VirtualEvent) = bump.alloc(move |evt: VirtualEvent| match evt { + VirtualEvent::$wrapper(event) => callback(event), + _ => unreachable!("Downcasted VirtualEvent to wrong event type - this is an internal bug!") + }); + + let callback: BumpBox = unsafe { BumpBox::from_raw(cb) }; + Listener { event: stringify!($name), mounted_node: Cell::new(None), scope: c.scope.our_arena_idx, - callback: RefCell::new(bump.alloc(move |evt: VirtualEvent| match evt { - VirtualEvent::$wrapper(event) => callback(event), - _ => unreachable!("Downcasted VirtualEvent to wrong event type - this is an internal bug!") - })), + callback: RefCell::new(callback), } } )* diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index d549aa1dd..36fce44b8 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -7,6 +7,7 @@ use crate::{ events::VirtualEvent, innerlude::{empty_cell, Context, DomTree, ElementId, Properties, Scope, ScopeId, FC}, }; +use bumpalo::boxed::Box as BumpBox; use std::{ cell::{Cell, RefCell}, fmt::{Arguments, Debug, Formatter}, @@ -112,7 +113,7 @@ pub struct Listener<'bump> { pub mounted_node: Cell>, - pub(crate) callback: RefCell<&'bump mut dyn FnMut(VirtualEvent)>, + pub(crate) callback: RefCell>, } /// Virtual Components for custom user-defined components From 7dfe89c9581f45a445f17f9fe4bb94e61f67e971 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 24 Jul 2021 02:52:05 -0400 Subject: [PATCH 07/21] feat: wire up event delegator for webview --- examples/basic.rs | 7 +- examples/calculator.rs | 3 +- examples/model.rs | 1 + examples/webview.rs | 1 + packages/core-macro/src/htm.rs | 12 +- packages/core-macro/src/lib.rs | 1 - packages/core-macro/src/rsx/body.rs | 1 - packages/core-macro/src/rsx/element.rs | 3 +- packages/core-macro/src/util.rs | 179 ------------------------- packages/core/src/arena.rs | 3 +- packages/core/src/diff.rs | 11 +- packages/core/src/events.rs | 7 +- packages/core/src/lib.rs | 6 +- packages/core/src/nodes.rs | 3 +- packages/core/src/scope.rs | 2 + packages/core/src/virtual_dom.rs | 6 +- packages/desktop/Cargo.toml | 2 +- packages/desktop/src/events.rs | 85 ++++++++++++ packages/desktop/src/index.html | 153 ++++++++++++++------- packages/desktop/src/lib.rs | 47 +++++-- packages/hooks/src/usestate.rs | 4 +- 21 files changed, 265 insertions(+), 272 deletions(-) delete mode 100644 packages/core-macro/src/util.rs create mode 100644 packages/desktop/src/events.rs diff --git a/examples/basic.rs b/examples/basic.rs index 63ab7d3fc..12958072a 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -5,10 +5,9 @@ fn main() { use dioxus_elements::{GlobalAttributes, SvgAttributes}; __cx.element( dioxus_elements::button, - __cx.bump() - .alloc([dioxus::events::on::onclick(__cx, move |_| {})]), - __cx.bump().alloc([]), - __cx.bump().alloc([]), + [dioxus::events::on::onclick(__cx, move |_| {})], + [], + [], None, ) }); diff --git a/examples/calculator.rs b/examples/calculator.rs index 67dc79be4..2dbbde02b 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -5,6 +5,7 @@ // use dioxus::prelude::*; fn main() { + env_logger::init(); dioxus::desktop::launch(App, |cfg| cfg); } @@ -23,7 +24,7 @@ const App: FC<()> = |cx| { let operator = use_state(cx, || None as Option); let display_value = use_state(cx, || "".to_string()); - let clear_display = display_value.eq("0"); + let clear_display = display_value == "0"; let clear_text = if clear_display { "C" } else { "AC" }; let input_digit = move |num: u8| display_value.get_mut().push_str(num.to_string().as_str()); diff --git a/examples/model.rs b/examples/model.rs index 7ddd36854..bc5185b32 100644 --- a/examples/model.rs +++ b/examples/model.rs @@ -20,6 +20,7 @@ use dioxus::prelude::*; const STYLE: &str = include_str!("./assets/calculator.css"); fn main() { + env_logger::init(); dioxus::desktop::launch(App, |cfg| cfg.with_title("Calculator Demo")) .expect("failed to launch dioxus app"); } diff --git a/examples/webview.rs b/examples/webview.rs index 2d51f2ead..f10117908 100644 --- a/examples/webview.rs +++ b/examples/webview.rs @@ -12,6 +12,7 @@ use dioxus::prelude::*; fn main() { + env_logger::init(); dioxus::desktop::launch(App, |c| c); } diff --git a/packages/core-macro/src/htm.rs b/packages/core-macro/src/htm.rs index 068a8009d..a5f6dbd8b 100644 --- a/packages/core-macro/src/htm.rs +++ b/packages/core-macro/src/htm.rs @@ -13,8 +13,6 @@ //! //! -use crate::util::is_valid_svg_tag; - use { proc_macro::TokenStream, proc_macro2::{Span, TokenStream as TokenStream2}, @@ -163,11 +161,11 @@ impl ToTokens for ToToksCtx<&Element> { self.recurse(attr).to_tokens(tokens); } - if is_valid_svg_tag(&name.to_string()) { - tokens.append_all(quote! { - .namespace(Some("http://www.w3.org/2000/svg")) - }); - } + // if is_valid_svg_tag(&name.to_string()) { + // tokens.append_all(quote! { + // .namespace(Some("http://www.w3.org/2000/svg")) + // }); + // } match &self.inner.children { MaybeExpr::Expr(expr) => tokens.append_all(quote! { diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 8c3c00bf1..8d124800a 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -7,7 +7,6 @@ pub(crate) mod htm; pub(crate) mod ifmt; pub(crate) mod props; pub(crate) mod rsx; -pub(crate) mod util; #[proc_macro] pub fn format_args_f(input: TokenStream) -> TokenStream { diff --git a/packages/core-macro/src/rsx/body.rs b/packages/core-macro/src/rsx/body.rs index 9ea37585d..8e0e97d46 100644 --- a/packages/core-macro/src/rsx/body.rs +++ b/packages/core-macro/src/rsx/body.rs @@ -1,4 +1,3 @@ -use crate::util::is_valid_tag; use proc_macro2::TokenStream as TokenStream2; use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ diff --git a/packages/core-macro/src/rsx/element.rs b/packages/core-macro/src/rsx/element.rs index 45c391831..6250cb123 100644 --- a/packages/core-macro/src/rsx/element.rs +++ b/packages/core-macro/src/rsx/element.rs @@ -273,8 +273,7 @@ fn parse_rsx_element_field( let ty: AttrType = match name_str.as_str() { // short circuit early if style is using the special syntax - "style" if stream.peek(Token![:]) => { - stream.parse::().unwrap(); + "style" if stream.peek(token::Brace) => { let inner; syn::braced!(inner in stream); diff --git a/packages/core-macro/src/util.rs b/packages/core-macro/src/util.rs deleted file mode 100644 index 1e595f8f8..000000000 --- a/packages/core-macro/src/util.rs +++ /dev/null @@ -1,179 +0,0 @@ -// use lazy_static::lazy_static; -use once_cell::sync::Lazy; -use std::collections::hash_set::HashSet; -use syn::{parse::ParseBuffer, Expr}; - -pub fn try_parse_bracketed(stream: &ParseBuffer) -> syn::Result { - let content; - syn::braced!(content in stream); - content.parse() -} - -/// rsx! and html! macros support the html namespace as well as svg namespace -static HTML_TAGS: Lazy> = Lazy::new(|| { - [ - "a", - "abbr", - "address", - "area", - "article", - "aside", - "audio", - "b", - "base", - "bdi", - "bdo", - "big", - "blockquote", - "body", - "br", - "button", - "canvas", - "caption", - "cite", - "code", - "col", - "colgroup", - "command", - "data", - "datalist", - "dd", - "del", - "details", - "dfn", - "dialog", - "div", - "dl", - "dt", - "em", - "embed", - "fieldset", - "figcaption", - "figure", - "footer", - "form", - "h1", - "h2", - "h3", - "h4", - "h5", - "h6", - "head", - "header", - "hr", - "html", - "i", - "iframe", - "img", - "input", - "ins", - "kbd", - "keygen", - "label", - "legend", - "li", - "link", - "main", - "map", - "mark", - "menu", - "menuitem", - "meta", - "meter", - "nav", - "noscript", - "object", - "ol", - "optgroup", - "option", - "output", - "p", - "param", - "picture", - "pre", - "progress", - "q", - "rp", - "rt", - "ruby", - "s", - "samp", - "script", - "section", - "select", - "small", - "source", - "span", - "strong", - "style", - "sub", - "summary", - "sup", - "table", - "tbody", - "td", - "textarea", - "tfoot", - "th", - "thead", - "time", - "title", - "tr", - "track", - "u", - "ul", - "var", - "video", - "wbr", - ] - .iter() - .cloned() - .collect() -}); - -static SVG_TAGS: Lazy> = Lazy::new(|| { - [ - // SVTG - "svg", "path", "g", "text", - ] - .iter() - .cloned() - .collect() -}); - -// these tags are reserved by dioxus for any reason -// They might not all be used -static RESERVED_TAGS: Lazy> = Lazy::new(|| { - [ - // a fragment - "fragment", - ] - .iter() - .cloned() - .collect() -}); - -/// Whether or not this tag is valid -/// -/// ``` -/// use html_validation::is_valid_tag; -/// -/// assert_eq!(is_valid_tag("br"), true); -/// -/// assert_eq!(is_valid_tag("random"), false); -/// ``` -pub fn is_valid_tag(tag: &str) -> bool { - is_valid_html_tag(tag) || is_valid_svg_tag(tag) || is_valid_reserved_tag(tag) -} - -pub fn is_valid_html_tag(tag: &str) -> bool { - HTML_TAGS.contains(tag) -} - -pub fn is_valid_svg_tag(tag: &str) -> bool { - SVG_TAGS.contains(tag) -} - -pub fn is_valid_reserved_tag(tag: &str) -> bool { - RESERVED_TAGS.contains(tag) -} diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 2fd1f1d2f..0b6f41c1a 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -20,8 +20,7 @@ pub struct ElementId(pub usize); impl ElementId { pub fn as_u64(self) -> u64 { - todo!() - // self.0.as_ffi() + self.0 as u64 } } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 1ce705e4d..3a43d7a1c 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -85,7 +85,7 @@ impl<'r, 'b> DiffMachine<'r, 'b> { pub fn get_scope_mut(&mut self, id: &ScopeId) -> Option<&'b mut Scope> { // ensure we haven't seen this scope before // if we have, then we're trying to alias it, which is not allowed - debug_assert!(!self.seen_nodes.contains(id)); + // debug_assert!(!self.seen_nodes.contains(id)); unsafe { self.vdom.get_scope_mut(*id) } } pub fn get_scope(&mut self, id: &ScopeId) -> Option<&'b Scope> { @@ -120,10 +120,11 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // // each function call assumes the stack is fresh (empty). pub fn diff_node(&mut self, old_node: &'bump VNode<'bump>, new_node: &'bump VNode<'bump>) { - let root = old_node - .dom_id - .get() - .expect("Should not be diffing old nodes that were never assigned"); + // currently busted for components - need to fid + let root = old_node.dom_id.get().expect(&format!( + "Should not be diffing old nodes that were never assigned, {:#?}", + old_node + )); match (&old_node.kind, &new_node.kind) { // Handle the "sane" cases first. diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 45ceabf53..33874b219 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -125,6 +125,8 @@ pub enum VirtualEvent { hook_idx: usize, domnode: Rc>>, }, + // TOOD: make garbage collection its own dedicated event + // GarbageCollection {} } pub mod on { @@ -183,6 +185,7 @@ pub mod on { where F: FnMut($wrapper) + 'a { let bump = &c.bump(); + let cb: &mut dyn FnMut(VirtualEvent) = bump.alloc(move |evt: VirtualEvent| match evt { VirtualEvent::$wrapper(event) => callback(event), _ => unreachable!("Downcasted VirtualEvent to wrong event type - this is an internal bug!") @@ -190,8 +193,10 @@ pub mod on { let callback: BumpBox = unsafe { BumpBox::from_raw(cb) }; + let event_name = stringify!($name); + let shortname: &'static str = &event_name[2..]; Listener { - event: stringify!($name), + event: shortname, mounted_node: Cell::new(None), scope: c.scope.our_arena_idx, callback: RefCell::new(callback), diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 5d36787e2..fa9f4e221 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -10,9 +10,9 @@ //! pub use crate::innerlude::{ - format_args_f, html, rsx, Context, DioxusElement, DomEdit, DomTree, ElementId, EventTrigger, - LazyNodes, NodeFactory, Properties, RealDom, ScopeId, VNode, VNodeKind, VirtualDom, - VirtualEvent, FC, + format_args_f, html, rsx, Context, DioxusElement, DomEdit, DomTree, ElementId, EventPriority, + EventTrigger, LazyNodes, NodeFactory, Properties, RealDom, ScopeId, VNode, VNodeKind, + VirtualDom, VirtualEvent, FC, }; pub mod prelude { diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 36fce44b8..f439a080b 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -249,13 +249,12 @@ impl<'a> NodeFactory<'a> { // We take the references directly from the bump arena // - // // TODO: this code shouldn't necessarily be here of all places // It would make more sense to do this in diffing let mut queue = self.scope.listeners.borrow_mut(); for listener in listeners.iter() { - let long_listener: &Listener<'static> = unsafe { std::mem::transmute(listener) }; + let long_listener: &'a Listener<'static> = unsafe { std::mem::transmute(listener) }; queue.push(long_listener as *const _) } diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index 1a70fc13f..4f15f150d 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -108,6 +108,8 @@ impl Scope { let next_frame = self.frames.prev_frame_mut(); next_frame.bump.reset(); + // make sure we call the drop implementation on all the listeners + // this is important to not leak memory self.listeners.borrow_mut().clear(); unsafe { self.hooks.reset() }; diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index f82026a8d..243b4c5d4 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -258,10 +258,10 @@ impl VirtualDom { // but the guarantees provide a safe, fast, and efficient abstraction for the VirtualDOM updating framework. // // A good project would be to remove all unsafe from this crate and move the unsafety into safer abstractions. - pub async fn progress_with_event<'s>( + pub async fn progress_with_event<'a, 's>( &'s mut self, - realdom: &'s mut dyn RealDom<'s>, - edits: &mut Vec>, + realdom: &'a mut dyn RealDom<'s>, + edits: &'a mut Vec>, ) -> Result<()> { let trigger = self.triggers.borrow_mut().pop().expect("failed"); diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 4c44aecb1..2c12663ba 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -20,13 +20,13 @@ log = "0.4.13" fern = { version = "0.6.0", features = ["colored"] } html-escape = "0.2.9" wry = "0.11.0" +async-std = { version = "1.9.0", features = ["attributes"] } [dev-dependencies] dioxus-html = { path = "../html" } tide = "0.15.0" tide-websockets = "0.3.0" -async-std = { version = "1.9.0", features = ["attributes"] } # thiserror = "1.0.23" # log = "0.4.13" diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs new file mode 100644 index 000000000..308726f51 --- /dev/null +++ b/packages/desktop/src/events.rs @@ -0,0 +1,85 @@ +//! Convert a serialized event to an event Trigger +//! + +use std::rc::Rc; + +use dioxus_core::{ + events::{ + on::{MouseEvent, MouseEventInner}, + VirtualEvent, + }, + ElementId, EventPriority, EventTrigger, ScopeId, +}; + +#[derive(serde::Serialize, serde::Deserialize)] +struct ImEvent { + event: String, + mounted_dom_id: u64, + scope: u64, +} +pub fn trigger_from_serialized(val: serde_json::Value) -> EventTrigger { + let mut data: Vec = serde_json::from_value(val).unwrap(); + let data = data.drain(..).next().unwrap(); + + let event = VirtualEvent::MouseEvent(MouseEvent(Rc::new(WebviewMouseEvent))); + let scope = ScopeId(data.scope as usize); + let mounted_dom_id = Some(ElementId(data.mounted_dom_id as usize)); + let priority = EventPriority::High; + EventTrigger::new(event, scope, mounted_dom_id, priority) +} + +#[derive(Debug)] +struct WebviewMouseEvent; +impl MouseEventInner for WebviewMouseEvent { + fn alt_key(&self) -> bool { + todo!() + } + + fn button(&self) -> i16 { + todo!() + } + + fn buttons(&self) -> u16 { + todo!() + } + + fn client_x(&self) -> i32 { + todo!() + } + + fn client_y(&self) -> i32 { + todo!() + } + + fn ctrl_key(&self) -> bool { + todo!() + } + + fn meta_key(&self) -> bool { + todo!() + } + + fn page_x(&self) -> i32 { + todo!() + } + + fn page_y(&self) -> i32 { + todo!() + } + + fn screen_x(&self) -> i32 { + todo!() + } + + fn screen_y(&self) -> i32 { + todo!() + } + + fn shift_key(&self) -> bool { + todo!() + } + + fn get_modifier_state(&self, key_code: &str) -> bool { + todo!() + } +} diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html index 584685419..c734af71c 100644 --- a/packages/desktop/src/index.html +++ b/packages/desktop/src/index.html @@ -5,10 +5,13 @@

+ diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 83283932f..afcfc8383 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -3,6 +3,7 @@ use std::ops::{Deref, DerefMut}; use std::sync::mpsc::channel; use std::sync::{Arc, RwLock}; +use cfg::DesktopConfig; use dioxus_core::*; pub use wry; @@ -15,6 +16,7 @@ use wry::{ webview::{RpcRequest, RpcResponse}, }; +mod cfg; mod dom; mod escape; mod events; @@ -24,14 +26,14 @@ static HTML_CONTENT: &'static str = include_str!("./index.html"); pub fn launch( root: FC<()>, - builder: impl FnOnce(WindowBuilder) -> WindowBuilder, + builder: impl for<'a, 'b> FnOnce(&'b mut DesktopConfig<'a>) -> &'b mut DesktopConfig<'a>, ) -> anyhow::Result<()> { launch_with_props(root, (), builder) } pub fn launch_with_props( root: FC

, props: P, - builder: impl FnOnce(WindowBuilder) -> WindowBuilder, + builder: impl for<'a, 'b> FnOnce(&'b mut DesktopConfig<'a>) -> &'b mut DesktopConfig<'a>, ) -> anyhow::Result<()> { WebviewRenderer::run(root, props, builder) } @@ -46,11 +48,17 @@ enum RpcEvent<'a> { Initialize { edits: Vec> }, } +#[derive(Serialize)] +struct Response<'a> { + pre_rendered: Option, + edits: Vec>, +} + impl WebviewRenderer { pub fn run( root: FC, props: T, - user_builder: impl FnOnce(WindowBuilder) -> WindowBuilder, + user_builder: impl for<'a, 'b> FnOnce(&'b mut DesktopConfig<'a>) -> &'b mut DesktopConfig<'a>, ) -> anyhow::Result<()> { Self::run_with_edits(root, props, user_builder, None) } @@ -58,13 +66,22 @@ impl WebviewRenderer { pub fn run_with_edits( root: FC, props: T, - user_builder: impl FnOnce(WindowBuilder) -> WindowBuilder, + user_builder: impl for<'a, 'b> FnOnce(&'a mut DesktopConfig<'b>) -> &'a mut DesktopConfig<'b>, redits: Option>>, ) -> anyhow::Result<()> { log::info!("hello edits"); let event_loop = EventLoop::new(); - let window = user_builder(WindowBuilder::new()).build(&event_loop)?; + let mut cfg = DesktopConfig::new(); + user_builder(&mut cfg); + + let DesktopConfig { + window, + manual_edits, + pre_rendered, + } = cfg; + + let window = window.build(&event_loop)?; let vir = VirtualDom::new_with_props(root, props); @@ -73,8 +90,6 @@ impl WebviewRenderer { // let registry = Arc::new(RwLock::new(Some(WebviewRegistry::new()))); let webview = WebViewBuilder::new(window)? - // .with_visible(false) - // .with_transparent(true) .with_url(&format!("data:text/html,{}", HTML_CONTENT))? .with_rpc_handler(move |_window: &Window, mut req: RpcRequest| { match req.method.as_str() { @@ -87,17 +102,32 @@ impl WebviewRenderer { // Create the thin wrapper around the registry to collect the edits into let mut real = dom::WebviewDom::new(); + let pre = pre_rendered.clone(); - // Serialize the edit stream - let edits = { - let mut edits = Vec::new(); - lock.rebuild(&mut real, &mut edits).unwrap(); - serde_json::to_value(edits).unwrap() + let response = match pre { + Some(content) => { + lock.rebuild_in_place().unwrap(); + + Response { + edits: Vec::new(), + pre_rendered: Some(content), + } + } + None => { + // + let edits = { + let mut edits = Vec::new(); + lock.rebuild(&mut real, &mut edits).unwrap(); + edits + }; + Response { + edits, + pre_rendered: None, + } + } }; - // Give back the registry into its slot - // *reg_lock = Some(real.consume()); - edits + serde_json::to_value(&response).unwrap() }; // Return the edits into the webview runtime @@ -128,13 +158,18 @@ impl WebviewRenderer { lock.progress_with_event(&mut real, &mut edits) .await .expect("failed to progress"); - let edits = serde_json::to_value(edits).unwrap(); + + let response = Response { + edits, + pre_rendered: None, + }; + let response = serde_json::to_value(&response).unwrap(); // Give back the registry into its slot // *reg_lock = Some(real.consume()); // Return the edits into the webview runtime - Some(RpcResponse::new_result(req.id.take(), Some(edits))) + Some(RpcResponse::new_result(req.id.take(), Some(response))) }); response diff --git a/packages/ssr/README.md b/packages/ssr/README.md index 451f9f250..77fbc97e1 100644 --- a/packages/ssr/README.md +++ b/packages/ssr/README.md @@ -16,3 +16,9 @@ vdom.rebuild_in_place(); let text = dioxus_ssr::render_root(&vdom); assert_eq!(text, "

hello world!
") ``` + + + +## Pre-rendering + + diff --git a/packages/ssr/examples/hydration.rs b/packages/ssr/examples/hydration.rs new file mode 100644 index 000000000..92eee69fa --- /dev/null +++ b/packages/ssr/examples/hydration.rs @@ -0,0 +1,10 @@ +//! Example: realworld usage of hydration +//! +//! +use dioxus::virtual_dom::VirtualDom; +use dioxus_core as dioxus; +use dioxus_core::prelude::*; +use dioxus_hooks::use_state; +use dioxus_html as dioxus_elements; + +fn main() {} diff --git a/packages/ssr/examples/tide.rs b/packages/ssr/examples/tide.rs index b6c96f8c3..26f742bcf 100644 --- a/packages/ssr/examples/tide.rs +++ b/packages/ssr/examples/tide.rs @@ -22,7 +22,7 @@ async fn main() -> Result<(), std::io::Error> { let dom = VirtualDom::launch_with_props_in_place(Example, ExampleProps { initial_name }); Ok(Response::builder(200) - .body(format!("{}", dioxus_ssr::render_vdom(&dom))) + .body(format!("{}", dioxus_ssr::render_vdom(&dom, |c| c))) .content_type(tide::http::mime::HTML) .build()) }); diff --git a/packages/ssr/examples/tofile.rs b/packages/ssr/examples/tofile.rs index eded9265c..35ce6db89 100644 --- a/packages/ssr/examples/tofile.rs +++ b/packages/ssr/examples/tofile.rs @@ -15,8 +15,11 @@ fn main() { let mut dom = VirtualDom::new(App); dom.rebuild_in_place().expect("failed to run virtualdom"); - file.write_fmt(format_args!("{}", TextRenderer::from_vdom(&dom))) - .unwrap(); + file.write_fmt(format_args!( + "{}", + TextRenderer::from_vdom(&dom, Default::default()) + )) + .unwrap(); } pub static App: FC<()> = |cx| { diff --git a/packages/ssr/src/lib.rs b/packages/ssr/src/lib.rs index 3d81c33b6..d8f4d700f 100644 --- a/packages/ssr/src/lib.rs +++ b/packages/ssr/src/lib.rs @@ -1,3 +1,7 @@ +//! +//! +//! +//! //! This crate demonstrates how to implement a custom renderer for Dioxus VNodes via the `TextRenderer` renderer. //! The `TextRenderer` consumes a Dioxus Virtual DOM, progresses its event queue, and renders the VNodes to a String. //! @@ -12,8 +16,11 @@ use dioxus_core::*; pub fn render_vnode(vnode: &VNode, string: &mut String) {} -pub fn render_vdom(dom: &VirtualDom) -> String { - format!("{:}", TextRenderer::from_vdom(dom)) +pub fn render_vdom(dom: &VirtualDom, cfg: impl FnOnce(SsrConfig) -> SsrConfig) -> String { + format!( + "{:}", + TextRenderer::from_vdom(dom, cfg(SsrConfig::default())) + ) } pub fn render_vdom_scope(vdom: &VirtualDom, scope: ScopeId) -> Option { @@ -27,29 +34,6 @@ pub fn render_vdom_scope(vdom: &VirtualDom, scope: ScopeId) -> Option { )) } -pub struct SsrConfig { - // currently not supported - control if we indent the HTML output - indent: bool, - - // Control if elements are written onto a new line - newline: bool, - - // Currently not implemented - // Don't proceed onto new components. Instead, put the name of the component. - // TODO: components don't have names :( - _skip_components: bool, -} - -impl Default for SsrConfig { - fn default() -> Self { - Self { - indent: false, - - newline: false, - _skip_components: false, - } - } -} /// A configurable text renderer for the Dioxus VirtualDOM. /// /// @@ -60,7 +44,7 @@ impl Default for SsrConfig { /// /// ## Example /// ```ignore -/// const App: FC<()> = |cx| cx.render(rsx!(div { "hello world" })); +/// static App: FC<()> = |cx| cx.render(rsx!(div { "hello world" })); /// let mut vdom = VirtualDom::new(App); /// vdom.rebuild_in_place(); /// @@ -81,11 +65,11 @@ impl Display for TextRenderer<'_> { } impl<'a> TextRenderer<'a> { - pub fn from_vdom(vdom: &'a VirtualDom) -> Self { + pub fn from_vdom(vdom: &'a VirtualDom, cfg: SsrConfig) -> Self { Self { + cfg, root: vdom.base_scope().root(), vdom: Some(vdom), - cfg: SsrConfig::default(), } } @@ -132,6 +116,21 @@ impl<'a> TextRenderer<'a> { } } + // we write the element's id as a data attribute + // + // when the page is loaded, the `querySelectorAll` will be used to collect all the nodes, and then add + // them interpreter's stack + match (self.cfg.pre_render, node.element_id()) { + (true, Some(id)) => { + write!(f, " dio_el=\"{}\"", id)?; + // + for listener in el.listeners { + // write the listeners + } + } + _ => {} + } + match self.cfg.newline { true => write!(f, ">\n")?, false => write!(f, ">")?, @@ -162,9 +161,14 @@ impl<'a> TextRenderer<'a> { } VNodeKind::Component(vcomp) => { let idx = vcomp.ass_scope.get().unwrap(); - if let Some(vdom) = self.vdom { - let new_node = vdom.get_scope(idx).unwrap().root(); - self.html_render(new_node, f, il + 1)?; + match (self.vdom, self.cfg.skip_components) { + (Some(vdom), false) => { + let new_node = vdom.get_scope(idx).unwrap().root(); + self.html_render(new_node, f, il + 1)?; + } + _ => { + // render the component by name + } } } VNodeKind::Suspended { .. } => { @@ -175,6 +179,52 @@ impl<'a> TextRenderer<'a> { } } +pub struct SsrConfig { + // currently not supported - control if we indent the HTML output + indent: bool, + + // Control if elements are written onto a new line + newline: bool, + + // Choose to write ElementIDs into elements so the page can be re-hydrated later on + pre_render: bool, + + // Currently not implemented + // Don't proceed onto new components. Instead, put the name of the component. + // TODO: components don't have names :( + skip_components: bool, +} + +impl Default for SsrConfig { + fn default() -> Self { + Self { + indent: false, + pre_render: false, + newline: false, + skip_components: false, + } + } +} + +impl SsrConfig { + pub fn indent(mut self, a: bool) -> Self { + self.indent = a; + self + } + pub fn newline(mut self, a: bool) -> Self { + self.newline = a; + self + } + pub fn pre_render(mut self, a: bool) -> Self { + self.pre_render = a; + self + } + pub fn skip_components(mut self, a: bool) -> Self { + self.skip_components = a; + self + } +} + #[cfg(test)] mod tests { use super::*; @@ -182,15 +232,14 @@ mod tests { use dioxus_core as dioxus; use dioxus_core::prelude::*; use dioxus_html as dioxus_elements; - use dioxus_html::GlobalAttributes; - const SIMPLE_APP: FC<()> = |cx| { + static SIMPLE_APP: FC<()> = |cx| { cx.render(rsx!(div { "hello world!" })) }; - const SLIGHTLY_MORE_COMPLEX: FC<()> = |cx| { + static SLIGHTLY_MORE_COMPLEX: FC<()> = |cx| { cx.render(rsx! { div { title: "About W3Schools" @@ -209,14 +258,14 @@ mod tests { }) }; - const NESTED_APP: FC<()> = |cx| { + static NESTED_APP: FC<()> = |cx| { cx.render(rsx!( div { SIMPLE_APP {} } )) }; - const FRAGMENT_APP: FC<()> = |cx| { + static FRAGMENT_APP: FC<()> = |cx| { cx.render(rsx!( div { "f1" } div { "f2" } @@ -229,21 +278,28 @@ mod tests { fn to_string_works() { let mut dom = VirtualDom::new(SIMPLE_APP); dom.rebuild_in_place().expect("failed to run virtualdom"); - dbg!(render_vdom(&dom)); + dbg!(render_vdom(&dom, |c| c)); + } + + #[test] + fn hydration() { + let mut dom = VirtualDom::new(NESTED_APP); + dom.rebuild_in_place().expect("failed to run virtualdom"); + dbg!(render_vdom(&dom, |c| c.pre_render(true))); } #[test] fn nested() { let mut dom = VirtualDom::new(NESTED_APP); dom.rebuild_in_place().expect("failed to run virtualdom"); - dbg!(render_vdom(&dom)); + dbg!(render_vdom(&dom, |c| c)); } #[test] fn fragment_app() { let mut dom = VirtualDom::new(FRAGMENT_APP); dom.rebuild_in_place().expect("failed to run virtualdom"); - dbg!(render_vdom(&dom)); + dbg!(render_vdom(&dom, |c| c)); } #[test] @@ -256,26 +312,23 @@ mod tests { let mut dom = VirtualDom::new(SLIGHTLY_MORE_COMPLEX); dom.rebuild_in_place().expect("failed to run virtualdom"); - file.write_fmt(format_args!("{}", TextRenderer::from_vdom(&dom))) - .unwrap(); + file.write_fmt(format_args!( + "{}", + TextRenderer::from_vdom(&dom, SsrConfig::default()) + )) + .unwrap(); } - // #[test] - // fn styles() { - // const STLYE_APP: FC<()> = |cx| { - // // - // cx.render(rsx! { - // div { - // style: { - // color: "blue", - // font_size: "46px" - // } - // } - // }) - // }; + #[test] + fn styles() { + static STLYE_APP: FC<()> = |cx| { + cx.render(rsx! { + div { style: { color: "blue", font_size: "46px" } } + }) + }; - // let mut dom = VirtualDom::new(STLYE_APP); - // dom.rebuild_in_place().expect("failed to run virtualdom"); - // dbg!(render_vdom(&dom)); - // } + let mut dom = VirtualDom::new(STLYE_APP); + dom.rebuild_in_place().expect("failed to run virtualdom"); + dbg!(render_vdom(&dom, |c| c)); + } } diff --git a/packages/web/src/cfg.rs b/packages/web/src/cfg.rs new file mode 100644 index 000000000..002dc3535 --- /dev/null +++ b/packages/web/src/cfg.rs @@ -0,0 +1,21 @@ +pub struct WebConfig { + pub(crate) hydrate: bool, +} +impl Default for WebConfig { + fn default() -> Self { + Self { hydrate: false } + } +} +impl WebConfig { + /// Enable SSR hydration + /// + /// This enables Dioxus to pick up work from a pre-renderd HTML file. Hydration will completely skip over any async + /// work and suspended nodes. + /// + /// Dioxus will load up all the elements with the `dio_el` data attribute into memory when the page is loaded. + /// + pub fn hydrate(mut self, f: bool) -> Self { + self.hydrate = f; + self + } +} diff --git a/packages/web/src/new.rs b/packages/web/src/dom.rs similarity index 91% rename from packages/web/src/new.rs rename to packages/web/src/dom.rs index 51110cd11..2ae5ca822 100644 --- a/packages/web/src/new.rs +++ b/packages/web/src/dom.rs @@ -8,12 +8,19 @@ use fxhash::FxHashMap; use wasm_bindgen::{closure::Closure, JsCast}; use web_sys::{ window, Document, Element, Event, HtmlElement, HtmlInputElement, HtmlOptionElement, Node, + NodeList, }; +use crate::{nodeslab::NodeSlab, WebConfig}; + pub struct WebsysDom { - pub stack: Stack, - nodes: Vec, + stack: Stack, + + /// A map from ElementID (index) to Node + nodes: NodeSlab, + document: Document, + root: Element, event_receiver: async_channel::Receiver, @@ -33,11 +40,8 @@ pub struct WebsysDom { last_node_was_text: bool, } impl WebsysDom { - pub fn new(root: Element) -> Self { - let document = window() - .expect("must have access to the window") - .document() - .expect("must have access to the Document"); + pub fn new(root: Element, cfg: WebConfig) -> Self { + let document = load_document(); let (sender, receiver) = async_channel::unbounded::(); @@ -48,14 +52,36 @@ impl WebsysDom { }); }); - let mut nodes = Vec::with_capacity(1000); + let mut nodes = NodeSlab::new(2000); + let mut listeners = FxHashMap::default(); - // let root_id = nodes.insert(root.clone().dyn_into::().unwrap()); + // re-hydrate the page - only supports one virtualdom per page + if cfg.hydrate { + // Load all the elements into the arena + let node_list: NodeList = document.query_selector_all("dio_el").unwrap(); + let len = node_list.length() as usize; + + for x in 0..len { + let node: Node = node_list.get(x as u32).unwrap(); + let el: &Element = node.dyn_ref::().unwrap(); + let id: String = el.get_attribute("dio_el").unwrap(); + let id = id.parse::().unwrap(); + + // this autoresizes the vector if needed + nodes[id] = Some(node); + } + + // Load all the event listeners into our listener register + } + + let mut stack = Stack::with_capacity(10); + let root_node = root.clone().dyn_into::().unwrap(); + stack.push(root_node); Self { - stack: Stack::with_capacity(10), + stack, nodes, - listeners: FxHashMap::default(), + listeners, document, event_receiver: receiver, trigger: sender_callback, @@ -97,19 +123,14 @@ impl WebsysDom { } } fn push(&mut self, root: u64) { - // let key = DefaultKey::from(KeyData::from_ffi(root)); let key = root as usize; - let domnode = self.nodes.get_mut(key); + let domnode = &self.nodes[key]; let real_node: Node = match domnode { Some(n) => n.clone(), None => todo!(), }; - // let domnode = domnode.unwrap().as_mut().unwrap(); - // .expect(&format!("Failed to pop know root: {:#?}", key)) - // .unwrap(); - self.stack.push(real_node); } // drop the node off the stack @@ -205,7 +226,7 @@ impl WebsysDom { let id = id as usize; self.stack.push(textnode.clone()); - *self.nodes.get_mut(id).unwrap() = textnode; + self.nodes[id] = Some(textnode); } fn create_element(&mut self, tag: &str, ns: Option<&'static str>, id: u64) { @@ -227,7 +248,7 @@ impl WebsysDom { let id = id as usize; self.stack.push(el.clone()); - *self.nodes.get_mut(id).unwrap() = el; + self.nodes[id] = Some(el); // let nid = self.node_counter.?next(); // let nid = self.nodes.insert(el).data().as_ffi(); // log::debug!("Called [`create_element`]: {}, {:?}", tag, nid); @@ -264,6 +285,9 @@ impl WebsysDom { ) .unwrap(); + el.set_attribute(&format!("dioxus-event"), &format!("{}", event)) + .unwrap(); + // Register the callback to decode if let Some(entry) = self.listeners.get_mut(event) { @@ -512,26 +536,6 @@ fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent { } } VirtualEvent::MouseEvent(MouseEvent(Rc::new(CustomMouseEvent(evt)))) - // MouseEvent(Box::new(RawMouseEvent { - // alt_key: evt.alt_key(), - // button: evt.button() as i32, - // buttons: evt.buttons() as i32, - // 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(), - // get_modifier_state: GetModifierKey(Box::new(|f| { - // // evt.get_modifier_state(f) - // todo!("This is not yet implemented properly, sorry :("); - // })), - // })) - // todo!() - // VirtualEvent::MouseEvent() } "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" @@ -645,3 +649,14 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { dioxus_core::events::EventPriority::High, )) } + +pub fn prepare_websys_dom() -> Element { + load_document().get_element_by_id("dioxusroot").unwrap() +} + +pub fn load_document() -> Document { + web_sys::window() + .expect("should have access to the Window") + .document() + .expect("should have access to the Document") +} diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 1a41b930b..5b6229a2f 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -2,38 +2,66 @@ //! -------------- //! This crate implements a renderer of the Dioxus Virtual DOM for the web browser using Websys. +pub use crate::cfg::WebConfig; +use crate::dom::load_document; use dioxus::prelude::{Context, Properties, VNode}; use dioxus::virtual_dom::VirtualDom; pub use dioxus_core as dioxus; +use dioxus_core::error::Result; use dioxus_core::{events::EventTrigger, prelude::FC}; use futures_util::{pin_mut, Stream, StreamExt}; use fxhash::FxHashMap; -use web_sys::{window, Document, Element, Event, Node}; +use js_sys::Iterator; +use web_sys::{window, Document, Element, Event, Node, NodeList}; mod cache; -mod new; +mod cfg; +mod dom; +mod nodeslab; /// Launches the VirtualDOM from the specified component function. /// /// This method will block the thread with `spawn_local` +/// +/// # Example +/// +/// +/// pub fn launch(root: FC<()>, config: F) where - F: FnOnce(()), + F: FnOnce(WebConfig) -> WebConfig, { - wasm_bindgen_futures::spawn_local(run(root)) + launch_with_props(root, (), config) } +/// Launches the VirtualDOM from the specified component function and props. +/// +/// This method will block the thread with `spawn_local` +/// +/// # Example +/// +/// pub fn launch_with_props(root: FC, root_props: T, config: F) where T: Properties + 'static, - F: FnOnce(()), + F: FnOnce(WebConfig) -> WebConfig, { - wasm_bindgen_futures::spawn_local(run_with_props(root, root_props)) + let config = config(WebConfig::default()); + let fut = run_with_props(root, root_props, config); + + wasm_bindgen_futures::spawn_local(async { + match fut.await { + Ok(_) => log::error!("Your app completed running... somehow?"), + Err(e) => log::error!("Your app crashed! {}", e), + } + }); } /// This method is the primary entrypoint for Websys Dioxus apps. Will panic if an error occurs while rendering. /// See DioxusErrors for more information on how these errors could occour. /// +/// # Example +/// /// ```ignore /// fn main() { /// wasm_bindgen_futures::spawn_local(WebsysRenderer::start(Example)); @@ -42,29 +70,19 @@ where /// /// Run the app to completion, panicing if any error occurs while rendering. /// Pairs well with the wasm_bindgen async handler -pub async fn run(root: FC<()>) { - run_with_props(root, ()).await; -} - -pub async fn run_with_props(root: FC, root_props: T) { +pub async fn run_with_props( + root: FC, + root_props: T, + cfg: WebConfig, +) -> Result<()> { let dom = VirtualDom::new_with_props(root, root_props); - event_loop(dom).await.expect("Event loop failed"); -} -pub async fn event_loop(mut internal_dom: VirtualDom) -> dioxus_core::error::Result<()> { - use wasm_bindgen::JsCast; + let root_el = load_document().get_element_by_id("dioxus_root").unwrap(); + let mut websys_dom = dom::WebsysDom::new(root_el, cfg); - let root = prepare_websys_dom(); - let root_node = root.clone().dyn_into::().unwrap(); - - let mut websys_dom = crate::new::WebsysDom::new(root.clone()); - - websys_dom.stack.push(root_node.clone()); - websys_dom.stack.push(root_node); - - let mut edits = Vec::new(); - internal_dom.rebuild(&mut websys_dom, &mut edits)?; - websys_dom.process_edits(&mut edits); + // let mut edits = Vec::new(); + // internal_dom.rebuild(&mut websys_dom, &mut edits)?; + // websys_dom.process_edits(&mut edits); log::info!("Going into event loop"); loop { @@ -105,11 +123,14 @@ pub async fn event_loop(mut internal_dom: VirtualDom) -> dioxus_core::error::Res Ok(()) } -fn prepare_websys_dom() -> Element { - web_sys::window() - .expect("should have access to the Window") - .document() - .expect("should have access to the Document") - .get_element_by_id("dioxusroot") - .unwrap() +fn iter_node_list() {} + +// struct NodeListIter { +// node_list: NodeList, +// } +// impl Iterator for NodeListIter {} + +struct HydrationNode { + id: usize, + node: Node, } diff --git a/packages/web/src/nodeslab.rs b/packages/web/src/nodeslab.rs new file mode 100644 index 000000000..5f6eb2496 --- /dev/null +++ b/packages/web/src/nodeslab.rs @@ -0,0 +1,41 @@ +use std::ops::{Index, IndexMut}; + +use web_sys::Node; + +pub struct NodeSlab { + nodes: Vec>, +} + +impl NodeSlab { + pub fn new(capacity: usize) -> NodeSlab { + NodeSlab { + nodes: Vec::with_capacity(capacity), + } + } + + fn insert_and_extend(&mut self, node: Node, id: usize) { + if id > self.nodes.len() * 3 { + panic!("Trying to insert an element way too far out of bounds"); + } + + if id < self.nodes.len() {} + } +} +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.len() * 3 { + panic!("Trying to mutate an element way too far out of bounds"); + } + if index > self.nodes.len() { + self.nodes.resize_with(index, || None); + } + &mut self.nodes[index] + } +} From 63a85e4865929dbd32a1b72ebeac271561e9ed92 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 29 Jul 2021 00:04:59 -0400 Subject: [PATCH 12/21] wip: more work on bumpframe --- packages/core/src/bumpframe.rs | 14 +- packages/core/src/diff.rs | 325 +++++++++++++++++++++---------- packages/core/src/editor.rs | 138 +------------ packages/core/src/virtual_dom.rs | 6 +- 4 files changed, 230 insertions(+), 253 deletions(-) diff --git a/packages/core/src/bumpframe.rs b/packages/core/src/bumpframe.rs index f1ff888d3..59a6252a5 100644 --- a/packages/core/src/bumpframe.rs +++ b/packages/core/src/bumpframe.rs @@ -15,7 +15,7 @@ pub struct BumpFrame { pub(crate) head_node: VNode<'static>, // used internally for debugging - name: &'static str, + _name: &'static str, } impl ActiveFrame { @@ -23,12 +23,12 @@ impl ActiveFrame { let frame_a = BumpFrame { bump: Bump::new(), head_node: NodeFactory::unstable_place_holder(), - name: "wip", + _name: "wip", }; let frame_b = BumpFrame { bump: Bump::new(), head_node: NodeFactory::unstable_place_holder(), - name: "fin", + _name: "fin", }; Self { generation: 0.into(), @@ -103,14 +103,14 @@ mod tests { for _ in 0..5 { let fin = frames.finished_frame(); let wip = frames.wip_frame(); - assert_eq!(wip.name, "wip"); - assert_eq!(fin.name, "fin"); + assert_eq!(wip._name, "wip"); + assert_eq!(fin._name, "fin"); frames.cycle_frame(); let fin = frames.finished_frame(); let wip = frames.wip_frame(); - assert_eq!(wip.name, "fin"); - assert_eq!(fin.name, "wip"); + assert_eq!(wip._name, "fin"); + assert_eq!(fin._name, "wip"); frames.cycle_frame(); } assert_eq!(frames.generation.get(), 10); diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index e872a6232..3288602cb 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -55,7 +55,8 @@ use crate::{arena::SharedResources, innerlude::*}; use fxhash::{FxHashMap, FxHashSet}; use smallvec::{smallvec, SmallVec}; -use std::{any::Any, cmp::Ordering, process::Child}; +use std::{any::Any, cell::Cell, cmp::Ordering}; +use DomEdit::*; /// Instead of having handles directly over nodes, Dioxus uses simple u32 as node IDs. /// The expectation is that the underlying renderer will mainain their Nodes in vec where the ids are the index. This allows @@ -74,7 +75,7 @@ pub struct DiffMachine<'real, 'bump> { pub vdom: &'bump SharedResources, - pub edits: DomEditor<'real, 'bump>, + pub edits: &'real mut Vec>, pub scheduled_garbage: Vec<&'bump VNode<'bump>>, @@ -94,7 +95,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { ) -> Self { Self { real_dom: dom, - edits: DomEditor::new(edits), + edits, cur_idxs: smallvec![cur_idx], vdom: shared, scheduled_garbage: vec![], @@ -120,9 +121,9 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { let root = root.unwrap(); if old.text != new.text { - self.edits.push_root(root); - self.edits.set_text(new.text); - self.edits.pop(); + self.push_root(root); + self.set_text(new.text); + self.pop(); } new_node.dom_id.set(Some(root)); @@ -136,11 +137,11 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // // This case is rather rare (typically only in non-keyed lists) if new.tag_name != old.tag_name || new.namespace != old.namespace { - self.edits.push_root(root); + self.push_root(root); let meta = self.create(new_node); - self.edits.replace_with(meta.added_to_stack); + self.replace_with(meta.added_to_stack); self.scheduled_garbage.push(old_node); - self.edits.pop(); + self.pop(); return; } @@ -148,10 +149,10 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // Don't push the root if we don't have to let mut has_comitted = false; - let mut please_commit = |edits: &mut DomEditor| { + let mut please_commit = |edits: &mut Vec| { if !has_comitted { has_comitted = true; - edits.push_root(root); + edits.push(PushRoot { id: root.as_u64() }); } }; @@ -165,17 +166,17 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { for (old_attr, new_attr) in old.attributes.iter().zip(new.attributes.iter()) { if old_attr.value != new_attr.value { please_commit(&mut self.edits); - self.edits.set_attribute(new_attr); + self.set_attribute(new_attr); } } } else { // TODO: provide some sort of report on how "good" the diffing was please_commit(&mut self.edits); for attribute in old.attributes { - self.edits.remove_attribute(attribute); + self.remove_attribute(attribute); } for attribute in new.attributes { - self.edits.set_attribute(attribute) + self.set_attribute(attribute) } } @@ -189,29 +190,29 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) { if old_l.event != new_l.event { please_commit(&mut self.edits); - self.edits.remove_event_listener(old_l.event); - self.edits.new_event_listener(new_l); + self.remove_event_listener(old_l.event); + self.new_event_listener(new_l); } new_l.mounted_node.set(old_l.mounted_node.get()); } } else { please_commit(&mut self.edits); for listener in old.listeners { - self.edits.remove_event_listener(listener.event); + self.remove_event_listener(listener.event); } for listener in new.listeners { listener.mounted_node.set(Some(root)); - self.edits.new_event_listener(listener); + self.new_event_listener(listener); } } if has_comitted { - self.edits.pop(); + self.pop(); } // Each child pushes its own root, so it doesn't need our current root - todo!(); - // self.diff_children(old.children, new.children); + + self.diff_children(old.children, new.children, None, &mut None); } (VNodeKind::Component(old), VNodeKind::Component(new)) => { @@ -249,7 +250,6 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { self.seen_nodes.insert(scope_addr); } else { - // this seems to be a fairy common code path that we could let mut old_iter = RealChildIterator::new(old_node, &self.vdom); let first = old_iter .next() @@ -257,16 +257,16 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // remove any leftovers for to_remove in old_iter { - self.edits.push_root(to_remove.element_id().unwrap()); - self.edits.remove(); + self.push_root(to_remove.element_id().unwrap()); + self.remove(); } // seems like we could combine this into a single instruction.... - self.edits.push_root(first.element_id().unwrap()); + self.push_root(first.element_id().unwrap()); let meta = self.create(new_node); - self.edits.replace_with(meta.added_to_stack); + self.replace_with(meta.added_to_stack); self.scheduled_garbage.push(old_node); - self.edits.pop(); + self.pop(); // Wipe the old one and plant the new one let old_scope = old.ass_scope.get().unwrap(); @@ -282,7 +282,16 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { return; } - self.diff_children(old, new, old, new_anchor) + let mut new_anchor = None; + self.diff_children( + old.children, + new.children, + old_node.dom_id.get(), + &mut new_anchor, + ); + if let Some(anchor) = new_anchor { + new_node.dom_id.set(Some(anchor)); + } } // The strategy here is to pick the first possible node from the previous set and use that as our replace with root @@ -326,8 +335,8 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // remove any leftovers for to_remove in old_iter { - self.edits.push_root(to_remove.element_id().unwrap()); - self.edits.remove(); + self.push_root(to_remove.element_id().unwrap()); + self.remove(); } back_node.element_id().unwrap() @@ -335,9 +344,9 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { }; // replace the placeholder or first node with the nodes generated from the "new" - self.edits.push_root(back_node_id); + self.push_root(back_node_id); let meta = self.create(new_node); - self.edits.replace_with(meta.added_to_stack); + self.replace_with(meta.added_to_stack); // todo use the is_static metadata to update this subtree } @@ -365,7 +374,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { match &node.kind { VNodeKind::Text(text) => { let real_id = self.vdom.reserve_node(); - self.edits.create_text_node(text.text, real_id); + self.create_text_node(text.text, real_id); node.dom_id.set(Some(real_id)); CreateMeta::new(text.is_static, 1) @@ -391,17 +400,16 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { let real_id = self.vdom.reserve_node(); if let Some(namespace) = namespace { - self.edits - .create_element(tag_name, Some(namespace), real_id) + self.create_element(tag_name, Some(namespace), real_id) } else { - self.edits.create_element(tag_name, None, real_id) + self.create_element(tag_name, None, real_id) }; node.dom_id.set(Some(real_id)); listeners.iter().for_each(|listener| { log::info!("setting listener id to {:#?}", real_id); listener.mounted_node.set(Some(real_id)); - self.edits.new_event_listener(listener); + self.new_event_listener(listener); // if the node has an event listener, then it must be visited ? is_static = false; @@ -409,7 +417,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { for attr in *attributes { is_static = is_static && attr.is_static; - self.edits.set_attribute(attr); + self.set_attribute(attr); } // Fast path: if there is a single text child, it is faster to @@ -424,7 +432,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // TODO move over // if children.len() == 1 { // if let VNodeKind::Text(text) = &children[0].kind { - // self.edits.set_text(text.text); + // self.set_text(text.text); // return CreateMeta::new(is_static, 1); // } // } @@ -434,7 +442,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { is_static = is_static && child_meta.is_static; // append whatever children were generated by this call - self.edits.append_children(child_meta.added_to_stack); + self.append_children(child_meta.added_to_stack); } // if is_static { @@ -520,7 +528,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { VNodeKind::Suspended { node: real_node } => { let id = self.vdom.reserve_node(); - self.edits.create_placeholder(id); + self.create_placeholder(id); node.dom_id.set(Some(id)); real_node.set(Some(id)); CreateMeta::new(false, 1) @@ -594,7 +602,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { &mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>], - old_anchor: &mut Option, + old_anchor: Option, new_anchor: &mut Option, ) { const IS_EMPTY: bool = true; @@ -602,33 +610,36 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { match (old_anchor, new.is_empty()) { // Both are empty, dealing only with potential anchors - (Some(_), IS_EMPTY) => { - *new_anchor = *old_anchor; + (Some(anchor), IS_EMPTY) => { + // new_anchor.set(Some(anchor)); if old.len() > 0 { // clean up these virtual nodes (components, fragments, etc) } } // Completely adding new nodes, removing any placeholder if it exists - (Some(anchor), IS_NOT_EMPTY) => match old_anchor { + (Some(anchor), IS_NOT_EMPTY) => { + // + // match old_anchor { // If there's anchor to work from, then we replace it with the new children - Some(anchor) => { - self.edits.push_root(*anchor); - let meta = self.create_children(new); - if meta.added_to_stack > 0 { - self.edits.replace_with(meta.added_to_stack) - } else { - // no items added to the stack... hmmmm.... - *new_anchor = *old_anchor; - } + // Some(anchor) => { + self.push_root(anchor); + let meta = self.create_children(new); + if meta.added_to_stack > 0 { + self.replace_with(meta.added_to_stack) + } else { + // no items added to the stack... hmmmm.... + *new_anchor = old_anchor; } + // } // If there's no anchor to work with, we just straight up append them - None => { - let meta = self.create_children(new); - self.edits.append_children(meta.added_to_stack); - } - }, + // None => { + let meta = self.create_children(new); + self.append_children(meta.added_to_stack); + // } + // } + } // Completely removing old nodes and putting an anchor in its place // no anchor (old has nodes) and the new is empty @@ -637,24 +648,24 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // load the first real if let Some(to_replace) = find_first_real_node(old, self.vdom) { // - self.edits.push_root(to_replace.dom_id.get().unwrap()); + self.push_root(to_replace.dom_id.get().unwrap()); // Create the anchor let anchor_id = self.vdom.reserve_node(); - self.edits.create_placeholder(anchor_id); - *new_anchor = Some(anchor_id); + self.create_placeholder(anchor_id); + // *new_anchor = Some(anchor_id); // Replace that node - self.edits.replace_with(1); + self.replace_with(1); } else { // no real nodes - - *new_anchor = *old_anchor; + // new_anchor.set(old_anchor); } // remove the rest for child in &old[1..] { - self.edits.push_root(child.element_id().unwrap()); - self.edits.remove(); + self.push_root(child.element_id().unwrap()); + self.remove(); } } @@ -784,7 +795,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>], ) -> KeyedPrefixResult { - // self.edits.go_down(); + // self.go_down(); let mut shared_prefix_count = 0; @@ -794,7 +805,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { break; } - // self.edits.go_to_sibling(i); + // self.go_to_sibling(i); self.diff_node(old, new); @@ -804,8 +815,8 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // If that was all of the old children, then create and append the remaining // new children and we're finished. if shared_prefix_count == old.len() { - // self.edits.go_up(); - // self.edits.commit_traversal(); + // self.go_up(); + // self.commit_traversal(); self.create_and_append_children(&new[shared_prefix_count..]); return KeyedPrefixResult::Finished; } @@ -813,13 +824,13 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // And if that was all of the new children, then remove all of the remaining // old children and we're finished. if shared_prefix_count == new.len() { - // self.edits.go_to_sibling(shared_prefix_count); - // self.edits.commit_traversal(); + // self.go_to_sibling(shared_prefix_count); + // self.commit_traversal(); self.remove_self_and_next_siblings(&old[shared_prefix_count..]); return KeyedPrefixResult::Finished; } // - // self.edits.go_up(); + // self.go_up(); KeyedPrefixResult::MoreWorkToDo(shared_prefix_count) } @@ -831,7 +842,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // // When this function returns, the change list stack is in the same state. fn remove_all_children(&mut self, old: &'bump [VNode<'bump>]) { - // debug_assert!(self.edits.traversal_is_committed()); + // debug_assert!(self.traversal_is_committed()); log::debug!("REMOVING CHILDREN"); for _child in old { // registry.remove_subtree(child); @@ -839,7 +850,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // Fast way to remove all children: set the node's textContent to an empty // string. todo!() - // self.edits.set_inner_text(""); + // self.set_inner_text(""); } // Create the given children and append them to the parent node. @@ -852,7 +863,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { pub fn create_and_append_children(&mut self, new: &'bump [VNode<'bump>]) { for child in new { let meta = self.create(child); - self.edits.append_children(meta.added_to_stack); + self.append_children(meta.added_to_stack); } } @@ -919,11 +930,11 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // // afresh. // if shared_suffix_count == 0 && shared_keys.is_empty() { // if shared_prefix_count == 0 { - // // self.edits.commit_traversal(); + // // self.commit_traversal(); // self.remove_all_children(old); // } else { - // // self.edits.go_down_to_child(shared_prefix_count); - // // self.edits.commit_traversal(); + // // self.go_down_to_child(shared_prefix_count); + // // self.commit_traversal(); // self.remove_self_and_next_siblings(&old[shared_prefix_count..]); // } @@ -945,8 +956,8 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // .unwrap_or(old.len()); // if end - start > 0 { - // // self.edits.commit_traversal(); - // let mut t = self.edits.save_children_to_temporaries( + // // self.commit_traversal(); + // let mut t = self.save_children_to_temporaries( // shared_prefix_count + start, // shared_prefix_count + end, // ); @@ -971,9 +982,9 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // if !shared_keys.contains(&old_child.key()) { // // registry.remove_subtree(old_child); // // todo - // // self.edits.commit_traversal(); - // self.edits.remove(old_child.dom_id.get()); - // self.edits.remove_child(i + shared_prefix_count); + // // self.commit_traversal(); + // self.remove(old_child.dom_id.get()); + // self.remove_child(i + shared_prefix_count); // removed_count += 1; // } // } @@ -1032,29 +1043,29 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // let old_index = new_index_to_old_index[last_index]; // let temp = old_index_to_temp[old_index]; // // [... parent] - // self.edits.go_down_to_temp_child(temp); + // self.go_down_to_temp_child(temp); // // [... parent last] // self.diff_node(&old[old_index], last); // if new_index_is_in_lis.contains(&last_index) { // // Don't move it, since it is already where it needs to be. // } else { - // // self.edits.commit_traversal(); + // // self.commit_traversal(); // // [... parent last] - // self.edits.append_child(); + // self.append_child(); // // [... parent] - // self.edits.go_down_to_temp_child(temp); + // self.go_down_to_temp_child(temp); // // [... parent last] // } // } else { - // // self.edits.commit_traversal(); + // // self.commit_traversal(); // // [... parent] // self.create(last); // // [... parent last] - // self.edits.append_child(); + // self.append_child(); // // [... parent] - // self.edits.go_down_to_reverse_child(0); + // self.go_down_to_reverse_child(0); // // [... parent last] // } // } @@ -1063,11 +1074,11 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // let old_index = new_index_to_old_index[new_index]; // if old_index == u32::MAX as usize { // debug_assert!(!shared_keys.contains(&new_child.key())); - // // self.edits.commit_traversal(); + // // self.commit_traversal(); // // [... parent successor] // self.create(new_child); // // [... parent successor new_child] - // self.edits.insert_before(); + // self.insert_before(); // // [... parent new_child] // } else { // debug_assert!(shared_keys.contains(&new_child.key())); @@ -1076,14 +1087,14 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // if new_index_is_in_lis.contains(&new_index) { // // [... parent successor] - // self.edits.go_to_temp_sibling(temp); + // self.go_to_temp_sibling(temp); // // [... parent new_child] // } else { - // // self.edits.commit_traversal(); + // // self.commit_traversal(); // // [... parent successor] - // self.edits.push_temporary(temp); + // self.push_temporary(temp); // // [... parent successor new_child] - // self.edits.insert_before(); + // self.insert_before(); // // [... parent new_child] // } @@ -1136,17 +1147,17 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { Ordering::Greater => { for item in &old[new.len()..] { for i in RealChildIterator::new(item, self.vdom) { - self.edits.push_root(i.element_id().unwrap()); - self.edits.remove(); + self.push_root(i.element_id().unwrap()); + self.remove(); } } } // old.len < new.len -> adding some nodes Ordering::Less => { // [... parent last_child] - // self.edits.go_up(); + // self.go_up(); // [... parent] - // self.edits.commit_traversal(); + // self.commit_traversal(); self.create_and_append_children(&new[old.len()..]); } // old.len == new.len -> no nodes added/removed, but πerhaps changed @@ -1168,7 +1179,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // // [... parent] fn remove_self_and_next_siblings(&self, old: &[VNode<'bump>]) { - // debug_assert!(self.edits.traversal_is_committed()); + // debug_assert!(self.traversal_is_committed()); for child in old { if let VNodeKind::Component(_vcomp) = child.kind { // dom @@ -1182,7 +1193,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // }) // let id = get_id(); // *component.stable_addr.as_ref().borrow_mut() = Some(id); - // self.edits.save_known_root(id); + // self.save_known_root(id); // let scope = Rc::downgrade(&component.ass_scope); // self.lifecycle_events.push_back(LifeCycleEvent::Mount { // caller: Rc::downgrade(&component.caller), @@ -1194,7 +1205,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // registry.remove_subtree(child); } todo!() - // self.edits.remove_self_and_next_siblings(); + // self.remove_self_and_next_siblings(); } pub fn get_scope_mut(&mut self, id: &ScopeId) -> Option<&'bump mut Scope> { @@ -1209,6 +1220,108 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // if we have, then we're trying to alias it, which is not allowed unsafe { self.vdom.get_scope(*id) } } + + // Navigation + pub(crate) fn push_root(&mut self, root: ElementId) { + let id = root.as_u64(); + self.edits.push(PushRoot { id }); + } + + pub(crate) fn pop(&mut self) { + self.edits.push(PopRoot {}); + } + + // Add Nodes to the dom + // add m nodes from the stack + pub(crate) fn append_children(&mut self, many: u32) { + self.edits.push(AppendChildren { many }); + } + + // replace the n-m node on the stack with the m nodes + // ends with the last element of the chain on the top of the stack + pub(crate) fn replace_with(&mut self, many: u32) { + self.edits.push(ReplaceWith { many }); + } + + // Remove Nodesfrom the dom + pub(crate) fn remove(&mut self) { + self.edits.push(Remove); + } + + // Create + pub(crate) fn create_text_node(&mut self, text: &'bump str, id: ElementId) { + let id = id.as_u64(); + self.edits.push(CreateTextNode { text, id }); + } + + pub(crate) fn create_element( + &mut self, + tag: &'static str, + ns: Option<&'static str>, + id: ElementId, + ) { + let id = id.as_u64(); + match ns { + Some(ns) => self.edits.push(CreateElementNs { id, ns, tag }), + None => self.edits.push(CreateElement { id, tag }), + } + } + + // placeholders are nodes that don't get rendered but still exist as an "anchor" in the real dom + pub(crate) fn create_placeholder(&mut self, id: ElementId) { + let id = id.as_u64(); + self.edits.push(CreatePlaceholder { id }); + } + + // events + pub(crate) fn new_event_listener(&mut self, listener: &Listener) { + let Listener { + event, + scope, + mounted_node, + .. + } = listener; + + let element_id = mounted_node.get().unwrap().as_u64(); + + self.edits.push(NewEventListener { + scope: scope.clone(), + event_name: event, + mounted_node_id: element_id, + }); + } + + pub(crate) fn remove_event_listener(&mut self, event: &'static str) { + self.edits.push(RemoveEventListener { event }); + } + + // modify + pub(crate) fn set_text(&mut self, text: &'bump str) { + self.edits.push(SetText { text }); + } + + pub(crate) fn set_attribute(&mut self, attribute: &'bump Attribute) { + let Attribute { + name, + value, + is_static, + is_volatile, + namespace, + } = attribute; + // field: &'static str, + // value: &'bump str, + // ns: Option<&'static str>, + self.edits.push(SetAttribute { + field: name, + value, + ns: *namespace, + }); + } + + pub(crate) fn remove_attribute(&mut self, attribute: &Attribute) { + let name = attribute.name; + self.edits.push(RemoveAttribute { name }); + } } // When we create new nodes, we need to propagate some information back up the call chain. @@ -1237,11 +1350,11 @@ enum KeyedPrefixResult { } fn find_first_real_node<'a>( - nodes: &'a [VNode<'a>], + nodes: impl IntoIterator>, scopes: &'a SharedResources, ) -> Option<&'a VNode<'a>> { for node in nodes { - let iter = RealChildIterator::new(node, scopes); + let mut iter = RealChildIterator::new(node, scopes); if let Some(node) = iter.next() { return Some(node); } diff --git a/packages/core/src/editor.rs b/packages/core/src/editor.rs index e51d564d7..5c2e116eb 100644 --- a/packages/core/src/editor.rs +++ b/packages/core/src/editor.rs @@ -5,143 +5,7 @@ //! //! -use crate::{ - innerlude::{Attribute, Listener, ScopeId}, - ElementId, -}; - -/// The `DomEditor` provides an imperative interface for the Diffing algorithm to plan out its changes. -/// -/// However, the DomEditor only builds a change list - it does not apply them. In contrast with the "RealDom", the DomEditor -/// is cancellable and flushable. At any moment in time, Dioxus may choose to completely clear the edit list and start over. -/// -/// This behavior is used in the cooperative scheduling algorithm. -pub struct DomEditor<'real, 'bump> { - pub edits: &'real mut Vec>, -} - -use DomEdit::*; -impl<'real, 'bump> DomEditor<'real, 'bump> { - pub fn new(edits: &'real mut Vec>) -> Self { - Self { edits } - } - - // Navigation - pub(crate) fn push_root(&mut self, root: ElementId) { - let id = root.as_u64(); - self.edits.push(PushRoot { id }); - } - - #[inline] - pub(crate) fn pop(&mut self) { - self.edits.push(PopRoot {}); - } - - // Add Nodes to the dom - // add m nodes from the stack - #[inline] - pub(crate) fn append_children(&mut self, many: u32) { - self.edits.push(AppendChildren { many }); - } - - // replace the n-m node on the stack with the m nodes - // ends with the last element of the chain on the top of the stack - #[inline] - pub(crate) fn replace_with(&mut self, many: u32) { - self.edits.push(ReplaceWith { many }); - } - - // Remove Nodesfrom the dom - #[inline] - pub(crate) fn remove(&mut self) { - self.edits.push(Remove); - } - - #[inline] - pub(crate) fn remove_all_children(&mut self) { - self.edits.push(RemoveAllChildren); - } - - // Create - #[inline] - pub(crate) fn create_text_node(&mut self, text: &'bump str, id: ElementId) { - let id = id.as_u64(); - self.edits.push(CreateTextNode { text, id }); - } - - pub(crate) fn create_element( - &mut self, - tag: &'static str, - ns: Option<&'static str>, - id: ElementId, - ) { - let id = id.as_u64(); - match ns { - Some(ns) => self.edits.push(CreateElementNs { id, ns, tag }), - None => self.edits.push(CreateElement { id, tag }), - } - } - - // placeholders are nodes that don't get rendered but still exist as an "anchor" in the real dom - pub(crate) fn create_placeholder(&mut self, id: ElementId) { - let id = id.as_u64(); - self.edits.push(CreatePlaceholder { id }); - } - - // events - pub(crate) fn new_event_listener(&mut self, listener: &Listener) { - let Listener { - event, - scope, - mounted_node, - .. - } = listener; - - let element_id = mounted_node.get().unwrap().as_u64(); - - self.edits.push(NewEventListener { - scope: scope.clone(), - event_name: event, - mounted_node_id: element_id, - }); - } - - #[inline] - pub(crate) fn remove_event_listener(&mut self, event: &'static str) { - self.edits.push(RemoveEventListener { event }); - } - - // modify - #[inline] - pub(crate) fn set_text(&mut self, text: &'bump str) { - self.edits.push(SetText { text }); - } - - #[inline] - pub(crate) fn set_attribute(&mut self, attribute: &'bump Attribute) { - let Attribute { - name, - value, - is_static, - is_volatile, - namespace, - } = attribute; - // field: &'static str, - // value: &'bump str, - // ns: Option<&'static str>, - self.edits.push(SetAttribute { - field: name, - value, - ns: *namespace, - }); - } - - #[inline] - pub(crate) fn remove_attribute(&mut self, attribute: &Attribute) { - let name = attribute.name; - self.edits.push(RemoveAttribute { name }); - } -} +use crate::innerlude::ScopeId; /// A `DomEdit` represents a serialzied form of the VirtualDom's trait-based API. This allows streaming edits across the /// network or through FFI boundaries. diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index ef0a11694..b096f8d5f 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -194,7 +194,7 @@ impl VirtualDom { // We run the component. If it succeeds, then we can diff it and add the changes to the dom. if cur_component.run_scope().is_ok() { let meta = diff_machine.create(cur_component.frames.fin_head()); - diff_machine.edits.append_children(meta.added_to_stack); + diff_machine.append_children(meta.added_to_stack); } else { // todo: should this be a hard error? log::warn!( @@ -295,13 +295,13 @@ impl VirtualDom { // push the old node's root onto the stack let real_id = domnode.get().ok_or(Error::NotMounted)?; - diff_machine.edits.push_root(real_id); + diff_machine.push_root(real_id); // push these new nodes onto the diff machines stack let meta = diff_machine.create(&*nodes); // replace the placeholder with the new nodes we just pushed on the stack - diff_machine.edits.replace_with(meta.added_to_stack); + diff_machine.replace_with(meta.added_to_stack); } } } From d618092e9d150589e61516f7bbb169f2db49d3f2 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 29 Jul 2021 18:04:09 -0400 Subject: [PATCH 13/21] feat: a new vnode type for anchors --- packages/core/Cargo.toml | 1 + packages/core/README.md | 8 +- packages/core/src/arena.rs | 9 +- packages/core/src/context.rs | 10 +- packages/core/src/diff.rs | 764 +++++++++++++++++-------------- packages/core/src/editor.rs | 21 + packages/core/src/events.rs | 19 +- packages/core/src/hooks.rs | 8 +- packages/core/src/lib.rs | 1 + packages/core/src/nodes.rs | 101 ++-- packages/core/src/scope.rs | 45 +- packages/core/src/util.rs | 2 +- packages/core/src/virtual_dom.rs | 57 ++- packages/core/tests/diffing.rs | 108 ++++- packages/ssr/src/lib.rs | 2 +- 15 files changed, 715 insertions(+), 441 deletions(-) diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 5798abc23..450b64b23 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -41,6 +41,7 @@ slab = "0.4.3" [dev-dependencies] +anyhow = "1.0.42" dioxus-html = { path = "../html" } diff --git a/packages/core/README.md b/packages/core/README.md index 2d6c19658..01152bfc9 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -2,18 +2,18 @@ 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. -We reserve the "dioxus" name and aggregate all the various renderers under it. If you want just a single dioxus renderer, then chose from "dioxus-web", "dioxus-desktop", etc. +To build new apps with Dioxus or to extend the ecosystem with new hooks or components, use the `Dioxus` crate with the appropriate feature flags. ## Internals Dioxus-core builds off the many frameworks that came before it. Notably, Dioxus borrows these concepts: - React: hooks, concurrency, suspense -- Dodrio: bump allocation, double buffering, and source code for NodeBuilder +- Dodrio: bump allocation, double buffering, and some diffing architecture - Percy: html! macro architecture, platform-agnostic edits -- Yew: passion and inspiration ❤️ -- InfernoJS: approach to fragments and node diffing +- InfernoJS: approach to keyed diffing - Preact: approach for normalization and ref +- Yew: passion and inspiration ❤️ Dioxus-core leverages some really cool techniques and hits a very high level of parity with mature frameworks. Some unique features include: diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 4e1a1561e..ce74b6876 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -5,7 +5,9 @@ use std::{cell::UnsafeCell, rc::Rc}; use crate::heuristics::*; use crate::innerlude::*; use futures_util::stream::FuturesUnordered; +use fxhash::{FxHashMap, FxHashSet}; use slab::Slab; +use smallvec::SmallVec; // slotmap::new_key_type! { // // A dedicated key type for the all the scopes @@ -47,7 +49,7 @@ pub struct SharedResources { /// We use a SlotSet to keep track of the keys that are currently being used. /// However, we don't store any specific data since the "mirror" - pub raw_elements: Shared>, + pub raw_elements: Rc>>, pub task_setter: Rc, } @@ -133,7 +135,10 @@ impl SharedResources { } /// return the id, freeing the space of the original node - pub fn collect_garbage(&self, id: ElementId) {} + pub fn collect_garbage(&self, id: ElementId) { + let mut r: RefMut> = self.raw_elements.borrow_mut(); + r.remove(id.0); + } pub fn borrow_queue(&self) -> RefMut> { self.event_queue.borrow_mut() diff --git a/packages/core/src/context.rs b/packages/core/src/context.rs index f8b538d9f..5f82c31e5 100644 --- a/packages/core/src/context.rs +++ b/packages/core/src/context.rs @@ -138,8 +138,8 @@ impl<'src, P> Context<'src, P> { self, lazy_nodes: LazyNodes<'src, F>, ) -> DomTree<'src> { - let scope_ref = self.scope; - Some(lazy_nodes.into_vnode(NodeFactory { scope: scope_ref })) + let bump = &self.scope.frames.wip_frame().bump; + Some(lazy_nodes.into_vnode(NodeFactory { bump })) } /// `submit_task` will submit the future to be polled. @@ -178,7 +178,7 @@ impl<'src, P> Context<'src, P> { /// pub fn consume_shared_state(self) -> Option> { let mut scope = Some(self.scope); - let mut par = None; + let mut parent = None; let ty = TypeId::of::(); while let Some(inner) = scope { @@ -202,7 +202,7 @@ impl<'src, P> Context<'src, P> { .clone() .downcast::() .expect("Should not fail, already validated the type from the hashmap"); - par = Some(rc); + parent = Some(rc); break; } else { match inner.parent_idx { @@ -213,7 +213,7 @@ impl<'src, P> Context<'src, P> { } } } - par + parent } /// Store a value between renders diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 3288602cb..41b9084cd 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -18,6 +18,14 @@ //! always make sure to copy over the ID. If we don't do this properly, the ElementId will be populated incorrectly and //! brick the user's page. //! +//! ### Fragment Support +//! +//! Fragments (nodes without a parent) are supported through a combination of "replace with" and anchor vnodes. Fragments +//! can be particularly challenging when they are empty, so the placeholder node lets us "reserve" a spot for the empty +//! fragment to be replaced with when it is no longer empty. This is guaranteed by logic in the NodeFactory - it is +//! impossible to craft a fragment with 0 elements - they must always have at least a single placeholder element. This is +//! slightly inefficient, but represents a such an uncommon use case that it is not worth optimizing. +//! //! ## Subtree Memoization //! ----------------------- //! We also employ "subtree memoization" which saves us from having to check trees which take no dynamic content. We can @@ -45,6 +53,11 @@ //! so the client only needs to maintain a simple list of nodes. By default, Dioxus will not manually clean up old nodes //! for the client. As new nodes are created, old nodes will be over-written. //! +//! HEADS-UP: +//! For now, deferred garabge collection is disabled. The code-paths are almost wired up, but it's quite complex to +//! get working safely and efficiently. For now, garabge is collected immediately during diffing. This adds extra +//! overhead, but is faster to implement in the short term. +//! //! Further Reading and Thoughts //! ---------------------------- //! There are more ways of increasing diff performance here that are currently not implemented. @@ -77,30 +90,48 @@ pub struct DiffMachine<'real, 'bump> { pub edits: &'real mut Vec>, - pub scheduled_garbage: Vec<&'bump VNode<'bump>>, - - pub cur_idxs: SmallVec<[ScopeId; 5]>, + pub scope_stack: SmallVec<[ScopeId; 5]>, pub diffed: FxHashSet, - pub seen_nodes: FxHashSet, + // will be used later for garbage collection + // we check every seen node and then schedule its eventual deletion + pub seen_scopes: FxHashSet, } impl<'real, 'bump> DiffMachine<'real, 'bump> { - pub fn new( + pub(crate) fn new( edits: &'real mut Vec>, dom: &'real dyn RealDom<'bump>, - cur_idx: ScopeId, + cur_scope: ScopeId, shared: &'bump SharedResources, ) -> Self { Self { real_dom: dom, edits, - cur_idxs: smallvec![cur_idx], + scope_stack: smallvec![cur_scope], vdom: shared, - scheduled_garbage: vec![], diffed: FxHashSet::default(), - seen_nodes: FxHashSet::default(), + seen_scopes: FxHashSet::default(), + } + } + + /// Allows the creation of a diff machine without the concept of scopes or a virtualdom + /// this is mostly useful for testing + /// + /// This will PANIC if any component elements are passed in. + pub fn new_headless( + edits: &'real mut Vec>, + dom: &'real dyn RealDom<'bump>, + shared: &'bump SharedResources, + ) -> Self { + Self { + real_dom: dom, + edits, + scope_stack: smallvec![ScopeId(0)], + vdom: shared, + diffed: FxHashSet::default(), + seen_scopes: FxHashSet::default(), } } @@ -111,41 +142,35 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // // each function call assumes the stack is fresh (empty). pub fn diff_node(&mut self, old_node: &'bump VNode<'bump>, new_node: &'bump VNode<'bump>) { - let root = old_node.dom_id.get(); - match (&old_node.kind, &new_node.kind) { // Handle the "sane" cases first. // The rsx and html macros strongly discourage dynamic lists not encapsulated by a "Fragment". // So the sane (and fast!) cases are where the virtual structure stays the same and is easily diffable. (VNodeKind::Text(old), VNodeKind::Text(new)) => { - let root = root.unwrap(); + let root = old_node.direct_id(); if old.text != new.text { - self.push_root(root); - self.set_text(new.text); - self.pop(); + self.edit_push_root(root); + self.edit_set_text(new.text); + self.edit_pop(); } - new_node.dom_id.set(Some(root)); + new.dom_id.set(Some(root)); } (VNodeKind::Element(old), VNodeKind::Element(new)) => { - let root = root.unwrap(); + let root = old_node.direct_id(); // If the element type is completely different, the element needs to be re-rendered completely // This is an optimization React makes due to how users structure their code // // This case is rather rare (typically only in non-keyed lists) if new.tag_name != old.tag_name || new.namespace != old.namespace { - self.push_root(root); - let meta = self.create(new_node); - self.replace_with(meta.added_to_stack); - self.scheduled_garbage.push(old_node); - self.pop(); + self.replace_node_with_node(root, old_node, new_node); return; } - new_node.dom_id.set(Some(root)); + new.dom_id.set(Some(root)); // Don't push the root if we don't have to let mut has_comitted = false; @@ -166,17 +191,17 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { for (old_attr, new_attr) in old.attributes.iter().zip(new.attributes.iter()) { if old_attr.value != new_attr.value { please_commit(&mut self.edits); - self.set_attribute(new_attr); + self.edit_set_attribute(new_attr); } } } else { // TODO: provide some sort of report on how "good" the diffing was please_commit(&mut self.edits); for attribute in old.attributes { - self.remove_attribute(attribute); + self.edit_remove_attribute(attribute); } for attribute in new.attributes { - self.set_attribute(attribute) + self.edit_set_attribute(attribute) } } @@ -186,33 +211,38 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // In the cases where the listeners change, we completely wipe the data attributes and add new ones // // TODO: take a more efficient path than this + let cur_scope: ScopeId = self.scope_stack.last().unwrap().clone(); if old.listeners.len() == new.listeners.len() { for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) { if old_l.event != new_l.event { please_commit(&mut self.edits); - self.remove_event_listener(old_l.event); - self.new_event_listener(new_l); + self.edit_remove_event_listener(old_l.event); + self.edit_new_event_listener(new_l, cur_scope); } new_l.mounted_node.set(old_l.mounted_node.get()); + self.fix_listener(new_l); } } else { please_commit(&mut self.edits); for listener in old.listeners { - self.remove_event_listener(listener.event); + self.edit_remove_event_listener(listener.event); } for listener in new.listeners { listener.mounted_node.set(Some(root)); - self.new_event_listener(listener); + self.edit_new_event_listener(listener, cur_scope); + + // Make sure the listener gets attached to the scope list + self.fix_listener(listener); } } if has_comitted { - self.pop(); + self.edit_pop(); } // Each child pushes its own root, so it doesn't need our current root - self.diff_children(old.children, new.children, None, &mut None); + self.diff_children(old.children, new.children); } (VNodeKind::Component(old), VNodeKind::Component(new)) => { @@ -221,7 +251,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // Make sure we're dealing with the same component (by function pointer) if old.user_fc == new.user_fc { // - self.cur_idxs.push(scope_addr); + self.scope_stack.push(scope_addr); // Make sure the new component vnode is referencing the right scope id new.ass_scope.set(Some(scope_addr)); @@ -246,9 +276,9 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { } } - self.cur_idxs.pop(); + self.scope_stack.pop(); - self.seen_nodes.insert(scope_addr); + self.seen_scopes.insert(scope_addr); } else { let mut old_iter = RealChildIterator::new(old_node, &self.vdom); let first = old_iter @@ -257,16 +287,12 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // remove any leftovers for to_remove in old_iter { - self.push_root(to_remove.element_id().unwrap()); - self.remove(); + self.edit_push_root(to_remove.direct_id()); + self.edit_remove(); } // seems like we could combine this into a single instruction.... - self.push_root(first.element_id().unwrap()); - let meta = self.create(new_node); - self.replace_with(meta.added_to_stack); - self.scheduled_garbage.push(old_node); - self.pop(); + self.replace_node_with_node(first.direct_id(), old_node, new_node); // Wipe the old one and plant the new one let old_scope = old.ass_scope.get().unwrap(); @@ -282,16 +308,11 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { return; } - let mut new_anchor = None; - self.diff_children( - old.children, - new.children, - old_node.dom_id.get(), - &mut new_anchor, - ); - if let Some(anchor) = new_anchor { - new_node.dom_id.set(Some(anchor)); - } + self.diff_children(old.children, new.children); + } + + (VNodeKind::Anchor(old), VNodeKind::Anchor(new)) => { + new.dom_id.set(old.dom_id.get()); } // The strategy here is to pick the first possible node from the previous set and use that as our replace with root @@ -305,54 +326,57 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { VNodeKind::Component(_) | VNodeKind::Fragment(_) | VNodeKind::Text(_) - | VNodeKind::Element(_), + | VNodeKind::Element(_) + | VNodeKind::Anchor(_), VNodeKind::Component(_) | VNodeKind::Fragment(_) | VNodeKind::Text(_) - | VNodeKind::Element(_), + | VNodeKind::Element(_) + | VNodeKind::Anchor(_), ) => { - log::info!( - "taking the awkard diffing path {:#?}, {:#?}", - old_node, - new_node - ); - // currently busted for components - need to fid - let root = old_node.dom_id.get().expect(&format!( - "Should not be diffing old nodes that were never assigned, {:#?}", - old_node - )); - // Choose the node to use as the placeholder for replacewith - let back_node_id = match old_node.kind { - // We special case these two types to avoid allocating the small-vecs - VNodeKind::Element(_) | VNodeKind::Text(_) => root, + todo!(); + // log::info!( + // "taking the awkard diffing path {:#?}, {:#?}", + // old_node, + // new_node + // ); + // // currently busted for components - need to fid + // let root = old_node.dom_id.get().expect(&format!( + // "Should not be diffing old nodes that were never assigned, {:#?}", + // old_node + // )); + // // Choose the node to use as the placeholder for replacewith + // let back_node_id = match old_node.kind { + // // We special case these two types to avoid allocating the small-vecs + // VNodeKind::Element(_) | VNodeKind::Text(_) => root, - _ => { - let mut old_iter = RealChildIterator::new(old_node, &self.vdom); + // _ => { + // let mut old_iter = RealChildIterator::new(old_node, &self.vdom); - let back_node = old_iter - .next() - .expect("Empty fragments should generate a placeholder."); + // let back_node = old_iter + // .next() + // .expect("Empty fragments should generate a placeholder."); - // remove any leftovers - for to_remove in old_iter { - self.push_root(to_remove.element_id().unwrap()); - self.remove(); - } + // // remove any leftovers + // for to_remove in old_iter { + // self.edit_push_root(to_remove.dom_id().unwrap()); + // self.edit_remove(); + // } - back_node.element_id().unwrap() - } - }; + // back_node.dom_id().unwrap() + // } + // }; - // replace the placeholder or first node with the nodes generated from the "new" - self.push_root(back_node_id); - let meta = self.create(new_node); - self.replace_with(meta.added_to_stack); + // // replace the placeholder or first node with the nodes generated from the "new" + // self.edit_push_root(back_node_id); + // let meta = self.create_vnode(new_node); + // self.edit_replace_with(meta.added_to_stack); - // todo use the is_static metadata to update this subtree + // // todo use the is_static metadata to update this subtree } // TODO - (VNodeKind::Suspended { node }, new) => todo!(), + (VNodeKind::Suspended(node), new) => todo!(), (old, VNodeKind::Suspended { .. }) => { // a node that was once real is now suspended // @@ -369,16 +393,23 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // When this function returns, the new node is on top of the change list stack: // // [... node] - pub fn create(&mut self, node: &'bump VNode<'bump>) -> CreateMeta { + pub fn create_vnode(&mut self, node: &'bump VNode<'bump>) -> CreateMeta { log::warn!("Creating node! ... {:#?}", node); match &node.kind { VNodeKind::Text(text) => { let real_id = self.vdom.reserve_node(); - self.create_text_node(text.text, real_id); - node.dom_id.set(Some(real_id)); - + self.edit_create_text_node(text.text, real_id); + text.dom_id.set(Some(real_id)); CreateMeta::new(text.is_static, 1) } + + VNodeKind::Anchor(anchor) => { + let real_id = self.vdom.reserve_node(); + self.edit_create_placeholder(real_id); + anchor.dom_id.set(Some(real_id)); + CreateMeta::new(false, 1) + } + VNodeKind::Element(el) => { // we have the potential to completely eliminate working on this node in the future(!) // @@ -396,20 +427,23 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { static_attrs: _, static_children: _, static_listeners: _, + dom_id, } = el; let real_id = self.vdom.reserve_node(); if let Some(namespace) = namespace { - self.create_element(tag_name, Some(namespace), real_id) + self.edit_create_element(tag_name, Some(namespace), real_id) } else { - self.create_element(tag_name, None, real_id) + self.edit_create_element(tag_name, None, real_id) }; - node.dom_id.set(Some(real_id)); + dom_id.set(Some(real_id)); + let cur_scope = self.current_scope().unwrap(); listeners.iter().for_each(|listener| { log::info!("setting listener id to {:#?}", real_id); + self.fix_listener(listener); listener.mounted_node.set(Some(real_id)); - self.new_event_listener(listener); + self.edit_new_event_listener(listener, cur_scope.clone()); // if the node has an event listener, then it must be visited ? is_static = false; @@ -417,7 +451,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { for attr in *attributes { is_static = is_static && attr.is_static; - self.set_attribute(attr); + self.edit_set_attribute(attr); } // Fast path: if there is a single text child, it is faster to @@ -438,20 +472,13 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // } for child in *children { - let child_meta = self.create(child); + let child_meta = self.create_vnode(child); is_static = is_static && child_meta.is_static; // append whatever children were generated by this call - self.append_children(child_meta.added_to_stack); + self.edit_append_children(child_meta.added_to_stack); } - // if is_static { - // log::debug!("created a static node {:#?}", node); - // } else { - // log::debug!("created a dynamic node {:#?}", node); - // } - - // el_is_static.set(is_static); CreateMeta::new(is_static, 1) } @@ -459,7 +486,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { log::debug!("Mounting a new component"); let caller = vcomponent.caller.clone(); - let parent_idx = self.cur_idxs.last().unwrap().clone(); + let parent_idx = self.scope_stack.last().unwrap().clone(); // Insert a new scope into our component list let new_idx = self.vdom.insert_scope_with_key(|new_idx| { @@ -475,17 +502,6 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { ) }); - // This code is supposed to insert the new idx into the parent's descendent list, but it doesn't really work. - // This is mostly used for cleanup - to remove old scopes when components are destroyed. - // TODO - // - // self.components - // .try_get_mut(idx) - // .unwrap() - // .descendents - // .borrow_mut() - // .insert(idx); - // TODO: abstract this unsafe into the arena abstraction let new_component = self.get_scope_mut(&new_idx).unwrap(); @@ -495,15 +511,24 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // Run the scope for one iteration to initialize it new_component.run_scope().unwrap(); - // TODO: we need to delete (IE relcaim this node, otherwise the arena will grow infinitely) + // Take the node that was just generated from running the component let nextnode = new_component.frames.fin_head(); - self.cur_idxs.push(new_idx); - let meta = self.create(nextnode); - self.cur_idxs.pop(); - node.dom_id.set(nextnode.dom_id.get()); - // Finally, insert this node as a seen node. - self.seen_nodes.insert(new_idx); + // Push the new scope onto the stack + self.scope_stack.push(new_idx); + + // Run the creation algorithm with this scope on the stack + let meta = self.create_vnode(nextnode); + + // pop the scope off the stack + self.scope_stack.pop(); + + if meta.added_to_stack == 0 { + panic!("This should never happen"); + } + + // Finally, insert this scope as a seen node. + self.seen_scopes.insert(new_idx); CreateMeta::new(vcomponent.is_static, meta.added_to_stack) } @@ -511,27 +536,15 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // Fragments are the only nodes that can contain dynamic content (IE through curlies or iterators). // We can never ignore their contents, so the prescence of a fragment indicates that we need always diff them. // Fragments will just put all their nodes onto the stack after creation - VNodeKind::Fragment(frag) => { - let mut nodes_added = 0; - for child in frag.children.iter().rev() { - // different types of nodes will generate different amounts on the stack - // nested fragments will spew a ton of nodes onto the stack - // TODO: make sure that our order (.rev) makes sense in a nested situation - let new_meta = self.create(child); - nodes_added += new_meta.added_to_stack; - } - log::info!("This fragment added {} nodes to the stack", nodes_added); + VNodeKind::Fragment(frag) => self.create_children(frag.children), - // Never ignore - CreateMeta::new(false, nodes_added) - } - - VNodeKind::Suspended { node: real_node } => { - let id = self.vdom.reserve_node(); - self.create_placeholder(id); - node.dom_id.set(Some(id)); - real_node.set(Some(id)); - CreateMeta::new(false, 1) + VNodeKind::Suspended(VSuspended { node: real_node }) => { + todo!("wip on adding suspense back in"); + // let id = self.vdom.reserve_node(); + // self.edit_create_placeholder(id); + // node.dom_id.set(Some(id)); + // real_node.set(Some(id)); + // CreateMeta::new(false, 1) } } } @@ -541,7 +554,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { let mut added_to_stack = 0; for child in children { - let child_meta = self.create(child); + let child_meta = self.create_vnode(child); is_static = is_static && child_meta.is_static; added_to_stack += child_meta.added_to_stack; } @@ -552,8 +565,6 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { } } - pub fn replace_vnode(&mut self, old_node: &'bump VNode<'bump>, new_node: &'bump VNode<'bump>) {} - /// Destroy a scope and all of its descendents. /// /// Calling this will run the destuctors on all hooks in the tree. @@ -565,7 +576,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // explore the scope tree breadth first while let Some(scope_id) = scopes_to_explore.pop() { // If we're planning on deleting this node, then we don't need to both rendering it - self.seen_nodes.insert(scope_id); + self.seen_scopes.insert(scope_id); let scope = self.get_scope(&scope_id).unwrap(); for child in scope.descendents.borrow().iter() { // Add this node to be explored @@ -598,94 +609,63 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // If old no anchors are provided, then it's assumed that we can freely append to the parent. // // Remember, non-empty lists does not mean that there are real elements, just that there are virtual elements. - fn diff_children( - &mut self, - old: &'bump [VNode<'bump>], - new: &'bump [VNode<'bump>], - old_anchor: Option, - new_anchor: &mut Option, - ) { + fn diff_children(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { const IS_EMPTY: bool = true; const IS_NOT_EMPTY: bool = false; - match (old_anchor, new.is_empty()) { - // Both are empty, dealing only with potential anchors - (Some(anchor), IS_EMPTY) => { - // new_anchor.set(Some(anchor)); - if old.len() > 0 { - // clean up these virtual nodes (components, fragments, etc) - } - } + match (old.is_empty(), new.is_empty()) { + (IS_EMPTY, IS_EMPTY) => {} // Completely adding new nodes, removing any placeholder if it exists - (Some(anchor), IS_NOT_EMPTY) => { - // - // match old_anchor { - // If there's anchor to work from, then we replace it with the new children - // Some(anchor) => { - self.push_root(anchor); + (IS_EMPTY, IS_NOT_EMPTY) => { let meta = self.create_children(new); - if meta.added_to_stack > 0 { - self.replace_with(meta.added_to_stack) - } else { - // no items added to the stack... hmmmm.... - *new_anchor = old_anchor; - } - // } - - // If there's no anchor to work with, we just straight up append them - // None => { - let meta = self.create_children(new); - self.append_children(meta.added_to_stack); - // } - // } + self.edit_append_children(meta.added_to_stack); } // Completely removing old nodes and putting an anchor in its place // no anchor (old has nodes) and the new is empty // remove all the old nodes - (None, IS_EMPTY) => { - // load the first real - if let Some(to_replace) = find_first_real_node(old, self.vdom) { - // - self.push_root(to_replace.dom_id.get().unwrap()); - - // Create the anchor - let anchor_id = self.vdom.reserve_node(); - self.create_placeholder(anchor_id); - // *new_anchor = Some(anchor_id); - - // Replace that node - self.replace_with(1); - } else { - // no real nodes - - // new_anchor.set(old_anchor); - } - - // remove the rest - for child in &old[1..] { - self.push_root(child.element_id().unwrap()); - self.remove(); + (IS_NOT_EMPTY, IS_EMPTY) => { + for node in old { + self.remove_vnode(node); } } - (None, IS_NOT_EMPTY) => { - let new_is_keyed = new[0].key.is_some(); - let old_is_keyed = old[0].key.is_some(); + (IS_NOT_EMPTY, IS_NOT_EMPTY) => { + let first_old = &old[0]; + let first_new = &new[0]; - debug_assert!( - new.iter().all(|n| n.key.is_some() == new_is_keyed), - "all siblings must be keyed or all siblings must be non-keyed" - ); - debug_assert!( - old.iter().all(|o| o.key.is_some() == old_is_keyed), - "all siblings must be keyed or all siblings must be non-keyed" - ); + match (&first_old.kind, &first_new.kind) { + (VNodeKind::Anchor(old_anchor), VNodeKind::Anchor(new_anchor)) => { + old_anchor.dom_id.set(new_anchor.dom_id.get()); + } + (VNodeKind::Anchor(anchor), _) => { + // replace new anchor with many + todo!(); + } + (_, VNodeKind::Anchor(anchor)) => { + // replace many with new anchor + todo!(); + } + _ => { + let new_is_keyed = new[0].key.is_some(); + let old_is_keyed = old[0].key.is_some(); - if new_is_keyed && old_is_keyed { - self.diff_keyed_children(old, new); - } else { - self.diff_non_keyed_children(old, new); + debug_assert!( + new.iter().all(|n| n.key.is_some() == new_is_keyed), + "all siblings must be keyed or all siblings must be non-keyed" + ); + debug_assert!( + old.iter().all(|o| o.key.is_some() == old_is_keyed), + "all siblings must be keyed or all siblings must be non-keyed" + ); + + if new_is_keyed && old_is_keyed { + self.diff_keyed_children(old, new); + } else { + self.diff_non_keyed_children(old, new); + } + } } } } @@ -795,8 +775,6 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>], ) -> KeyedPrefixResult { - // self.go_down(); - let mut shared_prefix_count = 0; for (i, (old, new)) in old.iter().zip(new.iter()).enumerate() { @@ -805,8 +783,6 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { break; } - // self.go_to_sibling(i); - self.diff_node(old, new); shared_prefix_count += 1; @@ -815,8 +791,6 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // If that was all of the old children, then create and append the remaining // new children and we're finished. if shared_prefix_count == old.len() { - // self.go_up(); - // self.commit_traversal(); self.create_and_append_children(&new[shared_prefix_count..]); return KeyedPrefixResult::Finished; } @@ -829,28 +803,8 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { self.remove_self_and_next_siblings(&old[shared_prefix_count..]); return KeyedPrefixResult::Finished; } - // - // self.go_up(); - KeyedPrefixResult::MoreWorkToDo(shared_prefix_count) - } - // Remove all of a node's children. - // - // The change list stack must have this shape upon entry to this function: - // - // [... parent] - // - // When this function returns, the change list stack is in the same state. - fn remove_all_children(&mut self, old: &'bump [VNode<'bump>]) { - // debug_assert!(self.traversal_is_committed()); - log::debug!("REMOVING CHILDREN"); - for _child in old { - // registry.remove_subtree(child); - } - // Fast way to remove all children: set the node's textContent to an empty - // string. - todo!() - // self.set_inner_text(""); + KeyedPrefixResult::MoreWorkToDo(shared_prefix_count) } // Create the given children and append them to the parent node. @@ -862,8 +816,8 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // When this function returns, the change list stack is in the same state. pub fn create_and_append_children(&mut self, new: &'bump [VNode<'bump>]) { for child in new { - let meta = self.create(child); - self.append_children(meta.added_to_stack); + let meta = self.create_vnode(child); + self.edit_append_children(meta.added_to_stack); } } @@ -881,99 +835,99 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // // Upon exit from this function, it will be restored to that same state. fn diff_keyed_middle( - &self, - old: &[VNode<'bump>], - new: &[VNode<'bump>], + &mut self, + old: &'bump [VNode<'bump>], + new: &'bump [VNode<'bump>], shared_prefix_count: usize, shared_suffix_count: usize, old_shared_suffix_start: usize, ) { - // // Should have already diffed the shared-key prefixes and suffixes. - // debug_assert_ne!(new.first().map(|n| n.key()), old.first().map(|o| o.key())); - // debug_assert_ne!(new.last().map(|n| n.key()), old.last().map(|o| o.key())); + // Should have already diffed the shared-key prefixes and suffixes. + debug_assert_ne!(new.first().map(|n| n.key()), old.first().map(|o| o.key())); + debug_assert_ne!(new.last().map(|n| n.key()), old.last().map(|o| o.key())); - // // The algorithm below relies upon using `u32::MAX` as a sentinel - // // value, so if we have that many new nodes, it won't work. This - // // check is a bit academic (hence only enabled in debug), since - // // wasm32 doesn't have enough address space to hold that many nodes - // // in memory. - // debug_assert!(new.len() < u32::MAX as usize); + // The algorithm below relies upon using `u32::MAX` as a sentinel + // value, so if we have that many new nodes, it won't work. This + // check is a bit academic (hence only enabled in debug), since + // wasm32 doesn't have enough address space to hold that many nodes + // in memory. + debug_assert!(new.len() < u32::MAX as usize); - // // Map from each `old` node's key to its index within `old`. - // // IE if the keys were A B C, then we would have (A, 1) (B, 2) (C, 3). - // let mut old_key_to_old_index = old - // .iter() - // .enumerate() - // .map(|(i, o)| (o.key(), i)) - // .collect::>(); + // Map from each `old` node's key to its index within `old`. + // IE if the keys were A B C, then we would have (A, 1) (B, 2) (C, 3). + let mut old_key_to_old_index = old + .iter() + .enumerate() + .map(|(i, o)| (o.key(), i)) + .collect::>(); - // // The set of shared keys between `new` and `old`. - // let mut shared_keys = FxHashSet::default(); + // The set of shared keys between `new` and `old`. + let mut shared_keys = FxHashSet::default(); - // // Map from each index in `new` to the index of the node in `old` that - // // has the same key. - // let mut new_index_to_old_index = new - // .iter() - // .map(|n| { - // let key = n.key(); - // if let Some(&i) = old_key_to_old_index.get(&key) { - // shared_keys.insert(key); - // i - // } else { - // u32::MAX as usize - // } - // }) - // .collect::>(); + // Map from each index in `new` to the index of the node in `old` that + // has the same key. + let mut new_index_to_old_index = new + .iter() + .map(|n| { + let key = n.key(); + if let Some(&i) = old_key_to_old_index.get(&key) { + shared_keys.insert(key); + i + } else { + u32::MAX as usize + } + }) + .collect::>(); - // // If none of the old keys are reused by the new children, then we - // // remove all the remaining old children and create the new children - // // afresh. - // if shared_suffix_count == 0 && shared_keys.is_empty() { - // if shared_prefix_count == 0 { - // // self.commit_traversal(); - // self.remove_all_children(old); - // } else { - // // self.go_down_to_child(shared_prefix_count); - // // self.commit_traversal(); - // self.remove_self_and_next_siblings(&old[shared_prefix_count..]); - // } + // If none of the old keys are reused by the new children, then we + // remove all the remaining old children and create the new children + // afresh. + if shared_suffix_count == 0 && shared_keys.is_empty() { + if shared_prefix_count == 0 { + // self.commit_traversal(); + self.remove_all_children(old); + } else { + // self.go_down_to_child(shared_prefix_count); + // self.commit_traversal(); + self.remove_self_and_next_siblings(&old[shared_prefix_count..]); + } - // self.create_and_append_children(new); + self.create_and_append_children(new); - // return; - // } + return; + } - // // Save each of the old children whose keys are reused in the new - // // children. - // let mut old_index_to_temp = vec![u32::MAX; old.len()]; - // let mut start = 0; - // loop { - // let end = (start..old.len()) - // .find(|&i| { - // let key = old[i].key(); - // !shared_keys.contains(&key) - // }) - // .unwrap_or(old.len()); + // Save each of the old children whose keys are reused in the new + // children. + let mut old_index_to_temp = vec![u32::MAX; old.len()]; + let mut start = 0; + loop { + let end = (start..old.len()) + .find(|&i| { + let key = old[i].key(); + !shared_keys.contains(&key) + }) + .unwrap_or(old.len()); - // if end - start > 0 { - // // self.commit_traversal(); - // let mut t = self.save_children_to_temporaries( - // shared_prefix_count + start, - // shared_prefix_count + end, - // ); - // for i in start..end { - // old_index_to_temp[i] = t; - // t += 1; - // } - // } + if end - start > 0 { + // self.commit_traversal(); + // let mut t = self.save_children_to_temporaries( + // shared_prefix_count + start, + // shared_prefix_count + end, + // ); + // for i in start..end { + // old_index_to_temp[i] = t; + // t += 1; + // } + } - // debug_assert!(end <= old.len()); - // if end == old.len() { - // break; - // } else { - // start = end + 1; - // } - // } + debug_assert!(end <= old.len()); + if end == old.len() { + break; + } else { + start = end + 1; + } + } // // Remove any old children whose keys were not reused in the new // // children. Remove from the end first so that we don't mess up indices. @@ -1147,17 +1101,13 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { Ordering::Greater => { for item in &old[new.len()..] { for i in RealChildIterator::new(item, self.vdom) { - self.push_root(i.element_id().unwrap()); - self.remove(); + self.edit_push_root(i.direct_id()); + self.edit_remove(); } } } // old.len < new.len -> adding some nodes Ordering::Less => { - // [... parent last_child] - // self.go_up(); - // [... parent] - // self.commit_traversal(); self.create_and_append_children(&new[old.len()..]); } // old.len == new.len -> no nodes added/removed, but πerhaps changed @@ -1168,7 +1118,24 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // ====================== // Support methods // ====================== - + // Remove all of a node's children. + // + // The change list stack must have this shape upon entry to this function: + // + // [... parent] + // + // When this function returns, the change list stack is in the same state. + fn remove_all_children(&mut self, old: &'bump [VNode<'bump>]) { + // debug_assert!(self.traversal_is_committed()); + log::debug!("REMOVING CHILDREN"); + for _child in old { + // registry.remove_subtree(child); + } + // Fast way to remove all children: set the node's textContent to an empty + // string. + todo!() + // self.set_inner_text(""); + } // Remove the current child and all of its following siblings. // // The change list stack must have this shape upon entry to this function: @@ -1208,10 +1175,78 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // self.remove_self_and_next_siblings(); } + fn create_garbage(&mut self, node: &'bump VNode<'bump>) { + let cur_scope = self.current_scope().unwrap(); + let scope = self.get_scope(&cur_scope).unwrap(); + let garbage: &'bump VNode<'static> = unsafe { std::mem::transmute(node) }; + scope.pending_garbage.borrow_mut().push(garbage); + } + + fn immediately_dispose_garabage(&mut self, node: ElementId) { + self.vdom.collect_garbage(node) + } + + fn replace_node_with_node( + &mut self, + anchor: ElementId, + old_node: &'bump VNode<'bump>, + new_node: &'bump VNode<'bump>, + ) { + self.edit_push_root(anchor); + let meta = self.create_vnode(new_node); + self.edit_replace_with(meta.added_to_stack); + self.create_garbage(old_node); + self.edit_pop(); + } + + // assumes + fn remove_many_and_return_the_first_root(&mut self) -> ElementId { + todo!() + } + + fn remove_vnode(&mut self, node: &'bump VNode<'bump>) { + match &node.kind { + VNodeKind::Text(el) => self.immediately_dispose_garabage(node.direct_id()), + VNodeKind::Element(el) => { + self.immediately_dispose_garabage(node.direct_id()); + for child in el.children { + self.remove_vnode(&child); + } + } + VNodeKind::Anchor(a) => { + // + } + VNodeKind::Fragment(frag) => { + for child in frag.children { + self.remove_vnode(&child); + } + } + VNodeKind::Component(el) => { + // + // self.destroy_scopes(old_scope) + } + VNodeKind::Suspended(_) => todo!(), + } + } + + fn current_scope(&self) -> Option { + self.scope_stack.last().map(|f| f.clone()) + } + + fn fix_listener<'a>(&mut self, listener: &'a Listener<'a>) { + let scope_id = self.current_scope(); + if let Some(scope_id) = scope_id { + let scope = self.get_scope(&scope_id).unwrap(); + let mut queue = scope.listeners.borrow_mut(); + let long_listener: &'a Listener<'static> = unsafe { std::mem::transmute(listener) }; + queue.push(long_listener as *const _) + } + } + pub fn get_scope_mut(&mut self, id: &ScopeId) -> Option<&'bump mut Scope> { // ensure we haven't seen this scope before // if we have, then we're trying to alias it, which is not allowed - debug_assert!(!self.seen_nodes.contains(id)); + debug_assert!(!self.seen_scopes.contains(id)); unsafe { self.vdom.get_scope_mut(*id) } } @@ -1222,39 +1257,39 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { } // Navigation - pub(crate) fn push_root(&mut self, root: ElementId) { + pub(crate) fn edit_push_root(&mut self, root: ElementId) { let id = root.as_u64(); self.edits.push(PushRoot { id }); } - pub(crate) fn pop(&mut self) { + pub(crate) fn edit_pop(&mut self) { self.edits.push(PopRoot {}); } // Add Nodes to the dom // add m nodes from the stack - pub(crate) fn append_children(&mut self, many: u32) { + pub(crate) fn edit_append_children(&mut self, many: u32) { self.edits.push(AppendChildren { many }); } // replace the n-m node on the stack with the m nodes // ends with the last element of the chain on the top of the stack - pub(crate) fn replace_with(&mut self, many: u32) { + pub(crate) fn edit_replace_with(&mut self, many: u32) { self.edits.push(ReplaceWith { many }); } // Remove Nodesfrom the dom - pub(crate) fn remove(&mut self) { + pub(crate) fn edit_remove(&mut self) { self.edits.push(Remove); } // Create - pub(crate) fn create_text_node(&mut self, text: &'bump str, id: ElementId) { + pub(crate) fn edit_create_text_node(&mut self, text: &'bump str, id: ElementId) { let id = id.as_u64(); self.edits.push(CreateTextNode { text, id }); } - pub(crate) fn create_element( + pub(crate) fn edit_create_element( &mut self, tag: &'static str, ns: Option<&'static str>, @@ -1268,16 +1303,15 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { } // placeholders are nodes that don't get rendered but still exist as an "anchor" in the real dom - pub(crate) fn create_placeholder(&mut self, id: ElementId) { + pub(crate) fn edit_create_placeholder(&mut self, id: ElementId) { let id = id.as_u64(); self.edits.push(CreatePlaceholder { id }); } // events - pub(crate) fn new_event_listener(&mut self, listener: &Listener) { + pub(crate) fn edit_new_event_listener(&mut self, listener: &Listener, scope: ScopeId) { let Listener { event, - scope, mounted_node, .. } = listener; @@ -1285,22 +1319,22 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { let element_id = mounted_node.get().unwrap().as_u64(); self.edits.push(NewEventListener { - scope: scope.clone(), + scope, event_name: event, mounted_node_id: element_id, }); } - pub(crate) fn remove_event_listener(&mut self, event: &'static str) { + pub(crate) fn edit_remove_event_listener(&mut self, event: &'static str) { self.edits.push(RemoveEventListener { event }); } // modify - pub(crate) fn set_text(&mut self, text: &'bump str) { + pub(crate) fn edit_set_text(&mut self, text: &'bump str) { self.edits.push(SetText { text }); } - pub(crate) fn set_attribute(&mut self, attribute: &'bump Attribute) { + pub(crate) fn edit_set_attribute(&mut self, attribute: &'bump Attribute) { let Attribute { name, value, @@ -1318,7 +1352,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { }); } - pub(crate) fn remove_attribute(&mut self, attribute: &Attribute) { + pub(crate) fn edit_remove_attribute(&mut self, attribute: &Attribute) { let name = attribute.name; self.edits.push(RemoveAttribute { name }); } @@ -1367,7 +1401,7 @@ fn find_first_real_node<'a>( /// /// This iterator is useful when it's important to load the next real root onto the top of the stack for operations like /// "InsertBefore". -struct RealChildIterator<'a> { +pub struct RealChildIterator<'a> { scopes: &'a SharedResources, // Heuristcally we should never bleed into 4 completely nested fragments/components @@ -1377,7 +1411,7 @@ struct RealChildIterator<'a> { } impl<'a> RealChildIterator<'a> { - fn new(starter: &'a VNode<'a>, scopes: &'a SharedResources) -> Self { + pub fn new(starter: &'a VNode<'a>, scopes: &'a SharedResources) -> Self { Self { scopes, stack: smallvec::smallvec![(0, starter)], @@ -1420,12 +1454,30 @@ impl<'a> Iterator for RealChildIterator<'a> { should_push = Some(&frag.children[subcount]); } } + // // If we get a fragment we push the next child + // VNodeKind::Fragment(frag) => { + // let subcount = *count as usize; + + // if frag.children.len() == 0 { + // should_pop = true; + // returned_node = Some(&*node); + // } + + // if subcount >= frag.children.len() { + // should_pop = true; + // } else { + // should_push = Some(&frag.children[subcount]); + // } + // } // Immediately abort suspended nodes - can't do anything with them yet - // VNodeKind::Suspended => should_pop = true, - VNodeKind::Suspended { node, .. } => { + VNodeKind::Suspended(node) => { + // VNodeKind::Suspended => should_pop = true, + todo!() + } + + VNodeKind::Anchor(a) => { todo!() - // *node = node.as_ref().borrow().get().expect("msg"); } // For components, we load their root and push them onto the stack diff --git a/packages/core/src/editor.rs b/packages/core/src/editor.rs index 5c2e116eb..38244552d 100644 --- a/packages/core/src/editor.rs +++ b/packages/core/src/editor.rs @@ -64,3 +64,24 @@ pub enum DomEdit<'bump> { name: &'static str, }, } +impl DomEdit<'_> { + pub fn is(&self, id: &'static str) -> bool { + match self { + DomEdit::PushRoot { .. } => id == "PushRoot", + DomEdit::PopRoot => id == "PopRoot", + DomEdit::AppendChildren { .. } => id == "AppendChildren", + DomEdit::ReplaceWith { .. } => id == "ReplaceWith", + DomEdit::Remove => id == "Remove", + DomEdit::RemoveAllChildren => id == "RemoveAllChildren", + DomEdit::CreateTextNode { .. } => id == "CreateTextNode", + DomEdit::CreateElement { .. } => id == "CreateElement", + DomEdit::CreateElementNs { .. } => id == "CreateElementNs", + DomEdit::CreatePlaceholder { .. } => id == "CreatePlaceholder", + DomEdit::NewEventListener { .. } => id == "NewEventListener", + DomEdit::RemoveEventListener { .. } => id == "RemoveEventListener", + DomEdit::SetText { .. } => id == "SetText", + DomEdit::SetAttribute { .. } => id == "SetAttribute", + DomEdit::RemoveAttribute { .. } => id == "RemoveAttribute", + } + } +} diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 66c59681f..59c520f49 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -9,7 +9,10 @@ use std::{ rc::Rc, }; -use crate::innerlude::{ElementId, HeightMarker, ScopeId}; +use crate::{ + innerlude::{ElementId, HeightMarker, ScopeId}, + VNode, +}; #[derive(Debug)] pub struct EventTrigger { @@ -46,15 +49,16 @@ impl EventTrigger { /// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes. #[derive(Debug)] pub enum EventPriority { - /// "Immediate" work will interrupt whatever work is currently being done and force its way through. This type of work - /// is typically reserved for small changes to single elements. + /// Garbage collection is a type of work than can be scheduled around other work, but must be completed in a specific + /// order. The GC must be run for a component before any other future work for that component is run. Otherwise, + /// we will leak slots in our slab. /// - /// The primary user of the "Immediate" priority is the `Signal` API which performs surgical mutations to the DOM. - Immediate, + /// Garbage collection mixes with the safety aspects of the virtualdom so it's very important to get it done before + /// other work. + GarbageCollection, /// "High Priority" work will not interrupt other high priority work, but will interrupt long medium and low priority work. /// - /// /// This is typically reserved for things like user interaction. High, @@ -106,6 +110,8 @@ pub enum VirtualEvent { MouseEvent(on::MouseEvent), PointerEvent(on::PointerEvent), + GarbageCollection, + // image event has conflicting method types // ImageEvent(event_data::ImageEvent), SetStateEvent { @@ -198,7 +204,6 @@ pub mod on { Listener { event: shortname, mounted_node: Cell::new(None), - scope: c.scope.our_arena_idx, callback: RefCell::new(Some(callback)), } } diff --git a/packages/core/src/hooks.rs b/packages/core/src/hooks.rs index 3add4a7f1..d7e478606 100644 --- a/packages/core/src/hooks.rs +++ b/packages/core/src/hooks.rs @@ -190,11 +190,10 @@ where None => { // Some(VNode { - dom_id: empty_cell(), key: None, - kind: VNodeKind::Suspended { + kind: VNodeKind::Suspended(VSuspended { node: domnode.clone(), - }, + }), }) } } @@ -250,7 +249,8 @@ impl<'src> SuspendedContext<'src> { lazy_nodes: LazyNodes<'src, F>, ) -> DomTree<'src> { let scope_ref = self.inner.scope; + let bump = &self.inner.scope.frames.wip_frame().bump; - Some(lazy_nodes.into_vnode(NodeFactory { scope: scope_ref })) + Some(lazy_nodes.into_vnode(NodeFactory { bump })) } } diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 14c9855ab..34506c4e0 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -1,4 +1,5 @@ #![allow(non_snake_case)] +#![doc = include_str!("../README.md")] //! Dioxus Core //! ---------- //! diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index c2a4c83a4..1b7de1c00 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -7,7 +7,7 @@ use crate::{ events::VirtualEvent, innerlude::{empty_cell, Context, DomTree, ElementId, Properties, Scope, ScopeId, FC}, }; -use bumpalo::boxed::Box as BumpBox; +use bumpalo::{boxed::Box as BumpBox, Bump}; use std::{ cell::{Cell, RefCell}, fmt::{Arguments, Debug, Formatter}, @@ -20,16 +20,24 @@ pub struct VNode<'src> { pub kind: VNodeKind<'src>, pub(crate) key: Option<&'src str>, - - /// ElementId supports NonZero32 and Cell is zero cost, so the size of this field is unaffected - pub(crate) dom_id: Cell>, } -impl VNode<'_> { - pub fn key(&self) -> Option<&str> { + +impl<'src> VNode<'src> { + pub fn key(&self) -> Option<&'src str> { self.key } - pub fn element_id(&self) -> Option { - self.dom_id.get() + pub fn direct_id(&self) -> ElementId { + self.try_direct_id().unwrap() + } + pub fn try_direct_id(&self) -> Option { + match &self.kind { + VNodeKind::Text(el) => el.dom_id.get(), + VNodeKind::Element(el) => el.dom_id.get(), + VNodeKind::Anchor(el) => el.dom_id.get(), + VNodeKind::Fragment(_) => None, + VNodeKind::Component(_) => None, + VNodeKind::Suspended(_) => None, + } } } @@ -47,11 +55,18 @@ pub enum VNodeKind<'src> { Component(&'src VComponent<'src>), - Suspended { node: Rc>> }, + Suspended(VSuspended), + + Anchor(VAnchor), +} + +pub struct VAnchor { + pub dom_id: Cell>, } pub struct VText<'src> { pub text: &'src str, + pub dom_id: Cell>, pub is_static: bool, } @@ -77,6 +92,7 @@ pub struct VElement<'a> { // tag is always static pub tag_name: &'static str, pub namespace: Option<&'static str>, + pub dom_id: Cell>, pub static_listeners: bool, pub listeners: &'a [Listener<'a>], @@ -110,8 +126,6 @@ pub struct Listener<'bump> { /// The type of event to listen for. pub(crate) event: &'static str, - pub scope: ScopeId, - pub mounted_node: Cell>, pub(crate) callback: RefCell>>, @@ -150,27 +164,42 @@ pub struct VComponent<'src> { pub(crate) user_fc: *const (), } +pub struct VSuspended { + pub node: Rc>>, +} + /// This struct provides an ergonomic API to quickly build VNodes. /// /// NodeFactory is used to build VNodes in the component's memory space. /// This struct adds metadata to the final VNode about listeners, attributes, and children #[derive(Copy, Clone)] pub struct NodeFactory<'a> { - pub scope: &'a Scope, + pub(crate) bump: &'a Bump, } impl<'a> NodeFactory<'a> { + pub fn new(bump: &'a Bump) -> NodeFactory<'a> { + NodeFactory { bump } + } + #[inline] pub fn bump(&self) -> &'a bumpalo::Bump { - &self.scope.frames.wip_frame().bump + &self.bump + } + + pub fn render_directly(&self, lazy_nodes: LazyNodes<'a, F>) -> DomTree<'a> + where + F: FnOnce(NodeFactory<'a>) -> VNode<'a>, + { + Some(lazy_nodes.into_vnode(NodeFactory { bump: self.bump })) } pub fn unstable_place_holder() -> VNode<'static> { VNode { - dom_id: empty_cell(), key: None, kind: VNodeKind::Text(VText { text: "", + dom_id: empty_cell(), is_static: true, }), } @@ -179,9 +208,9 @@ impl<'a> NodeFactory<'a> { /// Used in a place or two to make it easier to build vnodes from dummy text pub fn static_text(&self, text: &'static str) -> VNode<'a> { VNode { - dom_id: empty_cell(), key: None, kind: VNodeKind::Text(VText { + dom_id: empty_cell(), text, is_static: true, }), @@ -208,9 +237,12 @@ impl<'a> NodeFactory<'a> { pub fn text(&self, args: Arguments) -> VNode<'a> { let (text, is_static) = self.raw_text(args); VNode { - dom_id: empty_cell(), key: None, - kind: VNodeKind::Text(VText { text, is_static }), + kind: VNodeKind::Text(VText { + text, + is_static, + dom_id: empty_cell(), + }), } } @@ -260,19 +292,7 @@ impl<'a> NodeFactory<'a> { let children: &'a V = self.bump().alloc(children); let children = children.as_ref(); - // We take the references directly from the bump arena - // - // TODO: this code shouldn't necessarily be here of all places - // It would make more sense to do this in diffing - - let mut queue = self.scope.listeners.borrow_mut(); - for listener in listeners.iter() { - let long_listener: &'a Listener<'static> = unsafe { std::mem::transmute(listener) }; - queue.push(long_listener as *const _) - } - VNode { - dom_id: empty_cell(), key, kind: VNodeKind::Element(self.bump().alloc(VElement { tag_name: tag, @@ -280,6 +300,7 @@ impl<'a> NodeFactory<'a> { listeners, attributes, children, + dom_id: empty_cell(), // todo: wire up more constization static_listeners: false, @@ -291,11 +312,10 @@ impl<'a> NodeFactory<'a> { pub fn suspended() -> VNode<'static> { VNode { - dom_id: empty_cell(), key: None, - kind: VNodeKind::Suspended { + kind: VNodeKind::Suspended(VSuspended { node: Rc::new(empty_cell()), - }, + }), } } @@ -390,7 +410,6 @@ impl<'a> NodeFactory<'a> { VNode { key, - dom_id: empty_cell(), kind: VNodeKind::Component(self.bump().alloc_with(|| VComponent { user_fc, comparator, @@ -430,7 +449,6 @@ impl<'a> NodeFactory<'a> { let children = node_iter.into_vnode_list(self); VNode { - dom_id: empty_cell(), key: None, kind: VNodeKind::Fragment(VFragment { children, @@ -491,6 +509,15 @@ where } } + if nodes.len() == 0 { + nodes.push(VNode { + kind: VNodeKind::Anchor(VAnchor { + dom_id: empty_cell(), + }), + key: None, + }); + } + nodes.into_bump_slice() } } @@ -625,8 +652,10 @@ impl Debug for NodeFactory<'_> { impl Debug for VNode<'_> { fn fmt(&self, s: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { match &self.kind { - VNodeKind::Element(el) => write!(s, "element, {}", el.tag_name), - VNodeKind::Text(t) => write!(s, "text, {}", t.text), + VNodeKind::Element(el) => write!(s, "VElement {{ name: {} }}", el.tag_name), + VNodeKind::Text(t) => write!(s, "VText {{ text: {} }}", t.text), + VNodeKind::Anchor(a) => write!(s, "VAnchor"), + VNodeKind::Fragment(_) => write!(s, "fragment"), VNodeKind::Suspended { .. } => write!(s, "suspended"), VNodeKind::Component(_) => write!(s, "component"), diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index 65bcc1999..cffdaf8c9 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -1,5 +1,6 @@ use crate::innerlude::*; use bumpalo::boxed::Box as BumpBox; +use fxhash::FxHashSet; use std::{ any::{Any, TypeId}, borrow::BorrowMut, @@ -24,13 +25,15 @@ pub struct Scope { pub(crate) parent_idx: Option, pub(crate) our_arena_idx: ScopeId, pub(crate) height: u32, - pub(crate) descendents: RefCell>, + pub(crate) descendents: RefCell>, // Nodes // an internal, highly efficient storage of vnodes + // lots of safety condsiderations pub(crate) frames: ActiveFrame, pub(crate) caller: Rc, pub(crate) child_nodes: ScopeChildren<'static>, + pub(crate) pending_garbage: RefCell>>, // Listeners pub(crate) listeners: RefCell>>, @@ -67,11 +70,18 @@ impl Scope { height: u32, child_nodes: ScopeChildren, - // child_nodes: &'creator_node [VNode<'creator_node>], + vdom: SharedResources, ) -> Self { let child_nodes = unsafe { child_nodes.extend_lifetime() }; + // insert ourself as a descendent of the parent + // when the parent is removed, this map will be traversed, and we will also be cleaned up. + if let Some(parent) = &parent { + let parent = unsafe { vdom.get_scope(*parent) }.unwrap(); + parent.descendents.borrow_mut().insert(arena_idx); + } + Self { child_nodes, caller, @@ -80,10 +90,12 @@ impl Scope { height, vdom, frames: ActiveFrame::new(), + hooks: Default::default(), shared_contexts: Default::default(), listeners: Default::default(), descendents: Default::default(), + pending_garbage: Default::default(), } } @@ -102,6 +114,9 @@ impl Scope { // 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 + if !self.pending_garbage.borrow().is_empty() { + panic!("cannot run scope while garbage is pending! Please clean up your mess first"); + } log::debug!("reset okay"); @@ -189,11 +204,11 @@ impl Scope { if let Some(raw_listener) = raw_listener { let listener = unsafe { &**raw_listener }; - log::info!( - "calling listener {:?}, {:?}", - listener.event, - listener.scope - ); + // log::info!( + // "calling listener {:?}, {:?}", + // listener.event, + // // listener.scope + // ); let mut cb = listener.callback.borrow_mut(); if let Some(cb) = cb.as_mut() { (cb)(event); @@ -209,10 +224,20 @@ impl Scope { self.frames.fin_head() } - // #[inline] pub fn child_nodes<'a>(&'a self) -> ScopeChildren { - // self.child_nodes unsafe { self.child_nodes.unextend_lfetime() } - // unsafe { std::mem::transmute(self.child_nodes) } + } + + pub fn consume_garbage(&self) -> Vec<&VNode> { + let mut garbage = self.pending_garbage.borrow_mut(); + garbage + .drain(..) + .map(|node| { + // safety: scopes cannot cycle without their garbage being collected. these nodes are safe + let node: &VNode<'static> = unsafe { &*node }; + let node: &VNode = unsafe { std::mem::transmute(node) }; + node + }) + .collect::>() } } diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs index b4409a813..673a1923c 100644 --- a/packages/core/src/util.rs +++ b/packages/core/src/util.rs @@ -5,7 +5,7 @@ use crate::innerlude::*; // create a cell with a "none" value #[inline] pub fn empty_cell() -> Cell> { - Cell::new(None as Option) + Cell::new(None) } /// A helper type that lets scopes be ordered by their height diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index b096f8d5f..40b4b8569 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -25,7 +25,7 @@ use crate::{arena::SharedResources, innerlude::*}; use std::any::Any; use std::any::TypeId; -use std::cell::RefCell; +use std::cell::{Ref, RefCell, RefMut}; use std::pin::Pin; /// An integrated virtual node system that progresses events and diffs UI trees. @@ -193,8 +193,8 @@ impl VirtualDom { // We run the component. If it succeeds, then we can diff it and add the changes to the dom. if cur_component.run_scope().is_ok() { - let meta = diff_machine.create(cur_component.frames.fin_head()); - diff_machine.append_children(meta.added_to_stack); + let meta = diff_machine.create_vnode(cur_component.frames.fin_head()); + diff_machine.edit_append_children(meta.added_to_stack); } else { // todo: should this be a hard error? log::warn!( @@ -269,6 +269,47 @@ impl VirtualDom { let mut diff_machine = DiffMachine::new(edits, realdom, trigger.originator, &self.shared); match &trigger.event { + // When a scope gets destroyed during a diff, it gets its own garbage collection event + // However, an old scope might be attached + VirtualEvent::GarbageCollection => { + let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); + + let mut garbage_list = scope.consume_garbage(); + + while let Some(node) = garbage_list.pop() { + match &node.kind { + VNodeKind::Text(_) => { + // + self.shared.collect_garbage(node.direct_id()) + } + VNodeKind::Anchor(anchor) => { + // + } + + VNodeKind::Element(el) => { + self.shared.collect_garbage(node.direct_id()); + for child in el.children { + garbage_list.push(child); + } + } + + VNodeKind::Fragment(frag) => { + for child in frag.children { + garbage_list.push(child); + } + } + VNodeKind::Component(comp) => { + // run the destructors + todo!(); + } + VNodeKind::Suspended(node) => { + // make sure the task goes away + todo!(); + } + } + } + } + // Nothing yet VirtualEvent::AsyncEvent { .. } => {} @@ -295,13 +336,13 @@ impl VirtualDom { // push the old node's root onto the stack let real_id = domnode.get().ok_or(Error::NotMounted)?; - diff_machine.push_root(real_id); + diff_machine.edit_push_root(real_id); // push these new nodes onto the diff machines stack - let meta = diff_machine.create(&*nodes); + let meta = diff_machine.create_vnode(&*nodes); // replace the placeholder with the new nodes we just pushed on the stack - diff_machine.replace_with(meta.added_to_stack); + diff_machine.edit_replace_with(meta.added_to_stack); } } } @@ -330,7 +371,7 @@ impl VirtualDom { // Make sure this isn't a node we've already seen, we don't want to double-render anything // If we double-renderer something, this would cause memory safety issues - if diff_machine.seen_nodes.contains(&update.idx) { + if diff_machine.seen_scopes.contains(&update.idx) { log::debug!("Skipping update for: {:#?}", update); continue; } @@ -342,7 +383,7 @@ impl VirtualDom { .expect("Failed to find scope or borrow would be aliasing"); // Now, all the "seen nodes" are nodes that got notified by running this listener - diff_machine.seen_nodes.insert(update.idx.clone()); + diff_machine.seen_scopes.insert(update.idx.clone()); if cur_component.run_scope().is_ok() { let (old, new) = ( diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index 73ce38ada..bf0420de4 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -1,17 +1,111 @@ -use dioxus::prelude::*; +use bumpalo::Bump; + +use anyhow::{Context, Result}; +use dioxus::{ + arena::SharedResources, + diff::{CreateMeta, DiffMachine}, + prelude::*, + util::DebugDom, + DomEdit, +}; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; +struct TestDom { + bump: Bump, + resources: SharedResources, +} +impl TestDom { + fn new() -> TestDom { + let bump = Bump::new(); + let resources = SharedResources::new(); + TestDom { bump, resources } + } + fn new_factory<'a>(&'a self) -> NodeFactory<'a> { + NodeFactory::new(&self.bump) + } + + fn render<'a, F>(&'a self, lazy_nodes: LazyNodes<'a, F>) -> VNode<'a> + where + F: FnOnce(NodeFactory<'a>) -> VNode<'a>, + { + use dioxus_core::nodes::{IntoVNode, IntoVNodeList}; + lazy_nodes.into_vnode(NodeFactory::new(&self.bump)) + } + + fn diff<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Vec> { + let mut edits = Vec::new(); + let dom = DebugDom::new(); + let mut machine = DiffMachine::new_headless(&mut edits, &dom, &self.resources); + machine.diff_node(old, new); + edits + } + + fn create<'a, F1>(&'a self, left: LazyNodes<'a, F1>) -> (CreateMeta, Vec>) + where + F1: FnOnce(NodeFactory<'a>) -> VNode<'a>, + { + let old = self.bump.alloc(self.render(left)); + let mut edits = Vec::new(); + let dom = DebugDom::new(); + + let mut machine = DiffMachine::new_headless(&mut edits, &dom, &self.resources); + let meta = machine.create_vnode(old); + (meta, edits) + } + + fn lazy_diff<'a, F1, F2>( + &'a self, + left: LazyNodes<'a, F1>, + right: LazyNodes<'a, F2>, + ) -> Vec> + where + F1: FnOnce(NodeFactory<'a>) -> VNode<'a>, + F2: FnOnce(NodeFactory<'a>) -> VNode<'a>, + { + let old = self.bump.alloc(self.render(left)); + + let new = self.bump.alloc(self.render(right)); + + let mut edits = Vec::new(); + let dom = DebugDom::new(); + + let mut machine = DiffMachine::new_headless(&mut edits, &dom, &self.resources); + machine.create_vnode(old); + edits.clear(); + + let mut machine = DiffMachine::new_headless(&mut edits, &dom, &self.resources); + machine.diff_node(old, new); + edits + } +} + #[test] fn diffing_works() {} #[test] fn html_and_rsx_generate_the_same_output() { - let old = rsx! { - div { "Hello world!" } - }; + let dom = TestDom::new(); - // let new = html! { - //
"Hello world!"
- // }; + let edits = dom.lazy_diff( + rsx! ( div { "Hello world" } ), + rsx! ( div { "Goodbye world" } ), + ); + dbg!(edits); +} + +#[test] +fn fragments_create_properly() { + let dom = TestDom::new(); + let (meta, edits) = dom.create(rsx! { + div { "Hello a" } + div { "Hello b" } + div { "Hello c" } + }); + assert!(&edits[0].is("CreateElement")); + assert!(&edits[3].is("CreateElement")); + assert!(&edits[6].is("CreateElement")); + + assert_eq!(meta.added_to_stack, 3); + dbg!(edits); } diff --git a/packages/ssr/src/lib.rs b/packages/ssr/src/lib.rs index d8f4d700f..25881c400 100644 --- a/packages/ssr/src/lib.rs +++ b/packages/ssr/src/lib.rs @@ -120,7 +120,7 @@ impl<'a> TextRenderer<'a> { // // when the page is loaded, the `querySelectorAll` will be used to collect all the nodes, and then add // them interpreter's stack - match (self.cfg.pre_render, node.element_id()) { + match (self.cfg.pre_render, node.dom_id()) { (true, Some(id)) => { write!(f, " dio_el=\"{}\"", id)?; // From 1749eba8eb6c8ffa38ab3ad52160b637fe021e86 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 29 Jul 2021 18:56:09 -0400 Subject: [PATCH 14/21] feat: more and more sophisticated diff --- packages/core/src/diff.rs | 126 +++++++++++++++++-------------- packages/core/src/editor.rs | 6 +- packages/core/src/virtual_dom.rs | 2 +- 3 files changed, 76 insertions(+), 58 deletions(-) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 41b9084cd..cc9f48139 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -334,53 +334,14 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { | VNodeKind::Element(_) | VNodeKind::Anchor(_), ) => { - todo!(); - // log::info!( - // "taking the awkard diffing path {:#?}, {:#?}", - // old_node, - // new_node - // ); - // // currently busted for components - need to fid - // let root = old_node.dom_id.get().expect(&format!( - // "Should not be diffing old nodes that were never assigned, {:#?}", - // old_node - // )); - // // Choose the node to use as the placeholder for replacewith - // let back_node_id = match old_node.kind { - // // We special case these two types to avoid allocating the small-vecs - // VNodeKind::Element(_) | VNodeKind::Text(_) => root, - - // _ => { - // let mut old_iter = RealChildIterator::new(old_node, &self.vdom); - - // let back_node = old_iter - // .next() - // .expect("Empty fragments should generate a placeholder."); - - // // remove any leftovers - // for to_remove in old_iter { - // self.edit_push_root(to_remove.dom_id().unwrap()); - // self.edit_remove(); - // } - - // back_node.dom_id().unwrap() - // } - // }; - - // // replace the placeholder or first node with the nodes generated from the "new" - // self.edit_push_root(back_node_id); - // let meta = self.create_vnode(new_node); - // self.edit_replace_with(meta.added_to_stack); - - // // todo use the is_static metadata to update this subtree + self.replace_many_with_many([old_node], new_node); } // TODO - (VNodeKind::Suspended(node), new) => todo!(), - (old, VNodeKind::Suspended { .. }) => { - // a node that was once real is now suspended - // - } + (VNodeKind::Suspended(_), new) => todo!(), + + // a node that was once real is now suspended + (old, VNodeKind::Suspended(_)) => todo!(), } } @@ -636,17 +597,28 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { let first_new = &new[0]; match (&first_old.kind, &first_new.kind) { + // Anchors can only appear in empty fragments (VNodeKind::Anchor(old_anchor), VNodeKind::Anchor(new_anchor)) => { old_anchor.dom_id.set(new_anchor.dom_id.get()); } + + // Replace the anchor with whatever new nodes are coming down the pipe (VNodeKind::Anchor(anchor), _) => { - // replace new anchor with many - todo!(); + self.edit_push_root(anchor.dom_id.get().unwrap()); + let mut added = 0; + for el in new { + let meta = self.create_vnode(el); + added += meta.added_to_stack; + } + self.edit_replace_with(1, added); } + + // Replace whatever nodes are sitting there with the anchor (_, VNodeKind::Anchor(anchor)) => { - // replace many with new anchor - todo!(); + self.replace_many_with_many(old, first_new); } + + // Use the complex diff algorithm to diff the nodes _ => { let new_is_keyed = new[0].key.is_some(); let old_is_keyed = old[0].key.is_some(); @@ -1175,6 +1147,53 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // self.remove_self_and_next_siblings(); } + fn replace_many_with_many( + &mut self, + old_node: impl IntoIterator>, + // old_node: &'bump VNode<'bump>, + new_node: &'bump VNode<'bump>, + ) { + let mut nodes_to_replace = Vec::new(); + let mut nodes_to_search = old_node.into_iter().collect::>(); + let mut scopes_obliterated = Vec::new(); + while let Some(node) = nodes_to_search.pop() { + match &node.kind { + // the ones that have a direct id + VNodeKind::Text(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), + VNodeKind::Element(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), + VNodeKind::Anchor(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), + VNodeKind::Suspended(el) => nodes_to_replace.push(el.node.get().unwrap()), + + VNodeKind::Fragment(frag) => { + for child in frag.children { + nodes_to_search.push(child); + } + } + VNodeKind::Component(el) => { + let scope_id = el.ass_scope.get().unwrap(); + let scope = self.get_scope(&scope_id).unwrap(); + let root = scope.root(); + nodes_to_search.push(root); + scopes_obliterated.push(scope_id); + } + } + } + + let n = nodes_to_replace.len(); + for node in nodes_to_replace { + self.edit_push_root(node); + } + + let meta = self.create_vnode(new_node); + + self.edit_replace_with(n as u32, meta.added_to_stack); + + // obliterate! + for scope in scopes_obliterated { + self.destroy_scopes(scope); + } + } + fn create_garbage(&mut self, node: &'bump VNode<'bump>) { let cur_scope = self.current_scope().unwrap(); let scope = self.get_scope(&cur_scope).unwrap(); @@ -1194,16 +1213,11 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { ) { self.edit_push_root(anchor); let meta = self.create_vnode(new_node); - self.edit_replace_with(meta.added_to_stack); + self.edit_replace_with(1, meta.added_to_stack); self.create_garbage(old_node); self.edit_pop(); } - // assumes - fn remove_many_and_return_the_first_root(&mut self) -> ElementId { - todo!() - } - fn remove_vnode(&mut self, node: &'bump VNode<'bump>) { match &node.kind { VNodeKind::Text(el) => self.immediately_dispose_garabage(node.direct_id()), @@ -1274,8 +1288,8 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // replace the n-m node on the stack with the m nodes // ends with the last element of the chain on the top of the stack - pub(crate) fn edit_replace_with(&mut self, many: u32) { - self.edits.push(ReplaceWith { many }); + pub(crate) fn edit_replace_with(&mut self, n: u32, m: u32) { + self.edits.push(ReplaceWith { n, m }); } // Remove Nodesfrom the dom diff --git a/packages/core/src/editor.rs b/packages/core/src/editor.rs index 38244552d..a30a89498 100644 --- a/packages/core/src/editor.rs +++ b/packages/core/src/editor.rs @@ -24,7 +24,11 @@ pub enum DomEdit<'bump> { many: u32, }, ReplaceWith { - many: u32, + // the first n elements + n: u32, + + // the last m elements + m: u32, }, Remove, RemoveAllChildren, diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 40b4b8569..95d1f8d03 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -342,7 +342,7 @@ impl VirtualDom { let meta = diff_machine.create_vnode(&*nodes); // replace the placeholder with the new nodes we just pushed on the stack - diff_machine.edit_replace_with(meta.added_to_stack); + diff_machine.edit_replace_with(1, meta.added_to_stack); } } } From 0a0be95c3e58dc065409f02f703b82700c1003f8 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 30 Jul 2021 10:35:47 -0400 Subject: [PATCH 15/21] wip: get keyed diffing compiling --- packages/core-macro/src/rsx/component.rs | 10 +- packages/core-macro/src/rsx/element.rs | 15 +- packages/core/src/diff.rs | 404 ++++++++++++++--------- packages/core/src/editor.rs | 4 + packages/core/src/nodes.rs | 19 +- packages/core/tests/diffing.rs | 213 +++++++++++- 6 files changed, 485 insertions(+), 180 deletions(-) diff --git a/packages/core-macro/src/rsx/component.rs b/packages/core-macro/src/rsx/component.rs index f493cbe3b..b30d2c8ee 100644 --- a/packages/core-macro/src/rsx/component.rs +++ b/packages/core-macro/src/rsx/component.rs @@ -261,12 +261,10 @@ impl ToTokens for Component { let key_token = match has_key { Some(field) => { - let inners = field.content.to_token_stream(); - quote! { - Some(#inners) - } + let inners = &field.content; + quote! { Some(format_args_f!(#inners)) } } - None => quote! {None}, + None => quote! { None }, }; let childs = &self.children; @@ -279,7 +277,7 @@ impl ToTokens for Component { #name, #builder, #key_token, - __cx.bump().alloc(#children) + #children ) }) } diff --git a/packages/core-macro/src/rsx/element.rs b/packages/core-macro/src/rsx/element.rs index 6250cb123..0a6b89d17 100644 --- a/packages/core-macro/src/rsx/element.rs +++ b/packages/core-macro/src/rsx/element.rs @@ -13,7 +13,7 @@ use syn::{ // ======================================= pub struct Element { name: Ident, - key: Option, + key: Option, attributes: Vec>, listeners: Vec>, children: Vec>, @@ -195,6 +195,10 @@ impl ToTokens for Element { let attr = &self.attributes; let childs = &self.children; let listeners = &self.listeners; + let key = match &self.key { + Some(ty) => quote! { Some(format_args_f!(#ty)) }, + None => quote! { None }, + }; tokens.append_all(quote! { __cx.element( @@ -202,10 +206,7 @@ impl ToTokens for Element { [ #(#listeners),* ], [ #(#attr),* ], [ #(#childs),* ], - // __cx.bump().alloc([ #(#listeners),* ]), - // __cx.bump().alloc([ #(#attr),* ]), - // __cx.bump().alloc([ #(#childs),* ]), - None, + #key, ) }); } @@ -235,7 +236,7 @@ fn parse_rsx_element_field( stream: ParseStream, attrs: &mut Vec>, listeners: &mut Vec>, - key: &mut Option, + key: &mut Option, element_name: Ident, ) -> Result<()> { let name = Ident::parse_any(stream)?; @@ -301,7 +302,7 @@ fn parse_rsx_element_field( return Ok(()); } "key" => { - *key = Some(AttrType::BumpText(stream.parse::()?)); + *key = Some(stream.parse::()?); return Ok(()); } "classes" => { diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index cc9f48139..34106f3a8 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -334,7 +334,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { | VNodeKind::Element(_) | VNodeKind::Anchor(_), ) => { - self.replace_many_with_many([old_node], new_node); + self.replace_many_with_many([old_node], [new_node]); } // TODO @@ -615,7 +615,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // Replace whatever nodes are sitting there with the anchor (_, VNodeKind::Anchor(anchor)) => { - self.replace_many_with_many(old, first_new); + self.replace_many_with_many(old, [first_new]); } // Use the complex diff algorithm to diff the nodes @@ -665,6 +665,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // // Upon exiting, the change list stack is in the same state. fn diff_keyed_children(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { + println!("diffing keyed children!"); if cfg!(debug_assertions) { let mut keys = fxhash::FxHashSet::default(); let mut assert_unique_keys = |children: &'bump [VNode<'bump>]| { @@ -749,7 +750,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { ) -> KeyedPrefixResult { let mut shared_prefix_count = 0; - for (i, (old, new)) in old.iter().zip(new.iter()).enumerate() { + for (old, new) in old.iter().zip(new.iter()) { // abort early if we finally run into nodes with different keys if old.key() != new.key() { break; @@ -770,8 +771,6 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // And if that was all of the new children, then remove all of the remaining // old children and we're finished. if shared_prefix_count == new.len() { - // self.go_to_sibling(shared_prefix_count); - // self.commit_traversal(); self.remove_self_and_next_siblings(&old[shared_prefix_count..]); return KeyedPrefixResult::Finished; } @@ -801,15 +800,17 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // this subsequence will remain in place, minimizing the number of DOM moves we // will have to do. // - // Upon entry to this function, the change list stack must be: + // Upon entry to this function, the change list stack must be empty. // + // This function will load the appropriate nodes onto the stack and do diffing in place. + // + // used to be: // [... parent] - // // Upon exit from this function, it will be restored to that same state. fn diff_keyed_middle( &mut self, old: &'bump [VNode<'bump>], - new: &'bump [VNode<'bump>], + mut new: &'bump [VNode<'bump>], shared_prefix_count: usize, shared_suffix_count: usize, old_shared_suffix_start: usize, @@ -855,20 +856,23 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // remove all the remaining old children and create the new children // afresh. if shared_suffix_count == 0 && shared_keys.is_empty() { - if shared_prefix_count == 0 { - // self.commit_traversal(); - self.remove_all_children(old); - } else { - // self.go_down_to_child(shared_prefix_count); - // self.commit_traversal(); - self.remove_self_and_next_siblings(&old[shared_prefix_count..]); - } + // create all the children anew + // load all the old childrne + // replace n to m - self.create_and_append_children(new); + // if shared_prefix_count == 0 { + // self.remove_all_children(old); + // } else { + // self.remove_self_and_next_siblings(&old[shared_prefix_count..]); + // } + + // self.create_and_append_children(new); return; } + dbg!("hello!"); + // Save each of the old children whose keys are reused in the new // children. let mut old_index_to_temp = vec![u32::MAX; old.len()]; @@ -883,14 +887,15 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { if end - start > 0 { // self.commit_traversal(); + let mut t = 5; // let mut t = self.save_children_to_temporaries( // shared_prefix_count + start, // shared_prefix_count + end, // ); - // for i in start..end { - // old_index_to_temp[i] = t; - // t += 1; - // } + for i in start..end { + old_index_to_temp[i] = t; + t += 1; + } } debug_assert!(end <= old.len()); @@ -901,132 +906,133 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { } } - // // Remove any old children whose keys were not reused in the new - // // children. Remove from the end first so that we don't mess up indices. - // let mut removed_count = 0; - // for (i, old_child) in old.iter().enumerate().rev() { - // if !shared_keys.contains(&old_child.key()) { - // // registry.remove_subtree(old_child); - // // todo - // // self.commit_traversal(); - // self.remove(old_child.dom_id.get()); - // self.remove_child(i + shared_prefix_count); - // removed_count += 1; - // } - // } + // Remove any old children whose keys were not reused in the new + // children. Remove from the end first so that we don't mess up indices. + let mut removed_count = 0; + for (i, old_child) in old.iter().enumerate().rev() { + if !shared_keys.contains(&old_child.key()) { + // self.remove(old_child.dom_id.get()); + // self.remove_child(i + shared_prefix_count); + removed_count += 1; + } + } - // // If there aren't any more new children, then we are done! - // if new.is_empty() { - // return; - // } + // If there aren't any more new children, then we are done! + if new.is_empty() { + return; + } - // // The longest increasing subsequence within `new_index_to_old_index`. This - // // is the longest sequence on DOM nodes in `old` that are relatively ordered - // // correctly within `new`. We will leave these nodes in place in the DOM, - // // and only move nodes that are not part of the LIS. This results in the - // // maximum number of DOM nodes left in place, AKA the minimum number of DOM - // // nodes moved. - // let mut new_index_is_in_lis = FxHashSet::default(); - // new_index_is_in_lis.reserve(new_index_to_old_index.len()); - // let mut predecessors = vec![0; new_index_to_old_index.len()]; - // let mut starts = vec![0; new_index_to_old_index.len()]; - // longest_increasing_subsequence::lis_with( - // &new_index_to_old_index, - // &mut new_index_is_in_lis, - // |a, b| a < b, - // &mut predecessors, - // &mut starts, - // ); + // The longest increasing subsequence within `new_index_to_old_index`. This + // is the longest sequence on DOM nodes in `old` that are relatively ordered + // correctly within `new`. We will leave these nodes in place in the DOM, + // and only move nodes that are not part of the LIS. This results in the + // maximum number of DOM nodes left in place, AKA the minimum number of DOM + // nodes moved. + let mut new_index_is_in_lis = FxHashSet::default(); + new_index_is_in_lis.reserve(new_index_to_old_index.len()); + let mut predecessors = vec![0; new_index_to_old_index.len()]; + let mut starts = vec![0; new_index_to_old_index.len()]; + longest_increasing_subsequence::lis_with( + &new_index_to_old_index, + &mut new_index_is_in_lis, + |a, b| a < b, + &mut predecessors, + &mut starts, + ); - // // Now we will iterate from the end of the new children back to the - // // beginning, diffing old children we are reusing and if they aren't in the - // // LIS moving them to their new destination, or creating new children. Note - // // that iterating in reverse order lets us use `Node.prototype.insertBefore` - // // to move/insert children. - // // - // // But first, we ensure that we have a child on the change list stack that - // // we can `insertBefore`. We handle this once before looping over `new` - // // children, so that we don't have to keep checking on every loop iteration. - // if shared_suffix_count > 0 { - // // There is a shared suffix after these middle children. We will be - // // inserting before that shared suffix, so add the first child of that - // // shared suffix to the change list stack. - // // - // // [... parent] - // self.edits - // .go_down_to_child(old_shared_suffix_start - removed_count); - // // [... parent first_child_of_shared_suffix] - // } else { - // // There is no shared suffix coming after these middle children. - // // Therefore we have to process the last child in `new` and move it to - // // the end of the parent's children if it isn't already there. - // let last_index = new.len() - 1; - // // uhhhh why an unwrap? - // let last = new.last().unwrap(); - // // let last = new.last().unwrap_throw(); - // new = &new[..new.len() - 1]; - // if shared_keys.contains(&last.key()) { - // let old_index = new_index_to_old_index[last_index]; - // let temp = old_index_to_temp[old_index]; - // // [... parent] - // self.go_down_to_temp_child(temp); - // // [... parent last] - // self.diff_node(&old[old_index], last); + // Now we will iterate from the end of the new children back to the + // beginning, diffing old children we are reusing and if they aren't in the + // LIS moving them to their new destination, or creating new children. Note + // that iterating in reverse order lets us use `Node.prototype.insertBefore` + // to move/insert children. + // + // But first, we ensure that we have a child on the change list stack that + // we can `insertBefore`. We handle this once before looping over `new` + // children, so that we don't have to keep checking on every loop iteration. + if shared_suffix_count > 0 { + // There is a shared suffix after these middle children. We will be + // inserting before that shared suffix, so add the first child of that + // shared suffix to the change list stack. + // + // [... parent] - // if new_index_is_in_lis.contains(&last_index) { - // // Don't move it, since it is already where it needs to be. - // } else { - // // self.commit_traversal(); - // // [... parent last] - // self.append_child(); - // // [... parent] - // self.go_down_to_temp_child(temp); - // // [... parent last] - // } - // } else { - // // self.commit_traversal(); - // // [... parent] - // self.create(last); + // TODO - // // [... parent last] - // self.append_child(); - // // [... parent] - // self.go_down_to_reverse_child(0); - // // [... parent last] - // } - // } + // self.edits + // .go_down_to_child(old_shared_suffix_start - removed_count); + // [... parent first_child_of_shared_suffix] + } else { + // There is no shared suffix coming after these middle children. + // Therefore we have to process the last child in `new` and move it to + // the end of the parent's children if it isn't already there. + let last_index = new.len() - 1; + // uhhhh why an unwrap? + let last = new.last().unwrap(); + // let last = new.last().unwrap_throw(); + new = &new[..new.len() - 1]; + if shared_keys.contains(&last.key()) { + let old_index = new_index_to_old_index[last_index]; + let temp = old_index_to_temp[old_index]; + // [... parent] + // self.go_down_to_temp_child(temp); + // [... parent last] + self.diff_node(&old[old_index], last); - // for (new_index, new_child) in new.iter().enumerate().rev() { - // let old_index = new_index_to_old_index[new_index]; - // if old_index == u32::MAX as usize { - // debug_assert!(!shared_keys.contains(&new_child.key())); - // // self.commit_traversal(); - // // [... parent successor] - // self.create(new_child); - // // [... parent successor new_child] - // self.insert_before(); - // // [... parent new_child] - // } else { - // debug_assert!(shared_keys.contains(&new_child.key())); - // let temp = old_index_to_temp[old_index]; - // debug_assert_ne!(temp, u32::MAX); + if new_index_is_in_lis.contains(&last_index) { + // Don't move it, since it is already where it needs to be. + } else { + // self.commit_traversal(); + // [... parent last] + // self.append_child(); + // [... parent] + // self.go_down_to_temp_child(temp); + // [... parent last] + } + } else { + // self.commit_traversal(); + // [... parent] + let meta = self.create_vnode(last); - // if new_index_is_in_lis.contains(&new_index) { - // // [... parent successor] - // self.go_to_temp_sibling(temp); - // // [... parent new_child] - // } else { - // // self.commit_traversal(); - // // [... parent successor] - // self.push_temporary(temp); - // // [... parent successor new_child] - // self.insert_before(); - // // [... parent new_child] - // } + // [... parent last] + // self.append_child(); + // [... parent] + // self.go_down_to_reverse_child(0); + // [... parent last] + } + } - // self.diff_node(&old[old_index], new_child); - // } - // } + for (new_index, new_child) in new.iter().enumerate().rev() { + let old_index = new_index_to_old_index[new_index]; + if old_index == u32::MAX as usize { + debug_assert!(!shared_keys.contains(&new_child.key())); + // self.commit_traversal(); + // [... parent successor] + let meta = self.create_vnode(new_child); + // [... parent successor new_child] + self.edit_insert_after(meta.added_to_stack); + // self.insert_before(); + // [... parent new_child] + } else { + debug_assert!(shared_keys.contains(&new_child.key())); + let temp = old_index_to_temp[old_index]; + debug_assert_ne!(temp, u32::MAX); + + if new_index_is_in_lis.contains(&new_index) { + // [... parent successor] + // self.go_to_temp_sibling(temp); + // [... parent new_child] + } else { + // self.commit_traversal(); + // [... parent successor] + // self.push_temporary(temp); + // [... parent successor new_child] + // self.insert_before(); + // [... parent new_child] + } + + self.diff_node(&old[old_index], new_child); + } + } } // Diff the suffix of keyed children that share the same keys in the same order. @@ -1061,16 +1067,19 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { fn diff_non_keyed_children(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { // Handled these cases in `diff_children` before calling this function. // - // debug_assert!(!new.is_empty()); - // debug_assert!(!old.is_empty()); - - for (_i, (new_child, old_child)) in new.iter().zip(old.iter()).enumerate() { - self.diff_node(old_child, new_child); - } + debug_assert!(!new.is_empty()); + debug_assert!(!old.is_empty()); match old.len().cmp(&new.len()) { // old.len > new.len -> removing some nodes Ordering::Greater => { + // diff them together + for (new_child, old_child) in new.iter().zip(old.iter()) { + self.diff_node(old_child, new_child); + } + + // todo: we would emit fewer instructions if we just did a replace many + // remove whatever is still dangling for item in &old[new.len()..] { for i in RealChildIterator::new(item, self.vdom) { self.edit_push_root(i.direct_id()); @@ -1078,12 +1087,34 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { } } } + // old.len < new.len -> adding some nodes + // this is wrong in the case where we're diffing fragments + // + // we need to save the last old element and then replace it with all the new ones Ordering::Less => { - self.create_and_append_children(&new[old.len()..]); + // Add the new elements to the last old element while it still exists + let last = self.find_last_element(old.last().unwrap()); + self.edit_push_root(last.direct_id()); + + // create the rest and insert them + let meta = self.create_children(&new[old.len()..]); + self.edit_insert_after(meta.added_to_stack); + + self.edit_pop(); + + // diff the rest + new.iter() + .zip(old.iter()) + .for_each(|(new_child, old_child)| self.diff_node(old_child, new_child)); + } + + // old.len == new.len -> no nodes added/removed, but perhaps changed + Ordering::Equal => { + for (new_child, old_child) in new.iter().zip(old.iter()) { + self.diff_node(old_child, new_child); + } } - // old.len == new.len -> no nodes added/removed, but πerhaps changed - Ordering::Equal => {} } } @@ -1147,11 +1178,58 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // self.remove_self_and_next_siblings(); } + fn find_last_element(&mut self, vnode: &'bump VNode<'bump>) -> &'bump VNode<'bump> { + let mut search_node = Some(vnode); + + loop { + let node = search_node.take().unwrap(); + match &node.kind { + // the ones that have a direct id + VNodeKind::Text(_) + | VNodeKind::Element(_) + | VNodeKind::Anchor(_) + | VNodeKind::Suspended(_) => break node, + + VNodeKind::Fragment(frag) => { + search_node = frag.children.last(); + } + VNodeKind::Component(el) => { + let scope_id = el.ass_scope.get().unwrap(); + let scope = self.get_scope(&scope_id).unwrap(); + search_node = Some(scope.root()); + } + } + } + } + + fn find_first_element(&mut self, vnode: &'bump VNode<'bump>) -> &'bump VNode<'bump> { + let mut search_node = Some(vnode); + + loop { + let node = search_node.take().unwrap(); + match &node.kind { + // the ones that have a direct id + VNodeKind::Text(_) + | VNodeKind::Element(_) + | VNodeKind::Anchor(_) + | VNodeKind::Suspended(_) => break node, + + VNodeKind::Fragment(frag) => { + search_node = Some(&frag.children[0]); + } + VNodeKind::Component(el) => { + let scope_id = el.ass_scope.get().unwrap(); + let scope = self.get_scope(&scope_id).unwrap(); + search_node = Some(scope.root()); + } + } + } + } + fn replace_many_with_many( &mut self, old_node: impl IntoIterator>, - // old_node: &'bump VNode<'bump>, - new_node: &'bump VNode<'bump>, + new_node: impl IntoIterator>, ) { let mut nodes_to_replace = Vec::new(); let mut nodes_to_search = old_node.into_iter().collect::>(); @@ -1184,9 +1262,13 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { self.edit_push_root(node); } - let meta = self.create_vnode(new_node); + let mut nodes_created = 0; + for node in new_node { + let meta = self.create_vnode(node); + nodes_created += meta.added_to_stack; + } - self.edit_replace_with(n as u32, meta.added_to_stack); + self.edit_replace_with(n as u32, nodes_created); // obliterate! for scope in scopes_obliterated { @@ -1195,10 +1277,15 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { } fn create_garbage(&mut self, node: &'bump VNode<'bump>) { - let cur_scope = self.current_scope().unwrap(); - let scope = self.get_scope(&cur_scope).unwrap(); - let garbage: &'bump VNode<'static> = unsafe { std::mem::transmute(node) }; - scope.pending_garbage.borrow_mut().push(garbage); + match self.current_scope().and_then(|id| self.get_scope(&id)) { + Some(scope) => { + let garbage: &'bump VNode<'static> = unsafe { std::mem::transmute(node) }; + scope.pending_garbage.borrow_mut().push(garbage); + } + None => { + log::info!("No scope to collect garbage into") + } + } } fn immediately_dispose_garabage(&mut self, node: ElementId) { @@ -1292,6 +1379,10 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { self.edits.push(ReplaceWith { n, m }); } + pub(crate) fn edit_insert_after(&mut self, n: u32) { + self.edits.push(InsertAfter { n }); + } + // Remove Nodesfrom the dom pub(crate) fn edit_remove(&mut self) { self.edits.push(Remove); @@ -1374,6 +1465,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // When we create new nodes, we need to propagate some information back up the call chain. // This gives the caller some information on how to handle things like insertins, appending, and subtree discarding. +#[derive(Debug)] pub struct CreateMeta { pub is_static: bool, pub added_to_stack: u32, diff --git a/packages/core/src/editor.rs b/packages/core/src/editor.rs index a30a89498..b3efe140b 100644 --- a/packages/core/src/editor.rs +++ b/packages/core/src/editor.rs @@ -30,6 +30,9 @@ pub enum DomEdit<'bump> { // the last m elements m: u32, }, + InsertAfter { + n: u32, + }, Remove, RemoveAllChildren, CreateTextNode { @@ -71,6 +74,7 @@ pub enum DomEdit<'bump> { impl DomEdit<'_> { pub fn is(&self, id: &'static str) -> bool { match self { + DomEdit::InsertAfter { .. } => id == "InsertAfter", DomEdit::PushRoot { .. } => id == "PushRoot", DomEdit::PopRoot => id == "PopRoot", DomEdit::AppendChildren { .. } => id == "AppendChildren", diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 1b7de1c00..4986dd07a 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -252,7 +252,7 @@ impl<'a> NodeFactory<'a> { listeners: L, attributes: A, children: V, - key: Option<&'a str>, + key: Option, ) -> VNode<'a> where L: 'a + AsRef<[Listener<'a>]>, @@ -276,7 +276,7 @@ impl<'a> NodeFactory<'a> { listeners: L, attributes: A, children: V, - key: Option<&'a str>, + key: Option, ) -> VNode<'a> where L: 'a + AsRef<[Listener<'a>]>, @@ -292,6 +292,8 @@ impl<'a> NodeFactory<'a> { let children: &'a V = self.bump().alloc(children); let children = children.as_ref(); + let key = key.map(|f| self.raw_text(f).0); + VNode { key, kind: VNodeKind::Element(self.bump().alloc(VElement { @@ -352,21 +354,26 @@ impl<'a> NodeFactory<'a> { } } - pub fn component

( + pub fn component( &self, component: FC

, props: P, - key: Option<&'a str>, - children: &'a [VNode<'a>], + key: Option, + // key: Option<&'a str>, + children: V, ) -> VNode<'a> where P: Properties + 'a, + V: 'a + AsRef<[VNode<'a>]>, { // TODO // It's somewhat wrong to go about props like this // We don't want the fat part of the fat pointer // This function does static dispatch so we don't need any VTable stuff + let children: &'a V = self.bump().alloc(children); + let children = children.as_ref(); + let props = self.bump().alloc(props); let raw_props = props as *mut P as *mut (); @@ -408,6 +415,8 @@ impl<'a> NodeFactory<'a> { let is_static = children.len() == 0 && P::IS_STATIC && key.is_none(); + let key = key.map(|f| self.raw_text(f).0); + VNode { key, kind: VNodeKind::Component(self.bump().alloc_with(|| VComponent { diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index bf0420de4..1f99d56f5 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -1,3 +1,9 @@ +//! Diffing Tests +//! ------------- +//! +//! These should always compile and run, but the result is not validated for each test. +//! TODO: Validate the results beyond visual inspection. + use bumpalo::Bump; use anyhow::{Context, Result}; @@ -58,7 +64,7 @@ impl TestDom { &'a self, left: LazyNodes<'a, F1>, right: LazyNodes<'a, F2>, - ) -> Vec> + ) -> (Vec>, Vec>) where F1: FnOnce(NodeFactory<'a>) -> VNode<'a>, F2: FnOnce(NodeFactory<'a>) -> VNode<'a>, @@ -67,26 +73,26 @@ impl TestDom { let new = self.bump.alloc(self.render(right)); - let mut edits = Vec::new(); + let mut create_edits = Vec::new(); let dom = DebugDom::new(); - let mut machine = DiffMachine::new_headless(&mut edits, &dom, &self.resources); + let mut machine = DiffMachine::new_headless(&mut create_edits, &dom, &self.resources); machine.create_vnode(old); - edits.clear(); + let mut edits = Vec::new(); let mut machine = DiffMachine::new_headless(&mut edits, &dom, &self.resources); machine.diff_node(old, new); - edits + (create_edits, edits) } } #[test] fn diffing_works() {} +/// Should push the text node onto the stack and modify it #[test] fn html_and_rsx_generate_the_same_output() { let dom = TestDom::new(); - let edits = dom.lazy_diff( rsx! ( div { "Hello world" } ), rsx! ( div { "Goodbye world" } ), @@ -94,6 +100,7 @@ fn html_and_rsx_generate_the_same_output() { dbg!(edits); } +/// Should result in 3 elements on the stack #[test] fn fragments_create_properly() { let dom = TestDom::new(); @@ -109,3 +116,197 @@ fn fragments_create_properly() { assert_eq!(meta.added_to_stack, 3); dbg!(edits); } + +/// Should result in the creation of an anchor (placeholder) and then a replacewith +#[test] +fn empty_fragments_create_anchors() { + let dom = TestDom::new(); + + let left = rsx!({ (0..0).map(|f| rsx! { div {}}) }); + let right = rsx!({ (0..1).map(|f| rsx! { div {}}) }); + + let edits = dom.lazy_diff(left, right); + dbg!(edits); +} + +/// Should result in the creation of an anchor (placeholder) and then a replacewith m=5 +#[test] +fn empty_fragments_create_many_anchors() { + let dom = TestDom::new(); + + let left = rsx!({ (0..0).map(|f| rsx! { div {}}) }); + let right = rsx!({ (0..5).map(|f| rsx! { div {}}) }); + + let edits = dom.lazy_diff(left, right); + dbg!(edits); +} + +/// 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 = TestDom::new(); + + let left = rsx!({ (0..0).map(|f| rsx! { div {} }) }); + let right = rsx!({ + (0..5).map(|f| { + rsx! { div { "hello" }} + }) + }); + + let edits = dom.lazy_diff(left, right); + dbg!(&edits); + let last_edit = edits.1.last().unwrap(); + assert!(last_edit.is("ReplaceWith")); +} + +/// Should result in every node being pushed and then replaced with an anchor +#[test] +fn many_items_become_fragment() { + let dom = TestDom::new(); + + let left = rsx!({ + (0..2).map(|f| { + rsx! { div { "hello" }} + }) + }); + let right = rsx!({ (0..0).map(|f| rsx! { div {} }) }); + + let edits = dom.lazy_diff(left, right); + dbg!(&edits); +} + +/// Should result in no edits +#[test] +fn two_equal_fragments_are_equal() { + let dom = TestDom::new(); + + let left = rsx!({ + (0..2).map(|f| { + rsx! { div { "hello" }} + }) + }); + let right = rsx!({ + (0..2).map(|f| { + rsx! { div { "hello" }} + }) + }); + + let edits = dom.lazy_diff(left, right); + dbg!(&edits); + assert!(edits.1.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 = TestDom::new(); + + let left = rsx!( + {(0..2).map(|f| {rsx! { div { }}})} + p {} + ); + let right = rsx!( + {(0..5).map(|f| {rsx! { h1 { }}})} + p {} + ); + + let edits = dom.lazy_diff(left, right); + dbg!(&edits); +} + +/// Should result in multiple nodes destroyed - with changes to the first nodes +#[test] +fn two_fragments_with_differrent_elements_are_differet_shorter() { + let dom = TestDom::new(); + + let left = rsx!( + {(0..5).map(|f| {rsx! { div { }}})} + p {} + ); + let right = rsx!( + {(0..2).map(|f| {rsx! { h1 { }}})} + p {} + ); + + let edits = dom.lazy_diff(left, right); + dbg!(&edits); +} + +/// Should result in multiple nodes destroyed - with no changes +#[test] +fn two_fragments_with_same_elements_are_differet() { + let dom = TestDom::new(); + + let left = rsx!( + {(0..2).map(|f| {rsx! { div { }}})} + p {} + ); + let right = rsx!( + {(0..5).map(|f| {rsx! { div { }}})} + p {} + ); + + let edits = dom.lazy_diff(left, right); + dbg!(&edits); +} + +// Similar test from above, but with extra child nodes +#[test] +fn two_fragments_with_same_elements_are_differet_shorter() { + let dom = TestDom::new(); + + let left = rsx!( + {(0..5).map(|f| {rsx! { div { }}})} + p {"e"} + ); + let right = rsx!( + {(0..2).map(|f| {rsx! { div { }}})} + p {"e"} + ); + + let edits = dom.lazy_diff(left, right); + dbg!(&edits); +} + +/// should result in the removal of elements +#[test] +fn keyed_diffing_order() { + let dom = TestDom::new(); + + let left = rsx!( + {(0..5).map(|f| {rsx! { div { key: "{f}" }}})} + p {"e"} + ); + let right = rsx!( + {(0..2).map(|f| {rsx! { div { key: "{f}" }}})} + p {"e"} + ); + + let edits = dom.lazy_diff(left, right); + dbg!(&edits); +} + +#[test] +fn fragment_keys() { + let r = 1; + let p = rsx! { + Fragment { key: "asd {r}" } + }; +} + +/// Should result in moves, but not removals or additions +#[test] +fn keyed_diffing_out_of_order() { + let dom = TestDom::new(); + let left = rsx!( + {(0..5).map(|f| {rsx! { div { key: "{f}" }}})} + p {"e"} + ); + let right = rsx!( + {(0..5).rev().map(|f| {rsx! { div { key: "{f}" }}})} + p {"e"} + ); + let edits = dom.lazy_diff(left, right); + dbg!(&edits); +} From d717c22d9c5da7b6f343fede11faaf953a3a29e0 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 30 Jul 2021 16:07:42 -0400 Subject: [PATCH 16/21] feat: keyed diffing!! --- packages/core/src/diff.rs | 570 +++++++++++++++++----------- packages/core/src/editor.rs | 4 + packages/core/src/scope.rs | 4 +- packages/core/src/virtual_dom.rs | 5 +- packages/core/tests/diffing.rs | 48 ++- packages/core/tests/vdom_rebuild.rs | 99 +++++ 6 files changed, 494 insertions(+), 236 deletions(-) create mode 100644 packages/core/tests/vdom_rebuild.rs diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 34106f3a8..9e88f9242 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -65,7 +65,7 @@ //! - https://hacks.mozilla.org/2019/03/fast-bump-allocated-virtual-doms-with-rust-and-wasm/ use crate::{arena::SharedResources, innerlude::*}; -use fxhash::{FxHashMap, FxHashSet}; +use fxhash::{FxBuildHasher, FxHashMap, FxHashSet}; use smallvec::{smallvec, SmallVec}; use std::{any::Any, cell::Cell, cmp::Ordering}; @@ -210,6 +210,8 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // It's extraordinarily rare to have the number/order of listeners change // In the cases where the listeners change, we completely wipe the data attributes and add new ones // + // We also need to make sure that all listeners are properly attached to the parent scope (fix_listener) + // // TODO: take a more efficient path than this let cur_scope: ScopeId = self.scope_stack.last().unwrap().clone(); if old.listeners.len() == new.listeners.len() { @@ -240,8 +242,6 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { self.edit_pop(); } - // Each child pushes its own root, so it doesn't need our current root - self.diff_children(old.children, new.children); } @@ -334,12 +334,11 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { | VNodeKind::Element(_) | VNodeKind::Anchor(_), ) => { - self.replace_many_with_many([old_node], [new_node]); + self.replace_and_create_many_with_many([old_node], [new_node]); } // TODO (VNodeKind::Suspended(_), new) => todo!(), - // a node that was once real is now suspended (old, VNodeKind::Suspended(_)) => todo!(), } @@ -355,7 +354,6 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // // [... node] pub fn create_vnode(&mut self, node: &'bump VNode<'bump>) -> CreateMeta { - log::warn!("Creating node! ... {:#?}", node); match &node.kind { VNodeKind::Text(text) => { let real_id = self.vdom.reserve_node(); @@ -401,7 +399,6 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { let cur_scope = self.current_scope().unwrap(); listeners.iter().for_each(|listener| { - log::info!("setting listener id to {:#?}", real_id); self.fix_listener(listener); listener.mounted_node.set(Some(real_id)); self.edit_new_event_listener(listener, cur_scope.clone()); @@ -463,14 +460,29 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { ) }); - // TODO: abstract this unsafe into the arena abstraction - let new_component = self.get_scope_mut(&new_idx).unwrap(); - // Actually initialize the caller's slot with the right address vcomponent.ass_scope.set(Some(new_idx)); + // TODO: + // Noderefs + // Effects + + let new_component = self.get_scope_mut(&new_idx).unwrap(); + // Run the scope for one iteration to initialize it - new_component.run_scope().unwrap(); + match new_component.run_scope() { + Ok(_) => { + // all good, new nodes exist + } + Err(err) => { + // failed to run. this is the first time the component ran, and it failed + // we manually set its head node to an empty fragment + panic!("failing components not yet implemented"); + + // new_component.frames.head + // self.frames.wip_frame_mut().head_node = unsafe { std::mem::transmute(new_head) }; + } + } // Take the node that was just generated from running the component let nextnode = new_component.frames.fin_head(); @@ -485,7 +497,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { self.scope_stack.pop(); if meta.added_to_stack == 0 { - panic!("This should never happen"); + panic!("Components should *always* generate nodes - even if they fail"); } // Finally, insert this scope as a seen node. @@ -615,7 +627,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // Replace whatever nodes are sitting there with the anchor (_, VNodeKind::Anchor(anchor)) => { - self.replace_many_with_many(old, [first_new]); + self.replace_and_create_many_with_many(old, [first_new]); } // Use the complex diff algorithm to diff the nodes @@ -658,14 +670,8 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // // https://github.com/infernojs/inferno/blob/36fd96/packages/inferno/src/DOM/patching.ts#L530-L739 // - // When entering this function, the parent must be on top of the change list - // stack: - // - // [... parent] - // - // Upon exiting, the change list stack is in the same state. + // The stack is empty upon entry. fn diff_keyed_children(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { - println!("diffing keyed children!"); if cfg!(debug_assertions) { let mut keys = fxhash::FxHashSet::default(); let mut assert_unique_keys = |children: &'bump [VNode<'bump>]| { @@ -693,6 +699,8 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // // `shared_prefix_count` is the count of how many nodes at the start of // `new` and `old` share the same keys. + // + // TODO: just inline this let shared_prefix_count = match self.diff_keyed_prefix(old, new) { KeyedPrefixResult::Finished => return, KeyedPrefixResult::MoreWorkToDo(count) => count, @@ -738,11 +746,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // Diff the prefix of children in `new` and `old` that share the same keys in // the same order. // - // Upon entry of this function, the change list stack must be: - // - // [... parent] - // - // Upon exit, the change list stack is the same. + // The stack is empty upon entry. fn diff_keyed_prefix( &mut self, old: &'bump [VNode<'bump>], @@ -755,23 +759,28 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { if old.key() != new.key() { break; } - self.diff_node(old, new); - shared_prefix_count += 1; } // If that was all of the old children, then create and append the remaining // new children and we're finished. if shared_prefix_count == old.len() { - self.create_and_append_children(&new[shared_prefix_count..]); + // Load the last element + let last_node = self.find_last_element(new.last().unwrap()).direct_id(); + self.edit_push_root(last_node); + + // Create the new children and insert them after + let meta = self.create_children(&new[shared_prefix_count..]); + self.edit_insert_after(meta.added_to_stack); + return KeyedPrefixResult::Finished; } // And if that was all of the new children, then remove all of the remaining // old children and we're finished. if shared_prefix_count == new.len() { - self.remove_self_and_next_siblings(&old[shared_prefix_count..]); + self.remove_children(&old[shared_prefix_count..]); return KeyedPrefixResult::Finished; } @@ -804,8 +813,6 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // // This function will load the appropriate nodes onto the stack and do diffing in place. // - // used to be: - // [... parent] // Upon exit from this function, it will be restored to that same state. fn diff_keyed_middle( &mut self, @@ -819,108 +826,74 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { debug_assert_ne!(new.first().map(|n| n.key()), old.first().map(|o| o.key())); debug_assert_ne!(new.last().map(|n| n.key()), old.last().map(|o| o.key())); - // The algorithm below relies upon using `u32::MAX` as a sentinel - // value, so if we have that many new nodes, it won't work. This - // check is a bit academic (hence only enabled in debug), since - // wasm32 doesn't have enough address space to hold that many nodes - // in memory. - debug_assert!(new.len() < u32::MAX as usize); + // // The algorithm below relies upon using `u32::MAX` as a sentinel + // // value, so if we have that many new nodes, it won't work. This + // // check is a bit academic (hence only enabled in debug), since + // // wasm32 doesn't have enough address space to hold that many nodes + // // in memory. + // debug_assert!(new.len() < u32::MAX as usize); // Map from each `old` node's key to its index within `old`. // IE if the keys were A B C, then we would have (A, 1) (B, 2) (C, 3). let mut old_key_to_old_index = old .iter() .enumerate() - .map(|(i, o)| (o.key(), i)) + .map(|(i, o)| (o.key().unwrap(), i)) .collect::>(); // The set of shared keys between `new` and `old`. let mut shared_keys = FxHashSet::default(); + // let mut to_remove = FxHashSet::default(); + let mut to_add = FxHashSet::default(); // Map from each index in `new` to the index of the node in `old` that // has the same key. let mut new_index_to_old_index = new .iter() .map(|n| { - let key = n.key(); - if let Some(&i) = old_key_to_old_index.get(&key) { - shared_keys.insert(key); - i - } else { - u32::MAX as usize + let key = n.key().unwrap(); + match old_key_to_old_index.get(&key) { + Some(&index) => { + shared_keys.insert(key); + index + } + None => { + // + to_add.insert(key); + u32::MAX as usize + } } }) .collect::>(); + dbg!(&shared_keys); + dbg!(&to_add); + // If none of the old keys are reused by the new children, then we // remove all the remaining old children and create the new children // afresh. if shared_suffix_count == 0 && shared_keys.is_empty() { - // create all the children anew - // load all the old childrne - // replace n to m - - // if shared_prefix_count == 0 { - // self.remove_all_children(old); - // } else { - // self.remove_self_and_next_siblings(&old[shared_prefix_count..]); - // } - - // self.create_and_append_children(new); - + self.replace_and_create_many_with_many(old, new); return; } - dbg!("hello!"); + // // Remove any old children whose keys were not reused in the new + // // children. Remove from the end first so that we don't mess up indices. + // for old_child in old.iter().rev() { + // if !shared_keys.contains(&old_child.key()) { + // self.remove_child(old_child); + // } + // } - // Save each of the old children whose keys are reused in the new - // children. - let mut old_index_to_temp = vec![u32::MAX; old.len()]; - let mut start = 0; - loop { - let end = (start..old.len()) - .find(|&i| { - let key = old[i].key(); - !shared_keys.contains(&key) - }) - .unwrap_or(old.len()); + // let old_keyds = old.iter().map(|f| f.key()).collect::>(); + // let new_keyds = new.iter().map(|f| f.key()).collect::>(); + // dbg!(old_keyds); + // dbg!(new_keyds); - if end - start > 0 { - // self.commit_traversal(); - let mut t = 5; - // let mut t = self.save_children_to_temporaries( - // shared_prefix_count + start, - // shared_prefix_count + end, - // ); - for i in start..end { - old_index_to_temp[i] = t; - t += 1; - } - } - - debug_assert!(end <= old.len()); - if end == old.len() { - break; - } else { - start = end + 1; - } - } - - // Remove any old children whose keys were not reused in the new - // children. Remove from the end first so that we don't mess up indices. - let mut removed_count = 0; - for (i, old_child) in old.iter().enumerate().rev() { - if !shared_keys.contains(&old_child.key()) { - // self.remove(old_child.dom_id.get()); - // self.remove_child(i + shared_prefix_count); - removed_count += 1; - } - } - - // If there aren't any more new children, then we are done! - if new.is_empty() { - return; - } + // // If there aren't any more new children, then we are done! + // if new.is_empty() { + // return; + // } // The longest increasing subsequence within `new_index_to_old_index`. This // is the longest sequence on DOM nodes in `old` that are relatively ordered @@ -930,8 +903,10 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // nodes moved. let mut new_index_is_in_lis = FxHashSet::default(); new_index_is_in_lis.reserve(new_index_to_old_index.len()); + let mut predecessors = vec![0; new_index_to_old_index.len()]; let mut starts = vec![0; new_index_to_old_index.len()]; + longest_increasing_subsequence::lis_with( &new_index_to_old_index, &mut new_index_is_in_lis, @@ -940,99 +915,121 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { &mut starts, ); - // Now we will iterate from the end of the new children back to the - // beginning, diffing old children we are reusing and if they aren't in the - // LIS moving them to their new destination, or creating new children. Note - // that iterating in reverse order lets us use `Node.prototype.insertBefore` - // to move/insert children. - // - // But first, we ensure that we have a child on the change list stack that - // we can `insertBefore`. We handle this once before looping over `new` - // children, so that we don't have to keep checking on every loop iteration. - if shared_suffix_count > 0 { - // There is a shared suffix after these middle children. We will be - // inserting before that shared suffix, so add the first child of that - // shared suffix to the change list stack. - // - // [... parent] + dbg!(&new_index_is_in_lis); + // use the old nodes to navigate the new nodes - // TODO + let mut lis_in_order = new_index_is_in_lis.into_iter().collect::>(); + lis_in_order.sort_unstable(); - // self.edits - // .go_down_to_child(old_shared_suffix_start - removed_count); - // [... parent first_child_of_shared_suffix] - } else { - // There is no shared suffix coming after these middle children. - // Therefore we have to process the last child in `new` and move it to - // the end of the parent's children if it isn't already there. - let last_index = new.len() - 1; - // uhhhh why an unwrap? - let last = new.last().unwrap(); - // let last = new.last().unwrap_throw(); - new = &new[..new.len() - 1]; - if shared_keys.contains(&last.key()) { - let old_index = new_index_to_old_index[last_index]; - let temp = old_index_to_temp[old_index]; - // [... parent] - // self.go_down_to_temp_child(temp); - // [... parent last] - self.diff_node(&old[old_index], last); + dbg!(&lis_in_order); - if new_index_is_in_lis.contains(&last_index) { - // Don't move it, since it is already where it needs to be. - } else { - // self.commit_traversal(); - // [... parent last] - // self.append_child(); - // [... parent] - // self.go_down_to_temp_child(temp); - // [... parent last] - } - } else { - // self.commit_traversal(); - // [... parent] - let meta = self.create_vnode(last); + // we walk front to back, creating the head node - // [... parent last] - // self.append_child(); - // [... parent] - // self.go_down_to_reverse_child(0); - // [... parent last] - } + // diff the shared, in-place nodes first + // this makes sure we can rely on their first/last nodes being correct later on + for id in &lis_in_order { + let new_node = &new[*id]; + let key = new_node.key().unwrap(); + let old_index = old_key_to_old_index.get(&key).unwrap(); + let old_node = &old[*old_index]; + self.diff_node(old_node, new_node); } - for (new_index, new_child) in new.iter().enumerate().rev() { - let old_index = new_index_to_old_index[new_index]; - if old_index == u32::MAX as usize { - debug_assert!(!shared_keys.contains(&new_child.key())); - // self.commit_traversal(); - // [... parent successor] - let meta = self.create_vnode(new_child); - // [... parent successor new_child] + // return the old node from the key + let load_old_node_from_lsi = |key| -> &VNode { + let old_index = old_key_to_old_index.get(key).unwrap(); + let old_node = &old[*old_index]; + old_node + }; + + let mut root = None; + let mut new_iter = new.iter().enumerate(); + for lis_id in &lis_in_order { + eprintln!("tracking {:?}", lis_id); + // this is the next milestone node we are working up to + let new_anchor = &new[*lis_id]; + root = Some(new_anchor); + + let anchor_el = self.find_first_element(new_anchor); + self.edit_push_root(anchor_el.direct_id()); + // let mut pushed = false; + + 'inner: loop { + let (next_id, next_new) = new_iter.next().unwrap(); + if next_id == *lis_id { + // we've reached the milestone, break this loop so we can step to the next milestone + // remember: we already diffed this node + eprintln!("breaking {:?}", next_id); + break 'inner; + } else { + let key = next_new.key().unwrap(); + eprintln!("found key {:?}", key); + if shared_keys.contains(&key) { + eprintln!("key is contained {:?}", key); + shared_keys.remove(key); + // diff the two nodes + let old_node = load_old_node_from_lsi(key); + self.diff_node(old_node, next_new); + + // now move all the nodes into the right spot + for child in RealChildIterator::new(next_new, self.vdom) { + let el = child.direct_id(); + self.edit_push_root(el); + self.edit_insert_before(1); + } + } else { + eprintln!("key is not contained {:?}", key); + // new node needs to be created + // insert it before the current milestone + let meta = self.create_vnode(next_new); + self.edit_insert_before(meta.added_to_stack); + } + } + } + + self.edit_pop(); + } + + let final_lis_node = root.unwrap(); + let final_el_node = self.find_last_element(final_lis_node); + let final_el = final_el_node.direct_id(); + self.edit_push_root(final_el); + + let mut last_iter = new.iter().rev().enumerate(); + let last_key = final_lis_node.key().unwrap(); + loop { + let (last_id, last_node) = last_iter.next().unwrap(); + let key = last_node.key().unwrap(); + + eprintln!("checking final nodes {:?}", key); + + if last_key == key { + eprintln!("breaking final nodes"); + break; + } + + if shared_keys.contains(&key) { + eprintln!("key is contained {:?}", key); + shared_keys.remove(key); + // diff the two nodes + let old_node = load_old_node_from_lsi(key); + self.diff_node(old_node, last_node); + + // now move all the nodes into the right spot + for child in RealChildIterator::new(last_node, self.vdom) { + let el = child.direct_id(); + self.edit_push_root(el); + self.edit_insert_after(1); + } + } else { + eprintln!("key is not contained {:?}", key); + // new node needs to be created + // insert it before the current milestone + let meta = self.create_vnode(last_node); self.edit_insert_after(meta.added_to_stack); - // self.insert_before(); - // [... parent new_child] - } else { - debug_assert!(shared_keys.contains(&new_child.key())); - let temp = old_index_to_temp[old_index]; - debug_assert_ne!(temp, u32::MAX); - - if new_index_is_in_lis.contains(&new_index) { - // [... parent successor] - // self.go_to_temp_sibling(temp); - // [... parent new_child] - } else { - // self.commit_traversal(); - // [... parent successor] - // self.push_temporary(temp); - // [... parent successor new_child] - // self.insert_before(); - // [... parent new_child] - } - - self.diff_node(&old[old_index], new_child); } } + self.edit_pop(); } // Diff the suffix of keyed children that share the same keys in the same order. @@ -1051,7 +1048,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { debug_assert_eq!(old.len(), new.len()); debug_assert!(!old.is_empty()); - for (i, (old_child, new_child)) in old.iter().zip(new.iter()).enumerate() { + for (old_child, new_child) in old.iter().zip(new.iter()) { self.diff_node(old_child, new_child); } } @@ -1148,34 +1145,8 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { // After the function returns, the child is no longer on the change list stack: // // [... parent] - fn remove_self_and_next_siblings(&self, old: &[VNode<'bump>]) { - // debug_assert!(self.traversal_is_committed()); - for child in old { - if let VNodeKind::Component(_vcomp) = child.kind { - // dom - // .create_text_node("placeholder for vcomponent"); - - todo!() - // let root_id = vcomp.stable_addr.as_ref().borrow().unwrap(); - // self.lifecycle_events.push_back(LifeCycleEvent::Remove { - // root_id, - // stable_scope_addr: Rc::downgrade(&vcomp.ass_scope), - // }) - // let id = get_id(); - // *component.stable_addr.as_ref().borrow_mut() = Some(id); - // self.save_known_root(id); - // let scope = Rc::downgrade(&component.ass_scope); - // self.lifecycle_events.push_back(LifeCycleEvent::Mount { - // caller: Rc::downgrade(&component.caller), - // root_id: id, - // stable_scope_addr: scope, - // }); - } - - // registry.remove_subtree(child); - } - todo!() - // self.remove_self_and_next_siblings(); + fn remove_children(&mut self, old: &'bump [VNode<'bump>]) { + self.replace_and_create_many_with_many(old, None) } fn find_last_element(&mut self, vnode: &'bump VNode<'bump>) -> &'bump VNode<'bump> { @@ -1226,27 +1197,38 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { } } - fn replace_many_with_many( + fn remove_child(&mut self, node: &'bump VNode<'bump>) { + self.replace_and_create_many_with_many(Some(node), None); + } + + /// Remove all the old nodes and replace them with newly created new nodes. + /// + /// The new nodes *will* be created - don't create them yourself! + fn replace_and_create_many_with_many( &mut self, - old_node: impl IntoIterator>, - new_node: impl IntoIterator>, + old_nodes: impl IntoIterator>, + new_nodes: impl IntoIterator>, ) { let mut nodes_to_replace = Vec::new(); - let mut nodes_to_search = old_node.into_iter().collect::>(); + let mut nodes_to_search = old_nodes.into_iter().collect::>(); let mut scopes_obliterated = Vec::new(); while let Some(node) = nodes_to_search.pop() { match &node.kind { - // the ones that have a direct id + // the ones that have a direct id return immediately VNodeKind::Text(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), VNodeKind::Element(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), VNodeKind::Anchor(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), VNodeKind::Suspended(el) => nodes_to_replace.push(el.node.get().unwrap()), + // Fragments will either have a single anchor or a list of children VNodeKind::Fragment(frag) => { for child in frag.children { nodes_to_search.push(child); } } + + // Components can be any of the nodes above + // However, we do need to track which components need to be removed VNodeKind::Component(el) => { let scope_id = el.ass_scope.get().unwrap(); let scope = self.get_scope(&scope_id).unwrap(); @@ -1255,6 +1237,8 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { scopes_obliterated.push(scope_id); } } + // TODO: enable internal garabge collection + // self.create_garbage(node); } let n = nodes_to_replace.len(); @@ -1263,11 +1247,12 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { } let mut nodes_created = 0; - for node in new_node { + for node in new_nodes { let meta = self.create_vnode(node); nodes_created += meta.added_to_stack; } + // if 0 nodes are created, then it gets interperted as a deletion self.edit_replace_with(n as u32, nodes_created); // obliterate! @@ -1383,6 +1368,10 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { self.edits.push(InsertAfter { n }); } + pub(crate) fn edit_insert_before(&mut self, n: u32) { + self.edits.push(InsertBefore { n }); + } + // Remove Nodesfrom the dom pub(crate) fn edit_remove(&mut self) { self.edits.push(Remove); @@ -1503,7 +1492,7 @@ fn find_first_real_node<'a>( None } -/// This iterator iterates through a list of virtual children and only returns real children (Elements or Text). +/// This iterator iterates through a list of virtual children and only returns real children (Elements, Text, Anchors). /// /// This iterator is useful when it's important to load the next real root onto the top of the stack for operations like /// "InsertBefore". @@ -1523,6 +1512,11 @@ impl<'a> RealChildIterator<'a> { stack: smallvec::smallvec![(0, starter)], } } + // keep the memory around + pub fn reset_with(&mut self, node: &'a VNode<'a>) { + self.stack.clear(); + self.stack.push((0, node)); + } } impl<'a> Iterator for RealChildIterator<'a> { @@ -1628,3 +1622,131 @@ fn compare_strs(a: &str, b: &str) -> bool { true } } + +// // Now we will iterate from the end of the new children back to the +// // beginning, diffing old children we are reusing and if they aren't in the +// // LIS moving them to their new destination, or creating new children. Note +// // that iterating in reverse order lets us use `Node.prototype.insertBefore` +// // to move/insert children. +// // +// // But first, we ensure that we have a child on the change list stack that +// // we can `insertBefore`. We handle this once before looping over `new` +// // children, so that we don't have to keep checking on every loop iteration. +// if shared_suffix_count > 0 { +// // There is a shared suffix after these middle children. We will be +// // inserting before that shared suffix, so add the first child of that +// // shared suffix to the change list stack. +// // +// // [... parent] + +// // TODO + +// // self.edits +// // .go_down_to_child(old_shared_suffix_start - removed_count); +// // [... parent first_child_of_shared_suffix] +// } else { +// // There is no shared suffix coming after these middle children. +// // Therefore we have to process the last child in `new` and move it to +// // the end of the parent's children if it isn't already there. +// let last_index = new.len() - 1; +// // uhhhh why an unwrap? +// let last = new.last().unwrap(); +// // let last = new.last().unwrap_throw(); +// new = &new[..new.len() - 1]; +// if shared_keys.contains(&last.key()) { +// let old_index = new_index_to_old_index[last_index]; +// let temp = old_index_to_temp[old_index]; +// // [... parent] +// // self.go_down_to_temp_child(temp); +// // [... parent last] +// self.diff_node(&old[old_index], last); + +// if new_index_is_in_lis.contains(&last_index) { +// // Don't move it, since it is already where it needs to be. +// } else { +// // self.commit_traversal(); +// // [... parent last] +// // self.append_child(); +// // [... parent] +// // self.go_down_to_temp_child(temp); +// // [... parent last] +// } +// } else { +// // self.commit_traversal(); +// // [... parent] +// let meta = self.create_vnode(last); + +// // [... parent last] +// // self.append_child(); +// // [... parent] +// // self.go_down_to_reverse_child(0); +// // [... parent last] +// } +// } + +// for (new_index, new_child) in new.iter().enumerate().rev() { +// let old_index = new_index_to_old_index[new_index]; +// if old_index == u32::MAX as usize { +// debug_assert!(!shared_keys.contains(&new_child.key())); +// // self.commit_traversal(); +// // [... parent successor] +// let meta = self.create_vnode(new_child); +// // [... parent successor new_child] +// self.edit_insert_after(meta.added_to_stack); +// // self.insert_before(); +// // [... parent new_child] +// } else { +// debug_assert!(shared_keys.contains(&new_child.key())); +// let temp = old_index_to_temp[old_index]; +// debug_assert_ne!(temp, u32::MAX); + +// if new_index_is_in_lis.contains(&new_index) { +// // [... parent successor] +// // self.go_to_temp_sibling(temp); +// // [... parent new_child] +// } else { +// // self.commit_traversal(); +// // [... parent successor] +// // self.push_temporary(temp); +// // [... parent successor new_child] +// // self.insert_before(); +// // [... parent new_child] +// } + +// self.diff_node(&old[old_index], new_child); +// } +// } + +// Save each of the old children whose keys are reused in the new +// children +// let reused_children = vec![]; +// let mut old_index_to_temp = vec![u32::MAX; old.len()]; +// let mut start = 0; +// loop { +// let end = (start..old.len()) +// .find(|&i| { +// let key = old[i].key(); +// !shared_keys.contains(&key) +// }) +// .unwrap_or(old.len()); + +// if end - start > 0 { +// // self.commit_traversal(); +// // let mut t = 5; +// let mut t = self.save_children_to_temporaries( +// shared_prefix_count + start, +// shared_prefix_count + end, +// ); +// for i in start..end { +// old_index_to_temp[i] = t; +// t += 1; +// } +// } + +// debug_assert!(end <= old.len()); +// if end == old.len() { +// break; +// } else { +// start = end + 1; +// } +// } diff --git a/packages/core/src/editor.rs b/packages/core/src/editor.rs index b3efe140b..3bc959b4a 100644 --- a/packages/core/src/editor.rs +++ b/packages/core/src/editor.rs @@ -33,6 +33,9 @@ pub enum DomEdit<'bump> { InsertAfter { n: u32, }, + InsertBefore { + n: u32, + }, Remove, RemoveAllChildren, CreateTextNode { @@ -75,6 +78,7 @@ impl DomEdit<'_> { pub fn is(&self, id: &'static str) -> bool { match self { DomEdit::InsertAfter { .. } => id == "InsertAfter", + DomEdit::InsertBefore { .. } => id == "InsertBefore", DomEdit::PushRoot { .. } => id == "PushRoot", DomEdit::PopRoot => id == "PopRoot", DomEdit::AppendChildren { .. } => id == "AppendChildren", diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index cffdaf8c9..5c1352a16 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -145,9 +145,9 @@ impl Scope { unsafe { self.frames.reset_wip_frame() }; // Cast the caller ptr from static to one with our own reference - let c3: &WrappedCaller = self.caller.as_ref(); + let render: &WrappedCaller = self.caller.as_ref(); - match c3(self) { + match render(self) { None => { // the user's component failed. We avoid cycling to the next frame log::error!("Running your component failed! It will no longer receive events."); diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 95d1f8d03..0536492dc 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -171,10 +171,11 @@ impl VirtualDom { /// This method uses the `DebugDom` under the hood - essentially making the VirtualDOM's diffing patches a "no-op". /// /// SSR takes advantage of this by using Dioxus itself as the source of truth, and rendering from the tree directly. - pub fn rebuild_in_place(&mut self) -> Result<()> { + pub fn rebuild_in_place(&mut self) -> Result> { let mut realdom = DebugDom::new(); let mut edits = Vec::new(); - self.rebuild(&mut realdom, &mut edits) + self.rebuild(&mut realdom, &mut edits)?; + Ok(edits) } /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom rom scratch diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index 1f99d56f5..e8a7522b7 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -299,14 +299,46 @@ fn fragment_keys() { #[test] fn keyed_diffing_out_of_order() { let dom = TestDom::new(); - let left = rsx!( - {(0..5).map(|f| {rsx! { div { key: "{f}" }}})} - p {"e"} - ); - let right = rsx!( - {(0..5).rev().map(|f| {rsx! { div { key: "{f}" }}})} - p {"e"} - ); + + // 0, 1, 2, 3, 4, 5, 6, 7, 8, + let left = rsx!({ + (0..3).chain(3..6).chain(6..9).map(|f| { + rsx! { div { key: "{f}" }} + }) + }); + + // 0, 1, 2, 6, 5, 4, 3, 7, 8, 9 + let right = rsx!({ + (0..3).chain((3..7).rev()).chain(7..10).map(|f| { + rsx! { div { key: "{f}" }} + }) + }); + + // LIS: 3, 7, 8, + let edits = dom.lazy_diff(left, right); + dbg!(&edits); +} + +#[test] +fn controlled_keyed_diffing_out_of_order() { + let dom = TestDom::new(); + + let left = [4, 5, 6, 7]; + let left = rsx!({ + left.iter().map(|f| { + rsx! { div { key: "{f}" "{f}" }} + }) + }); + + // 0, 1, 2, 6, 5, 4, 3, 7, 8, 9 + let right = [0, 5, 9, 6, 4]; + let right = rsx!({ + right.iter().map(|f| { + rsx! { div { key: "{f}" "{f}" }} + }) + }); + + // LIS: 3, 7, 8, let edits = dom.lazy_diff(left, right); dbg!(&edits); } diff --git a/packages/core/tests/vdom_rebuild.rs b/packages/core/tests/vdom_rebuild.rs new file mode 100644 index 000000000..54d1676c8 --- /dev/null +++ b/packages/core/tests/vdom_rebuild.rs @@ -0,0 +1,99 @@ +//! 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 as dioxus; +use dioxus_html as dioxus_elements; + +#[test] +fn app_runs() { + static App: FC<()> = |cx| { + // + cx.render(rsx!( div{"hello"} )) + }; + let mut vdom = VirtualDom::new(App); + let edits = vdom.rebuild_in_place().unwrap(); + dbg!(edits); +} + +#[test] +fn fragments_work() { + static App: FC<()> = |cx| { + cx.render(rsx!( + div{"hello"} + div{"goodbye"} + )) + }; + let mut vdom = VirtualDom::new(App); + let edits = vdom.rebuild_in_place().unwrap(); + // should result in a final "appendchildren n=2" + dbg!(edits); +} + +#[test] +fn lists_work() { + static App: FC<()> = |cx| { + cx.render(rsx!( + h1 {"hello"} + {(0..6).map(|f| rsx!(span{ "{f}" }))} + )) + }; + let mut vdom = VirtualDom::new(App); + let edits = vdom.rebuild_in_place().unwrap(); + dbg!(edits); +} + +#[test] +fn conditional_rendering() { + static App: FC<()> = |cx| { + cx.render(rsx!( + h1 {"hello"} + {true.then(|| rsx!(span{ "a" }))} + {false.then(|| rsx!(span{ "b" }))} + )) + }; + let mut vdom = VirtualDom::new(App); + + let edits = vdom.rebuild_in_place().unwrap(); + dbg!(&edits); + // the "false" fragment should generate an empty placeholder to re-visit + assert!(edits[edits.len() - 2].is("CreatePlaceholder")); +} + +#[test] +fn child_components() { + static App: FC<()> = |cx| { + cx.render(rsx!( + {true.then(|| rsx!(Child { }))} + {false.then(|| rsx!(Child { }))} + )) + }; + static Child: FC<()> = |cx| { + cx.render(rsx!( + h1 {"hello"} + h1 {"goodbye"} + )) + }; + let mut vdom = VirtualDom::new(App); + let edits = vdom.rebuild_in_place().unwrap(); + dbg!(edits); +} + +#[test] +fn suspended_works() { + static App: FC<()> = |cx| { + let title = use_suspense(cx, || async { "bob" }, |cx, f| cx.render(rsx! { "{f}"})); + cx.render(rsx!("hello" { title })) + }; + + let mut vdom = VirtualDom::new(App); + let edits = vdom.rebuild_in_place().unwrap(); + dbg!(edits); +} From cfa0247cbb1233e1df275374a73f431650a9250f Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 30 Jul 2021 17:04:04 -0400 Subject: [PATCH 17/21] wip: websys dom working properly --- packages/core/src/arena.rs | 2 +- packages/core/src/diff.rs | 3 +- packages/core/src/virtual_dom.rs | 16 ++++ packages/ssr/src/lib.rs | 11 ++- packages/web/Cargo.toml | 6 +- packages/web/examples/basic.rs | 45 +++++++++++ packages/web/src/dom.rs | 124 +++++++++++++++++++++---------- packages/web/src/lib.rs | 70 ++++++++--------- packages/web/src/nodeslab.rs | 20 ++--- 9 files changed, 200 insertions(+), 97 deletions(-) create mode 100644 packages/web/examples/basic.rs diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index ce74b6876..18fdee8a0 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -45,7 +45,7 @@ pub struct SharedResources { pub(crate) heuristics: Shared, - pub(crate) tasks: Shared>, + pub tasks: Shared>, /// We use a SlotSet to keep track of the keys that are currently being used. /// However, we don't store any specific data since the "mirror" diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 9e88f9242..72e7f0e80 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -526,7 +526,8 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { let mut is_static = true; let mut added_to_stack = 0; - for child in children { + // add them backwards + for child in children.iter().rev() { let child_meta = self.create_vnode(child); is_static = is_static && child_meta.is_static; added_to_stack += child_meta.added_to_stack; diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 0536492dc..5d4aa11bb 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -19,6 +19,8 @@ //! This module includes just the barebones for a complete VirtualDOM API. //! Additional functionality is defined in the respective files. +use futures_util::StreamExt; + use crate::hooks::{SuspendedContext, SuspenseHook}; use crate::{arena::SharedResources, innerlude::*}; @@ -400,6 +402,20 @@ impl VirtualDom { Ok(()) } + pub async fn wait_for_event(&mut self) -> Option { + let r = self.shared.tasks.clone(); + let mut r = r.borrow_mut(); + let gh = r.next().await; + + gh + } + + pub fn any_pending_events(&self) -> bool { + let r = self.shared.tasks.clone(); + let r = r.borrow(); + !r.is_empty() + } + pub fn base_scope(&self) -> &Scope { unsafe { self.shared.get_scope(self.base_scope).unwrap() } } diff --git a/packages/ssr/src/lib.rs b/packages/ssr/src/lib.rs index 25881c400..bd323e77d 100644 --- a/packages/ssr/src/lib.rs +++ b/packages/ssr/src/lib.rs @@ -83,6 +83,15 @@ impl<'a> TextRenderer<'a> { } write!(f, "{}", text.text)? } + VNodeKind::Anchor(anchor) => { + // + if self.cfg.indent { + for _ in 0..il { + write!(f, " ")?; + } + } + write!(f, "")?; + } VNodeKind::Element(el) => { if self.cfg.indent { for _ in 0..il { @@ -120,7 +129,7 @@ impl<'a> TextRenderer<'a> { // // when the page is loaded, the `querySelectorAll` will be used to collect all the nodes, and then add // them interpreter's stack - match (self.cfg.pre_render, node.dom_id()) { + match (self.cfg.pre_render, node.try_direct_id()) { (true, Some(id)) => { write!(f, " dio_el=\"{}\"", id)?; // diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 78000c4b7..82b9e85d0 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -78,7 +78,7 @@ uuid = { version = "0.8.2", features = ["v4", "wasm-bindgen"] } dioxus-hooks = { path = "../hooks" } gloo-timers = { version = "0.2.1", features = ["futures"] } serde = { version = "1.0.126", features = ["derive"] } -surf = { version = "2.2.0", default-features = false, features = [ - "wasm-client", -] } +# surf = { git = "https://github.com/http-rs/surf", default-features = false, features = [ +# "wasm-client", +# ] } # wasm-timer = "0.2.5" diff --git a/packages/web/examples/basic.rs b/packages/web/examples/basic.rs new file mode 100644 index 000000000..518115f7c --- /dev/null +++ b/packages/web/examples/basic.rs @@ -0,0 +1,45 @@ +//! Basic example that renders a simple VNode to the browser. + +use dioxus::events::on::MouseEvent; +use dioxus_core as dioxus; +use dioxus_core::prelude::*; +use dioxus_hooks::*; +use dioxus_html as dioxus_elements; +// use wasm_timer; + +use std::future::Future; + +use std::{pin::Pin, time::Duration}; + +use dioxus::prelude::*; + +use dioxus_web::*; + +fn main() { + // Setup logging + wasm_logger::init(wasm_logger::Config::new(log::Level::Debug)); + console_error_panic_hook::set_once(); + + // Run the app + dioxus_web::launch(App, |c| c) +} + +static App: FC<()> = |cx| { + let mut count = use_state(cx, || 0); + + cx.render(rsx! { + div { + button { + "add" + onclick: move |_| count += 1 + } + ul { + {(0..*count).map(|f| rsx!{ + li { "a - {f}" } + li { "b - {f}" } + li { "c - {f}" } + })} + } + } + }) +}; diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 2ae5ca822..0567a53b8 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -48,6 +48,7 @@ impl WebsysDom { let sender_callback = Arc::new(move |ev| { let c = sender.clone(); wasm_bindgen_futures::spawn_local(async move { + log::debug!("sending event through channel"); c.send(ev).await.unwrap(); }); }); @@ -102,7 +103,7 @@ impl WebsysDom { DomEdit::PushRoot { id: root } => self.push(root), DomEdit::PopRoot => self.pop(), DomEdit::AppendChildren { many } => self.append_children(many), - DomEdit::ReplaceWith { many } => self.replace_with(many), + DomEdit::ReplaceWith { n, m } => self.replace_with(n, m), DomEdit::Remove => self.remove(), DomEdit::RemoveAllChildren => self.remove_all_children(), DomEdit::CreateTextNode { text, id } => self.create_text_node(text, id), @@ -113,12 +114,15 @@ impl WebsysDom { event_name: event, scope, mounted_node_id: node, - element_id: idx, - } => self.new_event_listener(event, scope, idx, node), + } => self.new_event_listener(event, scope, node), + DomEdit::RemoveEventListener { event } => todo!(), DomEdit::SetText { text } => self.set_text(text), DomEdit::SetAttribute { field, value, ns } => self.set_attribute(field, value, ns), DomEdit::RemoveAttribute { name } => self.remove_attribute(name), + + DomEdit::InsertAfter { n } => self.insert_after(n), + DomEdit::InsertBefore { n } => self.insert_before(n), } } } @@ -169,37 +173,51 @@ impl WebsysDom { } } - fn replace_with(&mut self, many: u32) { + fn replace_with(&mut self, n: u32, m: u32) { log::debug!("Called [`replace_with`]"); - let new_node = self.stack.pop(); - let old_node = self.stack.pop(); - // TODO: use different-sized replace withs - if many == 1 { - if old_node.has_type::() { - old_node - .dyn_ref::() - .unwrap() - .replace_with_with_node_1(&new_node) - .unwrap(); - } else if old_node.has_type::() { - old_node - .dyn_ref::() - .unwrap() - .replace_with_with_node_1(&new_node) - .unwrap(); - } else if old_node.has_type::() { - old_node - .dyn_ref::() - .unwrap() - .replace_with_with_node_1(&new_node) - .unwrap(); - } else { - panic!("Cannot replace node: {:?}", old_node); - } + let mut new_nodes = vec![]; + for _ in 0..m { + new_nodes.push(self.stack.pop()); } - self.stack.push(new_node); + let mut old_nodes = vec![]; + for _ in 0..n { + old_nodes.push(self.stack.pop()); + } + + let old = old_nodes[0].clone(); + let arr: js_sys::Array = new_nodes.iter().collect(); + let el = old.dyn_into::().unwrap(); + el.replace_with_with_node(&arr).unwrap(); + // let arr = js_sys::Array::from(); + + // TODO: use different-sized replace withs + // if m == 1 { + // if old_node.has_type::() { + // old_node + // .dyn_ref::() + // .unwrap() + // .replace_with_with_node_1(&new_node) + // .unwrap(); + // } else if old_node.has_type::() { + // old_node + // .dyn_ref::() + // .unwrap() + // .replace_with_with_node_1(&new_node) + // .unwrap(); + // } else if old_node.has_type::() { + // old_node + // .dyn_ref::() + // .unwrap() + // .replace_with_with_node_1(&new_node) + // .unwrap(); + // } else { + // panic!("Cannot replace node: {:?}", old_node); + // } + // } + + // self.stack.push(new_node); } fn remove(&mut self) { @@ -213,7 +231,8 @@ impl WebsysDom { } fn create_placeholder(&mut self, id: u64) { - self.create_element("pre", None, id) + self.create_element("pre", None, id); + self.set_attribute("hidden", "", None); } fn create_text_node(&mut self, text: &str, id: u64) { // let nid = self.node_counter.next(); @@ -255,14 +274,8 @@ impl WebsysDom { // ElementId::new(nid) } - fn new_event_listener( - &mut self, - event: &'static str, - scope: ScopeId, - _element_id: usize, - real_id: u64, - ) { - let (_on, event) = event.split_at(2); + fn new_event_listener(&mut self, event: &'static str, scope: ScopeId, real_id: u64) { + // let (_on, event) = event.split_at(2); let event = wasm_bindgen::intern(event); // attach the correct attributes to the element @@ -367,8 +380,37 @@ impl WebsysDom { } } - fn raw_node_as_any(&self) -> &mut dyn std::any::Any { - todo!() + fn insert_after(&mut self, n: u32) { + let mut new_nodes = vec![]; + for _ in 0..n { + new_nodes.push(self.stack.pop()); + } + + let after = self.stack.top().clone(); + let arr: js_sys::Array = new_nodes.iter().collect(); + + let el = after.dyn_into::().unwrap(); + el.after_with_node(&arr).unwrap(); + // let mut old_nodes = vec![]; + // for _ in 0..n { + // old_nodes.push(self.stack.pop()); + // } + + // let el = self.stack.top(); + } + + fn insert_before(&mut self, n: u32) { + let n = n as usize; + let root = self + .stack + .list + .get(self.stack.list.len() - n) + .unwrap() + .clone(); + for _ in 0..n { + let el = self.stack.pop(); + root.insert_before(&el, None).unwrap(); + } } } diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 5b6229a2f..d70ac567f 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -75,61 +75,55 @@ pub async fn run_with_props( root_props: T, cfg: WebConfig, ) -> Result<()> { - let dom = VirtualDom::new_with_props(root, root_props); + let mut dom = VirtualDom::new_with_props(root, root_props); - let root_el = load_document().get_element_by_id("dioxus_root").unwrap(); + // let tasks = dom.shared.tasks.clone(); + + let root_el = load_document().get_element_by_id("dioxusroot").unwrap(); let mut websys_dom = dom::WebsysDom::new(root_el, cfg); - // let mut edits = Vec::new(); - // internal_dom.rebuild(&mut websys_dom, &mut edits)?; - // websys_dom.process_edits(&mut edits); + let mut edits = Vec::new(); + dom.rebuild(&mut websys_dom, &mut edits)?; + websys_dom.process_edits(&mut edits); log::info!("Going into event loop"); + + // #[allow(unreachable_code)] loop { - todo!(); - // let trigger = { - // let real_queue = websys_dom.wait_for_event(); - // if internal_dom.tasks.is_empty() { - // log::info!("tasks is empty, waiting for dom event to trigger soemthing"); - // real_queue.await - // } else { - // log::info!("tasks is not empty, waiting for either tasks or event system"); - // let task_queue = (&mut internal_dom.tasks).next(); + let trigger = { + let real_queue = websys_dom.wait_for_event(); + if dom.any_pending_events() { + log::info!("tasks is not empty, waiting for either tasks or event system"); + let mut task = dom.wait_for_event(); - // pin_mut!(real_queue); - // pin_mut!(task_queue); + pin_mut!(real_queue); + pin_mut!(task); - // match futures_util::future::select(real_queue, task_queue).await { - // futures_util::future::Either::Left((trigger, _)) => trigger, - // futures_util::future::Either::Right((trigger, _)) => trigger, - // } - // } - // }; + match futures_util::future::select(real_queue, task).await { + futures_util::future::Either::Left((trigger, _)) => trigger, + futures_util::future::Either::Right((trigger, _)) => trigger, + } + } else { + log::info!("tasks is empty, waiting for dom event to trigger soemthing"); + real_queue.await + } + }; - // if let Some(real_trigger) = trigger { - // log::info!("event received"); + if let Some(real_trigger) = trigger { + log::info!("event received"); - // internal_dom.queue_event(real_trigger); + dom.queue_event(real_trigger); - // let mut edits = Vec::new(); - // internal_dom - // .progress_with_event(&mut websys_dom, &mut edits) - // .await?; - // websys_dom.process_edits(&mut edits); - // } + let mut edits = Vec::new(); + dom.progress_with_event(&mut websys_dom, &mut edits).await?; + websys_dom.process_edits(&mut edits); + } } // should actually never return from this, should be an error, rustc just cant see it Ok(()) } -fn iter_node_list() {} - -// struct NodeListIter { -// node_list: NodeList, -// } -// impl Iterator for NodeListIter {} - struct HydrationNode { id: usize, node: Node, diff --git a/packages/web/src/nodeslab.rs b/packages/web/src/nodeslab.rs index 5f6eb2496..e374a3dc9 100644 --- a/packages/web/src/nodeslab.rs +++ b/packages/web/src/nodeslab.rs @@ -8,17 +8,12 @@ pub struct NodeSlab { impl NodeSlab { pub fn new(capacity: usize) -> NodeSlab { - NodeSlab { - nodes: Vec::with_capacity(capacity), - } - } - - fn insert_and_extend(&mut self, node: Node, id: usize) { - if id > self.nodes.len() * 3 { - panic!("Trying to insert an element way too far out of bounds"); + let mut nodes = Vec::with_capacity(capacity); + for x in 0..5 { + nodes.push(None); } - if id < self.nodes.len() {} + NodeSlab { nodes } } } impl Index for NodeSlab { @@ -30,11 +25,12 @@ impl Index for NodeSlab { impl IndexMut for NodeSlab { fn index_mut(&mut self, index: usize) -> &mut Self::Output { - if index >= self.nodes.len() * 3 { + if index >= self.nodes.capacity() * 3 { panic!("Trying to mutate an element way too far out of bounds"); } - if index > self.nodes.len() { - self.nodes.resize_with(index, || None); + + if index + 1 > self.nodes.len() { + self.nodes.resize_with(index + 1, || None); } &mut self.nodes[index] } From 687cda1b6d9595357d1dc8720ebe921f61098d8f Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 30 Jul 2021 22:14:06 -0400 Subject: [PATCH 18/21] feat: re-enable suspense --- packages/core/src/diff.rs | 20 ++-- packages/core/src/events.rs | 62 ++++++++---- packages/core/src/virtual_dom.rs | 2 +- packages/web/Cargo.toml | 9 +- packages/web/src/dom.rs | 158 +++++++++++++++++++++++++------ 5 files changed, 193 insertions(+), 58 deletions(-) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 72e7f0e80..f640e1f4c 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -338,9 +338,15 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { } // TODO - (VNodeKind::Suspended(_), new) => todo!(), + (VNodeKind::Suspended(old), new) => { + // + self.replace_and_create_many_with_many([old_node], [new_node]); + } // a node that was once real is now suspended - (old, VNodeKind::Suspended(_)) => todo!(), + (old, VNodeKind::Suspended(_)) => { + // + self.replace_and_create_many_with_many([old_node], [new_node]); + } } } @@ -512,12 +518,10 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { VNodeKind::Fragment(frag) => self.create_children(frag.children), VNodeKind::Suspended(VSuspended { node: real_node }) => { - todo!("wip on adding suspense back in"); - // let id = self.vdom.reserve_node(); - // self.edit_create_placeholder(id); - // node.dom_id.set(Some(id)); - // real_node.set(Some(id)); - // CreateMeta::new(false, 1) + let id = self.vdom.reserve_node(); + self.edit_create_placeholder(id); + real_node.set(Some(id)); + CreateMeta::new(false, 1) } } } diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 59c520f49..3c8cc13c8 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -91,7 +91,6 @@ impl EventTrigger { } } -#[derive(Debug)] pub enum VirtualEvent { // Real events ClipboardEvent(on::ClipboardEvent), @@ -135,6 +134,34 @@ pub enum VirtualEvent { // GarbageCollection {} } +impl std::fmt::Debug for VirtualEvent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let name = match self { + VirtualEvent::ClipboardEvent(_) => "ClipboardEvent", + VirtualEvent::CompositionEvent(_) => "CompositionEvent", + VirtualEvent::KeyboardEvent(_) => "KeyboardEvent", + VirtualEvent::FocusEvent(_) => "FocusEvent", + VirtualEvent::FormEvent(_) => "FormEvent", + VirtualEvent::SelectionEvent(_) => "SelectionEvent", + VirtualEvent::TouchEvent(_) => "TouchEvent", + VirtualEvent::UIEvent(_) => "UIEvent", + VirtualEvent::WheelEvent(_) => "WheelEvent", + VirtualEvent::MediaEvent(_) => "MediaEvent", + VirtualEvent::AnimationEvent(_) => "AnimationEvent", + VirtualEvent::TransitionEvent(_) => "TransitionEvent", + VirtualEvent::ToggleEvent(_) => "ToggleEvent", + VirtualEvent::MouseEvent(_) => "MouseEvent", + VirtualEvent::PointerEvent(_) => "PointerEvent", + VirtualEvent::GarbageCollection => "GarbageCollection", + VirtualEvent::SetStateEvent { .. } => "SetStateEvent", + VirtualEvent::AsyncEvent { .. } => "AsyncEvent", + VirtualEvent::SuspenseEvent { .. } => "SuspenseEvent", + }; + + f.debug_struct("VirtualEvent").field("type", &name).finish() + } +} + pub mod on { //! This module defines the synthetic events that all Dioxus apps enable. No matter the platform, every dioxus renderer //! will implement the same events and same behavior (bubbling, cancelation, etc). @@ -169,7 +196,6 @@ pub mod on { )* ) => { $( $(#[$attr])* - #[derive(Debug)] pub struct $wrapper(pub Rc); // todo: derefing to the event is fine (and easy) but breaks some IDE stuff like (go to source) @@ -523,15 +549,15 @@ pub mod on { fn time_stamp(&self) -> usize; } - pub trait ClipboardEventInner: Debug + GenericEventInner { + pub trait ClipboardEventInner { // DOMDataTransfer clipboardData } - pub trait CompositionEventInner: Debug { + pub trait CompositionEventInner { fn data(&self) -> String; } - pub trait KeyboardEventInner: Debug { + pub trait KeyboardEventInner { fn char_code(&self) -> u32; /// Get the key code as an enum Variant. @@ -588,15 +614,15 @@ pub mod on { fn get_modifier_state(&self, key_code: usize) -> bool; } - pub trait FocusEventInner: Debug { + pub trait FocusEventInner { /* DOMEventInnerTarget relatedTarget */ } - pub trait FormEventInner: Debug { + pub trait FormEventInner { fn value(&self) -> String; } - pub trait MouseEventInner: Debug { + pub trait MouseEventInner { fn alt_key(&self) -> bool; fn button(&self) -> i16; fn buttons(&self) -> u16; @@ -613,7 +639,7 @@ pub mod on { fn get_modifier_state(&self, key_code: &str) -> bool; } - pub trait PointerEventInner: Debug { + pub trait PointerEventInner { // Mouse only fn alt_key(&self) -> bool; fn button(&self) -> usize; @@ -640,9 +666,9 @@ pub mod on { fn is_primary(&self) -> bool; } - pub trait SelectionEventInner: Debug {} + pub trait SelectionEventInner {} - pub trait TouchEventInner: Debug { + pub trait TouchEventInner { fn alt_key(&self) -> bool; fn ctrl_key(&self) -> bool; fn meta_key(&self) -> bool; @@ -653,37 +679,37 @@ pub mod on { // touches: DOMTouchList, } - pub trait UIEventInner: Debug { + pub trait UIEventInner { // DOMAbstractView view fn detail(&self) -> i32; } - pub trait WheelEventInner: Debug { + pub trait WheelEventInner { fn delta_mode(&self) -> i32; fn delta_x(&self) -> i32; fn delta_y(&self) -> i32; fn delta_z(&self) -> i32; } - pub trait MediaEventInner: Debug {} + pub trait MediaEventInner {} - pub trait ImageEventInner: Debug { + pub trait ImageEventInner { // load error } - pub trait AnimationEventInner: Debug { + pub trait AnimationEventInner { fn animation_name(&self) -> String; fn pseudo_element(&self) -> String; fn elapsed_time(&self) -> f32; } - pub trait TransitionEventInner: Debug { + pub trait TransitionEventInner { fn property_name(&self) -> String; fn pseudo_element(&self) -> String; fn elapsed_time(&self) -> f32; } - pub trait ToggleEventInner: Debug {} + pub trait ToggleEventInner {} pub use util::KeyCode; mod util { diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 5d4aa11bb..e3dbd2ab2 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -334,7 +334,7 @@ impl VirtualDom { log::warn!("Suspense event came through, but there was no mounted node to update >:("); } Some(nodes) => { - todo!("using the wrong frame"); + // todo!("using the wrong frame"); let nodes = scope.frames.finished_frame().bump.alloc(nodes); // push the old node's root onto the stack diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 82b9e85d0..3b25700dd 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -27,7 +27,7 @@ anyhow = "1.0" futures-util = "0.3.15" [dependencies.web-sys] -version = "0.3.50" +version = "0.3.51" features = [ "Comment", "Attr", @@ -56,6 +56,7 @@ features = [ "PointerEvent", "FocusEvent", "CompositionEvent", + "ClipboardEvent", "DocumentType", "CharacterData", "SvgElement", @@ -78,7 +79,7 @@ uuid = { version = "0.8.2", features = ["v4", "wasm-bindgen"] } dioxus-hooks = { path = "../hooks" } gloo-timers = { version = "0.2.1", features = ["futures"] } serde = { version = "1.0.126", features = ["derive"] } -# surf = { git = "https://github.com/http-rs/surf", default-features = false, features = [ -# "wasm-client", -# ] } +surf = { git = "https://github.com/http-rs/surf", rev = "1ffaba8873", default-features = false, features = [ + "wasm-client", +] } # wasm-timer = "0.2.5" diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 0567a53b8..5223fdc36 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -186,6 +186,10 @@ impl WebsysDom { old_nodes.push(self.stack.pop()); } + for node in &old_nodes[1..] { + node.dyn_ref::().unwrap().remove(); + } + let old = old_nodes[0].clone(); let arr: js_sys::Array = new_nodes.iter().collect(); let el = old.dyn_into::().unwrap(); @@ -466,30 +470,140 @@ fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent { use dioxus_core::events::on::*; match event.type_().as_str() { "copy" | "cut" | "paste" => { - // let evt: web_sys::ClipboardEvent = event.clone().dyn_into().unwrap(); - - todo!() + struct WebsysClipboardEvent(); + impl ClipboardEventInner for WebsysClipboardEvent {} + VirtualEvent::ClipboardEvent(ClipboardEvent(Rc::new(WebsysClipboardEvent()))) } "compositionend" | "compositionstart" | "compositionupdate" => { let evt: web_sys::CompositionEvent = event.clone().dyn_into().unwrap(); - todo!() + struct WebsysCompositionEvent(web_sys::CompositionEvent); + impl CompositionEventInner for WebsysCompositionEvent { + fn data(&self) -> String { + todo!() + } + } + VirtualEvent::CompositionEvent(CompositionEvent(Rc::new(WebsysCompositionEvent(evt)))) } "keydown" | "keypress" | "keyup" => { + struct Event(web_sys::KeyboardEvent); + impl KeyboardEventInner for Event { + fn char_code(&self) -> u32 { + todo!() + } + fn key_code(&self) -> KeyCode { + todo!() + } + fn ctrl_key(&self) -> bool { + todo!() + } + + fn key(&self) -> String { + todo!() + } + + fn locale(&self) -> String { + todo!() + } + + fn location(&self) -> usize { + todo!() + } + + fn meta_key(&self) -> bool { + todo!() + } + + fn repeat(&self) -> bool { + todo!() + } + + fn shift_key(&self) -> bool { + todo!() + } + + fn which(&self) -> usize { + todo!() + } + + fn get_modifier_state(&self, key_code: usize) -> bool { + todo!() + } + } let evt: web_sys::KeyboardEvent = event.clone().dyn_into().unwrap(); - todo!() + VirtualEvent::KeyboardEvent(KeyboardEvent(Rc::new(Event(evt)))) } "focus" | "blur" => { + struct Event(web_sys::FocusEvent); + impl FocusEventInner for Event {} let evt: web_sys::FocusEvent = event.clone().dyn_into().unwrap(); - todo!() + VirtualEvent::FocusEvent(FocusEvent(Rc::new(Event(evt)))) } "change" => { - let evt: web_sys::Event = event.clone().dyn_into().expect("wrong error typ"); + // struct Event(web_sys::Event); + // impl GenericEventInner for Event { + // fn bubbles(&self) -> bool { + // todo!() + // } + + // fn cancel_bubble(&self) { + // todo!() + // } + + // fn cancelable(&self) -> bool { + // todo!() + // } + + // fn composed(&self) -> bool { + // todo!() + // } + + // fn composed_path(&self) -> String { + // todo!() + // } + + // fn current_target(&self) { + // todo!() + // } + + // fn default_prevented(&self) -> bool { + // todo!() + // } + + // fn event_phase(&self) -> usize { + // todo!() + // } + + // fn is_trusted(&self) -> bool { + // todo!() + // } + + // fn prevent_default(&self) { + // todo!() + // } + + // fn stop_immediate_propagation(&self) { + // todo!() + // } + + // fn stop_propagation(&self) { + // todo!() + // } + + // fn target(&self) { + // todo!() + // } + + // fn time_stamp(&self) -> usize { + // todo!() + // } + // } + // let evt: web_sys::Event = event.clone().dyn_into().expect("wrong error typ"); + // VirtualEvent::Event(GenericEvent(Rc::new(Event(evt)))) todo!() - // VirtualEvent::FormEvent(FormEvent {value:}) } "input" | "invalid" | "reset" | "submit" => { @@ -513,15 +627,6 @@ fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent { }) .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); - // let p2 = evt.data_transfer(); - - // let value: Option = (&evt).data(); - // let value = val; - // let value = value.unwrap_or_default(); - // let value = (&evt).data().expect("No data to unwrap"); - - // todo - this needs to be a "controlled" event - // these events won't carry the right data with them todo!() // VirtualEvent::FormEvent(FormEvent { value }) } @@ -606,6 +711,15 @@ fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent { let evt: web_sys::WheelEvent = event.clone().dyn_into().unwrap(); todo!() } + "animationstart" | "animationend" | "animationiteration" => { + let evt: web_sys::AnimationEvent = event.clone().dyn_into().unwrap(); + todo!() + } + + "transitionend" => { + let evt: web_sys::TransitionEvent = event.clone().dyn_into().unwrap(); + todo!() + } "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted" | "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play" @@ -618,16 +732,6 @@ fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent { todo!() } - "animationstart" | "animationend" | "animationiteration" => { - let evt: web_sys::AnimationEvent = event.clone().dyn_into().unwrap(); - todo!() - } - - "transitionend" => { - let evt: web_sys::TransitionEvent = event.clone().dyn_into().unwrap(); - todo!() - } - "toggle" => { // not required to construct anything special beyond standard event stuff (target) From f2334c17be2612d926361686d7d40a57e3ffe9b9 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 5 Aug 2021 22:23:41 -0400 Subject: [PATCH 19/21] wip: bottom up dropping --- examples/borrowed.rs | 86 +++++++ packages/core-macro/README.md | 7 + packages/core/Cargo.toml | 3 + packages/core/src/arena.rs | 53 ++-- packages/core/src/diff.rs | 204 +++++---------- packages/core/src/events.rs | 101 +++++--- packages/core/src/hooks.rs | 4 +- packages/core/src/nodes.rs | 1 - packages/core/src/scope.rs | 19 +- packages/core/src/util.rs | 34 +-- packages/core/src/virtual_dom.rs | 419 ++++++++++++++++--------------- packages/core/tests/channels.rs | 19 ++ packages/core/tests/diffing.rs | 11 +- packages/desktop/src/dom.rs | 2 +- packages/desktop/src/lib.rs | 255 ++++++++----------- packages/mobile/src/dom.rs | 2 +- packages/mobile/src/lib.rs | 4 +- packages/web/Cargo.toml | 1 + packages/web/src/cfg.rs | 11 +- packages/web/src/dom.rs | 63 ++--- packages/web/src/lib.rs | 62 ++--- 21 files changed, 682 insertions(+), 679 deletions(-) create mode 100644 examples/borrowed.rs create mode 100644 packages/core-macro/README.md create mode 100644 packages/core/tests/channels.rs diff --git a/examples/borrowed.rs b/examples/borrowed.rs new file mode 100644 index 000000000..0be0c668b --- /dev/null +++ b/examples/borrowed.rs @@ -0,0 +1,86 @@ +#![allow(non_snake_case)] +//! Example: Extremely nested borrowing +//! ----------------------------------- +//! +//! Dioxus manages borrow lifetimes for you. This means any child may borrow from its parent. However, it is not possible +//! to hand out an &mut T to children - all props are consumed by &P, so you'd only get an &&mut T. +//! +//! How does it work? +//! +//! Dioxus will manually drop closures and props - things that borrow data before the component is ran again. This is done +//! "bottom up" from the lowest child all the way to the initiating parent. As it traverses each listener and prop, the +//! drop implementation is manually called, freeing any memory and ensuring that memory is not leaked. +//! +//! We cannot drop from the parent to the children - if the drop implementation modifies the data, downstream references +//! might be broken since we take an &mut T and and &T to the data. Instead, we work bottom up, making sure to remove any +//! potential references to the data before finally giving out an &mut T. This prevents us from mutably aliasing the data, +//! and is proven to be safe with MIRI. + +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(App, |c| c); +} + +fn App<'a>(cx: Context<'a, ()>) -> DomTree<'a> { + let text: &'a mut Vec = cx.use_hook(|_| vec![String::from("abc=def")], |f| f, |_| {}); + + let first = text.get_mut(0).unwrap(); + + cx.render(rsx! { + div { + Child1 { + text: first + } + } + }) +} + +#[derive(Props)] +struct C1Props<'a> { + text: &'a mut String, +} + +impl<'a> Drop for C1Props<'a> { + fn drop(&mut self) {} +} + +fn Child1<'a>(cx: Context<'a, C1Props>) -> DomTree<'a> { + let (left, right) = cx.text.split_once("=").unwrap(); + + cx.render(rsx! { + div { + Child2 { text: left } + Child2 { text: right } + } + }) +} + +#[derive(Props)] +struct C2Props<'a> { + text: &'a str, +} +impl<'a> Drop for C2Props<'a> { + fn drop(&mut self) { + todo!() + } +} + +fn Child2<'a>(cx: Context<'a, C2Props>) -> DomTree<'a> { + cx.render(rsx! { + Child3 { + text: cx.text + } + }) +} + +#[derive(Props)] +struct C3Props<'a> { + text: &'a str, +} + +fn Child3<'a>(cx: Context<'a, C3Props>) -> DomTree<'a> { + cx.render(rsx! { + div { "{cx.text}"} + }) +} diff --git a/packages/core-macro/README.md b/packages/core-macro/README.md new file mode 100644 index 000000000..d9527a411 --- /dev/null +++ b/packages/core-macro/README.md @@ -0,0 +1,7 @@ +# core-macro + +This crate implements these macros: +- `format_args_f`: for f-string interpolation inside of text blocks +- `Props`: derive macro for typed-builder with props configurations +- +- diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 450b64b23..00038baca 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -39,9 +39,12 @@ smallvec = "1.6.1" slab = "0.4.3" +futures-channel = "0.3.16" + [dev-dependencies] anyhow = "1.0.42" +async-std = { version = "1.9.0", features = ["attributes"] } dioxus-html = { path = "../html" } diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 18fdee8a0..1759aeebc 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -33,19 +33,22 @@ impl ElementId { } type Shared = Rc>; +type TaskReceiver = futures_channel::mpsc::UnboundedReceiver; +type TaskSender = futures_channel::mpsc::UnboundedSender; /// These are resources shared among all the components and the virtualdom itself #[derive(Clone)] pub struct SharedResources { pub components: Rc>>, - pub event_queue: Shared>, - - pub events: Shared>, - pub(crate) heuristics: Shared, - pub tasks: Shared>, + /// + pub task_sender: TaskSender, + + pub task_receiver: Shared, + + pub async_tasks: Shared>, /// We use a SlotSet to keep track of the keys that are currently being used. /// However, we don't store any specific data since the "mirror" @@ -63,30 +66,39 @@ impl SharedResources { // elements are super cheap - the value takes no space let raw_elements = Slab::with_capacity(2000); - let event_queue = Rc::new(RefCell::new(Vec::new())); - let tasks = Vec::new(); + // let pending_events = Rc::new(RefCell::new(Vec::new())); + + let (sender, receiver) = futures_channel::mpsc::unbounded(); + let heuristics = HeuristicsEngine::new(); + // we allocate this task setter once to save us from having to allocate later let task_setter = { - let queue = event_queue.clone(); + let queue = sender.clone(); let components = components.clone(); Rc::new(move |idx: ScopeId| { let comps = unsafe { &*components.get() }; if let Some(scope) = comps.get(idx.0) { - queue.borrow_mut().push(HeightMarker { - height: scope.height, - idx, - }) + queue + .unbounded_send(EventTrigger::new( + VirtualEvent::ScheduledUpdate { + height: scope.height, + }, + idx, + None, + EventPriority::High, + )) + .expect("The event queu receiver should *never* be dropped"); } - }) + }) as Rc }; Self { - event_queue, components, - tasks: Rc::new(RefCell::new(FuturesUnordered::new())), - events: Rc::new(RefCell::new(tasks)), + async_tasks: Rc::new(RefCell::new(FuturesUnordered::new())), + task_receiver: Rc::new(RefCell::new(receiver)), + task_sender: sender, heuristics: Rc::new(RefCell::new(heuristics)), raw_elements: Rc::new(RefCell::new(raw_elements)), task_setter, @@ -136,12 +148,7 @@ impl SharedResources { /// return the id, freeing the space of the original node pub fn collect_garbage(&self, id: ElementId) { - let mut r: RefMut> = self.raw_elements.borrow_mut(); - r.remove(id.0); - } - - pub fn borrow_queue(&self) -> RefMut> { - self.event_queue.borrow_mut() + self.raw_elements.borrow_mut().remove(id.0); } pub fn insert_scope_with_key(&self, f: impl FnOnce(ScopeId) -> Scope) -> ScopeId { @@ -157,7 +164,7 @@ impl SharedResources { } pub fn submit_task(&self, task: FiberTask) -> TaskHandle { - self.tasks.borrow_mut().push(task); + self.async_tasks.borrow_mut().push(task); TaskHandle {} } } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index f640e1f4c..475bc530e 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -65,10 +65,11 @@ //! - https://hacks.mozilla.org/2019/03/fast-bump-allocated-virtual-doms-with-rust-and-wasm/ use crate::{arena::SharedResources, innerlude::*}; +use futures_util::Future; use fxhash::{FxBuildHasher, FxHashMap, FxHashSet}; use smallvec::{smallvec, SmallVec}; -use std::{any::Any, cell::Cell, cmp::Ordering}; +use std::{any::Any, cell::Cell, cmp::Ordering, pin::Pin}; use DomEdit::*; /// Instead of having handles directly over nodes, Dioxus uses simple u32 as node IDs. @@ -79,16 +80,32 @@ use DomEdit::*; /// nodes as the diffing algorithm descenes through the tree. This means that whatever is on top of the stack will receive /// any modifications that follow. This technique enables the diffing algorithm to avoid directly handling or storing any /// target-specific Node type as well as easily serializing the edits to be sent over a network or IPC connection. -pub trait RealDom<'a> { +pub trait RealDom { fn raw_node_as_any(&self) -> &mut dyn Any; + + /// Essentially "are we out of time to do more work?" + /// Right now, defaults to "no" giving us unlimited time to process work. + /// This will lead to blocking behavior in the UI, so implementors will want to override this. + fn must_commit(&self) -> bool { + false + } + + fn commit_edits<'a>(&mut self, edits: &mut Vec>) {} + + // pause the virtualdom until the main loop is ready to process more work + fn wait_until_ready<'s>(&'s mut self) -> Pin + 's>> { + // + Box::pin(async { + // + }) + } } -pub struct DiffMachine<'real, 'bump> { - pub real_dom: &'real dyn RealDom<'bump>, - +pub struct DiffMachine<'r, 'bump> { + // pub real_dom: &'real dyn RealDom, pub vdom: &'bump SharedResources, - pub edits: &'real mut Vec>, + pub edits: &'r mut Vec>, pub scope_stack: SmallVec<[ScopeId; 5]>, @@ -99,15 +116,13 @@ pub struct DiffMachine<'real, 'bump> { pub seen_scopes: FxHashSet, } -impl<'real, 'bump> DiffMachine<'real, 'bump> { +impl<'r, 'bump> DiffMachine<'r, 'bump> { pub(crate) fn new( - edits: &'real mut Vec>, - dom: &'real dyn RealDom<'bump>, + edits: &'r mut Vec>, cur_scope: ScopeId, shared: &'bump SharedResources, ) -> Self { Self { - real_dom: dom, edits, scope_stack: smallvec![cur_scope], vdom: shared, @@ -121,12 +136,10 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { /// /// This will PANIC if any component elements are passed in. pub fn new_headless( - edits: &'real mut Vec>, - dom: &'real dyn RealDom<'bump>, + edits: &'r mut Vec>, shared: &'bump SharedResources, ) -> Self { Self { - real_dom: dom, edits, scope_stack: smallvec![ScopeId(0)], vdom: shared, @@ -135,6 +148,14 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { } } + // + pub fn diff_scope(&mut self, id: ScopeId) -> Result<()> { + let component = self.get_scope_mut(&id).ok_or_else(|| Error::NotMounted)?; + let (old, new) = (component.frames.wip_head(), component.frames.fin_head()); + self.diff_node(old, new); + Ok(()) + } + // Diff the `old` node with the `new` node. Emits instructions to modify a // physical DOM node that reflects `old` into something that reflects `new`. // @@ -396,11 +417,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { } = el; let real_id = self.vdom.reserve_node(); - if let Some(namespace) = namespace { - self.edit_create_element(tag_name, Some(namespace), real_id) - } else { - self.edit_create_element(tag_name, None, real_id) - }; + self.edit_create_element(tag_name, *namespace, real_id); dom_id.set(Some(real_id)); let cur_scope = self.current_scope().unwrap(); @@ -1451,6 +1468,29 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { }); } + pub(crate) fn edit_set_attribute_ns( + &mut self, + attribute: &'bump Attribute, + namespace: &'bump str, + ) { + let Attribute { + name, + value, + is_static, + is_volatile, + // namespace, + .. + } = attribute; + // field: &'static str, + // value: &'bump str, + // ns: Option<&'static str>, + self.edits.push(SetAttribute { + field: name, + value, + ns: Some(namespace), + }); + } + pub(crate) fn edit_remove_attribute(&mut self, attribute: &Attribute) { let name = attribute.name; self.edits.push(RemoveAttribute { name }); @@ -1627,131 +1667,3 @@ fn compare_strs(a: &str, b: &str) -> bool { true } } - -// // Now we will iterate from the end of the new children back to the -// // beginning, diffing old children we are reusing and if they aren't in the -// // LIS moving them to their new destination, or creating new children. Note -// // that iterating in reverse order lets us use `Node.prototype.insertBefore` -// // to move/insert children. -// // -// // But first, we ensure that we have a child on the change list stack that -// // we can `insertBefore`. We handle this once before looping over `new` -// // children, so that we don't have to keep checking on every loop iteration. -// if shared_suffix_count > 0 { -// // There is a shared suffix after these middle children. We will be -// // inserting before that shared suffix, so add the first child of that -// // shared suffix to the change list stack. -// // -// // [... parent] - -// // TODO - -// // self.edits -// // .go_down_to_child(old_shared_suffix_start - removed_count); -// // [... parent first_child_of_shared_suffix] -// } else { -// // There is no shared suffix coming after these middle children. -// // Therefore we have to process the last child in `new` and move it to -// // the end of the parent's children if it isn't already there. -// let last_index = new.len() - 1; -// // uhhhh why an unwrap? -// let last = new.last().unwrap(); -// // let last = new.last().unwrap_throw(); -// new = &new[..new.len() - 1]; -// if shared_keys.contains(&last.key()) { -// let old_index = new_index_to_old_index[last_index]; -// let temp = old_index_to_temp[old_index]; -// // [... parent] -// // self.go_down_to_temp_child(temp); -// // [... parent last] -// self.diff_node(&old[old_index], last); - -// if new_index_is_in_lis.contains(&last_index) { -// // Don't move it, since it is already where it needs to be. -// } else { -// // self.commit_traversal(); -// // [... parent last] -// // self.append_child(); -// // [... parent] -// // self.go_down_to_temp_child(temp); -// // [... parent last] -// } -// } else { -// // self.commit_traversal(); -// // [... parent] -// let meta = self.create_vnode(last); - -// // [... parent last] -// // self.append_child(); -// // [... parent] -// // self.go_down_to_reverse_child(0); -// // [... parent last] -// } -// } - -// for (new_index, new_child) in new.iter().enumerate().rev() { -// let old_index = new_index_to_old_index[new_index]; -// if old_index == u32::MAX as usize { -// debug_assert!(!shared_keys.contains(&new_child.key())); -// // self.commit_traversal(); -// // [... parent successor] -// let meta = self.create_vnode(new_child); -// // [... parent successor new_child] -// self.edit_insert_after(meta.added_to_stack); -// // self.insert_before(); -// // [... parent new_child] -// } else { -// debug_assert!(shared_keys.contains(&new_child.key())); -// let temp = old_index_to_temp[old_index]; -// debug_assert_ne!(temp, u32::MAX); - -// if new_index_is_in_lis.contains(&new_index) { -// // [... parent successor] -// // self.go_to_temp_sibling(temp); -// // [... parent new_child] -// } else { -// // self.commit_traversal(); -// // [... parent successor] -// // self.push_temporary(temp); -// // [... parent successor new_child] -// // self.insert_before(); -// // [... parent new_child] -// } - -// self.diff_node(&old[old_index], new_child); -// } -// } - -// Save each of the old children whose keys are reused in the new -// children -// let reused_children = vec![]; -// let mut old_index_to_temp = vec![u32::MAX; old.len()]; -// let mut start = 0; -// loop { -// let end = (start..old.len()) -// .find(|&i| { -// let key = old[i].key(); -// !shared_keys.contains(&key) -// }) -// .unwrap_or(old.len()); - -// if end - start > 0 { -// // self.commit_traversal(); -// // let mut t = 5; -// let mut t = self.save_children_to_temporaries( -// shared_prefix_count + start, -// shared_prefix_count + end, -// ); -// for i in start..end { -// old_index_to_temp[i] = t; -// t += 1; -// } -// } - -// debug_assert!(end <= old.len()); -// if end == old.len() { -// break; -// } else { -// start = end + 1; -// } -// } diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 3c8cc13c8..17ec98d81 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -10,7 +10,7 @@ use std::{ }; use crate::{ - innerlude::{ElementId, HeightMarker, ScopeId}, + innerlude::{ElementId, ScopeId}, VNode, }; @@ -28,18 +28,35 @@ pub struct EventTrigger { /// The priority of the event pub priority: EventPriority, } - impl EventTrigger { - pub fn new_from_task(originator: ScopeId, hook_idx: usize) -> Self { - Self { - originator, - event: VirtualEvent::AsyncEvent { hook_idx }, - priority: EventPriority::Low, - real_node_id: None, + pub fn make_key(&self) -> EventKey { + EventKey { + originator: self.originator, + priority: self.priority, } } } +#[derive(PartialEq, Eq)] +pub struct EventKey { + /// The originator of the event trigger + pub originator: ScopeId, + /// The priority of the event + pub priority: EventPriority, + // TODO: add the time that the event was queued +} + +impl PartialOrd for EventKey { + fn partial_cmp(&self, other: &Self) -> Option { + todo!() + } +} +impl Ord for EventKey { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + todo!() + } +} + /// Priority of Event Triggers. /// /// Internally, Dioxus will abort work that's taking too long if new, more important, work arrives. Unlike React, Dioxus @@ -47,7 +64,7 @@ impl EventTrigger { /// 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. -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] pub enum EventPriority { /// Garbage collection is a type of work than can be scheduled around other work, but must be completed in a specific /// order. The GC must be run for a component before any other future work for that component is run. Otherwise, @@ -92,6 +109,45 @@ impl EventTrigger { } pub enum VirtualEvent { + /// Generated during diffing to signal that a component's nodes to be given back + /// + /// Typically has a high priority + /// + /// If an event is scheduled for a component that has "garbage", that garabge will be cleaned up before the event can + /// be processed. + GarbageCollection, + + /// + DiffComponent, + + /// A type of "immediate" event scheduled by components + ScheduledUpdate { + height: u32, + }, + + // Whenever a task is ready (complete) Dioxus produces this "AsyncEvent" + // + // Async events don't necessarily propagate into a scope being ran. It's up to the event itself + // to force an update for itself. + // + // Most async events should have a low priority. + // + // This type exists for the task/concurrency system to signal that a task is ready. + // However, this does not necessarily signal that a scope must be re-ran, so the hook implementation must cause its + // own re-run. + AsyncEvent {}, + + // Suspense events are a type of async event + // + // they have the lowest priority + SuspenseEvent { + hook_idx: usize, + domnode: Rc>>, + }, + + // image event has conflicting method types + // ImageEvent(event_data::ImageEvent), + // Real events ClipboardEvent(on::ClipboardEvent), CompositionEvent(on::CompositionEvent), @@ -108,30 +164,6 @@ pub enum VirtualEvent { ToggleEvent(on::ToggleEvent), MouseEvent(on::MouseEvent), PointerEvent(on::PointerEvent), - - GarbageCollection, - - // image event has conflicting method types - // ImageEvent(event_data::ImageEvent), - SetStateEvent { - height: HeightMarker, - }, - - // Whenever a task is ready (complete) Dioxus produces this "AsyncEvent" - // - // Async events don't necessarily propagate into a scope being ran. It's up to the event itself - // to force an update for itself. - AsyncEvent { - hook_idx: usize, - }, - - // These are more intrusive than the rest - SuspenseEvent { - hook_idx: usize, - domnode: Rc>>, - }, - // TOOD: make garbage collection its own dedicated event - // GarbageCollection {} } impl std::fmt::Debug for VirtualEvent { @@ -153,9 +185,10 @@ impl std::fmt::Debug for VirtualEvent { VirtualEvent::MouseEvent(_) => "MouseEvent", VirtualEvent::PointerEvent(_) => "PointerEvent", VirtualEvent::GarbageCollection => "GarbageCollection", - VirtualEvent::SetStateEvent { .. } => "SetStateEvent", + VirtualEvent::ScheduledUpdate { .. } => "SetStateEvent", VirtualEvent::AsyncEvent { .. } => "AsyncEvent", VirtualEvent::SuspenseEvent { .. } => "SuspenseEvent", + VirtualEvent::DiffComponent { .. } => "DiffComponent", }; f.debug_struct("VirtualEvent").field("type", &name).finish() diff --git a/packages/core/src/hooks.rs b/packages/core/src/hooks.rs index d7e478606..4fef6aa1c 100644 --- a/packages/core/src/hooks.rs +++ b/packages/core/src/hooks.rs @@ -111,7 +111,7 @@ where // whenever the task is complete, save it into th cx.use_hook( - move |hook_idx| { + move |_| { let task_fut = task_initializer(); let task_dump = Rc::new(RefCell::new(None)); @@ -127,7 +127,7 @@ where *slot.as_ref().borrow_mut() = Some(output); updater(update_id); EventTrigger { - event: VirtualEvent::AsyncEvent { hook_idx }, + event: VirtualEvent::AsyncEvent {}, originator, priority: EventPriority::Low, real_node_id: None, diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 4986dd07a..49958d905 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -359,7 +359,6 @@ impl<'a> NodeFactory<'a> { component: FC

, props: P, key: Option, - // key: Option<&'a str>, children: V, ) -> VNode<'a> where diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index 5c1352a16..b303170f3 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -115,6 +115,8 @@ impl Scope { // This breaks any latent references, invalidating every pointer referencing into it. // Remove all the outdated listeners if !self.pending_garbage.borrow().is_empty() { + // We have some garbage to clean up + log::error!("Cleaning up garabge"); panic!("cannot run scope while garbage is pending! Please clean up your mess first"); } @@ -122,18 +124,17 @@ impl Scope { // make sure we call the drop implementation on all the listeners // this is important to not leak memory - for listener in self - .listeners + self.listeners .borrow_mut() .drain(..) .map(|li| unsafe { &*li }) - { - let mut cb = listener.callback.borrow_mut(); - match cb.take() { - Some(val) => std::mem::drop(val), - None => log::info!("no callback to drop. component must be broken"), - }; - } + .for_each(|listener| { + listener.callback.borrow_mut().take(); + }); + + // make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we + // run the hooks (which hold an &mut Referrence) + // right now, we don't drop // Safety: // - We dropped the listeners, so no more &mut T can be used while these are held diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs index 673a1923c..33a6d9fb2 100644 --- a/packages/core/src/util.rs +++ b/packages/core/src/util.rs @@ -8,24 +8,24 @@ pub fn empty_cell() -> Cell> { Cell::new(None) } -/// A helper type that lets scopes be ordered by their height -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct HeightMarker { - pub idx: ScopeId, - pub height: u32, -} +// /// A helper type that lets scopes be ordered by their height +// #[derive(Debug, Clone, Copy, PartialEq, Eq)] +// pub struct HeightMarker { +// pub idx: ScopeId, +// pub height: u32, +// } -impl Ord for HeightMarker { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.height.cmp(&other.height) - } -} +// impl Ord for HeightMarker { +// fn cmp(&self, other: &Self) -> std::cmp::Ordering { +// self.height.cmp(&other.height) +// } +// } -impl PartialOrd for HeightMarker { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} +// impl PartialOrd for HeightMarker { +// fn partial_cmp(&self, other: &Self) -> Option { +// Some(self.cmp(other)) +// } +// } pub struct DebugDom {} impl DebugDom { @@ -34,7 +34,7 @@ impl DebugDom { } } -impl<'a> RealDom<'a> for DebugDom { +impl RealDom for DebugDom { fn raw_node_as_any(&self) -> &mut dyn std::any::Any { todo!() } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index e3dbd2ab2..335b5f8c1 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -18,8 +18,9 @@ //! //! This module includes just the barebones for a complete VirtualDOM API. //! Additional functionality is defined in the respective files. - +#![allow(unreachable_code)] use futures_util::StreamExt; +use fxhash::FxHashMap; use crate::hooks::{SuspendedContext, SuspenseHook}; use crate::{arena::SharedResources, innerlude::*}; @@ -28,6 +29,7 @@ use std::any::Any; use std::any::TypeId; use std::cell::{Ref, RefCell, RefMut}; +use std::collections::{BTreeMap, BTreeSet, BinaryHeap}; use std::pin::Pin; /// An integrated virtual node system that progresses events and diffs UI trees. @@ -51,7 +53,7 @@ pub struct VirtualDom { /// Should always be the first (gen=0, id=0) pub base_scope: ScopeId, - pub triggers: RefCell>, + pending_events: BTreeMap, // for managing the props that were used to create the dom #[doc(hidden)] @@ -61,9 +63,6 @@ pub struct VirtualDom { _root_props: std::pin::Pin>, } -// ====================================== -// Public Methods for the VirtualDom -// ====================================== impl VirtualDom { /// Create a new instance of the Dioxus Virtual Dom with no properties for the root component. /// @@ -145,14 +144,14 @@ impl VirtualDom { base_scope, _root_props: root_props, shared: components, - triggers: Default::default(), + pending_events: BTreeMap::new(), _root_prop_type: TypeId::of::

(), } } pub fn launch_in_place(root: FC<()>) -> Self { let mut s = Self::new(root); - s.rebuild_in_place(); + s.rebuild_in_place().unwrap(); s } @@ -160,10 +159,18 @@ impl VirtualDom { /// pub fn launch_with_props_in_place(root: FC

, root_props: P) -> Self { let mut s = Self::new_with_props(root, root_props); - s.rebuild_in_place(); + s.rebuild_in_place().unwrap(); s } + pub fn base_scope(&self) -> &Scope { + unsafe { self.shared.get_scope(self.base_scope).unwrap() } + } + + pub fn get_scope(&self, id: ScopeId) -> Option<&Scope> { + unsafe { self.shared.get_scope(id) } + } + /// Rebuilds the VirtualDOM from scratch, but uses a "dummy" RealDom. /// /// Used in contexts where a real copy of the structure doesn't matter, and the VirtualDOM is the source of truth. @@ -174,21 +181,22 @@ impl VirtualDom { /// /// SSR takes advantage of this by using Dioxus itself as the source of truth, and rendering from the tree directly. pub fn rebuild_in_place(&mut self) -> Result> { - let mut realdom = DebugDom::new(); - let mut edits = Vec::new(); - self.rebuild(&mut realdom, &mut edits)?; - Ok(edits) + todo!(); + // let mut realdom = DebugDom::new(); + // let mut edits = Vec::new(); + // self.rebuild(&mut realdom, &mut edits)?; + // Ok(edits) } /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom rom scratch /// /// The diff machine expects the RealDom's stack to be the root of the application - pub fn rebuild<'s>( - &'s mut self, - realdom: &'_ mut dyn RealDom<'s>, - edits: &'_ mut Vec>, - ) -> Result<()> { - let mut diff_machine = DiffMachine::new(edits, realdom, self.base_scope, &self.shared); + /// + /// Events like garabge collection, application of refs, etc are not handled by this method and can only be progressed + /// through "run" + /// + pub fn rebuild<'s>(&'s mut self, edits: &'_ mut Vec>) -> Result<()> { + let mut diff_machine = DiffMachine::new(edits, self.base_scope, &self.shared); let cur_component = diff_machine .get_scope_mut(&self.base_scope) @@ -209,219 +217,220 @@ impl VirtualDom { Ok(()) } - /// - /// - /// - /// - /// - pub fn queue_event(&self, trigger: EventTrigger) { - let mut triggers = self.triggers.borrow_mut(); - triggers.push(trigger); + async fn select_next_event(&mut self) -> Option { + let mut receiver = self.shared.task_receiver.borrow_mut(); + + // drain the in-flight events so that we can sort them out with the current events + while let Ok(Some(trigger)) = receiver.try_next() { + log::info!("scooping event from receiver"); + self.pending_events.insert(trigger.make_key(), trigger); + } + + if self.pending_events.is_empty() { + // Continuously poll the future pool and the event receiver for work + let mut tasks = self.shared.async_tasks.borrow_mut(); + let tasks_tasks = tasks.next(); + + let mut receiver = self.shared.task_receiver.borrow_mut(); + let reciv_task = receiver.next(); + + futures_util::pin_mut!(tasks_tasks); + futures_util::pin_mut!(reciv_task); + + let trigger = match futures_util::future::select(tasks_tasks, reciv_task).await { + futures_util::future::Either::Left((trigger, _)) => trigger, + futures_util::future::Either::Right((trigger, _)) => trigger, + } + .unwrap(); + self.pending_events.insert(trigger.make_key(), trigger); + } + + todo!() + + // let trigger = self.select_next_event().unwrap(); + // trigger } - /// This method is the most sophisticated way of updating the virtual dom after an external event has been triggered. - /// - /// Given a synthetic event, the component that triggered the event, and the index of the callback, this runs the virtual - /// dom to completion, tagging components that need updates, compressing events together, and finally emitting a single - /// change list. - /// - /// If implementing an external renderer, this is the perfect method to combine with an async event loop that waits on - /// listeners, something like this: - /// - /// ```ignore - /// while let Ok(event) = receiver.recv().await { - /// let edits = self.internal_dom.progress_with_event(event)?; - /// for edit in &edits { - /// patch_machine.handle_edit(edit); - /// } - /// } - /// ``` - /// - /// Note: this method is not async and does not provide suspense-like functionality. It is up to the renderer to provide the - /// executor and handlers for suspense as show in the example. - /// - /// ```ignore - /// let (sender, receiver) = channel::new(); - /// sender.send(EventTrigger::start()); - /// - /// let mut dom = VirtualDom::new(); - /// dom.suspense_handler(|event| sender.send(event)); - /// - /// while let Ok(diffs) = dom.progress_with_event(receiver.recv().await) { - /// render(diffs); - /// } - /// - /// ``` + // the cooperartive, fiber-based scheduler + // is "awaited" and will always return some edits for the real dom to apply // - // Developer notes: - // ---- - // This method has some pretty complex safety guarantees to uphold. - // We interact with bump arenas, raw pointers, and use UnsafeCell to get a partial borrow of the arena. - // The final EditList has edits that pull directly from the Bump Arenas which add significant complexity - // in crafting a 100% safe solution with traditional lifetimes. Consider this method to be internally unsafe - // but the guarantees provide a safe, fast, and efficient abstraction for the VirtualDOM updating framework. + // Right now, we'll happily partially render component trees // - // A good project would be to remove all unsafe from this crate and move the unsafety into safer abstractions. - pub async fn progress_with_event<'a, 's>( - &'s mut self, - realdom: &'a mut dyn RealDom<'s>, - edits: &'a mut Vec>, - ) -> Result<()> { - let trigger = self.triggers.borrow_mut().pop().expect("failed"); + // Normally, you would descend through the tree depth-first, but we actually descend breadth-first. + pub async fn run(&mut self, realdom: &mut dyn RealDom) -> Result<()> { + let cur_component = self.base_scope; + let mut edits = Vec::new(); + let resources = self.shared.clone(); - let mut diff_machine = DiffMachine::new(edits, realdom, trigger.originator, &self.shared); + let mut diff_machine = DiffMachine::new(&mut edits, cur_component, &resources); - match &trigger.event { - // When a scope gets destroyed during a diff, it gets its own garbage collection event - // However, an old scope might be attached - VirtualEvent::GarbageCollection => { - let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); + loop { + let trigger = self.select_next_event().await.unwrap(); - let mut garbage_list = scope.consume_garbage(); + match &trigger.event { + // Collecting garabge is not currently interruptible. + // + // In the future, it could be though + VirtualEvent::GarbageCollection => { + let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); - while let Some(node) = garbage_list.pop() { - match &node.kind { - VNodeKind::Text(_) => { - // - self.shared.collect_garbage(node.direct_id()) - } - VNodeKind::Anchor(anchor) => { - // - } + let mut garbage_list = scope.consume_garbage(); - VNodeKind::Element(el) => { - self.shared.collect_garbage(node.direct_id()); - for child in el.children { - garbage_list.push(child); + let mut scopes_to_kill = Vec::new(); + while let Some(node) = garbage_list.pop() { + match &node.kind { + VNodeKind::Text(_) => { + self.shared.collect_garbage(node.direct_id()); + } + VNodeKind::Anchor(_) => { + self.shared.collect_garbage(node.direct_id()); + } + VNodeKind::Suspended(_) => { + self.shared.collect_garbage(node.direct_id()); + } + + VNodeKind::Element(el) => { + self.shared.collect_garbage(node.direct_id()); + for child in el.children { + garbage_list.push(child); + } + } + + VNodeKind::Fragment(frag) => { + for child in frag.children { + garbage_list.push(child); + } + } + + VNodeKind::Component(comp) => { + // TODO: run the hook destructors and then even delete the scope + // TODO: allow interruption here + if !realdom.must_commit() { + let scope_id = comp.ass_scope.get().unwrap(); + let scope = self.get_scope(scope_id).unwrap(); + let root = scope.root(); + garbage_list.push(root); + scopes_to_kill.push(scope_id); + } } } + } - VNodeKind::Fragment(frag) => { - for child in frag.children { - garbage_list.push(child); - } - } - VNodeKind::Component(comp) => { - // run the destructors - todo!(); - } - VNodeKind::Suspended(node) => { - // make sure the task goes away - todo!(); - } + for scope in scopes_to_kill { + // oy kill em + log::debug!("should be removing scope {:#?}", scope); } } - } - // Nothing yet - VirtualEvent::AsyncEvent { .. } => {} - - // Suspense Events! A component's suspended node is updated - VirtualEvent::SuspenseEvent { hook_idx, domnode } => { - // Safety: this handler is the only thing that can mutate shared items at this moment in tim - let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); - - // safety: we are sure that there are no other references to the inner content of suspense hooks - let hook = unsafe { scope.hooks.get_mut::(*hook_idx) }.unwrap(); - - let cx = Context { scope, props: &() }; - let scx = SuspendedContext { inner: cx }; - - // generate the new node! - let nodes: Option> = (&hook.callback)(scx); - match nodes { - None => { - log::warn!("Suspense event came through, but there was no mounted node to update >:("); - } - Some(nodes) => { - // todo!("using the wrong frame"); - let nodes = scope.frames.finished_frame().bump.alloc(nodes); - - // push the old node's root onto the stack - let real_id = domnode.get().ok_or(Error::NotMounted)?; - diff_machine.edit_push_root(real_id); - - // push these new nodes onto the diff machines stack - let meta = diff_machine.create_vnode(&*nodes); - - // replace the placeholder with the new nodes we just pushed on the stack - diff_machine.edit_replace_with(1, meta.added_to_stack); - } + VirtualEvent::AsyncEvent { .. } => { + // we want to progress these events + // However, there's nothing we can do for these events, they must generate their own events. } - } - // This is the "meat" of our cooperative scheduler - // As updates flow in, we re-evalute the event queue and decide if we should be switching the type of work - // - // We use the reconciler to request new IDs and then commit/uncommit the IDs when the scheduler is finished - _ => { - diff_machine - .get_scope_mut(&trigger.originator) - .map(|f| f.call_listener(trigger)); + // Suspense Events! A component's suspended node is updated + VirtualEvent::SuspenseEvent { hook_idx, domnode } => { + // Safety: this handler is the only thing that can mutate shared items at this moment in tim + let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); - // Now, there are events in the queue - let mut updates = self.shared.borrow_queue(); + // safety: we are sure that there are no other references to the inner content of suspense hooks + let hook = unsafe { scope.hooks.get_mut::(*hook_idx) }.unwrap(); - // Order the nodes by their height, we want the nodes with the smallest depth on top - // This prevents us from running the same component multiple times - updates.sort_unstable(); + let cx = Context { scope, props: &() }; + let scx = SuspendedContext { inner: cx }; - log::debug!("There are: {:#?} updates to be processed", updates.len()); - - // Iterate through the triggered nodes (sorted by height) and begin to diff them - for update in updates.drain(..) { - log::debug!("Running updates for: {:#?}", update); - - // Make sure this isn't a node we've already seen, we don't want to double-render anything - // If we double-renderer something, this would cause memory safety issues - if diff_machine.seen_scopes.contains(&update.idx) { - log::debug!("Skipping update for: {:#?}", update); - continue; - } - - // Start a new mutable borrow to components - // We are guaranteeed that this scope is unique because we are tracking which nodes have modified in the diff machine - let cur_component = diff_machine - .get_scope_mut(&update.idx) - .expect("Failed to find scope or borrow would be aliasing"); - - // Now, all the "seen nodes" are nodes that got notified by running this listener - diff_machine.seen_scopes.insert(update.idx.clone()); - - if cur_component.run_scope().is_ok() { - let (old, new) = ( - cur_component.frames.wip_head(), - cur_component.frames.fin_head(), + // generate the new node! + let nodes: Option = (&hook.callback)(scx); + match nodes { + None => { + log::warn!( + "Suspense event came through, but there were no generated nodes >:(." ); - diff_machine.diff_node(old, new); + } + Some(nodes) => { + // allocate inside the finished frame - not the WIP frame + let nodes = scope.frames.finished_frame().bump.alloc(nodes); + + // push the old node's root onto the stack + let real_id = domnode.get().ok_or(Error::NotMounted)?; + diff_machine.edit_push_root(real_id); + + // push these new nodes onto the diff machines stack + let meta = diff_machine.create_vnode(&*nodes); + + // replace the placeholder with the new nodes we just pushed on the stack + diff_machine.edit_replace_with(1, meta.added_to_stack); + } } } + + // Run the component + VirtualEvent::ScheduledUpdate { height: u32 } => { + let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); + + match scope.run_scope() { + Ok(_) => { + let event = VirtualEvent::DiffComponent; + let trigger = EventTrigger { + event, + originator: trigger.originator, + priority: EventPriority::High, + real_node_id: None, + }; + self.shared.task_sender.unbounded_send(trigger); + } + Err(_) => { + log::error!("failed to run this component!"); + } + } + } + + VirtualEvent::DiffComponent => { + diff_machine.diff_scope(trigger.originator)?; + } + + // Process any user-facing events just by calling their listeners + // This performs no actual work - but the listeners themselves will cause insert new work + VirtualEvent::ClipboardEvent(_) + | VirtualEvent::CompositionEvent(_) + | VirtualEvent::KeyboardEvent(_) + | VirtualEvent::FocusEvent(_) + | VirtualEvent::FormEvent(_) + | VirtualEvent::SelectionEvent(_) + | VirtualEvent::TouchEvent(_) + | VirtualEvent::UIEvent(_) + | VirtualEvent::WheelEvent(_) + | VirtualEvent::MediaEvent(_) + | VirtualEvent::AnimationEvent(_) + | VirtualEvent::TransitionEvent(_) + | VirtualEvent::ToggleEvent(_) + | VirtualEvent::MouseEvent(_) + | VirtualEvent::PointerEvent(_) => { + let scope_id = &trigger.originator; + let scope = unsafe { self.shared.get_scope_mut(*scope_id) }; + match scope { + Some(scope) => scope.call_listener(trigger)?, + None => { + log::warn!("No scope found for event: {:#?}", scope_id); + } + } + } + } + + if realdom.must_commit() || self.pending_events.is_empty() { + realdom.commit_edits(&mut diff_machine.edits); + realdom.wait_until_ready().await; } } Ok(()) } - pub async fn wait_for_event(&mut self) -> Option { - let r = self.shared.tasks.clone(); - let mut r = r.borrow_mut(); - let gh = r.next().await; - - gh - } - - pub fn any_pending_events(&self) -> bool { - let r = self.shared.tasks.clone(); - let r = r.borrow(); - !r.is_empty() - } - - pub fn base_scope(&self) -> &Scope { - unsafe { self.shared.get_scope(self.base_scope).unwrap() } - } - - pub fn get_scope(&self, id: ScopeId) -> Option<&Scope> { - unsafe { self.shared.get_scope(id) } + pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender { + todo!() + // std::rc::Rc::new(move |_| { + // // + // }) + // todo() } } @@ -429,3 +438,9 @@ impl VirtualDom { // These impls are actually wrong. The DOM needs to have a mutex implemented. unsafe impl Sync for VirtualDom {} unsafe impl Send for VirtualDom {} + +fn select_next_event( + pending_events: &mut BTreeMap, +) -> Option { + None +} diff --git a/packages/core/tests/channels.rs b/packages/core/tests/channels.rs new file mode 100644 index 000000000..2eb64e5b1 --- /dev/null +++ b/packages/core/tests/channels.rs @@ -0,0 +1,19 @@ +use futures_channel::mpsc::unbounded; + +#[async_std::test] +async fn channels() { + let (sender, mut receiver) = unbounded::(); + + // drop(sender); + + match receiver.try_next() { + Ok(a) => { + dbg!(a); + } + Err(no) => { + dbg!(no); + } + } + + sender.unbounded_send(1).unwrap(); +} diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index e8a7522b7..8f7049431 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -41,8 +41,7 @@ impl TestDom { fn diff<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Vec> { let mut edits = Vec::new(); - let dom = DebugDom::new(); - let mut machine = DiffMachine::new_headless(&mut edits, &dom, &self.resources); + let mut machine = DiffMachine::new_headless(&mut edits, &self.resources); machine.diff_node(old, new); edits } @@ -53,9 +52,8 @@ impl TestDom { { let old = self.bump.alloc(self.render(left)); let mut edits = Vec::new(); - let dom = DebugDom::new(); - let mut machine = DiffMachine::new_headless(&mut edits, &dom, &self.resources); + let mut machine = DiffMachine::new_headless(&mut edits, &self.resources); let meta = machine.create_vnode(old); (meta, edits) } @@ -74,13 +72,12 @@ impl TestDom { let new = self.bump.alloc(self.render(right)); let mut create_edits = Vec::new(); - let dom = DebugDom::new(); - let mut machine = DiffMachine::new_headless(&mut create_edits, &dom, &self.resources); + let mut machine = DiffMachine::new_headless(&mut create_edits, &self.resources); machine.create_vnode(old); let mut edits = Vec::new(); - let mut machine = DiffMachine::new_headless(&mut edits, &dom, &self.resources); + let mut machine = DiffMachine::new_headless(&mut edits, &self.resources); machine.diff_node(old, new); (create_edits, edits) } diff --git a/packages/desktop/src/dom.rs b/packages/desktop/src/dom.rs index 4c08b0c82..001552586 100644 --- a/packages/desktop/src/dom.rs +++ b/packages/desktop/src/dom.rs @@ -29,7 +29,7 @@ impl WebviewDom<'_> { // self.registry // } } -impl<'bump> RealDom<'bump> for WebviewDom<'bump> { +impl RealDom for WebviewDom<'_> { fn raw_node_as_any(&self) -> &mut dyn std::any::Any { todo!() // self.edits.push(PushRoot { root }); diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index afcfc8383..108883991 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -5,6 +5,7 @@ use std::sync::{Arc, RwLock}; use cfg::DesktopConfig; use dioxus_core::*; +use serde::{Deserialize, Serialize}; pub use wry; use wry::application::event::{Event, WindowEvent}; @@ -83,7 +84,33 @@ impl WebviewRenderer { let window = window.build(&event_loop)?; - let vir = VirtualDom::new_with_props(root, props); + let mut vir = VirtualDom::new_with_props(root, props); + + let channel = vir.get_event_sender(); + struct WebviewBridge {} + impl RealDom for WebviewBridge { + fn raw_node_as_any(&self) -> &mut dyn std::any::Any { + todo!() + } + + fn must_commit(&self) -> bool { + false + } + + fn commit_edits<'a>(&mut self, edits: &mut Vec>) {} + + fn wait_until_ready<'s>( + &'s mut self, + ) -> std::pin::Pin + 's>> { + // + Box::pin(async { + // + }) + } + } + + let mut real_dom = WebviewBridge {}; + // async_std::task::spawn_local(vir.run(&mut real_dom)); // todo: combine these or something let vdom = Arc::new(RwLock::new(vir)); @@ -91,94 +118,80 @@ impl WebviewRenderer { let webview = WebViewBuilder::new(window)? .with_url(&format!("data:text/html,{}", HTML_CONTENT))? - .with_rpc_handler(move |_window: &Window, mut req: RpcRequest| { - match req.method.as_str() { - "initiate" => { - let edits = if let Some(edits) = &redits { - serde_json::to_value(edits).unwrap() - } else { - let mut lock = vdom.write().unwrap(); - // let mut reg_lock = registry.write().unwrap(); - - // Create the thin wrapper around the registry to collect the edits into - let mut real = dom::WebviewDom::new(); - let pre = pre_rendered.clone(); - - let response = match pre { - Some(content) => { - lock.rebuild_in_place().unwrap(); - - Response { - edits: Vec::new(), - pre_rendered: Some(content), - } - } - None => { - // - let edits = { - let mut edits = Vec::new(); - lock.rebuild(&mut real, &mut edits).unwrap(); - edits - }; - Response { - edits, - pre_rendered: None, - } - } - }; - - serde_json::to_value(&response).unwrap() - }; - - // Return the edits into the webview runtime - Some(RpcResponse::new_result(req.id.take(), Some(edits))) - } - "user_event" => { - log::debug!("User event received"); - - // let registry = registry.clone(); - let vdom = vdom.clone(); - let response = async_std::task::block_on(async move { - let mut lock = vdom.write().unwrap(); - // let mut reg_lock = registry.write().unwrap(); - - // a deserialized event - let data = req.params.unwrap(); - log::debug!("Data: {:#?}", data); - let event = trigger_from_serialized(data); - - lock.queue_event(event); - - // Create the thin wrapper around the registry to collect the edits into - let mut real = dom::WebviewDom::new(); - - // Serialize the edit stream - // - let mut edits = Vec::new(); - lock.progress_with_event(&mut real, &mut edits) - .await - .expect("failed to progress"); - - let response = Response { - edits, - pre_rendered: None, - }; - let response = serde_json::to_value(&response).unwrap(); - - // Give back the registry into its slot - // *reg_lock = Some(real.consume()); - - // Return the edits into the webview runtime - Some(RpcResponse::new_result(req.id.take(), Some(response))) - }); - - response - - // spawn a task to clean up the garbage - } - _ => todo!("this message failed"), - } - }) + // .with_rpc_handler(move |_window: &Window, mut req: RpcRequest| { + // match req.method.as_str() { + // "initiate" => { + // let edits = if let Some(edits) = &redits { + // serde_json::to_value(edits).unwrap() + // } else { + // let mut lock = vdom.write().unwrap(); + // // let mut reg_lock = registry.write().unwrap(); + // // Create the thin wrapper around the registry to collect the edits into + // let mut real = dom::WebviewDom::new(); + // let pre = pre_rendered.clone(); + // let response = match pre { + // Some(content) => { + // lock.rebuild_in_place().unwrap(); + // Response { + // edits: Vec::new(), + // pre_rendered: Some(content), + // } + // } + // None => { + // // + // let edits = { + // // let mut edits = Vec::new(); + // todo!() + // // lock.rebuild(&mut real, &mut edits).unwrap(); + // // edits + // }; + // Response { + // edits, + // pre_rendered: None, + // } + // } + // }; + // serde_json::to_value(&response).unwrap() + // }; + // // Return the edits into the webview runtime + // Some(RpcResponse::new_result(req.id.take(), Some(edits))) + // } + // "user_event" => { + // log::debug!("User event received"); + // // let registry = registry.clone(); + // let vdom = vdom.clone(); + // let response = async_std::task::block_on(async move { + // let mut lock = vdom.write().unwrap(); + // // let mut reg_lock = registry.write().unwrap(); + // // a deserialized event + // let data = req.params.unwrap(); + // log::debug!("Data: {:#?}", data); + // let event = trigger_from_serialized(data); + // // lock.queue_event(event); + // // Create the thin wrapper around the registry to collect the edits into + // let mut real = dom::WebviewDom::new(); + // // Serialize the edit stream + // // + // let mut edits = Vec::new(); + // // lock.run(&mut real, &mut edits) + // // .await + // // .expect("failed to progress"); + // let response = Response { + // edits, + // pre_rendered: None, + // }; + // let response = serde_json::to_value(&response).unwrap(); + // // Give back the registry into its slot + // // *reg_lock = Some(real.consume()); + // // Return the edits into the webview runtime + // Some(RpcResponse::new_result(req.id.take(), Some(response))) + // }); + // response + // // spawn a task to clean up the garbage + // } + // _ => todo!("this message failed"), + // } + // }) .build()?; event_loop.run(move |event, _, control_flow| { @@ -194,33 +207,17 @@ impl WebviewRenderer { } _ => {} }, + Event::MainEventsCleared => { webview.resize(); // window.request_redraw(); } - _ => {} // Event::WindowEvent { event, .. } => { - // // - // match event { - // WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, - // _ => {} - // } - // } - // _ => { - // // let _ = webview.resize(); - // } + _ => {} } }); } - /// Create a new text-renderer instance from a functional component root. - /// Automatically progresses the creation of the VNode tree to completion. - /// - /// A VDom is automatically created. If you want more granular control of the VDom, use `from_vdom` - // pub fn new(root: FC, builder: impl FnOnce() -> WVResult>) -> Self { - // Self { root } - // } - /// Create a new text renderer from an existing Virtual DOM. /// This will progress the existing VDom's events to completion. pub fn from_vdom() -> Self { @@ -237,47 +234,3 @@ impl WebviewRenderer { todo!() } } - -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -// use crate::dom::WebviewRegistry; - -#[derive(Debug, Serialize, Deserialize)] -struct MessageParameters { - message: String, -} - -fn HANDLER(window: &Window, mut req: RpcRequest) -> Option { - let mut response = None; - if &req.method == "fullscreen" { - if let Some(params) = req.params.take() { - if let Ok(mut args) = serde_json::from_value::>(params) { - if !args.is_empty() { - if args.swap_remove(0) { - window.set_fullscreen(Some(Fullscreen::Borderless(None))); - } else { - window.set_fullscreen(None); - } - }; - response = Some(RpcResponse::new_result(req.id.take(), None)); - } - } - } else if &req.method == "send-parameters" { - if let Some(params) = req.params.take() { - if let Ok(mut args) = serde_json::from_value::>(params) { - let result = if !args.is_empty() { - let msg = args.swap_remove(0); - Some(Value::String(format!("Hello, {}!", msg.message))) - } else { - // NOTE: in the real-world we should send an error response here! - None - }; - // Must always send a response as this is a `call()` - response = Some(RpcResponse::new_result(req.id.take(), result)); - } - } - } - - response -} diff --git a/packages/mobile/src/dom.rs b/packages/mobile/src/dom.rs index cea4f530d..c9e457479 100644 --- a/packages/mobile/src/dom.rs +++ b/packages/mobile/src/dom.rs @@ -30,7 +30,7 @@ impl WebviewDom<'_> { self.registry } } -impl<'bump> RealDom<'bump> for WebviewDom<'bump> { +impl<'bump> RealDom for WebviewDom<'bump> { fn raw_node_as_any(&self) -> &mut dyn std::any::Any { todo!() // self.edits.push(PushRoot { root }); diff --git a/packages/mobile/src/lib.rs b/packages/mobile/src/lib.rs index 9cd24460a..1ee4bc652 100644 --- a/packages/mobile/src/lib.rs +++ b/packages/mobile/src/lib.rs @@ -118,7 +118,7 @@ impl WebviewRenderer { // Serialize the edit stream let edits = { let mut edits = Vec::new(); - lock.rebuild(&mut real, &mut edits).unwrap(); + lock.rebuild(&mut edits).unwrap(); serde_json::to_value(edits).unwrap() }; @@ -140,7 +140,7 @@ impl WebviewRenderer { // Serialize the edit stream let edits = { let mut edits = Vec::new(); - lock.rebuild(&mut real, &mut edits).unwrap(); + lock.rebuild(&mut edits).unwrap(); serde_json::to_value(edits).unwrap() }; diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 3b25700dd..10300c023 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -33,6 +33,7 @@ features = [ "Attr", "Document", "Element", + "CssStyleDeclaration", "HtmlElement", "HtmlInputElement", "HtmlSelectElement", diff --git a/packages/web/src/cfg.rs b/packages/web/src/cfg.rs index 002dc3535..376e466c9 100644 --- a/packages/web/src/cfg.rs +++ b/packages/web/src/cfg.rs @@ -1,9 +1,13 @@ pub struct WebConfig { pub(crate) hydrate: bool, + pub(crate) rootname: String, } impl Default for WebConfig { fn default() -> Self { - Self { hydrate: false } + Self { + hydrate: false, + rootname: "dioxusroot".to_string(), + } } } impl WebConfig { @@ -18,4 +22,9 @@ impl WebConfig { self.hydrate = f; self } + + pub fn rootname(mut self, name: impl Into) -> Self { + self.rootname = name.into(); + self + } } diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 5223fdc36..37bbfc50c 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -7,8 +7,8 @@ use dioxus_core::{ use fxhash::FxHashMap; use wasm_bindgen::{closure::Closure, JsCast}; use web_sys::{ - window, Document, Element, Event, HtmlElement, HtmlInputElement, HtmlOptionElement, Node, - NodeList, + window, CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement, + HtmlOptionElement, Node, NodeList, }; use crate::{nodeslab::NodeSlab, WebConfig}; @@ -23,9 +23,7 @@ pub struct WebsysDom { root: Element, - event_receiver: async_channel::Receiver, - - trigger: Arc, + sender_callback: Rc, // map of listener types to number of those listeners // This is roughly a delegater @@ -40,19 +38,9 @@ pub struct WebsysDom { last_node_was_text: bool, } impl WebsysDom { - pub fn new(root: Element, cfg: WebConfig) -> Self { + pub fn new(root: Element, cfg: WebConfig, sender_callback: Rc) -> Self { let document = load_document(); - let (sender, receiver) = async_channel::unbounded::(); - - let sender_callback = Arc::new(move |ev| { - let c = sender.clone(); - wasm_bindgen_futures::spawn_local(async move { - log::debug!("sending event through channel"); - c.send(ev).await.unwrap(); - }); - }); - let mut nodes = NodeSlab::new(2000); let mut listeners = FxHashMap::default(); @@ -67,12 +55,11 @@ impl WebsysDom { let el: &Element = node.dyn_ref::().unwrap(); let id: String = el.get_attribute("dio_el").unwrap(); let id = id.parse::().unwrap(); - - // this autoresizes the vector if needed nodes[id] = Some(node); } // Load all the event listeners into our listener register + // TODO } let mut stack = Stack::with_capacity(10); @@ -84,18 +71,12 @@ impl WebsysDom { nodes, listeners, document, - event_receiver: receiver, - trigger: sender_callback, + sender_callback, root, last_node_was_text: false, } } - pub async fn wait_for_event(&mut self) -> Option { - let v = self.event_receiver.recv().await.unwrap(); - Some(v) - } - pub fn process_edits(&mut self, edits: &mut Vec) { for edit in edits.drain(..) { log::info!("Handling edit: {:#?}", edit); @@ -310,7 +291,7 @@ impl WebsysDom { if let Some(entry) = self.listeners.get_mut(event) { entry.0 += 1; } else { - let trigger = self.trigger.clone(); + let trigger = self.sender_callback.clone(); let handler = Closure::wrap(Box::new(move |event: &web_sys::Event| { // "Result" cannot be received from JS @@ -339,25 +320,21 @@ impl WebsysDom { } fn set_attribute(&mut self, name: &str, value: &str, ns: Option<&str>) { - if name == "class" { + if let Some(el) = self.stack.top().dyn_ref::() { match ns { - Some("http://www.w3.org/2000/svg") => { - // - if let Some(el) = self.stack.top().dyn_ref::() { - let r: web_sys::SvgAnimatedString = el.class_name(); - r.set_base_val(value); - // el.set_class_name(value); - } - } - _ => { - if let Some(el) = self.stack.top().dyn_ref::() { - el.set_class_name(value); - } + // inline style support + Some("style") => { + let el = el.dyn_ref::().unwrap(); + let style_dc: CssStyleDeclaration = el.style(); + style_dc.set_property(name, value).unwrap(); } + _ => el.set_attribute(name, value).unwrap(), } - } else { - if let Some(el) = self.stack.top().dyn_ref::() { - el.set_attribute(name, value).unwrap(); + match name { + "value" => {} + "checked" => {} + "selected" => {} + _ => {} } } } @@ -418,7 +395,7 @@ impl WebsysDom { } } -impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom { +impl<'a> dioxus_core::diff::RealDom for WebsysDom { // fn request_available_node(&mut self) -> ElementId { // let key = self.nodes.insert(None); // log::debug!("making new key: {:#?}", key); diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index d70ac567f..d7cf7b653 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -2,6 +2,8 @@ //! -------------- //! This crate implements a renderer of the Dioxus Virtual DOM for the web browser using Websys. +use std::rc::Rc; + pub use crate::cfg::WebConfig; use crate::dom::load_document; use dioxus::prelude::{Context, Properties, VNode}; @@ -77,50 +79,25 @@ pub async fn run_with_props( ) -> Result<()> { let mut dom = VirtualDom::new_with_props(root, root_props); - // let tasks = dom.shared.tasks.clone(); + let root_el = load_document().get_element_by_id(&cfg.rootname).unwrap(); - let root_el = load_document().get_element_by_id("dioxusroot").unwrap(); - let mut websys_dom = dom::WebsysDom::new(root_el, cfg); + let tasks = dom.get_event_sender(); - let mut edits = Vec::new(); - dom.rebuild(&mut websys_dom, &mut edits)?; - websys_dom.process_edits(&mut edits); + let mut real = RealDomWebsys {}; - log::info!("Going into event loop"); - - // #[allow(unreachable_code)] - loop { - let trigger = { - let real_queue = websys_dom.wait_for_event(); - if dom.any_pending_events() { - log::info!("tasks is not empty, waiting for either tasks or event system"); - let mut task = dom.wait_for_event(); - - pin_mut!(real_queue); - pin_mut!(task); - - match futures_util::future::select(real_queue, task).await { - futures_util::future::Either::Left((trigger, _)) => trigger, - futures_util::future::Either::Right((trigger, _)) => trigger, - } - } else { - log::info!("tasks is empty, waiting for dom event to trigger soemthing"); - real_queue.await - } - }; - - if let Some(real_trigger) = trigger { - log::info!("event received"); - - dom.queue_event(real_trigger); - - let mut edits = Vec::new(); - dom.progress_with_event(&mut websys_dom, &mut edits).await?; - websys_dom.process_edits(&mut edits); - } + // initialize the virtualdom first + if cfg.hydrate { + dom.rebuild_in_place()?; } - // should actually never return from this, should be an error, rustc just cant see it + let mut websys_dom = dom::WebsysDom::new( + root_el, + cfg, + Rc::new(move |event| tasks.unbounded_send(event).unwrap()), + ); + + dom.run(&mut websys_dom).await?; + Ok(()) } @@ -128,3 +105,10 @@ struct HydrationNode { id: usize, node: Node, } + +struct RealDomWebsys {} +impl dioxus::RealDom for RealDomWebsys { + fn raw_node_as_any(&self) -> &mut dyn std::any::Any { + todo!() + } +} From fac42339c272b0e430ebf4f31b6061a0635d3e19 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 8 Aug 2021 15:15:16 -0400 Subject: [PATCH 20/21] feat: mutations --- packages/core-macro/examples/prop_test.rs | 1 + packages/core-macro/src/props/mod.rs | 6 + packages/core/examples/borrowed.rs | 1 + packages/core/src/arena.rs | 19 +- packages/core/src/component.rs | 2 +- packages/core/src/diff.rs | 103 +++--- packages/core/src/events.rs | 22 +- packages/core/src/hooks.rs | 4 +- packages/core/src/lib.rs | 4 +- packages/core/src/nodes.rs | 35 +- packages/core/src/scope.rs | 68 ++-- packages/core/src/util.rs | 13 - packages/core/src/virtual_dom.rs | 392 +++++++++++++++------- packages/core/tests/diffing.rs | 1 - packages/core/tests/eventsystem.rs | 32 ++ packages/desktop/src/lib.rs | 34 +- packages/web/src/lib.rs | 2 +- 17 files changed, 464 insertions(+), 275 deletions(-) create mode 100644 packages/core/tests/eventsystem.rs diff --git a/packages/core-macro/examples/prop_test.rs b/packages/core-macro/examples/prop_test.rs index 9a38fd604..d80c0cc35 100644 --- a/packages/core-macro/examples/prop_test.rs +++ b/packages/core-macro/examples/prop_test.rs @@ -4,6 +4,7 @@ pub mod dioxus { pub mod prelude { pub trait Properties { type Builder; + const IS_STATIC: bool; fn builder() -> Self::Builder; unsafe fn memoize(&self, other: &Self) -> bool; } diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index f91eebbda..7088f1da2 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -662,6 +662,11 @@ Finally, call `.build()` to create the instance of `{name}`. false => quote! { self == other }, }; + let is_static = match are_there_generics { + true => quote! { false }, + false => quote! { true }, + }; + Ok(quote! { impl #impl_generics #name #ty_generics #where_clause { #[doc = #builder_method_doc] @@ -693,6 +698,7 @@ Finally, call `.build()` to create the instance of `{name}`. impl #impl_generics dioxus::prelude::Properties for #name #ty_generics{ type Builder = #builder_name #generics_with_empty; + const IS_STATIC: bool = #is_static; fn builder() -> Self::Builder { #name::builder() } diff --git a/packages/core/examples/borrowed.rs b/packages/core/examples/borrowed.rs index 7959e0f44..e30447548 100644 --- a/packages/core/examples/borrowed.rs +++ b/packages/core/examples/borrowed.rs @@ -67,6 +67,7 @@ impl PartialEq for ChildProps { } impl Properties for ChildProps { type Builder = (); + const IS_STATIC: bool = true; fn builder() -> Self::Builder { () } diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 1759aeebc..44c4b34a8 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -66,8 +66,6 @@ impl SharedResources { // elements are super cheap - the value takes no space let raw_elements = Slab::with_capacity(2000); - // let pending_events = Rc::new(RefCell::new(Vec::new())); - let (sender, receiver) = futures_channel::mpsc::unbounded(); let heuristics = HeuristicsEngine::new(); @@ -106,8 +104,8 @@ impl SharedResources { } /// this is unsafe because the caller needs to track which other scopes it's already using - pub unsafe fn get_scope(&self, idx: ScopeId) -> Option<&Scope> { - let inner = &*self.components.get(); + pub fn get_scope(&self, idx: ScopeId) -> Option<&Scope> { + let inner = unsafe { &*self.components.get() }; inner.get(idx.0) } @@ -167,6 +165,19 @@ impl SharedResources { self.async_tasks.borrow_mut().push(task); TaskHandle {} } + + pub fn make_trigger_key(&self, trigger: &EventTrigger) -> EventKey { + let height = self + .get_scope(trigger.originator) + .map(|f| f.height) + .unwrap(); + + EventKey { + height, + originator: trigger.originator, + priority: trigger.priority, + } + } } pub struct TaskHandle {} diff --git a/packages/core/src/component.rs b/packages/core/src/component.rs index 754fa5498..ee25bd5bd 100644 --- a/packages/core/src/component.rs +++ b/packages/core/src/component.rs @@ -41,7 +41,7 @@ use crate::innerlude::{Context, DomTree, LazyNodes, FC}; /// ``` pub trait Properties: Sized { type Builder; - const IS_STATIC: bool = false; + const IS_STATIC: bool; fn builder() -> Self::Builder; /// Memoization can only happen if the props are 'static diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 475bc530e..e3a8003bf 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -69,43 +69,13 @@ use futures_util::Future; use fxhash::{FxBuildHasher, FxHashMap, FxHashSet}; use smallvec::{smallvec, SmallVec}; -use std::{any::Any, cell::Cell, cmp::Ordering, pin::Pin}; +use std::{any::Any, cell::Cell, cmp::Ordering, marker::PhantomData, pin::Pin}; use DomEdit::*; -/// Instead of having handles directly over nodes, Dioxus uses simple u32 as node IDs. -/// The expectation is that the underlying renderer will mainain their Nodes in vec where the ids are the index. This allows -/// for a form of passive garbage collection where nodes aren't immedately cleaned up. -/// -/// The "RealDom" abstracts over the... real dom. The RealDom trait assumes that the renderer maintains a stack of real -/// nodes as the diffing algorithm descenes through the tree. This means that whatever is on top of the stack will receive -/// any modifications that follow. This technique enables the diffing algorithm to avoid directly handling or storing any -/// target-specific Node type as well as easily serializing the edits to be sent over a network or IPC connection. -pub trait RealDom { - fn raw_node_as_any(&self) -> &mut dyn Any; - - /// Essentially "are we out of time to do more work?" - /// Right now, defaults to "no" giving us unlimited time to process work. - /// This will lead to blocking behavior in the UI, so implementors will want to override this. - fn must_commit(&self) -> bool { - false - } - - fn commit_edits<'a>(&mut self, edits: &mut Vec>) {} - - // pause the virtualdom until the main loop is ready to process more work - fn wait_until_ready<'s>(&'s mut self) -> Pin + 's>> { - // - Box::pin(async { - // - }) - } -} - pub struct DiffMachine<'r, 'bump> { - // pub real_dom: &'real dyn RealDom, pub vdom: &'bump SharedResources, - pub edits: &'r mut Vec>, + pub edits: Mutations<'bump>, pub scope_stack: SmallVec<[ScopeId; 5]>, @@ -114,11 +84,13 @@ pub struct DiffMachine<'r, 'bump> { // will be used later for garbage collection // we check every seen node and then schedule its eventual deletion pub seen_scopes: FxHashSet, + + _r: PhantomData<&'r ()>, } impl<'r, 'bump> DiffMachine<'r, 'bump> { pub(crate) fn new( - edits: &'r mut Vec>, + edits: Mutations<'bump>, cur_scope: ScopeId, shared: &'bump SharedResources, ) -> Self { @@ -128,6 +100,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { vdom: shared, diffed: FxHashSet::default(), seen_scopes: FxHashSet::default(), + _r: PhantomData, } } @@ -136,15 +109,16 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { /// /// This will PANIC if any component elements are passed in. pub fn new_headless( - edits: &'r mut Vec>, + // edits: &'r mut Vec>, shared: &'bump SharedResources, ) -> Self { Self { - edits, + edits: Mutations { edits: Vec::new() }, scope_stack: smallvec![ScopeId(0)], vdom: shared, diffed: FxHashSet::default(), seen_scopes: FxHashSet::default(), + _r: PhantomData, } } @@ -211,13 +185,13 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { if old.attributes.len() == new.attributes.len() { for (old_attr, new_attr) in old.attributes.iter().zip(new.attributes.iter()) { if old_attr.value != new_attr.value { - please_commit(&mut self.edits); + please_commit(&mut self.edits.edits); self.edit_set_attribute(new_attr); } } } else { // TODO: provide some sort of report on how "good" the diffing was - please_commit(&mut self.edits); + please_commit(&mut self.edits.edits); for attribute in old.attributes { self.edit_remove_attribute(attribute); } @@ -238,7 +212,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { if old.listeners.len() == new.listeners.len() { for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) { if old_l.event != new_l.event { - please_commit(&mut self.edits); + please_commit(&mut self.edits.edits); self.edit_remove_event_listener(old_l.event); self.edit_new_event_listener(new_l, cur_scope); } @@ -246,7 +220,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { self.fix_listener(new_l); } } else { - please_commit(&mut self.edits); + please_commit(&mut self.edits.edits); for listener in old.listeners { self.edit_remove_event_listener(listener.event); } @@ -421,6 +395,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { dom_id.set(Some(real_id)); let cur_scope = self.current_scope().unwrap(); + listeners.iter().for_each(|listener| { self.fix_listener(listener); listener.mounted_node.set(Some(real_id)); @@ -464,7 +439,6 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { } VNodeKind::Component(vcomponent) => { - log::debug!("Mounting a new component"); let caller = vcomponent.caller.clone(); let parent_idx = self.scope_stack.last().unwrap().clone(); @@ -486,9 +460,17 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { // Actually initialize the caller's slot with the right address vcomponent.ass_scope.set(Some(new_idx)); + if !vcomponent.can_memoize { + let cur_scope = self.get_scope_mut(&parent_idx).unwrap(); + let extended = *vcomponent as *const VComponent; + let extended: *const VComponent<'static> = + unsafe { std::mem::transmute(extended) }; + cur_scope.borrowed_props.borrow_mut().push(extended); + } + // TODO: - // Noderefs - // Effects + // add noderefs to current noderef list Noderefs + // add effects to current effect list Effects let new_component = self.get_scope_mut(&new_idx).unwrap(); @@ -501,9 +483,6 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { // failed to run. this is the first time the component ran, and it failed // we manually set its head node to an empty fragment panic!("failing components not yet implemented"); - - // new_component.frames.head - // self.frames.wip_frame_mut().head_node = unsafe { std::mem::transmute(new_head) }; } } @@ -1367,42 +1346,42 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { // Navigation pub(crate) fn edit_push_root(&mut self, root: ElementId) { let id = root.as_u64(); - self.edits.push(PushRoot { id }); + self.edits.edits.push(PushRoot { id }); } pub(crate) fn edit_pop(&mut self) { - self.edits.push(PopRoot {}); + self.edits.edits.push(PopRoot {}); } // Add Nodes to the dom // add m nodes from the stack pub(crate) fn edit_append_children(&mut self, many: u32) { - self.edits.push(AppendChildren { many }); + self.edits.edits.push(AppendChildren { many }); } // replace the n-m node on the stack with the m nodes // ends with the last element of the chain on the top of the stack pub(crate) fn edit_replace_with(&mut self, n: u32, m: u32) { - self.edits.push(ReplaceWith { n, m }); + self.edits.edits.push(ReplaceWith { n, m }); } pub(crate) fn edit_insert_after(&mut self, n: u32) { - self.edits.push(InsertAfter { n }); + self.edits.edits.push(InsertAfter { n }); } pub(crate) fn edit_insert_before(&mut self, n: u32) { - self.edits.push(InsertBefore { n }); + self.edits.edits.push(InsertBefore { n }); } // Remove Nodesfrom the dom pub(crate) fn edit_remove(&mut self) { - self.edits.push(Remove); + self.edits.edits.push(Remove); } // Create pub(crate) fn edit_create_text_node(&mut self, text: &'bump str, id: ElementId) { let id = id.as_u64(); - self.edits.push(CreateTextNode { text, id }); + self.edits.edits.push(CreateTextNode { text, id }); } pub(crate) fn edit_create_element( @@ -1413,15 +1392,15 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { ) { let id = id.as_u64(); match ns { - Some(ns) => self.edits.push(CreateElementNs { id, ns, tag }), - None => self.edits.push(CreateElement { id, tag }), + Some(ns) => self.edits.edits.push(CreateElementNs { id, ns, tag }), + None => self.edits.edits.push(CreateElement { id, tag }), } } // placeholders are nodes that don't get rendered but still exist as an "anchor" in the real dom pub(crate) fn edit_create_placeholder(&mut self, id: ElementId) { let id = id.as_u64(); - self.edits.push(CreatePlaceholder { id }); + self.edits.edits.push(CreatePlaceholder { id }); } // events @@ -1434,7 +1413,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { let element_id = mounted_node.get().unwrap().as_u64(); - self.edits.push(NewEventListener { + self.edits.edits.push(NewEventListener { scope, event_name: event, mounted_node_id: element_id, @@ -1442,12 +1421,12 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { } pub(crate) fn edit_remove_event_listener(&mut self, event: &'static str) { - self.edits.push(RemoveEventListener { event }); + self.edits.edits.push(RemoveEventListener { event }); } // modify pub(crate) fn edit_set_text(&mut self, text: &'bump str) { - self.edits.push(SetText { text }); + self.edits.edits.push(SetText { text }); } pub(crate) fn edit_set_attribute(&mut self, attribute: &'bump Attribute) { @@ -1461,7 +1440,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { // field: &'static str, // value: &'bump str, // ns: Option<&'static str>, - self.edits.push(SetAttribute { + self.edits.edits.push(SetAttribute { field: name, value, ns: *namespace, @@ -1484,7 +1463,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { // field: &'static str, // value: &'bump str, // ns: Option<&'static str>, - self.edits.push(SetAttribute { + self.edits.edits.push(SetAttribute { field: name, value, ns: Some(namespace), @@ -1493,7 +1472,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { pub(crate) fn edit_remove_attribute(&mut self, attribute: &Attribute) { let name = attribute.name; - self.edits.push(RemoveAttribute { name }); + self.edits.edits.push(RemoveAttribute { name }); } } diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 17ec98d81..6804a9f95 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -28,21 +28,15 @@ pub struct EventTrigger { /// The priority of the event pub priority: EventPriority, } -impl EventTrigger { - pub fn make_key(&self) -> EventKey { - EventKey { - originator: self.originator, - priority: self.priority, - } - } -} -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Clone, Copy)] pub struct EventKey { /// The originator of the event trigger pub originator: ScopeId, /// The priority of the event pub priority: EventPriority, + /// The height of the scope (used for ordering) + pub height: u32, // TODO: add the time that the event was queued } @@ -117,9 +111,6 @@ pub enum VirtualEvent { /// be processed. GarbageCollection, - /// - DiffComponent, - /// A type of "immediate" event scheduled by components ScheduledUpdate { height: u32, @@ -135,9 +126,11 @@ pub enum VirtualEvent { // This type exists for the task/concurrency system to signal that a task is ready. // However, this does not necessarily signal that a scope must be re-ran, so the hook implementation must cause its // own re-run. - AsyncEvent {}, + AsyncEvent { + should_rerender: bool, + }, - // Suspense events are a type of async event + // Suspense events are a type of async event generated when suspended nodes are ready to be processed. // // they have the lowest priority SuspenseEvent { @@ -188,7 +181,6 @@ impl std::fmt::Debug for VirtualEvent { VirtualEvent::ScheduledUpdate { .. } => "SetStateEvent", VirtualEvent::AsyncEvent { .. } => "AsyncEvent", VirtualEvent::SuspenseEvent { .. } => "SuspenseEvent", - VirtualEvent::DiffComponent { .. } => "DiffComponent", }; f.debug_struct("VirtualEvent").field("type", &name).finish() diff --git a/packages/core/src/hooks.rs b/packages/core/src/hooks.rs index 4fef6aa1c..538e2ae18 100644 --- a/packages/core/src/hooks.rs +++ b/packages/core/src/hooks.rs @@ -127,7 +127,9 @@ where *slot.as_ref().borrow_mut() = Some(output); updater(update_id); EventTrigger { - event: VirtualEvent::AsyncEvent {}, + event: VirtualEvent::AsyncEvent { + should_rerender: false, + }, originator, priority: EventPriority::Low, real_node_id: None, diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 34506c4e0..c627496b2 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -12,8 +12,8 @@ pub use crate::innerlude::{ format_args_f, html, rsx, Context, DioxusElement, DomEdit, DomTree, ElementId, EventPriority, - EventTrigger, LazyNodes, NodeFactory, Properties, RealDom, ScopeId, SuspendedContext, VNode, - VNodeKind, VirtualDom, VirtualEvent, FC, + EventTrigger, LazyNodes, NodeFactory, Properties, ScopeId, SuspendedContext, VNode, VNodeKind, + VirtualDom, VirtualEvent, FC, }; pub mod prelude { diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 49958d905..fae6c31d4 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -152,15 +152,16 @@ pub struct VComponent<'src> { pub(crate) comparator: Option<&'src dyn Fn(&VComponent) -> bool>, - pub(crate) drop_props: Option<&'src dyn FnOnce()>, + pub(crate) drop_props: RefCell>>, pub is_static: bool, + pub can_memoize: bool, + // a pointer into the bump arena (given by the 'src lifetime) pub(crate) raw_props: *const (), // a pointer to the raw fn typ - // pub(crate) user_fc: BumpB\, pub(crate) user_fc: *const (), } @@ -365,20 +366,19 @@ impl<'a> NodeFactory<'a> { P: Properties + 'a, V: 'a + AsRef<[VNode<'a>]>, { - // TODO - // It's somewhat wrong to go about props like this + let bump = self.bump(); // We don't want the fat part of the fat pointer // This function does static dispatch so we don't need any VTable stuff - let children: &'a V = self.bump().alloc(children); + let children: &'a V = bump.alloc(children); let children = children.as_ref(); - let props = self.bump().alloc(props); + let props = bump.alloc(props); let raw_props = props as *mut P as *mut (); let user_fc = component as *const (); - let comparator: Option<&dyn Fn(&VComponent) -> bool> = Some(self.bump().alloc_with(|| { + let comparator: Option<&dyn Fn(&VComponent) -> bool> = Some(bump.alloc_with(|| { move |other: &VComponent| { if user_fc == other.user_fc { // Safety @@ -404,13 +404,19 @@ impl<'a> NodeFactory<'a> { })); // create a closure to drop the props - let drop_props: Option<&dyn FnOnce()> = Some(self.bump().alloc_with(|| { + let mut has_dropped = false; + let drop_props: &mut dyn FnMut() = bump.alloc_with(|| { move || unsafe { - let real_other = raw_props as *mut _ as *mut P; - let b = BumpBox::from_raw(real_other); - std::mem::drop(b); + if !has_dropped { + let real_other = raw_props as *mut _ as *mut P; + let b = BumpBox::from_raw(real_other); + std::mem::drop(b); + + has_dropped = true; + } } - })); + }); + let drop_props = unsafe { BumpBox::from_raw(drop_props) }; let is_static = children.len() == 0 && P::IS_STATIC && key.is_none(); @@ -418,14 +424,15 @@ impl<'a> NodeFactory<'a> { VNode { key, - kind: VNodeKind::Component(self.bump().alloc_with(|| VComponent { + kind: VNodeKind::Component(bump.alloc_with(|| VComponent { user_fc, comparator, raw_props, children, caller: NodeFactory::create_component_caller(component, raw_props), is_static, - drop_props, + drop_props: RefCell::new(Some(drop_props)), + can_memoize: P::IS_STATIC, ass_scope: Cell::new(None), })), } diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index b303170f3..951b7280b 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -37,6 +37,7 @@ pub struct Scope { // Listeners pub(crate) listeners: RefCell>>, + pub(crate) borrowed_props: RefCell>>, // State pub(crate) hooks: HookList, @@ -94,6 +95,7 @@ impl Scope { hooks: Default::default(), shared_contexts: Default::default(), listeners: Default::default(), + borrowed_props: Default::default(), descendents: Default::default(), pending_garbage: Default::default(), } @@ -114,27 +116,8 @@ impl Scope { // 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 - if !self.pending_garbage.borrow().is_empty() { - // We have some garbage to clean up - log::error!("Cleaning up garabge"); - panic!("cannot run scope while garbage is pending! Please clean up your mess first"); - } - log::debug!("reset okay"); - - // make sure we call the drop implementation on all the listeners - // this is important to not leak memory - self.listeners - .borrow_mut() - .drain(..) - .map(|li| unsafe { &*li }) - .for_each(|listener| { - listener.callback.borrow_mut().take(); - }); - - // make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we - // run the hooks (which hold an &mut Referrence) - // right now, we don't drop + self.ensure_drop_safety(); // Safety: // - We dropped the listeners, so no more &mut T can be used while these are held @@ -164,6 +147,51 @@ impl Scope { } } + /// This method cleans up any references to data held within our hook list. This prevents mutable aliasing from + /// causuing UB in our tree. + /// + /// This works by cleaning up our references from the bottom of the tree to the top. The directed graph of components + /// essentially forms a dependency tree that we can traverse from the bottom to the top. As we traverse, we remove + /// any possible references to the data in the hook list. + /// + /// Refrences to hook data can only be stored in listeners and component props. During diffing, we make sure to log + /// all listeners and borrowed props so we can clear them here. + fn ensure_drop_safety(&mut self) { + // make sure all garabge is collected before trying to proceed with anything else + debug_assert!( + self.pending_garbage.borrow().is_empty(), + "clean up your garabge please" + ); + + // make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we + // run the hooks (which hold an &mut Referrence) + // right now, we don't drop + let vdom = &self.vdom; + self.borrowed_props + .get_mut() + .drain(..) + .map(|li| unsafe { &*li }) + .for_each(|comp| { + // First drop the component's undropped references + let scope_id = comp.ass_scope.get().unwrap(); + let scope = unsafe { vdom.get_scope_mut(scope_id) }.unwrap(); + scope.ensure_drop_safety(); + + // Now, drop our own reference + let mut dropper = comp.drop_props.borrow_mut().take().unwrap(); + dropper(); + }); + + // Now that all the references are gone, we can safely drop our own references in our listeners. + self.listeners + .get_mut() + .drain(..) + .map(|li| unsafe { &*li }) + .for_each(|listener| { + listener.callback.borrow_mut().take(); + }); + } + // A safe wrapper around calling listeners // calling listeners will invalidate the list of listeners // The listener list will be completely drained because the next frame will write over previous listeners diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs index 33a6d9fb2..0add0f456 100644 --- a/packages/core/src/util.rs +++ b/packages/core/src/util.rs @@ -26,16 +26,3 @@ pub fn empty_cell() -> Cell> { // Some(self.cmp(other)) // } // } - -pub struct DebugDom {} -impl DebugDom { - pub fn new() -> Self { - Self {} - } -} - -impl RealDom for DebugDom { - fn raw_node_as_any(&self) -> &mut dyn std::any::Any { - todo!() - } -} diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 335b5f8c1..090282728 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -29,7 +29,7 @@ use std::any::Any; use std::any::TypeId; use std::cell::{Ref, RefCell, RefMut}; -use std::collections::{BTreeMap, BTreeSet, BinaryHeap}; +use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashSet}; use std::pin::Pin; /// An integrated virtual node system that progresses events and diffs UI trees. @@ -53,6 +53,8 @@ pub struct VirtualDom { /// Should always be the first (gen=0, id=0) pub base_scope: ScopeId, + active_fibers: Vec>, + pending_events: BTreeMap, // for managing the props that were used to create the dom @@ -144,6 +146,7 @@ impl VirtualDom { base_scope, _root_props: root_props, shared: components, + active_fibers: Vec::new(), pending_events: BTreeMap::new(), _root_prop_type: TypeId::of::

(), } @@ -195,8 +198,10 @@ impl VirtualDom { /// Events like garabge collection, application of refs, etc are not handled by this method and can only be progressed /// through "run" /// - pub fn rebuild<'s>(&'s mut self, edits: &'_ mut Vec>) -> Result<()> { - let mut diff_machine = DiffMachine::new(edits, self.base_scope, &self.shared); + pub fn rebuild<'s>(&'s mut self) -> Result>> { + let mut edits = Vec::new(); + let mutations = Mutations { edits: Vec::new() }; + let mut diff_machine = DiffMachine::new(mutations, self.base_scope, &self.shared); let cur_component = diff_machine .get_scope_mut(&self.base_scope) @@ -214,7 +219,7 @@ impl VirtualDom { ); } - Ok(()) + Ok(edits) } async fn select_next_event(&mut self) -> Option { @@ -222,8 +227,9 @@ impl VirtualDom { // drain the in-flight events so that we can sort them out with the current events while let Ok(Some(trigger)) = receiver.try_next() { - log::info!("scooping event from receiver"); - self.pending_events.insert(trigger.make_key(), trigger); + log::info!("retrieving event from receiver"); + let key = self.shared.make_trigger_key(&trigger); + self.pending_events.insert(key, trigger); } if self.pending_events.is_empty() { @@ -242,83 +248,172 @@ impl VirtualDom { futures_util::future::Either::Right((trigger, _)) => trigger, } .unwrap(); - self.pending_events.insert(trigger.make_key(), trigger); + let key = self.shared.make_trigger_key(&trigger); + self.pending_events.insert(key, trigger); } - todo!() + // pop the most important event off + let key = self.pending_events.keys().next().unwrap().clone(); + let trigger = self.pending_events.remove(&key).unwrap(); - // let trigger = self.select_next_event().unwrap(); - // trigger + Some(trigger) } - // the cooperartive, fiber-based scheduler - // is "awaited" and will always return some edits for the real dom to apply - // - // Right now, we'll happily partially render component trees - // - // Normally, you would descend through the tree depth-first, but we actually descend breadth-first. - pub async fn run(&mut self, realdom: &mut dyn RealDom) -> Result<()> { - let cur_component = self.base_scope; - let mut edits = Vec::new(); - let resources = self.shared.clone(); + /// Runs the virtualdom immediately, not waiting for any suspended nodes to complete. + /// + /// This method will not wait for any suspended tasks, completely skipping over + pub fn run_immediate<'s>(&'s mut self) -> Result> { + // - let mut diff_machine = DiffMachine::new(&mut edits, cur_component, &resources); + todo!() + } + + /// Runs the virtualdom with no time limit. + /// + /// If there are pending tasks, they will be progressed before returning. This is useful when rendering an application + /// that has suspended nodes or suspended tasks. Be warned - any async tasks running forever will prevent this method + /// from completing. Consider using `run` and specifing a deadline. + pub async fn run_unbounded<'s>(&'s mut self) -> Result> { + self.run_with_deadline(|| false).await + } + + /// Run the virtualdom with a time limit. + /// + /// This method will progress async tasks until the deadline is reached. If tasks are completed before the deadline, + /// and no tasks are pending, this method will return immediately. If tasks are still pending, then this method will + /// exhaust the deadline working on them. + /// + /// This method is useful when needing to schedule the virtualdom around other tasks on the main thread to prevent + /// "jank". It will try to finish whatever work it has by the deadline to free up time for other work. + /// + /// Due to platform differences in how time is handled, this method accepts a closure that must return true when the + /// deadline is exceeded. However, the deadline won't be met precisely, so you might want to build some wiggle room + /// into the deadline closure manually. + /// + /// The deadline is checked before starting to diff components. This strikes a balance between the overhead of checking + /// the deadline and just completing the work. However, if an individual component takes more than 16ms to render, then + /// the screen will "jank" up. In debug, this will trigger an alert. + /// + /// # Example + /// + /// ```no_run + /// let mut dom = VirtualDom::new(|cx| cx.render(rsx!( div {"hello"} ))); + /// loop { + /// let started = std::time::Instant::now(); + /// let deadline = move || std::time::Instant::now() - started > std::time::Duration::from_millis(16); + /// + /// let mutations = dom.run_with_deadline(deadline).await; + /// apply_mutations(mutations); + /// } + /// ``` + pub async fn run_with_deadline<'s>( + &'s mut self, + mut deadline_exceeded: impl FnMut() -> bool, + ) -> Result> { + let cur_component = self.base_scope; + + let mut mutations = Mutations { edits: Vec::new() }; + + let mut diff_machine = DiffMachine::new(mutations, cur_component, &self.shared); + + let must_be_re_rendered = HashSet::::new(); + + let mut receiver = self.shared.task_receiver.borrow_mut(); + + // loop { - let trigger = self.select_next_event().await.unwrap(); + if deadline_exceeded() { + break; + } + + /* + Strategy: + 1. Check if there are any events in the receiver. + 2. If there are, process them and create a new fiber. + 3. If there are no events, then choose a fiber to work on. + 4. If there are no fibers, then wait for the next event from the receiver. + 5. While processing a fiber, periodically check if we're out of time + 6. If we are almost out of time, then commit our edits to the realdom + 7. Whenever a fiber is finished, immediately commit it. (IE so deadlines can be infinite if unsupported) + + + + + The user of this method will loop over "run", waiting for edits and then committing them IE + + + task::spawn_local(async { + let vdom = VirtualDom::new(App); + loop { + let deadline = wait_for_idle().await; + let mutations = vdom.run_with_deadline(deadline); + realdom.apply_edits(mutations.edits); + realdom.apply_refs(mutations.refs); + } + }); + + + let vdom = VirtualDom::new(App); + let realdom = WebsysDom::new(App); + loop { + let deadline = wait_for_idle().await; + let mutations = vdom.run_with_deadline(deadline); + + realdom.apply_edits(mutations.edits); + realdom.apply_refs(mutations.refs); + } + + + + ``` + task::spawn_local(async move { + let vdom = VirtualDom::new(App); + loop { + let mutations = vdom.run_with_deadline(16); + realdom.apply_edits(mutations.edits)?; + realdom.apply_refs(mutations.refs)?; + } + }); + + event_loop.run(move |event, _, flow| { + + }); + ``` + */ + let mut receiver = self.shared.task_receiver.borrow_mut(); + + // match receiver.try_next() {} + + let trigger = receiver.next().await.unwrap(); match &trigger.event { - // Collecting garabge is not currently interruptible. + // If any user event is received, then we run the listener and let it dump "needs updates" into the queue // - // In the future, it could be though - VirtualEvent::GarbageCollection => { - let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); - - let mut garbage_list = scope.consume_garbage(); - - let mut scopes_to_kill = Vec::new(); - while let Some(node) = garbage_list.pop() { - match &node.kind { - VNodeKind::Text(_) => { - self.shared.collect_garbage(node.direct_id()); - } - VNodeKind::Anchor(_) => { - self.shared.collect_garbage(node.direct_id()); - } - VNodeKind::Suspended(_) => { - self.shared.collect_garbage(node.direct_id()); - } - - VNodeKind::Element(el) => { - self.shared.collect_garbage(node.direct_id()); - for child in el.children { - garbage_list.push(child); - } - } - - VNodeKind::Fragment(frag) => { - for child in frag.children { - garbage_list.push(child); - } - } - - VNodeKind::Component(comp) => { - // TODO: run the hook destructors and then even delete the scope - // TODO: allow interruption here - if !realdom.must_commit() { - let scope_id = comp.ass_scope.get().unwrap(); - let scope = self.get_scope(scope_id).unwrap(); - let root = scope.root(); - garbage_list.push(root); - scopes_to_kill.push(scope_id); - } - } + VirtualEvent::ClipboardEvent(_) + | VirtualEvent::CompositionEvent(_) + | VirtualEvent::KeyboardEvent(_) + | VirtualEvent::FocusEvent(_) + | VirtualEvent::FormEvent(_) + | VirtualEvent::SelectionEvent(_) + | VirtualEvent::TouchEvent(_) + | VirtualEvent::UIEvent(_) + | VirtualEvent::WheelEvent(_) + | VirtualEvent::MediaEvent(_) + | VirtualEvent::AnimationEvent(_) + | VirtualEvent::TransitionEvent(_) + | VirtualEvent::ToggleEvent(_) + | VirtualEvent::MouseEvent(_) + | VirtualEvent::PointerEvent(_) => { + let scope_id = &trigger.originator; + let scope = unsafe { self.shared.get_scope_mut(*scope_id) }; + match scope { + Some(scope) => { + scope.call_listener(trigger)?; + } + None => { + log::warn!("No scope found for event: {:#?}", scope_id); } - } - - for scope in scopes_to_kill { - // oy kill em - log::debug!("should be removing scope {:#?}", scope); } } @@ -363,74 +458,93 @@ impl VirtualDom { } } + // Collecting garabge is not currently interruptible. + // + // In the future, it could be though + VirtualEvent::GarbageCollection => { + let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); + + let mut garbage_list = scope.consume_garbage(); + + let mut scopes_to_kill = Vec::new(); + while let Some(node) = garbage_list.pop() { + match &node.kind { + VNodeKind::Text(_) => { + self.shared.collect_garbage(node.direct_id()); + } + VNodeKind::Anchor(_) => { + self.shared.collect_garbage(node.direct_id()); + } + VNodeKind::Suspended(_) => { + self.shared.collect_garbage(node.direct_id()); + } + + VNodeKind::Element(el) => { + self.shared.collect_garbage(node.direct_id()); + for child in el.children { + garbage_list.push(child); + } + } + + VNodeKind::Fragment(frag) => { + for child in frag.children { + garbage_list.push(child); + } + } + + VNodeKind::Component(comp) => { + // TODO: run the hook destructors and then even delete the scope + + let scope_id = comp.ass_scope.get().unwrap(); + let scope = self.get_scope(scope_id).unwrap(); + let root = scope.root(); + garbage_list.push(root); + scopes_to_kill.push(scope_id); + } + } + } + + for scope in scopes_to_kill { + // oy kill em + log::debug!("should be removing scope {:#?}", scope); + } + } + // Run the component VirtualEvent::ScheduledUpdate { height: u32 } => { let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); match scope.run_scope() { Ok(_) => { - let event = VirtualEvent::DiffComponent; - let trigger = EventTrigger { - event, - originator: trigger.originator, - priority: EventPriority::High, - real_node_id: None, - }; - self.shared.task_sender.unbounded_send(trigger); + todo!(); + // let event = VirtualEvent::DiffComponent; + // let trigger = EventTrigger { + // event, + // originator: trigger.originator, + // priority: EventPriority::High, + // real_node_id: None, + // }; + // self.shared.task_sender.unbounded_send(trigger); } Err(_) => { log::error!("failed to run this component!"); } } } - - VirtualEvent::DiffComponent => { - diff_machine.diff_scope(trigger.originator)?; - } - - // Process any user-facing events just by calling their listeners - // This performs no actual work - but the listeners themselves will cause insert new work - VirtualEvent::ClipboardEvent(_) - | VirtualEvent::CompositionEvent(_) - | VirtualEvent::KeyboardEvent(_) - | VirtualEvent::FocusEvent(_) - | VirtualEvent::FormEvent(_) - | VirtualEvent::SelectionEvent(_) - | VirtualEvent::TouchEvent(_) - | VirtualEvent::UIEvent(_) - | VirtualEvent::WheelEvent(_) - | VirtualEvent::MediaEvent(_) - | VirtualEvent::AnimationEvent(_) - | VirtualEvent::TransitionEvent(_) - | VirtualEvent::ToggleEvent(_) - | VirtualEvent::MouseEvent(_) - | VirtualEvent::PointerEvent(_) => { - let scope_id = &trigger.originator; - let scope = unsafe { self.shared.get_scope_mut(*scope_id) }; - match scope { - Some(scope) => scope.call_listener(trigger)?, - None => { - log::warn!("No scope found for event: {:#?}", scope_id); - } - } - } } - if realdom.must_commit() || self.pending_events.is_empty() { - realdom.commit_edits(&mut diff_machine.edits); - realdom.wait_until_ready().await; - } + // // + // if realdom.must_commit() { + // // commit these edits and then wait for the next idle period + // realdom.commit_edits(&mut diff_machine.edits).await; + // } } - Ok(()) + Ok(diff_machine.edits) } pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender { - todo!() - // std::rc::Rc::new(move |_| { - // // - // }) - // todo() + self.shared.task_sender.clone() } } @@ -439,8 +553,38 @@ impl VirtualDom { unsafe impl Sync for VirtualDom {} unsafe impl Send for VirtualDom {} -fn select_next_event( - pending_events: &mut BTreeMap, -) -> Option { - None +struct Fiber<'a> { + trigger: EventTrigger, + + // scopes that haven't been updated yet + pending_scopes: Vec, + + pending_nodes: Vec<*const VNode<'a>>, + + // WIP edits + edits: Vec>, + + started: bool, + + completed: bool, +} + +impl Fiber<'_> { + fn new(trigger: EventTrigger) -> Self { + Self { + trigger, + pending_scopes: Vec::new(), + pending_nodes: Vec::new(), + edits: Vec::new(), + started: false, + completed: false, + } + } +} + +/// The "Mutations" object holds the changes that need to be made to the DOM. +pub struct Mutations<'s> { + // todo: apply node refs + // todo: apply effects + pub edits: Vec>, } diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index 8f7049431..97c794b6e 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -11,7 +11,6 @@ use dioxus::{ arena::SharedResources, diff::{CreateMeta, DiffMachine}, prelude::*, - util::DebugDom, DomEdit, }; use dioxus_core as dioxus; diff --git a/packages/core/tests/eventsystem.rs b/packages/core/tests/eventsystem.rs new file mode 100644 index 000000000..cb593dee7 --- /dev/null +++ b/packages/core/tests/eventsystem.rs @@ -0,0 +1,32 @@ +use bumpalo::Bump; + +use anyhow::{Context, Result}; +use dioxus::{ + arena::SharedResources, + diff::{CreateMeta, DiffMachine}, + prelude::*, + DomEdit, +}; +use dioxus_core as dioxus; +use dioxus_html as dioxus_elements; + +#[async_std::test] +async fn event_queue_works() { + static App: FC<()> = |cx| { + cx.render(rsx! { + div { "hello world" } + }) + }; + + let mut dom = VirtualDom::new(App); + let edits = dom.rebuild().unwrap(); + + async_std::task::spawn_local(async move { + match dom.run_unbounded().await { + Err(_) => todo!(), + Ok(mutations) => { + // + } + } + }); +} diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 108883991..c9c7153b4 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -88,26 +88,26 @@ impl WebviewRenderer { let channel = vir.get_event_sender(); struct WebviewBridge {} - impl RealDom for WebviewBridge { - fn raw_node_as_any(&self) -> &mut dyn std::any::Any { - todo!() - } + // impl RealDom for WebviewBridge { + // fn raw_node_as_any(&self) -> &mut dyn std::any::Any { + // todo!() + // } - fn must_commit(&self) -> bool { - false - } + // fn must_commit(&self) -> bool { + // false + // } - fn commit_edits<'a>(&mut self, edits: &mut Vec>) {} + // fn commit_edits<'a>(&mut self, edits: &mut Vec>) {} - fn wait_until_ready<'s>( - &'s mut self, - ) -> std::pin::Pin + 's>> { - // - Box::pin(async { - // - }) - } - } + // fn wait_until_ready<'s>( + // &'s mut self, + // ) -> std::pin::Pin + 's>> { + // // + // Box::pin(async { + // // + // }) + // } + // } let mut real_dom = WebviewBridge {}; // async_std::task::spawn_local(vir.run(&mut real_dom)); diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index d7cf7b653..aa941cd75 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -96,7 +96,7 @@ pub async fn run_with_props( Rc::new(move |event| tasks.unbounded_send(event).unwrap()), ); - dom.run(&mut websys_dom).await?; + dom.run_with_deadline(&mut websys_dom).await?; Ok(()) } From 1a323835c8c4f667e5744bbf5447776f1dc51fca Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 9 Aug 2021 02:37:11 -0400 Subject: [PATCH 21/21] wip: on collaborative scheduling --- packages/core/src/arena.rs | 4 +- packages/core/src/diff.rs | 5 +- packages/core/src/events.rs | 28 ++++ packages/core/src/virtual_dom.rs | 216 ++++++++++--------------------- packages/web/src/lib.rs | 24 ++++ 5 files changed, 122 insertions(+), 155 deletions(-) diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 44c4b34a8..55c5c31e3 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -110,8 +110,8 @@ impl SharedResources { } /// this is unsafe because the caller needs to track which other scopes it's already using - pub unsafe fn get_scope_mut(&self, idx: ScopeId) -> Option<&mut Scope> { - let inner = &mut *self.components.get(); + pub fn get_scope_mut(&self, idx: ScopeId) -> Option<&mut Scope> { + let inner = unsafe { &mut *self.components.get() }; inner.get_mut(idx.0) } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index e3a8003bf..09c4ea92e 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -108,10 +108,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { /// this is mostly useful for testing /// /// This will PANIC if any component elements are passed in. - pub fn new_headless( - // edits: &'r mut Vec>, - shared: &'bump SharedResources, - ) -> Self { + pub fn new_headless(shared: &'bump SharedResources) -> Self { Self { edits: Mutations { edits: Vec::new() }, scope_stack: smallvec![ScopeId(0)], diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 6804a9f95..3fe04f28f 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -112,6 +112,8 @@ pub enum VirtualEvent { GarbageCollection, /// A type of "immediate" event scheduled by components + /// + /// Usually called through "set_state" ScheduledUpdate { height: u32, }, @@ -158,6 +160,32 @@ pub enum VirtualEvent { MouseEvent(on::MouseEvent), PointerEvent(on::PointerEvent), } +impl VirtualEvent { + pub fn is_input_event(&self) -> bool { + match self { + VirtualEvent::ClipboardEvent(_) + | VirtualEvent::CompositionEvent(_) + | VirtualEvent::KeyboardEvent(_) + | VirtualEvent::FocusEvent(_) + | VirtualEvent::FormEvent(_) + | VirtualEvent::SelectionEvent(_) + | VirtualEvent::TouchEvent(_) + | VirtualEvent::UIEvent(_) + | VirtualEvent::WheelEvent(_) + | VirtualEvent::MediaEvent(_) + | VirtualEvent::AnimationEvent(_) + | VirtualEvent::TransitionEvent(_) + | VirtualEvent::ToggleEvent(_) + | VirtualEvent::MouseEvent(_) + | VirtualEvent::PointerEvent(_) => true, + + VirtualEvent::GarbageCollection + | VirtualEvent::ScheduledUpdate { .. } + | VirtualEvent::AsyncEvent { .. } + | VirtualEvent::SuspenseEvent { .. } => false, + } + } +} impl std::fmt::Debug for VirtualEvent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 090282728..e6f27c6c5 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -55,8 +55,6 @@ pub struct VirtualDom { active_fibers: Vec>, - pending_events: BTreeMap, - // for managing the props that were used to create the dom #[doc(hidden)] _root_prop_type: std::any::TypeId, @@ -147,7 +145,6 @@ impl VirtualDom { _root_props: root_props, shared: components, active_fibers: Vec::new(), - pending_events: BTreeMap::new(), _root_prop_type: TypeId::of::

(), } } @@ -222,42 +219,42 @@ impl VirtualDom { Ok(edits) } - async fn select_next_event(&mut self) -> Option { - let mut receiver = self.shared.task_receiver.borrow_mut(); + // async fn select_next_event(&mut self) -> Option { + // let mut receiver = self.shared.task_receiver.borrow_mut(); - // drain the in-flight events so that we can sort them out with the current events - while let Ok(Some(trigger)) = receiver.try_next() { - log::info!("retrieving event from receiver"); - let key = self.shared.make_trigger_key(&trigger); - self.pending_events.insert(key, trigger); - } + // // drain the in-flight events so that we can sort them out with the current events + // while let Ok(Some(trigger)) = receiver.try_next() { + // log::info!("retrieving event from receiver"); + // let key = self.shared.make_trigger_key(&trigger); + // self.pending_events.insert(key, trigger); + // } - if self.pending_events.is_empty() { - // Continuously poll the future pool and the event receiver for work - let mut tasks = self.shared.async_tasks.borrow_mut(); - let tasks_tasks = tasks.next(); + // if self.pending_events.is_empty() { + // // Continuously poll the future pool and the event receiver for work + // let mut tasks = self.shared.async_tasks.borrow_mut(); + // let tasks_tasks = tasks.next(); - let mut receiver = self.shared.task_receiver.borrow_mut(); - let reciv_task = receiver.next(); + // let mut receiver = self.shared.task_receiver.borrow_mut(); + // let reciv_task = receiver.next(); - futures_util::pin_mut!(tasks_tasks); - futures_util::pin_mut!(reciv_task); + // futures_util::pin_mut!(tasks_tasks); + // futures_util::pin_mut!(reciv_task); - let trigger = match futures_util::future::select(tasks_tasks, reciv_task).await { - futures_util::future::Either::Left((trigger, _)) => trigger, - futures_util::future::Either::Right((trigger, _)) => trigger, - } - .unwrap(); - let key = self.shared.make_trigger_key(&trigger); - self.pending_events.insert(key, trigger); - } + // let trigger = match futures_util::future::select(tasks_tasks, reciv_task).await { + // futures_util::future::Either::Left((trigger, _)) => trigger, + // futures_util::future::Either::Right((trigger, _)) => trigger, + // } + // .unwrap(); + // let key = self.shared.make_trigger_key(&trigger); + // self.pending_events.insert(key, trigger); + // } - // pop the most important event off - let key = self.pending_events.keys().next().unwrap().clone(); - let trigger = self.pending_events.remove(&key).unwrap(); + // // pop the most important event off + // let key = self.pending_events.keys().next().unwrap().clone(); + // let trigger = self.pending_events.remove(&key).unwrap(); - Some(trigger) - } + // Some(trigger) + // } /// Runs the virtualdom immediately, not waiting for any suspended nodes to complete. /// @@ -312,84 +309,28 @@ impl VirtualDom { ) -> Result> { let cur_component = self.base_scope; - let mut mutations = Mutations { edits: Vec::new() }; + let mut diff_machine = + DiffMachine::new(Mutations { edits: Vec::new() }, cur_component, &self.shared); - let mut diff_machine = DiffMachine::new(mutations, cur_component, &self.shared); - - let must_be_re_rendered = HashSet::::new(); + /* + Strategy: + 1. Check if there are any events in the receiver. + 2. If there are, process them and create a new fiber. + 3. If there are no events, then choose a fiber to work on. + 4. If there are no fibers, then wait for the next event from the receiver. + 5. While processing a fiber, periodically check if we're out of time + 6. If we are almost out of time, then commit our edits to the realdom + 7. Whenever a fiber is finished, immediately commit it. (IE so deadlines can be infinite if unsupported) + */ + // 1. Consume any pending events and create new fibers let mut receiver = self.shared.task_receiver.borrow_mut(); - - // - - loop { - if deadline_exceeded() { - break; - } - - /* - Strategy: - 1. Check if there are any events in the receiver. - 2. If there are, process them and create a new fiber. - 3. If there are no events, then choose a fiber to work on. - 4. If there are no fibers, then wait for the next event from the receiver. - 5. While processing a fiber, periodically check if we're out of time - 6. If we are almost out of time, then commit our edits to the realdom - 7. Whenever a fiber is finished, immediately commit it. (IE so deadlines can be infinite if unsupported) - - - - - The user of this method will loop over "run", waiting for edits and then committing them IE - - - task::spawn_local(async { - let vdom = VirtualDom::new(App); - loop { - let deadline = wait_for_idle().await; - let mutations = vdom.run_with_deadline(deadline); - realdom.apply_edits(mutations.edits); - realdom.apply_refs(mutations.refs); - } - }); - - - let vdom = VirtualDom::new(App); - let realdom = WebsysDom::new(App); - loop { - let deadline = wait_for_idle().await; - let mutations = vdom.run_with_deadline(deadline); - - realdom.apply_edits(mutations.edits); - realdom.apply_refs(mutations.refs); - } - - - - ``` - task::spawn_local(async move { - let vdom = VirtualDom::new(App); - loop { - let mutations = vdom.run_with_deadline(16); - realdom.apply_edits(mutations.edits)?; - realdom.apply_refs(mutations.refs)?; - } - }); - - event_loop.run(move |event, _, flow| { - - }); - ``` - */ - let mut receiver = self.shared.task_receiver.borrow_mut(); - - // match receiver.try_next() {} - - let trigger = receiver.next().await.unwrap(); + while let Ok(Some(trigger)) = receiver.try_next() { + // todo: cache the fibers + let mut fiber = Fiber::new(); match &trigger.event { - // If any user event is received, then we run the listener and let it dump "needs updates" into the queue - // + // If any input event is received, then we need to create a new fiber VirtualEvent::ClipboardEvent(_) | VirtualEvent::CompositionEvent(_) | VirtualEvent::KeyboardEvent(_) @@ -405,23 +346,21 @@ impl VirtualDom { | VirtualEvent::ToggleEvent(_) | VirtualEvent::MouseEvent(_) | VirtualEvent::PointerEvent(_) => { - let scope_id = &trigger.originator; - let scope = unsafe { self.shared.get_scope_mut(*scope_id) }; - match scope { - Some(scope) => { - scope.call_listener(trigger)?; - } - None => { - log::warn!("No scope found for event: {:#?}", scope_id); - } + if let Some(scope) = self.shared.get_scope_mut(trigger.originator) { + scope.call_listener(trigger)?; } } VirtualEvent::AsyncEvent { .. } => { - // we want to progress these events - // However, there's nothing we can do for these events, they must generate their own events. + while let Ok(Some(event)) = receiver.try_next() { + fiber.pending_scopes.push(event.originator); + } } + // These shouldn't normally be received, but if they are, it's done because some task set state manually + // Instead of batching the results, + VirtualEvent::ScheduledUpdate { height: u32 } => {} + // Suspense Events! A component's suspended node is updated VirtualEvent::SuspenseEvent { hook_idx, domnode } => { // Safety: this handler is the only thing that can mutate shared items at this moment in tim @@ -438,8 +377,8 @@ impl VirtualDom { match nodes { None => { log::warn!( - "Suspense event came through, but there were no generated nodes >:(." - ); + "Suspense event came through, but there were no generated nodes >:(." + ); } Some(nodes) => { // allocate inside the finished frame - not the WIP frame @@ -509,35 +448,13 @@ impl VirtualDom { log::debug!("should be removing scope {:#?}", scope); } } - - // Run the component - VirtualEvent::ScheduledUpdate { height: u32 } => { - let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); - - match scope.run_scope() { - Ok(_) => { - todo!(); - // let event = VirtualEvent::DiffComponent; - // let trigger = EventTrigger { - // event, - // originator: trigger.originator, - // priority: EventPriority::High, - // real_node_id: None, - // }; - // self.shared.task_sender.unbounded_send(trigger); - } - Err(_) => { - log::error!("failed to run this component!"); - } - } - } } + } - // // - // if realdom.must_commit() { - // // commit these edits and then wait for the next idle period - // realdom.commit_edits(&mut diff_machine.edits).await; - // } + while !deadline_exceeded() { + let mut receiver = self.shared.task_receiver.borrow_mut(); + + // no messages to receive, just work on the fiber } Ok(diff_machine.edits) @@ -546,6 +463,10 @@ impl VirtualDom { pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender { self.shared.task_sender.clone() } + + fn get_scope_mut(&mut self, id: ScopeId) -> Option<&mut Scope> { + unsafe { self.shared.get_scope_mut(id) } + } } // TODO! @@ -554,8 +475,6 @@ unsafe impl Sync for VirtualDom {} unsafe impl Send for VirtualDom {} struct Fiber<'a> { - trigger: EventTrigger, - // scopes that haven't been updated yet pending_scopes: Vec, @@ -570,9 +489,8 @@ struct Fiber<'a> { } impl Fiber<'_> { - fn new(trigger: EventTrigger) -> Self { + fn new() -> Self { Self { - trigger, pending_scopes: Vec::new(), pending_nodes: Vec::new(), edits: Vec::new(), diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index aa941cd75..abf9c7a83 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -2,6 +2,30 @@ //! -------------- //! This crate implements a renderer of the Dioxus Virtual DOM for the web browser using Websys. +/* +From Google's guide on rAF and rIC: +-------- + +If the callback is fired at the end of the frame, it will be scheduled to go after the current frame has been committed, +which means that style changes will have been applied, and, importantly, layout calculated. If we make DOM changes inside + of the idle callback, those layout calculations will be invalidated. If there are any kind of layout reads in the next + frame, e.g. getBoundingClientRect, clientWidth, etc, the browser will have to perform a Forced Synchronous Layout, + which is a potential performance bottleneck. + +Another reason not trigger DOM changes in the idle callback is that the time impact of changing the DOM is unpredictable, +and as such we could easily go past the deadline the browser provided. + +The best practice is to only make DOM changes inside of a requestAnimationFrame callback, since it is scheduled by the +browser with that type of work in mind. That means that our code will need to use a document fragment, which can then +be appended in the next requestAnimationFrame callback. If you are using a VDOM library, you would use requestIdleCallback +to make changes, but you would apply the DOM patches in the next requestAnimationFrame callback, not the idle callback. + +Essentially: +------------ +- Do the VDOM work during the idlecallback +- Do DOM work in the next requestAnimationFrame callback +*/ + use std::rc::Rc; pub use crate::cfg::WebConfig;