diff --git a/demos/kibodo/game/game.go b/demos/kibodo/game/game.go index 360445a..4e1e3c5 100644 --- a/demos/kibodo/game/game.go +++ b/demos/kibodo/game/game.go @@ -2,7 +2,6 @@ package game import ( "fmt" - "image" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/ebitenutil" @@ -73,6 +72,8 @@ func (g *game) showKeyboard() { } else if input.Key == ebiten.KeyEnter { g.userInput = nil continue + } else if input.Key < 0 { + continue } g.userInput = append(g.userInput, []byte("<"+input.Key.String()+">")...) } @@ -89,10 +90,13 @@ func (g *game) Layout(outsideWidth, outsideHeight int) (int, int) { g.w, g.h = outsideWidth, outsideHeight - g.buffer = ebiten.NewImageFromImage(image.NewRGBA(image.Rect(0, 0, g.w, g.h))) + g.buffer = ebiten.NewImage(g.w, g.h) - sizeH := 200 - g.k.SetRect(0, sizeH, g.w, g.h-sizeH) + y := 200 + if g.h > g.w && (g.h-g.w) > 200 { + y = g.h - g.w + } + g.k.SetRect(0, y, g.w, g.h-y) return outsideWidth, outsideHeight } diff --git a/go.mod b/go.mod index 8ac7475..13d4455 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,16 @@ module code.rocketnine.space/tslocum/kibodo go 1.17 require ( - github.com/hajimehoshi/ebiten/v2 v2.2.0-alpha.13.0.20210909171729-37771717cc52 + github.com/hajimehoshi/ebiten/v2 v2.2.0-alpha.14.0.20210915040438-ec2f82342091 golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d ) require ( github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be // indirect - golang.org/x/exp v0.0.0-20210910231120-3d0173ecaa1e // indirect + github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240 // indirect + golang.org/x/exp v0.0.0-20210915225539-aeb18aa42a84 // indirect golang.org/x/mobile v0.0.0-20210902104108-5d9a33257ab5 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect - golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 // indirect + golang.org/x/sys v0.0.0-20210915083310-ed5796bab164 // indirect golang.org/x/text v0.3.7 // indirect ) diff --git a/go.sum b/go.sum index ef399b3..132728d 100644 --- a/go.sum +++ b/go.sum @@ -98,12 +98,12 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hajimehoshi/bitmapfont/v2 v2.1.3 h1:JefUkL0M4nrdVwVq7MMZxSTh6mSxOylm+C4Anoucbb0= github.com/hajimehoshi/bitmapfont/v2 v2.1.3/go.mod h1:2BnYrkTQGThpr/CY6LorYtt/zEPNzvE/ND69CRTaHMs= -github.com/hajimehoshi/ebiten/v2 v2.2.0-alpha.13.0.20210909171729-37771717cc52 h1:FVGJlUvpO/i9TiN/S1kpkPxIge7nR7R15Ux0zF3jr+4= -github.com/hajimehoshi/ebiten/v2 v2.2.0-alpha.13.0.20210909171729-37771717cc52/go.mod h1:44O6eBPGyRv8YctRbfzaqUH2sek5UdXh0aLWOP02ELI= +github.com/hajimehoshi/ebiten/v2 v2.2.0-alpha.14.0.20210915040438-ec2f82342091 h1:z3+aOqAqZhE0ppbhy89l5T3RDB7d+S9TiyGbtVJ3NvA= +github.com/hajimehoshi/ebiten/v2 v2.2.0-alpha.14.0.20210915040438-ec2f82342091/go.mod h1:fS7PLZeV3mclX0J6qubENa9ms3NWmZdNJkCOeEHmF74= github.com/hajimehoshi/file2byteslice v0.0.0-20200812174855-0e5e8a80490e/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE= github.com/hajimehoshi/go-mp3 v0.3.2/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= -github.com/hajimehoshi/oto/v2 v2.0.0-alpha.2/go.mod h1:rUKQmwMkqmRxe+IAof9+tuYA2ofm8cAWXFmSfzDN8vQ= +github.com/hajimehoshi/oto/v2 v2.1.0-alpha.0.20210912073017-18657977e3dc/go.mod h1:rUKQmwMkqmRxe+IAof9+tuYA2ofm8cAWXFmSfzDN8vQ= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -129,6 +129,8 @@ github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmK github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/jakecoffman/cp v1.1.0/go.mod h1:JjY/Fp6d8E1CHnu74gWNnU0+b9VzEdUVPoJxg2PsTQg= +github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240 h1:dy+DS31tGEGCsZzB45HmJJNHjur8GDgtRNX9U7HnSX4= +github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240/go.mod h1:3P4UH/k22rXyHIJD2w4h2XMqPX4Of/eySEZq9L6wqc4= github.com/jfreymuth/oggvorbis v1.0.3/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII= github.com/jfreymuth/vorbis v1.0.2/go.mod h1:DoftRo4AznKnShRl1GxiTFCseHr4zR9BN3TWXyuzrqQ= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= @@ -283,8 +285,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= -golang.org/x/exp v0.0.0-20210910231120-3d0173ecaa1e h1:W9ijN+u/MsYtKK8c9OVJM3Wc3GyrQO2hlRdbrwyzJMc= -golang.org/x/exp v0.0.0-20210910231120-3d0173ecaa1e/go.mod h1:a3o/VtDNHN+dCVLEpzjjUHOzR+Ln3DHX056ZPzoZGGA= +golang.org/x/exp v0.0.0-20210915225539-aeb18aa42a84 h1:rZZoVfcp9bNtyV7GbMqs6qTtgn/htRKdyfzKQ4EiLFE= +golang.org/x/exp v0.0.0-20210915225539-aeb18aa42a84/go.mod h1:a3o/VtDNHN+dCVLEpzjjUHOzR+Ln3DHX056ZPzoZGGA= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -360,8 +362,8 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210820121016-41cdb8703e55/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 h1:xrCZDmdtoloIiooiA9q0OQb9r8HejIHYoHGhGCe1pGg= -golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210915083310-ed5796bab164 h1:7ZDGnxgHAMw7thfC5bEos0RDAccZKxioiWBhfIe+tvw= +golang.org/x/sys v0.0.0-20210915083310-ed5796bab164/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= diff --git a/key.go b/key.go index 9b6cf65..b7d4968 100644 --- a/key.go +++ b/key.go @@ -4,6 +4,7 @@ import ( "github.com/hajimehoshi/ebiten/v2" ) +// Key represents a virtual key. type Key struct { LowerLabel string UpperLabel string @@ -13,9 +14,11 @@ type Key struct { x, y int w, h int - pressed bool + pressed bool + pressedTouchID ebiten.TouchID } +// Input represents the input from a key press. type Input struct { Rune rune Key ebiten.Key diff --git a/keyboard.go b/keyboard.go index 454a0c4..ad23d3c 100644 --- a/keyboard.go +++ b/keyboard.go @@ -29,7 +29,8 @@ type Keyboard struct { keys [][]*Key - background *ebiten.Image + backgroundLower *ebiten.Image + backgroundUpper *ebiten.Image backgroundDirty bool op *ebiten.DrawImageOptions @@ -46,7 +47,7 @@ type Keyboard struct { labelFont font.Face } -// NewKeyboard returns a new Keyboard. +// NewKeyboard returns a new Keyboard widget. func NewKeyboard() *Keyboard { fontFace, err := defaultFontFace(64) if err != nil { @@ -58,7 +59,9 @@ func NewKeyboard() *Keyboard { op: &ebiten.DrawImageOptions{ Filter: ebiten.FilterNearest, }, - background: ebiten.NewImage(1, 1), + keys: KeysQWERTY, + backgroundLower: ebiten.NewImage(1, 1), + backgroundUpper: ebiten.NewImage(1, 1), backgroundColor: color.Black, labelFont: fontFace, } @@ -93,10 +96,12 @@ func (k *Keyboard) SetRect(x, y, w, h int) { k.backgroundDirty = true } +// GetKeys returns the keys of the keyboard. func (k *Keyboard) GetKeys() [][]*Key { return k.keys } +// SetKeys sets the keys of the keyboard. func (k *Keyboard) SetKeys(keys [][]*Key) { k.keys = keys @@ -104,12 +109,15 @@ func (k *Keyboard) SetKeys(keys [][]*Key) { k.backgroundDirty = true } +// SetLabelFont sets the key label font. func (k *Keyboard) SetLabelFont(face font.Face) { k.labelFont = face k.backgroundDirty = true } +// SetHideShortcuts sets the key shortcuts which, when pressed, will hide the +// keyboard. func (k *Keyboard) SetHideShortcuts(shortcuts []ebiten.Key) { k.hideShortcuts = shortcuts } @@ -126,8 +134,9 @@ func (k *Keyboard) updateKeyRects() { } } - cellPaddingW := 7 - cellPaddingH := 7 + // TODO user configurable + cellPaddingW := 2 + cellPaddingH := 2 cellH := (k.h - (cellPaddingH * (len(k.keys) - 1))) / len(k.keys) @@ -186,6 +195,7 @@ func (k *Keyboard) handleHideKey(inputKey ebiten.Key) bool { return false } +// Hit handles a key press. func (k *Keyboard) Hit(key *Key) { input := key.LowerInput if k.shift { @@ -194,7 +204,7 @@ func (k *Keyboard) Hit(key *Key) { if input.Key == ebiten.KeyShift { k.shift = !k.shift - k.backgroundDirty = true + ebiten.ScheduleFrame() return } else if k.handleHideKey(input.Key) { // Hidden @@ -204,7 +214,7 @@ func (k *Keyboard) Hit(key *Key) { k.inputBuffer <- input } -// Update handles user input. +// Update handles user input. This function is called by Ebiten. func (k *Keyboard) Update() error { if !k.visible { return nil @@ -232,6 +242,7 @@ func (k *Keyboard) Update() error { } } // Handle mouse input + pressDuration := 50 * time.Millisecond if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft) { x, y := ebiten.CursorPosition() @@ -248,7 +259,7 @@ func (k *Keyboard) Update() error { // TODO replace with pressUntil go func() { - time.Sleep(50 * time.Millisecond) + time.Sleep(pressDuration) key.pressed = false ebiten.ScheduleFrame() @@ -260,6 +271,15 @@ func (k *Keyboard) Update() error { key := k.at(x, y) if key != nil { key.pressed = true + + for _, rowKeys := range k.keys { + for _, rowKey := range rowKeys { + if rowKey == key || !rowKey.pressed { + continue + } + rowKey.pressed = false + } + } } } // Handle touch input @@ -268,17 +288,47 @@ func (k *Keyboard) Update() error { x, y := ebiten.TouchPosition(id) key := k.at(x, y) - if key != nil { + if key != nil && !key.pressed { key.pressed = true + key.pressedTouchID = id + + for _, rowKeys := range k.keys { + for _, rowKey := range rowKeys { + if rowKey != key && rowKey.pressed { + rowKey.pressed = false + } + } + } k.Hit(key) - // TODO replace with pressUntil go func() { - time.Sleep(50 * time.Millisecond) + var touchIDs []ebiten.TouchID + t := time.NewTicker(pressDuration) + for range t.C { + touchIDs = ebiten.AppendTouchIDs(touchIDs[:0]) + + var found bool + for _, touchID := range touchIDs { + if id == touchID { + found = true + break + } + } - key.pressed = false - ebiten.ScheduleFrame() + if found { + tx, ty := ebiten.TouchPosition(id) + if tx != 0 || ty != 0 { + x, y = tx, ty + } + } + + if !found { + key.pressed = false + ebiten.ScheduleFrame() + return + } + } }() } } @@ -294,44 +344,63 @@ func (k *Keyboard) drawBackground() { return } - if k.background == nil || k.background.Bounds() != image.Rect(0, 0, k.w, k.h) || k.backgroundColor != k.lastBackgroundColor { - k.background = ebiten.NewImageFromImage(image.NewRGBA(image.Rect(0, 0, k.w, k.h))) - k.background.Fill(k.backgroundColor) + if k.backgroundLower.Bounds() != image.Rect(0, 0, k.w, k.h) || k.backgroundUpper.Bounds() != image.Rect(0, 0, k.w, k.h) || k.backgroundColor != k.lastBackgroundColor { + k.backgroundLower = ebiten.NewImage(k.w, k.h) + k.backgroundLower.Fill(k.backgroundColor) + + k.backgroundUpper = ebiten.NewImage(k.w, k.h) + k.backgroundUpper.Fill(k.backgroundColor) k.lastBackgroundColor = k.backgroundColor } - for _, rowKeys := range k.keys { - for _, key := range rowKeys { - label := key.LowerLabel - if k.shift { - label = key.UpperLabel - } + var img *ebiten.Image + for i := 0; i < 2; i++ { + shift := i == 1 + for _, rowKeys := range k.keys { + for _, key := range rowKeys { + if img == nil { + img = ebiten.NewImage(key.w, key.h) + } else { + bounds := img.Bounds() + if bounds.Dx() != key.w || bounds.Dy() != key.h { + img = ebiten.NewImage(key.w, key.h) + } + } - // Draw key background - img := ebiten.NewImageFromImage(image.NewRGBA(image.Rect(0, 0, key.w, key.h))) - img.Fill(color.RGBA{100, 100, 100, 255}) + label := key.LowerLabel + if shift { + label = key.UpperLabel + } - // Draw key label - bounds := text.BoundString(k.labelFont, label) - x := (key.w - bounds.Dx()) / 2 - if x < 0 { - x = 0 - } - y := key.h / 2 - text.Draw(img, label, k.labelFont, x, y, color.White) + // Draw key background + img.Fill(color.RGBA{100, 100, 100, 255}) - // Draw key - k.op.GeoM.Reset() - k.op.GeoM.Translate(float64(key.x), float64(key.y)) - k.background.DrawImage(img, k.op) + // Draw key label + bounds := text.BoundString(k.labelFont, label) + x := (key.w - bounds.Dx()) / 2 + if x < 0 { + x = 0 + } + y := key.h / 2 + text.Draw(img, label, k.labelFont, x, y, color.White) - k.op.ColorM.Reset() + // Draw key + k.op.GeoM.Reset() + k.op.GeoM.Translate(float64(key.x), float64(key.y)) + + if !shift { + k.backgroundLower.DrawImage(img, k.op) + } else { + k.backgroundUpper.DrawImage(img, k.op) + } + k.op.ColorM.Reset() + } } } } -// Draw draws the widget on the provided image. +// Draw draws the widget on the provided image. This function is called by Ebiten. func (k *Keyboard) Draw(target *ebiten.Image) { if !k.visible { return @@ -342,10 +411,17 @@ func (k *Keyboard) Draw(target *ebiten.Image) { k.backgroundDirty = false } + var background *ebiten.Image + if !k.shift { + background = k.backgroundLower + } else { + background = k.backgroundUpper + } + k.op.GeoM.Reset() k.op.GeoM.Translate(float64(k.x), float64(k.y)) k.op.ColorM.Scale(1, 1, 1, k.alpha) - target.DrawImage(k.background, k.op) + target.DrawImage(background, k.op) k.op.ColorM.Reset() // Draw pressed keys @@ -360,7 +436,7 @@ func (k *Keyboard) Draw(target *ebiten.Image) { k.op.GeoM.Translate(float64(k.x+key.x), float64(k.y+key.y)) k.op.ColorM.Scale(0.75, 0.75, 0.75, k.alpha) - target.DrawImage(k.background.SubImage(image.Rect(key.x, key.y, key.x+key.w, key.y+key.h)).(*ebiten.Image), k.op) + target.DrawImage(background.SubImage(image.Rect(key.x, key.y, key.x+key.w, key.y+key.h)).(*ebiten.Image), k.op) k.op.ColorM.Reset() } } @@ -394,6 +470,7 @@ func (k *Keyboard) Show(inputBuffer chan *Input) { k.visible = true } +// Visible returns whether the keyboard is currently shown. func (k *Keyboard) Visible() bool { return k.visible } diff --git a/keyboard_test.go b/keyboard_test.go index 824d29a..39177ce 100644 --- a/keyboard_test.go +++ b/keyboard_test.go @@ -1,15 +1,24 @@ package kibodo import ( + "runtime" "testing" + "time" + + "github.com/hajimehoshi/ebiten/v2" ) -func BenchmarkKeyboard_Draw(b *testing.B) { - ch := make(chan *Input, 10) +// TODO test presses registered - k := NewKeyboard() - k.SetRect(0, 0, 100, 100) - k.Show(ch) +func TestKeyboard_Draw(t *testing.T) { + k, _ := newTestKeyboard() + + // Warm caches + k.drawBackground() +} + +func BenchmarkKeyboard_Draw(b *testing.B) { + k, _ := newTestKeyboard() // Warm caches k.drawBackground() @@ -20,3 +29,60 @@ func BenchmarkKeyboard_Draw(b *testing.B) { k.drawBackground() } } + +func BenchmarkKeyboard_Press(b *testing.B) { + go func() { + time.Sleep(2 * time.Second) + + k, _ := newTestKeyboard() + + // Warm caches + k.drawBackground() + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + k.drawBackground() + k.keys[0][0].pressed = true + k.drawBackground() + k.keys[0][0].pressed = false + } + }() + + runtime.LockOSThread() + + err := ebiten.RunGame(NewDummyGame()) + if err != nil { + b.Error(err) + } +} + +func newTestKeyboard() (*Keyboard, chan *Input) { + ch := make(chan *Input, 10) + + k := NewKeyboard() + k.SetRect(0, 0, 300, 100) + k.Show(ch) + + return k, ch +} + +type DummyGame struct { + ready bool +} + +func (d *DummyGame) Update() error { + return nil +} + +func (d *DummyGame) Draw(screen *ebiten.Image) { + d.ready = true +} + +func (d *DummyGame) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { + return outsideWidth, outsideHeight +} + +func NewDummyGame() *DummyGame { + return &DummyGame{} +} diff --git a/keys.go b/keys.go index e24ab19..e9d3760 100644 --- a/keys.go +++ b/keys.go @@ -2,6 +2,7 @@ package kibodo import "github.com/hajimehoshi/ebiten/v2" +// KeysQWERTY is a standard QWERTY keyboard layout. var KeysQWERTY = [][]*Key{ { {