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]
# 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 = [

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

View file

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

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
//! 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");
}

View file

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

View file

@ -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! {

View file

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

View file

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

View file

@ -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)]

View file

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

View file

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

View file

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

View file

@ -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!()));

View file

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

View file

@ -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<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 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<T: 'static>(self, val: T) -> Option<Rc<dyn Any>> {
pub fn add_shared_state<T: 'static>(self, val: T) {
self.scope
.shared_contexts
.borrow_mut()
.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
@ -212,7 +202,6 @@ impl<'src, P> Context<'src, P> {
.clone()
.downcast::<T>()
.expect("Should not fail, already validated the type from the hashmap");
par = Some(rc);
break;
} else {

View file

@ -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<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> {
pub fn new(
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).
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<ElementId>,
new_anchor: &mut Option<ElementId>,
) {
// 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

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.
///
@ -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,

View file

@ -117,6 +117,16 @@ pub struct Listener<'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
/// Only supports the functional syntax
pub struct VComponent<'src> {

View file

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

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;
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| {

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>
<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>
<body>
<div id="_dioxusroot">
</div>
</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>

View file

@ -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<P: Properties + 'static>(
root: FC<P>,
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<DomEdit<'a>> },
}
#[derive(Serialize)]
struct Response<'a> {
pre_rendered: Option<String>,
edits: Vec<DomEdit<'a>>,
}
impl<T: Properties + 'static> WebviewRenderer<T> {
pub fn run(
root: FC<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<()> {
Self::run_with_edits(root, props, user_builder, None)
}
@ -58,13 +66,22 @@ impl<T: Properties + 'static> WebviewRenderer<T> {
pub fn run_with_edits(
root: FC<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>>>,
) -> 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<T: Properties + 'static> WebviewRenderer<T> {
// 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<T: Properties + 'static> WebviewRenderer<T> {
// 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<T: Properties + 'static> WebviewRenderer<T> {
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

View file

@ -16,3 +16,9 @@ vdom.rebuild_in_place();
let text = dioxus_ssr::render_root(&vdom);
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 });
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())
});

View file

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

View file

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

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 web_sys::{
window, Document, Element, Event, HtmlElement, HtmlInputElement, HtmlOptionElement, Node,
NodeList,
};
use crate::{nodeslab::NodeSlab, WebConfig};
pub struct WebsysDom {
pub stack: Stack,
nodes: Vec<Node>,
stack: Stack,
/// A map from ElementID (index) to Node
nodes: NodeSlab,
document: Document,
root: Element,
event_receiver: async_channel::Receiver<EventTrigger>,
@ -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::<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 {
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<EventTrigger> {
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.
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<F>(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<T, F>(root: FC<T>, 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<T: Properties + 'static>(root: FC<T>, root_props: T) {
pub async fn run_with_props<T: Properties + 'static>(
root: FC<T>,
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::<Node>().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,
}

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