diff --git a/.github/workflows/docs stable.yml b/.github/workflows/docs stable.yml
index 642216f0f5..523a240670 100644
--- a/.github/workflows/docs stable.yml
+++ b/.github/workflows/docs stable.yml
@@ -33,7 +33,7 @@ jobs:
# cd fermi && mdbook build -d ../nightly/fermi && cd ..
- name: Deploy π
- uses: JamesIves/github-pages-deploy-action@v4.4.3
+ uses: JamesIves/github-pages-deploy-action@v4.5.0
with:
branch: gh-pages # The branch the action should deploy to.
folder: docs/nightly # The folder the action should deploy.
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 88542aea8e..7701209c19 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -39,7 +39,7 @@ jobs:
# cd fermi && mdbook build -d ../nightly/fermi && cd ..
- name: Deploy π
- uses: JamesIves/github-pages-deploy-action@v4.4.3
+ uses: JamesIves/github-pages-deploy-action@v4.5.0
with:
branch: gh-pages # The branch the action should deploy to.
folder: docs/nightly # The folder the action should deploy.
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 037525557d..1b1f1ed7b2 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -124,8 +124,6 @@ jobs:
}
steps:
- - uses: actions/checkout@v4
-
- name: install stable
uses: dtolnay/rust-toolchain@master
with:
@@ -141,6 +139,11 @@ jobs:
workspaces: core -> ../target
save-if: ${{ matrix.features.key == 'all' }}
+ - name: Install rustfmt
+ run: rustup component add rustfmt
+
+ - uses: actions/checkout@v4
+
- name: test
run: |
${{ env.RUST_CARGO_COMMAND }} ${{ matrix.platform.command }} ${{ matrix.platform.args }} --target ${{ matrix.platform.target }}
diff --git a/.github/workflows/miri.yml b/.github/workflows/miri.yml
index 4a467b9b28..f9ad3855f7 100644
--- a/.github/workflows/miri.yml
+++ b/.github/workflows/miri.yml
@@ -86,8 +86,7 @@ jobs:
# working-directory: tokio
env:
- # todo: disable memory leaks ignore
- MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance -Zmiri-retag-fields -Zmiri-ignore-leaks
+ MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance -Zmiri-retag-fields
PROPTEST_CASES: 10
# Cache the global cargo directory, but NOT the local `target` directory which
diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
index 500d415642..7b23c61115 100644
--- a/.github/workflows/playwright.yml
+++ b/.github/workflows/playwright.yml
@@ -20,7 +20,7 @@ jobs:
steps:
# Do our best to cache the toolchain and node install steps
- uses: actions/checkout@v4
- - uses: actions/setup-node@v3
+ - uses: actions/setup-node@v4
with:
node-version: 16
- name: Install Rust
@@ -43,7 +43,7 @@ jobs:
# args: --path packages/cli
- name: Run Playwright tests
run: npx playwright test
- - uses: actions/upload-artifact@v3
+ - uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
diff --git a/.gitignore b/.gitignore
index b8025753ed..364e8d0fb5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@
/dist
Cargo.lock
.DS_Store
+/examples/assets/test_video.mp4
.vscode/*
!.vscode/settings.json
diff --git a/Cargo.toml b/Cargo.toml
index a78f60d3e9..29ae2d30dd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -50,7 +50,7 @@ members = [
exclude = ["examples/mobile_demo"]
[workspace.package]
-version = "0.4.2"
+version = "0.4.3"
# dependencies that are shared across packages
[workspace.dependencies]
@@ -77,7 +77,7 @@ dioxus-native-core = { path = "packages/native-core", version = "0.4.0" }
dioxus-native-core-macro = { path = "packages/native-core-macro", version = "0.4.0" }
rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.4.0" }
dioxus-signals = { path = "packages/signals" }
-generational-box = { path = "packages/generational-box" }
+generational-box = { path = "packages/generational-box", version = "0.4.3" }
dioxus-hot-reload = { path = "packages/hot-reload", version = "0.4.0" }
dioxus-fullstack = { path = "packages/fullstack", version = "0.4.1" }
dioxus_server_macro = { path = "packages/server-macro", version = "0.4.1" }
@@ -88,7 +88,7 @@ slab = "0.4.2"
futures-channel = "0.3.21"
futures-util = { version = "0.3", default-features = false }
rustc-hash = "1.1.0"
-wasm-bindgen = "0.2.87"
+wasm-bindgen = "0.2.88"
html_parser = "0.7.0"
thiserror = "1.0.40"
prettyplease = { package = "prettier-please", version = "0.2", features = [
@@ -99,7 +99,7 @@ prettyplease = { package = "prettier-please", version = "0.2", features = [
# It is not meant to be published, but is used so "cargo run --example XYZ" works properly
[package]
name = "dioxus-examples"
-version = "0.0.0"
+version = "0.4.3"
authors = ["Jonathan Kelley"]
edition = "2021"
description = "Top level crate for the Dioxus repository"
@@ -133,3 +133,4 @@ fern = { version = "0.6.0", features = ["colored"] }
env_logger = "0.10.0"
simple_logger = "4.0.0"
thiserror = { workspace = true }
+http-range = "0.1.5"
diff --git a/Makefile.toml b/Makefile.toml
index 6f331b98f1..1bfb022177 100644
--- a/Makefile.toml
+++ b/Makefile.toml
@@ -24,12 +24,64 @@ script = [
]
script_runner = "@duckscript"
+[tasks.format]
+command = "cargo"
+args = ["fmt", "--all"]
+
+[tasks.check]
+command = "cargo"
+args = ["check", "--workspace", "--examples", "--tests"]
+
+[tasks.clippy]
+command = "cargo"
+args = [
+ "clippy",
+ "--workspace",
+ "--examples",
+ "--tests",
+ "--",
+ "-D",
+ "warnings",
+]
+
+[tasks.tidy]
+category = "Formatting"
+dependencies = ["format", "check", "clippy"]
+description = "Format and Check workspace"
+
+[tasks.install-miri]
+toolchain = "nightly"
+install_crate = { rustup_component_name = "miri", binary = "cargo +nightly miri", test_arg = "--help" }
+private = true
+
+[tasks.miri-native]
+command = "cargo"
+toolchain = "nightly"
+dependencies = ["install-miri"]
+args = [
+ "miri",
+ "test",
+ "--package",
+ "dioxus-native-core",
+ "--test",
+ "miri_native",
+]
+
+[tasks.miri-stress]
+command = "cargo"
+toolchain = "nightly"
+dependencies = ["install-miri"]
+args = ["miri", "test", "--package", "dioxus-core", "--test", "miri_stress"]
+
+[tasks.miri]
+dependencies = ["miri-native", "miri-stress"]
+
[tasks.tests]
category = "Testing"
dependencies = ["tests-setup"]
description = "Run all tests"
-env = {CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = ["**/examples/*"]}
-run_task = {name = ["test-flow", "test-with-browser"], fork = true}
+env = { CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = ["**/examples/*"] }
+run_task = { name = ["test-flow", "test-with-browser"], fork = true }
[tasks.build]
command = "cargo"
@@ -42,10 +94,24 @@ private = true
[tasks.test]
dependencies = ["build"]
command = "cargo"
-args = ["test", "--lib", "--bins", "--tests", "--examples", "--workspace", "--exclude", "dioxus-router", "--exclude", "dioxus-desktop"]
+args = [
+ "test",
+ "--lib",
+ "--bins",
+ "--tests",
+ "--examples",
+ "--workspace",
+ "--exclude",
+ "dioxus-router",
+ "--exclude",
+ "dioxus-desktop",
+]
private = true
[tasks.test-with-browser]
-env = { CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS = ["**/packages/router", "**/packages/desktop"] }
+env = { CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS = [
+ "**/packages/router",
+ "**/packages/desktop",
+] }
private = true
workspace = true
diff --git a/README.md b/README.md
index d1ec98019f..65cd082637 100644
--- a/README.md
+++ b/README.md
@@ -161,7 +161,7 @@ So... Dioxus is great, but why won't it work for me?
## Contributing
- Check out the website [section on contributing](https://dioxuslabs.com/learn/0.4/contributing).
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
-- Join the discord and ask questions!
+- [Join](https://discord.gg/XgGxMSkvUM) the discord and ask questions!
diff --git a/examples/all_events.rs b/examples/all_events.rs
index 594a736d50..3f57ec7478 100644
--- a/examples/all_events.rs
+++ b/examples/all_events.rs
@@ -53,8 +53,7 @@ fn app(cx: Scope) -> Element {
};
cx.render(rsx! (
- div {
- style: "{CONTAINER_STYLE}",
+ div { style: "{CONTAINER_STYLE}",
div {
style: "{RECT_STYLE}",
// focusing is necessary to catch keyboard events
@@ -62,7 +61,7 @@ fn app(cx: Scope) -> Element {
onmousemove: move |event| log_event(Event::MouseMove(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)),
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"
}
- div {
- events.read().iter().map(|event| rsx!( div { "{event:?}" } ))
- },
- },
+ div { events.read().iter().map(|event| rsx!( div { "{event:?}" } )) }
+ }
))
}
diff --git a/examples/calculator.rs b/examples/calculator.rs
index bc7f13459c..648ccfbc33 100644
--- a/examples/calculator.rs
+++ b/examples/calculator.rs
@@ -62,6 +62,7 @@ fn app(cx: Scope) -> Element {
div { id: "wrapper",
div { class: "app",
div { class: "calculator",
+ tabindex: "0",
onkeydown: handle_key_down_event,
div { class: "calculator-display", val.to_string() }
div { class: "calculator-keypad",
diff --git a/examples/compose.rs b/examples/compose.rs
index f544b5a9cb..6a303a701b 100644
--- a/examples/compose.rs
+++ b/examples/compose.rs
@@ -25,14 +25,8 @@ fn app(cx: Scope) -> Element {
button {
onclick: move |_| {
- let dom = VirtualDom::new_with_props(compose, ComposeProps {
- app_tx: tx.clone()
- });
-
- // 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.
- dioxus_desktop::window().new_window(dom, Default::default());
+ let dom = VirtualDom::new_with_props(compose, ComposeProps { app_tx: tx.clone() });
+ window.new_window(dom, Default::default());
},
"Click to compose a new email"
}
diff --git a/examples/dynamic_asset.rs b/examples/dynamic_asset.rs
new file mode 100644
index 0000000000..1c004e0157
--- /dev/null
+++ b/examples/dynamic_asset.rs
@@ -0,0 +1,29 @@
+use dioxus::prelude::*;
+use dioxus_desktop::wry::http::Response;
+use dioxus_desktop::{use_asset_handler, AssetRequest};
+use std::path::Path;
+
+fn main() {
+ dioxus_desktop::launch(app);
+}
+
+fn app(cx: Scope) -> Element {
+ use_asset_handler(cx, |request: &AssetRequest| {
+ let path = request.path().to_path_buf();
+ async move {
+ if path != Path::new("logo.png") {
+ return None;
+ }
+ let image_data: &[u8] = include_bytes!("./assets/logo.png");
+ Some(Response::new(image_data.into()))
+ }
+ });
+
+ cx.render(rsx! {
+ div {
+ img {
+ src: "logo.png"
+ }
+ }
+ })
+}
diff --git a/examples/mobile_demo/Cargo.toml b/examples/mobile_demo/Cargo.toml
index d22269a0c0..a90ff4f369 100644
--- a/examples/mobile_demo/Cargo.toml
+++ b/examples/mobile_demo/Cargo.toml
@@ -35,7 +35,7 @@ frameworks = ["WebKit"]
[dependencies]
anyhow = "1.0.56"
log = "0.4.11"
-wry = "0.28.0"
+wry = "0.34.0"
dioxus = { path = "../../packages/dioxus" }
dioxus-desktop = { path = "../../packages/desktop", features = [
"tokio_runtime",
diff --git a/examples/openid_connect_demo/Cargo.toml b/examples/openid_connect_demo/Cargo.toml
index 4c5f47061c..2a1abe1730 100644
--- a/examples/openid_connect_demo/Cargo.toml
+++ b/examples/openid_connect_demo/Cargo.toml
@@ -2,6 +2,7 @@
name = "openid_auth_demo"
version = "0.1.0"
edition = "2021"
+publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
diff --git a/examples/openid_connect_demo/src/errors.rs b/examples/openid_connect_demo/src/errors.rs
index 2b7e3f1451..30005c1d87 100644
--- a/examples/openid_connect_demo/src/errors.rs
+++ b/examples/openid_connect_demo/src/errors.rs
@@ -1,20 +1,20 @@
-use openidconnect::{core::CoreErrorResponseType, url, RequestTokenError, StandardErrorResponse};
-use thiserror::Error;
-
-#[derive(Error, Debug)]
-pub enum Error {
- #[error("Discovery error: {0}")]
- OpenIdConnect(
- #[from] openidconnect::DiscoveryError>,
- ),
- #[error("Parsing error: {0}")]
- Parse(#[from] url::ParseError),
- #[error("Request token error: {0}")]
- RequestToken(
- #[from]
- RequestTokenError<
- openidconnect::reqwest::Error,
- StandardErrorResponse,
- >,
- ),
-}
+use openidconnect::{core::CoreErrorResponseType, url, RequestTokenError, StandardErrorResponse};
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum Error {
+ #[error("Discovery error: {0}")]
+ OpenIdConnect(
+ #[from] openidconnect::DiscoveryError>,
+ ),
+ #[error("Parsing error: {0}")]
+ Parse(#[from] url::ParseError),
+ #[error("Request token error: {0}")]
+ RequestToken(
+ #[from]
+ RequestTokenError<
+ openidconnect::reqwest::Error,
+ StandardErrorResponse,
+ >,
+ ),
+}
diff --git a/examples/optional_props.rs b/examples/optional_props.rs
index c5652a2e7b..8108d437d9 100644
--- a/examples/optional_props.rs
+++ b/examples/optional_props.rs
@@ -16,8 +16,20 @@ fn app(cx: Scope) -> Element {
a: "asd".to_string(),
c: "asd".to_string(),
d: Some("asd".to_string()),
+ e: Some("asd".to_string()),
+ }
+ Button {
+ a: "asd".to_string(),
+ b: "asd".to_string(),
+ c: "asd".to_string(),
+ d: Some("asd".to_string()),
e: "asd".to_string(),
}
+ Button {
+ a: "asd".to_string(),
+ c: "asd".to_string(),
+ d: Some("asd".to_string()),
+ }
})
}
diff --git a/examples/query_segments_demo/Cargo.toml b/examples/query_segments_demo/Cargo.toml
index 4a7d784c78..ee9f49c70b 100644
--- a/examples/query_segments_demo/Cargo.toml
+++ b/examples/query_segments_demo/Cargo.toml
@@ -2,6 +2,7 @@
name = "query_segments_demo"
version = "0.1.0"
edition = "2021"
+publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs
index f9ecb49392..317288d558 100644
--- a/examples/rsx_usage.rs
+++ b/examples/rsx_usage.rs
@@ -53,6 +53,7 @@ fn App(cx: Scope) -> Element {
let formatting = "formatting!";
let formatting_tuple = ("a", "b");
let lazy_fmt = format_args!("lazily formatted text");
+ let asd = 123;
cx.render(rsx! {
div {
// Elements
@@ -80,6 +81,10 @@ fn App(cx: Scope) -> Element {
// pass simple rust expressions in
class: lazy_fmt,
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 {
class: {
const WORD: &str = "expressions";
diff --git a/examples/tailwind/Cargo.toml b/examples/tailwind/Cargo.toml
index 5c2fbe6664..26ac202f10 100644
--- a/examples/tailwind/Cargo.toml
+++ b/examples/tailwind/Cargo.toml
@@ -18,4 +18,4 @@ dioxus = { path = "../../packages/dioxus" }
dioxus-desktop = { path = "../../packages/desktop" }
[target.'cfg(target_arch = "wasm32")'.dependencies]
-dioxus-web = { path = "../../packages/web" }
\ No newline at end of file
+dioxus-web = { path = "../../packages/web" }
diff --git a/examples/tailwind/src/main.rs b/examples/tailwind/src/main.rs
index 2dbb183c9f..8a49071db2 100644
--- a/examples/tailwind/src/main.rs
+++ b/examples/tailwind/src/main.rs
@@ -14,9 +14,13 @@ fn main() {
}
pub fn app(cx: Scope) -> Element {
+ let grey_background = true;
cx.render(rsx!(
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",
a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0",
StacksIcon {}
diff --git a/examples/todomvc.rs b/examples/todomvc.rs
index 9ec2dc2289..0f3fada716 100644
--- a/examples/todomvc.rs
+++ b/examples/todomvc.rs
@@ -48,11 +48,8 @@ pub fn app(cx: Scope<()>) -> Element {
cx.render(rsx! {
section { class: "todoapp",
style { include_str!("./assets/todomvc.css") }
- TodoHeader {
- todos: todos,
- }
- section {
- class: "main",
+ TodoHeader { todos: todos }
+ section { class: "main",
if !todos.is_empty() {
rsx! {
input {
@@ -103,31 +100,34 @@ pub fn TodoHeader<'a>(cx: Scope<'a, TodoHeaderProps<'a>>) -> Element {
cx.render(rsx! {
header { class: "header",
- h1 {"todos"}
- input {
- class: "new-todo",
- placeholder: "What needs to be done?",
- value: "{draft}",
- autofocus: "true",
- oninput: move |evt| {
- draft.set(evt.value.clone());
- },
- onkeydown: move |evt| {
- if evt.key() == Key::Enter && !draft.is_empty() {
- cx.props.todos.make_mut().insert(
- **todo_id,
- TodoItem {
- id: **todo_id,
- checked: false,
- contents: draft.to_string(),
- },
- );
- *todo_id.make_mut() += 1;
- draft.set("".to_string());
+ h1 { "todos" }
+ input {
+ class: "new-todo",
+ placeholder: "What needs to be done?",
+ value: "{draft}",
+ autofocus: "true",
+ oninput: move |evt| {
+ draft.set(evt.value.clone());
+ },
+ onkeydown: move |evt| {
+ if evt.key() == Key::Enter && !draft.is_empty() {
+ cx.props
+ .todos
+ .make_mut()
+ .insert(
+ **todo_id,
+ TodoItem {
+ id: **todo_id,
+ checked: false,
+ contents: draft.to_string(),
+ },
+ );
+ *todo_id.make_mut() += 1;
+ draft.set("".to_string());
+ }
}
}
}
- }
})
}
@@ -146,8 +146,7 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
let editing = if **is_editing { "editing" } else { "" };
cx.render(rsx!{
- li {
- class: "{completed} {editing}",
+ li { class: "{completed} {editing}",
div { class: "view",
input {
class: "toggle",
@@ -160,14 +159,16 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
}
label {
r#for: "cbg-{todo.id}",
- ondblclick: move |_| is_editing.set(true),
+ ondoubleclick: move |_| is_editing.set(true),
prevent_default: "onclick",
"{todo.contents}"
}
button {
class: "destroy",
- onclick: move |_| { cx.props.todos.make_mut().remove(&todo.id); },
- prevent_default: "onclick",
+ onclick: move |_| {
+ cx.props.todos.make_mut().remove(&todo.id);
+ },
+ prevent_default: "onclick"
}
}
is_editing.then(|| rsx!{
@@ -213,15 +214,15 @@ pub fn ListFooter<'a>(cx: Scope<'a, ListFooterProps<'a>>) -> Element {
cx.render(rsx! {
footer { class: "footer",
span { class: "todo-count",
- strong {"{active_todo_count} "}
- span {"{active_todo_text} left"}
+ strong { "{active_todo_count} " }
+ span { "{active_todo_text} left" }
}
ul { class: "filters",
- for (state, state_text, url) in [
- (FilterState::All, "All", "#/"),
- (FilterState::Active, "Active", "#/active"),
- (FilterState::Completed, "Completed", "#/completed"),
- ] {
+ for (state , state_text , url) in [
+ (FilterState::All, "All", "#/"),
+ (FilterState::Active, "Active", "#/active"),
+ (FilterState::Completed, "Completed", "#/completed"),
+] {
li {
a {
href: url,
@@ -250,8 +251,14 @@ pub fn PageFooter(cx: Scope) -> Element {
cx.render(rsx! {
footer { class: "info",
p { "Double-click to edit a todo" }
- p { "Created by ", a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }}
- p { "Part of ", a { href: "http://todomvc.com", "TodoMVC" }}
+ p {
+ "Created by "
+ a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }
+ }
+ p {
+ "Part of "
+ a { href: "http://todomvc.com", "TodoMVC" }
+ }
}
})
}
diff --git a/examples/video_stream.rs b/examples/video_stream.rs
new file mode 100644
index 0000000000..f495299f42
--- /dev/null
+++ b/examples/video_stream.rs
@@ -0,0 +1,184 @@
+use dioxus::prelude::*;
+use dioxus_desktop::wry::http;
+use dioxus_desktop::wry::http::Response;
+use dioxus_desktop::{use_asset_handler, AssetRequest};
+use http::{header::*, response::Builder as ResponseBuilder, status::StatusCode};
+use std::borrow::Cow;
+use std::{io::SeekFrom, path::PathBuf};
+use tokio::io::AsyncReadExt;
+use tokio::io::AsyncSeekExt;
+use tokio::io::AsyncWriteExt;
+
+const VIDEO_PATH: &str = "./examples/assets/test_video.mp4";
+
+fn main() {
+ let video_file = PathBuf::from(VIDEO_PATH);
+ if !video_file.exists() {
+ tokio::runtime::Runtime::new()
+ .unwrap()
+ .block_on(async move {
+ println!("Downloading video file...");
+ let video_url =
+ "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
+ let mut response = reqwest::get(video_url).await.unwrap();
+ let mut file = tokio::fs::File::create(&video_file).await.unwrap();
+ while let Some(chunk) = response.chunk().await.unwrap() {
+ file.write_all(&chunk).await.unwrap();
+ }
+ });
+ }
+ dioxus_desktop::launch(app);
+}
+
+fn app(cx: Scope) -> Element {
+ use_asset_handler(cx, move |request: &AssetRequest| {
+ let request = request.clone();
+ async move {
+ let video_file = PathBuf::from(VIDEO_PATH);
+ let mut file = tokio::fs::File::open(&video_file).await.unwrap();
+ let response: Option>> =
+ match get_stream_response(&mut file, &request).await {
+ Ok(response) => Some(response.map(Cow::Owned)),
+ Err(err) => {
+ eprintln!("Error: {}", err);
+ None
+ }
+ };
+ response
+ }
+ });
+
+ render! {
+ div { video { src: "test_video.mp4", autoplay: true, controls: true, width: 640, height: 480 } }
+ }
+}
+
+async fn get_stream_response(
+ asset: &mut (impl tokio::io::AsyncSeek + tokio::io::AsyncRead + Unpin + Send + Sync),
+ request: &AssetRequest,
+) -> Result>, Box> {
+ // 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 , example: 0-499
+ .map(|r| (r.start, r.start + r.length - 1))
+ .collect::>()
+ } 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::>();
+
+ let boundary = format!("{:x}", rand::random::());
+ 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)
+}
diff --git a/packages/autofmt/src/buffer.rs b/packages/autofmt/src/buffer.rs
index fea5a4a3fb..f19a7d8dce 100644
--- a/packages/autofmt/src/buffer.rs
+++ b/packages/autofmt/src/buffer.rs
@@ -8,13 +8,14 @@ use std::fmt::{Result, Write};
use dioxus_rsx::IfmtInput;
-use crate::write_ifmt;
+use crate::{indent::IndentOptions, write_ifmt};
/// The output buffer that tracks indent and string
#[derive(Debug, Default)]
pub struct Buffer {
pub buf: String,
- pub indent: usize,
+ pub indent_level: usize,
+ pub indent: IndentOptions,
}
impl Buffer {
@@ -31,16 +32,16 @@ impl Buffer {
}
pub fn tab(&mut self) -> Result {
- self.write_tabs(self.indent)
+ self.write_tabs(self.indent_level)
}
pub fn indented_tab(&mut self) -> Result {
- self.write_tabs(self.indent + 1)
+ self.write_tabs(self.indent_level + 1)
}
pub fn write_tabs(&mut self, num: usize) -> std::fmt::Result {
for _ in 0..num {
- write!(self.buf, " ")?
+ write!(self.buf, "{}", self.indent.indent_str())?
}
Ok(())
}
diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs
index dbc336401d..ebbce75386 100644
--- a/packages/autofmt/src/element.rs
+++ b/packages/autofmt/src/element.rs
@@ -49,6 +49,7 @@ impl Writer<'_> {
attributes,
children,
brace,
+ ..
} = el;
/*
@@ -66,7 +67,7 @@ impl Writer<'_> {
// check if we have a lot of attributes
let attr_len = self.is_short_attrs(attributes);
- let is_short_attr_list = (attr_len + self.out.indent * 4) < 80;
+ let is_short_attr_list = (attr_len + self.out.indent_level * 4) < 80;
let children_len = self.is_short_children(children);
let is_small_children = children_len.is_some();
@@ -86,7 +87,7 @@ impl Writer<'_> {
// if we have few children and few attributes, make it a one-liner
if is_short_attr_list && is_small_children {
- if children_len.unwrap() + attr_len + self.out.indent * 4 < 100 {
+ if children_len.unwrap() + attr_len + self.out.indent_level * 4 < 100 {
opt_level = ShortOptimization::Oneliner;
} else {
opt_level = ShortOptimization::PropsOnTop;
@@ -185,11 +186,11 @@ impl Writer<'_> {
}
while let Some(attr) = attr_iter.next() {
- self.out.indent += 1;
+ self.out.indent_level += 1;
if !sameline {
self.write_comments(attr.attr.start())?;
}
- self.out.indent -= 1;
+ self.out.indent_level -= 1;
if !sameline {
self.out.indented_tabbed_line()?;
@@ -209,12 +210,34 @@ impl Writer<'_> {
Ok(())
}
- fn write_attribute(&mut self, attr: &ElementAttrNamed) -> Result {
- match &attr.attr {
- ElementAttr::AttrText { name, value } => {
- write!(self.out, "{name}: {value}", value = ifmt_to_string(value))?;
+ fn write_attribute_name(&mut self, attr: &ElementAttrName) -> Result {
+ match attr {
+ ElementAttrName::BuiltIn(name) => {
+ write!(self.out, "{}", name)?;
+ }
+ ElementAttrName::Custom(name) => {
+ write!(self.out, "{}", name.to_token_stream())?;
}
- ElementAttr::AttrExpression { name, value } => {
+ }
+
+ 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 mut lines = out.split('\n').peekable();
let first = lines.next().unwrap();
@@ -222,9 +245,9 @@ impl Writer<'_> {
// a one-liner for whatever reason
// Does not need a new line
if lines.peek().is_none() {
- write!(self.out, "{name}: {first}")?;
+ write!(self.out, "{first}")?;
} else {
- writeln!(self.out, "{name}: {first}")?;
+ writeln!(self.out, "{first}")?;
while let Some(line) = lines.next() {
self.out.indented_tab()?;
@@ -237,22 +260,7 @@ impl Writer<'_> {
}
}
}
-
- 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 } => {
+ ElementAttrValue::EventTokens(tokens) => {
let out = self.retrieve_formatted_expr(tokens).to_string();
let mut lines = out.split('\n').peekable();
@@ -261,9 +269,9 @@ impl Writer<'_> {
// a one-liner for whatever reason
// Does not need a new line
if lines.peek().is_none() {
- write!(self.out, "{name}: {first}")?;
+ write!(self.out, "{first}")?;
} else {
- writeln!(self.out, "{name}: {first}")?;
+ writeln!(self.out, "{first}")?;
while let Some(line) = lines.next() {
self.out.indented_tab()?;
@@ -281,6 +289,14 @@ impl Writer<'_> {
Ok(())
}
+ fn write_attribute(&mut self, attr: &ElementAttrNamed) -> Result {
+ self.write_attribute_name(&attr.attr.name)?;
+ write!(self.out, ": ")?;
+ self.write_attribute_value(&attr.attr.value)?;
+
+ Ok(())
+ }
+
// make sure the comments are actually relevant to this element.
// test by making sure this element is the primary element on this line
pub fn current_span_is_primary(&self, location: Span) -> bool {
@@ -398,14 +414,14 @@ impl Writer<'_> {
for idx in start.line..end.line {
let line = &self.src[idx];
if line.trim().starts_with("//") {
- for _ in 0..self.out.indent + 1 {
+ for _ in 0..self.out.indent_level + 1 {
write!(self.out, " ")?
}
writeln!(self.out, "{}", line.trim()).unwrap();
}
}
- for _ in 0..self.out.indent {
+ for _ in 0..self.out.indent_level {
write!(self.out, " ")?
}
diff --git a/packages/autofmt/src/expr.rs b/packages/autofmt/src/expr.rs
index b0257f8c33..b7a93a4fc3 100644
--- a/packages/autofmt/src/expr.rs
+++ b/packages/autofmt/src/expr.rs
@@ -29,7 +29,7 @@ impl Writer<'_> {
let first_line = &self.src[start.line - 1];
write!(self.out, "{}", &first_line[start.column - 1..].trim_start())?;
- let prev_block_indent_level = crate::leading_whitespaces(first_line) / 4;
+ let prev_block_indent_level = self.out.indent.count_indents(first_line);
for (id, line) in self.src[start.line..end.line].iter().enumerate() {
writeln!(self.out)?;
@@ -43,9 +43,9 @@ impl Writer<'_> {
};
// trim the leading whitespace
- let previous_indent = crate::leading_whitespaces(line) / 4;
+ let previous_indent = self.out.indent.count_indents(line);
let offset = previous_indent.saturating_sub(prev_block_indent_level);
- let required_indent = self.out.indent + offset;
+ let required_indent = self.out.indent_level + offset;
self.out.write_tabs(required_indent)?;
let line = line.trim_start();
diff --git a/packages/autofmt/src/indent.rs b/packages/autofmt/src/indent.rs
new file mode 100644
index 0000000000..2cce7cf1e7
--- /dev/null
+++ b/packages/autofmt/src/indent.rs
@@ -0,0 +1,108 @@
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+pub enum IndentType {
+ Spaces,
+ Tabs,
+}
+
+#[derive(Debug, Clone)]
+pub struct IndentOptions {
+ width: usize,
+ indent_string: String,
+}
+
+impl IndentOptions {
+ pub fn new(typ: IndentType, width: usize) -> Self {
+ assert_ne!(width, 0, "Cannot have an indent width of 0");
+ Self {
+ width,
+ indent_string: match typ {
+ IndentType::Tabs => "\t".into(),
+ IndentType::Spaces => " ".repeat(width),
+ },
+ }
+ }
+
+ /// Gets a string containing one indent worth of whitespace
+ pub fn indent_str(&self) -> &str {
+ &self.indent_string
+ }
+
+ /// Computes the line length in characters, counting tabs as the indent width.
+ pub fn line_length(&self, line: &str) -> usize {
+ line.chars()
+ .map(|ch| if ch == '\t' { self.width } else { 1 })
+ .sum()
+ }
+
+ /// Estimates how many times the line has been indented.
+ pub fn count_indents(&self, mut line: &str) -> usize {
+ let mut indent = 0;
+ while !line.is_empty() {
+ // Try to count tabs
+ let num_tabs = line.chars().take_while(|ch| *ch == '\t').count();
+ if num_tabs > 0 {
+ indent += num_tabs;
+ line = &line[num_tabs..];
+ continue;
+ }
+
+ // Try to count spaces
+ let num_spaces = line.chars().take_while(|ch| *ch == ' ').count();
+ if num_spaces >= self.width {
+ // Intentionally floor here to take only the amount of space that matches an indent
+ let num_space_indents = num_spaces / self.width;
+ indent += num_space_indents;
+ line = &line[num_space_indents * self.width..];
+ continue;
+ }
+
+ // Line starts with either non-indent characters or an unevent amount of spaces,
+ // so no more indent remains.
+ break;
+ }
+ indent
+ }
+}
+
+impl Default for IndentOptions {
+ fn default() -> Self {
+ Self::new(IndentType::Spaces, 4)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn count_indents() {
+ assert_eq!(
+ IndentOptions::new(IndentType::Spaces, 4).count_indents("no indentation here!"),
+ 0
+ );
+ assert_eq!(
+ IndentOptions::new(IndentType::Spaces, 4).count_indents(" v += 2"),
+ 1
+ );
+ assert_eq!(
+ IndentOptions::new(IndentType::Spaces, 4).count_indents(" v += 2"),
+ 2
+ );
+ assert_eq!(
+ IndentOptions::new(IndentType::Spaces, 4).count_indents(" v += 2"),
+ 2
+ );
+ assert_eq!(
+ IndentOptions::new(IndentType::Spaces, 4).count_indents("\t\tv += 2"),
+ 2
+ );
+ assert_eq!(
+ IndentOptions::new(IndentType::Spaces, 4).count_indents("\t\t v += 2"),
+ 2
+ );
+ assert_eq!(
+ IndentOptions::new(IndentType::Spaces, 2).count_indents(" v += 2"),
+ 2
+ );
+ }
+}
diff --git a/packages/autofmt/src/lib.rs b/packages/autofmt/src/lib.rs
index 64a342885a..0c3fe6bb83 100644
--- a/packages/autofmt/src/lib.rs
+++ b/packages/autofmt/src/lib.rs
@@ -16,8 +16,11 @@ mod collect_macros;
mod component;
mod element;
mod expr;
+mod indent;
mod writer;
+pub use indent::{IndentOptions, IndentType};
+
/// A modification to the original file to be applied by an IDE
///
/// Right now this re-writes entire rsx! blocks at a time, instead of precise line-by-line changes.
@@ -47,7 +50,7 @@ pub struct FormattedBlock {
/// back to the file precisely.
///
/// Nested blocks of RSX will be handled automatically
-pub fn fmt_file(contents: &str) -> Vec {
+pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec {
let mut formatted_blocks = Vec::new();
let parsed = syn::parse_file(contents).unwrap();
@@ -61,6 +64,7 @@ pub fn fmt_file(contents: &str) -> Vec {
}
let mut writer = Writer::new(contents);
+ writer.out.indent = indent;
// Don't parse nested macros
let mut end_span = LineColumn { column: 0, line: 0 };
@@ -76,7 +80,10 @@ pub fn fmt_file(contents: &str) -> Vec {
let rsx_start = macro_path.span().start();
- writer.out.indent = leading_whitespaces(writer.src[rsx_start.line - 1]) / 4;
+ writer.out.indent_level = writer
+ .out
+ .indent
+ .count_indents(writer.src[rsx_start.line - 1]);
write_body(&mut writer, &body);
@@ -159,12 +166,13 @@ pub fn fmt_block_from_expr(raw: &str, expr: ExprMacro) -> Option {
buf.consume()
}
-pub fn fmt_block(block: &str, indent_level: usize) -> Option {
+pub fn fmt_block(block: &str, indent_level: usize, indent: IndentOptions) -> Option {
let body = syn::parse_str::(block).unwrap();
let mut buf = Writer::new(block);
- buf.out.indent = indent_level;
+ buf.out.indent = indent;
+ buf.out.indent_level = indent_level;
write_body(&mut buf, &body);
@@ -230,14 +238,3 @@ pub(crate) fn write_ifmt(input: &IfmtInput, writable: &mut impl Write) -> std::f
let display = DisplayIfmt(input);
write!(writable, "{}", display)
}
-
-pub fn leading_whitespaces(input: &str) -> usize {
- input
- .chars()
- .map_while(|c| match c {
- ' ' => Some(1),
- '\t' => Some(4),
- _ => None,
- })
- .sum()
-}
diff --git a/packages/autofmt/src/writer.rs b/packages/autofmt/src/writer.rs
index 59f1355922..ede35d7681 100644
--- a/packages/autofmt/src/writer.rs
+++ b/packages/autofmt/src/writer.rs
@@ -1,4 +1,4 @@
-use dioxus_rsx::{BodyNode, ElementAttr, ElementAttrNamed, ForLoop};
+use dioxus_rsx::{BodyNode, ElementAttrNamed, ElementAttrValue, ForLoop};
use proc_macro2::{LineColumn, Span};
use quote::ToTokens;
use std::{
@@ -96,11 +96,11 @@ impl<'a> Writer<'a> {
// Push out the indent level and write each component, line by line
pub fn write_body_indented(&mut self, children: &[BodyNode]) -> Result {
- self.out.indent += 1;
+ self.out.indent_level += 1;
self.write_body_no_indent(children)?;
- self.out.indent -= 1;
+ self.out.indent_level -= 1;
Ok(())
}
@@ -132,6 +132,39 @@ impl<'a> Writer<'a> {
Ok(())
}
+ pub(crate) fn attr_value_len(&mut self, value: &ElementAttrValue) -> usize {
+ match value {
+ ElementAttrValue::AttrOptionalExpr { condition, value } => {
+ let condition_len = self.retrieve_formatted_expr(condition).len();
+ let value_len = self.attr_value_len(value);
+
+ condition_len + value_len + 6
+ }
+ ElementAttrValue::AttrLiteral(lit) => ifmt_to_string(lit).len(),
+ ElementAttrValue::AttrExpr(expr) => expr.span().line_length(),
+ ElementAttrValue::EventTokens(tokens) => {
+ let location = Location::new(tokens.span().start());
+
+ let len = if let std::collections::hash_map::Entry::Vacant(e) =
+ self.cached_formats.entry(location)
+ {
+ let formatted = prettyplease::unparse_expr(tokens);
+ let len = if formatted.contains('\n') {
+ 10000
+ } else {
+ formatted.len()
+ };
+ e.insert(formatted);
+ len
+ } else {
+ self.cached_formats[&location].len()
+ };
+
+ len
+ }
+ }
+ }
+
pub(crate) fn is_short_attrs(&mut self, attributes: &[ElementAttrNamed]) -> usize {
let mut total = 0;
@@ -146,40 +179,17 @@ impl<'a> Writer<'a> {
}
}
- 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 len = if let std::collections::hash_map::Entry::Vacant(e) =
- self.cached_formats.entry(location)
- {
- let formatted = prettyplease::unparse_expr(tokens);
- let len = if formatted.contains('\n') {
- 10000
- } else {
- formatted.len()
- };
- e.insert(formatted);
- len
- } else {
- self.cached_formats[&location].len()
- };
-
- len + name.span().line_length() + 6
+ total += 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 += self.attr_value_len(&attr.attr.value);
+
+ total += 6;
}
total
@@ -218,7 +228,7 @@ impl<'a> Writer<'a> {
}
}
-trait SpanLength {
+pub(crate) trait SpanLength {
fn line_length(&self) -> usize;
}
impl SpanLength for Span {
diff --git a/packages/autofmt/tests/samples.rs b/packages/autofmt/tests/samples.rs
index 8145b0e8e3..9431d700e1 100644
--- a/packages/autofmt/tests/samples.rs
+++ b/packages/autofmt/tests/samples.rs
@@ -12,7 +12,7 @@ macro_rules! twoway {
#[test]
fn $name() {
let src = include_str!(concat!("./samples/", stringify!($name), ".rsx"));
- let formatted = dioxus_autofmt::fmt_file(src);
+ let formatted = dioxus_autofmt::fmt_file(src, Default::default());
let out = dioxus_autofmt::apply_formats(src, formatted);
// normalize line endings
let out = out.replace("\r", "");
diff --git a/packages/autofmt/tests/samples/simple.rsx b/packages/autofmt/tests/samples/simple.rsx
index a0e5739741..a3bfd13882 100644
--- a/packages/autofmt/tests/samples/simple.rsx
+++ b/packages/autofmt/tests/samples/simple.rsx
@@ -33,7 +33,7 @@ rsx! {
}
// 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
div {
diff --git a/packages/autofmt/tests/wrong.rs b/packages/autofmt/tests/wrong.rs
index 5a0fb8a879..06a0f00d10 100644
--- a/packages/autofmt/tests/wrong.rs
+++ b/packages/autofmt/tests/wrong.rs
@@ -1,10 +1,12 @@
+use dioxus_autofmt::{IndentOptions, IndentType};
+
macro_rules! twoway {
- ($val:literal => $name:ident) => {
+ ($val:literal => $name:ident ($indent:expr)) => {
#[test]
fn $name() {
let src_right = include_str!(concat!("./wrong/", $val, ".rsx"));
let src_wrong = include_str!(concat!("./wrong/", $val, ".wrong.rsx"));
- let formatted = dioxus_autofmt::fmt_file(src_wrong);
+ let formatted = dioxus_autofmt::fmt_file(src_wrong, $indent);
let out = dioxus_autofmt::apply_formats(src_wrong, formatted);
// normalize line endings
@@ -16,8 +18,11 @@ macro_rules! twoway {
};
}
-twoway!("comments" => comments);
+twoway!("comments-4sp" => comments_4sp (IndentOptions::new(IndentType::Spaces, 4)));
+twoway!("comments-tab" => comments_tab (IndentOptions::new(IndentType::Tabs, 4)));
-twoway!("multi" => multi);
+twoway!("multi-4sp" => multi_4sp (IndentOptions::new(IndentType::Spaces, 4)));
+twoway!("multi-tab" => multi_tab (IndentOptions::new(IndentType::Tabs, 4)));
-twoway!("multiexpr" => multiexpr);
+twoway!("multiexpr-4sp" => multiexpr_4sp (IndentOptions::new(IndentType::Spaces, 4)));
+twoway!("multiexpr-tab" => multiexpr_tab (IndentOptions::new(IndentType::Tabs, 4)));
diff --git a/packages/autofmt/tests/wrong/comments.rsx b/packages/autofmt/tests/wrong/comments-4sp.rsx
similarity index 100%
rename from packages/autofmt/tests/wrong/comments.rsx
rename to packages/autofmt/tests/wrong/comments-4sp.rsx
diff --git a/packages/autofmt/tests/wrong/comments.wrong.rsx b/packages/autofmt/tests/wrong/comments-4sp.wrong.rsx
similarity index 100%
rename from packages/autofmt/tests/wrong/comments.wrong.rsx
rename to packages/autofmt/tests/wrong/comments-4sp.wrong.rsx
diff --git a/packages/autofmt/tests/wrong/comments-tab.rsx b/packages/autofmt/tests/wrong/comments-tab.rsx
new file mode 100644
index 0000000000..4c05e347e6
--- /dev/null
+++ b/packages/autofmt/tests/wrong/comments-tab.rsx
@@ -0,0 +1,7 @@
+rsx! {
+ div {
+ // Comments
+ class: "asdasd",
+ "hello world"
+ }
+}
diff --git a/packages/autofmt/tests/wrong/comments-tab.wrong.rsx b/packages/autofmt/tests/wrong/comments-tab.wrong.rsx
new file mode 100644
index 0000000000..eac96642d7
--- /dev/null
+++ b/packages/autofmt/tests/wrong/comments-tab.wrong.rsx
@@ -0,0 +1,5 @@
+rsx! {
+ div {
+ // Comments
+ class: "asdasd", "hello world" }
+}
diff --git a/packages/autofmt/tests/wrong/multi.rsx b/packages/autofmt/tests/wrong/multi-4sp.rsx
similarity index 100%
rename from packages/autofmt/tests/wrong/multi.rsx
rename to packages/autofmt/tests/wrong/multi-4sp.rsx
diff --git a/packages/autofmt/tests/wrong/multi.wrong.rsx b/packages/autofmt/tests/wrong/multi-4sp.wrong.rsx
similarity index 100%
rename from packages/autofmt/tests/wrong/multi.wrong.rsx
rename to packages/autofmt/tests/wrong/multi-4sp.wrong.rsx
diff --git a/packages/autofmt/tests/wrong/multi-tab.rsx b/packages/autofmt/tests/wrong/multi-tab.rsx
new file mode 100644
index 0000000000..1fb85c3d03
--- /dev/null
+++ b/packages/autofmt/tests/wrong/multi-tab.rsx
@@ -0,0 +1,3 @@
+fn app(cx: Scope) -> Element {
+ cx.render(rsx! { div { "hello world" } })
+}
diff --git a/packages/autofmt/tests/wrong/multi-tab.wrong.rsx b/packages/autofmt/tests/wrong/multi-tab.wrong.rsx
new file mode 100644
index 0000000000..d818f95350
--- /dev/null
+++ b/packages/autofmt/tests/wrong/multi-tab.wrong.rsx
@@ -0,0 +1,5 @@
+fn app(cx: Scope) -> Element {
+ cx.render(rsx! {
+ div {"hello world" }
+ })
+}
diff --git a/packages/autofmt/tests/wrong/multiexpr.rsx b/packages/autofmt/tests/wrong/multiexpr-4sp.rsx
similarity index 100%
rename from packages/autofmt/tests/wrong/multiexpr.rsx
rename to packages/autofmt/tests/wrong/multiexpr-4sp.rsx
diff --git a/packages/autofmt/tests/wrong/multiexpr.wrong.rsx b/packages/autofmt/tests/wrong/multiexpr-4sp.wrong.rsx
similarity index 100%
rename from packages/autofmt/tests/wrong/multiexpr.wrong.rsx
rename to packages/autofmt/tests/wrong/multiexpr-4sp.wrong.rsx
diff --git a/packages/autofmt/tests/wrong/multiexpr-tab.rsx b/packages/autofmt/tests/wrong/multiexpr-tab.rsx
new file mode 100644
index 0000000000..1fc3401c48
--- /dev/null
+++ b/packages/autofmt/tests/wrong/multiexpr-tab.rsx
@@ -0,0 +1,8 @@
+fn ItWroks() {
+ cx.render(rsx! {
+ div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light",
+ left,
+ right
+ }
+ })
+}
diff --git a/packages/autofmt/tests/wrong/multiexpr-tab.wrong.rsx b/packages/autofmt/tests/wrong/multiexpr-tab.wrong.rsx
new file mode 100644
index 0000000000..0731075417
--- /dev/null
+++ b/packages/autofmt/tests/wrong/multiexpr-tab.wrong.rsx
@@ -0,0 +1,5 @@
+fn ItWroks() {
+ cx.render(rsx! {
+ div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right }
+ })
+}
diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml
index adb0239f2d..4c115184ef 100644
--- a/packages/cli/Cargo.toml
+++ b/packages/cli/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "dioxus-cli"
-version = "0.4.1"
+version = "0.4.3"
authors = ["Jonathan Kelley"]
edition = "2021"
description = "CLI tool for developing, testing, and publishing Dioxus apps"
@@ -83,6 +83,7 @@ dioxus-html = { workspace = true, features = ["hot-reload-context"] }
dioxus-core = { workspace = true, features = ["serialize"] }
dioxus-hot-reload = { workspace = true }
interprocess-docfix = { version = "1.2.2" }
+gitignore = "1.0.8"
[features]
default = []
diff --git a/packages/cli/README.md b/packages/cli/README.md
index 72a8134683..2325d6784b 100644
--- a/packages/cli/README.md
+++ b/packages/cli/README.md
@@ -10,8 +10,8 @@ It handles building, bundling, development and publishing to simplify developmen
### Install the stable version (recommended)
-```
-cargo install dioxus-cli --locked
+```shell
+cargo install dioxus-cli
```
### Install the latest development build through git
@@ -20,7 +20,7 @@ To get the latest bug fixes and features, you can install the development versio
However, this is not fully tested.
That means you're probably going to have more bugs despite having the latest bug fixes.
-```
+```shell
cargo install --git https://github.com/DioxusLabs/dioxus dioxus-cli
```
@@ -29,7 +29,7 @@ and install it in Cargo's global binary directory (`~/.cargo/bin/` by default).
### Install from local folder
-```
+```shell
cargo install --path . --debug
```
@@ -40,7 +40,7 @@ It will be cloned from the [dioxus-template](https://github.com/DioxusLabs/dioxu
Alternatively, you can specify the template path:
-```
+```shell
dx create hello --template gh:dioxuslabs/dioxus-template
```
diff --git a/packages/cli/src/assets/dioxus.toml b/packages/cli/src/assets/dioxus.toml
index 6386fb76ed..892a6cdf83 100644
--- a/packages/cli/src/assets/dioxus.toml
+++ b/packages/cli/src/assets/dioxus.toml
@@ -23,7 +23,7 @@ title = "Dioxus | An elegant GUI library for Rust"
index_on_404 = true
-watch_path = ["src"]
+watch_path = ["src", "examples"]
# include `assets` in web platform
[web.resource]
diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs
index 68c494c42f..a5721d7a63 100644
--- a/packages/cli/src/builder.rs
+++ b/packages/cli/src/builder.rs
@@ -48,14 +48,25 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result {
// [1] Build the .wasm module
log::info!("π
Running build command...");
+
+ let wasm_check_command = std::process::Command::new("rustup")
+ .args(["show"])
+ .output()?;
+ let wasm_check_output = String::from_utf8(wasm_check_command.stdout).unwrap();
+ if !wasm_check_output.contains("wasm32-unknown-unknown") {
+ log::info!("wasm32-unknown-unknown target not detected, installing..");
+ let _ = std::process::Command::new("rustup")
+ .args(["target", "add", "wasm32-unknown-unknown"])
+ .output()?;
+ }
+
let cmd = subprocess::Exec::cmd("cargo");
let cmd = cmd
.cwd(crate_dir)
.arg("build")
.arg("--target")
.arg("wasm32-unknown-unknown")
- .arg("--message-format=json")
- .arg("--quiet");
+ .arg("--message-format=json");
let cmd = if config.release {
cmd.arg("--release")
@@ -65,7 +76,7 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result {
let cmd = if config.verbose {
cmd.arg("--verbose")
} else {
- cmd
+ cmd.arg("--quiet")
};
let cmd = if config.custom_profile.is_some() {
@@ -82,6 +93,8 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result {
cmd
};
+ let cmd = cmd.args(&config.cargo_args);
+
let cmd = match executable {
ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
@@ -261,6 +274,8 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result Result cmd.arg("--bin").arg(name),
crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
@@ -290,12 +313,17 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result {
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) => {
file_name = name.clone();
config
.target_dir
+ .join(target_platform)
.join(release_type)
.join("examples")
.join(name)
@@ -468,7 +496,7 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
.unwrap_or_default()
.contains_key("tailwindcss")
{
- style_str.push_str("\n");
+ style_str.push_str("\n");
}
replace_or_insert_before("{style_include}", &style_str, " Result<()> {
+ let Autoformat { check, raw, file } = self;
+
// Default to formatting the project
- if self.raw.is_none() && self.file.is_none() {
- if let Err(e) = autoformat_project(self.check).await {
+ if raw.is_none() && file.is_none() {
+ if let Err(e) = autoformat_project(check).await {
eprintln!("error formatting project: {}", e);
exit(1);
}
}
- if let Some(raw) = self.raw {
- if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0) {
+ if let Some(raw) = raw {
+ let indent = indentation_for(".")?;
+ if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0, indent) {
println!("{}", inner);
} else {
// exit process with error
@@ -45,43 +49,90 @@ impl Autoformat {
}
// Format single file
- if let Some(file) = self.file {
- let file_content = if file == "-" {
- let mut contents = String::new();
- std::io::stdin().read_to_string(&mut contents)?;
- Ok(contents)
- } else {
- fs::read_to_string(&file)
- };
-
- match file_content {
- Ok(s) => {
- let edits = dioxus_autofmt::fmt_file(&s);
- let out = dioxus_autofmt::apply_formats(&s, edits);
- if file == "-" {
- print!("{}", out);
- } else {
- match fs::write(&file, out) {
- Ok(_) => {
- println!("formatted {}", file);
- }
- Err(e) => {
- eprintln!("failed to write formatted content to file: {}", e);
- }
- }
- }
- }
- Err(e) => {
- eprintln!("failed to open file: {}", e);
- exit(1);
- }
- }
+ if let Some(file) = file {
+ refactor_file(file)?;
}
Ok(())
}
}
+fn refactor_file(file: String) -> Result<(), Error> {
+ let indent = indentation_for(".")?;
+ let file_content = if file == "-" {
+ let mut contents = String::new();
+ std::io::stdin().read_to_string(&mut contents)?;
+ Ok(contents)
+ } else {
+ fs::read_to_string(&file)
+ };
+ let Ok(s) = file_content else {
+ eprintln!("failed to open file: {}", file_content.unwrap_err());
+ exit(1);
+ };
+ let edits = dioxus_autofmt::fmt_file(&s, indent);
+ let out = dioxus_autofmt::apply_formats(&s, edits);
+
+ if file == "-" {
+ print!("{}", out);
+ } else if let Err(e) = fs::write(&file, out) {
+ eprintln!("failed to write formatted content to file: {e}",);
+ } else {
+ println!("formatted {}", file);
+ }
+
+ Ok(())
+}
+
+fn get_project_files(config: &CrateConfig) -> Vec {
+ let mut files = vec![];
+
+ let gitignore_path = config.crate_dir.join(".gitignore");
+ if gitignore_path.is_file() {
+ let gitigno = gitignore::File::new(gitignore_path.as_path()).unwrap();
+ if let Ok(git_files) = gitigno.included_files() {
+ let git_files = git_files
+ .into_iter()
+ .filter(|f| f.ends_with(".rs") && !is_target_dir(f));
+ files.extend(git_files)
+ };
+ } else {
+ collect_rs_files(&config.crate_dir, &mut files);
+ }
+
+ files
+}
+
+fn is_target_dir(file: &Path) -> bool {
+ let stripped = if let Ok(cwd) = std::env::current_dir() {
+ file.strip_prefix(cwd).unwrap_or(file)
+ } else {
+ file
+ };
+ if let Some(first) = stripped.components().next() {
+ first.as_os_str() == "target"
+ } else {
+ false
+ }
+}
+
+async fn format_file(
+ path: impl AsRef,
+ indent: IndentOptions,
+) -> Result {
+ let contents = tokio::fs::read_to_string(&path).await?;
+
+ let edits = dioxus_autofmt::fmt_file(&contents, indent);
+ let len = edits.len();
+
+ if !edits.is_empty() {
+ let out = dioxus_autofmt::apply_formats(&contents, edits);
+ tokio::fs::write(path, out).await?;
+ }
+
+ Ok(len)
+}
+
/// Read every .rs file accessible when considering the .gitignore and try to format it
///
/// Runs using Tokio for multithreading, so it should be really really fast
@@ -90,42 +141,27 @@ impl Autoformat {
async fn autoformat_project(check: bool) -> Result<()> {
let crate_config = crate::CrateConfig::new(None)?;
- let mut files_to_format = vec![];
- collect_rs_files(&crate_config.crate_dir, &mut files_to_format);
+ let files_to_format = get_project_files(&crate_config);
+
+ if files_to_format.is_empty() {
+ return Ok(());
+ }
+
+ let indent = indentation_for(&files_to_format[0])?;
let counts = files_to_format
.into_iter()
- .filter(|file| {
- if file.components().any(|f| f.as_os_str() == "target") {
- return false;
- }
-
- true
- })
.map(|path| async {
- let _path = path.clone();
- let res = tokio::spawn(async move {
- let contents = tokio::fs::read_to_string(&path).await?;
-
- let edits = dioxus_autofmt::fmt_file(&contents);
- let len = edits.len();
-
- if !edits.is_empty() {
- let out = dioxus_autofmt::apply_formats(&contents, edits);
- tokio::fs::write(&path, out).await?;
- }
-
- Ok(len) as Result
- })
- .await;
+ let path_clone = path.clone();
+ let res = tokio::spawn(format_file(path, indent.clone())).await;
match res {
Err(err) => {
- eprintln!("error formatting file: {}\n{err}", _path.display());
+ eprintln!("error formatting file: {}\n{err}", path_clone.display());
None
}
Ok(Err(err)) => {
- eprintln!("error formatting file: {}\n{err}", _path.display());
+ eprintln!("error formatting file: {}\n{err}", path_clone.display());
None
}
Ok(Ok(res)) => Some(res),
@@ -135,13 +171,7 @@ async fn autoformat_project(check: bool) -> Result<()> {
.collect::>()
.await;
- let files_formatted: usize = counts
- .into_iter()
- .map(|f| match f {
- Some(res) => res,
- _ => 0,
- })
- .sum();
+ let files_formatted: usize = counts.into_iter().flatten().sum();
if files_formatted > 0 && check {
eprintln!("{} files needed formatting", files_formatted);
@@ -151,26 +181,67 @@ async fn autoformat_project(check: bool) -> Result<()> {
Ok(())
}
-fn collect_rs_files(folder: &Path, files: &mut Vec) {
- let Ok(folder) = folder.read_dir() else {
+fn indentation_for(file_or_dir: impl AsRef) -> Result {
+ let out = std::process::Command::new("cargo")
+ .args(["fmt", "--", "--print-config", "current"])
+ .arg(file_or_dir.as_ref())
+ .stdout(std::process::Stdio::piped())
+ .stderr(std::process::Stdio::inherit())
+ .output()?;
+ if !out.status.success() {
+ return Err(Error::CargoError("cargo fmt failed".into()));
+ }
+
+ let config = String::from_utf8_lossy(&out.stdout);
+
+ let hard_tabs = config
+ .lines()
+ .find(|line| line.starts_with("hard_tabs "))
+ .and_then(|line| line.split_once('='))
+ .map(|(_, value)| value.trim() == "true")
+ .ok_or_else(|| {
+ Error::RuntimeError("Could not find hard_tabs option in rustfmt config".into())
+ })?;
+ let tab_spaces = config
+ .lines()
+ .find(|line| line.starts_with("tab_spaces "))
+ .and_then(|line| line.split_once('='))
+ .map(|(_, value)| value.trim().parse::())
+ .ok_or_else(|| {
+ Error::RuntimeError("Could not find tab_spaces option in rustfmt config".into())
+ })?
+ .map_err(|_| {
+ Error::RuntimeError("Could not parse tab_spaces option in rustfmt config".into())
+ })?;
+
+ Ok(IndentOptions::new(
+ if hard_tabs {
+ IndentType::Tabs
+ } else {
+ IndentType::Spaces
+ },
+ tab_spaces,
+ ))
+}
+
+fn collect_rs_files(folder: &impl AsRef, files: &mut Vec) {
+ if is_target_dir(folder.as_ref()) {
+ return;
+ }
+ let Ok(folder) = folder.as_ref().read_dir() else {
return;
};
-
// load the gitignore
-
for entry in folder {
let Ok(entry) = entry else {
continue;
};
-
let path = entry.path();
-
if path.is_dir() {
collect_rs_files(&path, files);
}
-
if let Some(ext) = path.extension() {
- if ext == "rs" {
+ if ext == "rs" && !is_target_dir(&path) {
files.push(path);
}
}
diff --git a/packages/cli/src/cli/build.rs b/packages/cli/src/cli/build.rs
index 7fe29f8ce9..0deeb38ca8 100644
--- a/packages/cli/src/cli/build.rs
+++ b/packages/cli/src/cli/build.rs
@@ -37,8 +37,14 @@ impl Build {
.platform
.unwrap_or(crate_config.dioxus_config.application.default_platform);
- #[cfg(feature = "plugin")]
- let _ = PluginManager::on_build_start(&crate_config, &platform);
+ if let Some(target) = self.build.target {
+ crate_config.set_target(target);
+ }
+
+ crate_config.set_cargo_args(self.build.cargo_args);
+
+ // #[cfg(feature = "plugin")]
+ // let _ = PluginManager::on_build_start(&crate_config, &platform);
match platform {
Platform::Web => {
@@ -66,8 +72,8 @@ impl Build {
)?;
file.write_all(temp.as_bytes())?;
- #[cfg(feature = "plugin")]
- let _ = PluginManager::on_build_finish(&crate_config, &platform);
+ // #[cfg(feature = "plugin")]
+ // let _ = PluginManager::on_build_finish(&crate_config, &platform);
Ok(())
}
diff --git a/packages/cli/src/cli/bundle.rs b/packages/cli/src/cli/bundle.rs
index 80b52cf2fa..fd97930ed7 100644
--- a/packages/cli/src/cli/bundle.rs
+++ b/packages/cli/src/cli/bundle.rs
@@ -76,6 +76,12 @@ impl Bundle {
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_desktop(&crate_config, false)?;
@@ -148,6 +154,11 @@ impl Bundle {
.collect(),
);
}
+
+ if let Some(target) = &self.build.target {
+ settings = settings.target(target.to_string());
+ }
+
let settings = settings.build();
// on macos we need to set CI=true (https://github.com/tauri-apps/tauri/issues/2567)
@@ -156,9 +167,9 @@ impl Bundle {
tauri_bundler::bundle::bundle_project(settings.unwrap()).unwrap_or_else(|err|{
#[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"))]
- panic!("Failed to bundle project: {}", err);
+ panic!("Failed to bundle project: {:#?}", err);
});
Ok(())
diff --git a/packages/cli/src/cli/cfg.rs b/packages/cli/src/cli/cfg.rs
index 2a441a1c85..0d2a5e2128 100644
--- a/packages/cli/src/cli/cfg.rs
+++ b/packages/cli/src/cli/cfg.rs
@@ -6,10 +6,6 @@ use super::*;
/// Config options for the build system.
#[derive(Clone, Debug, Default, Deserialize, Parser)]
pub struct ConfigOptsBuild {
- /// The index HTML file to drive the bundling process [default: index.html]
- #[arg(long)]
- pub target: Option,
-
/// Build in release mode [default: false]
#[clap(long)]
#[serde(default)]
@@ -35,14 +31,18 @@ pub struct ConfigOptsBuild {
/// Space separated list of features to activate
#[clap(long)]
pub features: Option>,
+
+ /// Rustc platform triple
+ #[clap(long)]
+ pub target: Option,
+
+ /// Extra arguments passed to cargo build
+ #[clap(last = true)]
+ pub cargo_args: Vec,
}
#[derive(Clone, Debug, Default, Deserialize, Parser)]
pub struct ConfigOptsServe {
- /// The index HTML file to drive the bundling process [default: index.html]
- #[arg(short, long)]
- pub target: Option,
-
/// Port of dev server
#[clap(long)]
#[clap(default_value_t = 8080)]
@@ -89,6 +89,14 @@ pub struct ConfigOptsServe {
/// Space separated list of features to activate
#[clap(long)]
pub features: Option>,
+
+ /// Rustc platform triple
+ #[clap(long)]
+ pub target: Option,
+
+ /// Extra arguments passed to cargo build
+ #[clap(last = true)]
+ pub cargo_args: Vec,
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize, Debug)]
@@ -129,4 +137,12 @@ pub struct ConfigOptsBundle {
/// Space separated list of features to activate
#[clap(long)]
pub features: Option>,
+
+ /// Rustc platform triple
+ #[clap(long)]
+ pub target: Option,
+
+ /// Extra arguments passed to cargo build
+ #[clap(last = true)]
+ pub cargo_args: Vec,
}
diff --git a/packages/cli/src/cli/serve.rs b/packages/cli/src/cli/serve.rs
index 433d611b73..e08b02339d 100644
--- a/packages/cli/src/cli/serve.rs
+++ b/packages/cli/src/cli/serve.rs
@@ -34,6 +34,12 @@ impl Serve {
// Subdirectories don't work with the server
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
.serve
.platform
diff --git a/packages/cli/src/config.rs b/packages/cli/src/config.rs
index 0654dc3754..727504e6a0 100644
--- a/packages/cli/src/config.rs
+++ b/packages/cli/src/config.rs
@@ -105,7 +105,7 @@ impl Default for DioxusConfig {
},
proxy: Some(vec![]),
watcher: WebWatcherConfig {
- watch_path: Some(vec![PathBuf::from("src")]),
+ watch_path: Some(vec![PathBuf::from("src"), PathBuf::from("examples")]),
reload_html: Some(false),
index_on_404: Some(true),
},
@@ -211,6 +211,8 @@ pub struct CrateConfig {
pub verbose: bool,
pub custom_profile: Option,
pub features: Option>,
+ pub target: Option,
+ pub cargo_args: Vec,
}
#[derive(Debug, Clone)]
@@ -278,6 +280,8 @@ impl CrateConfig {
let verbose = false;
let custom_profile = None;
let features = None;
+ let target = None;
+ let cargo_args = vec![];
Ok(Self {
out_dir,
@@ -294,6 +298,8 @@ impl CrateConfig {
custom_profile,
features,
verbose,
+ target,
+ cargo_args,
})
}
@@ -331,6 +337,16 @@ impl CrateConfig {
self.features = Some(features);
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) -> &mut Self {
+ self.cargo_args = cargo_args;
+ self
+ }
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
diff --git a/packages/cli/src/error.rs b/packages/cli/src/error.rs
index 84b9d4b718..d577b2b40f 100644
--- a/packages/cli/src/error.rs
+++ b/packages/cli/src/error.rs
@@ -29,6 +29,9 @@ pub enum Error {
#[error("Cargo Error: {0}")]
CargoError(String),
+ #[error("Couldn't retrieve cargo metadata")]
+ CargoMetadata(#[source] cargo_metadata::Error),
+
#[error("{0}")]
CustomError(String),
diff --git a/packages/cli/src/logging.rs b/packages/cli/src/logging.rs
index 48c2a2788d..31d088737a 100644
--- a/packages/cli/src/logging.rs
+++ b/packages/cli/src/logging.rs
@@ -28,7 +28,19 @@ pub fn set_up_logging() {
message = message,
));
})
- .level(log::LevelFilter::Info)
+ .level(match std::env::var("DIOXUS_LOG") {
+ Ok(level) => match level.to_lowercase().as_str() {
+ "error" => log::LevelFilter::Error,
+ "warn" => log::LevelFilter::Warn,
+ "info" => log::LevelFilter::Info,
+ "debug" => log::LevelFilter::Debug,
+ "trace" => log::LevelFilter::Trace,
+ _ => {
+ panic!("Invalid log level: {}", level)
+ }
+ },
+ Err(_) => log::LevelFilter::Info,
+ })
.chain(std::io::stdout())
.apply()
.unwrap();
diff --git a/packages/cli/src/main.rs b/packages/cli/src/main.rs
index fe860a0e6a..2358551f2a 100644
--- a/packages/cli/src/main.rs
+++ b/packages/cli/src/main.rs
@@ -9,42 +9,31 @@ use dioxus_cli::plugin::PluginManager;
use Commands::*;
-fn get_bin(bin: Option) -> Result
");
let prop_def_link = format!("{props_def_link}::{arg_name}");
let mut arg_doc = format!("- [`{arg_name}`]({prop_def_link}) : `{arg_type}`");
if let Some(deprecation) = deprecation {
- arg_doc.push_str("
π Deprecated");
+ arg_doc.push_str("
π Deprecated");
if let Some(since) = deprecation.since {
arg_doc.push_str(&format!(" since {since}"));
@@ -205,14 +205,16 @@ fn get_props_docs(fn_ident: &Ident, inputs: Vec<&FnArg>) -> Vec {
arg_doc.push_str(&format!(": {note}"));
}
+ arg_doc.push_str("
");
+
if !input_arg_doc.is_empty() {
arg_doc.push_str("
");
}
- } else {
- arg_doc.push_str("
");
}
- arg_doc.push_str(&input_arg_doc);
+ if !input_arg_doc.is_empty() {
+ arg_doc.push_str(&format!("{input_arg_doc}
"));
+ }
props_docs.push(parse_quote! {
#[doc = #arg_doc]
diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs
index c1931e4977..d7a5807059 100644
--- a/packages/core-macro/src/props/mod.rs
+++ b/packages/core-macro/src/props/mod.rs
@@ -243,10 +243,6 @@ mod field_info {
}
.into()
}
-
- pub fn type_from_inside_option(&self, check_option_name: bool) -> Option<&syn::Type> {
- type_from_inside_option(self.ty, check_option_name)
- }
}
#[derive(Debug, Default, Clone)]
@@ -551,18 +547,16 @@ mod struct_info {
let generics_with_empty = modify_types_generics_hack(&ty_generics, |args| {
args.insert(0, syn::GenericArgument::Type(empties_tuple.clone().into()));
});
- let phantom_generics = self.generics.params.iter().map(|param| match param {
+ let phantom_generics = self.generics.params.iter().filter_map(|param| match param {
syn::GenericParam::Lifetime(lifetime) => {
let lifetime = &lifetime.lifetime;
- quote!(::core::marker::PhantomData<lifetime ()>)
+ Some(quote!(::core::marker::PhantomData<lifetime ()>))
}
syn::GenericParam::Type(ty) => {
let ty = &ty.ident;
- quote!(::core::marker::PhantomData<#ty>)
- }
- syn::GenericParam::Const(_cnst) => {
- quote!()
+ Some(quote!(::core::marker::PhantomData<#ty>))
}
+ syn::GenericParam::Const(_cnst) => None,
});
let builder_method_doc = match self.builder_attr.builder_method_doc {
Some(ref doc) => quote!(#doc),
@@ -633,7 +627,7 @@ Finally, call `.build()` to create the instance of `{name}`.
Ok(quote! {
impl #impl_generics #name #ty_generics #where_clause {
#[doc = #builder_method_doc]
- #[allow(dead_code)]
+ #[allow(dead_code, clippy::type_complexity)]
#vis fn builder() -> #builder_name #generics_with_empty {
#builder_name {
fields: #empties_tuple,
@@ -701,6 +695,14 @@ Finally, call `.build()` to create the instance of `{name}`.
}
pub fn field_impl(&self, field: &FieldInfo) -> Result {
+ let FieldInfo {
+ name: field_name,
+ ty: field_type,
+ ..
+ } = field;
+ 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"));
+ }
let StructInfo {
ref builder_name, ..
} = *self;
@@ -715,11 +717,6 @@ Finally, call `.build()` to create the instance of `{name}`.
});
let reconstructing = self.included_fields().map(|f| f.name);
- let FieldInfo {
- name: field_name,
- ty: field_type,
- ..
- } = field;
let mut ty_generics: Vec = self
.generics
.params
@@ -782,31 +779,16 @@ Finally, call `.build()` to create the instance of `{name}`.
None => quote!(),
};
- // NOTE: both auto_into and strip_option affect `arg_type` and `arg_expr`, but the order of
- // nesting is different so we have to do this little dance.
- let arg_type = if field.builder_attr.strip_option {
- field.type_from_inside_option(false).ok_or_else(|| {
- Error::new_spanned(
- field_type,
- "can't `strip_option` - field is not `Option<...>`",
+ let arg_type = field_type;
+ let (arg_type, arg_expr) =
+ if field.builder_attr.auto_into || field.builder_attr.strip_option {
+ (
+ quote!(impl ::core::convert::Into<#arg_type>),
+ quote!(#field_name.into()),
)
- })?
- } else {
- field_type
- };
- let (arg_type, arg_expr) = if field.builder_attr.auto_into {
- (
- quote!(impl ::core::convert::Into<#arg_type>),
- quote!(#field_name.into()),
- )
- } else {
- (quote!(#arg_type), quote!(#field_name))
- };
- let arg_expr = if field.builder_attr.strip_option {
- quote!(Some(#arg_expr))
- } else {
- arg_expr
- };
+ } else {
+ (quote!(#arg_type), quote!(#field_name))
+ };
let repeated_fields_error_type_name = syn::Ident::new(
&format!(
@@ -822,6 +804,7 @@ Finally, call `.build()` to create the instance of `{name}`.
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl #impl_generics #builder_name < #( #ty_generics ),* > #where_clause {
#doc
+ #[allow(clippy::type_complexity)]
pub fn #field_name (self, #field_name: #arg_type) -> #builder_name < #( #target_generics ),* > {
let #field_name = (#arg_expr,);
let ( #(#descructuring,)* ) = self.fields;
@@ -840,6 +823,7 @@ Finally, call `.build()` to create the instance of `{name}`.
#[deprecated(
note = #repeated_fields_error_message
)]
+ #[allow(clippy::type_complexity)]
pub fn #field_name (self, _: #repeated_fields_error_type_name) -> #builder_name < #( #target_generics ),* > {
self
}
diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs
index b7f768755c..ffdd4656b7 100644
--- a/packages/core/src/arena.rs
+++ b/packages/core/src/arena.rs
@@ -13,62 +13,50 @@ use crate::{
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
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
- pub path: ElementPath,
+ pub(crate) path: ElementPath,
// The actual template
- pub template: Option>>,
+ pub(crate) template: VNodeId,
// The scope the element belongs to
- pub scope: ScopeId,
+ pub(crate) scope: ScopeId,
}
#[derive(Clone, Copy, Debug)]
-pub enum ElementPath {
- Deep(&'static [u8]),
- Root(usize),
-}
-
-impl ElementRef {
- pub(crate) fn none() -> Self {
- Self {
- template: None,
- path: ElementPath::Root(0),
- scope: ScopeId::ROOT,
- }
- }
+pub struct ElementPath {
+ pub(crate) path: &'static [u8],
}
impl VirtualDom {
- pub(crate) fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId {
- self.next_reference(template, ElementPath::Deep(path))
+ pub(crate) fn next_element(&mut self) -> ElementId {
+ ElementId(self.elements.insert(None))
}
- pub(crate) fn next_root(&mut self, template: &VNode, path: usize) -> ElementId {
- self.next_reference(template, ElementPath::Root(path))
- }
-
- pub(crate) fn next_null(&mut self) -> ElementId {
- 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);
+ pub(crate) fn next_vnode_ref(&mut self, vnode: &VNode) -> VNodeId {
+ let new_id = VNodeId(self.element_refs.insert(Some(unsafe {
+ std::mem::transmute::, _>(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);
+ }
- 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)
+ new_id
}
pub(crate) fn reclaim(&mut self, el: ElementId) {
@@ -76,7 +64,7 @@ impl VirtualDom {
.unwrap_or_else(|| panic!("cannot reclaim {:?}", el));
}
- pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option {
+ pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option<()> {
if el.0 == 0 {
panic!(
"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) {
- let node: *const VNode = node as *const _;
- self.elements[el.0].template = unsafe { std::mem::transmute(node) };
+ pub(crate) fn set_template(&mut self, id: VNodeId, vnode: &VNode) {
+ self.element_refs[id.0] =
+ Some(unsafe { std::mem::transmute::, _>(vnode.into()) });
}
// Drop a scope and all its children
@@ -101,6 +89,15 @@ impl VirtualDom {
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);
if recursive {
@@ -145,14 +142,25 @@ impl VirtualDom {
}
/// 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];
+ {
+ // Drop all element refs that could be invalidated when the component was rerun
+ let mut element_refs = self.scopes[scope_id.0].element_refs_to_drop.borrow_mut();
+ let element_refs_slab = &mut self.element_refs;
+ for element_ref in element_refs.drain(..) {
+ if let Some(element_ref) = element_refs_slab.get_mut(element_ref.0) {
+ *element_ref = None;
+ }
+ }
+ }
+
// make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we
// run the hooks (which hold an &mut Reference)
// recursively call ensure_drop_safety on all children
- let mut props = scope.borrowed_props.borrow_mut();
- props.drain(..).for_each(|comp| {
+ let props = { scope.borrowed_props.borrow_mut().clone() };
+ for comp in props {
let comp = unsafe { &*comp };
match comp.scope.get() {
Some(child) if child != scope_id => self.ensure_drop_safety(child),
@@ -161,20 +169,16 @@ impl VirtualDom {
if let Ok(mut props) = comp.props.try_borrow_mut() {
*props = None;
}
- });
+ }
+ let scope = &self.scopes[scope_id.0];
+ scope.borrowed_props.borrow_mut().clear();
// Now that all the references are gone, we can safely drop our own references in our listeners.
- let mut listeners = scope.attributes_to_drop.borrow_mut();
+ let mut listeners = scope.attributes_to_drop_before_render.borrow_mut();
listeners.drain(..).for_each(|listener| {
let listener = unsafe { &*listener };
- match &listener.value {
- AttributeValue::Listener(l) => {
- _ = l.take();
- }
- AttributeValue::Any(a) => {
- _ = a.take();
- }
- _ => (),
+ if let AttributeValue::Listener(l) = &listener.value {
+ _ = l.take();
}
});
}
@@ -182,18 +186,12 @@ impl VirtualDom {
impl ElementPath {
pub(crate) fn is_decendant(&self, small: &&[u8]) -> bool {
- match *self {
- ElementPath::Deep(big) => small.len() <= big.len() && *small == &big[..small.len()],
- ElementPath::Root(r) => small.len() == 1 && small[0] == r as u8,
- }
+ small.len() <= self.path.len() && *small == &self.path[..small.len()]
}
}
impl PartialEq<&[u8]> for ElementPath {
fn eq(&self, other: &&[u8]) -> bool {
- match *self {
- ElementPath::Deep(deep) => deep.eq(*other),
- ElementPath::Root(r) => other.len() == 1 && other[0] == r as u8,
- }
+ self.path.eq(*other)
}
}
diff --git a/packages/core/src/bump_frame.rs b/packages/core/src/bump_frame.rs
index 0fe7b3867c..6927bd8bcb 100644
--- a/packages/core/src/bump_frame.rs
+++ b/packages/core/src/bump_frame.rs
@@ -1,10 +1,16 @@
use crate::nodes::RenderReturn;
+use crate::{Attribute, AttributeValue, VComponent};
use bumpalo::Bump;
+use std::cell::RefCell;
use std::cell::{Cell, UnsafeCell};
pub(crate) struct BumpFrame {
pub bump: UnsafeCell,
pub node: Cell<*const RenderReturn<'static>>,
+
+ // The bump allocator will not call the destructor of the objects it allocated. Attributes and props need to have there destructor called, so we keep a list of them to drop before the bump allocator is reset.
+ pub(crate) attributes_to_drop_before_reset: RefCell>>,
+ pub(crate) props_to_drop_before_reset: RefCell>>,
}
impl BumpFrame {
@@ -13,6 +19,8 @@ impl BumpFrame {
Self {
bump: UnsafeCell::new(bump),
node: Cell::new(std::ptr::null()),
+ attributes_to_drop_before_reset: Default::default(),
+ props_to_drop_before_reset: Default::default(),
}
}
@@ -31,8 +39,38 @@ impl BumpFrame {
unsafe { &*self.bump.get() }
}
- #[allow(clippy::mut_from_ref)]
- pub(crate) unsafe fn bump_mut(&self) -> &mut Bump {
- unsafe { &mut *self.bump.get() }
+ pub(crate) fn add_attribute_to_drop(&self, attribute: *const Attribute<'static>) {
+ self.attributes_to_drop_before_reset
+ .borrow_mut()
+ .push(attribute);
+ }
+
+ /// Reset the bump allocator and drop all the attributes and props that were allocated in it.
+ ///
+ /// # Safety
+ /// The caller must insure that no reference to anything allocated in the bump allocator is available after this function is called.
+ pub(crate) unsafe fn reset(&self) {
+ let mut attributes = self.attributes_to_drop_before_reset.borrow_mut();
+ attributes.drain(..).for_each(|attribute| {
+ let attribute = unsafe { &*attribute };
+ if let AttributeValue::Any(l) = &attribute.value {
+ _ = l.take();
+ }
+ });
+ let mut props = self.props_to_drop_before_reset.borrow_mut();
+ props.drain(..).for_each(|prop| {
+ let prop = unsafe { &*prop };
+ _ = prop.props.borrow_mut().take();
+ });
+ unsafe {
+ let bump = &mut *self.bump.get();
+ bump.reset();
+ }
+ }
+}
+
+impl Drop for BumpFrame {
+ fn drop(&mut self) {
+ unsafe { self.reset() }
}
}
diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs
index a61a8cace8..f3b89ae9ed 100644
--- a/packages/core/src/create.rs
+++ b/packages/core/src/create.rs
@@ -1,5 +1,7 @@
use crate::any_props::AnyProps;
-use crate::innerlude::{BorrowedAttributeValue, VComponent, VPlaceholder, VText};
+use crate::innerlude::{
+ BorrowedAttributeValue, ElementPath, ElementRef, VComponent, VPlaceholder, VText,
+};
use crate::mutations::Mutation;
use crate::mutations::Mutation::*;
use crate::nodes::VNode;
@@ -94,6 +96,9 @@ impl<'b> VirtualDom {
nodes_mut.resize(len, ElementId::default());
};
+ // Set this node id
+ node.stable_id.set(Some(self.next_vnode_ref(node)));
+
// The best renderers will have templates prehydrated and registered
// Just in case, let's create the template using instructions anyways
self.register_template(node.template.get());
@@ -181,15 +186,30 @@ impl<'b> VirtualDom {
use DynamicNode::*;
match &template.dynamic_nodes[idx] {
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 }) => {
- let id = self.set_slot(template, id, idx);
+ Placeholder(VPlaceholder { id, parent }) => {
+ let template_ref = ElementRef {
+ path: ElementPath {
+ path: template.template.get().node_paths[idx],
+ },
+ template: template.stable_id().unwrap(),
+ scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)),
+ };
+ parent.set(Some(template_ref));
+ let id = self.set_slot(id);
self.mutations.push(CreatePlaceholder { id });
1
}
Text(VText { id, value }) => {
- let id = self.set_slot(template, id, idx);
+ let id = self.set_slot(id);
self.create_static_text(value, id);
1
}
@@ -205,7 +225,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
///
@@ -265,7 +285,14 @@ impl<'b> VirtualDom {
.map(|sorted_index| dynamic_nodes[sorted_index].0);
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 {
// The path is one shorter because the top node is the root
let path = &template.template.get().node_paths[idx][1..];
@@ -279,15 +306,15 @@ impl<'b> VirtualDom {
attrs: &mut Peekable>,
root_idx: u8,
root: ElementId,
- node: &VNode,
+ node: &'b VNode<'b>,
) {
while let Some((mut attr_id, path)) =
attrs.next_if(|(_, p)| p.first().copied() == Some(root_idx))
{
- let id = self.assign_static_node_as_dynamic(path, root, node, attr_id);
+ let id = self.assign_static_node_as_dynamic(path, root);
loop {
- self.write_attribute(&node.dynamic_attrs[attr_id], id);
+ self.write_attribute(node, attr_id, &node.dynamic_attrs[attr_id], id);
// Only push the dynamic attributes forward if they match the current path (same element)
match attrs.next_if(|(_, p)| *p == path) {
@@ -298,7 +325,13 @@ impl<'b> VirtualDom {
}
}
- fn write_attribute(&mut self, attribute: &'b crate::Attribute<'b>, id: ElementId) {
+ fn write_attribute(
+ &mut self,
+ template: &'b VNode<'b>,
+ idx: usize,
+ attribute: &'b crate::Attribute<'b>,
+ id: ElementId,
+ ) {
// Make sure we set the attribute's associated id
attribute.mounted_element.set(id);
@@ -307,6 +340,13 @@ impl<'b> VirtualDom {
match &attribute.value {
AttributeValue::Listener(_) => {
+ let path = &template.template.get().attr_paths[idx];
+ let element_ref = ElementRef {
+ path: ElementPath { path },
+ template: template.stable_id().unwrap(),
+ scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)),
+ };
+ self.elements[id.0] = Some(element_ref);
self.mutations.push(NewEventListener {
// all listeners start with "on"
name: &unbounded_name[2..],
@@ -330,7 +370,7 @@ impl<'b> VirtualDom {
fn load_template_root(&mut self, template: &VNode, root_idx: usize) -> ElementId {
// Get an ID for this root since it's a real root
- let this_id = self.next_root(template, root_idx);
+ let this_id = self.next_element();
template.root_ids.borrow_mut()[root_idx] = this_id;
self.mutations.push(LoadTemplate {
@@ -353,8 +393,6 @@ impl<'b> VirtualDom {
&mut self,
path: &'static [u8],
this_id: ElementId,
- template: &VNode,
- attr_id: usize,
) -> ElementId {
if path.len() == 1 {
return this_id;
@@ -362,7 +400,7 @@ impl<'b> VirtualDom {
// if attribute is on a root node, then we've already created the element
// Else, it's deep in the template and we should create a new id for it
- let id = self.next_element(template, template.template.get().attr_paths[attr_id]);
+ let id = self.next_element();
self.mutations.push(Mutation::AssignId {
path: &path[1..],
@@ -405,6 +443,7 @@ impl<'b> VirtualDom {
#[allow(unused_mut)]
pub(crate) fn register_template(&mut self, mut template: Template<'static>) {
let (path, byte_index) = template.name.rsplit_once(':').unwrap();
+
let byte_index = byte_index.parse::().unwrap();
// First, check if we've already seen this template
if self
@@ -439,27 +478,21 @@ impl<'b> VirtualDom {
pub(crate) fn create_dynamic_node(
&mut self,
- template: &'b VNode<'b>,
+ parent: ElementRef,
node: &'b DynamicNode<'b>,
- idx: usize,
) -> usize {
use DynamicNode::*;
match node {
- Text(text) => self.create_dynamic_text(template, text, idx),
- Placeholder(place) => self.create_placeholder(place, template, idx),
- Component(component) => self.create_component_node(template, component),
- Fragment(frag) => frag.iter().map(|child| self.create(child)).sum(),
+ Text(text) => self.create_dynamic_text(parent, text),
+ Placeholder(place) => self.create_placeholder(place, parent),
+ Component(component) => self.create_component_node(Some(parent), component),
+ Fragment(frag) => self.create_children(*frag, Some(parent)),
}
}
- fn create_dynamic_text(
- &mut self,
- template: &'b VNode<'b>,
- text: &'b VText<'b>,
- idx: usize,
- ) -> usize {
+ fn create_dynamic_text(&mut self, parent: ElementRef, text: &'b VText<'b>) -> usize {
// Allocate a dynamic element reference for this text node
- let new_id = self.next_element(template, template.template.get().node_paths[idx]);
+ let new_id = self.next_element();
// Make sure the text node is assigned to the correct element
text.id.set(Some(new_id));
@@ -470,7 +503,7 @@ impl<'b> VirtualDom {
// Add the mutation to the list
self.mutations.push(HydrateText {
id: new_id,
- path: &template.template.get().node_paths[idx][1..],
+ path: &parent.path.path[1..],
value,
});
@@ -481,18 +514,20 @@ impl<'b> VirtualDom {
pub(crate) fn create_placeholder(
&mut self,
placeholder: &VPlaceholder,
- template: &'b VNode<'b>,
- idx: usize,
+ parent: ElementRef,
) -> usize {
// 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
placeholder.id.set(Some(id));
+ // Assign the placeholder's parent
+ placeholder.parent.set(Some(parent));
+
// Assign the ID to the existing node in the template
self.mutations.push(AssignId {
- path: &template.template.get().node_paths[idx][1..],
+ path: &parent.path.path[1..],
id,
});
@@ -502,7 +537,7 @@ impl<'b> VirtualDom {
pub(super) fn create_component_node(
&mut self,
- template: &'b VNode<'b>,
+ parent: Option,
component: &'b VComponent<'b>,
) -> usize {
use RenderReturn::*;
@@ -514,8 +549,11 @@ impl<'b> VirtualDom {
match unsafe { self.run_scope(scope).extend_lifetime_ref() } {
// Create the component's root element
- Ready(t) => self.create_scope(scope, t),
- Aborted(t) => self.mount_aborted(template, t),
+ Ready(t) => {
+ self.assign_boundary_ref(parent, t);
+ self.create_scope(scope, t)
+ }
+ Aborted(t) => self.mount_aborted(t, parent),
}
}
@@ -531,20 +569,17 @@ impl<'b> VirtualDom {
.unwrap_or_else(|| component.scope.get().unwrap())
}
- fn mount_aborted(&mut self, parent: &'b VNode<'b>, placeholder: &VPlaceholder) -> usize {
- let id = self.next_element(parent, &[]);
+ fn mount_aborted(&mut self, placeholder: &VPlaceholder, parent: Option) -> usize {
+ let id = self.next_element();
self.mutations.push(Mutation::CreatePlaceholder { id });
placeholder.id.set(Some(id));
+ placeholder.parent.set(parent);
+
1
}
- fn set_slot(
- &mut self,
- template: &'b VNode<'b>,
- slot: &'b Cell