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:
parent
488b1f2462
commit
a256ca115b
10 changed files with 239 additions and 6 deletions
|
@ -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`.
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
|
@ -295,6 +330,8 @@ impl RawInput {
|
|||
predicted_dt,
|
||||
modifiers,
|
||||
events,
|
||||
hovered_files,
|
||||
dropped_files,
|
||||
} = self;
|
||||
|
||||
ui.label(format!("scroll_delta: {:?} points", scroll_delta));
|
||||
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
@ -102,6 +104,8 @@ impl epi::App for WrapApp {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue