|
| 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) |
0 commit comments