1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-29 12:50:04 -04:00

Store web_sys::File inside of DroppedFile (#8354)

* Closes #4654
* Related #4667
* [x] I have followed the instructions in the PR template

This PR avoids materializing the contents of a file that was dragged
into an egui application on the web. It does so by storing the
`web_sys::File` handle directly on WASM.

This breaks the existing API of `DroppedFile` on the web, because there
is no way to retrieve the bytes synchronously form a `DroppedFile`
anymore, forcing handling call sites to become asynchronous.

The native API remains the same.

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
Jochen Görtler
2026-08-03 17:07:37 +02:00
committed by GitHub
parent 2e7a92bc37
commit 49d4befe6b
13 changed files with 197 additions and 120 deletions

View File

@@ -0,0 +1,48 @@
use std::{
future::Future,
path::{Path, PathBuf},
pin::Pin,
};
#[derive(Debug)]
pub(crate) struct WebFile {
file: web_sys::File,
// We store a `PathBuf` here so that we can hand out `Path`s
// without allocating each time.
path: PathBuf,
}
impl From<web_sys::File> for WebFile {
fn from(file: web_sys::File) -> Self {
let path = file.name().into();
Self { file, path }
}
}
impl egui::DroppedFile for WebFile {
fn path(&self) -> &Path {
&self.path
}
fn bytes_async(&self) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, String>> + '_>> {
let file = self.file.clone();
Box::pin(async move {
if file.size() > f64::from(u32::MAX) {
return Err(format!(
"File is too large: browser file reads are limited to {} bytes",
u32::MAX
));
}
let array_buffer = file
.array_buffer()
.await
.map_err(|err| crate::web::string_from_js_value(&err))?;
Ok(js_sys::Uint8Array::new(&array_buffer).to_vec())
})
}
fn web_file(&self) -> Option<&web_sys::File> {
Some(&self.file)
}
}

View File

@@ -973,62 +973,25 @@ fn install_drag_and_drop(runner_ref: &WebRunner, target: &EventTarget) -> Result
event.prevent_default();
})?;
runner_ref.add_event_listener(target, "drop", {
let runner_ref = runner_ref.clone();
runner_ref.add_event_listener(target, "drop", |event: web_sys::DragEvent, runner| {
if let Some(data_transfer) = event.data_transfer() {
// TODO(https://github.com/emilk/egui/issues/3702): support dropping folders
runner.input.raw.hovered_files.clear();
runner.needs_repaint.repaint_asap();
move |event: web_sys::DragEvent, runner| {
if let Some(data_transfer) = event.data_transfer() {
// TODO(https://github.com/emilk/egui/issues/3702): support dropping folders
runner.input.raw.hovered_files.clear();
runner.needs_repaint.repaint_asap();
if let Some(files) = data_transfer.files() {
for i in 0..files.length() {
if let Some(file) = files.get(i) {
log::debug!("Dropped {:?} ({} bytes)", file.name(), file.size());
if let Some(files) = data_transfer.files() {
for i in 0..files.length() {
if let Some(file) = files.get(i) {
let name = file.name();
let mime = file.type_();
let last_modified = std::time::UNIX_EPOCH
+ std::time::Duration::from_millis(file.last_modified() as u64);
log::debug!("Loading {:?} ({} bytes)…", name, file.size());
let future = wasm_bindgen_futures::JsFuture::from(file.array_buffer());
let runner_ref = runner_ref.clone();
let future = async move {
match future.await {
Ok(array_buffer) => {
let bytes = js_sys::Uint8Array::new(&array_buffer).to_vec();
log::debug!("Loaded {:?} ({} bytes).", name, bytes.len());
if let Some(mut runner_lock) = runner_ref.try_lock() {
runner_lock.input.raw.dropped_files.push(
egui::DroppedFile {
name,
mime,
last_modified: Some(last_modified),
bytes: Some(bytes.into()),
..Default::default()
},
);
runner_lock.needs_repaint.repaint_asap();
}
}
Err(err) => {
log::error!(
"Failed to read file: {}",
string_from_js_value(&err)
);
}
}
};
wasm_bindgen_futures::spawn_local(future);
}
runner.input.raw.dropped_files.push(std::sync::Arc::new(
super::dropped_file::WebFile::from(file),
));
}
}
event.stop_propagation();
event.prevent_default();
}
event.stop_propagation();
event.prevent_default();
}
})?;

View File

@@ -5,6 +5,7 @@
mod app_runner;
mod backend;
mod dropped_file;
mod events;
mod input;
mod panic_handler;
@@ -207,13 +208,12 @@ fn set_clipboard_text(s: &str) {
return;
}
let promise = window.navigator().clipboard().write_text(s);
let future = wasm_bindgen_futures::JsFuture::from(promise);
let future = async move {
if let Err(err) = future.await {
if let Err(err) = promise.await {
log::error!("Copy/cut action failed: {}", string_from_js_value(&err));
}
};
wasm_bindgen_futures::spawn_local(future);
js_sys::futures::spawn_local(future);
}
}
@@ -248,16 +248,15 @@ fn set_clipboard_image(image: &egui::ColorImage) {
};
let items = js_sys::Array::of1(&item);
let promise = window.navigator().clipboard().write(&items);
let future = wasm_bindgen_futures::JsFuture::from(promise);
let future = async move {
if let Err(err) = future.await {
if let Err(err) = promise.await {
log::error!(
"Copy/cut image action failed: {}",
string_from_js_value(&err)
);
}
};
wasm_bindgen_futures::spawn_local(future);
js_sys::futures::spawn_local(future);
}
}