diff --git a/CHANGELOG.md b/CHANGELOG.md index c72e97143c3..5299d3678a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`. diff --git a/eframe/CHANGELOG.md b/eframe/CHANGELOG.md index 7b7071aed75..8b7c654fe8e 100644 --- a/eframe/CHANGELOG.md +++ b/eframe/CHANGELOG.md @@ -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. diff --git a/egui/src/data/input.rs b/egui/src/data/input.rs index a1f400ab11b..ede6f674765 100644 --- a/egui/src/data/input.rs +++ b/egui/src/data/input.rs @@ -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, + + /// Dragged files hovering over egui. + pub hovered_files: Vec, + + /// Dragged files dropped into egui. + pub dropped_files: Vec, } 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, + /// 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, + /// Name of the file. Set by the `egui_web` backend. + pub name: String, + /// Set by the `egui_web` backend. + pub last_modified: Option, + /// Set by the `egui_web` backend. + pub bytes: Option>, +} + /// 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())); } } diff --git a/egui_demo_lib/src/wrap_app.rs b/egui_demo_lib/src/wrap_app.rs index 80e595a8c2e..e93b1c10201 100644 --- a/egui_demo_lib/src/wrap_app.rs +++ b/egui_demo_lib/src/wrap_app.rs @@ -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, } 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 { diff --git a/egui_glium/CHANGELOG.md b/egui_glium/CHANGELOG.md index a5d27ca1ed5..843dbb538c1 100644 --- a/egui_glium/CHANGELOG.md +++ b/egui_glium/CHANGELOG.md @@ -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 . * Don't restore window position on Windows, because the position would sometimes be invalid. diff --git a/egui_glium/src/backend.rs b/egui_glium/src/backend.rs index 66a545eae37..d7412fd837a 100644 --- a/egui_glium/src/backend.rs +++ b/egui_glium/src/backend.rs @@ -266,10 +266,10 @@ pub fn run(mut app: Box, 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, 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(); diff --git a/egui_glium/src/lib.rs b/egui_glium/src/lib.rs index 49ea1229618..47dcaf9dc90 100644 --- a/egui_glium/src/lib.rs +++ b/egui_glium/src/lib.rs @@ -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); } diff --git a/egui_web/CHANGELOG.md b/egui_web/CHANGELOG.md index 8ac165a4d1e..95d24eb663c 100644 --- a/egui_web/CHANGELOG.md +++ b/egui_web/CHANGELOG.md @@ -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 diff --git a/egui_web/Cargo.toml b/egui_web/Cargo.toml index 3ee6099f593..340ee6a9147 100644 --- a/egui_web/Cargo.toml +++ b/egui_web/Cargo.toml @@ -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", diff --git a/egui_web/src/lib.rs b/egui_web/src/lib.rs index 70ad59035e4..653ee600951 100644 --- a/egui_web/src/lib.rs +++ b/egui_web/src/lib.rs @@ -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); + 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); + 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); + canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?; + closure.forget(); + } + Ok(()) }