Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_text/src/editing.rs
30635 views
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
75
use crate::{
76
text_edit::{poll_and_apply_paste, TextEdit},
77
FontCx, FontHinting, LayoutCx, LineHeight, TextBrush, TextColor, TextFont, TextLayout,
78
};
79
use alloc::sync::Arc;
80
use bevy_clipboard::ClipboardRead;
81
use bevy_derive::{Deref, DerefMut};
82
use bevy_ecs::prelude::*;
83
use core::time::Duration;
84
use 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(Component, Clone)]
98
#[require(
99
TextLayout,
100
TextFont,
101
TextColor,
102
LineHeight,
103
FontHinting,
104
EditableTextGeneration
105
)]
106
pub 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
149
impl 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
166
impl 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(Component, PartialEq, Eq, Default, Clone, Copy, Deref, DerefMut)]
278
pub 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(Component, Clone, Default)]
284
pub struct EditableTextFilter(Option<Arc<dyn Fn(char) -> bool + Send + Sync + 'static>>);
285
286
impl 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.
294
pub 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.0,
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(EntityEvent)]
331
pub struct TextEditChange {
332
entity: Entity,
333
}
334
335