diff options
Diffstat (limited to 'go/ui')
| -rw-r--r-- | go/ui/collection.go | 15 | ||||
| -rw-r--r-- | go/ui/colors.go | 1 | ||||
| -rw-r--r-- | go/ui/textBox.go | 2 | ||||
| -rw-r--r-- | go/ui/textInput.go | 260 | ||||
| -rw-r--r-- | go/ui/textInput_generic.go | 15 | ||||
| -rw-r--r-- | go/ui/textInput_wasm.go | 49 |
6 files changed, 239 insertions, 103 deletions
diff --git a/go/ui/collection.go b/go/ui/collection.go index 3de52e11..662d1bbf 100644 --- a/go/ui/collection.go +++ b/go/ui/collection.go @@ -135,11 +135,6 @@ func (c *Collection) switchFocus(fcsbl Widget) { } } -func passInput(ev InputEvent, textInput *TextInput) { - textInput.AddInput(ev.Ctx.(TextCtx).in) - textInput.HandleKey() -} - func (c *Collection) IsUpdatable() bool { return true } @@ -180,10 +175,6 @@ func (c *Collection) Update() error { ConsumeInput(i) } case Text: - if txt, ok := w.(*TextInput); ok { - passInput(ev, txt) - ConsumeInput(i) - } case HoverEnd: if c.hovering != nil { c.hovering.Hover(false, ev.X, ev.Y) @@ -195,12 +186,6 @@ func (c *Collection) Update() error { c.hovering = w } } - // handle focused text input - } else if ev.Kind == Text { - if txt, ok := c.focused.(*TextInput); ok { - passInput(ev, txt) - ConsumeInput(i) - } } } diff --git a/go/ui/colors.go b/go/ui/colors.go index e647b1fc..6da64320 100644 --- a/go/ui/colors.go +++ b/go/ui/colors.go @@ -8,6 +8,7 @@ var ( Gray = color.RGBA{0x80, 0x80, 0x80, 0xff} ResourceBg = color.RGBA{230, 180, 128, 0xff} + FocusHintColor = color.RGBA{0x00, 0x00, 0xff, 0xff} HighlightSelectionColor = color.RGBA{0xc7, 0x89, 0x33, 0xff} HighlightOptionColor = color.RGBA{0xc7, 0x52, 0x33, 0xff} HighlightMovementColor = color.RGBA{27, 59, 217, 0x80} diff --git a/go/ui/textBox.go b/go/ui/textBox.go index 85580816..dd488c40 100644 --- a/go/ui/textBox.go +++ b/go/ui/textBox.go @@ -211,7 +211,7 @@ func NewNumberInput(x, y int, height, width int, number int) *NumberInput { ni := &NumberInput{ n: number, } - ni.InitTextInput(x, y, width, height, strconv.Itoa(number)) + ni.InitTextInput(x, y, width, height, strconv.Itoa(number), false) return ni } 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 +} diff --git a/go/ui/textInput_generic.go b/go/ui/textInput_generic.go deleted file mode 100644 index 7d1004ff..00000000 --- a/go/ui/textInput_generic.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build !wasm - -package ui - -func (ti *TextInput) Focus(focus bool) { - ti.focused = focus -} - -func (*TextInput) IsUpdatable() bool { return true } -func (ti *TextInput) Update() error { - if ti.focused { - } - - return nil -} diff --git a/go/ui/textInput_wasm.go b/go/ui/textInput_wasm.go deleted file mode 100644 index 0ced3ac4..00000000 --- a/go/ui/textInput_wasm.go +++ /dev/null @@ -1,49 +0,0 @@ -package ui - -import ( - "log" - "syscall/js" - "unicode/utf8" -) - -// rmueller's version from: -// ://stackoverflow.com/questions/1752414/how-to-reverse-a-string-in-go?page=1&tab=scoredesc#tab-top -func reverseString(s string) string { - size := len(s) - buf := make([]byte, size) - for start := 0; start < size; { - r, n := utf8.DecodeRuneInString(s[start:]) - start += n - utf8.EncodeRune(buf[size-start:], r) - } - return string(buf) -} - -func (ti *TextInput) Focus(focus bool) { - ti.focused = focus - log.Printf("%v set focus: %v\n", ti, focus) - if focus { - d := js.Global().Get("document") - i := d.Call("getElementById", "hiddenInput") - // i.Call("focus", map[string]any{"preventScroll": true, "focusVisible": false}) - i.Call("focus") - log.Println("open keyboard") - - log.Printf("set hidden input value to %s", ti.Text()) - // i.Set("value", ti.Text()) - i.Set("value", reverseString(ti.Text())) - } -} - -func (ti *TextInput) Update() error { - if ti.focused { - d := js.Global().Get("document") - i := d.Call("getElementById", "hiddenInput") - // t := i.Get("value").String() - t := reverseString(i.Get("value").String()) - ti.SetInput(t) - } - return nil -} - -func (*TextInput) IsUpdatable() bool { return true } |
