mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-27 14:40:44 +00:00
Merge pull request #17 from jkelleyrtp/jk/jankfree
feat: jank free rendering and noderefs
This commit is contained in:
commit
39cc0849b8
72 changed files with 5689 additions and 3873 deletions
1
.vscode/spellright.dict
vendored
1
.vscode/spellright.dict
vendored
|
@ -65,3 +65,4 @@ asynchronicity
|
|||
constified
|
||||
SegVec
|
||||
contentful
|
||||
Jank
|
||||
|
|
14
Cargo.toml
14
Cargo.toml
|
@ -51,14 +51,22 @@ argh = "0.1.5"
|
|||
env_logger = "*"
|
||||
async-std = { version = "1.9.0", features = ["attributes"] }
|
||||
rand = { version = "0.8.4", features = ["small_rng"] }
|
||||
surf = {version = "2.2.0", git = "https://github.com/jkelleyrtp/surf/", branch = "jk/fix-the-wasm"}
|
||||
surf = { version = "2.2.0", git = "https://github.com/jkelleyrtp/surf/", branch = "jk/fix-the-wasm" }
|
||||
gloo-timers = "0.2.1"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
gloo-timers = "0.2.1"
|
||||
surf = {version = "2.2.0", default-features = false, features = ["wasm-client"], git = "https://github.com/jkelleyrtp/surf/", branch = "jk/fix-the-wasm"}
|
||||
wasm-logger = "0.2.0"
|
||||
console_error_panic_hook = "0.1.6"
|
||||
rand = { version = "0.8.4", features = ["small_rng"] }
|
||||
wasm-bindgen = { version = "0.2.71", features = ["enable-interning"] }
|
||||
|
||||
# surf = { version = "2.2.0", default-features = false, features = [
|
||||
# "wasm-client",
|
||||
# ], git = "https://github.com/jkelleyrtp/surf/", branch = "jk/fix-the-wasm" }
|
||||
|
||||
|
||||
[dependencies.getrandom]
|
||||
[dev-dependencies.getrandom]
|
||||
version = "0.2"
|
||||
features = ["js"]
|
||||
|
||||
|
|
60
README.md
60
README.md
|
@ -147,37 +147,37 @@ Dioxus is heavily inspired by React, but we want your transition to feel like an
|
|||
|
||||
### Phase 1: The Basics
|
||||
|
||||
| Feature | Dioxus | React | Notes for Dioxus |
|
||||
| ------------------------- | ------ | ----- | ----------------------------------------------------------- |
|
||||
| Conditional Rendering | ✅ | ✅ | if/then to hide/show component |
|
||||
| Map, Iterator | ✅ | ✅ | map/filter/reduce to produce rsx! |
|
||||
| Keyed Components | ✅ | ✅ | advanced diffing with keys |
|
||||
| Web | ✅ | ✅ | renderer for web browser |
|
||||
| Desktop (webview) | ✅ | ✅ | renderer for desktop |
|
||||
| Shared State (Context) | ✅ | ✅ | share state through the tree |
|
||||
| Hooks | ✅ | ✅ | memory cells in components |
|
||||
| SSR | ✅ | ✅ | render directly to string |
|
||||
| Component Children | ✅ | ✅ | cx.children() as a list of nodes |
|
||||
| Headless components | ✅ | ✅ | components that don't return real elements |
|
||||
| Fragments | ✅ | ✅ | multiple elements without a real root |
|
||||
| Manual Props | ✅ | ✅ | Manually pass in props with spread syntax |
|
||||
| Controlled Inputs | ✅ | ✅ | stateful wrappers around inputs |
|
||||
| CSS/Inline Styles | ✅ | ✅ | syntax for inline styles/attribute groups |
|
||||
| Custom elements | ✅ | ✅ | Define new element primitives |
|
||||
| Suspense | ✅ | ✅ | schedule future render from future/promise |
|
||||
| Integrated error handling | ✅ | ✅ | Gracefully handle errors with ? syntax |
|
||||
| Re-hydration | ✅ | ✅ | Pre-render to HTML to speed up first contentful paint |
|
||||
| Cooperative Scheduling | 🛠 | ✅ | Prioritize important events over non-important events |
|
||||
| Runs natively | ✅ | ❓ | runs as a portable binary w/o a runtime (Node) |
|
||||
| 1st class global state | ✅ | ❓ | redux/recoil/mobx on top of context |
|
||||
| Subtree Memoization | ✅ | ❓ | skip diffing static element subtrees |
|
||||
| Compile-time correct | ✅ | ❓ | Throw errors on invalid template layouts |
|
||||
| Heuristic Engine | 🛠 | ❓ | track component memory usage to minimize future allocations |
|
||||
| Fine-grained reactivity | 🛠 | ❓ | Skip diffing for fine-grain updates |
|
||||
| Effects | 🛠 | ✅ | Run effects after a component has been committed to render |
|
||||
| NodeRef | 🛠 | ✅ | gain direct access to nodes [1] |
|
||||
| Feature | Dioxus | React | Notes for Dioxus |
|
||||
| ------------------------- | ------ | ----- | -------------------------------------------------------------------- |
|
||||
| Conditional Rendering | ✅ | ✅ | if/then to hide/show component |
|
||||
| Map, Iterator | ✅ | ✅ | map/filter/reduce to produce rsx! |
|
||||
| Keyed Components | ✅ | ✅ | advanced diffing with keys |
|
||||
| Web | ✅ | ✅ | renderer for web browser |
|
||||
| Desktop (webview) | ✅ | ✅ | renderer for desktop |
|
||||
| Shared State (Context) | ✅ | ✅ | share state through the tree |
|
||||
| Hooks | ✅ | ✅ | memory cells in components |
|
||||
| SSR | ✅ | ✅ | render directly to string |
|
||||
| Component Children | ✅ | ✅ | cx.children() as a list of nodes |
|
||||
| Headless components | ✅ | ✅ | components that don't return real elements |
|
||||
| Fragments | ✅ | ✅ | multiple elements without a real root |
|
||||
| Manual Props | ✅ | ✅ | Manually pass in props with spread syntax |
|
||||
| Controlled Inputs | ✅ | ✅ | stateful wrappers around inputs |
|
||||
| CSS/Inline Styles | ✅ | ✅ | syntax for inline styles/attribute groups |
|
||||
| Custom elements | ✅ | ✅ | Define new element primitives |
|
||||
| Suspense | ✅ | ✅ | schedule future render from future/promise |
|
||||
| Integrated error handling | ✅ | ✅ | Gracefully handle errors with ? syntax |
|
||||
| NodeRef | ✅ | ✅ | gain direct access to nodes |
|
||||
| Re-hydration | ✅ | ✅ | Pre-render to HTML to speed up first contentful paint |
|
||||
| Jank-Free Rendering | ✅ | ✅ | Large diffs are segmented across frames for silky-smooth transitions |
|
||||
| Cooperative Scheduling | ✅ | ✅ | Prioritize important events over non-important events |
|
||||
| Runs natively | ✅ | ❓ | runs as a portable binary w/o a runtime (Node) |
|
||||
| 1st class global state | ✅ | ❓ | redux/recoil/mobx on top of context |
|
||||
| Subtree Memoization | ✅ | ❓ | skip diffing static element subtrees |
|
||||
| Compile-time correct | ✅ | ❓ | Throw errors on invalid template layouts |
|
||||
| Heuristic Engine | ✅ | ❓ | track component memory usage to minimize future allocations |
|
||||
| Fine-grained reactivity | 🛠 | ❓ | Skip diffing for fine-grain updates |
|
||||
| Effects | 🛠 | ✅ | Run effects after a component has been committed to render |
|
||||
|
||||
- [1] Currently blocked until we figure out a cross-platform way of exposing an imperative Node API.
|
||||
|
||||
### Phase 2: Advanced Toolkits
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ pub static App: FC<()> = |cx| {
|
|||
}
|
||||
button {
|
||||
"Start counting"
|
||||
onclick: move |_| task.start()
|
||||
onclick: move |_| task.resume()
|
||||
}
|
||||
button {
|
||||
"Switch counting direcion"
|
||||
|
|
|
@ -13,7 +13,7 @@ use dioxus::prelude::*;
|
|||
use dioxus::ssr;
|
||||
|
||||
fn main() {
|
||||
let mut vdom = VirtualDom::launch_in_place(App);
|
||||
let vdom = VirtualDom::new(App);
|
||||
let content = ssr::render_vdom(&vdom, |f| f.pre_render(true));
|
||||
|
||||
dioxus::desktop::launch(App, |c| c.with_prerendered(content)).unwrap();
|
||||
|
|
|
@ -2,6 +2,8 @@ use dioxus::prelude::*;
|
|||
fn main() {}
|
||||
|
||||
pub static Example: FC<()> = |cx| {
|
||||
let p = 10;
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ pub static Example: FC<()> = |cx| {
|
|||
|
||||
// Tasks are 'static, so we need to copy relevant items in
|
||||
let (async_count, dir) = (count.for_async(), *direction);
|
||||
|
||||
|
||||
let (task, result) = use_task(cx, move || async move {
|
||||
loop {
|
||||
gloo_timers::future::TimeoutFuture::new(250).await;
|
||||
|
@ -47,7 +47,7 @@ pub static Example: FC<()> = |cx| {
|
|||
}
|
||||
button {
|
||||
"Start counting"
|
||||
onclick: move |_| task.start()
|
||||
onclick: move |_| task.resume()
|
||||
}
|
||||
button {
|
||||
"Switch counting direcion"
|
||||
|
|
|
@ -6,7 +6,7 @@ pub static Example: FC<()> = |cx| {
|
|||
// Currently, SSR is only supported for whole VirtualDOMs
|
||||
// This is an easy/low hanging fruit to improve upon
|
||||
let mut dom = VirtualDom::new(SomeApp);
|
||||
dom.rebuild_in_place().unwrap();
|
||||
dom.rebuild();
|
||||
ssr::render_vdom(&dom, |c| c)
|
||||
});
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ use dioxus::ssr;
|
|||
|
||||
fn main() {
|
||||
let mut vdom = VirtualDom::new(App);
|
||||
vdom.rebuild_in_place().expect("Rebuilding failed");
|
||||
// vdom.rebuild_in_place().expect("Rebuilding failed");
|
||||
println!("{}", ssr::render_vdom(&vdom, |c| c));
|
||||
}
|
||||
|
||||
|
@ -17,3 +17,10 @@ static App: FC<()> = |cx| {
|
|||
}
|
||||
))
|
||||
};
|
||||
|
||||
struct MyProps<'a> {
|
||||
text: &'a str,
|
||||
}
|
||||
fn App2<'a>(cx: Context<'a, MyProps>) -> DomTree<'a> {
|
||||
None
|
||||
}
|
||||
|
|
136
examples/web_tick.rs
Normal file
136
examples/web_tick.rs
Normal file
|
@ -0,0 +1,136 @@
|
|||
#![allow(non_upper_case_globals, non_snake_case)]
|
||||
//! Example: Webview Renderer
|
||||
//! -------------------------
|
||||
//!
|
||||
//! This example shows how to use the dioxus_desktop crate to build a basic desktop application.
|
||||
//!
|
||||
//! Under the hood, the dioxus_desktop crate bridges a native Dioxus VirtualDom with a custom prebuit application running
|
||||
//! in the webview runtime. Custom handlers are provided for the webview instance to consume patches and emit user events
|
||||
//! into the native VDom instance.
|
||||
//!
|
||||
//! Currently, NodeRefs won't work properly, but all other event functionality will.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
intern_strings();
|
||||
|
||||
dioxus::web::launch(App, |c| c);
|
||||
}
|
||||
|
||||
static App: FC<()> = |cx| {
|
||||
let mut rng = SmallRng::from_entropy();
|
||||
let rows = (0..1_000).map(|f| {
|
||||
let label = Label::new(&mut rng);
|
||||
rsx! {
|
||||
Row {
|
||||
row_id: f,
|
||||
label: label
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
cx.render(rsx! {
|
||||
table {
|
||||
tbody {
|
||||
{rows}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
struct RowProps {
|
||||
row_id: usize,
|
||||
label: Label,
|
||||
}
|
||||
fn Row<'a>(cx: Context<'a, RowProps>) -> DomTree {
|
||||
let [adj, col, noun] = cx.label.0;
|
||||
cx.render(rsx! {
|
||||
tr {
|
||||
td { class:"col-md-1", "{cx.row_id}" }
|
||||
td { class:"col-md-1", onclick: move |_| { /* run onselect */ }
|
||||
a { class: "lbl", "{adj}" "{col}" "{noun}" }
|
||||
}
|
||||
td { class: "col-md-1"
|
||||
a { class: "remove", onclick: move |_| {/* remove */}
|
||||
span { class: "glyphicon glyphicon-remove remove" aria_hidden: "true" }
|
||||
}
|
||||
}
|
||||
td { class: "col-md-6" }
|
||||
}
|
||||
})
|
||||
}
|
||||
use rand::prelude::*;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct Label([&'static str; 3]);
|
||||
|
||||
impl Label {
|
||||
fn new(rng: &mut SmallRng) -> Self {
|
||||
Label([
|
||||
ADJECTIVES.choose(rng).unwrap(),
|
||||
COLOURS.choose(rng).unwrap(),
|
||||
NOUNS.choose(rng).unwrap(),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
static ADJECTIVES: &[&str] = &[
|
||||
"pretty",
|
||||
"large",
|
||||
"big",
|
||||
"small",
|
||||
"tall",
|
||||
"short",
|
||||
"long",
|
||||
"handsome",
|
||||
"plain",
|
||||
"quaint",
|
||||
"clean",
|
||||
"elegant",
|
||||
"easy",
|
||||
"angry",
|
||||
"crazy",
|
||||
"helpful",
|
||||
"mushy",
|
||||
"odd",
|
||||
"unsightly",
|
||||
"adorable",
|
||||
"important",
|
||||
"inexpensive",
|
||||
"cheap",
|
||||
"expensive",
|
||||
"fancy",
|
||||
];
|
||||
|
||||
static COLOURS: &[&str] = &[
|
||||
"red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black",
|
||||
"orange",
|
||||
];
|
||||
|
||||
static NOUNS: &[&str] = &[
|
||||
"table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger",
|
||||
"pizza", "mouse", "keyboard",
|
||||
];
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn intern_strings() {
|
||||
for adj in ADJECTIVES {
|
||||
wasm_bindgen::intern(adj);
|
||||
}
|
||||
for col in COLOURS {
|
||||
wasm_bindgen::intern(col);
|
||||
}
|
||||
for no in NOUNS {
|
||||
wasm_bindgen::intern(no);
|
||||
}
|
||||
wasm_bindgen::intern("col-md-1");
|
||||
wasm_bindgen::intern("col-md-6");
|
||||
wasm_bindgen::intern("glyphicon glyphicon-remove remove");
|
||||
wasm_bindgen::intern("remove");
|
||||
wasm_bindgen::intern("dioxus");
|
||||
wasm_bindgen::intern("lbl");
|
||||
wasm_bindgen::intern("true");
|
||||
}
|
|
@ -39,7 +39,8 @@ impl Parse for AmbiguousElement<AS_RSX> {
|
|||
}
|
||||
}
|
||||
|
||||
if let Ok(name) = input.fork().parse::<Ident>() {
|
||||
use syn::ext::IdentExt;
|
||||
if let Ok(name) = input.fork().call(Ident::parse_any) {
|
||||
let name_str = name.to_string();
|
||||
|
||||
let first_char = name_str.chars().next().unwrap();
|
||||
|
@ -53,9 +54,6 @@ impl Parse for AmbiguousElement<AS_RSX> {
|
|||
.map(|c| AmbiguousElement::Element(c))
|
||||
}
|
||||
} else {
|
||||
if input.peek(LitStr) {
|
||||
panic!("it's actually a litstr");
|
||||
}
|
||||
Err(Error::new(input.span(), "Not a valid Html tag"))
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +78,8 @@ impl Parse for AmbiguousElement<AS_HTML> {
|
|||
}
|
||||
}
|
||||
|
||||
if let Ok(name) = input.fork().parse::<Ident>() {
|
||||
use syn::ext::IdentExt;
|
||||
if let Ok(name) = input.fork().call(Ident::parse_any) {
|
||||
let name_str = name.to_string();
|
||||
|
||||
let first_char = name_str.chars().next().unwrap();
|
||||
|
@ -94,9 +93,6 @@ impl Parse for AmbiguousElement<AS_HTML> {
|
|||
.map(|c| AmbiguousElement::Element(c))
|
||||
}
|
||||
} else {
|
||||
if input.peek(LitStr) {
|
||||
panic!("it's actually a litstr");
|
||||
}
|
||||
Err(Error::new(input.span(), "Not a valid Html tag"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ impl Parse for Element<AS_RSX> {
|
|||
let mut listeners: Vec<ElementAttr<AS_RSX>> = vec![];
|
||||
let mut children: Vec<BodyNode<AS_RSX>> = vec![];
|
||||
let mut key = None;
|
||||
let mut el_ref = None;
|
||||
|
||||
'parsing: loop {
|
||||
// [1] Break if empty
|
||||
|
@ -45,6 +46,7 @@ impl Parse for Element<AS_RSX> {
|
|||
&mut attributes,
|
||||
&mut listeners,
|
||||
&mut key,
|
||||
&mut el_ref,
|
||||
name.clone(),
|
||||
)?;
|
||||
} else {
|
||||
|
@ -237,6 +239,7 @@ fn parse_rsx_element_field(
|
|||
attrs: &mut Vec<ElementAttr<AS_RSX>>,
|
||||
listeners: &mut Vec<ElementAttr<AS_RSX>>,
|
||||
key: &mut Option<LitStr>,
|
||||
el_ref: &mut Option<Expr>,
|
||||
element_name: Ident,
|
||||
) -> Result<()> {
|
||||
let name = Ident::parse_any(stream)?;
|
||||
|
@ -311,8 +314,9 @@ fn parse_rsx_element_field(
|
|||
"namespace" => {
|
||||
todo!("custom namespace not supported")
|
||||
}
|
||||
"ref" => {
|
||||
todo!("NodeRefs are currently not supported! This is currently a reserved keyword.")
|
||||
"node_ref" => {
|
||||
*el_ref = Some(stream.parse::<Expr>()?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Fall through
|
||||
|
|
2
packages/core/.vscode/settings.json
vendored
2
packages/core/.vscode/settings.json
vendored
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"rust-analyzer.inlayHints.enable": true
|
||||
"rust-analyzer.inlayHints.enable": false
|
||||
}
|
||||
|
|
|
@ -16,9 +16,6 @@ dioxus-core-macro = { path = "../core-macro", version = "0.1.1" }
|
|||
# Bumpalo is used as a micro heap backing each component
|
||||
bumpalo = { version = "3.6.0", features = ["collections", "boxed"] }
|
||||
|
||||
# custom error type
|
||||
thiserror = "1"
|
||||
|
||||
# faster hashmaps
|
||||
fxhash = "0.2.1"
|
||||
|
||||
|
@ -26,12 +23,7 @@ fxhash = "0.2.1"
|
|||
longest-increasing-subsequence = "0.1.0"
|
||||
|
||||
# internall used
|
||||
log = "0.4"
|
||||
|
||||
# # Serialize the Edits for use in Webview/Liveview instances
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
|
||||
appendlist = "1.4.0"
|
||||
log = { verison = "0.4", features = ["release_max_level_off"] }
|
||||
|
||||
futures-util = "0.3.15"
|
||||
|
||||
|
@ -41,13 +33,33 @@ slab = "0.4.3"
|
|||
|
||||
futures-channel = "0.3.16"
|
||||
|
||||
# used for noderefs
|
||||
once_cell = "1.8.0"
|
||||
|
||||
# # Serialize the Edits for use in Webview/Liveview instances
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
|
||||
indexmap = "1.7.0"
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0.42"
|
||||
async-std = { version = "1.9.0", features = ["attributes"] }
|
||||
criterion = "0.3.5"
|
||||
dioxus-html = { path = "../html" }
|
||||
fern = { version = "0.6.0", features = ["colored"] }
|
||||
rand = { version = "0.8.4", features = ["small_rng"] }
|
||||
simple_logger = "1.13.0"
|
||||
|
||||
|
||||
[features]
|
||||
default = ["serialize"]
|
||||
serialize = ["serde"]
|
||||
|
||||
[[bench]]
|
||||
name = "create"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "jsframework"
|
||||
harness = false
|
||||
|
|
|
@ -40,42 +40,3 @@ We have big goals for Dioxus. The final implementation must:
|
|||
- Be "live". Components should be able to be both server rendered and client rendered without needing frontend APIs.
|
||||
- Be modular. Components and hooks should be work anywhere without worrying about target platform.
|
||||
|
||||
## Optimizations
|
||||
|
||||
- Support a pluggable allocation strategy that makes VNode creation **very** fast
|
||||
- Support lazy VNodes (ie VNodes that are not actually created when the html! macro is used)
|
||||
- Support advanced diffing strategies (patience, Meyers, etc)
|
||||
|
||||
```rust
|
||||
|
||||
rsx!{ "this is a text node" }
|
||||
|
||||
rsx!{
|
||||
div {}
|
||||
"asd"
|
||||
div {}
|
||||
div {}
|
||||
}
|
||||
rsx!{
|
||||
div {
|
||||
a {}
|
||||
b {}
|
||||
c {}
|
||||
Container {
|
||||
Container {
|
||||
Container {
|
||||
Container {
|
||||
Container {
|
||||
div {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
```
|
||||
|
|
1
packages/core/benches/create.rs
Normal file
1
packages/core/benches/create.rs
Normal file
|
@ -0,0 +1 @@
|
|||
fn main() {}
|
126
packages/core/benches/jsframework.rs
Normal file
126
packages/core/benches/jsframework.rs
Normal file
|
@ -0,0 +1,126 @@
|
|||
#![allow(non_snake_case, non_upper_case_globals)]
|
||||
//! This benchmark tests just the overhead of Dioxus itself.
|
||||
//!
|
||||
//! For the JS Framework Benchmark, both the framework and the browser is benchmarked together. Dioxus prepares changes
|
||||
//! to be made, but the change application phase will be just as performant as the vanilla wasm_bindgen code. In essence,
|
||||
//! we are measuring the overhead of Dioxus, not the performance of the "apply" phase.
|
||||
//!
|
||||
//! On my MBP 2019:
|
||||
//! - Dioxus takes 3ms to create 1_000 rows
|
||||
//! - Dioxus takes 30ms to create 10_000 rows
|
||||
//!
|
||||
//! As pure "overhead", these are amazing good numbers, mostly slowed down by hitting the global allocator.
|
||||
//! These numbers don't represent Dioxus with the heuristic engine installed, so I assume it'll be even faster.
|
||||
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use dioxus_core as dioxus;
|
||||
use dioxus_core::prelude::*;
|
||||
use dioxus_html as dioxus_elements;
|
||||
use rand::prelude::*;
|
||||
|
||||
criterion_group!(mbenches, create_rows);
|
||||
criterion_main!(mbenches);
|
||||
|
||||
fn create_rows(c: &mut Criterion) {
|
||||
static App: FC<()> = |cx| {
|
||||
let mut rng = SmallRng::from_entropy();
|
||||
let rows = (0..10_000).map(|f| {
|
||||
let label = Label::new(&mut rng);
|
||||
rsx! {
|
||||
Row {
|
||||
row_id: f,
|
||||
label: label
|
||||
}
|
||||
}
|
||||
});
|
||||
cx.render(rsx! {
|
||||
table {
|
||||
tbody {
|
||||
{rows}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
c.bench_function("create rows", |b| {
|
||||
b.iter(|| {
|
||||
let mut dom = VirtualDom::new(App);
|
||||
let g = dom.rebuild();
|
||||
assert!(g.edits.len() > 1);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
struct RowProps {
|
||||
row_id: usize,
|
||||
label: Label,
|
||||
}
|
||||
fn Row<'a>(cx: Context<'a, RowProps>) -> DomTree {
|
||||
let [adj, col, noun] = cx.label.0;
|
||||
cx.render(rsx! {
|
||||
tr {
|
||||
td { class:"col-md-1", "{cx.row_id}" }
|
||||
td { class:"col-md-1", onclick: move |_| { /* run onselect */ }
|
||||
a { class: "lbl", "{adj}" "{col}" "{noun}" }
|
||||
}
|
||||
td { class: "col-md-1"
|
||||
a { class: "remove", onclick: move |_| {/* remove */}
|
||||
span { class: "glyphicon glyphicon-remove remove" aria_hidden: "true" }
|
||||
}
|
||||
}
|
||||
td { class: "col-md-6" }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct Label([&'static str; 3]);
|
||||
|
||||
impl Label {
|
||||
fn new(rng: &mut SmallRng) -> Self {
|
||||
Label([
|
||||
ADJECTIVES.choose(rng).unwrap(),
|
||||
COLOURS.choose(rng).unwrap(),
|
||||
NOUNS.choose(rng).unwrap(),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
static ADJECTIVES: &[&str] = &[
|
||||
"pretty",
|
||||
"large",
|
||||
"big",
|
||||
"small",
|
||||
"tall",
|
||||
"short",
|
||||
"long",
|
||||
"handsome",
|
||||
"plain",
|
||||
"quaint",
|
||||
"clean",
|
||||
"elegant",
|
||||
"easy",
|
||||
"angry",
|
||||
"crazy",
|
||||
"helpful",
|
||||
"mushy",
|
||||
"odd",
|
||||
"unsightly",
|
||||
"adorable",
|
||||
"important",
|
||||
"inexpensive",
|
||||
"cheap",
|
||||
"expensive",
|
||||
"fancy",
|
||||
];
|
||||
|
||||
static COLOURS: &[&str] = &[
|
||||
"red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black",
|
||||
"orange",
|
||||
];
|
||||
|
||||
static NOUNS: &[&str] = &[
|
||||
"table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger",
|
||||
"pizza", "mouse", "keyboard",
|
||||
];
|
|
@ -3,20 +3,19 @@ use dioxus_core::prelude::*;
|
|||
fn main() {}
|
||||
|
||||
pub static Example: FC<()> = |cx| {
|
||||
let list = (0..10).map(|f| LazyNodes::new(move |f| todo!()));
|
||||
let list = (0..10).map(|_f| LazyNodes::new(move |_f| todo!()));
|
||||
|
||||
cx.render(LazyNodes::new(move |cx| {
|
||||
let bump = cx.bump();
|
||||
cx.raw_element(
|
||||
"div",
|
||||
None,
|
||||
&mut [],
|
||||
&mut [],
|
||||
cx.bump().alloc([
|
||||
[],
|
||||
[],
|
||||
[
|
||||
cx.text(format_args!("hello")),
|
||||
cx.text(format_args!("hello")),
|
||||
cx.fragment_from_iter(list),
|
||||
]),
|
||||
],
|
||||
None,
|
||||
)
|
||||
}))
|
||||
|
|
|
@ -21,7 +21,7 @@ const App: FC<()> = |cx| {
|
|||
};
|
||||
|
||||
const Task: FC<()> = |cx| {
|
||||
let (task, res) = use_task(cx, || async { true });
|
||||
let (_task, _res) = use_task(cx, || async { true });
|
||||
// task.pause();
|
||||
// task.restart();
|
||||
// task.stop();
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
|
||||
|
||||
fn main() {
|
||||
|
||||
// let g = rsx! {
|
||||
// Fragment {
|
||||
// // div {}
|
||||
// // div {}
|
||||
// // div {}
|
||||
// // div {}
|
||||
// // div {}
|
||||
// // div {}
|
||||
// // div {}
|
||||
// // div {}
|
||||
// // div {}
|
||||
// }
|
||||
// };
|
||||
}
|
|
@ -1,31 +1,27 @@
|
|||
fn main() {}
|
||||
|
||||
use dioxus_core::prelude::*;
|
||||
|
||||
fn App(cx: Context<()>) -> DomTree {
|
||||
//
|
||||
fn main() {}
|
||||
|
||||
fn app(cx: Context<()>) -> DomTree {
|
||||
let vak = use_suspense(
|
||||
cx,
|
||||
|| async {},
|
||||
|c, res| {
|
||||
//
|
||||
c.render(LazyNodes::new(move |f| f.text(format_args!(""))))
|
||||
},
|
||||
|c, _res| c.render(LazyNodes::new(move |f| f.text(format_args!("")))),
|
||||
);
|
||||
|
||||
let d1 = cx.render(LazyNodes::new(move |f| {
|
||||
f.raw_element(
|
||||
"div",
|
||||
None,
|
||||
&mut [],
|
||||
&[],
|
||||
f.bump().alloc([
|
||||
[],
|
||||
[],
|
||||
[
|
||||
f.fragment_from_iter(vak),
|
||||
f.text(format_args!("")),
|
||||
f.text(format_args!("")),
|
||||
f.text(format_args!("")),
|
||||
f.text(format_args!("")),
|
||||
]),
|
||||
],
|
||||
None,
|
||||
)
|
||||
}));
|
||||
|
@ -34,15 +30,15 @@ fn App(cx: Context<()>) -> DomTree {
|
|||
f.raw_element(
|
||||
"div",
|
||||
None,
|
||||
&mut [],
|
||||
&[],
|
||||
f.bump().alloc([
|
||||
[],
|
||||
[],
|
||||
[
|
||||
f.text(format_args!("")),
|
||||
f.text(format_args!("")),
|
||||
f.text(format_args!("")),
|
||||
f.text(format_args!("")),
|
||||
f.fragment_from_iter(d1),
|
||||
]),
|
||||
d1.unwrap(),
|
||||
],
|
||||
None,
|
||||
)
|
||||
}))
|
||||
|
|
109
packages/core/examples/jsframework.rs
Normal file
109
packages/core/examples/jsframework.rs
Normal file
|
@ -0,0 +1,109 @@
|
|||
use dioxus_core as dioxus;
|
||||
use dioxus_core::prelude::*;
|
||||
use dioxus_html as dioxus_elements;
|
||||
use rand::prelude::*;
|
||||
use std::fmt::Display;
|
||||
|
||||
fn main() {
|
||||
let mut dom = VirtualDom::new(App);
|
||||
let g = dom.rebuild();
|
||||
assert!(g.edits.len() > 1);
|
||||
}
|
||||
|
||||
static App: FC<()> = |cx| {
|
||||
let mut rng = SmallRng::from_entropy();
|
||||
let rows = (0..10_000).map(|f| {
|
||||
let label = Label::new(&mut rng);
|
||||
rsx! {
|
||||
Row {
|
||||
row_id: f,
|
||||
label: label
|
||||
}
|
||||
}
|
||||
});
|
||||
cx.render(rsx! {
|
||||
table {
|
||||
tbody {
|
||||
{rows}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
struct RowProps {
|
||||
row_id: usize,
|
||||
label: Label,
|
||||
}
|
||||
fn Row<'a>(cx: Context<'a, RowProps>) -> DomTree {
|
||||
cx.render(rsx! {
|
||||
tr {
|
||||
td { class:"col-md-1", "{cx.row_id}" }
|
||||
td { class:"col-md-1", onclick: move |_| { /* run onselect */ }
|
||||
a { class: "lbl", "{cx.label}" }
|
||||
}
|
||||
td { class: "col-md-1"
|
||||
a { class: "remove", onclick: move |_| {/* remove */}
|
||||
span { class: "glyphicon glyphicon-remove remove" aria_hidden: "true" }
|
||||
}
|
||||
}
|
||||
td { class: "col-md-6" }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct Label([&'static str; 3]);
|
||||
|
||||
impl Label {
|
||||
fn new(rng: &mut SmallRng) -> Self {
|
||||
Label([
|
||||
ADJECTIVES.choose(rng).unwrap(),
|
||||
COLOURS.choose(rng).unwrap(),
|
||||
NOUNS.choose(rng).unwrap(),
|
||||
])
|
||||
}
|
||||
}
|
||||
impl Display for Label {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{} {} {}", self.0[0], self.0[1], self.0[2])
|
||||
}
|
||||
}
|
||||
|
||||
static ADJECTIVES: &[&str] = &[
|
||||
"pretty",
|
||||
"large",
|
||||
"big",
|
||||
"small",
|
||||
"tall",
|
||||
"short",
|
||||
"long",
|
||||
"handsome",
|
||||
"plain",
|
||||
"quaint",
|
||||
"clean",
|
||||
"elegant",
|
||||
"easy",
|
||||
"angry",
|
||||
"crazy",
|
||||
"helpful",
|
||||
"mushy",
|
||||
"odd",
|
||||
"unsightly",
|
||||
"adorable",
|
||||
"important",
|
||||
"inexpensive",
|
||||
"cheap",
|
||||
"expensive",
|
||||
"fancy",
|
||||
];
|
||||
|
||||
static COLOURS: &[&str] = &[
|
||||
"red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black",
|
||||
"orange",
|
||||
];
|
||||
|
||||
static NOUNS: &[&str] = &[
|
||||
"table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger",
|
||||
"pizza", "mouse", "keyboard",
|
||||
];
|
|
@ -1,137 +0,0 @@
|
|||
// #![allow(unused, non_upper_case_globals, non_snake_case)]
|
||||
// use bumpalo::Bump;
|
||||
// use dioxus_core::nodebuilder::*;
|
||||
// use dioxus_core::prelude::*;
|
||||
// use std::{collections::HashMap, future::Future, marker::PhantomData};
|
||||
|
||||
fn main() {}
|
||||
// fn main() {
|
||||
// let mut vdom = VirtualDom::new_with_props(
|
||||
// component,
|
||||
// Props {
|
||||
// blah: false,
|
||||
// text: "blah".into(),
|
||||
// },
|
||||
// );
|
||||
|
||||
// vdom.progress();
|
||||
|
||||
// let somet = String::from("asd");
|
||||
// let text = somet.as_str();
|
||||
|
||||
// /*
|
||||
// this could be auto-generated via the macro
|
||||
// this props is allocated in this
|
||||
// but the component and props would like need to be cached
|
||||
// we could box this fn, abstracting away the props requirement and just keep the entrance and allocator requirement
|
||||
// How do we keep cached things around?
|
||||
// Need some sort of caching mechanism
|
||||
|
||||
// how do we enter into a childscope from a parent scope?
|
||||
|
||||
// Problems:
|
||||
// 1: Comp props need to be stored somewhere so we can re-evalute components when they receive updates
|
||||
// 2: Trees are not evaluated
|
||||
|
||||
// */
|
||||
// let example_caller = move |cx: &Bump| {
|
||||
// todo!()
|
||||
// // let p = Props { blah: true, text };
|
||||
// // let c = Context { props: &p };
|
||||
// // let r = component(&c);
|
||||
// };
|
||||
|
||||
// // check the edit list
|
||||
// }
|
||||
|
||||
// // ~~~ Text shared between components via props can be done with lifetimes! ~~~
|
||||
// // Super duper efficient :)
|
||||
// struct Props {
|
||||
// blah: bool,
|
||||
// text: String,
|
||||
// // text: &'src str,
|
||||
// }
|
||||
// impl Properties for Props {
|
||||
// fn new() -> Self {
|
||||
// todo!()
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn component<'a>(cx: Context<'a, Props>) -> DomTree<'a> {
|
||||
// // Write asynchronous rendering code that immediately returns a "suspended" VNode
|
||||
// // The concurrent API will then progress this component when the future finishes
|
||||
// // You can suspend the entire component, or just parts of it
|
||||
// let product_list = cx.suspend(async {
|
||||
// // Suspend the rendering that completes when the future is done
|
||||
// match fetch_data().await {
|
||||
// Ok(data) => html! { <div> "success!" </div>},
|
||||
// Err(_) => html! { <div> "failure :(" </div>},
|
||||
// }
|
||||
// });
|
||||
|
||||
// // todo!()
|
||||
// cx.render(html! {
|
||||
// <div>
|
||||
// <h1> "Products" </h1>
|
||||
// <button> "hello!" </button>
|
||||
// // Subnodes can even be suspended
|
||||
// // When completely rendered, they won't cause the component itself to re-render, just their slot
|
||||
|
||||
// // <p> { product_list } </p>
|
||||
// </div>
|
||||
// })
|
||||
// }
|
||||
|
||||
// fn BuilderComp<'a>(cx: Context<'a, Props>) -> DomTree<'a> {
|
||||
// // VNodes can be constructed via a builder or the html! macro
|
||||
// // However, both of these are "lazy" - they need to be evaluated (aka, "viewed")
|
||||
// // We can "view" them with Context for ultimate speed while inside components
|
||||
// cx.render(|bump| {
|
||||
// div(bump)
|
||||
// .attr("class", "edit")
|
||||
// .child(text("Hello"))
|
||||
// .child(text(cx.cx.text.as_ref()))
|
||||
// .finish()
|
||||
// })
|
||||
// }
|
||||
|
||||
// #[fc]
|
||||
// fn EffcComp(cx: Context, name: &str) -> DomTree {
|
||||
// // VNodes can be constructed via a builder or the html! macro
|
||||
// // However, both of these are "lazy" - they need to be evaluated (aka, "viewed")
|
||||
// // We can "view" them with Context for ultimate speed while inside components
|
||||
// // use "phase" style allocation;
|
||||
|
||||
// cx.render(html! {
|
||||
// <div>
|
||||
// // your template goes here
|
||||
// // feel free to directly use "name"
|
||||
// </div>
|
||||
// })
|
||||
// }
|
||||
|
||||
// fn FullySuspended<'a>(cx: &'a Context<Props>) -> DomTree<'a> {
|
||||
// cx.suspend(async {
|
||||
// let i: i32 = 0;
|
||||
|
||||
// // full suspended works great with just returning VNodes!
|
||||
// // Feel free to capture the html! macro directly
|
||||
// // Anything returned here is automatically viewed
|
||||
// let tex = match i {
|
||||
// 1 => html! { <div> </div> },
|
||||
// 2 => html! { <div> </div> },
|
||||
// _ => html! { <div> </div> },
|
||||
// };
|
||||
|
||||
// if cx.cx.blah {
|
||||
// html! { <div> </div> }
|
||||
// } else {
|
||||
// tex
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// /// An example of a datafetching service
|
||||
// async fn fetch_data() -> Result<String, ()> {
|
||||
// todo!()
|
||||
// }
|
15
packages/core/examples/vdom_usage.rs
Normal file
15
packages/core/examples/vdom_usage.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use dioxus_core::prelude::*;
|
||||
|
||||
#[async_std::main]
|
||||
async fn main() {
|
||||
static App: FC<()> = |cx| cx.render(LazyNodes::new(|f| f.text(format_args!("hello"))));
|
||||
|
||||
let mut dom = VirtualDom::new(App);
|
||||
|
||||
dom.rebuild();
|
||||
|
||||
let deadline = async_std::task::sleep(Duration::from_millis(50));
|
||||
let _fut = dom.run_with_deadline(deadline);
|
||||
}
|
|
@ -1,190 +0,0 @@
|
|||
use std::cell::{RefCell, RefMut};
|
||||
use std::fmt::Display;
|
||||
use std::{cell::UnsafeCell, rc::Rc};
|
||||
|
||||
use crate::heuristics::*;
|
||||
use crate::innerlude::*;
|
||||
use futures_util::stream::FuturesUnordered;
|
||||
use fxhash::{FxHashMap, FxHashSet};
|
||||
use slab::Slab;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
// slotmap::new_key_type! {
|
||||
// // A dedicated key type for the all the scopes
|
||||
// pub struct ScopeId;
|
||||
// }
|
||||
// #[cfg(feature = "serialize", serde::Serialize)]
|
||||
// #[cfg(feature = "serialize", serde::Serialize)]
|
||||
#[derive(serde::Serialize, serde::Deserialize, Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||
pub struct ScopeId(pub usize);
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||
pub struct ElementId(pub usize);
|
||||
impl Display for ElementId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ElementId {
|
||||
pub fn as_u64(self) -> u64 {
|
||||
self.0 as u64
|
||||
}
|
||||
}
|
||||
|
||||
type Shared<T> = Rc<RefCell<T>>;
|
||||
type TaskReceiver = futures_channel::mpsc::UnboundedReceiver<EventTrigger>;
|
||||
type TaskSender = futures_channel::mpsc::UnboundedSender<EventTrigger>;
|
||||
|
||||
/// These are resources shared among all the components and the virtualdom itself
|
||||
#[derive(Clone)]
|
||||
pub struct SharedResources {
|
||||
pub components: Rc<UnsafeCell<Slab<Scope>>>,
|
||||
|
||||
pub(crate) heuristics: Shared<HeuristicsEngine>,
|
||||
|
||||
///
|
||||
pub task_sender: TaskSender,
|
||||
|
||||
pub task_receiver: Shared<TaskReceiver>,
|
||||
|
||||
pub async_tasks: Shared<FuturesUnordered<FiberTask>>,
|
||||
|
||||
/// We use a SlotSet to keep track of the keys that are currently being used.
|
||||
/// However, we don't store any specific data since the "mirror"
|
||||
pub raw_elements: Rc<RefCell<Slab<()>>>,
|
||||
|
||||
pub task_setter: Rc<dyn Fn(ScopeId)>,
|
||||
}
|
||||
|
||||
impl SharedResources {
|
||||
pub fn new() -> Self {
|
||||
// preallocate 2000 elements and 20 scopes to avoid dynamic allocation
|
||||
let components: Rc<UnsafeCell<Slab<Scope>>> =
|
||||
Rc::new(UnsafeCell::new(Slab::with_capacity(100)));
|
||||
|
||||
// elements are super cheap - the value takes no space
|
||||
let raw_elements = Slab::with_capacity(2000);
|
||||
|
||||
let (sender, receiver) = futures_channel::mpsc::unbounded();
|
||||
|
||||
let heuristics = HeuristicsEngine::new();
|
||||
|
||||
// we allocate this task setter once to save us from having to allocate later
|
||||
let task_setter = {
|
||||
let queue = sender.clone();
|
||||
let components = components.clone();
|
||||
Rc::new(move |idx: ScopeId| {
|
||||
let comps = unsafe { &*components.get() };
|
||||
|
||||
if let Some(scope) = comps.get(idx.0) {
|
||||
queue
|
||||
.unbounded_send(EventTrigger::new(
|
||||
VirtualEvent::ScheduledUpdate {
|
||||
height: scope.height,
|
||||
},
|
||||
idx,
|
||||
None,
|
||||
EventPriority::High,
|
||||
))
|
||||
.expect("The event queu receiver should *never* be dropped");
|
||||
}
|
||||
}) as Rc<dyn Fn(ScopeId)>
|
||||
};
|
||||
|
||||
Self {
|
||||
components,
|
||||
async_tasks: Rc::new(RefCell::new(FuturesUnordered::new())),
|
||||
task_receiver: Rc::new(RefCell::new(receiver)),
|
||||
task_sender: sender,
|
||||
heuristics: Rc::new(RefCell::new(heuristics)),
|
||||
raw_elements: Rc::new(RefCell::new(raw_elements)),
|
||||
task_setter,
|
||||
}
|
||||
}
|
||||
|
||||
/// this is unsafe because the caller needs to track which other scopes it's already using
|
||||
pub fn get_scope(&self, idx: ScopeId) -> Option<&Scope> {
|
||||
let inner = unsafe { &*self.components.get() };
|
||||
inner.get(idx.0)
|
||||
}
|
||||
|
||||
/// this is unsafe because the caller needs to track which other scopes it's already using
|
||||
pub fn get_scope_mut(&self, idx: ScopeId) -> Option<&mut Scope> {
|
||||
let inner = unsafe { &mut *self.components.get() };
|
||||
inner.get_mut(idx.0)
|
||||
}
|
||||
|
||||
pub fn with_scope<'b, O: 'static>(
|
||||
&'b self,
|
||||
_id: ScopeId,
|
||||
_f: impl FnOnce(&'b mut Scope) -> O,
|
||||
) -> Result<O> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
// return a bumpframe with a lifetime attached to the arena borrow
|
||||
// this is useful for merging lifetimes
|
||||
pub fn with_scope_vnode<'b>(
|
||||
&self,
|
||||
_id: ScopeId,
|
||||
_f: impl FnOnce(&mut Scope) -> &VNode<'b>,
|
||||
) -> Result<&VNode<'b>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn try_remove(&self, id: ScopeId) -> Result<Scope> {
|
||||
let inner = unsafe { &mut *self.components.get() };
|
||||
Ok(inner.remove(id.0))
|
||||
// .try_remove(id.0)
|
||||
// .ok_or_else(|| Error::FatalInternal("Scope not found"))
|
||||
}
|
||||
|
||||
pub fn reserve_node(&self) -> ElementId {
|
||||
ElementId(self.raw_elements.borrow_mut().insert(()))
|
||||
}
|
||||
|
||||
/// return the id, freeing the space of the original node
|
||||
pub fn collect_garbage(&self, id: ElementId) {
|
||||
self.raw_elements.borrow_mut().remove(id.0);
|
||||
}
|
||||
|
||||
pub fn insert_scope_with_key(&self, f: impl FnOnce(ScopeId) -> Scope) -> ScopeId {
|
||||
let g = unsafe { &mut *self.components.get() };
|
||||
let entry = g.vacant_entry();
|
||||
let id = ScopeId(entry.key());
|
||||
entry.insert(f(id));
|
||||
id
|
||||
}
|
||||
|
||||
pub fn schedule_update(&self) -> Rc<dyn Fn(ScopeId)> {
|
||||
self.task_setter.clone()
|
||||
}
|
||||
|
||||
pub fn submit_task(&self, task: FiberTask) -> TaskHandle {
|
||||
self.async_tasks.borrow_mut().push(task);
|
||||
TaskHandle {}
|
||||
}
|
||||
|
||||
pub fn make_trigger_key(&self, trigger: &EventTrigger) -> EventKey {
|
||||
let height = self
|
||||
.get_scope(trigger.originator)
|
||||
.map(|f| f.height)
|
||||
.unwrap();
|
||||
|
||||
EventKey {
|
||||
height,
|
||||
originator: trigger.originator,
|
||||
priority: trigger.priority,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TaskHandle {}
|
||||
|
||||
impl TaskHandle {
|
||||
pub fn toggle(&self) {}
|
||||
pub fn start(&self) {}
|
||||
pub fn stop(&self) {}
|
||||
pub fn restart(&self) {}
|
||||
}
|
|
@ -2,7 +2,7 @@ use crate::innerlude::*;
|
|||
use bumpalo::Bump;
|
||||
use std::cell::Cell;
|
||||
|
||||
pub struct ActiveFrame {
|
||||
pub(crate) struct ActiveFrame {
|
||||
// We use a "generation" for users of contents in the bump frames to ensure their data isn't broken
|
||||
pub generation: Cell<usize>,
|
||||
|
||||
|
@ -10,7 +10,7 @@ pub struct ActiveFrame {
|
|||
pub frames: [BumpFrame; 2],
|
||||
}
|
||||
|
||||
pub struct BumpFrame {
|
||||
pub(crate) struct BumpFrame {
|
||||
pub bump: Bump,
|
||||
pub(crate) head_node: VNode<'static>,
|
||||
|
||||
|
@ -40,10 +40,6 @@ impl ActiveFrame {
|
|||
self.wip_frame_mut().bump.reset()
|
||||
}
|
||||
|
||||
pub fn update_head_node<'a>(&mut self, node: VNode<'a>) {
|
||||
self.wip_frame_mut().head_node = unsafe { std::mem::transmute(node) };
|
||||
}
|
||||
|
||||
/// The "work in progress frame" represents the frame that is currently being worked on.
|
||||
pub fn wip_frame(&self) -> &BumpFrame {
|
||||
match self.generation.get() & 1 == 0 {
|
||||
|
@ -67,12 +63,6 @@ impl ActiveFrame {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn finished_frame_mut(&mut self) -> &mut BumpFrame {
|
||||
match self.generation.get() & 1 == 1 {
|
||||
true => &mut self.frames[0],
|
||||
false => &mut self.frames[1],
|
||||
}
|
||||
}
|
||||
/// Give out our self-referential item with our own borrowed lifetime
|
||||
pub fn fin_head<'b>(&'b self) -> &'b VNode<'b> {
|
||||
let cur_head = &self.finished_frame().head_node;
|
||||
|
|
94
packages/core/src/childiter.rs
Normal file
94
packages/core/src/childiter.rs
Normal file
|
@ -0,0 +1,94 @@
|
|||
use crate::innerlude::*;
|
||||
|
||||
/// This iterator iterates through a list of virtual children and only returns real children (Elements, Text, Anchors).
|
||||
///
|
||||
/// This iterator is useful when it's important to load the next real root onto the top of the stack for operations like
|
||||
/// "InsertBefore".
|
||||
pub(crate) struct RealChildIterator<'a> {
|
||||
scopes: &'a ResourcePool,
|
||||
|
||||
// Heuristcally we should never bleed into 4 completely nested fragments/components
|
||||
// Smallvec lets us stack allocate our little stack machine so the vast majority of cases are sane
|
||||
// TODO: use const generics instead of the 4 estimation
|
||||
stack: smallvec::SmallVec<[(u16, &'a VNode<'a>); 4]>,
|
||||
}
|
||||
|
||||
impl<'a> RealChildIterator<'a> {
|
||||
pub fn new(starter: &'a VNode<'a>, scopes: &'a ResourcePool) -> Self {
|
||||
Self {
|
||||
scopes,
|
||||
stack: smallvec::smallvec![(0, starter)],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: Option<&'a VNode<'a>> = 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
|
||||
// This includes fragments and components when they're empty (have a single root)
|
||||
VNode::Element(_) | VNode::Text(_) | VNode::Suspended(_) | VNode::Anchor(_) => {
|
||||
// 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 subcount = *count as usize;
|
||||
|
||||
if frag.children.len() == 0 {
|
||||
should_pop = true;
|
||||
returned_node = Some(&*node);
|
||||
}
|
||||
|
||||
if subcount >= frag.children.len() {
|
||||
should_pop = true;
|
||||
} else {
|
||||
should_push = Some(&frag.children[subcount]);
|
||||
}
|
||||
}
|
||||
|
||||
// For components, we load their root and push them onto the stack
|
||||
VNode::Component(sc) => {
|
||||
let scope = self
|
||||
.scopes
|
||||
.get_scope(sc.associated_scope.get().unwrap())
|
||||
.unwrap();
|
||||
// let scope = self.scopes.get(sc.ass_scope.get().unwrap()).unwrap();
|
||||
|
||||
// Simply swap the current node on the stack with the root of the component
|
||||
*node = scope.frames.fin_head();
|
||||
}
|
||||
}
|
||||
} 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
|
||||
}
|
||||
}
|
|
@ -87,6 +87,10 @@ impl<'src, P> Context<'src, P> {
|
|||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Notes:
|
||||
///
|
||||
/// This method returns a "ScopeChildren" object. This object is copy-able and preserve the correct lifetime.
|
||||
pub fn children(&self) -> ScopeChildren<'src> {
|
||||
self.scope.child_nodes()
|
||||
}
|
||||
|
@ -96,27 +100,11 @@ impl<'src, P> Context<'src, P> {
|
|||
/// ## Notice: you should prefer using prepare_update and get_scope_id
|
||||
///
|
||||
pub fn schedule_update(&self) -> Rc<dyn Fn() + 'static> {
|
||||
let cb = self.scope.vdom.schedule_update();
|
||||
let id = self.get_scope_id();
|
||||
Rc::new(move || cb(id))
|
||||
self.scope.memoized_updater.clone()
|
||||
}
|
||||
|
||||
pub fn prepare_update(&self) -> Rc<dyn Fn(ScopeId)> {
|
||||
self.scope.vdom.schedule_update()
|
||||
}
|
||||
|
||||
pub fn schedule_effect(&self) -> Rc<dyn Fn() + 'static> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn schedule_layout_effect(&self) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Get's this component's unique identifier.
|
||||
///
|
||||
pub fn get_scope_id(&self) -> ScopeId {
|
||||
self.scope.our_arena_idx.clone()
|
||||
self.scope.shared.schedule_any_immediate.clone()
|
||||
}
|
||||
|
||||
/// Take a lazy VNode structure and actually build it with the context of the VDom's efficient VNode allocator.
|
||||
|
@ -160,7 +148,7 @@ impl<'src, P> Context<'src, P> {
|
|||
///
|
||||
///
|
||||
pub fn submit_task(&self, task: FiberTask) -> TaskHandle {
|
||||
self.scope.vdom.submit_task(task)
|
||||
(self.scope.shared.submit_task)(task)
|
||||
}
|
||||
|
||||
/// Add a state globally accessible to child components via tree walking
|
||||
|
@ -174,46 +162,11 @@ impl<'src, P> Context<'src, P> {
|
|||
});
|
||||
}
|
||||
|
||||
/// Walk the tree to find a shared state with the TypeId of the generic type
|
||||
///
|
||||
pub fn consume_shared_state<T: 'static>(self) -> Option<Rc<T>> {
|
||||
let mut scope = Some(self.scope);
|
||||
let mut parent = None;
|
||||
|
||||
let getter = &self.scope.shared.get_shared_context;
|
||||
let ty = TypeId::of::<T>();
|
||||
while let Some(inner) = scope {
|
||||
log::debug!(
|
||||
"Searching {:#?} for valid shared_context",
|
||||
inner.our_arena_idx
|
||||
);
|
||||
let shared_ctx = {
|
||||
let shared_contexts = inner.shared_contexts.borrow();
|
||||
|
||||
log::debug!(
|
||||
"This component has {} shared contexts",
|
||||
shared_contexts.len()
|
||||
);
|
||||
shared_contexts.get(&ty).map(|f| f.clone())
|
||||
};
|
||||
|
||||
if let Some(shared_cx) = shared_ctx {
|
||||
log::debug!("found matching cx");
|
||||
let rc = shared_cx
|
||||
.clone()
|
||||
.downcast::<T>()
|
||||
.expect("Should not fail, already validated the type from the hashmap");
|
||||
parent = Some(rc);
|
||||
break;
|
||||
} else {
|
||||
match inner.parent_idx {
|
||||
Some(parent_id) => {
|
||||
scope = unsafe { inner.vdom.get_scope(parent_id) };
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
parent
|
||||
let idx = self.scope.our_arena_idx;
|
||||
getter(idx, ty).map(|f| f.downcast().expect("TypeID already validated"))
|
||||
}
|
||||
|
||||
/// Store a value between renders
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
//! Debug virtual doms!
|
||||
//! This renderer comes built in with dioxus core and shows how to implement a basic renderer.
|
||||
//!
|
||||
//! Renderers don't actually need to own the virtual dom (it's up to the implementer).
|
||||
|
||||
use crate::innerlude::RealDom;
|
||||
use crate::{events::EventTrigger, virtual_dom::VirtualDom};
|
||||
use crate::{innerlude::Result, prelude::*};
|
||||
|
||||
pub struct DebugRenderer {
|
||||
internal_dom: VirtualDom,
|
||||
}
|
||||
|
||||
impl DebugRenderer {
|
||||
/// Create a new instance of the Dioxus Virtual Dom with no properties for the root component.
|
||||
///
|
||||
/// This means that the root component must either consumes its own context, or statics are used to generate the page.
|
||||
/// The root component can access things like routing in its context.
|
||||
pub fn new(root: FC<()>) -> Self {
|
||||
Self::new_with_props(root, ())
|
||||
}
|
||||
|
||||
/// 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_with_props<T: Properties + 'static>(root: FC<T>, root_props: T) -> Self {
|
||||
Self::from_vdom(VirtualDom::new_with_props(root, root_props))
|
||||
}
|
||||
|
||||
/// Create a new text renderer from an existing Virtual DOM.
|
||||
pub fn from_vdom(dom: VirtualDom) -> Self {
|
||||
// todo: initialize the event registry properly
|
||||
Self { internal_dom: dom }
|
||||
}
|
||||
|
||||
pub fn handle_event(&mut self, trigger: EventTrigger) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// pub fn step<Dom: RealDom>(&mut self, machine: &mut DiffMachine<Dom>) -> Result<()> {
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
// this does a "holy" compare - if something is missing in the rhs, it doesn't complain.
|
||||
// it only complains if something shows up that's not in the lhs, *or* if a value is different.
|
||||
// This lets you exclude various fields if you just want to drill in to a specific prop
|
||||
// It leverages the internal diffing mechanism.
|
||||
// If you have a list or "nth" child, you do need to list those children, but you don't need to
|
||||
// fill in their children/attrs/etc
|
||||
// Does not handle children or lifecycles and will always fail the test if they show up in the rhs
|
||||
pub fn compare<F>(&self, other: LazyNodes<F>) -> Result<()>
|
||||
where
|
||||
F: for<'b, 'c> FnOnce(&'b NodeFactory<'c>) -> VNode<'c>,
|
||||
{
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Do a full compare - everything must match
|
||||
// Ignores listeners and children components
|
||||
pub fn compare_full<F>(&self, other: LazyNodes<F>) -> Result<()>
|
||||
where
|
||||
F: for<'b, 'c> FnOnce(&'b NodeFactory<'c>) -> VNode<'c>,
|
||||
{
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn trigger_listener(&mut self, id: usize) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn render_nodes<F>(&self, other: LazyNodes<F>) -> Result<()>
|
||||
where
|
||||
F: for<'b, 'c> FnOnce(&'b NodeFactory<'c>) -> VNode<'c>,
|
||||
{
|
||||
Ok(())
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
116
packages/core/src/diff_stack.rs
Normal file
116
packages/core/src/diff_stack.rs
Normal file
|
@ -0,0 +1,116 @@
|
|||
use crate::innerlude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
|
||||
/// The stack instructions we use to diff and create new nodes.
|
||||
#[derive(Debug)]
|
||||
pub enum DiffInstruction<'a> {
|
||||
DiffNode {
|
||||
old: &'a VNode<'a>,
|
||||
new: &'a VNode<'a>,
|
||||
},
|
||||
|
||||
DiffChildren {
|
||||
old: &'a [VNode<'a>],
|
||||
new: &'a [VNode<'a>],
|
||||
},
|
||||
|
||||
Create {
|
||||
node: &'a VNode<'a>,
|
||||
},
|
||||
|
||||
/// pushes the node elements onto the stack for use in mount
|
||||
PrepareMoveNode {
|
||||
node: &'a VNode<'a>,
|
||||
},
|
||||
|
||||
Mount {
|
||||
and: MountType<'a>,
|
||||
},
|
||||
|
||||
PopScope,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum MountType<'a> {
|
||||
Absorb,
|
||||
Append,
|
||||
Replace { old: &'a VNode<'a> },
|
||||
ReplaceByElementId { el: ElementId },
|
||||
InsertAfter { other_node: &'a VNode<'a> },
|
||||
InsertBefore { other_node: &'a VNode<'a> },
|
||||
}
|
||||
|
||||
pub(crate) struct DiffStack<'bump> {
|
||||
instructions: Vec<DiffInstruction<'bump>>,
|
||||
nodes_created_stack: SmallVec<[usize; 10]>,
|
||||
pub scope_stack: SmallVec<[ScopeId; 5]>,
|
||||
}
|
||||
|
||||
impl<'bump> DiffStack<'bump> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
instructions: Vec::with_capacity(1000),
|
||||
nodes_created_stack: smallvec![],
|
||||
scope_stack: smallvec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.instructions.is_empty()
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) -> Option<DiffInstruction<'bump>> {
|
||||
self.instructions.pop()
|
||||
}
|
||||
|
||||
pub fn pop_scope(&mut self) -> Option<ScopeId> {
|
||||
self.scope_stack.pop()
|
||||
}
|
||||
|
||||
pub fn push(&mut self, instruction: DiffInstruction<'bump>) {
|
||||
self.instructions.push(instruction)
|
||||
}
|
||||
|
||||
pub fn create_children(&mut self, children: &'bump [VNode<'bump>], and: MountType<'bump>) {
|
||||
self.nodes_created_stack.push(0);
|
||||
self.instructions.push(DiffInstruction::Mount { and });
|
||||
|
||||
for child in children.into_iter().rev() {
|
||||
self.instructions
|
||||
.push(DiffInstruction::Create { node: child });
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_nodes_created(&mut self, count: usize) {
|
||||
self.nodes_created_stack.push(count);
|
||||
}
|
||||
|
||||
pub fn create_node(&mut self, node: &'bump VNode<'bump>, and: MountType<'bump>) {
|
||||
self.nodes_created_stack.push(0);
|
||||
self.instructions.push(DiffInstruction::Mount { and });
|
||||
self.instructions.push(DiffInstruction::Create { node });
|
||||
}
|
||||
|
||||
pub fn add_child_count(&mut self, count: usize) {
|
||||
*self.nodes_created_stack.last_mut().unwrap() += count;
|
||||
}
|
||||
|
||||
pub fn pop_nodes_created(&mut self) -> usize {
|
||||
self.nodes_created_stack.pop().unwrap()
|
||||
}
|
||||
|
||||
pub fn current_scope(&self) -> Option<ScopeId> {
|
||||
self.scope_stack.last().map(|f| f.clone())
|
||||
}
|
||||
|
||||
pub fn create_component(&mut self, idx: ScopeId, node: &'bump VNode<'bump>) {
|
||||
// Push the new scope onto the stack
|
||||
self.scope_stack.push(idx);
|
||||
|
||||
self.instructions.push(DiffInstruction::PopScope);
|
||||
|
||||
// Run the creation algorithm with this scope on the stack
|
||||
// ?? I think we treat components as framgnets??
|
||||
self.instructions.push(DiffInstruction::Create { node });
|
||||
}
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
|
||||
use crate::innerlude::ScopeId;
|
||||
|
||||
/// A `DomEdit` represents a serialzied form of the VirtualDom's trait-based API. This allows streaming edits across the
|
||||
/// network or through FFI boundaries.
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
serde(tag = "type")
|
||||
)]
|
||||
pub enum DomEdit<'bump> {
|
||||
PushRoot {
|
||||
id: u64,
|
||||
},
|
||||
PopRoot,
|
||||
AppendChildren {
|
||||
many: u32,
|
||||
},
|
||||
ReplaceWith {
|
||||
// the first n elements
|
||||
n: u32,
|
||||
|
||||
// the last m elements
|
||||
m: u32,
|
||||
},
|
||||
InsertAfter {
|
||||
n: u32,
|
||||
},
|
||||
InsertBefore {
|
||||
n: u32,
|
||||
},
|
||||
Remove,
|
||||
RemoveAllChildren,
|
||||
CreateTextNode {
|
||||
text: &'bump str,
|
||||
id: u64,
|
||||
},
|
||||
CreateElement {
|
||||
tag: &'bump str,
|
||||
id: u64,
|
||||
},
|
||||
CreateElementNs {
|
||||
tag: &'bump str,
|
||||
id: u64,
|
||||
ns: &'static str,
|
||||
},
|
||||
CreatePlaceholder {
|
||||
id: u64,
|
||||
},
|
||||
NewEventListener {
|
||||
event_name: &'static str,
|
||||
scope: ScopeId,
|
||||
mounted_node_id: u64,
|
||||
},
|
||||
RemoveEventListener {
|
||||
event: &'static str,
|
||||
},
|
||||
SetText {
|
||||
text: &'bump str,
|
||||
},
|
||||
SetAttribute {
|
||||
field: &'static str,
|
||||
value: &'bump str,
|
||||
ns: Option<&'bump str>,
|
||||
},
|
||||
RemoveAttribute {
|
||||
name: &'static str,
|
||||
},
|
||||
}
|
||||
impl DomEdit<'_> {
|
||||
pub fn is(&self, id: &'static str) -> bool {
|
||||
match self {
|
||||
DomEdit::InsertAfter { .. } => id == "InsertAfter",
|
||||
DomEdit::InsertBefore { .. } => id == "InsertBefore",
|
||||
DomEdit::PushRoot { .. } => id == "PushRoot",
|
||||
DomEdit::PopRoot => id == "PopRoot",
|
||||
DomEdit::AppendChildren { .. } => id == "AppendChildren",
|
||||
DomEdit::ReplaceWith { .. } => id == "ReplaceWith",
|
||||
DomEdit::Remove => id == "Remove",
|
||||
DomEdit::RemoveAllChildren => id == "RemoveAllChildren",
|
||||
DomEdit::CreateTextNode { .. } => id == "CreateTextNode",
|
||||
DomEdit::CreateElement { .. } => id == "CreateElement",
|
||||
DomEdit::CreateElementNs { .. } => id == "CreateElementNs",
|
||||
DomEdit::CreatePlaceholder { .. } => id == "CreatePlaceholder",
|
||||
DomEdit::NewEventListener { .. } => id == "NewEventListener",
|
||||
DomEdit::RemoveEventListener { .. } => id == "RemoveEventListener",
|
||||
DomEdit::SetText { .. } => id == "SetText",
|
||||
DomEdit::SetAttribute { .. } => id == "SetAttribute",
|
||||
DomEdit::RemoveAttribute { .. } => id == "RemoveAttribute",
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
//! Internal error handling for Dioxus
|
||||
//!
|
||||
//!
|
||||
|
||||
use thiserror::Error as ThisError;
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
#[derive(ThisError, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Fatal Internal Error: {0}")]
|
||||
FatalInternal(&'static str),
|
||||
|
||||
#[error("Context is missing")]
|
||||
MissingSharedContext,
|
||||
|
||||
#[error("No event to progress")]
|
||||
NoEvent,
|
||||
|
||||
#[error("Wrong Properties Type")]
|
||||
WrongProps,
|
||||
|
||||
#[error("The component failed to return VNodes")]
|
||||
ComponentFailed,
|
||||
|
||||
#[error("Base scope has not been mounted yet")]
|
||||
NotMounted,
|
||||
|
||||
#[error("I/O Error: {0}")]
|
||||
IO(#[from] std::io::Error),
|
||||
}
|
|
@ -3,212 +3,71 @@
|
|||
//!
|
||||
//! 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 crate::innerlude::{ElementId, ScopeId};
|
||||
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
rc::Rc,
|
||||
};
|
||||
use bumpalo::boxed::Box as BumpBox;
|
||||
use std::{any::Any, cell::RefCell, fmt::Debug, ops::Deref, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
innerlude::{ElementId, ScopeId},
|
||||
VNode,
|
||||
innerlude::NodeFactory,
|
||||
innerlude::{Attribute, Listener, VNode},
|
||||
};
|
||||
use std::cell::Cell;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EventTrigger {
|
||||
pub struct UserEvent {
|
||||
/// The originator of the event trigger
|
||||
pub originator: ScopeId,
|
||||
pub scope: ScopeId,
|
||||
|
||||
/// The optional real node associated with the trigger
|
||||
pub real_node_id: Option<ElementId>,
|
||||
pub mounted_dom_id: Option<ElementId>,
|
||||
|
||||
/// The event type IE "onclick" or "onmouseover"
|
||||
///
|
||||
/// The name that the renderer will use to mount the listener.
|
||||
pub name: &'static str,
|
||||
|
||||
/// The type of event
|
||||
pub event: VirtualEvent,
|
||||
|
||||
/// The priority of the event
|
||||
pub priority: EventPriority,
|
||||
pub event: SyntheticEvent,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||
pub struct EventKey {
|
||||
/// The originator of the event trigger
|
||||
pub originator: ScopeId,
|
||||
/// The priority of the event
|
||||
pub priority: EventPriority,
|
||||
/// The height of the scope (used for ordering)
|
||||
pub height: u32,
|
||||
// TODO: add the time that the event was queued
|
||||
}
|
||||
|
||||
impl PartialOrd for EventKey {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
impl Ord for EventKey {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// Priority of Event Triggers.
|
||||
///
|
||||
/// Internally, Dioxus will abort work that's taking too long if new, more important, work arrives. Unlike React, Dioxus
|
||||
/// won't be afraid to pause work or flush changes to the RealDOM. This is called "cooperative scheduling". Some Renderers
|
||||
/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well.
|
||||
///
|
||||
/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)]
|
||||
pub enum EventPriority {
|
||||
/// Garbage collection is a type of work than can be scheduled around other work, but must be completed in a specific
|
||||
/// order. The GC must be run for a component before any other future work for that component is run. Otherwise,
|
||||
/// we will leak slots in our slab.
|
||||
///
|
||||
/// Garbage collection mixes with the safety aspects of the virtualdom so it's very important to get it done before
|
||||
/// other work.
|
||||
GarbageCollection,
|
||||
|
||||
/// "High Priority" work will not interrupt other high priority work, but will interrupt long medium and low priority work.
|
||||
///
|
||||
/// This is typically reserved for things like user interaction.
|
||||
High,
|
||||
|
||||
/// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important
|
||||
/// than "High Priority" events and will take presedence over low priority events.
|
||||
///
|
||||
/// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input.
|
||||
Medium,
|
||||
|
||||
/// "Low Priority" work will always be pre-empted unless the work is significantly delayed, in which case it will be
|
||||
/// advanced to the front of the work queue until completed.
|
||||
///
|
||||
/// The primary user of Low Priority work is the asynchronous work system (suspense).
|
||||
Low,
|
||||
}
|
||||
|
||||
impl EventTrigger {
|
||||
pub fn new(
|
||||
event: VirtualEvent,
|
||||
scope: ScopeId,
|
||||
mounted_dom_id: Option<ElementId>,
|
||||
priority: EventPriority,
|
||||
) -> Self {
|
||||
Self {
|
||||
priority,
|
||||
originator: scope,
|
||||
real_node_id: mounted_dom_id,
|
||||
event,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum VirtualEvent {
|
||||
/// Generated during diffing to signal that a component's nodes to be given back
|
||||
///
|
||||
/// Typically has a high priority
|
||||
///
|
||||
/// If an event is scheduled for a component that has "garbage", that garabge will be cleaned up before the event can
|
||||
/// be processed.
|
||||
GarbageCollection,
|
||||
|
||||
/// A type of "immediate" event scheduled by components
|
||||
///
|
||||
/// Usually called through "set_state"
|
||||
ScheduledUpdate {
|
||||
height: u32,
|
||||
},
|
||||
|
||||
// Whenever a task is ready (complete) Dioxus produces this "AsyncEvent"
|
||||
//
|
||||
// Async events don't necessarily propagate into a scope being ran. It's up to the event itself
|
||||
// to force an update for itself.
|
||||
//
|
||||
// Most async events should have a low priority.
|
||||
//
|
||||
// This type exists for the task/concurrency system to signal that a task is ready.
|
||||
// However, this does not necessarily signal that a scope must be re-ran, so the hook implementation must cause its
|
||||
// own re-run.
|
||||
AsyncEvent {
|
||||
should_rerender: bool,
|
||||
},
|
||||
|
||||
// Suspense events are a type of async event generated when suspended nodes are ready to be processed.
|
||||
//
|
||||
// they have the lowest priority
|
||||
SuspenseEvent {
|
||||
hook_idx: usize,
|
||||
domnode: Rc<Cell<Option<ElementId>>>,
|
||||
},
|
||||
|
||||
// image event has conflicting method types
|
||||
// ImageEvent(event_data::ImageEvent),
|
||||
|
||||
// Real events
|
||||
pub enum SyntheticEvent {
|
||||
AnimationEvent(on::AnimationEvent),
|
||||
ClipboardEvent(on::ClipboardEvent),
|
||||
CompositionEvent(on::CompositionEvent),
|
||||
KeyboardEvent(on::KeyboardEvent),
|
||||
FocusEvent(on::FocusEvent),
|
||||
FormEvent(on::FormEvent),
|
||||
SelectionEvent(on::SelectionEvent),
|
||||
KeyboardEvent(on::KeyboardEvent),
|
||||
GenericEvent(on::GenericEvent),
|
||||
TouchEvent(on::TouchEvent),
|
||||
UIEvent(on::UIEvent),
|
||||
WheelEvent(on::WheelEvent),
|
||||
MediaEvent(on::MediaEvent),
|
||||
AnimationEvent(on::AnimationEvent),
|
||||
TransitionEvent(on::TransitionEvent),
|
||||
ToggleEvent(on::ToggleEvent),
|
||||
MediaEvent(on::MediaEvent),
|
||||
MouseEvent(on::MouseEvent),
|
||||
WheelEvent(on::WheelEvent),
|
||||
SelectionEvent(on::SelectionEvent),
|
||||
TransitionEvent(on::TransitionEvent),
|
||||
PointerEvent(on::PointerEvent),
|
||||
}
|
||||
impl VirtualEvent {
|
||||
pub fn is_input_event(&self) -> bool {
|
||||
match self {
|
||||
VirtualEvent::ClipboardEvent(_)
|
||||
| VirtualEvent::CompositionEvent(_)
|
||||
| VirtualEvent::KeyboardEvent(_)
|
||||
| VirtualEvent::FocusEvent(_)
|
||||
| VirtualEvent::FormEvent(_)
|
||||
| VirtualEvent::SelectionEvent(_)
|
||||
| VirtualEvent::TouchEvent(_)
|
||||
| VirtualEvent::UIEvent(_)
|
||||
| VirtualEvent::WheelEvent(_)
|
||||
| VirtualEvent::MediaEvent(_)
|
||||
| VirtualEvent::AnimationEvent(_)
|
||||
| VirtualEvent::TransitionEvent(_)
|
||||
| VirtualEvent::ToggleEvent(_)
|
||||
| VirtualEvent::MouseEvent(_)
|
||||
| VirtualEvent::PointerEvent(_) => true,
|
||||
// ImageEvent(event_data::ImageEvent),
|
||||
|
||||
VirtualEvent::GarbageCollection
|
||||
| VirtualEvent::ScheduledUpdate { .. }
|
||||
| VirtualEvent::AsyncEvent { .. }
|
||||
| VirtualEvent::SuspenseEvent { .. } => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for VirtualEvent {
|
||||
impl std::fmt::Debug for SyntheticEvent {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let name = match self {
|
||||
VirtualEvent::ClipboardEvent(_) => "ClipboardEvent",
|
||||
VirtualEvent::CompositionEvent(_) => "CompositionEvent",
|
||||
VirtualEvent::KeyboardEvent(_) => "KeyboardEvent",
|
||||
VirtualEvent::FocusEvent(_) => "FocusEvent",
|
||||
VirtualEvent::FormEvent(_) => "FormEvent",
|
||||
VirtualEvent::SelectionEvent(_) => "SelectionEvent",
|
||||
VirtualEvent::TouchEvent(_) => "TouchEvent",
|
||||
VirtualEvent::UIEvent(_) => "UIEvent",
|
||||
VirtualEvent::WheelEvent(_) => "WheelEvent",
|
||||
VirtualEvent::MediaEvent(_) => "MediaEvent",
|
||||
VirtualEvent::AnimationEvent(_) => "AnimationEvent",
|
||||
VirtualEvent::TransitionEvent(_) => "TransitionEvent",
|
||||
VirtualEvent::ToggleEvent(_) => "ToggleEvent",
|
||||
VirtualEvent::MouseEvent(_) => "MouseEvent",
|
||||
VirtualEvent::PointerEvent(_) => "PointerEvent",
|
||||
VirtualEvent::GarbageCollection => "GarbageCollection",
|
||||
VirtualEvent::ScheduledUpdate { .. } => "SetStateEvent",
|
||||
VirtualEvent::AsyncEvent { .. } => "AsyncEvent",
|
||||
VirtualEvent::SuspenseEvent { .. } => "SuspenseEvent",
|
||||
SyntheticEvent::ClipboardEvent(_) => "ClipboardEvent",
|
||||
SyntheticEvent::CompositionEvent(_) => "CompositionEvent",
|
||||
SyntheticEvent::KeyboardEvent(_) => "KeyboardEvent",
|
||||
SyntheticEvent::FocusEvent(_) => "FocusEvent",
|
||||
SyntheticEvent::FormEvent(_) => "FormEvent",
|
||||
SyntheticEvent::SelectionEvent(_) => "SelectionEvent",
|
||||
SyntheticEvent::TouchEvent(_) => "TouchEvent",
|
||||
SyntheticEvent::WheelEvent(_) => "WheelEvent",
|
||||
SyntheticEvent::MediaEvent(_) => "MediaEvent",
|
||||
SyntheticEvent::AnimationEvent(_) => "AnimationEvent",
|
||||
SyntheticEvent::TransitionEvent(_) => "TransitionEvent",
|
||||
SyntheticEvent::ToggleEvent(_) => "ToggleEvent",
|
||||
SyntheticEvent::MouseEvent(_) => "MouseEvent",
|
||||
SyntheticEvent::PointerEvent(_) => "PointerEvent",
|
||||
SyntheticEvent::GenericEvent(_) => "GenericEvent",
|
||||
};
|
||||
|
||||
f.debug_struct("VirtualEvent").field("type", &name).finish()
|
||||
|
@ -222,20 +81,8 @@ pub mod on {
|
|||
//! Synthetic events are immutable and wrapped in Arc. It is the intention for Dioxus renderers to re-use the underyling
|
||||
//! Arc allocation through "get_mut"
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
|
||||
#![allow(unused)]
|
||||
use bumpalo::boxed::Box as BumpBox;
|
||||
use std::{cell::RefCell, fmt::Debug, ops::Deref, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
innerlude::NodeFactory,
|
||||
innerlude::{Attribute, ElementId, Listener, VNode},
|
||||
};
|
||||
use std::cell::Cell;
|
||||
|
||||
use super::VirtualEvent;
|
||||
//! React recently dropped support for re-using event allocation and just passes the real event along.
|
||||
use super::*;
|
||||
|
||||
macro_rules! event_directory {
|
||||
( $(
|
||||
|
@ -271,12 +118,12 @@ pub mod on {
|
|||
{
|
||||
let bump = &c.bump();
|
||||
|
||||
let cb: &mut dyn FnMut(VirtualEvent) = bump.alloc(move |evt: VirtualEvent| match evt {
|
||||
VirtualEvent::$wrapper(event) => callback(event),
|
||||
_ => unreachable!("Downcasted VirtualEvent to wrong event type - this is an internal bug!")
|
||||
let cb: &mut dyn FnMut(SyntheticEvent) = bump.alloc(move |evt: SyntheticEvent| match evt {
|
||||
SyntheticEvent::$wrapper(event) => callback(event),
|
||||
_ => unreachable!("Downcasted SyntheticEvent to wrong event type - this is an internal bug!")
|
||||
});
|
||||
|
||||
let callback: BumpBox<dyn FnMut(VirtualEvent) + 'a> = unsafe { BumpBox::from_raw(cb) };
|
||||
let callback: BumpBox<dyn FnMut(SyntheticEvent) + 'a> = unsafe { BumpBox::from_raw(cb) };
|
||||
|
||||
let event_name = stringify!($name);
|
||||
let shortname: &'static str = &event_name[2..];
|
||||
|
@ -292,14 +139,6 @@ pub mod on {
|
|||
}
|
||||
|
||||
// The Dioxus Synthetic event system
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
event_directory! {
|
||||
ClipboardEventInner(ClipboardEvent): [
|
||||
/// Called when "copy"
|
||||
|
@ -447,7 +286,13 @@ pub mod on {
|
|||
onmousemove
|
||||
/// onmouseout
|
||||
onmouseout
|
||||
|
||||
///
|
||||
onscroll
|
||||
|
||||
/// onmouseover
|
||||
///
|
||||
/// Triggered when the users's mouse hovers over an element.
|
||||
onmouseover
|
||||
/// onmouseup
|
||||
onmouseup
|
||||
|
@ -492,14 +337,9 @@ pub mod on {
|
|||
ontouchstart
|
||||
];
|
||||
|
||||
UIEventInner(UIEvent): [
|
||||
///
|
||||
scroll
|
||||
];
|
||||
|
||||
WheelEventInner(WheelEvent): [
|
||||
///
|
||||
wheel
|
||||
onwheel
|
||||
];
|
||||
|
||||
MediaEventInner(MediaEvent): [
|
||||
|
@ -571,7 +411,11 @@ pub mod on {
|
|||
];
|
||||
}
|
||||
|
||||
pub struct GenericEvent(pub Rc<dyn GenericEventInner>);
|
||||
|
||||
pub trait GenericEventInner {
|
||||
/// Return a reference to the raw event. User will need to downcast the event to the right platform-specific type.
|
||||
fn raw_event(&self) -> &dyn Any;
|
||||
/// Returns whether or not a specific event is a bubbling event
|
||||
fn bubbles(&self) -> bool;
|
||||
/// Sets or returns whether the event should propagate up the hierarchy or not
|
||||
|
@ -580,14 +424,18 @@ pub mod on {
|
|||
fn cancelable(&self) -> bool;
|
||||
/// Returns whether the event is composed or not
|
||||
fn composed(&self) -> bool;
|
||||
/// Returns the event's path
|
||||
fn composed_path(&self) -> String;
|
||||
|
||||
// Currently not supported because those no way we could possibly support it
|
||||
// just cast the event to the right platform-specific type and return it
|
||||
// /// Returns the event's path
|
||||
// fn composed_path(&self) -> String;
|
||||
|
||||
/// Returns the element whose event listeners triggered the event
|
||||
fn current_target(&self);
|
||||
/// Returns whether or not the preventDefault method was called for the event
|
||||
fn default_prevented(&self) -> bool;
|
||||
/// Returns which phase of the event flow is currently being evaluated
|
||||
fn event_phase(&self) -> usize;
|
||||
fn event_phase(&self) -> u16;
|
||||
/// Returns whether or not an event is trusted
|
||||
fn is_trusted(&self) -> bool;
|
||||
/// Cancels the event if it is cancelable, meaning that the default action that belongs to the event will
|
||||
|
@ -599,7 +447,7 @@ pub mod on {
|
|||
/// Returns the element that triggered the event
|
||||
fn target(&self);
|
||||
/// Returns the time (in milliseconds relative to the epoch) at which the event was created
|
||||
fn time_stamp(&self) -> usize;
|
||||
fn time_stamp(&self) -> f64;
|
||||
}
|
||||
|
||||
pub trait ClipboardEventInner {
|
||||
|
@ -611,8 +459,31 @@ pub mod on {
|
|||
}
|
||||
|
||||
pub trait KeyboardEventInner {
|
||||
fn alt_key(&self) -> bool;
|
||||
|
||||
fn char_code(&self) -> u32;
|
||||
|
||||
/// Identify which "key" was entered.
|
||||
///
|
||||
/// This is the best method to use for all languages. They key gets mapped to a String sequence which you can match on.
|
||||
/// The key isn't an enum because there are just so many context-dependent keys.
|
||||
///
|
||||
/// A full list on which keys to use is available at:
|
||||
/// <https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values>
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// match event.key().as_str() {
|
||||
/// "Esc" | "Escape" => {}
|
||||
/// "ArrowDown" => {}
|
||||
/// "ArrowLeft" => {}
|
||||
/// _ => {}
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
fn key(&self) -> String;
|
||||
|
||||
/// Get the key code as an enum Variant.
|
||||
///
|
||||
/// This is intended for things like arrow keys, escape keys, function keys, and other non-international keys.
|
||||
|
@ -636,35 +507,14 @@ pub mod on {
|
|||
/// Check if the ctrl key was pressed down
|
||||
fn ctrl_key(&self) -> bool;
|
||||
|
||||
/// Identify which "key" was entered.
|
||||
///
|
||||
/// This is the best method to use for all languages. They key gets mapped to a String sequence which you can match on.
|
||||
/// The key isn't an enum because there are just so many context-dependent keys.
|
||||
///
|
||||
/// A full list on which keys to use is available at:
|
||||
/// <https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values>
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// match event.key().as_str() {
|
||||
/// "Esc" | "Escape" => {}
|
||||
/// "ArrowDown" => {}
|
||||
/// "ArrowLeft" => {}
|
||||
/// _ => {}
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
fn key(&self) -> String;
|
||||
fn get_modifier_state(&self, key_code: &str) -> bool;
|
||||
|
||||
// fn key(&self) -> String;
|
||||
fn locale(&self) -> String;
|
||||
fn location(&self) -> usize;
|
||||
fn meta_key(&self) -> bool;
|
||||
fn repeat(&self) -> bool;
|
||||
fn shift_key(&self) -> bool;
|
||||
fn which(&self) -> usize;
|
||||
fn get_modifier_state(&self, key_code: usize) -> bool;
|
||||
}
|
||||
|
||||
pub trait FocusEventInner {
|
||||
|
@ -695,8 +545,8 @@ pub mod on {
|
|||
pub trait PointerEventInner {
|
||||
// Mouse only
|
||||
fn alt_key(&self) -> bool;
|
||||
fn button(&self) -> usize;
|
||||
fn buttons(&self) -> usize;
|
||||
fn button(&self) -> i16;
|
||||
fn buttons(&self) -> u16;
|
||||
fn client_x(&self) -> i32;
|
||||
fn client_y(&self) -> i32;
|
||||
fn ctrl_key(&self) -> bool;
|
||||
|
@ -706,12 +556,12 @@ pub mod on {
|
|||
fn screen_x(&self) -> i32;
|
||||
fn screen_y(&self) -> i32;
|
||||
fn shift_key(&self) -> bool;
|
||||
fn get_modifier_state(&self, key_code: usize) -> bool;
|
||||
fn pointer_id(&self) -> usize;
|
||||
fn width(&self) -> usize;
|
||||
fn height(&self) -> usize;
|
||||
fn pressure(&self) -> usize;
|
||||
fn tangential_pressure(&self) -> usize;
|
||||
fn get_modifier_state(&self, key_code: &str) -> bool;
|
||||
fn pointer_id(&self) -> i32;
|
||||
fn width(&self) -> i32;
|
||||
fn height(&self) -> i32;
|
||||
fn pressure(&self) -> f32;
|
||||
fn tangential_pressure(&self) -> f32;
|
||||
fn tilt_x(&self) -> i32;
|
||||
fn tilt_y(&self) -> i32;
|
||||
fn twist(&self) -> i32;
|
||||
|
@ -726,7 +576,7 @@ pub mod on {
|
|||
fn ctrl_key(&self) -> bool;
|
||||
fn meta_key(&self) -> bool;
|
||||
fn shift_key(&self) -> bool;
|
||||
fn get_modifier_state(&self, key_code: usize) -> bool;
|
||||
fn get_modifier_state(&self, key_code: &str) -> bool;
|
||||
// changedTouches: DOMTouchList,
|
||||
// targetTouches: DOMTouchList,
|
||||
// touches: DOMTouchList,
|
||||
|
@ -738,10 +588,10 @@ pub mod on {
|
|||
}
|
||||
|
||||
pub trait WheelEventInner {
|
||||
fn delta_mode(&self) -> i32;
|
||||
fn delta_x(&self) -> i32;
|
||||
fn delta_y(&self) -> i32;
|
||||
fn delta_z(&self) -> i32;
|
||||
fn delta_mode(&self) -> u32;
|
||||
fn delta_x(&self) -> f64;
|
||||
fn delta_y(&self) -> f64;
|
||||
fn delta_z(&self) -> f64;
|
||||
}
|
||||
|
||||
pub trait MediaEventInner {}
|
||||
|
@ -788,16 +638,16 @@ pub mod on {
|
|||
DownArrow = 40,
|
||||
Insert = 45,
|
||||
Delete = 46,
|
||||
_0 = 48,
|
||||
_1 = 49,
|
||||
_2 = 50,
|
||||
_3 = 51,
|
||||
_4 = 52,
|
||||
_5 = 53,
|
||||
_6 = 54,
|
||||
_7 = 55,
|
||||
_8 = 56,
|
||||
_9 = 57,
|
||||
Num0 = 48,
|
||||
Num1 = 49,
|
||||
Num2 = 50,
|
||||
Num3 = 51,
|
||||
Num4 = 52,
|
||||
Num5 = 53,
|
||||
Num6 = 54,
|
||||
Num7 = 55,
|
||||
Num8 = 56,
|
||||
Num9 = 57,
|
||||
A = 65,
|
||||
B = 66,
|
||||
C = 67,
|
||||
|
@ -893,16 +743,16 @@ pub mod on {
|
|||
40 => DownArrow,
|
||||
45 => Insert,
|
||||
46 => Delete,
|
||||
48 => _0,
|
||||
49 => _1,
|
||||
50 => _2,
|
||||
51 => _3,
|
||||
52 => _4,
|
||||
53 => _5,
|
||||
54 => _6,
|
||||
55 => _7,
|
||||
56 => _8,
|
||||
57 => _9,
|
||||
48 => Num0,
|
||||
49 => Num1,
|
||||
50 => Num2,
|
||||
51 => Num3,
|
||||
52 => Num4,
|
||||
53 => Num5,
|
||||
54 => Num6,
|
||||
55 => Num7,
|
||||
56 => Num8,
|
||||
57 => Num9,
|
||||
65 => A,
|
||||
66 => B,
|
||||
67 => C,
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
use std::{
|
||||
any::Any,
|
||||
cell::{Cell, UnsafeCell},
|
||||
cell::{Cell, RefCell, UnsafeCell},
|
||||
};
|
||||
|
||||
pub struct HookList {
|
||||
vals: appendlist::AppendList<InnerHook<Box<dyn Any>>>,
|
||||
pub(crate) struct HookList {
|
||||
vals: RefCell<Vec<InnerHook<Box<dyn Any>>>>,
|
||||
idx: Cell<usize>,
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@ impl Default for HookList {
|
|||
struct InnerHook<T> {
|
||||
cell: UnsafeCell<T>,
|
||||
}
|
||||
|
||||
impl<T> InnerHook<T> {
|
||||
fn new(new: T) -> Self {
|
||||
Self {
|
||||
|
@ -29,33 +30,16 @@ impl<T> InnerHook<T> {
|
|||
}
|
||||
|
||||
impl HookList {
|
||||
/// Unsafely get a mutable reference to any of the hooks
|
||||
///
|
||||
/// This is unsafe because an &mut T might be aliased if the hook data is already borrowed/in use in the component
|
||||
///
|
||||
/// This method should be reserved for internal methods that are guaranteed that this hook is not aliased anyhwere
|
||||
/// inside the component body, or outside into children components.
|
||||
///
|
||||
/// This method is currently used only by the suspense system whose hook implementation guarantees that all &T is dropped
|
||||
/// before the suspense handler is ran.
|
||||
pub(crate) unsafe fn get_mut<T: 'static>(&self, idx: usize) -> Option<&mut T> {
|
||||
self.vals.get(idx).and_then(|inn| {
|
||||
let raw_box = unsafe { &mut *inn.cell.get() };
|
||||
raw_box.downcast_mut::<T>()
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn next<T: 'static>(&self) -> Option<&mut T> {
|
||||
self.vals.get(self.idx.get()).and_then(|inn| {
|
||||
self.vals.borrow().get(self.idx.get()).and_then(|inn| {
|
||||
self.idx.set(self.idx.get() + 1);
|
||||
let raw_box = unsafe { &mut *inn.cell.get() };
|
||||
raw_box.downcast_mut::<T>()
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn push<T: 'static>(&self, new: T) {
|
||||
self.vals.push(InnerHook::new(Box::new(new)))
|
||||
self.vals.borrow_mut().push(InnerHook::new(Box::new(new)))
|
||||
}
|
||||
|
||||
/// This resets the internal iterator count
|
||||
|
@ -70,7 +54,7 @@ impl HookList {
|
|||
|
||||
#[inline]
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.vals.len()
|
||||
self.vals.borrow().len()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
|
@ -3,17 +3,17 @@
|
|||
//! This module contains all the low-level built-in hooks that require 1st party support to work.
|
||||
//!
|
||||
//! Hooks:
|
||||
//! - use_hook
|
||||
//! - use_state_provider
|
||||
//! - use_state_consumer
|
||||
//! - use_task
|
||||
//! - use_suspense
|
||||
//! - [`use_hook`]
|
||||
//! - [`use_state_provider`]
|
||||
//! - [`use_state_consumer`]
|
||||
//! - [`use_task`]
|
||||
//! - [`use_suspense`]
|
||||
|
||||
use crate::innerlude::*;
|
||||
use futures_util::FutureExt;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cell::{Cell, RefCell},
|
||||
cell::RefCell,
|
||||
future::Future,
|
||||
rc::Rc,
|
||||
};
|
||||
|
@ -53,15 +53,7 @@ where
|
|||
(true, true) => {}
|
||||
|
||||
// Needs to be initialized
|
||||
(false, false) => {
|
||||
log::debug!("Initializing context...");
|
||||
cx.add_shared_state(init());
|
||||
log::info!(
|
||||
"There are now {} shared contexts for scope {:?}",
|
||||
cx.scope.shared_contexts.borrow().len(),
|
||||
cx.scope.our_arena_idx,
|
||||
);
|
||||
}
|
||||
(false, false) => cx.add_shared_state(init()),
|
||||
|
||||
_ => debug_assert!(false, "Cannot initialize two contexts of the same type"),
|
||||
};
|
||||
|
@ -119,21 +111,12 @@ where
|
|||
let slot = task_dump.clone();
|
||||
|
||||
let updater = cx.prepare_update();
|
||||
let update_id = cx.get_scope_id();
|
||||
|
||||
let originator = cx.scope.our_arena_idx.clone();
|
||||
let originator = cx.scope.our_arena_idx;
|
||||
|
||||
let handle = cx.submit_task(Box::pin(task_fut.then(move |output| async move {
|
||||
*slot.as_ref().borrow_mut() = Some(output);
|
||||
updater(update_id);
|
||||
EventTrigger {
|
||||
event: VirtualEvent::AsyncEvent {
|
||||
should_rerender: false,
|
||||
},
|
||||
originator,
|
||||
priority: EventPriority::Low,
|
||||
real_node_id: None,
|
||||
}
|
||||
updater(originator);
|
||||
originator
|
||||
})));
|
||||
|
||||
TaskHook {
|
||||
|
@ -172,79 +155,89 @@ where
|
|||
Out: 'static,
|
||||
Cb: for<'a> Fn(SuspendedContext<'a>, &Out) -> DomTree<'a> + 'static,
|
||||
{
|
||||
/*
|
||||
General strategy:
|
||||
- Create a slot for the future to dump its output into
|
||||
- Create a new future feeding off the user's future that feeds the output into that slot
|
||||
- Submit that future as a task
|
||||
- Take the task handle id and attach that to our suspended node
|
||||
- when the hook runs, check if the value exists
|
||||
- if it does, then we can render the node directly
|
||||
- if it doesn't, then we render a suspended node along with with the callback and task id
|
||||
*/
|
||||
cx.use_hook(
|
||||
move |hook_idx| {
|
||||
let value = Rc::new(RefCell::new(None));
|
||||
|
||||
let dom_node_id = Rc::new(empty_cell());
|
||||
let domnode = dom_node_id.clone();
|
||||
|
||||
let slot = value.clone();
|
||||
|
||||
let callback: SuspendedCallback = Box::new(move |ctx: SuspendedContext| {
|
||||
let v: std::cell::Ref<Option<Box<dyn Any>>> = slot.as_ref().borrow();
|
||||
match v.as_ref() {
|
||||
Some(a) => {
|
||||
let v: &dyn Any = a.as_ref();
|
||||
let real_val = v.downcast_ref::<Out>().unwrap();
|
||||
user_callback(ctx, real_val)
|
||||
}
|
||||
None => {
|
||||
//
|
||||
Some(VNode {
|
||||
key: None,
|
||||
kind: VNodeKind::Suspended(VSuspended {
|
||||
node: domnode.clone(),
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let originator = cx.scope.our_arena_idx.clone();
|
||||
let task_fut = task_initializer();
|
||||
let domnode = dom_node_id.clone();
|
||||
|
||||
let slot = value.clone();
|
||||
cx.submit_task(Box::pin(task_fut.then(move |output| async move {
|
||||
// When the new value arrives, set the hooks internal slot
|
||||
// Dioxus will call the user's callback to generate new nodes outside of the diffing system
|
||||
*slot.borrow_mut() = Some(Box::new(output) as Box<dyn Any>);
|
||||
EventTrigger {
|
||||
event: VirtualEvent::SuspenseEvent { hook_idx, domnode },
|
||||
originator,
|
||||
priority: EventPriority::Low,
|
||||
real_node_id: None,
|
||||
}
|
||||
})));
|
||||
let handle = cx.submit_task(Box::pin(task_initializer().then(
|
||||
move |output| async move {
|
||||
*slot.borrow_mut() = Some(Box::new(output) as Box<dyn Any>);
|
||||
originator
|
||||
},
|
||||
)));
|
||||
|
||||
SuspenseHook {
|
||||
value,
|
||||
callback,
|
||||
dom_node_id,
|
||||
}
|
||||
SuspenseHook { handle, value }
|
||||
},
|
||||
move |hook| {
|
||||
let cx = Context {
|
||||
scope: &cx.scope,
|
||||
props: &(),
|
||||
};
|
||||
let csx = SuspendedContext { inner: cx };
|
||||
(&hook.callback)(csx)
|
||||
move |hook| match hook.value.borrow().as_ref() {
|
||||
Some(value) => {
|
||||
let out = value.downcast_ref::<Out>().unwrap();
|
||||
let sus = SuspendedContext {
|
||||
inner: Context {
|
||||
props: &(),
|
||||
scope: cx.scope,
|
||||
},
|
||||
};
|
||||
user_callback(sus, out)
|
||||
}
|
||||
None => {
|
||||
let value = hook.value.clone();
|
||||
|
||||
cx.render(LazyNodes::new(|f| {
|
||||
let bump = f.bump();
|
||||
|
||||
use bumpalo::boxed::Box as BumpBox;
|
||||
|
||||
let f: &mut dyn FnMut(SuspendedContext<'src>) -> DomTree<'src> =
|
||||
bump.alloc(move |sus| {
|
||||
let val = value.borrow();
|
||||
|
||||
let out = val
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.downcast_ref::<Out>()
|
||||
.unwrap();
|
||||
|
||||
user_callback(sus, out)
|
||||
});
|
||||
let callback = unsafe { BumpBox::from_raw(f) };
|
||||
|
||||
VNode::Suspended(bump.alloc(VSuspended {
|
||||
dom_id: empty_cell(),
|
||||
task_id: hook.handle.our_id,
|
||||
callback: RefCell::new(Some(callback)),
|
||||
}))
|
||||
}))
|
||||
}
|
||||
},
|
||||
|_| {},
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) struct SuspenseHook {
|
||||
pub handle: TaskHandle,
|
||||
pub value: Rc<RefCell<Option<Box<dyn Any>>>>,
|
||||
pub callback: SuspendedCallback,
|
||||
pub dom_node_id: Rc<Cell<Option<ElementId>>>,
|
||||
}
|
||||
|
||||
type SuspendedCallback = Box<dyn for<'a> Fn(SuspendedContext<'a>) -> DomTree<'a>>;
|
||||
|
||||
pub struct SuspendedContext<'a> {
|
||||
pub(crate) inner: Context<'a, ()>,
|
||||
}
|
||||
|
||||
impl<'src> SuspendedContext<'src> {
|
||||
pub fn render<F: FnOnce(NodeFactory<'src>) -> VNode<'src>>(
|
||||
self,
|
||||
|
@ -256,3 +249,19 @@ impl<'src> SuspendedContext<'src> {
|
|||
Some(lazy_nodes.into_vnode(NodeFactory { bump }))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct NodeRef<'src, T: 'static>(&'src RefCell<T>);
|
||||
|
||||
pub fn use_node_ref<T, P>(cx: Context<P>) -> NodeRef<T> {
|
||||
cx.use_hook(
|
||||
|f| {},
|
||||
|f| {
|
||||
//
|
||||
todo!()
|
||||
},
|
||||
|f| {
|
||||
//
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,46 +1,51 @@
|
|||
#![allow(non_snake_case)]
|
||||
#![doc = include_str!("../README.md")]
|
||||
//! Dioxus Core
|
||||
//! ----------
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
|
||||
pub use crate::innerlude::{
|
||||
format_args_f, html, rsx, Context, DioxusElement, DomEdit, DomTree, ElementId, EventPriority,
|
||||
EventTrigger, LazyNodes, NodeFactory, Properties, ScopeId, SuspendedContext, VNode, VNodeKind,
|
||||
VirtualDom, VirtualEvent, FC,
|
||||
};
|
||||
/*
|
||||
Navigating this crate:
|
||||
- virtual_dom: the primary entrypoint for the crate
|
||||
- scheduler: the core interior logic called by virtual_dom
|
||||
- nodes: the definition of VNodes, listeners, etc.
|
||||
- diff: the stackmachine-based diffing algorithm
|
||||
- hooks: foundational hooks that require crate-private APIs
|
||||
- mutations: DomEdits/NodeRefs and internal API to create them
|
||||
|
||||
pub mod prelude {
|
||||
pub use crate::component::{fc_to_builder, Fragment, Properties};
|
||||
pub use crate::context::Context;
|
||||
pub use crate::hooks::*;
|
||||
pub use crate::innerlude::{DioxusElement, DomTree, LazyNodes, NodeFactory, FC};
|
||||
pub use crate::nodes::VNode;
|
||||
pub use crate::VirtualDom;
|
||||
pub use dioxus_core_macro::{format_args_f, html, rsx, Props};
|
||||
}
|
||||
Some utilities
|
||||
*/
|
||||
pub mod bumpframe;
|
||||
pub mod childiter;
|
||||
pub mod component;
|
||||
pub mod context;
|
||||
pub mod diff;
|
||||
pub mod diff_stack;
|
||||
pub mod events;
|
||||
pub mod heuristics;
|
||||
pub mod hooklist;
|
||||
pub mod hooks;
|
||||
pub mod mutations;
|
||||
pub mod nodes;
|
||||
pub mod scheduler;
|
||||
pub mod scope;
|
||||
pub mod test_dom;
|
||||
pub mod util;
|
||||
pub mod virtual_dom;
|
||||
|
||||
// types used internally that are important
|
||||
pub(crate) mod innerlude {
|
||||
pub use crate::arena::*;
|
||||
pub use crate::bumpframe::*;
|
||||
pub(crate) use crate::bumpframe::*;
|
||||
pub(crate) use crate::childiter::*;
|
||||
pub use crate::component::*;
|
||||
pub use crate::context::*;
|
||||
pub use crate::diff::*;
|
||||
pub use crate::editor::*;
|
||||
pub use crate::error::*;
|
||||
pub(crate) use crate::diff::*;
|
||||
pub use crate::diff_stack::*;
|
||||
pub use crate::events::*;
|
||||
pub use crate::heuristics::*;
|
||||
pub use crate::hooklist::*;
|
||||
pub(crate) use crate::hooklist::*;
|
||||
pub use crate::hooks::*;
|
||||
pub use crate::mutations::*;
|
||||
pub use crate::nodes::*;
|
||||
pub use crate::scheduler::*;
|
||||
pub use crate::scope::*;
|
||||
pub use crate::test_dom::*;
|
||||
pub use crate::util::*;
|
||||
pub use crate::virtual_dom::*;
|
||||
|
||||
|
@ -50,24 +55,24 @@ pub(crate) mod innerlude {
|
|||
pub use dioxus_core_macro::{format_args_f, html, rsx};
|
||||
}
|
||||
|
||||
pub mod exports {
|
||||
// export important things here
|
||||
pub use bumpalo;
|
||||
pub use crate::innerlude::{
|
||||
format_args_f, html, rsx, Context, DiffInstruction, DioxusElement, DomEdit, DomTree, ElementId,
|
||||
EventPriority, LazyNodes, MountType, Mutations, NodeFactory, Properties, ScopeId,
|
||||
SuspendedContext, SyntheticEvent, TestDom, UserEvent, VNode, VirtualDom, FC,
|
||||
};
|
||||
|
||||
pub mod prelude {
|
||||
pub use crate::component::{fc_to_builder, Fragment, Properties};
|
||||
pub use crate::context::Context;
|
||||
pub use crate::hooks::*;
|
||||
pub use crate::innerlude::{DioxusElement, DomTree, LazyNodes, Mutations, NodeFactory, FC};
|
||||
pub use crate::nodes::VNode;
|
||||
pub use crate::VirtualDom;
|
||||
pub use dioxus_core_macro::{format_args_f, html, rsx, Props};
|
||||
}
|
||||
|
||||
pub mod arena;
|
||||
pub mod bumpframe;
|
||||
pub mod component;
|
||||
pub mod context;
|
||||
pub mod diff;
|
||||
pub mod editor;
|
||||
pub mod error;
|
||||
pub mod events;
|
||||
pub mod heuristics;
|
||||
pub mod hooklist;
|
||||
pub mod hooks;
|
||||
pub mod nodes;
|
||||
pub mod scope;
|
||||
pub mod signals;
|
||||
pub mod util;
|
||||
pub mod virtual_dom;
|
||||
pub mod exports {
|
||||
//! Important dependencies that are used by the rest of the library
|
||||
// the foundation of this library
|
||||
pub use bumpalo;
|
||||
}
|
||||
|
|
247
packages/core/src/mutations.rs
Normal file
247
packages/core/src/mutations.rs
Normal file
|
@ -0,0 +1,247 @@
|
|||
//! Instructions returned by the VirtualDOM on how to modify the Real DOM.
|
||||
//!
|
||||
//! This module contains an internal API to generate these instructions.
|
||||
|
||||
use crate::innerlude::*;
|
||||
use std::any::Any;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Mutations<'a> {
|
||||
pub edits: Vec<DomEdit<'a>>,
|
||||
pub noderefs: Vec<NodeRefMutation<'a>>,
|
||||
}
|
||||
|
||||
use DomEdit::*;
|
||||
|
||||
impl<'a> Mutations<'a> {
|
||||
pub(crate) fn new() -> Self {
|
||||
let edits = Vec::new();
|
||||
let noderefs = Vec::new();
|
||||
Self { edits, noderefs }
|
||||
}
|
||||
|
||||
// Navigation
|
||||
pub(crate) fn push_root(&mut self, root: ElementId) {
|
||||
let id = root.as_u64();
|
||||
self.edits.push(PushRoot { id });
|
||||
}
|
||||
|
||||
pub(crate) fn pop(&mut self) {
|
||||
self.edits.push(PopRoot {});
|
||||
}
|
||||
|
||||
pub(crate) fn replace_with(&mut self, root: ElementId, m: u32) {
|
||||
let root = root.as_u64();
|
||||
self.edits.push(ReplaceWith { m, root });
|
||||
}
|
||||
|
||||
pub(crate) fn insert_after(&mut self, root: ElementId, n: u32) {
|
||||
let root = root.as_u64();
|
||||
self.edits.push(InsertAfter { n, root });
|
||||
}
|
||||
|
||||
pub(crate) fn insert_before(&mut self, root: ElementId, n: u32) {
|
||||
let root = root.as_u64();
|
||||
self.edits.push(InsertBefore { n, root });
|
||||
}
|
||||
|
||||
// Remove Nodesfrom the dom
|
||||
pub(crate) fn remove(&mut self, id: u64) {
|
||||
self.edits.push(Remove { root: id });
|
||||
}
|
||||
|
||||
// Create
|
||||
pub(crate) fn create_text_node(&mut self, text: &'a str, id: ElementId) {
|
||||
let id = id.as_u64();
|
||||
self.edits.push(CreateTextNode { text, id });
|
||||
}
|
||||
|
||||
pub(crate) fn create_element(
|
||||
&mut self,
|
||||
tag: &'static str,
|
||||
ns: Option<&'static str>,
|
||||
id: ElementId,
|
||||
) {
|
||||
let id = id.as_u64();
|
||||
match ns {
|
||||
Some(ns) => self.edits.push(CreateElementNs { id, ns, tag }),
|
||||
None => self.edits.push(CreateElement { id, tag }),
|
||||
}
|
||||
}
|
||||
// placeholders are nodes that don't get rendered but still exist as an "anchor" in the real dom
|
||||
pub(crate) fn create_placeholder(&mut self, id: ElementId) {
|
||||
let id = id.as_u64();
|
||||
self.edits.push(CreatePlaceholder { id });
|
||||
}
|
||||
|
||||
// events
|
||||
pub(crate) fn new_event_listener(&mut self, listener: &Listener, scope: ScopeId) {
|
||||
let Listener {
|
||||
event,
|
||||
mounted_node,
|
||||
..
|
||||
} = listener;
|
||||
|
||||
let element_id = mounted_node.get().unwrap().as_u64();
|
||||
|
||||
self.edits.push(NewEventListener {
|
||||
scope,
|
||||
event_name: event,
|
||||
mounted_node_id: element_id,
|
||||
});
|
||||
}
|
||||
pub(crate) fn remove_event_listener(&mut self, event: &'static str) {
|
||||
self.edits.push(RemoveEventListener { event });
|
||||
}
|
||||
|
||||
// modify
|
||||
pub(crate) fn set_text(&mut self, text: &'a str) {
|
||||
self.edits.push(SetText { text });
|
||||
}
|
||||
|
||||
pub(crate) fn set_attribute(&mut self, attribute: &'a Attribute) {
|
||||
let Attribute {
|
||||
name,
|
||||
value,
|
||||
namespace,
|
||||
..
|
||||
} = attribute;
|
||||
|
||||
self.edits.push(SetAttribute {
|
||||
field: name,
|
||||
value,
|
||||
ns: *namespace,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn remove_attribute(&mut self, attribute: &Attribute) {
|
||||
let name = attribute.name;
|
||||
self.edits.push(RemoveAttribute { name });
|
||||
}
|
||||
}
|
||||
|
||||
// refs are only assigned once
|
||||
pub struct NodeRefMutation<'a> {
|
||||
pub element: &'a mut Option<once_cell::sync::OnceCell<Box<dyn Any>>>,
|
||||
pub element_id: ElementId,
|
||||
}
|
||||
|
||||
impl<'a> std::fmt::Debug for NodeRefMutation<'a> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("NodeRefMutation")
|
||||
.field("element_id", &self.element_id)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> NodeRefMutation<'a> {
|
||||
pub fn downcast_ref<T: 'static>(&self) -> Option<&T> {
|
||||
self.element
|
||||
.as_ref()
|
||||
.and_then(|f| f.get())
|
||||
.and_then(|f| f.downcast_ref::<T>())
|
||||
}
|
||||
pub fn downcast_mut<T: 'static>(&mut self) -> Option<&mut T> {
|
||||
self.element
|
||||
.as_mut()
|
||||
.and_then(|f| f.get_mut())
|
||||
.and_then(|f| f.downcast_mut::<T>())
|
||||
}
|
||||
}
|
||||
|
||||
/// A `DomEdit` represents a serialzied form of the VirtualDom's trait-based API. This allows streaming edits across the
|
||||
/// network or through FFI boundaries.
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
serde(tag = "type")
|
||||
)]
|
||||
pub enum DomEdit<'bump> {
|
||||
PushRoot {
|
||||
id: u64,
|
||||
},
|
||||
PopRoot,
|
||||
|
||||
AppendChildren {
|
||||
many: u32,
|
||||
},
|
||||
|
||||
// "Root" refers to the item direclty
|
||||
// it's a waste of an instruction to push the root directly
|
||||
ReplaceWith {
|
||||
root: u64,
|
||||
m: u32,
|
||||
},
|
||||
InsertAfter {
|
||||
root: u64,
|
||||
n: u32,
|
||||
},
|
||||
InsertBefore {
|
||||
root: u64,
|
||||
n: u32,
|
||||
},
|
||||
Remove {
|
||||
root: u64,
|
||||
},
|
||||
|
||||
RemoveAllChildren,
|
||||
CreateTextNode {
|
||||
text: &'bump str,
|
||||
id: u64,
|
||||
},
|
||||
CreateElement {
|
||||
tag: &'bump str,
|
||||
id: u64,
|
||||
},
|
||||
CreateElementNs {
|
||||
tag: &'bump str,
|
||||
id: u64,
|
||||
ns: &'static str,
|
||||
},
|
||||
CreatePlaceholder {
|
||||
id: u64,
|
||||
},
|
||||
NewEventListener {
|
||||
event_name: &'static str,
|
||||
scope: ScopeId,
|
||||
mounted_node_id: u64,
|
||||
},
|
||||
RemoveEventListener {
|
||||
event: &'static str,
|
||||
},
|
||||
SetText {
|
||||
text: &'bump str,
|
||||
},
|
||||
SetAttribute {
|
||||
field: &'static str,
|
||||
value: &'bump str,
|
||||
ns: Option<&'bump str>,
|
||||
},
|
||||
RemoveAttribute {
|
||||
name: &'static str,
|
||||
},
|
||||
}
|
||||
impl DomEdit<'_> {
|
||||
pub fn is(&self, id: &'static str) -> bool {
|
||||
match self {
|
||||
DomEdit::InsertAfter { .. } => id == "InsertAfter",
|
||||
DomEdit::InsertBefore { .. } => id == "InsertBefore",
|
||||
DomEdit::PushRoot { .. } => id == "PushRoot",
|
||||
DomEdit::PopRoot => id == "PopRoot",
|
||||
DomEdit::AppendChildren { .. } => id == "AppendChildren",
|
||||
DomEdit::ReplaceWith { .. } => id == "ReplaceWith",
|
||||
DomEdit::Remove { .. } => id == "Remove",
|
||||
DomEdit::RemoveAllChildren => id == "RemoveAllChildren",
|
||||
DomEdit::CreateTextNode { .. } => id == "CreateTextNode",
|
||||
DomEdit::CreateElement { .. } => id == "CreateElement",
|
||||
DomEdit::CreateElementNs { .. } => id == "CreateElementNs",
|
||||
DomEdit::CreatePlaceholder { .. } => id == "CreatePlaceholder",
|
||||
DomEdit::NewEventListener { .. } => id == "NewEventListener",
|
||||
DomEdit::RemoveEventListener { .. } => id == "RemoveEventListener",
|
||||
DomEdit::SetText { .. } => id == "SetText",
|
||||
DomEdit::SetAttribute { .. } => id == "SetAttribute",
|
||||
DomEdit::RemoveAttribute { .. } => id == "RemoveAttribute",
|
||||
}
|
||||
}
|
||||
}
|
119
packages/core/src/noderef.rs
Normal file
119
packages/core/src/noderef.rs
Normal file
|
@ -0,0 +1,119 @@
|
|||
// let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap();
|
||||
|
||||
// let mut garbage_list = scope.consume_garbage();
|
||||
|
||||
// let mut scopes_to_kill = Vec::new();
|
||||
// while let Some(node) = garbage_list.pop() {
|
||||
// match &node.kind {
|
||||
// VNodeKind::Text(_) => {
|
||||
// self.shared.collect_garbage(node.direct_id());
|
||||
// }
|
||||
// VNodeKind::Anchor(_) => {
|
||||
// self.shared.collect_garbage(node.direct_id());
|
||||
// }
|
||||
// VNodeKind::Suspended(_) => {
|
||||
// self.shared.collect_garbage(node.direct_id());
|
||||
// }
|
||||
|
||||
// VNodeKind::Element(el) => {
|
||||
// self.shared.collect_garbage(node.direct_id());
|
||||
// for child in el.children {
|
||||
// garbage_list.push(child);
|
||||
// }
|
||||
// }
|
||||
|
||||
// VNodeKind::Fragment(frag) => {
|
||||
// for child in frag.children {
|
||||
// garbage_list.push(child);
|
||||
// }
|
||||
// }
|
||||
|
||||
// VNodeKind::Component(comp) => {
|
||||
// // TODO: run the hook destructors and then even delete the scope
|
||||
|
||||
// let scope_id = comp.ass_scope.get().unwrap();
|
||||
// let scope = self.get_scope(scope_id).unwrap();
|
||||
// let root = scope.root();
|
||||
// garbage_list.push(root);
|
||||
// scopes_to_kill.push(scope_id);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// for scope in scopes_to_kill {
|
||||
// // oy kill em
|
||||
// log::debug!("should be removing scope {:#?}", scope);
|
||||
// }
|
||||
|
||||
// // On the primary event queue, there is no batching, we take them off one-by-one
|
||||
// let trigger = match receiver.try_next() {
|
||||
// Ok(Some(trigger)) => trigger,
|
||||
// _ => {
|
||||
// // Continuously poll the future pool and the event receiver for work
|
||||
// let mut tasks = self.shared.async_tasks.borrow_mut();
|
||||
// let tasks_tasks = tasks.next();
|
||||
|
||||
// // if the new event generates work more important than our current fiber, we should consider switching
|
||||
// // only switch if it impacts different scopes.
|
||||
// let mut ui_receiver = self.shared.ui_event_receiver.borrow_mut();
|
||||
// let ui_reciv_task = ui_receiver.next();
|
||||
|
||||
// // right now, this polling method will only catch batched set_states that don't get awaited.
|
||||
// // However, in the future, we might be interested in batching set_states across await points
|
||||
// let immediate_tasks = ();
|
||||
|
||||
// futures_util::pin_mut!(tasks_tasks);
|
||||
// futures_util::pin_mut!(ui_reciv_task);
|
||||
|
||||
// // Poll the event receiver and the future pool for work
|
||||
// // Abort early if our deadline has ran out
|
||||
// let mut deadline = (&mut deadline_future).fuse();
|
||||
|
||||
// let trig = futures_util::select! {
|
||||
// trigger = tasks_tasks => trigger,
|
||||
// trigger = ui_reciv_task => trigger,
|
||||
|
||||
// // abort if we're out of time
|
||||
// _ = deadline => { return Ok(diff_machine.mutations); }
|
||||
// };
|
||||
|
||||
// trig.unwrap()
|
||||
// }
|
||||
// };
|
||||
|
||||
// async fn select_next_event(&mut self) -> Option<EventTrigger> {
|
||||
// let mut receiver = self.shared.task_receiver.borrow_mut();
|
||||
|
||||
// // drain the in-flight events so that we can sort them out with the current events
|
||||
// while let Ok(Some(trigger)) = receiver.try_next() {
|
||||
// log::info!("retrieving event from receiver");
|
||||
// let key = self.shared.make_trigger_key(&trigger);
|
||||
// self.pending_events.insert(key, trigger);
|
||||
// }
|
||||
|
||||
// if self.pending_events.is_empty() {
|
||||
// // Continuously poll the future pool and the event receiver for work
|
||||
// let mut tasks = self.shared.async_tasks.borrow_mut();
|
||||
// let tasks_tasks = tasks.next();
|
||||
|
||||
// let mut receiver = self.shared.task_receiver.borrow_mut();
|
||||
// let reciv_task = receiver.next();
|
||||
|
||||
// futures_util::pin_mut!(tasks_tasks);
|
||||
// futures_util::pin_mut!(reciv_task);
|
||||
|
||||
// let trigger = match futures_util::future::select(tasks_tasks, reciv_task).await {
|
||||
// futures_util::future::Either::Left((trigger, _)) => trigger,
|
||||
// futures_util::future::Either::Right((trigger, _)) => trigger,
|
||||
// }
|
||||
// .unwrap();
|
||||
// let key = self.shared.make_trigger_key(&trigger);
|
||||
// self.pending_events.insert(key, trigger);
|
||||
// }
|
||||
|
||||
// // pop the most important event off
|
||||
// let key = self.pending_events.keys().next().unwrap().clone();
|
||||
// let trigger = self.pending_events.remove(&key).unwrap();
|
||||
|
||||
// Some(trigger)
|
||||
// }
|
|
@ -1,80 +1,216 @@
|
|||
//! Virtual Node Support
|
||||
//! --------------------
|
||||
//! VNodes represent lazily-constructed VDom trees that support diffing and event handlers.
|
||||
//!
|
||||
//! These VNodes should be *very* cheap and *very* fast to construct - building a full tree should be insanely quick.
|
||||
use crate::{
|
||||
events::VirtualEvent,
|
||||
innerlude::{empty_cell, Context, DomTree, ElementId, Properties, Scope, ScopeId, FC},
|
||||
//! VNodes represent lazily-constructed VDom trees that support diffing and event handlers. These VNodes should be *very*
|
||||
//! cheap and *very* fast to construct - building a full tree should be quick.
|
||||
|
||||
use crate::innerlude::{
|
||||
empty_cell, Context, DomTree, ElementId, Properties, Scope, ScopeId, SuspendedContext,
|
||||
SyntheticEvent, FC,
|
||||
};
|
||||
use bumpalo::{boxed::Box as BumpBox, Bump};
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
fmt::{Arguments, Debug, Formatter},
|
||||
marker::PhantomData,
|
||||
mem::ManuallyDrop,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
pub struct VNode<'src> {
|
||||
pub kind: VNodeKind<'src>,
|
||||
/// A composable "VirtualNode" to declare a User Interface in the Dioxus VirtualDOM.
|
||||
///
|
||||
/// VNodes are designed to be lightweight and used with with a bump alloactor. To create a VNode, you can use either of:
|
||||
/// - the [`rsx`] macro
|
||||
/// - the [`html`] macro
|
||||
/// - the [`NodeFactory`] API
|
||||
pub enum VNode<'src> {
|
||||
/// Text VNodes simply bump-allocated (or static) string slices
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// let node = cx.render(rsx!{ "hello" }).unwrap();
|
||||
///
|
||||
/// if let VNode::Text(vtext) = node {
|
||||
/// assert_eq!(vtext.text, "hello");
|
||||
/// assert_eq!(vtext.dom_id.get(), None);
|
||||
/// assert_eq!(vtext.is_static, true);
|
||||
/// }
|
||||
/// ```
|
||||
Text(VText<'src>),
|
||||
|
||||
pub(crate) key: Option<&'src str>,
|
||||
/// Element VNodes are VNodes that may contain attributes, listeners, a key, a tag, and children.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// let node = cx.render(rsx!{
|
||||
/// div {
|
||||
/// key: "a",
|
||||
/// onclick: |e| log::info!("clicked"),
|
||||
/// hidden: "true",
|
||||
/// style: { background_color: "red" }
|
||||
/// "hello"
|
||||
/// }
|
||||
/// }).unwrap();
|
||||
/// if let VNode::Element(velement) = node {
|
||||
/// assert_eq!(velement.tag_name, "div");
|
||||
/// assert_eq!(velement.namespace, None);
|
||||
/// assert_eq!(velement.key, Some("a));
|
||||
/// }
|
||||
/// ```
|
||||
Element(&'src VElement<'src>),
|
||||
|
||||
/// Fragment nodes may contain many VNodes without a single root.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// rsx!{
|
||||
/// a {}
|
||||
/// link {}
|
||||
/// style {}
|
||||
/// "asd"
|
||||
/// Example {}
|
||||
/// }
|
||||
/// ```
|
||||
Fragment(VFragment<'src>),
|
||||
|
||||
/// Component nodes represent a mounted component with props, children, and a key.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// fn Example(cx: Context<()>) -> DomTree {
|
||||
/// todo!()
|
||||
/// }
|
||||
///
|
||||
/// let node = cx.render(rsx!{
|
||||
/// Example {}
|
||||
/// }).unwrap();
|
||||
///
|
||||
/// if let VNode::Component(vcomp) = node {
|
||||
/// assert_eq!(vcomp.user_fc, Example as *const ());
|
||||
/// }
|
||||
/// ```
|
||||
Component(&'src VComponent<'src>),
|
||||
|
||||
/// Suspended VNodes represent chunks of the UI tree that are not yet ready to be displayed.
|
||||
///
|
||||
/// These nodes currently can only be constructed via the [`use_suspense`] hook.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// rsx!{
|
||||
/// }
|
||||
/// ```
|
||||
Suspended(&'src VSuspended<'src>),
|
||||
|
||||
/// Anchors are a type of placeholder VNode used when fragments don't contain any children.
|
||||
///
|
||||
/// Anchors cannot be directly constructed via public APIs.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// let node = cx.render(rsx! ( Fragment {} )).unwrap();
|
||||
/// if let VNode::Fragment(frag) = node {
|
||||
/// let root = &frag.children[0];
|
||||
/// assert_eq!(root, VNode::Anchor);
|
||||
/// }
|
||||
/// ```
|
||||
Anchor(VAnchor),
|
||||
}
|
||||
|
||||
impl<'src> VNode<'src> {
|
||||
/// Get the VNode's "key" used in the keyed diffing algorithm.
|
||||
pub fn key(&self) -> Option<&'src str> {
|
||||
self.key
|
||||
match &self {
|
||||
VNode::Element(el) => el.key,
|
||||
VNode::Component(c) => c.key,
|
||||
VNode::Fragment(f) => f.key,
|
||||
VNode::Text(_t) => None,
|
||||
VNode::Suspended(_s) => None,
|
||||
VNode::Anchor(_f) => None,
|
||||
}
|
||||
}
|
||||
pub fn direct_id(&self) -> ElementId {
|
||||
self.try_direct_id().unwrap()
|
||||
|
||||
/// Get the ElementID of the mounted VNode.
|
||||
///
|
||||
/// Panics if the mounted ID is None or if the VNode is not represented by a single Element.
|
||||
pub fn mounted_id(&self) -> ElementId {
|
||||
self.try_mounted_id().unwrap()
|
||||
}
|
||||
pub fn try_direct_id(&self) -> Option<ElementId> {
|
||||
match &self.kind {
|
||||
VNodeKind::Text(el) => el.dom_id.get(),
|
||||
VNodeKind::Element(el) => el.dom_id.get(),
|
||||
VNodeKind::Anchor(el) => el.dom_id.get(),
|
||||
VNodeKind::Fragment(_) => None,
|
||||
VNodeKind::Component(_) => None,
|
||||
VNodeKind::Suspended(_) => None,
|
||||
|
||||
/// Try to get the ElementID of the mounted VNode.
|
||||
///
|
||||
/// Returns None if the VNode is not mounted, or if the VNode cannot be presented by a mounted ID (Fragment/Component)
|
||||
pub fn try_mounted_id(&self) -> Option<ElementId> {
|
||||
match &self {
|
||||
VNode::Text(el) => el.dom_id.get(),
|
||||
VNode::Element(el) => el.dom_id.get(),
|
||||
VNode::Anchor(el) => el.dom_id.get(),
|
||||
VNode::Suspended(el) => el.dom_id.get(),
|
||||
VNode::Fragment(_) => None,
|
||||
VNode::Component(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tools for the base unit of the virtual dom - the VNode
|
||||
/// VNodes are intended to be quickly-allocated, lightweight enum values.
|
||||
///
|
||||
/// Components will be generating a lot of these very quickly, so we want to
|
||||
/// limit the amount of heap allocations / overly large enum sizes.
|
||||
pub enum VNodeKind<'src> {
|
||||
Text(VText<'src>),
|
||||
|
||||
Element(&'src VElement<'src>),
|
||||
|
||||
Fragment(VFragment<'src>),
|
||||
|
||||
Component(&'src VComponent<'src>),
|
||||
|
||||
Suspended(VSuspended),
|
||||
|
||||
Anchor(VAnchor),
|
||||
}
|
||||
|
||||
/// A placeholder node only generated when Fragments don't have any children.
|
||||
pub struct VAnchor {
|
||||
pub dom_id: Cell<Option<ElementId>>,
|
||||
}
|
||||
|
||||
/// A bump-alloacted string slice and metadata.
|
||||
pub struct VText<'src> {
|
||||
pub text: &'src str,
|
||||
|
||||
pub dom_id: Cell<Option<ElementId>>,
|
||||
|
||||
pub is_static: bool,
|
||||
}
|
||||
|
||||
/// A list of VNodes with no single root.
|
||||
pub struct VFragment<'src> {
|
||||
pub key: Option<&'src str>,
|
||||
|
||||
pub children: &'src [VNode<'src>],
|
||||
|
||||
pub is_static: bool,
|
||||
}
|
||||
|
||||
/// An element like a "div" with children, listeners, and attributes.
|
||||
pub struct VElement<'a> {
|
||||
pub tag_name: &'static str,
|
||||
|
||||
pub namespace: Option<&'static str>,
|
||||
|
||||
pub key: Option<&'a str>,
|
||||
|
||||
pub dom_id: Cell<Option<ElementId>>,
|
||||
|
||||
pub listeners: &'a [Listener<'a>],
|
||||
|
||||
pub attributes: &'a [Attribute<'a>],
|
||||
|
||||
pub children: &'a [VNode<'a>],
|
||||
}
|
||||
|
||||
/// A trait for any generic Dioxus Element.
|
||||
///
|
||||
/// This trait provides the ability to use custom elements in the `rsx!` macro.
|
||||
///
|
||||
/// ```rust
|
||||
/// struct my_element;
|
||||
///
|
||||
/// impl DioxusElement for my_element {
|
||||
/// const TAG_NAME: "my_element";
|
||||
/// const NAME_SPACE: None;
|
||||
/// }
|
||||
///
|
||||
/// let _ = rsx!{
|
||||
/// my_element {}
|
||||
/// };
|
||||
/// ```
|
||||
pub trait DioxusElement {
|
||||
const TAG_NAME: &'static str;
|
||||
const NAME_SPACE: Option<&'static str>;
|
||||
|
@ -88,22 +224,6 @@ pub trait DioxusElement {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct VElement<'a> {
|
||||
// tag is always static
|
||||
pub tag_name: &'static str,
|
||||
pub namespace: Option<&'static str>,
|
||||
pub dom_id: Cell<Option<ElementId>>,
|
||||
|
||||
pub static_listeners: bool,
|
||||
pub listeners: &'a [Listener<'a>],
|
||||
|
||||
pub static_attrs: bool,
|
||||
pub attributes: &'a [Attribute<'a>],
|
||||
|
||||
pub static_children: bool,
|
||||
pub children: &'a [VNode<'a>],
|
||||
}
|
||||
|
||||
/// An attribute on a DOM node, such as `id="my-thing"` or
|
||||
/// `href="https://example.com"`.
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -116,35 +236,35 @@ pub struct Attribute<'a> {
|
|||
|
||||
pub is_volatile: bool,
|
||||
|
||||
// Doesn't exist in the html spec, mostly used to denote "style" tags - could be for any type of group
|
||||
// Doesn't exist in the html spec.
|
||||
// Used in Dioxus to denote "style" tags.
|
||||
pub namespace: Option<&'static str>,
|
||||
}
|
||||
|
||||
/// An event listener.
|
||||
/// IE onclick, onkeydown, etc
|
||||
pub struct Listener<'bump> {
|
||||
/// The type of event to listen for.
|
||||
pub(crate) event: &'static str,
|
||||
|
||||
pub mounted_node: Cell<Option<ElementId>>,
|
||||
|
||||
pub(crate) callback: RefCell<Option<BumpBox<'bump, dyn FnMut(VirtualEvent) + 'bump>>>,
|
||||
}
|
||||
/// The type of event to listen for.
|
||||
///
|
||||
/// IE "onclick" - whatever the renderer needs to attach the listener by name.
|
||||
pub event: &'static str,
|
||||
|
||||
impl Listener<'_> {
|
||||
// serialize the listener event stuff to a string
|
||||
pub fn serialize(&self) {
|
||||
//
|
||||
}
|
||||
pub fn deserialize() {
|
||||
//
|
||||
}
|
||||
pub(crate) callback: RefCell<Option<BumpBox<'bump, dyn FnMut(SyntheticEvent) + 'bump>>>,
|
||||
}
|
||||
|
||||
/// Virtual Components for custom user-defined components
|
||||
/// Only supports the functional syntax
|
||||
pub struct VComponent<'src> {
|
||||
pub ass_scope: Cell<Option<ScopeId>>,
|
||||
pub key: Option<&'src str>,
|
||||
|
||||
pub associated_scope: Cell<Option<ScopeId>>,
|
||||
|
||||
pub is_static: bool,
|
||||
|
||||
// Function pointer to the FC that was used to generate this component
|
||||
pub user_fc: *const (),
|
||||
|
||||
pub(crate) caller: Rc<dyn Fn(&Scope) -> DomTree>,
|
||||
|
||||
|
@ -154,19 +274,17 @@ pub struct VComponent<'src> {
|
|||
|
||||
pub(crate) drop_props: RefCell<Option<BumpBox<'src, dyn FnMut()>>>,
|
||||
|
||||
pub is_static: bool,
|
||||
pub(crate) can_memoize: bool,
|
||||
|
||||
pub can_memoize: bool,
|
||||
|
||||
// a pointer into the bump arena (given by the 'src lifetime)
|
||||
// Raw pointer into the bump arena for the props of the component
|
||||
pub(crate) raw_props: *const (),
|
||||
|
||||
// a pointer to the raw fn typ
|
||||
pub(crate) user_fc: *const (),
|
||||
}
|
||||
|
||||
pub struct VSuspended {
|
||||
pub node: Rc<Cell<Option<ElementId>>>,
|
||||
pub struct VSuspended<'a> {
|
||||
pub task_id: u64,
|
||||
pub dom_id: Cell<Option<ElementId>>,
|
||||
pub(crate) callback:
|
||||
RefCell<Option<BumpBox<'a, dyn FnMut(SuspendedContext<'a>) -> DomTree<'a>>>>,
|
||||
}
|
||||
|
||||
/// This struct provides an ergonomic API to quickly build VNodes.
|
||||
|
@ -196,26 +314,20 @@ impl<'a> NodeFactory<'a> {
|
|||
}
|
||||
|
||||
pub fn unstable_place_holder() -> VNode<'static> {
|
||||
VNode {
|
||||
key: None,
|
||||
kind: VNodeKind::Text(VText {
|
||||
text: "",
|
||||
dom_id: empty_cell(),
|
||||
is_static: true,
|
||||
}),
|
||||
}
|
||||
VNode::Text(VText {
|
||||
text: "",
|
||||
dom_id: empty_cell(),
|
||||
is_static: true,
|
||||
})
|
||||
}
|
||||
|
||||
/// Used in a place or two to make it easier to build vnodes from dummy text
|
||||
/// Directly pass in text blocks without the need to use the format_args macro.
|
||||
pub fn static_text(&self, text: &'static str) -> VNode<'a> {
|
||||
VNode {
|
||||
key: None,
|
||||
kind: VNodeKind::Text(VText {
|
||||
dom_id: empty_cell(),
|
||||
text,
|
||||
is_static: true,
|
||||
}),
|
||||
}
|
||||
VNode::Text(VText {
|
||||
dom_id: empty_cell(),
|
||||
text,
|
||||
is_static: true,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parses a lazy text Arguments and returns a string and a flag indicating if the text is 'static
|
||||
|
@ -226,9 +338,9 @@ impl<'a> NodeFactory<'a> {
|
|||
Some(static_str) => (static_str, true),
|
||||
None => {
|
||||
use bumpalo::core_alloc::fmt::Write;
|
||||
let mut s = bumpalo::collections::String::new_in(self.bump());
|
||||
s.write_fmt(args).unwrap();
|
||||
(s.into_bump_str(), false)
|
||||
let mut str_buf = bumpalo::collections::String::new_in(self.bump());
|
||||
str_buf.write_fmt(args).unwrap();
|
||||
(str_buf.into_bump_str(), false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -237,14 +349,12 @@ impl<'a> NodeFactory<'a> {
|
|||
///
|
||||
pub fn text(&self, args: Arguments) -> VNode<'a> {
|
||||
let (text, is_static) = self.raw_text(args);
|
||||
VNode {
|
||||
key: None,
|
||||
kind: VNodeKind::Text(VText {
|
||||
text,
|
||||
is_static,
|
||||
dom_id: empty_cell(),
|
||||
}),
|
||||
}
|
||||
|
||||
VNode::Text(VText {
|
||||
text,
|
||||
is_static,
|
||||
dom_id: empty_cell(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn element<L, A, V>(
|
||||
|
@ -272,7 +382,7 @@ impl<'a> NodeFactory<'a> {
|
|||
|
||||
pub fn raw_element<L, A, V>(
|
||||
&self,
|
||||
tag: &'static str,
|
||||
tag_name: &'static str,
|
||||
namespace: Option<&'static str>,
|
||||
listeners: L,
|
||||
attributes: A,
|
||||
|
@ -295,31 +405,15 @@ impl<'a> NodeFactory<'a> {
|
|||
|
||||
let key = key.map(|f| self.raw_text(f).0);
|
||||
|
||||
VNode {
|
||||
VNode::Element(self.bump().alloc(VElement {
|
||||
tag_name,
|
||||
key,
|
||||
kind: VNodeKind::Element(self.bump().alloc(VElement {
|
||||
tag_name: tag,
|
||||
namespace,
|
||||
listeners,
|
||||
attributes,
|
||||
children,
|
||||
dom_id: empty_cell(),
|
||||
|
||||
// todo: wire up more constization
|
||||
static_listeners: false,
|
||||
static_attrs: false,
|
||||
static_children: false,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn suspended() -> VNode<'static> {
|
||||
VNode {
|
||||
key: None,
|
||||
kind: VNodeKind::Suspended(VSuspended {
|
||||
node: Rc::new(empty_cell()),
|
||||
}),
|
||||
}
|
||||
namespace,
|
||||
listeners,
|
||||
attributes,
|
||||
children,
|
||||
dom_id: empty_cell(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn attr(
|
||||
|
@ -339,22 +433,6 @@ impl<'a> NodeFactory<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn attr_with_alloc_val(
|
||||
&self,
|
||||
name: &'static str,
|
||||
val: &'a str,
|
||||
namespace: Option<&'static str>,
|
||||
is_volatile: bool,
|
||||
) -> Attribute<'a> {
|
||||
Attribute {
|
||||
name,
|
||||
value: val,
|
||||
is_static: false,
|
||||
namespace,
|
||||
is_volatile,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn component<P, V>(
|
||||
&self,
|
||||
component: FC<P>,
|
||||
|
@ -367,14 +445,9 @@ impl<'a> NodeFactory<'a> {
|
|||
V: 'a + AsRef<[VNode<'a>]>,
|
||||
{
|
||||
let bump = self.bump();
|
||||
|
||||
// We don't want the fat part of the fat pointer
|
||||
// This function does static dispatch so we don't need any VTable stuff
|
||||
let children: &'a V = bump.alloc(children);
|
||||
let children = children.as_ref();
|
||||
|
||||
let props = bump.alloc(props);
|
||||
|
||||
let raw_props = props as *mut P as *mut ();
|
||||
let user_fc = component as *const ();
|
||||
|
||||
|
@ -403,42 +476,50 @@ impl<'a> NodeFactory<'a> {
|
|||
}
|
||||
}));
|
||||
|
||||
// create a closure to drop the props
|
||||
let mut has_dropped = false;
|
||||
let drop_props: &mut dyn FnMut() = bump.alloc_with(|| {
|
||||
move || unsafe {
|
||||
if !has_dropped {
|
||||
let real_other = raw_props as *mut _ as *mut P;
|
||||
let b = BumpBox::from_raw(real_other);
|
||||
std::mem::drop(b);
|
||||
let drop_props = {
|
||||
// create a closure to drop the props
|
||||
let mut has_dropped = false;
|
||||
|
||||
has_dropped = true;
|
||||
let drop_props: &mut dyn FnMut() = bump.alloc_with(|| {
|
||||
move || unsafe {
|
||||
if !has_dropped {
|
||||
let real_other = raw_props as *mut _ as *mut P;
|
||||
let b = BumpBox::from_raw(real_other);
|
||||
std::mem::drop(b);
|
||||
|
||||
has_dropped = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let drop_props = unsafe { BumpBox::from_raw(drop_props) };
|
||||
});
|
||||
|
||||
let drop_props = unsafe { BumpBox::from_raw(drop_props) };
|
||||
|
||||
RefCell::new(Some(drop_props))
|
||||
};
|
||||
|
||||
let is_static = children.len() == 0 && P::IS_STATIC && key.is_none();
|
||||
|
||||
let key = key.map(|f| self.raw_text(f).0);
|
||||
|
||||
VNode {
|
||||
let caller = NodeFactory::create_component_caller(component, raw_props);
|
||||
|
||||
let can_memoize = children.len() == 0 && P::IS_STATIC;
|
||||
|
||||
VNode::Component(bump.alloc_with(|| VComponent {
|
||||
user_fc,
|
||||
comparator,
|
||||
raw_props,
|
||||
children,
|
||||
caller,
|
||||
is_static,
|
||||
key,
|
||||
kind: VNodeKind::Component(bump.alloc_with(|| VComponent {
|
||||
user_fc,
|
||||
comparator,
|
||||
raw_props,
|
||||
children,
|
||||
caller: NodeFactory::create_component_caller(component, raw_props),
|
||||
is_static,
|
||||
drop_props: RefCell::new(Some(drop_props)),
|
||||
can_memoize: P::IS_STATIC,
|
||||
ass_scope: Cell::new(None),
|
||||
})),
|
||||
}
|
||||
can_memoize,
|
||||
drop_props,
|
||||
associated_scope: Cell::new(None),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn create_component_caller<'g, P: 'g>(
|
||||
pub(crate) fn create_component_caller<'g, P: 'g>(
|
||||
component: FC<P>,
|
||||
raw_props: *const (),
|
||||
) -> Rc<dyn for<'r> Fn(&'r Scope) -> DomTree<'r>> {
|
||||
|
@ -450,12 +531,8 @@ impl<'a> NodeFactory<'a> {
|
|||
props: safe_props,
|
||||
scope: scp,
|
||||
};
|
||||
|
||||
let res = component(cx);
|
||||
|
||||
let g2 = unsafe { std::mem::transmute(res) };
|
||||
|
||||
g2
|
||||
unsafe { std::mem::transmute(res) }
|
||||
});
|
||||
unsafe { std::mem::transmute::<_, Captured<'static>>(caller) }
|
||||
}
|
||||
|
@ -463,13 +540,30 @@ impl<'a> NodeFactory<'a> {
|
|||
pub fn fragment_from_iter(self, node_iter: impl IntoVNodeList<'a>) -> VNode<'a> {
|
||||
let children = node_iter.into_vnode_list(self);
|
||||
|
||||
VNode {
|
||||
// TODO
|
||||
// We need a dedicated path in the rsx! macro that will trigger the "you need keys" warning
|
||||
//
|
||||
// if cfg!(debug_assertions) {
|
||||
// if children.len() > 1 {
|
||||
// if children.last().unwrap().key().is_none() {
|
||||
// log::error!(
|
||||
// r#"
|
||||
// Warning: Each child in an array or iterator should have a unique "key" prop.
|
||||
// Not providing a key will lead to poor performance with lists.
|
||||
// See docs.rs/dioxus for more information.
|
||||
// ---
|
||||
// To help you identify where this error is coming from, we've generated a backtrace.
|
||||
// "#,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
VNode::Fragment(VFragment {
|
||||
children,
|
||||
key: None,
|
||||
kind: VNodeKind::Fragment(VFragment {
|
||||
children,
|
||||
is_static: false,
|
||||
}),
|
||||
}
|
||||
is_static: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -508,50 +602,51 @@ where
|
|||
nodes.push(node.into_vnode(cx));
|
||||
}
|
||||
|
||||
if cfg!(debug_assertions) {
|
||||
if nodes.len() > 1 {
|
||||
if nodes.last().unwrap().key().is_none() {
|
||||
log::error!(
|
||||
r#"
|
||||
Warning: Each child in an array or iterator should have a unique "key" prop.
|
||||
Not providing a key will lead to poor performance with lists.
|
||||
See docs.rs/dioxus for more information.
|
||||
---
|
||||
To help you identify where this error is coming from, we've generated a backtrace.
|
||||
"#,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if nodes.len() == 0 {
|
||||
nodes.push(VNode {
|
||||
kind: VNodeKind::Anchor(VAnchor {
|
||||
dom_id: empty_cell(),
|
||||
}),
|
||||
key: None,
|
||||
});
|
||||
nodes.push(VNode::Anchor(VAnchor {
|
||||
dom_id: empty_cell(),
|
||||
}));
|
||||
}
|
||||
|
||||
nodes.into_bump_slice()
|
||||
}
|
||||
}
|
||||
|
||||
/// Child nodes of the parent component.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// let children = cx.children();
|
||||
/// let first_node = &children[0];
|
||||
/// rsx!{
|
||||
/// h1 { {first_node} }
|
||||
/// p { {&children[1..]} }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
pub struct ScopeChildren<'a>(pub &'a [VNode<'a>]);
|
||||
|
||||
impl Copy for ScopeChildren<'_> {}
|
||||
|
||||
impl<'a> Clone for ScopeChildren<'a> {
|
||||
fn clone(&self) -> Self {
|
||||
ScopeChildren(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ScopeChildren<'_> {
|
||||
pub unsafe fn extend_lifetime(self) -> ScopeChildren<'static> {
|
||||
// dangerous method - used to fix the associated lifetime
|
||||
pub(crate) unsafe fn extend_lifetime(self) -> ScopeChildren<'static> {
|
||||
std::mem::transmute(self)
|
||||
}
|
||||
pub unsafe fn unextend_lfetime<'a>(self) -> ScopeChildren<'a> {
|
||||
|
||||
// dangerous method - used to fix the associated lifetime
|
||||
pub(crate) unsafe fn shorten_lifetime<'a>(self) -> ScopeChildren<'a> {
|
||||
std::mem::transmute(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoVNodeList<'a> for ScopeChildren<'a> {
|
||||
fn into_vnode_list(self, _: NodeFactory<'a>) -> &'a [VNode<'a>] {
|
||||
self.0
|
||||
|
@ -638,6 +733,7 @@ impl IntoVNode<'_> for Option<()> {
|
|||
cx.fragment_from_iter(None as Option<VNode>)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoVNode<'a> for Option<VNode<'a>> {
|
||||
fn into_vnode(self, cx: NodeFactory<'a>) -> VNode<'a> {
|
||||
match self {
|
||||
|
@ -666,14 +762,24 @@ impl Debug for NodeFactory<'_> {
|
|||
|
||||
impl Debug for VNode<'_> {
|
||||
fn fmt(&self, s: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
match &self.kind {
|
||||
VNodeKind::Element(el) => write!(s, "VElement {{ name: {} }}", el.tag_name),
|
||||
VNodeKind::Text(t) => write!(s, "VText {{ text: {} }}", t.text),
|
||||
VNodeKind::Anchor(a) => write!(s, "VAnchor"),
|
||||
match &self {
|
||||
VNode::Element(el) => {
|
||||
//
|
||||
s.debug_struct("VElement")
|
||||
.field("name", &el.tag_name)
|
||||
.field("key", &el.key)
|
||||
.finish()
|
||||
}
|
||||
VNode::Text(t) => write!(s, "VText {{ text: {} }}", t.text),
|
||||
VNode::Anchor(_) => write!(s, "VAnchor"),
|
||||
|
||||
VNodeKind::Fragment(_) => write!(s, "fragment"),
|
||||
VNodeKind::Suspended { .. } => write!(s, "suspended"),
|
||||
VNodeKind::Component(_) => write!(s, "component"),
|
||||
VNode::Fragment(frag) => write!(s, "VFragment {{ children: {:?} }}", frag.children),
|
||||
VNode::Suspended { .. } => write!(s, "VSuspended"),
|
||||
VNode::Component(comp) => write!(
|
||||
s,
|
||||
"VComponent {{ fc: {:?}, children: {:?} }}",
|
||||
comp.user_fc, comp.children
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
813
packages/core/src/scheduler.rs
Normal file
813
packages/core/src/scheduler.rs
Normal file
|
@ -0,0 +1,813 @@
|
|||
/*
|
||||
Welcome to Dioxus's cooperative, priority-based scheduler.
|
||||
|
||||
I hope you enjoy your stay.
|
||||
|
||||
Some essential reading:
|
||||
- https://github.com/facebook/react/blob/main/packages/scheduler/src/forks/Scheduler.js#L197-L200
|
||||
- https://github.com/facebook/react/blob/main/packages/scheduler/src/forks/Scheduler.js#L440
|
||||
- https://github.com/WICG/is-input-pending
|
||||
- https://web.dev/rail/
|
||||
- https://indepth.dev/posts/1008/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react
|
||||
|
||||
# What's going on?
|
||||
|
||||
Dioxus is a framework for "user experience" - not just "user interfaces." Part of the "experience" is keeping the UI
|
||||
snappy and "jank free" even under heavy work loads. Dioxus already has the "speed" part figured out - but there's no
|
||||
point if being "fast" if you can't also be "responsive."
|
||||
|
||||
As such, Dioxus can manually decide on what work is most important at any given moment in time. With a properly tuned
|
||||
priority system, Dioxus can ensure that user interaction is prioritized and committed as soon as possible (sub 100ms).
|
||||
The controller responsible for this priority management is called the "scheduler" and is responsible for juggling many
|
||||
different types of work simultaneously.
|
||||
|
||||
# How does it work?
|
||||
|
||||
Per the RAIL guide, we want to make sure that A) inputs are handled ASAP and B) animations are not blocked.
|
||||
React-three-fiber is a testament to how amazing this can be - a ThreeJS scene is threaded in between work periods of
|
||||
React, and the UI still stays snappy!
|
||||
|
||||
While it's straightforward to run code ASAP and be as "fast as possible", what's not _not_ straightforward is how to do
|
||||
this while not blocking the main thread. The current prevailing thought is to stop working periodically so the browser
|
||||
has time to paint and run animations. When the browser is finished, we can step in and continue our work.
|
||||
|
||||
React-Fiber uses the "Fiber" concept to achieve a pause-resume functionality. This is worth reading up on, but not
|
||||
necessary to understand what we're doing here. In Dioxus, our DiffMachine is guided by DiffInstructions - essentially
|
||||
"commands" that guide the Diffing algorithm through the tree. Our "diff_scope" method is async - we can literally pause
|
||||
our DiffMachine "mid-sentence" (so to speak) by just stopping the poll on the future. The DiffMachine periodically yields
|
||||
so Rust's async machinery can take over, allowing us to customize when exactly to pause it.
|
||||
|
||||
React's "should_yield" method is more complex than ours, and I assume we'll move in that direction as Dioxus matures. For
|
||||
now, Dioxus just assumes a TimeoutFuture, and selects! on both the Diff algorithm and timeout. If the DiffMachine finishes
|
||||
before the timeout, then Dioxus will work on any pending work in the interim. If there is no pending work, then the changes
|
||||
are committed, and coroutines are polled during the idle period. However, if the timeout expires, then the DiffMachine
|
||||
future is paused and saved (self-referentially).
|
||||
|
||||
# Priorty System
|
||||
|
||||
So far, we've been able to thread our Dioxus work between animation frames - the main thread is not blocked! But that
|
||||
doesn't help us _under load_. How do we still stay snappy... even if we're doing a lot of work? Well, that's where
|
||||
priorities come into play. The goal with priorities is to schedule shorter work as a "high" priority and longer work as
|
||||
a "lower" priority. That way, we can interrupt long-running low-prioty work with short-running high-priority work.
|
||||
|
||||
React's priority system is quite complex.
|
||||
|
||||
There are 5 levels of priority and 2 distinctions between UI events (discrete, continuous). I believe React really only
|
||||
uses 3 priority levels and "idle" priority isn't used... Regardless, there's some batching going on.
|
||||
|
||||
For Dioxus, we're going with a 4 tier priorty system:
|
||||
- Sync: Things that need to be done by the next frame, like TextInput on controlled elements
|
||||
- High: for events that block all others - clicks, keyboard, and hovers
|
||||
- Medium: for UI events caused by the user but not directly - scrolls/forms/focus (all other events)
|
||||
- Low: set_state called asynchronously, and anything generated by suspense
|
||||
|
||||
In "Sync" state, we abort our "idle wait" future, and resolve the sync queue immediately and escape. Because we completed
|
||||
work before the next rAF, any edits can be immediately processed before the frame ends. Generally though, we want to leave
|
||||
as much time to rAF as possible. "Sync" is currently only used by onInput - we'll leave some docs telling people not to
|
||||
do anything too arduous from onInput.
|
||||
|
||||
For the rest, we defer to the rIC period and work down each queue from high to low.
|
||||
*/
|
||||
use crate::heuristics::*;
|
||||
use crate::innerlude::*;
|
||||
use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
use futures_util::stream::FuturesUnordered;
|
||||
use futures_util::{future::FusedFuture, pin_mut, Future, FutureExt, StreamExt};
|
||||
use fxhash::{FxHashMap, FxHashSet};
|
||||
use indexmap::IndexSet;
|
||||
use slab::Slab;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cell::{Cell, RefCell, RefMut, UnsafeCell},
|
||||
collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque},
|
||||
fmt::Display,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EventChannel {
|
||||
pub task_counter: Rc<Cell<u64>>,
|
||||
pub sender: UnboundedSender<SchedulerMsg>,
|
||||
pub schedule_any_immediate: Rc<dyn Fn(ScopeId)>,
|
||||
pub submit_task: Rc<dyn Fn(FiberTask) -> TaskHandle>,
|
||||
pub get_shared_context: Rc<dyn Fn(ScopeId, TypeId) -> Option<Rc<dyn Any>>>,
|
||||
}
|
||||
|
||||
pub enum SchedulerMsg {
|
||||
Immediate(ScopeId),
|
||||
UiEvent(UserEvent),
|
||||
SubmitTask(FiberTask, u64),
|
||||
ToggleTask(u64),
|
||||
PauseTask(u64),
|
||||
ResumeTask(u64),
|
||||
DropTask(u64),
|
||||
}
|
||||
|
||||
/// The scheduler holds basically everything around "working"
|
||||
///
|
||||
/// Each scope has the ability to lightly interact with the scheduler (IE, schedule an update) but ultimately the scheduler calls the components.
|
||||
///
|
||||
/// In Dioxus, the scheduler provides 3 priority levels - each with their own "DiffMachine". The DiffMachine state can be saved if the deadline runs
|
||||
/// out.
|
||||
///
|
||||
/// Saved DiffMachine state can be self-referential, so we need to be careful about how we save it. All self-referential data is a link between
|
||||
/// pending DiffInstructions, Mutations, and their underlying Scope. It's okay for us to be self-referential with this data, provided we don't priority
|
||||
/// task shift to a higher priority task that needs mutable access to the same scopes.
|
||||
///
|
||||
/// We can prevent this safety issue from occurring if we track which scopes are invalidated when starting a new task.
|
||||
///
|
||||
///
|
||||
pub(crate) struct Scheduler {
|
||||
/// All mounted components are arena allocated to make additions, removals, and references easy to work with
|
||||
/// A generational arena is used to re-use slots of deleted scopes without having to resize the underlying arena.
|
||||
///
|
||||
/// This is wrapped in an UnsafeCell because we will need to get mutable access to unique values in unique bump arenas
|
||||
/// and rusts's guartnees cannot prove that this is safe. We will need to maintain the safety guarantees manually.
|
||||
pub pool: ResourcePool,
|
||||
|
||||
pub heuristics: HeuristicsEngine,
|
||||
|
||||
pub receiver: UnboundedReceiver<SchedulerMsg>,
|
||||
|
||||
// Garbage stored
|
||||
pub pending_garbage: FxHashSet<ScopeId>,
|
||||
|
||||
// In-flight futures
|
||||
pub async_tasks: FuturesUnordered<FiberTask>,
|
||||
|
||||
// scheduler stuff
|
||||
pub current_priority: EventPriority,
|
||||
|
||||
pub ui_events: VecDeque<UserEvent>,
|
||||
|
||||
pub pending_immediates: VecDeque<ScopeId>,
|
||||
|
||||
pub pending_tasks: VecDeque<UserEvent>,
|
||||
|
||||
pub garbage_scopes: HashSet<ScopeId>,
|
||||
|
||||
pub lanes: [PriorityLane; 4],
|
||||
}
|
||||
|
||||
impl Scheduler {
|
||||
pub fn new() -> Self {
|
||||
/*
|
||||
Preallocate 2000 elements and 100 scopes to avoid dynamic allocation.
|
||||
Perhaps this should be configurable from some external config?
|
||||
*/
|
||||
let components = Rc::new(UnsafeCell::new(Slab::with_capacity(100)));
|
||||
let raw_elements = Rc::new(UnsafeCell::new(Slab::with_capacity(2000)));
|
||||
|
||||
let heuristics = HeuristicsEngine::new();
|
||||
|
||||
let (sender, receiver) = futures_channel::mpsc::unbounded::<SchedulerMsg>();
|
||||
let task_counter = Rc::new(Cell::new(0));
|
||||
|
||||
let channel = EventChannel {
|
||||
task_counter: task_counter.clone(),
|
||||
sender: sender.clone(),
|
||||
schedule_any_immediate: {
|
||||
let sender = sender.clone();
|
||||
Rc::new(move |id| sender.unbounded_send(SchedulerMsg::Immediate(id)).unwrap())
|
||||
},
|
||||
submit_task: {
|
||||
let sender = sender.clone();
|
||||
Rc::new(move |fiber_task| {
|
||||
let task_id = task_counter.get();
|
||||
task_counter.set(task_id + 1);
|
||||
sender
|
||||
.unbounded_send(SchedulerMsg::SubmitTask(fiber_task, task_id))
|
||||
.unwrap();
|
||||
TaskHandle {
|
||||
our_id: task_id,
|
||||
sender: sender.clone(),
|
||||
}
|
||||
})
|
||||
},
|
||||
get_shared_context: {
|
||||
let components = components.clone();
|
||||
Rc::new(move |id, ty| {
|
||||
let components = unsafe { &*components.get() };
|
||||
let mut search: Option<&Scope> = components.get(id.0);
|
||||
while let Some(inner) = search.take() {
|
||||
if let Some(shared) = inner.shared_contexts.borrow().get(&ty) {
|
||||
return Some(shared.clone());
|
||||
} else {
|
||||
search = inner.parent_idx.map(|id| components.get(id.0)).flatten();
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
},
|
||||
};
|
||||
|
||||
let pool = ResourcePool {
|
||||
components: components.clone(),
|
||||
raw_elements,
|
||||
channel,
|
||||
};
|
||||
|
||||
Self {
|
||||
pool,
|
||||
receiver,
|
||||
|
||||
async_tasks: FuturesUnordered::new(),
|
||||
|
||||
pending_garbage: FxHashSet::default(),
|
||||
|
||||
heuristics,
|
||||
|
||||
// a storage for our receiver to dump into
|
||||
ui_events: VecDeque::new(),
|
||||
|
||||
pending_immediates: VecDeque::new(),
|
||||
|
||||
pending_tasks: VecDeque::new(),
|
||||
|
||||
garbage_scopes: HashSet::new(),
|
||||
|
||||
current_priority: EventPriority::Low,
|
||||
|
||||
// a dedicated fiber for each priority
|
||||
lanes: [
|
||||
PriorityLane::new(),
|
||||
PriorityLane::new(),
|
||||
PriorityLane::new(),
|
||||
PriorityLane::new(),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn manually_poll_events(&mut self) {
|
||||
while let Ok(Some(msg)) = self.receiver.try_next() {
|
||||
self.handle_channel_msg(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// Converts UI events into dirty scopes with various priorities
|
||||
pub fn consume_pending_events(&mut self) {
|
||||
while let Some(trigger) = self.ui_events.pop_back() {
|
||||
if let Some(scope) = self.pool.get_scope_mut(trigger.scope) {
|
||||
if let Some(element) = trigger.mounted_dom_id {
|
||||
let priority = match &trigger.event {
|
||||
SyntheticEvent::ClipboardEvent(_) => {}
|
||||
SyntheticEvent::CompositionEvent(_) => {}
|
||||
SyntheticEvent::KeyboardEvent(_) => {}
|
||||
SyntheticEvent::FocusEvent(_) => {}
|
||||
SyntheticEvent::FormEvent(_) => {}
|
||||
SyntheticEvent::SelectionEvent(_) => {}
|
||||
SyntheticEvent::TouchEvent(_) => {}
|
||||
SyntheticEvent::WheelEvent(_) => {}
|
||||
SyntheticEvent::MediaEvent(_) => {}
|
||||
SyntheticEvent::AnimationEvent(_) => {}
|
||||
SyntheticEvent::TransitionEvent(_) => {}
|
||||
SyntheticEvent::ToggleEvent(_) => {}
|
||||
SyntheticEvent::MouseEvent(_) => {}
|
||||
SyntheticEvent::PointerEvent(_) => {}
|
||||
SyntheticEvent::GenericEvent(_) => {}
|
||||
};
|
||||
|
||||
scope.call_listener(trigger.event, element);
|
||||
// let receiver = self.immediate_receiver.clone();
|
||||
// let mut receiver = receiver.borrow_mut();
|
||||
|
||||
// // Drain the immediates into the dirty scopes, setting the appropiate priorities
|
||||
// while let Ok(Some(dirty_scope)) = receiver.try_next() {
|
||||
// self.add_dirty_scope(dirty_scope, trigger.priority)
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// nothing to do, no events on channels, no work
|
||||
pub fn has_any_work(&self) -> bool {
|
||||
let pending_lanes = self.lanes.iter().find(|f| f.has_work()).is_some();
|
||||
pending_lanes || self.has_pending_events()
|
||||
}
|
||||
|
||||
pub fn has_pending_events(&self) -> bool {
|
||||
self.ui_events.len() > 0
|
||||
}
|
||||
|
||||
fn shift_priorities(&mut self) {
|
||||
self.current_priority = match (
|
||||
self.lanes[0].has_work(),
|
||||
self.lanes[1].has_work(),
|
||||
self.lanes[2].has_work(),
|
||||
self.lanes[3].has_work(),
|
||||
) {
|
||||
(true, _, _, _) => EventPriority::Immediate,
|
||||
(false, true, _, _) => EventPriority::High,
|
||||
(false, false, true, _) => EventPriority::Medium,
|
||||
(false, false, false, _) => EventPriority::Low,
|
||||
};
|
||||
}
|
||||
|
||||
/// re-balance the work lanes, ensuring high-priority work properly bumps away low priority work
|
||||
fn balance_lanes(&mut self) {}
|
||||
|
||||
fn load_current_lane(&mut self) -> &mut PriorityLane {
|
||||
match self.current_priority {
|
||||
EventPriority::Immediate => todo!(),
|
||||
EventPriority::High => todo!(),
|
||||
EventPriority::Medium => todo!(),
|
||||
EventPriority::Low => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn save_work(&mut self, lane: SavedDiffWork) {
|
||||
let saved: SavedDiffWork<'static> = unsafe { std::mem::transmute(lane) };
|
||||
self.load_current_lane().saved_state = Some(saved);
|
||||
}
|
||||
|
||||
fn load_work(&mut self) -> SavedDiffWork<'static> {
|
||||
match self.current_priority {
|
||||
EventPriority::Immediate => todo!(),
|
||||
EventPriority::High => todo!(),
|
||||
EventPriority::Medium => todo!(),
|
||||
EventPriority::Low => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Work the scheduler down, not polling any ongoing tasks.
|
||||
///
|
||||
/// Will use the standard priority-based scheduling, batching, etc, but just won't interact with the async reactor.
|
||||
pub fn work_sync<'a>(&'a mut self) -> Vec<Mutations<'a>> {
|
||||
let mut committed_mutations = Vec::new();
|
||||
|
||||
self.manually_poll_events();
|
||||
|
||||
if !self.has_any_work() {
|
||||
self.clean_up_garbage();
|
||||
return committed_mutations;
|
||||
}
|
||||
|
||||
self.consume_pending_events();
|
||||
|
||||
while self.has_any_work() {
|
||||
self.shift_priorities();
|
||||
self.work_on_current_lane(&mut || false, &mut committed_mutations);
|
||||
}
|
||||
|
||||
committed_mutations
|
||||
}
|
||||
|
||||
/// The primary workhorse of the VirtualDOM.
|
||||
///
|
||||
/// Uses some fairly complex logic to schedule what work should be produced.
|
||||
///
|
||||
/// Returns a list of successful mutations.
|
||||
pub async fn work_with_deadline<'a>(
|
||||
&'a mut self,
|
||||
mut deadline_reached: Pin<Box<impl FusedFuture<Output = ()>>>,
|
||||
) -> Vec<Mutations<'a>> {
|
||||
/*
|
||||
Strategy:
|
||||
- When called, check for any UI events that might've been received since the last frame.
|
||||
- Dump all UI events into a "pending discrete" queue and a "pending continuous" queue.
|
||||
|
||||
- If there are any pending discrete events, then elevate our priorty level. If our priority level is already "high,"
|
||||
then we need to finish the high priority work first. If the current work is "low" then analyze what scopes
|
||||
will be invalidated by this new work. If this interferes with any in-flight medium or low work, then we need
|
||||
to bump the other work out of the way, or choose to process it so we don't have any conflicts.
|
||||
'static components have a leg up here since their work can be re-used among multiple scopes.
|
||||
"High priority" is only for blocking! Should only be used on "clicks"
|
||||
|
||||
- If there are no pending discrete events, then check for continuous events. These can be completely batched
|
||||
|
||||
|
||||
Open questions:
|
||||
- what if we get two clicks from the component during the same slice?
|
||||
- should we batch?
|
||||
- react says no - they are continuous
|
||||
- but if we received both - then we don't need to diff, do we? run as many as we can and then finally diff?
|
||||
*/
|
||||
let mut committed_mutations = Vec::<Mutations<'static>>::new();
|
||||
|
||||
loop {
|
||||
// Internalize any pending work since the last time we ran
|
||||
self.manually_poll_events();
|
||||
|
||||
// Wait for any new events if we have nothing to do
|
||||
if !self.has_any_work() {
|
||||
self.clean_up_garbage();
|
||||
let deadline_expired = self.wait_for_any_trigger(&mut deadline_reached).await;
|
||||
|
||||
if deadline_expired {
|
||||
return committed_mutations;
|
||||
}
|
||||
}
|
||||
|
||||
// Create work from the pending event queue
|
||||
self.consume_pending_events();
|
||||
|
||||
// shift to the correct lane
|
||||
self.shift_priorities();
|
||||
|
||||
let mut deadline_reached = || (&mut deadline_reached).now_or_never().is_some();
|
||||
|
||||
let finished_before_deadline =
|
||||
self.work_on_current_lane(&mut deadline_reached, &mut committed_mutations);
|
||||
|
||||
if !finished_before_deadline {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
committed_mutations
|
||||
}
|
||||
|
||||
/// Load the current lane, and work on it, periodically checking in if the deadline has been reached.
|
||||
///
|
||||
/// Returns true if the lane is finished before the deadline could be met.
|
||||
pub fn work_on_current_lane(
|
||||
&mut self,
|
||||
deadline_reached: &mut impl FnMut() -> bool,
|
||||
mutations: &mut Vec<Mutations>,
|
||||
) -> bool {
|
||||
// Work through the current subtree, and commit the results when it finishes
|
||||
// When the deadline expires, give back the work
|
||||
let saved_state = self.load_work();
|
||||
|
||||
// We have to split away some parts of ourself - current lane is borrowed mutably
|
||||
let mut shared = self.pool.clone();
|
||||
let mut machine = unsafe { saved_state.promote(&mut shared) };
|
||||
|
||||
if machine.stack.is_empty() {
|
||||
let shared = self.pool.clone();
|
||||
|
||||
self.current_lane().dirty_scopes.sort_by(|a, b| {
|
||||
let h1 = shared.get_scope(*a).unwrap().height;
|
||||
let h2 = shared.get_scope(*b).unwrap().height;
|
||||
h1.cmp(&h2)
|
||||
});
|
||||
|
||||
if let Some(scope) = self.current_lane().dirty_scopes.pop() {
|
||||
let component = self.pool.get_scope(scope).unwrap();
|
||||
let (old, new) = (component.frames.wip_head(), component.frames.fin_head());
|
||||
machine.stack.push(DiffInstruction::DiffNode { new, old });
|
||||
}
|
||||
}
|
||||
|
||||
let deadline_expired = machine.work(deadline_reached);
|
||||
|
||||
let machine: DiffMachine<'static> = unsafe { std::mem::transmute(machine) };
|
||||
let mut saved = machine.save();
|
||||
|
||||
if deadline_expired {
|
||||
self.save_work(saved);
|
||||
false
|
||||
} else {
|
||||
for node in saved.seen_scopes.drain() {
|
||||
self.current_lane().dirty_scopes.remove(&node);
|
||||
}
|
||||
|
||||
let mut new_mutations = Mutations::new();
|
||||
std::mem::swap(&mut new_mutations, &mut saved.mutations);
|
||||
|
||||
mutations.push(new_mutations);
|
||||
self.save_work(saved);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
// waits for a trigger, canceling early if the deadline is reached
|
||||
// returns true if the deadline was reached
|
||||
// does not return the trigger, but caches it in the scheduler
|
||||
pub async fn wait_for_any_trigger(
|
||||
&mut self,
|
||||
deadline: &mut Pin<Box<impl FusedFuture<Output = ()>>>,
|
||||
) -> bool {
|
||||
use futures_util::future::{select, Either};
|
||||
|
||||
let event_fut = async {
|
||||
match select(self.receiver.next(), self.async_tasks.next()).await {
|
||||
Either::Left((msg, _other)) => {
|
||||
self.handle_channel_msg(msg.unwrap());
|
||||
}
|
||||
Either::Right((task, _other)) => {
|
||||
// do nothing, async task will likely generate a set of scheduler messages
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pin_mut!(event_fut);
|
||||
|
||||
match select(event_fut, deadline).await {
|
||||
Either::Left((msg, _other)) => false,
|
||||
Either::Right((deadline, _)) => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_lane(&mut self) -> &mut PriorityLane {
|
||||
match self.current_priority {
|
||||
EventPriority::Immediate => &mut self.lanes[0],
|
||||
EventPriority::High => &mut self.lanes[1],
|
||||
EventPriority::Medium => &mut self.lanes[2],
|
||||
EventPriority::Low => &mut self.lanes[3],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_channel_msg(&mut self, msg: SchedulerMsg) {
|
||||
match msg {
|
||||
SchedulerMsg::Immediate(_) => todo!(),
|
||||
SchedulerMsg::UiEvent(_) => todo!(),
|
||||
|
||||
//
|
||||
SchedulerMsg::SubmitTask(_, _) => todo!(),
|
||||
SchedulerMsg::ToggleTask(_) => todo!(),
|
||||
SchedulerMsg::PauseTask(_) => todo!(),
|
||||
SchedulerMsg::ResumeTask(_) => todo!(),
|
||||
SchedulerMsg::DropTask(_) => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_dirty_scope(&mut self, scope: ScopeId, priority: EventPriority) {
|
||||
todo!()
|
||||
// match priority {
|
||||
// EventPriority::High => self.high_priorty.dirty_scopes.insert(scope),
|
||||
// EventPriority::Medium => self.medium_priority.dirty_scopes.insert(scope),
|
||||
// EventPriority::Low => self.low_priority.dirty_scopes.insert(scope),
|
||||
// };
|
||||
}
|
||||
|
||||
fn collect_garbage(&mut self, id: ElementId) {
|
||||
//
|
||||
}
|
||||
|
||||
pub fn clean_up_garbage(&mut self) {
|
||||
let mut scopes_to_kill = Vec::new();
|
||||
let mut garbage_list = Vec::new();
|
||||
|
||||
for scope in self.garbage_scopes.drain() {
|
||||
let scope = self.pool.get_scope_mut(scope).unwrap();
|
||||
for node in scope.consume_garbage() {
|
||||
garbage_list.push(node);
|
||||
}
|
||||
|
||||
while let Some(node) = garbage_list.pop() {
|
||||
match &node {
|
||||
VNode::Text(_) => {
|
||||
self.pool.collect_garbage(node.mounted_id());
|
||||
}
|
||||
VNode::Anchor(_) => {
|
||||
self.pool.collect_garbage(node.mounted_id());
|
||||
}
|
||||
VNode::Suspended(_) => {
|
||||
self.pool.collect_garbage(node.mounted_id());
|
||||
}
|
||||
|
||||
VNode::Element(el) => {
|
||||
self.pool.collect_garbage(node.mounted_id());
|
||||
for child in el.children {
|
||||
garbage_list.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
VNode::Fragment(frag) => {
|
||||
for child in frag.children {
|
||||
garbage_list.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
VNode::Component(comp) => {
|
||||
// TODO: run the hook destructors and then even delete the scope
|
||||
|
||||
let scope_id = comp.associated_scope.get().unwrap();
|
||||
let scope = self.pool.get_scope(scope_id).unwrap();
|
||||
let root = scope.root();
|
||||
|
||||
garbage_list.push(root);
|
||||
scopes_to_kill.push(scope_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for scope in scopes_to_kill.drain(..) {
|
||||
//
|
||||
// kill em
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PriorityLane {
|
||||
pub dirty_scopes: IndexSet<ScopeId>,
|
||||
pub saved_state: Option<SavedDiffWork<'static>>,
|
||||
pub in_progress: bool,
|
||||
}
|
||||
|
||||
impl PriorityLane {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
saved_state: None,
|
||||
dirty_scopes: Default::default(),
|
||||
in_progress: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn has_work(&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn work(&mut self) {
|
||||
let scope = self.dirty_scopes.pop();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TaskHandle {
|
||||
pub sender: UnboundedSender<SchedulerMsg>,
|
||||
pub our_id: u64,
|
||||
}
|
||||
|
||||
impl TaskHandle {
|
||||
/// Toggles this coroutine off/on.
|
||||
///
|
||||
/// This method is not synchronous - your task will not stop immediately.
|
||||
pub fn toggle(&self) {
|
||||
self.sender
|
||||
.unbounded_send(SchedulerMsg::ToggleTask(self.our_id))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// This method is not synchronous - your task will not stop immediately.
|
||||
pub fn resume(&self) {
|
||||
self.sender
|
||||
.unbounded_send(SchedulerMsg::ResumeTask(self.our_id))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// This method is not synchronous - your task will not stop immediately.
|
||||
pub fn stop(&self) {
|
||||
self.sender
|
||||
.unbounded_send(SchedulerMsg::ToggleTask(self.our_id))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// This method is not synchronous - your task will not stop immediately.
|
||||
pub fn restart(&self) {
|
||||
self.sender
|
||||
.unbounded_send(SchedulerMsg::ToggleTask(self.our_id))
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||
pub struct ScopeId(pub usize);
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||
pub struct ElementId(pub usize);
|
||||
impl Display for ElementId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ElementId {
|
||||
pub fn as_u64(self) -> u64 {
|
||||
self.0 as u64
|
||||
}
|
||||
}
|
||||
|
||||
/// Priority of Event Triggers.
|
||||
///
|
||||
/// Internally, Dioxus will abort work that's taking too long if new, more important, work arrives. Unlike React, Dioxus
|
||||
/// won't be afraid to pause work or flush changes to the RealDOM. This is called "cooperative scheduling". Some Renderers
|
||||
/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well.
|
||||
///
|
||||
/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes.
|
||||
///
|
||||
/// React has a 5-tier priority system. However, they break things into "Continuous" and "Discrete" priority. For now,
|
||||
/// we keep it simple, and just use a 3-tier priority system.
|
||||
///
|
||||
/// - NoPriority = 0
|
||||
/// - LowPriority = 1
|
||||
/// - NormalPriority = 2
|
||||
/// - UserBlocking = 3
|
||||
/// - HighPriority = 4
|
||||
/// - ImmediatePriority = 5
|
||||
///
|
||||
/// We still have a concept of discrete vs continuous though - discrete events won't be batched, but continuous events will.
|
||||
/// This means that multiple "scroll" events will be processed in a single frame, but multiple "click" events will be
|
||||
/// flushed before proceeding. Multiple discrete events is highly unlikely, though.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)]
|
||||
pub enum EventPriority {
|
||||
/// Work that must be completed during the EventHandler phase.
|
||||
///
|
||||
/// Currently this is reserved for controlled inputs.
|
||||
Immediate = 3,
|
||||
|
||||
/// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work.
|
||||
///
|
||||
/// This is typically reserved for things like user interaction.
|
||||
///
|
||||
/// React calls these "discrete" events, but with an extra category of "user-blocking" (Immediate).
|
||||
High = 2,
|
||||
|
||||
/// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important
|
||||
/// than "High Priority" events and will take presedence over low priority events.
|
||||
///
|
||||
/// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input.
|
||||
///
|
||||
/// React calls these "continuous" events (e.g. mouse move, mouse wheel, touch move, etc).
|
||||
Medium = 1,
|
||||
|
||||
/// "Low Priority" work will always be pre-empted unless the work is significantly delayed, in which case it will be
|
||||
/// advanced to the front of the work queue until completed.
|
||||
///
|
||||
/// The primary user of Low Priority work is the asynchronous work system (suspense).
|
||||
///
|
||||
/// This is considered "idle" work or "background" work.
|
||||
Low = 0,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ResourcePool {
|
||||
/*
|
||||
This *has* to be an UnsafeCell.
|
||||
|
||||
Each BumpFrame and Scope is located in this Slab - and we'll need mutable access to a scope while holding on to
|
||||
its bumpframe conents immutably.
|
||||
|
||||
However, all of the interaction with this Slab is done in this module and the Diff module, so it should be fairly
|
||||
simple to audit.
|
||||
|
||||
Wrapped in Rc so the "get_shared_context" closure can walk the tree (immutably!)
|
||||
*/
|
||||
pub components: Rc<UnsafeCell<Slab<Scope>>>,
|
||||
|
||||
/*
|
||||
Yes, a slab of "nil". We use this for properly ordering ElementIDs - all we care about is the allocation strategy
|
||||
that slab uses. The slab essentially just provides keys for ElementIDs that we can re-use in a Vec on the client.
|
||||
|
||||
This just happened to be the simplest and most efficient way to implement a deterministic keyed map with slot reuse.
|
||||
|
||||
In the future, we could actually store a pointer to the VNode instead of nil to provide O(1) lookup for VNodes...
|
||||
*/
|
||||
pub raw_elements: Rc<UnsafeCell<Slab<()>>>,
|
||||
|
||||
pub channel: EventChannel,
|
||||
}
|
||||
|
||||
impl ResourcePool {
|
||||
/// this is unsafe because the caller needs to track which other scopes it's already using
|
||||
pub fn get_scope(&self, idx: ScopeId) -> Option<&Scope> {
|
||||
let inner = unsafe { &*self.components.get() };
|
||||
inner.get(idx.0)
|
||||
}
|
||||
|
||||
/// this is unsafe because the caller needs to track which other scopes it's already using
|
||||
pub fn get_scope_mut(&self, idx: ScopeId) -> Option<&mut Scope> {
|
||||
let inner = unsafe { &mut *self.components.get() };
|
||||
inner.get_mut(idx.0)
|
||||
}
|
||||
|
||||
pub fn with_scope<'b, O: 'static>(
|
||||
&'b self,
|
||||
_id: ScopeId,
|
||||
_f: impl FnOnce(&'b mut Scope) -> O,
|
||||
) -> Option<O> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
// return a bumpframe with a lifetime attached to the arena borrow
|
||||
// this is useful for merging lifetimes
|
||||
pub fn with_scope_vnode<'b>(
|
||||
&self,
|
||||
_id: ScopeId,
|
||||
_f: impl FnOnce(&mut Scope) -> &VNode<'b>,
|
||||
) -> Option<&VNode<'b>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn try_remove(&self, id: ScopeId) -> Option<Scope> {
|
||||
let inner = unsafe { &mut *self.components.get() };
|
||||
Some(inner.remove(id.0))
|
||||
// .try_remove(id.0)
|
||||
// .ok_or_else(|| Error::FatalInternal("Scope not found"))
|
||||
}
|
||||
|
||||
pub fn reserve_node(&self) -> ElementId {
|
||||
let els = unsafe { &mut *self.raw_elements.get() };
|
||||
ElementId(els.insert(()))
|
||||
}
|
||||
|
||||
/// return the id, freeing the space of the original node
|
||||
pub fn collect_garbage(&self, id: ElementId) {
|
||||
todo!("garabge collection currently WIP")
|
||||
// self.raw_elements.remove(id.0);
|
||||
}
|
||||
|
||||
pub fn insert_scope_with_key(&self, f: impl FnOnce(ScopeId) -> Scope) -> ScopeId {
|
||||
let g = unsafe { &mut *self.components.get() };
|
||||
let entry = g.vacant_entry();
|
||||
let id = ScopeId(entry.key());
|
||||
entry.insert(f(id));
|
||||
id
|
||||
}
|
||||
|
||||
pub fn borrow_bumpframe(&self) {}
|
||||
}
|
|
@ -1,11 +1,9 @@
|
|||
use crate::innerlude::*;
|
||||
use bumpalo::boxed::Box as BumpBox;
|
||||
use fxhash::FxHashSet;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
borrow::BorrowMut,
|
||||
cell::{Cell, RefCell},
|
||||
collections::{HashMap, HashSet},
|
||||
cell::RefCell,
|
||||
collections::HashMap,
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
|
@ -38,20 +36,23 @@ pub struct Scope {
|
|||
// Listeners
|
||||
pub(crate) listeners: RefCell<Vec<*const Listener<'static>>>,
|
||||
pub(crate) borrowed_props: RefCell<Vec<*const VComponent<'static>>>,
|
||||
pub(crate) suspended_nodes: RefCell<HashMap<u64, *const VSuspended<'static>>>,
|
||||
|
||||
// State
|
||||
pub(crate) hooks: HookList,
|
||||
pub(crate) shared_contexts: RefCell<HashMap<TypeId, Rc<dyn Any>>>,
|
||||
|
||||
// A reference to the resources shared by all the comonents
|
||||
pub(crate) vdom: SharedResources,
|
||||
pub(crate) memoized_updater: Rc<dyn Fn() + 'static>,
|
||||
|
||||
pub(crate) shared: EventChannel,
|
||||
}
|
||||
|
||||
// The type of closure that wraps calling components
|
||||
pub type WrappedCaller = dyn for<'b> Fn(&'b Scope) -> DomTree<'b>;
|
||||
|
||||
// The type of task that gets sent to the task scheduler
|
||||
pub type FiberTask = Pin<Box<dyn Future<Output = EventTrigger>>>;
|
||||
/// The type of task that gets sent to the task scheduler
|
||||
/// Submitting a fiber task returns a handle to that task, which can be used to wake up suspended nodes
|
||||
pub type FiberTask = Pin<Box<dyn Future<Output = ScopeId>>>;
|
||||
|
||||
impl Scope {
|
||||
// we are being created in the scope of an existing component (where the creator_node lifetime comes into play)
|
||||
|
@ -61,38 +62,30 @@ 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>(
|
||||
pub(crate) fn new<'creator_node>(
|
||||
caller: Rc<WrappedCaller>,
|
||||
|
||||
arena_idx: ScopeId,
|
||||
|
||||
parent: Option<ScopeId>,
|
||||
|
||||
height: u32,
|
||||
|
||||
child_nodes: ScopeChildren,
|
||||
|
||||
vdom: SharedResources,
|
||||
shared: EventChannel,
|
||||
) -> Self {
|
||||
let child_nodes = unsafe { child_nodes.extend_lifetime() };
|
||||
|
||||
// insert ourself as a descendent of the parent
|
||||
// when the parent is removed, this map will be traversed, and we will also be cleaned up.
|
||||
if let Some(parent) = &parent {
|
||||
let parent = unsafe { vdom.get_scope(*parent) }.unwrap();
|
||||
parent.descendents.borrow_mut().insert(arena_idx);
|
||||
}
|
||||
let up = shared.schedule_any_immediate.clone();
|
||||
let memoized_updater = Rc::new(move || up(arena_idx));
|
||||
|
||||
Self {
|
||||
memoized_updater,
|
||||
shared,
|
||||
child_nodes,
|
||||
caller,
|
||||
parent_idx: parent,
|
||||
our_arena_idx: arena_idx,
|
||||
height,
|
||||
vdom,
|
||||
frames: ActiveFrame::new(),
|
||||
|
||||
hooks: Default::default(),
|
||||
suspended_nodes: Default::default(),
|
||||
shared_contexts: Default::default(),
|
||||
listeners: Default::default(),
|
||||
borrowed_props: Default::default(),
|
||||
|
@ -107,17 +100,18 @@ impl Scope {
|
|||
child_nodes: ScopeChildren,
|
||||
) {
|
||||
self.caller = caller;
|
||||
// let child_nodes = unsafe { std::mem::transmute(child_nodes) };
|
||||
let child_nodes = unsafe { child_nodes.extend_lifetime() };
|
||||
self.child_nodes = child_nodes;
|
||||
}
|
||||
|
||||
pub(crate) fn run_scope<'sel>(&'sel mut self) -> Result<()> {
|
||||
/// Returns true if the scope completed successfully
|
||||
///
|
||||
pub(crate) fn run_scope<'sel>(&'sel mut self, pool: &ResourcePool) -> bool {
|
||||
// Cycle to the next frame and then reset it
|
||||
// This breaks any latent references, invalidating every pointer referencing into it.
|
||||
// Remove all the outdated listeners
|
||||
|
||||
self.ensure_drop_safety();
|
||||
self.ensure_drop_safety(pool);
|
||||
|
||||
// Safety:
|
||||
// - We dropped the listeners, so no more &mut T can be used while these are held
|
||||
|
@ -132,17 +126,12 @@ impl Scope {
|
|||
let render: &WrappedCaller = self.caller.as_ref();
|
||||
|
||||
match render(self) {
|
||||
None => {
|
||||
// the user's component failed. We avoid cycling to the next frame
|
||||
log::error!("Running your component failed! It will no longer receive events.");
|
||||
Err(Error::ComponentFailed)
|
||||
}
|
||||
None => false,
|
||||
Some(new_head) => {
|
||||
// the user's component succeeded. We can safely cycle to the next frame
|
||||
self.frames.wip_frame_mut().head_node = unsafe { std::mem::transmute(new_head) };
|
||||
self.frames.cycle_frame();
|
||||
log::debug!("Cycle okay");
|
||||
Ok(())
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -156,26 +145,28 @@ impl Scope {
|
|||
///
|
||||
/// Refrences to hook data can only be stored in listeners and component props. During diffing, we make sure to log
|
||||
/// all listeners and borrowed props so we can clear them here.
|
||||
fn ensure_drop_safety(&mut self) {
|
||||
pub(crate) fn ensure_drop_safety(&mut self, pool: &ResourcePool) {
|
||||
// make sure all garabge is collected before trying to proceed with anything else
|
||||
debug_assert!(
|
||||
self.pending_garbage.borrow().is_empty(),
|
||||
"clean up your garabge please"
|
||||
);
|
||||
|
||||
// todo!("arch changes");
|
||||
|
||||
// make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we
|
||||
// run the hooks (which hold an &mut Referrence)
|
||||
// right now, we don't drop
|
||||
let vdom = &self.vdom;
|
||||
// let vdom = &self.vdom;
|
||||
self.borrowed_props
|
||||
.get_mut()
|
||||
.drain(..)
|
||||
.map(|li| unsafe { &*li })
|
||||
.for_each(|comp| {
|
||||
// First drop the component's undropped references
|
||||
let scope_id = comp.ass_scope.get().unwrap();
|
||||
let scope = unsafe { vdom.get_scope_mut(scope_id) }.unwrap();
|
||||
scope.ensure_drop_safety();
|
||||
let scope_id = comp.associated_scope.get().unwrap();
|
||||
let scope = pool.get_scope_mut(scope_id).unwrap();
|
||||
scope.ensure_drop_safety(pool);
|
||||
|
||||
// Now, drop our own reference
|
||||
let mut dropper = comp.drop_props.borrow_mut().take().unwrap();
|
||||
|
@ -193,51 +184,24 @@ impl Scope {
|
|||
}
|
||||
|
||||
// A safe wrapper around calling listeners
|
||||
// calling listeners will invalidate the list of listeners
|
||||
// The listener list will be completely drained because the next frame will write over previous listeners
|
||||
pub(crate) fn call_listener(&mut self, trigger: EventTrigger) -> Result<()> {
|
||||
let EventTrigger {
|
||||
real_node_id,
|
||||
event,
|
||||
..
|
||||
} = trigger;
|
||||
|
||||
if let &VirtualEvent::AsyncEvent { .. } = &event {
|
||||
log::info!("arrived a fiber event");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::debug!(
|
||||
"There are {:?} listeners associated with this scope {:#?}",
|
||||
self.listeners.borrow().len(),
|
||||
self.our_arena_idx
|
||||
);
|
||||
|
||||
//
|
||||
//
|
||||
pub(crate) fn call_listener(&mut self, event: SyntheticEvent, element: ElementId) {
|
||||
let listners = self.listeners.borrow_mut();
|
||||
|
||||
let raw_listener = listners.iter().find(|lis| {
|
||||
let search = unsafe { &***lis };
|
||||
let search_id = search.mounted_node.get();
|
||||
log::info!(
|
||||
"searching listener {:#?} for real {:?}",
|
||||
search_id,
|
||||
real_node_id
|
||||
);
|
||||
|
||||
match (real_node_id, search_id) {
|
||||
(Some(e), Some(search_id)) => search_id == e,
|
||||
_ => false,
|
||||
// this assumes the node might not be mounted - should we assume that though?
|
||||
match search_id.map(|f| f == element) {
|
||||
Some(same) => same,
|
||||
None => false,
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(raw_listener) = raw_listener {
|
||||
let listener = unsafe { &**raw_listener };
|
||||
|
||||
// log::info!(
|
||||
// "calling listener {:?}, {:?}",
|
||||
// listener.event,
|
||||
// // listener.scope
|
||||
// );
|
||||
let mut cb = listener.callback.borrow_mut();
|
||||
if let Some(cb) = cb.as_mut() {
|
||||
(cb)(event);
|
||||
|
@ -245,21 +209,35 @@ impl Scope {
|
|||
} else {
|
||||
log::warn!("An event was triggered but there was no listener to handle it");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn root(&self) -> &VNode {
|
||||
self.frames.fin_head()
|
||||
pub(crate) fn child_nodes<'a>(&'a self) -> ScopeChildren {
|
||||
unsafe { self.child_nodes.shorten_lifetime() }
|
||||
}
|
||||
|
||||
pub fn child_nodes<'a>(&'a self) -> ScopeChildren {
|
||||
unsafe { self.child_nodes.unextend_lfetime() }
|
||||
pub(crate) fn call_suspended_node<'a>(&'a self, task: u64) {
|
||||
let g = self.suspended_nodes.borrow_mut();
|
||||
|
||||
if let Some(suspended) = g.get(&task) {
|
||||
let sus: &'a VSuspended<'static> = unsafe { &**suspended };
|
||||
let sus: &'a VSuspended<'a> = unsafe { std::mem::transmute(sus) };
|
||||
|
||||
let bump = self.frames.wip_frame();
|
||||
let mut cb = sus.callback.borrow_mut();
|
||||
let mut _cb = cb.take().unwrap();
|
||||
let cx: SuspendedContext<'a> = SuspendedContext {
|
||||
inner: Context {
|
||||
props: &(),
|
||||
scope: &self,
|
||||
},
|
||||
};
|
||||
let n: DomTree<'a> = (_cb)(cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn consume_garbage(&self) -> Vec<&VNode> {
|
||||
let mut garbage = self.pending_garbage.borrow_mut();
|
||||
garbage
|
||||
pub(crate) fn consume_garbage(&self) -> Vec<&VNode> {
|
||||
self.pending_garbage
|
||||
.borrow_mut()
|
||||
.drain(..)
|
||||
.map(|node| {
|
||||
// safety: scopes cannot cycle without their garbage being collected. these nodes are safe
|
||||
|
@ -269,4 +247,8 @@ impl Scope {
|
|||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub fn root(&self) -> &VNode {
|
||||
self.frames.fin_head()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
//! Signas: Avoid the Diff Engine
|
||||
//! -----------------------------
|
||||
//!
|
||||
//!
|
||||
//! TODO!
|
82
packages/core/src/test_dom.rs
Normal file
82
packages/core/src/test_dom.rs
Normal file
|
@ -0,0 +1,82 @@
|
|||
//! A DOM for testing - both internal and external code.
|
||||
use bumpalo::Bump;
|
||||
|
||||
use crate::innerlude::*;
|
||||
use crate::nodes::IntoVNode;
|
||||
|
||||
pub struct TestDom {
|
||||
bump: Bump,
|
||||
scheduler: Scheduler,
|
||||
}
|
||||
|
||||
impl TestDom {
|
||||
pub fn new() -> TestDom {
|
||||
let bump = Bump::new();
|
||||
let mut scheduler = Scheduler::new();
|
||||
TestDom { bump, scheduler }
|
||||
}
|
||||
|
||||
pub fn new_factory<'a>(&'a self) -> NodeFactory<'a> {
|
||||
NodeFactory::new(&self.bump)
|
||||
}
|
||||
|
||||
pub fn render<'a, F>(&'a self, lazy_nodes: LazyNodes<'a, F>) -> VNode<'a>
|
||||
where
|
||||
F: FnOnce(NodeFactory<'a>) -> VNode<'a>,
|
||||
{
|
||||
lazy_nodes.into_vnode(NodeFactory::new(&self.bump))
|
||||
}
|
||||
|
||||
pub fn diff<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Mutations<'a> {
|
||||
let mutations = Mutations::new();
|
||||
let mut machine = DiffMachine::new(mutations, &self.scheduler.pool);
|
||||
machine.stack.push(DiffInstruction::DiffNode { new, old });
|
||||
machine.mutations
|
||||
}
|
||||
|
||||
pub fn create<'a, F1>(&'a self, left: LazyNodes<'a, F1>) -> Mutations<'a>
|
||||
where
|
||||
F1: FnOnce(NodeFactory<'a>) -> VNode<'a>,
|
||||
{
|
||||
let old = self.bump.alloc(self.render(left));
|
||||
|
||||
let mut machine = DiffMachine::new(Mutations::new(), &self.scheduler.pool);
|
||||
|
||||
machine.stack.create_node(old, MountType::Append);
|
||||
|
||||
machine.work(&mut || false);
|
||||
|
||||
machine.mutations
|
||||
}
|
||||
|
||||
pub fn lazy_diff<'a, F1, F2>(
|
||||
&'a self,
|
||||
left: LazyNodes<'a, F1>,
|
||||
right: LazyNodes<'a, F2>,
|
||||
) -> (Mutations<'a>, Mutations<'a>)
|
||||
where
|
||||
F1: FnOnce(NodeFactory<'a>) -> VNode<'a>,
|
||||
F2: FnOnce(NodeFactory<'a>) -> VNode<'a>,
|
||||
{
|
||||
let old = self.bump.alloc(self.render(left));
|
||||
|
||||
let new = self.bump.alloc(self.render(right));
|
||||
|
||||
let mut machine = DiffMachine::new(Mutations::new(), &self.scheduler.pool);
|
||||
|
||||
machine.stack.create_node(old, MountType::Append);
|
||||
|
||||
machine.work(&mut || false);
|
||||
let create_edits = machine.mutations;
|
||||
|
||||
let mut machine = DiffMachine::new(Mutations::new(), &self.scheduler.pool);
|
||||
|
||||
machine.stack.push(DiffInstruction::DiffNode { old, new });
|
||||
|
||||
machine.work(&mut || false);
|
||||
|
||||
let edits = machine.mutations;
|
||||
|
||||
(create_edits, edits)
|
||||
}
|
||||
}
|
|
@ -8,21 +8,59 @@ pub fn empty_cell() -> Cell<Option<ElementId>> {
|
|||
Cell::new(None)
|
||||
}
|
||||
|
||||
// /// A helper type that lets scopes be ordered by their height
|
||||
// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
// pub struct HeightMarker {
|
||||
// pub idx: ScopeId,
|
||||
// pub height: u32,
|
||||
// }
|
||||
pub fn type_name_of<T>(_: T) -> &'static str {
|
||||
std::any::type_name::<T>()
|
||||
}
|
||||
|
||||
// impl Ord for HeightMarker {
|
||||
// fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
// self.height.cmp(&other.height)
|
||||
// }
|
||||
// }
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
// impl PartialOrd for HeightMarker {
|
||||
// fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
// Some(self.cmp(other))
|
||||
// }
|
||||
// }
|
||||
// use crate::task::{Context, Poll};
|
||||
|
||||
/// Cooperatively gives up a timeslice to the task scheduler.
|
||||
///
|
||||
/// Calling this function will move the currently executing future to the back
|
||||
/// of the execution queue, making room for other futures to execute. This is
|
||||
/// especially useful after running CPU-intensive operations inside a future.
|
||||
///
|
||||
/// See also [`task::spawn_blocking`].
|
||||
///
|
||||
/// [`task::spawn_blocking`]: fn.spawn_blocking.html
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Basic usage:
|
||||
///
|
||||
/// ```
|
||||
/// # async_std::task::block_on(async {
|
||||
/// #
|
||||
/// use async_std::task;
|
||||
///
|
||||
/// task::yield_now().await;
|
||||
/// #
|
||||
/// # })
|
||||
/// ```
|
||||
#[inline]
|
||||
pub async fn yield_now() {
|
||||
YieldNow(false).await
|
||||
}
|
||||
|
||||
struct YieldNow(bool);
|
||||
|
||||
impl Future for YieldNow {
|
||||
type Output = ();
|
||||
|
||||
// The futures executor is implemented as a FIFO queue, so all this future
|
||||
// does is re-schedule the future back to the end of the queue, giving room
|
||||
// for other futures to progress.
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
if !self.0 {
|
||||
self.0 = true;
|
||||
cx.waker().wake_by_ref();
|
||||
Poll::Pending
|
||||
} else {
|
||||
Poll::Ready(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,23 +14,17 @@
|
|||
//! - The [`ActiveFrame`] object for managing the Scope`s microheap
|
||||
//! - The [`Context`] object for exposing VirtualDOM API to components
|
||||
//! - The [`NodeFactory`] object for lazyily exposing the `Context` API to the nodebuilder API
|
||||
//! - The [`Hook`] object for exposing state management in components.
|
||||
//!
|
||||
//! This module includes just the barebones for a complete VirtualDOM API.
|
||||
//! Additional functionality is defined in the respective files.
|
||||
#![allow(unreachable_code)]
|
||||
use futures_util::StreamExt;
|
||||
use fxhash::FxHashMap;
|
||||
|
||||
use crate::hooks::{SuspendedContext, SuspenseHook};
|
||||
use crate::{arena::SharedResources, innerlude::*};
|
||||
|
||||
use std::any::Any;
|
||||
|
||||
use std::any::TypeId;
|
||||
use std::cell::{Ref, RefCell, RefMut};
|
||||
use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashSet};
|
||||
use std::pin::Pin;
|
||||
use crate::innerlude::*;
|
||||
use futures_util::{Future, FutureExt};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
/// An integrated virtual node system that progresses events and diffs UI trees.
|
||||
/// Differences are converted into patches which a renderer can use to draw the UI.
|
||||
|
@ -42,150 +36,146 @@ use std::pin::Pin;
|
|||
///
|
||||
///
|
||||
pub struct VirtualDom {
|
||||
/// All mounted components are arena allocated to make additions, removals, and references easy to work with
|
||||
/// A generational arena is used to re-use slots of deleted scopes without having to resize the underlying arena.
|
||||
///
|
||||
/// This is wrapped in an UnsafeCell because we will need to get mutable access to unique values in unique bump arenas
|
||||
/// and rusts's guartnees cannot prove that this is safe. We will need to maintain the safety guarantees manually.
|
||||
pub shared: SharedResources,
|
||||
scheduler: Scheduler,
|
||||
|
||||
/// The index of the root component
|
||||
/// Should always be the first (gen=0, id=0)
|
||||
pub base_scope: ScopeId,
|
||||
base_scope: ScopeId,
|
||||
|
||||
active_fibers: Vec<Fiber<'static>>,
|
||||
root_fc: Box<dyn Any>,
|
||||
|
||||
// for managing the props that were used to create the dom
|
||||
#[doc(hidden)]
|
||||
_root_prop_type: std::any::TypeId,
|
||||
|
||||
#[doc(hidden)]
|
||||
_root_props: std::pin::Pin<Box<dyn std::any::Any>>,
|
||||
root_props: Pin<Box<dyn std::any::Any>>,
|
||||
}
|
||||
|
||||
impl VirtualDom {
|
||||
/// Create a new instance of the Dioxus Virtual Dom with no properties for the root component.
|
||||
/// Create a new VirtualDOM with a component that does not have special props.
|
||||
///
|
||||
/// This means that the root component must either consumes its own context, or statics are used to generate the page.
|
||||
/// The root component can access things like routing in its context.
|
||||
/// # Description
|
||||
///
|
||||
/// As an end-user, you'll want to use the Renderer's "new" method instead of this method.
|
||||
/// Directly creating the VirtualDOM is only useful when implementing a new renderer.
|
||||
///
|
||||
///
|
||||
/// ```ignore
|
||||
/// // Directly from a closure
|
||||
///
|
||||
/// let dom = VirtualDom::new(|cx| cx.render(rsx!{ div {"hello world"} }));
|
||||
///
|
||||
/// // or pass in...
|
||||
///
|
||||
/// let root = |cx| {
|
||||
/// cx.render(rsx!{
|
||||
/// div {"hello world"}
|
||||
/// })
|
||||
/// }
|
||||
/// let dom = VirtualDom::new(root);
|
||||
///
|
||||
/// // or directly from a fn
|
||||
///
|
||||
/// fn Example(cx: Context<()>) -> DomTree {
|
||||
/// cx.render(rsx!{ div{"hello world"} })
|
||||
/// }
|
||||
///
|
||||
/// let dom = VirtualDom::new(Example);
|
||||
/// ```
|
||||
pub fn new(root: FC<()>) -> Self {
|
||||
Self::new_with_props(root, ())
|
||||
}
|
||||
|
||||
/// Start a new VirtualDom instance with a dependent cx.
|
||||
/// Later, the props can be updated by calling "update" with a new set of props, causing a set of re-renders.
|
||||
///
|
||||
/// This is useful when a component tree can be driven by external state (IE SSR) but it would be too expensive
|
||||
/// to toss out the entire tree.
|
||||
///
|
||||
/// ```ignore
|
||||
/// // Directly from a closure
|
||||
///
|
||||
/// let dom = VirtualDom::new(|cx| cx.render(rsx!{ div {"hello world"} }));
|
||||
///
|
||||
/// // or pass in...
|
||||
///
|
||||
/// let root = |cx| {
|
||||
/// cx.render(rsx!{
|
||||
/// div {"hello world"}
|
||||
/// })
|
||||
/// }
|
||||
/// let dom = VirtualDom::new(root);
|
||||
///
|
||||
/// // or directly from a fn
|
||||
///
|
||||
/// fn Example(cx: Context, props: &SomeProps) -> VNode {
|
||||
/// cx.render(rsx!{ div{"hello world"} })
|
||||
/// # Example
|
||||
/// ```
|
||||
/// fn Example(cx: Context<()>) -> DomTree {
|
||||
/// cx.render(rsx!( div { "hello world" } ))
|
||||
/// }
|
||||
///
|
||||
/// let dom = VirtualDom::new(Example);
|
||||
/// ```
|
||||
///
|
||||
/// Note: the VirtualDOM is not progressed, you must either "run_with_deadline" or use "rebuild" to progress it.
|
||||
pub fn new(root: FC<()>) -> Self {
|
||||
Self::new_with_props(root, ())
|
||||
}
|
||||
|
||||
/// Create a new VirtualDOM with the given properties for the root component.
|
||||
///
|
||||
/// # Description
|
||||
///
|
||||
/// Later, the props can be updated by calling "update" with a new set of props, causing a set of re-renders.
|
||||
///
|
||||
/// This is useful when a component tree can be driven by external state (IE SSR) but it would be too expensive
|
||||
/// to toss out the entire tree.
|
||||
///
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// #[derive(PartialEq, Props)]
|
||||
/// struct SomeProps {
|
||||
/// name: &'static str
|
||||
/// }
|
||||
///
|
||||
/// fn Example(cx: Context<SomeProps>) -> DomTree {
|
||||
/// cx.render(rsx!{ div{ "hello {cx.name}" } })
|
||||
/// }
|
||||
///
|
||||
/// let dom = VirtualDom::new(Example);
|
||||
/// ```
|
||||
///
|
||||
/// Note: the VirtualDOM is not progressed on creation. You must either "run_with_deadline" or use "rebuild" to progress it.
|
||||
///
|
||||
/// ```rust
|
||||
/// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" });
|
||||
/// let mutations = dom.rebuild();
|
||||
/// ```
|
||||
pub fn new_with_props<P: Properties + 'static>(root: FC<P>, root_props: P) -> Self {
|
||||
let components = SharedResources::new();
|
||||
let scheduler = Scheduler::new();
|
||||
|
||||
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;
|
||||
let props_ptr = root_props.downcast_ref::<P>().unwrap() as *const P;
|
||||
|
||||
let link = components.clone();
|
||||
|
||||
let base_scope = components.insert_scope_with_key(move |myidx| {
|
||||
let base_scope = scheduler.pool.insert_scope_with_key(|myidx| {
|
||||
let caller = NodeFactory::create_component_caller(root, props_ptr as *const _);
|
||||
Scope::new(caller, myidx, None, 0, ScopeChildren(&[]), link)
|
||||
Scope::new(
|
||||
caller,
|
||||
myidx,
|
||||
None,
|
||||
0,
|
||||
ScopeChildren(&[]),
|
||||
scheduler.pool.channel.clone(),
|
||||
)
|
||||
});
|
||||
|
||||
Self {
|
||||
root_fc: Box::new(root),
|
||||
base_scope,
|
||||
_root_props: root_props,
|
||||
shared: components,
|
||||
active_fibers: Vec::new(),
|
||||
_root_prop_type: TypeId::of::<P>(),
|
||||
scheduler,
|
||||
root_props,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn launch_in_place(root: FC<()>) -> Self {
|
||||
let mut s = Self::new(root);
|
||||
s.rebuild_in_place().unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
/// Creates a new virtualdom and immediately rebuilds it in place, not caring about the RealDom to write into.
|
||||
/// Get the [`Scope`] for the root component.
|
||||
///
|
||||
pub fn launch_with_props_in_place<P: Properties + 'static>(root: FC<P>, root_props: P) -> Self {
|
||||
let mut s = Self::new_with_props(root, root_props);
|
||||
s.rebuild_in_place().unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
/// This is useful for traversing the tree from the root for heuristics or altnerative renderers that use Dioxus
|
||||
/// directly.
|
||||
pub fn base_scope(&self) -> &Scope {
|
||||
unsafe { self.shared.get_scope(self.base_scope).unwrap() }
|
||||
self.scheduler.pool.get_scope(self.base_scope).unwrap()
|
||||
}
|
||||
|
||||
/// Get the [`Scope`] for a component given its [`ScopeId`]
|
||||
pub fn get_scope(&self, id: ScopeId) -> Option<&Scope> {
|
||||
unsafe { self.shared.get_scope(id) }
|
||||
self.scheduler.pool.get_scope(id)
|
||||
}
|
||||
|
||||
/// Rebuilds the VirtualDOM from scratch, but uses a "dummy" RealDom.
|
||||
/// Update the root props of this VirtualDOM.
|
||||
///
|
||||
/// Used in contexts where a real copy of the structure doesn't matter, and the VirtualDOM is the source of truth.
|
||||
/// This method retuns None if the old props could not be removed. The entire VirtualDOM will be rebuilt immediately,
|
||||
/// so calling this method will block the main thread until computation is done.
|
||||
///
|
||||
/// ## Why?
|
||||
/// ## Example
|
||||
///
|
||||
/// This method uses the `DebugDom` under the hood - essentially making the VirtualDOM's diffing patches a "no-op".
|
||||
/// ```rust
|
||||
/// #[derive(Props, PartialEq)]
|
||||
/// struct AppProps {
|
||||
/// route: &'static str
|
||||
/// }
|
||||
/// static App: FC<AppProps> = |cx| cx.render(rsx!{ "route is {cx.route}" });
|
||||
///
|
||||
/// 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<Vec<DomEdit>> {
|
||||
todo!();
|
||||
// let mut realdom = DebugDom::new();
|
||||
// let mut edits = Vec::new();
|
||||
// self.rebuild(&mut realdom, &mut edits)?;
|
||||
// Ok(edits)
|
||||
/// let mut dom = VirtualDom::new_with_props(App, AppProps { route: "start" });
|
||||
///
|
||||
/// let mutations = dom.update_root_props(AppProps { route: "end" }).unwrap();
|
||||
/// ```
|
||||
pub fn update_root_props<'s, P: 'static>(&'s mut self, root_props: P) -> Option<Mutations<'s>> {
|
||||
let root_scope = self.scheduler.pool.get_scope_mut(self.base_scope).unwrap();
|
||||
root_scope.ensure_drop_safety(&self.scheduler.pool);
|
||||
|
||||
let mut root_props: Pin<Box<dyn Any>> = Box::pin(root_props);
|
||||
|
||||
if let Some(props_ptr) = root_props.downcast_ref::<P>().map(|p| p as *const P) {
|
||||
std::mem::swap(&mut self.root_props, &mut root_props);
|
||||
|
||||
let root = *self.root_fc.downcast_ref::<FC<P>>().unwrap();
|
||||
|
||||
let new_caller = NodeFactory::create_component_caller(root, props_ptr as *const _);
|
||||
|
||||
root_scope.update_scope_dependencies(new_caller, ScopeChildren(&[]));
|
||||
|
||||
Some(self.rebuild())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom rom scratch
|
||||
|
@ -193,21 +183,40 @@ impl VirtualDom {
|
|||
/// The diff machine expects the RealDom's stack to be the root of the application
|
||||
///
|
||||
/// Events like garabge collection, application of refs, etc are not handled by this method and can only be progressed
|
||||
/// through "run"
|
||||
///
|
||||
pub fn rebuild<'s>(&'s mut self) -> Result<Vec<DomEdit<'s>>> {
|
||||
let mut edits = Vec::new();
|
||||
let mutations = Mutations { edits: Vec::new() };
|
||||
let mut diff_machine = DiffMachine::new(mutations, self.base_scope, &self.shared);
|
||||
/// through "run". We completely avoid the task scheduler infrastructure.
|
||||
pub fn rebuild<'s>(&'s mut self) -> Mutations<'s> {
|
||||
let mut fut = self.rebuild_async().boxed_local();
|
||||
|
||||
let cur_component = diff_machine
|
||||
.get_scope_mut(&self.base_scope)
|
||||
loop {
|
||||
if let Some(edits) = (&mut fut).now_or_never() {
|
||||
break edits;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rebuild the dom from the ground up
|
||||
///
|
||||
/// This method is asynchronous to prevent the application from blocking while the dom is being rebuilt. Computing
|
||||
/// the diff and creating nodes can be expensive, so we provide this method to avoid blocking the main thread. This
|
||||
/// method can be useful when needing to perform some crucial periodic tasks.
|
||||
pub async fn rebuild_async<'s>(&'s mut self) -> Mutations<'s> {
|
||||
let mut shared = self.scheduler.pool.clone();
|
||||
let mut diff_machine = DiffMachine::new(Mutations::new(), &mut shared);
|
||||
|
||||
let cur_component = self
|
||||
.scheduler
|
||||
.pool
|
||||
.get_scope_mut(self.base_scope)
|
||||
.expect("The base scope should never be moved");
|
||||
|
||||
// We run the component. If it succeeds, then we can diff it and add the changes to the dom.
|
||||
if cur_component.run_scope().is_ok() {
|
||||
let meta = diff_machine.create_vnode(cur_component.frames.fin_head());
|
||||
diff_machine.edit_append_children(meta.added_to_stack);
|
||||
if cur_component.run_scope(&self.scheduler.pool) {
|
||||
diff_machine
|
||||
.stack
|
||||
.create_node(cur_component.frames.fin_head(), MountType::Append);
|
||||
diff_machine.stack.scope_stack.push(self.base_scope);
|
||||
|
||||
// let completed = diff_machine.work();
|
||||
} else {
|
||||
// todo: should this be a hard error?
|
||||
log::warn!(
|
||||
|
@ -216,65 +225,42 @@ impl VirtualDom {
|
|||
);
|
||||
}
|
||||
|
||||
Ok(edits)
|
||||
unsafe { std::mem::transmute(diff_machine.mutations) }
|
||||
}
|
||||
|
||||
// async fn select_next_event(&mut self) -> Option<EventTrigger> {
|
||||
// let mut receiver = self.shared.task_receiver.borrow_mut();
|
||||
/// Compute a manual diff of the VirtualDOM between states.
|
||||
///
|
||||
/// This can be useful when state inside the DOM is remotely changed from the outside, but not propogated as an event.
|
||||
pub fn diff<'s>(&'s mut self) -> Mutations<'s> {
|
||||
let cur_component = self
|
||||
.scheduler
|
||||
.pool
|
||||
.get_scope_mut(self.base_scope)
|
||||
.expect("The base scope should never be moved");
|
||||
|
||||
// // drain the in-flight events so that we can sort them out with the current events
|
||||
// while let Ok(Some(trigger)) = receiver.try_next() {
|
||||
// log::info!("retrieving event from receiver");
|
||||
// let key = self.shared.make_trigger_key(&trigger);
|
||||
// self.pending_events.insert(key, trigger);
|
||||
// }
|
||||
|
||||
// if self.pending_events.is_empty() {
|
||||
// // Continuously poll the future pool and the event receiver for work
|
||||
// let mut tasks = self.shared.async_tasks.borrow_mut();
|
||||
// let tasks_tasks = tasks.next();
|
||||
|
||||
// let mut receiver = self.shared.task_receiver.borrow_mut();
|
||||
// let reciv_task = receiver.next();
|
||||
|
||||
// futures_util::pin_mut!(tasks_tasks);
|
||||
// futures_util::pin_mut!(reciv_task);
|
||||
|
||||
// let trigger = match futures_util::future::select(tasks_tasks, reciv_task).await {
|
||||
// futures_util::future::Either::Left((trigger, _)) => trigger,
|
||||
// futures_util::future::Either::Right((trigger, _)) => trigger,
|
||||
// }
|
||||
// .unwrap();
|
||||
// let key = self.shared.make_trigger_key(&trigger);
|
||||
// self.pending_events.insert(key, trigger);
|
||||
// }
|
||||
|
||||
// // pop the most important event off
|
||||
// let key = self.pending_events.keys().next().unwrap().clone();
|
||||
// let trigger = self.pending_events.remove(&key).unwrap();
|
||||
|
||||
// Some(trigger)
|
||||
// }
|
||||
if cur_component.run_scope(&self.scheduler.pool) {
|
||||
let mut diff_machine: DiffMachine<'s> =
|
||||
DiffMachine::new(Mutations::new(), &mut self.scheduler.pool);
|
||||
diff_machine.diff_scope(self.base_scope);
|
||||
diff_machine.mutations
|
||||
} else {
|
||||
Mutations::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs the virtualdom immediately, not waiting for any suspended nodes to complete.
|
||||
///
|
||||
/// This method will not wait for any suspended tasks, completely skipping over
|
||||
pub fn run_immediate<'s>(&'s mut self) -> Result<Mutations<'s>> {
|
||||
//
|
||||
|
||||
todo!()
|
||||
/// This method will not wait for any suspended nodes to complete. If there is no pending work, then this method will
|
||||
/// return "None"
|
||||
pub fn run_immediate<'s>(&'s mut self) -> Option<Vec<Mutations<'s>>> {
|
||||
if self.scheduler.has_any_work() {
|
||||
Some(self.scheduler.work_sync())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs the virtualdom with no time limit.
|
||||
///
|
||||
/// If there are pending tasks, they will be progressed before returning. This is useful when rendering an application
|
||||
/// that has suspended nodes or suspended tasks. Be warned - any async tasks running forever will prevent this method
|
||||
/// from completing. Consider using `run` and specifing a deadline.
|
||||
pub async fn run_unbounded<'s>(&'s mut self) -> Result<Mutations<'s>> {
|
||||
self.run_with_deadline(|| false).await
|
||||
}
|
||||
|
||||
/// Run the virtualdom with a time limit.
|
||||
/// Run the virtualdom with a deadline.
|
||||
///
|
||||
/// This method will progress async tasks until the deadline is reached. If tasks are completed before the deadline,
|
||||
/// and no tasks are pending, this method will return immediately. If tasks are still pending, then this method will
|
||||
|
@ -283,189 +269,59 @@ impl VirtualDom {
|
|||
/// This method is useful when needing to schedule the virtualdom around other tasks on the main thread to prevent
|
||||
/// "jank". It will try to finish whatever work it has by the deadline to free up time for other work.
|
||||
///
|
||||
/// Due to platform differences in how time is handled, this method accepts a closure that must return true when the
|
||||
/// deadline is exceeded. However, the deadline won't be met precisely, so you might want to build some wiggle room
|
||||
/// into the deadline closure manually.
|
||||
/// Due to platform differences in how time is handled, this method accepts a future that resolves when the deadline
|
||||
/// is exceeded. However, the deadline won't be met precisely, so you might want to build some wiggle room into the
|
||||
/// deadline closure manually.
|
||||
///
|
||||
/// The deadline is checked before starting to diff components. This strikes a balance between the overhead of checking
|
||||
/// The deadline is polled before starting to diff components. This strikes a balance between the overhead of checking
|
||||
/// the deadline and just completing the work. However, if an individual component takes more than 16ms to render, then
|
||||
/// the screen will "jank" up. In debug, this will trigger an alert.
|
||||
///
|
||||
/// If there are no in-flight fibers when this method is called, it will await any possible tasks, aborting early if
|
||||
/// the provided deadline future resolves.
|
||||
///
|
||||
/// For use in the web, it is expected that this method will be called to be executed during "idle times" and the
|
||||
/// mutations to be applied during the "paint times" IE "animation frames". With this strategy, it is possible to craft
|
||||
/// entirely jank-free applications that perform a ton of work.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// let mut dom = VirtualDom::new(|cx| cx.render(rsx!( div {"hello"} )));
|
||||
/// static App: FC<()> = |cx| rsx!(in cx, div {"hello"} );
|
||||
/// let mut dom = VirtualDom::new(App);
|
||||
/// loop {
|
||||
/// let started = std::time::Instant::now();
|
||||
/// let deadline = move || std::time::Instant::now() - started > std::time::Duration::from_millis(16);
|
||||
///
|
||||
/// let deadline = TimeoutFuture::from_ms(16);
|
||||
/// let mutations = dom.run_with_deadline(deadline).await;
|
||||
/// apply_mutations(mutations);
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Mutations
|
||||
///
|
||||
/// This method returns "mutations" - IE the necessary changes to get the RealDOM to match the VirtualDOM. It also
|
||||
/// includes a list of NodeRefs that need to be applied and effects that need to be triggered after the RealDOM has
|
||||
/// applied the edits.
|
||||
///
|
||||
/// Mutations are the only link between the RealDOM and the VirtualDOM.
|
||||
pub async fn run_with_deadline<'s>(
|
||||
&'s mut self,
|
||||
mut deadline_exceeded: impl FnMut() -> bool,
|
||||
) -> Result<Mutations<'s>> {
|
||||
let cur_component = self.base_scope;
|
||||
|
||||
let mut diff_machine =
|
||||
DiffMachine::new(Mutations { edits: Vec::new() }, cur_component, &self.shared);
|
||||
|
||||
/*
|
||||
Strategy:
|
||||
1. Check if there are any events in the receiver.
|
||||
2. If there are, process them and create a new fiber.
|
||||
3. If there are no events, then choose a fiber to work on.
|
||||
4. If there are no fibers, then wait for the next event from the receiver.
|
||||
5. While processing a fiber, periodically check if we're out of time
|
||||
6. If we are almost out of time, then commit our edits to the realdom
|
||||
7. Whenever a fiber is finished, immediately commit it. (IE so deadlines can be infinite if unsupported)
|
||||
*/
|
||||
|
||||
// 1. Consume any pending events and create new fibers
|
||||
let mut receiver = self.shared.task_receiver.borrow_mut();
|
||||
while let Ok(Some(trigger)) = receiver.try_next() {
|
||||
// todo: cache the fibers
|
||||
let mut fiber = Fiber::new();
|
||||
|
||||
match &trigger.event {
|
||||
// If any input event is received, then we need to create a new fiber
|
||||
VirtualEvent::ClipboardEvent(_)
|
||||
| VirtualEvent::CompositionEvent(_)
|
||||
| VirtualEvent::KeyboardEvent(_)
|
||||
| VirtualEvent::FocusEvent(_)
|
||||
| VirtualEvent::FormEvent(_)
|
||||
| VirtualEvent::SelectionEvent(_)
|
||||
| VirtualEvent::TouchEvent(_)
|
||||
| VirtualEvent::UIEvent(_)
|
||||
| VirtualEvent::WheelEvent(_)
|
||||
| VirtualEvent::MediaEvent(_)
|
||||
| VirtualEvent::AnimationEvent(_)
|
||||
| VirtualEvent::TransitionEvent(_)
|
||||
| VirtualEvent::ToggleEvent(_)
|
||||
| VirtualEvent::MouseEvent(_)
|
||||
| VirtualEvent::PointerEvent(_) => {
|
||||
if let Some(scope) = self.shared.get_scope_mut(trigger.originator) {
|
||||
scope.call_listener(trigger)?;
|
||||
}
|
||||
}
|
||||
|
||||
VirtualEvent::AsyncEvent { .. } => {
|
||||
while let Ok(Some(event)) = receiver.try_next() {
|
||||
fiber.pending_scopes.push(event.originator);
|
||||
}
|
||||
}
|
||||
|
||||
// These shouldn't normally be received, but if they are, it's done because some task set state manually
|
||||
// Instead of batching the results,
|
||||
VirtualEvent::ScheduledUpdate { height: u32 } => {}
|
||||
|
||||
// Suspense Events! A component's suspended node is updated
|
||||
VirtualEvent::SuspenseEvent { hook_idx, domnode } => {
|
||||
// Safety: this handler is the only thing that can mutate shared items at this moment in tim
|
||||
let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap();
|
||||
|
||||
// safety: we are sure that there are no other references to the inner content of suspense hooks
|
||||
let hook = unsafe { scope.hooks.get_mut::<SuspenseHook>(*hook_idx) }.unwrap();
|
||||
|
||||
let cx = Context { scope, props: &() };
|
||||
let scx = SuspendedContext { inner: cx };
|
||||
|
||||
// generate the new node!
|
||||
let nodes: Option<VNode> = (&hook.callback)(scx);
|
||||
match nodes {
|
||||
None => {
|
||||
log::warn!(
|
||||
"Suspense event came through, but there were no generated nodes >:(."
|
||||
);
|
||||
}
|
||||
Some(nodes) => {
|
||||
// allocate inside the finished frame - not the WIP frame
|
||||
let nodes = scope.frames.finished_frame().bump.alloc(nodes);
|
||||
|
||||
// push the old node's root onto the stack
|
||||
let real_id = domnode.get().ok_or(Error::NotMounted)?;
|
||||
diff_machine.edit_push_root(real_id);
|
||||
|
||||
// push these new nodes onto the diff machines stack
|
||||
let meta = diff_machine.create_vnode(&*nodes);
|
||||
|
||||
// replace the placeholder with the new nodes we just pushed on the stack
|
||||
diff_machine.edit_replace_with(1, meta.added_to_stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collecting garabge is not currently interruptible.
|
||||
//
|
||||
// In the future, it could be though
|
||||
VirtualEvent::GarbageCollection => {
|
||||
let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap();
|
||||
|
||||
let mut garbage_list = scope.consume_garbage();
|
||||
|
||||
let mut scopes_to_kill = Vec::new();
|
||||
while let Some(node) = garbage_list.pop() {
|
||||
match &node.kind {
|
||||
VNodeKind::Text(_) => {
|
||||
self.shared.collect_garbage(node.direct_id());
|
||||
}
|
||||
VNodeKind::Anchor(_) => {
|
||||
self.shared.collect_garbage(node.direct_id());
|
||||
}
|
||||
VNodeKind::Suspended(_) => {
|
||||
self.shared.collect_garbage(node.direct_id());
|
||||
}
|
||||
|
||||
VNodeKind::Element(el) => {
|
||||
self.shared.collect_garbage(node.direct_id());
|
||||
for child in el.children {
|
||||
garbage_list.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
VNodeKind::Fragment(frag) => {
|
||||
for child in frag.children {
|
||||
garbage_list.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
VNodeKind::Component(comp) => {
|
||||
// TODO: run the hook destructors and then even delete the scope
|
||||
|
||||
let scope_id = comp.ass_scope.get().unwrap();
|
||||
let scope = self.get_scope(scope_id).unwrap();
|
||||
let root = scope.root();
|
||||
garbage_list.push(root);
|
||||
scopes_to_kill.push(scope_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for scope in scopes_to_kill {
|
||||
// oy kill em
|
||||
log::debug!("should be removing scope {:#?}", scope);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while !deadline_exceeded() {
|
||||
let mut receiver = self.shared.task_receiver.borrow_mut();
|
||||
|
||||
// no messages to receive, just work on the fiber
|
||||
}
|
||||
|
||||
Ok(diff_machine.edits)
|
||||
deadline: impl Future<Output = ()>,
|
||||
) -> Vec<Mutations<'s>> {
|
||||
let mut deadline = Box::pin(deadline.fuse());
|
||||
self.scheduler.work_with_deadline(deadline).await
|
||||
}
|
||||
|
||||
pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender<EventTrigger> {
|
||||
self.shared.task_sender.clone()
|
||||
pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender<SchedulerMsg> {
|
||||
self.scheduler.pool.channel.sender.clone()
|
||||
}
|
||||
|
||||
fn get_scope_mut(&mut self, id: ScopeId) -> Option<&mut Scope> {
|
||||
unsafe { self.shared.get_scope_mut(id) }
|
||||
pub fn has_work(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub async fn wait_for_any_work(&mut self) {
|
||||
let mut timeout = Box::pin(futures_util::future::pending().fuse());
|
||||
self.scheduler.wait_for_any_trigger(&mut timeout).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -473,36 +329,3 @@ impl VirtualDom {
|
|||
// These impls are actually wrong. The DOM needs to have a mutex implemented.
|
||||
unsafe impl Sync for VirtualDom {}
|
||||
unsafe impl Send for VirtualDom {}
|
||||
|
||||
struct Fiber<'a> {
|
||||
// scopes that haven't been updated yet
|
||||
pending_scopes: Vec<ScopeId>,
|
||||
|
||||
pending_nodes: Vec<*const VNode<'a>>,
|
||||
|
||||
// WIP edits
|
||||
edits: Vec<DomEdit<'a>>,
|
||||
|
||||
started: bool,
|
||||
|
||||
completed: bool,
|
||||
}
|
||||
|
||||
impl Fiber<'_> {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
pending_scopes: Vec::new(),
|
||||
pending_nodes: Vec::new(),
|
||||
edits: Vec::new(),
|
||||
started: false,
|
||||
completed: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The "Mutations" object holds the changes that need to be made to the DOM.
|
||||
pub struct Mutations<'s> {
|
||||
// todo: apply node refs
|
||||
// todo: apply effects
|
||||
pub edits: Vec<DomEdit<'s>>,
|
||||
}
|
||||
|
|
263
packages/core/tests/create_iterative.rs
Normal file
263
packages/core/tests/create_iterative.rs
Normal file
|
@ -0,0 +1,263 @@
|
|||
//! tests to prove that the iterative implementation works
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use dioxus::{prelude::*, DomEdit, Mutations};
|
||||
mod test_logging;
|
||||
use dioxus_core as dioxus;
|
||||
use dioxus_html as dioxus_elements;
|
||||
use DomEdit::*;
|
||||
|
||||
const LOGGING_ENABLED: bool = false;
|
||||
|
||||
#[test]
|
||||
fn test_original_diff() {
|
||||
static App: FC<()> = |cx| {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
div {
|
||||
"Hello, world!"
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let mut dom = VirtualDom::new(App);
|
||||
let mutations = dom.rebuild();
|
||||
assert_eq!(
|
||||
mutations.edits,
|
||||
[
|
||||
CreateElement { id: 0, tag: "div" },
|
||||
CreateElement { id: 1, tag: "div" },
|
||||
CreateTextNode {
|
||||
id: 2,
|
||||
text: "Hello, world!"
|
||||
},
|
||||
AppendChildren { many: 1 },
|
||||
AppendChildren { many: 1 },
|
||||
AppendChildren { many: 1 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn create() {
|
||||
static App: FC<()> = |cx| {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
div {
|
||||
"Hello, world!"
|
||||
div {
|
||||
div {
|
||||
Fragment {
|
||||
"hello"
|
||||
"world"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
test_logging::set_up_logging(LOGGING_ENABLED);
|
||||
let mut dom = VirtualDom::new(App);
|
||||
let mutations = dom.rebuild_async().await;
|
||||
assert_eq!(
|
||||
mutations.edits,
|
||||
[
|
||||
CreateElement { id: 0, tag: "div" },
|
||||
CreateElement { id: 1, tag: "div" },
|
||||
CreateTextNode {
|
||||
id: 2,
|
||||
text: "Hello, world!"
|
||||
},
|
||||
CreateElement { id: 3, tag: "div" },
|
||||
CreateElement { id: 4, tag: "div" },
|
||||
CreateTextNode {
|
||||
id: 5,
|
||||
text: "hello"
|
||||
},
|
||||
CreateTextNode {
|
||||
id: 6,
|
||||
text: "world"
|
||||
},
|
||||
AppendChildren { many: 2 },
|
||||
AppendChildren { many: 1 },
|
||||
AppendChildren { many: 2 },
|
||||
AppendChildren { many: 1 },
|
||||
AppendChildren { many: 1 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn create_list() {
|
||||
static App: FC<()> = |cx| {
|
||||
cx.render(rsx! {
|
||||
{(0..3).map(|f| rsx!{ div {
|
||||
"hello"
|
||||
}})}
|
||||
})
|
||||
};
|
||||
|
||||
test_logging::set_up_logging(LOGGING_ENABLED);
|
||||
|
||||
let mut dom = VirtualDom::new(App);
|
||||
let mutations = dom.rebuild_async().await;
|
||||
|
||||
// copilot wrote this test :P
|
||||
assert_eq!(
|
||||
mutations.edits,
|
||||
[
|
||||
CreateElement { id: 0, tag: "div" },
|
||||
CreateTextNode {
|
||||
id: 1,
|
||||
text: "hello"
|
||||
},
|
||||
AppendChildren { many: 1 },
|
||||
CreateElement { id: 2, tag: "div" },
|
||||
CreateTextNode {
|
||||
id: 3,
|
||||
text: "hello"
|
||||
},
|
||||
AppendChildren { many: 1 },
|
||||
CreateElement { id: 4, tag: "div" },
|
||||
CreateTextNode {
|
||||
id: 5,
|
||||
text: "hello"
|
||||
},
|
||||
AppendChildren { many: 1 },
|
||||
AppendChildren { many: 3 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn create_simple() {
|
||||
static App: FC<()> = |cx| {
|
||||
cx.render(rsx! {
|
||||
div {}
|
||||
div {}
|
||||
div {}
|
||||
div {}
|
||||
})
|
||||
};
|
||||
|
||||
test_logging::set_up_logging(LOGGING_ENABLED);
|
||||
|
||||
let mut dom = VirtualDom::new(App);
|
||||
let mutations = dom.rebuild_async().await;
|
||||
|
||||
// copilot wrote this test :P
|
||||
assert_eq!(
|
||||
mutations.edits,
|
||||
[
|
||||
CreateElement { id: 0, tag: "div" },
|
||||
CreateElement { id: 1, tag: "div" },
|
||||
CreateElement { id: 2, tag: "div" },
|
||||
CreateElement { id: 3, tag: "div" },
|
||||
AppendChildren { many: 4 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn create_components() {
|
||||
static App: FC<()> = |cx| {
|
||||
cx.render(rsx! {
|
||||
Child { "abc1" }
|
||||
Child { "abc2" }
|
||||
Child { "abc3" }
|
||||
})
|
||||
};
|
||||
|
||||
static Child: FC<()> = |cx| {
|
||||
cx.render(rsx! {
|
||||
h1 {}
|
||||
div { {cx.children()} }
|
||||
p {}
|
||||
})
|
||||
};
|
||||
|
||||
test_logging::set_up_logging(LOGGING_ENABLED);
|
||||
|
||||
let mut dom = VirtualDom::new(App);
|
||||
let mutations = dom.rebuild_async().await;
|
||||
|
||||
assert_eq!(
|
||||
mutations.edits,
|
||||
[
|
||||
CreateElement { id: 0, tag: "h1" },
|
||||
CreateElement { id: 1, tag: "div" },
|
||||
CreateTextNode {
|
||||
id: 2,
|
||||
text: "abc1"
|
||||
},
|
||||
AppendChildren { many: 1 },
|
||||
CreateElement { id: 3, tag: "p" },
|
||||
CreateElement { id: 4, tag: "h1" },
|
||||
CreateElement { id: 5, tag: "div" },
|
||||
CreateTextNode {
|
||||
id: 6,
|
||||
text: "abc2"
|
||||
},
|
||||
AppendChildren { many: 1 },
|
||||
CreateElement { id: 7, tag: "p" },
|
||||
CreateElement { id: 8, tag: "h1" },
|
||||
CreateElement { id: 9, tag: "div" },
|
||||
CreateTextNode {
|
||||
id: 10,
|
||||
text: "abc3"
|
||||
},
|
||||
AppendChildren { many: 1 },
|
||||
CreateElement { id: 11, tag: "p" },
|
||||
AppendChildren { many: 9 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn anchors() {
|
||||
static App: FC<()> = |cx| {
|
||||
cx.render(rsx! {
|
||||
{true.then(|| rsx!{ div { "hello" } })}
|
||||
{false.then(|| rsx!{ div { "goodbye" } })}
|
||||
})
|
||||
};
|
||||
|
||||
test_logging::set_up_logging(LOGGING_ENABLED);
|
||||
|
||||
let mut dom = VirtualDom::new(App);
|
||||
let mutations = dom.rebuild_async().await;
|
||||
assert_eq!(
|
||||
mutations.edits,
|
||||
[
|
||||
CreateElement { id: 0, tag: "div" },
|
||||
CreateTextNode {
|
||||
id: 1,
|
||||
text: "hello"
|
||||
},
|
||||
AppendChildren { many: 1 },
|
||||
CreatePlaceholder { id: 2 },
|
||||
AppendChildren { many: 2 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn suspended() {
|
||||
static App: FC<()> = |cx| {
|
||||
let val = use_suspense(cx, || async {}, |cx, _| cx.render(rsx! { "hi "}));
|
||||
cx.render(rsx! { {val} })
|
||||
};
|
||||
|
||||
test_logging::set_up_logging(LOGGING_ENABLED);
|
||||
|
||||
let mut dom = VirtualDom::new(App);
|
||||
let mutations = dom.rebuild_async().await;
|
||||
|
||||
assert_eq!(
|
||||
mutations.edits,
|
||||
[CreatePlaceholder { id: 0 }, AppendChildren { many: 1 },]
|
||||
);
|
||||
}
|
2
packages/core/tests/debugdiff.rs
Normal file
2
packages/core/tests/debugdiff.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
/// A virtualdom wrapper used for testing purposes.
|
||||
pub struct DebugDiff {}
|
48
packages/core/tests/diff_iterative.rs
Normal file
48
packages/core/tests/diff_iterative.rs
Normal file
|
@ -0,0 +1,48 @@
|
|||
//! tests to prove that the iterative implementation works
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
mod test_logging;
|
||||
use dioxus_core as dioxus;
|
||||
use dioxus_html as dioxus_elements;
|
||||
|
||||
const LOGGING_ENABLED: bool = false;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_iterative_create_components() {
|
||||
static App: FC<()> = |cx| {
|
||||
// test root fragments
|
||||
cx.render(rsx! {
|
||||
Child { "abc1" }
|
||||
Child { "abc2" }
|
||||
Child { "abc3" }
|
||||
})
|
||||
};
|
||||
|
||||
fn Child(cx: Context<()>) -> DomTree {
|
||||
// test root fragments, anchors, and ChildNode type
|
||||
cx.render(rsx! {
|
||||
h1 {}
|
||||
div { {cx.children()} }
|
||||
Fragment {
|
||||
Fragment {
|
||||
Fragment {
|
||||
"wozza"
|
||||
}
|
||||
}
|
||||
}
|
||||
{(0..0).map(|_f| rsx!{ div { "walalla"}})}
|
||||
p {}
|
||||
})
|
||||
}
|
||||
|
||||
test_logging::set_up_logging(LOGGING_ENABLED);
|
||||
|
||||
let mut dom = VirtualDom::new(App);
|
||||
|
||||
let mutations = dom.rebuild_async().await;
|
||||
dbg!(mutations);
|
||||
|
||||
let mutations = dom.diff();
|
||||
dbg!(mutations);
|
||||
}
|
|
@ -1,85 +1,23 @@
|
|||
//! Diffing Tests
|
||||
//! -------------
|
||||
//!
|
||||
//! These should always compile and run, but the result is not validated for each test.
|
||||
//! TODO: Validate the results beyond visual inspection.
|
||||
//! These tests only verify that the diffing algorithm works properly for single components.
|
||||
//!
|
||||
//! It does not validated that component lifecycles work properly. This is done in another test file.
|
||||
|
||||
use bumpalo::Bump;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use dioxus::{
|
||||
arena::SharedResources,
|
||||
diff::{CreateMeta, DiffMachine},
|
||||
prelude::*,
|
||||
DomEdit,
|
||||
};
|
||||
use dioxus::{prelude::*, DomEdit, TestDom};
|
||||
use dioxus_core as dioxus;
|
||||
use dioxus_html as dioxus_elements;
|
||||
|
||||
struct TestDom {
|
||||
bump: Bump,
|
||||
resources: SharedResources,
|
||||
}
|
||||
impl TestDom {
|
||||
fn new() -> TestDom {
|
||||
let bump = Bump::new();
|
||||
let resources = SharedResources::new();
|
||||
TestDom { bump, resources }
|
||||
}
|
||||
fn new_factory<'a>(&'a self) -> NodeFactory<'a> {
|
||||
NodeFactory::new(&self.bump)
|
||||
}
|
||||
mod test_logging;
|
||||
use DomEdit::*;
|
||||
|
||||
fn render<'a, F>(&'a self, lazy_nodes: LazyNodes<'a, F>) -> VNode<'a>
|
||||
where
|
||||
F: FnOnce(NodeFactory<'a>) -> VNode<'a>,
|
||||
{
|
||||
use dioxus_core::nodes::{IntoVNode, IntoVNodeList};
|
||||
lazy_nodes.into_vnode(NodeFactory::new(&self.bump))
|
||||
}
|
||||
// logging is wired up to the test harness
|
||||
// feel free to enable while debugging
|
||||
const IS_LOGGING_ENABLED: bool = false;
|
||||
|
||||
fn diff<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Vec<DomEdit<'a>> {
|
||||
let mut edits = Vec::new();
|
||||
let mut machine = DiffMachine::new_headless(&mut edits, &self.resources);
|
||||
machine.diff_node(old, new);
|
||||
edits
|
||||
}
|
||||
|
||||
fn create<'a, F1>(&'a self, left: LazyNodes<'a, F1>) -> (CreateMeta, Vec<DomEdit<'a>>)
|
||||
where
|
||||
F1: FnOnce(NodeFactory<'a>) -> VNode<'a>,
|
||||
{
|
||||
let old = self.bump.alloc(self.render(left));
|
||||
let mut edits = Vec::new();
|
||||
|
||||
let mut machine = DiffMachine::new_headless(&mut edits, &self.resources);
|
||||
let meta = machine.create_vnode(old);
|
||||
(meta, edits)
|
||||
}
|
||||
|
||||
fn lazy_diff<'a, F1, F2>(
|
||||
&'a self,
|
||||
left: LazyNodes<'a, F1>,
|
||||
right: LazyNodes<'a, F2>,
|
||||
) -> (Vec<DomEdit<'a>>, Vec<DomEdit<'a>>)
|
||||
where
|
||||
F1: FnOnce(NodeFactory<'a>) -> VNode<'a>,
|
||||
F2: FnOnce(NodeFactory<'a>) -> VNode<'a>,
|
||||
{
|
||||
let old = self.bump.alloc(self.render(left));
|
||||
|
||||
let new = self.bump.alloc(self.render(right));
|
||||
|
||||
let mut create_edits = Vec::new();
|
||||
|
||||
let mut machine = DiffMachine::new_headless(&mut create_edits, &self.resources);
|
||||
machine.create_vnode(old);
|
||||
|
||||
let mut edits = Vec::new();
|
||||
let mut machine = DiffMachine::new_headless(&mut edits, &self.resources);
|
||||
machine.diff_node(old, new);
|
||||
(create_edits, edits)
|
||||
}
|
||||
fn new_dom() -> TestDom {
|
||||
test_logging::set_up_logging(IS_LOGGING_ENABLED);
|
||||
TestDom::new()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -88,78 +26,170 @@ fn diffing_works() {}
|
|||
/// Should push the text node onto the stack and modify it
|
||||
#[test]
|
||||
fn html_and_rsx_generate_the_same_output() {
|
||||
let dom = TestDom::new();
|
||||
let edits = dom.lazy_diff(
|
||||
let dom = new_dom();
|
||||
let (create, change) = dom.lazy_diff(
|
||||
rsx! ( div { "Hello world" } ),
|
||||
rsx! ( div { "Goodbye world" } ),
|
||||
);
|
||||
dbg!(edits);
|
||||
assert_eq!(
|
||||
create.edits,
|
||||
[
|
||||
CreateElement { id: 0, tag: "div" },
|
||||
CreateTextNode {
|
||||
id: 1,
|
||||
text: "Hello world"
|
||||
},
|
||||
AppendChildren { many: 1 },
|
||||
AppendChildren { many: 1 },
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
PushRoot { id: 1 },
|
||||
SetText {
|
||||
text: "Goodbye world"
|
||||
},
|
||||
PopRoot
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Should result in 3 elements on the stack
|
||||
#[test]
|
||||
fn fragments_create_properly() {
|
||||
let dom = TestDom::new();
|
||||
let (meta, edits) = dom.create(rsx! {
|
||||
let dom = new_dom();
|
||||
|
||||
let create = dom.create(rsx! {
|
||||
div { "Hello a" }
|
||||
div { "Hello b" }
|
||||
div { "Hello c" }
|
||||
});
|
||||
assert!(&edits[0].is("CreateElement"));
|
||||
assert!(&edits[3].is("CreateElement"));
|
||||
assert!(&edits[6].is("CreateElement"));
|
||||
|
||||
assert_eq!(meta.added_to_stack, 3);
|
||||
dbg!(edits);
|
||||
assert_eq!(
|
||||
create.edits,
|
||||
[
|
||||
CreateElement { id: 0, tag: "div" },
|
||||
CreateTextNode {
|
||||
id: 1,
|
||||
text: "Hello a"
|
||||
},
|
||||
AppendChildren { many: 1 },
|
||||
CreateElement { id: 2, tag: "div" },
|
||||
CreateTextNode {
|
||||
id: 3,
|
||||
text: "Hello b"
|
||||
},
|
||||
AppendChildren { many: 1 },
|
||||
CreateElement { id: 4, tag: "div" },
|
||||
CreateTextNode {
|
||||
id: 5,
|
||||
text: "Hello c"
|
||||
},
|
||||
AppendChildren { many: 1 },
|
||||
AppendChildren { many: 3 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Should result in the creation of an anchor (placeholder) and then a replacewith
|
||||
#[test]
|
||||
fn empty_fragments_create_anchors() {
|
||||
let dom = TestDom::new();
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({ (0..0).map(|f| rsx! { div {}}) });
|
||||
let right = rsx!({ (0..1).map(|f| rsx! { div {}}) });
|
||||
|
||||
let edits = dom.lazy_diff(left, right);
|
||||
dbg!(edits);
|
||||
let (create, change) = dom.lazy_diff(left, right);
|
||||
|
||||
assert_eq!(
|
||||
create.edits,
|
||||
[CreatePlaceholder { id: 0 }, AppendChildren { many: 1 }]
|
||||
);
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
CreateElement { id: 1, tag: "div" },
|
||||
ReplaceWith { m: 1, root: 0 }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Should result in the creation of an anchor (placeholder) and then a replacewith m=5
|
||||
#[test]
|
||||
fn empty_fragments_create_many_anchors() {
|
||||
let dom = TestDom::new();
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({ (0..0).map(|f| rsx! { div {}}) });
|
||||
let right = rsx!({ (0..5).map(|f| rsx! { div {}}) });
|
||||
|
||||
let edits = dom.lazy_diff(left, right);
|
||||
dbg!(edits);
|
||||
let (create, change) = dom.lazy_diff(left, right);
|
||||
assert_eq!(
|
||||
create.edits,
|
||||
[CreatePlaceholder { id: 0 }, AppendChildren { many: 1 }]
|
||||
);
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
CreateElement { id: 1, tag: "div" },
|
||||
CreateElement { id: 2, tag: "div" },
|
||||
CreateElement { id: 3, tag: "div" },
|
||||
CreateElement { id: 4, tag: "div" },
|
||||
CreateElement { id: 5, tag: "div" },
|
||||
ReplaceWith { m: 5, root: 0 }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Should result in the creation of an anchor (placeholder) and then a replacewith
|
||||
/// Includes child nodes inside the fragment
|
||||
#[test]
|
||||
fn empty_fragments_create_anchors_with_many_children() {
|
||||
let dom = TestDom::new();
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({ (0..0).map(|f| rsx! { div {} }) });
|
||||
let right = rsx!({
|
||||
(0..5).map(|f| {
|
||||
rsx! { div { "hello" }}
|
||||
(0..3).map(|f| {
|
||||
rsx! { div { "hello: {f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let edits = dom.lazy_diff(left, right);
|
||||
dbg!(&edits);
|
||||
let last_edit = edits.1.last().unwrap();
|
||||
assert!(last_edit.is("ReplaceWith"));
|
||||
let (create, change) = dom.lazy_diff(left, right);
|
||||
assert_eq!(
|
||||
create.edits,
|
||||
[CreatePlaceholder { id: 0 }, AppendChildren { many: 1 }]
|
||||
);
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
CreateElement { id: 1, tag: "div" },
|
||||
CreateTextNode {
|
||||
text: "hello: 0",
|
||||
id: 2
|
||||
},
|
||||
AppendChildren { many: 1 },
|
||||
CreateElement { id: 3, tag: "div" },
|
||||
CreateTextNode {
|
||||
text: "hello: 1",
|
||||
id: 4
|
||||
},
|
||||
AppendChildren { many: 1 },
|
||||
CreateElement { id: 5, tag: "div" },
|
||||
CreateTextNode {
|
||||
text: "hello: 2",
|
||||
id: 6
|
||||
},
|
||||
AppendChildren { many: 1 },
|
||||
ReplaceWith { m: 3, root: 0 }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Should result in every node being pushed and then replaced with an anchor
|
||||
#[test]
|
||||
fn many_items_become_fragment() {
|
||||
let dom = TestDom::new();
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({
|
||||
(0..2).map(|f| {
|
||||
|
@ -168,14 +198,41 @@ fn many_items_become_fragment() {
|
|||
});
|
||||
let right = rsx!({ (0..0).map(|f| rsx! { div {} }) });
|
||||
|
||||
let edits = dom.lazy_diff(left, right);
|
||||
dbg!(&edits);
|
||||
let (create, change) = dom.lazy_diff(left, right);
|
||||
assert_eq!(
|
||||
create.edits,
|
||||
[
|
||||
CreateElement { id: 0, tag: "div" },
|
||||
CreateTextNode {
|
||||
text: "hello",
|
||||
id: 1
|
||||
},
|
||||
AppendChildren { many: 1 },
|
||||
CreateElement { id: 2, tag: "div" },
|
||||
CreateTextNode {
|
||||
text: "hello",
|
||||
id: 3
|
||||
},
|
||||
AppendChildren { many: 1 },
|
||||
AppendChildren { many: 2 },
|
||||
]
|
||||
);
|
||||
|
||||
// hmmmmmmmmm worried about reusing IDs that we shouldnt be
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
Remove { root: 2 },
|
||||
CreatePlaceholder { id: 4 },
|
||||
ReplaceWith { root: 0, m: 1 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Should result in no edits
|
||||
#[test]
|
||||
fn two_equal_fragments_are_equal() {
|
||||
let dom = TestDom::new();
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({
|
||||
(0..2).map(|f| {
|
||||
|
@ -188,33 +245,47 @@ fn two_equal_fragments_are_equal() {
|
|||
})
|
||||
});
|
||||
|
||||
let edits = dom.lazy_diff(left, right);
|
||||
dbg!(&edits);
|
||||
assert!(edits.1.is_empty());
|
||||
let (create, change) = dom.lazy_diff(left, right);
|
||||
assert!(change.edits.is_empty());
|
||||
}
|
||||
|
||||
/// Should result the creation of more nodes appended after the old last node
|
||||
#[test]
|
||||
fn two_fragments_with_differrent_elements_are_differet() {
|
||||
let dom = TestDom::new();
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!(
|
||||
{(0..2).map(|f| {rsx! { div { }}})}
|
||||
{ (0..2).map(|_| rsx! { div { }} ) }
|
||||
p {}
|
||||
);
|
||||
let right = rsx!(
|
||||
{(0..5).map(|f| {rsx! { h1 { }}})}
|
||||
{ (0..5).map(|_| rsx! (h1 { }) ) }
|
||||
p {}
|
||||
);
|
||||
|
||||
let edits = dom.lazy_diff(left, right);
|
||||
dbg!(&edits);
|
||||
let (create, changes) = dom.lazy_diff(left, right);
|
||||
log::debug!("{:#?}", &changes);
|
||||
assert_eq!(
|
||||
changes.edits,
|
||||
[
|
||||
// create the new h1s
|
||||
CreateElement { tag: "h1", id: 3 },
|
||||
CreateElement { tag: "h1", id: 4 },
|
||||
CreateElement { tag: "h1", id: 5 },
|
||||
InsertAfter { root: 1, n: 3 },
|
||||
// replace the divs with new h1s
|
||||
CreateElement { tag: "h1", id: 6 },
|
||||
ReplaceWith { root: 0, m: 1 },
|
||||
CreateElement { tag: "h1", id: 7 },
|
||||
ReplaceWith { root: 1, m: 1 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Should result in multiple nodes destroyed - with changes to the first nodes
|
||||
#[test]
|
||||
fn two_fragments_with_differrent_elements_are_differet_shorter() {
|
||||
let dom = TestDom::new();
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!(
|
||||
{(0..5).map(|f| {rsx! { div { }}})}
|
||||
|
@ -225,14 +296,37 @@ fn two_fragments_with_differrent_elements_are_differet_shorter() {
|
|||
p {}
|
||||
);
|
||||
|
||||
let edits = dom.lazy_diff(left, right);
|
||||
dbg!(&edits);
|
||||
let (create, change) = dom.lazy_diff(left, right);
|
||||
assert_eq!(
|
||||
create.edits,
|
||||
[
|
||||
CreateElement { id: 0, tag: "div" },
|
||||
CreateElement { id: 1, tag: "div" },
|
||||
CreateElement { id: 2, tag: "div" },
|
||||
CreateElement { id: 3, tag: "div" },
|
||||
CreateElement { id: 4, tag: "div" },
|
||||
CreateElement { id: 5, tag: "p" },
|
||||
AppendChildren { many: 6 },
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
Remove { root: 2 },
|
||||
Remove { root: 3 },
|
||||
Remove { root: 4 },
|
||||
CreateElement { id: 6, tag: "h1" },
|
||||
ReplaceWith { root: 0, m: 1 },
|
||||
CreateElement { id: 7, tag: "h1" },
|
||||
ReplaceWith { root: 1, m: 1 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Should result in multiple nodes destroyed - with no changes
|
||||
#[test]
|
||||
fn two_fragments_with_same_elements_are_differet() {
|
||||
let dom = TestDom::new();
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!(
|
||||
{(0..2).map(|f| {rsx! { div { }}})}
|
||||
|
@ -243,32 +337,31 @@ fn two_fragments_with_same_elements_are_differet() {
|
|||
p {}
|
||||
);
|
||||
|
||||
let edits = dom.lazy_diff(left, right);
|
||||
dbg!(&edits);
|
||||
}
|
||||
|
||||
// Similar test from above, but with extra child nodes
|
||||
#[test]
|
||||
fn two_fragments_with_same_elements_are_differet_shorter() {
|
||||
let dom = TestDom::new();
|
||||
|
||||
let left = rsx!(
|
||||
{(0..5).map(|f| {rsx! { div { }}})}
|
||||
p {"e"}
|
||||
let (create, change) = dom.lazy_diff(left, right);
|
||||
assert_eq!(
|
||||
create.edits,
|
||||
[
|
||||
CreateElement { id: 0, tag: "div" },
|
||||
CreateElement { id: 1, tag: "div" },
|
||||
CreateElement { id: 2, tag: "p" },
|
||||
AppendChildren { many: 3 },
|
||||
]
|
||||
);
|
||||
let right = rsx!(
|
||||
{(0..2).map(|f| {rsx! { div { }}})}
|
||||
p {"e"}
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
CreateElement { id: 3, tag: "div" },
|
||||
CreateElement { id: 4, tag: "div" },
|
||||
CreateElement { id: 5, tag: "div" },
|
||||
InsertAfter { root: 1, n: 3 },
|
||||
]
|
||||
);
|
||||
|
||||
let edits = dom.lazy_diff(left, right);
|
||||
dbg!(&edits);
|
||||
}
|
||||
|
||||
/// should result in the removal of elements
|
||||
#[test]
|
||||
fn keyed_diffing_order() {
|
||||
let dom = TestDom::new();
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!(
|
||||
{(0..5).map(|f| {rsx! { div { key: "{f}" }}})}
|
||||
|
@ -279,62 +372,331 @@ fn keyed_diffing_order() {
|
|||
p {"e"}
|
||||
);
|
||||
|
||||
let edits = dom.lazy_diff(left, right);
|
||||
dbg!(&edits);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fragment_keys() {
|
||||
let r = 1;
|
||||
let p = rsx! {
|
||||
Fragment { key: "asd {r}" }
|
||||
};
|
||||
let (create, change) = dom.lazy_diff(left, right);
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[Remove { root: 2 }, Remove { root: 3 }, Remove { root: 4 },]
|
||||
);
|
||||
}
|
||||
|
||||
/// Should result in moves, but not removals or additions
|
||||
#[test]
|
||||
fn keyed_diffing_out_of_order() {
|
||||
let dom = TestDom::new();
|
||||
let dom = new_dom();
|
||||
|
||||
// 0, 1, 2, 3, 4, 5, 6, 7, 8,
|
||||
let left = rsx!({
|
||||
(0..3).chain(3..6).chain(6..9).map(|f| {
|
||||
[0, 1, 2, 3, /**/ 4, 5, 6, /**/ 7, 8, 9].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
// 0, 1, 2, 6, 5, 4, 3, 7, 8, 9
|
||||
let right = rsx!({
|
||||
(0..3).chain((3..7).rev()).chain(7..10).map(|f| {
|
||||
[0, 1, 2, 3, /**/ 6, 4, 5, /**/ 7, 8, 9].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
// LIS: 3, 7, 8,
|
||||
let edits = dom.lazy_diff(left, right);
|
||||
dbg!(&edits);
|
||||
let (_, changes) = dom.lazy_diff(left, right);
|
||||
log::debug!("{:?}", &changes);
|
||||
assert_eq!(
|
||||
changes.edits,
|
||||
[PushRoot { id: 6 }, InsertBefore { root: 4, n: 1 }]
|
||||
);
|
||||
}
|
||||
|
||||
/// Should result in moves only
|
||||
#[test]
|
||||
fn keyed_diffing_out_of_order_adds() {
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({
|
||||
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let right = rsx!({
|
||||
[/**/ 8, 7, 4, 5, 6 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let (_, change) = dom.lazy_diff(left, right);
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
PushRoot { id: 4 },
|
||||
PushRoot { id: 3 },
|
||||
InsertBefore { n: 2, root: 0 }
|
||||
]
|
||||
);
|
||||
}
|
||||
/// Should result in moves onl
|
||||
#[test]
|
||||
fn keyed_diffing_out_of_order_adds_2() {
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({
|
||||
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let right = rsx!({
|
||||
[/**/ 7, 8, 4, 5, 6 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let (_, change) = dom.lazy_diff(left, right);
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
PushRoot { id: 3 },
|
||||
PushRoot { id: 4 },
|
||||
InsertBefore { n: 2, root: 0 }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Should result in moves onl
|
||||
#[test]
|
||||
fn keyed_diffing_out_of_order_adds_3() {
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({
|
||||
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let right = rsx!({
|
||||
[/**/ 4, 8, 7, 5, 6 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let (_, change) = dom.lazy_diff(left, right);
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
PushRoot { id: 4 },
|
||||
PushRoot { id: 3 },
|
||||
InsertBefore { n: 2, root: 1 }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Should result in moves onl
|
||||
#[test]
|
||||
fn keyed_diffing_out_of_order_adds_4() {
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({
|
||||
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let right = rsx!({
|
||||
[/**/ 4, 5, 8, 7, 6 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let (_, change) = dom.lazy_diff(left, right);
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
PushRoot { id: 4 },
|
||||
PushRoot { id: 3 },
|
||||
InsertBefore { n: 2, root: 2 }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Should result in moves onl
|
||||
#[test]
|
||||
fn keyed_diffing_out_of_order_adds_5() {
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({
|
||||
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let right = rsx!({
|
||||
[/**/ 4, 5, 6, 8, 7 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let (_, change) = dom.lazy_diff(left, right);
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[PushRoot { id: 4 }, InsertBefore { n: 1, root: 3 }]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyed_diffing_additions() {
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({
|
||||
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let right = rsx!({
|
||||
[/**/ 4, 5, 6, 7, 8, 9, 10 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let (_, change) = dom.lazy_diff(left, right);
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
CreateElement { id: 5, tag: "div" },
|
||||
CreateElement { id: 6, tag: "div" },
|
||||
InsertAfter { n: 2, root: 4 }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyed_diffing_additions_and_moves_on_ends() {
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({
|
||||
[/**/ 4, 5, 6, 7 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let right = rsx!({
|
||||
[/**/ 7, 4, 5, 6, 11, 12 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let (_, change) = dom.lazy_diff(left, right);
|
||||
log::debug!("{:?}", change);
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
// create 11, 12
|
||||
CreateElement { tag: "div", id: 4 },
|
||||
CreateElement { tag: "div", id: 5 },
|
||||
InsertAfter { root: 2, n: 2 },
|
||||
// move 7 to the front
|
||||
PushRoot { id: 3 },
|
||||
InsertBefore { root: 0, n: 1 }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyed_diffing_additions_and_moves_in_middle() {
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({
|
||||
[/**/ 4, 5, 6, 7 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let right = rsx!({
|
||||
[/**/ 7, 4, 13, 17, 5, 11, 12, 6 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
// LIS: 4, 5, 6
|
||||
let (_, change) = dom.lazy_diff(left, right);
|
||||
log::debug!("{:#?}", change);
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
// create 13, 17
|
||||
CreateElement { tag: "div", id: 4 },
|
||||
CreateElement { tag: "div", id: 5 },
|
||||
InsertBefore { root: 1, n: 2 },
|
||||
// create 11, 12
|
||||
CreateElement { tag: "div", id: 6 },
|
||||
CreateElement { tag: "div", id: 7 },
|
||||
InsertBefore { root: 2, n: 2 },
|
||||
// move 7
|
||||
PushRoot { id: 3 },
|
||||
InsertBefore { root: 0, n: 1 }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn controlled_keyed_diffing_out_of_order() {
|
||||
let dom = TestDom::new();
|
||||
let dom = new_dom();
|
||||
|
||||
let left = [4, 5, 6, 7];
|
||||
let left = rsx!({
|
||||
left.iter().map(|f| {
|
||||
rsx! { div { key: "{f}" "{f}" }}
|
||||
[4, 5, 6, 7].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
// 0, 1, 2, 6, 5, 4, 3, 7, 8, 9
|
||||
let right = [0, 5, 9, 6, 4];
|
||||
let right = rsx!({
|
||||
right.iter().map(|f| {
|
||||
rsx! { div { key: "{f}" "{f}" }}
|
||||
[0, 5, 9, 6, 4].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
// LIS: 3, 7, 8,
|
||||
let edits = dom.lazy_diff(left, right);
|
||||
dbg!(&edits);
|
||||
// LIS: 5, 6
|
||||
let (_, changes) = dom.lazy_diff(left, right);
|
||||
log::debug!("{:#?}", &changes);
|
||||
assert_eq!(
|
||||
changes.edits,
|
||||
[
|
||||
// move 4 to after 6
|
||||
PushRoot { id: 0 },
|
||||
InsertAfter { n: 1, root: 2 },
|
||||
// remove 7
|
||||
|
||||
// create 9 and insert before 6
|
||||
CreateElement { id: 4, tag: "div" },
|
||||
InsertBefore { n: 1, root: 2 },
|
||||
// create 0 and insert before 5
|
||||
CreateElement { id: 5, tag: "div" },
|
||||
InsertBefore { n: 1, root: 1 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn controlled_keyed_diffing_out_of_order_max_test() {
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({
|
||||
[0, 1, 2, 3, 4].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let right = rsx!({
|
||||
[3, 0, 1, 10, 2].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let (_, changes) = dom.lazy_diff(left, right);
|
||||
log::debug!("{:#?}", &changes);
|
||||
assert_eq!(
|
||||
changes.edits,
|
||||
[
|
||||
CreateElement { id: 5, tag: "div" },
|
||||
InsertBefore { n: 1, root: 2 },
|
||||
PushRoot { id: 3 },
|
||||
InsertBefore { n: 1, root: 0 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
use bumpalo::Bump;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use dioxus::{
|
||||
arena::SharedResources,
|
||||
diff::{CreateMeta, DiffMachine},
|
||||
prelude::*,
|
||||
DomEdit,
|
||||
};
|
||||
use dioxus::{prelude::*, DomEdit};
|
||||
use dioxus_core as dioxus;
|
||||
use dioxus_html as dioxus_elements;
|
||||
|
||||
|
@ -19,14 +14,9 @@ async fn event_queue_works() {
|
|||
};
|
||||
|
||||
let mut dom = VirtualDom::new(App);
|
||||
let edits = dom.rebuild().unwrap();
|
||||
let edits = dom.rebuild();
|
||||
|
||||
async_std::task::spawn_local(async move {
|
||||
match dom.run_unbounded().await {
|
||||
Err(_) => todo!(),
|
||||
Ok(mutations) => {
|
||||
//
|
||||
}
|
||||
}
|
||||
// let mutations = dom.run_unbounded().await;
|
||||
});
|
||||
}
|
||||
|
|
21
packages/core/tests/hooks.rs
Normal file
21
packages/core/tests/hooks.rs
Normal file
|
@ -0,0 +1,21 @@
|
|||
use anyhow::{Context, Result};
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_core as dioxus;
|
||||
use dioxus_html as dioxus_elements;
|
||||
|
||||
#[test]
|
||||
fn sample_refs() {
|
||||
// static App: FC<()> = |cx| {
|
||||
// let div_ref = use_node_ref::<MyRef, _>(cx);
|
||||
|
||||
// cx.render(rsx! {
|
||||
// div {
|
||||
// style: { color: "red" },
|
||||
// node_ref: div_ref,
|
||||
// onmouseover: move |_| {
|
||||
// div_ref.borrow_mut().focus();
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
// };
|
||||
}
|
41
packages/core/tests/set_state_batch.rs
Normal file
41
packages/core/tests/set_state_batch.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
use futures_util::StreamExt;
|
||||
|
||||
/*
|
||||
furtures_channel provides us some batching simply due to how Rust's async works.
|
||||
|
||||
Any hook that uses schedule_update is simply deferring to unbounded_send. Multiple
|
||||
unbounded_sends can be linked together in succession provided there isn't an "await"
|
||||
between them. Our internal batching mechanism simply waits for the "schedule_update"
|
||||
to fire and then pulls any messages off the unbounded_send queue.
|
||||
|
||||
Additionally, due to how our "time slicing" works we'll always come back and check
|
||||
in for new work if the deadline hasn't expired. On average, our deadline should be
|
||||
about 10ms, which is way more than enough for diffing/creating to happen.
|
||||
*/
|
||||
#[async_std::test]
|
||||
async fn batch() {
|
||||
let (sender, mut recver) = futures_channel::mpsc::unbounded::<i32>();
|
||||
|
||||
let _handle = async_std::task::spawn(async move {
|
||||
let _msg = recver.next().await;
|
||||
while let Ok(msg) = recver.try_next() {
|
||||
println!("{:#?}", msg);
|
||||
}
|
||||
let _msg = recver.next().await;
|
||||
while let Ok(msg) = recver.try_next() {
|
||||
println!("{:#?}", msg);
|
||||
}
|
||||
});
|
||||
|
||||
sender.unbounded_send(1).unwrap();
|
||||
sender.unbounded_send(2).unwrap();
|
||||
sender.unbounded_send(3).unwrap();
|
||||
sender.unbounded_send(4).unwrap();
|
||||
|
||||
async_std::task::sleep(std::time::Duration::from_millis(100)).await;
|
||||
|
||||
sender.unbounded_send(5).unwrap();
|
||||
sender.unbounded_send(6).unwrap();
|
||||
sender.unbounded_send(7).unwrap();
|
||||
sender.unbounded_send(8).unwrap();
|
||||
}
|
52
packages/core/tests/test_logging.rs
Normal file
52
packages/core/tests/test_logging.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
pub fn set_up_logging(enabled: bool) {
|
||||
use fern::colors::{Color, ColoredLevelConfig};
|
||||
|
||||
|
||||
if !enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
// configure colors for the whole line
|
||||
let colors_line = ColoredLevelConfig::new()
|
||||
.error(Color::Red)
|
||||
.warn(Color::Yellow)
|
||||
// we actually don't need to specify the color for debug and info, they are white by default
|
||||
.info(Color::White)
|
||||
.debug(Color::White)
|
||||
// depending on the terminals color scheme, this is the same as the background color
|
||||
.trace(Color::BrightBlack);
|
||||
|
||||
// configure colors for the name of the level.
|
||||
// since almost all of them are the same as the color for the whole line, we
|
||||
// just clone `colors_line` and overwrite our changes
|
||||
let colors_level = colors_line.clone().info(Color::Green);
|
||||
// here we set up our fern Dispatch
|
||||
|
||||
// when running tests in batch, the logger is re-used, so ignore the logger error
|
||||
let _ = fern::Dispatch::new()
|
||||
.format(move |out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"{color_line}[{level}{color_line}] {message}\x1B[0m",
|
||||
color_line = format_args!(
|
||||
"\x1B[{}m",
|
||||
colors_line.get_color(&record.level()).to_fg_str()
|
||||
),
|
||||
level = colors_level.color(record.level()),
|
||||
message = message,
|
||||
));
|
||||
})
|
||||
// set the default log level. to filter out verbose log messages from dependencies, set
|
||||
// this to Warn and overwrite the log level for your crate.
|
||||
.level(log::LevelFilter::Debug)
|
||||
// .level(log::LevelFilter::Warn)
|
||||
// change log levels for individual modules. Note: This looks for the record's target
|
||||
// field which defaults to the module path but can be overwritten with the `target`
|
||||
// parameter:
|
||||
// `info!(target="special_target", "This log message is about special_target");`
|
||||
// .level_for("dioxus", log::LevelFilter::Debug)
|
||||
// .level_for("dioxus", log::LevelFilter::Info)
|
||||
// .level_for("pretty_colored", log::LevelFilter::Trace)
|
||||
// output to stdout
|
||||
.chain(std::io::stdout())
|
||||
.apply();
|
||||
}
|
|
@ -19,7 +19,7 @@ fn app_runs() {
|
|||
cx.render(rsx!( div{"hello"} ))
|
||||
};
|
||||
let mut vdom = VirtualDom::new(App);
|
||||
let edits = vdom.rebuild_in_place().unwrap();
|
||||
let edits = vdom.rebuild();
|
||||
dbg!(edits);
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ fn fragments_work() {
|
|||
))
|
||||
};
|
||||
let mut vdom = VirtualDom::new(App);
|
||||
let edits = vdom.rebuild_in_place().unwrap();
|
||||
let edits = vdom.rebuild();
|
||||
// should result in a final "appendchildren n=2"
|
||||
dbg!(edits);
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ fn lists_work() {
|
|||
))
|
||||
};
|
||||
let mut vdom = VirtualDom::new(App);
|
||||
let edits = vdom.rebuild_in_place().unwrap();
|
||||
let edits = vdom.rebuild();
|
||||
dbg!(edits);
|
||||
}
|
||||
|
||||
|
@ -61,10 +61,10 @@ fn conditional_rendering() {
|
|||
};
|
||||
let mut vdom = VirtualDom::new(App);
|
||||
|
||||
let edits = vdom.rebuild_in_place().unwrap();
|
||||
dbg!(&edits);
|
||||
let mutations = vdom.rebuild();
|
||||
dbg!(&mutations);
|
||||
// the "false" fragment should generate an empty placeholder to re-visit
|
||||
assert!(edits[edits.len() - 2].is("CreatePlaceholder"));
|
||||
assert!(mutations.edits[mutations.edits.len() - 2].is("CreatePlaceholder"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -82,7 +82,7 @@ fn child_components() {
|
|||
))
|
||||
};
|
||||
let mut vdom = VirtualDom::new(App);
|
||||
let edits = vdom.rebuild_in_place().unwrap();
|
||||
let edits = vdom.rebuild();
|
||||
dbg!(edits);
|
||||
}
|
||||
|
||||
|
@ -94,6 +94,6 @@ fn suspended_works() {
|
|||
};
|
||||
|
||||
let mut vdom = VirtualDom::new(App);
|
||||
let edits = vdom.rebuild_in_place().unwrap();
|
||||
let edits = vdom.rebuild();
|
||||
dbg!(edits);
|
||||
}
|
||||
|
|
30
packages/core/tests/work_sync.rs
Normal file
30
packages/core/tests/work_sync.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
//! Diffing is interruptible, but uses yield_now which is loop-pollable
|
||||
//!
|
||||
//! This means you can actually call it synchronously if you want.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use dioxus::{prelude::*, scope::Scope};
|
||||
use dioxus_core as dioxus;
|
||||
use dioxus_html as dioxus_elements;
|
||||
use futures_util::FutureExt;
|
||||
|
||||
#[test]
|
||||
fn worksync() {
|
||||
static App: FC<()> = |cx| {
|
||||
cx.render(rsx! {
|
||||
div {"hello"}
|
||||
})
|
||||
};
|
||||
let mut dom = VirtualDom::new(App);
|
||||
|
||||
let mut fut = dom.rebuild_async().boxed_local();
|
||||
|
||||
let mutations = loop {
|
||||
let g = (&mut fut).now_or_never();
|
||||
if g.is_some() {
|
||||
break g.unwrap();
|
||||
}
|
||||
};
|
||||
|
||||
dbg!(mutations);
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
//! webview dom
|
||||
|
||||
use dioxus_core::{DomEdit, RealDom};
|
||||
use dioxus_core::DomEdit;
|
||||
|
||||
// pub struct WebviewRegistry {}
|
||||
|
||||
|
@ -29,9 +29,3 @@ impl WebviewDom<'_> {
|
|||
// self.registry
|
||||
// }
|
||||
}
|
||||
impl RealDom for WebviewDom<'_> {
|
||||
fn raw_node_as_any(&self) -> &mut dyn std::any::Any {
|
||||
todo!()
|
||||
// self.edits.push(PushRoot { root });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,9 +6,9 @@ use std::rc::Rc;
|
|||
use dioxus_core::{
|
||||
events::{
|
||||
on::{MouseEvent, MouseEventInner},
|
||||
VirtualEvent,
|
||||
SyntheticEvent,
|
||||
},
|
||||
ElementId, EventPriority, EventTrigger, ScopeId,
|
||||
ElementId, EventPriority, ScopeId, UserEvent,
|
||||
};
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
|
@ -17,15 +17,19 @@ struct ImEvent {
|
|||
mounted_dom_id: u64,
|
||||
scope: u64,
|
||||
}
|
||||
pub fn trigger_from_serialized(val: serde_json::Value) -> EventTrigger {
|
||||
pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
|
||||
let mut data: Vec<ImEvent> = serde_json::from_value(val).unwrap();
|
||||
let data = data.drain(..).next().unwrap();
|
||||
|
||||
let event = VirtualEvent::MouseEvent(MouseEvent(Rc::new(WebviewMouseEvent)));
|
||||
let event = SyntheticEvent::MouseEvent(MouseEvent(Rc::new(WebviewMouseEvent)));
|
||||
let scope = ScopeId(data.scope as usize);
|
||||
let mounted_dom_id = Some(ElementId(data.mounted_dom_id as usize));
|
||||
let priority = EventPriority::High;
|
||||
EventTrigger::new(event, scope, mounted_dom_id, priority)
|
||||
UserEvent {
|
||||
name: todo!(),
|
||||
event,
|
||||
scope,
|
||||
mounted_dom_id,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
//! This module is not included anywhere.
|
||||
//!
|
||||
//! It is a prototype for a system that supports non-string attribute values.
|
||||
|
||||
trait AsAttributeValue: Sized {
|
||||
fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a>;
|
|
@ -1,6 +1,6 @@
|
|||
//! webview dom
|
||||
|
||||
use dioxus_core::{DomEdit, ElementId, RealDom, ScopeId};
|
||||
use dioxus_core::{DomEdit, ElementId, ScopeId};
|
||||
use DomEdit::*;
|
||||
|
||||
pub struct WebviewRegistry {}
|
||||
|
@ -30,9 +30,3 @@ impl WebviewDom<'_> {
|
|||
self.registry
|
||||
}
|
||||
}
|
||||
impl<'bump> RealDom for WebviewDom<'bump> {
|
||||
fn raw_node_as_any(&self) -> &mut dyn std::any::Any {
|
||||
todo!()
|
||||
// self.edits.push(PushRoot { root });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -117,8 +117,8 @@ impl<T: Properties + 'static> WebviewRenderer<T> {
|
|||
|
||||
// Serialize the edit stream
|
||||
let edits = {
|
||||
let mut edits = Vec::new();
|
||||
lock.rebuild(&mut edits).unwrap();
|
||||
let mut edits = Vec::<DomEdit>::new();
|
||||
// lock.rebuild(&mut edits).unwrap();
|
||||
serde_json::to_value(edits).unwrap()
|
||||
};
|
||||
|
||||
|
@ -139,8 +139,8 @@ impl<T: Properties + 'static> WebviewRenderer<T> {
|
|||
|
||||
// Serialize the edit stream
|
||||
let edits = {
|
||||
let mut edits = Vec::new();
|
||||
lock.rebuild(&mut edits).unwrap();
|
||||
let mut edits = Vec::<DomEdit>::new();
|
||||
// lock.rebuild(&mut edits).unwrap();
|
||||
serde_json::to_value(edits).unwrap()
|
||||
};
|
||||
|
||||
|
|
25
packages/ssr/examples/basic.rs
Normal file
25
packages/ssr/examples/basic.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
use dioxus::virtual_dom::VirtualDom;
|
||||
use dioxus_core as dioxus;
|
||||
use dioxus_core::prelude::*;
|
||||
use dioxus_html as dioxus_elements;
|
||||
|
||||
fn main() {
|
||||
let mut dom = VirtualDom::new(App);
|
||||
dom.rebuild();
|
||||
println!(
|
||||
"{}",
|
||||
dioxus_ssr::render_vdom(&dom, |c| c.newline(true).indent(true))
|
||||
)
|
||||
}
|
||||
|
||||
pub static App: FC<()> = |cx| {
|
||||
cx.render(rsx!(
|
||||
div {
|
||||
class: "overflow-hidden"
|
||||
ul {
|
||||
{(0..10).map(|i| rsx!{ li { class: "flex flex-col", "entry: {i}"}})}
|
||||
}
|
||||
"hello world!"
|
||||
}
|
||||
))
|
||||
};
|
|
@ -19,7 +19,9 @@ async fn main() -> Result<(), std::io::Error> {
|
|||
.map(|f| f.parse().unwrap_or("...?".to_string()))
|
||||
.unwrap_or("...?".to_string());
|
||||
|
||||
let dom = VirtualDom::launch_with_props_in_place(Example, ExampleProps { initial_name });
|
||||
let mut dom = VirtualDom::new_with_props(Example, ExampleProps { initial_name });
|
||||
|
||||
dom.rebuild();
|
||||
|
||||
Ok(Response::builder(200)
|
||||
.body(format!("{}", dioxus_ssr::render_vdom(&dom, |c| c)))
|
||||
|
|
|
@ -13,7 +13,8 @@ fn main() {
|
|||
let mut file = File::create("example.html").unwrap();
|
||||
|
||||
let mut dom = VirtualDom::new(App);
|
||||
dom.rebuild_in_place().expect("failed to run virtualdom");
|
||||
|
||||
dom.rebuild();
|
||||
|
||||
file.write_fmt(format_args!(
|
||||
"{}",
|
||||
|
|
|
@ -46,7 +46,7 @@ pub fn render_vdom_scope(vdom: &VirtualDom, scope: ScopeId) -> Option<String> {
|
|||
/// ```ignore
|
||||
/// static App: FC<()> = |cx| cx.render(rsx!(div { "hello world" }));
|
||||
/// let mut vdom = VirtualDom::new(App);
|
||||
/// vdom.rebuild_in_place();
|
||||
/// vdom.rebuild();
|
||||
///
|
||||
/// let renderer = TextRenderer::new(&vdom);
|
||||
/// let output = format!("{}", renderer);
|
||||
|
@ -74,8 +74,8 @@ impl<'a> TextRenderer<'a> {
|
|||
}
|
||||
|
||||
fn html_render(&self, node: &VNode, f: &mut std::fmt::Formatter, il: u16) -> std::fmt::Result {
|
||||
match &node.kind {
|
||||
VNodeKind::Text(text) => {
|
||||
match &node {
|
||||
VNode::Text(text) => {
|
||||
if self.cfg.indent {
|
||||
for _ in 0..il {
|
||||
write!(f, " ")?;
|
||||
|
@ -83,7 +83,7 @@ impl<'a> TextRenderer<'a> {
|
|||
}
|
||||
write!(f, "{}", text.text)?
|
||||
}
|
||||
VNodeKind::Anchor(anchor) => {
|
||||
VNode::Anchor(anchor) => {
|
||||
//
|
||||
if self.cfg.indent {
|
||||
for _ in 0..il {
|
||||
|
@ -92,7 +92,7 @@ impl<'a> TextRenderer<'a> {
|
|||
}
|
||||
write!(f, "<!-- -->")?;
|
||||
}
|
||||
VNodeKind::Element(el) => {
|
||||
VNode::Element(el) => {
|
||||
if self.cfg.indent {
|
||||
for _ in 0..il {
|
||||
write!(f, " ")?;
|
||||
|
@ -129,7 +129,7 @@ impl<'a> TextRenderer<'a> {
|
|||
//
|
||||
// when the page is loaded, the `querySelectorAll` will be used to collect all the nodes, and then add
|
||||
// them interpreter's stack
|
||||
match (self.cfg.pre_render, node.try_direct_id()) {
|
||||
match (self.cfg.pre_render, node.try_mounted_id()) {
|
||||
(true, Some(id)) => {
|
||||
write!(f, " dio_el=\"{}\"", id)?;
|
||||
//
|
||||
|
@ -163,13 +163,13 @@ impl<'a> TextRenderer<'a> {
|
|||
write!(f, "\n")?;
|
||||
}
|
||||
}
|
||||
VNodeKind::Fragment(frag) => {
|
||||
VNode::Fragment(frag) => {
|
||||
for child in frag.children {
|
||||
self.html_render(child, f, il + 1)?;
|
||||
}
|
||||
}
|
||||
VNodeKind::Component(vcomp) => {
|
||||
let idx = vcomp.ass_scope.get().unwrap();
|
||||
VNode::Component(vcomp) => {
|
||||
let idx = vcomp.associated_scope.get().unwrap();
|
||||
match (self.vdom, self.cfg.skip_components) {
|
||||
(Some(vdom), false) => {
|
||||
let new_node = vdom.get_scope(idx).unwrap().root();
|
||||
|
@ -180,7 +180,7 @@ impl<'a> TextRenderer<'a> {
|
|||
}
|
||||
}
|
||||
}
|
||||
VNodeKind::Suspended { .. } => {
|
||||
VNode::Suspended { .. } => {
|
||||
// we can't do anything with suspended nodes
|
||||
}
|
||||
}
|
||||
|
@ -286,28 +286,28 @@ mod tests {
|
|||
#[test]
|
||||
fn to_string_works() {
|
||||
let mut dom = VirtualDom::new(SIMPLE_APP);
|
||||
dom.rebuild_in_place().expect("failed to run virtualdom");
|
||||
dom.rebuild();
|
||||
dbg!(render_vdom(&dom, |c| c));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hydration() {
|
||||
let mut dom = VirtualDom::new(NESTED_APP);
|
||||
dom.rebuild_in_place().expect("failed to run virtualdom");
|
||||
dom.rebuild();
|
||||
dbg!(render_vdom(&dom, |c| c.pre_render(true)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested() {
|
||||
let mut dom = VirtualDom::new(NESTED_APP);
|
||||
dom.rebuild_in_place().expect("failed to run virtualdom");
|
||||
dom.rebuild();
|
||||
dbg!(render_vdom(&dom, |c| c));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fragment_app() {
|
||||
let mut dom = VirtualDom::new(FRAGMENT_APP);
|
||||
dom.rebuild_in_place().expect("failed to run virtualdom");
|
||||
dom.rebuild();
|
||||
dbg!(render_vdom(&dom, |c| c));
|
||||
}
|
||||
|
||||
|
@ -319,7 +319,7 @@ mod tests {
|
|||
let mut file = File::create("index.html").unwrap();
|
||||
|
||||
let mut dom = VirtualDom::new(SLIGHTLY_MORE_COMPLEX);
|
||||
dom.rebuild_in_place().expect("failed to run virtualdom");
|
||||
dom.rebuild();
|
||||
|
||||
file.write_fmt(format_args!(
|
||||
"{}",
|
||||
|
@ -337,7 +337,7 @@ mod tests {
|
|||
};
|
||||
|
||||
let mut dom = VirtualDom::new(STLYE_APP);
|
||||
dom.rebuild_in_place().expect("failed to run virtualdom");
|
||||
dom.rebuild();
|
||||
dbg!(render_vdom(&dom, |c| c));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,16 +14,16 @@ js-sys = "0.3"
|
|||
wasm-bindgen = { version = "0.2.71", features = ["enable-interning"] }
|
||||
lazy_static = "1.4.0"
|
||||
wasm-bindgen-futures = "0.4.20"
|
||||
wasm-logger = "0.2.0"
|
||||
log = "0.4.14"
|
||||
fxhash = "0.2.1"
|
||||
wasm-logger = "0.2.0"
|
||||
console_error_panic_hook = "0.1.6"
|
||||
generational-arena = "0.2.8"
|
||||
wasm-bindgen-test = "0.3.21"
|
||||
once_cell = "1.8"
|
||||
async-channel = "1.6.1"
|
||||
anyhow = "1.0"
|
||||
|
||||
gloo-timers = { version = "0.2.1", features = ["futures"] }
|
||||
futures-util = "0.3.15"
|
||||
|
||||
[dependencies.web-sys]
|
||||
|
@ -78,7 +78,6 @@ im-rc = "15.0.0"
|
|||
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"] }
|
||||
serde = { version = "1.0.126", features = ["derive"] }
|
||||
surf = { git = "https://github.com/http-rs/surf", rev = "1ffaba8873", default-features = false, features = [
|
||||
"wasm-client",
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
//! Basic example that renders a simple VNode to the browser.
|
||||
|
||||
// all these imports are done automatically with the `dioxus` crate and `prelude`
|
||||
// need to do them manually for this example
|
||||
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::*;
|
||||
use std::future::Future;
|
||||
use std::{pin::Pin, time::Duration};
|
||||
|
||||
fn main() {
|
||||
// Setup logging
|
||||
|
@ -21,18 +19,15 @@ fn main() {
|
|||
console_error_panic_hook::set_once();
|
||||
|
||||
// Run the app
|
||||
dioxus_web::launch(App, |c| c)
|
||||
dioxus_web::launch(APP, |c| c)
|
||||
}
|
||||
|
||||
static App: FC<()> = |cx| {
|
||||
let mut count = use_state(cx, || 0);
|
||||
static APP: FC<()> = |cx| {
|
||||
let mut count = use_state(cx, || 3);
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
button {
|
||||
"add"
|
||||
onclick: move |_| count += 1
|
||||
}
|
||||
button { onclick: move |_| count += 1, "add" }
|
||||
ul {
|
||||
{(0..*count).map(|f| rsx!{
|
||||
li { "a - {f}" }
|
||||
|
@ -40,6 +35,19 @@ static App: FC<()> = |cx| {
|
|||
li { "c - {f}" }
|
||||
})}
|
||||
}
|
||||
Child {}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
static Child: FC<()> = |cx| {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
div {
|
||||
div {
|
||||
"hello child"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
use std::{collections::HashMap, rc::Rc, sync::Arc};
|
||||
use std::{collections::HashMap, fmt::Debug, rc::Rc, sync::Arc};
|
||||
|
||||
use dioxus_core::{
|
||||
events::{EventTrigger, VirtualEvent},
|
||||
events::{on::GenericEventInner, SyntheticEvent, UserEvent},
|
||||
mutations::NodeRefMutation,
|
||||
scheduler::SchedulerMsg,
|
||||
DomEdit, ElementId, ScopeId,
|
||||
};
|
||||
use fxhash::FxHashMap;
|
||||
use wasm_bindgen::{closure::Closure, JsCast};
|
||||
use web_sys::{
|
||||
window, CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement,
|
||||
HtmlOptionElement, Node, NodeList,
|
||||
HtmlOptionElement, Node, NodeList, UiEvent,
|
||||
};
|
||||
|
||||
use crate::{nodeslab::NodeSlab, WebConfig};
|
||||
|
@ -23,7 +25,7 @@ pub struct WebsysDom {
|
|||
|
||||
root: Element,
|
||||
|
||||
sender_callback: Rc<dyn Fn(EventTrigger)>,
|
||||
sender_callback: Rc<dyn Fn(SchedulerMsg)>,
|
||||
|
||||
// map of listener types to number of those listeners
|
||||
// This is roughly a delegater
|
||||
|
@ -38,7 +40,7 @@ pub struct WebsysDom {
|
|||
last_node_was_text: bool,
|
||||
}
|
||||
impl WebsysDom {
|
||||
pub fn new(root: Element, cfg: WebConfig, sender_callback: Rc<dyn Fn(EventTrigger)>) -> Self {
|
||||
pub fn new(root: Element, cfg: WebConfig, sender_callback: Rc<dyn Fn(SchedulerMsg)>) -> Self {
|
||||
let document = load_document();
|
||||
|
||||
let mut nodes = NodeSlab::new(2000);
|
||||
|
@ -77,6 +79,18 @@ impl WebsysDom {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn apply_refs(&mut self, refs: &[NodeRefMutation]) {
|
||||
for item in refs {
|
||||
if let Some(bla) = &item.element {
|
||||
let node = self.nodes[item.element_id.as_u64() as usize]
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.clone();
|
||||
bla.set(Box::new(node)).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_edits(&mut self, edits: &mut Vec<DomEdit>) {
|
||||
for edit in edits.drain(..) {
|
||||
log::info!("Handling edit: {:#?}", edit);
|
||||
|
@ -84,26 +98,27 @@ impl WebsysDom {
|
|||
DomEdit::PushRoot { id: root } => self.push(root),
|
||||
DomEdit::PopRoot => self.pop(),
|
||||
DomEdit::AppendChildren { many } => self.append_children(many),
|
||||
DomEdit::ReplaceWith { n, m } => self.replace_with(n, m),
|
||||
DomEdit::Remove => self.remove(),
|
||||
DomEdit::ReplaceWith { m, root } => self.replace_with(m, root),
|
||||
DomEdit::Remove { root } => self.remove(root),
|
||||
DomEdit::RemoveAllChildren => self.remove_all_children(),
|
||||
DomEdit::CreateTextNode { text, id } => self.create_text_node(text, id),
|
||||
DomEdit::CreateElement { tag, id } => self.create_element(tag, None, id),
|
||||
DomEdit::CreateElementNs { tag, id, ns } => self.create_element(tag, Some(ns), id),
|
||||
DomEdit::CreatePlaceholder { id } => self.create_placeholder(id),
|
||||
DomEdit::NewEventListener {
|
||||
event_name: event,
|
||||
event_name,
|
||||
scope,
|
||||
mounted_node_id: node,
|
||||
} => self.new_event_listener(event, scope, node),
|
||||
mounted_node_id,
|
||||
} => self.new_event_listener(event_name, scope, mounted_node_id),
|
||||
|
||||
DomEdit::RemoveEventListener { event } => todo!(),
|
||||
|
||||
DomEdit::SetText { text } => self.set_text(text),
|
||||
DomEdit::SetAttribute { field, value, ns } => self.set_attribute(field, value, ns),
|
||||
DomEdit::RemoveAttribute { name } => self.remove_attribute(name),
|
||||
|
||||
DomEdit::InsertAfter { n } => self.insert_after(n),
|
||||
DomEdit::InsertBefore { n } => self.insert_before(n),
|
||||
DomEdit::InsertAfter { n, root } => self.insert_after(n, root),
|
||||
DomEdit::InsertBefore { n, root } => self.insert_before(n, root),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -118,14 +133,13 @@ impl WebsysDom {
|
|||
|
||||
self.stack.push(real_node);
|
||||
}
|
||||
|
||||
// drop the node off the stack
|
||||
fn pop(&mut self) {
|
||||
self.stack.pop();
|
||||
}
|
||||
|
||||
fn append_children(&mut self, many: u32) {
|
||||
log::debug!("Called [`append_child`]");
|
||||
|
||||
let root: Node = self
|
||||
.stack
|
||||
.list
|
||||
|
@ -133,9 +147,11 @@ impl WebsysDom {
|
|||
.unwrap()
|
||||
.clone();
|
||||
|
||||
for _ in 0..many {
|
||||
let child = self.stack.pop();
|
||||
|
||||
for child in self
|
||||
.stack
|
||||
.list
|
||||
.drain((self.stack.list.len() - many as usize)..)
|
||||
{
|
||||
if child.dyn_ref::<web_sys::Text>().is_some() {
|
||||
if self.last_node_was_text {
|
||||
let comment_node = self
|
||||
|
@ -143,75 +159,46 @@ impl WebsysDom {
|
|||
.create_comment("dioxus")
|
||||
.dyn_into::<Node>()
|
||||
.unwrap();
|
||||
self.stack.top().append_child(&comment_node).unwrap();
|
||||
root.append_child(&comment_node).unwrap();
|
||||
}
|
||||
self.last_node_was_text = true;
|
||||
} else {
|
||||
self.last_node_was_text = false;
|
||||
}
|
||||
|
||||
root.append_child(&child).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_with(&mut self, n: u32, m: u32) {
|
||||
log::debug!("Called [`replace_with`]");
|
||||
fn replace_with(&mut self, m: u32, root: u64) {
|
||||
let old = self.nodes[root as usize].as_ref().unwrap();
|
||||
|
||||
let mut new_nodes = vec![];
|
||||
for _ in 0..m {
|
||||
new_nodes.push(self.stack.pop());
|
||||
let arr: js_sys::Array = self
|
||||
.stack
|
||||
.list
|
||||
.drain((self.stack.list.len() - m as usize)..)
|
||||
.collect();
|
||||
|
||||
if let Some(el) = old.dyn_ref::<Element>() {
|
||||
el.replace_with_with_node(&arr).unwrap();
|
||||
} else if let Some(el) = old.dyn_ref::<web_sys::CharacterData>() {
|
||||
el.replace_with_with_node(&arr).unwrap();
|
||||
} else if let Some(el) = old.dyn_ref::<web_sys::DocumentType>() {
|
||||
el.replace_with_with_node(&arr).unwrap();
|
||||
}
|
||||
|
||||
let mut old_nodes = vec![];
|
||||
for _ in 0..n {
|
||||
old_nodes.push(self.stack.pop());
|
||||
}
|
||||
|
||||
for node in &old_nodes[1..] {
|
||||
node.dyn_ref::<Element>().unwrap().remove();
|
||||
}
|
||||
|
||||
let old = old_nodes[0].clone();
|
||||
let arr: js_sys::Array = new_nodes.iter().collect();
|
||||
let el = old.dyn_into::<Element>().unwrap();
|
||||
el.replace_with_with_node(&arr).unwrap();
|
||||
// let arr = js_sys::Array::from();
|
||||
|
||||
// TODO: use different-sized replace withs
|
||||
// if m == 1 {
|
||||
// if old_node.has_type::<Element>() {
|
||||
// old_node
|
||||
// .dyn_ref::<Element>()
|
||||
// .unwrap()
|
||||
// .replace_with_with_node_1(&new_node)
|
||||
// .unwrap();
|
||||
// } else if old_node.has_type::<web_sys::CharacterData>() {
|
||||
// old_node
|
||||
// .dyn_ref::<web_sys::CharacterData>()
|
||||
// .unwrap()
|
||||
// .replace_with_with_node_1(&new_node)
|
||||
// .unwrap();
|
||||
// } else if old_node.has_type::<web_sys::DocumentType>() {
|
||||
// old_node
|
||||
// .dyn_ref::<web_sys::DocumentType>()
|
||||
// .unwrap()
|
||||
// .replace_with_with_node_1(&new_node)
|
||||
// .unwrap();
|
||||
// } else {
|
||||
// panic!("Cannot replace node: {:?}", old_node);
|
||||
// }
|
||||
// }
|
||||
|
||||
// self.stack.push(new_node);
|
||||
}
|
||||
|
||||
fn remove(&mut self) {
|
||||
log::debug!("Called [`remove`]");
|
||||
todo!()
|
||||
fn remove(&mut self, root: u64) {
|
||||
let node = self.nodes[root as usize].as_ref().unwrap();
|
||||
if let Some(element) = node.dyn_ref::<Element>() {
|
||||
element.remove();
|
||||
} else {
|
||||
if let Some(parent) = node.parent_node() {
|
||||
parent.remove_child(&node).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_all_children(&mut self) {
|
||||
log::debug!("Called [`remove_all_children`]");
|
||||
todo!()
|
||||
}
|
||||
|
||||
|
@ -219,22 +206,22 @@ impl WebsysDom {
|
|||
self.create_element("pre", None, id);
|
||||
self.set_attribute("hidden", "", None);
|
||||
}
|
||||
fn create_text_node(&mut self, text: &str, id: u64) {
|
||||
// let nid = self.node_counter.next();
|
||||
|
||||
fn create_text_node(&mut self, text: &str, id: u64) {
|
||||
let textnode = self
|
||||
.document
|
||||
.create_text_node(text)
|
||||
.dyn_into::<Node>()
|
||||
.unwrap();
|
||||
|
||||
let id = id as usize;
|
||||
self.stack.push(textnode.clone());
|
||||
self.nodes[id] = Some(textnode);
|
||||
|
||||
self.nodes[(id as usize)] = Some(textnode);
|
||||
}
|
||||
|
||||
fn create_element(&mut self, tag: &str, ns: Option<&'static str>, id: u64) {
|
||||
let tag = wasm_bindgen::intern(tag);
|
||||
|
||||
let el = match ns {
|
||||
Some(ns) => self
|
||||
.document
|
||||
|
@ -249,18 +236,12 @@ impl WebsysDom {
|
|||
.dyn_into::<Node>()
|
||||
.unwrap(),
|
||||
};
|
||||
let id = id as usize;
|
||||
|
||||
self.stack.push(el.clone());
|
||||
self.nodes[id] = Some(el);
|
||||
// let nid = self.node_counter.?next();
|
||||
// let nid = self.nodes.insert(el).data().as_ffi();
|
||||
// log::debug!("Called [`create_element`]: {}, {:?}", tag, nid);
|
||||
// ElementId::new(nid)
|
||||
self.nodes[(id as usize)] = Some(el);
|
||||
}
|
||||
|
||||
fn new_event_listener(&mut self, event: &'static str, scope: ScopeId, real_id: u64) {
|
||||
// let (_on, event) = event.split_at(2);
|
||||
let event = wasm_bindgen::intern(event);
|
||||
|
||||
// attach the correct attributes to the element
|
||||
|
@ -297,7 +278,7 @@ impl WebsysDom {
|
|||
// "Result" cannot be received from JS
|
||||
// Instead, we just build and immediately execute a closure that returns result
|
||||
match decode_trigger(event) {
|
||||
Ok(synthetic_event) => trigger.as_ref()(synthetic_event),
|
||||
Ok(synthetic_event) => trigger.as_ref()(SchedulerMsg::UiEvent(synthetic_event)),
|
||||
Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e),
|
||||
};
|
||||
}) as Box<dyn FnMut(&Event)>);
|
||||
|
@ -320,7 +301,8 @@ impl WebsysDom {
|
|||
}
|
||||
|
||||
fn set_attribute(&mut self, name: &str, value: &str, ns: Option<&str>) {
|
||||
if let Some(el) = self.stack.top().dyn_ref::<Element>() {
|
||||
let node = self.stack.top();
|
||||
if let Some(el) = node.dyn_ref::<Element>() {
|
||||
match ns {
|
||||
// inline style support
|
||||
Some("style") => {
|
||||
|
@ -330,11 +312,20 @@ impl WebsysDom {
|
|||
}
|
||||
_ => el.set_attribute(name, value).unwrap(),
|
||||
}
|
||||
match name {
|
||||
"value" => {}
|
||||
"checked" => {}
|
||||
"selected" => {}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Some(node) = node.dyn_ref::<HtmlInputElement>() {
|
||||
if name == "value" {
|
||||
node.set_value(value);
|
||||
}
|
||||
if name == "checked" {
|
||||
node.set_checked(true);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(node) = node.dyn_ref::<HtmlOptionElement>() {
|
||||
if name == "selected" {
|
||||
node.set_selected(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -361,49 +352,52 @@ impl WebsysDom {
|
|||
}
|
||||
}
|
||||
|
||||
fn insert_after(&mut self, n: u32) {
|
||||
let mut new_nodes = vec![];
|
||||
for _ in 0..n {
|
||||
new_nodes.push(self.stack.pop());
|
||||
}
|
||||
fn insert_after(&mut self, n: u32, root: u64) {
|
||||
let old = self.nodes[root as usize].as_ref().unwrap();
|
||||
|
||||
let after = self.stack.top().clone();
|
||||
let arr: js_sys::Array = new_nodes.iter().collect();
|
||||
|
||||
let el = after.dyn_into::<Element>().unwrap();
|
||||
el.after_with_node(&arr).unwrap();
|
||||
// let mut old_nodes = vec![];
|
||||
// for _ in 0..n {
|
||||
// old_nodes.push(self.stack.pop());
|
||||
// }
|
||||
|
||||
// let el = self.stack.top();
|
||||
}
|
||||
|
||||
fn insert_before(&mut self, n: u32) {
|
||||
let n = n as usize;
|
||||
let root = self
|
||||
let arr: js_sys::Array = self
|
||||
.stack
|
||||
.list
|
||||
.get(self.stack.list.len() - n)
|
||||
.unwrap()
|
||||
.clone();
|
||||
for _ in 0..n {
|
||||
let el = self.stack.pop();
|
||||
root.insert_before(&el, None).unwrap();
|
||||
.drain((self.stack.list.len() - n as usize)..)
|
||||
.collect();
|
||||
|
||||
if let Some(el) = old.dyn_ref::<Element>() {
|
||||
el.after_with_node(&arr).unwrap();
|
||||
} else if let Some(el) = old.dyn_ref::<web_sys::CharacterData>() {
|
||||
el.after_with_node(&arr).unwrap();
|
||||
} else if let Some(el) = old.dyn_ref::<web_sys::DocumentType>() {
|
||||
el.after_with_node(&arr).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> dioxus_core::diff::RealDom for WebsysDom {
|
||||
// fn request_available_node(&mut self) -> ElementId {
|
||||
// let key = self.nodes.insert(None);
|
||||
// log::debug!("making new key: {:#?}", key);
|
||||
// ElementId(key.data().as_ffi())
|
||||
// }
|
||||
fn insert_before(&mut self, n: u32, root: u64) {
|
||||
let after = self.nodes[root as usize].as_ref().unwrap();
|
||||
|
||||
fn raw_node_as_any(&self) -> &mut dyn std::any::Any {
|
||||
todo!()
|
||||
if n == 1 {
|
||||
let before = self.stack.pop();
|
||||
|
||||
after
|
||||
.parent_node()
|
||||
.unwrap()
|
||||
.insert_before(&before, Some(&after))
|
||||
.unwrap();
|
||||
|
||||
after.insert_before(&before, None).unwrap();
|
||||
} else {
|
||||
let arr: js_sys::Array = self
|
||||
.stack
|
||||
.list
|
||||
.drain((self.stack.list.len() - n as usize)..)
|
||||
.collect();
|
||||
|
||||
if let Some(el) = after.dyn_ref::<Element>() {
|
||||
el.before_with_node(&arr).unwrap();
|
||||
} else if let Some(el) = after.dyn_ref::<web_sys::CharacterData>() {
|
||||
el.before_with_node(&arr).unwrap();
|
||||
} else if let Some(el) = after.dyn_ref::<web_sys::DocumentType>() {
|
||||
el.before_with_node(&arr).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -443,287 +437,89 @@ impl Stack {
|
|||
}
|
||||
}
|
||||
|
||||
fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent {
|
||||
fn virtual_event_from_websys_event(event: web_sys::Event) -> SyntheticEvent {
|
||||
use crate::events::*;
|
||||
use dioxus_core::events::on::*;
|
||||
match event.type_().as_str() {
|
||||
"copy" | "cut" | "paste" => {
|
||||
struct WebsysClipboardEvent();
|
||||
impl ClipboardEventInner for WebsysClipboardEvent {}
|
||||
VirtualEvent::ClipboardEvent(ClipboardEvent(Rc::new(WebsysClipboardEvent())))
|
||||
SyntheticEvent::ClipboardEvent(ClipboardEvent(Rc::new(WebsysClipboardEvent(event))))
|
||||
}
|
||||
|
||||
"compositionend" | "compositionstart" | "compositionupdate" => {
|
||||
let evt: web_sys::CompositionEvent = event.clone().dyn_into().unwrap();
|
||||
struct WebsysCompositionEvent(web_sys::CompositionEvent);
|
||||
impl CompositionEventInner for WebsysCompositionEvent {
|
||||
fn data(&self) -> String {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
VirtualEvent::CompositionEvent(CompositionEvent(Rc::new(WebsysCompositionEvent(evt))))
|
||||
SyntheticEvent::CompositionEvent(CompositionEvent(Rc::new(WebsysCompositionEvent(evt))))
|
||||
}
|
||||
|
||||
"keydown" | "keypress" | "keyup" => {
|
||||
struct Event(web_sys::KeyboardEvent);
|
||||
impl KeyboardEventInner for Event {
|
||||
fn char_code(&self) -> u32 {
|
||||
todo!()
|
||||
}
|
||||
fn key_code(&self) -> KeyCode {
|
||||
todo!()
|
||||
}
|
||||
fn ctrl_key(&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn key(&self) -> String {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn locale(&self) -> String {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn location(&self) -> usize {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn meta_key(&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn repeat(&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn shift_key(&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn which(&self) -> usize {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_modifier_state(&self, key_code: usize) -> bool {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
let evt: web_sys::KeyboardEvent = event.clone().dyn_into().unwrap();
|
||||
VirtualEvent::KeyboardEvent(KeyboardEvent(Rc::new(Event(evt))))
|
||||
let evt: web_sys::KeyboardEvent = event.dyn_into().unwrap();
|
||||
SyntheticEvent::KeyboardEvent(KeyboardEvent(Rc::new(WebsysKeyboardEvent(evt))))
|
||||
}
|
||||
|
||||
"focus" | "blur" => {
|
||||
struct Event(web_sys::FocusEvent);
|
||||
impl FocusEventInner for Event {}
|
||||
let evt: web_sys::FocusEvent = event.clone().dyn_into().unwrap();
|
||||
VirtualEvent::FocusEvent(FocusEvent(Rc::new(Event(evt))))
|
||||
let evt: web_sys::FocusEvent = event.dyn_into().unwrap();
|
||||
SyntheticEvent::FocusEvent(FocusEvent(Rc::new(WebsysFocusEvent(evt))))
|
||||
}
|
||||
|
||||
"change" => {
|
||||
// struct Event(web_sys::Event);
|
||||
// impl GenericEventInner for Event {
|
||||
// fn bubbles(&self) -> bool {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// fn cancel_bubble(&self) {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// fn cancelable(&self) -> bool {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// fn composed(&self) -> bool {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// fn composed_path(&self) -> String {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// fn current_target(&self) {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// fn default_prevented(&self) -> bool {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// fn event_phase(&self) -> usize {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// fn is_trusted(&self) -> bool {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// fn prevent_default(&self) {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// fn stop_immediate_propagation(&self) {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// fn stop_propagation(&self) {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// fn target(&self) {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// fn time_stamp(&self) -> usize {
|
||||
// todo!()
|
||||
// }
|
||||
// }
|
||||
// let evt: web_sys::Event = event.clone().dyn_into().expect("wrong error typ");
|
||||
// VirtualEvent::Event(GenericEvent(Rc::new(Event(evt))))
|
||||
todo!()
|
||||
let evt = event.dyn_into().unwrap();
|
||||
SyntheticEvent::GenericEvent(GenericEvent(Rc::new(WebsysGenericUiEvent(evt))))
|
||||
}
|
||||
|
||||
"input" | "invalid" | "reset" | "submit" => {
|
||||
// is a special react events
|
||||
let evt: web_sys::InputEvent = event.clone().dyn_into().expect("wrong event type");
|
||||
let this: web_sys::EventTarget = evt.target().unwrap();
|
||||
|
||||
let value = (&this)
|
||||
.dyn_ref()
|
||||
.map(|input: &web_sys::HtmlInputElement| input.value())
|
||||
.or_else(|| {
|
||||
(&this)
|
||||
.dyn_ref()
|
||||
.map(|input: &web_sys::HtmlTextAreaElement| input.value())
|
||||
})
|
||||
.or_else(|| {
|
||||
(&this)
|
||||
.dyn_ref::<web_sys::HtmlElement>()
|
||||
.unwrap()
|
||||
.text_content()
|
||||
})
|
||||
.expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener");
|
||||
|
||||
todo!()
|
||||
// VirtualEvent::FormEvent(FormEvent { value })
|
||||
let evt: web_sys::InputEvent = event.clone().dyn_into().unwrap();
|
||||
SyntheticEvent::FormEvent(FormEvent(Rc::new(WebsysFormEvent(evt))))
|
||||
}
|
||||
|
||||
"click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit"
|
||||
| "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter"
|
||||
| "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => {
|
||||
let evt: web_sys::MouseEvent = event.clone().dyn_into().unwrap();
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CustomMouseEvent(web_sys::MouseEvent);
|
||||
impl dioxus_core::events::on::MouseEventInner for CustomMouseEvent {
|
||||
fn alt_key(&self) -> bool {
|
||||
self.0.alt_key()
|
||||
}
|
||||
fn button(&self) -> i16 {
|
||||
self.0.button()
|
||||
}
|
||||
fn buttons(&self) -> u16 {
|
||||
self.0.buttons()
|
||||
}
|
||||
fn client_x(&self) -> i32 {
|
||||
self.0.client_x()
|
||||
}
|
||||
fn client_y(&self) -> i32 {
|
||||
self.0.client_y()
|
||||
}
|
||||
fn ctrl_key(&self) -> bool {
|
||||
self.0.ctrl_key()
|
||||
}
|
||||
fn meta_key(&self) -> bool {
|
||||
self.0.meta_key()
|
||||
}
|
||||
fn page_x(&self) -> i32 {
|
||||
self.0.page_x()
|
||||
}
|
||||
fn page_y(&self) -> i32 {
|
||||
self.0.page_y()
|
||||
}
|
||||
fn screen_x(&self) -> i32 {
|
||||
self.0.screen_x()
|
||||
}
|
||||
fn screen_y(&self) -> i32 {
|
||||
self.0.screen_y()
|
||||
}
|
||||
fn shift_key(&self) -> bool {
|
||||
self.0.shift_key()
|
||||
}
|
||||
|
||||
// yikes
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
|
||||
fn get_modifier_state(&self, key_code: &str) -> bool {
|
||||
self.0.get_modifier_state(key_code)
|
||||
}
|
||||
}
|
||||
VirtualEvent::MouseEvent(MouseEvent(Rc::new(CustomMouseEvent(evt))))
|
||||
SyntheticEvent::MouseEvent(MouseEvent(Rc::new(WebsysMouseEvent(evt))))
|
||||
}
|
||||
|
||||
"pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture"
|
||||
| "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => {
|
||||
let evt: web_sys::PointerEvent = event.clone().dyn_into().unwrap();
|
||||
todo!()
|
||||
SyntheticEvent::PointerEvent(PointerEvent(Rc::new(WebsysPointerEvent(evt))))
|
||||
}
|
||||
|
||||
"select" => {
|
||||
// let evt: web_sys::SelectionEvent = event.clone().dyn_into().unwrap();
|
||||
// not required to construct anything special beyond standard event stuff
|
||||
todo!()
|
||||
let evt: web_sys::UiEvent = event.clone().dyn_into().unwrap();
|
||||
SyntheticEvent::SelectionEvent(SelectionEvent(Rc::new(WebsysGenericUiEvent(evt))))
|
||||
}
|
||||
|
||||
"touchcancel" | "touchend" | "touchmove" | "touchstart" => {
|
||||
let evt: web_sys::TouchEvent = event.clone().dyn_into().unwrap();
|
||||
todo!()
|
||||
SyntheticEvent::TouchEvent(TouchEvent(Rc::new(WebsysTouchEvent(evt))))
|
||||
}
|
||||
|
||||
"scroll" => {
|
||||
// let evt: web_sys::UIEvent = event.clone().dyn_into().unwrap();
|
||||
todo!()
|
||||
let evt: web_sys::UiEvent = event.clone().dyn_into().unwrap();
|
||||
SyntheticEvent::GenericEvent(GenericEvent(Rc::new(WebsysGenericUiEvent(evt))))
|
||||
}
|
||||
|
||||
"wheel" => {
|
||||
let evt: web_sys::WheelEvent = event.clone().dyn_into().unwrap();
|
||||
todo!()
|
||||
SyntheticEvent::WheelEvent(WheelEvent(Rc::new(WebsysWheelEvent(evt))))
|
||||
}
|
||||
"animationstart" | "animationend" | "animationiteration" => {
|
||||
let evt: web_sys::AnimationEvent = event.clone().dyn_into().unwrap();
|
||||
todo!()
|
||||
SyntheticEvent::AnimationEvent(AnimationEvent(Rc::new(WebsysAnimationEvent(evt))))
|
||||
}
|
||||
|
||||
"transitionend" => {
|
||||
let evt: web_sys::TransitionEvent = event.clone().dyn_into().unwrap();
|
||||
todo!()
|
||||
SyntheticEvent::TransitionEvent(TransitionEvent(Rc::new(WebsysTransitionEvent(evt))))
|
||||
}
|
||||
|
||||
"abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted"
|
||||
| "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play"
|
||||
| "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend"
|
||||
| "timeupdate" | "volumechange" | "waiting" => {
|
||||
// not required to construct anything special beyond standard event stuff
|
||||
|
||||
// let evt: web_sys::MediaEvent = event.clone().dyn_into().unwrap();
|
||||
// let evt: web_sys::MediaEvent = event.clone().dyn_into().unwrap();
|
||||
todo!()
|
||||
let evt: web_sys::UiEvent = event.clone().dyn_into().unwrap();
|
||||
SyntheticEvent::MediaEvent(MediaEvent(Rc::new(WebsysMediaEvent(evt))))
|
||||
}
|
||||
|
||||
"toggle" => {
|
||||
// not required to construct anything special beyond standard event stuff (target)
|
||||
|
||||
// let evt: web_sys::ToggleEvent = event.clone().dyn_into().unwrap();
|
||||
todo!()
|
||||
let evt: web_sys::UiEvent = event.clone().dyn_into().unwrap();
|
||||
SyntheticEvent::ToggleEvent(ToggleEvent(Rc::new(WebsysToggleEvent(evt))))
|
||||
}
|
||||
_ => {
|
||||
todo!()
|
||||
let evt: web_sys::UiEvent = event.clone().dyn_into().unwrap();
|
||||
SyntheticEvent::GenericEvent(GenericEvent(Rc::new(WebsysGenericUiEvent(evt))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This function decodes a websys event and produces an EventTrigger
|
||||
/// With the websys implementation, we attach a unique key to the nodes
|
||||
fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<EventTrigger> {
|
||||
fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<UserEvent> {
|
||||
log::debug!("Handling event!");
|
||||
|
||||
let target = event
|
||||
|
@ -734,13 +530,13 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<EventTrigger> {
|
|||
|
||||
let typ = event.type_();
|
||||
|
||||
use anyhow::Context;
|
||||
// let attrs = target.attributes();
|
||||
// for x in 0..attrs.length() {
|
||||
// let attr = attrs.item(x).unwrap();
|
||||
// log::debug!("attrs include: {:#?}", attr);
|
||||
// }
|
||||
|
||||
let attrs = target.attributes();
|
||||
for x in 0..attrs.length() {
|
||||
let attr = attrs.item(x).unwrap();
|
||||
log::debug!("attrs include: {:#?}", attr);
|
||||
}
|
||||
use anyhow::Context;
|
||||
|
||||
// The error handling here is not very descriptive and needs to be replaced with a zero-cost error system
|
||||
let val: String = target
|
||||
|
@ -765,12 +561,12 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<EventTrigger> {
|
|||
let triggered_scope = gi_id;
|
||||
// let triggered_scope: ScopeId = KeyData::from_ffi(gi_id).into();
|
||||
log::debug!("Triggered scope is {:#?}", triggered_scope);
|
||||
Ok(EventTrigger::new(
|
||||
virtual_event_from_websys_event(event),
|
||||
ScopeId(triggered_scope as usize),
|
||||
Some(ElementId(real_id as usize)),
|
||||
dioxus_core::events::EventPriority::High,
|
||||
))
|
||||
Ok(UserEvent {
|
||||
name: event_name_from_typ(&typ),
|
||||
event: virtual_event_from_websys_event(event.clone()),
|
||||
mounted_dom_id: Some(ElementId(real_id as usize)),
|
||||
scope: ScopeId(triggered_scope as usize),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn prepare_websys_dom() -> Element {
|
||||
|
@ -783,3 +579,90 @@ pub fn load_document() -> Document {
|
|||
.document()
|
||||
.expect("should have access to the Document")
|
||||
}
|
||||
|
||||
pub fn event_name_from_typ(typ: &str) -> &'static str {
|
||||
match typ {
|
||||
"copy" => "oncopy",
|
||||
"cut" => "oncut",
|
||||
"paste" => "onpaste",
|
||||
"compositionend" => "oncompositionend",
|
||||
"compositionstart" => "oncompositionstart",
|
||||
"compositionupdate" => "oncompositionupdate",
|
||||
"keydown" => "onkeydown",
|
||||
"keypress" => "onkeypress",
|
||||
"keyup" => "onkeyup",
|
||||
"focus" => "onfocus",
|
||||
"blur" => "onblur",
|
||||
"change" => "onchange",
|
||||
"input" => "oninput",
|
||||
"invalid" => "oninvalid",
|
||||
"reset" => "onreset",
|
||||
"submit" => "onsubmit",
|
||||
"click" => "onclick",
|
||||
"contextmenu" => "oncontextmenu",
|
||||
"doubleclick" => "ondoubleclick",
|
||||
"drag" => "ondrag",
|
||||
"dragend" => "ondragend",
|
||||
"dragenter" => "ondragenter",
|
||||
"dragexit" => "ondragexit",
|
||||
"dragleave" => "ondragleave",
|
||||
"dragover" => "ondragover",
|
||||
"dragstart" => "ondragstart",
|
||||
"drop" => "ondrop",
|
||||
"mousedown" => "onmousedown",
|
||||
"mouseenter" => "onmouseenter",
|
||||
"mouseleave" => "onmouseleave",
|
||||
"mousemove" => "onmousemove",
|
||||
"mouseout" => "onmouseout",
|
||||
"mouseover" => "onmouseover",
|
||||
"mouseup" => "onmouseup",
|
||||
"pointerdown" => "onpointerdown",
|
||||
"pointermove" => "onpointermove",
|
||||
"pointerup" => "onpointerup",
|
||||
"pointercancel" => "onpointercancel",
|
||||
"gotpointercapture" => "ongotpointercapture",
|
||||
"lostpointercapture" => "onlostpointercapture",
|
||||
"pointerenter" => "onpointerenter",
|
||||
"pointerleave" => "onpointerleave",
|
||||
"pointerover" => "onpointerover",
|
||||
"pointerout" => "onpointerout",
|
||||
"select" => "onselect",
|
||||
"touchcancel" => "ontouchcancel",
|
||||
"touchend" => "ontouchend",
|
||||
"touchmove" => "ontouchmove",
|
||||
"touchstart" => "ontouchstart",
|
||||
"scroll" => "onscroll",
|
||||
"wheel" => "onwheel",
|
||||
"animationstart" => "onanimationstart",
|
||||
"animationend" => "onanimationend",
|
||||
"animationiteration" => "onanimationiteration",
|
||||
"transitionend" => "ontransitionend",
|
||||
"abort" => "onabort",
|
||||
"canplay" => "oncanplay",
|
||||
"canplaythrough" => "oncanplaythrough",
|
||||
"durationchange" => "ondurationchange",
|
||||
"emptied" => "onemptied",
|
||||
"encrypted" => "onencrypted",
|
||||
"ended" => "onended",
|
||||
"error" => "onerror",
|
||||
"loadeddata" => "onloadeddata",
|
||||
"loadedmetadata" => "onloadedmetadata",
|
||||
"loadstart" => "onloadstart",
|
||||
"pause" => "onpause",
|
||||
"play" => "onplay",
|
||||
"playing" => "onplaying",
|
||||
"progress" => "onprogress",
|
||||
"ratechange" => "onratechange",
|
||||
"seeked" => "onseeked",
|
||||
"seeking" => "onseeking",
|
||||
"stalled" => "onstalled",
|
||||
"suspend" => "onsuspend",
|
||||
"timeupdate" => "ontimeupdate",
|
||||
"volumechange" => "onvolumechange",
|
||||
"waiting" => "onwaiting",
|
||||
"toggle" => "ontoggle",
|
||||
_ => {
|
||||
panic!("unsupported event type")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
478
packages/web/src/events.rs
Normal file
478
packages/web/src/events.rs
Normal file
|
@ -0,0 +1,478 @@
|
|||
//! Ported events into Dioxus Synthetic Event system
|
||||
//!
|
||||
//! event porting is pretty boring, sorry.
|
||||
|
||||
use dioxus_core::events::on::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::{Event, UiEvent};
|
||||
|
||||
/// All events implement the generic event type - they're all UI events
|
||||
trait WebsysGenericEvent {
|
||||
fn as_ui_event(&self) -> &UiEvent;
|
||||
}
|
||||
|
||||
impl GenericEventInner for &dyn WebsysGenericEvent {
|
||||
/// On WebSys, this returns an &UiEvent which can be casted via dyn_ref into the correct sub type.
|
||||
fn raw_event(&self) -> &dyn std::any::Any {
|
||||
self.as_ui_event()
|
||||
}
|
||||
|
||||
fn bubbles(&self) -> bool {
|
||||
self.as_ui_event().bubbles()
|
||||
}
|
||||
|
||||
fn cancel_bubble(&self) {
|
||||
self.as_ui_event().cancel_bubble();
|
||||
}
|
||||
|
||||
fn cancelable(&self) -> bool {
|
||||
self.as_ui_event().cancelable()
|
||||
}
|
||||
|
||||
fn composed(&self) -> bool {
|
||||
self.as_ui_event().composed()
|
||||
}
|
||||
|
||||
fn current_target(&self) {
|
||||
if cfg!(debug_assertions) {
|
||||
todo!("Current target does not return anything useful.\nPlease try casting the event directly.");
|
||||
}
|
||||
// self.as_ui_event().current_target();
|
||||
}
|
||||
|
||||
fn default_prevented(&self) -> bool {
|
||||
self.as_ui_event().default_prevented()
|
||||
}
|
||||
|
||||
fn event_phase(&self) -> u16 {
|
||||
self.as_ui_event().event_phase()
|
||||
}
|
||||
|
||||
fn is_trusted(&self) -> bool {
|
||||
self.as_ui_event().is_trusted()
|
||||
}
|
||||
|
||||
fn prevent_default(&self) {
|
||||
self.as_ui_event().prevent_default()
|
||||
}
|
||||
|
||||
fn stop_immediate_propagation(&self) {
|
||||
self.as_ui_event().stop_immediate_propagation()
|
||||
}
|
||||
|
||||
fn stop_propagation(&self) {
|
||||
self.as_ui_event().stop_propagation()
|
||||
}
|
||||
|
||||
fn target(&self) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn time_stamp(&self) -> f64 {
|
||||
self.as_ui_event().time_stamp()
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! implement_generic_event {
|
||||
(
|
||||
$($event:ident),*
|
||||
) => {
|
||||
$(
|
||||
impl WebsysGenericEvent for $event {
|
||||
fn as_ui_event(&self) -> &UiEvent {
|
||||
self.0.dyn_ref().unwrap()
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
implement_generic_event! {
|
||||
WebsysClipboardEvent,
|
||||
WebsysCompositionEvent,
|
||||
WebsysKeyboardEvent,
|
||||
WebsysGenericUiEvent,
|
||||
WebsysFocusEvent,
|
||||
WebsysFormEvent,
|
||||
WebsysMouseEvent,
|
||||
WebsysPointerEvent,
|
||||
WebsysWheelEvent,
|
||||
WebsysAnimationEvent,
|
||||
WebsysTransitionEvent,
|
||||
WebsysTouchEvent,
|
||||
WebsysMediaEvent,
|
||||
WebsysToggleEvent
|
||||
}
|
||||
|
||||
// unfortunately, currently experimental, and web_sys needs to be configured to use it :>(
|
||||
pub struct WebsysClipboardEvent(pub Event);
|
||||
|
||||
impl ClipboardEventInner for WebsysClipboardEvent {}
|
||||
|
||||
pub struct WebsysCompositionEvent(pub web_sys::CompositionEvent);
|
||||
|
||||
impl CompositionEventInner for WebsysCompositionEvent {
|
||||
fn data(&self) -> String {
|
||||
self.0.data().unwrap_or_else(|| String::new())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WebsysKeyboardEvent(pub web_sys::KeyboardEvent);
|
||||
impl KeyboardEventInner for WebsysKeyboardEvent {
|
||||
fn alt_key(&self) -> bool {
|
||||
self.0.alt_key()
|
||||
}
|
||||
fn char_code(&self) -> u32 {
|
||||
self.0.char_code()
|
||||
}
|
||||
fn key(&self) -> String {
|
||||
self.0.key()
|
||||
}
|
||||
|
||||
fn key_code(&self) -> KeyCode {
|
||||
KeyCode::from_raw_code(self.0.key_code() as u8)
|
||||
}
|
||||
|
||||
fn ctrl_key(&self) -> bool {
|
||||
self.0.ctrl_key()
|
||||
}
|
||||
|
||||
fn get_modifier_state(&self, key_code: &str) -> bool {
|
||||
self.0.get_modifier_state(key_code)
|
||||
}
|
||||
|
||||
fn locale(&self) -> String {
|
||||
if cfg!(debug_assertions) {
|
||||
todo!("Locale is currently not supported. :(")
|
||||
} else {
|
||||
String::from("en-US")
|
||||
}
|
||||
}
|
||||
|
||||
fn location(&self) -> usize {
|
||||
self.0.location() as usize
|
||||
}
|
||||
|
||||
fn meta_key(&self) -> bool {
|
||||
self.0.meta_key()
|
||||
}
|
||||
|
||||
fn repeat(&self) -> bool {
|
||||
self.0.repeat()
|
||||
}
|
||||
|
||||
fn shift_key(&self) -> bool {
|
||||
self.0.shift_key()
|
||||
}
|
||||
|
||||
fn which(&self) -> usize {
|
||||
self.0.which() as usize
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WebsysGenericUiEvent(pub UiEvent);
|
||||
impl GenericEventInner for WebsysGenericUiEvent {
|
||||
fn raw_event(&self) -> &dyn std::any::Any {
|
||||
// self.0.raw_event()
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn bubbles(&self) -> bool {
|
||||
self.0.bubbles()
|
||||
}
|
||||
|
||||
fn cancel_bubble(&self) {
|
||||
self.0.cancel_bubble();
|
||||
}
|
||||
|
||||
fn cancelable(&self) -> bool {
|
||||
self.0.cancelable()
|
||||
}
|
||||
|
||||
fn composed(&self) -> bool {
|
||||
self.0.composed()
|
||||
}
|
||||
|
||||
fn current_target(&self) {
|
||||
// self.0.current_target()
|
||||
}
|
||||
|
||||
fn default_prevented(&self) -> bool {
|
||||
self.0.default_prevented()
|
||||
}
|
||||
|
||||
fn event_phase(&self) -> u16 {
|
||||
self.0.event_phase()
|
||||
}
|
||||
|
||||
fn is_trusted(&self) -> bool {
|
||||
self.0.is_trusted()
|
||||
}
|
||||
|
||||
fn prevent_default(&self) {
|
||||
self.0.prevent_default()
|
||||
}
|
||||
|
||||
fn stop_immediate_propagation(&self) {
|
||||
self.0.stop_immediate_propagation()
|
||||
}
|
||||
|
||||
fn stop_propagation(&self) {
|
||||
self.0.stop_propagation()
|
||||
}
|
||||
|
||||
fn target(&self) {
|
||||
// self.0.target()
|
||||
}
|
||||
|
||||
fn time_stamp(&self) -> f64 {
|
||||
self.0.time_stamp()
|
||||
}
|
||||
}
|
||||
|
||||
impl UIEventInner for WebsysGenericUiEvent {
|
||||
fn detail(&self) -> i32 {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectionEventInner for WebsysGenericUiEvent {}
|
||||
|
||||
pub struct WebsysFocusEvent(pub web_sys::FocusEvent);
|
||||
impl FocusEventInner for WebsysFocusEvent {}
|
||||
|
||||
pub struct WebsysFormEvent(pub web_sys::InputEvent);
|
||||
impl FormEventInner for WebsysFormEvent {
|
||||
// technically a controlled component, so we need to manually grab out the target data
|
||||
fn value(&self) -> String {
|
||||
let this: web_sys::EventTarget = self.0.target().unwrap();
|
||||
(&this)
|
||||
.dyn_ref()
|
||||
.map(|input: &web_sys::HtmlInputElement| input.value())
|
||||
.or_else(|| {
|
||||
this
|
||||
.dyn_ref()
|
||||
.map(|input: &web_sys::HtmlTextAreaElement| input.value())
|
||||
})
|
||||
.or_else(|| {
|
||||
this
|
||||
.dyn_ref::<web_sys::HtmlElement>()
|
||||
.unwrap()
|
||||
.text_content()
|
||||
})
|
||||
.expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WebsysMouseEvent(pub web_sys::MouseEvent);
|
||||
impl MouseEventInner for WebsysMouseEvent {
|
||||
fn alt_key(&self) -> bool {
|
||||
self.0.alt_key()
|
||||
}
|
||||
fn button(&self) -> i16 {
|
||||
self.0.button()
|
||||
}
|
||||
fn buttons(&self) -> u16 {
|
||||
self.0.buttons()
|
||||
}
|
||||
fn client_x(&self) -> i32 {
|
||||
self.0.client_x()
|
||||
}
|
||||
fn client_y(&self) -> i32 {
|
||||
self.0.client_y()
|
||||
}
|
||||
fn ctrl_key(&self) -> bool {
|
||||
self.0.ctrl_key()
|
||||
}
|
||||
fn meta_key(&self) -> bool {
|
||||
self.0.meta_key()
|
||||
}
|
||||
fn page_x(&self) -> i32 {
|
||||
self.0.page_x()
|
||||
}
|
||||
fn page_y(&self) -> i32 {
|
||||
self.0.page_y()
|
||||
}
|
||||
fn screen_x(&self) -> i32 {
|
||||
self.0.screen_x()
|
||||
}
|
||||
fn screen_y(&self) -> i32 {
|
||||
self.0.screen_y()
|
||||
}
|
||||
fn shift_key(&self) -> bool {
|
||||
self.0.shift_key()
|
||||
}
|
||||
|
||||
// yikes
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
|
||||
fn get_modifier_state(&self, key_code: &str) -> bool {
|
||||
self.0.get_modifier_state(key_code)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WebsysPointerEvent(pub web_sys::PointerEvent);
|
||||
impl PointerEventInner for WebsysPointerEvent {
|
||||
fn alt_key(&self) -> bool {
|
||||
self.0.alt_key()
|
||||
}
|
||||
fn button(&self) -> i16 {
|
||||
self.0.button()
|
||||
}
|
||||
fn buttons(&self) -> u16 {
|
||||
self.0.buttons()
|
||||
}
|
||||
fn client_x(&self) -> i32 {
|
||||
self.0.client_x()
|
||||
}
|
||||
fn client_y(&self) -> i32 {
|
||||
self.0.client_y()
|
||||
}
|
||||
fn ctrl_key(&self) -> bool {
|
||||
self.0.ctrl_key()
|
||||
}
|
||||
fn meta_key(&self) -> bool {
|
||||
self.0.meta_key()
|
||||
}
|
||||
fn page_x(&self) -> i32 {
|
||||
self.0.page_x()
|
||||
}
|
||||
fn page_y(&self) -> i32 {
|
||||
self.0.page_y()
|
||||
}
|
||||
fn screen_x(&self) -> i32 {
|
||||
self.0.screen_x()
|
||||
}
|
||||
fn screen_y(&self) -> i32 {
|
||||
self.0.screen_y()
|
||||
}
|
||||
fn shift_key(&self) -> bool {
|
||||
self.0.shift_key()
|
||||
}
|
||||
|
||||
// yikes
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
|
||||
fn get_modifier_state(&self, key_code: &str) -> bool {
|
||||
self.0.get_modifier_state(key_code)
|
||||
}
|
||||
|
||||
fn pointer_id(&self) -> i32 {
|
||||
self.0.pointer_id()
|
||||
}
|
||||
|
||||
fn width(&self) -> i32 {
|
||||
self.0.width()
|
||||
}
|
||||
|
||||
fn height(&self) -> i32 {
|
||||
self.0.height()
|
||||
}
|
||||
|
||||
fn pressure(&self) -> f32 {
|
||||
self.0.pressure()
|
||||
}
|
||||
|
||||
fn tangential_pressure(&self) -> f32 {
|
||||
self.0.tangential_pressure()
|
||||
}
|
||||
|
||||
fn tilt_x(&self) -> i32 {
|
||||
self.0.tilt_x()
|
||||
}
|
||||
|
||||
fn tilt_y(&self) -> i32 {
|
||||
self.0.tilt_y()
|
||||
}
|
||||
|
||||
fn twist(&self) -> i32 {
|
||||
self.0.twist()
|
||||
}
|
||||
|
||||
fn pointer_type(&self) -> String {
|
||||
self.0.pointer_type()
|
||||
}
|
||||
|
||||
fn is_primary(&self) -> bool {
|
||||
self.0.is_primary()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WebsysWheelEvent(pub web_sys::WheelEvent);
|
||||
impl WheelEventInner for WebsysWheelEvent {
|
||||
fn delta_mode(&self) -> u32 {
|
||||
self.0.delta_mode()
|
||||
}
|
||||
|
||||
fn delta_x(&self) -> f64 {
|
||||
self.0.delta_x()
|
||||
}
|
||||
|
||||
fn delta_y(&self) -> f64 {
|
||||
self.0.delta_y()
|
||||
}
|
||||
|
||||
fn delta_z(&self) -> f64 {
|
||||
self.0.delta_z()
|
||||
}
|
||||
}
|
||||
pub struct WebsysAnimationEvent(pub web_sys::AnimationEvent);
|
||||
impl AnimationEventInner for WebsysAnimationEvent {
|
||||
fn animation_name(&self) -> String {
|
||||
self.0.animation_name()
|
||||
}
|
||||
|
||||
fn pseudo_element(&self) -> String {
|
||||
self.0.pseudo_element()
|
||||
}
|
||||
|
||||
fn elapsed_time(&self) -> f32 {
|
||||
self.0.elapsed_time()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WebsysTransitionEvent(pub web_sys::TransitionEvent);
|
||||
impl TransitionEventInner for WebsysTransitionEvent {
|
||||
fn property_name(&self) -> String {
|
||||
self.0.property_name()
|
||||
}
|
||||
|
||||
fn pseudo_element(&self) -> String {
|
||||
self.0.pseudo_element()
|
||||
}
|
||||
|
||||
fn elapsed_time(&self) -> f32 {
|
||||
self.0.elapsed_time()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WebsysTouchEvent(pub web_sys::TouchEvent);
|
||||
impl TouchEventInner for WebsysTouchEvent {
|
||||
fn alt_key(&self) -> bool {
|
||||
self.0.alt_key()
|
||||
}
|
||||
|
||||
fn ctrl_key(&self) -> bool {
|
||||
self.0.ctrl_key()
|
||||
}
|
||||
|
||||
fn meta_key(&self) -> bool {
|
||||
self.0.meta_key()
|
||||
}
|
||||
|
||||
fn shift_key(&self) -> bool {
|
||||
self.0.shift_key()
|
||||
}
|
||||
|
||||
fn get_modifier_state(&self, key_code: &str) -> bool {
|
||||
if cfg!(debug_assertions) {
|
||||
todo!("get_modifier_state is not currently supported for touch events");
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WebsysMediaEvent(pub web_sys::UiEvent);
|
||||
impl MediaEventInner for WebsysMediaEvent {}
|
||||
|
||||
pub struct WebsysToggleEvent(pub web_sys::UiEvent);
|
||||
impl ToggleEventInner for WebsysToggleEvent {}
|
|
@ -1,49 +1,73 @@
|
|||
//! Dioxus WebSys
|
||||
//! --------------
|
||||
//! This crate implements a renderer of the Dioxus Virtual DOM for the web browser using Websys.
|
||||
|
||||
/*
|
||||
From Google's guide on rAF and rIC:
|
||||
--------
|
||||
|
||||
If the callback is fired at the end of the frame, it will be scheduled to go after the current frame has been committed,
|
||||
which means that style changes will have been applied, and, importantly, layout calculated. If we make DOM changes inside
|
||||
of the idle callback, those layout calculations will be invalidated. If there are any kind of layout reads in the next
|
||||
frame, e.g. getBoundingClientRect, clientWidth, etc, the browser will have to perform a Forced Synchronous Layout,
|
||||
which is a potential performance bottleneck.
|
||||
|
||||
Another reason not trigger DOM changes in the idle callback is that the time impact of changing the DOM is unpredictable,
|
||||
and as such we could easily go past the deadline the browser provided.
|
||||
|
||||
The best practice is to only make DOM changes inside of a requestAnimationFrame callback, since it is scheduled by the
|
||||
browser with that type of work in mind. That means that our code will need to use a document fragment, which can then
|
||||
be appended in the next requestAnimationFrame callback. If you are using a VDOM library, you would use requestIdleCallback
|
||||
to make changes, but you would apply the DOM patches in the next requestAnimationFrame callback, not the idle callback.
|
||||
|
||||
Essentially:
|
||||
------------
|
||||
- Do the VDOM work during the idlecallback
|
||||
- Do DOM work in the next requestAnimationFrame callback
|
||||
*/
|
||||
//!
|
||||
//! ## Overview
|
||||
//! ------------
|
||||
//! This crate implements a renderer of the Dioxus Virtual DOM for the web browser using WebSys. This web render for
|
||||
//! Dioxus is one of the more advanced renderers, supporting:
|
||||
//! - idle work
|
||||
//! - animations
|
||||
//! - jank-free rendering
|
||||
//! - noderefs
|
||||
//! - controlled components
|
||||
//! - re-hydration
|
||||
//! - and more.
|
||||
//!
|
||||
//! The actual implementation is farily thin, with the heavy lifting happening inside the Dioxus Core crate.
|
||||
//!
|
||||
//! To purview the examples, check of the root Dioxus crate - the examples in this crate are mostly meant to provide
|
||||
//! validation of websys-specific features and not the general use of Dioxus.
|
||||
//!
|
||||
//! ## RequestAnimationFrame and RequestIdleCallback
|
||||
//! ------------------------------------------------
|
||||
//! React implements "jank free rendering" by deliberately not blocking the browser's main thread. For large diffs, long
|
||||
//! running work, and integration with things like React-Three-Fiber, it's extremeley important to avoid blocking the
|
||||
//! main thread.
|
||||
//!
|
||||
//! React solves this problem by breaking up the rendering process into a "diff" phase and a "render" phase. In Dioxus,
|
||||
//! the diff phase is non-blocking, using "yield_now" to allow the browser to process other events. When the diff phase
|
||||
//! is finally complete, the VirtualDOM will return a set of "Mutations" for this crate to apply.
|
||||
//!
|
||||
//! Here, we schedule the "diff" phase during the browser's idle period, achieved by calling RequestIdleCallback and then
|
||||
//! setting a timeout from the that completes when the idleperiod is over. Then, we call requestAnimationFrame
|
||||
//!
|
||||
//! From Google's guide on rAF and rIC:
|
||||
//! -----------------------------------
|
||||
//!
|
||||
//! If the callback is fired at the end of the frame, it will be scheduled to go after the current frame has been committed,
|
||||
//! which means that style changes will have been applied, and, importantly, layout calculated. If we make DOM changes inside
|
||||
//! of the idle callback, those layout calculations will be invalidated. If there are any kind of layout reads in the next
|
||||
//! frame, e.g. getBoundingClientRect, clientWidth, etc, the browser will have to perform a Forced Synchronous Layout,
|
||||
//! which is a potential performance bottleneck.
|
||||
//!
|
||||
//! Another reason not trigger DOM changes in the idle callback is that the time impact of changing the DOM is unpredictable,
|
||||
//! and as such we could easily go past the deadline the browser provided.
|
||||
//!
|
||||
//! The best practice is to only make DOM changes inside of a requestAnimationFrame callback, since it is scheduled by the
|
||||
//! browser with that type of work in mind. That means that our code will need to use a document fragment, which can then
|
||||
//! be appended in the next requestAnimationFrame callback. If you are using a VDOM library, you would use requestIdleCallback
|
||||
//! to make changes, but you would apply the DOM patches in the next requestAnimationFrame callback, not the idle callback.
|
||||
//!
|
||||
//! Essentially:
|
||||
//! ------------
|
||||
//! - Do the VDOM work during the idlecallback
|
||||
//! - Do DOM work in the next requestAnimationFrame callback
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
pub use crate::cfg::WebConfig;
|
||||
use crate::dom::load_document;
|
||||
use dioxus::prelude::{Context, Properties, VNode};
|
||||
use cache::intern_cache;
|
||||
use dioxus::prelude::Properties;
|
||||
use dioxus::virtual_dom::VirtualDom;
|
||||
pub use dioxus_core as dioxus;
|
||||
use dioxus_core::error::Result;
|
||||
use dioxus_core::{events::EventTrigger, prelude::FC};
|
||||
use futures_util::{pin_mut, Stream, StreamExt};
|
||||
use fxhash::FxHashMap;
|
||||
use js_sys::Iterator;
|
||||
use web_sys::{window, Document, Element, Event, Node, NodeList};
|
||||
use dioxus_core::prelude::FC;
|
||||
|
||||
mod cache;
|
||||
mod cfg;
|
||||
mod dom;
|
||||
mod events;
|
||||
mod nodeslab;
|
||||
mod ric_raf;
|
||||
|
||||
/// Launches the VirtualDOM from the specified component function.
|
||||
///
|
||||
|
@ -73,16 +97,8 @@ where
|
|||
F: FnOnce(WebConfig) -> WebConfig,
|
||||
{
|
||||
let config = config(WebConfig::default());
|
||||
let fut = run_with_props(root, root_props, config);
|
||||
|
||||
wasm_bindgen_futures::spawn_local(async {
|
||||
match fut.await {
|
||||
Ok(_) => log::error!("Your app completed running... somehow?"),
|
||||
Err(e) => log::error!("Your app crashed! {}", e),
|
||||
}
|
||||
});
|
||||
wasm_bindgen_futures::spawn_local(run_with_props(root, root_props, config));
|
||||
}
|
||||
|
||||
/// This method is the primary entrypoint for Websys Dioxus apps. Will panic if an error occurs while rendering.
|
||||
/// See DioxusErrors for more information on how these errors could occour.
|
||||
///
|
||||
|
@ -96,43 +112,38 @@ where
|
|||
///
|
||||
/// Run the app to completion, panicing if any error occurs while rendering.
|
||||
/// Pairs well with the wasm_bindgen async handler
|
||||
pub async fn run_with_props<T: Properties + 'static>(
|
||||
root: FC<T>,
|
||||
root_props: T,
|
||||
cfg: WebConfig,
|
||||
) -> Result<()> {
|
||||
pub async fn run_with_props<T: Properties + 'static>(root: FC<T>, root_props: T, cfg: WebConfig) {
|
||||
let mut dom = VirtualDom::new_with_props(root, root_props);
|
||||
|
||||
intern_cache();
|
||||
|
||||
let hydrating = cfg.hydrate;
|
||||
|
||||
let root_el = load_document().get_element_by_id(&cfg.rootname).unwrap();
|
||||
|
||||
let tasks = dom.get_event_sender();
|
||||
let sender_callback = Rc::new(move |event| tasks.unbounded_send(event).unwrap());
|
||||
|
||||
let mut real = RealDomWebsys {};
|
||||
let mut websys_dom = dom::WebsysDom::new(root_el, cfg, sender_callback);
|
||||
|
||||
// initialize the virtualdom first
|
||||
if cfg.hydrate {
|
||||
dom.rebuild_in_place()?;
|
||||
let mut mutations = dom.rebuild();
|
||||
|
||||
// hydrating is simply running the dom for a single render. If the page is already written, then the corresponding
|
||||
// ElementIds should already line up because the web_sys dom has already loaded elements with the DioxusID into memory
|
||||
if !hydrating {
|
||||
websys_dom.process_edits(&mut mutations.edits);
|
||||
}
|
||||
|
||||
let mut websys_dom = dom::WebsysDom::new(
|
||||
root_el,
|
||||
cfg,
|
||||
Rc::new(move |event| tasks.unbounded_send(event).unwrap()),
|
||||
);
|
||||
let work_loop = ric_raf::RafLoop::new();
|
||||
loop {
|
||||
// if virtualdom has nothing, wait for it to have something before requesting idle time
|
||||
if !dom.has_work() {
|
||||
dom.wait_for_any_work().await;
|
||||
}
|
||||
|
||||
dom.run_with_deadline(&mut websys_dom).await?;
|
||||
let deadline = work_loop.wait_for_idle_time().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct HydrationNode {
|
||||
id: usize,
|
||||
node: Node,
|
||||
}
|
||||
|
||||
struct RealDomWebsys {}
|
||||
impl dioxus::RealDom for RealDomWebsys {
|
||||
fn raw_node_as_any(&self) -> &mut dyn std::any::Any {
|
||||
todo!()
|
||||
let mut mutations = dom.run_with_deadline(deadline).await;
|
||||
work_loop.wait_for_raf().await;
|
||||
websys_dom.process_edits(&mut mutations[0].edits);
|
||||
}
|
||||
}
|
||||
|
|
58
packages/web/src/ric_raf.rs
Normal file
58
packages/web/src/ric_raf.rs
Normal file
|
@ -0,0 +1,58 @@
|
|||
//! RequestAnimationFrame and RequestIdleCallback port and polyfill.
|
||||
|
||||
use gloo_timers::future::TimeoutFuture;
|
||||
use js_sys::Function;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::{prelude::Closure, JsValue};
|
||||
use web_sys::Window;
|
||||
|
||||
pub struct RafLoop {
|
||||
window: Window,
|
||||
ric_receiver: async_channel::Receiver<()>,
|
||||
raf_receiver: async_channel::Receiver<()>,
|
||||
ric_closure: Closure<dyn Fn(JsValue)>,
|
||||
raf_closure: Closure<dyn Fn(JsValue)>,
|
||||
}
|
||||
|
||||
impl RafLoop {
|
||||
pub fn new() -> Self {
|
||||
let (raf_sender, raf_receiver) = async_channel::unbounded();
|
||||
|
||||
let raf_closure: Closure<dyn Fn(JsValue)> = Closure::wrap(Box::new(move |_v: JsValue| {
|
||||
raf_sender.try_send(()).unwrap()
|
||||
}));
|
||||
|
||||
let (ric_sender, ric_receiver) = async_channel::unbounded();
|
||||
|
||||
let ric_closure: Closure<dyn Fn(JsValue)> = Closure::wrap(Box::new(move |_v: JsValue| {
|
||||
ric_sender.try_send(()).unwrap()
|
||||
}));
|
||||
|
||||
// execute the polyfill for safari
|
||||
Function::new_no_args(include_str!("./ricpolyfill.js"))
|
||||
.call0(&JsValue::NULL)
|
||||
.unwrap();
|
||||
|
||||
Self {
|
||||
window: web_sys::window().unwrap(),
|
||||
raf_receiver,
|
||||
raf_closure,
|
||||
ric_receiver,
|
||||
ric_closure,
|
||||
}
|
||||
}
|
||||
/// waits for some idle time and returns a timeout future that expires after the idle time has passed
|
||||
pub async fn wait_for_idle_time(&self) -> TimeoutFuture {
|
||||
let ric_fn = self.ric_closure.as_ref().dyn_ref::<Function>().unwrap();
|
||||
let deadline: u32 = self.window.request_idle_callback(ric_fn).unwrap();
|
||||
self.ric_receiver.recv().await.unwrap();
|
||||
let deadline = TimeoutFuture::new(deadline);
|
||||
deadline
|
||||
}
|
||||
|
||||
pub async fn wait_for_raf(&self) {
|
||||
let raf_fn = self.raf_closure.as_ref().dyn_ref::<Function>().unwrap();
|
||||
let _id: i32 = self.window.request_animation_frame(raf_fn).unwrap();
|
||||
self.raf_receiver.recv().await.unwrap();
|
||||
}
|
||||
}
|
28
packages/web/src/ricpolyfill.js
Normal file
28
packages/web/src/ricpolyfill.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
const requestIdleCallback =
|
||||
(typeof self !== 'undefined' &&
|
||||
self.requestIdleCallback &&
|
||||
self.requestIdleCallback.bind(window)) ||
|
||||
function (cb) {
|
||||
const start = Date.now();
|
||||
return setTimeout(() => {
|
||||
cb({
|
||||
didTimeout: false,
|
||||
timeRemaining: function () {
|
||||
return Math.max(0, 50 - (Date.now() - start));
|
||||
},
|
||||
});
|
||||
}, 1);
|
||||
};
|
||||
|
||||
const cancelIdleCallback =
|
||||
(typeof self !== 'undefined' &&
|
||||
self.cancelIdleCallback &&
|
||||
self.cancelIdleCallback.bind(window)) ||
|
||||
function (id) {
|
||||
return clearTimeout(id);
|
||||
};
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.requestIdleCallback = requestIdleCallback;
|
||||
window.cancelIdleCallback = cancelIdleCallback;
|
||||
}
|
Loading…
Reference in a new issue