123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331 |
- // Copyright 2023 The Ebitengine Authors
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- package main
- import (
- "image"
- "image/color"
- "log"
- "math"
- "strings"
- "unicode/utf8"
- "github.com/hajimehoshi/bitmapfont/v3"
- "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"
- )
- var fontFace = text.NewGoXFace(bitmapfont.FaceEA)
- const (
- screenWidth = 640
- screenHeight = 480
- )
- type TextField struct {
- bounds image.Rectangle
- multilines bool
- field textinput.Field
- }
- func NewTextField(bounds image.Rectangle, multilines bool) *TextField {
- return &TextField{
- bounds: bounds,
- multilines: multilines,
- }
- }
- func (t *TextField) Contains(x, y int) bool {
- return image.Pt(x, y).In(t.bounds)
- }
- func (t *TextField) SetSelectionStartByCursorPosition(x, y int) bool {
- idx, ok := t.textIndexByCursorPosition(x, y)
- if !ok {
- return false
- }
- t.field.SetSelection(idx, idx)
- return true
- }
- func (t *TextField) textIndexByCursorPosition(x, y int) (int, bool) {
- if !t.Contains(x, y) {
- return 0, false
- }
- x -= t.bounds.Min.X
- y -= t.bounds.Min.Y
- px, py := textFieldPadding()
- x -= px
- y -= py
- if x < 0 {
- x = 0
- }
- if y < 0 {
- y = 0
- }
- lineSpacingInPixels := int(fontFace.Metrics().HLineGap + fontFace.Metrics().HAscent + fontFace.Metrics().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], fontFace)
- 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], fontFace)
- 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 (t *TextField) Focus() {
- t.field.Focus()
- }
- func (t *TextField) Blur() {
- t.field.Blur()
- }
- func (t *TextField) Update() error {
- if !t.field.IsFocused() {
- return nil
- }
- 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)
- if err != nil {
- return err
- }
- if handled {
- return nil
- }
- switch {
- case inpututil.IsKeyJustPressed(ebiten.KeyEnter):
- if t.multilines {
- text := t.field.Text()
- selectionStart, selectionEnd := t.field.Selection()
- text = text[:selectionStart] + "\n" + text[selectionEnd:]
- selectionStart += len("\n")
- selectionEnd = selectionStart
- t.field.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.field.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.field.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.field.SetTextAndSelection(text, selectionEnd, selectionEnd)
- }
- if !t.multilines {
- 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
- }
- func (t *TextField) 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, fontFace))
- y := nlCount * int(fontFace.Metrics().HLineGap+fontFace.Metrics().HAscent+fontFace.Metrics().HDescent)
- return x, y
- }
- func (t *TextField) Draw(screen *ebiten.Image) {
- vector.DrawFilledRect(screen, float32(t.bounds.Min.X), float32(t.bounds.Min.Y), float32(t.bounds.Dx()), float32(t.bounds.Dy()), color.White, false)
- var clr color.Color = color.Black
- if t.field.IsFocused() {
- clr = color.RGBA{0, 0, 0xff, 0xff}
- }
- vector.StrokeRect(screen, float32(t.bounds.Min.X), float32(t.bounds.Min.Y), float32(t.bounds.Dx()), float32(t.bounds.Dy()), 1, clr, false)
- px, py := textFieldPadding()
- selectionStart, _ := t.field.Selection()
- if t.field.IsFocused() && selectionStart >= 0 {
- x, y := t.bounds.Min.X, t.bounds.Min.Y
- cx, cy := t.cursorPos()
- x += px + cx
- y += py + cy
- h := int(fontFace.Metrics().HLineGap + fontFace.Metrics().HAscent + fontFace.Metrics().HDescent)
- vector.StrokeLine(screen, float32(x), float32(y), float32(x), float32(y+h), 1, color.Black, false)
- }
- tx := t.bounds.Min.X + px
- ty := t.bounds.Min.Y + py
- op := &text.DrawOptions{}
- op.GeoM.Translate(float64(tx), float64(ty))
- op.ColorScale.ScaleWithColor(color.Black)
- op.LineSpacing = fontFace.Metrics().HLineGap + fontFace.Metrics().HAscent + fontFace.Metrics().HDescent
- text.Draw(screen, t.field.TextForRendering(), fontFace, op)
- }
- const textFieldHeight = 24
- func textFieldPadding() (int, int) {
- m := fontFace.Metrics()
- return 4, (textFieldHeight - int(m.HLineGap+m.HAscent+m.HDescent)) / 2
- }
- type Game struct {
- textFields []*TextField
- }
- func (g *Game) Update() error {
- if g.textFields == nil {
- g.textFields = append(g.textFields, NewTextField(image.Rect(16, 16, screenWidth-16, 16+textFieldHeight), false))
- g.textFields = append(g.textFields, NewTextField(image.Rect(16, 48, screenWidth-16, 48+textFieldHeight), false))
- g.textFields = append(g.textFields, NewTextField(image.Rect(16, 80, screenWidth-16, screenHeight-16), true))
- }
- ids := inpututil.AppendJustPressedTouchIDs(nil)
- if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) || len(ids) > 0 {
- var x, y int
- if len(ids) > 0 {
- x, y = ebiten.TouchPosition(ids[0])
- } else {
- x, y = ebiten.CursorPosition()
- }
- for _, tf := range g.textFields {
- if tf.Contains(x, y) {
- tf.Focus()
- tf.SetSelectionStartByCursorPosition(x, y)
- } else {
- tf.Blur()
- }
- }
- }
- for _, tf := range g.textFields {
- if err := tf.Update(); err != nil {
- return err
- }
- }
- x, y := ebiten.CursorPosition()
- var inTextField bool
- for _, tf := range g.textFields {
- if tf.Contains(x, y) {
- inTextField = true
- break
- }
- }
- if inTextField {
- ebiten.SetCursorShape(ebiten.CursorShapeText)
- } else {
- ebiten.SetCursorShape(ebiten.CursorShapeDefault)
- }
- return nil
- }
- func (g *Game) Draw(screen *ebiten.Image) {
- screen.Fill(color.RGBA{0xcc, 0xcc, 0xcc, 0xff})
- for _, tf := range g.textFields {
- tf.Draw(screen)
- }
- }
- func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
- return screenWidth, screenHeight
- }
- func main() {
- ebiten.SetWindowSize(screenWidth, screenHeight)
- ebiten.SetWindowTitle("Text Input (Ebitengine Demo)")
- if err := ebiten.RunGame(&Game{}); err != nil {
- log.Fatal(err)
- }
- }
|