exp/textinput: add HandleInputWithBounds

Closes #3233
This commit is contained in:
Hajime Hoshi
2025-05-12 21:42:25 +09:00
parent bba2a83aa6
commit 6bd9021a91
7 changed files with 60 additions and 28 deletions

View File

@@ -135,9 +135,11 @@ func (t *TextField) Update() error {
x, y := t.bounds.Min.X, t.bounds.Min.Y
cx, cy := t.cursorPos()
px, py := textFieldPadding()
x += cx + px
y += cy + py + int(fontFace.Metrics().HAscent)
handled, err := t.field.HandleInput(x, y)
x0 := x + cx + px
x1 := x + cx + px + 6 // To be exact, this should be the width of the current character.
y0 := y + cy + py
y1 := y0 + int(fontFace.Metrics().HLineGap+fontFace.Metrics().HAscent+fontFace.Metrics().HDescent)
handled, err := t.field.HandleInputWithBounds(image.Rect(x0, y0, x1, y1))
if err != nil {
return err
}

View File

@@ -15,6 +15,7 @@
package textinput
import (
"image"
"sync"
)
@@ -99,7 +100,21 @@ type Field struct {
// If HandleInput returns true, a Field user should not handle further input events.
//
// HandleInput returns an error when handling input causes an error.
//
// Deprecated: use HandleInputWithBounds instead.
func (f *Field) HandleInput(x, y int) (handled bool, err error) {
return f.HandleInputWithBounds(image.Rect(x, y, x+1, y+1))
}
// HandleInputWithBounds updates the field state.
// HandleInputWithBounds must be called every tick, i.e., every Update, when Field is focused.
// HandleInputWithBounds takes a character bounds, which decides the position where an IME window is shown if needed.
//
// HandleInputWithBounds returns whether the text inputting is handled or not.
// If HandleInputWithBounds returns true, a Field user should not handle further input events.
//
// HandleInputWithBounds returns an error when handling input causes an error.
func (f *Field) HandleInputWithBounds(bounds image.Rectangle) (handled bool, err error) {
if f.err != nil {
return false, f.err
}
@@ -113,7 +128,7 @@ func (f *Field) HandleInput(x, y int) (handled bool, err error) {
if f.ch == nil {
// TODO: On iOS Safari, Start doesn't work as expected (#2898).
// Handle a click event and focus the textarea there.
f.ch, f.end = start(x, y)
f.ch, f.end = start(bounds)
// Start returns nil for non-supported envrionments, or when unable to start text inputting for some reasons.
if f.ch == nil {
return handled, nil

View File

@@ -19,6 +19,7 @@
package textinput
import (
"image"
"unicode/utf16"
"unicode/utf8"
@@ -26,8 +27,6 @@ import (
)
// textInputState represents the current state of text inputting.
//
// textInputState is the low-level API. For most use cases, Field is easier to use.
type textInputState struct {
// Text represents the current inputting text.
Text string
@@ -58,12 +57,11 @@ type textInputState struct {
// start starts text inputting.
// start returns a channel to send the state repeatedly, and a function to end the text inputting.
//
// start is the low-level API. For most use cases, Field is easier to use.
//
// start returns nil and nil if the current environment doesn't support this package.
func start(x, y int) (states <-chan textInputState, close func()) {
cx, cy := ui.Get().LogicalPositionToClientPositionInNativePixels(float64(x), float64(y))
return theTextInput.Start(int(cx), int(cy))
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 {

View File

@@ -23,6 +23,8 @@ package textinput
import "C"
import (
"image"
"github.com/ebitengine/purego/objc"
"github.com/hajimehoshi/ebiten/v2/internal/ui"
@@ -118,9 +120,9 @@ type textInput struct {
var theTextInput textInput
func (t *textInput) Start(x, y int) (<-chan textInputState, func()) {
func (t *textInput) Start(bounds image.Rectangle) (<-chan textInputState, func()) {
ui.Get().RunOnMainThread(func() {
t.start(x, y)
t.start(bounds)
})
return t.session.ch, t.session.end
}
@@ -193,7 +195,7 @@ type nsRect struct {
size nsSize
}
func (t *textInput) start(x, y int) {
func (t *textInput) start(bounds image.Rectangle) {
t.endIfNeeded()
tc := getTextInputClient()
@@ -203,10 +205,11 @@ func (t *textInput) start(x, y int) {
window.Send(selMakeFirstResponder, tc)
r := objc.Send[nsRect](contentView, selFrame)
y = int(r.size.height) - y - 4
// The Y dirction is upward in the Cocoa coordinate system.
y := int(r.size.height) - bounds.Max.Y
tc.Send(selSetFrame, nsRect{
origin: nsPoint{float64(x), float64(y)},
size: nsSize{1, 1},
origin: nsPoint{float64(bounds.Min.X), float64(y)},
size: nsSize{float64(bounds.Dx()), float64(bounds.Dy())},
})
session := newSession()

View File

@@ -16,6 +16,7 @@ package textinput
import (
"fmt"
"image"
"syscall/js"
"github.com/hajimehoshi/ebiten/v2/internal/ui"
@@ -156,7 +157,7 @@ body.addEventListener("keyup", handler);`)
// TODO: What about other events like wheel?
}
func (t *textInput) Start(x, y int) (<-chan textInputState, func()) {
func (t *textInput) Start(bounds image.Rectangle) (<-chan textInputState, func()) {
if !t.textareaElement.Truthy() {
return nil, nil
}
@@ -177,8 +178,12 @@ func (t *textInput) Start(x, y int) (<-chan textInputState, func()) {
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))
style.Set("left", fmt.Sprintf("%dpx", bounds.Min.X))
style.Set("top", fmt.Sprintf("%dpx", bounds.Min.Y))
style.Set("width", fmt.Sprintf("%dpx", bounds.Dx()))
style.Set("height", fmt.Sprintf("%dpx", bounds.Dy()))
style.Set("font-size", fmt.Sprintf("%dpx", bounds.Dy()))
style.Set("line-height", fmt.Sprintf("%dpx", bounds.Dy()))
if t.session == nil {
s := newSession()
@@ -200,8 +205,8 @@ func (t *textInput) Start(x, y int) (<-chan textInputState, func()) {
// 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)
js.Global().Get("window").Set("_ebitengine_textinput_y", y)
js.Global().Get("window").Set("_ebitengine_textinput_x", bounds.Min.X)
js.Global().Get("window").Set("_ebitengine_textinput_y", bounds.Max.Y)
return nil, nil
}

View File

@@ -17,6 +17,8 @@
package textinput
import (
"image"
"github.com/hajimehoshi/ebiten/v2"
)
@@ -27,7 +29,7 @@ type textInput struct {
var theTextInput textInput
func (t *textInput) Start(x, y int) (<-chan textInputState, func()) {
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.
tick := ebiten.Tick()

View File

@@ -15,6 +15,7 @@
package textinput
import (
"image"
"sync"
"unsafe"
@@ -41,7 +42,7 @@ type textInput struct {
var theTextInput textInput
func (t *textInput) Start(x, y int) (<-chan textInputState, func()) {
func (t *textInput) Start(bounds image.Rectangle) (<-chan textInputState, func()) {
if microsoftgdk.IsXbox() {
return nil, nil
}
@@ -50,7 +51,7 @@ func (t *textInput) Start(x, y int) (<-chan textInputState, func()) {
var err error
ui.Get().RunOnMainThread(func() {
t.end()
err = t.start(x, y)
err = t.start(bounds)
session = newSession()
t.session = session
})
@@ -76,7 +77,7 @@ func (t *textInput) Start(x, y int) (<-chan textInputState, func()) {
}
// start must be called from the main thread.
func (t *textInput) start(x, y int) error {
func (t *textInput) start(bounds image.Rectangle) error {
if t.err != nil {
return t.err
}
@@ -117,8 +118,14 @@ func (t *textInput) start(x, y int) error {
dwIndex: 0,
dwStyle: _CFS_CANDIDATEPOS,
ptCurrentPos: _POINT{
x: int32(x),
y: int32(y),
x: int32(bounds.Min.X),
y: int32(bounds.Max.Y),
},
rcArea: _RECT{
left: int32(bounds.Min.X),
top: int32(bounds.Max.Y),
right: int32(bounds.Max.X),
bottom: int32(bounds.Max.Y),
},
}); err != nil {
return err