Merge branch 'master' into jk/faster-cache-ci

This commit is contained in:
Jonathan Kelley authored and GitHub committed 2024-01-08 11:22:42 -08:00
commit 5a3d4f9bff
347 files changed
+15908 -5231

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
# All of these variables are used in the `openid_connect_demo` example, they are set here for the CI to work, they are set here because as stated here for now: `https://doc.rust-lang.org/cargo/reference/config.html` the .cargo/config.toml of the inner workspaces are not read when being invoked from the root workspace.
[env]
DIOXUS_FRONT_ISSUER_URL = ""
DIOXUS_FRONT_CLIENT_ID = ""
DIOXUS_FRONT_URL = ""
+2
View File
@@ -36,6 +36,8 @@ jobs:
toolchain: ${{ matrix.platform.toolchain }}
targets: ${{ matrix.platform.target }}
- uses: ilammy/setup-nasm@v1
# Setup the Github Actions Cache for the CLI package
- name: Setup cache
uses: Swatinem/rust-cache@v2
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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.
+11 -2
View File
@@ -44,6 +44,7 @@ jobs:
steps:
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.3
- uses: ilammy/setup-nasm@v1
- run: sudo apt-get update
- run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev
- uses: actions/checkout@v4
@@ -61,8 +62,9 @@ jobs:
steps:
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.3
- uses: ilammy/setup-nasm@v1
- run: sudo apt-get update
- run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev
- run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev
- uses: davidB/rust-cargo-make@v1
- uses: browser-actions/setup-firefox@latest
- uses: jetli/wasm-pack-action@v0.4.0
@@ -81,6 +83,7 @@ jobs:
steps:
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.3
- uses: ilammy/setup-nasm@v1
- run: rustup component add rustfmt
- uses: actions/checkout@v4
- run: cargo fmt --all -- --check
@@ -97,6 +100,7 @@ jobs:
steps:
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.3
- uses: ilammy/setup-nasm@v1
- run: sudo apt-get update
- run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev
- run: rustup component add clippy
@@ -149,7 +153,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: ilammy/setup-nasm@v1
- name: install stable
uses: dtolnay/rust-toolchain@master
with:
@@ -165,6 +169,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 }}
+4 -4
View File
@@ -26,8 +26,8 @@ env:
RUST_BACKTRACE: 1
# Change to specific Rust release to pin
rust_stable: stable
rust_nightly: nightly-2022-11-03
rust_clippy: 1.65.0
rust_nightly: nightly-2023-11-16
rust_clippy: 1.70.0
# When updating this, also update:
# - README.md
# - tokio/README.md
@@ -70,6 +70,7 @@ jobs:
run: echo "MIRIFLAGS=-Zmiri-tag-gc=1" >> $GITHUB_ENV
- uses: actions/checkout@v4
- uses: ilammy/setup-nasm@v1
- name: Install Rust ${{ env.rust_nightly }}
uses: dtolnay/rust-toolchain@master
with:
@@ -86,8 +87,7 @@ jobs:
# working-directory: tokio
env:
# todo: disable memory leaks ignore
MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance -Zmiri-retag-fields -Zmiri-ignore-leaks
MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance -Zmiri-retag-fields
PROPTEST_CASES: 10
# Cache the global cargo directory, but NOT the local `target` directory which
+3 -2
View File
@@ -20,7 +20,8 @@ jobs:
steps:
# Do our best to cache the toolchain and node install steps
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
- uses: ilammy/setup-nasm@v1
- uses: actions/setup-node@v4
with:
node-version: 16
- name: Install Rust
@@ -43,7 +44,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
View File
@@ -4,6 +4,7 @@
/dist
Cargo.lock
.DS_Store
/examples/assets/test_video.mp4
.vscode/*
!.vscode/settings.json
+13 -5
View File
@@ -9,6 +9,7 @@ members = [
"packages/extension",
"packages/router",
"packages/html",
"packages/html-internal-macro",
"packages/hooks",
"packages/web",
"packages/ssr",
@@ -41,6 +42,7 @@ members = [
"examples/tailwind",
"examples/PWA-example",
"examples/query_segments_demo",
"examples/openid_connect_demo",
# Playwright tests
"playwright-tests/liveview",
"playwright-tests/web",
@@ -49,7 +51,7 @@ members = [
exclude = ["examples/mobile_demo"]
[workspace.package]
version = "0.4.2"
version = "0.4.3"
# dependencies that are shared across packages
[workspace.dependencies]
@@ -58,7 +60,8 @@ dioxus-core = { path = "packages/core", version = "0.4.2" }
dioxus-core-macro = { path = "packages/core-macro", version = "0.4.0" }
dioxus-router = { path = "packages/router", version = "0.4.1" }
dioxus-router-macro = { path = "packages/router-macro", version = "0.4.1" }
dioxus-html = { path = "packages/html", version = "0.4.0" }
dioxus-html = { path = "packages/html", default-features = false, version = "0.4.0" }
dioxus-html-internal-macro = { path = "packages/html-internal-macro", version = "0.4.0" }
dioxus-hooks = { path = "packages/hooks", version = "0.4.0" }
dioxus-web = { path = "packages/web", version = "0.4.0" }
dioxus-ssr = { path = "packages/ssr", version = "0.4.0" }
@@ -76,7 +79,7 @@ dioxus-native-core = { path = "packages/native-core", version = "0.4.0" }
dioxus-native-core-macro = { path = "packages/native-core-macro", version = "0.4.0" }
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" }
@@ -87,7 +90,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 = [
@@ -98,7 +101,7 @@ prettyplease = { package = "prettier-please", version = "0.2", features = [
# It is not meant to be published, but is used so "cargo run --example XYZ" works properly
[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"
@@ -132,3 +135,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"
+72 -4
View File
@@ -24,12 +24,64 @@ script = [
]
script_runner = "@duckscript"
[tasks.format]
command = "cargo"
args = ["fmt", "--all"]
[tasks.check]
command = "cargo"
args = ["check", "--workspace", "--examples", "--tests"]
[tasks.clippy]
command = "cargo"
args = [
"clippy",
"--workspace",
"--examples",
"--tests",
"--",
"-D",
"warnings",
]
[tasks.tidy]
category = "Formatting"
dependencies = ["format", "check", "clippy"]
description = "Format and Check workspace"
[tasks.install-miri]
toolchain = "nightly"
install_crate = { rustup_component_name = "miri", binary = "cargo +nightly miri", test_arg = "--help" }
private = true
[tasks.miri-native]
command = "cargo"
toolchain = "nightly"
dependencies = ["install-miri"]
args = [
"miri",
"test",
"--package",
"dioxus-native-core",
"--test",
"miri_native",
]
[tasks.miri-stress]
command = "cargo"
toolchain = "nightly"
dependencies = ["install-miri"]
args = ["miri", "test", "--package", "dioxus-core", "--test", "miri_stress"]
[tasks.miri]
dependencies = ["miri-native", "miri-stress"]
[tasks.tests]
category = "Testing"
dependencies = ["tests-setup"]
description = "Run all tests"
env = {CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = ["**/examples/*"]}
run_task = {name = ["test-flow", "test-with-browser"], fork = true}
env = { CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = ["**/examples/*"] }
run_task = { name = ["test-flow", "test-with-browser"], fork = true }
[tasks.build]
command = "cargo"
@@ -42,10 +94,26 @@ 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",
"--exclude",
"dioxus-mobile",
]
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
+2 -1
View File
@@ -159,8 +159,9 @@ 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">
-1
View File
@@ -139,7 +139,6 @@ Missing Features
Missing examples
- Shared state
- Root-less element groups
- Spread props
- Custom elements
- Component Children: Pass children into child components
- Render To string: Render a mounted virtualdom to a string
+4 -7
View File
@@ -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:?}" } )) }
}
))
}
-18
View File
@@ -1,18 +0,0 @@
use dioxus::prelude::*;
fn main() {
dioxus_desktop::launch(app);
}
fn app(cx: Scope) -> Element {
cx.render(rsx! {
button {
onclick: |_| async move {
println!("hello, desktop!");
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
println!("goodbye, desktop!");
},
"hello, desktop!"
}
})
}
+3 -2
View File
@@ -6,13 +6,13 @@ This calculator version uses React-style state management. All state is held as
use dioxus::events::*;
use dioxus::html::input_data::keyboard_types::Key;
use dioxus::prelude::*;
use dioxus_desktop::{Config, WindowBuilder};
use dioxus_desktop::{Config, LogicalSize, WindowBuilder};
fn main() {
let config = Config::new().with_window(
WindowBuilder::default()
.with_title("Calculator")
.with_inner_size(dioxus_desktop::LogicalSize::new(300.0, 500.0)),
.with_inner_size(LogicalSize::new(300.0, 500.0)),
);
dioxus_desktop::launch_cfg(app, config);
@@ -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 -1
View File
@@ -10,7 +10,7 @@ fn app(cx: Scope) -> Element {
use_future!(cx, || async move {
loop {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
count += 1;
println!("current: {count}");
}
+4 -18
View File
@@ -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,14 +25,8 @@ fn app(cx: Scope) -> Element {
button {
onclick: move |_| {
let dom = VirtualDom::new_with_props(compose, ComposeProps {
app_tx: tx.clone()
});
// this returns a weak reference to the other window
// Be careful not to keep a strong reference to the other window or it will never be dropped
// and the window will never close.
window.new_window(dom, Default::default());
let dom = VirtualDom::new_with_props(compose, ComposeProps { app_tx: tx.clone() });
dioxus_desktop::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.clone());
},
value: "{user_input}"
}
input { oninput: move |e| user_input.set(e.value()), value: "{user_input}" }
}
})
}
+1 -1
View File
@@ -22,7 +22,7 @@ fn app(cx: Scope) -> Element {
input {
value: "{counter}",
oninput: move |e| {
if let Ok(value) = e.value.parse::<usize>() {
if let Ok(value) = e.value().parse::<usize>() {
counters.make_mut()[i] = value;
}
}
+27 -63
View File
@@ -35,14 +35,16 @@ fn App(cx: Scope) -> Element {
rel: "stylesheet",
href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css",
integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5",
crossorigin: "anonymous",
crossorigin: "anonymous"
}
style { "
style {
"
.red {{
background-color: rgb(202, 60, 60) !important;
}}
" }
"
}
h1 { "Dioxus CRM Example" }
@@ -57,16 +59,8 @@ fn ClientList(cx: Scope) -> Element {
cx.render(rsx! {
h2 { "List of Clients" }
Link {
to: Route::ClientAdd {},
class: "pure-button pure-button-primary",
"Add Client"
}
Link {
to: Route::Settings {},
class: "pure-button",
"Settings"
}
Link { to: Route::ClientAdd {}, class: "pure-button pure-button-primary", "Add Client" }
Link { to: Route::Settings {}, class: "pure-button", "Settings" }
clients.read().iter().map(|client| rsx! {
div {
@@ -87,8 +81,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" }
@@ -96,79 +88,55 @@ fn ClientAdd(cx: Scope) -> Element {
class: "pure-form pure-form-aligned",
onsubmit: move |_| {
let mut clients = clients.write();
clients.push(Client {
first_name: first_name.to_string(),
last_name: last_name.to_string(),
description: description.to_string(),
});
navigator.push(Route::ClientList {});
clients
.push(Client {
first_name: first_name.to_string(),
last_name: last_name.to_string(),
description: description.to_string(),
});
dioxus_router::router().push(Route::ClientList {});
},
fieldset {
div {
class: "pure-control-group",
label {
"for": "first_name",
"First Name"
}
div { class: "pure-control-group",
label { "for": "first_name", "First Name" }
input {
id: "first_name",
"type": "text",
placeholder: "First Name…",
required: "",
value: "{first_name}",
oninput: move |e| first_name.set(e.value.clone())
oninput: move |e| first_name.set(e.value())
}
}
div {
class: "pure-control-group",
label {
"for": "last_name",
"Last Name"
}
div { class: "pure-control-group",
label { "for": "last_name", "Last Name" }
input {
id: "last_name",
"type": "text",
placeholder: "Last Name…",
required: "",
value: "{last_name}",
oninput: move |e| last_name.set(e.value.clone())
oninput: move |e| last_name.set(e.value())
}
}
div {
class: "pure-control-group",
label {
"for": "description",
"Description"
}
div { class: "pure-control-group",
label { "for": "description", "Description" }
textarea {
id: "description",
placeholder: "Description…",
value: "{description}",
oninput: move |e| description.set(e.value.clone())
oninput: move |e| description.set(e.value())
}
}
div {
class: "pure-controls",
button {
"type": "submit",
class: "pure-button pure-button-primary",
"Save"
}
Link {
to: Route::ClientList {},
class: "pure-button pure-button-primary red",
"Cancel"
}
div { class: "pure-controls",
button { "type": "submit", class: "pure-button pure-button-primary", "Save" }
Link { to: Route::ClientList {}, class: "pure-button pure-button-primary red", "Cancel" }
}
}
}
})
}
@@ -189,10 +157,6 @@ fn Settings(cx: Scope) -> Element {
"Remove all Clients"
}
Link {
to: Route::ClientList {},
class: "pure-button",
"Go back"
}
Link { to: Route::ClientList {}, class: "pure-button", "Go back" }
})
}
+1 -1
View File
@@ -10,7 +10,7 @@ fn app(cx: Scope) -> Element {
p {
"This should show an image:"
}
img { src: "examples/assets/logo.png" }
img { src: mg!(image("examples/assets/logo.png").format(ImageType::Avif)).to_string() }
}
})
}
+27
View File
@@ -0,0 +1,27 @@
use dioxus::prelude::*;
use dioxus_desktop::{use_asset_handler, wry::http::Response};
fn main() {
dioxus_desktop::launch(app);
}
fn app(cx: Scope) -> Element {
use_asset_handler(cx, "logos", |request, response| {
// Note that the "logos" prefix is stripped from the URI
//
// However, the asset is absolute to its "virtual folder" - meaning it starts with a leading slash
if request.uri().path() != "/logo.png" {
return;
}
response.respond(Response::new(include_bytes!("./assets/logo.png").to_vec()));
});
cx.render(rsx! {
div {
img {
src: "/logos/logo.png"
}
}
})
}
+11 -16
View File
@@ -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}"
}
})
}
+9 -14
View File
@@ -5,26 +5,21 @@ 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(
r#"
let future = use_future(cx, (), |_| async move {
let eval = eval(
r#"
dioxus.send("Hi from JS!");
let msg = await dioxus.recv();
console.log(msg);
return "hello world";
"#,
)
.unwrap();
)
.unwrap();
eval.send("Hi from Rust!".into()).unwrap();
let res = eval.recv().await.unwrap();
println!("{:?}", eval.await);
res
}
eval.send("Hi from Rust!".into()).unwrap();
let res = eval.recv().await.unwrap();
println!("{:?}", eval.await);
res
});
match future.value() {
+2 -1
View File
@@ -18,13 +18,14 @@ fn main() {
);
}
const _STYLE: &str = mg!(file("./examples/assets/fileexplorer.css"));
fn app(cx: Scope) -> Element {
let files = use_ref(cx, Files::new);
cx.render(rsx! {
div {
link { href:"https://fonts.googleapis.com/icon?family=Material+Icons", rel:"stylesheet", }
style { include_str!("./assets/fileexplorer.css") }
header {
i { class: "material-icons icon-menu", "menu" }
h1 { "Files: ", files.read().current() }
+2 -2
View File
@@ -16,7 +16,7 @@ fn App(cx: Scope) -> Element {
r#type: "checkbox",
checked: "{enable_directory_upload}",
oninput: move |evt| {
enable_directory_upload.set(evt.value.parse().unwrap());
enable_directory_upload.set(evt.value().parse().unwrap());
},
},
"Enable directory upload"
@@ -30,7 +30,7 @@ fn App(cx: Scope) -> Element {
onchange: |evt| {
to_owned![files_uploaded];
async move {
if let Some(file_engine) = &evt.files {
if let Some(file_engine) = &evt.files() {
let files = file_engine.files();
for file_name in files {
sleep(std::time::Duration::from_secs(1)).await;
+2 -2
View File
@@ -14,8 +14,8 @@ fn app(cx: Scope) -> Element {
div {
h1 { "Form" }
form {
onsubmit: move |ev| println!("Submitted {:?}", ev.values),
oninput: move |ev| println!("Input {:?}", ev.values),
onsubmit: move |ev| println!("Submitted {:?}", ev.values()),
oninput: move |ev| println!("Input {:?}", ev.values()),
input { r#type: "text", name: "username" }
input { r#type: "text", name: "full-name" }
input { r#type: "password", name: "password" }
+17 -20
View File
@@ -8,33 +8,30 @@ fn main() {
}
fn app(cx: Scope) -> Element {
let onsubmit = move |evt: FormEvent| {
cx.spawn(async move {
let resp = reqwest::Client::new()
.post("http://localhost:8080/login")
.form(&[
("username", &evt.values["username"]),
("password", &evt.values["password"]),
])
.send()
.await;
let onsubmit = move |evt: FormEvent| async move {
let resp = reqwest::Client::new()
.post("http://localhost:8080/login")
.form(&[
("username", &evt.values()["username"]),
("password", &evt.values()["password"]),
])
.send()
.await;
match resp {
// Parse data from here, such as storing a response token
Ok(_data) => println!("Login successful!"),
match resp {
// Parse data from here, such as storing a response token
Ok(_data) => println!("Login successful!"),
//Handle any errors from the fetch here
Err(_err) => {
println!("Login failed - you need a login server running on localhost:8080.")
}
//Handle any errors from the fetch here
Err(_err) => {
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 {}
+1 -1
View File
@@ -35,7 +35,7 @@ frameworks = ["WebKit"]
[dependencies]
anyhow = "1.0.56"
log = "0.4.11"
wry = "0.28.0"
wry = "0.35.0"
dioxus = { path = "../../packages/dioxus" }
dioxus-desktop = { path = "../../packages/desktop", features = [
"tokio_runtime",
+1 -3
View File
@@ -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"
}
+3
View File
@@ -0,0 +1,3 @@
/target
/dist
.env
+25
View File
@@ -0,0 +1,25 @@
[package]
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
[dependencies]
console_error_panic_hook = "0.1"
dioxus-logger = "0.4.1"
dioxus = { path = "../../packages/dioxus", version = "*" }
dioxus-router = { path = "../../packages/router", version = "*" }
dioxus-web = { path = "../../packages/web", version = "*" }
fermi = { path = "../../packages/fermi", version = "*" }
form_urlencoded = "1.2.0"
gloo-storage = "0.3.0"
log = "0.4"
openidconnect = "3.4.0"
reqwest = "0.11.20"
serde = { version = "1.0.188", features = ["derive"] }
serde_json = "1.0.105"
thiserror = "1.0.48"
uuid = "1.4"
web-sys = { version = "0.3", features = ["Request", "Document"] }
+47
View File
@@ -0,0 +1,47 @@
[application]
# dioxus project name
name = "OpenID Connect authentication demo"
# default platfrom
# you can also use `dioxus serve/build --platform XXX` to use other platform
# value: web | desktop
default_platform = "web"
# Web `build` & `serve` dist path
out_dir = "dist"
# resource (static) file folder
asset_dir = "public"
[web.app]
# HTML title tag content
title = "OpenID Connect authentication demo"
[web.watcher]
index_on_404 = true
watch_path = ["src"]
# include `assets` in web platform
[web.resource]
# CSS style file
style = []
# Javascript code file
script = []
[web.resource.dev]
# Javascript code file
# serve: [dev-server] only
script = []
[application.plugins]
available = true
required = []
+13
View File
@@ -0,0 +1,13 @@
# OpenID Connect example to show how to authenticate an user
The environment variables in `.cargo/config.toml` must be set in order for this example to work(if this example is just being compiled from the root workspace, the `.cargo/config.toml` from the root workspace must be set as stated in the [Cargo book](https://doc.rust-lang.org/cargo/reference/config.html)).
Once they are set, you can run `dx serve`
### Environment variables summary
```DIOXUS_FRONT_ISSUER_URL``` The openid-connect's issuer url
```DIOXUS_FRONT_CLIENT_ID``` The openid-connect's client id
```DIOXUS_FRONT_URL``` The url the frontend is supposed to be running on, it could be for example `http://localhost:8080`
@@ -0,0 +1,2 @@
pub const DIOXUS_FRONT_AUTH_TOKEN: &str = "auth_token";
pub const DIOXUS_FRONT_AUTH_REQUEST: &str = "auth_request";
@@ -0,0 +1,20 @@
use openidconnect::{core::CoreErrorResponseType, url, RequestTokenError, StandardErrorResponse};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("Discovery error: {0}")]
OpenIdConnect(
#[from] openidconnect::DiscoveryError<openidconnect::reqwest::Error<reqwest::Error>>,
),
#[error("Parsing error: {0}")]
Parse(#[from] url::ParseError),
#[error("Request token error: {0}")]
RequestToken(
#[from]
RequestTokenError<
openidconnect::reqwest::Error<reqwest::Error>,
StandardErrorResponse<CoreErrorResponseType>,
>,
),
}
+60
View File
@@ -0,0 +1,60 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
use fermi::*;
use gloo_storage::{LocalStorage, Storage};
use log::LevelFilter;
pub(crate) mod constants;
pub(crate) mod errors;
pub(crate) mod model;
pub(crate) mod oidc;
pub(crate) mod props;
pub(crate) mod router;
pub(crate) mod storage;
pub(crate) mod views;
use oidc::{AuthRequestState, AuthTokenState};
use router::Route;
use dioxus_router::prelude::*;
use crate::{
constants::{DIOXUS_FRONT_AUTH_REQUEST, DIOXUS_FRONT_AUTH_TOKEN},
oidc::ClientState,
};
pub static FERMI_CLIENT: fermi::AtomRef<ClientState> = AtomRef(|_| ClientState::default());
// An option is required to prevent the component from being constantly refreshed
pub static FERMI_AUTH_TOKEN: fermi::AtomRef<Option<AuthTokenState>> = AtomRef(|_| None);
pub static FERMI_AUTH_REQUEST: fermi::AtomRef<Option<AuthRequestState>> = AtomRef(|_| None);
pub static DIOXUS_FRONT_ISSUER_URL: &str = env!("DIOXUS_FRONT_ISSUER_URL");
pub static DIOXUS_FRONT_CLIENT_ID: &str = env!("DIOXUS_FRONT_CLIENT_ID");
pub static DIOXUS_FRONT_URL: &str = env!("DIOXUS_FRONT_URL");
fn App(cx: Scope) -> Element {
use_init_atom_root(cx);
// Retrieve the value stored in the browser's storage
let stored_auth_token = LocalStorage::get(DIOXUS_FRONT_AUTH_TOKEN)
.ok()
.unwrap_or(AuthTokenState::default());
let fermi_auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
if fermi_auth_token.read().is_none() {
*fermi_auth_token.write() = Some(stored_auth_token);
}
let stored_auth_request = LocalStorage::get(DIOXUS_FRONT_AUTH_REQUEST)
.ok()
.unwrap_or(AuthRequestState::default());
let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST);
if fermi_auth_request.read().is_none() {
*fermi_auth_request.write() = Some(stored_auth_request);
}
render! { Router::<Route> {} }
}
fn main() {
dioxus_logger::init(LevelFilter::Info).expect("failed to init logger");
console_error_panic_hook::set_once();
log::info!("starting app");
dioxus_web::launch(App);
}
@@ -0,0 +1 @@
pub(crate) mod user;
@@ -0,0 +1,7 @@
use uuid::Uuid;
#[derive(PartialEq)]
pub struct User {
pub id: Uuid,
pub name: String,
}
+125
View File
@@ -0,0 +1,125 @@
use openidconnect::{
core::{CoreClient, CoreErrorResponseType, CoreIdToken, CoreResponseType, CoreTokenResponse},
reqwest::async_http_client,
url::Url,
AuthenticationFlow, AuthorizationCode, ClaimsVerificationError, ClientId, CsrfToken, IssuerUrl,
LogoutRequest, Nonce, ProviderMetadataWithLogout, RedirectUrl, RefreshToken, RequestTokenError,
StandardErrorResponse,
};
use serde::{Deserialize, Serialize};
use crate::{props::client::ClientProps, DIOXUS_FRONT_CLIENT_ID};
#[derive(Clone, Debug, Default)]
pub struct ClientState {
pub oidc_client: Option<ClientProps>,
}
/// State that holds the nonce and authorization url and the nonce generated to log in an user
#[derive(Clone, Deserialize, Serialize, Default)]
pub struct AuthRequestState {
pub auth_request: Option<AuthRequest>,
}
#[derive(Clone, Deserialize, Serialize)]
pub struct AuthRequest {
pub nonce: Nonce,
pub authorize_url: String,
}
/// State the tokens returned once the user is authenticated
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
pub struct AuthTokenState {
/// Token used to identify the user
pub id_token: Option<CoreIdToken>,
/// Token used to refresh the tokens if they expire
pub refresh_token: Option<RefreshToken>,
}
pub fn email(
client: CoreClient,
id_token: CoreIdToken,
nonce: Nonce,
) -> Result<String, ClaimsVerificationError> {
match id_token.claims(&client.id_token_verifier(), &nonce) {
Ok(claims) => Ok(claims.clone().email().unwrap().to_string()),
Err(error) => Err(error),
}
}
pub fn authorize_url(client: CoreClient) -> AuthRequest {
let (authorize_url, _csrf_state, nonce) = client
.authorize_url(
AuthenticationFlow::<CoreResponseType>::AuthorizationCode,
CsrfToken::new_random,
Nonce::new_random,
)
.add_scope(openidconnect::Scope::new("email".to_string()))
.add_scope(openidconnect::Scope::new("profile".to_string()))
.url();
AuthRequest {
authorize_url: authorize_url.to_string(),
nonce,
}
}
pub async fn init_provider_metadata() -> Result<ProviderMetadataWithLogout, crate::errors::Error> {
let issuer_url = IssuerUrl::new(crate::DIOXUS_FRONT_ISSUER_URL.to_string())?;
Ok(ProviderMetadataWithLogout::discover_async(issuer_url, async_http_client).await?)
}
pub async fn init_oidc_client() -> Result<(ClientId, CoreClient), crate::errors::Error> {
let client_id = ClientId::new(crate::DIOXUS_FRONT_CLIENT_ID.to_string());
let provider_metadata = init_provider_metadata().await?;
let client_secret = None;
let redirect_url = RedirectUrl::new(format!("{}/login", crate::DIOXUS_FRONT_URL))?;
Ok((
client_id.clone(),
CoreClient::from_provider_metadata(provider_metadata, client_id, client_secret)
.set_redirect_uri(redirect_url),
))
}
///TODO: Add pkce_pacifier
pub async fn token_response(
oidc_client: CoreClient,
code: String,
) -> Result<CoreTokenResponse, crate::errors::Error> {
// let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
Ok(oidc_client
.exchange_code(AuthorizationCode::new(code.clone()))
// .set_pkce_verifier(pkce_verifier)
.request_async(async_http_client)
.await?)
}
pub async fn exchange_refresh_token(
oidc_client: CoreClient,
refresh_token: RefreshToken,
) -> Result<
CoreTokenResponse,
RequestTokenError<
openidconnect::reqwest::Error<reqwest::Error>,
StandardErrorResponse<CoreErrorResponseType>,
>,
> {
oidc_client
.exchange_refresh_token(&refresh_token)
.request_async(async_http_client)
.await
}
pub async fn log_out_url(id_token_hint: CoreIdToken) -> Result<Url, crate::errors::Error> {
let provider_metadata = init_provider_metadata().await?;
let end_session_url = provider_metadata
.additional_metadata()
.clone()
.end_session_endpoint
.unwrap();
let logout_request: LogoutRequest = LogoutRequest::from(end_session_url);
Ok(logout_request
.set_client_id(ClientId::new(DIOXUS_FRONT_CLIENT_ID.to_string()))
.set_id_token_hint(&id_token_hint)
.http_get_url())
}
@@ -0,0 +1,20 @@
use dioxus::prelude::*;
use openidconnect::{core::CoreClient, ClientId};
#[derive(Props, Clone, Debug)]
pub struct ClientProps {
pub client: CoreClient,
pub client_id: ClientId,
}
impl PartialEq for ClientProps {
fn eq(&self, other: &Self) -> bool {
self.client_id == other.client_id
}
}
impl ClientProps {
pub fn new(client_id: ClientId, client: CoreClient) -> Self {
ClientProps { client_id, client }
}
}
@@ -0,0 +1 @@
pub(crate) mod client;
@@ -0,0 +1,17 @@
use crate::views::{header::AuthHeader, home::Home, login::Login, not_found::NotFound};
use dioxus::prelude::*;
use dioxus_router::prelude::*;
#[derive(Routable, Clone)]
pub enum Route {
#[layout(AuthHeader)]
#[route("/")]
Home {},
// https://dioxuslabs.com/learn/0.4/router/reference/routes#query-segments
#[route("/login?:query_string")]
Login { query_string: String },
#[end_layout]
#[route("/:..route")]
NotFound { route: Vec<String> },
}
@@ -0,0 +1,38 @@
use fermi::UseAtomRef;
use gloo_storage::{LocalStorage, Storage};
use serde::{Deserialize, Serialize};
use crate::{
constants::{DIOXUS_FRONT_AUTH_REQUEST, DIOXUS_FRONT_AUTH_TOKEN},
oidc::{AuthRequestState, AuthTokenState},
};
#[derive(Serialize, Deserialize, Clone)]
pub struct StorageEntry<T> {
pub key: String,
pub value: T,
}
pub trait PersistentWrite<T: Serialize + Clone> {
fn persistent_set(atom_ref: &UseAtomRef<Option<T>>, entry: Option<T>);
}
impl PersistentWrite<AuthTokenState> for AuthTokenState {
fn persistent_set(
atom_ref: &UseAtomRef<Option<AuthTokenState>>,
entry: Option<AuthTokenState>,
) {
*atom_ref.write() = entry.clone();
LocalStorage::set(DIOXUS_FRONT_AUTH_TOKEN, entry).unwrap();
}
}
impl PersistentWrite<AuthRequestState> for AuthRequestState {
fn persistent_set(
atom_ref: &UseAtomRef<Option<AuthRequestState>>,
entry: Option<AuthRequestState>,
) {
*atom_ref.write() = entry.clone();
LocalStorage::set(DIOXUS_FRONT_AUTH_REQUEST, entry).unwrap();
}
}
@@ -0,0 +1,250 @@
use crate::{
oidc::{
authorize_url, email, exchange_refresh_token, init_oidc_client, log_out_url,
AuthRequestState, AuthTokenState, ClientState,
},
props::client::ClientProps,
router::Route,
storage::PersistentWrite,
FERMI_AUTH_REQUEST, FERMI_AUTH_TOKEN, FERMI_CLIENT,
};
use dioxus::prelude::*;
use dioxus_router::prelude::{Link, Outlet};
use fermi::*;
use openidconnect::{url::Url, OAuth2TokenResponse, TokenResponse};
#[component]
pub fn LogOut(cx: Scope<ClientProps>) -> Element {
let fermi_auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
let fermi_auth_token_read = fermi_auth_token.read().clone();
let log_out_url_state = use_state(cx, || None::<Option<Result<Url, crate::errors::Error>>>);
cx.render(match fermi_auth_token_read {
Some(fermi_auth_token_read) => match fermi_auth_token_read.id_token.clone() {
Some(id_token) => match log_out_url_state.get() {
Some(log_out_url_result) => match log_out_url_result {
Some(uri) => match uri {
Ok(uri) => {
rsx! {
Link {
onclick: move |_| {
{
AuthTokenState::persistent_set(
fermi_auth_token,
Some(AuthTokenState::default()),
);
}
},
to: uri.to_string(),
"Log out"
}
}
}
Err(error) => {
rsx! {
div { format!{"Failed to load disconnection url: {:?}", error} }
}
}
},
None => {
rsx! { div { "Loading... Please wait" } }
}
},
None => {
let logout_url_task = move || {
cx.spawn({
let log_out_url_state = log_out_url_state.to_owned();
async move {
let logout_url = log_out_url(id_token).await;
let logout_url_option = Some(logout_url);
log_out_url_state.set(Some(logout_url_option));
}
})
};
logout_url_task();
rsx! { div{"Loading log out url... Please wait"}}
}
},
None => {
rsx! {{}}
}
},
None => {
rsx! {{}}
}
})
}
#[component]
pub fn RefreshToken(cx: Scope<ClientProps>) -> Element {
let fermi_auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST);
let fermi_auth_token_read = fermi_auth_token.read().clone();
cx.render(match fermi_auth_token_read {
Some(fermi_auth_client_read) => match fermi_auth_client_read.refresh_token {
Some(refresh_token) => {
let fermi_auth_token = fermi_auth_token.to_owned();
let fermi_auth_request = fermi_auth_request.to_owned();
let client = cx.props.client.clone();
let exchange_refresh_token_spawn = move || {
cx.spawn({
async move {
let exchange_refresh_token =
exchange_refresh_token(client, refresh_token).await;
match exchange_refresh_token {
Ok(response_token) => {
AuthTokenState::persistent_set(
&fermi_auth_token,
Some(AuthTokenState {
id_token: response_token.id_token().cloned(),
refresh_token: response_token.refresh_token().cloned(),
}),
);
}
Err(_error) => {
AuthTokenState::persistent_set(
&fermi_auth_token,
Some(AuthTokenState::default()),
);
AuthRequestState::persistent_set(
&fermi_auth_request,
Some(AuthRequestState::default()),
);
}
}
}
})
};
exchange_refresh_token_spawn();
rsx! { div { "Refreshing session, please wait" } }
}
None => {
rsx! { div { "Id token expired and no refresh token found" } }
}
},
None => {
rsx! {{}}
}
})
}
#[component]
pub fn LoadClient(cx: Scope) -> Element {
let init_client_future = use_future(cx, (), |_| async move { init_oidc_client().await });
let fermi_client: &UseAtomRef<ClientState> = use_atom_ref(cx, &FERMI_CLIENT);
cx.render(match init_client_future.value() {
Some(client_props) => match client_props {
Ok((client_id, client)) => {
*fermi_client.write() = ClientState {
oidc_client: Some(ClientProps::new(client_id.clone(), client.clone())),
};
rsx! {
div { "Client successfully loaded" }
Outlet::<Route> {}
}
}
Err(error) => {
rsx! {
div { format!{"Failed to load client: {:?}", error} }
log::info!{"Failed to load client: {:?}", error},
Outlet::<Route> {}
}
}
},
None => {
rsx! {
div {
div { "Loading client, please wait" }
Outlet::<Route> {}
}
}
}
})
}
#[component]
pub fn AuthHeader(cx: Scope) -> Element {
let auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST);
let fermi_client: &UseAtomRef<ClientState> = use_atom_ref(cx, &FERMI_CLIENT);
let client = fermi_client.read().oidc_client.clone();
let auth_request_read = fermi_auth_request.read().clone();
let auth_token_read = auth_token.read().clone();
cx.render(match (client, auth_request_read, auth_token_read) {
// We have everything we need to attempt to authenticate the user
(Some(client_props), Some(auth_request), Some(auth_token)) => {
match auth_request.auth_request {
Some(auth_request) => {
match auth_token.id_token {
Some(id_token) => {
match email(
client_props.client.clone(),
id_token.clone(),
auth_request.nonce.clone(),
) {
Ok(email) => {
rsx! {
div {
div { email }
LogOut { client_id: client_props.client_id, client: client_props.client }
Outlet::<Route> {}
}
}
}
// Id token failed to be decoded
Err(error) => match error {
// Id token failed to be decoded because it expired, we refresh it
openidconnect::ClaimsVerificationError::Expired(_message) => {
log::info!("Token expired");
rsx! {
div {
RefreshToken {client_id: client_props.client_id, client: client_props.client}
Outlet::<Route> {}
}
}
}
// Other issue with token decoding
_ => {
log::info!("Other issue with token");
rsx! {
div {
div { error.to_string() }
Outlet::<Route> {}
}
}
}
},
}
}
// User is not logged in
None => {
rsx! {
div {
Link { to: auth_request.authorize_url.clone(), "Log in" }
Outlet::<Route> {}
}
}
}
}
}
None => {
let auth_request = authorize_url(client_props.client);
AuthRequestState::persistent_set(
fermi_auth_request,
Some(AuthRequestState {
auth_request: Some(auth_request),
}),
);
rsx! { div { "Loading nonce" } }
}
}
}
// Client is not initialized yet, we need it for everything
(None, _, _) => {
rsx! { LoadClient {} }
}
// We need everything loaded before doing anything
(_client, _auth_request, _auth_token) => {
rsx! {{}}
}
})
}
@@ -0,0 +1,5 @@
use dioxus::prelude::*;
pub fn Home(cx: Scope) -> Element {
render! { div { "Hello world" } }
}
@@ -0,0 +1,86 @@
use crate::{
oidc::{token_response, AuthRequestState, AuthTokenState},
router::Route,
storage::PersistentWrite,
DIOXUS_FRONT_URL, FERMI_AUTH_REQUEST, FERMI_AUTH_TOKEN, FERMI_CLIENT,
};
use dioxus::prelude::*;
use dioxus_router::prelude::{Link, NavigationTarget};
use fermi::*;
use openidconnect::{OAuth2TokenResponse, TokenResponse};
#[component]
pub fn Login(cx: Scope, query_string: String) -> Element {
let fermi_client = use_atom_ref(cx, &FERMI_CLIENT);
let fermi_auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
let home_url: NavigationTarget<Route> = DIOXUS_FRONT_URL.parse().unwrap();
let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST);
let client = fermi_client.read().oidc_client.clone();
let auth_token_read = fermi_auth_token.read().clone();
cx.render(match (client, auth_token_read) {
(Some(client_props), Some(auth_token_read)) => {
match (auth_token_read.id_token, auth_token_read.refresh_token) {
(Some(_id_token), Some(_refresh_token)) => {
rsx! {
div { "Sign in successful" }
Link { to: home_url, "Go back home" }
}
}
// If the refresh token is set but not the id_token, there was an error, we just go back home and reset their value
(None, Some(_)) | (Some(_), None) => {
rsx! {
div { "Error while attempting to log in" }
Link {
to: home_url,
onclick: move |_| {
AuthTokenState::persistent_set(fermi_auth_token, Some(AuthTokenState::default()));
AuthRequestState::persistent_set(
fermi_auth_request,
Some(AuthRequestState::default()),
);
},
"Go back home"
}
}
}
(None, None) => {
let mut query_pairs = form_urlencoded::parse(query_string.as_bytes());
let code_pair = query_pairs.find(|(key, _value)| key == "code");
match code_pair {
Some((_key, code)) => {
let auth_code = code.to_string();
let token_response_spawn = move ||{
cx.spawn({
let fermi_auth_token = fermi_auth_token.to_owned();
async move {
let token_response_result = token_response(client_props.client, auth_code).await;
match token_response_result{
Ok(token_response) => {
let id_token = token_response.id_token().unwrap();
AuthTokenState::persistent_set(&fermi_auth_token, Some(AuthTokenState {
id_token: Some(id_token.clone()),
refresh_token: token_response.refresh_token().cloned()
}));
}
Err(error) => {
log::warn!{"{error}"};
}
}
}
})
};
token_response_spawn();
rsx!{ div {} }
}
None => {
rsx! { div { "No code provided" } }
}
}
}
}
}
(_, _) => {
rsx! {{}}
}
})
}
@@ -0,0 +1,4 @@
pub(crate) mod header;
pub(crate) mod home;
pub(crate) mod login;
pub(crate) mod not_found;
@@ -0,0 +1,7 @@
use dioxus::prelude::*;
#[component]
pub fn NotFound(cx: Scope, route: Vec<String>) -> Element {
let routes = route.join("");
render! {rsx! {div{routes}}}
}
+12
View File
@@ -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()),
}
})
}
+2 -4
View File
@@ -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!"
+1 -1
View File
@@ -21,7 +21,7 @@ use dioxus::events::*;
use dioxus::html::input_data::keyboard_types::Key;
use dioxus::html::MouseEvent;
use dioxus::prelude::*;
use dioxus_desktop::wry::application::dpi::LogicalSize;
use dioxus_desktop::tao::dpi::LogicalSize;
use dioxus_desktop::{Config, WindowBuilder};
fn main() {
+1
View File
@@ -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
+21 -7
View File
@@ -14,29 +14,35 @@ use dioxus_router::prelude::*;
#[derive(Routable, Clone)]
#[rustfmt::skip]
enum Route {
// segments that start with ?: are query segments
#[route("/blog?:query_params")]
// segments that start with ?:.. are query segments that capture the entire query
#[route("/blog?:..query_params")]
BlogPost {
// You must include query segments in child variants
query_params: BlogQuerySegments,
query_params: ManualBlogQuerySegments,
},
// segments that follow the ?:field&:other_field syntax are query segments that follow the standard url query syntax
#[route("/autoblog?:name&:surname")]
AutomaticBlogPost {
name: String,
surname: String,
},
}
#[derive(Debug, Clone, PartialEq)]
struct BlogQuerySegments {
struct ManualBlogQuerySegments {
name: String,
surname: String,
}
/// The display impl needs to display the query in a way that can be parsed:
impl Display for BlogQuerySegments {
impl Display for ManualBlogQuerySegments {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "name={}&surname={}", self.name, self.surname)
}
}
/// The query segment is anything that implements <https://docs.rs/dioxus-router/latest/dioxus_router/routable/trait.FromQuery.html>. You can implement that trait for a struct if you want to parse multiple query parameters.
impl FromQuery for BlogQuerySegments {
impl FromQuery for ManualBlogQuerySegments {
fn from_query(query: &str) -> Self {
let mut name = None;
let mut surname = None;
@@ -57,13 +63,21 @@ impl FromQuery for BlogQuerySegments {
}
#[component]
fn BlogPost(cx: Scope, query_params: BlogQuerySegments) -> Element {
fn BlogPost(cx: Scope, query_params: ManualBlogQuerySegments) -> Element {
render! {
div{"This is your blogpost with a query segment:"}
div{format!("{:?}", query_params)}
}
}
#[component]
fn AutomaticBlogPost(cx: Scope, name: String, surname: String) -> Element {
render! {
div{"This is your blogpost with a query segment:"}
div{format!("name={}&surname={}", name, surname)}
}
}
#[component]
fn App(cx: Scope) -> Element {
render! { Router::<Route>{} }
+5
View File
@@ -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";
+1 -1
View File
@@ -64,7 +64,7 @@ fn DataEditor(cx: Scope, id: usize) -> Element {
fn DataView(cx: Scope, id: usize) -> Element {
let cool_data = use_shared_state::<CoolData>(cx).unwrap();
let oninput = |e: FormEvent| cool_data.write().set(*id, e.value.clone());
let oninput = |e: FormEvent| cool_data.write().set(*id, e.value());
let cool_data = cool_data.read();
let my_data = &cool_data.view(id).unwrap();
+36
View File
@@ -0,0 +1,36 @@
use dioxus::prelude::*;
fn main() {
let mut dom = VirtualDom::new(app);
let _ = dom.rebuild();
let html = dioxus_ssr::render(&dom);
println!("{}", html);
}
fn app(cx: Scope) -> Element {
render! {
Component {
width: "10px",
extra_data: "hello{1}",
extra_data2: "hello{2}",
height: "10px",
left: 1
}
}
}
#[component]
fn Component<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> {
render! {
audio { ..cx.props.attributes, "1: {cx.props.extra_data}\n2: {cx.props.extra_data2}" }
}
}
#[derive(Props)]
struct Props<'a> {
#[props(extends = GlobalAttributes)]
attributes: Vec<Attribute<'a>>,
extra_data: &'a str,
extra_data2: &'a str,
}
+1 -1
View File
@@ -18,4 +18,4 @@ dioxus = { path = "../../packages/dioxus" }
dioxus-desktop = { path = "../../packages/desktop" }
[target.'cfg(target_arch = "wasm32")'.dependencies]
dioxus-web = { path = "../../packages/web" }
dioxus-web = { path = "../../packages/web" }
+1 -1
View File
@@ -30,7 +30,7 @@ watch_path = ["src", "public"]
[web.resource]
# CSS style file
style = ["/tailwind.css"]
style = []
# Javascript code file
script = []
+1 -1
View File
@@ -7,7 +7,7 @@ This example shows how an app might be styled with TailwindCSS.
1. Install the Dioxus CLI:
```bash
cargo install --git https://github.com/DioxusLabs/cli
cargo install dioxus-cli
```
2. Install npm: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm
@@ -0,0 +1 @@
*,:before,:after{box-sizing:border-box;border:0 solid #e5e7eb}:before,:after{--tw-content:""}html{-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-feature-settings:normal;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}body{line-height:inherit;margin:0}hr{color:inherit;border-top-width:1px;height:0}abbr:where([title]){text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:#0000;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{margin:0;padding:0;list-style:none}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after,::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (width>=640px){.container{max-width:640px}}@media (width>=768px){.container{max-width:768px}}@media (width>=1024px){.container{max-width:1024px}}@media (width>=1280px){.container{max-width:1280px}}@media (width>=1536px){.container{max-width:1536px}}.mx-auto{margin-left:auto;margin-right:auto}.mb-16{margin-bottom:4rem}.mb-4{margin-bottom:1rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.mr-5{margin-right:1.25rem}.mt-4{margin-top:1rem}.flex{display:flex}.inline-flex{display:inline-flex}.hidden{display:none}.h-10{height:2.5rem}.h-4{height:1rem}.w-10{width:2.5rem}.w-4{width:1rem}.w-5\/6{width:83.3333%}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-center{justify-content:center}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.border-0{border-width:0}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.bg-indigo-500{--tw-bg-opacity:1;background-color:rgb(99 102 241/var(--tw-bg-opacity))}.object-cover{-o-object-fit:cover;object-fit:cover}.object-center{-o-object-position:center;object-position:center}.p-2{padding:.5rem}.p-5{padding:1.25rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-24{padding-top:6rem;padding-bottom:6rem}.text-center{text-align:center}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-medium{font-weight:500}.leading-relaxed{line-heiLine truncated
+8 -6
View File
@@ -2,21 +2,23 @@
use dioxus::prelude::*;
const _STYLE: &str = mg!(file("./public/tailwind.css"));
fn main() {
#[cfg(not(target_arch = "wasm32"))]
dioxus_desktop::launch_cfg(
app,
dioxus_desktop::Config::new()
.with_custom_head(r#"<link rel="stylesheet" href="public/tailwind.css">"#.to_string()),
);
dioxus_desktop::launch(app);
#[cfg(target_arch = "wasm32")]
dioxus_web::launch(app);
}
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 {}
+1 -1
View File
@@ -17,7 +17,7 @@ fn app(cx: Scope) -> Element {
rows: "10",
cols: "80",
value: "{model}",
oninput: move |e| model.set(e.value.clone()),
oninput: move |e| model.set(e.value().clone()),
}
})
}
+133 -79
View File
@@ -7,6 +7,8 @@ fn main() {
dioxus_desktop::launch(app);
}
const _STYLE: &str = mg!(file("./examples/assets/todomvc.css"));
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum FilterState {
All,
@@ -24,8 +26,6 @@ pub struct TodoItem {
pub fn app(cx: Scope<()>) -> Element {
let todos = use_state(cx, im_rc::HashMap::<u32, TodoItem>::default);
let filter = use_state(cx, || FilterState::All);
let draft = use_state(cx, || "".to_string());
let todo_id = use_state(cx, || 0);
// Filter the todos based on the filter state
let mut filtered_todos = todos
@@ -47,45 +47,10 @@ pub fn app(cx: Scope<()>) -> Element {
let show_clear_completed = todos.values().any(|todo| todo.checked);
let selected = |state| {
if *filter == state {
"selected"
} else {
"false"
}
};
cx.render(rsx! {
section { class: "todoapp",
style { include_str!("./assets/todomvc.css") }
header { class: "header",
h1 {"todos"}
input {
class: "new-todo",
placeholder: "What needs to be done?",
value: "{draft}",
autofocus: "true",
oninput: move |evt| {
draft.set(evt.value.clone());
},
onkeydown: move |evt| {
if evt.key() == Key::Enter && !draft.is_empty() {
todos.make_mut().insert(
**todo_id,
TodoItem {
id: **todo_id,
checked: false,
contents: draft.to_string(),
},
);
*todo_id.make_mut() += 1;
draft.set("".to_string());
}
}
}
}
section {
class: "main",
TodoHeader { todos: todos }
section { class: "main",
if !todos.is_empty() {
rsx! {
input {
@@ -111,43 +76,58 @@ pub fn app(cx: Scope<()>) -> Element {
}))
}
(!todos.is_empty()).then(|| rsx!(
footer { class: "footer",
span { class: "todo-count",
strong {"{active_todo_count} "}
span {"{active_todo_text} left"}
}
ul { class: "filters",
for (state, state_text, url) in [
(FilterState::All, "All", "#/"),
(FilterState::Active, "Active", "#/active"),
(FilterState::Completed, "Completed", "#/completed"),
] {
li {
a {
href: url,
class: selected(state),
onclick: move |_| filter.set(state),
prevent_default: "onclick",
state_text
}
}
}
}
show_clear_completed.then(|| rsx!(
button {
class: "clear-completed",
onclick: move |_| todos.make_mut().retain(|_, todo| !todo.checked),
"Clear completed"
}
))
ListFooter {
active_todo_count: active_todo_count,
active_todo_text: active_todo_text,
show_clear_completed: show_clear_completed,
todos: todos,
filter: filter,
}
))
}
}
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" }}
PageFooter {}
})
}
#[derive(Props)]
pub struct TodoHeaderProps<'a> {
todos: &'a UseState<im_rc::HashMap<u32, TodoItem>>,
}
pub fn TodoHeader<'a>(cx: Scope<'a, TodoHeaderProps<'a>>) -> Element {
let draft = use_state(cx, || "".to_string());
let todo_id = use_state(cx, || 0);
cx.render(rsx! {
header { class: "header",
h1 { "todos" }
input {
class: "new-todo",
placeholder: "What needs to be done?",
value: "{draft}",
autofocus: "true",
oninput: move |evt| {
draft.set(evt.value().clone());
},
onkeydown: move |evt| {
if evt.key() == Key::Enter && !draft.is_empty() {
cx.props
.todos
.make_mut()
.insert(
**todo_id,
TodoItem {
id: **todo_id,
checked: false,
contents: draft.to_string(),
},
);
*todo_id.make_mut() += 1;
draft.set("".to_string());
}
}
}
}
})
}
@@ -167,8 +147,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",
@@ -176,26 +155,28 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
id: "cbg-{todo.id}",
checked: "{todo.checked}",
oninput: move |evt| {
cx.props.todos.make_mut()[&cx.props.id].checked = evt.value.parse().unwrap();
cx.props.todos.make_mut()[&cx.props.id].checked = evt.value().parse().unwrap();
}
}
label {
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!{
input {
class: "edit",
value: "{todo.contents}",
oninput: move |evt| cx.props.todos.make_mut()[&cx.props.id].contents = evt.value.clone(),
oninput: move |evt| cx.props.todos.make_mut()[&cx.props.id].contents = evt.value(),
autofocus: "true",
onfocusout: move |_| is_editing.set(false),
onkeydown: move |evt| {
@@ -209,3 +190,76 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
}
})
}
#[derive(Props)]
pub struct ListFooterProps<'a> {
todos: &'a UseState<im_rc::HashMap<u32, TodoItem>>,
active_todo_count: usize,
active_todo_text: &'a str,
show_clear_completed: bool,
filter: &'a UseState<FilterState>,
}
pub fn ListFooter<'a>(cx: Scope<'a, ListFooterProps<'a>>) -> Element {
let active_todo_count = cx.props.active_todo_count;
let active_todo_text = cx.props.active_todo_text;
let selected = |state| {
if *cx.props.filter == state {
"selected"
} else {
"false"
}
};
cx.render(rsx! {
footer { class: "footer",
span { class: "todo-count",
strong { "{active_todo_count} " }
span { "{active_todo_text} left" }
}
ul { class: "filters",
for (state , state_text , url) in [
(FilterState::All, "All", "#/"),
(FilterState::Active, "Active", "#/active"),
(FilterState::Completed, "Completed", "#/completed"),
] {
li {
a {
href: url,
class: selected(state),
onclick: move |_| cx.props.filter.set(state),
prevent_default: "onclick",
state_text
}
}
}
}
if cx.props.show_clear_completed {
cx.render(rsx! {
button {
class: "clear-completed",
onclick: move |_| cx.props.todos.make_mut().retain(|_, todo| !todo.checked),
"Clear completed"
}
})
}
}
})
}
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" }
}
}
})
}
+188
View File
@@ -0,0 +1,188 @@
use dioxus::prelude::*;
use dioxus_desktop::wry::http;
use dioxus_desktop::wry::http::Response;
use dioxus_desktop::{use_asset_handler, AssetRequest};
use http::{header::*, response::Builder as ResponseBuilder, status::StatusCode};
use std::{io::SeekFrom, path::PathBuf};
use tokio::io::AsyncReadExt;
use tokio::io::AsyncSeekExt;
use tokio::io::AsyncWriteExt;
const VIDEO_PATH: &str = "./examples/assets/test_video.mp4";
fn main() {
let video_file = PathBuf::from(VIDEO_PATH);
if !video_file.exists() {
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
println!("Downloading video file...");
let video_url =
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
let mut response = reqwest::get(video_url).await.unwrap();
let mut file = tokio::fs::File::create(&video_file).await.unwrap();
while let Some(chunk) = response.chunk().await.unwrap() {
file.write_all(&chunk).await.unwrap();
}
});
}
dioxus_desktop::launch(app);
}
fn app(cx: Scope) -> Element {
use_asset_handler(cx, "videos", move |request, responder| {
// Using dioxus::spawn works, but is slower than a dedicated thread
tokio::task::spawn(async move {
let video_file = PathBuf::from(VIDEO_PATH);
let mut file = tokio::fs::File::open(&video_file).await.unwrap();
match get_stream_response(&mut file, &request).await {
Ok(response) => responder.respond(response),
Err(err) => eprintln!("Error: {}", err),
}
});
});
render! {
div {
video {
src: "/videos/test_video.mp4",
autoplay: true,
controls: true,
width: 640,
height: 480
}
}
}
}
/// This was taken from wry's example
async fn get_stream_response(
asset: &mut (impl tokio::io::AsyncSeek + tokio::io::AsyncRead + Unpin + Send + Sync),
request: &AssetRequest,
) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error>> {
// get stream length
let len = {
let old_pos = asset.stream_position().await?;
let len = asset.seek(SeekFrom::End(0)).await?;
asset.seek(SeekFrom::Start(old_pos)).await?;
len
};
let mut resp = ResponseBuilder::new().header(CONTENT_TYPE, "video/mp4");
// if the webview sent a range header, we need to send a 206 in return
// Actually only macOS and Windows are supported. Linux will ALWAYS return empty headers.
let http_response = if let Some(range_header) = request.headers().get("range") {
let not_satisfiable = || {
ResponseBuilder::new()
.status(StatusCode::RANGE_NOT_SATISFIABLE)
.header(CONTENT_RANGE, format!("bytes */{len}"))
.body(vec![])
};
// parse range header
let ranges = if let Ok(ranges) = http_range::HttpRange::parse(range_header.to_str()?, len) {
ranges
.iter()
// map the output back to spec range <start-end>, example: 0-499
.map(|r| (r.start, r.start + r.length - 1))
.collect::<Vec<_>>()
} else {
return Ok(not_satisfiable()?);
};
/// The Maximum bytes we send in one range
const MAX_LEN: u64 = 1000 * 1024;
if ranges.len() == 1 {
let &(start, mut end) = ranges.first().unwrap();
// check if a range is not satisfiable
//
// this should be already taken care of by HttpRange::parse
// but checking here again for extra assurance
if start >= len || end >= len || end < start {
return Ok(not_satisfiable()?);
}
// adjust end byte for MAX_LEN
end = start + (end - start).min(len - start).min(MAX_LEN - 1);
// calculate number of bytes needed to be read
let bytes_to_read = end + 1 - start;
// allocate a buf with a suitable capacity
let mut buf = Vec::with_capacity(bytes_to_read as usize);
// seek the file to the starting byte
asset.seek(SeekFrom::Start(start)).await?;
// read the needed bytes
asset.take(bytes_to_read).read_to_end(&mut buf).await?;
resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}"));
resp = resp.header(CONTENT_LENGTH, end + 1 - start);
resp = resp.status(StatusCode::PARTIAL_CONTENT);
resp.body(buf)
} else {
let mut buf = Vec::new();
let ranges = ranges
.iter()
.filter_map(|&(start, mut end)| {
// filter out unsatisfiable ranges
//
// this should be already taken care of by HttpRange::parse
// but checking here again for extra assurance
if start >= len || end >= len || end < start {
None
} else {
// adjust end byte for MAX_LEN
end = start + (end - start).min(len - start).min(MAX_LEN - 1);
Some((start, end))
}
})
.collect::<Vec<_>>();
let boundary = format!("{:x}", rand::random::<u64>());
let boundary_sep = format!("\r\n--{boundary}\r\n");
let boundary_closer = format!("\r\n--{boundary}\r\n");
resp = resp.header(
CONTENT_TYPE,
format!("multipart/byteranges; boundary={boundary}"),
);
for (end, start) in ranges {
// a new range is being written, write the range boundary
buf.write_all(boundary_sep.as_bytes()).await?;
// write the needed headers `Content-Type` and `Content-Range`
buf.write_all(format!("{CONTENT_TYPE}: video/mp4\r\n").as_bytes())
.await?;
buf.write_all(format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes())
.await?;
// write the separator to indicate the start of the range body
buf.write_all("\r\n".as_bytes()).await?;
// calculate number of bytes needed to be read
let bytes_to_read = end + 1 - start;
let mut local_buf = vec![0_u8; bytes_to_read as usize];
asset.seek(SeekFrom::Start(start)).await?;
asset.read_exact(&mut local_buf).await?;
buf.extend_from_slice(&local_buf);
}
// all ranges have been written, write the closing boundary
buf.write_all(boundary_closer.as_bytes()).await?;
resp.body(buf)
}
} else {
resp = resp.header(CONTENT_LENGTH, len);
let mut buf = Vec::with_capacity(len as usize);
asset.read_to_end(&mut buf).await?;
resp.body(buf)
};
http_response.map_err(Into::into)
}
+1 -1
View File
@@ -1,7 +1,7 @@
use dioxus::prelude::*;
use dioxus_desktop::tao::event::Event as WryEvent;
use dioxus_desktop::tao::event::WindowEvent;
use dioxus_desktop::use_wry_event_handler;
use dioxus_desktop::wry::application::event::Event as WryEvent;
use dioxus_desktop::{Config, WindowCloseBehaviour};
fn main() {
+2 -4
View File
@@ -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! {
@@ -14,9 +12,9 @@ fn app(cx: Scope) -> Element {
r#type: "number",
value: "{level}",
oninput: |e| {
if let Ok(new_zoom) = e.value.parse::<f64>() {
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);
}
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ fn app(cx: Scope) -> Element {
input {
value: "{contents}",
r#type: "text",
oninput: move |e| contents.set(e.value.clone()),
oninput: move |e| contents.set(e.value()),
}
}
})
Generated
+247
View File
@@ -0,0 +1,247 @@
{
"nodes": {
"crane": {
"inputs": {
"flake-compat": "flake-compat",
"flake-utils": "flake-utils",
"nixpkgs": [
"nixpkgs"
],
"rust-overlay": "rust-overlay"
},
"locked": {
"lastModified": 1696384830,
"narHash": "sha256-j8ZsVqzmj5sOm5MW9cqwQJUZELFFwOislDmqDDEMl6k=",
"owner": "ipetkov",
"repo": "crane",
"rev": "f2143cd27f8bd09ee4f0121336c65015a2a0a19c",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696267196,
"narHash": "sha256-AAQ/2sD+0D18bb8hKuEEVpHUYD1GmO2Uh/taFamn6XQ=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "4f910c9827911b1ec2bf26b5a062cd09f8d89f85",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1696343447,
"narHash": "sha256-B2xAZKLkkeRFG5XcHHSXXcP7To9Xzr59KXeZiRf4vdQ=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "c9afaba3dfa4085dbd2ccb38dfade5141e33d9d4",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1697009197,
"narHash": "sha256-viVRhBTFT8fPJTb1N3brQIpFZnttmwo3JVKNuWRVc3s=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "01441e14af5e29c9d27ace398e6dd0b293e25a54",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"dir": "lib",
"lastModified": 1696019113,
"narHash": "sha256-X3+DKYWJm93DRSdC5M6K5hLqzSya9BjibtBsuARoPco=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "f5892ddac112a1e9b3612c39af1b72987ee5783a",
"type": "github"
},
"original": {
"dir": "lib",
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1681358109,
"narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"crane": "crane",
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay_2",
"systems": "systems_3"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": [
"crane",
"flake-utils"
],
"nixpkgs": [
"crane",
"nixpkgs"
]
},
"locked": {
"lastModified": 1696299134,
"narHash": "sha256-RS77cAa0N+Sfj5EmKbm5IdncNXaBCE1BSSQvUE8exvo=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "611ccdceed92b4d94ae75328148d84ee4a5b462d",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"rust-overlay_2": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1697076655,
"narHash": "sha256-NcCtVUOd0X81srZkrdP8qoA1BMsPdO2tGtlZpsGijeU=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "aa7584f5bbf5947716ad8ec14eccc0334f0d28f0",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_3": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
+63
View File
@@ -0,0 +1,63 @@
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
systems.url = "github:nix-systems/default";
rust-overlay.url = "github:oxalica/rust-overlay";
crane.url = "github:ipetkov/crane";
crane.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = inputs:
inputs.flake-parts.lib.mkFlake { inherit inputs; } {
systems = import inputs.systems;
perSystem = { config, self', pkgs, lib, system, ... }:
let
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
extensions = [
"rust-src"
"rust-analyzer"
"clippy"
];
};
rustBuildInputs = [
pkgs.openssl
pkgs.libiconv
pkgs.pkg-config
] ++ lib.optionals pkgs.stdenv.isDarwin (with pkgs.darwin.apple_sdk.frameworks; [
IOKit
Carbon
WebKit
Security
Cocoa
]);
# This is useful when building crates as packages
# Note that it does require a `Cargo.lock` which this repo does not have
# craneLib = (inputs.crane.mkLib pkgs).overrideToolchain rustToolchain;
in
{
_module.args.pkgs = import inputs.nixpkgs {
inherit system;
overlays = [
inputs.rust-overlay.overlays.default
];
};
devShells.default = pkgs.mkShell {
name = "dioxus-dev";
buildInputs = rustBuildInputs;
nativeBuildInputs = [
# Add shell dependencies here
rustToolchain
];
shellHook = ''
# For rust-analyzer 'hover' tooltips to work.
export RUST_SRC_PATH="${rustToolchain}/lib/rustlib/src/rust/library";
'';
};
};
};
}
+6 -5
View File
@@ -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(())
}
+63 -33
View File
@@ -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;
@@ -165,7 +166,7 @@ impl Writer<'_> {
fn write_attributes(
&mut self,
attributes: &[ElementAttrNamed],
attributes: &[AttributeType],
key: &Option<IfmtInput>,
sameline: bool,
) -> Result {
@@ -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.write_comments(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,28 @@ impl Writer<'_> {
Ok(())
}
fn write_attribute(&mut self, attr: &AttributeType) -> Result {
match attr {
AttributeType::Named(attr) => self.write_named_attribute(attr),
AttributeType::Spread(attr) => self.write_spread_attribute(attr),
}
}
fn write_named_attribute(&mut self, attr: &ElementAttrNamed) -> Result {
self.write_attribute_name(&attr.attr.name)?;
write!(self.out, ": ")?;
self.write_attribute_value(&attr.attr.value)?;
Ok(())
}
fn write_spread_attribute(&mut self, attr: &Expr) -> Result {
write!(self.out, "..")?;
write!(self.out, "{}", prettyplease::unparse_expr(attr))?;
Ok(())
}
// make sure the comments are actually relevant to this element.
// 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 +428,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, " ")?
}
+3 -3
View File
@@ -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
View File
@@ -0,0 +1,108 @@
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum IndentType {
Spaces,
Tabs,
}
#[derive(Debug, Clone)]
pub struct IndentOptions {
width: usize,
indent_string: String,
}
impl IndentOptions {
pub fn new(typ: IndentType, width: usize) -> Self {
assert_ne!(width, 0, "Cannot have an indent width of 0");
Self {
width,
indent_string: match typ {
IndentType::Tabs => "\t".into(),
IndentType::Spaces => " ".repeat(width),
},
}
}
/// Gets a string containing one indent worth of whitespace
pub fn indent_str(&self) -> &str {
&self.indent_string
}
/// Computes the line length in characters, counting tabs as the indent width.
pub fn line_length(&self, line: &str) -> usize {
line.chars()
.map(|ch| if ch == '\t' { self.width } else { 1 })
.sum()
}
/// Estimates how many times the line has been indented.
pub fn count_indents(&self, mut line: &str) -> usize {
let mut indent = 0;
while !line.is_empty() {
// Try to count tabs
let num_tabs = line.chars().take_while(|ch| *ch == '\t').count();
if num_tabs > 0 {
indent += num_tabs;
line = &line[num_tabs..];
continue;
}
// Try to count spaces
let num_spaces = line.chars().take_while(|ch| *ch == ' ').count();
if num_spaces >= self.width {
// Intentionally floor here to take only the amount of space that matches an indent
let num_space_indents = num_spaces / self.width;
indent += num_space_indents;
line = &line[num_space_indents * self.width..];
continue;
}
// Line starts with either non-indent characters or an unevent amount of spaces,
// so no more indent remains.
break;
}
indent
}
}
impl Default for IndentOptions {
fn default() -> Self {
Self::new(IndentType::Spaces, 4)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn count_indents() {
assert_eq!(
IndentOptions::new(IndentType::Spaces, 4).count_indents("no indentation here!"),
0
);
assert_eq!(
IndentOptions::new(IndentType::Spaces, 4).count_indents(" v += 2"),
1
);
assert_eq!(
IndentOptions::new(IndentType::Spaces, 4).count_indents(" v += 2"),
2
);
assert_eq!(
IndentOptions::new(IndentType::Spaces, 4).count_indents(" v += 2"),
2
);
assert_eq!(
IndentOptions::new(IndentType::Spaces, 4).count_indents("\t\tv += 2"),
2
);
assert_eq!(
IndentOptions::new(IndentType::Spaces, 4).count_indents("\t\t v += 2"),
2
);
assert_eq!(
IndentOptions::new(IndentType::Spaces, 2).count_indents(" v += 2"),
2
);
}
}
+12 -15
View File
@@ -16,8 +16,11 @@ mod collect_macros;
mod component;
mod 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()
}
+56 -38
View File
@@ -1,4 +1,4 @@
use dioxus_rsx::{BodyNode, ElementAttr, ElementAttrNamed, ForLoop};
use dioxus_rsx::{AttributeType, BodyNode, 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,12 +132,45 @@ impl<'a> Writer<'a> {
Ok(())
}
pub(crate) fn is_short_attrs(&mut self, attributes: &[ElementAttrNamed]) -> usize {
pub(crate) fn attr_value_len(&mut self, value: &ElementAttrValue) -> usize {
match value {
ElementAttrValue::AttrOptionalExpr { condition, value } => {
let condition_len = self.retrieve_formatted_expr(condition).len();
let value_len = self.attr_value_len(value);
condition_len + value_len + 6
}
ElementAttrValue::AttrLiteral(lit) => ifmt_to_string(lit).len(),
ElementAttrValue::AttrExpr(expr) => expr.span().line_length(),
ElementAttrValue::EventTokens(tokens) => {
let location = Location::new(tokens.span().start());
let len = if let std::collections::hash_map::Entry::Vacant(e) =
self.cached_formats.entry(location)
{
let formatted = prettyplease::unparse_expr(tokens);
let len = if formatted.contains('\n') {
10000
} else {
formatted.len()
};
e.insert(formatted);
len
} else {
self.cached_formats[&location].len()
};
len
}
}
}
pub(crate) fn is_short_attrs(&mut self, attributes: &[AttributeType]) -> 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() {
if self.current_span_is_primary(attr.start()) {
'line: for line in self.src[..attr.start().start().line - 1].iter().rev() {
match (line.trim().starts_with("//"), line.is_empty()) {
(true, _) => return 100000,
(_, true) => continue 'line,
@@ -146,40 +179,25 @@ impl<'a> Writer<'a> {
}
}
total += match &attr.attr {
ElementAttr::AttrText { value, name } => {
ifmt_to_string(value).len() + name.span().line_length() + 6
}
ElementAttr::AttrExpression { name, value } => {
value.span().line_length() + name.span().line_length() + 6
}
ElementAttr::CustomAttrText { value, name } => {
ifmt_to_string(value).len() + name.to_token_stream().to_string().len() + 6
}
ElementAttr::CustomAttrExpression { name, value } => {
name.to_token_stream().to_string().len() + value.span().line_length() + 6
}
ElementAttr::EventTokens { tokens, name } => {
let location = Location::new(tokens.span().start());
let len = if let std::collections::hash_map::Entry::Vacant(e) =
self.cached_formats.entry(location)
{
let formatted = prettyplease::unparse_expr(tokens);
let len = if formatted.contains('\n') {
10000
} else {
formatted.len()
};
e.insert(formatted);
len
} else {
self.cached_formats[&location].len()
match attr {
AttributeType::Named(attr) => {
let name_len = match &attr.attr.name {
dioxus_rsx::ElementAttrName::BuiltIn(name) => {
let name = name.to_string();
name.len()
}
dioxus_rsx::ElementAttrName::Custom(name) => name.value().len() + 2,
};
len + name.span().line_length() + 6
total += name_len;
total += self.attr_value_len(&attr.attr.value);
}
AttributeType::Spread(expr) => {
let expr_len = self.retrieve_formatted_expr(expr).len();
total += expr_len + 3;
}
};
total += 6;
}
total
@@ -218,7 +236,7 @@ impl<'a> Writer<'a> {
}
}
trait SpanLength {
pub(crate) trait SpanLength {
fn line_length(&self) -> usize;
}
impl SpanLength for Span {
+1 -1
View File
@@ -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", "");
+1 -1
View File
@@ -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 {
+10 -5
View File
@@ -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)));
@@ -0,0 +1,7 @@
rsx! {
div {
// Comments
class: "asdasd",
"hello world"
}
}
@@ -0,0 +1,5 @@
rsx! {
div {
// Comments
class: "asdasd", "hello world" }
}
@@ -0,0 +1,3 @@
fn app(cx: Scope) -> Element {
cx.render(rsx! { div { "hello world" } })
}
@@ -0,0 +1,5 @@
fn app(cx: Scope) -> Element {
cx.render(rsx! {
div {"hello world" }
})
}
@@ -0,0 +1,8 @@
fn ItWroks() {
cx.render(rsx! {
div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light",
left,
right
}
})
}
@@ -0,0 +1,5 @@
fn ItWroks() {
cx.render(rsx! {
div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right }
})
}
+7 -4
View File
@@ -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"
@@ -30,7 +30,7 @@ cargo_metadata = "0.15.0"
tokio = { version = "1.16.1", features = ["fs", "sync", "rt", "macros"] }
atty = "0.2.14"
chrono = "0.4.19"
anyhow = "1.0.53"
anyhow = "1"
hyper = "0.14.17"
hyper-rustls = "0.23.2"
indicatif = "0.17.5"
@@ -72,8 +72,10 @@ cargo-generate = "0.18"
toml_edit = "0.19.11"
# bundling
tauri-bundler = { version = "=1.3.0", features = ["native-tls-vendored"] }
tauri-utils = "=1.4.*"
tauri-bundler = { version = "=1.4.*", features = ["native-tls-vendored"] }
tauri-utils = "=1.5.*"
manganis-cli-support= { git = "https://github.com/DioxusLabs/collect-assets", features = ["webp", "html"] }
dioxus-autofmt = { workspace = true }
dioxus-check = { workspace = true }
@@ -83,6 +85,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 = []
+5 -5
View File
@@ -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
```
+2 -2
View File
@@ -1,5 +1,5 @@
// Dioxus-CLI
// https://github.com/DioxusLabs/cli
// https://github.com/DioxusLabs/dioxus/tree/master/packages/cli
(function () {
var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
@@ -22,4 +22,4 @@
}
};
ws.onclose = reload_upon_connect;
})()
})()
+1 -1
View File
@@ -23,7 +23,7 @@ title = "Dioxus | An elegant GUI library for Rust"
index_on_404 = true
watch_path = ["src"]
watch_path = ["src", "examples"]
# include `assets` in web platform
[web.resource]
+126 -27
View File
@@ -2,31 +2,35 @@ use crate::{
config::{CrateConfig, ExecutableType},
error::{Error, Result},
tools::Tool,
DioxusConfig,
};
use cargo_metadata::{diagnostic::Diagnostic, Message};
use indicatif::{ProgressBar, ProgressStyle};
use lazy_static::lazy_static;
use manganis_cli_support::AssetManifestExt;
use serde::Serialize;
use std::{
fs::{copy, create_dir_all, File},
io::Read,
io::{Read, Write},
panic,
path::PathBuf,
time::Duration,
};
use wasm_bindgen_cli_support::Bindgen;
lazy_static! {
static ref PROGRESS_BARS: indicatif::MultiProgress = indicatif::MultiProgress::new();
}
#[derive(Serialize, Debug, Clone)]
pub struct BuildResult {
pub warnings: Vec<Diagnostic>,
pub elapsed_time: u128,
}
#[allow(unused)]
pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
pub fn build(config: &CrateConfig, _: bool, skip_assets: bool) -> Result<BuildResult> {
// [1] Build the project with cargo, generating a wasm32-unknown-unknown target (is there a more specific, better target to leverage?)
// [2] Generate the appropriate build folders
// [3] Wasm-bindgen the .wasm fiile, and move it into the {builddir}/modules/xxxx/xxxx_bg.wasm
// [3] Wasm-bindgen the .wasm file, and move it into the {builddir}/modules/xxxx/xxxx_bg.wasm
// [4] Wasm-opt the .wasm file with whatever optimizations need to be done
// [5][OPTIONAL] Builds the Tailwind CSS file using the Tailwind standalone binary
// [6] Link up the html page to the wasm module
@@ -41,6 +45,8 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
..
} = config;
let _gaurd = WebAssetConfigDropGuard::new();
// start to build the assets
let ignore_files = build_assets(config)?;
@@ -48,14 +54,25 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
// [1] Build the .wasm module
log::info!("🚅 Running build command...");
let cmd = subprocess::Exec::cmd("cargo");
let cmd = cmd
let wasm_check_command = std::process::Command::new("rustup")
.args(["show"])
.output()?;
let wasm_check_output = String::from_utf8(wasm_check_command.stdout).unwrap();
if !wasm_check_output.contains("wasm32-unknown-unknown") {
log::info!("wasm32-unknown-unknown target not detected, installing..");
let _ = std::process::Command::new("rustup")
.args(["target", "add", "wasm32-unknown-unknown"])
.output()?;
}
let cmd = subprocess::Exec::cmd("cargo")
.env("CARGO_TARGET_DIR", target_dir)
.cwd(crate_dir)
.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 +82,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 +99,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),
@@ -239,19 +258,28 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
}
}
if !skip_assets {
process_assets(config)?;
}
Ok(BuildResult {
warnings: warning_messages,
elapsed_time: t_start.elapsed().as_millis(),
})
}
pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResult> {
pub fn build_desktop(
config: &CrateConfig,
_is_serve: bool,
skip_assets: bool,
) -> Result<BuildResult> {
log::info!("🚅 Running build [Desktop] command...");
let t_start = std::time::Instant::now();
let ignore_files = build_assets(config)?;
let mut cmd = subprocess::Exec::cmd("cargo")
.env("CARGO_TARGET_DIR", &config.target_dir)
.cwd(&config.crate_dir)
.arg("build")
.arg("--message-format=json");
@@ -261,6 +289,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() {
@@ -273,6 +303,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),
@@ -290,12 +328,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)
@@ -347,6 +390,13 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
}
}
if !skip_assets {
// Collect assets
process_assets(config)?;
// Create the __assets_head.html file for bundling
create_assets_head(config)?;
}
log::info!(
"🚩 Build completed: [./{}]",
config
@@ -366,11 +416,19 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
})
}
fn create_assets_head(config: &CrateConfig) -> Result<()> {
let manifest = config.asset_manifest();
let mut file = File::create(config.out_dir.join("__assets_head.html"))?;
file.write_all(manifest.head().as_bytes())?;
Ok(())
}
fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result<Vec<Diagnostic>> {
let mut warning_messages: Vec<Diagnostic> = vec![];
let pb = ProgressBar::new_spinner();
let mut pb = ProgressBar::new_spinner();
pb.enable_steady_tick(Duration::from_millis(200));
pb = PROGRESS_BARS.add(pb);
pb.set_style(
ProgressStyle::with_template("{spinner:.dim.bold} {wide_msg}")
.unwrap()
@@ -378,14 +436,6 @@ fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result<Vec<Diagnostic>> {
);
pb.set_message("💼 Waiting to start build the project...");
struct StopSpinOnDrop(ProgressBar);
impl Drop for StopSpinOnDrop {
fn drop(&mut self) {
self.0.finish_and_clear();
}
}
let stdout = cmd.detached().stream_stdout()?;
let reader = std::io::BufReader::new(stdout);
@@ -421,13 +471,17 @@ fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result<Vec<Diagnostic>> {
std::process::exit(1);
}
}
_ => (), // Unknown message
_ => {
// Unknown message
}
}
}
Ok(warning_messages)
}
pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
pub fn gen_page(config: &CrateConfig, serve: bool, skip_assets: bool) -> String {
let _gaurd = WebAssetConfigDropGuard::new();
let crate_root = crate::cargo::crate_root().unwrap();
let custom_html_file = crate_root.join("index.html");
let mut html = if custom_html_file.is_file() {
@@ -442,7 +496,7 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
String::from(include_str!("./assets/index.html"))
};
let resources = config.web.resource.clone();
let resources = config.dioxus_config.web.resource.clone();
let mut style_list = resources.style.unwrap_or_default();
let mut script_list = resources.script.unwrap_or_default();
@@ -462,13 +516,18 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
))
}
if config
.dioxus_config
.application
.tools
.clone()
.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");
}
if !skip_assets {
let manifest = config.asset_manifest();
style_str.push_str(&manifest.head());
}
replace_or_insert_before("{style_include}", &style_str, "</head", &mut html);
@@ -490,11 +549,11 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
);
}
let base_path = match &config.web.app.base_path {
let base_path = match &config.dioxus_config.web.app.base_path {
Some(path) => path,
None => ".",
};
let app_name = &config.application.name;
let app_name = &config.dioxus_config.application.name;
// Check if a script already exists
if html.contains("{app_name}") && html.contains("{base_path}") {
html = html.replace("{app_name}", app_name);
@@ -519,6 +578,7 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
}
let title = config
.dioxus_config
.web
.app
.title
@@ -688,3 +748,42 @@ fn build_assets(config: &CrateConfig) -> Result<Vec<PathBuf>> {
Ok(result)
}
/// Process any assets collected from the binary
fn process_assets(config: &CrateConfig) -> anyhow::Result<()> {
let manifest = config.asset_manifest();
let static_asset_output_dir = PathBuf::from(
config
.dioxus_config
.web
.app
.base_path
.clone()
.unwrap_or_default(),
);
let static_asset_output_dir = config.out_dir.join(static_asset_output_dir);
manifest.copy_static_assets_to(static_asset_output_dir)?;
Ok(())
}
pub(crate) struct WebAssetConfigDropGuard;
impl WebAssetConfigDropGuard {
pub fn new() -> Self {
// Set up the collect asset config
manganis_cli_support::Config::default()
.with_assets_serve_location("/")
.save();
Self {}
}
}
impl Drop for WebAssetConfigDropGuard {
fn drop(&mut self) {
// Reset the config
manganis_cli_support::Config::default().save();
}
}
+147 -76
View File
@@ -1,3 +1,4 @@
use dioxus_autofmt::{IndentOptions, IndentType};
use futures::{stream::FuturesUnordered, StreamExt};
use 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,43 +49,90 @@ impl Autoformat {
}
// Format single file
if let Some(file) = self.file {
let file_content = if file == "-" {
let mut contents = String::new();
std::io::stdin().read_to_string(&mut contents)?;
Ok(contents)
} else {
fs::read_to_string(&file)
};
match file_content {
Ok(s) => {
let edits = dioxus_autofmt::fmt_file(&s);
let out = dioxus_autofmt::apply_formats(&s, edits);
if file == "-" {
print!("{}", out);
} else {
match fs::write(&file, out) {
Ok(_) => {
println!("formatted {}", file);
}
Err(e) => {
eprintln!("failed to write formatted content to file: {}", e);
}
}
}
}
Err(e) => {
eprintln!("failed to open file: {}", e);
exit(1);
}
}
if let Some(file) = file {
refactor_file(file)?;
}
Ok(())
}
}
fn refactor_file(file: String) -> Result<(), Error> {
let indent = indentation_for(".")?;
let file_content = if file == "-" {
let mut contents = String::new();
std::io::stdin().read_to_string(&mut contents)?;
Ok(contents)
} else {
fs::read_to_string(&file)
};
let Ok(s) = file_content else {
eprintln!("failed to open file: {}", file_content.unwrap_err());
exit(1);
};
let edits = dioxus_autofmt::fmt_file(&s, indent);
let out = dioxus_autofmt::apply_formats(&s, edits);
if file == "-" {
print!("{}", out);
} else if let Err(e) = fs::write(&file, out) {
eprintln!("failed to write formatted content to file: {e}",);
} else {
println!("formatted {}", file);
}
Ok(())
}
fn get_project_files(config: &CrateConfig) -> Vec<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
///
/// Runs using Tokio for multithreading, so it should be really really fast
@@ -90,42 +141,27 @@ impl Autoformat {
async fn autoformat_project(check: bool) -> Result<()> {
let crate_config = crate::CrateConfig::new(None)?;
let mut files_to_format = vec![];
collect_rs_files(&crate_config.crate_dir, &mut files_to_format);
let files_to_format = get_project_files(&crate_config);
if files_to_format.is_empty() {
return Ok(());
}
let indent = indentation_for(&files_to_format[0])?;
let counts = files_to_format
.into_iter()
.filter(|file| {
if file.components().any(|f| f.as_os_str() == "target") {
return false;
}
true
})
.map(|path| async {
let _path = path.clone();
let res = tokio::spawn(async move {
let contents = tokio::fs::read_to_string(&path).await?;
let edits = dioxus_autofmt::fmt_file(&contents);
let len = edits.len();
if !edits.is_empty() {
let out = dioxus_autofmt::apply_formats(&contents, edits);
tokio::fs::write(&path, out).await?;
}
Ok(len) as Result<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);
}
}
+54 -12
View File
@@ -1,6 +1,8 @@
use crate::cfg::Platform;
#[cfg(feature = "plugin")]
use crate::plugin::PluginManager;
use crate::server::fullstack::FullstackServerEnvGuard;
use crate::server::fullstack::FullstackWebEnvGuard;
use crate::{cfg::Platform, WebAssetConfigDropGuard};
use super::*;
@@ -13,23 +15,26 @@ pub struct Build {
}
impl Build {
pub fn build(self, bin: Option<PathBuf>) -> Result<()> {
pub fn build(self, bin: Option<PathBuf>, target_dir: Option<&std::path::Path>) -> Result<()> {
let mut crate_config = crate::CrateConfig::new(bin)?;
if let Some(target_dir) = target_dir {
crate_config.target_dir = target_dir.to_path_buf();
}
// change the release state.
crate_config.with_release(self.build.release);
crate_config.with_verbose(self.build.verbose);
if self.build.example.is_some() {
crate_config.as_example(self.build.example.unwrap());
crate_config.as_example(self.build.example.clone().unwrap());
}
if self.build.profile.is_some() {
crate_config.set_profile(self.build.profile.unwrap());
crate_config.set_profile(self.build.profile.clone().unwrap());
}
if self.build.features.is_some() {
crate_config.set_features(self.build.features.unwrap());
crate_config.set_features(self.build.features.clone().unwrap());
}
let platform = self
@@ -37,19 +42,56 @@ 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.clone() {
crate_config.set_target(target);
}
crate_config.set_cargo_args(self.build.cargo_args.clone());
// #[cfg(feature = "plugin")]
// let _ = PluginManager::on_build_start(&crate_config, &platform);
match platform {
Platform::Web => {
crate::builder::build(&crate_config, true)?;
crate::builder::build(&crate_config, false, self.build.skip_assets)?;
}
Platform::Desktop => {
crate::builder::build_desktop(&crate_config, false)?;
crate::builder::build_desktop(&crate_config, false, self.build.skip_assets)?;
}
Platform::Fullstack => {
// Fullstack mode must be built with web configs on the desktop (server) binary as well as the web binary
let _config = WebAssetConfigDropGuard::new();
{
let mut web_config = crate_config.clone();
let _gaurd = FullstackWebEnvGuard::new(&self.build);
let web_feature = self.build.client_feature;
let features = &mut web_config.features;
match features {
Some(features) => {
features.push(web_feature);
}
None => web_config.features = Some(vec![web_feature]),
};
crate::builder::build(&crate_config, false, self.build.skip_assets)?;
}
{
let mut desktop_config = crate_config.clone();
let desktop_feature = self.build.server_feature;
let features = &mut desktop_config.features;
match features {
Some(features) => {
features.push(desktop_feature);
}
None => desktop_config.features = Some(vec![desktop_feature]),
};
let _gaurd =
FullstackServerEnvGuard::new(self.build.force_debug, self.build.release);
crate::builder::build_desktop(&desktop_config, false, self.build.skip_assets)?;
}
}
}
let temp = gen_page(&crate_config.dioxus_config, false);
let temp = gen_page(&crate_config, false, self.build.skip_assets);
let mut file = std::fs::File::create(
crate_config
@@ -66,8 +108,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(())
}
+27 -3
View File
@@ -76,8 +76,14 @@ 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)?;
build_desktop(&crate_config, false, false)?;
// copy the binary to the out dir
let package = crate_config.manifest.package.unwrap();
@@ -128,6 +134,19 @@ impl Bundle {
}
}
// Add all assets from collect assets to the bundle
{
let config = manganis_cli_support::Config::current();
let location = config.assets_serve_location().to_string();
let location = format!("./{}", location);
println!("Adding assets from {} to bundle", location);
if let Some(resources) = &mut bundle_settings.resources {
resources.push(location);
} else {
bundle_settings.resources = Some(vec![location]);
}
}
let mut settings = SettingsBuilder::new()
.project_out_directory(crate_config.out_dir)
.package_settings(PackageSettings {
@@ -148,6 +167,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 +180,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(())
+83 -9
View File
@@ -6,15 +6,16 @@ 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)]
pub release: bool,
/// This flag only applies to fullstack builds. By default fullstack builds will run with something in between debug and release mode. This flag will force the build to run in debug mode. [default: false]
#[clap(long)]
#[serde(default)]
pub force_debug: bool,
// Use verbose output [default: false]
#[clap(long)]
#[serde(default)]
@@ -32,17 +33,53 @@ pub struct ConfigOptsBuild {
#[clap(long, value_enum)]
pub platform: Option<Platform>,
/// Skip collecting assets from dependencies [default: false]
#[clap(long)]
#[serde(default)]
pub skip_assets: bool,
/// Space separated list of features to activate
#[clap(long)]
pub features: Option<Vec<String>>,
/// The feature to use for the client in a fullstack app [default: "web"]
#[clap(long, default_value_t = { "web".to_string() })]
pub client_feature: String,
/// The feature to use for the server in a fullstack app [default: "ssr"]
#[clap(long, default_value_t = { "ssr".to_string() })]
pub server_feature: String,
/// Rustc platform triple
#[clap(long)]
pub target: Option<String>,
/// Extra arguments passed to cargo build
#[clap(last = true)]
pub cargo_args: Vec<String>,
}
impl From<ConfigOptsServe> for ConfigOptsBuild {
fn from(serve: ConfigOptsServe) -> Self {
Self {
target: serve.target,
release: serve.release,
verbose: serve.verbose,
example: serve.example,
profile: serve.profile,
platform: serve.platform,
features: serve.features,
client_feature: serve.client_feature,
server_feature: serve.server_feature,
skip_assets: serve.skip_assets,
force_debug: serve.force_debug,
cargo_args: serve.cargo_args,
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Parser)]
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)]
@@ -62,6 +99,11 @@ pub struct ConfigOptsServe {
#[serde(default)]
pub release: bool,
/// This flag only applies to fullstack builds. By default fullstack builds will run with something in between debug and release mode. This flag will force the build to run in debug mode. [default: false]
#[clap(long)]
#[serde(default)]
pub force_debug: bool,
// Use verbose output [default: false]
#[clap(long)]
#[serde(default)]
@@ -71,7 +113,7 @@ pub struct ConfigOptsServe {
#[clap(long)]
pub profile: Option<String>,
/// Build platform: support Web & Desktop [default: "default_platform"]
/// Build platform: support Web, Desktop, and Fullstack [default: "default_platform"]
#[clap(long, value_enum)]
pub platform: Option<Platform>,
@@ -89,6 +131,27 @@ pub struct ConfigOptsServe {
/// Space separated list of features to activate
#[clap(long)]
pub features: Option<Vec<String>>,
/// Skip collecting assets from dependencies [default: false]
#[clap(long)]
#[serde(default)]
pub skip_assets: bool,
/// The feature to use for the client in a fullstack app [default: "web"]
#[clap(long, default_value_t = { "web".to_string() })]
pub client_feature: String,
/// The feature to use for the server in a fullstack app [default: "ssr"]
#[clap(long, default_value_t = { "ssr".to_string() })]
pub server_feature: String,
/// Rustc platform triple
#[clap(long)]
pub target: Option<String>,
/// Extra arguments passed to cargo build
#[clap(last = true)]
pub cargo_args: Vec<String>,
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize, Debug)]
@@ -99,6 +162,9 @@ pub enum Platform {
#[clap(name = "desktop")]
#[serde(rename = "desktop")]
Desktop,
#[clap(name = "fullstack")]
#[serde(rename = "fullstack")]
Fullstack,
}
/// Config options for the bundling system.
@@ -129,4 +195,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>,
}
+6
View File
@@ -28,6 +28,12 @@ impl Clean {
remove_dir_all(crate_config.crate_dir.join(&out_dir))?;
}
let fullstack_out_dir = crate_config.crate_dir.join(".dioxus");
if fullstack_out_dir.is_dir() {
remove_dir_all(fullstack_out_dir)?;
}
Ok(())
}
}
Loaded 100 of 347 files, more files were not shown because too many files have changed in this diff. Show more