From 39bb92da03f41a867c452009f7b0d2abe595e707 Mon Sep 17 00:00:00 2001 From: Eric H Date: Sat, 1 Aug 2026 21:39:20 -0400 Subject: [PATCH 01/28] refactor keyscan to make easier to add gamepad scanning --- scan_key.go | 40 ++++++++++++++++++++---------------- scan_key_test.go | 53 ------------------------------------------------ 2 files changed, 22 insertions(+), 71 deletions(-) delete mode 100644 scan_key_test.go diff --git a/scan_key.go b/scan_key.go index 8e6d2ba..f41d745 100644 --- a/scan_key.go +++ b/scan_key.go @@ -63,7 +63,20 @@ func (s *KeyScanner) Scan() (Key, KeyScanStatus) { // This function doesn't have to be very fast, but it should be relatively // inexpensive for the "no keys were pressed" case. // When some keys combo is being pressed, it's OK to spend some resources. + k, status := s.scanKeyboard() + switch status { + case KeyScanCompleted: + s.lastNumKeys = 0 + s.key = Key{} + s.canScan = false + case KeyScanChanged: + s.key = k + } + return k, status +} + +func (s *KeyScanner) scanKeyboard() (Key, KeyScanStatus) { // This slice is stack-allocated; for the most cases, 4 keys are enough. keys := make([]ebiten.Key, 0, 4) keys = inpututil.AppendPressedKeys(keys) @@ -84,26 +97,12 @@ func (s *KeyScanner) Scan() (Key, KeyScanStatus) { // One or more keys are released. // Consider it to be a confirmation event. result := s.key - s.lastNumKeys = 0 - s.key = Key{} - s.canScan = false return result, KeyScanCompleted } s.lastNumKeys = len(keys) - - k, ok := scanKey(keys) - status := KeyScanUnchanged - if ok { - s.key = k - status = KeyScanChanged - } - return k, status -} - -func scanKey(keys []ebiten.Key) (Key, bool) { if len(keys) == 0 { - return Key{}, false + return Key{}, KeyScanUnchanged } containsKeyCode := func(keys []ebiten.Key, code int) bool { @@ -162,9 +161,9 @@ Loop: if mappedKey.name == "" { switch { case hasCtrl: - return ctrlKey, true + return ctrlKey, KeyScanUnchanged case hasShift: - return shiftKey, true + return shiftKey, KeyScanUnchanged } } @@ -184,5 +183,10 @@ Loop: } } - return mappedKey, mappedKey.name != "" + status := KeyScanUnchanged + if mappedKey.name != "" { + status = KeyScanChanged + } + + return mappedKey, status } diff --git a/scan_key_test.go b/scan_key_test.go deleted file mode 100644 index ce9a122..0000000 --- a/scan_key_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package input - -import ( - "testing" - - "github.com/hajimehoshi/ebiten/v2" -) - -func TestScanKey(t *testing.T) { - tests := []struct { - keys []ebiten.Key - want Key - }{ - // Sanity tests. - {[]ebiten.Key{}, Key{}}, - - // The simple cases with a single key. - {[]ebiten.Key{ebiten.KeyB}, KeyB}, - {[]ebiten.Key{ebiten.KeyEnter}, KeyEnter}, - {[]ebiten.Key{ebiten.KeyControlLeft, ebiten.KeyControl}, KeyControlLeft}, - {[]ebiten.Key{ebiten.KeyControlRight, ebiten.KeyControl}, KeyControlRight}, - {[]ebiten.Key{ebiten.KeyControl, ebiten.KeyControlRight}, KeyControlRight}, - - // Multiple key candidates without a way to merge them into a single Key. - {[]ebiten.Key{ebiten.KeyB, ebiten.KeyA}, KeyA}, - {[]ebiten.Key{ebiten.KeyA, ebiten.KeyB}, KeyA}, - - // Control modifiers. - {[]ebiten.Key{ebiten.KeyC, ebiten.KeyControlLeft, ebiten.KeyControl}, KeyWithModifier(KeyC, ModControl)}, - {[]ebiten.Key{ebiten.KeyC, ebiten.KeyControl, ebiten.KeyControlLeft}, KeyWithModifier(KeyC, ModControl)}, - {[]ebiten.Key{ebiten.KeyControl, ebiten.KeyControlLeft, ebiten.KeyC}, KeyWithModifier(KeyC, ModControl)}, - {[]ebiten.Key{ebiten.KeyE, ebiten.KeyControlLeft, ebiten.KeyControl}, KeyWithModifier(KeyE, ModControl)}, - {[]ebiten.Key{ebiten.KeyControlLeft, ebiten.KeyControl, ebiten.KeyE}, KeyWithModifier(KeyE, ModControl)}, - - // Shift modifiers. - {[]ebiten.Key{ebiten.KeyF, ebiten.KeyShiftLeft, ebiten.KeyShift}, KeyWithModifier(KeyF, ModShift)}, - - // Control+Shift modifiers. - {[]ebiten.Key{ebiten.KeyC, ebiten.KeyControlLeft, ebiten.KeyControl, ebiten.KeyShiftLeft, ebiten.KeyShift}, KeyWithModifier(KeyC, ModControlShift)}, - {[]ebiten.Key{ebiten.KeyA, ebiten.KeyControlLeft, ebiten.KeyControl, ebiten.KeyShiftLeft, ebiten.KeyShift}, KeyWithModifier(KeyA, ModControlShift)}, - {[]ebiten.Key{ebiten.KeyA, ebiten.KeyControlLeft, ebiten.KeyShiftLeft}, KeyWithModifier(KeyA, ModControlShift)}, - {[]ebiten.Key{ebiten.KeyA, ebiten.KeyControlRight, ebiten.KeyShiftRight}, KeyWithModifier(KeyA, ModControlShift)}, - {[]ebiten.Key{ebiten.KeyControlLeft, ebiten.KeyA, ebiten.KeyShiftRight}, KeyWithModifier(KeyA, ModControlShift)}, - } - - for i, test := range tests { - have, _ := scanKey(test.keys) - if have != test.want { - t.Fatalf("test[%d] failed:\nhave: %s (%#v)\nwant: %s (%#v)", - i, have, have, test.want, test.want) - } - } -} From e13d0a0ce10ef2608b298b6794c86f311bcc056d Mon Sep 17 00:00:00 2001 From: Eric H Date: Sat, 1 Aug 2026 22:14:08 -0400 Subject: [PATCH 02/28] use ebitengine just released keys functions --- scan_key.go | 57 ++++++++++++----------------------------------------- 1 file changed, 13 insertions(+), 44 deletions(-) diff --git a/scan_key.go b/scan_key.go index f41d745..4e62838 100644 --- a/scan_key.go +++ b/scan_key.go @@ -10,7 +10,6 @@ type KeyScanStatus int const ( KeyScanUnchanged KeyScanStatus = iota - KeyScanChanged KeyScanCompleted ) @@ -21,10 +20,8 @@ const ( // // Experimental: this is a part of a key remapping API, which is not stable yet. type KeyScanner struct { - lastNumKeys int - canScan bool - key Key - h *Handler + canScan bool + h *Handler } // NewKeyScanner creates a key scanner for the specifier input Handler. @@ -67,19 +64,18 @@ func (s *KeyScanner) Scan() (Key, KeyScanStatus) { switch status { case KeyScanCompleted: - s.lastNumKeys = 0 - s.key = Key{} s.canScan = false - case KeyScanChanged: - s.key = k } return k, status } func (s *KeyScanner) scanKeyboard() (Key, KeyScanStatus) { // This slice is stack-allocated; for the most cases, 4 keys are enough. + heldKeys := make([]ebiten.Key, 0, 4) + heldKeys = inpututil.AppendPressedKeys(heldKeys) + keys := make([]ebiten.Key, 0, 4) - keys = inpututil.AppendPressedKeys(keys) + keys = inpututil.AppendJustReleasedKeys(keys) if !s.canScan { if len(keys) != 0 { @@ -88,20 +84,8 @@ func (s *KeyScanner) scanKeyboard() (Key, KeyScanStatus) { s.canScan = true } - if len(keys) == s.lastNumKeys { - // It's either empty or we're still collecting the keys. - return Key{}, KeyScanUnchanged - } - - if len(keys) < s.lastNumKeys { - // One or more keys are released. - // Consider it to be a confirmation event. - result := s.key - return result, KeyScanCompleted - } - - s.lastNumKeys = len(keys) if len(keys) == 0 { + // We're still collecting the keys. return Key{}, KeyScanUnchanged } @@ -117,14 +101,10 @@ func (s *KeyScanner) scanKeyboard() (Key, KeyScanStatus) { // Parse the keys combination into something that this library can handle. // Round 1: walk the actual keys that are being pressed and collect the modifiers. - // Remove the modifiers from the slice (inplace). var ctrlKey Key var shiftKey Key - keysWithoutMods := keys[:0] - for _, k := range keys { + for _, k := range heldKeys { switch k { - case ebiten.KeyControl, ebiten.KeyShift: - // Just omit them from the slice. case ebiten.KeyControlLeft: ctrlKey = KeyControlLeft case ebiten.KeyControlRight: @@ -133,8 +113,6 @@ func (s *KeyScanner) scanKeyboard() (Key, KeyScanStatus) { shiftKey = KeyShiftLeft case ebiten.KeyShiftRight: shiftKey = KeyShiftRight - default: - keysWithoutMods = append(keysWithoutMods, k) } } hasCtrl := ctrlKey.name != "" @@ -148,25 +126,16 @@ func (s *KeyScanner) scanKeyboard() (Key, KeyScanStatus) { // Since this part of the code is not that performance-sensitive, // we'll handle it in a less efficient, but less memory-hungry way. Loop: - for _, lk := range allKeys { - switch lk.kind { + for _, k := range allKeys { + switch k.kind { case keyKeyboard: - if containsKeyCode(keysWithoutMods, lk.code) { - mappedKey = lk + if containsKeyCode(keys, k.code) { + mappedKey = k break Loop } } } - if mappedKey.name == "" { - switch { - case hasCtrl: - return ctrlKey, KeyScanUnchanged - case hasShift: - return shiftKey, KeyScanUnchanged - } - } - var keymod KeyModifier switch { case hasCtrl && hasShift: @@ -185,7 +154,7 @@ Loop: status := KeyScanUnchanged if mappedKey.name != "" { - status = KeyScanChanged + status = KeyScanCompleted } return mappedKey, status From 9c4925ad73f31835f6a357d1bf586ed187d47217 Mon Sep 17 00:00:00 2001 From: Eric H Date: Sat, 1 Aug 2026 22:18:47 -0400 Subject: [PATCH 03/28] add gamepad button scanning --- scan_key.go | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/scan_key.go b/scan_key.go index 4e62838..fa14845 100644 --- a/scan_key.go +++ b/scan_key.go @@ -62,6 +62,11 @@ func (s *KeyScanner) Scan() (Key, KeyScanStatus) { // When some keys combo is being pressed, it's OK to spend some resources. k, status := s.scanKeyboard() + if status == KeyScanUnchanged { + // scan for gamepad buttons also + k, status = s.scanGamepad() + } + switch status { case KeyScanCompleted: s.canScan = false @@ -69,6 +74,43 @@ func (s *KeyScanner) Scan() (Key, KeyScanStatus) { return k, status } +func (s *KeyScanner) scanGamepad() (Key, KeyScanStatus) { + gamepadKeys := make([]ebiten.StandardGamepadButton, 0, 4) + gamepadKeys = inpututil.AppendJustReleasedStandardGamepadButtons(ebiten.GamepadID(s.h.id), gamepadKeys) + if len(gamepadKeys) == 0 { + return Key{}, KeyScanUnchanged + } + + containsButtonCode := func(keys []ebiten.StandardGamepadButton, code int) bool { + for _, k := range keys { + if int(k) == code { + return true + } + } + return false + } + + var mappedKey Key + + // map the Ebitengine keys to the local types. +Loop: + for _, lk := range allKeys { + switch lk.kind { + case keyGamepad: + if containsButtonCode(gamepadKeys, lk.code) { + mappedKey = lk + break Loop + } + } + } + + status := KeyScanUnchanged + if mappedKey.name != "" { + status = KeyScanCompleted + } + return mappedKey, status +} + func (s *KeyScanner) scanKeyboard() (Key, KeyScanStatus) { // This slice is stack-allocated; for the most cases, 4 keys are enough. heldKeys := make([]ebiten.Key, 0, 4) @@ -156,6 +198,5 @@ Loop: if mappedKey.name != "" { status = KeyScanCompleted } - return mappedKey, status } From 57d1955cc60a8cb091891942c8de645f9e802882 Mon Sep 17 00:00:00 2001 From: Eric H Date: Sat, 1 Aug 2026 22:43:32 -0400 Subject: [PATCH 04/28] fix the remap example --- _examples/remap/main.go | 23 +++++++++-------------- scan_key.go | 2 +- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/_examples/remap/main.go b/_examples/remap/main.go index 19d0959..3c3b85d 100644 --- a/_examples/remap/main.go +++ b/_examples/remap/main.go @@ -32,7 +32,7 @@ type exampleGame struct { prevK input.Key scanning bool - keyScanner input.KeyScanner + keyScanner *input.KeyScanner inputHandler *input.Handler inputSystem input.System @@ -80,26 +80,20 @@ func (g *exampleGame) Update() error { func (g *exampleGame) handleRemap() { if !g.scanning { if g.inputHandler.ActionIsJustPressed(ActionRemap) { - g.prevK = g.k // Save it for an easier fallback g.scanning = true } return } k, status := g.keyScanner.Scan() - if status != input.KeyScanUnchanged { - g.k = k - } - if status == input.KeyScanCompleted { - g.scanning = false + if k == input.KeyWithModifier(input.KeyEnter, input.ModControl) || k == input.KeyControl || k == input.KeyEnter { + // reject the keys used to start key scanning for the purposes of the example + } else if status == input.KeyScanCompleted { // Check for the new key to be available. - // Resolve the conflicts here; I'll just reject - // the combination that is already in use. - if g.k == input.KeyWithModifier(input.KeyEnter, input.ModControl) { - g.k = g.prevK - } else { - g.inputHandler.Remap(g.makeKeymap()) - } + // Resolve the conflicts here. + g.scanning = false + g.k = k + g.inputHandler.Remap(g.makeKeymap()) } } @@ -113,4 +107,5 @@ func (g *exampleGame) makeKeymap() input.Keymap { func (g *exampleGame) Init() { g.k = input.KeyQ g.inputHandler = g.inputSystem.NewHandler(0, g.makeKeymap()) + g.keyScanner = input.NewKeyScanner(g.inputHandler) } diff --git a/scan_key.go b/scan_key.go index fa14845..20c6fbf 100644 --- a/scan_key.go +++ b/scan_key.go @@ -142,7 +142,7 @@ func (s *KeyScanner) scanKeyboard() (Key, KeyScanStatus) { // Parse the keys combination into something that this library can handle. - // Round 1: walk the actual keys that are being pressed and collect the modifiers. + // Round 1: walk the held keys that are being pressed to collect the modifiers. var ctrlKey Key var shiftKey Key for _, k := range heldKeys { From deae18ad08b96ad213c9fd257b32a8bf1c8cd3a0 Mon Sep 17 00:00:00 2001 From: Eric H Date: Sun, 2 Aug 2026 14:04:05 -0400 Subject: [PATCH 05/28] add missing gamepad canScan checking and setting --- scan_key.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scan_key.go b/scan_key.go index 20c6fbf..15e7f73 100644 --- a/scan_key.go +++ b/scan_key.go @@ -77,6 +77,14 @@ func (s *KeyScanner) Scan() (Key, KeyScanStatus) { func (s *KeyScanner) scanGamepad() (Key, KeyScanStatus) { gamepadKeys := make([]ebiten.StandardGamepadButton, 0, 4) gamepadKeys = inpututil.AppendJustReleasedStandardGamepadButtons(ebiten.GamepadID(s.h.id), gamepadKeys) + + if !s.canScan { + if len(gamepadKeys) != 0 { + return Key{}, KeyScanUnchanged + } + s.canScan = true + } + if len(gamepadKeys) == 0 { return Key{}, KeyScanUnchanged } From 9ec57db292a15cc7cb39151e8f6993559782ac76 Mon Sep 17 00:00:00 2001 From: Eric H Date: Sun, 2 Aug 2026 14:10:07 -0400 Subject: [PATCH 06/28] update actions/setup-go --- .github/workflows/go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 8c71e85..0f22423 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -12,7 +12,7 @@ "steps": [ { "name": "Set up Go 1.19", - "uses": "actions/setup-go@v1", + "uses": "actions/setup-go@v7", "with": {"go-version": 1.19}, "id": "go", }, From 92915c2aa6287390a25caf8bade8c41d8f2da83e Mon Sep 17 00:00:00 2001 From: Eric H Date: Sun, 2 Aug 2026 14:17:51 -0400 Subject: [PATCH 07/28] fix nil exception if KeyScanner is not initialized with handler pointer --- scan_key.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scan_key.go b/scan_key.go index 15e7f73..5f2e4c2 100644 --- a/scan_key.go +++ b/scan_key.go @@ -75,8 +75,12 @@ func (s *KeyScanner) Scan() (Key, KeyScanStatus) { } func (s *KeyScanner) scanGamepad() (Key, KeyScanStatus) { + var handlerID uint8 + if s.h != nil { + handlerID = s.h.id + } gamepadKeys := make([]ebiten.StandardGamepadButton, 0, 4) - gamepadKeys = inpututil.AppendJustReleasedStandardGamepadButtons(ebiten.GamepadID(s.h.id), gamepadKeys) + gamepadKeys = inpututil.AppendJustReleasedStandardGamepadButtons(ebiten.GamepadID(handlerID), gamepadKeys) if !s.canScan { if len(gamepadKeys) != 0 { From 99b5e10249d21c9fca553cabd1781fa8cad0d98d Mon Sep 17 00:00:00 2001 From: Eric H Date: Sun, 2 Aug 2026 14:42:53 -0400 Subject: [PATCH 08/28] add mouse button scanning --- scan_key.go | 65 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/scan_key.go b/scan_key.go index 5f2e4c2..d3a14a5 100644 --- a/scan_key.go +++ b/scan_key.go @@ -51,7 +51,6 @@ func NewKeyScanner(h *Handler) *KeyScanner { // * Completed - the user finished specifying the keys combination, you can use the Key as a new binding func (s *KeyScanner) Scan() (Key, KeyScanStatus) { // TODO: respect the enabled input devices. - // TODO: scan the gamepad buttons as well. // Note that this function may not be needed by some users, // so we're better of making it as independent as possible, so it @@ -61,9 +60,12 @@ func (s *KeyScanner) Scan() (Key, KeyScanStatus) { // inexpensive for the "no keys were pressed" case. // When some keys combo is being pressed, it's OK to spend some resources. k, status := s.scanKeyboard() - if status == KeyScanUnchanged { - // scan for gamepad buttons also + // scan for mouse buttons + k, status = s.scanMouse() + } + if status == KeyScanUnchanged { + // scan for gamepad buttons k, status = s.scanGamepad() } @@ -74,6 +76,55 @@ func (s *KeyScanner) Scan() (Key, KeyScanStatus) { return k, status } +func (s *KeyScanner) scanMouse() (Key, KeyScanStatus) { + mouseKeys := make([]ebiten.MouseButton, 0, 4) + for k := ebiten.MouseButton(0); k < ebiten.MouseButtonMax; k++ { + if inpututil.IsMouseButtonJustReleased(k) { + mouseKeys = append(mouseKeys, k) + } + } + + if !s.canScan { + if len(mouseKeys) != 0 { + return Key{}, KeyScanUnchanged + } + s.canScan = true + } + + if len(mouseKeys) == 0 { + return Key{}, KeyScanUnchanged + } + + containsButtonCode := func(keys []ebiten.MouseButton, code int) bool { + for _, k := range keys { + if int(k) == code { + return true + } + } + return false + } + + var mappedKey Key + + // map the Ebitengine keys to the local types. +Loop: + for _, k := range allKeys { + switch k.kind { + case keyMouse: + if containsButtonCode(mouseKeys, k.code) { + mappedKey = k + break Loop + } + } + } + + status := KeyScanUnchanged + if mappedKey.name != "" { + status = KeyScanCompleted + } + return mappedKey, status +} + func (s *KeyScanner) scanGamepad() (Key, KeyScanStatus) { var handlerID uint8 if s.h != nil { @@ -106,11 +157,11 @@ func (s *KeyScanner) scanGamepad() (Key, KeyScanStatus) { // map the Ebitengine keys to the local types. Loop: - for _, lk := range allKeys { - switch lk.kind { + for _, k := range allKeys { + switch k.kind { case keyGamepad: - if containsButtonCode(gamepadKeys, lk.code) { - mappedKey = lk + if containsButtonCode(gamepadKeys, k.code) { + mappedKey = k break Loop } } From f533652fef62ed79e36cf9b422f9e39a02a405bb Mon Sep 17 00:00:00 2001 From: Eric H Date: Sun, 2 Aug 2026 14:57:05 -0400 Subject: [PATCH 09/28] add key modifiers for mouse button presses --- scan_key.go | 86 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 35 deletions(-) diff --git a/scan_key.go b/scan_key.go index d3a14a5..e479e1c 100644 --- a/scan_key.go +++ b/scan_key.go @@ -118,6 +118,15 @@ Loop: } } + // attach any held key modifiers + keymod := s.scanKeyModifiers() + if keymod != ModUnknown { + switch mappedKey.kind { + case keyMouse: + mappedKey = KeyWithModifier(mappedKey, keymod) + } + } + status := KeyScanUnchanged if mappedKey.name != "" { status = KeyScanCompleted @@ -176,9 +185,6 @@ Loop: func (s *KeyScanner) scanKeyboard() (Key, KeyScanStatus) { // This slice is stack-allocated; for the most cases, 4 keys are enough. - heldKeys := make([]ebiten.Key, 0, 4) - heldKeys = inpututil.AppendPressedKeys(heldKeys) - keys := make([]ebiten.Key, 0, 4) keys = inpututil.AppendJustReleasedKeys(keys) @@ -203,9 +209,48 @@ func (s *KeyScanner) scanKeyboard() (Key, KeyScanStatus) { return false } - // Parse the keys combination into something that this library can handle. + // map the Ebitengine keys to the local types. + // In theory, we could generate a big LUT to make this mapping very fast. + // But this would mean more data reserved for this package. + // Since this part of the code is not that performance-sensitive, + // we'll handle it in a less efficient, but less memory-hungry way. + var mappedKey Key + +Loop: + for _, k := range allKeys { + switch k.kind { + case keyKeyboard: + if containsKeyCode(keys, k.code) { + mappedKey = k + break Loop + } + } + } + + // attach any held key modifiers + keymod := s.scanKeyModifiers() + if keymod != ModUnknown { + switch mappedKey.kind { + case keyKeyboard: + mappedKey = KeyWithModifier(mappedKey, keymod) + } + } + + status := KeyScanUnchanged + if mappedKey.name != "" { + status = KeyScanCompleted + } + return mappedKey, status +} + +func (s *KeyScanner) scanKeyModifiers() KeyModifier { + if !s.canScan { + return ModUnknown + } + + heldKeys := make([]ebiten.Key, 0, 4) + heldKeys = inpututil.AppendPressedKeys(heldKeys) - // Round 1: walk the held keys that are being pressed to collect the modifiers. var ctrlKey Key var shiftKey Key for _, k := range heldKeys { @@ -223,24 +268,6 @@ func (s *KeyScanner) scanKeyboard() (Key, KeyScanStatus) { hasCtrl := ctrlKey.name != "" hasShift := shiftKey.name != "" - var mappedKey Key - - // Round 2: map the Ebitengine keys to the local types. - // In theory, we could generate a big LUT to make this mapping very fast. - // But this would mean more data reserved for this package. - // Since this part of the code is not that performance-sensitive, - // we'll handle it in a less efficient, but less memory-hungry way. -Loop: - for _, k := range allKeys { - switch k.kind { - case keyKeyboard: - if containsKeyCode(keys, k.code) { - mappedKey = k - break Loop - } - } - } - var keymod KeyModifier switch { case hasCtrl && hasShift: @@ -250,16 +277,5 @@ Loop: case hasShift: keymod = ModShift } - if keymod != ModUnknown { - switch mappedKey.kind { - case keyKeyboard, keyMouse: - mappedKey = KeyWithModifier(mappedKey, keymod) - } - } - - status := KeyScanUnchanged - if mappedKey.name != "" { - status = KeyScanCompleted - } - return mappedKey, status + return keymod } From ade3921441d6e6e1c62a5bdc57df1a1486ea3550 Mon Sep 17 00:00:00 2001 From: Eric H Date: Sun, 2 Aug 2026 15:03:45 -0400 Subject: [PATCH 10/28] move example remap after ping to avoid gamepad pinging at the same time it was just set --- _examples/remap/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_examples/remap/main.go b/_examples/remap/main.go index 3c3b85d..d49a3fc 100644 --- a/_examples/remap/main.go +++ b/_examples/remap/main.go @@ -68,12 +68,12 @@ func (g *exampleGame) Update() error { g.started = true } - g.handleRemap() - if !g.scanning && g.inputHandler.ActionIsJustPressed(ActionPing) { fmt.Printf("ping! (activated with %s keybind)\n", g.k) } + g.handleRemap() + return nil } From ae4ef29b9c6a24268bf558da1168eb784adbe754 Mon Sep 17 00:00:00 2001 From: Eric H Date: Sun, 2 Aug 2026 16:24:22 -0400 Subject: [PATCH 11/28] initial axis scanner functionality --- scan_axes.go | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++ scan_key.go | 5 ++-- 2 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 scan_axes.go diff --git a/scan_axes.go b/scan_axes.go new file mode 100644 index 0000000..8046c07 --- /dev/null +++ b/scan_axes.go @@ -0,0 +1,64 @@ +package input + +const ( + scanAxesMouseWheel Action = iota + scanAxesGamepadLStick + scanAxesGamepadRStick + scanAxesActionCount // always last to keep accurate count to iterate over +) + +var ( + scanAxesHandler *Handler + scanAxesKeymap = Keymap{ + scanAxesMouseWheel: {KeyWheelVertical}, + scanAxesGamepadLStick: {KeyGamepadLStickMotion}, + scanAxesGamepadRStick: {KeyGamepadRStickMotion}, + } +) + +func newScanAxesHandler(scanHandler *Handler) *Handler { + return scanHandler.sys.NewHandler(scanHandler.id, scanAxesKeymap) +} + +// ScanAxes reads the axis changed state and tries to map them to a Key. +// +// It's intended to work with mouse, mouse wheel, and gamepad stick axes. +// +// This function should be called on every frame where you're reading +// the new keybind combination. +// See the remap example for more info. +// +// The function can return these result statuses: +// * Unchanged - nothing updated since the last Scan() operation +// * Completed - the user finished specifying the keys combination, you can use the Key as a new binding +func (s *KeyScanner) ScanAxes() (Key, KeyScanStatus) { + + // FIXME: if s.h == nil, panic or err because this method requires handler instance to be provided? + + if scanAxesHandler == nil { + // special Handler is needed to determine axis events using special keymap + scanAxesHandler = newScanAxesHandler(s.h) + } + + k, status := s.scanAxesActions() + + // FIXME: mouse motion axes do not appear to be available in ebitengine-input, can we add it? + + switch status { + case KeyScanCompleted: + s.canScan = false + scanAxesHandler = nil + } + return k, status +} + +func (s *KeyScanner) scanAxesActions() (Key, KeyScanStatus) { + for a := Action(0); a < scanAxesActionCount; a++ { + if _, ok := scanAxesHandler.JustPressedActionInfo(a); ok { + k := scanAxesKeymap[a][0] + return k, KeyScanCompleted + } + } + + return Key{}, KeyScanUnchanged +} diff --git a/scan_key.go b/scan_key.go index e479e1c..fededd7 100644 --- a/scan_key.go +++ b/scan_key.go @@ -38,8 +38,7 @@ func NewKeyScanner(h *Handler) *KeyScanner { // Scan reads the buttons state and tries to map them to a Key. // -// It's intended to work with keyboard keys as well as gamepad buttons, -// but right now it only works for the keyboard. +// It's intended to work with keyboard keys as well as mouse and gamepad buttons. // // This function should be called on every frame where you're reading // the new keybind combination. @@ -47,7 +46,6 @@ func NewKeyScanner(h *Handler) *KeyScanner { // // The function can return these result statuses: // * Unchanged - nothing updated since the last Scan() operation -// * Changed - some keys changed, you may want to update the prompt to the user // * Completed - the user finished specifying the keys combination, you can use the Key as a new binding func (s *KeyScanner) Scan() (Key, KeyScanStatus) { // TODO: respect the enabled input devices. @@ -77,6 +75,7 @@ func (s *KeyScanner) Scan() (Key, KeyScanStatus) { } func (s *KeyScanner) scanMouse() (Key, KeyScanStatus) { + // We will need to do our own "AppendJustReleased" for mouse button presses mouseKeys := make([]ebiten.MouseButton, 0, 4) for k := ebiten.MouseButton(0); k < ebiten.MouseButtonMax; k++ { if inpututil.IsMouseButtonJustReleased(k) { From c453aacd57e8e34cc6dbea52900bc510c31ed28e Mon Sep 17 00:00:00 2001 From: Eric H Date: Sun, 2 Aug 2026 19:53:42 -0400 Subject: [PATCH 12/28] fix mouse wheel action not firing for handler.PressedActionInfo - only JustPressedActionInfo previously handled mouse wheel events --- .gitignore | 29 +++++++++++++++++++++++++++++ _examples/scroll/main.go | 4 ++-- handler.go | 22 +++++++++++++++++----- 3 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb4a139 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Code coverage profiles and other test artifacts +*.out +coverage.* +*.coverprofile +profile.cov + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# Editor/IDE +.idea/ +.vscode/ diff --git a/_examples/scroll/main.go b/_examples/scroll/main.go index faea887..b96533b 100644 --- a/_examples/scroll/main.go +++ b/_examples/scroll/main.go @@ -66,11 +66,11 @@ func (g *exampleGame) Update() error { } func (g *exampleGame) handleScroll() { - if info, ok := g.inputHandler.JustPressedActionInfo(ActionFastScrollVertical); ok { + if info, ok := g.inputHandler.PressedActionInfo(ActionFastScrollVertical); ok { g.pos.Y += info.Pos.Y * 7 return } - if info, ok := g.inputHandler.JustPressedActionInfo(ActionScrollVertical); ok { + if info, ok := g.inputHandler.PressedActionInfo(ActionScrollVertical); ok { g.pos.Y += info.Pos.Y return } diff --git a/handler.go b/handler.go index b1fb00c..06dec2b 100644 --- a/handler.go +++ b/handler.go @@ -539,17 +539,17 @@ func (h *Handler) keyIsJustPressed(k Key) bool { ebiten.IsKeyPressed(ebiten.KeyShift) && inpututil.IsKeyJustPressed(ebiten.Key(k.code)) case keyWheel: - return h.wheelIsJustPressed(wheelCode(k.code)) + return h.wheelIsPressed(wheelCode(k.code)) case keyWheelWithCtrl: return ebiten.IsKeyPressed(ebiten.KeyControl) && - h.wheelIsJustPressed(wheelCode(k.code)) + h.wheelIsPressed(wheelCode(k.code)) case keyWheelWithShift: return ebiten.IsKeyPressed(ebiten.KeyShift) && - h.wheelIsJustPressed(wheelCode(k.code)) + h.wheelIsPressed(wheelCode(k.code)) case keyWheelWithCtrlShift: return ebiten.IsKeyPressed(ebiten.KeyControl) && ebiten.IsKeyPressed(ebiten.KeyShift) && - h.wheelIsJustPressed(wheelCode(k.code)) + h.wheelIsPressed(wheelCode(k.code)) default: return inpututil.IsKeyJustPressed(ebiten.Key(k.code)) } @@ -651,6 +651,18 @@ func (h *Handler) keyIsPressed(k Key) bool { return ebiten.IsKeyPressed(ebiten.KeyControl) && ebiten.IsKeyPressed(ebiten.KeyShift) && ebiten.IsKeyPressed(ebiten.Key(k.code)) + case keyWheel: + return h.wheelIsPressed(wheelCode(k.code)) + case keyWheelWithCtrl: + return ebiten.IsKeyPressed(ebiten.KeyControl) && + h.wheelIsPressed(wheelCode(k.code)) + case keyWheelWithShift: + return ebiten.IsKeyPressed(ebiten.KeyShift) && + h.wheelIsPressed(wheelCode(k.code)) + case keyWheelWithCtrlShift: + return ebiten.IsKeyPressed(ebiten.KeyControl) && + ebiten.IsKeyPressed(ebiten.KeyShift) && + h.wheelIsPressed(wheelCode(k.code)) default: return ebiten.IsKeyPressed(ebiten.Key(k.code)) } @@ -710,7 +722,7 @@ func (h *Handler) isDPadAxisActive(code int, vec Vec) bool { return false } -func (h *Handler) wheelIsJustPressed(code wheelCode) bool { +func (h *Handler) wheelIsPressed(code wheelCode) bool { switch code { case wheelDown: return h.sys.wheel.Y > 0 From c8137e6cfe9d730842e92fcc764e0042ad2abb6e Mon Sep 17 00:00:00 2001 From: Eric H Date: Sun, 2 Aug 2026 20:43:33 -0400 Subject: [PATCH 13/28] add capability to scan mouse wheel up/down as distinct keys - also had to swap wheel up/down determination since it was reporting the reverse when remapped --- _examples/remap/main.go | 126 +++++++++++++++++++++++++++++++++------- event.go | 5 ++ handler.go | 4 +- scan_axes.go | 6 +- scan_key.go | 41 +++++++++++++ 5 files changed, 156 insertions(+), 26 deletions(-) diff --git a/_examples/remap/main.go b/_examples/remap/main.go index d49a3fc..18f9093 100644 --- a/_examples/remap/main.go +++ b/_examples/remap/main.go @@ -11,14 +11,21 @@ import ( input "github.com/quasilyte/ebitengine-input" ) +const ( + width int = 640 + height int = 480 +) + const ( ActionUnknown input.Action = iota ActionPing - ActionRemap + ActionMove + ActionRemapKey + ActionRemapAxes ) func main() { - ebiten.SetWindowSize(640, 480) + ebiten.SetWindowSize(width, height) if err := ebiten.RunGame(newExampleGame()); err != nil { log.Fatal(err) @@ -28,18 +35,25 @@ func main() { type exampleGame struct { started bool - k input.Key - prevK input.Key - scanning bool + k input.Key + axes input.Key + scanningKey bool + scanningAxes bool keyScanner *input.KeyScanner + pos input.Vec + character string + inputHandler *input.Handler inputSystem input.System } func newExampleGame() *exampleGame { - g := &exampleGame{} + g := &exampleGame{ + pos: input.Vec{X: 200, Y: 200}, + character: "@", + } g.inputSystem.Init(input.SystemConfig{ DevicesEnabled: input.AnyDevice, @@ -49,15 +63,34 @@ func newExampleGame() *exampleGame { } func (g *exampleGame) Layout(_, _ int) (int, int) { - return 640, 480 + return width, height +} + +func screenClamp(x, y float64) (float64, float64) { + if x < 0 { + x = 0 + } + if x >= float64(width) { + x = float64(width) - 1 + } + if y < 0 { + y = 0 + } + if y >= float64(height) { + y = float64(height) - 1 + } + return x, y } func (g *exampleGame) Draw(screen *ebiten.Image) { - if g.scanning { - ebitenutil.DebugPrint(screen, fmt.Sprintf("keybind: %s\n", g.k)) + if g.scanningKey { + ebitenutil.DebugPrint(screen, fmt.Sprintf("keybind: %s\naxes: %s\n", g.k, g.axes)) + } else if g.scanningAxes { + ebitenutil.DebugPrint(screen, fmt.Sprintf("keybind: %s\naxes: %s\n", g.k, g.axes)) } else { - ebitenutil.DebugPrint(screen, fmt.Sprintf("keybind: %s\npress ctrl+enter to remap", g.k)) + ebitenutil.DebugPrint(screen, fmt.Sprintf("keybind: %s\naxes: %s\npress ctrl+enter to remap keybind\nor shift+enter to remap axes", g.k, g.axes)) } + ebitenutil.DebugPrintAt(screen, g.character, int(g.pos.X), int(g.pos.Y)) } func (g *exampleGame) Update() error { @@ -68,44 +101,95 @@ func (g *exampleGame) Update() error { g.started = true } - if !g.scanning && g.inputHandler.ActionIsJustPressed(ActionPing) { - fmt.Printf("ping! (activated with %s keybind)\n", g.k) + if !g.scanningKey { + // react to ping action by "highlighting" the "character" + if g.inputHandler.ActionIsJustPressed(ActionPing) { + fmt.Printf("ping! (activated with %s keybind)\n", g.k) + } + if g.inputHandler.ActionIsPressed(ActionPing) { + g.character = "***\n*@*\n***" + } else { + g.character = "@" + } + + // react to move action by moving the "character" + if info, ok := g.inputHandler.PressedActionInfo(ActionMove); ok { + if info.IsMouseWheelEvent() { + // mouse wheel moves in reverse direction of draw position + g.pos.Y -= info.Pos.Y + } else { + g.pos.X += info.Pos.X + g.pos.Y += info.Pos.Y + } + } + + // clamp position to screen window size + g.pos.X, g.pos.Y = screenClamp(g.pos.X, g.pos.Y) } - g.handleRemap() + // keep scanning of keys separate from axes to ensure events are isolated to just what is needed + if !g.scanningAxes { + g.handleRemapKey() + } + if !g.scanningKey { + g.handleRemapAxes() + } return nil } -func (g *exampleGame) handleRemap() { - if !g.scanning { - if g.inputHandler.ActionIsJustPressed(ActionRemap) { - g.scanning = true +func (g *exampleGame) handleRemapKey() { + if !g.scanningKey { + if g.inputHandler.ActionIsJustPressed(ActionRemapKey) { + g.scanningKey = true } return } k, status := g.keyScanner.Scan() - if k == input.KeyWithModifier(input.KeyEnter, input.ModControl) || k == input.KeyControl || k == input.KeyEnter { + if k == input.KeyWithModifier(input.KeyEnter, input.ModControl) || + k == input.KeyWithModifier(input.KeyEnter, input.ModShift) || + k == input.KeyControl || k == input.KeyShift || k == input.KeyEnter { // reject the keys used to start key scanning for the purposes of the example } else if status == input.KeyScanCompleted { // Check for the new key to be available. // Resolve the conflicts here. - g.scanning = false + g.scanningKey = false g.k = k g.inputHandler.Remap(g.makeKeymap()) } } +func (g *exampleGame) handleRemapAxes() { + if !g.scanningAxes { + if g.inputHandler.ActionIsJustPressed(ActionRemapAxes) { + g.scanningAxes = true + } + return + } + + axes, status := g.keyScanner.ScanAxes() + if status == input.KeyScanCompleted { + // Check for the new key to be available. + // Resolve the conflicts here. + g.scanningAxes = false + g.axes = axes + g.inputHandler.Remap(g.makeKeymap()) + } +} + func (g *exampleGame) makeKeymap() input.Keymap { return input.Keymap{ - ActionPing: {g.k}, - ActionRemap: {input.KeyWithModifier(input.KeyEnter, input.ModControl)}, + ActionPing: {g.k}, + ActionMove: {g.axes}, + ActionRemapKey: {input.KeyWithModifier(input.KeyEnter, input.ModControl)}, + ActionRemapAxes: {input.KeyWithModifier(input.KeyEnter, input.ModShift)}, } } func (g *exampleGame) Init() { g.k = input.KeyQ + g.axes = input.KeyGamepadLStickMotion g.inputHandler = g.inputSystem.NewHandler(0, g.makeKeymap()) g.keyScanner = input.NewKeyScanner(g.inputHandler) } diff --git a/event.go b/event.go index fa3d18d..7fce86d 100644 --- a/event.go +++ b/event.go @@ -66,6 +66,11 @@ func (e EventInfo) HasPos() bool { return e.hasPos } // Use Duration field to get the press duration value. func (e EventInfo) HasDuration() bool { return e.hasDuration } +// IsMouseWheelEvent reports whether this event was triggered by a mouse wheel. +func (e EventInfo) IsMouseWheelEvent() bool { + return e.kind == keyWheel || e.kind == keyWheelWithCtrl || e.kind == keyWheelWithShift || e.kind == keyWheelWithCtrlShift +} + // IsTouchEvent reports whether this event was triggered by a screen touch device. // // Deprecated: Use Source().IsTouch() instead. diff --git a/handler.go b/handler.go index 06dec2b..85c7978 100644 --- a/handler.go +++ b/handler.go @@ -725,9 +725,9 @@ func (h *Handler) isDPadAxisActive(code int, vec Vec) bool { func (h *Handler) wheelIsPressed(code wheelCode) bool { switch code { case wheelDown: - return h.sys.wheel.Y > 0 - case wheelUp: return h.sys.wheel.Y < 0 + case wheelUp: + return h.sys.wheel.Y > 0 case wheelVertical: return h.sys.wheel.Y != 0 default: diff --git a/scan_axes.go b/scan_axes.go index 8046c07..ee0629b 100644 --- a/scan_axes.go +++ b/scan_axes.go @@ -8,6 +8,7 @@ const ( ) var ( + // use special handler and keymap to detect axes action events for key scanning purposes scanAxesHandler *Handler scanAxesKeymap = Keymap{ scanAxesMouseWheel: {KeyWheelVertical}, @@ -40,7 +41,7 @@ func (s *KeyScanner) ScanAxes() (Key, KeyScanStatus) { scanAxesHandler = newScanAxesHandler(s.h) } - k, status := s.scanAxesActions() + k, status := s.scanAxesEvents() // FIXME: mouse motion axes do not appear to be available in ebitengine-input, can we add it? @@ -52,13 +53,12 @@ func (s *KeyScanner) ScanAxes() (Key, KeyScanStatus) { return k, status } -func (s *KeyScanner) scanAxesActions() (Key, KeyScanStatus) { +func (s *KeyScanner) scanAxesEvents() (Key, KeyScanStatus) { for a := Action(0); a < scanAxesActionCount; a++ { if _, ok := scanAxesHandler.JustPressedActionInfo(a); ok { k := scanAxesKeymap[a][0] return k, KeyScanCompleted } } - return Key{}, KeyScanUnchanged } diff --git a/scan_key.go b/scan_key.go index fededd7..b50f320 100644 --- a/scan_key.go +++ b/scan_key.go @@ -13,6 +13,25 @@ const ( KeyScanCompleted ) +const ( + scanKeyMouseWheelDown Action = iota + scanKeyMouseWheelUp + scanKeyActionCount // always last to keep accurate count to iterate over +) + +var ( + // use special handler and keymap to detect certain events for key scanning purposes + scanKeyHandler *Handler + scanKeyKeymap = Keymap{ + scanKeyMouseWheelDown: {KeyWheelDown}, + scanKeyMouseWheelUp: {KeyWheelUp}, + } +) + +func newScanKeyHandler(scanHandler *Handler) *Handler { + return scanHandler.sys.NewHandler(scanHandler.id, scanKeyKeymap) +} + // KeyScanner checks the currently pressed keys and buttons and tries to map them // to a local Key type that can be used in a Keymap. // @@ -50,6 +69,13 @@ func NewKeyScanner(h *Handler) *KeyScanner { func (s *KeyScanner) Scan() (Key, KeyScanStatus) { // TODO: respect the enabled input devices. + // FIXME: if s.h == nil, panic or err because this method requires handler instance to be provided? + + if scanKeyHandler == nil { + // special Handler is needed to determine certain events using special keymap + scanKeyHandler = newScanKeyHandler(s.h) + } + // Note that this function may not be needed by some users, // so we're better of making it as independent as possible, so it // doesn't make the package more expensive if you don't use it. @@ -66,14 +92,29 @@ func (s *KeyScanner) Scan() (Key, KeyScanStatus) { // scan for gamepad buttons k, status = s.scanGamepad() } + if status == KeyScanUnchanged { + // scan for special keys, like mouse wheel up/down + k, status = s.scanSpecialEvents() + } switch status { case KeyScanCompleted: s.canScan = false + scanKeyHandler = nil } return k, status } +func (s *KeyScanner) scanSpecialEvents() (Key, KeyScanStatus) { + for a := Action(0); a < scanKeyActionCount; a++ { + if _, ok := scanKeyHandler.JustPressedActionInfo(a); ok { + k := scanKeyKeymap[a][0] + return k, KeyScanCompleted + } + } + return Key{}, KeyScanUnchanged +} + func (s *KeyScanner) scanMouse() (Key, KeyScanStatus) { // We will need to do our own "AppendJustReleased" for mouse button presses mouseKeys := make([]ebiten.MouseButton, 0, 4) From 2e4f763324d3f604b2cf716708295e8d9e8ede9f Mon Sep 17 00:00:00 2001 From: Eric H Date: Sat, 8 Aug 2026 17:12:55 -0400 Subject: [PATCH 14/28] add ability to bind and scan mouse move actions/keys --- _examples/remap/main.go | 9 ++++++-- event.go | 6 +++++ handler.go | 49 ++++++++++++++++++++++++++++++++++++----- internal_key.go | 4 +++- internal_key_list.go | 1 + key.go | 1 + math_nodeps.go | 4 ++++ scan_axes.go | 12 ++++++++-- scan_key.go | 29 +++++++----------------- system.go | 4 ++++ 10 files changed, 88 insertions(+), 31 deletions(-) diff --git a/_examples/remap/main.go b/_examples/remap/main.go index 18f9093..1bb02c7 100644 --- a/_examples/remap/main.go +++ b/_examples/remap/main.go @@ -101,7 +101,7 @@ func (g *exampleGame) Update() error { g.started = true } - if !g.scanningKey { + if !g.scanningKey && !g.scanningAxes { // react to ping action by "highlighting" the "character" if g.inputHandler.ActionIsJustPressed(ActionPing) { fmt.Printf("ping! (activated with %s keybind)\n", g.k) @@ -117,6 +117,11 @@ func (g *exampleGame) Update() error { if info.IsMouseWheelEvent() { // mouse wheel moves in reverse direction of draw position g.pos.Y -= info.Pos.Y + } else if info.IsMouseMoveEvent() { + // mouse position (info.Pos) is absolute, the delta position (info.DeltaPos) + // can be useful, typically when using ebiten.CursorModeCaptured + g.pos.X += info.DeltaPos.X + g.pos.Y += info.DeltaPos.Y } else { g.pos.X += info.Pos.X g.pos.Y += info.Pos.Y @@ -189,7 +194,7 @@ func (g *exampleGame) makeKeymap() input.Keymap { func (g *exampleGame) Init() { g.k = input.KeyQ - g.axes = input.KeyGamepadLStickMotion + g.axes = input.KeyMouseMove g.inputHandler = g.inputSystem.NewHandler(0, g.makeKeymap()) g.keyScanner = input.NewKeyScanner(g.inputHandler) } diff --git a/event.go b/event.go index 7fce86d..a7fba87 100644 --- a/event.go +++ b/event.go @@ -55,6 +55,7 @@ type EventInfo struct { Duration int Pos Vec + DeltaPos Vec StartPos Vec } @@ -66,6 +67,11 @@ func (e EventInfo) HasPos() bool { return e.hasPos } // Use Duration field to get the press duration value. func (e EventInfo) HasDuration() bool { return e.hasDuration } +// IsMouseMoveEvent reports whether this event was triggered by mouse movement. +func (e EventInfo) IsMouseMoveEvent() bool { + return e.kind == keyMouseMove +} + // IsMouseWheelEvent reports whether this event was triggered by a mouse wheel. func (e EventInfo) IsMouseWheelEvent() bool { return e.kind == keyWheel || e.kind == keyWheelWithCtrl || e.kind == keyWheelWithShift || e.kind == keyWheelWithCtrlShift diff --git a/handler.go b/handler.go index 85c7978..cee6a7c 100644 --- a/handler.go +++ b/handler.go @@ -233,7 +233,7 @@ func (h *Handler) keyIsEnabled(k Key, mask DeviceKind) bool { return mask&MouseDevice != 0 case keyMouseWithShift: return mask&MouseDevice != 0 - case keyMouse: + case keyMouse, keyMouseMove: return mask&MouseDevice != 0 case keyWheel, keyWheelWithCtrl, keyWheelWithShift, keyWheelWithCtrlShift: return mask&MouseDevice != 0 @@ -272,6 +272,7 @@ func (h *Handler) JustReleasedActionInfo(action Action) (EventInfo, bool) { info.kind = k.kind info.hasPos = keyHasPos(k.kind) info.Pos = h.getKeyPos(k) + info.DeltaPos = deltaVec(info.Pos, h.getKeyPrevPos(k)) info.StartPos = h.getKeyStartPos(k) h.updateLastDevice(k.kind) return info, true @@ -340,6 +341,7 @@ func (h *Handler) JustPressedActionInfo(action Action) (EventInfo, bool) { info.kind = k.kind info.hasPos = keyHasPos(k.kind) info.Pos = h.getKeyPos(k) + info.DeltaPos = deltaVec(info.Pos, h.getKeyPrevPos(k)) info.StartPos = h.getKeyStartPos(k) h.updateLastDevice(k.kind) return info, true @@ -376,6 +378,7 @@ func (h *Handler) PressedActionInfo(action Action) (EventInfo, bool) { info.kind = k.kind info.hasPos = keyHasPos(k.kind) info.Pos = h.getKeyPos(k) + info.DeltaPos = deltaVec(info.Pos, h.getKeyPrevPos(k)) info.StartPos = h.getKeyStartPos(k) info.hasDuration = keyHasDuration(k.kind) info.Duration = h.getKeyPressDuration(k) @@ -516,6 +519,8 @@ func (h *Handler) keyIsJustPressed(k Key) bool { return h.gamepadStickIsJustPressed(stickCode(k.code), ebiten.StandardGamepadAxisRightStickHorizontal, ebiten.StandardGamepadAxisRightStickVertical) case keyGamepadStickMotion: return h.gamepadStickMotionIsJustPressed(stickCode(k.code)) + case keyMouseMove: + return h.mouseMoveIsJustPressed() case keyMouse: return inpututil.IsMouseButtonJustPressed(ebiten.MouseButton(k.code)) case keyMouseWithCtrl: @@ -569,8 +574,8 @@ func (h *Handler) getKeyStartPos(k Key) Vec { func (h *Handler) getKeyPos(k Key) Vec { var result Vec switch k.kind { - case keyMouse, keyMouseWithCtrl, keyMouseWithShift, keyMouseWithCtrlShift: - result = h.sys.cursorPos + case keyMouse, keyMouseMove, keyMouseWithCtrl, keyMouseWithShift, keyMouseWithCtrlShift: + result = h.getMouseVec() case keyTouch: result = h.sys.touchTapPos case keyTouchDrag: @@ -580,8 +585,22 @@ func (h *Handler) getKeyPos(k Key) Vec { case keyWheel, keyWheelWithCtrl, keyWheelWithShift, keyWheelWithCtrlShift: result = h.sys.wheel case keyGamepadStickMotion: - axis1, axis2 := h.getStickAxes(stickCode(k.code)) - result = h.getStickVec(axis1, axis2) + result = h.getStickVec(h.getStickAxes(stickCode(k.code))) + } + return result +} + +func (h *Handler) getKeyPrevPos(k Key) Vec { + var result Vec + switch k.kind { + case keyMouse, keyMouseMove, keyMouseWithCtrl, keyMouseWithShift, keyMouseWithCtrlShift: + result = h.getMousePrevVec() + case keyTouchDrag: + result = h.sys.touchStartPos + case keyMouseDrag: + result = h.sys.mouseStartPos + case keyGamepadStickMotion: + result = h.getStickPrevVec(h.getStickAxes(stickCode(k.code))) } return result } @@ -629,6 +648,8 @@ func (h *Handler) keyIsPressed(k Key) bool { return h.gamepadStickIsPressed(stickCode(k.code), ebiten.StandardGamepadAxisRightStickHorizontal, ebiten.StandardGamepadAxisRightStickVertical) case keyGamepadStickMotion: return h.gamepadStickMotionIsPressed(stickCode(k.code)) + case keyMouseMove: + return h.mouseMoveIsPressed() case keyMouse: return ebiten.IsMouseButtonPressed(ebiten.MouseButton(k.code)) case keyMouseWithCtrl: @@ -883,6 +904,24 @@ func (h *Handler) mappedGamepadKey(keyCode int) ebiten.GamepadButton { } } +func (h *Handler) mouseMoveIsJustPressed() bool { + prevDelta := h.sys.prevCursorDelta + return prevDelta.X == 0 && prevDelta.Y == 0 && h.mouseMoveIsPressed() +} + +func (h *Handler) mouseMoveIsPressed() bool { + delta := deltaVec(h.getMouseVec(), h.getMousePrevVec()) + return delta.X != 0 || delta.Y != 0 +} + +func (h *Handler) getMousePrevVec() Vec { + return h.sys.prevCursorPos +} + +func (h *Handler) getMouseVec() Vec { + return h.sys.cursorPos +} + func (h *Handler) updateLastDevice(kind keyKind) { h.last = kind.device() } diff --git a/internal_key.go b/internal_key.go index 56770a6..bcfc2c9 100644 --- a/internal_key.go +++ b/internal_key.go @@ -16,6 +16,7 @@ const ( keyMouseWithShift keyMouseWithCtrlShift keyMouseDrag + keyMouseMove keyTouch keyTouchDrag keyWheel @@ -31,7 +32,7 @@ func (k keyKind) device() DeviceKind { return KeyboardDevice case keyGamepad, keyGamepadLeftStick, keyGamepadRightStick, keyGamepadStickMotion: return GamepadDevice - case keyMouse, keyMouseDrag: + case keyMouse, keyMouseMove, keyMouseDrag: return MouseDevice case keyWheel, keyWheelWithCtrl, keyWheelWithShift, keyWheelWithCtrlShift: return MouseDevice @@ -103,6 +104,7 @@ var keyKindFlagTable = [256]keyKindFlag{ keyMouseWithCtrl: keyFlagHasPos, keyMouseWithShift: keyFlagHasPos, keyMouseWithCtrlShift: keyFlagHasPos, + keyMouseMove: keyFlagHasPos, keyMouseDrag: keyFlagHasPos, keyTouch: keyFlagHasPos, keyWheel: keyFlagHasPos, diff --git a/internal_key_list.go b/internal_key_list.go index 9970c6b..40bf503 100644 --- a/internal_key_list.go +++ b/internal_key_list.go @@ -96,6 +96,7 @@ var allKeys = []Key{ KeyMouseLeft, KeyMouseLeftDrag, KeyMouseMiddle, + KeyMouseMove, KeyMouseRight, KeyN, KeyNumLock, diff --git a/key.go b/key.go index de358dc..c53880a 100644 --- a/key.go +++ b/key.go @@ -108,6 +108,7 @@ var ( // Mouse keys. var ( + KeyMouseMove = Key{kind: keyMouseMove, name: "mouse_move"} KeyMouseLeft = Key{code: int(ebiten.MouseButtonLeft), kind: keyMouse, name: "mouse_left_button"} KeyMouseRight = Key{code: int(ebiten.MouseButtonRight), kind: keyMouse, name: "mouse_right_button"} KeyMouseMiddle = Key{code: int(ebiten.MouseButtonMiddle), kind: keyMouse, name: "mouse_middle_button"} diff --git a/math_nodeps.go b/math_nodeps.go index fd2f6a8..5caa83c 100644 --- a/math_nodeps.go +++ b/math_nodeps.go @@ -26,6 +26,10 @@ type Vec struct { Y float64 } +func deltaVec(v, v2 Vec) Vec { + return Vec{X: v.X - v2.X, Y: v.Y - v2.Y} +} + func vecDistance(v, v2 Vec) float64 { return math.Sqrt((v.X-v2.X)*(v.X-v2.X) + (v.Y-v2.Y)*(v.Y-v2.Y)) } diff --git a/scan_axes.go b/scan_axes.go index ee0629b..d2976e3 100644 --- a/scan_axes.go +++ b/scan_axes.go @@ -1,7 +1,8 @@ package input const ( - scanAxesMouseWheel Action = iota + scanAxesMouseMove Action = iota + scanAxesMouseWheel scanAxesGamepadLStick scanAxesGamepadRStick scanAxesActionCount // always last to keep accurate count to iterate over @@ -11,6 +12,7 @@ var ( // use special handler and keymap to detect axes action events for key scanning purposes scanAxesHandler *Handler scanAxesKeymap = Keymap{ + scanAxesMouseMove: {KeyMouseMove}, scanAxesMouseWheel: {KeyWheelVertical}, scanAxesGamepadLStick: {KeyGamepadLStickMotion}, scanAxesGamepadRStick: {KeyGamepadRStickMotion}, @@ -43,7 +45,13 @@ func (s *KeyScanner) ScanAxes() (Key, KeyScanStatus) { k, status := s.scanAxesEvents() - // FIXME: mouse motion axes do not appear to be available in ebitengine-input, can we add it? + if !s.canScan { + if k.name != "" { + // do not start scanning until preexisting axes presses are no longer held + return Key{}, KeyScanUnchanged + } + s.canScan = true + } switch status { case KeyScanCompleted: diff --git a/scan_key.go b/scan_key.go index b50f320..2f0bdfa 100644 --- a/scan_key.go +++ b/scan_key.go @@ -97,6 +97,14 @@ func (s *KeyScanner) Scan() (Key, KeyScanStatus) { k, status = s.scanSpecialEvents() } + if !s.canScan { + if k.name != "" { + // do not start scanning until preexisting button presses are no longer held + return Key{}, KeyScanUnchanged + } + s.canScan = true + } + switch status { case KeyScanCompleted: s.canScan = false @@ -124,13 +132,6 @@ func (s *KeyScanner) scanMouse() (Key, KeyScanStatus) { } } - if !s.canScan { - if len(mouseKeys) != 0 { - return Key{}, KeyScanUnchanged - } - s.canScan = true - } - if len(mouseKeys) == 0 { return Key{}, KeyScanUnchanged } @@ -182,13 +183,6 @@ func (s *KeyScanner) scanGamepad() (Key, KeyScanStatus) { gamepadKeys := make([]ebiten.StandardGamepadButton, 0, 4) gamepadKeys = inpututil.AppendJustReleasedStandardGamepadButtons(ebiten.GamepadID(handlerID), gamepadKeys) - if !s.canScan { - if len(gamepadKeys) != 0 { - return Key{}, KeyScanUnchanged - } - s.canScan = true - } - if len(gamepadKeys) == 0 { return Key{}, KeyScanUnchanged } @@ -228,13 +222,6 @@ func (s *KeyScanner) scanKeyboard() (Key, KeyScanStatus) { keys := make([]ebiten.Key, 0, 4) keys = inpututil.AppendJustReleasedKeys(keys) - if !s.canScan { - if len(keys) != 0 { - return Key{}, KeyScanUnchanged - } - s.canScan = true - } - if len(keys) == 0 { // We're still collecting the keys. return Key{}, KeyScanUnchanged diff --git a/system.go b/system.go index 394b73c..20404b8 100644 --- a/system.go +++ b/system.go @@ -56,6 +56,8 @@ type System struct { mouseStartPos Vec // For "drag" event mouseDragPos Vec // For "drag" event cursorPos Vec + prevCursorPos Vec // For mouse move events + prevCursorDelta Vec // For mouse just moved event wheel Vec } @@ -176,6 +178,8 @@ func (sys *System) UpdateWithDelta(delta float64) { if sys.mouseEnabled { x, y := ebiten.CursorPosition() + sys.prevCursorDelta = deltaVec(sys.cursorPos, sys.prevCursorPos) + sys.prevCursorPos = sys.cursorPos sys.cursorPos = Vec{X: float64(x), Y: float64(y)} // We copy a lot from the touch-style drag gesture. From 281b6f131320c15dd51dc122722275810529e684 Mon Sep 17 00:00:00 2001 From: Eric H Date: Sat, 8 Aug 2026 17:28:20 -0400 Subject: [PATCH 15/28] custom panic message if KeyScanner not initialized properly --- _examples/remap/main.go | 2 +- scan_axes.go | 6 +++--- scan_key.go | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/_examples/remap/main.go b/_examples/remap/main.go index 1bb02c7..998e3ee 100644 --- a/_examples/remap/main.go +++ b/_examples/remap/main.go @@ -118,7 +118,7 @@ func (g *exampleGame) Update() error { // mouse wheel moves in reverse direction of draw position g.pos.Y -= info.Pos.Y } else if info.IsMouseMoveEvent() { - // mouse position (info.Pos) is absolute, the delta position (info.DeltaPos) + // mouse move position (info.Pos) is absolute, the delta position (info.DeltaPos) // can be useful, typically when using ebiten.CursorModeCaptured g.pos.X += info.DeltaPos.X g.pos.Y += info.DeltaPos.Y diff --git a/scan_axes.go b/scan_axes.go index d2976e3..e5f5d80 100644 --- a/scan_axes.go +++ b/scan_axes.go @@ -35,9 +35,9 @@ func newScanAxesHandler(scanHandler *Handler) *Handler { // * Unchanged - nothing updated since the last Scan() operation // * Completed - the user finished specifying the keys combination, you can use the Key as a new binding func (s *KeyScanner) ScanAxes() (Key, KeyScanStatus) { - - // FIXME: if s.h == nil, panic or err because this method requires handler instance to be provided? - + if s == nil || s.h == nil || s.h.sys == nil { + panic("KeyScanner must be initialized using: NewKeyScanner(*Handler)") + } if scanAxesHandler == nil { // special Handler is needed to determine axis events using special keymap scanAxesHandler = newScanAxesHandler(s.h) diff --git a/scan_key.go b/scan_key.go index 2f0bdfa..ba42d1f 100644 --- a/scan_key.go +++ b/scan_key.go @@ -69,8 +69,9 @@ func NewKeyScanner(h *Handler) *KeyScanner { func (s *KeyScanner) Scan() (Key, KeyScanStatus) { // TODO: respect the enabled input devices. - // FIXME: if s.h == nil, panic or err because this method requires handler instance to be provided? - + if s == nil || s.h == nil || s.h.sys == nil { + panic("KeyScanner must be initialized using: NewKeyScanner(*Handler)") + } if scanKeyHandler == nil { // special Handler is needed to determine certain events using special keymap scanKeyHandler = newScanKeyHandler(s.h) From 6d2e778bb29f2bbdd47f871ca8b06b2261dfa80b Mon Sep 17 00:00:00 2001 From: Eric H Date: Sun, 9 Aug 2026 00:09:05 -0400 Subject: [PATCH 16/28] rename to mouse motion for consistency with stick motion key --- _examples/remap/main.go | 4 ++-- event.go | 6 +++--- handler.go | 20 ++++++++++---------- internal_key.go | 6 +++--- internal_key_list.go | 2 +- key.go | 2 +- scan_axes.go | 4 ++-- 7 files changed, 22 insertions(+), 22 deletions(-) diff --git a/_examples/remap/main.go b/_examples/remap/main.go index 998e3ee..7143f77 100644 --- a/_examples/remap/main.go +++ b/_examples/remap/main.go @@ -117,7 +117,7 @@ func (g *exampleGame) Update() error { if info.IsMouseWheelEvent() { // mouse wheel moves in reverse direction of draw position g.pos.Y -= info.Pos.Y - } else if info.IsMouseMoveEvent() { + } else if info.IsMouseMotionEvent() { // mouse move position (info.Pos) is absolute, the delta position (info.DeltaPos) // can be useful, typically when using ebiten.CursorModeCaptured g.pos.X += info.DeltaPos.X @@ -194,7 +194,7 @@ func (g *exampleGame) makeKeymap() input.Keymap { func (g *exampleGame) Init() { g.k = input.KeyQ - g.axes = input.KeyMouseMove + g.axes = input.KeyMouseMotion g.inputHandler = g.inputSystem.NewHandler(0, g.makeKeymap()) g.keyScanner = input.NewKeyScanner(g.inputHandler) } diff --git a/event.go b/event.go index a7fba87..9b85db6 100644 --- a/event.go +++ b/event.go @@ -67,9 +67,9 @@ func (e EventInfo) HasPos() bool { return e.hasPos } // Use Duration field to get the press duration value. func (e EventInfo) HasDuration() bool { return e.hasDuration } -// IsMouseMoveEvent reports whether this event was triggered by mouse movement. -func (e EventInfo) IsMouseMoveEvent() bool { - return e.kind == keyMouseMove +// IsMouseMotionEvent reports whether this event was triggered by mouse movement. +func (e EventInfo) IsMouseMotionEvent() bool { + return e.kind == keyMouseMotion } // IsMouseWheelEvent reports whether this event was triggered by a mouse wheel. diff --git a/handler.go b/handler.go index cee6a7c..f310808 100644 --- a/handler.go +++ b/handler.go @@ -233,7 +233,7 @@ func (h *Handler) keyIsEnabled(k Key, mask DeviceKind) bool { return mask&MouseDevice != 0 case keyMouseWithShift: return mask&MouseDevice != 0 - case keyMouse, keyMouseMove: + case keyMouse, keyMouseMotion: return mask&MouseDevice != 0 case keyWheel, keyWheelWithCtrl, keyWheelWithShift, keyWheelWithCtrlShift: return mask&MouseDevice != 0 @@ -519,8 +519,8 @@ func (h *Handler) keyIsJustPressed(k Key) bool { return h.gamepadStickIsJustPressed(stickCode(k.code), ebiten.StandardGamepadAxisRightStickHorizontal, ebiten.StandardGamepadAxisRightStickVertical) case keyGamepadStickMotion: return h.gamepadStickMotionIsJustPressed(stickCode(k.code)) - case keyMouseMove: - return h.mouseMoveIsJustPressed() + case keyMouseMotion: + return h.mouseMotionIsJustPressed() case keyMouse: return inpututil.IsMouseButtonJustPressed(ebiten.MouseButton(k.code)) case keyMouseWithCtrl: @@ -574,7 +574,7 @@ func (h *Handler) getKeyStartPos(k Key) Vec { func (h *Handler) getKeyPos(k Key) Vec { var result Vec switch k.kind { - case keyMouse, keyMouseMove, keyMouseWithCtrl, keyMouseWithShift, keyMouseWithCtrlShift: + case keyMouse, keyMouseMotion, keyMouseWithCtrl, keyMouseWithShift, keyMouseWithCtrlShift: result = h.getMouseVec() case keyTouch: result = h.sys.touchTapPos @@ -593,7 +593,7 @@ func (h *Handler) getKeyPos(k Key) Vec { func (h *Handler) getKeyPrevPos(k Key) Vec { var result Vec switch k.kind { - case keyMouse, keyMouseMove, keyMouseWithCtrl, keyMouseWithShift, keyMouseWithCtrlShift: + case keyMouse, keyMouseMotion, keyMouseWithCtrl, keyMouseWithShift, keyMouseWithCtrlShift: result = h.getMousePrevVec() case keyTouchDrag: result = h.sys.touchStartPos @@ -648,8 +648,8 @@ func (h *Handler) keyIsPressed(k Key) bool { return h.gamepadStickIsPressed(stickCode(k.code), ebiten.StandardGamepadAxisRightStickHorizontal, ebiten.StandardGamepadAxisRightStickVertical) case keyGamepadStickMotion: return h.gamepadStickMotionIsPressed(stickCode(k.code)) - case keyMouseMove: - return h.mouseMoveIsPressed() + case keyMouseMotion: + return h.mouseMotionIsPressed() case keyMouse: return ebiten.IsMouseButtonPressed(ebiten.MouseButton(k.code)) case keyMouseWithCtrl: @@ -904,12 +904,12 @@ func (h *Handler) mappedGamepadKey(keyCode int) ebiten.GamepadButton { } } -func (h *Handler) mouseMoveIsJustPressed() bool { +func (h *Handler) mouseMotionIsJustPressed() bool { prevDelta := h.sys.prevCursorDelta - return prevDelta.X == 0 && prevDelta.Y == 0 && h.mouseMoveIsPressed() + return prevDelta.X == 0 && prevDelta.Y == 0 && h.mouseMotionIsPressed() } -func (h *Handler) mouseMoveIsPressed() bool { +func (h *Handler) mouseMotionIsPressed() bool { delta := deltaVec(h.getMouseVec(), h.getMousePrevVec()) return delta.X != 0 || delta.Y != 0 } diff --git a/internal_key.go b/internal_key.go index bcfc2c9..14c9c1d 100644 --- a/internal_key.go +++ b/internal_key.go @@ -16,7 +16,7 @@ const ( keyMouseWithShift keyMouseWithCtrlShift keyMouseDrag - keyMouseMove + keyMouseMotion keyTouch keyTouchDrag keyWheel @@ -32,7 +32,7 @@ func (k keyKind) device() DeviceKind { return KeyboardDevice case keyGamepad, keyGamepadLeftStick, keyGamepadRightStick, keyGamepadStickMotion: return GamepadDevice - case keyMouse, keyMouseMove, keyMouseDrag: + case keyMouse, keyMouseMotion, keyMouseDrag: return MouseDevice case keyWheel, keyWheelWithCtrl, keyWheelWithShift, keyWheelWithCtrlShift: return MouseDevice @@ -104,7 +104,7 @@ var keyKindFlagTable = [256]keyKindFlag{ keyMouseWithCtrl: keyFlagHasPos, keyMouseWithShift: keyFlagHasPos, keyMouseWithCtrlShift: keyFlagHasPos, - keyMouseMove: keyFlagHasPos, + keyMouseMotion: keyFlagHasPos, keyMouseDrag: keyFlagHasPos, keyTouch: keyFlagHasPos, keyWheel: keyFlagHasPos, diff --git a/internal_key_list.go b/internal_key_list.go index 40bf503..701c70c 100644 --- a/internal_key_list.go +++ b/internal_key_list.go @@ -96,7 +96,7 @@ var allKeys = []Key{ KeyMouseLeft, KeyMouseLeftDrag, KeyMouseMiddle, - KeyMouseMove, + KeyMouseMotion, KeyMouseRight, KeyN, KeyNumLock, diff --git a/key.go b/key.go index c53880a..70ca365 100644 --- a/key.go +++ b/key.go @@ -108,7 +108,7 @@ var ( // Mouse keys. var ( - KeyMouseMove = Key{kind: keyMouseMove, name: "mouse_move"} + KeyMouseMotion = Key{kind: keyMouseMotion, name: "mouse_motion"} KeyMouseLeft = Key{code: int(ebiten.MouseButtonLeft), kind: keyMouse, name: "mouse_left_button"} KeyMouseRight = Key{code: int(ebiten.MouseButtonRight), kind: keyMouse, name: "mouse_right_button"} KeyMouseMiddle = Key{code: int(ebiten.MouseButtonMiddle), kind: keyMouse, name: "mouse_middle_button"} diff --git a/scan_axes.go b/scan_axes.go index e5f5d80..9443364 100644 --- a/scan_axes.go +++ b/scan_axes.go @@ -1,7 +1,7 @@ package input const ( - scanAxesMouseMove Action = iota + scanAxesMouse Action = iota scanAxesMouseWheel scanAxesGamepadLStick scanAxesGamepadRStick @@ -12,7 +12,7 @@ var ( // use special handler and keymap to detect axes action events for key scanning purposes scanAxesHandler *Handler scanAxesKeymap = Keymap{ - scanAxesMouseMove: {KeyMouseMove}, + scanAxesMouse: {KeyMouseMotion}, scanAxesMouseWheel: {KeyWheelVertical}, scanAxesGamepadLStick: {KeyGamepadLStickMotion}, scanAxesGamepadRStick: {KeyGamepadRStickMotion}, From 517d93d150862222a4d002ab0c60b1cedf501e3a Mon Sep 17 00:00:00 2001 From: Eric H Date: Sun, 9 Aug 2026 14:42:15 -0400 Subject: [PATCH 17/28] use positive logic in example for clarity --- _examples/remap/main.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/_examples/remap/main.go b/_examples/remap/main.go index 7143f77..44f800b 100644 --- a/_examples/remap/main.go +++ b/_examples/remap/main.go @@ -133,10 +133,9 @@ func (g *exampleGame) Update() error { } // keep scanning of keys separate from axes to ensure events are isolated to just what is needed - if !g.scanningAxes { + if g.scanningKey { g.handleRemapKey() - } - if !g.scanningKey { + } else if g.scanningAxes { g.handleRemapAxes() } From 5150358d7aef0d15d26a22635a25ba313310a01d Mon Sep 17 00:00:00 2001 From: Eric H Date: Sun, 9 Aug 2026 14:44:52 -0400 Subject: [PATCH 18/28] Revert "use positive logic in example for clarity" This reverts commit 517d93d150862222a4d002ab0c60b1cedf501e3a. --- _examples/remap/main.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/_examples/remap/main.go b/_examples/remap/main.go index 44f800b..7143f77 100644 --- a/_examples/remap/main.go +++ b/_examples/remap/main.go @@ -133,9 +133,10 @@ func (g *exampleGame) Update() error { } // keep scanning of keys separate from axes to ensure events are isolated to just what is needed - if g.scanningKey { + if !g.scanningAxes { g.handleRemapKey() - } else if g.scanningAxes { + } + if !g.scanningKey { g.handleRemapAxes() } From e6b0ce0d9873587961e5889ee0d074bc518a2e56 Mon Sep 17 00:00:00 2001 From: Eric H Date: Sun, 9 Aug 2026 14:52:57 -0400 Subject: [PATCH 19/28] simplify remap example to make it clearer --- _examples/remap/main.go | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/_examples/remap/main.go b/_examples/remap/main.go index 7143f77..8bff1db 100644 --- a/_examples/remap/main.go +++ b/_examples/remap/main.go @@ -102,6 +102,13 @@ func (g *exampleGame) Update() error { } if !g.scanningKey && !g.scanningAxes { + // react to remap actions + if g.inputHandler.ActionIsJustPressed(ActionRemapKey) { + g.scanningKey = true + } else if g.inputHandler.ActionIsJustPressed(ActionRemapAxes) { + g.scanningAxes = true + } + // react to ping action by "highlighting" the "character" if g.inputHandler.ActionIsJustPressed(ActionPing) { fmt.Printf("ping! (activated with %s keybind)\n", g.k) @@ -133,10 +140,10 @@ func (g *exampleGame) Update() error { } // keep scanning of keys separate from axes to ensure events are isolated to just what is needed - if !g.scanningAxes { + if g.scanningKey { g.handleRemapKey() } - if !g.scanningKey { + if g.scanningAxes { g.handleRemapAxes() } @@ -144,13 +151,6 @@ func (g *exampleGame) Update() error { } func (g *exampleGame) handleRemapKey() { - if !g.scanningKey { - if g.inputHandler.ActionIsJustPressed(ActionRemapKey) { - g.scanningKey = true - } - return - } - k, status := g.keyScanner.Scan() if k == input.KeyWithModifier(input.KeyEnter, input.ModControl) || k == input.KeyWithModifier(input.KeyEnter, input.ModShift) || @@ -166,13 +166,6 @@ func (g *exampleGame) handleRemapKey() { } func (g *exampleGame) handleRemapAxes() { - if !g.scanningAxes { - if g.inputHandler.ActionIsJustPressed(ActionRemapAxes) { - g.scanningAxes = true - } - return - } - axes, status := g.keyScanner.ScanAxes() if status == input.KeyScanCompleted { // Check for the new key to be available. From 52b594b74c80d9b6d081acc7f77bc041a1394205 Mon Sep 17 00:00:00 2001 From: Eric Harbeston Date: Mon, 17 Aug 2026 20:47:01 -0400 Subject: [PATCH 20/28] remove mouse motion to move to its own PR - separating out the mouse motion code to focus this PR on adding gamepad and axis scanning capability --- _examples/remap/main.go | 7 +------ event.go | 5 ----- handler.go | 20 +++----------------- internal_key.go | 4 +--- internal_key_list.go | 1 - key.go | 1 - scan_axes.go | 4 +--- 7 files changed, 6 insertions(+), 36 deletions(-) diff --git a/_examples/remap/main.go b/_examples/remap/main.go index 8bff1db..f14dd92 100644 --- a/_examples/remap/main.go +++ b/_examples/remap/main.go @@ -124,11 +124,6 @@ func (g *exampleGame) Update() error { if info.IsMouseWheelEvent() { // mouse wheel moves in reverse direction of draw position g.pos.Y -= info.Pos.Y - } else if info.IsMouseMotionEvent() { - // mouse move position (info.Pos) is absolute, the delta position (info.DeltaPos) - // can be useful, typically when using ebiten.CursorModeCaptured - g.pos.X += info.DeltaPos.X - g.pos.Y += info.DeltaPos.Y } else { g.pos.X += info.Pos.X g.pos.Y += info.Pos.Y @@ -187,7 +182,7 @@ func (g *exampleGame) makeKeymap() input.Keymap { func (g *exampleGame) Init() { g.k = input.KeyQ - g.axes = input.KeyMouseMotion + g.axes = input.KeyGamepadLStickMotion g.inputHandler = g.inputSystem.NewHandler(0, g.makeKeymap()) g.keyScanner = input.NewKeyScanner(g.inputHandler) } diff --git a/event.go b/event.go index 9b85db6..8ab15f9 100644 --- a/event.go +++ b/event.go @@ -67,11 +67,6 @@ func (e EventInfo) HasPos() bool { return e.hasPos } // Use Duration field to get the press duration value. func (e EventInfo) HasDuration() bool { return e.hasDuration } -// IsMouseMotionEvent reports whether this event was triggered by mouse movement. -func (e EventInfo) IsMouseMotionEvent() bool { - return e.kind == keyMouseMotion -} - // IsMouseWheelEvent reports whether this event was triggered by a mouse wheel. func (e EventInfo) IsMouseWheelEvent() bool { return e.kind == keyWheel || e.kind == keyWheelWithCtrl || e.kind == keyWheelWithShift || e.kind == keyWheelWithCtrlShift diff --git a/handler.go b/handler.go index f310808..3284617 100644 --- a/handler.go +++ b/handler.go @@ -233,7 +233,7 @@ func (h *Handler) keyIsEnabled(k Key, mask DeviceKind) bool { return mask&MouseDevice != 0 case keyMouseWithShift: return mask&MouseDevice != 0 - case keyMouse, keyMouseMotion: + case keyMouse: return mask&MouseDevice != 0 case keyWheel, keyWheelWithCtrl, keyWheelWithShift, keyWheelWithCtrlShift: return mask&MouseDevice != 0 @@ -519,8 +519,6 @@ func (h *Handler) keyIsJustPressed(k Key) bool { return h.gamepadStickIsJustPressed(stickCode(k.code), ebiten.StandardGamepadAxisRightStickHorizontal, ebiten.StandardGamepadAxisRightStickVertical) case keyGamepadStickMotion: return h.gamepadStickMotionIsJustPressed(stickCode(k.code)) - case keyMouseMotion: - return h.mouseMotionIsJustPressed() case keyMouse: return inpututil.IsMouseButtonJustPressed(ebiten.MouseButton(k.code)) case keyMouseWithCtrl: @@ -574,7 +572,7 @@ func (h *Handler) getKeyStartPos(k Key) Vec { func (h *Handler) getKeyPos(k Key) Vec { var result Vec switch k.kind { - case keyMouse, keyMouseMotion, keyMouseWithCtrl, keyMouseWithShift, keyMouseWithCtrlShift: + case keyMouse, keyMouseWithCtrl, keyMouseWithShift, keyMouseWithCtrlShift: result = h.getMouseVec() case keyTouch: result = h.sys.touchTapPos @@ -593,7 +591,7 @@ func (h *Handler) getKeyPos(k Key) Vec { func (h *Handler) getKeyPrevPos(k Key) Vec { var result Vec switch k.kind { - case keyMouse, keyMouseMotion, keyMouseWithCtrl, keyMouseWithShift, keyMouseWithCtrlShift: + case keyMouse, keyMouseWithCtrl, keyMouseWithShift, keyMouseWithCtrlShift: result = h.getMousePrevVec() case keyTouchDrag: result = h.sys.touchStartPos @@ -648,8 +646,6 @@ func (h *Handler) keyIsPressed(k Key) bool { return h.gamepadStickIsPressed(stickCode(k.code), ebiten.StandardGamepadAxisRightStickHorizontal, ebiten.StandardGamepadAxisRightStickVertical) case keyGamepadStickMotion: return h.gamepadStickMotionIsPressed(stickCode(k.code)) - case keyMouseMotion: - return h.mouseMotionIsPressed() case keyMouse: return ebiten.IsMouseButtonPressed(ebiten.MouseButton(k.code)) case keyMouseWithCtrl: @@ -904,16 +900,6 @@ func (h *Handler) mappedGamepadKey(keyCode int) ebiten.GamepadButton { } } -func (h *Handler) mouseMotionIsJustPressed() bool { - prevDelta := h.sys.prevCursorDelta - return prevDelta.X == 0 && prevDelta.Y == 0 && h.mouseMotionIsPressed() -} - -func (h *Handler) mouseMotionIsPressed() bool { - delta := deltaVec(h.getMouseVec(), h.getMousePrevVec()) - return delta.X != 0 || delta.Y != 0 -} - func (h *Handler) getMousePrevVec() Vec { return h.sys.prevCursorPos } diff --git a/internal_key.go b/internal_key.go index 14c9c1d..56770a6 100644 --- a/internal_key.go +++ b/internal_key.go @@ -16,7 +16,6 @@ const ( keyMouseWithShift keyMouseWithCtrlShift keyMouseDrag - keyMouseMotion keyTouch keyTouchDrag keyWheel @@ -32,7 +31,7 @@ func (k keyKind) device() DeviceKind { return KeyboardDevice case keyGamepad, keyGamepadLeftStick, keyGamepadRightStick, keyGamepadStickMotion: return GamepadDevice - case keyMouse, keyMouseMotion, keyMouseDrag: + case keyMouse, keyMouseDrag: return MouseDevice case keyWheel, keyWheelWithCtrl, keyWheelWithShift, keyWheelWithCtrlShift: return MouseDevice @@ -104,7 +103,6 @@ var keyKindFlagTable = [256]keyKindFlag{ keyMouseWithCtrl: keyFlagHasPos, keyMouseWithShift: keyFlagHasPos, keyMouseWithCtrlShift: keyFlagHasPos, - keyMouseMotion: keyFlagHasPos, keyMouseDrag: keyFlagHasPos, keyTouch: keyFlagHasPos, keyWheel: keyFlagHasPos, diff --git a/internal_key_list.go b/internal_key_list.go index 701c70c..9970c6b 100644 --- a/internal_key_list.go +++ b/internal_key_list.go @@ -96,7 +96,6 @@ var allKeys = []Key{ KeyMouseLeft, KeyMouseLeftDrag, KeyMouseMiddle, - KeyMouseMotion, KeyMouseRight, KeyN, KeyNumLock, diff --git a/key.go b/key.go index 70ca365..de358dc 100644 --- a/key.go +++ b/key.go @@ -108,7 +108,6 @@ var ( // Mouse keys. var ( - KeyMouseMotion = Key{kind: keyMouseMotion, name: "mouse_motion"} KeyMouseLeft = Key{code: int(ebiten.MouseButtonLeft), kind: keyMouse, name: "mouse_left_button"} KeyMouseRight = Key{code: int(ebiten.MouseButtonRight), kind: keyMouse, name: "mouse_right_button"} KeyMouseMiddle = Key{code: int(ebiten.MouseButtonMiddle), kind: keyMouse, name: "mouse_middle_button"} diff --git a/scan_axes.go b/scan_axes.go index 9443364..933791b 100644 --- a/scan_axes.go +++ b/scan_axes.go @@ -1,8 +1,7 @@ package input const ( - scanAxesMouse Action = iota - scanAxesMouseWheel + scanAxesMouseWheel Action = iota scanAxesGamepadLStick scanAxesGamepadRStick scanAxesActionCount // always last to keep accurate count to iterate over @@ -12,7 +11,6 @@ var ( // use special handler and keymap to detect axes action events for key scanning purposes scanAxesHandler *Handler scanAxesKeymap = Keymap{ - scanAxesMouse: {KeyMouseMotion}, scanAxesMouseWheel: {KeyWheelVertical}, scanAxesGamepadLStick: {KeyGamepadLStickMotion}, scanAxesGamepadRStick: {KeyGamepadRStickMotion}, From 55ebd4d8693633636f129552edb8472b753bb5f9 Mon Sep 17 00:00:00 2001 From: Eric Harbeston Date: Mon, 17 Aug 2026 21:12:57 -0400 Subject: [PATCH 21/28] replace use of singleton scanning handlers with instance property --- scan_axes.go | 12 +++++------- scan_key.go | 16 +++++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/scan_axes.go b/scan_axes.go index 933791b..8859b91 100644 --- a/scan_axes.go +++ b/scan_axes.go @@ -8,9 +8,7 @@ const ( ) var ( - // use special handler and keymap to detect axes action events for key scanning purposes - scanAxesHandler *Handler - scanAxesKeymap = Keymap{ + scanAxesKeymap = Keymap{ scanAxesMouseWheel: {KeyWheelVertical}, scanAxesGamepadLStick: {KeyGamepadLStickMotion}, scanAxesGamepadRStick: {KeyGamepadRStickMotion}, @@ -36,9 +34,9 @@ func (s *KeyScanner) ScanAxes() (Key, KeyScanStatus) { if s == nil || s.h == nil || s.h.sys == nil { panic("KeyScanner must be initialized using: NewKeyScanner(*Handler)") } - if scanAxesHandler == nil { + if s._scanAxesHelper == nil { // special Handler is needed to determine axis events using special keymap - scanAxesHandler = newScanAxesHandler(s.h) + s._scanAxesHelper = newScanAxesHandler(s.h) } k, status := s.scanAxesEvents() @@ -54,14 +52,14 @@ func (s *KeyScanner) ScanAxes() (Key, KeyScanStatus) { switch status { case KeyScanCompleted: s.canScan = false - scanAxesHandler = nil + s._scanAxesHelper = nil } return k, status } func (s *KeyScanner) scanAxesEvents() (Key, KeyScanStatus) { for a := Action(0); a < scanAxesActionCount; a++ { - if _, ok := scanAxesHandler.JustPressedActionInfo(a); ok { + if _, ok := s._scanAxesHelper.JustPressedActionInfo(a); ok { k := scanAxesKeymap[a][0] return k, KeyScanCompleted } diff --git a/scan_key.go b/scan_key.go index ba42d1f..a8ff091 100644 --- a/scan_key.go +++ b/scan_key.go @@ -20,9 +20,7 @@ const ( ) var ( - // use special handler and keymap to detect certain events for key scanning purposes - scanKeyHandler *Handler - scanKeyKeymap = Keymap{ + scanKeyKeymap = Keymap{ scanKeyMouseWheelDown: {KeyWheelDown}, scanKeyMouseWheelUp: {KeyWheelUp}, } @@ -41,6 +39,10 @@ func newScanKeyHandler(scanHandler *Handler) *Handler { type KeyScanner struct { canScan bool h *Handler + + // uses special handler and keymap to detect certain events for key and axes scanning purposes + _scanKeyHelper *Handler + _scanAxesHelper *Handler } // NewKeyScanner creates a key scanner for the specifier input Handler. @@ -72,9 +74,9 @@ func (s *KeyScanner) Scan() (Key, KeyScanStatus) { if s == nil || s.h == nil || s.h.sys == nil { panic("KeyScanner must be initialized using: NewKeyScanner(*Handler)") } - if scanKeyHandler == nil { + if s._scanKeyHelper == nil { // special Handler is needed to determine certain events using special keymap - scanKeyHandler = newScanKeyHandler(s.h) + s._scanKeyHelper = newScanKeyHandler(s.h) } // Note that this function may not be needed by some users, @@ -109,14 +111,14 @@ func (s *KeyScanner) Scan() (Key, KeyScanStatus) { switch status { case KeyScanCompleted: s.canScan = false - scanKeyHandler = nil + s._scanKeyHelper = nil } return k, status } func (s *KeyScanner) scanSpecialEvents() (Key, KeyScanStatus) { for a := Action(0); a < scanKeyActionCount; a++ { - if _, ok := scanKeyHandler.JustPressedActionInfo(a); ok { + if _, ok := s._scanKeyHelper.JustPressedActionInfo(a); ok { k := scanKeyKeymap[a][0] return k, KeyScanCompleted } From da5b71f63ac6071349d6c56014903d2500088ac1 Mon Sep 17 00:00:00 2001 From: Eric Harbeston Date: Mon, 17 Aug 2026 21:30:00 -0400 Subject: [PATCH 22/28] add missing deltaVec to gmath variant functions --- math_gmath.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/math_gmath.go b/math_gmath.go index bdab9f2..3c9aaa9 100644 --- a/math_gmath.go +++ b/math_gmath.go @@ -8,6 +8,10 @@ import ( type Vec = gmath.Vec +func deltaVec(v, v2 Vec) Vec { + return v.Sub(v2) +} + func vecDistance(v, v2 Vec) float64 { return v.DistanceTo(v2) } From 4e00767b3a84f7a996e587bbdc0d240a8d4c044c Mon Sep 17 00:00:00 2001 From: Eric H Date: Fri, 21 Aug 2026 19:53:17 -0400 Subject: [PATCH 23/28] update name of special key scanning funcs/vars --- scan_key.go | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/scan_key.go b/scan_key.go index a8ff091..aa1e2d5 100644 --- a/scan_key.go +++ b/scan_key.go @@ -16,18 +16,19 @@ const ( const ( scanKeyMouseWheelDown Action = iota scanKeyMouseWheelUp - scanKeyActionCount // always last to keep accurate count to iterate over + scanSpecialKeyActionCount // always last to keep accurate count to iterate over ) var ( - scanKeyKeymap = Keymap{ + specialKeyKeymap = Keymap{ scanKeyMouseWheelDown: {KeyWheelDown}, scanKeyMouseWheelUp: {KeyWheelUp}, } ) -func newScanKeyHandler(scanHandler *Handler) *Handler { - return scanHandler.sys.NewHandler(scanHandler.id, scanKeyKeymap) +func newSpecialKeyScanHandler(scanHandler *Handler) *Handler { + // create new handler used to help scan for special keys to be able to detect them + return scanHandler.sys.NewHandler(scanHandler.id, specialKeyKeymap) } // KeyScanner checks the currently pressed keys and buttons and tries to map them @@ -41,8 +42,8 @@ type KeyScanner struct { h *Handler // uses special handler and keymap to detect certain events for key and axes scanning purposes - _scanKeyHelper *Handler - _scanAxesHelper *Handler + _scanSpecialKeyHelper *Handler + _scanAxesHelper *Handler } // NewKeyScanner creates a key scanner for the specifier input Handler. @@ -74,9 +75,9 @@ func (s *KeyScanner) Scan() (Key, KeyScanStatus) { if s == nil || s.h == nil || s.h.sys == nil { panic("KeyScanner must be initialized using: NewKeyScanner(*Handler)") } - if s._scanKeyHelper == nil { + if s._scanSpecialKeyHelper == nil { // special Handler is needed to determine certain events using special keymap - s._scanKeyHelper = newScanKeyHandler(s.h) + s._scanSpecialKeyHelper = newSpecialKeyScanHandler(s.h) } // Note that this function may not be needed by some users, @@ -97,7 +98,7 @@ func (s *KeyScanner) Scan() (Key, KeyScanStatus) { } if status == KeyScanUnchanged { // scan for special keys, like mouse wheel up/down - k, status = s.scanSpecialEvents() + k, status = s.scanSpecialKeys() } if !s.canScan { @@ -111,15 +112,15 @@ func (s *KeyScanner) Scan() (Key, KeyScanStatus) { switch status { case KeyScanCompleted: s.canScan = false - s._scanKeyHelper = nil + s._scanSpecialKeyHelper = nil } return k, status } -func (s *KeyScanner) scanSpecialEvents() (Key, KeyScanStatus) { - for a := Action(0); a < scanKeyActionCount; a++ { - if _, ok := s._scanKeyHelper.JustPressedActionInfo(a); ok { - k := scanKeyKeymap[a][0] +func (s *KeyScanner) scanSpecialKeys() (Key, KeyScanStatus) { + for a := Action(0); a < scanSpecialKeyActionCount; a++ { + if _, ok := s._scanSpecialKeyHelper.JustPressedActionInfo(a); ok { + k := specialKeyKeymap[a][0] return k, KeyScanCompleted } } From da4d35f4301e207496a5fa2ab0e1adcaab2d2aaf Mon Sep 17 00:00:00 2001 From: Eric H Date: Fri, 21 Aug 2026 20:49:43 -0400 Subject: [PATCH 24/28] reintroduce unit test for scan keyboard --- scan_key.go | 21 ++++++++---------- scan_key_test.go | 58 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 12 deletions(-) create mode 100644 scan_key_test.go diff --git a/scan_key.go b/scan_key.go index aa1e2d5..e7bb961 100644 --- a/scan_key.go +++ b/scan_key.go @@ -87,7 +87,7 @@ func (s *KeyScanner) Scan() (Key, KeyScanStatus) { // This function doesn't have to be very fast, but it should be relatively // inexpensive for the "no keys were pressed" case. // When some keys combo is being pressed, it's OK to spend some resources. - k, status := s.scanKeyboard() + k, status := s.scanKeyboard(nil, nil) if status == KeyScanUnchanged { // scan for mouse buttons k, status = s.scanMouse() @@ -164,7 +164,7 @@ Loop: } // attach any held key modifiers - keymod := s.scanKeyModifiers() + keymod := s.scanKeyModifiers(nil) if keymod != ModUnknown { switch mappedKey.kind { case keyMouse: @@ -221,9 +221,7 @@ Loop: return mappedKey, status } -func (s *KeyScanner) scanKeyboard() (Key, KeyScanStatus) { - // This slice is stack-allocated; for the most cases, 4 keys are enough. - keys := make([]ebiten.Key, 0, 4) +func (s *KeyScanner) scanKeyboard(keys []ebiten.Key, heldKeys []ebiten.Key) (Key, KeyScanStatus) { keys = inpututil.AppendJustReleasedKeys(keys) if len(keys) == 0 { @@ -259,7 +257,7 @@ Loop: } // attach any held key modifiers - keymod := s.scanKeyModifiers() + keymod := s.scanKeyModifiers(heldKeys) if keymod != ModUnknown { switch mappedKey.kind { case keyKeyboard: @@ -274,22 +272,21 @@ Loop: return mappedKey, status } -func (s *KeyScanner) scanKeyModifiers() KeyModifier { - if !s.canScan { - return ModUnknown - } - - heldKeys := make([]ebiten.Key, 0, 4) +func (s *KeyScanner) scanKeyModifiers(heldKeys []ebiten.Key) KeyModifier { heldKeys = inpututil.AppendPressedKeys(heldKeys) var ctrlKey Key var shiftKey Key for _, k := range heldKeys { switch k { + case ebiten.KeyControl: + ctrlKey = KeyControl case ebiten.KeyControlLeft: ctrlKey = KeyControlLeft case ebiten.KeyControlRight: ctrlKey = KeyControlRight + case ebiten.KeyShift: + shiftKey = KeyShift case ebiten.KeyShiftLeft: shiftKey = KeyShiftLeft case ebiten.KeyShiftRight: diff --git a/scan_key_test.go b/scan_key_test.go new file mode 100644 index 0000000..3cc9b72 --- /dev/null +++ b/scan_key_test.go @@ -0,0 +1,58 @@ +package input + +import ( + "testing" + + "github.com/hajimehoshi/ebiten/v2" +) + +func TestScanKeyboard(t *testing.T) { + testHandler := &Handler{id: 0} + testScanner := NewKeyScanner(testHandler) + testScanner.canScan = true + + tests := []struct { + keys []ebiten.Key + heldKeys []ebiten.Key + want Key + }{ + // Sanity tests. + {[]ebiten.Key{}, nil, Key{}}, + + // The simple cases with a single key. + {[]ebiten.Key{ebiten.KeyB}, nil, KeyB}, + {[]ebiten.Key{ebiten.KeyEnter}, nil, KeyEnter}, + {[]ebiten.Key{ebiten.KeyControlLeft}, nil, KeyControlLeft}, + {[]ebiten.Key{ebiten.KeyControlRight}, nil, KeyControlRight}, + {[]ebiten.Key{ebiten.KeyControl}, nil, KeyControl}, + + // Multiple key candidates without a way to merge them into a single Key. + {[]ebiten.Key{ebiten.KeyB, ebiten.KeyA}, nil, KeyA}, + {[]ebiten.Key{ebiten.KeyA, ebiten.KeyB}, nil, KeyA}, + + // Control modifiers. + {[]ebiten.Key{ebiten.KeyC}, []ebiten.Key{ebiten.KeyControlLeft}, KeyWithModifier(KeyC, ModControl)}, + {[]ebiten.Key{ebiten.KeyC}, []ebiten.Key{ebiten.KeyControlRight}, KeyWithModifier(KeyC, ModControl)}, + {[]ebiten.Key{ebiten.KeyC}, []ebiten.Key{ebiten.KeyControl}, KeyWithModifier(KeyC, ModControl)}, + {[]ebiten.Key{ebiten.KeyE}, []ebiten.Key{ebiten.KeyControlLeft}, KeyWithModifier(KeyE, ModControl)}, + {[]ebiten.Key{ebiten.KeyE}, []ebiten.Key{ebiten.KeyControlRight}, KeyWithModifier(KeyE, ModControl)}, + + // Shift modifiers. + {[]ebiten.Key{ebiten.KeyF}, []ebiten.Key{ebiten.KeyShiftLeft, ebiten.KeyShift}, KeyWithModifier(KeyF, ModShift)}, + + // Control+Shift modifiers. + {[]ebiten.Key{ebiten.KeyC}, []ebiten.Key{ebiten.KeyControlRight, ebiten.KeyShiftLeft}, KeyWithModifier(KeyC, ModControlShift)}, + {[]ebiten.Key{ebiten.KeyA}, []ebiten.Key{ebiten.KeyControl, ebiten.KeyShift}, KeyWithModifier(KeyA, ModControlShift)}, + {[]ebiten.Key{ebiten.KeyA}, []ebiten.Key{ebiten.KeyControlLeft, ebiten.KeyShiftLeft}, KeyWithModifier(KeyA, ModControlShift)}, + {[]ebiten.Key{ebiten.KeyA}, []ebiten.Key{ebiten.KeyControlRight, ebiten.KeyShiftRight}, KeyWithModifier(KeyA, ModControlShift)}, + {[]ebiten.Key{ebiten.KeyA}, []ebiten.Key{ebiten.KeyControlLeft, ebiten.KeyShiftRight}, KeyWithModifier(KeyA, ModControlShift)}, + } + + for i, test := range tests { + have, _ := testScanner.scanKeyboard(test.keys, test.heldKeys) + if have != test.want { + t.Fatalf("test[%d] failed:\nhave: %s (%#v)\nwant: %s (%#v)", + i, have, have, test.want, test.want) + } + } +} From e0674e493ab1d9c75527fe33245c70dfdc81a918 Mon Sep 17 00:00:00 2001 From: Eric H Date: Fri, 21 Aug 2026 21:12:43 -0400 Subject: [PATCH 25/28] add unit tests for scan mouse/gamepad --- scan_key.go | 12 +++---- scan_key_test.go | 85 ++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 87 insertions(+), 10 deletions(-) diff --git a/scan_key.go b/scan_key.go index e7bb961..c2196e7 100644 --- a/scan_key.go +++ b/scan_key.go @@ -90,11 +90,11 @@ func (s *KeyScanner) Scan() (Key, KeyScanStatus) { k, status := s.scanKeyboard(nil, nil) if status == KeyScanUnchanged { // scan for mouse buttons - k, status = s.scanMouse() + k, status = s.scanMouse(nil, nil) } if status == KeyScanUnchanged { // scan for gamepad buttons - k, status = s.scanGamepad() + k, status = s.scanGamepad(nil) } if status == KeyScanUnchanged { // scan for special keys, like mouse wheel up/down @@ -127,9 +127,8 @@ func (s *KeyScanner) scanSpecialKeys() (Key, KeyScanStatus) { return Key{}, KeyScanUnchanged } -func (s *KeyScanner) scanMouse() (Key, KeyScanStatus) { +func (s *KeyScanner) scanMouse(mouseKeys []ebiten.MouseButton, heldKeys []ebiten.Key) (Key, KeyScanStatus) { // We will need to do our own "AppendJustReleased" for mouse button presses - mouseKeys := make([]ebiten.MouseButton, 0, 4) for k := ebiten.MouseButton(0); k < ebiten.MouseButtonMax; k++ { if inpututil.IsMouseButtonJustReleased(k) { mouseKeys = append(mouseKeys, k) @@ -164,7 +163,7 @@ Loop: } // attach any held key modifiers - keymod := s.scanKeyModifiers(nil) + keymod := s.scanKeyModifiers(heldKeys) if keymod != ModUnknown { switch mappedKey.kind { case keyMouse: @@ -179,12 +178,11 @@ Loop: return mappedKey, status } -func (s *KeyScanner) scanGamepad() (Key, KeyScanStatus) { +func (s *KeyScanner) scanGamepad(gamepadKeys []ebiten.StandardGamepadButton) (Key, KeyScanStatus) { var handlerID uint8 if s.h != nil { handlerID = s.h.id } - gamepadKeys := make([]ebiten.StandardGamepadButton, 0, 4) gamepadKeys = inpututil.AppendJustReleasedStandardGamepadButtons(ebiten.GamepadID(handlerID), gamepadKeys) if len(gamepadKeys) == 0 { diff --git a/scan_key_test.go b/scan_key_test.go index 3cc9b72..56cd881 100644 --- a/scan_key_test.go +++ b/scan_key_test.go @@ -35,13 +35,15 @@ func TestScanKeyboard(t *testing.T) { {[]ebiten.Key{ebiten.KeyC}, []ebiten.Key{ebiten.KeyControlRight}, KeyWithModifier(KeyC, ModControl)}, {[]ebiten.Key{ebiten.KeyC}, []ebiten.Key{ebiten.KeyControl}, KeyWithModifier(KeyC, ModControl)}, {[]ebiten.Key{ebiten.KeyE}, []ebiten.Key{ebiten.KeyControlLeft}, KeyWithModifier(KeyE, ModControl)}, - {[]ebiten.Key{ebiten.KeyE}, []ebiten.Key{ebiten.KeyControlRight}, KeyWithModifier(KeyE, ModControl)}, + {[]ebiten.Key{ebiten.KeyE}, []ebiten.Key{ebiten.KeyControlRight, ebiten.KeyA}, KeyWithModifier(KeyE, ModControl)}, // Shift modifiers. - {[]ebiten.Key{ebiten.KeyF}, []ebiten.Key{ebiten.KeyShiftLeft, ebiten.KeyShift}, KeyWithModifier(KeyF, ModShift)}, + {[]ebiten.Key{ebiten.KeyF}, []ebiten.Key{ebiten.KeyShiftLeft}, KeyWithModifier(KeyF, ModShift)}, + {[]ebiten.Key{ebiten.KeyF}, []ebiten.Key{ebiten.KeyShiftRight}, KeyWithModifier(KeyF, ModShift)}, + {[]ebiten.Key{ebiten.KeyF}, []ebiten.Key{ebiten.KeyShift}, KeyWithModifier(KeyF, ModShift)}, // Control+Shift modifiers. - {[]ebiten.Key{ebiten.KeyC}, []ebiten.Key{ebiten.KeyControlRight, ebiten.KeyShiftLeft}, KeyWithModifier(KeyC, ModControlShift)}, + {[]ebiten.Key{ebiten.KeyC}, []ebiten.Key{ebiten.KeyControlLeft, ebiten.KeyControl, ebiten.KeyShiftLeft, ebiten.KeyShift}, KeyWithModifier(KeyC, ModControlShift)}, {[]ebiten.Key{ebiten.KeyA}, []ebiten.Key{ebiten.KeyControl, ebiten.KeyShift}, KeyWithModifier(KeyA, ModControlShift)}, {[]ebiten.Key{ebiten.KeyA}, []ebiten.Key{ebiten.KeyControlLeft, ebiten.KeyShiftLeft}, KeyWithModifier(KeyA, ModControlShift)}, {[]ebiten.Key{ebiten.KeyA}, []ebiten.Key{ebiten.KeyControlRight, ebiten.KeyShiftRight}, KeyWithModifier(KeyA, ModControlShift)}, @@ -56,3 +58,80 @@ func TestScanKeyboard(t *testing.T) { } } } + +func TestScanMouse(t *testing.T) { + testHandler := &Handler{id: 0} + testScanner := NewKeyScanner(testHandler) + testScanner.canScan = true + + tests := []struct { + keys []ebiten.MouseButton + heldKeys []ebiten.Key + want Key + }{ + // Sanity tests. + {[]ebiten.MouseButton{}, nil, Key{}}, + + // The simple cases with a single key. + {[]ebiten.MouseButton{ebiten.MouseButtonLeft}, nil, KeyMouseLeft}, + {[]ebiten.MouseButton{ebiten.MouseButtonRight}, nil, KeyMouseRight}, + {[]ebiten.MouseButton{ebiten.MouseButtonMiddle}, nil, KeyMouseMiddle}, + {[]ebiten.MouseButton{ebiten.MouseButton3}, nil, KeyMouseBack}, + {[]ebiten.MouseButton{ebiten.MouseButton4}, nil, KeyMouseForward}, + + // Multiple key candidates without a way to merge them into a single Key. + {[]ebiten.MouseButton{ebiten.MouseButtonLeft, ebiten.MouseButtonRight}, nil, KeyMouseLeft}, + {[]ebiten.MouseButton{ebiten.MouseButtonRight, ebiten.MouseButtonLeft}, nil, KeyMouseLeft}, + + // Control modifiers. + {[]ebiten.MouseButton{ebiten.MouseButtonLeft}, []ebiten.Key{ebiten.KeyControl}, KeyWithModifier(KeyMouseLeft, ModControl)}, + + // Shift modifiers. + {[]ebiten.MouseButton{ebiten.MouseButtonRight}, []ebiten.Key{ebiten.KeyShift}, KeyWithModifier(KeyMouseRight, ModShift)}, + + // Control+Shift modifiers. + {[]ebiten.MouseButton{ebiten.MouseButtonLeft}, []ebiten.Key{ebiten.KeyControlLeft, ebiten.KeyShiftLeft}, KeyWithModifier(KeyMouseLeft, ModControlShift)}, + {[]ebiten.MouseButton{ebiten.MouseButtonRight}, []ebiten.Key{ebiten.KeyControlRight, ebiten.KeyShiftRight}, KeyWithModifier(KeyMouseRight, ModControlShift)}, + {[]ebiten.MouseButton{ebiten.MouseButtonMiddle}, []ebiten.Key{ebiten.KeyControl, ebiten.KeyShift}, KeyWithModifier(KeyMouseMiddle, ModControlShift)}, + } + + for i, test := range tests { + have, _ := testScanner.scanMouse(test.keys, test.heldKeys) + if have != test.want { + t.Fatalf("test[%d] failed:\nhave: %s (%#v)\nwant: %s (%#v)", + i, have, have, test.want, test.want) + } + } +} + +func TestScanGamepad(t *testing.T) { + testHandler := &Handler{id: 0} + testScanner := NewKeyScanner(testHandler) + testScanner.canScan = true + + tests := []struct { + keys []ebiten.StandardGamepadButton + want Key + }{ + // Sanity tests. + {[]ebiten.StandardGamepadButton{}, Key{}}, + + // The simple cases with a single button. + {[]ebiten.StandardGamepadButton{ebiten.StandardGamepadButtonRightBottom}, KeyGamepadA}, + {[]ebiten.StandardGamepadButton{ebiten.StandardGamepadButtonRightRight}, KeyGamepadB}, + {[]ebiten.StandardGamepadButton{ebiten.StandardGamepadButtonRightLeft}, KeyGamepadX}, + {[]ebiten.StandardGamepadButton{ebiten.StandardGamepadButtonRightTop}, KeyGamepadY}, + + // Multiple button candidates without a way to merge them into a single Key. + {[]ebiten.StandardGamepadButton{ebiten.StandardGamepadButtonRightBottom, ebiten.StandardGamepadButtonLeftStick}, KeyGamepadA}, + {[]ebiten.StandardGamepadButton{ebiten.StandardGamepadButtonLeftStick, ebiten.StandardGamepadButtonRightBottom}, KeyGamepadA}, + } + + for i, test := range tests { + have, _ := testScanner.scanGamepad(test.keys) + if have != test.want { + t.Fatalf("test[%d] failed:\nhave: %s (%#v)\nwant: %s (%#v)", + i, have, have, test.want, test.want) + } + } +} From c2918849d566af922b6e036902b594a4c01fddd0 Mon Sep 17 00:00:00 2001 From: Eric H Date: Sun, 30 Aug 2026 21:50:05 -0400 Subject: [PATCH 26/28] update scanning keys/axes only for enabled devices --- event.go | 2 +- handler.go | 15 ++++++++ internal_key.go | 7 ++-- scan_axes.go | 11 ++++-- scan_key.go | 91 +++++++++++++++++++++++++++--------------------- scan_key_test.go | 40 ++++++++++++++++----- system.go | 5 +++ 7 files changed, 115 insertions(+), 56 deletions(-) diff --git a/event.go b/event.go index 8ab15f9..451159c 100644 --- a/event.go +++ b/event.go @@ -105,7 +105,7 @@ func (e EventInfo) IsGamepadEvent() bool { // Usually, it returns a single device mask, but sometimes an action // can be triggered by several devices. // For example, a ctrl+click key would involve both keyboard and mouse devices, -// resulting in a KeyboardDevice|KeyboardMouse value. +// resulting in a KeyboardDevice|MouseDevice value. // // A simulated event would have a zero mask returned, meaning // no real device was involved. diff --git a/handler.go b/handler.go index 3284617..a06c1b6 100644 --- a/handler.go +++ b/handler.go @@ -58,6 +58,21 @@ func (h *Handler) GamepadConnected() bool { return false } +// GamepadEventsEnabled reports whether this handler can receive gamepad events. +func (h *Handler) GamepadEventsEnabled() bool { + return h.sys.gamepadEnabled +} + +// KeyboardEventsEnabled reports whether this handler can receive keyboard events. +func (h *Handler) KeyboardEventsEnabled() bool { + return h.sys.keyboardEnabled +} + +// MouseEventsEnabled reports whether this handler can receive mouse events. +func (h *Handler) MouseEventsEnabled() bool { + return h.sys.mouseEnabled +} + // TouchEventsEnabled reports whether this handler can receive screen touch events. func (h *Handler) TouchEventsEnabled() bool { return h.sys.touchEnabled diff --git a/internal_key.go b/internal_key.go index 56770a6..9663e94 100644 --- a/internal_key.go +++ b/internal_key.go @@ -31,11 +31,10 @@ func (k keyKind) device() DeviceKind { return KeyboardDevice case keyGamepad, keyGamepadLeftStick, keyGamepadRightStick, keyGamepadStickMotion: return GamepadDevice - case keyMouse, keyMouseDrag: + case keyMouse, keyMouseDrag, keyWheel: return MouseDevice - case keyWheel, keyWheelWithCtrl, keyWheelWithShift, keyWheelWithCtrlShift: - return MouseDevice - case keyMouseWithCtrl, keyMouseWithShift, keyMouseWithCtrlShift: + case keyMouseWithCtrl, keyMouseWithShift, keyMouseWithCtrlShift, + keyWheelWithCtrl, keyWheelWithShift, keyWheelWithCtrlShift: return MouseDevice | KeyboardDevice case keyTouch, keyTouchDrag: return TouchDevice diff --git a/scan_axes.go b/scan_axes.go index 8859b91..80dee5b 100644 --- a/scan_axes.go +++ b/scan_axes.go @@ -4,7 +4,6 @@ const ( scanAxesMouseWheel Action = iota scanAxesGamepadLStick scanAxesGamepadRStick - scanAxesActionCount // always last to keep accurate count to iterate over ) var ( @@ -58,10 +57,16 @@ func (s *KeyScanner) ScanAxes() (Key, KeyScanStatus) { } func (s *KeyScanner) scanAxesEvents() (Key, KeyScanStatus) { - for a := Action(0); a < scanAxesActionCount; a++ { + for a := range scanAxesKeymap { if _, ok := s._scanAxesHelper.JustPressedActionInfo(a); ok { k := scanAxesKeymap[a][0] - return k, KeyScanCompleted + // make sure the device for the axes is enabled for the input system + device := k.kind.device() + if (s.h.KeyboardEventsEnabled() && device.IsKeyboard()) || + (s.h.MouseEventsEnabled() && device.IsMouse()) || + (s.h.GamepadEventsEnabled() && device.IsGamepad()) { + return k, KeyScanCompleted + } } } return Key{}, KeyScanUnchanged diff --git a/scan_key.go b/scan_key.go index c2196e7..ba4832d 100644 --- a/scan_key.go +++ b/scan_key.go @@ -13,24 +13,6 @@ const ( KeyScanCompleted ) -const ( - scanKeyMouseWheelDown Action = iota - scanKeyMouseWheelUp - scanSpecialKeyActionCount // always last to keep accurate count to iterate over -) - -var ( - specialKeyKeymap = Keymap{ - scanKeyMouseWheelDown: {KeyWheelDown}, - scanKeyMouseWheelUp: {KeyWheelUp}, - } -) - -func newSpecialKeyScanHandler(scanHandler *Handler) *Handler { - // create new handler used to help scan for special keys to be able to detect them - return scanHandler.sys.NewHandler(scanHandler.id, specialKeyKeymap) -} - // KeyScanner checks the currently pressed keys and buttons and tries to map them // to a local Key type that can be used in a Keymap. // @@ -41,9 +23,9 @@ type KeyScanner struct { canScan bool h *Handler - // uses special handler and keymap to detect certain events for key and axes scanning purposes - _scanSpecialKeyHelper *Handler - _scanAxesHelper *Handler + // uses special handlers to detect certain events for key and axes scanning purposes + _scanMouseKeyHelper *Handler + _scanAxesHelper *Handler } // NewKeyScanner creates a key scanner for the specifier input Handler. @@ -70,15 +52,9 @@ func NewKeyScanner(h *Handler) *KeyScanner { // * Unchanged - nothing updated since the last Scan() operation // * Completed - the user finished specifying the keys combination, you can use the Key as a new binding func (s *KeyScanner) Scan() (Key, KeyScanStatus) { - // TODO: respect the enabled input devices. - if s == nil || s.h == nil || s.h.sys == nil { panic("KeyScanner must be initialized using: NewKeyScanner(*Handler)") } - if s._scanSpecialKeyHelper == nil { - // special Handler is needed to determine certain events using special keymap - s._scanSpecialKeyHelper = newSpecialKeyScanHandler(s.h) - } // Note that this function may not be needed by some users, // so we're better of making it as independent as possible, so it @@ -96,10 +72,6 @@ func (s *KeyScanner) Scan() (Key, KeyScanStatus) { // scan for gamepad buttons k, status = s.scanGamepad(nil) } - if status == KeyScanUnchanged { - // scan for special keys, like mouse wheel up/down - k, status = s.scanSpecialKeys() - } if !s.canScan { if k.name != "" { @@ -112,15 +84,33 @@ func (s *KeyScanner) Scan() (Key, KeyScanStatus) { switch status { case KeyScanCompleted: s.canScan = false - s._scanSpecialKeyHelper = nil + } return k, status } -func (s *KeyScanner) scanSpecialKeys() (Key, KeyScanStatus) { - for a := Action(0); a < scanSpecialKeyActionCount; a++ { - if _, ok := s._scanSpecialKeyHelper.JustPressedActionInfo(a); ok { - k := specialKeyKeymap[a][0] +const ( + scanKeyMouseWheelDown Action = iota + scanKeyMouseWheelUp +) + +var ( + specialMouseKeyKeymap = Keymap{ + scanKeyMouseWheelDown: {KeyWheelDown}, + scanKeyMouseWheelUp: {KeyWheelUp}, + } +) + +// newMouseKeyScanHandler creates a new handler used to help scan for +// special mouse keys to be able to detect them. +func newMouseKeyScanHandler(scanHandler *Handler) *Handler { + return scanHandler.sys.NewHandler(scanHandler.id, specialMouseKeyKeymap) +} + +func (s *KeyScanner) scanSpecialMouseKeys() (Key, KeyScanStatus) { + for a := range specialMouseKeyKeymap { + if _, ok := s._scanMouseKeyHelper.JustPressedActionInfo(a); ok { + k := specialMouseKeyKeymap[a][0] return k, KeyScanCompleted } } @@ -128,6 +118,15 @@ func (s *KeyScanner) scanSpecialKeys() (Key, KeyScanStatus) { } func (s *KeyScanner) scanMouse(mouseKeys []ebiten.MouseButton, heldKeys []ebiten.Key) (Key, KeyScanStatus) { + if !s.h.MouseEventsEnabled() { + return Key{}, KeyScanUnchanged + } + + if s._scanMouseKeyHelper == nil { + // special Handler needed to determine certain events using special keymap + s._scanMouseKeyHelper = newMouseKeyScanHandler(s.h) + } + // We will need to do our own "AppendJustReleased" for mouse button presses for k := ebiten.MouseButton(0); k < ebiten.MouseButtonMax; k++ { if inpututil.IsMouseButtonJustReleased(k) { @@ -171,14 +170,20 @@ Loop: } } - status := KeyScanUnchanged if mappedKey.name != "" { - status = KeyScanCompleted + // mouse button pressed, scan complete + s._scanMouseKeyHelper = nil + return mappedKey, KeyScanCompleted } - return mappedKey, status + // no standard mouse button pressed, check special mouse key presses + return s.scanSpecialMouseKeys() } func (s *KeyScanner) scanGamepad(gamepadKeys []ebiten.StandardGamepadButton) (Key, KeyScanStatus) { + if !s.h.GamepadEventsEnabled() { + return Key{}, KeyScanUnchanged + } + var handlerID uint8 if s.h != nil { handlerID = s.h.id @@ -220,8 +225,11 @@ Loop: } func (s *KeyScanner) scanKeyboard(keys []ebiten.Key, heldKeys []ebiten.Key) (Key, KeyScanStatus) { - keys = inpututil.AppendJustReleasedKeys(keys) + if !s.h.KeyboardEventsEnabled() { + return Key{}, KeyScanUnchanged + } + keys = inpututil.AppendJustReleasedKeys(keys) if len(keys) == 0 { // We're still collecting the keys. return Key{}, KeyScanUnchanged @@ -271,6 +279,9 @@ Loop: } func (s *KeyScanner) scanKeyModifiers(heldKeys []ebiten.Key) KeyModifier { + if !s.h.KeyboardEventsEnabled() { + return 0 + } heldKeys = inpututil.AppendPressedKeys(heldKeys) var ctrlKey Key diff --git a/scan_key_test.go b/scan_key_test.go index 56cd881..e5b33d4 100644 --- a/scan_key_test.go +++ b/scan_key_test.go @@ -6,10 +6,17 @@ import ( "github.com/hajimehoshi/ebiten/v2" ) -func TestScanKeyboard(t *testing.T) { - testHandler := &Handler{id: 0} +func newTestKeyScanner(config SystemConfig) *KeyScanner { + var sys System + sys.Init(config) + testHandler := sys.NewHandler(0, Keymap{}) testScanner := NewKeyScanner(testHandler) testScanner.canScan = true + return testScanner +} + +func TestScanKeyboard(t *testing.T) { + testScanner := newTestKeyScanner(SystemConfig{DevicesEnabled: KeyboardDevice}) tests := []struct { keys []ebiten.Key @@ -57,12 +64,19 @@ func TestScanKeyboard(t *testing.T) { i, have, have, test.want, test.want) } } + + // test mouse press denied due to SystemConfig.DevicesEnabled mask + key, _ := testScanner.scanMouse([]ebiten.MouseButton{ebiten.MouseButtonMiddle}, nil) + if key.name != "" { + t.Fatalf("test device mask failed:\nkey: %s (%#v)", key, key) + } } func TestScanMouse(t *testing.T) { - testHandler := &Handler{id: 0} - testScanner := NewKeyScanner(testHandler) - testScanner.canScan = true + testScanner := newTestKeyScanner( + // enabling mouse with keyboard device for modifiers + SystemConfig{DevicesEnabled: MouseDevice | KeyboardDevice}, + ) tests := []struct { keys []ebiten.MouseButton @@ -102,12 +116,16 @@ func TestScanMouse(t *testing.T) { i, have, have, test.want, test.want) } } + + // test gamepad press denied due to SystemConfig.DevicesEnabled mask + key, _ := testScanner.scanGamepad([]ebiten.StandardGamepadButton{ebiten.StandardGamepadButtonRightBottom}) + if key.name != "" { + t.Fatalf("test device mask failed:\nkey: %s (%#v)", key, key) + } } func TestScanGamepad(t *testing.T) { - testHandler := &Handler{id: 0} - testScanner := NewKeyScanner(testHandler) - testScanner.canScan = true + testScanner := newTestKeyScanner(SystemConfig{DevicesEnabled: GamepadDevice}) tests := []struct { keys []ebiten.StandardGamepadButton @@ -134,4 +152,10 @@ func TestScanGamepad(t *testing.T) { i, have, have, test.want, test.want) } } + + // test keyboard press denied due to SystemConfig.DevicesEnabled mask + key, _ := testScanner.scanKeyboard([]ebiten.Key{ebiten.KeyEscape}, nil) + if key.name != "" { + t.Fatalf("test device mask failed:\nkey: %s (%#v)", key, key) + } } diff --git a/system.go b/system.go index 20404b8..9d6917e 100644 --- a/system.go +++ b/system.go @@ -34,6 +34,9 @@ type System struct { simulatedEvents []simulatedEvent hasSimulatedActions bool + keyboardEnabled bool + gamepadEnabled bool + touchEnabled bool touchHasTap bool touchHasLongTap bool @@ -73,6 +76,8 @@ func (sys *System) Init(config SystemConfig) { sys.keySlice = make([]ebiten.Key, 0, 4) sys.gamepadKeySlice = make([]ebiten.GamepadButton, 0, 2) + sys.keyboardEnabled = config.DevicesEnabled&KeyboardDevice != 0 + sys.gamepadEnabled = config.DevicesEnabled&GamepadDevice != 0 sys.touchEnabled = config.DevicesEnabled&TouchDevice != 0 sys.mouseEnabled = config.DevicesEnabled&MouseDevice != 0 From cd7721062cdcfb483b533f8b45390181862b26ef Mon Sep 17 00:00:00 2001 From: Eric H Date: Fri, 4 Sep 2026 20:08:56 -0400 Subject: [PATCH 27/28] fix logic escape the prevented mouse wheel key scanning --- scan_key.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/scan_key.go b/scan_key.go index ba4832d..9823ac8 100644 --- a/scan_key.go +++ b/scan_key.go @@ -134,10 +134,6 @@ func (s *KeyScanner) scanMouse(mouseKeys []ebiten.MouseButton, heldKeys []ebiten } } - if len(mouseKeys) == 0 { - return Key{}, KeyScanUnchanged - } - containsButtonCode := func(keys []ebiten.MouseButton, code int) bool { for _, k := range keys { if int(k) == code { @@ -161,6 +157,11 @@ Loop: } } + if mappedKey.name == "" { + // check special mouse key presses which cannot be detected via inpututil funcs (e.g. mouse wheel up/down) + mappedKey, _ = s.scanSpecialMouseKeys() + } + // attach any held key modifiers keymod := s.scanKeyModifiers(heldKeys) if keymod != ModUnknown { @@ -170,13 +171,13 @@ Loop: } } + status := KeyScanUnchanged if mappedKey.name != "" { // mouse button pressed, scan complete + status = KeyScanCompleted s._scanMouseKeyHelper = nil - return mappedKey, KeyScanCompleted } - // no standard mouse button pressed, check special mouse key presses - return s.scanSpecialMouseKeys() + return mappedKey, status } func (s *KeyScanner) scanGamepad(gamepadKeys []ebiten.StandardGamepadButton) (Key, KeyScanStatus) { From eb72f5253e65191c916e44518c589ce9ffb3940a Mon Sep 17 00:00:00 2001 From: Eric H Date: Fri, 4 Sep 2026 20:22:15 -0400 Subject: [PATCH 28/28] fix KeyMouseForward not being scannable due to for loop range --- scan_key.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scan_key.go b/scan_key.go index 9823ac8..d3c893b 100644 --- a/scan_key.go +++ b/scan_key.go @@ -128,7 +128,7 @@ func (s *KeyScanner) scanMouse(mouseKeys []ebiten.MouseButton, heldKeys []ebiten } // We will need to do our own "AppendJustReleased" for mouse button presses - for k := ebiten.MouseButton(0); k < ebiten.MouseButtonMax; k++ { + for k := ebiten.MouseButton(0); k <= ebiten.MouseButtonMax; k++ { if inpututil.IsMouseButtonJustReleased(k) { mouseKeys = append(mouseKeys, k) }