diff --git a/examples/keyboard/main.go b/examples/keyboard/main.go index 874e48f2e..b6ecda4c2 100644 --- a/examples/keyboard/main.go +++ b/examples/keyboard/main.go @@ -17,15 +17,18 @@ package main import ( "bytes" "image" + "image/color" _ "image/png" "log" "strings" + "github.com/hajimehoshi/bitmapfont/v2" + "github.com/hajimehoshi/ebiten/v2" - "github.com/hajimehoshi/ebiten/v2/ebitenutil" "github.com/hajimehoshi/ebiten/v2/examples/keyboard/keyboard" rkeyboard "github.com/hajimehoshi/ebiten/v2/examples/resources/images/keyboard" "github.com/hajimehoshi/ebiten/v2/inpututil" + "github.com/hajimehoshi/ebiten/v2/text" ) const ( @@ -78,11 +81,17 @@ func (g *Game) Draw(screen *ebiten.Image) { screen.DrawImage(keyboardImage.SubImage(r).(*ebiten.Image), op) } - keyStrs := []string{} + var keyStrs []string + var keyNames []string for _, k := range g.keys { keyStrs = append(keyStrs, k.String()) + if name := ebiten.KeyName(k); name != "" { + keyNames = append(keyNames, name) + } } - ebitenutil.DebugPrint(screen, strings.Join(keyStrs, ", ")) + + // Use bitmapfont.Face instead of ebitenutil.DebugPrint, since some key names might not be printed with DebugPrint. + text.Draw(screen, strings.Join(keyStrs, ", ")+"\n"+strings.Join(keyNames, ", "), bitmapfont.Face, 8, 12, color.White) } func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { diff --git a/input.go b/input.go index d35b6033c..ddcac2acb 100644 --- a/input.go +++ b/input.go @@ -69,6 +69,19 @@ func IsKeyPressed(key Key) bool { return theInputState.isKeyPressed(key) } +// KeyName returns a key name for the current keyboard layout. +// For example, KeyName(KeyQ) returns 'q' for a QWERTY keyboard, and returns 'a' for an AZERTY keyboard. +// +// KeyName returns an empty string if 1) the key doesn't have a phisical key name, 2) the platform doesn't support KeyName, +// or 3) the main loop doesn't start yet. +// +// KeyName is supported by desktops and browsers. +// +// KeyName is concurrent-safe. +func KeyName(key Key) string { + return ui.KeyName(ui.Key(key)) +} + // CursorPosition returns a position of a mouse cursor relative to the game screen (window). The cursor position is // 'logical' position and this considers the scale of the screen. // diff --git a/internal/glfw/glfw_notwindows.go b/internal/glfw/glfw_notwindows.go index e43b525ec..1b5d3b946 100644 --- a/internal/glfw/glfw_notwindows.go +++ b/internal/glfw/glfw_notwindows.go @@ -273,6 +273,10 @@ func CreateWindow(width, height int, title string, monitor *Monitor, share *Wind return theWindows.add(w), nil } +func GetKeyName(key Key, scancode int) string { + return glfw.GetKeyName(glfw.Key(key), scancode) +} + func GetMonitors() []*Monitor { ms := []*Monitor{} for _, m := range glfw.GetMonitors() { diff --git a/internal/glfw/glfw_windows.go b/internal/glfw/glfw_windows.go index a044a4a9d..314c1c572 100644 --- a/internal/glfw/glfw_windows.go +++ b/internal/glfw/glfw_windows.go @@ -306,6 +306,14 @@ func CreateWindow(width, height int, title string, monitor *Monitor, share *Wind return (*Window)(w), err } +func GetKeyName(key Key, scancode int) string { + name, err := glfwwin.GetKeyName(glfwwin.Key(key), scancode) + if err != nil { + panic(err) + } + return name +} + func GetMonitors() []*Monitor { ms, err := glfwwin.GetMonitors() if err != nil { diff --git a/internal/glfwwin/api_windows.go b/internal/glfwwin/api_windows.go index 92890ea04..f9cdf65ce 100644 --- a/internal/glfwwin/api_windows.go +++ b/internal/glfwwin/api_windows.go @@ -845,6 +845,7 @@ var ( procSetWindowTextW = user32.NewProc("SetWindowTextW") procShowWindow = user32.NewProc("ShowWindow") procSystemParametersInfoW = user32.NewProc("SystemParametersInfoW") + procToUnicode = user32.NewProc("ToUnicode") procTranslateMessage = user32.NewProc("TranslateMessage") procTrackMouseEvent = user32.NewProc("TrackMouseEvent") procUnregisterClassW = user32.NewProc("UnregisterClassW") @@ -1718,6 +1719,24 @@ func _TlsSetValue(dwTlsIndex uint32, lpTlsValue uintptr) error { return nil } +func _ToUnicode(wVirtualKey uint32, wScanCode uint32, keyState []byte, buff []uint16, cchBuff int32, wFlags uint32) int32 { + var lpKeyState *byte + if len(keyState) > 0 { + lpKeyState = &keyState[0] + } + var pwszBuff *uint16 + if len(buff) > 0 { + pwszBuff = &buff[0] + } + + r, _, _ := procToUnicode.Call(uintptr(wVirtualKey), uintptr(wScanCode), uintptr(unsafe.Pointer(lpKeyState)), + uintptr(unsafe.Pointer(pwszBuff)), uintptr(cchBuff), uintptr(wFlags)) + runtime.KeepAlive(lpKeyState) + runtime.KeepAlive(pwszBuff) + + return int32(r) +} + func _TranslateMessage(lpMsg *_MSG) bool { r, _, _ := procTranslateMessage.Call(uintptr(unsafe.Pointer(lpMsg))) return int32(r) != 0 diff --git a/internal/glfwwin/input_windows.go b/internal/glfwwin/input_windows.go index 27aa41ca7..6132a763f 100644 --- a/internal/glfwwin/input_windows.go +++ b/internal/glfwwin/input_windows.go @@ -240,7 +240,20 @@ func RawMouseMotionSupported() (bool, error) { return platformRawMouseMotionSupported(), nil } -// GetKeyName is not implemented. +func GetKeyName(key Key, scancode int) (string, error) { + if !_glfw.initialized { + return "", NotInitialized + } + + if key != KeyUnknown { + if key != KeyKPEqual && (key < KeyKP0 || key > KeyKPAdd) && (key < KeyApostrophe || key > KeyWorld2) { + return "", nil + } + scancode = platformGetKeyScancode(key) + } + + return platformGetScancodeName(scancode) +} func GetKeyScancode(key Key) (int, error) { if !_glfw.initialized { diff --git a/internal/glfwwin/internal_windows.go b/internal/glfwwin/internal_windows.go index 77cce5d49..8c8e19a49 100644 --- a/internal/glfwwin/internal_windows.go +++ b/internal/glfwwin/internal_windows.go @@ -261,6 +261,7 @@ type library struct { clipboardString string keycodes [512]Key scancodes [KeyLast + 1]int + keynames [KeyLast + 1]string // Where to place the cursor when re-enabled restoreCursorPosX float64 diff --git a/internal/glfwwin/win32init_windows.go b/internal/glfwwin/win32init_windows.go index f4c683b5e..65c5d1383 100644 --- a/internal/glfwwin/win32init_windows.go +++ b/internal/glfwwin/win32init_windows.go @@ -151,6 +151,47 @@ func createKeyTables() { } } +func updateKeyNamesWin32() { + for i := range _glfw.win32.keynames { + _glfw.win32.keynames[i] = "" + } + + var state [256]byte + + for key := KeySpace; key <= KeyLast; key++ { + scancode := _glfw.win32.scancodes[key] + if scancode == -1 { + continue + } + + var vk uint32 + if key >= KeyKP0 && key <= KeyKPAdd { + vks := []uint32{ + _VK_NUMPAD0, _VK_NUMPAD1, _VK_NUMPAD2, _VK_NUMPAD3, + _VK_NUMPAD4, _VK_NUMPAD5, _VK_NUMPAD6, _VK_NUMPAD7, + _VK_NUMPAD8, _VK_NUMPAD9, _VK_DECIMAL, _VK_DIVIDE, + _VK_MULTIPLY, _VK_SUBTRACT, _VK_ADD, + } + vk = vks[key-KeyKP0] + } else { + vk = _MapVirtualKeyW(uint32(scancode), _MAPVK_VSC_TO_VK) + } + + var chars [16]uint16 + length := _ToUnicode(vk, uint32(scancode), state[:], chars[:], int32(len(chars)), 0) + if length == -1 { + // This is a dead key, so we need a second simulated key press + // to make it output its own character (usually a diacritic) + length = _ToUnicode(vk, uint32(scancode), state[:], chars[:], int32(len(chars)), 0) + } + if length < 1 { + continue + } + + _glfw.win32.keynames[key] = windows.UTF16ToString(chars[:length]) + } +} + func createHelperWindow() error { h, err := _CreateWindowExW(_WS_EX_OVERLAPPEDWINDOW, _GLFW_WNDCLASSNAME, "GLFW message window", _WS_CLIPSIBLINGS|_WS_CLIPCHILDREN, 0, 0, 1, 1, 0, 0, _glfw.win32.instance, nil) if err != nil { @@ -238,6 +279,7 @@ func platformInit() error { _glfw.win32.instance = _HINSTANCE(m) createKeyTables() + updateKeyNamesWin32() if isWindows10CreatorsUpdateOrGreaterWin32() { if !microsoftgdk.IsXbox() { diff --git a/internal/glfwwin/win32window_windows.go b/internal/glfwwin/win32window_windows.go index 9035b49bc..1fdef41f2 100644 --- a/internal/glfwwin/win32window_windows.go +++ b/internal/glfwwin/win32window_windows.go @@ -690,7 +690,8 @@ func windowProc(hWnd windows.HWND, uMsg uint32, wParam _WPARAM, lParam _LPARAM) return 0 case _WM_INPUTLANGCHANGE: - // Do nothing + updateKeyNamesWin32() + return 0 case _WM_CHAR, _WM_SYSCHAR: if wParam >= 0xd800 && wParam <= 0xdbff { @@ -2204,6 +2205,13 @@ func (w *Window) platformSetCursorMode(mode int) error { return nil } +func platformGetScancodeName(scancode int) (string, error) { + if scancode < 0 || scancode > (_KF_EXTENDED|0xff) || _glfw.win32.keycodes[scancode] == KeyUnknown { + return "", fmt.Errorf("glwfwin: invalid scancode %d: %w", scancode, InvalidValue) + } + return _glfw.win32.keynames[_glfw.win32.keycodes[scancode]], nil +} + func platformGetKeyScancode(key Key) int { return _glfw.win32.scancodes[key] } diff --git a/internal/ui/input_glfw.go b/internal/ui/input_glfw.go index 411d98b96..b258c5075 100644 --- a/internal/ui/input_glfw.go +++ b/internal/ui/input_glfw.go @@ -76,3 +76,24 @@ func (u *userInterfaceImpl) updateInputState() error { } return nil } + +func KeyName(key Key) string { + return theUI.keyName(key) +} + +func (u *userInterfaceImpl) keyName(key Key) string { + if !u.isRunning() { + return "" + } + + gk, ok := uiKeyToGLFWKey[key] + if !ok { + return "" + } + + var name string + u.t.Call(func() { + name = glfw.GetKeyName(gk, 0) + }) + return name +} diff --git a/internal/ui/input_js.go b/internal/ui/input_js.go index 48fbacaee..ef1e97b22 100644 --- a/internal/ui/input_js.go +++ b/internal/ui/input_js.go @@ -167,3 +167,51 @@ func isKeyString(str string) bool { } return true } + +var ( + jsKeyboard = js.Global().Get("navigator").Get("keyboard") + jsKeyboardGetLayoutMap js.Value + jsKeyboardGetLayoutMapCh chan js.Value + jsKeyboardGetLayoutMapCallback js.Func +) + +func init() { + if !jsKeyboard.Truthy() { + return + } + + jsKeyboardGetLayoutMap = jsKeyboard.Get("getLayoutMap").Call("bind", jsKeyboard) + jsKeyboardGetLayoutMapCh = make(chan js.Value, 1) + jsKeyboardGetLayoutMapCallback = js.FuncOf(func(this js.Value, args []js.Value) any { + jsKeyboardGetLayoutMapCh <- args[0] + return nil + }) +} + +func KeyName(key Key) string { + return theUI.keyName(key) +} + +func (u *userInterfaceImpl) keyName(key Key) string { + if !u.running { + return "" + } + + // keyboardLayoutMap is reset every tick. + if u.keyboardLayoutMap.IsUndefined() { + if !jsKeyboard.Truthy() { + return "" + } + + // Invoke getLayoutMap every tick to detect the keyboard change. + // TODO: Calling this every tick might be inefficient. Is there a way to detect a keyboard change? + jsKeyboardGetLayoutMap.Invoke().Call("then", jsKeyboardGetLayoutMapCallback) + u.keyboardLayoutMap = <-jsKeyboardGetLayoutMapCh + } + + n := u.keyboardLayoutMap.Call("get", uiKeyToJSKey[key]) + if n.IsUndefined() { + return "" + } + return n.String() +} diff --git a/internal/ui/input_mobile.go b/internal/ui/input_mobile.go index cfcdd409c..16330e5ef 100644 --- a/internal/ui/input_mobile.go +++ b/internal/ui/input_mobile.go @@ -51,3 +51,8 @@ func (u *userInterfaceImpl) updateInputState(keys map[Key]struct{}, runes []rune } } } + +func KeyName(key Key) string { + // TODO: Implement this. + return "" +} diff --git a/internal/ui/input_nintendosdk.go b/internal/ui/input_nintendosdk.go index fe8355c29..9a76183c9 100644 --- a/internal/ui/input_nintendosdk.go +++ b/internal/ui/input_nintendosdk.go @@ -37,3 +37,7 @@ func (u *userInterfaceImpl) updateInputState() { } } } + +func KeyName(key Key) string { + return "" +} diff --git a/internal/ui/ui_js.go b/internal/ui/ui_js.go index a35ca175d..826e4a098 100644 --- a/internal/ui/ui_js.go +++ b/internal/ui/ui_js.go @@ -91,6 +91,8 @@ type userInterfaceImpl struct { origCursorX int origCursorY int + keyboardLayoutMap js.Value + m sync.Mutex } @@ -686,6 +688,7 @@ func (u *userInterfaceImpl) readInputState(inputState *InputState) { } func (u *userInterfaceImpl) resetForTick() { + u.keyboardLayoutMap = js.Value{} } func (u *userInterfaceImpl) Window() Window {