Merge branch 'master' into pr/atty303/1349

This commit is contained in:
Evan Almloff committed 2024-01-06 08:52:37 -06:00
commit e5df83ceec
278 files changed
+11299 -3674

No files matched your search

+2
View File
@@ -36,6 +36,8 @@ jobs:
toolchain: ${{ matrix.platform.toolchain }} toolchain: ${{ matrix.platform.toolchain }}
targets: ${{ matrix.platform.target }} targets: ${{ matrix.platform.target }}
- uses: ilammy/setup-nasm@v1
# Setup the Github Actions Cache for the CLI package # Setup the Github Actions Cache for the CLI package
- name: Setup cache - name: Setup cache
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
# cd fermi && mdbook build -d ../nightly/fermi && cd .. # cd fermi && mdbook build -d ../nightly/fermi && cd ..
- name: Deploy 🚀 - name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@v4.4.3 uses: JamesIves/github-pages-deploy-action@v4.5.0
with: with:
branch: gh-pages # The branch the action should deploy to. branch: gh-pages # The branch the action should deploy to.
folder: docs/nightly # The folder the action should deploy. folder: docs/nightly # The folder the action should deploy.
+1 -1
View File
@@ -39,7 +39,7 @@ jobs:
# cd fermi && mdbook build -d ../nightly/fermi && cd .. # cd fermi && mdbook build -d ../nightly/fermi && cd ..
- name: Deploy 🚀 - name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@v4.4.3 uses: JamesIves/github-pages-deploy-action@v4.5.0
with: with:
branch: gh-pages # The branch the action should deploy to. branch: gh-pages # The branch the action should deploy to.
folder: docs/nightly # The folder the action should deploy. folder: docs/nightly # The folder the action should deploy.
+10 -1
View File
@@ -39,6 +39,7 @@ jobs:
steps: steps:
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- uses: ilammy/setup-nasm@v1
- run: sudo apt-get update - run: sudo apt-get update
- run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -51,6 +52,7 @@ jobs:
steps: steps:
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- uses: ilammy/setup-nasm@v1
- run: sudo apt-get update - run: sudo apt-get update
- run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev
- uses: davidB/rust-cargo-make@v1 - uses: davidB/rust-cargo-make@v1
@@ -66,6 +68,7 @@ jobs:
steps: steps:
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- uses: ilammy/setup-nasm@v1
- run: rustup component add rustfmt - run: rustup component add rustfmt
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- run: cargo fmt --all -- --check - run: cargo fmt --all -- --check
@@ -77,6 +80,7 @@ jobs:
steps: steps:
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- uses: ilammy/setup-nasm@v1
- run: sudo apt-get update - run: sudo apt-get update
- run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev
- run: rustup component add clippy - run: rustup component add clippy
@@ -125,7 +129,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: ilammy/setup-nasm@v1
- name: install stable - name: install stable
uses: dtolnay/rust-toolchain@master uses: dtolnay/rust-toolchain@master
with: with:
@@ -141,6 +145,11 @@ jobs:
workspaces: core -> ../target workspaces: core -> ../target
save-if: ${{ matrix.features.key == 'all' }} save-if: ${{ matrix.features.key == 'all' }}
- name: Install rustfmt
run: rustup component add rustfmt
- uses: actions/checkout@v4
- name: test - name: test
run: | run: |
${{ env.RUST_CARGO_COMMAND }} ${{ matrix.platform.command }} ${{ matrix.platform.args }} --target ${{ matrix.platform.target }} ${{ env.RUST_CARGO_COMMAND }} ${{ matrix.platform.command }} ${{ matrix.platform.args }} --target ${{ matrix.platform.target }}
+4 -4
View File
@@ -26,8 +26,8 @@ env:
RUST_BACKTRACE: 1 RUST_BACKTRACE: 1
# Change to specific Rust release to pin # Change to specific Rust release to pin
rust_stable: stable rust_stable: stable
rust_nightly: nightly-2022-11-03 rust_nightly: nightly-2023-11-16
rust_clippy: 1.65.0 rust_clippy: 1.70.0
# When updating this, also update: # When updating this, also update:
# - README.md # - README.md
# - tokio/README.md # - tokio/README.md
@@ -70,6 +70,7 @@ jobs:
run: echo "MIRIFLAGS=-Zmiri-tag-gc=1" >> $GITHUB_ENV run: echo "MIRIFLAGS=-Zmiri-tag-gc=1" >> $GITHUB_ENV
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: ilammy/setup-nasm@v1
- name: Install Rust ${{ env.rust_nightly }} - name: Install Rust ${{ env.rust_nightly }}
uses: dtolnay/rust-toolchain@master uses: dtolnay/rust-toolchain@master
with: with:
@@ -86,8 +87,7 @@ jobs:
# working-directory: tokio # working-directory: tokio
env: env:
# todo: disable memory leaks ignore MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance -Zmiri-retag-fields
MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance -Zmiri-retag-fields -Zmiri-ignore-leaks
PROPTEST_CASES: 10 PROPTEST_CASES: 10
# Cache the global cargo directory, but NOT the local `target` directory which # Cache the global cargo directory, but NOT the local `target` directory which
+2 -1
View File
@@ -20,6 +20,7 @@ jobs:
steps: steps:
# Do our best to cache the toolchain and node install steps # Do our best to cache the toolchain and node install steps
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: ilammy/setup-nasm@v1
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 16 node-version: 16
@@ -43,7 +44,7 @@ jobs:
# args: --path packages/cli # args: --path packages/cli
- name: Run Playwright tests - name: Run Playwright tests
run: npx playwright test run: npx playwright test
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v4
if: always() if: always()
with: with:
name: playwright-report name: playwright-report
+1
View File
@@ -4,6 +4,7 @@
/dist /dist
Cargo.lock Cargo.lock
.DS_Store .DS_Store
/examples/assets/test_video.mp4
.vscode/* .vscode/*
!.vscode/settings.json !.vscode/settings.json
+9 -4
View File
@@ -51,7 +51,7 @@ members = [
exclude = ["examples/mobile_demo"] exclude = ["examples/mobile_demo"]
[workspace.package] [workspace.package]
version = "0.4.2" version = "0.4.3"
# dependencies that are shared across packages # dependencies that are shared across packages
[workspace.dependencies] [workspace.dependencies]
@@ -60,7 +60,7 @@ dioxus-core = { path = "packages/core", version = "0.4.2" }
dioxus-core-macro = { path = "packages/core-macro", version = "0.4.0" } dioxus-core-macro = { path = "packages/core-macro", version = "0.4.0" }
dioxus-router = { path = "packages/router", version = "0.4.1" } dioxus-router = { path = "packages/router", version = "0.4.1" }
dioxus-router-macro = { path = "packages/router-macro", version = "0.4.1" } dioxus-router-macro = { path = "packages/router-macro", version = "0.4.1" }
dioxus-html = { path = "packages/html", version = "0.4.0" } dioxus-html = { path = "packages/html", default-features = false, version = "0.4.0" }
dioxus-html-internal-macro = { path = "packages/html-internal-macro", version = "0.4.0" } dioxus-html-internal-macro = { path = "packages/html-internal-macro", version = "0.4.0" }
dioxus-hooks = { path = "packages/hooks", version = "0.4.0" } dioxus-hooks = { path = "packages/hooks", version = "0.4.0" }
dioxus-web = { path = "packages/web", version = "0.4.0" } dioxus-web = { path = "packages/web", version = "0.4.0" }
@@ -79,7 +79,7 @@ dioxus-native-core = { path = "packages/native-core", version = "0.4.0" }
dioxus-native-core-macro = { path = "packages/native-core-macro", version = "0.4.0" } dioxus-native-core-macro = { path = "packages/native-core-macro", version = "0.4.0" }
rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.4.0" } rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.4.0" }
dioxus-signals = { path = "packages/signals" } dioxus-signals = { path = "packages/signals" }
generational-box = { path = "packages/generational-box" } generational-box = { path = "packages/generational-box", version = "0.4.3" }
dioxus-hot-reload = { path = "packages/hot-reload", version = "0.4.0" } dioxus-hot-reload = { path = "packages/hot-reload", version = "0.4.0" }
dioxus-fullstack = { path = "packages/fullstack", version = "0.4.1" } dioxus-fullstack = { path = "packages/fullstack", version = "0.4.1" }
dioxus_server_macro = { path = "packages/server-macro", version = "0.4.1" } dioxus_server_macro = { path = "packages/server-macro", version = "0.4.1" }
@@ -101,7 +101,7 @@ prettyplease = { package = "prettier-please", version = "0.2", features = [
# It is not meant to be published, but is used so "cargo run --example XYZ" works properly # It is not meant to be published, but is used so "cargo run --example XYZ" works properly
[package] [package]
name = "dioxus-examples" name = "dioxus-examples"
version = "0.0.0" version = "0.4.3"
authors = ["Jonathan Kelley"] authors = ["Jonathan Kelley"]
edition = "2021" edition = "2021"
description = "Top level crate for the Dioxus repository" description = "Top level crate for the Dioxus repository"
@@ -135,3 +135,8 @@ fern = { version = "0.6.0", features = ["colored"] }
env_logger = "0.10.0" env_logger = "0.10.0"
simple_logger = "4.0.0" simple_logger = "4.0.0"
thiserror = { workspace = true } thiserror = { workspace = true }
[dependencies]
tracing-subscriber = "0.3.17"
http-range = "0.1.5"
+70 -4
View File
@@ -24,12 +24,64 @@ script = [
] ]
script_runner = "@duckscript" script_runner = "@duckscript"
[tasks.format]
command = "cargo"
args = ["fmt", "--all"]
[tasks.check]
command = "cargo"
args = ["check", "--workspace", "--examples", "--tests"]
[tasks.clippy]
command = "cargo"
args = [
"clippy",
"--workspace",
"--examples",
"--tests",
"--",
"-D",
"warnings",
]
[tasks.tidy]
category = "Formatting"
dependencies = ["format", "check", "clippy"]
description = "Format and Check workspace"
[tasks.install-miri]
toolchain = "nightly"
install_crate = { rustup_component_name = "miri", binary = "cargo +nightly miri", test_arg = "--help" }
private = true
[tasks.miri-native]
command = "cargo"
toolchain = "nightly"
dependencies = ["install-miri"]
args = [
"miri",
"test",
"--package",
"dioxus-native-core",
"--test",
"miri_native",
]
[tasks.miri-stress]
command = "cargo"
toolchain = "nightly"
dependencies = ["install-miri"]
args = ["miri", "test", "--package", "dioxus-core", "--test", "miri_stress"]
[tasks.miri]
dependencies = ["miri-native", "miri-stress"]
[tasks.tests] [tasks.tests]
category = "Testing" category = "Testing"
dependencies = ["tests-setup"] dependencies = ["tests-setup"]
description = "Run all tests" description = "Run all tests"
env = {CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = ["**/examples/*"]} env = { CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = ["**/examples/*"] }
run_task = {name = ["test-flow", "test-with-browser"], fork = true} run_task = { name = ["test-flow", "test-with-browser"], fork = true }
[tasks.build] [tasks.build]
command = "cargo" command = "cargo"
@@ -42,10 +94,24 @@ private = true
[tasks.test] [tasks.test]
dependencies = ["build"] dependencies = ["build"]
command = "cargo" command = "cargo"
args = ["test", "--lib", "--bins", "--tests", "--examples", "--workspace", "--exclude", "dioxus-router", "--exclude", "dioxus-desktop"] args = [
"test",
"--lib",
"--bins",
"--tests",
"--examples",
"--workspace",
"--exclude",
"dioxus-router",
"--exclude",
"dioxus-desktop",
]
private = true private = true
[tasks.test-with-browser] [tasks.test-with-browser]
env = { CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS = ["**/packages/router", "**/packages/desktop"] } env = { CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS = [
"**/packages/router",
"**/packages/desktop",
] }
private = true private = true
workspace = true workspace = true
+1 -1
View File
@@ -161,7 +161,7 @@ So... Dioxus is great, but why won't it work for me?
## Contributing ## Contributing
- Check out the website [section on contributing](https://dioxuslabs.com/learn/0.4/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). - Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
- Join the discord and ask questions! - [Join](https://discord.gg/XgGxMSkvUM) the discord and ask questions!
<a href="https://github.com/dioxuslabs/dioxus/graphs/contributors"> <a href="https://github.com/dioxuslabs/dioxus/graphs/contributors">
+4 -7
View File
@@ -53,8 +53,7 @@ fn app(cx: Scope) -> Element {
}; };
cx.render(rsx! ( cx.render(rsx! (
div { div { style: "{CONTAINER_STYLE}",
style: "{CONTAINER_STYLE}",
div { div {
style: "{RECT_STYLE}", style: "{RECT_STYLE}",
// focusing is necessary to catch keyboard events // focusing is necessary to catch keyboard events
@@ -62,7 +61,7 @@ fn app(cx: Scope) -> Element {
onmousemove: move |event| log_event(Event::MouseMove(event)), onmousemove: move |event| log_event(Event::MouseMove(event)),
onclick: move |event| log_event(Event::MouseClick(event)), onclick: move |event| log_event(Event::MouseClick(event)),
ondblclick: move |event| log_event(Event::MouseDoubleClick(event)), ondoubleclick: move |event| log_event(Event::MouseDoubleClick(event)),
onmousedown: move |event| log_event(Event::MouseDown(event)), onmousedown: move |event| log_event(Event::MouseDown(event)),
onmouseup: move |event| log_event(Event::MouseUp(event)), onmouseup: move |event| log_event(Event::MouseUp(event)),
@@ -77,9 +76,7 @@ fn app(cx: Scope) -> Element {
"Hover, click, type or scroll to see the info down below" "Hover, click, type or scroll to see the info down below"
} }
div { div { events.read().iter().map(|event| rsx!( div { "{event:?}" } )) }
events.read().iter().map(|event| rsx!( div { "{event:?}" } )) }
},
},
)) ))
} }
-18
View File
@@ -1,18 +0,0 @@
use dioxus::prelude::*;
fn main() {
dioxus_desktop::launch(app);
}
fn app(cx: Scope) -> Element {
cx.render(rsx! {
button {
onclick: |_| async move {
println!("hello, desktop!");
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
println!("goodbye, desktop!");
},
"hello, desktop!"
}
})
}
+5 -19
View File
@@ -1,7 +1,6 @@
//! This example shows how to create a popup window and send data back to the parent window. //! This example shows how to create a popup window and send data back to the parent window.
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_desktop::use_window;
use futures_util::StreamExt; use futures_util::StreamExt;
fn main() { fn main() {
@@ -9,7 +8,6 @@ fn main() {
} }
fn app(cx: Scope) -> Element { fn app(cx: Scope) -> Element {
let window = use_window(cx);
let emails_sent = use_ref(cx, Vec::new); let emails_sent = use_ref(cx, Vec::new);
let tx = use_coroutine(cx, |mut rx: UnboundedReceiver<String>| { let tx = use_coroutine(cx, |mut rx: UnboundedReceiver<String>| {
@@ -27,14 +25,8 @@ fn app(cx: Scope) -> Element {
button { button {
onclick: move |_| { onclick: move |_| {
let dom = VirtualDom::new_with_props(compose, ComposeProps { let dom = VirtualDom::new_with_props(compose, ComposeProps { app_tx: tx.clone() });
app_tx: tx.clone() dioxus_desktop::window().new_window(dom, Default::default());
});
// this returns a weak reference to the other window
// Be careful not to keep a strong reference to the other window or it will never be dropped
// and the window will never close.
window.new_window(dom, Default::default());
}, },
"Click to compose a new email" "Click to compose a new email"
} }
@@ -57,7 +49,6 @@ struct ComposeProps {
fn compose(cx: Scope<ComposeProps>) -> Element { fn compose(cx: Scope<ComposeProps>) -> Element {
let user_input = use_state(cx, String::new); let user_input = use_state(cx, String::new);
let window = use_window(cx);
cx.render(rsx! { cx.render(rsx! {
div { div {
@@ -66,17 +57,12 @@ fn compose(cx: Scope<ComposeProps>) -> Element {
button { button {
onclick: move |_| { onclick: move |_| {
cx.props.app_tx.send(user_input.get().clone()); cx.props.app_tx.send(user_input.get().clone());
window.close(); dioxus_desktop::window().close();
}, },
"Click to send" "Click to send"
} }
input { input { oninput: move |e| user_input.set(e.value()), value: "{user_input}" }
oninput: move |e| {
user_input.set(e.value.clone());
},
value: "{user_input}"
}
} }
}) })
} }
+1 -1
View File
@@ -22,7 +22,7 @@ fn app(cx: Scope) -> Element {
input { input {
value: "{counter}", value: "{counter}",
oninput: move |e| { oninput: move |e| {
if let Ok(value) = e.value.parse::<usize>() { if let Ok(value) = e.value().parse::<usize>() {
counters.make_mut()[i] = value; counters.make_mut()[i] = value;
} }
} }
+27 -63
View File
@@ -35,14 +35,16 @@ fn App(cx: Scope) -> Element {
rel: "stylesheet", rel: "stylesheet",
href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css", href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css",
integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5", integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5",
crossorigin: "anonymous", crossorigin: "anonymous"
} }
style { " style {
"
.red {{ .red {{
background-color: rgb(202, 60, 60) !important; background-color: rgb(202, 60, 60) !important;
}} }}
" } "
}
h1 { "Dioxus CRM Example" } h1 { "Dioxus CRM Example" }
@@ -57,16 +59,8 @@ fn ClientList(cx: Scope) -> Element {
cx.render(rsx! { cx.render(rsx! {
h2 { "List of Clients" } h2 { "List of Clients" }
Link { Link { to: Route::ClientAdd {}, class: "pure-button pure-button-primary", "Add Client" }
to: Route::ClientAdd {}, Link { to: Route::Settings {}, class: "pure-button", "Settings" }
class: "pure-button pure-button-primary",
"Add Client"
}
Link {
to: Route::Settings {},
class: "pure-button",
"Settings"
}
clients.read().iter().map(|client| rsx! { clients.read().iter().map(|client| rsx! {
div { div {
@@ -87,8 +81,6 @@ fn ClientAdd(cx: Scope) -> Element {
let last_name = use_state(cx, String::new); let last_name = use_state(cx, String::new);
let description = use_state(cx, String::new); let description = use_state(cx, String::new);
let navigator = use_navigator(cx);
cx.render(rsx! { cx.render(rsx! {
h2 { "Add new Client" } h2 { "Add new Client" }
@@ -96,79 +88,55 @@ fn ClientAdd(cx: Scope) -> Element {
class: "pure-form pure-form-aligned", class: "pure-form pure-form-aligned",
onsubmit: move |_| { onsubmit: move |_| {
let mut clients = clients.write(); let mut clients = clients.write();
clients
clients.push(Client { .push(Client {
first_name: first_name.to_string(), first_name: first_name.to_string(),
last_name: last_name.to_string(), last_name: last_name.to_string(),
description: description.to_string(), description: description.to_string(),
}); });
dioxus_router::router().push(Route::ClientList {});
navigator.push(Route::ClientList {});
}, },
fieldset { fieldset {
div { div { class: "pure-control-group",
class: "pure-control-group", label { "for": "first_name", "First Name" }
label {
"for": "first_name",
"First Name"
}
input { input {
id: "first_name", id: "first_name",
"type": "text", "type": "text",
placeholder: "First Name…", placeholder: "First Name…",
required: "", required: "",
value: "{first_name}", value: "{first_name}",
oninput: move |e| first_name.set(e.value.clone()) oninput: move |e| first_name.set(e.value())
} }
} }
div { div { class: "pure-control-group",
class: "pure-control-group", label { "for": "last_name", "Last Name" }
label {
"for": "last_name",
"Last Name"
}
input { input {
id: "last_name", id: "last_name",
"type": "text", "type": "text",
placeholder: "Last Name…", placeholder: "Last Name…",
required: "", required: "",
value: "{last_name}", value: "{last_name}",
oninput: move |e| last_name.set(e.value.clone()) oninput: move |e| last_name.set(e.value())
} }
} }
div { div { class: "pure-control-group",
class: "pure-control-group", label { "for": "description", "Description" }
label {
"for": "description",
"Description"
}
textarea { textarea {
id: "description", id: "description",
placeholder: "Description…", placeholder: "Description…",
value: "{description}", value: "{description}",
oninput: move |e| description.set(e.value.clone()) oninput: move |e| description.set(e.value())
} }
} }
div { div { class: "pure-controls",
class: "pure-controls", button { "type": "submit", class: "pure-button pure-button-primary", "Save" }
button { Link { to: Route::ClientList {}, class: "pure-button pure-button-primary red", "Cancel" }
"type": "submit",
class: "pure-button pure-button-primary",
"Save"
}
Link {
to: Route::ClientList {},
class: "pure-button pure-button-primary red",
"Cancel"
}
} }
} }
} }
}) })
} }
@@ -189,10 +157,6 @@ fn Settings(cx: Scope) -> Element {
"Remove all Clients" "Remove all Clients"
} }
Link { Link { to: Route::ClientList {}, class: "pure-button", "Go back" }
to: Route::ClientList {},
class: "pure-button",
"Go back"
}
}) })
} }
+1 -1
View File
@@ -10,7 +10,7 @@ fn app(cx: Scope) -> Element {
p { p {
"This should show an image:" "This should show an image:"
} }
img { src: "examples/assets/logo.png" } img { src: mg!(image("examples/assets/logo.png").format(ImageType::Avif)).to_string() }
} }
}) })
} }
+29
View File
@@ -0,0 +1,29 @@
use dioxus::prelude::*;
use dioxus_desktop::wry::http::Response;
use dioxus_desktop::{use_asset_handler, AssetRequest};
use std::path::Path;
fn main() {
dioxus_desktop::launch(app);
}
fn app(cx: Scope) -> Element {
use_asset_handler(cx, |request: &AssetRequest| {
let path = request.path().to_path_buf();
async move {
if path != Path::new("logo.png") {
return None;
}
let image_data: &[u8] = include_bytes!("./assets/logo.png");
Some(Response::new(image_data.into()))
}
});
cx.render(rsx! {
div {
img {
src: "logo.png"
}
}
})
}
+11 -16
View File
@@ -1,4 +1,4 @@
use dioxus::prelude::*; use dioxus::{core::CapturedError, prelude::*};
fn main() { fn main() {
dioxus_desktop::launch(App); dioxus_desktop::launch(App);
@@ -6,30 +6,25 @@ fn main() {
#[component] #[component]
fn App(cx: Scope) -> Element { fn App(cx: Scope) -> Element {
let val = use_state(cx, || "0.0001");
let num = match val.parse::<f32>() {
Err(_) => return cx.render(rsx!("Parsing failed")),
Ok(num) => num,
};
cx.render(rsx! { cx.render(rsx! {
h1 { "The parsed value is {num}" } ErrorBoundary {
button { handle_error: |error: CapturedError| rsx! {"Found error {error}"},
onclick: move |_| val.set("invalid"), DemoC {
"Set an invalid number" x: 1
}
} }
(0..5).map(|i| rsx! {
DemoC { x: i }
})
}) })
} }
#[component] #[component]
fn DemoC(cx: Scope, x: i32) -> Element { fn DemoC(cx: Scope, x: i32) -> Element {
let result = Err("Error");
result.throw()?;
cx.render(rsx! { cx.render(rsx! {
h1 { h1 {
"asdasdasdasd {x}" "{x}"
} }
}) })
} }
+9 -14
View File
@@ -5,26 +5,21 @@ fn main() {
} }
fn app(cx: Scope) -> Element { fn app(cx: Scope) -> Element {
let eval_provider = use_eval(cx); let future = use_future(cx, (), |_| async move {
let eval = eval(
let future = use_future(cx, (), |_| { r#"
to_owned![eval_provider];
async move {
let eval = eval_provider(
r#"
dioxus.send("Hi from JS!"); dioxus.send("Hi from JS!");
let msg = await dioxus.recv(); let msg = await dioxus.recv();
console.log(msg); console.log(msg);
return "hello world"; return "hello world";
"#, "#,
) )
.unwrap(); .unwrap();
eval.send("Hi from Rust!".into()).unwrap(); eval.send("Hi from Rust!".into()).unwrap();
let res = eval.recv().await.unwrap(); let res = eval.recv().await.unwrap();
println!("{:?}", eval.await); println!("{:?}", eval.await);
res res
}
}); });
match future.value() { match future.value() {
+2 -1
View File
@@ -18,13 +18,14 @@ fn main() {
); );
} }
const _STYLE: &str = mg!(file("./examples/assets/fileexplorer.css"));
fn app(cx: Scope) -> Element { fn app(cx: Scope) -> Element {
let files = use_ref(cx, Files::new); let files = use_ref(cx, Files::new);
cx.render(rsx! { cx.render(rsx! {
div { div {
link { href:"https://fonts.googleapis.com/icon?family=Material+Icons", rel:"stylesheet", } link { href:"https://fonts.googleapis.com/icon?family=Material+Icons", rel:"stylesheet", }
style { include_str!("./assets/fileexplorer.css") }
header { header {
i { class: "material-icons icon-menu", "menu" } i { class: "material-icons icon-menu", "menu" }
h1 { "Files: ", files.read().current() } h1 { "Files: ", files.read().current() }
+2 -2
View File
@@ -16,7 +16,7 @@ fn App(cx: Scope) -> Element {
r#type: "checkbox", r#type: "checkbox",
checked: "{enable_directory_upload}", checked: "{enable_directory_upload}",
oninput: move |evt| { oninput: move |evt| {
enable_directory_upload.set(evt.value.parse().unwrap()); enable_directory_upload.set(evt.value().parse().unwrap());
}, },
}, },
"Enable directory upload" "Enable directory upload"
@@ -30,7 +30,7 @@ fn App(cx: Scope) -> Element {
onchange: |evt| { onchange: |evt| {
to_owned![files_uploaded]; to_owned![files_uploaded];
async move { async move {
if let Some(file_engine) = &evt.files { if let Some(file_engine) = &evt.files() {
let files = file_engine.files(); let files = file_engine.files();
for file_name in files { for file_name in files {
sleep(std::time::Duration::from_secs(1)).await; sleep(std::time::Duration::from_secs(1)).await;
+2 -2
View File
@@ -14,8 +14,8 @@ fn app(cx: Scope) -> Element {
div { div {
h1 { "Form" } h1 { "Form" }
form { form {
onsubmit: move |ev| println!("Submitted {:?}", ev.values), onsubmit: move |ev| println!("Submitted {:?}", ev.values()),
oninput: move |ev| println!("Input {:?}", ev.values), oninput: move |ev| println!("Input {:?}", ev.values()),
input { r#type: "text", name: "username" } input { r#type: "text", name: "username" }
input { r#type: "text", name: "full-name" } input { r#type: "text", name: "full-name" }
input { r#type: "password", name: "password" } input { r#type: "password", name: "password" }
+17 -20
View File
@@ -8,33 +8,30 @@ fn main() {
} }
fn app(cx: Scope) -> Element { fn app(cx: Scope) -> Element {
let onsubmit = move |evt: FormEvent| { let onsubmit = move |evt: FormEvent| async move {
cx.spawn(async move { let resp = reqwest::Client::new()
let resp = reqwest::Client::new() .post("http://localhost:8080/login")
.post("http://localhost:8080/login") .form(&[
.form(&[ ("username", &evt.values()["username"]),
("username", &evt.values["username"]), ("password", &evt.values()["password"]),
("password", &evt.values["password"]), ])
]) .send()
.send() .await;
.await;
match resp { match resp {
// Parse data from here, such as storing a response token // Parse data from here, such as storing a response token
Ok(_data) => println!("Login successful!"), Ok(_data) => println!("Login successful!"),
//Handle any errors from the fetch here //Handle any errors from the fetch here
Err(_err) => { Err(_err) => {
println!("Login failed - you need a login server running on localhost:8080.") println!("Login failed - you need a login server running on localhost:8080.")
}
} }
}); }
}; };
cx.render(rsx! { cx.render(rsx! {
h1 { "Login" } h1 { "Login" }
form { form { onsubmit: onsubmit,
onsubmit: onsubmit,
input { r#type: "text", id: "username", name: "username" } input { r#type: "text", id: "username", name: "username" }
label { "Username" } label { "Username" }
br {} br {}
+1 -1
View File
@@ -35,7 +35,7 @@ frameworks = ["WebKit"]
[dependencies] [dependencies]
anyhow = "1.0.56" anyhow = "1.0.56"
log = "0.4.11" log = "0.4.11"
wry = "0.28.0" wry = "0.34.0"
dioxus = { path = "../../packages/dioxus" } dioxus = { path = "../../packages/dioxus" }
dioxus-desktop = { path = "../../packages/desktop", features = [ dioxus-desktop = { path = "../../packages/desktop", features = [
"tokio_runtime", "tokio_runtime",
+1 -3
View File
@@ -5,14 +5,12 @@ fn main() {
} }
fn app(cx: Scope) -> Element { fn app(cx: Scope) -> Element {
let window = dioxus_desktop::use_window(cx);
cx.render(rsx! { cx.render(rsx! {
div { div {
button { button {
onclick: move |_| { onclick: move |_| {
let dom = VirtualDom::new(popup); let dom = VirtualDom::new(popup);
window.new_window(dom, Default::default()); dioxus_desktop::window().new_window(dom, Default::default());
}, },
"New Window" "New Window"
} }
+1
View File
@@ -2,6 +2,7 @@
name = "openid_auth_demo" name = "openid_auth_demo"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+20 -20
View File
@@ -1,20 +1,20 @@
use openidconnect::{core::CoreErrorResponseType, url, RequestTokenError, StandardErrorResponse}; use openidconnect::{core::CoreErrorResponseType, url, RequestTokenError, StandardErrorResponse};
use thiserror::Error; use thiserror::Error;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum Error { pub enum Error {
#[error("Discovery error: {0}")] #[error("Discovery error: {0}")]
OpenIdConnect( OpenIdConnect(
#[from] openidconnect::DiscoveryError<openidconnect::reqwest::Error<reqwest::Error>>, #[from] openidconnect::DiscoveryError<openidconnect::reqwest::Error<reqwest::Error>>,
), ),
#[error("Parsing error: {0}")] #[error("Parsing error: {0}")]
Parse(#[from] url::ParseError), Parse(#[from] url::ParseError),
#[error("Request token error: {0}")] #[error("Request token error: {0}")]
RequestToken( RequestToken(
#[from] #[from]
RequestTokenError< RequestTokenError<
openidconnect::reqwest::Error<reqwest::Error>, openidconnect::reqwest::Error<reqwest::Error>,
StandardErrorResponse<CoreErrorResponseType>, StandardErrorResponse<CoreErrorResponseType>,
>, >,
), ),
} }
+12
View File
@@ -16,8 +16,20 @@ fn app(cx: Scope) -> Element {
a: "asd".to_string(), a: "asd".to_string(),
c: "asd".to_string(), c: "asd".to_string(),
d: Some("asd".to_string()), d: Some("asd".to_string()),
e: Some("asd".to_string()),
}
Button {
a: "asd".to_string(),
b: "asd".to_string(),
c: "asd".to_string(),
d: Some("asd".to_string()),
e: "asd".to_string(), e: "asd".to_string(),
} }
Button {
a: "asd".to_string(),
c: "asd".to_string(),
d: Some("asd".to_string()),
}
}) })
} }
+2 -4
View File
@@ -1,13 +1,11 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_desktop::{tao::dpi::PhysicalPosition, use_window, LogicalSize, WindowBuilder}; use dioxus_desktop::{tao::dpi::PhysicalPosition, LogicalSize, WindowBuilder};
fn main() { fn main() {
dioxus_desktop::launch_cfg(app, make_config()); dioxus_desktop::launch_cfg(app, make_config());
} }
fn app(cx: Scope) -> Element { fn app(cx: Scope) -> Element {
let window = use_window(cx);
cx.render(rsx! { cx.render(rsx! {
div { div {
width: "100%", width: "100%",
@@ -19,7 +17,7 @@ fn app(cx: Scope) -> Element {
width: "100%", width: "100%",
height: "10px", height: "10px",
background_color: "black", background_color: "black",
onmousedown: move |_| window.drag(), onmousedown: move |_| dioxus_desktop::window().drag(),
} }
"This is an overlay!" "This is an overlay!"
+1
View File
@@ -2,6 +2,7 @@
name = "query_segments_demo" name = "query_segments_demo"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+21 -7
View File
@@ -14,29 +14,35 @@ use dioxus_router::prelude::*;
#[derive(Routable, Clone)] #[derive(Routable, Clone)]
#[rustfmt::skip] #[rustfmt::skip]
enum Route { enum Route {
// segments that start with ?: are query segments // segments that start with ?:.. are query segments that capture the entire query
#[route("/blog?:query_params")] #[route("/blog?:..query_params")]
BlogPost { BlogPost {
// You must include query segments in child variants // You must include query segments in child variants
query_params: BlogQuerySegments, query_params: ManualBlogQuerySegments,
},
// segments that follow the ?:field&:other_field syntax are query segments that follow the standard url query syntax
#[route("/autoblog?:name&:surname")]
AutomaticBlogPost {
name: String,
surname: String,
}, },
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
struct BlogQuerySegments { struct ManualBlogQuerySegments {
name: String, name: String,
surname: String, surname: String,
} }
/// The display impl needs to display the query in a way that can be parsed: /// The display impl needs to display the query in a way that can be parsed:
impl Display for BlogQuerySegments { impl Display for ManualBlogQuerySegments {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "name={}&surname={}", self.name, self.surname) write!(f, "name={}&surname={}", self.name, self.surname)
} }
} }
/// The query segment is anything that implements <https://docs.rs/dioxus-router/latest/dioxus_router/routable/trait.FromQuery.html>. You can implement that trait for a struct if you want to parse multiple query parameters. /// The query segment is anything that implements <https://docs.rs/dioxus-router/latest/dioxus_router/routable/trait.FromQuery.html>. You can implement that trait for a struct if you want to parse multiple query parameters.
impl FromQuery for BlogQuerySegments { impl FromQuery for ManualBlogQuerySegments {
fn from_query(query: &str) -> Self { fn from_query(query: &str) -> Self {
let mut name = None; let mut name = None;
let mut surname = None; let mut surname = None;
@@ -57,13 +63,21 @@ impl FromQuery for BlogQuerySegments {
} }
#[component] #[component]
fn BlogPost(cx: Scope, query_params: BlogQuerySegments) -> Element { fn BlogPost(cx: Scope, query_params: ManualBlogQuerySegments) -> Element {
render! { render! {
div{"This is your blogpost with a query segment:"} div{"This is your blogpost with a query segment:"}
div{format!("{:?}", query_params)} div{format!("{:?}", query_params)}
} }
} }
#[component]
fn AutomaticBlogPost(cx: Scope, name: String, surname: String) -> Element {
render! {
div{"This is your blogpost with a query segment:"}
div{format!("name={}&surname={}", name, surname)}
}
}
#[component] #[component]
fn App(cx: Scope) -> Element { fn App(cx: Scope) -> Element {
render! { Router::<Route>{} } render! { Router::<Route>{} }
+1 -1
View File
@@ -64,7 +64,7 @@ fn DataEditor(cx: Scope, id: usize) -> Element {
fn DataView(cx: Scope, id: usize) -> Element { fn DataView(cx: Scope, id: usize) -> Element {
let cool_data = use_shared_state::<CoolData>(cx).unwrap(); let cool_data = use_shared_state::<CoolData>(cx).unwrap();
let oninput = |e: FormEvent| cool_data.write().set(*id, e.value.clone()); let oninput = |e: FormEvent| cool_data.write().set(*id, e.value());
let cool_data = cool_data.read(); let cool_data = cool_data.read();
let my_data = &cool_data.view(id).unwrap(); let my_data = &cool_data.view(id).unwrap();
+1 -1
View File
@@ -18,4 +18,4 @@ dioxus = { path = "../../packages/dioxus" }
dioxus-desktop = { path = "../../packages/desktop" } dioxus-desktop = { path = "../../packages/desktop" }
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
dioxus-web = { path = "../../packages/web" } dioxus-web = { path = "../../packages/web" }
+1 -1
View File
@@ -30,7 +30,7 @@ watch_path = ["src", "public"]
[web.resource] [web.resource]
# CSS style file # CSS style file
style = ["/tailwind.css"] style = []
# Javascript code file # Javascript code file
script = [] script = []
+1 -1
View File
@@ -7,7 +7,7 @@ This example shows how an app might be styled with TailwindCSS.
1. Install the Dioxus CLI: 1. Install the Dioxus CLI:
```bash ```bash
cargo install --git https://github.com/DioxusLabs/cli cargo install dioxus-cli
``` ```
2. Install npm: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm 2. Install npm: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm
@@ -0,0 +1 @@
*,:before,:after{box-sizing:border-box;border:0 solid #e5e7eb}:before,:after{--tw-content:""}html{-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-feature-settings:normal;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}body{line-height:inherit;margin:0}hr{color:inherit;border-top-width:1px;height:0}abbr:where([title]){text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:#0000;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{margin:0;padding:0;list-style:none}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after,::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (width>=640px){.container{max-width:640px}}@media (width>=768px){.container{max-width:768px}}@media (width>=1024px){.container{max-width:1024px}}@media (width>=1280px){.container{max-width:1280px}}@media (width>=1536px){.container{max-width:1536px}}.mx-auto{margin-left:auto;margin-right:auto}.mb-16{margin-bottom:4rem}.mb-4{margin-bottom:1rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.mr-5{margin-right:1.25rem}.mt-4{margin-top:1rem}.flex{display:flex}.inline-flex{display:inline-flex}.hidden{display:none}.h-10{height:2.5rem}.h-4{height:1rem}.w-10{width:2.5rem}.w-4{width:1rem}.w-5\/6{width:83.3333%}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-center{justify-content:center}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.border-0{border-width:0}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.bg-indigo-500{--tw-bg-opacity:1;background-color:rgb(99 102 241/var(--tw-bg-opacity))}.object-cover{-o-object-fit:cover;object-fit:cover}.object-center{-o-object-position:center;object-position:center}.p-2{padding:.5rem}.p-5{padding:1.25rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-24{padding-top:6rem;padding-bottom:6rem}.text-center{text-align:center}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-medium{font-weight:500}.leading-relaxed{line-heiLine truncated
+3 -5
View File
@@ -2,13 +2,11 @@
use dioxus::prelude::*; use dioxus::prelude::*;
const _STYLE: &str = mg!(file("./public/tailwind.css"));
fn main() { fn main() {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
dioxus_desktop::launch_cfg( dioxus_desktop::launch(app);
app,
dioxus_desktop::Config::new()
.with_custom_head(r#"<link rel="stylesheet" href="public/tailwind.css">"#.to_string()),
);
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
dioxus_web::launch(app); dioxus_web::launch(app);
} }
+1 -1
View File
@@ -17,7 +17,7 @@ fn app(cx: Scope) -> Element {
rows: "10", rows: "10",
cols: "80", cols: "80",
value: "{model}", value: "{model}",
oninput: move |e| model.set(e.value.clone()), oninput: move |e| model.set(e.value().clone()),
} }
}) })
} }
+52 -44
View File
@@ -7,6 +7,8 @@ fn main() {
dioxus_desktop::launch(app); dioxus_desktop::launch(app);
} }
const _STYLE: &str = mg!(file("./examples/assets/todomvc.css"));
#[derive(PartialEq, Eq, Clone, Copy)] #[derive(PartialEq, Eq, Clone, Copy)]
pub enum FilterState { pub enum FilterState {
All, All,
@@ -47,12 +49,8 @@ pub fn app(cx: Scope<()>) -> Element {
cx.render(rsx! { cx.render(rsx! {
section { class: "todoapp", section { class: "todoapp",
style { include_str!("./assets/todomvc.css") } TodoHeader { todos: todos }
TodoHeader { section { class: "main",
todos: todos,
}
section {
class: "main",
if !todos.is_empty() { if !todos.is_empty() {
rsx! { rsx! {
input { input {
@@ -103,31 +101,34 @@ pub fn TodoHeader<'a>(cx: Scope<'a, TodoHeaderProps<'a>>) -> Element {
cx.render(rsx! { cx.render(rsx! {
header { class: "header", header { class: "header",
h1 {"todos"} h1 { "todos" }
input { input {
class: "new-todo", class: "new-todo",
placeholder: "What needs to be done?", placeholder: "What needs to be done?",
value: "{draft}", value: "{draft}",
autofocus: "true", autofocus: "true",
oninput: move |evt| { oninput: move |evt| {
draft.set(evt.value.clone()); draft.set(evt.value().clone());
}, },
onkeydown: move |evt| { onkeydown: move |evt| {
if evt.key() == Key::Enter && !draft.is_empty() { if evt.key() == Key::Enter && !draft.is_empty() {
cx.props.todos.make_mut().insert( cx.props
**todo_id, .todos
TodoItem { .make_mut()
id: **todo_id, .insert(
checked: false, **todo_id,
contents: draft.to_string(), TodoItem {
}, id: **todo_id,
); checked: false,
*todo_id.make_mut() += 1; contents: draft.to_string(),
draft.set("".to_string()); },
);
*todo_id.make_mut() += 1;
draft.set("".to_string());
}
} }
} }
} }
}
}) })
} }
@@ -146,8 +147,7 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
let editing = if **is_editing { "editing" } else { "" }; let editing = if **is_editing { "editing" } else { "" };
cx.render(rsx!{ cx.render(rsx!{
li { li { class: "{completed} {editing}",
class: "{completed} {editing}",
div { class: "view", div { class: "view",
input { input {
class: "toggle", class: "toggle",
@@ -155,26 +155,28 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
id: "cbg-{todo.id}", id: "cbg-{todo.id}",
checked: "{todo.checked}", checked: "{todo.checked}",
oninput: move |evt| { oninput: move |evt| {
cx.props.todos.make_mut()[&cx.props.id].checked = evt.value.parse().unwrap(); cx.props.todos.make_mut()[&cx.props.id].checked = evt.value().parse().unwrap();
} }
} }
label { label {
r#for: "cbg-{todo.id}", r#for: "cbg-{todo.id}",
ondblclick: move |_| is_editing.set(true), ondoubleclick: move |_| is_editing.set(true),
prevent_default: "onclick", prevent_default: "onclick",
"{todo.contents}" "{todo.contents}"
} }
button { button {
class: "destroy", class: "destroy",
onclick: move |_| { cx.props.todos.make_mut().remove(&todo.id); }, onclick: move |_| {
prevent_default: "onclick", cx.props.todos.make_mut().remove(&todo.id);
},
prevent_default: "onclick"
} }
} }
is_editing.then(|| rsx!{ is_editing.then(|| rsx!{
input { input {
class: "edit", class: "edit",
value: "{todo.contents}", value: "{todo.contents}",
oninput: move |evt| cx.props.todos.make_mut()[&cx.props.id].contents = evt.value.clone(), oninput: move |evt| cx.props.todos.make_mut()[&cx.props.id].contents = evt.value(),
autofocus: "true", autofocus: "true",
onfocusout: move |_| is_editing.set(false), onfocusout: move |_| is_editing.set(false),
onkeydown: move |evt| { onkeydown: move |evt| {
@@ -213,15 +215,15 @@ pub fn ListFooter<'a>(cx: Scope<'a, ListFooterProps<'a>>) -> Element {
cx.render(rsx! { cx.render(rsx! {
footer { class: "footer", footer { class: "footer",
span { class: "todo-count", span { class: "todo-count",
strong {"{active_todo_count} "} strong { "{active_todo_count} " }
span {"{active_todo_text} left"} span { "{active_todo_text} left" }
} }
ul { class: "filters", ul { class: "filters",
for (state, state_text, url) in [ for (state , state_text , url) in [
(FilterState::All, "All", "#/"), (FilterState::All, "All", "#/"),
(FilterState::Active, "Active", "#/active"), (FilterState::Active, "Active", "#/active"),
(FilterState::Completed, "Completed", "#/completed"), (FilterState::Completed, "Completed", "#/completed"),
] { ] {
li { li {
a { a {
href: url, href: url,
@@ -250,8 +252,14 @@ pub fn PageFooter(cx: Scope) -> Element {
cx.render(rsx! { cx.render(rsx! {
footer { class: "info", footer { class: "info",
p { "Double-click to edit a todo" } p { "Double-click to edit a todo" }
p { "Created by ", a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }} p {
p { "Part of ", a { href: "http://todomvc.com", "TodoMVC" }} "Created by "
a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }
}
p {
"Part of "
a { href: "http://todomvc.com", "TodoMVC" }
}
} }
}) })
} }
+184
View File
@@ -0,0 +1,184 @@
use dioxus::prelude::*;
use dioxus_desktop::wry::http;
use dioxus_desktop::wry::http::Response;
use dioxus_desktop::{use_asset_handler, AssetRequest};
use http::{header::*, response::Builder as ResponseBuilder, status::StatusCode};
use std::borrow::Cow;
use std::{io::SeekFrom, path::PathBuf};
use tokio::io::AsyncReadExt;
use tokio::io::AsyncSeekExt;
use tokio::io::AsyncWriteExt;
const VIDEO_PATH: &str = "./examples/assets/test_video.mp4";
fn main() {
let video_file = PathBuf::from(VIDEO_PATH);
if !video_file.exists() {
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
println!("Downloading video file...");
let video_url =
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
let mut response = reqwest::get(video_url).await.unwrap();
let mut file = tokio::fs::File::create(&video_file).await.unwrap();
while let Some(chunk) = response.chunk().await.unwrap() {
file.write_all(&chunk).await.unwrap();
}
});
}
dioxus_desktop::launch(app);
}
fn app(cx: Scope) -> Element {
use_asset_handler(cx, move |request: &AssetRequest| {
let request = request.clone();
async move {
let video_file = PathBuf::from(VIDEO_PATH);
let mut file = tokio::fs::File::open(&video_file).await.unwrap();
let response: Option<Response<Cow<'static, [u8]>>> =
match get_stream_response(&mut file, &request).await {
Ok(response) => Some(response.map(Cow::Owned)),
Err(err) => {
eprintln!("Error: {}", err);
None
}
};
response
}
});
render! {
div { video { src: "test_video.mp4", autoplay: true, controls: true, width: 640, height: 480 } }
}
}
async fn get_stream_response(
asset: &mut (impl tokio::io::AsyncSeek + tokio::io::AsyncRead + Unpin + Send + Sync),
request: &AssetRequest,
) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error>> {
// get stream length
let len = {
let old_pos = asset.stream_position().await?;
let len = asset.seek(SeekFrom::End(0)).await?;
asset.seek(SeekFrom::Start(old_pos)).await?;
len
};
let mut resp = ResponseBuilder::new().header(CONTENT_TYPE, "video/mp4");
// if the webview sent a range header, we need to send a 206 in return
// Actually only macOS and Windows are supported. Linux will ALWAYS return empty headers.
let http_response = if let Some(range_header) = request.headers().get("range") {
let not_satisfiable = || {
ResponseBuilder::new()
.status(StatusCode::RANGE_NOT_SATISFIABLE)
.header(CONTENT_RANGE, format!("bytes */{len}"))
.body(vec![])
};
// parse range header
let ranges = if let Ok(ranges) = http_range::HttpRange::parse(range_header.to_str()?, len) {
ranges
.iter()
// map the output back to spec range <start-end>, example: 0-499
.map(|r| (r.start, r.start + r.length - 1))
.collect::<Vec<_>>()
} else {
return Ok(not_satisfiable()?);
};
/// The Maximum bytes we send in one range
const MAX_LEN: u64 = 1000 * 1024;
if ranges.len() == 1 {
let &(start, mut end) = ranges.first().unwrap();
// check if a range is not satisfiable
//
// this should be already taken care of by HttpRange::parse
// but checking here again for extra assurance
if start >= len || end >= len || end < start {
return Ok(not_satisfiable()?);
}
// adjust end byte for MAX_LEN
end = start + (end - start).min(len - start).min(MAX_LEN - 1);
// calculate number of bytes needed to be read
let bytes_to_read = end + 1 - start;
// allocate a buf with a suitable capacity
let mut buf = Vec::with_capacity(bytes_to_read as usize);
// seek the file to the starting byte
asset.seek(SeekFrom::Start(start)).await?;
// read the needed bytes
asset.take(bytes_to_read).read_to_end(&mut buf).await?;
resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}"));
resp = resp.header(CONTENT_LENGTH, end + 1 - start);
resp = resp.status(StatusCode::PARTIAL_CONTENT);
resp.body(buf)
} else {
let mut buf = Vec::new();
let ranges = ranges
.iter()
.filter_map(|&(start, mut end)| {
// filter out unsatisfiable ranges
//
// this should be already taken care of by HttpRange::parse
// but checking here again for extra assurance
if start >= len || end >= len || end < start {
None
} else {
// adjust end byte for MAX_LEN
end = start + (end - start).min(len - start).min(MAX_LEN - 1);
Some((start, end))
}
})
.collect::<Vec<_>>();
let boundary = format!("{:x}", rand::random::<u64>());
let boundary_sep = format!("\r\n--{boundary}\r\n");
let boundary_closer = format!("\r\n--{boundary}\r\n");
resp = resp.header(
CONTENT_TYPE,
format!("multipart/byteranges; boundary={boundary}"),
);
for (end, start) in ranges {
// a new range is being written, write the range boundary
buf.write_all(boundary_sep.as_bytes()).await?;
// write the needed headers `Content-Type` and `Content-Range`
buf.write_all(format!("{CONTENT_TYPE}: video/mp4\r\n").as_bytes())
.await?;
buf.write_all(format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes())
.await?;
// write the separator to indicate the start of the range body
buf.write_all("\r\n".as_bytes()).await?;
// calculate number of bytes needed to be read
let bytes_to_read = end + 1 - start;
let mut local_buf = vec![0_u8; bytes_to_read as usize];
asset.seek(SeekFrom::Start(start)).await?;
asset.read_exact(&mut local_buf).await?;
buf.extend_from_slice(&local_buf);
}
// all ranges have been written, write the closing boundary
buf.write_all(boundary_closer.as_bytes()).await?;
resp.body(buf)
}
} else {
resp = resp.header(CONTENT_LENGTH, len);
let mut buf = Vec::with_capacity(len as usize);
asset.read_to_end(&mut buf).await?;
resp.body(buf)
};
http_response.map_err(Into::into)
}
+2 -4
View File
@@ -1,12 +1,10 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_desktop::use_window;
fn main() { fn main() {
dioxus_desktop::launch(app); dioxus_desktop::launch(app);
} }
fn app(cx: Scope) -> Element { fn app(cx: Scope) -> Element {
let window = use_window(cx);
let level = use_state(cx, || 1.0); let level = use_state(cx, || 1.0);
cx.render(rsx! { cx.render(rsx! {
@@ -14,9 +12,9 @@ fn app(cx: Scope) -> Element {
r#type: "number", r#type: "number",
value: "{level}", value: "{level}",
oninput: |e| { oninput: |e| {
if let Ok(new_zoom) = e.value.parse::<f64>() { if let Ok(new_zoom) = e.value().parse::<f64>() {
level.set(new_zoom); level.set(new_zoom);
window.webview.zoom(new_zoom); dioxus_desktop::window().webview.zoom(new_zoom);
} }
} }
} }
+1 -1
View File
@@ -20,7 +20,7 @@ fn app(cx: Scope) -> Element {
input { input {
value: "{contents}", value: "{contents}",
r#type: "text", r#type: "text",
oninput: move |e| contents.set(e.value.clone()), oninput: move |e| contents.set(e.value()),
} }
} }
}) })
+6 -5
View File
@@ -8,13 +8,14 @@ use std::fmt::{Result, Write};
use dioxus_rsx::IfmtInput; use dioxus_rsx::IfmtInput;
use crate::write_ifmt; use crate::{indent::IndentOptions, write_ifmt};
/// The output buffer that tracks indent and string /// The output buffer that tracks indent and string
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Buffer { pub struct Buffer {
pub buf: String, pub buf: String,
pub indent: usize, pub indent_level: usize,
pub indent: IndentOptions,
} }
impl Buffer { impl Buffer {
@@ -31,16 +32,16 @@ impl Buffer {
} }
pub fn tab(&mut self) -> Result { pub fn tab(&mut self) -> Result {
self.write_tabs(self.indent) self.write_tabs(self.indent_level)
} }
pub fn indented_tab(&mut self) -> Result { pub fn indented_tab(&mut self) -> Result {
self.write_tabs(self.indent + 1) self.write_tabs(self.indent_level + 1)
} }
pub fn write_tabs(&mut self, num: usize) -> std::fmt::Result { pub fn write_tabs(&mut self, num: usize) -> std::fmt::Result {
for _ in 0..num { for _ in 0..num {
write!(self.buf, " ")? write!(self.buf, "{}", self.indent.indent_str())?
} }
Ok(()) Ok(())
} }
+6 -6
View File
@@ -67,7 +67,7 @@ impl Writer<'_> {
// check if we have a lot of attributes // check if we have a lot of attributes
let attr_len = self.is_short_attrs(attributes); let attr_len = self.is_short_attrs(attributes);
let is_short_attr_list = (attr_len + self.out.indent * 4) < 80; let is_short_attr_list = (attr_len + self.out.indent_level * 4) < 80;
let children_len = self.is_short_children(children); let children_len = self.is_short_children(children);
let is_small_children = children_len.is_some(); let is_small_children = children_len.is_some();
@@ -87,7 +87,7 @@ impl Writer<'_> {
// if we have few children and few attributes, make it a one-liner // if we have few children and few attributes, make it a one-liner
if is_short_attr_list && is_small_children { if is_short_attr_list && is_small_children {
if children_len.unwrap() + attr_len + self.out.indent * 4 < 100 { if children_len.unwrap() + attr_len + self.out.indent_level * 4 < 100 {
opt_level = ShortOptimization::Oneliner; opt_level = ShortOptimization::Oneliner;
} else { } else {
opt_level = ShortOptimization::PropsOnTop; opt_level = ShortOptimization::PropsOnTop;
@@ -186,11 +186,11 @@ impl Writer<'_> {
} }
while let Some(attr) = attr_iter.next() { while let Some(attr) = attr_iter.next() {
self.out.indent += 1; self.out.indent_level += 1;
if !sameline { if !sameline {
self.write_comments(attr.start())?; self.write_comments(attr.start())?;
} }
self.out.indent -= 1; self.out.indent_level -= 1;
if !sameline { if !sameline {
self.out.indented_tabbed_line()?; self.out.indented_tabbed_line()?;
@@ -428,14 +428,14 @@ impl Writer<'_> {
for idx in start.line..end.line { for idx in start.line..end.line {
let line = &self.src[idx]; let line = &self.src[idx];
if line.trim().starts_with("//") { if line.trim().starts_with("//") {
for _ in 0..self.out.indent + 1 { for _ in 0..self.out.indent_level + 1 {
write!(self.out, " ")? write!(self.out, " ")?
} }
writeln!(self.out, "{}", line.trim()).unwrap(); writeln!(self.out, "{}", line.trim()).unwrap();
} }
} }
for _ in 0..self.out.indent { for _ in 0..self.out.indent_level {
write!(self.out, " ")? write!(self.out, " ")?
} }
+3 -3
View File
@@ -29,7 +29,7 @@ impl Writer<'_> {
let first_line = &self.src[start.line - 1]; let first_line = &self.src[start.line - 1];
write!(self.out, "{}", &first_line[start.column - 1..].trim_start())?; write!(self.out, "{}", &first_line[start.column - 1..].trim_start())?;
let prev_block_indent_level = crate::leading_whitespaces(first_line) / 4; let prev_block_indent_level = self.out.indent.count_indents(first_line);
for (id, line) in self.src[start.line..end.line].iter().enumerate() { for (id, line) in self.src[start.line..end.line].iter().enumerate() {
writeln!(self.out)?; writeln!(self.out)?;
@@ -43,9 +43,9 @@ impl Writer<'_> {
}; };
// trim the leading whitespace // trim the leading whitespace
let previous_indent = crate::leading_whitespaces(line) / 4; let previous_indent = self.out.indent.count_indents(line);
let offset = previous_indent.saturating_sub(prev_block_indent_level); let offset = previous_indent.saturating_sub(prev_block_indent_level);
let required_indent = self.out.indent + offset; let required_indent = self.out.indent_level + offset;
self.out.write_tabs(required_indent)?; self.out.write_tabs(required_indent)?;
let line = line.trim_start(); let line = line.trim_start();
+108
View File
@@ -0,0 +1,108 @@
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum IndentType {
Spaces,
Tabs,
}
#[derive(Debug, Clone)]
pub struct IndentOptions {
width: usize,
indent_string: String,
}
impl IndentOptions {
pub fn new(typ: IndentType, width: usize) -> Self {
assert_ne!(width, 0, "Cannot have an indent width of 0");
Self {
width,
indent_string: match typ {
IndentType::Tabs => "\t".into(),
IndentType::Spaces => " ".repeat(width),
},
}
}
/// Gets a string containing one indent worth of whitespace
pub fn indent_str(&self) -> &str {
&self.indent_string
}
/// Computes the line length in characters, counting tabs as the indent width.
pub fn line_length(&self, line: &str) -> usize {
line.chars()
.map(|ch| if ch == '\t' { self.width } else { 1 })
.sum()
}
/// Estimates how many times the line has been indented.
pub fn count_indents(&self, mut line: &str) -> usize {
let mut indent = 0;
while !line.is_empty() {
// Try to count tabs
let num_tabs = line.chars().take_while(|ch| *ch == '\t').count();
if num_tabs > 0 {
indent += num_tabs;
line = &line[num_tabs..];
continue;
}
// Try to count spaces
let num_spaces = line.chars().take_while(|ch| *ch == ' ').count();
if num_spaces >= self.width {
// Intentionally floor here to take only the amount of space that matches an indent
let num_space_indents = num_spaces / self.width;
indent += num_space_indents;
line = &line[num_space_indents * self.width..];
continue;
}
// Line starts with either non-indent characters or an unevent amount of spaces,
// so no more indent remains.
break;
}
indent
}
}
impl Default for IndentOptions {
fn default() -> Self {
Self::new(IndentType::Spaces, 4)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn count_indents() {
assert_eq!(
IndentOptions::new(IndentType::Spaces, 4).count_indents("no indentation here!"),
0
);
assert_eq!(
IndentOptions::new(IndentType::Spaces, 4).count_indents(" v += 2"),
1
);
assert_eq!(
IndentOptions::new(IndentType::Spaces, 4).count_indents(" v += 2"),
2
);
assert_eq!(
IndentOptions::new(IndentType::Spaces, 4).count_indents(" v += 2"),
2
);
assert_eq!(
IndentOptions::new(IndentType::Spaces, 4).count_indents("\t\tv += 2"),
2
);
assert_eq!(
IndentOptions::new(IndentType::Spaces, 4).count_indents("\t\t v += 2"),
2
);
assert_eq!(
IndentOptions::new(IndentType::Spaces, 2).count_indents(" v += 2"),
2
);
}
}
+12 -15
View File
@@ -16,8 +16,11 @@ mod collect_macros;
mod component; mod component;
mod element; mod element;
mod expr; mod expr;
mod indent;
mod writer; mod writer;
pub use indent::{IndentOptions, IndentType};
/// A modification to the original file to be applied by an IDE /// A modification to the original file to be applied by an IDE
/// ///
/// Right now this re-writes entire rsx! blocks at a time, instead of precise line-by-line changes. /// Right now this re-writes entire rsx! blocks at a time, instead of precise line-by-line changes.
@@ -47,7 +50,7 @@ pub struct FormattedBlock {
/// back to the file precisely. /// back to the file precisely.
/// ///
/// Nested blocks of RSX will be handled automatically /// Nested blocks of RSX will be handled automatically
pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> { pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
let mut formatted_blocks = Vec::new(); let mut formatted_blocks = Vec::new();
let parsed = syn::parse_file(contents).unwrap(); let parsed = syn::parse_file(contents).unwrap();
@@ -61,6 +64,7 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
} }
let mut writer = Writer::new(contents); let mut writer = Writer::new(contents);
writer.out.indent = indent;
// Don't parse nested macros // Don't parse nested macros
let mut end_span = LineColumn { column: 0, line: 0 }; let mut end_span = LineColumn { column: 0, line: 0 };
@@ -76,7 +80,10 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
let rsx_start = macro_path.span().start(); let rsx_start = macro_path.span().start();
writer.out.indent = leading_whitespaces(writer.src[rsx_start.line - 1]) / 4; writer.out.indent_level = writer
.out
.indent
.count_indents(writer.src[rsx_start.line - 1]);
write_body(&mut writer, &body); write_body(&mut writer, &body);
@@ -159,12 +166,13 @@ pub fn fmt_block_from_expr(raw: &str, expr: ExprMacro) -> Option<String> {
buf.consume() buf.consume()
} }
pub fn fmt_block(block: &str, indent_level: usize) -> Option<String> { pub fn fmt_block(block: &str, indent_level: usize, indent: IndentOptions) -> Option<String> {
let body = syn::parse_str::<dioxus_rsx::CallBody>(block).unwrap(); let body = syn::parse_str::<dioxus_rsx::CallBody>(block).unwrap();
let mut buf = Writer::new(block); let mut buf = Writer::new(block);
buf.out.indent = indent_level; buf.out.indent = indent;
buf.out.indent_level = indent_level;
write_body(&mut buf, &body); write_body(&mut buf, &body);
@@ -230,14 +238,3 @@ pub(crate) fn write_ifmt(input: &IfmtInput, writable: &mut impl Write) -> std::f
let display = DisplayIfmt(input); let display = DisplayIfmt(input);
write!(writable, "{}", display) write!(writable, "{}", display)
} }
pub fn leading_whitespaces(input: &str) -> usize {
input
.chars()
.map_while(|c| match c {
' ' => Some(1),
'\t' => Some(4),
_ => None,
})
.sum()
}
+2 -2
View File
@@ -96,11 +96,11 @@ impl<'a> Writer<'a> {
// Push out the indent level and write each component, line by line // Push out the indent level and write each component, line by line
pub fn write_body_indented(&mut self, children: &[BodyNode]) -> Result { pub fn write_body_indented(&mut self, children: &[BodyNode]) -> Result {
self.out.indent += 1; self.out.indent_level += 1;
self.write_body_no_indent(children)?; self.write_body_no_indent(children)?;
self.out.indent -= 1; self.out.indent_level -= 1;
Ok(()) Ok(())
} }
+1 -1
View File
@@ -12,7 +12,7 @@ macro_rules! twoway {
#[test] #[test]
fn $name() { fn $name() {
let src = include_str!(concat!("./samples/", stringify!($name), ".rsx")); let src = include_str!(concat!("./samples/", stringify!($name), ".rsx"));
let formatted = dioxus_autofmt::fmt_file(src); let formatted = dioxus_autofmt::fmt_file(src, Default::default());
let out = dioxus_autofmt::apply_formats(src, formatted); let out = dioxus_autofmt::apply_formats(src, formatted);
// normalize line endings // normalize line endings
let out = out.replace("\r", ""); let out = out.replace("\r", "");
+10 -5
View File
@@ -1,10 +1,12 @@
use dioxus_autofmt::{IndentOptions, IndentType};
macro_rules! twoway { macro_rules! twoway {
($val:literal => $name:ident) => { ($val:literal => $name:ident ($indent:expr)) => {
#[test] #[test]
fn $name() { fn $name() {
let src_right = include_str!(concat!("./wrong/", $val, ".rsx")); let src_right = include_str!(concat!("./wrong/", $val, ".rsx"));
let src_wrong = include_str!(concat!("./wrong/", $val, ".wrong.rsx")); let src_wrong = include_str!(concat!("./wrong/", $val, ".wrong.rsx"));
let formatted = dioxus_autofmt::fmt_file(src_wrong); let formatted = dioxus_autofmt::fmt_file(src_wrong, $indent);
let out = dioxus_autofmt::apply_formats(src_wrong, formatted); let out = dioxus_autofmt::apply_formats(src_wrong, formatted);
// normalize line endings // normalize line endings
@@ -16,8 +18,11 @@ macro_rules! twoway {
}; };
} }
twoway!("comments" => comments); twoway!("comments-4sp" => comments_4sp (IndentOptions::new(IndentType::Spaces, 4)));
twoway!("comments-tab" => comments_tab (IndentOptions::new(IndentType::Tabs, 4)));
twoway!("multi" => multi); twoway!("multi-4sp" => multi_4sp (IndentOptions::new(IndentType::Spaces, 4)));
twoway!("multi-tab" => multi_tab (IndentOptions::new(IndentType::Tabs, 4)));
twoway!("multiexpr" => multiexpr); twoway!("multiexpr-4sp" => multiexpr_4sp (IndentOptions::new(IndentType::Spaces, 4)));
twoway!("multiexpr-tab" => multiexpr_tab (IndentOptions::new(IndentType::Tabs, 4)));
@@ -0,0 +1,7 @@
rsx! {
div {
// Comments
class: "asdasd",
"hello world"
}
}
@@ -0,0 +1,5 @@
rsx! {
div {
// Comments
class: "asdasd", "hello world" }
}
@@ -0,0 +1,3 @@
fn app(cx: Scope) -> Element {
cx.render(rsx! { div { "hello world" } })
}
@@ -0,0 +1,5 @@
fn app(cx: Scope) -> Element {
cx.render(rsx! {
div {"hello world" }
})
}
@@ -0,0 +1,8 @@
fn ItWroks() {
cx.render(rsx! {
div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light",
left,
right
}
})
}
@@ -0,0 +1,5 @@
fn ItWroks() {
cx.render(rsx! {
div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right }
})
}
+5 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "dioxus-cli" name = "dioxus-cli"
version = "0.4.1" version = "0.4.3"
authors = ["Jonathan Kelley"] authors = ["Jonathan Kelley"]
edition = "2021" edition = "2021"
description = "CLI tool for developing, testing, and publishing Dioxus apps" description = "CLI tool for developing, testing, and publishing Dioxus apps"
@@ -30,7 +30,7 @@ cargo_metadata = "0.15.0"
tokio = { version = "1.16.1", features = ["fs", "sync", "rt", "macros"] } tokio = { version = "1.16.1", features = ["fs", "sync", "rt", "macros"] }
atty = "0.2.14" atty = "0.2.14"
chrono = "0.4.19" chrono = "0.4.19"
anyhow = "1.0.53" anyhow = "1"
hyper = "0.14.17" hyper = "0.14.17"
hyper-rustls = "0.23.2" hyper-rustls = "0.23.2"
indicatif = "0.17.5" indicatif = "0.17.5"
@@ -75,6 +75,8 @@ toml_edit = "0.19.11"
tauri-bundler = { version = "=1.3.0", features = ["native-tls-vendored"] } tauri-bundler = { version = "=1.3.0", features = ["native-tls-vendored"] }
tauri-utils = "=1.4.*" tauri-utils = "=1.4.*"
manganis-cli-support= { git = "https://github.com/DioxusLabs/collect-assets", features = ["webp", "html"] }
dioxus-autofmt = { workspace = true } dioxus-autofmt = { workspace = true }
dioxus-check = { workspace = true } dioxus-check = { workspace = true }
rsx-rosetta = { workspace = true } rsx-rosetta = { workspace = true }
@@ -83,6 +85,7 @@ dioxus-html = { workspace = true, features = ["hot-reload-context"] }
dioxus-core = { workspace = true, features = ["serialize"] } dioxus-core = { workspace = true, features = ["serialize"] }
dioxus-hot-reload = { workspace = true } dioxus-hot-reload = { workspace = true }
interprocess-docfix = { version = "1.2.2" } interprocess-docfix = { version = "1.2.2" }
gitignore = "1.0.8"
[features] [features]
default = [] default = []
+4 -4
View File
@@ -10,7 +10,7 @@ It handles building, bundling, development and publishing to simplify developmen
### Install the stable version (recommended) ### Install the stable version (recommended)
``` ```shell
cargo install dioxus-cli cargo install dioxus-cli
``` ```
@@ -20,7 +20,7 @@ To get the latest bug fixes and features, you can install the development versio
However, this is not fully tested. However, this is not fully tested.
That means you're probably going to have more bugs despite having the latest bug fixes. That means you're probably going to have more bugs despite having the latest bug fixes.
``` ```shell
cargo install --git https://github.com/DioxusLabs/dioxus dioxus-cli cargo install --git https://github.com/DioxusLabs/dioxus dioxus-cli
``` ```
@@ -29,7 +29,7 @@ and install it in Cargo's global binary directory (`~/.cargo/bin/` by default).
### Install from local folder ### Install from local folder
``` ```shell
cargo install --path . --debug cargo install --path . --debug
``` ```
@@ -40,7 +40,7 @@ It will be cloned from the [dioxus-template](https://github.com/DioxusLabs/dioxu
Alternatively, you can specify the template path: Alternatively, you can specify the template path:
``` ```shell
dx create hello --template gh:dioxuslabs/dioxus-template dx create hello --template gh:dioxuslabs/dioxus-template
``` ```
+2 -2
View File
@@ -1,5 +1,5 @@
// Dioxus-CLI // Dioxus-CLI
// https://github.com/DioxusLabs/cli // https://github.com/DioxusLabs/dioxus/tree/master/packages/cli
(function () { (function () {
var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
@@ -22,4 +22,4 @@
} }
}; };
ws.onclose = reload_upon_connect; ws.onclose = reload_upon_connect;
})() })()
+125 -27
View File
@@ -2,31 +2,35 @@ use crate::{
config::{CrateConfig, ExecutableType}, config::{CrateConfig, ExecutableType},
error::{Error, Result}, error::{Error, Result},
tools::Tool, tools::Tool,
DioxusConfig,
}; };
use cargo_metadata::{diagnostic::Diagnostic, Message}; use cargo_metadata::{diagnostic::Diagnostic, Message};
use indicatif::{ProgressBar, ProgressStyle}; use indicatif::{ProgressBar, ProgressStyle};
use lazy_static::lazy_static;
use manganis_cli_support::AssetManifestExt;
use serde::Serialize; use serde::Serialize;
use std::{ use std::{
fs::{copy, create_dir_all, File}, fs::{copy, create_dir_all, File},
io::Read, io::{Read, Write},
panic, panic,
path::PathBuf, path::PathBuf,
time::Duration, time::Duration,
}; };
use wasm_bindgen_cli_support::Bindgen; use wasm_bindgen_cli_support::Bindgen;
lazy_static! {
static ref PROGRESS_BARS: indicatif::MultiProgress = indicatif::MultiProgress::new();
}
#[derive(Serialize, Debug, Clone)] #[derive(Serialize, Debug, Clone)]
pub struct BuildResult { pub struct BuildResult {
pub warnings: Vec<Diagnostic>, pub warnings: Vec<Diagnostic>,
pub elapsed_time: u128, pub elapsed_time: u128,
} }
#[allow(unused)] pub fn build(config: &CrateConfig, _: bool, skip_assets: bool) -> Result<BuildResult> {
pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
// [1] Build the project with cargo, generating a wasm32-unknown-unknown target (is there a more specific, better target to leverage?) // [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 // [2] Generate the appropriate build folders
// [3] Wasm-bindgen the .wasm fiile, and move it into the {builddir}/modules/xxxx/xxxx_bg.wasm // [3] Wasm-bindgen the .wasm file, and move it into the {builddir}/modules/xxxx/xxxx_bg.wasm
// [4] Wasm-opt the .wasm file with whatever optimizations need to be done // [4] Wasm-opt the .wasm file with whatever optimizations need to be done
// [5][OPTIONAL] Builds the Tailwind CSS file using the Tailwind standalone binary // [5][OPTIONAL] Builds the Tailwind CSS file using the Tailwind standalone binary
// [6] Link up the html page to the wasm module // [6] Link up the html page to the wasm module
@@ -41,6 +45,8 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
.. ..
} = config; } = config;
let _gaurd = WebAssetConfigDropGuard::new();
// start to build the assets // start to build the assets
let ignore_files = build_assets(config)?; let ignore_files = build_assets(config)?;
@@ -48,14 +54,25 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
// [1] Build the .wasm module // [1] Build the .wasm module
log::info!("🚅 Running build command..."); log::info!("🚅 Running build command...");
let cmd = subprocess::Exec::cmd("cargo");
let cmd = cmd let wasm_check_command = std::process::Command::new("rustup")
.args(["show"])
.output()?;
let wasm_check_output = String::from_utf8(wasm_check_command.stdout).unwrap();
if !wasm_check_output.contains("wasm32-unknown-unknown") {
log::info!("wasm32-unknown-unknown target not detected, installing..");
let _ = std::process::Command::new("rustup")
.args(["target", "add", "wasm32-unknown-unknown"])
.output()?;
}
let cmd = subprocess::Exec::cmd("cargo")
.env("CARGO_TARGET_DIR", target_dir)
.cwd(crate_dir) .cwd(crate_dir)
.arg("build") .arg("build")
.arg("--target") .arg("--target")
.arg("wasm32-unknown-unknown") .arg("wasm32-unknown-unknown")
.arg("--message-format=json") .arg("--message-format=json");
.arg("--quiet");
let cmd = if config.release { let cmd = if config.release {
cmd.arg("--release") cmd.arg("--release")
@@ -65,7 +82,7 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
let cmd = if config.verbose { let cmd = if config.verbose {
cmd.arg("--verbose") cmd.arg("--verbose")
} else { } else {
cmd cmd.arg("--quiet")
}; };
let cmd = if config.custom_profile.is_some() { let cmd = if config.custom_profile.is_some() {
@@ -82,6 +99,8 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
cmd cmd
}; };
let cmd = cmd.args(&config.cargo_args);
let cmd = match executable { let cmd = match executable {
ExecutableType::Binary(name) => cmd.arg("--bin").arg(name), ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
ExecutableType::Lib(name) => cmd.arg("--lib").arg(name), ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
@@ -239,22 +258,30 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
} }
} }
if !skip_assets {
process_assets(config)?;
}
Ok(BuildResult { Ok(BuildResult {
warnings: warning_messages, warnings: warning_messages,
elapsed_time: t_start.elapsed().as_millis(), elapsed_time: t_start.elapsed().as_millis(),
}) })
} }
pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResult> { pub fn build_desktop(
config: &CrateConfig,
_is_serve: bool,
skip_assets: bool,
) -> Result<BuildResult> {
log::info!("🚅 Running build [Desktop] command..."); log::info!("🚅 Running build [Desktop] command...");
let t_start = std::time::Instant::now(); let t_start = std::time::Instant::now();
let ignore_files = build_assets(config)?; let ignore_files = build_assets(config)?;
let mut cmd = subprocess::Exec::cmd("cargo") let mut cmd = subprocess::Exec::cmd("cargo")
.env("CARGO_TARGET_DIR", &config.target_dir)
.cwd(&config.crate_dir) .cwd(&config.crate_dir)
.arg("build") .arg("build")
.arg("--quiet")
.arg("--message-format=json"); .arg("--message-format=json");
if config.release { if config.release {
@@ -262,6 +289,8 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
} }
if config.verbose { if config.verbose {
cmd = cmd.arg("--verbose"); cmd = cmd.arg("--verbose");
} else {
cmd = cmd.arg("--quiet");
} }
if config.custom_profile.is_some() { if config.custom_profile.is_some() {
@@ -274,6 +303,14 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
cmd = cmd.arg("--features").arg(features_str); cmd = cmd.arg("--features").arg(features_str);
} }
if let Some(target) = &config.target {
cmd = cmd.arg("--target").arg(target);
}
let target_platform = config.target.as_deref().unwrap_or("");
cmd = cmd.args(&config.cargo_args);
let cmd = match &config.executable { let cmd = match &config.executable {
crate::ExecutableType::Binary(name) => cmd.arg("--bin").arg(name), crate::ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name), crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
@@ -291,12 +328,17 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
let mut res_path = match &config.executable { let mut res_path = match &config.executable {
crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => { crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => {
file_name = name.clone(); file_name = name.clone();
config.target_dir.join(release_type).join(name) config
.target_dir
.join(target_platform)
.join(release_type)
.join(name)
} }
crate::ExecutableType::Example(name) => { crate::ExecutableType::Example(name) => {
file_name = name.clone(); file_name = name.clone();
config config
.target_dir .target_dir
.join(target_platform)
.join(release_type) .join(release_type)
.join("examples") .join("examples")
.join(name) .join(name)
@@ -348,6 +390,13 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
} }
} }
if !skip_assets {
// Collect assets
process_assets(config)?;
// Create the __assets_head.html file for bundling
create_assets_head(config)?;
}
log::info!( log::info!(
"🚩 Build completed: [./{}]", "🚩 Build completed: [./{}]",
config config
@@ -367,11 +416,19 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
}) })
} }
fn create_assets_head(config: &CrateConfig) -> Result<()> {
let manifest = config.asset_manifest();
let mut file = File::create(config.out_dir.join("__assets_head.html"))?;
file.write_all(manifest.head().as_bytes())?;
Ok(())
}
fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result<Vec<Diagnostic>> { fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result<Vec<Diagnostic>> {
let mut warning_messages: Vec<Diagnostic> = vec![]; let mut warning_messages: Vec<Diagnostic> = vec![];
let pb = ProgressBar::new_spinner(); let mut pb = ProgressBar::new_spinner();
pb.enable_steady_tick(Duration::from_millis(200)); pb.enable_steady_tick(Duration::from_millis(200));
pb = PROGRESS_BARS.add(pb);
pb.set_style( pb.set_style(
ProgressStyle::with_template("{spinner:.dim.bold} {wide_msg}") ProgressStyle::with_template("{spinner:.dim.bold} {wide_msg}")
.unwrap() .unwrap()
@@ -379,14 +436,6 @@ fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result<Vec<Diagnostic>> {
); );
pb.set_message("💼 Waiting to start build the project..."); pb.set_message("💼 Waiting to start build the project...");
struct StopSpinOnDrop(ProgressBar);
impl Drop for StopSpinOnDrop {
fn drop(&mut self) {
self.0.finish_and_clear();
}
}
let stdout = cmd.detached().stream_stdout()?; let stdout = cmd.detached().stream_stdout()?;
let reader = std::io::BufReader::new(stdout); let reader = std::io::BufReader::new(stdout);
@@ -422,13 +471,17 @@ fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result<Vec<Diagnostic>> {
std::process::exit(1); std::process::exit(1);
} }
} }
_ => (), // Unknown message _ => {
// Unknown message
}
} }
} }
Ok(warning_messages) Ok(warning_messages)
} }
pub fn gen_page(config: &DioxusConfig, serve: bool) -> String { pub fn gen_page(config: &CrateConfig, serve: bool, skip_assets: bool) -> String {
let _gaurd = WebAssetConfigDropGuard::new();
let crate_root = crate::cargo::crate_root().unwrap(); let crate_root = crate::cargo::crate_root().unwrap();
let custom_html_file = crate_root.join("index.html"); let custom_html_file = crate_root.join("index.html");
let mut html = if custom_html_file.is_file() { let mut html = if custom_html_file.is_file() {
@@ -443,7 +496,7 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
String::from(include_str!("./assets/index.html")) String::from(include_str!("./assets/index.html"))
}; };
let resources = config.web.resource.clone(); let resources = config.dioxus_config.web.resource.clone();
let mut style_list = resources.style.unwrap_or_default(); let mut style_list = resources.style.unwrap_or_default();
let mut script_list = resources.script.unwrap_or_default(); let mut script_list = resources.script.unwrap_or_default();
@@ -463,6 +516,7 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
)) ))
} }
if config if config
.dioxus_config
.application .application
.tools .tools
.clone() .clone()
@@ -471,6 +525,10 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
{ {
style_str.push_str("<link rel=\"stylesheet\" href=\"/{base_path}/tailwind.css\">\n"); style_str.push_str("<link rel=\"stylesheet\" href=\"/{base_path}/tailwind.css\">\n");
} }
if !skip_assets {
let manifest = config.asset_manifest();
style_str.push_str(&manifest.head());
}
replace_or_insert_before("{style_include}", &style_str, "</head", &mut html); replace_or_insert_before("{style_include}", &style_str, "</head", &mut html);
@@ -491,11 +549,11 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
); );
} }
let base_path = match &config.web.app.base_path { let base_path = match &config.dioxus_config.web.app.base_path {
Some(path) => path, Some(path) => path,
None => ".", None => ".",
}; };
let app_name = &config.application.name; let app_name = &config.dioxus_config.application.name;
// Check if a script already exists // Check if a script already exists
if html.contains("{app_name}") && html.contains("{base_path}") { if html.contains("{app_name}") && html.contains("{base_path}") {
html = html.replace("{app_name}", app_name); html = html.replace("{app_name}", app_name);
@@ -520,6 +578,7 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
} }
let title = config let title = config
.dioxus_config
.web .web
.app .app
.title .title
@@ -689,3 +748,42 @@ fn build_assets(config: &CrateConfig) -> Result<Vec<PathBuf>> {
Ok(result) Ok(result)
} }
/// Process any assets collected from the binary
fn process_assets(config: &CrateConfig) -> anyhow::Result<()> {
let manifest = config.asset_manifest();
let static_asset_output_dir = PathBuf::from(
config
.dioxus_config
.web
.app
.base_path
.clone()
.unwrap_or_default(),
);
let static_asset_output_dir = config.out_dir.join(static_asset_output_dir);
manifest.copy_static_assets_to(static_asset_output_dir)?;
Ok(())
}
pub(crate) struct WebAssetConfigDropGuard;
impl WebAssetConfigDropGuard {
pub fn new() -> Self {
// Set up the collect asset config
manganis_cli_support::Config::default()
.with_assets_serve_location("/")
.save();
Self {}
}
}
impl Drop for WebAssetConfigDropGuard {
fn drop(&mut self) {
// Reset the config
manganis_cli_support::Config::default().save();
}
}
+147 -76
View File
@@ -1,3 +1,4 @@
use dioxus_autofmt::{IndentOptions, IndentType};
use futures::{stream::FuturesUnordered, StreamExt}; use futures::{stream::FuturesUnordered, StreamExt};
use std::{fs, path::Path, process::exit}; use std::{fs, path::Path, process::exit};
@@ -26,16 +27,19 @@ pub struct Autoformat {
impl Autoformat { impl Autoformat {
// Todo: autoformat the entire crate // Todo: autoformat the entire crate
pub async fn autoformat(self) -> Result<()> { pub async fn autoformat(self) -> Result<()> {
let Autoformat { check, raw, file } = self;
// Default to formatting the project // Default to formatting the project
if self.raw.is_none() && self.file.is_none() { if raw.is_none() && file.is_none() {
if let Err(e) = autoformat_project(self.check).await { if let Err(e) = autoformat_project(check).await {
eprintln!("error formatting project: {}", e); eprintln!("error formatting project: {}", e);
exit(1); exit(1);
} }
} }
if let Some(raw) = self.raw { if let Some(raw) = raw {
if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0) { let indent = indentation_for(".")?;
if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0, indent) {
println!("{}", inner); println!("{}", inner);
} else { } else {
// exit process with error // exit process with error
@@ -45,43 +49,90 @@ impl Autoformat {
} }
// Format single file // Format single file
if let Some(file) = self.file { if let Some(file) = file {
let file_content = if file == "-" { refactor_file(file)?;
let mut contents = String::new();
std::io::stdin().read_to_string(&mut contents)?;
Ok(contents)
} else {
fs::read_to_string(&file)
};
match file_content {
Ok(s) => {
let edits = dioxus_autofmt::fmt_file(&s);
let out = dioxus_autofmt::apply_formats(&s, edits);
if file == "-" {
print!("{}", out);
} else {
match fs::write(&file, out) {
Ok(_) => {
println!("formatted {}", file);
}
Err(e) => {
eprintln!("failed to write formatted content to file: {}", e);
}
}
}
}
Err(e) => {
eprintln!("failed to open file: {}", e);
exit(1);
}
}
} }
Ok(()) Ok(())
} }
} }
fn refactor_file(file: String) -> Result<(), Error> {
let indent = indentation_for(".")?;
let file_content = if file == "-" {
let mut contents = String::new();
std::io::stdin().read_to_string(&mut contents)?;
Ok(contents)
} else {
fs::read_to_string(&file)
};
let Ok(s) = file_content else {
eprintln!("failed to open file: {}", file_content.unwrap_err());
exit(1);
};
let edits = dioxus_autofmt::fmt_file(&s, indent);
let out = dioxus_autofmt::apply_formats(&s, edits);
if file == "-" {
print!("{}", out);
} else if let Err(e) = fs::write(&file, out) {
eprintln!("failed to write formatted content to file: {e}",);
} else {
println!("formatted {}", file);
}
Ok(())
}
fn get_project_files(config: &CrateConfig) -> Vec<PathBuf> {
let mut files = vec![];
let gitignore_path = config.crate_dir.join(".gitignore");
if gitignore_path.is_file() {
let gitigno = gitignore::File::new(gitignore_path.as_path()).unwrap();
if let Ok(git_files) = gitigno.included_files() {
let git_files = git_files
.into_iter()
.filter(|f| f.ends_with(".rs") && !is_target_dir(f));
files.extend(git_files)
};
} else {
collect_rs_files(&config.crate_dir, &mut files);
}
files
}
fn is_target_dir(file: &Path) -> bool {
let stripped = if let Ok(cwd) = std::env::current_dir() {
file.strip_prefix(cwd).unwrap_or(file)
} else {
file
};
if let Some(first) = stripped.components().next() {
first.as_os_str() == "target"
} else {
false
}
}
async fn format_file(
path: impl AsRef<Path>,
indent: IndentOptions,
) -> Result<usize, tokio::io::Error> {
let contents = tokio::fs::read_to_string(&path).await?;
let edits = dioxus_autofmt::fmt_file(&contents, indent);
let len = edits.len();
if !edits.is_empty() {
let out = dioxus_autofmt::apply_formats(&contents, edits);
tokio::fs::write(path, out).await?;
}
Ok(len)
}
/// Read every .rs file accessible when considering the .gitignore and try to format it /// Read every .rs file accessible when considering the .gitignore and try to format it
/// ///
/// Runs using Tokio for multithreading, so it should be really really fast /// Runs using Tokio for multithreading, so it should be really really fast
@@ -90,42 +141,27 @@ impl Autoformat {
async fn autoformat_project(check: bool) -> Result<()> { async fn autoformat_project(check: bool) -> Result<()> {
let crate_config = crate::CrateConfig::new(None)?; let crate_config = crate::CrateConfig::new(None)?;
let mut files_to_format = vec![]; let files_to_format = get_project_files(&crate_config);
collect_rs_files(&crate_config.crate_dir, &mut files_to_format);
if files_to_format.is_empty() {
return Ok(());
}
let indent = indentation_for(&files_to_format[0])?;
let counts = files_to_format let counts = files_to_format
.into_iter() .into_iter()
.filter(|file| {
if file.components().any(|f| f.as_os_str() == "target") {
return false;
}
true
})
.map(|path| async { .map(|path| async {
let _path = path.clone(); let path_clone = path.clone();
let res = tokio::spawn(async move { let res = tokio::spawn(format_file(path, indent.clone())).await;
let contents = tokio::fs::read_to_string(&path).await?;
let edits = dioxus_autofmt::fmt_file(&contents);
let len = edits.len();
if !edits.is_empty() {
let out = dioxus_autofmt::apply_formats(&contents, edits);
tokio::fs::write(&path, out).await?;
}
Ok(len) as Result<usize, tokio::io::Error>
})
.await;
match res { match res {
Err(err) => { Err(err) => {
eprintln!("error formatting file: {}\n{err}", _path.display()); eprintln!("error formatting file: {}\n{err}", path_clone.display());
None None
} }
Ok(Err(err)) => { Ok(Err(err)) => {
eprintln!("error formatting file: {}\n{err}", _path.display()); eprintln!("error formatting file: {}\n{err}", path_clone.display());
None None
} }
Ok(Ok(res)) => Some(res), Ok(Ok(res)) => Some(res),
@@ -135,13 +171,7 @@ async fn autoformat_project(check: bool) -> Result<()> {
.collect::<Vec<_>>() .collect::<Vec<_>>()
.await; .await;
let files_formatted: usize = counts let files_formatted: usize = counts.into_iter().flatten().sum();
.into_iter()
.map(|f| match f {
Some(res) => res,
_ => 0,
})
.sum();
if files_formatted > 0 && check { if files_formatted > 0 && check {
eprintln!("{} files needed formatting", files_formatted); eprintln!("{} files needed formatting", files_formatted);
@@ -151,26 +181,67 @@ async fn autoformat_project(check: bool) -> Result<()> {
Ok(()) Ok(())
} }
fn collect_rs_files(folder: &Path, files: &mut Vec<PathBuf>) { fn indentation_for(file_or_dir: impl AsRef<Path>) -> Result<IndentOptions> {
let Ok(folder) = folder.read_dir() else { let out = std::process::Command::new("cargo")
.args(["fmt", "--", "--print-config", "current"])
.arg(file_or_dir.as_ref())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::inherit())
.output()?;
if !out.status.success() {
return Err(Error::CargoError("cargo fmt failed".into()));
}
let config = String::from_utf8_lossy(&out.stdout);
let hard_tabs = config
.lines()
.find(|line| line.starts_with("hard_tabs "))
.and_then(|line| line.split_once('='))
.map(|(_, value)| value.trim() == "true")
.ok_or_else(|| {
Error::RuntimeError("Could not find hard_tabs option in rustfmt config".into())
})?;
let tab_spaces = config
.lines()
.find(|line| line.starts_with("tab_spaces "))
.and_then(|line| line.split_once('='))
.map(|(_, value)| value.trim().parse::<usize>())
.ok_or_else(|| {
Error::RuntimeError("Could not find tab_spaces option in rustfmt config".into())
})?
.map_err(|_| {
Error::RuntimeError("Could not parse tab_spaces option in rustfmt config".into())
})?;
Ok(IndentOptions::new(
if hard_tabs {
IndentType::Tabs
} else {
IndentType::Spaces
},
tab_spaces,
))
}
fn collect_rs_files(folder: &impl AsRef<Path>, files: &mut Vec<PathBuf>) {
if is_target_dir(folder.as_ref()) {
return;
}
let Ok(folder) = folder.as_ref().read_dir() else {
return; return;
}; };
// load the gitignore // load the gitignore
for entry in folder { for entry in folder {
let Ok(entry) = entry else { let Ok(entry) = entry else {
continue; continue;
}; };
let path = entry.path(); let path = entry.path();
if path.is_dir() { if path.is_dir() {
collect_rs_files(&path, files); collect_rs_files(&path, files);
} }
if let Some(ext) = path.extension() { if let Some(ext) = path.extension() {
if ext == "rs" { if ext == "rs" && !is_target_dir(&path) {
files.push(path); files.push(path);
} }
} }
+54 -12
View File
@@ -1,6 +1,8 @@
use crate::cfg::Platform;
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
use crate::plugin::PluginManager; use crate::plugin::PluginManager;
use crate::server::fullstack::FullstackServerEnvGuard;
use crate::server::fullstack::FullstackWebEnvGuard;
use crate::{cfg::Platform, WebAssetConfigDropGuard};
use super::*; use super::*;
@@ -13,23 +15,26 @@ pub struct Build {
} }
impl Build { impl Build {
pub fn build(self, bin: Option<PathBuf>) -> Result<()> { pub fn build(self, bin: Option<PathBuf>, target_dir: Option<&std::path::Path>) -> Result<()> {
let mut crate_config = crate::CrateConfig::new(bin)?; let mut crate_config = crate::CrateConfig::new(bin)?;
if let Some(target_dir) = target_dir {
crate_config.target_dir = target_dir.to_path_buf();
}
// change the release state. // change the release state.
crate_config.with_release(self.build.release); crate_config.with_release(self.build.release);
crate_config.with_verbose(self.build.verbose); crate_config.with_verbose(self.build.verbose);
if self.build.example.is_some() { if self.build.example.is_some() {
crate_config.as_example(self.build.example.unwrap()); crate_config.as_example(self.build.example.clone().unwrap());
} }
if self.build.profile.is_some() { if self.build.profile.is_some() {
crate_config.set_profile(self.build.profile.unwrap()); crate_config.set_profile(self.build.profile.clone().unwrap());
} }
if self.build.features.is_some() { if self.build.features.is_some() {
crate_config.set_features(self.build.features.unwrap()); crate_config.set_features(self.build.features.clone().unwrap());
} }
let platform = self let platform = self
@@ -37,19 +42,56 @@ impl Build {
.platform .platform
.unwrap_or(crate_config.dioxus_config.application.default_platform); .unwrap_or(crate_config.dioxus_config.application.default_platform);
#[cfg(feature = "plugin")] if let Some(target) = self.build.target.clone() {
let _ = PluginManager::on_build_start(&crate_config, &platform); crate_config.set_target(target);
}
crate_config.set_cargo_args(self.build.cargo_args.clone());
// #[cfg(feature = "plugin")]
// let _ = PluginManager::on_build_start(&crate_config, &platform);
match platform { match platform {
Platform::Web => { Platform::Web => {
crate::builder::build(&crate_config, true)?; crate::builder::build(&crate_config, false, self.build.skip_assets)?;
} }
Platform::Desktop => { Platform::Desktop => {
crate::builder::build_desktop(&crate_config, false)?; crate::builder::build_desktop(&crate_config, false, self.build.skip_assets)?;
}
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();
{
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 {
Some(features) => {
features.push(web_feature);
}
None => web_config.features = Some(vec![web_feature]),
};
crate::builder::build(&crate_config, false, self.build.skip_assets)?;
}
{
let mut desktop_config = crate_config.clone();
let desktop_feature = self.build.server_feature;
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.build.force_debug, self.build.release);
crate::builder::build_desktop(&desktop_config, false, self.build.skip_assets)?;
}
} }
} }
let temp = gen_page(&crate_config.dioxus_config, false); let temp = gen_page(&crate_config, false, self.build.skip_assets);
let mut file = std::fs::File::create( let mut file = std::fs::File::create(
crate_config crate_config
@@ -66,8 +108,8 @@ impl Build {
)?; )?;
file.write_all(temp.as_bytes())?; file.write_all(temp.as_bytes())?;
#[cfg(feature = "plugin")] // #[cfg(feature = "plugin")]
let _ = PluginManager::on_build_finish(&crate_config, &platform); // let _ = PluginManager::on_build_finish(&crate_config, &platform);
Ok(()) Ok(())
} }
+27 -3
View File
@@ -76,8 +76,14 @@ impl Bundle {
crate_config.set_profile(self.build.profile.unwrap()); crate_config.set_profile(self.build.profile.unwrap());
} }
if let Some(target) = &self.build.target {
crate_config.set_target(target.to_string());
}
crate_config.set_cargo_args(self.build.cargo_args);
// build the desktop app // build the desktop app
build_desktop(&crate_config, false)?; build_desktop(&crate_config, false, false)?;
// copy the binary to the out dir // copy the binary to the out dir
let package = crate_config.manifest.package.unwrap(); let package = crate_config.manifest.package.unwrap();
@@ -128,6 +134,19 @@ impl Bundle {
} }
} }
// Add all assets from collect assets to the bundle
{
let config = manganis_cli_support::Config::current();
let location = config.assets_serve_location().to_string();
let location = format!("./{}", location);
println!("Adding assets from {} to bundle", location);
if let Some(resources) = &mut bundle_settings.resources {
resources.push(location);
} else {
bundle_settings.resources = Some(vec![location]);
}
}
let mut settings = SettingsBuilder::new() let mut settings = SettingsBuilder::new()
.project_out_directory(crate_config.out_dir) .project_out_directory(crate_config.out_dir)
.package_settings(PackageSettings { .package_settings(PackageSettings {
@@ -148,6 +167,11 @@ impl Bundle {
.collect(), .collect(),
); );
} }
if let Some(target) = &self.build.target {
settings = settings.target(target.to_string());
}
let settings = settings.build(); let settings = settings.build();
// on macos we need to set CI=true (https://github.com/tauri-apps/tauri/issues/2567) // on macos we need to set CI=true (https://github.com/tauri-apps/tauri/issues/2567)
@@ -156,9 +180,9 @@ impl Bundle {
tauri_bundler::bundle::bundle_project(settings.unwrap()).unwrap_or_else(|err|{ tauri_bundler::bundle::bundle_project(settings.unwrap()).unwrap_or_else(|err|{
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
panic!("Failed to bundle project: {}\nMake sure you have automation enabled in your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208) and full disk access enabled for your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208)", err); panic!("Failed to bundle project: {:#?}\nMake sure you have automation enabled in your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208) and full disk access enabled for your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208)", err);
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
panic!("Failed to bundle project: {}", err); panic!("Failed to bundle project: {:#?}", err);
}); });
Ok(()) Ok(())
+83 -9
View File
@@ -6,15 +6,16 @@ use super::*;
/// Config options for the build system. /// Config options for the build system.
#[derive(Clone, Debug, Default, Deserialize, Parser)] #[derive(Clone, Debug, Default, Deserialize, Parser)]
pub struct ConfigOptsBuild { pub struct ConfigOptsBuild {
/// The index HTML file to drive the bundling process [default: index.html]
#[arg(long)]
pub target: Option<PathBuf>,
/// Build in release mode [default: false] /// Build in release mode [default: false]
#[clap(long)] #[clap(long)]
#[serde(default)] #[serde(default)]
pub release: bool, pub release: bool,
/// This flag only applies to fullstack builds. By default fullstack builds will run with something in between debug and release mode. This flag will force the build to run in debug mode. [default: false]
#[clap(long)]
#[serde(default)]
pub force_debug: bool,
// Use verbose output [default: false] // Use verbose output [default: false]
#[clap(long)] #[clap(long)]
#[serde(default)] #[serde(default)]
@@ -32,17 +33,53 @@ pub struct ConfigOptsBuild {
#[clap(long, value_enum)] #[clap(long, value_enum)]
pub platform: Option<Platform>, pub platform: Option<Platform>,
/// Skip collecting assets from dependencies [default: false]
#[clap(long)]
#[serde(default)]
pub skip_assets: bool,
/// Space separated list of features to activate /// Space separated list of features to activate
#[clap(long)] #[clap(long)]
pub features: Option<Vec<String>>, pub features: Option<Vec<String>>,
/// The feature to use for the client in a fullstack app [default: "web"]
#[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() })]
pub server_feature: String,
/// Rustc platform triple
#[clap(long)]
pub target: Option<String>,
/// Extra arguments passed to cargo build
#[clap(last = true)]
pub cargo_args: Vec<String>,
}
impl From<ConfigOptsServe> for ConfigOptsBuild {
fn from(serve: ConfigOptsServe) -> Self {
Self {
target: serve.target,
release: serve.release,
verbose: serve.verbose,
example: serve.example,
profile: serve.profile,
platform: serve.platform,
features: serve.features,
client_feature: serve.client_feature,
server_feature: serve.server_feature,
skip_assets: serve.skip_assets,
force_debug: serve.force_debug,
cargo_args: serve.cargo_args,
}
}
} }
#[derive(Clone, Debug, Default, Deserialize, Parser)] #[derive(Clone, Debug, Default, Deserialize, Parser)]
pub struct ConfigOptsServe { pub struct ConfigOptsServe {
/// The index HTML file to drive the bundling process [default: index.html]
#[arg(short, long)]
pub target: Option<PathBuf>,
/// Port of dev server /// Port of dev server
#[clap(long)] #[clap(long)]
#[clap(default_value_t = 8080)] #[clap(default_value_t = 8080)]
@@ -62,6 +99,11 @@ pub struct ConfigOptsServe {
#[serde(default)] #[serde(default)]
pub release: bool, pub release: bool,
/// This flag only applies to fullstack builds. By default fullstack builds will run with something in between debug and release mode. This flag will force the build to run in debug mode. [default: false]
#[clap(long)]
#[serde(default)]
pub force_debug: bool,
// Use verbose output [default: false] // Use verbose output [default: false]
#[clap(long)] #[clap(long)]
#[serde(default)] #[serde(default)]
@@ -71,7 +113,7 @@ pub struct ConfigOptsServe {
#[clap(long)] #[clap(long)]
pub profile: Option<String>, pub profile: Option<String>,
/// Build platform: support Web & Desktop [default: "default_platform"] /// Build platform: support Web, Desktop, and Fullstack [default: "default_platform"]
#[clap(long, value_enum)] #[clap(long, value_enum)]
pub platform: Option<Platform>, pub platform: Option<Platform>,
@@ -89,6 +131,27 @@ pub struct ConfigOptsServe {
/// Space separated list of features to activate /// Space separated list of features to activate
#[clap(long)] #[clap(long)]
pub features: Option<Vec<String>>, pub features: Option<Vec<String>>,
/// Skip collecting assets from dependencies [default: false]
#[clap(long)]
#[serde(default)]
pub skip_assets: bool,
/// The feature to use for the client in a fullstack app [default: "web"]
#[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() })]
pub server_feature: String,
/// Rustc platform triple
#[clap(long)]
pub target: Option<String>,
/// Extra arguments passed to cargo build
#[clap(last = true)]
pub cargo_args: Vec<String>,
} }
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize, Debug)] #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize, Debug)]
@@ -99,6 +162,9 @@ pub enum Platform {
#[clap(name = "desktop")] #[clap(name = "desktop")]
#[serde(rename = "desktop")] #[serde(rename = "desktop")]
Desktop, Desktop,
#[clap(name = "fullstack")]
#[serde(rename = "fullstack")]
Fullstack,
} }
/// Config options for the bundling system. /// Config options for the bundling system.
@@ -129,4 +195,12 @@ pub struct ConfigOptsBundle {
/// Space separated list of features to activate /// Space separated list of features to activate
#[clap(long)] #[clap(long)]
pub features: Option<Vec<String>>, pub features: Option<Vec<String>>,
/// Rustc platform triple
#[clap(long)]
pub target: Option<String>,
/// Extra arguments passed to cargo build
#[clap(last = true)]
pub cargo_args: Vec<String>,
} }
+6
View File
@@ -28,6 +28,12 @@ impl Clean {
remove_dir_all(crate_config.crate_dir.join(&out_dir))?; remove_dir_all(crate_config.crate_dir.join(&out_dir))?;
} }
let fullstack_out_dir = crate_config.crate_dir.join(".dioxus");
if fullstack_out_dir.is_dir() {
remove_dir_all(fullstack_out_dir)?;
}
Ok(()) Ok(())
} }
} }
+21 -6
View File
@@ -12,6 +12,7 @@ pub struct Serve {
impl Serve { impl Serve {
pub async fn serve(self, bin: Option<PathBuf>) -> Result<()> { pub async fn serve(self, bin: Option<PathBuf>) -> Result<()> {
let mut crate_config = crate::CrateConfig::new(bin)?; let mut crate_config = crate::CrateConfig::new(bin)?;
let serve_cfg = self.serve.clone();
// change the relase state. // change the relase state.
crate_config.with_hot_reload(self.serve.hot_reload); crate_config.with_hot_reload(self.serve.hot_reload);
@@ -34,6 +35,12 @@ impl Serve {
// Subdirectories don't work with the server // Subdirectories don't work with the server
crate_config.dioxus_config.web.app.base_path = None; crate_config.dioxus_config.web.app.base_path = None;
if let Some(target) = self.serve.target {
crate_config.set_target(target);
}
crate_config.set_cargo_args(self.serve.cargo_args);
let platform = self let platform = self
.serve .serve
.platform .platform
@@ -42,21 +49,29 @@ impl Serve {
match platform { match platform {
cfg::Platform::Web => { cfg::Platform::Web => {
// generate dev-index page // generate dev-index page
Serve::regen_dev_page(&crate_config)?; Serve::regen_dev_page(&crate_config, self.serve.skip_assets)?;
// start the develop server // start the develop server
server::web::startup(self.serve.port, crate_config.clone(), self.serve.open) server::web::startup(
.await?; self.serve.port,
crate_config.clone(),
self.serve.open,
self.serve.skip_assets,
)
.await?;
} }
cfg::Platform::Desktop => { cfg::Platform::Desktop => {
server::desktop::startup(crate_config.clone()).await?; server::desktop::startup(crate_config.clone(), &serve_cfg).await?;
}
cfg::Platform::Fullstack => {
server::fullstack::startup(crate_config.clone(), &serve_cfg).await?;
} }
} }
Ok(()) Ok(())
} }
pub fn regen_dev_page(crate_config: &CrateConfig) -> Result<()> { pub fn regen_dev_page(crate_config: &CrateConfig, skip_assets: bool) -> Result<()> {
let serve_html = gen_page(&crate_config.dioxus_config, true); let serve_html = gen_page(crate_config, true, skip_assets);
let dist_path = crate_config.crate_dir.join( let dist_path = crate_config.crate_dir.join(
crate_config crate_config
+25
View File
@@ -1,4 +1,6 @@
use crate::{cfg::Platform, error::Result}; use crate::{cfg::Platform, error::Result};
use manganis_cli_support::AssetManifest;
use manganis_cli_support::AssetManifestExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
collections::HashMap, collections::HashMap,
@@ -211,6 +213,8 @@ pub struct CrateConfig {
pub verbose: bool, pub verbose: bool,
pub custom_profile: Option<String>, pub custom_profile: Option<String>,
pub features: Option<Vec<String>>, pub features: Option<Vec<String>>,
pub target: Option<String>,
pub cargo_args: Vec<String>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -278,6 +282,8 @@ impl CrateConfig {
let verbose = false; let verbose = false;
let custom_profile = None; let custom_profile = None;
let features = None; let features = None;
let target = None;
let cargo_args = vec![];
Ok(Self { Ok(Self {
out_dir, out_dir,
@@ -294,9 +300,18 @@ impl CrateConfig {
custom_profile, custom_profile,
features, features,
verbose, verbose,
target,
cargo_args,
}) })
} }
pub fn asset_manifest(&self) -> AssetManifest {
AssetManifest::load_from_path(
self.crate_dir.join("Cargo.toml"),
self.workspace_dir.join("Cargo.lock"),
)
}
pub fn as_example(&mut self, example_name: String) -> &mut Self { pub fn as_example(&mut self, example_name: String) -> &mut Self {
self.executable = ExecutableType::Example(example_name); self.executable = ExecutableType::Example(example_name);
self self
@@ -331,6 +346,16 @@ impl CrateConfig {
self.features = Some(features); self.features = Some(features);
self self
} }
pub fn set_target(&mut self, target: String) -> &mut Self {
self.target = Some(target);
self
}
pub fn set_cargo_args(&mut self, cargo_args: Vec<String>) -> &mut Self {
self.cargo_args = cargo_args;
self
}
} }
#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]
+13 -1
View File
@@ -28,7 +28,19 @@ pub fn set_up_logging() {
message = message, message = message,
)); ));
}) })
.level(log::LevelFilter::Info) .level(match std::env::var("DIOXUS_LOG") {
Ok(level) => match level.to_lowercase().as_str() {
"error" => log::LevelFilter::Error,
"warn" => log::LevelFilter::Warn,
"info" => log::LevelFilter::Info,
"debug" => log::LevelFilter::Debug,
"trace" => log::LevelFilter::Trace,
_ => {
panic!("Invalid log level: {}", level)
}
},
Err(_) => log::LevelFilter::Info,
})
.chain(std::io::stdout()) .chain(std::io::stdout())
.apply() .apply()
.unwrap(); .unwrap();
+16 -13
View File
@@ -42,34 +42,36 @@ async fn main() -> anyhow::Result<()> {
set_up_logging(); set_up_logging();
let bin = get_bin(args.bin)?; let bin = get_bin(args.bin);
let _dioxus_config = DioxusConfig::load(Some(bin.clone())) if let Ok(bin) = &bin {
let _dioxus_config = DioxusConfig::load(Some(bin.clone()))
.map_err(|e| anyhow!("Failed to load Dioxus config because: {e}"))? .map_err(|e| anyhow!("Failed to load Dioxus config because: {e}"))?
.unwrap_or_else(|| { .unwrap_or_else(|| {
log::warn!("You appear to be creating a Dioxus project from scratch; we will use the default config"); log::warn!("You appear to be creating a Dioxus project from scratch; we will use the default config");
DioxusConfig::default() DioxusConfig::default()
}); });
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
PluginManager::init(_dioxus_config.plugin) PluginManager::init(_dioxus_config.plugin)
.map_err(|e| anyhow!("🚫 Plugin system initialization failed: {e}"))?; .map_err(|e| anyhow!("🚫 Plugin system initialization failed: {e}"))?;
}
match args.action { match args.action {
Translate(opts) => opts Translate(opts) => opts
.translate() .translate()
.map_err(|e| anyhow!("🚫 Translation of HTML into RSX failed: {}", e)), .map_err(|e| anyhow!("🚫 Translation of HTML into RSX failed: {}", e)),
Build(opts) => opts Build(opts) if bin.is_ok() => opts
.build(Some(bin.clone())) .build(Some(bin.unwrap().clone()), None)
.map_err(|e| anyhow!("🚫 Building project failed: {}", e)), .map_err(|e| anyhow!("🚫 Building project failed: {}", e)),
Clean(opts) => opts Clean(opts) if bin.is_ok() => opts
.clean(Some(bin.clone())) .clean(Some(bin.unwrap().clone()))
.map_err(|e| anyhow!("🚫 Cleaning project failed: {}", e)), .map_err(|e| anyhow!("🚫 Cleaning project failed: {}", e)),
Serve(opts) => opts Serve(opts) if bin.is_ok() => opts
.serve(Some(bin.clone())) .serve(Some(bin.unwrap().clone()))
.await .await
.map_err(|e| anyhow!("🚫 Serving project failed: {}", e)), .map_err(|e| anyhow!("🚫 Serving project failed: {}", e)),
@@ -81,8 +83,8 @@ async fn main() -> anyhow::Result<()> {
.config() .config()
.map_err(|e| anyhow!("🚫 Configuring new project failed: {}", e)), .map_err(|e| anyhow!("🚫 Configuring new project failed: {}", e)),
Bundle(opts) => opts Bundle(opts) if bin.is_ok() => opts
.bundle(Some(bin.clone())) .bundle(Some(bin.unwrap().clone()))
.map_err(|e| anyhow!("🚫 Bundling project failed: {}", e)), .map_err(|e| anyhow!("🚫 Bundling project failed: {}", e)),
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
@@ -107,5 +109,6 @@ async fn main() -> anyhow::Result<()> {
Ok(()) Ok(())
} }
_ => Err(anyhow::anyhow!(bin.unwrap_err())),
} }
} }
+90 -41
View File
@@ -1,11 +1,12 @@
use crate::server::Platform;
use crate::{ use crate::{
cfg::ConfigOptsServe,
server::{ server::{
output::{print_console_info, PrettierOptions}, output::{print_console_info, PrettierOptions},
setup_file_watcher, setup_file_watcher,
}, },
BuildResult, CrateConfig, Result, BuildResult, CrateConfig, Result,
}; };
use dioxus_hot_reload::HotReloadMsg; use dioxus_hot_reload::HotReloadMsg;
use dioxus_html::HtmlCtx; use dioxus_html::HtmlCtx;
use dioxus_rsx::hot_reload::*; use dioxus_rsx::hot_reload::*;
@@ -21,7 +22,14 @@ use plugin::PluginManager;
use super::HotReloadState; use super::HotReloadState;
pub async fn startup(config: CrateConfig) -> Result<()> { pub async fn startup(config: CrateConfig, serve: &ConfigOptsServe) -> Result<()> {
startup_with_platform::<DesktopPlatform>(config, serve).await
}
pub(crate) async fn startup_with_platform<P: Platform + Send + 'static>(
config: CrateConfig,
serve_cfg: &ConfigOptsServe,
) -> Result<()> {
// ctrl-c shutdown checker // ctrl-c shutdown checker
let _crate_config = config.clone(); let _crate_config = config.clone();
let _ = ctrlc::set_handler(move || { let _ = ctrlc::set_handler(move || {
@@ -43,8 +51,6 @@ pub async fn startup(config: CrateConfig) -> Result<()> {
let hot_reload_tx = broadcast::channel(100).0; let hot_reload_tx = broadcast::channel(100).0;
clear_paths();
Some(HotReloadState { Some(HotReloadState {
messages: hot_reload_tx.clone(), messages: hot_reload_tx.clone(),
file_map: file_map.clone(), file_map: file_map.clone(),
@@ -53,15 +59,18 @@ pub async fn startup(config: CrateConfig) -> Result<()> {
false => None, false => None,
}; };
serve(config, hot_reload_state).await?; serve::<P>(config, serve_cfg, hot_reload_state).await?;
Ok(()) Ok(())
} }
/// Start the server without hot reload /// Start the server without hot reload
pub async fn serve(config: CrateConfig, hot_reload_state: Option<HotReloadState>) -> Result<()> { async fn serve<P: Platform + Send + 'static>(
let (child, first_build_result) = start_desktop(&config)?; config: CrateConfig,
let currently_running_child: RwLock<Child> = RwLock::new(child); serve: &ConfigOptsServe,
hot_reload_state: Option<HotReloadState>,
) -> Result<()> {
let platform = RwLock::new(P::start(&config, serve)?);
log::info!("🚀 Starting development server..."); log::info!("🚀 Starting development server...");
@@ -70,14 +79,7 @@ pub async fn serve(config: CrateConfig, hot_reload_state: Option<HotReloadState>
let _watcher = setup_file_watcher( let _watcher = setup_file_watcher(
{ {
let config = config.clone(); let config = config.clone();
move || platform.write().unwrap().rebuild(&config)
move || {
let mut current_child = currently_running_child.write().unwrap();
current_child.kill()?;
let (child, result) = start_desktop(&config)?;
*current_child = child;
Ok(result)
}
}, },
&config, &config,
None, None,
@@ -85,19 +87,9 @@ pub async fn serve(config: CrateConfig, hot_reload_state: Option<HotReloadState>
) )
.await?; .await?;
// Print serve info
print_console_info(
&config,
PrettierOptions {
changed: vec![],
warnings: first_build_result.warnings,
elapsed_time: first_build_result.elapsed_time,
},
None,
);
match hot_reload_state { match hot_reload_state {
Some(hot_reload_state) => { Some(hot_reload_state) => {
// The open interprocess sockets
start_desktop_hot_reload(hot_reload_state).await?; start_desktop_hot_reload(hot_reload_state).await?;
} }
None => { None => {
@@ -109,7 +101,14 @@ pub async fn serve(config: CrateConfig, hot_reload_state: Option<HotReloadState>
} }
async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()> { async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()> {
match LocalSocketListener::bind("@dioxusin") { let metadata = cargo_metadata::MetadataCommand::new()
.no_deps()
.exec()
.unwrap();
let target_dir = metadata.target_directory.as_std_path();
let path = target_dir.join("dioxusin");
clear_paths(&path);
match LocalSocketListener::bind(path) {
Ok(local_socket_stream) => { Ok(local_socket_stream) => {
let aborted = Arc::new(Mutex::new(false)); let aborted = Arc::new(Mutex::new(false));
// States // States
@@ -121,9 +120,9 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
let file_map = hot_reload_state.file_map.clone(); let file_map = hot_reload_state.file_map.clone();
let channels = channels.clone(); let channels = channels.clone();
let aborted = aborted.clone(); let aborted = aborted.clone();
let _ = local_socket_stream.set_nonblocking(true);
move || { move || {
loop { loop {
//accept() will block the thread when local_socket_stream is in blocking mode (default)
match local_socket_stream.accept() { match local_socket_stream.accept() {
Ok(mut connection) => { Ok(mut connection) => {
// send any templates than have changed before the socket connected // send any templates than have changed before the socket connected
@@ -148,7 +147,11 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
println!("Connected to hot reloading 🚀"); println!("Connected to hot reloading 🚀");
} }
Err(err) => { Err(err) => {
if err.kind() != std::io::ErrorKind::WouldBlock { let error_string = err.to_string();
// Filter out any error messages about a operation that may block and an error message that triggers on some operating systems that says "Waiting for a process to open the other end of the pipe" without WouldBlock being set
let display_error = err.kind() != std::io::ErrorKind::WouldBlock
&& !error_string.contains("Waiting for a process");
if display_error {
println!("Error connecting to hot reloading: {} (Hot reloading is a feature of the dioxus-cli. If you are not using the CLI, this error can be ignored)", err); println!("Error connecting to hot reloading: {} (Hot reloading is a feature of the dioxus-cli. If you are not using the CLI, this error can be ignored)", err);
} }
} }
@@ -181,17 +184,14 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
Ok(()) Ok(())
} }
fn clear_paths() { fn clear_paths(file_socket_path: &std::path::Path) {
if cfg!(target_os = "macos") { if cfg!(unix) {
// On unix, if you force quit the application, it can leave the file socket open // On unix, if you force quit the application, it can leave the file socket open
// This will cause the local socket listener to fail to open // This will cause the local socket listener to fail to open
// We check if the file socket is already open from an old session and then delete it // We check if the file socket is already open from an old session and then delete it
let paths = ["./dioxusin", "./@dioxusin"];
for path in paths { if file_socket_path.exists() {
let path = std::path::PathBuf::from(path); let _ = std::fs::remove_file(file_socket_path);
if path.exists() {
let _ = std::fs::remove_file(path);
}
} }
} }
} }
@@ -210,9 +210,9 @@ fn send_msg(msg: HotReloadMsg, channel: &mut impl std::io::Write) -> bool {
} }
} }
pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> { fn start_desktop(config: &CrateConfig, skip_assets: bool) -> Result<(RAIIChild, BuildResult)> {
// Run the desktop application // Run the desktop application
let result = crate::builder::build_desktop(config, true)?; let result = crate::builder::build_desktop(config, true, skip_assets)?;
match &config.executable { match &config.executable {
crate::ExecutableType::Binary(name) crate::ExecutableType::Binary(name)
@@ -222,9 +222,58 @@ pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> {
if cfg!(windows) { if cfg!(windows) {
file.set_extension("exe"); file.set_extension("exe");
} }
let child = Command::new(file.to_str().unwrap()).spawn()?; let active = "DIOXUS_ACTIVE";
let child = RAIIChild(
Command::new(file.to_str().unwrap())
.env(active, "true")
.spawn()?,
);
Ok((child, result)) Ok((child, result))
} }
} }
} }
pub(crate) struct DesktopPlatform {
currently_running_child: RAIIChild,
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)?;
log::info!("🚀 Starting development server...");
// Print serve info
print_console_info(
config,
PrettierOptions {
changed: vec![],
warnings: first_build_result.warnings,
elapsed_time: first_build_result.elapsed_time,
},
None,
);
Ok(Self {
currently_running_child: child,
skip_assets: serve.skip_assets,
})
}
fn rebuild(&mut self, config: &CrateConfig) -> Result<BuildResult> {
self.currently_running_child.0.kill()?;
let (child, result) = start_desktop(config, self.skip_assets)?;
self.currently_running_child = child;
Ok(result)
}
}
struct RAIIChild(Child);
impl Drop for RAIIChild {
fn drop(&mut self) {
let _ = self.0.kill();
}
}
+161
View File
@@ -0,0 +1,161 @@
use crate::{
cfg::{ConfigOptsBuild, ConfigOptsServe},
CrateConfig, Result, WebAssetConfigDropGuard,
};
use super::{desktop, Platform};
pub async fn startup(config: CrateConfig, serve: &ConfigOptsServe) -> Result<()> {
desktop::startup_with_platform::<FullstackPlatform>(config, serve).await
}
fn start_web_build_thread(
config: &CrateConfig,
serve: &ConfigOptsServe,
) -> std::thread::JoinHandle<Result<()>> {
let serve = serve.clone();
let target_directory = config.crate_dir.join(".dioxus").join("web");
std::fs::create_dir_all(&target_directory).unwrap();
std::thread::spawn(move || build_web(serve, &target_directory))
}
struct FullstackPlatform {
serve: ConfigOptsServe,
desktop: desktop::DesktopPlatform,
_config: WebAssetConfigDropGuard,
}
impl Platform for FullstackPlatform {
fn start(config: &CrateConfig, serve: &ConfigOptsServe) -> Result<Self>
where
Self: Sized,
{
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)?;
thread_handle
.join()
.map_err(|_| anyhow::anyhow!("Failed to join thread"))??;
Ok(Self {
desktop,
serve: serve.clone(),
_config: config,
})
}
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)
};
thread_handle
.join()
.map_err(|_| anyhow::anyhow!("Failed to join thread"))??;
result
}
}
fn build_web(serve: ConfigOptsServe, target_directory: &std::path::Path) -> Result<()> {
let mut web_config: ConfigOptsBuild = serve.into();
let web_feature = web_config.client_feature.clone();
let features = &mut web_config.features;
match features {
Some(features) => {
features.push(web_feature);
}
None => web_config.features = Some(vec![web_feature]),
};
web_config.platform = Some(crate::cfg::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);
}
}
}
+19 -1
View File
@@ -1,4 +1,4 @@
use crate::{BuildResult, CrateConfig, Result}; use crate::{cfg::ConfigOptsServe, BuildResult, CrateConfig, Result};
use cargo_metadata::diagnostic::Diagnostic; use cargo_metadata::diagnostic::Diagnostic;
use dioxus_core::Template; use dioxus_core::Template;
@@ -14,6 +14,7 @@ use tokio::sync::broadcast::{self};
mod output; mod output;
use output::*; use output::*;
pub mod desktop; pub mod desktop;
pub mod fullstack;
pub mod web; pub mod web;
/// Sets up a file watcher /// Sets up a file watcher
@@ -55,6 +56,16 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
break; break;
} }
// Workaround for notify and vscode-like editor:
// when edit & save a file in vscode, there will be two notifications,
// the first one is a file with empty content.
// filter the empty file notification to avoid false rebuild during hot-reload
if let Ok(metadata) = fs::metadata(path) {
if metadata.len() == 0 {
continue;
}
}
match rsx_file_map.update_rsx(path, &config.crate_dir) { match rsx_file_map.update_rsx(path, &config.crate_dir) {
Ok(UpdateResult::UpdatedRsx(msgs)) => { Ok(UpdateResult::UpdatedRsx(msgs)) => {
messages.extend(msgs); messages.extend(msgs);
@@ -131,6 +142,13 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
Ok(watcher) Ok(watcher)
} }
pub(crate) trait Platform {
fn start(config: &CrateConfig, serve: &ConfigOptsServe) -> Result<Self>
where
Self: Sized;
fn rebuild(&mut self, config: &CrateConfig) -> Result<BuildResult>;
}
#[derive(Clone)] #[derive(Clone)]
pub struct HotReloadState { pub struct HotReloadState {
pub messages: broadcast::Sender<Template<'static>>, pub messages: broadcast::Sender<Template<'static>>,
+14 -11
View File
@@ -22,17 +22,20 @@ pub fn print_console_info(
options: PrettierOptions, options: PrettierOptions,
web_info: Option<WebServerInfo>, web_info: Option<WebServerInfo>,
) { ) {
if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") { // Don't clear the screen if the user has set the DIOXUS_LOG environment variable to "trace" so that we can see the logs
"cls" if Some("trace") != std::env::var("DIOXUS_LOG").ok().as_deref() {
} else { if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") {
"clear" "cls"
}) } else {
.output() "clear"
{ })
print!("{}", String::from_utf8_lossy(&native_clearseq.stdout)); .output()
} else { {
// Try ANSI-Escape characters print!("{}", String::from_utf8_lossy(&native_clearseq.stdout));
print!("\x1b[2J\x1b[H"); } else {
// Try ANSI-Escape characters
print!("\x1b[2J\x1b[H");
}
} }
let mut profile = if config.release { "Release" } else { "Debug" }.to_string(); let mut profile = if config.release { "Release" } else { "Debug" }.to_string();
+30 -8
View File
@@ -11,6 +11,7 @@ use axum::{
body::{Full, HttpBody}, body::{Full, HttpBody},
extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade}, extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
http::{ http::{
self,
header::{HeaderName, HeaderValue}, header::{HeaderName, HeaderValue},
Method, Response, StatusCode, Method, Response, StatusCode,
}, },
@@ -47,7 +48,12 @@ struct WsReloadState {
update: broadcast::Sender<()>, update: broadcast::Sender<()>,
} }
pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Result<()> { pub async fn startup(
port: u16,
config: CrateConfig,
start_browser: bool,
skip_assets: bool,
) -> Result<()> {
// ctrl-c shutdown checker // ctrl-c shutdown checker
let _crate_config = config.clone(); let _crate_config = config.clone();
let _ = ctrlc::set_handler(move || { let _ = ctrlc::set_handler(move || {
@@ -79,7 +85,15 @@ pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Res
false => None, false => None,
}; };
serve(ip, port, config, start_browser, hot_reload_state).await?; serve(
ip,
port,
config,
start_browser,
skip_assets,
hot_reload_state,
)
.await?;
Ok(()) Ok(())
} }
@@ -90,9 +104,10 @@ pub async fn serve(
port: u16, port: u16,
config: CrateConfig, config: CrateConfig,
start_browser: bool, start_browser: bool,
skip_assets: bool,
hot_reload_state: Option<HotReloadState>, hot_reload_state: Option<HotReloadState>,
) -> Result<()> { ) -> Result<()> {
let first_build_result = crate::builder::build(&config, true)?; let first_build_result = crate::builder::build(&config, false, skip_assets)?;
log::info!("🚀 Starting development server..."); log::info!("🚀 Starting development server...");
@@ -105,7 +120,7 @@ pub async fn serve(
{ {
let config = config.clone(); let config = config.clone();
let reload_tx = reload_tx.clone(); let reload_tx = reload_tx.clone();
move || build(&config, &reload_tx) move || build(&config, &reload_tx, skip_assets)
}, },
&config, &config,
Some(WebServerInfo { Some(WebServerInfo {
@@ -262,7 +277,7 @@ async fn setup_router(
.override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop) .override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
.and_then( .and_then(
move |response: Response<ServeFileSystemResponseBody>| async move { move |response: Response<ServeFileSystemResponseBody>| async move {
let response = if file_service_config let mut response = if file_service_config
.dioxus_config .dioxus_config
.web .web
.watcher .watcher
@@ -290,6 +305,13 @@ async fn setup_router(
} else { } else {
response.map(|body| body.boxed()) response.map(|body| body.boxed())
}; };
let headers = response.headers_mut();
headers.insert(
http::header::CACHE_CONTROL,
HeaderValue::from_static("no-cache"),
);
headers.insert(http::header::PRAGMA, HeaderValue::from_static("no-cache"));
headers.insert(http::header::EXPIRES, HeaderValue::from_static("0"));
Ok(response) Ok(response)
}, },
) )
@@ -412,8 +434,8 @@ async fn ws_handler(
}) })
} }
fn build(config: &CrateConfig, reload_tx: &Sender<()>) -> Result<BuildResult> { fn build(config: &CrateConfig, reload_tx: &Sender<()>, skip_assets: bool) -> Result<BuildResult> {
let result = builder::build(config, true)?; let result = builder::build(config, true, skip_assets)?;
// change the websocket reload state to true; // change the websocket reload state to true;
// the page will auto-reload. // the page will auto-reload.
if config if config
@@ -423,7 +445,7 @@ fn build(config: &CrateConfig, reload_tx: &Sender<()>) -> Result<BuildResult> {
.reload_html .reload_html
.unwrap_or(false) .unwrap_or(false)
{ {
let _ = Serve::regen_dev_page(config); let _ = Serve::regen_dev_page(config, skip_assets);
} }
let _ = reload_tx.send(()); let _ = reload_tx.send(());
Ok(result) Ok(result)
@@ -58,8 +58,11 @@ impl ToTokens for ComponentDeserializerOutput {
fn to_tokens(&self, tokens: &mut TokenStream2) { fn to_tokens(&self, tokens: &mut TokenStream2) {
let comp_fn = &self.comp_fn; let comp_fn = &self.comp_fn;
let props_struct = &self.props_struct; let props_struct = &self.props_struct;
let fn_ident = &comp_fn.sig.ident;
let doc = format!("Properties for the [`{fn_ident}`] component.");
tokens.append_all(quote! { tokens.append_all(quote! {
#[doc = #doc]
#props_struct #props_struct
#[allow(non_snake_case)] #[allow(non_snake_case)]
#comp_fn #comp_fn
+9 -28
View File
@@ -255,10 +255,6 @@ mod field_info {
} }
.into() .into()
} }
pub fn type_from_inside_option(&self, check_option_name: bool) -> Option<&syn::Type> {
type_from_inside_option(self.ty, check_option_name)
}
} }
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
@@ -1021,31 +1017,16 @@ Finally, call `.build()` to create the instance of `{name}`.
None => quote!(), None => quote!(),
}; };
// NOTE: both auto_into and strip_option affect `arg_type` and `arg_expr`, but the order of let arg_type = field_type;
// nesting is different so we have to do this little dance. let (arg_type, arg_expr) =
let arg_type = if field.builder_attr.strip_option { if field.builder_attr.auto_into || field.builder_attr.strip_option {
field.type_from_inside_option(false).ok_or_else(|| { (
Error::new_spanned( quote!(impl ::core::convert::Into<#arg_type>),
field_type, quote!(#field_name.into()),
"can't `strip_option` - field is not `Option<...>`",
) )
})? } else {
} else { (quote!(#arg_type), quote!(#field_name))
field_type };
};
let (arg_type, arg_expr) = if field.builder_attr.auto_into {
(
quote!(impl ::core::convert::Into<#arg_type>),
quote!(#field_name.into()),
)
} else {
(quote!(#arg_type), quote!(#field_name))
};
let arg_expr = if field.builder_attr.strip_option {
quote!(Some(#arg_expr))
} else {
arg_expr
};
let repeated_fields_error_type_name = syn::Ident::new( let repeated_fields_error_type_name = syn::Ident::new(
&format!( &format!(
+1
View File
@@ -36,6 +36,7 @@ serde = { version = "1", features = ["derive"], optional = true }
[dev-dependencies] [dev-dependencies]
tokio = { workspace = true, features = ["full"] } tokio = { workspace = true, features = ["full"] }
dioxus = { workspace = true } dioxus = { workspace = true }
dioxus-html = { workspace = true, features = ["serialize"] }
pretty_assertions = "1.3.0" pretty_assertions = "1.3.0"
rand = "0.8.5" rand = "0.8.5"
dioxus-ssr = { workspace = true } dioxus-ssr = { workspace = true }
+62 -58
View File
@@ -13,62 +13,50 @@ use crate::{
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct ElementId(pub usize); pub struct ElementId(pub usize);
pub(crate) struct ElementRef { /// An Element that can be bubbled to's unique identifier.
///
/// `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(Debug, Clone, Copy)]
pub struct ElementRef {
// the pathway of the real element inside the template // the pathway of the real element inside the template
pub path: ElementPath, pub(crate) path: ElementPath,
// The actual template // The actual template
pub template: Option<NonNull<VNode<'static>>>, pub(crate) template: VNodeId,
// The scope the element belongs to // The scope the element belongs to
pub scope: ScopeId, pub(crate) scope: ScopeId,
} }
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub enum ElementPath { pub struct ElementPath {
Deep(&'static [u8]), pub(crate) path: &'static [u8],
Root(usize),
}
impl ElementRef {
pub(crate) fn none() -> Self {
Self {
template: None,
path: ElementPath::Root(0),
scope: ScopeId::ROOT,
}
}
} }
impl VirtualDom { impl VirtualDom {
pub(crate) fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId { pub(crate) fn next_element(&mut self) -> ElementId {
self.next_reference(template, ElementPath::Deep(path)) ElementId(self.elements.insert(None))
} }
pub(crate) fn next_root(&mut self, template: &VNode, path: usize) -> ElementId { pub(crate) fn next_vnode_ref(&mut self, vnode: &VNode) -> VNodeId {
self.next_reference(template, ElementPath::Root(path)) let new_id = VNodeId(self.element_refs.insert(Some(unsafe {
} std::mem::transmute::<NonNull<VNode>, _>(vnode.into())
})));
pub(crate) fn next_null(&mut self) -> ElementId { // Set this id to be dropped when the scope is rerun
let entry = self.elements.vacant_entry(); if let Some(scope) = self.runtime.current_scope_id() {
let id = entry.key(); self.scopes[scope.0]
.element_refs_to_drop
.borrow_mut()
.push(new_id);
}
entry.insert(ElementRef::none()); new_id
ElementId(id)
}
fn next_reference(&mut self, template: &VNode, path: ElementPath) -> ElementId {
let entry = self.elements.vacant_entry();
let id = entry.key();
let scope = self.runtime.current_scope_id().unwrap_or(ScopeId::ROOT);
entry.insert(ElementRef {
// We know this is non-null because it comes from a reference
template: Some(unsafe { NonNull::new_unchecked(template as *const _ as *mut _) }),
path,
scope,
});
ElementId(id)
} }
pub(crate) fn reclaim(&mut self, el: ElementId) { pub(crate) fn reclaim(&mut self, el: ElementId) {
@@ -76,7 +64,7 @@ impl VirtualDom {
.unwrap_or_else(|| panic!("cannot reclaim {:?}", el)); .unwrap_or_else(|| panic!("cannot reclaim {:?}", el));
} }
pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option<ElementRef> { pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option<()> {
if el.0 == 0 { if el.0 == 0 {
panic!( panic!(
"Cannot reclaim the root element - {:#?}", "Cannot reclaim the root element - {:#?}",
@@ -84,12 +72,12 @@ impl VirtualDom {
); );
} }
self.elements.try_remove(el.0) self.elements.try_remove(el.0).map(|_| ())
} }
pub(crate) fn update_template(&mut self, el: ElementId, node: &VNode) { pub(crate) fn set_template(&mut self, id: VNodeId, vnode: &VNode) {
let node: *const VNode = node as *const _; self.element_refs[id.0] =
self.elements[el.0].template = unsafe { std::mem::transmute(node) }; Some(unsafe { std::mem::transmute::<NonNull<VNode>, _>(vnode.into()) });
} }
// Drop a scope and all its children // Drop a scope and all its children
@@ -101,6 +89,15 @@ impl VirtualDom {
id, id,
}); });
// 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); self.ensure_drop_safety(id);
if recursive { if recursive {
@@ -145,14 +142,25 @@ impl VirtualDom {
} }
/// Descend through the tree, removing any borrowed props and listeners /// Descend through the tree, removing any borrowed props and listeners
pub(crate) fn ensure_drop_safety(&self, scope_id: ScopeId) { pub(crate) fn ensure_drop_safety(&mut self, scope_id: ScopeId) {
let scope = &self.scopes[scope_id.0]; 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 // 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) // run the hooks (which hold an &mut Reference)
// recursively call ensure_drop_safety on all children // recursively call ensure_drop_safety on all children
let mut props = scope.borrowed_props.borrow_mut(); let props = { scope.borrowed_props.borrow_mut().clone() };
props.drain(..).for_each(|comp| { for comp in props {
let comp = unsafe { &*comp }; let comp = unsafe { &*comp };
match comp.scope.get() { match comp.scope.get() {
Some(child) if child != scope_id => self.ensure_drop_safety(child), Some(child) if child != scope_id => self.ensure_drop_safety(child),
@@ -161,7 +169,9 @@ impl VirtualDom {
if let Ok(mut props) = comp.props.try_borrow_mut() { if let Ok(mut props) = comp.props.try_borrow_mut() {
*props = None; *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. // Now that all the references are gone, we can safely drop our own references in our listeners.
let mut listeners = scope.attributes_to_drop_before_render.borrow_mut(); let mut listeners = scope.attributes_to_drop_before_render.borrow_mut();
@@ -176,18 +186,12 @@ impl VirtualDom {
impl ElementPath { impl ElementPath {
pub(crate) fn is_decendant(&self, small: &&[u8]) -> bool { pub(crate) fn is_decendant(&self, small: &&[u8]) -> bool {
match *self { small.len() <= self.path.len() && *small == &self.path[..small.len()]
ElementPath::Deep(big) => small.len() <= big.len() && *small == &big[..small.len()],
ElementPath::Root(r) => small.len() == 1 && small[0] == r as u8,
}
} }
} }
impl PartialEq<&[u8]> for ElementPath { impl PartialEq<&[u8]> for ElementPath {
fn eq(&self, other: &&[u8]) -> bool { fn eq(&self, other: &&[u8]) -> bool {
match *self { self.path.eq(*other)
ElementPath::Deep(deep) => deep.eq(*other),
ElementPath::Root(r) => other.len() == 1 && other[0] == r as u8,
}
} }
} }
+20 -1
View File
@@ -1,5 +1,5 @@
use crate::nodes::RenderReturn; use crate::nodes::RenderReturn;
use crate::{Attribute, AttributeValue}; use crate::{Attribute, AttributeValue, VComponent};
use bumpalo::Bump; use bumpalo::Bump;
use std::cell::RefCell; use std::cell::RefCell;
use std::cell::{Cell, UnsafeCell}; use std::cell::{Cell, UnsafeCell};
@@ -7,7 +7,10 @@ use std::cell::{Cell, UnsafeCell};
pub(crate) struct BumpFrame { pub(crate) struct BumpFrame {
pub bump: UnsafeCell<Bump>, pub bump: UnsafeCell<Bump>,
pub node: Cell<*const RenderReturn<'static>>, 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) attributes_to_drop_before_reset: RefCell<Vec<*const Attribute<'static>>>,
pub(crate) props_to_drop_before_reset: RefCell<Vec<*const VComponent<'static>>>,
} }
impl BumpFrame { impl BumpFrame {
@@ -17,6 +20,7 @@ impl BumpFrame {
bump: UnsafeCell::new(bump), bump: UnsafeCell::new(bump),
node: Cell::new(std::ptr::null()), node: Cell::new(std::ptr::null()),
attributes_to_drop_before_reset: Default::default(), attributes_to_drop_before_reset: Default::default(),
props_to_drop_before_reset: Default::default(),
} }
} }
@@ -41,6 +45,10 @@ impl BumpFrame {
.push(attribute); .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) { pub(crate) unsafe fn reset(&self) {
let mut attributes = self.attributes_to_drop_before_reset.borrow_mut(); let mut attributes = self.attributes_to_drop_before_reset.borrow_mut();
attributes.drain(..).for_each(|attribute| { attributes.drain(..).for_each(|attribute| {
@@ -49,9 +57,20 @@ impl BumpFrame {
_ = l.take(); _ = 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 { unsafe {
let bump = &mut *self.bump.get(); let bump = &mut *self.bump.get();
bump.reset(); bump.reset();
} }
} }
} }
impl Drop for BumpFrame {
fn drop(&mut self) {
unsafe { self.reset() }
}
}
+88 -48
View File
@@ -1,6 +1,7 @@
use crate::any_props::AnyProps; use crate::any_props::AnyProps;
use crate::innerlude::{ use crate::innerlude::{
AttributeType, BorrowedAttributeValue, MountedAttribute, VComponent, VPlaceholder, VText, AttributeType, BorrowedAttributeValue, ElementPath, ElementRef, MountedAttribute, VComponent,
VPlaceholder, VText,
}; };
use crate::mutations::Mutation; use crate::mutations::Mutation;
use crate::mutations::Mutation::*; use crate::mutations::Mutation::*;
@@ -96,6 +97,9 @@ impl<'b> VirtualDom {
nodes_mut.resize(len, ElementId::default()); 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 // The best renderers will have templates prehydrated and registered
// Just in case, let's create the template using instructions anyways // Just in case, let's create the template using instructions anyways
self.register_template(node.template.get()); self.register_template(node.template.get());
@@ -183,15 +187,30 @@ impl<'b> VirtualDom {
use DynamicNode::*; use DynamicNode::*;
match &template.dynamic_nodes[idx] { match &template.dynamic_nodes[idx] {
node @ Component { .. } | node @ Fragment(_) => { node @ Component { .. } | node @ Fragment(_) => {
self.create_dynamic_node(template, node, idx) 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 }) => { Placeholder(VPlaceholder { id, parent }) => {
let id = self.set_slot(template, id, idx); 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 }); self.mutations.push(CreatePlaceholder { id });
1 1
} }
Text(VText { id, value }) => { Text(VText { id, value }) => {
let id = self.set_slot(template, id, idx); let id = self.set_slot(id);
self.create_static_text(value, id); self.create_static_text(value, id);
1 1
} }
@@ -207,7 +226,7 @@ impl<'b> VirtualDom {
}); });
} }
/// We write all the descndent data for this element /// We write all the descendent data for this element
/// ///
/// Elements can contain other nodes - and those nodes can be dynamic or static /// Elements can contain other nodes - and those nodes can be dynamic or static
/// ///
@@ -267,7 +286,14 @@ impl<'b> VirtualDom {
.map(|sorted_index| dynamic_nodes[sorted_index].0); .map(|sorted_index| dynamic_nodes[sorted_index].0);
for idx in reversed_iter { for idx in reversed_iter {
let m = self.create_dynamic_node(template, &template.dynamic_nodes[idx], idx); 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 { if m > 0 {
// The path is one shorter because the top node is the root // The path is one shorter because the top node is the root
let path = &template.template.get().node_paths[idx][1..]; let path = &template.template.get().node_paths[idx][1..];
@@ -281,15 +307,15 @@ impl<'b> VirtualDom {
attrs: &mut Peekable<impl Iterator<Item = (usize, &'static [u8])>>, attrs: &mut Peekable<impl Iterator<Item = (usize, &'static [u8])>>,
root_idx: u8, root_idx: u8,
root: ElementId, root: ElementId,
node: &VNode, node: &'b VNode<'b>,
) { ) {
while let Some((mut attr_id, path)) = while let Some((mut attr_id, path)) =
attrs.next_if(|(_, p)| p.first().copied() == Some(root_idx)) attrs.next_if(|(_, p)| p.first().copied() == Some(root_idx))
{ {
let id = self.assign_static_node_as_dynamic(path, root, node, attr_id); let id = self.assign_static_node_as_dynamic(path, root);
loop { loop {
self.write_attribute_type(&node.dynamic_attrs[attr_id], id); 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) // Only push the dynamic attributes forward if they match the current path (same element)
match attrs.next_if(|(_, p)| *p == path) { match attrs.next_if(|(_, p)| *p == path) {
@@ -300,25 +326,44 @@ impl<'b> VirtualDom {
} }
} }
fn write_attribute_type(&mut self, attribute: &'b MountedAttribute<'b>, id: ElementId) { 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 // Make sure we set the attribute's associated id
attribute.mounted_element.set(id); attribute.mounted_element.set(id);
match &attribute.ty { match &attribute.ty {
AttributeType::Single(attribute) => self.write_attribute(attribute, id), AttributeType::Single(attribute) => self.write_attribute(vnode, attribute, idx, id),
AttributeType::Many(attribute) => { AttributeType::Many(attribute) => {
for attribute in *attribute { for attribute in *attribute {
self.write_attribute(attribute, id); self.write_attribute(vnode, attribute, idx, id);
} }
} }
} }
} }
pub(crate) fn write_attribute(&mut self, attribute: &'b crate::Attribute<'b>, id: ElementId) { 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 // 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) }; let unbounded_name: &str = unsafe { std::mem::transmute(attribute.name) };
match &attribute.value { match &attribute.value {
AttributeValue::Listener(_) => { 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 { self.mutations.push(NewEventListener {
// all listeners start with "on" // all listeners start with "on"
name: &unbounded_name[2..], name: &unbounded_name[2..],
@@ -342,7 +387,7 @@ impl<'b> VirtualDom {
fn load_template_root(&mut self, template: &VNode, root_idx: usize) -> ElementId { fn load_template_root(&mut self, template: &VNode, root_idx: usize) -> ElementId {
// Get an ID for this root since it's a real root // Get an ID for this root since it's a real root
let this_id = self.next_root(template, root_idx); let this_id = self.next_element();
template.root_ids.borrow_mut()[root_idx] = this_id; template.root_ids.borrow_mut()[root_idx] = this_id;
self.mutations.push(LoadTemplate { self.mutations.push(LoadTemplate {
@@ -365,8 +410,6 @@ impl<'b> VirtualDom {
&mut self, &mut self,
path: &'static [u8], path: &'static [u8],
this_id: ElementId, this_id: ElementId,
template: &VNode,
attr_id: usize,
) -> ElementId { ) -> ElementId {
if path.len() == 1 { if path.len() == 1 {
return this_id; return this_id;
@@ -374,7 +417,7 @@ impl<'b> VirtualDom {
// if attribute is on a root node, then we've already created the element // 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 // Else, it's deep in the template and we should create a new id for it
let id = self.next_element(template, template.template.get().attr_paths[attr_id]); let id = self.next_element();
self.mutations.push(Mutation::AssignId { self.mutations.push(Mutation::AssignId {
path: &path[1..], path: &path[1..],
@@ -417,6 +460,7 @@ impl<'b> VirtualDom {
#[allow(unused_mut)] #[allow(unused_mut)]
pub(crate) fn register_template(&mut self, mut template: Template<'static>) { pub(crate) fn register_template(&mut self, mut template: Template<'static>) {
let (path, byte_index) = template.name.rsplit_once(':').unwrap(); let (path, byte_index) = template.name.rsplit_once(':').unwrap();
let byte_index = byte_index.parse::<usize>().unwrap(); let byte_index = byte_index.parse::<usize>().unwrap();
// First, check if we've already seen this template // First, check if we've already seen this template
if self if self
@@ -451,27 +495,21 @@ impl<'b> VirtualDom {
pub(crate) fn create_dynamic_node( pub(crate) fn create_dynamic_node(
&mut self, &mut self,
template: &'b VNode<'b>, parent: ElementRef,
node: &'b DynamicNode<'b>, node: &'b DynamicNode<'b>,
idx: usize,
) -> usize { ) -> usize {
use DynamicNode::*; use DynamicNode::*;
match node { match node {
Text(text) => self.create_dynamic_text(template, text, idx), Text(text) => self.create_dynamic_text(parent, text),
Placeholder(place) => self.create_placeholder(place, template, idx), Placeholder(place) => self.create_placeholder(place, parent),
Component(component) => self.create_component_node(template, component), Component(component) => self.create_component_node(Some(parent), component),
Fragment(frag) => frag.iter().map(|child| self.create(child)).sum(), Fragment(frag) => self.create_children(*frag, Some(parent)),
} }
} }
fn create_dynamic_text( fn create_dynamic_text(&mut self, parent: ElementRef, text: &'b VText<'b>) -> usize {
&mut self,
template: &'b VNode<'b>,
text: &'b VText<'b>,
idx: usize,
) -> usize {
// Allocate a dynamic element reference for this text node // Allocate a dynamic element reference for this text node
let new_id = self.next_element(template, template.template.get().node_paths[idx]); let new_id = self.next_element();
// Make sure the text node is assigned to the correct element // Make sure the text node is assigned to the correct element
text.id.set(Some(new_id)); text.id.set(Some(new_id));
@@ -482,7 +520,7 @@ impl<'b> VirtualDom {
// Add the mutation to the list // Add the mutation to the list
self.mutations.push(HydrateText { self.mutations.push(HydrateText {
id: new_id, id: new_id,
path: &template.template.get().node_paths[idx][1..], path: &parent.path.path[1..],
value, value,
}); });
@@ -493,18 +531,20 @@ impl<'b> VirtualDom {
pub(crate) fn create_placeholder( pub(crate) fn create_placeholder(
&mut self, &mut self,
placeholder: &VPlaceholder, placeholder: &VPlaceholder,
template: &'b VNode<'b>, parent: ElementRef,
idx: usize,
) -> usize { ) -> usize {
// Allocate a dynamic element reference for this text node // Allocate a dynamic element reference for this text node
let id = self.next_element(template, template.template.get().node_paths[idx]); let id = self.next_element();
// Make sure the text node is assigned to the correct element // Make sure the text node is assigned to the correct element
placeholder.id.set(Some(id)); 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 // Assign the ID to the existing node in the template
self.mutations.push(AssignId { self.mutations.push(AssignId {
path: &template.template.get().node_paths[idx][1..], path: &parent.path.path[1..],
id, id,
}); });
@@ -514,7 +554,7 @@ impl<'b> VirtualDom {
pub(super) fn create_component_node( pub(super) fn create_component_node(
&mut self, &mut self,
template: &'b VNode<'b>, parent: Option<ElementRef>,
component: &'b VComponent<'b>, component: &'b VComponent<'b>,
) -> usize { ) -> usize {
use RenderReturn::*; use RenderReturn::*;
@@ -526,8 +566,11 @@ impl<'b> VirtualDom {
match unsafe { self.run_scope(scope).extend_lifetime_ref() } { match unsafe { self.run_scope(scope).extend_lifetime_ref() } {
// Create the component's root element // Create the component's root element
Ready(t) => self.create_scope(scope, t), Ready(t) => {
Aborted(t) => self.mount_aborted(template, t), self.assign_boundary_ref(parent, t);
self.create_scope(scope, t)
}
Aborted(t) => self.mount_aborted(t, parent),
} }
} }
@@ -543,20 +586,17 @@ impl<'b> VirtualDom {
.unwrap_or_else(|| component.scope.get().unwrap()) .unwrap_or_else(|| component.scope.get().unwrap())
} }
fn mount_aborted(&mut self, parent: &'b VNode<'b>, placeholder: &VPlaceholder) -> usize { fn mount_aborted(&mut self, placeholder: &VPlaceholder, parent: Option<ElementRef>) -> usize {
let id = self.next_element(parent, &[]); let id = self.next_element();
self.mutations.push(Mutation::CreatePlaceholder { id }); self.mutations.push(Mutation::CreatePlaceholder { id });
placeholder.id.set(Some(id)); placeholder.id.set(Some(id));
placeholder.parent.set(parent);
1 1
} }
fn set_slot( fn set_slot(&mut self, slot: &'b Cell<Option<ElementId>>) -> ElementId {
&mut self, let id = self.next_element();
template: &'b VNode<'b>,
slot: &'b Cell<Option<ElementId>>,
id: usize,
) -> ElementId {
let id = self.next_element(template, template.template.get().node_paths[id]);
slot.set(Some(id)); slot.set(Some(id));
id id
} }
+147 -64
View File
@@ -2,7 +2,8 @@ use crate::{
any_props::AnyProps, any_props::AnyProps,
arena::ElementId, arena::ElementId,
innerlude::{ innerlude::{
AttributeType, BorrowedAttributeValue, DirtyScope, VComponent, VPlaceholder, VText, AttributeType, BorrowedAttributeValue, DirtyScope, ElementPath, ElementRef, VComponent,
VPlaceholder, VText,
}, },
mutations::Mutation, mutations::Mutation,
nodes::RenderReturn, nodes::RenderReturn,
@@ -41,19 +42,27 @@ impl<'b> VirtualDom {
(Ready(l), Aborted(p)) => self.diff_ok_to_err(l, p), (Ready(l), Aborted(p)) => self.diff_ok_to_err(l, p),
// Just move over the placeholder // Just move over the placeholder
(Aborted(l), Aborted(r)) => r.id.set(l.id.get()), (Aborted(l), Aborted(r)) => {
r.id.set(l.id.get());
r.parent.set(l.parent.get())
}
// Placeholder becomes something // Placeholder becomes something
// We should also clear the error now // We should also clear the error now
(Aborted(l), Ready(r)) => self.replace_placeholder(l, [r]), (Aborted(l), Ready(r)) => self.replace_placeholder(
l,
[r],
l.parent.get().expect("root node should not be none"),
),
}; };
} }
self.runtime.scope_stack.borrow_mut().pop(); self.runtime.scope_stack.borrow_mut().pop();
} }
fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, p: &'b VPlaceholder) { fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, p: &'b VPlaceholder) {
let id = self.next_null(); let id = self.next_element();
p.id.set(Some(id)); p.id.set(Some(id));
p.parent.set(l.parent.get());
self.mutations.push(Mutation::CreatePlaceholder { id }); self.mutations.push(Mutation::CreatePlaceholder { id });
let pre_edits = self.mutations.edits.len(); let pre_edits = self.mutations.edits.len();
@@ -83,12 +92,24 @@ impl<'b> VirtualDom {
if let Some(&template) = map.get(&byte_index) { if let Some(&template) = map.get(&byte_index) {
right_template.template.set(template); right_template.template.set(template);
if template != left_template.template.get() { if template != left_template.template.get() {
return self.replace(left_template, [right_template]); let parent = left_template.parent.take();
return self.replace(left_template, [right_template], parent);
} }
} }
} }
} }
// Copy over the parent
{
right_template.parent.set(left_template.parent.get());
}
// Update the bubble id pointer
right_template.stable_id.set(left_template.stable_id.get());
if let Some(bubble_id) = right_template.stable_id.get() {
self.set_template(bubble_id, right_template);
}
// If the templates are the same, we don't need to do anything, nor do we want to // If the templates are the same, we don't need to do anything, nor do we want to
if templates_are_the_same(left_template, right_template) { if templates_are_the_same(left_template, right_template) {
return; return;
@@ -107,11 +128,8 @@ impl<'b> VirtualDom {
.zip(right_template.dynamic_attrs.iter()) .zip(right_template.dynamic_attrs.iter())
.for_each(|(left_attr, right_attr)| { .for_each(|(left_attr, right_attr)| {
// Move over the ID from the old to the new // Move over the ID from the old to the new
let mounted_id = left_attr.mounted_element.get(); let mounted_element = left_attr.mounted_element.get();
right_attr.mounted_element.set(mounted_id); right_attr.mounted_element.set(mounted_element);
// We want to make sure anything that gets pulled is valid
self.update_template(left_attr.mounted_element.get(), right_template);
match (&left_attr.ty, &right_attr.ty) { match (&left_attr.ty, &right_attr.ty) {
(AttributeType::Single(left), AttributeType::Single(right)) => { (AttributeType::Single(left), AttributeType::Single(right)) => {
@@ -160,8 +178,16 @@ impl<'b> VirtualDom {
.dynamic_nodes .dynamic_nodes
.iter() .iter()
.zip(right_template.dynamic_nodes.iter()) .zip(right_template.dynamic_nodes.iter())
.for_each(|(left_node, right_node)| { .enumerate()
self.diff_dynamic_node(left_node, right_node, right_template); .for_each(|(dyn_node_idx, (left_node, right_node))| {
let current_ref = ElementRef {
template: right_template.stable_id().unwrap(),
path: ElementPath {
path: left_template.template.get().node_paths[dyn_node_idx],
},
scope: self.runtime.scope_stack.borrow().last().copied().unwrap(),
};
self.diff_dynamic_node(left_node, right_node, current_ref);
}); });
// Make sure the roots get transferred over while we're here // Make sure the roots get transferred over while we're here
@@ -172,15 +198,6 @@ impl<'b> VirtualDom {
right.push(element); right.push(element);
} }
} }
let root_ids = right_template.root_ids.borrow();
// Update the node refs
for i in 0..root_ids.len() {
if let Some(root_id) = root_ids.get(i) {
self.update_template(*root_id, right_template);
}
}
} }
fn diff_attribute( fn diff_attribute(
@@ -199,15 +216,18 @@ impl<'b> VirtualDom {
&mut self, &mut self,
left_node: &'b DynamicNode<'b>, left_node: &'b DynamicNode<'b>,
right_node: &'b DynamicNode<'b>, right_node: &'b DynamicNode<'b>,
node: &'b VNode<'b>, parent: ElementRef,
) { ) {
match (left_node, right_node) { match (left_node, right_node) {
(Text(left), Text(right)) => self.diff_vtext(left, right, node), (Text(left), Text(right)) => self.diff_vtext(left, right),
(Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right), (Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right, parent),
(Placeholder(left), Placeholder(right)) => right.id.set(left.id.get()), (Placeholder(left), Placeholder(right)) => {
(Component(left), Component(right)) => self.diff_vcomponent(left, right, node), right.id.set(left.id.get());
(Placeholder(left), Fragment(right)) => self.replace_placeholder(left, *right), right.parent.set(left.parent.get());
(Fragment(left), Placeholder(right)) => self.node_to_placeholder(left, right), },
(Component(left), Component(right)) => self.diff_vcomponent(left, right, Some(parent)),
(Placeholder(left), Fragment(right)) => self.replace_placeholder(left, *right, parent),
(Fragment(left), Placeholder(right)) => self.node_to_placeholder(left, right, parent),
_ => todo!("This is an usual custom case for dynamic nodes. We don't know how to handle it yet."), _ => todo!("This is an usual custom case for dynamic nodes. We don't know how to handle it yet."),
}; };
} }
@@ -245,7 +265,7 @@ impl<'b> VirtualDom {
&mut self, &mut self,
left: &'b VComponent<'b>, left: &'b VComponent<'b>,
right: &'b VComponent<'b>, right: &'b VComponent<'b>,
right_template: &'b VNode<'b>, parent: Option<ElementRef>,
) { ) {
if std::ptr::eq(left, right) { if std::ptr::eq(left, right) {
return; return;
@@ -253,7 +273,7 @@ impl<'b> VirtualDom {
// Replace components that have different render fns // Replace components that have different render fns
if left.render_fn != right.render_fn { if left.render_fn != right.render_fn {
return self.replace_vcomponent(right_template, right, left); return self.replace_vcomponent(right, left, parent);
} }
// Make sure the new vcomponent has the right scopeid associated to it // Make sure the new vcomponent has the right scopeid associated to it
@@ -294,11 +314,11 @@ impl<'b> VirtualDom {
fn replace_vcomponent( fn replace_vcomponent(
&mut self, &mut self,
right_template: &'b VNode<'b>,
right: &'b VComponent<'b>, right: &'b VComponent<'b>,
left: &'b VComponent<'b>, left: &'b VComponent<'b>,
parent: Option<ElementRef>,
) { ) {
let m = self.create_component_node(right_template, right); let m = self.create_component_node(parent, right);
let pre_edits = self.mutations.edits.len(); let pre_edits = self.mutations.edits.len();
@@ -353,11 +373,12 @@ impl<'b> VirtualDom {
/// } /// }
/// ``` /// ```
fn light_diff_templates(&mut self, left: &'b VNode<'b>, right: &'b VNode<'b>) { fn light_diff_templates(&mut self, left: &'b VNode<'b>, right: &'b VNode<'b>) {
let parent = left.parent.take();
match matching_components(left, right) { match matching_components(left, right) {
None => self.replace(left, [right]), None => self.replace(left, [right], parent),
Some(components) => components Some(components) => components
.into_iter() .into_iter()
.for_each(|(l, r)| self.diff_vcomponent(l, r, right)), .for_each(|(l, r)| self.diff_vcomponent(l, r, parent)),
} }
} }
@@ -365,11 +386,8 @@ impl<'b> VirtualDom {
/// ///
/// This just moves the ID of the old node over to the new node, and then sets the text of the new node if it's /// This just moves the ID of the old node over to the new node, and then sets the text of the new node if it's
/// different. /// different.
fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>, node: &'b VNode<'b>) { fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>) {
let id = left let id = left.id.get().unwrap_or_else(|| self.next_element());
.id
.get()
.unwrap_or_else(|| self.next_element(node, &[0]));
right.id.set(Some(id)); right.id.set(Some(id));
if left.value != right.value { if left.value != right.value {
@@ -378,7 +396,12 @@ impl<'b> VirtualDom {
} }
} }
fn diff_non_empty_fragment(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { fn diff_non_empty_fragment(
&mut self,
old: &'b [VNode<'b>],
new: &'b [VNode<'b>],
parent: ElementRef,
) {
let new_is_keyed = new[0].key.is_some(); let new_is_keyed = new[0].key.is_some();
let old_is_keyed = old[0].key.is_some(); let old_is_keyed = old[0].key.is_some();
debug_assert!( debug_assert!(
@@ -391,9 +414,9 @@ impl<'b> VirtualDom {
); );
if new_is_keyed && old_is_keyed { if new_is_keyed && old_is_keyed {
self.diff_keyed_children(old, new); self.diff_keyed_children(old, new, parent);
} else { } else {
self.diff_non_keyed_children(old, new); self.diff_non_keyed_children(old, new, parent);
} }
} }
@@ -405,7 +428,12 @@ impl<'b> VirtualDom {
// [... parent] // [... parent]
// //
// the change list stack is in the same state when this function returns. // the change list stack is in the same state when this function returns.
fn diff_non_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { fn diff_non_keyed_children(
&mut self,
old: &'b [VNode<'b>],
new: &'b [VNode<'b>],
parent: ElementRef,
) {
use std::cmp::Ordering; use std::cmp::Ordering;
// Handled these cases in `diff_children` before calling this function. // Handled these cases in `diff_children` before calling this function.
@@ -414,7 +442,9 @@ impl<'b> VirtualDom {
match old.len().cmp(&new.len()) { match old.len().cmp(&new.len()) {
Ordering::Greater => self.remove_nodes(&old[new.len()..]), Ordering::Greater => self.remove_nodes(&old[new.len()..]),
Ordering::Less => self.create_and_insert_after(&new[old.len()..], old.last().unwrap()), Ordering::Less => {
self.create_and_insert_after(&new[old.len()..], old.last().unwrap(), parent)
}
Ordering::Equal => {} Ordering::Equal => {}
} }
@@ -439,7 +469,12 @@ impl<'b> VirtualDom {
// https://github.com/infernojs/inferno/blob/36fd96/packages/inferno/src/DOM/patching.ts#L530-L739 // https://github.com/infernojs/inferno/blob/36fd96/packages/inferno/src/DOM/patching.ts#L530-L739
// //
// The stack is empty upon entry. // The stack is empty upon entry.
fn diff_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { fn diff_keyed_children(
&mut self,
old: &'b [VNode<'b>],
new: &'b [VNode<'b>],
parent: ElementRef,
) {
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
let mut keys = rustc_hash::FxHashSet::default(); let mut keys = rustc_hash::FxHashSet::default();
let mut assert_unique_keys = |children: &'b [VNode<'b>]| { let mut assert_unique_keys = |children: &'b [VNode<'b>]| {
@@ -467,7 +502,7 @@ impl<'b> VirtualDom {
// //
// `shared_prefix_count` is the count of how many nodes at the start of // `shared_prefix_count` is the count of how many nodes at the start of
// `new` and `old` share the same keys. // `new` and `old` share the same keys.
let (left_offset, right_offset) = match self.diff_keyed_ends(old, new) { let (left_offset, right_offset) = match self.diff_keyed_ends(old, new, parent) {
Some(count) => count, Some(count) => count,
None => return, None => return,
}; };
@@ -493,18 +528,18 @@ impl<'b> VirtualDom {
if left_offset == 0 { if left_offset == 0 {
// insert at the beginning of the old list // insert at the beginning of the old list
let foothold = &old[old.len() - right_offset]; let foothold = &old[old.len() - right_offset];
self.create_and_insert_before(new_middle, foothold); self.create_and_insert_before(new_middle, foothold, parent);
} else if right_offset == 0 { } else if right_offset == 0 {
// insert at the end the old list // insert at the end the old list
let foothold = old.last().unwrap(); let foothold = old.last().unwrap();
self.create_and_insert_after(new_middle, foothold); self.create_and_insert_after(new_middle, foothold, parent);
} else { } else {
// inserting in the middle // inserting in the middle
let foothold = &old[left_offset - 1]; let foothold = &old[left_offset - 1];
self.create_and_insert_after(new_middle, foothold); self.create_and_insert_after(new_middle, foothold, parent);
} }
} else { } else {
self.diff_keyed_middle(old_middle, new_middle); self.diff_keyed_middle(old_middle, new_middle, parent);
} }
} }
@@ -517,6 +552,7 @@ impl<'b> VirtualDom {
&mut self, &mut self,
old: &'b [VNode<'b>], old: &'b [VNode<'b>],
new: &'b [VNode<'b>], new: &'b [VNode<'b>],
parent: ElementRef,
) -> Option<(usize, usize)> { ) -> Option<(usize, usize)> {
let mut left_offset = 0; let mut left_offset = 0;
@@ -532,7 +568,7 @@ impl<'b> VirtualDom {
// If that was all of the old children, then create and append the remaining // If that was all of the old children, then create and append the remaining
// new children and we're finished. // new children and we're finished.
if left_offset == old.len() { if left_offset == old.len() {
self.create_and_insert_after(&new[left_offset..], old.last().unwrap()); self.create_and_insert_after(&new[left_offset..], old.last().unwrap(), parent);
return None; return None;
} }
@@ -571,7 +607,12 @@ impl<'b> VirtualDom {
// //
// Upon exit from this function, it will be restored to that same self. // Upon exit from this function, it will be restored to that same self.
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
fn diff_keyed_middle(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { fn diff_keyed_middle(
&mut self,
old: &'b [VNode<'b>],
new: &'b [VNode<'b>],
parent: ElementRef,
) {
/* /*
1. Map the old keys into a numerical ordering based on indices. 1. Map the old keys into a numerical ordering based on indices.
2. Create a map of old key to its index 2. Create a map of old key to its index
@@ -628,7 +669,7 @@ impl<'b> VirtualDom {
if shared_keys.is_empty() { if shared_keys.is_empty() {
if !old.is_empty() { if !old.is_empty() {
self.remove_nodes(&old[1..]); self.remove_nodes(&old[1..]);
self.replace(&old[0], new); self.replace(&old[0], new, Some(parent));
} else { } else {
// I think this is wrong - why are we appending? // I think this is wrong - why are we appending?
// only valid of the if there are no trailing elements // only valid of the if there are no trailing elements
@@ -805,20 +846,38 @@ impl<'b> VirtualDom {
.sum() .sum()
} }
fn create_children(&mut self, nodes: impl IntoIterator<Item = &'b VNode<'b>>) -> usize { pub(crate) fn create_children(
&mut self,
nodes: impl IntoIterator<Item = &'b VNode<'b>>,
parent: Option<ElementRef>,
) -> usize {
nodes nodes
.into_iter() .into_iter()
.fold(0, |acc, child| acc + self.create(child)) .map(|child| {
self.assign_boundary_ref(parent, child);
self.create(child)
})
.sum()
} }
fn create_and_insert_before(&mut self, new: &'b [VNode<'b>], before: &'b VNode<'b>) { fn create_and_insert_before(
let m = self.create_children(new); &mut self,
new: &'b [VNode<'b>],
before: &'b VNode<'b>,
parent: ElementRef,
) {
let m = self.create_children(new, Some(parent));
let id = self.find_first_element(before); let id = self.find_first_element(before);
self.mutations.push(Mutation::InsertBefore { id, m }) self.mutations.push(Mutation::InsertBefore { id, m })
} }
fn create_and_insert_after(&mut self, new: &'b [VNode<'b>], after: &'b VNode<'b>) { fn create_and_insert_after(
let m = self.create_children(new); &mut self,
new: &'b [VNode<'b>],
after: &'b VNode<'b>,
parent: ElementRef,
) {
let m = self.create_children(new, Some(parent));
let id = self.find_last_element(after); let id = self.find_last_element(after);
self.mutations.push(Mutation::InsertAfter { id, m }) self.mutations.push(Mutation::InsertAfter { id, m })
} }
@@ -828,15 +887,21 @@ impl<'b> VirtualDom {
&mut self, &mut self,
l: &'b VPlaceholder, l: &'b VPlaceholder,
r: impl IntoIterator<Item = &'b VNode<'b>>, r: impl IntoIterator<Item = &'b VNode<'b>>,
parent: ElementRef,
) { ) {
let m = self.create_children(r); let m = self.create_children(r, Some(parent));
let id = l.id.get().unwrap(); let id = l.id.get().unwrap();
self.mutations.push(Mutation::ReplaceWith { id, m }); self.mutations.push(Mutation::ReplaceWith { id, m });
self.reclaim(id); self.reclaim(id);
} }
fn replace(&mut self, left: &'b VNode<'b>, right: impl IntoIterator<Item = &'b VNode<'b>>) { fn replace(
let m = self.create_children(right); &mut self,
left: &'b VNode<'b>,
right: impl IntoIterator<Item = &'b VNode<'b>>,
parent: Option<ElementRef>,
) {
let m = self.create_children(right, parent);
let pre_edits = self.mutations.edits.len(); let pre_edits = self.mutations.edits.len();
@@ -855,11 +920,12 @@ impl<'b> VirtualDom {
}; };
} }
fn node_to_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b VPlaceholder) { fn node_to_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b VPlaceholder, parent: ElementRef) {
// Create the placeholder first, ensuring we get a dedicated ID for the placeholder // Create the placeholder first, ensuring we get a dedicated ID for the placeholder
let placeholder = self.next_element(&l[0], &[]); let placeholder = self.next_element();
r.id.set(Some(placeholder)); r.id.set(Some(placeholder));
r.parent.set(Some(parent));
self.mutations self.mutations
.push(Mutation::CreatePlaceholder { id: placeholder }); .push(Mutation::CreatePlaceholder { id: placeholder });
@@ -897,6 +963,16 @@ impl<'b> VirtualDom {
// Clean up the roots, assuming we need to generate mutations for these // Clean up the roots, assuming we need to generate mutations for these
// This is done last in order to preserve Node ID reclaim order (reclaim in reverse order of claim) // This is done last in order to preserve Node ID reclaim order (reclaim in reverse order of claim)
self.reclaim_roots(node, gen_muts); self.reclaim_roots(node, gen_muts);
// Clean up the vnode id
self.reclaim_vnode_id(node);
}
fn reclaim_vnode_id(&mut self, node: &'b VNode<'b>) {
// Clean up the vnode id
if let Some(id) = node.stable_id() {
self.element_refs.remove(id.0);
}
} }
fn reclaim_roots(&mut self, node: &VNode, gen_muts: bool) { fn reclaim_roots(&mut self, node: &VNode, gen_muts: bool) {
@@ -1055,6 +1131,13 @@ impl<'b> VirtualDom {
} }
} }
} }
pub(crate) fn assign_boundary_ref(&mut self, parent: Option<ElementRef>, child: &'b VNode<'b>) {
if let Some(parent) = parent {
// assign the parent of the child
child.parent.set(Some(parent));
}
}
} }
/// Are the templates the same? /// Are the templates the same?
+350 -62
View File
@@ -1,25 +1,68 @@
use crate::{ScopeId, ScopeState}; use crate::{
scope_context::{consume_context, current_scope_id, schedule_update_any},
Element, IntoDynNode, LazyNodes, Properties, Scope, ScopeId, ScopeState, Template,
TemplateAttribute, TemplateNode, VNode,
};
use std::{ use std::{
any::{Any, TypeId}, any::{Any, TypeId},
backtrace::Backtrace,
cell::RefCell, cell::RefCell,
fmt::Debug, error::Error,
fmt::{Debug, Display},
rc::Rc,
sync::Arc,
}; };
/// A boundary that will capture any errors from child components /// Provide an error boundary to catch errors from child components
pub struct ErrorBoundary { pub fn use_error_boundary(cx: &ScopeState) -> &ErrorBoundary {
error: RefCell<Option<CapturedError>>, cx.use_hook(|| cx.provide_context(ErrorBoundary::new()))
_id: ScopeId,
} }
/// A boundary that will capture any errors from child components
#[derive(Debug, Clone, Default)]
pub struct ErrorBoundary {
inner: Rc<ErrorBoundaryInner>,
}
/// A boundary that will capture any errors from child components
pub struct ErrorBoundaryInner {
error: RefCell<Option<CapturedError>>,
_id: ScopeId,
rerun_boundary: Arc<dyn Fn(ScopeId) + Send + Sync>,
}
impl Debug for ErrorBoundaryInner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ErrorBoundaryInner")
.field("error", &self.error)
.finish()
}
}
#[derive(Debug)]
/// An instance of an error captured by a descendant component. /// An instance of an error captured by a descendant component.
pub struct CapturedError { pub struct CapturedError {
/// The error captured by the error boundary /// The error captured by the error boundary
pub error: Box<dyn Debug + 'static>, pub error: Box<dyn Debug + 'static>,
/// The backtrace of the error
pub backtrace: Backtrace,
/// The scope that threw the error /// The scope that threw the error
pub scope: ScopeId, pub scope: ScopeId,
} }
impl Display for CapturedError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(
"Encountered error: {:?}\nIn scope: {:?}\nBacktrace: {}",
self.error, self.scope, self.backtrace
))
}
}
impl Error for CapturedError {}
impl CapturedError { impl CapturedError {
/// Downcast the error type into a concrete error type /// Downcast the error type into a concrete error type
pub fn downcast<T: 'static>(&self) -> Option<&T> { pub fn downcast<T: 'static>(&self) -> Option<&T> {
@@ -32,17 +75,56 @@ impl CapturedError {
} }
} }
impl ErrorBoundary { impl Default for ErrorBoundaryInner {
pub fn new(id: ScopeId) -> Self { fn default() -> Self {
Self { Self {
error: RefCell::new(None), error: RefCell::new(None),
_id: id, _id: current_scope_id()
.expect("Cannot create an error boundary outside of a component's scope."),
rerun_boundary: schedule_update_any().unwrap(),
}
}
}
impl ErrorBoundary {
/// Create a new error boundary
pub fn new() -> Self {
Self::default()
}
/// 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 {
Self {
inner: Rc::new(ErrorBoundaryInner {
error: RefCell::new(None),
_id: scope,
rerun_boundary,
}),
} }
} }
/// Push an error into this Error Boundary /// Push an error into this Error Boundary
pub fn insert_error(&self, scope: ScopeId, error: Box<dyn Debug + 'static>) { pub fn insert_error(
self.error.replace(Some(CapturedError { error, scope })); &self,
scope: ScopeId,
error: Box<dyn Debug + 'static>,
backtrace: Backtrace,
) {
println!("{:?} {:?}", error, self.inner._id);
self.inner.error.replace(Some(CapturedError {
error,
scope,
backtrace,
}));
(self.inner.rerun_boundary)(self.inner._id);
}
/// Take any error that has been captured by this error boundary
pub fn take_error(&self) -> Option<CapturedError> {
self.inner.error.take()
} }
} }
@@ -59,7 +141,7 @@ impl ErrorBoundary {
/// ```rust, ignore /// ```rust, ignore
/// #[component] /// #[component]
/// fn App(cx: Scope, count: String) -> Element { /// fn App(cx: Scope, count: String) -> Element {
/// let id: i32 = count.parse().throw(cx)?; /// let id: i32 = count.parse().throw()?;
/// ///
/// cx.render(rsx! { /// cx.render(rsx! {
/// div { "Count {}" } /// div { "Count {}" }
@@ -84,14 +166,14 @@ pub trait Throw<S = ()>: Sized {
/// ```rust, ignore /// ```rust, ignore
/// #[component] /// #[component]
/// fn App(cx: Scope, count: String) -> Element { /// fn App(cx: Scope, count: String) -> Element {
/// let id: i32 = count.parse().throw(cx)?; /// let id: i32 = count.parse().throw()?;
/// ///
/// cx.render(rsx! { /// cx.render(rsx! {
/// div { "Count {}" } /// div { "Count {}" }
/// }) /// })
/// } /// }
/// ``` /// ```
fn throw(self, cx: &ScopeState) -> Option<Self::Out>; fn throw(self) -> Option<Self::Out>;
/// Returns an option that evaluates to None if there is an error, injecting the error to the nearest error boundary. /// Returns an option that evaluates to None if there is an error, injecting the error to the nearest error boundary.
/// ///
@@ -107,45 +189,46 @@ pub trait Throw<S = ()>: Sized {
/// ```rust, ignore /// ```rust, ignore
/// #[component] /// #[component]
/// fn App(cx: Scope, count: String) -> Element { /// fn App(cx: Scope, count: String) -> Element {
/// let id: i32 = count.parse().throw(cx)?; /// let id: i32 = count.parse().throw()?;
/// ///
/// cx.render(rsx! { /// cx.render(rsx! {
/// div { "Count {}" } /// div { "Count {}" }
/// }) /// })
/// } /// }
/// ``` /// ```
fn throw_with<D: Debug + 'static>( fn throw_with<D: Debug + 'static>(self, e: impl FnOnce() -> D) -> Option<Self::Out> {
self, self.throw().or_else(|| throw_error(e()))
cx: &ScopeState, }
e: impl FnOnce() -> D, }
) -> Option<Self::Out>;
fn throw_error<T>(e: impl Debug + 'static) -> Option<T> {
if let Some(cx) = consume_context::<ErrorBoundary>() {
match current_scope_id() {
Some(id) => cx.insert_error(id, Box::new(e), Backtrace::capture()),
None => {
tracing::error!("Cannot throw error outside of a component's scope.")
}
}
}
None
} }
/// We call clone on any errors that can be owned out of a reference /// We call clone on any errors that can be owned out of a reference
impl<'a, T, O: Debug + 'static, E: ToOwned<Owned = O>> Throw for &'a Result<T, E> { impl<'a, T, O: Debug + 'static, E: ToOwned<Owned = O>> Throw for &'a Result<T, E> {
type Out = &'a T; type Out = &'a T;
fn throw(self, cx: &ScopeState) -> Option<Self::Out> { fn throw(self) -> Option<Self::Out> {
match self { match self {
Ok(t) => Some(t), Ok(t) => Some(t),
Err(e) => { Err(e) => throw_error(e.to_owned()),
cx.throw(e.to_owned());
None
}
} }
} }
fn throw_with<D: Debug + 'static>( fn throw_with<D: Debug + 'static>(self, err: impl FnOnce() -> D) -> Option<Self::Out> {
self,
cx: &ScopeState,
err: impl FnOnce() -> D,
) -> Option<Self::Out> {
match self { match self {
Ok(t) => Some(t), Ok(t) => Some(t),
Err(_e) => { Err(_e) => throw_error(err()),
cx.throw(err());
None
}
} }
} }
} }
@@ -154,25 +237,15 @@ impl<'a, T, O: Debug + 'static, E: ToOwned<Owned = O>> Throw for &'a Result<T, E
impl<T, E: Debug + 'static> Throw for Result<T, E> { impl<T, E: Debug + 'static> Throw for Result<T, E> {
type Out = T; type Out = T;
fn throw(self, cx: &ScopeState) -> Option<T> { fn throw(self) -> Option<T> {
match self { match self {
Ok(t) => Some(t), Ok(t) => Some(t),
Err(e) => { Err(e) => throw_error(e),
cx.throw(e);
None
}
} }
} }
fn throw_with<D: Debug + 'static>( fn throw_with<D: Debug + 'static>(self, error: impl FnOnce() -> D) -> Option<Self::Out> {
self, self.ok().or_else(|| throw_error(error()))
cx: &ScopeState,
error: impl FnOnce() -> D,
) -> Option<Self::Out> {
self.ok().or_else(|| {
cx.throw(error());
None
})
} }
} }
@@ -180,21 +253,236 @@ impl<T, E: Debug + 'static> Throw for Result<T, E> {
impl<T> Throw for Option<T> { impl<T> Throw for Option<T> {
type Out = T; type Out = T;
fn throw(self, cx: &ScopeState) -> Option<T> { fn throw(self) -> Option<T> {
self.or_else(|| { self.or_else(|| throw_error("Attempted to unwrap a None value."))
cx.throw("None error.");
None
})
} }
fn throw_with<D: Debug + 'static>( fn throw_with<D: Debug + 'static>(self, error: impl FnOnce() -> D) -> Option<Self::Out> {
self, self.or_else(|| throw_error(error()))
cx: &ScopeState, }
error: impl FnOnce() -> D, }
) -> Option<Self::Out> {
self.or_else(|| { pub struct ErrorHandler<'a>(Box<dyn Fn(CapturedError) -> LazyNodes<'a, 'a> + 'a>);
cx.throw(error()); impl<'a, F: Fn(CapturedError) -> LazyNodes<'a, 'a> + 'a> From<F> for ErrorHandler<'a> {
None fn from(value: F) -> Self {
}) Self(Box::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 }],
}],
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([]),
}
})
}
pub struct ErrorBoundaryProps<'a> {
children: Element<'a>,
handle_error: ErrorHandler<'a>,
}
impl<'a> ErrorBoundaryProps<'a> {
/**
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(),
}
}
}
#[must_use]
#[doc(hidden)]
#[allow(dead_code, non_camel_case_types, non_snake_case)]
pub struct ErrorBoundaryPropsBuilder<'a, TypedBuilderFields> {
fields: TypedBuilderFields,
_phantom: ::core::marker::PhantomData<&'a ()>,
}
impl<'a, TypedBuilderFields> Clone for ErrorBoundaryPropsBuilder<'a, TypedBuilderFields>
where
TypedBuilderFields: Clone,
{
fn clone(&self) -> Self {
Self {
fields: self.fields.clone(),
_phantom: ::core::default::Default::default(),
}
}
}
impl<'a> Properties for ErrorBoundaryProps<'a> {
type Builder = ErrorBoundaryPropsBuilder<'a, ((), ())>;
const IS_STATIC: bool = false;
fn builder() -> Self::Builder {
ErrorBoundaryProps::builder()
}
unsafe fn memoize(&self, _: &Self) -> bool {
false
}
}
#[doc(hidden)]
#[allow(dead_code, non_camel_case_types, non_snake_case)]
pub trait ErrorBoundaryPropsBuilder_Optional<T> {
fn into_value<F: FnOnce() -> T>(self, default: F) -> T;
}
impl<T> ErrorBoundaryPropsBuilder_Optional<T> for () {
fn into_value<F: FnOnce() -> T>(self, default: F) -> T {
default()
}
}
impl<T> ErrorBoundaryPropsBuilder_Optional<T> for (T,) {
fn into_value<F: FnOnce() -> T>(self, _: F) -> T {
self.0
}
}
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl<'a, __handle_error> ErrorBoundaryPropsBuilder<'a, ((), __handle_error)> {
pub fn children(
self,
children: Element<'a>,
) -> ErrorBoundaryPropsBuilder<'a, ((Element<'a>,), __handle_error)> {
let children = (children,);
let (_, handle_error) = self.fields;
ErrorBoundaryPropsBuilder {
fields: (children, handle_error),
_phantom: self._phantom,
}
}
}
#[doc(hidden)]
#[allow(dead_code, non_camel_case_types, non_snake_case)]
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)> {
#[deprecated(note = "Repeated field children")]
pub fn children(
self,
_: ErrorBoundaryPropsBuilder_Error_Repeated_field_children,
) -> ErrorBoundaryPropsBuilder<'a, ((Element<'a>,), __handle_error)> {
self
}
}
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl<'a, __children> ErrorBoundaryPropsBuilder<'a, (__children, ())> {
pub fn handle_error(
self,
handle_error: impl ::core::convert::Into<ErrorHandler<'a>>,
) -> ErrorBoundaryPropsBuilder<'a, (__children, (ErrorHandler<'a>,))> {
let handle_error = (handle_error.into(),);
let (children, _) = self.fields;
ErrorBoundaryPropsBuilder {
fields: (children, handle_error),
_phantom: self._phantom,
}
}
}
#[doc(hidden)]
#[allow(dead_code, non_camel_case_types, non_snake_case)]
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>,))> {
#[deprecated(note = "Repeated field handle_error")]
pub fn handle_error(
self,
_: ErrorBoundaryPropsBuilder_Error_Repeated_field_handle_error,
) -> ErrorBoundaryPropsBuilder<'a, (__children, (ErrorHandler<'a>,))> {
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)>
{
pub fn build(self) -> ErrorBoundaryProps<'a> {
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))
});
ErrorBoundaryProps {
children,
handle_error,
}
}
}
/// Create a new error boundary component.
///
/// ## Details
///
/// Error boundaries handle errors within a specific part of your application. Any errors passed in a child with [`Throw`] will be caught by the nearest error boundary.
///
/// ## Example
///
/// ```rust, ignore
/// rsx!{
/// ErrorBoundary {
/// handle_error: |error| rsx! { "Oops, we encountered an error. Please report {error} to the developer of this application" }
/// ThrowsError {}
/// }
/// }
/// ```
///
/// ## Usage
///
/// Error boundaries are an easy way to handle errors in your application.
/// 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);
match error_boundary.take_error() {
Some(error) => cx.render((cx.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: 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([{
let ___nodes = (&cx.props.children).into_dyn_node(__cx);
___nodes
}]),
dynamic_attrs: __cx.bump().alloc([]),
}
}),
} }
} }
+21 -2
View File
@@ -28,6 +28,27 @@ pub struct Event<T: 'static + ?Sized> {
} }
impl<T> Event<T> { impl<T> Event<T> {
/// Map the event data to a new type
///
/// # Example
///
/// ```rust, ignore
/// rsx! {
/// button {
/// onclick: move |evt: Event<FormData>| {
/// let data = evt.map(|data| data.value());
/// assert_eq!(data.inner(), "hello world");
/// }
/// }
/// }
/// ```
pub fn map<U: 'static, F: FnOnce(&T) -> U>(&self, f: F) -> Event<U> {
Event {
data: Rc::new(f(&self.data)),
propagates: self.propagates.clone(),
}
}
/// Prevent this event from continuing to bubble up the tree to parent elements. /// Prevent this event from continuing to bubble up the tree to parent elements.
/// ///
/// # Example /// # Example
@@ -107,8 +128,6 @@ impl<T: std::fmt::Debug> std::fmt::Debug for Event<T> {
} }
} }
#[doc(hidden)]
/// The callback type generated by the `rsx!` macro when an `on` field is specified for components. /// The callback type generated by the `rsx!` macro when an `on` field is specified for components.
/// ///
/// This makes it possible to pass `move |evt| {}` style closures into components as property fields. /// This makes it possible to pass `move |evt| {}` style closures into components as property fields.
+2 -1
View File
@@ -30,7 +30,8 @@ pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element {
let children = cx.props.0.as_ref()?; let children = cx.props.0.as_ref()?;
Some(VNode { Some(VNode {
key: children.key, key: children.key,
parent: children.parent, parent: children.parent.clone(),
stable_id: children.stable_id.clone(),
template: children.template.clone(), template: children.template.clone(),
root_ids: children.root_ids.clone(), root_ids: children.root_ids.clone(),
dynamic_nodes: children.dynamic_nodes, dynamic_nodes: children.dynamic_nodes,
+1 -1
View File
@@ -18,7 +18,7 @@ use crate::{innerlude::VNode, ScopeState};
/// A concrete type provider for closures that build [`VNode`] structures. /// 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 /// 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. /// closures, but if we wrap the closure in a concrete type, we can use it for different branches in matching.
/// ///
/// ///
+6 -5
View File
@@ -90,11 +90,12 @@ pub mod prelude {
pub use crate::innerlude::{ pub use crate::innerlude::{
consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, has_context, 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, provide_context, provide_context_to_scope, provide_root_context, push_future,
remove_future, schedule_update_any, spawn, spawn_forever, suspend, throw, AnyValue, remove_future, schedule_update_any, spawn, spawn_forever, suspend, throw,
Attribute, AttributeType, Component, Element, Event, EventHandler, Fragment, HasAttributes, use_error_boundary, AnyValue, AnyValue, Attribute, AttributeType, Component, Element,
IntoAttributeValue, IntoDynNode, LazyNodes, MountedAttribute, Properties, Runtime, ErrorBoundary, Event, EventHandler, Fragment, HasAttributes, IntoAttributeValue,
RuntimeGuard, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, IntoDynNode, LazyNodes, MountedAttribute, Properties, Runtime, RuntimeGuard, Scope,
TemplateNode, Throw, VNode, VirtualDom, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, Throw,
VNode, VirtualDom,
}; };
} }
+2 -2
View File
@@ -26,7 +26,7 @@ pub struct Mutations<'a> {
/// Any templates encountered while diffing the DOM. /// Any templates encountered while diffing the DOM.
/// ///
/// These must be loaded into a cache before applying the edits /// These must be loaded into a cache before applying the edits
pub templates: Vec<Template<'a>>, pub templates: Vec<Template<'static>>,
/// Any mutations required to patch the renderer to match the layout of the VirtualDom /// Any mutations required to patch the renderer to match the layout of the VirtualDom
pub edits: Vec<Mutation<'a>>, pub edits: Vec<Mutation<'a>>,
@@ -91,7 +91,7 @@ pub enum Mutation<'a> {
id: ElementId, id: ElementId,
}, },
/// Create an placeholder int he DOM that we will use later. /// 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 /// Dioxus currently requires the use of placeholders to maintain a re-entrance point for things like list diffing
CreatePlaceholder { CreatePlaceholder {
+64 -32
View File
@@ -1,3 +1,4 @@
use crate::innerlude::{ElementRef, VNodeId};
use crate::{ use crate::{
any_props::AnyProps, arena::ElementId, Element, Event, LazyNodes, ScopeId, ScopeState, any_props::AnyProps, arena::ElementId, Element, Event, LazyNodes, ScopeId, ScopeState,
}; };
@@ -47,7 +48,10 @@ pub struct VNode<'a> {
pub key: Option<&'a str>, pub key: Option<&'a str>,
/// When rendered, this template will be linked to its parent manually /// When rendered, this template will be linked to its parent manually
pub parent: Option<ElementId>, pub(crate) parent: Cell<Option<ElementRef>>,
/// The bubble id assigned to the child that we need to update and drop when diffing happens
pub(crate) stable_id: Cell<Option<VNodeId>>,
/// The static nodes and static descriptor of the template /// The static nodes and static descriptor of the template
pub template: Cell<Template<'static>>, pub template: Cell<Template<'static>>,
@@ -68,7 +72,8 @@ impl<'a> VNode<'a> {
pub fn empty(cx: &'a ScopeState) -> Element<'a> { pub fn empty(cx: &'a ScopeState) -> Element<'a> {
Some(VNode { Some(VNode {
key: None, key: None,
parent: None, parent: Default::default(),
stable_id: Default::default(),
root_ids: RefCell::new(bumpalo::collections::Vec::new_in(cx.bump())), root_ids: RefCell::new(bumpalo::collections::Vec::new_in(cx.bump())),
dynamic_nodes: &[], dynamic_nodes: &[],
dynamic_attrs: &[], dynamic_attrs: &[],
@@ -81,6 +86,30 @@ impl<'a> VNode<'a> {
}) })
} }
/// Create a new VNode
pub fn new(
key: Option<&'a str>,
template: Template<'static>,
root_ids: bumpalo::collections::Vec<'a, ElementId>,
dynamic_nodes: &'a [DynamicNode<'a>],
dynamic_attrs: &'a [Attribute<'a>],
) -> Self {
Self {
key,
parent: Cell::new(None),
stable_id: Cell::new(None),
template: Cell::new(template),
root_ids: RefCell::new(root_ids),
dynamic_nodes,
dynamic_attrs,
}
}
/// Get the stable id of this node used for bubbling events
pub(crate) fn stable_id(&self) -> Option<VNodeId> {
self.stable_id.get()
}
/// Load a dynamic root at the given index /// Load a dynamic root at the given index
/// ///
/// Returns [`None`] if the root is actually a static node (Element/Text) /// Returns [`None`] if the root is actually a static node (Element/Text)
@@ -319,7 +348,7 @@ pub struct VComponent<'a> {
/// The function pointer of the component, known at compile time /// The function pointer of the component, known at compile time
/// ///
/// It is possible that components get folded at comppile time, so these shouldn't be really used as a key /// It is possible that components get folded at compile time, so these shouldn't be really used as a key
pub(crate) render_fn: *const (), pub(crate) render_fn: *const (),
pub(crate) props: RefCell<Option<Box<dyn AnyProps<'a> + 'a>>>, pub(crate) props: RefCell<Option<Box<dyn AnyProps<'a> + 'a>>>,
@@ -372,6 +401,8 @@ impl<'a> VText<'a> {
pub struct VPlaceholder { pub struct VPlaceholder {
/// The ID of this node in the real DOM /// The ID of this node in the real DOM
pub(crate) id: Cell<Option<ElementId>>, pub(crate) id: Cell<Option<ElementId>>,
/// The parent of this node
pub(crate) parent: Cell<Option<ElementRef>>,
} }
impl VPlaceholder { impl VPlaceholder {
@@ -720,52 +751,52 @@ pub trait IntoDynNode<'a, A = ()> {
/// Consume this item along with a scopestate and produce a DynamicNode /// Consume this item along with a scopestate and produce a DynamicNode
/// ///
/// You can use the bump alloactor of the scopestate to creat the dynamic node /// You can use the bump alloactor of the scopestate to creat the dynamic node
fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a>; fn into_dyn_node(self, cx: &'a ScopeState) -> DynamicNode<'a>;
} }
impl<'a> IntoDynNode<'a> for () { impl<'a> IntoDynNode<'a> for () {
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { fn into_dyn_node(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
DynamicNode::default() DynamicNode::default()
} }
} }
impl<'a> IntoDynNode<'a> for VNode<'a> { impl<'a> IntoDynNode<'a> for VNode<'a> {
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { fn into_dyn_node(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
DynamicNode::Fragment(_cx.bump().alloc([self])) DynamicNode::Fragment(_cx.bump().alloc([self]))
} }
} }
impl<'a> IntoDynNode<'a> for DynamicNode<'a> { impl<'a> IntoDynNode<'a> for DynamicNode<'a> {
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { fn into_dyn_node(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
self self
} }
} }
impl<'a, T: IntoDynNode<'a>> IntoDynNode<'a> for Option<T> { impl<'a, T: IntoDynNode<'a>> IntoDynNode<'a> for Option<T> {
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { fn into_dyn_node(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
match self { match self {
Some(val) => val.into_vnode(_cx), Some(val) => val.into_dyn_node(_cx),
None => DynamicNode::default(), None => DynamicNode::default(),
} }
} }
} }
impl<'a> IntoDynNode<'a> for &Element<'a> { impl<'a> IntoDynNode<'a> for &Element<'a> {
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { fn into_dyn_node(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
match self.as_ref() { match self.as_ref() {
Some(val) => val.clone().into_vnode(_cx), Some(val) => val.clone().into_dyn_node(_cx),
_ => DynamicNode::default(), _ => DynamicNode::default(),
} }
} }
} }
impl<'a, 'b> IntoDynNode<'a> for LazyNodes<'a, 'b> { impl<'a, 'b> IntoDynNode<'a> for LazyNodes<'a, 'b> {
fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a> { fn into_dyn_node(self, cx: &'a ScopeState) -> DynamicNode<'a> {
DynamicNode::Fragment(cx.bump().alloc([cx.render(self).unwrap()])) DynamicNode::Fragment(cx.bump().alloc([cx.render(self).unwrap()]))
} }
} }
impl<'a, 'b> IntoDynNode<'b> for &'a str { impl<'a, 'b> IntoDynNode<'b> for &'a str {
fn into_vnode(self, cx: &'b ScopeState) -> DynamicNode<'b> { fn into_dyn_node(self, cx: &'b ScopeState) -> DynamicNode<'b> {
DynamicNode::Text(VText { DynamicNode::Text(VText {
value: cx.bump().alloc_str(self), value: cx.bump().alloc_str(self),
id: Default::default(), id: Default::default(),
@@ -774,24 +805,25 @@ impl<'a, 'b> IntoDynNode<'b> for &'a str {
} }
impl IntoDynNode<'_> for String { impl IntoDynNode<'_> for String {
fn into_vnode(self, cx: &ScopeState) -> DynamicNode { fn into_dyn_node(self, cx: &ScopeState) -> DynamicNode {
DynamicNode::Text(VText { DynamicNode::Text(VText {
value: cx.bump().alloc(self), value: cx.bump().alloc_str(&self),
id: Default::default(), id: Default::default(),
}) })
} }
} }
impl<'b> IntoDynNode<'b> for Arguments<'_> { impl<'b> IntoDynNode<'b> for Arguments<'_> {
fn into_vnode(self, cx: &'b ScopeState) -> DynamicNode<'b> { fn into_dyn_node(self, cx: &'b ScopeState) -> DynamicNode<'b> {
cx.text_node(self) cx.text_node(self)
} }
} }
impl<'a> IntoDynNode<'a> for &'a VNode<'a> { impl<'a> IntoDynNode<'a> for &'a VNode<'a> {
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { fn into_dyn_node(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
DynamicNode::Fragment(_cx.bump().alloc([VNode { DynamicNode::Fragment(_cx.bump().alloc([VNode {
parent: self.parent, parent: self.parent.clone(),
stable_id: self.stable_id.clone(),
template: self.template.clone(), template: self.template.clone(),
root_ids: self.root_ids.clone(), root_ids: self.root_ids.clone(),
key: self.key, key: self.key,
@@ -801,24 +833,24 @@ impl<'a> IntoDynNode<'a> for &'a VNode<'a> {
} }
} }
pub trait IntoTemplate<'a> { pub trait IntoVNode<'a> {
fn into_template(self, _cx: &'a ScopeState) -> VNode<'a>; fn into_vnode(self, _cx: &'a ScopeState) -> VNode<'a>;
} }
impl<'a> IntoTemplate<'a> for VNode<'a> { impl<'a> IntoVNode<'a> for VNode<'a> {
fn into_template(self, _cx: &'a ScopeState) -> VNode<'a> { fn into_vnode(self, _cx: &'a ScopeState) -> VNode<'a> {
self self
} }
} }
impl<'a> IntoTemplate<'a> for Element<'a> { impl<'a> IntoVNode<'a> for Element<'a> {
fn into_template(self, cx: &'a ScopeState) -> VNode<'a> { fn into_vnode(self, cx: &'a ScopeState) -> VNode<'a> {
match self { match self {
Some(val) => val.into_template(cx), Some(val) => val.into_vnode(cx),
_ => VNode::empty(cx).unwrap(), _ => VNode::empty(cx).unwrap(),
} }
} }
} }
impl<'a, 'b> IntoTemplate<'a> for LazyNodes<'a, 'b> { impl<'a, 'b> IntoVNode<'a> for LazyNodes<'a, 'b> {
fn into_template(self, cx: &'a ScopeState) -> VNode<'a> { fn into_vnode(self, cx: &'a ScopeState) -> VNode<'a> {
cx.render(self).unwrap() cx.render(self).unwrap()
} }
} }
@@ -828,12 +860,12 @@ pub struct FromNodeIterator;
impl<'a, T, I> IntoDynNode<'a, FromNodeIterator> for T impl<'a, T, I> IntoDynNode<'a, FromNodeIterator> for T
where where
T: Iterator<Item = I>, T: Iterator<Item = I>,
I: IntoTemplate<'a>, I: IntoVNode<'a>,
{ {
fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a> { fn into_dyn_node(self, cx: &'a ScopeState) -> DynamicNode<'a> {
let mut nodes = bumpalo::collections::Vec::new_in(cx.bump()); let mut nodes = bumpalo::collections::Vec::new_in(cx.bump());
nodes.extend(self.into_iter().map(|node| node.into_template(cx))); nodes.extend(self.into_iter().map(|node| node.into_vnode(cx)));
match nodes.into_bump_slice() { match nodes.into_bump_slice() {
children if children.is_empty() => DynamicNode::default(), children if children.is_empty() => DynamicNode::default(),
@@ -861,8 +893,8 @@ impl<'a> IntoAttributeValue<'a> for &'a str {
} }
impl<'a> IntoAttributeValue<'a> for String { impl<'a> IntoAttributeValue<'a> for String {
fn into_value(self, bump: &'a Bump) -> AttributeValue<'a> { fn into_value(self, cx: &'a Bump) -> AttributeValue<'a> {
AttributeValue::Text(bump.alloc(self)) AttributeValue::Text(cx.alloc_str(&self))
} }
} }
+1 -1
View File
@@ -98,7 +98,7 @@ impl Runtime {
} }
} }
/// A gaurd 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. /// 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.
/// ///
/// ```rust /// ```rust
/// use dioxus::prelude::*; /// use dioxus::prelude::*;
+11 -7
View File
@@ -19,7 +19,7 @@ pub struct TaskId(pub usize);
/// the task itself is the waker /// the task itself is the waker
pub(crate) struct LocalTask { pub(crate) struct LocalTask {
pub scope: ScopeId, pub scope: ScopeId,
pub(super) task: RefCell<Pin<Box<dyn Future<Output = ()> + 'static>>>, pub task: RefCell<Pin<Box<dyn Future<Output = ()> + 'static>>>,
pub waker: Waker, pub waker: Waker,
} }
@@ -48,11 +48,15 @@ impl Scheduler {
})), })),
}; };
entry.insert(task); let mut cx = std::task::Context::from_waker(&task.waker);
self.sender if !task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() {
.unbounded_send(SchedulerMsg::TaskNotified(task_id)) self.sender
.expect("Scheduler should exist"); .unbounded_send(SchedulerMsg::TaskNotified(task_id))
.expect("Scheduler should exist");
}
entry.insert(task);
task_id task_id
} }
@@ -60,8 +64,8 @@ impl Scheduler {
/// Drop the future with the given TaskId /// 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 /// 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) { pub fn remove(&self, id: TaskId) -> Option<LocalTask> {
self.tasks.borrow_mut().try_remove(id.0); self.tasks.borrow_mut().try_remove(id.0)
} }
} }
+1
View File
@@ -36,6 +36,7 @@ impl VirtualDom {
borrowed_props: Default::default(), borrowed_props: Default::default(),
attributes_to_drop_before_render: Default::default(), attributes_to_drop_before_render: Default::default(),
element_refs_to_drop: Default::default(),
})); }));
let context = let context =
+2 -31
View File
@@ -1,5 +1,5 @@
use crate::{ use crate::{
innerlude::{ErrorBoundary, Scheduler, SchedulerMsg}, innerlude::{Scheduler, SchedulerMsg},
runtime::{with_current_scope, with_runtime}, runtime::{with_current_scope, with_runtime},
Element, ScopeId, TaskId, Element, ScopeId, TaskId,
}; };
@@ -7,7 +7,6 @@ use rustc_hash::FxHashSet;
use std::{ use std::{
any::Any, any::Any,
cell::{Cell, RefCell}, cell::{Cell, RefCell},
fmt::Debug,
future::Future, future::Future,
rc::Rc, rc::Rc,
sync::Arc, sync::Arc,
@@ -230,17 +229,7 @@ impl ScopeContext {
/// This is good for tasks that need to be run after the component has been dropped. /// 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) -> TaskId {
// The root scope will never be unmounted so we can just add the task at the top of the app // The root scope will never be unmounted so we can just add the task at the top of the app
let id = self.tasks.spawn(ScopeId::ROOT, fut); self.tasks.spawn(ScopeId::ROOT, fut)
// wake up the scheduler if it is sleeping
self.tasks
.sender
.unbounded_send(SchedulerMsg::TaskNotified(id))
.expect("Scheduler should exist");
self.spawned_tasks.borrow_mut().insert(id);
id
} }
/// Informs the scheduler that this task is no longer needed and should be removed. /// Informs the scheduler that this task is no longer needed and should be removed.
@@ -250,19 +239,6 @@ impl ScopeContext {
self.tasks.remove(id); self.tasks.remove(id);
} }
/// Inject an error into the nearest error boundary and quit rendering
///
/// The error doesn't need to implement Error or any specific traits since the boundary
/// itself will downcast the error into a trait object.
pub fn throw(&self, error: impl Debug + 'static) -> Option<()> {
if let Some(cx) = self.consume_context::<Rc<ErrorBoundary>>() {
cx.insert_error(self.scope_id(), Box::new(error));
}
// Always return none during a throw
None
}
/// Mark this component as suspended and then return None /// Mark this component as suspended and then return None
pub fn suspend(&self) -> Option<Element> { pub fn suspend(&self) -> Option<Element> {
self.suspended.set(true); self.suspended.set(true);
@@ -332,11 +308,6 @@ pub fn suspend() -> Option<Element<'static>> {
None None
} }
/// Throw an error into the nearest error boundary
pub fn throw(error: impl Debug + 'static) -> Option<()> {
with_current_scope(|cx| cx.throw(error)).flatten()
}
/// Pushes the future onto the poll queue to be polled after the component renders. /// 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> { pub fn push_future(fut: impl Future<Output = ()> + 'static) -> Option<TaskId> {
with_current_scope(|cx| cx.push_future(fut)) with_current_scope(|cx| cx.push_future(fut))
+10 -18
View File
@@ -2,8 +2,7 @@ use crate::{
any_props::AnyProps, any_props::AnyProps,
any_props::VProps, any_props::VProps,
bump_frame::BumpFrame, bump_frame::BumpFrame,
innerlude::ErrorBoundary, innerlude::{DynamicNode, EventHandler, VComponent, VNodeId, VText},
innerlude::{DynamicNode, EventHandler, VComponent, VText},
lazynodes::LazyNodes, lazynodes::LazyNodes,
nodes::{IntoAttributeValue, IntoDynNode, RenderReturn}, nodes::{IntoAttributeValue, IntoDynNode, RenderReturn},
runtime::Runtime, runtime::Runtime,
@@ -95,6 +94,7 @@ pub struct ScopeState {
pub(crate) hook_idx: Cell<usize>, pub(crate) hook_idx: Cell<usize>,
pub(crate) borrowed_props: RefCell<Vec<*const VComponent<'static>>>, 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) attributes_to_drop_before_render: RefCell<Vec<*const Attribute<'static>>>,
pub(crate) props: Option<Box<dyn AnyProps<'static>>>, pub(crate) props: Option<Box<dyn AnyProps<'static>>>,
@@ -370,12 +370,17 @@ impl<'src> ScopeState {
} }
let mut props = self.borrowed_props.borrow_mut(); 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 element.dynamic_nodes { for node in element.dynamic_nodes {
if let DynamicNode::Component(comp) = node { if let DynamicNode::Component(comp) = node {
let unbounded = unsafe { std::mem::transmute(comp as *const VComponent) };
if !comp.static_props { if !comp.static_props {
let unbounded = unsafe { std::mem::transmute(comp as *const VComponent) };
props.push(unbounded); props.push(unbounded);
} }
drop_props.push(unbounded);
} }
} }
@@ -404,7 +409,7 @@ impl<'src> ScopeState {
/// Convert any item that implements [`IntoDynNode`] into a [`DynamicNode`] using the internal [`Bump`] allocator /// 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 { pub fn make_node<'c, I>(&'src self, into: impl IntoDynNode<'src, I> + 'c) -> DynamicNode {
into.into_vnode(self) into.into_dyn_node(self)
} }
/// Create a new [`Attribute`] from a name, value, namespace, and volatile bool /// Create a new [`Attribute`] from a name, value, namespace, and volatile bool
@@ -466,7 +471,7 @@ impl<'src> ScopeState {
render_fn: component as *const (), render_fn: component as *const (),
static_props: P::IS_STATIC, static_props: P::IS_STATIC,
props: RefCell::new(Some(extended)), props: RefCell::new(Some(extended)),
scope: Cell::new(None), scope: Default::default(),
}) })
} }
@@ -517,19 +522,6 @@ impl<'src> ScopeState {
AttributeValue::Any(RefCell::new(Some(boxed))) AttributeValue::Any(RefCell::new(Some(boxed)))
} }
/// Inject an error into the nearest error boundary and quit rendering
///
/// The error doesn't need to implement Error or any specific traits since the boundary
/// itself will downcast the error into a trait object.
pub fn throw(&self, error: impl Debug + 'static) -> Option<()> {
if let Some(cx) = self.consume_context::<Rc<ErrorBoundary>>() {
cx.insert_error(self.scope_id(), Box::new(error));
}
// Always return none during a throw
None
}
/// Mark this component as suspended and then return None /// Mark this component as suspended and then return None
pub fn suspend(&self) -> Option<Element> { pub fn suspend(&self) -> Option<Element> {
let cx = self.context(); let cx = self.context();
+91 -107
View File
@@ -11,12 +11,14 @@ use crate::{
nodes::{Template, TemplateId}, nodes::{Template, TemplateId},
runtime::{Runtime, RuntimeGuard}, runtime::{Runtime, RuntimeGuard},
scopes::{ScopeId, ScopeState}, scopes::{ScopeId, ScopeState},
Attribute, AttributeValue, Element, Event, Scope, Attribute, AttributeValue, Element, Event, Scope, VNode,
}; };
use futures_util::{pin_mut, StreamExt}; use futures_util::{pin_mut, StreamExt};
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::{FxHashMap, FxHashSet};
use slab::Slab; use slab::Slab;
use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, rc::Rc}; use std::{
any::Any, cell::Cell, collections::BTreeSet, future::Future, ptr::NonNull, rc::Rc, sync::Arc,
};
/// A virtual node system that progresses user events and diffs UI trees. /// A virtual node system that progresses user events and diffs UI trees.
/// ///
@@ -186,7 +188,10 @@ pub struct VirtualDom {
pub(crate) templates: FxHashMap<TemplateId, FxHashMap<usize, Template<'static>>>, pub(crate) templates: FxHashMap<TemplateId, FxHashMap<usize, Template<'static>>>,
// Every element is actually a dual reference - one to the template and the other to the dynamic node in that template // Every element is actually a dual reference - one to the template and the other to the dynamic node in that template
pub(crate) elements: Slab<ElementRef>, pub(crate) element_refs: Slab<Option<NonNull<VNode<'static>>>>,
// The element ids that are used in the renderer
pub(crate) elements: Slab<Option<ElementRef>>,
pub(crate) mutations: Mutations<'static>, pub(crate) mutations: Mutations<'static>,
@@ -263,6 +268,7 @@ impl VirtualDom {
dirty_scopes: Default::default(), dirty_scopes: Default::default(),
templates: Default::default(), templates: Default::default(),
elements: Default::default(), elements: Default::default(),
element_refs: Default::default(),
mutations: Mutations::default(), mutations: Mutations::default(),
suspended_scopes: Default::default(), suspended_scopes: Default::default(),
}; };
@@ -273,10 +279,13 @@ impl VirtualDom {
); );
// Unlike react, we provide a default error boundary that just renders the error as a string // Unlike react, we provide a default error boundary that just renders the error as a string
root.provide_context(Rc::new(ErrorBoundary::new(ScopeId::ROOT))); root.provide_context(Rc::new(ErrorBoundary::new_in_scope(
ScopeId::ROOT,
Arc::new(|_| {}),
)));
// the root element is always given element ID 0 since it's the container for the entire tree // the root element is always given element ID 0 since it's the container for the entire tree
dom.elements.insert(ElementRef::none()); dom.elements.insert(None);
dom dom
} }
@@ -314,9 +323,9 @@ impl VirtualDom {
} }
} }
/// Call a listener inside the VirtualDom with data from outside the VirtualDom. /// 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.**
/// ///
/// This method will identify the appropriate element. The data must match up with the listener delcared. Note that /// 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, /// this method does not give any indication as to the success of the listener call. If the listener is not found,
/// nothing will happen. /// nothing will happen.
/// ///
@@ -353,7 +362,15 @@ impl VirtualDom {
| | | <-- no, broke early | | | <-- no, broke early
| <-- no, broke early | <-- no, broke early
*/ */
let mut parent_path = self.elements.get(element.0); 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));
let mut listeners = vec![]; let mut listeners = vec![];
// We will clone this later. The data itself is wrapped in RC to be used in callbacks if required // We will clone this later. The data itself is wrapped in RC to be used in callbacks if required
@@ -365,121 +382,91 @@ impl VirtualDom {
// If the event bubbles, we traverse through the tree until we find the target element. // If the event bubbles, we traverse through the tree until we find the target element.
if bubbles { if bubbles {
// Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent. // Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent.
while let Some(el_ref) = parent_path { while let Some((path, el_ref)) = parent_node {
// safety: we maintain references of all vnodes in the element slab // safety: we maintain references of all vnodes in the element slab
if let Some(template) = el_ref.template { let template = unsafe { el_ref.unwrap().as_ref() };
let template = unsafe { template.as_ref() }; let node_template = template.template.get();
let node_template = template.template.get(); let target_path = path.path;
let target_path = el_ref.path;
for (idx, attr) in template.dynamic_attrs.iter().enumerate() { for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
let this_path = node_template.attr_paths[idx]; let this_path = node_template.attr_paths[idx];
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
if target_path.is_decendant(&this_path) { if target_path.is_decendant(&this_path) {
attr.ty.for_each(|attribute| { attr.ty.for_each(|attribute| {
if attribute.name.trim_start_matches("on") == name { if attribute.name.trim_start_matches("on") == name {
listeners.push(&attribute.value); if let AttributeValue::Listener(listener) = &attribute.value {
listeners.push(&listener);
} }
});
// 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;
} }
});
// 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.drain(..).rev() {
if let AttributeValue::Listener(listener) = listener {
let origin = el_ref.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_path = template.parent.and_then(|id| self.elements.get(id.0));
} else {
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.drain(..).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 { } else {
// Otherwise, we just call the listener on the target element // Otherwise, we just call the listener on the target element
if let Some(el_ref) = parent_path { if let Some((path, el_ref)) = parent_node {
// safety: we maintain references of all vnodes in the element slab // safety: we maintain references of all vnodes in the element slab
if let Some(template) = el_ref.template { let template = unsafe { el_ref.unwrap().as_ref() };
let template = unsafe { template.as_ref() }; let node_template = template.template.get();
let node_template = template.template.get(); let target_path = path.path;
let target_path = el_ref.path;
for (idx, attr) in template.dynamic_attrs.iter().enumerate() { for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
let this_path = node_template.attr_paths[idx]; let this_path = node_template.attr_paths[idx];
fn call_listener( // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
attribute: &Attribute, // Only call the listener if this is the exact target element.
event_name: &str, if target_path == this_path {
uievent: &Event<dyn Any>, let mut should_stop = false;
runtime: &Runtime, attr.ty.for_each(|attribute| {
origin: ScopeId, if attribute.name.trim_start_matches("on") == name {
) -> bool { if let AttributeValue::Listener(listener) = &attr.value {
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one let origin = path.scope;
// Only call the listener if this is the exact target element. self.runtime.scope_stack.borrow_mut().push(origin);
if attribute.name.trim_start_matches("on") == event_name { self.runtime.rendering.set(false);
if let AttributeValue::Listener(listener) = &attribute.value {
runtime.scope_stack.borrow_mut().push(origin);
runtime.rendering.set(false);
if let Some(cb) = listener.borrow_mut().as_deref_mut() { if let Some(cb) = listener.borrow_mut().as_deref_mut() {
cb(uievent.clone()); cb(uievent.clone());
} }
runtime.scope_stack.borrow_mut().pop(); self.runtime.scope_stack.borrow_mut().pop();
runtime.rendering.set(true); self.runtime.rendering.set(true);
return true; should_stop = true;
}
}
false
}
if target_path == this_path {
match &attr.ty {
AttributeType::Single(attribute) => {
if call_listener(
attribute,
name,
&uievent,
&self.runtime,
el_ref.scope,
) {
return;
}
}
AttributeType::Many(attributes) => {
for attribute in *attributes {
if call_listener(
attribute,
name,
&uievent,
&self.runtime,
el_ref.scope,
) {
return;
}
}
} }
} }
});
if should_stop {
return;
} }
} }
} }
@@ -602,7 +589,7 @@ impl VirtualDom {
// If an error occurs, we should try to render the default error component and context where the error occured // If an error occurs, we should try to render the default error component and context where the error occured
RenderReturn::Aborted(placeholder) => { RenderReturn::Aborted(placeholder) => {
tracing::debug!("Ran into suspended or aborted scope during rebuild"); tracing::debug!("Ran into suspended or aborted scope during rebuild");
let id = self.next_null(); let id = self.next_element();
placeholder.id.set(Some(id)); placeholder.id.set(Some(id));
self.mutations.push(Mutation::CreatePlaceholder { id }); self.mutations.push(Mutation::CreatePlaceholder { id });
} }
@@ -634,15 +621,12 @@ impl VirtualDom {
/// The mutations will be thrown out, so it's best to use this method for things like SSR that have async content /// The mutations will be thrown out, so it's best to use this method for things like SSR that have async content
pub async fn wait_for_suspense(&mut self) { pub async fn wait_for_suspense(&mut self) {
loop { loop {
// println!("waiting for suspense {:?}", self.suspended_scopes);
if self.suspended_scopes.is_empty() { if self.suspended_scopes.is_empty() {
return; return;
} }
// println!("waiting for suspense");
self.wait_for_work().await; self.wait_for_work().await;
// println!("Rendered immediately");
_ = self.render_immediate(); _ = self.render_immediate();
} }
} }
Loaded 100 of 278 files, more files were not shown because too many files have changed in this diff. Show more