wip: making progress on diffing and hydration

This commit is contained in:
Jonathan Kelley 2021-07-28 21:46:53 -04:00
parent e5c88fe3a4
commit 49856ccd68
34 changed files with 1023 additions and 750 deletions

View file

@ -19,7 +19,7 @@ dioxus-mobile = { path = "./packages/mobile", optional = true }
[features] [features]
# core # core
default = ["core", "ssr", "desktop"] default = ["core", "ssr"]
core = ["macro", "hooks", "html"] core = ["macro", "hooks", "html"]
macro = ["dioxus-core-macro"] macro = ["dioxus-core-macro"]
hooks = ["dioxus-hooks"] hooks = ["dioxus-hooks"]
@ -37,18 +37,31 @@ mobile = ["dioxus-mobile"]
[dev-dependencies] [dev-dependencies]
futures = "0.3.15" futures-util = "0.3.16"
log = "0.4.14" log = "0.4.14"
num-format = "0.4.0" num-format = "0.4.0"
separator = "0.4.1" separator = "0.4.1"
serde = { version = "1.0.126", features = ["derive"] } 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 = "*" env_logger = "*"
async-std = { version = "1.9.0", features = ["attributes"] } async-std = { version = "1.9.0", features = ["attributes"] }
im-rc = "15.0.0"
rand = { version = "0.8.4", features = ["small_rng"] } 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" 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] [workspace]
members = [ members = [

View file

@ -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 | | Custom elements | ✅ | ✅ | Define new element primitives |
| Suspense | ✅ | ✅ | schedule future render from future/promise | | Suspense | ✅ | ✅ | schedule future render from future/promise |
| Integrated error handling | ✅ | ✅ | Gracefully handle errors with ? syntax | | 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 | | Cooperative Scheduling | 🛠 | ✅ | Prioritize important events over non-important events |
| Runs natively | ✅ | ❓ | runs as a portable binary w/o a runtime (Node) | | Runs natively | ✅ | ❓ | runs as a portable binary w/o a runtime (Node) |
| 1st class global state | ✅ | ❓ | redux/recoil/mobx on top of context | | 1st class global state | ✅ | ❓ | redux/recoil/mobx on top of context |

View file

@ -12,8 +12,10 @@ use std::fs::{self, DirEntry};
fn main() { fn main() {
env_logger::init(); env_logger::init();
dioxus::desktop::launch(App, |c| { dioxus::desktop::launch(App, |c| {
c.with_resizable(false) c.with_window(|w| {
.with_inner_size(LogicalSize::new(800.0, 400.0)) w.with_resizable(false)
.with_inner_size(LogicalSize::new(800.0, 400.0))
})
}) })
.unwrap(); .unwrap();
} }

33
examples/hydration.rs Normal file
View file

@ -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
}
}
})
};

View file

@ -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 //! 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. //! RefMuts at the same time.
use dioxus::desktop::wry::application::dpi::LogicalSize;
use dioxus::events::on::*; use dioxus::events::on::*;
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_desktop::wry::application::dpi::LogicalSize;
const STYLE: &str = include_str!("./assets/calculator.css"); const STYLE: &str = include_str!("./assets/calculator.css");
fn main() { fn main() {
env_logger::init(); env_logger::init();
dioxus::desktop::launch(App, |cfg| { dioxus::desktop::launch(App, |cfg| {
cfg.with_title("Calculator Demo") cfg.with_window(|w| {
.with_resizable(false) w.with_title("Calculator Demo")
.with_inner_size(LogicalSize::new(320.0, 530.0)) .with_resizable(false)
.with_inner_size(LogicalSize::new(320.0, 530.0))
})
}) })
.expect("failed to launch dioxus app"); .expect("failed to launch dioxus app");
} }

View file

@ -30,8 +30,8 @@ pub static Example: FC<()> = |cx| {
// Tasks are 'static, so we need to copy relevant items in // Tasks are 'static, so we need to copy relevant items in
let (async_count, dir) = (count.for_async(), *direction); let (async_count, dir) = (count.for_async(), *direction);
let (task, result) = use_task(cx, move || async move { let (task, result) = use_task(cx, move || async move {
// Count infinitely!
loop { loop {
gloo_timers::future::TimeoutFuture::new(250).await; gloo_timers::future::TimeoutFuture::new(250).await;
*async_count.get_mut() += dir; *async_count.get_mut() += dir;

View file

@ -7,7 +7,7 @@ pub static Example: FC<()> = |cx| {
// This is an easy/low hanging fruit to improve upon // This is an easy/low hanging fruit to improve upon
let mut dom = VirtualDom::new(SomeApp); let mut dom = VirtualDom::new(SomeApp);
dom.rebuild_in_place().unwrap(); dom.rebuild_in_place().unwrap();
ssr::render_vdom(&dom) ssr::render_vdom(&dom, |c| c)
}); });
cx.render(rsx! { cx.render(rsx! {

View file

@ -1,13 +1,15 @@
#![allow(non_upper_case_globals)]
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus::ssr; use dioxus::ssr;
fn main() { fn main() {
let mut vdom = VirtualDom::new(App); let mut vdom = VirtualDom::new(App);
vdom.rebuild_in_place(); vdom.rebuild_in_place().expect("Rebuilding failed");
println!("{}", ssr::render_vdom(&vdom)); println!("{}", ssr::render_vdom(&vdom, |c| c));
} }
const App: FC<()> = |cx| { static App: FC<()> = |cx| {
cx.render(rsx!( cx.render(rsx!(
div { div {
h1 { "Title" } h1 { "Title" }

View file

@ -3,10 +3,12 @@ use dioxus::prelude::*;
fn main() { fn main() {
use dioxus::desktop::wry::application::platform::macos::*; use dioxus::desktop::wry::application::platform::macos::*;
dioxus::desktop::launch(App, |c| { dioxus::desktop::launch(App, |c| {
c.with_fullsize_content_view(true) c.with_window(|w| {
.with_titlebar_buttons_hidden(false) w.with_fullsize_content_view(true)
.with_titlebar_transparent(true) .with_titlebar_buttons_hidden(false)
.with_movable_by_window_background(true) .with_titlebar_transparent(true)
.with_movable_by_window_background(true)
})
}); });
} }

View file

@ -1,14 +1,10 @@
#![allow(non_upper_case_globals, non_snake_case)]
use dioxus::prelude::*; use dioxus::prelude::*;
use im_rc::HashMap; use im_rc::HashMap;
use std::rc::Rc; use std::rc::Rc;
fn main() { fn main() -> anyhow::Result<()> {
#[cfg(feature = "desktop")] dioxus::desktop::launch(App, |c| c)
// #[cfg(not(target_arch = "wasm32"))]
dioxus::desktop::launch(App, |c| c);
#[cfg(feature = "desktop")]
dioxus::web::launch(App, |c| c);
} }
#[derive(PartialEq)] #[derive(PartialEq)]

View file

@ -8,12 +8,13 @@
//! into the native VDom instance. //! into the native VDom instance.
//! //!
//! Currently, NodeRefs won't work properly, but all other event functionality will. //! 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::*}; use dioxus::{events::on::MouseEvent, prelude::*};
fn main() { fn main() -> anyhow::Result<()> {
env_logger::init(); env_logger::init();
dioxus::desktop::launch(App, |c| c); dioxus::desktop::launch(App, |c| c)
} }
static App: FC<()> = |cx| { static App: FC<()> = |cx| {

View file

@ -1,3 +1,4 @@
#![allow(non_upper_case_globals, non_snake_case)]
//! Example: Webview Renderer //! Example: Webview Renderer
//! ------------------------- //! -------------------------
//! //!
@ -11,53 +12,19 @@
use dioxus::prelude::*; use dioxus::prelude::*;
// #[cfg]
fn main() { fn main() {
// env_logger::init();
dioxus::web::launch(App, |c| c); dioxus::web::launch(App, |c| c);
} }
static App: FC<()> = |cx| { 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 mut count = use_state(cx, || 0);
// let d = Dropper { name: "asd" };
// let handler = move |_| {
// dbg!(d.name);
// };
cx.render(rsx! { cx.render(rsx! {
div { div {
h1 { "Hifive counter: {count}" } h1 { "Hifive counter: {count}" }
{cx.children()} {cx.children()}
button { onclick: move |_| count += 1, "Up high!" } button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" } 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");
// }
// }

View file

@ -40,6 +40,10 @@ smallvec = "1.6.1"
slab = "0.4.3" slab = "0.4.3"
[dev-dependencies]
dioxus-html = { path = "../html" }
[features] [features]
default = ["serialize"] default = ["serialize"]
serialize = ["serde"] serialize = ["serde"]

View file

@ -1,9 +1,7 @@
fn main() {}
use dioxus::*;
use dioxus_core as dioxus;
use dioxus_core::prelude::*; use dioxus_core::prelude::*;
fn main() {}
pub static Example: FC<()> = |cx| { pub static Example: FC<()> = |cx| {
let list = (0..10).map(|f| LazyNodes::new(move |f| todo!())); let list = (0..10).map(|f| LazyNodes::new(move |f| todo!()));

View file

@ -1,4 +1,5 @@
use std::cell::{RefCell, RefMut}; use std::cell::{RefCell, RefMut};
use std::fmt::Display;
use std::{cell::UnsafeCell, rc::Rc}; use std::{cell::UnsafeCell, rc::Rc};
use crate::heuristics::*; use crate::heuristics::*;
@ -17,6 +18,11 @@ pub struct ScopeId(pub usize);
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct ElementId(pub usize); 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 { impl ElementId {
pub fn as_u64(self) -> u64 { pub fn as_u64(self) -> u64 {

View file

@ -1,24 +1,24 @@
use crate::innerlude::*; //! Public APIs for managing component state, tasks, and lifecycles.
use futures_util::FutureExt; use crate::innerlude::*;
use std::{ use std::{any::TypeId, ops::Deref, rc::Rc};
any::{Any, TypeId},
cell::{Cell, RefCell},
future::Future,
ops::Deref,
rc::Rc,
};
/// Components in Dioxus use the "Context" object to interact with their lifecycle. /// 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 /// ```ignore
/// #[derive(Properties)] /// #[derive(Properties)]
/// struct Props { /// struct Props {
/// name: String /// name: String
///
/// } /// }
/// ///
/// fn example(cx: Context<Props>) -> VNode { /// fn example(cx: Context<Props>) -> 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 struct Context<'src, T> {
pub props: &'src T, pub props: &'src T,
pub scope: &'src Scope, pub scope: &'src Scope,
@ -123,7 +113,8 @@ impl<'src, P> Context<'src, P> {
todo!() todo!()
} }
/// Get's this context's ScopeId /// Get's this component's unique identifier.
///
pub fn get_scope_id(&self) -> ScopeId { pub fn get_scope_id(&self) -> ScopeId {
self.scope.our_arena_idx.clone() self.scope.our_arena_idx.clone()
} }
@ -148,11 +139,7 @@ impl<'src, P> Context<'src, P> {
lazy_nodes: LazyNodes<'src, F>, lazy_nodes: LazyNodes<'src, F>,
) -> DomTree<'src> { ) -> DomTree<'src> {
let scope_ref = self.scope; let scope_ref = self.scope;
// let listener_id = &scope_ref.listener_idx; Some(lazy_nodes.into_vnode(NodeFactory { scope: scope_ref }))
Some(lazy_nodes.into_vnode(NodeFactory {
scope: scope_ref,
// listener_id,
}))
} }
/// `submit_task` will submit the future to be polled. /// `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 /// Add a state globally accessible to child components via tree walking
pub fn add_shared_state<T: 'static>(self, val: T) -> Option<Rc<dyn Any>> { pub fn add_shared_state<T: 'static>(self, val: T) {
self.scope self.scope
.shared_contexts .shared_contexts
.borrow_mut() .borrow_mut()
.insert(TypeId::of::<T>(), Rc::new(val)) .insert(TypeId::of::<T>(), 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 /// 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() .clone()
.downcast::<T>() .downcast::<T>()
.expect("Should not fail, already validated the type from the hashmap"); .expect("Should not fail, already validated the type from the hashmap");
par = Some(rc); par = Some(rc);
break; break;
} else { } else {

View file

@ -55,7 +55,7 @@ use crate::{arena::SharedResources, innerlude::*};
use fxhash::{FxHashMap, FxHashSet}; use fxhash::{FxHashMap, FxHashSet};
use smallvec::{smallvec, SmallVec}; 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. /// 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 /// 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 struct DiffMachine<'real, 'bump> {
pub real_dom: &'real dyn RealDom<'bump>, pub real_dom: &'real dyn RealDom<'bump>,
pub vdom: &'bump SharedResources, pub vdom: &'bump SharedResources,
pub edits: DomEditor<'real, 'bump>, pub edits: DomEditor<'real, 'bump>,
pub scheduled_garbage: Vec<&'bump VNode<'bump>>, pub scheduled_garbage: Vec<&'bump VNode<'bump>>,
pub cur_idxs: SmallVec<[ScopeId; 5]>, pub cur_idxs: SmallVec<[ScopeId; 5]>,
pub diffed: FxHashSet<ScopeId>, pub diffed: FxHashSet<ScopeId>,
pub seen_nodes: FxHashSet<ScopeId>, pub seen_nodes: FxHashSet<ScopeId>,
} }
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> { impl<'real, 'bump> DiffMachine<'real, 'bump> {
pub fn new( pub fn new(
edits: &'real mut Vec<DomEdit<'bump>>, edits: &'real mut Vec<DomEdit<'bump>>,
@ -121,20 +110,17 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> {
// //
// each function call assumes the stack is fresh (empty). // 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>) { 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) { match (&old_node.kind, &new_node.kind) {
// Handle the "sane" cases first. // Handle the "sane" cases first.
// The rsx and html macros strongly discourage dynamic lists not encapsulated by a "Fragment". // 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. // So the sane (and fast!) cases are where the virtual structure stays the same and is easily diffable.
(VNodeKind::Text(old), VNodeKind::Text(new)) => { (VNodeKind::Text(old), VNodeKind::Text(new)) => {
// currently busted for components - need to fid let root = root.unwrap();
let root = old_node.dom_id.get().expect(&format!(
"Should not be diffing old nodes that were never assigned, {:#?}",
old_node
));
if old.text != new.text { if old.text != new.text {
self.edits.push_root(root); self.edits.push_root(root);
log::debug!("Text has changed {}, {}", old.text, new.text);
self.edits.set_text(new.text); self.edits.set_text(new.text);
self.edits.pop(); self.edits.pop();
} }
@ -143,16 +129,12 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> {
} }
(VNodeKind::Element(old), VNodeKind::Element(new)) => { (VNodeKind::Element(old), VNodeKind::Element(new)) => {
// currently busted for components - need to fid let root = root.unwrap();
let root = old_node.dom_id.get().expect(&format!(
"Should not be diffing old nodes that were never assigned, {:#?}",
old_node
));
// If the element type is completely different, the element needs to be re-rendered completely // If the element type is completely different, the element needs to be re-rendered completely
// This is an optimization React makes due to how users structure their code // This 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 { if new.tag_name != old.tag_name || new.namespace != old.namespace {
self.edits.push_root(root); self.edits.push_root(root);
let meta = self.create(new_node); let meta = self.create(new_node);
@ -164,59 +146,105 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> {
new_node.dom_id.set(Some(root)); new_node.dom_id.set(Some(root));
// push it just in case // Don't push the root if we don't have to
// TODO: remove this - it clogs up things and is inefficient
// self.edits.push_root(root);
let mut has_comitted = false; let mut has_comitted = false;
self.edits.push_root(root); let mut please_commit = |edits: &mut DomEditor| {
// dbg!("diffing listeners"); if !has_comitted {
self.diff_listeners(&mut has_comitted, old.listeners, new.listeners); has_comitted = true;
// dbg!("diffing attrs"); edits.push_root(root);
self.diff_attr( }
&mut has_comitted, };
old.attributes,
new.attributes, // Diff Attributes
new.namespace, //
); // It's extraordinarily rare to have the number/order of attributes change
// dbg!("diffing childrne"); // In these cases, we just completely erase the old set and make a new set
self.diff_children(&mut has_comitted, old.children, new.children); //
self.edits.pop(); // TODO: take a more efficient path than this
// if has_comitted { if old.attributes.len() == new.attributes.len() {
// self.edits.pop(); 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)) => { (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 { 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 // 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)); new.ass_scope.set(Some(scope_addr));
// make sure the component's caller function is up to date // make sure the component's caller function is up to date
let scope = self.get_scope_mut(&scope_addr).unwrap(); let scope = self.get_scope_mut(&scope_addr).unwrap();
scope.caller = new.caller.clone(); scope
.update_scope_dependencies(new.caller.clone(), ScopeChildren(new.children));
// ack - this doesn't work on its own!
scope.update_children(ScopeChildren(new.children));
// React doesn't automatically memoize, but we do. // React doesn't automatically memoize, but we do.
let are_the_same = match old.comparator { let compare = old.comparator.unwrap();
Some(comparator) => comparator(new),
None => false,
};
if !are_the_same { match compare(new) {
scope.run_scope().unwrap(); true => {
self.diff_node(scope.frames.wip_head(), scope.frames.fin_head()); // the props are the same...
} else { }
// 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.cur_idxs.pop();
self.seen_nodes.insert(scope_addr); self.seen_nodes.insert(scope_addr);
@ -254,14 +282,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> {
return; return;
} }
// Diff using the approach where we're looking for added or removed nodes. self.diff_children(old, new, old, new_anchor)
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);
} }
// The strategy here is to pick the first possible node from the previous set and use that as our replace with root // 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. // Emit instructions to create the given virtual node.
// //
// The change list stack may have any shape upon entering this function: // 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)); 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); log::info!("setting listener id to {:#?}", real_id);
listener.mounted_node.set(Some(real_id)); listener.mounted_node.set(Some(real_id));
self.edits self.edits.new_event_listener(listener);
.new_event_listener(listener.event, listener.scope, idx, real_id);
// if the node has an event listener, then it must be visited ? // if the node has an event listener, then it must be visited ?
is_static = false; is_static = false;
@ -407,8 +409,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> {
for attr in *attributes { for attr in *attributes {
is_static = is_static && attr.is_static; is_static = is_static && attr.is_static;
self.edits self.edits.set_attribute(attr);
.set_attribute(&attr.name, &attr.value, *namespace);
} }
// Fast path: if there is a single text child, it is faster to // 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. /// Destroy a scope and all of its descendents.
/// ///
/// Calling this will run the destuctors on all hooks in the tree. /// 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. // Diff the given set of old and new children.
// //
// The parent must be on top of the change list stack when this function is // 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] // [... parent]
// //
// the change list stack is in the same state when this function returns. // 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( fn diff_children(
&mut self, &mut self,
committed: &mut bool,
old: &'bump [VNode<'bump>], old: &'bump [VNode<'bump>],
new: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>],
old_anchor: &mut Option<ElementId>,
new_anchor: &mut Option<ElementId>,
) { ) {
// if new.is_empty() { const IS_EMPTY: bool = true;
// if !old.is_empty() { const IS_NOT_EMPTY: bool = false;
// // self.edits.commit_traversal();
// self.remove_all_children(old);
// }
// return;
// }
// if new.len() == 1 { match (old_anchor, new.is_empty()) {
// match (&old.first(), &new[0]) { // Both are empty, dealing only with potential anchors
// (Some(VNodeKind::Text(old_vtext)), VNodeKind::Text(new_vtext)) (Some(_), IS_EMPTY) => {
// if old_vtext.text == new_vtext.text => *new_anchor = *old_anchor;
// { if old.len() > 0 {
// // Don't take this fast path... // clean up these virtual nodes (components, fragments, etc)
// } }
}
// (_, VNodeKind::Text(text)) => { // Completely adding new nodes, removing any placeholder if it exists
// // self.edits.commit_traversal(); (Some(anchor), IS_NOT_EMPTY) => match old_anchor {
// log::debug!("using optimized text set"); // If there's anchor to work from, then we replace it with the new children
// self.edits.set_text(text.text); Some(anchor) => {
// return; 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() { // Completely removing old nodes and putting an anchor in its place
// if !new.is_empty() { // no anchor (old has nodes) and the new is empty
// // self.edits.commit_traversal(); // remove all the old nodes
// self.create_and_append_children(new); (None, IS_EMPTY) => {
// } // load the first real
// return; 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(); // Create the anchor
// let old_is_keyed = old[0].key.is_some(); let anchor_id = self.vdom.reserve_node();
self.edits.create_placeholder(anchor_id);
*new_anchor = Some(anchor_id);
// debug_assert!( // Replace that node
// new.iter().all(|n| n.key.is_some() == new_is_keyed), self.edits.replace_with(1);
// "all siblings must be keyed or all siblings must be non-keyed" } else {
// ); // no real nodes -
// debug_assert!( *new_anchor = *old_anchor;
// 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 { // remove the rest
// // log::warn!("using the wrong approach"); for child in &old[1..] {
// self.diff_non_keyed_children(old, new); self.edits.push_root(child.element_id().unwrap());
// // todo!("Not yet implemented a migration away from temporaries"); self.edits.remove();
// // let t = self.edits.next_temporary(); }
// // self.diff_keyed_children(old, new); }
// // self.edits.set_next_temporary(t);
// } else { (None, IS_NOT_EMPTY) => {
// // log::debug!("diffing non keyed children"); let new_is_keyed = new[0].key.is_some();
// self.diff_non_keyed_children(old, new); let old_is_keyed = old[0].key.is_some();
// }
self.diff_non_keyed_children(old, new); debug_assert!(
new.iter().all(|n| n.key.is_some() == new_is_keyed),
"all siblings must be keyed or all siblings must be non-keyed"
);
debug_assert!(
old.iter().all(|o| o.key.is_some() == old_is_keyed),
"all siblings must be keyed or all siblings must be non-keyed"
);
if new_is_keyed && old_is_keyed {
self.diff_keyed_children(old, new);
} else {
self.diff_non_keyed_children(old, new);
}
}
}
} }
// Diffing "keyed" children. // Diffing "keyed" children.
@ -1280,6 +1196,35 @@ impl<'a, 'bump> DiffMachine<'a, 'bump> {
todo!() todo!()
// self.edits.remove_self_and_next_siblings(); // 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 { enum KeyedPrefixResult {
@ -1291,6 +1236,20 @@ enum KeyedPrefixResult {
MoreWorkToDo(usize), 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 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 /// This iterator is useful when it's important to load the next real root onto the top of the stack for operations like

View file

@ -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. /// 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 // events
pub(crate) fn new_event_listener( pub(crate) fn new_event_listener(&mut self, listener: &Listener) {
&mut self, let Listener {
event: &'static str, event,
scope: ScopeId,
element_id: usize,
realnode: ElementId,
) {
self.edits.push(NewEventListener {
scope, scope,
mounted_node,
..
} = listener;
let element_id = mounted_node.get().unwrap().as_u64();
self.edits.push(NewEventListener {
scope: scope.clone(),
event_name: event, event_name: event,
element_id, mounted_node_id: element_id,
mounted_node_id: realnode.as_u64(),
}); });
} }
@ -113,17 +118,27 @@ impl<'real, 'bump> DomEditor<'real, 'bump> {
} }
#[inline] #[inline]
pub(crate) fn set_attribute( pub(crate) fn set_attribute(&mut self, attribute: &'bump Attribute) {
&mut self, let Attribute {
field: &'static str, name,
value: &'bump str, value,
ns: Option<&'static str>, is_static,
) { is_volatile,
self.edits.push(SetAttribute { field, value, ns }); namespace,
} = attribute;
// field: &'static str,
// value: &'bump str,
// ns: Option<&'static str>,
self.edits.push(SetAttribute {
field: name,
value,
ns: *namespace,
});
} }
#[inline] #[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 }); self.edits.push(RemoveAttribute { name });
} }
} }
@ -169,7 +184,6 @@ pub enum DomEdit<'bump> {
event_name: &'static str, event_name: &'static str,
scope: ScopeId, scope: ScopeId,
mounted_node_id: u64, mounted_node_id: u64,
element_id: usize,
}, },
RemoveEventListener { RemoveEventListener {
event: &'static str, event: &'static str,

View file

@ -117,6 +117,16 @@ pub struct Listener<'bump> {
pub(crate) callback: RefCell<Option<BumpBox<'bump, dyn FnMut(VirtualEvent) + 'bump>>>, pub(crate) callback: RefCell<Option<BumpBox<'bump, dyn FnMut(VirtualEvent) + 'bump>>>,
} }
impl Listener<'_> {
// serialize the listener event stuff to a string
pub fn serialize(&self) {
//
}
pub fn deserialize() {
//
}
}
/// Virtual Components for custom user-defined components /// Virtual Components for custom user-defined components
/// Only supports the functional syntax /// Only supports the functional syntax
pub struct VComponent<'src> { pub struct VComponent<'src> {

View file

@ -87,15 +87,12 @@ impl Scope {
} }
} }
pub(crate) fn update_caller<'creator_node>(&mut self, caller: Rc<WrappedCaller>) { pub(crate) fn update_scope_dependencies<'creator_node>(
self.caller = caller;
}
pub(crate) fn update_children<'creator_node>(
&mut self, &mut self,
caller: Rc<WrappedCaller>,
child_nodes: ScopeChildren, 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 { std::mem::transmute(child_nodes) };
let child_nodes = unsafe { child_nodes.extend_lifetime() }; let child_nodes = unsafe { child_nodes.extend_lifetime() };
self.child_nodes = child_nodes; self.child_nodes = child_nodes;

View file

@ -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! {
// <div>"Hello world!"</div>
// };
}

View file

@ -3,7 +3,7 @@ use dioxus_core::prelude::*;
use dioxus_html as dioxus_elements; use dioxus_html as dioxus_elements;
fn main() { 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| { static App: FC<()> = |cx| {

View file

@ -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<DomEdit<'a>>,
pub(crate) pre_rendered: Option<String>,
}
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
}
}

View file

@ -2,208 +2,219 @@
<html> <html>
<head> <head>
<script>
class Interpreter {
constructor(root) {
this.root = root;
this.stack = [root];
this.listeners = {
"onclick": {}
};
this.lastNodeWasText = false;
this.nodes = [root, root, root, root];
}
top() {
return this.stack[this.stack.length - 1];
}
pop() {
return this.stack.pop();
}
PushRoot(edit) {
const id = edit.id;
const node = this.nodes[id];
console.log("pushing root ", node, "with id", id);
this.stack.push(node);
}
PopRoot(edit) {
this.stack.pop();
}
AppendChildren(edit) {
let root = this.stack[this.stack.length - (edit.many + 1)];
for (let i = 0; i < edit.many; i++) {
console.log("popping ", i, edit.many);
let node = this.pop();
root.appendChild(node);
}
}
ReplaceWith(edit) {
let root = this.stack[this.stack.length - (edit.many + 1)];
let els = [];
for (let i = 0; i < edit.many; i++) {
els.push(this.pop());
}
root.replaceWith(...els);
}
Remove(edit) {
const node = this.stack.pop();
node.remove();
}
RemoveAllChildren(edit) {}
CreateTextNode(edit) {
const node = document.createTextNode(edit.text);
this.nodes[edit.id] = node;
this.stack.push(node);
}
CreateElement(edit) {
const tagName = edit.tag;
const el = document.createElement(tagName);
this.nodes[edit.id] = el;
console.log(`creating element: `, edit);
this.stack.push(el);
}
CreateElementNs(edit) {
const tagName = edit.tag;
console.log(`creating namespaced element: `, edit);
this.stack.push(document.createElementNS(edit.ns, edit.tag));
}
CreatePlaceholder(edit) {
const a = `this.stack.push(document.createElement(" pre"))`;
this.stack.push(document.createComment("vroot"));
}
NewEventListener(edit) {
const element_id = edit.element_id;
const event_name = edit.event_name;
const mounted_node_id = edit.mounted_node_id;
const scope = edit.scope;
const element = this.top();
element.setAttribute(`dioxus-event-${event_name}`, `${scope}.${mounted_node_id}`);
console.log("listener map is", this.listeners);
if (this.listeners[event_name] === undefined) {
console.log("adding listener!");
this.listeners[event_name] = "bla";
this.root.addEventListener(event_name, (event) => {
const target = event.target;
const type = event.type;
const val = target.getAttribute(`dioxus-event-${event_name}`);
const fields = val.split(".");
const scope_id = parseInt(fields[0]);
const real_id = parseInt(fields[1]);
console.log(`parsed event with scope_id ${scope_id} and real_id ${real_id}`);
rpc.call('user_event', {
event: event_name,
scope: scope_id,
mounted_dom_id: real_id,
}).then((reply) => {
console.log(reply);
this.stack.push(this.root);
for (let x = 0; x < reply.length; x++) {
let edit = reply[x];
console.log(edit);
let f = this[edit.type];
f.call(this, edit);
}
console.log("initiated");
}).catch((err) => {
console.log("failed to initiate", err);
});
});
}
}
RemoveEventListener(edit) {}
SetText(edit) {
this.top().textContent = edit.text;
}
SetAttribute(edit) {
const name = edit.field;
const value = edit.value;
const ns = edit.ns;
const node = this.top(this.stack);
if (ns == "style") {
node.style[name] = value;
} else if (ns !== undefined) {
node.setAttributeNS(ns, name, value);
} else {
node.setAttribute(name, value);
}
if (name === "value") {
node.value = value;
}
if (name === "checked") {
node.checked = true;
}
if (name === "selected") {
node.selected = true;
}
}
RemoveAttribute(edit) {
const name = edit.field;
const node = this.top(this.stack);
node.removeAttribute(name);
if (name === "value") {
node.value = null;
}
if (name === "checked") {
node.checked = false;
}
if (name === "selected") {
node.selected = false;
}
}
}
async function initialize() {
const reply = await rpc.call('initiate');
const interpreter = new Interpreter(window.document.getElementById("_dioxusroot"));
console.log(reply);
for (let x = 0; x < reply.length; x++) {
let edit = reply[x];
console.log(edit);
let f = interpreter[edit.type];
f.call(interpreter, edit);
}
console.log("stack completed: ", interpreter.stack);
}
console.log("initializing...");
initialize();
</script>
</head> </head>
<body> <body>
<div id="_dioxusroot"> <div id="_dioxusroot">
</div> </div>
</body> </body>
<script>
class Interpreter {
constructor(root) {
this.root = root;
this.stack = [root];
this.listeners = {
"onclick": {}
};
this.lastNodeWasText = false;
this.nodes = [root, root, root, root];
}
top() {
return this.stack[this.stack.length - 1];
}
pop() {
return this.stack.pop();
}
PushRoot(edit) {
const id = edit.id;
const node = this.nodes[id];
console.log("pushing root ", node, "with id", id);
this.stack.push(node);
}
PopRoot(edit) {
this.stack.pop();
}
AppendChildren(edit) {
let root = this.stack[this.stack.length - (edit.many + 1)];
for (let i = 0; i < edit.many; i++) {
console.log("popping ", i, edit.many);
let node = this.pop();
root.appendChild(node);
}
}
ReplaceWith(edit) {
let root = this.stack[this.stack.length - (edit.many + 1)];
let els = [];
for (let i = 0; i < edit.many; i++) {
els.push(this.pop());
}
root.replaceWith(...els);
}
Remove(edit) {
const node = this.stack.pop();
node.remove();
}
RemoveAllChildren(edit) {}
CreateTextNode(edit) {
const node = document.createTextNode(edit.text);
this.nodes[edit.id] = node;
this.stack.push(node);
}
CreateElement(edit) {
const tagName = edit.tag;
const el = document.createElement(tagName);
this.nodes[edit.id] = el;
console.log(`creating element: `, edit);
this.stack.push(el);
}
CreateElementNs(edit) {
const tagName = edit.tag;
console.log(`creating namespaced element: `, edit);
this.stack.push(document.createElementNS(edit.ns, edit.tag));
}
CreatePlaceholder(edit) {
const a = `this.stack.push(document.createElement(" pre"))`;
this.stack.push(document.createComment("vroot"));
}
NewEventListener(edit) {
const element_id = edit.element_id;
const event_name = edit.event_name;
const mounted_node_id = edit.mounted_node_id;
const scope = edit.scope;
const element = this.top();
element.setAttribute(`dioxus-event-${event_name}`, `${scope}.${mounted_node_id}`);
console.log("listener map is", this.listeners);
if (this.listeners[event_name] === undefined) {
console.log("adding listener!");
this.listeners[event_name] = "bla";
this.root.addEventListener(event_name, (event) => {
const target = event.target;
const type = event.type;
const val = target.getAttribute(`dioxus-event-${event_name}`);
const fields = val.split(".");
const scope_id = parseInt(fields[0]);
const real_id = parseInt(fields[1]);
console.log(`parsed event with scope_id ${scope_id} and real_id ${real_id}`);
rpc.call('user_event', {
event: event_name,
scope: scope_id,
mounted_dom_id: real_id,
}).then((reply) => {
console.log(reply);
this.stack.push(this.root);
let edits = reply.edits;
for (let x = 0; x < edits.length; x++) {
let edit = edits[x];
console.log(edit);
let f = this[edit.type];
f.call(this, edit);
}
console.log("initiated");
}).catch((err) => {
console.log("failed to initiate", err);
});
});
}
}
RemoveEventListener(edit) {}
SetText(edit) {
this.top().textContent = edit.text;
}
SetAttribute(edit) {
const name = edit.field;
const value = edit.value;
const ns = edit.ns;
const node = this.top(this.stack);
if (ns == "style") {
node.style[name] = value;
} else if (ns !== undefined) {
node.setAttributeNS(ns, name, value);
} else {
node.setAttribute(name, value);
}
if (name === "value") {
node.value = value;
}
if (name === "checked") {
node.checked = true;
}
if (name === "selected") {
node.selected = true;
}
}
RemoveAttribute(edit) {
const name = edit.field;
const node = this.top(this.stack);
node.removeAttribute(name);
if (name === "value") {
node.value = null;
}
if (name === "checked") {
node.checked = false;
}
if (name === "selected") {
node.selected = false;
}
}
}
async function initialize() {
const reply = await rpc.call('initiate');
let root = window.document.getElementById("_dioxusroot");
const interpreter = new Interpreter(root);
console.log(reply);
let pre_rendered = reply.pre_rendered;
if (pre_rendered !== undefined) {
root.innerHTML = pre_rendered;
}
const edits = reply.edits;
for (let x = 0; x < edits.length; x++) {
let edit = edits[x];
console.log(edit);
let f = interpreter[edit.type];
f.call(interpreter, edit);
}
console.log("stack completed: ", interpreter.stack);
}
console.log("initializing...");
initialize();
</script>
</html> </html>

View file

@ -3,6 +3,7 @@ use std::ops::{Deref, DerefMut};
use std::sync::mpsc::channel; use std::sync::mpsc::channel;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use cfg::DesktopConfig;
use dioxus_core::*; use dioxus_core::*;
pub use wry; pub use wry;
@ -15,6 +16,7 @@ use wry::{
webview::{RpcRequest, RpcResponse}, webview::{RpcRequest, RpcResponse},
}; };
mod cfg;
mod dom; mod dom;
mod escape; mod escape;
mod events; mod events;
@ -24,14 +26,14 @@ static HTML_CONTENT: &'static str = include_str!("./index.html");
pub fn launch( pub fn launch(
root: FC<()>, root: FC<()>,
builder: impl FnOnce(WindowBuilder) -> WindowBuilder, builder: impl for<'a, 'b> FnOnce(&'b mut DesktopConfig<'a>) -> &'b mut DesktopConfig<'a>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
launch_with_props(root, (), builder) launch_with_props(root, (), builder)
} }
pub fn launch_with_props<P: Properties + 'static>( pub fn launch_with_props<P: Properties + 'static>(
root: FC<P>, root: FC<P>,
props: P, props: P,
builder: impl FnOnce(WindowBuilder) -> WindowBuilder, builder: impl for<'a, 'b> FnOnce(&'b mut DesktopConfig<'a>) -> &'b mut DesktopConfig<'a>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
WebviewRenderer::run(root, props, builder) WebviewRenderer::run(root, props, builder)
} }
@ -46,11 +48,17 @@ enum RpcEvent<'a> {
Initialize { edits: Vec<DomEdit<'a>> }, Initialize { edits: Vec<DomEdit<'a>> },
} }
#[derive(Serialize)]
struct Response<'a> {
pre_rendered: Option<String>,
edits: Vec<DomEdit<'a>>,
}
impl<T: Properties + 'static> WebviewRenderer<T> { impl<T: Properties + 'static> WebviewRenderer<T> {
pub fn run( pub fn run(
root: FC<T>, root: FC<T>,
props: T, 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<()> { ) -> anyhow::Result<()> {
Self::run_with_edits(root, props, user_builder, None) Self::run_with_edits(root, props, user_builder, None)
} }
@ -58,13 +66,22 @@ impl<T: Properties + 'static> WebviewRenderer<T> {
pub fn run_with_edits( pub fn run_with_edits(
root: FC<T>, root: FC<T>,
props: T, 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<Vec<DomEdit<'static>>>, redits: Option<Vec<DomEdit<'static>>>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
log::info!("hello edits"); log::info!("hello edits");
let event_loop = EventLoop::new(); 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); let vir = VirtualDom::new_with_props(root, props);
@ -73,8 +90,6 @@ impl<T: Properties + 'static> WebviewRenderer<T> {
// let registry = Arc::new(RwLock::new(Some(WebviewRegistry::new()))); // let registry = Arc::new(RwLock::new(Some(WebviewRegistry::new())));
let webview = WebViewBuilder::new(window)? let webview = WebViewBuilder::new(window)?
// .with_visible(false)
// .with_transparent(true)
.with_url(&format!("data:text/html,{}", HTML_CONTENT))? .with_url(&format!("data:text/html,{}", HTML_CONTENT))?
.with_rpc_handler(move |_window: &Window, mut req: RpcRequest| { .with_rpc_handler(move |_window: &Window, mut req: RpcRequest| {
match req.method.as_str() { match req.method.as_str() {
@ -87,17 +102,32 @@ impl<T: Properties + 'static> WebviewRenderer<T> {
// Create the thin wrapper around the registry to collect the edits into // Create the thin wrapper around the registry to collect the edits into
let mut real = dom::WebviewDom::new(); let mut real = dom::WebviewDom::new();
let pre = pre_rendered.clone();
// Serialize the edit stream let response = match pre {
let edits = { Some(content) => {
let mut edits = Vec::new(); lock.rebuild_in_place().unwrap();
lock.rebuild(&mut real, &mut edits).unwrap();
serde_json::to_value(edits).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 serde_json::to_value(&response).unwrap()
// *reg_lock = Some(real.consume());
edits
}; };
// Return the edits into the webview runtime // Return the edits into the webview runtime
@ -128,13 +158,18 @@ impl<T: Properties + 'static> WebviewRenderer<T> {
lock.progress_with_event(&mut real, &mut edits) lock.progress_with_event(&mut real, &mut edits)
.await .await
.expect("failed to progress"); .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 // Give back the registry into its slot
// *reg_lock = Some(real.consume()); // *reg_lock = Some(real.consume());
// Return the edits into the webview runtime // 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 response

View file

@ -16,3 +16,9 @@ vdom.rebuild_in_place();
let text = dioxus_ssr::render_root(&vdom); let text = dioxus_ssr::render_root(&vdom);
assert_eq!(text, "<div>hello world!</div>") assert_eq!(text, "<div>hello world!</div>")
``` ```
## Pre-rendering

View file

@ -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() {}

View file

@ -22,7 +22,7 @@ async fn main() -> Result<(), std::io::Error> {
let dom = VirtualDom::launch_with_props_in_place(Example, ExampleProps { initial_name }); let dom = VirtualDom::launch_with_props_in_place(Example, ExampleProps { initial_name });
Ok(Response::builder(200) 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) .content_type(tide::http::mime::HTML)
.build()) .build())
}); });

View file

@ -15,8 +15,11 @@ fn main() {
let mut dom = VirtualDom::new(App); let mut dom = VirtualDom::new(App);
dom.rebuild_in_place().expect("failed to run virtualdom"); dom.rebuild_in_place().expect("failed to run virtualdom");
file.write_fmt(format_args!("{}", TextRenderer::from_vdom(&dom))) file.write_fmt(format_args!(
.unwrap(); "{}",
TextRenderer::from_vdom(&dom, Default::default())
))
.unwrap();
} }
pub static App: FC<()> = |cx| { pub static App: FC<()> = |cx| {

View file

@ -1,3 +1,7 @@
//!
//!
//!
//!
//! This crate demonstrates how to implement a custom renderer for Dioxus VNodes via the `TextRenderer` renderer. //! 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. //! 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_vnode(vnode: &VNode, string: &mut String) {}
pub fn render_vdom(dom: &VirtualDom) -> String { pub fn render_vdom(dom: &VirtualDom, cfg: impl FnOnce(SsrConfig) -> SsrConfig) -> String {
format!("{:}", TextRenderer::from_vdom(dom)) format!(
"{:}",
TextRenderer::from_vdom(dom, cfg(SsrConfig::default()))
)
} }
pub fn render_vdom_scope(vdom: &VirtualDom, scope: ScopeId) -> Option<String> { pub fn render_vdom_scope(vdom: &VirtualDom, scope: ScopeId) -> Option<String> {
@ -27,29 +34,6 @@ pub fn render_vdom_scope(vdom: &VirtualDom, scope: ScopeId) -> Option<String> {
)) ))
} }
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. /// A configurable text renderer for the Dioxus VirtualDOM.
/// ///
/// ///
@ -60,7 +44,7 @@ impl Default for SsrConfig {
/// ///
/// ## Example /// ## Example
/// ```ignore /// ```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); /// let mut vdom = VirtualDom::new(App);
/// vdom.rebuild_in_place(); /// vdom.rebuild_in_place();
/// ///
@ -81,11 +65,11 @@ impl Display for TextRenderer<'_> {
} }
impl<'a> TextRenderer<'a> { impl<'a> TextRenderer<'a> {
pub fn from_vdom(vdom: &'a VirtualDom) -> Self { pub fn from_vdom(vdom: &'a VirtualDom, cfg: SsrConfig) -> Self {
Self { Self {
cfg,
root: vdom.base_scope().root(), root: vdom.base_scope().root(),
vdom: Some(vdom), 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 { match self.cfg.newline {
true => write!(f, ">\n")?, true => write!(f, ">\n")?,
false => write!(f, ">")?, false => write!(f, ">")?,
@ -162,9 +161,14 @@ impl<'a> TextRenderer<'a> {
} }
VNodeKind::Component(vcomp) => { VNodeKind::Component(vcomp) => {
let idx = vcomp.ass_scope.get().unwrap(); let idx = vcomp.ass_scope.get().unwrap();
if let Some(vdom) = self.vdom { match (self.vdom, self.cfg.skip_components) {
let new_node = vdom.get_scope(idx).unwrap().root(); (Some(vdom), false) => {
self.html_render(new_node, f, il + 1)?; let new_node = vdom.get_scope(idx).unwrap().root();
self.html_render(new_node, f, il + 1)?;
}
_ => {
// render the component by name
}
} }
} }
VNodeKind::Suspended { .. } => { 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -182,15 +232,14 @@ mod tests {
use dioxus_core as dioxus; use dioxus_core as dioxus;
use dioxus_core::prelude::*; use dioxus_core::prelude::*;
use dioxus_html as dioxus_elements; use dioxus_html as dioxus_elements;
use dioxus_html::GlobalAttributes;
const SIMPLE_APP: FC<()> = |cx| { static SIMPLE_APP: FC<()> = |cx| {
cx.render(rsx!(div { cx.render(rsx!(div {
"hello world!" "hello world!"
})) }))
}; };
const SLIGHTLY_MORE_COMPLEX: FC<()> = |cx| { static SLIGHTLY_MORE_COMPLEX: FC<()> = |cx| {
cx.render(rsx! { cx.render(rsx! {
div { div {
title: "About W3Schools" title: "About W3Schools"
@ -209,14 +258,14 @@ mod tests {
}) })
}; };
const NESTED_APP: FC<()> = |cx| { static NESTED_APP: FC<()> = |cx| {
cx.render(rsx!( cx.render(rsx!(
div { div {
SIMPLE_APP {} SIMPLE_APP {}
} }
)) ))
}; };
const FRAGMENT_APP: FC<()> = |cx| { static FRAGMENT_APP: FC<()> = |cx| {
cx.render(rsx!( cx.render(rsx!(
div { "f1" } div { "f1" }
div { "f2" } div { "f2" }
@ -229,21 +278,28 @@ mod tests {
fn to_string_works() { fn to_string_works() {
let mut dom = VirtualDom::new(SIMPLE_APP); let mut dom = VirtualDom::new(SIMPLE_APP);
dom.rebuild_in_place().expect("failed to run virtualdom"); 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] #[test]
fn nested() { fn nested() {
let mut dom = VirtualDom::new(NESTED_APP); let mut dom = VirtualDom::new(NESTED_APP);
dom.rebuild_in_place().expect("failed to run virtualdom"); dom.rebuild_in_place().expect("failed to run virtualdom");
dbg!(render_vdom(&dom)); dbg!(render_vdom(&dom, |c| c));
} }
#[test] #[test]
fn fragment_app() { fn fragment_app() {
let mut dom = VirtualDom::new(FRAGMENT_APP); let mut dom = VirtualDom::new(FRAGMENT_APP);
dom.rebuild_in_place().expect("failed to run virtualdom"); dom.rebuild_in_place().expect("failed to run virtualdom");
dbg!(render_vdom(&dom)); dbg!(render_vdom(&dom, |c| c));
} }
#[test] #[test]
@ -256,26 +312,23 @@ mod tests {
let mut dom = VirtualDom::new(SLIGHTLY_MORE_COMPLEX); let mut dom = VirtualDom::new(SLIGHTLY_MORE_COMPLEX);
dom.rebuild_in_place().expect("failed to run virtualdom"); dom.rebuild_in_place().expect("failed to run virtualdom");
file.write_fmt(format_args!("{}", TextRenderer::from_vdom(&dom))) file.write_fmt(format_args!(
.unwrap(); "{}",
TextRenderer::from_vdom(&dom, SsrConfig::default())
))
.unwrap();
} }
// #[test] #[test]
// fn styles() { fn styles() {
// const STLYE_APP: FC<()> = |cx| { static STLYE_APP: FC<()> = |cx| {
// // cx.render(rsx! {
// cx.render(rsx! { div { style: { color: "blue", font_size: "46px" } }
// div { })
// style: { };
// color: "blue",
// font_size: "46px"
// }
// }
// })
// };
// let mut dom = VirtualDom::new(STLYE_APP); let mut dom = VirtualDom::new(STLYE_APP);
// dom.rebuild_in_place().expect("failed to run virtualdom"); dom.rebuild_in_place().expect("failed to run virtualdom");
// dbg!(render_vdom(&dom)); dbg!(render_vdom(&dom, |c| c));
// } }
} }

21
packages/web/src/cfg.rs Normal file
View file

@ -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
}
}

View file

@ -8,12 +8,19 @@ use fxhash::FxHashMap;
use wasm_bindgen::{closure::Closure, JsCast}; use wasm_bindgen::{closure::Closure, JsCast};
use web_sys::{ use web_sys::{
window, Document, Element, Event, HtmlElement, HtmlInputElement, HtmlOptionElement, Node, window, Document, Element, Event, HtmlElement, HtmlInputElement, HtmlOptionElement, Node,
NodeList,
}; };
use crate::{nodeslab::NodeSlab, WebConfig};
pub struct WebsysDom { pub struct WebsysDom {
pub stack: Stack, stack: Stack,
nodes: Vec<Node>,
/// A map from ElementID (index) to Node
nodes: NodeSlab,
document: Document, document: Document,
root: Element, root: Element,
event_receiver: async_channel::Receiver<EventTrigger>, event_receiver: async_channel::Receiver<EventTrigger>,
@ -33,11 +40,8 @@ pub struct WebsysDom {
last_node_was_text: bool, last_node_was_text: bool,
} }
impl WebsysDom { impl WebsysDom {
pub fn new(root: Element) -> Self { pub fn new(root: Element, cfg: WebConfig) -> Self {
let document = window() let document = load_document();
.expect("must have access to the window")
.document()
.expect("must have access to the Document");
let (sender, receiver) = async_channel::unbounded::<EventTrigger>(); let (sender, receiver) = async_channel::unbounded::<EventTrigger>();
@ -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::<Node>().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::<Element>().unwrap();
let id: String = el.get_attribute("dio_el").unwrap();
let id = id.parse::<usize>().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::<Node>().unwrap();
stack.push(root_node);
Self { Self {
stack: Stack::with_capacity(10), stack,
nodes, nodes,
listeners: FxHashMap::default(), listeners,
document, document,
event_receiver: receiver, event_receiver: receiver,
trigger: sender_callback, trigger: sender_callback,
@ -97,19 +123,14 @@ impl WebsysDom {
} }
} }
fn push(&mut self, root: u64) { fn push(&mut self, root: u64) {
// let key = DefaultKey::from(KeyData::from_ffi(root));
let key = root as usize; let key = root as usize;
let domnode = self.nodes.get_mut(key); let domnode = &self.nodes[key];
let real_node: Node = match domnode { let real_node: Node = match domnode {
Some(n) => n.clone(), Some(n) => n.clone(),
None => todo!(), None => todo!(),
}; };
// let domnode = domnode.unwrap().as_mut().unwrap();
// .expect(&format!("Failed to pop know root: {:#?}", key))
// .unwrap();
self.stack.push(real_node); self.stack.push(real_node);
} }
// drop the node off the stack // drop the node off the stack
@ -205,7 +226,7 @@ impl WebsysDom {
let id = id as usize; let id = id as usize;
self.stack.push(textnode.clone()); 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) { fn create_element(&mut self, tag: &str, ns: Option<&'static str>, id: u64) {
@ -227,7 +248,7 @@ impl WebsysDom {
let id = id as usize; let id = id as usize;
self.stack.push(el.clone()); 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.node_counter.?next();
// let nid = self.nodes.insert(el).data().as_ffi(); // let nid = self.nodes.insert(el).data().as_ffi();
// log::debug!("Called [`create_element`]: {}, {:?}", tag, nid); // log::debug!("Called [`create_element`]: {}, {:?}", tag, nid);
@ -264,6 +285,9 @@ impl WebsysDom {
) )
.unwrap(); .unwrap();
el.set_attribute(&format!("dioxus-event"), &format!("{}", event))
.unwrap();
// Register the callback to decode // Register the callback to decode
if let Some(entry) = self.listeners.get_mut(event) { 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)))) 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" "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture"
@ -645,3 +649,14 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<EventTrigger> {
dioxus_core::events::EventPriority::High, 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")
}

View file

@ -2,38 +2,66 @@
//! -------------- //! --------------
//! This crate implements a renderer of the Dioxus Virtual DOM for the web browser using Websys. //! 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::prelude::{Context, Properties, VNode};
use dioxus::virtual_dom::VirtualDom; use dioxus::virtual_dom::VirtualDom;
pub use dioxus_core as dioxus; pub use dioxus_core as dioxus;
use dioxus_core::error::Result;
use dioxus_core::{events::EventTrigger, prelude::FC}; use dioxus_core::{events::EventTrigger, prelude::FC};
use futures_util::{pin_mut, Stream, StreamExt}; use futures_util::{pin_mut, Stream, StreamExt};
use fxhash::FxHashMap; 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 cache;
mod new; mod cfg;
mod dom;
mod nodeslab;
/// Launches the VirtualDOM from the specified component function. /// Launches the VirtualDOM from the specified component function.
/// ///
/// This method will block the thread with `spawn_local` /// This method will block the thread with `spawn_local`
///
/// # Example
///
///
///
pub fn launch<F>(root: FC<()>, config: F) pub fn launch<F>(root: FC<()>, config: F)
where 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<T, F>(root: FC<T>, root_props: T, config: F) pub fn launch_with_props<T, F>(root: FC<T>, root_props: T, config: F)
where where
T: Properties + 'static, 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. /// 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. /// See DioxusErrors for more information on how these errors could occour.
/// ///
/// # Example
///
/// ```ignore /// ```ignore
/// fn main() { /// fn main() {
/// wasm_bindgen_futures::spawn_local(WebsysRenderer::start(Example)); /// 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. /// Run the app to completion, panicing if any error occurs while rendering.
/// Pairs well with the wasm_bindgen async handler /// Pairs well with the wasm_bindgen async handler
pub async fn run(root: FC<()>) { pub async fn run_with_props<T: Properties + 'static>(
run_with_props(root, ()).await; root: FC<T>,
} root_props: T,
cfg: WebConfig,
pub async fn run_with_props<T: Properties + 'static>(root: FC<T>, root_props: T) { ) -> Result<()> {
let dom = VirtualDom::new_with_props(root, root_props); 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<()> { let root_el = load_document().get_element_by_id("dioxus_root").unwrap();
use wasm_bindgen::JsCast; let mut websys_dom = dom::WebsysDom::new(root_el, cfg);
let root = prepare_websys_dom(); // let mut edits = Vec::new();
let root_node = root.clone().dyn_into::<Node>().unwrap(); // internal_dom.rebuild(&mut websys_dom, &mut edits)?;
// websys_dom.process_edits(&mut edits);
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);
log::info!("Going into event loop"); log::info!("Going into event loop");
loop { loop {
@ -105,11 +123,14 @@ pub async fn event_loop(mut internal_dom: VirtualDom) -> dioxus_core::error::Res
Ok(()) Ok(())
} }
fn prepare_websys_dom() -> Element { fn iter_node_list() {}
web_sys::window()
.expect("should have access to the Window") // struct NodeListIter {
.document() // node_list: NodeList,
.expect("should have access to the Document") // }
.get_element_by_id("dioxusroot") // impl Iterator for NodeListIter {}
.unwrap()
struct HydrationNode {
id: usize,
node: Node,
} }

View file

@ -0,0 +1,41 @@
use std::ops::{Index, IndexMut};
use web_sys::Node;
pub struct NodeSlab {
nodes: Vec<Option<Node>>,
}
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<usize> for NodeSlab {
type Output = Option<Node>;
fn index(&self, index: usize) -> &Self::Output {
&self.nodes[index]
}
}
impl IndexMut<usize> 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]
}
}