Skip to content

Commit

Permalink
Implement WASM clipboard
Browse files Browse the repository at this point in the history
  • Loading branch information
DouglasDwyer committed Aug 17, 2024
1 parent 151e679 commit 63f97a9
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 3 deletions.
82 changes: 82 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
5 changes: 2 additions & 3 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ impl std::fmt::Debug for Error {
}

impl Error {
#[cfg(windows)]
pub(crate) fn unknown<M: Into<String>>(message: M) -> Self {
Error::Unknown { description: message.into() }
}
Expand Down Expand Up @@ -174,9 +173,9 @@ impl<F: FnOnce()> Drop for ScopeGuard<F> {

/// 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<'_> {}
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions src/platform/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@ pub use windows::*;
mod osx;
#[cfg(target_os = "macos")]
pub(crate) use osx::*;

#[cfg(target_arch = "wasm32")]
mod wasm;

Check warning on line 20 in src/platform/mod.rs

View workflow job for this annotation

GitHub Actions / rustfmt

Diff in /home/runner/work/arboard/arboard/src/platform/mod.rs
#[cfg(target_arch = "wasm32")]
pub(crate) use wasm::*;
133 changes: 133 additions & 0 deletions src/platform/wasm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#[cfg(feature = "image-data")]

Check warning on line 1 in src/platform/wasm.rs

View workflow job for this annotation

GitHub Actions / rustfmt

Diff in /home/runner/work/arboard/arboard/src/platform/wasm.rs
use crate::common::ImageData;
use crate::common::Error;
use js_sys::wasm_bindgen::JsCast;
use std::borrow::Cow;

pub(crate) struct Clipboard {

Check warning on line 7 in src/platform/wasm.rs

View workflow job for this annotation

GitHub Actions / rustfmt

Diff in /home/runner/work/arboard/arboard/src/platform/wasm.rs
inner: web_sys::Clipboard,
window: web_sys::Window,
_paste_callback: web_sys::wasm_bindgen::closure::Closure<dyn FnMut(web_sys::ClipboardEvent)>
}

impl Clipboard {
const GLOBAL_CLIPBOARD_OBJECT: &str = "__arboard_global_clipboard";

Check warning on line 14 in src/platform/wasm.rs

View workflow job for this annotation

GitHub Actions / rustfmt

Diff in /home/runner/work/arboard/arboard/src/platform/wasm.rs

pub(crate) fn new() -> Result<Self, Error> {
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<dyn FnMut(_)>);

// 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 }
}

Check warning on line 57 in src/platform/wasm.rs

View workflow job for this annotation

GitHub Actions / rustfmt

Diff in /home/runner/work/arboard/arboard/src/platform/wasm.rs

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

Check warning on line 73 in src/platform/wasm.rs

View workflow job for this annotation

GitHub Actions / rustfmt

Diff in /home/runner/work/arboard/arboard/src/platform/wasm.rs

pub(crate) fn text(self) -> Result<String, Error> {
Ok(self.clipboard.get_last_clipboard())
}

#[cfg(feature = "image-data")]
pub(crate) fn image(self) -> Result<ImageData<'static>, Error> {

Check warning on line 80 in src/platform/wasm.rs

View workflow job for this annotation

GitHub Actions / rustfmt

Diff in /home/runner/work/arboard/arboard/src/platform/wasm.rs
Err(Error::ConversionFailure)
}
}

pub(crate) struct Set<'clipboard> {
clipboard: &'clipboard mut Clipboard,
}

Check warning on line 88 in src/platform/wasm.rs

View workflow job for this annotation

GitHub Actions / rustfmt

Diff in /home/runner/work/arboard/arboard/src/platform/wasm.rs
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);

Check warning on line 97 in src/platform/wasm.rs

View workflow job for this annotation

GitHub Actions / rustfmt

Diff in /home/runner/work/arboard/arboard/src/platform/wasm.rs
self.clipboard.set_last_clipboard(&data);
Ok(())
}

pub(crate) fn html(self, html: Cow<'_, str>, alt: Option<Cow<'_, str>>) -> Result<(), Error> {
let alt = match alt {
Some(s) => s.into(),

Check warning on line 104 in src/platform/wasm.rs

View workflow job for this annotation

GitHub Actions / rustfmt

Diff in /home/runner/work/arboard/arboard/src/platform/wasm.rs
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)
}
}

0 comments on commit 63f97a9

Please sign in to comment.