You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
504 lines
11 KiB
504 lines
11 KiB
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 Ebiten. |
|
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 Ebiten. |
|
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 |
|
}
|
|
|