Skip to content

Commit 2bbfccf

Browse files
authored
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 <[email protected]> * Remove println statements Signed-off-by: Nico Burns <[email protected]> --------- Signed-off-by: Nico Burns <[email protected]>
1 parent c55a29e commit 2bbfccf

File tree

7 files changed

+227
-6
lines changed

7 files changed

+227
-6
lines changed

Cargo.lock

Lines changed: 6 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ depinfo = { path = "packages/depinfo", version = "=0.7.0-rc.3" }
216216
warnings = { version = "0.2.1" }
217217

218218
# blitz
219-
blitz-dom = { version = "0.2", default-features = false }
219+
blitz-dom = { version = "0.2.4", default-features = false }
220220
blitz-net = { version = "0.2" }
221221
blitz-html = { version = "0.2" }
222222
blitz-paint = { version = "0.2" }
@@ -336,6 +336,7 @@ inventory = { version = "0.3" }
336336
macro-string = "0.1.4"
337337
walkdir = "2.5.0"
338338
url = "2"
339+
data-url = "0.3.2"
339340
separator = "0.4.1"
340341
pretty_assertions = "1"
341342
serde_repr = "0.1"

examples/10-integrations/native-headless-in-bevy/Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,25 @@ license = "MIT"
99
publish = false
1010

1111
[features]
12+
default = ["hot-reload"]
1213
tracing = ["dep:tracing-subscriber", "dioxus-native-dom/tracing"]
14+
hot-reload = ["dep:dioxus-devtools"]
1315

1416
[dependencies]
15-
dioxus = { workspace = true }
17+
dioxus = { workspace = true, features = ["document"]}
1618
dioxus-native-dom = { workspace = true }
19+
dioxus-asset-resolver = { workspace = true, features = ["native"] }
20+
dioxus-devtools = { workspace = true, optional = true }
1721
blitz-dom = { workspace = true, default-features = true }
1822
blitz-paint = { workspace = true, default-features = true }
1923
blitz-traits = { workspace = true, default-features = true }
24+
data-url = { workspace = true, default-features = true }
2025
anyrender_vello = { workspace = true }
2126
wgpu = { workspace = true }
2227
tracing-subscriber = { workspace = true, optional = true }
2328
bevy = { workspace = true }
2429
vello = { workspace = true }
30+
bytes = { workspace = true }
2531
async-std = "1.13"
2632
crossbeam-channel = "0.5"
2733
paste = "1.0"

examples/10-integrations/native-headless-in-bevy/src/dioxus_in_bevy_plugin.rs

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::rc::Rc;
2+
use std::sync::Arc;
13
use std::time::Instant;
24

35
use bevy::asset::RenderAssetUsages;
@@ -25,9 +27,13 @@ use blitz_paint::paint_scene;
2527
use blitz_traits::events::{
2628
BlitzKeyEvent, BlitzMouseButtonEvent, KeyState, MouseEventButton, MouseEventButtons, UiEvent,
2729
};
30+
use blitz_traits::net::{NetCallback, NetProvider};
2831
use blitz_traits::shell::{ColorScheme, Viewport};
32+
use bytes::Bytes;
2933
use crossbeam_channel::{Receiver, Sender};
34+
use data_url::DataUrl;
3035
use dioxus::prelude::*;
36+
use dioxus_devtools::DevserverMsg;
3137
use dioxus_native_dom::DioxusDocument;
3238
use vello::{
3339
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
5157
{
5258
fn build(&self, app: &mut App) {
5359
let epoch = AnimationTime(Instant::now());
60+
let (s, r) = crossbeam_channel::unbounded();
5461

5562
// Create the dioxus virtual dom and the dioxus-native document
5663
// The viewport will be set in setup_ui after we get the window size
5764
let vdom = VirtualDom::new_with_props(self.ui, self.props.clone());
5865
// FIXME add a NetProvider
5966
let mut dioxus_doc = DioxusDocument::new(vdom, DocumentConfig::default());
67+
68+
// Setup NetProvider
69+
let net_provider = BevyNetProvider::shared(s.clone());
70+
dioxus_doc.set_net_provider(net_provider);
71+
72+
// Setup DocumentProxy to process CreateHeadElement messages
73+
let proxy = Rc::new(DioxusDocumentProxy::new(s.clone()));
74+
dioxus_doc.vdom.in_scope(ScopeId::ROOT, move || {
75+
provide_context(proxy as Rc<dyn dioxus::document::Document>);
76+
});
77+
6078
dioxus_doc.initial_build();
6179
dioxus_doc.resolve(0.0);
6280

@@ -67,9 +85,14 @@ impl<UIProps: std::marker::Send + std::marker::Sync + std::clone::Clone + 'stati
6785
}
6886
let waker = std::task::Waker::from(std::sync::Arc::new(NullWake));
6987

88+
// Setup devtools listener for hot-reloading
89+
dioxus_devtools::connect(move |msg| s.send(DioxusMessage::Devserver(msg)).unwrap());
90+
app.insert_resource(DioxusMessages(r));
91+
7092
app.insert_non_send_resource(dioxus_doc);
7193
app.insert_non_send_resource(waker);
7294
app.insert_resource(epoch);
95+
7396
app.add_systems(Startup, setup_ui);
7497
app.add_systems(
7598
PreUpdate,
@@ -201,6 +224,21 @@ fn extract_texture_image(
201224
}
202225
}
203226

227+
struct HeadElement {
228+
name: String,
229+
attributes: Vec<(String, String)>,
230+
contents: Option<String>,
231+
}
232+
233+
enum DioxusMessage {
234+
Devserver(DevserverMsg),
235+
CreateHeadElement(HeadElement),
236+
ResourceLoad(blitz_dom::net::Resource),
237+
}
238+
239+
#[derive(Resource, Deref)]
240+
struct DioxusMessages(Receiver<DioxusMessage>);
241+
204242
#[derive(Component)]
205243
pub struct DioxusUiQuad;
206244

@@ -255,6 +293,7 @@ fn setup_ui(
255293
#[allow(clippy::too_many_arguments)]
256294
fn update_ui(
257295
mut dioxus_doc: NonSendMut<DioxusDocument>,
296+
dioxus_messages: Res<DioxusMessages>,
258297
waker: NonSendMut<std::task::Waker>,
259298
vello_renderer: Option<NonSendMut<VelloRenderer>>,
260299
render_device: Res<RenderDevice>,
@@ -263,6 +302,33 @@ fn update_ui(
263302
animation_epoch: Res<AnimationTime>,
264303
mut cached_texture: Local<Option<RenderTexture>>,
265304
) {
305+
while let Ok(msg) = dioxus_messages.0.try_recv() {
306+
match msg {
307+
DioxusMessage::Devserver(devserver_msg) => match devserver_msg {
308+
dioxus_devtools::DevserverMsg::HotReload(hotreload_message) => {
309+
// Apply changes to vdom
310+
dioxus_devtools::apply_changes(&dioxus_doc.vdom, &hotreload_message);
311+
312+
// Reload changed assets
313+
for asset_path in &hotreload_message.assets {
314+
if let Some(url) = asset_path.to_str() {
315+
dioxus_doc.reload_resource_by_href(url);
316+
}
317+
}
318+
}
319+
dioxus_devtools::DevserverMsg::FullReloadStart => {}
320+
_ => {}
321+
},
322+
DioxusMessage::CreateHeadElement(el) => {
323+
dioxus_doc.create_head_element(&el.name, &el.attributes, &el.contents);
324+
dioxus_doc.poll(Some(std::task::Context::from_waker(&waker)));
325+
}
326+
DioxusMessage::ResourceLoad(resource) => {
327+
dioxus_doc.load_resource(resource);
328+
}
329+
};
330+
}
331+
266332
while let Ok(texture) = receiver.try_recv() {
267333
*cached_texture = Some(texture);
268334
}
@@ -519,6 +585,140 @@ fn handle_keyboard_events(
519585
// dioxus_doc.resolve();
520586
}
521587

588+
pub struct DioxusDocumentProxy {
589+
sender: Sender<DioxusMessage>,
590+
}
591+
592+
impl DioxusDocumentProxy {
593+
fn new(sender: Sender<DioxusMessage>) -> Self {
594+
Self { sender }
595+
}
596+
}
597+
598+
impl dioxus::document::Document for DioxusDocumentProxy {
599+
fn eval(&self, _js: String) -> dioxus::document::Eval {
600+
dioxus::document::NoOpDocument.eval(_js)
601+
}
602+
603+
fn create_head_element(
604+
&self,
605+
name: &str,
606+
attributes: &[(&str, String)],
607+
contents: Option<String>,
608+
) {
609+
self.sender
610+
.send(DioxusMessage::CreateHeadElement(HeadElement {
611+
name: name.to_string(),
612+
attributes: attributes
613+
.iter()
614+
.map(|(name, value)| (name.to_string(), value.clone()))
615+
.collect(),
616+
contents,
617+
}))
618+
.unwrap();
619+
}
620+
621+
fn set_title(&self, title: String) {
622+
self.create_head_element("title", &[], Some(title));
623+
}
624+
625+
fn create_meta(&self, props: dioxus::document::MetaProps) {
626+
let attributes = props.attributes();
627+
self.create_head_element("meta", &attributes, None);
628+
}
629+
630+
fn create_script(&self, props: dioxus::document::ScriptProps) {
631+
let attributes = props.attributes();
632+
self.create_head_element("script", &attributes, props.script_contents().ok());
633+
}
634+
635+
fn create_style(&self, props: dioxus::document::StyleProps) {
636+
let attributes = props.attributes();
637+
self.create_head_element("style", &attributes, props.style_contents().ok());
638+
}
639+
640+
fn create_link(&self, props: dioxus::document::LinkProps) {
641+
let attributes = props.attributes();
642+
self.create_head_element("link", &attributes, None);
643+
}
644+
645+
fn create_head_component(&self) -> bool {
646+
true
647+
}
648+
}
649+
650+
struct BevyNetCallback {
651+
sender: Sender<DioxusMessage>,
652+
}
653+
654+
use blitz_dom::net::Resource as BlitzResource;
655+
use blitz_traits::net::NetHandler;
656+
657+
impl NetCallback<BlitzResource> for BevyNetCallback {
658+
fn call(&self, _doc_id: usize, result: core::result::Result<BlitzResource, Option<String>>) {
659+
if let Ok(res) = result {
660+
self.sender.send(DioxusMessage::ResourceLoad(res)).unwrap();
661+
}
662+
}
663+
}
664+
665+
pub struct BevyNetProvider {
666+
callback: Arc<dyn NetCallback<BlitzResource> + 'static>,
667+
}
668+
impl BevyNetProvider {
669+
fn shared(sender: Sender<DioxusMessage>) -> Arc<dyn NetProvider<BlitzResource>> {
670+
Arc::new(Self::new(sender)) as _
671+
}
672+
673+
fn new(sender: Sender<DioxusMessage>) -> Self {
674+
Self {
675+
callback: Arc::new(BevyNetCallback { sender }) as _,
676+
}
677+
}
678+
}
679+
680+
impl NetProvider<BlitzResource> for BevyNetProvider {
681+
fn fetch(
682+
&self,
683+
doc_id: usize,
684+
request: blitz_traits::net::Request,
685+
handler: Box<dyn NetHandler<BlitzResource>>,
686+
) {
687+
match request.url.scheme() {
688+
// Load Dioxus assets
689+
"dioxus" => match dioxus_asset_resolver::native::serve_asset(request.url.path()) {
690+
Ok(res) => handler.bytes(doc_id, res.into_body().into(), self.callback.clone()),
691+
Err(_) => {
692+
self.callback.call(
693+
doc_id,
694+
Err(Some(String::from("Error loading Dioxus asset"))),
695+
);
696+
}
697+
},
698+
// Decode data URIs
699+
"data" => {
700+
let Ok(data_url) = DataUrl::process(request.url.as_str()) else {
701+
self.callback
702+
.call(doc_id, Err(Some(String::from("Failed to parse data uri"))));
703+
return;
704+
};
705+
let Ok(decoded) = data_url.decode_to_vec() else {
706+
self.callback
707+
.call(doc_id, Err(Some(String::from("Failed to decode data uri"))));
708+
return;
709+
};
710+
let bytes = Bytes::from(decoded.0);
711+
handler.bytes(doc_id, bytes, Arc::clone(&self.callback));
712+
}
713+
// TODO: support http requests
714+
_ => {
715+
self.callback
716+
.call(doc_id, Err(Some(String::from("UnsupportedScheme"))));
717+
}
718+
}
719+
}
720+
}
721+
522722
fn bevy_key_to_blitz_key(key: &BevyKey) -> Key {
523723
match key {
524724
BevyKey::Character(c) => Key::Character(c.to_string()),

examples/10-integrations/native-headless-in-bevy/src/ui.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ pub fn ui(props: UIProps) -> Element {
110110
println!("rgba({r}, {g}, {b}, {a})");
111111

112112
rsx! {
113-
style { {include_str!("./ui.css")} }
113+
document::Stylesheet { href: asset!("/src/ui.css") }
114114
div {
115115
id: "panel",
116116
class: "catch-events",

packages/native/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ svg = ["blitz-dom/svg", "blitz-paint/svg"]
1717
net = ["dep:tokio", "dep:blitz-net"]
1818
html = ["dep:blitz-html"]
1919
accessibility = ["blitz-shell/accessibility", "blitz-dom/accessibility"]
20-
tracing = ["dep:tracing", "blitz-shell/tracing", "blitz-dom/tracing"]
20+
tracing = ["dep:tracing", "dioxus-native-dom/tracing", "blitz-shell/tracing", "blitz-dom/tracing"]
2121
hot-reload = ["dep:dioxus-cli-config", "dep:dioxus-devtools"]
2222
system-fonts = ["blitz-dom/system_fonts"]
2323
autofocus = []

packages/native/src/dioxus_application.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,17 @@ impl DioxusNativeApplication {
7070
dioxus_devtools::DevserverMsg::HotReload(hotreload_message) => {
7171
for window in self.inner.windows.values_mut() {
7272
let doc = window.downcast_doc_mut::<DioxusDocument>();
73+
74+
// Apply changes to vdom
7375
dioxus_devtools::apply_changes(&doc.vdom, hotreload_message);
76+
77+
// Reload changed assets
78+
for asset_path in &hotreload_message.assets {
79+
if let Some(url) = asset_path.to_str() {
80+
doc.reload_resource_by_href(url);
81+
}
82+
}
83+
7484
window.poll();
7585
}
7686
}

0 commit comments

Comments
 (0)