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