mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-10 06:34:20 +00:00
Merge branch 'master' into events-2
This commit is contained in:
commit
dccfba12d8
209 changed files with 6270 additions and 2789 deletions
2
.github/workflows/docs stable.yml
vendored
2
.github/workflows/docs stable.yml
vendored
|
@ -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.
|
||||
|
|
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
|
@ -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.
|
||||
|
|
7
.github/workflows/main.yml
vendored
7
.github/workflows/main.yml
vendored
|
@ -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 }}
|
||||
|
|
2
.github/workflows/playwright.yml
vendored
2
.github/workflows/playwright.yml
vendored
|
@ -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
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -4,6 +4,7 @@
|
|||
/dist
|
||||
Cargo.lock
|
||||
.DS_Store
|
||||
/examples/assets/test_video.mp4
|
||||
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
|
|
13
Cargo.toml
13
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,8 @@ fern = { version = "0.6.0", features = ["colored"] }
|
|||
env_logger = "0.10.0"
|
||||
simple_logger = "4.0.0"
|
||||
thiserror = { workspace = true }
|
||||
|
||||
|
||||
[dependencies]
|
||||
tracing-subscriber = "0.3.17"
|
||||
http-range = "0.1.5"
|
||||
|
|
|
@ -24,6 +24,58 @@ 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"]
|
||||
|
@ -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
|
||||
|
|
|
@ -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!
|
||||
|
||||
|
||||
<a href="https://github.com/dioxuslabs/dioxus/graphs/contributors">
|
||||
|
|
|
@ -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:?}" } )) }
|
||||
}
|
||||
))
|
||||
}
|
||||
|
|
|
@ -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!"
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
//! This example shows how to create a popup window and send data back to the parent window.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_desktop::use_window;
|
||||
use futures_util::StreamExt;
|
||||
|
||||
fn main() {
|
||||
|
@ -9,7 +8,6 @@ fn main() {
|
|||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let window = use_window(cx);
|
||||
let emails_sent = use_ref(cx, Vec::new);
|
||||
|
||||
let tx = use_coroutine(cx, |mut rx: UnboundedReceiver<String>| {
|
||||
|
@ -27,13 +25,7 @@ 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.
|
||||
let dom = VirtualDom::new_with_props(compose, ComposeProps { app_tx: tx.clone() });
|
||||
window.new_window(dom, Default::default());
|
||||
},
|
||||
"Click to compose a new email"
|
||||
|
@ -57,7 +49,6 @@ struct ComposeProps {
|
|||
|
||||
fn compose(cx: Scope<ComposeProps>) -> Element {
|
||||
let user_input = use_state(cx, String::new);
|
||||
let window = use_window(cx);
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
|
@ -66,17 +57,12 @@ fn compose(cx: Scope<ComposeProps>) -> Element {
|
|||
button {
|
||||
onclick: move |_| {
|
||||
cx.props.app_tx.send(user_input.get().clone());
|
||||
window.close();
|
||||
dioxus_desktop::window().close();
|
||||
},
|
||||
"Click to send"
|
||||
}
|
||||
|
||||
input {
|
||||
oninput: move |e| {
|
||||
user_input.set(e.value());
|
||||
},
|
||||
value: "{user_input}"
|
||||
}
|
||||
input { oninput: move |e| user_input.set(e.value()), value: "{user_input}" }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -87,8 +87,6 @@ fn ClientAdd(cx: Scope) -> Element {
|
|||
let last_name = use_state(cx, String::new);
|
||||
let description = use_state(cx, String::new);
|
||||
|
||||
let navigator = use_navigator(cx);
|
||||
|
||||
cx.render(rsx! {
|
||||
h2 { "Add new Client" }
|
||||
|
||||
|
@ -103,7 +101,7 @@ fn ClientAdd(cx: Scope) -> Element {
|
|||
description: description.to_string(),
|
||||
});
|
||||
|
||||
navigator.push(Route::ClientList {});
|
||||
dioxus_router::router().push(Route::ClientList {});
|
||||
},
|
||||
|
||||
fieldset {
|
||||
|
|
29
examples/dynamic_asset.rs
Normal file
29
examples/dynamic_asset.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
use dioxus::prelude::*;
|
||||
use dioxus_desktop::wry::http::Response;
|
||||
use dioxus_desktop::{use_asset_handler, AssetRequest};
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
dioxus_desktop::launch(app);
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
use_asset_handler(cx, |request: &AssetRequest| {
|
||||
let path = request.path().to_path_buf();
|
||||
async move {
|
||||
if path != Path::new("logo.png") {
|
||||
return None;
|
||||
}
|
||||
let image_data: &[u8] = include_bytes!("./assets/logo.png");
|
||||
Some(Response::new(image_data.into()))
|
||||
}
|
||||
});
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
img {
|
||||
src: "logo.png"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
use dioxus::prelude::*;
|
||||
use dioxus::{core::CapturedError, prelude::*};
|
||||
|
||||
fn main() {
|
||||
dioxus_desktop::launch(App);
|
||||
|
@ -6,30 +6,25 @@ fn main() {
|
|||
|
||||
#[component]
|
||||
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! {
|
||||
h1 { "The parsed value is {num}" }
|
||||
button {
|
||||
onclick: move |_| val.set("invalid"),
|
||||
"Set an invalid number"
|
||||
ErrorBoundary {
|
||||
handle_error: |error: CapturedError| rsx! {"Found error {error}"},
|
||||
DemoC {
|
||||
x: 1
|
||||
}
|
||||
}
|
||||
(0..5).map(|i| rsx! {
|
||||
DemoC { x: i }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn DemoC(cx: Scope, x: i32) -> Element {
|
||||
let result = Err("Error");
|
||||
|
||||
result.throw()?;
|
||||
|
||||
cx.render(rsx! {
|
||||
h1 {
|
||||
"asdasdasdasd {x}"
|
||||
"{x}"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,12 +5,8 @@ fn main() {
|
|||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let eval_provider = use_eval(cx);
|
||||
|
||||
let future = use_future(cx, (), |_| {
|
||||
to_owned![eval_provider];
|
||||
async move {
|
||||
let eval = eval_provider(
|
||||
let future = use_future(cx, (), |_| async move {
|
||||
let eval = eval(
|
||||
r#"
|
||||
dioxus.send("Hi from JS!");
|
||||
let msg = await dioxus.recv();
|
||||
|
@ -24,7 +20,6 @@ fn app(cx: Scope) -> Element {
|
|||
let res = eval.recv().await.unwrap();
|
||||
println!("{:?}", eval.await);
|
||||
res
|
||||
}
|
||||
});
|
||||
|
||||
match future.value() {
|
||||
|
|
|
@ -8,8 +8,7 @@ fn main() {
|
|||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let onsubmit = move |evt: FormEvent| {
|
||||
cx.spawn(async move {
|
||||
let onsubmit = move |evt: FormEvent| async move {
|
||||
let resp = reqwest::Client::new()
|
||||
.post("http://localhost:8080/login")
|
||||
.form(&[
|
||||
|
@ -28,13 +27,11 @@ fn app(cx: Scope) -> Element {
|
|||
println!("Login failed - you need a login server running on localhost:8080.")
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
cx.render(rsx! {
|
||||
h1 { "Login" }
|
||||
form {
|
||||
onsubmit: onsubmit,
|
||||
form { onsubmit: onsubmit,
|
||||
input { r#type: "text", id: "username", name: "username" }
|
||||
label { "Username" }
|
||||
br {}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -5,14 +5,12 @@ fn main() {
|
|||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let window = dioxus_desktop::use_window(cx);
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
button {
|
||||
onclick: move |_| {
|
||||
let dom = VirtualDom::new(popup);
|
||||
window.new_window(dom, Default::default());
|
||||
dioxus_desktop::window().new_window(dom, Default::default());
|
||||
},
|
||||
"New Window"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
use dioxus::prelude::*;
|
||||
use dioxus_desktop::{tao::dpi::PhysicalPosition, use_window, LogicalSize, WindowBuilder};
|
||||
use dioxus_desktop::{tao::dpi::PhysicalPosition, LogicalSize, WindowBuilder};
|
||||
|
||||
fn main() {
|
||||
dioxus_desktop::launch_cfg(app, make_config());
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let window = use_window(cx);
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
width: "100%",
|
||||
|
@ -19,7 +17,7 @@ fn app(cx: Scope) -> Element {
|
|||
width: "100%",
|
||||
height: "10px",
|
||||
background_color: "black",
|
||||
onmousedown: move |_| window.drag(),
|
||||
onmousedown: move |_| dioxus_desktop::window().drag(),
|
||||
}
|
||||
|
||||
"This is an overlay!"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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 {
|
||||
|
@ -114,7 +111,10 @@ pub fn TodoHeader<'a>(cx: Scope<'a, TodoHeaderProps<'a>>) -> Element {
|
|||
},
|
||||
onkeydown: move |evt| {
|
||||
if evt.key() == Key::Enter && !draft.is_empty() {
|
||||
cx.props.todos.make_mut().insert(
|
||||
cx.props
|
||||
.todos
|
||||
.make_mut()
|
||||
.insert(
|
||||
**todo_id,
|
||||
TodoItem {
|
||||
id: **todo_id,
|
||||
|
@ -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!{
|
||||
|
@ -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" }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
184
examples/video_stream.rs
Normal file
184
examples/video_stream.rs
Normal file
|
@ -0,0 +1,184 @@
|
|||
use dioxus::prelude::*;
|
||||
use dioxus_desktop::wry::http;
|
||||
use dioxus_desktop::wry::http::Response;
|
||||
use dioxus_desktop::{use_asset_handler, AssetRequest};
|
||||
use http::{header::*, response::Builder as ResponseBuilder, status::StatusCode};
|
||||
use std::borrow::Cow;
|
||||
use std::{io::SeekFrom, path::PathBuf};
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::AsyncSeekExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
const VIDEO_PATH: &str = "./examples/assets/test_video.mp4";
|
||||
|
||||
fn main() {
|
||||
let video_file = PathBuf::from(VIDEO_PATH);
|
||||
if !video_file.exists() {
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(async move {
|
||||
println!("Downloading video file...");
|
||||
let video_url =
|
||||
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
|
||||
let mut response = reqwest::get(video_url).await.unwrap();
|
||||
let mut file = tokio::fs::File::create(&video_file).await.unwrap();
|
||||
while let Some(chunk) = response.chunk().await.unwrap() {
|
||||
file.write_all(&chunk).await.unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
dioxus_desktop::launch(app);
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
use_asset_handler(cx, move |request: &AssetRequest| {
|
||||
let request = request.clone();
|
||||
async move {
|
||||
let video_file = PathBuf::from(VIDEO_PATH);
|
||||
let mut file = tokio::fs::File::open(&video_file).await.unwrap();
|
||||
let response: Option<Response<Cow<'static, [u8]>>> =
|
||||
match get_stream_response(&mut file, &request).await {
|
||||
Ok(response) => Some(response.map(Cow::Owned)),
|
||||
Err(err) => {
|
||||
eprintln!("Error: {}", err);
|
||||
None
|
||||
}
|
||||
};
|
||||
response
|
||||
}
|
||||
});
|
||||
|
||||
render! {
|
||||
div { video { src: "test_video.mp4", autoplay: true, controls: true, width: 640, height: 480 } }
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_stream_response(
|
||||
asset: &mut (impl tokio::io::AsyncSeek + tokio::io::AsyncRead + Unpin + Send + Sync),
|
||||
request: &AssetRequest,
|
||||
) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error>> {
|
||||
// get stream length
|
||||
let len = {
|
||||
let old_pos = asset.stream_position().await?;
|
||||
let len = asset.seek(SeekFrom::End(0)).await?;
|
||||
asset.seek(SeekFrom::Start(old_pos)).await?;
|
||||
len
|
||||
};
|
||||
|
||||
let mut resp = ResponseBuilder::new().header(CONTENT_TYPE, "video/mp4");
|
||||
|
||||
// if the webview sent a range header, we need to send a 206 in return
|
||||
// Actually only macOS and Windows are supported. Linux will ALWAYS return empty headers.
|
||||
let http_response = if let Some(range_header) = request.headers().get("range") {
|
||||
let not_satisfiable = || {
|
||||
ResponseBuilder::new()
|
||||
.status(StatusCode::RANGE_NOT_SATISFIABLE)
|
||||
.header(CONTENT_RANGE, format!("bytes */{len}"))
|
||||
.body(vec![])
|
||||
};
|
||||
|
||||
// parse range header
|
||||
let ranges = if let Ok(ranges) = http_range::HttpRange::parse(range_header.to_str()?, len) {
|
||||
ranges
|
||||
.iter()
|
||||
// map the output back to spec range <start-end>, example: 0-499
|
||||
.map(|r| (r.start, r.start + r.length - 1))
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
return Ok(not_satisfiable()?);
|
||||
};
|
||||
|
||||
/// The Maximum bytes we send in one range
|
||||
const MAX_LEN: u64 = 1000 * 1024;
|
||||
|
||||
if ranges.len() == 1 {
|
||||
let &(start, mut end) = ranges.first().unwrap();
|
||||
|
||||
// check if a range is not satisfiable
|
||||
//
|
||||
// this should be already taken care of by HttpRange::parse
|
||||
// but checking here again for extra assurance
|
||||
if start >= len || end >= len || end < start {
|
||||
return Ok(not_satisfiable()?);
|
||||
}
|
||||
|
||||
// adjust end byte for MAX_LEN
|
||||
end = start + (end - start).min(len - start).min(MAX_LEN - 1);
|
||||
|
||||
// calculate number of bytes needed to be read
|
||||
let bytes_to_read = end + 1 - start;
|
||||
|
||||
// allocate a buf with a suitable capacity
|
||||
let mut buf = Vec::with_capacity(bytes_to_read as usize);
|
||||
// seek the file to the starting byte
|
||||
asset.seek(SeekFrom::Start(start)).await?;
|
||||
// read the needed bytes
|
||||
asset.take(bytes_to_read).read_to_end(&mut buf).await?;
|
||||
|
||||
resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}"));
|
||||
resp = resp.header(CONTENT_LENGTH, end + 1 - start);
|
||||
resp = resp.status(StatusCode::PARTIAL_CONTENT);
|
||||
resp.body(buf)
|
||||
} else {
|
||||
let mut buf = Vec::new();
|
||||
let ranges = ranges
|
||||
.iter()
|
||||
.filter_map(|&(start, mut end)| {
|
||||
// filter out unsatisfiable ranges
|
||||
//
|
||||
// this should be already taken care of by HttpRange::parse
|
||||
// but checking here again for extra assurance
|
||||
if start >= len || end >= len || end < start {
|
||||
None
|
||||
} else {
|
||||
// adjust end byte for MAX_LEN
|
||||
end = start + (end - start).min(len - start).min(MAX_LEN - 1);
|
||||
Some((start, end))
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let boundary = format!("{:x}", rand::random::<u64>());
|
||||
let boundary_sep = format!("\r\n--{boundary}\r\n");
|
||||
let boundary_closer = format!("\r\n--{boundary}\r\n");
|
||||
|
||||
resp = resp.header(
|
||||
CONTENT_TYPE,
|
||||
format!("multipart/byteranges; boundary={boundary}"),
|
||||
);
|
||||
|
||||
for (end, start) in ranges {
|
||||
// a new range is being written, write the range boundary
|
||||
buf.write_all(boundary_sep.as_bytes()).await?;
|
||||
|
||||
// write the needed headers `Content-Type` and `Content-Range`
|
||||
buf.write_all(format!("{CONTENT_TYPE}: video/mp4\r\n").as_bytes())
|
||||
.await?;
|
||||
buf.write_all(format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes())
|
||||
.await?;
|
||||
|
||||
// write the separator to indicate the start of the range body
|
||||
buf.write_all("\r\n".as_bytes()).await?;
|
||||
|
||||
// calculate number of bytes needed to be read
|
||||
let bytes_to_read = end + 1 - start;
|
||||
|
||||
let mut local_buf = vec![0_u8; bytes_to_read as usize];
|
||||
asset.seek(SeekFrom::Start(start)).await?;
|
||||
asset.read_exact(&mut local_buf).await?;
|
||||
buf.extend_from_slice(&local_buf);
|
||||
}
|
||||
// all ranges have been written, write the closing boundary
|
||||
buf.write_all(boundary_closer.as_bytes()).await?;
|
||||
|
||||
resp.body(buf)
|
||||
}
|
||||
} else {
|
||||
resp = resp.header(CONTENT_LENGTH, len);
|
||||
let mut buf = Vec::with_capacity(len as usize);
|
||||
asset.read_to_end(&mut buf).await?;
|
||||
resp.body(buf)
|
||||
};
|
||||
|
||||
http_response.map_err(Into::into)
|
||||
}
|
|
@ -1,12 +1,10 @@
|
|||
use dioxus::prelude::*;
|
||||
use dioxus_desktop::use_window;
|
||||
|
||||
fn main() {
|
||||
dioxus_desktop::launch(app);
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let window = use_window(cx);
|
||||
let level = use_state(cx, || 1.0);
|
||||
|
||||
cx.render(rsx! {
|
||||
|
@ -16,7 +14,7 @@ fn app(cx: Scope) -> Element {
|
|||
oninput: |e| {
|
||||
if let Ok(new_zoom) = e.value().parse::<f64>() {
|
||||
level.set(new_zoom);
|
||||
window.webview.zoom(new_zoom);
|
||||
dioxus_desktop::window().webview.zoom(new_zoom);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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)?;
|
||||
}
|
||||
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 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, " ")?
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
108
packages/autofmt/src/indent.rs
Normal file
108
packages/autofmt/src/indent.rs
Normal file
|
@ -0,0 +1,108 @@
|
|||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum IndentType {
|
||||
Spaces,
|
||||
Tabs,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IndentOptions {
|
||||
width: usize,
|
||||
indent_string: String,
|
||||
}
|
||||
|
||||
impl IndentOptions {
|
||||
pub fn new(typ: IndentType, width: usize) -> Self {
|
||||
assert_ne!(width, 0, "Cannot have an indent width of 0");
|
||||
Self {
|
||||
width,
|
||||
indent_string: match typ {
|
||||
IndentType::Tabs => "\t".into(),
|
||||
IndentType::Spaces => " ".repeat(width),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a string containing one indent worth of whitespace
|
||||
pub fn indent_str(&self) -> &str {
|
||||
&self.indent_string
|
||||
}
|
||||
|
||||
/// Computes the line length in characters, counting tabs as the indent width.
|
||||
pub fn line_length(&self, line: &str) -> usize {
|
||||
line.chars()
|
||||
.map(|ch| if ch == '\t' { self.width } else { 1 })
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Estimates how many times the line has been indented.
|
||||
pub fn count_indents(&self, mut line: &str) -> usize {
|
||||
let mut indent = 0;
|
||||
while !line.is_empty() {
|
||||
// Try to count tabs
|
||||
let num_tabs = line.chars().take_while(|ch| *ch == '\t').count();
|
||||
if num_tabs > 0 {
|
||||
indent += num_tabs;
|
||||
line = &line[num_tabs..];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to count spaces
|
||||
let num_spaces = line.chars().take_while(|ch| *ch == ' ').count();
|
||||
if num_spaces >= self.width {
|
||||
// Intentionally floor here to take only the amount of space that matches an indent
|
||||
let num_space_indents = num_spaces / self.width;
|
||||
indent += num_space_indents;
|
||||
line = &line[num_space_indents * self.width..];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Line starts with either non-indent characters or an unevent amount of spaces,
|
||||
// so no more indent remains.
|
||||
break;
|
||||
}
|
||||
indent
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for IndentOptions {
|
||||
fn default() -> Self {
|
||||
Self::new(IndentType::Spaces, 4)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn count_indents() {
|
||||
assert_eq!(
|
||||
IndentOptions::new(IndentType::Spaces, 4).count_indents("no indentation here!"),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
IndentOptions::new(IndentType::Spaces, 4).count_indents(" v += 2"),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
IndentOptions::new(IndentType::Spaces, 4).count_indents(" v += 2"),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
IndentOptions::new(IndentType::Spaces, 4).count_indents(" v += 2"),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
IndentOptions::new(IndentType::Spaces, 4).count_indents("\t\tv += 2"),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
IndentOptions::new(IndentType::Spaces, 4).count_indents("\t\t v += 2"),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
IndentOptions::new(IndentType::Spaces, 2).count_indents(" v += 2"),
|
||||
2
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<FormattedBlock> {
|
||||
pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
|
||||
let mut formatted_blocks = Vec::new();
|
||||
|
||||
let parsed = syn::parse_file(contents).unwrap();
|
||||
|
@ -61,6 +64,7 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
|
|||
}
|
||||
|
||||
let mut writer = Writer::new(contents);
|
||||
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<FormattedBlock> {
|
|||
|
||||
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<String> {
|
|||
buf.consume()
|
||||
}
|
||||
|
||||
pub fn fmt_block(block: &str, indent_level: usize) -> Option<String> {
|
||||
pub fn fmt_block(block: &str, indent_level: usize, indent: IndentOptions) -> Option<String> {
|
||||
let body = syn::parse_str::<dioxus_rsx::CallBody>(block).unwrap();
|
||||
|
||||
let 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()
|
||||
}
|
||||
|
|
|
@ -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,34 +132,17 @@ impl<'a> Writer<'a> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn is_short_attrs(&mut self, attributes: &[ElementAttrNamed]) -> usize {
|
||||
let mut total = 0;
|
||||
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);
|
||||
|
||||
for attr in attributes {
|
||||
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,
|
||||
condition_len + value_len + 6
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 } => {
|
||||
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) =
|
||||
|
@ -177,9 +160,36 @@ impl<'a> Writer<'a> {
|
|||
self.cached_formats[&location].len()
|
||||
};
|
||||
|
||||
len + name.span().line_length() + 6
|
||||
len
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_short_attrs(&mut self, attributes: &[ElementAttrNamed]) -> usize {
|
||||
let mut total = 0;
|
||||
|
||||
for attr in attributes {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
|
@ -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", "");
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)));
|
||||
|
|
7
packages/autofmt/tests/wrong/comments-tab.rsx
Normal file
7
packages/autofmt/tests/wrong/comments-tab.rsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
rsx! {
|
||||
div {
|
||||
// Comments
|
||||
class: "asdasd",
|
||||
"hello world"
|
||||
}
|
||||
}
|
5
packages/autofmt/tests/wrong/comments-tab.wrong.rsx
Normal file
5
packages/autofmt/tests/wrong/comments-tab.wrong.rsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
rsx! {
|
||||
div {
|
||||
// Comments
|
||||
class: "asdasd", "hello world" }
|
||||
}
|
3
packages/autofmt/tests/wrong/multi-tab.rsx
Normal file
3
packages/autofmt/tests/wrong/multi-tab.rsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx! { div { "hello world" } })
|
||||
}
|
5
packages/autofmt/tests/wrong/multi-tab.wrong.rsx
Normal file
5
packages/autofmt/tests/wrong/multi-tab.wrong.rsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
div {"hello world" }
|
||||
})
|
||||
}
|
8
packages/autofmt/tests/wrong/multiexpr-tab.rsx
Normal file
8
packages/autofmt/tests/wrong/multiexpr-tab.rsx
Normal file
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
5
packages/autofmt/tests/wrong/multiexpr-tab.wrong.rsx
Normal file
5
packages/autofmt/tests/wrong/multiexpr-tab.wrong.rsx
Normal file
|
@ -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 }
|
||||
})
|
||||
}
|
|
@ -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 = []
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
||||
|
|
|
@ -48,14 +48,25 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
|
|||
|
||||
// [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<BuildResult> {
|
|||
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<BuildResult> {
|
|||
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),
|
||||
|
@ -254,7 +267,6 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
|
|||
let mut cmd = subprocess::Exec::cmd("cargo")
|
||||
.cwd(&config.crate_dir)
|
||||
.arg("build")
|
||||
.arg("--quiet")
|
||||
.arg("--message-format=json");
|
||||
|
||||
if config.release {
|
||||
|
@ -262,6 +274,8 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
|
|||
}
|
||||
if config.verbose {
|
||||
cmd = cmd.arg("--verbose");
|
||||
} else {
|
||||
cmd = cmd.arg("--quiet");
|
||||
}
|
||||
|
||||
if config.custom_profile.is_some() {
|
||||
|
@ -274,6 +288,14 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
|
|||
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 {
|
||||
crate::ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
|
||||
crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
|
||||
|
@ -291,12 +313,17 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
|
|||
let mut res_path = match &config.executable {
|
||||
crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => {
|
||||
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)
|
||||
|
@ -469,7 +496,7 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
|
|||
.unwrap_or_default()
|
||||
.contains_key("tailwindcss")
|
||||
{
|
||||
style_str.push_str("<link rel=\"stylesheet\" href=\"tailwind.css\">\n");
|
||||
style_str.push_str("<link rel=\"stylesheet\" href=\"/{base_path}/tailwind.css\">\n");
|
||||
}
|
||||
|
||||
replace_or_insert_before("{style_include}", &style_str, "</head", &mut html);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use dioxus_autofmt::{IndentOptions, IndentType};
|
||||
use futures::{stream::FuturesUnordered, StreamExt};
|
||||
use std::{fs, path::Path, process::exit};
|
||||
|
||||
|
@ -26,16 +27,19 @@ pub struct Autoformat {
|
|||
impl Autoformat {
|
||||
// Todo: autoformat the entire crate
|
||||
pub async fn autoformat(self) -> 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,7 +49,16 @@ impl Autoformat {
|
|||
}
|
||||
|
||||
// Format single file
|
||||
if let Some(file) = self.file {
|
||||
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)?;
|
||||
|
@ -53,33 +66,71 @@ impl Autoformat {
|
|||
} else {
|
||||
fs::read_to_string(&file)
|
||||
};
|
||||
|
||||
match file_content {
|
||||
Ok(s) => {
|
||||
let edits = dioxus_autofmt::fmt_file(&s);
|
||||
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 {
|
||||
match fs::write(&file, out) {
|
||||
Ok(_) => {
|
||||
println!("formatted {}", file);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("failed to write formatted content to file: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("failed to open file: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_project_files(config: &CrateConfig) -> Vec<PathBuf> {
|
||||
let mut files = vec![];
|
||||
|
||||
let gitignore_path = config.crate_dir.join(".gitignore");
|
||||
if gitignore_path.is_file() {
|
||||
let gitigno = gitignore::File::new(gitignore_path.as_path()).unwrap();
|
||||
if let Ok(git_files) = gitigno.included_files() {
|
||||
let git_files = git_files
|
||||
.into_iter()
|
||||
.filter(|f| f.ends_with(".rs") && !is_target_dir(f));
|
||||
files.extend(git_files)
|
||||
};
|
||||
} else {
|
||||
collect_rs_files(&config.crate_dir, &mut files);
|
||||
}
|
||||
|
||||
files
|
||||
}
|
||||
|
||||
fn is_target_dir(file: &Path) -> bool {
|
||||
let stripped = if let Ok(cwd) = std::env::current_dir() {
|
||||
file.strip_prefix(cwd).unwrap_or(file)
|
||||
} else {
|
||||
file
|
||||
};
|
||||
if let Some(first) = stripped.components().next() {
|
||||
first.as_os_str() == "target"
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
async fn format_file(
|
||||
path: impl AsRef<Path>,
|
||||
indent: IndentOptions,
|
||||
) -> Result<usize, tokio::io::Error> {
|
||||
let contents = tokio::fs::read_to_string(&path).await?;
|
||||
|
||||
let edits = dioxus_autofmt::fmt_file(&contents, indent);
|
||||
let len = edits.len();
|
||||
|
||||
if !edits.is_empty() {
|
||||
let out = dioxus_autofmt::apply_formats(&contents, edits);
|
||||
tokio::fs::write(path, out).await?;
|
||||
}
|
||||
|
||||
Ok(len)
|
||||
}
|
||||
|
||||
/// Read every .rs file accessible when considering the .gitignore and try to format it
|
||||
|
@ -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<usize, tokio::io::Error>
|
||||
})
|
||||
.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::<Vec<_>>()
|
||||
.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<PathBuf>) {
|
||||
let Ok(folder) = folder.read_dir() else {
|
||||
fn indentation_for(file_or_dir: impl AsRef<Path>) -> Result<IndentOptions> {
|
||||
let out = std::process::Command::new("cargo")
|
||||
.args(["fmt", "--", "--print-config", "current"])
|
||||
.arg(file_or_dir.as_ref())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::inherit())
|
||||
.output()?;
|
||||
if !out.status.success() {
|
||||
return Err(Error::CargoError("cargo fmt failed".into()));
|
||||
}
|
||||
|
||||
let config = String::from_utf8_lossy(&out.stdout);
|
||||
|
||||
let hard_tabs = config
|
||||
.lines()
|
||||
.find(|line| line.starts_with("hard_tabs "))
|
||||
.and_then(|line| line.split_once('='))
|
||||
.map(|(_, value)| value.trim() == "true")
|
||||
.ok_or_else(|| {
|
||||
Error::RuntimeError("Could not find hard_tabs option in rustfmt config".into())
|
||||
})?;
|
||||
let tab_spaces = config
|
||||
.lines()
|
||||
.find(|line| line.starts_with("tab_spaces "))
|
||||
.and_then(|line| line.split_once('='))
|
||||
.map(|(_, value)| value.trim().parse::<usize>())
|
||||
.ok_or_else(|| {
|
||||
Error::RuntimeError("Could not find tab_spaces option in rustfmt config".into())
|
||||
})?
|
||||
.map_err(|_| {
|
||||
Error::RuntimeError("Could not parse tab_spaces option in rustfmt config".into())
|
||||
})?;
|
||||
|
||||
Ok(IndentOptions::new(
|
||||
if hard_tabs {
|
||||
IndentType::Tabs
|
||||
} else {
|
||||
IndentType::Spaces
|
||||
},
|
||||
tab_spaces,
|
||||
))
|
||||
}
|
||||
|
||||
fn collect_rs_files(folder: &impl AsRef<Path>, files: &mut Vec<PathBuf>) {
|
||||
if is_target_dir(folder.as_ref()) {
|
||||
return;
|
||||
}
|
||||
let Ok(folder) = folder.as_ref().read_dir() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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<PathBuf>,
|
||||
|
||||
/// 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<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>,
|
||||
}
|
||||
|
||||
#[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<PathBuf>,
|
||||
|
||||
/// 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<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>,
|
||||
}
|
||||
|
||||
#[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<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>,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -211,6 +211,8 @@ pub struct CrateConfig {
|
|||
pub verbose: bool,
|
||||
pub custom_profile: Option<String>,
|
||||
pub features: Option<Vec<String>>,
|
||||
pub target: Option<String>,
|
||||
pub cargo_args: Vec<String>,
|
||||
}
|
||||
|
||||
#[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<String>) -> &mut Self {
|
||||
self.cargo_args = cargo_args;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
|
|
|
@ -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),
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -9,42 +9,31 @@ use dioxus_cli::plugin::PluginManager;
|
|||
|
||||
use Commands::*;
|
||||
|
||||
fn get_bin(bin: Option<String>) -> Result<Option<PathBuf>> {
|
||||
const ERR_MESSAGE: &str = "The `--bin` flag has to be ran in a Cargo workspace.";
|
||||
|
||||
if let Some(ref bin) = bin {
|
||||
let manifest = cargo_toml::Manifest::from_path("./Cargo.toml")
|
||||
.map_err(|_| Error::CargoError(ERR_MESSAGE.to_string()))?;
|
||||
|
||||
if let Some(workspace) = manifest.workspace {
|
||||
for item in workspace.members.iter() {
|
||||
let path = PathBuf::from(item);
|
||||
|
||||
if !path.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if path.ends_with(bin.clone()) {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
}
|
||||
fn get_bin(bin: Option<String>) -> Result<PathBuf> {
|
||||
let metadata = cargo_metadata::MetadataCommand::new()
|
||||
.exec()
|
||||
.map_err(Error::CargoMetadata)?;
|
||||
let package = if let Some(bin) = bin {
|
||||
metadata
|
||||
.workspace_packages()
|
||||
.into_iter()
|
||||
.find(|p| p.name == bin)
|
||||
.ok_or(format!("no such package: {}", bin))
|
||||
.map_err(Error::CargoError)?
|
||||
} else {
|
||||
return Err(Error::CargoError(ERR_MESSAGE.to_string()));
|
||||
}
|
||||
}
|
||||
metadata
|
||||
.root_package()
|
||||
.ok_or("no root package?".into())
|
||||
.map_err(Error::CargoError)?
|
||||
};
|
||||
|
||||
// If the bin exists but we couldn't find it
|
||||
if bin.is_some() {
|
||||
return Err(Error::CargoError(
|
||||
"The specified bin does not exist.".to_string(),
|
||||
));
|
||||
}
|
||||
let crate_dir = package
|
||||
.manifest_path
|
||||
.parent()
|
||||
.ok_or("couldn't take parent dir".into())
|
||||
.map_err(Error::CargoError)?;
|
||||
|
||||
Ok(None)
|
||||
Ok(crate_dir.into())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
|
@ -53,9 +42,10 @@ async fn main() -> anyhow::Result<()> {
|
|||
|
||||
set_up_logging();
|
||||
|
||||
let bin = get_bin(args.bin)?;
|
||||
let bin = get_bin(args.bin);
|
||||
|
||||
let _dioxus_config = DioxusConfig::load(bin.clone())
|
||||
if let Ok(bin) = &bin {
|
||||
let _dioxus_config = DioxusConfig::load(Some(bin.clone()))
|
||||
.map_err(|e| anyhow!("Failed to load Dioxus config because: {e}"))?
|
||||
.unwrap_or_else(|| {
|
||||
log::warn!("You appear to be creating a Dioxus project from scratch; we will use the default config");
|
||||
|
@ -65,22 +55,23 @@ async fn main() -> anyhow::Result<()> {
|
|||
#[cfg(feature = "plugin")]
|
||||
PluginManager::init(_dioxus_config.plugin)
|
||||
.map_err(|e| anyhow!("🚫 Plugin system initialization failed: {e}"))?;
|
||||
}
|
||||
|
||||
match args.action {
|
||||
Translate(opts) => opts
|
||||
.translate()
|
||||
.map_err(|e| anyhow!("🚫 Translation of HTML into RSX failed: {}", e)),
|
||||
|
||||
Build(opts) => opts
|
||||
.build(bin.clone())
|
||||
Build(opts) if bin.is_ok() => opts
|
||||
.build(Some(bin.unwrap().clone()))
|
||||
.map_err(|e| anyhow!("🚫 Building project failed: {}", e)),
|
||||
|
||||
Clean(opts) => opts
|
||||
.clean(bin.clone())
|
||||
Clean(opts) if bin.is_ok() => opts
|
||||
.clean(Some(bin.unwrap().clone()))
|
||||
.map_err(|e| anyhow!("🚫 Cleaning project failed: {}", e)),
|
||||
|
||||
Serve(opts) => opts
|
||||
.serve(bin.clone())
|
||||
Serve(opts) if bin.is_ok() => opts
|
||||
.serve(Some(bin.unwrap().clone()))
|
||||
.await
|
||||
.map_err(|e| anyhow!("🚫 Serving project failed: {}", e)),
|
||||
|
||||
|
@ -92,8 +83,8 @@ async fn main() -> anyhow::Result<()> {
|
|||
.config()
|
||||
.map_err(|e| anyhow!("🚫 Configuring new project failed: {}", e)),
|
||||
|
||||
Bundle(opts) => opts
|
||||
.bundle(bin.clone())
|
||||
Bundle(opts) if bin.is_ok() => opts
|
||||
.bundle(Some(bin.unwrap().clone()))
|
||||
.map_err(|e| anyhow!("🚫 Bundling project failed: {}", e)),
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
|
@ -118,5 +109,6 @@ async fn main() -> anyhow::Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(anyhow::anyhow!(bin.unwrap_err())),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,8 +43,6 @@ pub async fn startup(config: CrateConfig) -> Result<()> {
|
|||
|
||||
let hot_reload_tx = broadcast::channel(100).0;
|
||||
|
||||
clear_paths();
|
||||
|
||||
Some(HotReloadState {
|
||||
messages: hot_reload_tx.clone(),
|
||||
file_map: file_map.clone(),
|
||||
|
@ -73,6 +71,7 @@ pub async fn serve(config: CrateConfig, hot_reload_state: Option<HotReloadState>
|
|||
|
||||
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;
|
||||
|
@ -109,7 +108,14 @@ pub async fn serve(config: CrateConfig, hot_reload_state: Option<HotReloadState>
|
|||
}
|
||||
|
||||
async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()> {
|
||||
match LocalSocketListener::bind("@dioxusin") {
|
||||
let metadata = cargo_metadata::MetadataCommand::new()
|
||||
.no_deps()
|
||||
.exec()
|
||||
.unwrap();
|
||||
let target_dir = metadata.target_directory.as_std_path();
|
||||
let path = target_dir.join("dioxusin");
|
||||
clear_paths(&path);
|
||||
match LocalSocketListener::bind(path) {
|
||||
Ok(local_socket_stream) => {
|
||||
let aborted = Arc::new(Mutex::new(false));
|
||||
// States
|
||||
|
@ -121,9 +127,9 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
|
|||
let file_map = hot_reload_state.file_map.clone();
|
||||
let channels = channels.clone();
|
||||
let aborted = aborted.clone();
|
||||
let _ = local_socket_stream.set_nonblocking(true);
|
||||
move || {
|
||||
loop {
|
||||
//accept() will block the thread when local_socket_stream is in blocking mode (default)
|
||||
match local_socket_stream.accept() {
|
||||
Ok(mut connection) => {
|
||||
// send any templates than have changed before the socket connected
|
||||
|
@ -148,7 +154,11 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
|
|||
println!("Connected to hot reloading 🚀");
|
||||
}
|
||||
Err(err) => {
|
||||
if err.kind() != std::io::ErrorKind::WouldBlock {
|
||||
let error_string = err.to_string();
|
||||
// Filter out any error messages about a operation that may block and an error message that triggers on some operating systems that says "Waiting for a process to open the other end of the pipe" without WouldBlock being set
|
||||
let display_error = err.kind() != std::io::ErrorKind::WouldBlock
|
||||
&& !error_string.contains("Waiting for a process");
|
||||
if display_error {
|
||||
println!("Error connecting to hot reloading: {} (Hot reloading is a feature of the dioxus-cli. If you are not using the CLI, this error can be ignored)", err);
|
||||
}
|
||||
}
|
||||
|
@ -181,17 +191,14 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_paths() {
|
||||
fn clear_paths(file_socket_path: &std::path::Path) {
|
||||
if cfg!(target_os = "macos") {
|
||||
// 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
|
||||
// We check if the file socket is already open from an old session and then delete it
|
||||
let paths = ["./dioxusin", "./@dioxusin"];
|
||||
for path in paths {
|
||||
let path = std::path::PathBuf::from(path);
|
||||
if path.exists() {
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
if file_socket_path.exists() {
|
||||
let _ = std::fs::remove_file(file_socket_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -212,6 +219,7 @@ fn send_msg(msg: HotReloadMsg, channel: &mut impl std::io::Write) -> bool {
|
|||
|
||||
pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> {
|
||||
// Run the desktop application
|
||||
log::trace!("Building application");
|
||||
let result = crate::builder::build_desktop(config, true)?;
|
||||
|
||||
match &config.executable {
|
||||
|
@ -222,6 +230,7 @@ pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> {
|
|||
if cfg!(windows) {
|
||||
file.set_extension("exe");
|
||||
}
|
||||
log::trace!("Running application from {:?}", file);
|
||||
let child = Command::new(file.to_str().unwrap()).spawn()?;
|
||||
|
||||
Ok((child, result))
|
||||
|
|
|
@ -55,6 +55,16 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
|
|||
break;
|
||||
}
|
||||
|
||||
// Workaround for notify and vscode-like editor:
|
||||
// when edit & save a file in vscode, there will be two notifications,
|
||||
// the first one is a file with empty content.
|
||||
// filter the empty file notification to avoid false rebuild during hot-reload
|
||||
if let Ok(metadata) = fs::metadata(path) {
|
||||
if metadata.len() == 0 {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
match rsx_file_map.update_rsx(path, &config.crate_dir) {
|
||||
Ok(UpdateResult::UpdatedRsx(msgs)) => {
|
||||
messages.extend(msgs);
|
||||
|
|
|
@ -22,6 +22,8 @@ pub fn print_console_info(
|
|||
options: PrettierOptions,
|
||||
web_info: Option<WebServerInfo>,
|
||||
) {
|
||||
// Don't clear the screen if the user has set the DIOXUS_LOG environment variable to "trace" so that we can see the logs
|
||||
if Some("trace") != std::env::var("DIOXUS_LOG").ok().as_deref() {
|
||||
if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") {
|
||||
"cls"
|
||||
} else {
|
||||
|
@ -34,6 +36,7 @@ pub fn print_console_info(
|
|||
// Try ANSI-Escape characters
|
||||
print!("\x1b[2J\x1b[H");
|
||||
}
|
||||
}
|
||||
|
||||
let mut profile = if config.release { "Release" } else { "Debug" }.to_string();
|
||||
if config.custom_profile.is_some() {
|
||||
|
|
|
@ -11,6 +11,7 @@ use axum::{
|
|||
body::{Full, HttpBody},
|
||||
extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
|
||||
http::{
|
||||
self,
|
||||
header::{HeaderName, HeaderValue},
|
||||
Method, Response, StatusCode,
|
||||
},
|
||||
|
@ -262,7 +263,7 @@ async fn setup_router(
|
|||
.override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
|
||||
.and_then(
|
||||
move |response: Response<ServeFileSystemResponseBody>| async move {
|
||||
let response = if file_service_config
|
||||
let mut response = if file_service_config
|
||||
.dioxus_config
|
||||
.web
|
||||
.watcher
|
||||
|
@ -290,6 +291,13 @@ async fn setup_router(
|
|||
} else {
|
||||
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)
|
||||
},
|
||||
)
|
||||
|
|
|
@ -31,6 +31,7 @@ fn get_out_comp_fn(orig_comp_fn: &ItemFn, cx_pat: &Pat) -> ItemFn {
|
|||
block: parse_quote! {
|
||||
{
|
||||
#[warn(non_snake_case)]
|
||||
#[allow(clippy::inline_always)]
|
||||
#[inline(always)]
|
||||
#inner_comp_fn
|
||||
#inner_comp_ident (#cx_pat)
|
||||
|
@ -57,8 +58,11 @@ impl ToTokens for ComponentDeserializerOutput {
|
|||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let comp_fn = &self.comp_fn;
|
||||
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! {
|
||||
#[doc = #doc]
|
||||
#props_struct
|
||||
#[allow(non_snake_case)]
|
||||
#comp_fn
|
||||
|
|
|
@ -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,
|
||||
|
@ -785,19 +779,9 @@ 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<...>`",
|
||||
)
|
||||
})?
|
||||
} else {
|
||||
field_type
|
||||
};
|
||||
let (arg_type, arg_expr) = if field.builder_attr.auto_into {
|
||||
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()),
|
||||
|
@ -805,11 +789,6 @@ Finally, call `.build()` to create the instance of `{name}`.
|
|||
} else {
|
||||
(quote!(#arg_type), quote!(#field_name))
|
||||
};
|
||||
let arg_expr = if field.builder_attr.strip_option {
|
||||
quote!(Some(#arg_expr))
|
||||
} else {
|
||||
arg_expr
|
||||
};
|
||||
|
||||
let repeated_fields_error_type_name = syn::Ident::new(
|
||||
&format!(
|
||||
|
@ -825,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;
|
||||
|
@ -843,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
|
||||
}
|
||||
|
|
|
@ -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<NonNull<VNode<'static>>>,
|
||||
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_vnode_ref(&mut self, vnode: &VNode) -> VNodeId {
|
||||
let new_id = VNodeId(self.element_refs.insert(Some(unsafe {
|
||||
std::mem::transmute::<NonNull<VNode>, _>(vnode.into())
|
||||
})));
|
||||
|
||||
// Set this id to be dropped when the scope is rerun
|
||||
if let Some(scope) = self.runtime.current_scope_id() {
|
||||
self.scopes[scope.0]
|
||||
.element_refs_to_drop
|
||||
.borrow_mut()
|
||||
.push(new_id);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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<ElementRef> {
|
||||
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::<NonNull<VNode>, _>(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,7 +169,9 @@ 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_before_render.borrow_mut();
|
||||
|
@ -176,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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<impl Iterator<Item = (usize, &'static [u8])>>,
|
||||
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::<usize>().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<ElementRef>,
|
||||
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<ElementRef>) -> usize {
|
||||
let id = self.next_element();
|
||||
self.mutations.push(Mutation::CreatePlaceholder { id });
|
||||
placeholder.id.set(Some(id));
|
||||
placeholder.parent.set(parent);
|
||||
|
||||
1
|
||||
}
|
||||
|
||||
fn set_slot(
|
||||
&mut self,
|
||||
template: &'b VNode<'b>,
|
||||
slot: &'b Cell<Option<ElementId>>,
|
||||
id: usize,
|
||||
) -> ElementId {
|
||||
let id = self.next_element(template, template.template.get().node_paths[id]);
|
||||
fn set_slot(&mut self, slot: &'b Cell<Option<ElementId>>) -> ElementId {
|
||||
let id = self.next_element();
|
||||
slot.set(Some(id));
|
||||
id
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
use crate::{
|
||||
any_props::AnyProps,
|
||||
arena::ElementId,
|
||||
innerlude::{BorrowedAttributeValue, DirtyScope, VComponent, VPlaceholder, VText},
|
||||
innerlude::{
|
||||
BorrowedAttributeValue, DirtyScope, ElementPath, ElementRef, VComponent, VPlaceholder,
|
||||
VText,
|
||||
},
|
||||
mutations::Mutation,
|
||||
nodes::RenderReturn,
|
||||
nodes::{DynamicNode, VNode},
|
||||
|
@ -39,19 +42,27 @@ impl<'b> VirtualDom {
|
|||
(Ready(l), Aborted(p)) => self.diff_ok_to_err(l, p),
|
||||
|
||||
// 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
|
||||
// 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();
|
||||
}
|
||||
|
||||
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.parent.set(l.parent.get());
|
||||
self.mutations.push(Mutation::CreatePlaceholder { id });
|
||||
|
||||
let pre_edits = self.mutations.edits.len();
|
||||
|
@ -81,12 +92,24 @@ impl<'b> VirtualDom {
|
|||
if let Some(&template) = map.get(&byte_index) {
|
||||
right_template.template.set(template);
|
||||
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 templates_are_the_same(left_template, right_template) {
|
||||
return;
|
||||
|
@ -105,12 +128,8 @@ impl<'b> VirtualDom {
|
|||
.zip(right_template.dynamic_attrs.iter())
|
||||
.for_each(|(left_attr, right_attr)| {
|
||||
// Move over the ID from the old to the new
|
||||
right_attr
|
||||
.mounted_element
|
||||
.set(left_attr.mounted_element.get());
|
||||
|
||||
// We want to make sure anything that gets pulled is valid
|
||||
self.update_template(left_attr.mounted_element.get(), right_template);
|
||||
let mounted_element = left_attr.mounted_element.get();
|
||||
right_attr.mounted_element.set(mounted_element);
|
||||
|
||||
// If the attributes are different (or volatile), we need to update them
|
||||
if left_attr.value != right_attr.value || left_attr.volatile {
|
||||
|
@ -123,8 +142,16 @@ impl<'b> VirtualDom {
|
|||
.dynamic_nodes
|
||||
.iter()
|
||||
.zip(right_template.dynamic_nodes.iter())
|
||||
.for_each(|(left_node, right_node)| {
|
||||
self.diff_dynamic_node(left_node, right_node, right_template);
|
||||
.enumerate()
|
||||
.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
|
||||
|
@ -135,30 +162,24 @@ impl<'b> VirtualDom {
|
|||
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_dynamic_node(
|
||||
&mut self,
|
||||
left_node: &'b DynamicNode<'b>,
|
||||
right_node: &'b DynamicNode<'b>,
|
||||
node: &'b VNode<'b>,
|
||||
parent: ElementRef,
|
||||
) {
|
||||
match (left_node, right_node) {
|
||||
(Text(left), Text(right)) => self.diff_vtext(left, right, node),
|
||||
(Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right),
|
||||
(Placeholder(left), Placeholder(right)) => right.id.set(left.id.get()),
|
||||
(Component(left), Component(right)) => self.diff_vcomponent(left, right, node),
|
||||
(Placeholder(left), Fragment(right)) => self.replace_placeholder(left, *right),
|
||||
(Fragment(left), Placeholder(right)) => self.node_to_placeholder(left, right),
|
||||
(Text(left), Text(right)) => self.diff_vtext(left, right),
|
||||
(Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right, parent),
|
||||
(Placeholder(left), Placeholder(right)) => {
|
||||
right.id.set(left.id.get());
|
||||
right.parent.set(left.parent.get());
|
||||
},
|
||||
(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."),
|
||||
};
|
||||
}
|
||||
|
@ -179,7 +200,7 @@ impl<'b> VirtualDom {
|
|||
&mut self,
|
||||
left: &'b VComponent<'b>,
|
||||
right: &'b VComponent<'b>,
|
||||
right_template: &'b VNode<'b>,
|
||||
parent: Option<ElementRef>,
|
||||
) {
|
||||
if std::ptr::eq(left, right) {
|
||||
return;
|
||||
|
@ -187,7 +208,7 @@ impl<'b> VirtualDom {
|
|||
|
||||
// Replace components that have different render fns
|
||||
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
|
||||
|
@ -228,11 +249,11 @@ impl<'b> VirtualDom {
|
|||
|
||||
fn replace_vcomponent(
|
||||
&mut self,
|
||||
right_template: &'b VNode<'b>,
|
||||
right: &'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();
|
||||
|
||||
|
@ -287,11 +308,12 @@ impl<'b> VirtualDom {
|
|||
/// }
|
||||
/// ```
|
||||
fn light_diff_templates(&mut self, left: &'b VNode<'b>, right: &'b VNode<'b>) {
|
||||
let parent = left.parent.take();
|
||||
match matching_components(left, right) {
|
||||
None => self.replace(left, [right]),
|
||||
None => self.replace(left, [right], parent),
|
||||
Some(components) => components
|
||||
.into_iter()
|
||||
.for_each(|(l, r)| self.diff_vcomponent(l, r, right)),
|
||||
.for_each(|(l, r)| self.diff_vcomponent(l, r, parent)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -299,11 +321,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
|
||||
/// different.
|
||||
fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>, node: &'b VNode<'b>) {
|
||||
let id = left
|
||||
.id
|
||||
.get()
|
||||
.unwrap_or_else(|| self.next_element(node, &[0]));
|
||||
fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>) {
|
||||
let id = left.id.get().unwrap_or_else(|| self.next_element());
|
||||
|
||||
right.id.set(Some(id));
|
||||
if left.value != right.value {
|
||||
|
@ -312,7 +331,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 old_is_keyed = old[0].key.is_some();
|
||||
debug_assert!(
|
||||
|
@ -325,9 +349,9 @@ impl<'b> VirtualDom {
|
|||
);
|
||||
|
||||
if new_is_keyed && old_is_keyed {
|
||||
self.diff_keyed_children(old, new);
|
||||
self.diff_keyed_children(old, new, parent);
|
||||
} else {
|
||||
self.diff_non_keyed_children(old, new);
|
||||
self.diff_non_keyed_children(old, new, parent);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -339,7 +363,12 @@ impl<'b> VirtualDom {
|
|||
// [... parent]
|
||||
//
|
||||
// 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;
|
||||
|
||||
// Handled these cases in `diff_children` before calling this function.
|
||||
|
@ -348,7 +377,9 @@ impl<'b> VirtualDom {
|
|||
|
||||
match old.len().cmp(&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 => {}
|
||||
}
|
||||
|
||||
|
@ -373,7 +404,12 @@ impl<'b> VirtualDom {
|
|||
// https://github.com/infernojs/inferno/blob/36fd96/packages/inferno/src/DOM/patching.ts#L530-L739
|
||||
//
|
||||
// The stack is empty upon entry.
|
||||
fn diff_keyed_children(&mut self, 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) {
|
||||
let mut keys = rustc_hash::FxHashSet::default();
|
||||
let mut assert_unique_keys = |children: &'b [VNode<'b>]| {
|
||||
|
@ -401,7 +437,7 @@ impl<'b> VirtualDom {
|
|||
//
|
||||
// `shared_prefix_count` is the count of how many nodes at the start of
|
||||
// `new` and `old` share the same keys.
|
||||
let (left_offset, right_offset) = match self.diff_keyed_ends(old, new) {
|
||||
let (left_offset, right_offset) = match self.diff_keyed_ends(old, new, parent) {
|
||||
Some(count) => count,
|
||||
None => return,
|
||||
};
|
||||
|
@ -427,18 +463,18 @@ impl<'b> VirtualDom {
|
|||
if left_offset == 0 {
|
||||
// insert at the beginning of the old list
|
||||
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 {
|
||||
// insert at the end the old list
|
||||
let foothold = old.last().unwrap();
|
||||
self.create_and_insert_after(new_middle, foothold);
|
||||
self.create_and_insert_after(new_middle, foothold, parent);
|
||||
} else {
|
||||
// inserting in the middle
|
||||
let foothold = &old[left_offset - 1];
|
||||
self.create_and_insert_after(new_middle, foothold);
|
||||
self.create_and_insert_after(new_middle, foothold, parent);
|
||||
}
|
||||
} else {
|
||||
self.diff_keyed_middle(old_middle, new_middle);
|
||||
self.diff_keyed_middle(old_middle, new_middle, parent);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -451,6 +487,7 @@ impl<'b> VirtualDom {
|
|||
&mut self,
|
||||
old: &'b [VNode<'b>],
|
||||
new: &'b [VNode<'b>],
|
||||
parent: ElementRef,
|
||||
) -> Option<(usize, usize)> {
|
||||
let mut left_offset = 0;
|
||||
|
||||
|
@ -466,7 +503,7 @@ impl<'b> VirtualDom {
|
|||
// If that was all of the old children, then create and append the remaining
|
||||
// new children and we're finished.
|
||||
if left_offset == old.len() {
|
||||
self.create_and_insert_after(&new[left_offset..], old.last().unwrap());
|
||||
self.create_and_insert_after(&new[left_offset..], old.last().unwrap(), parent);
|
||||
return None;
|
||||
}
|
||||
|
||||
|
@ -505,7 +542,12 @@ impl<'b> VirtualDom {
|
|||
//
|
||||
// Upon exit from this function, it will be restored to that same self.
|
||||
#[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.
|
||||
2. Create a map of old key to its index
|
||||
|
@ -560,9 +602,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
|
||||
// create the new children afresh.
|
||||
if shared_keys.is_empty() {
|
||||
if old.get(0).is_some() {
|
||||
if !old.is_empty() {
|
||||
self.remove_nodes(&old[1..]);
|
||||
self.replace(&old[0], new);
|
||||
self.replace(&old[0], new, Some(parent));
|
||||
} else {
|
||||
// I think this is wrong - why are we appending?
|
||||
// only valid of the if there are no trailing elements
|
||||
|
@ -739,20 +781,38 @@ impl<'b> VirtualDom {
|
|||
.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
|
||||
.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>) {
|
||||
let m = self.create_children(new);
|
||||
fn create_and_insert_before(
|
||||
&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);
|
||||
self.mutations.push(Mutation::InsertBefore { id, m })
|
||||
}
|
||||
|
||||
fn create_and_insert_after(&mut self, new: &'b [VNode<'b>], after: &'b VNode<'b>) {
|
||||
let m = self.create_children(new);
|
||||
fn create_and_insert_after(
|
||||
&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);
|
||||
self.mutations.push(Mutation::InsertAfter { id, m })
|
||||
}
|
||||
|
@ -762,15 +822,21 @@ impl<'b> VirtualDom {
|
|||
&mut self,
|
||||
l: &'b VPlaceholder,
|
||||
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();
|
||||
self.mutations.push(Mutation::ReplaceWith { id, m });
|
||||
self.reclaim(id);
|
||||
}
|
||||
|
||||
fn replace(&mut self, left: &'b VNode<'b>, right: impl IntoIterator<Item = &'b VNode<'b>>) {
|
||||
let m = self.create_children(right);
|
||||
fn replace(
|
||||
&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();
|
||||
|
||||
|
@ -789,11 +855,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
|
||||
let placeholder = self.next_element(&l[0], &[]);
|
||||
let placeholder = self.next_element();
|
||||
|
||||
r.id.set(Some(placeholder));
|
||||
r.parent.set(Some(parent));
|
||||
|
||||
self.mutations
|
||||
.push(Mutation::CreatePlaceholder { id: placeholder });
|
||||
|
@ -831,6 +898,16 @@ impl<'b> VirtualDom {
|
|||
// 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)
|
||||
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) {
|
||||
|
@ -989,6 +1066,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?
|
||||
|
|
|
@ -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::{
|
||||
any::{Any, TypeId},
|
||||
backtrace::Backtrace,
|
||||
cell::RefCell,
|
||||
fmt::Debug,
|
||||
error::Error,
|
||||
fmt::{Debug, Display},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
/// A boundary that will capture any errors from child components
|
||||
pub struct ErrorBoundary {
|
||||
error: RefCell<Option<CapturedError>>,
|
||||
_id: ScopeId,
|
||||
/// Provide an error boundary to catch errors from child components
|
||||
pub fn use_error_boundary(cx: &ScopeState) -> &ErrorBoundary {
|
||||
cx.use_hook(|| cx.provide_context(ErrorBoundary::new()))
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub struct CapturedError {
|
||||
/// The error captured by the error boundary
|
||||
pub error: Box<dyn Debug + 'static>,
|
||||
|
||||
/// The backtrace of the error
|
||||
pub backtrace: Backtrace,
|
||||
|
||||
/// The scope that threw the error
|
||||
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 {
|
||||
/// Downcast the error type into a concrete error type
|
||||
pub fn downcast<T: 'static>(&self) -> Option<&T> {
|
||||
|
@ -32,17 +75,56 @@ impl CapturedError {
|
|||
}
|
||||
}
|
||||
|
||||
impl ErrorBoundary {
|
||||
pub fn new(id: ScopeId) -> Self {
|
||||
impl Default for ErrorBoundaryInner {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
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
|
||||
pub fn insert_error(&self, scope: ScopeId, error: Box<dyn Debug + 'static>) {
|
||||
self.error.replace(Some(CapturedError { error, scope }));
|
||||
pub fn insert_error(
|
||||
&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
|
||||
/// #[component]
|
||||
/// fn App(cx: Scope, count: String) -> Element {
|
||||
/// let id: i32 = count.parse().throw(cx)?;
|
||||
/// let id: i32 = count.parse().throw()?;
|
||||
///
|
||||
/// cx.render(rsx! {
|
||||
/// div { "Count {}" }
|
||||
|
@ -84,14 +166,14 @@ pub trait Throw<S = ()>: Sized {
|
|||
/// ```rust, ignore
|
||||
/// #[component]
|
||||
/// fn App(cx: Scope, count: String) -> Element {
|
||||
/// let id: i32 = count.parse().throw(cx)?;
|
||||
/// let id: i32 = count.parse().throw()?;
|
||||
///
|
||||
/// cx.render(rsx! {
|
||||
/// 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.
|
||||
///
|
||||
|
@ -107,45 +189,46 @@ pub trait Throw<S = ()>: Sized {
|
|||
/// ```rust, ignore
|
||||
/// #[component]
|
||||
/// fn App(cx: Scope, count: String) -> Element {
|
||||
/// let id: i32 = count.parse().throw(cx)?;
|
||||
/// let id: i32 = count.parse().throw()?;
|
||||
///
|
||||
/// cx.render(rsx! {
|
||||
/// div { "Count {}" }
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
fn throw_with<D: Debug + 'static>(
|
||||
self,
|
||||
cx: &ScopeState,
|
||||
e: impl FnOnce() -> D,
|
||||
) -> Option<Self::Out>;
|
||||
fn throw_with<D: Debug + 'static>(self, e: impl FnOnce() -> D) -> Option<Self::Out> {
|
||||
self.throw().or_else(|| throw_error(e()))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
impl<'a, T, O: Debug + 'static, E: ToOwned<Owned = O>> Throw for &'a Result<T, E> {
|
||||
type Out = &'a T;
|
||||
|
||||
fn throw(self, cx: &ScopeState) -> Option<Self::Out> {
|
||||
fn throw(self) -> Option<Self::Out> {
|
||||
match self {
|
||||
Ok(t) => Some(t),
|
||||
Err(e) => {
|
||||
cx.throw(e.to_owned());
|
||||
None
|
||||
}
|
||||
Err(e) => throw_error(e.to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
fn throw_with<D: Debug + 'static>(
|
||||
self,
|
||||
cx: &ScopeState,
|
||||
err: impl FnOnce() -> D,
|
||||
) -> Option<Self::Out> {
|
||||
fn throw_with<D: Debug + 'static>(self, err: impl FnOnce() -> D) -> Option<Self::Out> {
|
||||
match self {
|
||||
Ok(t) => Some(t),
|
||||
Err(_e) => {
|
||||
cx.throw(err());
|
||||
None
|
||||
}
|
||||
Err(_e) => throw_error(err()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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> {
|
||||
type Out = T;
|
||||
|
||||
fn throw(self, cx: &ScopeState) -> Option<T> {
|
||||
fn throw(self) -> Option<T> {
|
||||
match self {
|
||||
Ok(t) => Some(t),
|
||||
Err(e) => {
|
||||
cx.throw(e);
|
||||
None
|
||||
}
|
||||
Err(e) => throw_error(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn throw_with<D: Debug + 'static>(
|
||||
self,
|
||||
cx: &ScopeState,
|
||||
error: impl FnOnce() -> D,
|
||||
) -> Option<Self::Out> {
|
||||
self.ok().or_else(|| {
|
||||
cx.throw(error());
|
||||
None
|
||||
})
|
||||
fn throw_with<D: Debug + 'static>(self, error: impl FnOnce() -> D) -> Option<Self::Out> {
|
||||
self.ok().or_else(|| throw_error(error()))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -180,21 +253,236 @@ impl<T, E: Debug + 'static> Throw for Result<T, E> {
|
|||
impl<T> Throw for Option<T> {
|
||||
type Out = T;
|
||||
|
||||
fn throw(self, cx: &ScopeState) -> Option<T> {
|
||||
self.or_else(|| {
|
||||
cx.throw("None error.");
|
||||
None
|
||||
})
|
||||
fn throw(self) -> Option<T> {
|
||||
self.or_else(|| throw_error("Attempted to unwrap a None value."))
|
||||
}
|
||||
|
||||
fn throw_with<D: Debug + 'static>(
|
||||
self,
|
||||
cx: &ScopeState,
|
||||
error: impl FnOnce() -> D,
|
||||
) -> Option<Self::Out> {
|
||||
self.or_else(|| {
|
||||
cx.throw(error());
|
||||
None
|
||||
fn throw_with<D: Debug + 'static>(self, error: impl FnOnce() -> D) -> Option<Self::Out> {
|
||||
self.or_else(|| throw_error(error()))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ErrorHandler<'a>(Box<dyn Fn(CapturedError) -> LazyNodes<'a, 'a> + 'a>);
|
||||
impl<'a, F: Fn(CapturedError) -> LazyNodes<'a, 'a> + 'a> From<F> for ErrorHandler<'a> {
|
||||
fn from(value: F) -> Self {
|
||||
Self(Box::new(value))
|
||||
}
|
||||
}
|
||||
fn default_handler<'a>(error: CapturedError) -> LazyNodes<'a, 'a> {
|
||||
LazyNodes::new(move |__cx: &ScopeState| -> VNode {
|
||||
static TEMPLATE: Template = Template {
|
||||
name: "error_handle.rs:42:5:884",
|
||||
roots: &[TemplateNode::Element {
|
||||
tag: "pre",
|
||||
namespace: None,
|
||||
attrs: &[TemplateAttribute::Static {
|
||||
name: "color",
|
||||
namespace: Some("style"),
|
||||
value: "red",
|
||||
}],
|
||||
children: &[TemplateNode::DynamicText { id: 0usize }],
|
||||
}],
|
||||
node_paths: &[&[0u8, 0u8]],
|
||||
attr_paths: &[],
|
||||
};
|
||||
VNode {
|
||||
parent: Default::default(),
|
||||
stable_id: Default::default(),
|
||||
key: None,
|
||||
template: std::cell::Cell::new(TEMPLATE),
|
||||
root_ids: bumpalo::collections::Vec::with_capacity_in(1usize, __cx.bump()).into(),
|
||||
dynamic_nodes: __cx
|
||||
.bump()
|
||||
.alloc([__cx.text_node(format_args!("{0}", error))]),
|
||||
dynamic_attrs: __cx.bump().alloc([]),
|
||||
}
|
||||
})
|
||||
}
|
||||
pub struct ErrorBoundaryProps<'a> {
|
||||
children: Element<'a>,
|
||||
handle_error: ErrorHandler<'a>,
|
||||
}
|
||||
impl<'a> ErrorBoundaryProps<'a> {
|
||||
/**
|
||||
Create a builder for building `ErrorBoundaryProps`.
|
||||
On the builder, call `.children(...)`(optional), `.handle_error(...)`(optional) to set the values of the fields.
|
||||
Finally, call `.build()` to create the instance of `ErrorBoundaryProps`.
|
||||
*/
|
||||
#[allow(dead_code)]
|
||||
pub fn builder() -> ErrorBoundaryPropsBuilder<'a, ((), ())> {
|
||||
ErrorBoundaryPropsBuilder {
|
||||
fields: ((), ()),
|
||||
_phantom: ::core::default::Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[must_use]
|
||||
#[doc(hidden)]
|
||||
#[allow(dead_code, non_camel_case_types, non_snake_case)]
|
||||
pub struct ErrorBoundaryPropsBuilder<'a, TypedBuilderFields> {
|
||||
fields: TypedBuilderFields,
|
||||
_phantom: ::core::marker::PhantomData<&'a ()>,
|
||||
}
|
||||
impl<'a, TypedBuilderFields> Clone for ErrorBoundaryPropsBuilder<'a, TypedBuilderFields>
|
||||
where
|
||||
TypedBuilderFields: Clone,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
fields: self.fields.clone(),
|
||||
_phantom: ::core::default::Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'a> Properties for ErrorBoundaryProps<'a> {
|
||||
type Builder = ErrorBoundaryPropsBuilder<'a, ((), ())>;
|
||||
const IS_STATIC: bool = false;
|
||||
fn builder() -> Self::Builder {
|
||||
ErrorBoundaryProps::builder()
|
||||
}
|
||||
unsafe fn memoize(&self, _: &Self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
#[doc(hidden)]
|
||||
#[allow(dead_code, non_camel_case_types, non_snake_case)]
|
||||
pub trait ErrorBoundaryPropsBuilder_Optional<T> {
|
||||
fn into_value<F: FnOnce() -> T>(self, default: F) -> T;
|
||||
}
|
||||
impl<T> ErrorBoundaryPropsBuilder_Optional<T> for () {
|
||||
fn into_value<F: FnOnce() -> T>(self, default: F) -> T {
|
||||
default()
|
||||
}
|
||||
}
|
||||
impl<T> ErrorBoundaryPropsBuilder_Optional<T> for (T,) {
|
||||
fn into_value<F: FnOnce() -> T>(self, _: F) -> T {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
||||
impl<'a, __handle_error> ErrorBoundaryPropsBuilder<'a, ((), __handle_error)> {
|
||||
pub fn children(
|
||||
self,
|
||||
children: Element<'a>,
|
||||
) -> ErrorBoundaryPropsBuilder<'a, ((Element<'a>,), __handle_error)> {
|
||||
let children = (children,);
|
||||
let (_, handle_error) = self.fields;
|
||||
ErrorBoundaryPropsBuilder {
|
||||
fields: (children, handle_error),
|
||||
_phantom: self._phantom,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[doc(hidden)]
|
||||
#[allow(dead_code, non_camel_case_types, non_snake_case)]
|
||||
pub enum ErrorBoundaryPropsBuilder_Error_Repeated_field_children {}
|
||||
#[doc(hidden)]
|
||||
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
||||
impl<'a, __handle_error> ErrorBoundaryPropsBuilder<'a, ((Element<'a>,), __handle_error)> {
|
||||
#[deprecated(note = "Repeated field children")]
|
||||
pub fn children(
|
||||
self,
|
||||
_: ErrorBoundaryPropsBuilder_Error_Repeated_field_children,
|
||||
) -> ErrorBoundaryPropsBuilder<'a, ((Element<'a>,), __handle_error)> {
|
||||
self
|
||||
}
|
||||
}
|
||||
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
||||
impl<'a, __children> ErrorBoundaryPropsBuilder<'a, (__children, ())> {
|
||||
pub fn handle_error(
|
||||
self,
|
||||
handle_error: impl ::core::convert::Into<ErrorHandler<'a>>,
|
||||
) -> ErrorBoundaryPropsBuilder<'a, (__children, (ErrorHandler<'a>,))> {
|
||||
let handle_error = (handle_error.into(),);
|
||||
let (children, _) = self.fields;
|
||||
ErrorBoundaryPropsBuilder {
|
||||
fields: (children, handle_error),
|
||||
_phantom: self._phantom,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[doc(hidden)]
|
||||
#[allow(dead_code, non_camel_case_types, non_snake_case)]
|
||||
pub enum ErrorBoundaryPropsBuilder_Error_Repeated_field_handle_error {}
|
||||
#[doc(hidden)]
|
||||
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
||||
impl<'a, __children> ErrorBoundaryPropsBuilder<'a, (__children, (ErrorHandler<'a>,))> {
|
||||
#[deprecated(note = "Repeated field handle_error")]
|
||||
pub fn handle_error(
|
||||
self,
|
||||
_: ErrorBoundaryPropsBuilder_Error_Repeated_field_handle_error,
|
||||
) -> ErrorBoundaryPropsBuilder<'a, (__children, (ErrorHandler<'a>,))> {
|
||||
self
|
||||
}
|
||||
}
|
||||
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
||||
impl<
|
||||
'a,
|
||||
__handle_error: ErrorBoundaryPropsBuilder_Optional<ErrorHandler<'a>>,
|
||||
__children: ErrorBoundaryPropsBuilder_Optional<Element<'a>>,
|
||||
> ErrorBoundaryPropsBuilder<'a, (__children, __handle_error)>
|
||||
{
|
||||
pub fn build(self) -> ErrorBoundaryProps<'a> {
|
||||
let (children, handle_error) = self.fields;
|
||||
let children = ErrorBoundaryPropsBuilder_Optional::into_value(children, || {
|
||||
::core::default::Default::default()
|
||||
});
|
||||
let handle_error = ErrorBoundaryPropsBuilder_Optional::into_value(handle_error, || {
|
||||
ErrorHandler(Box::new(default_handler))
|
||||
});
|
||||
ErrorBoundaryProps {
|
||||
children,
|
||||
handle_error,
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Create a new error boundary component.
|
||||
///
|
||||
/// ## Details
|
||||
///
|
||||
/// Error boundaries handle errors within a specific part of your application. Any errors passed in a child with [`Throw`] will be caught by the nearest error boundary.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// rsx!{
|
||||
/// ErrorBoundary {
|
||||
/// handle_error: |error| rsx! { "Oops, we encountered an error. Please report {error} to the developer of this application" }
|
||||
/// ThrowsError {}
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Usage
|
||||
///
|
||||
/// Error boundaries are an easy way to handle errors in your application.
|
||||
/// They are similar to `try/catch` in JavaScript, but they only catch errors in the tree below them.
|
||||
/// Error boundaries are quick to implement, but it can be useful to individually handle errors in your components to provide a better user experience when you know that an error is likely to occur.
|
||||
#[allow(non_upper_case_globals, non_snake_case)]
|
||||
pub fn ErrorBoundary<'a>(cx: Scope<'a, ErrorBoundaryProps<'a>>) -> Element {
|
||||
let error_boundary = use_error_boundary(cx);
|
||||
match error_boundary.take_error() {
|
||||
Some(error) => cx.render((cx.props.handle_error.0)(error)),
|
||||
None => Some({
|
||||
let __cx = cx;
|
||||
static TEMPLATE: Template = Template {
|
||||
name: "examples/error_handle.rs:81:17:2342",
|
||||
roots: &[TemplateNode::Dynamic { id: 0usize }],
|
||||
node_paths: &[&[0u8]],
|
||||
attr_paths: &[],
|
||||
};
|
||||
VNode {
|
||||
parent: Default::default(),
|
||||
stable_id: Default::default(),
|
||||
key: None,
|
||||
template: std::cell::Cell::new(TEMPLATE),
|
||||
root_ids: bumpalo::collections::Vec::with_capacity_in(1usize, __cx.bump()).into(),
|
||||
dynamic_nodes: __cx.bump().alloc([{
|
||||
let ___nodes = (&cx.props.children).into_dyn_node(__cx);
|
||||
___nodes
|
||||
}]),
|
||||
dynamic_attrs: __cx.bump().alloc([]),
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -128,8 +128,6 @@ impl<T: std::fmt::Debug> std::fmt::Debug for Event<T> {
|
|||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
|
||||
/// The callback type generated by the `rsx!` macro when an `on` field is specified for components.
|
||||
///
|
||||
/// This makes it possible to pass `move |evt| {}` style closures into components as property fields.
|
||||
|
|
|
@ -30,7 +30,8 @@ pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element {
|
|||
let children = cx.props.0.as_ref()?;
|
||||
Some(VNode {
|
||||
key: children.key,
|
||||
parent: children.parent,
|
||||
parent: children.parent.clone(),
|
||||
stable_id: children.stable_id.clone(),
|
||||
template: children.template.clone(),
|
||||
root_ids: children.root_ids.clone(),
|
||||
dynamic_nodes: children.dynamic_nodes,
|
||||
|
|
|
@ -18,13 +18,42 @@ use crate::{innerlude::VNode, ScopeState};
|
|||
|
||||
/// A concrete type provider for closures that build [`VNode`] structures.
|
||||
///
|
||||
/// This struct wraps lazy structs that build [`VNode`] trees Normally, we cannot perform a blanket implementation over
|
||||
/// This struct wraps lazy structs that build [`VNode`] trees. Normally, we cannot perform a blanket implementation over
|
||||
/// closures, but if we wrap the closure in a concrete type, we can use it for different branches in matching.
|
||||
///
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// LazyNodes::new(|f| f.element("div", [], [], [] None))
|
||||
/// LazyNodes::new(|f| {
|
||||
/// static TEMPLATE: dioxus::core::Template = dioxus::core::Template {
|
||||
/// name: "main.rs:5:5:20", // Source location of the template for hot reloading
|
||||
/// roots: &[
|
||||
/// dioxus::core::TemplateNode::Element {
|
||||
/// tag: dioxus_elements::div::TAG_NAME,
|
||||
/// namespace: dioxus_elements::div::NAME_SPACE,
|
||||
/// attrs: &[],
|
||||
/// children: &[],
|
||||
/// },
|
||||
/// ],
|
||||
/// node_paths: &[],
|
||||
/// attr_paths: &[],
|
||||
/// };
|
||||
/// dioxus::core::VNode {
|
||||
/// parent: None,
|
||||
/// key: None,
|
||||
/// template: std::cell::Cell::new(TEMPLATE),
|
||||
/// root_ids: dioxus::core::exports::bumpalo::collections::Vec::with_capacity_in(
|
||||
/// 1usize,
|
||||
/// f.bump(),
|
||||
/// )
|
||||
/// .into(),
|
||||
/// dynamic_nodes: f.bump().alloc([]),
|
||||
/// dynamic_attrs: f.bump().alloc([]),
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Find more information about how to construct [`VNode`] at <https://dioxuslabs.com/learn/0.4/contributing/walkthrough_readme#the-rsx-macro>
|
||||
|
||||
pub struct LazyNodes<'a, 'b> {
|
||||
#[cfg(not(miri))]
|
||||
inner: SmallBox<dyn FnMut(&'a ScopeState) -> VNode<'a> + 'b, S16>,
|
||||
|
@ -61,7 +90,7 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
|
|||
/// Call the closure with the given factory to produce real [`VNode`].
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// let f = LazyNodes::new(move |f| f.element("div", [], [], [] None));
|
||||
/// let f = LazyNodes::new(/* Closure for creating VNodes */);
|
||||
///
|
||||
/// let node = f.call(cac);
|
||||
/// ```
|
||||
|
|
|
@ -90,10 +90,11 @@ pub mod prelude {
|
|||
pub use crate::innerlude::{
|
||||
consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, has_context,
|
||||
provide_context, provide_context_to_scope, provide_root_context, push_future,
|
||||
remove_future, schedule_update_any, spawn, spawn_forever, suspend, throw, AnyValue,
|
||||
Component, Element, Event, EventHandler, Fragment, IntoAttributeValue, IntoDynNode,
|
||||
LazyNodes, Properties, Runtime, RuntimeGuard, Scope, ScopeId, ScopeState, Scoped, TaskId,
|
||||
Template, TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom,
|
||||
remove_future, schedule_update_any, spawn, spawn_forever, suspend, use_error_boundary,
|
||||
AnyValue, Component, Element, ErrorBoundary, Event, EventHandler, Fragment,
|
||||
IntoAttributeValue, IntoDynNode, LazyNodes, 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.
|
||||
///
|
||||
/// 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
|
||||
pub edits: Vec<Mutation<'a>>,
|
||||
|
@ -91,7 +91,7 @@ pub enum Mutation<'a> {
|
|||
id: ElementId,
|
||||
},
|
||||
|
||||
/// Create an placeholder int he DOM that we will use later.
|
||||
/// Create a placeholder in the DOM that we will use later.
|
||||
///
|
||||
/// Dioxus currently requires the use of placeholders to maintain a re-entrance point for things like list diffing
|
||||
CreatePlaceholder {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use crate::innerlude::{ElementRef, VNodeId};
|
||||
use crate::{
|
||||
any_props::AnyProps, arena::ElementId, Element, Event, LazyNodes, ScopeId, ScopeState,
|
||||
};
|
||||
|
@ -47,7 +48,10 @@ pub struct VNode<'a> {
|
|||
pub key: Option<&'a str>,
|
||||
|
||||
/// 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
|
||||
pub template: Cell<Template<'static>>,
|
||||
|
@ -68,7 +72,8 @@ impl<'a> VNode<'a> {
|
|||
pub fn empty(cx: &'a ScopeState) -> Element<'a> {
|
||||
Some(VNode {
|
||||
key: None,
|
||||
parent: None,
|
||||
parent: Default::default(),
|
||||
stable_id: Default::default(),
|
||||
root_ids: RefCell::new(bumpalo::collections::Vec::new_in(cx.bump())),
|
||||
dynamic_nodes: &[],
|
||||
dynamic_attrs: &[],
|
||||
|
@ -81,6 +86,30 @@ impl<'a> VNode<'a> {
|
|||
})
|
||||
}
|
||||
|
||||
/// Create a new VNode
|
||||
pub fn new(
|
||||
key: Option<&'a str>,
|
||||
template: Template<'static>,
|
||||
root_ids: bumpalo::collections::Vec<'a, ElementId>,
|
||||
dynamic_nodes: &'a [DynamicNode<'a>],
|
||||
dynamic_attrs: &'a [Attribute<'a>],
|
||||
) -> Self {
|
||||
Self {
|
||||
key,
|
||||
parent: Cell::new(None),
|
||||
stable_id: Cell::new(None),
|
||||
template: Cell::new(template),
|
||||
root_ids: RefCell::new(root_ids),
|
||||
dynamic_nodes,
|
||||
dynamic_attrs,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the stable id of this node used for bubbling events
|
||||
pub(crate) fn stable_id(&self) -> Option<VNodeId> {
|
||||
self.stable_id.get()
|
||||
}
|
||||
|
||||
/// Load a dynamic root at the given index
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// 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) props: RefCell<Option<Box<dyn AnyProps<'a> + 'a>>>,
|
||||
|
@ -372,6 +401,8 @@ impl<'a> VText<'a> {
|
|||
pub struct VPlaceholder {
|
||||
/// The ID of this node in the real DOM
|
||||
pub(crate) id: Cell<Option<ElementId>>,
|
||||
/// The parent of this node
|
||||
pub(crate) parent: Cell<Option<ElementRef>>,
|
||||
}
|
||||
|
||||
impl VPlaceholder {
|
||||
|
@ -651,52 +682,52 @@ pub trait IntoDynNode<'a, A = ()> {
|
|||
/// 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
|
||||
fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a>;
|
||||
fn into_dyn_node(self, cx: &'a ScopeState) -> DynamicNode<'a>;
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
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]))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
Some(val) => val.into_vnode(_cx),
|
||||
Some(val) => val.into_dyn_node(_cx),
|
||||
None => DynamicNode::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
Some(val) => val.clone().into_vnode(_cx),
|
||||
Some(val) => val.clone().into_dyn_node(_cx),
|
||||
_ => DynamicNode::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()]))
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
value: cx.bump().alloc_str(self),
|
||||
id: Default::default(),
|
||||
|
@ -705,24 +736,25 @@ impl<'a, 'b> IntoDynNode<'b> for &'a str {
|
|||
}
|
||||
|
||||
impl IntoDynNode<'_> for String {
|
||||
fn into_vnode(self, cx: &ScopeState) -> DynamicNode {
|
||||
fn into_dyn_node(self, cx: &ScopeState) -> DynamicNode {
|
||||
DynamicNode::Text(VText {
|
||||
value: cx.bump().alloc(self),
|
||||
value: cx.bump().alloc_str(&self),
|
||||
id: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
parent: self.parent,
|
||||
parent: self.parent.clone(),
|
||||
stable_id: self.stable_id.clone(),
|
||||
template: self.template.clone(),
|
||||
root_ids: self.root_ids.clone(),
|
||||
key: self.key,
|
||||
|
@ -732,24 +764,24 @@ impl<'a> IntoDynNode<'a> for &'a VNode<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub trait IntoTemplate<'a> {
|
||||
fn into_template(self, _cx: &'a ScopeState) -> VNode<'a>;
|
||||
pub trait IntoVNode<'a> {
|
||||
fn into_vnode(self, _cx: &'a ScopeState) -> VNode<'a>;
|
||||
}
|
||||
impl<'a> IntoTemplate<'a> for VNode<'a> {
|
||||
fn into_template(self, _cx: &'a ScopeState) -> VNode<'a> {
|
||||
impl<'a> IntoVNode<'a> for VNode<'a> {
|
||||
fn into_vnode(self, _cx: &'a ScopeState) -> VNode<'a> {
|
||||
self
|
||||
}
|
||||
}
|
||||
impl<'a> IntoTemplate<'a> for Element<'a> {
|
||||
fn into_template(self, cx: &'a ScopeState) -> VNode<'a> {
|
||||
impl<'a> IntoVNode<'a> for Element<'a> {
|
||||
fn into_vnode(self, cx: &'a ScopeState) -> VNode<'a> {
|
||||
match self {
|
||||
Some(val) => val.into_template(cx),
|
||||
Some(val) => val.into_vnode(cx),
|
||||
_ => VNode::empty(cx).unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'a, 'b> IntoTemplate<'a> for LazyNodes<'a, 'b> {
|
||||
fn into_template(self, cx: &'a ScopeState) -> VNode<'a> {
|
||||
impl<'a, 'b> IntoVNode<'a> for LazyNodes<'a, 'b> {
|
||||
fn into_vnode(self, cx: &'a ScopeState) -> VNode<'a> {
|
||||
cx.render(self).unwrap()
|
||||
}
|
||||
}
|
||||
|
@ -759,12 +791,12 @@ pub struct FromNodeIterator;
|
|||
impl<'a, T, I> IntoDynNode<'a, FromNodeIterator> for T
|
||||
where
|
||||
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());
|
||||
|
||||
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() {
|
||||
children if children.is_empty() => DynamicNode::default(),
|
||||
|
@ -791,6 +823,12 @@ impl<'a> IntoAttributeValue<'a> for &'a str {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoAttributeValue<'a> for String {
|
||||
fn into_value(self, cx: &'a Bump) -> AttributeValue<'a> {
|
||||
AttributeValue::Text(cx.alloc_str(&self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoAttributeValue<'a> for f64 {
|
||||
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
|
||||
AttributeValue::Float(self)
|
||||
|
|
|
@ -19,7 +19,7 @@ pub struct TaskId(pub usize);
|
|||
/// the task itself is the waker
|
||||
pub(crate) struct LocalTask {
|
||||
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,
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
.unbounded_send(SchedulerMsg::TaskNotified(task_id))
|
||||
.expect("Scheduler should exist");
|
||||
}
|
||||
|
||||
entry.insert(task);
|
||||
|
||||
task_id
|
||||
}
|
||||
|
@ -60,8 +64,8 @@ impl Scheduler {
|
|||
/// Drop the future with the given TaskId
|
||||
///
|
||||
/// This does not abort the task, so you'll want to wrap it in an aborthandle if that's important to you
|
||||
pub fn remove(&self, id: TaskId) {
|
||||
self.tasks.borrow_mut().try_remove(id.0);
|
||||
pub fn remove(&self, id: TaskId) -> Option<LocalTask> {
|
||||
self.tasks.borrow_mut().try_remove(id.0)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ impl VirtualDom {
|
|||
|
||||
borrowed_props: Default::default(),
|
||||
attributes_to_drop_before_render: Default::default(),
|
||||
element_refs_to_drop: Default::default(),
|
||||
}));
|
||||
|
||||
let context =
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{
|
||||
innerlude::{ErrorBoundary, Scheduler, SchedulerMsg},
|
||||
innerlude::{Scheduler, SchedulerMsg},
|
||||
runtime::{with_current_scope, with_runtime},
|
||||
Element, ScopeId, TaskId,
|
||||
};
|
||||
|
@ -7,7 +7,6 @@ use rustc_hash::FxHashSet;
|
|||
use std::{
|
||||
any::Any,
|
||||
cell::{Cell, RefCell},
|
||||
fmt::Debug,
|
||||
future::Future,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
|
@ -230,17 +229,7 @@ impl ScopeContext {
|
|||
/// 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 {
|
||||
// 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);
|
||||
|
||||
// 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
|
||||
self.tasks.spawn(ScopeId::ROOT, fut)
|
||||
}
|
||||
|
||||
/// Informs the scheduler that this task is no longer needed and should be removed.
|
||||
|
@ -250,19 +239,6 @@ impl ScopeContext {
|
|||
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
|
||||
pub fn suspend(&self) -> Option<Element> {
|
||||
self.suspended.set(true);
|
||||
|
@ -332,11 +308,6 @@ pub fn suspend() -> Option<Element<'static>> {
|
|||
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.
|
||||
pub fn push_future(fut: impl Future<Output = ()> + 'static) -> Option<TaskId> {
|
||||
with_current_scope(|cx| cx.push_future(fut))
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::{
|
|||
any_props::VProps,
|
||||
bump_frame::BumpFrame,
|
||||
innerlude::ErrorBoundary,
|
||||
innerlude::{DynamicNode, EventHandler, VComponent, VText},
|
||||
innerlude::{DynamicNode, EventHandler, VComponent, VNodeId, VText},
|
||||
lazynodes::LazyNodes,
|
||||
nodes::{IntoAttributeValue, IntoDynNode, RenderReturn},
|
||||
runtime::Runtime,
|
||||
|
@ -94,6 +94,7 @@ pub struct ScopeState {
|
|||
pub(crate) hook_idx: Cell<usize>,
|
||||
|
||||
pub(crate) borrowed_props: RefCell<Vec<*const VComponent<'static>>>,
|
||||
pub(crate) element_refs_to_drop: RefCell<Vec<VNodeId>>,
|
||||
pub(crate) attributes_to_drop_before_render: RefCell<Vec<*const Attribute<'static>>>,
|
||||
|
||||
pub(crate) props: Option<Box<dyn AnyProps<'static>>>,
|
||||
|
@ -406,7 +407,7 @@ impl<'src> ScopeState {
|
|||
|
||||
/// Convert any item that implements [`IntoDynNode`] into a [`DynamicNode`] using the internal [`Bump`] allocator
|
||||
pub fn make_node<'c, I>(&'src self, into: impl IntoDynNode<'src, I> + 'c) -> DynamicNode {
|
||||
into.into_vnode(self)
|
||||
into.into_dyn_node(self)
|
||||
}
|
||||
|
||||
/// Create a new [`Attribute`] from a name, value, namespace, and volatile bool
|
||||
|
@ -466,7 +467,7 @@ impl<'src> ScopeState {
|
|||
render_fn: component as *const (),
|
||||
static_props: P::IS_STATIC,
|
||||
props: RefCell::new(Some(extended)),
|
||||
scope: Cell::new(None),
|
||||
scope: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -517,19 +518,6 @@ impl<'src> ScopeState {
|
|||
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
|
||||
pub fn suspend(&self) -> Option<Element> {
|
||||
let cx = self.context();
|
||||
|
|
|
@ -4,19 +4,19 @@
|
|||
|
||||
use crate::{
|
||||
any_props::VProps,
|
||||
arena::{ElementId, ElementRef},
|
||||
innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg},
|
||||
arena::ElementId,
|
||||
innerlude::{DirtyScope, ElementRef, ErrorBoundary, Mutations, Scheduler, SchedulerMsg},
|
||||
mutations::Mutation,
|
||||
nodes::RenderReturn,
|
||||
nodes::{Template, TemplateId},
|
||||
runtime::{Runtime, RuntimeGuard},
|
||||
scopes::{ScopeId, ScopeState},
|
||||
AttributeValue, Element, Event, Scope,
|
||||
AttributeValue, Element, Event, Scope, VNode,
|
||||
};
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
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.
|
||||
///
|
||||
|
@ -186,7 +186,10 @@ pub struct VirtualDom {
|
|||
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
|
||||
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>,
|
||||
|
||||
|
@ -263,6 +266,7 @@ impl VirtualDom {
|
|||
dirty_scopes: Default::default(),
|
||||
templates: Default::default(),
|
||||
elements: Default::default(),
|
||||
element_refs: Default::default(),
|
||||
mutations: Mutations::default(),
|
||||
suspended_scopes: Default::default(),
|
||||
};
|
||||
|
@ -273,10 +277,13 @@ impl VirtualDom {
|
|||
);
|
||||
|
||||
// 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
|
||||
dom.elements.insert(ElementRef::none());
|
||||
dom.elements.insert(None);
|
||||
|
||||
dom
|
||||
}
|
||||
|
@ -314,9 +321,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,
|
||||
/// nothing will happen.
|
||||
///
|
||||
|
@ -353,7 +360,15 @@ impl VirtualDom {
|
|||
| | | <-- no, broke early
|
||||
| <-- no, broke early
|
||||
*/
|
||||
let mut parent_path = self.elements.get(element.0);
|
||||
let parent_path = match self.elements.get(element.0) {
|
||||
Some(Some(el)) => el,
|
||||
_ => return,
|
||||
};
|
||||
let mut parent_node = self
|
||||
.element_refs
|
||||
.get(parent_path.template.0)
|
||||
.cloned()
|
||||
.map(|el| (*parent_path, el));
|
||||
let mut listeners = vec![];
|
||||
|
||||
// We will clone this later. The data itself is wrapped in RC to be used in callbacks if required
|
||||
|
@ -365,12 +380,11 @@ impl VirtualDom {
|
|||
// If the event bubbles, we traverse through the tree until we find the target element.
|
||||
if bubbles {
|
||||
// Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent.
|
||||
while let Some(el_ref) = parent_path {
|
||||
while let Some((path, el_ref)) = parent_node {
|
||||
// safety: we maintain references of all vnodes in the element slab
|
||||
if let Some(template) = el_ref.template {
|
||||
let template = unsafe { template.as_ref() };
|
||||
let template = unsafe { el_ref.unwrap().as_ref() };
|
||||
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() {
|
||||
let this_path = node_template.attr_paths[idx];
|
||||
|
@ -394,7 +408,7 @@ impl VirtualDom {
|
|||
// We check the bubble state between each call to see if the event has been stopped from bubbling
|
||||
for listener in listeners.drain(..).rev() {
|
||||
if let AttributeValue::Listener(listener) = listener {
|
||||
let origin = el_ref.scope;
|
||||
let origin = path.scope;
|
||||
self.runtime.scope_stack.borrow_mut().push(origin);
|
||||
self.runtime.rendering.set(false);
|
||||
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
|
||||
|
@ -409,19 +423,20 @@ impl VirtualDom {
|
|||
}
|
||||
}
|
||||
|
||||
parent_path = template.parent.and_then(|id| self.elements.get(id.0));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
parent_node = template.parent.get().and_then(|element_ref| {
|
||||
self.element_refs
|
||||
.get(element_ref.template.0)
|
||||
.cloned()
|
||||
.map(|el| (element_ref, el))
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Otherwise, we just call the listener on the target element
|
||||
if let Some(el_ref) = parent_path {
|
||||
if let Some((path, el_ref)) = parent_node {
|
||||
// safety: we maintain references of all vnodes in the element slab
|
||||
if let Some(template) = el_ref.template {
|
||||
let template = unsafe { template.as_ref() };
|
||||
let template = unsafe { el_ref.unwrap().as_ref() };
|
||||
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() {
|
||||
let this_path = node_template.attr_paths[idx];
|
||||
|
@ -430,7 +445,7 @@ impl VirtualDom {
|
|||
// Only call the listener if this is the exact target element.
|
||||
if attr.name.trim_start_matches("on") == name && target_path == this_path {
|
||||
if let AttributeValue::Listener(listener) = &attr.value {
|
||||
let origin = el_ref.scope;
|
||||
let origin = path.scope;
|
||||
self.runtime.scope_stack.borrow_mut().push(origin);
|
||||
self.runtime.rendering.set(false);
|
||||
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
|
||||
|
@ -446,7 +461,6 @@ impl VirtualDom {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait for the scheduler to have any work.
|
||||
///
|
||||
|
@ -563,7 +577,7 @@ impl VirtualDom {
|
|||
// If an error occurs, we should try to render the default error component and context where the error occured
|
||||
RenderReturn::Aborted(placeholder) => {
|
||||
tracing::debug!("Ran into suspended or aborted scope during rebuild");
|
||||
let id = self.next_null();
|
||||
let id = self.next_element();
|
||||
placeholder.id.set(Some(id));
|
||||
self.mutations.push(Mutation::CreatePlaceholder { id });
|
||||
}
|
||||
|
@ -595,15 +609,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
|
||||
pub async fn wait_for_suspense(&mut self) {
|
||||
loop {
|
||||
// println!("waiting for suspense {:?}", self.suspended_scopes);
|
||||
if self.suspended_scopes.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// println!("waiting for suspense");
|
||||
self.wait_for_work().await;
|
||||
|
||||
// println!("Rendered immediately");
|
||||
_ = self.render_immediate();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,9 +24,9 @@ fn NoneChild(_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! {
|
||||
div {}
|
||||
|
|
69
packages/core/tests/event_propagation.rs
Normal file
69
packages/core/tests/event_propagation.rs
Normal file
|
@ -0,0 +1,69 @@
|
|||
use dioxus::prelude::*;
|
||||
use dioxus_core::ElementId;
|
||||
use std::{rc::Rc, sync::Mutex};
|
||||
|
||||
static CLICKS: Mutex<usize> = Mutex::new(0);
|
||||
|
||||
#[test]
|
||||
fn events_propagate() {
|
||||
let mut dom = VirtualDom::new(app);
|
||||
_ = dom.rebuild();
|
||||
|
||||
// Top-level click is registered
|
||||
dom.handle_event("click", Rc::new(MouseData::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(MouseData::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(MouseData::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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use dioxus::prelude::Props;
|
||||
use dioxus_core::*;
|
||||
use std::{cell::Cell, collections::HashSet};
|
||||
use std::{cfg, collections::HashSet};
|
||||
|
||||
fn random_ns() -> Option<&'static str> {
|
||||
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 };
|
||||
match rand::random::<u8>() % range {
|
||||
0 => DynamicNode::Placeholder(Default::default()),
|
||||
1 => cx.make_node((0..(rand::random::<u8>() % 5)).map(|_| VNode {
|
||||
key: None,
|
||||
parent: Default::default(),
|
||||
template: Cell::new(Template {
|
||||
1 => cx.make_node((0..(rand::random::<u8>() % 5)).map(|_| {
|
||||
VNode::new(
|
||||
None,
|
||||
Template {
|
||||
name: concat!(file!(), ":", line!(), ":", column!(), ":0"),
|
||||
roots: &[TemplateNode::Dynamic { id: 0 }],
|
||||
node_paths: &[&[0]],
|
||||
attr_paths: &[],
|
||||
}),
|
||||
root_ids: bumpalo::collections::Vec::new_in(cx.bump()).into(),
|
||||
dynamic_nodes: cx.bump().alloc([cx.component(
|
||||
},
|
||||
bumpalo::collections::Vec::new_in(cx.bump()),
|
||||
cx.bump().alloc([cx.component(
|
||||
create_random_element,
|
||||
DepthProps { depth, root: false },
|
||||
"create_random_element",
|
||||
)]),
|
||||
dynamic_attrs: &[],
|
||||
&[],
|
||||
)
|
||||
})),
|
||||
2 => cx.component(
|
||||
create_random_element,
|
||||
|
@ -271,13 +272,11 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
|
|||
)
|
||||
.into_boxed_str(),
|
||||
));
|
||||
// println!("{template:#?}");
|
||||
let node = VNode {
|
||||
key: None,
|
||||
parent: None,
|
||||
template: Cell::new(template),
|
||||
root_ids: bumpalo::collections::Vec::new_in(cx.bump()).into(),
|
||||
dynamic_nodes: {
|
||||
let node = VNode::new(
|
||||
None,
|
||||
template,
|
||||
bumpalo::collections::Vec::new_in(cx.bump()),
|
||||
{
|
||||
let dynamic_nodes: Vec<_> = dynamic_node_types
|
||||
.iter()
|
||||
.map(|ty| match ty {
|
||||
|
@ -291,12 +290,12 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
|
|||
.collect();
|
||||
cx.bump().alloc(dynamic_nodes)
|
||||
},
|
||||
dynamic_attrs: cx.bump().alloc(
|
||||
cx.bump().alloc(
|
||||
(0..template.attr_paths.len())
|
||||
.map(|_| create_random_dynamic_attr(cx))
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
};
|
||||
);
|
||||
Some(node)
|
||||
}
|
||||
_ => None,
|
||||
|
@ -306,10 +305,10 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
|
|||
}
|
||||
|
||||
// test for panics when creating random nodes and templates
|
||||
#[cfg(not(miri))]
|
||||
#[test]
|
||||
fn create() {
|
||||
for _ in 0..1000 {
|
||||
let repeat_count = if cfg!(miri) { 100 } else { 1000 };
|
||||
for _ in 0..repeat_count {
|
||||
let mut vdom =
|
||||
VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true });
|
||||
let _ = vdom.rebuild();
|
||||
|
@ -318,10 +317,10 @@ fn create() {
|
|||
|
||||
// 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
|
||||
#[cfg(not(miri))]
|
||||
#[test]
|
||||
fn diff() {
|
||||
for _ in 0..100000 {
|
||||
let repeat_count = if cfg!(miri) { 100 } else { 1000 };
|
||||
for _ in 0..repeat_count {
|
||||
let mut vdom =
|
||||
VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true });
|
||||
let _ = vdom.rebuild();
|
||||
|
|
|
@ -10,6 +10,8 @@ fn basic_syntax_is_a_template(cx: Scope) -> Element {
|
|||
div { key: "12345",
|
||||
class: "asd",
|
||||
class: "{asd}",
|
||||
class: if true { "{asd}" },
|
||||
class: if false { "{asd}" },
|
||||
onclick: move |_| {},
|
||||
div { "{var}" }
|
||||
div {
|
||||
|
@ -24,6 +26,7 @@ fn basic_syntax_is_a_template(cx: Scope) -> Element {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dual_stream() {
|
||||
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) },
|
||||
SetAttribute {
|
||||
name: "class",
|
||||
value: (&*bump.alloc("123".into_value(&bump))).into(),
|
||||
value: (&*bump.alloc("asd 123 123".into_value(&bump))).into(),
|
||||
id: ElementId(1),
|
||||
ns: None,
|
||||
},
|
||||
|
|
|
@ -11,7 +11,7 @@ fn it_works() {
|
|||
let mut dom = VirtualDom::new(app);
|
||||
_ = dom.rebuild();
|
||||
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>");
|
||||
|
||||
|
|
1
packages/desktop/.gitignore
vendored
Normal file
1
packages/desktop/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/src/minified.js
|
|
@ -12,14 +12,14 @@ keywords = ["dom", "ui", "gui", "react"]
|
|||
[dependencies]
|
||||
dioxus-core = { workspace = true, features = ["serialize"] }
|
||||
dioxus-html = { workspace = true, features = ["serialize", "native-bind", "mounted", "eval"] }
|
||||
dioxus-interpreter-js = { workspace = true }
|
||||
dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] }
|
||||
dioxus-hot-reload = { workspace = true, optional = true }
|
||||
|
||||
serde = "1.0.136"
|
||||
serde_json = "1.0.79"
|
||||
thiserror = { workspace = true }
|
||||
wry = { version = "0.28.0", default-features = false, features = ["protocol", "file-drop"] }
|
||||
tracing = { workspace = true }
|
||||
wry = { version = "0.34.0", default-features = false, features = ["tao", "protocol", "file-drop"] }
|
||||
futures-channel = { workspace = true }
|
||||
tokio = { workspace = true, features = [
|
||||
"sync",
|
||||
|
@ -33,14 +33,17 @@ webbrowser = "0.8.0"
|
|||
infer = "0.11.0"
|
||||
dunce = "1.0.2"
|
||||
slab = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
|
||||
futures-util = { workspace = true }
|
||||
urlencoding = "2.1.2"
|
||||
async-trait = "0.1.68"
|
||||
crossbeam-channel = "0.5.8"
|
||||
|
||||
|
||||
[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"
|
||||
rfd = "0.12"
|
||||
global-hotkey = { git = "https://github.com/tauri-apps/global-hotkey" }
|
||||
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
objc = "0.2.7"
|
||||
|
@ -56,13 +59,12 @@ tokio_runtime = ["tokio"]
|
|||
fullscreen = ["wry/fullscreen"]
|
||||
transparent = ["wry/transparent"]
|
||||
devtools = ["wry/devtools"]
|
||||
tray = ["wry/tray"]
|
||||
dox = ["wry/dox"]
|
||||
hot-reload = ["dioxus-hot-reload"]
|
||||
gnu = []
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
default-features = false
|
||||
features = [ "dox", "tokio_runtime", "hot-reload" ]
|
||||
features = ["tokio_runtime", "hot-reload"]
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus-core-macro = { workspace = true }
|
||||
|
@ -71,6 +73,10 @@ dioxus = { workspace = true }
|
|||
exitcode = "1.1.2"
|
||||
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.
|
||||
[[test]]
|
||||
name = "check_events"
|
||||
|
|
94
packages/desktop/build.rs
Normal file
94
packages/desktop/build.rs
Normal file
|
@ -0,0 +1,94 @@
|
|||
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
|
||||
if std::env::var("CARGO_CFG_WINDOWS").is_ok()
|
||||
&& std::env::var("CARGO_CFG_TARGET_ENV").unwrap() == "gnu"
|
||||
&& !cfg!(feature = "gnu")
|
||||
{
|
||||
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);
|
||||
}
|
|
@ -52,7 +52,7 @@ fn mock_event(cx: &ScopeState, id: &'static str, value: &'static str) {
|
|||
#[allow(deprecated)]
|
||||
fn app(cx: Scope) -> Element {
|
||||
let desktop_context: DesktopContext = cx.consume_context().unwrap();
|
||||
let recieved_events = use_state(cx, || 0);
|
||||
let received_events = use_state(cx, || 0);
|
||||
|
||||
// button
|
||||
mock_event(
|
||||
|
@ -216,7 +216,7 @@ fn app(cx: Scope) -> Element {
|
|||
r#"new FocusEvent("focusout",{bubbles: true})"#,
|
||||
);
|
||||
|
||||
if **recieved_events == 12 {
|
||||
if **received_events == 12 {
|
||||
println!("all events recieved");
|
||||
desktop_context.close();
|
||||
}
|
||||
|
@ -230,8 +230,14 @@ fn app(cx: Scope) -> Element {
|
|||
assert!(event.data.modifiers().is_empty());
|
||||
assert!(event.data.held_buttons().is_empty());
|
||||
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary));
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
received_events.modify(|x| *x + 1)
|
||||
},
|
||||
assert_eq!(
|
||||
event.data.trigger_button(),
|
||||
Some(dioxus_html::input_data::MouseButton::Primary),
|
||||
);
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
div {
|
||||
id: "mouse_move_div",
|
||||
|
@ -239,8 +245,8 @@ fn app(cx: Scope) -> Element {
|
|||
println!("{:?}", event.data);
|
||||
assert!(event.data.modifiers().is_empty());
|
||||
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
},
|
||||
received_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
div {
|
||||
id: "mouse_click_div",
|
||||
|
@ -249,18 +255,18 @@ fn app(cx: Scope) -> Element {
|
|||
assert!(event.data.modifiers().is_empty());
|
||||
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));
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
},
|
||||
received_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
div {
|
||||
id: "mouse_dblclick_div",
|
||||
ondblclick: move |event| {
|
||||
ondoubleclick: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
assert!(event.data.modifiers().is_empty());
|
||||
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary));
|
||||
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));
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
received_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
div {
|
||||
|
@ -270,7 +276,7 @@ fn app(cx: Scope) -> Element {
|
|||
assert!(event.data.modifiers().is_empty());
|
||||
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));
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
received_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
div {
|
||||
|
@ -280,7 +286,7 @@ fn app(cx: Scope) -> Element {
|
|||
assert!(event.data.modifiers().is_empty());
|
||||
assert!(event.data.held_buttons().is_empty());
|
||||
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary));
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
received_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
div {
|
||||
|
@ -291,10 +297,9 @@ fn app(cx: Scope) -> Element {
|
|||
onwheel: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
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));
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
received_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
input {
|
||||
|
@ -306,8 +311,8 @@ fn app(cx: Scope) -> Element {
|
|||
assert_eq!(event.data.code().to_string(), "KeyA");
|
||||
assert_eq!(event.data.location(), Location::Standard);
|
||||
assert!(event.data.is_auto_repeating());
|
||||
received_events.modify(|x| *x + 1)
|
||||
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
input {
|
||||
|
@ -319,8 +324,7 @@ fn app(cx: Scope) -> Element {
|
|||
assert_eq!(event.data.code().to_string(), "KeyA");
|
||||
assert_eq!(event.data.location(), Location::Standard);
|
||||
assert!(!event.data.is_auto_repeating());
|
||||
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
received_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
input {
|
||||
|
@ -332,22 +336,21 @@ fn app(cx: Scope) -> Element {
|
|||
assert_eq!(event.data.code().to_string(), "KeyA");
|
||||
assert_eq!(event.data.location(), Location::Standard);
|
||||
assert!(!event.data.is_auto_repeating());
|
||||
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
received_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
input {
|
||||
id: "focus_in_div",
|
||||
onfocusin: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
received_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
input {
|
||||
id: "focus_out_div",
|
||||
onfocusout: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
received_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ use wry::{
|
|||
application::window::{Window, WindowBuilder},
|
||||
http::{Request as HttpRequest, Response as HttpResponse},
|
||||
webview::FileDropEvent,
|
||||
Result as WryResult,
|
||||
};
|
||||
|
||||
// pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView);
|
||||
|
@ -36,13 +35,14 @@ pub struct Config {
|
|||
pub(crate) root_name: String,
|
||||
pub(crate) background_color: Option<(u8, u8, u8, u8)>,
|
||||
pub(crate) last_window_close_behaviour: WindowCloseBehaviour,
|
||||
pub(crate) enable_default_menu_bar: bool,
|
||||
}
|
||||
|
||||
type DropHandler = Box<dyn Fn(&Window, FileDropEvent) -> bool>;
|
||||
|
||||
pub(crate) type WryProtocol = (
|
||||
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 {
|
||||
|
@ -65,9 +65,18 @@ impl Config {
|
|||
root_name: "main".to_string(),
|
||||
background_color: None,
|
||||
last_window_close_behaviour: WindowCloseBehaviour::LastWindowExitsApp,
|
||||
enable_default_menu_bar: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
pub fn with_resource_directory(mut self, path: impl Into<PathBuf>) -> Self {
|
||||
self.resource_dir = Some(path.into());
|
||||
|
@ -120,7 +129,7 @@ impl Config {
|
|||
/// Set a custom protocol
|
||||
pub fn with_custom_protocol<F>(mut self, name: String, handler: F) -> Self
|
||||
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
|
||||
|
|
|
@ -1,21 +1,27 @@
|
|||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::rc::Weak;
|
||||
|
||||
use crate::create_new_window;
|
||||
use crate::events::IpcMessage;
|
||||
use crate::protocol::AssetFuture;
|
||||
use crate::protocol::AssetHandlerRegistry;
|
||||
use crate::query::QueryEngine;
|
||||
use crate::shortcut::ShortcutId;
|
||||
use crate::shortcut::ShortcutRegistry;
|
||||
use crate::shortcut::ShortcutRegistryError;
|
||||
use crate::shortcut::{HotKey, ShortcutId, ShortcutRegistry, ShortcutRegistryError};
|
||||
use crate::AssetHandler;
|
||||
use crate::Config;
|
||||
use crate::WebviewHandler;
|
||||
use dioxus_core::ScopeState;
|
||||
use dioxus_core::VirtualDom;
|
||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||
use dioxus_hot_reload::HotReloadMsg;
|
||||
use dioxus_interpreter_js::binary_protocol::Channel;
|
||||
use rustc_hash::FxHashMap;
|
||||
use slab::Slab;
|
||||
use wry::application::accelerator::Accelerator;
|
||||
use std::cell::RefCell;
|
||||
use std::fmt::Debug;
|
||||
use std::fmt::Formatter;
|
||||
use std::rc::Rc;
|
||||
use std::rc::Weak;
|
||||
use std::sync::atomic::AtomicU16;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use wry::application::event::Event;
|
||||
use wry::application::event_loop::EventLoopProxy;
|
||||
use wry::application::event_loop::EventLoopWindowTarget;
|
||||
|
@ -28,13 +34,62 @@ use wry::webview::WebView;
|
|||
|
||||
pub type ProxyType = EventLoopProxy<UserWindowEvent>;
|
||||
|
||||
/// Get an imperative handle to the current window without using a hook
|
||||
///
|
||||
/// ## Panics
|
||||
///
|
||||
/// This function will panic if it is called outside of the context of a Dioxus App.
|
||||
pub fn window() -> DesktopContext {
|
||||
dioxus_core::prelude::consume_context().unwrap()
|
||||
}
|
||||
|
||||
/// Get an imperative handle to the current window
|
||||
#[deprecated = "Prefer the using the `window` function directly for cleaner code"]
|
||||
pub fn use_window(cx: &ScopeState) -> &DesktopContext {
|
||||
cx.use_hook(|| cx.consume_context::<DesktopContext>())
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// This handles communication between the requests that the webview makes and the interpreter. The interpreter constantly makes long running requests to the webview to get any edits that should be made to the DOM almost like server side events.
|
||||
/// It will hold onto the requests until the interpreter is ready to handle them and hold onto any pending edits until a new request is made.
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct EditQueue {
|
||||
queue: Arc<Mutex<Vec<Vec<u8>>>>,
|
||||
responder: Arc<Mutex<Option<wry::webview::RequestAsyncResponder>>>,
|
||||
}
|
||||
|
||||
impl Debug for EditQueue {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("EditQueue")
|
||||
.field("queue", &self.queue)
|
||||
.field("responder", {
|
||||
&self.responder.lock().unwrap().as_ref().map(|_| ())
|
||||
})
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl EditQueue {
|
||||
pub fn handle_request(&self, responder: wry::webview::RequestAsyncResponder) {
|
||||
let mut queue = self.queue.lock().unwrap();
|
||||
if let Some(bytes) = queue.pop() {
|
||||
responder.respond(wry::http::Response::new(bytes));
|
||||
} else {
|
||||
*self.responder.lock().unwrap() = Some(responder);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_edits(&self, edits: Vec<u8>) {
|
||||
let mut responder = self.responder.lock().unwrap();
|
||||
if let Some(responder) = responder.take() {
|
||||
responder.respond(wry::http::Response::new(edits));
|
||||
} else {
|
||||
self.queue.lock().unwrap().push(edits);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type WebviewQueue = Rc<RefCell<Vec<WebviewHandler>>>;
|
||||
|
||||
/// An imperative interface to the current window.
|
||||
|
@ -67,6 +122,13 @@ pub struct DesktopService {
|
|||
|
||||
pub(crate) shortcut_manager: ShortcutRegistry,
|
||||
|
||||
pub(crate) edit_queue: EditQueue,
|
||||
pub(crate) templates: RefCell<FxHashMap<String, u16>>,
|
||||
pub(crate) max_template_count: AtomicU16,
|
||||
|
||||
pub(crate) channel: RefCell<Channel>,
|
||||
pub(crate) asset_handlers: AssetHandlerRegistry,
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
pub(crate) views: Rc<RefCell<Vec<*mut objc::runtime::Object>>>,
|
||||
}
|
||||
|
@ -91,6 +153,8 @@ impl DesktopService {
|
|||
webviews: WebviewQueue,
|
||||
event_handlers: WindowEventHandlers,
|
||||
shortcut_manager: ShortcutRegistry,
|
||||
edit_queue: EditQueue,
|
||||
asset_handlers: AssetHandlerRegistry,
|
||||
) -> Self {
|
||||
Self {
|
||||
webview: Rc::new(webview),
|
||||
|
@ -100,6 +164,11 @@ impl DesktopService {
|
|||
pending_windows: webviews,
|
||||
event_handlers,
|
||||
shortcut_manager,
|
||||
edit_queue,
|
||||
templates: Default::default(),
|
||||
max_template_count: Default::default(),
|
||||
channel: Default::default(),
|
||||
asset_handlers,
|
||||
#[cfg(target_os = "ios")]
|
||||
views: Default::default(),
|
||||
}
|
||||
|
@ -233,11 +302,11 @@ impl DesktopService {
|
|||
/// Linux: Only works on x11. See [this issue](https://github.com/tauri-apps/tao/issues/331) for more information.
|
||||
pub fn create_shortcut(
|
||||
&self,
|
||||
accelerator: Accelerator,
|
||||
hotkey: HotKey,
|
||||
callback: impl FnMut() + 'static,
|
||||
) -> Result<ShortcutId, ShortcutRegistryError> {
|
||||
self.shortcut_manager
|
||||
.add_shortcut(accelerator, Box::new(callback))
|
||||
.add_shortcut(hotkey, Box::new(callback))
|
||||
}
|
||||
|
||||
/// Remove a global shortcut
|
||||
|
@ -250,6 +319,20 @@ impl DesktopService {
|
|||
self.shortcut_manager.remove_all()
|
||||
}
|
||||
|
||||
/// Provide a callback to handle asset loading yourself.
|
||||
///
|
||||
/// See [`use_asset_handle`](crate::use_asset_handle) for a convenient hook.
|
||||
pub async fn register_asset_handler<F: AssetFuture>(&self, f: impl AssetHandler<F>) -> usize {
|
||||
self.asset_handlers.register_handler(f).await
|
||||
}
|
||||
|
||||
/// Removes an asset handler by its identifier.
|
||||
///
|
||||
/// Returns `None` if the handler did not exist.
|
||||
pub async fn remove_asset_handler(&self, id: usize) -> Option<()> {
|
||||
self.asset_handlers.remove_handler(id).await
|
||||
}
|
||||
|
||||
/// Push an objc view to the window
|
||||
#[cfg(target_os = "ios")]
|
||||
pub fn push_view(&self, view: objc_id::ShareId<objc::runtime::Object>) {
|
||||
|
@ -369,18 +452,11 @@ impl WryWindowEventHandlerInner {
|
|||
target: &EventLoopWindowTarget<UserWindowEvent>,
|
||||
) {
|
||||
// if this event does not apply to the window this listener cares about, return
|
||||
match event {
|
||||
Event::WindowEvent { window_id, .. }
|
||||
| Event::MenuEvent {
|
||||
window_id: Some(window_id),
|
||||
..
|
||||
} => {
|
||||
if let Event::WindowEvent { window_id, .. } = event {
|
||||
if *window_id != self.window_id {
|
||||
return;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
(self.handler)(event, target)
|
||||
}
|
||||
}
|
||||
|
@ -390,9 +466,8 @@ pub fn use_wry_event_handler(
|
|||
cx: &ScopeState,
|
||||
handler: impl FnMut(&Event<UserWindowEvent>, &EventLoopWindowTarget<UserWindowEvent>) + 'static,
|
||||
) -> &WryEventHandler {
|
||||
let desktop = use_window(cx);
|
||||
cx.use_hook(move || {
|
||||
let desktop = desktop.clone();
|
||||
let desktop = window();
|
||||
|
||||
let id = desktop.create_wry_event_handler(handler);
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ impl RenderedElementBacking for DesktopElement {
|
|||
>,
|
||||
>,
|
||||
> {
|
||||
let script = format!("return window.interpreter.GetClientRect({});", self.id.0);
|
||||
let script = format!("return window.interpreter.getClientRect({});", self.id.0);
|
||||
|
||||
let fut = self
|
||||
.query
|
||||
|
@ -55,7 +55,7 @@ impl RenderedElementBacking for DesktopElement {
|
|||
behavior: dioxus_html::ScrollBehavior,
|
||||
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
|
||||
let script = format!(
|
||||
"return window.interpreter.ScrollTo({}, {});",
|
||||
"return window.interpreter.scrollTo({}, {});",
|
||||
self.id.0,
|
||||
serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior")
|
||||
);
|
||||
|
@ -82,7 +82,7 @@ impl RenderedElementBacking for DesktopElement {
|
|||
focus: bool,
|
||||
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
|
||||
let script = format!(
|
||||
"return window.interpreter.SetFocus({}, {});",
|
||||
"return window.interpreter.setFocus({}, {});",
|
||||
self.id.0, focus
|
||||
);
|
||||
|
||||
|
|
|
@ -10,33 +10,38 @@ mod escape;
|
|||
mod eval;
|
||||
mod events;
|
||||
mod file_upload;
|
||||
#[cfg(any(target_os = "ios", target_os = "android"))]
|
||||
mod mobile_shortcut;
|
||||
mod protocol;
|
||||
mod query;
|
||||
mod shortcut;
|
||||
mod waker;
|
||||
mod webview;
|
||||
|
||||
#[cfg(any(target_os = "ios", target_os = "android"))]
|
||||
mod mobile_shortcut;
|
||||
|
||||
use crate::query::QueryResult;
|
||||
use crate::shortcut::GlobalHotKeyEvent;
|
||||
pub use cfg::{Config, WindowCloseBehaviour};
|
||||
pub use desktop_context::DesktopContext;
|
||||
#[allow(deprecated)]
|
||||
pub use desktop_context::{
|
||||
use_window, use_wry_event_handler, DesktopService, WryEventHandler, WryEventHandlerId,
|
||||
use_window, use_wry_event_handler, window, DesktopService, WryEventHandler, WryEventHandlerId,
|
||||
};
|
||||
use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers};
|
||||
use dioxus_core::*;
|
||||
use dioxus_html::{native_bind::NativeFileEngine, HtmlEvent};
|
||||
use dioxus_html::{FileEngine, HasFormData, MountedData, PlatformEventData};
|
||||
use dioxus_html::{event_bubbles, FileEngine, HasFormData, MountedData, PlatformEventData};
|
||||
use dioxus_interpreter_js::binary_protocol::Channel;
|
||||
use element::DesktopElement;
|
||||
use eval::init_eval;
|
||||
use events::SerializedHtmlEventConverter;
|
||||
use futures_util::{pin_mut, FutureExt};
|
||||
pub use protocol::{use_asset_handler, AssetFuture, AssetHandler, AssetRequest, AssetResponse};
|
||||
use rustc_hash::FxHashMap;
|
||||
use shortcut::ShortcutRegistry;
|
||||
pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::AtomicU16;
|
||||
use std::task::Waker;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
pub use tao::dpi::{LogicalSize, PhysicalSize};
|
||||
|
@ -44,10 +49,12 @@ use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
|
|||
pub use tao::window::WindowBuilder;
|
||||
use tao::{
|
||||
event::{Event, StartCause, WindowEvent},
|
||||
event_loop::{ControlFlow, EventLoop},
|
||||
event_loop::ControlFlow,
|
||||
};
|
||||
// pub use webview::build_default_menu_bar;
|
||||
pub use wry;
|
||||
pub use wry::application as tao;
|
||||
use wry::application::event_loop::EventLoopBuilder;
|
||||
use wry::webview::WebView;
|
||||
use wry::{application::window::WindowId, webview::WebContext};
|
||||
|
||||
|
@ -55,7 +62,7 @@ use wry::{application::window::WindowId, webview::WebContext};
|
|||
///
|
||||
/// This function will start a multithreaded Tokio runtime as well the WebView event loop.
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// ```rust, no_run
|
||||
/// use dioxus::prelude::*;
|
||||
///
|
||||
/// fn main() {
|
||||
|
@ -78,11 +85,12 @@ pub fn launch(root: Component) {
|
|||
///
|
||||
/// You can configure the WebView window with a configuration closure
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// ```rust, no_run
|
||||
/// use dioxus::prelude::*;
|
||||
/// use dioxus_desktop::*;
|
||||
///
|
||||
/// fn main() {
|
||||
/// dioxus_desktop::launch_cfg(app, |c| c.with_window(|w| w.with_title("My App")));
|
||||
/// dioxus_desktop::launch_cfg(app, Config::default().with_window(WindowBuilder::new().with_title("My App")));
|
||||
/// }
|
||||
///
|
||||
/// fn app(cx: Scope) -> Element {
|
||||
|
@ -101,8 +109,9 @@ pub fn launch_cfg(root: Component, config_builder: Config) {
|
|||
///
|
||||
/// You can configure the WebView window with a configuration closure
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// ```rust, no_run
|
||||
/// use dioxus::prelude::*;
|
||||
/// use dioxus_desktop::Config;
|
||||
///
|
||||
/// fn main() {
|
||||
/// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, Config::default());
|
||||
|
@ -119,7 +128,7 @@ pub fn launch_cfg(root: Component, config_builder: Config) {
|
|||
/// }
|
||||
/// ```
|
||||
pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config) {
|
||||
let event_loop = EventLoop::<UserWindowEvent>::with_user_event();
|
||||
let event_loop = EventLoopBuilder::<UserWindowEvent>::with_user_event().build();
|
||||
|
||||
let proxy = event_loop.create_proxy();
|
||||
|
||||
|
@ -159,7 +168,8 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
|||
|
||||
let queue = WebviewQueue::default();
|
||||
|
||||
let shortcut_manager = ShortcutRegistry::new(&event_loop);
|
||||
let shortcut_manager = ShortcutRegistry::new();
|
||||
let global_hotkey_channel = GlobalHotKeyEvent::receiver();
|
||||
|
||||
// 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
|
||||
|
@ -168,10 +178,14 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
|||
let mut is_visible_before_start = true;
|
||||
|
||||
event_loop.run(move |window_event, event_loop, control_flow| {
|
||||
*control_flow = ControlFlow::Wait;
|
||||
*control_flow = ControlFlow::Poll;
|
||||
|
||||
event_handlers.apply_event(&window_event, event_loop);
|
||||
|
||||
if let Ok(event) = global_hotkey_channel.try_recv() {
|
||||
shortcut_manager.call_handlers(event);
|
||||
}
|
||||
|
||||
match window_event {
|
||||
Event::WindowEvent {
|
||||
event, window_id, ..
|
||||
|
@ -274,7 +288,10 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
|||
|
||||
let evt = match serde_json::from_value::<HtmlEvent>(params) {
|
||||
Ok(value) => value,
|
||||
Err(_) => return,
|
||||
Err(err) => {
|
||||
tracing::error!("Error parsing user_event: {:?}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let HtmlEvent {
|
||||
|
@ -306,7 +323,7 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
|||
|
||||
view.dom.handle_event(&name, as_any, element, bubbles);
|
||||
|
||||
send_edits(view.dom.render_immediate(), &view.desktop_context.webview);
|
||||
send_edits(view.dom.render_immediate(), &view.desktop_context);
|
||||
}
|
||||
|
||||
// When the webview sends a query, we need to send it to the query manager which handles dispatching the data to the correct pending query
|
||||
|
@ -329,7 +346,7 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
|||
|
||||
EventData::Ipc(msg) if msg.method() == "initialize" => {
|
||||
let view = webviews.get_mut(&event.1).unwrap();
|
||||
send_edits(view.dom.rebuild(), &view.desktop_context.webview);
|
||||
send_edits(view.dom.rebuild(), &view.desktop_context);
|
||||
view.desktop_context
|
||||
.webview
|
||||
.window()
|
||||
|
@ -384,13 +401,12 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
|||
view.dom.handle_event(event_name, data, id, event_bubbles);
|
||||
}
|
||||
|
||||
send_edits(view.dom.render_immediate(), &view.desktop_context.webview);
|
||||
send_edits(view.dom.render_immediate(), &view.desktop_context);
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
},
|
||||
Event::GlobalShortcutEvent(id) => shortcut_manager.call_handlers(id),
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
|
@ -405,7 +421,8 @@ fn create_new_window(
|
|||
event_handlers: &WindowEventHandlers,
|
||||
shortcut_manager: ShortcutRegistry,
|
||||
) -> WebviewHandler {
|
||||
let (webview, web_context) = webview::build(&mut cfg, event_loop, proxy.clone());
|
||||
let (webview, web_context, asset_handlers, edit_queue) =
|
||||
webview::build(&mut cfg, event_loop, proxy.clone());
|
||||
let desktop_context = Rc::from(DesktopService::new(
|
||||
webview,
|
||||
proxy.clone(),
|
||||
|
@ -413,6 +430,8 @@ fn create_new_window(
|
|||
queue.clone(),
|
||||
event_handlers.clone(),
|
||||
shortcut_manager,
|
||||
edit_queue,
|
||||
asset_handlers,
|
||||
));
|
||||
|
||||
let cx = dom.base_scope();
|
||||
|
@ -459,16 +478,149 @@ fn poll_vdom(view: &mut WebviewHandler) {
|
|||
}
|
||||
}
|
||||
|
||||
send_edits(view.dom.render_immediate(), &view.desktop_context.webview);
|
||||
send_edits(view.dom.render_immediate(), &view.desktop_context);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a list of mutations to the webview
|
||||
fn send_edits(edits: Mutations, webview: &WebView) {
|
||||
let serialized = serde_json::to_string(&edits).unwrap();
|
||||
fn send_edits(edits: Mutations, desktop_context: &DesktopContext) {
|
||||
let mut channel = desktop_context.channel.borrow_mut();
|
||||
let mut templates = desktop_context.templates.borrow_mut();
|
||||
if let Some(bytes) = apply_edits(
|
||||
edits,
|
||||
&mut channel,
|
||||
&mut templates,
|
||||
&desktop_context.max_template_count,
|
||||
) {
|
||||
desktop_context.edit_queue.add_edits(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
// todo: use SSE and binary data to send the edits with lower overhead
|
||||
_ = webview.evaluate_script(&format!("window.interpreter.handleEdits({serialized})"));
|
||||
fn apply_edits(
|
||||
mutations: Mutations,
|
||||
channel: &mut Channel,
|
||||
templates: &mut FxHashMap<String, u16>,
|
||||
max_template_count: &AtomicU16,
|
||||
) -> Option<Vec<u8>> {
|
||||
use dioxus_core::Mutation::*;
|
||||
if mutations.templates.is_empty() && mutations.edits.is_empty() {
|
||||
return None;
|
||||
}
|
||||
for template in mutations.templates {
|
||||
add_template(&template, channel, templates, max_template_count);
|
||||
}
|
||||
for edit in mutations.edits {
|
||||
match edit {
|
||||
AppendChildren { id, m } => channel.append_children(id.0 as u32, m as u16),
|
||||
AssignId { path, id } => channel.assign_id(path, id.0 as u32),
|
||||
CreatePlaceholder { id } => channel.create_placeholder(id.0 as u32),
|
||||
CreateTextNode { value, id } => channel.create_text_node(value, id.0 as u32),
|
||||
HydrateText { path, value, id } => channel.hydrate_text(path, value, id.0 as u32),
|
||||
LoadTemplate { name, index, id } => {
|
||||
if let Some(tmpl_id) = templates.get(name) {
|
||||
channel.load_template(*tmpl_id, index as u16, id.0 as u32)
|
||||
}
|
||||
}
|
||||
ReplaceWith { id, m } => channel.replace_with(id.0 as u32, m as u16),
|
||||
ReplacePlaceholder { path, m } => channel.replace_placeholder(path, m as u16),
|
||||
InsertAfter { id, m } => channel.insert_after(id.0 as u32, m as u16),
|
||||
InsertBefore { id, m } => channel.insert_before(id.0 as u32, m as u16),
|
||||
SetAttribute {
|
||||
name,
|
||||
value,
|
||||
id,
|
||||
ns,
|
||||
} => match value {
|
||||
BorrowedAttributeValue::Text(txt) => {
|
||||
channel.set_attribute(id.0 as u32, name, txt, ns.unwrap_or_default())
|
||||
}
|
||||
BorrowedAttributeValue::Float(f) => {
|
||||
channel.set_attribute(id.0 as u32, name, &f.to_string(), ns.unwrap_or_default())
|
||||
}
|
||||
BorrowedAttributeValue::Int(n) => {
|
||||
channel.set_attribute(id.0 as u32, name, &n.to_string(), ns.unwrap_or_default())
|
||||
}
|
||||
BorrowedAttributeValue::Bool(b) => channel.set_attribute(
|
||||
id.0 as u32,
|
||||
name,
|
||||
if b { "true" } else { "false" },
|
||||
ns.unwrap_or_default(),
|
||||
),
|
||||
BorrowedAttributeValue::None => {
|
||||
channel.remove_attribute(id.0 as u32, name, ns.unwrap_or_default())
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
SetText { value, id } => channel.set_text(id.0 as u32, value),
|
||||
NewEventListener { name, id, .. } => {
|
||||
channel.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8)
|
||||
}
|
||||
RemoveEventListener { name, id } => {
|
||||
channel.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8)
|
||||
}
|
||||
Remove { id } => channel.remove(id.0 as u32),
|
||||
PushRoot { id } => channel.push_root(id.0 as u32),
|
||||
}
|
||||
}
|
||||
|
||||
let bytes: Vec<_> = channel.export_memory().collect();
|
||||
channel.reset();
|
||||
Some(bytes)
|
||||
}
|
||||
|
||||
fn add_template(
|
||||
template: &Template<'static>,
|
||||
channel: &mut Channel,
|
||||
templates: &mut FxHashMap<String, u16>,
|
||||
max_template_count: &AtomicU16,
|
||||
) {
|
||||
let current_max_template_count = max_template_count.load(std::sync::atomic::Ordering::Relaxed);
|
||||
for root in template.roots.iter() {
|
||||
create_template_node(channel, root);
|
||||
templates.insert(template.name.to_owned(), current_max_template_count);
|
||||
}
|
||||
channel.add_templates(current_max_template_count, template.roots.len() as u16);
|
||||
|
||||
max_template_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn create_template_node(channel: &mut Channel, v: &'static TemplateNode<'static>) {
|
||||
use TemplateNode::*;
|
||||
match v {
|
||||
Element {
|
||||
tag,
|
||||
namespace,
|
||||
attrs,
|
||||
children,
|
||||
..
|
||||
} => {
|
||||
// Push the current node onto the stack
|
||||
match namespace {
|
||||
Some(ns) => channel.create_element_ns(tag, ns),
|
||||
None => channel.create_element(tag),
|
||||
}
|
||||
// Set attributes on the current node
|
||||
for attr in *attrs {
|
||||
if let TemplateAttribute::Static {
|
||||
name,
|
||||
value,
|
||||
namespace,
|
||||
} = attr
|
||||
{
|
||||
channel.set_top_attribute(name, value, namespace.unwrap_or_default())
|
||||
}
|
||||
}
|
||||
// Add each child to the stack
|
||||
for child in *children {
|
||||
create_template_node(channel, child);
|
||||
}
|
||||
// Add all children to the parent
|
||||
channel.append_children_to_top(children.len() as u16);
|
||||
}
|
||||
Text { text } => channel.create_raw_text(text),
|
||||
DynamicText { .. } => channel.create_raw_text("p"),
|
||||
Dynamic { .. } => channel.add_placeholder(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Different hide implementations per platform
|
||||
|
|
|
@ -1,29 +1,51 @@
|
|||
#![allow(unused)]
|
||||
|
||||
use super::*;
|
||||
use wry::application::accelerator::Accelerator;
|
||||
use std::str::FromStr;
|
||||
use wry::application::event_loop::EventLoopWindowTarget;
|
||||
|
||||
pub struct GlobalShortcut();
|
||||
pub struct ShortcutManager();
|
||||
use dioxus_html::input_data::keyboard_types::Modifiers;
|
||||
|
||||
impl ShortcutManager {
|
||||
pub fn new<T>(target: &EventLoopWindowTarget<T>) -> Self {
|
||||
Self()
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Accelerator;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct HotKey;
|
||||
|
||||
impl HotKey {
|
||||
pub fn new(mods: Option<Modifiers>, key: Code) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn register(
|
||||
&mut self,
|
||||
accelerator: Accelerator,
|
||||
) -> Result<GlobalShortcut, ShortcutManagerError> {
|
||||
Ok(GlobalShortcut())
|
||||
pub fn id(&self) -> u32 {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unregister(&mut self, id: ShortcutId) -> Result<(), ShortcutManagerError> {
|
||||
impl FromStr for HotKey {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(HotKey)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GlobalHotKeyManager();
|
||||
|
||||
impl GlobalHotKeyManager {
|
||||
pub fn new() -> Result<Self, HotkeyError> {
|
||||
Ok(Self())
|
||||
}
|
||||
|
||||
pub fn register(&mut self, accelerator: HotKey) -> Result<HotKey, HotkeyError> {
|
||||
Ok(HotKey)
|
||||
}
|
||||
|
||||
pub fn unregister(&mut self, id: HotKey) -> Result<(), HotkeyError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn unregister_all(&mut self) -> Result<(), ShortcutManagerError> {
|
||||
pub fn unregister_all(&mut self, _: &[HotKey]) -> Result<(), HotkeyError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -33,23 +55,35 @@ use std::{error, fmt};
|
|||
/// An error whose cause the `ShortcutManager` to fail.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug)]
|
||||
pub enum ShortcutManagerError {
|
||||
pub enum HotkeyError {
|
||||
AcceleratorAlreadyRegistered(Accelerator),
|
||||
AcceleratorNotRegistered(Accelerator),
|
||||
InvalidAccelerator(String),
|
||||
HotKeyParseError(String),
|
||||
}
|
||||
|
||||
impl error::Error for ShortcutManagerError {}
|
||||
impl fmt::Display for ShortcutManagerError {
|
||||
impl error::Error for HotkeyError {}
|
||||
impl fmt::Display for HotkeyError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
match self {
|
||||
ShortcutManagerError::AcceleratorAlreadyRegistered(e) => {
|
||||
HotkeyError::AcceleratorAlreadyRegistered(e) => {
|
||||
f.pad(&format!("hotkey already registered: {:?}", e))
|
||||
}
|
||||
ShortcutManagerError::AcceleratorNotRegistered(e) => {
|
||||
HotkeyError::AcceleratorNotRegistered(e) => {
|
||||
f.pad(&format!("hotkey not registered: {:?}", e))
|
||||
}
|
||||
ShortcutManagerError::InvalidAccelerator(e) => e.fmt(f),
|
||||
HotkeyError::HotKeyParseError(e) => e.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GlobalHotKeyEvent {
|
||||
pub id: u32,
|
||||
}
|
||||
|
||||
impl GlobalHotKeyEvent {
|
||||
pub fn receiver() -> crossbeam_channel::Receiver<GlobalHotKeyEvent> {
|
||||
crossbeam_channel::unbounded().1
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type Code = dioxus_html::input_data::keyboard_types::Code;
|
||||
|
|
|
@ -1,14 +1,30 @@
|
|||
use dioxus_interpreter_js::{COMMON_JS, INTERPRETER_JS};
|
||||
use crate::{window, DesktopContext};
|
||||
use dioxus_core::ScopeState;
|
||||
use dioxus_interpreter_js::INTERPRETER_JS;
|
||||
use slab::Slab;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
future::Future,
|
||||
ops::Deref,
|
||||
path::{Path, PathBuf},
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::{
|
||||
runtime::Handle,
|
||||
sync::{OnceCell, RwLock},
|
||||
};
|
||||
use wry::{
|
||||
http::{status::StatusCode, Request, Response},
|
||||
Result,
|
||||
};
|
||||
|
||||
fn module_loader(root_name: &str) -> String {
|
||||
use crate::desktop_context::EditQueue;
|
||||
|
||||
static MINIFIED: &str = include_str!("./minified.js");
|
||||
|
||||
fn module_loader(root_name: &str, headless: bool) -> String {
|
||||
let js = INTERPRETER_JS.replace(
|
||||
"/*POST_HANDLE_EDITS*/",
|
||||
r#"// Prevent file inputs from opening the file dialog on click
|
||||
|
@ -35,35 +51,189 @@ fn module_loader(root_name: &str) -> String {
|
|||
}
|
||||
}"#,
|
||||
);
|
||||
|
||||
format!(
|
||||
r#"
|
||||
<script type="module">
|
||||
{js}
|
||||
|
||||
{MINIFIED}
|
||||
// Wait for the page to load
|
||||
window.onload = function() {{
|
||||
let rootname = "{root_name}";
|
||||
let root = window.document.getElementById(rootname);
|
||||
if (root != null) {{
|
||||
window.interpreter = new Interpreter(root, new InterpreterConfig(true));
|
||||
window.ipc.postMessage(serializeIpcMessage("initialize"));
|
||||
let root_element = window.document.getElementById(rootname);
|
||||
if (root_element != null) {{
|
||||
window.interpreter.initialize(root_element);
|
||||
window.ipc.postMessage(window.interpreter.serializeIpcMessage("initialize"));
|
||||
}}
|
||||
window.interpreter.wait_for_request({headless});
|
||||
}}
|
||||
</script>
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn desktop_handler(
|
||||
request: &Request<Vec<u8>>,
|
||||
/// An arbitrary asset is an HTTP response containing a binary body.
|
||||
pub type AssetResponse = Response<Cow<'static, [u8]>>;
|
||||
|
||||
/// A future that returns an [`AssetResponse`]. This future may be spawned in a new thread,
|
||||
/// so it must be [`Send`], [`Sync`], and `'static`.
|
||||
pub trait AssetFuture: Future<Output = Option<AssetResponse>> + Send + Sync + 'static {}
|
||||
impl<T: Future<Output = Option<AssetResponse>> + Send + Sync + 'static> AssetFuture for T {}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// A request for an asset. This is a wrapper around [`Request<Vec<u8>>`] that provides methods specific to asset requests.
|
||||
pub struct AssetRequest {
|
||||
path: PathBuf,
|
||||
request: Arc<Request<Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl AssetRequest {
|
||||
/// Get the path the asset request is for
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Request<Vec<u8>>> for AssetRequest {
|
||||
fn from(request: Request<Vec<u8>>) -> Self {
|
||||
let decoded = urlencoding::decode(request.uri().path().trim_start_matches('/'))
|
||||
.expect("expected URL to be UTF-8 encoded");
|
||||
let path = PathBuf::from(&*decoded);
|
||||
Self {
|
||||
request: Arc::new(request),
|
||||
path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for AssetRequest {
|
||||
type Target = Request<Vec<u8>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.request
|
||||
}
|
||||
}
|
||||
|
||||
/// A handler that takes an [`AssetRequest`] and returns a future that either loads the asset, or returns `None`.
|
||||
/// This handler is stashed indefinitely in a context object, so it must be `'static`.
|
||||
pub trait AssetHandler<F: AssetFuture>: Send + Sync + 'static {
|
||||
/// Handle an asset request, returning a future that either loads the asset, or returns `None`
|
||||
fn handle_request(&self, request: &AssetRequest) -> F;
|
||||
}
|
||||
|
||||
impl<F: AssetFuture, T: Fn(&AssetRequest) -> F + Send + Sync + 'static> AssetHandler<F> for T {
|
||||
fn handle_request(&self, request: &AssetRequest) -> F {
|
||||
self(request)
|
||||
}
|
||||
}
|
||||
|
||||
type AssetHandlerRegistryInner =
|
||||
Slab<Box<dyn Fn(&AssetRequest) -> Pin<Box<dyn AssetFuture>> + Send + Sync + 'static>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AssetHandlerRegistry(Arc<RwLock<AssetHandlerRegistryInner>>);
|
||||
|
||||
impl AssetHandlerRegistry {
|
||||
pub fn new() -> Self {
|
||||
AssetHandlerRegistry(Arc::new(RwLock::new(Slab::new())))
|
||||
}
|
||||
|
||||
pub async fn register_handler<F: AssetFuture>(&self, f: impl AssetHandler<F>) -> usize {
|
||||
let mut registry = self.0.write().await;
|
||||
registry.insert(Box::new(move |req| Box::pin(f.handle_request(req))))
|
||||
}
|
||||
|
||||
pub async fn remove_handler(&self, id: usize) -> Option<()> {
|
||||
let mut registry = self.0.write().await;
|
||||
registry.try_remove(id).map(|_| ())
|
||||
}
|
||||
|
||||
pub async fn try_handlers(&self, req: &AssetRequest) -> Option<AssetResponse> {
|
||||
let registry = self.0.read().await;
|
||||
for (_, handler) in registry.iter() {
|
||||
if let Some(response) = handler(req).await {
|
||||
return Some(response);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// A handle to a registered asset handler.
|
||||
pub struct AssetHandlerHandle {
|
||||
desktop: DesktopContext,
|
||||
handler_id: Rc<OnceCell<usize>>,
|
||||
}
|
||||
|
||||
impl AssetHandlerHandle {
|
||||
/// Returns the ID for this handle.
|
||||
///
|
||||
/// Because registering an ID is asynchronous, this may return `None` if the
|
||||
/// registration has not completed yet.
|
||||
pub fn handler_id(&self) -> Option<usize> {
|
||||
self.handler_id.get().copied()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AssetHandlerHandle {
|
||||
fn drop(&mut self) {
|
||||
let cell = Rc::clone(&self.handler_id);
|
||||
let desktop = Rc::clone(&self.desktop);
|
||||
tokio::task::block_in_place(move || {
|
||||
Handle::current().block_on(async move {
|
||||
if let Some(id) = cell.get() {
|
||||
desktop.asset_handlers.remove_handler(*id).await;
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Provide a callback to handle asset loading yourself.
|
||||
///
|
||||
/// The callback takes a path as requested by the web view, and it should return `Some(response)`
|
||||
/// if you want to load the asset, and `None` if you want to fallback on the default behavior.
|
||||
pub fn use_asset_handler<F: AssetFuture>(
|
||||
cx: &ScopeState,
|
||||
handler: impl AssetHandler<F>,
|
||||
) -> &AssetHandlerHandle {
|
||||
cx.use_hook(|| {
|
||||
let desktop = window();
|
||||
let handler_id = Rc::new(OnceCell::new());
|
||||
let handler_id_ref = Rc::clone(&handler_id);
|
||||
let desktop_ref = Rc::clone(&desktop);
|
||||
cx.push_future(async move {
|
||||
let id = desktop.asset_handlers.register_handler(handler).await;
|
||||
handler_id.set(id).unwrap();
|
||||
});
|
||||
AssetHandlerHandle {
|
||||
desktop: desktop_ref,
|
||||
handler_id: handler_id_ref,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) async fn desktop_handler(
|
||||
request: Request<Vec<u8>>,
|
||||
custom_head: Option<String>,
|
||||
custom_index: Option<String>,
|
||||
root_name: &str,
|
||||
) -> Result<Response<Cow<'static, [u8]>>> {
|
||||
asset_handlers: &AssetHandlerRegistry,
|
||||
edit_queue: &EditQueue,
|
||||
headless: bool,
|
||||
responder: wry::webview::RequestAsyncResponder,
|
||||
) {
|
||||
let request = AssetRequest::from(request);
|
||||
|
||||
// If the request is for the root, we'll serve the index.html file.
|
||||
if request.uri().path() == "/" {
|
||||
// If a custom index is provided, just defer to that, expecting the user to know what they're doing.
|
||||
// we'll look for the closing </body> tag and insert our little module loader there.
|
||||
let body = match custom_index {
|
||||
Some(custom_index) => custom_index
|
||||
.replace("</body>", &format!("{}</body>", module_loader(root_name)))
|
||||
.replace(
|
||||
"</body>",
|
||||
&format!("{}</body>", module_loader(root_name, headless)),
|
||||
)
|
||||
.into_bytes(),
|
||||
|
||||
None => {
|
||||
|
@ -75,47 +245,84 @@ pub(super) fn desktop_handler(
|
|||
}
|
||||
|
||||
template
|
||||
.replace("<!-- MODULE LOADER -->", &module_loader(root_name))
|
||||
.replace(
|
||||
"<!-- MODULE LOADER -->",
|
||||
&module_loader(root_name, headless),
|
||||
)
|
||||
.into_bytes()
|
||||
}
|
||||
};
|
||||
|
||||
return Response::builder()
|
||||
match Response::builder()
|
||||
.header("Content-Type", "text/html")
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.body(Cow::from(body))
|
||||
.map_err(From::from);
|
||||
} else if request.uri().path() == "/common.js" {
|
||||
return Response::builder()
|
||||
.header("Content-Type", "text/javascript")
|
||||
.body(Cow::from(COMMON_JS.as_bytes()))
|
||||
.map_err(From::from);
|
||||
{
|
||||
Ok(response) => {
|
||||
responder.respond(response);
|
||||
return;
|
||||
}
|
||||
Err(err) => tracing::error!("error building response: {}", err),
|
||||
}
|
||||
} else if request.uri().path().trim_matches('/') == "edits" {
|
||||
edit_queue.handle_request(responder);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user provided a custom asset handler, then call it and return the response
|
||||
// if the request was handled.
|
||||
if let Some(response) = asset_handlers.try_handlers(&request).await {
|
||||
responder.respond(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// Else, try to serve a file from the filesystem.
|
||||
let decoded = urlencoding::decode(request.uri().path().trim_start_matches('/'))
|
||||
.expect("expected URL to be UTF-8 encoded");
|
||||
let path = PathBuf::from(&*decoded);
|
||||
|
||||
// If the path is relative, we'll try to serve it from the assets directory.
|
||||
let mut asset = get_asset_root()
|
||||
.unwrap_or_else(|| Path::new(".").to_path_buf())
|
||||
.join(&path);
|
||||
.join(&request.path);
|
||||
|
||||
if !asset.exists() {
|
||||
asset = PathBuf::from("/").join(path);
|
||||
asset = PathBuf::from("/").join(request.path);
|
||||
}
|
||||
|
||||
if asset.exists() {
|
||||
return Response::builder()
|
||||
.header("Content-Type", get_mime_from_path(&asset)?)
|
||||
.body(Cow::from(std::fs::read(asset)?))
|
||||
.map_err(From::from);
|
||||
let content_type = match get_mime_from_path(&asset) {
|
||||
Ok(content_type) => content_type,
|
||||
Err(err) => {
|
||||
tracing::error!("error getting mime type: {}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let asset = match std::fs::read(asset) {
|
||||
Ok(asset) => asset,
|
||||
Err(err) => {
|
||||
tracing::error!("error reading asset: {}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
match Response::builder()
|
||||
.header("Content-Type", content_type)
|
||||
.body(Cow::from(asset))
|
||||
{
|
||||
Ok(response) => {
|
||||
responder.respond(response);
|
||||
return;
|
||||
}
|
||||
Err(err) => tracing::error!("error building response: {}", err),
|
||||
}
|
||||
}
|
||||
|
||||
Response::builder()
|
||||
match Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(Cow::from(String::from("Not Found").into_bytes()))
|
||||
.map_err(From::from)
|
||||
{
|
||||
Ok(response) => {
|
||||
responder.respond(response);
|
||||
}
|
||||
Err(err) => tracing::error!("error building response: {}", err),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
|
|
|
@ -3,13 +3,9 @@ use std::{cell::RefCell, collections::HashMap, rc::Rc, str::FromStr};
|
|||
use dioxus_core::ScopeState;
|
||||
use dioxus_html::input_data::keyboard_types::Modifiers;
|
||||
use slab::Slab;
|
||||
use wry::application::{
|
||||
accelerator::{Accelerator, AcceleratorId},
|
||||
event_loop::EventLoopWindowTarget,
|
||||
keyboard::{KeyCode, ModifiersState},
|
||||
};
|
||||
use wry::application::keyboard::ModifiersState;
|
||||
|
||||
use crate::{desktop_context::DesktopContext, use_window};
|
||||
use crate::{desktop_context::DesktopContext, window};
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
|
@ -20,22 +16,25 @@ use crate::{desktop_context::DesktopContext, use_window};
|
|||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
use wry::application::global_shortcut::{GlobalShortcut, ShortcutManager, ShortcutManagerError};
|
||||
pub use global_hotkey::{
|
||||
hotkey::{Code, HotKey},
|
||||
Error as HotkeyError, GlobalHotKeyEvent, GlobalHotKeyManager,
|
||||
};
|
||||
|
||||
#[cfg(any(target_os = "ios", target_os = "android"))]
|
||||
pub use crate::mobile_shortcut::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ShortcutRegistry {
|
||||
manager: Rc<RefCell<ShortcutManager>>,
|
||||
manager: Rc<RefCell<GlobalHotKeyManager>>,
|
||||
shortcuts: ShortcutMap,
|
||||
}
|
||||
|
||||
type ShortcutMap = Rc<RefCell<HashMap<AcceleratorId, Shortcut>>>;
|
||||
type ShortcutMap = Rc<RefCell<HashMap<u32, Shortcut>>>;
|
||||
|
||||
struct Shortcut {
|
||||
#[allow(unused)]
|
||||
shortcut: GlobalShortcut,
|
||||
shortcut: HotKey,
|
||||
callbacks: Slab<Box<dyn FnMut()>>,
|
||||
}
|
||||
|
||||
|
@ -54,15 +53,15 @@ impl Shortcut {
|
|||
}
|
||||
|
||||
impl ShortcutRegistry {
|
||||
pub fn new<T>(target: &EventLoopWindowTarget<T>) -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
manager: Rc::new(RefCell::new(ShortcutManager::new(target))),
|
||||
manager: Rc::new(RefCell::new(GlobalHotKeyManager::new().unwrap())),
|
||||
shortcuts: Rc::new(RefCell::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn call_handlers(&self, id: AcceleratorId) {
|
||||
if let Some(Shortcut { callbacks, .. }) = self.shortcuts.borrow_mut().get_mut(&id) {
|
||||
pub(crate) fn call_handlers(&self, id: GlobalHotKeyEvent) {
|
||||
if let Some(Shortcut { callbacks, .. }) = self.shortcuts.borrow_mut().get_mut(&id.id) {
|
||||
for (_, callback) in callbacks.iter_mut() {
|
||||
(callback)();
|
||||
}
|
||||
|
@ -71,10 +70,10 @@ impl ShortcutRegistry {
|
|||
|
||||
pub(crate) fn add_shortcut(
|
||||
&self,
|
||||
accelerator: Accelerator,
|
||||
hotkey: HotKey,
|
||||
callback: Box<dyn FnMut()>,
|
||||
) -> Result<ShortcutId, ShortcutRegistryError> {
|
||||
let accelerator_id = accelerator.clone().id();
|
||||
let accelerator_id = hotkey.clone().id();
|
||||
let mut shortcuts = self.shortcuts.borrow_mut();
|
||||
Ok(
|
||||
if let Some(callbacks) = shortcuts.get_mut(&accelerator_id) {
|
||||
|
@ -84,12 +83,12 @@ impl ShortcutRegistry {
|
|||
number: id,
|
||||
}
|
||||
} else {
|
||||
match self.manager.borrow_mut().register(accelerator) {
|
||||
Ok(global_shortcut) => {
|
||||
match self.manager.borrow_mut().register(hotkey) {
|
||||
Ok(_) => {
|
||||
let mut slab = Slab::new();
|
||||
let id = slab.insert(callback);
|
||||
let shortcut = Shortcut {
|
||||
shortcut: global_shortcut,
|
||||
shortcut: hotkey,
|
||||
callbacks: slab,
|
||||
};
|
||||
shortcuts.insert(accelerator_id, shortcut);
|
||||
|
@ -98,7 +97,7 @@ impl ShortcutRegistry {
|
|||
number: id,
|
||||
}
|
||||
}
|
||||
Err(ShortcutManagerError::InvalidAccelerator(shortcut)) => {
|
||||
Err(HotkeyError::HotKeyParseError(shortcut)) => {
|
||||
return Err(ShortcutRegistryError::InvalidShortcut(shortcut))
|
||||
}
|
||||
Err(err) => return Err(ShortcutRegistryError::Other(Box::new(err))),
|
||||
|
@ -113,15 +112,6 @@ impl ShortcutRegistry {
|
|||
callbacks.remove(id.number);
|
||||
if callbacks.is_empty() {
|
||||
if let Some(_shortcut) = shortcuts.remove(&id.id) {
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
target_os = "macos",
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
let _ = self.manager.borrow_mut().unregister(_shortcut.shortcut);
|
||||
}
|
||||
}
|
||||
|
@ -130,8 +120,8 @@ impl ShortcutRegistry {
|
|||
|
||||
pub(crate) fn remove_all(&self) {
|
||||
let mut shortcuts = self.shortcuts.borrow_mut();
|
||||
shortcuts.clear();
|
||||
let _ = self.manager.borrow_mut().unregister_all();
|
||||
let hotkeys: Vec<_> = shortcuts.drain().map(|(_, v)| v.shortcut).collect();
|
||||
let _ = self.manager.borrow_mut().unregister_all(&hotkeys);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -148,7 +138,7 @@ pub enum ShortcutRegistryError {
|
|||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
/// An global id for a shortcut.
|
||||
pub struct ShortcutId {
|
||||
id: AcceleratorId,
|
||||
id: u32,
|
||||
number: usize,
|
||||
}
|
||||
|
||||
|
@ -160,30 +150,30 @@ pub struct ShortcutHandle {
|
|||
}
|
||||
|
||||
pub trait IntoAccelerator {
|
||||
fn accelerator(&self) -> Accelerator;
|
||||
fn accelerator(&self) -> HotKey;
|
||||
}
|
||||
|
||||
impl IntoAccelerator for (dioxus_html::KeyCode, ModifiersState) {
|
||||
fn accelerator(&self) -> Accelerator {
|
||||
Accelerator::new(Some(self.1), self.0.into_key_code())
|
||||
fn accelerator(&self) -> HotKey {
|
||||
HotKey::new(Some(self.1.into_modifiers_state()), self.0.into_key_code())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAccelerator for (ModifiersState, dioxus_html::KeyCode) {
|
||||
fn accelerator(&self) -> Accelerator {
|
||||
Accelerator::new(Some(self.0), self.1.into_key_code())
|
||||
fn accelerator(&self) -> HotKey {
|
||||
HotKey::new(Some(self.0.into_modifiers_state()), self.1.into_key_code())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAccelerator for dioxus_html::KeyCode {
|
||||
fn accelerator(&self) -> Accelerator {
|
||||
Accelerator::new(None, self.into_key_code())
|
||||
fn accelerator(&self) -> HotKey {
|
||||
HotKey::new(None, self.into_key_code())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAccelerator for &str {
|
||||
fn accelerator(&self) -> Accelerator {
|
||||
Accelerator::from_str(self).unwrap()
|
||||
fn accelerator(&self) -> HotKey {
|
||||
HotKey::from_str(self).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -193,9 +183,8 @@ pub fn use_global_shortcut(
|
|||
accelerator: impl IntoAccelerator,
|
||||
handler: impl FnMut() + 'static,
|
||||
) -> &Result<ShortcutHandle, ShortcutRegistryError> {
|
||||
let desktop = use_window(cx);
|
||||
cx.use_hook(move || {
|
||||
let desktop = desktop.clone();
|
||||
let desktop = window();
|
||||
|
||||
let id = desktop.create_shortcut(accelerator.accelerator(), handler);
|
||||
|
||||
|
@ -220,143 +209,144 @@ impl Drop for ShortcutHandle {
|
|||
}
|
||||
|
||||
pub trait IntoModifersState {
|
||||
fn into_modifiers_state(self) -> ModifiersState;
|
||||
fn into_modifiers_state(self) -> Modifiers;
|
||||
}
|
||||
|
||||
impl IntoModifersState for ModifiersState {
|
||||
fn into_modifiers_state(self) -> ModifiersState {
|
||||
self
|
||||
fn into_modifiers_state(self) -> Modifiers {
|
||||
let mut modifiers = Modifiers::default();
|
||||
if self.shift_key() {
|
||||
modifiers |= Modifiers::SHIFT;
|
||||
}
|
||||
if self.control_key() {
|
||||
modifiers |= Modifiers::CONTROL;
|
||||
}
|
||||
if self.alt_key() {
|
||||
modifiers |= Modifiers::ALT;
|
||||
}
|
||||
if self.super_key() {
|
||||
modifiers |= Modifiers::META;
|
||||
}
|
||||
|
||||
modifiers
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoModifersState for Modifiers {
|
||||
fn into_modifiers_state(self) -> ModifiersState {
|
||||
let mut state = ModifiersState::empty();
|
||||
if self.contains(Modifiers::SHIFT) {
|
||||
state |= ModifiersState::SHIFT
|
||||
}
|
||||
if self.contains(Modifiers::CONTROL) {
|
||||
state |= ModifiersState::CONTROL
|
||||
}
|
||||
if self.contains(Modifiers::ALT) {
|
||||
state |= ModifiersState::ALT
|
||||
}
|
||||
if self.contains(Modifiers::META) || self.contains(Modifiers::SUPER) {
|
||||
state |= ModifiersState::SUPER
|
||||
}
|
||||
state
|
||||
fn into_modifiers_state(self) -> Modifiers {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoKeyCode {
|
||||
fn into_key_code(self) -> KeyCode;
|
||||
fn into_key_code(self) -> Code;
|
||||
}
|
||||
|
||||
impl IntoKeyCode for KeyCode {
|
||||
fn into_key_code(self) -> KeyCode {
|
||||
impl IntoKeyCode for Code {
|
||||
fn into_key_code(self) -> Code {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoKeyCode for dioxus_html::KeyCode {
|
||||
fn into_key_code(self) -> KeyCode {
|
||||
fn into_key_code(self) -> Code {
|
||||
match self {
|
||||
dioxus_html::KeyCode::Backspace => KeyCode::Backspace,
|
||||
dioxus_html::KeyCode::Tab => KeyCode::Tab,
|
||||
dioxus_html::KeyCode::Clear => KeyCode::NumpadClear,
|
||||
dioxus_html::KeyCode::Enter => KeyCode::Enter,
|
||||
dioxus_html::KeyCode::Shift => KeyCode::ShiftLeft,
|
||||
dioxus_html::KeyCode::Ctrl => KeyCode::ControlLeft,
|
||||
dioxus_html::KeyCode::Alt => KeyCode::AltLeft,
|
||||
dioxus_html::KeyCode::Pause => KeyCode::Pause,
|
||||
dioxus_html::KeyCode::CapsLock => KeyCode::CapsLock,
|
||||
dioxus_html::KeyCode::Escape => KeyCode::Escape,
|
||||
dioxus_html::KeyCode::Space => KeyCode::Space,
|
||||
dioxus_html::KeyCode::PageUp => KeyCode::PageUp,
|
||||
dioxus_html::KeyCode::PageDown => KeyCode::PageDown,
|
||||
dioxus_html::KeyCode::End => KeyCode::End,
|
||||
dioxus_html::KeyCode::Home => KeyCode::Home,
|
||||
dioxus_html::KeyCode::LeftArrow => KeyCode::ArrowLeft,
|
||||
dioxus_html::KeyCode::UpArrow => KeyCode::ArrowUp,
|
||||
dioxus_html::KeyCode::RightArrow => KeyCode::ArrowRight,
|
||||
dioxus_html::KeyCode::DownArrow => KeyCode::ArrowDown,
|
||||
dioxus_html::KeyCode::Insert => KeyCode::Insert,
|
||||
dioxus_html::KeyCode::Delete => KeyCode::Delete,
|
||||
dioxus_html::KeyCode::Num0 => KeyCode::Numpad0,
|
||||
dioxus_html::KeyCode::Num1 => KeyCode::Numpad1,
|
||||
dioxus_html::KeyCode::Num2 => KeyCode::Numpad2,
|
||||
dioxus_html::KeyCode::Num3 => KeyCode::Numpad3,
|
||||
dioxus_html::KeyCode::Num4 => KeyCode::Numpad4,
|
||||
dioxus_html::KeyCode::Num5 => KeyCode::Numpad5,
|
||||
dioxus_html::KeyCode::Num6 => KeyCode::Numpad6,
|
||||
dioxus_html::KeyCode::Num7 => KeyCode::Numpad7,
|
||||
dioxus_html::KeyCode::Num8 => KeyCode::Numpad8,
|
||||
dioxus_html::KeyCode::Num9 => KeyCode::Numpad9,
|
||||
dioxus_html::KeyCode::A => KeyCode::KeyA,
|
||||
dioxus_html::KeyCode::B => KeyCode::KeyB,
|
||||
dioxus_html::KeyCode::C => KeyCode::KeyC,
|
||||
dioxus_html::KeyCode::D => KeyCode::KeyD,
|
||||
dioxus_html::KeyCode::E => KeyCode::KeyE,
|
||||
dioxus_html::KeyCode::F => KeyCode::KeyF,
|
||||
dioxus_html::KeyCode::G => KeyCode::KeyG,
|
||||
dioxus_html::KeyCode::H => KeyCode::KeyH,
|
||||
dioxus_html::KeyCode::I => KeyCode::KeyI,
|
||||
dioxus_html::KeyCode::J => KeyCode::KeyJ,
|
||||
dioxus_html::KeyCode::K => KeyCode::KeyK,
|
||||
dioxus_html::KeyCode::L => KeyCode::KeyL,
|
||||
dioxus_html::KeyCode::M => KeyCode::KeyM,
|
||||
dioxus_html::KeyCode::N => KeyCode::KeyN,
|
||||
dioxus_html::KeyCode::O => KeyCode::KeyO,
|
||||
dioxus_html::KeyCode::P => KeyCode::KeyP,
|
||||
dioxus_html::KeyCode::Q => KeyCode::KeyQ,
|
||||
dioxus_html::KeyCode::R => KeyCode::KeyR,
|
||||
dioxus_html::KeyCode::S => KeyCode::KeyS,
|
||||
dioxus_html::KeyCode::T => KeyCode::KeyT,
|
||||
dioxus_html::KeyCode::U => KeyCode::KeyU,
|
||||
dioxus_html::KeyCode::V => KeyCode::KeyV,
|
||||
dioxus_html::KeyCode::W => KeyCode::KeyW,
|
||||
dioxus_html::KeyCode::X => KeyCode::KeyX,
|
||||
dioxus_html::KeyCode::Y => KeyCode::KeyY,
|
||||
dioxus_html::KeyCode::Z => KeyCode::KeyZ,
|
||||
dioxus_html::KeyCode::Numpad0 => KeyCode::Numpad0,
|
||||
dioxus_html::KeyCode::Numpad1 => KeyCode::Numpad1,
|
||||
dioxus_html::KeyCode::Numpad2 => KeyCode::Numpad2,
|
||||
dioxus_html::KeyCode::Numpad3 => KeyCode::Numpad3,
|
||||
dioxus_html::KeyCode::Numpad4 => KeyCode::Numpad4,
|
||||
dioxus_html::KeyCode::Numpad5 => KeyCode::Numpad5,
|
||||
dioxus_html::KeyCode::Numpad6 => KeyCode::Numpad6,
|
||||
dioxus_html::KeyCode::Numpad7 => KeyCode::Numpad7,
|
||||
dioxus_html::KeyCode::Numpad8 => KeyCode::Numpad8,
|
||||
dioxus_html::KeyCode::Numpad9 => KeyCode::Numpad9,
|
||||
dioxus_html::KeyCode::Multiply => KeyCode::NumpadMultiply,
|
||||
dioxus_html::KeyCode::Add => KeyCode::NumpadAdd,
|
||||
dioxus_html::KeyCode::Subtract => KeyCode::NumpadSubtract,
|
||||
dioxus_html::KeyCode::DecimalPoint => KeyCode::NumpadDecimal,
|
||||
dioxus_html::KeyCode::Divide => KeyCode::NumpadDivide,
|
||||
dioxus_html::KeyCode::F1 => KeyCode::F1,
|
||||
dioxus_html::KeyCode::F2 => KeyCode::F2,
|
||||
dioxus_html::KeyCode::F3 => KeyCode::F3,
|
||||
dioxus_html::KeyCode::F4 => KeyCode::F4,
|
||||
dioxus_html::KeyCode::F5 => KeyCode::F5,
|
||||
dioxus_html::KeyCode::F6 => KeyCode::F6,
|
||||
dioxus_html::KeyCode::F7 => KeyCode::F7,
|
||||
dioxus_html::KeyCode::F8 => KeyCode::F8,
|
||||
dioxus_html::KeyCode::F9 => KeyCode::F9,
|
||||
dioxus_html::KeyCode::F10 => KeyCode::F10,
|
||||
dioxus_html::KeyCode::F11 => KeyCode::F11,
|
||||
dioxus_html::KeyCode::F12 => KeyCode::F12,
|
||||
dioxus_html::KeyCode::NumLock => KeyCode::NumLock,
|
||||
dioxus_html::KeyCode::ScrollLock => KeyCode::ScrollLock,
|
||||
dioxus_html::KeyCode::Semicolon => KeyCode::Semicolon,
|
||||
dioxus_html::KeyCode::EqualSign => KeyCode::Equal,
|
||||
dioxus_html::KeyCode::Comma => KeyCode::Comma,
|
||||
dioxus_html::KeyCode::Period => KeyCode::Period,
|
||||
dioxus_html::KeyCode::ForwardSlash => KeyCode::Slash,
|
||||
dioxus_html::KeyCode::GraveAccent => KeyCode::Backquote,
|
||||
dioxus_html::KeyCode::OpenBracket => KeyCode::BracketLeft,
|
||||
dioxus_html::KeyCode::BackSlash => KeyCode::Backslash,
|
||||
dioxus_html::KeyCode::CloseBraket => KeyCode::BracketRight,
|
||||
dioxus_html::KeyCode::SingleQuote => KeyCode::Quote,
|
||||
dioxus_html::KeyCode::Backspace => Code::Backspace,
|
||||
dioxus_html::KeyCode::Tab => Code::Tab,
|
||||
dioxus_html::KeyCode::Clear => Code::NumpadClear,
|
||||
dioxus_html::KeyCode::Enter => Code::Enter,
|
||||
dioxus_html::KeyCode::Shift => Code::ShiftLeft,
|
||||
dioxus_html::KeyCode::Ctrl => Code::ControlLeft,
|
||||
dioxus_html::KeyCode::Alt => Code::AltLeft,
|
||||
dioxus_html::KeyCode::Pause => Code::Pause,
|
||||
dioxus_html::KeyCode::CapsLock => Code::CapsLock,
|
||||
dioxus_html::KeyCode::Escape => Code::Escape,
|
||||
dioxus_html::KeyCode::Space => Code::Space,
|
||||
dioxus_html::KeyCode::PageUp => Code::PageUp,
|
||||
dioxus_html::KeyCode::PageDown => Code::PageDown,
|
||||
dioxus_html::KeyCode::End => Code::End,
|
||||
dioxus_html::KeyCode::Home => Code::Home,
|
||||
dioxus_html::KeyCode::LeftArrow => Code::ArrowLeft,
|
||||
dioxus_html::KeyCode::UpArrow => Code::ArrowUp,
|
||||
dioxus_html::KeyCode::RightArrow => Code::ArrowRight,
|
||||
dioxus_html::KeyCode::DownArrow => Code::ArrowDown,
|
||||
dioxus_html::KeyCode::Insert => Code::Insert,
|
||||
dioxus_html::KeyCode::Delete => Code::Delete,
|
||||
dioxus_html::KeyCode::Num0 => Code::Numpad0,
|
||||
dioxus_html::KeyCode::Num1 => Code::Numpad1,
|
||||
dioxus_html::KeyCode::Num2 => Code::Numpad2,
|
||||
dioxus_html::KeyCode::Num3 => Code::Numpad3,
|
||||
dioxus_html::KeyCode::Num4 => Code::Numpad4,
|
||||
dioxus_html::KeyCode::Num5 => Code::Numpad5,
|
||||
dioxus_html::KeyCode::Num6 => Code::Numpad6,
|
||||
dioxus_html::KeyCode::Num7 => Code::Numpad7,
|
||||
dioxus_html::KeyCode::Num8 => Code::Numpad8,
|
||||
dioxus_html::KeyCode::Num9 => Code::Numpad9,
|
||||
dioxus_html::KeyCode::A => Code::KeyA,
|
||||
dioxus_html::KeyCode::B => Code::KeyB,
|
||||
dioxus_html::KeyCode::C => Code::KeyC,
|
||||
dioxus_html::KeyCode::D => Code::KeyD,
|
||||
dioxus_html::KeyCode::E => Code::KeyE,
|
||||
dioxus_html::KeyCode::F => Code::KeyF,
|
||||
dioxus_html::KeyCode::G => Code::KeyG,
|
||||
dioxus_html::KeyCode::H => Code::KeyH,
|
||||
dioxus_html::KeyCode::I => Code::KeyI,
|
||||
dioxus_html::KeyCode::J => Code::KeyJ,
|
||||
dioxus_html::KeyCode::K => Code::KeyK,
|
||||
dioxus_html::KeyCode::L => Code::KeyL,
|
||||
dioxus_html::KeyCode::M => Code::KeyM,
|
||||
dioxus_html::KeyCode::N => Code::KeyN,
|
||||
dioxus_html::KeyCode::O => Code::KeyO,
|
||||
dioxus_html::KeyCode::P => Code::KeyP,
|
||||
dioxus_html::KeyCode::Q => Code::KeyQ,
|
||||
dioxus_html::KeyCode::R => Code::KeyR,
|
||||
dioxus_html::KeyCode::S => Code::KeyS,
|
||||
dioxus_html::KeyCode::T => Code::KeyT,
|
||||
dioxus_html::KeyCode::U => Code::KeyU,
|
||||
dioxus_html::KeyCode::V => Code::KeyV,
|
||||
dioxus_html::KeyCode::W => Code::KeyW,
|
||||
dioxus_html::KeyCode::X => Code::KeyX,
|
||||
dioxus_html::KeyCode::Y => Code::KeyY,
|
||||
dioxus_html::KeyCode::Z => Code::KeyZ,
|
||||
dioxus_html::KeyCode::Numpad0 => Code::Numpad0,
|
||||
dioxus_html::KeyCode::Numpad1 => Code::Numpad1,
|
||||
dioxus_html::KeyCode::Numpad2 => Code::Numpad2,
|
||||
dioxus_html::KeyCode::Numpad3 => Code::Numpad3,
|
||||
dioxus_html::KeyCode::Numpad4 => Code::Numpad4,
|
||||
dioxus_html::KeyCode::Numpad5 => Code::Numpad5,
|
||||
dioxus_html::KeyCode::Numpad6 => Code::Numpad6,
|
||||
dioxus_html::KeyCode::Numpad7 => Code::Numpad7,
|
||||
dioxus_html::KeyCode::Numpad8 => Code::Numpad8,
|
||||
dioxus_html::KeyCode::Numpad9 => Code::Numpad9,
|
||||
dioxus_html::KeyCode::Multiply => Code::NumpadMultiply,
|
||||
dioxus_html::KeyCode::Add => Code::NumpadAdd,
|
||||
dioxus_html::KeyCode::Subtract => Code::NumpadSubtract,
|
||||
dioxus_html::KeyCode::DecimalPoint => Code::NumpadDecimal,
|
||||
dioxus_html::KeyCode::Divide => Code::NumpadDivide,
|
||||
dioxus_html::KeyCode::F1 => Code::F1,
|
||||
dioxus_html::KeyCode::F2 => Code::F2,
|
||||
dioxus_html::KeyCode::F3 => Code::F3,
|
||||
dioxus_html::KeyCode::F4 => Code::F4,
|
||||
dioxus_html::KeyCode::F5 => Code::F5,
|
||||
dioxus_html::KeyCode::F6 => Code::F6,
|
||||
dioxus_html::KeyCode::F7 => Code::F7,
|
||||
dioxus_html::KeyCode::F8 => Code::F8,
|
||||
dioxus_html::KeyCode::F9 => Code::F9,
|
||||
dioxus_html::KeyCode::F10 => Code::F10,
|
||||
dioxus_html::KeyCode::F11 => Code::F11,
|
||||
dioxus_html::KeyCode::F12 => Code::F12,
|
||||
dioxus_html::KeyCode::NumLock => Code::NumLock,
|
||||
dioxus_html::KeyCode::ScrollLock => Code::ScrollLock,
|
||||
dioxus_html::KeyCode::Semicolon => Code::Semicolon,
|
||||
dioxus_html::KeyCode::EqualSign => Code::Equal,
|
||||
dioxus_html::KeyCode::Comma => Code::Comma,
|
||||
dioxus_html::KeyCode::Period => Code::Period,
|
||||
dioxus_html::KeyCode::ForwardSlash => Code::Slash,
|
||||
dioxus_html::KeyCode::GraveAccent => Code::Backquote,
|
||||
dioxus_html::KeyCode::OpenBracket => Code::BracketLeft,
|
||||
dioxus_html::KeyCode::BackSlash => Code::Backslash,
|
||||
dioxus_html::KeyCode::CloseBraket => Code::BracketRight,
|
||||
dioxus_html::KeyCode::SingleQuote => Code::Quote,
|
||||
key => panic!("Failed to convert {:?} to tao::keyboard::KeyCode, try using tao::keyboard::KeyCode directly", key),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
use crate::desktop_context::EventData;
|
||||
use crate::protocol;
|
||||
use crate::desktop_context::{EditQueue, EventData};
|
||||
use crate::protocol::{self, AssetHandlerRegistry};
|
||||
use crate::{desktop_context::UserWindowEvent, Config};
|
||||
use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
|
||||
pub use wry;
|
||||
pub use wry::application as tao;
|
||||
use wry::application::window::Window;
|
||||
use wry::http::Response;
|
||||
use wry::webview::{WebContext, WebView, WebViewBuilder};
|
||||
|
||||
pub fn build(
|
||||
pub(crate) fn build(
|
||||
cfg: &mut Config,
|
||||
event_loop: &EventLoopWindowTarget<UserWindowEvent>,
|
||||
proxy: EventLoopProxy<UserWindowEvent>,
|
||||
) -> (WebView, WebContext) {
|
||||
) -> (WebView, WebContext, AssetHandlerRegistry, EditQueue) {
|
||||
let builder = cfg.window.clone();
|
||||
let window = builder.with_visible(false).build(event_loop).unwrap();
|
||||
let file_handler = cfg.file_drop_handler.take();
|
||||
|
@ -19,6 +20,11 @@ pub fn build(
|
|||
let index_file = cfg.custom_index.clone();
|
||||
let root_name = cfg.root_name.clone();
|
||||
|
||||
// TODO: restore the menu bar with muda: https://github.com/tauri-apps/muda/blob/dev/examples/wry.rs
|
||||
// if cfg.enable_default_menu_bar {
|
||||
// builder = builder.with_menu(build_default_menu_bar());
|
||||
// }
|
||||
|
||||
// We assume that if the icon is None in cfg, then the user just didnt set it
|
||||
if cfg.window.window.window_icon.is_none() {
|
||||
window.set_window_icon(Some(
|
||||
|
@ -32,6 +38,10 @@ pub fn build(
|
|||
}
|
||||
|
||||
let mut web_context = WebContext::new(cfg.data_dir.clone());
|
||||
let edit_queue = EditQueue::default();
|
||||
let headless = !cfg.window.window.visible;
|
||||
let asset_handlers = AssetHandlerRegistry::new();
|
||||
let asset_handlers_ref = asset_handlers.clone();
|
||||
|
||||
let mut webview = WebViewBuilder::new(window)
|
||||
.unwrap()
|
||||
|
@ -44,8 +54,28 @@ pub fn build(
|
|||
_ = proxy.send_event(UserWindowEvent(EventData::Ipc(message), window.id()));
|
||||
}
|
||||
})
|
||||
.with_custom_protocol(String::from("dioxus"), move |r| {
|
||||
protocol::desktop_handler(r, custom_head.clone(), index_file.clone(), &root_name)
|
||||
.with_asynchronous_custom_protocol(String::from("dioxus"), {
|
||||
let edit_queue = edit_queue.clone();
|
||||
move |request, responder| {
|
||||
let custom_head = custom_head.clone();
|
||||
let index_file = index_file.clone();
|
||||
let root_name = root_name.clone();
|
||||
let asset_handlers_ref = asset_handlers_ref.clone();
|
||||
let edit_queue = edit_queue.clone();
|
||||
tokio::spawn(async move {
|
||||
protocol::desktop_handler(
|
||||
request,
|
||||
custom_head.clone(),
|
||||
index_file.clone(),
|
||||
&root_name,
|
||||
&asset_handlers_ref,
|
||||
&edit_queue,
|
||||
headless,
|
||||
responder,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
})
|
||||
.with_file_drop_handler(move |window, evet| {
|
||||
file_handler
|
||||
|
@ -71,7 +101,7 @@ pub fn build(
|
|||
// .with_web_context(&mut web_context);
|
||||
|
||||
for (name, handler) in cfg.protocols.drain(..) {
|
||||
webview = webview.with_custom_protocol(name, handler)
|
||||
webview = webview.with_custom_protocol(name, move |r| handler(r))
|
||||
}
|
||||
|
||||
if cfg.disable_context_menu {
|
||||
|
@ -94,5 +124,68 @@ pub fn build(
|
|||
webview = webview.with_devtools(true);
|
||||
}
|
||||
|
||||
(webview.build().unwrap(), web_context)
|
||||
(
|
||||
webview.build().unwrap(),
|
||||
web_context,
|
||||
asset_handlers,
|
||||
edit_queue,
|
||||
)
|
||||
}
|
||||
|
||||
// /// Builds a standard menu bar depending on the users platform. It may be used as a starting point
|
||||
// /// to further customize the menu bar and pass it to a [`WindowBuilder`](tao::window::WindowBuilder).
|
||||
// /// > Note: The default menu bar enables macOS shortcuts like cut/copy/paste.
|
||||
// /// > The menu bar differs per platform because of constraints introduced
|
||||
// /// > by [`MenuItem`](tao::menu::MenuItem).
|
||||
// pub fn build_default_menu_bar() -> MenuBar {
|
||||
// let mut menu_bar = MenuBar::new();
|
||||
|
||||
// // since it is uncommon on windows to have an "application menu"
|
||||
// // we add a "window" menu to be more consistent across platforms with the standard menu
|
||||
// let mut window_menu = MenuBar::new();
|
||||
// #[cfg(target_os = "macos")]
|
||||
// {
|
||||
// window_menu.add_native_item(MenuItem::EnterFullScreen);
|
||||
// window_menu.add_native_item(MenuItem::Zoom);
|
||||
// window_menu.add_native_item(MenuItem::Separator);
|
||||
// }
|
||||
|
||||
// window_menu.add_native_item(MenuItem::Hide);
|
||||
|
||||
// #[cfg(target_os = "macos")]
|
||||
// {
|
||||
// window_menu.add_native_item(MenuItem::HideOthers);
|
||||
// window_menu.add_native_item(MenuItem::ShowAll);
|
||||
// }
|
||||
|
||||
// window_menu.add_native_item(MenuItem::Minimize);
|
||||
// window_menu.add_native_item(MenuItem::CloseWindow);
|
||||
// window_menu.add_native_item(MenuItem::Separator);
|
||||
// window_menu.add_native_item(MenuItem::Quit);
|
||||
// menu_bar.add_submenu("Window", true, window_menu);
|
||||
|
||||
// // since tao supports none of the below items on linux we should only add them on macos/windows
|
||||
// #[cfg(not(target_os = "linux"))]
|
||||
// {
|
||||
// let mut edit_menu = MenuBar::new();
|
||||
// #[cfg(target_os = "macos")]
|
||||
// {
|
||||
// edit_menu.add_native_item(MenuItem::Undo);
|
||||
// edit_menu.add_native_item(MenuItem::Redo);
|
||||
// edit_menu.add_native_item(MenuItem::Separator);
|
||||
// }
|
||||
|
||||
// edit_menu.add_native_item(MenuItem::Cut);
|
||||
// edit_menu.add_native_item(MenuItem::Copy);
|
||||
// edit_menu.add_native_item(MenuItem::Paste);
|
||||
|
||||
// #[cfg(target_os = "macos")]
|
||||
// {
|
||||
// edit_menu.add_native_item(MenuItem::Separator);
|
||||
// edit_menu.add_native_item(MenuItem::SelectAll);
|
||||
// }
|
||||
// menu_bar.add_submenu("Edit", true, edit_menu);
|
||||
// }
|
||||
|
||||
// menu_bar
|
||||
// }
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue