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 internalBuffer []rune inputBuffer chan *Input keys [][]*Key background *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. func NewKeyboard() *Keyboard { fontFace, err := defaultFontFace(64) if err != nil { log.Fatal(err) } k := &Keyboard{ alpha: 1.0, op: &ebiten.DrawImageOptions{ Filter: ebiten.FilterNearest, }, background: 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 } func (k *Keyboard) GetKeys() [][]*Key { return k.keys } func (k *Keyboard) SetKeys(keys [][]*Key) { k.keys = keys k.updateKeyRects() k.backgroundDirty = true } func (k *Keyboard) SetLabelFont(face font.Face) { k.labelFont = face k.backgroundDirty = true } 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) } } cellPaddingW := 7 cellPaddingH := 7 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 } func (k *Keyboard) Hit(key *Key) { input := key.LowerInput if k.shift { input = key.UpperInput } if input.Key == ebiten.KeyShift { k.shift = !k.shift k.backgroundDirty = true return } else if k.handleHideKey(input.Key) { // Hidden return } k.inputBuffer <- input } // Update handles user input. func (k *Keyboard) Update() error { if !k.visible { return nil } // Pass through physical keyboard input if k.passPhysical { // Read input characters k.internalBuffer = ebiten.AppendInputChars(k.internalBuffer[:0]) if len(k.internalBuffer) > 0 { for _, r := range k.internalBuffer { k.inputBuffer <- &Input{Rune: r} // Pass through } } else { // Read keys for _, key := range allKeys { if inpututil.IsKeyJustPressed(key) { if k.handleHideKey(key) { // Hidden return nil } k.inputBuffer <- &Input{Key: key} // Pass through } } } } // Handle mouse input 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.backgroundDirty = true k.Hit(key) // TODO replace with pressUntil go func() { time.Sleep(50 * time.Millisecond) key.pressed = false k.backgroundDirty = true ebiten.ScheduleFrame() }() } } else if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { x, y := ebiten.CursorPosition() key := k.at(x, y) if key != nil { key.pressed = true k.backgroundDirty = true } } // 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 = true k.backgroundDirty = true k.Hit(key) // TODO replace with pressUntil go func() { time.Sleep(50 * time.Millisecond) key.pressed = false k.backgroundDirty = true ebiten.ScheduleFrame() }() } } 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.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) k.lastBackgroundColor = k.backgroundColor } for _, rowKeys := range k.keys { for _, key := range rowKeys { label := key.LowerLabel if k.shift { label = key.UpperLabel } // Draw key background img := ebiten.NewImageFromImage(image.NewRGBA(image.Rect(0, 0, key.w, key.h))) img.Fill(color.RGBA{100, 100, 100, 255}) // 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) if key.pressed { k.op.ColorM.Scale(0.75, 0.75, 0.75, 1) } // Draw key k.op.GeoM.Reset() k.op.GeoM.Translate(float64(key.x), float64(key.y)) k.background.DrawImage(img, k.op) k.op.ColorM.Reset() } } } // Draw draws the widget on the provided image. func (k *Keyboard) Draw(target *ebiten.Image) { if !k.visible { return } if k.backgroundDirty { k.drawBackground() k.backgroundDirty = false } 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) k.op.ColorM.Reset() } // SetAllowUserHide sets a flag that controls whether the widget may be hidden // by the user. The input buffer channel is closed when the widget is hidden. 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 and begins sending input to the provided channel. The // channel is closed if the widget is hidden. func (k *Keyboard) Show(inputBuffer chan *Input) { if k.inputBuffer != nil { close(k.inputBuffer) } k.inputBuffer = inputBuffer k.visible = true } func (k *Keyboard) Visible() bool { return k.visible } // Hide hides the widget and closes the input buffer channel. func (k *Keyboard) Hide() { if k.inputBuffer != nil { close(k.inputBuffer) k.inputBuffer = nil } k.visible = false }