mirror of
https://github.com/emilk/egui.git
synced 2026-08-29 04:40:03 -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:
@@ -1284,7 +1284,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"static_assertions",
|
"static_assertions",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"web-time",
|
"web-time",
|
||||||
"wgpu",
|
"wgpu",
|
||||||
@@ -1311,6 +1310,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -209,7 +209,6 @@ image = { workspace = true, features = ["png"] } # For copying images
|
|||||||
js-sys.workspace = true
|
js-sys.workspace = true
|
||||||
percent-encoding.workspace = true
|
percent-encoding.workspace = true
|
||||||
wasm-bindgen.workspace = true
|
wasm-bindgen.workspace = true
|
||||||
wasm-bindgen-futures.workspace = true
|
|
||||||
web-sys = { workspace = true, features = [
|
web-sys = { workspace = true, features = [
|
||||||
"AddEventListenerOptions",
|
"AddEventListenerOptions",
|
||||||
"BinaryType",
|
"BinaryType",
|
||||||
|
|||||||
48
crates/eframe/src/web/dropped_file.rs
Normal file
48
crates/eframe/src/web/dropped_file.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -973,10 +973,7 @@ fn install_drag_and_drop(runner_ref: &WebRunner, target: &EventTarget) -> Result
|
|||||||
event.prevent_default();
|
event.prevent_default();
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
runner_ref.add_event_listener(target, "drop", {
|
runner_ref.add_event_listener(target, "drop", |event: web_sys::DragEvent, runner| {
|
||||||
let runner_ref = runner_ref.clone();
|
|
||||||
|
|
||||||
move |event: web_sys::DragEvent, runner| {
|
|
||||||
if let Some(data_transfer) = event.data_transfer() {
|
if let Some(data_transfer) = event.data_transfer() {
|
||||||
// TODO(https://github.com/emilk/egui/issues/3702): support dropping folders
|
// TODO(https://github.com/emilk/egui/issues/3702): support dropping folders
|
||||||
runner.input.raw.hovered_files.clear();
|
runner.input.raw.hovered_files.clear();
|
||||||
@@ -985,51 +982,17 @@ fn install_drag_and_drop(runner_ref: &WebRunner, target: &EventTarget) -> Result
|
|||||||
if let Some(files) = data_transfer.files() {
|
if let Some(files) = data_transfer.files() {
|
||||||
for i in 0..files.length() {
|
for i in 0..files.length() {
|
||||||
if let Some(file) = files.get(i) {
|
if let Some(file) = files.get(i) {
|
||||||
let name = file.name();
|
log::debug!("Dropped {:?} ({} bytes)", file.name(), file.size());
|
||||||
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());
|
runner.input.raw.dropped_files.push(std::sync::Arc::new(
|
||||||
|
super::dropped_file::WebFile::from(file),
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
event.stop_propagation();
|
event.stop_propagation();
|
||||||
event.prevent_default();
|
event.prevent_default();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
mod app_runner;
|
mod app_runner;
|
||||||
mod backend;
|
mod backend;
|
||||||
|
mod dropped_file;
|
||||||
mod events;
|
mod events;
|
||||||
mod input;
|
mod input;
|
||||||
mod panic_handler;
|
mod panic_handler;
|
||||||
@@ -207,13 +208,12 @@ fn set_clipboard_text(s: &str) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let promise = window.navigator().clipboard().write_text(s);
|
let promise = window.navigator().clipboard().write_text(s);
|
||||||
let future = wasm_bindgen_futures::JsFuture::from(promise);
|
|
||||||
let future = async move {
|
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));
|
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 items = js_sys::Array::of1(&item);
|
||||||
let promise = window.navigator().clipboard().write(&items);
|
let promise = window.navigator().clipboard().write(&items);
|
||||||
let future = wasm_bindgen_futures::JsFuture::from(promise);
|
|
||||||
let future = async move {
|
let future = async move {
|
||||||
if let Err(err) = future.await {
|
if let Err(err) = promise.await {
|
||||||
log::error!(
|
log::error!(
|
||||||
"Copy/cut image action failed: {}",
|
"Copy/cut image action failed: {}",
|
||||||
string_from_js_value(&err)
|
string_from_js_value(&err)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
wasm_bindgen_futures::spawn_local(future);
|
js_sys::futures::spawn_local(future);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
22
crates/egui-winit/src/dropped_file.rs
Normal file
22
crates/egui-winit/src/dropped_file.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct NativeFile {
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PathBuf> for NativeFile {
|
||||||
|
fn from(path: PathBuf) -> Self {
|
||||||
|
Self { path }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl egui::DroppedFile for NativeFile {
|
||||||
|
fn path(&self) -> &Path {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bytes(&self) -> Result<Vec<u8>, String> {
|
||||||
|
std::fs::read(&self.path).map_err(|err| err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ use egui::{Pos2, Rect, Theme, Vec2, ViewportBuilder, ViewportCommand, ViewportId
|
|||||||
pub use winit;
|
pub use winit;
|
||||||
|
|
||||||
pub mod clipboard;
|
pub mod clipboard;
|
||||||
|
mod dropped_file;
|
||||||
mod safe_area;
|
mod safe_area;
|
||||||
mod window_settings;
|
mod window_settings;
|
||||||
|
|
||||||
@@ -28,6 +29,8 @@ pub use window_settings::WindowSettings;
|
|||||||
|
|
||||||
use raw_window_handle::HasDisplayHandle;
|
use raw_window_handle::HasDisplayHandle;
|
||||||
|
|
||||||
|
use dropped_file::NativeFile;
|
||||||
|
|
||||||
use winit::{
|
use winit::{
|
||||||
dpi::{PhysicalPosition, PhysicalSize},
|
dpi::{PhysicalPosition, PhysicalSize},
|
||||||
event::ElementState,
|
event::ElementState,
|
||||||
@@ -470,10 +473,9 @@ impl State {
|
|||||||
}
|
}
|
||||||
WindowEvent::DroppedFile(path) => {
|
WindowEvent::DroppedFile(path) => {
|
||||||
self.egui_input.hovered_files.clear();
|
self.egui_input.hovered_files.clear();
|
||||||
self.egui_input.dropped_files.push(egui::DroppedFile {
|
self.egui_input
|
||||||
path: Some(path.clone()),
|
.dropped_files
|
||||||
..Default::default()
|
.push(std::sync::Arc::new(NativeFile::from(path.clone())));
|
||||||
});
|
|
||||||
EventResponse {
|
EventResponse {
|
||||||
repaint: true,
|
repaint: true,
|
||||||
consumed: false,
|
consumed: false,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ workspace = true
|
|||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
rustdoc-args = ["--generate-link-to-definition"]
|
rustdoc-args = ["--generate-link-to-definition"]
|
||||||
|
targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
|
||||||
@@ -90,3 +91,9 @@ document-features = { workspace = true, optional = true }
|
|||||||
|
|
||||||
ron = { workspace = true, optional = true }
|
ron = { workspace = true, optional = true }
|
||||||
serde = { workspace = true, optional = true, features = ["derive", "rc"] }
|
serde = { workspace = true, optional = true, features = ["derive", "rc"] }
|
||||||
|
|
||||||
|
|
||||||
|
# web:
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
# For `DroppedFile`, which hands web apps a file handle instead of its contents.
|
||||||
|
web-sys = { workspace = true, features = ["File"] }
|
||||||
|
|||||||
@@ -1,19 +1,51 @@
|
|||||||
|
use std::{path::Path, sync::Arc};
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use std::{future::Future, pin::Pin};
|
||||||
|
|
||||||
/// A file dropped into egui.
|
/// A file dropped into egui.
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
///
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
/// The integration owns the concrete file handle, letting egui remain independent of windowing
|
||||||
pub struct DroppedFile {
|
/// backends and file APIs.
|
||||||
/// Set by the `egui-winit` backend.
|
pub trait DroppedFile: std::fmt::Debug {
|
||||||
pub path: Option<std::path::PathBuf>,
|
/// The path of the dropped file.
|
||||||
|
///
|
||||||
|
/// This is an absolute path on native platforms. On the web, it is a relative path containing
|
||||||
|
/// only the file name because browsers do not expose the file's local path.
|
||||||
|
fn path(&self) -> &Path;
|
||||||
|
|
||||||
/// Name of the file. Set by the `eframe` web backend.
|
/// Read the file contents.
|
||||||
pub name: String,
|
///
|
||||||
|
/// This is asynchronous because browsers can only read files asynchronously.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the browser cannot read the file.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn bytes_async(&self) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, String>> + '_>>;
|
||||||
|
|
||||||
/// With the `eframe` web backend, this is set to the mime-type of the file (if available).
|
/// Read the file contents.
|
||||||
pub mime: String,
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the file cannot be read.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn bytes(&self) -> Result<Vec<u8>, String>;
|
||||||
|
|
||||||
/// Set by the `eframe` web backend.
|
/// The browser file handle, if this file was dropped on the web.
|
||||||
pub last_modified: Option<std::time::SystemTime>,
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn web_file(&self) -> Option<&web_sys::File> {
|
||||||
/// Set by the `eframe` web backend.
|
None
|
||||||
pub bytes: Option<std::sync::Arc<[u8]>>,
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A shared reference to a dropped file.
|
||||||
|
#[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))]
|
||||||
|
pub type DroppedFileHandle = Arc<dyn DroppedFile + Send + Sync>;
|
||||||
|
|
||||||
|
/// A shared reference to a dropped file.
|
||||||
|
///
|
||||||
|
/// This is not necessarily `Send + Sync` when wasm threads are enabled, because
|
||||||
|
/// [`web_sys::File`] is not thread-safe in that configuration.
|
||||||
|
#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))]
|
||||||
|
pub type DroppedFileHandle = Arc<dyn DroppedFile>;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ mod touch;
|
|||||||
mod viewport_info;
|
mod viewport_info;
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
dropped_file::DroppedFile,
|
dropped_file::{DroppedFile, DroppedFileHandle},
|
||||||
event::Event,
|
event::Event,
|
||||||
event_filter::EventFilter,
|
event_filter::EventFilter,
|
||||||
hovered_file::HoveredFile,
|
hovered_file::HoveredFile,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::{OrderedViewportIdMap, Theme, ViewportId, ViewportIdMap, emath::Rect};
|
use crate::{OrderedViewportIdMap, Theme, ViewportId, ViewportIdMap, emath::Rect};
|
||||||
|
|
||||||
use super::{DroppedFile, Event, HoveredFile, SafeAreaInsets, ViewportInfo};
|
use super::{DroppedFileHandle, Event, HoveredFile, SafeAreaInsets, ViewportInfo};
|
||||||
|
|
||||||
/// What the integrations provides to egui at the start of each frame.
|
/// What the integrations provides to egui at the start of each frame.
|
||||||
///
|
///
|
||||||
@@ -13,7 +13,7 @@ use super::{DroppedFile, Event, HoveredFile, SafeAreaInsets, ViewportInfo};
|
|||||||
///
|
///
|
||||||
/// Ii "points" can be calculated from native physical pixels
|
/// Ii "points" can be calculated from native physical pixels
|
||||||
/// using `pixels_per_point` = [`crate::Context::zoom_factor`] * `native_pixels_per_point`;
|
/// using `pixels_per_point` = [`crate::Context::zoom_factor`] * `native_pixels_per_point`;
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub struct RawInput {
|
pub struct RawInput {
|
||||||
/// The id of the active viewport.
|
/// The id of the active viewport.
|
||||||
@@ -65,9 +65,20 @@ pub struct RawInput {
|
|||||||
|
|
||||||
/// Dragged files dropped into egui.
|
/// Dragged files dropped into egui.
|
||||||
///
|
///
|
||||||
|
/// egui never reads the file contents.
|
||||||
|
#[cfg_attr(
|
||||||
|
not(target_arch = "wasm32"),
|
||||||
|
doc = "Call [`crate::DroppedFile::bytes`] to read a dropped file."
|
||||||
|
)]
|
||||||
|
#[cfg_attr(
|
||||||
|
target_arch = "wasm32",
|
||||||
|
doc = "Call [`crate::DroppedFile::bytes_async`] to read a dropped file."
|
||||||
|
)]
|
||||||
|
///
|
||||||
/// Note: when using `eframe` on Windows, this will always be empty if drag-and-drop support has
|
/// Note: when using `eframe` on Windows, this will always be empty if drag-and-drop support has
|
||||||
/// been disabled in [`crate::viewport::ViewportBuilder`].
|
/// been disabled in [`crate::viewport::ViewportBuilder`].
|
||||||
pub dropped_files: Vec<DroppedFile>,
|
#[cfg_attr(feature = "serde", serde(skip))]
|
||||||
|
pub dropped_files: Vec<DroppedFileHandle>,
|
||||||
|
|
||||||
/// The native window has the keyboard focus (i.e. is receiving key presses).
|
/// The native window has the keyboard focus (i.e. is receiving key presses).
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ pub struct WrapApp {
|
|||||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||||
custom3d: Option<crate::apps::Custom3d>,
|
custom3d: Option<crate::apps::Custom3d>,
|
||||||
|
|
||||||
dropped_files: Vec<egui::DroppedFile>,
|
dropped_files: Vec<egui::DroppedFileHandle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WrapApp {
|
impl WrapApp {
|
||||||
@@ -519,25 +519,23 @@ impl WrapApp {
|
|||||||
.open(&mut open)
|
.open(&mut open)
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
for file in &self.dropped_files {
|
for file in &self.dropped_files {
|
||||||
let mut info = if let Some(path) = &file.path {
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
path.display().to_string()
|
let info = file.path().display().to_string();
|
||||||
} else if file.name.is_empty() {
|
|
||||||
"???".to_owned()
|
|
||||||
} else {
|
|
||||||
file.name.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut additional_info = vec![];
|
// The size and mime-type are free to read; the contents are not,
|
||||||
if !file.mime.is_empty() {
|
// so we never touch them here.
|
||||||
additional_info.push(format!("type: {}", file.mime));
|
#[cfg(target_arch = "wasm32")]
|
||||||
}
|
let info = {
|
||||||
if let Some(bytes) = &file.bytes {
|
let Some(web_file) = file.web_file() else {
|
||||||
additional_info.push(format!("{} bytes", bytes.len()));
|
continue;
|
||||||
}
|
};
|
||||||
if !additional_info.is_empty() {
|
let (name, mime) = (web_file.name(), web_file.type_());
|
||||||
use std::fmt::Write as _;
|
if mime.is_empty() {
|
||||||
write!(info, " ({})", additional_info.join(", ")).ok();
|
format!("{name} ({} bytes)", web_file.size())
|
||||||
|
} else {
|
||||||
|
format!("{name} ({} bytes, type: {mime})", web_file.size())
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
ui.label(info);
|
ui.label(info);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ fn main() -> eframe::Result {
|
|||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct MyApp {
|
struct MyApp {
|
||||||
dropped_files: Vec<egui::DroppedFile>,
|
dropped_files: Vec<egui::DroppedFileHandle>,
|
||||||
picked_path: Option<String>,
|
picked_path: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,27 +48,23 @@ impl eframe::App for MyApp {
|
|||||||
ui.label("Dropped files:");
|
ui.label("Dropped files:");
|
||||||
|
|
||||||
for file in &self.dropped_files {
|
for file in &self.dropped_files {
|
||||||
let mut info = if let Some(path) = &file.path {
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
path.display().to_string()
|
ui.label(file.path().display().to_string());
|
||||||
} else if file.name.is_empty() {
|
|
||||||
"???".to_owned()
|
#[cfg(target_arch = "wasm32")]
|
||||||
} else {
|
{
|
||||||
file.name.clone()
|
let Some(web_file) = file.web_file() else {
|
||||||
|
continue;
|
||||||
};
|
};
|
||||||
|
let name = web_file.name();
|
||||||
let mut additional_info = vec![];
|
let mime = web_file.type_();
|
||||||
if !file.mime.is_empty() {
|
let size = web_file.size();
|
||||||
additional_info.push(format!("type: {}", file.mime));
|
if mime.is_empty() {
|
||||||
|
ui.label(format!("{name} ({size} bytes)"));
|
||||||
|
} else {
|
||||||
|
ui.label(format!("{name} (type: {mime}, {size} bytes)"));
|
||||||
}
|
}
|
||||||
if let Some(bytes) = &file.bytes {
|
|
||||||
additional_info.push(format!("{} bytes", bytes.len()));
|
|
||||||
}
|
}
|
||||||
if !additional_info.is_empty() {
|
|
||||||
use std::fmt::Write as _;
|
|
||||||
write!(info, " ({})", additional_info.join(", ")).ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.label(info);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user