diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8001af650..f0b66b951 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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' }} diff --git a/Cargo.toml b/Cargo.toml index 9fc38df5f..25beb4d33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 } diff --git a/Makefile.toml b/Makefile.toml index 1bfb02217..98af226aa 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -105,6 +105,8 @@ args = [ "dioxus-router", "--exclude", "dioxus-desktop", + "--exclude", + "dioxus-mobile", ] private = true diff --git a/examples/README.md b/examples/README.md index 93530ba30..a20ffe60e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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 diff --git a/examples/calculator.rs b/examples/calculator.rs index 648ccfbc3..2f8d4ee5f 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -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); diff --git a/examples/clock.rs b/examples/clock.rs index f9c1892d9..ba47d994a 100644 --- a/examples/clock.rs +++ b/examples/clock.rs @@ -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}"); } diff --git a/examples/dynamic_asset.rs b/examples/dynamic_asset.rs index 1c004e015..51dcfac3a 100644 --- a/examples/dynamic_asset.rs +++ b/examples/dynamic_asset.rs @@ -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" } } }) diff --git a/examples/file_upload.rs b/examples/file_upload.rs index 882285ddf..b78a5b343 100644 --- a/examples/file_upload.rs +++ b/examples/file_upload.rs @@ -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() { diff --git a/examples/mobile_demo/Cargo.toml b/examples/mobile_demo/Cargo.toml index a90ff4f36..e980774e7 100644 --- a/examples/mobile_demo/Cargo.toml +++ b/examples/mobile_demo/Cargo.toml @@ -35,7 +35,7 @@ frameworks = ["WebKit"] [dependencies] anyhow = "1.0.56" log = "0.4.11" -wry = "0.34.0" +wry = "0.35.0" dioxus = { path = "../../packages/dioxus" } dioxus-desktop = { path = "../../packages/desktop", features = [ "tokio_runtime", diff --git a/examples/pattern_model.rs b/examples/pattern_model.rs index a6c35d17a..2f6cc2803 100644 --- a/examples/pattern_model.rs +++ b/examples/pattern_model.rs @@ -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() { diff --git a/examples/spread.rs b/examples/spread.rs new file mode 100644 index 000000000..45bdc90af --- /dev/null +++ b/examples/spread.rs @@ -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>, + extra_data: &'a str, + extra_data2: &'a str, +} diff --git a/examples/streams.rs b/examples/streams.rs new file mode 100644 index 000000000..005f73f53 --- /dev/null +++ b/examples/streams.rs @@ -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::pin( + stream::once(future::ready(0)).chain(stream::iter(1..).then(|second| async move { + tokio::time::sleep(Duration::from_secs(1)).await; + second + })), + ) +} diff --git a/examples/video_stream.rs b/examples/video_stream.rs index f495299f4..c1e0c3c49 100644 --- a/examples/video_stream.rs +++ b/examples/video_stream.rs @@ -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>> = - 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, diff --git a/examples/window_focus.rs b/examples/window_focus.rs index 79a5e5044..612e38d6d 100644 --- a/examples/window_focus.rs +++ b/examples/window_focus.rs @@ -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() { diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..e5c50616d --- /dev/null +++ b/flake.lock @@ -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 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..7f151a660 --- /dev/null +++ b/flake.nix @@ -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"; + ''; + }; + }; + }; +} diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs index ebbce7538..48077fa16 100644 --- a/packages/autofmt/src/element.rs +++ b/packages/autofmt/src/element.rs @@ -166,7 +166,7 @@ impl Writer<'_> { fn write_attributes( &mut self, - attributes: &[ElementAttrNamed], + attributes: &[AttributeType], key: &Option, 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 { diff --git a/packages/autofmt/src/writer.rs b/packages/autofmt/src/writer.rs index ede35d768..d569cdb9f 100644 --- a/packages/autofmt/src/writer.rs +++ b/packages/autofmt/src/writer.rs @@ -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; } diff --git a/packages/cli-config/.gitignore b/packages/cli-config/.gitignore new file mode 100644 index 000000000..6700b1332 --- /dev/null +++ b/packages/cli-config/.gitignore @@ -0,0 +1,4 @@ +/target +Cargo.lock +.DS_Store +.idea/ diff --git a/packages/cli-config/Cargo.toml b/packages/cli-config/Cargo.toml new file mode 100644 index 000000000..d6f422b3c --- /dev/null +++ b/packages/cli-config/Cargo.toml @@ -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"] diff --git a/packages/cli-config/README.md b/packages/cli-config/README.md new file mode 100644 index 000000000..ef2435713 --- /dev/null +++ b/packages/cli-config/README.md @@ -0,0 +1,5 @@ +
+

📦✨ Dioxus CLI Configuration

+
+ +The **dioxus-cli-config** contains the configuration for the **dioxus-cli**. diff --git a/packages/cli-config/src/bundle.rs b/packages/cli-config/src/bundle.rs new file mode 100644 index 000000000..07c4fec21 --- /dev/null +++ b/packages/cli-config/src/bundle.rs @@ -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, + pub publisher: Option, + pub icon: Option>, + pub resources: Option>, + pub copyright: Option, + pub category: Option, + pub short_description: Option, + pub long_description: Option, + pub external_bin: Option>, + pub deb: Option, + pub macos: Option, + pub windows: Option, +} + +#[cfg(feature = "cli")] +impl From 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>, + pub files: HashMap, + pub nsis: Option, +} + +#[cfg(feature = "cli")] +impl From 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)>, + pub template: Option, + pub fragment_paths: Vec, + pub component_group_refs: Vec, + pub component_refs: Vec, + pub feature_group_refs: Vec, + pub feature_refs: Vec, + pub merge_refs: Vec, + pub skip_webview_install: bool, + pub license: Option, + pub enable_elevated_update_task: bool, + pub banner_path: Option, + pub dialog_image_path: Option, + pub fips_compliant: bool, +} + +#[cfg(feature = "cli")] +impl From 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>, + pub minimum_system_version: Option, + pub license: Option, + pub exception_domain: Option, + pub signing_identity: Option, + pub provider_short_name: Option, + pub entitlements: Option, + pub info_plist_path: Option, +} + +#[cfg(feature = "cli")] +impl From 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, + pub certificate_thumbprint: Option, + pub timestamp_url: Option, + pub tsp: bool, + pub wix: Option, + pub icon_path: Option, + pub webview_install_mode: WebviewInstallMode, + pub webview_fixed_runtime_path: Option, + pub allow_downgrades: bool, + pub nsis: Option, +} + +#[cfg(feature = "cli")] +impl From 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, + pub license: Option, + pub header_image: Option, + pub sidebar_image: Option, + pub installer_icon: Option, + pub install_mode: NSISInstallerMode, + pub languages: Option>, + pub custom_language_files: Option>, + pub display_language_selector: bool, +} + +#[cfg(feature = "cli")] +impl From 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 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 } + } +} diff --git a/packages/cli/src/cargo.rs b/packages/cli-config/src/cargo.rs similarity index 69% rename from packages/cli/src/cargo.rs rename to packages/cli-config/src/cargo.rs index b52fa4b09..97e4040db 100644 --- a/packages/cli/src/cargo.rs +++ b/packages/cli-config/src/cargo.rs @@ -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 { +pub fn crate_root() -> Result { // 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 { 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 { + pub fn get() -> Result { 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())) } } diff --git a/packages/cli-config/src/config.rs b/packages/cli-config/src/config.rs new file mode 100644 index 000000000..716b2bac4 --- /dev/null +++ b/packages/cli-config/src/config.rs @@ -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 for CrateConfigError { + fn from(err: CargoError) -> Self { + Self::Cargo(err) + } +} + +impl From for CrateConfigError { + fn from(err: std::io::Error) -> Self { + Self::Io(err) + } +} + +#[cfg(feature = "cli")] +impl From for CrateConfigError { + fn from(err: toml::de::Error) -> Self { + Self::Toml(err) + } +} + +impl From 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) -> Result, 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::(&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 { + // 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, + + #[serde(default)] + pub sub_package: Option, +} + +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, + #[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, +} + +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, + #[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 { + vec![PathBuf::from("src"), PathBuf::from("examples")] +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct WebResourceConfig { + pub dev: WebDevResourceConfig, + pub style: Option>, + pub script: Option>, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct WebDevResourceConfig { + #[serde(default)] + pub style: Vec, + #[serde(default)] + pub script: Vec, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct WebHttpsConfig { + pub enabled: Option, + pub mkcert: Option, + pub key_path: Option, + pub cert_path: Option, +} + +#[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, + 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, + pub features: Option>, + pub target: Option, + pub cargo_args: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ExecutableType { + Binary(String), + Lib(String), + Example(String), +} + +impl CrateConfig { + #[cfg(feature = "cli")] + pub fn new(bin: Option) -> Result { + 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) -> &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) -> &mut Self { + self.cargo_args = cargo_args; + self + } +} + +fn true_bool() -> bool { + true +} diff --git a/packages/cli-config/src/lib.rs b/packages/cli-config/src/lib.rs new file mode 100644 index 000000000..da7446918 --- /dev/null +++ b/packages/cli-config/src/lib.rs @@ -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, +> = 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"); diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index 4335faa22..d22ac7031 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -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 } diff --git a/packages/cli/src/assets.rs b/packages/cli/src/assets.rs new file mode 100644 index 000000000..081c1c85c --- /dev/null +++ b/packages/cli/src/assets.rs @@ -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(); + } +} diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs index 6d1ccea05..bed60ff5a 100644 --- a/packages/cli/src/builder.rs +++ b/packages/cli/src/builder.rs @@ -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 Result 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> { let mut warning_messages: Vec = vec![]; @@ -482,7 +473,7 @@ fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result> { 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("\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, " Result> { 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> { 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(); - } -} diff --git a/packages/cli/src/cli/autoformat.rs b/packages/cli/src/cli/autoformat.rs index 2c0691d6c..c3c22c29e 100644 --- a/packages/cli/src/cli/autoformat.rs +++ b/packages/cli/src/cli/autoformat.rs @@ -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); diff --git a/packages/cli/src/cli/build.rs b/packages/cli/src/cli/build.rs index d8032f034..fb0bdfb24 100644 --- a/packages/cli/src/cli/build.rs +++ b/packages/cli/src/cli/build.rs @@ -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, 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())?; diff --git a/packages/cli/src/cli/bundle.rs b/packages/cli/src/cli/bundle.rs index aaa823373..a2231b31f 100644 --- a/packages/cli/src/cli/bundle.rs +++ b/packages/cli/src/cli/bundle.rs @@ -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 for tauri_bundler::PackageType { impl Bundle { pub fn bundle(self, bin: Option) -> 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) { diff --git a/packages/cli/src/cli/cfg.rs b/packages/cli/src/cli/cfg.rs index 62212e574..877fdff33 100644 --- a/packages/cli/src/cli/cfg.rs +++ b/packages/cli/src/cli/cfg.rs @@ -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, } -#[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 { diff --git a/packages/cli/src/cli/check.rs b/packages/cli/src/cli/check.rs index a4d84addb..0832647f2 100644 --- a/packages/cli/src/cli/check.rs +++ b/packages/cli/src/cli/check.rs @@ -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); diff --git a/packages/cli/src/cli/clean.rs b/packages/cli/src/cli/clean.rs index 9a0f0d4fb..04e0cb45b 100644 --- a/packages/cli/src/cli/clean.rs +++ b/packages/cli/src/cli/clean.rs @@ -7,7 +7,7 @@ pub struct Clean {} impl Clean { pub fn clean(self, bin: Option) -> 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))?; } diff --git a/packages/cli/src/cli/config.rs b/packages/cli/src/cli/config.rs index 45fcc2f23..1a1072c2f 100644 --- a/packages/cli/src/cli/config.rs +++ b/packages/cli/src/cli/config.rs @@ -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"); diff --git a/packages/cli/src/cli/mod.rs b/packages/cli/src/cli/mod.rs index 9b3be33bc..4295bfa9b 100644 --- a/packages/cli/src/cli/mod.rs +++ b/packages/cli/src/cli/mod.rs @@ -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::{ diff --git a/packages/cli/src/cli/serve.rs b/packages/cli/src/cli/serve.rs index 1bb24ecdb..4b0f3f457 100644 --- a/packages/cli/src/cli/serve.rs +++ b/packages/cli/src/cli/serve.rs @@ -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) -> 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)?; } diff --git a/packages/cli/src/config.rs b/packages/cli/src/config.rs deleted file mode 100644 index 5b4f23524..000000000 --- a/packages/cli/src/config.rs +++ /dev/null @@ -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) -> crate::error::Result> { - 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::(&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 { - // 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, - pub asset_dir: Option, - - pub tools: Option>, - - pub sub_package: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WebConfig { - pub app: WebAppConfig, - pub proxy: Option>, - pub watcher: WebWatcherConfig, - pub resource: WebResourceConfig, - #[serde(default)] - pub https: WebHttpsConfig, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WebAppConfig { - pub title: Option, - pub base_path: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WebProxyConfig { - pub backend: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WebWatcherConfig { - pub watch_path: Option>, - pub reload_html: Option, - pub index_on_404: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WebResourceConfig { - pub dev: WebDevResourceConfig, - pub style: Option>, - pub script: Option>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WebDevResourceConfig { - pub style: Option>, - pub script: Option>, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct WebHttpsConfig { - pub enabled: Option, - pub mkcert: Option, - pub key_path: Option, - pub cert_path: Option, -} - -#[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, - 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, - pub features: Option>, - pub target: Option, - pub cargo_args: Vec, -} - -#[derive(Debug, Clone)] -pub enum ExecutableType { - Binary(String), - Lib(String), - Example(String), -} - -impl CrateConfig { - pub fn new(bin: Option) -> Result { - 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) -> &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) -> &mut Self { - self.cargo_args = cargo_args; - self - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct BundleConfig { - pub identifier: Option, - pub publisher: Option, - pub icon: Option>, - pub resources: Option>, - pub copyright: Option, - pub category: Option, - pub short_description: Option, - pub long_description: Option, - pub external_bin: Option>, - pub deb: Option, - pub macos: Option, - pub windows: Option, -} - -impl From 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>, - pub files: HashMap, - pub nsis: Option, -} - -impl From 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)>, - pub template: Option, - pub fragment_paths: Vec, - pub component_group_refs: Vec, - pub component_refs: Vec, - pub feature_group_refs: Vec, - pub feature_refs: Vec, - pub merge_refs: Vec, - pub skip_webview_install: bool, - pub license: Option, - pub enable_elevated_update_task: bool, - pub banner_path: Option, - pub dialog_image_path: Option, - pub fips_compliant: bool, -} - -impl From 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>, - pub minimum_system_version: Option, - pub license: Option, - pub exception_domain: Option, - pub signing_identity: Option, - pub provider_short_name: Option, - pub entitlements: Option, - pub info_plist_path: Option, -} - -impl From 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, - pub certificate_thumbprint: Option, - pub timestamp_url: Option, - pub tsp: bool, - pub wix: Option, - pub icon_path: Option, - pub webview_install_mode: WebviewInstallMode, - pub webview_fixed_runtime_path: Option, - pub allow_downgrades: bool, - pub nsis: Option, -} - -impl From 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, - pub license: Option, - pub header_image: Option, - pub sidebar_image: Option, - pub installer_icon: Option, - pub install_mode: NSISInstallerMode, - pub languages: Option>, - pub custom_language_files: Option>, - pub display_language_selector: bool, -} - -impl From 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 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 } - } -} diff --git a/packages/cli/src/error.rs b/packages/cli/src/error.rs index d577b2b40..35eea7eab 100644 --- a/packages/cli/src/error.rs +++ b/packages/cli/src/error.rs @@ -72,6 +72,24 @@ impl From for Error { } } +impl From for Error { + fn from(e: dioxus_cli_config::LoadDioxusConfigError) -> Self { + Self::RuntimeError(e.to_string()) + } +} + +impl From for Error { + fn from(e: dioxus_cli_config::CargoError) -> Self { + Self::CargoError(e.to_string()) + } +} + +impl From for Error { + fn from(e: dioxus_cli_config::CrateConfigError) -> Self { + Self::RuntimeError(e.to_string()) + } +} + #[macro_export] macro_rules! custom_error { ($msg:literal $(,)?) => { diff --git a/packages/cli/src/lib.rs b/packages/cli/src/lib.rs index b27cf55a9..191c65008 100644 --- a/packages/cli/src/lib.rs +++ b/packages/cli/src/lib.rs @@ -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::*; diff --git a/packages/cli/src/main.rs b/packages/cli/src/main.rs index 0695d7ac8..54211a254 100644 --- a/packages/cli/src/main.rs +++ b/packages/cli/src/main.rs @@ -1,3 +1,4 @@ +use dioxus_cli_config::DioxusConfig; use std::path::PathBuf; use anyhow::anyhow; diff --git a/packages/cli/src/server/desktop/mod.rs b/packages/cli/src/server/desktop/mod.rs index 34f85a312..ad4b7bac8 100644 --- a/packages/cli/src/server/desktop/mod.rs +++ b/packages/cli/src/server/desktop/mod.rs @@ -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"); diff --git a/packages/cli/src/server/fullstack/mod.rs b/packages/cli/src/server/fullstack/mod.rs index fa9c77459..60d685ced 100644 --- a/packages/cli/src/server/fullstack/mod.rs +++ b/packages/cli/src/server/fullstack/mod.rs @@ -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)) diff --git a/packages/cli/src/server/mod.rs b/packages/cli/src/server/mod.rs index 895d9e51e..1fe5f2ea5 100644 --- a/packages/cli/src/server/mod.rs +++ b/packages/cli/src/server/mod.rs @@ -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 Result + 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| { diff --git a/packages/cli/src/server/output.rs b/packages/cli/src/server/output.rs index 4e148b0f1..a61dd1f59 100644 --- a/packages/cli/src/server/output.rs +++ b/packages/cli/src/server/output.rs @@ -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()); diff --git a/packages/cli/src/server/web/mod.rs b/packages/cli/src/server/web/mod.rs index 896d08643..47661e3dd 100644 --- a/packages/cli/src/server/web/mod.rs +++ b/packages/cli/src/server/web/mod.rs @@ -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| 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(()); diff --git a/packages/cli/src/server/web/proxy.rs b/packages/cli/src/server/web/proxy.rs index e35635b6b..4242a55eb 100644 --- a/packages/cli/src/server/web/proxy.rs +++ b/packages/cli/src/server/web/proxy.rs @@ -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}; diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index 1898d8bd1..ae3dc9cdd 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -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 diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index d7a580705..fff8bcbf1 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -24,6 +24,10 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result { .included_fields() .map(|f| struct_info.field_impl(f)) .collect::, _>>()?; + let extends = struct_info + .extend_fields() + .map(|f| struct_info.extends_impl(f)) + .collect::, _>>()?; 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 { #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, } 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> { - 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> { + self.fields + .iter() + .filter(|f| !f.builder_attr.extends.is_empty()) + } + + fn extend_lifetime(&self) -> syn::Result> { + let first_extend = self.extend_fields().next(); + + match first_extend { + Some(f) => { + struct VisitFirstLifetime(Option); + + 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 { + 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 = 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 { 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 = 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 = 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 { diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index f3b89ae9e..8602b93bd 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -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); diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 43b27f2e7..a116aee4d 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -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, diff --git a/packages/core/src/diff/node.rs b/packages/core/src/diff/node.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/packages/core/src/diff/node.rs @@ -0,0 +1 @@ + diff --git a/packages/core/src/error_boundary.rs b/packages/core/src/error_boundary.rs index d42f59395..26155704b 100644 --- a/packages/core/src/error_boundary.rs +++ b/packages/core/src/error_boundary.rs @@ -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(), diff --git a/packages/core/src/fragment.rs b/packages/core/src/fragment.rs index dbc8e8131..4a775a57c 100644 --- a/packages/core/src/fragment.rs +++ b/packages/core/src/fragment.rs @@ -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 { diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 13d55826b..c478ec657 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -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, }; } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index d3ad1875f..6396504a4 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -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, +} + +impl<'a> From> 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>> for MountedAttribute<'a> { + fn from(attr: &'a Vec>) -> 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, } 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 { } } } + +/// 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; +} diff --git a/packages/core/src/properties.rs b/packages/core/src/properties.rs index 3f0d9ef97..b2263e039 100644 --- a/packages/core/src/properties.rs +++ b/packages/core/src/properties.rs @@ -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))] diff --git a/packages/core/src/runtime.rs b/packages/core/src/runtime.rs index 6b5e5408a..f3735b020 100644 --- a/packages/core/src/runtime.rs +++ b/packages/core/src/runtime.rs @@ -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(&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(runtime: Rc, scope: Option, 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 { diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index e4cb256d2..ffb9e74a1 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -318,11 +318,16 @@ pub fn spawn(fut: impl Future + 'static) { with_current_scope(|cx| cx.spawn(fut)); } +/// Spawn a future on a component given its [`ScopeId`]. +pub fn spawn_at(fut: impl Future + 'static, scope_id: ScopeId) -> Option { + 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 + 'static) -> Option { - 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. diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index ef4a5771f..64a60b112 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -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, { diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 9327e8f29..28ccbc76a 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -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; } } } diff --git a/packages/core/tests/fuzzing.rs b/packages/core/tests/fuzzing.rs index 65d131eb8..af5e9d05c 100644 --- a/packages/core/tests/fuzzing.rs +++ b/packages/core/tests/fuzzing.rs @@ -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::() % 7 { 0 => AttributeValue::Text(Box::leak( format!("{}", rand::random::()).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| 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; diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 357cf63e2..d7c1bb403 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -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] diff --git a/packages/desktop/headless_tests/rendering.rs b/packages/desktop/headless_tests/rendering.rs index e12d8644d..50d7b8419 100644 --- a/packages/desktop/headless_tests/rendering.rs +++ b/packages/desktop/headless_tests/rendering.rs @@ -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(); diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs new file mode 100644 index 000000000..d2fec9d55 --- /dev/null +++ b/packages/desktop/src/app.rs @@ -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

{ + // 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>, + pub(crate) cfg: Cell>, + + // Stuff we need mutable access to + pub(crate) root: Component

, + pub(crate) control_flow: ControlFlow, + pub(crate) is_visible_before_start: bool, + pub(crate) window_behavior: WindowCloseBehaviour, + pub(crate) webviews: HashMap, + + /// 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, +} + +/// 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>, + pub(crate) shortcut_manager: ShortcutRegistry, + pub(crate) global_hotkey_channel: Receiver, + pub(crate) proxy: EventLoopProxy, + pub(crate) target: EventLoopWindowTarget, +} + +impl App

{ + pub fn new(cfg: Config, props: P, root: Component

) -> (EventLoop, Self) { + let event_loop = EventLoopBuilder::::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::(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::(msg.params()) else { + return; + }; + struct DesktopFileUploadForm { + files: Arc, + } + + impl HasFileData for DesktopFileUploadForm { + fn files(&self) -> Option> { + 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::(); + let _: () = msg_send![app, hide: nil]; + }); + } +} diff --git a/packages/desktop/src/assets.rs b/packages/desktop/src/assets.rs new file mode 100644 index 000000000..f0f5a03fc --- /dev/null +++ b/packages/desktop/src/assets.rs @@ -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>; + +pub struct AssetHandler { + f: Box, + scope: ScopeId, +} + +#[derive(Clone)] +pub struct AssetHandlerRegistry { + dom_rt: Rc, + handlers: Rc>>, +} + +impl AssetHandlerRegistry { + pub fn new(dom_rt: Rc) -> 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, + scope: ScopeId, + ) { + self.handlers + .borrow_mut() + .insert(name, AssetHandler { f, scope }); + } + + pub fn remove_handler(&self, name: &str) -> Option { + self.handlers.borrow_mut().remove(name) + } +} diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/config.rs similarity index 85% rename from packages/desktop/src/cfg.rs rename to packages/desktop/src/config.rs index 596b21938..2b4f5d0d6 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/config.rs @@ -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 bool>; +type DropHandler = Box 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(self, root: Component

, 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 diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 62a574d9b..c97272293 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -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; +#[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::()) - .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>>>, - responder: Arc>>, -} - -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) { - 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>>; +/// A handle to the [`DesktopService`] that can be passed around. +pub type DesktopContext = Rc; /// An imperative interface to the current window. /// @@ -106,26 +52,18 @@ pub(crate) type WebviewQueue = Rc>>; /// ``` pub struct DesktopService { /// The wry/tao proxy to the current window - pub webview: Rc, + pub webview: WebView, - /// The proxy to the event loop - pub proxy: ProxyType, + /// The tao window itself + pub window: Window, + + pub(crate) shared: Rc, /// The receiver for queries about the current window pub(super) query: QueryEngine, - - pub(super) pending_windows: WebviewQueue, - - pub(crate) event_loop: EventLoopWindowTarget, - - pub(crate) event_handlers: WindowEventHandlers, - - pub(crate) shortcut_manager: ShortcutRegistry, - pub(crate) edit_queue: EditQueue, pub(crate) templates: RefCell>, pub(crate) max_template_count: AtomicU16, - pub(crate) channel: RefCell, pub(crate) asset_handlers: AssetHandlerRegistry, @@ -133,47 +71,50 @@ pub struct DesktopService { pub(crate) views: Rc>>, } -/// A handle to the [`DesktopService`] that can be passed around. -pub type DesktopContext = Rc; - /// 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, - webviews: WebviewQueue, - event_handlers: WindowEventHandlers, - shortcut_manager: ShortcutRegistry, + window: Window, + shared: Rc, 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 { - 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::>() + 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, &EventLoopWindowTarget) + '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 { - 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(&self, f: impl AssetHandler) -> usize { - self.asset_handlers.register_handler(f).await + pub fn register_asset_handler( + &self, + name: String, + f: Box, + scope: Option, + ) { + 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) { - 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, &EventLoopWindowTarget) + '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, } diff --git a/packages/desktop/src/edits.rs b/packages/desktop/src/edits.rs new file mode 100644 index 000000000..4562f2ebf --- /dev/null +++ b/packages/desktop/src/edits.rs @@ -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>>>, + responder: Arc>>, +} + +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) { + 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, + max_template_count: &AtomicU16, +) -> Option> { + 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, + 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(), + } +} diff --git a/packages/desktop/src/eval.rs b/packages/desktop/src/eval.rs index e187be450..e9b0f036b 100644 --- a/packages/desktop/src/eval.rs +++ b/packages/desktop/src/eval.rs @@ -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::().unwrap(); - let provider: Rc = 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 { diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index a4fbb4e1e..ac5a3df0d 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -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; diff --git a/packages/desktop/src/file_upload.rs b/packages/desktop/src/file_upload.rs index 7b5f08fc3..98dbec437 100644 --- a/packages/desktop/src/file_upload.rs +++ b/packages/desktop/src/file_upload.rs @@ -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 { - 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 { + 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 { - fn get_file_event_for_folder( - request: &FileDialogRequest, - dialog: rfd::FileDialog, - ) -> Vec { - 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 { + fn get_file_event_for_folder( + request: &FileDialogRequest, + dialog: rfd::FileDialog, + ) -> Vec { + 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 { - 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 { + 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) + } } } diff --git a/packages/desktop/src/hooks.rs b/packages/desktop/src/hooks.rs new file mode 100644 index 000000000..85d1a170b --- /dev/null +++ b/packages/desktop/src/hooks.rs @@ -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::()) + .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, &EventLoopWindowTarget) + '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 { + cx.use_hook(move || { + let desktop = window(); + + let id = desktop.create_shortcut(accelerator.accelerator(), handler); + + Ok(ShortcutHandle { + desktop, + shortcut_id: id?, + }) + }) +} diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html index 0704fd0f0..45926e3f1 100644 --- a/packages/desktop/src/index.html +++ b/packages/desktop/src/index.html @@ -1,12 +1,12 @@ - - Dioxus app - - - - -

- - + + Dioxus app + + + + +
+ + diff --git a/packages/desktop/src/ipc.rs b/packages/desktop/src/ipc.rs new file mode 100644 index 000000000..c8810c63a --- /dev/null +++ b/packages/desktop/src/ipc.rs @@ -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 + } +} diff --git a/packages/desktop/src/launch.rs b/packages/desktop/src/launch.rs new file mode 100644 index 000000000..e53a5ea77 --- /dev/null +++ b/packages/desktop/src/launch.rs @@ -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) -> Element { +/// cx.render(rsx!{ +/// h1 {"hello {cx.props.name}!"} +/// }) +/// } +/// ``` +pub fn launch_with_props(root: Component

, 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(root: Component

, 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; + }) +} diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index b9be87071..9cd94d128 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -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) -> Element { -/// cx.render(rsx!{ -/// h1 {"hello {cx.props.name}!"} -/// }) -/// } -/// ``` -pub fn launch_with_props(root: Component

, props: P, cfg: Config) { - let event_loop = EventLoopBuilder::::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::::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::(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::() - .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::(params) { - let view = webviews.get(&event.1).unwrap(); - let query = view - .dom - .base_scope() - .consume_context::() - .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::(msg.params()) - { - struct DesktopFileUploadForm { - files: Arc, - } - - impl HasFormData for DesktopFileUploadForm { - fn files(&self) -> Option> { - 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, - proxy: &EventLoopProxy, - 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, - max_template_count: &AtomicU16, -) -> Option> { - 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, - 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::(); - 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; diff --git a/packages/desktop/src/menubar.rs b/packages/desktop/src/menubar.rs new file mode 100644 index 000000000..71e424240 --- /dev/null +++ b/packages/desktop/src/menubar.rs @@ -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 + } +} diff --git a/packages/desktop/src/mobile_shortcut.rs b/packages/desktop/src/mobile_shortcut.rs index c6144344b..03566bf36 100644 --- a/packages/desktop/src/mobile_shortcut.rs +++ b/packages/desktop/src/mobile_shortcut.rs @@ -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 { + pub fn register(&self, accelerator: HotKey) -> Result { 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(()) } } diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 19bb6e55f..06304cdd2 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -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>, + custom_head: Option, + custom_index: Option, + root_name: &str, + headless: bool, +) -> Option>> { + // 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("").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("").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>, + 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>> { + // 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>, 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#"