Skip to content

Commit

Permalink
Drag and drop files into egui_glium and egui_web (#637)
Browse files Browse the repository at this point in the history
* Implement file drag-and-drop for egui_glium

* Implement file drag-and-drop into egui_web

* Cleanup
  • Loading branch information
emilk authored Aug 20, 2021
1 parent 488b1f2 commit a256ca1
Show file tree
Hide file tree
Showing 10 changed files with 239 additions and 6 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ NOTE: [`eframe`](eframe/CHANGELOG.md), [`egui_web`](egui_web/CHANGELOG.md) and [
### Added ⭐
* Plot:
* [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)
* `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 🔧
* Return closure return value from `Area::show`, `ComboBox::show_ui`, `ComboBox::combo_box_with_label`, `Window::show`, `popup::*`, `menu::menu`.
Expand Down
1 change: 1 addition & 0 deletions eframe/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ All notable changes to the `eframe` crate.


## Unreleased
* Add dragging and dropping files into egui.
* Improve http fetch API.
* `run_native` now returns when the app is closed.

Expand Down
41 changes: 40 additions & 1 deletion egui/src/data/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ pub struct RawInput {
/// 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`].
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 {
Expand All @@ -72,12 +78,17 @@ impl Default for RawInput {
predicted_dt: 1.0 / 60.0,
modifiers: Modifiers::default(),
events: vec![],
hovered_files: Default::default(),
dropped_files: Default::default(),
}
}
}

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 {
#![allow(deprecated)] // for screen_size
let zoom = self.zoom_delta;
Expand All @@ -92,10 +103,34 @@ impl RawInput {
predicted_dt: self.predicted_dt,
modifiers: self.modifiers,
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.
///
/// This only covers events that egui cares about.
Expand Down Expand Up @@ -295,6 +330,8 @@ impl RawInput {
predicted_dt,
modifiers,
events,
hovered_files,
dropped_files,
} = self;

ui.label(format!("scroll_delta: {:?} points", scroll_delta));
Expand All @@ -313,6 +350,8 @@ impl RawInput {
ui.label(format!("modifiers: {:#?}", modifiers));
ui.label(format!("events: {:?}", events))
.on_hover_text("key presses etc");
ui.label(format!("hovered_files: {}", hovered_files.len()));
ui.label(format!("dropped_files: {}", dropped_files.len()));
}
}

Expand Down
65 changes: 65 additions & 0 deletions egui_demo_lib/src/wrap_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ pub struct WrapApp {
selected_anchor: String,
apps: Apps,
backend_panel: super::backend_panel::BackendPanel,
#[cfg_attr(feature = "persistence", serde(skip))]
dropped_files: Vec<egui::DroppedFile>,
}

impl epi::App for WrapApp {
Expand Down Expand Up @@ -102,6 +104,8 @@ impl epi::App for WrapApp {
}

self.backend_panel.end_of_frame(ctx);

self.ui_file_drag_and_drop(ctx);
}
}

Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 3 additions & 2 deletions egui_glium/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ All notable changes to the `egui_glium` integration will be noted in this file.


## Unreleased
* Fix native file dialogs hanging (eg. when using [`nfd2`](https://github.com/EmbarkStudios/nfd2)
* [Fix minimize on Windows](https://github.com/emilk/egui/issues/518)
* Fix native file dialogs hanging (eg. when using [`rfd`](https://github.com/PolyMeilex/rfd)).
* 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>.
* Don't restore window position on Windows, because the position would sometimes be invalid.

Expand Down
5 changes: 3 additions & 2 deletions egui_glium/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,10 +266,10 @@ pub fn run(mut app: Box<dyn epi::App>, native_options: epi::NativeOptions) {
} else {
// Winit uses up all the CPU of one core when returning ControlFlow::Wait.
// 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));
} else {
std::thread::sleep(std::time::Duration::from_millis(100));
std::thread::sleep(std::time::Duration::from_millis(50));
}
}
}
Expand All @@ -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.
display.gl_window().window().request_redraw();
repaint_asap = true;
}
glutin::event::Event::UserEvent(RequestRepaintEvent) => {
display.gl_window().window().request_redraw();
Expand Down
16 changes: 16 additions & 0 deletions egui_glium/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
3 changes: 3 additions & 0 deletions egui_web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ All notable changes to the `egui_web` integration will be noted in this file.

## Unreleased

### Added ⭐
* Added support for dragging and dropping files into the browser window.


## 0.13.0 - 2021-06-24

Expand Down
7 changes: 7 additions & 0 deletions egui_web/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,25 @@ screen_reader = ["tts"] # experimental
[dependencies.web-sys]
version = "0.3.52"
features = [
"BinaryType",
"Blob",
"Clipboard",
"ClipboardEvent",
"CompositionEvent",
"console",
"CssStyleDeclaration",
"DataTransfer",
"DataTransferItem",
"DataTransferItemList",
"Document",
"DomRect",
"DragEvent",
"Element",
"Event",
"EventListener",
"EventTarget",
"File",
"FileList",
"FocusEvent",
"HtmlCanvasElement",
"HtmlElement",
Expand Down
99 changes: 99 additions & 0 deletions egui_web/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1087,6 +1087,105 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
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(())
}

Expand Down

0 comments on commit a256ca1

Please sign in to comment.