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 support for wasm32 + web #160

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
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 @@
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)>
DouglasDwyer marked this conversation as resolved.
Show resolved Hide resolved
}

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)?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Can this approach work in service workers/background workers? If not we can keep this hardcoded to window instead of global but that should be documented in the Clipboard documentation or similar.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately web workers cannot be supported - the clipboard API is not available in web workers (see the note on this page). I will add some documentation to mention this.

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.");
DouglasDwyer marked this conversation as resolved.
Show resolved Hide resolved
}
}) 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()
DouglasDwyer marked this conversation as resolved.
Show resolved Hide resolved
}

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());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the various clipboard calls failing return Error::Unknown?

Copy link
Author

@DouglasDwyer DouglasDwyer Aug 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The calls with let _ = don't fail immediately; they return a Promise that resolves once the browser has updated the clipboard contents. Since this is a synchronous API, I just fire and forget the promise here.

The only instances in which these calls asynchronously fail (according to Mozilla docs) is if the page doesn't have permission to write to the user's clipboard. Clipboard permissions are granted with transient activation - that is, once the user interacts with the page, writing to the clipboard is allowed.

It would be nice if there was a way to report the error here. The fire and forget approach is a bit jank - but it does work for my use case, which is just handling normal copy/paste interactions with egui on the web.

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);
DouglasDwyer marked this conversation as resolved.
Show resolved Hide resolved
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)
}
}
Loading