Skip to content

Commit 9da9673

Browse files
authored
Drop post draft (#6)
1 parent 25e8f40 commit 9da9673

File tree

4 files changed

+214
-0
lines changed

4 files changed

+214
-0
lines changed

.vscode/settings.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,11 @@
44
"language": "markdown",
55
"scheme": "file"
66
}
7+
],
8+
"cSpell.words": [
9+
"bindgen",
10+
"dragover",
11+
"gloo",
12+
"usize"
713
]
814
}
3.59 MB
Loading
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
+++
2+
title = "Drag & Drop Images into Bevy 0.15 on the web"
3+
date = 2024-12-10
4+
[extra]
5+
tags=["rust","bevy","web"]
6+
hidden = true
7+
custom_summary = "In this post we talk about how to integrate web native APIs via WASM with Bevy."
8+
+++
9+
10+
<img src="demo.gif" alt="demo" style="width: 50%; max-width: 500px" class="inline-img" />
11+
12+
In this post we talk about how to integrate web native APIs via WASM with Bevy.
13+
14+
We utilize the recently released [bevy_channel_trigger](https://crates.io/crates/bevy_channel_trigger) crate, [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen) and [gloo](https://github.com/rustwasm/gloo).
15+
16+
If you just want to jump right into the code and tinker with it, find it on [GitHub](https://github.com/rustunit/bevy_web_drop_image_as_sprite).
17+
18+
# What is the use case?
19+
20+
In this example we want to allow the user to drop a *PNG* image into our Bevy app running in the Browser. The app should load the image into the Bevy Asset machinery and display it like any other image file. See the animation to the right visualizing this.
21+
22+
The steps to making this work are:
23+
24+
1. Prepare the DOM to receive drop events
25+
2. Handle Drop-Events (containing file data)
26+
3. Forward events to Bevy
27+
4. Receive and load image data in Bevy
28+
29+
Let's dive into the details of each of these steps.
30+
31+
## 1. Prepare the DOM to receive drop events
32+
33+
The first thing we need to do is to register the event listeners for `dragover` and `drop` on the DOM element we want to receive the drop events. We will be using wasm-bindgen and gloo to be able to do this right from our rust code:
34+
35+
```rust
36+
pub fn register_drop(id: &str) -> Option<()> {
37+
let doc = gloo::utils::document();
38+
let element = doc.get_element_by_id(id)?;
39+
40+
EventListener::new_with_options(
41+
&element,
42+
"dragover",
43+
EventListenerOptions::enable_prevent_default(),
44+
move |event| {
45+
let event: DragEvent = event.clone().dyn_into().unwrap();
46+
event.stop_propagation();
47+
event.prevent_default();
48+
49+
event
50+
.data_transfer()
51+
.unwrap()
52+
.set_drop_effect("copy");
53+
event
54+
.data_transfer()
55+
.unwrap()
56+
.set_effect_allowed("all");
57+
},
58+
)
59+
.forget();
60+
61+
//...
62+
}
63+
```
64+
65+
You can find the full function [here](https://github.com/rustunit/bevy_web_drop_image_as_sprite/blob/10eb3fca875eb0edb608cf8903b314fa5cebcac9/src/web/web.rs#L7) but the important part is that the code is essentially 1:1 from how you would solve this in vanilla javascript translated to rust while looking at the [web_sys docs](https://docs.rs/web-sys/latest/web_sys/) for reference.
66+
67+
> Note that we are setting the `drop_effect` and `effect_allowed` here to make sure the browser will allow us to receive the drop and to show this on the mouse cursor to the user as well.
68+
69+
The handling of the `drop` event is a bit more involved as we need to extract the file data from the event and forward it to Bevy. We will cover this in the next section.
70+
71+
## 2. Handle Drop-Events
72+
73+
The following code is the second half of the `register_drop` function we started in the previous section. It handles the `drop` event and forwards the file data to Bevy:
74+
75+
```rust
76+
EventListener::new_with_options(
77+
&element,
78+
"drop",
79+
EventListenerOptions::enable_prevent_default(),
80+
move |event| {
81+
let event: DragEvent = event.clone().dyn_into().unwrap();
82+
event.stop_propagation();
83+
event.prevent_default();
84+
85+
let transfer = event.data_transfer().unwrap();
86+
let files = transfer.items();
87+
88+
for idx in 0..files.length() {
89+
let file = files.get(idx).unwrap();
90+
let file_info = file.get_as_file().ok().flatten().unwrap();
91+
92+
let file_reader = FileReader::new().unwrap();
93+
94+
{
95+
let file_reader = file_reader.clone();
96+
let file_info = file_info.clone();
97+
98+
// register the listener for when the file is loaded
99+
EventListener::new(&file_reader.clone(), "load", move |_event| {
100+
// read the binary data from the file
101+
let result = file_reader.result().unwrap();
102+
let result = web_sys::js_sys::Uint8Array::new(&result);
103+
let mut data: Vec<u8> = vec![0; result.length() as usize];
104+
result.copy_to(&mut data);
105+
106+
// send the binary data to our bevy app logic
107+
send_event(crate::web::WebEvent::Drop {
108+
name: file_info.name(),
109+
data,
110+
mime_type: file_info.type_(),
111+
});
112+
})
113+
.forget();
114+
}
115+
116+
// this will start the reading and trigger the above event listener eventually
117+
file_reader.read_as_array_buffer(&file_info).unwrap();
118+
}
119+
},
120+
)
121+
.forget();
122+
```
123+
Find the full function code up on GitHub [here](https://github.com/rustunit/bevy_web_drop_image_as_sprite/blob/10eb3fca875eb0edb608cf8903b314fa5cebcac9/src/web/web.rs#L7).
124+
125+
Once again the way we handle the drop and extract the binary content of the file dropped is very similar to how you would do it in vanilla javascript. We are using the `FileReader` API to read the binary data and some metadata from the file and then forward it to Bevy via `send_event`.
126+
127+
We will look into how exactly we bridge the two worlds of DOM-events and Bevy-events in the next section.
128+
129+
You will notice a lot of un-idiomatic rust here just unwrapping instead of handling the errors. This is because we are in a demo and we want to keep the code as simple as possible. In a real world application you would want to handle the errors properly.
130+
131+
> We are using `.forget` in this demo for simplicities sake which will leak the event listeners. Just like with `unwrap` it would be different in a real world application - you would want to store the event listeners in a struct and drop them when they are no longer needed.
132+
133+
## 3. Forward events to Bevy
134+
135+
In the above event listener we are calling `send_event` to forward the file data to Bevy. Lets look at how this function works:
136+
137+
```rust
138+
static SENDER: OnceLock<Option<ChannelSender<WebEvent>>> = OnceLock::new();
139+
140+
pub fn send_event(e: WebEvent) {
141+
let Some(sender) = SENDER.get().map(Option::as_ref).flatten() else {
142+
return bevy::log::error!("`WebPlugin` not installed correctly (no sender found)");
143+
};
144+
sender.send(e);
145+
}
146+
```
147+
148+
`ChannelSender` is a type from the `bevy_channel_trigger` that effectively is a multi-producer single-consumer channel (the sending part of it) that we can use to send events from the web side to the Bevy side. Exactly how we are going to receive these events in Bevy is covered in the next section.
149+
150+
> Our previous blog post dives into detail how `bevy_channel_trigger` works and how you can use it in your own projects. You can find it [here](https://rustunit.com/blog/2024/11-15-bevy-channel-trigger).
151+
152+
## 4. Receive and load image data in Bevy
153+
154+
The final piece of the puzzle is the receiving side in Bevy to process a binary blob we expect to be an image file and load it as an `Image` Asset. If that succeeds we can start using it for rendering:
155+
156+
```rust
157+
fn process_web_events(
158+
trigger: Trigger<WebEvent>,
159+
assets: Res<AssetServer>,
160+
mut sprite: Query<&mut Sprite>,
161+
) {
162+
let e = trigger.event();
163+
let WebEvent::Drop {
164+
data,
165+
mime_type,
166+
name,
167+
} = e;
168+
169+
let Ok(image) = Image::from_buffer(
170+
data,
171+
ImageType::MimeType(mime_type),
172+
CompressedImageFormats::default(),
173+
true,
174+
ImageSampler::Default,
175+
RenderAssetUsages::RENDER_WORLD,
176+
) else {
177+
warn!("could not load image: '{name}' of type {mime_type}");
178+
return;
179+
};
180+
181+
let handle = assets.add(image);
182+
183+
sprite.single_mut().image = handle;
184+
}
185+
```
186+
187+
The above function `process_web_events` is registered as an observer into our `App` and trigger anytime the `send_event` function from earlier is called.
188+
189+
At the core of it we are trying to create an `Image` from a buffer, providing the mime-type to help choosing the encoder. If it fails we either have no way to parse the file format as an image or the dropped file was no image in the first place and we return.
190+
191+
If the image loading was successful we keep the image as an asset and use the `Handle<Image>` to swap out the sprite moving up and down the screen.
192+
193+
# Conclusion
194+
195+
In the demo we have shown how to integrate web native APIs via WASM with Bevy. In this post we focused on the key aspects of the code base. There is more though, feel free to dig into the project on GitHub, run it and tinker with it.
196+
197+
Bevy is a strong tool to bring interactive applications to the web and with the help of WASM and the right crates you can integrate web native APIs with ease.
198+
199+
---
200+
201+
You need support building your Bevy or Rust project? Our team of experts can support you! [Contact us.](@/contact.md)

sass/_blog_page.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@
5252
margin-bottom: 1em;
5353
}
5454

55+
h2 {
56+
font-size: 22px;
57+
font-weight: bold;
58+
margin-top: 1.5em;
59+
margin-bottom: 1em;
60+
}
61+
5562
p {
5663
margin: 8px 0px 8px 0px;
5764
}

0 commit comments

Comments
 (0)