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 x, y := t.bounds.Min.X, t.bounds.Min.Y
cx, cy := t.cursorPos() cx, cy := t.cursorPos()
px, py := textFieldPadding() px, py := textFieldPadding()
x += cx + px x0 := x + cx + px
y += cy + py + int(fontFace.Metrics().HAscent) x1 := x + cx + px + 6 // To be exact, this should be the width of the current character.
handled, err := t.field.HandleInput(x, y) 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 { if err != nil {
return err return err
} }

View File

@@ -15,6 +15,7 @@
package textinput package textinput
import ( import (
"image"
"sync" "sync"
) )
@@ -99,7 +100,21 @@ type Field struct {
// If HandleInput returns true, a Field user should not handle further input events. // If HandleInput returns true, a Field user should not handle further input events.
// //
// HandleInput returns an error when handling input causes an error. // 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) { 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 { if f.err != nil {
return false, f.err return false, f.err
} }
@@ -113,7 +128,7 @@ func (f *Field) HandleInput(x, y int) (handled bool, err error) {
if f.ch == nil { if f.ch == nil {
// TODO: On iOS Safari, Start doesn't work as expected (#2898). // TODO: On iOS Safari, Start doesn't work as expected (#2898).
// Handle a click event and focus the textarea there. // 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. // Start returns nil for non-supported envrionments, or when unable to start text inputting for some reasons.
if f.ch == nil { if f.ch == nil {
return handled, nil return handled, nil

View File

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

View File

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

View File

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

View File

@@ -17,6 +17,8 @@
package textinput package textinput
import ( import (
"image"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
) )
@@ -27,7 +29,7 @@ type textInput struct {
var theTextInput textInput 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. // AppendInputChars is updated only when the tick is updated.
// If the tick is not updated, return nil immediately. // If the tick is not updated, return nil immediately.
tick := ebiten.Tick() tick := ebiten.Tick()

View File

@@ -15,6 +15,7 @@
package textinput package textinput
import ( import (
"image"
"sync" "sync"
"unsafe" "unsafe"
@@ -41,7 +42,7 @@ type textInput struct {
var theTextInput textInput 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() { if microsoftgdk.IsXbox() {
return nil, nil return nil, nil
} }
@@ -50,7 +51,7 @@ func (t *textInput) Start(x, y int) (<-chan textInputState, func()) {
var err error var err error
ui.Get().RunOnMainThread(func() { ui.Get().RunOnMainThread(func() {
t.end() t.end()
err = t.start(x, y) err = t.start(bounds)
session = newSession() session = newSession()
t.session = session 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. // 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 { if t.err != nil {
return t.err return t.err
} }
@@ -117,8 +118,14 @@ func (t *textInput) start(x, y int) error {
dwIndex: 0, dwIndex: 0,
dwStyle: _CFS_CANDIDATEPOS, dwStyle: _CFS_CANDIDATEPOS,
ptCurrentPos: _POINT{ ptCurrentPos: _POINT{
x: int32(x), x: int32(bounds.Min.X),
y: int32(y), 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 { }); err != nil {
return err return err