Merge pull request #17 from jkelleyrtp/jk/jankfree

feat: jank free rendering and noderefs
This commit is contained in:
Jonathan Kelley 2021-09-03 11:25:53 -04:00 committed by GitHub
commit 39cc0849b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
72 changed files with 5689 additions and 3873 deletions

View file

@ -65,3 +65,4 @@ asynchronicity
constified
SegVec
contentful
Jank

View file

@ -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"]

View file

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

View file

@ -28,7 +28,7 @@ pub static App: FC<()> = |cx| {
}
button {
"Start counting"
onclick: move |_| task.start()
onclick: move |_| task.resume()
}
button {
"Switch counting direcion"

View file

@ -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();

View file

@ -2,6 +2,8 @@ use dioxus::prelude::*;
fn main() {}
pub static Example: FC<()> = |cx| {
let p = 10;
cx.render(rsx! {
div {

View file

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

View file

@ -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)
});

View file

@ -5,7 +5,7 @@ use dioxus::ssr;
fn main() {
let mut vdom = VirtualDom::new(App);
vdom.rebuild_in_place().expect("Rebuilding failed");
// vdom.rebuild_in_place().expect("Rebuilding failed");
println!("{}", ssr::render_vdom(&vdom, |c| c));
}
@ -17,3 +17,10 @@ static App: FC<()> = |cx| {
}
))
};
struct MyProps<'a> {
text: &'a str,
}
fn App2<'a>(cx: Context<'a, MyProps>) -> DomTree<'a> {
None
}

136
examples/web_tick.rs Normal file
View file

@ -0,0 +1,136 @@
#![allow(non_upper_case_globals, non_snake_case)]
//! Example: Webview Renderer
//! -------------------------
//!
//! This example shows how to use the dioxus_desktop crate to build a basic desktop application.
//!
//! Under the hood, the dioxus_desktop crate bridges a native Dioxus VirtualDom with a custom prebuit application running
//! in the webview runtime. Custom handlers are provided for the webview instance to consume patches and emit user events
//! into the native VDom instance.
//!
//! Currently, NodeRefs won't work properly, but all other event functionality will.
use dioxus::prelude::*;
fn main() {
#[cfg(target_arch = "wasm32")]
intern_strings();
dioxus::web::launch(App, |c| c);
}
static App: FC<()> = |cx| {
let mut rng = SmallRng::from_entropy();
let rows = (0..1_000).map(|f| {
let label = Label::new(&mut rng);
rsx! {
Row {
row_id: f,
label: label
}
}
});
cx.render(rsx! {
table {
tbody {
{rows}
}
}
})
};
#[derive(PartialEq, Props)]
struct RowProps {
row_id: usize,
label: Label,
}
fn Row<'a>(cx: Context<'a, RowProps>) -> DomTree {
let [adj, col, noun] = cx.label.0;
cx.render(rsx! {
tr {
td { class:"col-md-1", "{cx.row_id}" }
td { class:"col-md-1", onclick: move |_| { /* run onselect */ }
a { class: "lbl", "{adj}" "{col}" "{noun}" }
}
td { class: "col-md-1"
a { class: "remove", onclick: move |_| {/* remove */}
span { class: "glyphicon glyphicon-remove remove" aria_hidden: "true" }
}
}
td { class: "col-md-6" }
}
})
}
use rand::prelude::*;
#[derive(PartialEq)]
struct Label([&'static str; 3]);
impl Label {
fn new(rng: &mut SmallRng) -> Self {
Label([
ADJECTIVES.choose(rng).unwrap(),
COLOURS.choose(rng).unwrap(),
NOUNS.choose(rng).unwrap(),
])
}
}
static ADJECTIVES: &[&str] = &[
"pretty",
"large",
"big",
"small",
"tall",
"short",
"long",
"handsome",
"plain",
"quaint",
"clean",
"elegant",
"easy",
"angry",
"crazy",
"helpful",
"mushy",
"odd",
"unsightly",
"adorable",
"important",
"inexpensive",
"cheap",
"expensive",
"fancy",
];
static COLOURS: &[&str] = &[
"red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black",
"orange",
];
static NOUNS: &[&str] = &[
"table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger",
"pizza", "mouse", "keyboard",
];
#[cfg(target_arch = "wasm32")]
fn intern_strings() {
for adj in ADJECTIVES {
wasm_bindgen::intern(adj);
}
for col in COLOURS {
wasm_bindgen::intern(col);
}
for no in NOUNS {
wasm_bindgen::intern(no);
}
wasm_bindgen::intern("col-md-1");
wasm_bindgen::intern("col-md-6");
wasm_bindgen::intern("glyphicon glyphicon-remove remove");
wasm_bindgen::intern("remove");
wasm_bindgen::intern("dioxus");
wasm_bindgen::intern("lbl");
wasm_bindgen::intern("true");
}

View file

@ -39,7 +39,8 @@ impl Parse for AmbiguousElement<AS_RSX> {
}
}
if let Ok(name) = input.fork().parse::<Ident>() {
use syn::ext::IdentExt;
if let Ok(name) = input.fork().call(Ident::parse_any) {
let name_str = name.to_string();
let first_char = name_str.chars().next().unwrap();
@ -53,9 +54,6 @@ impl Parse for AmbiguousElement<AS_RSX> {
.map(|c| AmbiguousElement::Element(c))
}
} else {
if input.peek(LitStr) {
panic!("it's actually a litstr");
}
Err(Error::new(input.span(), "Not a valid Html tag"))
}
}
@ -80,7 +78,8 @@ impl Parse for AmbiguousElement<AS_HTML> {
}
}
if let Ok(name) = input.fork().parse::<Ident>() {
use syn::ext::IdentExt;
if let Ok(name) = input.fork().call(Ident::parse_any) {
let name_str = name.to_string();
let first_char = name_str.chars().next().unwrap();
@ -94,9 +93,6 @@ impl Parse for AmbiguousElement<AS_HTML> {
.map(|c| AmbiguousElement::Element(c))
}
} else {
if input.peek(LitStr) {
panic!("it's actually a litstr");
}
Err(Error::new(input.span(), "Not a valid Html tag"))
}
}

View file

@ -32,6 +32,7 @@ impl Parse for Element<AS_RSX> {
let mut listeners: Vec<ElementAttr<AS_RSX>> = vec![];
let mut children: Vec<BodyNode<AS_RSX>> = vec![];
let mut key = None;
let mut el_ref = None;
'parsing: loop {
// [1] Break if empty
@ -45,6 +46,7 @@ impl Parse for Element<AS_RSX> {
&mut attributes,
&mut listeners,
&mut key,
&mut el_ref,
name.clone(),
)?;
} else {
@ -237,6 +239,7 @@ fn parse_rsx_element_field(
attrs: &mut Vec<ElementAttr<AS_RSX>>,
listeners: &mut Vec<ElementAttr<AS_RSX>>,
key: &mut Option<LitStr>,
el_ref: &mut Option<Expr>,
element_name: Ident,
) -> Result<()> {
let name = Ident::parse_any(stream)?;
@ -311,8 +314,9 @@ fn parse_rsx_element_field(
"namespace" => {
todo!("custom namespace not supported")
}
"ref" => {
todo!("NodeRefs are currently not supported! This is currently a reserved keyword.")
"node_ref" => {
*el_ref = Some(stream.parse::<Expr>()?);
return Ok(());
}
// Fall through

View file

@ -1,3 +1,3 @@
{
"rust-analyzer.inlayHints.enable": true
"rust-analyzer.inlayHints.enable": false
}

View file

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

View file

@ -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 {}
}
}
}
}
}
}
}
```

View file

@ -0,0 +1 @@
fn main() {}

View file

@ -0,0 +1,126 @@
#![allow(non_snake_case, non_upper_case_globals)]
//! This benchmark tests just the overhead of Dioxus itself.
//!
//! For the JS Framework Benchmark, both the framework and the browser is benchmarked together. Dioxus prepares changes
//! to be made, but the change application phase will be just as performant as the vanilla wasm_bindgen code. In essence,
//! we are measuring the overhead of Dioxus, not the performance of the "apply" phase.
//!
//! On my MBP 2019:
//! - Dioxus takes 3ms to create 1_000 rows
//! - Dioxus takes 30ms to create 10_000 rows
//!
//! As pure "overhead", these are amazing good numbers, mostly slowed down by hitting the global allocator.
//! These numbers don't represent Dioxus with the heuristic engine installed, so I assume it'll be even faster.
use criterion::{criterion_group, criterion_main, Criterion};
use dioxus_core as dioxus;
use dioxus_core::prelude::*;
use dioxus_html as dioxus_elements;
use rand::prelude::*;
criterion_group!(mbenches, create_rows);
criterion_main!(mbenches);
fn create_rows(c: &mut Criterion) {
static App: FC<()> = |cx| {
let mut rng = SmallRng::from_entropy();
let rows = (0..10_000).map(|f| {
let label = Label::new(&mut rng);
rsx! {
Row {
row_id: f,
label: label
}
}
});
cx.render(rsx! {
table {
tbody {
{rows}
}
}
})
};
c.bench_function("create rows", |b| {
b.iter(|| {
let mut dom = VirtualDom::new(App);
let g = dom.rebuild();
assert!(g.edits.len() > 1);
})
});
}
#[derive(PartialEq, Props)]
struct RowProps {
row_id: usize,
label: Label,
}
fn Row<'a>(cx: Context<'a, RowProps>) -> DomTree {
let [adj, col, noun] = cx.label.0;
cx.render(rsx! {
tr {
td { class:"col-md-1", "{cx.row_id}" }
td { class:"col-md-1", onclick: move |_| { /* run onselect */ }
a { class: "lbl", "{adj}" "{col}" "{noun}" }
}
td { class: "col-md-1"
a { class: "remove", onclick: move |_| {/* remove */}
span { class: "glyphicon glyphicon-remove remove" aria_hidden: "true" }
}
}
td { class: "col-md-6" }
}
})
}
#[derive(PartialEq)]
struct Label([&'static str; 3]);
impl Label {
fn new(rng: &mut SmallRng) -> Self {
Label([
ADJECTIVES.choose(rng).unwrap(),
COLOURS.choose(rng).unwrap(),
NOUNS.choose(rng).unwrap(),
])
}
}
static ADJECTIVES: &[&str] = &[
"pretty",
"large",
"big",
"small",
"tall",
"short",
"long",
"handsome",
"plain",
"quaint",
"clean",
"elegant",
"easy",
"angry",
"crazy",
"helpful",
"mushy",
"odd",
"unsightly",
"adorable",
"important",
"inexpensive",
"cheap",
"expensive",
"fancy",
];
static COLOURS: &[&str] = &[
"red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black",
"orange",
];
static NOUNS: &[&str] = &[
"table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger",
"pizza", "mouse", "keyboard",
];

View file

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

View file

@ -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();

View file

@ -1,18 +0,0 @@
fn main() {
// let g = rsx! {
// Fragment {
// // div {}
// // div {}
// // div {}
// // div {}
// // div {}
// // div {}
// // div {}
// // div {}
// // div {}
// }
// };
}

View file

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

View file

@ -0,0 +1,109 @@
use dioxus_core as dioxus;
use dioxus_core::prelude::*;
use dioxus_html as dioxus_elements;
use rand::prelude::*;
use std::fmt::Display;
fn main() {
let mut dom = VirtualDom::new(App);
let g = dom.rebuild();
assert!(g.edits.len() > 1);
}
static App: FC<()> = |cx| {
let mut rng = SmallRng::from_entropy();
let rows = (0..10_000).map(|f| {
let label = Label::new(&mut rng);
rsx! {
Row {
row_id: f,
label: label
}
}
});
cx.render(rsx! {
table {
tbody {
{rows}
}
}
})
};
#[derive(PartialEq, Props)]
struct RowProps {
row_id: usize,
label: Label,
}
fn Row<'a>(cx: Context<'a, RowProps>) -> DomTree {
cx.render(rsx! {
tr {
td { class:"col-md-1", "{cx.row_id}" }
td { class:"col-md-1", onclick: move |_| { /* run onselect */ }
a { class: "lbl", "{cx.label}" }
}
td { class: "col-md-1"
a { class: "remove", onclick: move |_| {/* remove */}
span { class: "glyphicon glyphicon-remove remove" aria_hidden: "true" }
}
}
td { class: "col-md-6" }
}
})
}
#[derive(PartialEq)]
struct Label([&'static str; 3]);
impl Label {
fn new(rng: &mut SmallRng) -> Self {
Label([
ADJECTIVES.choose(rng).unwrap(),
COLOURS.choose(rng).unwrap(),
NOUNS.choose(rng).unwrap(),
])
}
}
impl Display for Label {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} {} {}", self.0[0], self.0[1], self.0[2])
}
}
static ADJECTIVES: &[&str] = &[
"pretty",
"large",
"big",
"small",
"tall",
"short",
"long",
"handsome",
"plain",
"quaint",
"clean",
"elegant",
"easy",
"angry",
"crazy",
"helpful",
"mushy",
"odd",
"unsightly",
"adorable",
"important",
"inexpensive",
"cheap",
"expensive",
"fancy",
];
static COLOURS: &[&str] = &[
"red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black",
"orange",
];
static NOUNS: &[&str] = &[
"table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger",
"pizza", "mouse", "keyboard",
];

View file

@ -1,137 +0,0 @@
// #![allow(unused, non_upper_case_globals, non_snake_case)]
// use bumpalo::Bump;
// use dioxus_core::nodebuilder::*;
// use dioxus_core::prelude::*;
// use std::{collections::HashMap, future::Future, marker::PhantomData};
fn main() {}
// fn main() {
// let mut vdom = VirtualDom::new_with_props(
// component,
// Props {
// blah: false,
// text: "blah".into(),
// },
// );
// vdom.progress();
// let somet = String::from("asd");
// let text = somet.as_str();
// /*
// this could be auto-generated via the macro
// this props is allocated in this
// but the component and props would like need to be cached
// we could box this fn, abstracting away the props requirement and just keep the entrance and allocator requirement
// How do we keep cached things around?
// Need some sort of caching mechanism
// how do we enter into a childscope from a parent scope?
// Problems:
// 1: Comp props need to be stored somewhere so we can re-evalute components when they receive updates
// 2: Trees are not evaluated
// */
// let example_caller = move |cx: &Bump| {
// todo!()
// // let p = Props { blah: true, text };
// // let c = Context { props: &p };
// // let r = component(&c);
// };
// // check the edit list
// }
// // ~~~ Text shared between components via props can be done with lifetimes! ~~~
// // Super duper efficient :)
// struct Props {
// blah: bool,
// text: String,
// // text: &'src str,
// }
// impl Properties for Props {
// fn new() -> Self {
// todo!()
// }
// }
// fn component<'a>(cx: Context<'a, Props>) -> DomTree<'a> {
// // Write asynchronous rendering code that immediately returns a "suspended" VNode
// // The concurrent API will then progress this component when the future finishes
// // You can suspend the entire component, or just parts of it
// let product_list = cx.suspend(async {
// // Suspend the rendering that completes when the future is done
// match fetch_data().await {
// Ok(data) => html! { <div> "success!" </div>},
// Err(_) => html! { <div> "failure :(" </div>},
// }
// });
// // todo!()
// cx.render(html! {
// <div>
// <h1> "Products" </h1>
// <button> "hello!" </button>
// // Subnodes can even be suspended
// // When completely rendered, they won't cause the component itself to re-render, just their slot
// // <p> { product_list } </p>
// </div>
// })
// }
// fn BuilderComp<'a>(cx: Context<'a, Props>) -> DomTree<'a> {
// // VNodes can be constructed via a builder or the html! macro
// // However, both of these are "lazy" - they need to be evaluated (aka, "viewed")
// // We can "view" them with Context for ultimate speed while inside components
// cx.render(|bump| {
// div(bump)
// .attr("class", "edit")
// .child(text("Hello"))
// .child(text(cx.cx.text.as_ref()))
// .finish()
// })
// }
// #[fc]
// fn EffcComp(cx: Context, name: &str) -> DomTree {
// // VNodes can be constructed via a builder or the html! macro
// // However, both of these are "lazy" - they need to be evaluated (aka, "viewed")
// // We can "view" them with Context for ultimate speed while inside components
// // use "phase" style allocation;
// cx.render(html! {
// <div>
// // your template goes here
// // feel free to directly use "name"
// </div>
// })
// }
// fn FullySuspended<'a>(cx: &'a Context<Props>) -> DomTree<'a> {
// cx.suspend(async {
// let i: i32 = 0;
// // full suspended works great with just returning VNodes!
// // Feel free to capture the html! macro directly
// // Anything returned here is automatically viewed
// let tex = match i {
// 1 => html! { <div> </div> },
// 2 => html! { <div> </div> },
// _ => html! { <div> </div> },
// };
// if cx.cx.blah {
// html! { <div> </div> }
// } else {
// tex
// }
// })
// }
// /// An example of a datafetching service
// async fn fetch_data() -> Result<String, ()> {
// todo!()
// }

View file

@ -0,0 +1,15 @@
use std::time::Duration;
use dioxus_core::prelude::*;
#[async_std::main]
async fn main() {
static App: FC<()> = |cx| cx.render(LazyNodes::new(|f| f.text(format_args!("hello"))));
let mut dom = VirtualDom::new(App);
dom.rebuild();
let deadline = async_std::task::sleep(Duration::from_millis(50));
let _fut = dom.run_with_deadline(deadline);
}

View file

@ -1,190 +0,0 @@
use std::cell::{RefCell, RefMut};
use std::fmt::Display;
use std::{cell::UnsafeCell, rc::Rc};
use crate::heuristics::*;
use crate::innerlude::*;
use futures_util::stream::FuturesUnordered;
use fxhash::{FxHashMap, FxHashSet};
use slab::Slab;
use smallvec::SmallVec;
// slotmap::new_key_type! {
// // A dedicated key type for the all the scopes
// pub struct ScopeId;
// }
// #[cfg(feature = "serialize", serde::Serialize)]
// #[cfg(feature = "serialize", serde::Serialize)]
#[derive(serde::Serialize, serde::Deserialize, Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct ScopeId(pub usize);
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct ElementId(pub usize);
impl Display for ElementId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl ElementId {
pub fn as_u64(self) -> u64 {
self.0 as u64
}
}
type Shared<T> = Rc<RefCell<T>>;
type TaskReceiver = futures_channel::mpsc::UnboundedReceiver<EventTrigger>;
type TaskSender = futures_channel::mpsc::UnboundedSender<EventTrigger>;
/// These are resources shared among all the components and the virtualdom itself
#[derive(Clone)]
pub struct SharedResources {
pub components: Rc<UnsafeCell<Slab<Scope>>>,
pub(crate) heuristics: Shared<HeuristicsEngine>,
///
pub task_sender: TaskSender,
pub task_receiver: Shared<TaskReceiver>,
pub async_tasks: Shared<FuturesUnordered<FiberTask>>,
/// We use a SlotSet to keep track of the keys that are currently being used.
/// However, we don't store any specific data since the "mirror"
pub raw_elements: Rc<RefCell<Slab<()>>>,
pub task_setter: Rc<dyn Fn(ScopeId)>,
}
impl SharedResources {
pub fn new() -> Self {
// preallocate 2000 elements and 20 scopes to avoid dynamic allocation
let components: Rc<UnsafeCell<Slab<Scope>>> =
Rc::new(UnsafeCell::new(Slab::with_capacity(100)));
// elements are super cheap - the value takes no space
let raw_elements = Slab::with_capacity(2000);
let (sender, receiver) = futures_channel::mpsc::unbounded();
let heuristics = HeuristicsEngine::new();
// we allocate this task setter once to save us from having to allocate later
let task_setter = {
let queue = sender.clone();
let components = components.clone();
Rc::new(move |idx: ScopeId| {
let comps = unsafe { &*components.get() };
if let Some(scope) = comps.get(idx.0) {
queue
.unbounded_send(EventTrigger::new(
VirtualEvent::ScheduledUpdate {
height: scope.height,
},
idx,
None,
EventPriority::High,
))
.expect("The event queu receiver should *never* be dropped");
}
}) as Rc<dyn Fn(ScopeId)>
};
Self {
components,
async_tasks: Rc::new(RefCell::new(FuturesUnordered::new())),
task_receiver: Rc::new(RefCell::new(receiver)),
task_sender: sender,
heuristics: Rc::new(RefCell::new(heuristics)),
raw_elements: Rc::new(RefCell::new(raw_elements)),
task_setter,
}
}
/// this is unsafe because the caller needs to track which other scopes it's already using
pub fn get_scope(&self, idx: ScopeId) -> Option<&Scope> {
let inner = unsafe { &*self.components.get() };
inner.get(idx.0)
}
/// this is unsafe because the caller needs to track which other scopes it's already using
pub fn get_scope_mut(&self, idx: ScopeId) -> Option<&mut Scope> {
let inner = unsafe { &mut *self.components.get() };
inner.get_mut(idx.0)
}
pub fn with_scope<'b, O: 'static>(
&'b self,
_id: ScopeId,
_f: impl FnOnce(&'b mut Scope) -> O,
) -> Result<O> {
todo!()
}
// return a bumpframe with a lifetime attached to the arena borrow
// this is useful for merging lifetimes
pub fn with_scope_vnode<'b>(
&self,
_id: ScopeId,
_f: impl FnOnce(&mut Scope) -> &VNode<'b>,
) -> Result<&VNode<'b>> {
todo!()
}
pub fn try_remove(&self, id: ScopeId) -> Result<Scope> {
let inner = unsafe { &mut *self.components.get() };
Ok(inner.remove(id.0))
// .try_remove(id.0)
// .ok_or_else(|| Error::FatalInternal("Scope not found"))
}
pub fn reserve_node(&self) -> ElementId {
ElementId(self.raw_elements.borrow_mut().insert(()))
}
/// return the id, freeing the space of the original node
pub fn collect_garbage(&self, id: ElementId) {
self.raw_elements.borrow_mut().remove(id.0);
}
pub fn insert_scope_with_key(&self, f: impl FnOnce(ScopeId) -> Scope) -> ScopeId {
let g = unsafe { &mut *self.components.get() };
let entry = g.vacant_entry();
let id = ScopeId(entry.key());
entry.insert(f(id));
id
}
pub fn schedule_update(&self) -> Rc<dyn Fn(ScopeId)> {
self.task_setter.clone()
}
pub fn submit_task(&self, task: FiberTask) -> TaskHandle {
self.async_tasks.borrow_mut().push(task);
TaskHandle {}
}
pub fn make_trigger_key(&self, trigger: &EventTrigger) -> EventKey {
let height = self
.get_scope(trigger.originator)
.map(|f| f.height)
.unwrap();
EventKey {
height,
originator: trigger.originator,
priority: trigger.priority,
}
}
}
pub struct TaskHandle {}
impl TaskHandle {
pub fn toggle(&self) {}
pub fn start(&self) {}
pub fn stop(&self) {}
pub fn restart(&self) {}
}

View file

@ -2,7 +2,7 @@ use crate::innerlude::*;
use bumpalo::Bump;
use std::cell::Cell;
pub struct ActiveFrame {
pub(crate) struct ActiveFrame {
// We use a "generation" for users of contents in the bump frames to ensure their data isn't broken
pub generation: Cell<usize>,
@ -10,7 +10,7 @@ pub struct ActiveFrame {
pub frames: [BumpFrame; 2],
}
pub struct BumpFrame {
pub(crate) struct BumpFrame {
pub bump: Bump,
pub(crate) head_node: VNode<'static>,
@ -40,10 +40,6 @@ impl ActiveFrame {
self.wip_frame_mut().bump.reset()
}
pub fn update_head_node<'a>(&mut self, node: VNode<'a>) {
self.wip_frame_mut().head_node = unsafe { std::mem::transmute(node) };
}
/// The "work in progress frame" represents the frame that is currently being worked on.
pub fn wip_frame(&self) -> &BumpFrame {
match self.generation.get() & 1 == 0 {
@ -67,12 +63,6 @@ impl ActiveFrame {
}
}
pub fn finished_frame_mut(&mut self) -> &mut BumpFrame {
match self.generation.get() & 1 == 1 {
true => &mut self.frames[0],
false => &mut self.frames[1],
}
}
/// Give out our self-referential item with our own borrowed lifetime
pub fn fin_head<'b>(&'b self) -> &'b VNode<'b> {
let cur_head = &self.finished_frame().head_node;

View file

@ -0,0 +1,94 @@
use crate::innerlude::*;
/// This iterator iterates through a list of virtual children and only returns real children (Elements, Text, Anchors).
///
/// This iterator is useful when it's important to load the next real root onto the top of the stack for operations like
/// "InsertBefore".
pub(crate) struct RealChildIterator<'a> {
scopes: &'a ResourcePool,
// Heuristcally we should never bleed into 4 completely nested fragments/components
// Smallvec lets us stack allocate our little stack machine so the vast majority of cases are sane
// TODO: use const generics instead of the 4 estimation
stack: smallvec::SmallVec<[(u16, &'a VNode<'a>); 4]>,
}
impl<'a> RealChildIterator<'a> {
pub fn new(starter: &'a VNode<'a>, scopes: &'a ResourcePool) -> Self {
Self {
scopes,
stack: smallvec::smallvec![(0, starter)],
}
}
}
impl<'a> Iterator for RealChildIterator<'a> {
type Item = &'a VNode<'a>;
fn next(&mut self) -> Option<&'a VNode<'a>> {
let mut should_pop = false;
let mut returned_node: Option<&'a VNode<'a>> = None;
let mut should_push = None;
while returned_node.is_none() {
if let Some((count, node)) = self.stack.last_mut() {
match &node {
// We can only exit our looping when we get "real" nodes
// This includes fragments and components when they're empty (have a single root)
VNode::Element(_) | VNode::Text(_) | VNode::Suspended(_) | VNode::Anchor(_) => {
// We've recursed INTO an element/text
// We need to recurse *out* of it and move forward to the next
should_pop = true;
returned_node = Some(&*node);
}
// If we get a fragment we push the next child
VNode::Fragment(frag) => {
let subcount = *count as usize;
if frag.children.len() == 0 {
should_pop = true;
returned_node = Some(&*node);
}
if subcount >= frag.children.len() {
should_pop = true;
} else {
should_push = Some(&frag.children[subcount]);
}
}
// For components, we load their root and push them onto the stack
VNode::Component(sc) => {
let scope = self
.scopes
.get_scope(sc.associated_scope.get().unwrap())
.unwrap();
// let scope = self.scopes.get(sc.ass_scope.get().unwrap()).unwrap();
// Simply swap the current node on the stack with the root of the component
*node = scope.frames.fin_head();
}
}
} else {
// If there's no more items on the stack, we're done!
return None;
}
if should_pop {
self.stack.pop();
if let Some((id, _)) = self.stack.last_mut() {
*id += 1;
}
should_pop = false;
}
if let Some(push) = should_push {
self.stack.push((0, push));
should_push = None;
}
}
returned_node
}
}

View file

@ -87,6 +87,10 @@ impl<'src, P> Context<'src, P> {
/// })
/// }
/// ```
///
/// ## Notes:
///
/// This method returns a "ScopeChildren" object. This object is copy-able and preserve the correct lifetime.
pub fn children(&self) -> ScopeChildren<'src> {
self.scope.child_nodes()
}
@ -96,27 +100,11 @@ impl<'src, P> Context<'src, P> {
/// ## Notice: you should prefer using prepare_update and get_scope_id
///
pub fn schedule_update(&self) -> Rc<dyn Fn() + 'static> {
let cb = self.scope.vdom.schedule_update();
let id = self.get_scope_id();
Rc::new(move || cb(id))
self.scope.memoized_updater.clone()
}
pub fn prepare_update(&self) -> Rc<dyn Fn(ScopeId)> {
self.scope.vdom.schedule_update()
}
pub fn schedule_effect(&self) -> Rc<dyn Fn() + 'static> {
todo!()
}
pub fn schedule_layout_effect(&self) {
todo!()
}
/// Get's this component's unique identifier.
///
pub fn get_scope_id(&self) -> ScopeId {
self.scope.our_arena_idx.clone()
self.scope.shared.schedule_any_immediate.clone()
}
/// Take a lazy VNode structure and actually build it with the context of the VDom's efficient VNode allocator.
@ -160,7 +148,7 @@ impl<'src, P> Context<'src, P> {
///
///
pub fn submit_task(&self, task: FiberTask) -> TaskHandle {
self.scope.vdom.submit_task(task)
(self.scope.shared.submit_task)(task)
}
/// Add a state globally accessible to child components via tree walking
@ -174,46 +162,11 @@ impl<'src, P> Context<'src, P> {
});
}
/// Walk the tree to find a shared state with the TypeId of the generic type
///
pub fn consume_shared_state<T: 'static>(self) -> Option<Rc<T>> {
let mut scope = Some(self.scope);
let mut parent = None;
let getter = &self.scope.shared.get_shared_context;
let ty = TypeId::of::<T>();
while let Some(inner) = scope {
log::debug!(
"Searching {:#?} for valid shared_context",
inner.our_arena_idx
);
let shared_ctx = {
let shared_contexts = inner.shared_contexts.borrow();
log::debug!(
"This component has {} shared contexts",
shared_contexts.len()
);
shared_contexts.get(&ty).map(|f| f.clone())
};
if let Some(shared_cx) = shared_ctx {
log::debug!("found matching cx");
let rc = shared_cx
.clone()
.downcast::<T>()
.expect("Should not fail, already validated the type from the hashmap");
parent = Some(rc);
break;
} else {
match inner.parent_idx {
Some(parent_id) => {
scope = unsafe { inner.vdom.get_scope(parent_id) };
}
None => break,
}
}
}
parent
let idx = self.scope.our_arena_idx;
getter(idx, ty).map(|f| f.downcast().expect("TypeID already validated"))
}
/// Store a value between renders

View file

@ -1,78 +0,0 @@
//! Debug virtual doms!
//! This renderer comes built in with dioxus core and shows how to implement a basic renderer.
//!
//! Renderers don't actually need to own the virtual dom (it's up to the implementer).
use crate::innerlude::RealDom;
use crate::{events::EventTrigger, virtual_dom::VirtualDom};
use crate::{innerlude::Result, prelude::*};
pub struct DebugRenderer {
internal_dom: VirtualDom,
}
impl DebugRenderer {
/// Create a new instance of the Dioxus Virtual Dom with no properties for the root component.
///
/// This means that the root component must either consumes its own context, or statics are used to generate the page.
/// The root component can access things like routing in its context.
pub fn new(root: FC<()>) -> Self {
Self::new_with_props(root, ())
}
/// Create a new text-renderer instance from a functional component root.
/// Automatically progresses the creation of the VNode tree to completion.
///
/// A VDom is automatically created. If you want more granular control of the VDom, use `from_vdom`
pub fn new_with_props<T: Properties + 'static>(root: FC<T>, root_props: T) -> Self {
Self::from_vdom(VirtualDom::new_with_props(root, root_props))
}
/// Create a new text renderer from an existing Virtual DOM.
pub fn from_vdom(dom: VirtualDom) -> Self {
// todo: initialize the event registry properly
Self { internal_dom: dom }
}
pub fn handle_event(&mut self, trigger: EventTrigger) -> Result<()> {
Ok(())
}
// pub fn step<Dom: RealDom>(&mut self, machine: &mut DiffMachine<Dom>) -> Result<()> {
// Ok(())
// }
// this does a "holy" compare - if something is missing in the rhs, it doesn't complain.
// it only complains if something shows up that's not in the lhs, *or* if a value is different.
// This lets you exclude various fields if you just want to drill in to a specific prop
// It leverages the internal diffing mechanism.
// If you have a list or "nth" child, you do need to list those children, but you don't need to
// fill in their children/attrs/etc
// Does not handle children or lifecycles and will always fail the test if they show up in the rhs
pub fn compare<F>(&self, other: LazyNodes<F>) -> Result<()>
where
F: for<'b, 'c> FnOnce(&'b NodeFactory<'c>) -> VNode<'c>,
{
Ok(())
}
// Do a full compare - everything must match
// Ignores listeners and children components
pub fn compare_full<F>(&self, other: LazyNodes<F>) -> Result<()>
where
F: for<'b, 'c> FnOnce(&'b NodeFactory<'c>) -> VNode<'c>,
{
Ok(())
}
pub fn trigger_listener(&mut self, id: usize) -> Result<()> {
Ok(())
}
pub fn render_nodes<F>(&self, other: LazyNodes<F>) -> Result<()>
where
F: for<'b, 'c> FnOnce(&'b NodeFactory<'c>) -> VNode<'c>,
{
Ok(())
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,116 @@
use crate::innerlude::*;
use smallvec::{smallvec, SmallVec};
/// The stack instructions we use to diff and create new nodes.
#[derive(Debug)]
pub enum DiffInstruction<'a> {
DiffNode {
old: &'a VNode<'a>,
new: &'a VNode<'a>,
},
DiffChildren {
old: &'a [VNode<'a>],
new: &'a [VNode<'a>],
},
Create {
node: &'a VNode<'a>,
},
/// pushes the node elements onto the stack for use in mount
PrepareMoveNode {
node: &'a VNode<'a>,
},
Mount {
and: MountType<'a>,
},
PopScope,
}
#[derive(Debug, Clone, Copy)]
pub enum MountType<'a> {
Absorb,
Append,
Replace { old: &'a VNode<'a> },
ReplaceByElementId { el: ElementId },
InsertAfter { other_node: &'a VNode<'a> },
InsertBefore { other_node: &'a VNode<'a> },
}
pub(crate) struct DiffStack<'bump> {
instructions: Vec<DiffInstruction<'bump>>,
nodes_created_stack: SmallVec<[usize; 10]>,
pub scope_stack: SmallVec<[ScopeId; 5]>,
}
impl<'bump> DiffStack<'bump> {
pub fn new() -> Self {
Self {
instructions: Vec::with_capacity(1000),
nodes_created_stack: smallvec![],
scope_stack: smallvec![],
}
}
pub fn is_empty(&self) -> bool {
self.instructions.is_empty()
}
pub fn pop(&mut self) -> Option<DiffInstruction<'bump>> {
self.instructions.pop()
}
pub fn pop_scope(&mut self) -> Option<ScopeId> {
self.scope_stack.pop()
}
pub fn push(&mut self, instruction: DiffInstruction<'bump>) {
self.instructions.push(instruction)
}
pub fn create_children(&mut self, children: &'bump [VNode<'bump>], and: MountType<'bump>) {
self.nodes_created_stack.push(0);
self.instructions.push(DiffInstruction::Mount { and });
for child in children.into_iter().rev() {
self.instructions
.push(DiffInstruction::Create { node: child });
}
}
pub fn push_nodes_created(&mut self, count: usize) {
self.nodes_created_stack.push(count);
}
pub fn create_node(&mut self, node: &'bump VNode<'bump>, and: MountType<'bump>) {
self.nodes_created_stack.push(0);
self.instructions.push(DiffInstruction::Mount { and });
self.instructions.push(DiffInstruction::Create { node });
}
pub fn add_child_count(&mut self, count: usize) {
*self.nodes_created_stack.last_mut().unwrap() += count;
}
pub fn pop_nodes_created(&mut self) -> usize {
self.nodes_created_stack.pop().unwrap()
}
pub fn current_scope(&self) -> Option<ScopeId> {
self.scope_stack.last().map(|f| f.clone())
}
pub fn create_component(&mut self, idx: ScopeId, node: &'bump VNode<'bump>) {
// Push the new scope onto the stack
self.scope_stack.push(idx);
self.instructions.push(DiffInstruction::PopScope);
// Run the creation algorithm with this scope on the stack
// ?? I think we treat components as framgnets??
self.instructions.push(DiffInstruction::Create { node });
}
}

View file

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

View file

@ -1,30 +0,0 @@
//! Internal error handling for Dioxus
//!
//!
use thiserror::Error as ThisError;
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(ThisError, Debug)]
pub enum Error {
#[error("Fatal Internal Error: {0}")]
FatalInternal(&'static str),
#[error("Context is missing")]
MissingSharedContext,
#[error("No event to progress")]
NoEvent,
#[error("Wrong Properties Type")]
WrongProps,
#[error("The component failed to return VNodes")]
ComponentFailed,
#[error("Base scope has not been mounted yet")]
NotMounted,
#[error("I/O Error: {0}")]
IO(#[from] std::io::Error),
}

View file

@ -3,212 +3,71 @@
//!
//! 3rd party renderers are responsible for converting their native events into these virtual event types. Events might
//! be heavy or need to interact through FFI, so the events themselves are designed to be lazy.
use crate::innerlude::{ElementId, ScopeId};
use std::{
cell::{Cell, RefCell},
rc::Rc,
};
use bumpalo::boxed::Box as BumpBox;
use std::{any::Any, cell::RefCell, fmt::Debug, ops::Deref, rc::Rc};
use crate::{
innerlude::{ElementId, ScopeId},
VNode,
innerlude::NodeFactory,
innerlude::{Attribute, Listener, VNode},
};
use std::cell::Cell;
#[derive(Debug)]
pub struct EventTrigger {
pub struct UserEvent {
/// The originator of the event trigger
pub originator: ScopeId,
pub scope: ScopeId,
/// The optional real node associated with the trigger
pub real_node_id: Option<ElementId>,
pub mounted_dom_id: Option<ElementId>,
/// The event type IE "onclick" or "onmouseover"
///
/// The name that the renderer will use to mount the listener.
pub name: &'static str,
/// The type of event
pub event: VirtualEvent,
/// The priority of the event
pub priority: EventPriority,
pub event: SyntheticEvent,
}
#[derive(PartialEq, Eq, Clone, Copy)]
pub struct EventKey {
/// The originator of the event trigger
pub originator: ScopeId,
/// The priority of the event
pub priority: EventPriority,
/// The height of the scope (used for ordering)
pub height: u32,
// TODO: add the time that the event was queued
}
impl PartialOrd for EventKey {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
todo!()
}
}
impl Ord for EventKey {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
todo!()
}
}
/// Priority of Event Triggers.
///
/// Internally, Dioxus will abort work that's taking too long if new, more important, work arrives. Unlike React, Dioxus
/// won't be afraid to pause work or flush changes to the RealDOM. This is called "cooperative scheduling". Some Renderers
/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well.
///
/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes.
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)]
pub enum EventPriority {
/// Garbage collection is a type of work than can be scheduled around other work, but must be completed in a specific
/// order. The GC must be run for a component before any other future work for that component is run. Otherwise,
/// we will leak slots in our slab.
///
/// Garbage collection mixes with the safety aspects of the virtualdom so it's very important to get it done before
/// other work.
GarbageCollection,
/// "High Priority" work will not interrupt other high priority work, but will interrupt long medium and low priority work.
///
/// This is typically reserved for things like user interaction.
High,
/// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important
/// than "High Priority" events and will take presedence over low priority events.
///
/// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input.
Medium,
/// "Low Priority" work will always be pre-empted unless the work is significantly delayed, in which case it will be
/// advanced to the front of the work queue until completed.
///
/// The primary user of Low Priority work is the asynchronous work system (suspense).
Low,
}
impl EventTrigger {
pub fn new(
event: VirtualEvent,
scope: ScopeId,
mounted_dom_id: Option<ElementId>,
priority: EventPriority,
) -> Self {
Self {
priority,
originator: scope,
real_node_id: mounted_dom_id,
event,
}
}
}
pub enum VirtualEvent {
/// Generated during diffing to signal that a component's nodes to be given back
///
/// Typically has a high priority
///
/// If an event is scheduled for a component that has "garbage", that garabge will be cleaned up before the event can
/// be processed.
GarbageCollection,
/// A type of "immediate" event scheduled by components
///
/// Usually called through "set_state"
ScheduledUpdate {
height: u32,
},
// Whenever a task is ready (complete) Dioxus produces this "AsyncEvent"
//
// Async events don't necessarily propagate into a scope being ran. It's up to the event itself
// to force an update for itself.
//
// Most async events should have a low priority.
//
// This type exists for the task/concurrency system to signal that a task is ready.
// However, this does not necessarily signal that a scope must be re-ran, so the hook implementation must cause its
// own re-run.
AsyncEvent {
should_rerender: bool,
},
// Suspense events are a type of async event generated when suspended nodes are ready to be processed.
//
// they have the lowest priority
SuspenseEvent {
hook_idx: usize,
domnode: Rc<Cell<Option<ElementId>>>,
},
// image event has conflicting method types
// ImageEvent(event_data::ImageEvent),
// Real events
pub enum SyntheticEvent {
AnimationEvent(on::AnimationEvent),
ClipboardEvent(on::ClipboardEvent),
CompositionEvent(on::CompositionEvent),
KeyboardEvent(on::KeyboardEvent),
FocusEvent(on::FocusEvent),
FormEvent(on::FormEvent),
SelectionEvent(on::SelectionEvent),
KeyboardEvent(on::KeyboardEvent),
GenericEvent(on::GenericEvent),
TouchEvent(on::TouchEvent),
UIEvent(on::UIEvent),
WheelEvent(on::WheelEvent),
MediaEvent(on::MediaEvent),
AnimationEvent(on::AnimationEvent),
TransitionEvent(on::TransitionEvent),
ToggleEvent(on::ToggleEvent),
MediaEvent(on::MediaEvent),
MouseEvent(on::MouseEvent),
WheelEvent(on::WheelEvent),
SelectionEvent(on::SelectionEvent),
TransitionEvent(on::TransitionEvent),
PointerEvent(on::PointerEvent),
}
impl VirtualEvent {
pub fn is_input_event(&self) -> bool {
match self {
VirtualEvent::ClipboardEvent(_)
| VirtualEvent::CompositionEvent(_)
| VirtualEvent::KeyboardEvent(_)
| VirtualEvent::FocusEvent(_)
| VirtualEvent::FormEvent(_)
| VirtualEvent::SelectionEvent(_)
| VirtualEvent::TouchEvent(_)
| VirtualEvent::UIEvent(_)
| VirtualEvent::WheelEvent(_)
| VirtualEvent::MediaEvent(_)
| VirtualEvent::AnimationEvent(_)
| VirtualEvent::TransitionEvent(_)
| VirtualEvent::ToggleEvent(_)
| VirtualEvent::MouseEvent(_)
| VirtualEvent::PointerEvent(_) => true,
// ImageEvent(event_data::ImageEvent),
VirtualEvent::GarbageCollection
| VirtualEvent::ScheduledUpdate { .. }
| VirtualEvent::AsyncEvent { .. }
| VirtualEvent::SuspenseEvent { .. } => false,
}
}
}
impl std::fmt::Debug for VirtualEvent {
impl std::fmt::Debug for SyntheticEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let name = match self {
VirtualEvent::ClipboardEvent(_) => "ClipboardEvent",
VirtualEvent::CompositionEvent(_) => "CompositionEvent",
VirtualEvent::KeyboardEvent(_) => "KeyboardEvent",
VirtualEvent::FocusEvent(_) => "FocusEvent",
VirtualEvent::FormEvent(_) => "FormEvent",
VirtualEvent::SelectionEvent(_) => "SelectionEvent",
VirtualEvent::TouchEvent(_) => "TouchEvent",
VirtualEvent::UIEvent(_) => "UIEvent",
VirtualEvent::WheelEvent(_) => "WheelEvent",
VirtualEvent::MediaEvent(_) => "MediaEvent",
VirtualEvent::AnimationEvent(_) => "AnimationEvent",
VirtualEvent::TransitionEvent(_) => "TransitionEvent",
VirtualEvent::ToggleEvent(_) => "ToggleEvent",
VirtualEvent::MouseEvent(_) => "MouseEvent",
VirtualEvent::PointerEvent(_) => "PointerEvent",
VirtualEvent::GarbageCollection => "GarbageCollection",
VirtualEvent::ScheduledUpdate { .. } => "SetStateEvent",
VirtualEvent::AsyncEvent { .. } => "AsyncEvent",
VirtualEvent::SuspenseEvent { .. } => "SuspenseEvent",
SyntheticEvent::ClipboardEvent(_) => "ClipboardEvent",
SyntheticEvent::CompositionEvent(_) => "CompositionEvent",
SyntheticEvent::KeyboardEvent(_) => "KeyboardEvent",
SyntheticEvent::FocusEvent(_) => "FocusEvent",
SyntheticEvent::FormEvent(_) => "FormEvent",
SyntheticEvent::SelectionEvent(_) => "SelectionEvent",
SyntheticEvent::TouchEvent(_) => "TouchEvent",
SyntheticEvent::WheelEvent(_) => "WheelEvent",
SyntheticEvent::MediaEvent(_) => "MediaEvent",
SyntheticEvent::AnimationEvent(_) => "AnimationEvent",
SyntheticEvent::TransitionEvent(_) => "TransitionEvent",
SyntheticEvent::ToggleEvent(_) => "ToggleEvent",
SyntheticEvent::MouseEvent(_) => "MouseEvent",
SyntheticEvent::PointerEvent(_) => "PointerEvent",
SyntheticEvent::GenericEvent(_) => "GenericEvent",
};
f.debug_struct("VirtualEvent").field("type", &name).finish()
@ -222,20 +81,8 @@ pub mod on {
//! Synthetic events are immutable and wrapped in Arc. It is the intention for Dioxus renderers to re-use the underyling
//! Arc allocation through "get_mut"
//!
//!
//!
#![allow(unused)]
use bumpalo::boxed::Box as BumpBox;
use std::{cell::RefCell, fmt::Debug, ops::Deref, rc::Rc};
use crate::{
innerlude::NodeFactory,
innerlude::{Attribute, ElementId, Listener, VNode},
};
use std::cell::Cell;
use super::VirtualEvent;
//! React recently dropped support for re-using event allocation and just passes the real event along.
use super::*;
macro_rules! event_directory {
( $(
@ -271,12 +118,12 @@ pub mod on {
{
let bump = &c.bump();
let cb: &mut dyn FnMut(VirtualEvent) = bump.alloc(move |evt: VirtualEvent| match evt {
VirtualEvent::$wrapper(event) => callback(event),
_ => unreachable!("Downcasted VirtualEvent to wrong event type - this is an internal bug!")
let cb: &mut dyn FnMut(SyntheticEvent) = bump.alloc(move |evt: SyntheticEvent| match evt {
SyntheticEvent::$wrapper(event) => callback(event),
_ => unreachable!("Downcasted SyntheticEvent to wrong event type - this is an internal bug!")
});
let callback: BumpBox<dyn FnMut(VirtualEvent) + 'a> = unsafe { BumpBox::from_raw(cb) };
let callback: BumpBox<dyn FnMut(SyntheticEvent) + 'a> = unsafe { BumpBox::from_raw(cb) };
let event_name = stringify!($name);
let shortname: &'static str = &event_name[2..];
@ -292,14 +139,6 @@ pub mod on {
}
// The Dioxus Synthetic event system
//
//
//
//
//
//
//
//
event_directory! {
ClipboardEventInner(ClipboardEvent): [
/// Called when "copy"
@ -447,7 +286,13 @@ pub mod on {
onmousemove
/// onmouseout
onmouseout
///
onscroll
/// onmouseover
///
/// Triggered when the users's mouse hovers over an element.
onmouseover
/// onmouseup
onmouseup
@ -492,14 +337,9 @@ pub mod on {
ontouchstart
];
UIEventInner(UIEvent): [
///
scroll
];
WheelEventInner(WheelEvent): [
///
wheel
onwheel
];
MediaEventInner(MediaEvent): [
@ -571,7 +411,11 @@ pub mod on {
];
}
pub struct GenericEvent(pub Rc<dyn GenericEventInner>);
pub trait GenericEventInner {
/// Return a reference to the raw event. User will need to downcast the event to the right platform-specific type.
fn raw_event(&self) -> &dyn Any;
/// Returns whether or not a specific event is a bubbling event
fn bubbles(&self) -> bool;
/// Sets or returns whether the event should propagate up the hierarchy or not
@ -580,14 +424,18 @@ pub mod on {
fn cancelable(&self) -> bool;
/// Returns whether the event is composed or not
fn composed(&self) -> bool;
/// Returns the event's path
fn composed_path(&self) -> String;
// Currently not supported because those no way we could possibly support it
// just cast the event to the right platform-specific type and return it
// /// Returns the event's path
// fn composed_path(&self) -> String;
/// Returns the element whose event listeners triggered the event
fn current_target(&self);
/// Returns whether or not the preventDefault method was called for the event
fn default_prevented(&self) -> bool;
/// Returns which phase of the event flow is currently being evaluated
fn event_phase(&self) -> usize;
fn event_phase(&self) -> u16;
/// Returns whether or not an event is trusted
fn is_trusted(&self) -> bool;
/// Cancels the event if it is cancelable, meaning that the default action that belongs to the event will
@ -599,7 +447,7 @@ pub mod on {
/// Returns the element that triggered the event
fn target(&self);
/// Returns the time (in milliseconds relative to the epoch) at which the event was created
fn time_stamp(&self) -> usize;
fn time_stamp(&self) -> f64;
}
pub trait ClipboardEventInner {
@ -611,8 +459,31 @@ pub mod on {
}
pub trait KeyboardEventInner {
fn alt_key(&self) -> bool;
fn char_code(&self) -> u32;
/// Identify which "key" was entered.
///
/// This is the best method to use for all languages. They key gets mapped to a String sequence which you can match on.
/// The key isn't an enum because there are just so many context-dependent keys.
///
/// A full list on which keys to use is available at:
/// <https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values>
///
/// # Example
///
/// ```rust
/// match event.key().as_str() {
/// "Esc" | "Escape" => {}
/// "ArrowDown" => {}
/// "ArrowLeft" => {}
/// _ => {}
/// }
/// ```
///
fn key(&self) -> String;
/// Get the key code as an enum Variant.
///
/// This is intended for things like arrow keys, escape keys, function keys, and other non-international keys.
@ -636,35 +507,14 @@ pub mod on {
/// Check if the ctrl key was pressed down
fn ctrl_key(&self) -> bool;
/// Identify which "key" was entered.
///
/// This is the best method to use for all languages. They key gets mapped to a String sequence which you can match on.
/// The key isn't an enum because there are just so many context-dependent keys.
///
/// A full list on which keys to use is available at:
/// <https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values>
///
/// # Example
///
/// ```rust
/// match event.key().as_str() {
/// "Esc" | "Escape" => {}
/// "ArrowDown" => {}
/// "ArrowLeft" => {}
/// _ => {}
/// }
/// ```
///
fn key(&self) -> String;
fn get_modifier_state(&self, key_code: &str) -> bool;
// fn key(&self) -> String;
fn locale(&self) -> String;
fn location(&self) -> usize;
fn meta_key(&self) -> bool;
fn repeat(&self) -> bool;
fn shift_key(&self) -> bool;
fn which(&self) -> usize;
fn get_modifier_state(&self, key_code: usize) -> bool;
}
pub trait FocusEventInner {
@ -695,8 +545,8 @@ pub mod on {
pub trait PointerEventInner {
// Mouse only
fn alt_key(&self) -> bool;
fn button(&self) -> usize;
fn buttons(&self) -> usize;
fn button(&self) -> i16;
fn buttons(&self) -> u16;
fn client_x(&self) -> i32;
fn client_y(&self) -> i32;
fn ctrl_key(&self) -> bool;
@ -706,12 +556,12 @@ pub mod on {
fn screen_x(&self) -> i32;
fn screen_y(&self) -> i32;
fn shift_key(&self) -> bool;
fn get_modifier_state(&self, key_code: usize) -> bool;
fn pointer_id(&self) -> usize;
fn width(&self) -> usize;
fn height(&self) -> usize;
fn pressure(&self) -> usize;
fn tangential_pressure(&self) -> usize;
fn get_modifier_state(&self, key_code: &str) -> bool;
fn pointer_id(&self) -> i32;
fn width(&self) -> i32;
fn height(&self) -> i32;
fn pressure(&self) -> f32;
fn tangential_pressure(&self) -> f32;
fn tilt_x(&self) -> i32;
fn tilt_y(&self) -> i32;
fn twist(&self) -> i32;
@ -726,7 +576,7 @@ pub mod on {
fn ctrl_key(&self) -> bool;
fn meta_key(&self) -> bool;
fn shift_key(&self) -> bool;
fn get_modifier_state(&self, key_code: usize) -> bool;
fn get_modifier_state(&self, key_code: &str) -> bool;
// changedTouches: DOMTouchList,
// targetTouches: DOMTouchList,
// touches: DOMTouchList,
@ -738,10 +588,10 @@ pub mod on {
}
pub trait WheelEventInner {
fn delta_mode(&self) -> i32;
fn delta_x(&self) -> i32;
fn delta_y(&self) -> i32;
fn delta_z(&self) -> i32;
fn delta_mode(&self) -> u32;
fn delta_x(&self) -> f64;
fn delta_y(&self) -> f64;
fn delta_z(&self) -> f64;
}
pub trait MediaEventInner {}
@ -788,16 +638,16 @@ pub mod on {
DownArrow = 40,
Insert = 45,
Delete = 46,
_0 = 48,
_1 = 49,
_2 = 50,
_3 = 51,
_4 = 52,
_5 = 53,
_6 = 54,
_7 = 55,
_8 = 56,
_9 = 57,
Num0 = 48,
Num1 = 49,
Num2 = 50,
Num3 = 51,
Num4 = 52,
Num5 = 53,
Num6 = 54,
Num7 = 55,
Num8 = 56,
Num9 = 57,
A = 65,
B = 66,
C = 67,
@ -893,16 +743,16 @@ pub mod on {
40 => DownArrow,
45 => Insert,
46 => Delete,
48 => _0,
49 => _1,
50 => _2,
51 => _3,
52 => _4,
53 => _5,
54 => _6,
55 => _7,
56 => _8,
57 => _9,
48 => Num0,
49 => Num1,
50 => Num2,
51 => Num3,
52 => Num4,
53 => Num5,
54 => Num6,
55 => Num7,
56 => Num8,
57 => Num9,
65 => A,
66 => B,
67 => C,

View file

@ -1,10 +1,10 @@
use std::{
any::Any,
cell::{Cell, UnsafeCell},
cell::{Cell, RefCell, UnsafeCell},
};
pub struct HookList {
vals: appendlist::AppendList<InnerHook<Box<dyn Any>>>,
pub(crate) struct HookList {
vals: RefCell<Vec<InnerHook<Box<dyn Any>>>>,
idx: Cell<usize>,
}
@ -20,6 +20,7 @@ impl Default for HookList {
struct InnerHook<T> {
cell: UnsafeCell<T>,
}
impl<T> InnerHook<T> {
fn new(new: T) -> Self {
Self {
@ -29,33 +30,16 @@ impl<T> InnerHook<T> {
}
impl HookList {
/// Unsafely get a mutable reference to any of the hooks
///
/// This is unsafe because an &mut T might be aliased if the hook data is already borrowed/in use in the component
///
/// This method should be reserved for internal methods that are guaranteed that this hook is not aliased anyhwere
/// inside the component body, or outside into children components.
///
/// This method is currently used only by the suspense system whose hook implementation guarantees that all &T is dropped
/// before the suspense handler is ran.
pub(crate) unsafe fn get_mut<T: 'static>(&self, idx: usize) -> Option<&mut T> {
self.vals.get(idx).and_then(|inn| {
let raw_box = unsafe { &mut *inn.cell.get() };
raw_box.downcast_mut::<T>()
})
}
pub(crate) fn next<T: 'static>(&self) -> Option<&mut T> {
self.vals.get(self.idx.get()).and_then(|inn| {
self.vals.borrow().get(self.idx.get()).and_then(|inn| {
self.idx.set(self.idx.get() + 1);
let raw_box = unsafe { &mut *inn.cell.get() };
raw_box.downcast_mut::<T>()
})
}
#[inline]
pub(crate) fn push<T: 'static>(&self, new: T) {
self.vals.push(InnerHook::new(Box::new(new)))
self.vals.borrow_mut().push(InnerHook::new(Box::new(new)))
}
/// This resets the internal iterator count
@ -70,7 +54,7 @@ impl HookList {
#[inline]
pub(crate) fn len(&self) -> usize {
self.vals.len()
self.vals.borrow().len()
}
#[inline]

View file

@ -3,17 +3,17 @@
//! This module contains all the low-level built-in hooks that require 1st party support to work.
//!
//! Hooks:
//! - use_hook
//! - use_state_provider
//! - use_state_consumer
//! - use_task
//! - use_suspense
//! - [`use_hook`]
//! - [`use_state_provider`]
//! - [`use_state_consumer`]
//! - [`use_task`]
//! - [`use_suspense`]
use crate::innerlude::*;
use futures_util::FutureExt;
use std::{
any::{Any, TypeId},
cell::{Cell, RefCell},
cell::RefCell,
future::Future,
rc::Rc,
};
@ -53,15 +53,7 @@ where
(true, true) => {}
// Needs to be initialized
(false, false) => {
log::debug!("Initializing context...");
cx.add_shared_state(init());
log::info!(
"There are now {} shared contexts for scope {:?}",
cx.scope.shared_contexts.borrow().len(),
cx.scope.our_arena_idx,
);
}
(false, false) => cx.add_shared_state(init()),
_ => debug_assert!(false, "Cannot initialize two contexts of the same type"),
};
@ -119,21 +111,12 @@ where
let slot = task_dump.clone();
let updater = cx.prepare_update();
let update_id = cx.get_scope_id();
let originator = cx.scope.our_arena_idx.clone();
let originator = cx.scope.our_arena_idx;
let handle = cx.submit_task(Box::pin(task_fut.then(move |output| async move {
*slot.as_ref().borrow_mut() = Some(output);
updater(update_id);
EventTrigger {
event: VirtualEvent::AsyncEvent {
should_rerender: false,
},
originator,
priority: EventPriority::Low,
real_node_id: None,
}
updater(originator);
originator
})));
TaskHook {
@ -172,79 +155,89 @@ where
Out: 'static,
Cb: for<'a> Fn(SuspendedContext<'a>, &Out) -> DomTree<'a> + 'static,
{
/*
General strategy:
- Create a slot for the future to dump its output into
- Create a new future feeding off the user's future that feeds the output into that slot
- Submit that future as a task
- Take the task handle id and attach that to our suspended node
- when the hook runs, check if the value exists
- if it does, then we can render the node directly
- if it doesn't, then we render a suspended node along with with the callback and task id
*/
cx.use_hook(
move |hook_idx| {
let value = Rc::new(RefCell::new(None));
let dom_node_id = Rc::new(empty_cell());
let domnode = dom_node_id.clone();
let slot = value.clone();
let callback: SuspendedCallback = Box::new(move |ctx: SuspendedContext| {
let v: std::cell::Ref<Option<Box<dyn Any>>> = slot.as_ref().borrow();
match v.as_ref() {
Some(a) => {
let v: &dyn Any = a.as_ref();
let real_val = v.downcast_ref::<Out>().unwrap();
user_callback(ctx, real_val)
}
None => {
//
Some(VNode {
key: None,
kind: VNodeKind::Suspended(VSuspended {
node: domnode.clone(),
}),
})
}
}
});
let originator = cx.scope.our_arena_idx.clone();
let task_fut = task_initializer();
let domnode = dom_node_id.clone();
let slot = value.clone();
cx.submit_task(Box::pin(task_fut.then(move |output| async move {
// When the new value arrives, set the hooks internal slot
// Dioxus will call the user's callback to generate new nodes outside of the diffing system
*slot.borrow_mut() = Some(Box::new(output) as Box<dyn Any>);
EventTrigger {
event: VirtualEvent::SuspenseEvent { hook_idx, domnode },
originator,
priority: EventPriority::Low,
real_node_id: None,
}
})));
let handle = cx.submit_task(Box::pin(task_initializer().then(
move |output| async move {
*slot.borrow_mut() = Some(Box::new(output) as Box<dyn Any>);
originator
},
)));
SuspenseHook {
value,
callback,
dom_node_id,
}
SuspenseHook { handle, value }
},
move |hook| {
let cx = Context {
scope: &cx.scope,
props: &(),
};
let csx = SuspendedContext { inner: cx };
(&hook.callback)(csx)
move |hook| match hook.value.borrow().as_ref() {
Some(value) => {
let out = value.downcast_ref::<Out>().unwrap();
let sus = SuspendedContext {
inner: Context {
props: &(),
scope: cx.scope,
},
};
user_callback(sus, out)
}
None => {
let value = hook.value.clone();
cx.render(LazyNodes::new(|f| {
let bump = f.bump();
use bumpalo::boxed::Box as BumpBox;
let f: &mut dyn FnMut(SuspendedContext<'src>) -> DomTree<'src> =
bump.alloc(move |sus| {
let val = value.borrow();
let out = val
.as_ref()
.unwrap()
.as_ref()
.downcast_ref::<Out>()
.unwrap();
user_callback(sus, out)
});
let callback = unsafe { BumpBox::from_raw(f) };
VNode::Suspended(bump.alloc(VSuspended {
dom_id: empty_cell(),
task_id: hook.handle.our_id,
callback: RefCell::new(Some(callback)),
}))
}))
}
},
|_| {},
)
}
pub(crate) struct SuspenseHook {
pub handle: TaskHandle,
pub value: Rc<RefCell<Option<Box<dyn Any>>>>,
pub callback: SuspendedCallback,
pub dom_node_id: Rc<Cell<Option<ElementId>>>,
}
type SuspendedCallback = Box<dyn for<'a> Fn(SuspendedContext<'a>) -> DomTree<'a>>;
pub struct SuspendedContext<'a> {
pub(crate) inner: Context<'a, ()>,
}
impl<'src> SuspendedContext<'src> {
pub fn render<F: FnOnce(NodeFactory<'src>) -> VNode<'src>>(
self,
@ -256,3 +249,19 @@ impl<'src> SuspendedContext<'src> {
Some(lazy_nodes.into_vnode(NodeFactory { bump }))
}
}
#[derive(Clone, Copy)]
pub struct NodeRef<'src, T: 'static>(&'src RefCell<T>);
pub fn use_node_ref<T, P>(cx: Context<P>) -> NodeRef<T> {
cx.use_hook(
|f| {},
|f| {
//
todo!()
},
|f| {
//
},
)
}

View file

@ -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;
}

View file

@ -0,0 +1,247 @@
//! Instructions returned by the VirtualDOM on how to modify the Real DOM.
//!
//! This module contains an internal API to generate these instructions.
use crate::innerlude::*;
use std::any::Any;
#[derive(Debug)]
pub struct Mutations<'a> {
pub edits: Vec<DomEdit<'a>>,
pub noderefs: Vec<NodeRefMutation<'a>>,
}
use DomEdit::*;
impl<'a> Mutations<'a> {
pub(crate) fn new() -> Self {
let edits = Vec::new();
let noderefs = Vec::new();
Self { edits, noderefs }
}
// Navigation
pub(crate) fn push_root(&mut self, root: ElementId) {
let id = root.as_u64();
self.edits.push(PushRoot { id });
}
pub(crate) fn pop(&mut self) {
self.edits.push(PopRoot {});
}
pub(crate) fn replace_with(&mut self, root: ElementId, m: u32) {
let root = root.as_u64();
self.edits.push(ReplaceWith { m, root });
}
pub(crate) fn insert_after(&mut self, root: ElementId, n: u32) {
let root = root.as_u64();
self.edits.push(InsertAfter { n, root });
}
pub(crate) fn insert_before(&mut self, root: ElementId, n: u32) {
let root = root.as_u64();
self.edits.push(InsertBefore { n, root });
}
// Remove Nodesfrom the dom
pub(crate) fn remove(&mut self, id: u64) {
self.edits.push(Remove { root: id });
}
// Create
pub(crate) fn create_text_node(&mut self, text: &'a str, id: ElementId) {
let id = id.as_u64();
self.edits.push(CreateTextNode { text, id });
}
pub(crate) fn create_element(
&mut self,
tag: &'static str,
ns: Option<&'static str>,
id: ElementId,
) {
let id = id.as_u64();
match ns {
Some(ns) => self.edits.push(CreateElementNs { id, ns, tag }),
None => self.edits.push(CreateElement { id, tag }),
}
}
// placeholders are nodes that don't get rendered but still exist as an "anchor" in the real dom
pub(crate) fn create_placeholder(&mut self, id: ElementId) {
let id = id.as_u64();
self.edits.push(CreatePlaceholder { id });
}
// events
pub(crate) fn new_event_listener(&mut self, listener: &Listener, scope: ScopeId) {
let Listener {
event,
mounted_node,
..
} = listener;
let element_id = mounted_node.get().unwrap().as_u64();
self.edits.push(NewEventListener {
scope,
event_name: event,
mounted_node_id: element_id,
});
}
pub(crate) fn remove_event_listener(&mut self, event: &'static str) {
self.edits.push(RemoveEventListener { event });
}
// modify
pub(crate) fn set_text(&mut self, text: &'a str) {
self.edits.push(SetText { text });
}
pub(crate) fn set_attribute(&mut self, attribute: &'a Attribute) {
let Attribute {
name,
value,
namespace,
..
} = attribute;
self.edits.push(SetAttribute {
field: name,
value,
ns: *namespace,
});
}
pub(crate) fn remove_attribute(&mut self, attribute: &Attribute) {
let name = attribute.name;
self.edits.push(RemoveAttribute { name });
}
}
// refs are only assigned once
pub struct NodeRefMutation<'a> {
pub element: &'a mut Option<once_cell::sync::OnceCell<Box<dyn Any>>>,
pub element_id: ElementId,
}
impl<'a> std::fmt::Debug for NodeRefMutation<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NodeRefMutation")
.field("element_id", &self.element_id)
.finish()
}
}
impl<'a> NodeRefMutation<'a> {
pub fn downcast_ref<T: 'static>(&self) -> Option<&T> {
self.element
.as_ref()
.and_then(|f| f.get())
.and_then(|f| f.downcast_ref::<T>())
}
pub fn downcast_mut<T: 'static>(&mut self) -> Option<&mut T> {
self.element
.as_mut()
.and_then(|f| f.get_mut())
.and_then(|f| f.downcast_mut::<T>())
}
}
/// A `DomEdit` represents a serialzied form of the VirtualDom's trait-based API. This allows streaming edits across the
/// network or through FFI boundaries.
#[derive(Debug, PartialEq)]
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type")
)]
pub enum DomEdit<'bump> {
PushRoot {
id: u64,
},
PopRoot,
AppendChildren {
many: u32,
},
// "Root" refers to the item direclty
// it's a waste of an instruction to push the root directly
ReplaceWith {
root: u64,
m: u32,
},
InsertAfter {
root: u64,
n: u32,
},
InsertBefore {
root: u64,
n: u32,
},
Remove {
root: u64,
},
RemoveAllChildren,
CreateTextNode {
text: &'bump str,
id: u64,
},
CreateElement {
tag: &'bump str,
id: u64,
},
CreateElementNs {
tag: &'bump str,
id: u64,
ns: &'static str,
},
CreatePlaceholder {
id: u64,
},
NewEventListener {
event_name: &'static str,
scope: ScopeId,
mounted_node_id: u64,
},
RemoveEventListener {
event: &'static str,
},
SetText {
text: &'bump str,
},
SetAttribute {
field: &'static str,
value: &'bump str,
ns: Option<&'bump str>,
},
RemoveAttribute {
name: &'static str,
},
}
impl DomEdit<'_> {
pub fn is(&self, id: &'static str) -> bool {
match self {
DomEdit::InsertAfter { .. } => id == "InsertAfter",
DomEdit::InsertBefore { .. } => id == "InsertBefore",
DomEdit::PushRoot { .. } => id == "PushRoot",
DomEdit::PopRoot => id == "PopRoot",
DomEdit::AppendChildren { .. } => id == "AppendChildren",
DomEdit::ReplaceWith { .. } => id == "ReplaceWith",
DomEdit::Remove { .. } => id == "Remove",
DomEdit::RemoveAllChildren => id == "RemoveAllChildren",
DomEdit::CreateTextNode { .. } => id == "CreateTextNode",
DomEdit::CreateElement { .. } => id == "CreateElement",
DomEdit::CreateElementNs { .. } => id == "CreateElementNs",
DomEdit::CreatePlaceholder { .. } => id == "CreatePlaceholder",
DomEdit::NewEventListener { .. } => id == "NewEventListener",
DomEdit::RemoveEventListener { .. } => id == "RemoveEventListener",
DomEdit::SetText { .. } => id == "SetText",
DomEdit::SetAttribute { .. } => id == "SetAttribute",
DomEdit::RemoveAttribute { .. } => id == "RemoveAttribute",
}
}
}

View file

@ -0,0 +1,119 @@
// let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap();
// let mut garbage_list = scope.consume_garbage();
// let mut scopes_to_kill = Vec::new();
// while let Some(node) = garbage_list.pop() {
// match &node.kind {
// VNodeKind::Text(_) => {
// self.shared.collect_garbage(node.direct_id());
// }
// VNodeKind::Anchor(_) => {
// self.shared.collect_garbage(node.direct_id());
// }
// VNodeKind::Suspended(_) => {
// self.shared.collect_garbage(node.direct_id());
// }
// VNodeKind::Element(el) => {
// self.shared.collect_garbage(node.direct_id());
// for child in el.children {
// garbage_list.push(child);
// }
// }
// VNodeKind::Fragment(frag) => {
// for child in frag.children {
// garbage_list.push(child);
// }
// }
// VNodeKind::Component(comp) => {
// // TODO: run the hook destructors and then even delete the scope
// let scope_id = comp.ass_scope.get().unwrap();
// let scope = self.get_scope(scope_id).unwrap();
// let root = scope.root();
// garbage_list.push(root);
// scopes_to_kill.push(scope_id);
// }
// }
// }
// for scope in scopes_to_kill {
// // oy kill em
// log::debug!("should be removing scope {:#?}", scope);
// }
// // On the primary event queue, there is no batching, we take them off one-by-one
// let trigger = match receiver.try_next() {
// Ok(Some(trigger)) => trigger,
// _ => {
// // Continuously poll the future pool and the event receiver for work
// let mut tasks = self.shared.async_tasks.borrow_mut();
// let tasks_tasks = tasks.next();
// // if the new event generates work more important than our current fiber, we should consider switching
// // only switch if it impacts different scopes.
// let mut ui_receiver = self.shared.ui_event_receiver.borrow_mut();
// let ui_reciv_task = ui_receiver.next();
// // right now, this polling method will only catch batched set_states that don't get awaited.
// // However, in the future, we might be interested in batching set_states across await points
// let immediate_tasks = ();
// futures_util::pin_mut!(tasks_tasks);
// futures_util::pin_mut!(ui_reciv_task);
// // Poll the event receiver and the future pool for work
// // Abort early if our deadline has ran out
// let mut deadline = (&mut deadline_future).fuse();
// let trig = futures_util::select! {
// trigger = tasks_tasks => trigger,
// trigger = ui_reciv_task => trigger,
// // abort if we're out of time
// _ = deadline => { return Ok(diff_machine.mutations); }
// };
// trig.unwrap()
// }
// };
// async fn select_next_event(&mut self) -> Option<EventTrigger> {
// let mut receiver = self.shared.task_receiver.borrow_mut();
// // drain the in-flight events so that we can sort them out with the current events
// while let Ok(Some(trigger)) = receiver.try_next() {
// log::info!("retrieving event from receiver");
// let key = self.shared.make_trigger_key(&trigger);
// self.pending_events.insert(key, trigger);
// }
// if self.pending_events.is_empty() {
// // Continuously poll the future pool and the event receiver for work
// let mut tasks = self.shared.async_tasks.borrow_mut();
// let tasks_tasks = tasks.next();
// let mut receiver = self.shared.task_receiver.borrow_mut();
// let reciv_task = receiver.next();
// futures_util::pin_mut!(tasks_tasks);
// futures_util::pin_mut!(reciv_task);
// let trigger = match futures_util::future::select(tasks_tasks, reciv_task).await {
// futures_util::future::Either::Left((trigger, _)) => trigger,
// futures_util::future::Either::Right((trigger, _)) => trigger,
// }
// .unwrap();
// let key = self.shared.make_trigger_key(&trigger);
// self.pending_events.insert(key, trigger);
// }
// // pop the most important event off
// let key = self.pending_events.keys().next().unwrap().clone();
// let trigger = self.pending_events.remove(&key).unwrap();
// Some(trigger)
// }

View file

@ -1,80 +1,216 @@
//! Virtual Node Support
//! --------------------
//! VNodes represent lazily-constructed VDom trees that support diffing and event handlers.
//!
//! These VNodes should be *very* cheap and *very* fast to construct - building a full tree should be insanely quick.
use crate::{
events::VirtualEvent,
innerlude::{empty_cell, Context, DomTree, ElementId, Properties, Scope, ScopeId, FC},
//! VNodes represent lazily-constructed VDom trees that support diffing and event handlers. These VNodes should be *very*
//! cheap and *very* fast to construct - building a full tree should be quick.
use crate::innerlude::{
empty_cell, Context, DomTree, ElementId, Properties, Scope, ScopeId, SuspendedContext,
SyntheticEvent, FC,
};
use bumpalo::{boxed::Box as BumpBox, Bump};
use std::{
cell::{Cell, RefCell},
fmt::{Arguments, Debug, Formatter},
marker::PhantomData,
mem::ManuallyDrop,
rc::Rc,
};
pub struct VNode<'src> {
pub kind: VNodeKind<'src>,
/// A composable "VirtualNode" to declare a User Interface in the Dioxus VirtualDOM.
///
/// VNodes are designed to be lightweight and used with with a bump alloactor. To create a VNode, you can use either of:
/// - the [`rsx`] macro
/// - the [`html`] macro
/// - the [`NodeFactory`] API
pub enum VNode<'src> {
/// Text VNodes simply bump-allocated (or static) string slices
///
/// # Example
///
/// let node = cx.render(rsx!{ "hello" }).unwrap();
///
/// if let VNode::Text(vtext) = node {
/// assert_eq!(vtext.text, "hello");
/// assert_eq!(vtext.dom_id.get(), None);
/// assert_eq!(vtext.is_static, true);
/// }
/// ```
Text(VText<'src>),
pub(crate) key: Option<&'src str>,
/// Element VNodes are VNodes that may contain attributes, listeners, a key, a tag, and children.
///
/// # Example
///
/// ```rust
/// let node = cx.render(rsx!{
/// div {
/// key: "a",
/// onclick: |e| log::info!("clicked"),
/// hidden: "true",
/// style: { background_color: "red" }
/// "hello"
/// }
/// }).unwrap();
/// if let VNode::Element(velement) = node {
/// assert_eq!(velement.tag_name, "div");
/// assert_eq!(velement.namespace, None);
/// assert_eq!(velement.key, Some("a));
/// }
/// ```
Element(&'src VElement<'src>),
/// Fragment nodes may contain many VNodes without a single root.
///
/// # Example
///
/// ```rust
/// rsx!{
/// a {}
/// link {}
/// style {}
/// "asd"
/// Example {}
/// }
/// ```
Fragment(VFragment<'src>),
/// Component nodes represent a mounted component with props, children, and a key.
///
/// # Example
///
/// ```rust
/// fn Example(cx: Context<()>) -> DomTree {
/// todo!()
/// }
///
/// let node = cx.render(rsx!{
/// Example {}
/// }).unwrap();
///
/// if let VNode::Component(vcomp) = node {
/// assert_eq!(vcomp.user_fc, Example as *const ());
/// }
/// ```
Component(&'src VComponent<'src>),
/// Suspended VNodes represent chunks of the UI tree that are not yet ready to be displayed.
///
/// These nodes currently can only be constructed via the [`use_suspense`] hook.
///
/// # Example
///
/// ```rust
/// rsx!{
/// }
/// ```
Suspended(&'src VSuspended<'src>),
/// Anchors are a type of placeholder VNode used when fragments don't contain any children.
///
/// Anchors cannot be directly constructed via public APIs.
///
/// # Example
///
/// ```rust
/// let node = cx.render(rsx! ( Fragment {} )).unwrap();
/// if let VNode::Fragment(frag) = node {
/// let root = &frag.children[0];
/// assert_eq!(root, VNode::Anchor);
/// }
/// ```
Anchor(VAnchor),
}
impl<'src> VNode<'src> {
/// Get the VNode's "key" used in the keyed diffing algorithm.
pub fn key(&self) -> Option<&'src str> {
self.key
match &self {
VNode::Element(el) => el.key,
VNode::Component(c) => c.key,
VNode::Fragment(f) => f.key,
VNode::Text(_t) => None,
VNode::Suspended(_s) => None,
VNode::Anchor(_f) => None,
}
}
pub fn direct_id(&self) -> ElementId {
self.try_direct_id().unwrap()
/// Get the ElementID of the mounted VNode.
///
/// Panics if the mounted ID is None or if the VNode is not represented by a single Element.
pub fn mounted_id(&self) -> ElementId {
self.try_mounted_id().unwrap()
}
pub fn try_direct_id(&self) -> Option<ElementId> {
match &self.kind {
VNodeKind::Text(el) => el.dom_id.get(),
VNodeKind::Element(el) => el.dom_id.get(),
VNodeKind::Anchor(el) => el.dom_id.get(),
VNodeKind::Fragment(_) => None,
VNodeKind::Component(_) => None,
VNodeKind::Suspended(_) => None,
/// Try to get the ElementID of the mounted VNode.
///
/// Returns None if the VNode is not mounted, or if the VNode cannot be presented by a mounted ID (Fragment/Component)
pub fn try_mounted_id(&self) -> Option<ElementId> {
match &self {
VNode::Text(el) => el.dom_id.get(),
VNode::Element(el) => el.dom_id.get(),
VNode::Anchor(el) => el.dom_id.get(),
VNode::Suspended(el) => el.dom_id.get(),
VNode::Fragment(_) => None,
VNode::Component(_) => None,
}
}
}
/// Tools for the base unit of the virtual dom - the VNode
/// VNodes are intended to be quickly-allocated, lightweight enum values.
///
/// Components will be generating a lot of these very quickly, so we want to
/// limit the amount of heap allocations / overly large enum sizes.
pub enum VNodeKind<'src> {
Text(VText<'src>),
Element(&'src VElement<'src>),
Fragment(VFragment<'src>),
Component(&'src VComponent<'src>),
Suspended(VSuspended),
Anchor(VAnchor),
}
/// A placeholder node only generated when Fragments don't have any children.
pub struct VAnchor {
pub dom_id: Cell<Option<ElementId>>,
}
/// A bump-alloacted string slice and metadata.
pub struct VText<'src> {
pub text: &'src str,
pub dom_id: Cell<Option<ElementId>>,
pub is_static: bool,
}
/// A list of VNodes with no single root.
pub struct VFragment<'src> {
pub key: Option<&'src str>,
pub children: &'src [VNode<'src>],
pub is_static: bool,
}
/// An element like a "div" with children, listeners, and attributes.
pub struct VElement<'a> {
pub tag_name: &'static str,
pub namespace: Option<&'static str>,
pub key: Option<&'a str>,
pub dom_id: Cell<Option<ElementId>>,
pub listeners: &'a [Listener<'a>],
pub attributes: &'a [Attribute<'a>],
pub children: &'a [VNode<'a>],
}
/// A trait for any generic Dioxus Element.
///
/// This trait provides the ability to use custom elements in the `rsx!` macro.
///
/// ```rust
/// struct my_element;
///
/// impl DioxusElement for my_element {
/// const TAG_NAME: "my_element";
/// const NAME_SPACE: None;
/// }
///
/// let _ = rsx!{
/// my_element {}
/// };
/// ```
pub trait DioxusElement {
const TAG_NAME: &'static str;
const NAME_SPACE: Option<&'static str>;
@ -88,22 +224,6 @@ pub trait DioxusElement {
}
}
pub struct VElement<'a> {
// tag is always static
pub tag_name: &'static str,
pub namespace: Option<&'static str>,
pub dom_id: Cell<Option<ElementId>>,
pub static_listeners: bool,
pub listeners: &'a [Listener<'a>],
pub static_attrs: bool,
pub attributes: &'a [Attribute<'a>],
pub static_children: bool,
pub children: &'a [VNode<'a>],
}
/// An attribute on a DOM node, such as `id="my-thing"` or
/// `href="https://example.com"`.
#[derive(Clone, Debug)]
@ -116,35 +236,35 @@ pub struct Attribute<'a> {
pub is_volatile: bool,
// Doesn't exist in the html spec, mostly used to denote "style" tags - could be for any type of group
// Doesn't exist in the html spec.
// Used in Dioxus to denote "style" tags.
pub namespace: Option<&'static str>,
}
/// An event listener.
/// IE onclick, onkeydown, etc
pub struct Listener<'bump> {
/// The type of event to listen for.
pub(crate) event: &'static str,
pub mounted_node: Cell<Option<ElementId>>,
pub(crate) callback: RefCell<Option<BumpBox<'bump, dyn FnMut(VirtualEvent) + 'bump>>>,
}
/// The type of event to listen for.
///
/// IE "onclick" - whatever the renderer needs to attach the listener by name.
pub event: &'static str,
impl Listener<'_> {
// serialize the listener event stuff to a string
pub fn serialize(&self) {
//
}
pub fn deserialize() {
//
}
pub(crate) callback: RefCell<Option<BumpBox<'bump, dyn FnMut(SyntheticEvent) + 'bump>>>,
}
/// Virtual Components for custom user-defined components
/// Only supports the functional syntax
pub struct VComponent<'src> {
pub ass_scope: Cell<Option<ScopeId>>,
pub key: Option<&'src str>,
pub associated_scope: Cell<Option<ScopeId>>,
pub is_static: bool,
// Function pointer to the FC that was used to generate this component
pub user_fc: *const (),
pub(crate) caller: Rc<dyn Fn(&Scope) -> DomTree>,
@ -154,19 +274,17 @@ pub struct VComponent<'src> {
pub(crate) drop_props: RefCell<Option<BumpBox<'src, dyn FnMut()>>>,
pub is_static: bool,
pub(crate) can_memoize: bool,
pub can_memoize: bool,
// a pointer into the bump arena (given by the 'src lifetime)
// Raw pointer into the bump arena for the props of the component
pub(crate) raw_props: *const (),
// a pointer to the raw fn typ
pub(crate) user_fc: *const (),
}
pub struct VSuspended {
pub node: Rc<Cell<Option<ElementId>>>,
pub struct VSuspended<'a> {
pub task_id: u64,
pub dom_id: Cell<Option<ElementId>>,
pub(crate) callback:
RefCell<Option<BumpBox<'a, dyn FnMut(SuspendedContext<'a>) -> DomTree<'a>>>>,
}
/// This struct provides an ergonomic API to quickly build VNodes.
@ -196,26 +314,20 @@ impl<'a> NodeFactory<'a> {
}
pub fn unstable_place_holder() -> VNode<'static> {
VNode {
key: None,
kind: VNodeKind::Text(VText {
text: "",
dom_id: empty_cell(),
is_static: true,
}),
}
VNode::Text(VText {
text: "",
dom_id: empty_cell(),
is_static: true,
})
}
/// Used in a place or two to make it easier to build vnodes from dummy text
/// Directly pass in text blocks without the need to use the format_args macro.
pub fn static_text(&self, text: &'static str) -> VNode<'a> {
VNode {
key: None,
kind: VNodeKind::Text(VText {
dom_id: empty_cell(),
text,
is_static: true,
}),
}
VNode::Text(VText {
dom_id: empty_cell(),
text,
is_static: true,
})
}
/// Parses a lazy text Arguments and returns a string and a flag indicating if the text is 'static
@ -226,9 +338,9 @@ impl<'a> NodeFactory<'a> {
Some(static_str) => (static_str, true),
None => {
use bumpalo::core_alloc::fmt::Write;
let mut s = bumpalo::collections::String::new_in(self.bump());
s.write_fmt(args).unwrap();
(s.into_bump_str(), false)
let mut str_buf = bumpalo::collections::String::new_in(self.bump());
str_buf.write_fmt(args).unwrap();
(str_buf.into_bump_str(), false)
}
}
}
@ -237,14 +349,12 @@ impl<'a> NodeFactory<'a> {
///
pub fn text(&self, args: Arguments) -> VNode<'a> {
let (text, is_static) = self.raw_text(args);
VNode {
key: None,
kind: VNodeKind::Text(VText {
text,
is_static,
dom_id: empty_cell(),
}),
}
VNode::Text(VText {
text,
is_static,
dom_id: empty_cell(),
})
}
pub fn element<L, A, V>(
@ -272,7 +382,7 @@ impl<'a> NodeFactory<'a> {
pub fn raw_element<L, A, V>(
&self,
tag: &'static str,
tag_name: &'static str,
namespace: Option<&'static str>,
listeners: L,
attributes: A,
@ -295,31 +405,15 @@ impl<'a> NodeFactory<'a> {
let key = key.map(|f| self.raw_text(f).0);
VNode {
VNode::Element(self.bump().alloc(VElement {
tag_name,
key,
kind: VNodeKind::Element(self.bump().alloc(VElement {
tag_name: tag,
namespace,
listeners,
attributes,
children,
dom_id: empty_cell(),
// todo: wire up more constization
static_listeners: false,
static_attrs: false,
static_children: false,
})),
}
}
pub fn suspended() -> VNode<'static> {
VNode {
key: None,
kind: VNodeKind::Suspended(VSuspended {
node: Rc::new(empty_cell()),
}),
}
namespace,
listeners,
attributes,
children,
dom_id: empty_cell(),
}))
}
pub fn attr(
@ -339,22 +433,6 @@ impl<'a> NodeFactory<'a> {
}
}
pub fn attr_with_alloc_val(
&self,
name: &'static str,
val: &'a str,
namespace: Option<&'static str>,
is_volatile: bool,
) -> Attribute<'a> {
Attribute {
name,
value: val,
is_static: false,
namespace,
is_volatile,
}
}
pub fn component<P, V>(
&self,
component: FC<P>,
@ -367,14 +445,9 @@ impl<'a> NodeFactory<'a> {
V: 'a + AsRef<[VNode<'a>]>,
{
let bump = self.bump();
// We don't want the fat part of the fat pointer
// This function does static dispatch so we don't need any VTable stuff
let children: &'a V = bump.alloc(children);
let children = children.as_ref();
let props = bump.alloc(props);
let raw_props = props as *mut P as *mut ();
let user_fc = component as *const ();
@ -403,42 +476,50 @@ impl<'a> NodeFactory<'a> {
}
}));
// create a closure to drop the props
let mut has_dropped = false;
let drop_props: &mut dyn FnMut() = bump.alloc_with(|| {
move || unsafe {
if !has_dropped {
let real_other = raw_props as *mut _ as *mut P;
let b = BumpBox::from_raw(real_other);
std::mem::drop(b);
let drop_props = {
// create a closure to drop the props
let mut has_dropped = false;
has_dropped = true;
let drop_props: &mut dyn FnMut() = bump.alloc_with(|| {
move || unsafe {
if !has_dropped {
let real_other = raw_props as *mut _ as *mut P;
let b = BumpBox::from_raw(real_other);
std::mem::drop(b);
has_dropped = true;
}
}
}
});
let drop_props = unsafe { BumpBox::from_raw(drop_props) };
});
let drop_props = unsafe { BumpBox::from_raw(drop_props) };
RefCell::new(Some(drop_props))
};
let is_static = children.len() == 0 && P::IS_STATIC && key.is_none();
let key = key.map(|f| self.raw_text(f).0);
VNode {
let caller = NodeFactory::create_component_caller(component, raw_props);
let can_memoize = children.len() == 0 && P::IS_STATIC;
VNode::Component(bump.alloc_with(|| VComponent {
user_fc,
comparator,
raw_props,
children,
caller,
is_static,
key,
kind: VNodeKind::Component(bump.alloc_with(|| VComponent {
user_fc,
comparator,
raw_props,
children,
caller: NodeFactory::create_component_caller(component, raw_props),
is_static,
drop_props: RefCell::new(Some(drop_props)),
can_memoize: P::IS_STATIC,
ass_scope: Cell::new(None),
})),
}
can_memoize,
drop_props,
associated_scope: Cell::new(None),
}))
}
pub fn create_component_caller<'g, P: 'g>(
pub(crate) fn create_component_caller<'g, P: 'g>(
component: FC<P>,
raw_props: *const (),
) -> Rc<dyn for<'r> Fn(&'r Scope) -> DomTree<'r>> {
@ -450,12 +531,8 @@ impl<'a> NodeFactory<'a> {
props: safe_props,
scope: scp,
};
let res = component(cx);
let g2 = unsafe { std::mem::transmute(res) };
g2
unsafe { std::mem::transmute(res) }
});
unsafe { std::mem::transmute::<_, Captured<'static>>(caller) }
}
@ -463,13 +540,30 @@ impl<'a> NodeFactory<'a> {
pub fn fragment_from_iter(self, node_iter: impl IntoVNodeList<'a>) -> VNode<'a> {
let children = node_iter.into_vnode_list(self);
VNode {
// TODO
// We need a dedicated path in the rsx! macro that will trigger the "you need keys" warning
//
// if cfg!(debug_assertions) {
// if children.len() > 1 {
// if children.last().unwrap().key().is_none() {
// log::error!(
// r#"
// Warning: Each child in an array or iterator should have a unique "key" prop.
// Not providing a key will lead to poor performance with lists.
// See docs.rs/dioxus for more information.
// ---
// To help you identify where this error is coming from, we've generated a backtrace.
// "#,
// );
// }
// }
// }
VNode::Fragment(VFragment {
children,
key: None,
kind: VNodeKind::Fragment(VFragment {
children,
is_static: false,
}),
}
is_static: false,
})
}
}
@ -508,50 +602,51 @@ where
nodes.push(node.into_vnode(cx));
}
if cfg!(debug_assertions) {
if nodes.len() > 1 {
if nodes.last().unwrap().key().is_none() {
log::error!(
r#"
Warning: Each child in an array or iterator should have a unique "key" prop.
Not providing a key will lead to poor performance with lists.
See docs.rs/dioxus for more information.
---
To help you identify where this error is coming from, we've generated a backtrace.
"#,
);
}
}
}
if nodes.len() == 0 {
nodes.push(VNode {
kind: VNodeKind::Anchor(VAnchor {
dom_id: empty_cell(),
}),
key: None,
});
nodes.push(VNode::Anchor(VAnchor {
dom_id: empty_cell(),
}));
}
nodes.into_bump_slice()
}
}
/// Child nodes of the parent component.
///
/// # Example
///
/// ```rust
/// let children = cx.children();
/// let first_node = &children[0];
/// rsx!{
/// h1 { {first_node} }
/// p { {&children[1..]} }
/// }
/// ```
///
pub struct ScopeChildren<'a>(pub &'a [VNode<'a>]);
impl Copy for ScopeChildren<'_> {}
impl<'a> Clone for ScopeChildren<'a> {
fn clone(&self) -> Self {
ScopeChildren(self.0)
}
}
impl ScopeChildren<'_> {
pub unsafe fn extend_lifetime(self) -> ScopeChildren<'static> {
// dangerous method - used to fix the associated lifetime
pub(crate) unsafe fn extend_lifetime(self) -> ScopeChildren<'static> {
std::mem::transmute(self)
}
pub unsafe fn unextend_lfetime<'a>(self) -> ScopeChildren<'a> {
// dangerous method - used to fix the associated lifetime
pub(crate) unsafe fn shorten_lifetime<'a>(self) -> ScopeChildren<'a> {
std::mem::transmute(self)
}
}
impl<'a> IntoVNodeList<'a> for ScopeChildren<'a> {
fn into_vnode_list(self, _: NodeFactory<'a>) -> &'a [VNode<'a>] {
self.0
@ -638,6 +733,7 @@ impl IntoVNode<'_> for Option<()> {
cx.fragment_from_iter(None as Option<VNode>)
}
}
impl<'a> IntoVNode<'a> for Option<VNode<'a>> {
fn into_vnode(self, cx: NodeFactory<'a>) -> VNode<'a> {
match self {
@ -666,14 +762,24 @@ impl Debug for NodeFactory<'_> {
impl Debug for VNode<'_> {
fn fmt(&self, s: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
match &self.kind {
VNodeKind::Element(el) => write!(s, "VElement {{ name: {} }}", el.tag_name),
VNodeKind::Text(t) => write!(s, "VText {{ text: {} }}", t.text),
VNodeKind::Anchor(a) => write!(s, "VAnchor"),
match &self {
VNode::Element(el) => {
//
s.debug_struct("VElement")
.field("name", &el.tag_name)
.field("key", &el.key)
.finish()
}
VNode::Text(t) => write!(s, "VText {{ text: {} }}", t.text),
VNode::Anchor(_) => write!(s, "VAnchor"),
VNodeKind::Fragment(_) => write!(s, "fragment"),
VNodeKind::Suspended { .. } => write!(s, "suspended"),
VNodeKind::Component(_) => write!(s, "component"),
VNode::Fragment(frag) => write!(s, "VFragment {{ children: {:?} }}", frag.children),
VNode::Suspended { .. } => write!(s, "VSuspended"),
VNode::Component(comp) => write!(
s,
"VComponent {{ fc: {:?}, children: {:?} }}",
comp.user_fc, comp.children
),
}
}
}

View file

@ -0,0 +1,813 @@
/*
Welcome to Dioxus's cooperative, priority-based scheduler.
I hope you enjoy your stay.
Some essential reading:
- https://github.com/facebook/react/blob/main/packages/scheduler/src/forks/Scheduler.js#L197-L200
- https://github.com/facebook/react/blob/main/packages/scheduler/src/forks/Scheduler.js#L440
- https://github.com/WICG/is-input-pending
- https://web.dev/rail/
- https://indepth.dev/posts/1008/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react
# What's going on?
Dioxus is a framework for "user experience" - not just "user interfaces." Part of the "experience" is keeping the UI
snappy and "jank free" even under heavy work loads. Dioxus already has the "speed" part figured out - but there's no
point if being "fast" if you can't also be "responsive."
As such, Dioxus can manually decide on what work is most important at any given moment in time. With a properly tuned
priority system, Dioxus can ensure that user interaction is prioritized and committed as soon as possible (sub 100ms).
The controller responsible for this priority management is called the "scheduler" and is responsible for juggling many
different types of work simultaneously.
# How does it work?
Per the RAIL guide, we want to make sure that A) inputs are handled ASAP and B) animations are not blocked.
React-three-fiber is a testament to how amazing this can be - a ThreeJS scene is threaded in between work periods of
React, and the UI still stays snappy!
While it's straightforward to run code ASAP and be as "fast as possible", what's not _not_ straightforward is how to do
this while not blocking the main thread. The current prevailing thought is to stop working periodically so the browser
has time to paint and run animations. When the browser is finished, we can step in and continue our work.
React-Fiber uses the "Fiber" concept to achieve a pause-resume functionality. This is worth reading up on, but not
necessary to understand what we're doing here. In Dioxus, our DiffMachine is guided by DiffInstructions - essentially
"commands" that guide the Diffing algorithm through the tree. Our "diff_scope" method is async - we can literally pause
our DiffMachine "mid-sentence" (so to speak) by just stopping the poll on the future. The DiffMachine periodically yields
so Rust's async machinery can take over, allowing us to customize when exactly to pause it.
React's "should_yield" method is more complex than ours, and I assume we'll move in that direction as Dioxus matures. For
now, Dioxus just assumes a TimeoutFuture, and selects! on both the Diff algorithm and timeout. If the DiffMachine finishes
before the timeout, then Dioxus will work on any pending work in the interim. If there is no pending work, then the changes
are committed, and coroutines are polled during the idle period. However, if the timeout expires, then the DiffMachine
future is paused and saved (self-referentially).
# Priorty System
So far, we've been able to thread our Dioxus work between animation frames - the main thread is not blocked! But that
doesn't help us _under load_. How do we still stay snappy... even if we're doing a lot of work? Well, that's where
priorities come into play. The goal with priorities is to schedule shorter work as a "high" priority and longer work as
a "lower" priority. That way, we can interrupt long-running low-prioty work with short-running high-priority work.
React's priority system is quite complex.
There are 5 levels of priority and 2 distinctions between UI events (discrete, continuous). I believe React really only
uses 3 priority levels and "idle" priority isn't used... Regardless, there's some batching going on.
For Dioxus, we're going with a 4 tier priorty system:
- Sync: Things that need to be done by the next frame, like TextInput on controlled elements
- High: for events that block all others - clicks, keyboard, and hovers
- Medium: for UI events caused by the user but not directly - scrolls/forms/focus (all other events)
- Low: set_state called asynchronously, and anything generated by suspense
In "Sync" state, we abort our "idle wait" future, and resolve the sync queue immediately and escape. Because we completed
work before the next rAF, any edits can be immediately processed before the frame ends. Generally though, we want to leave
as much time to rAF as possible. "Sync" is currently only used by onInput - we'll leave some docs telling people not to
do anything too arduous from onInput.
For the rest, we defer to the rIC period and work down each queue from high to low.
*/
use crate::heuristics::*;
use crate::innerlude::*;
use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
use futures_util::stream::FuturesUnordered;
use futures_util::{future::FusedFuture, pin_mut, Future, FutureExt, StreamExt};
use fxhash::{FxHashMap, FxHashSet};
use indexmap::IndexSet;
use slab::Slab;
use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
cell::{Cell, RefCell, RefMut, UnsafeCell},
collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque},
fmt::Display,
pin::Pin,
rc::Rc,
};
#[derive(Clone)]
pub struct EventChannel {
pub task_counter: Rc<Cell<u64>>,
pub sender: UnboundedSender<SchedulerMsg>,
pub schedule_any_immediate: Rc<dyn Fn(ScopeId)>,
pub submit_task: Rc<dyn Fn(FiberTask) -> TaskHandle>,
pub get_shared_context: Rc<dyn Fn(ScopeId, TypeId) -> Option<Rc<dyn Any>>>,
}
pub enum SchedulerMsg {
Immediate(ScopeId),
UiEvent(UserEvent),
SubmitTask(FiberTask, u64),
ToggleTask(u64),
PauseTask(u64),
ResumeTask(u64),
DropTask(u64),
}
/// The scheduler holds basically everything around "working"
///
/// Each scope has the ability to lightly interact with the scheduler (IE, schedule an update) but ultimately the scheduler calls the components.
///
/// In Dioxus, the scheduler provides 3 priority levels - each with their own "DiffMachine". The DiffMachine state can be saved if the deadline runs
/// out.
///
/// Saved DiffMachine state can be self-referential, so we need to be careful about how we save it. All self-referential data is a link between
/// pending DiffInstructions, Mutations, and their underlying Scope. It's okay for us to be self-referential with this data, provided we don't priority
/// task shift to a higher priority task that needs mutable access to the same scopes.
///
/// We can prevent this safety issue from occurring if we track which scopes are invalidated when starting a new task.
///
///
pub(crate) struct Scheduler {
/// All mounted components are arena allocated to make additions, removals, and references easy to work with
/// A generational arena is used to re-use slots of deleted scopes without having to resize the underlying arena.
///
/// This is wrapped in an UnsafeCell because we will need to get mutable access to unique values in unique bump arenas
/// and rusts's guartnees cannot prove that this is safe. We will need to maintain the safety guarantees manually.
pub pool: ResourcePool,
pub heuristics: HeuristicsEngine,
pub receiver: UnboundedReceiver<SchedulerMsg>,
// Garbage stored
pub pending_garbage: FxHashSet<ScopeId>,
// In-flight futures
pub async_tasks: FuturesUnordered<FiberTask>,
// scheduler stuff
pub current_priority: EventPriority,
pub ui_events: VecDeque<UserEvent>,
pub pending_immediates: VecDeque<ScopeId>,
pub pending_tasks: VecDeque<UserEvent>,
pub garbage_scopes: HashSet<ScopeId>,
pub lanes: [PriorityLane; 4],
}
impl Scheduler {
pub fn new() -> Self {
/*
Preallocate 2000 elements and 100 scopes to avoid dynamic allocation.
Perhaps this should be configurable from some external config?
*/
let components = Rc::new(UnsafeCell::new(Slab::with_capacity(100)));
let raw_elements = Rc::new(UnsafeCell::new(Slab::with_capacity(2000)));
let heuristics = HeuristicsEngine::new();
let (sender, receiver) = futures_channel::mpsc::unbounded::<SchedulerMsg>();
let task_counter = Rc::new(Cell::new(0));
let channel = EventChannel {
task_counter: task_counter.clone(),
sender: sender.clone(),
schedule_any_immediate: {
let sender = sender.clone();
Rc::new(move |id| sender.unbounded_send(SchedulerMsg::Immediate(id)).unwrap())
},
submit_task: {
let sender = sender.clone();
Rc::new(move |fiber_task| {
let task_id = task_counter.get();
task_counter.set(task_id + 1);
sender
.unbounded_send(SchedulerMsg::SubmitTask(fiber_task, task_id))
.unwrap();
TaskHandle {
our_id: task_id,
sender: sender.clone(),
}
})
},
get_shared_context: {
let components = components.clone();
Rc::new(move |id, ty| {
let components = unsafe { &*components.get() };
let mut search: Option<&Scope> = components.get(id.0);
while let Some(inner) = search.take() {
if let Some(shared) = inner.shared_contexts.borrow().get(&ty) {
return Some(shared.clone());
} else {
search = inner.parent_idx.map(|id| components.get(id.0)).flatten();
}
}
None
})
},
};
let pool = ResourcePool {
components: components.clone(),
raw_elements,
channel,
};
Self {
pool,
receiver,
async_tasks: FuturesUnordered::new(),
pending_garbage: FxHashSet::default(),
heuristics,
// a storage for our receiver to dump into
ui_events: VecDeque::new(),
pending_immediates: VecDeque::new(),
pending_tasks: VecDeque::new(),
garbage_scopes: HashSet::new(),
current_priority: EventPriority::Low,
// a dedicated fiber for each priority
lanes: [
PriorityLane::new(),
PriorityLane::new(),
PriorityLane::new(),
PriorityLane::new(),
],
}
}
pub fn manually_poll_events(&mut self) {
while let Ok(Some(msg)) = self.receiver.try_next() {
self.handle_channel_msg(msg);
}
}
// Converts UI events into dirty scopes with various priorities
pub fn consume_pending_events(&mut self) {
while let Some(trigger) = self.ui_events.pop_back() {
if let Some(scope) = self.pool.get_scope_mut(trigger.scope) {
if let Some(element) = trigger.mounted_dom_id {
let priority = match &trigger.event {
SyntheticEvent::ClipboardEvent(_) => {}
SyntheticEvent::CompositionEvent(_) => {}
SyntheticEvent::KeyboardEvent(_) => {}
SyntheticEvent::FocusEvent(_) => {}
SyntheticEvent::FormEvent(_) => {}
SyntheticEvent::SelectionEvent(_) => {}
SyntheticEvent::TouchEvent(_) => {}
SyntheticEvent::WheelEvent(_) => {}
SyntheticEvent::MediaEvent(_) => {}
SyntheticEvent::AnimationEvent(_) => {}
SyntheticEvent::TransitionEvent(_) => {}
SyntheticEvent::ToggleEvent(_) => {}
SyntheticEvent::MouseEvent(_) => {}
SyntheticEvent::PointerEvent(_) => {}
SyntheticEvent::GenericEvent(_) => {}
};
scope.call_listener(trigger.event, element);
// let receiver = self.immediate_receiver.clone();
// let mut receiver = receiver.borrow_mut();
// // Drain the immediates into the dirty scopes, setting the appropiate priorities
// while let Ok(Some(dirty_scope)) = receiver.try_next() {
// self.add_dirty_scope(dirty_scope, trigger.priority)
// }
}
}
}
}
// nothing to do, no events on channels, no work
pub fn has_any_work(&self) -> bool {
let pending_lanes = self.lanes.iter().find(|f| f.has_work()).is_some();
pending_lanes || self.has_pending_events()
}
pub fn has_pending_events(&self) -> bool {
self.ui_events.len() > 0
}
fn shift_priorities(&mut self) {
self.current_priority = match (
self.lanes[0].has_work(),
self.lanes[1].has_work(),
self.lanes[2].has_work(),
self.lanes[3].has_work(),
) {
(true, _, _, _) => EventPriority::Immediate,
(false, true, _, _) => EventPriority::High,
(false, false, true, _) => EventPriority::Medium,
(false, false, false, _) => EventPriority::Low,
};
}
/// re-balance the work lanes, ensuring high-priority work properly bumps away low priority work
fn balance_lanes(&mut self) {}
fn load_current_lane(&mut self) -> &mut PriorityLane {
match self.current_priority {
EventPriority::Immediate => todo!(),
EventPriority::High => todo!(),
EventPriority::Medium => todo!(),
EventPriority::Low => todo!(),
}
}
fn save_work(&mut self, lane: SavedDiffWork) {
let saved: SavedDiffWork<'static> = unsafe { std::mem::transmute(lane) };
self.load_current_lane().saved_state = Some(saved);
}
fn load_work(&mut self) -> SavedDiffWork<'static> {
match self.current_priority {
EventPriority::Immediate => todo!(),
EventPriority::High => todo!(),
EventPriority::Medium => todo!(),
EventPriority::Low => todo!(),
}
}
/// Work the scheduler down, not polling any ongoing tasks.
///
/// Will use the standard priority-based scheduling, batching, etc, but just won't interact with the async reactor.
pub fn work_sync<'a>(&'a mut self) -> Vec<Mutations<'a>> {
let mut committed_mutations = Vec::new();
self.manually_poll_events();
if !self.has_any_work() {
self.clean_up_garbage();
return committed_mutations;
}
self.consume_pending_events();
while self.has_any_work() {
self.shift_priorities();
self.work_on_current_lane(&mut || false, &mut committed_mutations);
}
committed_mutations
}
/// The primary workhorse of the VirtualDOM.
///
/// Uses some fairly complex logic to schedule what work should be produced.
///
/// Returns a list of successful mutations.
pub async fn work_with_deadline<'a>(
&'a mut self,
mut deadline_reached: Pin<Box<impl FusedFuture<Output = ()>>>,
) -> Vec<Mutations<'a>> {
/*
Strategy:
- When called, check for any UI events that might've been received since the last frame.
- Dump all UI events into a "pending discrete" queue and a "pending continuous" queue.
- If there are any pending discrete events, then elevate our priorty level. If our priority level is already "high,"
then we need to finish the high priority work first. If the current work is "low" then analyze what scopes
will be invalidated by this new work. If this interferes with any in-flight medium or low work, then we need
to bump the other work out of the way, or choose to process it so we don't have any conflicts.
'static components have a leg up here since their work can be re-used among multiple scopes.
"High priority" is only for blocking! Should only be used on "clicks"
- If there are no pending discrete events, then check for continuous events. These can be completely batched
Open questions:
- what if we get two clicks from the component during the same slice?
- should we batch?
- react says no - they are continuous
- but if we received both - then we don't need to diff, do we? run as many as we can and then finally diff?
*/
let mut committed_mutations = Vec::<Mutations<'static>>::new();
loop {
// Internalize any pending work since the last time we ran
self.manually_poll_events();
// Wait for any new events if we have nothing to do
if !self.has_any_work() {
self.clean_up_garbage();
let deadline_expired = self.wait_for_any_trigger(&mut deadline_reached).await;
if deadline_expired {
return committed_mutations;
}
}
// Create work from the pending event queue
self.consume_pending_events();
// shift to the correct lane
self.shift_priorities();
let mut deadline_reached = || (&mut deadline_reached).now_or_never().is_some();
let finished_before_deadline =
self.work_on_current_lane(&mut deadline_reached, &mut committed_mutations);
if !finished_before_deadline {
break;
}
}
committed_mutations
}
/// Load the current lane, and work on it, periodically checking in if the deadline has been reached.
///
/// Returns true if the lane is finished before the deadline could be met.
pub fn work_on_current_lane(
&mut self,
deadline_reached: &mut impl FnMut() -> bool,
mutations: &mut Vec<Mutations>,
) -> bool {
// Work through the current subtree, and commit the results when it finishes
// When the deadline expires, give back the work
let saved_state = self.load_work();
// We have to split away some parts of ourself - current lane is borrowed mutably
let mut shared = self.pool.clone();
let mut machine = unsafe { saved_state.promote(&mut shared) };
if machine.stack.is_empty() {
let shared = self.pool.clone();
self.current_lane().dirty_scopes.sort_by(|a, b| {
let h1 = shared.get_scope(*a).unwrap().height;
let h2 = shared.get_scope(*b).unwrap().height;
h1.cmp(&h2)
});
if let Some(scope) = self.current_lane().dirty_scopes.pop() {
let component = self.pool.get_scope(scope).unwrap();
let (old, new) = (component.frames.wip_head(), component.frames.fin_head());
machine.stack.push(DiffInstruction::DiffNode { new, old });
}
}
let deadline_expired = machine.work(deadline_reached);
let machine: DiffMachine<'static> = unsafe { std::mem::transmute(machine) };
let mut saved = machine.save();
if deadline_expired {
self.save_work(saved);
false
} else {
for node in saved.seen_scopes.drain() {
self.current_lane().dirty_scopes.remove(&node);
}
let mut new_mutations = Mutations::new();
std::mem::swap(&mut new_mutations, &mut saved.mutations);
mutations.push(new_mutations);
self.save_work(saved);
true
}
}
// waits for a trigger, canceling early if the deadline is reached
// returns true if the deadline was reached
// does not return the trigger, but caches it in the scheduler
pub async fn wait_for_any_trigger(
&mut self,
deadline: &mut Pin<Box<impl FusedFuture<Output = ()>>>,
) -> bool {
use futures_util::future::{select, Either};
let event_fut = async {
match select(self.receiver.next(), self.async_tasks.next()).await {
Either::Left((msg, _other)) => {
self.handle_channel_msg(msg.unwrap());
}
Either::Right((task, _other)) => {
// do nothing, async task will likely generate a set of scheduler messages
}
}
};
pin_mut!(event_fut);
match select(event_fut, deadline).await {
Either::Left((msg, _other)) => false,
Either::Right((deadline, _)) => true,
}
}
pub fn current_lane(&mut self) -> &mut PriorityLane {
match self.current_priority {
EventPriority::Immediate => &mut self.lanes[0],
EventPriority::High => &mut self.lanes[1],
EventPriority::Medium => &mut self.lanes[2],
EventPriority::Low => &mut self.lanes[3],
}
}
pub fn handle_channel_msg(&mut self, msg: SchedulerMsg) {
match msg {
SchedulerMsg::Immediate(_) => todo!(),
SchedulerMsg::UiEvent(_) => todo!(),
//
SchedulerMsg::SubmitTask(_, _) => todo!(),
SchedulerMsg::ToggleTask(_) => todo!(),
SchedulerMsg::PauseTask(_) => todo!(),
SchedulerMsg::ResumeTask(_) => todo!(),
SchedulerMsg::DropTask(_) => todo!(),
}
}
fn add_dirty_scope(&mut self, scope: ScopeId, priority: EventPriority) {
todo!()
// match priority {
// EventPriority::High => self.high_priorty.dirty_scopes.insert(scope),
// EventPriority::Medium => self.medium_priority.dirty_scopes.insert(scope),
// EventPriority::Low => self.low_priority.dirty_scopes.insert(scope),
// };
}
fn collect_garbage(&mut self, id: ElementId) {
//
}
pub fn clean_up_garbage(&mut self) {
let mut scopes_to_kill = Vec::new();
let mut garbage_list = Vec::new();
for scope in self.garbage_scopes.drain() {
let scope = self.pool.get_scope_mut(scope).unwrap();
for node in scope.consume_garbage() {
garbage_list.push(node);
}
while let Some(node) = garbage_list.pop() {
match &node {
VNode::Text(_) => {
self.pool.collect_garbage(node.mounted_id());
}
VNode::Anchor(_) => {
self.pool.collect_garbage(node.mounted_id());
}
VNode::Suspended(_) => {
self.pool.collect_garbage(node.mounted_id());
}
VNode::Element(el) => {
self.pool.collect_garbage(node.mounted_id());
for child in el.children {
garbage_list.push(child);
}
}
VNode::Fragment(frag) => {
for child in frag.children {
garbage_list.push(child);
}
}
VNode::Component(comp) => {
// TODO: run the hook destructors and then even delete the scope
let scope_id = comp.associated_scope.get().unwrap();
let scope = self.pool.get_scope(scope_id).unwrap();
let root = scope.root();
garbage_list.push(root);
scopes_to_kill.push(scope_id);
}
}
}
}
for scope in scopes_to_kill.drain(..) {
//
// kill em
}
}
}
pub(crate) struct PriorityLane {
pub dirty_scopes: IndexSet<ScopeId>,
pub saved_state: Option<SavedDiffWork<'static>>,
pub in_progress: bool,
}
impl PriorityLane {
pub fn new() -> Self {
Self {
saved_state: None,
dirty_scopes: Default::default(),
in_progress: false,
}
}
fn has_work(&self) -> bool {
todo!()
}
fn work(&mut self) {
let scope = self.dirty_scopes.pop();
}
}
pub struct TaskHandle {
pub sender: UnboundedSender<SchedulerMsg>,
pub our_id: u64,
}
impl TaskHandle {
/// Toggles this coroutine off/on.
///
/// This method is not synchronous - your task will not stop immediately.
pub fn toggle(&self) {
self.sender
.unbounded_send(SchedulerMsg::ToggleTask(self.our_id))
.unwrap()
}
/// This method is not synchronous - your task will not stop immediately.
pub fn resume(&self) {
self.sender
.unbounded_send(SchedulerMsg::ResumeTask(self.our_id))
.unwrap()
}
/// This method is not synchronous - your task will not stop immediately.
pub fn stop(&self) {
self.sender
.unbounded_send(SchedulerMsg::ToggleTask(self.our_id))
.unwrap()
}
/// This method is not synchronous - your task will not stop immediately.
pub fn restart(&self) {
self.sender
.unbounded_send(SchedulerMsg::ToggleTask(self.our_id))
.unwrap()
}
}
#[derive(serde::Serialize, serde::Deserialize, Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct ScopeId(pub usize);
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct ElementId(pub usize);
impl Display for ElementId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl ElementId {
pub fn as_u64(self) -> u64 {
self.0 as u64
}
}
/// Priority of Event Triggers.
///
/// Internally, Dioxus will abort work that's taking too long if new, more important, work arrives. Unlike React, Dioxus
/// won't be afraid to pause work or flush changes to the RealDOM. This is called "cooperative scheduling". Some Renderers
/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well.
///
/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes.
///
/// React has a 5-tier priority system. However, they break things into "Continuous" and "Discrete" priority. For now,
/// we keep it simple, and just use a 3-tier priority system.
///
/// - NoPriority = 0
/// - LowPriority = 1
/// - NormalPriority = 2
/// - UserBlocking = 3
/// - HighPriority = 4
/// - ImmediatePriority = 5
///
/// We still have a concept of discrete vs continuous though - discrete events won't be batched, but continuous events will.
/// This means that multiple "scroll" events will be processed in a single frame, but multiple "click" events will be
/// flushed before proceeding. Multiple discrete events is highly unlikely, though.
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)]
pub enum EventPriority {
/// Work that must be completed during the EventHandler phase.
///
/// Currently this is reserved for controlled inputs.
Immediate = 3,
/// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work.
///
/// This is typically reserved for things like user interaction.
///
/// React calls these "discrete" events, but with an extra category of "user-blocking" (Immediate).
High = 2,
/// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important
/// than "High Priority" events and will take presedence over low priority events.
///
/// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input.
///
/// React calls these "continuous" events (e.g. mouse move, mouse wheel, touch move, etc).
Medium = 1,
/// "Low Priority" work will always be pre-empted unless the work is significantly delayed, in which case it will be
/// advanced to the front of the work queue until completed.
///
/// The primary user of Low Priority work is the asynchronous work system (suspense).
///
/// This is considered "idle" work or "background" work.
Low = 0,
}
#[derive(Clone)]
pub(crate) struct ResourcePool {
/*
This *has* to be an UnsafeCell.
Each BumpFrame and Scope is located in this Slab - and we'll need mutable access to a scope while holding on to
its bumpframe conents immutably.
However, all of the interaction with this Slab is done in this module and the Diff module, so it should be fairly
simple to audit.
Wrapped in Rc so the "get_shared_context" closure can walk the tree (immutably!)
*/
pub components: Rc<UnsafeCell<Slab<Scope>>>,
/*
Yes, a slab of "nil". We use this for properly ordering ElementIDs - all we care about is the allocation strategy
that slab uses. The slab essentially just provides keys for ElementIDs that we can re-use in a Vec on the client.
This just happened to be the simplest and most efficient way to implement a deterministic keyed map with slot reuse.
In the future, we could actually store a pointer to the VNode instead of nil to provide O(1) lookup for VNodes...
*/
pub raw_elements: Rc<UnsafeCell<Slab<()>>>,
pub channel: EventChannel,
}
impl ResourcePool {
/// this is unsafe because the caller needs to track which other scopes it's already using
pub fn get_scope(&self, idx: ScopeId) -> Option<&Scope> {
let inner = unsafe { &*self.components.get() };
inner.get(idx.0)
}
/// this is unsafe because the caller needs to track which other scopes it's already using
pub fn get_scope_mut(&self, idx: ScopeId) -> Option<&mut Scope> {
let inner = unsafe { &mut *self.components.get() };
inner.get_mut(idx.0)
}
pub fn with_scope<'b, O: 'static>(
&'b self,
_id: ScopeId,
_f: impl FnOnce(&'b mut Scope) -> O,
) -> Option<O> {
todo!()
}
// return a bumpframe with a lifetime attached to the arena borrow
// this is useful for merging lifetimes
pub fn with_scope_vnode<'b>(
&self,
_id: ScopeId,
_f: impl FnOnce(&mut Scope) -> &VNode<'b>,
) -> Option<&VNode<'b>> {
todo!()
}
pub fn try_remove(&self, id: ScopeId) -> Option<Scope> {
let inner = unsafe { &mut *self.components.get() };
Some(inner.remove(id.0))
// .try_remove(id.0)
// .ok_or_else(|| Error::FatalInternal("Scope not found"))
}
pub fn reserve_node(&self) -> ElementId {
let els = unsafe { &mut *self.raw_elements.get() };
ElementId(els.insert(()))
}
/// return the id, freeing the space of the original node
pub fn collect_garbage(&self, id: ElementId) {
todo!("garabge collection currently WIP")
// self.raw_elements.remove(id.0);
}
pub fn insert_scope_with_key(&self, f: impl FnOnce(ScopeId) -> Scope) -> ScopeId {
let g = unsafe { &mut *self.components.get() };
let entry = g.vacant_entry();
let id = ScopeId(entry.key());
entry.insert(f(id));
id
}
pub fn borrow_bumpframe(&self) {}
}

View file

@ -1,11 +1,9 @@
use crate::innerlude::*;
use bumpalo::boxed::Box as BumpBox;
use fxhash::FxHashSet;
use std::{
any::{Any, TypeId},
borrow::BorrowMut,
cell::{Cell, RefCell},
collections::{HashMap, HashSet},
cell::RefCell,
collections::HashMap,
future::Future,
pin::Pin,
rc::Rc,
@ -38,20 +36,23 @@ pub struct Scope {
// Listeners
pub(crate) listeners: RefCell<Vec<*const Listener<'static>>>,
pub(crate) borrowed_props: RefCell<Vec<*const VComponent<'static>>>,
pub(crate) suspended_nodes: RefCell<HashMap<u64, *const VSuspended<'static>>>,
// State
pub(crate) hooks: HookList,
pub(crate) shared_contexts: RefCell<HashMap<TypeId, Rc<dyn Any>>>,
// A reference to the resources shared by all the comonents
pub(crate) vdom: SharedResources,
pub(crate) memoized_updater: Rc<dyn Fn() + 'static>,
pub(crate) shared: EventChannel,
}
// The type of closure that wraps calling components
pub type WrappedCaller = dyn for<'b> Fn(&'b Scope) -> DomTree<'b>;
// The type of task that gets sent to the task scheduler
pub type FiberTask = Pin<Box<dyn Future<Output = EventTrigger>>>;
/// The type of task that gets sent to the task scheduler
/// Submitting a fiber task returns a handle to that task, which can be used to wake up suspended nodes
pub type FiberTask = Pin<Box<dyn Future<Output = ScopeId>>>;
impl Scope {
// we are being created in the scope of an existing component (where the creator_node lifetime comes into play)
@ -61,38 +62,30 @@ impl Scope {
//
// Scopes cannot be made anywhere else except for this file
// Therefore, their lifetimes are connected exclusively to the virtual dom
pub fn new<'creator_node>(
pub(crate) fn new<'creator_node>(
caller: Rc<WrappedCaller>,
arena_idx: ScopeId,
parent: Option<ScopeId>,
height: u32,
child_nodes: ScopeChildren,
vdom: SharedResources,
shared: EventChannel,
) -> Self {
let child_nodes = unsafe { child_nodes.extend_lifetime() };
// insert ourself as a descendent of the parent
// when the parent is removed, this map will be traversed, and we will also be cleaned up.
if let Some(parent) = &parent {
let parent = unsafe { vdom.get_scope(*parent) }.unwrap();
parent.descendents.borrow_mut().insert(arena_idx);
}
let up = shared.schedule_any_immediate.clone();
let memoized_updater = Rc::new(move || up(arena_idx));
Self {
memoized_updater,
shared,
child_nodes,
caller,
parent_idx: parent,
our_arena_idx: arena_idx,
height,
vdom,
frames: ActiveFrame::new(),
hooks: Default::default(),
suspended_nodes: Default::default(),
shared_contexts: Default::default(),
listeners: Default::default(),
borrowed_props: Default::default(),
@ -107,17 +100,18 @@ impl Scope {
child_nodes: ScopeChildren,
) {
self.caller = caller;
// let child_nodes = unsafe { std::mem::transmute(child_nodes) };
let child_nodes = unsafe { child_nodes.extend_lifetime() };
self.child_nodes = child_nodes;
}
pub(crate) fn run_scope<'sel>(&'sel mut self) -> Result<()> {
/// Returns true if the scope completed successfully
///
pub(crate) fn run_scope<'sel>(&'sel mut self, pool: &ResourcePool) -> bool {
// Cycle to the next frame and then reset it
// This breaks any latent references, invalidating every pointer referencing into it.
// Remove all the outdated listeners
self.ensure_drop_safety();
self.ensure_drop_safety(pool);
// Safety:
// - We dropped the listeners, so no more &mut T can be used while these are held
@ -132,17 +126,12 @@ impl Scope {
let render: &WrappedCaller = self.caller.as_ref();
match render(self) {
None => {
// the user's component failed. We avoid cycling to the next frame
log::error!("Running your component failed! It will no longer receive events.");
Err(Error::ComponentFailed)
}
None => false,
Some(new_head) => {
// the user's component succeeded. We can safely cycle to the next frame
self.frames.wip_frame_mut().head_node = unsafe { std::mem::transmute(new_head) };
self.frames.cycle_frame();
log::debug!("Cycle okay");
Ok(())
true
}
}
}
@ -156,26 +145,28 @@ impl Scope {
///
/// Refrences to hook data can only be stored in listeners and component props. During diffing, we make sure to log
/// all listeners and borrowed props so we can clear them here.
fn ensure_drop_safety(&mut self) {
pub(crate) fn ensure_drop_safety(&mut self, pool: &ResourcePool) {
// make sure all garabge is collected before trying to proceed with anything else
debug_assert!(
self.pending_garbage.borrow().is_empty(),
"clean up your garabge please"
);
// todo!("arch changes");
// make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we
// run the hooks (which hold an &mut Referrence)
// right now, we don't drop
let vdom = &self.vdom;
// let vdom = &self.vdom;
self.borrowed_props
.get_mut()
.drain(..)
.map(|li| unsafe { &*li })
.for_each(|comp| {
// First drop the component's undropped references
let scope_id = comp.ass_scope.get().unwrap();
let scope = unsafe { vdom.get_scope_mut(scope_id) }.unwrap();
scope.ensure_drop_safety();
let scope_id = comp.associated_scope.get().unwrap();
let scope = pool.get_scope_mut(scope_id).unwrap();
scope.ensure_drop_safety(pool);
// Now, drop our own reference
let mut dropper = comp.drop_props.borrow_mut().take().unwrap();
@ -193,51 +184,24 @@ impl Scope {
}
// A safe wrapper around calling listeners
// calling listeners will invalidate the list of listeners
// The listener list will be completely drained because the next frame will write over previous listeners
pub(crate) fn call_listener(&mut self, trigger: EventTrigger) -> Result<()> {
let EventTrigger {
real_node_id,
event,
..
} = trigger;
if let &VirtualEvent::AsyncEvent { .. } = &event {
log::info!("arrived a fiber event");
return Ok(());
}
log::debug!(
"There are {:?} listeners associated with this scope {:#?}",
self.listeners.borrow().len(),
self.our_arena_idx
);
//
//
pub(crate) fn call_listener(&mut self, event: SyntheticEvent, element: ElementId) {
let listners = self.listeners.borrow_mut();
let raw_listener = listners.iter().find(|lis| {
let search = unsafe { &***lis };
let search_id = search.mounted_node.get();
log::info!(
"searching listener {:#?} for real {:?}",
search_id,
real_node_id
);
match (real_node_id, search_id) {
(Some(e), Some(search_id)) => search_id == e,
_ => false,
// this assumes the node might not be mounted - should we assume that though?
match search_id.map(|f| f == element) {
Some(same) => same,
None => false,
}
});
if let Some(raw_listener) = raw_listener {
let listener = unsafe { &**raw_listener };
// log::info!(
// "calling listener {:?}, {:?}",
// listener.event,
// // listener.scope
// );
let mut cb = listener.callback.borrow_mut();
if let Some(cb) = cb.as_mut() {
(cb)(event);
@ -245,21 +209,35 @@ impl Scope {
} else {
log::warn!("An event was triggered but there was no listener to handle it");
}
Ok(())
}
pub fn root(&self) -> &VNode {
self.frames.fin_head()
pub(crate) fn child_nodes<'a>(&'a self) -> ScopeChildren {
unsafe { self.child_nodes.shorten_lifetime() }
}
pub fn child_nodes<'a>(&'a self) -> ScopeChildren {
unsafe { self.child_nodes.unextend_lfetime() }
pub(crate) fn call_suspended_node<'a>(&'a self, task: u64) {
let g = self.suspended_nodes.borrow_mut();
if let Some(suspended) = g.get(&task) {
let sus: &'a VSuspended<'static> = unsafe { &**suspended };
let sus: &'a VSuspended<'a> = unsafe { std::mem::transmute(sus) };
let bump = self.frames.wip_frame();
let mut cb = sus.callback.borrow_mut();
let mut _cb = cb.take().unwrap();
let cx: SuspendedContext<'a> = SuspendedContext {
inner: Context {
props: &(),
scope: &self,
},
};
let n: DomTree<'a> = (_cb)(cx);
}
}
pub fn consume_garbage(&self) -> Vec<&VNode> {
let mut garbage = self.pending_garbage.borrow_mut();
garbage
pub(crate) fn consume_garbage(&self) -> Vec<&VNode> {
self.pending_garbage
.borrow_mut()
.drain(..)
.map(|node| {
// safety: scopes cannot cycle without their garbage being collected. these nodes are safe
@ -269,4 +247,8 @@ impl Scope {
})
.collect::<Vec<_>>()
}
pub fn root(&self) -> &VNode {
self.frames.fin_head()
}
}

View file

@ -1,5 +0,0 @@
//! Signas: Avoid the Diff Engine
//! -----------------------------
//!
//!
//! TODO!

View file

@ -0,0 +1,82 @@
//! A DOM for testing - both internal and external code.
use bumpalo::Bump;
use crate::innerlude::*;
use crate::nodes::IntoVNode;
pub struct TestDom {
bump: Bump,
scheduler: Scheduler,
}
impl TestDom {
pub fn new() -> TestDom {
let bump = Bump::new();
let mut scheduler = Scheduler::new();
TestDom { bump, scheduler }
}
pub fn new_factory<'a>(&'a self) -> NodeFactory<'a> {
NodeFactory::new(&self.bump)
}
pub fn render<'a, F>(&'a self, lazy_nodes: LazyNodes<'a, F>) -> VNode<'a>
where
F: FnOnce(NodeFactory<'a>) -> VNode<'a>,
{
lazy_nodes.into_vnode(NodeFactory::new(&self.bump))
}
pub fn diff<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Mutations<'a> {
let mutations = Mutations::new();
let mut machine = DiffMachine::new(mutations, &self.scheduler.pool);
machine.stack.push(DiffInstruction::DiffNode { new, old });
machine.mutations
}
pub fn create<'a, F1>(&'a self, left: LazyNodes<'a, F1>) -> Mutations<'a>
where
F1: FnOnce(NodeFactory<'a>) -> VNode<'a>,
{
let old = self.bump.alloc(self.render(left));
let mut machine = DiffMachine::new(Mutations::new(), &self.scheduler.pool);
machine.stack.create_node(old, MountType::Append);
machine.work(&mut || false);
machine.mutations
}
pub fn lazy_diff<'a, F1, F2>(
&'a self,
left: LazyNodes<'a, F1>,
right: LazyNodes<'a, F2>,
) -> (Mutations<'a>, Mutations<'a>)
where
F1: FnOnce(NodeFactory<'a>) -> VNode<'a>,
F2: FnOnce(NodeFactory<'a>) -> VNode<'a>,
{
let old = self.bump.alloc(self.render(left));
let new = self.bump.alloc(self.render(right));
let mut machine = DiffMachine::new(Mutations::new(), &self.scheduler.pool);
machine.stack.create_node(old, MountType::Append);
machine.work(&mut || false);
let create_edits = machine.mutations;
let mut machine = DiffMachine::new(Mutations::new(), &self.scheduler.pool);
machine.stack.push(DiffInstruction::DiffNode { old, new });
machine.work(&mut || false);
let edits = machine.mutations;
(create_edits, edits)
}
}

View file

@ -8,21 +8,59 @@ pub fn empty_cell() -> Cell<Option<ElementId>> {
Cell::new(None)
}
// /// A helper type that lets scopes be ordered by their height
// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
// pub struct HeightMarker {
// pub idx: ScopeId,
// pub height: u32,
// }
pub fn type_name_of<T>(_: T) -> &'static str {
std::any::type_name::<T>()
}
// impl Ord for HeightMarker {
// fn cmp(&self, other: &Self) -> std::cmp::Ordering {
// self.height.cmp(&other.height)
// }
// }
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
// impl PartialOrd for HeightMarker {
// fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
// Some(self.cmp(other))
// }
// }
// use crate::task::{Context, Poll};
/// Cooperatively gives up a timeslice to the task scheduler.
///
/// Calling this function will move the currently executing future to the back
/// of the execution queue, making room for other futures to execute. This is
/// especially useful after running CPU-intensive operations inside a future.
///
/// See also [`task::spawn_blocking`].
///
/// [`task::spawn_blocking`]: fn.spawn_blocking.html
///
/// # Examples
///
/// Basic usage:
///
/// ```
/// # async_std::task::block_on(async {
/// #
/// use async_std::task;
///
/// task::yield_now().await;
/// #
/// # })
/// ```
#[inline]
pub async fn yield_now() {
YieldNow(false).await
}
struct YieldNow(bool);
impl Future for YieldNow {
type Output = ();
// The futures executor is implemented as a FIFO queue, so all this future
// does is re-schedule the future back to the end of the queue, giving room
// for other futures to progress.
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if !self.0 {
self.0 = true;
cx.waker().wake_by_ref();
Poll::Pending
} else {
Poll::Ready(())
}
}
}

View file

@ -14,23 +14,17 @@
//! - The [`ActiveFrame`] object for managing the Scope`s microheap
//! - The [`Context`] object for exposing VirtualDOM API to components
//! - The [`NodeFactory`] object for lazyily exposing the `Context` API to the nodebuilder API
//! - The [`Hook`] object for exposing state management in components.
//!
//! This module includes just the barebones for a complete VirtualDOM API.
//! Additional functionality is defined in the respective files.
#![allow(unreachable_code)]
use futures_util::StreamExt;
use fxhash::FxHashMap;
use crate::hooks::{SuspendedContext, SuspenseHook};
use crate::{arena::SharedResources, innerlude::*};
use std::any::Any;
use std::any::TypeId;
use std::cell::{Ref, RefCell, RefMut};
use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashSet};
use std::pin::Pin;
use crate::innerlude::*;
use futures_util::{Future, FutureExt};
use std::{
any::{Any, TypeId},
pin::Pin,
rc::Rc,
};
/// An integrated virtual node system that progresses events and diffs UI trees.
/// Differences are converted into patches which a renderer can use to draw the UI.
@ -42,150 +36,146 @@ use std::pin::Pin;
///
///
pub struct VirtualDom {
/// All mounted components are arena allocated to make additions, removals, and references easy to work with
/// A generational arena is used to re-use slots of deleted scopes without having to resize the underlying arena.
///
/// This is wrapped in an UnsafeCell because we will need to get mutable access to unique values in unique bump arenas
/// and rusts's guartnees cannot prove that this is safe. We will need to maintain the safety guarantees manually.
pub shared: SharedResources,
scheduler: Scheduler,
/// The index of the root component
/// Should always be the first (gen=0, id=0)
pub base_scope: ScopeId,
base_scope: ScopeId,
active_fibers: Vec<Fiber<'static>>,
root_fc: Box<dyn Any>,
// for managing the props that were used to create the dom
#[doc(hidden)]
_root_prop_type: std::any::TypeId,
#[doc(hidden)]
_root_props: std::pin::Pin<Box<dyn std::any::Any>>,
root_props: Pin<Box<dyn std::any::Any>>,
}
impl VirtualDom {
/// Create a new instance of the Dioxus Virtual Dom with no properties for the root component.
/// Create a new VirtualDOM with a component that does not have special props.
///
/// This means that the root component must either consumes its own context, or statics are used to generate the page.
/// The root component can access things like routing in its context.
/// # Description
///
/// As an end-user, you'll want to use the Renderer's "new" method instead of this method.
/// Directly creating the VirtualDOM is only useful when implementing a new renderer.
///
///
/// ```ignore
/// // Directly from a closure
///
/// let dom = VirtualDom::new(|cx| cx.render(rsx!{ div {"hello world"} }));
///
/// // or pass in...
///
/// let root = |cx| {
/// cx.render(rsx!{
/// div {"hello world"}
/// })
/// }
/// let dom = VirtualDom::new(root);
///
/// // or directly from a fn
///
/// fn Example(cx: Context<()>) -> DomTree {
/// cx.render(rsx!{ div{"hello world"} })
/// }
///
/// let dom = VirtualDom::new(Example);
/// ```
pub fn new(root: FC<()>) -> Self {
Self::new_with_props(root, ())
}
/// Start a new VirtualDom instance with a dependent cx.
/// Later, the props can be updated by calling "update" with a new set of props, causing a set of re-renders.
///
/// This is useful when a component tree can be driven by external state (IE SSR) but it would be too expensive
/// to toss out the entire tree.
///
/// ```ignore
/// // Directly from a closure
///
/// let dom = VirtualDom::new(|cx| cx.render(rsx!{ div {"hello world"} }));
///
/// // or pass in...
///
/// let root = |cx| {
/// cx.render(rsx!{
/// div {"hello world"}
/// })
/// }
/// let dom = VirtualDom::new(root);
///
/// // or directly from a fn
///
/// fn Example(cx: Context, props: &SomeProps) -> VNode {
/// cx.render(rsx!{ div{"hello world"} })
/// # Example
/// ```
/// fn Example(cx: Context<()>) -> DomTree {
/// cx.render(rsx!( div { "hello world" } ))
/// }
///
/// let dom = VirtualDom::new(Example);
/// ```
///
/// Note: the VirtualDOM is not progressed, you must either "run_with_deadline" or use "rebuild" to progress it.
pub fn new(root: FC<()>) -> Self {
Self::new_with_props(root, ())
}
/// Create a new VirtualDOM with the given properties for the root component.
///
/// # Description
///
/// Later, the props can be updated by calling "update" with a new set of props, causing a set of re-renders.
///
/// This is useful when a component tree can be driven by external state (IE SSR) but it would be too expensive
/// to toss out the entire tree.
///
///
/// # Example
/// ```
/// #[derive(PartialEq, Props)]
/// struct SomeProps {
/// name: &'static str
/// }
///
/// fn Example(cx: Context<SomeProps>) -> DomTree {
/// cx.render(rsx!{ div{ "hello {cx.name}" } })
/// }
///
/// let dom = VirtualDom::new(Example);
/// ```
///
/// Note: the VirtualDOM is not progressed on creation. You must either "run_with_deadline" or use "rebuild" to progress it.
///
/// ```rust
/// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" });
/// let mutations = dom.rebuild();
/// ```
pub fn new_with_props<P: Properties + 'static>(root: FC<P>, root_props: P) -> Self {
let components = SharedResources::new();
let scheduler = Scheduler::new();
let root_props: Pin<Box<dyn Any>> = Box::pin(root_props);
let props_ptr = root_props.as_ref().downcast_ref::<P>().unwrap() as *const P;
let props_ptr = root_props.downcast_ref::<P>().unwrap() as *const P;
let link = components.clone();
let base_scope = components.insert_scope_with_key(move |myidx| {
let base_scope = scheduler.pool.insert_scope_with_key(|myidx| {
let caller = NodeFactory::create_component_caller(root, props_ptr as *const _);
Scope::new(caller, myidx, None, 0, ScopeChildren(&[]), link)
Scope::new(
caller,
myidx,
None,
0,
ScopeChildren(&[]),
scheduler.pool.channel.clone(),
)
});
Self {
root_fc: Box::new(root),
base_scope,
_root_props: root_props,
shared: components,
active_fibers: Vec::new(),
_root_prop_type: TypeId::of::<P>(),
scheduler,
root_props,
}
}
pub fn launch_in_place(root: FC<()>) -> Self {
let mut s = Self::new(root);
s.rebuild_in_place().unwrap();
s
}
/// Creates a new virtualdom and immediately rebuilds it in place, not caring about the RealDom to write into.
/// Get the [`Scope`] for the root component.
///
pub fn launch_with_props_in_place<P: Properties + 'static>(root: FC<P>, root_props: P) -> Self {
let mut s = Self::new_with_props(root, root_props);
s.rebuild_in_place().unwrap();
s
}
/// This is useful for traversing the tree from the root for heuristics or altnerative renderers that use Dioxus
/// directly.
pub fn base_scope(&self) -> &Scope {
unsafe { self.shared.get_scope(self.base_scope).unwrap() }
self.scheduler.pool.get_scope(self.base_scope).unwrap()
}
/// Get the [`Scope`] for a component given its [`ScopeId`]
pub fn get_scope(&self, id: ScopeId) -> Option<&Scope> {
unsafe { self.shared.get_scope(id) }
self.scheduler.pool.get_scope(id)
}
/// Rebuilds the VirtualDOM from scratch, but uses a "dummy" RealDom.
/// Update the root props of this VirtualDOM.
///
/// Used in contexts where a real copy of the structure doesn't matter, and the VirtualDOM is the source of truth.
/// This method retuns None if the old props could not be removed. The entire VirtualDOM will be rebuilt immediately,
/// so calling this method will block the main thread until computation is done.
///
/// ## Why?
/// ## Example
///
/// This method uses the `DebugDom` under the hood - essentially making the VirtualDOM's diffing patches a "no-op".
/// ```rust
/// #[derive(Props, PartialEq)]
/// struct AppProps {
/// route: &'static str
/// }
/// static App: FC<AppProps> = |cx| cx.render(rsx!{ "route is {cx.route}" });
///
/// SSR takes advantage of this by using Dioxus itself as the source of truth, and rendering from the tree directly.
pub fn rebuild_in_place(&mut self) -> Result<Vec<DomEdit>> {
todo!();
// let mut realdom = DebugDom::new();
// let mut edits = Vec::new();
// self.rebuild(&mut realdom, &mut edits)?;
// Ok(edits)
/// let mut dom = VirtualDom::new_with_props(App, AppProps { route: "start" });
///
/// let mutations = dom.update_root_props(AppProps { route: "end" }).unwrap();
/// ```
pub fn update_root_props<'s, P: 'static>(&'s mut self, root_props: P) -> Option<Mutations<'s>> {
let root_scope = self.scheduler.pool.get_scope_mut(self.base_scope).unwrap();
root_scope.ensure_drop_safety(&self.scheduler.pool);
let mut root_props: Pin<Box<dyn Any>> = Box::pin(root_props);
if let Some(props_ptr) = root_props.downcast_ref::<P>().map(|p| p as *const P) {
std::mem::swap(&mut self.root_props, &mut root_props);
let root = *self.root_fc.downcast_ref::<FC<P>>().unwrap();
let new_caller = NodeFactory::create_component_caller(root, props_ptr as *const _);
root_scope.update_scope_dependencies(new_caller, ScopeChildren(&[]));
Some(self.rebuild())
} else {
None
}
}
/// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom rom scratch
@ -193,21 +183,40 @@ impl VirtualDom {
/// The diff machine expects the RealDom's stack to be the root of the application
///
/// Events like garabge collection, application of refs, etc are not handled by this method and can only be progressed
/// through "run"
///
pub fn rebuild<'s>(&'s mut self) -> Result<Vec<DomEdit<'s>>> {
let mut edits = Vec::new();
let mutations = Mutations { edits: Vec::new() };
let mut diff_machine = DiffMachine::new(mutations, self.base_scope, &self.shared);
/// through "run". We completely avoid the task scheduler infrastructure.
pub fn rebuild<'s>(&'s mut self) -> Mutations<'s> {
let mut fut = self.rebuild_async().boxed_local();
let cur_component = diff_machine
.get_scope_mut(&self.base_scope)
loop {
if let Some(edits) = (&mut fut).now_or_never() {
break edits;
}
}
}
/// Rebuild the dom from the ground up
///
/// This method is asynchronous to prevent the application from blocking while the dom is being rebuilt. Computing
/// the diff and creating nodes can be expensive, so we provide this method to avoid blocking the main thread. This
/// method can be useful when needing to perform some crucial periodic tasks.
pub async fn rebuild_async<'s>(&'s mut self) -> Mutations<'s> {
let mut shared = self.scheduler.pool.clone();
let mut diff_machine = DiffMachine::new(Mutations::new(), &mut shared);
let cur_component = self
.scheduler
.pool
.get_scope_mut(self.base_scope)
.expect("The base scope should never be moved");
// We run the component. If it succeeds, then we can diff it and add the changes to the dom.
if cur_component.run_scope().is_ok() {
let meta = diff_machine.create_vnode(cur_component.frames.fin_head());
diff_machine.edit_append_children(meta.added_to_stack);
if cur_component.run_scope(&self.scheduler.pool) {
diff_machine
.stack
.create_node(cur_component.frames.fin_head(), MountType::Append);
diff_machine.stack.scope_stack.push(self.base_scope);
// let completed = diff_machine.work();
} else {
// todo: should this be a hard error?
log::warn!(
@ -216,65 +225,42 @@ impl VirtualDom {
);
}
Ok(edits)
unsafe { std::mem::transmute(diff_machine.mutations) }
}
// async fn select_next_event(&mut self) -> Option<EventTrigger> {
// let mut receiver = self.shared.task_receiver.borrow_mut();
/// Compute a manual diff of the VirtualDOM between states.
///
/// This can be useful when state inside the DOM is remotely changed from the outside, but not propogated as an event.
pub fn diff<'s>(&'s mut self) -> Mutations<'s> {
let cur_component = self
.scheduler
.pool
.get_scope_mut(self.base_scope)
.expect("The base scope should never be moved");
// // drain the in-flight events so that we can sort them out with the current events
// while let Ok(Some(trigger)) = receiver.try_next() {
// log::info!("retrieving event from receiver");
// let key = self.shared.make_trigger_key(&trigger);
// self.pending_events.insert(key, trigger);
// }
// if self.pending_events.is_empty() {
// // Continuously poll the future pool and the event receiver for work
// let mut tasks = self.shared.async_tasks.borrow_mut();
// let tasks_tasks = tasks.next();
// let mut receiver = self.shared.task_receiver.borrow_mut();
// let reciv_task = receiver.next();
// futures_util::pin_mut!(tasks_tasks);
// futures_util::pin_mut!(reciv_task);
// let trigger = match futures_util::future::select(tasks_tasks, reciv_task).await {
// futures_util::future::Either::Left((trigger, _)) => trigger,
// futures_util::future::Either::Right((trigger, _)) => trigger,
// }
// .unwrap();
// let key = self.shared.make_trigger_key(&trigger);
// self.pending_events.insert(key, trigger);
// }
// // pop the most important event off
// let key = self.pending_events.keys().next().unwrap().clone();
// let trigger = self.pending_events.remove(&key).unwrap();
// Some(trigger)
// }
if cur_component.run_scope(&self.scheduler.pool) {
let mut diff_machine: DiffMachine<'s> =
DiffMachine::new(Mutations::new(), &mut self.scheduler.pool);
diff_machine.diff_scope(self.base_scope);
diff_machine.mutations
} else {
Mutations::new()
}
}
/// Runs the virtualdom immediately, not waiting for any suspended nodes to complete.
///
/// This method will not wait for any suspended tasks, completely skipping over
pub fn run_immediate<'s>(&'s mut self) -> Result<Mutations<'s>> {
//
todo!()
/// This method will not wait for any suspended nodes to complete. If there is no pending work, then this method will
/// return "None"
pub fn run_immediate<'s>(&'s mut self) -> Option<Vec<Mutations<'s>>> {
if self.scheduler.has_any_work() {
Some(self.scheduler.work_sync())
} else {
None
}
}
/// Runs the virtualdom with no time limit.
///
/// If there are pending tasks, they will be progressed before returning. This is useful when rendering an application
/// that has suspended nodes or suspended tasks. Be warned - any async tasks running forever will prevent this method
/// from completing. Consider using `run` and specifing a deadline.
pub async fn run_unbounded<'s>(&'s mut self) -> Result<Mutations<'s>> {
self.run_with_deadline(|| false).await
}
/// Run the virtualdom with a time limit.
/// Run the virtualdom with a deadline.
///
/// This method will progress async tasks until the deadline is reached. If tasks are completed before the deadline,
/// and no tasks are pending, this method will return immediately. If tasks are still pending, then this method will
@ -283,189 +269,59 @@ impl VirtualDom {
/// This method is useful when needing to schedule the virtualdom around other tasks on the main thread to prevent
/// "jank". It will try to finish whatever work it has by the deadline to free up time for other work.
///
/// Due to platform differences in how time is handled, this method accepts a closure that must return true when the
/// deadline is exceeded. However, the deadline won't be met precisely, so you might want to build some wiggle room
/// into the deadline closure manually.
/// Due to platform differences in how time is handled, this method accepts a future that resolves when the deadline
/// is exceeded. However, the deadline won't be met precisely, so you might want to build some wiggle room into the
/// deadline closure manually.
///
/// The deadline is checked before starting to diff components. This strikes a balance between the overhead of checking
/// The deadline is polled before starting to diff components. This strikes a balance between the overhead of checking
/// the deadline and just completing the work. However, if an individual component takes more than 16ms to render, then
/// the screen will "jank" up. In debug, this will trigger an alert.
///
/// If there are no in-flight fibers when this method is called, it will await any possible tasks, aborting early if
/// the provided deadline future resolves.
///
/// For use in the web, it is expected that this method will be called to be executed during "idle times" and the
/// mutations to be applied during the "paint times" IE "animation frames". With this strategy, it is possible to craft
/// entirely jank-free applications that perform a ton of work.
///
/// # Example
///
/// ```no_run
/// let mut dom = VirtualDom::new(|cx| cx.render(rsx!( div {"hello"} )));
/// static App: FC<()> = |cx| rsx!(in cx, div {"hello"} );
/// let mut dom = VirtualDom::new(App);
/// loop {
/// let started = std::time::Instant::now();
/// let deadline = move || std::time::Instant::now() - started > std::time::Duration::from_millis(16);
///
/// let deadline = TimeoutFuture::from_ms(16);
/// let mutations = dom.run_with_deadline(deadline).await;
/// apply_mutations(mutations);
/// }
/// ```
///
/// ## Mutations
///
/// This method returns "mutations" - IE the necessary changes to get the RealDOM to match the VirtualDOM. It also
/// includes a list of NodeRefs that need to be applied and effects that need to be triggered after the RealDOM has
/// applied the edits.
///
/// Mutations are the only link between the RealDOM and the VirtualDOM.
pub async fn run_with_deadline<'s>(
&'s mut self,
mut deadline_exceeded: impl FnMut() -> bool,
) -> Result<Mutations<'s>> {
let cur_component = self.base_scope;
let mut diff_machine =
DiffMachine::new(Mutations { edits: Vec::new() }, cur_component, &self.shared);
/*
Strategy:
1. Check if there are any events in the receiver.
2. If there are, process them and create a new fiber.
3. If there are no events, then choose a fiber to work on.
4. If there are no fibers, then wait for the next event from the receiver.
5. While processing a fiber, periodically check if we're out of time
6. If we are almost out of time, then commit our edits to the realdom
7. Whenever a fiber is finished, immediately commit it. (IE so deadlines can be infinite if unsupported)
*/
// 1. Consume any pending events and create new fibers
let mut receiver = self.shared.task_receiver.borrow_mut();
while let Ok(Some(trigger)) = receiver.try_next() {
// todo: cache the fibers
let mut fiber = Fiber::new();
match &trigger.event {
// If any input event is received, then we need to create a new fiber
VirtualEvent::ClipboardEvent(_)
| VirtualEvent::CompositionEvent(_)
| VirtualEvent::KeyboardEvent(_)
| VirtualEvent::FocusEvent(_)
| VirtualEvent::FormEvent(_)
| VirtualEvent::SelectionEvent(_)
| VirtualEvent::TouchEvent(_)
| VirtualEvent::UIEvent(_)
| VirtualEvent::WheelEvent(_)
| VirtualEvent::MediaEvent(_)
| VirtualEvent::AnimationEvent(_)
| VirtualEvent::TransitionEvent(_)
| VirtualEvent::ToggleEvent(_)
| VirtualEvent::MouseEvent(_)
| VirtualEvent::PointerEvent(_) => {
if let Some(scope) = self.shared.get_scope_mut(trigger.originator) {
scope.call_listener(trigger)?;
}
}
VirtualEvent::AsyncEvent { .. } => {
while let Ok(Some(event)) = receiver.try_next() {
fiber.pending_scopes.push(event.originator);
}
}
// These shouldn't normally be received, but if they are, it's done because some task set state manually
// Instead of batching the results,
VirtualEvent::ScheduledUpdate { height: u32 } => {}
// Suspense Events! A component's suspended node is updated
VirtualEvent::SuspenseEvent { hook_idx, domnode } => {
// Safety: this handler is the only thing that can mutate shared items at this moment in tim
let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap();
// safety: we are sure that there are no other references to the inner content of suspense hooks
let hook = unsafe { scope.hooks.get_mut::<SuspenseHook>(*hook_idx) }.unwrap();
let cx = Context { scope, props: &() };
let scx = SuspendedContext { inner: cx };
// generate the new node!
let nodes: Option<VNode> = (&hook.callback)(scx);
match nodes {
None => {
log::warn!(
"Suspense event came through, but there were no generated nodes >:(."
);
}
Some(nodes) => {
// allocate inside the finished frame - not the WIP frame
let nodes = scope.frames.finished_frame().bump.alloc(nodes);
// push the old node's root onto the stack
let real_id = domnode.get().ok_or(Error::NotMounted)?;
diff_machine.edit_push_root(real_id);
// push these new nodes onto the diff machines stack
let meta = diff_machine.create_vnode(&*nodes);
// replace the placeholder with the new nodes we just pushed on the stack
diff_machine.edit_replace_with(1, meta.added_to_stack);
}
}
}
// Collecting garabge is not currently interruptible.
//
// In the future, it could be though
VirtualEvent::GarbageCollection => {
let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap();
let mut garbage_list = scope.consume_garbage();
let mut scopes_to_kill = Vec::new();
while let Some(node) = garbage_list.pop() {
match &node.kind {
VNodeKind::Text(_) => {
self.shared.collect_garbage(node.direct_id());
}
VNodeKind::Anchor(_) => {
self.shared.collect_garbage(node.direct_id());
}
VNodeKind::Suspended(_) => {
self.shared.collect_garbage(node.direct_id());
}
VNodeKind::Element(el) => {
self.shared.collect_garbage(node.direct_id());
for child in el.children {
garbage_list.push(child);
}
}
VNodeKind::Fragment(frag) => {
for child in frag.children {
garbage_list.push(child);
}
}
VNodeKind::Component(comp) => {
// TODO: run the hook destructors and then even delete the scope
let scope_id = comp.ass_scope.get().unwrap();
let scope = self.get_scope(scope_id).unwrap();
let root = scope.root();
garbage_list.push(root);
scopes_to_kill.push(scope_id);
}
}
}
for scope in scopes_to_kill {
// oy kill em
log::debug!("should be removing scope {:#?}", scope);
}
}
}
}
while !deadline_exceeded() {
let mut receiver = self.shared.task_receiver.borrow_mut();
// no messages to receive, just work on the fiber
}
Ok(diff_machine.edits)
deadline: impl Future<Output = ()>,
) -> Vec<Mutations<'s>> {
let mut deadline = Box::pin(deadline.fuse());
self.scheduler.work_with_deadline(deadline).await
}
pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender<EventTrigger> {
self.shared.task_sender.clone()
pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender<SchedulerMsg> {
self.scheduler.pool.channel.sender.clone()
}
fn get_scope_mut(&mut self, id: ScopeId) -> Option<&mut Scope> {
unsafe { self.shared.get_scope_mut(id) }
pub fn has_work(&self) -> bool {
true
}
pub async fn wait_for_any_work(&mut self) {
let mut timeout = Box::pin(futures_util::future::pending().fuse());
self.scheduler.wait_for_any_trigger(&mut timeout).await;
}
}
@ -473,36 +329,3 @@ impl VirtualDom {
// These impls are actually wrong. The DOM needs to have a mutex implemented.
unsafe impl Sync for VirtualDom {}
unsafe impl Send for VirtualDom {}
struct Fiber<'a> {
// scopes that haven't been updated yet
pending_scopes: Vec<ScopeId>,
pending_nodes: Vec<*const VNode<'a>>,
// WIP edits
edits: Vec<DomEdit<'a>>,
started: bool,
completed: bool,
}
impl Fiber<'_> {
fn new() -> Self {
Self {
pending_scopes: Vec::new(),
pending_nodes: Vec::new(),
edits: Vec::new(),
started: false,
completed: false,
}
}
}
/// The "Mutations" object holds the changes that need to be made to the DOM.
pub struct Mutations<'s> {
// todo: apply node refs
// todo: apply effects
pub edits: Vec<DomEdit<'s>>,
}

View file

@ -0,0 +1,263 @@
//! tests to prove that the iterative implementation works
use anyhow::{Context, Result};
use dioxus::{prelude::*, DomEdit, Mutations};
mod test_logging;
use dioxus_core as dioxus;
use dioxus_html as dioxus_elements;
use DomEdit::*;
const LOGGING_ENABLED: bool = false;
#[test]
fn test_original_diff() {
static App: FC<()> = |cx| {
cx.render(rsx! {
div {
div {
"Hello, world!"
}
}
})
};
let mut dom = VirtualDom::new(App);
let mutations = dom.rebuild();
assert_eq!(
mutations.edits,
[
CreateElement { id: 0, tag: "div" },
CreateElement { id: 1, tag: "div" },
CreateTextNode {
id: 2,
text: "Hello, world!"
},
AppendChildren { many: 1 },
AppendChildren { many: 1 },
AppendChildren { many: 1 },
]
);
}
#[async_std::test]
async fn create() {
static App: FC<()> = |cx| {
cx.render(rsx! {
div {
div {
"Hello, world!"
div {
div {
Fragment {
"hello"
"world"
}
}
}
}
}
})
};
test_logging::set_up_logging(LOGGING_ENABLED);
let mut dom = VirtualDom::new(App);
let mutations = dom.rebuild_async().await;
assert_eq!(
mutations.edits,
[
CreateElement { id: 0, tag: "div" },
CreateElement { id: 1, tag: "div" },
CreateTextNode {
id: 2,
text: "Hello, world!"
},
CreateElement { id: 3, tag: "div" },
CreateElement { id: 4, tag: "div" },
CreateTextNode {
id: 5,
text: "hello"
},
CreateTextNode {
id: 6,
text: "world"
},
AppendChildren { many: 2 },
AppendChildren { many: 1 },
AppendChildren { many: 2 },
AppendChildren { many: 1 },
AppendChildren { many: 1 },
]
);
}
#[async_std::test]
async fn create_list() {
static App: FC<()> = |cx| {
cx.render(rsx! {
{(0..3).map(|f| rsx!{ div {
"hello"
}})}
})
};
test_logging::set_up_logging(LOGGING_ENABLED);
let mut dom = VirtualDom::new(App);
let mutations = dom.rebuild_async().await;
// copilot wrote this test :P
assert_eq!(
mutations.edits,
[
CreateElement { id: 0, tag: "div" },
CreateTextNode {
id: 1,
text: "hello"
},
AppendChildren { many: 1 },
CreateElement { id: 2, tag: "div" },
CreateTextNode {
id: 3,
text: "hello"
},
AppendChildren { many: 1 },
CreateElement { id: 4, tag: "div" },
CreateTextNode {
id: 5,
text: "hello"
},
AppendChildren { many: 1 },
AppendChildren { many: 3 },
]
);
}
#[async_std::test]
async fn create_simple() {
static App: FC<()> = |cx| {
cx.render(rsx! {
div {}
div {}
div {}
div {}
})
};
test_logging::set_up_logging(LOGGING_ENABLED);
let mut dom = VirtualDom::new(App);
let mutations = dom.rebuild_async().await;
// copilot wrote this test :P
assert_eq!(
mutations.edits,
[
CreateElement { id: 0, tag: "div" },
CreateElement { id: 1, tag: "div" },
CreateElement { id: 2, tag: "div" },
CreateElement { id: 3, tag: "div" },
AppendChildren { many: 4 },
]
);
}
#[async_std::test]
async fn create_components() {
static App: FC<()> = |cx| {
cx.render(rsx! {
Child { "abc1" }
Child { "abc2" }
Child { "abc3" }
})
};
static Child: FC<()> = |cx| {
cx.render(rsx! {
h1 {}
div { {cx.children()} }
p {}
})
};
test_logging::set_up_logging(LOGGING_ENABLED);
let mut dom = VirtualDom::new(App);
let mutations = dom.rebuild_async().await;
assert_eq!(
mutations.edits,
[
CreateElement { id: 0, tag: "h1" },
CreateElement { id: 1, tag: "div" },
CreateTextNode {
id: 2,
text: "abc1"
},
AppendChildren { many: 1 },
CreateElement { id: 3, tag: "p" },
CreateElement { id: 4, tag: "h1" },
CreateElement { id: 5, tag: "div" },
CreateTextNode {
id: 6,
text: "abc2"
},
AppendChildren { many: 1 },
CreateElement { id: 7, tag: "p" },
CreateElement { id: 8, tag: "h1" },
CreateElement { id: 9, tag: "div" },
CreateTextNode {
id: 10,
text: "abc3"
},
AppendChildren { many: 1 },
CreateElement { id: 11, tag: "p" },
AppendChildren { many: 9 },
]
);
}
#[async_std::test]
async fn anchors() {
static App: FC<()> = |cx| {
cx.render(rsx! {
{true.then(|| rsx!{ div { "hello" } })}
{false.then(|| rsx!{ div { "goodbye" } })}
})
};
test_logging::set_up_logging(LOGGING_ENABLED);
let mut dom = VirtualDom::new(App);
let mutations = dom.rebuild_async().await;
assert_eq!(
mutations.edits,
[
CreateElement { id: 0, tag: "div" },
CreateTextNode {
id: 1,
text: "hello"
},
AppendChildren { many: 1 },
CreatePlaceholder { id: 2 },
AppendChildren { many: 2 },
]
);
}
#[async_std::test]
async fn suspended() {
static App: FC<()> = |cx| {
let val = use_suspense(cx, || async {}, |cx, _| cx.render(rsx! { "hi "}));
cx.render(rsx! { {val} })
};
test_logging::set_up_logging(LOGGING_ENABLED);
let mut dom = VirtualDom::new(App);
let mutations = dom.rebuild_async().await;
assert_eq!(
mutations.edits,
[CreatePlaceholder { id: 0 }, AppendChildren { many: 1 },]
);
}

View file

@ -0,0 +1,2 @@
/// A virtualdom wrapper used for testing purposes.
pub struct DebugDiff {}

View file

@ -0,0 +1,48 @@
//! tests to prove that the iterative implementation works
use dioxus::prelude::*;
mod test_logging;
use dioxus_core as dioxus;
use dioxus_html as dioxus_elements;
const LOGGING_ENABLED: bool = false;
#[async_std::test]
async fn test_iterative_create_components() {
static App: FC<()> = |cx| {
// test root fragments
cx.render(rsx! {
Child { "abc1" }
Child { "abc2" }
Child { "abc3" }
})
};
fn Child(cx: Context<()>) -> DomTree {
// test root fragments, anchors, and ChildNode type
cx.render(rsx! {
h1 {}
div { {cx.children()} }
Fragment {
Fragment {
Fragment {
"wozza"
}
}
}
{(0..0).map(|_f| rsx!{ div { "walalla"}})}
p {}
})
}
test_logging::set_up_logging(LOGGING_ENABLED);
let mut dom = VirtualDom::new(App);
let mutations = dom.rebuild_async().await;
dbg!(mutations);
let mutations = dom.diff();
dbg!(mutations);
}

View file

@ -1,85 +1,23 @@
//! Diffing Tests
//! -------------
//!
//! These should always compile and run, but the result is not validated for each test.
//! TODO: Validate the results beyond visual inspection.
//! These tests only verify that the diffing algorithm works properly for single components.
//!
//! It does not validated that component lifecycles work properly. This is done in another test file.
use bumpalo::Bump;
use anyhow::{Context, Result};
use dioxus::{
arena::SharedResources,
diff::{CreateMeta, DiffMachine},
prelude::*,
DomEdit,
};
use dioxus::{prelude::*, DomEdit, TestDom};
use dioxus_core as dioxus;
use dioxus_html as dioxus_elements;
struct TestDom {
bump: Bump,
resources: SharedResources,
}
impl TestDom {
fn new() -> TestDom {
let bump = Bump::new();
let resources = SharedResources::new();
TestDom { bump, resources }
}
fn new_factory<'a>(&'a self) -> NodeFactory<'a> {
NodeFactory::new(&self.bump)
}
mod test_logging;
use DomEdit::*;
fn render<'a, F>(&'a self, lazy_nodes: LazyNodes<'a, F>) -> VNode<'a>
where
F: FnOnce(NodeFactory<'a>) -> VNode<'a>,
{
use dioxus_core::nodes::{IntoVNode, IntoVNodeList};
lazy_nodes.into_vnode(NodeFactory::new(&self.bump))
}
// logging is wired up to the test harness
// feel free to enable while debugging
const IS_LOGGING_ENABLED: bool = false;
fn diff<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Vec<DomEdit<'a>> {
let mut edits = Vec::new();
let mut machine = DiffMachine::new_headless(&mut edits, &self.resources);
machine.diff_node(old, new);
edits
}
fn create<'a, F1>(&'a self, left: LazyNodes<'a, F1>) -> (CreateMeta, Vec<DomEdit<'a>>)
where
F1: FnOnce(NodeFactory<'a>) -> VNode<'a>,
{
let old = self.bump.alloc(self.render(left));
let mut edits = Vec::new();
let mut machine = DiffMachine::new_headless(&mut edits, &self.resources);
let meta = machine.create_vnode(old);
(meta, edits)
}
fn lazy_diff<'a, F1, F2>(
&'a self,
left: LazyNodes<'a, F1>,
right: LazyNodes<'a, F2>,
) -> (Vec<DomEdit<'a>>, Vec<DomEdit<'a>>)
where
F1: FnOnce(NodeFactory<'a>) -> VNode<'a>,
F2: FnOnce(NodeFactory<'a>) -> VNode<'a>,
{
let old = self.bump.alloc(self.render(left));
let new = self.bump.alloc(self.render(right));
let mut create_edits = Vec::new();
let mut machine = DiffMachine::new_headless(&mut create_edits, &self.resources);
machine.create_vnode(old);
let mut edits = Vec::new();
let mut machine = DiffMachine::new_headless(&mut edits, &self.resources);
machine.diff_node(old, new);
(create_edits, edits)
}
fn new_dom() -> TestDom {
test_logging::set_up_logging(IS_LOGGING_ENABLED);
TestDom::new()
}
#[test]
@ -88,78 +26,170 @@ fn diffing_works() {}
/// Should push the text node onto the stack and modify it
#[test]
fn html_and_rsx_generate_the_same_output() {
let dom = TestDom::new();
let edits = dom.lazy_diff(
let dom = new_dom();
let (create, change) = dom.lazy_diff(
rsx! ( div { "Hello world" } ),
rsx! ( div { "Goodbye world" } ),
);
dbg!(edits);
assert_eq!(
create.edits,
[
CreateElement { id: 0, tag: "div" },
CreateTextNode {
id: 1,
text: "Hello world"
},
AppendChildren { many: 1 },
AppendChildren { many: 1 },
]
);
assert_eq!(
change.edits,
[
PushRoot { id: 1 },
SetText {
text: "Goodbye world"
},
PopRoot
]
);
}
/// Should result in 3 elements on the stack
#[test]
fn fragments_create_properly() {
let dom = TestDom::new();
let (meta, edits) = dom.create(rsx! {
let dom = new_dom();
let create = dom.create(rsx! {
div { "Hello a" }
div { "Hello b" }
div { "Hello c" }
});
assert!(&edits[0].is("CreateElement"));
assert!(&edits[3].is("CreateElement"));
assert!(&edits[6].is("CreateElement"));
assert_eq!(meta.added_to_stack, 3);
dbg!(edits);
assert_eq!(
create.edits,
[
CreateElement { id: 0, tag: "div" },
CreateTextNode {
id: 1,
text: "Hello a"
},
AppendChildren { many: 1 },
CreateElement { id: 2, tag: "div" },
CreateTextNode {
id: 3,
text: "Hello b"
},
AppendChildren { many: 1 },
CreateElement { id: 4, tag: "div" },
CreateTextNode {
id: 5,
text: "Hello c"
},
AppendChildren { many: 1 },
AppendChildren { many: 3 },
]
);
}
/// Should result in the creation of an anchor (placeholder) and then a replacewith
#[test]
fn empty_fragments_create_anchors() {
let dom = TestDom::new();
let dom = new_dom();
let left = rsx!({ (0..0).map(|f| rsx! { div {}}) });
let right = rsx!({ (0..1).map(|f| rsx! { div {}}) });
let edits = dom.lazy_diff(left, right);
dbg!(edits);
let (create, change) = dom.lazy_diff(left, right);
assert_eq!(
create.edits,
[CreatePlaceholder { id: 0 }, AppendChildren { many: 1 }]
);
assert_eq!(
change.edits,
[
CreateElement { id: 1, tag: "div" },
ReplaceWith { m: 1, root: 0 }
]
);
}
/// Should result in the creation of an anchor (placeholder) and then a replacewith m=5
#[test]
fn empty_fragments_create_many_anchors() {
let dom = TestDom::new();
let dom = new_dom();
let left = rsx!({ (0..0).map(|f| rsx! { div {}}) });
let right = rsx!({ (0..5).map(|f| rsx! { div {}}) });
let edits = dom.lazy_diff(left, right);
dbg!(edits);
let (create, change) = dom.lazy_diff(left, right);
assert_eq!(
create.edits,
[CreatePlaceholder { id: 0 }, AppendChildren { many: 1 }]
);
assert_eq!(
change.edits,
[
CreateElement { id: 1, tag: "div" },
CreateElement { id: 2, tag: "div" },
CreateElement { id: 3, tag: "div" },
CreateElement { id: 4, tag: "div" },
CreateElement { id: 5, tag: "div" },
ReplaceWith { m: 5, root: 0 }
]
);
}
/// Should result in the creation of an anchor (placeholder) and then a replacewith
/// Includes child nodes inside the fragment
#[test]
fn empty_fragments_create_anchors_with_many_children() {
let dom = TestDom::new();
let dom = new_dom();
let left = rsx!({ (0..0).map(|f| rsx! { div {} }) });
let right = rsx!({
(0..5).map(|f| {
rsx! { div { "hello" }}
(0..3).map(|f| {
rsx! { div { "hello: {f}" }}
})
});
let edits = dom.lazy_diff(left, right);
dbg!(&edits);
let last_edit = edits.1.last().unwrap();
assert!(last_edit.is("ReplaceWith"));
let (create, change) = dom.lazy_diff(left, right);
assert_eq!(
create.edits,
[CreatePlaceholder { id: 0 }, AppendChildren { many: 1 }]
);
assert_eq!(
change.edits,
[
CreateElement { id: 1, tag: "div" },
CreateTextNode {
text: "hello: 0",
id: 2
},
AppendChildren { many: 1 },
CreateElement { id: 3, tag: "div" },
CreateTextNode {
text: "hello: 1",
id: 4
},
AppendChildren { many: 1 },
CreateElement { id: 5, tag: "div" },
CreateTextNode {
text: "hello: 2",
id: 6
},
AppendChildren { many: 1 },
ReplaceWith { m: 3, root: 0 }
]
);
}
/// Should result in every node being pushed and then replaced with an anchor
#[test]
fn many_items_become_fragment() {
let dom = TestDom::new();
let dom = new_dom();
let left = rsx!({
(0..2).map(|f| {
@ -168,14 +198,41 @@ fn many_items_become_fragment() {
});
let right = rsx!({ (0..0).map(|f| rsx! { div {} }) });
let edits = dom.lazy_diff(left, right);
dbg!(&edits);
let (create, change) = dom.lazy_diff(left, right);
assert_eq!(
create.edits,
[
CreateElement { id: 0, tag: "div" },
CreateTextNode {
text: "hello",
id: 1
},
AppendChildren { many: 1 },
CreateElement { id: 2, tag: "div" },
CreateTextNode {
text: "hello",
id: 3
},
AppendChildren { many: 1 },
AppendChildren { many: 2 },
]
);
// hmmmmmmmmm worried about reusing IDs that we shouldnt be
assert_eq!(
change.edits,
[
Remove { root: 2 },
CreatePlaceholder { id: 4 },
ReplaceWith { root: 0, m: 1 },
]
);
}
/// Should result in no edits
#[test]
fn two_equal_fragments_are_equal() {
let dom = TestDom::new();
let dom = new_dom();
let left = rsx!({
(0..2).map(|f| {
@ -188,33 +245,47 @@ fn two_equal_fragments_are_equal() {
})
});
let edits = dom.lazy_diff(left, right);
dbg!(&edits);
assert!(edits.1.is_empty());
let (create, change) = dom.lazy_diff(left, right);
assert!(change.edits.is_empty());
}
/// Should result the creation of more nodes appended after the old last node
#[test]
fn two_fragments_with_differrent_elements_are_differet() {
let dom = TestDom::new();
let dom = new_dom();
let left = rsx!(
{(0..2).map(|f| {rsx! { div { }}})}
{ (0..2).map(|_| rsx! { div { }} ) }
p {}
);
let right = rsx!(
{(0..5).map(|f| {rsx! { h1 { }}})}
{ (0..5).map(|_| rsx! (h1 { }) ) }
p {}
);
let edits = dom.lazy_diff(left, right);
dbg!(&edits);
let (create, changes) = dom.lazy_diff(left, right);
log::debug!("{:#?}", &changes);
assert_eq!(
changes.edits,
[
// create the new h1s
CreateElement { tag: "h1", id: 3 },
CreateElement { tag: "h1", id: 4 },
CreateElement { tag: "h1", id: 5 },
InsertAfter { root: 1, n: 3 },
// replace the divs with new h1s
CreateElement { tag: "h1", id: 6 },
ReplaceWith { root: 0, m: 1 },
CreateElement { tag: "h1", id: 7 },
ReplaceWith { root: 1, m: 1 },
]
);
}
/// Should result in multiple nodes destroyed - with changes to the first nodes
#[test]
fn two_fragments_with_differrent_elements_are_differet_shorter() {
let dom = TestDom::new();
let dom = new_dom();
let left = rsx!(
{(0..5).map(|f| {rsx! { div { }}})}
@ -225,14 +296,37 @@ fn two_fragments_with_differrent_elements_are_differet_shorter() {
p {}
);
let edits = dom.lazy_diff(left, right);
dbg!(&edits);
let (create, change) = dom.lazy_diff(left, right);
assert_eq!(
create.edits,
[
CreateElement { id: 0, tag: "div" },
CreateElement { id: 1, tag: "div" },
CreateElement { id: 2, tag: "div" },
CreateElement { id: 3, tag: "div" },
CreateElement { id: 4, tag: "div" },
CreateElement { id: 5, tag: "p" },
AppendChildren { many: 6 },
]
);
assert_eq!(
change.edits,
[
Remove { root: 2 },
Remove { root: 3 },
Remove { root: 4 },
CreateElement { id: 6, tag: "h1" },
ReplaceWith { root: 0, m: 1 },
CreateElement { id: 7, tag: "h1" },
ReplaceWith { root: 1, m: 1 },
]
);
}
/// Should result in multiple nodes destroyed - with no changes
#[test]
fn two_fragments_with_same_elements_are_differet() {
let dom = TestDom::new();
let dom = new_dom();
let left = rsx!(
{(0..2).map(|f| {rsx! { div { }}})}
@ -243,32 +337,31 @@ fn two_fragments_with_same_elements_are_differet() {
p {}
);
let edits = dom.lazy_diff(left, right);
dbg!(&edits);
}
// Similar test from above, but with extra child nodes
#[test]
fn two_fragments_with_same_elements_are_differet_shorter() {
let dom = TestDom::new();
let left = rsx!(
{(0..5).map(|f| {rsx! { div { }}})}
p {"e"}
let (create, change) = dom.lazy_diff(left, right);
assert_eq!(
create.edits,
[
CreateElement { id: 0, tag: "div" },
CreateElement { id: 1, tag: "div" },
CreateElement { id: 2, tag: "p" },
AppendChildren { many: 3 },
]
);
let right = rsx!(
{(0..2).map(|f| {rsx! { div { }}})}
p {"e"}
assert_eq!(
change.edits,
[
CreateElement { id: 3, tag: "div" },
CreateElement { id: 4, tag: "div" },
CreateElement { id: 5, tag: "div" },
InsertAfter { root: 1, n: 3 },
]
);
let edits = dom.lazy_diff(left, right);
dbg!(&edits);
}
/// should result in the removal of elements
#[test]
fn keyed_diffing_order() {
let dom = TestDom::new();
let dom = new_dom();
let left = rsx!(
{(0..5).map(|f| {rsx! { div { key: "{f}" }}})}
@ -279,62 +372,331 @@ fn keyed_diffing_order() {
p {"e"}
);
let edits = dom.lazy_diff(left, right);
dbg!(&edits);
}
#[test]
fn fragment_keys() {
let r = 1;
let p = rsx! {
Fragment { key: "asd {r}" }
};
let (create, change) = dom.lazy_diff(left, right);
assert_eq!(
change.edits,
[Remove { root: 2 }, Remove { root: 3 }, Remove { root: 4 },]
);
}
/// Should result in moves, but not removals or additions
#[test]
fn keyed_diffing_out_of_order() {
let dom = TestDom::new();
let dom = new_dom();
// 0, 1, 2, 3, 4, 5, 6, 7, 8,
let left = rsx!({
(0..3).chain(3..6).chain(6..9).map(|f| {
[0, 1, 2, 3, /**/ 4, 5, 6, /**/ 7, 8, 9].iter().map(|f| {
rsx! { div { key: "{f}" }}
})
});
// 0, 1, 2, 6, 5, 4, 3, 7, 8, 9
let right = rsx!({
(0..3).chain((3..7).rev()).chain(7..10).map(|f| {
[0, 1, 2, 3, /**/ 6, 4, 5, /**/ 7, 8, 9].iter().map(|f| {
rsx! { div { key: "{f}" }}
})
});
// LIS: 3, 7, 8,
let edits = dom.lazy_diff(left, right);
dbg!(&edits);
let (_, changes) = dom.lazy_diff(left, right);
log::debug!("{:?}", &changes);
assert_eq!(
changes.edits,
[PushRoot { id: 6 }, InsertBefore { root: 4, n: 1 }]
);
}
/// Should result in moves only
#[test]
fn keyed_diffing_out_of_order_adds() {
let dom = new_dom();
let left = rsx!({
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
})
});
let right = rsx!({
[/**/ 8, 7, 4, 5, 6 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
})
});
let (_, change) = dom.lazy_diff(left, right);
assert_eq!(
change.edits,
[
PushRoot { id: 4 },
PushRoot { id: 3 },
InsertBefore { n: 2, root: 0 }
]
);
}
/// Should result in moves onl
#[test]
fn keyed_diffing_out_of_order_adds_2() {
let dom = new_dom();
let left = rsx!({
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
})
});
let right = rsx!({
[/**/ 7, 8, 4, 5, 6 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
})
});
let (_, change) = dom.lazy_diff(left, right);
assert_eq!(
change.edits,
[
PushRoot { id: 3 },
PushRoot { id: 4 },
InsertBefore { n: 2, root: 0 }
]
);
}
/// Should result in moves onl
#[test]
fn keyed_diffing_out_of_order_adds_3() {
let dom = new_dom();
let left = rsx!({
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
})
});
let right = rsx!({
[/**/ 4, 8, 7, 5, 6 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
})
});
let (_, change) = dom.lazy_diff(left, right);
assert_eq!(
change.edits,
[
PushRoot { id: 4 },
PushRoot { id: 3 },
InsertBefore { n: 2, root: 1 }
]
);
}
/// Should result in moves onl
#[test]
fn keyed_diffing_out_of_order_adds_4() {
let dom = new_dom();
let left = rsx!({
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
})
});
let right = rsx!({
[/**/ 4, 5, 8, 7, 6 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
})
});
let (_, change) = dom.lazy_diff(left, right);
assert_eq!(
change.edits,
[
PushRoot { id: 4 },
PushRoot { id: 3 },
InsertBefore { n: 2, root: 2 }
]
);
}
/// Should result in moves onl
#[test]
fn keyed_diffing_out_of_order_adds_5() {
let dom = new_dom();
let left = rsx!({
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
})
});
let right = rsx!({
[/**/ 4, 5, 6, 8, 7 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
})
});
let (_, change) = dom.lazy_diff(left, right);
assert_eq!(
change.edits,
[PushRoot { id: 4 }, InsertBefore { n: 1, root: 3 }]
);
}
#[test]
fn keyed_diffing_additions() {
let dom = new_dom();
let left = rsx!({
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
})
});
let right = rsx!({
[/**/ 4, 5, 6, 7, 8, 9, 10 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
})
});
let (_, change) = dom.lazy_diff(left, right);
assert_eq!(
change.edits,
[
CreateElement { id: 5, tag: "div" },
CreateElement { id: 6, tag: "div" },
InsertAfter { n: 2, root: 4 }
]
);
}
#[test]
fn keyed_diffing_additions_and_moves_on_ends() {
let dom = new_dom();
let left = rsx!({
[/**/ 4, 5, 6, 7 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
})
});
let right = rsx!({
[/**/ 7, 4, 5, 6, 11, 12 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
})
});
let (_, change) = dom.lazy_diff(left, right);
log::debug!("{:?}", change);
assert_eq!(
change.edits,
[
// create 11, 12
CreateElement { tag: "div", id: 4 },
CreateElement { tag: "div", id: 5 },
InsertAfter { root: 2, n: 2 },
// move 7 to the front
PushRoot { id: 3 },
InsertBefore { root: 0, n: 1 }
]
);
}
#[test]
fn keyed_diffing_additions_and_moves_in_middle() {
let dom = new_dom();
let left = rsx!({
[/**/ 4, 5, 6, 7 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
})
});
let right = rsx!({
[/**/ 7, 4, 13, 17, 5, 11, 12, 6 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
})
});
// LIS: 4, 5, 6
let (_, change) = dom.lazy_diff(left, right);
log::debug!("{:#?}", change);
assert_eq!(
change.edits,
[
// create 13, 17
CreateElement { tag: "div", id: 4 },
CreateElement { tag: "div", id: 5 },
InsertBefore { root: 1, n: 2 },
// create 11, 12
CreateElement { tag: "div", id: 6 },
CreateElement { tag: "div", id: 7 },
InsertBefore { root: 2, n: 2 },
// move 7
PushRoot { id: 3 },
InsertBefore { root: 0, n: 1 }
]
);
}
#[test]
fn controlled_keyed_diffing_out_of_order() {
let dom = TestDom::new();
let dom = new_dom();
let left = [4, 5, 6, 7];
let left = rsx!({
left.iter().map(|f| {
rsx! { div { key: "{f}" "{f}" }}
[4, 5, 6, 7].iter().map(|f| {
rsx! { div { key: "{f}" }}
})
});
// 0, 1, 2, 6, 5, 4, 3, 7, 8, 9
let right = [0, 5, 9, 6, 4];
let right = rsx!({
right.iter().map(|f| {
rsx! { div { key: "{f}" "{f}" }}
[0, 5, 9, 6, 4].iter().map(|f| {
rsx! { div { key: "{f}" }}
})
});
// LIS: 3, 7, 8,
let edits = dom.lazy_diff(left, right);
dbg!(&edits);
// LIS: 5, 6
let (_, changes) = dom.lazy_diff(left, right);
log::debug!("{:#?}", &changes);
assert_eq!(
changes.edits,
[
// move 4 to after 6
PushRoot { id: 0 },
InsertAfter { n: 1, root: 2 },
// remove 7
// create 9 and insert before 6
CreateElement { id: 4, tag: "div" },
InsertBefore { n: 1, root: 2 },
// create 0 and insert before 5
CreateElement { id: 5, tag: "div" },
InsertBefore { n: 1, root: 1 },
]
);
}
#[test]
fn controlled_keyed_diffing_out_of_order_max_test() {
let dom = new_dom();
let left = rsx!({
[0, 1, 2, 3, 4].iter().map(|f| {
rsx! { div { key: "{f}" }}
})
});
let right = rsx!({
[3, 0, 1, 10, 2].iter().map(|f| {
rsx! { div { key: "{f}" }}
})
});
let (_, changes) = dom.lazy_diff(left, right);
log::debug!("{:#?}", &changes);
assert_eq!(
changes.edits,
[
CreateElement { id: 5, tag: "div" },
InsertBefore { n: 1, root: 2 },
PushRoot { id: 3 },
InsertBefore { n: 1, root: 0 },
]
);
}

View file

@ -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;
});
}

View file

@ -0,0 +1,21 @@
use anyhow::{Context, Result};
use dioxus::prelude::*;
use dioxus_core as dioxus;
use dioxus_html as dioxus_elements;
#[test]
fn sample_refs() {
// static App: FC<()> = |cx| {
// let div_ref = use_node_ref::<MyRef, _>(cx);
// cx.render(rsx! {
// div {
// style: { color: "red" },
// node_ref: div_ref,
// onmouseover: move |_| {
// div_ref.borrow_mut().focus();
// },
// },
// })
// };
}

View file

@ -0,0 +1,41 @@
use futures_util::StreamExt;
/*
furtures_channel provides us some batching simply due to how Rust's async works.
Any hook that uses schedule_update is simply deferring to unbounded_send. Multiple
unbounded_sends can be linked together in succession provided there isn't an "await"
between them. Our internal batching mechanism simply waits for the "schedule_update"
to fire and then pulls any messages off the unbounded_send queue.
Additionally, due to how our "time slicing" works we'll always come back and check
in for new work if the deadline hasn't expired. On average, our deadline should be
about 10ms, which is way more than enough for diffing/creating to happen.
*/
#[async_std::test]
async fn batch() {
let (sender, mut recver) = futures_channel::mpsc::unbounded::<i32>();
let _handle = async_std::task::spawn(async move {
let _msg = recver.next().await;
while let Ok(msg) = recver.try_next() {
println!("{:#?}", msg);
}
let _msg = recver.next().await;
while let Ok(msg) = recver.try_next() {
println!("{:#?}", msg);
}
});
sender.unbounded_send(1).unwrap();
sender.unbounded_send(2).unwrap();
sender.unbounded_send(3).unwrap();
sender.unbounded_send(4).unwrap();
async_std::task::sleep(std::time::Duration::from_millis(100)).await;
sender.unbounded_send(5).unwrap();
sender.unbounded_send(6).unwrap();
sender.unbounded_send(7).unwrap();
sender.unbounded_send(8).unwrap();
}

View file

@ -0,0 +1,52 @@
pub fn set_up_logging(enabled: bool) {
use fern::colors::{Color, ColoredLevelConfig};
if !enabled {
return;
}
// configure colors for the whole line
let colors_line = ColoredLevelConfig::new()
.error(Color::Red)
.warn(Color::Yellow)
// we actually don't need to specify the color for debug and info, they are white by default
.info(Color::White)
.debug(Color::White)
// depending on the terminals color scheme, this is the same as the background color
.trace(Color::BrightBlack);
// configure colors for the name of the level.
// since almost all of them are the same as the color for the whole line, we
// just clone `colors_line` and overwrite our changes
let colors_level = colors_line.clone().info(Color::Green);
// here we set up our fern Dispatch
// when running tests in batch, the logger is re-used, so ignore the logger error
let _ = fern::Dispatch::new()
.format(move |out, message, record| {
out.finish(format_args!(
"{color_line}[{level}{color_line}] {message}\x1B[0m",
color_line = format_args!(
"\x1B[{}m",
colors_line.get_color(&record.level()).to_fg_str()
),
level = colors_level.color(record.level()),
message = message,
));
})
// set the default log level. to filter out verbose log messages from dependencies, set
// this to Warn and overwrite the log level for your crate.
.level(log::LevelFilter::Debug)
// .level(log::LevelFilter::Warn)
// change log levels for individual modules. Note: This looks for the record's target
// field which defaults to the module path but can be overwritten with the `target`
// parameter:
// `info!(target="special_target", "This log message is about special_target");`
// .level_for("dioxus", log::LevelFilter::Debug)
// .level_for("dioxus", log::LevelFilter::Info)
// .level_for("pretty_colored", log::LevelFilter::Trace)
// output to stdout
.chain(std::io::stdout())
.apply();
}

View file

@ -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);
}

View file

@ -0,0 +1,30 @@
//! Diffing is interruptible, but uses yield_now which is loop-pollable
//!
//! This means you can actually call it synchronously if you want.
use anyhow::{Context, Result};
use dioxus::{prelude::*, scope::Scope};
use dioxus_core as dioxus;
use dioxus_html as dioxus_elements;
use futures_util::FutureExt;
#[test]
fn worksync() {
static App: FC<()> = |cx| {
cx.render(rsx! {
div {"hello"}
})
};
let mut dom = VirtualDom::new(App);
let mut fut = dom.rebuild_async().boxed_local();
let mutations = loop {
let g = (&mut fut).now_or_never();
if g.is_some() {
break g.unwrap();
}
};
dbg!(mutations);
}

View file

@ -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 });
}
}

View file

@ -6,9 +6,9 @@ use std::rc::Rc;
use dioxus_core::{
events::{
on::{MouseEvent, MouseEventInner},
VirtualEvent,
SyntheticEvent,
},
ElementId, EventPriority, EventTrigger, ScopeId,
ElementId, EventPriority, ScopeId, UserEvent,
};
#[derive(serde::Serialize, serde::Deserialize)]
@ -17,15 +17,19 @@ struct ImEvent {
mounted_dom_id: u64,
scope: u64,
}
pub fn trigger_from_serialized(val: serde_json::Value) -> EventTrigger {
pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
let mut data: Vec<ImEvent> = serde_json::from_value(val).unwrap();
let data = data.drain(..).next().unwrap();
let event = VirtualEvent::MouseEvent(MouseEvent(Rc::new(WebviewMouseEvent)));
let event = SyntheticEvent::MouseEvent(MouseEvent(Rc::new(WebviewMouseEvent)));
let scope = ScopeId(data.scope as usize);
let mounted_dom_id = Some(ElementId(data.mounted_dom_id as usize));
let priority = EventPriority::High;
EventTrigger::new(event, scope, mounted_dom_id, priority)
UserEvent {
name: todo!(),
event,
scope,
mounted_dom_id,
}
}
#[derive(Debug)]

View file

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

View file

@ -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 });
}
}

View file

@ -117,8 +117,8 @@ impl<T: Properties + 'static> WebviewRenderer<T> {
// Serialize the edit stream
let edits = {
let mut edits = Vec::new();
lock.rebuild(&mut edits).unwrap();
let mut edits = Vec::<DomEdit>::new();
// lock.rebuild(&mut edits).unwrap();
serde_json::to_value(edits).unwrap()
};
@ -139,8 +139,8 @@ impl<T: Properties + 'static> WebviewRenderer<T> {
// Serialize the edit stream
let edits = {
let mut edits = Vec::new();
lock.rebuild(&mut edits).unwrap();
let mut edits = Vec::<DomEdit>::new();
// lock.rebuild(&mut edits).unwrap();
serde_json::to_value(edits).unwrap()
};

View file

@ -0,0 +1,25 @@
use dioxus::virtual_dom::VirtualDom;
use dioxus_core as dioxus;
use dioxus_core::prelude::*;
use dioxus_html as dioxus_elements;
fn main() {
let mut dom = VirtualDom::new(App);
dom.rebuild();
println!(
"{}",
dioxus_ssr::render_vdom(&dom, |c| c.newline(true).indent(true))
)
}
pub static App: FC<()> = |cx| {
cx.render(rsx!(
div {
class: "overflow-hidden"
ul {
{(0..10).map(|i| rsx!{ li { class: "flex flex-col", "entry: {i}"}})}
}
"hello world!"
}
))
};

View file

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

View file

@ -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!(
"{}",

View file

@ -46,7 +46,7 @@ pub fn render_vdom_scope(vdom: &VirtualDom, scope: ScopeId) -> Option<String> {
/// ```ignore
/// static App: FC<()> = |cx| cx.render(rsx!(div { "hello world" }));
/// let mut vdom = VirtualDom::new(App);
/// vdom.rebuild_in_place();
/// vdom.rebuild();
///
/// let renderer = TextRenderer::new(&vdom);
/// let output = format!("{}", renderer);
@ -74,8 +74,8 @@ impl<'a> TextRenderer<'a> {
}
fn html_render(&self, node: &VNode, f: &mut std::fmt::Formatter, il: u16) -> std::fmt::Result {
match &node.kind {
VNodeKind::Text(text) => {
match &node {
VNode::Text(text) => {
if self.cfg.indent {
for _ in 0..il {
write!(f, " ")?;
@ -83,7 +83,7 @@ impl<'a> TextRenderer<'a> {
}
write!(f, "{}", text.text)?
}
VNodeKind::Anchor(anchor) => {
VNode::Anchor(anchor) => {
//
if self.cfg.indent {
for _ in 0..il {
@ -92,7 +92,7 @@ impl<'a> TextRenderer<'a> {
}
write!(f, "<!-- -->")?;
}
VNodeKind::Element(el) => {
VNode::Element(el) => {
if self.cfg.indent {
for _ in 0..il {
write!(f, " ")?;
@ -129,7 +129,7 @@ impl<'a> TextRenderer<'a> {
//
// when the page is loaded, the `querySelectorAll` will be used to collect all the nodes, and then add
// them interpreter's stack
match (self.cfg.pre_render, node.try_direct_id()) {
match (self.cfg.pre_render, node.try_mounted_id()) {
(true, Some(id)) => {
write!(f, " dio_el=\"{}\"", id)?;
//
@ -163,13 +163,13 @@ impl<'a> TextRenderer<'a> {
write!(f, "\n")?;
}
}
VNodeKind::Fragment(frag) => {
VNode::Fragment(frag) => {
for child in frag.children {
self.html_render(child, f, il + 1)?;
}
}
VNodeKind::Component(vcomp) => {
let idx = vcomp.ass_scope.get().unwrap();
VNode::Component(vcomp) => {
let idx = vcomp.associated_scope.get().unwrap();
match (self.vdom, self.cfg.skip_components) {
(Some(vdom), false) => {
let new_node = vdom.get_scope(idx).unwrap().root();
@ -180,7 +180,7 @@ impl<'a> TextRenderer<'a> {
}
}
}
VNodeKind::Suspended { .. } => {
VNode::Suspended { .. } => {
// we can't do anything with suspended nodes
}
}
@ -286,28 +286,28 @@ mod tests {
#[test]
fn to_string_works() {
let mut dom = VirtualDom::new(SIMPLE_APP);
dom.rebuild_in_place().expect("failed to run virtualdom");
dom.rebuild();
dbg!(render_vdom(&dom, |c| c));
}
#[test]
fn hydration() {
let mut dom = VirtualDom::new(NESTED_APP);
dom.rebuild_in_place().expect("failed to run virtualdom");
dom.rebuild();
dbg!(render_vdom(&dom, |c| c.pre_render(true)));
}
#[test]
fn nested() {
let mut dom = VirtualDom::new(NESTED_APP);
dom.rebuild_in_place().expect("failed to run virtualdom");
dom.rebuild();
dbg!(render_vdom(&dom, |c| c));
}
#[test]
fn fragment_app() {
let mut dom = VirtualDom::new(FRAGMENT_APP);
dom.rebuild_in_place().expect("failed to run virtualdom");
dom.rebuild();
dbg!(render_vdom(&dom, |c| c));
}
@ -319,7 +319,7 @@ mod tests {
let mut file = File::create("index.html").unwrap();
let mut dom = VirtualDom::new(SLIGHTLY_MORE_COMPLEX);
dom.rebuild_in_place().expect("failed to run virtualdom");
dom.rebuild();
file.write_fmt(format_args!(
"{}",
@ -337,7 +337,7 @@ mod tests {
};
let mut dom = VirtualDom::new(STLYE_APP);
dom.rebuild_in_place().expect("failed to run virtualdom");
dom.rebuild();
dbg!(render_vdom(&dom, |c| c));
}
}

View file

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

View file

@ -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"
}
}
}
})
};

View file

@ -1,14 +1,16 @@
use std::{collections::HashMap, rc::Rc, sync::Arc};
use std::{collections::HashMap, fmt::Debug, rc::Rc, sync::Arc};
use dioxus_core::{
events::{EventTrigger, VirtualEvent},
events::{on::GenericEventInner, SyntheticEvent, UserEvent},
mutations::NodeRefMutation,
scheduler::SchedulerMsg,
DomEdit, ElementId, ScopeId,
};
use fxhash::FxHashMap;
use wasm_bindgen::{closure::Closure, JsCast};
use web_sys::{
window, CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement,
HtmlOptionElement, Node, NodeList,
HtmlOptionElement, Node, NodeList, UiEvent,
};
use crate::{nodeslab::NodeSlab, WebConfig};
@ -23,7 +25,7 @@ pub struct WebsysDom {
root: Element,
sender_callback: Rc<dyn Fn(EventTrigger)>,
sender_callback: Rc<dyn Fn(SchedulerMsg)>,
// map of listener types to number of those listeners
// This is roughly a delegater
@ -38,7 +40,7 @@ pub struct WebsysDom {
last_node_was_text: bool,
}
impl WebsysDom {
pub fn new(root: Element, cfg: WebConfig, sender_callback: Rc<dyn Fn(EventTrigger)>) -> Self {
pub fn new(root: Element, cfg: WebConfig, sender_callback: Rc<dyn Fn(SchedulerMsg)>) -> Self {
let document = load_document();
let mut nodes = NodeSlab::new(2000);
@ -77,6 +79,18 @@ impl WebsysDom {
}
}
pub fn apply_refs(&mut self, refs: &[NodeRefMutation]) {
for item in refs {
if let Some(bla) = &item.element {
let node = self.nodes[item.element_id.as_u64() as usize]
.as_ref()
.unwrap()
.clone();
bla.set(Box::new(node)).unwrap();
}
}
}
pub fn process_edits(&mut self, edits: &mut Vec<DomEdit>) {
for edit in edits.drain(..) {
log::info!("Handling edit: {:#?}", edit);
@ -84,26 +98,27 @@ impl WebsysDom {
DomEdit::PushRoot { id: root } => self.push(root),
DomEdit::PopRoot => self.pop(),
DomEdit::AppendChildren { many } => self.append_children(many),
DomEdit::ReplaceWith { n, m } => self.replace_with(n, m),
DomEdit::Remove => self.remove(),
DomEdit::ReplaceWith { m, root } => self.replace_with(m, root),
DomEdit::Remove { root } => self.remove(root),
DomEdit::RemoveAllChildren => self.remove_all_children(),
DomEdit::CreateTextNode { text, id } => self.create_text_node(text, id),
DomEdit::CreateElement { tag, id } => self.create_element(tag, None, id),
DomEdit::CreateElementNs { tag, id, ns } => self.create_element(tag, Some(ns), id),
DomEdit::CreatePlaceholder { id } => self.create_placeholder(id),
DomEdit::NewEventListener {
event_name: event,
event_name,
scope,
mounted_node_id: node,
} => self.new_event_listener(event, scope, node),
mounted_node_id,
} => self.new_event_listener(event_name, scope, mounted_node_id),
DomEdit::RemoveEventListener { event } => todo!(),
DomEdit::SetText { text } => self.set_text(text),
DomEdit::SetAttribute { field, value, ns } => self.set_attribute(field, value, ns),
DomEdit::RemoveAttribute { name } => self.remove_attribute(name),
DomEdit::InsertAfter { n } => self.insert_after(n),
DomEdit::InsertBefore { n } => self.insert_before(n),
DomEdit::InsertAfter { n, root } => self.insert_after(n, root),
DomEdit::InsertBefore { n, root } => self.insert_before(n, root),
}
}
}
@ -118,14 +133,13 @@ impl WebsysDom {
self.stack.push(real_node);
}
// drop the node off the stack
fn pop(&mut self) {
self.stack.pop();
}
fn append_children(&mut self, many: u32) {
log::debug!("Called [`append_child`]");
let root: Node = self
.stack
.list
@ -133,9 +147,11 @@ impl WebsysDom {
.unwrap()
.clone();
for _ in 0..many {
let child = self.stack.pop();
for child in self
.stack
.list
.drain((self.stack.list.len() - many as usize)..)
{
if child.dyn_ref::<web_sys::Text>().is_some() {
if self.last_node_was_text {
let comment_node = self
@ -143,75 +159,46 @@ impl WebsysDom {
.create_comment("dioxus")
.dyn_into::<Node>()
.unwrap();
self.stack.top().append_child(&comment_node).unwrap();
root.append_child(&comment_node).unwrap();
}
self.last_node_was_text = true;
} else {
self.last_node_was_text = false;
}
root.append_child(&child).unwrap();
}
}
fn replace_with(&mut self, n: u32, m: u32) {
log::debug!("Called [`replace_with`]");
fn replace_with(&mut self, m: u32, root: u64) {
let old = self.nodes[root as usize].as_ref().unwrap();
let mut new_nodes = vec![];
for _ in 0..m {
new_nodes.push(self.stack.pop());
let arr: js_sys::Array = self
.stack
.list
.drain((self.stack.list.len() - m as usize)..)
.collect();
if let Some(el) = old.dyn_ref::<Element>() {
el.replace_with_with_node(&arr).unwrap();
} else if let Some(el) = old.dyn_ref::<web_sys::CharacterData>() {
el.replace_with_with_node(&arr).unwrap();
} else if let Some(el) = old.dyn_ref::<web_sys::DocumentType>() {
el.replace_with_with_node(&arr).unwrap();
}
let mut old_nodes = vec![];
for _ in 0..n {
old_nodes.push(self.stack.pop());
}
for node in &old_nodes[1..] {
node.dyn_ref::<Element>().unwrap().remove();
}
let old = old_nodes[0].clone();
let arr: js_sys::Array = new_nodes.iter().collect();
let el = old.dyn_into::<Element>().unwrap();
el.replace_with_with_node(&arr).unwrap();
// let arr = js_sys::Array::from();
// TODO: use different-sized replace withs
// if m == 1 {
// if old_node.has_type::<Element>() {
// old_node
// .dyn_ref::<Element>()
// .unwrap()
// .replace_with_with_node_1(&new_node)
// .unwrap();
// } else if old_node.has_type::<web_sys::CharacterData>() {
// old_node
// .dyn_ref::<web_sys::CharacterData>()
// .unwrap()
// .replace_with_with_node_1(&new_node)
// .unwrap();
// } else if old_node.has_type::<web_sys::DocumentType>() {
// old_node
// .dyn_ref::<web_sys::DocumentType>()
// .unwrap()
// .replace_with_with_node_1(&new_node)
// .unwrap();
// } else {
// panic!("Cannot replace node: {:?}", old_node);
// }
// }
// self.stack.push(new_node);
}
fn remove(&mut self) {
log::debug!("Called [`remove`]");
todo!()
fn remove(&mut self, root: u64) {
let node = self.nodes[root as usize].as_ref().unwrap();
if let Some(element) = node.dyn_ref::<Element>() {
element.remove();
} else {
if let Some(parent) = node.parent_node() {
parent.remove_child(&node).unwrap();
}
}
}
fn remove_all_children(&mut self) {
log::debug!("Called [`remove_all_children`]");
todo!()
}
@ -219,22 +206,22 @@ impl WebsysDom {
self.create_element("pre", None, id);
self.set_attribute("hidden", "", None);
}
fn create_text_node(&mut self, text: &str, id: u64) {
// let nid = self.node_counter.next();
fn create_text_node(&mut self, text: &str, id: u64) {
let textnode = self
.document
.create_text_node(text)
.dyn_into::<Node>()
.unwrap();
let id = id as usize;
self.stack.push(textnode.clone());
self.nodes[id] = Some(textnode);
self.nodes[(id as usize)] = Some(textnode);
}
fn create_element(&mut self, tag: &str, ns: Option<&'static str>, id: u64) {
let tag = wasm_bindgen::intern(tag);
let el = match ns {
Some(ns) => self
.document
@ -249,18 +236,12 @@ impl WebsysDom {
.dyn_into::<Node>()
.unwrap(),
};
let id = id as usize;
self.stack.push(el.clone());
self.nodes[id] = Some(el);
// let nid = self.node_counter.?next();
// let nid = self.nodes.insert(el).data().as_ffi();
// log::debug!("Called [`create_element`]: {}, {:?}", tag, nid);
// ElementId::new(nid)
self.nodes[(id as usize)] = Some(el);
}
fn new_event_listener(&mut self, event: &'static str, scope: ScopeId, real_id: u64) {
// let (_on, event) = event.split_at(2);
let event = wasm_bindgen::intern(event);
// attach the correct attributes to the element
@ -297,7 +278,7 @@ impl WebsysDom {
// "Result" cannot be received from JS
// Instead, we just build and immediately execute a closure that returns result
match decode_trigger(event) {
Ok(synthetic_event) => trigger.as_ref()(synthetic_event),
Ok(synthetic_event) => trigger.as_ref()(SchedulerMsg::UiEvent(synthetic_event)),
Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e),
};
}) as Box<dyn FnMut(&Event)>);
@ -320,7 +301,8 @@ impl WebsysDom {
}
fn set_attribute(&mut self, name: &str, value: &str, ns: Option<&str>) {
if let Some(el) = self.stack.top().dyn_ref::<Element>() {
let node = self.stack.top();
if let Some(el) = node.dyn_ref::<Element>() {
match ns {
// inline style support
Some("style") => {
@ -330,11 +312,20 @@ impl WebsysDom {
}
_ => el.set_attribute(name, value).unwrap(),
}
match name {
"value" => {}
"checked" => {}
"selected" => {}
_ => {}
}
if let Some(node) = node.dyn_ref::<HtmlInputElement>() {
if name == "value" {
node.set_value(value);
}
if name == "checked" {
node.set_checked(true);
}
}
if let Some(node) = node.dyn_ref::<HtmlOptionElement>() {
if name == "selected" {
node.set_selected(true);
}
}
}
@ -361,49 +352,52 @@ impl WebsysDom {
}
}
fn insert_after(&mut self, n: u32) {
let mut new_nodes = vec![];
for _ in 0..n {
new_nodes.push(self.stack.pop());
}
fn insert_after(&mut self, n: u32, root: u64) {
let old = self.nodes[root as usize].as_ref().unwrap();
let after = self.stack.top().clone();
let arr: js_sys::Array = new_nodes.iter().collect();
let el = after.dyn_into::<Element>().unwrap();
el.after_with_node(&arr).unwrap();
// let mut old_nodes = vec![];
// for _ in 0..n {
// old_nodes.push(self.stack.pop());
// }
// let el = self.stack.top();
}
fn insert_before(&mut self, n: u32) {
let n = n as usize;
let root = self
let arr: js_sys::Array = self
.stack
.list
.get(self.stack.list.len() - n)
.unwrap()
.clone();
for _ in 0..n {
let el = self.stack.pop();
root.insert_before(&el, None).unwrap();
.drain((self.stack.list.len() - n as usize)..)
.collect();
if let Some(el) = old.dyn_ref::<Element>() {
el.after_with_node(&arr).unwrap();
} else if let Some(el) = old.dyn_ref::<web_sys::CharacterData>() {
el.after_with_node(&arr).unwrap();
} else if let Some(el) = old.dyn_ref::<web_sys::DocumentType>() {
el.after_with_node(&arr).unwrap();
}
}
}
impl<'a> dioxus_core::diff::RealDom for WebsysDom {
// fn request_available_node(&mut self) -> ElementId {
// let key = self.nodes.insert(None);
// log::debug!("making new key: {:#?}", key);
// ElementId(key.data().as_ffi())
// }
fn insert_before(&mut self, n: u32, root: u64) {
let after = self.nodes[root as usize].as_ref().unwrap();
fn raw_node_as_any(&self) -> &mut dyn std::any::Any {
todo!()
if n == 1 {
let before = self.stack.pop();
after
.parent_node()
.unwrap()
.insert_before(&before, Some(&after))
.unwrap();
after.insert_before(&before, None).unwrap();
} else {
let arr: js_sys::Array = self
.stack
.list
.drain((self.stack.list.len() - n as usize)..)
.collect();
if let Some(el) = after.dyn_ref::<Element>() {
el.before_with_node(&arr).unwrap();
} else if let Some(el) = after.dyn_ref::<web_sys::CharacterData>() {
el.before_with_node(&arr).unwrap();
} else if let Some(el) = after.dyn_ref::<web_sys::DocumentType>() {
el.before_with_node(&arr).unwrap();
}
}
}
}
@ -443,287 +437,89 @@ impl Stack {
}
}
fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent {
fn virtual_event_from_websys_event(event: web_sys::Event) -> SyntheticEvent {
use crate::events::*;
use dioxus_core::events::on::*;
match event.type_().as_str() {
"copy" | "cut" | "paste" => {
struct WebsysClipboardEvent();
impl ClipboardEventInner for WebsysClipboardEvent {}
VirtualEvent::ClipboardEvent(ClipboardEvent(Rc::new(WebsysClipboardEvent())))
SyntheticEvent::ClipboardEvent(ClipboardEvent(Rc::new(WebsysClipboardEvent(event))))
}
"compositionend" | "compositionstart" | "compositionupdate" => {
let evt: web_sys::CompositionEvent = event.clone().dyn_into().unwrap();
struct WebsysCompositionEvent(web_sys::CompositionEvent);
impl CompositionEventInner for WebsysCompositionEvent {
fn data(&self) -> String {
todo!()
}
}
VirtualEvent::CompositionEvent(CompositionEvent(Rc::new(WebsysCompositionEvent(evt))))
SyntheticEvent::CompositionEvent(CompositionEvent(Rc::new(WebsysCompositionEvent(evt))))
}
"keydown" | "keypress" | "keyup" => {
struct Event(web_sys::KeyboardEvent);
impl KeyboardEventInner for Event {
fn char_code(&self) -> u32 {
todo!()
}
fn key_code(&self) -> KeyCode {
todo!()
}
fn ctrl_key(&self) -> bool {
todo!()
}
fn key(&self) -> String {
todo!()
}
fn locale(&self) -> String {
todo!()
}
fn location(&self) -> usize {
todo!()
}
fn meta_key(&self) -> bool {
todo!()
}
fn repeat(&self) -> bool {
todo!()
}
fn shift_key(&self) -> bool {
todo!()
}
fn which(&self) -> usize {
todo!()
}
fn get_modifier_state(&self, key_code: usize) -> bool {
todo!()
}
}
let evt: web_sys::KeyboardEvent = event.clone().dyn_into().unwrap();
VirtualEvent::KeyboardEvent(KeyboardEvent(Rc::new(Event(evt))))
let evt: web_sys::KeyboardEvent = event.dyn_into().unwrap();
SyntheticEvent::KeyboardEvent(KeyboardEvent(Rc::new(WebsysKeyboardEvent(evt))))
}
"focus" | "blur" => {
struct Event(web_sys::FocusEvent);
impl FocusEventInner for Event {}
let evt: web_sys::FocusEvent = event.clone().dyn_into().unwrap();
VirtualEvent::FocusEvent(FocusEvent(Rc::new(Event(evt))))
let evt: web_sys::FocusEvent = event.dyn_into().unwrap();
SyntheticEvent::FocusEvent(FocusEvent(Rc::new(WebsysFocusEvent(evt))))
}
"change" => {
// struct Event(web_sys::Event);
// impl GenericEventInner for Event {
// fn bubbles(&self) -> bool {
// todo!()
// }
// fn cancel_bubble(&self) {
// todo!()
// }
// fn cancelable(&self) -> bool {
// todo!()
// }
// fn composed(&self) -> bool {
// todo!()
// }
// fn composed_path(&self) -> String {
// todo!()
// }
// fn current_target(&self) {
// todo!()
// }
// fn default_prevented(&self) -> bool {
// todo!()
// }
// fn event_phase(&self) -> usize {
// todo!()
// }
// fn is_trusted(&self) -> bool {
// todo!()
// }
// fn prevent_default(&self) {
// todo!()
// }
// fn stop_immediate_propagation(&self) {
// todo!()
// }
// fn stop_propagation(&self) {
// todo!()
// }
// fn target(&self) {
// todo!()
// }
// fn time_stamp(&self) -> usize {
// todo!()
// }
// }
// let evt: web_sys::Event = event.clone().dyn_into().expect("wrong error typ");
// VirtualEvent::Event(GenericEvent(Rc::new(Event(evt))))
todo!()
let evt = event.dyn_into().unwrap();
SyntheticEvent::GenericEvent(GenericEvent(Rc::new(WebsysGenericUiEvent(evt))))
}
"input" | "invalid" | "reset" | "submit" => {
// is a special react events
let evt: web_sys::InputEvent = event.clone().dyn_into().expect("wrong event type");
let this: web_sys::EventTarget = evt.target().unwrap();
let value = (&this)
.dyn_ref()
.map(|input: &web_sys::HtmlInputElement| input.value())
.or_else(|| {
(&this)
.dyn_ref()
.map(|input: &web_sys::HtmlTextAreaElement| input.value())
})
.or_else(|| {
(&this)
.dyn_ref::<web_sys::HtmlElement>()
.unwrap()
.text_content()
})
.expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener");
todo!()
// VirtualEvent::FormEvent(FormEvent { value })
let evt: web_sys::InputEvent = event.clone().dyn_into().unwrap();
SyntheticEvent::FormEvent(FormEvent(Rc::new(WebsysFormEvent(evt))))
}
"click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit"
| "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter"
| "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => {
let evt: web_sys::MouseEvent = event.clone().dyn_into().unwrap();
#[derive(Debug)]
pub struct CustomMouseEvent(web_sys::MouseEvent);
impl dioxus_core::events::on::MouseEventInner for CustomMouseEvent {
fn alt_key(&self) -> bool {
self.0.alt_key()
}
fn button(&self) -> i16 {
self.0.button()
}
fn buttons(&self) -> u16 {
self.0.buttons()
}
fn client_x(&self) -> i32 {
self.0.client_x()
}
fn client_y(&self) -> i32 {
self.0.client_y()
}
fn ctrl_key(&self) -> bool {
self.0.ctrl_key()
}
fn meta_key(&self) -> bool {
self.0.meta_key()
}
fn page_x(&self) -> i32 {
self.0.page_x()
}
fn page_y(&self) -> i32 {
self.0.page_y()
}
fn screen_x(&self) -> i32 {
self.0.screen_x()
}
fn screen_y(&self) -> i32 {
self.0.screen_y()
}
fn shift_key(&self) -> bool {
self.0.shift_key()
}
// yikes
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
fn get_modifier_state(&self, key_code: &str) -> bool {
self.0.get_modifier_state(key_code)
}
}
VirtualEvent::MouseEvent(MouseEvent(Rc::new(CustomMouseEvent(evt))))
SyntheticEvent::MouseEvent(MouseEvent(Rc::new(WebsysMouseEvent(evt))))
}
"pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture"
| "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => {
let evt: web_sys::PointerEvent = event.clone().dyn_into().unwrap();
todo!()
SyntheticEvent::PointerEvent(PointerEvent(Rc::new(WebsysPointerEvent(evt))))
}
"select" => {
// let evt: web_sys::SelectionEvent = event.clone().dyn_into().unwrap();
// not required to construct anything special beyond standard event stuff
todo!()
let evt: web_sys::UiEvent = event.clone().dyn_into().unwrap();
SyntheticEvent::SelectionEvent(SelectionEvent(Rc::new(WebsysGenericUiEvent(evt))))
}
"touchcancel" | "touchend" | "touchmove" | "touchstart" => {
let evt: web_sys::TouchEvent = event.clone().dyn_into().unwrap();
todo!()
SyntheticEvent::TouchEvent(TouchEvent(Rc::new(WebsysTouchEvent(evt))))
}
"scroll" => {
// let evt: web_sys::UIEvent = event.clone().dyn_into().unwrap();
todo!()
let evt: web_sys::UiEvent = event.clone().dyn_into().unwrap();
SyntheticEvent::GenericEvent(GenericEvent(Rc::new(WebsysGenericUiEvent(evt))))
}
"wheel" => {
let evt: web_sys::WheelEvent = event.clone().dyn_into().unwrap();
todo!()
SyntheticEvent::WheelEvent(WheelEvent(Rc::new(WebsysWheelEvent(evt))))
}
"animationstart" | "animationend" | "animationiteration" => {
let evt: web_sys::AnimationEvent = event.clone().dyn_into().unwrap();
todo!()
SyntheticEvent::AnimationEvent(AnimationEvent(Rc::new(WebsysAnimationEvent(evt))))
}
"transitionend" => {
let evt: web_sys::TransitionEvent = event.clone().dyn_into().unwrap();
todo!()
SyntheticEvent::TransitionEvent(TransitionEvent(Rc::new(WebsysTransitionEvent(evt))))
}
"abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted"
| "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play"
| "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend"
| "timeupdate" | "volumechange" | "waiting" => {
// not required to construct anything special beyond standard event stuff
// let evt: web_sys::MediaEvent = event.clone().dyn_into().unwrap();
// let evt: web_sys::MediaEvent = event.clone().dyn_into().unwrap();
todo!()
let evt: web_sys::UiEvent = event.clone().dyn_into().unwrap();
SyntheticEvent::MediaEvent(MediaEvent(Rc::new(WebsysMediaEvent(evt))))
}
"toggle" => {
// not required to construct anything special beyond standard event stuff (target)
// let evt: web_sys::ToggleEvent = event.clone().dyn_into().unwrap();
todo!()
let evt: web_sys::UiEvent = event.clone().dyn_into().unwrap();
SyntheticEvent::ToggleEvent(ToggleEvent(Rc::new(WebsysToggleEvent(evt))))
}
_ => {
todo!()
let evt: web_sys::UiEvent = event.clone().dyn_into().unwrap();
SyntheticEvent::GenericEvent(GenericEvent(Rc::new(WebsysGenericUiEvent(evt))))
}
}
}
/// This function decodes a websys event and produces an EventTrigger
/// With the websys implementation, we attach a unique key to the nodes
fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<EventTrigger> {
fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<UserEvent> {
log::debug!("Handling event!");
let target = event
@ -734,13 +530,13 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<EventTrigger> {
let typ = event.type_();
use anyhow::Context;
// let attrs = target.attributes();
// for x in 0..attrs.length() {
// let attr = attrs.item(x).unwrap();
// log::debug!("attrs include: {:#?}", attr);
// }
let attrs = target.attributes();
for x in 0..attrs.length() {
let attr = attrs.item(x).unwrap();
log::debug!("attrs include: {:#?}", attr);
}
use anyhow::Context;
// The error handling here is not very descriptive and needs to be replaced with a zero-cost error system
let val: String = target
@ -765,12 +561,12 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<EventTrigger> {
let triggered_scope = gi_id;
// let triggered_scope: ScopeId = KeyData::from_ffi(gi_id).into();
log::debug!("Triggered scope is {:#?}", triggered_scope);
Ok(EventTrigger::new(
virtual_event_from_websys_event(event),
ScopeId(triggered_scope as usize),
Some(ElementId(real_id as usize)),
dioxus_core::events::EventPriority::High,
))
Ok(UserEvent {
name: event_name_from_typ(&typ),
event: virtual_event_from_websys_event(event.clone()),
mounted_dom_id: Some(ElementId(real_id as usize)),
scope: ScopeId(triggered_scope as usize),
})
}
pub fn prepare_websys_dom() -> Element {
@ -783,3 +579,90 @@ pub fn load_document() -> Document {
.document()
.expect("should have access to the Document")
}
pub fn event_name_from_typ(typ: &str) -> &'static str {
match typ {
"copy" => "oncopy",
"cut" => "oncut",
"paste" => "onpaste",
"compositionend" => "oncompositionend",
"compositionstart" => "oncompositionstart",
"compositionupdate" => "oncompositionupdate",
"keydown" => "onkeydown",
"keypress" => "onkeypress",
"keyup" => "onkeyup",
"focus" => "onfocus",
"blur" => "onblur",
"change" => "onchange",
"input" => "oninput",
"invalid" => "oninvalid",
"reset" => "onreset",
"submit" => "onsubmit",
"click" => "onclick",
"contextmenu" => "oncontextmenu",
"doubleclick" => "ondoubleclick",
"drag" => "ondrag",
"dragend" => "ondragend",
"dragenter" => "ondragenter",
"dragexit" => "ondragexit",
"dragleave" => "ondragleave",
"dragover" => "ondragover",
"dragstart" => "ondragstart",
"drop" => "ondrop",
"mousedown" => "onmousedown",
"mouseenter" => "onmouseenter",
"mouseleave" => "onmouseleave",
"mousemove" => "onmousemove",
"mouseout" => "onmouseout",
"mouseover" => "onmouseover",
"mouseup" => "onmouseup",
"pointerdown" => "onpointerdown",
"pointermove" => "onpointermove",
"pointerup" => "onpointerup",
"pointercancel" => "onpointercancel",
"gotpointercapture" => "ongotpointercapture",
"lostpointercapture" => "onlostpointercapture",
"pointerenter" => "onpointerenter",
"pointerleave" => "onpointerleave",
"pointerover" => "onpointerover",
"pointerout" => "onpointerout",
"select" => "onselect",
"touchcancel" => "ontouchcancel",
"touchend" => "ontouchend",
"touchmove" => "ontouchmove",
"touchstart" => "ontouchstart",
"scroll" => "onscroll",
"wheel" => "onwheel",
"animationstart" => "onanimationstart",
"animationend" => "onanimationend",
"animationiteration" => "onanimationiteration",
"transitionend" => "ontransitionend",
"abort" => "onabort",
"canplay" => "oncanplay",
"canplaythrough" => "oncanplaythrough",
"durationchange" => "ondurationchange",
"emptied" => "onemptied",
"encrypted" => "onencrypted",
"ended" => "onended",
"error" => "onerror",
"loadeddata" => "onloadeddata",
"loadedmetadata" => "onloadedmetadata",
"loadstart" => "onloadstart",
"pause" => "onpause",
"play" => "onplay",
"playing" => "onplaying",
"progress" => "onprogress",
"ratechange" => "onratechange",
"seeked" => "onseeked",
"seeking" => "onseeking",
"stalled" => "onstalled",
"suspend" => "onsuspend",
"timeupdate" => "ontimeupdate",
"volumechange" => "onvolumechange",
"waiting" => "onwaiting",
"toggle" => "ontoggle",
_ => {
panic!("unsupported event type")
}
}
}

478
packages/web/src/events.rs Normal file
View file

@ -0,0 +1,478 @@
//! Ported events into Dioxus Synthetic Event system
//!
//! event porting is pretty boring, sorry.
use dioxus_core::events::on::*;
use wasm_bindgen::JsCast;
use web_sys::{Event, UiEvent};
/// All events implement the generic event type - they're all UI events
trait WebsysGenericEvent {
fn as_ui_event(&self) -> &UiEvent;
}
impl GenericEventInner for &dyn WebsysGenericEvent {
/// On WebSys, this returns an &UiEvent which can be casted via dyn_ref into the correct sub type.
fn raw_event(&self) -> &dyn std::any::Any {
self.as_ui_event()
}
fn bubbles(&self) -> bool {
self.as_ui_event().bubbles()
}
fn cancel_bubble(&self) {
self.as_ui_event().cancel_bubble();
}
fn cancelable(&self) -> bool {
self.as_ui_event().cancelable()
}
fn composed(&self) -> bool {
self.as_ui_event().composed()
}
fn current_target(&self) {
if cfg!(debug_assertions) {
todo!("Current target does not return anything useful.\nPlease try casting the event directly.");
}
// self.as_ui_event().current_target();
}
fn default_prevented(&self) -> bool {
self.as_ui_event().default_prevented()
}
fn event_phase(&self) -> u16 {
self.as_ui_event().event_phase()
}
fn is_trusted(&self) -> bool {
self.as_ui_event().is_trusted()
}
fn prevent_default(&self) {
self.as_ui_event().prevent_default()
}
fn stop_immediate_propagation(&self) {
self.as_ui_event().stop_immediate_propagation()
}
fn stop_propagation(&self) {
self.as_ui_event().stop_propagation()
}
fn target(&self) {
todo!()
}
fn time_stamp(&self) -> f64 {
self.as_ui_event().time_stamp()
}
}
macro_rules! implement_generic_event {
(
$($event:ident),*
) => {
$(
impl WebsysGenericEvent for $event {
fn as_ui_event(&self) -> &UiEvent {
self.0.dyn_ref().unwrap()
}
}
)*
};
}
implement_generic_event! {
WebsysClipboardEvent,
WebsysCompositionEvent,
WebsysKeyboardEvent,
WebsysGenericUiEvent,
WebsysFocusEvent,
WebsysFormEvent,
WebsysMouseEvent,
WebsysPointerEvent,
WebsysWheelEvent,
WebsysAnimationEvent,
WebsysTransitionEvent,
WebsysTouchEvent,
WebsysMediaEvent,
WebsysToggleEvent
}
// unfortunately, currently experimental, and web_sys needs to be configured to use it :>(
pub struct WebsysClipboardEvent(pub Event);
impl ClipboardEventInner for WebsysClipboardEvent {}
pub struct WebsysCompositionEvent(pub web_sys::CompositionEvent);
impl CompositionEventInner for WebsysCompositionEvent {
fn data(&self) -> String {
self.0.data().unwrap_or_else(|| String::new())
}
}
pub struct WebsysKeyboardEvent(pub web_sys::KeyboardEvent);
impl KeyboardEventInner for WebsysKeyboardEvent {
fn alt_key(&self) -> bool {
self.0.alt_key()
}
fn char_code(&self) -> u32 {
self.0.char_code()
}
fn key(&self) -> String {
self.0.key()
}
fn key_code(&self) -> KeyCode {
KeyCode::from_raw_code(self.0.key_code() as u8)
}
fn ctrl_key(&self) -> bool {
self.0.ctrl_key()
}
fn get_modifier_state(&self, key_code: &str) -> bool {
self.0.get_modifier_state(key_code)
}
fn locale(&self) -> String {
if cfg!(debug_assertions) {
todo!("Locale is currently not supported. :(")
} else {
String::from("en-US")
}
}
fn location(&self) -> usize {
self.0.location() as usize
}
fn meta_key(&self) -> bool {
self.0.meta_key()
}
fn repeat(&self) -> bool {
self.0.repeat()
}
fn shift_key(&self) -> bool {
self.0.shift_key()
}
fn which(&self) -> usize {
self.0.which() as usize
}
}
pub struct WebsysGenericUiEvent(pub UiEvent);
impl GenericEventInner for WebsysGenericUiEvent {
fn raw_event(&self) -> &dyn std::any::Any {
// self.0.raw_event()
todo!()
}
fn bubbles(&self) -> bool {
self.0.bubbles()
}
fn cancel_bubble(&self) {
self.0.cancel_bubble();
}
fn cancelable(&self) -> bool {
self.0.cancelable()
}
fn composed(&self) -> bool {
self.0.composed()
}
fn current_target(&self) {
// self.0.current_target()
}
fn default_prevented(&self) -> bool {
self.0.default_prevented()
}
fn event_phase(&self) -> u16 {
self.0.event_phase()
}
fn is_trusted(&self) -> bool {
self.0.is_trusted()
}
fn prevent_default(&self) {
self.0.prevent_default()
}
fn stop_immediate_propagation(&self) {
self.0.stop_immediate_propagation()
}
fn stop_propagation(&self) {
self.0.stop_propagation()
}
fn target(&self) {
// self.0.target()
}
fn time_stamp(&self) -> f64 {
self.0.time_stamp()
}
}
impl UIEventInner for WebsysGenericUiEvent {
fn detail(&self) -> i32 {
todo!()
}
}
impl SelectionEventInner for WebsysGenericUiEvent {}
pub struct WebsysFocusEvent(pub web_sys::FocusEvent);
impl FocusEventInner for WebsysFocusEvent {}
pub struct WebsysFormEvent(pub web_sys::InputEvent);
impl FormEventInner for WebsysFormEvent {
// technically a controlled component, so we need to manually grab out the target data
fn value(&self) -> String {
let this: web_sys::EventTarget = self.0.target().unwrap();
(&this)
.dyn_ref()
.map(|input: &web_sys::HtmlInputElement| input.value())
.or_else(|| {
this
.dyn_ref()
.map(|input: &web_sys::HtmlTextAreaElement| input.value())
})
.or_else(|| {
this
.dyn_ref::<web_sys::HtmlElement>()
.unwrap()
.text_content()
})
.expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener")
}
}
pub struct WebsysMouseEvent(pub web_sys::MouseEvent);
impl MouseEventInner for WebsysMouseEvent {
fn alt_key(&self) -> bool {
self.0.alt_key()
}
fn button(&self) -> i16 {
self.0.button()
}
fn buttons(&self) -> u16 {
self.0.buttons()
}
fn client_x(&self) -> i32 {
self.0.client_x()
}
fn client_y(&self) -> i32 {
self.0.client_y()
}
fn ctrl_key(&self) -> bool {
self.0.ctrl_key()
}
fn meta_key(&self) -> bool {
self.0.meta_key()
}
fn page_x(&self) -> i32 {
self.0.page_x()
}
fn page_y(&self) -> i32 {
self.0.page_y()
}
fn screen_x(&self) -> i32 {
self.0.screen_x()
}
fn screen_y(&self) -> i32 {
self.0.screen_y()
}
fn shift_key(&self) -> bool {
self.0.shift_key()
}
// yikes
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
fn get_modifier_state(&self, key_code: &str) -> bool {
self.0.get_modifier_state(key_code)
}
}
pub struct WebsysPointerEvent(pub web_sys::PointerEvent);
impl PointerEventInner for WebsysPointerEvent {
fn alt_key(&self) -> bool {
self.0.alt_key()
}
fn button(&self) -> i16 {
self.0.button()
}
fn buttons(&self) -> u16 {
self.0.buttons()
}
fn client_x(&self) -> i32 {
self.0.client_x()
}
fn client_y(&self) -> i32 {
self.0.client_y()
}
fn ctrl_key(&self) -> bool {
self.0.ctrl_key()
}
fn meta_key(&self) -> bool {
self.0.meta_key()
}
fn page_x(&self) -> i32 {
self.0.page_x()
}
fn page_y(&self) -> i32 {
self.0.page_y()
}
fn screen_x(&self) -> i32 {
self.0.screen_x()
}
fn screen_y(&self) -> i32 {
self.0.screen_y()
}
fn shift_key(&self) -> bool {
self.0.shift_key()
}
// yikes
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
fn get_modifier_state(&self, key_code: &str) -> bool {
self.0.get_modifier_state(key_code)
}
fn pointer_id(&self) -> i32 {
self.0.pointer_id()
}
fn width(&self) -> i32 {
self.0.width()
}
fn height(&self) -> i32 {
self.0.height()
}
fn pressure(&self) -> f32 {
self.0.pressure()
}
fn tangential_pressure(&self) -> f32 {
self.0.tangential_pressure()
}
fn tilt_x(&self) -> i32 {
self.0.tilt_x()
}
fn tilt_y(&self) -> i32 {
self.0.tilt_y()
}
fn twist(&self) -> i32 {
self.0.twist()
}
fn pointer_type(&self) -> String {
self.0.pointer_type()
}
fn is_primary(&self) -> bool {
self.0.is_primary()
}
}
pub struct WebsysWheelEvent(pub web_sys::WheelEvent);
impl WheelEventInner for WebsysWheelEvent {
fn delta_mode(&self) -> u32 {
self.0.delta_mode()
}
fn delta_x(&self) -> f64 {
self.0.delta_x()
}
fn delta_y(&self) -> f64 {
self.0.delta_y()
}
fn delta_z(&self) -> f64 {
self.0.delta_z()
}
}
pub struct WebsysAnimationEvent(pub web_sys::AnimationEvent);
impl AnimationEventInner for WebsysAnimationEvent {
fn animation_name(&self) -> String {
self.0.animation_name()
}
fn pseudo_element(&self) -> String {
self.0.pseudo_element()
}
fn elapsed_time(&self) -> f32 {
self.0.elapsed_time()
}
}
pub struct WebsysTransitionEvent(pub web_sys::TransitionEvent);
impl TransitionEventInner for WebsysTransitionEvent {
fn property_name(&self) -> String {
self.0.property_name()
}
fn pseudo_element(&self) -> String {
self.0.pseudo_element()
}
fn elapsed_time(&self) -> f32 {
self.0.elapsed_time()
}
}
pub struct WebsysTouchEvent(pub web_sys::TouchEvent);
impl TouchEventInner for WebsysTouchEvent {
fn alt_key(&self) -> bool {
self.0.alt_key()
}
fn ctrl_key(&self) -> bool {
self.0.ctrl_key()
}
fn meta_key(&self) -> bool {
self.0.meta_key()
}
fn shift_key(&self) -> bool {
self.0.shift_key()
}
fn get_modifier_state(&self, key_code: &str) -> bool {
if cfg!(debug_assertions) {
todo!("get_modifier_state is not currently supported for touch events");
} else {
false
}
}
}
pub struct WebsysMediaEvent(pub web_sys::UiEvent);
impl MediaEventInner for WebsysMediaEvent {}
pub struct WebsysToggleEvent(pub web_sys::UiEvent);
impl ToggleEventInner for WebsysToggleEvent {}

View file

@ -1,49 +1,73 @@
//! Dioxus WebSys
//! --------------
//! This crate implements a renderer of the Dioxus Virtual DOM for the web browser using Websys.
/*
From Google's guide on rAF and rIC:
--------
If the callback is fired at the end of the frame, it will be scheduled to go after the current frame has been committed,
which means that style changes will have been applied, and, importantly, layout calculated. If we make DOM changes inside
of the idle callback, those layout calculations will be invalidated. If there are any kind of layout reads in the next
frame, e.g. getBoundingClientRect, clientWidth, etc, the browser will have to perform a Forced Synchronous Layout,
which is a potential performance bottleneck.
Another reason not trigger DOM changes in the idle callback is that the time impact of changing the DOM is unpredictable,
and as such we could easily go past the deadline the browser provided.
The best practice is to only make DOM changes inside of a requestAnimationFrame callback, since it is scheduled by the
browser with that type of work in mind. That means that our code will need to use a document fragment, which can then
be appended in the next requestAnimationFrame callback. If you are using a VDOM library, you would use requestIdleCallback
to make changes, but you would apply the DOM patches in the next requestAnimationFrame callback, not the idle callback.
Essentially:
------------
- Do the VDOM work during the idlecallback
- Do DOM work in the next requestAnimationFrame callback
*/
//!
//! ## Overview
//! ------------
//! This crate implements a renderer of the Dioxus Virtual DOM for the web browser using WebSys. This web render for
//! Dioxus is one of the more advanced renderers, supporting:
//! - idle work
//! - animations
//! - jank-free rendering
//! - noderefs
//! - controlled components
//! - re-hydration
//! - and more.
//!
//! The actual implementation is farily thin, with the heavy lifting happening inside the Dioxus Core crate.
//!
//! To purview the examples, check of the root Dioxus crate - the examples in this crate are mostly meant to provide
//! validation of websys-specific features and not the general use of Dioxus.
//!
//! ## RequestAnimationFrame and RequestIdleCallback
//! ------------------------------------------------
//! React implements "jank free rendering" by deliberately not blocking the browser's main thread. For large diffs, long
//! running work, and integration with things like React-Three-Fiber, it's extremeley important to avoid blocking the
//! main thread.
//!
//! React solves this problem by breaking up the rendering process into a "diff" phase and a "render" phase. In Dioxus,
//! the diff phase is non-blocking, using "yield_now" to allow the browser to process other events. When the diff phase
//! is finally complete, the VirtualDOM will return a set of "Mutations" for this crate to apply.
//!
//! Here, we schedule the "diff" phase during the browser's idle period, achieved by calling RequestIdleCallback and then
//! setting a timeout from the that completes when the idleperiod is over. Then, we call requestAnimationFrame
//!
//! From Google's guide on rAF and rIC:
//! -----------------------------------
//!
//! If the callback is fired at the end of the frame, it will be scheduled to go after the current frame has been committed,
//! which means that style changes will have been applied, and, importantly, layout calculated. If we make DOM changes inside
//! of the idle callback, those layout calculations will be invalidated. If there are any kind of layout reads in the next
//! frame, e.g. getBoundingClientRect, clientWidth, etc, the browser will have to perform a Forced Synchronous Layout,
//! which is a potential performance bottleneck.
//!
//! Another reason not trigger DOM changes in the idle callback is that the time impact of changing the DOM is unpredictable,
//! and as such we could easily go past the deadline the browser provided.
//!
//! The best practice is to only make DOM changes inside of a requestAnimationFrame callback, since it is scheduled by the
//! browser with that type of work in mind. That means that our code will need to use a document fragment, which can then
//! be appended in the next requestAnimationFrame callback. If you are using a VDOM library, you would use requestIdleCallback
//! to make changes, but you would apply the DOM patches in the next requestAnimationFrame callback, not the idle callback.
//!
//! Essentially:
//! ------------
//! - Do the VDOM work during the idlecallback
//! - Do DOM work in the next requestAnimationFrame callback
use std::rc::Rc;
pub use crate::cfg::WebConfig;
use crate::dom::load_document;
use dioxus::prelude::{Context, Properties, VNode};
use cache::intern_cache;
use dioxus::prelude::Properties;
use dioxus::virtual_dom::VirtualDom;
pub use dioxus_core as dioxus;
use dioxus_core::error::Result;
use dioxus_core::{events::EventTrigger, prelude::FC};
use futures_util::{pin_mut, Stream, StreamExt};
use fxhash::FxHashMap;
use js_sys::Iterator;
use web_sys::{window, Document, Element, Event, Node, NodeList};
use dioxus_core::prelude::FC;
mod cache;
mod cfg;
mod dom;
mod events;
mod nodeslab;
mod ric_raf;
/// Launches the VirtualDOM from the specified component function.
///
@ -73,16 +97,8 @@ where
F: FnOnce(WebConfig) -> WebConfig,
{
let config = config(WebConfig::default());
let fut = run_with_props(root, root_props, config);
wasm_bindgen_futures::spawn_local(async {
match fut.await {
Ok(_) => log::error!("Your app completed running... somehow?"),
Err(e) => log::error!("Your app crashed! {}", e),
}
});
wasm_bindgen_futures::spawn_local(run_with_props(root, root_props, config));
}
/// This method is the primary entrypoint for Websys Dioxus apps. Will panic if an error occurs while rendering.
/// See DioxusErrors for more information on how these errors could occour.
///
@ -96,43 +112,38 @@ where
///
/// Run the app to completion, panicing if any error occurs while rendering.
/// Pairs well with the wasm_bindgen async handler
pub async fn run_with_props<T: Properties + 'static>(
root: FC<T>,
root_props: T,
cfg: WebConfig,
) -> Result<()> {
pub async fn run_with_props<T: Properties + 'static>(root: FC<T>, root_props: T, cfg: WebConfig) {
let mut dom = VirtualDom::new_with_props(root, root_props);
intern_cache();
let hydrating = cfg.hydrate;
let root_el = load_document().get_element_by_id(&cfg.rootname).unwrap();
let tasks = dom.get_event_sender();
let sender_callback = Rc::new(move |event| tasks.unbounded_send(event).unwrap());
let mut real = RealDomWebsys {};
let mut websys_dom = dom::WebsysDom::new(root_el, cfg, sender_callback);
// initialize the virtualdom first
if cfg.hydrate {
dom.rebuild_in_place()?;
let mut mutations = dom.rebuild();
// hydrating is simply running the dom for a single render. If the page is already written, then the corresponding
// ElementIds should already line up because the web_sys dom has already loaded elements with the DioxusID into memory
if !hydrating {
websys_dom.process_edits(&mut mutations.edits);
}
let mut websys_dom = dom::WebsysDom::new(
root_el,
cfg,
Rc::new(move |event| tasks.unbounded_send(event).unwrap()),
);
let work_loop = ric_raf::RafLoop::new();
loop {
// if virtualdom has nothing, wait for it to have something before requesting idle time
if !dom.has_work() {
dom.wait_for_any_work().await;
}
dom.run_with_deadline(&mut websys_dom).await?;
let deadline = work_loop.wait_for_idle_time().await;
Ok(())
}
struct HydrationNode {
id: usize,
node: Node,
}
struct RealDomWebsys {}
impl dioxus::RealDom for RealDomWebsys {
fn raw_node_as_any(&self) -> &mut dyn std::any::Any {
todo!()
let mut mutations = dom.run_with_deadline(deadline).await;
work_loop.wait_for_raf().await;
websys_dom.process_edits(&mut mutations[0].edits);
}
}

View file

@ -0,0 +1,58 @@
//! RequestAnimationFrame and RequestIdleCallback port and polyfill.
use gloo_timers::future::TimeoutFuture;
use js_sys::Function;
use wasm_bindgen::JsCast;
use wasm_bindgen::{prelude::Closure, JsValue};
use web_sys::Window;
pub struct RafLoop {
window: Window,
ric_receiver: async_channel::Receiver<()>,
raf_receiver: async_channel::Receiver<()>,
ric_closure: Closure<dyn Fn(JsValue)>,
raf_closure: Closure<dyn Fn(JsValue)>,
}
impl RafLoop {
pub fn new() -> Self {
let (raf_sender, raf_receiver) = async_channel::unbounded();
let raf_closure: Closure<dyn Fn(JsValue)> = Closure::wrap(Box::new(move |_v: JsValue| {
raf_sender.try_send(()).unwrap()
}));
let (ric_sender, ric_receiver) = async_channel::unbounded();
let ric_closure: Closure<dyn Fn(JsValue)> = Closure::wrap(Box::new(move |_v: JsValue| {
ric_sender.try_send(()).unwrap()
}));
// execute the polyfill for safari
Function::new_no_args(include_str!("./ricpolyfill.js"))
.call0(&JsValue::NULL)
.unwrap();
Self {
window: web_sys::window().unwrap(),
raf_receiver,
raf_closure,
ric_receiver,
ric_closure,
}
}
/// waits for some idle time and returns a timeout future that expires after the idle time has passed
pub async fn wait_for_idle_time(&self) -> TimeoutFuture {
let ric_fn = self.ric_closure.as_ref().dyn_ref::<Function>().unwrap();
let deadline: u32 = self.window.request_idle_callback(ric_fn).unwrap();
self.ric_receiver.recv().await.unwrap();
let deadline = TimeoutFuture::new(deadline);
deadline
}
pub async fn wait_for_raf(&self) {
let raf_fn = self.raf_closure.as_ref().dyn_ref::<Function>().unwrap();
let _id: i32 = self.window.request_animation_frame(raf_fn).unwrap();
self.raf_receiver.recv().await.unwrap();
}
}

View file

@ -0,0 +1,28 @@
const requestIdleCallback =
(typeof self !== 'undefined' &&
self.requestIdleCallback &&
self.requestIdleCallback.bind(window)) ||
function (cb) {
const start = Date.now();
return setTimeout(() => {
cb({
didTimeout: false,
timeRemaining: function () {
return Math.max(0, 50 - (Date.now() - start));
},
});
}, 1);
};
const cancelIdleCallback =
(typeof self !== 'undefined' &&
self.cancelIdleCallback &&
self.cancelIdleCallback.bind(window)) ||
function (id) {
return clearTimeout(id);
};
if (typeof window !== 'undefined') {
window.requestIdleCallback = requestIdleCallback;
window.cancelIdleCallback = cancelIdleCallback;
}