diff --git a/.vscode/spellright.dict b/.vscode/spellright.dict index c15bc778c..348655db4 100644 --- a/.vscode/spellright.dict +++ b/.vscode/spellright.dict @@ -65,3 +65,4 @@ asynchronicity constified SegVec contentful +Jank diff --git a/Cargo.toml b/Cargo.toml index 1286072f5..043c7c802 100644 --- a/Cargo.toml +++ b/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"] diff --git a/README.md b/README.md index 271942169..f1a6e02a1 100644 --- a/README.md +++ b/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 diff --git a/examples/async.rs b/examples/async.rs index 8e6766bd1..111102921 100644 --- a/examples/async.rs +++ b/examples/async.rs @@ -28,7 +28,7 @@ pub static App: FC<()> = |cx| { } button { "Start counting" - onclick: move |_| task.start() + onclick: move |_| task.resume() } button { "Switch counting direcion" diff --git a/examples/hydration.rs b/examples/hydration.rs index dc9188f5a..0f083d933 100644 --- a/examples/hydration.rs +++ b/examples/hydration.rs @@ -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(); diff --git a/examples/reference/noderefs.rs b/examples/reference/noderefs.rs index fdab747b6..195210cfd 100644 --- a/examples/reference/noderefs.rs +++ b/examples/reference/noderefs.rs @@ -2,6 +2,8 @@ use dioxus::prelude::*; fn main() {} pub static Example: FC<()> = |cx| { + let p = 10; + cx.render(rsx! { div { diff --git a/examples/reference/task.rs b/examples/reference/task.rs index e455b6ff8..8f1e61e0a 100644 --- a/examples/reference/task.rs +++ b/examples/reference/task.rs @@ -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" diff --git a/examples/reference/tostring.rs b/examples/reference/tostring.rs index 8827932f2..02b1daf81 100644 --- a/examples/reference/tostring.rs +++ b/examples/reference/tostring.rs @@ -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) }); diff --git a/examples/ssr.rs b/examples/ssr.rs index c5bc8d744..9a74031d4 100644 --- a/examples/ssr.rs +++ b/examples/ssr.rs @@ -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 +} diff --git a/examples/web_tick.rs b/examples/web_tick.rs new file mode 100644 index 000000000..e82f131ec --- /dev/null +++ b/examples/web_tick.rs @@ -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"); +} diff --git a/packages/core-macro/src/rsx/ambiguous.rs b/packages/core-macro/src/rsx/ambiguous.rs index d75b07fc5..bfcfbc374 100644 --- a/packages/core-macro/src/rsx/ambiguous.rs +++ b/packages/core-macro/src/rsx/ambiguous.rs @@ -39,7 +39,8 @@ impl Parse for AmbiguousElement { } } - if let Ok(name) = input.fork().parse::() { + 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 { .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 { } } - if let Ok(name) = input.fork().parse::() { + 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 { .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")) } } diff --git a/packages/core-macro/src/rsx/element.rs b/packages/core-macro/src/rsx/element.rs index 0a6b89d17..4d9a2f09e 100644 --- a/packages/core-macro/src/rsx/element.rs +++ b/packages/core-macro/src/rsx/element.rs @@ -32,6 +32,7 @@ impl Parse for Element { let mut listeners: Vec> = vec![]; let mut children: Vec> = vec![]; let mut key = None; + let mut el_ref = None; 'parsing: loop { // [1] Break if empty @@ -45,6 +46,7 @@ impl Parse for Element { &mut attributes, &mut listeners, &mut key, + &mut el_ref, name.clone(), )?; } else { @@ -237,6 +239,7 @@ fn parse_rsx_element_field( attrs: &mut Vec>, listeners: &mut Vec>, key: &mut Option, + el_ref: &mut Option, 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::()?); + return Ok(()); } // Fall through diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json index 1c68f97cc..79505b018 100644 --- a/packages/core/.vscode/settings.json +++ b/packages/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": true + "rust-analyzer.inlayHints.enable": false } diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 00038baca..3b3c39c28 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -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 diff --git a/packages/core/README.md b/packages/core/README.md index 01152bfc9..62a876453 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -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 {} - } - } - } - } - } - } -} - - - - -``` diff --git a/packages/core/benches/create.rs b/packages/core/benches/create.rs new file mode 100644 index 000000000..f328e4d9d --- /dev/null +++ b/packages/core/benches/create.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/packages/core/benches/jsframework.rs b/packages/core/benches/jsframework.rs new file mode 100644 index 000000000..294a9a952 --- /dev/null +++ b/packages/core/benches/jsframework.rs @@ -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", +]; diff --git a/packages/core/examples/alternative.rs b/packages/core/examples/alternative.rs index fc30beb62..95a8469ff 100644 --- a/packages/core/examples/alternative.rs +++ b/packages/core/examples/alternative.rs @@ -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, ) })) diff --git a/packages/core/examples/async.rs b/packages/core/examples/async.rs index 9c6463e8a..73a83b456 100644 --- a/packages/core/examples/async.rs +++ b/packages/core/examples/async.rs @@ -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(); diff --git a/packages/core/examples/fragment.rs b/packages/core/examples/fragment.rs deleted file mode 100644 index 3d4b97b8b..000000000 --- a/packages/core/examples/fragment.rs +++ /dev/null @@ -1,18 +0,0 @@ - - -fn main() { - - // let g = rsx! { - // Fragment { - // // div {} - // // div {} - // // div {} - // // div {} - // // div {} - // // div {} - // // div {} - // // div {} - // // div {} - // } - // }; -} diff --git a/packages/core/examples/fragment_from_iter.rs b/packages/core/examples/fragment_from_iter.rs index 08018c68c..58c4df70f 100644 --- a/packages/core/examples/fragment_from_iter.rs +++ b/packages/core/examples/fragment_from_iter.rs @@ -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, ) })) diff --git a/packages/core/examples/jsframework.rs b/packages/core/examples/jsframework.rs new file mode 100644 index 000000000..79400afd1 --- /dev/null +++ b/packages/core/examples/jsframework.rs @@ -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", +]; diff --git a/packages/core/examples/macrosrc.rs b/packages/core/examples/macrosrc.rs deleted file mode 100644 index 2a8dfe4cd..000000000 --- a/packages/core/examples/macrosrc.rs +++ /dev/null @@ -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! {
"success!"
}, -// Err(_) => html! {
"failure :("
}, -// } -// }); - -// // todo!() -// cx.render(html! { -//
-//

"Products"

-// -// // Subnodes can even be suspended -// // When completely rendered, they won't cause the component itself to re-render, just their slot - -// //

{ product_list }

-//
-// }) -// } - -// 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! { -//
-// // your template goes here -// // feel free to directly use "name" -//
-// }) -// } - -// fn FullySuspended<'a>(cx: &'a Context) -> 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! {
}, -// 2 => html! {
}, -// _ => html! {
}, -// }; - -// if cx.cx.blah { -// html! {
} -// } else { -// tex -// } -// }) -// } - -// /// An example of a datafetching service -// async fn fetch_data() -> Result { -// todo!() -// } diff --git a/packages/core/examples/vdom_usage.rs b/packages/core/examples/vdom_usage.rs new file mode 100644 index 000000000..c5d643df5 --- /dev/null +++ b/packages/core/examples/vdom_usage.rs @@ -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); +} diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs deleted file mode 100644 index 55c5c31e3..000000000 --- a/packages/core/src/arena.rs +++ /dev/null @@ -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 = Rc>; -type TaskReceiver = futures_channel::mpsc::UnboundedReceiver; -type TaskSender = futures_channel::mpsc::UnboundedSender; - -/// These are resources shared among all the components and the virtualdom itself -#[derive(Clone)] -pub struct SharedResources { - pub components: Rc>>, - - pub(crate) heuristics: Shared, - - /// - pub task_sender: TaskSender, - - pub task_receiver: Shared, - - pub async_tasks: Shared>, - - /// 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>>, - - pub task_setter: Rc, -} - -impl SharedResources { - pub fn new() -> Self { - // preallocate 2000 elements and 20 scopes to avoid dynamic allocation - let components: Rc>> = - 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 - }; - - 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 { - 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 { - 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 { - 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) {} -} diff --git a/packages/core/src/bumpframe.rs b/packages/core/src/bumpframe.rs index 59a6252a5..8cb6d6734 100644 --- a/packages/core/src/bumpframe.rs +++ b/packages/core/src/bumpframe.rs @@ -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, @@ -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; diff --git a/packages/core/src/childiter.rs b/packages/core/src/childiter.rs new file mode 100644 index 000000000..daf5281c4 --- /dev/null +++ b/packages/core/src/childiter.rs @@ -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 + } +} diff --git a/packages/core/src/context.rs b/packages/core/src/context.rs index 5f82c31e5..21d89436e 100644 --- a/packages/core/src/context.rs +++ b/packages/core/src/context.rs @@ -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 { - 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 { - self.scope.vdom.schedule_update() - } - - pub fn schedule_effect(&self) -> Rc { - 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(self) -> Option> { - let mut scope = Some(self.scope); - let mut parent = None; - + let getter = &self.scope.shared.get_shared_context; let ty = TypeId::of::(); - 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::() - .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 diff --git a/packages/core/src/debug_renderer.rs b/packages/core/src/debug_renderer.rs deleted file mode 100644 index 38604cb1c..000000000 --- a/packages/core/src/debug_renderer.rs +++ /dev/null @@ -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(root: FC, 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(&mut self, machine: &mut DiffMachine) -> 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(&self, other: LazyNodes) -> 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(&self, other: LazyNodes) -> 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(&self, other: LazyNodes) -> Result<()> - where - F: for<'b, 'c> FnOnce(&'b NodeFactory<'c>) -> VNode<'c>, - { - Ok(()) - } -} diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 09c4ea92e..bafdfea35 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -1,30 +1,53 @@ //! This module contains the stateful DiffMachine and all methods to diff VNodes, their properties, and their children. -//! The DiffMachine calculates the diffs between the old and new frames, updates the new nodes, and modifies the real dom. +//! +//! The [`DiffMachine`] calculates the diffs between the old and new frames, updates the new nodes, and generates a set +//! of mutations for the RealDom to apply. //! //! ## Notice: +//! //! The inspiration and code for this module was originally taken from Dodrio (@fitzgen) and then modified to support -//! Components, Fragments, Suspense, SubTree memoization, and additional batching operations. +//! Components, Fragments, Suspense, SubTree memoization, incremental diffing, cancelation, NodeRefs, pausing, priority +//! scheduling, and additional batching operations. //! //! ## Implementation Details: //! //! ### IDs for elements //! -------------------- //! All nodes are addressed by their IDs. The RealDom provides an imperative interface for making changes to these nodes. -//! We don't necessarily require that DOM changes happen instnatly during the diffing process, so the implementor may choose -//! to batch nodes if it is more performant for their application. The expectation is that renderers use a Slotmap for nodes -//! whose keys can be converted to u64 on FFI boundaries. +//! We don't necessarily require that DOM changes happen instantly during the diffing process, so the implementor may choose +//! to batch nodes if it is more performant for their application. The element IDs are indicies into the internal element +//! array. The expectation is that implemenetors will use the ID as an index into a Vec of real nodes, allowing for passive +//! garbage collection as the VirtualDOM replaces old nodes. //! -//! When new nodes are created through `render`, they won't know which real node they correspond to. During diffing, we -//! always make sure to copy over the ID. If we don't do this properly, the ElementId will be populated incorrectly and -//! brick the user's page. +//! When new vnodes are created through `cx.render`, they won't know which real node they correspond to. During diffing, +//! we always make sure to copy over the ID. If we don't do this properly, the ElementId will be populated incorrectly +//! and brick the user's page. //! //! ### Fragment Support -//! +//! -------------------- //! Fragments (nodes without a parent) are supported through a combination of "replace with" and anchor vnodes. Fragments -//! can be particularly challenging when they are empty, so the placeholder node lets us "reserve" a spot for the empty +//! can be particularly challenging when they are empty, so the anchor node lets us "reserve" a spot for the empty //! fragment to be replaced with when it is no longer empty. This is guaranteed by logic in the NodeFactory - it is -//! impossible to craft a fragment with 0 elements - they must always have at least a single placeholder element. This is -//! slightly inefficient, but represents a such an uncommon use case that it is not worth optimizing. +//! impossible to craft a fragment with 0 elements - they must always have at least a single placeholder element. Adding +//! "dummy" nodes _is_ inefficient, but it makes our diffing algorithm faster and the implementation is completely up to +//! the platform. +//! +//! Other implementations either don't support fragments or use a "child + sibling" pattern to represent them. Our code is +//! vastly simpler and more performant when we can just create a placeholder element while the fragment has no children. +//! +//! ### Suspense +//! ------------ +//! Dioxus implements suspense slightly differently than React. In React, each fiber is manually progressed until it runs +//! into a promise-like value. React will then work on the next "ready" fiber, checking back on the previous fiber once +//! it has finished its new work. In Dioxus, we use a similar approach, but try to completely render the tree before +//! switching sub-fibers. Instead, each future is submitted into a futures-queue and the node is manually loaded later on. +//! Due to the frequent calls to "yield_now" we can get the pure "fetch-as-you-render" behavior of React fiber. +//! +//! We're able to use this approach because we use placeholder nodes - futures that aren't ready still get submitted to +//! DOM, but as a placeholder. +//! +//! Right now, the "suspense" queue is intertwined the hooks. In the future, we should allow any future to drive attributes +//! and contents, without the need for the "use_suspense" hook. In the interim, this is the quickest way to get suspense working. //! //! ## Subtree Memoization //! ----------------------- @@ -35,13 +58,15 @@ //! rsx!( div { class: "hello world", "this node is entirely static" } ) //! ``` //! Because the subtrees won't be diffed, their "real node" data will be stale (invalid), so its up to the reconciler to -//! track nodes created in a scope and clean up all relevant data. Support for this is currently WIP +//! track nodes created in a scope and clean up all relevant data. Support for this is currently WIP and depends on comp-time +//! hashing of the subtree from the rsx! macro. We do a very limited form of static analysis via static string pointers as +//! a way of short-circuiting the most expensive checks. //! //! ## Bloom Filter and Heuristics //! ------------------------------ //! For all components, we employ some basic heuristics to speed up allocations and pre-size bump arenas. The heuristics are -//! currently very rough, but will get better as time goes on. For FFI, we recommend using a bloom filter to cache strings. -//! +//! currently very rough, but will get better as time goes on. The information currently tracked includes the size of a +//! bump arena after first render, the number of hooks, and the number of nodes in the tree. //! //! ## Garbage Collection //! --------------------- @@ -53,520 +78,504 @@ //! so the client only needs to maintain a simple list of nodes. By default, Dioxus will not manually clean up old nodes //! for the client. As new nodes are created, old nodes will be over-written. //! -//! HEADS-UP: -//! For now, deferred garabge collection is disabled. The code-paths are almost wired up, but it's quite complex to -//! get working safely and efficiently. For now, garabge is collected immediately during diffing. This adds extra -//! overhead, but is faster to implement in the short term. -//! -//! Further Reading and Thoughts +//! ## Further Reading and Thoughts //! ---------------------------- //! There are more ways of increasing diff performance here that are currently not implemented. +//! - Strong memoization of subtrees. +//! - Guided diffing. +//! - Certain web-dom-specific optimizations. +//! //! More info on how to improve this diffing algorithm: //! - https://hacks.mozilla.org/2019/03/fast-bump-allocated-virtual-doms-with-rust-and-wasm/ -use crate::{arena::SharedResources, innerlude::*}; -use futures_util::Future; -use fxhash::{FxBuildHasher, FxHashMap, FxHashSet}; -use smallvec::{smallvec, SmallVec}; - -use std::{any::Any, cell::Cell, cmp::Ordering, marker::PhantomData, pin::Pin}; +use crate::innerlude::*; +use fxhash::{FxHashMap, FxHashSet}; use DomEdit::*; -pub struct DiffMachine<'r, 'bump> { - pub vdom: &'bump SharedResources, - - pub edits: Mutations<'bump>, - - pub scope_stack: SmallVec<[ScopeId; 5]>, - - pub diffed: FxHashSet, - - // will be used later for garbage collection - // we check every seen node and then schedule its eventual deletion +/// Our DiffMachine is an iterative tree differ. +/// +/// It uses techniques of a stack machine to allow pausing and restarting of the diff algorithm. This +/// was origially implemented using recursive techniques, but Rust lacks the abilty to call async functions recursively, +/// meaning we could not "pause" the original diffing algorithm. +/// +/// Instead, we use a traditional stack machine approach to diff and create new nodes. The diff algorithm periodically +/// calls "yield_now" which allows the machine to pause and return control to the caller. The caller can then wait for +/// the next period of idle time, preventing our diff algorithm from blocking the main thread. +/// +/// Funnily enough, this stack machine's entire job is to create instructions for another stack machine to execute. It's +/// stack machines all the way down! +pub(crate) struct DiffMachine<'bump> { + pub vdom: &'bump ResourcePool, + pub mutations: Mutations<'bump>, + pub stack: DiffStack<'bump>, pub seen_scopes: FxHashSet, - - _r: PhantomData<&'r ()>, } -impl<'r, 'bump> DiffMachine<'r, 'bump> { - pub(crate) fn new( - edits: Mutations<'bump>, - cur_scope: ScopeId, - shared: &'bump SharedResources, - ) -> Self { +/// a "saved" form of a diff machine +/// in regular diff machine, the &'bump reference is a stack borrow, but the +/// bump lifetimes are heap borrows. +pub(crate) struct SavedDiffWork<'bump> { + pub mutations: Mutations<'bump>, + pub stack: DiffStack<'bump>, + pub seen_scopes: FxHashSet, +} + +impl<'a> SavedDiffWork<'a> { + pub unsafe fn extend(self: SavedDiffWork<'a>) -> SavedDiffWork<'static> { + std::mem::transmute(self) + } + pub unsafe fn promote<'b>(self, vdom: &'b mut ResourcePool) -> DiffMachine<'b> { + let extended: SavedDiffWork<'b> = std::mem::transmute(self); + DiffMachine { + vdom, + mutations: extended.mutations, + stack: extended.stack, + seen_scopes: extended.seen_scopes, + } + } +} + +impl<'bump> DiffMachine<'bump> { + pub(crate) fn new(edits: Mutations<'bump>, shared: &'bump ResourcePool) -> Self { Self { - edits, - scope_stack: smallvec![cur_scope], + stack: DiffStack::new(), + mutations: edits, vdom: shared, - diffed: FxHashSet::default(), seen_scopes: FxHashSet::default(), - _r: PhantomData, } } - /// Allows the creation of a diff machine without the concept of scopes or a virtualdom - /// this is mostly useful for testing + pub fn save(self) -> SavedDiffWork<'bump> { + SavedDiffWork { + mutations: self.mutations, + stack: self.stack, + seen_scopes: self.seen_scopes, + } + } + + pub fn diff_scope(&mut self, id: ScopeId) { + if let Some(component) = self.vdom.get_scope_mut(id) { + let (old, new) = (component.frames.wip_head(), component.frames.fin_head()); + self.stack.push(DiffInstruction::DiffNode { new, old }); + } + } + + /// Progress the diffing for this "fiber" /// - /// This will PANIC if any component elements are passed in. - pub fn new_headless(shared: &'bump SharedResources) -> Self { - Self { - edits: Mutations { edits: Vec::new() }, - scope_stack: smallvec![ScopeId(0)], - vdom: shared, - diffed: FxHashSet::default(), - seen_scopes: FxHashSet::default(), - _r: PhantomData, + /// This method implements a depth-first iterative tree traversal. + /// + /// We do depth-first to maintain high cache locality (nodes were originally generated recursively). + /// + /// Returns a `bool` indicating that the work completed properly. + pub fn work(&mut self, deadline_expired: &mut impl FnMut() -> bool) -> bool { + while let Some(instruction) = self.stack.pop() { + // defer to individual functions so the compiler produces better code + // large functions tend to be difficult for the compiler to work with + match instruction { + DiffInstruction::PopScope => { + self.stack.pop_scope(); + } + + DiffInstruction::DiffNode { old, new, .. } => self.diff_node(old, new), + + DiffInstruction::DiffChildren { old, new } => self.diff_children(old, new), + + DiffInstruction::Create { node } => self.create_node(node), + + DiffInstruction::Mount { and } => self.mount(and), + + DiffInstruction::PrepareMoveNode { node } => self.prepare_move_node(node), + }; + + if deadline_expired() { + return false; + } + } + + true + } + + fn prepare_move_node(&mut self, node: &'bump VNode<'bump>) { + for el in RealChildIterator::new(node, self.vdom) { + self.mutations.push_root(el.mounted_id()); + self.stack.add_child_count(1); } } - // - pub fn diff_scope(&mut self, id: ScopeId) -> Result<()> { - let component = self.get_scope_mut(&id).ok_or_else(|| Error::NotMounted)?; - let (old, new) = (component.frames.wip_head(), component.frames.fin_head()); - self.diff_node(old, new); - Ok(()) + fn mount(&mut self, and: MountType<'bump>) { + let nodes_created = self.stack.pop_nodes_created(); + match and { + // add the nodes from this virtual list to the parent + // used by fragments and components + MountType::Absorb => { + self.stack.add_child_count(nodes_created); + } + MountType::Append => { + self.mutations.edits.push(AppendChildren { + many: nodes_created as u32, + }); + } + + MountType::Replace { old } => { + let mut iter = RealChildIterator::new(old, self.vdom); + let first = iter.next().unwrap(); + self.mutations + .replace_with(first.mounted_id(), nodes_created as u32); + self.remove_nodes(iter); + } + + MountType::ReplaceByElementId { el: old } => { + self.mutations.replace_with(old, nodes_created as u32); + } + + MountType::InsertAfter { other_node } => { + let root = self.find_last_element(other_node).unwrap(); + self.mutations.insert_after(root, nodes_created as u32); + } + + MountType::InsertBefore { other_node } => { + let root = self.find_first_element_id(other_node).unwrap(); + self.mutations.insert_before(root, nodes_created as u32); + } + } } - // Diff the `old` node with the `new` node. Emits instructions to modify a - // physical DOM node that reflects `old` into something that reflects `new`. - // - // the real stack should be what it is coming in and out of this function (ideally empty) - // - // each function call assumes the stack is fresh (empty). + // ================================= + // Tools for creating new nodes + // ================================= + + fn create_node(&mut self, node: &'bump VNode<'bump>) { + match node { + VNode::Text(vtext) => self.create_text_node(vtext), + VNode::Suspended(suspended) => self.create_suspended_node(suspended), + VNode::Anchor(anchor) => self.create_anchor_node(anchor), + VNode::Element(element) => self.create_element_node(element), + VNode::Fragment(frag) => self.create_fragment_node(frag), + VNode::Component(component) => self.create_component_node(component), + } + } + + fn create_text_node(&mut self, vtext: &'bump VText<'bump>) { + let real_id = self.vdom.reserve_node(); + self.mutations.create_text_node(vtext.text, real_id); + vtext.dom_id.set(Some(real_id)); + self.stack.add_child_count(1); + } + + fn create_suspended_node(&mut self, suspended: &'bump VSuspended) { + let real_id = self.vdom.reserve_node(); + self.mutations.create_placeholder(real_id); + suspended.dom_id.set(Some(real_id)); + self.stack.add_child_count(1); + } + + fn create_anchor_node(&mut self, anchor: &'bump VAnchor) { + let real_id = self.vdom.reserve_node(); + self.mutations.create_placeholder(real_id); + anchor.dom_id.set(Some(real_id)); + self.stack.add_child_count(1); + } + + fn create_element_node(&mut self, element: &'bump VElement<'bump>) { + let VElement { + tag_name, + listeners, + attributes, + children, + namespace, + dom_id, + .. + } = element; + + let real_id = self.vdom.reserve_node(); + dom_id.set(Some(real_id)); + + self.mutations.create_element(tag_name, *namespace, real_id); + + self.stack.add_child_count(1); + + if let Some(cur_scope_id) = self.stack.current_scope() { + let scope = self.vdom.get_scope(cur_scope_id).unwrap(); + + listeners.iter().for_each(|listener| { + self.attach_listener_to_scope(listener, scope); + listener.mounted_node.set(Some(real_id)); + self.mutations + .new_event_listener(listener, cur_scope_id.clone()); + }); + } else { + log::warn!("create element called with no scope on the stack - this is an error for a live dom"); + } + + for attr in *attributes { + self.mutations.set_attribute(attr); + } + + if children.len() > 0 { + self.stack.create_children(children, MountType::Append); + } + } + + fn create_fragment_node(&mut self, frag: &'bump VFragment<'bump>) { + self.stack.create_children(frag.children, MountType::Absorb); + } + + fn create_component_node(&mut self, vcomponent: &'bump VComponent<'bump>) { + let caller = vcomponent.caller.clone(); + + let parent_idx = self.stack.current_scope().unwrap(); + + let shared = self.vdom.channel.clone(); + // Insert a new scope into our component list + let parent_scope = self.vdom.get_scope(parent_idx).unwrap(); + let new_idx = self.vdom.insert_scope_with_key(|new_idx| { + let height = parent_scope.height + 1; + Scope::new( + caller, + new_idx, + Some(parent_idx), + height, + ScopeChildren(vcomponent.children), + shared, + ) + }); + + // Actually initialize the caller's slot with the right address + vcomponent.associated_scope.set(Some(new_idx)); + + if !vcomponent.can_memoize { + let cur_scope = self.vdom.get_scope_mut(parent_idx).unwrap(); + let extended = vcomponent as *const VComponent; + let extended: *const VComponent<'static> = unsafe { std::mem::transmute(extended) }; + cur_scope.borrowed_props.borrow_mut().push(extended); + } + + // TODO: + // add noderefs to current noderef list Noderefs + // add effects to current effect list Effects + + let new_component = self.vdom.get_scope_mut(new_idx).unwrap(); + + // Run the scope for one iteration to initialize it + if new_component.run_scope(self.vdom) { + // Take the node that was just generated from running the component + let nextnode = new_component.frames.fin_head(); + self.stack.create_component(new_idx, nextnode); + } + + // Finally, insert this scope as a seen node. + self.seen_scopes.insert(new_idx); + } + + // ================================= + // Tools for diffing nodes + // ================================= + pub fn diff_node(&mut self, old_node: &'bump VNode<'bump>, new_node: &'bump VNode<'bump>) { - match (&old_node.kind, &new_node.kind) { - // Handle the "sane" cases first. - // The rsx and html macros strongly discourage dynamic lists not encapsulated by a "Fragment". - // So the sane (and fast!) cases are where the virtual structure stays the same and is easily diffable. - (VNodeKind::Text(old), VNodeKind::Text(new)) => { - let root = old_node.direct_id(); + use VNode::*; + match (old_node, new_node) { + // Check the most common cases first + (Text(old), Text(new)) => self.diff_text_nodes(old, new), + (Component(old), Component(new)) => self.diff_component_nodes(old, new), + (Fragment(old), Fragment(new)) => self.diff_fragment_nodes(old, new), + (Anchor(old), Anchor(new)) => new.dom_id.set(old.dom_id.get()), + (Suspended(old), Suspended(new)) => new.dom_id.set(old.dom_id.get()), + (Element(old), Element(new)) => self.diff_element_nodes(old, new), - if old.text != new.text { - self.edit_push_root(root); - self.edit_set_text(new.text); - self.edit_pop(); - } - - new.dom_id.set(Some(root)); - } - - (VNodeKind::Element(old), VNodeKind::Element(new)) => { - let root = old_node.direct_id(); - - // If the element type is completely different, the element needs to be re-rendered completely - // This is an optimization React makes due to how users structure their code - // - // This case is rather rare (typically only in non-keyed lists) - if new.tag_name != old.tag_name || new.namespace != old.namespace { - self.replace_node_with_node(root, old_node, new_node); - return; - } - - new.dom_id.set(Some(root)); - - // Don't push the root if we don't have to - let mut has_comitted = false; - let mut please_commit = |edits: &mut Vec| { - if !has_comitted { - has_comitted = true; - edits.push(PushRoot { id: root.as_u64() }); - } - }; - - // Diff Attributes - // - // It's extraordinarily rare to have the number/order of attributes change - // In these cases, we just completely erase the old set and make a new set - // - // TODO: take a more efficient path than this - if old.attributes.len() == new.attributes.len() { - for (old_attr, new_attr) in old.attributes.iter().zip(new.attributes.iter()) { - if old_attr.value != new_attr.value { - please_commit(&mut self.edits.edits); - self.edit_set_attribute(new_attr); - } - } - } else { - // TODO: provide some sort of report on how "good" the diffing was - please_commit(&mut self.edits.edits); - for attribute in old.attributes { - self.edit_remove_attribute(attribute); - } - for attribute in new.attributes { - self.edit_set_attribute(attribute) - } - } - - // Diff listeners - // - // It's extraordinarily rare to have the number/order of listeners change - // In the cases where the listeners change, we completely wipe the data attributes and add new ones - // - // We also need to make sure that all listeners are properly attached to the parent scope (fix_listener) - // - // TODO: take a more efficient path than this - let cur_scope: ScopeId = self.scope_stack.last().unwrap().clone(); - if old.listeners.len() == new.listeners.len() { - for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) { - if old_l.event != new_l.event { - please_commit(&mut self.edits.edits); - self.edit_remove_event_listener(old_l.event); - self.edit_new_event_listener(new_l, cur_scope); - } - new_l.mounted_node.set(old_l.mounted_node.get()); - self.fix_listener(new_l); - } - } else { - please_commit(&mut self.edits.edits); - for listener in old.listeners { - self.edit_remove_event_listener(listener.event); - } - for listener in new.listeners { - listener.mounted_node.set(Some(root)); - self.edit_new_event_listener(listener, cur_scope); - - // Make sure the listener gets attached to the scope list - self.fix_listener(listener); - } - } - - if has_comitted { - self.edit_pop(); - } - - self.diff_children(old.children, new.children); - } - - (VNodeKind::Component(old), VNodeKind::Component(new)) => { - let scope_addr = old.ass_scope.get().unwrap(); - - // Make sure we're dealing with the same component (by function pointer) - if old.user_fc == new.user_fc { - // - self.scope_stack.push(scope_addr); - - // Make sure the new component vnode is referencing the right scope id - new.ass_scope.set(Some(scope_addr)); - - // make sure the component's caller function is up to date - let scope = self.get_scope_mut(&scope_addr).unwrap(); - - scope - .update_scope_dependencies(new.caller.clone(), ScopeChildren(new.children)); - - // React doesn't automatically memoize, but we do. - let compare = old.comparator.unwrap(); - - match compare(new) { - true => { - // the props are the same... - } - false => { - // the props are different... - scope.run_scope().unwrap(); - self.diff_node(scope.frames.wip_head(), scope.frames.fin_head()); - } - } - - self.scope_stack.pop(); - - self.seen_scopes.insert(scope_addr); - } else { - let mut old_iter = RealChildIterator::new(old_node, &self.vdom); - let first = old_iter - .next() - .expect("Components should generate a placeholder root"); - - // remove any leftovers - for to_remove in old_iter { - self.edit_push_root(to_remove.direct_id()); - self.edit_remove(); - } - - // seems like we could combine this into a single instruction.... - self.replace_node_with_node(first.direct_id(), old_node, new_node); - - // Wipe the old one and plant the new one - let old_scope = old.ass_scope.get().unwrap(); - self.destroy_scopes(old_scope); - } - } - - (VNodeKind::Fragment(old), VNodeKind::Fragment(new)) => { - // This is the case where options or direct vnodes might be used. - // In this case, it's faster to just skip ahead to their diff - if old.children.len() == 1 && new.children.len() == 1 { - self.diff_node(&old.children[0], &new.children[0]); - return; - } - - self.diff_children(old.children, new.children); - } - - (VNodeKind::Anchor(old), VNodeKind::Anchor(new)) => { - new.dom_id.set(old.dom_id.get()); - } - - // The strategy here is to pick the first possible node from the previous set and use that as our replace with root - // - // We also walk the "real node" list to make sure all latent roots are claened up - // This covers the case any time a fragment or component shows up with pretty much anything else - // - // This likely isn't the fastest way to go about replacing one node with a virtual node, but the "insane" cases - // are pretty rare. IE replacing a list (component or fragment) with a single node. + // Anything else is just a basic replace and create ( - VNodeKind::Component(_) - | VNodeKind::Fragment(_) - | VNodeKind::Text(_) - | VNodeKind::Element(_) - | VNodeKind::Anchor(_), - VNodeKind::Component(_) - | VNodeKind::Fragment(_) - | VNodeKind::Text(_) - | VNodeKind::Element(_) - | VNodeKind::Anchor(_), - ) => { - self.replace_and_create_many_with_many([old_node], [new_node]); - } - - // TODO - (VNodeKind::Suspended(old), new) => { - // - self.replace_and_create_many_with_many([old_node], [new_node]); - } - // a node that was once real is now suspended - (old, VNodeKind::Suspended(_)) => { - // - self.replace_and_create_many_with_many([old_node], [new_node]); - } + Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_) | Suspended(_), + Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_) | Suspended(_), + ) => self + .stack + .create_node(new_node, MountType::Replace { old: old_node }), } } - // Emit instructions to create the given virtual node. - // - // The change list stack may have any shape upon entering this function: - // - // [...] - // - // When this function returns, the new node is on top of the change list stack: - // - // [... node] - pub fn create_vnode(&mut self, node: &'bump VNode<'bump>) -> CreateMeta { - match &node.kind { - VNodeKind::Text(text) => { - let real_id = self.vdom.reserve_node(); - self.edit_create_text_node(text.text, real_id); - text.dom_id.set(Some(real_id)); - CreateMeta::new(text.is_static, 1) + fn diff_text_nodes(&mut self, old: &'bump VText<'bump>, new: &'bump VText<'bump>) { + let root = old.dom_id.get().unwrap(); + + if old.text != new.text { + self.mutations.push_root(root); + self.mutations.set_text(new.text); + self.mutations.pop(); + } + + new.dom_id.set(Some(root)); + } + + fn diff_element_nodes(&mut self, old: &'bump VElement<'bump>, new: &'bump VElement<'bump>) { + let root = old.dom_id.get().unwrap(); + + // If the element type is completely different, the element needs to be re-rendered completely + // This is an optimization React makes due to how users structure their code + // + // This case is rather rare (typically only in non-keyed lists) + if new.tag_name != old.tag_name || new.namespace != old.namespace { + // maybe make this an instruction? + // issue is that we need the "vnode" but this method only has the velement + self.stack.push_nodes_created(0); + self.stack.push(DiffInstruction::Mount { + and: MountType::ReplaceByElementId { + el: old.dom_id.get().unwrap(), + }, + }); + self.create_element_node(new); + return; + } + + new.dom_id.set(Some(root)); + + // Don't push the root if we don't have to + let mut has_comitted = false; + let mut please_commit = |edits: &mut Vec| { + if !has_comitted { + has_comitted = true; + edits.push(PushRoot { id: root.as_u64() }); } + }; - VNodeKind::Anchor(anchor) => { - let real_id = self.vdom.reserve_node(); - self.edit_create_placeholder(real_id); - anchor.dom_id.set(Some(real_id)); - CreateMeta::new(false, 1) + // Diff Attributes + // + // It's extraordinarily rare to have the number/order of attributes change + // In these cases, we just completely erase the old set and make a new set + // + // TODO: take a more efficient path than this + if old.attributes.len() == new.attributes.len() { + for (old_attr, new_attr) in old.attributes.iter().zip(new.attributes.iter()) { + if old_attr.value != new_attr.value { + please_commit(&mut self.mutations.edits); + self.mutations.set_attribute(new_attr); + } } - - VNodeKind::Element(el) => { - // we have the potential to completely eliminate working on this node in the future(!) - // - // This can only be done if all of the elements properties (attrs, children, listeners, etc) are static - // While creating these things, keep track if we can memoize this element. - // At the end, we'll set this flag on the element to skip it - let mut is_static: bool = true; - - let VElement { - tag_name, - listeners, - attributes, - children, - namespace, - static_attrs: _, - static_children: _, - static_listeners: _, - dom_id, - } = el; - - let real_id = self.vdom.reserve_node(); - self.edit_create_element(tag_name, *namespace, real_id); - dom_id.set(Some(real_id)); - - let cur_scope = self.current_scope().unwrap(); - - listeners.iter().for_each(|listener| { - self.fix_listener(listener); - listener.mounted_node.set(Some(real_id)); - self.edit_new_event_listener(listener, cur_scope.clone()); - - // if the node has an event listener, then it must be visited ? - is_static = false; - }); - - for attr in *attributes { - is_static = is_static && attr.is_static; - self.edit_set_attribute(attr); - } - - // Fast path: if there is a single text child, it is faster to - // create-and-append the text node all at once via setting the - // parent's `textContent` in a single change list instruction than - // to emit three instructions to (1) create a text node, (2) set its - // text content, and finally (3) append the text node to this - // parent. - // - // Notice: this is a web-specific optimization and may be changed in the future - // - // TODO move over - // if children.len() == 1 { - // if let VNodeKind::Text(text) = &children[0].kind { - // self.set_text(text.text); - // return CreateMeta::new(is_static, 1); - // } - // } - - for child in *children { - let child_meta = self.create_vnode(child); - is_static = is_static && child_meta.is_static; - - // append whatever children were generated by this call - self.edit_append_children(child_meta.added_to_stack); - } - - CreateMeta::new(is_static, 1) + } else { + // TODO: provide some sort of report on how "good" the diffing was + please_commit(&mut self.mutations.edits); + for attribute in old.attributes { + self.mutations.remove_attribute(attribute); } + for attribute in new.attributes { + self.mutations.set_attribute(attribute) + } + } - VNodeKind::Component(vcomponent) => { - let caller = vcomponent.caller.clone(); + // Diff listeners + // + // It's extraordinarily rare to have the number/order of listeners change + // In the cases where the listeners change, we completely wipe the data attributes and add new ones + // + // We also need to make sure that all listeners are properly attached to the parent scope (fix_listener) + // + // TODO: take a more efficient path than this - let parent_idx = self.scope_stack.last().unwrap().clone(); + if let Some(cur_scope_id) = self.stack.current_scope() { + let scope = self.vdom.get_scope(cur_scope_id).unwrap(); - // Insert a new scope into our component list - let new_idx = self.vdom.insert_scope_with_key(|new_idx| { - let parent_scope = self.get_scope(&parent_idx).unwrap(); - let height = parent_scope.height + 1; - Scope::new( - caller, - new_idx, - Some(parent_idx), - height, - ScopeChildren(vcomponent.children), - self.vdom.clone(), - ) - }); - - // Actually initialize the caller's slot with the right address - vcomponent.ass_scope.set(Some(new_idx)); - - if !vcomponent.can_memoize { - let cur_scope = self.get_scope_mut(&parent_idx).unwrap(); - let extended = *vcomponent as *const VComponent; - let extended: *const VComponent<'static> = - unsafe { std::mem::transmute(extended) }; - cur_scope.borrowed_props.borrow_mut().push(extended); - } - - // TODO: - // add noderefs to current noderef list Noderefs - // add effects to current effect list Effects - - let new_component = self.get_scope_mut(&new_idx).unwrap(); - - // Run the scope for one iteration to initialize it - match new_component.run_scope() { - Ok(_) => { - // all good, new nodes exist + if old.listeners.len() == new.listeners.len() { + for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) { + if old_l.event != new_l.event { + please_commit(&mut self.mutations.edits); + self.mutations.remove_event_listener(old_l.event); + self.mutations.new_event_listener(new_l, cur_scope_id); } - Err(err) => { - // failed to run. this is the first time the component ran, and it failed - // we manually set its head node to an empty fragment - panic!("failing components not yet implemented"); + new_l.mounted_node.set(old_l.mounted_node.get()); + self.attach_listener_to_scope(new_l, scope); + } + } else { + please_commit(&mut self.mutations.edits); + for listener in old.listeners { + self.mutations.remove_event_listener(listener.event); + } + for listener in new.listeners { + listener.mounted_node.set(Some(root)); + self.mutations.new_event_listener(listener, cur_scope_id); + self.attach_listener_to_scope(listener, scope); + } + } + } + + if has_comitted { + self.mutations.pop(); + } + + self.diff_children(old.children, new.children); + } + + fn diff_component_nodes( + &mut self, + old: &'bump VComponent<'bump>, + new: &'bump VComponent<'bump>, + ) { + let scope_addr = old.associated_scope.get().unwrap(); + + // Make sure we're dealing with the same component (by function pointer) + if old.user_fc == new.user_fc { + // + self.stack.scope_stack.push(scope_addr); + + // Make sure the new component vnode is referencing the right scope id + new.associated_scope.set(Some(scope_addr)); + + // make sure the component's caller function is up to date + let scope = self.vdom.get_scope_mut(scope_addr).unwrap(); + + scope.update_scope_dependencies(new.caller.clone(), ScopeChildren(new.children)); + + // React doesn't automatically memoize, but we do. + let compare = old.comparator.unwrap(); + + match compare(new) { + true => { + // the props are the same... + } + false => { + // the props are different... + if scope.run_scope(self.vdom) { + self.diff_node(scope.frames.wip_head(), scope.frames.fin_head()); } } - - // Take the node that was just generated from running the component - let nextnode = new_component.frames.fin_head(); - - // Push the new scope onto the stack - self.scope_stack.push(new_idx); - - // Run the creation algorithm with this scope on the stack - let meta = self.create_vnode(nextnode); - - // pop the scope off the stack - self.scope_stack.pop(); - - if meta.added_to_stack == 0 { - panic!("Components should *always* generate nodes - even if they fail"); - } - - // Finally, insert this scope as a seen node. - self.seen_scopes.insert(new_idx); - - CreateMeta::new(vcomponent.is_static, meta.added_to_stack) } - // Fragments are the only nodes that can contain dynamic content (IE through curlies or iterators). - // We can never ignore their contents, so the prescence of a fragment indicates that we need always diff them. - // Fragments will just put all their nodes onto the stack after creation - VNodeKind::Fragment(frag) => self.create_children(frag.children), + self.stack.scope_stack.pop(); - VNodeKind::Suspended(VSuspended { node: real_node }) => { - let id = self.vdom.reserve_node(); - self.edit_create_placeholder(id); - real_node.set(Some(id)); - CreateMeta::new(false, 1) - } + self.seen_scopes.insert(scope_addr); + } else { + todo!(); + + // let mut old_iter = RealChildIterator::new(old_node, &self.vdom); + // let first = old_iter + // .next() + // .expect("Components should generate a placeholder root"); + + // // remove any leftovers + // for to_remove in old_iter { + // self.mutations.push_root(to_remove.direct_id()); + // self.mutations.remove(); + // } + + // // seems like we could combine this into a single instruction.... + // self.replace_node_with_node(first.direct_id(), old_node, new_node); + + // // Wipe the old one and plant the new one + // let old_scope = old.ass_scope.get().unwrap(); + // self.destroy_scopes(old_scope); } } - fn create_children(&mut self, children: &'bump [VNode<'bump>]) -> CreateMeta { - let mut is_static = true; - let mut added_to_stack = 0; - - // add them backwards - for child in children.iter().rev() { - let child_meta = self.create_vnode(child); - is_static = is_static && child_meta.is_static; - added_to_stack += child_meta.added_to_stack; + fn diff_fragment_nodes(&mut self, old: &'bump VFragment<'bump>, new: &'bump VFragment<'bump>) { + // This is the case where options or direct vnodes might be used. + // In this case, it's faster to just skip ahead to their diff + if old.children.len() == 1 && new.children.len() == 1 { + self.diff_node(&old.children[0], &new.children[0]); + return; } - CreateMeta { - is_static, - added_to_stack, - } + self.diff_children(old.children, new.children); } - /// Destroy a scope and all of its descendents. - /// - /// Calling this will run the destuctors on all hooks in the tree. - /// It will also add the destroyed nodes to the `seen_nodes` cache to prevent them from being renderered. - fn destroy_scopes(&mut self, old_scope: ScopeId) { - let mut nodes_to_delete = vec![old_scope]; - let mut scopes_to_explore = vec![old_scope]; - - // explore the scope tree breadth first - while let Some(scope_id) = scopes_to_explore.pop() { - // If we're planning on deleting this node, then we don't need to both rendering it - self.seen_scopes.insert(scope_id); - let scope = self.get_scope(&scope_id).unwrap(); - for child in scope.descendents.borrow().iter() { - // Add this node to be explored - scopes_to_explore.push(child.clone()); - - // Also add it for deletion - nodes_to_delete.push(child.clone()); - } - } - - // Delete all scopes that we found as part of this subtree - for node in nodes_to_delete { - log::debug!("Removing scope {:#?}", node); - let _scope = self.vdom.try_remove(node).unwrap(); - // do anything we need to do to delete the scope - // I think we need to run the destructors on the hooks - // TODO - } - } + // ============================================= + // Utilites for creating new diff instructions + // ============================================= // Diff the given set of old and new children. // @@ -580,79 +589,85 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { // If old no anchors are provided, then it's assumed that we can freely append to the parent. // // Remember, non-empty lists does not mean that there are real elements, just that there are virtual elements. + // + // Frament nodes cannot generate empty children lists, so we can assume that when a list is empty, it belongs only + // to an element, and appending makes sense. fn diff_children(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { - const IS_EMPTY: bool = true; - const IS_NOT_EMPTY: bool = false; - - match (old.is_empty(), new.is_empty()) { - (IS_EMPTY, IS_EMPTY) => {} - - // Completely adding new nodes, removing any placeholder if it exists - (IS_EMPTY, IS_NOT_EMPTY) => { - let meta = self.create_children(new); - self.edit_append_children(meta.added_to_stack); + // Remember, fragments can never be empty (they always have a single child) + match (old, new) { + ([], []) => {} + ([], _) => { + self.stack.create_children(new, MountType::Append); } - - // Completely removing old nodes and putting an anchor in its place - // no anchor (old has nodes) and the new is empty - // remove all the old nodes - (IS_NOT_EMPTY, IS_EMPTY) => { + (_, []) => { for node in old { - self.remove_vnode(node); + self.remove_nodes(Some(node)); } } + ([VNode::Anchor(old_anchor)], [VNode::Anchor(new_anchor)]) => { + old_anchor.dom_id.set(new_anchor.dom_id.get()); + } + ([VNode::Anchor(anchor)], _) => { + let el = anchor.dom_id.get().unwrap(); + self.stack + .create_children(new, MountType::ReplaceByElementId { el }); + } + (_, [VNode::Anchor(_)]) => { + self.replace_and_create_many_with_one(old, &new[0]); + } + _ => { + let new_is_keyed = new[0].key().is_some(); + let old_is_keyed = old[0].key().is_some(); - (IS_NOT_EMPTY, IS_NOT_EMPTY) => { - let first_old = &old[0]; - let first_new = &new[0]; + debug_assert!( + new.iter().all(|n| n.key().is_some() == new_is_keyed), + "all siblings must be keyed or all siblings must be non-keyed" + ); + debug_assert!( + old.iter().all(|o| o.key().is_some() == old_is_keyed), + "all siblings must be keyed or all siblings must be non-keyed" + ); - match (&first_old.kind, &first_new.kind) { - // Anchors can only appear in empty fragments - (VNodeKind::Anchor(old_anchor), VNodeKind::Anchor(new_anchor)) => { - old_anchor.dom_id.set(new_anchor.dom_id.get()); - } - - // Replace the anchor with whatever new nodes are coming down the pipe - (VNodeKind::Anchor(anchor), _) => { - self.edit_push_root(anchor.dom_id.get().unwrap()); - let mut added = 0; - for el in new { - let meta = self.create_vnode(el); - added += meta.added_to_stack; - } - self.edit_replace_with(1, added); - } - - // Replace whatever nodes are sitting there with the anchor - (_, VNodeKind::Anchor(anchor)) => { - self.replace_and_create_many_with_many(old, [first_new]); - } - - // Use the complex diff algorithm to diff the nodes - _ => { - let new_is_keyed = new[0].key.is_some(); - let old_is_keyed = old[0].key.is_some(); - - debug_assert!( - new.iter().all(|n| n.key.is_some() == new_is_keyed), - "all siblings must be keyed or all siblings must be non-keyed" - ); - debug_assert!( - old.iter().all(|o| o.key.is_some() == old_is_keyed), - "all siblings must be keyed or all siblings must be non-keyed" - ); - - if new_is_keyed && old_is_keyed { - self.diff_keyed_children(old, new); - } else { - self.diff_non_keyed_children(old, new); - } - } + if new_is_keyed && old_is_keyed { + self.diff_keyed_children(old, new); + } else { + self.diff_non_keyed_children(old, new); } } } } + // Diff children that are not keyed. + // + // The parent must be on the top of the change list stack when entering this + // function: + // + // [... parent] + // + // the change list stack is in the same state when this function returns. + fn diff_non_keyed_children(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { + // Handled these cases in `diff_children` before calling this function. + log::debug!("diffing non-keyed case"); + debug_assert!(!new.is_empty()); + debug_assert!(!old.is_empty()); + + for (new, old) in new.iter().zip(old.iter()).rev() { + self.stack.push(DiffInstruction::DiffNode { new, old }); + } + + if old.len() > new.len() { + self.remove_nodes(&old[new.len()..]); + } else if new.len() > old.len() { + log::debug!("Calling create children on array differences"); + self.stack.create_children( + &new[old.len()..], + MountType::InsertAfter { + other_node: old.last().unwrap(), + }, + ); + } + } + // Diffing "keyed" children. // // With keyed children, we care about whether we delete, move, or create nodes @@ -675,7 +690,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { let mut assert_unique_keys = |children: &'bump [VNode<'bump>]| { keys.clear(); for child in children { - let key = child.key; + let key = child.key(); debug_assert!( key.is_some(), "if any sibling is keyed, all siblings must be keyed" @@ -697,106 +712,77 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { // // `shared_prefix_count` is the count of how many nodes at the start of // `new` and `old` share the same keys. - // - // TODO: just inline this - let shared_prefix_count = match self.diff_keyed_prefix(old, new) { - KeyedPrefixResult::Finished => return, - KeyedPrefixResult::MoreWorkToDo(count) => count, + let (left_offset, right_offset) = match self.diff_keyed_ends(old, new) { + Some(count) => count, + None => return, }; - - // Next, we find out how many of the nodes at the end of the children have - // the same key. We do _not_ diff them yet, since we want to emit the change - // list instructions such that they can be applied in a single pass over the - // DOM. Instead, we just save this information for later. - // - // `shared_suffix_count` is the count of how many nodes at the end of `new` - // and `old` share the same keys. - let shared_suffix_count = old[shared_prefix_count..] - .iter() - .rev() - .zip(new[shared_prefix_count..].iter().rev()) - .take_while(|&(old, new)| old.key == new.key) - .count(); - - let old_shared_suffix_start = old.len() - shared_suffix_count; - let new_shared_suffix_start = new.len() - shared_suffix_count; + log::debug!( + "Left offset, right offset, {}, {}", + left_offset, + right_offset, + ); // Ok, we now hopefully have a smaller range of children in the middle // within which to re-order nodes with the same keys, remove old nodes with // now-unused keys, and create new nodes with fresh keys. self.diff_keyed_middle( - &old[shared_prefix_count..old_shared_suffix_start], - &new[shared_prefix_count..new_shared_suffix_start], - shared_prefix_count, - shared_suffix_count, - old_shared_suffix_start, + &old[left_offset..(old.len() - right_offset)], + &new[left_offset..(new.len() - right_offset)], ); - - // Finally, diff the nodes at the end of `old` and `new` that share keys. - let old_suffix = &old[old_shared_suffix_start..]; - let new_suffix = &new[new_shared_suffix_start..]; - debug_assert_eq!(old_suffix.len(), new_suffix.len()); - if !old_suffix.is_empty() { - self.diff_keyed_suffix(old_suffix, new_suffix, new_shared_suffix_start) - } } - // Diff the prefix of children in `new` and `old` that share the same keys in - // the same order. - // - // The stack is empty upon entry. - fn diff_keyed_prefix( + /// Diff both ends of the children that share keys. + /// + /// Returns a left offset and right offset of that indicates a smaller section to pass onto the middle diffing. + /// + /// If there is no offset, then this function returns None and the diffing is complete. + fn diff_keyed_ends( &mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>], - ) -> KeyedPrefixResult { - let mut shared_prefix_count = 0; + ) -> Option<(usize, usize)> { + let mut left_offset = 0; for (old, new) in old.iter().zip(new.iter()) { // abort early if we finally run into nodes with different keys if old.key() != new.key() { break; } - self.diff_node(old, new); - shared_prefix_count += 1; + self.stack.push(DiffInstruction::DiffNode { old, new }); + left_offset += 1; } // If that was all of the old children, then create and append the remaining // new children and we're finished. - if shared_prefix_count == old.len() { - // Load the last element - let last_node = self.find_last_element(new.last().unwrap()).direct_id(); - self.edit_push_root(last_node); - - // Create the new children and insert them after - let meta = self.create_children(&new[shared_prefix_count..]); - self.edit_insert_after(meta.added_to_stack); - - return KeyedPrefixResult::Finished; + if left_offset == old.len() { + self.stack.create_children( + &new[left_offset..], + MountType::InsertAfter { + other_node: old.last().unwrap(), + }, + ); + return None; } // And if that was all of the new children, then remove all of the remaining // old children and we're finished. - if shared_prefix_count == new.len() { - self.remove_children(&old[shared_prefix_count..]); - return KeyedPrefixResult::Finished; + if left_offset == new.len() { + self.remove_nodes(&old[left_offset..]); + return None; } - KeyedPrefixResult::MoreWorkToDo(shared_prefix_count) - } - - // Create the given children and append them to the parent node. - // - // The parent node must currently be on top of the change list stack: - // - // [... parent] - // - // When this function returns, the change list stack is in the same state. - pub fn create_and_append_children(&mut self, new: &'bump [VNode<'bump>]) { - for child in new { - let meta = self.create_vnode(child); - self.edit_append_children(meta.added_to_stack); + // if the shared prefix is less than either length, then we need to walk backwards + let mut right_offset = 0; + for (old, new) in old.iter().rev().zip(new.iter().rev()) { + // abort early if we finally run into nodes with different keys + if old.key() != new.key() { + break; + } + self.diff_node(old, new); + right_offset += 1; } + + Some((left_offset, right_offset)) } // The most-general, expensive code path for keyed children diffing. @@ -812,391 +798,259 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { // This function will load the appropriate nodes onto the stack and do diffing in place. // // Upon exit from this function, it will be restored to that same state. - fn diff_keyed_middle( - &mut self, - old: &'bump [VNode<'bump>], - mut new: &'bump [VNode<'bump>], - shared_prefix_count: usize, - shared_suffix_count: usize, - old_shared_suffix_start: usize, - ) { + fn diff_keyed_middle(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { + /* + 1. Map the old keys into a numerical ordering based on indicies. + 2. Create a map of old key to its index + 3. Map each new key to the old key, carrying over the old index. + - IE if we have ABCD becomes BACD, our sequence would be 1,0,2,3 + - if we have ABCD to ABDE, our sequence would be 0,1,3,MAX because E doesn't exist + + now, we should have a list of integers that indicates where in the old list the new items map to. + + 4. Compute the LIS of this list + - this indicates the longest list of new children that won't need to be moved. + + 5. Identify which nodes need to be removed + 6. Identify which nodes will need to be diffed + + 7. Going along each item in the new list, create it and insert it before the next closest item in the LIS. + - if the item already existed, just move it to the right place. + + 8. Finally, generate instructions to remove any old children. + 9. Generate instructions to finally diff children that are the same between both + */ + + // 0. Debug sanity checks // Should have already diffed the shared-key prefixes and suffixes. debug_assert_ne!(new.first().map(|n| n.key()), old.first().map(|o| o.key())); debug_assert_ne!(new.last().map(|n| n.key()), old.last().map(|o| o.key())); - // // The algorithm below relies upon using `u32::MAX` as a sentinel - // // value, so if we have that many new nodes, it won't work. This - // // check is a bit academic (hence only enabled in debug), since - // // wasm32 doesn't have enough address space to hold that many nodes - // // in memory. - // debug_assert!(new.len() < u32::MAX as usize); - - // Map from each `old` node's key to its index within `old`. + // 1. Map the old keys into a numerical ordering based on indicies. + // 2. Create a map of old key to its index // IE if the keys were A B C, then we would have (A, 1) (B, 2) (C, 3). - let mut old_key_to_old_index = old + let old_key_to_old_index = old .iter() .enumerate() .map(|(i, o)| (o.key().unwrap(), i)) .collect::>(); - // The set of shared keys between `new` and `old`. let mut shared_keys = FxHashSet::default(); - // let mut to_remove = FxHashSet::default(); - let mut to_add = FxHashSet::default(); - // Map from each index in `new` to the index of the node in `old` that - // has the same key. - let mut new_index_to_old_index = new + // 3. Map each new key to the old key, carrying over the old index. + let new_index_to_old_index = new .iter() - .map(|n| { - let key = n.key().unwrap(); - match old_key_to_old_index.get(&key) { - Some(&index) => { - shared_keys.insert(key); - index - } - None => { - // - to_add.insert(key); - u32::MAX as usize - } + .map(|node| { + let key = node.key().unwrap(); + if let Some(&index) = old_key_to_old_index.get(&key) { + shared_keys.insert(key); + index + } else { + u32::MAX as usize } }) .collect::>(); - dbg!(&shared_keys); - dbg!(&to_add); - - // If none of the old keys are reused by the new children, then we - // remove all the remaining old children and create the new children - // afresh. - if shared_suffix_count == 0 && shared_keys.is_empty() { + // If none of the old keys are reused by the new children, then we remove all the remaining old children and + // create the new children afresh. + if shared_keys.is_empty() { self.replace_and_create_many_with_many(old, new); return; } - // // Remove any old children whose keys were not reused in the new - // // children. Remove from the end first so that we don't mess up indices. - // for old_child in old.iter().rev() { - // if !shared_keys.contains(&old_child.key()) { - // self.remove_child(old_child); - // } - // } - - // let old_keyds = old.iter().map(|f| f.key()).collect::>(); - // let new_keyds = new.iter().map(|f| f.key()).collect::>(); - // dbg!(old_keyds); - // dbg!(new_keyds); - - // // If there aren't any more new children, then we are done! - // if new.is_empty() { - // return; - // } - - // The longest increasing subsequence within `new_index_to_old_index`. This - // is the longest sequence on DOM nodes in `old` that are relatively ordered - // correctly within `new`. We will leave these nodes in place in the DOM, - // and only move nodes that are not part of the LIS. This results in the - // maximum number of DOM nodes left in place, AKA the minimum number of DOM - // nodes moved. - let mut new_index_is_in_lis = FxHashSet::default(); - new_index_is_in_lis.reserve(new_index_to_old_index.len()); + // 4. Compute the LIS of this list + let mut lis_sequence = Vec::default(); + lis_sequence.reserve(new_index_to_old_index.len()); let mut predecessors = vec![0; new_index_to_old_index.len()]; let mut starts = vec![0; new_index_to_old_index.len()]; longest_increasing_subsequence::lis_with( &new_index_to_old_index, - &mut new_index_is_in_lis, + &mut lis_sequence, |a, b| a < b, &mut predecessors, &mut starts, ); - dbg!(&new_index_is_in_lis); - // use the old nodes to navigate the new nodes + // the lis comes out backwards, I think. can't quite tell. + lis_sequence.sort_unstable(); - let mut lis_in_order = new_index_is_in_lis.into_iter().collect::>(); - lis_in_order.sort_unstable(); - - dbg!(&lis_in_order); - - // we walk front to back, creating the head node - - // diff the shared, in-place nodes first - // this makes sure we can rely on their first/last nodes being correct later on - for id in &lis_in_order { - let new_node = &new[*id]; - let key = new_node.key().unwrap(); - let old_index = old_key_to_old_index.get(&key).unwrap(); - let old_node = &old[*old_index]; - self.diff_node(old_node, new_node); + // if a new node gets u32 max and is at the end, then it might be part of our LIS (because u32 max is a valid LIS) + if lis_sequence.last().map(|f| new_index_to_old_index[*f]) == Some(u32::MAX as usize) { + lis_sequence.pop(); } - // return the old node from the key - let load_old_node_from_lsi = |key| -> &VNode { - let old_index = old_key_to_old_index.get(key).unwrap(); - let old_node = &old[*old_index]; - old_node + let apply = |new_idx, new_node: &'bump VNode<'bump>, stack: &mut DiffStack<'bump>| { + let old_index = new_index_to_old_index[new_idx]; + if old_index == u32::MAX as usize { + stack.create_node(new_node, MountType::Absorb); + } else { + // this funciton should never take LIS indicies + stack.push(DiffInstruction::PrepareMoveNode { node: new_node }); + stack.push(DiffInstruction::DiffNode { + new: new_node, + old: &old[old_index], + }); + } }; - let mut root = None; - let mut new_iter = new.iter().enumerate(); - for lis_id in &lis_in_order { - eprintln!("tracking {:?}", lis_id); - // this is the next milestone node we are working up to - let new_anchor = &new[*lis_id]; - root = Some(new_anchor); + // add mount instruction for the last items not covered by the lis + let first_lis = *lis_sequence.first().unwrap(); + if first_lis > 0 { + self.stack.push_nodes_created(0); + self.stack.push(DiffInstruction::Mount { + and: MountType::InsertBefore { + other_node: &new[first_lis], + }, + }); - let anchor_el = self.find_first_element(new_anchor); - self.edit_push_root(anchor_el.direct_id()); - // let mut pushed = false; - - 'inner: loop { - let (next_id, next_new) = new_iter.next().unwrap(); - if next_id == *lis_id { - // we've reached the milestone, break this loop so we can step to the next milestone - // remember: we already diffed this node - eprintln!("breaking {:?}", next_id); - break 'inner; - } else { - let key = next_new.key().unwrap(); - eprintln!("found key {:?}", key); - if shared_keys.contains(&key) { - eprintln!("key is contained {:?}", key); - shared_keys.remove(key); - // diff the two nodes - let old_node = load_old_node_from_lsi(key); - self.diff_node(old_node, next_new); - - // now move all the nodes into the right spot - for child in RealChildIterator::new(next_new, self.vdom) { - let el = child.direct_id(); - self.edit_push_root(el); - self.edit_insert_before(1); - } - } else { - eprintln!("key is not contained {:?}", key); - // new node needs to be created - // insert it before the current milestone - let meta = self.create_vnode(next_new); - self.edit_insert_before(meta.added_to_stack); - } - } - } - - self.edit_pop(); - } - - let final_lis_node = root.unwrap(); - let final_el_node = self.find_last_element(final_lis_node); - let final_el = final_el_node.direct_id(); - self.edit_push_root(final_el); - - let mut last_iter = new.iter().rev().enumerate(); - let last_key = final_lis_node.key().unwrap(); - loop { - let (last_id, last_node) = last_iter.next().unwrap(); - let key = last_node.key().unwrap(); - - eprintln!("checking final nodes {:?}", key); - - if last_key == key { - eprintln!("breaking final nodes"); - break; - } - - if shared_keys.contains(&key) { - eprintln!("key is contained {:?}", key); - shared_keys.remove(key); - // diff the two nodes - let old_node = load_old_node_from_lsi(key); - self.diff_node(old_node, last_node); - - // now move all the nodes into the right spot - for child in RealChildIterator::new(last_node, self.vdom) { - let el = child.direct_id(); - self.edit_push_root(el); - self.edit_insert_after(1); - } - } else { - eprintln!("key is not contained {:?}", key); - // new node needs to be created - // insert it before the current milestone - let meta = self.create_vnode(last_node); - self.edit_insert_after(meta.added_to_stack); + for (idx, new_node) in new[..first_lis].iter().enumerate().rev() { + apply(idx, new_node, &mut self.stack); } } - self.edit_pop(); - } - // Diff the suffix of keyed children that share the same keys in the same order. - // - // The parent must be on the change list stack when we enter this function: - // - // [... parent] - // - // When this function exits, the change list stack remains the same. - fn diff_keyed_suffix( - &mut self, - old: &'bump [VNode<'bump>], - new: &'bump [VNode<'bump>], - new_shared_suffix_start: usize, - ) { - debug_assert_eq!(old.len(), new.len()); - debug_assert!(!old.is_empty()); + // for each spacing, generate a mount instruction + let mut lis_iter = lis_sequence.iter().rev(); + let mut last = *lis_iter.next().unwrap(); + while let Some(&next) = lis_iter.next() { + if last - next > 1 { + self.stack.push_nodes_created(0); + self.stack.push(DiffInstruction::Mount { + and: MountType::InsertBefore { + other_node: &new[last], + }, + }); + for (idx, new_node) in new[(next + 1)..last].iter().enumerate().rev() { + apply(idx + next + 1, new_node, &mut self.stack); + } + } + last = next; + } - for (old_child, new_child) in old.iter().zip(new.iter()) { - self.diff_node(old_child, new_child); + // add mount instruction for the first items not covered by the lis + let last = *lis_sequence.last().unwrap(); + if last < (new.len() - 1) { + self.stack.push_nodes_created(0); + self.stack.push(DiffInstruction::Mount { + and: MountType::InsertAfter { + other_node: &new[last], + }, + }); + for (idx, new_node) in new[(last + 1)..].iter().enumerate().rev() { + apply(idx + last + 1, new_node, &mut self.stack); + } + } + + for idx in lis_sequence.iter().rev() { + self.stack.push(DiffInstruction::DiffNode { + new: &new[*idx], + old: &old[new_index_to_old_index[*idx]], + }); } } - // Diff children that are not keyed. - // - // The parent must be on the top of the change list stack when entering this - // function: - // - // [... parent] - // - // the change list stack is in the same state when this function returns. - fn diff_non_keyed_children(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { - // Handled these cases in `diff_children` before calling this function. - // - debug_assert!(!new.is_empty()); - debug_assert!(!old.is_empty()); + // ===================== + // Utilities + // ===================== - match old.len().cmp(&new.len()) { - // old.len > new.len -> removing some nodes - Ordering::Greater => { - // diff them together - for (new_child, old_child) in new.iter().zip(old.iter()) { - self.diff_node(old_child, new_child); - } - - // todo: we would emit fewer instructions if we just did a replace many - // remove whatever is still dangling - for item in &old[new.len()..] { - for i in RealChildIterator::new(item, self.vdom) { - self.edit_push_root(i.direct_id()); - self.edit_remove(); - } - } - } - - // old.len < new.len -> adding some nodes - // this is wrong in the case where we're diffing fragments - // - // we need to save the last old element and then replace it with all the new ones - Ordering::Less => { - // Add the new elements to the last old element while it still exists - let last = self.find_last_element(old.last().unwrap()); - self.edit_push_root(last.direct_id()); - - // create the rest and insert them - let meta = self.create_children(&new[old.len()..]); - self.edit_insert_after(meta.added_to_stack); - - self.edit_pop(); - - // diff the rest - new.iter() - .zip(old.iter()) - .for_each(|(new_child, old_child)| self.diff_node(old_child, new_child)); - } - - // old.len == new.len -> no nodes added/removed, but perhaps changed - Ordering::Equal => { - for (new_child, old_child) in new.iter().zip(old.iter()) { - self.diff_node(old_child, new_child); - } - } - } - } - - // ====================== - // Support methods - // ====================== - // Remove all of a node's children. - // - // The change list stack must have this shape upon entry to this function: - // - // [... parent] - // - // When this function returns, the change list stack is in the same state. - fn remove_all_children(&mut self, old: &'bump [VNode<'bump>]) { - // debug_assert!(self.traversal_is_committed()); - log::debug!("REMOVING CHILDREN"); - for _child in old { - // registry.remove_subtree(child); - } - // Fast way to remove all children: set the node's textContent to an empty - // string. - todo!() - // self.set_inner_text(""); - } - // Remove the current child and all of its following siblings. - // - // The change list stack must have this shape upon entry to this function: - // - // [... parent child] - // - // After the function returns, the child is no longer on the change list stack: - // - // [... parent] - fn remove_children(&mut self, old: &'bump [VNode<'bump>]) { - self.replace_and_create_many_with_many(old, None) - } - - fn find_last_element(&mut self, vnode: &'bump VNode<'bump>) -> &'bump VNode<'bump> { + fn find_last_element(&mut self, vnode: &'bump VNode<'bump>) -> Option { let mut search_node = Some(vnode); loop { - let node = search_node.take().unwrap(); - match &node.kind { - // the ones that have a direct id - VNodeKind::Text(_) - | VNodeKind::Element(_) - | VNodeKind::Anchor(_) - | VNodeKind::Suspended(_) => break node, + match &search_node.take().unwrap() { + VNode::Text(t) => break t.dom_id.get(), + VNode::Element(t) => break t.dom_id.get(), + VNode::Suspended(t) => break t.dom_id.get(), + VNode::Anchor(t) => break t.dom_id.get(), - VNodeKind::Fragment(frag) => { + VNode::Fragment(frag) => { search_node = frag.children.last(); } - VNodeKind::Component(el) => { - let scope_id = el.ass_scope.get().unwrap(); - let scope = self.get_scope(&scope_id).unwrap(); + VNode::Component(el) => { + let scope_id = el.associated_scope.get().unwrap(); + let scope = self.vdom.get_scope(scope_id).unwrap(); search_node = Some(scope.root()); } } } } - fn find_first_element(&mut self, vnode: &'bump VNode<'bump>) -> &'bump VNode<'bump> { + fn find_first_element_id(&mut self, vnode: &'bump VNode<'bump>) -> Option { let mut search_node = Some(vnode); loop { - let node = search_node.take().unwrap(); - match &node.kind { + match &search_node.take().unwrap() { // the ones that have a direct id - VNodeKind::Text(_) - | VNodeKind::Element(_) - | VNodeKind::Anchor(_) - | VNodeKind::Suspended(_) => break node, - - VNodeKind::Fragment(frag) => { + VNode::Fragment(frag) => { search_node = Some(&frag.children[0]); } - VNodeKind::Component(el) => { - let scope_id = el.ass_scope.get().unwrap(); - let scope = self.get_scope(&scope_id).unwrap(); + VNode::Component(el) => { + let scope_id = el.associated_scope.get().unwrap(); + let scope = self.vdom.get_scope(scope_id).unwrap(); search_node = Some(scope.root()); } + VNode::Text(t) => break t.dom_id.get(), + VNode::Element(t) => break t.dom_id.get(), + VNode::Suspended(t) => break t.dom_id.get(), + VNode::Anchor(t) => break t.dom_id.get(), } } } - fn remove_child(&mut self, node: &'bump VNode<'bump>) { - self.replace_and_create_many_with_many(Some(node), None); + fn replace_and_create_many_with_one( + &mut self, + old: &'bump [VNode<'bump>], + new: &'bump VNode<'bump>, + ) { + if let Some(first_old) = old.get(0) { + self.remove_nodes(&old[1..]); + self.stack + .create_node(new, MountType::Replace { old: first_old }); + } else { + self.stack.create_node(new, MountType::Append {}); + } + } + + /// schedules nodes for garbage collection and pushes "remove" to the mutation stack + /// remove can happen whenever + fn remove_nodes(&mut self, nodes: impl IntoIterator>) { + // or cache the vec on the diff machine + for node in nodes { + match node { + VNode::Text(t) => { + t.dom_id.get().map(|id| { + self.mutations.remove(id.as_u64()); + self.vdom.collect_garbage(id); + }); + } + VNode::Suspended(s) => { + s.dom_id.get().map(|id| { + self.mutations.remove(id.as_u64()); + self.vdom.collect_garbage(id); + }); + } + VNode::Anchor(a) => { + a.dom_id.get().map(|id| { + self.mutations.remove(id.as_u64()); + self.vdom.collect_garbage(id); + }); + } + VNode::Element(e) => { + e.dom_id.get().map(|id| self.mutations.remove(id.as_u64())); + } + VNode::Fragment(f) => { + self.remove_nodes(f.children); + } + + VNode::Component(c) => { + let scope_id = c.associated_scope.get().unwrap(); + let scope = self.vdom.get_scope(scope_id).unwrap(); + let root = scope.root(); + self.remove_nodes(Some(root)); + } + } + } } /// Remove all the old nodes and replace them with newly created new nodes. @@ -1204,63 +1058,24 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { /// The new nodes *will* be created - don't create them yourself! fn replace_and_create_many_with_many( &mut self, - old_nodes: impl IntoIterator>, - new_nodes: impl IntoIterator>, + old: &'bump [VNode<'bump>], + new: &'bump [VNode<'bump>], ) { - let mut nodes_to_replace = Vec::new(); - let mut nodes_to_search = old_nodes.into_iter().collect::>(); - let mut scopes_obliterated = Vec::new(); - while let Some(node) = nodes_to_search.pop() { - match &node.kind { - // the ones that have a direct id return immediately - VNodeKind::Text(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), - VNodeKind::Element(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), - VNodeKind::Anchor(el) => nodes_to_replace.push(el.dom_id.get().unwrap()), - VNodeKind::Suspended(el) => nodes_to_replace.push(el.node.get().unwrap()), - - // Fragments will either have a single anchor or a list of children - VNodeKind::Fragment(frag) => { - for child in frag.children { - nodes_to_search.push(child); - } - } - - // Components can be any of the nodes above - // However, we do need to track which components need to be removed - VNodeKind::Component(el) => { - let scope_id = el.ass_scope.get().unwrap(); - let scope = self.get_scope(&scope_id).unwrap(); - let root = scope.root(); - nodes_to_search.push(root); - scopes_obliterated.push(scope_id); - } - } - // TODO: enable internal garabge collection - // self.create_garbage(node); - } - - let n = nodes_to_replace.len(); - for node in nodes_to_replace { - self.edit_push_root(node); - } - - let mut nodes_created = 0; - for node in new_nodes { - let meta = self.create_vnode(node); - nodes_created += meta.added_to_stack; - } - - // if 0 nodes are created, then it gets interperted as a deletion - self.edit_replace_with(n as u32, nodes_created); - - // obliterate! - for scope in scopes_obliterated { - self.destroy_scopes(scope); + if let Some(first_old) = old.get(0) { + self.remove_nodes(&old[1..]); + self.stack + .create_children(new, MountType::Replace { old: first_old }) + } else { + self.stack.create_children(new, MountType::Append {}); } } fn create_garbage(&mut self, node: &'bump VNode<'bump>) { - match self.current_scope().and_then(|id| self.get_scope(&id)) { + match self + .stack + .current_scope() + .and_then(|id| self.vdom.get_scope(id)) + { Some(scope) => { let garbage: &'bump VNode<'static> = unsafe { std::mem::transmute(node) }; scope.pending_garbage.borrow_mut().push(garbage); @@ -1271,366 +1086,43 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> { } } - fn immediately_dispose_garabage(&mut self, node: ElementId) { - self.vdom.collect_garbage(node) + /// Adds a listener closure to a scope during diff. + fn attach_listener_to_scope<'a>(&mut self, listener: &'a Listener<'a>, scope: &Scope) { + let mut queue = scope.listeners.borrow_mut(); + let long_listener: &'a Listener<'static> = unsafe { std::mem::transmute(listener) }; + queue.push(long_listener as *const _) } - fn replace_node_with_node( - &mut self, - anchor: ElementId, - old_node: &'bump VNode<'bump>, - new_node: &'bump VNode<'bump>, - ) { - self.edit_push_root(anchor); - let meta = self.create_vnode(new_node); - self.edit_replace_with(1, meta.added_to_stack); - self.create_garbage(old_node); - self.edit_pop(); - } + /// Destroy a scope and all of its descendents. + /// + /// Calling this will run the destuctors on all hooks in the tree. + /// It will also add the destroyed nodes to the `seen_nodes` cache to prevent them from being renderered. + fn destroy_scopes(&mut self, old_scope: ScopeId) { + let mut nodes_to_delete = vec![old_scope]; + let mut scopes_to_explore = vec![old_scope]; - fn remove_vnode(&mut self, node: &'bump VNode<'bump>) { - match &node.kind { - VNodeKind::Text(el) => self.immediately_dispose_garabage(node.direct_id()), - VNodeKind::Element(el) => { - self.immediately_dispose_garabage(node.direct_id()); - for child in el.children { - self.remove_vnode(&child); - } - } - VNodeKind::Anchor(a) => { - // - } - VNodeKind::Fragment(frag) => { - for child in frag.children { - self.remove_vnode(&child); - } - } - VNodeKind::Component(el) => { - // - // self.destroy_scopes(old_scope) - } - VNodeKind::Suspended(_) => todo!(), - } - } + // explore the scope tree breadth first + while let Some(scope_id) = scopes_to_explore.pop() { + // If we're planning on deleting this node, then we don't need to both rendering it + self.seen_scopes.insert(scope_id); + let scope = self.vdom.get_scope(scope_id).unwrap(); + for child in scope.descendents.borrow().iter() { + // Add this node to be explored + scopes_to_explore.push(child.clone()); - fn current_scope(&self) -> Option { - self.scope_stack.last().map(|f| f.clone()) - } - - fn fix_listener<'a>(&mut self, listener: &'a Listener<'a>) { - let scope_id = self.current_scope(); - if let Some(scope_id) = scope_id { - let scope = self.get_scope(&scope_id).unwrap(); - let mut queue = scope.listeners.borrow_mut(); - let long_listener: &'a Listener<'static> = unsafe { std::mem::transmute(listener) }; - queue.push(long_listener as *const _) - } - } - - pub fn get_scope_mut(&mut self, id: &ScopeId) -> Option<&'bump mut Scope> { - // ensure we haven't seen this scope before - // if we have, then we're trying to alias it, which is not allowed - debug_assert!(!self.seen_scopes.contains(id)); - - unsafe { self.vdom.get_scope_mut(*id) } - } - pub fn get_scope(&mut self, id: &ScopeId) -> Option<&'bump Scope> { - // ensure we haven't seen this scope before - // if we have, then we're trying to alias it, which is not allowed - unsafe { self.vdom.get_scope(*id) } - } - - // Navigation - pub(crate) fn edit_push_root(&mut self, root: ElementId) { - let id = root.as_u64(); - self.edits.edits.push(PushRoot { id }); - } - - pub(crate) fn edit_pop(&mut self) { - self.edits.edits.push(PopRoot {}); - } - - // Add Nodes to the dom - // add m nodes from the stack - pub(crate) fn edit_append_children(&mut self, many: u32) { - self.edits.edits.push(AppendChildren { many }); - } - - // replace the n-m node on the stack with the m nodes - // ends with the last element of the chain on the top of the stack - pub(crate) fn edit_replace_with(&mut self, n: u32, m: u32) { - self.edits.edits.push(ReplaceWith { n, m }); - } - - pub(crate) fn edit_insert_after(&mut self, n: u32) { - self.edits.edits.push(InsertAfter { n }); - } - - pub(crate) fn edit_insert_before(&mut self, n: u32) { - self.edits.edits.push(InsertBefore { n }); - } - - // Remove Nodesfrom the dom - pub(crate) fn edit_remove(&mut self) { - self.edits.edits.push(Remove); - } - - // Create - pub(crate) fn edit_create_text_node(&mut self, text: &'bump str, id: ElementId) { - let id = id.as_u64(); - self.edits.edits.push(CreateTextNode { text, id }); - } - - pub(crate) fn edit_create_element( - &mut self, - tag: &'static str, - ns: Option<&'static str>, - id: ElementId, - ) { - let id = id.as_u64(); - match ns { - Some(ns) => self.edits.edits.push(CreateElementNs { id, ns, tag }), - None => self.edits.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 edit_create_placeholder(&mut self, id: ElementId) { - let id = id.as_u64(); - self.edits.edits.push(CreatePlaceholder { id }); - } - - // events - pub(crate) fn edit_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.edits.push(NewEventListener { - scope, - event_name: event, - mounted_node_id: element_id, - }); - } - - pub(crate) fn edit_remove_event_listener(&mut self, event: &'static str) { - self.edits.edits.push(RemoveEventListener { event }); - } - - // modify - pub(crate) fn edit_set_text(&mut self, text: &'bump str) { - self.edits.edits.push(SetText { text }); - } - - pub(crate) fn edit_set_attribute(&mut self, attribute: &'bump Attribute) { - let Attribute { - name, - value, - is_static, - is_volatile, - namespace, - } = attribute; - // field: &'static str, - // value: &'bump str, - // ns: Option<&'static str>, - self.edits.edits.push(SetAttribute { - field: name, - value, - ns: *namespace, - }); - } - - pub(crate) fn edit_set_attribute_ns( - &mut self, - attribute: &'bump Attribute, - namespace: &'bump str, - ) { - let Attribute { - name, - value, - is_static, - is_volatile, - // namespace, - .. - } = attribute; - // field: &'static str, - // value: &'bump str, - // ns: Option<&'static str>, - self.edits.edits.push(SetAttribute { - field: name, - value, - ns: Some(namespace), - }); - } - - pub(crate) fn edit_remove_attribute(&mut self, attribute: &Attribute) { - let name = attribute.name; - self.edits.edits.push(RemoveAttribute { name }); - } -} - -// When we create new nodes, we need to propagate some information back up the call chain. -// This gives the caller some information on how to handle things like insertins, appending, and subtree discarding. -#[derive(Debug)] -pub struct CreateMeta { - pub is_static: bool, - pub added_to_stack: u32, -} - -impl CreateMeta { - fn new(is_static: bool, added_to_tack: u32) -> Self { - Self { - is_static, - added_to_stack: added_to_tack, - } - } -} - -enum KeyedPrefixResult { - // Fast path: we finished diffing all the children just by looking at the - // prefix of shared keys! - Finished, - // There is more diffing work to do. Here is a count of how many children at - // the beginning of `new` and `old` we already processed. - MoreWorkToDo(usize), -} - -fn find_first_real_node<'a>( - nodes: impl IntoIterator>, - scopes: &'a SharedResources, -) -> Option<&'a VNode<'a>> { - for node in nodes { - let mut iter = RealChildIterator::new(node, scopes); - if let Some(node) = iter.next() { - return Some(node); - } - } - - None -} - -/// This iterator iterates through a list of virtual children and only returns real children (Elements, 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 struct RealChildIterator<'a> { - scopes: &'a SharedResources, - - // 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 SharedResources) -> Self { - Self { - scopes, - stack: smallvec::smallvec![(0, starter)], - } - } - // keep the memory around - pub fn reset_with(&mut self, node: &'a VNode<'a>) { - self.stack.clear(); - self.stack.push((0, node)); - } -} - -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.kind { - // We can only exit our looping when we get "real" nodes - // This includes fragments and components when they're empty (have a single root) - VNodeKind::Element(_) | VNodeKind::Text(_) => { - // We've recursed INTO an element/text - // We need to recurse *out* of it and move forward to the next - should_pop = true; - returned_node = Some(&*node); - } - - // If we get a fragment we push the next child - VNodeKind::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]); - } - } - // // If we get a fragment we push the next child - // VNodeKind::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]); - // } - // } - - // Immediately abort suspended nodes - can't do anything with them yet - VNodeKind::Suspended(node) => { - // VNodeKind::Suspended => should_pop = true, - todo!() - } - - VNodeKind::Anchor(a) => { - todo!() - } - - // For components, we load their root and push them onto the stack - VNodeKind::Component(sc) => { - let scope = - unsafe { self.scopes.get_scope(sc.ass_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; + // Also add it for deletion + nodes_to_delete.push(child.clone()); } } - returned_node + // Delete all scopes that we found as part of this subtree + for node in nodes_to_delete { + log::debug!("Removing scope {:#?}", node); + let _scope = self.vdom.try_remove(node).unwrap(); + // do anything we need to do to delete the scope + // I think we need to run the destructors on the hooks + // TODO + } } } diff --git a/packages/core/src/diff_stack.rs b/packages/core/src/diff_stack.rs new file mode 100644 index 000000000..5c4f39646 --- /dev/null +++ b/packages/core/src/diff_stack.rs @@ -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>, + 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> { + self.instructions.pop() + } + + pub fn pop_scope(&mut self) -> Option { + 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 { + 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 }); + } +} diff --git a/packages/core/src/editor.rs b/packages/core/src/editor.rs deleted file mode 100644 index 3bc959b4a..000000000 --- a/packages/core/src/editor.rs +++ /dev/null @@ -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", - } - } -} diff --git a/packages/core/src/error.rs b/packages/core/src/error.rs deleted file mode 100644 index 8e675b026..000000000 --- a/packages/core/src/error.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! Internal error handling for Dioxus -//! -//! - -use thiserror::Error as ThisError; -pub type Result = std::result::Result; - -#[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), -} diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 3fe04f28f..119afbf35 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -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, + pub mounted_dom_id: Option, + + /// 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 { - 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, - 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>>, - }, - - // 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 = unsafe { BumpBox::from_raw(cb) }; + let callback: BumpBox = 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); + 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: + /// + /// + /// # 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: - /// - /// - /// # 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, diff --git a/packages/core/src/hooklist.rs b/packages/core/src/hooklist.rs index 06ed54d32..ebfe5723a 100644 --- a/packages/core/src/hooklist.rs +++ b/packages/core/src/hooklist.rs @@ -1,10 +1,10 @@ use std::{ any::Any, - cell::{Cell, UnsafeCell}, + cell::{Cell, RefCell, UnsafeCell}, }; -pub struct HookList { - vals: appendlist::AppendList>>, +pub(crate) struct HookList { + vals: RefCell>>>, idx: Cell, } @@ -20,6 +20,7 @@ impl Default for HookList { struct InnerHook { cell: UnsafeCell, } + impl InnerHook { fn new(new: T) -> Self { Self { @@ -29,33 +30,16 @@ impl InnerHook { } 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(&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::() - }) - } - pub(crate) fn next(&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::() }) } - #[inline] pub(crate) fn push(&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] diff --git a/packages/core/src/hooks.rs b/packages/core/src/hooks.rs index 538e2ae18..47e7fcda9 100644 --- a/packages/core/src/hooks.rs +++ b/packages/core/src/hooks.rs @@ -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>> = slot.as_ref().borrow(); - match v.as_ref() { - Some(a) => { - let v: &dyn Any = a.as_ref(); - let real_val = v.downcast_ref::().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); - 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); + 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::().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::() + .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>>>, - pub callback: SuspendedCallback, - pub dom_node_id: Rc>>, } + type SuspendedCallback = Box Fn(SuspendedContext<'a>) -> DomTree<'a>>; + pub struct SuspendedContext<'a> { pub(crate) inner: Context<'a, ()>, } + impl<'src> SuspendedContext<'src> { pub fn render) -> 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); + +pub fn use_node_ref(cx: Context

) -> NodeRef { + cx.use_hook( + |f| {}, + |f| { + // + todo!() + }, + |f| { + // + }, + ) +} diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index c627496b2..b588215b3 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -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; +} diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs new file mode 100644 index 000000000..19c590e70 --- /dev/null +++ b/packages/core/src/mutations.rs @@ -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>, + pub noderefs: Vec>, +} + +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>>, + 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(&self) -> Option<&T> { + self.element + .as_ref() + .and_then(|f| f.get()) + .and_then(|f| f.downcast_ref::()) + } + pub fn downcast_mut(&mut self) -> Option<&mut T> { + self.element + .as_mut() + .and_then(|f| f.get_mut()) + .and_then(|f| f.downcast_mut::()) + } +} + +/// 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", + } + } +} diff --git a/packages/core/src/noderef.rs b/packages/core/src/noderef.rs new file mode 100644 index 000000000..16db8fe0d --- /dev/null +++ b/packages/core/src/noderef.rs @@ -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 { +// 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) +// } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index fae6c31d4..58f259c72 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -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 { - 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 { + 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>, } +/// A bump-alloacted string slice and metadata. pub struct VText<'src> { pub text: &'src str, + pub dom_id: Cell>, + 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>, + + 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>, - - 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>, - pub(crate) callback: RefCell>>, -} + /// 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>>, } /// Virtual Components for custom user-defined components /// Only supports the functional syntax pub struct VComponent<'src> { - pub ass_scope: Cell>, + pub key: Option<&'src str>, + + pub associated_scope: Cell>, + + pub is_static: bool, + + // Function pointer to the FC that was used to generate this component + pub user_fc: *const (), pub(crate) caller: Rc DomTree>, @@ -154,19 +274,17 @@ pub struct VComponent<'src> { pub(crate) drop_props: RefCell>>, - 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>>, +pub struct VSuspended<'a> { + pub task_id: u64, + pub dom_id: Cell>, + pub(crate) callback: + RefCell) -> 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( @@ -272,7 +382,7 @@ impl<'a> NodeFactory<'a> { pub fn raw_element( &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( &self, component: FC

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

, raw_props: *const (), ) -> Rc 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) } } + impl<'a> IntoVNode<'a> for Option> { 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 + ), } } } diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs new file mode 100644 index 000000000..d173a81f5 --- /dev/null +++ b/packages/core/src/scheduler.rs @@ -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>, + pub sender: UnboundedSender, + pub schedule_any_immediate: Rc, + pub submit_task: Rc TaskHandle>, + pub get_shared_context: Rc Option>>, +} + +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, + + // Garbage stored + pub pending_garbage: FxHashSet, + + // In-flight futures + pub async_tasks: FuturesUnordered, + + // scheduler stuff + pub current_priority: EventPriority, + + pub ui_events: VecDeque, + + pub pending_immediates: VecDeque, + + pub pending_tasks: VecDeque, + + pub garbage_scopes: HashSet, + + 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::(); + 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> { + 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>>, + ) -> Vec> { + /* + 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::>::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, + ) -> 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>>, + ) -> 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, + pub saved_state: Option>, + 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, + 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>>, + + /* + 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>>, + + 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 { + 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 { + 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) {} +} diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index 951b7280b..0f5cae2c7 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -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>>, pub(crate) borrowed_props: RefCell>>, + pub(crate) suspended_nodes: RefCell>>, // State pub(crate) hooks: HookList, pub(crate) shared_contexts: RefCell>>, - // A reference to the resources shared by all the comonents - pub(crate) vdom: SharedResources, + pub(crate) memoized_updater: Rc, + + 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>>; +/// 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>>; 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, - arena_idx: ScopeId, - parent: Option, - 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::>() } + + pub fn root(&self) -> &VNode { + self.frames.fin_head() + } } diff --git a/packages/core/src/signals.rs b/packages/core/src/signals.rs deleted file mode 100644 index bbce9a861..000000000 --- a/packages/core/src/signals.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Signas: Avoid the Diff Engine -//! ----------------------------- -//! -//! -//! TODO! diff --git a/packages/core/src/test_dom.rs b/packages/core/src/test_dom.rs new file mode 100644 index 000000000..39b9e5e74 --- /dev/null +++ b/packages/core/src/test_dom.rs @@ -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) + } +} diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs index 0add0f456..d2df10eab 100644 --- a/packages/core/src/util.rs +++ b/packages/core/src/util.rs @@ -8,21 +8,59 @@ pub fn empty_cell() -> Cell> { 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) -> &'static str { + std::any::type_name::() +} -// 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 { -// 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 { + if !self.0 { + self.0 = true; + cx.waker().wake_by_ref(); + Poll::Pending + } else { + Poll::Ready(()) + } + } +} diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index e6f27c6c5..b43907ea2 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -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>, + root_fc: Box, - // 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>, + root_props: Pin>, } 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) -> 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(root: FC

, root_props: P) -> Self { - let components = SharedResources::new(); + let scheduler = Scheduler::new(); let root_props: Pin> = Box::pin(root_props); - let props_ptr = root_props.as_ref().downcast_ref::

().unwrap() as *const P; + let props_ptr = root_props.downcast_ref::

().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::

(), + 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(root: FC

, 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 = |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> { - 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> { + 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::pin(root_props); + + if let Some(props_ptr) = root_props.downcast_ref::

().map(|p| p as *const P) { + std::mem::swap(&mut self.root_props, &mut root_props); + + let root = *self.root_fc.downcast_ref::>().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>> { - 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 { - // 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> { - // - - 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>> { + 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> { - 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> { - 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::(*hook_idx) }.unwrap(); - - let cx = Context { scope, props: &() }; - let scx = SuspendedContext { inner: cx }; - - // generate the new node! - let nodes: Option = (&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, + ) -> Vec> { + let mut deadline = Box::pin(deadline.fuse()); + self.scheduler.work_with_deadline(deadline).await } - pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender { - self.shared.task_sender.clone() + pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender { + 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, - - pending_nodes: Vec<*const VNode<'a>>, - - // WIP edits - edits: Vec>, - - 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>, -} diff --git a/packages/core/tests/create_iterative.rs b/packages/core/tests/create_iterative.rs new file mode 100644 index 000000000..d95b3b0dd --- /dev/null +++ b/packages/core/tests/create_iterative.rs @@ -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 },] + ); +} diff --git a/packages/core/tests/debugdiff.rs b/packages/core/tests/debugdiff.rs new file mode 100644 index 000000000..9f6aac6c1 --- /dev/null +++ b/packages/core/tests/debugdiff.rs @@ -0,0 +1,2 @@ +/// A virtualdom wrapper used for testing purposes. +pub struct DebugDiff {} diff --git a/packages/core/tests/diff_iterative.rs b/packages/core/tests/diff_iterative.rs new file mode 100644 index 000000000..0e1a9b1b5 --- /dev/null +++ b/packages/core/tests/diff_iterative.rs @@ -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); +} diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index 97c794b6e..2f58eb0ae 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -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> { - 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>) - 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>, Vec>) - 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 }, + ] + ); } diff --git a/packages/core/tests/eventsystem.rs b/packages/core/tests/eventsystem.rs index cb593dee7..9c7748e44 100644 --- a/packages/core/tests/eventsystem.rs +++ b/packages/core/tests/eventsystem.rs @@ -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; }); } diff --git a/packages/core/tests/hooks.rs b/packages/core/tests/hooks.rs new file mode 100644 index 000000000..66b2f5906 --- /dev/null +++ b/packages/core/tests/hooks.rs @@ -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::(cx); + + // cx.render(rsx! { + // div { + // style: { color: "red" }, + // node_ref: div_ref, + // onmouseover: move |_| { + // div_ref.borrow_mut().focus(); + // }, + // }, + // }) + // }; +} diff --git a/packages/core/tests/set_state_batch.rs b/packages/core/tests/set_state_batch.rs new file mode 100644 index 000000000..fc897102e --- /dev/null +++ b/packages/core/tests/set_state_batch.rs @@ -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::(); + + 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(); +} diff --git a/packages/core/tests/test_logging.rs b/packages/core/tests/test_logging.rs new file mode 100644 index 000000000..8f696be89 --- /dev/null +++ b/packages/core/tests/test_logging.rs @@ -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(); +} diff --git a/packages/core/tests/vdom_rebuild.rs b/packages/core/tests/vdom_rebuild.rs index 54d1676c8..3cd985f11 100644 --- a/packages/core/tests/vdom_rebuild.rs +++ b/packages/core/tests/vdom_rebuild.rs @@ -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); } diff --git a/packages/core/tests/work_sync.rs b/packages/core/tests/work_sync.rs new file mode 100644 index 000000000..b36c65b1a --- /dev/null +++ b/packages/core/tests/work_sync.rs @@ -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); +} diff --git a/packages/desktop/src/dom.rs b/packages/desktop/src/dom.rs index 001552586..09323d40b 100644 --- a/packages/desktop/src/dom.rs +++ b/packages/desktop/src/dom.rs @@ -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 }); - } -} diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index 308726f51..83f2a8bba 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -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 = 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)] diff --git a/packages/html/src/attrval.s b/packages/html/src/attrval.rs similarity index 86% rename from packages/html/src/attrval.s rename to packages/html/src/attrval.rs index cb081f4cb..9ac1f1373 100644 --- a/packages/html/src/attrval.s +++ b/packages/html/src/attrval.rs @@ -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>; diff --git a/packages/mobile/src/dom.rs b/packages/mobile/src/dom.rs index c9e457479..35e74986c 100644 --- a/packages/mobile/src/dom.rs +++ b/packages/mobile/src/dom.rs @@ -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 }); - } -} diff --git a/packages/mobile/src/lib.rs b/packages/mobile/src/lib.rs index 1ee4bc652..fccdd4317 100644 --- a/packages/mobile/src/lib.rs +++ b/packages/mobile/src/lib.rs @@ -117,8 +117,8 @@ impl WebviewRenderer { // Serialize the edit stream let edits = { - let mut edits = Vec::new(); - lock.rebuild(&mut edits).unwrap(); + let mut edits = Vec::::new(); + // lock.rebuild(&mut edits).unwrap(); serde_json::to_value(edits).unwrap() }; @@ -139,8 +139,8 @@ impl WebviewRenderer { // Serialize the edit stream let edits = { - let mut edits = Vec::new(); - lock.rebuild(&mut edits).unwrap(); + let mut edits = Vec::::new(); + // lock.rebuild(&mut edits).unwrap(); serde_json::to_value(edits).unwrap() }; diff --git a/packages/ssr/examples/basic.rs b/packages/ssr/examples/basic.rs new file mode 100644 index 000000000..ece077759 --- /dev/null +++ b/packages/ssr/examples/basic.rs @@ -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!" + } + )) +}; diff --git a/packages/ssr/examples/tide.rs b/packages/ssr/examples/tide.rs index 26f742bcf..9fc58d2be 100644 --- a/packages/ssr/examples/tide.rs +++ b/packages/ssr/examples/tide.rs @@ -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))) diff --git a/packages/ssr/examples/tofile.rs b/packages/ssr/examples/tofile.rs index 35ce6db89..ec72532d0 100644 --- a/packages/ssr/examples/tofile.rs +++ b/packages/ssr/examples/tofile.rs @@ -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!( "{}", diff --git a/packages/ssr/src/lib.rs b/packages/ssr/src/lib.rs index bd323e77d..a03520b27 100644 --- a/packages/ssr/src/lib.rs +++ b/packages/ssr/src/lib.rs @@ -46,7 +46,7 @@ pub fn render_vdom_scope(vdom: &VirtualDom, scope: ScopeId) -> Option { /// ```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)); } } diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 10300c023..cc588eb91 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -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", diff --git a/packages/web/examples/basic.rs b/packages/web/examples/basic.rs index 518115f7c..451a29606 100644 --- a/packages/web/examples/basic.rs +++ b/packages/web/examples/basic.rs @@ -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" + } + } } }) }; diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 37bbfc50c..621ad89c3 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -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, + sender_callback: Rc, // 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) -> Self { + pub fn new(root: Element, cfg: WebConfig, sender_callback: Rc) -> 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) { 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::().is_some() { if self.last_node_was_text { let comment_node = self @@ -143,75 +159,46 @@ impl WebsysDom { .create_comment("dioxus") .dyn_into::() .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::() { + el.replace_with_with_node(&arr).unwrap(); + } else if let Some(el) = old.dyn_ref::() { + el.replace_with_with_node(&arr).unwrap(); + } else if let Some(el) = old.dyn_ref::() { + 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::().unwrap().remove(); - } - - let old = old_nodes[0].clone(); - let arr: js_sys::Array = new_nodes.iter().collect(); - let el = old.dyn_into::().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::() { - // old_node - // .dyn_ref::() - // .unwrap() - // .replace_with_with_node_1(&new_node) - // .unwrap(); - // } else if old_node.has_type::() { - // old_node - // .dyn_ref::() - // .unwrap() - // .replace_with_with_node_1(&new_node) - // .unwrap(); - // } else if old_node.has_type::() { - // old_node - // .dyn_ref::() - // .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.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::() .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::() .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); @@ -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::() { + let node = self.stack.top(); + if let Some(el) = node.dyn_ref::() { 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::() { + if name == "value" { + node.set_value(value); + } + if name == "checked" { + node.set_checked(true); + } + } + + if let Some(node) = node.dyn_ref::() { + 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::().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::() { + el.after_with_node(&arr).unwrap(); + } else if let Some(el) = old.dyn_ref::() { + el.after_with_node(&arr).unwrap(); + } else if let Some(el) = old.dyn_ref::() { + 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::() { + el.before_with_node(&arr).unwrap(); + } else if let Some(el) = after.dyn_ref::() { + el.before_with_node(&arr).unwrap(); + } else if let Some(el) = after.dyn_ref::() { + 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::() - .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 { +fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { log::debug!("Handling event!"); let target = event @@ -734,13 +530,13 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { 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 { 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") + } + } +} diff --git a/packages/web/src/events.rs b/packages/web/src/events.rs new file mode 100644 index 000000000..ff4ced474 --- /dev/null +++ b/packages/web/src/events.rs @@ -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::() + .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 {} diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index abf9c7a83..b22481676 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -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( - root: FC, - root_props: T, - cfg: WebConfig, -) -> Result<()> { +pub async fn run_with_props(root: FC, 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); } } diff --git a/packages/web/src/ric_raf.rs b/packages/web/src/ric_raf.rs new file mode 100644 index 000000000..0a914540c --- /dev/null +++ b/packages/web/src/ric_raf.rs @@ -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, + raf_closure: Closure, +} + +impl RafLoop { + pub fn new() -> Self { + let (raf_sender, raf_receiver) = async_channel::unbounded(); + + let raf_closure: Closure = Closure::wrap(Box::new(move |_v: JsValue| { + raf_sender.try_send(()).unwrap() + })); + + let (ric_sender, ric_receiver) = async_channel::unbounded(); + + let ric_closure: Closure = 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::().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::().unwrap(); + let _id: i32 = self.window.request_animation_frame(raf_fn).unwrap(); + self.raf_receiver.recv().await.unwrap(); + } +} diff --git a/packages/web/src/ricpolyfill.js b/packages/web/src/ricpolyfill.js new file mode 100644 index 000000000..cd114f4ca --- /dev/null +++ b/packages/web/src/ricpolyfill.js @@ -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; +}