Drag and drop files into egui_glium and egui_web (#637)

* Implement file drag-and-drop for egui_glium

* Implement file drag-and-drop into egui_web

* Cleanup
This commit is contained in:
Emil Ernerfeldt 2021-08-20 22:20:45 +02:00 committed by GitHub
parent 488b1f2462
commit a256ca115b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 239 additions and 6 deletions

View file

@ -10,9 +10,10 @@ NOTE: [`eframe`](eframe/CHANGELOG.md), [`egui_web`](egui_web/CHANGELOG.md) and [
### Added ⭐ ### Added ⭐
* Plot: * Plot:
* [Line styles](https://github.com/emilk/egui/pull/482) * [Line styles](https://github.com/emilk/egui/pull/482)
* Add `show_background` and `show_axes` methods to `Plot`.
* [Progress bar](https://github.com/emilk/egui/pull/519) * [Progress bar](https://github.com/emilk/egui/pull/519)
* `Grid::num_columns`: allow the last column to take up the rest of the space of the parent `Ui`. * `Grid::num_columns`: allow the last column to take up the rest of the space of the parent `Ui`.
* Add `show_background` and `show_axes` methods to `Plot`. * Add an API for dropping files into egui (see `RawInput`).
### Changed 🔧 ### Changed 🔧
* Return closure return value from `Area::show`, `ComboBox::show_ui`, `ComboBox::combo_box_with_label`, `Window::show`, `popup::*`, `menu::menu`. * Return closure return value from `Area::show`, `ComboBox::show_ui`, `ComboBox::combo_box_with_label`, `Window::show`, `popup::*`, `menu::menu`.

View file

@ -3,6 +3,7 @@ All notable changes to the `eframe` crate.
## Unreleased ## Unreleased
* Add dragging and dropping files into egui.
* Improve http fetch API. * Improve http fetch API.
* `run_native` now returns when the app is closed. * `run_native` now returns when the app is closed.

View file

@ -57,6 +57,12 @@ pub struct RawInput {
/// but you can check if egui is using the keyboard with [`crate::Context::wants_keyboard_input`] /// but you can check if egui is using the keyboard with [`crate::Context::wants_keyboard_input`]
/// and/or the pointer (mouse/touch) with [`crate::Context::is_using_pointer`]. /// and/or the pointer (mouse/touch) with [`crate::Context::is_using_pointer`].
pub events: Vec<Event>, pub events: Vec<Event>,
/// Dragged files hovering over egui.
pub hovered_files: Vec<HoveredFile>,
/// Dragged files dropped into egui.
pub dropped_files: Vec<DroppedFile>,
} }
impl Default for RawInput { impl Default for RawInput {
@ -72,12 +78,17 @@ impl Default for RawInput {
predicted_dt: 1.0 / 60.0, predicted_dt: 1.0 / 60.0,
modifiers: Modifiers::default(), modifiers: Modifiers::default(),
events: vec![], events: vec![],
hovered_files: Default::default(),
dropped_files: Default::default(),
} }
} }
} }
impl RawInput { impl RawInput {
/// Helper: move volatile (deltas and events), clone the rest /// Helper: move volatile (deltas and events), clone the rest.
///
/// * [`Self::hovered_files`] is cloned.
/// * [`Self::dropped_files`] is moved.
pub fn take(&mut self) -> RawInput { pub fn take(&mut self) -> RawInput {
#![allow(deprecated)] // for screen_size #![allow(deprecated)] // for screen_size
let zoom = self.zoom_delta; let zoom = self.zoom_delta;
@ -92,10 +103,34 @@ impl RawInput {
predicted_dt: self.predicted_dt, predicted_dt: self.predicted_dt,
modifiers: self.modifiers, modifiers: self.modifiers,
events: std::mem::take(&mut self.events), events: std::mem::take(&mut self.events),
hovered_files: self.hovered_files.clone(),
dropped_files: std::mem::take(&mut self.dropped_files),
} }
} }
} }
/// A file about to be dropped into egui.
#[derive(Clone, Debug, Default)]
pub struct HoveredFile {
/// Set by the `egui_glium` backend.
pub path: Option<std::path::PathBuf>,
/// With the `egui_web` backend, this is set to the mime-type of the file (if available).
pub mime: String,
}
/// A file dropped into egui.
#[derive(Clone, Debug, Default)]
pub struct DroppedFile {
/// Set by the `egui_glium` backend.
pub path: Option<std::path::PathBuf>,
/// Name of the file. Set by the `egui_web` backend.
pub name: String,
/// Set by the `egui_web` backend.
pub last_modified: Option<std::time::SystemTime>,
/// Set by the `egui_web` backend.
pub bytes: Option<std::sync::Arc<[u8]>>,
}
/// An input event generated by the integration. /// An input event generated by the integration.
/// ///
/// This only covers events that egui cares about. /// This only covers events that egui cares about.
@ -295,6 +330,8 @@ impl RawInput {
predicted_dt, predicted_dt,
modifiers, modifiers,
events, events,
hovered_files,
dropped_files,
} = self; } = self;
ui.label(format!("scroll_delta: {:?} points", scroll_delta)); ui.label(format!("scroll_delta: {:?} points", scroll_delta));
@ -313,6 +350,8 @@ impl RawInput {
ui.label(format!("modifiers: {:#?}", modifiers)); ui.label(format!("modifiers: {:#?}", modifiers));
ui.label(format!("events: {:?}", events)) ui.label(format!("events: {:?}", events))
.on_hover_text("key presses etc"); .on_hover_text("key presses etc");
ui.label(format!("hovered_files: {}", hovered_files.len()));
ui.label(format!("dropped_files: {}", dropped_files.len()));
} }
} }

View file

@ -33,6 +33,8 @@ pub struct WrapApp {
selected_anchor: String, selected_anchor: String,
apps: Apps, apps: Apps,
backend_panel: super::backend_panel::BackendPanel, backend_panel: super::backend_panel::BackendPanel,
#[cfg_attr(feature = "persistence", serde(skip))]
dropped_files: Vec<egui::DroppedFile>,
} }
impl epi::App for WrapApp { impl epi::App for WrapApp {
@ -102,6 +104,8 @@ impl epi::App for WrapApp {
} }
self.backend_panel.end_of_frame(ctx); self.backend_panel.end_of_frame(ctx);
self.ui_file_drag_and_drop(ctx);
} }
} }
@ -144,6 +148,67 @@ impl WrapApp {
}); });
}); });
} }
fn ui_file_drag_and_drop(&mut self, ctx: &egui::CtxRef) {
use egui::*;
// Preview hovering files:
if !ctx.input().raw.hovered_files.is_empty() {
let mut text = "Dropping files:\n".to_owned();
for file in &ctx.input().raw.hovered_files {
if let Some(path) = &file.path {
text += &format!("\n{}", path.display());
} else if !file.mime.is_empty() {
text += &format!("\n{}", file.mime);
} else {
text += "\n???";
}
}
let painter =
ctx.layer_painter(LayerId::new(Order::Foreground, Id::new("file_drop_target")));
let screen_rect = ctx.input().screen_rect();
painter.rect_filled(screen_rect, 0.0, Color32::from_black_alpha(192));
painter.text(
screen_rect.center(),
Align2::CENTER_CENTER,
text,
TextStyle::Heading,
Color32::WHITE,
);
}
// Collect dropped files:
if !ctx.input().raw.dropped_files.is_empty() {
self.dropped_files = ctx.input().raw.dropped_files.clone();
}
// Show dropped files (if any):
if !self.dropped_files.is_empty() {
let mut open = true;
egui::Window::new("Dropped files")
.open(&mut open)
.show(ctx, |ui| {
for file in &self.dropped_files {
let mut info = if let Some(path) = &file.path {
path.display().to_string()
} else if !file.name.is_empty() {
file.name.clone()
} else {
"???".to_owned()
};
if let Some(bytes) = &file.bytes {
info += &format!(" ({} bytes)", bytes.len());
}
ui.label(info);
}
});
if !open {
self.dropped_files.clear();
}
}
}
} }
fn clock_button(ui: &mut egui::Ui, seconds_since_midnight: f64) -> egui::Response { fn clock_button(ui: &mut egui::Ui, seconds_since_midnight: f64) -> egui::Response {

View file

@ -4,8 +4,9 @@ All notable changes to the `egui_glium` integration will be noted in this file.
## Unreleased ## Unreleased
* Fix native file dialogs hanging (eg. when using [`nfd2`](https://github.com/EmbarkStudios/nfd2) * Fix native file dialogs hanging (eg. when using [`rfd`](https://github.com/PolyMeilex/rfd)).
* [Fix minimize on Windows](https://github.com/emilk/egui/issues/518) * Implement drag-and-dropping files into the application.
* [Fix minimize on Windows](https://github.com/emilk/egui/issues/518).
* Change `drag_and_drop_support` to `false` by default (Windows only). See <https://github.com/emilk/egui/issues/598>. * Change `drag_and_drop_support` to `false` by default (Windows only). See <https://github.com/emilk/egui/issues/598>.
* Don't restore window position on Windows, because the position would sometimes be invalid. * Don't restore window position on Windows, because the position would sometimes be invalid.

View file

@ -266,10 +266,10 @@ pub fn run(mut app: Box<dyn epi::App>, native_options: epi::NativeOptions) {
} else { } else {
// Winit uses up all the CPU of one core when returning ControlFlow::Wait. // Winit uses up all the CPU of one core when returning ControlFlow::Wait.
// Sleeping here helps, but still uses 1-3% of CPU :( // Sleeping here helps, but still uses 1-3% of CPU :(
if is_focused { if is_focused || !egui.input_state.raw.hovered_files.is_empty() {
std::thread::sleep(std::time::Duration::from_millis(10)); std::thread::sleep(std::time::Duration::from_millis(10));
} else { } else {
std::thread::sleep(std::time::Duration::from_millis(100)); std::thread::sleep(std::time::Duration::from_millis(50));
} }
} }
} }
@ -287,6 +287,7 @@ pub fn run(mut app: Box<dyn epi::App>, native_options: epi::NativeOptions) {
// TODO: ask egui if the events warrants a repaint instead of repainting on each event. // TODO: ask egui if the events warrants a repaint instead of repainting on each event.
display.gl_window().window().request_redraw(); display.gl_window().window().request_redraw();
repaint_asap = true;
} }
glutin::event::Event::UserEvent(RequestRepaintEvent) => { glutin::event::Event::UserEvent(RequestRepaintEvent) => {
display.gl_window().window().request_redraw(); display.gl_window().window().request_redraw();

View file

@ -255,6 +255,22 @@ pub fn input_to_egui(
}, },
}); });
} }
WindowEvent::HoveredFile(path) => {
input_state.raw.hovered_files.push(egui::HoveredFile {
path: Some(path.clone()),
..Default::default()
});
}
WindowEvent::HoveredFileCancelled => {
input_state.raw.hovered_files.clear();
}
WindowEvent::DroppedFile(path) => {
input_state.raw.hovered_files.clear();
input_state.raw.dropped_files.push(egui::DroppedFile {
path: Some(path.clone()),
..Default::default()
});
}
_ => { _ => {
// dbg!(event); // dbg!(event);
} }

View file

@ -5,6 +5,9 @@ All notable changes to the `egui_web` integration will be noted in this file.
## Unreleased ## Unreleased
### Added ⭐
* Added support for dragging and dropping files into the browser window.
## 0.13.0 - 2021-06-24 ## 0.13.0 - 2021-06-24

View file

@ -56,18 +56,25 @@ screen_reader = ["tts"] # experimental
[dependencies.web-sys] [dependencies.web-sys]
version = "0.3.52" version = "0.3.52"
features = [ features = [
"BinaryType",
"Blob",
"Clipboard", "Clipboard",
"ClipboardEvent", "ClipboardEvent",
"CompositionEvent", "CompositionEvent",
"console", "console",
"CssStyleDeclaration", "CssStyleDeclaration",
"DataTransfer", "DataTransfer",
"DataTransferItem",
"DataTransferItemList",
"Document", "Document",
"DomRect", "DomRect",
"DragEvent",
"Element", "Element",
"Event", "Event",
"EventListener", "EventListener",
"EventTarget", "EventTarget",
"File",
"FileList",
"FocusEvent", "FocusEvent",
"HtmlCanvasElement", "HtmlCanvasElement",
"HtmlElement", "HtmlElement",

View file

@ -1087,6 +1087,105 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
closure.forget(); closure.forget();
} }
{
let event_name = "dragover";
let runner_ref = runner_ref.clone();
let closure = Closure::wrap(Box::new(move |event: web_sys::DragEvent| {
if let Some(data_transfer) = event.data_transfer() {
let mut runner_lock = runner_ref.0.lock();
runner_lock.input.raw.hovered_files.clear();
for i in 0..data_transfer.items().length() {
if let Some(item) = data_transfer.items().get(i) {
runner_lock.input.raw.hovered_files.push(egui::HoveredFile {
mime: item.type_(),
..Default::default()
});
}
}
runner_lock.needs_repaint.set_true();
event.stop_propagation();
event.prevent_default();
}
}) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
closure.forget();
}
{
let event_name = "dragleave";
let runner_ref = runner_ref.clone();
let closure = Closure::wrap(Box::new(move |event: web_sys::DragEvent| {
let mut runner_lock = runner_ref.0.lock();
runner_lock.input.raw.hovered_files.clear();
runner_lock.needs_repaint.set_true();
event.stop_propagation();
event.prevent_default();
}) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
closure.forget();
}
{
let event_name = "drop";
let runner_ref = runner_ref.clone();
let closure = Closure::wrap(Box::new(move |event: web_sys::DragEvent| {
if let Some(data_transfer) = event.data_transfer() {
{
let mut runner_lock = runner_ref.0.lock();
runner_lock.input.raw.hovered_files.clear();
runner_lock.needs_repaint.set_true();
}
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 last_modified = std::time::UNIX_EPOCH
+ std::time::Duration::from_millis(file.last_modified() as u64);
console_log(format!("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();
console_log(format!(
"Loaded {:?} ({} bytes).",
name,
bytes.len()
));
let mut runner_lock = runner_ref.0.lock();
runner_lock.input.raw.dropped_files.push(
egui::DroppedFile {
name,
last_modified: Some(last_modified),
bytes: Some(bytes.into()),
..Default::default()
},
);
runner_lock.needs_repaint.set_true();
}
Err(err) => {
console_error(format!("Failed to read file: {:?}", err));
}
}
};
wasm_bindgen_futures::spawn_local(future);
}
}
}
event.stop_propagation();
event.prevent_default();
}
}) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
closure.forget();
}
Ok(()) Ok(())
} }