diff --git a/Cargo.toml b/Cargo.toml index a7d1c05c11f2a..66be632d7ad42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/bevy_clipboard/Cargo.toml b/crates/bevy_clipboard/Cargo.toml new file mode 100644 index 0000000000000..bd066241b305a --- /dev/null +++ b/crates/bevy_clipboard/Cargo.toml @@ -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 diff --git a/crates/bevy_clipboard/LICENSE-APACHE b/crates/bevy_clipboard/LICENSE-APACHE new file mode 100644 index 0000000000000..d9a10c0d8e868 --- /dev/null +++ b/crates/bevy_clipboard/LICENSE-APACHE @@ -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 diff --git a/crates/bevy_clipboard/LICENSE-MIT b/crates/bevy_clipboard/LICENSE-MIT new file mode 100644 index 0000000000000..9cf106272ac3b --- /dev/null +++ b/crates/bevy_clipboard/LICENSE-MIT @@ -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. diff --git a/crates/bevy_clipboard/README.md b/crates/bevy_clipboard/README.md new file mode 100644 index 0000000000000..d96116fb9d4d8 --- /dev/null +++ b/crates/bevy_clipboard/README.md @@ -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) diff --git a/crates/bevy_clipboard/src/lib.rs b/crates/bevy_clipboard/src/lib.rs new file mode 100644 index 0000000000000..41b9bc65e77b0 --- /dev/null +++ b/crates/bevy_clipboard/src/lib.rs @@ -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::(); + } +} + +/// 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 { + /// The clipboard contents are ready to be accessed. + Ready(Result), + /// 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>>>), + /// The clipboard contents have already been taken by a previous call to [`ClipboardRead::poll_result`]. + Taken, +} + +impl ClipboardRead { + /// 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> { + 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 { + 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, 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, + // 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 { + 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>>(&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 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(), + }, + } + } +} diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 0745855658859..36943d996b107 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -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] diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index 54aa118557b79..26df25b475326 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -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")] diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index c9ecefbc6889c..bc278b9b4f125 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -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")] diff --git a/crates/bevy_internal/src/prelude.rs b/crates/bevy_internal/src/prelude.rs index 581643b5f1c72..6b03a6adf9057 100644 --- a/crates/bevy_internal/src/prelude.rs +++ b/crates/bevy_internal/src/prelude.rs @@ -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::*; diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index 0cab6fdf5dc5f..010bf414b34db 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -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" } diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 34a2377de461d..159f887355541 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -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::() { + app.add_plugins(bevy_clipboard::ClipboardPlugin); + } app.init_asset::() .init_asset_loader::() .init_resource::() @@ -107,7 +122,6 @@ impl Plugin for TextPlugin { .init_resource::() .init_resource::() .init_resource::() - .init_resource::() .add_systems( PostUpdate, ( diff --git a/crates/bevy_text/src/text_edit.rs b/crates/bevy_text/src/text_edit.rs index 26b30a05f71df..2cbb6dc2c9fea 100644 --- a/crates/bevy_text/src/text_edit.rs +++ b/crates/bevy_text/src/text_edit.rs @@ -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, - clipboard_text: &mut String, + clipboard: &mut bevy_clipboard::Clipboard, max_characters: Option, 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, + text: &str, + max_characters: Option, + 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, + max_characters: Option, + 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, + } +} diff --git a/crates/bevy_text/src/text_editable.rs b/crates/bevy_text/src/text_editable.rs index 8416c0ae6272d..a017207d37792 100644 --- a/crates/bevy_text/src/text_editable.rs +++ b/crates/bevy_text/src/text_editable.rs @@ -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, + /// 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, /// 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, - 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, mut layout_context: ResMut, - mut clipboard_text: ResMut, + mut clipboard: ResMut, 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, diff --git a/deny.toml b/deny.toml index 41a1a7bbdd81a..e380ba63b1463 100644 --- a/deny.toml +++ b/deny.toml @@ -29,6 +29,7 @@ allow = [ "Unlicense", "Zlib", "Unicode-3.0", + "BSL-1.0", ] exceptions = [ diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 515b207791325..724db9e1f7de0 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -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| diff --git a/examples/ui/text/text_input.rs b/examples/ui/text/text_input.rs index 86ca44da374a0..5aca9e0b42b66 100644 --- a/examples/ui/text/text_input.rs +++ b/examples/ui/text/text_input.rs @@ -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;