From a2f1ca31a085be1974d50102a82137a18b734483 Mon Sep 17 00:00:00 2001 From: bu5hm4nn Date: Mon, 22 Apr 2024 01:06:33 -0600 Subject: [PATCH] Add `ViewportCommand::RequestCut`, `RequestCopy` and `RequestPaste` to trigger Clipboard actions (#4035) ### Motivation We want to offer our users a context menu with `Cut`, `Copy` and `Paste` actions. `Paste` is not possible out of the box. ### Changes This PR adds `ViewportCommand::RequestCut`, `ViewportCommand::RequestCopy` and `ViewportCommand::RequestPaste`. They are routed and handled after the example of `ViewportCommand::Screenshot` and result in the same code being executed as when the user uses `CTRL+V` style keyboard commands. ### Reasoning In our last release we used an instance of `egui_winit::clipboard::Clipboard` in order to get the `Paste` functionality. However Linux users on Wayland complained about broken clipboard interaction (https://github.com/mikedilger/gossip/issues/617). After a while of digging I could not find the issue although I have found references to problems with multiple clipboards per handle before (https://gitlab.gnome.org/GNOME/mutter/-/issues/1250) but I compared mutter with weston and the problem occured on both. So to solve this I set out to extend egui to access the clipboard instance already present in egui_winit. Since there was no trivial way to reach the instance of `egui_winit::State` I felt the best approach was to follow the logic of the new `ViewportCommand::Screenshot`. ### Variations It could make sense to make the introduced `enum ActionRequested` a part of crates/egui/src/viewport.rs and to then wrap them into one single `ViewportCommand::ActionRequest(ActionRequested)`. ### Example ```Rust let mut text = String::new(); let response = ui.text_edit_singleline(&mut text); if ui.button("Paste").clicked() { response.request_focus(); ui.ctx().send_viewport_cmd(ViewportCommand::RequestPaste); } ``` --------- Co-authored-by: Emil Ernerfeldt --- crates/eframe/src/native/glow_integration.rs | 55 ++++++++++++++------ crates/eframe/src/native/wgpu_integration.rs | 45 +++++++++++++--- crates/egui-winit/src/lib.rs | 28 ++++++++-- crates/egui/src/viewport.rs | 15 ++++++ crates/egui_glow/src/winit.rs | 10 ++-- 5 files changed, 119 insertions(+), 34 deletions(-) diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 7c9eada1ccc..880b906c2b8 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -9,6 +9,7 @@ use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc, time::Instant}; +use egui_winit::ActionRequested; use glutin::{ config::GlConfig, context::NotCurrentGlContext, @@ -22,8 +23,9 @@ use winit::{ }; use egui::{ - epaint::ahash::HashMap, DeferredViewportUiCallback, ImmediateViewport, ViewportBuilder, - ViewportClass, ViewportId, ViewportIdMap, ViewportIdPair, ViewportInfo, ViewportOutput, + ahash::HashSet, epaint::ahash::HashMap, DeferredViewportUiCallback, ImmediateViewport, + ViewportBuilder, ViewportClass, ViewportId, ViewportIdMap, ViewportIdPair, ViewportInfo, + ViewportOutput, }; #[cfg(feature = "accesskit")] use egui_winit::accesskit_winit; @@ -104,7 +106,7 @@ struct Viewport { builder: ViewportBuilder, deferred_commands: Vec, info: ViewportInfo, - screenshot_requested: bool, + actions_requested: HashSet, /// The user-callback that shows the ui. /// None for immediate viewports. @@ -682,17 +684,38 @@ impl GlowWinitRunning { ); { - let screenshot_requested = std::mem::take(&mut viewport.screenshot_requested); - if screenshot_requested { - let screenshot = painter.read_screen_rgba(screen_size_in_pixels); - egui_winit - .egui_input_mut() - .events - .push(egui::Event::Screenshot { - viewport_id, - image: screenshot.into(), - }); + for action in viewport.actions_requested.drain() { + match action { + ActionRequested::Screenshot => { + let screenshot = painter.read_screen_rgba(screen_size_in_pixels); + egui_winit + .egui_input_mut() + .events + .push(egui::Event::Screenshot { + viewport_id, + image: screenshot.into(), + }); + } + ActionRequested::Cut => { + egui_winit.egui_input_mut().events.push(egui::Event::Cut); + } + ActionRequested::Copy => { + egui_winit.egui_input_mut().events.push(egui::Event::Copy); + } + ActionRequested::Paste => { + if let Some(contents) = egui_winit.clipboard_text() { + let contents = contents.replace("\r\n", "\n"); + if !contents.is_empty() { + egui_winit + .egui_input_mut() + .events + .push(egui::Event::Paste(contents)); + } + } + } + } } + integration.post_rendering(&window); } @@ -1020,7 +1043,7 @@ impl GlutinWindowContext { builder: viewport_builder, deferred_commands: vec![], info, - screenshot_requested: false, + actions_requested: Default::default(), viewport_ui_cb: None, gl_surface: None, window: window.map(Arc::new), @@ -1277,7 +1300,7 @@ impl GlutinWindowContext { std::mem::take(&mut viewport.deferred_commands), window, is_viewport_focused, - &mut viewport.screenshot_requested, + &mut viewport.actions_requested, ); // For Wayland : https://github.com/emilk/egui/issues/4196 @@ -1323,7 +1346,7 @@ fn initialize_or_update_viewport( builder, deferred_commands: vec![], info: Default::default(), - screenshot_requested: false, + actions_requested: Default::default(), viewport_ui_cb, window: None, egui_winit: None, diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index 9e0022489e7..1287ce8a01f 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -7,6 +7,7 @@ use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc, time::Instant}; +use egui_winit::ActionRequested; use parking_lot::Mutex; use raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _}; use winit::{ @@ -15,9 +16,9 @@ use winit::{ }; use egui::{ - ahash::HashMap, DeferredViewportUiCallback, FullOutput, ImmediateViewport, ViewportBuilder, - ViewportClass, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportInfo, - ViewportOutput, + ahash::{HashMap, HashSet, HashSetExt}, + DeferredViewportUiCallback, FullOutput, ImmediateViewport, ViewportBuilder, ViewportClass, + ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportInfo, ViewportOutput, }; #[cfg(feature = "accesskit")] use egui_winit::accesskit_winit; @@ -78,7 +79,7 @@ pub struct Viewport { builder: ViewportBuilder, deferred_commands: Vec, info: ViewportInfo, - screenshot_requested: bool, + actions_requested: HashSet, /// `None` for sync viewports. viewport_ui_cb: Option>, @@ -289,7 +290,7 @@ impl WgpuWinitApp { builder, deferred_commands: vec![], info, - screenshot_requested: false, + actions_requested: Default::default(), viewport_ui_cb: None, window: Some(window), egui_winit: Some(egui_winit), @@ -676,7 +677,10 @@ impl WgpuWinitRunning { let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point); - let screenshot_requested = std::mem::take(&mut viewport.screenshot_requested); + let screenshot_requested = viewport + .actions_requested + .take(&ActionRequested::Screenshot) + .is_some(); let (vsync_secs, screenshot) = painter.paint_and_update_textures( viewport_id, pixels_per_point, @@ -695,6 +699,31 @@ impl WgpuWinitRunning { }); } + for action in viewport.actions_requested.drain() { + match action { + ActionRequested::Screenshot => { + // already handled above + } + ActionRequested::Cut => { + egui_winit.egui_input_mut().events.push(egui::Event::Cut); + } + ActionRequested::Copy => { + egui_winit.egui_input_mut().events.push(egui::Event::Copy); + } + ActionRequested::Paste => { + if let Some(contents) = egui_winit.clipboard_text() { + let contents = contents.replace("\r\n", "\n"); + if !contents.is_empty() { + egui_winit + .egui_input_mut() + .events + .push(egui::Event::Paste(contents)); + } + } + } + } + } + integration.post_rendering(window); let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect(); @@ -1073,7 +1102,7 @@ fn handle_viewport_output( std::mem::take(&mut viewport.deferred_commands), window, is_viewport_focused, - &mut viewport.screenshot_requested, + &mut viewport.actions_requested, ); // For Wayland : https://github.com/emilk/egui/issues/4196 @@ -1120,7 +1149,7 @@ fn initialize_or_update_viewport( builder, deferred_commands: vec![], info: Default::default(), - screenshot_requested: false, + actions_requested: HashSet::new(), viewport_ui_cb, window: None, egui_winit: None, diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 5abfa0f6898..1bba39b3cc6 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -14,7 +14,9 @@ pub use accesskit_winit; pub use egui; #[cfg(feature = "accesskit")] use egui::accesskit; -use egui::{Pos2, Rect, Vec2, ViewportBuilder, ViewportCommand, ViewportId, ViewportInfo}; +use egui::{ + ahash::HashSet, Pos2, Rect, Vec2, ViewportBuilder, ViewportCommand, ViewportId, ViewportInfo, +}; pub use winit; pub mod clipboard; @@ -1254,6 +1256,13 @@ fn translate_cursor(cursor_icon: egui::CursorIcon) -> Option, window: &Window, is_viewport_focused: bool, - screenshot_requested: &mut bool, + actions_requested: &mut HashSet, ) { for command in commands { process_viewport_command( @@ -1270,7 +1279,7 @@ pub fn process_viewport_commands( command, info, is_viewport_focused, - screenshot_requested, + actions_requested, ); } } @@ -1281,7 +1290,7 @@ fn process_viewport_command( command: ViewportCommand, info: &mut ViewportInfo, is_viewport_focused: bool, - screenshot_requested: &mut bool, + actions_requested: &mut HashSet, ) { crate::profile_function!(); @@ -1478,7 +1487,16 @@ fn process_viewport_command( } } ViewportCommand::Screenshot => { - *screenshot_requested = true; + actions_requested.insert(ActionRequested::Screenshot); + } + ViewportCommand::RequestCut => { + actions_requested.insert(ActionRequested::Cut); + } + ViewportCommand::RequestCopy => { + actions_requested.insert(ActionRequested::Copy); + } + ViewportCommand::RequestPaste => { + actions_requested.insert(ActionRequested::Paste); } } } diff --git a/crates/egui/src/viewport.rs b/crates/egui/src/viewport.rs index aed8d35165e..ad7332877c0 100644 --- a/crates/egui/src/viewport.rs +++ b/crates/egui/src/viewport.rs @@ -1038,6 +1038,21 @@ pub enum ViewportCommand { /// /// The results are returned in `crate::Event::Screenshot`. Screenshot, + + /// Request cut of the current selection + /// + /// This is equivalent to the system keyboard shortcut for cut (e.g. CTRL + X). + RequestCut, + + /// Request a copy of the current selection. + /// + /// This is equivalent to the system keyboard shortcut for copy (e.g. CTRL + C). + RequestCopy, + + /// Request a paste from the clipboard to the current focused TextEdit if any. + /// + /// This is equivalent to the system keyboard shortcut for paste (e.g. CTRL + V). + RequestPaste, } impl ViewportCommand { diff --git a/crates/egui_glow/src/winit.rs b/crates/egui_glow/src/winit.rs index 5d87d000416..0c407ccb75d 100644 --- a/crates/egui_glow/src/winit.rs +++ b/crates/egui_glow/src/winit.rs @@ -1,7 +1,7 @@ pub use egui_winit; pub use egui_winit::EventResponse; -use egui::{ViewportId, ViewportOutput}; +use egui::{ahash::HashSet, ViewportId, ViewportOutput}; use egui_winit::winit; use crate::shader_version::ShaderVersion; @@ -79,17 +79,17 @@ impl EguiGlow { log::warn!("Multiple viewports not yet supported by EguiGlow"); } for (_, ViewportOutput { commands, .. }) in viewport_output { - let mut screenshot_requested = false; + let mut actions_requested: HashSet = Default::default(); egui_winit::process_viewport_commands( &self.egui_ctx, &mut self.viewport_info, commands, window, true, - &mut screenshot_requested, + &mut actions_requested, ); - if screenshot_requested { - log::warn!("Screenshot not yet supported by EguiGlow"); + for action in actions_requested { + log::warn!("{:?} not yet supported by EguiGlow", action); } }