Basic clipboard support (#19106)

# Objective

Add a platform-agnostic interface for interacting with the clipboard.

## Solution

New crate `bevy_clipboard` with a `ClipboardPlugin` that adds a
`Clipboard` resource. The clipboard is accessed using the methods
`fetch_text`, `fetch_image`, `set_text` and `set_image` on the
`Clipboard` resource. `fetch_text` returns a `ClipboardRead` with a
`poll_result` method that's used to get the actual value once it's
ready.

The `windows` and `unix` implementations both use the `arboard` crate.
On windows the `Clipboard` resource is a unit struct and a new arboard
clipboard instance is created and dropped for each clipboard access. On
unix targets the `Clipboard` resource holds a clipboard instance it
reuses each time. On both targets the `fetch_*` and `set_*` methods work
instantly.

On `wasm32` `Clipboard` is a unit struct. The `fetch_text` and
`set_text` functions spawn async tasks. The task spawned by `fetch_text`
updates the shared arc mutex option once the future is evaluated to get
the clipboard text. There is no image support on `wasm32`.

Everything seems to work but it feels like the design is a bit clumsy
and not very idiomatic. I don't tend to do much asynchronous
programming, maybe a reviewer can suggest an improved construction.

I also added an alternative `fetch_text_async` function for async access
that returns a `Result<String, ClipboardError>` future.

### Notes
* Doesn't support android targets yet. 
* The wasm32 implementation doesn't support images. It's much more
complicated and probably best to left to a follow up.

## Testing

The PR includes a basic example `clipboard` that can be used for
testing.
The image display will only work if the image is already in the
clipboard before the example starts.

---------

Co-authored-by: Gilles Henaux <ghx_github_priv@fastmail.com>
Co-authored-by: Andrew Zhurov <zhurov.andrew@gmail.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
ickshonpe
2026-04-23 22:18:07 +01:00
committed by GitHub
parent 58e91dd1d9
commit 41f170c0a3
17 changed files with 888 additions and 61 deletions
+12 -1
View File
@@ -187,6 +187,9 @@ default_platform = [
"android-game-activity",
"bevy_gilrs",
"bevy_winit",
# Note: OS-integrated clipboard support is gated behind the `system_clipboard` feature,
# which is not enabled by default for security reasons.
"bevy_clipboard",
"default_font",
"multi_threaded",
"webgl2",
@@ -395,6 +398,9 @@ bevy_log = ["bevy_internal/bevy_log"]
# Enable input focus subsystem
bevy_input_focus = ["bevy_internal/bevy_input_focus"]
# Clipboard resource and management. See `system_clipboard` for OS-integrated clipboard support.
bevy_clipboard = ["bevy_internal/bevy_clipboard"]
# Headless widget collection for Bevy UI.
bevy_ui_widgets = ["bevy_internal/bevy_ui_widgets"]
@@ -434,6 +440,12 @@ basis-universal = ["bevy_internal/basis-universal"]
# Enables compressed KTX2 UASTC texture output on the asset processor
compressed_image_saver = ["bevy_internal/compressed_image_saver"]
# Enables system-level clipboard support.
system_clipboard = ["bevy_clipboard", "bevy_internal/system_clipboard"]
# Enables image copy/paste via the system clipboard. Not supported on WASM.
clipboard_image = ["system_clipboard", "bevy_internal/clipboard_image"]
# BMP image format support
bmp = ["bevy_internal/bmp"]
@@ -3849,7 +3861,6 @@ description = "Simple example demonstrating the OverflowClipMargin style propert
category = "UI (User Interface)"
wasm = true
[[example]]
name = "overflow_debug"
path = "examples/ui/scroll_and_overflow/overflow_debug.rs"
+45
View File
@@ -0,0 +1,45 @@
[package]
name = "bevy_clipboard"
version = "0.19.0-dev"
edition = "2024"
description = "Provides clipboard support for Bevy Engine"
homepage = "https://bevyengine.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy", "clipboard"]
[features]
default = []
system_clipboard = ["dep:arboard"]
image = [
"system_clipboard",
"dep:bevy_asset",
"dep:bevy_image",
"dep:wgpu-types",
"arboard/image-data",
]
[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.19.0-dev", default-features = false }
bevy_ecs = { path = "../bevy_ecs", version = "0.19.0-dev", default-features = false }
bevy_log = { path = "../bevy_log", version = "0.19.0-dev", default-features = false }
bevy_platform = { path = "../bevy_platform", version = "0.19.0-dev", default-features = false }
[target.'cfg(any(windows, unix))'.dependencies]
bevy_asset = { path = "../bevy_asset", version = "0.19.0-dev", default-features = false, optional = true }
bevy_image = { path = "../bevy_image", version = "0.19.0-dev", default-features = false, optional = true }
wgpu-types = { version = "29.0.1", default-features = false, optional = true }
arboard = { version = "3.6.1", default-features = false, optional = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = { version = "0.2" }
web-sys = { version = "0.3", features = ["Window", "Navigator", "Clipboard"] }
wasm-bindgen-futures = "0.4"
[lints]
workspace = true
[package.metadata.docs.rs]
rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"]
all-features = true
+176
View File
@@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
+19
View File
@@ -0,0 +1,19 @@
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+7
View File
@@ -0,0 +1,7 @@
# Bevy Clipboard
[![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](https://github.com/bevyengine/bevy#license)
[![Crates.io](https://img.shields.io/crates/v/bevy_clipboard.svg)](https://crates.io/crates/bevy_clipboard)
[![Downloads](https://img.shields.io/crates/d/bevy_clipboard.svg)](https://crates.io/crates/bevy_clipboard)
[![Docs](https://docs.rs/bevy_clipboard/badge.svg)](https://docs.rs/bevy_clipboard/latest/bevy_clipboard/)
[![Discord](https://img.shields.io/discord/691052431525675048.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/bevy)
+420
View File
@@ -0,0 +1,420 @@
//! This crate provides a platform-agnostic interface for accessing the clipboard.
//!
//! Read (and write) to the [`Clipboard`] resource to interact with the system clipboard.
//!
//! Note that this crate is deliberately low-level with minimal dependencies:
//! it does not provide any input integration for clipboard operations,
//! such as Ctrl+C/Ctrl+V support.
//!
//! This should be provided by other crates (or your own systems) which depend on `bevy_clipboard`,
//! such as `bevy_ui_widgets` in the case of text editing.
//!
//! `bevy_clipboard`'s primary advantage over using [`arboard`](https://crates.io/crates/arboard) directly is that
//! it provides a consistent API across all platforms, with a simple but robust fallback when `arboard`
//! is not available or clipboard permissions are not granted.
//!
//! ## Platform support
//!
//! On Android and iOS, `arboard` is not available and the `system_clipboard` feature has no
//! effect. The [`Clipboard`] resource still works, but reads and writes go to an in-process
//! buffer that is invisible to other applications and does not survive process exit.
//!
//! On Windows and Unix, clipboard operations are performed synchronously and results are
//! available immediately. On wasm32, results are accessed via [`ClipboardRead`], which can
//! be polled for completion.
//!
//! Images are supported on Windows and Unix when the `image` feature is enabled, which depends on `system_clipboard`.
//! Image support is not available on wasm32, Android, or iOS.
extern crate alloc;
use alloc::borrow::Cow;
#[cfg(feature = "image")]
use bevy_asset::RenderAssetUsages;
use bevy_ecs::resource::Resource;
#[cfg(feature = "image")]
use bevy_image::Image;
#[cfg(feature = "image")]
use wgpu_types::{Extent3d, TextureDimension, TextureFormat};
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_futures::JsFuture;
use {alloc::sync::Arc, bevy_platform::sync::Mutex};
/// Commonly used types and traits from `bevy_clipboard`.
pub mod prelude {
pub use crate::{Clipboard, ClipboardPlugin, ClipboardRead};
}
/// Adds clipboard support to a Bevy app.
///
/// The [`Clipboard`] resource is your main entry point.
///
/// See the [crate docs](crate) for more details.
#[derive(Default)]
pub struct ClipboardPlugin;
impl bevy_app::Plugin for ClipboardPlugin {
fn build(&self, app: &mut bevy_app::App) {
app.init_resource::<Clipboard>();
}
}
/// Represents an attempt to read from the clipboard.
///
/// On desktop targets the result is available immediately.
/// On web, the result is fetched asynchronously.
///
/// The generic `T` parameter represents the type of clipboard content that we are attempting to read,
/// which is `String` by default for text reads.
/// If the clipboard contents do not match this type,
/// the read will fail with a [`ClipboardError::ContentNotAvailable`]
/// or [`ClipboardError::ConversionFailure`] error.
///
/// ## Note on cloning
///
/// [`Clone`] on a [`ClipboardRead::Pending`] shares the underlying in-flight read, since
/// the inner state is held in an [`Arc`].
/// Only the first of the clones to successfully [`poll_result`](ClipboardRead::poll_result) will observe the value;
/// subsequent pollers will see `None` as if the read were still pending.
#[derive(Debug, Clone)]
pub enum ClipboardRead<T = String> {
/// The clipboard contents are ready to be accessed.
Ready(Result<T, ClipboardError>),
/// The clipboard contents are being fetched asynchronously.
///
/// The `Option` is `None` while the read is still pending, and becomes `Some` once the read completes with either success or error.
/// `Some(Ok)` indicates a successful read with the clipboard contents, while `Some(Err)` indicates a failure to read the clipboard.
Pending(Arc<Mutex<Option<Result<T, ClipboardError>>>>),
/// The clipboard contents have already been taken by a previous call to [`ClipboardRead::poll_result`].
Taken,
}
impl<T> ClipboardRead<T> {
/// The result of an attempt to read from the clipboard, once ready.
///
/// Returns `None` if the result is still pending or has already been taken.
pub fn poll_result(&mut self) -> Option<Result<T, ClipboardError>> {
match self {
Self::Pending(shared) => {
let contents = shared.lock().ok().and_then(|mut inner| inner.take())?;
*self = Self::Taken;
Some(contents)
}
Self::Ready(_) => {
let Self::Ready(inner) = core::mem::replace(self, Self::Taken) else {
unreachable!()
};
Some(inner)
}
Self::Taken => None,
}
}
}
#[cfg(feature = "image")]
fn try_image_from_imagedata(image: arboard::ImageData<'static>) -> Result<Image, ClipboardError> {
let size = Extent3d {
width: u32::try_from(image.width).map_err(|_| ClipboardError::ConversionFailure)?,
height: u32::try_from(image.height).map_err(|_| ClipboardError::ConversionFailure)?,
depth_or_array_layers: 1,
};
Ok(Image::new(
size,
TextureDimension::D2,
image.bytes.into_owned(),
TextureFormat::Rgba8UnormSrgb,
RenderAssetUsages::default(),
))
}
#[cfg(feature = "image")]
fn try_imagedata_from_image(image: &Image) -> Result<arboard::ImageData<'_>, ClipboardError> {
// arboard expects packed RGBA8.
// We need to reject anything else: a same-size format like
// Bgra8Unorm would pass the length check but produce corrupt colors.
if !matches!(
image.texture_descriptor.format,
TextureFormat::Rgba8Unorm | TextureFormat::Rgba8UnormSrgb
) {
return Err(ClipboardError::ConversionFailure);
}
let width = image.width() as usize;
let height = image.height() as usize;
let data = image
.data
.as_ref()
.ok_or(ClipboardError::ConversionFailure)?;
if data.len()
!= width
.checked_mul(height)
.and_then(|pixels| pixels.checked_mul(4))
.ok_or(ClipboardError::ConversionFailure)?
{
return Err(ClipboardError::ConversionFailure);
}
Ok(arboard::ImageData {
width,
height,
bytes: Cow::Borrowed(data.as_slice()),
})
}
/// A resource which provides access to the system clipboard.
///
/// Use [`Clipboard::fetch_text`] to read text from the clipboard,
/// and [`Clipboard::set_text`] to write text to the clipboard.
///
/// ## Warning: `system_clipboard` support is off-by-default
///
/// When the `system_clipboard` feature is disabled, operations read from and write to
/// an in-process [`String`] buffer rather than the clipboard provided by the operating system.
/// This means that you will not be able to copy and paste between your application and other applications,
/// and clipboard contents will not persist after your application exits.
/// This is a secure-by-default setup, but is not correct for many applications which require clipboard functionality.
///
/// The fallback is intended to allow clipboard functionality on platforms where `arboard` is not available (e.g. Android, iOS),
/// and to allow applications to have basic clipboard-like functionality without requiring enhanced permissions.
///
/// ## Warning: multithreading deadlock risks
///
/// As the [`arboard`] documentation [warns](https://docs.rs/arboard/latest/arboard/struct.Clipboard.html#windows),
/// accessing the system clipboard on Windows can cause deadlocks if multiple threads or processes attempt to access it simultaneously.
/// Typical usage of the [`Clipboard`] resource should not encounter this issue: Bevy's copy of the [`Clipboard`] resource is unique,
/// and both reading from and writing to it requires exclusive access, enforced by Rust's borrowing rules.
///
/// However, care should be taken to avoid cloning the [`Clipboard`] resource, duplicating it between worlds, reading from it in parallel,
/// or otherwise sharing it across threads, as this could lead to multiple instances attempting to access the clipboard simultaneously and causing a deadlock.
#[derive(Resource)]
pub struct Clipboard {
#[cfg(all(any(unix, windows), feature = "system_clipboard"))]
system_clipboard: Option<arboard::Clipboard>,
// Unfortunately, this cannot be simplified to `not(any(feature = "system_clipboard", target_arch = "wasm32"))`.
// `system_clipboard` is a platform-conditional dependency (windows/unix only), so on other platforms
// (Android, iOS, etc.) `cfg(feature = "system_clipboard")` can be true even though the crate is not
// present. Removing the platform guard would leave those targets with an empty struct and a
// broken fallback. wasm32 is excluded separately because it calls web-sys directly and stores
// no state in the struct.
#[cfg(not(any(
all(any(windows, unix), feature = "system_clipboard"),
target_arch = "wasm32"
)))]
text: String,
}
impl Default for Clipboard {
fn default() -> Self {
Self {
#[cfg(all(any(unix, windows), feature = "system_clipboard"))]
system_clipboard: arboard::Clipboard::new().ok(),
#[cfg(not(any(
all(any(windows, unix), feature = "system_clipboard"),
target_arch = "wasm32"
)))]
text: String::new(),
}
}
}
impl Clipboard {
/// Fetches UTF-8 text from the clipboard and returns it via a `ClipboardRead`.
///
/// On Windows and Unix `ClipboardRead`s are completed instantly, on wasm32 the result is fetched asynchronously.
pub fn fetch_text(&mut self) -> ClipboardRead {
#[cfg(all(any(unix, windows), feature = "system_clipboard"))]
{
ClipboardRead::Ready(
self.system_clipboard
.as_mut()
.ok_or(ClipboardError::ClipboardNotSupported)
.and_then(|clipboard| clipboard.get_text().map_err(ClipboardError::from)),
)
}
#[cfg(target_arch = "wasm32")]
{
if let Some(clipboard) = web_sys::window().map(|w| w.navigator().clipboard()) {
let shared = Arc::new(Mutex::new(None));
let shared_clone = shared.clone();
wasm_bindgen_futures::spawn_local(async move {
let text = JsFuture::from(clipboard.read_text()).await;
let text = match text {
Ok(text) => text.as_string().ok_or(ClipboardError::ConversionFailure),
Err(_) => Err(ClipboardError::ContentNotAvailable),
};
if let Ok(mut guard) = shared.lock() {
guard.replace(text);
}
});
ClipboardRead::Pending(shared_clone)
} else {
ClipboardRead::Ready(Err(ClipboardError::ClipboardNotSupported))
}
}
#[cfg(not(any(
all(any(windows, unix), feature = "system_clipboard"),
target_arch = "wasm32"
)))]
{
#[cfg(any(windows, unix))]
bevy_log::warn_once!(
"Clipboard read used an in-process fallback buffer rather than the OS clipboard. \
Enable the `system_clipboard` feature on `bevy_clipboard` to use the OS clipboard."
);
ClipboardRead::Ready(Ok(self.text.clone()))
}
}
/// Fetches image data from the clipboard.
///
/// Only supported on Windows and Unix platforms with the `image` feature enabled.
#[cfg(feature = "image")]
pub fn fetch_image(&mut self) -> Result<Image, ClipboardError> {
self.system_clipboard
.as_mut()
.ok_or(ClipboardError::ClipboardNotSupported)
.and_then(|clipboard| {
clipboard
.get_image()
.map_err(ClipboardError::from)
.and_then(try_image_from_imagedata)
})
}
/// Places the text onto the clipboard. Any valid UTF-8 string is accepted.
///
/// # Errors
///
/// Returns error if `text` failed to be stored on the clipboard.
pub fn set_text<'a, T: Into<Cow<'a, str>>>(&mut self, text: T) -> Result<(), ClipboardError> {
#[cfg(all(any(unix, windows), feature = "system_clipboard"))]
{
self.system_clipboard
.as_mut()
.ok_or(ClipboardError::ClipboardNotSupported)
.and_then(|clipboard| clipboard.set_text(text).map_err(ClipboardError::from))
}
#[cfg(target_arch = "wasm32")]
{
web_sys::window()
.map(|w| w.navigator().clipboard())
.ok_or(ClipboardError::ClipboardNotSupported)
.map(|clipboard| {
let text = text.into().to_string();
wasm_bindgen_futures::spawn_local(async move {
if let Err(e) = JsFuture::from(clipboard.write_text(&text)).await {
bevy_log::warn!("Failed to write text to clipboard: {e:?}");
}
});
})
}
#[cfg(not(any(
all(any(windows, unix), feature = "system_clipboard"),
target_arch = "wasm32"
)))]
{
#[cfg(any(windows, unix))]
bevy_log::warn_once!(
"Clipboard write used an in-process fallback buffer rather than the OS clipboard. \
Enable the `system_clipboard` feature on `bevy_clipboard` to use the OS clipboard."
);
self.text = text.into().into_owned();
Ok(())
}
}
/// Places image data onto the clipboard.
///
/// The image must contain initialized 2D pixel data in packed RGBA8 row-major order.
/// Only supported on Windows and Unix platforms with the `image` feature enabled.
///
/// # Errors
///
/// Returns an error if the image data is invalid or the clipboard write fails.
#[cfg(feature = "image")]
pub fn set_image(&mut self, image: &Image) -> Result<(), ClipboardError> {
self.system_clipboard
.as_mut()
.ok_or(ClipboardError::ClipboardNotSupported)
.and_then(|clipboard| {
clipboard
.set_image(try_imagedata_from_image(image)?)
.map_err(ClipboardError::from)
})
}
}
/// An error that might happen during a clipboard operation.
#[non_exhaustive]
#[derive(Debug, Clone)]
pub enum ClipboardError {
/// Clipboard contents were unavailable or not in the expected format.
ContentNotAvailable,
/// No suitable clipboard backend was available
ClipboardNotSupported,
/// Clipboard access is temporarily locked by another process or thread.
ClipboardOccupied,
/// The data could not be converted to or from the required format.
ConversionFailure,
/// An unknown error
Unknown {
/// String describing the error
description: String,
},
}
impl core::fmt::Display for ClipboardError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::ContentNotAvailable => {
write!(
f,
"clipboard contents were unavailable or not in the expected format"
)
}
Self::ClipboardNotSupported => {
write!(f, "no suitable clipboard backend was available")
}
Self::ClipboardOccupied => {
write!(
f,
"clipboard access is temporarily locked by another process or thread"
)
}
Self::ConversionFailure => {
write!(
f,
"data could not be converted to or from the required format"
)
}
Self::Unknown { description } => write!(f, "unknown clipboard error: {description}"),
}
}
}
impl core::error::Error for ClipboardError {}
#[cfg(all(any(windows, unix), feature = "system_clipboard"))]
impl From<arboard::Error> for ClipboardError {
fn from(value: arboard::Error) -> Self {
match value {
arboard::Error::ContentNotAvailable => ClipboardError::ContentNotAvailable,
arboard::Error::ClipboardNotSupported => ClipboardError::ClipboardNotSupported,
arboard::Error::ClipboardOccupied => ClipboardError::ClipboardOccupied,
arboard::Error::ConversionFailure => ClipboardError::ConversionFailure,
arboard::Error::Unknown { description } => ClipboardError::Unknown { description },
_ => ClipboardError::Unknown {
description: "Unknown arboard error variant".to_owned(),
},
}
}
}
+10
View File
@@ -459,6 +459,15 @@ gamepad = ["bevy_input/gamepad", "bevy_input_focus?/gamepad"]
touch = ["bevy_input/touch"]
gestures = ["bevy_input/gestures"]
# Clipboard support
bevy_clipboard = ["dep:bevy_clipboard"]
system_clipboard = [
"dep:bevy_clipboard",
"bevy_clipboard/system_clipboard",
"bevy_text?/system_clipboard",
]
clipboard_image = ["dep:bevy_clipboard", "bevy_clipboard/image"]
hotpatching = ["bevy_app/hotpatching", "bevy_ecs/hotpatching"]
debug = ["bevy_utils/debug", "bevy_ecs/debug", "bevy_render?/debug"]
@@ -557,6 +566,7 @@ bevy_ui_render = { path = "../bevy_ui_render", optional = true, version = "0.19.
bevy_window = { path = "../bevy_window", optional = true, version = "0.19.0-dev", default-features = false, features = [
"bevy_reflect",
] }
bevy_clipboard = { path = "../bevy_clipboard", optional = true, version = "0.19.0-dev", default-features = false }
bevy_winit = { path = "../bevy_winit", optional = true, version = "0.19.0-dev", default-features = false }
[target.'cfg(target_os = "android")'.dependencies]
@@ -65,6 +65,8 @@ plugin_group! {
bevy_sprite:::SpritePlugin,
#[cfg(feature = "bevy_sprite_render")]
bevy_sprite_render:::SpriteRenderPlugin,
#[cfg(feature = "bevy_clipboard")]
bevy_clipboard:::ClipboardPlugin,
#[cfg(feature = "bevy_text")]
bevy_text:::TextPlugin,
#[cfg(feature = "bevy_ui")]
+2
View File
@@ -31,6 +31,8 @@ pub use bevy_audio as audio;
pub use bevy_camera as camera;
#[cfg(feature = "bevy_camera_controller")]
pub use bevy_camera_controller as camera_controller;
#[cfg(feature = "bevy_clipboard")]
pub use bevy_clipboard as clipboard;
#[cfg(feature = "bevy_color")]
pub use bevy_color as color;
#[cfg(feature = "bevy_core_pipeline")]
+4
View File
@@ -110,3 +110,7 @@ pub use crate::gltf::prelude::*;
#[doc(hidden)]
#[cfg(feature = "bevy_picking")]
pub use crate::picking::prelude::*;
#[doc(hidden)]
#[cfg(feature = "bevy_clipboard")]
pub use crate::clipboard::prelude::*;
+2
View File
@@ -11,11 +11,13 @@ keywords = ["bevy"]
[features]
default_font = []
system_font_discovery = ["parley/system"]
system_clipboard = ["bevy_clipboard/system_clipboard"]
[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.19.0-dev" }
bevy_asset = { path = "../bevy_asset", version = "0.19.0-dev" }
bevy_clipboard = { path = "../bevy_clipboard", version = "0.19.0-dev", default-features = false }
bevy_color = { path = "../bevy_color", version = "0.19.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.19.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.19.0-dev" }
+15 -1
View File
@@ -85,6 +85,18 @@ pub const DEFAULT_FONT_DATA: &[u8] = include_bytes!("FiraMono-subset.ttf");
///
/// When the `bevy_text` feature is enabled with the `bevy` crate, this
/// plugin is included by default in the `DefaultPlugins`.
///
/// ## Clipboard support
///
/// [`EditableText`] supports copy, cut, and paste via [`bevy_clipboard::Clipboard`].
/// By default, clipboard operations use an in-process fallback buffer rather than the OS clipboard.
/// To enable OS clipboard integration, activate the `system_clipboard` feature on this crate:
///
/// ```toml
/// bevy_text = { version = "...", features = ["system_clipboard"] }
/// ```
///
/// When using the top-level `bevy` crate, `bevy/system_clipboard` enables this transitively.
#[derive(Default)]
pub struct TextPlugin;
@@ -98,6 +110,9 @@ pub struct EditableTextSystems;
impl Plugin for TextPlugin {
fn build(&self, app: &mut App) {
if !app.is_plugin_added::<bevy_clipboard::ClipboardPlugin>() {
app.add_plugins(bevy_clipboard::ClipboardPlugin);
}
app.init_asset::<Font>()
.init_asset_loader::<FontLoader>()
.init_resource::<FontAtlasSet>()
@@ -107,7 +122,6 @@ impl Plugin for TextPlugin {
.init_resource::<ScaleCx>()
.init_resource::<TextIterScratch>()
.init_resource::<RemSize>()
.init_resource::<Clipboard>()
.add_systems(
PostUpdate,
(
+94 -43
View File
@@ -1,3 +1,4 @@
use bevy_clipboard::ClipboardRead;
use bevy_math::Vec2;
use bevy_reflect::Reflect;
use parley::PlainEditorDriver;
@@ -188,66 +189,45 @@ impl TextEdit {
}
}
/// Apply the `TextEdit` to the text editor driver
/// Apply the [`TextEdit`] to the text editor driver.
///
/// Note that some edits, such as [`TextEdit::Paste`], may need to be deferred across frames due to asynchronous clipboard I/O.
/// For proper handling of deferred edits, use [`EditableText::apply_pending_edits`](super::EditableText::apply_pending_edits) instead,
/// which manages the queuing and application of edits by storing them in the [`EditableText`](super::EditableText) component.
pub fn apply<'a>(
self,
driver: &'a mut PlainEditorDriver<TextBrush>,
clipboard_text: &mut String,
clipboard: &mut bevy_clipboard::Clipboard,
max_characters: Option<usize>,
char_filter: impl Fn(char) -> bool,
) {
match self {
TextEdit::Copy => {
if let Some(text) = driver.editor.selected_text() {
clipboard_text.clear();
clipboard_text.push_str(text);
if let Some(text) = driver.editor.selected_text()
&& let Err(e) = clipboard.set_text(text)
{
bevy_log::warn!("Failed to write selection to clipboard: {e:?}");
}
}
TextEdit::Cut => {
if let Some(text) = driver.editor.selected_text() {
clipboard_text.clear();
clipboard_text.push_str(text);
driver.delete();
match clipboard.set_text(text) {
Ok(()) => driver.delete(),
Err(e) => bevy_log::warn!("Failed to write selection to clipboard: {e:?}"),
}
}
}
TextEdit::Paste => {
if !clipboard_text.chars().all(char_filter) {
return;
}
if let Some(max) = max_characters {
let select_len = driver
.editor
.selected_text()
.map(str::chars)
.map(Iterator::count)
.unwrap_or(0);
if max
< driver.editor.text().chars().count() - select_len
+ clipboard_text.chars().count()
{
return;
}
}
driver.insert_or_replace_selection(clipboard_text.as_str());
// It's nice to be able to provide apply as a public method, but Paste is a little buggy.
// We'll try our best since that works on native, but we should warn users away from doing so.
bevy_log::warn_once!("Directly applying a Paste edit is not recommended, as it cannot defer asynchronous clipboard reads.
For proper handling of async clipboard operations, use `EditableText::apply_pending_edits` instead.");
let mut read = clipboard.fetch_text();
poll_and_apply_paste(&mut read, driver, max_characters, char_filter);
}
TextEdit::Insert(text) => {
if !text.chars().all(char_filter) {
return;
}
if let Some(max) = max_characters {
let select_len = driver
.editor
.selected_text()
.map(str::chars)
.map(Iterator::count)
.unwrap_or(0);
if max
< driver.editor.text().chars().count() - select_len + text.chars().count()
{
return;
}
}
driver.insert_or_replace_selection(text.as_str());
let _ = insert_filtered(driver, text.as_str(), max_characters, char_filter);
}
TextEdit::Backspace => driver.backdelete(),
TextEdit::BackspaceWord => driver.backdelete_word(),
@@ -305,3 +285,74 @@ impl TextEdit {
}
}
}
/// Reason an [`insert_filtered`] call was rejected.
///
/// The two branches matter to callers (paste warns on [`CharFilter`](Self::CharFilter) but
/// not on [`MaxLength`](Self::MaxLength)), so a bool return wouldn't suffice.
enum InsertRejection {
/// At least one character failed the user-supplied filter.
CharFilter,
/// The insertion would exceed `max_characters`.
MaxLength,
}
/// Insert (or replace the current selection with) `text`, subject to the char filter and
/// `max_characters`.
///
/// Shared by [`TextEdit::Insert`] and [`TextEdit::Paste`] paths to ensure consistent behavior.
fn insert_filtered(
driver: &mut PlainEditorDriver<TextBrush>,
text: &str,
max_characters: Option<usize>,
char_filter: impl Fn(char) -> bool,
) -> Result<(), InsertRejection> {
if !text.chars().all(char_filter) {
return Err(InsertRejection::CharFilter);
}
if let Some(max) = max_characters {
let select_len = driver
.editor
.selected_text()
.map(str::chars)
.map(Iterator::count)
.unwrap_or(0);
if max < driver.editor.text().chars().count() - select_len + text.chars().count() {
return Err(InsertRejection::MaxLength);
}
}
driver.insert_or_replace_selection(text);
Ok(())
}
/// Polls a clipboard read and, if ready, applies the resulting text as a paste.
///
/// Returns `true` when the read has resolved (applied, filter-rejected, or errored)
/// and the caller should move on.
/// Returns `false` when the read is still pending
/// and the caller should hold onto the [`ClipboardRead`] to poll again on a later frame.
pub(crate) fn poll_and_apply_paste(
read: &mut ClipboardRead,
driver: &mut PlainEditorDriver<TextBrush>,
max_characters: Option<usize>,
char_filter: impl Fn(char) -> bool,
) -> bool {
match read.poll_result() {
Some(Ok(text)) => {
if matches!(
insert_filtered(driver, &text, max_characters, char_filter),
Err(InsertRejection::CharFilter)
) {
bevy_log::debug!(
"Paste rejected: clipboard contents contained characters not allowed by the char filter."
);
}
true
}
Some(Err(e)) => {
bevy_log::warn!("Failed to read clipboard for paste: {e:?}");
true
}
None => false,
}
}
+58 -15
View File
@@ -13,6 +13,7 @@
//! - Basic keyboard-driven cursor movement (arrow keys, home/end keys)
//! - Home / End key support for moving the cursor to the start / end of the text
//! - Backspace and delete operations
//! - Clipboard operations (copy, cut, paste) — requires the `system_clipboard` feature for OS clipboard integration
//! - Click to place cursor
//! - Cursor blinking
//! - Newline support for multi-line input
@@ -52,7 +53,6 @@
//! However, the following features are planned but currently not implemented:
//!
//! - Placeholder text (displayed when the input is empty)
//! - Clipboard operations (copy, cut, paste)
//! - Undo/redo functionality
//! - Text validation (e.g., email format, numeric input, max length)
//! - Password-style character masking
@@ -69,21 +69,16 @@
// and `bevy_ui`, such as text layout and font management.
use crate::{
text_edit::TextEdit, FontCx, FontHinting, LayoutCx, LineHeight, TextBrush, TextColor, TextFont,
TextLayout,
text_edit::{poll_and_apply_paste, TextEdit},
FontCx, FontHinting, LayoutCx, LineHeight, TextBrush, TextColor, TextFont, TextLayout,
};
use alloc::sync::Arc;
use bevy_clipboard::ClipboardRead;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::prelude::*;
use core::time::Duration;
use parley::{FontContext, LayoutContext, PlainEditor, SplitString};
/// Resource containing the current contents of the clipboard.
///
/// Placeholder for a proper clipboard implementation with support for the OS clipboard and non-text content.
#[derive(Resource, Default)]
pub struct Clipboard(pub String);
/// A plain-text text input field.
///
/// Please see this module docs for more details on usage and functionality.
@@ -120,6 +115,15 @@ pub struct EditableText {
///
/// These edits are processed in first-in, first-out order.
pub pending_edits: Vec<TextEdit>,
/// A paste operation that is awaiting clipboard I/O.
///
/// On platforms where system clipboard reads are asynchronous (currently wasm32), a
/// [`TextEdit::Paste`] may not resolve in the same frame it was queued.
///
/// While this field is `Some`, [`apply_pending_edits`](Self::apply_pending_edits) waits for this to resolve,
/// rather than draining further edits, so that everything after the paste stays correctly ordered *behind* it.
// TODO: this may cause unexpected stalls if the clipboard read takes too long. We may want to add a timeout.
pub pending_paste: Option<ClipboardRead>,
/// Cursor width, relative to font size
pub cursor_width: f32,
/// Cursor blink period in seconds.
@@ -144,6 +148,7 @@ impl Default for EditableText {
// Defaults selected to match `Text::default()`
editor: PlainEditor::new(100.),
pending_edits: Vec::new(),
pending_paste: None,
cursor_width: 0.2,
cursor_blink_period: Duration::from_secs(1),
max_characters: None,
@@ -190,31 +195,67 @@ impl EditableText {
/// Applies all [`TextEdit`]s in `pending_edits` immediately, updating the [`PlainEditor`] text / cursor state accordingly.
///
/// [`FontContext`] should be gathered from the [`FontCx`] resource, and [`LayoutContext`] should be gathered from the [`LayoutCx`] resource.
///
/// On platforms with async clipboard reads (wasm32), a [`TextEdit::Paste`] whose
/// contents aren't yet available acts as a barrier: this call parks the in-flight
/// read on [`EditableText`] and leaves the remaining edits queued in order. Each
/// subsequent frame re-polls the read, and processing resumes once it resolves.
/// On native targets clipboard reads are synchronous, so this barrier collapses.
pub fn apply_pending_edits(
&mut self,
font_context: &mut FontContext,
layout_context: &mut LayoutContext<TextBrush>,
clipboard_text: &mut String,
clipboard: &mut bevy_clipboard::Clipboard,
char_filter: impl Fn(char) -> bool,
) {
let Self {
editor,
pending_edits,
pending_paste,
max_characters,
..
} = self;
let mut driver = editor.driver(font_context, layout_context);
for edit in pending_edits.drain(..) {
edit.apply(&mut driver, clipboard_text, *max_characters, &char_filter);
// First: resolve any paste carried over from a previous frame. If it's still
// pending, hold the remaining edits (untouched in `pending_edits`) for next frame
// so ordering relative to the paste is preserved.
if let Some(mut read) = pending_paste.take()
&& !poll_and_apply_paste(&mut read, &mut driver, *max_characters, &char_filter)
{
*pending_paste = Some(read);
return;
}
// Drain edits one at a time. A paste that resolves synchronously (always the case
// on native) applies immediately, but a still-pending paste stashes its `ClipboardRead` and
// requeues the *remaining* edits, so this loop continually requeues the pending paste until it resolves.
let mut edits = core::mem::take(pending_edits).into_iter();
while let Some(edit) = edits.next() {
match edit {
TextEdit::Paste => {
let mut read = clipboard.fetch_text();
if !poll_and_apply_paste(&mut read, &mut driver, *max_characters, &char_filter)
{
*pending_paste = Some(read);
pending_edits.extend(edits);
return;
}
}
other => other.apply(&mut driver, clipboard, *max_characters, &char_filter),
}
}
}
/// Clears the input's text buffer and any pending edits.
///
/// Also drops any in-flight paste. The underlying clipboard read task
/// will still complete, but its result is discarded.
pub fn clear(&mut self) {
self.editor.set_text("");
self.pending_edits.clear();
self.pending_paste = None;
}
/// Is the IME currently composing text for this input?
@@ -255,15 +296,17 @@ pub fn apply_text_edits(
)>,
mut font_context: ResMut<FontCx>,
mut layout_context: ResMut<LayoutCx>,
mut clipboard_text: ResMut<Clipboard>,
mut clipboard: ResMut<bevy_clipboard::Clipboard>,
mut commands: Commands,
) {
for (entity, mut editable_text, filter, generation) in query.iter_mut() {
if !editable_text.pending_edits.is_empty() {
// `pending_paste` can hold a cross-frame paste even when no new edits are queued,
// so check for either before doing work.
if !editable_text.pending_edits.is_empty() || editable_text.pending_paste.is_some() {
editable_text.apply_pending_edits(
&mut font_context.0,
&mut layout_context.0,
&mut clipboard_text.0,
&mut clipboard,
match filter {
Some(EditableTextFilter(Some(filter))) => filter.as_ref(),
_ => &|_| true,
+1
View File
@@ -29,6 +29,7 @@ allow = [
"Unlicense",
"Zlib",
"Unicode-3.0",
"BSL-1.0",
]
exceptions = [
+4 -1
View File
@@ -42,7 +42,7 @@ collections to build your own "profile" equivalent, without needing to manually
|scene|Features used to compose Bevy scenes. **Feature set:** `bevy_world_serialization`, `bevy_scene`.|
|picking|Enables picking with all backends. **Feature set:** `bevy_picking`, `mesh_picking`, `sprite_picking`, `ui_picking`.|
|default_app|The core pieces that most apps need. This serves as a baseline feature set for other higher level feature collections (such as "2d" and "3d"). It is also useful as a baseline feature set for scenarios like headless apps that require no rendering (ex: command line tools, servers, etc). **Feature set:** `async_executor`, `bevy_asset`, `bevy_input_focus`, `bevy_log`, `bevy_state`, `bevy_window`, `custom_cursor`, `reflect_auto_register`.|
|default_platform|These are platform support features, such as OS support/features, windowing and input backends, etc. **Feature set:** `std`, `android-game-activity`, `bevy_gilrs`, `bevy_winit`, `default_font`, `multi_threaded`, `webgl2`, `x11`, `wayland`, `sysinfo_plugin`.|
|default_platform|These are platform support features, such as OS support/features, windowing and input backends, etc. **Feature set:** `std`, `android-game-activity`, `bevy_gilrs`, `bevy_winit`, `bevy_clipboard`, `default_font`, `multi_threaded`, `webgl2`, `x11`, `wayland`, `sysinfo_plugin`.|
|common_api|Default scene definition features. Note that this does not include an actual renderer, such as bevy_render (Bevy's default render backend). **Feature set:** `bevy_animation`, `bevy_camera`, `bevy_color`, `bevy_gizmos`, `bevy_image`, `bevy_mesh`, `bevy_shader`, `bevy_material`, `bevy_text`, `hdr`, `png`.|
|2d_api|Features used to build 2D Bevy apps (does not include a render backend). You generally don't need to worry about this unless you are using a custom renderer. **Feature set:** `common_api`, `bevy_sprite`.|
|2d_bevy_render|Bevy's built-in 2D renderer, built on top of `bevy_render`. **Feature set:** `2d_api`, `bevy_render`, `bevy_core_pipeline`, `bevy_post_process`, `bevy_sprite_render`, `bevy_gizmos_render`.|
@@ -73,6 +73,7 @@ This is the complete `bevy` cargo feature list, without "profiles" or "collectio
|bevy_camera|Provides camera and visibility types, as well as culling primitives.|
|bevy_camera_controller|Provides a collection of prebuilt camera controllers|
|bevy_ci_testing|Enable systems that allow for automated testing on CI|
|bevy_clipboard|Clipboard resource and management. See `system_clipboard` for OS-integrated clipboard support.|
|bevy_color|Provides shared color types and operations|
|bevy_core_pipeline|Provides cameras and other basic render pipeline features|
|bevy_debug_stepping|Enable stepping-based debugging of Bevy systems|
@@ -110,6 +111,7 @@ This is the complete `bevy` cargo feature list, without "profiles" or "collectio
|bevy_world_serialization|Provides ECS serialization functionality|
|bluenoise_texture|Include spatio-temporal blue noise KTX2 file used by generated environment maps, Solari and atmosphere|
|bmp|BMP image format support|
|clipboard_image|Enables image copy/paste via the system clipboard. Not supported on WASM.|
|compressed_image_saver|Enables compressed KTX2 UASTC texture output on the asset processor|
|critical-section|`critical-section` provides the building blocks for synchronization primitives on all platforms, including `no_std`.|
|custom_cursor|Enable winit custom cursor support|
@@ -183,6 +185,7 @@ This is the complete `bevy` cargo feature list, without "profiles" or "collectio
|symphonia-vorbis|OGG/VORBIS audio format support (through `symphonia`)|
|symphonia-wav|WAV audio format support (through `symphonia`)|
|sysinfo_plugin|Enables system information diagnostic plugin|
|system_clipboard|Enables system-level clipboard support.|
|system_font_discovery|Allows for discovery of preloaded system fonts|
|tga|TGA image format support|
|tiff|TIFF image format support|
+17
View File
@@ -4,6 +4,23 @@
//! In most cases, this should be combined with other entities to create a compound widget
//! that includes e.g. a background, border, and text label.
//!
//! Note that while Bevy does offer clipboard support, access to the system clipboard is gated
//! behind an off-by-default feature (`system_clipboard` on `bevy_clipboard`).
//! When this is disabled, clipboard operations (copy, cut, paste) will operate on a simple in-memory buffer
//! that is not shared with the operating system.
//! This means that, unless you enable this feature,
//! you will not be able to copy text from your application and paste it into another application, or vice versa.
//!
//! Most applications that use text input will want to enable system clipboard support to meet user expectations for copy/paste behavior.
//! It is off by default to avoid forcing clipboard permissions on applications that do not need it but wish to use Bevy's UI solution for other widgets,
//! and to avoid including the `arboard` dependency on platforms where it is not supported or where clipboard access is not desired.
//! While desktop platforms generally support clipboard access without special permissions, some platforms (notably web and mobile)
//! may require additional permissions or user gestures to allow clipboard access;
//! this approach allows developers to opt in to full clipboard support only when they genuinely need it.
//!
//! To test this example using the system feature, run `cargo run --example text_input --features="system_clipboard"`.
//! To enable this feature in your own project, add the `system_clipboard` feature to your list of enabled features for `bevy` in your `Cargo.toml`.
//!
//! See the module documentation for [`editable_text`](bevy::ui_widgets::editable_text) for more details.
use bevy::color::palettes::css::{DARK_GREY, YELLOW};
use bevy::input_focus::AutoFocus;