feat: static node infrastructure and ssr changes
This commit is contained in:
1 parent
975fa566f9
commit
9abb0470b7
34 files changed
+1022
-855
No files matched your search
+10
-6
@@ -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",
|
||||
]
|
||||
@@ -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
@@ -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"}
|
||||
}
|
||||
})
|
||||
};
|
||||
Vendored
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"rust-analyzer.inlayHints.enable": true
|
||||
"rust-analyzer.inlayHints.enable": false
|
||||
}
|
||||
@@ -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"]
|
||||
@@ -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,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(""),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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"),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -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;
|
||||
// }
|
||||
// }
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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" }
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
Vendored
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"rust-analyzer.inlayHints.enable": false
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user