diff --git a/Cargo.lock b/Cargo.lock index 318db752e7..0bfce99b8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2977,9 +2977,9 @@ dependencies = [ [[package]] name = "blitz-dom" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "961605e9c4cc60eb93bc54172650d3f7b868d15a6e3345419f5e76d1df367b45" +checksum = "c81d0f520bad6798a54cc4360bcabd245dc0aba2ce5917217d1d82ac5ee6bfed" dependencies = [ "accesskit 0.17.1", "app_units", @@ -11195,8 +11195,12 @@ dependencies = [ "blitz-dom", "blitz-paint", "blitz-traits", + "bytes", "crossbeam-channel", + "data-url 0.3.2", "dioxus", + "dioxus-asset-resolver", + "dioxus-devtools", "dioxus-native-dom", "paste", "rustc-hash 1.1.0", diff --git a/Cargo.toml b/Cargo.toml index b7ab62285c..96b0503ddb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -216,7 +216,7 @@ depinfo = { path = "packages/depinfo", version = "=0.7.0-rc.3" } warnings = { version = "0.2.1" } # blitz -blitz-dom = { version = "0.2", default-features = false } +blitz-dom = { version = "0.2.4", default-features = false } blitz-net = { version = "0.2" } blitz-html = { version = "0.2" } blitz-paint = { version = "0.2" } @@ -336,6 +336,7 @@ inventory = { version = "0.3" } macro-string = "0.1.4" walkdir = "2.5.0" url = "2" +data-url = "0.3.2" separator = "0.4.1" pretty_assertions = "1" serde_repr = "0.1" diff --git a/examples/10-integrations/native-headless-in-bevy/Cargo.toml b/examples/10-integrations/native-headless-in-bevy/Cargo.toml index 6d49ba6206..74bbf252e9 100644 --- a/examples/10-integrations/native-headless-in-bevy/Cargo.toml +++ b/examples/10-integrations/native-headless-in-bevy/Cargo.toml @@ -9,19 +9,25 @@ license = "MIT" publish = false [features] +default = ["hot-reload"] tracing = ["dep:tracing-subscriber", "dioxus-native-dom/tracing"] +hot-reload = ["dep:dioxus-devtools"] [dependencies] -dioxus = { workspace = true } +dioxus = { workspace = true, features = ["document"]} dioxus-native-dom = { workspace = true } +dioxus-asset-resolver = { workspace = true, features = ["native"] } +dioxus-devtools = { workspace = true, optional = true } blitz-dom = { workspace = true, default-features = true } blitz-paint = { workspace = true, default-features = true } blitz-traits = { workspace = true, default-features = true } +data-url = { workspace = true, default-features = true } anyrender_vello = { workspace = true } wgpu = { workspace = true } tracing-subscriber = { workspace = true, optional = true } bevy = { workspace = true } vello = { workspace = true } +bytes = { workspace = true } async-std = "1.13" crossbeam-channel = "0.5" paste = "1.0" diff --git a/examples/10-integrations/native-headless-in-bevy/src/dioxus_in_bevy_plugin.rs b/examples/10-integrations/native-headless-in-bevy/src/dioxus_in_bevy_plugin.rs index 5df939a88f..b3134844a3 100644 --- a/examples/10-integrations/native-headless-in-bevy/src/dioxus_in_bevy_plugin.rs +++ b/examples/10-integrations/native-headless-in-bevy/src/dioxus_in_bevy_plugin.rs @@ -1,3 +1,5 @@ +use std::rc::Rc; +use std::sync::Arc; use std::time::Instant; use bevy::asset::RenderAssetUsages; @@ -25,9 +27,13 @@ use blitz_paint::paint_scene; use blitz_traits::events::{ BlitzKeyEvent, BlitzMouseButtonEvent, KeyState, MouseEventButton, MouseEventButtons, UiEvent, }; +use blitz_traits::net::{NetCallback, NetProvider}; use blitz_traits::shell::{ColorScheme, Viewport}; +use bytes::Bytes; use crossbeam_channel::{Receiver, Sender}; +use data_url::DataUrl; use dioxus::prelude::*; +use dioxus_devtools::DevserverMsg; use dioxus_native_dom::DioxusDocument; use vello::{ peniko::color::AlphaColor, RenderParams, Renderer as VelloRenderer, RendererOptions, Scene, @@ -51,12 +57,24 @@ impl); + }); + dioxus_doc.initial_build(); dioxus_doc.resolve(0.0); @@ -67,9 +85,14 @@ impl, + contents: Option, +} + +enum DioxusMessage { + Devserver(DevserverMsg), + CreateHeadElement(HeadElement), + ResourceLoad(blitz_dom::net::Resource), +} + +#[derive(Resource, Deref)] +struct DioxusMessages(Receiver); + #[derive(Component)] pub struct DioxusUiQuad; @@ -255,6 +293,7 @@ fn setup_ui( #[allow(clippy::too_many_arguments)] fn update_ui( mut dioxus_doc: NonSendMut, + dioxus_messages: Res, waker: NonSendMut, vello_renderer: Option>, render_device: Res, @@ -263,6 +302,33 @@ fn update_ui( animation_epoch: Res, mut cached_texture: Local>, ) { + while let Ok(msg) = dioxus_messages.0.try_recv() { + match msg { + DioxusMessage::Devserver(devserver_msg) => match devserver_msg { + dioxus_devtools::DevserverMsg::HotReload(hotreload_message) => { + // Apply changes to vdom + dioxus_devtools::apply_changes(&dioxus_doc.vdom, &hotreload_message); + + // Reload changed assets + for asset_path in &hotreload_message.assets { + if let Some(url) = asset_path.to_str() { + dioxus_doc.reload_resource_by_href(url); + } + } + } + dioxus_devtools::DevserverMsg::FullReloadStart => {} + _ => {} + }, + DioxusMessage::CreateHeadElement(el) => { + dioxus_doc.create_head_element(&el.name, &el.attributes, &el.contents); + dioxus_doc.poll(Some(std::task::Context::from_waker(&waker))); + } + DioxusMessage::ResourceLoad(resource) => { + dioxus_doc.load_resource(resource); + } + }; + } + while let Ok(texture) = receiver.try_recv() { *cached_texture = Some(texture); } @@ -519,6 +585,140 @@ fn handle_keyboard_events( // dioxus_doc.resolve(); } +pub struct DioxusDocumentProxy { + sender: Sender, +} + +impl DioxusDocumentProxy { + fn new(sender: Sender) -> Self { + Self { sender } + } +} + +impl dioxus::document::Document for DioxusDocumentProxy { + fn eval(&self, _js: String) -> dioxus::document::Eval { + dioxus::document::NoOpDocument.eval(_js) + } + + fn create_head_element( + &self, + name: &str, + attributes: &[(&str, String)], + contents: Option, + ) { + self.sender + .send(DioxusMessage::CreateHeadElement(HeadElement { + name: name.to_string(), + attributes: attributes + .iter() + .map(|(name, value)| (name.to_string(), value.clone())) + .collect(), + contents, + })) + .unwrap(); + } + + fn set_title(&self, title: String) { + self.create_head_element("title", &[], Some(title)); + } + + fn create_meta(&self, props: dioxus::document::MetaProps) { + let attributes = props.attributes(); + self.create_head_element("meta", &attributes, None); + } + + fn create_script(&self, props: dioxus::document::ScriptProps) { + let attributes = props.attributes(); + self.create_head_element("script", &attributes, props.script_contents().ok()); + } + + fn create_style(&self, props: dioxus::document::StyleProps) { + let attributes = props.attributes(); + self.create_head_element("style", &attributes, props.style_contents().ok()); + } + + fn create_link(&self, props: dioxus::document::LinkProps) { + let attributes = props.attributes(); + self.create_head_element("link", &attributes, None); + } + + fn create_head_component(&self) -> bool { + true + } +} + +struct BevyNetCallback { + sender: Sender, +} + +use blitz_dom::net::Resource as BlitzResource; +use blitz_traits::net::NetHandler; + +impl NetCallback for BevyNetCallback { + fn call(&self, _doc_id: usize, result: core::result::Result>) { + if let Ok(res) = result { + self.sender.send(DioxusMessage::ResourceLoad(res)).unwrap(); + } + } +} + +pub struct BevyNetProvider { + callback: Arc + 'static>, +} +impl BevyNetProvider { + fn shared(sender: Sender) -> Arc> { + Arc::new(Self::new(sender)) as _ + } + + fn new(sender: Sender) -> Self { + Self { + callback: Arc::new(BevyNetCallback { sender }) as _, + } + } +} + +impl NetProvider for BevyNetProvider { + fn fetch( + &self, + doc_id: usize, + request: blitz_traits::net::Request, + handler: Box>, + ) { + match request.url.scheme() { + // Load Dioxus assets + "dioxus" => match dioxus_asset_resolver::native::serve_asset(request.url.path()) { + Ok(res) => handler.bytes(doc_id, res.into_body().into(), self.callback.clone()), + Err(_) => { + self.callback.call( + doc_id, + Err(Some(String::from("Error loading Dioxus asset"))), + ); + } + }, + // Decode data URIs + "data" => { + let Ok(data_url) = DataUrl::process(request.url.as_str()) else { + self.callback + .call(doc_id, Err(Some(String::from("Failed to parse data uri")))); + return; + }; + let Ok(decoded) = data_url.decode_to_vec() else { + self.callback + .call(doc_id, Err(Some(String::from("Failed to decode data uri")))); + return; + }; + let bytes = Bytes::from(decoded.0); + handler.bytes(doc_id, bytes, Arc::clone(&self.callback)); + } + // TODO: support http requests + _ => { + self.callback + .call(doc_id, Err(Some(String::from("UnsupportedScheme")))); + } + } + } +} + fn bevy_key_to_blitz_key(key: &BevyKey) -> Key { match key { BevyKey::Character(c) => Key::Character(c.to_string()), diff --git a/examples/10-integrations/native-headless-in-bevy/src/ui.rs b/examples/10-integrations/native-headless-in-bevy/src/ui.rs index 829354711a..c7d07d55f8 100644 --- a/examples/10-integrations/native-headless-in-bevy/src/ui.rs +++ b/examples/10-integrations/native-headless-in-bevy/src/ui.rs @@ -110,7 +110,7 @@ pub fn ui(props: UIProps) -> Element { println!("rgba({r}, {g}, {b}, {a})"); rsx! { - style { {include_str!("./ui.css")} } + document::Stylesheet { href: asset!("/src/ui.css") } div { id: "panel", class: "catch-events", diff --git a/packages/native/Cargo.toml b/packages/native/Cargo.toml index fbf4e7d7cc..1da72434ea 100644 --- a/packages/native/Cargo.toml +++ b/packages/native/Cargo.toml @@ -17,7 +17,7 @@ svg = ["blitz-dom/svg", "blitz-paint/svg"] net = ["dep:tokio", "dep:blitz-net"] html = ["dep:blitz-html"] accessibility = ["blitz-shell/accessibility", "blitz-dom/accessibility"] -tracing = ["dep:tracing", "blitz-shell/tracing", "blitz-dom/tracing"] +tracing = ["dep:tracing", "dioxus-native-dom/tracing", "blitz-shell/tracing", "blitz-dom/tracing"] hot-reload = ["dep:dioxus-cli-config", "dep:dioxus-devtools"] system-fonts = ["blitz-dom/system_fonts"] autofocus = [] diff --git a/packages/native/src/dioxus_application.rs b/packages/native/src/dioxus_application.rs index bbb8739d02..65033d0628 100644 --- a/packages/native/src/dioxus_application.rs +++ b/packages/native/src/dioxus_application.rs @@ -70,7 +70,17 @@ impl DioxusNativeApplication { dioxus_devtools::DevserverMsg::HotReload(hotreload_message) => { for window in self.inner.windows.values_mut() { let doc = window.downcast_doc_mut::(); + + // Apply changes to vdom dioxus_devtools::apply_changes(&doc.vdom, hotreload_message); + + // Reload changed assets + for asset_path in &hotreload_message.assets { + if let Some(url) = asset_path.to_str() { + doc.reload_resource_by_href(url); + } + } + window.poll(); } }