mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-10 06:34:20 +00:00
wip: making progress on diffing and hydration
This commit is contained in:
parent
e5c88fe3a4
commit
49856ccd68
34 changed files with 1023 additions and 750 deletions
23
Cargo.toml
23
Cargo.toml
|
@ -19,7 +19,7 @@ dioxus-mobile = { path = "./packages/mobile", optional = true }
|
|||
|
||||
[features]
|
||||
# core
|
||||
default = ["core", "ssr", "desktop"]
|
||||
default = ["core", "ssr"]
|
||||
core = ["macro", "hooks", "html"]
|
||||
macro = ["dioxus-core-macro"]
|
||||
hooks = ["dioxus-hooks"]
|
||||
|
@ -37,18 +37,31 @@ mobile = ["dioxus-mobile"]
|
|||
|
||||
|
||||
[dev-dependencies]
|
||||
futures = "0.3.15"
|
||||
futures-util = "0.3.16"
|
||||
log = "0.4.14"
|
||||
num-format = "0.4.0"
|
||||
separator = "0.4.1"
|
||||
serde = { version = "1.0.126", features = ["derive"] }
|
||||
surf = "2.2.0"
|
||||
im-rc = "15.0.0"
|
||||
fxhash = "0.2.1"
|
||||
anyhow = "1.0.42"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
argh = "0.1.5"
|
||||
env_logger = "*"
|
||||
async-std = { version = "1.9.0", features = ["attributes"] }
|
||||
im-rc = "15.0.0"
|
||||
rand = { version = "0.8.4", features = ["small_rng"] }
|
||||
fxhash = "0.2.1"
|
||||
surf = {version = "2.2.0", git = "https://github.com/jkelleyrtp/surf/", branch = "jk/fix-the-wasm"}
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
gloo-timers = "0.2.1"
|
||||
surf = {version = "2.2.0", default-features = false, features = ["wasm-client"], git = "https://github.com/jkelleyrtp/surf/", branch = "jk/fix-the-wasm"}
|
||||
|
||||
|
||||
[dependencies.getrandom]
|
||||
version = "0.2"
|
||||
features = ["js"]
|
||||
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
|
|
|
@ -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 |
|
||||
|
|
|
@ -12,9 +12,11 @@ use std::fs::{self, DirEntry};
|
|||
fn main() {
|
||||
env_logger::init();
|
||||
dioxus::desktop::launch(App, |c| {
|
||||
c.with_resizable(false)
|
||||
c.with_window(|w| {
|
||||
w.with_resizable(false)
|
||||
.with_inner_size(LogicalSize::new(800.0, 400.0))
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
|
|
33
examples/hydration.rs
Normal file
33
examples/hydration.rs
Normal 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
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
|
@ -15,18 +15,20 @@
|
|||
//! 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")
|
||||
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");
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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! {
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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)
|
||||
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)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -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");
|
||||
// }
|
||||
// }
|
||||
|
|
|
@ -40,6 +40,10 @@ smallvec = "1.6.1"
|
|||
slab = "0.4.3"
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus-html = { path = "../html" }
|
||||
|
||||
|
||||
[features]
|
||||
default = ["serialize"]
|
||||
serialize = ["serde"]
|
||||
|
|
|
@ -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!()));
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
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();
|
||||
// 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);
|
||||
if old.user_fc == new.user_fc {
|
||||
let scope_addr = old.ass_scope.get().unwrap();
|
||||
|
||||
// Make sure we're dealing with the same component (by function pointer)
|
||||
self.cur_idxs.push(old.ass_scope.get().unwrap());
|
||||
if old.user_fc == new.user_fc {
|
||||
//
|
||||
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 {
|
||||
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());
|
||||
} else {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
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,73 +586,99 @@ 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);
|
||||
// }
|
||||
// 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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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;
|
||||
|
|
17
packages/core/tests/diffing.rs
Normal file
17
packages/core/tests/diffing.rs
Normal 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>
|
||||
// };
|
||||
}
|
|
@ -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| {
|
||||
|
|
45
packages/desktop/src/cfg.rs
Normal file
45
packages/desktop/src/cfg.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -2,7 +2,14 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="_dioxusroot">
|
||||
</div>
|
||||
</body>
|
||||
<script>
|
||||
class Interpreter {
|
||||
constructor(root) {
|
||||
this.root = root;
|
||||
|
@ -117,8 +124,10 @@
|
|||
console.log(reply);
|
||||
this.stack.push(this.root);
|
||||
|
||||
for (let x = 0; x < reply.length; x++) {
|
||||
let edit = reply[x];
|
||||
let edits = reply.edits;
|
||||
|
||||
for (let x = 0; x < edits.length; x++) {
|
||||
let edit = edits[x];
|
||||
console.log(edit);
|
||||
|
||||
let f = this[edit.type];
|
||||
|
@ -183,11 +192,19 @@
|
|||
|
||||
async function initialize() {
|
||||
const reply = await rpc.call('initiate');
|
||||
const interpreter = new Interpreter(window.document.getElementById("_dioxusroot"));
|
||||
let root = window.document.getElementById("_dioxusroot");
|
||||
const interpreter = new Interpreter(root);
|
||||
console.log(reply);
|
||||
|
||||
for (let x = 0; x < reply.length; x++) {
|
||||
let edit = reply[x];
|
||||
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];
|
||||
|
@ -198,12 +215,6 @@
|
|||
}
|
||||
console.log("initializing...");
|
||||
initialize();
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="_dioxusroot">
|
||||
</div>
|
||||
</body>
|
||||
</script>
|
||||
|
||||
</html>
|
||||
|
|
|
@ -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 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();
|
||||
serde_json::to_value(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
|
||||
|
|
|
@ -16,3 +16,9 @@ vdom.rebuild_in_place();
|
|||
let text = dioxus_ssr::render_root(&vdom);
|
||||
assert_eq!(text, "<div>hello world!</div>")
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Pre-rendering
|
||||
|
||||
|
||||
|
|
10
packages/ssr/examples/hydration.rs
Normal file
10
packages/ssr/examples/hydration.rs
Normal 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() {}
|
|
@ -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())
|
||||
});
|
||||
|
|
|
@ -15,7 +15,10 @@ 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)))
|
||||
file.write_fmt(format_args!(
|
||||
"{}",
|
||||
TextRenderer::from_vdom(&dom, Default::default())
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
|
|
|
@ -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,10 +161,15 @@ impl<'a> TextRenderer<'a> {
|
|||
}
|
||||
VNodeKind::Component(vcomp) => {
|
||||
let idx = vcomp.ass_scope.get().unwrap();
|
||||
if let Some(vdom) = self.vdom {
|
||||
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 { .. } => {
|
||||
// we can't do anything with suspended nodes
|
||||
|
@ -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)))
|
||||
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
21
packages/web/src/cfg.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
|
41
packages/web/src/nodeslab.rs
Normal file
41
packages/web/src/nodeslab.rs
Normal 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]
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue