From 9abb0470b7869019d539a2fc21da3872348ae38b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 11 Jul 2021 14:49:52 -0400 Subject: [PATCH] feat: static node infrastructure and ssr changes --- Cargo.toml | 16 +- README.md | 18 +- examples/async.rs | 26 +- packages/core/.vscode/settings.json | 2 +- packages/core/Cargo.toml | 19 +- packages/core/examples/async.rs | 4 +- packages/core/src/bumpframe.rs | 5 +- packages/core/src/childiter.rs | 164 ++++++++++++ packages/core/src/component.rs | 2 + packages/core/src/context.rs | 61 +++-- packages/core/src/diff.rs | 380 +++++++++++----------------- packages/core/src/events.rs | 2 +- packages/core/src/lib.rs | 2 + packages/core/src/nodebuilder.rs | 227 ++++++----------- packages/core/src/nodes.rs | 57 ++++- packages/core/src/scope.rs | 70 +++-- packages/core/src/styles.rs | 5 +- packages/core/src/tasks.rs | 80 +++--- packages/core/src/util.rs | 76 +++++- packages/core/src/virtual_dom.rs | 86 ++++--- packages/hooks/Cargo.toml | 2 +- packages/hooks/src/usestate.rs | 8 + packages/ssr/Cargo.toml | 1 + packages/ssr/README.md | 18 ++ packages/ssr/index.html | 103 +------- packages/ssr/src/lib.rs | 157 ++++++++---- packages/ssr/src/tostring.rs | 89 ------- packages/web/.vscode/settings.json | 2 +- packages/web/Cargo.toml | 8 +- packages/web/examples/async_web.rs | 71 ++++++ packages/web/examples/blah.rs | 52 ++++ packages/web/src/lib.rs | 35 ++- packages/web/src/new.rs | 25 +- packages/webview/src/dom.rs | 4 +- 34 files changed, 1022 insertions(+), 855 deletions(-) create mode 100644 packages/core/src/childiter.rs create mode 100644 packages/ssr/README.md delete mode 100644 packages/ssr/src/tostring.rs create mode 100644 packages/web/examples/async_web.rs create mode 100644 packages/web/examples/blah.rs diff --git a/Cargo.toml b/Cargo.toml index be7e9a2a3..af9b5e0c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,24 +9,27 @@ description = "Core functionality for Dioxus - a concurrent renderer-agnostic Vi [dependencies] dioxus-core = { path="./packages/core", version="0.1.0" } -dioxus-core-macro = { path="./packages/core-macro", version="0.1.0" } +dioxus-core-macro = { path="./packages/core-macro", version="0.1.0", optional=true } dioxus-html = { path="./packages/html", optional=true } dioxus-web = { path="./packages/web", optional=true } dioxus-webview = { path="./packages/webview", optional=true } -dioxus-hooks = { path="./packages/hooks", version="0.0.0" } +dioxus-hooks = { path="./packages/hooks", version="0.0.0", optional=true } [features] -default = ["ssr", "hooks", "router", "core", "atoms", "html", "web", "desktop"] +default = ["macro", "ssr", "hooks", "router", "core", "atoms", "html", "web", "desktop"] atoms = [] -core = [] +macro = ["dioxus-core-macro"] ssr = [] -hooks = [] +hooks = ["dioxus-hooks"] router = [] html = ["dioxus-html"] web = ["dioxus-web"] desktop = ["dioxus-webview"] +# Core just includes dioxus-core (the only required package) +core = [] + [profile.dev] split-debuginfo = "unpacked" @@ -41,6 +44,7 @@ separator = "0.4.1" serde = { version="1.0.126", features=["derive"] } surf = "2.2.0" env_logger = "*" +async-std = { version="1.9.0", features=["attributes"] } [workspace] members = [ @@ -50,7 +54,7 @@ members = [ "packages/web", "packages/webview", "packages/hooks", + "packages/ssr", # "packages/atoms", - # "packages/ssr", # "packages/docsite", ] diff --git a/README.md b/README.md index cddbd2beb..616db0ff4 100644 --- a/README.md +++ b/README.md @@ -36,14 +36,6 @@ If you know React, then you already know Dioxus. - Powerful and simple integrated state management - Rust! (enums, static types, modules, efficiency) -### Key Differentiators - -- Immutability by default -- Built-in suspense system -- Integrations for isomorphic apps -- Skip diffing altogether with signal API -- Extremely portable without runtime requirements - ## Get Started with... @@ -121,13 +113,15 @@ Dioxus is heavily inspired by React, but we want your transition to feel like an | Controlled Inputs | ✅ | ✅ | stateful wrappers around inputs | | CSS/Inline Styles | ✅ | ✅ | syntax for inline styles/attribute groups | | Custom elements | ✅ | ✅ | Define new element primitives | -| Compile-time correct | ✅ | ✅ | Throw errors on invalid template layouts | -| 1st class global state | ✅ | ❓ | redux/recoil/mobx on top of context | | Suspense | 🛠 | ✅ | schedule future render from future/promise | | Cooperative Scheduling | 🛠 | ✅ | Prioritize important events over non-important events | -| Fine-grained reactivity | 🛠 | ❓ | Skip diffing for fine-grain updates | -| Runs natively | ✅ | ❓ | runs as a portable binary w/o a runtime (Node) | | NodeRef | 🛠 | ✅ | gain direct access to nodes [1] | +| Fine-grained reactivity | 🛠 | ❓ | Skip diffing for fine-grain updates | +| Compile-time correct | ✅ | ❓ | Throw errors on invalid template layouts | +| Runs natively | ✅ | ❓ | runs as a portable binary w/o a runtime (Node) | +| 1st class global state | ✅ | ❓ | redux/recoil/mobx on top of context | +| Subtree Memoization | ✅ | ❓ | skip diffing static element subtrees | +| Heuristic Engine | ✅ | ❓ | track component memory usage to minimize allocations | - [1] Currently blocked until we figure out a cross-platform way of exposing an imperative Node API. diff --git a/examples/async.rs b/examples/async.rs index 98e847cce..988c99e05 100644 --- a/examples/async.rs +++ b/examples/async.rs @@ -2,7 +2,7 @@ //! //! The example from the README.md -use std::pin::Pin; +use std::{pin::Pin, time::Duration}; use dioxus::prelude::*; use futures::Future; @@ -19,23 +19,25 @@ struct DogApi { const ENDPOINT: &str = "https://dog.ceo/api/breeds/image/random"; -struct Ex(Pin + 'static>>); static App: FC<()> = |cx| { - // let mut count = use_state(cx, || 0); + let mut count = use_state(cx, || 0); let mut fut = cx.use_hook( move || { - Ex(Box::pin(async { + Box::pin(async { // + let mut tick = 0; loop { - match surf::get(ENDPOINT).recv_json::().await { - Ok(_) => (), - Err(_) => (), - } + async_std::task::sleep(Duration::from_millis(250)).await; + log::debug!("ticking forward... {}", tick); + tick += 1; + // match surf::get(ENDPOINT).recv_json::().await { + // Ok(_) => (), + // Err(_) => (), + // } } - }) - as Pin + 'static>>) + }) as Pin + 'static>> }, - |h| &mut h.0, + |h| h, |_| {}, ); @@ -43,7 +45,7 @@ static App: FC<()> = |cx| { cx.render(rsx! { div { - + h1 {"it's working somewhat properly"} } }) }; diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json index 34a811347..2b58ae251 100644 --- a/packages/core/.vscode/settings.json +++ b/packages/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": true + "rust-analyzer.inlayHints.enable": false } diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index daf17b964..815f14414 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -31,20 +31,19 @@ log = "0.4" # # Serialize the Edits for use in Webview/Liveview instances serde = { version="1", features=["derive"], optional=true } -smallvec = "1.6.1" - # Backs scopes and unique keys slotmap = "1.0.3" -# backs the fiber system for suspended components -# todo: would like to use something smaller or just roll our own futures manually -futures = "0.3.15" - - -async-std = { version="1.9.0", features=["attributes"] } appendlist = "1.4.0" +futures-util = "0.3.15" + +[dev-dependencies] +dioxus-html = { path="../html" } +dioxus-hooks = { path="../hooks" } +env_logger = "*" +# async-std = { version="1.9.0", features=["attributes"] } + [features] -default = ["serialize"] -# default = [] +default = [] serialize = ["slotmap/serde", "serde"] diff --git a/packages/core/examples/async.rs b/packages/core/examples/async.rs index ddd67fd3d..a6a0749a7 100644 --- a/packages/core/examples/async.rs +++ b/packages/core/examples/async.rs @@ -1,7 +1,7 @@ use std::pin::Pin; use dioxus_core::prelude::*; -use futures::Future; +use std::future::Future; fn main() {} @@ -18,7 +18,7 @@ const App: FC<()> = |cx| { ); // let g = unsafe { Pin::new_unchecked(fut) }; - cx.submit_task(fut); + // cx.submit_task(fut); todo!() }; diff --git a/packages/core/src/bumpframe.rs b/packages/core/src/bumpframe.rs index bc0ca97d8..ade95d7db 100644 --- a/packages/core/src/bumpframe.rs +++ b/packages/core/src/bumpframe.rs @@ -2,7 +2,6 @@ use crate::hooklist::HookList; use crate::{arena::SharedArena, innerlude::*}; use appendlist::AppendList; use bumpalo::Bump; -use futures::FutureExt; use slotmap::DefaultKey; use slotmap::SlotMap; use std::marker::PhantomData; @@ -34,11 +33,11 @@ impl ActiveFrame { Self::from_frames( BumpFrame { bump: Bump::new(), - head_node: VNode::text(""), + head_node: VNode::static_text(""), }, BumpFrame { bump: Bump::new(), - head_node: VNode::text(""), + head_node: VNode::static_text(""), }, ) } diff --git a/packages/core/src/childiter.rs b/packages/core/src/childiter.rs new file mode 100644 index 000000000..fa738335c --- /dev/null +++ b/packages/core/src/childiter.rs @@ -0,0 +1,164 @@ +/// 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> DoubleEndedIterator for ChildIterator<'a> { +// fn next_back(&mut self) -> Option { +// todo!() +// } +// } + +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 + 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 _count = *count as usize; + if _count >= frag.children.len() { + should_pop = true; + } else { + should_push = Some(&frag.children[_count]); + } + } + + // 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 + } +} + +mod tests { + use super::*; + use crate as dioxus; + use crate::innerlude::*; + use crate::util::DebugDom; + use dioxus_core_macro::*; + + // #[test] + // fn test_child_iterator() { + // static App: FC<()> = |cx| { + // cx.render(rsx! { + // Fragment { + // div {} + // h1 {} + // h2 {} + // h3 {} + // Fragment { + // "internal node" + // div { + // "baller text shouldn't show up" + // } + // p { + + // } + // Fragment { + // Fragment { + // "wow you really like framgents" + // Fragment { + // "why are you like this" + // Fragment { + // "just stop now please" + // Fragment { + // "this hurts" + // Fragment { + // "who needs this many fragments?????" + // Fragment { + // "just... fine..." + // Fragment { + // "no" + // } + // } + // } + // } + // } + // } + // } + // } + // } + // "my text node 1" + // "my text node 2" + // "my text node 3" + // "my text node 4" + // } + // }) + // }; + // let mut dom = VirtualDom::new(App); + // let mut renderer = DebugDom::new(); + // dom.rebuild(&mut renderer).unwrap(); + // let starter = dom.base_scope().root(); + // let ite = RealChildIterator::new(starter, &dom.components); + // for child in ite { + // match child { + // VNode::Element(el) => println!("Found: Element {}", el.tag_name), + // VNode::Text(t) => println!("Found: Text {:?}", t.text), + + // // These would represent failing cases. + // VNode::Fragment(_) => panic!("Found: Fragment"), + // VNode::Suspended { real } => panic!("Found: Suspended"), + // VNode::Component(_) => panic!("Found: Component"), + // } + // } + // } +} diff --git a/packages/core/src/component.rs b/packages/core/src/component.rs index 6687bb873..f5bc7ea79 100644 --- a/packages/core/src/component.rs +++ b/packages/core/src/component.rs @@ -9,6 +9,7 @@ use crate::innerlude::FC; pub trait Properties: Sized { type Builder; + const IS_STATIC: bool = false; fn builder() -> Self::Builder; /// Memoization can only happen if the props are 'static @@ -19,6 +20,7 @@ pub trait Properties: Sized { impl Properties for () { type Builder = EmptyBuilder; + const IS_STATIC: bool = true; fn builder() -> Self::Builder { EmptyBuilder {} } diff --git a/packages/core/src/context.rs b/packages/core/src/context.rs index c1b5f62a0..6396d5e02 100644 --- a/packages/core/src/context.rs +++ b/packages/core/src/context.rs @@ -2,7 +2,6 @@ use crate::hooklist::HookList; use crate::{arena::SharedArena, innerlude::*}; use appendlist::AppendList; use bumpalo::Bump; -use futures::FutureExt; use slotmap::DefaultKey; use slotmap::SlotMap; use std::marker::PhantomData; @@ -40,11 +39,9 @@ use std::{ pub struct Context<'src, T> { pub props: &'src T, pub scope: &'src Scope, - pub tasks: &'src AppendList<&'src mut DTask<'src>>, + pub tasks: &'src RefCell>, } - -pub type DTask<'s> = dyn Future + 's; -// pub type DTask = Pin>>; +pub type PinnedTask = Pin>>; impl<'src, T> Copy for Context<'src, T> {} impl<'src, T> Clone for Context<'src, T> { @@ -133,36 +130,34 @@ impl<'src, P> Context<'src, P> { /// ) /// } /// ``` - pub fn use_hook( + pub fn use_hook( self, - - // The closure that builds the hook state - initializer: impl FnOnce() -> InternalHookState, - - // The closure that takes the hookstate and returns some value - runner: impl FnOnce(&'src mut InternalHookState) -> Output, - - // The closure that cleans up whatever mess is left when the component gets torn down - // TODO: add this to the "clean up" group for when the component is dropped - cleanup: impl FnOnce(InternalHookState), - ) -> Output { + initializer: Init, + runner: Run, + cleanup: Cleanup, + ) -> Output + where + State: 'static, + Output: 'src, + Init: FnOnce() -> State, + Run: FnOnce(&'src mut State) -> Output, + Cleanup: FnOnce(State), + { // If the idx is the same as the hook length, then we need to add the current hook if self.scope.hooks.at_end() { let new_state = initializer(); self.scope.hooks.push(new_state); } - let state = self.scope.hooks.next::().expect( - r###" - Unable to retrive the hook that was initialized in this index. - Consult the `rules of hooks` to understand how to use hooks properly. - - You likely used the hook in a conditional. Hooks rely on consistent ordering between renders. - Any function prefixed with "use" should not be called conditionally. - "###, - ); + const ERR_MSG: &str = r###" +Unable to retrive the hook that was initialized in this index. +Consult the `rules of hooks` to understand how to use hooks properly. - runner(state) +You likely used the hook in a conditional. Hooks rely on consistent ordering between renders. +Any function prefixed with "use" should not be called conditionally. +"###; + + runner(self.scope.hooks.next::().expect(ERR_MSG)) } /// This hook enables the ability to expose state to children further down the VirtualDOM Tree. @@ -178,8 +173,7 @@ impl<'src, P> Context<'src, P> { /// /// pub fn use_create_context(&self, init: impl Fn() -> T) { - let mut scope = self.scope; - let mut cxs = scope.shared_contexts.borrow_mut(); + let mut cxs = self.scope.shared_contexts.borrow_mut(); let ty = TypeId::of::(); let is_initialized = self.use_hook( @@ -274,6 +268,7 @@ impl<'src, P> Context<'src, P> { fut: &'src mut Pin + 'static>>, callback: Fut, ) -> VNode<'src> { + use futures_util::FutureExt; match fut.now_or_never() { Some(out) => { let suspended_cx = SuspendedContext {}; @@ -290,6 +285,10 @@ impl<'src, P> Context<'src, P> { } /// `submit_task` will submit the future to be polled. + /// + /// This is useful when you have some async task that needs to be progressed. + /// + /// ## Explanation /// Dioxus will step its internal event loop if the future returns if the future completes while waiting. /// /// Tasks can't return anything, but they can be controlled with the returned handle @@ -299,8 +298,8 @@ impl<'src, P> Context<'src, P> { /// /// /// - pub fn submit_task(&self, mut task: &'src mut DTask<'src>) -> TaskHandle { - self.tasks.push(task); + pub fn submit_task(&self, task: &'src mut PinnedTask) -> TaskHandle { + self.tasks.borrow_mut().push(task); TaskHandle { _p: PhantomData {} } } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 9c268be6a..e989c4e37 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -5,14 +5,31 @@ //! ------ //! //! The inspiration and code for this module was originally taken from Dodrio (@fitzgen) and then modified to support -//! Components, Fragments, Suspense, and additional batching operations. +//! Components, Fragments, Suspense, SubTree memoization, and additional batching operations. //! -//! Implementation Details: -//! ----------------------- +//! ## Implementation Details: +//! +//! ### IDs for elements //! //! All nodes are addressed by their IDs. The RealDom provides an imperative interface for making changes to these nodes. //! We don't necessarily require that DOM changes happen instnatly during the diffing process, so the implementor may choose -//! to batch nodes if it is more performant for their application. We care about an ID size of u32 +//! to batch nodes if it is more performant for their application. The expectation is that renderers use a Slotmap for nodes +//! 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 +//! brick the user's page. +//! +//! ## Subtree Memoization +//! We also employ "subtree memoization" which saves us from having to check trees which take no dynamic content. We can +//! detect if a subtree is "static" by checking if its children are "static". Since we dive into the tree depth-first, the +//! calls to "create" propogate this information upwards. Structures like the one below are entirely static: +//! ```rust +//! rsx!( div { class: "hello world", "this node is entirely static" } ) +//! ``` +//! Because the subtrees won't be diffed, their "real node" data will be stale (invalid), so its up to the reconciler to +//! track nodes created in a scope and clean up all relevant data. Support for this is currently WIP +//! //! //! //! Further Reading and Thoughts @@ -25,10 +42,7 @@ use crate::{arena::SharedArena, innerlude::*, tasks::TaskQueue}; use fxhash::{FxHashMap, FxHashSet}; -use std::{ - any::Any, - rc::{Rc, Weak}, -}; +use std::any::Any; /// The accompanying "real dom" exposes an imperative API for controlling the UI layout /// @@ -46,8 +60,10 @@ pub trait RealDom<'a> { fn push_root(&mut self, root: RealDomNode); // Add Nodes to the dom - fn append_child(&mut self); - fn replace_with(&mut self); + // add m nodes from the stack + fn append_children(&mut self, many: u32); + // replace the n-m node on the stack with the m nodes + fn replace_with(&mut self, many: u32); // Remove Nodesfrom the dom fn remove(&mut self); @@ -101,7 +117,10 @@ pub struct DiffMachine<'real, 'bump, Dom: RealDom<'bump>> { pub seen_nodes: FxHashSet, } -impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> { +impl<'real, 'bump, Dom> DiffMachine<'real, 'bump, Dom> +where + Dom: RealDom<'bump>, +{ pub fn new( dom: &'real mut Dom, components: &'bump SharedArena, @@ -141,6 +160,10 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> { // 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. (VNode::Text(old), VNode::Text(new)) => { + if old.is_static { + log::debug!("encountered static text node: {:?}", old.text); + } + if old.text != new.text { self.dom.push_root(old.dom_id.get()); log::debug!("Text has changed {}, {}", old.text, new.text); @@ -156,8 +179,8 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> { // In Dioxus, this is less likely to occur unless through a fragment if new.tag_name != old.tag_name || new.namespace != old.namespace { self.dom.push_root(old.dom_id.get()); - self.create(new_node); - self.dom.replace_with(); + let meta = self.create(new_node); + self.dom.replace_with(meta.added_to_stack); return; } new.dom_id.set(old.dom_id.get()); @@ -181,7 +204,7 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> { // make sure the component's caller function is up to date let scope = self.components.try_get_mut(scope_id.unwrap()).unwrap(); // .with_scope(scope_id.unwrap(), |scope| { - scope.caller = Rc::downgrade(&new.caller); + scope.caller = new.caller.clone(); // ack - this doesn't work on its own! scope.update_children(new.children); @@ -265,17 +288,17 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> { // in the case where the old node was a fragment but the new nodes are text, (VNode::Fragment(_) | VNode::Component(_), VNode::Element(_) | VNode::Text(_)) => { // find the first real element int the old node - let mut iter = RealChildIterator::new(old_node, self.components); - if let Some(first) = iter.next() { - // replace the very first node with the creation of the element or text - } else { - // there are no real elements in the old fragment... - // We need to load up the next real - } - if let VNode::Component(old) = old_node { - // schedule this component's destructor to be run - todo!() - } + // let mut iter = RealChildIterator::new(old_node, self.components); + // if let Some(first) = iter.next() { + // // replace the very first node with the creation of the element or text + // } else { + // // there are no real elements in the old fragment... + // // We need to load up the next real + // } + // if let VNode::Component(old) = old_node { + // // schedule this component's destructor to be run + // todo!() + // } } // In the case where real nodes are being replaced by potentially (VNode::Element(_) | VNode::Text(_), VNode::Fragment(new)) => { @@ -283,11 +306,11 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> { } (VNode::Text(_), VNode::Element(_)) => { self.create(new_node); - self.dom.replace_with(); + self.dom.replace_with(1); } (VNode::Element(_), VNode::Text(_)) => { self.create(new_node); - self.dom.replace_with(); + self.dom.replace_with(1); } _ => { @@ -295,7 +318,28 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> { } } } +} +// 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. +struct CreateMeta { + is_static: bool, + added_to_stack: u32, +} + +impl CreateMeta { + fn new(is_static: bool, added_to_tack: u32) -> Self { + Self { + is_static, + added_to_stack: added_to_tack, + } + } +} + +impl<'real, 'bump, Dom> DiffMachine<'real, 'bump, Dom> +where + Dom: RealDom<'bump>, +{ // Emit instructions to create the given virtual node. // // The change list stack may have any shape upon entering this function: @@ -305,15 +349,22 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> { // When this function returns, the new node is on top of the change list stack: // // [... node] - fn create(&mut self, node: &'bump VNode<'bump>) { - // debug_assert!(self.dom.traversal_is_committed()); - log::warn!("Creating node!"); + fn create(&mut self, node: &'bump VNode<'bump>) -> CreateMeta { + log::warn!("Creating node! ... {:#?}", node); match node { VNode::Text(text) => { let real_id = self.dom.create_text_node(text.text); text.dom_id.set(real_id); + CreateMeta::new(text.is_static, 1) } VNode::Element(el) => { + // we have the potential to completely eliminate working on this node in the future(!) + // + // This can only be done if all of the elements properties (attrs, children, listeners, etc) are static + // While creating these things, keep track if we can memoize this element. + // At the end, we'll set this flag on the element to skip it + let mut is_static: bool = true; + let VElement { key, tag_name, @@ -322,22 +373,27 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> { children, namespace, dom_id, + is_static: el_is_static, } = el; - // log::info!("Creating {:#?}", node); + let real_id = if let Some(namespace) = namespace { self.dom.create_element(tag_name, Some(namespace)) } else { self.dom.create_element(tag_name, None) }; - el.dom_id.set(real_id); + dom_id.set(real_id); listeners.iter().enumerate().for_each(|(idx, listener)| { + listener.mounted_node.set(real_id); self.dom .new_event_listener(listener.event, listener.scope, idx, real_id); - listener.mounted_node.set(real_id); + + // if the node has an event listener, then it must be visited ? + is_static = false; }); for attr in *attributes { + is_static = is_static && attr.is_static; self.dom.set_attribute(&attr.name, &attr.value, *namespace); } @@ -359,28 +415,26 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> { // } for child in *children { - self.create(child); - if let VNode::Fragment(_) = child { - // do nothing - // fragments append themselves - } else { - self.dom.append_child(); - } + let child_meta = self.create(child); + is_static = is_static && child_meta.is_static; + + // append whatever children were generated by this call + self.dom.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) } - VNode::Component(component) => { - // let real_id = self.dom.create_placeholder(); - - // let root_id = next_id(); - // self.dom.save_known_root(root_id); - + VNode::Component(vcomponent) => { log::debug!("Mounting a new component"); - let caller: Weak = Rc::downgrade(&component.caller); - - // We're modifying the component arena while holding onto references into the assoiated bump arenas of its children - // those references are stable, even if the component arena moves around in memory, thanks to the bump arenas. - // However, there is no way to convey this to rust, so we need to use unsafe to pierce through the lifetime. + let caller = vcomponent.caller.clone(); let parent_idx = self.cur_idx; @@ -398,50 +452,69 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> { height, self.event_queue.new_channel(height, new_idx), self.components.clone(), - component.children, + vcomponent.children, + self.task_queue.new_submitter(), ) }) }) .unwrap(); - { - let cur_component = self.components.try_get_mut(idx).unwrap(); - let mut ch = cur_component.descendents.borrow_mut(); - ch.insert(idx); - std::mem::drop(ch); - } + // 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); - // yaaaaay lifetimes out of thin air - // really tho, we're merging the frame lifetimes together + // TODO: abstract this unsafe into the arena abstraction let inner: &'bump mut _ = unsafe { &mut *self.components.components.get() }; let new_component = inner.get_mut(idx).unwrap(); // Actually initialize the caller's slot with the right address - component.ass_scope.set(Some(idx)); + vcomponent.ass_scope.set(Some(idx)); // Run the scope for one iteration to initialize it new_component.run_scope().unwrap(); - // And then run the diff algorithm - let _real_id = self.dom.create_placeholder(); + // By using "diff_node" instead of "create", we delegate the mutations to the child + // However, "diff_node" always expects a real node on the stack, so we put a placeholder so it knows where to start. + // + // TODO: we need to delete (IE relcaim this node, otherwise the arena will grow infinitely) + let _ = self.dom.create_placeholder(); self.diff_node(new_component.old_frame(), new_component.next_frame()); // Finally, insert this node as a seen node. self.seen_nodes.insert(idx); + + // Virtual Components don't result in new nodes on the stack + // However, we can skip them from future diffing if they take no children, have no props, take no key, etc. + CreateMeta::new(vcomponent.is_static, 0) } - // we go the the "known root" but only operate on a sibling basis + // 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. VNode::Fragment(frag) => { - // create the children directly in the space - for child in frag.children { - self.create(child); - self.dom.append_child(); + 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; } + + // Never ignore + CreateMeta::new(false, nodes_added) } VNode::Suspended { real } => { let id = self.dom.create_placeholder(); real.set(id); + CreateMeta::new(false, 1) } } } @@ -1171,11 +1244,9 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // // 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>]) { - // debug_assert!(self.dom.traversal_is_committed()); for child in new { - // self.create_and_append(node, parent) - self.create(child); - self.dom.append_child(); + let meta = self.create(child); + self.dom.append_children(meta.added_to_stack); } } @@ -1227,168 +1298,3 @@ enum KeyedPrefixResult { // the beginning of `new` and `old` we already processed. MoreWorkToDo(usize), } - -/// 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> DoubleEndedIterator for ChildIterator<'a> { -// fn next_back(&mut self) -> Option { -// todo!() -// } -// } - -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 - 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 _count = *count as usize; - if _count >= frag.children.len() { - should_pop = true; - } else { - should_push = Some(&frag.children[_count]); - } - } - - // 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 - } -} - -mod tests { - use super::*; - use crate as dioxus; - use crate::innerlude::*; - use crate::util::DebugDom; - use dioxus_core_macro::*; - - // #[test] - // fn test_child_iterator() { - // static App: FC<()> = |cx| { - // cx.render(rsx! { - // Fragment { - // div {} - // h1 {} - // h2 {} - // h3 {} - // Fragment { - // "internal node" - // div { - // "baller text shouldn't show up" - // } - // p { - - // } - // Fragment { - // Fragment { - // "wow you really like framgents" - // Fragment { - // "why are you like this" - // Fragment { - // "just stop now please" - // Fragment { - // "this hurts" - // Fragment { - // "who needs this many fragments?????" - // Fragment { - // "just... fine..." - // Fragment { - // "no" - // } - // } - // } - // } - // } - // } - // } - // } - // } - // "my text node 1" - // "my text node 2" - // "my text node 3" - // "my text node 4" - // } - // }) - // }; - // let mut dom = VirtualDom::new(App); - // let mut renderer = DebugDom::new(); - // dom.rebuild(&mut renderer).unwrap(); - // let starter = dom.base_scope().root(); - // let ite = RealChildIterator::new(starter, &dom.components); - // for child in ite { - // match child { - // VNode::Element(el) => println!("Found: Element {}", el.tag_name), - // VNode::Text(t) => println!("Found: Text {:?}", t.text), - - // // These would represent failing cases. - // VNode::Fragment(_) => panic!("Found: Fragment"), - // VNode::Suspended { real } => panic!("Found: Suspended"), - // VNode::Component(_) => panic!("Found: Component"), - // } - // } - // } -} diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index e6bb1a911..63add18b6 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -4,7 +4,7 @@ //! 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::{ops::Deref, rc::Rc}; +use std::{cell::Cell, ops::Deref, rc::Rc}; use crate::innerlude::{RealDomNode, ScopeIdx}; diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 228f585c0..a6e762c1a 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -43,6 +43,7 @@ pub(crate) mod innerlude { pub use crate::nodebuilder::*; pub use crate::nodes::*; pub use crate::scope::*; + pub use crate::tasks::*; pub use crate::util::*; pub use crate::virtual_dom::*; @@ -62,6 +63,7 @@ pub mod prelude { pub use crate::styles::{AsAttr, StyleBuilder}; pub use crate::util::RealDomNode; + pub use crate::virtual_dom::VirtualDom; pub use nodes::*; pub use crate::nodebuilder::LazyNodes; diff --git a/packages/core/src/nodebuilder.rs b/packages/core/src/nodebuilder.rs index 0255682a1..d3b45d4ce 100644 --- a/packages/core/src/nodebuilder.rs +++ b/packages/core/src/nodebuilder.rs @@ -308,7 +308,7 @@ where /// let my_div: VNode = builder.finish(); /// ``` #[inline] - pub fn finish(self) -> VNode<'a> { + pub fn finish(mut self) -> VNode<'a> { let bump = self.cx.bump(); let children: &'a Children = bump.alloc(self.children); @@ -317,6 +317,23 @@ where let listeners: &'a Listeners = bump.alloc(self.listeners); let listeners: &'a [Listener<'a>] = listeners.as_ref(); + for listener in listeners { + // bump the context id forward + let id = self.cx.listener_id.get(); + self.cx.listener_id.set(id + 1); + + // Add this listener to the context list + // This casts the listener to a self-referential pointer + // This is okay because the bump arena is stable + + // TODO: maybe not add it into CTX immediately + let r = unsafe { std::mem::transmute::<&Listener<'a>, &Listener<'static>>(listener) }; + self.cx.scope_ref.listeners.borrow_mut().push(( + r.mounted_node as *const _ as *mut _, + r.callback as *const _ as *mut _, + )); + } + let attributes: &'a Attributes = bump.alloc(self.attributes); let attributes: &'a [Attribute<'a>] = attributes.as_ref(); @@ -338,57 +355,57 @@ where Attributes: 'a + AsRef<[Attribute<'a>]>, Children: 'a + AsRef<[VNode<'a>]>, { - /// Add a new event listener to this element. - /// - /// The `event` string specifies which event will be listened for. The - /// `callback` function is the function that will be invoked if the - /// specified event occurs. - /// - /// # Example - /// - /// ```no_run - /// use dioxus::{builder::*, bumpalo::Bump}; - /// - /// let b = Bump::new(); - /// - /// // A button that does something when clicked! - /// let my_button = button(&b) - /// .on("click", |event| { - /// // ... - /// }) - /// .finish(); - /// ``` - pub fn on(self, event: &'static str, callback: impl FnMut(VirtualEvent) + 'a) -> Self { - let bump = &self.cx.bump(); - let listener = Listener { - event, - callback: bump.alloc(callback), - scope: self.cx.scope_ref.arena_idx, - mounted_node: bump.alloc(Cell::new(RealDomNode::empty())), - }; - self.add_listener(listener) - } + // / Add a new event listener to this element. + // / + // / The `event` string specifies which event will be listened for. The + // / `callback` function is the function that will be invoked if the + // / specified event occurs. + // / + // / # Example + // / + // / ```no_run + // / use dioxus::{builder::*, bumpalo::Bump}; + // / + // / let b = Bump::new(); + // / + // / // A button that does something when clicked! + // / let my_button = button(&b) + // / .on("click", |event| { + // / // ... + // / }) + // / .finish(); + // / ``` + // pub fn on(self, event: &'static str, callback: impl FnMut(VirtualEvent) + 'a) -> Self { + // let bump = &self.cx.bump(); + // let listener = Listener { + // event, + // callback: bump.alloc(callback), + // scope: self.cx.scope_ref.arena_idx, + // mounted_node: bump.alloc(Cell::new(RealDomNode::empty())), + // }; + // self.add_listener(listener) + // } - pub fn add_listener(mut self, listener: Listener<'a>) -> Self { - self.listeners.push(listener); + // pub fn add_listener(mut self, listener: Listener<'a>) -> Self { + // self.listeners.push(listener); - // bump the context id forward - let id = self.cx.listener_id.get(); - self.cx.listener_id.set(id + 1); + // // bump the context id forward + // let id = self.cx.listener_id.get(); + // self.cx.listener_id.set(id + 1); - // Add this listener to the context list - // This casts the listener to a self-referential pointer - // This is okay because the bump arena is stable - self.listeners.last().map(|g| { - let r = unsafe { std::mem::transmute::<&Listener<'a>, &Listener<'static>>(g) }; - self.cx.scope_ref.listeners.borrow_mut().push(( - r.mounted_node as *const _ as *mut _, - r.callback as *const _ as *mut _, - )); - }); + // // Add this listener to the context list + // // This casts the listener to a self-referential pointer + // // This is okay because the bump arena is stable + // self.listeners.last().map(|g| { + // let r = unsafe { std::mem::transmute::<&Listener<'a>, &Listener<'static>>(g) }; + // self.cx.scope_ref.listeners.borrow_mut().push(( + // r.mounted_node as *const _ as *mut _, + // r.callback as *const _ as *mut _, + // )); + // }); - self - } + // self + // } } impl<'a, 'b, Listeners, Children> @@ -410,61 +427,16 @@ where /// let my_div = div(&b).attr("id", "my-div").finish(); /// ``` pub fn attr(mut self, name: &'static str, args: std::fmt::Arguments) -> Self { - let value = match args.as_str() { - Some(static_str) => static_str, - None => { - use bumpalo::core_alloc::fmt::Write; - let mut s = bumpalo::collections::String::new_in(self.cx.bump()); - s.write_fmt(args).unwrap(); - s.into_bump_str() - } - }; + let (value, is_static) = raw_text(self.cx.bump(), args); self.attributes.push(Attribute { name, value, + is_static, namespace: None, }); self } - - /// Conditionally add a "boolean-style" attribute to this element. - /// - /// If the `should_add` parameter is true, then adds an attribute with the - /// given `name` and an empty string value. If the `should_add` parameter is - /// false, then the attribute is not added. - /// - /// This method is useful for attributes whose semantics are defined by - /// whether or not the attribute is present or not, and whose value is - /// ignored. Example attributes like this include: - /// - /// * `checked` - /// * `hidden` - /// * `selected` - /// - /// # Example - /// - /// ```no_run - /// use dioxus::{builder::*, bumpalo::Bump}; - /// use js_sys::Math; - /// - /// let b = Bump::new(); - /// - /// // Create the `
` that is randomly hidden 50% of the time. - /// let my_div = div(&b) - /// .bool_attr("hidden", Math::random() >= 0.5) - /// .finish(); - /// ``` - pub fn bool_attr(mut self, name: &'static str, should_add: bool) -> Self { - if should_add { - self.attributes.push(Attribute { - name, - value: "", - namespace: None, - }); - } - self - } } impl<'a, 'b, Listeners, Attributes> @@ -618,72 +590,18 @@ impl IntoVNode<'_> for Option<()> { } } -/// Construct a text VNode. -/// -/// This is `dioxus`'s virtual DOM equivalent of `document.createTextVNode`. -/// -/// # Example -/// -/// ```no_run -/// use dioxus::builder::*; -/// -/// let my_text = text("hello, dioxus!"); -/// ``` -#[inline] -pub fn text<'a>(contents: &'a str) -> VNode<'a> { - VNode::text(contents) -} - -pub fn text2<'a>(contents: bumpalo::collections::String<'a>) -> VNode<'a> { - let f: &'a str = contents.into_bump_str(); - VNode::text(f) -} - -pub fn text3<'a>(bump: &'a bumpalo::Bump, args: std::fmt::Arguments) -> VNode<'a> { - // This is a cute little optimization - // - // We use format_args! on everything. However, not all textnodes have dynamic content, and thus are completely static. - // we can just short-circuit to the &'static str version instead of having to allocate in the bump arena. - // - // In the most general case, this prevents the need for any string allocations for simple code IE: - // ``` - // div {"abc"} - // ``` - VNode::text(raw_text(bump, args)) -} - -pub fn raw_text<'a>(bump: &'a bumpalo::Bump, args: std::fmt::Arguments) -> &'a str { +pub fn raw_text<'a>(bump: &'a bumpalo::Bump, args: std::fmt::Arguments) -> (&'a str, bool) { match args.as_str() { - Some(static_str) => static_str, + Some(static_str) => (static_str, true), None => { use bumpalo::core_alloc::fmt::Write; let mut s = bumpalo::collections::String::new_in(bump); s.write_fmt(args).unwrap(); - s.into_bump_str() + (s.into_bump_str(), false) } } } -/// Construct an attribute for an element. -/// -/// # Example -/// -/// This example creates the `id="my-id"` for some element like `
`. -/// -/// ```no_run -/// use dioxus::builder::*; -/// -/// let my_id_attr = attr("id", "my-id"); -/// ``` -pub fn attr<'a>(name: &'static str, value: &'a str) -> Attribute<'a> { - Attribute { - name, - value, - namespace: None, - } -} - pub fn virtual_child<'a, T: Properties + 'a>( cx: NodeFactory<'a>, f: FC, @@ -726,7 +644,7 @@ impl<'a> NodeFactory<'a> { /// Create some text that's allocated along with the other vnodes pub fn text(&self, args: Arguments) -> VNode<'a> { - text3(self.bump(), args) + VNode::text(self.bump(), args) } /// Create an element builder @@ -749,10 +667,11 @@ impl<'a> NodeFactory<'a> { val: Arguments, namespace: Option<&'static str>, ) -> Attribute<'a> { - let value = raw_text(self.bump(), val); + let (value, is_static) = raw_text(self.bump(), val); Attribute { name, value, + is_static, namespace, } } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index c82d31e9a..991b85897 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -7,7 +7,7 @@ use crate::{ arena::SharedArena, events::VirtualEvent, innerlude::{Context, Properties, RealDom, RealDomNode, Scope, ScopeIdx, FC}, - nodebuilder::{text3, NodeFactory}, + nodebuilder::NodeFactory, }; use appendlist::AppendList; use bumpalo::Bump; @@ -39,7 +39,7 @@ pub enum VNode<'src> { /// A "suspended component" /// This is a masqeurade over an underlying future that needs to complete - /// When the future is completed, the VNode will then trigger a render + /// When the future is completed, the VNode will then trigger a render and the `real` field gets populated Suspended { real: Cell }, /// A User-defined componen node (node type COMPONENT_NODE) @@ -84,6 +84,7 @@ impl<'old, 'new> VNode<'old> { }, namespace: el.namespace.clone(), dom_id: el.dom_id.clone(), + is_static: el.is_static.clone(), }; VNode::Element(new.alloc_with(move || new_el)) @@ -121,21 +122,33 @@ impl<'a> VNode<'a> { children, namespace, dom_id: Cell::new(RealDomNode::empty()), + is_static: Cell::new(false), }); VNode::Element(element) } - /// Construct a new text node with the given text. - #[inline] - pub fn text(text: &'a str) -> VNode<'a> { + pub fn static_text(text: &'static str) -> VNode { VNode::Text(VText { text, + is_static: true, dom_id: Cell::new(RealDomNode::empty()), }) } - - pub fn text_args(bump: &'a Bump, args: Arguments) -> VNode<'a> { - text3(bump, args) + /// Construct a new text node with the given text. + pub fn text(bump: &'a Bump, args: Arguments) -> VNode<'a> { + match args.as_str() { + Some(text) => VNode::static_text(text), + None => { + use bumpalo::core_alloc::fmt::Write; + let mut s = bumpalo::collections::String::new_in(bump); + s.write_fmt(args).unwrap(); + VNode::Text(VText { + text: s.into_bump_str(), + is_static: false, + dom_id: Cell::new(RealDomNode::empty()), + }) + } + } } #[inline] @@ -191,6 +204,7 @@ impl Debug for VNode<'_> { #[derive(Clone)] pub struct VText<'src> { pub text: &'src str, + pub is_static: bool, pub dom_id: Cell, } @@ -208,6 +222,7 @@ pub struct VElement<'a> { pub children: &'a [VNode<'a>], pub namespace: Option<&'static str>, pub dom_id: Cell, + pub is_static: Cell, } /// An attribute on a DOM node, such as `id="my-thing"` or @@ -216,6 +231,7 @@ pub struct VElement<'a> { pub struct Attribute<'a> { pub name: &'static str, pub value: &'a str, + pub is_static: bool, /// If an attribute is "namespaced", then it belongs to a group /// The most common namespace is the "style" namespace @@ -336,6 +352,8 @@ pub struct VComponent<'src> { pub comparator: Option<&'src dyn Fn(&VComponent) -> bool>, + pub is_static: bool, + // a pointer into the bump arena (given by the 'src lifetime) // raw_props: Box, raw_props: *const (), @@ -402,6 +420,15 @@ impl<'a> VComponent<'a> { None => NodeKey(None), }; + let caller = create_component_caller(component, raw_props); + + // If the component does not have children, has no props (we can't memoize props), and has no no key, then we don't + // need to bother diffing it in the future + // + // This is more of an optimization to prevent unnecessary descending through the tree during diffing, rather than + // actually speeding up the diff process itself + let is_static = children.len() == 0 && P::IS_STATIC && key.is_none(); + Self { user_fc, comparator, @@ -409,7 +436,8 @@ impl<'a> VComponent<'a> { children, ass_scope: Cell::new(None), key, - caller: create_closure(component, raw_props), + caller, + is_static, mounted_root: Cell::new(RealDomNode::empty()), } } @@ -417,14 +445,14 @@ impl<'a> VComponent<'a> { type Captured<'a> = Rc Fn(&'r Scope) -> VNode<'r> + 'a>; -fn create_closure<'a, P: 'a>( +pub fn create_component_caller<'a, P: 'a>( user_component: FC

, raw_props: *const (), ) -> Rc Fn(&'r Scope) -> VNode<'r>> { let g: Captured = Rc::new(move |scp: &Scope| -> VNode { // cast back into the right lifetime let safe_props: &'_ P = unsafe { &*(raw_props as *const P) }; - let tasks = AppendList::new(); + let tasks = RefCell::new(Vec::new()); let cx: Context

= Context { props: safe_props, scope: scp, @@ -433,11 +461,12 @@ fn create_closure<'a, P: 'a>( let g = user_component(cx); - // collect the submitted tasks - println!("tasks submittted: {:#?}", tasks.len()); - // log::debug!("tasks submittted: {:#?}", tasks.len()); + for task in tasks.borrow_mut().drain(..) { + scp.submit_task(task); + } let g2 = unsafe { std::mem::transmute(g) }; + g2 }); let r: Captured<'static> = unsafe { std::mem::transmute(g) }; diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index c022ea09d..5d0783a90 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -2,10 +2,10 @@ use crate::hooklist::HookList; use crate::{arena::SharedArena, innerlude::*}; use appendlist::AppendList; use bumpalo::Bump; -use futures::FutureExt; use slotmap::DefaultKey; use slotmap::SlotMap; use std::marker::PhantomData; +use std::sync::Arc; use std::{ any::{Any, TypeId}, cell::{Cell, RefCell}, @@ -14,7 +14,7 @@ use std::{ future::Future, ops::Deref, pin::Pin, - rc::{Rc, Weak}, + rc::Rc, }; // We need to pin the hook so it doesn't move as we initialize the list of hooks @@ -51,7 +51,7 @@ pub struct Scope { pub event_channel: Rc, - pub caller: Weak, + pub caller: Rc, // ========================== // slightly unsafe stuff @@ -73,6 +73,8 @@ pub struct Scope { // NEEDS TO BE PRIVATE pub(crate) listeners: RefCell, *mut dyn FnMut(VirtualEvent))>>, + pub task_submitter: TaskSubmitter, + pub(crate) suspended_tasks: Vec<*mut Pin>>>>, } @@ -85,13 +87,14 @@ impl Scope { // Scopes cannot be made anywhere else except for this file // Therefore, their lifetimes are connected exclusively to the virtual dom pub fn new<'creator_node>( - caller: Weak, + caller: Rc, arena_idx: ScopeIdx, parent: Option, height: u32, event_channel: EventChannel, arena_link: SharedArena, child_nodes: &'creator_node [VNode<'creator_node>], + task_submitter: TaskSubmitter, ) -> Self { log::debug!( "New scope created, height is {}, idx is {:?}", @@ -109,6 +112,7 @@ impl Scope { height, event_channel, arena_link, + task_submitter, listener_idx: Default::default(), frames: ActiveFrame::new(), hooks: Default::default(), @@ -119,7 +123,7 @@ impl Scope { } } - pub fn update_caller<'creator_node>(&mut self, caller: Weak) { + pub fn update_caller<'creator_node>(&mut self, caller: Rc) { self.caller = caller; } @@ -136,19 +140,15 @@ impl Scope { // This breaks any latent references, invalidating every pointer referencing into it. self.frames.next().bump.reset(); + log::debug!("clearing listeners!"); // Remove all the outdated listeners self.listeners.borrow_mut().clear(); unsafe { self.hooks.reset() }; self.listener_idx.set(0); - let caller = self - .caller - .upgrade() - .ok_or(Error::FatalInternal("Failed to get caller"))?; - // Cast the caller ptr from static to one with our own reference - let c3: &WrappedCaller = caller.as_ref(); + let c3: &WrappedCaller = self.caller.as_ref(); self.frames.cur_frame_mut().head_node = unsafe { self.call_user_component(c3) }; @@ -176,28 +176,42 @@ impl Scope { // Convert the raw ptr into an actual object // This operation is assumed to be safe - log::debug!("Calling listeners! {:?}", self.listeners.borrow().len()); - let mut listners = self.listeners.borrow_mut(); - let (_, listener) = listners - .iter() - .find(|(domptr, _)| { - let p = unsafe { &**domptr }; - p.get() == real_node_id.expect("realdomnode not found, propery handling of true virtual events not managed") - }) - .expect(&format!( - "Failed to find real node with ID {:?}", - real_node_id - )); + log::debug!( + "There are {:?} listeners associated with this scope {:#?}", + self.listeners.borrow().len(), + self.arena_idx + ); - // TODO: Don'tdo a linear scan! Do a hashmap lookup! It'll be faster! - unsafe { - let mut listener_fn = &mut **listener; - listener_fn(event); + let mut listners = self.listeners.borrow_mut(); + + // let listener = listners.get(trigger); + let raw_listener = listners.iter().find(|(domptr, _)| { + let search = unsafe { &**domptr }; + let search_id = search.get(); + log::info!("searching listener {:#?}", search_id); + match real_node_id { + Some(e) => search_id == e, + None => 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!(), } Ok(()) } + pub fn submit_task(&self, task: &mut Pin>>) { + log::debug!("Task submitted into scope"); + (self.task_submitter)(DTask::new(task, self.arena_idx)); + } + pub(crate) fn next_frame<'bump>(&'bump self) -> &'bump VNode<'bump> { self.frames.current_head_node() } @@ -210,7 +224,7 @@ impl Scope { self.frames.cur_frame() } - pub(crate) fn root<'a>(&'a self) -> &'a VNode<'a> { + pub fn root<'a>(&'a self) -> &'a VNode<'a> { &self.frames.cur_frame().head_node } } diff --git a/packages/core/src/styles.rs b/packages/core/src/styles.rs index 433ea55f0..e16fa84da 100644 --- a/packages/core/src/styles.rs +++ b/packages/core/src/styles.rs @@ -12,10 +12,7 @@ //! //! -use crate::{ - innerlude::Attribute, - nodebuilder::{text3, NodeFactory}, -}; +use crate::{innerlude::Attribute, nodebuilder::NodeFactory}; pub struct StyleBuilder; pub trait AsAttr<'a> { diff --git a/packages/core/src/tasks.rs b/packages/core/src/tasks.rs index febdaff41..22a31e88c 100644 --- a/packages/core/src/tasks.rs +++ b/packages/core/src/tasks.rs @@ -16,14 +16,16 @@ use std::{ task::{Context, Poll}, }; -use futures::{Future, Stream, StreamExt}; +use futures_util::{Future, Stream, StreamExt}; use slotmap::{DefaultKey, SlotMap}; use crate::{events::EventTrigger, prelude::ScopeIdx}; +pub type TaskSubmitter = Arc; + pub struct TaskQueue { slots: Arc>>, - submitter: Arc, + submitter: TaskSubmitter, } impl TaskQueue { @@ -34,11 +36,16 @@ impl TaskQueue { let submitter = Arc::new(move |task| { let mut slots = slots2.write().unwrap(); + log::debug!("Task submitted into global task queue"); slots.insert(task); }); Self { slots, submitter } } + pub fn new_submitter(&self) -> TaskSubmitter { + self.submitter.clone() + } + pub fn submit_task(&mut self, task: DTask) -> TaskHandle { let key = self.slots.write().unwrap().insert(task); TaskHandle { key } @@ -126,43 +133,44 @@ impl DTask { } } -mod tests { - use std::time::Duration; +// #[cfg(test)] +// mod tests { +// use std::time::Duration; - use super::*; - use bumpalo::Bump; +// use super::*; +// use bumpalo::Bump; - #[async_std::test] - async fn example() { - let bump = Bump::new(); - type RawTask = Pin>>; - // build the three - let f1 = bump.alloc(Box::pin(async { - // - async_std::task::sleep(Duration::from_secs(3)).await; - println!("3 sec") - }) as RawTask); +// #[async_std::test] +// async fn example() { +// let bump = Bump::new(); +// type RawTask = Pin>>; +// // build the three +// let f1 = bump.alloc(Box::pin(async { +// // +// async_std::task::sleep(Duration::from_secs(3)).await; +// println!("3 sec") +// }) as RawTask); - let f2 = bump.alloc(Box::pin(async { - // - async_std::task::sleep(Duration::from_secs(2)).await; - println!("2 sec") - }) as RawTask); +// let f2 = bump.alloc(Box::pin(async { +// // +// async_std::task::sleep(Duration::from_secs(2)).await; +// println!("2 sec") +// }) as RawTask); - let f3 = bump.alloc(Box::pin(async { - // - async_std::task::sleep(Duration::from_secs(1)).await; - println!("1 sec"); - }) as RawTask); +// let f3 = bump.alloc(Box::pin(async { +// // +// async_std::task::sleep(Duration::from_secs(1)).await; +// println!("1 sec"); +// }) as RawTask); - let mut queue = TaskQueue::new(); - queue.submit_task(DTask::debug_new(f1)); - queue.submit_task(DTask::debug_new(f2)); - queue.submit_task(DTask::debug_new(f3)); +// let mut queue = TaskQueue::new(); +// queue.submit_task(DTask::debug_new(f1)); +// queue.submit_task(DTask::debug_new(f2)); +// queue.submit_task(DTask::debug_new(f3)); - while !queue.is_empty() { - let next = queue.next().await; - println!("Event received {:#?}", next); - } - } -} +// while !queue.is_empty() { +// let next = queue.next().await; +// println!("Event received {:#?}", next); +// } +// } +// } diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs index 6a6614e1e..4ae22a923 100644 --- a/packages/core/src/util.rs +++ b/packages/core/src/util.rs @@ -4,6 +4,8 @@ use std::{ vec::Drain, }; +use futures_util::StreamExt; + use crate::innerlude::*; #[derive(PartialEq, Debug, Clone, Default)] @@ -67,18 +69,28 @@ impl RealDomNode { pub struct DebugDom { counter: u64, + logging: bool, } impl DebugDom { pub fn new() -> Self { - Self { counter: 0 } + Self { + counter: 0, + logging: false, + } + } + pub fn with_logging_enabled() -> Self { + Self { + counter: 0, + logging: true, + } } } impl<'a> RealDom<'a> for DebugDom { fn push_root(&mut self, root: RealDomNode) {} - fn append_child(&mut self) {} + fn append_children(&mut self, many: u32) {} - fn replace_with(&mut self) {} + fn replace_with(&mut self, many: u32) {} fn remove(&mut self) {} @@ -120,3 +132,61 @@ impl<'a> RealDom<'a> for DebugDom { todo!() } } + +async fn launch_demo(app: FC<()>) { + let mut dom = VirtualDom::new(app); + let mut real_dom = DebugDom::new(); + dom.rebuild(&mut real_dom).unwrap(); + + while let Some(evt) = dom.tasks.next().await { + // + log::debug!("Event triggered! {:#?}", evt); + } +} + +// #[cfg(test)] +// mod tests { +// use super::*; +// use crate as dioxus; +// use std::{pin::Pin, time::Duration}; + +// use crate::builder::DioxusElement; +// use dioxus::prelude::*; +// use futures::Future; + +// #[async_std::test] +// async fn async_tick() { +// static App: FC<()> = |cx| { +// // let mut count = use_state(cx, || 0); +// let mut fut = cx.use_hook( +// move || { +// Box::pin(async { +// // +// let mut tick: i32 = 0; +// loop { +// async_std::task::sleep(Duration::from_millis(250)).await; +// log::debug!("ticking forward... {}", tick); +// tick += 1; +// // match surf::get(ENDPOINT).recv_json::().await { +// // Ok(_) => (), +// // Err(_) => (), +// // } +// } +// }) as Pin + 'static>> +// }, +// |h| h, +// |_| {}, +// ); + +// cx.submit_task(fut); + +// cx.render(LazyNodes::new(move |f| { +// f.text(format_args!("it's sorta working")) +// })) +// }; + +// std::env::set_var("RUST_LOG", "debug"); +// env_logger::init(); +// launch_demo(App).await; +// } +// } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index c8d63bdb1..af0775f6c 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -24,6 +24,9 @@ use crate::{arena::SharedArena, innerlude::*}; use appendlist::AppendList; use slotmap::DefaultKey; use slotmap::SlotMap; +use std::any::Any; +use std::cell::RefCell; +use std::pin::Pin; use std::{any::TypeId, fmt::Debug, rc::Rc}; pub type ScopeIdx = DefaultKey; @@ -45,11 +48,9 @@ pub struct VirtualDom { /// All components dump their updates into a queue to be processed pub(crate) event_queue: EventQueue, - pub(crate) tasks: TaskQueue, + pub tasks: TaskQueue, - /// a strong allocation to the "caller" for the original component and its props - #[doc(hidden)] - _root_caller: Rc, + root_props: std::pin::Pin>, /// Type of the original cx. This is stored as TypeId so VirtualDom does not need to be generic. /// @@ -129,59 +130,36 @@ impl VirtualDom { pub fn new_with_props(root: FC

, root_props: P) -> Self { let components = SharedArena::new(SlotMap::new()); - // Normally, a component would be passed as a child in the RSX macro which automatically produces OpaqueComponents - // Here, we need to make it manually, using an RC to force the Weak reference to stick around for the main scope. - let _root_caller: Rc = Rc::new(move |scope: &Scope| { - // let _root_caller: Rc> = Rc::new(move |scope| { - // the lifetime of this closure is just as long as the lifetime on the scope reference - // this closure moves root props (which is static) into this closure - let props = unsafe { &*(&root_props as *const _) }; - let tasks = AppendList::new(); - let t2 = &tasks; - - let cx = Context { - props, - scope, - tasks: t2, - }; - let nodes = root(cx); - - log::debug!("There were {:?} tasks submitted", tasks.len()); - // cast a weird lifetime to shake the appendlist thing - // TODO: move all of this into the same logic that governs other components - // we want to wrap everything in a dioxus root component - unsafe { std::mem::transmute(nodes) } - // std::mem::drop(tasks); - // - // nodes - }); - - // Create a weak reference to the OpaqueComponent for the root scope to use as its render function - let caller_ref = Rc::downgrade(&_root_caller); + 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(); - // Make the first scope - // We don't run the component though, so renderers will need to call "rebuild" when they initialize their DOM 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); - Scope::new(caller_ref, myidx, None, 0, event_channel, link, &[]) + let caller = crate::nodes::create_component_caller(root, props_ptr as *const _); + Scope::new(caller, myidx, None, 0, event_channel, link, &[], submitter) }) }) .unwrap(); + log::debug!("base scope is {:#?}", base_scope); + Self { - _root_caller, base_scope, event_queue, components, - tasks: TaskQueue::new(), + root_props, + tasks, _root_prop_type: TypeId::of::

(), } } @@ -191,6 +169,38 @@ impl VirtualDom { // Private Methods for the VirtualDom // ====================================== impl VirtualDom { + /// 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. + /// + /// ## Why? + /// + /// 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<()> { + let mut realdom = DebugDom::new(); + let mut diff_machine = DiffMachine::new( + &mut realdom, + &self.components, + self.base_scope, + self.event_queue.clone(), + &self.tasks, + ); + + // Schedule an update and then immediately call it on the root component + // This is akin to a hook being called from a listener and requring a re-render + // Instead, this is done on top-level component + let base = self.components.try_get(self.base_scope)?; + + let update = &base.event_channel; + update(); + + self.progress_completely(&mut diff_machine)?; + + Ok(()) + } + /// 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 pub fn rebuild<'s, Dom: RealDom<'s>>(&'s mut self, realdom: &mut Dom) -> Result<()> { diff --git a/packages/hooks/Cargo.toml b/packages/hooks/Cargo.toml index 3696a9fa5..40b58a22e 100644 --- a/packages/hooks/Cargo.toml +++ b/packages/hooks/Cargo.toml @@ -7,4 +7,4 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../core" } +dioxus-core = { path="../core" } diff --git a/packages/hooks/src/usestate.rs b/packages/hooks/src/usestate.rs index c95ca80b6..bd7b8ad27 100644 --- a/packages/hooks/src/usestate.rs +++ b/packages/hooks/src/usestate.rs @@ -24,6 +24,14 @@ use std::{ /// Additionally, a ton of std::ops traits are implemented for the `UseState` wrapper, meaning any mutative type operations /// will automatically be called on the WIP value. /// +/// ## Combinators +/// +/// On top of the methods to set/get state, `use_state` also supports fancy combinators to extend its functionality: +/// - `.classic()` and `.split()` convert the hook into the classic React-style hook +/// ```rust +/// let (state, set_state) = use_state(cx, || 10).split() +/// ``` +/// /// /// Usage: /// ```ignore diff --git a/packages/ssr/Cargo.toml b/packages/ssr/Cargo.toml index 2bf128676..3783e6bf9 100644 --- a/packages/ssr/Cargo.toml +++ b/packages/ssr/Cargo.toml @@ -11,6 +11,7 @@ dioxus-core = { path = "../core", version = "0.1.0", features = ["serialize"] } [dev-dependencies] +dioxus-html = { path = "../html" } tide-websockets = "*" thiserror = "1.0.23" log = "0.4.13" diff --git a/packages/ssr/README.md b/packages/ssr/README.md new file mode 100644 index 000000000..451f9f250 --- /dev/null +++ b/packages/ssr/README.md @@ -0,0 +1,18 @@ +# Dioxus SSR + +Render a Dioxus VirtualDOM to a string. + +```rust +// Our app: +const App: FC<()> = |cx| cx.render(rsx!(div {"hello world!"})); + +// Build the virtualdom from our app +let mut vdom = VirtualDOM::new(App); + +// This runs components, lifecycles, etc. without needing a physical dom. Some features (like noderef) won't work. +vdom.rebuild_in_place(); + +// Render the entire virtualdom from the root +let text = dioxus_ssr::render_root(&vdom); +assert_eq!(text, "

hello world!
") +``` diff --git a/packages/ssr/index.html b/packages/ssr/index.html index 9021c236e..fe9879cff 100644 --- a/packages/ssr/index.html +++ b/packages/ssr/index.html @@ -1,102 +1,3 @@ -
-
-

- Hello world!: 0 -

-
-
-

- Hello world!: 1 -

-
-
-

- Hello world!: 2 -

-
-
-

- Hello world!: 3 -

-
-
-

- Hello world!: 4 -

-
-
-

- Hello world!: 5 -

-
-
-

- Hello world!: 6 -

-
-
-

- Hello world!: 7 -

-
-
-

- Hello world!: 8 -

-
-
-

- Hello world!: 9 -

-
-
-

- Hello world!: 10 -

-
-
-

- Hello world!: 11 -

-
-
-

- Hello world!: 12 -

-
-
-

- Hello world!: 13 -

-
-
-

- Hello world!: 14 -

-
-
-

- Hello world!: 15 -

-
-
-

- Hello world!: 16 -

-
-
-

- Hello world!: 17 -

-
-
-

- Hello world!: 18 -

-
-
-

- Hello world!: 19 -

-
+
+hello world!
diff --git a/packages/ssr/src/lib.rs b/packages/ssr/src/lib.rs index babdbe364..8ada48b7a 100644 --- a/packages/ssr/src/lib.rs +++ b/packages/ssr/src/lib.rs @@ -7,79 +7,128 @@ //! an external renderer is needed to progress the component lifecycles. The `TextRenderer` shows how to use the Virtual DOM //! API to progress these lifecycle events to generate a fully-mounted Virtual DOM instance which can be renderer in the //! `render` method. -//! -//! ```ignore -//! fn main() { -//! let renderer = TextRenderer::<()>::new(|_| html! {
"Hello world"
}); -//! let output = renderer.render(); -//! assert_eq!(output, "
Hello World
"); -//! } -//! ``` -//! -//! The `TextRenderer` is particularly useful when needing to cache a Virtual DOM in between requests -//! -use dioxus_core::prelude::{VNode, FC}; -pub mod tostring; +use std::fmt::{Arguments, Display, Formatter}; -pub mod prelude { - pub use dioxus_core::prelude::*; +use dioxus_core::prelude::*; +use dioxus_core::{nodes::VNode, prelude::ScopeIdx, virtual_dom::VirtualDom}; +use std::io::{BufWriter, Result, Write}; + +pub fn render_root(vdom: &VirtualDom) -> String { + format!("{:}", TextRenderer::new(vdom)) } -/// The `TextRenderer` provides a way of rendering a Dioxus Virtual DOM to a String. +/// A configurable text renderer for the Dioxus VirtualDOM. +/// +/// +/// ## Details +/// +/// This uses the `Formatter` infrastructure so you can write into anything that supports `write_fmt`. We can't accept +/// any generic writer, so you need to "Display" the text renderer. This is done through `format!` or `format_args!` /// /// /// -pub struct TextRenderer { - _root_type: std::marker::PhantomData, +/// +pub struct TextRenderer<'a> { + vdom: &'a VirtualDom, } -impl TextRenderer { - /// 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) -> Self { - Self { - _root_type: std::marker::PhantomData {}, +impl<'a> TextRenderer<'a> { + fn new(vdom: &'a VirtualDom) -> Self { + Self { vdom } + } + + fn html_render(&self, node: &VNode, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match node { + VNode::Element(el) => { + write!(f, "<{}", el.tag_name)?; + for attr in el.attributes { + write!(f, " {}=\"{}\"", attr.name, attr.value)?; + } + write!(f, ">\n")?; + for child in el.children { + self.html_render(child, f)?; + } + write!(f, "\n", el.tag_name)?; + Ok(()) + } + VNode::Text(text) => write!(f, "{}", text.text), + VNode::Suspended { .. } => todo!(), + VNode::Component(vcomp) => { + todo!() + // let id = vcomp.ass_scope.borrow().unwrap(); + // let id = vcomp.ass_scope.as_ref().borrow().unwrap(); + // let new_node = dom + // .components + // .try_get(id) + // .unwrap() + // .frames + // .current_head_node(); + // html_render(&dom, new_node, f) + } + VNode::Fragment(_) => todo!(), } } +} - /// 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 { - todo!() +impl Display for TextRenderer<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let root = self.vdom.base_scope(); + let root_node = root.root(); + self.html_render(root_node, f) } +} - /// Pass new args to the root function - pub fn update(&mut self, new_val: T) { - todo!() - } +#[cfg(test)] +mod tests { + use super::*; + + use dioxus_core as dioxus; + use dioxus_html as dioxus_elements; + + const SIMPLE_APP: FC<()> = |cx| { + // + cx.render(rsx!(div { + "hello world!" + })) + }; + + const SLIGHTLY_MORE_COMPLEX: FC<()> = |cx| { + cx.render(rsx! { + div { + title: "About W3Schools" + {(0..20).map(|f| rsx!{ + div { + title: "About W3Schools" + style: "color:blue;text-align:center" + class: "About W3Schools" + p { + title: "About W3Schools" + "Hello world!: {f}" + } + } + })} + } + }) + }; - /// Modify the root function in place, forcing a re-render regardless if the props changed - pub fn update_mut(&mut self, modifier: impl Fn(&mut T)) { - todo!() + #[test] + fn test_to_string_works() { + let mut dom = VirtualDom::new(SIMPLE_APP); + dom.rebuild_in_place().expect("failed to run virtualdom"); + dbg!(render_root(&dom)); } - /// Immediately render a VNode to string - pub fn to_text(root: VNode) -> String { - todo!() - } + #[test] + fn test_write_to_file() { + use std::fs::File; - /// Render the virtual DOM to a string - pub fn render(&self) -> String { - let mut buffer = String::new(); + let mut file = File::create("index.html").unwrap(); - // iterate through the internal patch queue of virtual dom, and apply them to the buffer - /* - */ - todo!() - } + let mut dom = VirtualDom::new(SIMPLE_APP); + dom.rebuild_in_place().expect("failed to run virtualdom"); - /// Render VDom to an existing buffer - /// TODO @Jon, support non-string buffers to actually make this useful - /// Currently, this only supports overwriting an existing buffer, instead of just - pub fn render_mut(&self, buf: &mut String) { - todo!() + file.write_fmt(format_args!("{}", TextRenderer::new(&dom))) + .unwrap(); } } diff --git a/packages/ssr/src/tostring.rs b/packages/ssr/src/tostring.rs deleted file mode 100644 index fafc73cc0..000000000 --- a/packages/ssr/src/tostring.rs +++ /dev/null @@ -1,89 +0,0 @@ -use std::fmt::Display; - -use dioxus_core::prelude::*; -use dioxus_core::{nodes::VNode, prelude::ScopeIdx, virtual_dom::VirtualDom}; - -struct SsrRenderer { - dom: VirtualDom, -} - -impl Display for SsrRenderer { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let node = self.dom.base_scope().frames.current_head_node(); - - html_render(&self.dom, node, f) - } -} - -// recursively walk the tree -fn html_render( - dom: &VirtualDom, - node: &VNode, - f: &mut std::fmt::Formatter<'_>, -) -> std::fmt::Result { - match node { - VNode::Element(el) => { - write!(f, "<{}", el.tag_name)?; - for attr in el.attributes { - write!(f, " {}=\"{}\"", attr.name, attr.value)?; - } - write!(f, ">\n")?; - for child in el.children { - html_render(dom, child, f)?; - } - write!(f, "\n", el.tag_name)?; - Ok(()) - } - VNode::Text(text) => write!(f, "{}", text), - VNode::Suspended => todo!(), - VNode::Component(vcomp) => { - let id = vcomp.ass_scope.borrow().unwrap(); - // let id = vcomp.ass_scope.as_ref().borrow().unwrap(); - let new_node = dom - .components - .try_get(id) - .unwrap() - .frames - .current_head_node(); - html_render(&dom, new_node, f) - } - VNode::Fragment(_) => todo!(), - } -} - -#[test] -fn test_serialize() { - let mut dom = VirtualDom::new(|cx| { - // - // - // - cx.render(rsx! { - div { - title: "About W3Schools" - {(0..20).map(|f| rsx!{ - div { - title: "About W3Schools" - // style: "color:blue;text-align:center" - class: "About W3Schools" - p { - title: "About W3Schools" - "Hello world!: {f}" - } - } - })} - } - }) - }); - - dom.rebuild(); - let renderer = SsrRenderer { dom }; - - use std::fs::File; - use std::io::prelude::*; - - let mut file = File::create("index.html").unwrap(); - let buf = renderer.to_string(); - // dbg!(buf); - file.write(buf.as_bytes()); - // dbg!(renderer.to_string()); -} diff --git a/packages/web/.vscode/settings.json b/packages/web/.vscode/settings.json index 80f51cff8..2b58ae251 100644 --- a/packages/web/.vscode/settings.json +++ b/packages/web/.vscode/settings.json @@ -1,3 +1,3 @@ { "rust-analyzer.inlayHints.enable": false -} \ No newline at end of file +} diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 73584d522..72171a09a 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -23,11 +23,12 @@ generational-arena = "0.2.8" wasm-bindgen-test = "0.3.21" once_cell = "1.7.2" atoms = { path="../atoms" } - async-channel = "1.6.1" anyhow = "1.0.41" slotmap = "1.0.3" +futures-util = "0.3.15" + [dependencies.web-sys] version = "0.3.50" features = [ @@ -72,6 +73,9 @@ crate-type = ["cdylib", "rlib"] [dev-dependencies] im-rc = "15.0.0" -rand = { version="0.8.4", features=["small_rng"] } +# rand = { version="0.8.4", features=["small_rng"] } separator = "0.4.1" uuid = { version="0.8.2", features=["v4", "wasm-bindgen"] } +dioxus-hooks = { path="../hooks" } +gloo-timers = { version="0.2.1", features=["futures"] } +# wasm-timer = "0.2.5" diff --git a/packages/web/examples/async_web.rs b/packages/web/examples/async_web.rs new file mode 100644 index 000000000..1f2b66780 --- /dev/null +++ b/packages/web/examples/async_web.rs @@ -0,0 +1,71 @@ +//! 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 + wasm_bindgen_futures::spawn_local(WebsysRenderer::start(App)); +} + +static App: FC<()> = |cx| { + // let mut count = use_state(cx, || 0); + let fut = cx.use_hook( + move || { + Box::pin(async { + let mut tick: i32 = 0; + log::debug!("yeet!"); + loop { + gloo_timers::future::TimeoutFuture::new(250).await; + log::debug!("ticking forward... {}", tick); + tick += 1; + } + }) as Pin + 'static>> + }, + |h| h, + |_| {}, + ); + + cx.submit_task(fut); + + let state = use_state(cx, || 0); + cx.render(rsx! { + div { + section { class: "py-12 px-4 text-center" + div { class: "w-full max-w-2xl mx-auto" + span { class: "text-sm font-semibold" + "count: {state}" + } + div { + button { + onclick: move |_| state.set(state + 1) + "incr" + } + br {} + br {} + button { + onclick: move |_| state.set(state - 1) + "decr" + } + } + } + } + } + }) +}; diff --git a/packages/web/examples/blah.rs b/packages/web/examples/blah.rs new file mode 100644 index 000000000..d17426425 --- /dev/null +++ b/packages/web/examples/blah.rs @@ -0,0 +1,52 @@ +//! 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 + wasm_bindgen_futures::spawn_local(WebsysRenderer::start(App)); +} + +static App: FC<()> = |cx| { + let mut state = use_state(cx, || 0); + cx.render(rsx! { + div { + section { class: "py-12 px-4 text-center" + div { class: "w-full max-w-2xl mx-auto" + span { class: "text-sm font-semibold" + "static subtree" + } + } + } + section { class: "py-12 px-4 text-center" + div { class: "w-full max-w-2xl mx-auto" + span { class: "text-sm font-semibold" + "dynamic subtree {state}" + } + div { + button { onclick: move |_| state+=1, "incr" } + br {} + button { onclick: move |_| state-=1, "decr" } + } + } + } + } + }) +}; diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index f5637d9f3..93f0f8464 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -3,6 +3,8 @@ //! This crate implements a renderer of the Dioxus Virtual DOM for the web browser using Websys. use dioxus::prelude::{Context, Properties, VNode}; +use futures_util::{pin_mut, Stream, StreamExt}; + use fxhash::FxHashMap; use web_sys::{window, Document, Element, Event, Node}; // use futures::{channel::mpsc, SinkExt, StreamExt}; @@ -70,12 +72,41 @@ impl WebsysRenderer { self.internal_dom.rebuild(&mut websys_dom)?; - while let Some(trigger) = websys_dom.wait_for_event().await { + log::info!("Going into event loop"); + loop { + let trigger = { + let real_queue = websys_dom.wait_for_event(); + let task_queue = (&mut self.internal_dom.tasks).next(); + + 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, + } + }; + + log::info!("event received"); let root_node = body_element.first_child().unwrap(); websys_dom.stack.push(root_node.clone()); self.internal_dom - .progress_with_event(&mut websys_dom, trigger)?; + .progress_with_event(&mut websys_dom, trigger.unwrap())?; + + // let t2 = self.internal_dom.tasks.next(); + // futures::select! { + // trigger = t1 => { + // log::info!("event received"); + // let root_node = body_element.first_child().unwrap(); + // websys_dom.stack.push(root_node.clone()); + // self.internal_dom + // .progress_with_event(&mut websys_dom, trigger)?; + // }, + // () = t2 => {} + // }; } + // while let Some(trigger) = websys_dom.wait_for_event().await { + // } Ok(()) // 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 eeb4b547d..231b43c0d 100644 --- a/packages/web/src/new.rs +++ b/packages/web/src/new.rs @@ -78,7 +78,7 @@ impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom { self.stack.push(domnode.clone()); } - fn append_child(&mut self) { + fn append_children(&mut self, many: u32) { log::debug!("Called [`append_child`]"); let child = self.stack.pop(); @@ -99,7 +99,7 @@ impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom { self.stack.top().append_child(&child).unwrap(); } - fn replace_with(&mut self) { + fn replace_with(&mut self, many: u32) { log::debug!("Called [`replace_with`]"); let new_node = self.stack.pop(); let old_node = self.stack.pop(); @@ -193,15 +193,16 @@ impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom { &mut self, event: &'static str, scope: dioxus_core::prelude::ScopeIdx, - el_id: usize, + _element_id: usize, real_id: RealDomNode, ) { + let (_on, event) = event.split_at(2); let event = wasm_bindgen::intern(event); log::debug!( "Called [`new_event_listener`]: {}, {:?}, {}, {:?}", event, scope, - el_id, + _element_id, real_id ); // attach the correct attributes to the element @@ -215,10 +216,11 @@ impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom { .dyn_ref::() .expect(&format!("not an element: {:?}", el)); - let gi_id = scope.data().as_ffi(); + let scope_id = scope.data().as_ffi(); el.set_attribute( &format!("dioxus-event-{}", event), - &format!("{}.{}.{}", gi_id, el_id, real_id.0), + &format!("{}.{}", scope_id, real_id.0), + // &format!("{}.{}.{}", gi_id, el_id, real_id.0), ) .unwrap(); @@ -566,19 +568,20 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { .next() .and_then(|f| f.parse::().ok()) .context("failed to parse gi id")?; - let el_id = fields - .next() - .and_then(|f| f.parse::().ok()) - .context("failed to parse el id")?; + // let el_id = fields + // .next() + // .and_then(|f| f.parse::().ok()) + // .context("failed to parse el id")?; let real_id = fields .next() .and_then(|f| f.parse::().ok().map(RealDomNode::new)) .context("failed to parse real id")?; // Call the trigger - log::debug!("decoded gi_id: {}, li_idx: {}", gi_id, el_id); + log::debug!("decoded scope_id: {}, node_id: {:#?}", gi_id, real_id); let triggered_scope: ScopeIdx = KeyData::from_ffi(gi_id).into(); + log::debug!("Triggered scope is {:#?}", triggered_scope); Ok(EventTrigger::new( virtual_event_from_websys_event(event), triggered_scope, diff --git a/packages/webview/src/dom.rs b/packages/webview/src/dom.rs index 6c79dcd9e..602ce1c04 100644 --- a/packages/webview/src/dom.rs +++ b/packages/webview/src/dom.rs @@ -37,11 +37,11 @@ impl<'bump> RealDom<'bump> for WebviewDom<'bump> { self.edits.push(PushRoot { root: root.0 }); } - fn append_child(&mut self) { + fn append_children(&mut self, many: u32) { self.edits.push(AppendChild); } - fn replace_with(&mut self) { + fn replace_with(&mut self, many: u32) { self.edits.push(ReplaceWith); }