diff --git a/Cargo.toml b/Cargo.toml index 6ccba2e5c..1286072f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ dioxus-mobile = { path = "./packages/mobile", optional = true } [features] # core -default = ["core", "ssr", "desktop"] +default = ["core", "ssr"] core = ["macro", "hooks", "html"] macro = ["dioxus-core-macro"] hooks = ["dioxus-hooks"] @@ -37,18 +37,31 @@ mobile = ["dioxus-mobile"] [dev-dependencies] -futures = "0.3.15" +futures-util = "0.3.16" log = "0.4.14" num-format = "0.4.0" separator = "0.4.1" serde = { version = "1.0.126", features = ["derive"] } -surf = "2.2.0" +im-rc = "15.0.0" +fxhash = "0.2.1" +anyhow = "1.0.42" + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +argh = "0.1.5" env_logger = "*" async-std = { version = "1.9.0", features = ["attributes"] } -im-rc = "15.0.0" rand = { version = "0.8.4", features = ["small_rng"] } -fxhash = "0.2.1" +surf = {version = "2.2.0", git = "https://github.com/jkelleyrtp/surf/", branch = "jk/fix-the-wasm"} + +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] gloo-timers = "0.2.1" +surf = {version = "2.2.0", default-features = false, features = ["wasm-client"], git = "https://github.com/jkelleyrtp/surf/", branch = "jk/fix-the-wasm"} + + +[dependencies.getrandom] +version = "0.2" +features = ["js"] + [workspace] members = [ diff --git a/README.md b/README.md index b65f9df8b..271942169 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ Dioxus is heavily inspired by React, but we want your transition to feel like an | Custom elements | ✅ | ✅ | Define new element primitives | | Suspense | ✅ | ✅ | schedule future render from future/promise | | Integrated error handling | ✅ | ✅ | Gracefully handle errors with ? syntax | -| Re-hydration | 🛠 | ✅ | Pre-render to HTML to speed up first contentful paint | +| Re-hydration | ✅ | ✅ | Pre-render to HTML to speed up first contentful paint | | Cooperative Scheduling | 🛠 | ✅ | Prioritize important events over non-important events | | Runs natively | ✅ | ❓ | runs as a portable binary w/o a runtime (Node) | | 1st class global state | ✅ | ❓ | redux/recoil/mobx on top of context | diff --git a/examples/file_explorer.rs b/examples/file_explorer.rs index da566b42c..bedb3d23d 100644 --- a/examples/file_explorer.rs +++ b/examples/file_explorer.rs @@ -12,8 +12,10 @@ use std::fs::{self, DirEntry}; fn main() { env_logger::init(); dioxus::desktop::launch(App, |c| { - c.with_resizable(false) - .with_inner_size(LogicalSize::new(800.0, 400.0)) + c.with_window(|w| { + w.with_resizable(false) + .with_inner_size(LogicalSize::new(800.0, 400.0)) + }) }) .unwrap(); } diff --git a/examples/hydration.rs b/examples/hydration.rs new file mode 100644 index 000000000..dc9188f5a --- /dev/null +++ b/examples/hydration.rs @@ -0,0 +1,33 @@ +//! Example: realworld usage of hydration +//! ------------------------------------ +//! +//! This example shows how to pre-render a page using dioxus SSR and then how to rehydrate it on the client side. +//! +//! To accomplish hydration on the web, you'll want to set up a slightly more sophisticated build & bundle strategy. In +//! the official docs, we have a guide for using DioxusStudio as a build tool with pre-rendering and hydration. +//! +//! In this example, we pre-render the page to HTML and then pass it into the desktop configuration. This serves as a +//! proof-of-concept for the hydration feature, but you'll probably only want to use hydration for the web. + +use dioxus::prelude::*; +use dioxus::ssr; + +fn main() { + let mut vdom = VirtualDom::launch_in_place(App); + let content = ssr::render_vdom(&vdom, |f| f.pre_render(true)); + + dioxus::desktop::launch(App, |c| c.with_prerendered(content)).unwrap(); +} + +static App: FC<()> = |cx| { + let mut val = use_state(cx, || 0); + cx.render(rsx! { + div { + h1 {"hello world. Count: {val}"} + button { + "click to increment" + onclick: move |_| val += 1 + } + } + }) +}; diff --git a/examples/model.rs b/examples/model.rs index b3d87c612..f65355a8a 100644 --- a/examples/model.rs +++ b/examples/model.rs @@ -15,17 +15,19 @@ //! the RefCell will panic and crash. You can use `try_get_mut` or `.modify` to avoid this problem, or just not hold two //! RefMuts at the same time. +use dioxus::desktop::wry::application::dpi::LogicalSize; use dioxus::events::on::*; use dioxus::prelude::*; -use dioxus_desktop::wry::application::dpi::LogicalSize; const STYLE: &str = include_str!("./assets/calculator.css"); fn main() { env_logger::init(); dioxus::desktop::launch(App, |cfg| { - cfg.with_title("Calculator Demo") - .with_resizable(false) - .with_inner_size(LogicalSize::new(320.0, 530.0)) + cfg.with_window(|w| { + w.with_title("Calculator Demo") + .with_resizable(false) + .with_inner_size(LogicalSize::new(320.0, 530.0)) + }) }) .expect("failed to launch dioxus app"); } diff --git a/examples/reference/task.rs b/examples/reference/task.rs index 649c3d97a..e455b6ff8 100644 --- a/examples/reference/task.rs +++ b/examples/reference/task.rs @@ -30,8 +30,8 @@ pub static Example: FC<()> = |cx| { // Tasks are 'static, so we need to copy relevant items in let (async_count, dir) = (count.for_async(), *direction); + let (task, result) = use_task(cx, move || async move { - // Count infinitely! loop { gloo_timers::future::TimeoutFuture::new(250).await; *async_count.get_mut() += dir; diff --git a/examples/reference/tostring.rs b/examples/reference/tostring.rs index 5edf45b61..8827932f2 100644 --- a/examples/reference/tostring.rs +++ b/examples/reference/tostring.rs @@ -7,7 +7,7 @@ pub static Example: FC<()> = |cx| { // This is an easy/low hanging fruit to improve upon let mut dom = VirtualDom::new(SomeApp); dom.rebuild_in_place().unwrap(); - ssr::render_vdom(&dom) + ssr::render_vdom(&dom, |c| c) }); cx.render(rsx! { diff --git a/examples/ssr.rs b/examples/ssr.rs index b2f87770c..c5bc8d744 100644 --- a/examples/ssr.rs +++ b/examples/ssr.rs @@ -1,13 +1,15 @@ +#![allow(non_upper_case_globals)] + use dioxus::prelude::*; use dioxus::ssr; fn main() { let mut vdom = VirtualDom::new(App); - vdom.rebuild_in_place(); - println!("{}", ssr::render_vdom(&vdom)); + vdom.rebuild_in_place().expect("Rebuilding failed"); + println!("{}", ssr::render_vdom(&vdom, |c| c)); } -const App: FC<()> = |cx| { +static App: FC<()> = |cx| { cx.render(rsx!( div { h1 { "Title" } diff --git a/examples/tailwind.rs b/examples/tailwind.rs index 31f4b04e1..c4149faeb 100644 --- a/examples/tailwind.rs +++ b/examples/tailwind.rs @@ -3,10 +3,12 @@ use dioxus::prelude::*; fn main() { use dioxus::desktop::wry::application::platform::macos::*; dioxus::desktop::launch(App, |c| { - c.with_fullsize_content_view(true) - .with_titlebar_buttons_hidden(false) - .with_titlebar_transparent(true) - .with_movable_by_window_background(true) + c.with_window(|w| { + w.with_fullsize_content_view(true) + .with_titlebar_buttons_hidden(false) + .with_titlebar_transparent(true) + .with_movable_by_window_background(true) + }) }); } diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 988da23e5..5add6a446 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -1,14 +1,10 @@ +#![allow(non_upper_case_globals, non_snake_case)] use dioxus::prelude::*; use im_rc::HashMap; use std::rc::Rc; -fn main() { - #[cfg(feature = "desktop")] - // #[cfg(not(target_arch = "wasm32"))] - dioxus::desktop::launch(App, |c| c); - - #[cfg(feature = "desktop")] - dioxus::web::launch(App, |c| c); +fn main() -> anyhow::Result<()> { + dioxus::desktop::launch(App, |c| c) } #[derive(PartialEq)] diff --git a/examples/webview.rs b/examples/webview.rs index 22a18cedc..8759fc733 100644 --- a/examples/webview.rs +++ b/examples/webview.rs @@ -8,12 +8,13 @@ //! into the native VDom instance. //! //! Currently, NodeRefs won't work properly, but all other event functionality will. +#![allow(non_upper_case_globals, non_snake_case)] use dioxus::{events::on::MouseEvent, prelude::*}; -fn main() { +fn main() -> anyhow::Result<()> { env_logger::init(); - dioxus::desktop::launch(App, |c| c); + dioxus::desktop::launch(App, |c| c) } static App: FC<()> = |cx| { diff --git a/examples/webview_web.rs b/examples/webview_web.rs index 5d8a02802..b897ffb5a 100644 --- a/examples/webview_web.rs +++ b/examples/webview_web.rs @@ -1,3 +1,4 @@ +#![allow(non_upper_case_globals, non_snake_case)] //! Example: Webview Renderer //! ------------------------- //! @@ -11,53 +12,19 @@ use dioxus::prelude::*; -// #[cfg] fn main() { - // env_logger::init(); dioxus::web::launch(App, |c| c); } static App: FC<()> = |cx| { - dbg!("rednering parent"); - cx.render(rsx! { - div { - But { - h1 {"he"} - } - // But { - // h1 {"llo"} - // } - // But { - // h1 {"world"} - // } - } - }) -}; - -static But: FC<()> = |cx| { let mut count = use_state(cx, || 0); - // let d = Dropper { name: "asd" }; - // let handler = move |_| { - // dbg!(d.name); - // }; - cx.render(rsx! { div { h1 { "Hifive counter: {count}" } {cx.children()} button { onclick: move |_| count += 1, "Up high!" } button { onclick: move |_| count -= 1, "Down low!" } - // button { onclick: {handler}, "Down low!" } } }) }; - -// struct Dropper { -// name: &'static str, -// } -// impl Drop for Dropper { -// fn drop(&mut self) { -// dbg!("dropped"); -// } -// } diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 7febd1fac..5798abc23 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -40,6 +40,10 @@ smallvec = "1.6.1" slab = "0.4.3" +[dev-dependencies] +dioxus-html = { path = "../html" } + + [features] default = ["serialize"] serialize = ["serde"] diff --git a/packages/core/examples/alternative.rs b/packages/core/examples/alternative.rs index 441924326..fc30beb62 100644 --- a/packages/core/examples/alternative.rs +++ b/packages/core/examples/alternative.rs @@ -1,9 +1,7 @@ -fn main() {} - -use dioxus::*; -use dioxus_core as dioxus; use dioxus_core::prelude::*; +fn main() {} + pub static Example: FC<()> = |cx| { let list = (0..10).map(|f| LazyNodes::new(move |f| todo!())); diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 0b6f41c1a..4e1a1561e 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -1,4 +1,5 @@ use std::cell::{RefCell, RefMut}; +use std::fmt::Display; use std::{cell::UnsafeCell, rc::Rc}; use crate::heuristics::*; @@ -17,6 +18,11 @@ pub struct ScopeId(pub usize); #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] pub struct ElementId(pub usize); +impl Display for ElementId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} impl ElementId { pub fn as_u64(self) -> u64 { diff --git a/packages/core/src/context.rs b/packages/core/src/context.rs index 9519f2108..f8b538d9f 100644 --- a/packages/core/src/context.rs +++ b/packages/core/src/context.rs @@ -1,24 +1,24 @@ -use crate::innerlude::*; +//! Public APIs for managing component state, tasks, and lifecycles. -use futures_util::FutureExt; -use std::{ - any::{Any, TypeId}, - cell::{Cell, RefCell}, - future::Future, - ops::Deref, - rc::Rc, -}; +use crate::innerlude::*; +use std::{any::TypeId, ops::Deref, rc::Rc}; /// Components in Dioxus use the "Context" object to interact with their lifecycle. -/// This lets components schedule updates, integrate hooks, and expose their context via the context api. /// -/// Properties passed down from the parent component are also directly accessible via the exposed "props" field. +/// This lets components access props, schedule updates, integrate hooks, and expose shared state. +/// +/// Note: all of these methods are *imperative* - they do not act as hooks! They are meant to be used by hooks +/// to provide complex behavior. For instance, calling "add_shared_state" on every render is considered a leak. This method +/// exists for the `use_provide_state` hook to provide a shared state object. +/// +/// For the most part, the only method you should be using regularly is `render`. +/// +/// ## Example /// /// ```ignore /// #[derive(Properties)] /// struct Props { /// name: String -/// /// } /// /// fn example(cx: Context) -> VNode { @@ -27,16 +27,6 @@ use std::{ /// } /// } /// ``` -/// -/// ## Available Methods: -/// - render -/// - use_hook -/// - use_task -/// - use_suspense -/// - submit_task -/// - children -/// - use_effect -/// pub struct Context<'src, T> { pub props: &'src T, pub scope: &'src Scope, @@ -123,7 +113,8 @@ impl<'src, P> Context<'src, P> { todo!() } - /// Get's this context's ScopeId + /// Get's this component's unique identifier. + /// pub fn get_scope_id(&self) -> ScopeId { self.scope.our_arena_idx.clone() } @@ -148,11 +139,7 @@ impl<'src, P> Context<'src, P> { lazy_nodes: LazyNodes<'src, F>, ) -> DomTree<'src> { let scope_ref = self.scope; - // let listener_id = &scope_ref.listener_idx; - Some(lazy_nodes.into_vnode(NodeFactory { - scope: scope_ref, - // listener_id, - })) + Some(lazy_nodes.into_vnode(NodeFactory { scope: scope_ref })) } /// `submit_task` will submit the future to be polled. @@ -177,11 +164,14 @@ impl<'src, P> Context<'src, P> { } /// Add a state globally accessible to child components via tree walking - pub fn add_shared_state(self, val: T) -> Option> { + pub fn add_shared_state(self, val: T) { self.scope .shared_contexts .borrow_mut() .insert(TypeId::of::(), Rc::new(val)) + .map(|_| { + log::warn!("A shared state was replaced with itself. This is does not result in a panic, but is probably not what you are trying to do"); + }); } /// Walk the tree to find a shared state with the TypeId of the generic type @@ -212,7 +202,6 @@ impl<'src, P> Context<'src, P> { .clone() .downcast::() .expect("Should not fail, already validated the type from the hashmap"); - par = Some(rc); break; } else { diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 45a8ac630..e872a6232 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -55,7 +55,7 @@ use crate::{arena::SharedResources, innerlude::*}; use fxhash::{FxHashMap, FxHashSet}; use smallvec::{smallvec, SmallVec}; -use std::{any::Any, borrow::Borrow, cmp::Ordering}; +use std::{any::Any, cmp::Ordering, process::Child}; /// 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 @@ -71,31 +71,20 @@ pub trait RealDom<'a> { 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> 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)); - - 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 - unsafe { self.vdom.get_scope(*id) } - } -} - impl<'real, 'bump> DiffMachine<'real, 'bump> { pub fn new( edits: &'real mut Vec>, @@ -121,20 +110,17 @@ 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)) => { - // 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 - )); + let root = root.unwrap(); if old.text != new.text { self.edits.push_root(root); - log::debug!("Text has changed {}, {}", old.text, new.text); self.edits.set_text(new.text); self.edits.pop(); } @@ -143,16 +129,12 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { } (VNodeKind::Element(old), VNodeKind::Element(new)) => { - // 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 - )); + let root = root.unwrap(); // 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 // - // In Dioxus, this is less likely to occur unless through a fragment + // 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); let meta = self.create(new_node); @@ -164,59 +146,105 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { 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(root); - + // Don't push the root if we don't have to let mut has_comitted = false; - self.edits.push_root(root); - // dbg!("diffing listeners"); - self.diff_listeners(&mut has_comitted, old.listeners, new.listeners); - // dbg!("diffing attrs"); - self.diff_attr( - &mut has_comitted, - old.attributes, - new.attributes, - new.namespace, - ); - // dbg!("diffing childrne"); - self.diff_children(&mut has_comitted, old.children, new.children); - self.edits.pop(); - // if has_comitted { - // self.edits.pop(); - // } + let mut please_commit = |edits: &mut DomEditor| { + if !has_comitted { + has_comitted = true; + edits.push_root(root); + } + }; + + // Diff Attributes + // + // It's extraordinarily rare to have the number/order of attributes change + // In these cases, we just completely erase the old set and make a new set + // + // TODO: take a more efficient path than this + 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); + self.edits.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); + } + for attribute in new.attributes { + self.edits.set_attribute(attribute) + } + } + + // Diff listeners + // + // 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 + // + // TODO: take a more efficient path than this + 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.edits.remove_event_listener(old_l.event); + self.edits.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); + } + for listener in new.listeners { + listener.mounted_node.set(Some(root)); + self.edits.new_event_listener(listener); + } + } + + if has_comitted { + self.edits.pop(); + } + + // Each child pushes its own root, so it doesn't need our current root + todo!(); + // self.diff_children(old.children, new.children); } (VNodeKind::Component(old), VNodeKind::Component(new)) => { - log::warn!("diffing components? {:#?}", new.user_fc); + let scope_addr = old.ass_scope.get().unwrap(); + + // Make sure we're dealing with the same component (by function pointer) if old.user_fc == new.user_fc { - // Make sure we're dealing with the same component (by function pointer) - self.cur_idxs.push(old.ass_scope.get().unwrap()); + // + self.cur_idxs.push(scope_addr); // Make sure the new component vnode is referencing the right 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.get_scope_mut(&scope_addr).unwrap(); - scope.caller = new.caller.clone(); - - // ack - this doesn't work on its own! - scope.update_children(ScopeChildren(new.children)); + scope + .update_scope_dependencies(new.caller.clone(), ScopeChildren(new.children)); // React doesn't automatically memoize, but we do. - let are_the_same = match old.comparator { - Some(comparator) => comparator(new), - None => false, - }; + let compare = old.comparator.unwrap(); - if !are_the_same { - scope.run_scope().unwrap(); - self.diff_node(scope.frames.wip_head(), scope.frames.fin_head()); - } else { - // + match compare(new) { + true => { + // the props are the same... + } + false => { + // the props are different... + scope.run_scope().unwrap(); + self.diff_node(scope.frames.wip_head(), scope.frames.fin_head()); + } } + self.cur_idxs.pop(); self.seen_nodes.insert(scope_addr); @@ -254,14 +282,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { return; } - // Diff using the approach where we're looking for added or removed nodes. - if old.children.len() != new.children.len() {} - - // Diff where we think the elements are the same - if old.children.len() == new.children.len() {} - - let mut has_comitted = false; - self.diff_children(&mut has_comitted, old.children, new.children); + self.diff_children(old, new, old, new_anchor) } // The strategy here is to pick the first possible node from the previous set and use that as our replace with root @@ -329,25 +350,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. -pub struct CreateMeta { - pub is_static: bool, - pub 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> DiffMachine<'real, 'bump> { // Emit instructions to create the given virtual node. // // The change list stack may have any shape upon entering this function: @@ -395,11 +398,10 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { }; node.dom_id.set(Some(real_id)); - listeners.iter().enumerate().for_each(|(idx, listener)| { + 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.event, listener.scope, idx, real_id); + self.edits.new_event_listener(listener); // if the node has an event listener, then it must be visited ? is_static = false; @@ -407,8 +409,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { for attr in *attributes { is_static = is_static && attr.is_static; - self.edits - .set_attribute(&attr.name, &attr.value, *namespace); + self.edits.set_attribute(attr); } // Fast path: if there is a single text child, it is faster to @@ -526,9 +527,25 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { } } } -} -impl<'a, 'bump> DiffMachine<'a, 'bump> { + fn create_children(&mut self, children: &'bump [VNode<'bump>]) -> CreateMeta { + let mut is_static = true; + let mut added_to_stack = 0; + + for child in children { + let child_meta = self.create(child); + is_static = is_static && child_meta.is_static; + added_to_stack += child_meta.added_to_stack; + } + + CreateMeta { + is_static, + added_to_stack, + } + } + + 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. @@ -561,133 +578,6 @@ impl<'a, 'bump> DiffMachine<'a, 'bump> { } } - // Diff event listeners between `old` and `new`. - // - // The listeners' node must be on top of the change list stack: - // - // [... node] - // - // The change list stack is left unchanged. - fn diff_listeners(&mut self, committed: &mut bool, old: &[Listener<'_>], new: &[Listener<'_>]) { - if !old.is_empty() || !new.is_empty() { - // self.edits.commit_traversal(); - } - // TODO - // what does "diffing listeners" even mean? - - for (old_l, new_l) in old.iter().zip(new.iter()) { - log::info!( - "moving listener forward with event. old: {:#?}", - old_l.mounted_node.get() - ); - new_l.mounted_node.set(old_l.mounted_node.get()); - } - // 'outer1: for (_l_idx, new_l) in new.iter().enumerate() { - // // go through each new listener - // // find its corresponding partner in the old list - // // if any characteristics changed, remove and then re-add - - // // if nothing changed, then just move on - // let _event_type = new_l.event; - - // for old_l in old { - // if new_l.event == old_l.event { - // log::info!( - // "moving listener forward with event. old: {:#?}", - // old_l.mounted_node.get() - // ); - // 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 ElementId - // // // self.edits - // // // .update_event_listener(event_type, new_l.scope, new_l.id) - // // } - - // continue 'outer1; - // } - // } - - // self.edits - // .new_event_listener(event_type, new_l.scope, new_l.id); - // } - - // 'outer2: for old_l in old { - // for new_l in new { - // if new_l.event == old_l.event { - // continue 'outer2; - // } - // } - // self.edits.remove_event_listener(old_l.event); - // } - } - - // Diff a node's attributes. - // - // The attributes' node must be on top of the change list stack: - // - // [... node] - // - // The change list stack is left unchanged. - fn diff_attr( - &mut self, - committed: &mut bool, - old: &'bump [Attribute<'bump>], - new: &'bump [Attribute<'bump>], - namespace: Option<&'static str>, - ) { - for (old_attr, new_attr) in old.iter().zip(new.iter()) { - if old_attr.value != new_attr.value { - if !*committed { - *committed = true; - // self.edits.push_root(); - } - } - // if old_attr.name == new_attr.name { - // } - } - // Do O(n^2) passes to add/update and remove attributes, since - // there are almost always very few attributes. - // - // The "fast" path is when the list of attributes name is identical and in the same order - // With the Rsx and Html macros, this will almost always be the case - // 'outer: for new_attr in new { - // if new_attr.is_volatile { - // // self.edits.commit_traversal(); - // self.edits - // .set_attribute(new_attr.name, new_attr.value, namespace); - // } else { - // for old_attr in old { - // if old_attr.name == new_attr.name { - // if old_attr.value != new_attr.value { - // // self.edits.commit_traversal(); - // self.edits - // .set_attribute(new_attr.name, new_attr.value, namespace); - // } - // continue 'outer; - // } else { - // // names are different, a varying order of attributes has arrived - // } - // } - - // // self.edits.commit_traversal(); - // self.edits - // .set_attribute(new_attr.name, new_attr.value, namespace); - // } - // } - - // 'outer2: for old_attr in old { - // for new_attr in new { - // if old_attr.name == new_attr.name { - // continue 'outer2; - // } - // } - - // // self.edits.commit_traversal(); - // self.edits.remove_attribute(old_attr.name); - // } - } - // Diff the given set of old and new children. // // The parent must be on top of the change list stack when this function is @@ -696,72 +586,98 @@ impl<'a, 'bump> DiffMachine<'a, 'bump> { // [... parent] // // the change list stack is in the same state when this function returns. + // + // 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, - committed: &mut bool, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>], + old_anchor: &mut Option, + new_anchor: &mut Option, ) { - // if new.is_empty() { - // if !old.is_empty() { - // // self.edits.commit_traversal(); - // self.remove_all_children(old); - // } - // return; - // } + const IS_EMPTY: bool = true; + const IS_NOT_EMPTY: bool = false; - // if new.len() == 1 { - // match (&old.first(), &new[0]) { - // (Some(VNodeKind::Text(old_vtext)), VNodeKind::Text(new_vtext)) - // if old_vtext.text == new_vtext.text => - // { - // // Don't take this fast path... - // } + match (old_anchor, new.is_empty()) { + // Both are empty, dealing only with potential anchors + (Some(_), IS_EMPTY) => { + *new_anchor = *old_anchor; + if old.len() > 0 { + // clean up these virtual nodes (components, fragments, etc) + } + } - // (_, VNodeKind::Text(text)) => { - // // self.edits.commit_traversal(); - // log::debug!("using optimized text set"); - // self.edits.set_text(text.text); - // return; - // } + // 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.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; + } + } - // todo: any more optimizations - // (_, _) => {} - // } - // } + // 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); + } + }, - // if old.is_empty() { - // if !new.is_empty() { - // // self.edits.commit_traversal(); - // self.create_and_append_children(new); - // } - // return; - // } + // 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.edits.push_root(to_replace.dom_id.get().unwrap()); - // let new_is_keyed = new[0].key.is_some(); - // let old_is_keyed = old[0].key.is_some(); + // Create the anchor + let anchor_id = self.vdom.reserve_node(); + self.edits.create_placeholder(anchor_id); + *new_anchor = Some(anchor_id); - // 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" - // ); + // Replace that node + self.edits.replace_with(1); + } else { + // no real nodes - + *new_anchor = *old_anchor; + } - // if new_is_keyed && old_is_keyed { - // // log::warn!("using the wrong approach"); - // self.diff_non_keyed_children(old, new); - // // todo!("Not yet implemented a migration away from temporaries"); - // // let t = self.edits.next_temporary(); - // // self.diff_keyed_children(old, new); - // // self.edits.set_next_temporary(t); - // } else { - // // log::debug!("diffing non keyed children"); - // self.diff_non_keyed_children(old, new); - // } - self.diff_non_keyed_children(old, new); + // remove the rest + for child in &old[1..] { + self.edits.push_root(child.element_id().unwrap()); + self.edits.remove(); + } + } + + (None, IS_NOT_EMPTY) => { + let new_is_keyed = new[0].key.is_some(); + let old_is_keyed = old[0].key.is_some(); + + debug_assert!( + new.iter().all(|n| n.key.is_some() == new_is_keyed), + "all siblings must be keyed or all siblings must be non-keyed" + ); + debug_assert!( + old.iter().all(|o| o.key.is_some() == old_is_keyed), + "all siblings must be keyed or all siblings must be non-keyed" + ); + + if new_is_keyed && old_is_keyed { + self.diff_keyed_children(old, new); + } else { + self.diff_non_keyed_children(old, new); + } + } + } } // Diffing "keyed" children. @@ -1280,6 +1196,35 @@ impl<'a, 'bump> DiffMachine<'a, 'bump> { todo!() // self.edits.remove_self_and_next_siblings(); } + + 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)); + + unsafe { self.vdom.get_scope_mut(*id) } + } + pub fn get_scope(&mut self, id: &ScopeId) -> Option<&'bump Scope> { + // ensure we haven't seen this scope before + // if we have, then we're trying to alias it, which is not allowed + unsafe { self.vdom.get_scope(*id) } + } +} + +// 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. +pub struct CreateMeta { + pub is_static: bool, + pub 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, + } + } } enum KeyedPrefixResult { @@ -1291,6 +1236,20 @@ enum KeyedPrefixResult { MoreWorkToDo(usize), } +fn find_first_real_node<'a>( + nodes: &'a [VNode<'a>], + scopes: &'a SharedResources, +) -> Option<&'a VNode<'a>> { + for node in nodes { + let iter = RealChildIterator::new(node, scopes); + if let Some(node) = iter.next() { + return Some(node); + } + } + + None +} + /// 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 diff --git a/packages/core/src/editor.rs b/packages/core/src/editor.rs index 5e8a685c8..e51d564d7 100644 --- a/packages/core/src/editor.rs +++ b/packages/core/src/editor.rs @@ -5,7 +5,10 @@ //! //! -use crate::{innerlude::ScopeId, ElementId}; +use crate::{ + innerlude::{Attribute, Listener, ScopeId}, + ElementId, +}; /// The `DomEditor` provides an imperative interface for the Diffing algorithm to plan out its changes. /// @@ -86,18 +89,20 @@ impl<'real, 'bump> DomEditor<'real, 'bump> { } // events - pub(crate) fn new_event_listener( - &mut self, - event: &'static str, - scope: ScopeId, - element_id: usize, - realnode: ElementId, - ) { - self.edits.push(NewEventListener { + 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, - element_id, - mounted_node_id: realnode.as_u64(), + mounted_node_id: element_id, }); } @@ -113,17 +118,27 @@ impl<'real, 'bump> DomEditor<'real, 'bump> { } #[inline] - pub(crate) fn set_attribute( - &mut self, - field: &'static str, - value: &'bump str, - ns: Option<&'static str>, - ) { - self.edits.push(SetAttribute { field, value, ns }); + 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, name: &'static str) { + pub(crate) fn remove_attribute(&mut self, attribute: &Attribute) { + let name = attribute.name; self.edits.push(RemoveAttribute { name }); } } @@ -169,7 +184,6 @@ pub enum DomEdit<'bump> { event_name: &'static str, scope: ScopeId, mounted_node_id: u64, - element_id: usize, }, RemoveEventListener { event: &'static str, diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index b8e68b7ef..c2a4c83a4 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -117,6 +117,16 @@ pub struct Listener<'bump> { pub(crate) callback: RefCell>>, } +impl Listener<'_> { + // serialize the listener event stuff to a string + pub fn serialize(&self) { + // + } + pub fn deserialize() { + // + } +} + /// Virtual Components for custom user-defined components /// Only supports the functional syntax pub struct VComponent<'src> { diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index 645f4f238..65bcc1999 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -87,15 +87,12 @@ impl Scope { } } - pub(crate) fn update_caller<'creator_node>(&mut self, caller: Rc) { - self.caller = caller; - } - - pub(crate) fn update_children<'creator_node>( + pub(crate) fn update_scope_dependencies<'creator_node>( &mut self, + caller: Rc, child_nodes: ScopeChildren, - // child_nodes: &'creator_node [VNode<'creator_node>], ) { + self.caller = caller; // let child_nodes = unsafe { std::mem::transmute(child_nodes) }; let child_nodes = unsafe { child_nodes.extend_lifetime() }; self.child_nodes = child_nodes; diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs new file mode 100644 index 000000000..73ce38ada --- /dev/null +++ b/packages/core/tests/diffing.rs @@ -0,0 +1,17 @@ +use dioxus::prelude::*; +use dioxus_core as dioxus; +use dioxus_html as dioxus_elements; + +#[test] +fn diffing_works() {} + +#[test] +fn html_and_rsx_generate_the_same_output() { + let old = rsx! { + div { "Hello world!" } + }; + + // let new = html! { + //
"Hello world!"
+ // }; +} diff --git a/packages/desktop/examples/test.rs b/packages/desktop/examples/test.rs index f18103c71..cebd522b5 100644 --- a/packages/desktop/examples/test.rs +++ b/packages/desktop/examples/test.rs @@ -3,7 +3,7 @@ use dioxus_core::prelude::*; use dioxus_html as dioxus_elements; fn main() { - dioxus_desktop::launch(App, |f| f.with_maximized(true)).expect("Failed"); + dioxus_desktop::launch(App, |f| f.with_window(|w| w.with_maximized(true))).expect("Failed"); } static App: FC<()> = |cx| { diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs new file mode 100644 index 000000000..4895c3e2f --- /dev/null +++ b/packages/desktop/src/cfg.rs @@ -0,0 +1,45 @@ +use std::ops::{Deref, DerefMut}; + +use dioxus_core::DomEdit; +use wry::{ + application::{ + error::OsError, + event_loop::EventLoopWindowTarget, + menu::MenuBar, + window::{Fullscreen, Icon, Window, WindowBuilder}, + }, + webview::{RpcRequest, RpcResponse}, +}; + +pub struct DesktopConfig<'a> { + pub window: WindowBuilder, + pub(crate) manual_edits: Option>, + pub(crate) pre_rendered: Option, +} + +impl DesktopConfig<'_> { + /// Initializes a new `WindowBuilder` with default values. + #[inline] + pub fn new() -> Self { + Self { + window: Default::default(), + pre_rendered: None, + manual_edits: None, + } + } + + pub fn with_prerendered(&mut self, content: String) -> &mut Self { + self.pre_rendered = Some(content); + self + } + + pub fn with_window(&mut self, f: impl FnOnce(WindowBuilder) -> WindowBuilder) -> &mut Self { + // gots to do a swap because the window builder only takes itself as muy self + // I wish more people knew about returning &mut Self + let mut builder = WindowBuilder::default(); + std::mem::swap(&mut self.window, &mut builder); + builder = f(builder); + std::mem::swap(&mut self.window, &mut builder); + self + } +} diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html index bbe3d2d84..7ce555efa 100644 --- a/packages/desktop/src/index.html +++ b/packages/desktop/src/index.html @@ -2,208 +2,219 @@ -
+ 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] + } +}