1
0
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:
BernardDGS
2026-09-01 17:00:43 +00:00
committed by GitHub
parent e1507d031d
commit dadf573dde
5 changed files with 146 additions and 1 deletions

View File

@@ -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)));
} }
} }
} }

View File

@@ -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)));
} }
} }
} }

View File

@@ -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)
);
}
}

View File

@@ -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;
} }

View File

@@ -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`]).