mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-09-21 06:41:54 +00:00
Merge branch 'master' into liveview-history-default
This commit is contained in:
commit
08a679147f
249 changed files with 12339 additions and 4555 deletions
2
.github/workflows/cli_release.yml
vendored
2
.github/workflows/cli_release.yml
vendored
|
@ -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
|
||||||
|
|
8
.github/workflows/main.yml
vendored
8
.github/workflows/main.yml
vendored
|
@ -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,8 +52,9 @@ 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 libxdo-dev
|
||||||
- uses: davidB/rust-cargo-make@v1
|
- uses: davidB/rust-cargo-make@v1
|
||||||
- uses: browser-actions/setup-firefox@latest
|
- uses: browser-actions/setup-firefox@latest
|
||||||
- uses: jetli/wasm-pack-action@v0.4.0
|
- uses: jetli/wasm-pack-action@v0.4.0
|
||||||
|
@ -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
|
||||||
|
@ -124,6 +128,8 @@ jobs:
|
||||||
}
|
}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- 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:
|
||||||
|
|
5
.github/workflows/miri.yml
vendored
5
.github/workflows/miri.yml
vendored
|
@ -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:
|
||||||
|
|
1
.github/workflows/playwright.yml
vendored
1
.github/workflows/playwright.yml
vendored
|
@ -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
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -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,6 +9,7 @@ members = [
|
||||||
"packages/extension",
|
"packages/extension",
|
||||||
"packages/router",
|
"packages/router",
|
||||||
"packages/html",
|
"packages/html",
|
||||||
|
"packages/html-internal-macro",
|
||||||
"packages/hooks",
|
"packages/hooks",
|
||||||
"packages/web",
|
"packages/web",
|
||||||
"packages/ssr",
|
"packages/ssr",
|
||||||
|
@ -59,7 +60,8 @@ 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-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" }
|
||||||
dioxus-ssr = { path = "packages/ssr", version = "0.4.0" }
|
dioxus-ssr = { path = "packages/ssr", version = "0.4.0" }
|
||||||
|
@ -133,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"
|
||||||
|
|
|
@ -105,6 +105,8 @@ args = [
|
||||||
"dioxus-router",
|
"dioxus-router",
|
||||||
"--exclude",
|
"--exclude",
|
||||||
"dioxus-desktop",
|
"dioxus-desktop",
|
||||||
|
"--exclude",
|
||||||
|
"dioxus-mobile",
|
||||||
]
|
]
|
||||||
private = true
|
private = true
|
||||||
|
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -139,7 +139,6 @@ Missing Features
|
||||||
Missing examples
|
Missing examples
|
||||||
- Shared state
|
- Shared state
|
||||||
- Root-less element groups
|
- Root-less element groups
|
||||||
- Spread props
|
|
||||||
- Custom elements
|
- Custom elements
|
||||||
- Component Children: Pass children into child components
|
- Component Children: Pass children into child components
|
||||||
- Render To string: Render a mounted virtualdom to a string
|
- Render To string: Render a mounted virtualdom to a string
|
||||||
|
|
|
@ -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:?}" } ))
|
}
|
||||||
},
|
|
||||||
},
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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!"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -6,13 +6,13 @@ This calculator version uses React-style state management. All state is held as
|
||||||
use dioxus::events::*;
|
use dioxus::events::*;
|
||||||
use dioxus::html::input_data::keyboard_types::Key;
|
use dioxus::html::input_data::keyboard_types::Key;
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use dioxus_desktop::{Config, WindowBuilder};
|
use dioxus_desktop::{Config, LogicalSize, WindowBuilder};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let config = Config::new().with_window(
|
let config = Config::new().with_window(
|
||||||
WindowBuilder::default()
|
WindowBuilder::default()
|
||||||
.with_title("Calculator")
|
.with_title("Calculator")
|
||||||
.with_inner_size(dioxus_desktop::LogicalSize::new(300.0, 500.0)),
|
.with_inner_size(LogicalSize::new(300.0, 500.0)),
|
||||||
);
|
);
|
||||||
|
|
||||||
dioxus_desktop::launch_cfg(app, config);
|
dioxus_desktop::launch_cfg(app, config);
|
||||||
|
|
|
@ -10,7 +10,7 @@ fn app(cx: Scope) -> Element {
|
||||||
|
|
||||||
use_future!(cx, || async move {
|
use_future!(cx, || async move {
|
||||||
loop {
|
loop {
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||||
count += 1;
|
count += 1;
|
||||||
println!("current: {count}");
|
println!("current: {count}");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,80 +88,56 @@ 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"
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
27
examples/dynamic_asset.rs
Normal file
27
examples/dynamic_asset.rs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_desktop::{use_asset_handler, wry::http::Response};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus_desktop::launch(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
use_asset_handler(cx, "logos", |request, response| {
|
||||||
|
// Note that the "logos" prefix is stripped from the URI
|
||||||
|
//
|
||||||
|
// However, the asset is absolute to its "virtual folder" - meaning it starts with a leading slash
|
||||||
|
if request.uri().path() != "/logo.png" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.respond(Response::new(include_bytes!("./assets/logo.png").to_vec()));
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.render(rsx! {
|
||||||
|
div {
|
||||||
|
img {
|
||||||
|
src: "/logos/logo.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -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}"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,8 @@ 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, (), |_| {
|
|
||||||
to_owned![eval_provider];
|
|
||||||
async move {
|
|
||||||
let eval = eval_provider(
|
|
||||||
r#"
|
r#"
|
||||||
dioxus.send("Hi from JS!");
|
dioxus.send("Hi from JS!");
|
||||||
let msg = await dioxus.recv();
|
let msg = await dioxus.recv();
|
||||||
|
@ -24,7 +20,6 @@ fn app(cx: Scope) -> Element {
|
||||||
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() {
|
||||||
|
|
|
@ -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() }
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
|
@ -8,13 +8,12 @@ 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;
|
||||||
|
@ -28,13 +27,11 @@ fn app(cx: Scope) -> Element {
|
||||||
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 {}
|
||||||
|
|
|
@ -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.35.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",
|
||||||
|
|
|
@ -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,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!"
|
||||||
|
|
|
@ -21,7 +21,7 @@ use dioxus::events::*;
|
||||||
use dioxus::html::input_data::keyboard_types::Key;
|
use dioxus::html::input_data::keyboard_types::Key;
|
||||||
use dioxus::html::MouseEvent;
|
use dioxus::html::MouseEvent;
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use dioxus_desktop::wry::application::dpi::LogicalSize;
|
use dioxus_desktop::tao::dpi::LogicalSize;
|
||||||
use dioxus_desktop::{Config, WindowBuilder};
|
use dioxus_desktop::{Config, WindowBuilder};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
|
@ -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>{} }
|
||||||
|
|
|
@ -53,6 +53,7 @@ fn App(cx: Scope) -> Element {
|
||||||
let formatting = "formatting!";
|
let formatting = "formatting!";
|
||||||
let formatting_tuple = ("a", "b");
|
let formatting_tuple = ("a", "b");
|
||||||
let lazy_fmt = format_args!("lazily formatted text");
|
let lazy_fmt = format_args!("lazily formatted text");
|
||||||
|
let asd = 123;
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
// Elements
|
// Elements
|
||||||
|
@ -80,6 +81,10 @@ fn App(cx: Scope) -> Element {
|
||||||
// pass simple rust expressions in
|
// pass simple rust expressions in
|
||||||
class: lazy_fmt,
|
class: lazy_fmt,
|
||||||
id: format_args!("attributes can be passed lazily with std::fmt::Arguments"),
|
id: format_args!("attributes can be passed lazily with std::fmt::Arguments"),
|
||||||
|
class: "asd",
|
||||||
|
class: "{asd}",
|
||||||
|
// if statements can be used to conditionally render attributes
|
||||||
|
class: if formatting.contains("form") { "{asd}" },
|
||||||
div {
|
div {
|
||||||
class: {
|
class: {
|
||||||
const WORD: &str = "expressions";
|
const WORD: &str = "expressions";
|
||||||
|
|
|
@ -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();
|
||||||
|
|
36
examples/spread.rs
Normal file
36
examples/spread.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut dom = VirtualDom::new(app);
|
||||||
|
let _ = dom.rebuild();
|
||||||
|
let html = dioxus_ssr::render(&dom);
|
||||||
|
|
||||||
|
println!("{}", html);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
render! {
|
||||||
|
Component {
|
||||||
|
width: "10px",
|
||||||
|
extra_data: "hello{1}",
|
||||||
|
extra_data2: "hello{2}",
|
||||||
|
height: "10px",
|
||||||
|
left: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn Component<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> {
|
||||||
|
render! {
|
||||||
|
audio { ..cx.props.attributes, "1: {cx.props.extra_data}\n2: {cx.props.extra_data2}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Props)]
|
||||||
|
struct Props<'a> {
|
||||||
|
#[props(extends = GlobalAttributes)]
|
||||||
|
attributes: Vec<Attribute<'a>>,
|
||||||
|
extra_data: &'a str,
|
||||||
|
extra_data2: &'a str,
|
||||||
|
}
|
|
@ -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 = []
|
||||||
|
|
|
@ -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
|
||||||
|
|
1
examples/tailwind/dist/tailwind3531548035813279582.css
vendored
Normal file
1
examples/tailwind/dist/tailwind3531548035813279582.css
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -2,21 +2,23 @@
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn app(cx: Scope) -> Element {
|
pub fn app(cx: Scope) -> Element {
|
||||||
|
let grey_background = true;
|
||||||
cx.render(rsx!(
|
cx.render(rsx!(
|
||||||
div {
|
div {
|
||||||
header { class: "text-gray-400 bg-gray-900 body-font",
|
header {
|
||||||
|
class: "text-gray-400 body-font",
|
||||||
|
// you can use optional attributes to optionally apply a tailwind class
|
||||||
|
class: if grey_background { "bg-gray-900" },
|
||||||
div { class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center",
|
div { class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center",
|
||||||
a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0",
|
a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0",
|
||||||
StacksIcon {}
|
StacksIcon {}
|
||||||
|
|
|
@ -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()),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
@ -110,11 +108,14 @@ pub fn TodoHeader<'a>(cx: Scope<'a, TodoHeaderProps<'a>>) -> Element {
|
||||||
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
|
||||||
|
.todos
|
||||||
|
.make_mut()
|
||||||
|
.insert(
|
||||||
**todo_id,
|
**todo_id,
|
||||||
TodoItem {
|
TodoItem {
|
||||||
id: **todo_id,
|
id: **todo_id,
|
||||||
|
@ -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| {
|
||||||
|
@ -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" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
188
examples/video_stream.rs
Normal file
188
examples/video_stream.rs
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
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::{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, "videos", move |request, responder| {
|
||||||
|
// Using dioxus::spawn works, but is slower than a dedicated thread
|
||||||
|
tokio::task::spawn(async move {
|
||||||
|
let video_file = PathBuf::from(VIDEO_PATH);
|
||||||
|
let mut file = tokio::fs::File::open(&video_file).await.unwrap();
|
||||||
|
|
||||||
|
match get_stream_response(&mut file, &request).await {
|
||||||
|
Ok(response) => responder.respond(response),
|
||||||
|
Err(err) => eprintln!("Error: {}", err),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
render! {
|
||||||
|
div {
|
||||||
|
video {
|
||||||
|
src: "/videos/test_video.mp4",
|
||||||
|
autoplay: true,
|
||||||
|
controls: true,
|
||||||
|
width: 640,
|
||||||
|
height: 480
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This was taken from wry's example
|
||||||
|
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)
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_desktop::tao::event::Event as WryEvent;
|
||||||
use dioxus_desktop::tao::event::WindowEvent;
|
use dioxus_desktop::tao::event::WindowEvent;
|
||||||
use dioxus_desktop::use_wry_event_handler;
|
use dioxus_desktop::use_wry_event_handler;
|
||||||
use dioxus_desktop::wry::application::event::Event as WryEvent;
|
|
||||||
use dioxus_desktop::{Config, WindowCloseBehaviour};
|
use dioxus_desktop::{Config, WindowCloseBehaviour};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
247
flake.lock
Normal file
247
flake.lock
Normal file
|
@ -0,0 +1,247 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"crane": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-compat": "flake-compat",
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"rust-overlay": "rust-overlay"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1696384830,
|
||||||
|
"narHash": "sha256-j8ZsVqzmj5sOm5MW9cqwQJUZELFFwOislDmqDDEMl6k=",
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"rev": "f2143cd27f8bd09ee4f0121336c65015a2a0a19c",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-compat": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1696267196,
|
||||||
|
"narHash": "sha256-AAQ/2sD+0D18bb8hKuEEVpHUYD1GmO2Uh/taFamn6XQ=",
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"rev": "4f910c9827911b1ec2bf26b5a062cd09f8d89f85",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-parts": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs-lib": "nixpkgs-lib"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1696343447,
|
||||||
|
"narHash": "sha256-B2xAZKLkkeRFG5XcHHSXXcP7To9Xzr59KXeZiRf4vdQ=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"rev": "c9afaba3dfa4085dbd2ccb38dfade5141e33d9d4",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1694529238,
|
||||||
|
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils_2": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems_2"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681202837,
|
||||||
|
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1697009197,
|
||||||
|
"narHash": "sha256-viVRhBTFT8fPJTb1N3brQIpFZnttmwo3JVKNuWRVc3s=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "01441e14af5e29c9d27ace398e6dd0b293e25a54",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs-lib": {
|
||||||
|
"locked": {
|
||||||
|
"dir": "lib",
|
||||||
|
"lastModified": 1696019113,
|
||||||
|
"narHash": "sha256-X3+DKYWJm93DRSdC5M6K5hLqzSya9BjibtBsuARoPco=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "f5892ddac112a1e9b3612c39af1b72987ee5783a",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"dir": "lib",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681358109,
|
||||||
|
"narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"crane": "crane",
|
||||||
|
"flake-parts": "flake-parts",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"rust-overlay": "rust-overlay_2",
|
||||||
|
"systems": "systems_3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-overlay": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": [
|
||||||
|
"crane",
|
||||||
|
"flake-utils"
|
||||||
|
],
|
||||||
|
"nixpkgs": [
|
||||||
|
"crane",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1696299134,
|
||||||
|
"narHash": "sha256-RS77cAa0N+Sfj5EmKbm5IdncNXaBCE1BSSQvUE8exvo=",
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"rev": "611ccdceed92b4d94ae75328148d84ee4a5b462d",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-overlay_2": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils_2",
|
||||||
|
"nixpkgs": "nixpkgs_2"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1697076655,
|
||||||
|
"narHash": "sha256-NcCtVUOd0X81srZkrdP8qoA1BMsPdO2tGtlZpsGijeU=",
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"rev": "aa7584f5bbf5947716ad8ec14eccc0334f0d28f0",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems_3": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
63
flake.nix
Normal file
63
flake.nix
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
|
||||||
|
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||||
|
systems.url = "github:nix-systems/default";
|
||||||
|
|
||||||
|
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||||
|
crane.url = "github:ipetkov/crane";
|
||||||
|
crane.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = inputs:
|
||||||
|
inputs.flake-parts.lib.mkFlake { inherit inputs; } {
|
||||||
|
systems = import inputs.systems;
|
||||||
|
|
||||||
|
perSystem = { config, self', pkgs, lib, system, ... }:
|
||||||
|
let
|
||||||
|
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
|
||||||
|
extensions = [
|
||||||
|
"rust-src"
|
||||||
|
"rust-analyzer"
|
||||||
|
"clippy"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
rustBuildInputs = [
|
||||||
|
pkgs.openssl
|
||||||
|
pkgs.libiconv
|
||||||
|
pkgs.pkg-config
|
||||||
|
] ++ lib.optionals pkgs.stdenv.isDarwin (with pkgs.darwin.apple_sdk.frameworks; [
|
||||||
|
IOKit
|
||||||
|
Carbon
|
||||||
|
WebKit
|
||||||
|
Security
|
||||||
|
Cocoa
|
||||||
|
]);
|
||||||
|
|
||||||
|
# This is useful when building crates as packages
|
||||||
|
# Note that it does require a `Cargo.lock` which this repo does not have
|
||||||
|
# craneLib = (inputs.crane.mkLib pkgs).overrideToolchain rustToolchain;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
_module.args.pkgs = import inputs.nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
overlays = [
|
||||||
|
inputs.rust-overlay.overlays.default
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
name = "dioxus-dev";
|
||||||
|
buildInputs = rustBuildInputs;
|
||||||
|
nativeBuildInputs = [
|
||||||
|
# Add shell dependencies here
|
||||||
|
rustToolchain
|
||||||
|
];
|
||||||
|
shellHook = ''
|
||||||
|
# For rust-analyzer 'hover' tooltips to work.
|
||||||
|
export RUST_SRC_PATH="${rustToolchain}/lib/rustlib/src/rust/library";
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -49,6 +49,7 @@ impl Writer<'_> {
|
||||||
attributes,
|
attributes,
|
||||||
children,
|
children,
|
||||||
brace,
|
brace,
|
||||||
|
..
|
||||||
} = el;
|
} = el;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -165,7 +166,7 @@ impl Writer<'_> {
|
||||||
|
|
||||||
fn write_attributes(
|
fn write_attributes(
|
||||||
&mut self,
|
&mut self,
|
||||||
attributes: &[ElementAttrNamed],
|
attributes: &[AttributeType],
|
||||||
key: &Option<IfmtInput>,
|
key: &Option<IfmtInput>,
|
||||||
sameline: bool,
|
sameline: bool,
|
||||||
) -> Result {
|
) -> Result {
|
||||||
|
@ -187,7 +188,7 @@ impl Writer<'_> {
|
||||||
while let Some(attr) = attr_iter.next() {
|
while let Some(attr) = attr_iter.next() {
|
||||||
self.out.indent_level += 1;
|
self.out.indent_level += 1;
|
||||||
if !sameline {
|
if !sameline {
|
||||||
self.write_comments(attr.attr.start())?;
|
self.write_comments(attr.start())?;
|
||||||
}
|
}
|
||||||
self.out.indent_level -= 1;
|
self.out.indent_level -= 1;
|
||||||
|
|
||||||
|
@ -209,12 +210,34 @@ impl Writer<'_> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_attribute(&mut self, attr: &ElementAttrNamed) -> Result {
|
fn write_attribute_name(&mut self, attr: &ElementAttrName) -> Result {
|
||||||
match &attr.attr {
|
match attr {
|
||||||
ElementAttr::AttrText { name, value } => {
|
ElementAttrName::BuiltIn(name) => {
|
||||||
write!(self.out, "{name}: {value}", value = ifmt_to_string(value))?;
|
write!(self.out, "{}", name)?;
|
||||||
}
|
}
|
||||||
ElementAttr::AttrExpression { name, value } => {
|
ElementAttrName::Custom(name) => {
|
||||||
|
write!(self.out, "{}", name.to_token_stream())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_attribute_value(&mut self, value: &ElementAttrValue) -> Result {
|
||||||
|
match value {
|
||||||
|
ElementAttrValue::AttrOptionalExpr { condition, value } => {
|
||||||
|
write!(
|
||||||
|
self.out,
|
||||||
|
"if {condition} {{ ",
|
||||||
|
condition = prettyplease::unparse_expr(condition),
|
||||||
|
)?;
|
||||||
|
self.write_attribute_value(value)?;
|
||||||
|
write!(self.out, " }}")?;
|
||||||
|
}
|
||||||
|
ElementAttrValue::AttrLiteral(value) => {
|
||||||
|
write!(self.out, "{value}", value = ifmt_to_string(value))?;
|
||||||
|
}
|
||||||
|
ElementAttrValue::AttrExpr(value) => {
|
||||||
let out = prettyplease::unparse_expr(value);
|
let out = prettyplease::unparse_expr(value);
|
||||||
let mut lines = out.split('\n').peekable();
|
let mut lines = out.split('\n').peekable();
|
||||||
let first = lines.next().unwrap();
|
let first = lines.next().unwrap();
|
||||||
|
@ -222,9 +245,9 @@ impl Writer<'_> {
|
||||||
// a one-liner for whatever reason
|
// a one-liner for whatever reason
|
||||||
// Does not need a new line
|
// Does not need a new line
|
||||||
if lines.peek().is_none() {
|
if lines.peek().is_none() {
|
||||||
write!(self.out, "{name}: {first}")?;
|
write!(self.out, "{first}")?;
|
||||||
} else {
|
} else {
|
||||||
writeln!(self.out, "{name}: {first}")?;
|
writeln!(self.out, "{first}")?;
|
||||||
|
|
||||||
while let Some(line) = lines.next() {
|
while let Some(line) = lines.next() {
|
||||||
self.out.indented_tab()?;
|
self.out.indented_tab()?;
|
||||||
|
@ -237,22 +260,7 @@ impl Writer<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ElementAttrValue::EventTokens(tokens) => {
|
||||||
ElementAttr::CustomAttrText { name, value } => {
|
|
||||||
write!(
|
|
||||||
self.out,
|
|
||||||
"{name}: {value}",
|
|
||||||
name = name.to_token_stream(),
|
|
||||||
value = ifmt_to_string(value)
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
ElementAttr::CustomAttrExpression { name, value } => {
|
|
||||||
let out = prettyplease::unparse_expr(value);
|
|
||||||
write!(self.out, "{}: {}", name.to_token_stream(), out)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
ElementAttr::EventTokens { name, tokens } => {
|
|
||||||
let out = self.retrieve_formatted_expr(tokens).to_string();
|
let out = self.retrieve_formatted_expr(tokens).to_string();
|
||||||
|
|
||||||
let mut lines = out.split('\n').peekable();
|
let mut lines = out.split('\n').peekable();
|
||||||
|
@ -261,9 +269,9 @@ impl Writer<'_> {
|
||||||
// a one-liner for whatever reason
|
// a one-liner for whatever reason
|
||||||
// Does not need a new line
|
// Does not need a new line
|
||||||
if lines.peek().is_none() {
|
if lines.peek().is_none() {
|
||||||
write!(self.out, "{name}: {first}")?;
|
write!(self.out, "{first}")?;
|
||||||
} else {
|
} else {
|
||||||
writeln!(self.out, "{name}: {first}")?;
|
writeln!(self.out, "{first}")?;
|
||||||
|
|
||||||
while let Some(line) = lines.next() {
|
while let Some(line) = lines.next() {
|
||||||
self.out.indented_tab()?;
|
self.out.indented_tab()?;
|
||||||
|
@ -281,6 +289,28 @@ impl Writer<'_> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn write_attribute(&mut self, attr: &AttributeType) -> Result {
|
||||||
|
match attr {
|
||||||
|
AttributeType::Named(attr) => self.write_named_attribute(attr),
|
||||||
|
AttributeType::Spread(attr) => self.write_spread_attribute(attr),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_named_attribute(&mut self, attr: &ElementAttrNamed) -> Result {
|
||||||
|
self.write_attribute_name(&attr.attr.name)?;
|
||||||
|
write!(self.out, ": ")?;
|
||||||
|
self.write_attribute_value(&attr.attr.value)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_spread_attribute(&mut self, attr: &Expr) -> Result {
|
||||||
|
write!(self.out, "..")?;
|
||||||
|
write!(self.out, "{}", prettyplease::unparse_expr(attr))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// make sure the comments are actually relevant to this element.
|
// make sure the comments are actually relevant to this element.
|
||||||
// test by making sure this element is the primary element on this line
|
// test by making sure this element is the primary element on this line
|
||||||
pub fn current_span_is_primary(&self, location: Span) -> bool {
|
pub fn current_span_is_primary(&self, location: Span) -> bool {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use dioxus_rsx::{BodyNode, ElementAttr, ElementAttrNamed, ForLoop};
|
use dioxus_rsx::{AttributeType, BodyNode, ElementAttrValue, ForLoop};
|
||||||
use proc_macro2::{LineColumn, Span};
|
use proc_macro2::{LineColumn, Span};
|
||||||
use quote::ToTokens;
|
use quote::ToTokens;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -132,34 +132,17 @@ impl<'a> Writer<'a> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_short_attrs(&mut self, attributes: &[ElementAttrNamed]) -> usize {
|
pub(crate) fn attr_value_len(&mut self, value: &ElementAttrValue) -> usize {
|
||||||
let mut total = 0;
|
match value {
|
||||||
|
ElementAttrValue::AttrOptionalExpr { condition, value } => {
|
||||||
|
let condition_len = self.retrieve_formatted_expr(condition).len();
|
||||||
|
let value_len = self.attr_value_len(value);
|
||||||
|
|
||||||
for attr in attributes {
|
condition_len + value_len + 6
|
||||||
if self.current_span_is_primary(attr.attr.start()) {
|
|
||||||
'line: for line in self.src[..attr.attr.start().start().line - 1].iter().rev() {
|
|
||||||
match (line.trim().starts_with("//"), line.is_empty()) {
|
|
||||||
(true, _) => return 100000,
|
|
||||||
(_, true) => continue 'line,
|
|
||||||
_ => break 'line,
|
|
||||||
}
|
}
|
||||||
}
|
ElementAttrValue::AttrLiteral(lit) => ifmt_to_string(lit).len(),
|
||||||
}
|
ElementAttrValue::AttrExpr(expr) => expr.span().line_length(),
|
||||||
|
ElementAttrValue::EventTokens(tokens) => {
|
||||||
total += match &attr.attr {
|
|
||||||
ElementAttr::AttrText { value, name } => {
|
|
||||||
ifmt_to_string(value).len() + name.span().line_length() + 6
|
|
||||||
}
|
|
||||||
ElementAttr::AttrExpression { name, value } => {
|
|
||||||
value.span().line_length() + name.span().line_length() + 6
|
|
||||||
}
|
|
||||||
ElementAttr::CustomAttrText { value, name } => {
|
|
||||||
ifmt_to_string(value).len() + name.to_token_stream().to_string().len() + 6
|
|
||||||
}
|
|
||||||
ElementAttr::CustomAttrExpression { name, value } => {
|
|
||||||
name.to_token_stream().to_string().len() + value.span().line_length() + 6
|
|
||||||
}
|
|
||||||
ElementAttr::EventTokens { tokens, name } => {
|
|
||||||
let location = Location::new(tokens.span().start());
|
let location = Location::new(tokens.span().start());
|
||||||
|
|
||||||
let len = if let std::collections::hash_map::Entry::Vacant(e) =
|
let len = if let std::collections::hash_map::Entry::Vacant(e) =
|
||||||
|
@ -177,9 +160,44 @@ impl<'a> Writer<'a> {
|
||||||
self.cached_formats[&location].len()
|
self.cached_formats[&location].len()
|
||||||
};
|
};
|
||||||
|
|
||||||
len + name.span().line_length() + 6
|
len
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_short_attrs(&mut self, attributes: &[AttributeType]) -> usize {
|
||||||
|
let mut total = 0;
|
||||||
|
|
||||||
|
for attr in attributes {
|
||||||
|
if self.current_span_is_primary(attr.start()) {
|
||||||
|
'line: for line in self.src[..attr.start().start().line - 1].iter().rev() {
|
||||||
|
match (line.trim().starts_with("//"), line.is_empty()) {
|
||||||
|
(true, _) => return 100000,
|
||||||
|
(_, true) => continue 'line,
|
||||||
|
_ => break 'line,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match attr {
|
||||||
|
AttributeType::Named(attr) => {
|
||||||
|
let name_len = match &attr.attr.name {
|
||||||
|
dioxus_rsx::ElementAttrName::BuiltIn(name) => {
|
||||||
|
let name = name.to_string();
|
||||||
|
name.len()
|
||||||
|
}
|
||||||
|
dioxus_rsx::ElementAttrName::Custom(name) => name.value().len() + 2,
|
||||||
|
};
|
||||||
|
total += name_len;
|
||||||
|
total += self.attr_value_len(&attr.attr.value);
|
||||||
|
}
|
||||||
|
AttributeType::Spread(expr) => {
|
||||||
|
let expr_len = self.retrieve_formatted_expr(expr).len();
|
||||||
|
total += expr_len + 3;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
total += 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
total
|
total
|
||||||
|
@ -218,7 +236,7 @@ impl<'a> Writer<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trait SpanLength {
|
pub(crate) trait SpanLength {
|
||||||
fn line_length(&self) -> usize;
|
fn line_length(&self) -> usize;
|
||||||
}
|
}
|
||||||
impl SpanLength for Span {
|
impl SpanLength for Span {
|
||||||
|
|
|
@ -33,7 +33,7 @@ rsx! {
|
||||||
}
|
}
|
||||||
|
|
||||||
// No children, minimal props
|
// No children, minimal props
|
||||||
img { class: "mb-6 mx-auto h-24", src: "artemis-assets/images/friends.png", alt: "" }
|
img { class: "mb-6 mx-auto h-24", src: "artemis-assets/images/friends.png" }
|
||||||
|
|
||||||
// One level compression
|
// One level compression
|
||||||
div {
|
div {
|
||||||
|
|
|
@ -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"
|
||||||
|
@ -72,8 +72,10 @@ cargo-generate = "0.18"
|
||||||
toml_edit = "0.19.11"
|
toml_edit = "0.19.11"
|
||||||
|
|
||||||
# bundling
|
# bundling
|
||||||
tauri-bundler = { version = "=1.3.0", features = ["native-tls-vendored"] }
|
tauri-bundler = { version = "=1.4.*", features = ["native-tls-vendored"] }
|
||||||
tauri-utils = "=1.4.*"
|
tauri-utils = "=1.5.*"
|
||||||
|
|
||||||
|
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 }
|
||||||
|
|
|
@ -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:';
|
||||||
|
|
|
@ -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)?;
|
||||||
|
|
||||||
|
@ -60,8 +66,8 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
|
||||||
.output()?;
|
.output()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cmd = subprocess::Exec::cmd("cargo");
|
let cmd = subprocess::Exec::cmd("cargo")
|
||||||
let cmd = cmd
|
.env("CARGO_TARGET_DIR", target_dir)
|
||||||
.cwd(crate_dir)
|
.cwd(crate_dir)
|
||||||
.arg("build")
|
.arg("build")
|
||||||
.arg("--target")
|
.arg("--target")
|
||||||
|
@ -93,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),
|
||||||
|
@ -250,19 +258,28 @@ 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("--message-format=json");
|
.arg("--message-format=json");
|
||||||
|
@ -286,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),
|
||||||
|
@ -303,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)
|
||||||
|
@ -360,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
|
||||||
|
@ -379,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()
|
||||||
|
@ -391,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);
|
||||||
|
|
||||||
|
@ -434,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() {
|
||||||
|
@ -455,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();
|
||||||
|
@ -475,6 +516,7 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
if config
|
if config
|
||||||
|
.dioxus_config
|
||||||
.application
|
.application
|
||||||
.tools
|
.tools
|
||||||
.clone()
|
.clone()
|
||||||
|
@ -483,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);
|
||||||
|
|
||||||
|
@ -503,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);
|
||||||
|
@ -532,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
|
||||||
|
@ -701,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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
if let Some(target) = self.build.target.clone() {
|
||||||
|
crate_config.set_target(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
crate_config.set_cargo_args(self.build.cargo_args.clone());
|
||||||
|
|
||||||
// #[cfg(feature = "plugin")]
|
// #[cfg(feature = "plugin")]
|
||||||
// let _ = PluginManager::on_build_start(&crate_config, &platform);
|
// 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
|
||||||
|
|
|
@ -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(())
|
||||||
|
|
|
@ -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>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
self.serve.port,
|
||||||
|
crate_config.clone(),
|
||||||
|
self.serve.open,
|
||||||
|
self.serve.skip_assets,
|
||||||
|
)
|
||||||
.await?;
|
.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
|
||||||
|
|
|
@ -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)]
|
||||||
|
@ -525,6 +550,7 @@ impl From<NsisSettings> for tauri_bundler::NsisSettings {
|
||||||
display_language_selector: val.display_language_selector,
|
display_language_selector: val.display_language_selector,
|
||||||
custom_language_files: None,
|
custom_language_files: None,
|
||||||
template: None,
|
template: None,
|
||||||
|
compression: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
.map_err(|e| anyhow!("🚫 Translation of HTML into RSX failed: {}", e)),
|
.map_err(|e| anyhow!("🚫 Translation of HTML into RSX failed: {}", e)),
|
||||||
|
|
||||||
Build(opts) if bin.is_ok() => opts
|
Build(opts) if bin.is_ok() => opts
|
||||||
.build(Some(bin.unwrap().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) if bin.is_ok() => opts
|
Clean(opts) if bin.is_ok() => opts
|
||||||
|
|
|
@ -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 || {
|
||||||
|
@ -51,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...");
|
||||||
|
|
||||||
|
@ -68,15 +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();
|
|
||||||
log::trace!("Killing old process");
|
|
||||||
current_child.kill()?;
|
|
||||||
let (child, result) = start_desktop(&config)?;
|
|
||||||
*current_child = child;
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
&config,
|
&config,
|
||||||
None,
|
None,
|
||||||
|
@ -84,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 => {
|
||||||
|
@ -192,7 +185,7 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_paths(file_socket_path: &std::path::Path) {
|
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
|
||||||
|
@ -217,10 +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
|
||||||
log::trace!("Building application");
|
let result = crate::builder::build_desktop(config, true, skip_assets)?;
|
||||||
let result = crate::builder::build_desktop(config, true)?;
|
|
||||||
|
|
||||||
match &config.executable {
|
match &config.executable {
|
||||||
crate::ExecutableType::Binary(name)
|
crate::ExecutableType::Binary(name)
|
||||||
|
@ -230,10 +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");
|
||||||
}
|
}
|
||||||
log::trace!("Running application from {:?}", file);
|
let active = "DIOXUS_ACTIVE";
|
||||||
let child = Command::new(file.to_str().unwrap()).spawn()?;
|
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
packages/cli/src/server/fullstack/mod.rs
Normal file
161
packages/cli/src/server/fullstack/mod.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
@ -141,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>>,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -15,10 +15,11 @@ proc-macro = true
|
||||||
[dependencies]
|
[dependencies]
|
||||||
proc-macro2 = { version = "1.0" }
|
proc-macro2 = { version = "1.0" }
|
||||||
quote = "1.0"
|
quote = "1.0"
|
||||||
syn = { version = "2.0", features = ["full", "extra-traits"] }
|
syn = { version = "2.0", features = ["full", "extra-traits", "visit"] }
|
||||||
dioxus-rsx = { workspace = true }
|
dioxus-rsx = { workspace = true }
|
||||||
dioxus-core = { workspace = true }
|
dioxus-core = { workspace = true }
|
||||||
constcat = "0.3.0"
|
constcat = "0.3.0"
|
||||||
|
convert_case = "^0.6.0"
|
||||||
prettyplease = "0.2.15"
|
prettyplease = "0.2.15"
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -24,6 +24,10 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result<TokenStream, Error> {
|
||||||
.included_fields()
|
.included_fields()
|
||||||
.map(|f| struct_info.field_impl(f))
|
.map(|f| struct_info.field_impl(f))
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
let extends = struct_info
|
||||||
|
.extend_fields()
|
||||||
|
.map(|f| struct_info.extends_impl(f))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
let fields = quote!(#(#fields)*).into_iter();
|
let fields = quote!(#(#fields)*).into_iter();
|
||||||
let required_fields = struct_info
|
let required_fields = struct_info
|
||||||
.included_fields()
|
.included_fields()
|
||||||
|
@ -36,6 +40,7 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result<TokenStream, Error> {
|
||||||
#builder_creation
|
#builder_creation
|
||||||
#conversion_helper
|
#conversion_helper
|
||||||
#( #fields )*
|
#( #fields )*
|
||||||
|
#( #extends )*
|
||||||
#( #required_fields )*
|
#( #required_fields )*
|
||||||
#build_method
|
#build_method
|
||||||
}
|
}
|
||||||
|
@ -167,8 +172,8 @@ mod field_info {
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::spanned::Spanned;
|
use syn::spanned::Spanned;
|
||||||
use syn::Expr;
|
|
||||||
use syn::{parse::Error, punctuated::Punctuated};
|
use syn::{parse::Error, punctuated::Punctuated};
|
||||||
|
use syn::{Expr, Path};
|
||||||
|
|
||||||
use super::util::{
|
use super::util::{
|
||||||
expr_to_single_string, ident_to_type, path_to_single_string, strip_raw_ident_prefix,
|
expr_to_single_string, ident_to_type, path_to_single_string, strip_raw_ident_prefix,
|
||||||
|
@ -199,6 +204,13 @@ mod field_info {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extended field is automatically empty
|
||||||
|
if !builder_attr.extends.is_empty() {
|
||||||
|
builder_attr.default = Some(
|
||||||
|
syn::parse(quote!(::core::default::Default::default()).into()).unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// auto detect optional
|
// auto detect optional
|
||||||
let strip_option_auto = builder_attr.strip_option
|
let strip_option_auto = builder_attr.strip_option
|
||||||
|| !builder_attr.ignore_option
|
|| !builder_attr.ignore_option
|
||||||
|
@ -253,6 +265,7 @@ mod field_info {
|
||||||
pub auto_into: bool,
|
pub auto_into: bool,
|
||||||
pub strip_option: bool,
|
pub strip_option: bool,
|
||||||
pub ignore_option: bool,
|
pub ignore_option: bool,
|
||||||
|
pub extends: Vec<Path>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FieldBuilderAttr {
|
impl FieldBuilderAttr {
|
||||||
|
@ -305,6 +318,17 @@ mod field_info {
|
||||||
let name = expr_to_single_string(&assign.left)
|
let name = expr_to_single_string(&assign.left)
|
||||||
.ok_or_else(|| Error::new_spanned(&assign.left, "Expected identifier"))?;
|
.ok_or_else(|| Error::new_spanned(&assign.left, "Expected identifier"))?;
|
||||||
match name.as_str() {
|
match name.as_str() {
|
||||||
|
"extends" => {
|
||||||
|
if let syn::Expr::Path(path) = *assign.right {
|
||||||
|
self.extends.push(path.path);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::new_spanned(
|
||||||
|
assign.right,
|
||||||
|
"Expected simple identifier",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
"default" => {
|
"default" => {
|
||||||
self.default = Some(*assign.right);
|
self.default = Some(*assign.right);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -359,6 +383,11 @@ mod field_info {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"extend" => {
|
||||||
|
self.extends.push(path.path);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
macro_rules! handle_fields {
|
macro_rules! handle_fields {
|
||||||
( $( $flag:expr, $field:ident, $already:expr; )* ) => {
|
( $( $flag:expr, $field:ident, $already:expr; )* ) => {
|
||||||
|
@ -462,11 +491,14 @@ fn type_from_inside_option(ty: &syn::Type, check_option_name: bool) -> Option<&s
|
||||||
}
|
}
|
||||||
|
|
||||||
mod struct_info {
|
mod struct_info {
|
||||||
|
use convert_case::{Case, Casing};
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::parse::Error;
|
use syn::parse::Error;
|
||||||
use syn::punctuated::Punctuated;
|
use syn::punctuated::Punctuated;
|
||||||
use syn::Expr;
|
use syn::spanned::Spanned;
|
||||||
|
use syn::visit::Visit;
|
||||||
|
use syn::{parse_quote, Expr, Ident};
|
||||||
|
|
||||||
use super::field_info::{FieldBuilderAttr, FieldInfo};
|
use super::field_info::{FieldBuilderAttr, FieldInfo};
|
||||||
use super::util::{
|
use super::util::{
|
||||||
|
@ -489,7 +521,46 @@ mod struct_info {
|
||||||
|
|
||||||
impl<'a> StructInfo<'a> {
|
impl<'a> StructInfo<'a> {
|
||||||
pub fn included_fields(&self) -> impl Iterator<Item = &FieldInfo<'a>> {
|
pub fn included_fields(&self) -> impl Iterator<Item = &FieldInfo<'a>> {
|
||||||
self.fields.iter().filter(|f| !f.builder_attr.skip)
|
self.fields
|
||||||
|
.iter()
|
||||||
|
.filter(|f| !f.builder_attr.skip && f.builder_attr.extends.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extend_fields(&self) -> impl Iterator<Item = &FieldInfo<'a>> {
|
||||||
|
self.fields
|
||||||
|
.iter()
|
||||||
|
.filter(|f| !f.builder_attr.extends.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extend_lifetime(&self) -> syn::Result<Option<syn::Lifetime>> {
|
||||||
|
let first_extend = self.extend_fields().next();
|
||||||
|
|
||||||
|
match first_extend {
|
||||||
|
Some(f) => {
|
||||||
|
struct VisitFirstLifetime(Option<syn::Lifetime>);
|
||||||
|
|
||||||
|
impl Visit<'_> for VisitFirstLifetime {
|
||||||
|
fn visit_lifetime(&mut self, lifetime: &'_ syn::Lifetime) {
|
||||||
|
if self.0.is_none() {
|
||||||
|
self.0 = Some(lifetime.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = f.name;
|
||||||
|
let mut visitor = VisitFirstLifetime(None);
|
||||||
|
|
||||||
|
visitor.visit_type(f.ty);
|
||||||
|
|
||||||
|
visitor.0.ok_or_else(|| {
|
||||||
|
syn::Error::new_spanned(
|
||||||
|
name,
|
||||||
|
"Unable to find lifetime for extended field. Please specify it manually",
|
||||||
|
)
|
||||||
|
}).map(Some)
|
||||||
|
}
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
@ -536,7 +607,17 @@ mod struct_info {
|
||||||
// Therefore, we will generate code that shortcircuits the "comparison" in memoization
|
// Therefore, we will generate code that shortcircuits the "comparison" in memoization
|
||||||
let are_there_generics = !self.generics.params.is_empty();
|
let are_there_generics = !self.generics.params.is_empty();
|
||||||
|
|
||||||
let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
|
let extend_lifetime = self.extend_lifetime()?;
|
||||||
|
|
||||||
|
let generics = self.generics.clone();
|
||||||
|
let (_, ty_generics, where_clause) = generics.split_for_impl();
|
||||||
|
let impl_generics = self.modify_generics(|g| {
|
||||||
|
if extend_lifetime.is_none() {
|
||||||
|
g.params.insert(0, parse_quote!('__bump));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let (impl_generics, _, _) = impl_generics.split_for_impl();
|
||||||
|
let (_, b_initial_generics, _) = self.generics.split_for_impl();
|
||||||
let all_fields_param = syn::GenericParam::Type(
|
let all_fields_param = syn::GenericParam::Type(
|
||||||
syn::Ident::new("TypedBuilderFields", proc_macro2::Span::call_site()).into(),
|
syn::Ident::new("TypedBuilderFields", proc_macro2::Span::call_site()).into(),
|
||||||
);
|
);
|
||||||
|
@ -544,7 +625,7 @@ mod struct_info {
|
||||||
g.params.insert(0, all_fields_param.clone());
|
g.params.insert(0, all_fields_param.clone());
|
||||||
});
|
});
|
||||||
let empties_tuple = type_tuple(self.included_fields().map(|_| empty_type()));
|
let empties_tuple = type_tuple(self.included_fields().map(|_| empty_type()));
|
||||||
let generics_with_empty = modify_types_generics_hack(&ty_generics, |args| {
|
let generics_with_empty = modify_types_generics_hack(&b_initial_generics, |args| {
|
||||||
args.insert(0, syn::GenericArgument::Type(empties_tuple.clone().into()));
|
args.insert(0, syn::GenericArgument::Type(empties_tuple.clone().into()));
|
||||||
});
|
});
|
||||||
let phantom_generics = self.generics.params.iter().filter_map(|param| match param {
|
let phantom_generics = self.generics.params.iter().filter_map(|param| match param {
|
||||||
|
@ -603,8 +684,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||||
quote!(#[doc(hidden)])
|
quote!(#[doc(hidden)])
|
||||||
};
|
};
|
||||||
|
|
||||||
let (b_generics_impl, b_generics_ty, b_generics_where_extras_predicates) =
|
let (_, _, b_generics_where_extras_predicates) = b_generics.split_for_impl();
|
||||||
b_generics.split_for_impl();
|
|
||||||
let mut b_generics_where: syn::WhereClause = syn::parse2(quote! {
|
let mut b_generics_where: syn::WhereClause = syn::parse2(quote! {
|
||||||
where TypedBuilderFields: Clone
|
where TypedBuilderFields: Clone
|
||||||
})?;
|
})?;
|
||||||
|
@ -624,12 +704,39 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||||
false => quote! { true },
|
false => quote! { true },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let extend_fields = self.extend_fields().map(|f| {
|
||||||
|
let name = f.name;
|
||||||
|
let ty = f.ty;
|
||||||
|
quote!(#name: #ty)
|
||||||
|
});
|
||||||
|
let extend_fields_value = self.extend_fields().map(|f| {
|
||||||
|
let name = f.name;
|
||||||
|
quote!(#name: Vec::new())
|
||||||
|
});
|
||||||
|
let has_extend_fields = self.extend_fields().next().is_some();
|
||||||
|
let take_bump = if has_extend_fields {
|
||||||
|
quote!(bump: _cx.bump(),)
|
||||||
|
} else {
|
||||||
|
quote!()
|
||||||
|
};
|
||||||
|
let bump_field = if has_extend_fields {
|
||||||
|
quote!(bump: & #extend_lifetime ::dioxus::core::exports::bumpalo::Bump,)
|
||||||
|
} else {
|
||||||
|
quote!()
|
||||||
|
};
|
||||||
|
let extend_lifetime = extend_lifetime.unwrap_or(syn::Lifetime::new(
|
||||||
|
"'__bump",
|
||||||
|
proc_macro2::Span::call_site(),
|
||||||
|
));
|
||||||
|
|
||||||
Ok(quote! {
|
Ok(quote! {
|
||||||
impl #impl_generics #name #ty_generics #where_clause {
|
impl #impl_generics #name #ty_generics #where_clause {
|
||||||
#[doc = #builder_method_doc]
|
#[doc = #builder_method_doc]
|
||||||
#[allow(dead_code, clippy::type_complexity)]
|
#[allow(dead_code, clippy::type_complexity)]
|
||||||
#vis fn builder() -> #builder_name #generics_with_empty {
|
#vis fn builder(_cx: & #extend_lifetime ::dioxus::prelude::ScopeState) -> #builder_name #generics_with_empty {
|
||||||
#builder_name {
|
#builder_name {
|
||||||
|
#(#extend_fields_value,)*
|
||||||
|
#take_bump
|
||||||
fields: #empties_tuple,
|
fields: #empties_tuple,
|
||||||
_phantom: ::core::default::Default::default(),
|
_phantom: ::core::default::Default::default(),
|
||||||
}
|
}
|
||||||
|
@ -640,26 +747,19 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||||
#builder_type_doc
|
#builder_type_doc
|
||||||
#[allow(dead_code, non_camel_case_types, non_snake_case)]
|
#[allow(dead_code, non_camel_case_types, non_snake_case)]
|
||||||
#vis struct #builder_name #b_generics {
|
#vis struct #builder_name #b_generics {
|
||||||
|
#(#extend_fields,)*
|
||||||
|
#bump_field
|
||||||
fields: #all_fields_param,
|
fields: #all_fields_param,
|
||||||
_phantom: (#( #phantom_generics ),*),
|
_phantom: (#( #phantom_generics ),*),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl #b_generics_impl Clone for #builder_name #b_generics_ty #b_generics_where {
|
impl #impl_generics ::dioxus::prelude::Properties<#extend_lifetime> for #name #ty_generics
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self {
|
|
||||||
fields: self.fields.clone(),
|
|
||||||
_phantom: ::core::default::Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl #impl_generics ::dioxus::prelude::Properties for #name #ty_generics
|
|
||||||
#b_generics_where_extras_predicates
|
#b_generics_where_extras_predicates
|
||||||
{
|
{
|
||||||
type Builder = #builder_name #generics_with_empty;
|
type Builder = #builder_name #generics_with_empty;
|
||||||
const IS_STATIC: bool = #is_static;
|
const IS_STATIC: bool = #is_static;
|
||||||
fn builder() -> Self::Builder {
|
fn builder(_cx: &#extend_lifetime ::dioxus::prelude::ScopeState) -> Self::Builder {
|
||||||
#name::builder()
|
#name::builder(_cx)
|
||||||
}
|
}
|
||||||
unsafe fn memoize(&self, other: &Self) -> bool {
|
unsafe fn memoize(&self, other: &Self) -> bool {
|
||||||
#can_memoize
|
#can_memoize
|
||||||
|
@ -694,11 +794,143 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn extends_impl(&self, field: &FieldInfo) -> Result<TokenStream, Error> {
|
||||||
|
let StructInfo {
|
||||||
|
ref builder_name, ..
|
||||||
|
} = *self;
|
||||||
|
|
||||||
|
let field_name = field.name;
|
||||||
|
|
||||||
|
let descructuring = self.included_fields().map(|f| {
|
||||||
|
if f.ordinal == field.ordinal {
|
||||||
|
quote!(_)
|
||||||
|
} else {
|
||||||
|
let name = f.name;
|
||||||
|
quote!(#name)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let reconstructing = self.included_fields().map(|f| f.name);
|
||||||
|
|
||||||
|
// Add the bump lifetime to the generics
|
||||||
|
let mut ty_generics: Vec<syn::GenericArgument> = self
|
||||||
|
.generics
|
||||||
|
.params
|
||||||
|
.iter()
|
||||||
|
.map(|generic_param| match generic_param {
|
||||||
|
syn::GenericParam::Type(type_param) => {
|
||||||
|
let ident = type_param.ident.clone();
|
||||||
|
syn::parse(quote!(#ident).into()).unwrap()
|
||||||
|
}
|
||||||
|
syn::GenericParam::Lifetime(lifetime_def) => {
|
||||||
|
syn::GenericArgument::Lifetime(lifetime_def.lifetime.clone())
|
||||||
|
}
|
||||||
|
syn::GenericParam::Const(const_param) => {
|
||||||
|
let ident = const_param.ident.clone();
|
||||||
|
syn::parse(quote!(#ident).into()).unwrap()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mut target_generics_tuple = empty_type_tuple();
|
||||||
|
let mut ty_generics_tuple = empty_type_tuple();
|
||||||
|
let generics = self.modify_generics(|g| {
|
||||||
|
let index_after_lifetime_in_generics = g
|
||||||
|
.params
|
||||||
|
.iter()
|
||||||
|
.filter(|arg| matches!(arg, syn::GenericParam::Lifetime(_)))
|
||||||
|
.count();
|
||||||
|
for f in self.included_fields() {
|
||||||
|
if f.ordinal == field.ordinal {
|
||||||
|
ty_generics_tuple.elems.push_value(empty_type());
|
||||||
|
target_generics_tuple
|
||||||
|
.elems
|
||||||
|
.push_value(f.tuplized_type_ty_param());
|
||||||
|
} else {
|
||||||
|
g.params
|
||||||
|
.insert(index_after_lifetime_in_generics, f.generic_ty_param());
|
||||||
|
let generic_argument: syn::Type = f.type_ident();
|
||||||
|
ty_generics_tuple.elems.push_value(generic_argument.clone());
|
||||||
|
target_generics_tuple.elems.push_value(generic_argument);
|
||||||
|
}
|
||||||
|
ty_generics_tuple.elems.push_punct(Default::default());
|
||||||
|
target_generics_tuple.elems.push_punct(Default::default());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let mut target_generics = ty_generics.clone();
|
||||||
|
let index_after_lifetime_in_generics = target_generics
|
||||||
|
.iter()
|
||||||
|
.filter(|arg| matches!(arg, syn::GenericArgument::Lifetime(_)))
|
||||||
|
.count();
|
||||||
|
target_generics.insert(
|
||||||
|
index_after_lifetime_in_generics,
|
||||||
|
syn::GenericArgument::Type(target_generics_tuple.into()),
|
||||||
|
);
|
||||||
|
ty_generics.insert(
|
||||||
|
index_after_lifetime_in_generics,
|
||||||
|
syn::GenericArgument::Type(ty_generics_tuple.into()),
|
||||||
|
);
|
||||||
|
let (impl_generics, _, where_clause) = generics.split_for_impl();
|
||||||
|
|
||||||
|
let forward_extended_fields = self.extend_fields().map(|f| {
|
||||||
|
let name = f.name;
|
||||||
|
quote!(#name: self.#name)
|
||||||
|
});
|
||||||
|
|
||||||
|
let extend_lifetime = self.extend_lifetime()?.ok_or(Error::new_spanned(
|
||||||
|
field_name,
|
||||||
|
"Unable to find lifetime for extended field. Please specify it manually",
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let extends_impl = field.builder_attr.extends.iter().map(|path| {
|
||||||
|
let name_str = path_to_single_string(path).unwrap();
|
||||||
|
let camel_name = name_str.to_case(Case::UpperCamel);
|
||||||
|
let marker_name = Ident::new(
|
||||||
|
format!("{}Extension", &camel_name).as_str(),
|
||||||
|
path.span(),
|
||||||
|
);
|
||||||
|
quote! {
|
||||||
|
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
||||||
|
impl #impl_generics dioxus_elements::extensions::#marker_name < #extend_lifetime > for #builder_name < #( #ty_generics ),* > #where_clause {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(quote! {
|
||||||
|
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
||||||
|
impl #impl_generics ::dioxus::prelude::HasAttributes<#extend_lifetime> for #builder_name < #( #ty_generics ),* > #where_clause {
|
||||||
|
fn push_attribute(
|
||||||
|
mut self,
|
||||||
|
name: &#extend_lifetime str,
|
||||||
|
ns: Option<&'static str>,
|
||||||
|
attr: impl ::dioxus::prelude::IntoAttributeValue<#extend_lifetime>,
|
||||||
|
volatile: bool
|
||||||
|
) -> Self {
|
||||||
|
let ( #(#descructuring,)* ) = self.fields;
|
||||||
|
self.#field_name.push(
|
||||||
|
::dioxus::core::Attribute::new(
|
||||||
|
name,
|
||||||
|
{
|
||||||
|
use ::dioxus::prelude::IntoAttributeValue;
|
||||||
|
attr.into_value(self.bump)
|
||||||
|
},
|
||||||
|
ns,
|
||||||
|
volatile,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
#builder_name {
|
||||||
|
#(#forward_extended_fields,)*
|
||||||
|
bump: self.bump,
|
||||||
|
fields: ( #(#reconstructing,)* ),
|
||||||
|
_phantom: self._phantom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#(#extends_impl)*
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn field_impl(&self, field: &FieldInfo) -> Result<TokenStream, Error> {
|
pub fn field_impl(&self, field: &FieldInfo) -> Result<TokenStream, Error> {
|
||||||
let FieldInfo {
|
let FieldInfo {
|
||||||
name: field_name,
|
name: field_name, ..
|
||||||
ty: field_type,
|
|
||||||
..
|
|
||||||
} = field;
|
} = field;
|
||||||
if *field_name == "key" {
|
if *field_name == "key" {
|
||||||
return Err(Error::new_spanned(field_name, "Naming a prop `key` is not allowed because the name can conflict with the built in key attribute. See https://dioxuslabs.com/learn/0.4/reference/dynamic_rendering#rendering-lists for more information about keys"));
|
return Err(Error::new_spanned(field_name, "Naming a prop `key` is not allowed because the name can conflict with the built in key attribute. See https://dioxuslabs.com/learn/0.4/reference/dynamic_rendering#rendering-lists for more information about keys"));
|
||||||
|
@ -717,6 +949,12 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||||
});
|
});
|
||||||
let reconstructing = self.included_fields().map(|f| f.name);
|
let reconstructing = self.included_fields().map(|f| f.name);
|
||||||
|
|
||||||
|
let FieldInfo {
|
||||||
|
name: field_name,
|
||||||
|
ty: field_type,
|
||||||
|
..
|
||||||
|
} = field;
|
||||||
|
// Add the bump lifetime to the generics
|
||||||
let mut ty_generics: Vec<syn::GenericArgument> = self
|
let mut ty_generics: Vec<syn::GenericArgument> = self
|
||||||
.generics
|
.generics
|
||||||
.params
|
.params
|
||||||
|
@ -800,6 +1038,16 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||||
);
|
);
|
||||||
let repeated_fields_error_message = format!("Repeated field {field_name}");
|
let repeated_fields_error_message = format!("Repeated field {field_name}");
|
||||||
|
|
||||||
|
let forward_extended_fields = self.extend_fields().map(|f| {
|
||||||
|
let name = f.name;
|
||||||
|
quote!(#name: self.#name)
|
||||||
|
});
|
||||||
|
let forward_bump = if self.extend_fields().next().is_some() {
|
||||||
|
quote!(bump: self.bump,)
|
||||||
|
} else {
|
||||||
|
quote!()
|
||||||
|
};
|
||||||
|
|
||||||
Ok(quote! {
|
Ok(quote! {
|
||||||
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
||||||
impl #impl_generics #builder_name < #( #ty_generics ),* > #where_clause {
|
impl #impl_generics #builder_name < #( #ty_generics ),* > #where_clause {
|
||||||
|
@ -809,6 +1057,8 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||||
let #field_name = (#arg_expr,);
|
let #field_name = (#arg_expr,);
|
||||||
let ( #(#descructuring,)* ) = self.fields;
|
let ( #(#descructuring,)* ) = self.fields;
|
||||||
#builder_name {
|
#builder_name {
|
||||||
|
#(#forward_extended_fields,)*
|
||||||
|
#forward_bump
|
||||||
fields: ( #(#reconstructing,)* ),
|
fields: ( #(#reconstructing,)* ),
|
||||||
_phantom: self._phantom,
|
_phantom: self._phantom,
|
||||||
}
|
}
|
||||||
|
@ -842,6 +1092,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||||
name: ref field_name,
|
name: ref field_name,
|
||||||
..
|
..
|
||||||
} = field;
|
} = field;
|
||||||
|
// Add a bump lifetime to the generics
|
||||||
let mut builder_generics: Vec<syn::GenericArgument> = self
|
let mut builder_generics: Vec<syn::GenericArgument> = self
|
||||||
.generics
|
.generics
|
||||||
.params
|
.params
|
||||||
|
@ -1009,7 +1260,9 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||||
// reordering based on that, but for now this much simpler thing is a reasonable approach.
|
// reordering based on that, but for now this much simpler thing is a reasonable approach.
|
||||||
let assignments = self.fields.iter().map(|field| {
|
let assignments = self.fields.iter().map(|field| {
|
||||||
let name = &field.name;
|
let name = &field.name;
|
||||||
if let Some(ref default) = field.builder_attr.default {
|
if !field.builder_attr.extends.is_empty() {
|
||||||
|
quote!(let #name = self.#name;)
|
||||||
|
} else if let Some(ref default) = field.builder_attr.default {
|
||||||
if field.builder_attr.skip {
|
if field.builder_attr.skip {
|
||||||
quote!(let #name = #default;)
|
quote!(let #name = #default;)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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())
|
||||||
|
})));
|
||||||
|
|
||||||
|
// Set this id to be dropped when the scope is rerun
|
||||||
|
if let Some(scope) = self.runtime.current_scope_id() {
|
||||||
|
self.scopes[scope.0]
|
||||||
|
.element_refs_to_drop
|
||||||
|
.borrow_mut()
|
||||||
|
.push(new_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn next_null(&mut self) -> ElementId {
|
new_id
|
||||||
let entry = self.elements.vacant_entry();
|
|
||||||
let id = entry.key();
|
|
||||||
|
|
||||||
entry.insert(ElementRef::none());
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
use crate::any_props::AnyProps;
|
use crate::any_props::AnyProps;
|
||||||
use crate::innerlude::{BorrowedAttributeValue, VComponent, VPlaceholder, VText};
|
use crate::innerlude::{
|
||||||
|
AttributeType, BorrowedAttributeValue, ElementPath, ElementRef, MountedAttribute, VComponent,
|
||||||
|
VPlaceholder, VText,
|
||||||
|
};
|
||||||
use crate::mutations::Mutation;
|
use crate::mutations::Mutation;
|
||||||
use crate::mutations::Mutation::*;
|
use crate::mutations::Mutation::*;
|
||||||
use crate::nodes::VNode;
|
use crate::nodes::VNode;
|
||||||
|
@ -94,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());
|
||||||
|
@ -181,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
|
||||||
}
|
}
|
||||||
|
@ -205,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
|
||||||
///
|
///
|
||||||
|
@ -265,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..];
|
||||||
|
@ -279,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(&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) {
|
||||||
|
@ -298,15 +326,44 @@ impl<'b> VirtualDom {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_attribute(&mut self, attribute: &'b crate::Attribute<'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 {
|
||||||
|
AttributeType::Single(attribute) => self.write_attribute(vnode, attribute, idx, id),
|
||||||
|
AttributeType::Many(attribute) => {
|
||||||
|
for attribute in *attribute {
|
||||||
|
self.write_attribute(vnode, attribute, idx, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn write_attribute(
|
||||||
|
&mut self,
|
||||||
|
vnode: &'b VNode<'b>,
|
||||||
|
attribute: &'b crate::Attribute<'b>,
|
||||||
|
idx: usize,
|
||||||
|
id: ElementId,
|
||||||
|
) {
|
||||||
// Safety: we promise not to re-alias this text later on after committing it to the mutation
|
// 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..],
|
||||||
|
@ -330,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 {
|
||||||
|
@ -353,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;
|
||||||
|
@ -362,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..],
|
||||||
|
@ -405,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
|
||||||
|
@ -439,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));
|
||||||
|
@ -470,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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -481,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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -502,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::*;
|
||||||
|
@ -514,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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -531,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
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
any_props::AnyProps,
|
any_props::AnyProps,
|
||||||
arena::ElementId,
|
arena::ElementId,
|
||||||
innerlude::{BorrowedAttributeValue, DirtyScope, VComponent, VPlaceholder, VText},
|
innerlude::{
|
||||||
|
AttributeType, BorrowedAttributeValue, DirtyScope, ElementPath, ElementRef, VComponent,
|
||||||
|
VPlaceholder, VText,
|
||||||
|
},
|
||||||
mutations::Mutation,
|
mutations::Mutation,
|
||||||
nodes::RenderReturn,
|
nodes::RenderReturn,
|
||||||
nodes::{DynamicNode, VNode},
|
nodes::{DynamicNode, VNode},
|
||||||
|
@ -39,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();
|
||||||
|
@ -81,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;
|
||||||
|
@ -103,18 +126,54 @@ impl<'b> VirtualDom {
|
||||||
.dynamic_attrs
|
.dynamic_attrs
|
||||||
.iter()
|
.iter()
|
||||||
.zip(right_template.dynamic_attrs.iter())
|
.zip(right_template.dynamic_attrs.iter())
|
||||||
.for_each(|(left_attr, right_attr)| {
|
.enumerate()
|
||||||
|
.for_each(|(idx, (left_attr, right_attr))| {
|
||||||
// Move over the ID from the old to the new
|
// Move over the ID from the old to the new
|
||||||
right_attr
|
let mounted_id = left_attr.mounted_element.get();
|
||||||
.mounted_element
|
right_attr.mounted_element.set(mounted_id);
|
||||||
.set(left_attr.mounted_element.get());
|
|
||||||
|
|
||||||
// We want to make sure anything that gets pulled is valid
|
match (&left_attr.ty, &right_attr.ty) {
|
||||||
self.update_template(left_attr.mounted_element.get(), right_template);
|
(AttributeType::Single(left), AttributeType::Single(right)) => {
|
||||||
|
self.diff_attribute(left, right, mounted_id)
|
||||||
|
}
|
||||||
|
(AttributeType::Many(left), AttributeType::Many(right)) => {
|
||||||
|
let mut left_iter = left.iter().peekable();
|
||||||
|
let mut right_iter = right.iter().peekable();
|
||||||
|
|
||||||
// If the attributes are different (or volatile), we need to update them
|
loop {
|
||||||
if left_attr.value != right_attr.value || left_attr.volatile {
|
match (left_iter.peek(), right_iter.peek()) {
|
||||||
self.update_attribute(right_attr, left_attr);
|
(Some(left), Some(right)) => {
|
||||||
|
// check which name is greater
|
||||||
|
match left.name.cmp(right.name) {
|
||||||
|
std::cmp::Ordering::Less => self.remove_attribute(
|
||||||
|
left.name,
|
||||||
|
left.namespace,
|
||||||
|
mounted_id,
|
||||||
|
),
|
||||||
|
std::cmp::Ordering::Greater => self.write_attribute(
|
||||||
|
right_template,
|
||||||
|
right,
|
||||||
|
idx,
|
||||||
|
mounted_id,
|
||||||
|
),
|
||||||
|
std::cmp::Ordering::Equal => {
|
||||||
|
self.diff_attribute(left, right, mounted_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Some(_), None) => {
|
||||||
|
let left = left_iter.next().unwrap();
|
||||||
|
self.remove_attribute(left.name, left.namespace, mounted_id)
|
||||||
|
}
|
||||||
|
(None, Some(_)) => {
|
||||||
|
let right = right_iter.next().unwrap();
|
||||||
|
self.write_attribute(right_template, right, idx, mounted_id)
|
||||||
|
}
|
||||||
|
(None, None) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unreachable!("The macro should never generate this case"),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -123,8 +182,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
|
||||||
|
@ -135,14 +202,17 @@ 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(
|
||||||
|
&mut self,
|
||||||
|
left_attr: &'b Attribute<'b>,
|
||||||
|
right_attr: &'b Attribute<'b>,
|
||||||
|
id: ElementId,
|
||||||
|
) {
|
||||||
|
// If the attributes are different (or volatile), we need to update them
|
||||||
|
if left_attr.value != right_attr.value || left_attr.volatile {
|
||||||
|
self.update_attribute(right_attr, left_attr, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,25 +220,45 @@ 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."),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_attribute(&mut self, right_attr: &'b Attribute<'b>, left_attr: &'b Attribute) {
|
fn remove_attribute(&mut self, name: &'b str, ns: Option<&'static str>, id: ElementId) {
|
||||||
|
let name = unsafe { std::mem::transmute(name) };
|
||||||
|
let value: BorrowedAttributeValue<'b> = BorrowedAttributeValue::None;
|
||||||
|
let value = unsafe { std::mem::transmute(value) };
|
||||||
|
self.mutations.push(Mutation::SetAttribute {
|
||||||
|
id,
|
||||||
|
ns,
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_attribute(
|
||||||
|
&mut self,
|
||||||
|
right_attr: &'b Attribute<'b>,
|
||||||
|
left_attr: &'b Attribute<'b>,
|
||||||
|
id: ElementId,
|
||||||
|
) {
|
||||||
let name = unsafe { std::mem::transmute(left_attr.name) };
|
let name = unsafe { std::mem::transmute(left_attr.name) };
|
||||||
let value: BorrowedAttributeValue<'b> = (&right_attr.value).into();
|
let value: BorrowedAttributeValue<'b> = (&right_attr.value).into();
|
||||||
let value = unsafe { std::mem::transmute(value) };
|
let value = unsafe { std::mem::transmute(value) };
|
||||||
self.mutations.push(Mutation::SetAttribute {
|
self.mutations.push(Mutation::SetAttribute {
|
||||||
id: left_attr.mounted_element.get(),
|
id,
|
||||||
ns: right_attr.namespace,
|
ns: right_attr.namespace,
|
||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
|
@ -179,7 +269,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;
|
||||||
|
@ -187,7 +277,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
|
||||||
|
@ -228,11 +318,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();
|
||||||
|
|
||||||
|
@ -287,11 +377,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)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -299,11 +390,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 {
|
||||||
|
@ -312,7 +400,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!(
|
||||||
|
@ -325,9 +418,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -339,7 +432,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.
|
||||||
|
@ -348,7 +446,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 => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -373,7 +473,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>]| {
|
||||||
|
@ -401,7 +506,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,
|
||||||
};
|
};
|
||||||
|
@ -427,18 +532,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -451,6 +556,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;
|
||||||
|
|
||||||
|
@ -466,7 +572,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -505,7 +611,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
|
||||||
|
@ -560,9 +671,9 @@ impl<'b> VirtualDom {
|
||||||
// If none of the old keys are reused by the new children, then we remove all the remaining old children and
|
// If none of the old keys are reused by the new children, then we remove all the remaining old children and
|
||||||
// create the new children afresh.
|
// create the new children afresh.
|
||||||
if shared_keys.is_empty() {
|
if shared_keys.is_empty() {
|
||||||
if old.first().is_some() {
|
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
|
||||||
|
@ -739,20 +850,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 })
|
||||||
}
|
}
|
||||||
|
@ -762,15 +891,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();
|
||||||
|
|
||||||
|
@ -789,11 +924,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 });
|
||||||
|
@ -831,6 +967,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) {
|
||||||
|
@ -989,6 +1135,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?
|
||||||
|
|
|
@ -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},
|
||||||
cell::RefCell,
|
backtrace::Backtrace,
|
||||||
fmt::Debug,
|
cell::{Cell, RefCell},
|
||||||
|
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<'a> for ErrorBoundaryProps<'a> {
|
||||||
|
type Builder = ErrorBoundaryPropsBuilder<'a, ((), ())>;
|
||||||
|
const IS_STATIC: bool = false;
|
||||||
|
fn builder(_: &'a ScopeState) -> 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: Cell::new(None),
|
||||||
|
stable_id: Cell::new(None),
|
||||||
|
key: None,
|
||||||
|
template: std::cell::Cell::new(TEMPLATE),
|
||||||
|
root_ids: bumpalo::collections::Vec::with_capacity_in(1usize, __cx.bump()).into(),
|
||||||
|
dynamic_nodes: __cx.bump().alloc([{
|
||||||
|
let ___nodes = (&cx.props.children).into_dyn_node(__cx);
|
||||||
|
___nodes
|
||||||
|
}]),
|
||||||
|
dynamic_attrs: __cx.bump().alloc([]),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
@ -91,10 +92,10 @@ impl<'a, const A: bool> FragmentBuilder<'a, A> {
|
||||||
/// })
|
/// })
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
impl<'a> Properties for FragmentProps<'a> {
|
impl<'a> Properties<'_> for FragmentProps<'a> {
|
||||||
type Builder = FragmentBuilder<'a, false>;
|
type Builder = FragmentBuilder<'a, false>;
|
||||||
const IS_STATIC: bool = false;
|
const IS_STATIC: bool = false;
|
||||||
fn builder() -> Self::Builder {
|
fn builder(_cx: &ScopeState) -> Self::Builder {
|
||||||
FragmentBuilder(None)
|
FragmentBuilder(None)
|
||||||
}
|
}
|
||||||
unsafe fn memoize(&self, _other: &Self) -> bool {
|
unsafe fn memoize(&self, _other: &Self) -> bool {
|
||||||
|
|
|
@ -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.
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
|
|
|
@ -76,11 +76,11 @@ pub(crate) mod innerlude {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub use crate::innerlude::{
|
pub use crate::innerlude::{
|
||||||
fc_to_builder, vdom_is_rendering, AnyValue, Attribute, AttributeValue, BorrowedAttributeValue,
|
fc_to_builder, vdom_is_rendering, AnyValue, Attribute, AttributeType, AttributeValue,
|
||||||
CapturedError, Component, DynamicNode, Element, ElementId, Event, Fragment, IntoDynNode,
|
BorrowedAttributeValue, CapturedError, Component, DynamicNode, Element, ElementId, Event,
|
||||||
LazyNodes, Mutation, Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped,
|
Fragment, HasAttributes, IntoDynNode, LazyNodes, MountedAttribute, Mutation, Mutations,
|
||||||
TaskId, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText,
|
Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, TaskId, Template,
|
||||||
VirtualDom,
|
TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText, VirtualDom,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The purpose of this module is to alleviate imports of many common types
|
/// The purpose of this module is to alleviate imports of many common types
|
||||||
|
@ -90,10 +90,11 @@ 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, use_error_boundary,
|
||||||
Component, Element, Event, EventHandler, Fragment, IntoAttributeValue, IntoDynNode,
|
AnyValue, Attribute, AttributeType, Component, Element, ErrorBoundary, Event, EventHandler,
|
||||||
LazyNodes, Properties, Runtime, RuntimeGuard, Scope, ScopeId, ScopeState, Scoped, TaskId,
|
Fragment, HasAttributes, IntoAttributeValue, IntoDynNode, LazyNodes, MountedAttribute,
|
||||||
Template, TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom,
|
Properties, Runtime, RuntimeGuard, Scope, ScopeId, ScopeState, Scoped, TaskId, Template,
|
||||||
|
TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>>,
|
||||||
|
|
|
@ -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>>,
|
||||||
|
@ -60,7 +64,7 @@ pub struct VNode<'a> {
|
||||||
pub dynamic_nodes: &'a [DynamicNode<'a>],
|
pub dynamic_nodes: &'a [DynamicNode<'a>],
|
||||||
|
|
||||||
/// The dynamic parts of the template
|
/// The dynamic parts of the template
|
||||||
pub dynamic_attrs: &'a [Attribute<'a>],
|
pub dynamic_attrs: &'a [MountedAttribute<'a>],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> VNode<'a> {
|
impl<'a> VNode<'a> {
|
||||||
|
@ -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 [MountedAttribute<'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 {
|
||||||
|
@ -414,6 +445,51 @@ pub enum TemplateAttribute<'a> {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An attribute with information about its position in the DOM and the element it was mounted to
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct MountedAttribute<'a> {
|
||||||
|
pub(crate) ty: AttributeType<'a>,
|
||||||
|
|
||||||
|
/// The element in the DOM that this attribute belongs to
|
||||||
|
pub(crate) mounted_element: Cell<ElementId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<Attribute<'a>> for MountedAttribute<'a> {
|
||||||
|
fn from(attr: Attribute<'a>) -> Self {
|
||||||
|
Self {
|
||||||
|
ty: AttributeType::Single(attr),
|
||||||
|
mounted_element: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a [Attribute<'a>]> for MountedAttribute<'a> {
|
||||||
|
fn from(attr: &'a [Attribute<'a>]) -> Self {
|
||||||
|
Self {
|
||||||
|
ty: AttributeType::Many(attr),
|
||||||
|
mounted_element: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a Vec<Attribute<'a>>> for MountedAttribute<'a> {
|
||||||
|
fn from(attr: &'a Vec<Attribute<'a>>) -> Self {
|
||||||
|
attr.as_slice().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> MountedAttribute<'a> {
|
||||||
|
/// Get the type of this attribute
|
||||||
|
pub fn attribute_type(&self) -> &AttributeType<'a> {
|
||||||
|
&self.ty
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the element that this attribute is mounted to
|
||||||
|
pub fn mounted_element(&self) -> ElementId {
|
||||||
|
self.mounted_element.get()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// An attribute on a DOM node, such as `id="my-thing"` or `href="https://example.com"`
|
/// An attribute on a DOM node, such as `id="my-thing"` or `href="https://example.com"`
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Attribute<'a> {
|
pub struct Attribute<'a> {
|
||||||
|
@ -430,9 +506,6 @@ pub struct Attribute<'a> {
|
||||||
|
|
||||||
/// An indication of we should always try and set the attribute. Used in controlled components to ensure changes are propagated
|
/// An indication of we should always try and set the attribute. Used in controlled components to ensure changes are propagated
|
||||||
pub volatile: bool,
|
pub volatile: bool,
|
||||||
|
|
||||||
/// The element in the DOM that this attribute belongs to
|
|
||||||
pub(crate) mounted_element: Cell<ElementId>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Attribute<'a> {
|
impl<'a> Attribute<'a> {
|
||||||
|
@ -448,13 +521,40 @@ impl<'a> Attribute<'a> {
|
||||||
value,
|
value,
|
||||||
namespace,
|
namespace,
|
||||||
volatile,
|
volatile,
|
||||||
mounted_element: Cell::new(ElementId::default()),
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the element that this attribute is mounted to
|
/// The type of an attribute
|
||||||
pub fn mounted_element(&self) -> ElementId {
|
#[derive(Debug)]
|
||||||
self.mounted_element.get()
|
pub enum AttributeType<'a> {
|
||||||
|
/// A single attribute
|
||||||
|
Single(Attribute<'a>),
|
||||||
|
/// Many different attributes sorted by name
|
||||||
|
Many(&'a [Attribute<'a>]),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> AttributeType<'a> {
|
||||||
|
/// Call the given function on each attribute
|
||||||
|
pub fn for_each<'b, F>(&'b self, mut f: F)
|
||||||
|
where
|
||||||
|
F: FnMut(&'b Attribute<'a>),
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
Self::Single(attr) => f(attr),
|
||||||
|
Self::Many(attrs) => attrs.iter().for_each(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to call the given function on each attribute
|
||||||
|
pub fn try_for_each<'b, F, E>(&'b self, mut f: F) -> Result<(), E>
|
||||||
|
where
|
||||||
|
F: FnMut(&'b Attribute<'a>) -> Result<(), E>,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
Self::Single(attr) => f(attr),
|
||||||
|
Self::Many(attrs) => attrs.iter().try_for_each(f),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -651,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(),
|
||||||
|
@ -705,7 +805,7 @@ 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_str(&self),
|
value: cx.bump().alloc_str(&self),
|
||||||
id: Default::default(),
|
id: Default::default(),
|
||||||
|
@ -714,15 +814,16 @@ impl IntoDynNode<'_> for String {
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
|
@ -732,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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -759,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(),
|
||||||
|
@ -838,3 +939,15 @@ impl<'a, T: IntoAttributeValue<'a>> IntoAttributeValue<'a> for Option<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A trait for anything that has a dynamic list of attributes
|
||||||
|
pub trait HasAttributes<'a> {
|
||||||
|
/// Push an attribute onto the list of attributes
|
||||||
|
fn push_attribute(
|
||||||
|
self,
|
||||||
|
name: &'a str,
|
||||||
|
ns: Option<&'static str>,
|
||||||
|
attr: impl IntoAttributeValue<'a>,
|
||||||
|
volatile: bool,
|
||||||
|
) -> Self;
|
||||||
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ use crate::innerlude::*;
|
||||||
/// data: &'a str
|
/// data: &'a str
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub trait Properties: Sized {
|
pub trait Properties<'a>: Sized {
|
||||||
/// The type of the builder for this component.
|
/// The type of the builder for this component.
|
||||||
/// Used to create "in-progress" versions of the props.
|
/// Used to create "in-progress" versions of the props.
|
||||||
type Builder;
|
type Builder;
|
||||||
|
@ -41,7 +41,7 @@ pub trait Properties: Sized {
|
||||||
const IS_STATIC: bool;
|
const IS_STATIC: bool;
|
||||||
|
|
||||||
/// Create a builder for this component.
|
/// Create a builder for this component.
|
||||||
fn builder() -> Self::Builder;
|
fn builder(cx: &'a ScopeState) -> Self::Builder;
|
||||||
|
|
||||||
/// Memoization can only happen if the props are valid for the 'static lifetime
|
/// Memoization can only happen if the props are valid for the 'static lifetime
|
||||||
///
|
///
|
||||||
|
@ -51,10 +51,10 @@ pub trait Properties: Sized {
|
||||||
unsafe fn memoize(&self, other: &Self) -> bool;
|
unsafe fn memoize(&self, other: &Self) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Properties for () {
|
impl Properties<'_> for () {
|
||||||
type Builder = EmptyBuilder;
|
type Builder = EmptyBuilder;
|
||||||
const IS_STATIC: bool = true;
|
const IS_STATIC: bool = true;
|
||||||
fn builder() -> Self::Builder {
|
fn builder(_cx: &ScopeState) -> Self::Builder {
|
||||||
EmptyBuilder {}
|
EmptyBuilder {}
|
||||||
}
|
}
|
||||||
unsafe fn memoize(&self, _other: &Self) -> bool {
|
unsafe fn memoize(&self, _other: &Self) -> bool {
|
||||||
|
@ -70,8 +70,11 @@ impl EmptyBuilder {
|
||||||
|
|
||||||
/// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern
|
/// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern
|
||||||
/// to initialize a component's props.
|
/// to initialize a component's props.
|
||||||
pub fn fc_to_builder<'a, T: Properties + 'a>(_: fn(Scope<'a, T>) -> Element<'a>) -> T::Builder {
|
pub fn fc_to_builder<'a, T: Properties<'a> + 'a>(
|
||||||
T::builder()
|
cx: &'a ScopeState,
|
||||||
|
_: fn(Scope<'a, T>) -> Element<'a>,
|
||||||
|
) -> T::Builder {
|
||||||
|
T::builder(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(miri))]
|
#[cfg(not(miri))]
|
||||||
|
|
|
@ -87,6 +87,16 @@ impl Runtime {
|
||||||
self.scope_stack.borrow().last().copied()
|
self.scope_stack.borrow().last().copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Call this function with the current scope set to the given scope
|
||||||
|
///
|
||||||
|
/// Useful in a limited number of scenarios, not public.
|
||||||
|
pub(crate) fn with_scope<O>(&self, id: ScopeId, f: impl FnOnce() -> O) -> O {
|
||||||
|
self.scope_stack.borrow_mut().push(id);
|
||||||
|
let o = f();
|
||||||
|
self.scope_stack.borrow_mut().pop();
|
||||||
|
o
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the context for any scope given its ID
|
/// Get the context for any scope given its ID
|
||||||
///
|
///
|
||||||
/// This is useful for inserting or removing contexts from a scope, or rendering out its root node
|
/// This is useful for inserting or removing contexts from a scope, or rendering out its root node
|
||||||
|
@ -98,7 +108,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::*;
|
||||||
|
@ -137,6 +147,17 @@ impl RuntimeGuard {
|
||||||
push_runtime(runtime.clone());
|
push_runtime(runtime.clone());
|
||||||
Self(runtime)
|
Self(runtime)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run a function with a given runtime and scope in context
|
||||||
|
pub fn with<O>(runtime: Rc<Runtime>, scope: Option<ScopeId>, f: impl FnOnce() -> O) -> O {
|
||||||
|
let guard = Self::new(runtime.clone());
|
||||||
|
let o = match scope {
|
||||||
|
Some(scope) => Runtime::with_scope(&runtime, scope, f),
|
||||||
|
None => f(),
|
||||||
|
};
|
||||||
|
drop(guard);
|
||||||
|
o
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for RuntimeGuard {
|
impl Drop for RuntimeGuard {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
if !task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() {
|
||||||
self.sender
|
self.sender
|
||||||
.unbounded_send(SchedulerMsg::TaskNotified(task_id))
|
.unbounded_send(SchedulerMsg::TaskNotified(task_id))
|
||||||
.expect("Scheduler should exist");
|
.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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))
|
||||||
|
@ -347,11 +318,16 @@ pub fn spawn(fut: impl Future<Output = ()> + 'static) {
|
||||||
with_current_scope(|cx| cx.spawn(fut));
|
with_current_scope(|cx| cx.spawn(fut));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Spawn a future on a component given its [`ScopeId`].
|
||||||
|
pub fn spawn_at(fut: impl Future<Output = ()> + 'static, scope_id: ScopeId) -> Option<TaskId> {
|
||||||
|
with_runtime(|rt| rt.get_context(scope_id).unwrap().push_future(fut))
|
||||||
|
}
|
||||||
|
|
||||||
/// Spawn a future that Dioxus won't clean up when this component is unmounted
|
/// Spawn a future that Dioxus won't clean up when this component is unmounted
|
||||||
///
|
///
|
||||||
/// This is good for tasks that need to be run after the component has been dropped.
|
/// This is good for tasks that need to be run after the component has been dropped.
|
||||||
pub fn spawn_forever(fut: impl Future<Output = ()> + 'static) -> Option<TaskId> {
|
pub fn spawn_forever(fut: impl Future<Output = ()> + 'static) -> Option<TaskId> {
|
||||||
with_current_scope(|cx| cx.spawn_forever(fut))
|
spawn_at(fut, ScopeId(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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.
|
||||||
|
|
|
@ -2,13 +2,13 @@ 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,
|
||||||
scope_context::ScopeContext,
|
scope_context::ScopeContext,
|
||||||
AnyValue, Attribute, AttributeValue, Element, Event, Properties, TaskId,
|
AnyValue, Attribute, AttributeType, AttributeValue, Element, Event, MountedAttribute,
|
||||||
|
Properties, TaskId,
|
||||||
};
|
};
|
||||||
use bumpalo::{boxed::Box as BumpBox, Bump};
|
use bumpalo::{boxed::Box as BumpBox, Bump};
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -94,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>>>,
|
||||||
|
@ -350,6 +351,7 @@ impl<'src> ScopeState {
|
||||||
|
|
||||||
let mut listeners = self.attributes_to_drop_before_render.borrow_mut();
|
let mut listeners = self.attributes_to_drop_before_render.borrow_mut();
|
||||||
for attr in element.dynamic_attrs {
|
for attr in element.dynamic_attrs {
|
||||||
|
attr.ty.for_each(|attr| {
|
||||||
match attr.value {
|
match attr.value {
|
||||||
// We need to drop listeners before the next render because they may borrow data from the borrowed props which will be dropped
|
// We need to drop listeners before the next render because they may borrow data from the borrowed props which will be dropped
|
||||||
AttributeValue::Listener(_) => {
|
AttributeValue::Listener(_) => {
|
||||||
|
@ -364,6 +366,7 @@ impl<'src> ScopeState {
|
||||||
|
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut props = self.borrowed_props.borrow_mut();
|
let mut props = self.borrowed_props.borrow_mut();
|
||||||
|
@ -406,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
|
||||||
|
@ -419,13 +422,15 @@ impl<'src> ScopeState {
|
||||||
value: impl IntoAttributeValue<'src>,
|
value: impl IntoAttributeValue<'src>,
|
||||||
namespace: Option<&'static str>,
|
namespace: Option<&'static str>,
|
||||||
volatile: bool,
|
volatile: bool,
|
||||||
) -> Attribute<'src> {
|
) -> MountedAttribute<'src> {
|
||||||
Attribute {
|
MountedAttribute {
|
||||||
|
ty: AttributeType::Single(Attribute {
|
||||||
name,
|
name,
|
||||||
namespace,
|
namespace,
|
||||||
volatile,
|
volatile,
|
||||||
mounted_element: Default::default(),
|
|
||||||
value: value.into_value(self.bump()),
|
value: value.into_value(self.bump()),
|
||||||
|
}),
|
||||||
|
mounted_element: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -451,7 +456,7 @@ impl<'src> ScopeState {
|
||||||
) -> DynamicNode<'src>
|
) -> DynamicNode<'src>
|
||||||
where
|
where
|
||||||
// The properties must be valid until the next bump frame
|
// The properties must be valid until the next bump frame
|
||||||
P: Properties + 'src,
|
P: Properties<'src> + 'src,
|
||||||
// The current bump allocator frame must outlive the child's borrowed props
|
// The current bump allocator frame must outlive the child's borrowed props
|
||||||
'src: 'child,
|
'src: 'child,
|
||||||
{
|
{
|
||||||
|
@ -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();
|
||||||
|
|
|
@ -11,12 +11,14 @@ use crate::{
|
||||||
nodes::{Template, TemplateId},
|
nodes::{Template, TemplateId},
|
||||||
runtime::{Runtime, RuntimeGuard},
|
runtime::{Runtime, RuntimeGuard},
|
||||||
scopes::{ScopeId, ScopeState},
|
scopes::{ScopeId, ScopeState},
|
||||||
AttributeValue, Element, Event, Scope,
|
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,8 +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) {
|
||||||
let mut listeners = vec![];
|
Some(Some(el)) => el,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
let mut parent_node = self
|
||||||
|
.element_refs
|
||||||
|
.get(parent_path.template.0)
|
||||||
|
.cloned()
|
||||||
|
.map(|el| (*parent_path, el));
|
||||||
|
|
||||||
// We will clone this later. The data itself is wrapped in RC to be used in callbacks if required
|
// We will clone this later. The data itself is wrapped in RC to be used in callbacks if required
|
||||||
let uievent = Event {
|
let uievent = Event {
|
||||||
|
@ -365,21 +381,26 @@ 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 {
|
||||||
|
let mut listeners = vec![];
|
||||||
|
|
||||||
// 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 = el_ref.path;
|
let target_path = path.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 attr.name.trim_start_matches("on") == name
|
if target_path.is_decendant(&this_path) {
|
||||||
&& target_path.is_decendant(&this_path)
|
attr.ty.for_each(|attribute| {
|
||||||
{
|
if attribute.name.trim_start_matches("on") == name {
|
||||||
listeners.push(&attr.value);
|
if let AttributeValue::Listener(listener) = &attribute.value {
|
||||||
|
listeners.push(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Break if this is the exact target element.
|
// 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
|
// This means we won't call two listeners with the same name on the same element. This should be
|
||||||
|
@ -392,9 +413,8 @@ impl VirtualDom {
|
||||||
|
|
||||||
// Now that we've accumulated all the parent attributes for the target element, call them in reverse order
|
// 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
|
// We check the bubble state between each call to see if the event has been stopped from bubbling
|
||||||
for listener in listeners.drain(..).rev() {
|
for listener in listeners.into_iter().rev() {
|
||||||
if let AttributeValue::Listener(listener) = listener {
|
let origin = path.scope;
|
||||||
let origin = el_ref.scope;
|
|
||||||
self.runtime.scope_stack.borrow_mut().push(origin);
|
self.runtime.scope_stack.borrow_mut().push(origin);
|
||||||
self.runtime.rendering.set(false);
|
self.runtime.rendering.set(false);
|
||||||
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
|
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
|
||||||
|
@ -407,30 +427,33 @@ impl VirtualDom {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
parent_path = template.parent.and_then(|id| self.elements.get(id.0));
|
parent_node = template.parent.get().and_then(|element_ref| {
|
||||||
} else {
|
self.element_refs
|
||||||
break;
|
.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 = el_ref.path;
|
let target_path = path.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
|
||||||
// Only call the listener if this is the exact target element.
|
// Only call the listener if this is the exact target element.
|
||||||
if attr.name.trim_start_matches("on") == name && target_path == this_path {
|
if target_path == this_path {
|
||||||
if let AttributeValue::Listener(listener) = &attr.value {
|
let mut should_stop = false;
|
||||||
let origin = el_ref.scope;
|
attr.ty.for_each(|attribute| {
|
||||||
|
if attribute.name.trim_start_matches("on") == name {
|
||||||
|
if let AttributeValue::Listener(listener) = &attribute.value {
|
||||||
|
let origin = path.scope;
|
||||||
self.runtime.scope_stack.borrow_mut().push(origin);
|
self.runtime.scope_stack.borrow_mut().push(origin);
|
||||||
self.runtime.rendering.set(false);
|
self.runtime.rendering.set(false);
|
||||||
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
|
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
|
||||||
|
@ -439,9 +462,13 @@ impl VirtualDom {
|
||||||
self.runtime.scope_stack.borrow_mut().pop();
|
self.runtime.scope_stack.borrow_mut().pop();
|
||||||
self.runtime.rendering.set(true);
|
self.runtime.rendering.set(true);
|
||||||
|
|
||||||
break;
|
should_stop = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
if should_stop {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -563,7 +590,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 });
|
||||||
}
|
}
|
||||||
|
@ -595,15 +622,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,9 +24,9 @@ fn NoneChild(_cx: Scope) -> Element {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ThrowChild(cx: Scope) -> Element {
|
fn ThrowChild(cx: Scope) -> Element {
|
||||||
cx.throw(std::io::Error::new(std::io::ErrorKind::AddrInUse, "asd"))?;
|
Err(std::io::Error::new(std::io::ErrorKind::AddrInUse, "asd")).throw()?;
|
||||||
|
|
||||||
let _g: i32 = "123123".parse().throw(cx)?;
|
let _g: i32 = "123123".parse().throw()?;
|
||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {}
|
div {}
|
||||||
|
|
82
packages/core/tests/event_propagation.rs
Normal file
82
packages/core/tests/event_propagation.rs
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_core::ElementId;
|
||||||
|
use std::{rc::Rc, sync::Mutex};
|
||||||
|
|
||||||
|
static CLICKS: Mutex<usize> = Mutex::new(0);
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn events_propagate() {
|
||||||
|
set_event_converter(Box::new(dioxus_html::SerializedHtmlEventConverter));
|
||||||
|
|
||||||
|
let mut dom = VirtualDom::new(app);
|
||||||
|
_ = dom.rebuild();
|
||||||
|
|
||||||
|
// Top-level click is registered
|
||||||
|
dom.handle_event(
|
||||||
|
"click",
|
||||||
|
Rc::new(PlatformEventData::new(Box::<SerializedMouseData>::default())),
|
||||||
|
ElementId(1),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert_eq!(*CLICKS.lock().unwrap(), 1);
|
||||||
|
|
||||||
|
// break reference....
|
||||||
|
for _ in 0..5 {
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
_ = dom.render_immediate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lower click is registered
|
||||||
|
dom.handle_event(
|
||||||
|
"click",
|
||||||
|
Rc::new(PlatformEventData::new(Box::<SerializedMouseData>::default())),
|
||||||
|
ElementId(2),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert_eq!(*CLICKS.lock().unwrap(), 3);
|
||||||
|
|
||||||
|
// break reference....
|
||||||
|
for _ in 0..5 {
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
_ = dom.render_immediate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop propagation occurs
|
||||||
|
dom.handle_event(
|
||||||
|
"click",
|
||||||
|
Rc::new(PlatformEventData::new(Box::<SerializedMouseData>::default())),
|
||||||
|
ElementId(2),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert_eq!(*CLICKS.lock().unwrap(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
render! {
|
||||||
|
div { onclick: move |_| {
|
||||||
|
println!("top clicked");
|
||||||
|
*CLICKS.lock().unwrap() += 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
vec![
|
||||||
|
render! {
|
||||||
|
problematic_child {}
|
||||||
|
}
|
||||||
|
].into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn problematic_child(cx: Scope) -> Element {
|
||||||
|
render! {
|
||||||
|
button { onclick: move |evt| {
|
||||||
|
println!("bottom clicked");
|
||||||
|
let mut clicks = CLICKS.lock().unwrap();
|
||||||
|
if *clicks == 3 {
|
||||||
|
evt.stop_propagation();
|
||||||
|
} else {
|
||||||
|
*clicks += 1;
|
||||||
|
}
|
||||||
|
} }
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
#![cfg(not(miri))]
|
#![cfg(not(miri))]
|
||||||
|
|
||||||
use dioxus::prelude::Props;
|
use dioxus::prelude::Props;
|
||||||
use dioxus_core::*;
|
use dioxus_core::{MountedAttribute, *};
|
||||||
use std::{cell::Cell, collections::HashSet};
|
use std::{cfg, collections::HashSet};
|
||||||
|
|
||||||
fn random_ns() -> Option<&'static str> {
|
fn random_ns() -> Option<&'static str> {
|
||||||
let namespace = rand::random::<u8>() % 2;
|
let namespace = rand::random::<u8>() % 2;
|
||||||
|
@ -170,22 +170,23 @@ fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode {
|
||||||
let range = if depth > 5 { 1 } else { 4 };
|
let range = if depth > 5 { 1 } else { 4 };
|
||||||
match rand::random::<u8>() % range {
|
match rand::random::<u8>() % range {
|
||||||
0 => DynamicNode::Placeholder(Default::default()),
|
0 => DynamicNode::Placeholder(Default::default()),
|
||||||
1 => cx.make_node((0..(rand::random::<u8>() % 5)).map(|_| VNode {
|
1 => cx.make_node((0..(rand::random::<u8>() % 5)).map(|_| {
|
||||||
key: None,
|
VNode::new(
|
||||||
parent: Default::default(),
|
None,
|
||||||
template: Cell::new(Template {
|
Template {
|
||||||
name: concat!(file!(), ":", line!(), ":", column!(), ":0"),
|
name: concat!(file!(), ":", line!(), ":", column!(), ":0"),
|
||||||
roots: &[TemplateNode::Dynamic { id: 0 }],
|
roots: &[TemplateNode::Dynamic { id: 0 }],
|
||||||
node_paths: &[&[0]],
|
node_paths: &[&[0]],
|
||||||
attr_paths: &[],
|
attr_paths: &[],
|
||||||
}),
|
},
|
||||||
root_ids: bumpalo::collections::Vec::new_in(cx.bump()).into(),
|
bumpalo::collections::Vec::new_in(cx.bump()),
|
||||||
dynamic_nodes: cx.bump().alloc([cx.component(
|
cx.bump().alloc([cx.component(
|
||||||
create_random_element,
|
create_random_element,
|
||||||
DepthProps { depth, root: false },
|
DepthProps { depth, root: false },
|
||||||
"create_random_element",
|
"create_random_element",
|
||||||
)]),
|
)]),
|
||||||
dynamic_attrs: &[],
|
&[],
|
||||||
|
)
|
||||||
})),
|
})),
|
||||||
2 => cx.component(
|
2 => cx.component(
|
||||||
create_random_element,
|
create_random_element,
|
||||||
|
@ -205,7 +206,7 @@ fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute {
|
fn create_random_dynamic_attr(cx: &ScopeState) -> MountedAttribute {
|
||||||
let value = match rand::random::<u8>() % 7 {
|
let value = match rand::random::<u8>() % 7 {
|
||||||
0 => AttributeValue::Text(Box::leak(
|
0 => AttributeValue::Text(Box::leak(
|
||||||
format!("{}", rand::random::<usize>()).into_boxed_str(),
|
format!("{}", rand::random::<usize>()).into_boxed_str(),
|
||||||
|
@ -217,7 +218,7 @@ fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute {
|
||||||
5 => AttributeValue::None,
|
5 => AttributeValue::None,
|
||||||
6 => {
|
6 => {
|
||||||
let value = cx.listener(|e: Event<String>| println!("{:?}", e));
|
let value = cx.listener(|e: Event<String>| println!("{:?}", e));
|
||||||
return Attribute::new("ondata", value, None, false);
|
return Attribute::new("ondata", value, None, false).into();
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
|
@ -227,6 +228,7 @@ fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute {
|
||||||
random_ns(),
|
random_ns(),
|
||||||
rand::random(),
|
rand::random(),
|
||||||
)
|
)
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
static mut TEMPLATE_COUNT: usize = 0;
|
static mut TEMPLATE_COUNT: usize = 0;
|
||||||
|
@ -271,13 +273,11 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
|
||||||
)
|
)
|
||||||
.into_boxed_str(),
|
.into_boxed_str(),
|
||||||
));
|
));
|
||||||
// println!("{template:#?}");
|
let node = VNode::new(
|
||||||
let node = VNode {
|
None,
|
||||||
key: None,
|
template,
|
||||||
parent: None,
|
bumpalo::collections::Vec::new_in(cx.bump()),
|
||||||
template: Cell::new(template),
|
{
|
||||||
root_ids: bumpalo::collections::Vec::new_in(cx.bump()).into(),
|
|
||||||
dynamic_nodes: {
|
|
||||||
let dynamic_nodes: Vec<_> = dynamic_node_types
|
let dynamic_nodes: Vec<_> = dynamic_node_types
|
||||||
.iter()
|
.iter()
|
||||||
.map(|ty| match ty {
|
.map(|ty| match ty {
|
||||||
|
@ -291,12 +291,12 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
|
||||||
.collect();
|
.collect();
|
||||||
cx.bump().alloc(dynamic_nodes)
|
cx.bump().alloc(dynamic_nodes)
|
||||||
},
|
},
|
||||||
dynamic_attrs: cx.bump().alloc(
|
cx.bump().alloc(
|
||||||
(0..template.attr_paths.len())
|
(0..template.attr_paths.len())
|
||||||
.map(|_| create_random_dynamic_attr(cx))
|
.map(|_| create_random_dynamic_attr(cx))
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
),
|
),
|
||||||
};
|
);
|
||||||
Some(node)
|
Some(node)
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
|
@ -306,10 +306,10 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
|
||||||
}
|
}
|
||||||
|
|
||||||
// test for panics when creating random nodes and templates
|
// test for panics when creating random nodes and templates
|
||||||
#[cfg(not(miri))]
|
|
||||||
#[test]
|
#[test]
|
||||||
fn create() {
|
fn create() {
|
||||||
for _ in 0..1000 {
|
let repeat_count = if cfg!(miri) { 100 } else { 1000 };
|
||||||
|
for _ in 0..repeat_count {
|
||||||
let mut vdom =
|
let mut vdom =
|
||||||
VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true });
|
VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true });
|
||||||
let _ = vdom.rebuild();
|
let _ = vdom.rebuild();
|
||||||
|
@ -318,10 +318,10 @@ fn create() {
|
||||||
|
|
||||||
// test for panics when diffing random nodes
|
// test for panics when diffing random nodes
|
||||||
// This test will change the template every render which is not very realistic, but it helps stress the system
|
// This test will change the template every render which is not very realistic, but it helps stress the system
|
||||||
#[cfg(not(miri))]
|
|
||||||
#[test]
|
#[test]
|
||||||
fn diff() {
|
fn diff() {
|
||||||
for _ in 0..100000 {
|
let repeat_count = if cfg!(miri) { 100 } else { 1000 };
|
||||||
|
for _ in 0..repeat_count {
|
||||||
let mut vdom =
|
let mut vdom =
|
||||||
VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true });
|
VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true });
|
||||||
let _ = vdom.rebuild();
|
let _ = vdom.rebuild();
|
||||||
|
|
|
@ -10,6 +10,8 @@ fn basic_syntax_is_a_template(cx: Scope) -> Element {
|
||||||
div { key: "12345",
|
div { key: "12345",
|
||||||
class: "asd",
|
class: "asd",
|
||||||
class: "{asd}",
|
class: "{asd}",
|
||||||
|
class: if true { "{asd}" },
|
||||||
|
class: if false { "{asd}" },
|
||||||
onclick: move |_| {},
|
onclick: move |_| {},
|
||||||
div { "{var}" }
|
div { "{var}" }
|
||||||
div {
|
div {
|
||||||
|
@ -24,6 +26,7 @@ fn basic_syntax_is_a_template(cx: Scope) -> Element {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dual_stream() {
|
fn dual_stream() {
|
||||||
let mut dom = VirtualDom::new(basic_syntax_is_a_template);
|
let mut dom = VirtualDom::new(basic_syntax_is_a_template);
|
||||||
|
@ -36,7 +39,7 @@ fn dual_stream() {
|
||||||
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
||||||
SetAttribute {
|
SetAttribute {
|
||||||
name: "class",
|
name: "class",
|
||||||
value: (&*bump.alloc("123".into_value(&bump))).into(),
|
value: (&*bump.alloc("asd 123 123".into_value(&bump))).into(),
|
||||||
id: ElementId(1),
|
id: ElementId(1),
|
||||||
ns: None,
|
ns: None,
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
//! Tests for the lifecycle of components.
|
//! Tests for the lifecycle of components.
|
||||||
use dioxus::core::{ElementId, Mutation::*};
|
use dioxus::core::{ElementId, Mutation::*};
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_html::SerializedHtmlEventConverter;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
@ -39,6 +40,7 @@ fn manual_diffing() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn events_generate() {
|
fn events_generate() {
|
||||||
|
set_event_converter(Box::new(SerializedHtmlEventConverter));
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
let count = cx.use_hook(|| 0);
|
let count = cx.use_hook(|| 0);
|
||||||
|
|
||||||
|
@ -56,7 +58,12 @@ fn events_generate() {
|
||||||
let mut dom = VirtualDom::new(app);
|
let mut dom = VirtualDom::new(app);
|
||||||
_ = dom.rebuild();
|
_ = dom.rebuild();
|
||||||
|
|
||||||
dom.handle_event("click", Rc::new(MouseData::default()), ElementId(1), true);
|
dom.handle_event(
|
||||||
|
"click",
|
||||||
|
Rc::new(PlatformEventData::new(Box::<SerializedMouseData>::default())),
|
||||||
|
ElementId(1),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::ROOT);
|
||||||
let edits = dom.render_immediate();
|
let edits = dom.render_immediate();
|
||||||
|
|
|
@ -1,15 +1,23 @@
|
||||||
|
use crate::dioxus_elements::SerializedMouseData;
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use dioxus_core::ElementId;
|
use dioxus_core::ElementId;
|
||||||
|
use dioxus_elements::SerializedHtmlEventConverter;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn miri_rollover() {
|
fn miri_rollover() {
|
||||||
|
set_event_converter(Box::new(SerializedHtmlEventConverter));
|
||||||
let mut dom = VirtualDom::new(App);
|
let mut dom = VirtualDom::new(App);
|
||||||
|
|
||||||
_ = dom.rebuild();
|
_ = dom.rebuild();
|
||||||
|
|
||||||
for _ in 0..3 {
|
for _ in 0..3 {
|
||||||
dom.handle_event("click", Rc::new(MouseData::default()), ElementId(2), true);
|
dom.handle_event(
|
||||||
|
"click",
|
||||||
|
Rc::new(PlatformEventData::new(Box::<SerializedMouseData>::default())),
|
||||||
|
ElementId(2),
|
||||||
|
true,
|
||||||
|
);
|
||||||
dom.process_events();
|
dom.process_events();
|
||||||
_ = dom.render_immediate();
|
_ = dom.render_immediate();
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ fn it_works() {
|
||||||
let mut dom = VirtualDom::new(app);
|
let mut dom = VirtualDom::new(app);
|
||||||
_ = dom.rebuild();
|
_ = dom.rebuild();
|
||||||
dom.wait_for_suspense().await;
|
dom.wait_for_suspense().await;
|
||||||
let out = dioxus_ssr::pre_render(&dom);
|
let out = dioxus_ssr::render(&dom);
|
||||||
|
|
||||||
assert_eq!(out, "<div>Waiting for... child</div>");
|
assert_eq!(out, "<div>Waiting for... child</div>");
|
||||||
|
|
||||||
|
|
|
@ -1,29 +1,12 @@
|
||||||
//! Verify that tasks get polled by the virtualdom properly, and that we escape wait_for_work safely
|
//! Verify that tasks get polled by the virtualdom properly, and that we escape wait_for_work safely
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
|
||||||
use std::{sync::atomic::AtomicUsize, time::Duration};
|
|
||||||
|
|
||||||
static POLL_COUNT: AtomicUsize = AtomicUsize::new(0);
|
|
||||||
|
|
||||||
#[cfg(not(miri))]
|
#[cfg(not(miri))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn it_works() {
|
async fn it_works() {
|
||||||
let mut dom = VirtualDom::new(app);
|
use dioxus::prelude::*;
|
||||||
|
use std::{sync::atomic::AtomicUsize, time::Duration};
|
||||||
|
|
||||||
let _ = dom.rebuild();
|
static POLL_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
|
||||||
tokio::select! {
|
|
||||||
_ = dom.wait_for_work() => {}
|
|
||||||
_ = tokio::time::sleep(Duration::from_millis(500)) => {}
|
|
||||||
};
|
|
||||||
|
|
||||||
// By the time the tasks are finished, we should've accumulated ticks from two tasks
|
|
||||||
// Be warned that by setting the delay to too short, tokio might not schedule in the tasks
|
|
||||||
assert_eq!(
|
|
||||||
POLL_COUNT.fetch_add(0, std::sync::atomic::Ordering::Relaxed),
|
|
||||||
135
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
cx.use_hook(|| {
|
cx.use_hook(|| {
|
||||||
|
@ -44,3 +27,20 @@ fn app(cx: Scope) -> Element {
|
||||||
|
|
||||||
cx.render(rsx!(()))
|
cx.render(rsx!(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut dom = VirtualDom::new(app);
|
||||||
|
|
||||||
|
let _ = dom.rebuild();
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = dom.wait_for_work() => {}
|
||||||
|
_ = tokio::time::sleep(Duration::from_millis(500)) => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// By the time the tasks are finished, we should've accumulated ticks from two tasks
|
||||||
|
// Be warned that by setting the delay to too short, tokio might not schedule in the tasks
|
||||||
|
assert_eq!(
|
||||||
|
POLL_COUNT.fetch_add(0, std::sync::atomic::Ordering::Relaxed),
|
||||||
|
135
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
1
packages/desktop/.gitignore
vendored
Normal file
1
packages/desktop/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/src/minified.js
|
|
@ -11,15 +11,24 @@ keywords = ["dom", "ui", "gui", "react"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dioxus-core = { workspace = true, features = ["serialize"] }
|
dioxus-core = { workspace = true, features = ["serialize"] }
|
||||||
dioxus-html = { workspace = true, features = ["serialize", "native-bind"] }
|
dioxus-html = { workspace = true, features = [
|
||||||
dioxus-interpreter-js = { workspace = true }
|
"serialize",
|
||||||
|
"native-bind",
|
||||||
|
"mounted",
|
||||||
|
"eval",
|
||||||
|
] }
|
||||||
|
dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] }
|
||||||
dioxus-hot-reload = { workspace = true, optional = true }
|
dioxus-hot-reload = { workspace = true, optional = true }
|
||||||
|
|
||||||
serde = "1.0.136"
|
serde = "1.0.136"
|
||||||
serde_json = "1.0.79"
|
serde_json = "1.0.79"
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
wry = { version = "0.28.0", default-features = false, features = ["protocol", "file-drop"] }
|
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
|
wry = { version = "0.35.0", default-features = false, features = [
|
||||||
|
"os-webview",
|
||||||
|
"protocol",
|
||||||
|
"file-drop",
|
||||||
|
] }
|
||||||
futures-channel = { workspace = true }
|
futures-channel = { workspace = true }
|
||||||
tokio = { workspace = true, features = [
|
tokio = { workspace = true, features = [
|
||||||
"sync",
|
"sync",
|
||||||
|
@ -33,14 +42,23 @@ webbrowser = "0.8.0"
|
||||||
infer = "0.11.0"
|
infer = "0.11.0"
|
||||||
dunce = "1.0.2"
|
dunce = "1.0.2"
|
||||||
slab = { workspace = true }
|
slab = { workspace = true }
|
||||||
|
rustc-hash = { workspace = true }
|
||||||
|
|
||||||
futures-util = { workspace = true }
|
futures-util = { workspace = true }
|
||||||
urlencoding = "2.1.2"
|
urlencoding = "2.1.2"
|
||||||
async-trait = "0.1.68"
|
async-trait = "0.1.68"
|
||||||
|
crossbeam-channel = "0.5.8"
|
||||||
|
tao = { version = "0.24.0", features = ["rwh_05"] }
|
||||||
|
|
||||||
[target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux",target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies]
|
[target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux",target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies]
|
||||||
rfd = "0.11.3"
|
# This is only for debug mode, and it appears mobile does not support some packages this uses
|
||||||
|
manganis-cli-support = { git = "https://github.com/DioxusLabs/collect-assets", optional = true, features = [
|
||||||
|
"webp",
|
||||||
|
"html",
|
||||||
|
] }
|
||||||
|
rfd = "0.12"
|
||||||
|
global-hotkey = "0.4.1"
|
||||||
|
muda = "0.11.3"
|
||||||
|
|
||||||
[target.'cfg(target_os = "ios")'.dependencies]
|
[target.'cfg(target_os = "ios")'.dependencies]
|
||||||
objc = "0.2.7"
|
objc = "0.2.7"
|
||||||
|
@ -56,14 +74,13 @@ tokio_runtime = ["tokio"]
|
||||||
fullscreen = ["wry/fullscreen"]
|
fullscreen = ["wry/fullscreen"]
|
||||||
transparent = ["wry/transparent"]
|
transparent = ["wry/transparent"]
|
||||||
devtools = ["wry/devtools"]
|
devtools = ["wry/devtools"]
|
||||||
tray = ["wry/tray"]
|
|
||||||
dox = ["wry/dox"]
|
|
||||||
hot-reload = ["dioxus-hot-reload"]
|
hot-reload = ["dioxus-hot-reload"]
|
||||||
|
asset-collect = ["manganis-cli-support"]
|
||||||
gnu = []
|
gnu = []
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
default-features = false
|
default-features = false
|
||||||
features = [ "dox", "tokio_runtime", "hot-reload" ]
|
features = ["tokio_runtime", "hot-reload"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
dioxus-core-macro = { workspace = true }
|
dioxus-core-macro = { workspace = true }
|
||||||
|
@ -72,6 +89,10 @@ dioxus = { workspace = true }
|
||||||
exitcode = "1.1.2"
|
exitcode = "1.1.2"
|
||||||
scraper = "0.16.0"
|
scraper = "0.16.0"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] }
|
||||||
|
minify-js = "0.5.6"
|
||||||
|
|
||||||
# These tests need to be run on the main thread, so they cannot use rust's test harness.
|
# These tests need to be run on the main thread, so they cannot use rust's test harness.
|
||||||
[[test]]
|
[[test]]
|
||||||
name = "check_events"
|
name = "check_events"
|
||||||
|
|
|
@ -1,4 +1,19 @@
|
||||||
fn main() {
|
use dioxus_interpreter_js::binary_protocol::SLEDGEHAMMER_JS;
|
||||||
|
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
const EDITS_PATH: &str = {
|
||||||
|
#[cfg(any(target_os = "android", target_os = "windows"))]
|
||||||
|
{
|
||||||
|
"http://dioxus.index.html/edits"
|
||||||
|
}
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "windows")))]
|
||||||
|
{
|
||||||
|
"dioxus://index.html/edits"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn check_gnu() {
|
||||||
// WARN about wry support on windows gnu targets. GNU windows targets don't work well in wry currently
|
// WARN about wry support on windows gnu targets. GNU windows targets don't work well in wry currently
|
||||||
if std::env::var("CARGO_CFG_WINDOWS").is_ok()
|
if std::env::var("CARGO_CFG_WINDOWS").is_ok()
|
||||||
&& std::env::var("CARGO_CFG_TARGET_ENV").unwrap() == "gnu"
|
&& std::env::var("CARGO_CFG_TARGET_ENV").unwrap() == "gnu"
|
||||||
|
@ -7,3 +22,73 @@ fn main() {
|
||||||
println!("cargo:warning=GNU windows targets have some limitations within Wry. Using the MSVC windows toolchain is recommended. If you would like to use continue using GNU, you can read https://github.com/wravery/webview2-rs#cross-compilation and disable this warning by adding the gnu feature to dioxus-desktop in your Cargo.toml")
|
println!("cargo:warning=GNU windows targets have some limitations within Wry. Using the MSVC windows toolchain is recommended. If you would like to use continue using GNU, you can read https://github.com/wravery/webview2-rs#cross-compilation and disable this warning by adding the gnu feature to dioxus-desktop in your Cargo.toml")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
check_gnu();
|
||||||
|
|
||||||
|
let prevent_file_upload = r#"// Prevent file inputs from opening the file dialog on click
|
||||||
|
let inputs = document.querySelectorAll("input");
|
||||||
|
for (let input of inputs) {
|
||||||
|
if (!input.getAttribute("data-dioxus-file-listener")) {
|
||||||
|
// prevent file inputs from opening the file dialog on click
|
||||||
|
const type = input.getAttribute("type");
|
||||||
|
if (type === "file") {
|
||||||
|
input.setAttribute("data-dioxus-file-listener", true);
|
||||||
|
input.addEventListener("click", (event) => {
|
||||||
|
let target = event.target;
|
||||||
|
let target_id = find_real_id(target);
|
||||||
|
if (target_id !== null) {
|
||||||
|
const send = (event_name) => {
|
||||||
|
const message = window.interpreter.serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name });
|
||||||
|
window.ipc.postMessage(message);
|
||||||
|
};
|
||||||
|
send("change&input");
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
let polling_request = format!(
|
||||||
|
r#"// Poll for requests
|
||||||
|
window.interpreter.wait_for_request = (headless) => {{
|
||||||
|
fetch(new Request("{EDITS_PATH}"))
|
||||||
|
.then(response => {{
|
||||||
|
response.arrayBuffer()
|
||||||
|
.then(bytes => {{
|
||||||
|
// In headless mode, the requestAnimationFrame callback is never called, so we need to run the bytes directly
|
||||||
|
if (headless) {{
|
||||||
|
run_from_bytes(bytes);
|
||||||
|
}}
|
||||||
|
else {{
|
||||||
|
requestAnimationFrame(() => {{
|
||||||
|
run_from_bytes(bytes);
|
||||||
|
}});
|
||||||
|
}}
|
||||||
|
window.interpreter.wait_for_request(headless);
|
||||||
|
}});
|
||||||
|
}})
|
||||||
|
}}"#
|
||||||
|
);
|
||||||
|
let mut interpreter = SLEDGEHAMMER_JS
|
||||||
|
.replace("/*POST_HANDLE_EDITS*/", prevent_file_upload)
|
||||||
|
.replace("export", "")
|
||||||
|
+ &polling_request;
|
||||||
|
while let Some(import_start) = interpreter.find("import") {
|
||||||
|
let import_end = interpreter[import_start..]
|
||||||
|
.find(|c| c == ';' || c == '\n')
|
||||||
|
.map(|i| i + import_start)
|
||||||
|
.unwrap_or_else(|| interpreter.len());
|
||||||
|
interpreter.replace_range(import_start..import_end, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
let js = format!("{interpreter}\nconst config = new InterpreterConfig(false);");
|
||||||
|
|
||||||
|
use minify_js::*;
|
||||||
|
let session = Session::new();
|
||||||
|
let mut out = Vec::new();
|
||||||
|
minify(&session, TopLevelMode::Module, js.as_bytes(), &mut out).unwrap();
|
||||||
|
let minified = String::from_utf8(out).unwrap();
|
||||||
|
let mut file = std::fs::File::create("src/minified.js").unwrap();
|
||||||
|
file.write_all(minified.as_bytes()).unwrap();
|
||||||
|
}
|
||||||
|
|
32
packages/desktop/examples/stress.rs
Normal file
32
packages/desktop/examples/stress.rs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let state = use_state(cx, || 0);
|
||||||
|
use_future(cx, (), |_| {
|
||||||
|
to_owned![state];
|
||||||
|
async move {
|
||||||
|
loop {
|
||||||
|
state += 1;
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.render(rsx! {
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
state.set(0);
|
||||||
|
},
|
||||||
|
"reset"
|
||||||
|
}
|
||||||
|
for _ in 0..10000 {
|
||||||
|
div {
|
||||||
|
"hello desktop! {state}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus_desktop::launch(app);
|
||||||
|
}
|
|
@ -17,7 +17,7 @@ pub(crate) fn check_app_exits(app: Component) {
|
||||||
|
|
||||||
dioxus_desktop::launch_cfg(
|
dioxus_desktop::launch_cfg(
|
||||||
app,
|
app,
|
||||||
Config::new().with_window(WindowBuilder::new().with_visible(false)),
|
Config::new().with_window(WindowBuilder::new().with_visible(true)),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Stop deadman's switch
|
// Stop deadman's switch
|
||||||
|
@ -52,7 +52,7 @@ fn mock_event(cx: &ScopeState, id: &'static str, value: &'static str) {
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
let desktop_context: DesktopContext = cx.consume_context().unwrap();
|
let desktop_context: DesktopContext = cx.consume_context().unwrap();
|
||||||
let recieved_events = use_state(cx, || 0);
|
let received_events = use_state(cx, || 0);
|
||||||
|
|
||||||
// button
|
// button
|
||||||
mock_event(
|
mock_event(
|
||||||
|
@ -216,12 +216,12 @@ fn app(cx: Scope) -> Element {
|
||||||
r#"new FocusEvent("focusout",{bubbles: true})"#,
|
r#"new FocusEvent("focusout",{bubbles: true})"#,
|
||||||
);
|
);
|
||||||
|
|
||||||
if **recieved_events == 12 {
|
if **received_events == 12 {
|
||||||
println!("all events recieved");
|
println!("all events recieved");
|
||||||
desktop_context.close();
|
desktop_context.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.render(rsx! {
|
render! {
|
||||||
div {
|
div {
|
||||||
button {
|
button {
|
||||||
id: "button",
|
id: "button",
|
||||||
|
@ -229,38 +229,64 @@ fn app(cx: Scope) -> Element {
|
||||||
println!("{:?}", event.data);
|
println!("{:?}", event.data);
|
||||||
assert!(event.data.modifiers().is_empty());
|
assert!(event.data.modifiers().is_empty());
|
||||||
assert!(event.data.held_buttons().is_empty());
|
assert!(event.data.held_buttons().is_empty());
|
||||||
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary));
|
assert_eq!(
|
||||||
recieved_events.modify(|x| *x + 1)
|
event.data.trigger_button(),
|
||||||
},
|
Some(dioxus_html::input_data::MouseButton::Primary),
|
||||||
|
);
|
||||||
|
received_events.modify(|x| *x + 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
id: "mouse_move_div",
|
id: "mouse_move_div",
|
||||||
onmousemove: move |event| {
|
onmousemove: move |event| {
|
||||||
println!("{:?}", event.data);
|
println!("{:?}", event.data);
|
||||||
assert!(event.data.modifiers().is_empty());
|
assert!(event.data.modifiers().is_empty());
|
||||||
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
|
assert!(
|
||||||
recieved_events.modify(|x| *x + 1)
|
event
|
||||||
},
|
.data
|
||||||
|
.held_buttons()
|
||||||
|
.contains(dioxus_html::input_data::MouseButton::Secondary),
|
||||||
|
);
|
||||||
|
received_events.modify(|x| *x + 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
id: "mouse_click_div",
|
id: "mouse_click_div",
|
||||||
onclick: move |event| {
|
onclick: move |event| {
|
||||||
println!("{:?}", event.data);
|
println!("{:?}", event.data);
|
||||||
assert!(event.data.modifiers().is_empty());
|
assert!(event.data.modifiers().is_empty());
|
||||||
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
|
assert!(
|
||||||
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary));
|
event
|
||||||
recieved_events.modify(|x| *x + 1)
|
.data
|
||||||
},
|
.held_buttons()
|
||||||
|
.contains(dioxus_html::input_data::MouseButton::Secondary),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
event.data.trigger_button(),
|
||||||
|
Some(dioxus_html::input_data::MouseButton::Secondary),
|
||||||
|
);
|
||||||
|
received_events.modify(|x| *x + 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
id: "mouse_dblclick_div",
|
id: "mouse_dblclick_div",
|
||||||
ondblclick: move |event| {
|
ondoubleclick: move |event| {
|
||||||
println!("{:?}", event.data);
|
println!("{:?}", event.data);
|
||||||
assert!(event.data.modifiers().is_empty());
|
assert!(event.data.modifiers().is_empty());
|
||||||
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary));
|
assert!(
|
||||||
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
|
event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary),
|
||||||
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary));
|
);
|
||||||
recieved_events.modify(|x| *x + 1)
|
assert!(
|
||||||
|
event
|
||||||
|
.data
|
||||||
|
.held_buttons()
|
||||||
|
.contains(dioxus_html::input_data::MouseButton::Secondary),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
event.data.trigger_button(),
|
||||||
|
Some(dioxus_html::input_data::MouseButton::Secondary),
|
||||||
|
);
|
||||||
|
received_events.modify(|x| *x + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
|
@ -268,9 +294,17 @@ fn app(cx: Scope) -> Element {
|
||||||
onmousedown: move |event| {
|
onmousedown: move |event| {
|
||||||
println!("{:?}", event.data);
|
println!("{:?}", event.data);
|
||||||
assert!(event.data.modifiers().is_empty());
|
assert!(event.data.modifiers().is_empty());
|
||||||
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
|
assert!(
|
||||||
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary));
|
event
|
||||||
recieved_events.modify(|x| *x + 1)
|
.data
|
||||||
|
.held_buttons()
|
||||||
|
.contains(dioxus_html::input_data::MouseButton::Secondary),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
event.data.trigger_button(),
|
||||||
|
Some(dioxus_html::input_data::MouseButton::Secondary),
|
||||||
|
);
|
||||||
|
received_events.modify(|x| *x + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
|
@ -279,8 +313,11 @@ fn app(cx: Scope) -> Element {
|
||||||
println!("{:?}", event.data);
|
println!("{:?}", event.data);
|
||||||
assert!(event.data.modifiers().is_empty());
|
assert!(event.data.modifiers().is_empty());
|
||||||
assert!(event.data.held_buttons().is_empty());
|
assert!(event.data.held_buttons().is_empty());
|
||||||
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary));
|
assert_eq!(
|
||||||
recieved_events.modify(|x| *x + 1)
|
event.data.trigger_button(),
|
||||||
|
Some(dioxus_html::input_data::MouseButton::Primary),
|
||||||
|
);
|
||||||
|
received_events.modify(|x| *x + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
|
@ -291,10 +328,9 @@ fn app(cx: Scope) -> Element {
|
||||||
onwheel: move |event| {
|
onwheel: move |event| {
|
||||||
println!("{:?}", event.data);
|
println!("{:?}", event.data);
|
||||||
let dioxus_html::geometry::WheelDelta::Pixels(delta) = event.data.delta() else {
|
let dioxus_html::geometry::WheelDelta::Pixels(delta) = event.data.delta() else {
|
||||||
panic!("Expected delta to be in pixels")
|
panic!("Expected delta to be in pixels") };
|
||||||
};
|
|
||||||
assert_eq!(delta, Vector3D::new(1.0, 2.0, 3.0));
|
assert_eq!(delta, Vector3D::new(1.0, 2.0, 3.0));
|
||||||
recieved_events.modify(|x| *x + 1)
|
received_events.modify(|x| *x + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
input {
|
input {
|
||||||
|
@ -304,10 +340,9 @@ fn app(cx: Scope) -> Element {
|
||||||
assert!(event.data.modifiers().is_empty());
|
assert!(event.data.modifiers().is_empty());
|
||||||
assert_eq!(event.data.key().to_string(), "a");
|
assert_eq!(event.data.key().to_string(), "a");
|
||||||
assert_eq!(event.data.code().to_string(), "KeyA");
|
assert_eq!(event.data.code().to_string(), "KeyA");
|
||||||
assert_eq!(event.data.location, 0);
|
assert_eq!(event.data.location(), Location::Standard);
|
||||||
assert!(event.data.is_auto_repeating());
|
assert!(event.data.is_auto_repeating());
|
||||||
|
received_events.modify(|x| *x + 1)
|
||||||
recieved_events.modify(|x| *x + 1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
input {
|
input {
|
||||||
|
@ -317,10 +352,9 @@ fn app(cx: Scope) -> Element {
|
||||||
assert!(event.data.modifiers().is_empty());
|
assert!(event.data.modifiers().is_empty());
|
||||||
assert_eq!(event.data.key().to_string(), "a");
|
assert_eq!(event.data.key().to_string(), "a");
|
||||||
assert_eq!(event.data.code().to_string(), "KeyA");
|
assert_eq!(event.data.code().to_string(), "KeyA");
|
||||||
assert_eq!(event.data.location, 0);
|
assert_eq!(event.data.location(), Location::Standard);
|
||||||
assert!(!event.data.is_auto_repeating());
|
assert!(!event.data.is_auto_repeating());
|
||||||
|
received_events.modify(|x| *x + 1)
|
||||||
recieved_events.modify(|x| *x + 1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
input {
|
input {
|
||||||
|
@ -330,26 +364,25 @@ fn app(cx: Scope) -> Element {
|
||||||
assert!(event.data.modifiers().is_empty());
|
assert!(event.data.modifiers().is_empty());
|
||||||
assert_eq!(event.data.key().to_string(), "a");
|
assert_eq!(event.data.key().to_string(), "a");
|
||||||
assert_eq!(event.data.code().to_string(), "KeyA");
|
assert_eq!(event.data.code().to_string(), "KeyA");
|
||||||
assert_eq!(event.data.location, 0);
|
assert_eq!(event.data.location(), Location::Standard);
|
||||||
assert!(!event.data.is_auto_repeating());
|
assert!(!event.data.is_auto_repeating());
|
||||||
|
received_events.modify(|x| *x + 1)
|
||||||
recieved_events.modify(|x| *x + 1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
input {
|
input {
|
||||||
id: "focus_in_div",
|
id: "focus_in_div",
|
||||||
onfocusin: move |event| {
|
onfocusin: move |event| {
|
||||||
println!("{:?}", event.data);
|
println!("{:?}", event.data);
|
||||||
recieved_events.modify(|x| *x + 1)
|
received_events.modify(|x| *x + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
input {
|
input {
|
||||||
id: "focus_out_div",
|
id: "focus_out_div",
|
||||||
onfocusout: move |event| {
|
onfocusout: move |event| {
|
||||||
println!("{:?}", event.data);
|
println!("{:?}", event.data);
|
||||||
recieved_events.modify(|x| *x + 1)
|
received_events.modify(|x| *x + 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,8 @@ use dioxus::prelude::*;
|
||||||
use dioxus_desktop::DesktopContext;
|
use dioxus_desktop::DesktopContext;
|
||||||
|
|
||||||
pub(crate) fn check_app_exits(app: Component) {
|
pub(crate) fn check_app_exits(app: Component) {
|
||||||
use dioxus_desktop::tao::window::WindowBuilder;
|
|
||||||
use dioxus_desktop::Config;
|
use dioxus_desktop::Config;
|
||||||
|
use tao::window::WindowBuilder;
|
||||||
// This is a deadman's switch to ensure that the app exits
|
// This is a deadman's switch to ensure that the app exits
|
||||||
let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
|
let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
|
||||||
let should_panic_clone = should_panic.clone();
|
let should_panic_clone = should_panic.clone();
|
||||||
|
@ -16,7 +16,7 @@ pub(crate) fn check_app_exits(app: Component) {
|
||||||
|
|
||||||
dioxus_desktop::launch_cfg(
|
dioxus_desktop::launch_cfg(
|
||||||
app,
|
app,
|
||||||
Config::new().with_window(WindowBuilder::new().with_visible(false)),
|
Config::new().with_window(WindowBuilder::new().with_visible(true)),
|
||||||
);
|
);
|
||||||
|
|
||||||
should_panic.store(false, std::sync::atomic::Ordering::SeqCst);
|
should_panic.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
|
358
packages/desktop/src/app.rs
Normal file
358
packages/desktop/src/app.rs
Normal file
|
@ -0,0 +1,358 @@
|
||||||
|
use crate::{
|
||||||
|
config::{Config, WindowCloseBehaviour},
|
||||||
|
desktop_context::WindowEventHandlers,
|
||||||
|
element::DesktopElement,
|
||||||
|
file_upload::FileDialogRequest,
|
||||||
|
ipc::IpcMessage,
|
||||||
|
ipc::{EventData, UserWindowEvent},
|
||||||
|
query::QueryResult,
|
||||||
|
shortcut::{GlobalHotKeyEvent, ShortcutRegistry},
|
||||||
|
webview::WebviewInstance,
|
||||||
|
};
|
||||||
|
use crossbeam_channel::Receiver;
|
||||||
|
use dioxus_core::{Component, ElementId, VirtualDom};
|
||||||
|
use dioxus_html::{
|
||||||
|
native_bind::NativeFileEngine, FileEngine, HasFormData, HtmlEvent, MountedData,
|
||||||
|
PlatformEventData, SerializedHtmlEventConverter,
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
cell::{Cell, RefCell},
|
||||||
|
collections::HashMap,
|
||||||
|
rc::Rc,
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
use tao::{
|
||||||
|
event::Event,
|
||||||
|
event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget},
|
||||||
|
window::WindowId,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The single top-level object that manages all the running windows, assets, shortcuts, etc
|
||||||
|
pub(crate) struct App<P> {
|
||||||
|
// move the props into a cell so we can pop it out later to create the first window
|
||||||
|
// iOS panics if we create a window before the event loop is started, so we toss them into a cell
|
||||||
|
pub(crate) props: Cell<Option<P>>,
|
||||||
|
pub(crate) cfg: Cell<Option<Config>>,
|
||||||
|
|
||||||
|
// Stuff we need mutable access to
|
||||||
|
pub(crate) root: Component<P>,
|
||||||
|
pub(crate) control_flow: ControlFlow,
|
||||||
|
pub(crate) is_visible_before_start: bool,
|
||||||
|
pub(crate) window_behavior: WindowCloseBehaviour,
|
||||||
|
pub(crate) webviews: HashMap<WindowId, WebviewInstance>,
|
||||||
|
|
||||||
|
/// This single blob of state is shared between all the windows so they have access to the runtime state
|
||||||
|
///
|
||||||
|
/// This includes stuff like the event handlers, shortcuts, etc as well as ways to modify *other* windows
|
||||||
|
pub(crate) shared: Rc<SharedContext>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A bundle of state shared between all the windows, providing a way for us to communicate with running webview.
|
||||||
|
///
|
||||||
|
/// Todo: everything in this struct is wrapped in Rc<>, but we really only need the one top-level refcell
|
||||||
|
pub struct SharedContext {
|
||||||
|
pub(crate) event_handlers: WindowEventHandlers,
|
||||||
|
pub(crate) pending_webviews: RefCell<Vec<WebviewInstance>>,
|
||||||
|
pub(crate) shortcut_manager: ShortcutRegistry,
|
||||||
|
pub(crate) global_hotkey_channel: Receiver<GlobalHotKeyEvent>,
|
||||||
|
pub(crate) proxy: EventLoopProxy<UserWindowEvent>,
|
||||||
|
pub(crate) target: EventLoopWindowTarget<UserWindowEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: 'static> App<P> {
|
||||||
|
pub fn new(cfg: Config, props: P, root: Component<P>) -> (EventLoop<UserWindowEvent>, Self) {
|
||||||
|
let event_loop = EventLoopBuilder::<UserWindowEvent>::with_user_event().build();
|
||||||
|
|
||||||
|
let mut app = Self {
|
||||||
|
root,
|
||||||
|
window_behavior: cfg.last_window_close_behaviour,
|
||||||
|
is_visible_before_start: true,
|
||||||
|
webviews: HashMap::new(),
|
||||||
|
control_flow: ControlFlow::Wait,
|
||||||
|
props: Cell::new(Some(props)),
|
||||||
|
cfg: Cell::new(Some(cfg)),
|
||||||
|
shared: Rc::new(SharedContext {
|
||||||
|
event_handlers: WindowEventHandlers::default(),
|
||||||
|
pending_webviews: Default::default(),
|
||||||
|
shortcut_manager: ShortcutRegistry::new(),
|
||||||
|
global_hotkey_channel: GlobalHotKeyEvent::receiver().clone(),
|
||||||
|
proxy: event_loop.create_proxy(),
|
||||||
|
target: event_loop.clone(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Copy over any assets we find
|
||||||
|
// todo - re-enable this when we have a faster way of copying assets
|
||||||
|
#[cfg(feature = "collect-assets")]
|
||||||
|
crate::collect_assets::copy_assets();
|
||||||
|
|
||||||
|
// Set the event converter
|
||||||
|
dioxus_html::set_event_converter(Box::new(SerializedHtmlEventConverter));
|
||||||
|
|
||||||
|
// Allow hotreloading to work - but only in debug mode
|
||||||
|
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||||
|
app.connect_hotreload();
|
||||||
|
|
||||||
|
(event_loop, app)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(&mut self, window_event: &Event<'_, UserWindowEvent>) {
|
||||||
|
self.control_flow = ControlFlow::Wait;
|
||||||
|
|
||||||
|
self.shared
|
||||||
|
.event_handlers
|
||||||
|
.apply_event(window_event, &self.shared.target);
|
||||||
|
|
||||||
|
if let Ok(event) = self.shared.global_hotkey_channel.try_recv() {
|
||||||
|
self.shared.shortcut_manager.call_handlers(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||||
|
pub fn connect_hotreload(&mut self) {
|
||||||
|
dioxus_hot_reload::connect({
|
||||||
|
let proxy = self.shared.proxy.clone();
|
||||||
|
move |template| {
|
||||||
|
let _ = proxy.send_event(UserWindowEvent(
|
||||||
|
EventData::HotReloadEvent(template),
|
||||||
|
unsafe { WindowId::dummy() },
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_new_window(&mut self) {
|
||||||
|
for handler in self.shared.pending_webviews.borrow_mut().drain(..) {
|
||||||
|
let id = handler.desktop_context.window.id();
|
||||||
|
self.webviews.insert(id, handler);
|
||||||
|
_ = self
|
||||||
|
.shared
|
||||||
|
.proxy
|
||||||
|
.send_event(UserWindowEvent(EventData::Poll, id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_close_requested(&mut self, id: WindowId) {
|
||||||
|
use WindowCloseBehaviour::*;
|
||||||
|
|
||||||
|
match self.window_behavior {
|
||||||
|
LastWindowExitsApp => {
|
||||||
|
self.webviews.remove(&id);
|
||||||
|
if self.webviews.is_empty() {
|
||||||
|
self.control_flow = ControlFlow::Exit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LastWindowHides => {
|
||||||
|
let Some(webview) = self.webviews.get(&id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
hide_app_window(&webview.desktop_context.webview);
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseWindow => {
|
||||||
|
self.webviews.remove(&id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn window_destroyed(&mut self, id: WindowId) {
|
||||||
|
self.webviews.remove(&id);
|
||||||
|
|
||||||
|
if matches!(
|
||||||
|
self.window_behavior,
|
||||||
|
WindowCloseBehaviour::LastWindowExitsApp
|
||||||
|
) && self.webviews.is_empty()
|
||||||
|
{
|
||||||
|
self.control_flow = ControlFlow::Exit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_start_cause_init(&mut self) {
|
||||||
|
let props = self.props.take().unwrap();
|
||||||
|
let cfg = self.cfg.take().unwrap();
|
||||||
|
|
||||||
|
self.is_visible_before_start = cfg.window.window.visible;
|
||||||
|
|
||||||
|
let webview = WebviewInstance::new(
|
||||||
|
cfg,
|
||||||
|
VirtualDom::new_with_props(self.root, props),
|
||||||
|
self.shared.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let id = webview.desktop_context.window.id();
|
||||||
|
self.webviews.insert(id, webview);
|
||||||
|
|
||||||
|
_ = self
|
||||||
|
.shared
|
||||||
|
.proxy
|
||||||
|
.send_event(UserWindowEvent(EventData::Poll, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_browser_open(&mut self, msg: IpcMessage) {
|
||||||
|
if let Some(temp) = msg.params().as_object() {
|
||||||
|
if temp.contains_key("href") {
|
||||||
|
let open = webbrowser::open(temp["href"].as_str().unwrap());
|
||||||
|
if let Err(e) = open {
|
||||||
|
tracing::error!("Open Browser error: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_initialize_msg(&mut self, id: WindowId) {
|
||||||
|
let view = self.webviews.get_mut(&id).unwrap();
|
||||||
|
view.desktop_context.send_edits(view.dom.rebuild());
|
||||||
|
view.desktop_context
|
||||||
|
.window
|
||||||
|
.set_visible(self.is_visible_before_start);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_close_msg(&mut self, id: WindowId) {
|
||||||
|
self.webviews.remove(&id);
|
||||||
|
|
||||||
|
if self.webviews.is_empty() {
|
||||||
|
self.control_flow = ControlFlow::Exit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_query_msg(&mut self, msg: IpcMessage, id: WindowId) {
|
||||||
|
let Ok(result) = serde_json::from_value::<QueryResult>(msg.params()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(view) = self.webviews.get(&id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
view.desktop_context.query.send(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_user_event_msg(&mut self, msg: IpcMessage, id: WindowId) {
|
||||||
|
let parsed_params = serde_json::from_value(msg.params())
|
||||||
|
.map_err(|err| tracing::error!("Error parsing user_event: {:?}", err));
|
||||||
|
|
||||||
|
let Ok(evt) = parsed_params else { return };
|
||||||
|
|
||||||
|
let HtmlEvent {
|
||||||
|
element,
|
||||||
|
name,
|
||||||
|
bubbles,
|
||||||
|
data,
|
||||||
|
} = evt;
|
||||||
|
|
||||||
|
let view = self.webviews.get_mut(&id).unwrap();
|
||||||
|
let query = view.desktop_context.query.clone();
|
||||||
|
|
||||||
|
// check for a mounted event placeholder and replace it with a desktop specific element
|
||||||
|
let as_any = match data {
|
||||||
|
dioxus_html::EventData::Mounted => {
|
||||||
|
let element = DesktopElement::new(element, view.desktop_context.clone(), query);
|
||||||
|
Rc::new(PlatformEventData::new(Box::new(MountedData::new(element))))
|
||||||
|
}
|
||||||
|
_ => data.into_any(),
|
||||||
|
};
|
||||||
|
|
||||||
|
view.dom.handle_event(&name, as_any, element, bubbles);
|
||||||
|
view.desktop_context.send_edits(view.dom.render_immediate());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||||
|
pub fn handle_hot_reload_msg(&mut self, msg: dioxus_hot_reload::HotReloadMsg) {
|
||||||
|
match msg {
|
||||||
|
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => {
|
||||||
|
for webview in self.webviews.values_mut() {
|
||||||
|
webview.dom.replace_template(template);
|
||||||
|
webview.poll_vdom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dioxus_hot_reload::HotReloadMsg::Shutdown => {
|
||||||
|
self.control_flow = ControlFlow::Exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_file_dialog_msg(&mut self, msg: IpcMessage, window: WindowId) {
|
||||||
|
let Ok(file_dialog) = serde_json::from_value::<FileDialogRequest>(msg.params()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
struct DesktopFileUploadForm {
|
||||||
|
files: Arc<NativeFileEngine>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasFormData for DesktopFileUploadForm {
|
||||||
|
fn files(&self) -> Option<Arc<dyn FileEngine>> {
|
||||||
|
Some(self.files.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = ElementId(file_dialog.target);
|
||||||
|
let event_name = &file_dialog.event;
|
||||||
|
let event_bubbles = file_dialog.bubbles;
|
||||||
|
let files = file_dialog.get_file_event();
|
||||||
|
|
||||||
|
let data = Rc::new(PlatformEventData::new(Box::new(DesktopFileUploadForm {
|
||||||
|
files: Arc::new(NativeFileEngine::new(files)),
|
||||||
|
})));
|
||||||
|
|
||||||
|
let view = self.webviews.get_mut(&window).unwrap();
|
||||||
|
|
||||||
|
if event_name == "change&input" {
|
||||||
|
view.dom
|
||||||
|
.handle_event("input", data.clone(), id, event_bubbles);
|
||||||
|
view.dom.handle_event("change", data, id, event_bubbles);
|
||||||
|
} else {
|
||||||
|
view.dom.handle_event(event_name, data, id, event_bubbles);
|
||||||
|
}
|
||||||
|
|
||||||
|
view.desktop_context.send_edits(view.dom.render_immediate());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Poll the virtualdom until it's pending
|
||||||
|
///
|
||||||
|
/// The waker we give it is connected to the event loop, so it will wake up the event loop when it's ready to be polled again
|
||||||
|
///
|
||||||
|
/// All IO is done on the tokio runtime we started earlier
|
||||||
|
pub fn poll_vdom(&mut self, id: WindowId) {
|
||||||
|
let Some(view) = self.webviews.get_mut(&id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
view.poll_vdom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Different hide implementations per platform
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn hide_app_window(window: &wry::WebView) {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
use tao::platform::windows::WindowExtWindows;
|
||||||
|
window.set_visible(false);
|
||||||
|
// window.set_skip_taskbar(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
use tao::platform::unix::WindowExtUnix;
|
||||||
|
window.set_visible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
// window.set_visible(false); has the wrong behaviour on macOS
|
||||||
|
// It will hide the window but not show it again when the user switches
|
||||||
|
// back to the app. `NSApplication::hide:` has the correct behaviour
|
||||||
|
use objc::runtime::Object;
|
||||||
|
use objc::{msg_send, sel, sel_impl};
|
||||||
|
objc::rc::autoreleasepool(|| unsafe {
|
||||||
|
let app: *mut Object = msg_send![objc::class!(NSApplication), sharedApplication];
|
||||||
|
let nil = std::ptr::null_mut::<Object>();
|
||||||
|
let _: () = msg_send![app, hide: nil];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
61
packages/desktop/src/assets.rs
Normal file
61
packages/desktop/src/assets.rs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
use dioxus_core::prelude::{Runtime, RuntimeGuard, ScopeId};
|
||||||
|
use rustc_hash::FxHashMap;
|
||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
use wry::{http::Request, RequestAsyncResponder};
|
||||||
|
|
||||||
|
///
|
||||||
|
pub type AssetRequest = Request<Vec<u8>>;
|
||||||
|
|
||||||
|
pub struct AssetHandler {
|
||||||
|
f: Box<dyn Fn(AssetRequest, RequestAsyncResponder) + 'static>,
|
||||||
|
scope: ScopeId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AssetHandlerRegistry {
|
||||||
|
dom_rt: Rc<Runtime>,
|
||||||
|
handlers: Rc<RefCell<FxHashMap<String, AssetHandler>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssetHandlerRegistry {
|
||||||
|
pub fn new(dom_rt: Rc<Runtime>) -> Self {
|
||||||
|
AssetHandlerRegistry {
|
||||||
|
dom_rt,
|
||||||
|
handlers: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_handler(&self, name: &str) -> bool {
|
||||||
|
self.handlers.borrow().contains_key(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_request(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
request: AssetRequest,
|
||||||
|
responder: RequestAsyncResponder,
|
||||||
|
) {
|
||||||
|
if let Some(handler) = self.handlers.borrow().get(name) {
|
||||||
|
// make sure the runtime is alive for the duration of the handler
|
||||||
|
// We should do this for all the things - not just asset handlers
|
||||||
|
RuntimeGuard::with(self.dom_rt.clone(), Some(handler.scope), || {
|
||||||
|
(handler.f)(request, responder)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_handler(
|
||||||
|
&self,
|
||||||
|
name: String,
|
||||||
|
f: Box<dyn Fn(AssetRequest, RequestAsyncResponder) + 'static>,
|
||||||
|
scope: ScopeId,
|
||||||
|
) {
|
||||||
|
self.handlers
|
||||||
|
.borrow_mut()
|
||||||
|
.insert(name, AssetHandler { f, scope });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_handler(&self, name: &str) -> Option<AssetHandler> {
|
||||||
|
self.handlers.borrow_mut().remove(name)
|
||||||
|
}
|
||||||
|
}
|
60
packages/desktop/src/collect_assets.rs
Normal file
60
packages/desktop/src/collect_assets.rs
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
pub fn copy_assets() {
|
||||||
|
#[cfg(all(
|
||||||
|
debug_assertions,
|
||||||
|
any(
|
||||||
|
target_os = "windows",
|
||||||
|
target_os = "macos",
|
||||||
|
target_os = "linux",
|
||||||
|
target_os = "dragonfly",
|
||||||
|
target_os = "freebsd",
|
||||||
|
target_os = "netbsd",
|
||||||
|
target_os = "openbsd"
|
||||||
|
)
|
||||||
|
))]
|
||||||
|
{
|
||||||
|
// The CLI will copy assets to the current working directory
|
||||||
|
if std::env::var_os("DIOXUS_ACTIVE").is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
use manganis_cli_support::AssetManifest;
|
||||||
|
use manganis_cli_support::AssetManifestExt;
|
||||||
|
use manganis_cli_support::Config;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
let config = Config::current();
|
||||||
|
let asset_location = config.assets_serve_location();
|
||||||
|
let asset_location = PathBuf::from(asset_location);
|
||||||
|
let _ = std::fs::remove_dir_all(&asset_location);
|
||||||
|
|
||||||
|
println!("Finding assets... (Note: if you run a dioxus desktop application with the CLI. This process will be significantly faster.)");
|
||||||
|
let manifest = AssetManifest::load();
|
||||||
|
let has_assets = manifest
|
||||||
|
.packages()
|
||||||
|
.iter()
|
||||||
|
.any(|package| !package.assets().is_empty());
|
||||||
|
|
||||||
|
if has_assets {
|
||||||
|
println!("Copying and optimizing assets...");
|
||||||
|
manifest.copy_static_assets_to(&asset_location).unwrap();
|
||||||
|
println!("Copied assets to {}", asset_location.display());
|
||||||
|
} else {
|
||||||
|
println!("No assets found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(all(
|
||||||
|
debug_assertions,
|
||||||
|
any(
|
||||||
|
target_os = "windows",
|
||||||
|
target_os = "macos",
|
||||||
|
target_os = "linux",
|
||||||
|
target_os = "dragonfly",
|
||||||
|
target_os = "freebsd",
|
||||||
|
target_os = "netbsd",
|
||||||
|
target_os = "openbsd"
|
||||||
|
)
|
||||||
|
)))]
|
||||||
|
{
|
||||||
|
println!(
|
||||||
|
"Skipping assets in release mode. You compile assets with the dioxus-cli in release mode"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,16 +1,13 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use wry::application::window::Icon;
|
use dioxus_core::prelude::Component;
|
||||||
|
use tao::window::{Icon, WindowBuilder, WindowId};
|
||||||
use wry::{
|
use wry::{
|
||||||
application::window::{Window, WindowBuilder},
|
|
||||||
http::{Request as HttpRequest, Response as HttpResponse},
|
http::{Request as HttpRequest, Response as HttpResponse},
|
||||||
webview::FileDropEvent,
|
FileDropEvent,
|
||||||
Result as WryResult,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView);
|
|
||||||
|
|
||||||
/// The behaviour of the application when the last window is closed.
|
/// The behaviour of the application when the last window is closed.
|
||||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||||
pub enum WindowCloseBehaviour {
|
pub enum WindowCloseBehaviour {
|
||||||
|
@ -36,13 +33,14 @@ pub struct Config {
|
||||||
pub(crate) root_name: String,
|
pub(crate) root_name: String,
|
||||||
pub(crate) background_color: Option<(u8, u8, u8, u8)>,
|
pub(crate) background_color: Option<(u8, u8, u8, u8)>,
|
||||||
pub(crate) last_window_close_behaviour: WindowCloseBehaviour,
|
pub(crate) last_window_close_behaviour: WindowCloseBehaviour,
|
||||||
|
pub(crate) enable_default_menu_bar: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
type DropHandler = Box<dyn Fn(&Window, FileDropEvent) -> bool>;
|
type DropHandler = Box<dyn Fn(WindowId, FileDropEvent) -> bool>;
|
||||||
|
|
||||||
pub(crate) type WryProtocol = (
|
pub(crate) type WryProtocol = (
|
||||||
String,
|
String,
|
||||||
Box<dyn Fn(&HttpRequest<Vec<u8>>) -> WryResult<HttpResponse<Cow<'static, [u8]>>> + 'static>,
|
Box<dyn Fn(HttpRequest<Vec<u8>>) -> HttpResponse<Cow<'static, [u8]>> + 'static>,
|
||||||
);
|
);
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
@ -65,9 +63,32 @@ impl Config {
|
||||||
root_name: "main".to_string(),
|
root_name: "main".to_string(),
|
||||||
background_color: None,
|
background_color: None,
|
||||||
last_window_close_behaviour: WindowCloseBehaviour::LastWindowExitsApp,
|
last_window_close_behaviour: WindowCloseBehaviour::LastWindowExitsApp,
|
||||||
|
enable_default_menu_bar: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Launch a Dioxus app using the given component and config
|
||||||
|
///
|
||||||
|
/// See the [`crate::launch::launch`] function for more details.
|
||||||
|
pub fn launch(self, root: Component<()>) {
|
||||||
|
crate::launch::launch_cfg(root, self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Launch a Dioxus app using the given component, config, and props
|
||||||
|
///
|
||||||
|
/// See the [`crate::launch::launch_with_props`] function for more details.
|
||||||
|
pub fn launch_with_props<P: 'static>(self, root: Component<P>, props: P) {
|
||||||
|
crate::launch::launch_with_props(root, props, self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set whether the default menu bar should be enabled.
|
||||||
|
///
|
||||||
|
/// > Note: `enable` is `true` by default. To disable the default menu bar pass `false`.
|
||||||
|
pub fn with_default_menu_bar(mut self, enable: bool) -> Self {
|
||||||
|
self.enable_default_menu_bar = enable;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// set the directory from which assets will be searched in release mode
|
/// set the directory from which assets will be searched in release mode
|
||||||
pub fn with_resource_directory(mut self, path: impl Into<PathBuf>) -> Self {
|
pub fn with_resource_directory(mut self, path: impl Into<PathBuf>) -> Self {
|
||||||
self.resource_dir = Some(path.into());
|
self.resource_dir = Some(path.into());
|
||||||
|
@ -111,7 +132,7 @@ impl Config {
|
||||||
/// Set a file drop handler
|
/// Set a file drop handler
|
||||||
pub fn with_file_drop_handler(
|
pub fn with_file_drop_handler(
|
||||||
mut self,
|
mut self,
|
||||||
handler: impl Fn(&Window, FileDropEvent) -> bool + 'static,
|
handler: impl Fn(WindowId, FileDropEvent) -> bool + 'static,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.file_drop_handler = Some(Box::new(handler));
|
self.file_drop_handler = Some(Box::new(handler));
|
||||||
self
|
self
|
||||||
|
@ -120,7 +141,7 @@ impl Config {
|
||||||
/// Set a custom protocol
|
/// Set a custom protocol
|
||||||
pub fn with_custom_protocol<F>(mut self, name: String, handler: F) -> Self
|
pub fn with_custom_protocol<F>(mut self, name: String, handler: F) -> Self
|
||||||
where
|
where
|
||||||
F: Fn(&HttpRequest<Vec<u8>>) -> WryResult<HttpResponse<Cow<'static, [u8]>>> + 'static,
|
F: Fn(HttpRequest<Vec<u8>>) -> HttpResponse<Cow<'static, [u8]>> + 'static,
|
||||||
{
|
{
|
||||||
self.protocols.push((name, Box::new(handler)));
|
self.protocols.push((name, Box::new(handler)));
|
||||||
self
|
self
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue