exp/textinput: bug fix: flaky behavior on iOS Safari

Closes #2898
This commit is contained in:
Hajime Hoshi 2024-02-06 00:57:50 +09:00
parent d7fc49355b
commit dd6f5c4565
2 changed files with 55 additions and 26 deletions

View File

@ -64,11 +64,11 @@ func (t *TextField) Contains(x, y int) bool {
} }
func (t *TextField) SetSelectionStartByCursorPosition(x, y int) bool { func (t *TextField) SetSelectionStartByCursorPosition(x, y int) bool {
t.cleanUp()
idx, ok := t.textIndexByCursorPosition(x, y) idx, ok := t.textIndexByCursorPosition(x, y)
if !ok { if !ok {
return false return false
} }
t.selectionStart = idx t.selectionStart = idx
t.selectionEnd = idx t.selectionEnd = idx
return true return true
@ -136,29 +136,33 @@ func (t *TextField) Blur() {
t.focused = false 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() { func (t *TextField) Update() {
if !t.focused { if !t.focused {
// If the text field still has a session, read the last state and process it just in case. // If the text field still has a session, read the last state and process it just in case.
if t.ch != nil { t.cleanUp()
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{}
}
return return
} }

View File

@ -15,6 +15,7 @@
package textinput package textinput
import ( import (
"fmt"
"syscall/js" "syscall/js"
"github.com/hajimehoshi/ebiten/v2/internal/ui" "github.com/hajimehoshi/ebiten/v2/internal/ui"
@ -77,6 +78,10 @@ func (t *textInput) init() {
if e.Get("code").String() == "Tab" { if e.Get("code").String() == "Tab" {
e.Call("preventDefault") 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() { if !e.Get("isComposing").Bool() {
ui.Get().UpdateInputFromEvent(e) ui.Get().UpdateInputFromEvent(e)
} }
@ -104,6 +109,7 @@ func (t *textInput) init() {
t.trySend(true) t.trySend(true)
return nil return nil
} }
// Though `isComposing` is false, send the text as being not committed for text completion on mobile browsers.
t.trySend(false) t.trySend(false)
return nil return nil
})) }))
@ -143,18 +149,37 @@ func (t *textInput) Start(x, y int) (chan State, func()) {
return nil, nil return nil, nil
} }
if t.session != nil {
t.session.end()
t.session = nil
}
if js.Global().Get("_ebitengine_textinput_ready").Truthy() { if js.Global().Get("_ebitengine_textinput_ready").Truthy() {
if t.session != nil {
t.session.end()
}
s := newSession() s := newSession()
t.session = s t.session = s
js.Global().Get("window").Set("_ebitengine_textinput_ready", js.Undefined()) js.Global().Get("window").Set("_ebitengine_textinput_ready", js.Undefined())
return s.ch, s.end 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). // 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. // 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) js.Global().Get("window").Set("_ebitengine_textinput_x", x)