Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
39bb92d
refactor keyscan to make easier to add gamepad scanning
harbdog Aug 2, 2026
e13d0a0
use ebitengine just released keys functions
harbdog Aug 2, 2026
9c4925a
add gamepad button scanning
harbdog Aug 2, 2026
57d1955
fix the remap example
harbdog Aug 2, 2026
deae18a
add missing gamepad canScan checking and setting
harbdog Aug 2, 2026
9ec57db
update actions/setup-go
harbdog Aug 2, 2026
92915c2
fix nil exception if KeyScanner is not initialized with handler pointer
harbdog Aug 2, 2026
99b5e10
add mouse button scanning
harbdog Aug 2, 2026
f533652
add key modifiers for mouse button presses
harbdog Aug 2, 2026
ade3921
move example remap after ping to avoid gamepad pinging at the same ti…
harbdog Aug 2, 2026
ae4ef29
initial axis scanner functionality
harbdog Aug 2, 2026
c453aac
fix mouse wheel action not firing for handler.PressedActionInfo
harbdog Aug 2, 2026
c8137e6
add capability to scan mouse wheel up/down as distinct keys
harbdog Aug 3, 2026
2e4f763
add ability to bind and scan mouse move actions/keys
harbdog Aug 8, 2026
281b6f1
custom panic message if KeyScanner not initialized properly
harbdog Aug 8, 2026
6d2e778
rename to mouse motion for consistency with stick motion key
harbdog Aug 9, 2026
517d93d
use positive logic in example for clarity
harbdog Aug 9, 2026
5150358
Revert "use positive logic in example for clarity"
harbdog Aug 9, 2026
e6b0ce0
simplify remap example to make it clearer
harbdog Aug 9, 2026
52b594b
remove mouse motion to move to its own PR
harbdog Aug 18, 2026
55ebd4d
replace use of singleton scanning handlers with instance property
harbdog Aug 18, 2026
da5b71f
add missing deltaVec to gmath variant functions
harbdog Aug 18, 2026
4e00767
update name of special key scanning funcs/vars
harbdog Aug 21, 2026
da4d35f
reintroduce unit test for scan keyboard
harbdog Aug 22, 2026
e0674e4
add unit tests for scan mouse/gamepad
harbdog Aug 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
Expand Down
29 changes: 29 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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/
140 changes: 106 additions & 34 deletions _examples/remap/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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,
Expand All @@ -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<scanning the new keybing>", g.k))
if g.scanningKey {
ebitenutil.DebugPrint(screen, fmt.Sprintf("keybind: %s\naxes: %s\n<scanning the new keybind>", g.k, g.axes))
} else if g.scanningAxes {
ebitenutil.DebugPrint(screen, fmt.Sprintf("keybind: %s\naxes: %s\n<scanning the new axes>", 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 {
Expand All @@ -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)
}
4 changes: 2 additions & 2 deletions _examples/scroll/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
6 changes: 6 additions & 0 deletions event.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type EventInfo struct {

Duration int
Pos Vec
DeltaPos Vec
StartPos Vec
}

Expand All @@ -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.
Expand Down
Loading
Loading