TextInput#24929
Conversation
* FeathersLazyMenu * FeathersMenuToolButton
| 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, | ||
| } |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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| { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Maybe we could have a separate function that returns true if a TextEdit is destructive that we can filter by in queue_edit?
There was a problem hiding this comment.
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.
| if *text_input == TextInput::Editable | ||
| || (readonly && *text_input == TextInput::ReadOnly) | ||
| && keyboard_input.input.state.is_pressed() | ||
| { |
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
| queue_edit(TextEdit::Paste, true); | |
| queue_edit(TextEdit::Paste, false); |
|
|
||
| if *text_input != TextInput::Editable { | ||
| return; | ||
| } |
There was a problem hiding this comment.
Need to the drain IME reader the same as with the other guards.
| } | |
| 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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
| if *text_input == TextInput::DisplayOnly { | |
| if *text_input != TextInput::Editable { |
| @@ -360,7 +413,7 @@ fn update_ime_position( | |||
| /// IME is enabled when an `EditableText` gains focus and disabled when focus moves elsewhere. | |||
There was a problem hiding this comment.
Needs to be updated to add that IME state also depends on TextInput now.
| 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()); |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
Maybe:
| ReadOnly, | |
| NavigableOnly, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
Then this could be NonInteractable or something.
There was a problem hiding this comment.
I also thought about Static
Co-authored-by: ickshonpe <david.curthoys@googlemail.com>
|
@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 As I see it, we have two choices about how we can structure this: Choice 1: The biggest problem here is what to do about accessibility: we need to add Choice 2: Finally, I want to mention that I need to solve this problem to unblock my work on |
|
Responded in #24929 (comment) :) Let me know if you can't work out the resolution of the ambiguity and I'll take a look. |
| | TextEdit::BackspaceWord | ||
| | TextEdit::Delete | ||
| | TextEdit::DeleteWord | ||
| | TextEdit::ImeSetCompose { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
I think that neither |
Read-only state is now controlled by `TextReadWriteMode`.
|
I restructured @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 |
|
Also, I'm not sure how to fix the system ambiguity with the ImeSystems sets. |
| // 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>(); |
There was a problem hiding this comment.
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>, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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 |

This PR splits the functionality of the
EditableTextcomponent into two separate components:EditableText(which lives in bevy_text) andTextInput(which lives in bevy_ui_widgets).See release notes for more details.