Skip to content

TextInput#24929

Open
viridia wants to merge 10 commits into
bevyengine:mainfrom
viridia:text_input
Open

TextInput#24929
viridia wants to merge 10 commits into
bevyengine:mainfrom
viridia:text_input

Conversation

@viridia

@viridia viridia commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

This PR splits the functionality of the EditableText component into two separate components: EditableText (which lives in bevy_text) and TextInput (which lives in bevy_ui_widgets).

See release notes for more details.

@viridia viridia requested a review from ickshonpe July 9, 2026 16:20
Comment on lines +55 to +63
pub enum TextInput {
/// Text input functions normally
#[default]
Editable,
/// Cursor movement, selection, and copy to clipboard is still enabled, but no mutations are allowed
ReadOnly,
/// Display only, all interactions disabled - this is used by number input widget when dragging.
DisplayOnly,
}

@ickshonpe ickshonpe Jul 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the variant names either. Not sure I have any better suggestions though, probably not worth fussing about.

#[require(EditableText)]
#[require(AccessibilityNode(accesskit::Node::new(Role::TextInput)))]
#[reflect(Component)]
pub enum TextInput {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be called something like TextInputMode or TextInputInteractionState. The name TextInput suggests that this is the primary text input component. But it's not part of the user-facing API, it's used for tracking the input's interaction state internally.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK so here's the problem I have: this component does more than just control the readonly mode - it also pulls in the AccessibilityNode required component. If I were to rename this to ReadWriteMode as we discussed, then this required component doesn't make sense, and I'd have to move it to EditableText. But I'm not sure if bevy_text should be pulling in accessibility - it depends on whether you consider EditableText to be a widget, or just a buffer.

Worse, if this truly is just a mode switch and not a "widget" then it probably should be optional, which means I definitely can't put the accessibility node required component here.


let mut queue_edit = |edit| {
if keyboard_input.input.state.is_pressed() {
let mut queue_edit = |edit, readonly| {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The readonly param feels a bit error prone. It'd be clearer with two functions queue_edit and queue_read_only or something but it'd need to use a RefCell or macro.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could have a separate function that returns true if a TextEdit is destructive that we can filter by in queue_edit?

@ickshonpe ickshonpe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems fine, agree with the overall direction. I think maybe instead of the bool param on queue_edit, we could implement an is_destructive method for TextEdit that returns true for destructive TextEdit s.

Also I haven't thought it through properly but possibly there could be problems with IME if the widget is changed to readonly during composition? I'm not sure we should worry too much though.

Comment on lines +104 to +107
if *text_input == TextInput::Editable
|| (readonly && *text_input == TextInput::ReadOnly)
&& keyboard_input.input.state.is_pressed()
{

@ickshonpe ickshonpe Jul 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't check is_pressed in TextInput::Editable mode. It doesn't double enter characters because the

if let Some(text) = &keyboard_input.input.text

guard fails, but everything else like cursor and delete inputs are applied on release as well as press now.

Suggested change
if *text_input == TextInput::Editable
|| (readonly && *text_input == TextInput::ReadOnly)
&& keyboard_input.input.state.is_pressed()
{
if keyboard_input.input.state.is_pressed()
&& (*text_input == TextInput::Editable
|| (readonly && *text_input == TextInput::ReadOnly))
{

(COMMAND, Key::Character(c)) if c.eq_ignore_ascii_case("x") => queue_edit(TextEdit::Cut),
(COMMAND, Key::Character(c)) if c.eq_ignore_ascii_case("v") => {
queue_edit(TextEdit::Paste);
queue_edit(TextEdit::Paste, true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
queue_edit(TextEdit::Paste, true);
queue_edit(TextEdit::Paste, false);


if *text_input != TextInput::Editable {
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to the drain IME reader the same as with the other guards.

Suggested change
}
if *text_input != TextInput::Editable {
// Still need to drain the reader to prevent stale events on next focus.
ime_reader.read().for_each(drop);
return;
}

Maybe it would be more robust if we consolidated the three guards and just drain one time.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about this, but even though they have the same effect, the comments in the blocks are different.

return;
};

if *text_input == TextInput::DisplayOnly {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if *text_input == TextInput::DisplayOnly {
if *text_input != TextInput::Editable {

Comment thread crates/bevy_ui_widgets/src/text_input.rs
Comment thread crates/bevy_ui_widgets/src/text_input.rs Outdated
Comment thread crates/bevy_ui_widgets/src/text_input.rs Outdated
Comment thread crates/bevy_ui_widgets/src/text_input.rs Outdated
Comment on lines 410 to 413
/// System that enables or disables IME on the primary window based on whether the focused entity
/// is an [`EditableText`].
///
/// IME is enabled when an `EditableText` gains focus and disabled when focus moves elsewhere.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to be updated to add that IME state also depends on TextInput now.

Comment thread crates/bevy_ui_widgets/src/text_input.rs Outdated
mut editable_text_query: Query<&mut EditableText, With<TextInput>>,
) {
if let Ok(mut editable_text) = editable_text_query.get_mut(trigger.entity) {
editable_text.queue_edit(TextEdit::clear_ime_compose());

@ickshonpe ickshonpe Jul 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also need to queue TextEdit::clear_ime_compose() somewhere when TextInput changes away from Editable.

Edit: Added it to the suggested changes for listen_for_ime_input_when_text_input_focused.

#[default]
Editable,
/// Cursor movement, selection, and copy to clipboard is still enabled, but no mutations are allowed
ReadOnly,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe:

Suggested change
ReadOnly,
NavigableOnly,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One reason for preferring ReadOnly is that it's consistent with the behavior of the HTML readonly attribute: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/readonly

/// Cursor movement, selection, and copy to clipboard is still enabled, but no mutations are allowed
ReadOnly,
/// Display only, all interactions disabled - this is used by number input widget when dragging.
DisplayOnly,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then this could be NonInteractable or something.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also thought about Static

viridia and others added 3 commits July 11, 2026 09:26
Co-authored-by: ickshonpe <david.curthoys@googlemail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants