Skip to content

Commit 66b4d4e

Browse files
committed
fix: place text field caret at end of preloaded text
1 parent fe9b306 commit 66b4d4e

2 files changed

Lines changed: 94 additions & 6 deletions

File tree

textfield.go

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"unicode/utf8"
1212

1313
"github.com/hajimehoshi/ebiten/v2"
14+
"github.com/hajimehoshi/ebiten/v2/exp/textinput"
1415
"github.com/hajimehoshi/ebiten/v2/inpututil"
1516
)
1617

@@ -41,8 +42,12 @@ func (c *Context) textFieldRaw(buf *string, id widgetID, opt option) (EventHandl
4142

4243
f := c.currentContainer().textInputTextField(id, true)
4344
if c.focus == id {
45+
// A freshly focused text input field still has its own cursor/selection state.
46+
// Seed that state from the bound string before reading input so typing starts
47+
// after any existing text instead of inserting at the beginning.
48+
focusTextInputField(f, *buf)
49+
4450
// handle text input
45-
f.Focus()
4651
x := bounds.Min.X + c.style().padding + textWidth(*buf)
4752
y := bounds.Min.Y + lineHeight()
4853
handled, err := f.HandleInput(x, y)
@@ -58,15 +63,17 @@ func (c *Context) textFieldRaw(buf *string, id widgetID, opt option) (EventHandl
5863
if inpututil.IsKeyJustPressed(ebiten.KeyBackspace) && len(*buf) > 0 {
5964
_, size := utf8.DecodeLastRuneInString(*buf)
6065
*buf = (*buf)[:len(*buf)-size]
61-
f.SetTextAndSelection(*buf, len(*buf), len(*buf))
66+
setTextInputFieldValue(f, *buf)
6267
}
6368
if inpututil.IsKeyJustPressed(ebiten.KeyEnter) {
6469
e = &eventHandler{}
6570
}
6671
}
6772
} else {
6873
if *buf != f.Text() {
69-
f.SetTextAndSelection(*buf, len(*buf), len(*buf))
74+
// Keep the cached text-input object in sync while it is unfocused so the
75+
// next focus starts from the latest value and with the caret at the end.
76+
setTextInputFieldValue(f, *buf)
7077
}
7178
if wasFocused {
7279
e = &eventHandler{}
@@ -100,13 +107,30 @@ func (c *Context) textFieldRaw(buf *string, id widgetID, opt option) (EventHandl
100107
})
101108
}
102109

110+
func focusTextInputField(f *textinput.Field, value string) {
111+
// Focus() does not rewrite the field's text or selection. If this field is being
112+
// focused for the first time, its selection is still 0,0, so copy in the current
113+
// value first and move the caret to the end.
114+
if !f.IsFocused() && value != f.Text() {
115+
setTextInputFieldValue(f, value)
116+
}
117+
f.Focus()
118+
}
119+
120+
func setTextInputFieldValue(f *textinput.Field, value string) {
121+
// Treat programmatic value changes the same way a user expects to keep typing:
122+
// after loading text, place the caret at the end ready for appending.
123+
f.SetTextAndSelection(value, len(value), len(value))
124+
}
125+
103126
// SetTextFieldValue sets the value of the current text field.
127+
// The caret is moved to the end of the new text.
104128
//
105129
// If the last widget is not a text field, this function does nothing.
106130
func (c *Context) SetTextFieldValue(value string) {
107131
_ = c.wrapEventHandlerAndError(func() (EventHandler, error) {
108132
if f := c.currentContainer().textInputTextField(c.currentID, false); f != nil {
109-
f.SetTextAndSelection(value, 0, 0)
133+
setTextInputFieldValue(f, value)
110134
}
111135
return nil, nil
112136
})
@@ -201,7 +225,7 @@ func (c *Context) numberField(value *int, step int, idPart string, opt option) (
201225
if updated {
202226
buf := fmt.Sprintf("%d", *value)
203227
if f := c.currentContainer().textInputTextField(id, false); f != nil {
204-
f.SetTextAndSelection(buf, len(buf), len(buf))
228+
setTextInputFieldValue(f, buf)
205229
}
206230
e = &eventHandler{}
207231
}
@@ -277,7 +301,7 @@ func (c *Context) numberFieldF(value *float64, step float64, digits int, idPart
277301
if updated {
278302
buf := formatNumber(*value, digits)
279303
if f := c.currentContainer().textInputTextField(id, false); f != nil {
280-
f.SetTextAndSelection(buf, len(buf), len(buf))
304+
setTextInputFieldValue(f, buf)
281305
}
282306
e = &eventHandler{}
283307
}

textfield_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+
package debugui
5+
6+
import (
7+
"testing"
8+
9+
"github.com/hajimehoshi/ebiten/v2/exp/textinput"
10+
)
11+
12+
func TestFocusTextInputFieldInitializesTextAndCaretAtEnd(t *testing.T) {
13+
var f textinput.Field
14+
t.Cleanup(f.Blur)
15+
16+
// This is the original regression: focusing a field with preloaded text must
17+
// also initialize the internal selection so the next typed character appends.
18+
focusTextInputField(&f, "hello")
19+
20+
if got, want := f.Text(), "hello"; got != want {
21+
t.Fatalf("text = %q, want %q", got, want)
22+
}
23+
if !f.IsFocused() {
24+
t.Fatal("field is not focused")
25+
}
26+
27+
start, end := f.Selection()
28+
if got, want := start, len("hello"); got != want {
29+
t.Fatalf("selection start = %d, want %d", got, want)
30+
}
31+
if got, want := end, len("hello"); got != want {
32+
t.Fatalf("selection end = %d, want %d", got, want)
33+
}
34+
}
35+
36+
func TestSetTextFieldValueMovesCaretToEnd(t *testing.T) {
37+
var c Context
38+
cnt := &container{}
39+
c.containerStack = []*container{cnt}
40+
41+
id := widgetID{}.push("field")
42+
c.currentID = id
43+
cnt.textInputTextField(id, true)
44+
45+
// SetTextFieldValue is used to replace the visible contents, so it must leave
46+
// the hidden caret state at the end of the new text as well.
47+
c.SetTextFieldValue("loaded")
48+
49+
f := cnt.textInputTextField(id, false)
50+
if f == nil {
51+
t.Fatal("field was not created")
52+
}
53+
if got, want := f.Text(), "loaded"; got != want {
54+
t.Fatalf("text = %q, want %q", got, want)
55+
}
56+
57+
start, end := f.Selection()
58+
if got, want := start, len("loaded"); got != want {
59+
t.Fatalf("selection start = %d, want %d", got, want)
60+
}
61+
if got, want := end, len("loaded"); got != want {
62+
t.Fatalf("selection end = %d, want %d", got, want)
63+
}
64+
}

0 commit comments

Comments
 (0)