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", }, 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/remap/main.go b/_examples/remap/main.go index 19d0959..f14dd92 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 - 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,49 +101,88 @@ func (g *exampleGame) Update() error { g.started = true } - g.handleRemap() + 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 + } - if !g.scanning && g.inputHandler.ActionIsJustPressed(ActionPing) { - fmt.Printf("ping! (activated with %s keybind)\n", g.k) + // 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) + } + + // keep scanning of keys separate from axes to ensure events are isolated to just what is needed + if g.scanningKey { + g.handleRemapKey() + } + if g.scanningAxes { + g.handleRemapAxes() } return nil } -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 - } - +func (g *exampleGame) handleRemapKey() { k, status := g.keyScanner.Scan() - if status != input.KeyScanUnchanged { + 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.scanningKey = false g.k = k + g.inputHandler.Remap(g.makeKeymap()) } +} + +func (g *exampleGame) handleRemapAxes() { + axes, status := g.keyScanner.ScanAxes() if status == input.KeyScanCompleted { - g.scanning = false // 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.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/_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/event.go b/event.go index fa3d18d..8ab15f9 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 } +// 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 b1fb00c..3284617 100644 --- a/handler.go +++ b/handler.go @@ -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) @@ -539,17 +542,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)) } @@ -570,7 +573,7 @@ func (h *Handler) getKeyPos(k Key) Vec { var result Vec switch k.kind { case keyMouse, keyMouseWithCtrl, keyMouseWithShift, keyMouseWithCtrlShift: - result = h.sys.cursorPos + result = h.getMouseVec() case keyTouch: result = h.sys.touchTapPos case keyTouchDrag: @@ -580,8 +583,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, 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 } @@ -651,6 +668,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,12 +739,12 @@ 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 - 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: @@ -871,6 +900,14 @@ func (h *Handler) mappedGamepadKey(keyCode int) ebiten.GamepadButton { } } +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/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) } 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 new file mode 100644 index 0000000..8859b91 --- /dev/null +++ b/scan_axes.go @@ -0,0 +1,68 @@ +package input + +const ( + scanAxesMouseWheel Action = iota + scanAxesGamepadLStick + scanAxesGamepadRStick + scanAxesActionCount // always last to keep accurate count to iterate over +) + +var ( + 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) { + if s == nil || s.h == nil || s.h.sys == nil { + panic("KeyScanner must be initialized using: NewKeyScanner(*Handler)") + } + if s._scanAxesHelper == nil { + // special Handler is needed to determine axis events using special keymap + s._scanAxesHelper = newScanAxesHandler(s.h) + } + + k, status := s.scanAxesEvents() + + 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: + s.canScan = false + s._scanAxesHelper = nil + } + return k, status +} + +func (s *KeyScanner) scanAxesEvents() (Key, KeyScanStatus) { + for a := Action(0); a < scanAxesActionCount; a++ { + if _, ok := s._scanAxesHelper.JustPressedActionInfo(a); ok { + k := scanAxesKeymap[a][0] + return k, KeyScanCompleted + } + } + return Key{}, KeyScanUnchanged +} diff --git a/scan_key.go b/scan_key.go index 8e6d2ba..c2196e7 100644 --- a/scan_key.go +++ b/scan_key.go @@ -10,10 +10,27 @@ type KeyScanStatus int const ( KeyScanUnchanged KeyScanStatus = iota - KeyScanChanged 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. // @@ -21,10 +38,12 @@ 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 + + // uses special handler and keymap to detect certain events for key and axes scanning purposes + _scanSpecialKeyHelper *Handler + _scanAxesHelper *Handler } // NewKeyScanner creates a key scanner for the specifier input Handler. @@ -41,8 +60,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. @@ -50,11 +68,17 @@ 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. - // TODO: scan the gamepad buttons as well. + + 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 @@ -63,50 +87,109 @@ 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. - - // This slice is stack-allocated; for the most cases, 4 keys are enough. - keys := make([]ebiten.Key, 0, 4) - keys = inpututil.AppendPressedKeys(keys) + k, status := s.scanKeyboard(nil, nil) + if status == KeyScanUnchanged { + // scan for mouse buttons + k, status = s.scanMouse(nil, nil) + } + if status == KeyScanUnchanged { + // 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 len(keys) != 0 { + if k.name != "" { + // do not start scanning until preexisting button presses are no longer held return Key{}, KeyScanUnchanged } s.canScan = true } - if len(keys) == s.lastNumKeys { - // It's either empty or we're still collecting the keys. + 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] + return k, KeyScanCompleted + } + } + return Key{}, KeyScanUnchanged +} + +func (s *KeyScanner) scanMouse(mouseKeys []ebiten.MouseButton, heldKeys []ebiten.Key) (Key, KeyScanStatus) { + // 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) { + mouseKeys = append(mouseKeys, k) + } + } + + if len(mouseKeys) == 0 { return Key{}, KeyScanUnchanged } - if len(keys) < s.lastNumKeys { - // 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 + containsButtonCode := func(keys []ebiten.MouseButton, code int) bool { + for _, k := range keys { + if int(k) == code { + return true + } + } + return false } - s.lastNumKeys = len(keys) + 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 + } + } + } + + // attach any held key modifiers + keymod := s.scanKeyModifiers(heldKeys) + if keymod != ModUnknown { + switch mappedKey.kind { + case keyMouse: + mappedKey = KeyWithModifier(mappedKey, keymod) + } + } - k, ok := scanKey(keys) status := KeyScanUnchanged - if ok { - s.key = k - status = KeyScanChanged + if mappedKey.name != "" { + status = KeyScanCompleted } - return k, status + return mappedKey, status } -func scanKey(keys []ebiten.Key) (Key, bool) { - if len(keys) == 0 { - return Key{}, false +func (s *KeyScanner) scanGamepad(gamepadKeys []ebiten.StandardGamepadButton) (Key, KeyScanStatus) { + var handlerID uint8 + if s.h != nil { + handlerID = s.h.id } + gamepadKeys = inpututil.AppendJustReleasedStandardGamepadButtons(ebiten.GamepadID(handlerID), gamepadKeys) - containsKeyCode := func(keys []ebiten.Key, code int) bool { + 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 @@ -115,58 +198,101 @@ func scanKey(keys []ebiten.Key) (Key, bool) { return false } - // Parse the keys combination into something that this library can handle. + var mappedKey Key - // 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 { - switch k { - case ebiten.KeyControl, ebiten.KeyShift: - // Just omit them from the slice. - case ebiten.KeyControlLeft: - ctrlKey = KeyControlLeft - case ebiten.KeyControlRight: - ctrlKey = KeyControlRight - case ebiten.KeyShiftLeft: - shiftKey = KeyShiftLeft - case ebiten.KeyShiftRight: - shiftKey = KeyShiftRight - default: - keysWithoutMods = append(keysWithoutMods, k) + // map the Ebitengine keys to the local types. +Loop: + for _, k := range allKeys { + switch k.kind { + case keyGamepad: + if containsButtonCode(gamepadKeys, k.code) { + mappedKey = k + break Loop + } } } - hasCtrl := ctrlKey.name != "" - hasShift := shiftKey.name != "" - var mappedKey Key + status := KeyScanUnchanged + if mappedKey.name != "" { + status = KeyScanCompleted + } + return mappedKey, status +} - // Round 2: map the Ebitengine keys to the local types. +func (s *KeyScanner) scanKeyboard(keys []ebiten.Key, heldKeys []ebiten.Key) (Key, KeyScanStatus) { + keys = inpututil.AppendJustReleasedKeys(keys) + + if len(keys) == 0 { + // We're still collecting the keys. + return Key{}, KeyScanUnchanged + } + + containsKeyCode := func(keys []ebiten.Key, code int) bool { + for _, k := range keys { + if int(k) == code { + return true + } + } + return false + } + + // 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 _, 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, true - case hasShift: - return shiftKey, true + // attach any held key modifiers + keymod := s.scanKeyModifiers(heldKeys) + 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(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: + shiftKey = KeyShiftRight } } + hasCtrl := ctrlKey.name != "" + hasShift := shiftKey.name != "" var keymod KeyModifier switch { @@ -177,12 +303,5 @@ Loop: case hasShift: keymod = ModShift } - if keymod != ModUnknown { - switch mappedKey.kind { - case keyKeyboard, keyMouse: - mappedKey = KeyWithModifier(mappedKey, keymod) - } - } - - return mappedKey, mappedKey.name != "" + return keymod } diff --git a/scan_key_test.go b/scan_key_test.go index ce9a122..56cd881 100644 --- a/scan_key_test.go +++ b/scan_key_test.go @@ -6,45 +6,129 @@ import ( "github.com/hajimehoshi/ebiten/v2" ) -func TestScanKey(t *testing.T) { +func TestScanKeyboard(t *testing.T) { + testHandler := &Handler{id: 0} + testScanner := NewKeyScanner(testHandler) + testScanner.canScan = true + tests := []struct { - keys []ebiten.Key - want Key + keys []ebiten.Key + heldKeys []ebiten.Key + want Key }{ // Sanity tests. - {[]ebiten.Key{}, Key{}}, + {[]ebiten.Key{}, nil, 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}, + {[]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}, KeyA}, - {[]ebiten.Key{ebiten.KeyA, ebiten.KeyB}, KeyA}, + {[]ebiten.Key{ebiten.KeyB, ebiten.KeyA}, nil, KeyA}, + {[]ebiten.Key{ebiten.KeyA, ebiten.KeyB}, nil, 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)}, + {[]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, ebiten.KeyA}, KeyWithModifier(KeyE, ModControl)}, // Shift modifiers. - {[]ebiten.Key{ebiten.KeyF, 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.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)}, + {[]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)}, + {[]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) + } + } +} + +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, _ := scanKey(test.keys) + 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) 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.