//! This crate provides a platform-agnostic interface for accessing the clipboard.1//!2//! Read (and write) to the [`Clipboard`] resource to interact with the system clipboard.3//!4//! Note that this crate is deliberately low-level with minimal dependencies:5//! it does not provide any input integration for clipboard operations,6//! such as Ctrl+C/Ctrl+V support.7//!8//! This should be provided by other crates (or your own systems) which depend on `bevy_clipboard`,9//! such as `bevy_ui_widgets` in the case of text editing.10//!11//! `bevy_clipboard`'s primary advantage over using [`arboard`](https://crates.io/crates/arboard) directly is that12//! it provides a consistent API across all platforms, with a simple but robust fallback when `arboard`13//! is not available or clipboard permissions are not granted.14//!15//! ## Platform support16//!17//! On Android and iOS, `arboard` is not available and the `system_clipboard` feature has no18//! effect. The [`Clipboard`] resource still works, but reads and writes go to an in-process19//! buffer that is invisible to other applications and does not survive process exit.20//!21//! On Windows and Unix, clipboard operations are performed synchronously and results are22//! available immediately. On wasm32, results are accessed via [`ClipboardRead`], which can23//! be polled for completion.24//!25//! Images are supported on Windows and Unix when the `image` feature is enabled, which depends on `system_clipboard`.26//! Image support is not available on wasm32, Android, or iOS.2728extern crate alloc;2930use alloc::borrow::Cow;31#[cfg(feature = "image")]32use bevy_asset::RenderAssetUsages;33use bevy_ecs::resource::Resource;34#[cfg(feature = "image")]35use bevy_image::Image;36#[cfg(feature = "image")]37use wgpu_types::{Extent3d, TextureDimension, TextureFormat};3839#[cfg(target_arch = "wasm32")]40use wasm_bindgen_futures::JsFuture;41use {alloc::sync::Arc, bevy_platform::sync::Mutex};4243/// Commonly used types and traits from `bevy_clipboard`.44pub mod prelude {45pub use crate::{Clipboard, ClipboardPlugin, ClipboardRead};46}4748/// Adds clipboard support to a Bevy app.49///50/// The [`Clipboard`] resource is your main entry point.51///52/// See the [crate docs](crate) for more details.53#[derive(Default)]54pub struct ClipboardPlugin;5556impl bevy_app::Plugin for ClipboardPlugin {57fn build(&self, app: &mut bevy_app::App) {58app.init_resource::<Clipboard>();59}60}6162/// Represents an attempt to read from the clipboard.63///64/// On desktop targets the result is available immediately.65/// On web, the result is fetched asynchronously.66///67/// The generic `T` parameter represents the type of clipboard content that we are attempting to read,68/// which is `String` by default for text reads.69/// If the clipboard contents do not match this type,70/// the read will fail with a [`ClipboardError::ContentNotAvailable`]71/// or [`ClipboardError::ConversionFailure`] error.72///73/// ## Note on cloning74///75/// [`Clone`] on a [`ClipboardRead::Pending`] shares the underlying in-flight read, since76/// the inner state is held in an [`Arc`].77/// Only the first of the clones to successfully [`poll_result`](ClipboardRead::poll_result) will observe the value;78/// subsequent pollers will see `None` as if the read were still pending.79#[derive(Debug, Clone)]80pub enum ClipboardRead<T = String> {81/// The clipboard contents are ready to be accessed.82Ready(Result<T, ClipboardError>),83/// The clipboard contents are being fetched asynchronously.84///85/// The `Option` is `None` while the read is still pending, and becomes `Some` once the read completes with either success or error.86/// `Some(Ok)` indicates a successful read with the clipboard contents, while `Some(Err)` indicates a failure to read the clipboard.87Pending(Arc<Mutex<Option<Result<T, ClipboardError>>>>),88/// The clipboard contents have already been taken by a previous call to [`ClipboardRead::poll_result`].89Taken,90}9192impl<T> ClipboardRead<T> {93/// The result of an attempt to read from the clipboard, once ready.94///95/// Returns `None` if the result is still pending or has already been taken.96pub fn poll_result(&mut self) -> Option<Result<T, ClipboardError>> {97match self {98Self::Pending(shared) => {99let contents = shared.lock().ok().and_then(|mut inner| inner.take())?;100*self = Self::Taken;101Some(contents)102}103Self::Ready(_) => {104let Self::Ready(inner) = core::mem::replace(self, Self::Taken) else {105unreachable!()106};107Some(inner)108}109Self::Taken => None,110}111}112}113114#[cfg(feature = "image")]115fn try_image_from_imagedata(image: arboard::ImageData<'static>) -> Result<Image, ClipboardError> {116let size = Extent3d {117width: u32::try_from(image.width).map_err(|_| ClipboardError::ConversionFailure)?,118height: u32::try_from(image.height).map_err(|_| ClipboardError::ConversionFailure)?,119depth_or_array_layers: 1,120};121Ok(Image::new(122size,123TextureDimension::D2,124image.bytes.into_owned(),125TextureFormat::Rgba8UnormSrgb,126RenderAssetUsages::default(),127))128}129130#[cfg(feature = "image")]131fn try_imagedata_from_image(image: &Image) -> Result<arboard::ImageData<'_>, ClipboardError> {132// arboard expects packed RGBA8.133// We need to reject anything else: a same-size format like134// Bgra8Unorm would pass the length check but produce corrupt colors.135if !matches!(136image.texture_descriptor.format,137TextureFormat::Rgba8Unorm | TextureFormat::Rgba8UnormSrgb138) {139return Err(ClipboardError::ConversionFailure);140}141142let width = image.width() as usize;143let height = image.height() as usize;144let data = image145.data146.as_ref()147.ok_or(ClipboardError::ConversionFailure)?;148if data.len()149!= width150.checked_mul(height)151.and_then(|pixels| pixels.checked_mul(4))152.ok_or(ClipboardError::ConversionFailure)?153{154return Err(ClipboardError::ConversionFailure);155}156157Ok(arboard::ImageData {158width,159height,160bytes: Cow::Borrowed(data.as_slice()),161})162}163164/// A resource which provides access to the system clipboard.165///166/// Use [`Clipboard::fetch_text`] to read text from the clipboard,167/// and [`Clipboard::set_text`] to write text to the clipboard.168///169/// ## Warning: `system_clipboard` support is off-by-default170///171/// When the `system_clipboard` feature is disabled, operations read from and write to172/// an in-process [`String`] buffer rather than the clipboard provided by the operating system.173/// This means that you will not be able to copy and paste between your application and other applications,174/// and clipboard contents will not persist after your application exits.175/// This is a secure-by-default setup, but is not correct for many applications which require clipboard functionality.176///177/// The fallback is intended to allow clipboard functionality on platforms where `arboard` is not available (e.g. Android, iOS),178/// and to allow applications to have basic clipboard-like functionality without requiring enhanced permissions.179///180/// ## Warning: multithreading deadlock risks181///182/// As the [`arboard`] documentation [warns](https://docs.rs/arboard/latest/arboard/struct.Clipboard.html#windows),183/// accessing the system clipboard on Windows can cause deadlocks if multiple threads or processes attempt to access it simultaneously.184/// Typical usage of the [`Clipboard`] resource should not encounter this issue: Bevy's copy of the [`Clipboard`] resource is unique,185/// and both reading from and writing to it requires exclusive access, enforced by Rust's borrowing rules.186///187/// However, care should be taken to avoid cloning the [`Clipboard`] resource, duplicating it between worlds, reading from it in parallel,188/// or otherwise sharing it across threads, as this could lead to multiple instances attempting to access the clipboard simultaneously and causing a deadlock.189#[derive(Resource)]190pub struct Clipboard {191#[cfg(all(any(unix, windows), feature = "system_clipboard"))]192system_clipboard: Option<arboard::Clipboard>,193// Unfortunately, this cannot be simplified to `not(any(feature = "system_clipboard", target_arch = "wasm32"))`.194// `system_clipboard` is a platform-conditional dependency (windows/unix only), so on other platforms195// (Android, iOS, etc.) `cfg(feature = "system_clipboard")` can be true even though the crate is not196// present. Removing the platform guard would leave those targets with an empty struct and a197// broken fallback. wasm32 is excluded separately because it calls web-sys directly and stores198// no state in the struct.199#[cfg(not(any(200all(any(windows, unix), feature = "system_clipboard"),201target_arch = "wasm32"202)))]203text: String,204}205206#[cfg_attr(207not(all(any(unix, windows), feature = "system_clipboard")),208expect(209clippy::derivable_impls,210reason = "non-derivable on unix/windows with system_clipboard"211)212)]213impl Default for Clipboard {214fn default() -> Self {215Self {216#[cfg(all(any(unix, windows), feature = "system_clipboard"))]217system_clipboard: arboard::Clipboard::new().ok(),218#[cfg(not(any(219all(any(windows, unix), feature = "system_clipboard"),220target_arch = "wasm32"221)))]222text: String::new(),223}224}225}226227impl Clipboard {228/// Fetches UTF-8 text from the clipboard and returns it via a `ClipboardRead`.229///230/// On Windows and Unix `ClipboardRead`s are completed instantly, on wasm32 the result is fetched asynchronously.231pub fn fetch_text(&mut self) -> ClipboardRead {232#[cfg(all(any(unix, windows), feature = "system_clipboard"))]233{234ClipboardRead::Ready(235self.system_clipboard236.as_mut()237.ok_or(ClipboardError::ClipboardNotSupported)238.and_then(|clipboard| clipboard.get_text().map_err(ClipboardError::from)),239)240}241242#[cfg(target_arch = "wasm32")]243{244if let Some(clipboard) = web_sys::window().map(|w| w.navigator().clipboard()) {245let shared = Arc::new(Mutex::new(None));246let shared_clone = shared.clone();247wasm_bindgen_futures::spawn_local(async move {248let text = JsFuture::from(clipboard.read_text()).await;249let text = match text {250Ok(text) => text.as_string().ok_or(ClipboardError::ConversionFailure),251Err(_) => Err(ClipboardError::ContentNotAvailable),252};253if let Ok(mut guard) = shared.lock() {254guard.replace(text);255}256});257ClipboardRead::Pending(shared_clone)258} else {259ClipboardRead::Ready(Err(ClipboardError::ClipboardNotSupported))260}261}262263#[cfg(not(any(264all(any(windows, unix), feature = "system_clipboard"),265target_arch = "wasm32"266)))]267{268#[cfg(any(windows, unix))]269bevy_log::warn_once!(270"Clipboard read used an in-process fallback buffer rather than the OS clipboard. \271Enable the `system_clipboard` feature on `bevy_clipboard` to use the OS clipboard."272);273ClipboardRead::Ready(Ok(self.text.clone()))274}275}276277/// Fetches image data from the clipboard.278///279/// Only supported on Windows and Unix platforms with the `image` feature enabled.280#[cfg(feature = "image")]281pub fn fetch_image(&mut self) -> Result<Image, ClipboardError> {282self.system_clipboard283.as_mut()284.ok_or(ClipboardError::ClipboardNotSupported)285.and_then(|clipboard| {286clipboard287.get_image()288.map_err(ClipboardError::from)289.and_then(try_image_from_imagedata)290})291}292293/// Places the text onto the clipboard. Any valid UTF-8 string is accepted.294///295/// # Errors296///297/// Returns error if `text` failed to be stored on the clipboard.298pub fn set_text<'a, T: Into<Cow<'a, str>>>(&mut self, text: T) -> Result<(), ClipboardError> {299#[cfg(all(any(unix, windows), feature = "system_clipboard"))]300{301self.system_clipboard302.as_mut()303.ok_or(ClipboardError::ClipboardNotSupported)304.and_then(|clipboard| clipboard.set_text(text).map_err(ClipboardError::from))305}306307#[cfg(target_arch = "wasm32")]308{309web_sys::window()310.map(|w| w.navigator().clipboard())311.ok_or(ClipboardError::ClipboardNotSupported)312.map(|clipboard| {313let text = text.into().to_string();314wasm_bindgen_futures::spawn_local(async move {315if let Err(e) = JsFuture::from(clipboard.write_text(&text)).await {316bevy_log::warn!("Failed to write text to clipboard: {e:?}");317}318});319})320}321322#[cfg(not(any(323all(any(windows, unix), feature = "system_clipboard"),324target_arch = "wasm32"325)))]326{327#[cfg(any(windows, unix))]328bevy_log::warn_once!(329"Clipboard write used an in-process fallback buffer rather than the OS clipboard. \330Enable the `system_clipboard` feature on `bevy_clipboard` to use the OS clipboard."331);332self.text = text.into().into_owned();333Ok(())334}335}336337/// Places image data onto the clipboard.338///339/// The image must contain initialized 2D pixel data in packed RGBA8 row-major order.340/// Only supported on Windows and Unix platforms with the `image` feature enabled.341///342/// # Errors343///344/// Returns an error if the image data is invalid or the clipboard write fails.345#[cfg(feature = "image")]346pub fn set_image(&mut self, image: &Image) -> Result<(), ClipboardError> {347self.system_clipboard348.as_mut()349.ok_or(ClipboardError::ClipboardNotSupported)350.and_then(|clipboard| {351clipboard352.set_image(try_imagedata_from_image(image)?)353.map_err(ClipboardError::from)354})355}356}357358/// An error that might happen during a clipboard operation.359#[non_exhaustive]360#[derive(Debug, Clone)]361pub enum ClipboardError {362/// Clipboard contents were unavailable or not in the expected format.363ContentNotAvailable,364365/// No suitable clipboard backend was available366ClipboardNotSupported,367368/// Clipboard access is temporarily locked by another process or thread.369ClipboardOccupied,370371/// The data could not be converted to or from the required format.372ConversionFailure,373374/// An unknown error375Unknown {376/// String describing the error377description: String,378},379}380381impl core::fmt::Display for ClipboardError {382fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {383match self {384Self::ContentNotAvailable => {385write!(386f,387"clipboard contents were unavailable or not in the expected format"388)389}390Self::ClipboardNotSupported => {391write!(f, "no suitable clipboard backend was available")392}393Self::ClipboardOccupied => {394write!(395f,396"clipboard access is temporarily locked by another process or thread"397)398}399Self::ConversionFailure => {400write!(401f,402"data could not be converted to or from the required format"403)404}405Self::Unknown { description } => write!(f, "unknown clipboard error: {description}"),406}407}408}409410impl core::error::Error for ClipboardError {}411412#[cfg(all(any(windows, unix), feature = "system_clipboard"))]413impl From<arboard::Error> for ClipboardError {414fn from(value: arboard::Error) -> Self {415match value {416arboard::Error::ContentNotAvailable => ClipboardError::ContentNotAvailable,417arboard::Error::ClipboardNotSupported => ClipboardError::ClipboardNotSupported,418arboard::Error::ClipboardOccupied => ClipboardError::ClipboardOccupied,419arboard::Error::ConversionFailure => ClipboardError::ConversionFailure,420arboard::Error::Unknown { description } => ClipboardError::Unknown { description },421_ => ClipboardError::Unknown {422description: "Unknown arboard error variant".to_owned(),423},424}425}426}427428429