2021-08-30 19:48:39 +00:00
|
|
|
package kibodo
|
|
|
|
|
|
|
|
import (
|
|
|
|
"image"
|
|
|
|
"image/color"
|
2021-09-11 03:53:25 +00:00
|
|
|
"log"
|
2021-09-14 02:49:10 +00:00
|
|
|
"time"
|
2021-08-30 19:48:39 +00:00
|
|
|
|
|
|
|
"github.com/hajimehoshi/ebiten/v2"
|
2021-09-11 03:53:25 +00:00
|
|
|
"github.com/hajimehoshi/ebiten/v2/examples/resources/fonts"
|
2021-08-30 19:48:39 +00:00
|
|
|
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
2021-09-11 03:53:25 +00:00
|
|
|
"github.com/hajimehoshi/ebiten/v2/text"
|
|
|
|
"golang.org/x/image/font"
|
|
|
|
"golang.org/x/image/font/opentype"
|
2021-08-30 19:48:39 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// Keyboard is an on-screen keyboard widget.
|
|
|
|
type Keyboard struct {
|
|
|
|
x, y int
|
|
|
|
w, h int
|
|
|
|
|
|
|
|
visible bool
|
|
|
|
alpha float64
|
|
|
|
passPhysical bool
|
|
|
|
allowUserHide bool
|
|
|
|
|
2021-10-19 02:55:39 +00:00
|
|
|
incomingBuffer []rune
|
|
|
|
|
|
|
|
inputEvents []*Input
|
2021-08-30 19:48:39 +00:00
|
|
|
|
2023-11-04 06:31:34 +00:00
|
|
|
keys [][]*Key
|
|
|
|
normalKeys [][]*Key
|
|
|
|
extendedKeys [][]*Key
|
|
|
|
showExtended bool
|
2021-08-30 19:48:39 +00:00
|
|
|
|
2021-09-16 07:37:37 +00:00
|
|
|
backgroundLower *ebiten.Image
|
|
|
|
backgroundUpper *ebiten.Image
|
2021-09-14 02:49:10 +00:00
|
|
|
backgroundDirty bool
|
2021-08-30 19:48:39 +00:00
|
|
|
|
|
|
|
op *ebiten.DrawImageOptions
|
2021-09-08 01:02:04 +00:00
|
|
|
|
2021-09-14 05:43:52 +00:00
|
|
|
backgroundColor color.Color
|
|
|
|
lastBackgroundColor color.Color
|
2021-09-11 03:53:25 +00:00
|
|
|
|
|
|
|
shift bool
|
|
|
|
|
2023-11-05 23:31:32 +00:00
|
|
|
touchIDs []ebiten.TouchID
|
|
|
|
holdTouchID ebiten.TouchID
|
|
|
|
holdKey *Key
|
2024-01-09 20:03:17 +00:00
|
|
|
wasPressed bool
|
2021-09-11 03:53:25 +00:00
|
|
|
|
|
|
|
hideShortcuts []ebiten.Key
|
|
|
|
|
2023-10-24 23:30:02 +00:00
|
|
|
labelFont font.Face
|
2023-10-22 18:44:35 +00:00
|
|
|
lineHeight int
|
|
|
|
lineOffset int
|
2023-11-05 23:31:32 +00:00
|
|
|
|
|
|
|
backspaceDelay time.Duration
|
|
|
|
backspaceRepeat time.Duration
|
|
|
|
backspaceLast time.Time
|
2023-12-14 09:34:08 +00:00
|
|
|
|
|
|
|
scheduleFrameFunc func()
|
2021-08-30 19:48:39 +00:00
|
|
|
}
|
|
|
|
|
2021-09-16 07:37:37 +00:00
|
|
|
// NewKeyboard returns a new Keyboard widget.
|
2021-08-30 19:48:39 +00:00
|
|
|
func NewKeyboard() *Keyboard {
|
2021-09-11 03:53:25 +00:00
|
|
|
fontFace, err := defaultFontFace(64)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
2021-08-30 19:48:39 +00:00
|
|
|
k := &Keyboard{
|
|
|
|
alpha: 1.0,
|
|
|
|
op: &ebiten.DrawImageOptions{
|
|
|
|
Filter: ebiten.FilterNearest,
|
|
|
|
},
|
2021-09-16 07:37:37 +00:00
|
|
|
keys: KeysQWERTY,
|
2023-11-04 06:31:34 +00:00
|
|
|
normalKeys: KeysQWERTY,
|
2021-09-16 07:37:37 +00:00
|
|
|
backgroundLower: ebiten.NewImage(1, 1),
|
|
|
|
backgroundUpper: ebiten.NewImage(1, 1),
|
2021-09-08 01:02:04 +00:00
|
|
|
backgroundColor: color.Black,
|
2023-11-05 23:31:32 +00:00
|
|
|
holdTouchID: -1,
|
2021-09-11 03:53:25 +00:00
|
|
|
labelFont: fontFace,
|
2023-11-05 23:31:32 +00:00
|
|
|
backspaceDelay: 500 * time.Millisecond,
|
|
|
|
backspaceRepeat: 75 * time.Millisecond,
|
2021-08-30 19:48:39 +00:00
|
|
|
}
|
2023-10-22 18:44:35 +00:00
|
|
|
k.fontUpdated()
|
2021-08-30 19:48:39 +00:00
|
|
|
return k
|
|
|
|
}
|
|
|
|
|
2021-09-11 03:53:25 +00:00
|
|
|
func defaultFont() (*opentype.Font, error) {
|
|
|
|
return opentype.Parse(fonts.MPlus1pRegular_ttf)
|
|
|
|
}
|
|
|
|
|
|
|
|
func defaultFontFace(size float64) (font.Face, error) {
|
|
|
|
f, err := defaultFont()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
const dpi = 72 // TODO
|
|
|
|
return opentype.NewFace(f, &opentype.FaceOptions{
|
|
|
|
Size: size,
|
|
|
|
DPI: dpi,
|
|
|
|
Hinting: font.HintingFull,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-08-30 19:48:39 +00:00
|
|
|
// SetRect sets the position and size of the widget.
|
|
|
|
func (k *Keyboard) SetRect(x, y, w, h int) {
|
|
|
|
if k.x == x && k.y == y && k.w == w && k.h == h {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
k.x, k.y, k.w, k.h = x, y, w, h
|
|
|
|
|
|
|
|
k.updateKeyRects()
|
2021-09-14 02:49:10 +00:00
|
|
|
k.backgroundDirty = true
|
2021-08-30 19:48:39 +00:00
|
|
|
}
|
|
|
|
|
2023-10-24 23:30:02 +00:00
|
|
|
// Rect returns the position and size of the widget.
|
|
|
|
func (k *Keyboard) Rect() image.Rectangle {
|
|
|
|
return image.Rect(k.x, k.y, k.x+k.w, k.y+k.h)
|
|
|
|
}
|
|
|
|
|
2021-09-16 07:37:37 +00:00
|
|
|
// GetKeys returns the keys of the keyboard.
|
2021-08-30 19:48:39 +00:00
|
|
|
func (k *Keyboard) GetKeys() [][]*Key {
|
|
|
|
return k.keys
|
|
|
|
}
|
|
|
|
|
2021-09-16 07:37:37 +00:00
|
|
|
// SetKeys sets the keys of the keyboard.
|
2021-08-30 19:48:39 +00:00
|
|
|
func (k *Keyboard) SetKeys(keys [][]*Key) {
|
2023-11-04 06:31:34 +00:00
|
|
|
k.normalKeys = keys
|
2021-08-30 19:48:39 +00:00
|
|
|
|
2023-11-04 08:03:54 +00:00
|
|
|
if !k.showExtended && !keysEqual(keys, k.keys) {
|
2023-11-04 06:31:34 +00:00
|
|
|
k.keys = keys
|
|
|
|
k.updateKeyRects()
|
|
|
|
k.backgroundDirty = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetExtendedKeys sets the keys of the keyboard when the .
|
|
|
|
func (k *Keyboard) SetExtendedKeys(keys [][]*Key) {
|
|
|
|
k.extendedKeys = keys
|
|
|
|
|
2023-11-04 08:03:54 +00:00
|
|
|
if k.showExtended && !keysEqual(keys, k.keys) {
|
2023-11-04 06:31:34 +00:00
|
|
|
k.keys = keys
|
|
|
|
k.updateKeyRects()
|
|
|
|
k.backgroundDirty = true
|
|
|
|
}
|
2021-08-30 19:48:39 +00:00
|
|
|
}
|
|
|
|
|
2023-11-04 08:03:54 +00:00
|
|
|
// SetShowExtended sets whether the normal or extended keyboard is shown.
|
|
|
|
func (k *Keyboard) SetShowExtended(show bool) {
|
|
|
|
if k.showExtended == show {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
k.showExtended = show
|
|
|
|
if k.showExtended {
|
|
|
|
k.keys = k.extendedKeys
|
|
|
|
} else {
|
|
|
|
k.keys = k.normalKeys
|
|
|
|
}
|
|
|
|
k.updateKeyRects()
|
|
|
|
k.backgroundDirty = true
|
|
|
|
}
|
|
|
|
|
2021-09-16 07:37:37 +00:00
|
|
|
// SetLabelFont sets the key label font.
|
2021-09-11 03:53:25 +00:00
|
|
|
func (k *Keyboard) SetLabelFont(face font.Face) {
|
|
|
|
k.labelFont = face
|
2023-10-22 18:44:35 +00:00
|
|
|
k.fontUpdated()
|
2021-09-11 03:53:25 +00:00
|
|
|
|
2021-09-14 02:49:10 +00:00
|
|
|
k.backgroundDirty = true
|
2021-09-11 03:53:25 +00:00
|
|
|
}
|
|
|
|
|
2023-10-22 18:44:35 +00:00
|
|
|
func (k *Keyboard) fontUpdated() {
|
|
|
|
m := k.labelFont.Metrics()
|
|
|
|
k.lineHeight = m.Height.Round()
|
|
|
|
k.lineOffset = m.Ascent.Round()
|
|
|
|
}
|
|
|
|
|
2021-09-16 07:37:37 +00:00
|
|
|
// SetHideShortcuts sets the key shortcuts which, when pressed, will hide the
|
|
|
|
// keyboard.
|
2021-09-11 03:53:25 +00:00
|
|
|
func (k *Keyboard) SetHideShortcuts(shortcuts []ebiten.Key) {
|
|
|
|
k.hideShortcuts = shortcuts
|
|
|
|
}
|
|
|
|
|
2021-08-30 19:48:39 +00:00
|
|
|
func (k *Keyboard) updateKeyRects() {
|
2021-09-14 05:43:52 +00:00
|
|
|
if len(k.keys) == 0 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-09-08 01:02:04 +00:00
|
|
|
maxCells := 0
|
|
|
|
for _, rowKeys := range k.keys {
|
|
|
|
if len(rowKeys) > maxCells {
|
|
|
|
maxCells = len(rowKeys)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-16 07:37:37 +00:00
|
|
|
// TODO user configurable
|
2021-09-17 00:24:34 +00:00
|
|
|
cellPaddingW := 1
|
|
|
|
cellPaddingH := 1
|
2021-09-08 01:02:04 +00:00
|
|
|
|
|
|
|
cellH := (k.h - (cellPaddingH * (len(k.keys) - 1))) / len(k.keys)
|
|
|
|
|
2021-09-14 05:43:52 +00:00
|
|
|
row := 0
|
2023-11-04 06:31:34 +00:00
|
|
|
x, y := 0, 0
|
2021-09-14 05:43:52 +00:00
|
|
|
for _, rowKeys := range k.keys {
|
|
|
|
if len(rowKeys) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2023-11-04 06:31:34 +00:00
|
|
|
availableWidth := k.w
|
|
|
|
for _, key := range rowKeys {
|
|
|
|
if key.Wide {
|
|
|
|
availableWidth = availableWidth / 2
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2021-09-08 01:02:04 +00:00
|
|
|
|
2023-11-04 06:31:34 +00:00
|
|
|
cellW := (availableWidth - (cellPaddingW * (len(rowKeys) - 1))) / len(rowKeys)
|
|
|
|
|
|
|
|
x = 0
|
2021-08-30 19:48:39 +00:00
|
|
|
for i, key := range rowKeys {
|
2023-11-04 06:31:34 +00:00
|
|
|
key.w, key.h = cellW, cellH
|
|
|
|
key.x, key.y = x, y
|
2021-09-14 02:49:10 +00:00
|
|
|
|
|
|
|
if i == len(rowKeys)-1 {
|
|
|
|
key.w = k.w - key.x
|
|
|
|
}
|
2023-11-04 06:31:34 +00:00
|
|
|
|
|
|
|
if key.Wide {
|
|
|
|
key.w = k.w - k.w/2 + (cellW)
|
|
|
|
}
|
|
|
|
|
|
|
|
x += key.w
|
2021-08-30 19:48:39 +00:00
|
|
|
}
|
2021-09-14 05:43:52 +00:00
|
|
|
|
|
|
|
// Count non-empty rows only
|
|
|
|
row++
|
2023-11-04 06:31:34 +00:00
|
|
|
y += (cellH + cellPaddingH)
|
2021-08-30 19:48:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (k *Keyboard) at(x, y int) *Key {
|
|
|
|
if !k.visible {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if x >= k.x && x <= k.x+k.w && y >= k.y && y <= k.y+k.h {
|
|
|
|
x, y = x-k.x, y-k.y // Offset
|
|
|
|
for _, rowKeys := range k.keys {
|
|
|
|
for _, key := range rowKeys {
|
|
|
|
if x >= key.x && x <= key.x+key.w && y >= key.y && y <= key.y+key.h {
|
|
|
|
return key
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-01-09 20:03:17 +00:00
|
|
|
// KeyAt returns the key located at the specified position, or nil if no key is found.
|
|
|
|
func (k *Keyboard) KeyAt(x, y int) *Key {
|
|
|
|
return k.at(x, y)
|
|
|
|
}
|
|
|
|
|
2023-11-04 06:31:34 +00:00
|
|
|
func (k *Keyboard) handleToggleExtendedKey(inputKey ebiten.Key) bool {
|
|
|
|
if inputKey != KeyToggleExtended {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
k.showExtended = !k.showExtended
|
|
|
|
if k.showExtended {
|
|
|
|
k.keys = k.extendedKeys
|
|
|
|
} else {
|
|
|
|
k.keys = k.normalKeys
|
|
|
|
}
|
|
|
|
k.updateKeyRects()
|
|
|
|
k.backgroundDirty = true
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2021-09-11 03:53:25 +00:00
|
|
|
func (k *Keyboard) handleHideKey(inputKey ebiten.Key) bool {
|
|
|
|
if !k.allowUserHide {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, key := range k.hideShortcuts {
|
|
|
|
if key == inputKey {
|
|
|
|
k.Hide()
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2021-09-16 07:37:37 +00:00
|
|
|
// Hit handles a key press.
|
2021-08-30 19:48:39 +00:00
|
|
|
func (k *Keyboard) Hit(key *Key) {
|
2021-09-11 03:53:25 +00:00
|
|
|
input := key.LowerInput
|
|
|
|
if k.shift {
|
|
|
|
input = key.UpperInput
|
|
|
|
}
|
|
|
|
|
|
|
|
if input.Key == ebiten.KeyShift {
|
|
|
|
k.shift = !k.shift
|
2023-12-14 09:34:08 +00:00
|
|
|
if k.scheduleFrameFunc != nil {
|
|
|
|
k.scheduleFrameFunc()
|
|
|
|
}
|
2021-09-11 03:53:25 +00:00
|
|
|
return
|
2023-11-04 06:31:34 +00:00
|
|
|
} else if k.handleToggleExtendedKey(input.Key) || k.handleHideKey(input.Key) {
|
2021-09-11 03:53:25 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-10-19 02:55:39 +00:00
|
|
|
k.inputEvents = append(k.inputEvents, input)
|
2021-08-30 19:48:39 +00:00
|
|
|
}
|
|
|
|
|
2024-01-09 20:03:17 +00:00
|
|
|
// HandleMouse passes the specified mouse event to the on-screen keyboard.
|
|
|
|
func (k *Keyboard) HandleMouse(cursor image.Point, pressed bool, clicked bool) (handled bool, err error) {
|
|
|
|
if k.backgroundDirty {
|
|
|
|
k.drawBackground()
|
|
|
|
k.backgroundDirty = false
|
|
|
|
}
|
|
|
|
|
|
|
|
pressDuration := 50 * time.Millisecond
|
|
|
|
if k.wasPressed && !pressed && !clicked {
|
|
|
|
var key *Key
|
|
|
|
if cursor.X != 0 || cursor.Y != 0 {
|
|
|
|
key = k.at(cursor.X, cursor.Y)
|
|
|
|
} else {
|
|
|
|
PRESSKEY:
|
|
|
|
for _, rowKeys := range k.keys {
|
|
|
|
for _, rowKey := range rowKeys {
|
|
|
|
if rowKey.pressed {
|
|
|
|
key = rowKey
|
|
|
|
break PRESSKEY
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, rowKeys := range k.keys {
|
|
|
|
for _, rowKey := range rowKeys {
|
|
|
|
if key != nil && rowKey == key {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
rowKey.pressed = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if key != nil {
|
|
|
|
key.pressed = true
|
|
|
|
|
|
|
|
k.Hit(key)
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
time.Sleep(pressDuration)
|
|
|
|
|
|
|
|
key.pressed = false
|
|
|
|
if k.scheduleFrameFunc != nil {
|
|
|
|
k.scheduleFrameFunc()
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
k.wasPressed = false
|
|
|
|
} else if pressed {
|
|
|
|
key := k.at(cursor.X, cursor.Y)
|
|
|
|
if key != nil {
|
|
|
|
if !key.pressed {
|
|
|
|
input := key.LowerInput
|
|
|
|
if k.shift {
|
|
|
|
input = key.UpperInput
|
|
|
|
}
|
|
|
|
|
|
|
|
// Repeat backspace and delete operations.
|
|
|
|
if input.Key == ebiten.KeyBackspace || input.Key == ebiten.KeyDelete {
|
|
|
|
k.backspaceLast = time.Now().Add(k.backspaceDelay)
|
|
|
|
}
|
|
|
|
go func() {
|
|
|
|
t := time.NewTicker(k.backspaceRepeat)
|
|
|
|
for {
|
|
|
|
<-t.C
|
|
|
|
|
|
|
|
if !key.pressed {
|
|
|
|
t.Stop()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (input.Key == ebiten.KeyBackspace || input.Key == ebiten.KeyDelete) && time.Since(k.backspaceLast) >= k.backspaceRepeat {
|
|
|
|
k.backspaceLast = time.Now()
|
|
|
|
k.inputEvents = append(k.inputEvents, &Input{Key: input.Key})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
key.pressed = true
|
|
|
|
k.wasPressed = true
|
|
|
|
|
|
|
|
for _, rowKeys := range k.keys {
|
|
|
|
for _, rowKey := range rowKeys {
|
|
|
|
if rowKey == key || !rowKey.pressed {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
rowKey.pressed = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
2022-06-13 21:16:08 +00:00
|
|
|
// Update handles user input. This function is called by Ebitengine.
|
2021-08-30 19:48:39 +00:00
|
|
|
func (k *Keyboard) Update() error {
|
2023-12-14 06:57:14 +00:00
|
|
|
if !k.visible {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-11-02 01:15:32 +00:00
|
|
|
if k.backgroundDirty {
|
|
|
|
k.drawBackground()
|
|
|
|
k.backgroundDirty = false
|
|
|
|
}
|
|
|
|
|
2021-08-30 19:48:39 +00:00
|
|
|
// Pass through physical keyboard input
|
|
|
|
if k.passPhysical {
|
|
|
|
// Read input characters
|
2021-10-19 02:55:39 +00:00
|
|
|
k.incomingBuffer = ebiten.AppendInputChars(k.incomingBuffer[:0])
|
|
|
|
if len(k.incomingBuffer) > 0 {
|
|
|
|
for _, r := range k.incomingBuffer {
|
|
|
|
k.inputEvents = append(k.inputEvents, &Input{Rune: r}) // Pass through
|
2021-08-30 19:48:39 +00:00
|
|
|
}
|
2021-09-14 02:49:10 +00:00
|
|
|
} else {
|
|
|
|
// Read keys
|
|
|
|
for _, key := range allKeys {
|
|
|
|
if inpututil.IsKeyJustPressed(key) {
|
|
|
|
if k.handleHideKey(key) {
|
|
|
|
// Hidden
|
|
|
|
return nil
|
|
|
|
}
|
2021-10-19 02:55:39 +00:00
|
|
|
k.inputEvents = append(k.inputEvents, &Input{Key: key}) // Pass through
|
2021-09-11 03:53:25 +00:00
|
|
|
}
|
|
|
|
}
|
2021-08-30 19:48:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// Handle mouse input
|
2021-09-16 07:37:37 +00:00
|
|
|
pressDuration := 50 * time.Millisecond
|
2021-09-14 02:49:10 +00:00
|
|
|
if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft) {
|
2021-08-30 19:48:39 +00:00
|
|
|
x, y := ebiten.CursorPosition()
|
2021-09-11 03:53:25 +00:00
|
|
|
|
|
|
|
key := k.at(x, y)
|
|
|
|
if key != nil {
|
2021-09-14 02:49:10 +00:00
|
|
|
for _, rowKeys := range k.keys {
|
|
|
|
for _, rowKey := range rowKeys {
|
|
|
|
rowKey.pressed = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
key.pressed = true
|
|
|
|
|
2021-09-11 03:53:25 +00:00
|
|
|
k.Hit(key)
|
2021-09-14 02:49:10 +00:00
|
|
|
|
|
|
|
go func() {
|
2021-09-16 07:37:37 +00:00
|
|
|
time.Sleep(pressDuration)
|
2021-09-14 02:49:10 +00:00
|
|
|
|
|
|
|
key.pressed = false
|
2023-12-14 09:34:08 +00:00
|
|
|
if k.scheduleFrameFunc != nil {
|
|
|
|
k.scheduleFrameFunc()
|
|
|
|
}
|
2021-09-14 02:49:10 +00:00
|
|
|
}()
|
|
|
|
}
|
|
|
|
} else if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
|
|
|
|
x, y := ebiten.CursorPosition()
|
|
|
|
|
|
|
|
key := k.at(x, y)
|
|
|
|
if key != nil {
|
2023-11-05 23:31:32 +00:00
|
|
|
if !key.pressed {
|
|
|
|
input := key.LowerInput
|
|
|
|
if k.shift {
|
|
|
|
input = key.UpperInput
|
|
|
|
}
|
|
|
|
|
|
|
|
// Repeat backspace and delete operations.
|
|
|
|
if input.Key == ebiten.KeyBackspace || input.Key == ebiten.KeyDelete {
|
|
|
|
k.backspaceLast = time.Now().Add(k.backspaceDelay)
|
|
|
|
}
|
|
|
|
go func() {
|
|
|
|
t := time.NewTicker(k.backspaceRepeat)
|
|
|
|
for {
|
|
|
|
<-t.C
|
|
|
|
|
|
|
|
if !key.pressed {
|
|
|
|
t.Stop()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (input.Key == ebiten.KeyBackspace || input.Key == ebiten.KeyDelete) && time.Since(k.backspaceLast) >= k.backspaceRepeat {
|
|
|
|
k.backspaceLast = time.Now()
|
|
|
|
k.inputEvents = append(k.inputEvents, &Input{Key: input.Key})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}()
|
|
|
|
}
|
2021-09-14 02:49:10 +00:00
|
|
|
key.pressed = true
|
2021-09-16 07:37:37 +00:00
|
|
|
|
|
|
|
for _, rowKeys := range k.keys {
|
|
|
|
for _, rowKey := range rowKeys {
|
|
|
|
if rowKey == key || !rowKey.pressed {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
rowKey.pressed = false
|
|
|
|
}
|
|
|
|
}
|
2021-09-11 03:53:25 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// Handle touch input
|
2023-11-05 23:31:32 +00:00
|
|
|
if k.holdTouchID != -1 {
|
|
|
|
x, y := ebiten.TouchPosition(k.holdTouchID)
|
|
|
|
if x == 0 && y == 0 {
|
|
|
|
k.holdTouchID = -1
|
|
|
|
} else {
|
|
|
|
key := k.at(x, y)
|
|
|
|
if key != k.holdKey {
|
|
|
|
k.holdTouchID = -1
|
|
|
|
return nil
|
2021-09-16 07:37:37 +00:00
|
|
|
}
|
2023-11-05 23:31:32 +00:00
|
|
|
//k.Hold(key)
|
|
|
|
k.holdKey = key
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if k.holdTouchID == -1 {
|
|
|
|
k.touchIDs = inpututil.AppendJustPressedTouchIDs(k.touchIDs[:0])
|
|
|
|
for _, id := range k.touchIDs {
|
|
|
|
x, y := ebiten.TouchPosition(id)
|
|
|
|
|
|
|
|
key := k.at(x, y)
|
|
|
|
if key != nil {
|
|
|
|
input := key.LowerInput
|
|
|
|
if k.shift {
|
|
|
|
input = key.UpperInput
|
|
|
|
}
|
2021-09-14 02:49:10 +00:00
|
|
|
|
2023-11-05 23:31:32 +00:00
|
|
|
if !key.pressed {
|
|
|
|
key.pressed = true
|
|
|
|
key.pressedTouchID = id
|
2021-09-14 02:49:10 +00:00
|
|
|
|
2023-11-05 23:31:32 +00:00
|
|
|
for _, rowKeys := range k.keys {
|
|
|
|
for _, rowKey := range rowKeys {
|
|
|
|
if rowKey != key && rowKey.pressed {
|
|
|
|
rowKey.pressed = false
|
|
|
|
}
|
2021-09-16 07:37:37 +00:00
|
|
|
}
|
|
|
|
}
|
2021-09-14 02:49:10 +00:00
|
|
|
|
2023-11-05 23:31:32 +00:00
|
|
|
k.Hit(key)
|
|
|
|
k.holdTouchID = id
|
|
|
|
k.holdKey = key
|
2021-09-16 07:37:37 +00:00
|
|
|
|
2023-11-05 23:31:32 +00:00
|
|
|
// Repeat backspace and delete operations.
|
|
|
|
if input.Key == ebiten.KeyBackspace || input.Key == ebiten.KeyDelete {
|
|
|
|
k.backspaceLast = time.Now().Add(k.backspaceDelay)
|
2021-09-16 07:37:37 +00:00
|
|
|
}
|
2023-11-05 23:31:32 +00:00
|
|
|
|
|
|
|
go func() {
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if found {
|
|
|
|
tx, ty := ebiten.TouchPosition(id)
|
|
|
|
if tx != 0 || ty != 0 {
|
|
|
|
x, y = tx, ty
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if !found {
|
|
|
|
key.pressed = false
|
2023-12-14 09:34:08 +00:00
|
|
|
if k.scheduleFrameFunc != nil {
|
|
|
|
k.scheduleFrameFunc()
|
|
|
|
}
|
2023-11-05 23:31:32 +00:00
|
|
|
t.Stop()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Repeat backspace and delete operations.
|
|
|
|
if (input.Key == ebiten.KeyBackspace || input.Key == ebiten.KeyDelete) && time.Since(k.backspaceLast) >= k.backspaceRepeat {
|
|
|
|
k.backspaceLast = time.Now()
|
|
|
|
k.inputEvents = append(k.inputEvents, &Input{Key: input.Key})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
2021-09-16 07:37:37 +00:00
|
|
|
}
|
2023-11-05 23:31:32 +00:00
|
|
|
}
|
2021-08-30 19:48:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (k *Keyboard) drawBackground() {
|
|
|
|
if k.w == 0 || k.h == 0 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-11-02 01:15:32 +00:00
|
|
|
if !k.backgroundLower.Bounds().Eq(image.Rect(0, 0, k.w, k.h)) || !k.backgroundUpper.Bounds().Eq(image.Rect(0, 0, k.w, k.h)) || k.backgroundColor != k.lastBackgroundColor {
|
2021-09-16 07:37:37 +00:00
|
|
|
k.backgroundLower = ebiten.NewImage(k.w, k.h)
|
|
|
|
k.backgroundUpper = ebiten.NewImage(k.w, k.h)
|
2021-09-14 05:43:52 +00:00
|
|
|
k.lastBackgroundColor = k.backgroundColor
|
|
|
|
}
|
2023-11-04 06:31:34 +00:00
|
|
|
k.backgroundLower.Fill(k.backgroundColor)
|
|
|
|
k.backgroundUpper.Fill(k.backgroundColor)
|
2021-08-30 19:48:39 +00:00
|
|
|
|
2023-10-22 18:44:35 +00:00
|
|
|
halfLineHeight := k.lineHeight / 2
|
|
|
|
|
2023-11-02 01:15:32 +00:00
|
|
|
lightShade := color.RGBA{150, 150, 150, 255}
|
|
|
|
darkShade := color.RGBA{30, 30, 30, 255}
|
|
|
|
|
2023-12-14 06:57:14 +00:00
|
|
|
var keyImage *ebiten.Image
|
2021-09-16 07:37:37 +00:00
|
|
|
for i := 0; i < 2; i++ {
|
|
|
|
shift := i == 1
|
2023-12-14 06:57:14 +00:00
|
|
|
img := k.backgroundLower
|
|
|
|
if shift {
|
|
|
|
img = k.backgroundUpper
|
|
|
|
}
|
2021-09-16 07:37:37 +00:00
|
|
|
for _, rowKeys := range k.keys {
|
|
|
|
for _, key := range rowKeys {
|
2023-12-14 06:57:14 +00:00
|
|
|
r := image.Rect(key.x, key.y, key.x+key.w, key.y+key.h)
|
|
|
|
keyImage = img.SubImage(r).(*ebiten.Image)
|
2021-09-11 03:53:25 +00:00
|
|
|
|
2021-09-17 00:24:34 +00:00
|
|
|
// Draw key background
|
|
|
|
// TODO configurable
|
2023-12-14 06:57:14 +00:00
|
|
|
keyImage.Fill(color.RGBA{90, 90, 90, 255})
|
2021-09-17 00:24:34 +00:00
|
|
|
|
|
|
|
// Draw key label
|
2021-09-16 07:37:37 +00:00
|
|
|
label := key.LowerLabel
|
|
|
|
if shift {
|
|
|
|
label = key.UpperLabel
|
|
|
|
}
|
2021-09-11 03:53:25 +00:00
|
|
|
|
2021-09-16 07:37:37 +00:00
|
|
|
bounds := text.BoundString(k.labelFont, label)
|
|
|
|
x := (key.w - bounds.Dx()) / 2
|
|
|
|
if x < 0 {
|
|
|
|
x = 0
|
|
|
|
}
|
2023-10-22 18:44:35 +00:00
|
|
|
y := halfLineHeight + (key.h-halfLineHeight)/2
|
2023-12-14 06:57:14 +00:00
|
|
|
text.Draw(keyImage, label, k.labelFont, key.x+x, key.y+y, color.White)
|
2021-09-14 02:49:10 +00:00
|
|
|
|
2021-09-17 00:24:34 +00:00
|
|
|
// Draw border
|
2023-12-14 06:57:14 +00:00
|
|
|
keyImage.SubImage(image.Rect(key.x, key.y, key.x+key.w, key.y+1)).(*ebiten.Image).Fill(lightShade)
|
|
|
|
keyImage.SubImage(image.Rect(key.x, key.y, key.x+1, key.y+key.h)).(*ebiten.Image).Fill(lightShade)
|
|
|
|
keyImage.SubImage(image.Rect(key.x, key.y+key.h-1, key.x+key.w, key.y+key.h)).(*ebiten.Image).Fill(darkShade)
|
|
|
|
keyImage.SubImage(image.Rect(key.x+key.w-1, key.y, key.x+key.w, key.y+key.h)).(*ebiten.Image).Fill(darkShade)
|
2021-09-16 07:37:37 +00:00
|
|
|
}
|
2021-08-30 19:48:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-13 21:16:08 +00:00
|
|
|
// Draw draws the widget on the provided image. This function is called by Ebitengine.
|
2021-08-30 19:48:39 +00:00
|
|
|
func (k *Keyboard) Draw(target *ebiten.Image) {
|
2023-12-14 06:57:14 +00:00
|
|
|
if !k.visible {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-09-14 02:49:10 +00:00
|
|
|
if k.backgroundDirty {
|
|
|
|
k.drawBackground()
|
|
|
|
k.backgroundDirty = false
|
|
|
|
}
|
|
|
|
|
2021-09-16 07:37:37 +00:00
|
|
|
var background *ebiten.Image
|
|
|
|
if !k.shift {
|
|
|
|
background = k.backgroundLower
|
|
|
|
} else {
|
|
|
|
background = k.backgroundUpper
|
|
|
|
}
|
|
|
|
|
2021-08-30 19:48:39 +00:00
|
|
|
k.op.GeoM.Reset()
|
|
|
|
k.op.GeoM.Translate(float64(k.x), float64(k.y))
|
|
|
|
k.op.ColorM.Scale(1, 1, 1, k.alpha)
|
2021-09-16 07:37:37 +00:00
|
|
|
target.DrawImage(background, k.op)
|
2021-08-30 19:48:39 +00:00
|
|
|
k.op.ColorM.Reset()
|
2021-09-15 15:21:13 +00:00
|
|
|
|
|
|
|
// Draw pressed keys
|
|
|
|
for _, rowKeys := range k.keys {
|
|
|
|
for _, key := range rowKeys {
|
|
|
|
if !key.pressed {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO buffer to prevent issues with alpha channel
|
|
|
|
k.op.GeoM.Reset()
|
|
|
|
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)
|
|
|
|
|
2021-09-16 07:37:37 +00:00
|
|
|
target.DrawImage(background.SubImage(image.Rect(key.x, key.y, key.x+key.w, key.y+key.h)).(*ebiten.Image), k.op)
|
2021-09-15 15:21:13 +00:00
|
|
|
k.op.ColorM.Reset()
|
2021-09-17 00:24:34 +00:00
|
|
|
|
2021-10-20 01:57:36 +00:00
|
|
|
// Draw shadow.
|
|
|
|
darkShade := color.RGBA{60, 60, 60, 255}
|
|
|
|
subImg := target.SubImage(image.Rect(k.x+key.x, k.y+key.y, k.x+key.x+key.w, k.y+key.y+1)).(*ebiten.Image)
|
|
|
|
subImg.Fill(darkShade)
|
|
|
|
subImg = target.SubImage(image.Rect(k.x+key.x, k.y+key.y, k.x+key.x+1, k.y+key.y+key.h)).(*ebiten.Image)
|
|
|
|
subImg.Fill(darkShade)
|
2021-09-15 15:21:13 +00:00
|
|
|
}
|
|
|
|
}
|
2021-08-30 19:48:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// SetAllowUserHide sets a flag that controls whether the widget may be hidden
|
2021-10-19 02:55:39 +00:00
|
|
|
// by the user.
|
2021-08-30 19:48:39 +00:00
|
|
|
func (k *Keyboard) SetAllowUserHide(allow bool) {
|
|
|
|
k.allowUserHide = allow
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetPassThroughPhysicalInput sets a flag that controls whether physical
|
|
|
|
// keyboard input is passed through to the widget's input buffer. This is not
|
|
|
|
// enabled by default.
|
|
|
|
func (k *Keyboard) SetPassThroughPhysicalInput(pass bool) {
|
|
|
|
k.passPhysical = pass
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetAlpha sets the transparency level of the widget on a scale of 0 to 1.0.
|
|
|
|
func (k *Keyboard) SetAlpha(alpha float64) {
|
|
|
|
k.alpha = alpha
|
|
|
|
}
|
|
|
|
|
2021-10-19 02:55:39 +00:00
|
|
|
// Show shows the widget.
|
|
|
|
func (k *Keyboard) Show() {
|
2021-08-30 19:48:39 +00:00
|
|
|
k.visible = true
|
|
|
|
}
|
|
|
|
|
2021-10-19 02:55:39 +00:00
|
|
|
// Visible returns whether the widget is currently shown.
|
2021-09-11 03:53:25 +00:00
|
|
|
func (k *Keyboard) Visible() bool {
|
|
|
|
return k.visible
|
|
|
|
}
|
|
|
|
|
2021-10-19 02:55:39 +00:00
|
|
|
// Hide hides the widget.
|
2021-08-30 19:48:39 +00:00
|
|
|
func (k *Keyboard) Hide() {
|
|
|
|
k.visible = false
|
2023-11-04 08:03:54 +00:00
|
|
|
if k.showExtended {
|
|
|
|
k.showExtended = false
|
|
|
|
k.keys = k.normalKeys
|
|
|
|
k.updateKeyRects()
|
|
|
|
k.backgroundDirty = true
|
|
|
|
}
|
2021-08-30 19:48:39 +00:00
|
|
|
}
|
2021-10-19 02:55:39 +00:00
|
|
|
|
|
|
|
// AppendInput appends user input that was received since the function was last called.
|
|
|
|
func (k *Keyboard) AppendInput(events []*Input) []*Input {
|
|
|
|
events = append(events, k.inputEvents...)
|
|
|
|
k.inputEvents = nil
|
|
|
|
return events
|
|
|
|
}
|
2023-11-04 08:03:54 +00:00
|
|
|
|
2023-12-14 09:34:08 +00:00
|
|
|
// SetScheduleFrameFunc sets the function called whenever the screen should be redrawn.
|
|
|
|
func (k *Keyboard) SetScheduleFrameFunc(f func()) {
|
|
|
|
k.scheduleFrameFunc = f
|
|
|
|
}
|
|
|
|
|
2023-11-04 08:03:54 +00:00
|
|
|
func keysEqual(a [][]*Key, b [][]*Key) bool {
|
|
|
|
if len(a) != len(b) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for i := range a {
|
|
|
|
if len(a[i]) != len(b[i]) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for j := range b[i] {
|
|
|
|
if a[i][j] != b[i][j] {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|