Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Context::copy_image #5533

Merged
merged 7 commits into from
Dec 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,8 @@
"--all-features",
],
"rust-analyzer.showUnlinkedFileNotification": false,

// Uncomment the following options and restart rust-analyzer to get it to check code behind `cfg(target_arch=wasm32)`.
// Don't forget to put it in a comment again before committing.
// "rust-analyzer.cargo.target": "wasm32-unknown-unknown",
}
22 changes: 22 additions & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -240,11 +240,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df099ccb16cd014ff054ac1bf392c67feeef57164b05c42f037cd40f5d4357f4"
dependencies = [
"clipboard-win",
"core-graphics",
"image",
"log",
"objc2",
"objc2-app-kit",
"objc2-foundation",
"parking_lot",
"windows-sys 0.48.0",
"x11rb",
]

Expand Down Expand Up @@ -1292,6 +1295,7 @@ dependencies = [
"accesskit_winit",
"ahash",
"arboard",
"bytemuck",
"document-features",
"egui",
"log",
Expand Down Expand Up @@ -2205,6 +2209,7 @@ dependencies = [
"image-webp",
"num-traits",
"png",
"tiff",
"zune-core",
"zune-jpeg",
]
Expand Down Expand Up @@ -2311,6 +2316,12 @@ dependencies = [
"libc",
]

[[package]]
name = "jpeg-decoder"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"

[[package]]
name = "js-sys"
version = "0.3.72"
Expand Down Expand Up @@ -3882,6 +3893,17 @@ dependencies = [
"syn",
]

[[package]]
name = "tiff"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e"
dependencies = [
"flate2",
"jpeg-decoder",
"weezl",
]

[[package]]
name = "time"
version = "0.3.36"
Expand Down
3 changes: 3 additions & 0 deletions crates/eframe/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -203,15 +203,18 @@ windows-sys = { workspace = true, features = [
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]
bytemuck.workspace = true
image = { workspace = true, features = ["png"] } # For copying images
js-sys = "0.3"
percent-encoding = "2.1"
wasm-bindgen.workspace = true
wasm-bindgen-futures.workspace = true
web-sys = { workspace = true, features = [
"BinaryType",
"Blob",
"BlobPropertyBag",
"Clipboard",
"ClipboardEvent",
"ClipboardItem",
"CompositionEvent",
"console",
"CssStyleDeclaration",
Expand Down
3 changes: 3 additions & 0 deletions crates/eframe/src/web/app_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,9 @@ impl AppRunner {
egui::OutputCommand::CopyText(text) => {
super::set_clipboard_text(&text);
}
egui::OutputCommand::CopyImage(image) => {
super::set_clipboard_image(&image);
}
egui::OutputCommand::OpenUrl(open_url) => {
super::open_url(&open_url.url, open_url.new_tab);
}
Expand Down
89 changes: 89 additions & 0 deletions crates/eframe/src/web/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,95 @@ fn set_clipboard_text(s: &str) {
}
}

/// Set the clipboard image.
fn set_clipboard_image(image: &egui::ColorImage) {
if let Some(window) = web_sys::window() {
if !window.is_secure_context() {
log::error!(
"Clipboard is not available because we are not in a secure context. \
See https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts"
);
return;
}

let png_bytes = to_image(image).and_then(|image| to_png_bytes(&image));
let png_bytes = match png_bytes {
Ok(png_bytes) => png_bytes,
Err(err) => {
log::error!("Failed to encode image to png: {err}");
return;
}
};

let mime = "image/png";

let item = match create_clipboard_item(mime, &png_bytes) {
Ok(item) => item,
Err(err) => {
log::error!("Failed to copy image: {}", string_from_js_value(&err));
return;
}
};
let items = js_sys::Array::of1(&item);
let promise = window.navigator().clipboard().write(&items);
let future = wasm_bindgen_futures::JsFuture::from(promise);
let future = async move {
if let Err(err) = future.await {
log::error!(
"Copy/cut image action failed: {}",
string_from_js_value(&err)
);
}
};
wasm_bindgen_futures::spawn_local(future);
}
}

fn to_image(image: &egui::ColorImage) -> Result<image::RgbaImage, String> {
profiling::function_scope!();
image::RgbaImage::from_raw(
image.width() as _,
image.height() as _,
bytemuck::cast_slice(&image.pixels).to_vec(),
)
.ok_or_else(|| "Invalid IconData".to_owned())
}

fn to_png_bytes(image: &image::RgbaImage) -> Result<Vec<u8>, String> {
profiling::function_scope!();
let mut png_bytes: Vec<u8> = Vec::new();
image
.write_to(
&mut std::io::Cursor::new(&mut png_bytes),
image::ImageFormat::Png,
)
.map_err(|err| err.to_string())?;
Ok(png_bytes)
}

fn create_clipboard_item(mime: &str, bytes: &[u8]) -> Result<web_sys::ClipboardItem, JsValue> {
let array = js_sys::Uint8Array::from(bytes);
let blob_parts = js_sys::Array::new();
blob_parts.push(&array);

let options = web_sys::BlobPropertyBag::new();
options.set_type(mime);

let blob = web_sys::Blob::new_with_u8_array_sequence_and_options(&blob_parts, &options)?;

let items = js_sys::Object::new();

// SAFETY: I hope so
#[allow(unsafe_code, unused_unsafe)] // Weird false positive
unsafe {
js_sys::Reflect::set(&items, &JsValue::from_str(mime), &blob)?
};

let clipboard_item = web_sys::ClipboardItem::new_with_record_from_str_to_blob_promise(&items)?;

Ok(clipboard_item)
}

fn cursor_web_name(cursor: egui::CursorIcon) -> &'static str {
match cursor {
egui::CursorIcon::Alias => "alias",
Expand Down
10 changes: 7 additions & 3 deletions crates/egui-winit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ android-game-activity = ["winit/android-game-activity"]
android-native-activity = ["winit/android-native-activity"]

## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast [`egui::epaint::Vertex`], [`egui::Vec2`] etc to `&[u8]`.
bytemuck = ["egui/bytemuck"]
bytemuck = ["egui/bytemuck", "dep:bytemuck"]

## Enable cut/copy/paste to OS clipboard.
## If disabled a clipboard will be simulated so you can still copy/paste within the egui app.
clipboard = ["arboard", "smithay-clipboard"]
clipboard = ["arboard", "bytemuck", "smithay-clipboard"]

## Enable opening links in a browser when an egui hyperlink is clicked.
links = ["webbrowser"]
Expand Down Expand Up @@ -69,6 +69,8 @@ winit = { workspace = true, default-features = false }
# feature accesskit
accesskit_winit = { version = "0.23", optional = true }

bytemuck = { workspace = true, optional = true }

## Enable this when generating docs.
document-features = { workspace = true, optional = true }

Expand All @@ -84,4 +86,6 @@ smithay-clipboard = { version = "0.7.2", optional = true }
wayland-cursor = { version = "0.31.1", default-features = false, optional = true }

[target.'cfg(not(target_os = "android"))'.dependencies]
arboard = { version = "3.3", optional = true, default-features = false }
arboard = { version = "3.3", optional = true, default-features = false, features = [
"image-data",
] }
20 changes: 19 additions & 1 deletion crates/egui-winit/src/clipboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ impl Clipboard {
Some(self.clipboard.clone())
}

pub fn set(&mut self, text: String) {
pub fn set_text(&mut self, text: String) {
#[cfg(all(
any(
target_os = "linux",
Expand All @@ -108,6 +108,24 @@ impl Clipboard {

self.clipboard = text;
}

pub fn set_image(&mut self, image: &egui::ColorImage) {
#[cfg(all(feature = "arboard", not(target_os = "android")))]
if let Some(clipboard) = &mut self.arboard {
if let Err(err) = clipboard.set_image(arboard::ImageData {
width: image.width(),
height: image.height(),
bytes: std::borrow::Cow::Borrowed(bytemuck::cast_slice(&image.pixels)),
}) {
log::error!("arboard copy/cut error: {err}");
}
log::debug!("Copied image to clipboard");
return;
}

log::error!("Copying images is not supported. Enable the 'clipboard' feature of `egui-winit` to enable it.");
_ = image;
}
}

#[cfg(all(feature = "arboard", not(target_os = "android")))]
Expand Down
9 changes: 6 additions & 3 deletions crates/egui-winit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ impl State {

/// Places the text onto the clipboard.
pub fn set_clipboard_text(&mut self, text: String) {
self.clipboard.set(text);
self.clipboard.set_text(text);
}

/// Returns [`false`] or the last value that [`Window::set_ime_allowed()`] was called with, used for debouncing.
Expand Down Expand Up @@ -840,7 +840,10 @@ impl State {
for command in commands {
match command {
egui::OutputCommand::CopyText(text) => {
self.clipboard.set(text);
self.clipboard.set_text(text);
}
egui::OutputCommand::CopyImage(image) => {
self.clipboard.set_image(&image);
}
egui::OutputCommand::OpenUrl(open_url) => {
open_url_in_browser(&open_url.url);
Expand All @@ -855,7 +858,7 @@ impl State {
}

if !copied_text.is_empty() {
self.clipboard.set(copied_text);
self.clipboard.set_text(copied_text);
}

let allow_ime = ime.is_some();
Expand Down
11 changes: 10 additions & 1 deletion crates/egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1439,13 +1439,22 @@ impl Context {

/// Copy the given text to the system clipboard.
///
/// Note that in wasm applications, the clipboard is only accessible in secure contexts (e.g.,
/// Note that in web applications, the clipboard is only accessible in secure contexts (e.g.,
/// HTTPS or localhost). If this method is used outside of a secure context, it will log an
/// error and do nothing. See <https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts>.
pub fn copy_text(&self, text: String) {
self.send_cmd(crate::OutputCommand::CopyText(text));
}

/// Copy the given image to the system clipboard.
///
/// Note that in web applications, the clipboard is only accessible in secure contexts (e.g.,
/// HTTPS or localhost). If this method is used outside of a secure context, it will log an
/// error and do nothing. See <https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts>.
pub fn copy_image(&self, image: crate::ColorImage) {
self.send_cmd(crate::OutputCommand::CopyImage(image));
}

/// Format the given shortcut in a human-readable way (e.g. `Ctrl+Shift+X`).
///
/// Can be used to get the text for [`crate::Button::shortcut_text`].
Expand Down
5 changes: 4 additions & 1 deletion crates/egui/src/data/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,14 @@ pub struct IMEOutput {
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum OutputCommand {
/// Put this text in the system clipboard.
/// Put this text to the system clipboard.
///
/// This is often a response to [`crate::Event::Copy`] or [`crate::Event::Cut`].
CopyText(String),

/// Put this image to the system clipboard.
CopyImage(crate::ColorImage),

/// Open this url in a browser.
OpenUrl(OpenUrl),
}
Expand Down
1 change: 1 addition & 0 deletions crates/egui_demo_lib/src/demo/demo_app_windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ impl Default for DemoGroups {
Box::<super::window_options::WindowOptions>::default(),
]),
tests: DemoGroup::new(vec![
Box::<super::tests::ClipboardTest>::default(),
Box::<super::tests::CursorTest>::default(),
Box::<super::tests::GridTest>::default(),
Box::<super::tests::IdTest>::default(),
Expand Down
Loading
Loading