feat: static node infrastructure and ssr changes

This commit is contained in:
Jonathan Kelley committed 2021-07-11 14:49:52 -04:00
1 parent 975fa566f9
commit 9abb0470b7
34 files changed
+1022 -855

No files matched your search

+10 -6
View File
@@ -9,24 +9,27 @@ description = "Core functionality for Dioxus - a concurrent renderer-agnostic Vi
[dependencies]
dioxus-core = { path="./packages/core", version="0.1.0" }
dioxus-core-macro = { path="./packages/core-macro", version="0.1.0" }
dioxus-core-macro = { path="./packages/core-macro", version="0.1.0", optional=true }
dioxus-html = { path="./packages/html", optional=true }
dioxus-web = { path="./packages/web", optional=true }
dioxus-webview = { path="./packages/webview", optional=true }
dioxus-hooks = { path="./packages/hooks", version="0.0.0" }
dioxus-hooks = { path="./packages/hooks", version="0.0.0", optional=true }
[features]
default = ["ssr", "hooks", "router", "core", "atoms", "html", "web", "desktop"]
default = ["macro", "ssr", "hooks", "router", "core", "atoms", "html", "web", "desktop"]
atoms = []
core = []
macro = ["dioxus-core-macro"]
ssr = []
hooks = []
hooks = ["dioxus-hooks"]
router = []
html = ["dioxus-html"]
web = ["dioxus-web"]
desktop = ["dioxus-webview"]
# Core just includes dioxus-core (the only required package)
core = []
[profile.dev]
split-debuginfo = "unpacked"
@@ -41,6 +44,7 @@ separator = "0.4.1"
serde = { version="1.0.126", features=["derive"] }
surf = "2.2.0"
env_logger = "*"
async-std = { version="1.9.0", features=["attributes"] }
[workspace]
members = [
@@ -50,7 +54,7 @@ members = [
"packages/web",
"packages/webview",
"packages/hooks",
"packages/ssr",
# "packages/atoms",
# "packages/ssr",
# "packages/docsite",
]
+6 -12
View File
@@ -36,14 +36,6 @@ If you know React, then you already know Dioxus.
- Powerful and simple integrated state management
- Rust! (enums, static types, modules, efficiency)
### Key Differentiators
- Immutability by default
- Built-in suspense system
- Integrations for isomorphic apps
- Skip diffing altogether with signal API
- Extremely portable without runtime requirements
## Get Started with...
<table style="width:100%" align="center">
@@ -121,13 +113,15 @@ Dioxus is heavily inspired by React, but we want your transition to feel like an
| Controlled Inputs | ✅ | ✅ | stateful wrappers around inputs |
| CSS/Inline Styles | ✅ | ✅ | syntax for inline styles/attribute groups |
| Custom elements | ✅ | ✅ | Define new element primitives |
| Compile-time correct | ✅ | ✅ | Throw errors on invalid template layouts |
| 1st class global state | ✅ | ❓ | redux/recoil/mobx on top of context |
| Suspense | 🛠 | ✅ | schedule future render from future/promise |
| Cooperative Scheduling | 🛠 | ✅ | Prioritize important events over non-important events |
| Fine-grained reactivity | 🛠 | ❓ | Skip diffing for fine-grain updates |
| Runs natively | ✅ | ❓ | runs as a portable binary w/o a runtime (Node) |
| NodeRef | 🛠 | ✅ | gain direct access to nodes [1] |
| Fine-grained reactivity | 🛠 | ❓ | Skip diffing for fine-grain updates |
| Compile-time correct | ✅ | ❓ | Throw errors on invalid template layouts |
| Runs natively | ✅ | ❓ | runs as a portable binary w/o a runtime (Node) |
| 1st class global state | ✅ | ❓ | redux/recoil/mobx on top of context |
| Subtree Memoization | ✅ | ❓ | skip diffing static element subtrees |
| Heuristic Engine | ✅ | ❓ | track component memory usage to minimize allocations |
- [1] Currently blocked until we figure out a cross-platform way of exposing an imperative Node API.
+14 -12
View File
@@ -2,7 +2,7 @@
//!
//! The example from the README.md
use std::pin::Pin;
use std::{pin::Pin, time::Duration};
use dioxus::prelude::*;
use futures::Future;
@@ -19,23 +19,25 @@ struct DogApi {
const ENDPOINT: &str = "https://dog.ceo/api/breeds/image/random";
struct Ex(Pin<Box<dyn Future<Output = ()> + 'static>>);
static App: FC<()> = |cx| {
// let mut count = use_state(cx, || 0);
let mut count = use_state(cx, || 0);
let mut fut = cx.use_hook(
move || {
Ex(Box::pin(async {
Box::pin(async {
//
let mut tick = 0;
loop {
match surf::get(ENDPOINT).recv_json::<DogApi>().await {
Ok(_) => (),
Err(_) => (),
}
async_std::task::sleep(Duration::from_millis(250)).await;
log::debug!("ticking forward... {}", tick);
tick += 1;
// match surf::get(ENDPOINT).recv_json::<DogApi>().await {
// Ok(_) => (),
// Err(_) => (),
// }
}
})
as Pin<Box<dyn Future<Output = ()> + 'static>>)
}) as Pin<Box<dyn Future<Output = ()> + 'static>>
},
|h| &mut h.0,
|h| h,
|_| {},
);
@@ -43,7 +45,7 @@ static App: FC<()> = |cx| {
cx.render(rsx! {
div {
h1 {"it's working somewhat properly"}
}
})
};
+1 -1
View File
@@ -1,3 +1,3 @@
{
"rust-analyzer.inlayHints.enable": true
"rust-analyzer.inlayHints.enable": false
}
+9 -10
View File
@@ -31,20 +31,19 @@ log = "0.4"
# # Serialize the Edits for use in Webview/Liveview instances
serde = { version="1", features=["derive"], optional=true }
smallvec = "1.6.1"
# Backs scopes and unique keys
slotmap = "1.0.3"
# backs the fiber system for suspended components
# todo: would like to use something smaller or just roll our own futures manually
futures = "0.3.15"
async-std = { version="1.9.0", features=["attributes"] }
appendlist = "1.4.0"
futures-util = "0.3.15"
[dev-dependencies]
dioxus-html = { path="../html" }
dioxus-hooks = { path="../hooks" }
env_logger = "*"
# async-std = { version="1.9.0", features=["attributes"] }
[features]
default = ["serialize"]
# default = []
default = []
serialize = ["slotmap/serde", "serde"]
+2 -2
View File
@@ -1,7 +1,7 @@
use std::pin::Pin;
use dioxus_core::prelude::*;
use futures::Future;
use std::future::Future;
fn main() {}
@@ -18,7 +18,7 @@ const App: FC<()> = |cx| {
);
// let g = unsafe { Pin::new_unchecked(fut) };
cx.submit_task(fut);
// cx.submit_task(fut);
todo!()
};
+2 -3
View File
@@ -2,7 +2,6 @@ use crate::hooklist::HookList;
use crate::{arena::SharedArena, innerlude::*};
use appendlist::AppendList;
use bumpalo::Bump;
use futures::FutureExt;
use slotmap::DefaultKey;
use slotmap::SlotMap;
use std::marker::PhantomData;
@@ -34,11 +33,11 @@ impl ActiveFrame {
Self::from_frames(
BumpFrame {
bump: Bump::new(),
head_node: VNode::text(""),
head_node: VNode::static_text(""),
},
BumpFrame {
bump: Bump::new(),
head_node: VNode::text(""),
head_node: VNode::static_text(""),
},
)
}
+164
View File
@@ -0,0 +1,164 @@
/// This iterator iterates through a list of virtual children and only returns real children (Elements or Text).
///
/// This iterator is useful when it's important to load the next real root onto the top of the stack for operations like
/// "InsertBefore".
struct RealChildIterator<'a> {
scopes: &'a SharedArena,
// Heuristcally we should never bleed into 5 completely nested fragments/components
// Smallvec lets us stack allocate our little stack machine so the vast majority of cases are sane
stack: smallvec::SmallVec<[(u16, &'a VNode<'a>); 5]>,
}
impl<'a> RealChildIterator<'a> {
fn new(starter: &'a VNode<'a>, scopes: &'a SharedArena) -> Self {
Self {
scopes,
stack: smallvec::smallvec![(0, starter)],
}
}
}
// impl<'a> DoubleEndedIterator for ChildIterator<'a> {
// fn next_back(&mut self) -> Option<Self::Item> {
// todo!()
// }
// }
impl<'a> Iterator for RealChildIterator<'a> {
type Item = &'a VNode<'a>;
fn next(&mut self) -> Option<&'a VNode<'a>> {
let mut should_pop = false;
let mut returned_node = None;
let mut should_push = None;
while returned_node.is_none() {
if let Some((count, node)) = self.stack.last_mut() {
match node {
// We can only exit our looping when we get "real" nodes
VNode::Element(_) | VNode::Text(_) => {
// We've recursed INTO an element/text
// We need to recurse *out* of it and move forward to the next
should_pop = true;
returned_node = Some(&**node);
}
// If we get a fragment we push the next child
VNode::Fragment(frag) => {
let _count = *count as usize;
if _count >= frag.children.len() {
should_pop = true;
} else {
should_push = Some(&frag.children[_count]);
}
}
// Immediately abort suspended nodes - can't do anything with them yet
// VNode::Suspended => should_pop = true,
VNode::Suspended { real } => todo!(),
// For components, we load their root and push them onto the stack
VNode::Component(sc) => {
let scope = self.scopes.try_get(sc.ass_scope.get().unwrap()).unwrap();
// Simply swap the current node on the stack with the root of the component
*node = scope.root();
}
}
} else {
// If there's no more items on the stack, we're done!
return None;
}
if should_pop {
self.stack.pop();
if let Some((id, _)) = self.stack.last_mut() {
*id += 1;
}
should_pop = false;
}
if let Some(push) = should_push {
self.stack.push((0, push));
should_push = None;
}
}
returned_node
}
}
mod tests {
use super::*;
use crate as dioxus;
use crate::innerlude::*;
use crate::util::DebugDom;
use dioxus_core_macro::*;
// #[test]
// fn test_child_iterator() {
// static App: FC<()> = |cx| {
// cx.render(rsx! {
// Fragment {
// div {}
// h1 {}
// h2 {}
// h3 {}
// Fragment {
// "internal node"
// div {
// "baller text shouldn't show up"
// }
// p {
// }
// Fragment {
// Fragment {
// "wow you really like framgents"
// Fragment {
// "why are you like this"
// Fragment {
// "just stop now please"
// Fragment {
// "this hurts"
// Fragment {
// "who needs this many fragments?????"
// Fragment {
// "just... fine..."
// Fragment {
// "no"
// }
// }
// }
// }
// }
// }
// }
// }
// }
// "my text node 1"
// "my text node 2"
// "my text node 3"
// "my text node 4"
// }
// })
// };
// let mut dom = VirtualDom::new(App);
// let mut renderer = DebugDom::new();
// dom.rebuild(&mut renderer).unwrap();
// let starter = dom.base_scope().root();
// let ite = RealChildIterator::new(starter, &dom.components);
// for child in ite {
// match child {
// VNode::Element(el) => println!("Found: Element {}", el.tag_name),
// VNode::Text(t) => println!("Found: Text {:?}", t.text),
// // These would represent failing cases.
// VNode::Fragment(_) => panic!("Found: Fragment"),
// VNode::Suspended { real } => panic!("Found: Suspended"),
// VNode::Component(_) => panic!("Found: Component"),
// }
// }
// }
}
+2
View File
@@ -9,6 +9,7 @@ use crate::innerlude::FC;
pub trait Properties: Sized {
type Builder;
const IS_STATIC: bool = false;
fn builder() -> Self::Builder;
/// Memoization can only happen if the props are 'static
@@ -19,6 +20,7 @@ pub trait Properties: Sized {
impl Properties for () {
type Builder = EmptyBuilder;
const IS_STATIC: bool = true;
fn builder() -> Self::Builder {
EmptyBuilder {}
}
+30 -31
View File
@@ -2,7 +2,6 @@ use crate::hooklist::HookList;
use crate::{arena::SharedArena, innerlude::*};
use appendlist::AppendList;
use bumpalo::Bump;
use futures::FutureExt;
use slotmap::DefaultKey;
use slotmap::SlotMap;
use std::marker::PhantomData;
@@ -40,11 +39,9 @@ use std::{
pub struct Context<'src, T> {
pub props: &'src T,
pub scope: &'src Scope,
pub tasks: &'src AppendList<&'src mut DTask<'src>>,
pub tasks: &'src RefCell<Vec<&'src mut PinnedTask>>,
}
pub type DTask<'s> = dyn Future<Output = ()> + 's;
// pub type DTask = Pin<Box<dyn Future<Output = ()>>>;
pub type PinnedTask = Pin<Box<dyn Future<Output = ()>>>;
impl<'src, T> Copy for Context<'src, T> {}
impl<'src, T> Clone for Context<'src, T> {
@@ -133,36 +130,34 @@ impl<'src, P> Context<'src, P> {
/// )
/// }
/// ```
pub fn use_hook<InternalHookState: 'static, Output: 'src>(
pub fn use_hook<State, Output, Init, Run, Cleanup>(
self,
// The closure that builds the hook state
initializer: impl FnOnce() -> InternalHookState,
// The closure that takes the hookstate and returns some value
runner: impl FnOnce(&'src mut InternalHookState) -> Output,
// The closure that cleans up whatever mess is left when the component gets torn down
// TODO: add this to the "clean up" group for when the component is dropped
cleanup: impl FnOnce(InternalHookState),
) -> Output {
initializer: Init,
runner: Run,
cleanup: Cleanup,
) -> Output
where
State: 'static,
Output: 'src,
Init: FnOnce() -> State,
Run: FnOnce(&'src mut State) -> Output,
Cleanup: FnOnce(State),
{
// If the idx is the same as the hook length, then we need to add the current hook
if self.scope.hooks.at_end() {
let new_state = initializer();
self.scope.hooks.push(new_state);
}
let state = self.scope.hooks.next::<InternalHookState>().expect(
r###"
Unable to retrive the hook that was initialized in this index.
Consult the `rules of hooks` to understand how to use hooks properly.
You likely used the hook in a conditional. Hooks rely on consistent ordering between renders.
Any function prefixed with "use" should not be called conditionally.
"###,
);
const ERR_MSG: &str = r###"
Unable to retrive the hook that was initialized in this index.
Consult the `rules of hooks` to understand how to use hooks properly.
runner(state)
You likely used the hook in a conditional. Hooks rely on consistent ordering between renders.
Any function prefixed with "use" should not be called conditionally.
"###;
runner(self.scope.hooks.next::<State>().expect(ERR_MSG))
}
/// This hook enables the ability to expose state to children further down the VirtualDOM Tree.
@@ -178,8 +173,7 @@ impl<'src, P> Context<'src, P> {
///
///
pub fn use_create_context<T: 'static>(&self, init: impl Fn() -> T) {
let mut scope = self.scope;
let mut cxs = scope.shared_contexts.borrow_mut();
let mut cxs = self.scope.shared_contexts.borrow_mut();
let ty = TypeId::of::<T>();
let is_initialized = self.use_hook(
@@ -274,6 +268,7 @@ impl<'src, P> Context<'src, P> {
fut: &'src mut Pin<Box<dyn Future<Output = Output> + 'static>>,
callback: Fut,
) -> VNode<'src> {
use futures_util::FutureExt;
match fut.now_or_never() {
Some(out) => {
let suspended_cx = SuspendedContext {};
@@ -290,6 +285,10 @@ impl<'src, P> Context<'src, P> {
}
/// `submit_task` will submit the future to be polled.
///
/// This is useful when you have some async task that needs to be progressed.
///
/// ## Explanation
/// Dioxus will step its internal event loop if the future returns if the future completes while waiting.
///
/// Tasks can't return anything, but they can be controlled with the returned handle
@@ -299,8 +298,8 @@ impl<'src, P> Context<'src, P> {
///
///
///
pub fn submit_task(&self, mut task: &'src mut DTask<'src>) -> TaskHandle {
self.tasks.push(task);
pub fn submit_task(&self, task: &'src mut PinnedTask) -> TaskHandle {
self.tasks.borrow_mut().push(task);
TaskHandle { _p: PhantomData {} }
}
+143 -237
View File
@@ -5,14 +5,31 @@
//! ------
//!
//! The inspiration and code for this module was originally taken from Dodrio (@fitzgen) and then modified to support
//! Components, Fragments, Suspense, and additional batching operations.
//! Components, Fragments, Suspense, SubTree memoization, and additional batching operations.
//!
//! Implementation Details:
//! -----------------------
//! ## Implementation Details:
//!
//! ### IDs for elements
//!
//! All nodes are addressed by their IDs. The RealDom provides an imperative interface for making changes to these nodes.
//! We don't necessarily require that DOM changes happen instnatly during the diffing process, so the implementor may choose
//! to batch nodes if it is more performant for their application. We care about an ID size of u32
//! to batch nodes if it is more performant for their application. The expectation is that renderers use a Slotmap for nodes
//! whose keys can be converted to u64 on FFI boundaries.
//!
//! When new nodes are created through `render`, they won't know which real node they correspond to. During diffing, we
//! always make sure to copy over the ID. If we don't do this properly, the realdomnode will be populated incorrectly and
//! brick the user's page.
//!
//! ## Subtree Memoization
//! We also employ "subtree memoization" which saves us from having to check trees which take no dynamic content. We can
//! detect if a subtree is "static" by checking if its children are "static". Since we dive into the tree depth-first, the
//! calls to "create" propogate this information upwards. Structures like the one below are entirely static:
//! ```rust
//! rsx!( div { class: "hello world", "this node is entirely static" } )
//! ```
//! Because the subtrees won't be diffed, their "real node" data will be stale (invalid), so its up to the reconciler to
//! track nodes created in a scope and clean up all relevant data. Support for this is currently WIP
//!
//!
//!
//! Further Reading and Thoughts
@@ -25,10 +42,7 @@
use crate::{arena::SharedArena, innerlude::*, tasks::TaskQueue};
use fxhash::{FxHashMap, FxHashSet};
use std::{
any::Any,
rc::{Rc, Weak},
};
use std::any::Any;
/// The accompanying "real dom" exposes an imperative API for controlling the UI layout
///
@@ -46,8 +60,10 @@ pub trait RealDom<'a> {
fn push_root(&mut self, root: RealDomNode);
// Add Nodes to the dom
fn append_child(&mut self);
fn replace_with(&mut self);
// add m nodes from the stack
fn append_children(&mut self, many: u32);
// replace the n-m node on the stack with the m nodes
fn replace_with(&mut self, many: u32);
// Remove Nodesfrom the dom
fn remove(&mut self);
@@ -101,7 +117,10 @@ pub struct DiffMachine<'real, 'bump, Dom: RealDom<'bump>> {
pub seen_nodes: FxHashSet<ScopeIdx>,
}
impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> {
impl<'real, 'bump, Dom> DiffMachine<'real, 'bump, Dom>
where
Dom: RealDom<'bump>,
{
pub fn new(
dom: &'real mut Dom,
components: &'bump SharedArena,
@@ -141,6 +160,10 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> {
// The rsx and html macros strongly discourage dynamic lists not encapsulated by a "Fragment".
// So the sane (and fast!) cases are where the virtual structure stays the same and is easily diffable.
(VNode::Text(old), VNode::Text(new)) => {
if old.is_static {
log::debug!("encountered static text node: {:?}", old.text);
}
if old.text != new.text {
self.dom.push_root(old.dom_id.get());
log::debug!("Text has changed {}, {}", old.text, new.text);
@@ -156,8 +179,8 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> {
// In Dioxus, this is less likely to occur unless through a fragment
if new.tag_name != old.tag_name || new.namespace != old.namespace {
self.dom.push_root(old.dom_id.get());
self.create(new_node);
self.dom.replace_with();
let meta = self.create(new_node);
self.dom.replace_with(meta.added_to_stack);
return;
}
new.dom_id.set(old.dom_id.get());
@@ -181,7 +204,7 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> {
// make sure the component's caller function is up to date
let scope = self.components.try_get_mut(scope_id.unwrap()).unwrap();
// .with_scope(scope_id.unwrap(), |scope| {
scope.caller = Rc::downgrade(&new.caller);
scope.caller = new.caller.clone();
// ack - this doesn't work on its own!
scope.update_children(new.children);
@@ -265,17 +288,17 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> {
// in the case where the old node was a fragment but the new nodes are text,
(VNode::Fragment(_) | VNode::Component(_), VNode::Element(_) | VNode::Text(_)) => {
// find the first real element int the old node
let mut iter = RealChildIterator::new(old_node, self.components);
if let Some(first) = iter.next() {
// replace the very first node with the creation of the element or text
} else {
// there are no real elements in the old fragment...
// We need to load up the next real
}
if let VNode::Component(old) = old_node {
// schedule this component's destructor to be run
todo!()
}
// let mut iter = RealChildIterator::new(old_node, self.components);
// if let Some(first) = iter.next() {
// // replace the very first node with the creation of the element or text
// } else {
// // there are no real elements in the old fragment...
// // We need to load up the next real
// }
// if let VNode::Component(old) = old_node {
// // schedule this component's destructor to be run
// todo!()
// }
}
// In the case where real nodes are being replaced by potentially
(VNode::Element(_) | VNode::Text(_), VNode::Fragment(new)) => {
@@ -283,11 +306,11 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> {
}
(VNode::Text(_), VNode::Element(_)) => {
self.create(new_node);
self.dom.replace_with();
self.dom.replace_with(1);
}
(VNode::Element(_), VNode::Text(_)) => {
self.create(new_node);
self.dom.replace_with();
self.dom.replace_with(1);
}
_ => {
@@ -295,7 +318,28 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> {
}
}
}
}
// When we create new nodes, we need to propagate some information back up the call chain.
// This gives the caller some information on how to handle things like insertins, appending, and subtree discarding.
struct CreateMeta {
is_static: bool,
added_to_stack: u32,
}
impl CreateMeta {
fn new(is_static: bool, added_to_tack: u32) -> Self {
Self {
is_static,
added_to_stack: added_to_tack,
}
}
}
impl<'real, 'bump, Dom> DiffMachine<'real, 'bump, Dom>
where
Dom: RealDom<'bump>,
{
// Emit instructions to create the given virtual node.
//
// The change list stack may have any shape upon entering this function:
@@ -305,15 +349,22 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> {
// When this function returns, the new node is on top of the change list stack:
//
// [... node]
fn create(&mut self, node: &'bump VNode<'bump>) {
// debug_assert!(self.dom.traversal_is_committed());
log::warn!("Creating node!");
fn create(&mut self, node: &'bump VNode<'bump>) -> CreateMeta {
log::warn!("Creating node! ... {:#?}", node);
match node {
VNode::Text(text) => {
let real_id = self.dom.create_text_node(text.text);
text.dom_id.set(real_id);
CreateMeta::new(text.is_static, 1)
}
VNode::Element(el) => {
// we have the potential to completely eliminate working on this node in the future(!)
//
// This can only be done if all of the elements properties (attrs, children, listeners, etc) are static
// While creating these things, keep track if we can memoize this element.
// At the end, we'll set this flag on the element to skip it
let mut is_static: bool = true;
let VElement {
key,
tag_name,
@@ -322,22 +373,27 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> {
children,
namespace,
dom_id,
is_static: el_is_static,
} = el;
// log::info!("Creating {:#?}", node);
let real_id = if let Some(namespace) = namespace {
self.dom.create_element(tag_name, Some(namespace))
} else {
self.dom.create_element(tag_name, None)
};
el.dom_id.set(real_id);
dom_id.set(real_id);
listeners.iter().enumerate().for_each(|(idx, listener)| {
listener.mounted_node.set(real_id);
self.dom
.new_event_listener(listener.event, listener.scope, idx, real_id);
listener.mounted_node.set(real_id);
// if the node has an event listener, then it must be visited ?
is_static = false;
});
for attr in *attributes {
is_static = is_static && attr.is_static;
self.dom.set_attribute(&attr.name, &attr.value, *namespace);
}
@@ -359,28 +415,26 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> {
// }
for child in *children {
self.create(child);
if let VNode::Fragment(_) = child {
// do nothing
// fragments append themselves
} else {
self.dom.append_child();
}
let child_meta = self.create(child);
is_static = is_static && child_meta.is_static;
// append whatever children were generated by this call
self.dom.append_children(child_meta.added_to_stack);
}
if is_static {
log::debug!("created a static node {:#?}", node);
} else {
log::debug!("created a dynamic node {:#?}", node);
}
el_is_static.set(is_static);
CreateMeta::new(is_static, 1)
}
VNode::Component(component) => {
// let real_id = self.dom.create_placeholder();
// let root_id = next_id();
// self.dom.save_known_root(root_id);
VNode::Component(vcomponent) => {
log::debug!("Mounting a new component");
let caller: Weak<WrappedCaller> = Rc::downgrade(&component.caller);
// We're modifying the component arena while holding onto references into the assoiated bump arenas of its children
// those references are stable, even if the component arena moves around in memory, thanks to the bump arenas.
// However, there is no way to convey this to rust, so we need to use unsafe to pierce through the lifetime.
let caller = vcomponent.caller.clone();
let parent_idx = self.cur_idx;
@@ -398,50 +452,69 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> {
height,
self.event_queue.new_channel(height, new_idx),
self.components.clone(),
component.children,
vcomponent.children,
self.task_queue.new_submitter(),
)
})
})
.unwrap();
{
let cur_component = self.components.try_get_mut(idx).unwrap();
let mut ch = cur_component.descendents.borrow_mut();
ch.insert(idx);
std::mem::drop(ch);
}
// This code is supposed to insert the new idx into the parent's descendent list, but it doesn't really work.
// This is mostly used for cleanup - to remove old scopes when components are destroyed.
// TODO
//
// self.components
// .try_get_mut(idx)
// .unwrap()
// .descendents
// .borrow_mut()
// .insert(idx);
// yaaaaay lifetimes out of thin air
// really tho, we're merging the frame lifetimes together
// TODO: abstract this unsafe into the arena abstraction
let inner: &'bump mut _ = unsafe { &mut *self.components.components.get() };
let new_component = inner.get_mut(idx).unwrap();
// Actually initialize the caller's slot with the right address
component.ass_scope.set(Some(idx));
vcomponent.ass_scope.set(Some(idx));
// Run the scope for one iteration to initialize it
new_component.run_scope().unwrap();
// And then run the diff algorithm
let _real_id = self.dom.create_placeholder();
// By using "diff_node" instead of "create", we delegate the mutations to the child
// However, "diff_node" always expects a real node on the stack, so we put a placeholder so it knows where to start.
//
// TODO: we need to delete (IE relcaim this node, otherwise the arena will grow infinitely)
let _ = self.dom.create_placeholder();
self.diff_node(new_component.old_frame(), new_component.next_frame());
// Finally, insert this node as a seen node.
self.seen_nodes.insert(idx);
// Virtual Components don't result in new nodes on the stack
// However, we can skip them from future diffing if they take no children, have no props, take no key, etc.
CreateMeta::new(vcomponent.is_static, 0)
}
// we go the the "known root" but only operate on a sibling basis
// Fragments are the only nodes that can contain dynamic content (IE through curlies or iterators).
// We can never ignore their contents, so the prescence of a fragment indicates that we need always diff them.
VNode::Fragment(frag) => {
// create the children directly in the space
for child in frag.children {
self.create(child);
self.dom.append_child();
let mut nodes_added = 0;
for child in frag.children.iter().rev() {
// different types of nodes will generate different amounts on the stack
// nested fragments will spew a ton of nodes onto the stack
// TODO: make sure that our order (.rev) makes sense in a nested situation
let new_meta = self.create(child);
nodes_added += new_meta.added_to_stack;
}
// Never ignore
CreateMeta::new(false, nodes_added)
}
VNode::Suspended { real } => {
let id = self.dom.create_placeholder();
real.set(id);
CreateMeta::new(false, 1)
}
}
}
@@ -1171,11 +1244,9 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> {
//
// When this function returns, the change list stack is in the same state.
pub fn create_and_append_children(&mut self, new: &'bump [VNode<'bump>]) {
// debug_assert!(self.dom.traversal_is_committed());
for child in new {
// self.create_and_append(node, parent)
self.create(child);
self.dom.append_child();
let meta = self.create(child);
self.dom.append_children(meta.added_to_stack);
}
}
@@ -1227,168 +1298,3 @@ enum KeyedPrefixResult {
// the beginning of `new` and `old` we already processed.
MoreWorkToDo(usize),
}
/// This iterator iterates through a list of virtual children and only returns real children (Elements or Text).
///
/// This iterator is useful when it's important to load the next real root onto the top of the stack for operations like
/// "InsertBefore".
struct RealChildIterator<'a> {
scopes: &'a SharedArena,
// Heuristcally we should never bleed into 5 completely nested fragments/components
// Smallvec lets us stack allocate our little stack machine so the vast majority of cases are sane
stack: smallvec::SmallVec<[(u16, &'a VNode<'a>); 5]>,
}
impl<'a> RealChildIterator<'a> {
fn new(starter: &'a VNode<'a>, scopes: &'a SharedArena) -> Self {
Self {
scopes,
stack: smallvec::smallvec![(0, starter)],
}
}
}
// impl<'a> DoubleEndedIterator for ChildIterator<'a> {
// fn next_back(&mut self) -> Option<Self::Item> {
// todo!()
// }
// }
impl<'a> Iterator for RealChildIterator<'a> {
type Item = &'a VNode<'a>;
fn next(&mut self) -> Option<&'a VNode<'a>> {
let mut should_pop = false;
let mut returned_node = None;
let mut should_push = None;
while returned_node.is_none() {
if let Some((count, node)) = self.stack.last_mut() {
match node {
// We can only exit our looping when we get "real" nodes
VNode::Element(_) | VNode::Text(_) => {
// We've recursed INTO an element/text
// We need to recurse *out* of it and move forward to the next
should_pop = true;
returned_node = Some(&**node);
}
// If we get a fragment we push the next child
VNode::Fragment(frag) => {
let _count = *count as usize;
if _count >= frag.children.len() {
should_pop = true;
} else {
should_push = Some(&frag.children[_count]);
}
}
// Immediately abort suspended nodes - can't do anything with them yet
// VNode::Suspended => should_pop = true,
VNode::Suspended { real } => todo!(),
// For components, we load their root and push them onto the stack
VNode::Component(sc) => {
let scope = self.scopes.try_get(sc.ass_scope.get().unwrap()).unwrap();
// Simply swap the current node on the stack with the root of the component
*node = scope.root();
}
}
} else {
// If there's no more items on the stack, we're done!
return None;
}
if should_pop {
self.stack.pop();
if let Some((id, _)) = self.stack.last_mut() {
*id += 1;
}
should_pop = false;
}
if let Some(push) = should_push {
self.stack.push((0, push));
should_push = None;
}
}
returned_node
}
}
mod tests {
use super::*;
use crate as dioxus;
use crate::innerlude::*;
use crate::util::DebugDom;
use dioxus_core_macro::*;
// #[test]
// fn test_child_iterator() {
// static App: FC<()> = |cx| {
// cx.render(rsx! {
// Fragment {
// div {}
// h1 {}
// h2 {}
// h3 {}
// Fragment {
// "internal node"
// div {
// "baller text shouldn't show up"
// }
// p {
// }
// Fragment {
// Fragment {
// "wow you really like framgents"
// Fragment {
// "why are you like this"
// Fragment {
// "just stop now please"
// Fragment {
// "this hurts"
// Fragment {
// "who needs this many fragments?????"
// Fragment {
// "just... fine..."
// Fragment {
// "no"
// }
// }
// }
// }
// }
// }
// }
// }
// }
// "my text node 1"
// "my text node 2"
// "my text node 3"
// "my text node 4"
// }
// })
// };
// let mut dom = VirtualDom::new(App);
// let mut renderer = DebugDom::new();
// dom.rebuild(&mut renderer).unwrap();
// let starter = dom.base_scope().root();
// let ite = RealChildIterator::new(starter, &dom.components);
// for child in ite {
// match child {
// VNode::Element(el) => println!("Found: Element {}", el.tag_name),
// VNode::Text(t) => println!("Found: Text {:?}", t.text),
// // These would represent failing cases.
// VNode::Fragment(_) => panic!("Found: Fragment"),
// VNode::Suspended { real } => panic!("Found: Suspended"),
// VNode::Component(_) => panic!("Found: Component"),
// }
// }
// }
}
+1 -1
View File
@@ -4,7 +4,7 @@
//! 3rd party renderers are responsible for converting their native events into these virtual event types. Events might
//! be heavy or need to interact through FFI, so the events themselves are designed to be lazy.
use std::{ops::Deref, rc::Rc};
use std::{cell::Cell, ops::Deref, rc::Rc};
use crate::innerlude::{RealDomNode, ScopeIdx};
+2
View File
@@ -43,6 +43,7 @@ pub(crate) mod innerlude {
pub use crate::nodebuilder::*;
pub use crate::nodes::*;
pub use crate::scope::*;
pub use crate::tasks::*;
pub use crate::util::*;
pub use crate::virtual_dom::*;
@@ -62,6 +63,7 @@ pub mod prelude {
pub use crate::styles::{AsAttr, StyleBuilder};
pub use crate::util::RealDomNode;
pub use crate::virtual_dom::VirtualDom;
pub use nodes::*;
pub use crate::nodebuilder::LazyNodes;
+73 -154
View File
@@ -308,7 +308,7 @@ where
/// let my_div: VNode = builder.finish();
/// ```
#[inline]
pub fn finish(self) -> VNode<'a> {
pub fn finish(mut self) -> VNode<'a> {
let bump = self.cx.bump();
let children: &'a Children = bump.alloc(self.children);
@@ -317,6 +317,23 @@ where
let listeners: &'a Listeners = bump.alloc(self.listeners);
let listeners: &'a [Listener<'a>] = listeners.as_ref();
for listener in listeners {
// bump the context id forward
let id = self.cx.listener_id.get();
self.cx.listener_id.set(id + 1);
// Add this listener to the context list
// This casts the listener to a self-referential pointer
// This is okay because the bump arena is stable
// TODO: maybe not add it into CTX immediately
let r = unsafe { std::mem::transmute::<&Listener<'a>, &Listener<'static>>(listener) };
self.cx.scope_ref.listeners.borrow_mut().push((
r.mounted_node as *const _ as *mut _,
r.callback as *const _ as *mut _,
));
}
let attributes: &'a Attributes = bump.alloc(self.attributes);
let attributes: &'a [Attribute<'a>] = attributes.as_ref();
@@ -338,57 +355,57 @@ where
Attributes: 'a + AsRef<[Attribute<'a>]>,
Children: 'a + AsRef<[VNode<'a>]>,
{
/// Add a new event listener to this element.
///
/// The `event` string specifies which event will be listened for. The
/// `callback` function is the function that will be invoked if the
/// specified event occurs.
///
/// # Example
///
/// ```no_run
/// use dioxus::{builder::*, bumpalo::Bump};
///
/// let b = Bump::new();
///
/// // A button that does something when clicked!
/// let my_button = button(&b)
/// .on("click", |event| {
/// // ...
/// })
/// .finish();
/// ```
pub fn on(self, event: &'static str, callback: impl FnMut(VirtualEvent) + 'a) -> Self {
let bump = &self.cx.bump();
let listener = Listener {
event,
callback: bump.alloc(callback),
scope: self.cx.scope_ref.arena_idx,
mounted_node: bump.alloc(Cell::new(RealDomNode::empty())),
};
self.add_listener(listener)
}
// / Add a new event listener to this element.
// /
// / The `event` string specifies which event will be listened for. The
// / `callback` function is the function that will be invoked if the
// / specified event occurs.
// /
// / # Example
// /
// / ```no_run
// / use dioxus::{builder::*, bumpalo::Bump};
// /
// / let b = Bump::new();
// /
// / // A button that does something when clicked!
// / let my_button = button(&b)
// / .on("click", |event| {
// / // ...
// / })
// / .finish();
// / ```
// pub fn on(self, event: &'static str, callback: impl FnMut(VirtualEvent) + 'a) -> Self {
// let bump = &self.cx.bump();
// let listener = Listener {
// event,
// callback: bump.alloc(callback),
// scope: self.cx.scope_ref.arena_idx,
// mounted_node: bump.alloc(Cell::new(RealDomNode::empty())),
// };
// self.add_listener(listener)
// }
pub fn add_listener(mut self, listener: Listener<'a>) -> Self {
self.listeners.push(listener);
// pub fn add_listener(mut self, listener: Listener<'a>) -> Self {
// self.listeners.push(listener);
// bump the context id forward
let id = self.cx.listener_id.get();
self.cx.listener_id.set(id + 1);
// // bump the context id forward
// let id = self.cx.listener_id.get();
// self.cx.listener_id.set(id + 1);
// Add this listener to the context list
// This casts the listener to a self-referential pointer
// This is okay because the bump arena is stable
self.listeners.last().map(|g| {
let r = unsafe { std::mem::transmute::<&Listener<'a>, &Listener<'static>>(g) };
self.cx.scope_ref.listeners.borrow_mut().push((
r.mounted_node as *const _ as *mut _,
r.callback as *const _ as *mut _,
));
});
// // Add this listener to the context list
// // This casts the listener to a self-referential pointer
// // This is okay because the bump arena is stable
// self.listeners.last().map(|g| {
// let r = unsafe { std::mem::transmute::<&Listener<'a>, &Listener<'static>>(g) };
// self.cx.scope_ref.listeners.borrow_mut().push((
// r.mounted_node as *const _ as *mut _,
// r.callback as *const _ as *mut _,
// ));
// });
self
}
// self
// }
}
impl<'a, 'b, Listeners, Children>
@@ -410,61 +427,16 @@ where
/// let my_div = div(&b).attr("id", "my-div").finish();
/// ```
pub fn attr(mut self, name: &'static str, args: std::fmt::Arguments) -> Self {
let value = match args.as_str() {
Some(static_str) => static_str,
None => {
use bumpalo::core_alloc::fmt::Write;
let mut s = bumpalo::collections::String::new_in(self.cx.bump());
s.write_fmt(args).unwrap();
s.into_bump_str()
}
};
let (value, is_static) = raw_text(self.cx.bump(), args);
self.attributes.push(Attribute {
name,
value,
is_static,
namespace: None,
});
self
}
/// Conditionally add a "boolean-style" attribute to this element.
///
/// If the `should_add` parameter is true, then adds an attribute with the
/// given `name` and an empty string value. If the `should_add` parameter is
/// false, then the attribute is not added.
///
/// This method is useful for attributes whose semantics are defined by
/// whether or not the attribute is present or not, and whose value is
/// ignored. Example attributes like this include:
///
/// * `checked`
/// * `hidden`
/// * `selected`
///
/// # Example
///
/// ```no_run
/// use dioxus::{builder::*, bumpalo::Bump};
/// use js_sys::Math;
///
/// let b = Bump::new();
///
/// // Create the `<div>` that is randomly hidden 50% of the time.
/// let my_div = div(&b)
/// .bool_attr("hidden", Math::random() >= 0.5)
/// .finish();
/// ```
pub fn bool_attr(mut self, name: &'static str, should_add: bool) -> Self {
if should_add {
self.attributes.push(Attribute {
name,
value: "",
namespace: None,
});
}
self
}
}
impl<'a, 'b, Listeners, Attributes>
@@ -618,72 +590,18 @@ impl IntoVNode<'_> for Option<()> {
}
}
/// Construct a text VNode.
///
/// This is `dioxus`'s virtual DOM equivalent of `document.createTextVNode`.
///
/// # Example
///
/// ```no_run
/// use dioxus::builder::*;
///
/// let my_text = text("hello, dioxus!");
/// ```
#[inline]
pub fn text<'a>(contents: &'a str) -> VNode<'a> {
VNode::text(contents)
}
pub fn text2<'a>(contents: bumpalo::collections::String<'a>) -> VNode<'a> {
let f: &'a str = contents.into_bump_str();
VNode::text(f)
}
pub fn text3<'a>(bump: &'a bumpalo::Bump, args: std::fmt::Arguments) -> VNode<'a> {
// This is a cute little optimization
//
// We use format_args! on everything. However, not all textnodes have dynamic content, and thus are completely static.
// we can just short-circuit to the &'static str version instead of having to allocate in the bump arena.
//
// In the most general case, this prevents the need for any string allocations for simple code IE:
// ```
// div {"abc"}
// ```
VNode::text(raw_text(bump, args))
}
pub fn raw_text<'a>(bump: &'a bumpalo::Bump, args: std::fmt::Arguments) -> &'a str {
pub fn raw_text<'a>(bump: &'a bumpalo::Bump, args: std::fmt::Arguments) -> (&'a str, bool) {
match args.as_str() {
Some(static_str) => static_str,
Some(static_str) => (static_str, true),
None => {
use bumpalo::core_alloc::fmt::Write;
let mut s = bumpalo::collections::String::new_in(bump);
s.write_fmt(args).unwrap();
s.into_bump_str()
(s.into_bump_str(), false)
}
}
}
/// Construct an attribute for an element.
///
/// # Example
///
/// This example creates the `id="my-id"` for some element like `<div
/// id="my-id"/>`.
///
/// ```no_run
/// use dioxus::builder::*;
///
/// let my_id_attr = attr("id", "my-id");
/// ```
pub fn attr<'a>(name: &'static str, value: &'a str) -> Attribute<'a> {
Attribute {
name,
value,
namespace: None,
}
}
pub fn virtual_child<'a, T: Properties + 'a>(
cx: NodeFactory<'a>,
f: FC<T>,
@@ -726,7 +644,7 @@ impl<'a> NodeFactory<'a> {
/// Create some text that's allocated along with the other vnodes
pub fn text(&self, args: Arguments) -> VNode<'a> {
text3(self.bump(), args)
VNode::text(self.bump(), args)
}
/// Create an element builder
@@ -749,10 +667,11 @@ impl<'a> NodeFactory<'a> {
val: Arguments,
namespace: Option<&'static str>,
) -> Attribute<'a> {
let value = raw_text(self.bump(), val);
let (value, is_static) = raw_text(self.bump(), val);
Attribute {
name,
value,
is_static,
namespace,
}
}
+43 -14
View File
@@ -7,7 +7,7 @@ use crate::{
arena::SharedArena,
events::VirtualEvent,
innerlude::{Context, Properties, RealDom, RealDomNode, Scope, ScopeIdx, FC},
nodebuilder::{text3, NodeFactory},
nodebuilder::NodeFactory,
};
use appendlist::AppendList;
use bumpalo::Bump;
@@ -39,7 +39,7 @@ pub enum VNode<'src> {
/// A "suspended component"
/// This is a masqeurade over an underlying future that needs to complete
/// When the future is completed, the VNode will then trigger a render
/// When the future is completed, the VNode will then trigger a render and the `real` field gets populated
Suspended { real: Cell<RealDomNode> },
/// A User-defined componen node (node type COMPONENT_NODE)
@@ -84,6 +84,7 @@ impl<'old, 'new> VNode<'old> {
},
namespace: el.namespace.clone(),
dom_id: el.dom_id.clone(),
is_static: el.is_static.clone(),
};
VNode::Element(new.alloc_with(move || new_el))
@@ -121,21 +122,33 @@ impl<'a> VNode<'a> {
children,
namespace,
dom_id: Cell::new(RealDomNode::empty()),
is_static: Cell::new(false),
});
VNode::Element(element)
}
/// Construct a new text node with the given text.
#[inline]
pub fn text(text: &'a str) -> VNode<'a> {
pub fn static_text(text: &'static str) -> VNode {
VNode::Text(VText {
text,
is_static: true,
dom_id: Cell::new(RealDomNode::empty()),
})
}
pub fn text_args(bump: &'a Bump, args: Arguments) -> VNode<'a> {
text3(bump, args)
/// Construct a new text node with the given text.
pub fn text(bump: &'a Bump, args: Arguments) -> VNode<'a> {
match args.as_str() {
Some(text) => VNode::static_text(text),
None => {
use bumpalo::core_alloc::fmt::Write;
let mut s = bumpalo::collections::String::new_in(bump);
s.write_fmt(args).unwrap();
VNode::Text(VText {
text: s.into_bump_str(),
is_static: false,
dom_id: Cell::new(RealDomNode::empty()),
})
}
}
}
#[inline]
@@ -191,6 +204,7 @@ impl Debug for VNode<'_> {
#[derive(Clone)]
pub struct VText<'src> {
pub text: &'src str,
pub is_static: bool,
pub dom_id: Cell<RealDomNode>,
}
@@ -208,6 +222,7 @@ pub struct VElement<'a> {
pub children: &'a [VNode<'a>],
pub namespace: Option<&'static str>,
pub dom_id: Cell<RealDomNode>,
pub is_static: Cell<bool>,
}
/// An attribute on a DOM node, such as `id="my-thing"` or
@@ -216,6 +231,7 @@ pub struct VElement<'a> {
pub struct Attribute<'a> {
pub name: &'static str,
pub value: &'a str,
pub is_static: bool,
/// If an attribute is "namespaced", then it belongs to a group
/// The most common namespace is the "style" namespace
@@ -336,6 +352,8 @@ pub struct VComponent<'src> {
pub comparator: Option<&'src dyn Fn(&VComponent) -> bool>,
pub is_static: bool,
// a pointer into the bump arena (given by the 'src lifetime)
// raw_props: Box<dyn Any>,
raw_props: *const (),
@@ -402,6 +420,15 @@ impl<'a> VComponent<'a> {
None => NodeKey(None),
};
let caller = create_component_caller(component, raw_props);
// If the component does not have children, has no props (we can't memoize props), and has no no key, then we don't
// need to bother diffing it in the future
//
// This is more of an optimization to prevent unnecessary descending through the tree during diffing, rather than
// actually speeding up the diff process itself
let is_static = children.len() == 0 && P::IS_STATIC && key.is_none();
Self {
user_fc,
comparator,
@@ -409,7 +436,8 @@ impl<'a> VComponent<'a> {
children,
ass_scope: Cell::new(None),
key,
caller: create_closure(component, raw_props),
caller,
is_static,
mounted_root: Cell::new(RealDomNode::empty()),
}
}
@@ -417,14 +445,14 @@ impl<'a> VComponent<'a> {
type Captured<'a> = Rc<dyn for<'r> Fn(&'r Scope) -> VNode<'r> + 'a>;
fn create_closure<'a, P: 'a>(
pub fn create_component_caller<'a, P: 'a>(
user_component: FC<P>,
raw_props: *const (),
) -> Rc<dyn for<'r> Fn(&'r Scope) -> VNode<'r>> {
let g: Captured = Rc::new(move |scp: &Scope| -> VNode {
// cast back into the right lifetime
let safe_props: &'_ P = unsafe { &*(raw_props as *const P) };
let tasks = AppendList::new();
let tasks = RefCell::new(Vec::new());
let cx: Context<P> = Context {
props: safe_props,
scope: scp,
@@ -433,11 +461,12 @@ fn create_closure<'a, P: 'a>(
let g = user_component(cx);
// collect the submitted tasks
println!("tasks submittted: {:#?}", tasks.len());
// log::debug!("tasks submittted: {:#?}", tasks.len());
for task in tasks.borrow_mut().drain(..) {
scp.submit_task(task);
}
let g2 = unsafe { std::mem::transmute(g) };
g2
});
let r: Captured<'static> = unsafe { std::mem::transmute(g) };
+42 -28
View File
@@ -2,10 +2,10 @@ use crate::hooklist::HookList;
use crate::{arena::SharedArena, innerlude::*};
use appendlist::AppendList;
use bumpalo::Bump;
use futures::FutureExt;
use slotmap::DefaultKey;
use slotmap::SlotMap;
use std::marker::PhantomData;
use std::sync::Arc;
use std::{
any::{Any, TypeId},
cell::{Cell, RefCell},
@@ -14,7 +14,7 @@ use std::{
future::Future,
ops::Deref,
pin::Pin,
rc::{Rc, Weak},
rc::Rc,
};
// We need to pin the hook so it doesn't move as we initialize the list of hooks
@@ -51,7 +51,7 @@ pub struct Scope {
pub event_channel: Rc<dyn Fn() + 'static>,
pub caller: Weak<WrappedCaller>,
pub caller: Rc<WrappedCaller>,
// ==========================
// slightly unsafe stuff
@@ -73,6 +73,8 @@ pub struct Scope {
// NEEDS TO BE PRIVATE
pub(crate) listeners: RefCell<Vec<(*mut Cell<RealDomNode>, *mut dyn FnMut(VirtualEvent))>>,
pub task_submitter: TaskSubmitter,
pub(crate) suspended_tasks: Vec<*mut Pin<Box<dyn Future<Output = VNode<'static>>>>>,
}
@@ -85,13 +87,14 @@ impl Scope {
// Scopes cannot be made anywhere else except for this file
// Therefore, their lifetimes are connected exclusively to the virtual dom
pub fn new<'creator_node>(
caller: Weak<WrappedCaller>,
caller: Rc<WrappedCaller>,
arena_idx: ScopeIdx,
parent: Option<ScopeIdx>,
height: u32,
event_channel: EventChannel,
arena_link: SharedArena,
child_nodes: &'creator_node [VNode<'creator_node>],
task_submitter: TaskSubmitter,
) -> Self {
log::debug!(
"New scope created, height is {}, idx is {:?}",
@@ -109,6 +112,7 @@ impl Scope {
height,
event_channel,
arena_link,
task_submitter,
listener_idx: Default::default(),
frames: ActiveFrame::new(),
hooks: Default::default(),
@@ -119,7 +123,7 @@ impl Scope {
}
}
pub fn update_caller<'creator_node>(&mut self, caller: Weak<WrappedCaller>) {
pub fn update_caller<'creator_node>(&mut self, caller: Rc<WrappedCaller>) {
self.caller = caller;
}
@@ -136,19 +140,15 @@ impl Scope {
// This breaks any latent references, invalidating every pointer referencing into it.
self.frames.next().bump.reset();
log::debug!("clearing listeners!");
// Remove all the outdated listeners
self.listeners.borrow_mut().clear();
unsafe { self.hooks.reset() };
self.listener_idx.set(0);
let caller = self
.caller
.upgrade()
.ok_or(Error::FatalInternal("Failed to get caller"))?;
// Cast the caller ptr from static to one with our own reference
let c3: &WrappedCaller = caller.as_ref();
let c3: &WrappedCaller = self.caller.as_ref();
self.frames.cur_frame_mut().head_node = unsafe { self.call_user_component(c3) };
@@ -176,28 +176,42 @@ impl Scope {
// Convert the raw ptr into an actual object
// This operation is assumed to be safe
log::debug!("Calling listeners! {:?}", self.listeners.borrow().len());
let mut listners = self.listeners.borrow_mut();
let (_, listener) = listners
.iter()
.find(|(domptr, _)| {
let p = unsafe { &**domptr };
p.get() == real_node_id.expect("realdomnode not found, propery handling of true virtual events not managed")
})
.expect(&format!(
"Failed to find real node with ID {:?}",
real_node_id
));
log::debug!(
"There are {:?} listeners associated with this scope {:#?}",
self.listeners.borrow().len(),
self.arena_idx
);
// TODO: Don'tdo a linear scan! Do a hashmap lookup! It'll be faster!
unsafe {
let mut listener_fn = &mut **listener;
listener_fn(event);
let mut listners = self.listeners.borrow_mut();
// let listener = listners.get(trigger);
let raw_listener = listners.iter().find(|(domptr, _)| {
let search = unsafe { &**domptr };
let search_id = search.get();
log::info!("searching listener {:#?}", search_id);
match real_node_id {
Some(e) => search_id == e,
None => false,
}
});
match raw_listener {
Some((_node, listener)) => unsafe {
// TODO: Don'tdo a linear scan! Do a hashmap lookup! It'll be faster!
let listener_fn = &mut **listener;
listener_fn(event);
},
None => todo!(),
}
Ok(())
}
pub fn submit_task(&self, task: &mut Pin<Box<dyn Future<Output = ()>>>) {
log::debug!("Task submitted into scope");
(self.task_submitter)(DTask::new(task, self.arena_idx));
}
pub(crate) fn next_frame<'bump>(&'bump self) -> &'bump VNode<'bump> {
self.frames.current_head_node()
}
@@ -210,7 +224,7 @@ impl Scope {
self.frames.cur_frame()
}
pub(crate) fn root<'a>(&'a self) -> &'a VNode<'a> {
pub fn root<'a>(&'a self) -> &'a VNode<'a> {
&self.frames.cur_frame().head_node
}
}
+1 -4
View File
@@ -12,10 +12,7 @@
//!
//!
use crate::{
innerlude::Attribute,
nodebuilder::{text3, NodeFactory},
};
use crate::{innerlude::Attribute, nodebuilder::NodeFactory};
pub struct StyleBuilder;
pub trait AsAttr<'a> {
+44 -36
View File
@@ -16,14 +16,16 @@ use std::{
task::{Context, Poll},
};
use futures::{Future, Stream, StreamExt};
use futures_util::{Future, Stream, StreamExt};
use slotmap::{DefaultKey, SlotMap};
use crate::{events::EventTrigger, prelude::ScopeIdx};
pub type TaskSubmitter = Arc<dyn Fn(DTask)>;
pub struct TaskQueue {
slots: Arc<RwLock<SlotMap<DefaultKey, DTask>>>,
submitter: Arc<dyn Fn(DTask)>,
submitter: TaskSubmitter,
}
impl TaskQueue {
@@ -34,11 +36,16 @@ impl TaskQueue {
let submitter = Arc::new(move |task| {
let mut slots = slots2.write().unwrap();
log::debug!("Task submitted into global task queue");
slots.insert(task);
});
Self { slots, submitter }
}
pub fn new_submitter(&self) -> TaskSubmitter {
self.submitter.clone()
}
pub fn submit_task(&mut self, task: DTask) -> TaskHandle {
let key = self.slots.write().unwrap().insert(task);
TaskHandle { key }
@@ -126,43 +133,44 @@ impl DTask {
}
}
mod tests {
use std::time::Duration;
// #[cfg(test)]
// mod tests {
// use std::time::Duration;
use super::*;
use bumpalo::Bump;
// use super::*;
// use bumpalo::Bump;
#[async_std::test]
async fn example() {
let bump = Bump::new();
type RawTask = Pin<Box<dyn Future<Output = ()>>>;
// build the three
let f1 = bump.alloc(Box::pin(async {
//
async_std::task::sleep(Duration::from_secs(3)).await;
println!("3 sec")
}) as RawTask);
// #[async_std::test]
// async fn example() {
// let bump = Bump::new();
// type RawTask = Pin<Box<dyn Future<Output = ()>>>;
// // build the three
// let f1 = bump.alloc(Box::pin(async {
// //
// async_std::task::sleep(Duration::from_secs(3)).await;
// println!("3 sec")
// }) as RawTask);
let f2 = bump.alloc(Box::pin(async {
//
async_std::task::sleep(Duration::from_secs(2)).await;
println!("2 sec")
}) as RawTask);
// let f2 = bump.alloc(Box::pin(async {
// //
// async_std::task::sleep(Duration::from_secs(2)).await;
// println!("2 sec")
// }) as RawTask);
let f3 = bump.alloc(Box::pin(async {
//
async_std::task::sleep(Duration::from_secs(1)).await;
println!("1 sec");
}) as RawTask);
// let f3 = bump.alloc(Box::pin(async {
// //
// async_std::task::sleep(Duration::from_secs(1)).await;
// println!("1 sec");
// }) as RawTask);
let mut queue = TaskQueue::new();
queue.submit_task(DTask::debug_new(f1));
queue.submit_task(DTask::debug_new(f2));
queue.submit_task(DTask::debug_new(f3));
// let mut queue = TaskQueue::new();
// queue.submit_task(DTask::debug_new(f1));
// queue.submit_task(DTask::debug_new(f2));
// queue.submit_task(DTask::debug_new(f3));
while !queue.is_empty() {
let next = queue.next().await;
println!("Event received {:#?}", next);
}
}
}
// while !queue.is_empty() {
// let next = queue.next().await;
// println!("Event received {:#?}", next);
// }
// }
// }
+73 -3
View File
@@ -4,6 +4,8 @@ use std::{
vec::Drain,
};
use futures_util::StreamExt;
use crate::innerlude::*;
#[derive(PartialEq, Debug, Clone, Default)]
@@ -67,18 +69,28 @@ impl RealDomNode {
pub struct DebugDom {
counter: u64,
logging: bool,
}
impl DebugDom {
pub fn new() -> Self {
Self { counter: 0 }
Self {
counter: 0,
logging: false,
}
}
pub fn with_logging_enabled() -> Self {
Self {
counter: 0,
logging: true,
}
}
}
impl<'a> RealDom<'a> for DebugDom {
fn push_root(&mut self, root: RealDomNode) {}
fn append_child(&mut self) {}
fn append_children(&mut self, many: u32) {}
fn replace_with(&mut self) {}
fn replace_with(&mut self, many: u32) {}
fn remove(&mut self) {}
@@ -120,3 +132,61 @@ impl<'a> RealDom<'a> for DebugDom {
todo!()
}
}
async fn launch_demo(app: FC<()>) {
let mut dom = VirtualDom::new(app);
let mut real_dom = DebugDom::new();
dom.rebuild(&mut real_dom).unwrap();
while let Some(evt) = dom.tasks.next().await {
//
log::debug!("Event triggered! {:#?}", evt);
}
}
// #[cfg(test)]
// mod tests {
// use super::*;
// use crate as dioxus;
// use std::{pin::Pin, time::Duration};
// use crate::builder::DioxusElement;
// use dioxus::prelude::*;
// use futures::Future;
// #[async_std::test]
// async fn async_tick() {
// static App: FC<()> = |cx| {
// // let mut count = use_state(cx, || 0);
// let mut fut = cx.use_hook(
// move || {
// Box::pin(async {
// //
// let mut tick: i32 = 0;
// loop {
// async_std::task::sleep(Duration::from_millis(250)).await;
// log::debug!("ticking forward... {}", tick);
// tick += 1;
// // match surf::get(ENDPOINT).recv_json::<DogApi>().await {
// // Ok(_) => (),
// // Err(_) => (),
// // }
// }
// }) as Pin<Box<dyn Future<Output = ()> + 'static>>
// },
// |h| h,
// |_| {},
// );
// cx.submit_task(fut);
// cx.render(LazyNodes::new(move |f| {
// f.text(format_args!("it's sorta working"))
// }))
// };
// std::env::set_var("RUST_LOG", "debug");
// env_logger::init();
// launch_demo(App).await;
// }
// }
+48 -38
View File
@@ -24,6 +24,9 @@ use crate::{arena::SharedArena, innerlude::*};
use appendlist::AppendList;
use slotmap::DefaultKey;
use slotmap::SlotMap;
use std::any::Any;
use std::cell::RefCell;
use std::pin::Pin;
use std::{any::TypeId, fmt::Debug, rc::Rc};
pub type ScopeIdx = DefaultKey;
@@ -45,11 +48,9 @@ pub struct VirtualDom {
/// All components dump their updates into a queue to be processed
pub(crate) event_queue: EventQueue,
pub(crate) tasks: TaskQueue,
pub tasks: TaskQueue,
/// a strong allocation to the "caller" for the original component and its props
#[doc(hidden)]
_root_caller: Rc<WrappedCaller>,
root_props: std::pin::Pin<Box<dyn std::any::Any>>,
/// Type of the original cx. This is stored as TypeId so VirtualDom does not need to be generic.
///
@@ -129,59 +130,36 @@ impl VirtualDom {
pub fn new_with_props<P: Properties + 'static>(root: FC<P>, root_props: P) -> Self {
let components = SharedArena::new(SlotMap::new());
// Normally, a component would be passed as a child in the RSX macro which automatically produces OpaqueComponents
// Here, we need to make it manually, using an RC to force the Weak reference to stick around for the main scope.
let _root_caller: Rc<WrappedCaller> = Rc::new(move |scope: &Scope| {
// let _root_caller: Rc<OpaqueComponent<'static>> = Rc::new(move |scope| {
// the lifetime of this closure is just as long as the lifetime on the scope reference
// this closure moves root props (which is static) into this closure
let props = unsafe { &*(&root_props as *const _) };
let tasks = AppendList::new();
let t2 = &tasks;
let cx = Context {
props,
scope,
tasks: t2,
};
let nodes = root(cx);
log::debug!("There were {:?} tasks submitted", tasks.len());
// cast a weird lifetime to shake the appendlist thing
// TODO: move all of this into the same logic that governs other components
// we want to wrap everything in a dioxus root component
unsafe { std::mem::transmute(nodes) }
// std::mem::drop(tasks);
//
// nodes
});
// Create a weak reference to the OpaqueComponent for the root scope to use as its render function
let caller_ref = Rc::downgrade(&_root_caller);
let root_props: Pin<Box<dyn Any>> = Box::pin(root_props);
let props_ptr = root_props.as_ref().downcast_ref::<P>().unwrap() as *const P;
// Build a funnel for hooks to send their updates into. The `use_hook` method will call into the update funnel.
let event_queue = EventQueue::default();
let _event_queue = event_queue.clone();
// Make the first scope
// We don't run the component though, so renderers will need to call "rebuild" when they initialize their DOM
let link = components.clone();
let tasks = TaskQueue::new();
let submitter = tasks.new_submitter();
let base_scope = components
.with(|arena| {
arena.insert_with_key(move |myidx| {
let event_channel = _event_queue.new_channel(0, myidx);
Scope::new(caller_ref, myidx, None, 0, event_channel, link, &[])
let caller = crate::nodes::create_component_caller(root, props_ptr as *const _);
Scope::new(caller, myidx, None, 0, event_channel, link, &[], submitter)
})
})
.unwrap();
log::debug!("base scope is {:#?}", base_scope);
Self {
_root_caller,
base_scope,
event_queue,
components,
tasks: TaskQueue::new(),
root_props,
tasks,
_root_prop_type: TypeId::of::<P>(),
}
}
@@ -191,6 +169,38 @@ impl VirtualDom {
// Private Methods for the VirtualDom
// ======================================
impl VirtualDom {
/// Rebuilds the VirtualDOM from scratch, but uses a "dummy" RealDom.
///
/// Used in contexts where a real copy of the structure doesn't matter, and the VirtualDOM is the source of truth.
///
/// ## Why?
///
/// This method uses the `DebugDom` under the hood - essentially making the VirtualDOM's diffing patches a "no-op".
///
/// SSR takes advantage of this by using Dioxus itself as the source of truth, and rendering from the tree directly.
pub fn rebuild_in_place(&mut self) -> Result<()> {
let mut realdom = DebugDom::new();
let mut diff_machine = DiffMachine::new(
&mut realdom,
&self.components,
self.base_scope,
self.event_queue.clone(),
&self.tasks,
);
// Schedule an update and then immediately call it on the root component
// This is akin to a hook being called from a listener and requring a re-render
// Instead, this is done on top-level component
let base = self.components.try_get(self.base_scope)?;
let update = &base.event_channel;
update();
self.progress_completely(&mut diff_machine)?;
Ok(())
}
/// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom rom scratch
/// Currently this doesn't do what we want it to do
pub fn rebuild<'s, Dom: RealDom<'s>>(&'s mut self, realdom: &mut Dom) -> Result<()> {
+1 -1
View File
@@ -7,4 +7,4 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-core = { path = "../core" }
dioxus-core = { path="../core" }
+8
View File
@@ -24,6 +24,14 @@ use std::{
/// Additionally, a ton of std::ops traits are implemented for the `UseState` wrapper, meaning any mutative type operations
/// will automatically be called on the WIP value.
///
/// ## Combinators
///
/// On top of the methods to set/get state, `use_state` also supports fancy combinators to extend its functionality:
/// - `.classic()` and `.split()` convert the hook into the classic React-style hook
/// ```rust
/// let (state, set_state) = use_state(cx, || 10).split()
/// ```
///
///
/// Usage:
/// ```ignore
+1
View File
@@ -11,6 +11,7 @@ dioxus-core = { path = "../core", version = "0.1.0", features = ["serialize"] }
[dev-dependencies]
dioxus-html = { path = "../html" }
tide-websockets = "*"
thiserror = "1.0.23"
log = "0.4.13"
+18
View File
@@ -0,0 +1,18 @@
# Dioxus SSR
Render a Dioxus VirtualDOM to a string.
```rust
// Our app:
const App: FC<()> = |cx| cx.render(rsx!(div {"hello world!"}));
// Build the virtualdom from our app
let mut vdom = VirtualDOM::new(App);
// This runs components, lifecycles, etc. without needing a physical dom. Some features (like noderef) won't work.
vdom.rebuild_in_place();
// Render the entire virtualdom from the root
let text = dioxus_ssr::render_root(&vdom);
assert_eq!(text, "<div>hello world!</div>")
```
+2 -101
View File
@@ -1,102 +1,3 @@
<div title="About W3Schools">
<div title="About W3Schools" style="color:blue;text-align:center" class="About W3Schools">
<p title="About W3Schools">
Hello world!: 0
</p>
</div>
<div title="About W3Schools" style="color:blue;text-align:center" class="About W3Schools">
<p title="About W3Schools">
Hello world!: 1
</p>
</div>
<div title="About W3Schools" style="color:blue;text-align:center" class="About W3Schools">
<p title="About W3Schools">
Hello world!: 2
</p>
</div>
<div title="About W3Schools" style="color:blue;text-align:center" class="About W3Schools">
<p title="About W3Schools">
Hello world!: 3
</p>
</div>
<div title="About W3Schools" style="color:blue;text-align:center" class="About W3Schools">
<p title="About W3Schools">
Hello world!: 4
</p>
</div>
<div title="About W3Schools" style="color:blue;text-align:center" class="About W3Schools">
<p title="About W3Schools">
Hello world!: 5
</p>
</div>
<div title="About W3Schools" style="color:blue;text-align:center" class="About W3Schools">
<p title="About W3Schools">
Hello world!: 6
</p>
</div>
<div title="About W3Schools" style="color:blue;text-align:center" class="About W3Schools">
<p title="About W3Schools">
Hello world!: 7
</p>
</div>
<div title="About W3Schools" style="color:blue;text-align:center" class="About W3Schools">
<p title="About W3Schools">
Hello world!: 8
</p>
</div>
<div title="About W3Schools" style="color:blue;text-align:center" class="About W3Schools">
<p title="About W3Schools">
Hello world!: 9
</p>
</div>
<div title="About W3Schools" style="color:blue;text-align:center" class="About W3Schools">
<p title="About W3Schools">
Hello world!: 10
</p>
</div>
<div title="About W3Schools" style="color:blue;text-align:center" class="About W3Schools">
<p title="About W3Schools">
Hello world!: 11
</p>
</div>
<div title="About W3Schools" style="color:blue;text-align:center" class="About W3Schools">
<p title="About W3Schools">
Hello world!: 12
</p>
</div>
<div title="About W3Schools" style="color:blue;text-align:center" class="About W3Schools">
<p title="About W3Schools">
Hello world!: 13
</p>
</div>
<div title="About W3Schools" style="color:blue;text-align:center" class="About W3Schools">
<p title="About W3Schools">
Hello world!: 14
</p>
</div>
<div title="About W3Schools" style="color:blue;text-align:center" class="About W3Schools">
<p title="About W3Schools">
Hello world!: 15
</p>
</div>
<div title="About W3Schools" style="color:blue;text-align:center" class="About W3Schools">
<p title="About W3Schools">
Hello world!: 16
</p>
</div>
<div title="About W3Schools" style="color:blue;text-align:center" class="About W3Schools">
<p title="About W3Schools">
Hello world!: 17
</p>
</div>
<div title="About W3Schools" style="color:blue;text-align:center" class="About W3Schools">
<p title="About W3Schools">
Hello world!: 18
</p>
</div>
<div title="About W3Schools" style="color:blue;text-align:center" class="About W3Schools">
<p title="About W3Schools">
Hello world!: 19
</p>
</div>
<div>
hello world!
</div>
+103 -54
View File
@@ -7,79 +7,128 @@
//! an external renderer is needed to progress the component lifecycles. The `TextRenderer` shows how to use the Virtual DOM
//! API to progress these lifecycle events to generate a fully-mounted Virtual DOM instance which can be renderer in the
//! `render` method.
//!
//! ```ignore
//! fn main() {
//! let renderer = TextRenderer::<()>::new(|_| html! {<div> "Hello world" </div>});
//! let output = renderer.render();
//! assert_eq!(output, "<div>Hello World</div>");
//! }
//! ```
//!
//! The `TextRenderer` is particularly useful when needing to cache a Virtual DOM in between requests
//!
use dioxus_core::prelude::{VNode, FC};
pub mod tostring;
use std::fmt::{Arguments, Display, Formatter};
pub mod prelude {
pub use dioxus_core::prelude::*;
use dioxus_core::prelude::*;
use dioxus_core::{nodes::VNode, prelude::ScopeIdx, virtual_dom::VirtualDom};
use std::io::{BufWriter, Result, Write};
pub fn render_root(vdom: &VirtualDom) -> String {
format!("{:}", TextRenderer::new(vdom))
}
/// The `TextRenderer` provides a way of rendering a Dioxus Virtual DOM to a String.
/// A configurable text renderer for the Dioxus VirtualDOM.
///
///
/// ## Details
///
/// This uses the `Formatter` infrastructure so you can write into anything that supports `write_fmt`. We can't accept
/// any generic writer, so you need to "Display" the text renderer. This is done through `format!` or `format_args!`
///
///
///
pub struct TextRenderer<T> {
_root_type: std::marker::PhantomData<T>,
///
pub struct TextRenderer<'a> {
vdom: &'a VirtualDom,
}
impl<T> TextRenderer<T> {
/// Create a new text-renderer instance from a functional component root.
/// Automatically progresses the creation of the VNode tree to completion.
///
/// A VDom is automatically created. If you want more granular control of the VDom, use `from_vdom`
pub fn new(root: FC<T>) -> Self {
Self {
_root_type: std::marker::PhantomData {},
impl<'a> TextRenderer<'a> {
fn new(vdom: &'a VirtualDom) -> Self {
Self { vdom }
}
fn html_render(&self, node: &VNode, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match node {
VNode::Element(el) => {
write!(f, "<{}", el.tag_name)?;
for attr in el.attributes {
write!(f, " {}=\"{}\"", attr.name, attr.value)?;
}
write!(f, ">\n")?;
for child in el.children {
self.html_render(child, f)?;
}
write!(f, "\n</{}>", el.tag_name)?;
Ok(())
}
VNode::Text(text) => write!(f, "{}", text.text),
VNode::Suspended { .. } => todo!(),
VNode::Component(vcomp) => {
todo!()
// let id = vcomp.ass_scope.borrow().unwrap();
// let id = vcomp.ass_scope.as_ref().borrow().unwrap();
// let new_node = dom
// .components
// .try_get(id)
// .unwrap()
// .frames
// .current_head_node();
// html_render(&dom, new_node, f)
}
VNode::Fragment(_) => todo!(),
}
}
}
/// Create a new text renderer from an existing Virtual DOM.
/// This will progress the existing VDom's events to completion.
pub fn from_vdom() -> Self {
todo!()
impl Display for TextRenderer<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let root = self.vdom.base_scope();
let root_node = root.root();
self.html_render(root_node, f)
}
}
/// Pass new args to the root function
pub fn update(&mut self, new_val: T) {
todo!()
}
#[cfg(test)]
mod tests {
use super::*;
use dioxus_core as dioxus;
use dioxus_html as dioxus_elements;
const SIMPLE_APP: FC<()> = |cx| {
//
cx.render(rsx!(div {
"hello world!"
}))
};
const SLIGHTLY_MORE_COMPLEX: FC<()> = |cx| {
cx.render(rsx! {
div {
title: "About W3Schools"
{(0..20).map(|f| rsx!{
div {
title: "About W3Schools"
style: "color:blue;text-align:center"
class: "About W3Schools"
p {
title: "About W3Schools"
"Hello world!: {f}"
}
}
})}
}
})
};
/// Modify the root function in place, forcing a re-render regardless if the props changed
pub fn update_mut(&mut self, modifier: impl Fn(&mut T)) {
todo!()
#[test]
fn test_to_string_works() {
let mut dom = VirtualDom::new(SIMPLE_APP);
dom.rebuild_in_place().expect("failed to run virtualdom");
dbg!(render_root(&dom));
}
/// Immediately render a VNode to string
pub fn to_text(root: VNode) -> String {
todo!()
}
#[test]
fn test_write_to_file() {
use std::fs::File;
/// Render the virtual DOM to a string
pub fn render(&self) -> String {
let mut buffer = String::new();
let mut file = File::create("index.html").unwrap();
// iterate through the internal patch queue of virtual dom, and apply them to the buffer
/*
*/
todo!()
}
let mut dom = VirtualDom::new(SIMPLE_APP);
dom.rebuild_in_place().expect("failed to run virtualdom");
/// Render VDom to an existing buffer
/// TODO @Jon, support non-string buffers to actually make this useful
/// Currently, this only supports overwriting an existing buffer, instead of just
pub fn render_mut(&self, buf: &mut String) {
todo!()
file.write_fmt(format_args!("{}", TextRenderer::new(&dom)))
.unwrap();
}
}
-89
View File
@@ -1,89 +0,0 @@
use std::fmt::Display;
use dioxus_core::prelude::*;
use dioxus_core::{nodes::VNode, prelude::ScopeIdx, virtual_dom::VirtualDom};
struct SsrRenderer {
dom: VirtualDom,
}
impl Display for SsrRenderer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let node = self.dom.base_scope().frames.current_head_node();
html_render(&self.dom, node, f)
}
}
// recursively walk the tree
fn html_render(
dom: &VirtualDom,
node: &VNode,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
match node {
VNode::Element(el) => {
write!(f, "<{}", el.tag_name)?;
for attr in el.attributes {
write!(f, " {}=\"{}\"", attr.name, attr.value)?;
}
write!(f, ">\n")?;
for child in el.children {
html_render(dom, child, f)?;
}
write!(f, "\n</{}>", el.tag_name)?;
Ok(())
}
VNode::Text(text) => write!(f, "{}", text),
VNode::Suspended => todo!(),
VNode::Component(vcomp) => {
let id = vcomp.ass_scope.borrow().unwrap();
// let id = vcomp.ass_scope.as_ref().borrow().unwrap();
let new_node = dom
.components
.try_get(id)
.unwrap()
.frames
.current_head_node();
html_render(&dom, new_node, f)
}
VNode::Fragment(_) => todo!(),
}
}
#[test]
fn test_serialize() {
let mut dom = VirtualDom::new(|cx| {
//
//
//
cx.render(rsx! {
div {
title: "About W3Schools"
{(0..20).map(|f| rsx!{
div {
title: "About W3Schools"
// style: "color:blue;text-align:center"
class: "About W3Schools"
p {
title: "About W3Schools"
"Hello world!: {f}"
}
}
})}
}
})
});
dom.rebuild();
let renderer = SsrRenderer { dom };
use std::fs::File;
use std::io::prelude::*;
let mut file = File::create("index.html").unwrap();
let buf = renderer.to_string();
// dbg!(buf);
file.write(buf.as_bytes());
// dbg!(renderer.to_string());
}
+1 -1
View File
@@ -1,3 +1,3 @@
{
"rust-analyzer.inlayHints.enable": false
}
}
+6 -2
View File
@@ -23,11 +23,12 @@ generational-arena = "0.2.8"
wasm-bindgen-test = "0.3.21"
once_cell = "1.7.2"
atoms = { path="../atoms" }
async-channel = "1.6.1"
anyhow = "1.0.41"
slotmap = "1.0.3"
futures-util = "0.3.15"
[dependencies.web-sys]
version = "0.3.50"
features = [
@@ -72,6 +73,9 @@ crate-type = ["cdylib", "rlib"]
[dev-dependencies]
im-rc = "15.0.0"
rand = { version="0.8.4", features=["small_rng"] }
# rand = { version="0.8.4", features=["small_rng"] }
separator = "0.4.1"
uuid = { version="0.8.2", features=["v4", "wasm-bindgen"] }
dioxus-hooks = { path="../hooks" }
gloo-timers = { version="0.2.1", features=["futures"] }
# wasm-timer = "0.2.5"
+71
View File
@@ -0,0 +1,71 @@
//! Basic example that renders a simple VNode to the browser.
use dioxus::events::on::MouseEvent;
use dioxus_core as dioxus;
use dioxus_core::prelude::*;
use dioxus_hooks::*;
use dioxus_html as dioxus_elements;
// use wasm_timer;
use std::future::Future;
use std::{pin::Pin, time::Duration};
use dioxus::prelude::*;
use dioxus_web::*;
fn main() {
// Setup logging
wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
console_error_panic_hook::set_once();
// Run the app
wasm_bindgen_futures::spawn_local(WebsysRenderer::start(App));
}
static App: FC<()> = |cx| {
// let mut count = use_state(cx, || 0);
let fut = cx.use_hook(
move || {
Box::pin(async {
let mut tick: i32 = 0;
log::debug!("yeet!");
loop {
gloo_timers::future::TimeoutFuture::new(250).await;
log::debug!("ticking forward... {}", tick);
tick += 1;
}
}) as Pin<Box<dyn Future<Output = ()> + 'static>>
},
|h| h,
|_| {},
);
cx.submit_task(fut);
let state = use_state(cx, || 0);
cx.render(rsx! {
div {
section { class: "py-12 px-4 text-center"
div { class: "w-full max-w-2xl mx-auto"
span { class: "text-sm font-semibold"
"count: {state}"
}
div {
button {
onclick: move |_| state.set(state + 1)
"incr"
}
br {}
br {}
button {
onclick: move |_| state.set(state - 1)
"decr"
}
}
}
}
}
})
};
+52
View File
@@ -0,0 +1,52 @@
//! Basic example that renders a simple VNode to the browser.
use dioxus::events::on::MouseEvent;
use dioxus_core as dioxus;
use dioxus_core::prelude::*;
use dioxus_hooks::*;
use dioxus_html as dioxus_elements;
// use wasm_timer;
use std::future::Future;
use std::{pin::Pin, time::Duration};
use dioxus::prelude::*;
use dioxus_web::*;
fn main() {
// Setup logging
wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
console_error_panic_hook::set_once();
// Run the app
wasm_bindgen_futures::spawn_local(WebsysRenderer::start(App));
}
static App: FC<()> = |cx| {
let mut state = use_state(cx, || 0);
cx.render(rsx! {
div {
section { class: "py-12 px-4 text-center"
div { class: "w-full max-w-2xl mx-auto"
span { class: "text-sm font-semibold"
"static subtree"
}
}
}
section { class: "py-12 px-4 text-center"
div { class: "w-full max-w-2xl mx-auto"
span { class: "text-sm font-semibold"
"dynamic subtree {state}"
}
div {
button { onclick: move |_| state+=1, "incr" }
br {}
button { onclick: move |_| state-=1, "decr" }
}
}
}
}
})
};
+33 -2
View File
@@ -3,6 +3,8 @@
//! This crate implements a renderer of the Dioxus Virtual DOM for the web browser using Websys.
use dioxus::prelude::{Context, Properties, VNode};
use futures_util::{pin_mut, Stream, StreamExt};
use fxhash::FxHashMap;
use web_sys::{window, Document, Element, Event, Node};
// use futures::{channel::mpsc, SinkExt, StreamExt};
@@ -70,12 +72,41 @@ impl WebsysRenderer {
self.internal_dom.rebuild(&mut websys_dom)?;
while let Some(trigger) = websys_dom.wait_for_event().await {
log::info!("Going into event loop");
loop {
let trigger = {
let real_queue = websys_dom.wait_for_event();
let task_queue = (&mut self.internal_dom.tasks).next();
pin_mut!(real_queue);
pin_mut!(task_queue);
match futures_util::future::select(real_queue, task_queue).await {
futures_util::future::Either::Left((trigger, _)) => trigger,
futures_util::future::Either::Right((trigger, _)) => trigger,
}
};
log::info!("event received");
let root_node = body_element.first_child().unwrap();
websys_dom.stack.push(root_node.clone());
self.internal_dom
.progress_with_event(&mut websys_dom, trigger)?;
.progress_with_event(&mut websys_dom, trigger.unwrap())?;
// let t2 = self.internal_dom.tasks.next();
// futures::select! {
// trigger = t1 => {
// log::info!("event received");
// let root_node = body_element.first_child().unwrap();
// websys_dom.stack.push(root_node.clone());
// self.internal_dom
// .progress_with_event(&mut websys_dom, trigger)?;
// },
// () = t2 => {}
// };
}
// while let Some(trigger) = websys_dom.wait_for_event().await {
// }
Ok(()) // should actually never return from this, should be an error, rustc just cant see it
}
+14 -11
View File
@@ -78,7 +78,7 @@ impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom {
self.stack.push(domnode.clone());
}
fn append_child(&mut self) {
fn append_children(&mut self, many: u32) {
log::debug!("Called [`append_child`]");
let child = self.stack.pop();
@@ -99,7 +99,7 @@ impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom {
self.stack.top().append_child(&child).unwrap();
}
fn replace_with(&mut self) {
fn replace_with(&mut self, many: u32) {
log::debug!("Called [`replace_with`]");
let new_node = self.stack.pop();
let old_node = self.stack.pop();
@@ -193,15 +193,16 @@ impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom {
&mut self,
event: &'static str,
scope: dioxus_core::prelude::ScopeIdx,
el_id: usize,
_element_id: usize,
real_id: RealDomNode,
) {
let (_on, event) = event.split_at(2);
let event = wasm_bindgen::intern(event);
log::debug!(
"Called [`new_event_listener`]: {}, {:?}, {}, {:?}",
event,
scope,
el_id,
_element_id,
real_id
);
// attach the correct attributes to the element
@@ -215,10 +216,11 @@ impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom {
.dyn_ref::<Element>()
.expect(&format!("not an element: {:?}", el));
let gi_id = scope.data().as_ffi();
let scope_id = scope.data().as_ffi();
el.set_attribute(
&format!("dioxus-event-{}", event),
&format!("{}.{}.{}", gi_id, el_id, real_id.0),
&format!("{}.{}", scope_id, real_id.0),
// &format!("{}.{}.{}", gi_id, el_id, real_id.0),
)
.unwrap();
@@ -566,19 +568,20 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<EventTrigger> {
.next()
.and_then(|f| f.parse::<u64>().ok())
.context("failed to parse gi id")?;
let el_id = fields
.next()
.and_then(|f| f.parse::<usize>().ok())
.context("failed to parse el id")?;
// let el_id = fields
// .next()
// .and_then(|f| f.parse::<usize>().ok())
// .context("failed to parse el id")?;
let real_id = fields
.next()
.and_then(|f| f.parse::<u64>().ok().map(RealDomNode::new))
.context("failed to parse real id")?;
// Call the trigger
log::debug!("decoded gi_id: {}, li_idx: {}", gi_id, el_id);
log::debug!("decoded scope_id: {}, node_id: {:#?}", gi_id, real_id);
let triggered_scope: ScopeIdx = KeyData::from_ffi(gi_id).into();
log::debug!("Triggered scope is {:#?}", triggered_scope);
Ok(EventTrigger::new(
virtual_event_from_websys_event(event),
triggered_scope,
+2 -2
View File
@@ -37,11 +37,11 @@ impl<'bump> RealDom<'bump> for WebviewDom<'bump> {
self.edits.push(PushRoot { root: root.0 });
}
fn append_child(&mut self) {
fn append_children(&mut self, many: u32) {
self.edits.push(AppendChild);
}
fn replace_with(&mut self) {
fn replace_with(&mut self, many: u32) {
self.edits.push(ReplaceWith);
}