From bc19bc2360a5556821fe59db0a3ae8ae7eeb4d38 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 21:40:38 +0000 Subject: [PATCH 1/4] feat(ui): add text input fields backed by a hidden DOM input Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014VktGvQk7RUMN8yxuAUXER --- .cspell/project-words.txt | 3 + src/input/index.ts | 1 + .../keyboard-input-source.test.ts | 82 +++ .../input-sources/keyboard-input-source.ts | 53 +- src/input/text-entry/index.ts | 1 + src/input/text-entry/text-entry.test.ts | 193 ++++++ src/input/text-entry/text-entry.ts | 467 ++++++++++++++ src/rendering/systems/render-system.test.ts | 1 + src/text/components/text-mesh-component.ts | 8 + src/text/rendering/glyph-quad.test.ts | 2 + src/text/systems/text-shaping-system.test.ts | 1 + src/text/systems/text-shaping-system.ts | 3 +- src/text/utilities/shape-text.test.ts | 78 +++ src/text/utilities/shape-text.ts | 175 ++++- src/ui/components/index.ts | 1 + .../components/text-input-component.test.ts | 57 ++ src/ui/components/text-input-component.ts | 184 ++++++ src/ui/systems/index.ts | 1 + src/ui/systems/ui-interaction-system.ts | 2 +- src/ui/systems/ui-raycast-system.ts | 172 +---- src/ui/systems/ui-slider-system.ts | 7 +- src/ui/systems/ui-text-input-system.test.ts | 434 +++++++++++++ src/ui/systems/ui-text-input-system.ts | 609 ++++++++++++++++++ src/ui/utilities/create-text-input.test.ts | 116 ++++ src/ui/utilities/create-text-input.ts | 345 ++++++++++ src/ui/utilities/index.ts | 2 + src/ui/utilities/raycast-ui-canvas.test.ts | 84 +++ src/ui/utilities/raycast-ui-canvas.ts | 144 +++++ src/ui/utilities/register-ui-systems.ts | 15 +- .../resolve-canvas-pointer-position.test.ts | 15 +- .../resolve-canvas-pointer-position.ts | 12 +- 31 files changed, 3045 insertions(+), 223 deletions(-) create mode 100644 src/input/text-entry/index.ts create mode 100644 src/input/text-entry/text-entry.test.ts create mode 100644 src/input/text-entry/text-entry.ts create mode 100644 src/ui/components/text-input-component.test.ts create mode 100644 src/ui/components/text-input-component.ts create mode 100644 src/ui/systems/ui-text-input-system.test.ts create mode 100644 src/ui/systems/ui-text-input-system.ts create mode 100644 src/ui/utilities/create-text-input.test.ts create mode 100644 src/ui/utilities/create-text-input.ts create mode 100644 src/ui/utilities/raycast-ui-canvas.test.ts create mode 100644 src/ui/utilities/raycast-ui-canvas.ts diff --git a/.cspell/project-words.txt b/.cspell/project-words.txt index dbd1070ae..a418833cf 100644 --- a/.cspell/project-words.txt +++ b/.cspell/project-words.txt @@ -48,6 +48,7 @@ devicerestored diegetic dppx eamodio +enterkeyhint Erdokovy esbenp evented @@ -83,6 +84,7 @@ implementability impluse Infima inigo +inputmode interactable interactables isampler @@ -100,6 +102,7 @@ letterboxed letterboxing lifecycles matterjs +maxlength mediump Menlo mesher diff --git a/src/input/index.ts b/src/input/index.ts index 497d0ca76..c8b990076 100644 --- a/src/input/index.ts +++ b/src/input/index.ts @@ -11,3 +11,4 @@ export * from './mouse/index.js'; export * from './keyboard/index.js'; export * from './gamepad/index.js'; export * from './register-inputs.js'; +export * from './text-entry/index.js'; diff --git a/src/input/keyboard/input-sources/keyboard-input-source.test.ts b/src/input/keyboard/input-sources/keyboard-input-source.test.ts index 313c74b62..930053199 100644 --- a/src/input/keyboard/input-sources/keyboard-input-source.test.ts +++ b/src/input/keyboard/input-sources/keyboard-input-source.test.ts @@ -52,6 +52,7 @@ describe('KeyboardInputSource', () => { it('dispatches key up trigger actions', () => { expect(keyUpAction.isTriggered).toBe(false); + window.dispatchEvent(new KeyboardEvent('keydown', { code: keyCodes.a })); window.dispatchEvent(new KeyboardEvent('keyup', { code: keyCodes.a })); expect(keyUpAction.isTriggered).toBe(true); @@ -304,6 +305,87 @@ describe('KeyboardInputSource', () => { expect(axis1dAction.value).toBe(0); }); + describe('keys typed into editable elements', () => { + const dispatchKey = ( + target: EventTarget, + type: 'keydown' | 'keyup', + code: string, + ): void => { + target.dispatchEvent( + new KeyboardEvent(type, { code, bubbles: true, composed: true }), + ); + }; + + let input: HTMLInputElement; + + beforeEach(() => { + input = document.createElement('input'); + document.body.appendChild(input); + }); + + it('ignores a key pressed in an , and its release', () => { + dispatchKey(input, 'keydown', keyCodes.s); + expect(keyDownAction.isTriggered).toBe(false); + + dispatchKey(input, 'keydown', keyCodes.a); + dispatchKey(input, 'keyup', keyCodes.a); + expect(keyUpAction.isTriggered).toBe(false); + + input.remove(); + }); + + it('ignores keys pressed in a