diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2025-10-18 14:44:54 +0200 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2025-10-18 14:47:21 +0200 |
| commit | 03f09542930eb96a657a88e5b57bf47a95ac73da (patch) | |
| tree | 2b3de603b4ad25471da76dc04ce51a5e7f59f8c2 /go/ui/textInput.go | |
| parent | 848dceaef3578c874633611b81eae465c9255f52 (diff) | |
| download | muhqs-game-main.tar.gz muhqs-game-main.zip | |
Diffstat (limited to 'go/ui/textInput.go')
| -rw-r--r-- | go/ui/textInput.go | 260 |
1 files changed, 237 insertions, 23 deletions
diff --git a/go/ui/textInput.go b/go/ui/textInput.go index 3a7939c8..60f749f3 100644 --- a/go/ui/textInput.go +++ b/go/ui/textInput.go @@ -1,67 +1,281 @@ package ui import ( + "image" + "math" + "strings" + "unicode/utf8" + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/exp/textinput" "github.com/hajimehoshi/ebiten/v2/inpututil" + "github.com/hajimehoshi/ebiten/v2/text/v2" + "github.com/hajimehoshi/ebiten/v2/vector" ) +// TextInput is a widget allowing the user to input text. +// It is based on the ebiten examples/textinput licensed under the Apache License, Version 2.0 type TextInput struct { TextBox - input string - label string - focused bool + label string + multiline bool + field textinput.Field } -func (ti *TextInput) InitTextInput(x, y int, width, height int, label string) { +func (ti *TextInput) InitTextInput(x, y int, width, height int, label string, multiline bool) { ti.TextBox = *(NewFixedTextBox(x, y, width, height, "").Centering(true)) - ti.input = "" ti.label = label + ti.multiline = multiline ti.renderImpl = func() *ebiten.Image { - if ti.input == "" { + if ti.field.Text() == "" { ti.text = ti.label } else { - ti.text = ti.input + ti.text = ti.field.TextForRendering() + } + + img := ti.render() + + // Draw focus hint + if ti.field.IsFocused() { + vector.StrokeRect(img, float32(0), float32(0), float32(ti.Width), float32(ti.Height), 1, FocusHintColor, false) } - return ti.render() + // Draw the cursor + selectionStart, _ := ti.field.Selection() + if ti.field.IsFocused() && selectionStart >= 0 { + _cx, _cy := ti.cursorPos() + cx, cy := float64(_cx), float64(_cy) + + w, h := text.Measure(ti.text, ti.font, ti.font.Size*ti.lineSpacing) + if ti.center { + cx += (float64(ti.Width) - w) / 2 + } else { + cx += float64(ti.XMargin) + } + cy += (float64(ti.Height) - h) / 2 + + fMetrics := ti.font.Metrics() + ch := fMetrics.HLineGap + fMetrics.HAscent + fMetrics.HDescent + vector.StrokeLine(img, float32(cx), float32(cy), float32(cx), float32(cy+ch), 1, ti.fg, false) + } + + return img } } -func NewTextInput(x, y int, width, height int, label string) *TextInput { +func NewTextInput(x, y int, width, height int, label string, multiline bool) *TextInput { w := &TextInput{} - w.InitTextInput(x, y, width, height, label) + w.InitTextInput(x, y, width, height, label, multiline) return w } -func (ti *TextInput) SetInput(input string) { - ti.input = input - ti.ForceRedraw() +func (t *TextInput) SetSelectionStartByCursorPosition(x, y int) bool { + idx, ok := t.textIndexByCursorPosition(x, y) + if !ok { + return false + } + t.field.SetSelection(idx, idx) + return true } -func (ti *TextInput) AddInput(input []rune) { - ti.SetInput(ti.input + string(input)) +func (t *TextInput) textIndexByCursorPosition(x, y int) (int, bool) { + if !t.Contains(x, y) { + return 0, false + } + + x -= t.X + y -= t.Y + px, py := t.XMargin, t.YMargin + x -= px + y -= py + if x < 0 { + x = 0 + } + if y < 0 { + y = 0 + } + + fMetrics := t.font.Metrics() + lineSpacingInPixels := int(fMetrics.HLineGap + fMetrics.HAscent + fMetrics.HDescent) + var nlCount int + var lineStart int + var prevAdvance float64 + txt := t.field.Text() + for i, r := range txt { + var x0, x1 int + currentAdvance := text.Advance(txt[lineStart:i], t.font) + if lineStart < i { + x0 = int((prevAdvance + currentAdvance) / 2) + } + if r == '\n' { + x1 = int(math.MaxInt32) + } else if i < len(txt) { + nextI := i + 1 + for !utf8.ValidString(txt[i:nextI]) { + nextI++ + } + nextAdvance := text.Advance(txt[lineStart:nextI], t.font) + x1 = int((currentAdvance + nextAdvance) / 2) + } else { + x1 = int(currentAdvance) + } + if x0 <= x && x < x1 && nlCount*lineSpacingInPixels <= y && y < (nlCount+1)*lineSpacingInPixels { + return i, true + } + prevAdvance = currentAdvance + + if r == '\n' { + nlCount++ + lineStart = i + 1 + prevAdvance = 0 + } + } + + return len(txt), true } -func (ti *TextInput) HandleKey() { - if inpututil.IsKeyJustPressed(ebiten.KeyBackspace) { - if len(ti.input) > 0 { - ti.input = ti.input[:len(ti.input)-1] - ti.ForceRedraw() +func (t *TextInput) cursorPos() (int, int) { + var nlCount int + lastNLPos := -1 + txt := t.field.TextForRendering() + selectionStart, _ := t.field.Selection() + if s, _, ok := t.field.CompositionSelection(); ok { + selectionStart += s + } + txt = txt[:selectionStart] + for i, r := range txt { + if r == '\n' { + nlCount++ + lastNLPos = i } } + + txt = txt[lastNLPos+1:] + x := int(text.Advance(txt, t.font)) + fMetrics := t.font.Metrics() + y := nlCount * int((fMetrics.HLineGap+fMetrics.HAscent+fMetrics.HDescent)*t.lineSpacing) + return x, y +} + +// SetTextAndSelection sets the text and selection of the underlying TextInput field and redraws the widget. +func (t *TextInput) SetTextAndSelection(text string, selectionStartInBytes, selectionEndInBytes int) { + t.field.SetTextAndSelection(text, selectionStartInBytes, selectionEndInBytes) + t.ForceRedraw() +} + +// SetInput replaces the text and sets the selection to the end. +func (t *TextInput) SetInput(input string) { + t.SetTextAndSelection(input, len(input), len(input)) +} + +// AddInput adds input at the current selection and move the the selection. +func (t *TextInput) AddInput(input []rune) { + text := t.field.Text() + selStart, selEnd := t.field.Selection() + text = text[:selStart] + string(input) + text[selEnd:] + t.SetTextAndSelection(text, selStart+len(input), selEnd+len(input)) } func (ti *TextInput) Text() string { - return ti.input + return ti.field.Text() } // return the text or use label as fallback func (ti *TextInput) TextOrLabel() string { - if ti.input != "" { - return ti.input + if input := ti.field.Text(); input != "" { + return input } return ti.label } func (*TextInput) IsFocusable() bool { return true } + +func (ti *TextInput) Focus(focus bool) { + if focus { + ti.field.Focus() + } else { + ti.field.Blur() + } + ti.ForceRedraw() +} + +func (*TextInput) IsUpdatable() bool { return true } +func (t *TextInput) Update() error { + if !t.field.IsFocused() { + return nil + } + + x, y := t.X, t.Y + cx, cy := t.cursorPos() + px, py := t.XMargin, t.YMargin + x0 := x + cx + px + x1 := x0 + 1 + y0 := y + cy + py + fMetrics := t.font.Metrics() + y1 := y0 + int(fMetrics.HLineGap+fMetrics.HAscent+fMetrics.HDescent) + handled, err := t.field.HandleInputWithBounds(image.Rect(x0, y0, x1, y1)) + if err != nil { + return err + } + if handled { + t.ForceRedraw() + return nil + } + + switch { + case inpututil.IsKeyJustPressed(ebiten.KeyEnter): + if t.multiline { + text := t.field.Text() + selectionStart, selectionEnd := t.field.Selection() + text = text[:selectionStart] + "\n" + text[selectionEnd:] + selectionStart += len("\n") + selectionEnd = selectionStart + t.SetTextAndSelection(text, selectionStart, selectionEnd) + } + case inpututil.IsKeyJustPressed(ebiten.KeyBackspace): + text := t.field.Text() + selectionStart, selectionEnd := t.field.Selection() + if selectionStart != selectionEnd { + text = text[:selectionStart] + text[selectionEnd:] + } else if selectionStart > 0 { + // TODO: Remove a grapheme instead of a code point. + _, l := utf8.DecodeLastRuneInString(text[:selectionStart]) + text = text[:selectionStart-l] + text[selectionEnd:] + selectionStart -= l + } + selectionEnd = selectionStart + t.SetTextAndSelection(text, selectionStart, selectionEnd) + case inpututil.IsKeyJustPressed(ebiten.KeyLeft): + text := t.field.Text() + selectionStart, _ := t.field.Selection() + if selectionStart > 0 { + // TODO: Remove a grapheme instead of a code point. + _, l := utf8.DecodeLastRuneInString(text[:selectionStart]) + selectionStart -= l + } + t.SetTextAndSelection(text, selectionStart, selectionStart) + case inpututil.IsKeyJustPressed(ebiten.KeyRight): + text := t.field.Text() + _, selectionEnd := t.field.Selection() + if selectionEnd < len(text) { + // TODO: Remove a grapheme instead of a code point. + _, l := utf8.DecodeRuneInString(text[selectionEnd:]) + selectionEnd += l + } + t.SetTextAndSelection(text, selectionEnd, selectionEnd) + } + + if !t.multiline { + orig := t.field.Text() + new := strings.ReplaceAll(orig, "\n", "") + if new != orig { + selectionStart, selectionEnd := t.field.Selection() + selectionStart -= strings.Count(orig[:selectionStart], "\n") + selectionEnd -= strings.Count(orig[:selectionEnd], "\n") + t.field.SetSelection(selectionStart, selectionEnd) + } + } + + return nil +} |
