mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2026-04-23 00:07:15 +08:00
db24fb7c6b
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) <noreply@anthropic.com>
209 lines
5.0 KiB
Go
209 lines
5.0 KiB
Go
// Copyright 2023 The Ebitengine Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// Package textinput provides a text-inputting controller.
|
|
// This package is experimental and the API might be changed in the future.
|
|
//
|
|
// This package is supported on Windows, macOS, and Web browsers so far.
|
|
package textinput
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"slices"
|
|
"sync"
|
|
"unicode/utf16"
|
|
"unicode/utf8"
|
|
|
|
"github.com/hajimehoshi/ebiten/v2/internal/ui"
|
|
)
|
|
|
|
// textInputState represents the current state of text inputting.
|
|
type textInputState struct {
|
|
// Text represents the current inputting text.
|
|
Text string
|
|
|
|
// CompositionSelectionStartInBytes represents the start position of the selection in bytes.
|
|
CompositionSelectionStartInBytes int
|
|
|
|
// CompositionSelectionStartInBytes represents the end position of the selection in bytes.
|
|
CompositionSelectionEndInBytes int
|
|
|
|
// DeleteStartInBytes represents the start position of the range to be removed in bytes.
|
|
//
|
|
// DeleteStartInBytes is valid only when Committed is true.
|
|
DeleteStartInBytes int
|
|
|
|
// DeleteEndInBytes represents the end position of the range to be removed in bytes.
|
|
//
|
|
// DeleteEndInBytes is valid only when Committed is true.
|
|
DeleteEndInBytes int
|
|
|
|
// Committed reports whether the current Text is the settled text.
|
|
Committed bool
|
|
|
|
// Error is an error that happens during text inputting.
|
|
Error error
|
|
}
|
|
|
|
// start starts text inputting.
|
|
// start returns a channel to send the state repeatedly, and a function to end the text inputting.
|
|
//
|
|
// start returns nil and nil if the current environment doesn't support this package.
|
|
func start(bounds image.Rectangle) (states <-chan textInputState, close func()) {
|
|
cMinX, cMinY := ui.Get().LogicalPositionToClientPositionInNativePixels(float64(bounds.Min.X), float64(bounds.Min.Y))
|
|
cMaxX, cMaxY := ui.Get().LogicalPositionToClientPositionInNativePixels(float64(bounds.Max.X), float64(bounds.Max.Y))
|
|
return theTextInput.Start(image.Rect(int(cMinX), int(cMinY), int(cMaxX), int(cMaxY)))
|
|
}
|
|
|
|
func convertUTF16CountToByteCount(text string, c int) int {
|
|
if !utf8.ValidString(text) {
|
|
return -1
|
|
}
|
|
if c == 0 {
|
|
return 0
|
|
}
|
|
var utf16Len int
|
|
for idx, r := range text {
|
|
l16 := utf16.RuneLen(r)
|
|
if l16 < 0 {
|
|
panic(fmt.Sprintf("textinput: invalid rune: %c", r))
|
|
}
|
|
utf16Len += l16
|
|
if utf16Len >= c {
|
|
l8 := utf8.RuneLen(r)
|
|
if l8 < 0 {
|
|
panic(fmt.Sprintf("textinput: invalid rune: %c", r))
|
|
}
|
|
return idx + l8
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func convertByteCountToUTF16Count(text string, c int) int {
|
|
if !utf8.ValidString(text) {
|
|
return -1
|
|
}
|
|
if c == 0 {
|
|
return 0
|
|
}
|
|
var utf16Len int
|
|
for idx, r := range text {
|
|
l16 := utf16.RuneLen(r)
|
|
if l16 < 0 {
|
|
panic(fmt.Sprintf("textinput: invalid rune length for rune %c", r))
|
|
}
|
|
utf16Len += l16
|
|
l8 := utf8.RuneLen(r)
|
|
if l8 < 0 {
|
|
panic(fmt.Sprintf("textinput: invalid rune length for rune %c", r))
|
|
}
|
|
if idx+l8 >= c {
|
|
return utf16Len
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
type textInput struct {
|
|
textInputImpl
|
|
session session
|
|
}
|
|
|
|
var theTextInput textInput
|
|
|
|
type session struct {
|
|
ch chan textInputState
|
|
done chan struct{}
|
|
|
|
queuedStates []textInputState
|
|
|
|
m sync.Mutex
|
|
}
|
|
|
|
func (s *session) start() (ch chan textInputState, endFunc func()) {
|
|
s.m.Lock()
|
|
defer s.m.Unlock()
|
|
|
|
if s.ch == nil {
|
|
// 10 should be enough for most cases.
|
|
// Typical keyboards can send less than 10 events at the same time.
|
|
s.ch = make(chan textInputState, 10)
|
|
s.done = make(chan struct{})
|
|
}
|
|
s.flushStateQueue()
|
|
return s.ch, s.end
|
|
}
|
|
|
|
func (s *session) end() {
|
|
s.m.Lock()
|
|
defer s.m.Unlock()
|
|
|
|
if s.ch == nil {
|
|
return
|
|
}
|
|
close(s.ch)
|
|
s.ch = nil
|
|
close(s.done)
|
|
s.done = nil
|
|
}
|
|
|
|
func (s *session) send(state textInputState) {
|
|
s.m.Lock()
|
|
defer s.m.Unlock()
|
|
|
|
if s.ch != nil {
|
|
s.flushStateQueue()
|
|
s.doSend(state)
|
|
} else {
|
|
s.queuedStates = append(s.queuedStates, state)
|
|
}
|
|
}
|
|
|
|
func (s *session) doSend(state textInputState) {
|
|
if s.ch == nil {
|
|
panic("textinput: session is not started")
|
|
}
|
|
for {
|
|
select {
|
|
case s.ch <- state:
|
|
return
|
|
default:
|
|
// Ignore the first value.
|
|
select {
|
|
case <-s.ch:
|
|
case <-s.done:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
s.queuedStates = slices.Delete(s.queuedStates, 0, len(s.queuedStates))
|
|
}
|