This repository has been archived on 2024-01-17. You can view files and clone it, but cannot push or open issues or pull requests.
kibodo/keyboard.go

505 lines
11 KiB
Go

package kibodo
import (
"image"
"image/color"
"log"
"time"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/examples/resources/fonts"
"github.com/hajimehoshi/ebiten/v2/inpututil"
"github.com/hajimehoshi/ebiten/v2/text"
"golang.org/x/image/font"
"golang.org/x/image/font/opentype"
)
// Keyboard is an on-screen keyboard widget.
type Keyboard struct {
x, y int
w, h int
visible bool
alpha float64
passPhysical bool
allowUserHide bool
incomingBuffer []rune
inputEvents []*Input
keys [][]*Key
backgroundLower *ebiten.Image
backgroundUpper *ebiten.Image
backgroundDirty bool
op *ebiten.DrawImageOptions
backgroundColor color.Color
lastBackgroundColor color.Color
shift bool
touchIDs []ebiten.TouchID
hideShortcuts []ebiten.Key
labelFont font.Face
}
// NewKeyboard returns a new Keyboard widget.
func NewKeyboard() *Keyboard {
fontFace, err := defaultFontFace(64)
if err != nil {
log.Fatal(err)
}
k := &Keyboard{
alpha: 1.0,
op: &ebiten.DrawImageOptions{
Filter: ebiten.FilterNearest,
},
keys: KeysQWERTY,
backgroundLower: ebiten.NewImage(1, 1),
backgroundUpper: ebiten.NewImage(1, 1),
backgroundColor: color.Black,
labelFont: fontFace,
}
return k
}
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,
})
}
// 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()
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
k.updateKeyRects()
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
}
func (k *Keyboard) updateKeyRects() {
if len(k.keys) == 0 {
return
}
maxCells := 0
for _, rowKeys := range k.keys {
if len(rowKeys) > maxCells {
maxCells = len(rowKeys)
}
}
// TODO user configurable
cellPaddingW := 1
cellPaddingH := 1
cellH := (k.h - (cellPaddingH * (len(k.keys) - 1))) / len(k.keys)
row := 0
for _, rowKeys := range k.keys {
if len(rowKeys) == 0 {
continue
}
cellW := (k.w - (cellPaddingW * (len(rowKeys) - 1))) / len(rowKeys)
for i, key := range rowKeys {
key.x = (cellW + cellPaddingW) * i
key.y = (cellH + cellPaddingH) * row
key.w = cellW
key.h = cellH
if i == len(rowKeys)-1 {
key.w = k.w - key.x
}
}
// Count non-empty rows only
row++
}
}
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
}
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
}
// Hit handles a key press.
func (k *Keyboard) Hit(key *Key) {
input := key.LowerInput
if k.shift {
input = key.UpperInput
}
if input.Key == ebiten.KeyShift {
k.shift = !k.shift
ebiten.ScheduleFrame()
return
} else if k.handleHideKey(input.Key) {
// Hidden
return
}
k.inputEvents = append(k.inputEvents, input)
}
// Update handles user input. This function is called by Ebitengine.
func (k *Keyboard) Update() error {
if !k.visible {
return nil
}
// Pass through physical keyboard input
if k.passPhysical {
// Read input characters
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
}
} else {
// Read keys
for _, key := range allKeys {
if inpututil.IsKeyJustPressed(key) {
if k.handleHideKey(key) {
// Hidden
return nil
}
k.inputEvents = append(k.inputEvents, &Input{Key: key}) // Pass through
}
}
}
}
// Handle mouse input
pressDuration := 50 * time.Millisecond
if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft) {
x, y := ebiten.CursorPosition()
key := k.at(x, y)
if key != nil {
for _, rowKeys := range k.keys {
for _, rowKey := range rowKeys {
rowKey.pressed = false
}
}
key.pressed = true
k.Hit(key)
// TODO replace with pressUntil
go func() {
time.Sleep(pressDuration)
key.pressed = false
ebiten.ScheduleFrame()
}()
}
} else if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
x, y := ebiten.CursorPosition()
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
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 && !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)
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
ebiten.ScheduleFrame()
return
}
}
}()
}
}
return nil
}
func (k *Keyboard) offset(x, y int) (int, int) {
return x + k.x, y + k.y
}
func (k *Keyboard) drawBackground() {
if k.w == 0 || k.h == 0 {
return
}
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
}
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
// TODO configurable
img.Fill(color.RGBA{90, 90, 90, 255})
// Draw key label
label := key.LowerLabel
if shift {
label = key.UpperLabel
}
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 border
lightShade := color.RGBA{150, 150, 150, 255}
darkShade := color.RGBA{30, 30, 30, 255}
for j := 0; j < key.w; j++ {
img.Set(j, 0, lightShade)
img.Set(j, key.h-1, darkShade)
}
for j := 0; j < key.h; j++ {
img.Set(0, j, lightShade)
img.Set(key.w-1, j, darkShade)
}
// 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. This function is called by Ebitengine.
func (k *Keyboard) Draw(target *ebiten.Image) {
if !k.visible {
return
}
if k.backgroundDirty {
k.drawBackground()
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(background, k.op)
k.op.ColorM.Reset()
// 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)
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()
// 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)
}
}
}
// SetAllowUserHide sets a flag that controls whether the widget may be hidden
// by the user.
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
}
// Show shows the widget.
func (k *Keyboard) Show() {
k.visible = true
}
// Visible returns whether the widget is currently shown.
func (k *Keyboard) Visible() bool {
return k.visible
}
// Hide hides the widget.
func (k *Keyboard) Hide() {
k.visible = false
}
// 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
}