//! A simple text input widget for Bevy UI.1//!2//! The [`EditableText`] widget is an undecorated rectangular text input field,3//! which allows users to input and edit text within a Bevy UI application.4//! Every [`EditableText`] component is also a [`Node`](https://docs.rs/bevy/latest/bevy/prelude/struct.Node.html) in the Bevy UI hierarchy,5//! allowing you to position and size it using standard Bevy UI layout techniques.6//! You can think of it as the editable equivalent of [`Text`](https://docs.rs/bevy/latest/bevy/prelude/struct.Text.html),7//! and components such as [`TextFont`] and [`TextColor`] can be used to style it.8//!9//! [`EditableText`] supports the following functionality:10//!11//! - Text entry12//! - Keyboard-driven cursor movement: arrow keys, Home/End, and word-level shortcuts (Ctrl/Alt+arrow)13//! - Shift+arrow and Shift+word-arrow to extend the selection by character or word14//! - Backspace and delete operations, both for single characters and words15//! - Clipboard operations (copy, cut, paste) — requires the `system_clipboard` feature for OS clipboard integration16//! - Click to place cursor; click and drag to extend the selection17//! - Multi-click: double-click to select a word, triple-click to select a line18//! - Optional select-all on focus via the `SelectAllOnFocus` component19//! - Per-character input filtering via the [`EditableTextFilter`] component20//! - Max character limits via [`EditableText::max_characters`]21//! - Cursor blinking22//! - Newline support for multi-line input23//! - Soft-wrapping of long lines24//! - Vertical scrolling for multi-line input25//! - Horizontal scrolling for long lines26//! - Input Method Editor (IME) support for complex scripts (Japanese, Chinese, Korean, etc.)27//! - Bidirectional text support (e.g., mixing left-to-right and right-to-left scripts)28//! - Input consumption (preventing other systems from receiving keyboard input events when the text input is focused)29//!30//! You might use this widget as the basis for text input fields in forms, chat boxes, for naming characters,31//! or any other scenario where you want to extract an unformatted text string from the user.32//!33//! Reusable widgets that build on top of this basic text input field (as might be found in Bevy's Feathers UI framework),34//! will typically combine this widget with additional UI elements such as borders, backgrounds, and labels,35//! creating a multi-entity widget that matches the semantics and visual appearance required by the application.36//!37//! ## Handling user input38//!39//! User input is handled via a plugin in `bevy_ui_widgets`:40//! [`bevy_text`](crate) is not aware of input events directly.41//!42//! With the correct plugin enabled, when an [`EditableText`] entity is focused,43//! keyboard input events are captured and processed into [`TextEdit`] actions.44//!45//! ## Limitations46//!47//! The formatting of the text is uniform throughout the entire input field.48//! As a result, rich text-editing is out-of-scope:49//! this widget is not intended to form the basis for a full-featured text editor.50//!51//! Similarly, this widget is "headless": it has no built-in styling, and is intended to be used52//! with a themed UI framework of your choice (e.g. Feathers). This means that no text boxes, borders, or other53//! visual elements are provided by default, and must be added separately using Bevy UI entities / components,54//! and any reactive styling (e.g., focus/hover states) must also be implemented separately.55//!56//! However, the following features are planned but currently not implemented:57//!58//! - Placeholder text (displayed when the input is empty)59//! - Undo/redo functionality60//! - Text validation (e.g., email format, numeric input)61//! - Password-style character masking62//! - Mobile pop-up keyboard support63//! - Overwrite mode (typically toggled by the `Insert` key)64//! - AccessKit integration for screen readers and other assistive technologies65//! - World-space text input66//! - Text form submission handling67//!68//! If you require any of these features, please consider contributing it to the crate,69//! one feature at a time!70// Note: this logic is in `bevy_text`, rather than higher up in `bevy_ui` or `bevy_ui_widgets`,71// because doing so allows us to process `EditableText` in the various systems provided by `bevy_text`72// and `bevy_ui`, such as text layout and font management.7374use crate::{75text_edit::{poll_and_apply_paste, TextEdit},76FontCx, FontHinting, LayoutCx, LineHeight, TextBrush, TextColor, TextFont, TextLayout,77};78use alloc::sync::Arc;79use bevy_clipboard::ClipboardRead;80use bevy_derive::{Deref, DerefMut};81use bevy_ecs::prelude::*;82use core::time::Duration;83use parley::{FontContext, LayoutContext, PlainEditor, SplitString};8485/// A plain-text text input field.86///87/// Please see this module docs for more details on usage and functionality.88///89/// Note that text editing operations are trickier than they might first appear,90/// due to the complexities of Unicode text handling.91///92/// As a result, we store an internal [`PlainEditor`] instance,93/// which manages both the text content and the cursor position,94/// and provides methods for applying text edits and cursor movements correctly95/// according to Unicode rules.96#[derive(Component, Clone)]97#[require(98TextLayout,99TextFont,100TextColor,101LineHeight,102FontHinting,103EditableTextGeneration104)]105pub struct EditableText {106/// A [`parley::PlainEditor`], tracking both the text content and cursor position.107///108/// This serves as an analogue to [`ComputedTextBlock`](crate::ComputedTextBlock) for editable text.109///110/// In most cases, you should queue text edits via the [`EditableText::queue_edit`] method instead of directly manipulating the editor,111/// and then allow the [`apply_text_edits`] system to apply the edits at the appropriate time in the update cycle.112///113/// Note that many more complex editing operations require working with [`PlainEditor::driver`].114/// These operations should generally be batched together to avoid redundant layout work.115// The B: Brush generic here must match the brush used by `ComputedTextBlock` to ensure that the font system is compatible.116pub editor: PlainEditor<TextBrush>,117/// Text edit actions that have been requested but not yet applied.118///119/// These edits are processed in first-in, first-out order.120pub pending_edits: Vec<TextEdit>,121/// A paste operation that is awaiting clipboard I/O.122///123/// On platforms where system clipboard reads are asynchronous (currently wasm32), a124/// [`TextEdit::Paste`] may not resolve in the same frame it was queued.125///126/// While this field is `Some`, [`apply_pending_edits`](Self::apply_pending_edits) waits for this to resolve,127/// rather than draining further edits, so that everything after the paste stays correctly ordered *behind* it.128// TODO: this may cause unexpected stalls if the clipboard read takes too long. We may want to add a timeout.129pub pending_paste: Option<ClipboardRead>,130/// Cursor width, relative to font size131pub cursor_width: f32,132/// Cursor blink period in seconds.133pub cursor_blink_period: Duration,134/// Maximum number of characters the text input can contain.135///136/// Edits which would cause the length to exceed the maximum are ignored.137/// Does not stop setting a string longer than the maximum using `set_text`.138pub max_characters: Option<usize>,139/// Sets the input’s height in number of visible lines.140pub visible_lines: Option<f32>,141/// Sets the input's width in number of visible glyphs.142/// For proportional fonts the final size is the given value times the "0" advance width.143pub visible_width: Option<f32>,144/// Allow new lines145pub allow_newlines: bool,146}147148impl Default for EditableText {149fn default() -> Self {150Self {151// Defaults selected to match `Text::default()`152editor: PlainEditor::new(100.),153pending_edits: Vec::new(),154pending_paste: None,155cursor_width: 0.2,156cursor_blink_period: Duration::from_secs(1),157max_characters: None,158visible_lines: Some(1.),159visible_width: None,160allow_newlines: false,161}162}163}164165impl EditableText {166/// Creates a new `EditableText` with its buffer already containing some initial text and167/// its cursor positioned at the end.168pub fn new(initial_text: impl AsRef<str>) -> Self {169let mut editable_text = Self::default();170editable_text.editor.set_text(initial_text.as_ref());171editable_text.queue_edit(TextEdit::TextEnd(false));172editable_text173}174175/// Access the internal [`PlainEditor`].176pub fn editor(&self) -> &PlainEditor<TextBrush> {177&self.editor178}179180/// Mutably access the internal [`PlainEditor`].181///182pub fn editor_mut(&mut self) -> &mut PlainEditor<TextBrush> {183&mut self.editor184}185186/// Get the current text input as a [`SplitString`].187///188/// A [`SplitString`] can be converted into a [`String`] using `to_string` if needed.189pub fn value(&self) -> SplitString<'_> {190self.editor.text()191}192193/// Queue a [`TextEdit`] action to be applied later by the [`apply_text_edits`] system.194pub fn queue_edit(&mut self, edit: TextEdit) {195self.pending_edits.push(edit);196}197198/// Applies all [`TextEdit`]s in `pending_edits` immediately, updating the [`PlainEditor`] text / cursor state accordingly.199///200/// [`FontContext`] should be gathered from the [`FontCx`] resource, and [`LayoutContext`] should be gathered from the [`LayoutCx`] resource.201///202/// On platforms with async clipboard reads (wasm32), a [`TextEdit::Paste`] whose203/// contents aren't yet available acts as a barrier: this call parks the in-flight204/// read on [`EditableText`] and leaves the remaining edits queued in order. Each205/// subsequent frame re-polls the read, and processing resumes once it resolves.206/// On native targets clipboard reads are synchronous, so this barrier collapses.207pub fn apply_pending_edits(208&mut self,209font_context: &mut FontContext,210layout_context: &mut LayoutContext<TextBrush>,211clipboard: &mut bevy_clipboard::Clipboard,212char_filter: impl Fn(char) -> bool,213) {214let Self {215editor,216pending_edits,217pending_paste,218max_characters,219..220} = self;221222let mut driver = editor.driver(font_context, layout_context);223224// First: resolve any paste carried over from a previous frame. If it's still225// pending, hold the remaining edits (untouched in `pending_edits`) for next frame226// so ordering relative to the paste is preserved.227if let Some(mut read) = pending_paste.take()228&& !poll_and_apply_paste(&mut read, &mut driver, *max_characters, &char_filter)229{230*pending_paste = Some(read);231return;232}233234// Drain edits one at a time. A paste that resolves synchronously (always the case235// on native) applies immediately, but a still-pending paste stashes its `ClipboardRead` and236// requeues the *remaining* edits, so this loop continually requeues the pending paste until it resolves.237let mut edits = core::mem::take(pending_edits).into_iter();238while let Some(edit) = edits.next() {239match edit {240TextEdit::Paste => {241let mut read = clipboard.fetch_text();242if !poll_and_apply_paste(&mut read, &mut driver, *max_characters, &char_filter)243{244*pending_paste = Some(read);245pending_edits.extend(edits);246return;247}248}249other => other.apply(&mut driver, clipboard, *max_characters, &char_filter),250}251}252}253254/// Clears the input's text buffer and any pending edits.255///256/// Also drops any in-flight paste. The underlying clipboard read task257/// will still complete, but its result is discarded.258pub fn clear(&mut self) {259self.editor.set_text("");260self.pending_edits.clear();261self.pending_paste = None;262}263264/// Is the IME currently composing text for this input?265///266/// Some behavior (e.g. "submit on Enter") may want to be suppressed while the IME is active267/// to avoid interrupting the user's composition.268pub fn is_composing(&self) -> bool {269self.editor.is_composing()270}271}272273/// Wrapper around a `parley::Generation`. Used to track when `TextLayoutInfo` is stale and needs reupdating.274/// The initial `Generation` of the `PlainEditor` is not equal to the default `Generation` value, so the275/// `TextLayoutInfo` will always be given an initial update.276#[derive(Component, PartialEq, Eq, Default, Clone, Copy, Deref, DerefMut)]277pub struct EditableTextGeneration(parley::Generation);278279/// Sets a per-character filter for this text input. Insert and paste edits are ignored if the filter rejects any character.280///281/// The filter does not apply to characters already within the `EditableText`'s text buffer.282#[derive(Component, Clone, Default)]283pub struct EditableTextFilter(Option<Arc<dyn Fn(char) -> bool + Send + Sync + 'static>>);284285impl EditableTextFilter {286/// Create a new `EditableTextFilter` from the given filter function.287pub fn new(filter: impl Fn(char) -> bool + Send + Sync + 'static) -> Self {288Self(Some(Arc::new(filter)))289}290}291292/// Applies pending text edit actions to all [`EditableText`] widgets.293pub fn apply_text_edits(294mut query: Query<(295Entity,296&mut EditableText,297Option<&EditableTextFilter>,298&EditableTextGeneration,299)>,300mut font_context: ResMut<FontCx>,301mut layout_context: ResMut<LayoutCx>,302mut clipboard: ResMut<bevy_clipboard::Clipboard>,303mut commands: Commands,304) {305for (entity, mut editable_text, filter, generation) in query.iter_mut() {306// `pending_paste` can hold a cross-frame paste even when no new edits are queued,307// so check for either before doing work.308if !editable_text.pending_edits.is_empty() || editable_text.pending_paste.is_some() {309editable_text.apply_pending_edits(310&mut font_context.0,311&mut layout_context.0,312&mut clipboard,313match filter {314Some(EditableTextFilter(Some(filter))) => filter.as_ref(),315_ => &|_| true,316},317);318}319320if **generation != editable_text.editor.generation() {321commands.trigger(TextEditChange { entity });322}323}324}325326/// Triggered after applying all pending [`TextEdit`]s to the [`EditableText`] by [`apply_text_edits`].327///328/// As [`TextEdit`] includes cursor motions, this will be emitted even if [`EditableText::value`] is unchanged.329#[derive(EntityEvent)]330pub struct TextEditChange {331entity: Entity,332}333334335