Native: implement hot-reloading of stylesheets (#4830)

* Make `tracing` feature of dioxus-native enable `tracing` feature of dioxus-native-dom

* Native: implement hotreloading of stylesheet assets

* Add asset support for native-headless-in-bevy example

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Remove println statements

Signed-off-by: Nico Burns <nico@nicoburns.com>

---------

Signed-off-by: Nico Burns <nico@nicoburns.com>
This commit is contained in:
Nico Burns authored and GitHub committed 2025-10-24 15:56:55 -07:00
1 parent c55a29e930
commit 2bbfccfd04
7 files changed
+227 -6

No files matched your search

Generated
+6 -2
View File
@@ -2977,9 +2977,9 @@ dependencies = [
[[package]] [[package]]
name = "blitz-dom" name = "blitz-dom"
version = "0.2.2" version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "961605e9c4cc60eb93bc54172650d3f7b868d15a6e3345419f5e76d1df367b45" checksum = "c81d0f520bad6798a54cc4360bcabd245dc0aba2ce5917217d1d82ac5ee6bfed"
dependencies = [ dependencies = [
"accesskit 0.17.1", "accesskit 0.17.1",
"app_units", "app_units",
@@ -11195,8 +11195,12 @@ dependencies = [
"blitz-dom", "blitz-dom",
"blitz-paint", "blitz-paint",
"blitz-traits", "blitz-traits",
"bytes",
"crossbeam-channel", "crossbeam-channel",
"data-url 0.3.2",
"dioxus", "dioxus",
"dioxus-asset-resolver",
"dioxus-devtools",
"dioxus-native-dom", "dioxus-native-dom",
"paste", "paste",
"rustc-hash 1.1.0", "rustc-hash 1.1.0",
+2 -1
View File
@@ -216,7 +216,7 @@ depinfo = { path = "packages/depinfo", version = "=0.7.0-rc.3" }
warnings = { version = "0.2.1" } warnings = { version = "0.2.1" }
# blitz # blitz
blitz-dom = { version = "0.2", default-features = false } blitz-dom = { version = "0.2.4", default-features = false }
blitz-net = { version = "0.2" } blitz-net = { version = "0.2" }
blitz-html = { version = "0.2" } blitz-html = { version = "0.2" }
blitz-paint = { version = "0.2" } blitz-paint = { version = "0.2" }
@@ -336,6 +336,7 @@ inventory = { version = "0.3" }
macro-string = "0.1.4" macro-string = "0.1.4"
walkdir = "2.5.0" walkdir = "2.5.0"
url = "2" url = "2"
data-url = "0.3.2"
separator = "0.4.1" separator = "0.4.1"
pretty_assertions = "1" pretty_assertions = "1"
serde_repr = "0.1" serde_repr = "0.1"
@@ -9,19 +9,25 @@ license = "MIT"
publish = false publish = false
[features] [features]
default = ["hot-reload"]
tracing = ["dep:tracing-subscriber", "dioxus-native-dom/tracing"] tracing = ["dep:tracing-subscriber", "dioxus-native-dom/tracing"]
hot-reload = ["dep:dioxus-devtools"]
[dependencies] [dependencies]
dioxus = { workspace = true } dioxus = { workspace = true, features = ["document"]}
dioxus-native-dom = { workspace = true } 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-dom = { workspace = true, default-features = true }
blitz-paint = { workspace = true, default-features = true } blitz-paint = { workspace = true, default-features = true }
blitz-traits = { workspace = true, default-features = true } blitz-traits = { workspace = true, default-features = true }
data-url = { workspace = true, default-features = true }
anyrender_vello = { workspace = true } anyrender_vello = { workspace = true }
wgpu = { workspace = true } wgpu = { workspace = true }
tracing-subscriber = { workspace = true, optional = true } tracing-subscriber = { workspace = true, optional = true }
bevy = { workspace = true } bevy = { workspace = true }
vello = { workspace = true } vello = { workspace = true }
bytes = { workspace = true }
async-std = "1.13" async-std = "1.13"
crossbeam-channel = "0.5" crossbeam-channel = "0.5"
paste = "1.0" paste = "1.0"
@@ -1,3 +1,5 @@
use std::rc::Rc;
use std::sync::Arc;
use std::time::Instant; use std::time::Instant;
use bevy::asset::RenderAssetUsages; use bevy::asset::RenderAssetUsages;
@@ -25,9 +27,13 @@ use blitz_paint::paint_scene;
use blitz_traits::events::{ use blitz_traits::events::{
BlitzKeyEvent, BlitzMouseButtonEvent, KeyState, MouseEventButton, MouseEventButtons, UiEvent, BlitzKeyEvent, BlitzMouseButtonEvent, KeyState, MouseEventButton, MouseEventButtons, UiEvent,
}; };
use blitz_traits::net::{NetCallback, NetProvider};
use blitz_traits::shell::{ColorScheme, Viewport}; use blitz_traits::shell::{ColorScheme, Viewport};
use bytes::Bytes;
use crossbeam_channel::{Receiver, Sender}; use crossbeam_channel::{Receiver, Sender};
use data_url::DataUrl;
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_devtools::DevserverMsg;
use dioxus_native_dom::DioxusDocument; use dioxus_native_dom::DioxusDocument;
use vello::{ use vello::{
peniko::color::AlphaColor, RenderParams, Renderer as VelloRenderer, RendererOptions, Scene, peniko::color::AlphaColor, RenderParams, Renderer as VelloRenderer, RendererOptions, Scene,
@@ -51,12 +57,24 @@ impl<UIProps: std::marker::Send + std::marker::Sync + std::clone::Clone + 'stati
{ {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
let epoch = AnimationTime(Instant::now()); let epoch = AnimationTime(Instant::now());
let (s, r) = crossbeam_channel::unbounded();
// Create the dioxus virtual dom and the dioxus-native document // Create the dioxus virtual dom and the dioxus-native document
// The viewport will be set in setup_ui after we get the window size // The viewport will be set in setup_ui after we get the window size
let vdom = VirtualDom::new_with_props(self.ui, self.props.clone()); let vdom = VirtualDom::new_with_props(self.ui, self.props.clone());
// FIXME add a NetProvider // FIXME add a NetProvider
let mut dioxus_doc = DioxusDocument::new(vdom, DocumentConfig::default()); let mut dioxus_doc = DioxusDocument::new(vdom, DocumentConfig::default());
// Setup NetProvider
let net_provider = BevyNetProvider::shared(s.clone());
dioxus_doc.set_net_provider(net_provider);
// Setup DocumentProxy to process CreateHeadElement messages
let proxy = Rc::new(DioxusDocumentProxy::new(s.clone()));
dioxus_doc.vdom.in_scope(ScopeId::ROOT, move || {
provide_context(proxy as Rc<dyn dioxus::document::Document>);
});
dioxus_doc.initial_build(); dioxus_doc.initial_build();
dioxus_doc.resolve(0.0); dioxus_doc.resolve(0.0);
@@ -67,9 +85,14 @@ impl<UIProps: std::marker::Send + std::marker::Sync + std::clone::Clone + 'stati
} }
let waker = std::task::Waker::from(std::sync::Arc::new(NullWake)); let waker = std::task::Waker::from(std::sync::Arc::new(NullWake));
// Setup devtools listener for hot-reloading
dioxus_devtools::connect(move |msg| s.send(DioxusMessage::Devserver(msg)).unwrap());
app.insert_resource(DioxusMessages(r));
app.insert_non_send_resource(dioxus_doc); app.insert_non_send_resource(dioxus_doc);
app.insert_non_send_resource(waker); app.insert_non_send_resource(waker);
app.insert_resource(epoch); app.insert_resource(epoch);
app.add_systems(Startup, setup_ui); app.add_systems(Startup, setup_ui);
app.add_systems( app.add_systems(
PreUpdate, PreUpdate,
@@ -201,6 +224,21 @@ fn extract_texture_image(
} }
} }
struct HeadElement {
name: String,
attributes: Vec<(String, String)>,
contents: Option<String>,
}
enum DioxusMessage {
Devserver(DevserverMsg),
CreateHeadElement(HeadElement),
ResourceLoad(blitz_dom::net::Resource),
}
#[derive(Resource, Deref)]
struct DioxusMessages(Receiver<DioxusMessage>);
#[derive(Component)] #[derive(Component)]
pub struct DioxusUiQuad; pub struct DioxusUiQuad;
@@ -255,6 +293,7 @@ fn setup_ui(
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn update_ui( fn update_ui(
mut dioxus_doc: NonSendMut<DioxusDocument>, mut dioxus_doc: NonSendMut<DioxusDocument>,
dioxus_messages: Res<DioxusMessages>,
waker: NonSendMut<std::task::Waker>, waker: NonSendMut<std::task::Waker>,
vello_renderer: Option<NonSendMut<VelloRenderer>>, vello_renderer: Option<NonSendMut<VelloRenderer>>,
render_device: Res<RenderDevice>, render_device: Res<RenderDevice>,
@@ -263,6 +302,33 @@ fn update_ui(
animation_epoch: Res<AnimationTime>, animation_epoch: Res<AnimationTime>,
mut cached_texture: Local<Option<RenderTexture>>, mut cached_texture: Local<Option<RenderTexture>>,
) { ) {
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() { while let Ok(texture) = receiver.try_recv() {
*cached_texture = Some(texture); *cached_texture = Some(texture);
} }
@@ -519,6 +585,140 @@ fn handle_keyboard_events(
// dioxus_doc.resolve(); // dioxus_doc.resolve();
} }
pub struct DioxusDocumentProxy {
sender: Sender<DioxusMessage>,
}
impl DioxusDocumentProxy {
fn new(sender: Sender<DioxusMessage>) -> 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<String>,
) {
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<DioxusMessage>,
}
use blitz_dom::net::Resource as BlitzResource;
use blitz_traits::net::NetHandler;
impl NetCallback<BlitzResource> for BevyNetCallback {
fn call(&self, _doc_id: usize, result: core::result::Result<BlitzResource, Option<String>>) {
if let Ok(res) = result {
self.sender.send(DioxusMessage::ResourceLoad(res)).unwrap();
}
}
}
pub struct BevyNetProvider {
callback: Arc<dyn NetCallback<BlitzResource> + 'static>,
}
impl BevyNetProvider {
fn shared(sender: Sender<DioxusMessage>) -> Arc<dyn NetProvider<BlitzResource>> {
Arc::new(Self::new(sender)) as _
}
fn new(sender: Sender<DioxusMessage>) -> Self {
Self {
callback: Arc::new(BevyNetCallback { sender }) as _,
}
}
}
impl NetProvider<BlitzResource> for BevyNetProvider {
fn fetch(
&self,
doc_id: usize,
request: blitz_traits::net::Request,
handler: Box<dyn NetHandler<BlitzResource>>,
) {
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 { fn bevy_key_to_blitz_key(key: &BevyKey) -> Key {
match key { match key {
BevyKey::Character(c) => Key::Character(c.to_string()), BevyKey::Character(c) => Key::Character(c.to_string()),
@@ -110,7 +110,7 @@ pub fn ui(props: UIProps) -> Element {
println!("rgba({r}, {g}, {b}, {a})"); println!("rgba({r}, {g}, {b}, {a})");
rsx! { rsx! {
style { {include_str!("./ui.css")} } document::Stylesheet { href: asset!("/src/ui.css") }
div { div {
id: "panel", id: "panel",
class: "catch-events", class: "catch-events",
+1 -1
View File
@@ -17,7 +17,7 @@ svg = ["blitz-dom/svg", "blitz-paint/svg"]
net = ["dep:tokio", "dep:blitz-net"] net = ["dep:tokio", "dep:blitz-net"]
html = ["dep:blitz-html"] html = ["dep:blitz-html"]
accessibility = ["blitz-shell/accessibility", "blitz-dom/accessibility"] 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"] hot-reload = ["dep:dioxus-cli-config", "dep:dioxus-devtools"]
system-fonts = ["blitz-dom/system_fonts"] system-fonts = ["blitz-dom/system_fonts"]
autofocus = [] autofocus = []
+10
View File
@@ -70,7 +70,17 @@ impl DioxusNativeApplication {
dioxus_devtools::DevserverMsg::HotReload(hotreload_message) => { dioxus_devtools::DevserverMsg::HotReload(hotreload_message) => {
for window in self.inner.windows.values_mut() { for window in self.inner.windows.values_mut() {
let doc = window.downcast_doc_mut::<DioxusDocument>(); let doc = window.downcast_doc_mut::<DioxusDocument>();
// Apply changes to vdom
dioxus_devtools::apply_changes(&doc.vdom, hotreload_message); 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(); window.poll();
} }
} }