mirror of
https://github.com/emilk/egui.git
synced 2026-09-02 06:40:06 -04:00
Add clipboard image paste support (Event::PasteImage) (#8472)
Adds `egui::Event::PasteImage`, emitted on Ctrl+V/Cmd+V when the clipboard holds an image with no usable text representation (mirrors `OutputCommand::CopyImage` for the opposite direction). `egui-winit::Clipboard` gains `get_image()`, backed by `arboard::Clipboard::get_image()` — the symmetric counterpart to the `set_image()` already used for copy. Wired in the winit keyboard-shortcut path and in both the glow and wgpu eframe backends' AccessKit-driven paste action. Straight (unmultiplied) alpha from the OS clipboard is converted via the existing `ColorImage::from_rgba_unmultiplied` helper rather than reinterpreting bytes directly, since the two aren't bit-compatible for translucent pixels. Not covered here: the web (wasm32) target — browser clipboard image reads are inherently async (`Blob::array_buffer` returns a Promise), while `Event::PasteImage` as designed here carries an already-decoded `ColorImage`. Bridging that needs either an async event payload or a deferred/queued event on `AppRunner` — more of a design decision than a small mirrored addition, flagging it rather than guessing. * Related: #2108 (not fixed: web) --------- Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
@@ -831,6 +831,11 @@ impl GlowWinitRunning<'_> {
|
|||||||
.events
|
.events
|
||||||
.push(egui::Event::Paste(contents));
|
.push(egui::Event::Paste(contents));
|
||||||
}
|
}
|
||||||
|
} else if let Some(image) = egui_winit.clipboard_image() {
|
||||||
|
egui_winit
|
||||||
|
.egui_input_mut()
|
||||||
|
.events
|
||||||
|
.push(egui::Event::PasteImage(std::sync::Arc::new(image)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -838,6 +838,11 @@ impl WgpuWinitRunning<'_> {
|
|||||||
.events
|
.events
|
||||||
.push(egui::Event::Paste(contents));
|
.push(egui::Event::Paste(contents));
|
||||||
}
|
}
|
||||||
|
} else if let Some(image) = egui_winit.clipboard_image() {
|
||||||
|
egui_winit
|
||||||
|
.egui_input_mut()
|
||||||
|
.events
|
||||||
|
.push(egui::Event::PasteImage(std::sync::Arc::new(image)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,13 @@ impl Clipboard {
|
|||||||
return match clipboard.get_text() {
|
return match clipboard.get_text() {
|
||||||
Ok(text) => Some(text),
|
Ok(text) => Some(text),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("arboard paste error: {err}");
|
// Expected whenever the clipboard holds something other than text (e.g.
|
||||||
|
// an image copied with a screenshot tool) — the caller falls back to
|
||||||
|
// `Self::get_image` in that case, so this is not an error worth
|
||||||
|
// alarming the user/log about.
|
||||||
|
if !is_expected_content_absence(&err) {
|
||||||
|
log::error!("arboard paste error: {err}");
|
||||||
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -120,6 +126,34 @@ impl Clipboard {
|
|||||||
self.clipboard = text;
|
self.clipboard = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get an image from the clipboard, if there is one and the platform backend supports it.
|
||||||
|
///
|
||||||
|
/// This mirrors [`Self::set_image`] for the opposite direction, so that a Ctrl+V/Cmd+V
|
||||||
|
/// paste can carry an image (e.g. a screenshot or a copied image) instead of text — see
|
||||||
|
/// [`egui::Event::PasteImage`].
|
||||||
|
pub fn get_image(&mut self) -> Option<egui::ColorImage> {
|
||||||
|
#[cfg(all(
|
||||||
|
not(any(target_os = "android", target_os = "ios")),
|
||||||
|
feature = "arboard",
|
||||||
|
))]
|
||||||
|
if let Some(clipboard) = &mut self.arboard {
|
||||||
|
return match clipboard.get_image() {
|
||||||
|
Ok(image) => Some(color_image_from_arboard(&image)),
|
||||||
|
Err(err) => {
|
||||||
|
// Expected whenever the clipboard holds neither text nor an image (e.g.
|
||||||
|
// it's simply empty) — `Self::get` was already tried first and came up
|
||||||
|
// empty too, so this is the mundane "nothing to paste" case, not an error.
|
||||||
|
if !is_expected_content_absence(&err) {
|
||||||
|
log::error!("arboard paste-image error: {err}");
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_image(&mut self, image: &egui::ColorImage) {
|
pub fn set_image(&mut self, image: &egui::ColorImage) {
|
||||||
#[cfg(all(
|
#[cfg(all(
|
||||||
not(any(target_os = "android", target_os = "ios")),
|
not(any(target_os = "android", target_os = "ios")),
|
||||||
@@ -144,6 +178,29 @@ impl Clipboard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether an `arboard::Error` from reading the clipboard is the expected, mundane outcome
|
||||||
|
/// of the clipboard simply not holding the requested content type (e.g. text was asked for
|
||||||
|
/// but the clipboard holds an image, or vice versa, or it's just empty) — as opposed to a
|
||||||
|
/// genuine failure (permissions, a locked clipboard, a conversion error) worth an `error!` log.
|
||||||
|
///
|
||||||
|
/// Pulled out as its own pure function (rather than inlined in the two `match`es above) so it
|
||||||
|
/// can be unit-tested without touching the real OS clipboard, which CI can't rely on.
|
||||||
|
#[cfg(all(
|
||||||
|
not(any(target_os = "android", target_os = "ios")),
|
||||||
|
feature = "arboard",
|
||||||
|
))]
|
||||||
|
fn is_expected_content_absence(err: &arboard::Error) -> bool {
|
||||||
|
matches!(err, arboard::Error::ContentNotAvailable)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(
|
||||||
|
not(any(target_os = "android", target_os = "ios")),
|
||||||
|
feature = "arboard",
|
||||||
|
))]
|
||||||
|
fn color_image_from_arboard(image: &arboard::ImageData<'_>) -> egui::ColorImage {
|
||||||
|
egui::ColorImage::from_rgba_unmultiplied([image.width, image.height], &image.bytes)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(all(
|
#[cfg(all(
|
||||||
not(any(target_os = "android", target_os = "ios")),
|
not(any(target_os = "android", target_os = "ios")),
|
||||||
feature = "arboard",
|
feature = "arboard",
|
||||||
@@ -192,3 +249,58 @@ fn init_smithay_clipboard(
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(
|
||||||
|
not(any(target_os = "android", target_os = "ios")),
|
||||||
|
feature = "arboard",
|
||||||
|
))]
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{color_image_from_arboard, is_expected_content_absence};
|
||||||
|
|
||||||
|
/// Regression test for the spurious `error!`-level log a maintainer caught by manually
|
||||||
|
/// testing an image paste (nothing had exercised this distinction before): only
|
||||||
|
/// `ContentNotAvailable` — clipboard simply doesn't hold the requested content type — is
|
||||||
|
/// expected and should stay silent; every other `arboard::Error` variant is a real failure
|
||||||
|
/// and must still be logged.
|
||||||
|
#[test]
|
||||||
|
fn only_content_not_available_is_treated_as_expected() {
|
||||||
|
assert!(is_expected_content_absence(
|
||||||
|
&arboard::Error::ContentNotAvailable
|
||||||
|
));
|
||||||
|
|
||||||
|
assert!(!is_expected_content_absence(
|
||||||
|
&arboard::Error::ClipboardNotSupported
|
||||||
|
));
|
||||||
|
assert!(!is_expected_content_absence(
|
||||||
|
&arboard::Error::ClipboardOccupied
|
||||||
|
));
|
||||||
|
assert!(!is_expected_content_absence(
|
||||||
|
&arboard::Error::ConversionFailure
|
||||||
|
));
|
||||||
|
assert!(!is_expected_content_absence(&arboard::Error::Unknown {
|
||||||
|
description: "anything".to_owned(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn color_image_from_arboard_converts_straight_to_premultiplied_alpha() {
|
||||||
|
// 2x1 image: opaque red, then half-transparent white — straight (unmultiplied) alpha,
|
||||||
|
// as arboard/the OS clipboard would hand it to us.
|
||||||
|
let image = arboard::ImageData {
|
||||||
|
width: 2,
|
||||||
|
height: 1,
|
||||||
|
bytes: std::borrow::Cow::Borrowed(&[255, 0, 0, 255, 255, 255, 255, 128]),
|
||||||
|
};
|
||||||
|
let color_image = color_image_from_arboard(&image);
|
||||||
|
assert_eq!(color_image.size, [2, 1]);
|
||||||
|
assert_eq!(
|
||||||
|
color_image.pixels[0],
|
||||||
|
egui::Color32::from_rgba_unmultiplied(255, 0, 0, 255)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
color_image.pixels[1],
|
||||||
|
egui::Color32::from_rgba_unmultiplied(255, 255, 255, 128)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -220,6 +220,12 @@ impl State {
|
|||||||
self.clipboard.get()
|
self.clipboard.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetches an image from the clipboard and returns it, if there is one and the platform
|
||||||
|
/// backend supports it. Mirrors [`Self::clipboard_text`] for images.
|
||||||
|
pub fn clipboard_image(&mut self) -> Option<egui::ColorImage> {
|
||||||
|
self.clipboard.get_image()
|
||||||
|
}
|
||||||
|
|
||||||
/// Places the text onto the clipboard.
|
/// Places the text onto the clipboard.
|
||||||
pub fn set_clipboard_text(&mut self, text: String) {
|
pub fn set_clipboard_text(&mut self, text: String) {
|
||||||
self.clipboard.set_text(text);
|
self.clipboard.set_text(text);
|
||||||
@@ -1030,6 +1036,14 @@ impl State {
|
|||||||
if !contents.is_empty() {
|
if !contents.is_empty() {
|
||||||
self.egui_input.events.push(egui::Event::Paste(contents));
|
self.egui_input.events.push(egui::Event::Paste(contents));
|
||||||
}
|
}
|
||||||
|
} else if let Some(image) = self.clipboard.get_image() {
|
||||||
|
// No usable text on the clipboard (e.g. an image was copied with
|
||||||
|
// mspaint/Snipping Tool, which never puts a text representation
|
||||||
|
// alongside it) — fall back to an image paste rather than doing
|
||||||
|
// nothing, mirroring `Event::Copy`/`OutputCommand::CopyImage`.
|
||||||
|
self.egui_input
|
||||||
|
.events
|
||||||
|
.push(egui::Event::PasteImage(std::sync::Arc::new(image)));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,15 @@ pub enum Event {
|
|||||||
/// The integration detected a "paste" event (e.g. Cmd+V).
|
/// The integration detected a "paste" event (e.g. Cmd+V).
|
||||||
Paste(String),
|
Paste(String),
|
||||||
|
|
||||||
|
/// The integration detected a "paste" event (e.g. Cmd+V) where the clipboard held an
|
||||||
|
/// image instead of text (e.g. a screenshot, or an image copied from another app).
|
||||||
|
///
|
||||||
|
/// Mirrors [`crate::OutputCommand::CopyImage`] for the opposite direction: an integration
|
||||||
|
/// that supports copying an image out (via `arboard`, say) should support pasting one back
|
||||||
|
/// in the same way. Only emitted when the clipboard has no usable text representation —
|
||||||
|
/// [`Self::Paste`] still takes priority when both are available.
|
||||||
|
PasteImage(std::sync::Arc<ColorImage>),
|
||||||
|
|
||||||
/// Text input, e.g. via keyboard.
|
/// Text input, e.g. via keyboard.
|
||||||
///
|
///
|
||||||
/// When the user presses enter/return, do not send a [`Text`](Event::Text) (just [`Key::Enter`]).
|
/// When the user presses enter/return, do not send a [`Text`](Event::Text) (just [`Key::Enter`]).
|
||||||
|
|||||||
Reference in New Issue
Block a user