From db24fb7c6bc0d14874ef95e2a55b8f0e0c67ccfd Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Sat, 4 Apr 2026 02:50:20 +0900 Subject: [PATCH] exp/textinput: clear queued states when text field is unfocused The session's state queue buffers input events that arrive between a commit-end and the next session start, which is needed when multiple keys are pressed simultaneously (#3382). However, when a text field was unfocused, keystroke events continued to be queued by the platform layer and were replayed when a field was next focused, causing unexpected text to appear. Clear the queued states in Field.cleanUp(), which is called when a field is blurred. This preserves the queue for simultaneous keypresses within a focused field while discarding stale input from unfocused periods. Also refactor textInput to move the session field and theTextInput variable to the cross-platform textinput.go, with each platform defining only a textInputImpl with platform-specific fields. Updates #3382 Closes #3429 Co-Authored-By: Claude Opus 4.6 (1M context) --- exp/textinput/field.go | 2 ++ exp/textinput/textinput.go | 16 ++++++++++++++++ exp/textinput/textinput_darwin.go | 7 +------ exp/textinput/textinput_js.go | 6 +----- exp/textinput/textinput_noime.go | 5 +---- exp/textinput/textinput_windows.go | 6 +----- 6 files changed, 22 insertions(+), 20 deletions(-) diff --git a/exp/textinput/field.go b/exp/textinput/field.go index eef846f36..9c872f4bb 100644 --- a/exp/textinput/field.go +++ b/exp/textinput/field.go @@ -290,6 +290,8 @@ func (f *Field) cleanUp() { f.end = nil f.state = textInputState{} } + + theTextInput.session.clearQueue() } // Selection returns the current selection range in bytes. diff --git a/exp/textinput/textinput.go b/exp/textinput/textinput.go index 111c8b5f2..a2adb1729 100644 --- a/exp/textinput/textinput.go +++ b/exp/textinput/textinput.go @@ -117,6 +117,13 @@ func convertByteCountToUTF16Count(text string, c int) int { return -1 } +type textInput struct { + textInputImpl + session session +} + +var theTextInput textInput + type session struct { ch chan textInputState done chan struct{} @@ -184,6 +191,15 @@ func (s *session) doSend(state textInputState) { } } +// clearQueue clears queued states. +// This should be called when the text field is unfocused +// so that the queued states are not flushed when the next session starts (#3429). +func (s *session) clearQueue() { + s.m.Lock() + defer s.m.Unlock() + s.queuedStates = s.queuedStates[:0] +} + func (s *session) flushStateQueue() { for _, st := range s.queuedStates { s.doSend(st) diff --git a/exp/textinput/textinput_darwin.go b/exp/textinput/textinput_darwin.go index aac85ada1..5210fa7d8 100644 --- a/exp/textinput/textinput_darwin.go +++ b/exp/textinput/textinput_darwin.go @@ -25,12 +25,7 @@ import ( "github.com/hajimehoshi/ebiten/v2" ) -type textInput struct { - // session must be accessed from the main thread. - session session -} - -var theTextInput textInput +type textInputImpl struct{} func (t *textInput) Start(bounds image.Rectangle) (<-chan textInputState, func()) { var ch <-chan textInputState diff --git a/exp/textinput/textinput_js.go b/exp/textinput/textinput_js.go index 83ed07894..10c949832 100644 --- a/exp/textinput/textinput_js.go +++ b/exp/textinput/textinput_js.go @@ -34,14 +34,10 @@ func init() { theTextInput.init() } -type textInput struct { +type textInputImpl struct { textareaElement js.Value - - session session } -var theTextInput textInput - func (t *textInput) init() { t.textareaElement = document.Call("createElement", "textarea") t.textareaElement.Set("id", "ebitengine-textinput") diff --git a/exp/textinput/textinput_noime.go b/exp/textinput/textinput_noime.go index fafef8361..81aace6da 100644 --- a/exp/textinput/textinput_noime.go +++ b/exp/textinput/textinput_noime.go @@ -22,14 +22,11 @@ import ( "github.com/hajimehoshi/ebiten/v2" ) -type textInput struct { - session session +type textInputImpl struct { rs []rune lastTick int64 } -var theTextInput textInput - func (t *textInput) Start(bounds image.Rectangle) (<-chan textInputState, func()) { // AppendInputChars is updated only when the tick is updated. // If the tick is not updated, return nil immediately. diff --git a/exp/textinput/textinput_windows.go b/exp/textinput/textinput_windows.go index 5c7585780..71414a281 100644 --- a/exp/textinput/textinput_windows.go +++ b/exp/textinput/textinput_windows.go @@ -27,9 +27,7 @@ import ( "github.com/hajimehoshi/ebiten/v2/internal/ui" ) -type textInput struct { - session session - +type textInputImpl struct { origWndProc uintptr wndProcCallback uintptr window windows.HWND @@ -42,8 +40,6 @@ type textInput struct { err error } -var theTextInput textInput - func (t *textInput) Start(bounds image.Rectangle) (<-chan textInputState, func()) { if microsoftgdk.IsXbox() { return nil, nil