Merge branch 'master' into pr/Dangerised/1902

This commit is contained in:
Evan Almloff committed 2024-02-08 09:41:08 -06:00
commit 65d4f922b4
527 files changed
+17438 -20902

No files matched your search

@@ -1,4 +1,4 @@
fn it_works() {
cx.render(rsx!({()}))
rsx!({()})
}
+4 -4
View File
@@ -5,8 +5,8 @@ pub fn Explainer<'a>(
cx: Scope<'a>,
invert: bool,
title: &'static str,
content: Element<'a>,
flasher: Element<'a>,
content: Element,
flasher: Element,
) -> Element {
// pt-5 sm:pt-24 lg:pt-24
@@ -32,10 +32,10 @@ pub fn Explainer<'a>(
std::mem::swap(&mut left, &mut right);
}
cx.render(rsx! {
rsx! {
div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light",
{left},
{right}
}
})
}
}
@@ -1,4 +1,4 @@
fn SaveClipboard(cx: Scope) -> Element {
fn SaveClipboard() -> Element {
rsx! {
div { class: "relative w-1/2 {align} max-w-md leading-8",
h2 { class: "mb-6 text-3xl leading-tight md:text-4xl md:leading-tight lg:text-3xl lg:leading-tight font-heading font-mono font-bold",
@@ -7,7 +7,7 @@ fn SaveClipboard(cx: Scope) -> Element {
}
};
cx.render(rsx! {
rsx! {
div { "hello world", "hello world", "hello world" }
})
}
}
@@ -1,5 +1,5 @@
pub static Icon3: Component<()> = |cx| {
cx.render(rsx! {
rsx! {
svg {
class: "w-6 h-6",
stroke_linecap: "round",
@@ -11,5 +11,5 @@ pub static Icon3: Component<()> = |cx| {
path { d: "M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2" }
circle { cx: "12", cy: "7", r: "4" }
}
})
}
};
@@ -1,7 +1,7 @@
fn it_works() {
cx.render(rsx! {
rsx! {
div {
span { "Description: ", {package.description.as_deref().unwrap_or("❌❌❌❌ missing")} }
}
})
}
}
+2 -2
View File
@@ -1,3 +1,3 @@
fn app(cx: Scope) -> Element {
cx.render(rsx! { div { "hello world" } })
fn app() -> Element {
rsx! { div { "hello world" } }
}
@@ -1,5 +1,5 @@
fn app(cx: Scope) -> Element {
cx.render(rsx! {
fn app() -> Element {
rsx! {
div {"hello world" }
})
}
}
+2 -2
View File
@@ -1,3 +1,3 @@
fn app(cx: Scope) -> Element {
cx.render(rsx! { div { "hello world" } })
fn app() -> Element {
rsx! { div { "hello world" } }
}
@@ -1,5 +1,5 @@
fn app(cx: Scope) -> Element {
cx.render(rsx! {
fn app() -> Element {
rsx! {
div {"hello world" }
})
}
}
@@ -1,8 +1,8 @@
fn ItWroks() {
cx.render(rsx! {
rsx! {
div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light",
{left},
{right}
}
})
}
}
@@ -1,5 +1,5 @@
fn ItWroks() {
cx.render(rsx! {
rsx! {
div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", {left}, {right} }
})
}
}
@@ -1,8 +1,8 @@
fn ItWroks() {
cx.render(rsx! {
rsx! {
div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light",
{left},
{right}
}
})
}
}
@@ -1,5 +1,5 @@
fn ItWroks() {
cx.render(rsx! {
rsx! {
div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", {left}, {right} }
})
}
}
+52 -52
View File
@@ -279,7 +279,7 @@ mod tests {
#[test]
fn test_no_hooks() {
let contents = indoc! {r#"
fn App(cx: Scope) -> Element {
fn App() -> Element {
rsx! {
p { "Hello World" }
}
@@ -294,8 +294,8 @@ mod tests {
#[test]
fn test_hook_correctly_used_inside_component() {
let contents = indoc! {r#"
fn App(cx: Scope) -> Element {
let count = use_state(cx, || 0);
fn App() -> Element {
let count = use_signal(|| 0);
rsx! {
p { "Hello World: {count}" }
}
@@ -310,8 +310,8 @@ mod tests {
#[test]
fn test_hook_correctly_used_inside_hook_fn() {
let contents = indoc! {r#"
fn use_thing(cx: Scope) -> UseState<i32> {
use_state(cx, || 0)
fn use_thing() -> UseState<i32> {
use_signal(|| 0)
}
"#};
@@ -323,9 +323,9 @@ mod tests {
#[test]
fn test_hook_correctly_used_inside_hook_closure() {
let contents = indoc! {r#"
fn App(cx: Scope) -> Element {
fn App() -> Element {
let use_thing = || {
use_state(cx, || 0)
use_signal(|| 0)
};
let count = use_thing();
rsx! {
@@ -342,9 +342,9 @@ mod tests {
#[test]
fn test_conditional_hook_if() {
let contents = indoc! {r#"
fn App(cx: Scope) -> Element {
fn App() -> Element {
if you_are_happy && you_know_it {
let something = use_state(cx, || "hands");
let something = use_signal(|| "hands");
println!("clap your {something}")
}
}
@@ -357,18 +357,18 @@ mod tests {
vec![Issue::HookInsideConditional(
HookInfo::new(
Span::new_from_str(
r#"use_state(cx, || "hands")"#,
r#"use_signal(|| "hands")"#,
LineColumn { line: 3, column: 24 },
),
Span::new_from_str(
r#"use_state"#,
r#"use_signal"#,
LineColumn { line: 3, column: 24 },
),
"use_state".to_string()
"use_signal".to_string()
),
ConditionalInfo::If(IfInfo::new(
Span::new_from_str(
"if you_are_happy && you_know_it {\n let something = use_state(cx, || \"hands\");\n println!(\"clap your {something}\")\n }",
"if you_are_happy && you_know_it {\n let something = use_signal(|| \"hands\");\n println!(\"clap your {something}\")\n }",
LineColumn { line: 2, column: 4 },
),
Span::new_from_str(
@@ -383,10 +383,10 @@ mod tests {
#[test]
fn test_conditional_hook_match() {
let contents = indoc! {r#"
fn App(cx: Scope) -> Element {
fn App() -> Element {
match you_are_happy && you_know_it {
true => {
let something = use_state(cx, || "hands");
let something = use_signal(|| "hands");
println!("clap your {something}")
}
false => {}
@@ -400,13 +400,13 @@ mod tests {
report.issues,
vec![Issue::HookInsideConditional(
HookInfo::new(
Span::new_from_str(r#"use_state(cx, || "hands")"#, LineColumn { line: 4, column: 28 }),
Span::new_from_str(r#"use_state"#, LineColumn { line: 4, column: 28 }),
"use_state".to_string()
Span::new_from_str(r#"use_signal(|| "hands")"#, LineColumn { line: 4, column: 28 }),
Span::new_from_str(r#"use_signal"#, LineColumn { line: 4, column: 28 }),
"use_signal".to_string()
),
ConditionalInfo::Match(MatchInfo::new(
Span::new_from_str(
"match you_are_happy && you_know_it {\n true => {\n let something = use_state(cx, || \"hands\");\n println!(\"clap your {something}\")\n }\n false => {}\n }",
"match you_are_happy && you_know_it {\n true => {\n let something = use_signal(|| \"hands\");\n println!(\"clap your {something}\")\n }\n false => {}\n }",
LineColumn { line: 2, column: 4 },
),
Span::new_from_str("match you_are_happy && you_know_it", LineColumn { line: 2, column: 4 })
@@ -418,9 +418,9 @@ mod tests {
#[test]
fn test_for_loop_hook() {
let contents = indoc! {r#"
fn App(cx: Scope) -> Element {
fn App() -> Element {
for _name in &names {
let is_selected = use_state(cx, || false);
let is_selected = use_signal(|| false);
println!("selected: {is_selected}");
}
}
@@ -433,18 +433,18 @@ mod tests {
vec![Issue::HookInsideLoop(
HookInfo::new(
Span::new_from_str(
"use_state(cx, || false)",
"use_signal(|| false)",
LineColumn { line: 3, column: 26 },
),
Span::new_from_str(
"use_state",
"use_signal",
LineColumn { line: 3, column: 26 },
),
"use_state".to_string()
"use_signal".to_string()
),
AnyLoopInfo::For(ForInfo::new(
Span::new_from_str(
"for _name in &names {\n let is_selected = use_state(cx, || false);\n println!(\"selected: {is_selected}\");\n }",
"for _name in &names {\n let is_selected = use_signal(|| false);\n println!(\"selected: {is_selected}\");\n }",
LineColumn { line: 2, column: 4 },
),
Span::new_from_str(
@@ -459,9 +459,9 @@ mod tests {
#[test]
fn test_while_loop_hook() {
let contents = indoc! {r#"
fn App(cx: Scope) -> Element {
fn App() -> Element {
while true {
let something = use_state(cx, || "hands");
let something = use_signal(|| "hands");
println!("clap your {something}")
}
}
@@ -474,18 +474,18 @@ mod tests {
vec![Issue::HookInsideLoop(
HookInfo::new(
Span::new_from_str(
r#"use_state(cx, || "hands")"#,
r#"use_signal(|| "hands")"#,
LineColumn { line: 3, column: 24 },
),
Span::new_from_str(
"use_state",
"use_signal",
LineColumn { line: 3, column: 24 },
),
"use_state".to_string()
"use_signal".to_string()
),
AnyLoopInfo::While(WhileInfo::new(
Span::new_from_str(
"while true {\n let something = use_state(cx, || \"hands\");\n println!(\"clap your {something}\")\n }",
"while true {\n let something = use_signal(|| \"hands\");\n println!(\"clap your {something}\")\n }",
LineColumn { line: 2, column: 4 },
),
Span::new_from_str(
@@ -500,9 +500,9 @@ mod tests {
#[test]
fn test_loop_hook() {
let contents = indoc! {r#"
fn App(cx: Scope) -> Element {
fn App() -> Element {
loop {
let something = use_state(cx, || "hands");
let something = use_signal(|| "hands");
println!("clap your {something}")
}
}
@@ -515,17 +515,17 @@ mod tests {
vec![Issue::HookInsideLoop(
HookInfo::new(
Span::new_from_str(
r#"use_state(cx, || "hands")"#,
r#"use_signal(|| "hands")"#,
LineColumn { line: 3, column: 24 },
),
Span::new_from_str(
"use_state",
"use_signal",
LineColumn { line: 3, column: 24 },
),
"use_state".to_string()
"use_signal".to_string()
),
AnyLoopInfo::Loop(LoopInfo::new(Span::new_from_str(
"loop {\n let something = use_state(cx, || \"hands\");\n println!(\"clap your {something}\")\n }",
"loop {\n let something = use_signal(|| \"hands\");\n println!(\"clap your {something}\")\n }",
LineColumn { line: 2, column: 4 },
)))
)],
@@ -535,8 +535,8 @@ mod tests {
#[test]
fn test_conditional_okay() {
let contents = indoc! {r#"
fn App(cx: Scope) -> Element {
let something = use_state(cx, || "hands");
fn App() -> Element {
let something = use_signal(|| "hands");
if you_are_happy && you_know_it {
println!("clap your {something}")
}
@@ -551,9 +551,9 @@ mod tests {
#[test]
fn test_closure_hook() {
let contents = indoc! {r#"
fn App(cx: Scope) -> Element {
fn App() -> Element {
let _a = || {
let b = use_state(cx, || 0);
let b = use_signal(|| 0);
b.get()
};
}
@@ -566,23 +566,23 @@ mod tests {
vec![Issue::HookInsideClosure(
HookInfo::new(
Span::new_from_str(
"use_state(cx, || 0)",
"use_signal(|| 0)",
LineColumn {
line: 3,
column: 16
},
),
Span::new_from_str(
"use_state",
"use_signal",
LineColumn {
line: 3,
column: 16
},
),
"use_state".to_string()
"use_signal".to_string()
),
ClosureInfo::new(Span::new_from_str(
"|| {\n let b = use_state(cx, || 0);\n b.get()\n }",
"|| {\n let b = use_signal(|| 0);\n b.get()\n }",
LineColumn {
line: 2,
column: 13
@@ -595,8 +595,8 @@ mod tests {
#[test]
fn test_hook_outside_component() {
let contents = indoc! {r#"
fn not_component_or_hook(cx: Scope) {
let _a = use_state(cx, || 0);
fn not_component_or_hook() {
let _a = use_signal(|| 0);
}
"#};
@@ -606,20 +606,20 @@ mod tests {
report.issues,
vec![Issue::HookOutsideComponent(HookInfo::new(
Span::new_from_str(
"use_state(cx, || 0)",
"use_signal(|| 0)",
LineColumn {
line: 2,
column: 13
}
),
Span::new_from_str(
"use_state",
"use_signal",
LineColumn {
line: 2,
column: 13
},
),
"use_state".to_string()
"use_signal".to_string()
))]
);
}
@@ -627,8 +627,8 @@ mod tests {
#[test]
fn test_hook_inside_hook() {
let contents = indoc! {r#"
fn use_thing(cx: Scope) {
let _a = use_state(cx, || 0);
fn use_thing() {
let _a = use_signal(|| 0);
}
"#};
+35 -35
View File
@@ -230,9 +230,9 @@ mod tests {
let issue_report = check_file(
"src/main.rs".into(),
indoc! {r#"
fn App(cx: Scope) -> Element {
fn App() -> Element {
if you_are_happy && you_know_it {
let something = use_state(cx, || "hands");
let something = use_signal(|| "hands");
println!("clap your {something}")
}
}
@@ -240,11 +240,11 @@ mod tests {
);
let expected = indoc! {r#"
error: hook called conditionally: `use_state` (inside `if`)
error: hook called conditionally: `use_signal` (inside `if`)
--> src/main.rs:3:25
|
3 | let something = use_state(cx, || "hands");
| ^^^^^^^^^
3 | let something = use_signal(|| "hands");
| ^^^^^^^^^^
|
= note: `if you_are_happy && you_know_it { … }` is the conditional
"#};
@@ -258,10 +258,10 @@ mod tests {
let issue_report = check_file(
"src/main.rs".into(),
indoc! {r#"
fn App(cx: Scope) -> Element {
fn App() -> Element {
match you_are_happy && you_know_it {
true => {
let something = use_state(cx, || "hands");
let something = use_signal(|| "hands");
println!("clap your {something}")
}
_ => {}
@@ -271,11 +271,11 @@ mod tests {
);
let expected = indoc! {r#"
error: hook called conditionally: `use_state` (inside `match`)
error: hook called conditionally: `use_signal` (inside `match`)
--> src/main.rs:4:29
|
4 | let something = use_state(cx, || "hands");
| ^^^^^^^^^
4 | let something = use_signal(|| "hands");
| ^^^^^^^^^^
|
= note: `match you_are_happy && you_know_it { … }` is the conditional
"#};
@@ -289,9 +289,9 @@ mod tests {
let issue_report = check_file(
"src/main.rs".into(),
indoc! {r#"
fn App(cx: Scope) -> Element {
fn App() -> Element {
for i in 0..10 {
let something = use_state(cx, || "hands");
let something = use_signal(|| "hands");
println!("clap your {something}")
}
}
@@ -299,11 +299,11 @@ mod tests {
);
let expected = indoc! {r#"
error: hook called in a loop: `use_state` (inside `for` loop)
error: hook called in a loop: `use_signal` (inside `for` loop)
--> src/main.rs:3:25
|
3 | let something = use_state(cx, || "hands");
| ^^^^^^^^^
3 | let something = use_signal(|| "hands");
| ^^^^^^^^^^
|
= note: `for i in 0..10 { … }` is the loop
"#};
@@ -317,9 +317,9 @@ mod tests {
let issue_report = check_file(
"src/main.rs".into(),
indoc! {r#"
fn App(cx: Scope) -> Element {
fn App() -> Element {
while check_thing() {
let something = use_state(cx, || "hands");
let something = use_signal(|| "hands");
println!("clap your {something}")
}
}
@@ -327,11 +327,11 @@ mod tests {
);
let expected = indoc! {r#"
error: hook called in a loop: `use_state` (inside `while` loop)
error: hook called in a loop: `use_signal` (inside `while` loop)
--> src/main.rs:3:25
|
3 | let something = use_state(cx, || "hands");
| ^^^^^^^^^
3 | let something = use_signal(|| "hands");
| ^^^^^^^^^^
|
= note: `while check_thing() { … }` is the loop
"#};
@@ -345,9 +345,9 @@ mod tests {
let issue_report = check_file(
"src/main.rs".into(),
indoc! {r#"
fn App(cx: Scope) -> Element {
fn App() -> Element {
loop {
let something = use_state(cx, || "hands");
let something = use_signal(|| "hands");
println!("clap your {something}")
}
}
@@ -355,11 +355,11 @@ mod tests {
);
let expected = indoc! {r#"
error: hook called in a loop: `use_state` (inside `loop`)
error: hook called in a loop: `use_signal` (inside `loop`)
--> src/main.rs:3:25
|
3 | let something = use_state(cx, || "hands");
| ^^^^^^^^^
3 | let something = use_signal(|| "hands");
| ^^^^^^^^^^
|
= note: `loop { … }` is the loop
"#};
@@ -373,9 +373,9 @@ mod tests {
let issue_report = check_file(
"src/main.rs".into(),
indoc! {r#"
fn App(cx: Scope) -> Element {
fn App() -> Element {
let something = || {
let something = use_state(cx, || "hands");
let something = use_signal(|| "hands");
println!("clap your {something}")
};
}
@@ -383,11 +383,11 @@ mod tests {
);
let expected = indoc! {r#"
error: hook called in a closure: `use_state`
error: hook called in a closure: `use_signal`
--> src/main.rs:3:25
|
3 | let something = use_state(cx, || "hands");
| ^^^^^^^^^
3 | let something = use_signal(|| "hands");
| ^^^^^^^^^^
"#};
assert_eq!(expected, issue_report.to_string());
@@ -399,9 +399,9 @@ mod tests {
let issue_report = check_file(
"src/main.rs".into(),
indoc! {r#"
fn App(cx: Scope) -> Element {
fn App() -> Element {
if you_are_happy && you_know_it {
let something = use_state(cx, || {
let something = use_signal(|| {
"hands"
});
println!("clap your {something}")
@@ -411,11 +411,11 @@ mod tests {
);
let expected = indoc! {r#"
error: hook called conditionally: `use_state` (inside `if`)
error: hook called conditionally: `use_signal` (inside `if`)
--> src/main.rs:3:25
|
3 | let something = use_state(cx, || {
| ^^^^^^^^^
3 | let something = use_signal(|| {
| ^^^^^^^^^^
4 | "hands"
5 | });
|
+1 -1
View File
@@ -3,7 +3,7 @@
pub struct HookInfo {
/// The name of the hook, e.g. `use_state`.
pub name: String,
/// The span of the hook, e.g. `use_state(cx, || 0)`.
/// The span of the hook, e.g. `use_signal(|| 0)`.
pub span: Span,
/// The span of the name, e.g. `use_state`.
pub name_span: Span,
+1 -1
View File
@@ -15,7 +15,7 @@ serde_json = "1.0.79"
toml = { version = "0.5.8", optional = true }
cargo_toml = { version = "0.16.0", optional = true }
once_cell = "1.18.0"
tracing.workspace = true
tracing = { workspace = true }
# bundling
tauri-bundler = { version = "=1.4.0", features = ["native-tls-vendored"], optional = true }
+22 -1
View File
@@ -392,7 +392,12 @@ impl CrateConfig {
None => manifest
.bin
.iter()
.find(|b| b.name == manifest.package.as_ref().map(|pkg| pkg.name.clone()))
.find(|b| {
#[allow(clippy::useless_asref)]
let matching_bin =
b.name == manifest.package.as_ref().map(|pkg| pkg.name.clone());
matching_bin
})
.or(manifest
.bin
.iter()
@@ -448,6 +453,22 @@ impl CrateConfig {
self.crate_dir.join(&self.dioxus_config.application.out_dir)
}
/// Compose an out directory for the fullstack platform. See `out_dir()`
/// method.
pub fn fullstack_out_dir(&self) -> PathBuf {
self.crate_dir.join(".dioxus")
}
/// Compose a target directory for the server (fullstack-only?).
pub fn server_target_dir(&self) -> PathBuf {
self.fullstack_out_dir().join("ssr")
}
/// Compose a target directory for the client (fullstack-only?).
pub fn client_target_dir(&self) -> PathBuf {
self.fullstack_out_dir().join("web")
}
pub fn as_example(&mut self, example_name: String) -> &mut Self {
self.executable = ExecutableType::Example(example_name);
self
+1 -1
View File
@@ -24,7 +24,7 @@ serde_json = "1.0.79"
toml = "0.8.8"
fs_extra = "1.2.0"
cargo_toml = "0.18.0"
futures-util.workspace = true
futures-util = { workspace = true }
notify = { version = "5.0.0-pre.16", features = ["serde"] }
html_parser = { workspace = true }
cargo_metadata = "0.18.1"
+3 -3
View File
@@ -37,9 +37,9 @@ pub(crate) fn process_assets(config: &CrateConfig, manifest: &AssetManifest) ->
}
/// A guard that sets up the environment for the web renderer to compile in. This guard sets the location that assets will be served from
pub(crate) struct WebAssetConfigDropGuard;
pub(crate) struct AssetConfigDropGuard;
impl WebAssetConfigDropGuard {
impl AssetConfigDropGuard {
pub fn new() -> Self {
// Set up the collect asset config
manganis_cli_support::Config::default()
@@ -49,7 +49,7 @@ impl WebAssetConfigDropGuard {
}
}
impl Drop for WebAssetConfigDropGuard {
impl Drop for AssetConfigDropGuard {
fn drop(&mut self) {
// Reset the config
manganis_cli_support::Config::default().save();
+51 -4
View File
@@ -1,5 +1,5 @@
use crate::{
assets::{asset_manifest, create_assets_head, process_assets, WebAssetConfigDropGuard},
assets::{asset_manifest, create_assets_head, process_assets, AssetConfigDropGuard},
error::{Error, Result},
tools::Tool,
};
@@ -11,6 +11,7 @@ use indicatif::{ProgressBar, ProgressStyle};
use lazy_static::lazy_static;
use manganis_cli_support::{AssetManifest, ManganisSupportGuard};
use std::{
env,
fs::{copy, create_dir_all, File},
io::Read,
panic,
@@ -30,7 +31,45 @@ pub struct BuildResult {
pub assets: Option<AssetManifest>,
}
pub fn build(config: &CrateConfig, _: bool, skip_assets: bool) -> Result<BuildResult> {
/// This trait is only created for the convenient and concise way to set
/// `RUSTFLAGS` environment variable for the `subprocess::Exec`.
pub trait ExecWithRustFlagsSetter {
fn set_rust_flags(self, rust_flags: Option<String>) -> Self;
}
impl ExecWithRustFlagsSetter for subprocess::Exec {
/// Sets (appends to, if already set) `RUSTFLAGS` environment variable if
/// `rust_flags` is not `None`.
fn set_rust_flags(self, rust_flags: Option<String>) -> Self {
if let Some(rust_flags) = rust_flags {
// Some `RUSTFLAGS` might be already set in the environment or provided
// by the user. They should take higher priority than the default flags.
// If no default flags are provided, then there is no point in
// redefining the environment variable with the same value, if it is
// even set. If no custom flags are set, then there is no point in
// adding the unnecessary whitespace to the command.
self.env(
"RUSTFLAGS",
if let Ok(custom_rust_flags) = env::var("RUSTFLAGS") {
rust_flags + " " + custom_rust_flags.as_str()
} else {
rust_flags
},
)
} else {
self
}
}
}
/// Build client (WASM).
/// Note: `rust_flags` argument is only used for the fullstack platform.
pub fn build(
config: &CrateConfig,
_: bool,
skip_assets: bool,
rust_flags: Option<String>,
) -> Result<BuildResult> {
// [1] Build the project with cargo, generating a wasm32-unknown-unknown target (is there a more specific, better target to leverage?)
// [2] Generate the appropriate build folders
// [3] Wasm-bindgen the .wasm file, and move it into the {builddir}/modules/xxxx/xxxx_bg.wasm
@@ -48,7 +87,7 @@ pub fn build(config: &CrateConfig, _: bool, skip_assets: bool) -> Result<BuildRe
let out_dir = config.out_dir();
let asset_dir = config.asset_dir();
let _guard = WebAssetConfigDropGuard::new();
let _guard = AssetConfigDropGuard::new();
let _manganis_support = ManganisSupportGuard::default();
// start to build the assets
@@ -72,6 +111,7 @@ pub fn build(config: &CrateConfig, _: bool, skip_assets: bool) -> Result<BuildRe
}
let cmd = subprocess::Exec::cmd("cargo")
.set_rust_flags(rust_flags)
.env("CARGO_TARGET_DIR", target_dir)
.cwd(crate_dir)
.arg("build")
@@ -79,6 +119,8 @@ pub fn build(config: &CrateConfig, _: bool, skip_assets: bool) -> Result<BuildRe
.arg("wasm32-unknown-unknown")
.arg("--message-format=json");
// TODO: make the initial variable mutable to simplify all the expressions
// below. Look inside the `build_desktop()` as an example.
let cmd = if config.release {
cmd.arg("--release")
} else {
@@ -283,10 +325,13 @@ pub fn build(config: &CrateConfig, _: bool, skip_assets: bool) -> Result<BuildRe
})
}
/// Note: `rust_flags` argument is only used for the fullstack platform
/// (server).
pub fn build_desktop(
config: &CrateConfig,
_is_serve: bool,
skip_assets: bool,
rust_flags: Option<String>,
) -> Result<BuildResult> {
log::info!("🚅 Running build [Desktop] command...");
@@ -294,8 +339,10 @@ pub fn build_desktop(
let ignore_files = build_assets(config)?;
let _guard = dioxus_cli_config::__private::save_config(config);
let _manganis_support = ManganisSupportGuard::default();
let _guard = AssetConfigDropGuard::new();
let mut cmd = subprocess::Exec::cmd("cargo")
.set_rust_flags(rust_flags)
.env("CARGO_TARGET_DIR", &config.target_dir)
.cwd(&config.crate_dir)
.arg("build")
@@ -489,7 +536,7 @@ fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result<Vec<Diagnostic>> {
}
pub fn gen_page(config: &CrateConfig, manifest: Option<&AssetManifest>, serve: bool) -> String {
let _gaurd = WebAssetConfigDropGuard::new();
let _guard = AssetConfigDropGuard::new();
let crate_root = crate_root().unwrap();
let custom_html_file = crate_root.join("index.html");
+4
View File
@@ -161,6 +161,10 @@ async fn autoformat_project(
return Ok(());
}
if files_to_format.is_empty() {
return Ok(());
}
let indent = indentation_for(&files_to_format[0], split_line_attributes)?;
let counts = files_to_format
+34 -19
View File
@@ -1,8 +1,7 @@
use crate::assets::WebAssetConfigDropGuard;
use crate::assets::AssetConfigDropGuard;
#[cfg(feature = "plugin")]
use crate::plugin::PluginManager;
use crate::server::fullstack::FullstackServerEnvGuard;
use crate::server::fullstack::FullstackWebEnvGuard;
use crate::server::fullstack;
use dioxus_cli_config::Platform;
use super::*;
@@ -16,7 +15,13 @@ pub struct Build {
}
impl Build {
pub fn build(self, bin: Option<PathBuf>, target_dir: Option<&std::path::Path>) -> Result<()> {
/// Note: `rust_flags` argument is only used for the fullstack platform.
pub fn build(
self,
bin: Option<PathBuf>,
target_dir: Option<&std::path::Path>,
rust_flags: Option<String>,
) -> Result<()> {
let mut crate_config = dioxus_cli_config::CrateConfig::new(bin)?;
if let Some(target_dir) = target_dir {
crate_config.target_dir = target_dir.to_path_buf();
@@ -53,16 +58,23 @@ impl Build {
// let _ = PluginManager::on_build_start(&crate_config, &platform);
let build_result = match platform {
Platform::Web => crate::builder::build(&crate_config, false, self.build.skip_assets)?,
Platform::Web => {
// `rust_flags` are used by fullstack's client build.
crate::builder::build(&crate_config, false, self.build.skip_assets, rust_flags)?
}
Platform::Desktop => {
crate::builder::build_desktop(&crate_config, false, self.build.skip_assets)?
// Since desktop platform doesn't use `rust_flags`, this
// argument is explicitly set to `None`.
crate::builder::build_desktop(&crate_config, false, self.build.skip_assets, None)?
}
Platform::Fullstack => {
// Fullstack mode must be built with web configs on the desktop (server) binary as well as the web binary
let _config = WebAssetConfigDropGuard::new();
// Fullstack mode must be built with web configs on the desktop
// (server) binary as well as the web binary
let _config = AssetConfigDropGuard::new();
let client_rust_flags = fullstack::client_rust_flags(&self.build);
let server_rust_flags = fullstack::server_rust_flags(&self.build);
{
let mut web_config = crate_config.clone();
let _gaurd = FullstackWebEnvGuard::new(&self.build);
let web_feature = self.build.client_feature;
let features = &mut web_config.features;
match features {
@@ -71,7 +83,12 @@ impl Build {
}
None => web_config.features = Some(vec![web_feature]),
};
crate::builder::build(&crate_config, false, self.build.skip_assets)?;
crate::builder::build(
&web_config,
false,
self.build.skip_assets,
Some(client_rust_flags),
)?;
}
{
let mut desktop_config = crate_config.clone();
@@ -83,21 +100,19 @@ impl Build {
}
None => desktop_config.features = Some(vec![desktop_feature]),
};
let _gaurd =
FullstackServerEnvGuard::new(self.build.force_debug, self.build.release);
crate::builder::build_desktop(&desktop_config, false, self.build.skip_assets)?
crate::builder::build_desktop(
&desktop_config,
false,
self.build.skip_assets,
Some(server_rust_flags),
)?
}
}
};
let temp = gen_page(&crate_config, build_result.assets.as_ref(), false);
let mut file = std::fs::File::create(
crate_config
.crate_dir
.join(crate_config.dioxus_config.application.out_dir.clone())
.join("index.html"),
)?;
let mut file = std::fs::File::create(crate_config.out_dir().join("index.html"))?;
file.write_all(temp.as_bytes())?;
// #[cfg(feature = "plugin")]
+3 -1
View File
@@ -84,7 +84,9 @@ impl Bundle {
crate_config.set_cargo_args(self.build.cargo_args);
// build the desktop app
build_desktop(&crate_config, false, false)?;
// Since the `bundle()` function is only run for the desktop platform,
// the `rust_flags` argument is set to `None`.
build_desktop(&crate_config, false, false, None)?;
// copy the binary to the out dir
let package = crate_config.manifest.package.as_ref().unwrap();
+6 -6
View File
@@ -6,7 +6,7 @@ use super::*;
#[derive(Clone, Debug, Default, Deserialize, Parser)]
pub struct ConfigOptsBuild {
/// Build in release mode [default: false]
#[clap(long)]
#[clap(long, short)]
#[serde(default)]
pub release: bool,
@@ -45,8 +45,8 @@ pub struct ConfigOptsBuild {
#[clap(long, default_value_t = { "web".to_string() })]
pub client_feature: String,
/// The feature to use for the server in a fullstack app [default: "ssr"]
#[clap(long, default_value_t = { "ssr".to_string() })]
/// The feature to use for the server in a fullstack app [default: "server"]
#[clap(long, default_value_t = { "server".to_string() })]
pub server_feature: String,
/// Rustc platform triple
@@ -142,8 +142,8 @@ pub struct ConfigOptsServe {
#[clap(long, default_value_t = { "web".to_string() })]
pub client_feature: String,
/// The feature to use for the server in a fullstack app [default: "ssr"]
#[clap(long, default_value_t = { "ssr".to_string() })]
/// The feature to use for the server in a fullstack app [default: "server"]
#[clap(long, default_value_t = { "server".to_string() })]
pub server_feature: String,
/// Rustc platform triple
@@ -159,7 +159,7 @@ pub struct ConfigOptsServe {
#[derive(Clone, Debug, Default, Deserialize, Parser)]
pub struct ConfigOptsBundle {
/// Build in release mode [default: false]
#[clap(long)]
#[clap(long, short)]
#[serde(default)]
pub release: bool,
+4 -4
View File
@@ -19,12 +19,12 @@ impl Clean {
return custom_error!("Cargo clean failed.");
}
let out_dir = crate_config.dioxus_config.application.out_dir;
if crate_config.crate_dir.join(&out_dir).is_dir() {
remove_dir_all(crate_config.crate_dir.join(&out_dir))?;
let out_dir = &crate_config.out_dir();
if out_dir.is_dir() {
remove_dir_all(out_dir)?;
}
let fullstack_out_dir = crate_config.crate_dir.join(".dioxus");
let fullstack_out_dir = crate_config.fullstack_out_dir();
if fullstack_out_dir.is_dir() {
remove_dir_all(fullstack_out_dir)?;
+1 -3
View File
@@ -73,9 +73,7 @@ impl Serve {
) -> anyhow::Result<()> {
let serve_html = gen_page(crate_config, manifest, true);
let dist_path = crate_config
.crate_dir
.join(crate_config.dioxus_config.application.out_dir.clone());
let dist_path = crate_config.out_dir();
if !dist_path.is_dir() {
create_dir_all(&dist_path)?;
}
+2 -2
View File
@@ -71,7 +71,7 @@ fn write_callbody_with_icon_section(mut callbody: CallBody) -> String {
}
fn write_component_body(raw: String) -> String {
let mut out = String::from("fn component(cx: Scope) -> Element {\n cx.render(rsx! {");
let mut out = String::from("fn component() -> Element {\n rsx! {");
indent_and_write(&raw, 1, &mut out);
out.push_str(" })\n}");
out
@@ -84,7 +84,7 @@ fn write_svg_section(out: &mut String, svgs: Vec<BodyNode>) {
let raw = dioxus_autofmt::write_block_out(CallBody { roots: vec![icon] }).unwrap();
out.push_str("\n\n pub fn icon_");
out.push_str(&idx.to_string());
out.push_str("(cx: Scope) -> Element {\n cx.render(rsx! {");
out.push_str("() -> Element {\n rsx! {");
indent_and_write(&raw, 2, out);
out.push_str(" })\n }");
}
+1 -1
View File
@@ -99,7 +99,7 @@ async fn main() -> anyhow::Result<()> {
match action {
Build(opts) => opts
.build(Some(bin.clone()), None)
.build(Some(bin.clone()), None, None)
.context(error_wrapper("Building project failed")),
Clean(opts) => opts
+2 -5
View File
@@ -4,14 +4,11 @@ use std::{
sync::Mutex,
};
use crate::tools::{app_path, clone_repo};
use dioxus_cli_config::CrateConfig;
use mlua::{Lua, Table};
use serde_json::json;
use crate::{
tools::{app_path, clone_repo},
CrateConfig,
};
use self::{
interface::{
command::PluginCommander, dirs::PluginDirs, fs::PluginFileSystem, log::PluginLogger,
+45 -8
View File
@@ -14,6 +14,7 @@ use dioxus_hot_reload::HotReloadMsg;
use dioxus_html::HtmlCtx;
use dioxus_rsx::hot_reload::*;
use interprocess_docfix::local_socket::LocalSocketListener;
use std::fs::create_dir_all;
use std::{
process::{Child, Command},
sync::{Arc, Mutex, RwLock},
@@ -21,7 +22,7 @@ use std::{
use tokio::sync::broadcast::{self};
#[cfg(feature = "plugin")]
use plugin::PluginManager;
use crate::plugin::PluginManager;
use super::HotReloadState;
@@ -109,6 +110,7 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
.exec()
.unwrap();
let target_dir = metadata.target_directory.as_std_path();
let _ = create_dir_all(target_dir); // `_all` is for good measure and future-proofness.
let path = target_dir.join("dioxusin");
clear_paths(&path);
match LocalSocketListener::bind(path) {
@@ -213,9 +215,14 @@ fn send_msg(msg: HotReloadMsg, channel: &mut impl std::io::Write) -> bool {
}
}
fn start_desktop(config: &CrateConfig, skip_assets: bool) -> Result<(RAIIChild, BuildResult)> {
fn start_desktop(
config: &CrateConfig,
skip_assets: bool,
rust_flags: Option<String>,
) -> Result<(RAIIChild, BuildResult)> {
// Run the desktop application
let result = crate::builder::build_desktop(config, true, skip_assets)?;
// Only used for the fullstack platform,
let result = crate::builder::build_desktop(config, true, skip_assets, rust_flags)?;
match &config.executable {
ExecutableType::Binary(name)
@@ -242,9 +249,15 @@ pub(crate) struct DesktopPlatform {
skip_assets: bool,
}
impl Platform for DesktopPlatform {
fn start(config: &CrateConfig, serve: &ConfigOptsServe) -> Result<Self> {
let (child, first_build_result) = start_desktop(config, serve.skip_assets)?;
impl DesktopPlatform {
/// `rust_flags` argument is added because it is used by the
/// `DesktopPlatform`'s implementation of the `Platform::start()`.
pub fn start_with_options(
config: &CrateConfig,
serve: &ConfigOptsServe,
rust_flags: Option<String>,
) -> Result<Self> {
let (child, first_build_result) = start_desktop(config, serve.skip_assets, rust_flags)?;
log::info!("🚀 Starting development server...");
@@ -265,14 +278,38 @@ impl Platform for DesktopPlatform {
})
}
fn rebuild(&mut self, config: &CrateConfig) -> Result<BuildResult> {
/// `rust_flags` argument is added because it is used by the
/// `DesktopPlatform`'s implementation of the `Platform::rebuild()`.
pub fn rebuild_with_options(
&mut self,
config: &CrateConfig,
rust_flags: Option<String>,
) -> Result<BuildResult> {
self.currently_running_child.0.kill()?;
let (child, result) = start_desktop(config, self.skip_assets)?;
let (child, result) = start_desktop(config, self.skip_assets, rust_flags)?;
self.currently_running_child = child;
Ok(result)
}
}
impl Platform for DesktopPlatform {
fn start(config: &CrateConfig, serve: &ConfigOptsServe) -> Result<Self> {
// See `start_with_options()`'s docs for the explanation why the code
// was moved there.
// Since desktop platform doesn't use `rust_flags`, this argument is
// explicitly set to `None`.
DesktopPlatform::start_with_options(config, serve, None)
}
fn rebuild(&mut self, config: &CrateConfig) -> Result<BuildResult> {
// See `rebuild_with_options()`'s docs for the explanation why the code
// was moved there.
// Since desktop platform doesn't use `rust_flags`, this argument is
// explicitly set to `None`.
DesktopPlatform::rebuild_with_options(self, config, None)
}
}
struct RAIIChild(Child);
impl Drop for RAIIChild {
+61 -97
View File
@@ -1,13 +1,38 @@
use dioxus_cli_config::CrateConfig;
use crate::{
assets::WebAssetConfigDropGuard,
cfg::{ConfigOptsBuild, ConfigOptsServe},
Result,
};
use super::{desktop, Platform};
static CLIENT_RUST_FLAGS: &str = "-C debuginfo=none -C strip=debuginfo";
// The `opt-level=2` increases build times, but can noticeably decrease time
// between saving changes and being able to interact with an app. The "overall"
// time difference (between having and not having the optimization) can be
// almost imperceptible (~1 s) but also can be very noticeable (~6 s) — depends
// on setup (hardware, OS, browser, idle load).
static SERVER_RUST_FLAGS: &str = "-C opt-level=2";
static DEBUG_RUST_FLAG: &str = "-C debug-assertions";
fn rust_flags(build: &ConfigOptsBuild, base_flags: &str) -> String {
let mut rust_flags = base_flags.to_string();
if !build.release {
rust_flags += " ";
rust_flags += DEBUG_RUST_FLAG;
};
rust_flags
}
pub fn client_rust_flags(build: &ConfigOptsBuild) -> String {
rust_flags(build, CLIENT_RUST_FLAGS)
}
pub fn server_rust_flags(build: &ConfigOptsBuild) -> String {
rust_flags(build, SERVER_RUST_FLAGS)
}
pub async fn startup(config: CrateConfig, serve: &ConfigOptsServe) -> Result<()> {
desktop::startup_with_platform::<FullstackPlatform>(config, serve).await
}
@@ -17,15 +42,29 @@ fn start_web_build_thread(
serve: &ConfigOptsServe,
) -> std::thread::JoinHandle<Result<()>> {
let serve = serve.clone();
let target_directory = config.crate_dir.join(".dioxus").join("web");
let target_directory = config.client_target_dir();
std::fs::create_dir_all(&target_directory).unwrap();
std::thread::spawn(move || build_web(serve, &target_directory))
}
fn make_desktop_config(config: &CrateConfig, serve: &ConfigOptsServe) -> CrateConfig {
let mut desktop_config = config.clone();
desktop_config.target_dir = config.server_target_dir();
let desktop_feature = serve.server_feature.clone();
let features = &mut desktop_config.features;
match features {
Some(features) => {
features.push(desktop_feature);
}
None => desktop_config.features = Some(vec![desktop_feature]),
};
desktop_config
}
struct FullstackPlatform {
serve: ConfigOptsServe,
desktop: desktop::DesktopPlatform,
_config: WebAssetConfigDropGuard,
server_rust_flags: String,
}
impl Platform for FullstackPlatform {
@@ -35,17 +74,13 @@ impl Platform for FullstackPlatform {
{
let thread_handle = start_web_build_thread(config, serve);
let mut desktop_config = config.clone();
let desktop_feature = serve.server_feature.clone();
let features = &mut desktop_config.features;
match features {
Some(features) => {
features.push(desktop_feature);
}
None => desktop_config.features = Some(vec![desktop_feature]),
};
let config = WebAssetConfigDropGuard::new();
let desktop = desktop::DesktopPlatform::start(&desktop_config, serve)?;
let desktop_config = make_desktop_config(config, serve);
let server_rust_flags = server_rust_flags(&serve.clone().into());
let desktop = desktop::DesktopPlatform::start_with_options(
&desktop_config,
serve,
Some(server_rust_flags.clone()),
)?;
thread_handle
.join()
.map_err(|_| anyhow::anyhow!("Failed to join thread"))??;
@@ -53,25 +88,16 @@ impl Platform for FullstackPlatform {
Ok(Self {
desktop,
serve: serve.clone(),
_config: config,
server_rust_flags,
})
}
fn rebuild(&mut self, crate_config: &CrateConfig) -> Result<crate::BuildResult> {
let thread_handle = start_web_build_thread(crate_config, &self.serve);
let result = {
let mut desktop_config = crate_config.clone();
let desktop_feature = self.serve.server_feature.clone();
let features = &mut desktop_config.features;
match features {
Some(features) => {
features.push(desktop_feature);
}
None => desktop_config.features = Some(vec![desktop_feature]),
};
let _gaurd = FullstackServerEnvGuard::new(self.serve.force_debug, self.serve.release);
self.desktop.rebuild(&desktop_config)
};
let desktop_config = make_desktop_config(crate_config, &self.serve);
let result = self
.desktop
.rebuild_with_options(&desktop_config, Some(self.server_rust_flags.clone()));
thread_handle
.join()
.map_err(|_| anyhow::anyhow!("Failed to join thread"))??;
@@ -91,74 +117,12 @@ fn build_web(serve: ConfigOptsServe, target_directory: &std::path::Path) -> Resu
};
web_config.platform = Some(dioxus_cli_config::Platform::Web);
let _gaurd = FullstackWebEnvGuard::new(&web_config);
crate::cli::build::Build { build: web_config }.build(None, Some(target_directory))
}
// Debug mode web builds have a very large size by default. If debug mode is not enabled, we strip some of the debug info by default
// This reduces a hello world from ~40MB to ~2MB
pub(crate) struct FullstackWebEnvGuard {
old_rustflags: Option<String>,
}
impl FullstackWebEnvGuard {
pub fn new(serve: &ConfigOptsBuild) -> Self {
Self {
old_rustflags: (!serve.force_debug).then(|| {
let old_rustflags = std::env::var("RUSTFLAGS").unwrap_or_default();
let debug_assertions = if serve.release {
""
} else {
" -C debug-assertions"
};
std::env::set_var(
"RUSTFLAGS",
format!(
"{old_rustflags} -C debuginfo=none -C strip=debuginfo{debug_assertions}"
),
);
old_rustflags
}),
}
}
}
impl Drop for FullstackWebEnvGuard {
fn drop(&mut self) {
if let Some(old_rustflags) = self.old_rustflags.take() {
std::env::set_var("RUSTFLAGS", old_rustflags);
}
}
}
// Debug mode web builds have a very large size by default. If debug mode is not enabled, we strip some of the debug info by default
// This reduces a hello world from ~40MB to ~2MB
pub(crate) struct FullstackServerEnvGuard {
old_rustflags: Option<String>,
}
impl FullstackServerEnvGuard {
pub fn new(debug: bool, release: bool) -> Self {
Self {
old_rustflags: (!debug).then(|| {
let old_rustflags = std::env::var("RUSTFLAGS").unwrap_or_default();
let debug_assertions = if release { "" } else { " -C debug-assertions" };
std::env::set_var(
"RUSTFLAGS",
format!("{old_rustflags} -C opt-level=2 {debug_assertions}"),
);
old_rustflags
}),
}
}
}
impl Drop for FullstackServerEnvGuard {
fn drop(&mut self) {
if let Some(old_rustflags) = self.old_rustflags.take() {
std::env::set_var("RUSTFLAGS", old_rustflags);
}
crate::cli::build::Build {
build: web_config.clone(),
}
.build(
None,
Some(target_directory),
Some(client_rust_flags(&web_config)),
)
}
+2 -2
View File
@@ -40,7 +40,7 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
if let Some(hot_reload) = &hot_reload {
// find changes to the rsx in the file
let mut rsx_file_map = hot_reload.file_map.lock().unwrap();
let mut messages: Vec<Template<'static>> = Vec::new();
let mut messages: Vec<Template> = Vec::new();
// In hot reload mode, we only need to rebuild if non-rsx code is changed
needs_full_rebuild = false;
@@ -153,6 +153,6 @@ pub(crate) trait Platform {
#[derive(Clone)]
pub struct HotReloadState {
pub messages: broadcast::Sender<Template<'static>>,
pub messages: broadcast::Sender<Template>,
pub file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
}
+15 -15
View File
@@ -39,7 +39,7 @@ use tower_http::{
};
#[cfg(feature = "plugin")]
use plugin::PluginManager;
use crate::plugin::PluginManager;
mod proxy;
@@ -109,7 +109,9 @@ pub async fn serve(
skip_assets: bool,
hot_reload_state: Option<HotReloadState>,
) -> Result<()> {
let first_build_result = crate::builder::build(&config, false, skip_assets)?;
// Since web platform doesn't use `rust_flags`, this argument is explicitly
// set to `None`.
let first_build_result = crate::builder::build(&config, false, skip_assets, None)?;
// generate dev-index page
Serve::regen_dev_page(&config, first_build_result.assets.as_ref())?;
@@ -159,10 +161,10 @@ pub async fn serve(
);
// Router
let router = setup_router(config, ws_reload_state, hot_reload_state).await?;
let router = setup_router(config.clone(), ws_reload_state, hot_reload_state).await?;
// Start server
start_server(port, router, start_browser, rustls_config).await?;
start_server(port, router, start_browser, rustls_config, &config).await?;
Ok(())
}
@@ -287,14 +289,9 @@ async fn setup_router(
{
let body = Full::from(
// TODO: Cache/memoize this.
std::fs::read_to_string(
file_service_config
.crate_dir
.join(file_service_config.out_dir())
.join("index.html"),
)
.ok()
.unwrap(),
std::fs::read_to_string(file_service_config.out_dir().join("index.html"))
.ok()
.unwrap(),
)
.map_err(|err| match err {})
.boxed();
@@ -315,7 +312,7 @@ async fn setup_router(
Ok(response)
},
)
.service(ServeDir::new(config.crate_dir.join(config.out_dir())));
.service(ServeDir::new(config.out_dir()));
// Setup websocket
let mut router = Router::new().route("/_dioxus/ws", get(ws_handler));
@@ -366,10 +363,11 @@ async fn start_server(
router: Router,
start_browser: bool,
rustls: Option<RustlsConfig>,
_config: &CrateConfig,
) -> Result<()> {
// If plugins, call on_serve_start event
#[cfg(feature = "plugin")]
PluginManager::on_serve_start(&config)?;
PluginManager::on_serve_start(_config)?;
// Parse address
let addr = format!("0.0.0.0:{}", port).parse().unwrap();
@@ -447,7 +445,9 @@ async fn ws_handler(
}
fn build(config: &CrateConfig, reload_tx: &Sender<()>, skip_assets: bool) -> Result<BuildResult> {
let result = builder::build(config, true, skip_assets)?;
// Since web platform doesn't use `rust_flags`, this argument is explicitly
// set to `None`.
let result = builder::build(config, true, skip_assets, None)?;
// change the websocket reload state to true;
// the page will auto-reload.
if config.dioxus_config.web.watcher.reload_html {
+27
View File
@@ -0,0 +1,27 @@
[package]
name = "dioxus-config-macro"
version = { workspace = true }
authors = ["Jonathan Kelley"]
edition = "2021"
description = "Configuration macros for Dioxus"
license = "MIT OR Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
keywords = ["dom", "ui", "gui", "react"]
[lib]
proc-macro = true
[dependencies]
proc-macro2 = { version = "1.0" }
quote = "1.0"
[features]
default = []
fullstack = []
desktop = []
mobile = []
web = []
ssr = []
liveview = []
tui = []
+39
View File
@@ -0,0 +1,39 @@
# dioxus-config-macro
[![Crates.io][crates-badge]][crates-url]
[![MIT licensed][mit-badge]][mit-url]
[![Build Status][actions-badge]][actions-url]
[![Discord chat][discord-badge]][discord-url]
[crates-badge]: https://img.shields.io/crates/v/dioxus-config-macro.svg
[crates-url]: https://crates.io/crates/dioxus-config-macro
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
[discord-url]: https://discord.gg/XgGxMSkvUM
[Website](https://dioxuslabs.com) |
[Guides](https://dioxuslabs.com/learn/0.4/) |
[API Docs](https://docs.rs/dioxus-config-macro/latest/dioxus_core_macro) |
[Chat](https://discord.gg/XgGxMSkvUM)
## Overview
`dioxus-config-macro` provides a handful of helpful macros to make it easier to work with optional sections of the launch builder.
## Contributing
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
- Join the discord and ask questions!
## License
This project is licensed under the [MIT license].
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in Dioxus by you shall be licensed as MIT without any additional
terms or conditions.
+127
View File
@@ -0,0 +1,127 @@
#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
#[proc_macro]
pub fn server_only(input: TokenStream) -> TokenStream {
if cfg!(any(feature = "ssr", feature = "liveview")) {
let input = TokenStream2::from(input);
quote! {
#input
}
} else {
quote! {
()
}
}
.into()
}
#[proc_macro]
pub fn client(input: TokenStream) -> TokenStream {
if cfg!(any(feature = "desktop", feature = "web", feature = "tui")) {
let input = TokenStream2::from(input);
quote! {
#input
}
} else {
quote! {
()
}
}
.into()
}
#[proc_macro]
pub fn web(input: TokenStream) -> TokenStream {
if cfg!(feature = "web") {
let input = TokenStream2::from(input);
quote! {
#input
}
} else {
quote! {
()
}
}
.into()
}
#[proc_macro]
pub fn desktop(input: TokenStream) -> TokenStream {
if cfg!(feature = "desktop") {
let input = TokenStream2::from(input);
quote! {
#input
}
} else {
quote! {
()
}
}
.into()
}
#[proc_macro]
pub fn fullstack(input: TokenStream) -> TokenStream {
if cfg!(feature = "web") {
let input = TokenStream2::from(input);
quote! {
#input
}
} else {
quote! {
()
}
}
.into()
}
#[proc_macro]
pub fn ssr(input: TokenStream) -> TokenStream {
if cfg!(feature = "ssr") {
let input = TokenStream2::from(input);
quote! {
#input
}
} else {
quote! {
()
}
}
.into()
}
#[proc_macro]
pub fn liveview(input: TokenStream) -> TokenStream {
if cfg!(feature = "liveview") {
let input = TokenStream2::from(input);
quote! {
#input
}
} else {
quote! {
()
}
}
.into()
}
#[proc_macro]
pub fn tui(input: TokenStream) -> TokenStream {
if cfg!(feature = "tui") {
let input = TokenStream2::from(input);
quote! {
#input
}
} else {
quote! {
()
}
}
.into()
}
-1
View File
@@ -17,7 +17,6 @@ proc-macro2 = { version = "1.0" }
quote = "1.0"
syn = { version = "2.0", features = ["full", "extra-traits", "visit"] }
dioxus-rsx = { workspace = true }
dioxus-core = { workspace = true }
constcat = "0.3.0"
convert_case = "^0.6.0"
prettyplease = "0.2.15"
+6 -39
View File
@@ -66,12 +66,12 @@
//! use dioxus::prelude::*;
//!
//! #[name_changer(CoolName)]
//! pub fn LameName(cx: Scope) -> Element {
//! render! { "I want a cool name!" }
//! pub fn LameName() -> Element {
//! rsx! { "I want a cool name!" }
//! }
//!
//! pub fn App(cx: Scope) -> Element {
//! render! { CoolName {} } // Renders: "I want a cool name!"
//! pub fn App() -> Element {
//! rsx! { CoolName {} } // Renders: "I want a cool name!"
//! }
//! ```
//! ## Implementing a component body deserializer
@@ -147,9 +147,7 @@ pub mod utils;
pub use utils::DeserializerArgs;
pub use utils::DeserializerOutput;
pub use utils::TypeHelper;
use dioxus_core::{Element, Scope};
use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned;
use syn::*;
@@ -167,12 +165,6 @@ pub struct ComponentBody {
/// Keep this in mind when creating deserializers, because you often might want to ignore it.
/// That might be annoying, but it would be bad design for this kind of struct to not be parsable from itself.
pub item_fn: ItemFn,
/// Parsing tries to ensure that this argument will be a [`Scope`].
/// **However, macros have limitations that prevent this from always working,
/// so don't take this for granted!**
pub cx_arg: FnArg,
/// The pattern (name) and type of the context argument.
pub cx_pat_type: PatType,
/// If the function has any arguments other than the context.
pub has_extra_args: bool,
}
@@ -192,31 +184,8 @@ impl ComponentBody {
impl Parse for ComponentBody {
fn parse(input: ParseStream) -> Result<Self> {
let item_fn: ItemFn = input.parse()?;
let scope_type_path = Scope::get_path_string();
let (cx_arg, cx_pat_type) = if let Some(first_arg) = item_fn.sig.inputs.first() {
let incorrect_first_arg_err = Err(Error::new(
first_arg.span(),
format!("First argument must be a <{}>", scope_type_path),
));
match first_arg.to_owned() {
FnArg::Receiver(_) => {
return incorrect_first_arg_err;
}
FnArg::Typed(f) => (first_arg.to_owned(), f),
}
} else {
return Err(Error::new(
item_fn.sig.ident.span(), // Or maybe just item_f.sig.span()?
format!(
"Must have at least one argument that's a <{}>",
scope_type_path
),
));
};
let element_type_path = Element::get_path_string();
let element_type_path = "dioxus_core::Element";
if item_fn.sig.output == ReturnType::Default {
return Err(Error::new(
@@ -225,12 +194,10 @@ impl Parse for ComponentBody {
));
}
let has_extra_args = item_fn.sig.inputs.len() > 1;
let has_extra_args = !item_fn.sig.inputs.is_empty();
Ok(Self {
item_fn,
cx_arg,
cx_pat_type,
has_extra_args,
})
}
@@ -1,7 +1,5 @@
use crate::component_body::ComponentBody;
use dioxus_core::{Element, Scope};
use quote::ToTokens;
use syn::{parse_quote, Path};
/// The output produced by a deserializer.
///
@@ -36,22 +34,3 @@ where
#[allow(unused_qualifications)]
fn to_output(&self, component_body: &ComponentBody) -> syn::Result<TOutput>;
}
pub trait TypeHelper {
fn get_path() -> Path;
fn get_path_string() -> String {
Self::get_path().to_token_stream().to_string()
}
}
impl<'a> TypeHelper for Scope<'a> {
fn get_path() -> Path {
parse_quote!(::dioxus::core::Scope)
}
}
impl<'a> TypeHelper for Element<'a> {
fn get_path() -> Path {
parse_quote!(::dioxus::core::Element)
}
}
@@ -16,7 +16,7 @@ To ignore this check, pass the \"",
const INNER_FN_NAME: &str = "__dx_inner_comp";
fn get_out_comp_fn(orig_comp_fn: &ItemFn, cx_pat: &Pat) -> ItemFn {
fn get_out_comp_fn(orig_comp_fn: &ItemFn) -> ItemFn {
let inner_comp_ident = Ident::new(INNER_FN_NAME, orig_comp_fn.sig.ident.span());
let inner_comp_fn = ItemFn {
@@ -27,6 +27,11 @@ fn get_out_comp_fn(orig_comp_fn: &ItemFn, cx_pat: &Pat) -> ItemFn {
..orig_comp_fn.clone()
};
let props_ident = match orig_comp_fn.sig.inputs.is_empty() {
true => quote! {},
false => quote! { __props },
};
ItemFn {
block: parse_quote! {
{
@@ -34,7 +39,7 @@ fn get_out_comp_fn(orig_comp_fn: &ItemFn, cx_pat: &Pat) -> ItemFn {
#[allow(clippy::inline_always)]
#[inline(always)]
#inner_comp_fn
#inner_comp_ident (#cx_pat)
#inner_comp_ident(#props_ident)
}
},
..orig_comp_fn.clone()
@@ -88,14 +93,9 @@ impl DeserializerArgs<ComponentDeserializerOutput> for ComponentDeserializerArgs
impl ComponentDeserializerArgs {
fn deserialize_no_props(component_body: &ComponentBody) -> ComponentDeserializerOutput {
let ComponentBody {
item_fn,
cx_pat_type,
..
} = component_body;
let cx_pat = &cx_pat_type.pat;
let ComponentBody { item_fn, .. } = component_body;
let comp_fn = get_out_comp_fn(item_fn, cx_pat);
let comp_fn = get_out_comp_fn(item_fn);
ComponentDeserializerOutput {
comp_fn,
@@ -106,12 +106,7 @@ impl ComponentDeserializerArgs {
fn deserialize_with_props(
component_body: &ComponentBody,
) -> Result<ComponentDeserializerOutput> {
let ComponentBody {
item_fn,
cx_pat_type,
..
} = component_body;
let cx_pat = &cx_pat_type.pat;
let ComponentBody { item_fn, .. } = component_body;
let comp_parsed = match parse2::<ComponentBody>(quote!(#item_fn)) {
Ok(comp_body) => comp_body,
@@ -130,7 +125,7 @@ impl ComponentDeserializerArgs {
let props_struct = inlined_props_output.props_struct;
let props_fn = inlined_props_output.comp_fn;
let comp_fn = get_out_comp_fn(&props_fn, cx_pat);
let comp_fn = get_out_comp_fn(&props_fn);
Ok(ComponentDeserializerOutput {
comp_fn,
@@ -46,12 +46,20 @@ fn get_props_struct(component_body: &ComponentBody) -> ItemStruct {
..
} = sig;
// Skip first arg since that's the context
let struct_fields = inputs.iter().skip(1).map(move |f| {
let struct_fields = inputs.iter().map(move |f| {
match f {
FnArg::Receiver(_) => unreachable!(), // Unreachable because of ComponentBody parsing
FnArg::Typed(pt) => {
let arg_pat = &pt.pat; // Pattern (identifier)
let arg_pat = match pt.pat.as_ref() {
// rip off mutability
Pat::Ident(f) => {
let mut f = f.clone();
f.mutability = None;
quote! { #f }
}
a => quote! { #a },
};
let arg_colon = &pt.colon_token;
let arg_ty = &pt.ty; // Type
let arg_attrs = &pt.attrs; // Attributes
@@ -74,9 +82,9 @@ fn get_props_struct(component_body: &ComponentBody) -> ItemStruct {
};
let struct_attrs = if first_lifetime.is_some() {
quote! { #[derive(Props)] }
quote! { #[derive(Props, Clone)] }
} else {
quote! { #[derive(Props, PartialEq)] }
quote! { #[derive(Props, Clone, PartialEq)] }
};
let struct_generics = if first_lifetime.is_some() {
@@ -225,11 +233,7 @@ fn get_props_docs(fn_ident: &Ident, inputs: Vec<&FnArg>) -> Vec<Attribute> {
}
fn get_function(component_body: &ComponentBody) -> ItemFn {
let ComponentBody {
item_fn,
cx_pat_type,
..
} = component_body;
let ComponentBody { item_fn, .. } = component_body;
let ItemFn {
attrs: fn_attrs,
vis,
@@ -246,13 +250,23 @@ fn get_function(component_body: &ComponentBody) -> ItemFn {
} = sig;
let Generics { where_clause, .. } = generics;
let cx_pat = &cx_pat_type.pat;
let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
// Skip first arg since that's the context
let struct_field_names = inputs.iter().skip(1).filter_map(|f| match f {
let struct_field_names = inputs.iter().filter_map(|f| match f {
FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
FnArg::Typed(pt) => Some(&pt.pat),
FnArg::Typed(pt) => {
let pat = &pt.pat;
let mut pat = pat.clone();
// rip off mutability, but still write it out eventually
if let Pat::Ident(ref mut pat_ident) = pat.as_mut() {
pat_ident.mutability = None;
}
Some(quote!(mut #pat))
}
});
let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
@@ -261,7 +275,7 @@ fn get_function(component_body: &ComponentBody) -> ItemFn {
None
};
let (scope_lifetime, fn_generics) = if let Some(lt) = first_lifetime {
let (_scope_lifetime, fn_generics) = if let Some(lt) = first_lifetime {
(quote! { #lt, }, generics.clone())
} else {
let lifetime: LifetimeParam = parse_quote! { 'a };
@@ -298,10 +312,10 @@ fn get_function(component_body: &ComponentBody) -> ItemFn {
parse_quote! {
#(#fn_attrs)*
#(#props_docs)*
#asyncness #vis fn #fn_ident #fn_generics (#cx_pat: Scope<#scope_lifetime #struct_ident #generics_no_bounds>) #fn_output
#asyncness #vis fn #fn_ident #fn_generics (mut __props: #struct_ident #generics_no_bounds) #fn_output
#where_clause
{
let #struct_ident { #(#struct_field_names),* } = &#cx_pat.props;
let #struct_ident { #(#struct_field_names),* } = __props;
#fn_block
}
}
+24 -26
View File
@@ -39,24 +39,22 @@ pub fn derive_typed_builder(input: TokenStream) -> TokenStream {
/// The rsx! macro makes it easy for developers to write jsx-style markup in their components.
#[proc_macro]
pub fn rsx(s: TokenStream) -> TokenStream {
match syn::parse::<rsx::CallBody>(s) {
Err(err) => err.to_compile_error().into(),
Ok(body) => body.to_token_stream().into(),
}
}
/// The render! macro makes it easy for developers to write jsx-style markup in their components.
///
/// The render macro automatically renders rsx - making it unhygienic.
#[proc_macro]
pub fn render(s: TokenStream) -> TokenStream {
match syn::parse::<rsx::CallBody>(s) {
pub fn rsx(tokens: TokenStream) -> TokenStream {
match syn::parse::<rsx::CallBody>(tokens) {
Err(err) => err.to_compile_error().into(),
Ok(body) => RenderCallBody(body).into_token_stream().into(),
}
}
/// The rsx! macro makes it easy for developers to write jsx-style markup in their components.
///
/// The render macro automatically renders rsx - making it unhygienic.
#[deprecated(note = "Use `rsx!` instead.")]
#[proc_macro]
pub fn render(tokens: TokenStream) -> TokenStream {
rsx(tokens)
}
/// Derive props for a component within the component definition.
///
/// This macro provides a simple transformation from `Scope<{}>` to `Scope<P>`,
@@ -68,8 +66,8 @@ pub fn render(s: TokenStream) -> TokenStream {
/// # Example
/// ```rust,ignore
/// #[inline_props]
/// fn app(cx: Scope, bob: String) -> Element {
/// cx.render(rsx!("hello, {bob}"))
/// fn app(bob: String) -> Element {
/// rsx!("hello, {bob}"))
/// }
///
/// // is equivalent to
@@ -80,7 +78,7 @@ pub fn render(s: TokenStream) -> TokenStream {
/// }
///
/// fn app(cx: Scope<AppProps>) -> Element {
/// cx.render(rsx!("hello, {bob}"))
/// rsx!("hello, {bob}"))
/// }
/// ```
#[proc_macro_attribute]
@@ -131,18 +129,18 @@ pub(crate) const COMPONENT_ARG_CASE_CHECK_OFF: &str = "no_case_check";
/// * Without props:
/// ```rust,ignore
/// #[component]
/// fn GreetBob(cx: Scope) -> Element {
/// render! { "hello, bob" }
/// fn GreetBob() -> Element {
/// rsx! { "hello, bob" }
/// }
///
/// // is equivalent to
///
/// #[allow(non_snake_case)]
/// fn GreetBob(cx: Scope) -> Element {
/// fn GreetBob() -> Element {
/// #[warn(non_snake_case)]
/// #[inline(always)]
/// fn __dx_inner_comp(cx: Scope) -> Element {
/// render! { "hello, bob" }
/// fn __dx_inner_comp() -> Element {
/// rsx! { "hello, bob" }
/// }
/// // There's no function call overhead since __dx_inner_comp has the #[inline(always)] attribute,
/// // so don't worry about performance.
@@ -152,8 +150,8 @@ pub(crate) const COMPONENT_ARG_CASE_CHECK_OFF: &str = "no_case_check";
/// * With props:
/// ```rust,ignore
/// #[component(no_case_check)]
/// fn GreetPerson(cx: Scope, person: String) -> Element {
/// render! { "hello, {person}" }
/// fn GreetPerson(person: String) -> Element {
/// rsx! { "hello, {person}" }
/// }
///
/// // is equivalent to
@@ -165,13 +163,13 @@ pub(crate) const COMPONENT_ARG_CASE_CHECK_OFF: &str = "no_case_check";
/// }
///
/// #[allow(non_snake_case)]
/// fn GreetPerson<'a>(cx: Scope<'a, GreetPersonProps>) -> Element {
/// fn GreetPerson(props: GreetPersonProps>) -> Element {
/// #[warn(non_snake_case)]
/// #[inline(always)]
/// fn __dx_inner_comp<'a>(cx: Scope<'a, GreetPersonProps>e) -> Element {
/// fn __dx_inner_comp(props: GreetPersonProps>e) -> Element {
/// let GreetPersonProps { person } = &cx.props;
/// {
/// render! { "hello, {person}" }
/// rsx! { "hello, {person}" }
/// }
/// }
///
+291 -145
View File
@@ -3,15 +3,17 @@
//! However, it has been adopted to fit the Dioxus Props builder pattern.
//!
//! For Dioxus, we make a few changes:
//! - [ ] Automatically implement Into<Option> on the setters (IE the strip setter option)
//! - [ ] Automatically implement a default of none for optional fields (those explicitly wrapped with Option<T>)
//! - [x] Automatically implement Into<Option> on the setters (IE the strip setter option)
//! - [x] Automatically implement a default of none for optional fields (those explicitly wrapped with Option<T>)
use proc_macro2::TokenStream;
use syn::parse::Error;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::{parse::Error, PathArguments};
use quote::quote;
use syn::{parse_quote, Type};
pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result<TokenStream, Error> {
let data = match &ast.data {
@@ -173,7 +175,7 @@ mod field_info {
use quote::quote;
use syn::spanned::Spanned;
use syn::{parse::Error, punctuated::Punctuated};
use syn::{Expr, Path};
use syn::{parse_quote, Expr, Path};
use super::util::{
expr_to_single_string, ident_to_type, path_to_single_string, strip_raw_ident_prefix,
@@ -204,6 +206,15 @@ mod field_info {
);
}
// String fields automatically use impl Display
if field.ty == parse_quote!(::std::string::String)
|| field.ty == parse_quote!(std::string::String)
|| field.ty == parse_quote!(string::String)
|| field.ty == parse_quote!(String)
{
builder_attr.from_displayable = true;
}
// extended field is automatically empty
if !builder_attr.extends.is_empty() {
builder_attr.default = Some(
@@ -263,6 +274,7 @@ mod field_info {
pub doc: Option<syn::Expr>,
pub skip: bool,
pub auto_into: bool,
pub from_displayable: bool,
pub strip_option: bool,
pub ignore_option: bool,
pub extends: Vec<Path>,
@@ -412,6 +424,7 @@ mod field_info {
handle_fields!(
"skip", skip, "skipped";
"into", auto_into, "calling into() on the argument";
"displayable", from_displayable, "calling to_string() on the argument";
"strip_option", strip_option, "putting the argument in Some(...)";
)
}
@@ -443,6 +456,10 @@ mod field_info {
self.auto_into = false;
Ok(())
}
"displayable" => {
self.from_displayable = false;
Ok(())
}
"optional" => {
self.strip_option = false;
self.ignore_option = true;
@@ -497,10 +514,10 @@ mod struct_info {
use syn::parse::Error;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::visit::Visit;
use syn::{parse_quote, Expr, Ident};
use syn::{Expr, Ident};
use super::field_info::{FieldBuilderAttr, FieldInfo};
use super::looks_like_signal_type;
use super::util::{
empty_type, empty_type_tuple, expr_to_single_string, make_punctuated_single,
modify_types_generics_hack, path_to_single_string, strip_raw_ident_prefix, type_tuple,
@@ -532,37 +549,6 @@ mod struct_info {
.filter(|f| !f.builder_attr.extends.is_empty())
}
fn extend_lifetime(&self) -> syn::Result<Option<syn::Lifetime>> {
let first_extend = self.extend_fields().next();
match first_extend {
Some(f) => {
struct VisitFirstLifetime(Option<syn::Lifetime>);
impl Visit<'_> for VisitFirstLifetime {
fn visit_lifetime(&mut self, lifetime: &'_ syn::Lifetime) {
if self.0.is_none() {
self.0 = Some(lifetime.clone());
}
}
}
let name = f.name;
let mut visitor = VisitFirstLifetime(None);
visitor.visit_type(f.ty);
visitor.0.ok_or_else(|| {
syn::Error::new_spanned(
name,
"Unable to find lifetime for extended field. Please specify it manually",
)
}).map(Some)
}
None => Ok(None),
}
}
pub fn new(
ast: &'a syn::DeriveInput,
fields: impl Iterator<Item = &'a syn::Field>,
@@ -593,6 +579,89 @@ mod struct_info {
generics
}
fn has_signal_fields(&self) -> bool {
self.fields.iter().any(|f| looks_like_signal_type(f.ty))
}
fn memoize_impl(&self) -> Result<TokenStream, Error> {
// First check if there are any ReadOnlySignal fields, if there are not, we can just use the partialEq impl
let has_signal_fields = self.has_signal_fields();
if has_signal_fields {
let signal_fields: Vec<_> = self
.included_fields()
.filter(|f| looks_like_signal_type(f.ty))
.map(|f| {
let name = f.name;
quote!(#name)
})
.collect();
Ok(quote! {
// First check if the fields are equal
let exactly_equal = self == new;
if exactly_equal {
return true;
}
// If they are not, move over the signal fields and check if they are equal
#(
let mut #signal_fields = self.#signal_fields;
self.#signal_fields = new.#signal_fields;
)*
// Then check if the fields are equal again
let non_signal_fields_equal = self == new;
// If they are not equal, we still need to rerun the component
if !non_signal_fields_equal {
return false;
}
trait NonPartialEq: Sized {
fn compare(&self, other: &Self) -> bool;
}
impl<T> NonPartialEq for &&T {
fn compare(&self, other: &Self) -> bool {
false
}
}
trait CanPartialEq: PartialEq {
fn compare(&self, other: &Self) -> bool;
}
impl<T: PartialEq> CanPartialEq for T {
fn compare(&self, other: &Self) -> bool {
self == other
}
}
// If they are equal, we don't need to rerun the component we can just update the existing signals
#(
// Try to memo the signal
let field_eq = {
let old_value: &_ = &*#signal_fields.peek();
let new_value: &_ = &*new.#signal_fields.peek();
(&old_value).compare(&&new_value)
};
if !field_eq {
(#signal_fields).__set(new.#signal_fields.__take());
}
// Move the old value back
self.#signal_fields = #signal_fields;
)*
true
})
} else {
Ok(quote! {
self == new
})
}
}
pub fn builder_creation_impl(&self) -> Result<TokenStream, Error> {
let StructInfo {
ref vis,
@@ -601,22 +670,8 @@ mod struct_info {
..
} = *self;
// we're generating stuff that goes into unsafe code here
// we use the heuristic: are there *any* generic parameters?
// If so, then they might have non-static lifetimes and we can't compare two generic things that *might borrow*
// Therefore, we will generate code that shortcircuits the "comparison" in memoization
let are_there_generics = !self.generics.params.is_empty();
let extend_lifetime = self.extend_lifetime()?;
let generics = self.generics.clone();
let (_, ty_generics, where_clause) = generics.split_for_impl();
let impl_generics = self.modify_generics(|g| {
if extend_lifetime.is_none() {
g.params.insert(0, parse_quote!('__bump));
}
});
let (impl_generics, _, _) = impl_generics.split_for_impl();
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let (_, b_initial_generics, _) = self.generics.split_for_impl();
let all_fields_param = syn::GenericParam::Type(
syn::Ident::new("TypedBuilderFields", proc_macro2::Span::call_site()).into(),
@@ -694,49 +749,34 @@ Finally, call `.build()` to create the instance of `{name}`.
.extend(predicates.predicates.clone());
}
let can_memoize = match are_there_generics {
true => quote! { false },
false => quote! { self == other },
};
let memoize = self.memoize_impl()?;
let is_static = match are_there_generics {
true => quote! { false },
false => quote! { true },
};
let extend_fields = self.extend_fields().map(|f| {
let name = f.name;
let ty = f.ty;
quote!(#name: #ty)
});
let extend_fields_value = self.extend_fields().map(|f| {
let name = f.name;
quote!(#name: Vec::new())
});
let has_extend_fields = self.extend_fields().next().is_some();
let take_bump = if has_extend_fields {
quote!(bump: _cx.bump(),)
} else {
quote!()
};
let bump_field = if has_extend_fields {
quote!(bump: & #extend_lifetime ::dioxus::core::exports::bumpalo::Bump,)
} else {
quote!()
};
let extend_lifetime = extend_lifetime.unwrap_or(syn::Lifetime::new(
"'__bump",
proc_macro2::Span::call_site(),
));
let global_fields = self
.extend_fields()
.map(|f| {
let name = f.name;
let ty = f.ty;
quote!(#name: #ty)
})
.chain(self.has_signal_fields().then(|| quote!(owner: Owner)));
let global_fields_value = self
.extend_fields()
.map(|f| {
let name = f.name;
quote!(#name: Vec::new())
})
.chain(
self.has_signal_fields()
.then(|| quote!(owner: Owner::default())),
);
Ok(quote! {
impl #impl_generics #name #ty_generics #where_clause {
#[doc = #builder_method_doc]
#[allow(dead_code, clippy::type_complexity)]
#vis fn builder(_cx: & #extend_lifetime ::dioxus::prelude::ScopeState) -> #builder_name #generics_with_empty {
#vis fn builder() -> #builder_name #generics_with_empty {
#builder_name {
#(#extend_fields_value,)*
#take_bump
#(#global_fields_value,)*
fields: #empties_tuple,
_phantom: ::core::default::Default::default(),
}
@@ -747,25 +787,22 @@ Finally, call `.build()` to create the instance of `{name}`.
#builder_type_doc
#[allow(dead_code, non_camel_case_types, non_snake_case)]
#vis struct #builder_name #b_generics {
#(#extend_fields,)*
#bump_field
#(#global_fields,)*
fields: #all_fields_param,
_phantom: (#( #phantom_generics ),*),
}
impl #impl_generics ::dioxus::prelude::Properties<#extend_lifetime> for #name #ty_generics
impl #impl_generics dioxus_core::prelude::Properties for #name #ty_generics
#b_generics_where_extras_predicates
{
type Builder = #builder_name #generics_with_empty;
const IS_STATIC: bool = #is_static;
fn builder(_cx: &#extend_lifetime ::dioxus::prelude::ScopeState) -> Self::Builder {
#name::builder(_cx)
fn builder() -> Self::Builder {
#name::builder()
}
unsafe fn memoize(&self, other: &Self) -> bool {
#can_memoize
fn memoize(&mut self, new: &Self) -> bool {
#memoize
}
}
})
}
@@ -811,7 +848,6 @@ Finally, call `.build()` to create the instance of `{name}`.
});
let reconstructing = self.included_fields().map(|f| f.name);
// Add the bump lifetime to the generics
let mut ty_generics: Vec<syn::GenericArgument> = self
.generics
.params
@@ -875,11 +911,6 @@ Finally, call `.build()` to create the instance of `{name}`.
quote!(#name: self.#name)
});
let extend_lifetime = self.extend_lifetime()?.ok_or(Error::new_spanned(
field_name,
"Unable to find lifetime for extended field. Please specify it manually",
))?;
let extends_impl = field.builder_attr.extends.iter().map(|path| {
let name_str = path_to_single_string(path).unwrap();
let camel_name = name_str.to_case(Case::UpperCamel);
@@ -889,27 +920,27 @@ Finally, call `.build()` to create the instance of `{name}`.
);
quote! {
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl #impl_generics dioxus_elements::extensions::#marker_name < #extend_lifetime > for #builder_name < #( #ty_generics ),* > #where_clause {}
impl #impl_generics dioxus_elements::extensions::#marker_name for #builder_name < #( #ty_generics ),* > #where_clause {}
}
});
Ok(quote! {
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl #impl_generics ::dioxus::prelude::HasAttributes<#extend_lifetime> for #builder_name < #( #ty_generics ),* > #where_clause {
impl #impl_generics dioxus_core::prelude::HasAttributes for #builder_name < #( #ty_generics ),* > #where_clause {
fn push_attribute(
mut self,
name: &#extend_lifetime str,
name: &'static str,
ns: Option<&'static str>,
attr: impl ::dioxus::prelude::IntoAttributeValue<#extend_lifetime>,
attr: impl dioxus_core::prelude::IntoAttributeValue,
volatile: bool
) -> Self {
let ( #(#descructuring,)* ) = self.fields;
self.#field_name.push(
::dioxus::core::Attribute::new(
dioxus_core::Attribute::new(
name,
{
use ::dioxus::prelude::IntoAttributeValue;
attr.into_value(self.bump)
use dioxus_core::prelude::IntoAttributeValue;
attr.into_value()
},
ns,
volatile,
@@ -917,7 +948,6 @@ Finally, call `.build()` to create the instance of `{name}`.
);
#builder_name {
#(#forward_extended_fields,)*
bump: self.bump,
fields: ( #(#reconstructing,)* ),
_phantom: self._phantom,
}
@@ -1011,6 +1041,7 @@ Finally, call `.build()` to create the instance of `{name}`.
index_after_lifetime_in_generics,
syn::GenericArgument::Type(ty_generics_tuple.into()),
);
let (impl_generics, _, where_clause) = generics.split_for_impl();
let doc = match field.builder_attr.doc {
Some(ref doc) => quote!(#[doc = #doc]),
@@ -1018,15 +1049,31 @@ Finally, call `.build()` to create the instance of `{name}`.
};
let arg_type = field_type;
let (arg_type, arg_expr) =
if field.builder_attr.auto_into || field.builder_attr.strip_option {
(
quote!(impl ::core::convert::Into<#arg_type>),
quote!(#field_name.into()),
)
} else {
(quote!(#arg_type), quote!(#field_name))
};
// If the field is auto_into, we need to add a generic parameter to the builder for specialization
let mut marker = None;
let (arg_type, arg_expr) = if looks_like_signal_type(arg_type) {
let marker_ident = syn::Ident::new("__Marker", proc_macro2::Span::call_site());
marker = Some(marker_ident.clone());
(
quote!(impl dioxus_core::prelude::SuperInto<#arg_type, #marker_ident>),
// If this looks like a signal type, we automatically convert it with SuperInto and use the props struct as the owner
quote!(with_owner(self.owner.clone(), move || dioxus_core::prelude::SuperInto::super_into(#field_name))),
)
} else if field.builder_attr.auto_into || field.builder_attr.strip_option {
let marker_ident = syn::Ident::new("__Marker", proc_macro2::Span::call_site());
marker = Some(marker_ident.clone());
(
quote!(impl dioxus_core::prelude::SuperInto<#arg_type, #marker_ident>),
quote!(dioxus_core::prelude::SuperInto::super_into(#field_name)),
)
} else if field.builder_attr.from_displayable {
(
quote!(impl ::core::fmt::Display),
quote!(#field_name.to_string()),
)
} else {
(quote!(#arg_type), quote!(#field_name))
};
let repeated_fields_error_type_name = syn::Ident::new(
&format!(
@@ -1038,27 +1085,24 @@ Finally, call `.build()` to create the instance of `{name}`.
);
let repeated_fields_error_message = format!("Repeated field {field_name}");
let forward_extended_fields = self.extend_fields().map(|f| {
let name = f.name;
quote!(#name: self.#name)
});
let forward_bump = if self.extend_fields().next().is_some() {
quote!(bump: self.bump,)
} else {
quote!()
};
let forward_fields = self
.extend_fields()
.map(|f| {
let name = f.name;
quote!(#name: self.#name)
})
.chain(self.has_signal_fields().then(|| quote!(owner: self.owner)));
Ok(quote! {
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl #impl_generics #builder_name < #( #ty_generics ),* > #where_clause {
#doc
#[allow(clippy::type_complexity)]
pub fn #field_name (self, #field_name: #arg_type) -> #builder_name < #( #target_generics ),* > {
pub fn #field_name < #marker > (self, #field_name: #arg_type) -> #builder_name < #( #target_generics ),* > {
let #field_name = (#arg_expr,);
let ( #(#descructuring,)* ) = self.fields;
#builder_name {
#(#forward_extended_fields,)*
#forward_bump
#(#forward_fields,)*
fields: ( #(#reconstructing,)* ),
_phantom: self._phantom,
}
@@ -1074,7 +1118,7 @@ Finally, call `.build()` to create the instance of `{name}`.
note = #repeated_fields_error_message
)]
#[allow(clippy::type_complexity)]
pub fn #field_name (self, _: #repeated_fields_error_type_name) -> #builder_name < #( #target_generics ),* > {
pub fn #field_name< #marker > (self, _: #repeated_fields_error_type_name) -> #builder_name < #( #target_generics ),* > {
self
}
}
@@ -1183,7 +1227,7 @@ Finally, call `.build()` to create the instance of `{name}`.
note = #early_build_error_message
)]
pub fn build(self, _: #early_build_error_type_name) -> #name #ty_generics {
panic!();
panic!()
}
}
})
@@ -1280,26 +1324,84 @@ Finally, call `.build()` to create the instance of `{name}`.
// I’d prefer “a” or “an” to “its”, but determining which is grammatically
// correct is roughly impossible.
let doc =
format!("Finalise the builder and create its [`{name}`] instance");
format!("Finalize the builder and create its [`{name}`] instance");
quote!(#[doc = #doc])
}
}
} else {
quote!()
};
quote!(
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl #impl_generics #builder_name #modified_ty_generics #where_clause {
#doc
pub fn build(self) -> #name #ty_generics {
let ( #(#descructuring,)* ) = self.fields;
#( #assignments )*
#name {
#( #field_names ),*
if self.has_signal_fields() {
let name = Ident::new(&format!("{}WithOwner", name), name.span());
let original_name = &self.name;
quote! {
#[doc(hidden)]
#[allow(dead_code, non_camel_case_types, missing_docs)]
#[derive(Clone)]
struct #name #ty_generics {
inner: #original_name #ty_generics,
owner: Owner,
}
impl #impl_generics PartialEq for #name #ty_generics #where_clause {
fn eq(&self, other: &Self) -> bool {
self.inner.eq(&other.inner)
}
}
impl #impl_generics #name #ty_generics #where_clause {
/// Create a component from the props.
fn into_vcomponent<M: 'static>(
self,
render_fn: impl dioxus_core::prelude::ComponentFunction<#original_name #ty_generics, M>,
component_name: &'static str,
) -> dioxus_core::VComponent {
use dioxus_core::prelude::ComponentFunction;
dioxus_core::VComponent::new(move |wrapper: Self| render_fn.rebuild(wrapper.inner), self, component_name)
}
}
impl #impl_generics dioxus_core::prelude::Properties for #name #ty_generics #where_clause {
type Builder = ();
fn builder() -> Self::Builder {
unreachable!()
}
fn memoize(&mut self, new: &Self) -> bool {
self.inner.memoize(&new.inner)
}
}
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl #impl_generics #builder_name #modified_ty_generics #where_clause {
#doc
pub fn build(self) -> #name #ty_generics {
let ( #(#descructuring,)* ) = self.fields;
#( #assignments )*
#name {
inner: #original_name {
#( #field_names ),*
},
owner: self.owner,
}
}
}
}
)
} else {
quote!(
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl #impl_generics #builder_name #modified_ty_generics #where_clause {
#doc
pub fn build(self) -> #name #ty_generics {
let ( #(#descructuring,)* ) = self.fields;
#( #assignments )*
#name {
#( #field_names ),*
}
}
}
)
}
}
}
@@ -1425,3 +1527,47 @@ Finally, call `.build()` to create the instance of `{name}`.
}
}
}
fn looks_like_signal_type(ty: &Type) -> bool {
match ty {
Type::Path(ty) => {
if ty.qself.is_some() {
return false;
}
let path = &ty.path;
let mut path_segments_without_generics = Vec::new();
let mut generic_arg_count = 0;
for segment in &path.segments {
let mut segment = segment.clone();
match segment.arguments {
PathArguments::AngleBracketed(_) => generic_arg_count += 1,
PathArguments::Parenthesized(_) => {
return false;
}
_ => {}
}
segment.arguments = syn::PathArguments::None;
path_segments_without_generics.push(segment);
}
// If there is more than the type and the send/sync generic, it doesn't look like our signal
if generic_arg_count > 2 {
return false;
}
let path_without_generics = syn::Path {
leading_colon: None,
segments: Punctuated::from_iter(path_segments_without_generics),
};
path_without_generics == parse_quote!(dioxus_core::prelude::ReadOnlySignal)
|| path_without_generics == parse_quote!(prelude::ReadOnlySignal)
|| path_without_generics == parse_quote!(ReadOnlySignal)
}
_ => false,
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ use syn::spanned::Spanned;
use syn::{parse_quote, Expr, Lit, Meta, Token, Type};
const FORMATTED_TYPE_START: &str = "static TY_AFTER_HERE:";
const FORMATTED_TYPE_END: &str = "= todo!();";
const FORMATTED_TYPE_END: &str = "= unreachable!();";
/// Attempts to convert the given literal to a string.
/// Converts ints and floats to their base 10 counterparts.
-757
View File
@@ -1,757 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Documentation
- <csr-id-036a0ff49a7dade0e04c9c07071a1ff49133ee24/> add comments for the Handler
### New Features
- <csr-id-bad4b773b7bbab6a38d4f48d88d8c5a8787927ac/> add "spawn" method
- <csr-id-d2bd1751436ef4bec554bb361ba87aea1357036a/> allow providing context to the root component
### Bug Fixes
- <csr-id-d9a07ddddb45ba00e35645ac0e0ad1a4d2dd996d/> provide_root_context on root scopes
- <csr-id-c8d528b3b18e320ca0cee1542ae5fc659c1cca81/> proprogation of root context
- <csr-id-e47ead5347ce778935f8f2127fcb14f520eda0ce/> allow eventhandler to derive default
### Commit Statistics
<csr-read-only-do-not-edit/>
- 18 commits contributed to the release over the course of 13 calendar days.
- 8 commits where understood as [conventional](https://www.conventionalcommits.org).
- 0 issues like '(#ID)' where seen in commit messages
### Commit Details
<csr-read-only-do-not-edit/>
<details><summary>view details</summary>
* **Uncategorized**
- Merge pull request #169 from DioxusLabs/jk/router-userouter ([`3509602`](https://github.comgit//DioxusLabs/dioxus/commit/3509602c0bcd327a33bc8c95896775e24751da1a))
- Merge branch 'master' of github.com:DioxusLabs/dioxus ([`8899701`](https://github.comgit//DioxusLabs/dioxus/commit/88997019c5c74658bfd76344d6bb6f3565943b41))
- add miri stress test ([`e9792e9`](https://github.comgit//DioxusLabs/dioxus/commit/e9792e9b95521fcf0b0b4e240d7cf0397cd79b8f))
- add "spawn" method ([`bad4b77`](https://github.comgit//DioxusLabs/dioxus/commit/bad4b773b7bbab6a38d4f48d88d8c5a8787927ac))
- drop hooks before resetting bump arena ([`2e4f765`](https://github.comgit//DioxusLabs/dioxus/commit/2e4f7659327b69b74e20af59b6d5e65c699b6adb))
- provide_root_context on root scopes ([`d9a07dd`](https://github.comgit//DioxusLabs/dioxus/commit/d9a07ddddb45ba00e35645ac0e0ad1a4d2dd996d))
- Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d))
- Merge pull request #121 from DioxusLabs/jk/unify ([`b287a4c`](https://github.comgit//DioxusLabs/dioxus/commit/b287a4cab3eec9e6961a3f010846291f4f105747))
- Merge pull request #108 from DioxusLabs/jk/fstring-component-fields ([`f4132d1`](https://github.comgit//DioxusLabs/dioxus/commit/f4132d1874f7495049fac23ba0a022ac137ad74f))
- proprogation of root context ([`c8d528b`](https://github.comgit//DioxusLabs/dioxus/commit/c8d528b3b18e320ca0cee1542ae5fc659c1cca81))
- allow providing context to the root component ([`d2bd175`](https://github.comgit//DioxusLabs/dioxus/commit/d2bd1751436ef4bec554bb361ba87aea1357036a))
- Enable clippy ([`b6903bf`](https://github.comgit//DioxusLabs/dioxus/commit/b6903bf558bc7a3d0fe6794a137c44fca0957d11))
- Merge pull request #107 from autarch/autarch/half-assed-router ([`8d3ac3f`](https://github.comgit//DioxusLabs/dioxus/commit/8d3ac3ff148aef9d10a393eda453a11c1e882f58))
- Merge pull request #138 from mrxiaozhuox/master ([`8c7473d`](https://github.comgit//DioxusLabs/dioxus/commit/8c7473d1943dd133f388ec36116c9d8295861b97))
- Merge pull request #133 from mrxiaozhuox/master ([`887f69d`](https://github.comgit//DioxusLabs/dioxus/commit/887f69d5b47bdcde4fe0eab094c0cd0de23e4f3f))
- Don't expect all components to have a scope in ScopeArena.ensure_drop_safety ([`9b282d8`](https://github.comgit//DioxusLabs/dioxus/commit/9b282d877ba0fe2463687cde7ea899b7f8510425))
- add comments for the Handler ([`036a0ff`](https://github.comgit//DioxusLabs/dioxus/commit/036a0ff49a7dade0e04c9c07071a1ff49133ee24))
- allow eventhandler to derive default ([`e47ead5`](https://github.comgit//DioxusLabs/dioxus/commit/e47ead5347ce778935f8f2127fcb14f520eda0ce))
</details>
## v0.1.7 (2022-01-08)
### Documentation
- <csr-id-d11f322f554e7dbf43b988c9cfda56498cc49872/> add title to doc comment
- <csr-id-be9f1a52ad2b04f101397ae34482ea7394df653b/> better document the `EventHandler` type
- <csr-id-9874f342e2bc1116410142c691545d75b0e0d6fe/> more docs
- <csr-id-54d050cf897131ddc4f53651f1aeedaf03f23f57/> strong improvmenets, first major section done
- <csr-id-e4c06ce8e893779d2aad0883a1bb27d193bc5985/> update cargo tomls
- <csr-id-9b5f82af7dce18b75a781b86b188bc713860bb99/> start on components
- <csr-id-4de16c4779648e591b3869b5df31271ae603c812/> update local examples and docs to support new syntaxes
- <csr-id-e495b09bf1c99950b5ea8647b371f296dc64afbe/> fix table
- <csr-id-460783ad549818a85db634ed9c39ffce210b98ec/> lnks to projects
- <csr-id-583fdfa5618e11d660985b97e570d4503be2ff49/> big updates to the reference
- <csr-id-d9e6d0925b30690212d1d690dfba288f1a694a27/> examples
- <csr-id-0826fdfee13d26b08cb8a2fa45cbd59cc4f20c25/> more docs
- <csr-id-70cd46dbb2a689ae2d512e142b8aee9c80798430/> move around examples
- <csr-id-69f5cc3802af136729bc73e5c3d209270d41b184/> move into a fromjs tutorial
- <csr-id-7102fe5f984fe8692cf977ea3a43e2973eed4f45/> add some more sources in the core implementation
### New Features
- <csr-id-427b126bc17336d5d14d56eb7fddb8e07752495f/> add prevent default attribute and upgrade router
- <csr-id-bbb6ee10de824f2e3259576ac01768640c884279/> make hydration more robust
- <csr-id-06276edd0d4f1f6f6b0a3bf7a467931413ab33c3/> eanble bubbling
- <csr-id-d84fc0538670b2a3bda9ae41878896793b74e8ee/> plug in bubbling
- <csr-id-a4f280d16399205c638033bf9beb858e478e98ff/> more API updates
- <csr-id-b997b8ebbb82b5b9e9119bd2eb25335e2ed009d0/> enable children properly
- <csr-id-78d9056e35deb6e87ea2929cc5369e35b8a20847/> it works with a new bump each time!!
- <csr-id-96b3d56e09a38843f7dc6c1047fe2a3923bd8473/> move back to combined cx/props
- <csr-id-19df1bda109aba03c40ff631263bcb7035004ca0/> bubbling
- <csr-id-8acdd2ea830b995b608d8bac2ef527db8d40e662/> it compiles once more
- <csr-id-74c6211408ea0db5916a4e0973a3f9a7208faa44/> should be functional across the boar
- <csr-id-55e6dd9701d3710ff17a25581b99b8f159e609b9/> wire tasks up
- <csr-id-c10c1f418bf21e0e54b53e2babeb37c672c06901/> wire up linked nodes
- <csr-id-9d8c5ca5ab5784b3f17d7ee20a451ee68fd703d6/> it properly bubbles
- <csr-id-fd93ee89c19b085a04307ef30217170518defa8e/> upgrade syntax
- <csr-id-11757ddf61e1decb1bd1c2bb30455d0bd01a3e95/> fake bubbling
- <csr-id-f2234068ba7cd915a00a81e41660d7d6ee1177cc/> events bubble now
- <csr-id-2cf90b6903411e42f01a801f89037686194ee068/> pull children out of component definition
- <csr-id-84fd0c616252bf29cd665782258530032b54d13a/> cleanuup
- <csr-id-79503f15c5db04fa04575c8735941a2e3a75030b/> full html support
- <csr-id-6b2645fd80ab20153e7bb51887443e994fbab5cb/> improve FC handling to allow lifetimes of parent
- <csr-id-fda2ebc2a22965845e015384f39f34ce7cb3e428/> improve safety
- <csr-id-9726a065b0d4fb1ede5b53a2ddd58c855e51539f/> massage lifetimes
- <csr-id-efd0e9b5648c809057f339083ba9d454f810d483/> support desktop more completely
- <csr-id-a2b0c50a343005c63c7032bcefb8323b78350bb9/> add update functionality to useref
- <csr-id-5502429626023d0788cca352e94ac6ea67c2cb11/> desktop functioning well
- <csr-id-4a4c7afca7e1beadd4b213214074fdb420eb0923/> shared state mechanisms
- <csr-id-fac42339c272b0e430ebf4f31b6061a0635d3e19/> mutations
- <csr-id-687cda1b6d9595357d1dc8720ebe921f61098d8f/> re-enable suspense
- <csr-id-4a72b3140bd244da602deada1eeecded65ff5848/> amazingly awesome error handling
- <csr-id-d7940aa2ac2017316d62e0f2eac0701dc6ad1f09/> proper handling of events
- <csr-id-d717c22d9c5da7b6f343fede11faaf953a3a29e0/> keyed diffing!!
- <csr-id-fac2e56ed63fc708a89e14df15f1fc32f58391a9/> update root props
- <csr-id-c321532a6cef40b2d2e4adc8c7a55931b6755b08/> some docs, cleaning
- <csr-id-1749eba8eb6c8ffa38ab3ad52160b637fe021e86/> more and more sophisticated diff
- <csr-id-d618092e9d150589e61516f7bbb169f2db49d3f2/> a new vnode type for anchors
- <csr-id-00231adfa2e1d67a9d7ae2fa61c33e3a22d51978/> code quality improvements for core
- <csr-id-a2c7d17b0595769f60bc1c2bbf7cbe32cec37486/> mvoe away from compound context
- <csr-id-31702dbf878dd0207d101f7869ebefd2bb9f6860/> wire up resource pool
- <csr-id-e5c88fe3a49649ecb308decd14c2557963978619/> make hooks free-functions
- <csr-id-bfdcb20437c385116fd640e46b6361d09d8d5ff8/> transition to cx, props
- <csr-id-f782e142118fb7acf1b88a0f3fbb03e4a5e3e91e/> omg what a dumb mistake
- <csr-id-4837d8e741343d26f31b55e4478a374dc761e538/> suspense!
- <csr-id-4a0068f09918adbc299150edcf777f342ced0dd3/> bless up, no more segfaults
- <csr-id-de9f61bcf48c0d6e35e46c337b72a713c9f9f7d2/> more suspended nodes!
- <csr-id-7dfe89c9581f45a445f17f9fe4bb94e61f67e971/> wire up event delegator for webview
- <csr-id-0a907b35c647b67172080023462770b761364d51/> fix some lifetime issues
- <csr-id-80e6c256980eb3e8c32e30f3dbb43c8b3b9a9cf4/> move over to push based mechanism
- <csr-id-e7238762ae518c5688f9339d11832d17f99ad553/> architecture document and edit list
- <csr-id-3a57b942624afb8aa6650aeee05466c3c9ce967e/> task system works
but I broke the other things :(
- <csr-id-f457b7113129479cad577237ef21cb735fffe483/> rebuild doesn't return errors
- <csr-id-4091846934b4b3b2bc03d3ca8aaf7712aebd4e36/> add aria
- <csr-id-c79d9ae674e235c8e9c2c069d24902122b9c7464/> buff up html allowed attributes
- <csr-id-bbcb5a0234dbce48ffeb64903c3ec04562a87ad6/> enable components in ssr
- <csr-id-0479252a5fba96e554dcf2422a8566e248fc6593/> keyed diffing
- <csr-id-9abb0470b7869019d539a2fc21da3872348ae38b/> static node infrastructure and ssr changes
- <csr-id-e6f56563bc84516e017b3db06f11fab2549b9a50/> tests and benchmarks
- <csr-id-db6d0184aa5aa612bd7229a06f72b4c2ec5f6409/> dedicated mutations module
- <csr-id-1cc1679a6ba33ff68d45ad1e964b73230224bc23/> refactor out the hooks implementation
- <csr-id-2ce0752a9cd253fee1c8b205400aa191b09c9dcb/> fix compiling
- <csr-id-99d94b69aba192be7e41ebe891aca4bb4f6cae88/> move webview to wry
- <csr-id-e4cdb645aad800484b19ec35ba1f8bb9ccf71d12/> beaf up the use_state hook
- <csr-id-22e659c2bd7797ca5a822180aca0cb5d950c5287/> namespaced attributes
this commit adds namespaced attributes. This lets us support attribute groups, and thus, inline styles.
This namespaced attribute stuff is only available for styles at the moment, though it theoretically could be enabled for any other attributes.
- <csr-id-904b26f7111c3fc66400744ff6192e4b20bf6d74/> add edits back! and more webview support!
This commit adds a new type - the DomEdit - for serializing the changes made by the diffing machine. The architecture of how DomEdits fit into the cooperative scheduling is still TBD but it will allow us to build change lists without applying them immediately. This is more performant and allows us to only render parts of the page at a time.
This commit also adds more infrastructure around webview. Dioxus can now run on the web, generate static pages, run in the desktop, and run on mobile, with a large part of thanks to webview.
- <csr-id-b5e5ef171aa9f8986fb4ab04d793eb63f557c4ae/> two calculator examples
- <csr-id-7665f2c6cf05cea64bb9131381d4ac11cbdeb932/> move to slotmap
- <csr-id-f4fb5bb454536d9f108c7e276ce98a8924ab45e1/> integrate serialization and string borrowing
This commit adds lifetimes to the diff and realdom methods so consumers may borrow the contents of the DOM for serialization or asynchronous modifications.
- <csr-id-9d7ee79826a3b3fb952a70abcbb16dcd3363d2fb/> events work again!
- <csr-id-73047fe95678d50fcfd62a4ace7c6b406c5304e1/> props memoization is more powerful
This commit solves the memoization , properly memoizing properties that don't have any generic parameters. This is a rough heuristic to prevent non-static lifetimes from creeping into props and breaking our minual lifetime management.
Props that have a generic parameter are opted-out of the `partialeq` requirement and props *without* lifetimes must implement partialeq. We're going to leave manual disabling of memoization for future work.
- <csr-id-cfa0927cdd40bc3dba22996018605dbad91d0391/> todomvc
- <csr-id-d4f1ceaffbc0551ea3b179a101885275690cebec/> somewhat working with rc and weak
- <csr-id-89f22906926ccb5aa4cbe3fe1b6d834b06dd91e7/> dyn scope
- <csr-id-8329268d39238f030dd703ab4614253e58339e0b/> update builder
### Bug Fixes
- <csr-id-c439b0ac7e09f70a04262b7c29938d8c52197b76/> component pass thru events
- <csr-id-4aadec1e30e5e4aa86bdfa56d8fbff9dc7fa1c69/> ci and bug in setter
- <csr-id-2481cd05c2371b4a23b03d7710598708b1b2e491/> attempt to fix ice
- <csr-id-bd341f5571580cdf5e495379b49ca988fd9211c3/> tests
- <csr-id-75fa7b4aa672a8a10afcd11016a1b80e0e6f0f02/> make tests pass
- <csr-id-3dc0e59876f5aba88ed26f1bbd692820f239d4b0/> readme and examples syntax
- <csr-id-52c7154897111b570918127ffe3285bb1d5951a0/> really big bug around hooks
- <csr-id-868f6739d2b2c5f2ace0c5240cff8008901e818c/> keyword length
- <csr-id-a33f7701fcf5f917fea8719253650b5ad92554fd/> tags
- <csr-id-601078f9cf78a58d7502a377676ac94f3cf037bf/> desktop and mobile
- <csr-id-77686ba329cba0679d0339589a00df77d3c4c1a1/> it compiles again
- <csr-id-27d891934a70424b45e6278b7e2baaa2d1b78b35/> use annotation method from rust/58052 to fix closure lifetimes
- <csr-id-ba9e1dbb8fa24048a6c9ccef8a8722688226a845/> messed up how lifetimes worked, need to render once per component
- <csr-id-478255f40d4de1d2e3f3cc9b6d758b30ff394b39/> all the bugs!
### Performance
- <csr-id-8b3ac0b57ca073c1451e8d5df93882c9360ca52a/> remove global allocation for props
- <csr-id-ea91fc984dc648150acbafd0abb79d9f42aca500/> refcell to cell for hookidx
### Commit Statistics
<csr-read-only-do-not-edit/>
- 438 commits contributed to the release over the course of 358 calendar days.
- 416 commits where understood as [conventional](https://www.conventionalcommits.org).
- 0 issues like '(#ID)' where seen in commit messages
### Commit Details
<csr-read-only-do-not-edit/>
<details><summary>view details</summary>
* **Uncategorized**
- Release dioxus-core v0.1.7 ([`16d73b2`](https://github.comgit//DioxusLabs/dioxus/commit/16d73b240fc446c6f6996c19ccf52bbcda2eca79))
- add title to doc comment ([`d11f322`](https://github.comgit//DioxusLabs/dioxus/commit/d11f322f554e7dbf43b988c9cfda56498cc49872))
- Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307))
- better document the `EventHandler` type ([`be9f1a5`](https://github.comgit//DioxusLabs/dioxus/commit/be9f1a52ad2b04f101397ae34482ea7394df653b))
- component pass thru events ([`c439b0a`](https://github.comgit//DioxusLabs/dioxus/commit/c439b0ac7e09f70a04262b7c29938d8c52197b76))
- Merge pull request #74 from mrxiaozhuox/master ([`47056fd`](https://github.comgit//DioxusLabs/dioxus/commit/47056fda4577bcbdaa2a6f63d82eec876e5a5aee))
- Merge pull request #80 from DioxusLabs/jk/router2dotoh ([`cdc2d8e`](https://github.comgit//DioxusLabs/dioxus/commit/cdc2d8ec6d123245c2ea5f6d10af02b6a6833994))
- ci and bug in setter ([`4aadec1`](https://github.comgit//DioxusLabs/dioxus/commit/4aadec1e30e5e4aa86bdfa56d8fbff9dc7fa1c69))
- add prevent default attribute and upgrade router ([`427b126`](https://github.comgit//DioxusLabs/dioxus/commit/427b126bc17336d5d14d56eb7fddb8e07752495f))
- memoize dom in the prescence of identical components ([`cb2782b`](https://github.comgit//DioxusLabs/dioxus/commit/cb2782b4bb34cdaadfff590bfee930ae3ac6536c))
- make hydration more robust ([`bbb6ee1`](https://github.comgit//DioxusLabs/dioxus/commit/bbb6ee10de824f2e3259576ac01768640c884279))
- bump all versions ([`4f92ba4`](https://github.comgit//DioxusLabs/dioxus/commit/4f92ba41602d706449c1bddabd49829873ee72eb))
- attempt to fix ice ([`2481cd0`](https://github.comgit//DioxusLabs/dioxus/commit/2481cd05c2371b4a23b03d7710598708b1b2e491))
- tests ([`bd341f5`](https://github.comgit//DioxusLabs/dioxus/commit/bd341f5571580cdf5e495379b49ca988fd9211c3))
- update core, core-macro, and html ([`f9b9bb9`](https://github.comgit//DioxusLabs/dioxus/commit/f9b9bb9c0c2c55f55d2d6860e3d2d986debd6412))
- eanble bubbling ([`06276ed`](https://github.comgit//DioxusLabs/dioxus/commit/06276edd0d4f1f6f6b0a3bf7a467931413ab33c3))
- plug in bubbling ([`d84fc05`](https://github.comgit//DioxusLabs/dioxus/commit/d84fc0538670b2a3bda9ae41878896793b74e8ee))
- run cargo fmt ([`a95dead`](https://github.comgit//DioxusLabs/dioxus/commit/a95dead76d67646a862bb13421c2853cac7604ee))
- make tests pass ([`75fa7b4`](https://github.comgit//DioxusLabs/dioxus/commit/75fa7b4aa672a8a10afcd11016a1b80e0e6f0f02))
- remove runner on hook and then update docs ([`d156045`](https://github.comgit//DioxusLabs/dioxus/commit/d1560450bac55f9566e00e00ea405bd1c70b57e5))
- arbitrary expressions excepted without braces ([`4c85bcf`](https://github.comgit//DioxusLabs/dioxus/commit/4c85bcfdc84184b4fd0fb9317ba31fe569884890))
- polish some more things ([`1496102`](https://github.comgit//DioxusLabs/dioxus/commit/14961023f927b3a8bde83cfc7883aa8bfcca9e85))
- bump core version ([`ddfa2ba`](https://github.comgit//DioxusLabs/dioxus/commit/ddfa2bac3f598523e8d5508e7d99a7ff6fa11726))
- more API updates ([`a4f280d`](https://github.comgit//DioxusLabs/dioxus/commit/a4f280d16399205c638033bf9beb858e478e98ff))
- clean up examples and demo list ([`944b3a8`](https://github.comgit//DioxusLabs/dioxus/commit/944b3a8bc565da3142ebc8031f9c5fc565877bd8))
- upgrade hooks ([`b3ac2ee`](https://github.comgit//DioxusLabs/dioxus/commit/b3ac2ee3f76549cd1c7b6f9eee7e3382b07d873c))
- readme and examples syntax ([`3dc0e59`](https://github.comgit//DioxusLabs/dioxus/commit/3dc0e59876f5aba88ed26f1bbd692820f239d4b0))
- rip out unsafe task engine ([`c7d001c`](https://github.comgit//DioxusLabs/dioxus/commit/c7d001cbb457929b9742ad96c4997cdcc695bb1a))
- continue to consolidate ([`21e00c1`](https://github.comgit//DioxusLabs/dioxus/commit/21e00c114ea30ffd5d98d067103fe110b9c53c44))
- upgrade to new version of dioxus core. ([`cda759c`](https://github.comgit//DioxusLabs/dioxus/commit/cda759c659dfc4b1dde17e3896c35525005026df))
- clean it up a bit ([`fa106be`](https://github.comgit//DioxusLabs/dioxus/commit/fa106be1f5a45fa5707e66542e52c9f09e8cea7a))
- enable children properly ([`b997b8e`](https://github.comgit//DioxusLabs/dioxus/commit/b997b8ebbb82b5b9e9119bd2eb25335e2ed009d0))
- fix ssr ([`ded9696`](https://github.comgit//DioxusLabs/dioxus/commit/ded9696930ec825e0aba990494790e8be43a73e5))
- remove portals completely ([`2fd56e7`](https://github.comgit//DioxusLabs/dioxus/commit/2fd56e76192bc70d5503bfcd6b4127d383dd082c))
- some basic cleaning ([`f746793`](https://github.comgit//DioxusLabs/dioxus/commit/f746793890d091d3bca4b7c5261b8b2e2d080512))
- it works with a new bump each time!! ([`78d9056`](https://github.comgit//DioxusLabs/dioxus/commit/78d9056e35deb6e87ea2929cc5369e35b8a20847))
- miri stress tets ([`934de21`](https://github.comgit//DioxusLabs/dioxus/commit/934de21dd673b1b79904a3249998427f11428426))
- go back to noisy lifetime solution ([`8daf7a6`](https://github.comgit//DioxusLabs/dioxus/commit/8daf7a6ed86df72522b089aa2647eea7bee0f3b6))
- clean up the core crate ([`e6c6bbd`](https://github.comgit//DioxusLabs/dioxus/commit/e6c6bbdc1ec6a8c251b78c05ca104f006b6fad26))
- adjust memoization ([`e2e4d43`](https://github.comgit//DioxusLabs/dioxus/commit/e2e4d431e14e9e91b3301e994363c042400e687e))
- adjust semantics of placeholders and fragments ([`1c516ab`](https://github.comgit//DioxusLabs/dioxus/commit/1c516aba6a5dbe11c7a7162f3d65625e62461f03))
- rename fc to component ([`1e4a599`](https://github.comgit//DioxusLabs/dioxus/commit/1e4a599d14af85a2d1c29a442dd489f8dc8df321))
- docs ([`8814977`](https://github.comgit//DioxusLabs/dioxus/commit/8814977eeebe06748a3b9677a8070e42a037ebd7))
- polish ([`8bf57dc`](https://github.comgit//DioxusLabs/dioxus/commit/8bf57dc21dfbcbae5b95650203b68d3f41227652))
- prepare to change our fragment pattern. Add some more docs ([`2c3a046`](https://github.comgit//DioxusLabs/dioxus/commit/2c3a0464264fa11e8100df025d863931f9606cdb))
- update hooks ([`597a045`](https://github.comgit//DioxusLabs/dioxus/commit/597a0456f59872bd5dc60d382acdec76a98b1db2))
- really big bug around hooks ([`52c7154`](https://github.comgit//DioxusLabs/dioxus/commit/52c7154897111b570918127ffe3285bb1d5951a0))
- better desktop support ([`25a8411`](https://github.comgit//DioxusLabs/dioxus/commit/25a8411485e85bb7e3c8f20701d484529efe9a80))
- Release dioxus-core v0.1.3, dioxus-core-macro v0.1.2, dioxus-html v0.1.0, dioxus-desktop v0.0.0, dioxus-hooks v0.1.3, dioxus-liveview v0.1.0, dioxus-mobile v0.0.0, dioxus-router v0.1.0, dioxus-ssr v0.1.0, dioxus-web v0.0.0, dioxus v0.1.1 ([`2b92837`](https://github.comgit//DioxusLabs/dioxus/commit/2b928372fb1b74a4d4e220ff3d798bb7e52f79d2))
- move back to combined cx/props ([`96b3d56`](https://github.comgit//DioxusLabs/dioxus/commit/96b3d56e09a38843f7dc6c1047fe2a3923bd8473))
- rename ([`36d89be`](https://github.comgit//DioxusLabs/dioxus/commit/36d89beb34821694cb0afb546d3b0cb4e01aaae1))
- bubbling ([`19df1bd`](https://github.comgit//DioxusLabs/dioxus/commit/19df1bda109aba03c40ff631263bcb7035004ca0))
- it compiles once more ([`8acdd2e`](https://github.comgit//DioxusLabs/dioxus/commit/8acdd2ea830b995b608d8bac2ef527db8d40e662))
- some docs and suspense ([`93d4b8c`](https://github.comgit//DioxusLabs/dioxus/commit/93d4b8ca7c1b133e5dba2a8dc9a310dbe1357001))
- update readme ([`9bd56ee`](https://github.comgit//DioxusLabs/dioxus/commit/9bd56ee499dd8d3ce2b382655487d150e5a6cacf))
- should be functional across the boar ([`74c6211`](https://github.comgit//DioxusLabs/dioxus/commit/74c6211408ea0db5916a4e0973a3f9a7208faa44))
- move examples around ([`1e6e5e6`](https://github.comgit//DioxusLabs/dioxus/commit/1e6e5e611b61571f272289adefc9cdd7d59c4399))
- Release dioxus-core v0.1.3, dioxus-core-macro v0.1.2, dioxus-html v0.1.0, dioxus-desktop v0.0.0, dioxus-hooks v0.1.3, dioxus-liveview v0.1.0, dioxus-mobile v0.0.0, dioxus-router v0.1.0, dioxus-ssr v0.1.0, dioxus-web v0.0.0, dioxus v0.1.0 ([`0d480a4`](https://github.comgit//DioxusLabs/dioxus/commit/0d480a4c437d424f0eaff486e510a8fd3f3e6584))
- updates to router ([`bab21a0`](https://github.comgit//DioxusLabs/dioxus/commit/bab21a0aa1cbf8e6bd95f823e49f53c082e8d6cc))
- wire tasks up ([`55e6dd9`](https://github.comgit//DioxusLabs/dioxus/commit/55e6dd9701d3710ff17a25581b99b8f159e609b9))
- wire up linked nodes ([`c10c1f4`](https://github.comgit//DioxusLabs/dioxus/commit/c10c1f418bf21e0e54b53e2babeb37c672c06901))
- add router ([`d298b62`](https://github.comgit//DioxusLabs/dioxus/commit/d298b626d3ae21a39a8ec4426373369ac94edf9f))
- keyword length ([`868f673`](https://github.comgit//DioxusLabs/dioxus/commit/868f6739d2b2c5f2ace0c5240cff8008901e818c))
- docs and router ([`a5f05d7`](https://github.comgit//DioxusLabs/dioxus/commit/a5f05d73acc0e47b05cff64a373482519414bc7c))
- it properly bubbles ([`9d8c5ca`](https://github.comgit//DioxusLabs/dioxus/commit/9d8c5ca5ab5784b3f17d7ee20a451ee68fd703d6))
- upgrade syntax ([`fd93ee8`](https://github.comgit//DioxusLabs/dioxus/commit/fd93ee89c19b085a04307ef30217170518defa8e))
- more docs ([`9874f34`](https://github.comgit//DioxusLabs/dioxus/commit/9874f342e2bc1116410142c691545d75b0e0d6fe))
- strong improvmenets, first major section done ([`54d050c`](https://github.comgit//DioxusLabs/dioxus/commit/54d050cf897131ddc4f53651f1aeedaf03f23f57))
- Release dioxus-core v0.1.3, dioxus-core-macro v0.1.2, dioxus-html v0.1.0, dioxus-desktop v0.0.0, dioxus-hooks v0.1.3, dioxus-liveview v0.1.0, dioxus-mobile v0.0.0, dioxus-router v0.1.0, dioxus-ssr v0.1.0, dioxus-web v0.0.0, dioxus v0.1.0 ([`b32665d`](https://github.comgit//DioxusLabs/dioxus/commit/b32665d7212a5b9a3e21cb7af7abba63ae399fac))
- fake bubbling ([`11757dd`](https://github.comgit//DioxusLabs/dioxus/commit/11757ddf61e1decb1bd1c2bb30455d0bd01a3e95))
- remove scopechildren in favor of elements directly ([`574d7fd`](https://github.comgit//DioxusLabs/dioxus/commit/574d7fdb9e72a5254130589f13f9efd31e3d2870))
- remove send requirement on root props ([`5a21493`](https://github.comgit//DioxusLabs/dioxus/commit/5a21493fb7162287989bd7c0cd1c496cc3143605))
- tags ([`a33f770`](https://github.comgit//DioxusLabs/dioxus/commit/a33f7701fcf5f917fea8719253650b5ad92554fd))
- events bubble now ([`f223406`](https://github.comgit//DioxusLabs/dioxus/commit/f2234068ba7cd915a00a81e41660d7d6ee1177cc))
- move hooklist into scope ([`b9fc5fc`](https://github.comgit//DioxusLabs/dioxus/commit/b9fc5fc251b874fe4a9b5bc46718093029c07bad))
- Merge branch 'master' into jk/remove_node_safety ([`db00047`](https://github.comgit//DioxusLabs/dioxus/commit/db0004758c77331cc3b93ea8cf227c060028e12e))
- Release dioxus-core v0.1.3, dioxus-core-macro v0.1.2, dioxus-html v0.1.0, dioxus-desktop v0.0.0, dioxus-hooks v0.1.3, dioxus-liveview v0.1.0, dioxus-mobile v0.0.0, dioxus-router v0.1.0, dioxus-ssr v0.1.0, dioxus-web v0.0.0, dioxus v0.1.0 ([`3a706ac`](https://github.comgit//DioxusLabs/dioxus/commit/3a706ac4168db137723bea90d7a0058190adfc3c))
- bubbling reserves nodes ([`b6262ed`](https://github.comgit//DioxusLabs/dioxus/commit/b6262edd53767bf0d87ed1af3bb0c32612380a23))
- move testdom methods into virtualdom ([`d2f0547`](https://github.comgit//DioxusLabs/dioxus/commit/d2f0547692cb8b4594fbec1d9fa0934e169d1706))
- pull children out of component definition ([`2cf90b6`](https://github.comgit//DioxusLabs/dioxus/commit/2cf90b6903411e42f01a801f89037686194ee068))
- Merge branch 'master' of https://github.com/jkelleyrtp/dioxus ([`60d6eb2`](https://github.comgit//DioxusLabs/dioxus/commit/60d6eb204a10633e5e52f91e855bd12c5cda40f2))
- Various typos/grammar/rewording ([`5747e00`](https://github.comgit//DioxusLabs/dioxus/commit/5747e00b27b1b69c4f9c2820e7e78030feaff71e))
- update cargo tomls ([`e4c06ce`](https://github.comgit//DioxusLabs/dioxus/commit/e4c06ce8e893779d2aad0883a1bb27d193bc5985))
- bubbling in progress ([`a21020e`](https://github.comgit//DioxusLabs/dioxus/commit/a21020ea575e467ba0d608737269fe1b0792dba7))
- cleanup src ([`f2e343c`](https://github.comgit//DioxusLabs/dioxus/commit/f2e343c1548588ad01ddac6e39e5014132cff91c))
- move children onto scope ([`f438bbc`](https://github.comgit//DioxusLabs/dioxus/commit/f438bbcfd20be889aac9a9ca8486e5896af1ce14))
- desktop and mobile ([`601078f`](https://github.comgit//DioxusLabs/dioxus/commit/601078f9cf78a58d7502a377676ac94f3cf037bf))
- start on components ([`9b5f82a`](https://github.comgit//DioxusLabs/dioxus/commit/9b5f82af7dce18b75a781b86b188bc713860bb99))
- cleanuup ([`84fd0c6`](https://github.comgit//DioxusLabs/dioxus/commit/84fd0c616252bf29cd665782258530032b54d13a))
- Release dioxus-core v0.1.3, dioxus-core-macro v0.1.2, dioxus-html v0.1.0, dioxus-desktop v0.0.0, dioxus-hooks v0.1.3, dioxus-liveview v0.1.0, dioxus-mobile v0.0.0, dioxus-router v0.1.0, dioxus-ssr v0.1.0, dioxus-web v0.0.0, dioxus v0.1.0 ([`270dfc9`](https://github.comgit//DioxusLabs/dioxus/commit/270dfc9590b2354d083ea8da5cc0e1a1497d30e0))
- it compiles again ([`77686ba`](https://github.comgit//DioxusLabs/dioxus/commit/77686ba329cba0679d0339589a00df77d3c4c1a1))
- move debugging virtualdom into its own feature ([`d1b294f`](https://github.comgit//DioxusLabs/dioxus/commit/d1b294fff0c8618e50969ca59f198baf0565777d))
- clean up using new children syntax ([`9c13436`](https://github.comgit//DioxusLabs/dioxus/commit/9c1343610b3907bd8c6acadc086bfc9a8c7b3f3e))
- fix some bugs around the rsx macro ([`339e450`](https://github.comgit//DioxusLabs/dioxus/commit/339e450027b4a5d2e1317e13863cd1b2e7ab5853))
- full html support ([`79503f1`](https://github.comgit//DioxusLabs/dioxus/commit/79503f15c5db04fa04575c8735941a2e3a75030b))
- slim deps and upgrade docs ([`83dd49d`](https://github.comgit//DioxusLabs/dioxus/commit/83dd49d89044b618fa371e09f9f34ea28b9fd529))
- work on scheduler, async, coroutines, and merge scope into context ([`b56ea6c`](https://github.comgit//DioxusLabs/dioxus/commit/b56ea6c9a99cd747f376aca4a3c439c13f082714))
- implement dst magic ([`d4192d0`](https://github.comgit//DioxusLabs/dioxus/commit/d4192d0de2fb352d31f33f2fadfc21de0b1b950c))
- update local examples and docs to support new syntaxes ([`4de16c4`](https://github.comgit//DioxusLabs/dioxus/commit/4de16c4779648e591b3869b5df31271ae603c812))
- working on re-enabling components ([`fffc7ea`](https://github.comgit//DioxusLabs/dioxus/commit/fffc7ea061922120cd2288982a310a87691948cd))
- a few things left, slme cleanup ([`289d2f2`](https://github.comgit//DioxusLabs/dioxus/commit/289d2f2518cef937a6fc823495a8bd32f287b84e))
- implement lazy nodes properly ([`3e07214`](https://github.comgit//DioxusLabs/dioxus/commit/3e07214272f20a47b168d6431fcb39880cd41a9d))
- fix table ([`e495b09`](https://github.comgit//DioxusLabs/dioxus/commit/e495b09bf1c99950b5ea8647b371f296dc64afbe))
- fix rebuild ([`15c31d5`](https://github.comgit//DioxusLabs/dioxus/commit/15c31d545a84f05fe17254009dc275aac93e7574))
- dst drops properly ([`f33510b`](https://github.comgit//DioxusLabs/dioxus/commit/f33510b13fa515e496e6a934650c5a7c61838a39))
- remove cx.children. start to move towards a "children" field ([`7b97e00`](https://github.comgit//DioxusLabs/dioxus/commit/7b97e009ec74cd2f134bbb8e576404e7a964db74))
- clean up, use old approach to components ([`2559740`](https://github.comgit//DioxusLabs/dioxus/commit/2559740463d298d27a058607c5d80196dc736c11))
- dst nodes leak but work ([`62ed920`](https://github.comgit//DioxusLabs/dioxus/commit/62ed9208a44c7aa038613f73ff0573bd2010f4d0))
- improve FC handling to allow lifetimes of parent ([`6b2645f`](https://github.comgit//DioxusLabs/dioxus/commit/6b2645fd80ab20153e7bb51887443e994fbab5cb))
- improve safety ([`fda2ebc`](https://github.comgit//DioxusLabs/dioxus/commit/fda2ebc2a22965845e015384f39f34ce7cb3e428))
- move more stuff out ([`464b457`](https://github.comgit//DioxusLabs/dioxus/commit/464b457b80a40c0ff24386342a0e81ecfeac4723))
- try to get lifetimes passed in properly ([`f70b5b4`](https://github.comgit//DioxusLabs/dioxus/commit/f70b5b4e4acf589372f769153968922d2fcfedf1))
- listen to clippy ([`f492443`](https://github.comgit//DioxusLabs/dioxus/commit/f49244344c06a6d05edfe041fd3afa59ef89ca21))
- remove bumpframe ([`e4cda7c`](https://github.comgit//DioxusLabs/dioxus/commit/e4cda7c2cbba536b045fa7bed1263b78ed33c096))
- use annotation method from rust/58052 to fix closure lifetimes ([`27d8919`](https://github.comgit//DioxusLabs/dioxus/commit/27d891934a70424b45e6278b7e2baaa2d1b78b35))
- it compiles cleanly ([`fd0c384`](https://github.comgit//DioxusLabs/dioxus/commit/fd0c384daccf89cec408e3ad674ea04c00797eae))
- worked backwards a bit and got it slightly figured out ([`9ee2bfb`](https://github.comgit//DioxusLabs/dioxus/commit/9ee2bfb010ce90ec97e93e173c31aab281db32c4))
- massage lifetimes ([`9726a06`](https://github.comgit//DioxusLabs/dioxus/commit/9726a065b0d4fb1ede5b53a2ddd58c855e51539f))
- book documentation ([`16dbf4a`](https://github.comgit//DioxusLabs/dioxus/commit/16dbf4a6f84103857385fb4b142a718b0ce72118))
- more changes to scheduler ([`059294a`](https://github.comgit//DioxusLabs/dioxus/commit/059294ab55e9e945c9aede1fd4b4faf39a7b9ea9))
- messed up how lifetimes worked, need to render once per component ([`ba9e1db`](https://github.comgit//DioxusLabs/dioxus/commit/ba9e1dbb8fa24048a6c9ccef8a8722688226a845))
- major cleanups to scheduler ([`2933e4b`](https://github.comgit//DioxusLabs/dioxus/commit/2933e4bc11b3074c2bde8d76ec55364fca841988))
- move everything over to a stack dst ([`0e9d5fc`](https://github.comgit//DioxusLabs/dioxus/commit/0e9d5fc5306ab508d5af6999a4064f9b8b48460f))
- event system ([`1f22a06`](https://github.comgit//DioxusLabs/dioxus/commit/1f22a06a36f72073188b8c9009dd4950b3f4ff9a))
- no more lifetimes ([`d96276a`](https://github.comgit//DioxusLabs/dioxus/commit/d96276a715a9f62943909cfc429c9fe47fd28146))
- support desktop more completely ([`efd0e9b`](https://github.comgit//DioxusLabs/dioxus/commit/efd0e9b5648c809057f339083ba9d454f810d483))
- compiling again with runtime safety ([`857c92f`](https://github.comgit//DioxusLabs/dioxus/commit/857c92f7f096108317b1854715b4dcbeaae6f759))
- add update functionality to useref ([`a2b0c50`](https://github.comgit//DioxusLabs/dioxus/commit/a2b0c50a343005c63c7032bcefb8323b78350bb9))
- more raw ptrs ([`95bd17e`](https://github.comgit//DioxusLabs/dioxus/commit/95bd17e38fc936dcc9383d0ba8beac5ed64b41eb))
- lnks to projects ([`460783a`](https://github.comgit//DioxusLabs/dioxus/commit/460783ad549818a85db634ed9c39ffce210b98ec))
- desktop functioning well ([`5502429`](https://github.comgit//DioxusLabs/dioxus/commit/5502429626023d0788cca352e94ac6ea67c2cb11))
- remove resource pool in favor of raw ptrs ([`b8d9869`](https://github.comgit//DioxusLabs/dioxus/commit/b8d98698c6056a330fe62f52833504fb1c728bbb))
- overhaul event system ([`7a03c1d`](https://github.comgit//DioxusLabs/dioxus/commit/7a03c1d2b48590276b182465679387655fe08f3a))
- consolidation, simplification ([`70b983d`](https://github.comgit//DioxusLabs/dioxus/commit/70b983d0909a332d357da9bb07adc263059bce19))
- threadsafe ([`82953f2`](https://github.comgit//DioxusLabs/dioxus/commit/82953f2ac37913f83a822333acd0c47e20777d31))
- all the bugs! ([`478255f`](https://github.comgit//DioxusLabs/dioxus/commit/478255f40d4de1d2e3f3cc9b6d758b30ff394b39))
- ssr ([`71f0df6`](https://github.comgit//DioxusLabs/dioxus/commit/71f0df63745fe5c17468693144c552ea3a0a7101))
- shared state mechanisms ([`4a4c7af`](https://github.comgit//DioxusLabs/dioxus/commit/4a4c7afca7e1beadd4b213214074fdb420eb0923))
- fix typo ([`4a5041c`](https://github.comgit//DioxusLabs/dioxus/commit/4a5041c5902e1dab99f25ce00d754ac8a5ee30c1))
- fix web list issue ([`da4423c`](https://github.comgit//DioxusLabs/dioxus/commit/da4423c141f1f376df5f3f2580e5284831744a7e))
- move macro crate out of core ([`7bdad1e`](https://github.comgit//DioxusLabs/dioxus/commit/7bdad1e2e6f67e74c9f67dde2150140cf8a090e8))
- portals ([`4a0bb8c`](https://github.comgit//DioxusLabs/dioxus/commit/4a0bb8cf84da7960d7090de3d5f95248318540a3))
- polish ([`a5f82be`](https://github.comgit//DioxusLabs/dioxus/commit/a5f82be433309790e0cac07a77e59c996a53796d))
- exampels ([`04d16b4`](https://github.comgit//DioxusLabs/dioxus/commit/04d16b41dc9bc06ca0320a9fec27744318e48355))
- fix some event stuff for web and core ([`725b4a1`](https://github.comgit//DioxusLabs/dioxus/commit/725b4a1d7f5d629b1b0a163b65bfd93b2f8a151b))
- garbage collection ([`dfe9f86`](https://github.comgit//DioxusLabs/dioxus/commit/dfe9f86bfff48ae3166fd41d673245ed142c26f0))
- on collaborative scheduling ([`1a32383`](https://github.comgit//DioxusLabs/dioxus/commit/1a323835c8c4f667e5744bbf5447776f1dc51fca))
- examples ([`1a2f91e`](https://github.comgit//DioxusLabs/dioxus/commit/1a2f91ed91c13dae553ecde585462ab261b1b95d))
- working down warnings ([`958c02a`](https://github.comgit//DioxusLabs/dioxus/commit/958c02a26e86b07431d3e5f51f00afe116656981))
- mutations ([`fac4233`](https://github.comgit//DioxusLabs/dioxus/commit/fac42339c272b0e430ebf4f31b6061a0635d3e19))
- performance looks good, needs more testing ([`4b6ca05`](https://github.comgit//DioxusLabs/dioxus/commit/4b6ca05f2c3ad647842c858967da9c87f1915825))
- add test_dom ([`a652090`](https://github.comgit//DioxusLabs/dioxus/commit/a652090dc5708db334fa7430fededb1bac207880))
- bottom up dropping ([`f2334c1`](https://github.comgit//DioxusLabs/dioxus/commit/f2334c17be2612d926361686d7d40a57e3ffe9b9))
- cleanup ([`1745a44`](https://github.comgit//DioxusLabs/dioxus/commit/1745a44d949b994b64ea1fb715cbe36963ae7027))
- more scheduler work ([`44b4384`](https://github.comgit//DioxusLabs/dioxus/commit/44b43849613a52cee8dc98ea3068095416b201c2))
- re-enable suspense ([`687cda1`](https://github.comgit//DioxusLabs/dioxus/commit/687cda1b6d9595357d1dc8720ebe921f61098d8f))
- fill out the snippets ([`6051b0e`](https://github.comgit//DioxusLabs/dioxus/commit/6051b0ec86927704451f4ce6cdf8f988e59702ae))
- amazingly awesome error handling ([`4a72b31`](https://github.comgit//DioxusLabs/dioxus/commit/4a72b3140bd244da602deada1eeecded65ff5848))
- some ideas ([`05c909f`](https://github.comgit//DioxusLabs/dioxus/commit/05c909f320765aec1bf4c1c55ca59ffd5525a2c7))
- proper handling of events ([`d7940aa`](https://github.comgit//DioxusLabs/dioxus/commit/d7940aa2ac2017316d62e0f2eac0701dc6ad1f09))
- more work on scheduler ([`1cd5e69`](https://github.comgit//DioxusLabs/dioxus/commit/1cd5e697128877e911ece2c9b34ec38d74eeec80))
- websys dom working properly ([`cfa0247`](https://github.comgit//DioxusLabs/dioxus/commit/cfa0247cbb1233e1df275374a73f431650a9250f))
- big updates to the reference ([`583fdfa`](https://github.comgit//DioxusLabs/dioxus/commit/583fdfa5618e11d660985b97e570d4503be2ff49))
- fix invalid is_empty bug ([`c7d2d9d`](https://github.comgit//DioxusLabs/dioxus/commit/c7d2d9d050548f06244caeeedc2c0e0e52eb329f))
- more work on rebuild ([`5c3dc0a`](https://github.comgit//DioxusLabs/dioxus/commit/5c3dc0a8741df251dd3cb7f954fae2243df155f7))
- keyed diffing!! ([`d717c22`](https://github.comgit//DioxusLabs/dioxus/commit/d717c22d9c5da7b6f343fede11faaf953a3a29e0))
- docs, html! macro, more ([`caf772c`](https://github.comgit//DioxusLabs/dioxus/commit/caf772cf249d2f56c8d0b0fa2737ad48e32c6e82))
- update root props ([`fac2e56`](https://github.comgit//DioxusLabs/dioxus/commit/fac2e56ed63fc708a89e14df15f1fc32f58391a9))
- get keyed diffing compiling ([`0a0be95`](https://github.comgit//DioxusLabs/dioxus/commit/0a0be95c3e58dc065409f02f703b82700c1003f8))
- changes to scheduler ([`098d382`](https://github.comgit//DioxusLabs/dioxus/commit/098d3821ed89ad38d99077a6556b48a7e91fc3fc))
- some docs, cleaning ([`c321532`](https://github.comgit//DioxusLabs/dioxus/commit/c321532a6cef40b2d2e4adc8c7a55931b6755b08))
- more and more sophisticated diff ([`1749eba`](https://github.comgit//DioxusLabs/dioxus/commit/1749eba8eb6c8ffa38ab3ad52160b637fe021e86))
- clean up warnings ([`b32e261`](https://github.comgit//DioxusLabs/dioxus/commit/b32e2611e37b17c2371ffb10cf1ac647f017d917))
- a new vnode type for anchors ([`d618092`](https://github.comgit//DioxusLabs/dioxus/commit/d618092e9d150589e61516f7bbb169f2db49d3f2))
- more cleanup ([`5cbdc57`](https://github.comgit//DioxusLabs/dioxus/commit/5cbdc571e3abe54391aec8f296d7389410b373b6))
- more work on bumpframe ([`63a85e4`](https://github.comgit//DioxusLabs/dioxus/commit/63a85e4865929dbd32a1b72ebeac271561e9ed92))
- heuristics engine ([`6aaad1c`](https://github.comgit//DioxusLabs/dioxus/commit/6aaad1c9ef97fa407b660aec60999d263c172ecc))
- more cleanup in scheduler ([`9f99f46`](https://github.comgit//DioxusLabs/dioxus/commit/9f99f46cfd6355ce46afdf84d60dd049047c6b57))
- making progress on diffing and hydration ([`49856cc`](https://github.comgit//DioxusLabs/dioxus/commit/49856ccd6865f88d63765f26d27f7e945b554da0))
- code quality improvements for core ([`00231ad`](https://github.comgit//DioxusLabs/dioxus/commit/00231adfa2e1d67a9d7ae2fa61c33e3a22d51978))
- mvoe away from compound context ([`a2c7d17`](https://github.comgit//DioxusLabs/dioxus/commit/a2c7d17b0595769f60bc1c2bbf7cbe32cec37486))
- examples ([`f1cff84`](https://github.comgit//DioxusLabs/dioxus/commit/f1cff845ce11231cb4b2dd4857f9ca9f0265b925))
- wire up resource pool ([`31702db`](https://github.comgit//DioxusLabs/dioxus/commit/31702dbf878dd0207d101f7869ebefd2bb9f6860))
- make hooks free-functions ([`e5c88fe`](https://github.comgit//DioxusLabs/dioxus/commit/e5c88fe3a49649ecb308decd14c2557963978619))
- more work on suspense and documentation ([`37ed4be`](https://github.comgit//DioxusLabs/dioxus/commit/37ed4bed8cf28eb65465d41e15e8d758cb3d9679))
- transition to cx, props ([`bfdcb20`](https://github.comgit//DioxusLabs/dioxus/commit/bfdcb20437c385116fd640e46b6361d09d8d5ff8))
- more doc ([`adf202e`](https://github.comgit//DioxusLabs/dioxus/commit/adf202eab9201c69ec455e621c755500329815fe))
- cleanup public apis ([`927b05f`](https://github.comgit//DioxusLabs/dioxus/commit/927b05f358221aa4b9c440608704e86c2400ab64))
- omg what a dumb mistake ([`f782e14`](https://github.comgit//DioxusLabs/dioxus/commit/f782e142118fb7acf1b88a0f3fbb03e4a5e3e91e))
- suspense! ([`4837d8e`](https://github.comgit//DioxusLabs/dioxus/commit/4837d8e741343d26f31b55e4478a374dc761e538))
- refactor ([`8b0eb87`](https://github.comgit//DioxusLabs/dioxus/commit/8b0eb87c72ea9d444dee99a8b05643f19fea2634))
- bless up, no more segfaults ([`4a0068f`](https://github.comgit//DioxusLabs/dioxus/commit/4a0068f09918adbc299150edcf777f342ced0dd3))
- more suspended nodes! ([`de9f61b`](https://github.comgit//DioxusLabs/dioxus/commit/de9f61bcf48c0d6e35e46c337b72a713c9f9f7d2))
- more docs ([`34c3107`](https://github.comgit//DioxusLabs/dioxus/commit/34c3107418df89620cb9dfbd9300f89a1fe6e68f))
- algo for scheduler finished ([`9ad5e49`](https://github.comgit//DioxusLabs/dioxus/commit/9ad5e4965409464c6ca63b3434bbe182dde26943))
- wire up event delegator for webview ([`7dfe89c`](https://github.comgit//DioxusLabs/dioxus/commit/7dfe89c9581f45a445f17f9fe4bb94e61f67e971))
- basic support for scheduled rendering ([`c52af22`](https://github.comgit//DioxusLabs/dioxus/commit/c52af221f755601a9e826ffc2c355def138999d0))
- feeling goodish about scheduler ([`5f63eda`](https://github.comgit//DioxusLabs/dioxus/commit/5f63eda29473b82ce708ec66e4dd862c3717e958))
- fix some lifetime issues ([`0a907b3`](https://github.comgit//DioxusLabs/dioxus/commit/0a907b35c647b67172080023462770b761364d51))
- move over to push based mechanism ([`80e6c25`](https://github.comgit//DioxusLabs/dioxus/commit/80e6c256980eb3e8c32e30f3dbb43c8b3b9a9cf4))
- bubbling ([`7978a17`](https://github.comgit//DioxusLabs/dioxus/commit/7978a17419be0c1e6a7ddd2c21cfa96b9660038b))
- some structure ([`bcaa76a`](https://github.comgit//DioxusLabs/dioxus/commit/bcaa76a37fb7ff48ab3345c594a20fbdcda6074a))
- solve some issues regarding listeners ([`dfaf5ad`](https://github.comgit//DioxusLabs/dioxus/commit/dfaf5adee164f44a679ab21d730caaab3610e01f))
- architecture document and edit list ([`e723876`](https://github.comgit//DioxusLabs/dioxus/commit/e7238762ae518c5688f9339d11832d17f99ad553))
- before scheduler simplify ([`c16b92e`](https://github.comgit//DioxusLabs/dioxus/commit/c16b92ee7ee3a02153d651db1a84c7dc781c26da))
- considering making vdom passed verywehre ([`ac3a7b1`](https://github.comgit//DioxusLabs/dioxus/commit/ac3a7b1fbc1665482354ddf4f0de3d6ceb5b400a))
- move to slab ([`6084fbc`](https://github.comgit//DioxusLabs/dioxus/commit/6084fbcd11d1669db135098d920488e2c01a7014))
- change in cx to cx ([`9971ff2`](https://github.comgit//DioxusLabs/dioxus/commit/9971ff215db6f771b7ec1cae2517c85d47d38622))
- more work on scheduler ([`d4d7114`](https://github.comgit//DioxusLabs/dioxus/commit/d4d7114bebe09a5e265a29105a9bdf3b369ecd67))
- move things into a "shared" object ([`f644d7c`](https://github.comgit//DioxusLabs/dioxus/commit/f644d7c44159eef091552dcc90acbb151ea76b21))
- reduce warnings ([`0ded32e`](https://github.comgit//DioxusLabs/dioxus/commit/0ded32e184a4b48b848b078585fbe1ca61d09b1d))
- it compiles again ([`07efb6a`](https://github.comgit//DioxusLabs/dioxus/commit/07efb6a1b656b24eb1ff25c62f84bb529df936b2))
- task system works ([`3a57b94`](https://github.comgit//DioxusLabs/dioxus/commit/3a57b942624afb8aa6650aeee05466c3c9ce967e))
- more polish on display ([`12afd2b`](https://github.comgit//DioxusLabs/dioxus/commit/12afd2b0f24c6968c1498fcedebb03a06e9bc135))
- more overhaul on virtualevents ([`41cc429`](https://github.comgit//DioxusLabs/dioxus/commit/41cc42919d42453f8f2560aa852211364af4ad3d))
- cargo fix ([`84c3e9f`](https://github.comgit//DioxusLabs/dioxus/commit/84c3e9fcb11bab921818fbb090632a54dc16423f))
- more polish on scopes ([`2386772`](https://github.comgit//DioxusLabs/dioxus/commit/238677208ea93d39e26b17af0c17c4f0b512d007))
- more work on priority ([`cef116a`](https://github.comgit//DioxusLabs/dioxus/commit/cef116aa3a435857c9a3a818bcd81de8ef03a771))
- cargo fix ([`beceda5`](https://github.comgit//DioxusLabs/dioxus/commit/beceda511c514d743bce17ed19df9dba8675fcda))
- polish up some safety stuff and add suspense support in ([`ff1398b`](https://github.comgit//DioxusLabs/dioxus/commit/ff1398b943de14f7573ad13577e83500ed9c146e))
- refactor to fix some ownership quirks ([`05e960b`](https://github.comgit//DioxusLabs/dioxus/commit/05e960b6b037d237ddcaf4fef4d33179883b6644))
- cargo fix ([`fd03b9d`](https://github.comgit//DioxusLabs/dioxus/commit/fd03b9d252c9e57a281557fbe91786fba2f28e3b))
- remove unnecessary import ([`fb0c6d2`](https://github.comgit//DioxusLabs/dioxus/commit/fb0c6d2ea4d1edbf1cec4c78179395114071a520))
- fix props update ([`79e8df2`](https://github.comgit//DioxusLabs/dioxus/commit/79e8df26f5a84f5d765921ae307b33db83c14d4e))
- more work on web ([`3bf19d8`](https://github.comgit//DioxusLabs/dioxus/commit/3bf19d8106e0cca7749b7346e9b0adfce654d782))
- cargo fix ([`c7ca7c2`](https://github.comgit//DioxusLabs/dioxus/commit/c7ca7c227325dee85fec0f1bc7d8ab00cbf68f66))
- more clean up ([`5a09ec1`](https://github.comgit//DioxusLabs/dioxus/commit/5a09ec161031abaa01582435839a8077fd80b228))
- ricraf polyfill ([`59219b9`](https://github.comgit//DioxusLabs/dioxus/commit/59219b9ef22caa5896046ccaabc8a225cc4b5a53))
- cut down on errors ([`775e9e2`](https://github.comgit//DioxusLabs/dioxus/commit/775e9e29b8cde45a13bdae22a6f54b25518e7192))
- ....sigh..... so the diffing algorithm is robust ([`68ed1c0`](https://github.comgit//DioxusLabs/dioxus/commit/68ed1c04e7e773f9e6c0a5148f0ea89b97b6784e))
- remove global allocation for props ([`8b3ac0b`](https://github.comgit//DioxusLabs/dioxus/commit/8b3ac0b57ca073c1451e8d5df93882c9360ca52a))
- ric_raf wired up ([`8b0d04c`](https://github.comgit//DioxusLabs/dioxus/commit/8b0d04ce585596f561f15e92fc087e2c7063eb07))
- wip ([`996247a`](https://github.comgit//DioxusLabs/dioxus/commit/996247a1644d91ebb00e2b2188d21cacdc48257b))
- lots of changes to diffing ([`ff0a3d1`](https://github.comgit//DioxusLabs/dioxus/commit/ff0a3d1c83a4349a228a742fccc761822f1711d3))
- change around how deadline works ([`369b36b`](https://github.comgit//DioxusLabs/dioxus/commit/369b36b2c64a4bc3d4895f1e04b412c4edbf3c99))
- rebuild doesn't return errors ([`f457b71`](https://github.comgit//DioxusLabs/dioxus/commit/f457b7113129479cad577237ef21cb735fffe483))
- add aria ([`4091846`](https://github.comgit//DioxusLabs/dioxus/commit/4091846934b4b3b2bc03d3ca8aaf7712aebd4e36))
- more examples ([`56e7eb8`](https://github.comgit//DioxusLabs/dioxus/commit/56e7eb83a97ebd6d5bcd23464cfb9d718e5ac26d))
- tests and documentation ([`da81591`](https://github.comgit//DioxusLabs/dioxus/commit/da8159190b84d13caa9dfe7dea385a809cf84199))
- buff up html allowed attributes ([`c79d9ae`](https://github.comgit//DioxusLabs/dioxus/commit/c79d9ae674e235c8e9c2c069d24902122b9c7464))
- move examples around ([`304259d`](https://github.comgit//DioxusLabs/dioxus/commit/304259d8186d1d34224a74c95f4fd7d14126b499))
- it works but the page is backwards ([`cdcd861`](https://github.comgit//DioxusLabs/dioxus/commit/cdcd8611e87ffb5e24de7b9fe6c656af3053276e))
- use the new structure ([`a05047d`](https://github.comgit//DioxusLabs/dioxus/commit/a05047d01e606425fb0d6595e9d27d3e15f32050))
- ssr + tide ([`269e81b`](https://github.comgit//DioxusLabs/dioxus/commit/269e81b0fdb32ae0706160cd278cf3a1b731387b))
- more test ([`a0a977a`](https://github.comgit//DioxusLabs/dioxus/commit/a0a977a12d4008155fe332ee226f04e5f59acf95))
- enable components in ssr ([`bbcb5a0`](https://github.comgit//DioxusLabs/dioxus/commit/bbcb5a0234dbce48ffeb64903c3ec04562a87ad6))
- documentation and badges ([`3f68be7`](https://github.comgit//DioxusLabs/dioxus/commit/3f68be79ae326f6d169ee771bf86490aed4d7885))
- keyed diffing ([`0479252`](https://github.comgit//DioxusLabs/dioxus/commit/0479252a5fba96e554dcf2422a8566e248fc6593))
- static node infrastructure and ssr changes ([`9abb047`](https://github.comgit//DioxusLabs/dioxus/commit/9abb0470b7869019d539a2fc21da3872348ae38b))
- naming ([`1a6b238`](https://github.comgit//DioxusLabs/dioxus/commit/1a6b2388299bf005006e23b75c65a09449049f8d))
- it works? ([`778baff`](https://github.comgit//DioxusLabs/dioxus/commit/778baffb10c2b14ac350b38229d1b3acfa49ec39))
- more refactor for async ([`975fa56`](https://github.comgit//DioxusLabs/dioxus/commit/975fa566f9809f8fa2bb0bdb07fbfc7f855dcaeb))
- refactor non_keyed diff ([`082765f`](https://github.comgit//DioxusLabs/dioxus/commit/082765f6873d9ba72d67e3383dfab6c901eb772a))
- some project refactor ([`8cfc437`](https://github.comgit//DioxusLabs/dioxus/commit/8cfc437bfe4110d7f984428f01df90bdf8f8d9ec))
- tests and benchmarks ([`e6f5656`](https://github.comgit//DioxusLabs/dioxus/commit/e6f56563bc84516e017b3db06f11fab2549b9a50))
- some work ([`feab50f`](https://github.comgit//DioxusLabs/dioxus/commit/feab50f24a8ab82a2cf1d02af1c54d13dd539b01))
- structure coming together and tests ([`2d541ec`](https://github.comgit//DioxusLabs/dioxus/commit/2d541eca640d15bb56bccb4191b1dc0fe02dfc22))
- more refactor ([`58ab51a`](https://github.comgit//DioxusLabs/dioxus/commit/58ab51a4e4e8a2a7b9b03a4c9a136648112d39dc))
- compiles again ([`16bfc6d`](https://github.comgit//DioxusLabs/dioxus/commit/16bfc6d2488f7ec0266ff5b174142232fb04849f))
- more refactor ([`c811a89`](https://github.comgit//DioxusLabs/dioxus/commit/c811a8982c08a778f1484ed8aa1dd6b991790086))
- dedicated mutations module ([`db6d018`](https://github.comgit//DioxusLabs/dioxus/commit/db6d0184aa5aa612bd7229a06f72b4c2ec5f6409))
- refactor ([`9276fd7`](https://github.comgit//DioxusLabs/dioxus/commit/9276fd7db78e48c530b8f5dbc317be2d8d51ad7f))
- basic creates working properly ([`1622f48`](https://github.comgit//DioxusLabs/dioxus/commit/1622f484b34412110723645cffc4c8c286b6ca80))
- refactor out the hooks implementation ([`1cc1679`](https://github.comgit//DioxusLabs/dioxus/commit/1cc1679a6ba33ff68d45ad1e964b73230224bc23))
- tests added for new iterative ([`24572b6`](https://github.comgit//DioxusLabs/dioxus/commit/24572b63deec590f810df0b95a12cbd5b0bd61f5))
- webview and async ([`eb82051`](https://github.comgit//DioxusLabs/dioxus/commit/eb82051000fe8772b97f879195fc412092228023))
- fix compiling ([`2ce0752`](https://github.comgit//DioxusLabs/dioxus/commit/2ce0752a9cd253fee1c8b205400aa191b09c9dcb))
- move webview to wry ([`99d94b6`](https://github.comgit//DioxusLabs/dioxus/commit/99d94b69aba192be7e41ebe891aca4bb4f6cae88))
- moving over instructions ([`0987760`](https://github.comgit//DioxusLabs/dioxus/commit/098776095844013469f68c0b585f75587c0edd75))
- more examples ([`2547da3`](https://github.comgit//DioxusLabs/dioxus/commit/2547da36a089f09d219e7d701efd35f74f66784c))
- remove old create ([`7b06820`](https://github.comgit//DioxusLabs/dioxus/commit/7b068202cea3e513cf885826f303df7e4f707afd))
- beaf up the use_state hook ([`e4cdb64`](https://github.comgit//DioxusLabs/dioxus/commit/e4cdb645aad800484b19ec35ba1f8bb9ccf71d12))
- back to vnode enum ([`64f289a`](https://github.comgit//DioxusLabs/dioxus/commit/64f289a61c18b6b4c1adf785a864171e51615780))
- move from recursive to iterative ([`9652ccd`](https://github.comgit//DioxusLabs/dioxus/commit/9652ccdcf19189e2aac9262bcfa30e2b04c36d37))
- move CLI into its own "studio" app ([`fd79335`](https://github.comgit//DioxusLabs/dioxus/commit/fd7933561fe81922e4d5d77f6ac3b6f19efb5a90))
- working on async diff ([`f41cff5`](https://github.comgit//DioxusLabs/dioxus/commit/f41cff571fd11bb00820c0bf9d147e81ed5a6a53))
- more work on scheduler ([`882d695`](https://github.comgit//DioxusLabs/dioxus/commit/882d69571dcc807a5fa3734f14980e5233f895f5))
- move some examples around ([`98a0933`](https://github.comgit//DioxusLabs/dioxus/commit/98a09339fd3190799ea4dd316908f0a53fdf2413))
- scheduler skeleton ([`9d14faf`](https://github.comgit//DioxusLabs/dioxus/commit/9d14faf62c654e6958b5e5dc444374146977f52c))
- remove the scoped trait ([`cca7c5f`](https://github.comgit//DioxusLabs/dioxus/commit/cca7c5fc3a9e341996e2d5a23b981b658e54ea62))
- more work on deadline, support immediates ([`5965bee`](https://github.comgit//DioxusLabs/dioxus/commit/5965bee1d143ee50ed24d4bde5176e057bce4363))
- move the functions around a bit ([`36542b4`](https://github.comgit//DioxusLabs/dioxus/commit/36542b482f33e5abd58d3d2c40250f6ffac1a99d))
- got the structure figured out! ([`97745c6`](https://github.comgit//DioxusLabs/dioxus/commit/97745c6a7f2094deca32c7a1c30890853067848e))
- fix issues with lifetimes ([`a38a81e`](https://github.comgit//DioxusLabs/dioxus/commit/a38a81e1290375cae685f7c49d3745e4298fab26))
- close on putting it all together ([`85e2dc2`](https://github.comgit//DioxusLabs/dioxus/commit/85e2dc259aba38ec1654ab7581418aaa9c1c543c))
- namespaced attributes ([`22e659c`](https://github.comgit//DioxusLabs/dioxus/commit/22e659c2bd7797ca5a822180aca0cb5d950c5287))
- groundwork for noderefs ([`c1afeba`](https://github.comgit//DioxusLabs/dioxus/commit/c1afeba1efb1a063705466a14648beee08cacb86))
- more work on jank free ([`a44e9fc`](https://github.comgit//DioxusLabs/dioxus/commit/a44e9fcffa4da6dd01966a62575ff4f4832e5fcc))
- more examples ([`11f89e5`](https://github.comgit//DioxusLabs/dioxus/commit/11f89e5d338d14a7aeece0a6275c24ae65913ce7))
- add edits back! and more webview support! ([`904b26f`](https://github.comgit//DioxusLabs/dioxus/commit/904b26f7111c3fc66400744ff6192e4b20bf6d74))
- enable more diffing ([`e8f29a8`](https://github.comgit//DioxusLabs/dioxus/commit/e8f29a8f8ac56020bee0048021efa52547307a77))
- two calculator examples ([`b5e5ef1`](https://github.comgit//DioxusLabs/dioxus/commit/b5e5ef171aa9f8986fb4ab04d793eb63f557c4ae))
- examples ([`d9e6d09`](https://github.comgit//DioxusLabs/dioxus/commit/d9e6d0925b30690212d1d690dfba288f1a694a27))
- wip ([`952a91d`](https://github.comgit//DioxusLabs/dioxus/commit/952a91d5408aaf789b496f11d01c3b3f7fcf9059))
- integrate signals ([`93900aa`](https://github.comgit//DioxusLabs/dioxus/commit/93900aac4443db2e63631ed928294af74201a9ff))
- move to slotmap ([`7665f2c`](https://github.comgit//DioxusLabs/dioxus/commit/7665f2c6cf05cea64bb9131381d4ac11cbdeb932))
- more work on diffing ([`21685fb`](https://github.comgit//DioxusLabs/dioxus/commit/21685fbb164cf58120e9d37e994a25fa7d902b21))
- integrate serialization and string borrowing ([`f4fb5bb`](https://github.comgit//DioxusLabs/dioxus/commit/f4fb5bb454536d9f108c7e276ce98a8924ab45e1))
- more work on diffing machine ([`9813f23`](https://github.comgit//DioxusLabs/dioxus/commit/9813f23cdf7b32bc058771c91db3f182d6224905))
- more work on diffing. ([`ca3b80f`](https://github.comgit//DioxusLabs/dioxus/commit/ca3b80f06929f30a32c4d50387977cc8724daa8d))
- stack-based "real child iterator" ([`895cc01`](https://github.comgit//DioxusLabs/dioxus/commit/895cc0142b8c0a380ecc71aa6acb5c9d7820862d))
- more docs ([`0826fdf`](https://github.comgit//DioxusLabs/dioxus/commit/0826fdfee13d26b08cb8a2fa45cbd59cc4f20c25))
- refcell to cell for hookidx ([`ea91fc9`](https://github.comgit//DioxusLabs/dioxus/commit/ea91fc984dc648150acbafd0abb79d9f42aca500))
- rename ctx to cx ([`81382e7`](https://github.comgit//DioxusLabs/dioxus/commit/81382e7044fb3dba61d4abb1e6086b7b29143116))
- rethinking stack machine ([`62ae5d3`](https://github.comgit//DioxusLabs/dioxus/commit/62ae5d3bb94cb9ead030ae0b39d9d9bc2b8b4532))
- move around examples ([`70cd46d`](https://github.comgit//DioxusLabs/dioxus/commit/70cd46dbb2a689ae2d512e142b8aee9c80798430))
- start moving events to rc<event> ([`b9ff95f`](https://github.comgit//DioxusLabs/dioxus/commit/b9ff95fa12c46365fe73b64a4926a506d5da2342))
- rename recoil to atoms ([`36ea39a`](https://github.comgit//DioxusLabs/dioxus/commit/36ea39ae30aa3f1fb2d718c0fdf08850c6bfd3ac))
- more examples and docs ([`7fbaf69`](https://github.comgit//DioxusLabs/dioxus/commit/7fbaf69cabbdde712bb3fd9e4b2a5dc18b9390e9))
- more work on updating syntad ([`47e8960`](https://github.comgit//DioxusLabs/dioxus/commit/47e896038ef3655566f3eda83d1d2adfefbc8862))
- some cleanup and documentation ([`517d7f1`](https://github.comgit//DioxusLabs/dioxus/commit/517d7f14957c4dae9fc894bfbdcd00a955d09f20))
- pre-diffmachine merge fork ([`424a181`](https://github.comgit//DioxusLabs/dioxus/commit/424a18137f1fa9528408ce96a57e8120251b9ed5))
- docs ([`f5683a2`](https://github.comgit//DioxusLabs/dioxus/commit/f5683a23464992ecace463a61414795b5a2c58c8))
- pre vnodes instead of vnode ([`fe6938c`](https://github.comgit//DioxusLabs/dioxus/commit/fe6938ceb3dba0796ae8bab52ae41248dc0d3650))
- move into a fromjs tutorial ([`69f5cc3`](https://github.comgit//DioxusLabs/dioxus/commit/69f5cc3802af136729bc73e5c3d209270d41b184))
- events work again! ([`9d7ee79`](https://github.comgit//DioxusLabs/dioxus/commit/9d7ee79826a3b3fb952a70abcbb16dcd3363d2fb))
- successfully building ([`e3d9db0`](https://github.comgit//DioxusLabs/dioxus/commit/e3d9db0847ecc4ff5156929adb6d22e324f93df3))
- props memoization is more powerful ([`73047fe`](https://github.comgit//DioxusLabs/dioxus/commit/73047fe95678d50fcfd62a4ace7c6b406c5304e1))
- merge in some code from the other branch ([`7790750`](https://github.comgit//DioxusLabs/dioxus/commit/7790750349b40055673a0ec16074a0426b84d3b3))
- move the rsx macro around ([`50c8b93`](https://github.comgit//DioxusLabs/dioxus/commit/50c8b93aade1bfa83a091fb51ee48638507f89b0))
- add some more sources in the core implementation ([`7102fe5`](https://github.comgit//DioxusLabs/dioxus/commit/7102fe5f984fe8692cf977ea3a43e2973eed4f45))
- new approach at direct access to vdom ([`795a54a`](https://github.comgit//DioxusLabs/dioxus/commit/795a54a2e427bc7f2b37e636d50aa300977a42a1))
- some docs ([`1919f88`](https://github.comgit//DioxusLabs/dioxus/commit/1919f88f035fadadfc2b9bce6157f2effcd2a4b0))
- get diff compiling ([`cff0547`](https://github.comgit//DioxusLabs/dioxus/commit/cff0547f1a3e2e28ba1946f347c161078818ac66))
- massive changes to definition of components ([`508c560`](https://github.comgit//DioxusLabs/dioxus/commit/508c560320d78730fa058156421523ffa5695d9d))
- moving to IDs ([`79127ea`](https://github.comgit//DioxusLabs/dioxus/commit/79127ea6cdabf62bf91e777aaacb563e3aa5e619))
- move to static props ([`c1fd848`](https://github.comgit//DioxusLabs/dioxus/commit/c1fd848f89b0146581d8e485fa0d4a847387b963))
- moving to imperative method of dom ([`45ee803`](https://github.comgit//DioxusLabs/dioxus/commit/45ee803609bf94404f22d89ab2a20ce0698271ff))
- more progress on parity docs. ([`c5089ba`](https://github.comgit//DioxusLabs/dioxus/commit/c5089ba3c5a8daad4de4d6257604011cc87f6ac7))
- dirty hack to enable send + sync on virtual dom ([`4d5c528`](https://github.comgit//DioxusLabs/dioxus/commit/4d5c528b07e61d6cb0ac8fc0c27ce2e0fdf7e7d2))
- doesnt share on thread ([`fe67ff9`](https://github.comgit//DioxusLabs/dioxus/commit/fe67ff9fa4c9d5009670c922e192dccedb7cd09a))
- recoil ([`ee67654`](https://github.comgit//DioxusLabs/dioxus/commit/ee67654f58aecca91c640fc49451d0b99bc05981))
- Todomvc in progress ([`b843dbd`](https://github.comgit//DioxusLabs/dioxus/commit/b843dbd3679abf86a34347d87fd4ce5fe9e2aca5))
- introduce children for walking down the tree ([`0d44f00`](https://github.comgit//DioxusLabs/dioxus/commit/0d44f009b0176cb9cf8203374cf534f5af7de63c))
- context api wired up ([`24805a0`](https://github.comgit//DioxusLabs/dioxus/commit/24805a02f6a46539ff76ddcd569ecfc336ca2171))
- about to consolidate context and scope ([`4c8130c`](https://github.comgit//DioxusLabs/dioxus/commit/4c8130c4e48a4f0eaef53c696154958d6e0ec6f2))
- remove old code ([`3de54d0`](https://github.comgit//DioxusLabs/dioxus/commit/3de54d0b5202aca678d485a68ef8de006a63e21b))
- abstraction lifetimes work out nicely ([`2284b35`](https://github.comgit//DioxusLabs/dioxus/commit/2284b357823336aafd8c622b466cd4695b78c690))
- Clean up repo a bit ([`a99147c`](https://github.comgit//DioxusLabs/dioxus/commit/a99147c85b53b4ee336a94deee463d793cebf572))
- some code health ([`c28697e`](https://github.comgit//DioxusLabs/dioxus/commit/c28697e1fe3136d1835f2b663715f34aab9f4b17))
- major overhaul to diffing ([`9810fee`](https://github.comgit//DioxusLabs/dioxus/commit/9810feebf57f93114e3d7faf6de053ac192593a9))
- Wip ([`c809095`](https://github.comgit//DioxusLabs/dioxus/commit/c809095124d06175e7f49853c34e48d7335573c5))
- refactor a bit ([`2eeb8f2`](https://github.comgit//DioxusLabs/dioxus/commit/2eeb8f2386409b53836fab687097d89af7b883c3))
- todos ([`8c541f6`](https://github.comgit//DioxusLabs/dioxus/commit/8c541f66d5f7ef2286f2cdf9b0496a9c404471f9))
- todomvc ([`cfa0927`](https://github.comgit//DioxusLabs/dioxus/commit/cfa0927cdd40bc3dba22996018605dbad91d0391))
- todomvc ([`ce33031`](https://github.comgit//DioxusLabs/dioxus/commit/ce33031519fbbbd207f1dffb75acf62bf59e3c9e))
- more ergonomics, more examples ([`0bcff1f`](https://github.comgit//DioxusLabs/dioxus/commit/0bcff1f88e4b1a633b7a9b7c6c2e39b8bd3666c4))
- use rsx! inline! ([`44aad27`](https://github.comgit//DioxusLabs/dioxus/commit/44aad2746c117ba9742c86a53327f4f9e96509e7))
- building large apps, revamp macro ([`9f7f43b`](https://github.comgit//DioxusLabs/dioxus/commit/9f7f43b6614aaef2d7dded7058e81934f28f5dec))
- begint to accept iterator types ([`742f150`](https://github.comgit//DioxusLabs/dioxus/commit/742f150eb3eba89913f5a0fabb229e72e2a0a5ee))
- livehost bones ([`7856f2b`](https://github.comgit//DioxusLabs/dioxus/commit/7856f2b1537b52478a6fa1ca55d0a8c5793a91e5))
- some stuff related to event listeners. POC for lifecyel ([`5b7887d`](https://github.comgit//DioxusLabs/dioxus/commit/5b7887d76c714d11e0cea5207197ffaac856a0a7))
- diffing approach slightly broken ([`4e48e05`](https://github.comgit//DioxusLabs/dioxus/commit/4e48e0514e1caed9f60bcac4048114008ac76439))
- remove old macro ([`9d0727e`](https://github.comgit//DioxusLabs/dioxus/commit/9d0727edabbf759dac8a3cffabd7103d08b728c1))
- update examples ([`39bd185`](https://github.comgit//DioxusLabs/dioxus/commit/39bd1856f4d7c2364d6ca2c7812ab5d6e71252b9))
- ensure mutabality is okay when not double-using the components ([`305ff91`](https://github.comgit//DioxusLabs/dioxus/commit/305ff919effa919ff7ea5db2a71cf21eca106588))
- props now autoderives its own trait ([`b3c96a5`](https://github.comgit//DioxusLabs/dioxus/commit/b3c96a5996f434332813c737bb83ad564d91af5f))
- somewhat working with rc and weak ([`d4f1cea`](https://github.comgit//DioxusLabs/dioxus/commit/d4f1ceaffbc0551ea3b179a101885275690cebec))
- foregin eq from comparapable comp type. ([`ec801ea`](https://github.comgit//DioxusLabs/dioxus/commit/ec801eab167f9be5325ab877e74e2b65d501e129))
- staticify? ([`5ad8188`](https://github.comgit//DioxusLabs/dioxus/commit/5ad81885e499bf02ac79e0098f7956d02ee5f2e5))
- cargo fix to clean up things ([`78d093a`](https://github.comgit//DioxusLabs/dioxus/commit/78d093a9454386397a991bd01e603e4ad554521f))
- implement vcomp fully ([`29751a4`](https://github.comgit//DioxusLabs/dioxus/commit/29751a4babfb46da25446ecf4077bfe9aa695655))
- add some docs ([`5abda91`](https://github.comgit//DioxusLabs/dioxus/commit/5abda91892f32fd82a88d902aaefaccaabf07c7d))
- update component so build passes ([`8fcd001`](https://github.comgit//DioxusLabs/dioxus/commit/8fcd001677146d83dc8e89402495c4e2a0ccc2f8))
- wire up props macro ([`37f5a7a`](https://github.comgit//DioxusLabs/dioxus/commit/37f5a7ad33e272a9e210bf480304d54ff0df0d67))
- still a bit stumped on DFS vs BFS ([`3740f81`](https://github.comgit//DioxusLabs/dioxus/commit/3740f81383b45e3300edda9c04f414852a828d12))
- Feat: it's awersome ([`8dc2619`](https://github.comgit//DioxusLabs/dioxus/commit/8dc26195e274874a7de6c806372f9f77a5c82c5d))
- revert FC changes (like the old style). ([`7158bc3`](https://github.comgit//DioxusLabs/dioxus/commit/7158bc3575e180dbe8641549b040e74ae3baf80b))
- dyn scope ([`89f2290`](https://github.comgit//DioxusLabs/dioxus/commit/89f22906926ccb5aa4cbe3fe1b6d834b06dd91e7))
- yeet, synthetic somewhat wired up ([`d959806`](https://github.comgit//DioxusLabs/dioxus/commit/d9598066c2679d9d0b9ca0ce1d3f26110a238cd2))
- synthetic events wired up (ish) ([`3087813`](https://github.comgit//DioxusLabs/dioxus/commit/30878135707ea86702a2f1f8c6f269c14576879f))
- remove FC ([`92d9521`](https://github.comgit//DioxusLabs/dioxus/commit/92d9521a73aefb620b354ae5954617109dd06e7e))
- notes on safety, and inline listeners ([`bdd6be3`](https://github.comgit//DioxusLabs/dioxus/commit/bdd6be309e8fa4c05a03c5cfc164f7564e7c3051))
- cleanup edit module ([`c70652a`](https://github.comgit//DioxusLabs/dioxus/commit/c70652a3c91411e816012f79291926a716615d5c))
- more cleanup ([`5a9155b`](https://github.comgit//DioxusLabs/dioxus/commit/5a9155b059acc1fb3c8b8accbeca3701ce4f0ab6))
- add context to builder ([`cf16090`](https://github.comgit//DioxusLabs/dioxus/commit/cf16090838d127354e333dcbc0b06474835b87d6))
- listeners now have scope information ([`fcd68e6`](https://github.comgit//DioxusLabs/dioxus/commit/fcd68e61d2400628469ba193b009e7bf1fd3acdf))
- broken, but solved ([`cb74d70`](https://github.comgit//DioxusLabs/dioxus/commit/cb74d70f831b5510f1ee191d91eaff621ffa6256))
- use_reducer ([`879e107`](https://github.comgit//DioxusLabs/dioxus/commit/879e107634f596d555f62f8853afcf5b3796b180))
- wowza got it all working ([`4b8e9f4`](https://github.comgit//DioxusLabs/dioxus/commit/4b8e9f4a125b9d55439d919786f33d9d5df234e8))
- parse custom rsx syntax ([`da00df6`](https://github.comgit//DioxusLabs/dioxus/commit/da00df66889f4fa2e39651491e08794e1fe78549))
- update readme and examples ([`ffaf687`](https://github.comgit//DioxusLabs/dioxus/commit/ffaf6878963981860089c2362947bf77a84c9058))
- view -> render ([`c8bb392`](https://github.comgit//DioxusLabs/dioxus/commit/c8bb392cadb22ddb41e53a776ab0677945579a9c))
- bump core version ([`4997976`](https://github.comgit//DioxusLabs/dioxus/commit/499797626144738ec088ced0ca272c2e7a920c19))
- remove html crate ([`9dcee01`](https://github.comgit//DioxusLabs/dioxus/commit/9dcee01b335901cf2c80b453b97180e0d2551dc2))
- add core macro crate ([`c32a6ef`](https://github.comgit//DioxusLabs/dioxus/commit/c32a6ef7fe50d8901a0585c7e426ff0b16b1de6e))
- bump version for web release ([`422d5ac`](https://github.comgit//DioxusLabs/dioxus/commit/422d5ac5af4c3d05627edd36a623663db158c3dd))
- a few bugs, but the event system works! ([`3b30fa6`](https://github.comgit//DioxusLabs/dioxus/commit/3b30fa61b8f1927f22ffec373a3405ddfdefde39))
- patch to diff to allow scopes ([`2041c88`](https://github.comgit//DioxusLabs/dioxus/commit/2041c88d07e8d68494bc77a10eaeb4707da078d9))
- moving to CbIdx as serializable event system ([`e840f47`](https://github.comgit//DioxusLabs/dioxus/commit/e840f472faf25d8e1d65abeb610daed6a522771d))
- custom format_args for inlining variables into html templates ([`e4b1f6e`](https://github.comgit//DioxusLabs/dioxus/commit/e4b1f6ea0d0db707cf757dabf8635e9fc91a3e0f))
- begin WIP on html macro ([`a8b1225`](https://github.comgit//DioxusLabs/dioxus/commit/a8b1225c4853a61c7862e1a33eba64b8ed4e06d5))
- move webview logic into library ([`32b45e5`](https://github.comgit//DioxusLabs/dioxus/commit/32b45e5ba168390e338a343eef6baba5cca9468b))
- comments ([`18a7a1f`](https://github.comgit//DioxusLabs/dioxus/commit/18a7a1f9c40eb3b1ac263cbd21b1daa6da9c7093))
- wire up rebuild ([`06ae4fc`](https://github.comgit//DioxusLabs/dioxus/commit/06ae4fc17833ff9695b0645f940b5ea32eeb4ef0))
- clean up code ([`8345137`](https://github.comgit//DioxusLabs/dioxus/commit/83451372aa28ba4e137b553f07838a2db06c92da))
- fix internal lifecycle ([`5204862`](https://github.comgit//DioxusLabs/dioxus/commit/5204862bc220e55afe8d2f8f4422cb530a6ea1cf))
- add css example ([`edf09c1`](https://github.comgit//DioxusLabs/dioxus/commit/edf09c1892f2521f41bf84372629e84bed86e921))
- borrowing ([`7a4594e`](https://github.comgit//DioxusLabs/dioxus/commit/7a4594e237acc9864ca9426f6d7002a8ab223d83))
- finally solve the component lifetime problem <3 ([`bdc25b5`](https://github.comgit//DioxusLabs/dioxus/commit/bdc25b581b12ee536933032803785d3531b13693))
- WIP ctx ([`7a6aabe`](https://github.comgit//DioxusLabs/dioxus/commit/7a6aabe4f39aa38bec7b548d030c11b2d9f481cb))
- desktop app wired up ([`b3e6886`](https://github.comgit//DioxusLabs/dioxus/commit/b3e68863514a33291556ce278b7255fdf53e8b50))
- remove our use of ouroborous. ([`bcbb93b`](https://github.comgit//DioxusLabs/dioxus/commit/bcbb93b69764dbff0d6130dc0f32bcf58ad7807f))
- re-enable stack machine approach ([`e3ede7f`](https://github.comgit//DioxusLabs/dioxus/commit/e3ede7fcbf7acb5d8762bcddb5f1499104bb0ce7))
- WIP on deserialize ([`f22ff83`](https://github.comgit//DioxusLabs/dioxus/commit/f22ff8319078d283e675ff8dc735da5bf8efe0df))
- wire up a very basic dom updater ([`c4e8d8b`](https://github.comgit//DioxusLabs/dioxus/commit/c4e8d8bb31d54d70c8da2f4a5e3e3926ff7df92b))
- major overhaul to diffing, using a "diffing machine" now ([`4dfdf91`](https://github.comgit//DioxusLabs/dioxus/commit/4dfdf9123608c69b86a56acbd6d8810b0cf1918c))
- remove generic paramter on VDOM ([`4c291a0`](https://github.comgit//DioxusLabs/dioxus/commit/4c291a0efdbaadf5c2212248367c0a78046e2f83))
- wire up some of the changelist for diff ([`d063a19`](https://github.comgit//DioxusLabs/dioxus/commit/d063a199391383288e807ac03d333091bac6ba60))
- event loop ([`ea2aa4b`](https://github.comgit//DioxusLabs/dioxus/commit/ea2aa4b0c97b3292178284e05756d1415902a9e2))
- major overhaul, wire up event system ([`8295ac4`](https://github.comgit//DioxusLabs/dioxus/commit/8295ac4b3dd2954e2aa23ef3a1f2e7c740a29961))
- overall API updates ([`f47651b`](https://github.comgit//DioxusLabs/dioxus/commit/f47651b32afb385297ddae00616f4308083a36e6))
- push new component API and context API ([`125f542`](https://github.comgit//DioxusLabs/dioxus/commit/125f5426a4719bdbad42c6c166e3a6002a2ce3f0))
- update builder ([`8329268`](https://github.comgit//DioxusLabs/dioxus/commit/8329268d39238f030dd703ab4614253e58339e0b))
- update solved problems with context API ([`c97a964`](https://github.comgit//DioxusLabs/dioxus/commit/c97a9647e3ccc477f5068d567a17d540c0bd199c))
- Feat: - integrate subscription service into context. - Update documentation ([`204f0d9`](https://github.comgit//DioxusLabs/dioxus/commit/204f0d9f16c97fb847b7265296b43db36411b393))
- fix docs names ([`ee23ea6`](https://github.comgit//DioxusLabs/dioxus/commit/ee23ea6c3a43cb6a3f5f36d8eab3a0df0daeb760))
- implememt nodes better ([`edbb33b`](https://github.comgit//DioxusLabs/dioxus/commit/edbb33b2ee3692cac155b9e206cd7c2d6382eb9d))
- move out scope into its own file ([`c9d95dd`](https://github.comgit//DioxusLabs/dioxus/commit/c9d95dd1dc35bde6f395dd95a951a0260ecfbb8a))
- capture pointer type ([`e939373`](https://github.comgit//DioxusLabs/dioxus/commit/e939373616680ae74948fa2c36388332c52d0d1f))
- more updates to hooks ([`2c2882c`](https://github.comgit//DioxusLabs/dioxus/commit/2c2882c9a24ed22aa6d17800ad3b1c308eb6579f))
- found a fast solution to hook state ([`4d01436`](https://github.comgit//DioxusLabs/dioxus/commit/4d01436c3f45ef32ea17fa06c363f4c76dc8c227))
- comment out examples and move lifetime in FC type ([`62d4ad5`](https://github.comgit//DioxusLabs/dioxus/commit/62d4ad58787185032100a2d25e79b70f6ec97a3c))
- include the helper ([`07341d2`](https://github.comgit//DioxusLabs/dioxus/commit/07341d2c65dc61b90587e2e5daadf72ec82623a8))
- updates to docs, extension ([`a2406b3`](https://github.comgit//DioxusLabs/dioxus/commit/a2406b33d6c11174f95bd003a59650b86f14159f))
- webview example ([`7730fd4`](https://github.comgit//DioxusLabs/dioxus/commit/7730fd4a8c0183699684704777fb581129eddd8e))
- Dioxus-webview ([`9c01736`](https://github.comgit//DioxusLabs/dioxus/commit/9c0173689539210d14847613f9a1694e6cb34506))
- add router ([`6aeea9b`](https://github.comgit//DioxusLabs/dioxus/commit/6aeea9b79081a2a711d8eee7e6edd299caf379ef))
- dioxus frontend crate ([`4d7ac5b`](https://github.comgit//DioxusLabs/dioxus/commit/4d7ac5bb5d3aa1897c0f6c1f322aca08c0c791f0))
- Add versioning for package ([`33a805d`](https://github.comgit//DioxusLabs/dioxus/commit/33a805da4024a02f7fd2c68d97ce270c31154461))
- clean up naming ([`2afbfea`](https://github.comgit//DioxusLabs/dioxus/commit/2afbfea324317003c601a331bd759414cce6b742))
- prep for cargo ([`be8d267`](https://github.comgit//DioxusLabs/dioxus/commit/be8d2678b58ceb15c30ce75de630d0398c8951c3))
- include diffing and patching in Dioxus ([`f3c650b`](https://github.comgit//DioxusLabs/dioxus/commit/f3c650b073b57073d30b65fb4f7fe15a6c5b2be6))
- more docs, dissolve vnode crate into dioxus-core ([`9c616ea`](https://github.comgit//DioxusLabs/dioxus/commit/9c616ea5c092a756a437ccf175c8b04ada50b1b6))
- more docs, example, mroe nodes ([`d13e04c`](https://github.comgit//DioxusLabs/dioxus/commit/d13e04c9ff4c2a98e295d762d631d47e0c762049))
- some updates, pull in the vnode enums ([`152bced`](https://github.comgit//DioxusLabs/dioxus/commit/152bced9f38576f359c9270efd868179298528bc))
- WIP ([`ce34d0d`](https://github.comgit//DioxusLabs/dioxus/commit/ce34d0dfcda82d847a6032f8f42749b39e0fba18))
- docs, code frm percy ([`2b9c8d0`](https://github.comgit//DioxusLabs/dioxus/commit/2b9c8d09d926ff6b5ad8a7e7b7b0b6f93bb8eb36))
</details>
## v0.1.3 (2021-12-15)
### Documentation
- <csr-id-4de16c4779648e591b3869b5df31271ae603c812/> update local examples and docs to support new syntaxes
- <csr-id-e495b09bf1c99950b5ea8647b371f296dc64afbe/> fix table
- <csr-id-9874f342e2bc1116410142c691545d75b0e0d6fe/> more docs
- <csr-id-54d050cf897131ddc4f53651f1aeedaf03f23f57/> strong improvmenets, first major section done
- <csr-id-9b5f82af7dce18b75a781b86b188bc713860bb99/> start on components
- <csr-id-460783ad549818a85db634ed9c39ffce210b98ec/> lnks to projects
- <csr-id-583fdfa5618e11d660985b97e570d4503be2ff49/> big updates to the reference
- <csr-id-d9e6d0925b30690212d1d690dfba288f1a694a27/> examples
- <csr-id-0826fdfee13d26b08cb8a2fa45cbd59cc4f20c25/> more docs
- <csr-id-70cd46dbb2a689ae2d512e142b8aee9c80798430/> move around examples
- <csr-id-69f5cc3802af136729bc73e5c3d209270d41b184/> move into a fromjs tutorial
- <csr-id-7102fe5f984fe8692cf977ea3a43e2973eed4f45/> add some more sources in the core implementation
- <csr-id-e4c06ce8e893779d2aad0883a1bb27d193bc5985/> update cargo tomls
### New Features
- <csr-id-96b3d56e09a38843f7dc6c1047fe2a3923bd8473/> move back to combined cx/props
- <csr-id-19df1bda109aba03c40ff631263bcb7035004ca0/> bubbling
- <csr-id-8acdd2ea830b995b608d8bac2ef527db8d40e662/> it compiles once more
- <csr-id-74c6211408ea0db5916a4e0973a3f9a7208faa44/> should be functional across the boar
- <csr-id-55e6dd9701d3710ff17a25581b99b8f159e609b9/> wire tasks up
- <csr-id-c10c1f418bf21e0e54b53e2babeb37c672c06901/> wire up linked nodes
- <csr-id-9d8c5ca5ab5784b3f17d7ee20a451ee68fd703d6/> it properly bubbles
- <csr-id-fd93ee89c19b085a04307ef30217170518defa8e/> upgrade syntax
- <csr-id-11757ddf61e1decb1bd1c2bb30455d0bd01a3e95/> fake bubbling
- <csr-id-f2234068ba7cd915a00a81e41660d7d6ee1177cc/> events bubble now
- <csr-id-2cf90b6903411e42f01a801f89037686194ee068/> pull children out of component definition
- <csr-id-84fd0c616252bf29cd665782258530032b54d13a/> cleanuup
- <csr-id-79503f15c5db04fa04575c8735941a2e3a75030b/> full html support
- <csr-id-6b2645fd80ab20153e7bb51887443e994fbab5cb/> improve FC handling to allow lifetimes of parent
- <csr-id-fda2ebc2a22965845e015384f39f34ce7cb3e428/> improve safety
- <csr-id-9726a065b0d4fb1ede5b53a2ddd58c855e51539f/> massage lifetimes
- <csr-id-efd0e9b5648c809057f339083ba9d454f810d483/> support desktop more completely
- <csr-id-a2b0c50a343005c63c7032bcefb8323b78350bb9/> add update functionality to useref
- <csr-id-5502429626023d0788cca352e94ac6ea67c2cb11/> desktop functioning well
- <csr-id-4a4c7afca7e1beadd4b213214074fdb420eb0923/> shared state mechanisms
- <csr-id-fac42339c272b0e430ebf4f31b6061a0635d3e19/> mutations
- <csr-id-687cda1b6d9595357d1dc8720ebe921f61098d8f/> re-enable suspense
- <csr-id-4a72b3140bd244da602deada1eeecded65ff5848/> amazingly awesome error handling
- <csr-id-d7940aa2ac2017316d62e0f2eac0701dc6ad1f09/> proper handling of events
- <csr-id-d717c22d9c5da7b6f343fede11faaf953a3a29e0/> keyed diffing!!
- <csr-id-fac2e56ed63fc708a89e14df15f1fc32f58391a9/> update root props
- <csr-id-c321532a6cef40b2d2e4adc8c7a55931b6755b08/> some docs, cleaning
- <csr-id-1749eba8eb6c8ffa38ab3ad52160b637fe021e86/> more and more sophisticated diff
- <csr-id-d618092e9d150589e61516f7bbb169f2db49d3f2/> a new vnode type for anchors
- <csr-id-00231adfa2e1d67a9d7ae2fa61c33e3a22d51978/> code quality improvements for core
- <csr-id-a2c7d17b0595769f60bc1c2bbf7cbe32cec37486/> mvoe away from compound context
- <csr-id-31702dbf878dd0207d101f7869ebefd2bb9f6860/> wire up resource pool
- <csr-id-e5c88fe3a49649ecb308decd14c2557963978619/> make hooks free-functions
- <csr-id-bfdcb20437c385116fd640e46b6361d09d8d5ff8/> transition to cx, props
- <csr-id-f782e142118fb7acf1b88a0f3fbb03e4a5e3e91e/> omg what a dumb mistake
- <csr-id-4837d8e741343d26f31b55e4478a374dc761e538/> suspense!
- <csr-id-4a0068f09918adbc299150edcf777f342ced0dd3/> bless up, no more segfaults
- <csr-id-de9f61bcf48c0d6e35e46c337b72a713c9f9f7d2/> more suspended nodes!
- <csr-id-7dfe89c9581f45a445f17f9fe4bb94e61f67e971/> wire up event delegator for webview
- <csr-id-0a907b35c647b67172080023462770b761364d51/> fix some lifetime issues
- <csr-id-80e6c256980eb3e8c32e30f3dbb43c8b3b9a9cf4/> move over to push based mechanism
- <csr-id-e7238762ae518c5688f9339d11832d17f99ad553/> architecture document and edit list
- <csr-id-3a57b942624afb8aa6650aeee05466c3c9ce967e/> task system works
but I broke the other things :(
- <csr-id-f457b7113129479cad577237ef21cb735fffe483/> rebuild doesn't return errors
- <csr-id-4091846934b4b3b2bc03d3ca8aaf7712aebd4e36/> add aria
- <csr-id-c79d9ae674e235c8e9c2c069d24902122b9c7464/> buff up html allowed attributes
- <csr-id-bbcb5a0234dbce48ffeb64903c3ec04562a87ad6/> enable components in ssr
- <csr-id-0479252a5fba96e554dcf2422a8566e248fc6593/> keyed diffing
- <csr-id-9abb0470b7869019d539a2fc21da3872348ae38b/> static node infrastructure and ssr changes
- <csr-id-e6f56563bc84516e017b3db06f11fab2549b9a50/> tests and benchmarks
- <csr-id-db6d0184aa5aa612bd7229a06f72b4c2ec5f6409/> dedicated mutations module
- <csr-id-1cc1679a6ba33ff68d45ad1e964b73230224bc23/> refactor out the hooks implementation
- <csr-id-2ce0752a9cd253fee1c8b205400aa191b09c9dcb/> fix compiling
- <csr-id-99d94b69aba192be7e41ebe891aca4bb4f6cae88/> move webview to wry
- <csr-id-e4cdb645aad800484b19ec35ba1f8bb9ccf71d12/> beaf up the use_state hook
- <csr-id-22e659c2bd7797ca5a822180aca0cb5d950c5287/> namespaced attributes
this commit adds namespaced attributes. This lets us support attribute groups, and thus, inline styles.
This namespaced attribute stuff is only available for styles at the moment, though it theoretically could be enabled for any other attributes.
- <csr-id-904b26f7111c3fc66400744ff6192e4b20bf6d74/> add edits back! and more webview support!
This commit adds a new type - the DomEdit - for serializing the changes made by the diffing machine. The architecture of how DomEdits fit into the cooperative scheduling is still TBD but it will allow us to build change lists without applying them immediately. This is more performant and allows us to only render parts of the page at a time.
This commit also adds more infrastructure around webview. Dioxus can now run on the web, generate static pages, run in the desktop, and run on mobile, with a large part of thanks to webview.
- <csr-id-b5e5ef171aa9f8986fb4ab04d793eb63f557c4ae/> two calculator examples
- <csr-id-7665f2c6cf05cea64bb9131381d4ac11cbdeb932/> move to slotmap
- <csr-id-f4fb5bb454536d9f108c7e276ce98a8924ab45e1/> integrate serialization and string borrowing
This commit adds lifetimes to the diff and realdom methods so consumers may borrow the contents of the DOM for serialization or asynchronous modifications.
- <csr-id-9d7ee79826a3b3fb952a70abcbb16dcd3363d2fb/> events work again!
- <csr-id-73047fe95678d50fcfd62a4ace7c6b406c5304e1/> props memoization is more powerful
This commit solves the memoization , properly memoizing properties that don't have any generic parameters. This is a rough heuristic to prevent non-static lifetimes from creeping into props and breaking our minual lifetime management.
Props that have a generic parameter are opted-out of the `partialeq` requirement and props *without* lifetimes must implement partialeq. We're going to leave manual disabling of memoization for future work.
- <csr-id-cfa0927cdd40bc3dba22996018605dbad91d0391/> todomvc
- <csr-id-d4f1ceaffbc0551ea3b179a101885275690cebec/> somewhat working with rc and weak
- <csr-id-89f22906926ccb5aa4cbe3fe1b6d834b06dd91e7/> dyn scope
- <csr-id-8329268d39238f030dd703ab4614253e58339e0b/> update builder
### Bug Fixes
- <csr-id-52c7154897111b570918127ffe3285bb1d5951a0/> really big bug around hooks
- <csr-id-601078f9cf78a58d7502a377676ac94f3cf037bf/> desktop and mobile
- <csr-id-77686ba329cba0679d0339589a00df77d3c4c1a1/> it compiles again
- <csr-id-27d891934a70424b45e6278b7e2baaa2d1b78b35/> use annotation method from rust/58052 to fix closure lifetimes
- <csr-id-ba9e1dbb8fa24048a6c9ccef8a8722688226a845/> messed up how lifetimes worked, need to render once per component
- <csr-id-478255f40d4de1d2e3f3cc9b6d758b30ff394b39/> all the bugs!
- <csr-id-a33f7701fcf5f917fea8719253650b5ad92554fd/> tags
- <csr-id-868f6739d2b2c5f2ace0c5240cff8008901e818c/> keyword length
### Performance
- <csr-id-8b3ac0b57ca073c1451e8d5df93882c9360ca52a/> remove global allocation for props
- <csr-id-ea91fc984dc648150acbafd0abb79d9f42aca500/> refcell to cell for hookidx
+2 -16
View File
@@ -2,7 +2,7 @@
name = "dioxus-core"
version = { workspace = true }
authors = ["Jonathan Kelley"]
edition = "2018"
edition = "2021"
description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
license = "MIT OR Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
@@ -10,37 +10,23 @@ homepage = "https://dioxuslabs.com"
keywords = ["dom", "ui", "gui", "react"]
[dependencies]
# Bumpalo is used as a micro heap backing each component
bumpalo = { version = "3.6", features = ["collections", "boxed"] }
# faster hashmaps
rustc-hash = { workspace = true }
# Used in diffing
longest-increasing-subsequence = "0.1.0"
futures-util = { workspace = true, default-features = false, features = [
"alloc",
"std",
] }
slab = { workspace = true }
futures-channel = { workspace = true }
smallbox = "0.8.1"
tracing = { workspace = true }
# Serialize the Edits for use in Webview/Liveview instances
serde = { version = "1", features = ["derive"], optional = true }
[dev-dependencies]
tokio = { workspace = true, features = ["full"] }
dioxus = { workspace = true }
dioxus-html = { workspace = true, features = ["serialize"] }
pretty_assertions = "1.3.0"
rand = "0.8.5"
dioxus-ssr = { workspace = true }
trybuild = "1.0"
[features]
default = []
+58 -92
View File
@@ -1,47 +1,66 @@
# dioxus-core
dioxus-core is a fast and featureful VirtualDom implementation written in and for Rust.
`dioxus-core` provides a fast and featureful VirtualDom implementation for Rust.
# Features
```rust, ignore
use dioxus_core::prelude::*;
- Functions as components
- Hooks for local state
- Task pool for spawning futures
- Template-based architecture
- Asynchronous components
- Suspense boundaries
- Error boundaries through the `anyhow` crate
- Customizable memoization
let vdom = VirtualDom::new(app);
let real_dom = SomeRenderer::new();
loop {
select! {
evt = real_dom.event() => vdom.handle_event(evt),
_ = vdom.wait_for_work() => {}
}
vdom.render(&mut real_dom)
}
# fn app() -> Element { None }
# struct SomeRenderer; impl SomeRenderer { fn new() -> SomeRenderer { SomeRenderer; } async fn event() -> () { unimplemented!() } }
```
## Features
A virtualdom is an efficient and flexible tree datastructure that allows you to manage state for a graphical user interface. The Dioxus VirtualDom is perhaps the most fully-featured virtualdom implementation in Rust and powers renderers running across Web, Desktop, Mobile, SSR, TUI, LiveView, and more. When you use the Dioxus VirtualDom, you immediately enable users of your renderer to leverage the wide ecosystem of Dioxus components, hooks, and associated tooling.
Some features of `dioxus-core` include:
- UI components are just functions
- State is provided by hooks
- Deep integration with async
- Strong focus on performance
- Integrated hotreloading support
- Extensible system for UI elements and their attributes
If you are just starting, check out the Guides first.
# General Theory
## Understanding the implementation
The dioxus-core `VirtualDom` object is built around the concept of a `Template`. Templates describe a layout tree known at compile time with dynamic parts filled at runtime.
`dioxus-core` is designed to be a lightweight crate that. It exposes a number of flexible primitives without being deeply concerned about the intracices of state management itself. We proivde a number of useful abstractions built on these primitives in the `dioxus-hooks` crate as well as the `dioxus-signals` crate.
Each component in the VirtualDom works as a dedicated render loop where re-renders are triggered by events external to the VirtualDom, or from the components themselves.
The important abstractions to understand are:
- The [`VirtualDom`]
- The [`Component`] and its [`Properties`]
- Handling events
- Working with async
- Suspense
When each component re-renders, it must return an `Element`. In Dioxus, the `Element` type is an alias for `Result<VNode>`. Between two renders, Dioxus compares the inner `VNode` object and calculates the differences between the dynamic portions of each internal `Template`. If any attributes or elements are different between the old layout and the new layout, Dioxus will write modifications to the `Mutations` object.
## Usage
Dioxus expects the target renderer to save its nodes in a list. Each element is given a numerical ID which can be used to directly index into that list for O(1) lookups.
# Usage
All Dioxus apps start as just a function that takes the [`Scope`] object and returns an [`Element`].
The `dioxus` crate exports the `rsx` macro which transforms a helpful, simpler syntax of Rust into the logic required to build Templates.
The `dioxus` crate exports the `rsx` macro which transforms a helpful, simpler syntax of Rust.
First, start with your app:
```rust
# use dioxus::core::Mutations;
# use dioxus::dioxus_core::Mutations;
use dioxus::prelude::*;
// First, declare a root component
fn app(cx: Scope) -> Element {
cx.render(rsx!{
fn app() -> Element {
rsx!{
div { "hello world" }
})
}
}
fn main() {
@@ -49,79 +68,26 @@ fn main() {
let mut dom = VirtualDom::new(app);
// The initial render of the dom will generate a stream of edits for the real dom to apply
let mutations = dom.rebuild();
// Somehow, you can apply these edits to the real dom
apply_edits_to_real_dom(mutations);
let mutations = dom.rebuild_to_vec();
}
# fn apply_edits_to_real_dom(mutations: Mutations) {}
```
## Contributing
- Check out the website [section on contributing](https://dioxuslabs.com/learn/0.4/contributing).
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
- [Join](https://discord.gg/XgGxMSkvUM) the discord and ask questions!
We can then wait for any asynchronous components or pending futures using the `wait_for_work()` method. If we have a deadline, then we can use render_with_deadline instead:
```rust
# #![allow(unused)]
# use dioxus::prelude::*;
# use std::time::Duration;
# async fn wait(mut dom: VirtualDom) {
// Wait for the dom to be marked dirty internally
dom.wait_for_work().await;
<a href="https://github.com/dioxuslabs/dioxus/graphs/contributors">
<img src="https://contrib.rocks/image?repo=dioxuslabs/dioxus&max=30&columns=10" />
</a>
// Or wait for a deadline and then collect edits
let mutations = dom.render_with_deadline(tokio::time::sleep(Duration::from_millis(16)));
# }
```
If an event occurs from outside the VirtualDom while waiting for work, then we can cancel the wait using a `select!` block and inject the event.
## License
This project is licensed under the [MIT license].
```rust, ignore
loop {
select! {
evt = real_dom.event() => dom.handle_event("click", evt.data, evt.element, evt.bubbles),
_ = dom.wait_for_work() => {}
}
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
// Render any work without blocking the main thread for too long
let mutations = dom.render_with_deadline(tokio::time::sleep(Duration::from_millis(10)));
// And then apply the edits
real_dom.apply(mutations);
}
```
## Internals
Dioxus-core builds off the many frameworks that came before it. Notably, Dioxus borrows these concepts:
- React: hooks, concurrency, suspense
- Dodrio: bump allocation, double buffering, and some diffing architecture
Dioxus-core hits a very high level of parity with mature frameworks. However, Dioxus also brings some new unique features:
- managed lifetimes for borrowed data
- placeholder approach for suspended vnodes
- fiber/interruptible diffing algorithm
- custom memory allocator for VNodes and all text content
- support for fragments w/ lazy normalization
- slab allocator for scopes
- mirrored-slab approach for remote VirtualDoms
- dedicated subtrees for rendering into separate contexts from the same app
There's certainly more to the story, but these optimizations make Dioxus memory use and allocation count extremely minimal. For an average application, no allocations may be needed once the app has been loaded. Only when new components are added to the dom will allocations occur. For a given component, the space of old VNodes is dynamically recycled as new nodes are added. Additionally, Dioxus tracks the average memory footprint of previous components to estimate how much memory allocate for future components.
All in all, Dioxus treats memory as a valuable resource. Combined with the memory-efficient footprint of Wasm compilation, Dioxus apps can scale to thousands of components and still stay snappy.
## Goals
The final implementation of Dioxus must:
- Be **fast**. Allocators are typically slow in Wasm/Rust, so we should have a smart way of allocating.
- Be memory efficient. Servers should handle tens of thousands of simultaneous VDoms with no problem.
- Be concurrent. Components should be able to pause rendering to let the screen paint the next frame.
- Be disconnected from a specific renderer (no WebSys dependency in the core crate).
- Support server-side-rendering (SSR). VNodes should render to a string that can be served via a web server.
- Be "live". Components should be able to be both server-rendered and client rendered without needing frontend APIs.
- Be modular. Components and hooks should work anywhere without worrying about the target platform.
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in Dioxus by you, shall be licensed as MIT, without any additional
terms or conditions.
-215
View File
@@ -1,215 +0,0 @@
# Dioxus Core Architecture:
This document is mostly a brain-dump on how things work. A lot of this information might be outdated. Certain features (priority queues, swim lanes) might not be implemented yet.
Main topics covered here:
- Fiber, Concurrency, and Cooperative Scheduling
- Suspense
- Signals
- Patches
- Diffing
- Const/Static structures
- Components/Scope
- Hooks
- VNode Bump Arenas
## Components/Scope
All components in Dioxus are backed by something called the `Scope`. As a user, you will never directly interact with the `Scope` as most calls are shuttled through `Context`. Scopes manage all the internal state for components including hooks, bump arenas, and (unsafe!) lifetime management.
Whenever a new component is created from within a user's component, a new "caller" closure is created that captures the component's properties. In contrast to Yew, we allow components to borrow from their parents, provided their `memo` (essentially canComponentUpdate) method returns false for non-static items. The `Props` macro figures this out automatically, and implements the `Properties` trait with the correct `canComponentUpdate` flag. Implementing this method manually is unsafe! With the `Props` macro you can manually disable memoization, but cannot manually enable memoization for non 'static properties structs without invoking unsafe. For 99% of cases, this is fine.
During diffing, the "caller" closure is updated if the props are not `static. This is very important! If the props change, the old version will be referencing stale data that exists in an "old" bump frame. However, if we cycled the bump frames twice without updating the closure, then the props will point to invalid data and cause memory safety issues. Therefore, it's an invariant to ensure that non-'static Props are always updated during diffing.
## Hooks
Hooks are a form of state that's slightly more finicky than structs but more extensible overall. Hooks cannot be used in conditionals, but are portable enough to run on most targets.
The Dioxus hook model uses a Bump arena where user's data lives.
Initializing hooks:
- The component is created
- The virtualdom heuristics engine pre-allocates a calculated set of memory for the bump arena
- The component is called through "run_scope"
- Each call to use_hook allocates new data for the hook, stores the raw pointer, and pushes the hook index forward
- Each call to use_hook also stores a function pointer fn() that will be used to clean up the hook
- Once the component is finished running, the hook index is reset and the bump is shrunk to shrunk to fit
- The final size of the bump is then used in the heuristics engine for future components.
Running hooks:
- Each time use_hook is called, the internal hook state is fetched as &mut T
- We are guaranteed that our &mut T is not aliasing by re-generating any &mut T dependencies
- The hook counter is incremented
Dropping hooks:
- When the hook is scheduled for deletion, the "drop" function is run for each hook
- (dropping hooks is basically a drop implementation, but can be customized even for primitives)
## VNode Bump Arenas
## Diffing
The entire diffing logic for Dioxus lives in one file (diff.rs). Diffing in Dioxus is hyper-optimized for the types of structures generated by the rsx! and html! macros.
The diffing engine in Dioxus expects the RealDom
## Patches
Dioxus uses patches - not imperative methods - to modify the real dom. This speeds up the diffing operation and makes diffing cancelable which is useful for cooperative scheduling. In general, the RealDom trait exists so renderers can share "Node pointers" across runtime boundaries.
There are no contractual obligations between the VirtualDOM and RealDOM. When the VirtualDOM finishes its work, it releases a Vec of Edits (patches) which the RealDOM can use to update itself.
## Fiber/Concurrency and Cooperative Scheduling
When an EventTrigger enters the queue and "progress" is called (an async function), Dioxus will get to work running scopes and diffing nodes. Scopes are run and nodes are diffed together. Dioxus records which scopes get diffed to track the progress of its work.
While descending through the stack frame, Dioxus will query the RealDom for "time remaining." When the time runs out, Dioxus will escape the stack frame by queuing whatever work it didn't get to, and then bubbling up out of "diff_node". Dioxus will also bubble out of "diff_node" if more important work gets queued while it was descending.
Once bubbled out of diff_node, Dioxus will request the next idle callback and await for it to become available. The return of this callback is a "Deadline" object which Dioxus queries through the RealDom.
All of this is orchestrated to keep high priority events moving through the VirtualDOM and scheduling lower-priority work around the RealDOM's animations and periodic tasks.
```js
// returns a "deadline" object
function idle() {
return new Promise(resolve => requestIdleCallback(resolve));
}
```
## Suspense
In React, "suspense" is the ability render nodes outside of the traditional lifecycle. React will wait on a future to complete, and once the data is ready, will render those nodes. React's version of suspense is designed to make working with promises in components easier.
In Dioxus, we have similar philosophy, but the use and details of suspense is slightly different. For starters, we don't currently allow using futures in the element structure. Technically, we can allow futures - and we do with "Signals" - but the "suspense" feature itself is meant to be self-contained within a single component. This forces you to handle all the loading states within your component, instead of outside the component, keeping things a bit more containerized.
Internally, the flow of suspense works like this:
1. accept the user's future. the future must be owned.
2. wrap that owned future with a new future that returns an EventTrigger
3. submit the future to the VirtualDOM's task queue
4. Poll the task queue in the VirtualDOM's event loop
5. use the EventTrigger from the future to find the use_suspense hook again
6. run that hook's callback with the component's bump arena and result of the future
7. set the hook's "inner value" with the completed valued so futures calls can resolve instantly
8. load the original placeholder node (either a suspended node or an actual node)
9. diff that node with the new node with a low priority on its own fiber
10. return the patches back to the event loop
11. apply the patches to the real dom
/*
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
# Templates
If everything is a template, then we'll have the idea that the only children can b Templates
# 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 in 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).
# Priority 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-priority 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 priority 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.
*/
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 priority 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
- we batch completely until we run into a discrete event
- all continuous events are batched together
- so D C C C C C would be two separate events - D and C. IE onclick and onscroll
- D C C C C C C D C C C D would be D C D C D in 5 distinct phases.
- !listener bubbling is not currently implemented properly and will need to be implemented somehow in the future
- we need to keep track of element parents to be able to traverse properly
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?
## Render functions
One main issue of components is type eras
@@ -1,32 +0,0 @@
use dioxus::prelude::*;
fn main() {}
fn app(cx: Scope) -> Element {
let count: &RefCell<Vec<Element>> = cx.use_hook(|| RefCell::new(Vec::new()));
render! {
unsafe_child_component {
borrowed: count
}
}
}
#[derive(Props)]
struct Testing<'a> {
borrowed: &'a RefCell<Vec<Element<'a>>>,
}
fn unsafe_child_component<'a>(cx: Scope<'a, Testing<'a>>) -> Element<'a> {
let Testing { borrowed } = cx.props;
let borrowed_temporary_data =
cx.use_hook(|| String::from("This data is only valid for the lifetime of the child"));
borrowed
.borrow_mut()
.push(render! {"{borrowed_temporary_data}"});
cx.render(rsx! {
div { "Hello, world!" }
})
}
@@ -1,20 +0,0 @@
error[E0521]: borrowed data escapes outside of function
--> compile_tests/props_safety.rs:8:5
|
5 | fn app(cx: Scope) -> Element {
| --
| |
| `cx` is a reference that is only valid in the function body
| has type `&'1 Scoped<'1>`
...
8 | / render! {
9 | | unsafe_child_component {
10 | | borrowed: count
11 | | }
12 | | }
| | ^
| | |
| |_____`cx` escapes the function body here
| argument requires that `'1` must outlive `'static`
|
= note: this error originates in the macro `render` (in Nightly builds, run with -Z macro-backtrace for more info)
@@ -1,24 +0,0 @@
use dioxus::prelude::*;
fn main() {}
fn app(cx: Scope) -> Element {
let count = vec![1, 2, 3];
render! {
unsafe_child_component {
borrowed: &count
}
}
}
#[derive(Props)]
struct Testing<'a> {
borrowed: &'a Vec<u32>,
}
fn unsafe_child_component<'a>(cx: Scope<'a, Testing<'a>>) -> Element<'a> {
cx.render(rsx! {
div { "{cx.props.borrowed:?}" }
})
}
@@ -1,12 +0,0 @@
error[E0515]: cannot return value referencing local variable `count`
--> compile_tests/props_safety_temporary_values.rs:8:5
|
8 | / render! {
9 | | unsafe_child_component {
10 | | borrowed: &count
| | ------ `count` is borrowed here
11 | | }
12 | | }
| |_____^ returns a value referencing data owned by the current function
|
= note: this error originates in the macro `render` (in Nightly builds, run with -Z macro-backtrace for more info)
+69 -49
View File
@@ -1,76 +1,96 @@
use crate::{
innerlude::Scoped,
nodes::RenderReturn,
scopes::{Scope, ScopeState},
Element,
};
use std::panic::AssertUnwindSafe;
use crate::{nodes::RenderReturn, ComponentFunction};
use std::{any::Any, panic::AssertUnwindSafe};
/// A trait that essentially allows VComponentProps to be used generically
///
/// # Safety
///
/// This should not be implemented outside this module
pub(crate) unsafe trait AnyProps<'a> {
fn props_ptr(&self) -> *const ();
fn render(&'a self, bump: &'a ScopeState) -> RenderReturn<'a>;
unsafe fn memoize(&self, other: &dyn AnyProps) -> bool;
pub(crate) type BoxedAnyProps = Box<dyn AnyProps>;
/// A trait for a component that can be rendered.
pub(crate) trait AnyProps: 'static {
/// Render the component with the internal props.
fn render(&self) -> RenderReturn;
/// Check if the props are the same as the type erased props of another component.
fn memoize(&mut self, other: &dyn Any) -> bool;
/// Get the props as a type erased `dyn Any`.
fn props(&self) -> &dyn Any;
/// Duplicate this component into a new boxed component.
fn duplicate(&self) -> BoxedAnyProps;
}
pub(crate) struct VProps<'a, P> {
pub render_fn: fn(Scope<'a, P>) -> Element<'a>,
pub memo: unsafe fn(&P, &P) -> bool,
pub props: P,
/// A component along with the props the component uses to render.
pub(crate) struct VProps<F: ComponentFunction<P, M>, P, M> {
render_fn: F,
memo: fn(&mut P, &P) -> bool,
props: P,
name: &'static str,
phantom: std::marker::PhantomData<M>,
}
impl<'a, P> VProps<'a, P> {
pub(crate) fn new(
render_fn: fn(Scope<'a, P>) -> Element<'a>,
memo: unsafe fn(&P, &P) -> bool,
props: P,
) -> Self {
impl<F: ComponentFunction<P, M>, P: Clone, M> Clone for VProps<F, P, M> {
fn clone(&self) -> Self {
Self {
render_fn,
memo,
props,
render_fn: self.render_fn.clone(),
memo: self.memo,
props: self.props.clone(),
name: self.name,
phantom: std::marker::PhantomData,
}
}
}
unsafe impl<'a, P> AnyProps<'a> for VProps<'a, P> {
fn props_ptr(&self) -> *const () {
&self.props as *const _ as *const ()
impl<F: ComponentFunction<P, M> + Clone, P: Clone + 'static, M: 'static> VProps<F, P, M> {
/// Create a [`VProps`] object.
pub fn new(
render_fn: F,
memo: fn(&mut P, &P) -> bool,
props: P,
name: &'static str,
) -> VProps<F, P, M> {
VProps {
render_fn,
memo,
props,
name,
phantom: std::marker::PhantomData,
}
}
}
impl<F: ComponentFunction<P, M> + Clone, P: Clone + 'static, M: 'static> AnyProps
for VProps<F, P, M>
{
fn memoize(&mut self, other: &dyn Any) -> bool {
match other.downcast_ref::<P>() {
Some(other) => (self.memo)(&mut self.props, other),
None => false,
}
}
// Safety:
// this will downcast the other ptr as our swallowed type!
// you *must* make this check *before* calling this method
// if your functions are not the same, then you will downcast a pointer into a different type (UB)
unsafe fn memoize(&self, other: &dyn AnyProps) -> bool {
let real_other: &P = &*(other.props_ptr() as *const _ as *const P);
let real_us: &P = &*(self.props_ptr() as *const _ as *const P);
(self.memo)(real_us, real_other)
fn props(&self) -> &dyn Any {
&self.props
}
fn render(&'a self, cx: &'a ScopeState) -> RenderReturn<'a> {
fn render(&self) -> RenderReturn {
let res = std::panic::catch_unwind(AssertUnwindSafe(move || {
// Call the render function directly
let scope: &mut Scoped<P> = cx.bump().alloc(Scoped {
props: &self.props,
scope: cx,
});
(self.render_fn)(scope)
self.render_fn.rebuild(self.props.clone())
}));
match res {
Ok(Some(e)) => RenderReturn::Ready(e),
Ok(None) => RenderReturn::default(),
Err(err) => {
let component_name = cx.name();
let component_name = self.name;
tracing::error!("Error while rendering component `{component_name}`: {err:?}");
RenderReturn::default()
}
}
}
fn duplicate(&self) -> BoxedAnyProps {
Box::new(Self {
render_fn: self.render_fn.clone(),
memo: self.memo,
props: self.props.clone(),
name: self.name,
phantom: std::marker::PhantomData,
})
}
}
+30 -144
View File
@@ -1,10 +1,4 @@
use crate::ScopeState;
use std::ptr::NonNull;
use crate::{
innerlude::DirtyScope, nodes::RenderReturn, nodes::VNode, virtual_dom::VirtualDom,
AttributeValue, DynamicNode, ScopeId,
};
use crate::{innerlude::DirtyScope, virtual_dom::VirtualDom, ScopeId};
/// An Element's unique identifier.
///
@@ -19,19 +13,32 @@ pub struct ElementId(pub usize);
/// `BubbleId` is a `usize` that is unique across the entire VirtualDOM - but not unique across time. If a component is
/// unmounted, then the `BubbleId` will be reused for a new component.
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct VNodeId(pub usize);
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct MountId(pub(crate) usize);
impl Default for MountId {
fn default() -> Self {
Self(usize::MAX)
}
}
impl MountId {
pub(crate) fn as_usize(self) -> Option<usize> {
if self.0 == usize::MAX {
None
} else {
Some(self.0)
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ElementRef {
// the pathway of the real element inside the template
pub(crate) path: ElementPath,
// The actual template
pub(crate) template: VNodeId,
// The scope the element belongs to
pub(crate) scope: ScopeId,
// The actual element
pub(crate) mount: MountId,
}
#[derive(Clone, Copy, Debug)]
@@ -44,22 +51,6 @@ impl VirtualDom {
ElementId(self.elements.insert(None))
}
pub(crate) fn next_vnode_ref(&mut self, vnode: &VNode) -> VNodeId {
let new_id = VNodeId(self.element_refs.insert(Some(unsafe {
std::mem::transmute::<NonNull<VNode>, _>(vnode.into())
})));
// Set this id to be dropped when the scope is rerun
if let Some(scope) = self.runtime.current_scope_id() {
self.scopes[scope.0]
.element_refs_to_drop
.borrow_mut()
.push(new_id);
}
new_id
}
pub(crate) fn reclaim(&mut self, el: ElementId) {
self.try_reclaim(el)
.unwrap_or_else(|| panic!("cannot reclaim {:?}", el));
@@ -67,128 +58,23 @@ impl VirtualDom {
pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option<()> {
if el.0 == 0 {
panic!(
"Cannot reclaim the root element - {:#?}",
std::backtrace::Backtrace::force_capture()
);
panic!("Cannot reclaim the root element",);
}
self.elements.try_remove(el.0).map(|_| ())
}
pub(crate) fn set_template(&mut self, id: VNodeId, vnode: &VNode) {
self.element_refs[id.0] =
Some(unsafe { std::mem::transmute::<NonNull<VNode>, _>(vnode.into()) });
}
// Drop a scope and all its children
// Drop a scope without dropping its children
//
// Note: This will not remove any ids from the arena
pub(crate) fn drop_scope(&mut self, id: ScopeId, recursive: bool) {
self.dirty_scopes.remove(&DirtyScope {
height: self.scopes[id.0].height(),
id,
});
pub(crate) fn drop_scope(&mut self, id: ScopeId) {
let height = {
let scope = self.scopes.remove(id.0);
let context = scope.state();
context.height
};
// Remove all VNode ids from the scope
for id in self.scopes[id.0]
.element_refs_to_drop
.borrow_mut()
.drain(..)
{
self.element_refs.try_remove(id.0);
}
self.ensure_drop_safety(id);
if recursive {
if let Some(root) = self.scopes[id.0].try_root_node() {
if let RenderReturn::Ready(node) = unsafe { root.extend_lifetime_ref() } {
self.drop_scope_inner(node)
}
}
}
let scope = &mut self.scopes[id.0];
// Drop all the hooks once the children are dropped
// this means we'll drop hooks bottom-up
scope.hooks.get_mut().clear();
{
let context = scope.context();
// Drop all the futures once the hooks are dropped
for task_id in context.spawned_tasks.borrow_mut().drain() {
context.tasks.remove(task_id);
}
}
self.scopes.remove(id.0);
}
fn drop_scope_inner(&mut self, node: &VNode) {
node.dynamic_nodes.iter().for_each(|node| match node {
DynamicNode::Component(c) => {
if let Some(f) = c.scope.get() {
self.drop_scope(f, true);
}
c.props.take();
}
DynamicNode::Fragment(nodes) => {
nodes.iter().for_each(|node| self.drop_scope_inner(node))
}
DynamicNode::Placeholder(_) => {}
DynamicNode::Text(_) => {}
});
}
/// Descend through the tree, removing any borrowed props and listeners
pub(crate) fn ensure_drop_safety(&mut self, scope_id: ScopeId) {
let scope = &self.scopes[scope_id.0];
{
// Drop all element refs that could be invalidated when the component was rerun
let mut element_refs = self.scopes[scope_id.0].element_refs_to_drop.borrow_mut();
let element_refs_slab = &mut self.element_refs;
for element_ref in element_refs.drain(..) {
if let Some(element_ref) = element_refs_slab.get_mut(element_ref.0) {
*element_ref = None;
}
}
}
// 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 Reference)
// recursively call ensure_drop_safety on all children
let props = { scope.borrowed_props.borrow_mut().clone() };
for comp in props {
let comp = unsafe { &*comp };
match comp.scope.get() {
Some(child) if child != scope_id => self.ensure_drop_safety(child),
_ => (),
}
if let Ok(mut props) = comp.props.try_borrow_mut() {
*props = None;
}
}
let scope = &self.scopes[scope_id.0];
scope.borrowed_props.borrow_mut().clear();
// Now that all the references are gone, we can safely drop our own references in our listeners.
scope.drop_listeners();
}
}
impl ScopeState {
/// Drop all listeners that were allocated in the bump allocator.
pub(crate) fn drop_listeners(&self) {
let mut listeners = self.attributes_to_drop_before_render.borrow_mut();
listeners.drain(..).for_each(|listener| {
let listener = unsafe { &*listener };
if let AttributeValue::Listener(l) = &listener.value {
_ = l.take();
}
});
self.dirty_scopes.remove(&DirtyScope { height, id });
}
}
-76
View File
@@ -1,76 +0,0 @@
use crate::nodes::RenderReturn;
use crate::{Attribute, AttributeValue, VComponent};
use bumpalo::Bump;
use std::cell::RefCell;
use std::cell::{Cell, UnsafeCell};
pub(crate) struct BumpFrame {
pub bump: UnsafeCell<Bump>,
pub node: Cell<*const RenderReturn<'static>>,
// The bump allocator will not call the destructor of the objects it allocated. Attributes and props need to have there destructor called, so we keep a list of them to drop before the bump allocator is reset.
pub(crate) attributes_to_drop_before_reset: RefCell<Vec<*const Attribute<'static>>>,
pub(crate) props_to_drop_before_reset: RefCell<Vec<*const VComponent<'static>>>,
}
impl BumpFrame {
pub(crate) fn new(capacity: usize) -> Self {
let bump = Bump::with_capacity(capacity);
Self {
bump: UnsafeCell::new(bump),
node: Cell::new(std::ptr::null()),
attributes_to_drop_before_reset: Default::default(),
props_to_drop_before_reset: Default::default(),
}
}
/// Creates a new lifetime out of thin air
pub(crate) unsafe fn try_load_node<'b>(&self) -> Option<&'b RenderReturn<'b>> {
let node = self.node.get();
if node.is_null() {
return None;
}
unsafe { std::mem::transmute(&*node) }
}
pub(crate) fn bump(&self) -> &Bump {
unsafe { &*self.bump.get() }
}
pub(crate) fn add_attribute_to_drop(&self, attribute: *const Attribute<'static>) {
self.attributes_to_drop_before_reset
.borrow_mut()
.push(attribute);
}
/// Reset the bump allocator and drop all the attributes and props that were allocated in it.
///
/// # Safety
/// The caller must insure that no reference to anything allocated in the bump allocator is available after this function is called.
pub(crate) unsafe fn reset(&self) {
let mut attributes = self.attributes_to_drop_before_reset.borrow_mut();
attributes.drain(..).for_each(|attribute| {
let attribute = unsafe { &*attribute };
if let AttributeValue::Any(l) = &attribute.value {
_ = l.take();
}
});
let mut props = self.props_to_drop_before_reset.borrow_mut();
props.drain(..).for_each(|prop| {
let prop = unsafe { &*prop };
_ = prop.props.borrow_mut().take();
});
unsafe {
let bump = &mut *self.bump.get();
bump.reset();
}
}
}
impl Drop for BumpFrame {
fn drop(&mut self) {
unsafe { self.reset() }
}
}
-627
View File
@@ -1,627 +0,0 @@
use crate::any_props::AnyProps;
use crate::innerlude::{
AttributeType, BorrowedAttributeValue, ElementPath, ElementRef, MountedAttribute, VComponent,
VPlaceholder, VText,
};
use crate::mutations::Mutation;
use crate::mutations::Mutation::*;
use crate::nodes::VNode;
use crate::nodes::{DynamicNode, TemplateNode};
use crate::virtual_dom::VirtualDom;
use crate::{AttributeValue, ElementId, RenderReturn, ScopeId, Template};
use std::cell::Cell;
use std::iter::Peekable;
use TemplateNode::*;
#[cfg(debug_assertions)]
fn sort_bfs(paths: &[&'static [u8]]) -> Vec<(usize, &'static [u8])> {
let mut with_indecies = paths.iter().copied().enumerate().collect::<Vec<_>>();
with_indecies.sort_unstable_by(|(_, a), (_, b)| {
let mut a = a.iter();
let mut b = b.iter();
loop {
match (a.next(), b.next()) {
(Some(a), Some(b)) => {
if a != b {
return a.cmp(b);
}
}
// The shorter path goes first
(None, Some(_)) => return std::cmp::Ordering::Less,
(Some(_), None) => return std::cmp::Ordering::Greater,
(None, None) => return std::cmp::Ordering::Equal,
}
}
});
with_indecies
}
#[test]
#[cfg(debug_assertions)]
fn sorting() {
let r: [(usize, &[u8]); 5] = [
(0, &[0, 1]),
(1, &[0, 2]),
(2, &[1, 0]),
(3, &[1, 0, 1]),
(4, &[1, 2]),
];
assert_eq!(
sort_bfs(&[&[0, 1,], &[0, 2,], &[1, 0,], &[1, 0, 1,], &[1, 2,],]),
r
);
let r: [(usize, &[u8]); 6] = [
(0, &[0]),
(1, &[0, 1]),
(2, &[0, 1, 2]),
(3, &[1]),
(4, &[1, 2]),
(5, &[2]),
];
assert_eq!(
sort_bfs(&[&[0], &[0, 1], &[0, 1, 2], &[1], &[1, 2], &[2],]),
r
);
}
impl<'b> VirtualDom {
/// Create a new template [`VNode`] and write it to the [`Mutations`] buffer.
///
/// This method pushes the ScopeID to the internal scopestack and returns the number of nodes created.
pub(crate) fn create_scope(&mut self, scope: ScopeId, template: &'b VNode<'b>) -> usize {
self.runtime.scope_stack.borrow_mut().push(scope);
let nodes = self.create(template);
self.runtime.scope_stack.borrow_mut().pop();
nodes
}
/// Create this template and write its mutations
pub(crate) fn create(&mut self, node: &'b VNode<'b>) -> usize {
// check for a overriden template
#[cfg(debug_assertions)]
{
let (path, byte_index) = node.template.get().name.rsplit_once(':').unwrap();
if let Some(template) = self
.templates
.get(path)
.and_then(|map| map.get(&byte_index.parse().unwrap()))
{
node.template.set(*template);
}
}
// Initialize the root nodes slice
{
let mut nodes_mut = node.root_ids.borrow_mut();
let len = node.template.get().roots.len();
nodes_mut.resize(len, ElementId::default());
};
// Set this node id
node.stable_id.set(Some(self.next_vnode_ref(node)));
// The best renderers will have templates prehydrated and registered
// Just in case, let's create the template using instructions anyways
self.register_template(node.template.get());
// we know that this will generate at least one mutation per node
self.mutations
.edits
.reserve(node.template.get().roots.len());
// Walk the roots, creating nodes and assigning IDs
// nodes in an iterator of ((dynamic_node_index, sorted_index), path)
// todo: adjust dynamic nodes to be in the order of roots and then leaves (ie BFS)
#[cfg(not(debug_assertions))]
let (mut attrs, mut nodes) = (
node.template
.get()
.attr_paths
.iter()
.copied()
.enumerate()
.peekable(),
node.template
.get()
.node_paths
.iter()
.copied()
.enumerate()
.map(|(i, path)| ((i, i), path))
.peekable(),
);
// If this is a debug build, we need to check that the paths are in the correct order because hot reloading can cause scrambled states
#[cfg(debug_assertions)]
let (attrs_sorted, nodes_sorted) = {
(
sort_bfs(node.template.get().attr_paths),
sort_bfs(node.template.get().node_paths),
)
};
#[cfg(debug_assertions)]
let (mut attrs, mut nodes) = {
(
attrs_sorted.into_iter().peekable(),
nodes_sorted
.iter()
.copied()
.enumerate()
.map(|(i, (id, path))| ((id, i), path))
.peekable(),
)
};
node.template
.get()
.roots
.iter()
.enumerate()
.map(|(idx, root)| match root {
DynamicText { id } | Dynamic { id } => {
nodes.next().unwrap();
self.write_dynamic_root(node, *id)
}
Element { .. } => {
#[cfg(not(debug_assertions))]
let id = self.write_element_root(node, idx, &mut attrs, &mut nodes, &[]);
#[cfg(debug_assertions)]
let id =
self.write_element_root(node, idx, &mut attrs, &mut nodes, &nodes_sorted);
id
}
Text { .. } => self.write_static_text_root(node, idx),
})
.sum()
}
fn write_static_text_root(&mut self, node: &VNode, idx: usize) -> usize {
// Simply just load the template root, no modifications needed
self.load_template_root(node, idx);
// Text producs just one node on the stack
1
}
fn write_dynamic_root(&mut self, template: &'b VNode<'b>, idx: usize) -> usize {
use DynamicNode::*;
match &template.dynamic_nodes[idx] {
node @ Component { .. } | node @ Fragment(_) => {
let template_ref = ElementRef {
path: ElementPath {
path: template.template.get().node_paths[idx],
},
template: template.stable_id().unwrap(),
scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)),
};
self.create_dynamic_node(template_ref, node)
}
Placeholder(VPlaceholder { id, parent }) => {
let template_ref = ElementRef {
path: ElementPath {
path: template.template.get().node_paths[idx],
},
template: template.stable_id().unwrap(),
scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)),
};
parent.set(Some(template_ref));
let id = self.set_slot(id);
self.mutations.push(CreatePlaceholder { id });
1
}
Text(VText { id, value }) => {
let id = self.set_slot(id);
self.create_static_text(value, id);
1
}
}
}
fn create_static_text(&mut self, value: &str, id: ElementId) {
// Safety: we promise not to re-alias this text later on after committing it to the mutation
let unbounded_text: &str = unsafe { std::mem::transmute(value) };
self.mutations.push(CreateTextNode {
value: unbounded_text,
id,
});
}
/// We write all the descendent data for this element
///
/// Elements can contain other nodes - and those nodes can be dynamic or static
///
/// We want to make sure we write these nodes while on top of the root
fn write_element_root(
&mut self,
template: &'b VNode<'b>,
root_idx: usize,
dynamic_attrs: &mut Peekable<impl Iterator<Item = (usize, &'static [u8])>>,
dynamic_nodes_iter: &mut Peekable<impl Iterator<Item = ((usize, usize), &'static [u8])>>,
dynamic_nodes: &[(usize, &'static [u8])],
) -> usize {
// Load the template root and get the ID for the node on the stack
let root_on_stack = self.load_template_root(template, root_idx);
// Write all the attributes below this root
self.write_attrs_on_root(dynamic_attrs, root_idx as u8, root_on_stack, template);
// Load in all of the placeholder or dynamic content under this root too
self.load_placeholders(dynamic_nodes_iter, dynamic_nodes, root_idx as u8, template);
1
}
/// Load all of the placeholder nodes for descendents of this root node
///
/// ```rust, ignore
/// rsx! {
/// div {
/// // This is a placeholder
/// some_value,
///
/// // Load this too
/// "{some_text}"
/// }
/// }
/// ```
#[allow(unused)]
fn load_placeholders(
&mut self,
dynamic_nodes_iter: &mut Peekable<impl Iterator<Item = ((usize, usize), &'static [u8])>>,
dynamic_nodes: &[(usize, &'static [u8])],
root_idx: u8,
template: &'b VNode<'b>,
) {
let (start, end) = match collect_dyn_node_range(dynamic_nodes_iter, root_idx) {
Some((a, b)) => (a, b),
None => return,
};
// If hot reloading is enabled, we need to map the sorted index to the original index of the dynamic node. If it is disabled, we can just use the sorted index
#[cfg(not(debug_assertions))]
let reversed_iter = (start..=end).rev();
#[cfg(debug_assertions)]
let reversed_iter = (start..=end)
.rev()
.map(|sorted_index| dynamic_nodes[sorted_index].0);
for idx in reversed_iter {
let boundary_ref = ElementRef {
path: ElementPath {
path: template.template.get().node_paths[idx],
},
template: template.stable_id().unwrap(),
scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)),
};
let m = self.create_dynamic_node(boundary_ref, &template.dynamic_nodes[idx]);
if m > 0 {
// The path is one shorter because the top node is the root
let path = &template.template.get().node_paths[idx][1..];
self.mutations.push(ReplacePlaceholder { m, path });
}
}
}
fn write_attrs_on_root(
&mut self,
attrs: &mut Peekable<impl Iterator<Item = (usize, &'static [u8])>>,
root_idx: u8,
root: ElementId,
node: &'b VNode<'b>,
) {
while let Some((mut attr_id, path)) =
attrs.next_if(|(_, p)| p.first().copied() == Some(root_idx))
{
let id = self.assign_static_node_as_dynamic(path, root);
loop {
self.write_attribute_type(node, &node.dynamic_attrs[attr_id], attr_id, id);
// Only push the dynamic attributes forward if they match the current path (same element)
match attrs.next_if(|(_, p)| *p == path) {
Some((next_attr_id, _)) => attr_id = next_attr_id,
None => break,
}
}
}
}
fn write_attribute_type(
&mut self,
vnode: &'b VNode<'b>,
attribute: &'b MountedAttribute<'b>,
idx: usize,
id: ElementId,
) {
// Make sure we set the attribute's associated id
attribute.mounted_element.set(id);
match &attribute.ty {
AttributeType::Single(attribute) => self.write_attribute(vnode, attribute, idx, id),
AttributeType::Many(attribute) => {
for attribute in *attribute {
self.write_attribute(vnode, attribute, idx, id);
}
}
}
}
pub(crate) fn write_attribute(
&mut self,
vnode: &'b VNode<'b>,
attribute: &'b crate::Attribute<'b>,
idx: usize,
id: ElementId,
) {
// Safety: we promise not to re-alias this text later on after committing it to the mutation
let unbounded_name: &str = unsafe { std::mem::transmute(attribute.name) };
match &attribute.value {
AttributeValue::Listener(_) => {
let path = &vnode.template.get().attr_paths[idx];
let element_ref = ElementRef {
path: ElementPath { path },
template: vnode.stable_id().unwrap(),
scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)),
};
self.elements[id.0] = Some(element_ref);
self.mutations.push(NewEventListener {
// all listeners start with "on"
name: &unbounded_name[2..],
id,
})
}
_ => {
// Safety: we promise not to re-alias this text later on after committing it to the mutation
let value: BorrowedAttributeValue<'b> = (&attribute.value).into();
let unbounded_value = unsafe { std::mem::transmute(value) };
self.mutations.push(SetAttribute {
name: unbounded_name,
value: unbounded_value,
ns: attribute.namespace,
id,
})
}
}
}
fn load_template_root(&mut self, template: &VNode, root_idx: usize) -> ElementId {
// Get an ID for this root since it's a real root
let this_id = self.next_element();
template.root_ids.borrow_mut()[root_idx] = this_id;
self.mutations.push(LoadTemplate {
name: template.template.get().name,
index: root_idx,
id: this_id,
});
this_id
}
/// We have some dynamic attributes attached to a some node
///
/// That node needs to be loaded at runtime, so we need to give it an ID
///
/// If the node in question is on the stack, we just return that ID
///
/// If the node is not on the stack, we create a new ID for it and assign it
fn assign_static_node_as_dynamic(
&mut self,
path: &'static [u8],
this_id: ElementId,
) -> ElementId {
if path.len() == 1 {
return this_id;
}
// if attribute is on a root node, then we've already created the element
// Else, it's deep in the template and we should create a new id for it
let id = self.next_element();
self.mutations.push(Mutation::AssignId {
path: &path[1..],
id,
});
id
}
/// Insert a new template into the VirtualDom's template registry
pub(crate) fn register_template_first_byte_index(&mut self, mut template: Template<'static>) {
// First, make sure we mark the template as seen, regardless if we process it
let (path, _) = template.name.rsplit_once(':').unwrap();
if let Some((_, old_template)) = self
.templates
.entry(path)
.or_default()
.iter_mut()
.min_by_key(|(byte_index, _)| **byte_index)
{
// the byte index of the hot reloaded template could be different
template.name = old_template.name;
*old_template = template;
} else {
// This is a template without any current instances
self.templates
.entry(path)
.or_default()
.insert(usize::MAX, template);
}
// If it's all dynamic nodes, then we don't need to register it
if !template.is_completely_dynamic() {
self.mutations.templates.push(template);
}
}
/// Insert a new template into the VirtualDom's template registry
// used in conditional compilation
#[allow(unused_mut)]
pub(crate) fn register_template(&mut self, mut template: Template<'static>) {
let (path, byte_index) = template.name.rsplit_once(':').unwrap();
let byte_index = byte_index.parse::<usize>().unwrap();
// First, check if we've already seen this template
if self
.templates
.get(&path)
.filter(|set| set.contains_key(&byte_index))
.is_none()
{
// if hot reloading is enabled, then we need to check for a template that has overriten this one
#[cfg(debug_assertions)]
if let Some(mut new_template) = self
.templates
.get_mut(path)
.and_then(|map| map.remove(&usize::MAX))
{
// the byte index of the hot reloaded template could be different
new_template.name = template.name;
template = new_template;
}
self.templates
.entry(path)
.or_default()
.insert(byte_index, template);
// If it's all dynamic nodes, then we don't need to register it
if !template.is_completely_dynamic() {
self.mutations.templates.push(template);
}
}
}
pub(crate) fn create_dynamic_node(
&mut self,
parent: ElementRef,
node: &'b DynamicNode<'b>,
) -> usize {
use DynamicNode::*;
match node {
Text(text) => self.create_dynamic_text(parent, text),
Placeholder(place) => self.create_placeholder(place, parent),
Component(component) => self.create_component_node(Some(parent), component),
Fragment(frag) => self.create_children(*frag, Some(parent)),
}
}
fn create_dynamic_text(&mut self, parent: ElementRef, text: &'b VText<'b>) -> usize {
// Allocate a dynamic element reference for this text node
let new_id = self.next_element();
// Make sure the text node is assigned to the correct element
text.id.set(Some(new_id));
// Safety: we promise not to re-alias this text later on after committing it to the mutation
let value = unsafe { std::mem::transmute(text.value) };
// Add the mutation to the list
self.mutations.push(HydrateText {
id: new_id,
path: &parent.path.path[1..],
value,
});
// Since we're hydrating an existing node, we don't create any new nodes
0
}
pub(crate) fn create_placeholder(
&mut self,
placeholder: &VPlaceholder,
parent: ElementRef,
) -> usize {
// Allocate a dynamic element reference for this text node
let id = self.next_element();
// Make sure the text node is assigned to the correct element
placeholder.id.set(Some(id));
// Assign the placeholder's parent
placeholder.parent.set(Some(parent));
// Assign the ID to the existing node in the template
self.mutations.push(AssignId {
path: &parent.path.path[1..],
id,
});
// Since the placeholder is already in the DOM, we don't create any new nodes
0
}
pub(super) fn create_component_node(
&mut self,
parent: Option<ElementRef>,
component: &'b VComponent<'b>,
) -> usize {
use RenderReturn::*;
// Load up a ScopeId for this vcomponent
let scope = self.load_scope_from_vcomponent(component);
component.scope.set(Some(scope));
match unsafe { self.run_scope(scope).extend_lifetime_ref() } {
// Create the component's root element
Ready(t) => {
self.assign_boundary_ref(parent, t);
self.create_scope(scope, t)
}
Aborted(t) => self.mount_aborted(t, parent),
}
}
/// Load a scope from a vcomponent. If the props don't exist, that means the component is currently "live"
fn load_scope_from_vcomponent(&mut self, component: &VComponent) -> ScopeId {
component
.props
.take()
.map(|props| {
let unbounded_props: Box<dyn AnyProps> = unsafe { std::mem::transmute(props) };
self.new_scope(unbounded_props, component.name).context().id
})
.unwrap_or_else(|| component.scope.get().unwrap())
}
fn mount_aborted(&mut self, placeholder: &VPlaceholder, parent: Option<ElementRef>) -> usize {
let id = self.next_element();
self.mutations.push(Mutation::CreatePlaceholder { id });
placeholder.id.set(Some(id));
placeholder.parent.set(parent);
1
}
fn set_slot(&mut self, slot: &'b Cell<Option<ElementId>>) -> ElementId {
let id = self.next_element();
slot.set(Some(id));
id
}
}
fn collect_dyn_node_range(
dynamic_nodes: &mut Peekable<impl Iterator<Item = ((usize, usize), &'static [u8])>>,
root_idx: u8,
) -> Option<(usize, usize)> {
let start = match dynamic_nodes.peek() {
Some(((_, idx), [first, ..])) if *first == root_idx => *idx,
_ => return None,
};
let mut end = start;
while let Some(((_, idx), p)) =
dynamic_nodes.next_if(|(_, p)| matches!(p, [idx, ..] if *idx == root_idx))
{
if p.len() == 1 {
continue;
}
end = idx;
}
Some((start, end))
}
File diff suppressed because it is too large. Load diff
+143
View File
@@ -0,0 +1,143 @@
use std::ops::{Deref, DerefMut};
use crate::{
any_props::AnyProps,
innerlude::{DirtyScope, ElementRef, MountId, VComponent, WriteMutations},
nodes::RenderReturn,
nodes::VNode,
scopes::ScopeId,
virtual_dom::VirtualDom,
};
impl VirtualDom {
pub(crate) fn diff_scope(
&mut self,
to: &mut impl WriteMutations,
scope: ScopeId,
new_nodes: RenderReturn,
) {
self.runtime.scope_stack.borrow_mut().push(scope);
let scope_state = &mut self.scopes[scope.0];
// Load the old and new bump arenas
let new = &new_nodes;
let old = scope_state.last_rendered_node.take().unwrap();
old.diff_node(new, self, to);
let scope_state = &mut self.scopes[scope.0];
scope_state.last_rendered_node = Some(new_nodes);
self.runtime.scope_stack.borrow_mut().pop();
}
/// Create a new template [`VNode`] and write it to the [`Mutations`] buffer.
///
/// This method pushes the ScopeID to the internal scopestack and returns the number of nodes created.
pub(crate) fn create_scope(
&mut self,
to: &mut impl WriteMutations,
scope: ScopeId,
new_node: RenderReturn,
parent: Option<ElementRef>,
) -> usize {
self.runtime.scope_stack.borrow_mut().push(scope);
// Create the node
let nodes = new_node.create(self, to, parent);
// Then set the new node as the last rendered node
self.scopes[scope.0].last_rendered_node = Some(new_node);
self.runtime.scope_stack.borrow_mut().pop();
nodes
}
}
impl VNode {
pub(crate) fn diff_vcomponent(
&self,
mount: MountId,
idx: usize,
new: &VComponent,
old: &VComponent,
scope_id: ScopeId,
parent: Option<ElementRef>,
dom: &mut VirtualDom,
to: &mut impl WriteMutations,
) {
// Replace components that have different render fns
if old.render_fn != new.render_fn {
return self.replace_vcomponent(mount, idx, new, parent, dom, to);
}
// copy out the box for both
let old_scope = &mut dom.scopes[scope_id.0];
let old_props: &mut dyn AnyProps = old_scope.props.deref_mut();
let new_props: &dyn AnyProps = new.props.deref();
// If the props are static, then we try to memoize by setting the new with the old
// The target ScopeState still has the reference to the old props, so there's no need to update anything
// This also implicitly drops the new props since they're not used
if old_props.memoize(new_props.props()) {
tracing::trace!(
"Memoized props for component {:#?} ({})",
scope_id,
old_scope.state().name
);
return;
}
// First, move over the props from the old to the new, dropping old props in the process
dom.scopes[scope_id.0].props = new.props.duplicate();
// Now run the component and diff it
let new = dom.run_scope(scope_id);
dom.diff_scope(to, scope_id, new);
let height = dom.runtime.get_state(scope_id).unwrap().height;
dom.dirty_scopes.remove(&DirtyScope {
height,
id: scope_id,
});
}
fn replace_vcomponent(
&self,
mount: MountId,
idx: usize,
new: &VComponent,
parent: Option<ElementRef>,
dom: &mut VirtualDom,
to: &mut impl WriteMutations,
) {
let scope = ScopeId(dom.mounts[mount.0].mounted_dynamic_nodes[idx]);
let m = self.create_component_node(mount, idx, new, parent, dom, to);
// Instead of *just* removing it, we can use the replace mutation
dom.remove_component_node(to, scope, Some(m), true);
}
pub(super) fn create_component_node(
&self,
mount: MountId,
idx: usize,
component: &VComponent,
parent: Option<ElementRef>,
dom: &mut VirtualDom,
to: &mut impl WriteMutations,
) -> usize {
// Load up a ScopeId for this vcomponent. If it's already mounted, then we can just use that
let scope = dom
.new_scope(component.props.duplicate(), component.name)
.state()
.id;
// Store the scope id for the next render
dom.mounts[mount.0].mounted_dynamic_nodes[idx] = scope.0;
let new = dom.run_scope(scope);
dom.create_scope(to, scope, new, parent)
}
}
+469
View File
@@ -0,0 +1,469 @@
use crate::{
innerlude::{ElementRef, WriteMutations},
nodes::VNode,
DynamicNode, ScopeId, TemplateNode, VirtualDom,
};
use rustc_hash::{FxHashMap, FxHashSet};
impl VirtualDom {
pub(crate) fn diff_non_empty_fragment(
&mut self,
to: &mut impl WriteMutations,
old: &[VNode],
new: &[VNode],
parent: Option<ElementRef>,
) {
let new_is_keyed = new[0].key.is_some();
let old_is_keyed = old[0].key.is_some();
debug_assert!(
new.iter().all(|n| n.key.is_some() == new_is_keyed),
"all siblings must be keyed or all siblings must be non-keyed"
);
debug_assert!(
old.iter().all(|o| o.key.is_some() == old_is_keyed),
"all siblings must be keyed or all siblings must be non-keyed"
);
if new_is_keyed && old_is_keyed {
self.diff_keyed_children(to, old, new, parent);
} else {
self.diff_non_keyed_children(to, old, new, parent);
}
}
// Diff children that are not keyed.
//
// The parent must be on the top of the change list stack when entering this
// function:
//
// [... parent]
//
// the change list stack is in the same state when this function returns.
fn diff_non_keyed_children(
&mut self,
to: &mut impl WriteMutations,
old: &[VNode],
new: &[VNode],
parent: Option<ElementRef>,
) {
use std::cmp::Ordering;
// Handled these cases in `diff_children` before calling this function.
debug_assert!(!new.is_empty());
debug_assert!(!old.is_empty());
match old.len().cmp(&new.len()) {
Ordering::Greater => self.remove_nodes(to, &old[new.len()..], None),
Ordering::Less => {
self.create_and_insert_after(to, &new[old.len()..], old.last().unwrap(), parent)
}
Ordering::Equal => {}
}
for (new, old) in new.iter().zip(old.iter()) {
old.diff_node(new, self, to);
}
}
// Diffing "keyed" children.
//
// With keyed children, we care about whether we delete, move, or create nodes
// versus mutate existing nodes in place. Presumably there is some sort of CSS
// transition animation that makes the virtual DOM diffing algorithm
// observable. By specifying keys for nodes, we know which virtual DOM nodes
// must reuse (or not reuse) the same physical DOM nodes.
//
// This is loosely based on Inferno's keyed patching implementation. However, we
// have to modify the algorithm since we are compiling the diff down into change
// list instructions that will be executed later, rather than applying the
// changes to the DOM directly as we compare virtual DOMs.
//
// https://github.com/infernojs/inferno/blob/36fd96/packages/inferno/src/DOM/patching.ts#L530-L739
//
// The stack is empty upon entry.
fn diff_keyed_children(
&mut self,
to: &mut impl WriteMutations,
old: &[VNode],
new: &[VNode],
parent: Option<ElementRef>,
) {
if cfg!(debug_assertions) {
let mut keys = rustc_hash::FxHashSet::default();
let mut assert_unique_keys = |children: &[VNode]| {
keys.clear();
for child in children {
let key = child.key.clone();
debug_assert!(
key.is_some(),
"if any sibling is keyed, all siblings must be keyed"
);
keys.insert(key);
}
debug_assert_eq!(
children.len(),
keys.len(),
"keyed siblings must each have a unique key"
);
};
assert_unique_keys(old);
assert_unique_keys(new);
}
// First up, we diff all the nodes with the same key at the beginning of the
// children.
//
// `shared_prefix_count` is the count of how many nodes at the start of
// `new` and `old` share the same keys.
let (left_offset, right_offset) = match self.diff_keyed_ends(to, old, new, parent) {
Some(count) => count,
None => return,
};
// Ok, we now hopefully have a smaller range of children in the middle
// within which to re-order nodes with the same keys, remove old nodes with
// now-unused keys, and create new nodes with fresh keys.
let old_middle = &old[left_offset..(old.len() - right_offset)];
let new_middle = &new[left_offset..(new.len() - right_offset)];
debug_assert!(
!((old_middle.len() == new_middle.len()) && old_middle.is_empty()),
"keyed children must have the same number of children"
);
if new_middle.is_empty() {
// remove the old elements
self.remove_nodes(to, old_middle, None);
} else if old_middle.is_empty() {
// there were no old elements, so just create the new elements
// we need to find the right "foothold" though - we shouldn't use the "append" at all
if left_offset == 0 {
// insert at the beginning of the old list
let foothold = &old[old.len() - right_offset];
self.create_and_insert_before(to, new_middle, foothold, parent);
} else if right_offset == 0 {
// insert at the end the old list
let foothold = old.last().unwrap();
self.create_and_insert_after(to, new_middle, foothold, parent);
} else {
// inserting in the middle
let foothold = &old[left_offset - 1];
self.create_and_insert_after(to, new_middle, foothold, parent);
}
} else {
self.diff_keyed_middle(to, old_middle, new_middle, parent);
}
}
/// Diff both ends of the children that share keys.
///
/// Returns a left offset and right offset of that indicates a smaller section to pass onto the middle diffing.
///
/// If there is no offset, then this function returns None and the diffing is complete.
fn diff_keyed_ends(
&mut self,
to: &mut impl WriteMutations,
old: &[VNode],
new: &[VNode],
parent: Option<ElementRef>,
) -> Option<(usize, usize)> {
let mut left_offset = 0;
for (old, new) in old.iter().zip(new.iter()) {
// abort early if we finally run into nodes with different keys
if old.key != new.key {
break;
}
old.diff_node(new, self, to);
left_offset += 1;
}
// If that was all of the old children, then create and append the remaining
// new children and we're finished.
if left_offset == old.len() {
self.create_and_insert_after(to, &new[left_offset..], old.last().unwrap(), parent);
return None;
}
// And if that was all of the new children, then remove all of the remaining
// old children and we're finished.
if left_offset == new.len() {
self.remove_nodes(to, &old[left_offset..], None);
return None;
}
// if the shared prefix is less than either length, then we need to walk backwards
let mut right_offset = 0;
for (old, new) in old.iter().rev().zip(new.iter().rev()) {
// abort early if we finally run into nodes with different keys
if old.key != new.key {
break;
}
old.diff_node(new, self, to);
right_offset += 1;
}
Some((left_offset, right_offset))
}
// The most-general, expensive code path for keyed children diffing.
//
// We find the longest subsequence within `old` of children that are relatively
// ordered the same way in `new` (via finding a longest-increasing-subsequence
// of the old child's index within `new`). The children that are elements of
// this subsequence will remain in place, minimizing the number of DOM moves we
// will have to do.
//
// Upon entry to this function, the change list stack must be empty.
//
// This function will load the appropriate nodes onto the stack and do diffing in place.
//
// Upon exit from this function, it will be restored to that same self.
#[allow(clippy::too_many_lines)]
fn diff_keyed_middle(
&mut self,
to: &mut impl WriteMutations,
old: &[VNode],
new: &[VNode],
parent: Option<ElementRef>,
) {
/*
1. Map the old keys into a numerical ordering based on indices.
2. Create a map of old key to its index
3. Map each new key to the old key, carrying over the old index.
- IE if we have ABCD becomes BACD, our sequence would be 1,0,2,3
- if we have ABCD to ABDE, our sequence would be 0,1,3,MAX because E doesn't exist
now, we should have a list of integers that indicates where in the old list the new items mapto.
4. Compute the LIS of this list
- this indicates the longest list of new children that won't need to be moved.
5. Identify which nodes need to be removed
6. Identify which nodes will need to be diffed
7. Going along each item in the new list, create it and insert it before the next closest item in the LIS.
- if the item already existed, just move it to the right place.
8. Finally, generate instructions to remove any old children.
9. Generate instructions to finally diff children that are the same between both
*/
// 0. Debug sanity checks
// Should have already diffed the shared-key prefixes and suffixes.
debug_assert_ne!(new.first().map(|i| &i.key), old.first().map(|i| &i.key));
debug_assert_ne!(new.last().map(|i| &i.key), old.last().map(|i| &i.key));
// 1. Map the old keys into a numerical ordering based on indices.
// 2. Create a map of old key to its index
// IE if the keys were A B C, then we would have (A, 1) (B, 2) (C, 3).
let old_key_to_old_index = old
.iter()
.enumerate()
.map(|(i, o)| (o.key.as_ref().unwrap(), i))
.collect::<FxHashMap<_, _>>();
let mut shared_keys = FxHashSet::default();
// 3. Map each new key to the old key, carrying over the old index.
let new_index_to_old_index = new
.iter()
.map(|node| {
let key = node.key.as_ref().unwrap();
if let Some(&index) = old_key_to_old_index.get(&key) {
shared_keys.insert(key);
index
} else {
u32::MAX as usize
}
})
.collect::<Vec<_>>();
// If none of the old keys are reused by the new children, then we remove all the remaining old children and
// create the new children afresh.
if shared_keys.is_empty() {
if !old.is_empty() {
let m = self.create_children(to, new, parent);
self.remove_nodes(to, old, Some(m));
} else {
// I think this is wrong - why are we appending?
// only valid of the if there are no trailing elements
// self.create_and_append_children(new);
todo!("we should never be appending - just creating N");
}
return;
}
// remove any old children that are not shared
// todo: make this an iterator
for child in old {
let key = child.key.as_ref().unwrap();
if !shared_keys.contains(&key) {
child.remove_node(self, to, None, true);
}
}
// 4. Compute the LIS of this list
let mut lis_sequence = Vec::with_capacity(new_index_to_old_index.len());
let mut predecessors = vec![0; new_index_to_old_index.len()];
let mut starts = vec![0; new_index_to_old_index.len()];
longest_increasing_subsequence::lis_with(
&new_index_to_old_index,
&mut lis_sequence,
|a, b| a < b,
&mut predecessors,
&mut starts,
);
// the lis comes out backwards, I think. can't quite tell.
lis_sequence.sort_unstable();
// if a new node gets u32 max and is at the end, then it might be part of our LIS (because u32 max is a valid LIS)
if lis_sequence.last().map(|f| new_index_to_old_index[*f]) == Some(u32::MAX as usize) {
lis_sequence.pop();
}
for idx in &lis_sequence {
old[new_index_to_old_index[*idx]].diff_node(&new[*idx], self, to);
}
let mut nodes_created = 0;
// add mount instruction for the first items not covered by the lis
let last = *lis_sequence.last().unwrap();
if last < (new.len() - 1) {
for (idx, new_node) in new[(last + 1)..].iter().enumerate() {
let new_idx = idx + last + 1;
let old_index = new_index_to_old_index[new_idx];
if old_index == u32::MAX as usize {
nodes_created += new_node.create(self, to, parent);
} else {
old[old_index].diff_node(new_node, self, to);
nodes_created += new_node.push_all_real_nodes(self, to);
}
}
let id = new[last].find_last_element(self);
if nodes_created > 0 {
to.insert_nodes_after(id, nodes_created)
}
nodes_created = 0;
}
// for each spacing, generate a mount instruction
let mut lis_iter = lis_sequence.iter().rev();
let mut last = *lis_iter.next().unwrap();
for next in lis_iter {
if last - next > 1 {
for (idx, new_node) in new[(next + 1)..last].iter().enumerate() {
let new_idx = idx + next + 1;
let old_index = new_index_to_old_index[new_idx];
if old_index == u32::MAX as usize {
nodes_created += new_node.create(self, to, parent);
} else {
old[old_index].diff_node(new_node, self, to);
nodes_created += new_node.push_all_real_nodes(self, to);
}
}
let id = new[last].find_first_element(self);
if nodes_created > 0 {
to.insert_nodes_before(id, nodes_created);
}
nodes_created = 0;
}
last = *next;
}
// add mount instruction for the last items not covered by the lis
let first_lis = *lis_sequence.first().unwrap();
if first_lis > 0 {
for (idx, new_node) in new[..first_lis].iter().enumerate() {
let old_index = new_index_to_old_index[idx];
if old_index == u32::MAX as usize {
nodes_created += new_node.create(self, to, parent);
} else {
old[old_index].diff_node(new_node, self, to);
nodes_created += new_node.push_all_real_nodes(self, to);
}
}
let id = new[first_lis].find_first_element(self);
if nodes_created > 0 {
to.insert_nodes_before(id, nodes_created);
}
}
}
fn create_and_insert_before(
&mut self,
to: &mut impl WriteMutations,
new: &[VNode],
before: &VNode,
parent: Option<ElementRef>,
) {
let m = self.create_children(to, new, parent);
let id = before.find_first_element(self);
to.insert_nodes_before(id, m);
}
fn create_and_insert_after(
&mut self,
to: &mut impl WriteMutations,
new: &[VNode],
after: &VNode,
parent: Option<ElementRef>,
) {
let m = self.create_children(to, new, parent);
let id = after.find_last_element(self);
to.insert_nodes_after(id, m);
}
}
impl VNode {
/// Push all the real nodes on the stack
pub(crate) fn push_all_real_nodes(
&self,
dom: &VirtualDom,
to: &mut impl WriteMutations,
) -> usize {
let template = self.template.get();
let mount = dom.mounts.get(self.mount.get().0).unwrap();
template
.roots
.iter()
.enumerate()
.map(|(root_idx, _)| match &self.template.get().roots[root_idx] {
TemplateNode::Dynamic { id: idx } => match &self.dynamic_nodes[*idx] {
DynamicNode::Placeholder(_) | DynamicNode::Text(_) => {
to.push_root(mount.root_ids[root_idx]);
1
}
DynamicNode::Fragment(nodes) => {
let mut accumulated = 0;
for node in nodes {
accumulated += node.push_all_real_nodes(dom, to);
}
accumulated
}
DynamicNode::Component(_) => {
let scope = ScopeId(mount.mounted_dynamic_nodes[*idx]);
let node = dom.get_scope(scope).unwrap().root_node();
node.push_all_real_nodes(dom, to)
}
},
_ => {
to.push_root(mount.root_ids[root_idx]);
1
}
})
.sum()
}
}
+199
View File
@@ -0,0 +1,199 @@
#![allow(clippy::too_many_arguments)]
use crate::{
arena::ElementId,
innerlude::{ElementRef, MountId, WriteMutations},
nodes::VNode,
scopes::ScopeId,
virtual_dom::VirtualDom,
Template, TemplateNode,
};
mod component;
mod iterator;
mod node;
impl VirtualDom {
pub(crate) fn create_children<'a>(
&mut self,
to: &mut impl WriteMutations,
nodes: impl IntoIterator<Item = &'a VNode>,
parent: Option<ElementRef>,
) -> usize {
nodes
.into_iter()
.map(|child| child.create(self, to, parent))
.sum()
}
/// Simply replace a placeholder with a list of nodes
fn replace_placeholder<'a>(
&mut self,
to: &mut impl WriteMutations,
placeholder_id: ElementId,
r: impl IntoIterator<Item = &'a VNode>,
parent: Option<ElementRef>,
) {
let m = self.create_children(to, r, parent);
to.replace_node_with(placeholder_id, m);
self.reclaim(placeholder_id);
}
fn nodes_to_placeholder(
&mut self,
to: &mut impl WriteMutations,
mount: MountId,
dyn_node_idx: usize,
old_nodes: &[VNode],
) {
// Create the placeholder first, ensuring we get a dedicated ID for the placeholder
let placeholder = self.next_element();
// Set the id of the placeholder
self.mounts[mount.0].mounted_dynamic_nodes[dyn_node_idx] = placeholder.0;
to.create_placeholder(placeholder);
self.replace_nodes(to, old_nodes, 1);
}
/// Replace many nodes with a number of nodes on the stack
fn replace_nodes(&mut self, to: &mut impl WriteMutations, nodes: &[VNode], m: usize) {
debug_assert!(
!nodes.is_empty(),
"replace_nodes must have at least one node"
);
// We want to optimize the replace case to use one less mutation if possible
// Instead of *just* removing it, we can use the replace mutation
self.remove_nodes(to, nodes, Some(m));
}
/// Remove these nodes from the dom
/// Wont generate mutations for the inner nodes
fn remove_nodes(
&mut self,
to: &mut impl WriteMutations,
nodes: &[VNode],
replace_with: Option<usize>,
) {
for (i, node) in nodes.iter().rev().enumerate() {
let last_node = i == nodes.len() - 1;
node.remove_node(self, to, replace_with.filter(|_| last_node), true);
}
}
pub(crate) fn remove_component_node(
&mut self,
to: &mut impl WriteMutations,
scope: ScopeId,
replace_with: Option<usize>,
gen_muts: bool,
) {
// Remove the component from the dom
if let Some(node) = self.scopes[scope.0].last_rendered_node.take() {
node.remove_node(self, to, replace_with, gen_muts)
};
// Now drop all the resources
self.drop_scope(scope);
}
/// Insert a new template into the VirtualDom's template registry
// used in conditional compilation
#[allow(unused_mut)]
pub(crate) fn register_template(
&mut self,
to: &mut impl WriteMutations,
mut template: Template,
) {
let (path, byte_index) = template.name.rsplit_once(':').unwrap();
let byte_index = byte_index.parse::<usize>().unwrap();
// First, check if we've already seen this template
if self
.templates
.get(&path)
.filter(|set| set.contains_key(&byte_index))
.is_none()
{
// if hot reloading is enabled, then we need to check for a template that has overriten this one
#[cfg(debug_assertions)]
if let Some(mut new_template) = self
.templates
.get_mut(path)
.and_then(|map| map.remove(&usize::MAX))
{
// the byte index of the hot reloaded template could be different
new_template.name = template.name;
template = new_template;
}
self.templates
.entry(path)
.or_default()
.insert(byte_index, template);
// If it's all dynamic nodes, then we don't need to register it
if !template.is_completely_dynamic() {
to.register_template(template)
}
}
}
/// Insert a new template into the VirtualDom's template registry
pub(crate) fn register_template_first_byte_index(&mut self, mut template: Template) {
// First, make sure we mark the template as seen, regardless if we process it
let (path, _) = template.name.rsplit_once(':').unwrap();
if let Some((_, old_template)) = self
.templates
.entry(path)
.or_default()
.iter_mut()
.min_by_key(|(byte_index, _)| **byte_index)
{
// the byte index of the hot reloaded template could be different
template.name = old_template.name;
*old_template = template;
} else {
// This is a template without any current instances
self.templates
.entry(path)
.or_default()
.insert(usize::MAX, template);
}
// If it's all dynamic nodes, then we don't need to register it
if !template.is_completely_dynamic() {
self.queued_templates.push(template);
}
}
}
/// We can apply various optimizations to dynamic nodes that are the single child of their parent.
///
/// IE
/// - for text - we can use SetTextContent
/// - for clearing children we can use RemoveChildren
/// - for appending children we can use AppendChildren
#[allow(dead_code)]
fn is_dyn_node_only_child(node: &VNode, idx: usize) -> bool {
let template = node.template.get();
let path = template.node_paths[idx];
// use a loop to index every static node's children until the path has run out
// only break if the last path index is a dynamic node
let mut static_node = &template.roots[path[0] as usize];
for i in 1..path.len() - 1 {
match static_node {
TemplateNode::Element { children, .. } => static_node = &children[path[i] as usize],
_ => return false,
}
}
match static_node {
TemplateNode::Element { children, .. } => children.len() == 1,
_ => false,
}
}
File diff suppressed because it is too large. Load diff
+13 -1
View File
@@ -2,12 +2,24 @@ use std::hash::Hash;
use crate::ScopeId;
#[derive(Debug, Clone, Eq, PartialOrd, Ord)]
#[derive(Debug, Clone, Eq)]
pub struct DirtyScope {
pub height: u32,
pub id: ScopeId,
}
impl PartialOrd for DirtyScope {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for DirtyScope {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.height.cmp(&other.height).then(self.id.cmp(&other.id))
}
}
impl PartialEq for DirtyScope {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
+96 -116
View File
@@ -1,21 +1,21 @@
use crate::{
scope_context::{consume_context, current_scope_id, schedule_update_any},
Element, IntoDynNode, LazyNodes, Properties, Scope, ScopeId, ScopeState, Template,
TemplateAttribute, TemplateNode, VNode,
global_context::{current_scope_id, try_consume_context},
innerlude::provide_context,
use_hook, Element, IntoDynNode, Properties, ScopeId, Template, TemplateAttribute, TemplateNode,
VNode,
};
use std::{
any::{Any, TypeId},
backtrace::Backtrace,
cell::{Cell, RefCell},
cell::RefCell,
error::Error,
fmt::{Debug, Display},
rc::Rc,
sync::Arc,
};
/// Provide an error boundary to catch errors from child components
pub fn use_error_boundary(cx: &ScopeState) -> &ErrorBoundary {
cx.use_hook(|| cx.provide_context(ErrorBoundary::new()))
pub fn use_error_boundary() -> ErrorBoundary {
use_hook(|| provide_context(ErrorBoundary::new()))
}
/// A boundary that will capture any errors from child components
@@ -28,7 +28,6 @@ pub struct ErrorBoundary {
pub struct ErrorBoundaryInner {
error: RefCell<Option<CapturedError>>,
_id: ScopeId,
rerun_boundary: Arc<dyn Fn(ScopeId) + Send + Sync>,
}
impl Debug for ErrorBoundaryInner {
@@ -39,11 +38,22 @@ impl Debug for ErrorBoundaryInner {
}
}
/// A trait for any type that can be downcast to a concrete type and implements Debug. This is automatically implemented for all types that implement Any + Debug.
pub trait AnyDebug: Any + Debug {
fn as_any(&self) -> &dyn Any;
}
impl<T: Any + Debug> AnyDebug for T {
fn as_any(&self) -> &dyn Any {
self
}
}
#[derive(Debug)]
/// An instance of an error captured by a descendant component.
pub struct CapturedError {
/// The error captured by the error boundary
pub error: Box<dyn Debug + 'static>,
pub error: Box<dyn AnyDebug + 'static>,
/// The backtrace of the error
pub backtrace: Backtrace,
@@ -67,8 +77,7 @@ impl CapturedError {
/// Downcast the error type into a concrete error type
pub fn downcast<T: 'static>(&self) -> Option<&T> {
if TypeId::of::<T>() == self.error.type_id() {
let raw = self.error.as_ref() as *const _ as *const T;
Some(unsafe { &*raw })
self.error.as_any().downcast_ref::<T>()
} else {
None
}
@@ -81,7 +90,6 @@ impl Default for ErrorBoundaryInner {
error: RefCell::new(None),
_id: current_scope_id()
.expect("Cannot create an error boundary outside of a component's scope."),
rerun_boundary: schedule_update_any().unwrap(),
}
}
}
@@ -93,33 +101,25 @@ impl ErrorBoundary {
}
/// Create a new error boundary in the current scope
pub(crate) fn new_in_scope(
scope: ScopeId,
rerun_boundary: Arc<dyn Fn(ScopeId) + Send + Sync>,
) -> Self {
pub(crate) fn new_in_scope(scope: ScopeId) -> Self {
Self {
inner: Rc::new(ErrorBoundaryInner {
error: RefCell::new(None),
_id: scope,
rerun_boundary,
}),
}
}
/// Push an error into this Error Boundary
pub fn insert_error(
&self,
scope: ScopeId,
error: Box<dyn Debug + 'static>,
backtrace: Backtrace,
) {
println!("{:?} {:?}", error, self.inner._id);
pub fn insert_error(&self, scope: ScopeId, error: impl Debug + 'static, backtrace: Backtrace) {
self.inner.error.replace(Some(CapturedError {
error,
error: Box::new(error),
scope,
backtrace,
}));
(self.inner.rerun_boundary)(self.inner._id);
if self.inner._id != ScopeId::ROOT {
self.inner._id.needs_update();
}
}
/// Take any error that has been captured by this error boundary
@@ -140,10 +140,10 @@ impl ErrorBoundary {
///
/// ```rust, ignore
/// #[component]
/// fn App(cx: Scope, count: String) -> Element {
/// fn app( count: String) -> Element {
/// let id: i32 = count.parse().throw()?;
///
/// cx.render(rsx! {
/// rsx! {
/// div { "Count {}" }
/// })
/// }
@@ -165,10 +165,10 @@ pub trait Throw<S = ()>: Sized {
///
/// ```rust, ignore
/// #[component]
/// fn App(cx: Scope, count: String) -> Element {
/// fn app( count: String) -> Element {
/// let id: i32 = count.parse().throw()?;
///
/// cx.render(rsx! {
/// rsx! {
/// div { "Count {}" }
/// })
/// }
@@ -188,10 +188,10 @@ pub trait Throw<S = ()>: Sized {
///
/// ```rust, ignore
/// #[component]
/// fn App(cx: Scope, count: String) -> Element {
/// fn app( count: String) -> Element {
/// let id: i32 = count.parse().throw()?;
///
/// cx.render(rsx! {
/// rsx! {
/// div { "Count {}" }
/// })
/// }
@@ -202,7 +202,7 @@ pub trait Throw<S = ()>: Sized {
}
fn throw_error<T>(e: impl Debug + 'static) -> Option<T> {
if let Some(cx) = consume_context::<ErrorBoundary>() {
if let Some(cx) = try_consume_context::<ErrorBoundary>() {
match current_scope_id() {
Some(id) => cx.insert_error(id, Box::new(e), Backtrace::capture()),
None => {
@@ -262,85 +262,75 @@ impl<T> Throw for Option<T> {
}
}
pub struct ErrorHandler<'a>(Box<dyn Fn(CapturedError) -> LazyNodes<'a, 'a> + 'a>);
impl<'a, F: Fn(CapturedError) -> LazyNodes<'a, 'a> + 'a> From<F> for ErrorHandler<'a> {
#[derive(Clone)]
pub struct ErrorHandler(Rc<dyn Fn(CapturedError) -> Element>);
impl<F: Fn(CapturedError) -> Element + 'static> From<F> for ErrorHandler {
fn from(value: F) -> Self {
Self(Box::new(value))
Self(Rc::new(value))
}
}
fn default_handler<'a>(error: CapturedError) -> LazyNodes<'a, 'a> {
LazyNodes::new(move |__cx: &ScopeState| -> VNode {
static TEMPLATE: Template = Template {
name: "error_handle.rs:42:5:884",
roots: &[TemplateNode::Element {
tag: "pre",
namespace: None,
attrs: &[TemplateAttribute::Static {
name: "color",
namespace: Some("style"),
value: "red",
}],
children: &[TemplateNode::DynamicText { id: 0usize }],
fn default_handler(error: CapturedError) -> Element {
static TEMPLATE: Template = Template {
name: "error_handle.rs:42:5:884",
roots: &[TemplateNode::Element {
tag: "pre",
namespace: None,
attrs: &[TemplateAttribute::Static {
name: "color",
namespace: Some("style"),
value: "red",
}],
node_paths: &[&[0u8, 0u8]],
attr_paths: &[],
};
VNode {
parent: Default::default(),
stable_id: Default::default(),
key: None,
template: std::cell::Cell::new(TEMPLATE),
root_ids: bumpalo::collections::Vec::with_capacity_in(1usize, __cx.bump()).into(),
dynamic_nodes: __cx
.bump()
.alloc([__cx.text_node(format_args!("{0}", error))]),
dynamic_attrs: __cx.bump().alloc([]),
}
})
children: &[TemplateNode::DynamicText { id: 0usize }],
}],
node_paths: &[&[0u8, 0u8]],
attr_paths: &[],
};
Some(VNode::new(
None,
TEMPLATE,
Box::new([error.to_string().into_dyn_node()]),
Default::default(),
))
}
pub struct ErrorBoundaryProps<'a> {
children: Element<'a>,
handle_error: ErrorHandler<'a>,
#[derive(Clone)]
pub struct ErrorBoundaryProps {
children: Element,
handle_error: ErrorHandler,
}
impl<'a> ErrorBoundaryProps<'a> {
impl ErrorBoundaryProps {
/**
Create a builder for building `ErrorBoundaryProps`.
On the builder, call `.children(...)`(optional), `.handle_error(...)`(optional) to set the values of the fields.
Finally, call `.build()` to create the instance of `ErrorBoundaryProps`.
*/
#[allow(dead_code)]
pub fn builder() -> ErrorBoundaryPropsBuilder<'a, ((), ())> {
ErrorBoundaryPropsBuilder {
fields: ((), ()),
_phantom: ::core::default::Default::default(),
}
pub fn builder() -> ErrorBoundaryPropsBuilder<((), ())> {
ErrorBoundaryPropsBuilder { fields: ((), ()) }
}
}
#[must_use]
#[doc(hidden)]
#[allow(dead_code, non_camel_case_types, non_snake_case)]
pub struct ErrorBoundaryPropsBuilder<'a, TypedBuilderFields> {
pub struct ErrorBoundaryPropsBuilder<TypedBuilderFields> {
fields: TypedBuilderFields,
_phantom: ::core::marker::PhantomData<&'a ()>,
}
impl<'a, TypedBuilderFields> Clone for ErrorBoundaryPropsBuilder<'a, TypedBuilderFields>
impl<TypedBuilderFields> Clone for ErrorBoundaryPropsBuilder<TypedBuilderFields>
where
TypedBuilderFields: Clone,
{
fn clone(&self) -> Self {
Self {
fields: self.fields.clone(),
_phantom: ::core::default::Default::default(),
}
}
}
impl<'a> Properties<'a> for ErrorBoundaryProps<'a> {
type Builder = ErrorBoundaryPropsBuilder<'a, ((), ())>;
const IS_STATIC: bool = false;
fn builder(_: &'a ScopeState) -> Self::Builder {
impl Properties for ErrorBoundaryProps {
type Builder = ErrorBoundaryPropsBuilder<((), ())>;
fn builder() -> Self::Builder {
ErrorBoundaryProps::builder()
}
unsafe fn memoize(&self, _: &Self) -> bool {
fn memoize(&mut self, _: &Self) -> bool {
false
}
}
@@ -360,16 +350,15 @@ impl<T> ErrorBoundaryPropsBuilder_Optional<T> for (T,) {
}
}
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl<'a, __handle_error> ErrorBoundaryPropsBuilder<'a, ((), __handle_error)> {
impl<__handle_error> ErrorBoundaryPropsBuilder<((), __handle_error)> {
pub fn children(
self,
children: Element<'a>,
) -> ErrorBoundaryPropsBuilder<'a, ((Element<'a>,), __handle_error)> {
children: Element,
) -> ErrorBoundaryPropsBuilder<((Element,), __handle_error)> {
let children = (children,);
let (_, handle_error) = self.fields;
ErrorBoundaryPropsBuilder {
fields: (children, handle_error),
_phantom: self._phantom,
}
}
}
@@ -378,26 +367,25 @@ impl<'a, __handle_error> ErrorBoundaryPropsBuilder<'a, ((), __handle_error)> {
pub enum ErrorBoundaryPropsBuilder_Error_Repeated_field_children {}
#[doc(hidden)]
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl<'a, __handle_error> ErrorBoundaryPropsBuilder<'a, ((Element<'a>,), __handle_error)> {
impl<__handle_error> ErrorBoundaryPropsBuilder<((Element,), __handle_error)> {
#[deprecated(note = "Repeated field children")]
pub fn children(
self,
_: ErrorBoundaryPropsBuilder_Error_Repeated_field_children,
) -> ErrorBoundaryPropsBuilder<'a, ((Element<'a>,), __handle_error)> {
) -> ErrorBoundaryPropsBuilder<((Element,), __handle_error)> {
self
}
}
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl<'a, __children> ErrorBoundaryPropsBuilder<'a, (__children, ())> {
impl<__children> ErrorBoundaryPropsBuilder<(__children, ())> {
pub fn handle_error(
self,
handle_error: impl ::core::convert::Into<ErrorHandler<'a>>,
) -> ErrorBoundaryPropsBuilder<'a, (__children, (ErrorHandler<'a>,))> {
handle_error: impl ::core::convert::Into<ErrorHandler>,
) -> ErrorBoundaryPropsBuilder<(__children, (ErrorHandler,))> {
let handle_error = (handle_error.into(),);
let (children, _) = self.fields;
ErrorBoundaryPropsBuilder {
fields: (children, handle_error),
_phantom: self._phantom,
}
}
}
@@ -406,29 +394,28 @@ impl<'a, __children> ErrorBoundaryPropsBuilder<'a, (__children, ())> {
pub enum ErrorBoundaryPropsBuilder_Error_Repeated_field_handle_error {}
#[doc(hidden)]
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl<'a, __children> ErrorBoundaryPropsBuilder<'a, (__children, (ErrorHandler<'a>,))> {
impl<__children> ErrorBoundaryPropsBuilder<(__children, (ErrorHandler,))> {
#[deprecated(note = "Repeated field handle_error")]
pub fn handle_error(
self,
_: ErrorBoundaryPropsBuilder_Error_Repeated_field_handle_error,
) -> ErrorBoundaryPropsBuilder<'a, (__children, (ErrorHandler<'a>,))> {
) -> ErrorBoundaryPropsBuilder<(__children, (ErrorHandler,))> {
self
}
}
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl<
'a,
__handle_error: ErrorBoundaryPropsBuilder_Optional<ErrorHandler<'a>>,
__children: ErrorBoundaryPropsBuilder_Optional<Element<'a>>,
> ErrorBoundaryPropsBuilder<'a, (__children, __handle_error)>
__handle_error: ErrorBoundaryPropsBuilder_Optional<ErrorHandler>,
__children: ErrorBoundaryPropsBuilder_Optional<Element>,
> ErrorBoundaryPropsBuilder<(__children, __handle_error)>
{
pub fn build(self) -> ErrorBoundaryProps<'a> {
pub fn build(self) -> ErrorBoundaryProps {
let (children, handle_error) = self.fields;
let children = ErrorBoundaryPropsBuilder_Optional::into_value(children, || {
::core::default::Default::default()
});
let handle_error = ErrorBoundaryPropsBuilder_Optional::into_value(handle_error, || {
ErrorHandler(Box::new(default_handler))
ErrorHandler(Rc::new(default_handler))
});
ErrorBoundaryProps {
children,
@@ -459,30 +446,23 @@ impl<
/// They are similar to `try/catch` in JavaScript, but they only catch errors in the tree below them.
/// Error boundaries are quick to implement, but it can be useful to individually handle errors in your components to provide a better user experience when you know that an error is likely to occur.
#[allow(non_upper_case_globals, non_snake_case)]
pub fn ErrorBoundary<'a>(cx: Scope<'a, ErrorBoundaryProps<'a>>) -> Element {
let error_boundary = use_error_boundary(cx);
pub fn ErrorBoundary(props: ErrorBoundaryProps) -> Element {
let error_boundary = use_error_boundary();
match error_boundary.take_error() {
Some(error) => cx.render((cx.props.handle_error.0)(error)),
Some(error) => (props.handle_error.0)(error),
None => Some({
let __cx = cx;
static TEMPLATE: Template = Template {
name: "examples/error_handle.rs:81:17:2342",
roots: &[TemplateNode::Dynamic { id: 0usize }],
node_paths: &[&[0u8]],
attr_paths: &[],
};
VNode {
parent: Cell::new(None),
stable_id: Cell::new(None),
key: None,
template: std::cell::Cell::new(TEMPLATE),
root_ids: bumpalo::collections::Vec::with_capacity_in(1usize, __cx.bump()).into(),
dynamic_nodes: __cx.bump().alloc([{
let ___nodes = (&cx.props.children).into_dyn_node(__cx);
___nodes
}]),
dynamic_attrs: __cx.bump().alloc([]),
}
VNode::new(
None,
TEMPLATE,
Box::new([(props.children).into_dyn_node()]),
Default::default(),
)
}),
}
}
+50 -19
View File
@@ -1,4 +1,4 @@
use crate::{runtime::with_runtime, ScopeId};
use crate::{global_context::current_scope_id, Runtime, ScopeId};
use std::{
cell::{Cell, RefCell},
rc::Rc,
@@ -27,6 +27,15 @@ pub struct Event<T: 'static + ?Sized> {
pub(crate) propagates: Rc<Cell<bool>>,
}
impl<T: ?Sized + 'static> Event<T> {
pub(crate) fn new(data: Rc<T>, bubbles: bool) -> Self {
Self {
data,
propagates: Rc::new(Cell::new(bubbles)),
}
}
}
impl<T> Event<T> {
/// Map the event data to a new type
///
@@ -98,8 +107,8 @@ impl<T> Event<T> {
/// }
/// }
/// ```
pub fn inner(&self) -> &Rc<T> {
&self.data
pub fn data(&self) -> Rc<T> {
self.data.clone()
}
}
@@ -141,25 +150,40 @@ impl<T: std::fmt::Debug> std::fmt::Debug for Event<T> {
/// }
///
/// #[derive(Props)]
/// struct MyProps<'a> {
/// onclick: EventHandler<'a, MouseEvent>,
/// struct MyProps {
/// onclick: EventHandler<MouseEvent>,
/// }
///
/// fn MyComponent(cx: Scope<'a, MyProps<'a>>) -> Element {
/// cx.render(rsx!{
/// fn MyComponent(cx: MyProps) -> Element {
/// rsx!{
/// button {
/// onclick: move |evt| cx.props.onclick.call(evt),
/// onclick: move |evt| cx.onclick.call(evt),
/// }
/// })
/// }
///
/// ```
pub struct EventHandler<'bump, T = ()> {
pub struct EventHandler<T = ()> {
pub(crate) origin: ScopeId,
pub(super) callback: RefCell<Option<ExternalListenerCallback<'bump, T>>>,
pub(super) callback: Rc<RefCell<Option<ExternalListenerCallback<T>>>>,
}
impl<T> Default for EventHandler<'_, T> {
impl<T> Clone for EventHandler<T> {
fn clone(&self) -> Self {
Self {
origin: self.origin,
callback: self.callback.clone(),
}
}
}
impl<T> PartialEq for EventHandler<T> {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.callback, &other.callback)
}
}
impl<T> Default for EventHandler<T> {
fn default() -> Self {
Self {
origin: ScopeId::ROOT,
@@ -168,21 +192,28 @@ impl<T> Default for EventHandler<'_, T> {
}
}
type ExternalListenerCallback<'bump, T> = bumpalo::boxed::Box<'bump, dyn FnMut(T) + 'bump>;
type ExternalListenerCallback<T> = Box<dyn FnMut(T)>;
impl<T> EventHandler<T> {
/// Create a new [`EventHandler`] from an [`FnMut`]
pub fn new(mut f: impl FnMut(T) + 'static) -> EventHandler<T> {
let callback = Rc::new(RefCell::new(Some(Box::new(move |event: T| {
f(event);
}) as Box<dyn FnMut(T)>)));
EventHandler {
callback,
origin: current_scope_id().expect("to be in a dioxus runtime"),
}
}
impl<T> EventHandler<'_, T> {
/// Call this event handler with the appropriate event type
///
/// This borrows the event using a RefCell. Recursively calling a listener will cause a panic.
pub fn call(&self, event: T) {
if let Some(callback) = self.callback.borrow_mut().as_mut() {
with_runtime(|rt| {
rt.scope_stack.borrow_mut().push(self.origin);
});
Runtime::with(|rt| rt.scope_stack.borrow_mut().push(self.origin));
callback(event);
with_runtime(|rt| {
rt.scope_stack.borrow_mut().pop();
});
Runtime::with(|rt| rt.scope_stack.borrow_mut().pop());
}
}
+19 -26
View File
@@ -26,27 +26,21 @@ use crate::innerlude::*;
///
/// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it.
#[allow(non_upper_case_globals, non_snake_case)]
pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element {
let children = cx.props.0.as_ref()?;
Some(cx.vnode(
children.parent.clone(),
children.key,
children.template.clone(),
children.root_ids.clone(),
children.dynamic_nodes,
children.dynamic_attrs,
))
pub fn Fragment(cx: FragmentProps) -> Element {
cx.0.clone()
}
pub struct FragmentProps<'a>(Element<'a>);
pub struct FragmentBuilder<'a, const BUILT: bool>(Element<'a>);
impl<'a> FragmentBuilder<'a, false> {
pub fn children(self, children: Element<'a>) -> FragmentBuilder<'a, true> {
#[derive(Clone, PartialEq)]
pub struct FragmentProps(Element);
pub struct FragmentBuilder<const BUILT: bool>(Element);
impl FragmentBuilder<false> {
pub fn children(self, children: Element) -> FragmentBuilder<true> {
FragmentBuilder(children)
}
}
impl<'a, const A: bool> FragmentBuilder<'a, A> {
pub fn build(self) -> FragmentProps<'a> {
impl<const A: bool> FragmentBuilder<A> {
pub fn build(self) -> FragmentProps {
FragmentProps(self.0)
}
}
@@ -68,8 +62,8 @@ impl<'a, const A: bool> FragmentBuilder<'a, A> {
/// ## Example
///
/// ```rust, ignore
/// fn App(cx: Scope) -> Element {
/// cx.render(rsx!{
/// fn app() -> Element {
/// rsx!{
/// CustomCard {
/// h1 {}
/// p {}
@@ -82,22 +76,21 @@ impl<'a, const A: bool> FragmentBuilder<'a, A> {
/// children: Element
/// }
///
/// fn CustomCard(cx: Scope<CardProps>) -> Element {
/// cx.render(rsx!{
/// fn CustomCard(cx: CardProps) -> Element {
/// rsx!{
/// div {
/// h1 {"Title card"}
/// {cx.props.children}
/// {cx.children}
/// }
/// })
/// }
/// ```
impl<'a> Properties<'_> for FragmentProps<'a> {
type Builder = FragmentBuilder<'a, false>;
const IS_STATIC: bool = false;
fn builder(_cx: &ScopeState) -> Self::Builder {
impl Properties for FragmentProps {
type Builder = FragmentBuilder<false>;
fn builder() -> Self::Builder {
FragmentBuilder(None)
}
unsafe fn memoize(&self, _other: &Self) -> bool {
fn memoize(&mut self, _other: &Self) -> bool {
false
}
}
+278
View File
@@ -0,0 +1,278 @@
use crate::{runtime::Runtime, Element, ScopeId, Task};
use futures_util::Future;
use std::sync::Arc;
/// Get the current scope id
pub fn current_scope_id() -> Option<ScopeId> {
Runtime::with(|rt| rt.current_scope_id()).flatten()
}
#[doc(hidden)]
/// Check if the virtual dom is currently inside of the body of a component
pub fn vdom_is_rendering() -> bool {
Runtime::with(|rt| rt.rendering.get()).unwrap_or_default()
}
/// Consume context from the current scope
pub fn try_consume_context<T: 'static + Clone>() -> Option<T> {
Runtime::with_current_scope(|cx| cx.consume_context::<T>()).flatten()
}
/// Consume context from the current scope
pub fn consume_context<T: 'static + Clone>() -> T {
Runtime::with_current_scope(|cx| cx.consume_context::<T>())
.flatten()
.unwrap_or_else(|| panic!("Could not find context {}", std::any::type_name::<T>()))
}
/// Consume context from the current scope
pub fn consume_context_from_scope<T: 'static + Clone>(scope_id: ScopeId) -> Option<T> {
Runtime::with(|rt| {
rt.get_state(scope_id)
.and_then(|cx| cx.consume_context::<T>())
})
.flatten()
}
/// Check if the current scope has a context
pub fn has_context<T: 'static + Clone>() -> Option<T> {
Runtime::with_current_scope(|cx| cx.has_context::<T>()).flatten()
}
/// Provide context to the current scope
pub fn provide_context<T: 'static + Clone>(value: T) -> T {
Runtime::with_current_scope(|cx| cx.provide_context(value)).expect("to be in a dioxus runtime")
}
/// Provide a context to the root scope
pub fn provide_root_context<T: 'static + Clone>(value: T) -> T {
Runtime::with_current_scope(|cx| cx.provide_root_context(value))
.expect("to be in a dioxus runtime")
}
/// Suspends the current component
pub fn suspend() -> Option<Element> {
Runtime::with_current_scope(|cx| cx.suspend());
None
}
/// Spawns the future but does not return the [`TaskId`]
pub fn spawn(fut: impl Future<Output = ()> + 'static) -> Task {
Runtime::with_current_scope(|cx| cx.spawn(fut)).expect("to be in a dioxus runtime")
}
/// Spawn a future that Dioxus won't clean up when this component is unmounted
///
/// This is good for tasks that need to be run after the component has been dropped.
pub fn spawn_forever(fut: impl Future<Output = ()> + 'static) -> Option<Task> {
Runtime::with_current_scope(|cx| cx.spawn_forever(fut))
}
/// Informs the scheduler that this task is no longer needed and should be removed.
///
/// This drops the task immediately.
pub fn remove_future(id: Task) {
Runtime::with(|rt| rt.remove_task(id)).expect("Runtime to exist");
}
/// Store a value between renders. The foundational hook for all other hooks.
///
/// Accepts an `initializer` closure, which is run on the first use of the hook (typically the initial render). The return value of this closure is stored for the lifetime of the component, and a mutable reference to it is provided on every render as the return value of `use_hook`.
///
/// When the component is unmounted (removed from the UI), the value is dropped. This means you can return a custom type and provide cleanup code by implementing the [`Drop`] trait
///
/// # Example
///
/// ```
/// use dioxus_core::use_hook;
///
/// // prints a greeting on the initial render
/// pub fn use_hello_world() {
/// use_hook(|| println!("Hello, world!"));
/// }
/// ```
pub fn use_hook<State: Clone + 'static>(initializer: impl FnOnce() -> State) -> State {
Runtime::with_current_scope(|cx| cx.use_hook(initializer)).expect("to be in a dioxus runtime")
}
/// Get the current render since the inception of this component
///
/// This can be used as a helpful diagnostic when debugging hooks/renders, etc
pub fn generation() -> usize {
Runtime::with_current_scope(|cx| cx.generation()).expect("to be in a dioxus runtime")
}
/// Get the parent of the current scope if it exists
pub fn parent_scope() -> Option<ScopeId> {
Runtime::with_current_scope(|cx| cx.parent_id()).flatten()
}
/// Mark the current scope as dirty, causing it to re-render
pub fn needs_update() {
Runtime::with_current_scope(|cx| cx.needs_update());
}
/// Mark the current scope as dirty, causing it to re-render
pub fn needs_update_any(id: ScopeId) {
Runtime::with_current_scope(|cx| cx.needs_update_any(id));
}
/// Schedule an update for the current component
///
/// Note: Unlike [`needs_update`], the function returned by this method will work outside of the dioxus runtime.
///
/// You should prefer [`schedule_update_any`] if you need to update multiple components.
pub fn schedule_update() -> Arc<dyn Fn() + Send + Sync> {
Runtime::with_current_scope(|cx| cx.schedule_update()).expect("to be in a dioxus runtime")
}
/// Schedule an update for any component given its [`ScopeId`].
///
/// A component's [`ScopeId`] can be obtained from the [`current_scope_id`] method.
///
/// Note: Unlike [`needs_update`], the function returned by this method will work outside of the dioxus runtime.
pub fn schedule_update_any() -> Arc<dyn Fn(ScopeId) + Send + Sync> {
Runtime::with_current_scope(|cx| cx.schedule_update_any()).expect("to be in a dioxus runtime")
}
/// Creates a callback that will be run before the component is removed.
/// This can be used to clean up side effects from the component
/// (created with [`use_effect`](crate::use_effect)).
///
/// Example:
/// ```rust, ignore
/// use dioxus::prelude::*;
///
/// fn app() -> Element {
/// let state = use_signal(|| true);
/// rsx! {
/// for _ in 0..100 {
/// h1 {
/// "spacer"
/// }
/// }
/// if **state {
/// rsx! {
/// child_component {}
/// }
/// }
/// button {
/// onclick: move |_| {
/// state.set(!*state.get());
/// },
/// "Unmount element"
/// }
/// }
/// }
///
/// fn child_component() -> Element {
/// let original_scroll_position = use_signal(|| 0.0);
/// use_effect((), move |_| {
/// to_owned![original_scroll_position];
/// async move {
/// let window = web_sys::window().unwrap();
/// let document = window.document().unwrap();
/// let element = document.get_element_by_id("my_element").unwrap();
/// element.scroll_into_view();
/// original_scroll_position.set(window.scroll_y().unwrap());
/// }
/// });
///
/// use_drop({
/// to_owned![original_scroll_position];
/// /// restore scroll to the top of the page
/// move || {
/// let window = web_sys::window().unwrap();
/// window.scroll_with_x_and_y(*original_scroll_position.current(), 0.0);
/// }
/// });
///
/// rsx!{
/// div {
/// id: "my_element",
/// "hello"
/// }
/// }
/// }
/// ```
pub fn use_drop<D: FnOnce() + 'static>(destroy: D) {
struct LifeCycle<D: FnOnce()> {
/// Wrap the closure in an option so that we can take it out on drop.
ondestroy: Option<D>,
}
/// On drop, we want to run the closure.
impl<D: FnOnce()> Drop for LifeCycle<D> {
fn drop(&mut self) {
if let Some(f) = self.ondestroy.take() {
f();
}
}
}
// We need to impl clone for the lifecycle, but we don't want the drop handler for the closure to be called twice.
impl<D: FnOnce()> Clone for LifeCycle<D> {
fn clone(&self) -> Self {
Self { ondestroy: None }
}
}
use_hook(|| LifeCycle {
ondestroy: Some(destroy),
});
}
/// A hook that allows you to insert a "before render" function.
///
/// This function will always be called before dioxus tries to render your component. This should be used for safely handling
/// early returns
pub fn use_before_render(f: impl FnMut() + 'static) {
use_hook(|| before_render(f));
}
/// Push this function to be run after the next render
///
/// This function will always be called before dioxus tries to render your component. This should be used for safely handling
/// early returns
pub fn use_after_render(f: impl FnMut() + 'static) {
use_hook(|| after_render(f));
}
/// Push a function to be run before the next render
/// This is a hook and will always run, so you can't unschedule it
/// Will run for every progression of suspense, though this might change in the future
pub fn before_render(f: impl FnMut() + 'static) {
Runtime::with_current_scope(|cx| cx.push_before_render(f));
}
/// Push a function to be run after the render is complete, even if it didn't complete successfully
pub fn after_render(f: impl FnMut() + 'static) {
Runtime::with_current_scope(|cx| cx.push_after_render(f));
}
/// Wait for the virtualdom to finish its sync work before proceeding
///
/// This is useful if you've just triggered an update and want to wait for it to finish before proceeding with valid
/// DOM nodes.
///
/// Effects rely on this to ensure that they only run effects after the DOM has been updated. Without flush_sync effects
/// are run immediately before diffing the DOM, which causes all sorts of out-of-sync weirdness.
pub async fn flush_sync() {
// Wait for the flush lock to be available
// We release it immediately, so it's impossible for the lock to be held longer than this function
Runtime::with(|rt| rt.flush_mutex.clone())
.unwrap()
.lock()
.await;
}
/// Use a hook with a cleanup function
pub fn use_hook_with_cleanup<T: Clone + 'static>(
hook: impl FnOnce() -> T,
cleanup: impl FnOnce(T) + 'static,
) -> T {
let value = use_hook(hook);
let _value = value.clone();
use_drop(move || cleanup(_value));
value
}
-101
View File
@@ -1,101 +0,0 @@
//! Support for storing lazy-nodes on the stack
//!
//! This module provides support for a type called `LazyNodes` which is a micro-heap located on the stack to make calls
//! to `rsx!` more efficient.
//!
//! To support returning rsx! from branches in match statements, we need to use dynamic dispatch on [`ScopeState`] closures.
//!
//! This can be done either through boxing directly, or by using dynamic-sized-types and a custom allocator. In our case,
//! we build a tiny alloactor in the stack and allocate the closure into that.
//!
//! The logic for this was borrowed from <https://docs.rs/stack_dst/0.6.1/stack_dst/>. Unfortunately, this crate does not
//! support non-static closures, so we've implemented the core logic of `ValueA` in this module.
#[allow(unused_imports)]
use smallbox::{smallbox, space::S16, SmallBox};
use crate::{innerlude::VNode, ScopeState};
/// A concrete type provider for closures that build [`VNode`] structures.
///
/// This struct wraps lazy structs that build [`VNode`] trees. Normally, we cannot perform a blanket implementation over
/// closures, but if we wrap the closure in a concrete type, we can use it for different branches in matching.
///
///
/// ```rust, ignore
/// LazyNodes::new(|f| {
/// static TEMPLATE: dioxus::core::Template = dioxus::core::Template {
/// name: "main.rs:5:5:20", // Source location of the template for hot reloading
/// roots: &[
/// dioxus::core::TemplateNode::Element {
/// tag: dioxus_elements::div::TAG_NAME,
/// namespace: dioxus_elements::div::NAME_SPACE,
/// attrs: &[],
/// children: &[],
/// },
/// ],
/// node_paths: &[],
/// attr_paths: &[],
/// };
/// f.vnode(
/// None,
/// None,
/// std::cell::Cell::new(TEMPLATE),
/// dioxus::core::exports::bumpalo::collections::Vec::with_capacity_in(
/// 1usize,
/// f.bump(),
/// )
/// .into(),
/// f.bump().alloc([]),
/// f.bump().alloc([]),
/// )
/// })
/// ```
///
/// Find more information about how to construct [`VNode`] at <https://dioxuslabs.com/learn/0.4/contributing/walkthrough_readme#the-rsx-macro>
pub struct LazyNodes<'a, 'b> {
#[cfg(not(miri))]
inner: SmallBox<dyn FnMut(&'a ScopeState) -> VNode<'a> + 'b, S16>,
#[cfg(miri)]
inner: Box<dyn FnMut(&'a ScopeState) -> VNode<'a> + 'b>,
}
impl<'a, 'b> LazyNodes<'a, 'b> {
/// Create a new [`LazyNodes`] closure, optimistically placing it onto the stack.
///
/// If the closure cannot fit into the stack allocation (16 bytes), then it
/// is placed on the heap. Most closures will fit into the stack, and is
/// the most optimal way to use the creation function.
pub fn new(val: impl FnOnce(&'a ScopeState) -> VNode<'a> + 'b) -> Self {
// there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch
let mut slot = Some(val);
Self {
#[cfg(not(miri))]
inner: smallbox!(move |f| {
let val = slot.take().expect("cannot call LazyNodes twice");
val(f)
}),
#[cfg(miri)]
inner: Box::new(move |f| {
let val = slot.take().expect("cannot call LazyNodes twice");
val(f)
}),
}
}
/// Call the closure with the given factory to produce real [`VNode`].
///
/// ```rust, ignore
/// let f = LazyNodes::new(/* Closure for creating VNodes */);
///
/// let node = f.call(cac);
/// ```
#[must_use]
pub fn call(mut self, f: &'a ScopeState) -> VNode<'a> {
(self.inner)(f)
}
}
+25 -30
View File
@@ -5,45 +5,42 @@
mod any_props;
mod arena;
mod bump_frame;
mod create;
mod diff;
mod dirty_scope;
mod error_boundary;
mod events;
mod fragment;
mod lazynodes;
mod global_context;
mod mutations;
mod nodes;
mod properties;
mod runtime;
mod scheduler;
mod scope_arena;
mod scope_context;
mod scopes;
mod tasks;
mod virtual_dom;
pub(crate) mod innerlude {
pub(crate) use crate::any_props::*;
pub use crate::arena::*;
pub use crate::dirty_scope::*;
pub use crate::error_boundary::*;
pub use crate::events::*;
pub use crate::fragment::*;
pub use crate::lazynodes::*;
pub use crate::global_context::*;
pub use crate::mutations::*;
pub use crate::nodes::RenderReturn;
pub use crate::nodes::*;
pub use crate::properties::*;
pub use crate::runtime::{Runtime, RuntimeGuard};
pub use crate::scheduler::*;
pub use crate::scope_context::*;
pub use crate::scopes::*;
pub use crate::tasks::*;
pub use crate::virtual_dom::*;
/// An [`Element`] is a possibly-none [`VNode`] created by calling `render` on [`Scope`] or [`ScopeState`].
///
/// An Errored [`Element`] will propagate the error to the nearest error boundary.
pub type Element<'a> = Option<VNode<'a>>;
pub type Element = Option<VNode>;
/// A [`Component`] is a function that takes a [`Scope`] and returns an [`Element`].
///
@@ -54,7 +51,7 @@ pub(crate) mod innerlude {
/// ## Rust-Style
///
/// ```rust, ignore
/// fn example(cx: Scope<Props>) -> Element {
/// fn example(cx: Props) -> Element {
/// // ...
/// }
///
@@ -62,9 +59,10 @@ pub(crate) mod innerlude {
/// example()
/// )
/// ```
///
/// ## React-Style
/// ```rust, ignore
/// fn Example(cx: Scope<Props>) -> Element {
/// fn Example(cx: Props) -> Element {
/// // ...
/// }
///
@@ -72,15 +70,16 @@ pub(crate) mod innerlude {
/// Example {}
/// )
/// ```
pub type Component<P = ()> = fn(Scope<P>) -> Element;
pub type Component<P = ()> = fn(P) -> Element;
}
pub use crate::innerlude::{
fc_to_builder, vdom_is_rendering, AnyValue, Attribute, AttributeType, AttributeValue,
BorrowedAttributeValue, CapturedError, Component, DynamicNode, Element, ElementId, Event,
Fragment, HasAttributes, IntoDynNode, LazyNodes, MountedAttribute, Mutation, Mutations,
Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, TaskId, Template,
TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText, VirtualDom,
fc_to_builder, generation, schedule_update, schedule_update_any, use_hook, vdom_is_rendering,
AnyValue, Attribute, AttributeValue, CapturedError, Component, ComponentFunction, DynamicNode,
Element, ElementId, Event, Fragment, HasAttributes, IntoDynNode, Mutation, Mutations,
NoOpMutations, Properties, RenderReturn, Runtime, ScopeId, ScopeState, Task, Template,
TemplateAttribute, TemplateNode, VComponent, VNode, VNodeInner, VPlaceholder, VText,
VirtualDom, WriteMutations,
};
/// The purpose of this module is to alleviate imports of many common types
@@ -88,18 +87,14 @@ pub use crate::innerlude::{
/// This includes types like [`Scope`], [`Element`], and [`Component`].
pub mod prelude {
pub use crate::innerlude::{
consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, has_context,
provide_context, provide_context_to_scope, provide_root_context, push_future,
remove_future, schedule_update_any, spawn, spawn_forever, suspend, use_error_boundary,
AnyValue, Attribute, AttributeType, Component, Element, ErrorBoundary, Event, EventHandler,
Fragment, HasAttributes, IntoAttributeValue, IntoDynNode, LazyNodes, MountedAttribute,
Properties, Runtime, RuntimeGuard, Scope, ScopeId, ScopeState, Scoped, TaskId, Template,
TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom,
consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, flush_sync,
generation, has_context, needs_update, needs_update_any, parent_scope, provide_context,
provide_root_context, remove_future, schedule_update, schedule_update_any, spawn,
spawn_forever, suspend, try_consume_context, use_after_render, use_before_render, use_drop,
use_error_boundary, use_hook, use_hook_with_cleanup, AnyValue, Attribute, Component,
ComponentFunction, Element, ErrorBoundary, Event, EventHandler, Fragment, HasAttributes,
IntoAttributeValue, IntoDynNode, OptionStringFromMarker, Properties, Runtime, RuntimeGuard,
ScopeId, ScopeState, SuperFrom, SuperInto, Task, Template, TemplateAttribute, TemplateNode,
Throw, VNode, VNodeInner, VirtualDom,
};
}
pub mod exports {
//! Important dependencies that are used by the rest of the library
//! Feel free to just add the dependencies in your own Crates.toml
pub use bumpalo;
}
+318 -52
View File
@@ -1,8 +1,8 @@
use rustc_hash::FxHashSet;
use crate::{arena::ElementId, innerlude::BorrowedAttributeValue, ScopeId, Template};
use crate::{arena::ElementId, AttributeValue, ScopeId, Template};
/// A container for all the relevant steps to modify the Real DOM
/// Something that can handle the mutations that are generated by the diffing process and apply them to the Real DOM
///
/// This object provides a bunch of important information for a renderer to use patch the Real Dom with the state of the
/// VirtualDom. This includes the scopes that were modified, the templates that were discovered, and a list of changes
@@ -13,56 +13,136 @@ use crate::{arena::ElementId, innerlude::BorrowedAttributeValue, ScopeId, Templa
/// Templates, however, apply to all subtrees, not just target subtree.
///
/// Mutations are the only link between the RealDOM and the VirtualDOM.
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
#[derive(Debug, Default)]
#[must_use = "not handling edits can lead to visual inconsistencies in UI"]
pub struct Mutations<'a> {
/// The ID of the subtree that these edits are targetting
pub subtree: usize,
pub trait WriteMutations {
/// Register a template with the renderer
fn register_template(&mut self, template: Template);
/// The list of Scopes that were diffed, created, and removed during the Diff process.
pub dirty_scopes: FxHashSet<ScopeId>,
/// Any templates encountered while diffing the DOM.
/// Add these m children to the target element
///
/// These must be loaded into a cache before applying the edits
pub templates: Vec<Template<'static>>,
/// Id: The ID of the element being mounted to
/// M: The number of nodes on the stack to append to the target element
fn append_children(&mut self, id: ElementId, m: usize);
/// Any mutations required to patch the renderer to match the layout of the VirtualDom
pub edits: Vec<Mutation<'a>>,
}
impl<'a> Mutations<'a> {
/// Rewrites IDs to just be "template", so you can compare the mutations
/// Assign the element at the given path the target ElementId.
///
/// Used really only for testing
pub fn santize(mut self) -> Self {
for edit in self.edits.iter_mut() {
if let Mutation::LoadTemplate { name, .. } = edit {
*name = "template"
}
}
/// The path is in the form of a list of indices based on children. Templates cannot have more than 255 children per
/// element, hence the use of a single byte.
///
/// Path: The path of the child of the topmost node on the stack. A path of `[]` represents the topmost node. A path of `[0]` represents the first child. `[0,1,2]` represents 1st child's 2nd child's 3rd child.
/// Id: The ID we're assigning to this element/placeholder. This will be used later to modify the element or replace it with another element.
fn assign_node_id(&mut self, path: &'static [u8], id: ElementId);
self
}
/// Create a placeholder in the DOM that we will use later.
///
/// Dioxus currently requires the use of placeholders to maintain a re-entrance point for things like list diffing
///
/// Id: The ID we're assigning to this element/placeholder. This will be used later to modify the element or replace it with another element.
fn create_placeholder(&mut self, id: ElementId);
/// Push a new mutation into the dom_edits list
pub(crate) fn push(&mut self, mutation: Mutation<'static>) {
self.edits.push(mutation)
}
/// Create a node specifically for text with the given value
///
/// Value: The text content of this text node
/// Id: The ID we're assigning to this specific text nodes. This will be used later to modify the element or replace it with another element.
fn create_text_node(&mut self, value: &str, id: ElementId);
/// Hydrate an existing text node at the given path with the given text.
///
/// Assign this text node the given ID since we will likely need to modify this text at a later point
///
/// Path: The path of the child of the topmost node on the stack. A path of `[]` represents the topmost node. A path of `[0]` represents the first child. `[0,1,2]` represents 1st child's 2nd child's 3rd child.
/// Value: The value of the textnode that we want to set the placeholder with
/// Id: The ID we're assigning to this specific text nodes. This will be used later to modify the element or replace it with another element.
fn hydrate_text_node(&mut self, path: &'static [u8], value: &str, id: ElementId);
/// Load and clone an existing node from a template saved under that specific name
///
/// Dioxus guarantees that the renderer will have already been provided the template.
/// When the template is picked up in the template list, it should be saved under its "name" - here, the name
///
/// Name: The unique "name" of the template based on the template location. When paired with `rsx!`, this is autogenerated
/// Index: The index root we loading from the template. The template is stored as a list of nodes. This index represents the position of that root
/// Id: The ID we're assigning to this element being loaded from the template (This will be used later to move the element around in lists)
fn load_template(&mut self, name: &'static str, index: usize, id: ElementId);
/// Replace the target element (given by its ID) with the topmost m nodes on the stack
///
/// id: The ID of the node we're going to replace with new nodes
/// m: The number of nodes on the stack to replace the target element with
fn replace_node_with(&mut self, id: ElementId, m: usize);
/// Replace an existing element in the template at the given path with the m nodes on the stack
///
/// Path: The path of the child of the topmost node on the stack. A path of `[]` represents the topmost node. A path of `[0]` represents the first child. `[0,1,2]` represents 1st child's 2nd child's 3rd child.
/// M: The number of nodes on the stack to replace the target element with
fn replace_placeholder_with_nodes(&mut self, path: &'static [u8], m: usize);
/// Insert a number of nodes after a given node.
///
/// Id: The ID of the node to insert after.
/// M: The number of nodes on the stack to insert after the target node.
fn insert_nodes_after(&mut self, id: ElementId, m: usize);
/// Insert a number of nodes before a given node.
///
/// Id: The ID of the node to insert before.
/// M: The number of nodes on the stack to insert before the target node.
fn insert_nodes_before(&mut self, id: ElementId, m: usize);
/// Set the value of a node's attribute.
///
/// Name: The name of the attribute to set.
/// NS: The (optional) namespace of the attribute. For instance, "style" is in the "style" namespace.
/// Value: The value of the attribute.
/// Id: The ID of the node to set the attribute of.
fn set_attribute(
&mut self,
name: &'static str,
ns: Option<&'static str>,
value: &AttributeValue,
id: ElementId,
);
/// Set the text content of a node.
///
/// Value: The textcontent of the node
/// Id: The ID of the node to set the textcontent of.
fn set_node_text(&mut self, value: &str, id: ElementId);
/// Create a new Event Listener.
///
/// Name: The name of the event to listen for.
/// Id: The ID of the node to attach the listener to.
fn create_event_listener(&mut self, name: &'static str, id: ElementId);
/// Remove an existing Event Listener.
///
/// Name: The name of the event to remove.
/// Id: The ID of the node to remove.
fn remove_event_listener(&mut self, name: &'static str, id: ElementId);
/// Remove a particular node from the DOM
///
/// Id: The ID of the node to remove.
fn remove_node(&mut self, id: ElementId);
/// Push the given root node onto our stack.
///
/// Id: The ID of the root node to push.
fn push_root(&mut self, id: ElementId);
/// Swap to a new subtree
fn swap_subtree(&mut self, _subtree_index: usize) {}
/// Mark a scope as dirty
fn mark_scope_dirty(&mut self, _scope_id: ScopeId) {}
}
/// A `Mutation` represents a single instruction for the renderer to use to modify the UI tree to match the state
/// of the Dioxus VirtualDom.
///
/// These edits can be serialized and sent over the network or through any interface
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type")
)]
#[derive(Debug, PartialEq)]
pub enum Mutation<'a> {
pub enum Mutation {
/// Add these m children to the target element
AppendChildren {
/// The ID of the element being mounted to
@@ -104,7 +184,7 @@ pub enum Mutation<'a> {
/// Create a node specifically for text with the given value
CreateTextNode {
/// The text content of this text node
value: &'a str,
value: String,
/// The ID we're assigning to this specific text nodes
///
@@ -123,7 +203,7 @@ pub enum Mutation<'a> {
path: &'static [u8],
/// The value of the textnode that we want to set the placeholder with
value: &'a str,
value: String,
/// The ID we're assigning to this specific text nodes
///
@@ -192,23 +272,23 @@ pub enum Mutation<'a> {
/// Set the value of a node's attribute.
SetAttribute {
/// The name of the attribute to set.
name: &'a str,
/// The value of the attribute.
value: BorrowedAttributeValue<'a>,
/// The ID of the node to set the attribute of.
id: ElementId,
name: &'static str,
/// The (optional) namespace of the attribute.
/// For instance, "style" is in the "style" namespace.
ns: Option<&'a str>,
ns: Option<&'static str>,
/// The value of the attribute.
value: AttributeValue,
/// The ID of the node to set the attribute of.
id: ElementId,
},
/// Set the textcontent of a node.
SetText {
/// The textcontent of the node
value: &'a str,
value: String,
/// The ID of the node to set the textcontent of.
id: ElementId,
@@ -217,7 +297,7 @@ pub enum Mutation<'a> {
/// Create a new Event Listener.
NewEventListener {
/// The name of the event to listen for.
name: &'a str,
name: String,
/// The ID of the node to attach the listener to.
id: ElementId,
@@ -226,7 +306,7 @@ pub enum Mutation<'a> {
/// Remove an existing Event Listener.
RemoveEventListener {
/// The name of the event to remove.
name: &'a str,
name: String,
/// The ID of the node to remove.
id: ElementId,
@@ -244,3 +324,189 @@ pub enum Mutation<'a> {
id: ElementId,
},
}
/// A static list of mutations that can be applied to the DOM. Note: this list does not contain any `Any` attribute values
#[derive(Debug, PartialEq, Default)]
pub struct Mutations {
/// The list of Scopes that were diffed, created, and removed during the Diff process.
pub dirty_scopes: FxHashSet<ScopeId>,
/// Any templates encountered while diffing the DOM.
///
/// These must be loaded into a cache before applying the edits
pub templates: Vec<Template>,
/// Any mutations required to patch the renderer to match the layout of the VirtualDom
pub edits: Vec<Mutation>,
}
impl Mutations {
/// Rewrites IDs to just be "template", so you can compare the mutations
///
/// Used really only for testing
pub fn santize(mut self) -> Self {
for edit in self.edits.iter_mut() {
if let Mutation::LoadTemplate { name, .. } = edit {
*name = "template"
}
}
self
}
}
impl WriteMutations for Mutations {
fn register_template(&mut self, template: Template) {
self.templates.push(template)
}
fn append_children(&mut self, id: ElementId, m: usize) {
self.edits.push(Mutation::AppendChildren { id, m })
}
fn assign_node_id(&mut self, path: &'static [u8], id: ElementId) {
self.edits.push(Mutation::AssignId { path, id })
}
fn create_placeholder(&mut self, id: ElementId) {
self.edits.push(Mutation::CreatePlaceholder { id })
}
fn create_text_node(&mut self, value: &str, id: ElementId) {
self.edits.push(Mutation::CreateTextNode {
value: value.into(),
id,
})
}
fn hydrate_text_node(&mut self, path: &'static [u8], value: &str, id: ElementId) {
self.edits.push(Mutation::HydrateText {
path,
value: value.into(),
id,
})
}
fn load_template(&mut self, name: &'static str, index: usize, id: ElementId) {
self.edits.push(Mutation::LoadTemplate { name, index, id })
}
fn replace_node_with(&mut self, id: ElementId, m: usize) {
self.edits.push(Mutation::ReplaceWith { id, m })
}
fn replace_placeholder_with_nodes(&mut self, path: &'static [u8], m: usize) {
self.edits.push(Mutation::ReplacePlaceholder { path, m })
}
fn insert_nodes_after(&mut self, id: ElementId, m: usize) {
self.edits.push(Mutation::InsertAfter { id, m })
}
fn insert_nodes_before(&mut self, id: ElementId, m: usize) {
self.edits.push(Mutation::InsertBefore { id, m })
}
fn set_attribute(
&mut self,
name: &'static str,
ns: Option<&'static str>,
value: &AttributeValue,
id: ElementId,
) {
self.edits.push(Mutation::SetAttribute {
name,
ns,
value: match value {
AttributeValue::Text(s) => AttributeValue::Text(s.clone()),
AttributeValue::Bool(b) => AttributeValue::Bool(*b),
AttributeValue::Float(n) => AttributeValue::Float(*n),
AttributeValue::Int(n) => AttributeValue::Int(*n),
AttributeValue::None => AttributeValue::None,
_ => panic!("Cannot serialize attribute value"),
},
id,
})
}
fn set_node_text(&mut self, value: &str, id: ElementId) {
self.edits.push(Mutation::SetText {
value: value.into(),
id,
})
}
fn create_event_listener(&mut self, name: &'static str, id: ElementId) {
self.edits.push(Mutation::NewEventListener {
name: name.into(),
id,
})
}
fn remove_event_listener(&mut self, name: &'static str, id: ElementId) {
self.edits.push(Mutation::RemoveEventListener {
name: name.into(),
id,
})
}
fn remove_node(&mut self, id: ElementId) {
self.edits.push(Mutation::Remove { id })
}
fn push_root(&mut self, id: ElementId) {
self.edits.push(Mutation::PushRoot { id })
}
fn swap_subtree(&mut self, _subtree_index: usize) {}
fn mark_scope_dirty(&mut self, scope_id: ScopeId) {
self.dirty_scopes.insert(scope_id);
}
}
/// A struct that ignores all mutations
pub struct NoOpMutations;
impl WriteMutations for NoOpMutations {
fn register_template(&mut self, _: Template) {}
fn append_children(&mut self, _: ElementId, _: usize) {}
fn assign_node_id(&mut self, _: &'static [u8], _: ElementId) {}
fn create_placeholder(&mut self, _: ElementId) {}
fn create_text_node(&mut self, _: &str, _: ElementId) {}
fn hydrate_text_node(&mut self, _: &'static [u8], _: &str, _: ElementId) {}
fn load_template(&mut self, _: &'static str, _: usize, _: ElementId) {}
fn replace_node_with(&mut self, _: ElementId, _: usize) {}
fn replace_placeholder_with_nodes(&mut self, _: &'static [u8], _: usize) {}
fn insert_nodes_after(&mut self, _: ElementId, _: usize) {}
fn insert_nodes_before(&mut self, _: ElementId, _: usize) {}
fn set_attribute(
&mut self,
_: &'static str,
_: Option<&'static str>,
_: &AttributeValue,
_: ElementId,
) {
}
fn set_node_text(&mut self, _: &str, _: ElementId) {}
fn create_event_listener(&mut self, _: &'static str, _: ElementId) {}
fn remove_event_listener(&mut self, _: &'static str, _: ElementId) {}
fn remove_node(&mut self, _: ElementId) {}
fn push_root(&mut self, _: ElementId) {}
}
+459 -452
View File
File diff suppressed because it is too large. Load diff
+145 -38
View File
@@ -1,12 +1,12 @@
use std::{any::TypeId, fmt::Arguments};
use crate::innerlude::*;
/// Every "Props" used for a component must implement the `Properties` trait. This trait gives some hints to Dioxus
/// on how to memoize the props and some additional optimizations that can be made. We strongly encourage using the
/// derive macro to implement the `Properties` trait automatically as guarantee that your memoization strategy is safe.
///
/// If your props are 'static, then Dioxus will require that they also be PartialEq for the derived memoize strategy. However,
/// if your props borrow data, then the memoization strategy will simply default to "false" and the PartialEq will be ignored.
/// This tends to be useful when props borrow something that simply cannot be compared (IE a reference to a closure);
/// If your props are 'static, then Dioxus will require that they also be PartialEq for the derived memoize strategy.
///
/// By default, the memoization strategy is very conservative, but can be tuned to be more aggressive manually. However,
/// this is only safe if the props are 'static - otherwise you might borrow references after-free.
@@ -18,49 +18,67 @@ use crate::innerlude::*;
///
/// For props that are 'static:
/// ```rust, ignore
/// #[derive(Props, PartialEq)]
/// #[derive(Props, PartialEq, Clone)]
/// struct MyProps {
/// data: String
/// }
/// ```
///
/// For props that borrow:
///
/// ```rust, ignore
/// #[derive(Props)]
/// struct MyProps<'a >{
/// data: &'a str
/// }
/// ```
pub trait Properties<'a>: Sized {
pub trait Properties: Clone + Sized + 'static {
/// The type of the builder for this component.
/// Used to create "in-progress" versions of the props.
type Builder;
/// An indication if these props are can be memoized automatically.
const IS_STATIC: bool;
/// Create a builder for this component.
fn builder(cx: &'a ScopeState) -> Self::Builder;
fn builder() -> Self::Builder;
/// Memoization can only happen if the props are valid for the 'static lifetime
///
/// # Safety
/// The user must know if their props are static, but if they make a mistake, UB happens
/// Therefore it's unsafe to memoize.
unsafe fn memoize(&self, other: &Self) -> bool;
/// Compare two props to see if they are memoizable.
fn memoize(&mut self, other: &Self) -> bool;
/// Create a component from the props.
fn into_vcomponent<M: 'static>(
self,
render_fn: impl ComponentFunction<Self, M>,
component_name: &'static str,
) -> VComponent {
VComponent::new(render_fn, self, component_name)
}
}
impl Properties<'_> for () {
impl Properties for () {
type Builder = EmptyBuilder;
const IS_STATIC: bool = true;
fn builder(_cx: &ScopeState) -> Self::Builder {
fn builder() -> Self::Builder {
EmptyBuilder {}
}
unsafe fn memoize(&self, _other: &Self) -> bool {
fn memoize(&mut self, _other: &Self) -> bool {
true
}
}
/// Root properties never need to be memoized, so we can use a dummy implementation.
pub(crate) struct RootProps<P>(pub P);
impl<P> Clone for RootProps<P>
where
P: Clone,
{
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<P> Properties for RootProps<P>
where
P: Clone + 'static,
{
type Builder = P;
fn builder() -> Self::Builder {
unreachable!("Root props technically are never built")
}
fn memoize(&mut self, _other: &Self) -> bool {
true
}
}
// We allow components to use the () generic parameter if they have no props. This impl enables the "build" method
// that the macros use to anonymously complete prop construction.
pub struct EmptyBuilder;
@@ -70,17 +88,106 @@ impl EmptyBuilder {
/// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern
/// to initialize a component's props.
pub fn fc_to_builder<'a, T: Properties<'a> + 'a>(
cx: &'a ScopeState,
_: fn(Scope<'a, T>) -> Element<'a>,
) -> T::Builder {
T::builder(cx)
pub fn fc_to_builder<P, M>(_: impl ComponentFunction<P, M>) -> <P as Properties>::Builder
where
P: Properties,
{
P::builder()
}
#[cfg(not(miri))]
/// Any component that implements the `ComponentFn` trait can be used as a component.
pub trait ComponentFunction<Props, Marker = ()>: Clone + 'static {
/// Get the type id of the component.
fn id(&self) -> TypeId {
TypeId::of::<Self>()
}
/// Convert the component to a function that takes props and returns an element.
fn rebuild(&self, props: Props) -> Element;
}
/// Accept any callbacks that take props
impl<F: Fn(P) -> Element + Clone + 'static, P> ComponentFunction<P> for F {
fn rebuild(&self, props: P) -> Element {
self(props)
}
}
/// Accept any callbacks that take no props
pub struct EmptyMarker;
impl<F: Fn() -> Element + Clone + 'static> ComponentFunction<(), EmptyMarker> for F {
fn rebuild(&self, _: ()) -> Element {
self()
}
}
/// A enhanced version of the `Into` trait that allows with more flexibility.
pub trait SuperInto<O, M = ()> {
/// Convert from a type to another type.
fn super_into(self) -> O;
}
impl<T, O, M> SuperInto<O, M> for T
where
O: SuperFrom<T, M>,
{
fn super_into(self) -> O {
O::super_from(self)
}
}
/// A enhanced version of the `From` trait that allows with more flexibility.
pub trait SuperFrom<T, M = ()> {
/// Convert from a type to another type.
fn super_from(_: T) -> Self;
}
// first implement for all types that are that implement the From trait
impl<T, O> SuperFrom<T, ()> for O
where
O: From<T>,
{
fn super_from(input: T) -> Self {
Self::from(input)
}
}
#[doc(hidden)]
pub struct OptionStringFromMarker;
impl<'a> SuperFrom<&'a str, OptionStringFromMarker> for Option<String> {
fn super_from(input: &'a str) -> Self {
Some(String::from(input))
}
}
#[doc(hidden)]
pub struct OptionArgumentsFromMarker;
impl<'a> SuperFrom<Arguments<'a>, OptionArgumentsFromMarker> for Option<String> {
fn super_from(input: Arguments<'a>) -> Self {
Some(input.to_string())
}
}
#[test]
fn unsafe_props_fail() {
let t = trybuild::TestCases::new();
t.compile_fail("compile_tests/props_safety.rs");
t.compile_fail("compile_tests/props_safety_temporary_values.rs");
#[allow(unused)]
fn from_props_compiles() {
// T -> T works
let option: i32 = 0i32.super_into();
let option: i32 = 0.super_into(); // Note we don't need type hints on all inputs
let option: i128 = 0.super_into();
let option: &'static str = "hello world".super_into();
// // T -> From<T> works
let option: i64 = 0i32.super_into();
let option: String = "hello world".super_into();
// T -> Option works
let option: Option<i32> = 0i32.super_into();
let option: Option<i32> = 0.super_into();
let option: Option<i128> = 0.super_into();
fn takes_option_string<M>(_: impl SuperInto<Option<String>, M>) {}
takes_option_string("hello world");
takes_option_string("hello world".to_string());
}
+136 -84
View File
@@ -1,66 +1,53 @@
use std::cell::{Cell, Ref, RefCell};
use crate::{innerlude::Scheduler, scope_context::ScopeContext, scopes::ScopeId};
use std::rc::Rc;
use crate::{
innerlude::{LocalTask, SchedulerMsg},
scope_context::Scope,
scopes::ScopeId,
Task,
};
use std::{
cell::{Cell, Ref, RefCell},
rc::Rc,
sync::Arc,
};
thread_local! {
static RUNTIMES: RefCell<Vec<Rc<Runtime>>> = RefCell::new(vec![]);
}
/// Pushes a new scope onto the stack
pub(crate) fn push_runtime(runtime: Rc<Runtime>) {
RUNTIMES.with(|stack| stack.borrow_mut().push(runtime));
}
/// Pops a scope off the stack
pub(crate) fn pop_runtime() {
RUNTIMES.with(|stack| stack.borrow_mut().pop());
}
/// Runs a function with the current runtime
pub(crate) fn with_runtime<F, R>(f: F) -> Option<R>
where
F: FnOnce(&Runtime) -> R,
{
RUNTIMES.with(|stack| {
let stack = stack.borrow();
stack.last().map(|r| f(r))
})
}
/// Runs a function with the current scope
pub(crate) fn with_current_scope<F, R>(f: F) -> Option<R>
where
F: FnOnce(&ScopeContext) -> R,
{
with_runtime(|runtime| {
runtime
.current_scope_id()
.and_then(|scope| runtime.get_context(scope).map(|sc| f(&sc)))
})
.flatten()
static RUNTIMES: RefCell<Vec<Rc<Runtime>>> = const { RefCell::new(vec![]) };
}
/// A global runtime that is shared across all scopes that provides the async runtime and context API
pub struct Runtime {
pub(crate) scope_contexts: RefCell<Vec<Option<ScopeContext>>>,
pub(crate) scheduler: Rc<Scheduler>,
pub(crate) scope_states: RefCell<Vec<Option<Scope>>>,
// We use this to track the current scope
pub(crate) scope_stack: RefCell<Vec<ScopeId>>,
// We use this to track the current task
pub(crate) current_task: Cell<Option<Task>>,
pub(crate) rendering: Cell<bool>,
/// Tasks created with cx.spawn
pub(crate) tasks: RefCell<slab::Slab<Rc<LocalTask>>>,
pub(crate) sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
// the virtualdom will hold this lock while it's doing syncronous work
// when the lock is lifted, tasks waiting for the lock will be able to run
pub(crate) flush_mutex: Arc<futures_util::lock::Mutex<()>>,
pub(crate) flush_lock: Cell<Option<futures_util::lock::OwnedMutexGuard<()>>>,
}
impl Runtime {
pub(crate) fn new(scheduler: Rc<Scheduler>) -> Rc<Self> {
pub(crate) fn new(sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>) -> Rc<Self> {
Rc::new(Self {
scheduler,
scope_contexts: Default::default(),
scope_stack: Default::default(),
sender,
flush_mutex: Default::default(),
flush_lock: Default::default(),
rendering: Cell::new(true),
scope_states: Default::default(),
scope_stack: Default::default(),
current_task: Default::default(),
tasks: Default::default(),
})
}
@@ -70,16 +57,39 @@ impl Runtime {
}
/// Create a scope context. This slab is synchronized with the scope slab.
pub(crate) fn create_context_at(&self, id: ScopeId, context: ScopeContext) {
let mut contexts = self.scope_contexts.borrow_mut();
if contexts.len() <= id.0 {
contexts.resize_with(id.0 + 1, Default::default);
pub(crate) fn create_scope(&self, context: Scope) {
let id = context.id;
let mut scopes = self.scope_states.borrow_mut();
if scopes.len() <= id.0 {
scopes.resize_with(id.0 + 1, Default::default);
}
contexts[id.0] = Some(context);
scopes[id.0] = Some(context);
}
pub(crate) fn remove_context(&self, id: ScopeId) {
self.scope_contexts.borrow_mut()[id.0] = None;
pub(crate) fn remove_scope(self: &Rc<Self>, id: ScopeId) {
{
let borrow = self.scope_states.borrow();
if let Some(scope) = &borrow[id.0] {
let _runtime_guard = RuntimeGuard::new(self.clone());
// Manually drop tasks, hooks, and contexts inside of the runtime
self.on_scope(id, || {
// Drop all spawned tasks - order doesn't matter since tasks don't rely on eachother
// In theory nested tasks might not like this
for id in scope.spawned_tasks.take() {
self.remove_task(id);
}
// Drop all hooks in reverse order in case a hook depends on another hook.
for hook in scope.hooks.take().drain(..).rev() {
drop(hook);
}
// Drop all contexts
scope.shared_contexts.take();
});
}
}
self.scope_states.borrow_mut()[id.0].take();
}
/// Get the current scope id
@@ -89,23 +99,75 @@ impl Runtime {
/// Call this function with the current scope set to the given scope
///
/// Useful in a limited number of scenarios, not public.
pub(crate) fn with_scope<O>(&self, id: ScopeId, f: impl FnOnce() -> O) -> O {
self.scope_stack.borrow_mut().push(id);
/// Useful in a limited number of scenarios
pub fn on_scope<O>(&self, id: ScopeId, f: impl FnOnce() -> O) -> O {
{
self.scope_stack.borrow_mut().push(id);
}
let o = f();
self.scope_stack.borrow_mut().pop();
{
self.scope_stack.borrow_mut().pop();
}
o
}
/// Get the context for any scope given its ID
/// Get the state for any scope given its ID
///
/// This is useful for inserting or removing contexts from a scope, or rendering out its root node
pub(crate) fn get_context(&self, id: ScopeId) -> Option<Ref<'_, ScopeContext>> {
Ref::filter_map(self.scope_contexts.borrow(), |contexts| {
pub(crate) fn get_state(&self, id: ScopeId) -> Option<Ref<'_, Scope>> {
Ref::filter_map(self.scope_states.borrow(), |contexts| {
contexts.get(id.0).and_then(|f| f.as_ref())
})
.ok()
}
/// Pushes a new scope onto the stack
pub(crate) fn push(runtime: Rc<Runtime>) {
RUNTIMES.with(|stack| stack.borrow_mut().push(runtime));
}
/// Pops a scope off the stack
pub(crate) fn pop() {
RUNTIMES.with(|stack| stack.borrow_mut().pop());
}
/// Runs a function with the current runtime
pub(crate) fn with<R>(f: impl FnOnce(&Runtime) -> R) -> Option<R> {
RUNTIMES.with(|stack| stack.borrow().last().map(|r| f(r)))
}
/// Runs a function with the current scope
pub(crate) fn with_current_scope<R>(f: impl FnOnce(&Scope) -> R) -> Option<R> {
Self::with(|rt| {
rt.current_scope_id()
.and_then(|scope| rt.get_state(scope).map(|sc| f(&sc)))
})
.flatten()
}
/// Runs a function with the current scope
pub(crate) fn with_scope<R>(scope: ScopeId, f: impl FnOnce(&Scope) -> R) -> Option<R> {
Self::with(|rt| rt.get_state(scope).map(|sc| f(&sc))).flatten()
}
/// Acquire the flush lock and store it interally
///
/// This means the virtual dom is currently doing syncronous work
/// The lock will be held until `release_flush_lock` is called - and then the OwnedLock will be dropped
pub(crate) fn acquire_flush_lock(&self) {
// The flush lock might already be held...
if let Some(lock) = self.flush_mutex.try_lock_owned() {
self.flush_lock.set(Some(lock));
}
}
/// Release the flush lock
///
/// On the drop of the flush lock, all tasks waiting on `flush_sync` will spring to life via their wakers.
/// You can now freely poll those tasks and they can progress
pub(crate) fn release_flush_lock(&self) {
self.flush_lock.take();
}
}
/// A guard for a new runtime. This must be used to override the current runtime when importing components from a dynamic library that has it's own runtime.
@@ -117,12 +179,12 @@ impl Runtime {
/// let virtual_dom = VirtualDom::new(app);
/// }
///
/// fn app(cx: Scope) -> Element {
/// render!{ Component { runtime: Runtime::current().unwrap() } }
/// fn app() -> Element {
/// rsx!{ Component { runtime: Runtime::current().unwrap() } }
/// }
///
/// // In a dynamic library
/// #[derive(Props)]
/// #[derive(Props, Clone)]
/// struct ComponentProps {
/// runtime: std::rc::Rc<Runtime>,
/// }
@@ -133,36 +195,26 @@ impl Runtime {
/// }
/// }
///
/// # #[allow(non_snake_case)]
/// fn Component(cx: Scope<ComponentProps>) -> Element {
/// cx.use_hook(|| RuntimeGuard::new(cx.props.runtime.clone()));
/// fn Component(cx: ComponentProps) -> Element {
/// use_hook(|| {
/// let _guard = RuntimeGuard::new(cx.runtime.clone());
/// });
///
/// render! { div {} }
/// rsx! { div {} }
/// }
/// ```
pub struct RuntimeGuard(Rc<Runtime>);
pub struct RuntimeGuard(());
impl RuntimeGuard {
/// Create a new runtime guard that sets the current Dioxus runtime. The runtime will be reset when the guard is dropped
pub fn new(runtime: Rc<Runtime>) -> Self {
push_runtime(runtime.clone());
Self(runtime)
}
/// Run a function with a given runtime and scope in context
pub fn with<O>(runtime: Rc<Runtime>, scope: Option<ScopeId>, f: impl FnOnce() -> O) -> O {
let guard = Self::new(runtime.clone());
let o = match scope {
Some(scope) => Runtime::with_scope(&runtime, scope, f),
None => f(),
};
drop(guard);
o
Runtime::push(runtime);
Self(())
}
}
impl Drop for RuntimeGuard {
fn drop(&mut self) {
pop_runtime();
Runtime::pop();
}
}
-37
View File
@@ -1,37 +0,0 @@
use crate::ScopeId;
use slab::Slab;
mod task;
mod wait;
pub use task::*;
/// The type of message that can be sent to the scheduler.
///
/// These messages control how the scheduler will process updates to the UI.
#[derive(Debug)]
pub(crate) enum SchedulerMsg {
/// Immediate updates from Components that mark them as dirty
Immediate(ScopeId),
/// A task has woken and needs to be progressed
TaskNotified(TaskId),
}
use std::{cell::RefCell, rc::Rc};
pub(crate) struct Scheduler {
pub sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
/// Tasks created with cx.spawn
pub tasks: RefCell<Slab<LocalTask>>,
}
impl Scheduler {
pub fn new(sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>) -> Rc<Self> {
Rc::new(Scheduler {
sender,
tasks: RefCell::new(Slab::new()),
})
}
}
-32
View File
@@ -1,32 +0,0 @@
use futures_util::task::ArcWake;
use super::SchedulerMsg;
use crate::ElementId;
use crate::{innerlude::Mutations, Element, ScopeId};
use std::future::Future;
use std::sync::Arc;
use std::task::Waker;
use std::{
cell::{Cell, RefCell},
collections::HashSet,
};
/// A boundary in the VirtualDom that captures all suspended components below it
pub struct SuspenseContext {
pub(crate) id: ScopeId,
pub(crate) waiting_on: RefCell<HashSet<ScopeId>>,
}
impl SuspenseContext {
/// Create a new boundary for suspense
pub fn new(id: ScopeId) -> Self {
Self {
id,
waiting_on: Default::default(),
}
}
pub fn mark_suspend(&self, id: ScopeId) {
self.waiting_on.borrow_mut().insert(id);
}
}
-84
View File
@@ -1,84 +0,0 @@
use futures_util::task::ArcWake;
use super::{Scheduler, SchedulerMsg};
use crate::ScopeId;
use std::cell::RefCell;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::task::Waker;
/// A task's unique identifier.
///
/// `TaskId` is a `usize` that is unique across the entire VirtualDOM and across time. TaskIDs will never be reused
/// once a Task has been completed.
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct TaskId(pub usize);
/// the task itself is the waker
pub(crate) struct LocalTask {
pub scope: ScopeId,
pub task: RefCell<Pin<Box<dyn Future<Output = ()> + 'static>>>,
pub waker: Waker,
}
impl Scheduler {
/// Start a new future on the same thread as the rest of the VirtualDom.
///
/// This future will not contribute to suspense resolving, so you should primarily use this for reacting to changes
/// and long running tasks.
///
/// Whenever the component that owns this future is dropped, the future will be dropped as well.
///
/// Spawning a future onto the root scope will cause it to be dropped when the root component is dropped - which
/// will only occur when the VirtuaalDom itself has been dropped.
pub fn spawn(&self, scope: ScopeId, task: impl Future<Output = ()> + 'static) -> TaskId {
let mut tasks = self.tasks.borrow_mut();
let entry = tasks.vacant_entry();
let task_id = TaskId(entry.key());
let task = LocalTask {
task: RefCell::new(Box::pin(task)),
scope,
waker: futures_util::task::waker(Arc::new(LocalTaskHandle {
id: task_id,
tx: self.sender.clone(),
})),
};
let mut cx = std::task::Context::from_waker(&task.waker);
if !task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() {
self.sender
.unbounded_send(SchedulerMsg::TaskNotified(task_id))
.expect("Scheduler should exist");
}
entry.insert(task);
task_id
}
/// Drop the future with the given TaskId
///
/// This does not abort the task, so you'll want to wrap it in an aborthandle if that's important to you
pub fn remove(&self, id: TaskId) -> Option<LocalTask> {
self.tasks.borrow_mut().try_remove(id.0)
}
}
pub struct LocalTaskHandle {
id: TaskId,
tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
}
impl ArcWake for LocalTaskHandle {
fn wake_by_ref(arc_self: &Arc<Self>) {
// This can fail if the scheduler has been dropped while the application is shutting down
let _ = arc_self
.tx
.unbounded_send(SchedulerMsg::TaskNotified(arc_self.id));
}
}
-39
View File
@@ -1,39 +0,0 @@
use crate::{runtime::RuntimeGuard, TaskId, VirtualDom};
use std::task::Context;
impl VirtualDom {
/// Handle notifications by tasks inside the scheduler
///
/// This is precise, meaning we won't poll every task, just tasks that have woken up as notified to use by the
/// queue
pub(crate) fn handle_task_wakeup(&mut self, id: TaskId) {
let _runtime = RuntimeGuard::new(self.runtime.clone());
let mut tasks = self.runtime.scheduler.tasks.borrow_mut();
let task = match tasks.get(id.0) {
Some(task) => task,
// The task was removed from the scheduler, so we can just ignore it
None => return,
};
let mut cx = Context::from_waker(&task.waker);
// update the scope stack
self.runtime.scope_stack.borrow_mut().push(task.scope);
self.runtime.rendering.set(false);
// If the task completes...
if task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() {
// Remove it from the scope so we dont try to double drop it when the scope dropes
let scope = &self.get_scope(task.scope).unwrap();
scope.context().spawned_tasks.borrow_mut().remove(&id);
// Remove it from the scheduler
tasks.try_remove(id.0);
}
// Remove the scope from the stack
self.runtime.scope_stack.borrow_mut().pop();
self.runtime.rendering.set(true);
}
}
+38 -58
View File
@@ -1,87 +1,70 @@
use crate::{
any_props::AnyProps,
bump_frame::BumpFrame,
innerlude::DirtyScope,
any_props::{AnyProps, BoxedAnyProps},
innerlude::{DirtyScope, ScopeState},
nodes::RenderReturn,
scope_context::ScopeContext,
scopes::{ScopeId, ScopeState},
scope_context::Scope,
scopes::ScopeId,
virtual_dom::VirtualDom,
};
impl VirtualDom {
pub(super) fn new_scope(
&mut self,
props: Box<dyn AnyProps<'static>>,
name: &'static str,
) -> &ScopeState {
pub(super) fn new_scope(&mut self, props: BoxedAnyProps, name: &'static str) -> &ScopeState {
let parent_id = self.runtime.current_scope_id();
let height = parent_id
.and_then(|parent_id| self.get_scope(parent_id).map(|f| f.context().height + 1))
.and_then(|parent_id| self.runtime.get_state(parent_id).map(|f| f.height + 1))
.unwrap_or(0);
let entry = self.scopes.vacant_entry();
let id = ScopeId(entry.key());
let scope = entry.insert(Box::new(ScopeState {
let scope = entry.insert(ScopeState {
runtime: self.runtime.clone(),
context_id: id,
props,
last_rendered_node: Default::default(),
});
props: Some(props),
node_arena_1: BumpFrame::new(0),
node_arena_2: BumpFrame::new(0),
render_cnt: Default::default(),
hooks: Default::default(),
hook_idx: Default::default(),
borrowed_props: Default::default(),
attributes_to_drop_before_render: Default::default(),
element_refs_to_drop: Default::default(),
}));
let context =
ScopeContext::new(name, id, parent_id, height, self.runtime.scheduler.clone());
self.runtime.create_context_at(id, context);
self.runtime
.create_scope(Scope::new(name, id, parent_id, height));
scope
}
pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn {
pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> RenderReturn {
debug_assert!(
crate::Runtime::current().is_some(),
"Must be in a dioxus runtime"
);
self.runtime.scope_stack.borrow_mut().push(scope_id);
// 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(scope_id);
let scope = &self.scopes[scope_id.0];
let new_nodes = {
let context = scope.state();
let new_nodes = unsafe {
let scope = &self.scopes[scope_id.0];
scope.previous_frame().reset();
context.suspended.set(false);
context.hook_index.set(0);
scope.context().suspended.set(false);
scope.hook_idx.set(0);
// Run all pre-render hooks
for pre_run in context.before_render.borrow_mut().iter_mut() {
pre_run();
}
// safety: due to how we traverse the tree, we know that the scope is not currently aliased
let props: &dyn AnyProps = scope.props.as_ref().unwrap().as_ref();
let props: &dyn AnyProps = std::mem::transmute(props);
let props: &dyn AnyProps = &*scope.props;
let _span = tracing::trace_span!("render", scope = %scope.context().name);
props.render(scope).extend_lifetime()
let span = tracing::trace_span!("render", scope = %scope.state().name);
span.in_scope(|| props.render())
};
let scope = &self.scopes[scope_id.0];
let context = scope.state();
// We write on top of the previous frame and then make it the current by pushing the generation forward
let frame = scope.previous_frame();
// set the new head of the bump frame
let allocated = &*frame.bump().alloc(new_nodes);
frame.node.set(allocated);
// Run all post-render hooks
for post_run in context.after_render.borrow_mut().iter_mut() {
post_run();
}
// And move the render generation forward by one
scope.render_cnt.set(scope.render_cnt.get() + 1);
context.render_count.set(context.render_count.get() + 1);
let context = scope.context();
// remove this scope from dirty scopes
self.dirty_scopes.remove(&DirtyScope {
height: context.height,
@@ -89,18 +72,15 @@ impl VirtualDom {
});
if context.suspended.get() {
if matches!(allocated, RenderReturn::Aborted(_)) {
if matches!(new_nodes, RenderReturn::Aborted(_)) {
self.suspended_scopes.insert(context.id);
}
} else if !self.suspended_scopes.is_empty() {
_ = self.suspended_scopes.remove(&context.id);
}
// rebind the lifetime now that its stored internally
let result = unsafe { allocated.extend_lifetime_ref() };
self.runtime.scope_stack.borrow_mut().pop();
result
new_nodes
}
}
+198 -134
View File
@@ -1,52 +1,52 @@
use crate::{
innerlude::{Scheduler, SchedulerMsg},
runtime::{with_current_scope, with_runtime},
Element, ScopeId, TaskId,
};
use crate::{innerlude::SchedulerMsg, Element, Runtime, ScopeId, Task};
use rustc_hash::FxHashSet;
use std::{
any::Any,
cell::{Cell, RefCell},
future::Future,
rc::Rc,
sync::Arc,
};
/// A component's state separate from its props.
///
/// This struct exists to provide a common interface for all scopes without relying on generics.
pub(crate) struct ScopeContext {
pub(crate) struct Scope {
pub(crate) name: &'static str,
pub(crate) id: ScopeId,
pub(crate) parent_id: Option<ScopeId>,
pub(crate) height: u32,
pub(crate) render_count: Cell<usize>,
pub(crate) suspended: Cell<bool>,
// Note: the order of the hook and context fields is important. The hooks field must be dropped before the contexts field in case a hook drop implementation tries to access a context.
pub(crate) hooks: RefCell<Vec<Box<dyn Any>>>,
pub(crate) hook_index: Cell<usize>,
pub(crate) shared_contexts: RefCell<Vec<Box<dyn Any>>>,
pub(crate) tasks: Rc<Scheduler>,
pub(crate) spawned_tasks: RefCell<FxHashSet<TaskId>>,
pub(crate) spawned_tasks: RefCell<FxHashSet<Task>>,
pub(crate) before_render: RefCell<Vec<Box<dyn FnMut()>>>,
pub(crate) after_render: RefCell<Vec<Box<dyn FnMut()>>>,
}
impl ScopeContext {
impl Scope {
pub(crate) fn new(
name: &'static str,
id: ScopeId,
parent_id: Option<ScopeId>,
height: u32,
tasks: Rc<Scheduler>,
) -> Self {
Self {
name,
id,
parent_id,
height,
render_count: Cell::new(0),
suspended: Cell::new(false),
shared_contexts: RefCell::new(vec![]),
tasks,
spawned_tasks: RefCell::new(FxHashSet::default()),
hooks: RefCell::new(vec![]),
hook_index: Cell::new(0),
before_render: RefCell::new(vec![]),
after_render: RefCell::new(vec![]),
}
}
@@ -54,45 +54,42 @@ impl ScopeContext {
self.parent_id
}
pub fn scope_id(&self) -> ScopeId {
self.id
fn sender(&self) -> futures_channel::mpsc::UnboundedSender<SchedulerMsg> {
Runtime::with(|rt| rt.sender.clone()).unwrap()
}
/// Mark this scope as dirty, and schedule a render for it.
pub fn needs_update(&self) {
self.needs_update_any(self.id)
}
/// Mark this scope as dirty, and schedule a render for it.
pub fn needs_update_any(&self, id: ScopeId) {
self.sender()
.unbounded_send(SchedulerMsg::Immediate(id))
.expect("Scheduler to exist if scope exists");
}
/// Create a subscription that schedules a future render for the reference component
///
/// ## Notice: you should prefer using [`Self::schedule_update_any`] and [`Self::scope_id`]
pub fn schedule_update(&self) -> Arc<dyn Fn() + Send + Sync + 'static> {
let (chan, id) = (self.tasks.sender.clone(), self.scope_id());
let (chan, id) = (self.sender(), self.id);
Arc::new(move || drop(chan.unbounded_send(SchedulerMsg::Immediate(id))))
}
/// Schedule an update for any component given its [`ScopeId`].
///
/// A component's [`ScopeId`] can be obtained from `use_hook` or the [`ScopeState::scope_id`] method.
/// A component's [`ScopeId`] can be obtained from `use_hook` or the [`current_scope_id`] method.
///
/// This method should be used when you want to schedule an update for a component
pub fn schedule_update_any(&self) -> Arc<dyn Fn(ScopeId) + Send + Sync> {
let chan = self.tasks.sender.clone();
let chan = self.sender();
Arc::new(move |id| {
chan.unbounded_send(SchedulerMsg::Immediate(id)).unwrap();
})
}
/// Mark this scope as dirty, and schedule a render for it.
pub fn needs_update(&self) {
self.needs_update_any(self.scope_id());
}
/// Get the [`ScopeId`] of a mounted component.
///
/// `ScopeId` is not unique for the lifetime of the [`crate::VirtualDom`] - a [`ScopeId`] will be reused if a component is unmounted.
pub fn needs_update_any(&self, id: ScopeId) {
self.tasks
.sender
.unbounded_send(SchedulerMsg::Immediate(id))
.expect("Scheduler to exist if scope exists");
}
/// Return any context of type T if it exists on this scope
pub fn has_context<T: 'static + Clone>(&self) -> Option<T> {
self.shared_contexts
@@ -117,9 +114,9 @@ impl ScopeContext {
}
let mut search_parent = self.parent_id;
let cur_runtime = with_runtime(|runtime: &crate::runtime::Runtime| {
let cur_runtime = Runtime::with(|runtime| {
while let Some(parent_id) = search_parent {
let parent = runtime.get_context(parent_id).unwrap();
let parent = runtime.get_state(parent_id).unwrap();
tracing::trace!(
"looking for context {} ({:?}) in {}",
std::any::type_name::<T>(),
@@ -150,6 +147,23 @@ impl ScopeContext {
}
}
/// Inject a Box<dyn Any> into the context of this scope
pub(crate) fn provide_any_context(&self, mut value: Box<dyn Any>) {
let mut contexts = self.shared_contexts.borrow_mut();
// If the context exists, swap it out for the new value
for ctx in contexts.iter_mut() {
// Swap the ptr directly
if ctx.as_ref().type_id() == value.as_ref().type_id() {
std::mem::swap(ctx, &mut value);
return;
}
}
// Else, just push it
contexts.push(value);
}
/// Expose state to children further down the [`crate::VirtualDom`] Tree. Requires `Clone` on the context to allow getting values down the tree.
///
/// This is a "fundamental" operation and should only be called during initialization of a hook.
@@ -161,14 +175,14 @@ impl ScopeContext {
/// ```rust, ignore
/// struct SharedState(&'static str);
///
/// static App: Component = |cx| {
/// static app: Component = |cx| {
/// cx.use_hook(|| cx.provide_context(SharedState("world")));
/// render!(Child {})
/// rsx!(Child {})
/// }
///
/// static Child: Component = |cx| {
/// let state = cx.consume_state::<SharedState>();
/// render!(div { "hello {state.0}" })
/// rsx!(div { "hello {state.0}" })
/// }
/// ```
pub fn provide_context<T: 'static + Clone>(&self, value: T) -> T {
@@ -203,40 +217,28 @@ impl ScopeContext {
/// Note that you should be checking if the context existed before trying to provide a new one. Providing a context
/// when a context already exists will swap the context out for the new one, which may not be what you want.
pub fn provide_root_context<T: 'static + Clone>(&self, context: T) -> T {
with_runtime(|runtime| {
Runtime::with(|runtime| {
runtime
.get_context(ScopeId::ROOT)
.get_state(ScopeId::ROOT)
.unwrap()
.provide_context(context)
})
.expect("Runtime to exist")
}
/// Pushes the future onto the poll queue to be polled after the component renders.
pub fn push_future(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
let id = self.tasks.spawn(self.id, fut);
/// Spawns the future but does not return the [`TaskId`]
pub fn spawn(&self, fut: impl Future<Output = ()> + 'static) -> Task {
let id = Runtime::with(|rt| rt.spawn(self.id, fut)).expect("Runtime to exist");
self.spawned_tasks.borrow_mut().insert(id);
id
}
/// Spawns the future but does not return the [`TaskId`]
pub fn spawn(&self, fut: impl Future<Output = ()> + 'static) {
self.push_future(fut);
}
/// Spawn a future that Dioxus won't clean up when this component is unmounted
///
/// This is good for tasks that need to be run after the component has been dropped.
pub fn spawn_forever(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
pub fn spawn_forever(&self, fut: impl Future<Output = ()> + 'static) -> Task {
// The root scope will never be unmounted so we can just add the task at the top of the app
self.tasks.spawn(ScopeId::ROOT, fut)
}
/// Informs the scheduler that this task is no longer needed and should be removed.
///
/// This drops the task immediately.
pub fn remove_future(&self, id: TaskId) {
self.tasks.remove(id);
Runtime::with(|rt| rt.spawn(self.id, fut)).expect("Runtime to exist")
}
/// Mark this component as suspended and then return None
@@ -244,95 +246,157 @@ impl ScopeContext {
self.suspended.set(true);
None
}
/// Store a value between renders. The foundational hook for all other hooks.
///
/// Accepts an `initializer` closure, which is run on the first use of the hook (typically the initial render). The return value of this closure is stored for the lifetime of the component, and a mutable reference to it is provided on every render as the return value of `use_hook`.
///
/// When the component is unmounted (removed from the UI), the value is dropped. This means you can return a custom type and provide cleanup code by implementing the [`Drop`] trait
///
/// # Example
///
/// ```
/// # use dioxus::prelude::*;
/// // prints a greeting on the initial render
/// pub fn use_hello_world() {
/// use_hook(|| println!("Hello, world!"));
/// }
/// ```
pub fn use_hook<State: Clone + 'static>(&self, initializer: impl FnOnce() -> State) -> State {
let cur_hook = self.hook_index.get();
let mut hooks = self.hooks.try_borrow_mut().expect("The hook list is already borrowed: This error is likely caused by trying to use a hook inside a hook which violates the rules of hooks.");
if cur_hook >= hooks.len() {
hooks.push(Box::new(initializer()));
}
hooks
.get(cur_hook)
.and_then(|inn| {
self.hook_index.set(cur_hook + 1);
let raw_ref: &dyn Any = inn.as_ref();
raw_ref.downcast_ref::<State>().cloned()
})
.expect(
r#"
Unable to retrieve the hook that was initialized at this index.
Consult the `rules of hooks` to understand how to use hooks properly.
You likely used the hook in a conditional. Hooks rely on consistent ordering between renders.
Functions prefixed with "use" should never be called conditionally.
"#,
)
}
pub fn push_before_render(&self, f: impl FnMut() + 'static) {
self.before_render.borrow_mut().push(Box::new(f));
}
pub fn push_after_render(&self, f: impl FnMut() + 'static) {
self.after_render.borrow_mut().push(Box::new(f));
}
/// Get the current render since the inception of this component
///
/// This can be used as a helpful diagnostic when debugging hooks/renders, etc
pub fn generation(&self) -> usize {
self.render_count.get()
}
/// Get the height of this scope
pub fn height(&self) -> u32 {
self.height
}
}
/// Schedule an update for any component given its [`ScopeId`].
///
/// A component's [`ScopeId`] can be obtained from `use_hook` or the [`crate::scopes::ScopeState::scope_id`] method.
///
/// This method should be used when you want to schedule an update for a component
pub fn schedule_update_any() -> Option<Arc<dyn Fn(ScopeId) + Send + Sync>> {
with_current_scope(|cx| cx.schedule_update_any())
}
impl ScopeId {
/// Get the current scope id
pub fn current_scope_id(self) -> Option<ScopeId> {
Runtime::with(|rt| rt.current_scope_id()).flatten()
}
/// Get the current scope id
pub fn current_scope_id() -> Option<ScopeId> {
with_runtime(|rt| rt.current_scope_id()).flatten()
}
#[doc(hidden)]
/// Check if the virtual dom is currently inside of the body of a component
pub fn vdom_is_rendering(self) -> bool {
Runtime::with(|rt| rt.rendering.get()).unwrap_or_default()
}
#[doc(hidden)]
/// Check if the virtual dom is currently inside of the body of a component
pub fn vdom_is_rendering() -> bool {
with_runtime(|rt| rt.rendering.get()).unwrap_or_default()
}
/// Consume context from the current scope
pub fn consume_context<T: 'static + Clone>(self) -> Option<T> {
Runtime::with_scope(self, |cx| cx.consume_context::<T>()).flatten()
}
/// Consume context from the current scope
pub fn consume_context<T: 'static + Clone>() -> Option<T> {
with_current_scope(|cx| cx.consume_context::<T>()).flatten()
}
/// Consume context from the current scope
pub fn consume_context_from_scope<T: 'static + Clone>(self, scope_id: ScopeId) -> Option<T> {
Runtime::with(|rt| {
rt.get_state(scope_id)
.and_then(|cx| cx.consume_context::<T>())
})
.flatten()
}
/// Consume context from the current scope
pub fn consume_context_from_scope<T: 'static + Clone>(scope_id: ScopeId) -> Option<T> {
with_runtime(|rt| {
rt.get_context(scope_id)
.and_then(|cx| cx.consume_context::<T>())
})
.flatten()
}
/// Check if the current scope has a context
pub fn has_context<T: 'static + Clone>(self) -> Option<T> {
Runtime::with_scope(self, |cx| cx.has_context::<T>()).flatten()
}
/// Check if the current scope has a context
pub fn has_context<T: 'static + Clone>() -> Option<T> {
with_current_scope(|cx| cx.has_context::<T>()).flatten()
}
/// Provide context to the current scope
pub fn provide_context<T: 'static + Clone>(self, value: T) -> T {
Runtime::with_scope(self, |cx| cx.provide_context(value))
.expect("to be in a dioxus runtime")
}
/// Provide context to the current scope
pub fn provide_context<T: 'static + Clone>(value: T) -> Option<T> {
with_current_scope(|cx| cx.provide_context(value))
}
/// Suspends the current component
pub fn suspend(self) -> Option<Element> {
Runtime::with_scope(self, |cx| {
cx.suspend();
});
None
}
/// Provide context to the the given scope
pub fn provide_context_to_scope<T: 'static + Clone>(scope_id: ScopeId, value: T) -> Option<T> {
with_runtime(|rt| rt.get_context(scope_id).map(|cx| cx.provide_context(value))).flatten()
}
/// Pushes the future onto the poll queue to be polled after the component renders.
pub fn push_future(self, fut: impl Future<Output = ()> + 'static) -> Option<Task> {
Runtime::with_scope(self, |cx| cx.spawn(fut))
}
/// Provide a context to the root scope
pub fn provide_root_context<T: 'static + Clone>(value: T) -> Option<T> {
with_current_scope(|cx| cx.provide_root_context(value))
}
/// Spawns the future but does not return the [`TaskId`]
pub fn spawn(self, fut: impl Future<Output = ()> + 'static) {
Runtime::with_scope(self, |cx| cx.spawn(fut));
}
/// Suspends the current component
pub fn suspend() -> Option<Element<'static>> {
with_current_scope(|cx| {
cx.suspend();
});
None
}
/// Get the current render since the inception of this component
///
/// This can be used as a helpful diagnostic when debugging hooks/renders, etc
pub fn generation(self) -> Option<usize> {
Runtime::with_scope(self, |cx| Some(cx.generation())).expect("to be in a dioxus runtime")
}
/// Pushes the future onto the poll queue to be polled after the component renders.
pub fn push_future(fut: impl Future<Output = ()> + 'static) -> Option<TaskId> {
with_current_scope(|cx| cx.push_future(fut))
}
/// Get the parent of the current scope if it exists
pub fn parent_scope(self) -> Option<ScopeId> {
Runtime::with_scope(self, |cx| cx.parent_id()).flatten()
}
/// Spawns the future but does not return the [`TaskId`]
pub fn spawn(fut: impl Future<Output = ()> + 'static) {
with_current_scope(|cx| cx.spawn(fut));
}
/// Mark the current scope as dirty, causing it to re-render
pub fn needs_update(self) {
Runtime::with_scope(self, |cx| cx.needs_update());
}
/// Spawn a future on a component given its [`ScopeId`].
pub fn spawn_at(fut: impl Future<Output = ()> + 'static, scope_id: ScopeId) -> Option<TaskId> {
with_runtime(|rt| rt.get_context(scope_id).unwrap().push_future(fut))
}
/// Create a subscription that schedules a future render for the reference component. Unlike [`Self::needs_update`], this function will work outside of the dioxus runtime.
///
/// ## Notice: you should prefer using [`dioxus_core::schedule_update_any`] and [`Self::scope_id`]
pub fn schedule_update(&self) -> Arc<dyn Fn() + Send + Sync + 'static> {
Runtime::with_scope(*self, |cx| cx.schedule_update()).expect("to be in a dioxus runtime")
}
/// Spawn a future that Dioxus won't clean up when this component is unmounted
///
/// This is good for tasks that need to be run after the component has been dropped.
pub fn spawn_forever(fut: impl Future<Output = ()> + 'static) -> Option<TaskId> {
spawn_at(fut, ScopeId(0))
}
/// Get the height of the current scope
pub fn height(self) -> u32 {
Runtime::with_scope(self, |cx| cx.height()).expect("to be in a dioxus runtime")
}
/// Informs the scheduler that this task is no longer needed and should be removed.
///
/// This drops the task immediately.
pub fn remove_future(id: TaskId) {
with_current_scope(|cx| cx.remove_future(id));
/// Run a closure inside of scope's runtime
pub fn in_runtime<T>(self, f: impl FnOnce() -> T) -> T {
Runtime::current()
.expect("to be in a dioxus runtime")
.on_scope(self, f)
}
}
+11 -543
View File
@@ -1,58 +1,7 @@
use crate::{
any_props::AnyProps,
any_props::VProps,
bump_frame::BumpFrame,
innerlude::{DynamicNode, ElementRef, EventHandler, VComponent, VNodeId, VText},
lazynodes::LazyNodes,
nodes::{IntoAttributeValue, IntoDynNode, RenderReturn},
runtime::Runtime,
scope_context::ScopeContext,
AnyValue, Attribute, AttributeType, AttributeValue, Element, ElementId, Event,
MountedAttribute, Properties, TaskId, Template, VNode,
any_props::BoxedAnyProps, nodes::RenderReturn, runtime::Runtime, scope_context::Scope,
};
use bumpalo::{boxed::Box as BumpBox, Bump};
use std::{
any::Any,
cell::{Cell, Ref, RefCell, UnsafeCell},
fmt::{Arguments, Debug},
future::Future,
rc::Rc,
sync::Arc,
};
/// A wrapper around the [`Scoped`] object that contains a reference to the [`ScopeState`] and properties for a given
/// component.
///
/// The [`Scope`] is your handle to the [`crate::VirtualDom`] and the component state. Every component is given its own
/// [`ScopeState`] and merged with its properties to create a [`Scoped`].
///
/// The [`Scope`] handle specifically exists to provide a stable reference to these items for the lifetime of the
/// component render.
pub type Scope<'a, T = ()> = &'a Scoped<'a, T>;
// This ScopedType exists because we want to limit the amount of monomorphization that occurs when making inner
// state type generic over props. When the state is generic, it causes every method to be monomorphized for every
// instance of Scope<T> in the codebase.
//
//
/// A wrapper around a component's [`ScopeState`] and properties. The [`ScopeState`] provides the majority of methods
/// for the VirtualDom and component state.
pub struct Scoped<'a, T = ()> {
/// The component's state and handle to the scheduler.
///
/// Stores things like the custom bump arena, spawn functions, hooks, and the scheduler.
pub scope: &'a ScopeState,
/// The component's properties.
pub props: &'a T,
}
impl<'a, T> std::ops::Deref for Scoped<'a, T> {
type Target = &'a ScopeState;
fn deref(&self) -> &Self::Target {
&self.scope
}
}
use std::{cell::Ref, fmt::Debug, rc::Rc};
/// A component's unique identifier.
///
@@ -78,79 +27,23 @@ impl ScopeId {
pub const ROOT: ScopeId = ScopeId(0);
}
/// A component's state separate from its props.
/// A component's rendered state.
///
/// This struct exists to provide a common interface for all scopes without relying on generics.
/// This state erases the type of the component's props. It is used to store the state of a component in the runtime.
pub struct ScopeState {
pub(crate) runtime: Rc<Runtime>,
pub(crate) context_id: ScopeId,
pub(crate) render_cnt: Cell<usize>,
pub(crate) node_arena_1: BumpFrame,
pub(crate) node_arena_2: BumpFrame,
pub(crate) hooks: RefCell<Vec<Box<UnsafeCell<dyn Any>>>>,
pub(crate) hook_idx: Cell<usize>,
pub(crate) borrowed_props: RefCell<Vec<*const VComponent<'static>>>,
pub(crate) element_refs_to_drop: RefCell<Vec<VNodeId>>,
pub(crate) attributes_to_drop_before_render: RefCell<Vec<*const Attribute<'static>>>,
pub(crate) props: Option<Box<dyn AnyProps<'static>>>,
pub(crate) last_rendered_node: Option<RenderReturn>,
pub(crate) props: BoxedAnyProps,
}
impl Drop for ScopeState {
fn drop(&mut self) {
self.drop_listeners();
self.runtime.remove_context(self.context_id);
self.runtime.remove_scope(self.context_id);
}
}
impl<'src> ScopeState {
pub(crate) fn context(&self) -> Ref<'_, ScopeContext> {
self.runtime.get_context(self.context_id).unwrap()
}
pub(crate) fn current_frame(&self) -> &BumpFrame {
match self.render_cnt.get() % 2 {
0 => &self.node_arena_1,
1 => &self.node_arena_2,
_ => unreachable!(),
}
}
pub(crate) fn previous_frame(&self) -> &BumpFrame {
match self.render_cnt.get() % 2 {
1 => &self.node_arena_1,
0 => &self.node_arena_2,
_ => unreachable!(),
}
}
/// Get the name of this component
pub fn name(&self) -> &str {
self.context().name
}
/// Get the current render since the inception of this component
///
/// This can be used as a helpful diagnostic when debugging hooks/renders, etc
pub fn generation(&self) -> usize {
self.render_cnt.get()
}
/// Get a handle to the currently active bump arena for this Scope
///
/// This is a bump memory allocator. Be careful using this directly since the contents will be wiped on the next render.
/// It's easy to leak memory here since the drop implementation will not be called for any objects allocated in this arena.
///
/// If you need to allocate items that need to be dropped, use bumpalo's box.
pub fn bump(&self) -> &Bump {
// note that this is actually the previous frame since we use that as scratch space while the component is rendering
self.previous_frame().bump()
}
impl ScopeState {
/// Get a handle to the currently active head node arena for this Scope
///
/// This is useful for traversing the tree outside of the VirtualDom, such as in a custom renderer or in SSR.
@@ -167,435 +60,10 @@ impl<'src> ScopeState {
///
/// Returns [`None`] if the tree has not been built yet.
pub fn try_root_node(&self) -> Option<&RenderReturn> {
let ptr = self.current_frame().node.get();
if ptr.is_null() {
return None;
}
let r: &RenderReturn = unsafe { &*ptr };
unsafe { std::mem::transmute(r) }
self.last_rendered_node.as_ref()
}
/// Get the height of this Scope - IE the number of scopes above it.
///
/// A Scope with a height of `0` is the root scope - there are no other scopes above it.
///
/// # Example
///
/// ```rust, ignore
/// let mut dom = VirtualDom::new(|cx| cx.render(rsx!{ div {} }));
/// dom.rebuild();
///
/// let base = dom.base_scope();
///
/// assert_eq!(base.height(), 0);
/// ```
pub fn height(&self) -> u32 {
self.context().height
}
/// Get the Parent of this [`Scope`] within this Dioxus [`crate::VirtualDom`].
///
/// This ID is not unique across Dioxus [`crate::VirtualDom`]s or across time. IDs will be reused when components are unmounted.
///
/// The base component will not have a parent, and will return `None`.
///
/// # Example
///
/// ```rust, ignore
/// let mut dom = VirtualDom::new(|cx| cx.render(rsx!{ div {} }));
/// dom.rebuild();
///
/// let base = dom.base_scope();
///
/// assert_eq!(base.parent(), None);
/// ```
pub fn parent(&self) -> Option<ScopeId> {
// safety: the pointer to our parent is *always* valid thanks to the bump arena
self.context().parent_id()
}
/// Get the ID of this Scope within this Dioxus [`crate::VirtualDom`].
///
/// This ID is not unique across Dioxus [`crate::VirtualDom`]s or across time. IDs will be reused when components are unmounted.
///
/// # Example
///
/// ```rust, ignore
/// let mut dom = VirtualDom::new(|cx| cx.render(rsx!{ div {} }));
/// dom.rebuild();
/// let base = dom.base_scope();
///
/// assert_eq!(base.scope_id(), 0);
/// ```
pub fn scope_id(&self) -> ScopeId {
self.context().scope_id()
}
/// Create a subscription that schedules a future render for the reference component
///
/// ## Notice: you should prefer using [`Self::schedule_update_any`] and [`Self::scope_id`]
pub fn schedule_update(&self) -> Arc<dyn Fn() + Send + Sync + 'static> {
self.context().schedule_update()
}
/// Schedule an update for any component given its [`ScopeId`].
///
/// A component's [`ScopeId`] can be obtained from `use_hook` or the [`ScopeState::scope_id`] method.
///
/// This method should be used when you want to schedule an update for a component
pub fn schedule_update_any(&self) -> Arc<dyn Fn(ScopeId) + Send + Sync> {
self.context().schedule_update_any()
}
/// Mark this scope as dirty, and schedule a render for it.
pub fn needs_update(&self) {
self.context().needs_update()
}
/// Get the [`ScopeId`] of a mounted component.
///
/// `ScopeId` is not unique for the lifetime of the [`crate::VirtualDom`] - a [`ScopeId`] will be reused if a component is unmounted.
pub fn needs_update_any(&self, id: ScopeId) {
self.context().needs_update_any(id)
}
/// Return any context of type T if it exists on this scope
pub fn has_context<T: 'static + Clone>(&self) -> Option<T> {
self.context().has_context()
}
/// Try to retrieve a shared state with type `T` from any parent scope.
///
/// Clones the state if it exists.
pub fn consume_context<T: 'static + Clone>(&self) -> Option<T> {
self.context().consume_context()
}
/// Expose state to children further down the [`crate::VirtualDom`] Tree. Requires `Clone` on the context to allow getting values down the tree.
///
/// This is a "fundamental" operation and should only be called during initialization of a hook.
///
/// For a hook that provides the same functionality, use `use_provide_context` and `use_context` instead.
///
/// # Example
///
/// ```rust, ignore
/// struct SharedState(&'static str);
///
/// static App: Component = |cx| {
/// cx.use_hook(|| cx.provide_context(SharedState("world")));
/// render!(Child {})
/// }
///
/// static Child: Component = |cx| {
/// let state = cx.consume_state::<SharedState>();
/// render!(div { "hello {state.0}" })
/// }
/// ```
pub fn provide_context<T: 'static + Clone>(&self, value: T) -> T {
self.context().provide_context(value)
}
/// Provide a context to the root and then consume it
///
/// This is intended for "global" state management solutions that would rather be implicit for the entire app.
/// Things like signal runtimes and routers are examples of "singletons" that would benefit from lazy initialization.
///
/// Note that you should be checking if the context existed before trying to provide a new one. Providing a context
/// when a context already exists will swap the context out for the new one, which may not be what you want.
pub fn provide_root_context<T: 'static + Clone>(&self, context: T) -> T {
self.context().provide_root_context(context)
}
/// Pushes the future onto the poll queue to be polled after the component renders.
pub fn push_future(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
self.context().push_future(fut)
}
/// Spawns the future but does not return the [`TaskId`]
pub fn spawn(&self, fut: impl Future<Output = ()> + 'static) {
self.context().spawn(fut);
}
/// Spawn a future that Dioxus won't clean up when this component is unmounted
///
/// This is good for tasks that need to be run after the component has been dropped.
pub fn spawn_forever(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
self.context().spawn_forever(fut)
}
/// Informs the scheduler that this task is no longer needed and should be removed.
///
/// This drops the task immediately.
pub fn remove_future(&self, id: TaskId) {
self.context().remove_future(id);
}
/// Take a lazy [`crate::VNode`] structure and actually build it with the context of the efficient [`bumpalo::Bump`] allocator.
///
/// ## Example
///
/// ```ignore
/// fn Component(cx: Scope<Props>) -> Element {
/// // Lazy assemble the VNode tree
/// let lazy_nodes = rsx!("hello world");
///
/// // Actually build the tree and allocate it
/// cx.render(lazy_tree)
/// }
/// ```
pub fn render(&'src self, rsx: LazyNodes<'src, '_>) -> Element<'src> {
// Note: We can't do anything in this function related to safety because the user could always circumvent it by creating a VNode manually and return it without calling this function
Some(rsx.call(self))
}
/// Create a [`crate::VNode`] from the component parts
pub fn vnode(
&'src self,
parent: Cell<Option<ElementRef>>,
key: Option<&'src str>,
template: Cell<Template<'static>>,
root_ids: RefCell<bumpalo::collections::Vec<'src, ElementId>>,
dynamic_nodes: &'src [DynamicNode<'src>],
dynamic_attrs: &'src [MountedAttribute<'src>],
) -> VNode<'src> {
let mut listeners = self.attributes_to_drop_before_render.borrow_mut();
for attr in dynamic_attrs {
let attrs = match attr.ty {
AttributeType::Single(ref attr) => std::slice::from_ref(attr),
AttributeType::Many(attrs) => attrs,
};
for attr in attrs {
match attr.value {
// We need to drop listeners before the next render because they may borrow data from the borrowed props which will be dropped
AttributeValue::Listener(_) => {
let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) };
listeners.push(unbounded);
}
// We need to drop any values manually to make sure that their drop implementation is called before the next render
AttributeValue::Any(_) => {
let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) };
self.previous_frame().add_attribute_to_drop(unbounded);
}
_ => (),
}
}
}
let mut props = self.borrowed_props.borrow_mut();
let mut drop_props = self
.previous_frame()
.props_to_drop_before_reset
.borrow_mut();
for node in dynamic_nodes {
if let DynamicNode::Component(comp) = node {
let unbounded = unsafe { std::mem::transmute(comp as *const VComponent) };
if !comp.static_props {
props.push(unbounded);
}
drop_props.push(unbounded);
}
}
VNode {
stable_id: Default::default(),
key,
parent,
template,
root_ids,
dynamic_nodes,
dynamic_attrs,
}
}
/// Create a dynamic text node using [`Arguments`] and the [`ScopeState`]'s internal [`Bump`] allocator
pub fn text_node(&'src self, args: Arguments) -> DynamicNode<'src> {
DynamicNode::Text(VText {
value: self.raw_text(args),
id: Default::default(),
})
}
/// Allocate some text inside the [`ScopeState`] from [`Arguments`]
///
/// Uses the currently active [`Bump`] allocator
pub fn raw_text(&'src self, args: Arguments) -> &'src str {
args.as_str().unwrap_or_else(|| {
use bumpalo::core_alloc::fmt::Write;
let mut str_buf = bumpalo::collections::String::new_in(self.bump());
str_buf.write_fmt(args).unwrap();
str_buf.into_bump_str()
})
}
/// Convert any item that implements [`IntoDynNode`] into a [`DynamicNode`] using the internal [`Bump`] allocator
pub fn make_node<'c, I>(&'src self, into: impl IntoDynNode<'src, I> + 'c) -> DynamicNode {
into.into_dyn_node(self)
}
/// Create a new [`Attribute`] from a name, value, namespace, and volatile bool
///
/// "Volatile" referes to whether or not Dioxus should always override the value. This helps prevent the UI in
/// some renderers stay in sync with the VirtualDom's understanding of the world
pub fn attr(
&'src self,
name: &'static str,
value: impl IntoAttributeValue<'src>,
namespace: Option<&'static str>,
volatile: bool,
) -> MountedAttribute<'src> {
MountedAttribute {
ty: AttributeType::Single(Attribute {
name,
namespace,
volatile,
value: value.into_value(self.bump()),
}),
mounted_element: Default::default(),
}
}
/// Create a new [`DynamicNode::Component`] variant
///
///
/// The given component can be any of four signatures. Remember that an [`Element`] is really a [`Result<VNode>`].
///
/// ```rust, ignore
/// // Without explicit props
/// fn(Scope) -> Element;
/// async fn(Scope<'_>) -> Element;
///
/// // With explicit props
/// fn(Scope<Props>) -> Element;
/// async fn(Scope<Props<'_>>) -> Element;
/// ```
pub fn component<'child, P>(
&'src self,
component: fn(Scope<'child, P>) -> Element<'child>,
props: P,
fn_name: &'static str,
) -> DynamicNode<'src>
where
// The properties must be valid until the next bump frame
P: Properties<'src> + 'src,
// The current bump allocator frame must outlive the child's borrowed props
'src: 'child,
{
let vcomp = VProps::new(component, P::memoize, props);
// cast off the lifetime of the render return
let as_dyn: Box<dyn AnyProps<'child> + '_> = Box::new(vcomp);
let extended: Box<dyn AnyProps<'src> + 'src> = unsafe { std::mem::transmute(as_dyn) };
DynamicNode::Component(VComponent {
name: fn_name,
render_fn: component as *const (),
static_props: P::IS_STATIC,
props: RefCell::new(Some(extended)),
scope: Default::default(),
})
}
/// Create a new [`EventHandler`] from an [`FnMut`]
pub fn event_handler<T>(&'src self, f: impl FnMut(T) + 'src) -> EventHandler<'src, T> {
let handler: &mut dyn FnMut(T) = self.bump().alloc(f);
let caller = unsafe { BumpBox::from_raw(handler as *mut dyn FnMut(T)) };
let callback = RefCell::new(Some(caller));
EventHandler {
callback,
origin: self.context().id,
}
}
/// Create a new [`AttributeValue`] with the listener variant from a callback
///
/// The callback must be confined to the lifetime of the ScopeState
pub fn listener<T: 'static>(
&'src self,
mut callback: impl FnMut(Event<T>) + 'src,
) -> AttributeValue<'src> {
// safety: there's no other way to create a dynamicly-dispatched bump box other than alloc + from-raw
// This is the suggested way to build a bumpbox
//
// In theory, we could just use regular boxes
let boxed: BumpBox<'src, dyn FnMut(_) + 'src> = unsafe {
BumpBox::from_raw(self.bump().alloc(move |event: Event<dyn Any>| {
if let Ok(data) = event.data.downcast::<T>() {
callback(Event {
propagates: event.propagates,
data,
});
}
}))
};
AttributeValue::Listener(RefCell::new(Some(boxed)))
}
/// Create a new [`AttributeValue`] with a value that implements [`AnyValue`]
pub fn any_value<T: AnyValue>(&'src self, value: T) -> AttributeValue<'src> {
// safety: there's no other way to create a dynamicly-dispatched bump box other than alloc + from-raw
// This is the suggested way to build a bumpbox
//
// In theory, we could just use regular boxes
let boxed: BumpBox<'src, dyn AnyValue> =
unsafe { BumpBox::from_raw(self.bump().alloc(value)) };
AttributeValue::Any(RefCell::new(Some(boxed)))
}
/// Mark this component as suspended and then return None
pub fn suspend(&self) -> Option<Element> {
let cx = self.context();
cx.suspend();
None
}
/// Store a value between renders. The foundational hook for all other hooks.
///
/// Accepts an `initializer` closure, which is run on the first use of the hook (typically the initial render). The return value of this closure is stored for the lifetime of the component, and a mutable reference to it is provided on every render as the return value of `use_hook`.
///
/// When the component is unmounted (removed from the UI), the value is dropped. This means you can return a custom type and provide cleanup code by implementing the [`Drop`] trait
///
/// # Example
///
/// ```
/// use dioxus_core::ScopeState;
///
/// // prints a greeting on the initial render
/// pub fn use_hello_world(cx: &ScopeState) {
/// cx.use_hook(|| println!("Hello, world!"));
/// }
/// ```
#[allow(clippy::mut_from_ref)]
pub fn use_hook<State: 'static>(&self, initializer: impl FnOnce() -> State) -> &mut State {
let cur_hook = self.hook_idx.get();
let mut hooks = self.hooks.try_borrow_mut().expect("The hook list is already borrowed: This error is likely caused by trying to use a hook inside a hook which violates the rules of hooks.");
if cur_hook >= hooks.len() {
hooks.push(Box::new(UnsafeCell::new(initializer())));
}
hooks
.get(cur_hook)
.and_then(|inn| {
self.hook_idx.set(cur_hook + 1);
let raw_ref = unsafe { &mut *inn.get() };
raw_ref.downcast_mut::<State>()
})
.expect(
r#"
Unable to retrieve the hook that was initialized at this index.
Consult the `rules of hooks` to understand how to use hooks properly.
You likely used the hook in a conditional. Hooks rely on consistent ordering between renders.
Functions prefixed with "use" should never be called conditionally.
"#,
)
pub(crate) fn state(&self) -> Ref<'_, Scope> {
self.runtime.get_state(self.context_id).unwrap()
}
}
-30
View File
@@ -1,30 +0,0 @@
/*
This is a WIP module
Subtrees allow the virtualdom to split up the mutation stream into smaller chunks which can be directed to different parts of the dom.
It's core to implementing multiwindow desktop support, portals, and alternative inline renderers like react-three-fiber.
The primary idea is to give each renderer a linear element tree managed by Dioxus to maximize performance and minimize memory usage.
This can't be done if two renderers need to share the same native tree.
With subtrees, we have an entirely different slab of elements
*/
use std::borrow::Cow;
use slab::Slab;
use crate::{ElementPath, ScopeId};
/// A collection of elements confined to a single scope under a chunk of the tree
///
/// All elements in this collection are guaranteed to be in the same scope and share the same numbering
///
/// This unit can be multithreaded
/// Whenever multiple subtrees are present, we can perform **parallel diffing**
pub struct Subtree {
id: usize,
namespace: Cow<'static, str>,
root: ScopeId,
elements: Slab<ElementPath>,
}
+222
View File
@@ -0,0 +1,222 @@
use crate::innerlude::{remove_future, spawn, Runtime};
use crate::ScopeId;
use futures_util::task::ArcWake;
use std::sync::Arc;
use std::task::Waker;
use std::{cell::Cell, future::Future};
use std::{cell::RefCell, rc::Rc};
use std::{pin::Pin, task::Poll};
/// A task's unique identifier.
///
/// `Task` is a unique identifier for a task that has been spawned onto the runtime. It can be used to cancel the task
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct Task(pub(crate) usize);
impl Task {
/// Start a new future on the same thread as the rest of the VirtualDom.
///
/// This future will not contribute to suspense resolving, so you should primarily use this for reacting to changes
/// and long running tasks.
///
/// Whenever the component that owns this future is dropped, the future will be dropped as well.
///
/// Spawning a future onto the root scope will cause it to be dropped when the root component is dropped - which
/// will only occur when the VirtualDom itself has been dropped.
pub fn new(task: impl Future<Output = ()> + 'static) -> Self {
spawn(task)
}
/// Drop the task immediately.
///
/// This does not abort the task, so you'll want to wrap it in an abort handle if that's important to you
pub fn cancel(self) {
remove_future(self);
}
/// Pause the task.
pub fn pause(&self) {
self.set_active(false);
}
/// Resume the task.
pub fn resume(&self) {
self.set_active(true);
}
/// Check if the task is paused.
pub fn paused(&self) -> bool {
Runtime::with(|rt| {
if let Some(task) = rt.tasks.borrow().get(self.0) {
!task.active.get()
} else {
false
}
})
.unwrap_or_default()
}
/// Wake the task.
pub fn wake(&self) {
Runtime::with(|rt| _ = rt.sender.unbounded_send(SchedulerMsg::TaskNotified(*self)));
}
/// Poll the task immediately.
pub fn poll_now(&self) -> Poll<()> {
Runtime::with(|rt| rt.handle_task_wakeup(*self)).unwrap()
}
/// Set the task as active or paused.
pub fn set_active(&self, active: bool) {
Runtime::with(|rt| {
if let Some(task) = rt.tasks.borrow().get(self.0) {
let was_active = task.active.replace(active);
if !was_active && active {
_ = rt.sender.unbounded_send(SchedulerMsg::TaskNotified(*self));
}
}
});
}
}
impl Runtime {
/// Start a new future on the same thread as the rest of the VirtualDom.
///
/// This future will not contribute to suspense resolving, so you should primarily use this for reacting to changes
/// and long running tasks.
///
/// Whenever the component that owns this future is dropped, the future will be dropped as well.
///
/// Spawning a future onto the root scope will cause it to be dropped when the root component is dropped - which
/// will only occur when the VirtualDom itself has been dropped.
pub fn spawn(&self, scope: ScopeId, task: impl Future<Output = ()> + 'static) -> Task {
// Insert the task, temporarily holding a borrow on the tasks map
let (task, task_id) = {
let mut tasks = self.tasks.borrow_mut();
let entry = tasks.vacant_entry();
let task_id = Task(entry.key());
let task = Rc::new(LocalTask {
scope,
active: Cell::new(true),
parent: self.current_task(),
task: RefCell::new(Box::pin(task)),
waker: futures_util::task::waker(Arc::new(LocalTaskHandle {
id: task_id,
tx: self.sender.clone(),
})),
});
entry.insert(task.clone());
(task, task_id)
};
// Get a borrow on the task, holding no borrows on the tasks map
debug_assert!(self.tasks.try_borrow_mut().is_ok());
debug_assert!(task.task.try_borrow_mut().is_ok());
self.sender
.unbounded_send(SchedulerMsg::TaskNotified(task_id))
.expect("Scheduler should exist");
task_id
}
/// Get the currently running task
pub fn current_task(&self) -> Option<Task> {
self.current_task.get()
}
/// Get the parent task of the given task, if it exists
pub fn parent_task(&self, task: Task) -> Option<Task> {
self.tasks.borrow().get(task.0)?.parent
}
pub(crate) fn handle_task_wakeup(&self, id: Task) -> Poll<()> {
debug_assert!(Runtime::current().is_some(), "Must be in a dioxus runtime");
let task = self.tasks.borrow().get(id.0).cloned();
// The task was removed from the scheduler, so we can just ignore it
let Some(task) = task else {
return Poll::Ready(());
};
// If a task woke up but is paused, we can just ignore it
if !task.active.get() {
return Poll::Pending;
}
let mut cx = std::task::Context::from_waker(&task.waker);
// update the scope stack
self.scope_stack.borrow_mut().push(task.scope);
self.rendering.set(false);
self.current_task.set(Some(id));
let poll_result = task.task.borrow_mut().as_mut().poll(&mut cx);
if poll_result.is_ready() {
// Remove it from the scope so we dont try to double drop it when the scope dropes
self.get_state(task.scope)
.unwrap()
.spawned_tasks
.borrow_mut()
.remove(&id);
// Remove it from the scheduler
self.tasks.borrow_mut().try_remove(id.0);
}
// Remove the scope from the stack
self.scope_stack.borrow_mut().pop();
self.rendering.set(true);
self.current_task.set(None);
poll_result
}
/// Drop the future with the given TaskId
///
/// This does not abort the task, so you'll want to wrap it in an abort handle if that's important to you
pub(crate) fn remove_task(&self, id: Task) -> Option<Rc<LocalTask>> {
self.tasks.borrow_mut().try_remove(id.0)
}
}
/// the task itself is the waker
pub(crate) struct LocalTask {
scope: ScopeId,
parent: Option<Task>,
task: RefCell<Pin<Box<dyn Future<Output = ()> + 'static>>>,
waker: Waker,
active: Cell<bool>,
}
/// The type of message that can be sent to the scheduler.
///
/// These messages control how the scheduler will process updates to the UI.
#[derive(Debug)]
pub(crate) enum SchedulerMsg {
/// Immediate updates from Components that mark them as dirty
Immediate(ScopeId),
/// A task has woken and needs to be progressed
TaskNotified(Task),
}
struct LocalTaskHandle {
id: Task,
tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
}
impl ArcWake for LocalTaskHandle {
fn wake_by_ref(arc_self: &Arc<Self>) {
_ = arc_self
.tx
.unbounded_send(SchedulerMsg::TaskNotified(arc_self.id));
}
}
+340 -310
View File
@@ -3,22 +3,22 @@
//! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust.
use crate::{
any_props::VProps,
arena::{ElementId, ElementRef},
innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg},
mutations::Mutation,
any_props::AnyProps,
arena::ElementId,
innerlude::{
DirtyScope, ElementRef, ErrorBoundary, NoOpMutations, SchedulerMsg, ScopeState, VNodeMount,
VProps, WriteMutations,
},
nodes::RenderReturn,
nodes::{Template, TemplateId},
runtime::{Runtime, RuntimeGuard},
scopes::{ScopeId, ScopeState},
AttributeValue, Element, Event, Scope, VNode,
scopes::ScopeId,
AttributeValue, ComponentFunction, Element, Event, Mutations,
};
use futures_util::{pin_mut, StreamExt};
use futures_util::StreamExt;
use rustc_hash::{FxHashMap, FxHashSet};
use slab::Slab;
use std::{
any::Any, cell::Cell, collections::BTreeSet, future::Future, ptr::NonNull, rc::Rc, sync::Arc,
};
use std::{any::Any, collections::BTreeSet, rc::Rc};
/// A virtual node system that progresses user events and diffs UI trees.
///
@@ -29,15 +29,15 @@ use std::{
/// ```rust
/// # use dioxus::prelude::*;
///
/// #[derive(Props, PartialEq)]
/// #[derive(Props, PartialEq, Clone)]
/// struct AppProps {
/// title: String
/// }
///
/// fn App(cx: Scope<AppProps>) -> Element {
/// cx.render(rsx!(
/// div {"hello, {cx.props.title}"}
/// ))
/// fn app(cx: AppProps) -> Element {
/// rsx!(
/// div {"hello, {cx.title}"}
/// )
/// }
/// ```
///
@@ -47,7 +47,7 @@ use std::{
/// # #![allow(unused)]
/// # use dioxus::prelude::*;
///
/// # #[derive(Props, PartialEq)]
/// # #[derive(Props, PartialEq, Clone)]
/// # struct AppProps {
/// # title: String
/// # }
@@ -55,31 +55,31 @@ use std::{
/// static ROUTES: &str = "";
///
/// #[component]
/// fn App(cx: Scope<AppProps>) -> Element {
/// cx.render(rsx!(
/// fn app(cx: AppProps) -> Element {
/// rsx!(
/// NavBar { routes: ROUTES }
/// Title { "{cx.props.title}" }
/// Title { "{cx.title}" }
/// Footer {}
/// ))
/// )
/// }
///
/// #[component]
/// fn NavBar(cx: Scope, routes: &'static str) -> Element {
/// cx.render(rsx! {
/// fn NavBar( routes: &'static str) -> Element {
/// rsx! {
/// div { "Routes: {routes}" }
/// })
/// }
/// }
///
/// #[component]
/// fn Footer(cx: Scope) -> Element {
/// cx.render(rsx! { div { "Footer" } })
/// fn Footer() -> Element {
/// rsx! { div { "Footer" } }
/// }
///
/// #[component]
/// fn Title<'a>(cx: Scope<'a>, children: Element<'a>) -> Element {
/// cx.render(rsx! {
/// fn Title( children: Element) -> Element {
/// rsx! {
/// div { id: "title", {children} }
/// })
/// }
/// }
/// ```
///
@@ -88,10 +88,10 @@ use std::{
///
/// ```rust
/// # use dioxus::prelude::*;
/// # fn App(cx: Scope) -> Element { cx.render(rsx! { div {} }) }
/// # fn app() -> Element { rsx! { div {} } }
///
/// let mut vdom = VirtualDom::new(App);
/// let edits = vdom.rebuild();
/// let mut vdom = VirtualDom::new(app);
/// let edits = vdom.rebuild_to_vec();
/// ```
///
/// To call listeners inside the VirtualDom, call [`VirtualDom::handle_event`] with the appropriate event data.
@@ -127,13 +127,13 @@ use std::{
/// Putting everything together, you can build an event loop around Dioxus by using the methods outlined above.
/// ```rust, ignore
/// #[component]
/// fn App(cx: Scope) -> Element {
/// cx.render(rsx! {
/// fn app() -> Element {
/// rsx! {
/// div { "Hello World" }
/// })
/// }
/// }
///
/// let dom = VirtualDom::new(App);
/// let dom = VirtualDom::new(app);
///
/// real_dom.apply(dom.rebuild());
///
@@ -180,27 +180,28 @@ use std::{
/// }
/// ```
pub struct VirtualDom {
pub(crate) scopes: Slab<Box<ScopeState>>,
pub(crate) scopes: Slab<ScopeState>,
pub(crate) dirty_scopes: BTreeSet<DirtyScope>,
// Maps a template path to a map of byteindexes to templates
pub(crate) templates: FxHashMap<TemplateId, FxHashMap<usize, Template<'static>>>,
// Maps a template path to a map of byte indexes to templates
pub(crate) templates: FxHashMap<TemplateId, FxHashMap<usize, Template>>,
// Every element is actually a dual reference - one to the template and the other to the dynamic node in that template
pub(crate) element_refs: Slab<Option<NonNull<VNode<'static>>>>,
// Templates changes that are queued for the next render
pub(crate) queued_templates: Vec<Template>,
// The element ids that are used in the renderer
pub(crate) elements: Slab<Option<ElementRef>>,
pub(crate) mutations: Mutations<'static>,
// Once nodes are mounted, the information about where they are mounted is stored here
pub(crate) mounts: Slab<VNodeMount>,
pub(crate) runtime: Rc<Runtime>,
// Currently suspended scopes
pub(crate) suspended_scopes: FxHashSet<ScopeId>,
pub(crate) rx: futures_channel::mpsc::UnboundedReceiver<SchedulerMsg>,
rx: futures_channel::mpsc::UnboundedReceiver<SchedulerMsg>,
}
impl VirtualDom {
@@ -216,15 +217,15 @@ impl VirtualDom {
///
/// # Example
/// ```rust, ignore
/// fn Example(cx: Scope) -> Element {
/// cx.render(rsx!( div { "hello world" } ))
/// fn Example() -> Element {
/// 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(app: fn(Scope) -> Element) -> Self {
pub fn new(app: fn() -> Element) -> Self {
Self::new_with_props(app, ())
}
@@ -245,8 +246,8 @@ impl VirtualDom {
/// name: &'static str
/// }
///
/// fn Example(cx: Scope<SomeProps>) -> Element {
/// cx.render(rsx!{ div{ "hello {cx.props.name}" } })
/// fn Example(cx: SomeProps) -> Element {
/// rsx!{ div { "hello {cx.name}" } }
/// }
///
/// let dom = VirtualDom::new(Example);
@@ -258,31 +259,70 @@ impl VirtualDom {
/// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" });
/// let mutations = dom.rebuild();
/// ```
pub fn new_with_props<P: 'static>(root: fn(Scope<P>) -> Element, root_props: P) -> Self {
pub fn new_with_props<P: Clone + 'static, M: 'static>(
root: impl ComponentFunction<P, M>,
root_props: P,
) -> Self {
Self::new_with_component(VProps::new(root, |_, _| true, root_props, "root"))
}
/// Create a new virtualdom and build it immediately
pub fn prebuilt(app: fn() -> Element) -> Self {
let mut dom = Self::new(app);
dom.rebuild_in_place();
dom
}
/// 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
/// ```rust, ignore
/// #[derive(PartialEq, Props)]
/// struct SomeProps {
/// name: &'static str
/// }
///
/// fn Example(cx: SomeProps) -> Element {
/// 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, ignore
/// let mut dom = VirtualDom::new_from_root(VComponent::new(Example, SomeProps { name: "jane" }, "Example"));
/// let mutations = dom.rebuild();
/// ```
pub(crate) fn new_with_component(root: impl AnyProps + 'static) -> Self {
let (tx, rx) = futures_channel::mpsc::unbounded();
let scheduler = Scheduler::new(tx);
let mut dom = Self {
rx,
runtime: Runtime::new(scheduler),
runtime: Runtime::new(tx),
scopes: Default::default(),
dirty_scopes: Default::default(),
templates: Default::default(),
queued_templates: Default::default(),
elements: Default::default(),
element_refs: Default::default(),
mutations: Mutations::default(),
mounts: Default::default(),
suspended_scopes: Default::default(),
};
let root = dom.new_scope(
Box::new(VProps::new(root, |_, _| unreachable!(), root_props)),
"app",
);
let root = dom.new_scope(Box::new(root), "app");
// Unlike react, we provide a default error boundary that just renders the error as a string
root.provide_context(Rc::new(ErrorBoundary::new_in_scope(
ScopeId::ROOT,
Arc::new(|_| {}),
)));
root.state()
.provide_context(Rc::new(ErrorBoundary::new_in_scope(ScopeId::ROOT)));
// the root element is always given element ID 0 since it's the container for the entire tree
dom.elements.insert(None);
@@ -294,7 +334,7 @@ impl VirtualDom {
///
/// This is useful for inserting or removing contexts from a scope, or rendering out its root node
pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
self.scopes.get(id.0).map(|s| &**s)
self.scopes.get(id.0)
}
/// Get the single scope at the top of the VirtualDom tree that will always be around
@@ -304,26 +344,43 @@ impl VirtualDom {
self.get_scope(ScopeId::ROOT).unwrap()
}
/// Run a closure inside the dioxus runtime
pub fn in_runtime<O>(&self, f: impl FnOnce() -> O) -> O {
let _runtime = RuntimeGuard::new(self.runtime.clone());
f()
}
/// Build the virtualdom with a global context inserted into the base scope
///
/// This is useful for what is essentially dependency injection when building the app
pub fn with_root_context<T: Clone + 'static>(self, context: T) -> Self {
self.base_scope().provide_context(context);
self.base_scope().state().provide_context(context);
self
}
/// Build the virtualdom with a global context inserted into the base scope
///
/// This method is useful for when you want to provide a context in your app without knowing its type
pub fn insert_any_root_context(&mut self, context: Box<dyn Any>) {
self.base_scope().state().provide_any_context(context);
}
/// Manually mark a scope as requiring a re-render
///
/// Whenever the Runtime "works", it will re-render this scope
pub fn mark_dirty(&mut self, id: ScopeId) {
if let Some(scope) = self.get_scope(id) {
let height = scope.height();
tracing::trace!("Marking scope {:?} ({}) as dirty", id, scope.context().name);
self.dirty_scopes.insert(DirtyScope { height, id });
}
let Some(scope) = self.runtime.get_state(id) else {
return;
};
tracing::trace!("Marking scope {:?} ({}) as dirty", id, scope.name);
self.dirty_scopes.insert(DirtyScope {
height: scope.height(),
id,
});
}
/// Call a listener inside the VirtualDom with data from outside the VirtualDom. **The ElementId passed in must be the id of an dynamic element, not a static node or a text node.**
/// Call a listener inside the VirtualDom with data from outside the VirtualDom. **The ElementId passed in must be the id of an element with a listener, not a static node or a text node.**
///
/// This method will identify the appropriate element. The data must match up with the listener declared. Note that
/// this method does not give any indication as to the success of the listener call. If the listener is not found,
@@ -341,129 +398,11 @@ impl VirtualDom {
) {
let _runtime = RuntimeGuard::new(self.runtime.clone());
/*
------------------------
The algorithm works by walking through the list of dynamic attributes, checking their paths, and breaking when
we find the target path.
With the target path, we try and move up to the parent until there is no parent.
Due to how bubbling works, we call the listeners before walking to the parent.
If we wanted to do capturing, then we would accumulate all the listeners and call them in reverse order.
----------------------
For a visual demonstration, here we present a tree on the left and whether or not a listener is collected on the
right.
| <-- yes (is ascendant)
| | | <-- no (is not direct ascendant)
| | <-- yes (is ascendant)
| | | | | <--- target element, break early, don't check other listeners
| | | <-- no, broke early
| <-- no, broke early
*/
let parent_path = match self.elements.get(element.0) {
Some(Some(el)) => el,
_ => return,
};
let mut parent_node = self
.element_refs
.get(parent_path.template.0)
.cloned()
.map(|el| (*parent_path, el));
// We will clone this later. The data itself is wrapped in RC to be used in callbacks if required
let uievent = Event {
propagates: Rc::new(Cell::new(bubbles)),
data,
};
// If the event bubbles, we traverse through the tree until we find the target element.
if bubbles {
// Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent.
while let Some((path, el_ref)) = parent_node {
let mut listeners = vec![];
// safety: we maintain references of all vnodes in the element slab
let template = unsafe { el_ref.unwrap().as_ref() };
let node_template = template.template.get();
let target_path = path.path;
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
let this_path = node_template.attr_paths[idx];
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
if target_path.is_decendant(&this_path) {
attr.ty.for_each(|attribute| {
if attribute.name.trim_start_matches("on") == name {
if let AttributeValue::Listener(listener) = &attribute.value {
listeners.push(listener);
}
}
});
}
}
// Now that we've accumulated all the parent attributes for the target element, call them in reverse order
// We check the bubble state between each call to see if the event has been stopped from bubbling
for listener in listeners.into_iter().rev() {
let origin = path.scope;
self.runtime.scope_stack.borrow_mut().push(origin);
self.runtime.rendering.set(false);
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
cb(uievent.clone());
}
self.runtime.scope_stack.borrow_mut().pop();
self.runtime.rendering.set(true);
if !uievent.propagates.get() {
return;
}
}
parent_node = template.parent.get().and_then(|element_ref| {
self.element_refs
.get(element_ref.template.0)
.cloned()
.map(|el| (element_ref, el))
});
}
} else {
// Otherwise, we just call the listener on the target element
if let Some((path, el_ref)) = parent_node {
// safety: we maintain references of all vnodes in the element slab
let template = unsafe { el_ref.unwrap().as_ref() };
let node_template = template.template.get();
let target_path = path.path;
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
let this_path = node_template.attr_paths[idx];
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
// Only call the listener if this is the exact target element.
if target_path == this_path {
let mut should_stop = false;
attr.ty.for_each(|attribute| {
if attribute.name.trim_start_matches("on") == name {
if let AttributeValue::Listener(listener) = &attribute.value {
let origin = path.scope;
self.runtime.scope_stack.borrow_mut().push(origin);
self.runtime.rendering.set(false);
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
cb(uievent.clone());
}
self.runtime.scope_stack.borrow_mut().pop();
self.runtime.rendering.set(true);
should_stop = true;
}
}
});
if should_stop {
return;
}
}
}
if let Some(Some(parent_path)) = self.elements.get(element.0).copied() {
if bubbles {
self.handle_bubbling_event(Some(parent_path), name, Event::new(data, bubbles));
} else {
self.handle_non_bubbling_event(parent_path, name, Event::new(data, bubbles));
}
}
}
@@ -481,45 +420,55 @@ impl VirtualDom {
/// # Example
///
/// ```rust, ignore
/// let dom = VirtualDom::new(App);
/// let sender = dom.get_scheduler_channel();
/// let dom = VirtualDom::new(app);
/// ```
pub async fn wait_for_work(&mut self) {
let mut some_msg = None;
// And then poll the futures
self.poll_tasks().await;
}
///
async fn poll_tasks(&mut self) {
// Release the flush lock
// This will cause all the flush wakers to immediately spring to life, which we will off with process_events
self.runtime.release_flush_lock();
loop {
match some_msg.take() {
// If a bunch of messages are ready in a sequence, try to pop them off synchronously
Some(msg) => match msg {
SchedulerMsg::Immediate(id) => self.mark_dirty(id),
SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task),
},
// Process all events - Scopes are marked dirty, etc
// Sometimes when wakers fire we get a slew of updates at once, so its important that we drain this completely
self.process_events();
// If they're not ready, then we should wait for them to be ready
None => {
match self.rx.try_next() {
Ok(Some(val)) => some_msg = Some(val),
Ok(None) => return,
Err(_) => {
// If we have any dirty scopes, or finished fiber trees then we should exit
if !self.dirty_scopes.is_empty() || !self.suspended_scopes.is_empty() {
return;
}
some_msg = self.rx.next().await
}
}
}
// Now that we have collected all queued work, we should check if we have any dirty scopes. If there are not, then we can poll any queued futures
if !self.dirty_scopes.is_empty() || !self.suspended_scopes.is_empty() {
return;
}
// Make sure we set the runtime since we're running user code
let _runtime = RuntimeGuard::new(self.runtime.clone());
// Hold a lock to the flush sync to prevent tasks from running in the event we get an immediate
// When we're doing awaiting the rx, the lock will be dropped and tasks waiting on the lock will get waked
// We have to own the lock since poll_tasks is cancel safe - the future that this is running in might get dropped
// and if we held the lock in the scope, the lock would also get dropped prematurely
self.runtime.release_flush_lock();
self.runtime.acquire_flush_lock();
match self.rx.next().await.expect("channel should never close") {
SchedulerMsg::Immediate(id) => self.mark_dirty(id),
SchedulerMsg::TaskNotified(id) => _ = self.runtime.handle_task_wakeup(id),
};
}
}
/// Process all events in the queue until there are no more left
pub fn process_events(&mut self) {
let _runtime = RuntimeGuard::new(self.runtime.clone());
// Prevent a task from deadlocking the runtime by repeatedly queueing itself
while let Ok(Some(msg)) = self.rx.try_next() {
match msg {
SchedulerMsg::Immediate(id) => self.mark_dirty(id),
SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task),
SchedulerMsg::TaskNotified(task) => _ = self.runtime.handle_task_wakeup(task),
}
}
}
@@ -527,10 +476,10 @@ impl VirtualDom {
/// Replace a template at runtime. This will re-render all components that use this template.
/// This is the primitive that enables hot-reloading.
///
/// The caller must ensure that the template refrences the same dynamic attributes and nodes as the original template.
/// The caller must ensure that the template references the same dynamic attributes and nodes as the original template.
///
/// This will only replace the the parent template, not any nested templates.
pub fn replace_template(&mut self, template: Template<'static>) {
pub fn replace_template(&mut self, template: Template) {
self.register_template_first_byte_index(template);
// iterating a slab is very inefficient, but this is a rare operation that will only happen during development so it's fine
for (_, scope) in self.scopes.iter() {
@@ -538,7 +487,7 @@ impl VirtualDom {
if sync.template.get().name.rsplit_once(':').unwrap().0
== template.name.rsplit_once(':').unwrap().0
{
let context = scope.context();
let context = scope.state();
let height = context.height;
self.dirty_scopes.insert(DirtyScope {
height,
@@ -549,6 +498,21 @@ impl VirtualDom {
}
}
/// Rebuild the virtualdom without handling any of the mutations
///
/// This is useful for testing purposes and in cases where you render the output of the virtualdom without
/// handling any of its mutations.
pub fn rebuild_in_place(&mut self) {
self.rebuild(&mut NoOpMutations);
}
/// [`VirtualDom::rebuild`] to a vector of mutations for testing purposes
pub fn rebuild_to_vec(&mut self) -> Mutations {
let mut mutations = Mutations::default();
self.rebuild(&mut mutations);
mutations
}
/// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom from scratch.
///
/// The mutations item expects the RealDom's stack to be the root of the application.
@@ -562,133 +526,199 @@ impl VirtualDom {
///
/// # Example
/// ```rust, ignore
/// static App: Component = |cx| cx.render(rsx!{ "hello world" });
/// static app: Component = |cx| rsx!{ "hello world" });
///
/// let mut dom = VirtualDom::new();
/// let edits = dom.rebuild();
///
/// apply_edits(edits);
/// ```
pub fn rebuild(&mut self) -> Mutations {
pub fn rebuild(&mut self, to: &mut impl WriteMutations) {
self.flush_templates(to);
let _runtime = RuntimeGuard::new(self.runtime.clone());
match unsafe { self.run_scope(ScopeId::ROOT).extend_lifetime_ref() } {
// Rebuilding implies we append the created elements to the root
RenderReturn::Ready(node) => {
let m = self.create_scope(ScopeId::ROOT, node);
self.mutations.edits.push(Mutation::AppendChildren {
id: ElementId(0),
m,
});
}
// If an error occurs, we should try to render the default error component and context where the error occured
RenderReturn::Aborted(placeholder) => {
tracing::debug!("Ran into suspended or aborted scope during rebuild");
let id = self.next_element();
placeholder.id.set(Some(id));
self.mutations.push(Mutation::CreatePlaceholder { id });
}
}
let new_nodes = self.run_scope(ScopeId::ROOT);
self.finalize()
// Rebuilding implies we append the created elements to the root
let m = self.create_scope(to, ScopeId::ROOT, new_nodes, None);
to.append_children(ElementId(0), m);
}
/// Render whatever the VirtualDom has ready as fast as possible without requiring an executor to progress
/// suspended subtrees.
pub fn render_immediate(&mut self) -> Mutations {
// Build a waker that won't wake up since our deadline is already expired when it's polled
let waker = futures_util::task::noop_waker();
let mut cx = std::task::Context::from_waker(&waker);
pub fn render_immediate(&mut self, to: &mut impl WriteMutations) {
self.flush_templates(to);
// Now run render with deadline but dont even try to poll any async tasks
let fut = self.render_with_deadline(std::future::ready(()));
pin_mut!(fut);
// Process any events that might be pending in the queue
// Signals marked with .write() need a chance to be handled by the effect driver
// This also processes futures which might progress into immediates
self.process_events();
// The root component is not allowed to be async
match fut.poll(&mut cx) {
std::task::Poll::Ready(mutations) => mutations,
std::task::Poll::Pending => panic!("render_immediate should never return pending"),
// Next, diff any dirty scopes
// We choose not to poll the deadline since we complete pretty quickly anyways
while let Some(dirty) = self.dirty_scopes.pop_first() {
// If the scope doesn't exist for whatever reason, then we should skip it
if !self.scopes.contains(dirty.id.0) {
continue;
}
{
let _runtime = RuntimeGuard::new(self.runtime.clone());
// Run the scope and get the mutations
let new_nodes = self.run_scope(dirty.id);
self.diff_scope(to, dirty.id, new_nodes);
}
}
}
/// [`Self::render_immediate`] to a vector of mutations for testing purposes
pub fn render_immediate_to_vec(&mut self) -> Mutations {
let mut mutations = Mutations::default();
self.render_immediate(&mut mutations);
mutations
}
/// Render the virtual dom, waiting for all suspense to be finished
///
/// The mutations will be thrown out, so it's best to use this method for things like SSR that have async content
///
/// We don't call "flush_sync" here since there's no sync work to be done. Futures will be progressed like usual,
/// however any futures wating on flush_sync will remain pending
pub async fn wait_for_suspense(&mut self) {
loop {
if self.suspended_scopes.is_empty() {
return;
break;
}
self.wait_for_work().await;
// Wait for a work to be ready (IE new suspense leaves to pop up)
self.poll_tasks().await;
_ = self.render_immediate();
// Render whatever work needs to be rendered, unlocking new futures and suspense leaves
self.render_immediate(&mut NoOpMutations);
}
}
/// Render what you can given the timeline and then move on
///
/// It's generally a good idea to put some sort of limit on the suspense process in case a future is having issues.
///
/// If no suspense trees are present
pub async fn render_with_deadline(&mut self, deadline: impl Future<Output = ()>) -> Mutations {
pin_mut!(deadline);
self.process_events();
loop {
// Next, diff any dirty scopes
// We choose not to poll the deadline since we complete pretty quickly anyways
if let Some(dirty) = self.dirty_scopes.iter().next().cloned() {
self.dirty_scopes.remove(&dirty);
// If the scope doesn't exist for whatever reason, then we should skip it
if !self.scopes.contains(dirty.id.0) {
continue;
}
{
let _runtime = RuntimeGuard::new(self.runtime.clone());
// Run the scope and get the mutations
self.run_scope(dirty.id);
self.diff_scope(dirty.id);
}
}
// If there's more work, then just continue, plenty of work to do
if !self.dirty_scopes.is_empty() {
continue;
}
// Poll the suspense leaves in the meantime
let mut work = self.wait_for_work();
// safety: this is okay since we don't touch the original future
let pinned = unsafe { std::pin::Pin::new_unchecked(&mut work) };
// If the deadline is exceded (left) then we should return the mutations we have
use futures_util::future::{select, Either};
if let Either::Left((_, _)) = select(&mut deadline, pinned).await {
// release the borrowed
drop(work);
return self.finalize();
}
}
}
/// Swap the current mutations with a new
fn finalize(&mut self) -> Mutations {
std::mem::take(&mut self.mutations)
}
/// Get the current runtime
pub fn runtime(&self) -> Rc<Runtime> {
self.runtime.clone()
}
/// Flush any queued template changes
fn flush_templates(&mut self, to: &mut impl WriteMutations) {
for template in self.queued_templates.drain(..) {
to.register_template(template);
}
}
/*
------------------------
The algorithm works by walking through the list of dynamic attributes, checking their paths, and breaking when
we find the target path.
With the target path, we try and move up to the parent until there is no parent.
Due to how bubbling works, we call the listeners before walking to the parent.
If we wanted to do capturing, then we would accumulate all the listeners and call them in reverse order.
----------------------
For a visual demonstration, here we present a tree on the left and whether or not a listener is collected on the
right.
| <-- yes (is ascendant)
| | | <-- no (is not direct ascendant)
| | <-- yes (is ascendant)
| | | | | <--- target element, break early, don't check other listeners
| | | <-- no, broke early
| <-- no, broke early
*/
fn handle_bubbling_event(
&mut self,
mut parent: Option<ElementRef>,
name: &str,
uievent: Event<dyn Any>,
) {
// If the event bubbles, we traverse through the tree until we find the target element.
// Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent.
while let Some(path) = parent {
let mut listeners = vec![];
let el_ref = &self.mounts[path.mount.0].node;
let node_template = el_ref.template.get();
let target_path = path.path;
// Accumulate listeners into the listener list bottom to top
for (idx, attrs) in el_ref.dynamic_attrs.iter().enumerate() {
let this_path = node_template.attr_paths[idx];
for attr in attrs.iter() {
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
if attr.name.trim_start_matches("on") == name
&& target_path.is_decendant(&this_path)
{
listeners.push(&attr.value);
// Break if this is the exact target element.
// This means we won't call two listeners with the same name on the same element. This should be
// documented, or be rejected from the rsx! macro outright
if target_path == this_path {
break;
}
}
}
}
// Now that we've accumulated all the parent attributes for the target element, call them in reverse order
// We check the bubble state between each call to see if the event has been stopped from bubbling
for listener in listeners.into_iter().rev() {
if let AttributeValue::Listener(listener) = listener {
self.runtime.rendering.set(false);
listener.call(uievent.clone());
self.runtime.rendering.set(true);
if !uievent.propagates.get() {
return;
}
}
}
let mount = el_ref.mount.get().as_usize();
parent = mount.and_then(|id| self.mounts.get(id).and_then(|el| el.parent));
}
}
/// Call an event listener in the simplest way possible without bubbling upwards
fn handle_non_bubbling_event(&mut self, node: ElementRef, name: &str, uievent: Event<dyn Any>) {
let el_ref = &self.mounts[node.mount.0].node;
let node_template = el_ref.template.get();
let target_path = node.path;
for (idx, attr) in el_ref.dynamic_attrs.iter().enumerate() {
let this_path = node_template.attr_paths[idx];
for attr in attr.iter() {
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
// Only call the listener if this is the exact target element.
if attr.name.trim_start_matches("on") == name && target_path == this_path {
if let AttributeValue::Listener(listener) = &attr.value {
self.runtime.rendering.set(false);
listener.call(uievent.clone());
self.runtime.rendering.set(true);
break;
}
}
}
}
}
}
impl Drop for VirtualDom {
fn drop(&mut self) {
// Simply drop this scope which drops all of its children
self.drop_scope(ScopeId::ROOT, true);
// Drop all scopes in order of height
let mut scopes = self.scopes.drain().collect::<Vec<_>>();
scopes.sort_by_key(|scope| scope.state().height);
for scope in scopes.into_iter().rev() {
drop(scope);
}
}
}
+1 -1
View File
@@ -44,5 +44,5 @@ Hooks
VirtualDOM API
- [] work
- [] rebuild
- [] rebuild_to_vec
- [] change props
+17 -35
View File
@@ -2,32 +2,24 @@
//!
//! This tests to ensure we clean it up
use bumpalo::Bump;
use dioxus::core::{ElementId, Mutation::*};
use dioxus::dioxus_core::{ElementId, Mutation::*};
use dioxus::prelude::*;
use dioxus_core::BorrowedAttributeValue;
#[test]
fn attrs_cycle() {
let mut dom = VirtualDom::new(|cx| {
let id = cx.generation();
match cx.generation() % 2 {
0 => cx.render(rsx! {
div {}
}),
1 => cx.render(rsx! {
div {
h1 { class: "{id}", id: "{id}" }
}
}),
let mut dom = VirtualDom::new(|| {
let id = generation();
match id % 2 {
0 => rsx! { div {} },
1 => rsx! {
div { h1 { class: "{id}", id: "{id}" } }
},
_ => unreachable!(),
}
});
let bump = Bump::new();
assert_eq!(
dom.rebuild().santize().edits,
dom.rebuild_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
AppendChildren { m: 1, id: ElementId(0) },
@@ -36,29 +28,19 @@ fn attrs_cycle() {
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
AssignId { path: &[0,], id: ElementId(3,) },
SetAttribute {
name: "class",
value: (&*bump.alloc("1".into_value(&bump))).into(),
id: ElementId(3,),
ns: None
},
SetAttribute {
name: "id",
value: (&*bump.alloc("1".into_value(&bump))).into(),
id: ElementId(3,),
ns: None
},
SetAttribute { name: "class", value: "1".into_value(), id: ElementId(3,), ns: None },
SetAttribute { name: "id", value: "1".into_value(), id: ElementId(3,), ns: None },
ReplaceWith { id: ElementId(1,), m: 1 },
]
);
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
ReplaceWith { id: ElementId(2), m: 1 }
@@ -67,19 +49,19 @@ fn attrs_cycle() {
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
AssignId { path: &[0], id: ElementId(3) },
SetAttribute {
name: "class",
value: BorrowedAttributeValue::Text("3"),
value: dioxus_core::AttributeValue::Text("3".to_string()),
id: ElementId(3),
ns: None
},
SetAttribute {
name: "id",
value: BorrowedAttributeValue::Text("3"),
value: dioxus_core::AttributeValue::Text("3".to_string()),
id: ElementId(3),
ns: None
},
@@ -90,7 +72,7 @@ fn attrs_cycle() {
// we take the node taken by attributes since we reused it
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
ReplaceWith { id: ElementId(2), m: 1 }
+4 -6
View File
@@ -1,19 +1,17 @@
use bumpalo::Bump;
use dioxus::core::{ElementId, Mutation::*};
use dioxus::dioxus_core::{ElementId, Mutation::*};
use dioxus::prelude::*;
#[test]
fn bool_test() {
let mut app = VirtualDom::new(|cx| cx.render(rsx!(div { hidden: false })));
let bump = Bump::new();
let mut app = VirtualDom::new(|| rsx!(div { hidden: false }));
assert_eq!(
app.rebuild().santize().edits,
app.rebuild_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
SetAttribute {
name: "hidden",
value: (&*bump.alloc(false.into_value(&bump))).into(),
value: dioxus_core::AttributeValue::Bool(false),
id: ElementId(1,),
ns: None
},
-55
View File
@@ -1,55 +0,0 @@
#![allow(non_snake_case)]
use dioxus::core::{ElementId, Mutation::*};
use dioxus::prelude::*;
#[test]
fn test_borrowed_state() {
let mut dom = VirtualDom::new(Parent);
assert_eq!(
dom.rebuild().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
LoadTemplate { name: "template", index: 0, id: ElementId(3,) },
HydrateText { path: &[0,], value: "Hello w1!", id: ElementId(4,) },
ReplacePlaceholder { path: &[1,], m: 1 },
ReplacePlaceholder { path: &[0,], m: 1 },
AppendChildren { m: 1, id: ElementId(0) },
]
);
}
fn Parent(cx: Scope) -> Element {
let w1 = cx.use_hook(|| String::from("w1"));
cx.render(rsx! {
div {
Child { name: w1 }
}
})
}
#[derive(Props)]
struct ChildProps<'a> {
name: &'a str,
}
fn Child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element {
cx.render(rsx! {
div {
h1 { "it's nested" }
Child2 { name: cx.props.name }
}
})
}
#[derive(Props)]
struct Grandchild<'a> {
name: &'a str,
}
fn Child2<'a>(cx: Scope<'a, Grandchild<'a>>) -> Element {
cx.render(rsx!(div { "Hello {cx.props.name}!" }))
}
+5 -7
View File
@@ -2,8 +2,8 @@
use dioxus::prelude::*;
fn app(cx: Scope) -> Element {
let raw = match cx.generation() % 2 {
fn app() -> Element {
let raw = match generation() % 2 {
0 => "123.123",
1 => "123.123.123",
_ => unreachable!(),
@@ -11,9 +11,7 @@ fn app(cx: Scope) -> Element {
let value = raw.parse::<f32>().unwrap_or(123.123);
cx.render(rsx! {
div { "hello {value}" }
})
rsx! { div { "hello {value}" } }
}
#[test]
@@ -21,10 +19,10 @@ fn bubbles_error() {
let mut dom = VirtualDom::new(app);
{
let _edits = dom.rebuild().santize();
let _edits = dom.rebuild_to_vec().santize();
}
dom.mark_dirty(ScopeId::ROOT);
_ = dom.render_immediate();
_ = dom.render_immediate_to_vec();
}
+23 -19
View File
@@ -1,51 +1,55 @@
use dioxus::core::{ElementId, Mutation::*};
use dioxus::dioxus_core::{ElementId, Mutation::*};
use dioxus::prelude::*;
#[test]
fn state_shares() {
fn app(cx: Scope) -> Element {
cx.provide_context(cx.generation() as i32);
fn app() -> Element {
provide_context(generation() as i32);
cx.render(rsx!(child_1 {}))
rsx!(child_1 {})
}
fn child_1(cx: Scope) -> Element {
cx.render(rsx!(child_2 {}))
fn child_1() -> Element {
rsx!(child_2 {})
}
fn child_2(cx: Scope) -> Element {
let value = cx.consume_context::<i32>().unwrap();
cx.render(rsx!("Value is {value}"))
fn child_2() -> Element {
let value = consume_context::<i32>();
rsx!("Value is {value}")
}
let mut dom = VirtualDom::new(app);
assert_eq!(
dom.rebuild().santize().edits,
dom.rebuild_to_vec().santize().edits,
[
CreateTextNode { value: "Value is 0", id: ElementId(1,) },
CreateTextNode { value: "Value is 0".to_string(), id: ElementId(1,) },
AppendChildren { m: 1, id: ElementId(0) },
]
);
dom.mark_dirty(ScopeId::ROOT);
_ = dom.render_immediate();
assert_eq!(dom.base_scope().consume_context::<i32>().unwrap(), 1);
_ = dom.render_immediate_to_vec();
dom.in_runtime(|| {
assert_eq!(ScopeId::ROOT.consume_context::<i32>().unwrap(), 1);
});
dom.mark_dirty(ScopeId::ROOT);
_ = dom.render_immediate();
assert_eq!(dom.base_scope().consume_context::<i32>().unwrap(), 2);
_ = dom.render_immediate_to_vec();
dom.in_runtime(|| {
assert_eq!(ScopeId::ROOT.consume_context::<i32>().unwrap(), 2);
});
dom.mark_dirty(ScopeId(2));
assert_eq!(
dom.render_immediate().santize().edits,
[SetText { value: "Value is 2", id: ElementId(1,) },]
dom.render_immediate_to_vec().santize().edits,
[SetText { value: "Value is 2".to_string(), id: ElementId(1,) },]
);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId(2));
let edits = dom.render_immediate();
let edits = dom.render_immediate_to_vec();
assert_eq!(
edits.santize().edits,
[SetText { value: "Value is 3", id: ElementId(1,) },]
[SetText { value: "Value is 3".to_string(), id: ElementId(1,) },]
);
}
+35 -46
View File
@@ -2,26 +2,22 @@
//! Prove that the dom works normally through virtualdom methods.
//!
//! This methods all use "rebuild" which completely bypasses the scheduler.
//! Hard rebuilds don't consume any events from the event queue.
//! This methods all use "rebuild_to_vec" which completely bypasses the scheduler.
//! Hard rebuild_to_vecs don't consume any events from the event queue.
use dioxus::core::Mutation::*;
use dioxus::dioxus_core::Mutation::*;
use dioxus::prelude::*;
use dioxus_core::ElementId;
#[test]
fn test_original_diff() {
let mut dom = VirtualDom::new(|cx| {
cx.render(rsx! {
div {
div {
"Hello, world!"
}
}
})
let mut dom = VirtualDom::new(|| {
rsx! {
div { div { "Hello, world!" } }
}
});
let edits = dom.rebuild().santize();
let edits = dom.rebuild_to_vec().santize();
assert_eq!(
edits.edits,
@@ -35,25 +31,22 @@ fn test_original_diff() {
#[test]
fn create() {
let mut dom = VirtualDom::new(|cx| {
cx.render(rsx! {
let mut dom = VirtualDom::new(|| {
rsx! {
div {
div {
"Hello, world!"
div {
div {
Fragment {
"hello"
"world"
}
Fragment { "hello""world" }
}
}
}
}
})
}
});
let _edits = dom.rebuild().santize();
let _edits = dom.rebuild_to_vec().santize();
// todo: we don't test template mutations anymore since the templates are passed along
@@ -82,13 +75,9 @@ fn create() {
#[test]
fn create_list() {
let mut dom = VirtualDom::new(|cx| {
cx.render(rsx! {
{(0..3).map(|f| rsx!( div { "hello" } ))}
})
});
let mut dom = VirtualDom::new(|| rsx! {{(0..3).map(|f| rsx!( div { "hello" } ))}});
let _edits = dom.rebuild().santize();
let _edits = dom.rebuild_to_vec().santize();
// note: we dont test template edits anymore
// assert_eq!(
@@ -105,16 +94,16 @@ fn create_list() {
#[test]
fn create_simple() {
let mut dom = VirtualDom::new(|cx| {
cx.render(rsx! {
let mut dom = VirtualDom::new(|| {
rsx! {
div {}
div {}
div {}
div {}
})
}
});
let edits = dom.rebuild().santize();
let edits = dom.rebuild_to_vec().santize();
// note: we dont test template edits anymore
// assert_eq!(
@@ -132,47 +121,47 @@ fn create_simple() {
}
#[test]
fn create_components() {
let mut dom = VirtualDom::new(|cx| {
cx.render(rsx! {
let mut dom = VirtualDom::new(|| {
rsx! {
Child { "abc1" }
Child { "abc2" }
Child { "abc3" }
})
}
});
#[derive(Props)]
struct ChildProps<'a> {
children: Element<'a>,
#[derive(Props, Clone, PartialEq)]
struct ChildProps {
children: Element,
}
fn Child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element {
cx.render(rsx! {
fn Child(cx: ChildProps) -> Element {
rsx! {
h1 {}
div { {&cx.props.children} }
div { {cx.children} }
p {}
})
}
}
let _edits = dom.rebuild().santize();
let _edits = dom.rebuild_to_vec().santize();
// todo: test this
}
#[test]
fn anchors() {
let mut dom = VirtualDom::new(|cx| {
cx.render(rsx! {
let mut dom = VirtualDom::new(|| {
rsx! {
if true {
div { "hello" }
div { "hello" }
}
if false {
div { "goodbye" }
}
})
}
});
// note that the template under "false" doesn't show up since it's not loaded
let edits = dom.rebuild().santize();
let edits = dom.rebuild_to_vec().santize();
// note: we dont test template edits anymore
// assert_eq!(
+6 -6
View File
@@ -1,21 +1,21 @@
// use dioxus::core::Mutation::*;
// use dioxus::dioxus_core::Mutation::*;
use dioxus::prelude::*;
#[test]
fn multiroot() {
let mut dom = VirtualDom::new(|cx| {
cx.render(rsx! {
let mut dom = VirtualDom::new(|| {
rsx! {
div { "Hello a" }
div { "Hello b" }
div { "Hello c" }
})
}
});
// note: we dont test template edits anymore
let _templates = dom.rebuild().santize().templates;
let _templates = dom.rebuild_to_vec().santize().templates;
// assert_eq!(
// dom.rebuild().santize().templates,
// dom.rebuild_to_vec().santize().templates,
// [
// CreateElement { name: "div" },
// CreateStaticText { value: "Hello a" },
+23 -26
View File
@@ -1,17 +1,17 @@
//! Do we create fragments properly across complex boundaries?
use dioxus::core::Mutation::*;
use dioxus::dioxus_core::Mutation::*;
use dioxus::prelude::*;
use dioxus_core::ElementId;
#[test]
fn empty_fragment_creates_nothing() {
fn app(cx: Scope) -> Element {
cx.render(rsx!({}))
fn app() -> Element {
rsx!({})
}
let mut vdom = VirtualDom::new(app);
let edits = vdom.rebuild();
let edits = vdom.rebuild_to_vec();
assert_eq!(
edits.edits,
@@ -24,23 +24,23 @@ fn empty_fragment_creates_nothing() {
#[test]
fn root_fragments_work() {
let mut vdom = VirtualDom::new(|cx| {
cx.render(rsx!(
let mut vdom = VirtualDom::new(|| {
rsx!(
div { "hello" }
div { "goodbye" }
))
)
});
assert_eq!(
vdom.rebuild().edits.last().unwrap(),
vdom.rebuild_to_vec().edits.last().unwrap(),
&AppendChildren { id: ElementId(0), m: 2 }
);
}
#[test]
fn fragments_nested() {
let mut vdom = VirtualDom::new(|cx| {
cx.render(rsx!(
let mut vdom = VirtualDom::new(|| {
rsx!(
div { "hello" }
div { "goodbye" }
{rsx! {
@@ -55,50 +55,47 @@ fn fragments_nested() {
}}
}}
}}
))
)
});
assert_eq!(
vdom.rebuild().edits.last().unwrap(),
vdom.rebuild_to_vec().edits.last().unwrap(),
&AppendChildren { id: ElementId(0), m: 8 }
);
}
#[test]
fn fragments_across_components() {
fn app(cx: Scope) -> Element {
cx.render(rsx! {
fn app() -> Element {
rsx! {
demo_child {}
demo_child {}
demo_child {}
demo_child {}
})
}
}
fn demo_child(cx: Scope) -> Element {
fn demo_child() -> Element {
let world = "world";
cx.render(rsx! {
"hellO!"
{world}
})
rsx! { "hellO!", {world} }
}
assert_eq!(
VirtualDom::new(app).rebuild().edits.last().unwrap(),
VirtualDom::new(app).rebuild_to_vec().edits.last().unwrap(),
&AppendChildren { id: ElementId(0), m: 8 }
);
}
#[test]
fn list_fragments() {
fn app(cx: Scope) -> Element {
cx.render(rsx!(
h1 {"hello"}
fn app() -> Element {
rsx!(
h1 { "hello" }
{(0..6).map(|f| rsx!( span { "{f}" }))}
))
)
}
assert_eq!(
VirtualDom::new(app).rebuild().edits.last().unwrap(),
VirtualDom::new(app).rebuild_to_vec().edits.last().unwrap(),
&AppendChildren { id: ElementId(0), m: 7 }
);
}
+8 -8
View File
@@ -1,4 +1,4 @@
use dioxus::core::Mutation::*;
use dioxus::dioxus_core::Mutation::*;
use dioxus::prelude::*;
use dioxus_core::ElementId;
@@ -8,8 +8,8 @@ use dioxus_core::ElementId;
// In Dioxus, we memoize the rsx! body and simplify it down to a few template loads
//
// Also note that the IDs increase linearly. This lets us drive a vec on the renderer for O(1) re-indexing
fn app(cx: Scope) -> Element {
cx.render(rsx! {
fn app() -> Element {
rsx! {
div {
for i in 0..3 {
div {
@@ -18,14 +18,14 @@ fn app(cx: Scope) -> Element {
}
}
}
})
}
}
#[test]
fn list_renders() {
let mut dom = VirtualDom::new(app);
let edits = dom.rebuild().santize();
let edits = dom.rebuild_to_vec().santize();
// note: we dont test template edits anymore
// assert_eq!(
@@ -58,11 +58,11 @@ fn list_renders() {
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
// Load each template one-by-one, rehydrating it
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
HydrateText { path: &[1, 0], value: "0", id: ElementId(3) },
HydrateText { path: &[1, 0], value: "0".to_string(), id: ElementId(3) },
LoadTemplate { name: "template", index: 0, id: ElementId(4) },
HydrateText { path: &[1, 0], value: "1", id: ElementId(5) },
HydrateText { path: &[1, 0], value: "1".to_string(), id: ElementId(5) },
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
HydrateText { path: &[1, 0], value: "2", id: ElementId(7) },
HydrateText { path: &[1, 0], value: "2".to_string(), id: ElementId(7) },
// Replace the 0th childn on the div with the 3 templates on the stack
ReplacePlaceholder { m: 3, path: &[0] },
// Append the container div to the dom
+23 -30
View File
@@ -1,30 +1,27 @@
use dioxus::core::Mutation::*;
use dioxus::dioxus_core::Mutation::*;
use dioxus::prelude::*;
use dioxus_core::ElementId;
/// Should push the text node onto the stack and modify it
#[test]
fn nested_passthru_creates() {
#[component]
fn App(cx: Scope) -> Element {
cx.render(rsx! {
fn app() -> Element {
rsx! {
PassThru {
PassThru {
PassThru {
div { "hi" }
}
PassThru { div { "hi" } }
}
}
})
}
}
#[component]
fn PassThru<'a>(cx: Scope<'a>, children: Element<'a>) -> Element {
cx.render(rsx!({ children }))
fn PassThru(children: Element) -> Element {
rsx!({ children })
}
let mut dom = VirtualDom::new(App);
let edits = dom.rebuild().santize();
let mut dom = VirtualDom::new(app);
let edits = dom.rebuild_to_vec().santize();
assert_eq!(
edits.edits,
@@ -40,33 +37,30 @@ fn nested_passthru_creates() {
/// Take note on how we don't spit out the template for child_comp since it's entirely dynamic
#[test]
fn nested_passthru_creates_add() {
#[component]
fn App(cx: Scope) -> Element {
cx.render(rsx! {
fn app() -> Element {
rsx! {
ChildComp {
"1"
ChildComp {
"2"
ChildComp {
"3"
div {
"hi"
}
div { "hi" }
}
}
}
})
}
}
#[component]
fn ChildComp<'a>(cx: Scope, children: Element<'a>) -> Element {
cx.render(rsx! { {children} })
fn ChildComp(children: Element) -> Element {
rsx! {{children}}
}
let mut dom = VirtualDom::new(App);
let mut dom = VirtualDom::new(app);
assert_eq!(
dom.rebuild().santize().edits,
dom.rebuild_to_vec().santize().edits,
[
// load 1
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
@@ -84,15 +78,14 @@ fn nested_passthru_creates_add() {
/// note that the template is all dynamic roots - so it doesn't actually get cached as a template
#[test]
fn dynamic_node_as_root() {
#[component]
fn App(cx: Scope) -> Element {
fn app() -> Element {
let a = 123;
let b = 456;
cx.render(rsx! { "{a}" "{b}" })
rsx! { "{a}", "{b}" }
}
let mut dom = VirtualDom::new(App);
let edits = dom.rebuild().santize();
let mut dom = VirtualDom::new(app);
let edits = dom.rebuild_to_vec().santize();
// Since the roots were all dynamic, they should not cause any template muations
assert!(edits.templates.is_empty());
@@ -101,8 +94,8 @@ fn dynamic_node_as_root() {
assert_eq!(
edits.edits,
[
CreateTextNode { value: "123", id: ElementId(1) },
CreateTextNode { value: "456", id: ElementId(2) },
CreateTextNode { value: "123".to_string(), id: ElementId(1) },
CreateTextNode { value: "456".to_string(), id: ElementId(2) },
AppendChildren { id: ElementId(0), m: 2 }
]
)
+9 -11
View File
@@ -1,19 +1,17 @@
use dioxus::core::{ElementId, Mutation::*};
use dioxus::dioxus_core::{ElementId, Mutation::*};
use dioxus::prelude::*;
/// As we clean up old templates, the ID for the node should cycle
#[test]
fn cycling_elements() {
let mut dom = VirtualDom::new(|cx| {
cx.render(match cx.generation() % 2 {
0 => rsx! { div { "wasd" } },
1 => rsx! { div { "abcd" } },
_ => unreachable!(),
})
let mut dom = VirtualDom::new(|| match generation() % 2 {
0 => rsx! { div { "wasd" } },
1 => rsx! { div { "abcd" } },
_ => unreachable!(),
});
{
let edits = dom.rebuild().santize();
let edits = dom.rebuild_to_vec().santize();
assert_eq!(
edits.edits,
[
@@ -25,7 +23,7 @@ fn cycling_elements() {
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
ReplaceWith { id: ElementId(1,), m: 1 },
@@ -35,7 +33,7 @@ fn cycling_elements() {
// notice that the IDs cycle back to ElementId(1), preserving a minimal memory footprint
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
ReplaceWith { id: ElementId(2,), m: 1 },
@@ -44,7 +42,7 @@ fn cycling_elements() {
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
ReplaceWith { id: ElementId(1,), m: 1 },
+20 -23
View File
@@ -1,4 +1,4 @@
use dioxus::core::{ElementId, Mutation::*};
use dioxus::dioxus_core::{ElementId, Mutation::*};
use dioxus::prelude::*;
/// When returning sets of components, we do a light diff of the contents to preserve some react-like functionality
@@ -7,12 +7,12 @@ use dioxus::prelude::*;
/// different pointers
#[test]
fn component_swap() {
fn app(cx: Scope) -> Element {
let render_phase = cx.use_hook(|| 0);
fn app() -> Element {
let mut render_phase = use_signal(|| 0);
*render_phase += 1;
render_phase += 1;
cx.render(match *render_phase {
match render_phase() {
0 => rsx! {
nav_bar {}
dash_board {}
@@ -34,33 +34,30 @@ fn component_swap() {
dash_board {}
},
_ => rsx!("blah"),
})
}
}
fn nav_bar(cx: Scope) -> Element {
cx.render(rsx! {
h1 {
"NavBar"
{(0..3).map(|_| rsx!(nav_link {}))}
}
})
fn nav_bar() -> Element {
rsx! {
h1 { "NavBar", {(0..3).map(|_| rsx!(nav_link {}))} }
}
}
fn nav_link(cx: Scope) -> Element {
cx.render(rsx!( h1 { "nav_link" } ))
fn nav_link() -> Element {
rsx!( h1 { "nav_link" } )
}
fn dash_board(cx: Scope) -> Element {
cx.render(rsx!( div { "dashboard" } ))
fn dash_board() -> Element {
rsx!( div { "dashboard" } )
}
fn dash_results(cx: Scope) -> Element {
cx.render(rsx!( div { "results" } ))
fn dash_results() -> Element {
rsx!( div { "results" } )
}
let mut dom = VirtualDom::new(app);
{
let edits = dom.rebuild().santize();
let edits = dom.rebuild_to_vec().santize();
assert_eq!(
edits.edits,
[
@@ -77,7 +74,7 @@ fn component_swap() {
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
ReplaceWith { id: ElementId(5), m: 1 }
@@ -86,7 +83,7 @@ fn component_swap() {
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
ReplaceWith { id: ElementId(6), m: 1 }
@@ -95,7 +92,7 @@ fn component_swap() {
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
ReplaceWith { id: ElementId(5), m: 1 }
+55 -75
View File
@@ -1,54 +1,54 @@
use dioxus::core::Mutation::*;
use dioxus::dioxus_core::Mutation::*;
use dioxus::dioxus_core::{AttributeValue, ElementId, NoOpMutations};
use dioxus::prelude::*;
use dioxus_core::{AttributeValue, ElementId};
#[test]
fn text_diff() {
fn app(cx: Scope) -> Element {
let gen = cx.generation();
cx.render(rsx!( h1 { "hello {gen}" } ))
fn app() -> Element {
let gen = generation();
rsx!( h1 { "hello {gen}" } )
}
let mut vdom = VirtualDom::new(app);
_ = vdom.rebuild();
vdom.rebuild(&mut NoOpMutations);
vdom.mark_dirty(ScopeId::ROOT);
assert_eq!(
vdom.render_immediate().edits,
[SetText { value: "hello 1", id: ElementId(2) }]
vdom.render_immediate_to_vec().edits,
[SetText { value: "hello 1".to_string(), id: ElementId(2) }]
);
vdom.mark_dirty(ScopeId::ROOT);
assert_eq!(
vdom.render_immediate().edits,
[SetText { value: "hello 2", id: ElementId(2) }]
vdom.render_immediate_to_vec().edits,
[SetText { value: "hello 2".to_string(), id: ElementId(2) }]
);
vdom.mark_dirty(ScopeId::ROOT);
assert_eq!(
vdom.render_immediate().edits,
[SetText { value: "hello 3", id: ElementId(2) }]
vdom.render_immediate_to_vec().edits,
[SetText { value: "hello 3".to_string(), id: ElementId(2) }]
);
}
#[test]
fn element_swap() {
fn app(cx: Scope) -> Element {
let gen = cx.generation();
fn app() -> Element {
let gen = generation();
match gen % 2 {
0 => cx.render(rsx!( h1 { "hello 1" } )),
1 => cx.render(rsx!( h2 { "hello 2" } )),
0 => rsx!( h1 { "hello 1" } ),
1 => rsx!( h2 { "hello 2" } ),
_ => unreachable!(),
}
}
let mut vdom = VirtualDom::new(app);
_ = vdom.rebuild();
vdom.rebuild(&mut NoOpMutations);
vdom.mark_dirty(ScopeId::ROOT);
assert_eq!(
vdom.render_immediate().santize().edits,
vdom.render_immediate_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
ReplaceWith { id: ElementId(1,), m: 1 },
@@ -57,7 +57,7 @@ fn element_swap() {
vdom.mark_dirty(ScopeId::ROOT);
assert_eq!(
vdom.render_immediate().santize().edits,
vdom.render_immediate_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
ReplaceWith { id: ElementId(2,), m: 1 },
@@ -66,7 +66,7 @@ fn element_swap() {
vdom.mark_dirty(ScopeId::ROOT);
assert_eq!(
vdom.render_immediate().santize().edits,
vdom.render_immediate_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
ReplaceWith { id: ElementId(1,), m: 1 },
@@ -75,7 +75,7 @@ fn element_swap() {
vdom.mark_dirty(ScopeId::ROOT);
assert_eq!(
vdom.render_immediate().santize().edits,
vdom.render_immediate_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
ReplaceWith { id: ElementId(2,), m: 1 },
@@ -85,60 +85,60 @@ fn element_swap() {
#[test]
fn attribute_diff() {
fn app(cx: Scope) -> Element {
let gen = cx.generation();
fn app() -> Element {
let gen = generation();
// attributes have to be sorted by name
let attrs = match gen % 5 {
0 => cx.bump().alloc([Attribute::new(
0 => vec![Attribute::new(
"a",
AttributeValue::Text("hello"),
AttributeValue::Text("hello".into()),
None,
false,
)]) as &[Attribute],
1 => cx.bump().alloc([
Attribute::new("a", AttributeValue::Text("hello"), None, false),
Attribute::new("b", AttributeValue::Text("hello"), None, false),
Attribute::new("c", AttributeValue::Text("hello"), None, false),
]) as &[Attribute],
2 => cx.bump().alloc([
Attribute::new("c", AttributeValue::Text("hello"), None, false),
Attribute::new("d", AttributeValue::Text("hello"), None, false),
Attribute::new("e", AttributeValue::Text("hello"), None, false),
]) as &[Attribute],
3 => cx.bump().alloc([Attribute::new(
)],
1 => vec![
Attribute::new("a", AttributeValue::Text("hello".into()), None, false),
Attribute::new("b", AttributeValue::Text("hello".into()), None, false),
Attribute::new("c", AttributeValue::Text("hello".into()), None, false),
],
2 => vec![
Attribute::new("c", AttributeValue::Text("hello".into()), None, false),
Attribute::new("d", AttributeValue::Text("hello".into()), None, false),
Attribute::new("e", AttributeValue::Text("hello".into()), None, false),
],
3 => vec![Attribute::new(
"d",
AttributeValue::Text("world"),
AttributeValue::Text("world".into()),
None,
false,
)]) as &[Attribute],
)],
_ => unreachable!(),
};
cx.render(rsx!(
rsx!(
div {
..*attrs,
..attrs,
"hello"
}
))
)
}
let mut vdom = VirtualDom::new(app);
_ = vdom.rebuild();
vdom.rebuild(&mut NoOpMutations);
vdom.mark_dirty(ScopeId::ROOT);
assert_eq!(
vdom.render_immediate().santize().edits,
vdom.render_immediate_to_vec().santize().edits,
[
SetAttribute {
name: "b",
value: (&AttributeValue::Text("hello",)).into(),
value: (AttributeValue::Text("hello".into())),
id: ElementId(1,),
ns: None,
},
SetAttribute {
name: "c",
value: (&AttributeValue::Text("hello",)).into(),
value: (AttributeValue::Text("hello".into())),
id: ElementId(1,),
ns: None,
},
@@ -147,29 +147,19 @@ fn attribute_diff() {
vdom.mark_dirty(ScopeId::ROOT);
assert_eq!(
vdom.render_immediate().santize().edits,
vdom.render_immediate_to_vec().santize().edits,
[
SetAttribute {
name: "a",
value: (&AttributeValue::None).into(),
id: ElementId(1,),
ns: None,
},
SetAttribute {
name: "b",
value: (&AttributeValue::None).into(),
id: ElementId(1,),
ns: None,
},
SetAttribute { name: "a", value: AttributeValue::None, id: ElementId(1,), ns: None },
SetAttribute { name: "b", value: AttributeValue::None, id: ElementId(1,), ns: None },
SetAttribute {
name: "d",
value: (&AttributeValue::Text("hello",)).into(),
value: AttributeValue::Text("hello".into()),
id: ElementId(1,),
ns: None,
},
SetAttribute {
name: "e",
value: (&AttributeValue::Text("hello",)).into(),
value: AttributeValue::Text("hello".into()),
id: ElementId(1,),
ns: None,
},
@@ -178,26 +168,16 @@ fn attribute_diff() {
vdom.mark_dirty(ScopeId::ROOT);
assert_eq!(
vdom.render_immediate().santize().edits,
vdom.render_immediate_to_vec().santize().edits,
[
SetAttribute {
name: "c",
value: (&AttributeValue::None).into(),
id: ElementId(1,),
ns: None,
},
SetAttribute { name: "c", value: AttributeValue::None, id: ElementId(1,), ns: None },
SetAttribute {
name: "d",
value: (&AttributeValue::Text("world",)).into(),
id: ElementId(1,),
ns: None,
},
SetAttribute {
name: "e",
value: (&AttributeValue::None).into(),
value: AttributeValue::Text("world".into()),
id: ElementId(1,),
ns: None,
},
SetAttribute { name: "e", value: AttributeValue::None, id: ElementId(1,), ns: None },
]
);
}
+64 -64
View File
@@ -4,25 +4,25 @@
//!
//! It does not validated that component lifecycles work properly. This is done in another test file.
use dioxus::core::{ElementId, Mutation::*};
use dioxus::dioxus_core::{ElementId, Mutation::*};
use dioxus::prelude::*;
/// Should result in moves, but not removals or additions
#[test]
fn keyed_diffing_out_of_order() {
let mut dom = VirtualDom::new(|cx| {
let order = match cx.generation() % 2 {
let mut dom = VirtualDom::new(|| {
let order = match generation() % 2 {
0 => &[0, 1, 2, 3, /**/ 4, 5, 6, /**/ 7, 8, 9],
1 => &[0, 1, 2, 3, /**/ 6, 4, 5, /**/ 7, 8, 9],
_ => unreachable!(),
};
cx.render(rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) }))
rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) })
});
{
assert_eq!(
dom.rebuild().santize().edits,
dom.rebuild_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
@@ -41,7 +41,7 @@ fn keyed_diffing_out_of_order() {
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().edits,
dom.render_immediate_to_vec().edits,
[
PushRoot { id: ElementId(7,) },
InsertBefore { id: ElementId(5,), m: 1 },
@@ -52,21 +52,21 @@ fn keyed_diffing_out_of_order() {
/// Should result in moves only
#[test]
fn keyed_diffing_out_of_order_adds() {
let mut dom = VirtualDom::new(|cx| {
let order = match cx.generation() % 2 {
let mut dom = VirtualDom::new(|| {
let order = match generation() % 2 {
0 => &[/**/ 4, 5, 6, 7, 8 /**/],
1 => &[/**/ 8, 7, 4, 5, 6 /**/],
_ => unreachable!(),
};
cx.render(rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) }))
rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) })
});
_ = dom.rebuild();
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().edits,
dom.render_immediate_to_vec().edits,
[
PushRoot { id: ElementId(5,) },
PushRoot { id: ElementId(4,) },
@@ -78,21 +78,21 @@ fn keyed_diffing_out_of_order_adds() {
/// Should result in moves only
#[test]
fn keyed_diffing_out_of_order_adds_3() {
let mut dom = VirtualDom::new(|cx| {
let order = match cx.generation() % 2 {
let mut dom = VirtualDom::new(|| {
let order = match generation() % 2 {
0 => &[/**/ 4, 5, 6, 7, 8 /**/],
1 => &[/**/ 4, 8, 7, 5, 6 /**/],
_ => unreachable!(),
};
cx.render(rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) }))
rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) })
});
_ = dom.rebuild();
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().edits,
dom.render_immediate_to_vec().edits,
[
PushRoot { id: ElementId(5,) },
PushRoot { id: ElementId(4,) },
@@ -104,21 +104,21 @@ fn keyed_diffing_out_of_order_adds_3() {
/// Should result in moves onl
#[test]
fn keyed_diffing_out_of_order_adds_4() {
let mut dom = VirtualDom::new(|cx| {
let order = match cx.generation() % 2 {
let mut dom = VirtualDom::new(|| {
let order = match generation() % 2 {
0 => &[/**/ 4, 5, 6, 7, 8 /**/],
1 => &[/**/ 4, 5, 8, 7, 6 /**/],
_ => unreachable!(),
};
cx.render(rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) }))
rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) })
});
_ = dom.rebuild();
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().edits,
dom.render_immediate_to_vec().edits,
[
PushRoot { id: ElementId(5,) },
PushRoot { id: ElementId(4,) },
@@ -130,21 +130,21 @@ fn keyed_diffing_out_of_order_adds_4() {
/// Should result in moves onl
#[test]
fn keyed_diffing_out_of_order_adds_5() {
let mut dom = VirtualDom::new(|cx| {
let order = match cx.generation() % 2 {
let mut dom = VirtualDom::new(|| {
let order = match generation() % 2 {
0 => &[/**/ 4, 5, 6, 7, 8 /**/],
1 => &[/**/ 4, 5, 6, 8, 7 /**/],
_ => unreachable!(),
};
cx.render(rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) }))
rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) })
});
_ = dom.rebuild();
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().edits,
dom.render_immediate_to_vec().edits,
[
PushRoot { id: ElementId(5,) },
InsertBefore { id: ElementId(4,), m: 1 },
@@ -155,21 +155,21 @@ fn keyed_diffing_out_of_order_adds_5() {
/// Should result in moves onl
#[test]
fn keyed_diffing_additions() {
let mut dom = VirtualDom::new(|cx| {
let order: &[_] = match cx.generation() % 2 {
let mut dom = VirtualDom::new(|| {
let order: &[_] = match generation() % 2 {
0 => &[/**/ 4, 5, 6, 7, 8 /**/],
1 => &[/**/ 4, 5, 6, 7, 8, 9, 10 /**/],
_ => unreachable!(),
};
cx.render(rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) }))
rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) })
});
_ = dom.rebuild();
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
LoadTemplate { name: "template", index: 0, id: ElementId(7) },
@@ -180,21 +180,21 @@ fn keyed_diffing_additions() {
#[test]
fn keyed_diffing_additions_and_moves_on_ends() {
let mut dom = VirtualDom::new(|cx| {
let order: &[_] = match cx.generation() % 2 {
let mut dom = VirtualDom::new(|| {
let order: &[_] = match generation() % 2 {
0 => &[/**/ 4, 5, 6, 7 /**/],
1 => &[/**/ 7, 4, 5, 6, 11, 12 /**/],
_ => unreachable!(),
};
cx.render(rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) }))
rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) })
});
_ = dom.rebuild();
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[
// create 11, 12
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
@@ -209,22 +209,22 @@ fn keyed_diffing_additions_and_moves_on_ends() {
#[test]
fn keyed_diffing_additions_and_moves_in_middle() {
let mut dom = VirtualDom::new(|cx| {
let order: &[_] = match cx.generation() % 2 {
let mut dom = VirtualDom::new(|| {
let order: &[_] = match generation() % 2 {
0 => &[/**/ 1, 2, 3, 4 /**/],
1 => &[/**/ 4, 1, 7, 8, 2, 5, 6, 3 /**/],
_ => unreachable!(),
};
cx.render(rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) }))
rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) })
});
_ = dom.rebuild();
dom.rebuild(&mut dioxus_core::NoOpMutations);
// LIS: 4, 5, 6
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[
// create 5, 6
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
@@ -243,22 +243,22 @@ fn keyed_diffing_additions_and_moves_in_middle() {
#[test]
fn controlled_keyed_diffing_out_of_order() {
let mut dom = VirtualDom::new(|cx| {
let order: &[_] = match cx.generation() % 2 {
let mut dom = VirtualDom::new(|| {
let order: &[_] = match generation() % 2 {
0 => &[4, 5, 6, 7],
1 => &[0, 5, 9, 6, 4],
_ => unreachable!(),
};
cx.render(rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) }))
rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) })
});
_ = dom.rebuild();
dom.rebuild(&mut dioxus_core::NoOpMutations);
// LIS: 5, 6
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[
// remove 7
Remove { id: ElementId(4,) },
@@ -277,21 +277,21 @@ fn controlled_keyed_diffing_out_of_order() {
#[test]
fn controlled_keyed_diffing_out_of_order_max_test() {
let mut dom = VirtualDom::new(|cx| {
let order: &[_] = match cx.generation() % 2 {
let mut dom = VirtualDom::new(|| {
let order: &[_] = match generation() % 2 {
0 => &[0, 1, 2, 3, 4],
1 => &[3, 0, 1, 10, 2],
_ => unreachable!(),
};
cx.render(rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) }))
rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) })
});
_ = dom.rebuild();
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[
Remove { id: ElementId(5,) },
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
@@ -306,21 +306,21 @@ fn controlled_keyed_diffing_out_of_order_max_test() {
// just making sure it doesnt happen in the core implementation
#[test]
fn remove_list() {
let mut dom = VirtualDom::new(|cx| {
let order: &[_] = match cx.generation() % 2 {
let mut dom = VirtualDom::new(|| {
let order: &[_] = match generation() % 2 {
0 => &[9, 8, 7, 6, 5],
1 => &[9, 8],
_ => unreachable!(),
};
cx.render(rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) }))
rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) })
});
_ = dom.rebuild();
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[
Remove { id: ElementId(5) },
Remove { id: ElementId(4) },
@@ -331,27 +331,27 @@ fn remove_list() {
#[test]
fn no_common_keys() {
let mut dom = VirtualDom::new(|cx| {
let order: &[_] = match cx.generation() % 2 {
let mut dom = VirtualDom::new(|| {
let order: &[_] = match generation() % 2 {
0 => &[1, 2, 3],
1 => &[4, 5, 6],
_ => unreachable!(),
};
cx.render(rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) }))
rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) })
});
_ = dom.rebuild();
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(4) },
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
Remove { id: ElementId(3) },
Remove { id: ElementId(2) },
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
LoadTemplate { name: "template", index: 0, id: ElementId(3) },
LoadTemplate { name: "template", index: 0, id: ElementId(4) },
ReplaceWith { id: ElementId(1), m: 3 }
]
);
+82 -82
View File
@@ -1,24 +1,24 @@
use dioxus::core::{ElementId, Mutation::*};
use dioxus::dioxus_core::{ElementId, Mutation::*};
use dioxus::prelude::*;
use pretty_assertions::assert_eq;
#[test]
fn list_creates_one_by_one() {
let mut dom = VirtualDom::new(|cx| {
let gen = cx.generation();
let mut dom = VirtualDom::new(|| {
let gen = generation();
cx.render(rsx! {
rsx! {
div {
for i in 0..gen {
div { "{i}" }
}
}
})
}
});
// load the div and then assign the empty fragment as a placeholder
assert_eq!(
dom.rebuild().santize().edits,
dom.rebuild_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
AssignId { path: &[0], id: ElementId(2,) },
@@ -29,10 +29,10 @@ fn list_creates_one_by_one() {
// Rendering the first item should replace the placeholder with an element
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(3,) },
HydrateText { path: &[0], value: "0", id: ElementId(4,) },
HydrateText { path: &[0], value: "0".to_string(), id: ElementId(4,) },
ReplaceWith { id: ElementId(2,), m: 1 },
]
);
@@ -40,10 +40,10 @@ fn list_creates_one_by_one() {
// Rendering the next item should insert after the previous
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
HydrateText { path: &[0], value: "1", id: ElementId(5,) },
HydrateText { path: &[0], value: "1".to_string(), id: ElementId(5,) },
InsertAfter { id: ElementId(3,), m: 1 },
]
);
@@ -51,10 +51,10 @@ fn list_creates_one_by_one() {
// ... and again!
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(6,) },
HydrateText { path: &[0], value: "2", id: ElementId(7,) },
HydrateText { path: &[0], value: "2".to_string(), id: ElementId(7,) },
InsertAfter { id: ElementId(2,), m: 1 },
]
);
@@ -62,10 +62,10 @@ fn list_creates_one_by_one() {
// once more
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(8,) },
HydrateText { path: &[0], value: "3", id: ElementId(9,) },
HydrateText { path: &[0], value: "3".to_string(), id: ElementId(9,) },
InsertAfter { id: ElementId(6,), m: 1 },
]
);
@@ -73,31 +73,31 @@ fn list_creates_one_by_one() {
#[test]
fn removes_one_by_one() {
let mut dom = VirtualDom::new(|cx| {
let gen = 3 - cx.generation() % 4;
let mut dom = VirtualDom::new(|| {
let gen = 3 - generation() % 4;
cx.render(rsx! {
rsx! {
div {
for i in 0..gen {
div { "{i}" }
}
}
})
}
});
// load the div and then assign the empty fragment as a placeholder
assert_eq!(
dom.rebuild().santize().edits,
dom.rebuild_to_vec().santize().edits,
[
// The container
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
// each list item
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
HydrateText { path: &[0], value: "0", id: ElementId(3) },
HydrateText { path: &[0], value: "0".to_string(), id: ElementId(3) },
LoadTemplate { name: "template", index: 0, id: ElementId(4) },
HydrateText { path: &[0], value: "1", id: ElementId(5) },
HydrateText { path: &[0], value: "1".to_string(), id: ElementId(5) },
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
HydrateText { path: &[0], value: "2", id: ElementId(7) },
HydrateText { path: &[0], value: "2".to_string(), id: ElementId(7) },
// replace the placeholder in the template with the 3 templates on the stack
ReplacePlaceholder { m: 3, path: &[0] },
// Mount the div
@@ -109,14 +109,14 @@ fn removes_one_by_one() {
// Rendering the first item should replace the placeholder with an element
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[Remove { id: ElementId(6) }]
);
// Remove div(2)
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[Remove { id: ElementId(4) }]
);
@@ -124,7 +124,7 @@ fn removes_one_by_one() {
// todo: this should just be a remove with no placeholder
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[
CreatePlaceholder { id: ElementId(4) },
ReplaceWith { id: ElementId(2), m: 1 }
@@ -135,14 +135,14 @@ fn removes_one_by_one() {
// todo: this should actually be append to, but replace placeholder is fine for now
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
HydrateText { path: &[0], value: "0", id: ElementId(3) },
HydrateText { path: &[0], value: "0".to_string(), id: ElementId(3) },
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
HydrateText { path: &[0], value: "1", id: ElementId(6) },
HydrateText { path: &[0], value: "1".to_string(), id: ElementId(6) },
LoadTemplate { name: "template", index: 0, id: ElementId(7) },
HydrateText { path: &[0], value: "2", id: ElementId(8) },
HydrateText { path: &[0], value: "2".to_string(), id: ElementId(8) },
ReplaceWith { id: ElementId(4), m: 3 }
]
);
@@ -150,19 +150,19 @@ fn removes_one_by_one() {
#[test]
fn list_shrink_multiroot() {
let mut dom = VirtualDom::new(|cx| {
cx.render(rsx! {
let mut dom = VirtualDom::new(|| {
rsx! {
div {
for i in 0..cx.generation() {
for i in 0..generation() {
div { "{i}" }
div { "{i}" }
}
}
})
}
});
assert_eq!(
dom.rebuild().santize().edits,
dom.rebuild_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
AssignId { path: &[0,], id: ElementId(2,) },
@@ -172,36 +172,36 @@ fn list_shrink_multiroot() {
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(3) },
HydrateText { path: &[0], value: "0", id: ElementId(4) },
HydrateText { path: &[0], value: "0".to_string(), id: ElementId(4) },
LoadTemplate { name: "template", index: 1, id: ElementId(5) },
HydrateText { path: &[0], value: "0", id: ElementId(6) },
HydrateText { path: &[0], value: "0".to_string(), id: ElementId(6) },
ReplaceWith { id: ElementId(2), m: 2 }
]
);
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
HydrateText { path: &[0], value: "1", id: ElementId(7) },
HydrateText { path: &[0], value: "1".to_string(), id: ElementId(7) },
LoadTemplate { name: "template", index: 1, id: ElementId(8) },
HydrateText { path: &[0], value: "1", id: ElementId(9) },
HydrateText { path: &[0], value: "1".to_string(), id: ElementId(9) },
InsertAfter { id: ElementId(5), m: 2 }
]
);
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(10) },
HydrateText { path: &[0], value: "2", id: ElementId(11) },
HydrateText { path: &[0], value: "2".to_string(), id: ElementId(11) },
LoadTemplate { name: "template", index: 1, id: ElementId(12) },
HydrateText { path: &[0], value: "2", id: ElementId(13) },
HydrateText { path: &[0], value: "2".to_string(), id: ElementId(13) },
InsertAfter { id: ElementId(8), m: 2 }
]
);
@@ -209,39 +209,39 @@ fn list_shrink_multiroot() {
#[test]
fn removes_one_by_one_multiroot() {
let mut dom = VirtualDom::new(|cx| {
let gen = 3 - cx.generation() % 4;
let mut dom = VirtualDom::new(|| {
let gen = 3 - generation() % 4;
cx.render(rsx! {
rsx! {
div {
{(0..gen).map(|i| rsx! {
div { "{i}" }
div { "{i}" }
})}
}
})
}
});
// load the div and then assign the empty fragment as a placeholder
assert_eq!(
dom.rebuild().santize().edits,
dom.rebuild_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
//
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
HydrateText { path: &[0], value: "0", id: ElementId(3) },
HydrateText { path: &[0], value: "0".to_string(), id: ElementId(3) },
LoadTemplate { name: "template", index: 1, id: ElementId(4) },
HydrateText { path: &[0], value: "0", id: ElementId(5) },
HydrateText { path: &[0], value: "0".to_string(), id: ElementId(5) },
//
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
HydrateText { path: &[0], value: "1", id: ElementId(7) },
HydrateText { path: &[0], value: "1".to_string(), id: ElementId(7) },
LoadTemplate { name: "template", index: 1, id: ElementId(8) },
HydrateText { path: &[0], value: "1", id: ElementId(9) },
HydrateText { path: &[0], value: "1".to_string(), id: ElementId(9) },
//
LoadTemplate { name: "template", index: 0, id: ElementId(10) },
HydrateText { path: &[0], value: "2", id: ElementId(11) },
HydrateText { path: &[0], value: "2".to_string(), id: ElementId(11) },
LoadTemplate { name: "template", index: 1, id: ElementId(12) },
HydrateText { path: &[0], value: "2", id: ElementId(13) },
HydrateText { path: &[0], value: "2".to_string(), id: ElementId(13) },
//
ReplacePlaceholder { path: &[0], m: 6 },
//
@@ -251,19 +251,19 @@ fn removes_one_by_one_multiroot() {
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[Remove { id: ElementId(10) }, Remove { id: ElementId(12) }]
);
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[Remove { id: ElementId(6) }, Remove { id: ElementId(8) }]
);
dom.mark_dirty(ScopeId::ROOT);
assert_eq!(
dom.render_immediate().santize().edits,
dom.render_immediate_to_vec().santize().edits,
[
CreatePlaceholder { id: ElementId(8) },
Remove { id: ElementId(2) },
@@ -274,51 +274,51 @@ fn removes_one_by_one_multiroot() {
#[test]
fn two_equal_fragments_are_equal_static() {
let mut dom = VirtualDom::new(|cx| {
cx.render(rsx! {
let mut dom = VirtualDom::new(|| {
rsx! {
for _ in 0..5 {
div { "hello" }
}
})
}
});
_ = dom.rebuild();
assert!(dom.render_immediate().edits.is_empty());
dom.rebuild(&mut dioxus_core::NoOpMutations);
assert!(dom.render_immediate_to_vec().edits.is_empty());
}
#[test]
fn two_equal_fragments_are_equal() {
let mut dom = VirtualDom::new(|cx| {
cx.render(rsx! {
let mut dom = VirtualDom::new(|| {
rsx! {
for i in 0..5 {
div { "hello {i}" }
}
})
}
});
_ = dom.rebuild();
assert!(dom.render_immediate().edits.is_empty());
dom.rebuild(&mut dioxus_core::NoOpMutations);
assert!(dom.render_immediate_to_vec().edits.is_empty());
}
#[test]
fn remove_many() {
let mut dom = VirtualDom::new(|cx| {
let num = match cx.generation() % 3 {
let mut dom = VirtualDom::new(|| {
let num = match generation() % 3 {
0 => 0,
1 => 1,
2 => 5,
_ => unreachable!(),
};
cx.render(rsx! {
rsx! {
for i in 0..num {
div { "hello {i}" }
}
})
}
});
{
let edits = dom.rebuild().santize();
let edits = dom.rebuild_to_vec().santize();
assert!(edits.templates.is_empty());
assert_eq!(
edits.edits,
@@ -331,12 +331,12 @@ fn remove_many() {
{
dom.mark_dirty(ScopeId::ROOT);
let edits = dom.render_immediate().santize();
let edits = dom.render_immediate_to_vec().santize();
assert_eq!(
edits.edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
HydrateText { path: &[0,], value: "hello 0", id: ElementId(3,) },
HydrateText { path: &[0,], value: "hello 0".to_string(), id: ElementId(3,) },
ReplaceWith { id: ElementId(1,), m: 1 },
]
);
@@ -344,18 +344,18 @@ fn remove_many() {
{
dom.mark_dirty(ScopeId::ROOT);
let edits = dom.render_immediate().santize();
let edits = dom.render_immediate_to_vec().santize();
assert_eq!(
edits.edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
HydrateText { path: &[0,], value: "hello 1", id: ElementId(4,) },
HydrateText { path: &[0,], value: "hello 1".to_string(), id: ElementId(4,) },
LoadTemplate { name: "template", index: 0, id: ElementId(5,) },
HydrateText { path: &[0,], value: "hello 2", id: ElementId(6,) },
HydrateText { path: &[0,], value: "hello 2".to_string(), id: ElementId(6,) },
LoadTemplate { name: "template", index: 0, id: ElementId(7,) },
HydrateText { path: &[0,], value: "hello 3", id: ElementId(8,) },
HydrateText { path: &[0,], value: "hello 3".to_string(), id: ElementId(8,) },
LoadTemplate { name: "template", index: 0, id: ElementId(9,) },
HydrateText { path: &[0,], value: "hello 4", id: ElementId(10,) },
HydrateText { path: &[0,], value: "hello 4".to_string(), id: ElementId(10,) },
InsertAfter { id: ElementId(2,), m: 4 },
]
);
@@ -363,7 +363,7 @@ fn remove_many() {
{
dom.mark_dirty(ScopeId::ROOT);
let edits = dom.render_immediate().santize();
let edits = dom.render_immediate_to_vec().santize();
assert_eq!(
edits.edits,
[
@@ -379,12 +379,12 @@ fn remove_many() {
{
dom.mark_dirty(ScopeId::ROOT);
let edits = dom.render_immediate().santize();
let edits = dom.render_immediate_to_vec().santize();
assert_eq!(
edits.edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
HydrateText { path: &[0,], value: "hello 0", id: ElementId(3,) },
HydrateText { path: &[0,], value: "hello 0".to_string(), id: ElementId(3,) },
ReplaceWith { id: ElementId(11,), m: 1 },
]
)
+7 -9
View File
@@ -5,30 +5,28 @@ use dioxus::prelude::*;
#[test]
fn catches_panic() {
let mut dom = VirtualDom::new(app);
_ = dom.rebuild();
dom.rebuild(&mut dioxus_core::NoOpMutations);
}
fn app(cx: Scope) -> Element {
cx.render(rsx! {
fn app() -> Element {
rsx! {
div {
h1 { "Title" }
NoneChild {}
ThrowChild {}
}
})
}
}
fn NoneChild(_cx: Scope) -> Element {
fn NoneChild() -> Element {
None
}
fn ThrowChild(cx: Scope) -> Element {
fn ThrowChild() -> Element {
Err(std::io::Error::new(std::io::ErrorKind::AddrInUse, "asd")).throw()?;
let _g: i32 = "123123".parse().throw()?;
cx.render(rsx! {
div {}
})
rsx! { div {} }
}
Loaded 100 of 527 files, more files were not shown because too many files have changed in this diff. Show more