From dd6f5c4565b5481ffbe756329d570e7fbd2d5ecd Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Tue, 6 Feb 2024 00:57:50 +0900 Subject: [PATCH] exp/textinput: bug fix: flaky behavior on iOS Safari Closes #2898 --- examples/textinput/main.go | 46 +++++++++++++++++++---------------- exp/textinput/textinput_js.go | 35 ++++++++++++++++++++++---- 2 files changed, 55 insertions(+), 26 deletions(-) diff --git a/examples/textinput/main.go b/examples/textinput/main.go index 5e43048b7..38b250f67 100644 --- a/examples/textinput/main.go +++ b/examples/textinput/main.go @@ -64,11 +64,11 @@ func (t *TextField) Contains(x, y int) bool { } func (t *TextField) SetSelectionStartByCursorPosition(x, y int) bool { + t.cleanUp() idx, ok := t.textIndexByCursorPosition(x, y) if !ok { return false } - t.selectionStart = idx t.selectionEnd = idx return true @@ -136,29 +136,33 @@ func (t *TextField) Blur() { t.focused = false } +func (t *TextField) cleanUp() { + if t.ch != nil { + select { + case state, ok := <-t.ch: + if ok && state.Committed { + t.text = t.text[:t.selectionStart] + state.Text + t.text[t.selectionEnd:] + t.selectionStart += len(state.Text) + t.selectionEnd = t.selectionStart + t.state = textinput.State{} + } + t.state = state + default: + break + } + } + if t.end != nil { + t.end() + t.ch = nil + t.end = nil + t.state = textinput.State{} + } +} + func (t *TextField) Update() { if !t.focused { // If the text field still has a session, read the last state and process it just in case. - if t.ch != nil { - select { - case state, ok := <-t.ch: - if ok && state.Committed { - t.text = t.text[:t.selectionStart] + state.Text + t.text[t.selectionEnd:] - t.selectionStart += len(state.Text) - t.selectionEnd = t.selectionStart - t.state = textinput.State{} - } - t.state = state - default: - break - } - } - if t.end != nil { - t.end() - t.ch = nil - t.end = nil - t.state = textinput.State{} - } + t.cleanUp() return } diff --git a/exp/textinput/textinput_js.go b/exp/textinput/textinput_js.go index 34d5dde62..0a65e4e6e 100644 --- a/exp/textinput/textinput_js.go +++ b/exp/textinput/textinput_js.go @@ -15,6 +15,7 @@ package textinput import ( + "fmt" "syscall/js" "github.com/hajimehoshi/ebiten/v2/internal/ui" @@ -77,6 +78,10 @@ func (t *textInput) init() { if e.Get("code").String() == "Tab" { e.Call("preventDefault") } + if e.Get("code").String() == "Enter" || e.Get("key").String() == "Enter" { + // Ignore Enter key to avoid ebiten.IsKeyPressed(ebiten.KeyEnter) unexpectedly becomes true, especially for iOS Safari. + return nil + } if !e.Get("isComposing").Bool() { ui.Get().UpdateInputFromEvent(e) } @@ -104,6 +109,7 @@ func (t *textInput) init() { t.trySend(true) return nil } + // Though `isComposing` is false, send the text as being not committed for text completion on mobile browsers. t.trySend(false) return nil })) @@ -143,18 +149,37 @@ func (t *textInput) Start(x, y int) (chan State, func()) { return nil, nil } - if t.session != nil { - t.session.end() - t.session = nil - } - if js.Global().Get("_ebitengine_textinput_ready").Truthy() { + if t.session != nil { + t.session.end() + } s := newSession() t.session = s js.Global().Get("window").Set("_ebitengine_textinput_ready", js.Undefined()) return s.ch, s.end } + // If a textarea is focused, create a session immediately. + // A virtual keyboard should already be shown on mobile browsers. + if document.Get("activeElement").Equal(t.textareaElement) { + t.textareaElement.Set("value", "") + t.textareaElement.Call("focus") + style := t.textareaElement.Get("style") + style.Set("left", fmt.Sprintf("%dpx", x)) + style.Set("top", fmt.Sprintf("%dpx", y)) + + if t.session == nil { + s := newSession() + t.session = s + } + return t.session.ch, t.session.end + } + + if t.session != nil { + t.session.end() + t.session = nil + } + // On iOS Safari, `focus` works only in user-interaction events (#2898). // Assuming Start is called every tick, defer the starting process to the next user-interaction event. js.Global().Get("window").Set("_ebitengine_textinput_x", x)