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