Skip to content

TextInput#24929

Draft
viridia wants to merge 17 commits into
bevyengine:mainfrom
viridia:text_input
Draft

TextInput#24929
viridia wants to merge 17 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah these are fine.

#[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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

IMO this should just be an optional mode-switch, and not pull in accessibility components. Those should be attached to the actual widgets.

Strong preference for TextInputMode as a name; we can't use TextInput for something like this because it makes it seem too important.


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
@@ -360,7 +413,7 @@ fn update_ime_position(
/// 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

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.

Yep, it's fine. It's not user facing and the doc comments are clear about the meaning of each variant.

/// 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>
@viridia

viridia commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

@ickshonpe @alice-i-cecile So we have a couple of outstanding issues here.

First, there's a system ambiguity which is causing the CI to fail, and which I am unsure how to fix.

Second, there's some unfinished bikeshedding around the naming and role of the TextInput component.

As I see it, we have two choices about how we can structure this:

Choice 1: EditableText is the primary component: it's a widget in it's own right (meaning, there are a bunch of observers associated with it), and the read/write mode is an auxiliary, possibly optional component. If we choose this route, then it makes sense to rename TextInput to something like TextEditable or ReadWriteMode and make it optional.

The biggest problem here is what to do about accessibility: we need to add AccessibilityNode as a required component if we are going to be consistent with the other widgets, and the only two candidates for this are EditableText and TextInput. I don't like the idea of adding a dep from bevy_text to bevy_a11y, since a11y is really about interaction, and is outside of bevy_text's wheelhouse. The only other option is to add the requirement dynamically in the plugin rather than using a Rust annotation / derive macro.

Choice 2: EditableText represents just a buffer, a passive component with no associated event listeners but lots of methods for manipulating text via queued actions; the actual "widget" is TextInput, and it, along with all of its observers, live in bevy_ui_text.

Finally, I want to mention that I need to solve this problem to unblock my work on FeathersNumberInput and the color picker dropdown generally.

@alice-i-cecile

Copy link
Copy Markdown
Member

Responded in #24929 (comment) :) Let me know if you can't work out the resolution of the ambiguity and I'll take a look.

@alice-i-cecile alice-i-cecile added A-UI Graphical user interfaces, styles, layouts, and widgets C-Code-Quality A section of code that is hard to understand or change M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide X-Contentious There are nontrivial implications that should be thought through S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Jul 12, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in UI Jul 12, 2026
| TextEdit::BackspaceWord
| TextEdit::Delete
| TextEdit::DeleteWord
| TextEdit::ImeSetCompose {

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.

This is a bit annoying because technically ImeSetCompose isn't destructive I guess, depending on how you think about it. But pragmatically it should be on this side.

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.

Though thinking about it, the readonly and IME modes are meant to be mutually exclusive. If is_destructive gets called on ImeSetCompose, it might be indicative of an error and maybe we should emit a warning. It's not important for this PR though, I'll just make a note of it and look into later.

@ickshonpe

Copy link
Copy Markdown
Contributor

@ickshonpe @alice-i-cecile So we have a couple of outstanding issues here.

First, there's a system ambiguity which is causing the CI to fail, and which I am unsure how to fix.

Second, there's some unfinished bikeshedding around the naming and role of the TextInput component.

As I see it, we have two choices about how we can structure this:

Choice 1: EditableText is the primary component: it's a widget in it's own right (meaning, there are a bunch of observers associated with it), and the read/write mode is an auxiliary, possibly optional component. If we choose this route, then it makes sense to rename TextInput to something like TextEditable or ReadWriteMode and make it optional.

The biggest problem here is what to do about accessibility: we need to add AccessibilityNode as a required component if we are going to be consistent with the other widgets, and the only two candidates for this are EditableText and TextInput. I don't like the idea of adding a dep from bevy_text to bevy_a11y, since a11y is really about interaction, and is outside of bevy_text's wheelhouse. The only other option is to add the requirement dynamically in the plugin rather than using a Rust annotation / derive macro.

Choice 2: EditableText represents just a buffer, a passive component with no associated event listeners but lots of methods for manipulating text via queued actions; the actual "widget" is TextInput, and it, along with all of its observers, live in bevy_ui_text.

Finally, I want to mention that I need to solve this problem to unblock my work on FeathersNumberInput and the color picker dropdown generally.

I think that neither EditableText nor ReadWriteMode (assuming it's renamed) should be the primary component. Instead we should add another a new component TextInput that acts as the primary component and carries all the widget specific requirements. It can just be a zst for now, but later can we can migrate the customisation fields over to it from EditableText.

@viridia

viridia commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

I restructured TextInput based on our discussion, it's now a marker component, with a separate TextReadWriteMode enum.

@ickshonpe This was a difficult merge, so please make sure I didn't screw anything up.

I don't know a lot about IMEs, and the logic for detecting writeable / read-only transitions got a lot gnarlier now that TextReadWriteMode is optional - I needed to also detect component removal.

@alice-i-cecile alice-i-cecile added S-Needs-Review Needs reviewer attention (from anyone!) to move forward and removed S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Jul 14, 2026
@viridia

viridia commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Also, I'm not sure how to fix the system ambiguity with the ImeSystems sets.

Comment on lines 746 to 753
// These components cannot be registered in `bevy_text` where `EditableText` is defined,
// because that would create a circular dependency between `bevy_text` and `bevy_ui`.
app.register_required_components::<EditableText, Node>()
.register_required_components::<EditableText, TextNodeFlags>()
.register_required_components_with::<EditableText, AccessibilityNode>(|| {
AccessibilityNode(accesskit::Node::new(Role::TextInput))
})
.register_required_components::<EditableText, ContentSize>();

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.

These requirements can be moved onto TextInput now?

fn listen_for_ime_input_when_text_input_focused(
input_focus: Res<InputFocus>,
editable_text_query: Query<(), With<EditableText>>,
mut removed_rwmode: RemovedComponents<TextReadWriteMode>,

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.

Is there a reason for not just requiring TextReadWriteMode on TextInput? RemovedComponents is such a pain, supporting TextReadWriteMode for all text input types might be simpler.

@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.

I made a branch for testing where I added a TextReadWriteMode menu to the multiline_text_input example:

https://github.com/ickshonpe/bevy/tree/24929-textreadwritemode-example

In static mode, selection and scrolling is still possible if the text is longer than the view and you drag outside of the input.

@viridia

viridia commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

I made a branch for testing where I added a TextReadWriteMode menu to the multiline_text_input example:

Based on the discussion on discord, we might instead change this to be a pair of marker components, one of which is the pre-existing InteractionDisabled. This will be a PITA to implement (more component removal detection, yuck), but minimizes the number of new concepts being introduced.

@viridia
viridia marked this pull request as draft July 17, 2026 01:19
@viridia

viridia commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

I've updated the multiple_text_inputs example to include the readonly and static variants:

text-input-variations

In the process of developing this PR, I have learned a bunch of things:

  • My idea of using InteractionDisabled to mean either ReadOnly or Static didn't work out: in the current feathers code, InteractionDisabled causes an appearance change ("graying out"), and I needed a way to disable editing without this.
  • I've also found a number of bugs and incorrect code, which I've attempted to address. For example, some headless widgets were setting focus to themselves, and others were not - but this was all useless code, because the global "click-to-focus" observer was already doing this.
  • Also, I realized that the "click-to-focus" observer lives in a lower-level crate (bevy_input_focus), and because of this it cannot depend on InteractionDisabled which lives in bevy_ui. (Input focus is meant to work with 2d and 3d objects as well as UI, so it cannot depend on marker components in UI.)
  • I think that eventually we will need to rename InteractionDisabled to make it less confusing, and possibly split it into two separate components - one which makes the widget non-focusable, and one which makes it non-operable. This avoids arguments about the meaning of the word "interaction".
  • I've also realized that there are subtle differences between the strict interpretation of the W3C accessibility guidelines, and the common behavior of many UI frameworks (both web and native). There is a lot of inconsistency here, and no clear policy about which set of standards Bevy should adhere to.
  • For the moment I have decided to simply document the current behavior and move on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-UI Graphical user interfaces, styles, layouts, and widgets C-Code-Quality A section of code that is hard to understand or change M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Needs-Review Needs reviewer attention (from anyone!) to move forward X-Contentious There are nontrivial implications that should be thought through

Projects

Status: Needs SME Triage

Development

Successfully merging this pull request may close these issues.

3 participants