Repository navigation
feat(ui): add text input fields backed by a hidden DOM input - #730
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VktGvQk7RUMN8yxuAUXER
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VktGvQk7RUMN8yxuAUXER
…ut-field # Conflicts: # CHANGELOG.md
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VktGvQk7RUMN8yxuAUXER
…ut-field Caret stops are rebuilt on top of the rich text tag parser. Text fields set the new TextEcsComponent.richText to false, so text a player types is drawn as written and its caret stops line up with the input's value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VktGvQk7RUMN8yxuAUXER
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VktGvQk7RUMN8yxuAUXER
stormmuller
enabled auto-merge (squash)
October 7, 2026 06:58
stormmuller
pushed a commit
that referenced
this pull request
Oct 7, 2026
Resolve conflicts with text input fields (#730): keep caretStops and richText on the shaping side without the renderable fields, move raycast-ui-canvas onto sprite.category, and migrate the new text-input demo, e2e scene and tests to Texture, createImageSprite(texture), FontAtlasCache(renderContext) and createTextShapingEcsSystem(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RcdXTDthW5D8wqauepMX2e
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements
design/text-input-field.md(all phases) andui-system.mdbacklog item 3.5 (TextInputEcsComponent). The design doc is deleted,ui-system.md's status and row 3.5 are updated, and the link ininput-action-state.mdis reworded.demo-findings.mdis left for the coordinator.Phase 1: keyboard source.
KeyboardInputSourceignores keys whosekeydowntargets an editable element (<input>,<textarea>,<select>,contentEditable). It reads the target fromcomposedPath()[0], so elements inside a shadow root count too. Those keys never become held, and akeyupfor a key that isn't held is ignored. A key held before typing started is still released. The unused_keyPressesDown/_keyPressesUpssets are deleted. That leftreset()empty, so the source no longer implementsResettable.Phase 2: text entry and field.
createTextEntry(container)(src/input/text-entry) creates one hidden<input>, placed over a CSS rect inside the container. It exposes the value, the selection and the IME composition range. It enforces the held-key-doesn't-repeat rule, treats Enter and Escape as submit and cancel except during IME composition (holding Enter submits once), and cancels Tab. Events (changed/submitted/cancelled/blurred) are drained once per tick withtakeEvents().shapeTextreturnscaretStops: the final position of every UTF-16 boundary of the shaped text, including whitespace, missing glyphs and the inner boundary of a surrogate pair.TextMeshEcsComponent.caretStopscarries them.TextEcsComponent.richText(andshapeText'srichTextoption), defaulttrue. Withfalse,<b>/<color>tags are drawn as written. See "Merged with rich text tags" below.raycastUiCanvas.createUiRaycastEcsSystemcalls it once per canvas.TextInputEcsComponent,setTextInputValue,editTextInput,createTextInput, andcreateUiTextInputEcsSystem, whichregisterUiSystemsregisters.ui/text-input.mdandinput/text-entry.mdguides, a note ininput/keyboard.md, andrichText: falseintext/rendering-text.md.ui-text-input, a name entry form with two fields and filters.ui-text-inputscene and spec.Merged with rich text tags (#729)
#729 landed while this PR was open, and
shapeTextnow strips<b>/<color>tags before shaping. A text field can't use that. A player who typed<b>would see it vanish and their text turn bold, and the caret stops (computed for the tag-free string) would stop matching the input's value. #729 has no escape syntax, so the exact text<b>can't be written in tagged text at all. SoTextEcsComponentgainsrichText(defaulttrue, so authored labels keep the tags), and a text field's labels set it tofalse. This is TextMeshPro'srichTextflag, which input fields turn off for the same reason. It's a real per-text choice (authored labels versus what a player typed), not a switch between an old and a new behavior. Reviewers: say so if you'd rather settle this another way.Changes from the design, and why
TextInputEcsComponent.entryholds the field'sTextEntry, andisEditingis simplyentry.isFocused. SosetTextInputValuewrites the DOM input,editTextInputcallsfocus(), and submit and cancel callblur(). No request fields are needed, and the browser's one-focused-element rule gives "one editing field per page" for free. The system keeps a map of the entries it has seen so it can dispose them when a component is removed and incleanup. (Suggested by the solution reviewer; see below.)placeholderis a label entity, not a string. The design hadplaceholder: stringon the component. Instead the component holdsplaceholderLabel, whoseTextEcsComponentowns the text and colour, and the system only toggles itsenabled. This follows the pattern of the slider'shandle/filland Unity'sTMP_InputField.placeholder, and the string has one owner.maxLengthisn't set as the DOMmaxlength. The browser counted characters the font/game filters were about to drop. In the demo, typingax-7q99into a 6-character field ended atAX7Q9. The field appliesmaxLengthitself, after filtering, andTextInputAttributesomits it.TextEntryAttributes.maxLengthstays available to direct users of the text entry.pointerupit focuses a field only if the press and the release both hit that field (a drag doesn't count), and only if it is the single element hit across all canvases. Without a draw order between canvases, that's what keeps a field under a modal on another canvas from being edited.mousedownon a field ispreventDefaulted, so clicking the field you're typing in doesn't blur it for a frame.resolveCanvasPointerPositiontakes a viewport position instead of aUiPointerSource. The tap handler has an event position, not a pointer source. Callers updated; there's a changelog bullet.Decisions taken from the design's open questions
pointer-events: none.Solution reviewer verdict
REVISE, before any code was written. All of its required changes were applied except one:
mousedownon a field ispreventDefaulted.Resettablewas dropped.design/text-input-field.mdas historical rationale. The task said to delete a fully implemented design doc, and docs(design): remove design documents that have been implemented #720 did the same, so it is deleted.value/isEditingto bereadonly. The system has to write them without casts, so they followUiInteractableEcsComponent's convention of mutable fields documented as system-owned.It approved the placeholder entity and starting editing on
wasInvokedThisFrame. TherichTextaddition came later, from merging #729, and wasn't part of that review.Related issue(s)
Closes #586
Verification checklist
npm run check-typespasses with 0 errorsnpm testpasses (2170 tests, on the latestdev)npm run lintpasses with 0 errors (2 existing TODO warnings)npm run cspellpasses with 0 errorsnpm run check-exportspassesindex.ts/documentation-site/docs/docsis updatednpm run build, docstypecheckandbuild, then the built site opened in Chromium (again after mergingdev). Typing, filtering (leading spaces andédropped, badge upper-cased), submit and the caret all work in the new demo. The button, slider, dropdown and main-menu demos still render, and dragging in the slider demo moves its handle.dev), including the newui-text-input.spec.ts. That spec checks real typing grows the drawn text (a relative, same-run pixel check), held Enter submits once, Escape cancels, arrow keys typed into the field don't move UI focus, hovering another element doesn't end editing, and a tap on a panel covering the field doesn't start editing it.Changelog
## [Unreleased]: Added (ui text inputs,createTextEntry,TextEcsComponent.richText,caretStops,raycastUiCanvas), Changed (resolveCanvasPointerPosition), Fixed (keyboard source and editable elements).🤖 Generated with Claude Code
https://claude.ai/code/session_014VktGvQk7RUMN8yxuAUXER