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 221a883..39872cd 100644 --- a/src/common.rs +++ b/src/common.rs @@ -90,7 +90,6 @@ impl std::fmt::Debug for Error { } impl Error { - #[cfg(windows)] pub(crate) fn unknown>(message: M) -> Self { Error::Unknown { description: message.into() } } @@ -174,9 +173,9 @@ impl Drop for ScopeGuard { /// Common trait for sealing platform extension traits. pub(crate) mod private { - // This is currently unused on macOS, so silence the warning which appears + // 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. - #[cfg_attr(target_vendor = "apple", allow(unreachable_pub))] + #[allow(unreachable_pub, unused)] pub trait Sealed {} impl Sealed for crate::Get<'_> {} diff --git a/src/lib.rs b/src/lib.rs index 57256d5..c074e55 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -131,6 +131,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 /// @@ -226,6 +227,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 b336463..62f3110 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(crate) use osx::*; + +#[cfg(target_arch = "wasm32")] +mod wasm; +#[cfg(target_arch = "wasm32")] +pub(crate) use wasm::*; \ No newline at end of file diff --git a/src/platform/wasm.rs b/src/platform/wasm.rs new file mode 100644 index 0000000..ab6a69d --- /dev/null +++ b/src/platform/wasm.rs @@ -0,0 +1,133 @@ +#[cfg(feature = "image-data")] +use crate::common::ImageData; +use crate::common::Error; +use js_sys::wasm_bindgen::JsCast; +use std::borrow::Cow; + +pub(crate) struct Clipboard { + inner: web_sys::Clipboard, + window: web_sys::Window, + _paste_callback: web_sys::wasm_bindgen::closure::Closure +} + +impl Clipboard { + const GLOBAL_CLIPBOARD_OBJECT: &str = "__arboard_global_clipboard"; + + pub(crate) fn new() -> Result { + let window = web_sys::window().ok_or(Error::ClipboardNotSupported)?; + let inner = window.navigator().clipboard(); + + let window_clone = window.clone(); + let paste_callback = web_sys::wasm_bindgen::closure::Closure::wrap(Box::new(move |e: web_sys::ClipboardEvent| { + if let Some(data_transfer) = e.clipboard_data() { + js_sys::Reflect::set(&window_clone, &Self::GLOBAL_CLIPBOARD_OBJECT.into(), &data_transfer.get_data("text").unwrap_or_default().into()) + .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."))?; + + Ok(Self { + inner, + _paste_callback: paste_callback, + window + }) + } + + fn get_last_clipboard(&self) -> String { + js_sys::Reflect::get(&self.window, &Self::GLOBAL_CLIPBOARD_OBJECT.into()) + .ok().and_then(|x| x.as_string()).unwrap_or_default() + } + + 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(&js_sys::Array::default()); + 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 { + Ok(self.clipboard.get_last_clipboard()) + } + + #[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(), + }; + + self.clipboard.set_last_clipboard(&html); + let html_item = js_sys::Object::new(); + js_sys::Reflect::set(&html_item, &"text/html".into(), &html.into_owned().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); + Ok(()) + } + + #[cfg(feature = "image-data")] + pub(crate) fn image(self, _: ImageData) -> Result<(), Error> { + Err(Error::ConversionFailure) + } +} \ No newline at end of file