Merge branch 'master' into maybe-sync-signal

This commit is contained in:
ealmloff 2024-01-08 16:27:11 -06:00 committed by GitHub
commit c7e104c0f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
127 changed files with 4604 additions and 2940 deletions

View file

@ -36,9 +36,14 @@ jobs:
if: github.event.pull_request.draft == false
name: Check
runs-on: ubuntu-latest
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
steps:
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- 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
@ -49,12 +54,17 @@ jobs:
if: github.event.pull_request.draft == false
name: Test Suite
runs-on: ubuntu-latest
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
steps:
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- 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
@ -65,9 +75,14 @@ jobs:
if: github.event.pull_request.draft == false
name: Rustfmt
runs-on: ubuntu-latest
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
steps:
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- uses: mozilla-actions/sccache-action@v0.0.3
- uses: ilammy/setup-nasm@v1
- run: rustup component add rustfmt
- uses: actions/checkout@v4
@ -77,9 +92,14 @@ jobs:
if: github.event.pull_request.draft == false
name: Clippy
runs-on: ubuntu-latest
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
steps:
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- 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
@ -90,6 +110,10 @@ jobs:
matrix_test:
runs-on: ${{ matrix.platform.os }}
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
RUST_CARGO_COMMAND: ${{ matrix.platform.cross == true && 'cross' || 'cargo' }}
strategy:
matrix:
@ -140,7 +164,7 @@ jobs:
if: ${{ matrix.platform.cross == true }}
uses: taiki-e/install-action@cross
- uses: Swatinem/rust-cache@v2
- uses: mozilla-actions/sccache-action@v0.0.3
with:
workspaces: core -> ../target
save-if: ${{ matrix.features.key == 'all' }}

View file

@ -4,11 +4,13 @@ members = [
"packages/dioxus",
"packages/core",
"packages/cli",
"packages/cli-config",
"packages/core-macro",
"packages/router-macro",
"packages/extension",
"packages/router",
"packages/html",
"packages/html-internal-macro",
"packages/hooks",
"packages/web",
"packages/ssr",
@ -60,6 +62,7 @@ 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", 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" }
@ -77,6 +80,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" }
dioxus-cli-config = { path = "packages/cli-config", version = "0.4.1" }
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" }
@ -129,7 +133,6 @@ serde_json = "1.0.79"
rand = { version = "0.8.4", features = ["small_rng"] }
tokio = { version = "1.16.1", features = ["full"] }
reqwest = { version = "0.11.9", features = ["json"] }
fern = { version = "0.6.0", features = ["colored"] }
env_logger = "0.10.0"
simple_logger = "4.0.0"
thiserror = { workspace = true }

View file

@ -105,6 +105,8 @@ args = [
"dioxus-router",
"--exclude",
"dioxus-desktop",
"--exclude",
"dioxus-mobile",
]
private = true

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

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);

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}");
}

View file

@ -1,28 +1,26 @@
use dioxus::prelude::*;
use dioxus_desktop::wry::http::Response;
use dioxus_desktop::{use_asset_handler, AssetRequest};
use std::path::Path;
use dioxus_desktop::{use_asset_handler, wry::http::Response};
fn main() {
dioxus_desktop::launch(app);
}
fn app(cx: Scope) -> Element {
use_asset_handler(cx, |request: &AssetRequest| {
let path = request.path().to_path_buf();
async move {
if path != Path::new("logo.png") {
return None;
}
let image_data: &[u8] = include_bytes!("./assets/logo.png");
Some(Response::new(image_data.into()))
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: "logo.png"
src: "/logos/logo.png"
}
}
})

View file

@ -1,4 +1,5 @@
#![allow(non_snake_case)]
use dioxus::html::HasFileData;
use dioxus::prelude::*;
use tokio::time::sleep;
@ -39,9 +40,30 @@ fn App(cx: Scope) -> Element {
}
}
},
},
div { "progress: {files_uploaded.read().len()}" },
}
div {
width: "100px",
height: "100px",
border: "1px solid black",
prevent_default: "ondrop dragover dragenter",
ondrop: move |evt| {
to_owned![files_uploaded];
async move {
if let Some(file_engine) = &evt.files() {
let files = file_engine.files();
for file_name in &files {
if let Some(file) = file_engine.read_file_to_string(file_name).await{
files_uploaded.write().push(file);
}
}
}
}
},
ondragover: move |event: DragEvent| {
event.stop_propagation();
},
"Drop files here"
}
ul {
for file in files_uploaded.read().iter() {

View file

@ -35,7 +35,7 @@ frameworks = ["WebKit"]
[dependencies]
anyhow = "1.0.56"
log = "0.4.11"
wry = "0.34.0"
wry = "0.35.0"
dioxus = { path = "../../packages/dioxus" }
dioxus-desktop = { path = "../../packages/desktop", features = [
"tokio_runtime",

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() {

36
examples/spread.rs Normal file
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,
}

33
examples/streams.rs Normal file
View file

@ -0,0 +1,33 @@
use dioxus::prelude::*;
use dioxus_signals::use_signal;
use futures_util::{future, stream, Stream, StreamExt};
use std::time::Duration;
fn main() {
dioxus_desktop::launch(app);
}
fn app(cx: Scope) -> Element {
let count = use_signal(cx, || 10);
use_future(cx, (), |_| async move {
let mut stream = some_stream();
while let Some(second) = stream.next().await {
count.set(second);
}
});
cx.render(rsx! {
h1 { "High-Five counter: {count}" }
})
}
fn some_stream() -> std::pin::Pin<Box<dyn Stream<Item = i32>>> {
Box::pin(
stream::once(future::ready(0)).chain(stream::iter(1..).then(|second| async move {
tokio::time::sleep(Duration::from_secs(1)).await;
second
})),
)
}

View file

@ -3,7 +3,6 @@ use dioxus_desktop::wry::http;
use dioxus_desktop::wry::http::Response;
use dioxus_desktop::{use_asset_handler, AssetRequest};
use http::{header::*, response::Builder as ResponseBuilder, status::StatusCode};
use std::borrow::Cow;
use std::{io::SeekFrom, path::PathBuf};
use tokio::io::AsyncReadExt;
use tokio::io::AsyncSeekExt;
@ -31,28 +30,33 @@ fn main() {
}
fn app(cx: Scope) -> Element {
use_asset_handler(cx, move |request: &AssetRequest| {
let request = request.clone();
async move {
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();
let response: Option<Response<Cow<'static, [u8]>>> =
match get_stream_response(&mut file, &request).await {
Ok(response) => Some(response.map(Cow::Owned)),
Err(err) => {
eprintln!("Error: {}", err);
None
}
};
response
}
match get_stream_response(&mut file, &request).await {
Ok(response) => responder.respond(response),
Err(err) => eprintln!("Error: {}", err),
}
});
});
render! {
div { video { src: "test_video.mp4", autoplay: true, controls: true, width: 640, height: 480 } }
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,

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() {

247
flake.lock Normal file
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
flake.nix Normal file
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";
'';
};
};
};
}

View file

@ -166,7 +166,7 @@ impl Writer<'_> {
fn write_attributes(
&mut self,
attributes: &[ElementAttrNamed],
attributes: &[AttributeType],
key: &Option<IfmtInput>,
sameline: bool,
) -> Result {
@ -188,7 +188,7 @@ impl Writer<'_> {
while let Some(attr) = attr_iter.next() {
self.out.indent_level += 1;
if !sameline {
self.write_comments(attr.attr.start())?;
self.write_comments(attr.start())?;
}
self.out.indent_level -= 1;
@ -289,7 +289,14 @@ impl Writer<'_> {
Ok(())
}
fn write_attribute(&mut self, attr: &ElementAttrNamed) -> Result {
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)?;
@ -297,6 +304,13 @@ impl Writer<'_> {
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 {

View file

@ -1,4 +1,4 @@
use dioxus_rsx::{BodyNode, ElementAttrNamed, ElementAttrValue, ForLoop};
use dioxus_rsx::{AttributeType, BodyNode, ElementAttrValue, ForLoop};
use proc_macro2::{LineColumn, Span};
use quote::ToTokens;
use std::{
@ -165,12 +165,12 @@ impl<'a> Writer<'a> {
}
}
pub(crate) fn is_short_attrs(&mut self, attributes: &[ElementAttrNamed]) -> usize {
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,
@ -179,16 +179,24 @@ impl<'a> Writer<'a> {
}
}
total += match &attr.attr.name {
dioxus_rsx::ElementAttrName::BuiltIn(name) => {
let name = name.to_string();
name.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,
};
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;
}
dioxus_rsx::ElementAttrName::Custom(name) => name.value().len() + 2,
};
total += self.attr_value_len(&attr.attr.value);
total += 6;
}

4
packages/cli-config/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
/target
Cargo.lock
.DS_Store
.idea/

View file

@ -0,0 +1,26 @@
[package]
name = "dioxus-cli-config"
version = "0.4.1"
authors = ["Jonathan Kelley"]
edition = "2021"
description = "Configuration for the Dioxus CLI"
repository = "https://github.com/DioxusLabs/dioxus/"
license = "MIT OR Apache-2.0"
keywords = ["react", "gui", "cli", "dioxus", "wasm"]
[dependencies]
clap = { version = "4.2", features = ["derive"], optional = true }
serde = { version = "1.0.136", features = ["derive"] }
serde_json = "1.0.79"
toml = { version = "0.5.8", optional = true }
cargo_toml = { version = "0.16.0", optional = true }
once_cell = "1.18.0"
tracing.workspace = true
# bundling
tauri-bundler = { version = "=1.4.0", features = ["native-tls-vendored"], optional = true }
tauri-utils = { version = "=1.5.*", optional = true }
[features]
default = []
cli = ["tauri-bundler", "tauri-utils", "clap", "toml", "cargo_toml"]

View file

@ -0,0 +1,5 @@
<div style="text-align: center">
<h1>📦✨ Dioxus CLI Configuration</h1>
</div>
The **dioxus-cli-config** contains the configuration for the **dioxus-cli**.

View file

@ -0,0 +1,260 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BundleConfig {
pub identifier: Option<String>,
pub publisher: Option<String>,
pub icon: Option<Vec<String>>,
pub resources: Option<Vec<String>>,
pub copyright: Option<String>,
pub category: Option<String>,
pub short_description: Option<String>,
pub long_description: Option<String>,
pub external_bin: Option<Vec<String>>,
pub deb: Option<DebianSettings>,
pub macos: Option<MacOsSettings>,
pub windows: Option<WindowsSettings>,
}
#[cfg(feature = "cli")]
impl From<BundleConfig> for tauri_bundler::BundleSettings {
fn from(val: BundleConfig) -> Self {
tauri_bundler::BundleSettings {
identifier: val.identifier,
publisher: val.publisher,
icon: val.icon,
resources: val.resources,
copyright: val.copyright,
category: val.category.and_then(|c| c.parse().ok()),
short_description: val.short_description,
long_description: val.long_description,
external_bin: val.external_bin,
deb: val.deb.map(Into::into).unwrap_or_default(),
macos: val.macos.map(Into::into).unwrap_or_default(),
windows: val.windows.map(Into::into).unwrap_or_default(),
..Default::default()
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DebianSettings {
pub depends: Option<Vec<String>>,
pub files: HashMap<PathBuf, PathBuf>,
pub nsis: Option<NsisSettings>,
}
#[cfg(feature = "cli")]
impl From<DebianSettings> for tauri_bundler::DebianSettings {
fn from(val: DebianSettings) -> Self {
tauri_bundler::DebianSettings {
depends: val.depends,
files: val.files,
desktop_template: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct WixSettings {
pub language: Vec<(String, Option<PathBuf>)>,
pub template: Option<PathBuf>,
pub fragment_paths: Vec<PathBuf>,
pub component_group_refs: Vec<String>,
pub component_refs: Vec<String>,
pub feature_group_refs: Vec<String>,
pub feature_refs: Vec<String>,
pub merge_refs: Vec<String>,
pub skip_webview_install: bool,
pub license: Option<PathBuf>,
pub enable_elevated_update_task: bool,
pub banner_path: Option<PathBuf>,
pub dialog_image_path: Option<PathBuf>,
pub fips_compliant: bool,
}
#[cfg(feature = "cli")]
impl From<WixSettings> for tauri_bundler::WixSettings {
fn from(val: WixSettings) -> Self {
tauri_bundler::WixSettings {
language: tauri_bundler::bundle::WixLanguage({
let mut languages: Vec<_> = val
.language
.iter()
.map(|l| {
(
l.0.clone(),
tauri_bundler::bundle::WixLanguageConfig {
locale_path: l.1.clone(),
},
)
})
.collect();
if languages.is_empty() {
languages.push(("en-US".into(), Default::default()));
}
languages
}),
template: val.template,
fragment_paths: val.fragment_paths,
component_group_refs: val.component_group_refs,
component_refs: val.component_refs,
feature_group_refs: val.feature_group_refs,
feature_refs: val.feature_refs,
merge_refs: val.merge_refs,
skip_webview_install: val.skip_webview_install,
license: val.license,
enable_elevated_update_task: val.enable_elevated_update_task,
banner_path: val.banner_path,
dialog_image_path: val.dialog_image_path,
fips_compliant: val.fips_compliant,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MacOsSettings {
pub frameworks: Option<Vec<String>>,
pub minimum_system_version: Option<String>,
pub license: Option<String>,
pub exception_domain: Option<String>,
pub signing_identity: Option<String>,
pub provider_short_name: Option<String>,
pub entitlements: Option<String>,
pub info_plist_path: Option<PathBuf>,
}
#[cfg(feature = "cli")]
impl From<MacOsSettings> for tauri_bundler::MacOsSettings {
fn from(val: MacOsSettings) -> Self {
tauri_bundler::MacOsSettings {
frameworks: val.frameworks,
minimum_system_version: val.minimum_system_version,
license: val.license,
exception_domain: val.exception_domain,
signing_identity: val.signing_identity,
provider_short_name: val.provider_short_name,
entitlements: val.entitlements,
info_plist_path: val.info_plist_path,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WindowsSettings {
pub digest_algorithm: Option<String>,
pub certificate_thumbprint: Option<String>,
pub timestamp_url: Option<String>,
pub tsp: bool,
pub wix: Option<WixSettings>,
pub icon_path: Option<PathBuf>,
pub webview_install_mode: WebviewInstallMode,
pub webview_fixed_runtime_path: Option<PathBuf>,
pub allow_downgrades: bool,
pub nsis: Option<NsisSettings>,
}
#[cfg(feature = "cli")]
impl From<WindowsSettings> for tauri_bundler::WindowsSettings {
fn from(val: WindowsSettings) -> Self {
tauri_bundler::WindowsSettings {
digest_algorithm: val.digest_algorithm,
certificate_thumbprint: val.certificate_thumbprint,
timestamp_url: val.timestamp_url,
tsp: val.tsp,
wix: val.wix.map(Into::into),
icon_path: val.icon_path.unwrap_or("icons/icon.ico".into()),
webview_install_mode: val.webview_install_mode.into(),
webview_fixed_runtime_path: val.webview_fixed_runtime_path,
allow_downgrades: val.allow_downgrades,
nsis: val.nsis.map(Into::into),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NsisSettings {
pub template: Option<PathBuf>,
pub license: Option<PathBuf>,
pub header_image: Option<PathBuf>,
pub sidebar_image: Option<PathBuf>,
pub installer_icon: Option<PathBuf>,
pub install_mode: NSISInstallerMode,
pub languages: Option<Vec<String>>,
pub custom_language_files: Option<HashMap<String, PathBuf>>,
pub display_language_selector: bool,
}
#[cfg(feature = "cli")]
impl From<NsisSettings> for tauri_bundler::NsisSettings {
fn from(val: NsisSettings) -> Self {
tauri_bundler::NsisSettings {
license: val.license,
header_image: val.header_image,
sidebar_image: val.sidebar_image,
installer_icon: val.installer_icon,
install_mode: val.install_mode.into(),
languages: val.languages,
display_language_selector: val.display_language_selector,
custom_language_files: None,
template: None,
compression: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum NSISInstallerMode {
CurrentUser,
PerMachine,
Both,
}
#[cfg(feature = "cli")]
impl From<NSISInstallerMode> for tauri_utils::config::NSISInstallerMode {
fn from(val: NSISInstallerMode) -> Self {
match val {
NSISInstallerMode::CurrentUser => tauri_utils::config::NSISInstallerMode::CurrentUser,
NSISInstallerMode::PerMachine => tauri_utils::config::NSISInstallerMode::PerMachine,
NSISInstallerMode::Both => tauri_utils::config::NSISInstallerMode::Both,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum WebviewInstallMode {
Skip,
DownloadBootstrapper { silent: bool },
EmbedBootstrapper { silent: bool },
OfflineInstaller { silent: bool },
FixedRuntime { path: PathBuf },
}
#[cfg(feature = "cli")]
impl WebviewInstallMode {
fn into(self) -> tauri_utils::config::WebviewInstallMode {
match self {
Self::Skip => tauri_utils::config::WebviewInstallMode::Skip,
Self::DownloadBootstrapper { silent } => {
tauri_utils::config::WebviewInstallMode::DownloadBootstrapper { silent }
}
Self::EmbedBootstrapper { silent } => {
tauri_utils::config::WebviewInstallMode::EmbedBootstrapper { silent }
}
Self::OfflineInstaller { silent } => {
tauri_utils::config::WebviewInstallMode::OfflineInstaller { silent }
}
Self::FixedRuntime { path } => {
tauri_utils::config::WebviewInstallMode::FixedRuntime { path }
}
}
}
}
impl Default for WebviewInstallMode {
fn default() -> Self {
Self::OfflineInstaller { silent: false }
}
}

View file

@ -1,12 +1,33 @@
//! Utilities for working with cargo and rust files
use crate::error::{Error, Result};
use std::error::Error;
use std::{
env, fs,
env,
fmt::{Display, Formatter},
fs,
path::{Path, PathBuf},
process::Command,
str,
};
#[derive(Debug, Clone)]
pub struct CargoError {
msg: String,
}
impl CargoError {
pub fn new(msg: String) -> Self {
Self { msg }
}
}
impl Display for CargoError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "CargoError: {}", self.msg)
}
}
impl Error for CargoError {}
/// How many parent folders are searched for a `Cargo.toml`
const MAX_ANCESTORS: u32 = 10;
@ -19,7 +40,7 @@ pub struct Metadata {
/// Returns the root of the crate that the command is run from
///
/// If the command is run from the workspace root, this will return the top-level Cargo.toml
pub fn crate_root() -> Result<PathBuf> {
pub fn crate_root() -> Result<PathBuf, CargoError> {
// From the current directory we work our way up, looking for `Cargo.toml`
env::current_dir()
.ok()
@ -35,7 +56,7 @@ pub fn crate_root() -> Result<PathBuf> {
None
})
.ok_or_else(|| {
Error::CargoError("Failed to find directory containing Cargo.toml".to_string())
CargoError::new("Failed to find directory containing Cargo.toml".to_string())
})
}
@ -53,11 +74,11 @@ fn contains_manifest(path: &Path) -> bool {
impl Metadata {
/// Returns the struct filled from `cargo metadata` output
/// TODO @Jon, find a different way that doesn't rely on the cargo metadata command (it's slow)
pub fn get() -> Result<Self> {
pub fn get() -> Result<Self, CargoError> {
let output = Command::new("cargo")
.args(["metadata"])
.output()
.map_err(|_| Error::CargoError("Manifset".to_string()))?;
.map_err(|_| CargoError::new("Manifset".to_string()))?;
if !output.status.success() {
let mut msg = str::from_utf8(&output.stderr).unwrap().trim();
@ -65,22 +86,22 @@ impl Metadata {
msg = &msg[7..];
}
return Err(Error::CargoError(msg.to_string()));
return Err(CargoError::new(msg.to_string()));
}
let stdout = str::from_utf8(&output.stdout).unwrap();
if let Some(line) = stdout.lines().next() {
let meta: serde_json::Value = serde_json::from_str(line)
.map_err(|_| Error::CargoError("InvalidOutput".to_string()))?;
.map_err(|_| CargoError::new("InvalidOutput".to_string()))?;
let workspace_root = meta["workspace_root"]
.as_str()
.ok_or_else(|| Error::CargoError("InvalidOutput".to_string()))?
.ok_or_else(|| CargoError::new("InvalidOutput".to_string()))?
.into();
let target_directory = meta["target_directory"]
.as_str()
.ok_or_else(|| Error::CargoError("InvalidOutput".to_string()))?
.ok_or_else(|| CargoError::new("InvalidOutput".to_string()))?
.into();
return Ok(Self {
@ -89,6 +110,6 @@ impl Metadata {
});
}
Err(Error::CargoError("InvalidOutput".to_string()))
Err(CargoError::new("InvalidOutput".to_string()))
}
}

View file

@ -0,0 +1,492 @@
use crate::BundleConfig;
use crate::CargoError;
use core::fmt::{Display, Formatter};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Debug)]
#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
pub enum Platform {
#[cfg_attr(feature = "cli", clap(name = "web"))]
#[serde(rename = "web")]
Web,
#[cfg_attr(feature = "cli", clap(name = "desktop"))]
#[serde(rename = "desktop")]
Desktop,
#[cfg_attr(feature = "cli", clap(name = "fullstack"))]
#[serde(rename = "fullstack")]
Fullstack,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DioxusConfig {
pub application: ApplicationConfig,
pub web: WebConfig,
#[serde(default)]
pub bundle: BundleConfig,
#[cfg(feature = "cli")]
#[serde(default = "default_plugin")]
pub plugin: toml::Value,
}
#[cfg(feature = "cli")]
fn default_plugin() -> toml::Value {
toml::Value::Boolean(true)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoadDioxusConfigError {
location: String,
error: String,
}
impl std::fmt::Display for LoadDioxusConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} {}", self.location, self.error)
}
}
impl std::error::Error for LoadDioxusConfigError {}
#[derive(Debug)]
pub enum CrateConfigError {
Cargo(CargoError),
Io(std::io::Error),
#[cfg(feature = "cli")]
Toml(toml::de::Error),
LoadDioxusConfig(LoadDioxusConfigError),
}
impl From<CargoError> for CrateConfigError {
fn from(err: CargoError) -> Self {
Self::Cargo(err)
}
}
impl From<std::io::Error> for CrateConfigError {
fn from(err: std::io::Error) -> Self {
Self::Io(err)
}
}
#[cfg(feature = "cli")]
impl From<toml::de::Error> for CrateConfigError {
fn from(err: toml::de::Error) -> Self {
Self::Toml(err)
}
}
impl From<LoadDioxusConfigError> for CrateConfigError {
fn from(err: LoadDioxusConfigError) -> Self {
Self::LoadDioxusConfig(err)
}
}
impl Display for CrateConfigError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Cargo(err) => write!(f, "{}", err),
Self::Io(err) => write!(f, "{}", err),
#[cfg(feature = "cli")]
Self::Toml(err) => write!(f, "{}", err),
Self::LoadDioxusConfig(err) => write!(f, "{}", err),
}
}
}
impl std::error::Error for CrateConfigError {}
impl DioxusConfig {
#[cfg(feature = "cli")]
/// Load the dioxus config from a path
pub fn load(bin: Option<PathBuf>) -> Result<Option<DioxusConfig>, CrateConfigError> {
let crate_dir = crate::cargo::crate_root();
let crate_dir = match crate_dir {
Ok(dir) => {
if let Some(bin) = bin {
dir.join(bin)
} else {
dir
}
}
Err(_) => return Ok(None),
};
let crate_dir = crate_dir.as_path();
let Some(dioxus_conf_file) = acquire_dioxus_toml(crate_dir) else {
return Ok(None);
};
let dioxus_conf_file = dioxus_conf_file.as_path();
let cfg = toml::from_str::<DioxusConfig>(&std::fs::read_to_string(dioxus_conf_file)?)
.map_err(|err| {
let error_location = dioxus_conf_file
.strip_prefix(crate_dir)
.unwrap_or(dioxus_conf_file)
.display();
CrateConfigError::LoadDioxusConfig(LoadDioxusConfigError {
location: error_location.to_string(),
error: err.to_string(),
})
})
.map(Some);
match cfg {
Ok(Some(mut cfg)) => {
let name = cfg.application.name.clone();
if cfg.bundle.identifier.is_none() {
cfg.bundle.identifier = Some(format!("io.github.{name}"));
}
if cfg.bundle.publisher.is_none() {
cfg.bundle.publisher = Some(name);
}
Ok(Some(cfg))
}
cfg => cfg,
}
}
}
#[cfg(feature = "cli")]
fn acquire_dioxus_toml(dir: &std::path::Path) -> Option<PathBuf> {
// prefer uppercase
let uppercase_conf = dir.join("Dioxus.toml");
if uppercase_conf.is_file() {
return Some(uppercase_conf);
}
// lowercase is fine too
let lowercase_conf = dir.join("dioxus.toml");
if lowercase_conf.is_file() {
return Some(lowercase_conf);
}
None
}
impl Default for DioxusConfig {
fn default() -> Self {
let name = default_name();
Self {
application: ApplicationConfig {
name: name.clone(),
default_platform: default_platform(),
out_dir: out_dir_default(),
asset_dir: asset_dir_default(),
#[cfg(feature = "cli")]
tools: Default::default(),
sub_package: None,
},
web: WebConfig {
app: WebAppConfig {
title: default_title(),
base_path: None,
},
proxy: vec![],
watcher: Default::default(),
resource: WebResourceConfig {
dev: WebDevResourceConfig {
style: vec![],
script: vec![],
},
style: Some(vec![]),
script: Some(vec![]),
},
https: WebHttpsConfig {
enabled: None,
mkcert: None,
key_path: None,
cert_path: None,
},
},
bundle: BundleConfig {
identifier: Some(format!("io.github.{name}")),
publisher: Some(name),
..Default::default()
},
#[cfg(feature = "cli")]
plugin: toml::Value::Table(toml::map::Map::new()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApplicationConfig {
#[serde(default = "default_name")]
pub name: String,
#[serde(default = "default_platform")]
pub default_platform: Platform,
#[serde(default = "out_dir_default")]
pub out_dir: PathBuf,
#[serde(default = "asset_dir_default")]
pub asset_dir: PathBuf,
#[cfg(feature = "cli")]
#[serde(default)]
pub tools: std::collections::HashMap<String, toml::Value>,
#[serde(default)]
pub sub_package: Option<String>,
}
fn default_name() -> String {
"name".into()
}
fn default_platform() -> Platform {
Platform::Web
}
fn asset_dir_default() -> PathBuf {
PathBuf::from("public")
}
fn out_dir_default() -> PathBuf {
PathBuf::from("dist")
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebConfig {
#[serde(default)]
pub app: WebAppConfig,
#[serde(default)]
pub proxy: Vec<WebProxyConfig>,
#[serde(default)]
pub watcher: WebWatcherConfig,
#[serde(default)]
pub resource: WebResourceConfig,
#[serde(default)]
pub https: WebHttpsConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebAppConfig {
#[serde(default = "default_title")]
pub title: String,
pub base_path: Option<String>,
}
impl Default for WebAppConfig {
fn default() -> Self {
Self {
title: default_title(),
base_path: None,
}
}
}
fn default_title() -> String {
"dioxus | ⛺".into()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebProxyConfig {
pub backend: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebWatcherConfig {
#[serde(default = "watch_path_default")]
pub watch_path: Vec<PathBuf>,
#[serde(default)]
pub reload_html: bool,
#[serde(default = "true_bool")]
pub index_on_404: bool,
}
impl Default for WebWatcherConfig {
fn default() -> Self {
Self {
watch_path: watch_path_default(),
reload_html: false,
index_on_404: true,
}
}
}
fn watch_path_default() -> Vec<PathBuf> {
vec![PathBuf::from("src"), PathBuf::from("examples")]
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct WebResourceConfig {
pub dev: WebDevResourceConfig,
pub style: Option<Vec<PathBuf>>,
pub script: Option<Vec<PathBuf>>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct WebDevResourceConfig {
#[serde(default)]
pub style: Vec<PathBuf>,
#[serde(default)]
pub script: Vec<PathBuf>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct WebHttpsConfig {
pub enabled: Option<bool>,
pub mkcert: Option<bool>,
pub key_path: Option<String>,
pub cert_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrateConfig {
pub out_dir: PathBuf,
pub crate_dir: PathBuf,
pub workspace_dir: PathBuf,
pub target_dir: PathBuf,
pub asset_dir: PathBuf,
#[cfg(feature = "cli")]
pub manifest: cargo_toml::Manifest<cargo_toml::Value>,
pub executable: ExecutableType,
pub dioxus_config: DioxusConfig,
pub release: bool,
pub hot_reload: bool,
pub cross_origin_policy: bool,
pub verbose: bool,
pub custom_profile: Option<String>,
pub features: Option<Vec<String>>,
pub target: Option<String>,
pub cargo_args: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ExecutableType {
Binary(String),
Lib(String),
Example(String),
}
impl CrateConfig {
#[cfg(feature = "cli")]
pub fn new(bin: Option<PathBuf>) -> Result<Self, CrateConfigError> {
let dioxus_config = DioxusConfig::load(bin.clone())?.unwrap_or_default();
let crate_root = crate::crate_root()?;
let crate_dir = if let Some(package) = &dioxus_config.application.sub_package {
crate_root.join(package)
} else if let Some(bin) = bin {
crate_root.join(bin)
} else {
crate_root
};
let meta = crate::Metadata::get()?;
let workspace_dir = meta.workspace_root;
let target_dir = meta.target_directory;
let out_dir = crate_dir.join(&dioxus_config.application.out_dir);
let cargo_def = &crate_dir.join("Cargo.toml");
let asset_dir = crate_dir.join(&dioxus_config.application.asset_dir);
let manifest = cargo_toml::Manifest::from_path(cargo_def).unwrap();
let mut output_filename = String::from("dioxus_app");
if let Some(package) = &manifest.package.as_ref() {
output_filename = match &package.default_run {
Some(default_run_target) => default_run_target.to_owned(),
None => manifest
.bin
.iter()
.find(|b| b.name == manifest.package.as_ref().map(|pkg| pkg.name.clone()))
.or(manifest
.bin
.iter()
.find(|b| b.path == Some("src/main.rs".to_owned())))
.or(manifest.bin.first())
.or(manifest.lib.as_ref())
.and_then(|prod| prod.name.clone())
.unwrap_or(String::from("dioxus_app")),
};
}
let executable = ExecutableType::Binary(output_filename);
let release = false;
let hot_reload = false;
let verbose = false;
let custom_profile = None;
let features = None;
let target = None;
let cargo_args = vec![];
Ok(Self {
out_dir,
crate_dir,
workspace_dir,
target_dir,
asset_dir,
#[cfg(feature = "cli")]
manifest,
executable,
release,
dioxus_config,
hot_reload,
cross_origin_policy: false,
custom_profile,
features,
verbose,
target,
cargo_args,
})
}
pub fn as_example(&mut self, example_name: String) -> &mut Self {
self.executable = ExecutableType::Example(example_name);
self
}
pub fn with_release(&mut self, release: bool) -> &mut Self {
self.release = release;
self
}
pub fn with_hot_reload(&mut self, hot_reload: bool) -> &mut Self {
self.hot_reload = hot_reload;
self
}
pub fn with_cross_origin_policy(&mut self, cross_origin_policy: bool) -> &mut Self {
self.cross_origin_policy = cross_origin_policy;
self
}
pub fn with_verbose(&mut self, verbose: bool) -> &mut Self {
self.verbose = verbose;
self
}
pub fn set_profile(&mut self, profile: String) -> &mut Self {
self.custom_profile = Some(profile);
self
}
pub fn set_features(&mut self, features: Vec<String>) -> &mut Self {
self.features = Some(features);
self
}
pub fn set_target(&mut self, target: String) -> &mut Self {
self.target = Some(target);
self
}
pub fn set_cargo_args(&mut self, cargo_args: Vec<String>) -> &mut Self {
self.cargo_args = cargo_args;
self
}
}
fn true_bool() -> bool {
true
}

View file

@ -0,0 +1,58 @@
#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
mod config;
pub use config::*;
mod bundle;
pub use bundle::*;
mod cargo;
pub use cargo::*;
#[doc(hidden)]
pub mod __private {
use crate::CrateConfig;
pub const CONFIG_ENV: &str = "DIOXUS_CONFIG";
pub fn save_config(config: &CrateConfig) -> CrateConfigDropGuard {
std::env::set_var(CONFIG_ENV, serde_json::to_string(config).unwrap());
CrateConfigDropGuard
}
/// A guard that removes the config from the environment when dropped.
pub struct CrateConfigDropGuard;
impl Drop for CrateConfigDropGuard {
fn drop(&mut self) {
std::env::remove_var(CONFIG_ENV);
}
}
}
/// An error that occurs when the dioxus CLI was not used to build the application.
#[derive(Debug)]
pub struct DioxusCLINotUsed;
impl std::fmt::Display for DioxusCLINotUsed {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("dioxus CLI was not used to build the application")
}
}
impl std::error::Error for DioxusCLINotUsed {}
/// The current crate's configuration.
pub static CURRENT_CONFIG: once_cell::sync::Lazy<
Result<crate::config::CrateConfig, DioxusCLINotUsed>,
> = once_cell::sync::Lazy::new(|| {
CURRENT_CONFIG_JSON
.and_then(|config| serde_json::from_str(config).ok())
.ok_or_else(|| {
tracing::error!("A library is trying to access the crate's configuration, but the dioxus CLI was not used to build the application.");
DioxusCLINotUsed
})
});
/// The current crate's configuration.
pub const CURRENT_CONFIG_JSON: Option<&str> = std::option_env!("DIOXUS_CONFIG");

View file

@ -14,6 +14,7 @@ clap = { version = "4.2", features = ["derive"] }
thiserror = { workspace = true }
wasm-bindgen-cli-support = "0.2"
colored = "2.0.0"
dioxus-cli-config = { workspace = true, features = ["cli"] }
# features
log = "0.4.14"
@ -72,10 +73,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"] }
manganis-cli-support = { git = "https://github.com/DioxusLabs/collect-assets", features = ["webp", "html"] }
dioxus-autofmt = { workspace = true }
dioxus-check = { workspace = true }

View file

@ -0,0 +1,60 @@
use std::{fs::File, io::Write, path::PathBuf};
use crate::Result;
use dioxus_cli_config::CrateConfig;
use manganis_cli_support::{AssetManifest, AssetManifestExt};
pub fn asset_manifest(crate_config: &CrateConfig) -> AssetManifest {
AssetManifest::load_from_path(
crate_config.crate_dir.join("Cargo.toml"),
crate_config.workspace_dir.join("Cargo.lock"),
)
}
/// Create a head file that contains all of the imports for assets that the user project uses
pub fn create_assets_head(config: &CrateConfig) -> Result<()> {
let manifest = asset_manifest(config);
let mut file = File::create(config.out_dir.join("__assets_head.html"))?;
file.write_all(manifest.head().as_bytes())?;
Ok(())
}
/// Process any assets collected from the binary
pub(crate) fn process_assets(config: &CrateConfig) -> anyhow::Result<()> {
let manifest = asset_manifest(config);
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(())
}
/// A guard that sets up the environment for the web renderer to compile in. This guard sets the location that assets will be served from
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();
}
}

View file

@ -1,16 +1,18 @@
use crate::{
config::{CrateConfig, ExecutableType},
assets::{asset_manifest, create_assets_head, process_assets, WebAssetConfigDropGuard},
error::{Error, Result},
tools::Tool,
};
use cargo_metadata::{diagnostic::Diagnostic, Message};
use dioxus_cli_config::crate_root;
use dioxus_cli_config::CrateConfig;
use dioxus_cli_config::ExecutableType;
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, Write},
io::Read,
panic,
path::PathBuf,
time::Duration,
@ -51,6 +53,7 @@ pub fn build(config: &CrateConfig, _: bool, skip_assets: bool) -> Result<BuildRe
let ignore_files = build_assets(config)?;
let t_start = std::time::Instant::now();
let _guard = dioxus_cli_config::__private::save_config(config);
// [1] Build the .wasm module
log::info!("🚅 Running build command...");
@ -152,7 +155,7 @@ pub fn build(config: &CrateConfig, _: bool, skip_assets: bool) -> Result<BuildRe
}
// check binaryen:wasm-opt tool
let dioxus_tools = dioxus_config.application.tools.clone().unwrap_or_default();
let dioxus_tools = dioxus_config.application.tools.clone();
if dioxus_tools.contains_key("binaryen") {
let info = dioxus_tools.get("binaryen").unwrap();
let binaryen = crate::tools::Tool::Binaryen;
@ -277,6 +280,7 @@ pub fn build_desktop(
let t_start = std::time::Instant::now();
let ignore_files = build_assets(config)?;
let _guard = dioxus_cli_config::__private::save_config(config);
let mut cmd = subprocess::Exec::cmd("cargo")
.env("CARGO_TARGET_DIR", &config.target_dir)
@ -312,9 +316,9 @@ pub fn build_desktop(
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),
crate::ExecutableType::Example(name) => cmd.arg("--example").arg(name),
ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
ExecutableType::Example(name) => cmd.arg("--example").arg(name),
};
let warning_messages = prettier_build(cmd)?;
@ -326,7 +330,7 @@ pub fn build_desktop(
let file_name: String;
let mut res_path = match &config.executable {
crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => {
ExecutableType::Binary(name) | ExecutableType::Lib(name) => {
file_name = name.clone();
config
.target_dir
@ -334,7 +338,7 @@ pub fn build_desktop(
.join(release_type)
.join(name)
}
crate::ExecutableType::Example(name) => {
ExecutableType::Example(name) => {
file_name = name.clone();
config
.target_dir
@ -399,13 +403,7 @@ pub fn build_desktop(
log::info!(
"🚩 Build completed: [./{}]",
config
.dioxus_config
.application
.out_dir
.clone()
.unwrap_or_else(|| PathBuf::from("dist"))
.display()
config.dioxus_config.application.out_dir.clone().display()
);
println!("build desktop done");
@ -416,13 +414,6 @@ pub fn build_desktop(
})
}
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![];
@ -482,7 +473,7 @@ fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result<Vec<Diagnostic>> {
pub fn gen_page(config: &CrateConfig, serve: bool, skip_assets: bool) -> String {
let _gaurd = WebAssetConfigDropGuard::new();
let crate_root = crate::cargo::crate_root().unwrap();
let crate_root = crate_root().unwrap();
let custom_html_file = crate_root.join("index.html");
let mut html = if custom_html_file.is_file() {
let mut buf = String::new();
@ -502,8 +493,8 @@ pub fn gen_page(config: &CrateConfig, serve: bool, skip_assets: bool) -> String
let mut script_list = resources.script.unwrap_or_default();
if serve {
let mut dev_style = resources.dev.style.clone().unwrap_or_default();
let mut dev_script = resources.dev.script.unwrap_or_default();
let mut dev_style = resources.dev.style.clone();
let mut dev_script = resources.dev.script.clone();
style_list.append(&mut dev_style);
script_list.append(&mut dev_script);
}
@ -520,13 +511,12 @@ pub fn gen_page(config: &CrateConfig, serve: bool, skip_assets: bool) -> String
.application
.tools
.clone()
.unwrap_or_default()
.contains_key("tailwindcss")
{
style_str.push_str("<link rel=\"stylesheet\" href=\"/{base_path}/tailwind.css\">\n");
}
if !skip_assets {
let manifest = config.asset_manifest();
let manifest = asset_manifest(config);
style_str.push_str(&manifest.head());
}
@ -577,13 +567,7 @@ pub fn gen_page(config: &CrateConfig, serve: bool, skip_assets: bool) -> String
);
}
let title = config
.dioxus_config
.web
.app
.title
.clone()
.unwrap_or_else(|| "dioxus | ⛺".into());
let title = config.dioxus_config.web.app.title.clone();
replace_or_insert_before("{app_title}", &title, "</title", &mut html);
@ -610,7 +594,7 @@ fn build_assets(config: &CrateConfig) -> Result<Vec<PathBuf>> {
let mut result = vec![];
let dioxus_config = &config.dioxus_config;
let dioxus_tools = dioxus_config.application.tools.clone().unwrap_or_default();
let dioxus_tools = dioxus_config.application.tools.clone();
// check sass tool state
let sass = Tool::Sass;
@ -748,42 +732,3 @@ 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();
}
}

View file

@ -139,7 +139,7 @@ async fn format_file(
///
/// Doesn't do mod-descending, so it will still try to format unreachable files. TODO.
async fn autoformat_project(check: bool) -> Result<()> {
let crate_config = crate::CrateConfig::new(None)?;
let crate_config = dioxus_cli_config::CrateConfig::new(None)?;
let files_to_format = get_project_files(&crate_config);

View file

@ -1,8 +1,9 @@
use crate::assets::WebAssetConfigDropGuard;
#[cfg(feature = "plugin")]
use crate::plugin::PluginManager;
use crate::server::fullstack::FullstackServerEnvGuard;
use crate::server::fullstack::FullstackWebEnvGuard;
use crate::{cfg::Platform, WebAssetConfigDropGuard};
use dioxus_cli_config::Platform;
use super::*;
@ -16,7 +17,7 @@ pub struct Build {
impl Build {
pub fn build(self, bin: Option<PathBuf>, target_dir: Option<&std::path::Path>) -> Result<()> {
let mut crate_config = crate::CrateConfig::new(bin)?;
let mut crate_config = dioxus_cli_config::CrateConfig::new(bin)?;
if let Some(target_dir) = target_dir {
crate_config.target_dir = target_dir.to_path_buf();
}
@ -96,14 +97,7 @@ impl Build {
let mut file = std::fs::File::create(
crate_config
.crate_dir
.join(
crate_config
.dioxus_config
.application
.out_dir
.clone()
.unwrap_or_else(|| PathBuf::from("dist")),
)
.join(crate_config.dioxus_config.application.out_dir.clone())
.join("index.html"),
)?;
file.write_all(temp.as_bytes())?;

View file

@ -1,4 +1,5 @@
use core::panic;
use dioxus_cli_config::ExecutableType;
use std::{fs::create_dir_all, str::FromStr};
use tauri_bundler::{BundleSettings, PackageSettings, SettingsBuilder};
@ -62,7 +63,7 @@ impl From<PackageType> for tauri_bundler::PackageType {
impl Bundle {
pub fn bundle(self, bin: Option<PathBuf>) -> Result<()> {
let mut crate_config = crate::CrateConfig::new(bin)?;
let mut crate_config = dioxus_cli_config::CrateConfig::new(bin)?;
// change the release state.
crate_config.with_release(self.build.release);
@ -89,9 +90,9 @@ impl Bundle {
let package = crate_config.manifest.package.unwrap();
let mut name: PathBuf = match &crate_config.executable {
crate::ExecutableType::Binary(name)
| crate::ExecutableType::Lib(name)
| crate::ExecutableType::Example(name) => name,
ExecutableType::Binary(name)
| ExecutableType::Lib(name)
| ExecutableType::Example(name) => name,
}
.into();
if cfg!(windows) {

View file

@ -1,5 +1,4 @@
use clap::ValueEnum;
use serde::Serialize;
use dioxus_cli_config::Platform;
use super::*;
@ -154,19 +153,6 @@ pub struct ConfigOptsServe {
pub cargo_args: Vec<String>,
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize, Debug)]
pub enum Platform {
#[clap(name = "web")]
#[serde(rename = "web")]
Web,
#[clap(name = "desktop")]
#[serde(rename = "desktop")]
Desktop,
#[clap(name = "fullstack")]
#[serde(rename = "fullstack")]
Fullstack,
}
/// Config options for the bundling system.
#[derive(Clone, Debug, Default, Deserialize, Parser)]
pub struct ConfigOptsBundle {

View file

@ -47,7 +47,7 @@ async fn check_file_and_report(path: PathBuf) -> Result<()> {
///
/// Doesn't do mod-descending, so it will still try to check unreachable files. TODO.
async fn check_project_and_report() -> Result<()> {
let crate_config = crate::CrateConfig::new(None)?;
let crate_config = dioxus_cli_config::CrateConfig::new(None)?;
let mut files_to_check = vec![];
collect_rs_files(&crate_config.crate_dir, &mut files_to_check);

View file

@ -7,7 +7,7 @@ pub struct Clean {}
impl Clean {
pub fn clean(self, bin: Option<PathBuf>) -> Result<()> {
let crate_config = crate::CrateConfig::new(bin)?;
let crate_config = dioxus_cli_config::CrateConfig::new(bin)?;
let output = Command::new("cargo")
.arg("clean")
@ -19,11 +19,7 @@ impl Clean {
return custom_error!("Cargo clean failed.");
}
let out_dir = crate_config
.dioxus_config
.application
.out_dir
.unwrap_or_else(|| PathBuf::from("dist"));
let out_dir = crate_config.dioxus_config.application.out_dir;
if crate_config.crate_dir.join(&out_dir).is_dir() {
remove_dir_all(crate_config.crate_dir.join(&out_dir))?;
}

View file

@ -1,3 +1,5 @@
use dioxus_cli_config::crate_root;
use super::*;
/// Dioxus config file controls
@ -26,7 +28,7 @@ pub enum Config {
impl Config {
pub fn config(self) -> Result<()> {
let crate_root = crate::cargo::crate_root()?;
let crate_root = crate_root()?;
match self {
Config::Init {
name,
@ -48,7 +50,10 @@ impl Config {
log::info!("🚩 Init config file completed.");
}
Config::FormatPrint {} => {
println!("{:#?}", crate::CrateConfig::new(None)?.dioxus_config);
println!(
"{:#?}",
dioxus_cli_config::CrateConfig::new(None)?.dioxus_config
);
}
Config::CustomHtml {} => {
let html_path = crate_root.join("index.html");

View file

@ -15,9 +15,10 @@ use crate::{
cfg::{ConfigOptsBuild, ConfigOptsServe},
custom_error,
error::Result,
gen_page, server, CrateConfig, Error,
gen_page, server, Error,
};
use clap::{Parser, Subcommand};
use dioxus_cli_config::CrateConfig;
use html_parser::Dom;
use serde::Deserialize;
use std::{

View file

@ -1,3 +1,5 @@
use dioxus_cli_config::Platform;
use super::*;
use std::{fs::create_dir_all, io::Write, path::PathBuf};
@ -11,7 +13,7 @@ pub struct Serve {
impl Serve {
pub async fn serve(self, bin: Option<PathBuf>) -> Result<()> {
let mut crate_config = crate::CrateConfig::new(bin)?;
let mut crate_config = dioxus_cli_config::CrateConfig::new(bin)?;
let serve_cfg = self.serve.clone();
// change the relase state.
@ -32,9 +34,6 @@ impl Serve {
crate_config.set_features(self.serve.features.unwrap());
}
// Subdirectories don't work with the server
crate_config.dioxus_config.web.app.base_path = None;
if let Some(target) = self.serve.target {
crate_config.set_target(target);
}
@ -47,7 +46,7 @@ impl Serve {
.unwrap_or(crate_config.dioxus_config.application.default_platform);
match platform {
cfg::Platform::Web => {
Platform::Web => {
// generate dev-index page
Serve::regen_dev_page(&crate_config, self.serve.skip_assets)?;
@ -60,10 +59,10 @@ impl Serve {
)
.await?;
}
cfg::Platform::Desktop => {
Platform::Desktop => {
server::desktop::startup(crate_config.clone(), &serve_cfg).await?;
}
cfg::Platform::Fullstack => {
Platform::Fullstack => {
server::fullstack::startup(crate_config.clone(), &serve_cfg).await?;
}
}
@ -73,14 +72,9 @@ impl Serve {
pub fn regen_dev_page(crate_config: &CrateConfig, skip_assets: bool) -> Result<()> {
let serve_html = gen_page(crate_config, true, skip_assets);
let dist_path = crate_config.crate_dir.join(
crate_config
.dioxus_config
.application
.out_dir
.clone()
.unwrap_or_else(|| PathBuf::from("dist")),
);
let dist_path = crate_config
.crate_dir
.join(crate_config.dioxus_config.application.out_dir.clone());
if !dist_path.is_dir() {
create_dir_all(&dist_path)?;
}

View file

@ -1,607 +0,0 @@
use crate::{cfg::Platform, error::Result};
use manganis_cli_support::AssetManifest;
use manganis_cli_support::AssetManifestExt;
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
path::{Path, PathBuf},
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DioxusConfig {
pub application: ApplicationConfig,
pub web: WebConfig,
#[serde(default)]
pub bundle: BundleConfig,
#[serde(default = "default_plugin")]
pub plugin: toml::Value,
}
fn default_plugin() -> toml::Value {
toml::Value::Boolean(true)
}
impl DioxusConfig {
pub fn load(bin: Option<PathBuf>) -> crate::error::Result<Option<DioxusConfig>> {
let crate_dir = crate::cargo::crate_root();
let crate_dir = match crate_dir {
Ok(dir) => {
if let Some(bin) = bin {
dir.join(bin)
} else {
dir
}
}
Err(_) => return Ok(None),
};
let crate_dir = crate_dir.as_path();
let Some(dioxus_conf_file) = acquire_dioxus_toml(crate_dir) else {
return Ok(None);
};
let dioxus_conf_file = dioxus_conf_file.as_path();
let cfg = toml::from_str::<DioxusConfig>(&std::fs::read_to_string(dioxus_conf_file)?)
.map_err(|err| {
let error_location = dioxus_conf_file
.strip_prefix(crate_dir)
.unwrap_or(dioxus_conf_file)
.display();
crate::Error::Unique(format!("{error_location} {err}"))
})
.map(Some);
match cfg {
Ok(Some(mut cfg)) => {
let name = cfg.application.name.clone();
if cfg.bundle.identifier.is_none() {
cfg.bundle.identifier = Some(format!("io.github.{name}"));
}
if cfg.bundle.publisher.is_none() {
cfg.bundle.publisher = Some(name);
}
Ok(Some(cfg))
}
cfg => cfg,
}
}
}
fn acquire_dioxus_toml(dir: &Path) -> Option<PathBuf> {
// prefer uppercase
let uppercase_conf = dir.join("Dioxus.toml");
if uppercase_conf.is_file() {
return Some(uppercase_conf);
}
// lowercase is fine too
let lowercase_conf = dir.join("dioxus.toml");
if lowercase_conf.is_file() {
return Some(lowercase_conf);
}
None
}
impl Default for DioxusConfig {
fn default() -> Self {
let name = "name";
Self {
application: ApplicationConfig {
name: name.into(),
default_platform: Platform::Web,
out_dir: Some(PathBuf::from("dist")),
asset_dir: Some(PathBuf::from("public")),
tools: None,
sub_package: None,
},
web: WebConfig {
app: WebAppConfig {
title: Some("dioxus | ⛺".into()),
base_path: None,
},
proxy: Some(vec![]),
watcher: WebWatcherConfig {
watch_path: Some(vec![PathBuf::from("src"), PathBuf::from("examples")]),
reload_html: Some(false),
index_on_404: Some(true),
},
resource: WebResourceConfig {
dev: WebDevResourceConfig {
style: Some(vec![]),
script: Some(vec![]),
},
style: Some(vec![]),
script: Some(vec![]),
},
https: WebHttpsConfig {
enabled: None,
mkcert: None,
key_path: None,
cert_path: None,
},
},
bundle: BundleConfig {
identifier: Some(format!("io.github.{name}")),
publisher: Some(name.into()),
..Default::default()
},
plugin: toml::Value::Table(toml::map::Map::new()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApplicationConfig {
pub name: String,
pub default_platform: Platform,
pub out_dir: Option<PathBuf>,
pub asset_dir: Option<PathBuf>,
pub tools: Option<HashMap<String, toml::Value>>,
pub sub_package: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebConfig {
pub app: WebAppConfig,
pub proxy: Option<Vec<WebProxyConfig>>,
pub watcher: WebWatcherConfig,
pub resource: WebResourceConfig,
#[serde(default)]
pub https: WebHttpsConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebAppConfig {
pub title: Option<String>,
pub base_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebProxyConfig {
pub backend: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebWatcherConfig {
pub watch_path: Option<Vec<PathBuf>>,
pub reload_html: Option<bool>,
pub index_on_404: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebResourceConfig {
pub dev: WebDevResourceConfig,
pub style: Option<Vec<PathBuf>>,
pub script: Option<Vec<PathBuf>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebDevResourceConfig {
pub style: Option<Vec<PathBuf>>,
pub script: Option<Vec<PathBuf>>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct WebHttpsConfig {
pub enabled: Option<bool>,
pub mkcert: Option<bool>,
pub key_path: Option<String>,
pub cert_path: Option<String>,
}
#[derive(Debug, Clone)]
pub struct CrateConfig {
pub out_dir: PathBuf,
pub crate_dir: PathBuf,
pub workspace_dir: PathBuf,
pub target_dir: PathBuf,
pub asset_dir: PathBuf,
pub manifest: cargo_toml::Manifest<cargo_toml::Value>,
pub executable: ExecutableType,
pub dioxus_config: DioxusConfig,
pub release: bool,
pub hot_reload: bool,
pub cross_origin_policy: bool,
pub verbose: bool,
pub custom_profile: Option<String>,
pub features: Option<Vec<String>>,
pub target: Option<String>,
pub cargo_args: Vec<String>,
}
#[derive(Debug, Clone)]
pub enum ExecutableType {
Binary(String),
Lib(String),
Example(String),
}
impl CrateConfig {
pub fn new(bin: Option<PathBuf>) -> Result<Self> {
let dioxus_config = DioxusConfig::load(bin.clone())?.unwrap_or_default();
let crate_root = crate::cargo::crate_root()?;
let crate_dir = if let Some(package) = &dioxus_config.application.sub_package {
crate_root.join(package)
} else if let Some(bin) = bin {
crate_root.join(bin)
} else {
crate_root
};
let meta = crate::cargo::Metadata::get()?;
let workspace_dir = meta.workspace_root;
let target_dir = meta.target_directory;
let out_dir = match dioxus_config.application.out_dir {
Some(ref v) => crate_dir.join(v),
None => crate_dir.join("dist"),
};
let cargo_def = &crate_dir.join("Cargo.toml");
let asset_dir = match dioxus_config.application.asset_dir {
Some(ref v) => crate_dir.join(v),
None => crate_dir.join("public"),
};
let manifest = cargo_toml::Manifest::from_path(cargo_def).unwrap();
let mut output_filename = String::from("dioxus_app");
if let Some(package) = &manifest.package.as_ref() {
output_filename = match &package.default_run {
Some(default_run_target) => default_run_target.to_owned(),
None => manifest
.bin
.iter()
.find(|b| b.name == manifest.package.as_ref().map(|pkg| pkg.name.clone()))
.or(manifest
.bin
.iter()
.find(|b| b.path == Some("src/main.rs".to_owned())))
.or(manifest.bin.first())
.or(manifest.lib.as_ref())
.and_then(|prod| prod.name.clone())
.unwrap_or(String::from("dioxus_app")),
};
}
let executable = ExecutableType::Binary(output_filename);
let release = false;
let hot_reload = false;
let verbose = false;
let custom_profile = None;
let features = None;
let target = None;
let cargo_args = vec![];
Ok(Self {
out_dir,
crate_dir,
workspace_dir,
target_dir,
asset_dir,
manifest,
executable,
release,
dioxus_config,
hot_reload,
cross_origin_policy: false,
custom_profile,
features,
verbose,
target,
cargo_args,
})
}
pub fn asset_manifest(&self) -> AssetManifest {
AssetManifest::load_from_path(
self.crate_dir.join("Cargo.toml"),
self.workspace_dir.join("Cargo.lock"),
)
}
pub fn as_example(&mut self, example_name: String) -> &mut Self {
self.executable = ExecutableType::Example(example_name);
self
}
pub fn with_release(&mut self, release: bool) -> &mut Self {
self.release = release;
self
}
pub fn with_hot_reload(&mut self, hot_reload: bool) -> &mut Self {
self.hot_reload = hot_reload;
self
}
pub fn with_cross_origin_policy(&mut self, cross_origin_policy: bool) -> &mut Self {
self.cross_origin_policy = cross_origin_policy;
self
}
pub fn with_verbose(&mut self, verbose: bool) -> &mut Self {
self.verbose = verbose;
self
}
pub fn set_profile(&mut self, profile: String) -> &mut Self {
self.custom_profile = Some(profile);
self
}
pub fn set_features(&mut self, features: Vec<String>) -> &mut Self {
self.features = Some(features);
self
}
pub fn set_target(&mut self, target: String) -> &mut Self {
self.target = Some(target);
self
}
pub fn set_cargo_args(&mut self, cargo_args: Vec<String>) -> &mut Self {
self.cargo_args = cargo_args;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BundleConfig {
pub identifier: Option<String>,
pub publisher: Option<String>,
pub icon: Option<Vec<String>>,
pub resources: Option<Vec<String>>,
pub copyright: Option<String>,
pub category: Option<String>,
pub short_description: Option<String>,
pub long_description: Option<String>,
pub external_bin: Option<Vec<String>>,
pub deb: Option<DebianSettings>,
pub macos: Option<MacOsSettings>,
pub windows: Option<WindowsSettings>,
}
impl From<BundleConfig> for tauri_bundler::BundleSettings {
fn from(val: BundleConfig) -> Self {
tauri_bundler::BundleSettings {
identifier: val.identifier,
publisher: val.publisher,
icon: val.icon,
resources: val.resources,
copyright: val.copyright,
category: val.category.and_then(|c| c.parse().ok()),
short_description: val.short_description,
long_description: val.long_description,
external_bin: val.external_bin,
deb: val.deb.map(Into::into).unwrap_or_default(),
macos: val.macos.map(Into::into).unwrap_or_default(),
windows: val.windows.map(Into::into).unwrap_or_default(),
..Default::default()
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DebianSettings {
pub depends: Option<Vec<String>>,
pub files: HashMap<PathBuf, PathBuf>,
pub nsis: Option<NsisSettings>,
}
impl From<DebianSettings> for tauri_bundler::DebianSettings {
fn from(val: DebianSettings) -> Self {
tauri_bundler::DebianSettings {
depends: val.depends,
files: val.files,
desktop_template: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct WixSettings {
pub language: Vec<(String, Option<PathBuf>)>,
pub template: Option<PathBuf>,
pub fragment_paths: Vec<PathBuf>,
pub component_group_refs: Vec<String>,
pub component_refs: Vec<String>,
pub feature_group_refs: Vec<String>,
pub feature_refs: Vec<String>,
pub merge_refs: Vec<String>,
pub skip_webview_install: bool,
pub license: Option<PathBuf>,
pub enable_elevated_update_task: bool,
pub banner_path: Option<PathBuf>,
pub dialog_image_path: Option<PathBuf>,
pub fips_compliant: bool,
}
impl From<WixSettings> for tauri_bundler::WixSettings {
fn from(val: WixSettings) -> Self {
tauri_bundler::WixSettings {
language: tauri_bundler::bundle::WixLanguage({
let mut languages: Vec<_> = val
.language
.iter()
.map(|l| {
(
l.0.clone(),
tauri_bundler::bundle::WixLanguageConfig {
locale_path: l.1.clone(),
},
)
})
.collect();
if languages.is_empty() {
languages.push(("en-US".into(), Default::default()));
}
languages
}),
template: val.template,
fragment_paths: val.fragment_paths,
component_group_refs: val.component_group_refs,
component_refs: val.component_refs,
feature_group_refs: val.feature_group_refs,
feature_refs: val.feature_refs,
merge_refs: val.merge_refs,
skip_webview_install: val.skip_webview_install,
license: val.license,
enable_elevated_update_task: val.enable_elevated_update_task,
banner_path: val.banner_path,
dialog_image_path: val.dialog_image_path,
fips_compliant: val.fips_compliant,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MacOsSettings {
pub frameworks: Option<Vec<String>>,
pub minimum_system_version: Option<String>,
pub license: Option<String>,
pub exception_domain: Option<String>,
pub signing_identity: Option<String>,
pub provider_short_name: Option<String>,
pub entitlements: Option<String>,
pub info_plist_path: Option<PathBuf>,
}
impl From<MacOsSettings> for tauri_bundler::MacOsSettings {
fn from(val: MacOsSettings) -> Self {
tauri_bundler::MacOsSettings {
frameworks: val.frameworks,
minimum_system_version: val.minimum_system_version,
license: val.license,
exception_domain: val.exception_domain,
signing_identity: val.signing_identity,
provider_short_name: val.provider_short_name,
entitlements: val.entitlements,
info_plist_path: val.info_plist_path,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WindowsSettings {
pub digest_algorithm: Option<String>,
pub certificate_thumbprint: Option<String>,
pub timestamp_url: Option<String>,
pub tsp: bool,
pub wix: Option<WixSettings>,
pub icon_path: Option<PathBuf>,
pub webview_install_mode: WebviewInstallMode,
pub webview_fixed_runtime_path: Option<PathBuf>,
pub allow_downgrades: bool,
pub nsis: Option<NsisSettings>,
}
impl From<WindowsSettings> for tauri_bundler::WindowsSettings {
fn from(val: WindowsSettings) -> Self {
tauri_bundler::WindowsSettings {
digest_algorithm: val.digest_algorithm,
certificate_thumbprint: val.certificate_thumbprint,
timestamp_url: val.timestamp_url,
tsp: val.tsp,
wix: val.wix.map(Into::into),
icon_path: val.icon_path.unwrap_or("icons/icon.ico".into()),
webview_install_mode: val.webview_install_mode.into(),
webview_fixed_runtime_path: val.webview_fixed_runtime_path,
allow_downgrades: val.allow_downgrades,
nsis: val.nsis.map(Into::into),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NsisSettings {
pub template: Option<PathBuf>,
pub license: Option<PathBuf>,
pub header_image: Option<PathBuf>,
pub sidebar_image: Option<PathBuf>,
pub installer_icon: Option<PathBuf>,
pub install_mode: NSISInstallerMode,
pub languages: Option<Vec<String>>,
pub custom_language_files: Option<HashMap<String, PathBuf>>,
pub display_language_selector: bool,
}
impl From<NsisSettings> for tauri_bundler::NsisSettings {
fn from(val: NsisSettings) -> Self {
tauri_bundler::NsisSettings {
license: val.license,
header_image: val.header_image,
sidebar_image: val.sidebar_image,
installer_icon: val.installer_icon,
install_mode: val.install_mode.into(),
languages: val.languages,
display_language_selector: val.display_language_selector,
custom_language_files: None,
template: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum NSISInstallerMode {
CurrentUser,
PerMachine,
Both,
}
impl From<NSISInstallerMode> for tauri_utils::config::NSISInstallerMode {
fn from(val: NSISInstallerMode) -> Self {
match val {
NSISInstallerMode::CurrentUser => tauri_utils::config::NSISInstallerMode::CurrentUser,
NSISInstallerMode::PerMachine => tauri_utils::config::NSISInstallerMode::PerMachine,
NSISInstallerMode::Both => tauri_utils::config::NSISInstallerMode::Both,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum WebviewInstallMode {
Skip,
DownloadBootstrapper { silent: bool },
EmbedBootstrapper { silent: bool },
OfflineInstaller { silent: bool },
FixedRuntime { path: PathBuf },
}
impl WebviewInstallMode {
fn into(self) -> tauri_utils::config::WebviewInstallMode {
match self {
Self::Skip => tauri_utils::config::WebviewInstallMode::Skip,
Self::DownloadBootstrapper { silent } => {
tauri_utils::config::WebviewInstallMode::DownloadBootstrapper { silent }
}
Self::EmbedBootstrapper { silent } => {
tauri_utils::config::WebviewInstallMode::EmbedBootstrapper { silent }
}
Self::OfflineInstaller { silent } => {
tauri_utils::config::WebviewInstallMode::OfflineInstaller { silent }
}
Self::FixedRuntime { path } => {
tauri_utils::config::WebviewInstallMode::FixedRuntime { path }
}
}
}
}
impl Default for WebviewInstallMode {
fn default() -> Self {
Self::OfflineInstaller { silent: false }
}
}

View file

@ -72,6 +72,24 @@ impl From<hyper::Error> for Error {
}
}
impl From<dioxus_cli_config::LoadDioxusConfigError> for Error {
fn from(e: dioxus_cli_config::LoadDioxusConfigError) -> Self {
Self::RuntimeError(e.to_string())
}
}
impl From<dioxus_cli_config::CargoError> for Error {
fn from(e: dioxus_cli_config::CargoError) -> Self {
Self::CargoError(e.to_string())
}
}
impl From<dioxus_cli_config::CrateConfigError> for Error {
fn from(e: dioxus_cli_config::CrateConfigError) -> Self {
Self::RuntimeError(e.to_string())
}
}
#[macro_export]
macro_rules! custom_error {
($msg:literal $(,)?) => {

View file

@ -4,21 +4,16 @@
pub const DIOXUS_CLI_VERSION: &str = "0.4.1";
mod assets;
pub mod builder;
pub mod server;
pub mod tools;
pub use builder::*;
pub mod cargo;
pub use cargo::*;
pub mod cli;
pub use cli::*;
pub mod config;
pub use config::*;
pub mod error;
pub use error::*;

View file

@ -1,3 +1,4 @@
use dioxus_cli_config::DioxusConfig;
use std::path::PathBuf;
use anyhow::anyhow;

View file

@ -5,8 +5,11 @@ use crate::{
output::{print_console_info, PrettierOptions},
setup_file_watcher,
},
BuildResult, CrateConfig, Result,
BuildResult, Result,
};
use dioxus_cli_config::CrateConfig;
use dioxus_cli_config::ExecutableType;
use dioxus_hot_reload::HotReloadMsg;
use dioxus_html::HtmlCtx;
use dioxus_rsx::hot_reload::*;
@ -215,9 +218,9 @@ fn start_desktop(config: &CrateConfig, skip_assets: bool) -> Result<(RAIIChild,
let result = crate::builder::build_desktop(config, true, skip_assets)?;
match &config.executable {
crate::ExecutableType::Binary(name)
| crate::ExecutableType::Lib(name)
| crate::ExecutableType::Example(name) => {
ExecutableType::Binary(name)
| ExecutableType::Lib(name)
| ExecutableType::Example(name) => {
let mut file = config.out_dir.join(name);
if cfg!(windows) {
file.set_extension("exe");

View file

@ -1,6 +1,9 @@
use dioxus_cli_config::CrateConfig;
use crate::{
assets::WebAssetConfigDropGuard,
cfg::{ConfigOptsBuild, ConfigOptsServe},
CrateConfig, Result, WebAssetConfigDropGuard,
Result,
};
use super::{desktop, Platform};
@ -86,7 +89,7 @@ fn build_web(serve: ConfigOptsServe, target_directory: &std::path::Path) -> Resu
}
None => web_config.features = Some(vec![web_feature]),
};
web_config.platform = Some(crate::cfg::Platform::Web);
web_config.platform = Some(dioxus_cli_config::Platform::Web);
let _gaurd = FullstackWebEnvGuard::new(&web_config);
crate::cli::build::Build { build: web_config }.build(None, Some(target_directory))

View file

@ -1,14 +1,12 @@
use crate::{cfg::ConfigOptsServe, BuildResult, CrateConfig, Result};
use crate::{cfg::ConfigOptsServe, BuildResult, Result};
use dioxus_cli_config::CrateConfig;
use cargo_metadata::diagnostic::Diagnostic;
use dioxus_core::Template;
use dioxus_html::HtmlCtx;
use dioxus_rsx::hot_reload::*;
use notify::{RecommendedWatcher, Watcher};
use std::{
path::PathBuf,
sync::{Arc, Mutex},
};
use std::sync::{Arc, Mutex};
use tokio::sync::broadcast::{self};
mod output;
@ -27,13 +25,7 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
let mut last_update_time = chrono::Local::now().timestamp();
// file watcher: check file change
let allow_watch_path = config
.dioxus_config
.web
.watcher
.watch_path
.clone()
.unwrap_or_else(|| vec![PathBuf::from("src"), PathBuf::from("examples")]);
let allow_watch_path = config.dioxus_config.web.watcher.watch_path.clone();
let watcher_config = config.clone();
let mut watcher = notify::recommended_watcher(move |info: notify::Result<notify::Event>| {

View file

@ -1,6 +1,7 @@
use crate::server::Diagnostic;
use crate::CrateConfig;
use colored::Colorize;
use dioxus_cli_config::crate_root;
use dioxus_cli_config::CrateConfig;
use std::path::PathBuf;
use std::process::Command;
@ -43,25 +44,19 @@ pub fn print_console_info(
profile = config.custom_profile.as_ref().unwrap().to_string();
}
let hot_reload = if config.hot_reload { "RSX" } else { "Normal" };
let crate_root = crate::cargo::crate_root().unwrap();
let crate_root = crate_root().unwrap();
let custom_html_file = if crate_root.join("index.html").is_file() {
"Custom [index.html]"
} else {
"Default"
};
let url_rewrite = if config
.dioxus_config
.web
.watcher
.index_on_404
.unwrap_or(false)
{
let url_rewrite = if config.dioxus_config.web.watcher.index_on_404 {
"True"
} else {
"False"
};
let proxies = config.dioxus_config.web.proxy.as_ref();
let proxies = &config.dioxus_config.web.proxy;
if options.changed.is_empty() {
println!(
@ -109,12 +104,10 @@ pub fn print_console_info(
println!();
println!("\t> Profile : {}", profile.green());
println!("\t> Hot Reload : {}", hot_reload.cyan());
if let Some(proxies) = proxies {
if !proxies.is_empty() {
println!("\t> Proxies :");
for proxy in proxies {
println!("\t\t- {}", proxy.backend.blue());
}
if !proxies.is_empty() {
println!("\t> Proxies :");
for proxy in proxies {
println!("\t\t- {}", proxy.backend.blue());
}
}
println!("\t> Index Template : {}", custom_html_file.green());

View file

@ -5,7 +5,7 @@ use crate::{
output::{print_console_info, PrettierOptions, WebServerInfo},
setup_file_watcher, HotReloadState,
},
BuildResult, CrateConfig, Result, WebHttpsConfig,
BuildResult, Result,
};
use axum::{
body::{Full, HttpBody},
@ -20,6 +20,8 @@ use axum::{
Router,
};
use axum_server::tls_rustls::RustlsConfig;
use dioxus_cli_config::CrateConfig;
use dioxus_cli_config::WebHttpsConfig;
use dioxus_html::HtmlCtx;
use dioxus_rsx::hot_reload::*;
@ -277,12 +279,7 @@ async fn setup_router(
.override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
.and_then(
move |response: Response<ServeFileSystemResponseBody>| async move {
let mut response = if file_service_config
.dioxus_config
.web
.watcher
.index_on_404
.unwrap_or(false)
let mut response = if file_service_config.dioxus_config.web.watcher.index_on_404
&& response.status() == StatusCode::NOT_FOUND
{
let body = Full::from(
@ -321,7 +318,7 @@ async fn setup_router(
let mut router = Router::new().route("/_dioxus/ws", get(ws_handler));
// Setup proxy
for proxy_config in config.dioxus_config.web.proxy.unwrap_or_default() {
for proxy_config in config.dioxus_config.web.proxy {
router = proxy::add_proxy(router, &proxy_config)?;
}
@ -335,6 +332,18 @@ async fn setup_router(
},
));
router = if let Some(base_path) = config.dioxus_config.web.app.base_path.clone() {
let base_path = format!("/{}", base_path.trim_matches('/'));
Router::new()
.nest(&base_path, axum::routing::any_service(router))
.fallback(get(move || {
let base_path = base_path.clone();
async move { format!("Outside of the base path: {}", base_path) }
}))
} else {
router
};
// Setup routes
router = router
.route("/_dioxus/hot_reload", get(hot_reload_handler))
@ -438,13 +447,7 @@ fn build(config: &CrateConfig, reload_tx: &Sender<()>, skip_assets: bool) -> Res
let result = builder::build(config, true, skip_assets)?;
// change the websocket reload state to true;
// the page will auto-reload.
if config
.dioxus_config
.web
.watcher
.reload_html
.unwrap_or(false)
{
if config.dioxus_config.web.watcher.reload_html {
let _ = Serve::regen_dev_page(config, skip_assets);
}
let _ = reload_tx.send(());

View file

@ -1,4 +1,5 @@
use crate::{Result, WebProxyConfig};
use crate::Result;
use dioxus_cli_config::WebProxyConfig;
use anyhow::Context;
use axum::{http::StatusCode, routing::any, Router};

View file

@ -15,10 +15,11 @@ proc-macro = true
[dependencies]
proc-macro2 = { version = "1.0" }
quote = "1.0"
syn = { version = "2.0", features = ["full", "extra-traits"] }
syn = { version = "2.0", features = ["full", "extra-traits", "visit"] }
dioxus-rsx = { workspace = true }
dioxus-core = { workspace = true }
constcat = "0.3.0"
convert_case = "^0.6.0"
prettyplease = "0.2.15"
# testing

View file

@ -24,6 +24,10 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result<TokenStream, Error> {
.included_fields()
.map(|f| struct_info.field_impl(f))
.collect::<Result<Vec<_>, _>>()?;
let extends = struct_info
.extend_fields()
.map(|f| struct_info.extends_impl(f))
.collect::<Result<Vec<_>, _>>()?;
let fields = quote!(#(#fields)*).into_iter();
let required_fields = struct_info
.included_fields()
@ -36,6 +40,7 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result<TokenStream, Error> {
#builder_creation
#conversion_helper
#( #fields )*
#( #extends )*
#( #required_fields )*
#build_method
}
@ -167,8 +172,8 @@ mod field_info {
use proc_macro2::TokenStream;
use quote::quote;
use syn::spanned::Spanned;
use syn::Expr;
use syn::{parse::Error, punctuated::Punctuated};
use syn::{Expr, Path};
use super::util::{
expr_to_single_string, ident_to_type, path_to_single_string, strip_raw_ident_prefix,
@ -199,6 +204,13 @@ mod field_info {
);
}
// extended field is automatically empty
if !builder_attr.extends.is_empty() {
builder_attr.default = Some(
syn::parse(quote!(::core::default::Default::default()).into()).unwrap(),
);
}
// auto detect optional
let strip_option_auto = builder_attr.strip_option
|| !builder_attr.ignore_option
@ -253,6 +265,7 @@ mod field_info {
pub auto_into: bool,
pub strip_option: bool,
pub ignore_option: bool,
pub extends: Vec<Path>,
}
impl FieldBuilderAttr {
@ -305,6 +318,17 @@ mod field_info {
let name = expr_to_single_string(&assign.left)
.ok_or_else(|| Error::new_spanned(&assign.left, "Expected identifier"))?;
match name.as_str() {
"extends" => {
if let syn::Expr::Path(path) = *assign.right {
self.extends.push(path.path);
Ok(())
} else {
Err(Error::new_spanned(
assign.right,
"Expected simple identifier",
))
}
}
"default" => {
self.default = Some(*assign.right);
Ok(())
@ -359,6 +383,11 @@ mod field_info {
Ok(())
}
"extend" => {
self.extends.push(path.path);
Ok(())
}
_ => {
macro_rules! handle_fields {
( $( $flag:expr, $field:ident, $already:expr; )* ) => {
@ -462,11 +491,14 @@ fn type_from_inside_option(ty: &syn::Type, check_option_name: bool) -> Option<&s
}
mod struct_info {
use convert_case::{Case, Casing};
use proc_macro2::TokenStream;
use quote::quote;
use syn::parse::Error;
use syn::punctuated::Punctuated;
use syn::Expr;
use syn::spanned::Spanned;
use syn::visit::Visit;
use syn::{parse_quote, Expr, Ident};
use super::field_info::{FieldBuilderAttr, FieldInfo};
use super::util::{
@ -489,7 +521,46 @@ mod struct_info {
impl<'a> StructInfo<'a> {
pub fn included_fields(&self) -> impl Iterator<Item = &FieldInfo<'a>> {
self.fields.iter().filter(|f| !f.builder_attr.skip)
self.fields
.iter()
.filter(|f| !f.builder_attr.skip && f.builder_attr.extends.is_empty())
}
pub fn extend_fields(&self) -> impl Iterator<Item = &FieldInfo<'a>> {
self.fields
.iter()
.filter(|f| !f.builder_attr.extends.is_empty())
}
fn extend_lifetime(&self) -> syn::Result<Option<syn::Lifetime>> {
let first_extend = self.extend_fields().next();
match first_extend {
Some(f) => {
struct VisitFirstLifetime(Option<syn::Lifetime>);
impl Visit<'_> for VisitFirstLifetime {
fn visit_lifetime(&mut self, lifetime: &'_ syn::Lifetime) {
if self.0.is_none() {
self.0 = Some(lifetime.clone());
}
}
}
let name = f.name;
let mut visitor = VisitFirstLifetime(None);
visitor.visit_type(f.ty);
visitor.0.ok_or_else(|| {
syn::Error::new_spanned(
name,
"Unable to find lifetime for extended field. Please specify it manually",
)
}).map(Some)
}
None => Ok(None),
}
}
pub fn new(
@ -536,7 +607,17 @@ mod struct_info {
// Therefore, we will generate code that shortcircuits the "comparison" in memoization
let are_there_generics = !self.generics.params.is_empty();
let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
let extend_lifetime = self.extend_lifetime()?;
let generics = self.generics.clone();
let (_, ty_generics, where_clause) = generics.split_for_impl();
let impl_generics = self.modify_generics(|g| {
if extend_lifetime.is_none() {
g.params.insert(0, parse_quote!('__bump));
}
});
let (impl_generics, _, _) = impl_generics.split_for_impl();
let (_, b_initial_generics, _) = self.generics.split_for_impl();
let all_fields_param = syn::GenericParam::Type(
syn::Ident::new("TypedBuilderFields", proc_macro2::Span::call_site()).into(),
);
@ -544,7 +625,7 @@ mod struct_info {
g.params.insert(0, all_fields_param.clone());
});
let empties_tuple = type_tuple(self.included_fields().map(|_| empty_type()));
let generics_with_empty = modify_types_generics_hack(&ty_generics, |args| {
let generics_with_empty = modify_types_generics_hack(&b_initial_generics, |args| {
args.insert(0, syn::GenericArgument::Type(empties_tuple.clone().into()));
});
let phantom_generics = self.generics.params.iter().filter_map(|param| match param {
@ -603,8 +684,7 @@ Finally, call `.build()` to create the instance of `{name}`.
quote!(#[doc(hidden)])
};
let (b_generics_impl, b_generics_ty, b_generics_where_extras_predicates) =
b_generics.split_for_impl();
let (_, _, b_generics_where_extras_predicates) = b_generics.split_for_impl();
let mut b_generics_where: syn::WhereClause = syn::parse2(quote! {
where TypedBuilderFields: Clone
})?;
@ -624,12 +704,39 @@ Finally, call `.build()` to create the instance of `{name}`.
false => quote! { true },
};
let extend_fields = self.extend_fields().map(|f| {
let name = f.name;
let ty = f.ty;
quote!(#name: #ty)
});
let extend_fields_value = self.extend_fields().map(|f| {
let name = f.name;
quote!(#name: Vec::new())
});
let has_extend_fields = self.extend_fields().next().is_some();
let take_bump = if has_extend_fields {
quote!(bump: _cx.bump(),)
} else {
quote!()
};
let bump_field = if has_extend_fields {
quote!(bump: & #extend_lifetime ::dioxus::core::exports::bumpalo::Bump,)
} else {
quote!()
};
let extend_lifetime = extend_lifetime.unwrap_or(syn::Lifetime::new(
"'__bump",
proc_macro2::Span::call_site(),
));
Ok(quote! {
impl #impl_generics #name #ty_generics #where_clause {
#[doc = #builder_method_doc]
#[allow(dead_code, clippy::type_complexity)]
#vis fn builder() -> #builder_name #generics_with_empty {
#vis fn builder(_cx: & #extend_lifetime ::dioxus::prelude::ScopeState) -> #builder_name #generics_with_empty {
#builder_name {
#(#extend_fields_value,)*
#take_bump
fields: #empties_tuple,
_phantom: ::core::default::Default::default(),
}
@ -640,26 +747,19 @@ Finally, call `.build()` to create the instance of `{name}`.
#builder_type_doc
#[allow(dead_code, non_camel_case_types, non_snake_case)]
#vis struct #builder_name #b_generics {
#(#extend_fields,)*
#bump_field
fields: #all_fields_param,
_phantom: (#( #phantom_generics ),*),
}
impl #b_generics_impl Clone for #builder_name #b_generics_ty #b_generics_where {
fn clone(&self) -> Self {
Self {
fields: self.fields.clone(),
_phantom: ::core::default::Default::default(),
}
}
}
impl #impl_generics ::dioxus::prelude::Properties for #name #ty_generics
impl #impl_generics ::dioxus::prelude::Properties<#extend_lifetime> for #name #ty_generics
#b_generics_where_extras_predicates
{
type Builder = #builder_name #generics_with_empty;
const IS_STATIC: bool = #is_static;
fn builder() -> Self::Builder {
#name::builder()
fn builder(_cx: &#extend_lifetime ::dioxus::prelude::ScopeState) -> Self::Builder {
#name::builder(_cx)
}
unsafe fn memoize(&self, other: &Self) -> bool {
#can_memoize
@ -694,11 +794,143 @@ Finally, call `.build()` to create the instance of `{name}`.
})
}
pub fn extends_impl(&self, field: &FieldInfo) -> Result<TokenStream, Error> {
let StructInfo {
ref builder_name, ..
} = *self;
let field_name = field.name;
let descructuring = self.included_fields().map(|f| {
if f.ordinal == field.ordinal {
quote!(_)
} else {
let name = f.name;
quote!(#name)
}
});
let reconstructing = self.included_fields().map(|f| f.name);
// Add the bump lifetime to the generics
let mut ty_generics: Vec<syn::GenericArgument> = self
.generics
.params
.iter()
.map(|generic_param| match generic_param {
syn::GenericParam::Type(type_param) => {
let ident = type_param.ident.clone();
syn::parse(quote!(#ident).into()).unwrap()
}
syn::GenericParam::Lifetime(lifetime_def) => {
syn::GenericArgument::Lifetime(lifetime_def.lifetime.clone())
}
syn::GenericParam::Const(const_param) => {
let ident = const_param.ident.clone();
syn::parse(quote!(#ident).into()).unwrap()
}
})
.collect();
let mut target_generics_tuple = empty_type_tuple();
let mut ty_generics_tuple = empty_type_tuple();
let generics = self.modify_generics(|g| {
let index_after_lifetime_in_generics = g
.params
.iter()
.filter(|arg| matches!(arg, syn::GenericParam::Lifetime(_)))
.count();
for f in self.included_fields() {
if f.ordinal == field.ordinal {
ty_generics_tuple.elems.push_value(empty_type());
target_generics_tuple
.elems
.push_value(f.tuplized_type_ty_param());
} else {
g.params
.insert(index_after_lifetime_in_generics, f.generic_ty_param());
let generic_argument: syn::Type = f.type_ident();
ty_generics_tuple.elems.push_value(generic_argument.clone());
target_generics_tuple.elems.push_value(generic_argument);
}
ty_generics_tuple.elems.push_punct(Default::default());
target_generics_tuple.elems.push_punct(Default::default());
}
});
let mut target_generics = ty_generics.clone();
let index_after_lifetime_in_generics = target_generics
.iter()
.filter(|arg| matches!(arg, syn::GenericArgument::Lifetime(_)))
.count();
target_generics.insert(
index_after_lifetime_in_generics,
syn::GenericArgument::Type(target_generics_tuple.into()),
);
ty_generics.insert(
index_after_lifetime_in_generics,
syn::GenericArgument::Type(ty_generics_tuple.into()),
);
let (impl_generics, _, where_clause) = generics.split_for_impl();
let forward_extended_fields = self.extend_fields().map(|f| {
let name = f.name;
quote!(#name: self.#name)
});
let extend_lifetime = self.extend_lifetime()?.ok_or(Error::new_spanned(
field_name,
"Unable to find lifetime for extended field. Please specify it manually",
))?;
let extends_impl = field.builder_attr.extends.iter().map(|path| {
let name_str = path_to_single_string(path).unwrap();
let camel_name = name_str.to_case(Case::UpperCamel);
let marker_name = Ident::new(
format!("{}Extension", &camel_name).as_str(),
path.span(),
);
quote! {
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl #impl_generics dioxus_elements::extensions::#marker_name < #extend_lifetime > for #builder_name < #( #ty_generics ),* > #where_clause {}
}
});
Ok(quote! {
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl #impl_generics ::dioxus::prelude::HasAttributes<#extend_lifetime> for #builder_name < #( #ty_generics ),* > #where_clause {
fn push_attribute(
mut self,
name: &#extend_lifetime str,
ns: Option<&'static str>,
attr: impl ::dioxus::prelude::IntoAttributeValue<#extend_lifetime>,
volatile: bool
) -> Self {
let ( #(#descructuring,)* ) = self.fields;
self.#field_name.push(
::dioxus::core::Attribute::new(
name,
{
use ::dioxus::prelude::IntoAttributeValue;
attr.into_value(self.bump)
},
ns,
volatile,
)
);
#builder_name {
#(#forward_extended_fields,)*
bump: self.bump,
fields: ( #(#reconstructing,)* ),
_phantom: self._phantom,
}
}
}
#(#extends_impl)*
})
}
pub fn field_impl(&self, field: &FieldInfo) -> Result<TokenStream, Error> {
let FieldInfo {
name: field_name,
ty: field_type,
..
name: field_name, ..
} = field;
if *field_name == "key" {
return Err(Error::new_spanned(field_name, "Naming a prop `key` is not allowed because the name can conflict with the built in key attribute. See https://dioxuslabs.com/learn/0.4/reference/dynamic_rendering#rendering-lists for more information about keys"));
@ -717,6 +949,12 @@ Finally, call `.build()` to create the instance of `{name}`.
});
let reconstructing = self.included_fields().map(|f| f.name);
let FieldInfo {
name: field_name,
ty: field_type,
..
} = field;
// Add the bump lifetime to the generics
let mut ty_generics: Vec<syn::GenericArgument> = self
.generics
.params
@ -800,6 +1038,16 @@ Finally, call `.build()` to create the instance of `{name}`.
);
let repeated_fields_error_message = format!("Repeated field {field_name}");
let forward_extended_fields = self.extend_fields().map(|f| {
let name = f.name;
quote!(#name: self.#name)
});
let forward_bump = if self.extend_fields().next().is_some() {
quote!(bump: self.bump,)
} else {
quote!()
};
Ok(quote! {
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl #impl_generics #builder_name < #( #ty_generics ),* > #where_clause {
@ -809,6 +1057,8 @@ Finally, call `.build()` to create the instance of `{name}`.
let #field_name = (#arg_expr,);
let ( #(#descructuring,)* ) = self.fields;
#builder_name {
#(#forward_extended_fields,)*
#forward_bump
fields: ( #(#reconstructing,)* ),
_phantom: self._phantom,
}
@ -842,6 +1092,7 @@ Finally, call `.build()` to create the instance of `{name}`.
name: ref field_name,
..
} = field;
// Add a bump lifetime to the generics
let mut builder_generics: Vec<syn::GenericArgument> = self
.generics
.params
@ -1009,7 +1260,9 @@ Finally, call `.build()` to create the instance of `{name}`.
// reordering based on that, but for now this much simpler thing is a reasonable approach.
let assignments = self.fields.iter().map(|field| {
let name = &field.name;
if let Some(ref default) = field.builder_attr.default {
if !field.builder_attr.extends.is_empty() {
quote!(let #name = self.#name;)
} else if let Some(ref default) = field.builder_attr.default {
if field.builder_attr.skip {
quote!(let #name = #default;)
} else {

View file

@ -1,6 +1,7 @@
use crate::any_props::AnyProps;
use crate::innerlude::{
BorrowedAttributeValue, ElementPath, ElementRef, VComponent, VPlaceholder, VText,
AttributeType, BorrowedAttributeValue, ElementPath, ElementRef, MountedAttribute, VComponent,
VPlaceholder, VText,
};
use crate::mutations::Mutation;
use crate::mutations::Mutation::*;
@ -314,7 +315,7 @@ impl<'b> VirtualDom {
let id = self.assign_static_node_as_dynamic(path, root);
loop {
self.write_attribute(node, attr_id, &node.dynamic_attrs[attr_id], id);
self.write_attribute_type(node, &node.dynamic_attrs[attr_id], attr_id, id);
// Only push the dynamic attributes forward if they match the current path (same element)
match attrs.next_if(|(_, p)| *p == path) {
@ -325,25 +326,41 @@ impl<'b> VirtualDom {
}
}
fn write_attribute(
fn write_attribute_type(
&mut self,
template: &'b VNode<'b>,
vnode: &'b VNode<'b>,
attribute: &'b MountedAttribute<'b>,
idx: usize,
attribute: &'b crate::Attribute<'b>,
id: ElementId,
) {
// Make sure we set the attribute's associated id
attribute.mounted_element.set(id);
match &attribute.ty {
AttributeType::Single(attribute) => self.write_attribute(vnode, attribute, idx, id),
AttributeType::Many(attribute) => {
for attribute in *attribute {
self.write_attribute(vnode, attribute, idx, id);
}
}
}
}
pub(crate) fn write_attribute(
&mut self,
vnode: &'b VNode<'b>,
attribute: &'b crate::Attribute<'b>,
idx: usize,
id: ElementId,
) {
// Safety: we promise not to re-alias this text later on after committing it to the mutation
let unbounded_name: &str = unsafe { std::mem::transmute(attribute.name) };
match &attribute.value {
AttributeValue::Listener(_) => {
let path = &template.template.get().attr_paths[idx];
let path = &vnode.template.get().attr_paths[idx];
let element_ref = ElementRef {
path: ElementPath { path },
template: template.stable_id().unwrap(),
template: vnode.stable_id().unwrap(),
scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)),
};
self.elements[id.0] = Some(element_ref);

View file

@ -2,8 +2,8 @@ use crate::{
any_props::AnyProps,
arena::ElementId,
innerlude::{
BorrowedAttributeValue, DirtyScope, ElementPath, ElementRef, VComponent, VPlaceholder,
VText,
AttributeType, BorrowedAttributeValue, DirtyScope, ElementPath, ElementRef, VComponent,
VPlaceholder, VText,
},
mutations::Mutation,
nodes::RenderReturn,
@ -126,14 +126,54 @@ impl<'b> VirtualDom {
.dynamic_attrs
.iter()
.zip(right_template.dynamic_attrs.iter())
.for_each(|(left_attr, right_attr)| {
.enumerate()
.for_each(|(idx, (left_attr, right_attr))| {
// Move over the ID from the old to the new
let mounted_element = left_attr.mounted_element.get();
right_attr.mounted_element.set(mounted_element);
let mounted_id = left_attr.mounted_element.get();
right_attr.mounted_element.set(mounted_id);
// If the attributes are different (or volatile), we need to update them
if left_attr.value != right_attr.value || left_attr.volatile {
self.update_attribute(right_attr, left_attr);
match (&left_attr.ty, &right_attr.ty) {
(AttributeType::Single(left), AttributeType::Single(right)) => {
self.diff_attribute(left, right, mounted_id)
}
(AttributeType::Many(left), AttributeType::Many(right)) => {
let mut left_iter = left.iter().peekable();
let mut right_iter = right.iter().peekable();
loop {
match (left_iter.peek(), right_iter.peek()) {
(Some(left), Some(right)) => {
// check which name is greater
match left.name.cmp(right.name) {
std::cmp::Ordering::Less => self.remove_attribute(
left.name,
left.namespace,
mounted_id,
),
std::cmp::Ordering::Greater => self.write_attribute(
right_template,
right,
idx,
mounted_id,
),
std::cmp::Ordering::Equal => {
self.diff_attribute(left, right, mounted_id)
}
}
}
(Some(_), None) => {
let left = left_iter.next().unwrap();
self.remove_attribute(left.name, left.namespace, mounted_id)
}
(None, Some(_)) => {
let right = right_iter.next().unwrap();
self.write_attribute(right_template, right, idx, mounted_id)
}
(None, None) => break,
}
}
}
_ => unreachable!("The macro should never generate this case"),
}
});
@ -164,6 +204,18 @@ impl<'b> VirtualDom {
}
}
fn diff_attribute(
&mut self,
left_attr: &'b Attribute<'b>,
right_attr: &'b Attribute<'b>,
id: ElementId,
) {
// If the attributes are different (or volatile), we need to update them
if left_attr.value != right_attr.value || left_attr.volatile {
self.update_attribute(right_attr, left_attr, id);
}
}
fn diff_dynamic_node(
&mut self,
left_node: &'b DynamicNode<'b>,
@ -184,12 +236,29 @@ impl<'b> VirtualDom {
};
}
fn update_attribute(&mut self, right_attr: &'b Attribute<'b>, left_attr: &'b Attribute) {
fn remove_attribute(&mut self, name: &'b str, ns: Option<&'static str>, id: ElementId) {
let name = unsafe { std::mem::transmute(name) };
let value: BorrowedAttributeValue<'b> = BorrowedAttributeValue::None;
let value = unsafe { std::mem::transmute(value) };
self.mutations.push(Mutation::SetAttribute {
id,
ns,
name,
value,
});
}
fn update_attribute(
&mut self,
right_attr: &'b Attribute<'b>,
left_attr: &'b Attribute<'b>,
id: ElementId,
) {
let name = unsafe { std::mem::transmute(left_attr.name) };
let value: BorrowedAttributeValue<'b> = (&right_attr.value).into();
let value = unsafe { std::mem::transmute(value) };
self.mutations.push(Mutation::SetAttribute {
id: left_attr.mounted_element.get(),
id,
ns: right_attr.namespace,
name,
value,

View file

@ -0,0 +1 @@

View file

@ -6,7 +6,7 @@ use crate::{
use std::{
any::{Any, TypeId},
backtrace::Backtrace,
cell::RefCell,
cell::{Cell, RefCell},
error::Error,
fmt::{Debug, Display},
rc::Rc,
@ -334,10 +334,10 @@ where
}
}
}
impl<'a> Properties for ErrorBoundaryProps<'a> {
impl<'a> Properties<'a> for ErrorBoundaryProps<'a> {
type Builder = ErrorBoundaryPropsBuilder<'a, ((), ())>;
const IS_STATIC: bool = false;
fn builder() -> Self::Builder {
fn builder(_: &'a ScopeState) -> Self::Builder {
ErrorBoundaryProps::builder()
}
unsafe fn memoize(&self, _: &Self) -> bool {
@ -472,8 +472,8 @@ pub fn ErrorBoundary<'a>(cx: Scope<'a, ErrorBoundaryProps<'a>>) -> Element {
attr_paths: &[],
};
VNode {
parent: Default::default(),
stable_id: Default::default(),
parent: Cell::new(None),
stable_id: Cell::new(None),
key: None,
template: std::cell::Cell::new(TEMPLATE),
root_ids: bumpalo::collections::Vec::with_capacity_in(1usize, __cx.bump()).into(),

View file

@ -92,10 +92,10 @@ impl<'a, const A: bool> FragmentBuilder<'a, A> {
/// })
/// }
/// ```
impl<'a> Properties for FragmentProps<'a> {
impl<'a> Properties<'_> for FragmentProps<'a> {
type Builder = FragmentBuilder<'a, false>;
const IS_STATIC: bool = false;
fn builder() -> Self::Builder {
fn builder(_cx: &ScopeState) -> Self::Builder {
FragmentBuilder(None)
}
unsafe fn memoize(&self, _other: &Self) -> bool {

View file

@ -76,11 +76,11 @@ pub(crate) mod innerlude {
}
pub use crate::innerlude::{
fc_to_builder, vdom_is_rendering, AnyValue, Attribute, AttributeValue, BorrowedAttributeValue,
CapturedError, Component, DynamicNode, Element, ElementId, Event, Fragment, IntoDynNode,
LazyNodes, Mutation, Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped,
TaskId, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText,
VirtualDom,
fc_to_builder, vdom_is_rendering, AnyValue, Attribute, AttributeType, AttributeValue,
BorrowedAttributeValue, CapturedError, Component, DynamicNode, Element, ElementId, Event,
Fragment, HasAttributes, IntoDynNode, LazyNodes, MountedAttribute, Mutation, Mutations,
Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, TaskId, Template,
TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText, VirtualDom,
};
/// The purpose of this module is to alleviate imports of many common types
@ -91,10 +91,10 @@ pub mod prelude {
consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, has_context,
provide_context, provide_context_to_scope, provide_root_context, push_future,
remove_future, schedule_update_any, spawn, spawn_forever, suspend, use_error_boundary,
AnyValue, Component, Element, ErrorBoundary, Event, EventHandler, Fragment,
IntoAttributeValue, IntoDynNode, LazyNodes, Properties, Runtime, RuntimeGuard, Scope,
ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, Throw,
VNode, VirtualDom,
AnyValue, Attribute, AttributeType, Component, Element, ErrorBoundary, Event, EventHandler,
Fragment, HasAttributes, IntoAttributeValue, IntoDynNode, LazyNodes, MountedAttribute,
Properties, Runtime, RuntimeGuard, Scope, ScopeId, ScopeState, Scoped, TaskId, Template,
TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom,
};
}

View file

@ -64,7 +64,7 @@ pub struct VNode<'a> {
pub dynamic_nodes: &'a [DynamicNode<'a>],
/// The dynamic parts of the template
pub dynamic_attrs: &'a [Attribute<'a>],
pub dynamic_attrs: &'a [MountedAttribute<'a>],
}
impl<'a> VNode<'a> {
@ -92,7 +92,7 @@ impl<'a> VNode<'a> {
template: Template<'static>,
root_ids: bumpalo::collections::Vec<'a, ElementId>,
dynamic_nodes: &'a [DynamicNode<'a>],
dynamic_attrs: &'a [Attribute<'a>],
dynamic_attrs: &'a [MountedAttribute<'a>],
) -> Self {
Self {
key,
@ -445,6 +445,51 @@ pub enum TemplateAttribute<'a> {
},
}
/// An attribute with information about its position in the DOM and the element it was mounted to
#[derive(Debug)]
pub struct MountedAttribute<'a> {
pub(crate) ty: AttributeType<'a>,
/// The element in the DOM that this attribute belongs to
pub(crate) mounted_element: Cell<ElementId>,
}
impl<'a> From<Attribute<'a>> for MountedAttribute<'a> {
fn from(attr: Attribute<'a>) -> Self {
Self {
ty: AttributeType::Single(attr),
mounted_element: Default::default(),
}
}
}
impl<'a> From<&'a [Attribute<'a>]> for MountedAttribute<'a> {
fn from(attr: &'a [Attribute<'a>]) -> Self {
Self {
ty: AttributeType::Many(attr),
mounted_element: Default::default(),
}
}
}
impl<'a> From<&'a Vec<Attribute<'a>>> for MountedAttribute<'a> {
fn from(attr: &'a Vec<Attribute<'a>>) -> Self {
attr.as_slice().into()
}
}
impl<'a> MountedAttribute<'a> {
/// Get the type of this attribute
pub fn attribute_type(&self) -> &AttributeType<'a> {
&self.ty
}
/// Get the element that this attribute is mounted to
pub fn mounted_element(&self) -> ElementId {
self.mounted_element.get()
}
}
/// An attribute on a DOM node, such as `id="my-thing"` or `href="https://example.com"`
#[derive(Debug)]
pub struct Attribute<'a> {
@ -461,9 +506,6 @@ pub struct Attribute<'a> {
/// An indication of we should always try and set the attribute. Used in controlled components to ensure changes are propagated
pub volatile: bool,
/// The element in the DOM that this attribute belongs to
pub(crate) mounted_element: Cell<ElementId>,
}
impl<'a> Attribute<'a> {
@ -479,13 +521,40 @@ impl<'a> Attribute<'a> {
value,
namespace,
volatile,
mounted_element: Cell::new(ElementId::default()),
}
}
}
/// The type of an attribute
#[derive(Debug)]
pub enum AttributeType<'a> {
/// A single attribute
Single(Attribute<'a>),
/// Many different attributes sorted by name
Many(&'a [Attribute<'a>]),
}
impl<'a> AttributeType<'a> {
/// Call the given function on each attribute
pub fn for_each<'b, F>(&'b self, mut f: F)
where
F: FnMut(&'b Attribute<'a>),
{
match self {
Self::Single(attr) => f(attr),
Self::Many(attrs) => attrs.iter().for_each(f),
}
}
/// Get the element that this attribute is mounted to
pub fn mounted_element(&self) -> ElementId {
self.mounted_element.get()
/// Try to call the given function on each attribute
pub fn try_for_each<'b, F, E>(&'b self, mut f: F) -> Result<(), E>
where
F: FnMut(&'b Attribute<'a>) -> Result<(), E>,
{
match self {
Self::Single(attr) => f(attr),
Self::Many(attrs) => attrs.iter().try_for_each(f),
}
}
}
@ -870,3 +939,15 @@ impl<'a, T: IntoAttributeValue<'a>> IntoAttributeValue<'a> for Option<T> {
}
}
}
/// A trait for anything that has a dynamic list of attributes
pub trait HasAttributes<'a> {
/// Push an attribute onto the list of attributes
fn push_attribute(
self,
name: &'a str,
ns: Option<&'static str>,
attr: impl IntoAttributeValue<'a>,
volatile: bool,
) -> Self;
}

View file

@ -32,7 +32,7 @@ use crate::innerlude::*;
/// data: &'a str
/// }
/// ```
pub trait Properties: Sized {
pub trait Properties<'a>: Sized {
/// The type of the builder for this component.
/// Used to create "in-progress" versions of the props.
type Builder;
@ -41,7 +41,7 @@ pub trait Properties: Sized {
const IS_STATIC: bool;
/// Create a builder for this component.
fn builder() -> Self::Builder;
fn builder(cx: &'a ScopeState) -> Self::Builder;
/// Memoization can only happen if the props are valid for the 'static lifetime
///
@ -51,10 +51,10 @@ pub trait Properties: Sized {
unsafe fn memoize(&self, other: &Self) -> bool;
}
impl Properties for () {
impl Properties<'_> for () {
type Builder = EmptyBuilder;
const IS_STATIC: bool = true;
fn builder() -> Self::Builder {
fn builder(_cx: &ScopeState) -> Self::Builder {
EmptyBuilder {}
}
unsafe fn memoize(&self, _other: &Self) -> bool {
@ -70,8 +70,11 @@ impl EmptyBuilder {
/// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern
/// to initialize a component's props.
pub fn fc_to_builder<'a, T: Properties + 'a>(_: fn(Scope<'a, T>) -> Element<'a>) -> T::Builder {
T::builder()
pub fn fc_to_builder<'a, T: Properties<'a> + 'a>(
cx: &'a ScopeState,
_: fn(Scope<'a, T>) -> Element<'a>,
) -> T::Builder {
T::builder(cx)
}
#[cfg(not(miri))]

View file

@ -87,6 +87,16 @@ impl Runtime {
self.scope_stack.borrow().last().copied()
}
/// Call this function with the current scope set to the given scope
///
/// Useful in a limited number of scenarios, not public.
pub(crate) fn with_scope<O>(&self, id: ScopeId, f: impl FnOnce() -> O) -> O {
self.scope_stack.borrow_mut().push(id);
let o = f();
self.scope_stack.borrow_mut().pop();
o
}
/// Get the context for any scope given its ID
///
/// This is useful for inserting or removing contexts from a scope, or rendering out its root node
@ -137,6 +147,17 @@ impl RuntimeGuard {
push_runtime(runtime.clone());
Self(runtime)
}
/// Run a function with a given runtime and scope in context
pub fn with<O>(runtime: Rc<Runtime>, scope: Option<ScopeId>, f: impl FnOnce() -> O) -> O {
let guard = Self::new(runtime.clone());
let o = match scope {
Some(scope) => Runtime::with_scope(&runtime, scope, f),
None => f(),
};
drop(guard);
o
}
}
impl Drop for RuntimeGuard {

View file

@ -318,11 +318,16 @@ pub fn spawn(fut: impl Future<Output = ()> + 'static) {
with_current_scope(|cx| cx.spawn(fut));
}
/// Spawn a future on a component given its [`ScopeId`].
pub fn spawn_at(fut: impl Future<Output = ()> + 'static, scope_id: ScopeId) -> Option<TaskId> {
with_runtime(|rt| rt.get_context(scope_id).unwrap().push_future(fut))
}
/// Spawn a future that Dioxus won't clean up when this component is unmounted
///
/// This is good for tasks that need to be run after the component has been dropped.
pub fn spawn_forever(fut: impl Future<Output = ()> + 'static) -> Option<TaskId> {
with_current_scope(|cx| cx.spawn_forever(fut))
spawn_at(fut, ScopeId(0))
}
/// Informs the scheduler that this task is no longer needed and should be removed.

View file

@ -7,7 +7,8 @@ use crate::{
nodes::{IntoAttributeValue, IntoDynNode, RenderReturn},
runtime::Runtime,
scope_context::ScopeContext,
AnyValue, Attribute, AttributeValue, Element, Event, Properties, TaskId,
AnyValue, Attribute, AttributeType, AttributeValue, Element, Event, MountedAttribute,
Properties, TaskId,
};
use bumpalo::{boxed::Box as BumpBox, Bump};
use std::{
@ -350,20 +351,22 @@ impl<'src> ScopeState {
let mut listeners = self.attributes_to_drop_before_render.borrow_mut();
for attr in element.dynamic_attrs {
match attr.value {
// We need to drop listeners before the next render because they may borrow data from the borrowed props which will be dropped
AttributeValue::Listener(_) => {
let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) };
listeners.push(unbounded);
}
// We need to drop any values manually to make sure that their drop implementation is called before the next render
AttributeValue::Any(_) => {
let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) };
self.previous_frame().add_attribute_to_drop(unbounded);
}
attr.ty.for_each(|attr| {
match attr.value {
// We need to drop listeners before the next render because they may borrow data from the borrowed props which will be dropped
AttributeValue::Listener(_) => {
let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) };
listeners.push(unbounded);
}
// We need to drop any values manually to make sure that their drop implementation is called before the next render
AttributeValue::Any(_) => {
let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) };
self.previous_frame().add_attribute_to_drop(unbounded);
}
_ => (),
}
_ => (),
}
})
}
let mut props = self.borrowed_props.borrow_mut();
@ -419,13 +422,15 @@ impl<'src> ScopeState {
value: impl IntoAttributeValue<'src>,
namespace: Option<&'static str>,
volatile: bool,
) -> Attribute<'src> {
Attribute {
name,
namespace,
volatile,
) -> MountedAttribute<'src> {
MountedAttribute {
ty: AttributeType::Single(Attribute {
name,
namespace,
volatile,
value: value.into_value(self.bump()),
}),
mounted_element: Default::default(),
value: value.into_value(self.bump()),
}
}
@ -451,7 +456,7 @@ impl<'src> ScopeState {
) -> DynamicNode<'src>
where
// The properties must be valid until the next bump frame
P: Properties + 'src,
P: Properties<'src> + 'src,
// The current bump allocator frame must outlive the child's borrowed props
'src: 'child,
{

View file

@ -4,8 +4,8 @@
use crate::{
any_props::VProps,
arena::ElementId,
innerlude::{DirtyScope, ElementRef, ErrorBoundary, Mutations, Scheduler, SchedulerMsg},
arena::{ElementId, ElementRef},
innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg},
mutations::Mutation,
nodes::RenderReturn,
nodes::{Template, TemplateId},
@ -371,7 +371,6 @@ impl VirtualDom {
.get(parent_path.template.0)
.cloned()
.map(|el| (*parent_path, el));
let mut listeners = vec![];
// We will clone this later. The data itself is wrapped in RC to be used in callbacks if required
let uievent = Event {
@ -383,6 +382,8 @@ impl VirtualDom {
if bubbles {
// Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent.
while let Some((path, el_ref)) = parent_node {
let mut listeners = vec![];
// safety: we maintain references of all vnodes in the element slab
let template = unsafe { el_ref.unwrap().as_ref() };
let node_template = template.template.get();
@ -392,10 +393,14 @@ impl VirtualDom {
let this_path = node_template.attr_paths[idx];
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
if attr.name.trim_start_matches("on") == name
&& target_path.is_decendant(&this_path)
{
listeners.push(&attr.value);
if target_path.is_decendant(&this_path) {
attr.ty.for_each(|attribute| {
if attribute.name.trim_start_matches("on") == name {
if let AttributeValue::Listener(listener) = &attribute.value {
listeners.push(listener);
}
}
});
// Break if this is the exact target element.
// This means we won't call two listeners with the same name on the same element. This should be
@ -408,20 +413,18 @@ impl VirtualDom {
// Now that we've accumulated all the parent attributes for the target element, call them in reverse order
// We check the bubble state between each call to see if the event has been stopped from bubbling
for listener in listeners.drain(..).rev() {
if let AttributeValue::Listener(listener) = listener {
let origin = path.scope;
self.runtime.scope_stack.borrow_mut().push(origin);
self.runtime.rendering.set(false);
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
cb(uievent.clone());
}
self.runtime.scope_stack.borrow_mut().pop();
self.runtime.rendering.set(true);
for listener in listeners.into_iter().rev() {
let origin = path.scope;
self.runtime.scope_stack.borrow_mut().push(origin);
self.runtime.rendering.set(false);
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
cb(uievent.clone());
}
self.runtime.scope_stack.borrow_mut().pop();
self.runtime.rendering.set(true);
if !uievent.propagates.get() {
return;
}
if !uievent.propagates.get() {
return;
}
}
@ -445,18 +448,26 @@ impl VirtualDom {
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
// Only call the listener if this is the exact target element.
if attr.name.trim_start_matches("on") == name && target_path == this_path {
if let AttributeValue::Listener(listener) = &attr.value {
let origin = path.scope;
self.runtime.scope_stack.borrow_mut().push(origin);
self.runtime.rendering.set(false);
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
cb(uievent.clone());
}
self.runtime.scope_stack.borrow_mut().pop();
self.runtime.rendering.set(true);
if target_path == this_path {
let mut should_stop = false;
attr.ty.for_each(|attribute| {
if attribute.name.trim_start_matches("on") == name {
if let AttributeValue::Listener(listener) = &attribute.value {
let origin = path.scope;
self.runtime.scope_stack.borrow_mut().push(origin);
self.runtime.rendering.set(false);
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
cb(uievent.clone());
}
self.runtime.scope_stack.borrow_mut().pop();
self.runtime.rendering.set(true);
break;
should_stop = true;
}
}
});
if should_stop {
return;
}
}
}

View file

@ -1,7 +1,7 @@
#![cfg(not(miri))]
use dioxus::prelude::Props;
use dioxus_core::*;
use dioxus_core::{MountedAttribute, *};
use std::{cfg, collections::HashSet};
fn random_ns() -> Option<&'static str> {
@ -206,7 +206,7 @@ fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode {
}
}
fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute {
fn create_random_dynamic_attr(cx: &ScopeState) -> MountedAttribute {
let value = match rand::random::<u8>() % 7 {
0 => AttributeValue::Text(Box::leak(
format!("{}", rand::random::<usize>()).into_boxed_str(),
@ -218,7 +218,7 @@ fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute {
5 => AttributeValue::None,
6 => {
let value = cx.listener(|e: Event<String>| println!("{:?}", e));
return Attribute::new("ondata", value, None, false);
return Attribute::new("ondata", value, None, false).into();
}
_ => unreachable!(),
};
@ -228,6 +228,7 @@ fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute {
random_ns(),
rand::random(),
)
.into()
}
static mut TEMPLATE_COUNT: usize = 0;

View file

@ -11,15 +11,25 @@ keywords = ["dom", "ui", "gui", "react"]
[dependencies]
dioxus-core = { workspace = true, features = ["serialize"] }
dioxus-html = { workspace = true, features = ["serialize", "native-bind", "mounted", "eval"] }
dioxus-html = { workspace = true, features = [
"serialize",
"native-bind",
"mounted",
"eval",
] }
dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] }
dioxus-hot-reload = { workspace = true, optional = true }
dioxus-cli-config = { workspace = true }
serde = "1.0.136"
serde_json = "1.0.79"
thiserror = { workspace = true }
tracing.workspace = true
wry = { version = "0.34.0", default-features = false, features = ["tao", "protocol", "file-drop"] }
tracing = { workspace = true }
wry = { version = "0.35.0", default-features = false, features = [
"os-webview",
"protocol",
"file-drop",
] }
futures-channel = { workspace = true }
tokio = { workspace = true, features = [
"sync",
@ -39,12 +49,17 @@ futures-util = { workspace = true }
urlencoding = "2.1.2"
async-trait = "0.1.68"
crossbeam-channel = "0.5.8"
tao = { version = "0.24.0", features = ["rwh_05"] }
[target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux",target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies]
# This is only for debug mode, and it appears mobile does not support some packages this uses
manganis-cli-support = { git = "https://github.com/DioxusLabs/collect-assets", features = ["webp", "html"] }
manganis-cli-support = { git = "https://github.com/DioxusLabs/collect-assets", optional = true, features = [
"webp",
"html",
] }
rfd = "0.12"
global-hotkey = { git = "https://github.com/tauri-apps/global-hotkey" }
global-hotkey = "0.4.1"
muda = "0.11.3"
[target.'cfg(target_os = "ios")'.dependencies]
objc = "0.2.7"
@ -61,6 +76,7 @@ fullscreen = ["wry/fullscreen"]
transparent = ["wry/transparent"]
devtools = ["wry/devtools"]
hot-reload = ["dioxus-hot-reload"]
asset-collect = ["manganis-cli-support"]
gnu = []
[package.metadata.docs.rs]

View file

@ -2,8 +2,8 @@ use dioxus::prelude::*;
use dioxus_desktop::DesktopContext;
pub(crate) fn check_app_exits(app: Component) {
use dioxus_desktop::tao::window::WindowBuilder;
use dioxus_desktop::Config;
use tao::window::WindowBuilder;
// This is a deadman's switch to ensure that the app exits
let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
let should_panic_clone = should_panic.clone();

360
packages/desktop/src/app.rs Normal file
View file

@ -0,0 +1,360 @@
use crate::{
config::{Config, WindowCloseBehaviour},
desktop_context::WindowEventHandlers,
element::DesktopElement,
file_upload::FileDialogRequest,
ipc::IpcMessage,
ipc::{EventData, UserWindowEvent},
query::QueryResult,
shortcut::{GlobalHotKeyEvent, ShortcutRegistry},
webview::WebviewInstance,
};
use crossbeam_channel::Receiver;
use dioxus_core::{Component, ElementId, VirtualDom};
use dioxus_html::{
native_bind::NativeFileEngine, FileEngine, HasFileData, HasFormData, HtmlEvent,
PlatformEventData,
};
use std::{
cell::{Cell, RefCell},
collections::HashMap,
rc::Rc,
sync::Arc,
};
use tao::{
event::Event,
event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget},
window::WindowId,
};
/// The single top-level object that manages all the running windows, assets, shortcuts, etc
pub(crate) struct App<P> {
// move the props into a cell so we can pop it out later to create the first window
// iOS panics if we create a window before the event loop is started, so we toss them into a cell
pub(crate) props: Cell<Option<P>>,
pub(crate) cfg: Cell<Option<Config>>,
// Stuff we need mutable access to
pub(crate) root: Component<P>,
pub(crate) control_flow: ControlFlow,
pub(crate) is_visible_before_start: bool,
pub(crate) window_behavior: WindowCloseBehaviour,
pub(crate) webviews: HashMap<WindowId, WebviewInstance>,
/// This single blob of state is shared between all the windows so they have access to the runtime state
///
/// This includes stuff like the event handlers, shortcuts, etc as well as ways to modify *other* windows
pub(crate) shared: Rc<SharedContext>,
}
/// A bundle of state shared between all the windows, providing a way for us to communicate with running webview.
///
/// Todo: everything in this struct is wrapped in Rc<>, but we really only need the one top-level refcell
pub struct SharedContext {
pub(crate) event_handlers: WindowEventHandlers,
pub(crate) pending_webviews: RefCell<Vec<WebviewInstance>>,
pub(crate) shortcut_manager: ShortcutRegistry,
pub(crate) global_hotkey_channel: Receiver<GlobalHotKeyEvent>,
pub(crate) proxy: EventLoopProxy<UserWindowEvent>,
pub(crate) target: EventLoopWindowTarget<UserWindowEvent>,
}
impl<P: 'static> App<P> {
pub fn new(cfg: Config, props: P, root: Component<P>) -> (EventLoop<UserWindowEvent>, Self) {
let event_loop = EventLoopBuilder::<UserWindowEvent>::with_user_event().build();
let mut app = Self {
root,
window_behavior: cfg.last_window_close_behaviour,
is_visible_before_start: true,
webviews: HashMap::new(),
control_flow: ControlFlow::Wait,
props: Cell::new(Some(props)),
cfg: Cell::new(Some(cfg)),
shared: Rc::new(SharedContext {
event_handlers: WindowEventHandlers::default(),
pending_webviews: Default::default(),
shortcut_manager: ShortcutRegistry::new(),
global_hotkey_channel: GlobalHotKeyEvent::receiver().clone(),
proxy: event_loop.create_proxy(),
target: event_loop.clone(),
}),
};
// Copy over any assets we find
// todo - re-enable this when we have a faster way of copying assets
#[cfg(feature = "collect-assets")]
crate::collect_assets::copy_assets();
// Set the event converter
dioxus_html::set_event_converter(Box::new(crate::events::SerializedHtmlEventConverter));
// Allow hotreloading to work - but only in debug mode
#[cfg(all(feature = "hot-reload", debug_assertions))]
app.connect_hotreload();
(event_loop, app)
}
pub fn tick(&mut self, window_event: &Event<'_, UserWindowEvent>) {
self.control_flow = ControlFlow::Wait;
self.shared
.event_handlers
.apply_event(window_event, &self.shared.target);
if let Ok(event) = self.shared.global_hotkey_channel.try_recv() {
self.shared.shortcut_manager.call_handlers(event);
}
}
#[cfg(all(feature = "hot-reload", debug_assertions))]
pub fn connect_hotreload(&mut self) {
dioxus_hot_reload::connect({
let proxy = self.shared.proxy.clone();
move |template| {
let _ = proxy.send_event(UserWindowEvent(
EventData::HotReloadEvent(template),
unsafe { WindowId::dummy() },
));
}
});
}
pub fn handle_new_window(&mut self) {
for handler in self.shared.pending_webviews.borrow_mut().drain(..) {
let id = handler.desktop_context.window.id();
self.webviews.insert(id, handler);
_ = self
.shared
.proxy
.send_event(UserWindowEvent(EventData::Poll, id));
}
}
pub fn handle_close_requested(&mut self, id: WindowId) {
use WindowCloseBehaviour::*;
match self.window_behavior {
LastWindowExitsApp => {
self.webviews.remove(&id);
if self.webviews.is_empty() {
self.control_flow = ControlFlow::Exit
}
}
LastWindowHides => {
let Some(webview) = self.webviews.get(&id) else {
return;
};
hide_app_window(&webview.desktop_context.webview);
}
CloseWindow => {
self.webviews.remove(&id);
}
}
}
pub fn window_destroyed(&mut self, id: WindowId) {
self.webviews.remove(&id);
if matches!(
self.window_behavior,
WindowCloseBehaviour::LastWindowExitsApp
) && self.webviews.is_empty()
{
self.control_flow = ControlFlow::Exit
}
}
pub fn handle_start_cause_init(&mut self) {
let props = self.props.take().unwrap();
let cfg = self.cfg.take().unwrap();
self.is_visible_before_start = cfg.window.window.visible;
let webview = WebviewInstance::new(
cfg,
VirtualDom::new_with_props(self.root, props),
self.shared.clone(),
);
let id = webview.desktop_context.window.id();
self.webviews.insert(id, webview);
_ = self
.shared
.proxy
.send_event(UserWindowEvent(EventData::Poll, id));
}
pub fn handle_browser_open(&mut self, msg: IpcMessage) {
if let Some(temp) = msg.params().as_object() {
if temp.contains_key("href") {
let open = webbrowser::open(temp["href"].as_str().unwrap());
if let Err(e) = open {
tracing::error!("Open Browser error: {:?}", e);
}
}
}
}
pub fn handle_initialize_msg(&mut self, id: WindowId) {
let view = self.webviews.get_mut(&id).unwrap();
view.desktop_context.send_edits(view.dom.rebuild());
view.desktop_context
.window
.set_visible(self.is_visible_before_start);
}
pub fn handle_close_msg(&mut self, id: WindowId) {
self.webviews.remove(&id);
if self.webviews.is_empty() {
self.control_flow = ControlFlow::Exit
}
}
pub fn handle_query_msg(&mut self, msg: IpcMessage, id: WindowId) {
let Ok(result) = serde_json::from_value::<QueryResult>(msg.params()) else {
return;
};
let Some(view) = self.webviews.get(&id) else {
return;
};
view.desktop_context.query.send(result);
}
pub fn handle_user_event_msg(&mut self, msg: IpcMessage, id: WindowId) {
let parsed_params = serde_json::from_value(msg.params())
.map_err(|err| tracing::error!("Error parsing user_event: {:?}", err));
let Ok(evt) = parsed_params else { return };
let HtmlEvent {
element,
name,
bubbles,
data,
} = evt;
let view = self.webviews.get_mut(&id).unwrap();
let query = view.desktop_context.query.clone();
// check for a mounted event placeholder and replace it with a desktop specific element
let as_any = match data {
dioxus_html::EventData::Mounted => {
let element = DesktopElement::new(element, view.desktop_context.clone(), query);
Rc::new(PlatformEventData::new(Box::new(element)))
}
_ => data.into_any(),
};
view.dom.handle_event(&name, as_any, element, bubbles);
view.desktop_context.send_edits(view.dom.render_immediate());
}
#[cfg(all(feature = "hot-reload", debug_assertions))]
pub fn handle_hot_reload_msg(&mut self, msg: dioxus_hot_reload::HotReloadMsg) {
match msg {
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => {
for webview in self.webviews.values_mut() {
webview.dom.replace_template(template);
webview.poll_vdom();
}
}
dioxus_hot_reload::HotReloadMsg::Shutdown => {
self.control_flow = ControlFlow::Exit;
}
}
}
pub fn handle_file_dialog_msg(&mut self, msg: IpcMessage, window: WindowId) {
let Ok(file_dialog) = serde_json::from_value::<FileDialogRequest>(msg.params()) else {
return;
};
struct DesktopFileUploadForm {
files: Arc<NativeFileEngine>,
}
impl HasFileData for DesktopFileUploadForm {
fn files(&self) -> Option<Arc<dyn FileEngine>> {
Some(self.files.clone())
}
}
impl HasFormData for DesktopFileUploadForm {
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
let id = ElementId(file_dialog.target);
let event_name = &file_dialog.event;
let event_bubbles = file_dialog.bubbles;
let files = file_dialog.get_file_event();
let data = Rc::new(PlatformEventData::new(Box::new(DesktopFileUploadForm {
files: Arc::new(NativeFileEngine::new(files)),
})));
let view = self.webviews.get_mut(&window).unwrap();
if event_name == "change&input" {
view.dom
.handle_event("input", data.clone(), id, event_bubbles);
view.dom.handle_event("change", data, id, event_bubbles);
} else {
view.dom.handle_event(event_name, data, id, event_bubbles);
}
view.desktop_context.send_edits(view.dom.render_immediate());
}
/// Poll the virtualdom until it's pending
///
/// The waker we give it is connected to the event loop, so it will wake up the event loop when it's ready to be polled again
///
/// All IO is done on the tokio runtime we started earlier
pub fn poll_vdom(&mut self, id: WindowId) {
let Some(view) = self.webviews.get_mut(&id) else {
return;
};
view.poll_vdom();
}
}
/// Different hide implementations per platform
#[allow(unused)]
pub fn hide_app_window(window: &wry::WebView) {
#[cfg(target_os = "windows")]
{
use tao::platform::windows::WindowExtWindows;
window.set_visible(false);
// window.set_skip_taskbar(true);
}
#[cfg(target_os = "linux")]
{
use tao::platform::unix::WindowExtUnix;
window.set_visible(false);
}
#[cfg(target_os = "macos")]
{
// window.set_visible(false); has the wrong behaviour on macOS
// It will hide the window but not show it again when the user switches
// back to the app. `NSApplication::hide:` has the correct behaviour
use objc::runtime::Object;
use objc::{msg_send, sel, sel_impl};
objc::rc::autoreleasepool(|| unsafe {
let app: *mut Object = msg_send![objc::class!(NSApplication), sharedApplication];
let nil = std::ptr::null_mut::<Object>();
let _: () = msg_send![app, hide: nil];
});
}
}

View file

@ -0,0 +1,61 @@
use dioxus_core::prelude::{Runtime, RuntimeGuard, ScopeId};
use rustc_hash::FxHashMap;
use std::{cell::RefCell, rc::Rc};
use wry::{http::Request, RequestAsyncResponder};
///
pub type AssetRequest = Request<Vec<u8>>;
pub struct AssetHandler {
f: Box<dyn Fn(AssetRequest, RequestAsyncResponder) + 'static>,
scope: ScopeId,
}
#[derive(Clone)]
pub struct AssetHandlerRegistry {
dom_rt: Rc<Runtime>,
handlers: Rc<RefCell<FxHashMap<String, AssetHandler>>>,
}
impl AssetHandlerRegistry {
pub fn new(dom_rt: Rc<Runtime>) -> Self {
AssetHandlerRegistry {
dom_rt,
handlers: Default::default(),
}
}
pub fn has_handler(&self, name: &str) -> bool {
self.handlers.borrow().contains_key(name)
}
pub fn handle_request(
&self,
name: &str,
request: AssetRequest,
responder: RequestAsyncResponder,
) {
if let Some(handler) = self.handlers.borrow().get(name) {
// make sure the runtime is alive for the duration of the handler
// We should do this for all the things - not just asset handlers
RuntimeGuard::with(self.dom_rt.clone(), Some(handler.scope), || {
(handler.f)(request, responder)
});
}
}
pub fn register_handler(
&self,
name: String,
f: Box<dyn Fn(AssetRequest, RequestAsyncResponder) + 'static>,
scope: ScopeId,
) {
self.handlers
.borrow_mut()
.insert(name, AssetHandler { f, scope });
}
pub fn remove_handler(&self, name: &str) -> Option<AssetHandler> {
self.handlers.borrow_mut().remove(name)
}
}

View file

@ -1,15 +1,13 @@
use std::borrow::Cow;
use std::path::PathBuf;
use wry::application::window::Icon;
use dioxus_core::prelude::Component;
use tao::window::{Icon, WindowBuilder, WindowId};
use wry::{
application::window::{Window, WindowBuilder},
http::{Request as HttpRequest, Response as HttpResponse},
webview::FileDropEvent,
FileDropEvent,
};
// pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView);
/// The behaviour of the application when the last window is closed.
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum WindowCloseBehaviour {
@ -38,7 +36,7 @@ pub struct Config {
pub(crate) enable_default_menu_bar: bool,
}
type DropHandler = Box<dyn Fn(&Window, FileDropEvent) -> bool>;
type DropHandler = Box<dyn Fn(WindowId, FileDropEvent) -> bool>;
pub(crate) type WryProtocol = (
String,
@ -49,7 +47,12 @@ impl Config {
/// Initializes a new `WindowBuilder` with default values.
#[inline]
pub fn new() -> Self {
let window = WindowBuilder::new().with_title("Dioxus app");
let window = WindowBuilder::new().with_title(
dioxus_cli_config::CURRENT_CONFIG
.as_ref()
.map(|c| c.dioxus_config.application.name.clone())
.unwrap_or("Dioxus App".to_string()),
);
Self {
// event_handler: None,
@ -69,6 +72,20 @@ impl Config {
}
}
/// Launch a Dioxus app using the given component and config
///
/// See the [`crate::launch::launch`] function for more details.
pub fn launch(self, root: Component<()>) {
crate::launch::launch_cfg(root, self)
}
/// Launch a Dioxus app using the given component, config, and props
///
/// See the [`crate::launch::launch_with_props`] function for more details.
pub fn launch_with_props<P: 'static>(self, root: Component<P>, props: P) {
crate::launch::launch_with_props(root, props, self)
}
/// Set whether the default menu bar should be enabled.
///
/// > Note: `enable` is `true` by default. To disable the default menu bar pass `false`.
@ -117,10 +134,10 @@ impl Config {
self
}
/// Set a file drop handler
/// Set a file drop handler. If this is enabled, html drag events will be disabled.
pub fn with_file_drop_handler(
mut self,
handler: impl Fn(&Window, FileDropEvent) -> bool + 'static,
handler: impl Fn(WindowId, FileDropEvent) -> bool + 'static,
) -> Self {
self.file_drop_handler = Some(Box::new(handler));
self

View file

@ -1,38 +1,30 @@
use crate::create_new_window;
use crate::events::IpcMessage;
use crate::protocol::AssetFuture;
use crate::protocol::AssetHandlerRegistry;
use crate::query::QueryEngine;
use crate::shortcut::{HotKey, ShortcutId, ShortcutRegistry, ShortcutRegistryError};
use crate::AssetHandler;
use crate::Config;
use crate::WebviewHandler;
use dioxus_core::ScopeState;
use dioxus_core::VirtualDom;
#[cfg(all(feature = "hot-reload", debug_assertions))]
use dioxus_hot_reload::HotReloadMsg;
use crate::{
app::SharedContext,
assets::AssetHandlerRegistry,
edits::EditQueue,
ipc::{EventData, UserWindowEvent},
query::QueryEngine,
shortcut::{HotKey, ShortcutId, ShortcutRegistryError},
webview::WebviewInstance,
AssetRequest, Config,
};
use dioxus_core::{
prelude::{current_scope_id, ScopeId},
Mutations, VirtualDom,
};
use dioxus_interpreter_js::binary_protocol::Channel;
use rustc_hash::FxHashMap;
use slab::Slab;
use std::cell::RefCell;
use std::fmt::Debug;
use std::fmt::Formatter;
use std::rc::Rc;
use std::rc::Weak;
use std::sync::atomic::AtomicU16;
use std::sync::Arc;
use std::sync::Mutex;
use wry::application::event::Event;
use wry::application::event_loop::EventLoopProxy;
use wry::application::event_loop::EventLoopWindowTarget;
#[cfg(target_os = "ios")]
use wry::application::platform::ios::WindowExtIOS;
use wry::application::window::Fullscreen as WryFullscreen;
use wry::application::window::Window;
use wry::application::window::WindowId;
use wry::webview::WebView;
use std::{cell::RefCell, fmt::Debug, rc::Rc, rc::Weak, sync::atomic::AtomicU16};
use tao::{
event::Event,
event_loop::EventLoopWindowTarget,
window::{Fullscreen as WryFullscreen, Window, WindowId},
};
use wry::{RequestAsyncResponder, WebView};
pub type ProxyType = EventLoopProxy<UserWindowEvent>;
#[cfg(target_os = "ios")]
use tao::platform::ios::WindowExtIOS;
/// Get an imperative handle to the current window without using a hook
///
@ -43,54 +35,8 @@ pub fn window() -> DesktopContext {
dioxus_core::prelude::consume_context().unwrap()
}
/// Get an imperative handle to the current window
#[deprecated = "Prefer the using the `window` function directly for cleaner code"]
pub fn use_window(cx: &ScopeState) -> &DesktopContext {
cx.use_hook(|| cx.consume_context::<DesktopContext>())
.as_ref()
.unwrap()
}
/// This handles communication between the requests that the webview makes and the interpreter. The interpreter constantly makes long running requests to the webview to get any edits that should be made to the DOM almost like server side events.
/// It will hold onto the requests until the interpreter is ready to handle them and hold onto any pending edits until a new request is made.
#[derive(Default, Clone)]
pub(crate) struct EditQueue {
queue: Arc<Mutex<Vec<Vec<u8>>>>,
responder: Arc<Mutex<Option<wry::webview::RequestAsyncResponder>>>,
}
impl Debug for EditQueue {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EditQueue")
.field("queue", &self.queue)
.field("responder", {
&self.responder.lock().unwrap().as_ref().map(|_| ())
})
.finish()
}
}
impl EditQueue {
pub fn handle_request(&self, responder: wry::webview::RequestAsyncResponder) {
let mut queue = self.queue.lock().unwrap();
if let Some(bytes) = queue.pop() {
responder.respond(wry::http::Response::new(bytes));
} else {
*self.responder.lock().unwrap() = Some(responder);
}
}
pub fn add_edits(&self, edits: Vec<u8>) {
let mut responder = self.responder.lock().unwrap();
if let Some(responder) = responder.take() {
responder.respond(wry::http::Response::new(edits));
} else {
self.queue.lock().unwrap().push(edits);
}
}
}
pub(crate) type WebviewQueue = Rc<RefCell<Vec<WebviewHandler>>>;
/// A handle to the [`DesktopService`] that can be passed around.
pub type DesktopContext = Rc<DesktopService>;
/// An imperative interface to the current window.
///
@ -106,26 +52,18 @@ pub(crate) type WebviewQueue = Rc<RefCell<Vec<WebviewHandler>>>;
/// ```
pub struct DesktopService {
/// The wry/tao proxy to the current window
pub webview: Rc<WebView>,
pub webview: WebView,
/// The proxy to the event loop
pub proxy: ProxyType,
/// The tao window itself
pub window: Window,
pub(crate) shared: Rc<SharedContext>,
/// The receiver for queries about the current window
pub(super) query: QueryEngine,
pub(super) pending_windows: WebviewQueue,
pub(crate) event_loop: EventLoopWindowTarget<UserWindowEvent>,
pub(crate) event_handlers: WindowEventHandlers,
pub(crate) shortcut_manager: ShortcutRegistry,
pub(crate) edit_queue: EditQueue,
pub(crate) templates: RefCell<FxHashMap<String, u16>>,
pub(crate) max_template_count: AtomicU16,
pub(crate) channel: RefCell<Channel>,
pub(crate) asset_handlers: AssetHandlerRegistry,
@ -133,47 +71,50 @@ pub struct DesktopService {
pub(crate) views: Rc<RefCell<Vec<*mut objc::runtime::Object>>>,
}
/// A handle to the [`DesktopService`] that can be passed around.
pub type DesktopContext = Rc<DesktopService>;
/// A smart pointer to the current window.
impl std::ops::Deref for DesktopService {
type Target = Window;
fn deref(&self) -> &Self::Target {
self.webview.window()
&self.window
}
}
impl DesktopService {
pub(crate) fn new(
webview: WebView,
proxy: ProxyType,
event_loop: EventLoopWindowTarget<UserWindowEvent>,
webviews: WebviewQueue,
event_handlers: WindowEventHandlers,
shortcut_manager: ShortcutRegistry,
window: Window,
shared: Rc<SharedContext>,
edit_queue: EditQueue,
asset_handlers: AssetHandlerRegistry,
) -> Self {
Self {
webview: Rc::new(webview),
proxy,
event_loop,
query: Default::default(),
pending_windows: webviews,
event_handlers,
shortcut_manager,
window,
webview,
shared,
edit_queue,
asset_handlers,
query: Default::default(),
templates: Default::default(),
max_template_count: Default::default(),
channel: Default::default(),
asset_handlers,
#[cfg(target_os = "ios")]
views: Default::default(),
}
}
/// Send a list of mutations to the webview
pub(crate) fn send_edits(&self, edits: Mutations) {
if let Some(bytes) = crate::edits::apply_edits(
edits,
&mut self.channel.borrow_mut(),
&mut self.templates.borrow_mut(),
&self.max_template_count,
) {
self.edit_queue.add_edits(bytes)
}
}
/// Create a new window using the props and window builder
///
/// Returns the webview handle for the new window.
@ -182,35 +123,23 @@ impl DesktopService {
///
/// Be careful to not create a cycle of windows, or you might leak memory.
pub fn new_window(&self, dom: VirtualDom, cfg: Config) -> Weak<DesktopService> {
let window = create_new_window(
cfg,
&self.event_loop,
&self.proxy,
dom,
&self.pending_windows,
&self.event_handlers,
self.shortcut_manager.clone(),
);
let window = WebviewInstance::new(cfg, dom, self.shared.clone());
let desktop_context = window
.dom
.base_scope()
.consume_context::<Rc<DesktopService>>()
let cx = window.desktop_context.clone();
self.shared
.proxy
.send_event(UserWindowEvent(EventData::NewWindow, cx.id()))
.unwrap();
let id = window.desktop_context.webview.window().id();
self.proxy
.send_event(UserWindowEvent(EventData::NewWindow, id))
self.shared
.proxy
.send_event(UserWindowEvent(EventData::Poll, cx.id()))
.unwrap();
self.proxy
.send_event(UserWindowEvent(EventData::Poll, id))
.unwrap();
self.shared.pending_webviews.borrow_mut().push(window);
self.pending_windows.borrow_mut().push(window);
Rc::downgrade(&desktop_context)
Rc::downgrade(&cx)
}
/// trigger the drag-window event
@ -222,41 +151,38 @@ impl DesktopService {
/// onmousedown: move |_| { desktop.drag_window(); }
/// ```
pub fn drag(&self) {
let window = self.webview.window();
// if the drag_window has any errors, we don't do anything
if window.fullscreen().is_none() {
window.drag_window().unwrap();
if self.window.fullscreen().is_none() {
_ = self.window.drag_window();
}
}
/// Toggle whether the window is maximized or not
pub fn toggle_maximized(&self) {
let window = self.webview.window();
window.set_maximized(!window.is_maximized())
self.window.set_maximized(!self.window.is_maximized())
}
/// close window
/// Close this window
pub fn close(&self) {
let _ = self
.shared
.proxy
.send_event(UserWindowEvent(EventData::CloseWindow, self.id()));
}
/// close window
/// Close a particular window, given its ID
pub fn close_window(&self, id: WindowId) {
let _ = self
.shared
.proxy
.send_event(UserWindowEvent(EventData::CloseWindow, id));
}
/// change window to fullscreen
pub fn set_fullscreen(&self, fullscreen: bool) {
if let Some(handle) = self.webview.window().current_monitor() {
self.webview
.window()
.set_fullscreen(fullscreen.then_some(WryFullscreen::Borderless(Some(handle))));
if let Some(handle) = &self.window.current_monitor() {
self.window.set_fullscreen(
fullscreen.then_some(WryFullscreen::Borderless(Some(handle.clone()))),
);
}
}
@ -289,12 +215,12 @@ impl DesktopService {
&self,
handler: impl FnMut(&Event<UserWindowEvent>, &EventLoopWindowTarget<UserWindowEvent>) + 'static,
) -> WryEventHandlerId {
self.event_handlers.add(self.id(), handler)
self.shared.event_handlers.add(self.window.id(), handler)
}
/// Remove a wry event handler created with [`DesktopContext::create_wry_event_handler`]
pub fn remove_wry_event_handler(&self, id: WryEventHandlerId) {
self.event_handlers.remove(id)
self.shared.event_handlers.remove(id)
}
/// Create a global shortcut
@ -305,38 +231,52 @@ impl DesktopService {
hotkey: HotKey,
callback: impl FnMut() + 'static,
) -> Result<ShortcutId, ShortcutRegistryError> {
self.shortcut_manager
self.shared
.shortcut_manager
.add_shortcut(hotkey, Box::new(callback))
}
/// Remove a global shortcut
pub fn remove_shortcut(&self, id: ShortcutId) {
self.shortcut_manager.remove_shortcut(id)
self.shared.shortcut_manager.remove_shortcut(id)
}
/// Remove all global shortcuts
pub fn remove_all_shortcuts(&self) {
self.shortcut_manager.remove_all()
self.shared.shortcut_manager.remove_all()
}
/// Provide a callback to handle asset loading yourself.
/// If the ScopeId isn't provided, defaults to a global handler.
/// Note that the handler is namespaced by name, not ScopeId.
///
/// When the component is dropped, the handler is removed.
///
/// See [`use_asset_handle`](crate::use_asset_handle) for a convenient hook.
pub async fn register_asset_handler<F: AssetFuture>(&self, f: impl AssetHandler<F>) -> usize {
self.asset_handlers.register_handler(f).await
pub fn register_asset_handler(
&self,
name: String,
f: Box<dyn Fn(AssetRequest, RequestAsyncResponder) + 'static>,
scope: Option<ScopeId>,
) {
self.asset_handlers.register_handler(
name,
f,
scope.unwrap_or(current_scope_id().unwrap_or(ScopeId(0))),
)
}
/// Removes an asset handler by its identifier.
///
/// Returns `None` if the handler did not exist.
pub async fn remove_asset_handler(&self, id: usize) -> Option<()> {
self.asset_handlers.remove_handler(id).await
pub fn remove_asset_handler(&self, name: &str) -> Option<()> {
self.asset_handlers.remove_handler(name).map(|_| ())
}
/// Push an objc view to the window
#[cfg(target_os = "ios")]
pub fn push_view(&self, view: objc_id::ShareId<objc::runtime::Object>) {
let window = self.webview.window();
let window = &self.window;
unsafe {
use objc::runtime::Object;
@ -356,7 +296,7 @@ impl DesktopService {
/// Pop an objc view from the window
#[cfg(target_os = "ios")]
pub fn pop_view(&self) {
let window = self.webview.window();
let window = &self.window;
unsafe {
use objc::runtime::Object;
@ -370,23 +310,6 @@ impl DesktopService {
}
}
#[derive(Debug, Clone)]
pub struct UserWindowEvent(pub EventData, pub WindowId);
#[derive(Debug, Clone)]
pub enum EventData {
Poll,
Ipc(IpcMessage),
#[cfg(all(feature = "hot-reload", debug_assertions))]
HotReloadEvent(HotReloadMsg),
NewWindow,
CloseWindow,
}
#[cfg(target_os = "ios")]
fn is_main_thread() -> bool {
use objc::runtime::{Class, BOOL, NO};
@ -461,28 +384,11 @@ impl WryWindowEventHandlerInner {
}
}
/// Get a closure that executes any JavaScript in the WebView context.
pub fn use_wry_event_handler(
cx: &ScopeState,
handler: impl FnMut(&Event<UserWindowEvent>, &EventLoopWindowTarget<UserWindowEvent>) + 'static,
) -> &WryEventHandler {
cx.use_hook(move || {
let desktop = window();
let id = desktop.create_wry_event_handler(handler);
WryEventHandler {
handlers: desktop.event_handlers.clone(),
id,
}
})
}
/// A wry event handler that is scoped to the current component and window. The event handler will only receive events for the window it was created for and global events.
///
/// This will automatically be removed when the component is unmounted.
pub struct WryEventHandler {
handlers: WindowEventHandlers,
pub(crate) handlers: WindowEventHandlers,
/// The unique identifier of the event handler.
pub id: WryEventHandlerId,
}

View file

@ -0,0 +1,172 @@
use dioxus_core::{BorrowedAttributeValue, Mutations, Template, TemplateAttribute, TemplateNode};
use dioxus_html::event_bubbles;
use dioxus_interpreter_js::binary_protocol::Channel;
use rustc_hash::FxHashMap;
use std::{
sync::atomic::AtomicU16,
sync::Arc,
sync::{atomic::Ordering, Mutex},
};
use wry::RequestAsyncResponder;
/// This handles communication between the requests that the webview makes and the interpreter. The interpreter
/// constantly makes long running requests to the webview to get any edits that should be made to the DOM almost like
/// server side events.
///
/// It will hold onto the requests until the interpreter is ready to handle them and hold onto any pending edits until
/// a new request is made.
#[derive(Default, Clone)]
pub(crate) struct EditQueue {
queue: Arc<Mutex<Vec<Vec<u8>>>>,
responder: Arc<Mutex<Option<RequestAsyncResponder>>>,
}
impl EditQueue {
pub fn handle_request(&self, responder: RequestAsyncResponder) {
let mut queue = self.queue.lock().unwrap();
if let Some(bytes) = queue.pop() {
responder.respond(wry::http::Response::new(bytes));
} else {
*self.responder.lock().unwrap() = Some(responder);
}
}
pub fn add_edits(&self, edits: Vec<u8>) {
let mut responder = self.responder.lock().unwrap();
if let Some(responder) = responder.take() {
responder.respond(wry::http::Response::new(edits));
} else {
self.queue.lock().unwrap().push(edits);
}
}
}
pub(crate) fn apply_edits(
mutations: Mutations,
channel: &mut Channel,
templates: &mut FxHashMap<String, u16>,
max_template_count: &AtomicU16,
) -> Option<Vec<u8>> {
if mutations.templates.is_empty() && mutations.edits.is_empty() {
return None;
}
for template in mutations.templates {
add_template(&template, channel, templates, max_template_count);
}
use dioxus_core::Mutation::*;
for edit in mutations.edits {
match edit {
AppendChildren { id, m } => channel.append_children(id.0 as u32, m as u16),
AssignId { path, id } => channel.assign_id(path, id.0 as u32),
CreatePlaceholder { id } => channel.create_placeholder(id.0 as u32),
CreateTextNode { value, id } => channel.create_text_node(value, id.0 as u32),
HydrateText { path, value, id } => channel.hydrate_text(path, value, id.0 as u32),
LoadTemplate { name, index, id } => {
if let Some(tmpl_id) = templates.get(name) {
channel.load_template(*tmpl_id, index as u16, id.0 as u32)
}
}
ReplaceWith { id, m } => channel.replace_with(id.0 as u32, m as u16),
ReplacePlaceholder { path, m } => channel.replace_placeholder(path, m as u16),
InsertAfter { id, m } => channel.insert_after(id.0 as u32, m as u16),
InsertBefore { id, m } => channel.insert_before(id.0 as u32, m as u16),
SetAttribute {
name,
value,
id,
ns,
} => match value {
BorrowedAttributeValue::Text(txt) => {
channel.set_attribute(id.0 as u32, name, txt, ns.unwrap_or_default())
}
BorrowedAttributeValue::Float(f) => {
channel.set_attribute(id.0 as u32, name, &f.to_string(), ns.unwrap_or_default())
}
BorrowedAttributeValue::Int(n) => {
channel.set_attribute(id.0 as u32, name, &n.to_string(), ns.unwrap_or_default())
}
BorrowedAttributeValue::Bool(b) => channel.set_attribute(
id.0 as u32,
name,
if b { "true" } else { "false" },
ns.unwrap_or_default(),
),
BorrowedAttributeValue::None => {
channel.remove_attribute(id.0 as u32, name, ns.unwrap_or_default())
}
_ => unreachable!(),
},
SetText { value, id } => channel.set_text(id.0 as u32, value),
NewEventListener { name, id, .. } => {
channel.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8)
}
RemoveEventListener { name, id } => {
channel.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8)
}
Remove { id } => channel.remove(id.0 as u32),
PushRoot { id } => channel.push_root(id.0 as u32),
}
}
let bytes: Vec<_> = channel.export_memory().collect();
channel.reset();
Some(bytes)
}
pub fn add_template(
template: &Template<'static>,
channel: &mut Channel,
templates: &mut FxHashMap<String, u16>,
max_template_count: &AtomicU16,
) {
let current_max_template_count = max_template_count.load(Ordering::Relaxed);
for root in template.roots.iter() {
create_template_node(channel, root);
templates.insert(template.name.to_owned(), current_max_template_count);
}
channel.add_templates(current_max_template_count, template.roots.len() as u16);
max_template_count.fetch_add(1, Ordering::Relaxed);
}
pub fn create_template_node(channel: &mut Channel, node: &'static TemplateNode<'static>) {
use TemplateNode::*;
match node {
Element {
tag,
namespace,
attrs,
children,
..
} => {
// Push the current node onto the stack
match namespace {
Some(ns) => channel.create_element_ns(tag, ns),
None => channel.create_element(tag),
}
// Set attributes on the current node
for attr in *attrs {
if let TemplateAttribute::Static {
name,
value,
namespace,
} = attr
{
channel.set_top_attribute(name, value, namespace.unwrap_or_default())
}
}
// Add each child to the stack
for child in *children {
create_template_node(channel, child);
}
// Add all children to the parent
channel.append_children_to_top(children.len() as u16);
}
Text { text } => channel.create_raw_text(text),
DynamicText { .. } => channel.create_raw_text("p"),
Dynamic { .. } => channel.add_placeholder(),
}
}

View file

@ -1,21 +1,19 @@
#![allow(clippy::await_holding_refcell_ref)]
use async_trait::async_trait;
use dioxus_core::ScopeState;
use dioxus_html::prelude::{EvalError, EvalProvider, Evaluator};
use std::{cell::RefCell, rc::Rc};
use crate::{query::Query, DesktopContext};
/// Provides the DesktopEvalProvider through [`cx.provide_context`].
pub fn init_eval(cx: &ScopeState) {
let desktop_ctx = cx.consume_context::<DesktopContext>().unwrap();
let provider: Rc<dyn EvalProvider> = Rc::new(DesktopEvalProvider { desktop_ctx });
cx.provide_context(provider);
}
/// Reprents the desktop-target's provider of evaluators.
pub struct DesktopEvalProvider {
desktop_ctx: DesktopContext,
pub(crate) desktop_ctx: DesktopContext,
}
impl DesktopEvalProvider {
pub fn new(desktop_ctx: DesktopContext) -> Self {
Self { desktop_ctx }
}
}
impl EvalProvider for DesktopEvalProvider {

View file

@ -1,25 +1,7 @@
//! Convert a serialized event to an event trigger
use dioxus_html::*;
use serde::{Deserialize, Serialize};
use crate::element::DesktopElement;
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct IpcMessage {
method: String,
params: serde_json::Value,
}
impl IpcMessage {
pub(crate) fn method(&self) -> &str {
self.method.as_str()
}
pub(crate) fn params(self) -> serde_json::Value {
self.params
}
}
use dioxus_html::*;
pub(crate) struct SerializedHtmlEventConverter;

View file

@ -14,74 +14,77 @@ pub(crate) struct FileDialogRequest {
pub bubbles: bool,
}
#[cfg(not(any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)))]
pub(crate) fn get_file_event(_request: &FileDialogRequest) -> Vec<PathBuf> {
vec![]
}
#[allow(unused)]
impl FileDialogRequest {
#[cfg(not(any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)))]
pub(crate) fn get_file_event(&self) -> Vec<PathBuf> {
vec![]
}
#[cfg(any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
pub(crate) fn get_file_event(request: &FileDialogRequest) -> Vec<PathBuf> {
fn get_file_event_for_folder(
request: &FileDialogRequest,
dialog: rfd::FileDialog,
) -> Vec<PathBuf> {
if request.multiple {
dialog.pick_folders().into_iter().flatten().collect()
} else {
dialog.pick_folder().into_iter().collect()
#[cfg(any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
pub(crate) fn get_file_event(&self) -> Vec<PathBuf> {
fn get_file_event_for_folder(
request: &FileDialogRequest,
dialog: rfd::FileDialog,
) -> Vec<PathBuf> {
if request.multiple {
dialog.pick_folders().into_iter().flatten().collect()
} else {
dialog.pick_folder().into_iter().collect()
}
}
}
fn get_file_event_for_file(
request: &FileDialogRequest,
mut dialog: rfd::FileDialog,
) -> Vec<PathBuf> {
let filters: Vec<_> = request
.accept
.as_deref()
.unwrap_or_default()
.split(',')
.filter_map(|s| Filters::from_str(s).ok())
.collect();
fn get_file_event_for_file(
request: &FileDialogRequest,
mut dialog: rfd::FileDialog,
) -> Vec<PathBuf> {
let filters: Vec<_> = request
.accept
.as_deref()
.unwrap_or_default()
.split(',')
.filter_map(|s| Filters::from_str(s).ok())
.collect();
let file_extensions: Vec<_> = filters
.iter()
.flat_map(|f| f.as_extensions().into_iter())
.collect();
let file_extensions: Vec<_> = filters
.iter()
.flat_map(|f| f.as_extensions().into_iter())
.collect();
dialog = dialog.add_filter("name", file_extensions.as_slice());
dialog = dialog.add_filter("name", file_extensions.as_slice());
let files: Vec<_> = if request.multiple {
dialog.pick_files().into_iter().flatten().collect()
let files: Vec<_> = if request.multiple {
dialog.pick_files().into_iter().flatten().collect()
} else {
dialog.pick_file().into_iter().collect()
};
files
}
let dialog = rfd::FileDialog::new();
if self.directory {
get_file_event_for_folder(self, dialog)
} else {
dialog.pick_file().into_iter().collect()
};
files
}
let dialog = rfd::FileDialog::new();
if request.directory {
get_file_event_for_folder(request, dialog)
} else {
get_file_event_for_file(request, dialog)
get_file_event_for_file(self, dialog)
}
}
}

View file

@ -0,0 +1,77 @@
use crate::{
assets::*, ipc::UserWindowEvent, shortcut::IntoAccelerator, window, DesktopContext,
ShortcutHandle, ShortcutRegistryError, WryEventHandler,
};
use dioxus_core::ScopeState;
use tao::{event::Event, event_loop::EventLoopWindowTarget};
use wry::RequestAsyncResponder;
/// Get an imperative handle to the current window
pub fn use_window(cx: &ScopeState) -> &DesktopContext {
cx.use_hook(|| cx.consume_context::<DesktopContext>())
.as_ref()
.unwrap()
}
/// Get a closure that executes any JavaScript in the WebView context.
pub fn use_wry_event_handler(
cx: &ScopeState,
handler: impl FnMut(&Event<UserWindowEvent>, &EventLoopWindowTarget<UserWindowEvent>) + 'static,
) -> &WryEventHandler {
cx.use_hook(move || {
let desktop = window();
let id = desktop.create_wry_event_handler(handler);
WryEventHandler {
handlers: desktop.shared.event_handlers.clone(),
id,
}
})
}
/// Provide a callback to handle asset loading yourself.
///
/// The callback takes a path as requested by the web view, and it should return `Some(response)`
/// if you want to load the asset, and `None` if you want to fallback on the default behavior.
pub fn use_asset_handler(
cx: &ScopeState,
name: &str,
handler: impl Fn(AssetRequest, RequestAsyncResponder) + 'static,
) {
cx.use_hook(|| {
crate::window().asset_handlers.register_handler(
name.to_string(),
Box::new(handler),
cx.scope_id(),
);
Handler(name.to_string())
});
// todo: can we just put ondrop in core?
struct Handler(String);
impl Drop for Handler {
fn drop(&mut self) {
_ = crate::window().asset_handlers.remove_handler(&self.0);
}
}
}
/// Get a closure that executes any JavaScript in the WebView context.
pub fn use_global_shortcut(
cx: &ScopeState,
accelerator: impl IntoAccelerator,
handler: impl FnMut() + 'static,
) -> &Result<ShortcutHandle, ShortcutRegistryError> {
cx.use_hook(move || {
let desktop = window();
let id = desktop.create_shortcut(accelerator.accelerator(), handler);
Ok(ShortcutHandle {
desktop,
shortcut_id: id?,
})
})
}

View file

@ -1,12 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>Dioxus app</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- CUSTOM HEAD -->
</head>
<body>
<div id="main"></div>
<!-- MODULE LOADER -->
</body>
<head>
<title>Dioxus app</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- CUSTOM HEAD -->
</head>
<body>
<div id="main"></div>
<!-- MODULE LOADER -->
</body>
</html>

View file

@ -0,0 +1,64 @@
use serde::{Deserialize, Serialize};
use tao::window::WindowId;
/// A pair of data
#[derive(Debug, Clone)]
pub struct UserWindowEvent(pub EventData, pub WindowId);
/// The data that might eminate from any window/webview
#[derive(Debug, Clone)]
pub enum EventData {
/// Poll the virtualdom
Poll,
/// Handle an ipc message eminating from the window.postMessage of a given webview
Ipc(IpcMessage),
/// Handle a hotreload event, basically telling us to update our templates
#[cfg(all(feature = "hot-reload", debug_assertions))]
HotReloadEvent(dioxus_hot_reload::HotReloadMsg),
/// Create a new window
NewWindow,
/// Close a given window (could be any window!)
CloseWindow,
}
/// A message struct that manages the communication between the webview and the eventloop code
///
/// This needs to be serializable across the JS boundary, so the method names and structs are sensitive.
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct IpcMessage {
method: String,
params: serde_json::Value,
}
/// A set of known messages that we need to respond to
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum IpcMethod<'a> {
FileDialog,
UserEvent,
Query,
BrowserOpen,
Initialize,
Other(&'a str),
}
impl IpcMessage {
pub(crate) fn method(&self) -> IpcMethod {
match self.method.as_str() {
// todo: this is a misspelling, needs to be fixed
"file_diolog" => IpcMethod::FileDialog,
"user_event" => IpcMethod::UserEvent,
"query" => IpcMethod::Query,
"browser_open" => IpcMethod::BrowserOpen,
"initialize" => IpcMethod::Initialize,
_ => IpcMethod::Other(&self.method),
}
}
pub(crate) fn params(self) -> serde_json::Value {
self.params
}
}

View file

@ -0,0 +1,131 @@
use crate::{
app::App,
ipc::{EventData, IpcMethod, UserWindowEvent},
Config,
};
use dioxus_core::*;
use tao::event::{Event, StartCause, WindowEvent};
/// Launch the WebView and run the event loop.
///
/// This function will start a multithreaded Tokio runtime as well the WebView event loop.
///
/// ```rust, no_run
/// use dioxus::prelude::*;
///
/// fn main() {
/// dioxus_desktop::launch(app);
/// }
///
/// fn app(cx: Scope) -> Element {
/// cx.render(rsx!{
/// h1 {"hello world!"}
/// })
/// }
/// ```
pub fn launch(root: Component) {
launch_with_props(root, (), Config::default())
}
/// Launch the WebView and run the event loop, with configuration.
///
/// This function will start a multithreaded Tokio runtime as well the WebView event loop.
///
/// You can configure the WebView window with a configuration closure
///
/// ```rust, no_run
/// use dioxus::prelude::*;
/// use dioxus_desktop::*;
///
/// fn main() {
/// dioxus_desktop::launch_cfg(app, Config::default().with_window(WindowBuilder::new().with_title("My App")));
/// }
///
/// fn app(cx: Scope) -> Element {
/// cx.render(rsx!{
/// h1 {"hello world!"}
/// })
/// }
/// ```
pub fn launch_cfg(root: Component, config_builder: Config) {
launch_with_props(root, (), config_builder)
}
/// Launch the WebView and run the event loop, with configuration and root props.
///
/// If the [`tokio`] feature is enabled, this will also startup and block a tokio runtime using the unconstrained task.
/// This function will start a multithreaded Tokio runtime as well the WebView event loop. This will block the current thread.
///
/// You can configure the WebView window with a configuration closure
///
/// ```rust, no_run
/// use dioxus::prelude::*;
/// use dioxus_desktop::Config;
///
/// fn main() {
/// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, Config::default());
/// }
///
/// struct AppProps {
/// name: &'static str
/// }
///
/// fn app(cx: Scope<AppProps>) -> Element {
/// cx.render(rsx!{
/// h1 {"hello {cx.props.name}!"}
/// })
/// }
/// ```
pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config) {
#[cfg(feature = "tokio")]
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(tokio::task::unconstrained(async move {
launch_with_props_blocking(root, props, cfg);
}));
#[cfg(not(feature = "tokio"))]
launch_with_props_blocking(root, props, cfg);
}
/// Launch the WebView and run the event loop, with configuration and root props.
///
/// This will block the main thread, and *must* be spawned on the main thread. This function does not assume any runtime
/// and is equivalent to calling launch_with_props with the tokio feature disabled.
pub fn launch_with_props_blocking<P: 'static>(root: Component<P>, props: P, cfg: Config) {
let (event_loop, mut app) = App::new(cfg, props, root);
event_loop.run(move |window_event, _, control_flow| {
app.tick(&window_event);
match window_event {
Event::NewEvents(StartCause::Init) => app.handle_start_cause_init(),
Event::WindowEvent {
event, window_id, ..
} => match event {
WindowEvent::CloseRequested => app.handle_close_requested(window_id),
WindowEvent::Destroyed { .. } => app.window_destroyed(window_id),
_ => {}
},
Event::UserEvent(UserWindowEvent(event, id)) => match event {
EventData::Poll => app.poll_vdom(id),
EventData::NewWindow => app.handle_new_window(),
EventData::CloseWindow => app.handle_close_msg(id),
EventData::HotReloadEvent(msg) => app.handle_hot_reload_msg(msg),
EventData::Ipc(msg) => match msg.method() {
IpcMethod::FileDialog => app.handle_file_dialog_msg(msg, id),
IpcMethod::UserEvent => app.handle_user_event_msg(msg, id),
IpcMethod::Query => app.handle_query_msg(msg, id),
IpcMethod::BrowserOpen => app.handle_browser_open(msg),
IpcMethod::Initialize => app.handle_initialize_msg(id),
IpcMethod::Other(_) => {}
},
},
_ => {}
}
*control_flow = app.control_flow;
})
}

View file

@ -3,657 +3,49 @@
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
#![deny(missing_docs)]
mod cfg;
mod collect_assets;
mod app;
mod assets;
mod config;
mod desktop_context;
mod edits;
mod element;
mod escape;
mod eval;
mod events;
mod file_upload;
#[cfg(any(target_os = "ios", target_os = "android"))]
mod mobile_shortcut;
mod hooks;
mod ipc;
mod menubar;
mod protocol;
mod query;
mod shortcut;
mod waker;
mod webview;
use crate::query::QueryResult;
use crate::shortcut::GlobalHotKeyEvent;
pub use cfg::{Config, WindowCloseBehaviour};
pub use desktop_context::DesktopContext;
#[allow(deprecated)]
pub use desktop_context::{
use_window, use_wry_event_handler, window, DesktopService, WryEventHandler, WryEventHandlerId,
};
use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers};
use dioxus_core::*;
use dioxus_html::{event_bubbles, FileEngine, HasFormData, MountedData, PlatformEventData};
use dioxus_html::{native_bind::NativeFileEngine, HtmlEvent};
use dioxus_interpreter_js::binary_protocol::Channel;
use element::DesktopElement;
use eval::init_eval;
use events::SerializedHtmlEventConverter;
use futures_util::{pin_mut, FutureExt};
pub use protocol::{use_asset_handler, AssetFuture, AssetHandler, AssetRequest, AssetResponse};
use rustc_hash::FxHashMap;
use shortcut::ShortcutRegistry;
pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
use std::cell::Cell;
use std::rc::Rc;
use std::sync::atomic::AtomicU16;
use std::task::Waker;
use std::{collections::HashMap, sync::Arc};
pub use tao::dpi::{LogicalSize, PhysicalSize};
use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
#[cfg(feature = "collect-assets")]
mod collect_assets;
// mobile shortcut is only supported on mobile platforms
#[cfg(any(target_os = "ios", target_os = "android"))]
mod mobile_shortcut;
// The main entrypoint for this crate
pub use launch::*;
mod launch;
// Reexport tao and wry, might want to re-export other important things
pub use tao;
pub use tao::dpi::{LogicalPosition, LogicalSize};
pub use tao::event::WindowEvent;
pub use tao::window::WindowBuilder;
use tao::{
event::{Event, StartCause, WindowEvent},
event_loop::ControlFlow,
};
// pub use webview::build_default_menu_bar;
pub use wry;
pub use wry::application as tao;
use wry::application::event_loop::EventLoopBuilder;
use wry::webview::WebView;
use wry::{application::window::WindowId, webview::WebContext};
/// Launch the WebView and run the event loop.
///
/// This function will start a multithreaded Tokio runtime as well the WebView event loop.
///
/// ```rust, no_run
/// use dioxus::prelude::*;
///
/// fn main() {
/// dioxus_desktop::launch(app);
/// }
///
/// fn app(cx: Scope) -> Element {
/// cx.render(rsx!{
/// h1 {"hello world!"}
/// })
/// }
/// ```
pub fn launch(root: Component) {
launch_with_props(root, (), Config::default())
}
/// Launch the WebView and run the event loop, with configuration.
///
/// This function will start a multithreaded Tokio runtime as well the WebView event loop.
///
/// You can configure the WebView window with a configuration closure
///
/// ```rust, no_run
/// use dioxus::prelude::*;
/// use dioxus_desktop::*;
///
/// fn main() {
/// dioxus_desktop::launch_cfg(app, Config::default().with_window(WindowBuilder::new().with_title("My App")));
/// }
///
/// fn app(cx: Scope) -> Element {
/// cx.render(rsx!{
/// h1 {"hello world!"}
/// })
/// }
/// ```
pub fn launch_cfg(root: Component, config_builder: Config) {
launch_with_props(root, (), config_builder)
}
/// Launch the WebView and run the event loop, with configuration and root props.
///
/// This function will start a multithreaded Tokio runtime as well the WebView event loop. This will block the current thread.
///
/// You can configure the WebView window with a configuration closure
///
/// ```rust, no_run
/// use dioxus::prelude::*;
/// use dioxus_desktop::Config;
///
/// fn main() {
/// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, Config::default());
/// }
///
/// struct AppProps {
/// name: &'static str
/// }
///
/// fn app(cx: Scope<AppProps>) -> Element {
/// cx.render(rsx!{
/// h1 {"hello {cx.props.name}!"}
/// })
/// }
/// ```
pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config) {
let event_loop = EventLoopBuilder::<UserWindowEvent>::with_user_event().build();
let proxy = event_loop.create_proxy();
let window_behaviour = cfg.last_window_close_behaviour;
// Intialize hot reloading if it is enabled
#[cfg(all(feature = "hot-reload", debug_assertions))]
dioxus_hot_reload::connect({
let proxy = proxy.clone();
move |template| {
let _ = proxy.send_event(UserWindowEvent(
EventData::HotReloadEvent(template),
unsafe { WindowId::dummy() },
));
}
});
// Copy over any assets we find
crate::collect_assets::copy_assets();
// Set the event converter
dioxus_html::set_event_converter(Box::new(SerializedHtmlEventConverter));
// We start the tokio runtime *on this thread*
// Any future we poll later will use this runtime to spawn tasks and for IO
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap();
// We enter the runtime but we poll futures manually, circumventing the per-task runtime budget
let _guard = rt.enter();
// We only have one webview right now, but we'll have more later
// Store them in a hashmap so we can remove them when they're closed
let mut webviews = HashMap::<WindowId, WebviewHandler>::new();
// We use this to allow dynamically adding and removing window event handlers
let event_handlers = WindowEventHandlers::default();
let queue = WebviewQueue::default();
let shortcut_manager = ShortcutRegistry::new();
let global_hotkey_channel = GlobalHotKeyEvent::receiver();
// move the props into a cell so we can pop it out later to create the first window
// iOS panics if we create a window before the event loop is started
let props = Rc::new(Cell::new(Some(props)));
let cfg = Rc::new(Cell::new(Some(cfg)));
let mut is_visible_before_start = true;
event_loop.run(move |window_event, event_loop, control_flow| {
*control_flow = ControlFlow::Poll;
event_handlers.apply_event(&window_event, event_loop);
if let Ok(event) = global_hotkey_channel.try_recv() {
shortcut_manager.call_handlers(event);
}
match window_event {
Event::WindowEvent {
event, window_id, ..
} => match event {
WindowEvent::CloseRequested => match window_behaviour {
cfg::WindowCloseBehaviour::LastWindowExitsApp => {
webviews.remove(&window_id);
if webviews.is_empty() {
*control_flow = ControlFlow::Exit
}
}
cfg::WindowCloseBehaviour::LastWindowHides => {
let Some(webview) = webviews.get(&window_id) else {
return;
};
hide_app_window(&webview.desktop_context.webview);
}
cfg::WindowCloseBehaviour::CloseWindow => {
webviews.remove(&window_id);
}
},
WindowEvent::Destroyed { .. } => {
webviews.remove(&window_id);
if matches!(
window_behaviour,
cfg::WindowCloseBehaviour::LastWindowExitsApp
) && webviews.is_empty()
{
*control_flow = ControlFlow::Exit
}
}
_ => {}
},
Event::NewEvents(StartCause::Init) => {
let props = props.take().unwrap();
let cfg = cfg.take().unwrap();
// Create a dom
let dom = VirtualDom::new_with_props(root, props);
is_visible_before_start = cfg.window.window.visible;
let handler = create_new_window(
cfg,
event_loop,
&proxy,
dom,
&queue,
&event_handlers,
shortcut_manager.clone(),
);
let id = handler.desktop_context.webview.window().id();
webviews.insert(id, handler);
_ = proxy.send_event(UserWindowEvent(EventData::Poll, id));
}
Event::UserEvent(UserWindowEvent(EventData::NewWindow, _)) => {
for handler in queue.borrow_mut().drain(..) {
let id = handler.desktop_context.webview.window().id();
webviews.insert(id, handler);
_ = proxy.send_event(UserWindowEvent(EventData::Poll, id));
}
}
Event::UserEvent(event) => match event.0 {
#[cfg(all(feature = "hot-reload", debug_assertions))]
EventData::HotReloadEvent(msg) => match msg {
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => {
for webview in webviews.values_mut() {
webview.dom.replace_template(template);
poll_vdom(webview);
}
}
dioxus_hot_reload::HotReloadMsg::Shutdown => {
*control_flow = ControlFlow::Exit;
}
},
EventData::CloseWindow => {
webviews.remove(&event.1);
if webviews.is_empty() {
*control_flow = ControlFlow::Exit
}
}
EventData::Poll => {
if let Some(view) = webviews.get_mut(&event.1) {
poll_vdom(view);
}
}
EventData::Ipc(msg) if msg.method() == "user_event" => {
let params = msg.params();
let evt = match serde_json::from_value::<HtmlEvent>(params) {
Ok(value) => value,
Err(err) => {
tracing::error!("Error parsing user_event: {:?}", err);
return;
}
};
let HtmlEvent {
element,
name,
bubbles,
data,
} = evt;
let view = webviews.get_mut(&event.1).unwrap();
// check for a mounted event placeholder and replace it with a desktop specific element
let as_any = if let dioxus_html::EventData::Mounted = &data {
let query = view
.dom
.base_scope()
.consume_context::<DesktopContext>()
.unwrap()
.query
.clone();
let element =
DesktopElement::new(element, view.desktop_context.clone(), query);
Rc::new(PlatformEventData::new(Box::new(MountedData::new(element))))
} else {
data.into_any()
};
view.dom.handle_event(&name, as_any, element, bubbles);
send_edits(view.dom.render_immediate(), &view.desktop_context);
}
// When the webview sends a query, we need to send it to the query manager which handles dispatching the data to the correct pending query
EventData::Ipc(msg) if msg.method() == "query" => {
let params = msg.params();
if let Ok(result) = serde_json::from_value::<QueryResult>(params) {
let view = webviews.get(&event.1).unwrap();
let query = view
.dom
.base_scope()
.consume_context::<DesktopContext>()
.unwrap()
.query
.clone();
query.send(result);
}
}
EventData::Ipc(msg) if msg.method() == "initialize" => {
let view = webviews.get_mut(&event.1).unwrap();
send_edits(view.dom.rebuild(), &view.desktop_context);
view.desktop_context
.webview
.window()
.set_visible(is_visible_before_start);
}
EventData::Ipc(msg) if msg.method() == "browser_open" => {
if let Some(temp) = msg.params().as_object() {
if temp.contains_key("href") {
let open = webbrowser::open(temp["href"].as_str().unwrap());
if let Err(e) = open {
tracing::error!("Open Browser error: {:?}", e);
}
}
}
}
EventData::Ipc(msg) if msg.method() == "file_diolog" => {
if let Ok(file_diolog) =
serde_json::from_value::<file_upload::FileDialogRequest>(msg.params())
{
struct DesktopFileUploadForm {
files: Arc<NativeFileEngine>,
}
impl HasFormData for DesktopFileUploadForm {
fn files(&self) -> Option<Arc<dyn FileEngine>> {
Some(self.files.clone())
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
let id = ElementId(file_diolog.target);
let event_name = &file_diolog.event;
let event_bubbles = file_diolog.bubbles;
let files = file_upload::get_file_event(&file_diolog);
let data =
Rc::new(PlatformEventData::new(Box::new(DesktopFileUploadForm {
files: Arc::new(NativeFileEngine::new(files)),
})));
let view = webviews.get_mut(&event.1).unwrap();
if event_name == "change&input" {
view.dom
.handle_event("input", data.clone(), id, event_bubbles);
view.dom.handle_event("change", data, id, event_bubbles);
} else {
view.dom.handle_event(event_name, data, id, event_bubbles);
}
send_edits(view.dom.render_immediate(), &view.desktop_context);
}
}
_ => {}
},
_ => {}
}
})
}
fn create_new_window(
mut cfg: Config,
event_loop: &EventLoopWindowTarget<UserWindowEvent>,
proxy: &EventLoopProxy<UserWindowEvent>,
dom: VirtualDom,
queue: &WebviewQueue,
event_handlers: &WindowEventHandlers,
shortcut_manager: ShortcutRegistry,
) -> WebviewHandler {
let (webview, web_context, asset_handlers, edit_queue) =
webview::build(&mut cfg, event_loop, proxy.clone());
let desktop_context = Rc::from(DesktopService::new(
webview,
proxy.clone(),
event_loop.clone(),
queue.clone(),
event_handlers.clone(),
shortcut_manager,
edit_queue,
asset_handlers,
));
let cx = dom.base_scope();
cx.provide_context(desktop_context.clone());
// Init eval
init_eval(cx);
WebviewHandler {
// We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both
waker: waker::tao_waker(proxy, desktop_context.webview.window().id()),
desktop_context,
dom,
_web_context: web_context,
}
}
struct WebviewHandler {
dom: VirtualDom,
desktop_context: DesktopContext,
waker: Waker,
// Wry assumes the webcontext is alive for the lifetime of the webview.
// We need to keep the webcontext alive, otherwise the webview will crash
_web_context: WebContext,
}
/// Poll the virtualdom until it's pending
///
/// The waker we give it is connected to the event loop, so it will wake up the event loop when it's ready to be polled again
///
/// All IO is done on the tokio runtime we started earlier
fn poll_vdom(view: &mut WebviewHandler) {
let mut cx = std::task::Context::from_waker(&view.waker);
loop {
{
let fut = view.dom.wait_for_work();
pin_mut!(fut);
match fut.poll_unpin(&mut cx) {
std::task::Poll::Ready(_) => {}
std::task::Poll::Pending => break,
}
}
send_edits(view.dom.render_immediate(), &view.desktop_context);
}
}
/// Send a list of mutations to the webview
fn send_edits(edits: Mutations, desktop_context: &DesktopContext) {
let mut channel = desktop_context.channel.borrow_mut();
let mut templates = desktop_context.templates.borrow_mut();
if let Some(bytes) = apply_edits(
edits,
&mut channel,
&mut templates,
&desktop_context.max_template_count,
) {
desktop_context.edit_queue.add_edits(bytes)
}
}
fn apply_edits(
mutations: Mutations,
channel: &mut Channel,
templates: &mut FxHashMap<String, u16>,
max_template_count: &AtomicU16,
) -> Option<Vec<u8>> {
use dioxus_core::Mutation::*;
if mutations.templates.is_empty() && mutations.edits.is_empty() {
return None;
}
for template in mutations.templates {
add_template(&template, channel, templates, max_template_count);
}
for edit in mutations.edits {
match edit {
AppendChildren { id, m } => channel.append_children(id.0 as u32, m as u16),
AssignId { path, id } => channel.assign_id(path, id.0 as u32),
CreatePlaceholder { id } => channel.create_placeholder(id.0 as u32),
CreateTextNode { value, id } => channel.create_text_node(value, id.0 as u32),
HydrateText { path, value, id } => channel.hydrate_text(path, value, id.0 as u32),
LoadTemplate { name, index, id } => {
if let Some(tmpl_id) = templates.get(name) {
channel.load_template(*tmpl_id, index as u16, id.0 as u32)
}
}
ReplaceWith { id, m } => channel.replace_with(id.0 as u32, m as u16),
ReplacePlaceholder { path, m } => channel.replace_placeholder(path, m as u16),
InsertAfter { id, m } => channel.insert_after(id.0 as u32, m as u16),
InsertBefore { id, m } => channel.insert_before(id.0 as u32, m as u16),
SetAttribute {
name,
value,
id,
ns,
} => match value {
BorrowedAttributeValue::Text(txt) => {
channel.set_attribute(id.0 as u32, name, txt, ns.unwrap_or_default())
}
BorrowedAttributeValue::Float(f) => {
channel.set_attribute(id.0 as u32, name, &f.to_string(), ns.unwrap_or_default())
}
BorrowedAttributeValue::Int(n) => {
channel.set_attribute(id.0 as u32, name, &n.to_string(), ns.unwrap_or_default())
}
BorrowedAttributeValue::Bool(b) => channel.set_attribute(
id.0 as u32,
name,
if b { "true" } else { "false" },
ns.unwrap_or_default(),
),
BorrowedAttributeValue::None => {
channel.remove_attribute(id.0 as u32, name, ns.unwrap_or_default())
}
_ => unreachable!(),
},
SetText { value, id } => channel.set_text(id.0 as u32, value),
NewEventListener { name, id, .. } => {
channel.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8)
}
RemoveEventListener { name, id } => {
channel.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8)
}
Remove { id } => channel.remove(id.0 as u32),
PushRoot { id } => channel.push_root(id.0 as u32),
}
}
let bytes: Vec<_> = channel.export_memory().collect();
channel.reset();
Some(bytes)
}
fn add_template(
template: &Template<'static>,
channel: &mut Channel,
templates: &mut FxHashMap<String, u16>,
max_template_count: &AtomicU16,
) {
let current_max_template_count = max_template_count.load(std::sync::atomic::Ordering::Relaxed);
for root in template.roots.iter() {
create_template_node(channel, root);
templates.insert(template.name.to_owned(), current_max_template_count);
}
channel.add_templates(current_max_template_count, template.roots.len() as u16);
max_template_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
fn create_template_node(channel: &mut Channel, v: &'static TemplateNode<'static>) {
use TemplateNode::*;
match v {
Element {
tag,
namespace,
attrs,
children,
..
} => {
// Push the current node onto the stack
match namespace {
Some(ns) => channel.create_element_ns(tag, ns),
None => channel.create_element(tag),
}
// Set attributes on the current node
for attr in *attrs {
if let TemplateAttribute::Static {
name,
value,
namespace,
} = attr
{
channel.set_top_attribute(name, value, namespace.unwrap_or_default())
}
}
// Add each child to the stack
for child in *children {
create_template_node(channel, child);
}
// Add all children to the parent
channel.append_children_to_top(children.len() as u16);
}
Text { text } => channel.create_raw_text(text),
DynamicText { .. } => channel.create_raw_text("p"),
Dynamic { .. } => channel.add_placeholder(),
}
}
/// Different hide implementations per platform
#[allow(unused)]
fn hide_app_window(webview: &WebView) {
#[cfg(target_os = "windows")]
{
use wry::application::platform::windows::WindowExtWindows;
webview.window().set_visible(false);
webview.window().set_skip_taskbar(true);
}
#[cfg(target_os = "linux")]
{
use wry::application::platform::unix::WindowExtUnix;
webview.window().set_visible(false);
}
#[cfg(target_os = "macos")]
{
// webview.window().set_visible(false); has the wrong behaviour on macOS
// It will hide the window but not show it again when the user switches
// back to the app. `NSApplication::hide:` has the correct behaviour
use objc::runtime::Object;
use objc::{msg_send, sel, sel_impl};
objc::rc::autoreleasepool(|| unsafe {
let app: *mut Object = msg_send![objc::class!(NSApplication), sharedApplication];
let nil = std::ptr::null_mut::<Object>();
let _: () = msg_send![app, hide: nil];
});
}
}
// Public exports
pub use assets::AssetRequest;
pub use config::{Config, WindowCloseBehaviour};
pub use desktop_context::{
window, DesktopContext, DesktopService, WryEventHandler, WryEventHandlerId,
};
pub use hooks::{use_asset_handler, use_global_shortcut, use_window, use_wry_event_handler};
pub use shortcut::{ShortcutHandle, ShortcutId, ShortcutRegistryError};
pub use wry::RequestAsyncResponder;

View file

@ -0,0 +1,79 @@
use tao::window::Window;
#[allow(unused)]
pub fn build_menu(window: &Window, default_menu_bar: bool) {
if default_menu_bar {
#[cfg(not(any(target_os = "ios", target_os = "android")))]
impl_::build_menu_bar(impl_::build_default_menu_bar(), window)
}
}
#[cfg(not(any(target_os = "ios", target_os = "android")))]
mod impl_ {
use super::*;
use muda::{Menu, PredefinedMenuItem, Submenu};
#[allow(unused)]
pub fn build_menu_bar(menu: Menu, window: &Window) {
#[cfg(target_os = "windows")]
use tao::platform::windows::WindowExtWindows;
#[cfg(target_os = "windows")]
menu.init_for_hwnd(window.hwnd());
// #[cfg(target_os = "linux")]
// {
// use tao::platform::unix::WindowExtUnix;
// menu.init_for_gtk_window(window, None);
// menu.init_for_gtk_window(window, Some(&ertical_gtk_box));
// }
#[cfg(target_os = "macos")]
menu.init_for_nsapp();
}
/// Builds a standard menu bar depending on the users platform. It may be used as a starting point
/// to further customize the menu bar and pass it to a [`WindowBuilder`](tao::window::WindowBuilder).
/// > Note: The default menu bar enables macOS shortcuts like cut/copy/paste.
/// > The menu bar differs per platform because of constraints introduced
/// > by [`MenuItem`](tao::menu::MenuItem).
pub fn build_default_menu_bar() -> Menu {
let menu = Menu::new();
// since it is uncommon on windows to have an "application menu"
// we add a "window" menu to be more consistent across platforms with the standard menu
let window_menu = Submenu::new("Window", true);
window_menu
.append_items(&[
&PredefinedMenuItem::fullscreen(None),
&PredefinedMenuItem::separator(),
&PredefinedMenuItem::hide(None),
&PredefinedMenuItem::hide_others(None),
&PredefinedMenuItem::show_all(None),
&PredefinedMenuItem::maximize(None),
&PredefinedMenuItem::minimize(None),
&PredefinedMenuItem::close_window(None),
&PredefinedMenuItem::separator(),
&PredefinedMenuItem::quit(None),
])
.unwrap();
let edit_menu = Submenu::new("Window", true);
edit_menu
.append_items(&[
&PredefinedMenuItem::undo(None),
&PredefinedMenuItem::redo(None),
&PredefinedMenuItem::separator(),
&PredefinedMenuItem::cut(None),
&PredefinedMenuItem::copy(None),
&PredefinedMenuItem::paste(None),
&PredefinedMenuItem::separator(),
&PredefinedMenuItem::select_all(None),
])
.unwrap();
menu.append_items(&[&window_menu, &edit_menu]).unwrap();
menu
}
}

View file

@ -2,7 +2,7 @@
use super::*;
use std::str::FromStr;
use wry::application::event_loop::EventLoopWindowTarget;
use tao::event_loop::EventLoopWindowTarget;
use dioxus_html::input_data::keyboard_types::Modifiers;
@ -37,15 +37,15 @@ impl GlobalHotKeyManager {
Ok(Self())
}
pub fn register(&mut self, accelerator: HotKey) -> Result<HotKey, HotkeyError> {
pub fn register(&self, accelerator: HotKey) -> Result<HotKey, HotkeyError> {
Ok(HotKey)
}
pub fn unregister(&mut self, id: HotKey) -> Result<(), HotkeyError> {
pub fn unregister(&self, id: HotKey) -> Result<(), HotkeyError> {
Ok(())
}
pub fn unregister_all(&mut self, _: &[HotKey]) -> Result<(), HotkeyError> {
pub fn unregister_all(&self, _: &[HotKey]) -> Result<(), HotkeyError> {
Ok(())
}
}

View file

@ -1,64 +1,160 @@
use crate::{window, DesktopContext};
use dioxus_core::ScopeState;
use dioxus_interpreter_js::INTERPRETER_JS;
use slab::Slab;
use std::{
borrow::Cow,
future::Future,
ops::Deref,
path::{Path, PathBuf},
pin::Pin,
rc::Rc,
sync::Arc,
};
use tokio::{
runtime::Handle,
sync::{OnceCell, RwLock},
};
use crate::{assets::*, edits::EditQueue};
use std::path::{Path, PathBuf};
use wry::{
http::{status::StatusCode, Request, Response},
Result,
http::{status::StatusCode, Request, Response, Uri},
RequestAsyncResponder, Result,
};
use crate::desktop_context::EditQueue;
static MINIFIED: &str = include_str!("./minified.js");
static DEFAULT_INDEX: &str = include_str!("./index.html");
fn module_loader(root_name: &str, headless: bool) -> String {
let js = INTERPRETER_JS.replace(
"/*POST_HANDLE_EDITS*/",
r#"// Prevent file inputs from opening the file dialog on click
let inputs = document.querySelectorAll("input");
for (let input of inputs) {
if (!input.getAttribute("data-dioxus-file-listener")) {
// prevent file inputs from opening the file dialog on click
const type = input.getAttribute("type");
if (type === "file") {
input.setAttribute("data-dioxus-file-listener", true);
input.addEventListener("click", (event) => {
let target = event.target;
let target_id = find_real_id(target);
if (target_id !== null) {
const send = (event_name) => {
const message = serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name });
window.ipc.postMessage(message);
};
send("change&input");
}
event.preventDefault();
});
}
}
}"#,
/// Build the index.html file we use for bootstrapping a new app
///
/// We use wry/webview by building a special index.html that forms a bridge between the webview and your rust code
///
/// This is similar to tauri, except we give more power to your rust code and less power to your frontend code.
/// This lets us skip a build/bundle step - your code just works - but limits how your Rust code can actually
/// mess with UI elements. We make this decision since other renderers like LiveView are very separate and can
/// never properly bridge the gap. Eventually of course, the idea is to build a custom CSS/HTML renderer where you
/// *do* have native control over elements, but that still won't work with liveview.
pub(super) fn index_request(
request: &Request<Vec<u8>>,
custom_head: Option<String>,
custom_index: Option<String>,
root_name: &str,
headless: bool,
) -> Option<Response<Vec<u8>>> {
// If the request is for the root, we'll serve the index.html file.
if request.uri().path() != "/" {
return None;
}
// Load a custom index file if provided
let mut index = custom_index.unwrap_or_else(|| DEFAULT_INDEX.to_string());
// Insert a custom head if provided
// We look just for the closing head tag. If a user provided a custom index with weird syntax, this might fail
if let Some(head) = custom_head {
index.insert_str(index.find("</head>").expect("Head element to exist"), &head);
}
// Inject our module loader by looking for a body tag
// A failure mode here, obviously, is if the user provided a custom index without a body tag
// Might want to document this
index.insert_str(
index.find("</body>").expect("Body element to exist"),
&module_loader(root_name, headless),
);
Response::builder()
.header("Content-Type", "text/html")
.header("Access-Control-Allow-Origin", "*")
.body(index.into())
.ok()
}
/// Handle a request from the webview
///
/// - Tries to stream edits if they're requested.
/// - If that doesn't match, tries a user provided asset handler
/// - If that doesn't match, tries to serve a file from the filesystem
pub(super) fn desktop_handler(
mut request: Request<Vec<u8>>,
asset_handlers: AssetHandlerRegistry,
edit_queue: &EditQueue,
responder: RequestAsyncResponder,
) {
// If the request is asking for edits (ie binary protocol streaming, do that)
if request.uri().path().trim_matches('/') == "edits" {
return edit_queue.handle_request(responder);
}
// If the user provided a custom asset handler, then call it and return the response if the request was handled.
// The path is the first part of the URI, so we need to trim the leading slash.
let path = PathBuf::from(
urlencoding::decode(request.uri().path().trim_start_matches('/'))
.expect("expected URL to be UTF-8 encoded")
.as_ref(),
);
let Some(name) = path.parent() else {
return tracing::error!("Asset request has no root {path:?}");
};
if let Some(name) = name.to_str() {
if asset_handlers.has_handler(name) {
// Trim the leading path from the URI
//
// I hope this is reliable!
//
// so a request for /assets/logos/logo.png?query=123 will become /logos/logo.png?query=123
strip_uri_prefix(&mut request, name);
return asset_handlers.handle_request(name, request, responder);
}
}
// Else, try to serve a file from the filesystem.
match serve_from_fs(path) {
Ok(res) => responder.respond(res),
Err(e) => tracing::error!("Error serving request from filesystem {}", e),
}
}
fn serve_from_fs(path: PathBuf) -> Result<Response<Vec<u8>>> {
// If the path is relative, we'll try to serve it from the assets directory.
let mut asset = get_asset_root_or_default().join(&path);
// If we can't find it, make it absolute and try again
if !asset.exists() {
asset = PathBuf::from("/").join(path);
}
if !asset.exists() {
return Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(String::from("Not Found").into_bytes())?);
}
Ok(Response::builder()
.header("Content-Type", get_mime_from_path(&asset)?)
.body(std::fs::read(asset)?)?)
}
fn strip_uri_prefix(request: &mut Request<Vec<u8>>, name: &str) {
// trim the leading path
if let Some(path) = request.uri().path_and_query() {
let new_path = path
.path()
.trim_start_matches('/')
.strip_prefix(name)
.expect("expected path to have prefix");
let new_uri = Uri::builder()
.scheme(request.uri().scheme_str().unwrap_or("http"))
.path_and_query(format!("{}{}", new_path, path.query().unwrap_or("")))
.authority("index.html")
.build()
.expect("failed to build new URI");
*request.uri_mut() = new_uri;
}
}
/// Construct the inline script that boots up the page and bridges the webview with rust code.
///
/// The arguments here:
/// - root_name: the root element (by Id) that we stream edits into
/// - headless: is this page being loaded but invisible? Important because not all windows are visible and the
/// interpreter can't connect until the window is ready.
fn module_loader(root_id: &str, headless: bool) -> String {
format!(
r#"
<script type="module">
{MINIFIED}
// Wait for the page to load
window.onload = function() {{
let rootname = "{root_name}";
let rootname = "{root_id}";
let root_element = window.document.getElementById(rootname);
if (root_element != null) {{
window.interpreter.initialize(root_element);
@ -71,335 +167,38 @@ fn module_loader(root_name: &str, headless: bool) -> String {
)
}
/// An arbitrary asset is an HTTP response containing a binary body.
pub type AssetResponse = Response<Cow<'static, [u8]>>;
/// A future that returns an [`AssetResponse`]. This future may be spawned in a new thread,
/// so it must be [`Send`], [`Sync`], and `'static`.
pub trait AssetFuture: Future<Output = Option<AssetResponse>> + Send + Sync + 'static {}
impl<T: Future<Output = Option<AssetResponse>> + Send + Sync + 'static> AssetFuture for T {}
#[derive(Debug, Clone)]
/// A request for an asset. This is a wrapper around [`Request<Vec<u8>>`] that provides methods specific to asset requests.
pub struct AssetRequest {
path: PathBuf,
request: Arc<Request<Vec<u8>>>,
}
impl AssetRequest {
/// Get the path the asset request is for
pub fn path(&self) -> &Path {
&self.path
}
}
impl From<Request<Vec<u8>>> for AssetRequest {
fn from(request: Request<Vec<u8>>) -> Self {
let decoded = urlencoding::decode(request.uri().path().trim_start_matches('/'))
.expect("expected URL to be UTF-8 encoded");
let path = PathBuf::from(&*decoded);
Self {
request: Arc::new(request),
path,
}
}
}
impl Deref for AssetRequest {
type Target = Request<Vec<u8>>;
fn deref(&self) -> &Self::Target {
&self.request
}
}
/// A handler that takes an [`AssetRequest`] and returns a future that either loads the asset, or returns `None`.
/// This handler is stashed indefinitely in a context object, so it must be `'static`.
pub trait AssetHandler<F: AssetFuture>: Send + Sync + 'static {
/// Handle an asset request, returning a future that either loads the asset, or returns `None`
fn handle_request(&self, request: &AssetRequest) -> F;
}
impl<F: AssetFuture, T: Fn(&AssetRequest) -> F + Send + Sync + 'static> AssetHandler<F> for T {
fn handle_request(&self, request: &AssetRequest) -> F {
self(request)
}
}
type AssetHandlerRegistryInner =
Slab<Box<dyn Fn(&AssetRequest) -> Pin<Box<dyn AssetFuture>> + Send + Sync + 'static>>;
#[derive(Clone)]
pub struct AssetHandlerRegistry(Arc<RwLock<AssetHandlerRegistryInner>>);
impl AssetHandlerRegistry {
pub fn new() -> Self {
AssetHandlerRegistry(Arc::new(RwLock::new(Slab::new())))
}
pub async fn register_handler<F: AssetFuture>(&self, f: impl AssetHandler<F>) -> usize {
let mut registry = self.0.write().await;
registry.insert(Box::new(move |req| Box::pin(f.handle_request(req))))
}
pub async fn remove_handler(&self, id: usize) -> Option<()> {
let mut registry = self.0.write().await;
registry.try_remove(id).map(|_| ())
}
pub async fn try_handlers(&self, req: &AssetRequest) -> Option<AssetResponse> {
let registry = self.0.read().await;
for (_, handler) in registry.iter() {
if let Some(response) = handler(req).await {
return Some(response);
}
}
None
}
}
/// A handle to a registered asset handler.
pub struct AssetHandlerHandle {
desktop: DesktopContext,
handler_id: Rc<OnceCell<usize>>,
}
impl AssetHandlerHandle {
/// Returns the ID for this handle.
///
/// Because registering an ID is asynchronous, this may return `None` if the
/// registration has not completed yet.
pub fn handler_id(&self) -> Option<usize> {
self.handler_id.get().copied()
}
}
impl Drop for AssetHandlerHandle {
fn drop(&mut self) {
let cell = Rc::clone(&self.handler_id);
let desktop = Rc::clone(&self.desktop);
tokio::task::block_in_place(move || {
Handle::current().block_on(async move {
if let Some(id) = cell.get() {
desktop.asset_handlers.remove_handler(*id).await;
}
})
});
}
}
/// Provide a callback to handle asset loading yourself.
/// Get the assset directory, following tauri/cargo-bundles directory discovery approach
///
/// The callback takes a path as requested by the web view, and it should return `Some(response)`
/// if you want to load the asset, and `None` if you want to fallback on the default behavior.
pub fn use_asset_handler<F: AssetFuture>(
cx: &ScopeState,
handler: impl AssetHandler<F>,
) -> &AssetHandlerHandle {
cx.use_hook(|| {
let desktop = window();
let handler_id = Rc::new(OnceCell::new());
let handler_id_ref = Rc::clone(&handler_id);
let desktop_ref = Rc::clone(&desktop);
cx.push_future(async move {
let id = desktop.asset_handlers.register_handler(handler).await;
handler_id.set(id).unwrap();
});
AssetHandlerHandle {
desktop: desktop_ref,
handler_id: handler_id_ref,
}
})
}
pub(super) async fn desktop_handler(
request: Request<Vec<u8>>,
custom_head: Option<String>,
custom_index: Option<String>,
#[allow(unused_variables)] assets_head: Option<String>,
root_name: &str,
asset_handlers: &AssetHandlerRegistry,
edit_queue: &EditQueue,
headless: bool,
responder: wry::webview::RequestAsyncResponder,
) {
let request = AssetRequest::from(request);
// If the request is for the root, we'll serve the index.html file.
if request.uri().path() == "/" {
// If a custom index is provided, just defer to that, expecting the user to know what they're doing.
// we'll look for the closing </body> tag and insert our little module loader there.
let body = match custom_index {
Some(custom_index) => custom_index
.replace(
"</body>",
&format!("{}</body>", module_loader(root_name, headless)),
)
.into_bytes(),
None => {
// Otherwise, we'll serve the default index.html and apply a custom head if that's specified.
let mut template = include_str!("./index.html").to_string();
#[allow(unused_mut)]
let mut head = custom_head.unwrap_or_default();
#[cfg(all(
debug_assertions,
any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)
))]
{
use manganis_cli_support::AssetManifestExt;
let manifest = manganis_cli_support::AssetManifest::load();
head += &manifest.head();
}
#[cfg(not(all(
debug_assertions,
any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)
)))]
{
if let Some(assets_head) = assets_head {
head += &assets_head;
} else {
tracing::warn!("No assets head found. You can compile assets with the dioxus-cli in release mode");
}
}
template = template.replace("<!-- CUSTOM HEAD -->", &head);
template
.replace(
"<!-- MODULE LOADER -->",
&module_loader(root_name, headless),
)
.into_bytes()
}
};
match Response::builder()
.header("Content-Type", "text/html")
.header("Access-Control-Allow-Origin", "*")
.body(Cow::from(body))
{
Ok(response) => {
responder.respond(response);
return;
}
Err(err) => tracing::error!("error building response: {}", err),
}
} else if request.uri().path().trim_matches('/') == "edits" {
edit_queue.handle_request(responder);
return;
}
// If the user provided a custom asset handler, then call it and return the response
// if the request was handled.
if let Some(response) = asset_handlers.try_handlers(&request).await {
responder.respond(response);
return;
}
// Else, try to serve a file from the filesystem.
// If the path is relative, we'll try to serve it from the assets directory.
let mut asset = get_asset_root_or_default().join(&request.path);
if !asset.exists() {
asset = PathBuf::from("/").join(&request.path);
}
if asset.exists() {
let content_type = match get_mime_from_path(&asset) {
Ok(content_type) => content_type,
Err(err) => {
tracing::error!("error getting mime type: {}", err);
return;
}
};
let asset = match std::fs::read(&asset) {
Ok(asset) => asset,
Err(err) => {
tracing::error!("error reading asset: {}", err);
return;
}
};
match Response::builder()
.header("Content-Type", content_type)
.body(Cow::from(asset))
{
Ok(response) => {
responder.respond(response);
return;
}
Err(err) => tracing::error!("error building response: {}", err),
}
}
tracing::error!(
"Failed to find {} (as path {})",
request.uri().path(),
asset.display()
);
match Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Cow::from(String::from("Not Found").into_bytes()))
{
Ok(response) => {
responder.respond(response);
}
Err(err) => tracing::error!("error building response: {}", err),
}
}
#[allow(unreachable_code)]
pub(crate) fn get_asset_root_or_default() -> PathBuf {
/// Defaults to the current directory if no asset directory is found, which is useful for development when the app
/// isn't bundled.
fn get_asset_root_or_default() -> PathBuf {
get_asset_root().unwrap_or_else(|| Path::new(".").to_path_buf())
}
/// Get the asset directory, following tauri/cargo-bundles directory discovery approach
///
/// Currently supports:
/// - [x] macOS
/// - [ ] Windows
/// - [ ] Linux (rpm)
/// - [ ] Linux (deb)
/// - [ ] iOS
/// - [ ] Android
#[allow(unreachable_code)]
fn get_asset_root() -> Option<PathBuf> {
/*
We're matching exactly how cargo-bundle works.
- [x] macOS
- [ ] Windows
- [ ] Linux (rpm)
- [ ] Linux (deb)
- [ ] iOS
- [ ] Android
*/
if std::env::var_os("CARGO").is_some() || std::env::var_os("DIOXUS_ACTIVE").is_some() {
// If running under cargo, there's no bundle!
// There might be a smarter/more resilient way of doing this
if std::env::var_os("CARGO").is_some() {
return None;
}
// TODO: support for other platforms
#[cfg(target_os = "macos")]
{
let bundle = core_foundation::bundle::CFBundle::main_bundle();
let bundle_path = bundle.path()?;
let resources_path = bundle.resources_path()?;
let absolute_resources_root = bundle_path.join(resources_path);
let canonical_resources_root = dunce::canonicalize(absolute_resources_root).ok()?;
return Some(canonical_resources_root);
return dunce::canonicalize(absolute_resources_root).ok();
}
None
@ -411,18 +210,10 @@ fn get_mime_from_path(trimmed: &Path) -> Result<&'static str> {
return Ok("image/svg+xml");
}
let res = match infer::get_from_path(trimmed)?.map(|f| f.mime_type()) {
Some(f) => {
if f == "text/plain" {
get_mime_by_ext(trimmed)
} else {
f
}
}
None => get_mime_by_ext(trimmed),
};
Ok(res)
match infer::get_from_path(trimmed)?.map(|f| f.mime_type()) {
Some(f) if f != "text/plain" => Ok(f),
_ => Ok(get_mime_by_ext(trimmed)),
}
}
/// Get the mime type from a URI using its extension

View file

@ -1,11 +1,10 @@
use std::{cell::RefCell, collections::HashMap, rc::Rc, str::FromStr};
use std::{cell::RefCell, collections::HashMap, str::FromStr};
use dioxus_core::ScopeState;
use dioxus_html::input_data::keyboard_types::Modifiers;
use slab::Slab;
use wry::application::keyboard::ModifiersState;
use tao::keyboard::ModifiersState;
use crate::{desktop_context::DesktopContext, window};
use crate::desktop_context::DesktopContext;
#[cfg(any(
target_os = "windows",
@ -24,14 +23,11 @@ pub use global_hotkey::{
#[cfg(any(target_os = "ios", target_os = "android"))]
pub use crate::mobile_shortcut::*;
#[derive(Clone)]
pub(crate) struct ShortcutRegistry {
manager: Rc<RefCell<GlobalHotKeyManager>>,
shortcuts: ShortcutMap,
manager: GlobalHotKeyManager,
shortcuts: RefCell<HashMap<u32, Shortcut>>,
}
type ShortcutMap = Rc<RefCell<HashMap<u32, Shortcut>>>;
struct Shortcut {
#[allow(unused)]
shortcut: HotKey,
@ -55,8 +51,8 @@ impl Shortcut {
impl ShortcutRegistry {
pub fn new() -> Self {
Self {
manager: Rc::new(RefCell::new(GlobalHotKeyManager::new().unwrap())),
shortcuts: Rc::new(RefCell::new(HashMap::new())),
manager: GlobalHotKeyManager::new().unwrap(),
shortcuts: RefCell::new(HashMap::new()),
}
}
@ -74,36 +70,36 @@ impl ShortcutRegistry {
callback: Box<dyn FnMut()>,
) -> Result<ShortcutId, ShortcutRegistryError> {
let accelerator_id = hotkey.clone().id();
let mut shortcuts = self.shortcuts.borrow_mut();
Ok(
if let Some(callbacks) = shortcuts.get_mut(&accelerator_id) {
let id = callbacks.insert(callback);
ShortcutId {
id: accelerator_id,
number: id,
}
} else {
match self.manager.borrow_mut().register(hotkey) {
Ok(_) => {
let mut slab = Slab::new();
let id = slab.insert(callback);
let shortcut = Shortcut {
shortcut: hotkey,
callbacks: slab,
};
shortcuts.insert(accelerator_id, shortcut);
ShortcutId {
id: accelerator_id,
number: id,
}
}
Err(HotkeyError::HotKeyParseError(shortcut)) => {
return Err(ShortcutRegistryError::InvalidShortcut(shortcut))
}
Err(err) => return Err(ShortcutRegistryError::Other(Box::new(err))),
}
},
)
if let Some(callbacks) = shortcuts.get_mut(&accelerator_id) {
return Ok(ShortcutId {
id: accelerator_id,
number: callbacks.insert(callback),
});
};
self.manager.register(hotkey).map_err(|e| match e {
HotkeyError::HotKeyParseError(shortcut) => {
ShortcutRegistryError::InvalidShortcut(shortcut)
}
err => ShortcutRegistryError::Other(Box::new(err)),
})?;
let mut shortcut = Shortcut {
shortcut: hotkey,
callbacks: Slab::new(),
};
let id = shortcut.callbacks.insert(callback);
shortcuts.insert(accelerator_id, shortcut);
Ok(ShortcutId {
id: accelerator_id,
number: id,
})
}
pub(crate) fn remove_shortcut(&self, id: ShortcutId) {
@ -112,7 +108,7 @@ impl ShortcutRegistry {
callbacks.remove(id.number);
if callbacks.is_empty() {
if let Some(_shortcut) = shortcuts.remove(&id.id) {
let _ = self.manager.borrow_mut().unregister(_shortcut.shortcut);
let _ = self.manager.unregister(_shortcut.shortcut);
}
}
}
@ -121,7 +117,7 @@ impl ShortcutRegistry {
pub(crate) fn remove_all(&self) {
let mut shortcuts = self.shortcuts.borrow_mut();
let hotkeys: Vec<_> = shortcuts.drain().map(|(_, v)| v.shortcut).collect();
let _ = self.manager.borrow_mut().unregister_all(&hotkeys);
let _ = self.manager.unregister_all(&hotkeys);
}
}
@ -144,7 +140,7 @@ pub struct ShortcutId {
/// A global shortcut. This will be automatically removed when it is dropped.
pub struct ShortcutHandle {
desktop: DesktopContext,
pub(crate) desktop: DesktopContext,
/// The id of the shortcut
pub shortcut_id: ShortcutId,
}
@ -177,24 +173,6 @@ impl IntoAccelerator for &str {
}
}
/// Get a closure that executes any JavaScript in the WebView context.
pub fn use_global_shortcut(
cx: &ScopeState,
accelerator: impl IntoAccelerator,
handler: impl FnMut() + 'static,
) -> &Result<ShortcutHandle, ShortcutRegistryError> {
cx.use_hook(move || {
let desktop = window();
let id = desktop.create_shortcut(accelerator.accelerator(), handler);
Ok(ShortcutHandle {
desktop,
shortcut_id: id?,
})
})
}
impl ShortcutHandle {
/// Remove the shortcut.
pub fn remove(&self) {

View file

@ -1,14 +1,15 @@
use crate::desktop_context::{EventData, UserWindowEvent};
use crate::ipc::{EventData, UserWindowEvent};
use futures_util::task::ArcWake;
use std::sync::Arc;
use wry::application::{event_loop::EventLoopProxy, window::WindowId};
use tao::{event_loop::EventLoopProxy, window::WindowId};
/// Create a waker that will send a poll event to the event loop.
///
/// This lets the VirtualDom "come up for air" and process events while the main thread is blocked by the WebView.
///
/// All other IO lives in the Tokio runtime,
pub fn tao_waker(proxy: &EventLoopProxy<UserWindowEvent>, id: WindowId) -> std::task::Waker {
/// All IO and multithreading lives on other threads. Thanks to tokio's work stealing approach, the main thread can never
/// claim a task while it's blocked by the event loop.
pub fn tao_waker(proxy: EventLoopProxy<UserWindowEvent>, id: WindowId) -> std::task::Waker {
struct DomHandle {
proxy: EventLoopProxy<UserWindowEvent>,
id: WindowId,
@ -27,8 +28,5 @@ pub fn tao_waker(proxy: &EventLoopProxy<UserWindowEvent>, id: WindowId) -> std::
}
}
futures_util::task::waker(Arc::new(DomHandle {
id,
proxy: proxy.clone(),
}))
futures_util::task::waker(Arc::new(DomHandle { id, proxy }))
}

View file

@ -1,232 +1,185 @@
use crate::desktop_context::{EditQueue, EventData};
use crate::protocol::{self, AssetHandlerRegistry};
use crate::{desktop_context::UserWindowEvent, Config};
use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
pub use wry;
pub use wry::application as tao;
use wry::application::window::Window;
use wry::webview::{WebContext, WebView, WebViewBuilder};
use crate::{
app::SharedContext,
assets::AssetHandlerRegistry,
edits::EditQueue,
eval::DesktopEvalProvider,
ipc::{EventData, UserWindowEvent},
protocol::{self},
waker::tao_waker,
Config, DesktopContext, DesktopService,
};
use dioxus_core::VirtualDom;
use dioxus_html::prelude::EvalProvider;
use futures_util::{pin_mut, FutureExt};
use std::{rc::Rc, task::Waker};
use wry::{RequestAsyncResponder, WebContext, WebViewBuilder};
pub(crate) fn build(
cfg: &mut Config,
event_loop: &EventLoopWindowTarget<UserWindowEvent>,
proxy: EventLoopProxy<UserWindowEvent>,
) -> (WebView, WebContext, AssetHandlerRegistry, EditQueue) {
let builder = cfg.window.clone();
let window = builder.with_visible(false).build(event_loop).unwrap();
let file_handler = cfg.file_drop_handler.take();
let custom_head = cfg.custom_head.clone();
let index_file = cfg.custom_index.clone();
let root_name = cfg.root_name.clone();
let assets_head = {
#[cfg(all(
debug_assertions,
any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)
))]
{
None
}
#[cfg(not(all(
debug_assertions,
any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)
)))]
{
let head = crate::protocol::get_asset_root_or_default();
let head = head.join("dist/__assets_head.html");
match std::fs::read_to_string(&head) {
Ok(s) => Some(s),
Err(err) => {
tracing::error!("Failed to read {head:?}: {err}");
None
}
}
}
};
pub struct WebviewInstance {
pub dom: VirtualDom,
pub desktop_context: DesktopContext,
pub waker: Waker,
// TODO: restore the menu bar with muda: https://github.com/tauri-apps/muda/blob/dev/examples/wry.rs
// if cfg.enable_default_menu_bar {
// builder = builder.with_menu(build_default_menu_bar());
// }
// We assume that if the icon is None in cfg, then the user just didnt set it
if cfg.window.window.window_icon.is_none() {
window.set_window_icon(Some(
tao::window::Icon::from_rgba(
include_bytes!("./assets/default_icon.bin").to_vec(),
460,
460,
)
.expect("image parse failed"),
));
}
let mut web_context = WebContext::new(cfg.data_dir.clone());
let edit_queue = EditQueue::default();
let headless = !cfg.window.window.visible;
let asset_handlers = AssetHandlerRegistry::new();
let asset_handlers_ref = asset_handlers.clone();
let mut webview = WebViewBuilder::new(window)
.unwrap()
.with_transparent(cfg.window.window.transparent)
.with_url("dioxus://index.html/")
.unwrap()
.with_ipc_handler(move |window: &Window, payload: String| {
// defer the event to the main thread
if let Ok(message) = serde_json::from_str(&payload) {
_ = proxy.send_event(UserWindowEvent(EventData::Ipc(message), window.id()));
}
})
.with_asynchronous_custom_protocol(String::from("dioxus"), {
let edit_queue = edit_queue.clone();
move |request, responder| {
let custom_head = custom_head.clone();
let index_file = index_file.clone();
let assets_head = assets_head.clone();
let root_name = root_name.clone();
let asset_handlers_ref = asset_handlers_ref.clone();
let edit_queue = edit_queue.clone();
tokio::spawn(async move {
protocol::desktop_handler(
request,
custom_head.clone(),
index_file.clone(),
assets_head.clone(),
&root_name,
&asset_handlers_ref,
&edit_queue,
headless,
responder,
)
.await;
});
}
})
.with_file_drop_handler(move |window, evet| {
file_handler
.as_ref()
.map(|handler| handler(window, evet))
.unwrap_or_default()
})
.with_web_context(&mut web_context);
#[cfg(windows)]
{
// Windows has a platform specific settings to disable the browser shortcut keys
use wry::webview::WebViewBuilderExtWindows;
webview = webview.with_browser_accelerator_keys(false);
}
if let Some(color) = cfg.background_color {
webview = webview.with_background_color(color);
}
// These are commented out because wry is currently broken in wry
// let mut web_context = WebContext::new(cfg.data_dir.clone());
// .with_web_context(&mut web_context);
for (name, handler) in cfg.protocols.drain(..) {
webview = webview.with_custom_protocol(name, move |r| handler(r))
}
if cfg.disable_context_menu {
// in release mode, we don't want to show the dev tool or reload menus
webview = webview.with_initialization_script(
r#"
if (document.addEventListener) {
document.addEventListener('contextmenu', function(e) {
e.preventDefault();
}, false);
} else {
document.attachEvent('oncontextmenu', function() {
window.event.returnValue = false;
});
}
"#,
)
} else {
// in debug, we are okay with the reload menu showing and dev tool
webview = webview.with_devtools(true);
}
(
webview.build().unwrap(),
web_context,
asset_handlers,
edit_queue,
)
// Wry assumes the webcontext is alive for the lifetime of the webview.
// We need to keep the webcontext alive, otherwise the webview will crash
_web_context: WebContext,
}
// /// Builds a standard menu bar depending on the users platform. It may be used as a starting point
// /// to further customize the menu bar and pass it to a [`WindowBuilder`](tao::window::WindowBuilder).
// /// > Note: The default menu bar enables macOS shortcuts like cut/copy/paste.
// /// > The menu bar differs per platform because of constraints introduced
// /// > by [`MenuItem`](tao::menu::MenuItem).
// pub fn build_default_menu_bar() -> MenuBar {
// let mut menu_bar = MenuBar::new();
impl WebviewInstance {
pub fn new(mut cfg: Config, dom: VirtualDom, shared: Rc<SharedContext>) -> WebviewInstance {
let window = cfg.window.clone().build(&shared.target).unwrap();
// // since it is uncommon on windows to have an "application menu"
// // we add a "window" menu to be more consistent across platforms with the standard menu
// let mut window_menu = MenuBar::new();
// #[cfg(target_os = "macos")]
// {
// window_menu.add_native_item(MenuItem::EnterFullScreen);
// window_menu.add_native_item(MenuItem::Zoom);
// window_menu.add_native_item(MenuItem::Separator);
// }
// TODO: allow users to specify their own menubars, again :/
#[cfg(not(any(target_os = "android", target_os = "ios")))]
crate::menubar::build_menu(&window, cfg.enable_default_menu_bar);
// window_menu.add_native_item(MenuItem::Hide);
// We assume that if the icon is None in cfg, then the user just didnt set it
if cfg.window.window.window_icon.is_none() {
window.set_window_icon(Some(
tao::window::Icon::from_rgba(
include_bytes!("./assets/default_icon.bin").to_vec(),
460,
460,
)
.expect("image parse failed"),
));
}
// #[cfg(target_os = "macos")]
// {
// window_menu.add_native_item(MenuItem::HideOthers);
// window_menu.add_native_item(MenuItem::ShowAll);
// }
let mut web_context = WebContext::new(cfg.data_dir.clone());
let edit_queue = EditQueue::default();
let asset_handlers = AssetHandlerRegistry::new(dom.runtime());
let headless = !cfg.window.window.visible;
// window_menu.add_native_item(MenuItem::Minimize);
// window_menu.add_native_item(MenuItem::CloseWindow);
// window_menu.add_native_item(MenuItem::Separator);
// window_menu.add_native_item(MenuItem::Quit);
// menu_bar.add_submenu("Window", true, window_menu);
// Rust :(
let window_id = window.id();
let file_handler = cfg.file_drop_handler.take();
let custom_head = cfg.custom_head.clone();
let index_file = cfg.custom_index.clone();
let root_name = cfg.root_name.clone();
let asset_handlers_ = asset_handlers.clone();
let edit_queue_ = edit_queue.clone();
let proxy_ = shared.proxy.clone();
// // since tao supports none of the below items on linux we should only add them on macos/windows
// #[cfg(not(target_os = "linux"))]
// {
// let mut edit_menu = MenuBar::new();
// #[cfg(target_os = "macos")]
// {
// edit_menu.add_native_item(MenuItem::Undo);
// edit_menu.add_native_item(MenuItem::Redo);
// edit_menu.add_native_item(MenuItem::Separator);
// }
let request_handler = move |request, responder: RequestAsyncResponder| {
// Try to serve the index file first
let index_bytes = protocol::index_request(
&request,
custom_head.clone(),
index_file.clone(),
&root_name,
headless,
);
// edit_menu.add_native_item(MenuItem::Cut);
// edit_menu.add_native_item(MenuItem::Copy);
// edit_menu.add_native_item(MenuItem::Paste);
// Otherwise, try to serve an asset, either from the user or the filesystem
match index_bytes {
Some(body) => responder.respond(body),
None => protocol::desktop_handler(
request,
asset_handlers_.clone(),
&edit_queue_,
responder,
),
}
};
// #[cfg(target_os = "macos")]
// {
// edit_menu.add_native_item(MenuItem::Separator);
// edit_menu.add_native_item(MenuItem::SelectAll);
// }
// menu_bar.add_submenu("Edit", true, edit_menu);
// }
let ipc_handler = move |payload: String| {
// defer the event to the main thread
if let Ok(message) = serde_json::from_str(&payload) {
_ = proxy_.send_event(UserWindowEvent(EventData::Ipc(message), window_id));
}
};
// menu_bar
// }
let mut webview = WebViewBuilder::new(&window)
.with_transparent(cfg.window.window.transparent)
.with_url("dioxus://index.html/")
.unwrap()
.with_ipc_handler(ipc_handler)
.with_asynchronous_custom_protocol(String::from("dioxus"), request_handler)
.with_web_context(&mut web_context);
if let Some(handler) = file_handler {
webview = webview.with_file_drop_handler(move |evt| handler(window_id, evt))
}
// This was removed from wry, I'm not sure what replaced it
// #[cfg(windows)]
// {
// // Windows has a platform specific settings to disable the browser shortcut keys
// use wry::WebViewBuilderExtWindows;
// webview = webview.with_browser_accelerator_keys(false);
// }
if let Some(color) = cfg.background_color {
webview = webview.with_background_color(color);
}
for (name, handler) in cfg.protocols.drain(..) {
webview = webview.with_custom_protocol(name, handler);
}
const INITIALIZATION_SCRIPT: &str = r#"
if (document.addEventListener) {
document.addEventListener('contextmenu', function(e) {
e.preventDefault();
}, false);
} else {
document.attachEvent('oncontextmenu', function() {
window.event.returnValue = false;
});
}
"#;
if cfg.disable_context_menu {
// in release mode, we don't want to show the dev tool or reload menus
webview = webview.with_initialization_script(INITIALIZATION_SCRIPT)
} else {
// in debug, we are okay with the reload menu showing and dev tool
webview = webview.with_devtools(true);
}
let desktop_context = Rc::from(DesktopService::new(
webview.build().unwrap(),
window,
shared.clone(),
edit_queue,
asset_handlers,
));
// Provide the desktop context to the virtualdom
dom.base_scope().provide_context(desktop_context.clone());
// Also set up its eval provider
// It's important that we provide as dyn EvalProvider - using the concrete type has
// a different TypeId.
let provider: Rc<dyn EvalProvider> =
Rc::new(DesktopEvalProvider::new(desktop_context.clone()));
dom.base_scope().provide_context(provider);
WebviewInstance {
waker: tao_waker(shared.proxy.clone(), desktop_context.window.id()),
desktop_context,
dom,
_web_context: web_context,
}
}
pub fn poll_vdom(&mut self) {
let mut cx = std::task::Context::from_waker(&self.waker);
// Continously poll the virtualdom until it's pending
// Wait for work will return Ready when it has edits to be sent to the webview
// It will return Pending when it needs to be polled again - nothing is ready
loop {
{
let fut = self.dom.wait_for_work();
pin_mut!(fut);
match fut.poll_unpin(&mut cx) {
std::task::Poll::Ready(_) => {}
std::task::Poll::Pending => return,
}
}
self.desktop_context.send_edits(self.dom.render_immediate());
}
}
}

View file

@ -16,18 +16,14 @@ dioxus_server_macro = { workspace = true }
# warp
warp = { version = "0.3.5", features = ["compression-gzip"], optional = true }
http-body = { version = "0.4.5", optional = true }
http-body-util = "0.1.0-rc.2"
# axum
axum = { version = "0.6.1", features = ["ws", "macros"], optional = true }
axum = { version = "0.6.1", features = ["ws", "macros"], default-features = false, optional = true }
tower-http = { version = "0.4.0", optional = true, features = ["fs", "compression-gzip"] }
tower = { version = "0.4.13", features = ["util"], optional = true }
axum-macros = "0.3.7"
# salvo
salvo = { version = "0.63.0", optional = true, features = ["serve-static", "websocket", "compression"] }
serde = "1.0.159"
http-body-util = { version = "0.1.0-rc.2", optional = true }
# Dioxus + SSR
dioxus = { workspace = true }
@ -45,24 +41,25 @@ dioxus-desktop = { workspace = true, optional = true }
dioxus-router = { workspace = true, optional = true }
tracing = { workspace = true }
tracing-futures = { workspace = true }
tracing-futures = { workspace = true, optional = true }
once_cell = "1.17.1"
thiserror = { workspace = true }
tokio = { workspace = true, features = ["full"], optional = true }
tokio = { workspace = true, features = ["rt", "sync", "rt-multi-thread"], optional = true }
tokio-util = { version = "0.7.8", features = ["rt"], optional = true }
object-pool = "0.5.4"
anymap = "0.12.1"
anymap = { version = "0.12.1", optional = true }
serde = "1.0.159"
serde_json = { version = "1.0.95", optional = true }
tokio-stream = { version = "0.1.12", features = ["sync"], optional = true }
futures-util = { workspace = true, optional = true }
postcard = { version = "1.0.4", features = ["use-std"] }
futures-util = { workspace = true, default-features = false, optional = true }
ciborium = "0.2.1"
base64 = "0.21.0"
pin-project = "1.1.2"
pin-project = { version = "1.1.2", optional = true }
thiserror = { workspace = true, optional = true }
async-trait = "0.1.71"
bytes = "1.4.0"
tower-layer = "0.3.2"
tower = { version = "0.4.13", features = ["util"], optional = true }
tower-layer = { version = "0.3.2", optional = true }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
dioxus-hot-reload = { workspace = true }
@ -75,17 +72,14 @@ web-sys = { version = "0.3.61", features = ["Window", "Document", "Element", "Ht
manganis-cli-support = { git = "https://github.com/DioxusLabs/collect-assets", features = ["webp", "html"] }
[features]
default = ["hot-reload", "default-tls"]
default = ["hot-reload"]
router = ["dioxus-router"]
hot-reload = ["serde_json", "futures-util"]
web = ["dioxus-web"]
desktop = ["dioxus-desktop"]
warp = ["dep:warp", "ssr"]
axum = ["dep:axum", "tower-http", "ssr"]
salvo = ["dep:salvo", "ssr"]
ssr = ["server_fn/ssr", "dioxus_server_macro/ssr", "tokio", "tokio-util", "dioxus-ssr", "tower", "hyper", "http", "http-body", "dioxus-router/ssr", "tokio-stream"]
salvo = ["dep:salvo", "ssr", "http-body-util"]
ssr = ["server_fn/ssr", "dioxus_server_macro/ssr", "tokio", "tokio-util", "tokio-stream", "dioxus-ssr", "tower", "hyper", "http", "dioxus-router?/ssr", "tower-layer", "anymap", "tracing-futures", "pin-project", "thiserror"]
default-tls = ["server_fn/default-tls"]
rustls = ["server_fn/rustls"]
[dev-dependencies]
dioxus-fullstack = { path = ".", features = ["router"] }

View file

@ -9,7 +9,6 @@ publish = false
[dependencies]
dioxus = { workspace = true }
dioxus-fullstack = { workspace = true }
axum = { version = "0.6.12", optional = true }
serde = "1.0.159"
simple_logger = "4.2.0"
tracing-wasm = "0.2.1"
@ -19,5 +18,5 @@ reqwest = "0.11.18"
[features]
default = []
ssr = ["axum", "dioxus-fullstack/axum"]
ssr = ["dioxus-fullstack/axum"]
web = ["dioxus-fullstack/web"]

View file

@ -10,7 +10,6 @@ publish = false
dioxus-web = { workspace = true, features=["hydrate"], optional = true }
dioxus = { workspace = true }
dioxus-fullstack = { workspace = true }
tokio = { workspace = true, features = ["full"], optional = true }
serde = "1.0.159"
salvo = { version = "0.63.0", optional = true }
execute = "0.2.12"
@ -22,5 +21,5 @@ tracing-subscriber = "0.3.17"
[features]
default = []
ssr = ["salvo", "tokio", "dioxus-fullstack/salvo"]
ssr = ["salvo", "dioxus-fullstack/salvo"]
web = ["dioxus-web"]

View file

@ -10,7 +10,6 @@ publish = false
dioxus-web = { workspace = true, features=["hydrate"], optional = true }
dioxus = { workspace = true }
dioxus-fullstack = { workspace = true }
tokio = { workspace = true, features = ["full"], optional = true }
serde = "1.0.159"
warp = { version = "0.3.3", optional = true }
execute = "0.2.12"
@ -22,5 +21,5 @@ tracing-subscriber = "0.3.17"
[features]
default = []
ssr = ["warp", "tokio", "dioxus-fullstack/warp"]
ssr = ["warp", "dioxus-fullstack/warp"]
web = ["dioxus-web"]

View file

@ -15,7 +15,7 @@ pub(crate) fn serde_from_bytes<T: DeserializeOwned>(string: &[u8]) -> Option<T>
}
};
match postcard::from_bytes(&decompressed) {
match ciborium::from_reader(std::io::Cursor::new(decompressed)) {
Ok(data) => Some(data),
Err(err) => {
tracing::error!("Failed to deserialize: {}", err);

View file

@ -1,6 +1,6 @@
#![allow(unused)]
use std::sync::atomic::AtomicUsize;
use std::{io::Cursor, sync::atomic::AtomicUsize};
use serde::{de::DeserializeOwned, Serialize};
@ -15,7 +15,8 @@ pub(crate) struct HTMLData {
impl HTMLData {
pub(crate) fn push<T: Serialize>(&mut self, value: &T) {
let serialized = postcard::to_allocvec(value).unwrap();
let mut serialized = Vec::new();
serialize::serde_to_writable(value, &mut serialized).unwrap();
self.data.push(serialized);
}
@ -45,7 +46,7 @@ impl HTMLDataCursor {
}
let mut cursor = &self.data[current];
self.index.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
match postcard::from_bytes(cursor) {
match ciborium::from_reader(Cursor::new(cursor)) {
Ok(x) => Some(x),
Err(e) => {
tracing::error!("Error deserializing data: {:?}", e);
@ -57,7 +58,7 @@ impl HTMLDataCursor {
#[test]
fn serialized_and_deserializes() {
use postcard::to_allocvec;
use ciborium::{from_reader, into_writer};
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
struct Data {
@ -97,7 +98,9 @@ fn serialized_and_deserializes() {
"original size: {}",
std::mem::size_of::<Data>() * data.len()
);
println!("serialized size: {}", to_allocvec(&data).unwrap().len());
let mut bytes = Vec::new();
into_writer(&data, &mut bytes).unwrap();
println!("serialized size: {}", bytes.len());
println!("compressed size: {}", as_string.len());
let decoded: Vec<Data> = deserialize::serde_from_bytes(&as_string).unwrap();

View file

@ -7,8 +7,9 @@ use base64::Engine;
pub(crate) fn serde_to_writable<T: Serialize>(
value: &T,
write_to: &mut impl std::io::Write,
) -> std::io::Result<()> {
let serialized = postcard::to_allocvec(value).unwrap();
) -> Result<(), ciborium::ser::Error<std::io::Error>> {
let mut serialized = Vec::new();
ciborium::into_writer(value, &mut serialized)?;
write_to.write_all(STANDARD.encode(serialized).as_bytes())?;
Ok(())
}
@ -18,12 +19,12 @@ pub(crate) fn serde_to_writable<T: Serialize>(
pub(crate) fn encode_props_in_element<T: Serialize>(
data: &T,
write_to: &mut impl std::io::Write,
) -> std::io::Result<()> {
) -> Result<(), ciborium::ser::Error<std::io::Error>> {
write_to.write_all(
r#"<meta hidden="true" id="dioxus-storage-props" data-serialized=""#.as_bytes(),
)?;
serde_to_writable(data, write_to)?;
write_to.write_all(r#"" />"#.as_bytes())
Ok(write_to.write_all(r#"" />"#.as_bytes())?)
}
#[cfg(feature = "ssr")]
@ -31,10 +32,10 @@ pub(crate) fn encode_props_in_element<T: Serialize>(
pub(crate) fn encode_in_element(
data: &super::HTMLData,
write_to: &mut impl std::io::Write,
) -> std::io::Result<()> {
) -> Result<(), ciborium::ser::Error<std::io::Error>> {
write_to.write_all(
r#"<meta hidden="true" id="dioxus-storage-data" data-serialized=""#.as_bytes(),
)?;
serde_to_writable(&data, write_to)?;
write_to.write_all(r#"" />"#.as_bytes())
Ok(write_to.write_all(r#"" />"#.as_bytes())?)
}

View file

@ -65,7 +65,7 @@ impl SsrRendererPool {
}
if let Err(err) = renderer.render_to(&mut to, &vdom) {
let _ = tx.send(Err(
dioxus_router::prelude::IncrementalRendererError::RenderError(
dioxus_ssr::incremental::IncrementalRendererError::RenderError(
err,
),
));
@ -236,7 +236,9 @@ impl<P: Clone + Serialize + Send + Sync + 'static> dioxus_ssr::incremental::Wrap
to: &mut R,
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
// serialize the props
crate::html_storage::serialize::encode_props_in_element(&self.cfg.props, to)?;
crate::html_storage::serialize::encode_props_in_element(&self.cfg.props, to).map_err(
|err| dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(err)),
)?;
// serialize the server state
crate::html_storage::serialize::encode_in_element(
&*self.server_context.html_data().map_err(|_| {
@ -258,7 +260,8 @@ impl<P: Clone + Serialize + Send + Sync + 'static> dioxus_ssr::incremental::Wrap
}))
})?,
to,
)?;
)
.map_err(|err| dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(err)))?;
#[cfg(all(debug_assertions, feature = "hot-reload"))]
{

View file

@ -0,0 +1,28 @@
[package]
name = "dioxus-html-internal-macro"
version = { workspace = true }
edition = "2021"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
keywords = ["dom", "ui", "gui", "react", "liveview"]
license = "MIT OR Apache-2.0"
description = "HTML function macros for Dioxus"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
proc-macro2 = "1.0.66"
syn = { version = "2", features = ["full"] }
quote = "^1.0.26"
convert_case = "^0.6.0"
[lib]
proc-macro = true
[[test]]
name = "tests"
path = "tests/progress.rs"
[dev-dependencies]
trybuild = { version = "1.0.82", features = ["diff"] }

View file

@ -0,0 +1,76 @@
use proc_macro::TokenStream;
use convert_case::{Case, Casing};
use quote::{quote, ToTokens, TokenStreamExt};
use syn::__private::TokenStream2;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{braced, parse_macro_input, Ident, Token};
#[proc_macro]
pub fn impl_extension_attributes(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as ImplExtensionAttributes);
input.to_token_stream().into()
}
struct ImplExtensionAttributes {
is_element: bool,
name: Ident,
attrs: Punctuated<Ident, Token![,]>,
}
impl Parse for ImplExtensionAttributes {
fn parse(input: ParseStream) -> syn::Result<Self> {
let content;
let element: Ident = input.parse()?;
let name = input.parse()?;
braced!(content in input);
let attrs = content.parse_terminated(Ident::parse, Token![,])?;
Ok(ImplExtensionAttributes {
is_element: element == "ELEMENT",
name,
attrs,
})
}
}
impl ToTokens for ImplExtensionAttributes {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let name = &self.name;
let name_string = name.to_string();
let camel_name = name_string
.strip_prefix("r#")
.unwrap_or(&name_string)
.to_case(Case::UpperCamel);
let impl_name = Ident::new(format!("{}Impl", &camel_name).as_str(), name.span());
let extension_name = Ident::new(format!("{}Extension", &camel_name).as_str(), name.span());
if !self.is_element {
tokens.append_all(quote! {
struct #impl_name;
impl #name for #impl_name {}
});
}
let impls = self.attrs.iter().map(|ident| {
let d = if self.is_element {
quote! { #name::#ident }
} else {
quote! { <#impl_name as #name>::#ident }
};
quote! {
fn #ident(self, value: impl IntoAttributeValue<'a>) -> Self {
let d = #d;
self.push_attribute(d.0, d.1, value, d.2)
}
}
});
tokens.append_all(quote! {
pub trait #extension_name<'a>: HasAttributes<'a> + Sized {
#(#impls)*
}
});
}
}

View file

@ -0,0 +1 @@
fn main() {}

View file

@ -0,0 +1,5 @@
#[test]
fn tests() {
let t = trybuild::TestCases::new();
t.pass("tests/01-simple.rs");
}

View file

@ -12,6 +12,7 @@ keywords = ["dom", "ui", "gui", "react"]
[dependencies]
dioxus-core = { workspace = true }
dioxus-rsx = { workspace = true, features = ["hot_reload"], optional = true }
dioxus-html-internal-macro = { workspace = true }
serde = { version = "1", features = ["derive"], optional = true }
serde_repr = { version = "0.1", optional = true }
wasm-bindgen = { workspace = true, optional = true }
@ -19,7 +20,7 @@ euclid = "0.22.7"
enumset = "1.1.2"
keyboard-types = "0.7"
async-trait = "0.1.58"
serde-value = "0.7.0"
serde-value = { version = "0.7.0", optional = true }
tokio = { workspace = true, features = ["fs", "io-util"], optional = true }
rfd = { version = "0.12", optional = true }
async-channel = "1.8.0"
@ -33,6 +34,7 @@ features = [
"TouchList",
"TouchEvent",
"MouseEvent",
"DragEvent",
"InputEvent",
"ClipboardEvent",
"KeyboardEvent",
@ -50,21 +52,19 @@ serde_json = "1"
[features]
default = ["serialize", "mounted", "eval"]
serialize = [
"serde",
"serde/rc",
"serde_repr",
"serde_json",
"euclid/serde",
"keyboard-types/serde",
"dioxus-core/serialize",
"serde-value",
]
mounted = [
"web-sys/Element",
"web-sys/DomRect",
"web-sys/ScrollIntoViewOptions",
"web-sys/ScrollLogicalPosition",
"web-sys/ScrollBehavior",
"web-sys/HtmlElement",
"web-sys?/Element",
"web-sys?/DomRect",
"web-sys?/ScrollIntoViewOptions",
"web-sys?/ScrollLogicalPosition",
"web-sys?/ScrollBehavior",
"web-sys?/HtmlElement",
]
eval = [
"serde",

View file

@ -1,9 +1,14 @@
#![allow(non_upper_case_globals)]
use dioxus_core::prelude::IntoAttributeValue;
use dioxus_core::HasAttributes;
use dioxus_html_internal_macro::impl_extension_attributes;
#[cfg(feature = "hot-reload-context")]
use dioxus_rsx::HotReloadingContext;
#[cfg(feature = "hot-reload-context")]
use crate::{map_global_attributes, map_svg_attributes};
use crate::{GlobalAttributes, SvgAttributes};
#[cfg(feature = "hot-reload-context")]
use dioxus_rsx::HotReloadingContext;
pub type AttributeDiscription = (&'static str, Option<&'static str>, bool);
@ -378,6 +383,13 @@ macro_rules! builder_constructors {
}
);
)*
pub(crate) mod extensions {
use super::*;
$(
impl_extension_attributes![ELEMENT $name { $($fil,)* }];
)*
}
};
}

View file

@ -6,24 +6,31 @@ macro_rules! impl_event {
$data:ty;
$(
$( #[$attr:meta] )*
$name:ident
$name:ident $(: $js_name:literal)?
)*
) => {
$(
$( #[$attr] )*
#[inline]
pub fn $name<'a, E: crate::EventReturn<T>, T>(_cx: &'a ::dioxus_core::ScopeState, mut _f: impl FnMut(::dioxus_core::Event<$data>) -> E + 'a) -> ::dioxus_core::Attribute<'a> {
pub fn $name<'a, E: crate::EventReturn<T>, T>(_cx: &'a ::dioxus_core::ScopeState, mut _f: impl FnMut(::dioxus_core::Event<$data>) -> E + 'a) -> ::dioxus_core::MountedAttribute<'a> {
::dioxus_core::Attribute::new(
stringify!($name),
impl_event!(@name $name $($js_name)?),
_cx.listener(move |e: ::dioxus_core::Event<crate::PlatformEventData>| {
_f(e.map(|e|e.into())).spawn(_cx);
}),
None,
false,
)
).into()
}
)*
};
(@name $name:ident $js_name:literal) => {
$js_name
};
(@name $name:ident) => {
stringify!($name)
};
}
static EVENT_CONVERTER: RwLock<Option<Box<dyn HtmlEventConverter>>> = RwLock::new(None);

View file

@ -1,3 +1,4 @@
use crate::file_data::{FileEngine, HasFileData};
use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint};
use crate::input_data::{MouseButton, MouseButtonSet};
use crate::prelude::*;
@ -53,7 +54,13 @@ impl DragData {
/// Downcast this event data to a specific type
pub fn downcast<T: 'static>(&self) -> Option<&T> {
self.inner.as_any().downcast_ref::<T>()
HasDragData::as_any(&*self.inner).downcast_ref::<T>()
}
}
impl HasFileData for DragData {
fn files(&self) -> Option<std::sync::Arc<dyn FileEngine>> {
self.inner.files()
}
}
@ -103,19 +110,34 @@ impl PointerInteraction for DragData {
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
pub struct SerializedDragData {
mouse: crate::point_interaction::SerializedPointInteraction,
files: Option<crate::file_data::SerializedFileEngine>,
}
#[cfg(feature = "serialize")]
impl From<&DragData> for SerializedDragData {
fn from(data: &DragData) -> Self {
impl SerializedDragData {
fn new(drag: &DragData, files: Option<crate::file_data::SerializedFileEngine>) -> Self {
Self {
mouse: crate::point_interaction::SerializedPointInteraction::from(data),
mouse: crate::point_interaction::SerializedPointInteraction::from(drag),
files,
}
}
}
#[cfg(feature = "serialize")]
impl HasDragData for SerializedDragData {}
impl HasDragData for SerializedDragData {
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[cfg(feature = "serialize")]
impl HasFileData for SerializedDragData {
fn files(&self) -> Option<std::sync::Arc<dyn FileEngine>> {
self.files
.as_ref()
.map(|files| std::sync::Arc::new(files.clone()) as _)
}
}
#[cfg(feature = "serialize")]
impl HasMouseData for SerializedDragData {
@ -171,7 +193,7 @@ impl PointerInteraction for SerializedDragData {
#[cfg(feature = "serialize")]
impl serde::Serialize for DragData {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
SerializedDragData::from(self).serialize(serializer)
SerializedDragData::new(self, None).serialize(serializer)
}
}
@ -186,7 +208,10 @@ impl<'de> serde::Deserialize<'de> for DragData {
}
/// A trait for any object that has the data for a drag event
pub trait HasDragData: HasMouseData {}
pub trait HasDragData: HasMouseData + HasFileData {
/// return self as Any
fn as_any(&self) -> &dyn std::any::Any;
}
impl_event! {
DragData;

View file

@ -1,9 +1,25 @@
use std::{any::Any, collections::HashMap, fmt::Debug};
use crate::file_data::FileEngine;
use crate::file_data::HasFileData;
use std::{collections::HashMap, fmt::Debug};
use dioxus_core::Event;
pub type FormEvent = Event<FormData>;
/// A form value that may either be a list of values or a single value
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
// this will serialize Text(String) -> String and VecText(Vec<String>) to Vec<String>
serde(untagged)
)]
#[derive(Debug, Clone, PartialEq)]
pub enum FormValue {
Text(String),
VecText(Vec<String>),
}
/* DOMEvent: Send + SyncTarget relatedTarget */
pub struct FormData {
inner: Box<dyn HasFormData>,
}
@ -43,7 +59,7 @@ impl FormData {
}
/// Get the values of the form event
pub fn values(&self) -> HashMap<String, Vec<String>> {
pub fn values(&self) -> HashMap<String, FormValue> {
self.inner.values()
}
@ -59,30 +75,50 @@ impl FormData {
}
/// An object that has all the data for a form event
pub trait HasFormData: std::any::Any {
pub trait HasFormData: HasFileData + std::any::Any {
fn value(&self) -> String {
Default::default()
}
fn values(&self) -> HashMap<String, Vec<String>> {
fn values(&self) -> HashMap<String, FormValue> {
Default::default()
}
fn files(&self) -> Option<std::sync::Arc<dyn FileEngine>> {
None
}
/// return self as Any
fn as_any(&self) -> &dyn std::any::Any;
}
impl FormData {
#[cfg(feature = "serialize")]
/// Parse the values into a struct with one field per value
pub fn parsed_values<T>(&self) -> Result<T, serde_json::Error>
where
T: serde::de::DeserializeOwned,
{
use serde::Serialize;
fn convert_hashmap_to_json<K, V>(hashmap: &HashMap<K, V>) -> serde_json::Result<String>
where
K: Serialize + std::hash::Hash + Eq,
V: Serialize,
{
serde_json::to_string(hashmap)
}
let parsed_json =
convert_hashmap_to_json(&self.values()).expect("Failed to parse values to JSON");
serde_json::from_str(&parsed_json)
}
}
#[cfg(feature = "serialize")]
/// A serialized form data object
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
pub struct SerializedFormData {
value: String,
values: HashMap<String, Vec<String>>,
files: Option<std::sync::Arc<SerializedFileEngine>>,
values: HashMap<String, FormValue>,
files: Option<crate::file_data::SerializedFileEngine>,
}
#[cfg(feature = "serialize")]
@ -90,8 +126,8 @@ impl SerializedFormData {
/// Create a new serialized form data object
pub fn new(
value: String,
values: HashMap<String, Vec<String>>,
files: Option<std::sync::Arc<SerializedFileEngine>>,
values: HashMap<String, FormValue>,
files: Option<crate::file_data::SerializedFileEngine>,
) -> Self {
Self {
value,
@ -114,9 +150,9 @@ impl SerializedFormData {
resolved_files.insert(file, bytes.unwrap_or_default());
}
Some(std::sync::Arc::new(SerializedFileEngine {
Some(crate::file_data::SerializedFileEngine {
files: resolved_files,
}))
})
}
None => None,
},
@ -138,21 +174,24 @@ impl HasFormData for SerializedFormData {
self.value.clone()
}
fn values(&self) -> HashMap<String, Vec<String>> {
fn values(&self) -> HashMap<String, FormValue> {
self.values.clone()
}
fn files(&self) -> Option<std::sync::Arc<dyn FileEngine>> {
self.files
.as_ref()
.map(|files| std::sync::Arc::clone(files) as std::sync::Arc<dyn FileEngine>)
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[cfg(feature = "serialize")]
impl HasFileData for SerializedFormData {
fn files(&self) -> Option<std::sync::Arc<dyn FileEngine>> {
self.files
.as_ref()
.map(|files| std::sync::Arc::new(files.clone()) as _)
}
}
#[cfg(feature = "serialize")]
impl serde::Serialize for FormData {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
@ -170,52 +209,6 @@ impl<'de> serde::Deserialize<'de> for FormData {
}
}
#[cfg(feature = "serialize")]
/// A file engine that serializes files to bytes
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
pub struct SerializedFileEngine {
files: HashMap<String, Vec<u8>>,
}
#[cfg(feature = "serialize")]
#[async_trait::async_trait(?Send)]
impl FileEngine for SerializedFileEngine {
fn files(&self) -> Vec<String> {
self.files.keys().cloned().collect()
}
async fn read_file(&self, file: &str) -> Option<Vec<u8>> {
self.files.get(file).cloned()
}
async fn read_file_to_string(&self, file: &str) -> Option<String> {
self.read_file(file)
.await
.map(|bytes| String::from_utf8_lossy(&bytes).to_string())
}
async fn get_native_file(&self, file: &str) -> Option<Box<dyn Any>> {
self.read_file(file)
.await
.map(|val| Box::new(val) as Box<dyn Any>)
}
}
#[async_trait::async_trait(?Send)]
pub trait FileEngine {
// get a list of file names
fn files(&self) -> Vec<String>;
// read a file to bytes
async fn read_file(&self, file: &str) -> Option<Vec<u8>>;
// read a file to string
async fn read_file_to_string(&self, file: &str) -> Option<String>;
// returns a file in platform's native representation
async fn get_native_file(&self, file: &str) -> Option<Box<dyn Any>>;
}
impl_event! {
FormData;

Some files were not shown because too many files have changed in this diff Show more