diff --git a/Cargo.lock b/Cargo.lock index 2680970..50e4f80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,11 +25,13 @@ dependencies = [ "core-graphics", "env_logger", "image", + "js-sys", "log", "objc2", "objc2-app-kit", "objc2-foundation", "parking_lot", + "web-sys", "windows-sys", "wl-clipboard-rs", "x11rb", @@ -73,6 +75,12 @@ dependencies = [ "objc2", ] +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "bytecount" version = "0.6.3" @@ -342,6 +350,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -755,6 +772,61 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + [[package]] name = "wayland-backend" version = "0.3.2" @@ -828,6 +900,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "weezl" version = "0.1.7" diff --git a/Cargo.toml b/Cargo.toml index 5bbceaa..c8a7557 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,10 @@ wl-clipboard-rs = { version = "0.8", optional = true } image = { version = "0.25", optional = true, default-features = false, features = ["png"] } parking_lot = "0.12" +[target.'cfg(target_arch = "wasm32")'.dependencies] +js-sys = { version = "0.3.70", default-features = false } +web-sys = { version = "0.3.70", default-features = false, features = [ "Clipboard", "ClipboardEvent", "ClipboardItem", "DataTransfer", "Document", "FileList", "Navigator", "Window" ] } + [[example]] name = "get_image" required-features = ["image-data"] diff --git a/src/common.rs b/src/common.rs index 2e10fea..36b7c26 100644 --- a/src/common.rs +++ b/src/common.rs @@ -90,7 +90,7 @@ impl std::fmt::Debug for Error { } impl Error { - #[cfg(windows)] + #[allow(unused)] pub(crate) fn unknown>(message: M) -> Self { Error::Unknown { description: message.into() } } @@ -174,6 +174,9 @@ impl Drop for ScopeGuard { /// Common trait for sealing platform extension traits. pub(crate) mod private { + // This is currently unused on macOS and WASM, so silence the warning which appears + // since there's no extension traits making use of this trait sealing structure. + #[allow(unreachable_pub, unused, dead_code)] pub trait Sealed {} impl Sealed for crate::Get<'_> {} diff --git a/src/lib.rs b/src/lib.rs index ee752f6..df264b1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,6 +63,12 @@ pub use platform::SetExtApple; /// /// This means that attempting operations in parallel has a high likelihood to return an error or /// deadlock. As such, it is recommended to avoid creating/operating clipboard objects on >1 thread. +/// +/// ## WASM +/// +/// The `Clipboard` is only available on the main browser thread; attempting to use it from a worker +/// will panic. In addition, the user must perform a paste action on the web document before +/// the clipboard contents become available to read with `arboard`. #[allow(rustdoc::broken_intra_doc_links)] pub struct Clipboard { pub(crate) platform: platform::Clipboard, @@ -134,6 +140,7 @@ impl Clipboard { /// - On macOS: `NSImage` object /// - On Linux: PNG, under the atom `image/png` /// - On Windows: In order of priority `CF_DIB` and `CF_BITMAP` + /// - On WASM: Currently unsupported /// /// # Errors /// @@ -229,6 +236,7 @@ impl Set<'_> { /// - On macOS: `NSImage` object /// - On Linux: PNG, under the atom `image/png` /// - On Windows: In order of priority `CF_DIB` and `CF_BITMAP` + /// - On WASM: Currently unsupported #[cfg(feature = "image-data")] pub fn image(self, image: ImageData) -> Result<(), Error> { self.platform.image(image) diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 268eb47..84eec3a 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -15,3 +15,8 @@ pub use windows::*; mod osx; #[cfg(target_os = "macos")] pub use osx::*; + +#[cfg(target_arch = "wasm32")] +mod wasm; +#[cfg(target_arch = "wasm32")] +pub(crate) use wasm::*; diff --git a/src/platform/wasm.rs b/src/platform/wasm.rs new file mode 100644 index 0000000..0867bfe --- /dev/null +++ b/src/platform/wasm.rs @@ -0,0 +1,162 @@ +use crate::common::Error; +#[cfg(feature = "image-data")] +use crate::common::ImageData; +use js_sys::wasm_bindgen::JsCast; +use std::borrow::Cow; +use web_sys::wasm_bindgen::closure::Closure; + +pub(crate) struct Clipboard { + inner: web_sys::Clipboard, + window: web_sys::Window, +} + +impl Clipboard { + const GLOBAL_CLIPBOARD_OBJECT: &str = "__arboard_global_clipboard"; + const GLOBAL_CALLBACK_OBJECT: &str = "__arboard_global_callback"; + + pub(crate) fn new() -> Result { + let window = web_sys::window().ok_or(Error::ClipboardNotSupported)?; + let inner = window.navigator().clipboard(); + + // If the clipboard is being opened for the first time, add a paste callback + if js_sys::Reflect::get(&window, &Self::GLOBAL_CALLBACK_OBJECT.into()) + .map_err(|_| Error::ClipboardNotSupported)? + .is_falsy() + { + let window_clone = window.clone(); + + let paste_callback = Closure::wrap(Box::new(move |e: web_sys::ClipboardEvent| { + if let Some(data_transfer) = e.clipboard_data() { + let object_to_set = if let Ok(text_data) = data_transfer.get_data("text") { + text_data.into() + } else { + web_sys::wasm_bindgen::JsValue::NULL.clone() + }; + + js_sys::Reflect::set( + &window_clone, + &Self::GLOBAL_CLIPBOARD_OBJECT.into(), + &object_to_set, + ) + .expect("Failed to set global clipboard object."); + } + }) as Box); + + // Set this event handler to execute before any child elements (third argument `true`) so that it is subsequently observed by other events. + window + .document() + .ok_or(Error::ClipboardNotSupported)? + .add_event_listener_with_callback_and_bool( + "paste", + &paste_callback.as_ref().unchecked_ref(), + true, + ) + .map_err(|_| Error::unknown("Could not add paste event listener."))?; + + js_sys::Reflect::set( + &window, + &Self::GLOBAL_CALLBACK_OBJECT.into(), + &web_sys::wasm_bindgen::JsValue::TRUE, + ) + .expect("Failed to set global callback flag."); + + paste_callback.forget(); + } + + Ok(Self { inner, window }) + } + + fn get_last_clipboard(&self) -> Option { + js_sys::Reflect::get(&self.window, &Self::GLOBAL_CLIPBOARD_OBJECT.into()) + .ok() + .and_then(|x| x.as_string()) + } + + fn set_last_clipboard(&self, value: &str) { + js_sys::Reflect::set(&self.window, &Self::GLOBAL_CLIPBOARD_OBJECT.into(), &value.into()) + .expect("Failed to set global clipboard object."); + } +} + +pub(crate) struct Clear<'clipboard> { + clipboard: &'clipboard mut Clipboard, +} + +impl<'clipboard> Clear<'clipboard> { + pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self { + Self { clipboard } + } + + pub(crate) fn clear(self) -> Result<(), Error> { + let _ = self.clipboard.inner.write_text(""); + self.clipboard.set_last_clipboard(""); + Ok(()) + } +} + +pub(crate) struct Get<'clipboard> { + clipboard: &'clipboard mut Clipboard, +} + +impl<'clipboard> Get<'clipboard> { + pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self { + Self { clipboard } + } + + pub(crate) fn text(self) -> Result { + self.clipboard.get_last_clipboard().ok_or_else(|| Error::ContentNotAvailable) + } + + #[cfg(feature = "image-data")] + pub(crate) fn image(self) -> Result, Error> { + Err(Error::ConversionFailure) + } +} + +pub(crate) struct Set<'clipboard> { + clipboard: &'clipboard mut Clipboard, +} + +impl<'clipboard> Set<'clipboard> { + pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self { + Self { clipboard } + } + + pub(crate) fn text(self, data: Cow<'_, str>) -> Result<(), Error> { + let _ = self.clipboard.inner.write_text(&data); + self.clipboard.set_last_clipboard(&data); + Ok(()) + } + + pub(crate) fn html(self, html: Cow<'_, str>, alt: Option>) -> Result<(), Error> { + let alt = match alt { + Some(s) => s.into(), + None => String::new(), + }; + + let html_item = js_sys::Object::new(); + js_sys::Reflect::set(&html_item, &"text/html".into(), &(&*html).into()) + .expect("Failed to set HTML item text."); + + let alt_item = js_sys::Object::new(); + js_sys::Reflect::set(&alt_item, &"text/plain".into(), &alt.into()) + .expect("Failed to set alt item text."); + + let mut clipboard_items = js_sys::Array::default(); + clipboard_items.extend([ + web_sys::ClipboardItem::new_with_record_from_str_to_str_promise(&html_item) + .map_err(|_| Error::unknown("Failed to create HTML clipboard item."))?, + web_sys::ClipboardItem::new_with_record_from_str_to_str_promise(&alt_item) + .map_err(|_| Error::unknown("Failed to create alt clipboard item."))?, + ]); + + let _ = self.clipboard.inner.write(&clipboard_items); + self.clipboard.set_last_clipboard(&html); + Ok(()) + } + + #[cfg(feature = "image-data")] + pub(crate) fn image(self, _: ImageData) -> Result<(), Error> { + Err(Error::ConversionFailure) + } +}