Skip to main content

bevy_text/
editing.rs

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.
74
75use crate::{
76    text_edit::{poll_and_apply_paste, TextEdit},
77    FontCx, 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};
85
86/// 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.
117    pub 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.
121    pub 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.
130    pub pending_paste: Option<ClipboardRead>,
131    /// Cursor width, relative to font size
132    pub cursor_width: f32,
133    /// Cursor blink period in seconds.
134    pub 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`.
139    pub max_characters: Option<usize>,
140    /// Sets the input’s height in number of visible lines.
141    pub 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.
144    pub visible_width: Option<f32>,
145    /// Allow new lines
146    pub allow_newlines: bool,
147}
148
149impl Default for EditableText {
150    fn default() -> Self {
151        Self {
152            // Defaults selected to match `Text::default()`
153            editor: 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}
165
166impl EditableText {
167    /// Creates a new `EditableText` with its buffer already containing some initial text and
168    /// its cursor positioned at the end.
169    pub fn new(initial_text: impl AsRef<str>) -> Self {
170        let mut editable_text = Self::default();
171        editable_text.editor.set_text(initial_text.as_ref());
172        editable_text.queue_edit(TextEdit::TextEnd(false));
173        editable_text
174    }
175
176    /// Access the internal [`PlainEditor`].
177    pub fn editor(&self) -> &PlainEditor<TextBrush> {
178        &self.editor
179    }
180
181    /// Mutably access the internal [`PlainEditor`].
182    ///
183    pub fn editor_mut(&mut self) -> &mut PlainEditor<TextBrush> {
184        &mut self.editor
185    }
186
187    /// Get the current text input as a [`SplitString`].
188    ///
189    /// A [`SplitString`] can be converted into a [`String`] using `to_string` if needed.
190    pub fn value(&self) -> SplitString<'_> {
191        self.editor.text()
192    }
193
194    /// Queue a [`TextEdit`] action to be applied later by the [`apply_text_edits`] system.
195    pub fn queue_edit(&mut self, edit: TextEdit) {
196        self.pending_edits.push(edit);
197    }
198
199    /// 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.
208    pub 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    ) {
215        let Self {
216            editor,
217            pending_edits,
218            pending_paste,
219            max_characters,
220            ..
221        } = self;
222
223        let mut driver = editor.driver(font_context, layout_context);
224
225        // 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.
228        if 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);
232            return;
233        }
234
235        // 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.
238        let mut edits = core::mem::take(pending_edits).into_iter();
239        while let Some(edit) = edits.next() {
240            match edit {
241                TextEdit::Paste => {
242                    let mut read = clipboard.fetch_text();
243                    if !poll_and_apply_paste(&mut read, &mut driver, *max_characters, &char_filter)
244                    {
245                        *pending_paste = Some(read);
246                        pending_edits.extend(edits);
247                        return;
248                    }
249                }
250                other => other.apply(&mut driver, clipboard, *max_characters, &char_filter),
251            }
252        }
253    }
254
255    /// 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.
259    pub fn clear(&mut self) {
260        self.editor.set_text("");
261        self.pending_edits.clear();
262        self.pending_paste = None;
263    }
264
265    /// 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.
269    pub fn is_composing(&self) -> bool {
270        self.editor.is_composing()
271    }
272}
273
274/// 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);
279
280/// 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>>);
285
286impl EditableTextFilter {
287    /// Create a new `EditableTextFilter` from the given filter function.
288    pub fn new(filter: impl Fn(char) -> bool + Send + Sync + 'static) -> Self {
289        Self(Some(Arc::new(filter)))
290    }
291}
292
293/// Applies pending text edit actions to all [`EditableText`] widgets.
294pub fn apply_text_edits(
295    mut query: Query<(
296        Entity,
297        &mut EditableText,
298        Option<&EditableTextFilter>,
299        &EditableTextGeneration,
300    )>,
301    mut font_context: ResMut<FontCx>,
302    mut layout_context: ResMut<LayoutCx>,
303    mut clipboard: ResMut<bevy_clipboard::Clipboard>,
304    mut commands: Commands,
305) {
306    for (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.
309        if !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,
314                match filter {
315                    Some(EditableTextFilter(Some(filter))) => filter.as_ref(),
316                    _ => &|_| true,
317                },
318            );
319        }
320
321        if **generation != editable_text.editor.generation() {
322            commands.trigger(TextEditChange { entity });
323        }
324    }
325}
326
327/// 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}