tecs.input
Keyboard, pointer, touch and gamepad state folded from typed platform events.
Live state survives frame boundaries. Frame edges clear before each sealed host batch, while latched edges accumulate until the first fixed step consumes them. Blocking layers move capture and clear pending edges without inventing releases for controls that remain physically held.
Gamepads reach the same fold through a different door. The keyboard, the pointer and touch arrive in the host's sealed batch, because the window system owns them; a gamepad belongs to no window, so tecs.platform.gamepadbackend is polled once a frame by the tecs.PollGamepads system install adds, and what it saw becomes the same typed platform events the host would have sent. That is why handleEvent is the only fold in this module: a synthetic gamepad event from the debug server and a real one from a controller take one path.
Buttons use positional names. "south" is the button nearest the player on every pad, where "a" is the one nearest the player on some pads and the one to its right on others.
local pad <const> = state:gamepad(1)
if pad ~= nil and pad.connected then
local moveX <const> = pad:axis("leftX")
if pad:buttonPressed("south") then
print(moveX)
end
endDevice support#
Gamepads expose positional buttons, sticks, analog triggers, hot plug, power state, and rumble. Rumble is conditional: gilrs implements force feedback on evdev and XInput but not on IOKit, so Gamepad.rumble answers false on macOS and for devices without motors.
Touch is available on the platforms where winit reports it: Windows, Linux, Android, and the web. Touch pressure comes through where the surface measures it and reads zero where it does not.
The pen is the pointer. winit declares no pen event, so a stylus arrives as the cursor moving and its buttons as mouse buttons, and Tecs reads it that way rather than inventing a device the layer below does not report. A game that draws with a stylus reads pointerX, pointerY and the mouse buttons and gets what the platform sends.
Pen pressure, tilt, and eraser state are not exposed because winit declares no pen event. Supporting them would require separate NSEvent, WM_POINTER, and XInput2 tablet handling.
Gamepad motion sensors, touchpads, trigger rumble, light bars, player indexes, hardware button labels, device families, device paths, remap events, and standalone motion sensors are not part of the input surface. A game that wants a "cross" glyph rather than a "south" name reads the pad's name and chooses its own art.
Module contents
Types
| Type | Kind | Description |
|---|---|---|
Gamepad | record | One attached gamepad's identity, event state and outputs. |
GamepadAxis | type | A gamepad axis name. |
GamepadButton | type | A positional gamepad button name. |
Input | record | Live, frame-edge, and fixed-latched keyboard and pointer input. |
Layer | record | A position in the input capture stack. |
MouseButton | type | A conventional pointer button name. |
Touch | record | One finger on a touch surface, from the most recent touch event. |
Functions
| Function | Kind | Description |
|---|---|---|
install | function | Installs and returns the input state owned by one world. |
newInput | function | Creates empty input state with the readable base layer. |
of | function | Returns the input state installed in a world. |
Types#
Gamepadrecord#
record Gamepad ...One attached gamepad's identity, event state and outputs.
Input owns the object. A retained reference stays safe after the device goes away: queries answer with neutral values, rumble returns false, and connected stays false. A reconnect creates a new object rather than reviving the old one.
Members
| Name | Kind | Description |
|---|---|---|
id | field | Read-only. |
connected | field | Read-only. |
name | field | Read-only. |
guid | field | Read-only. |
backend | field | |
gateRead | method | Reads the shared layer stack. |
gateFixed | method | Reports whether queries should answer with fixed-step latched edges. |
down | field | |
pressed | field | |
released | field | |
latchedPressed | field | |
latchedReleased | field | |
axisValues | field | Latest value per axis. |
buttonDown | method | |
buttonPressed | method | |
buttonReleased | method | |
axis | method | |
hasButton | method | |
hasAxis | method | |
power | method | |
rumble | method |
idfield#
id: integerRead-only. Names the device, and matches the which field of every gamepad platform event about it.
connectedfield#
connected: booleanRead-only. Reports true from the moment Input opens the device until it goes away, and false permanently afterwards.
namefield#
name: stringRead-only. Reports the platform's display name for the device, and an empty string when it reports none. Saved bindings use guid.
guidfield#
guid: stringRead-only. Reports the stable identity a saved binding matches on, and an empty string when the platform reports none.
gateReadmethod#
gateRead: function(layer: Layer?): booleanReads the shared layer stack. A layer blocks every device or none, so this answers from the owning input state rather than from the pad.
Arguments
| Name | Type | Description |
|---|---|---|
layer | Layer? |
Returns
| Type | Description |
|---|---|
boolean |
gateFixedmethod#
gateFixed: function(): booleanReports whether queries should answer with fixed-step latched edges.
Returns
| Type | Description |
|---|---|
boolean |
axisValuesfield#
axisValues: {[integer]: number}Latest value per axis. An axis no event has mentioned reads as zero.
buttonDownmethod#
buttonDown: function(borrows self: Gamepad, button: GamepadButton | integer, layer: Layer?): booleanArguments
| Name | Type | Description |
|---|---|---|
borrows self | Gamepad | |
button | GamepadButton | integer | |
layer | Layer? |
Returns
| Type | Description |
|---|---|
boolean |
buttonPressedmethod#
buttonPressed: function(borrows self: Gamepad, button: GamepadButton | integer, layer: Layer?): booleanArguments
| Name | Type | Description |
|---|---|---|
borrows self | Gamepad | |
button | GamepadButton | integer | |
layer | Layer? |
Returns
| Type | Description |
|---|---|
boolean |
buttonReleasedmethod#
buttonReleased: function(borrows self: Gamepad, button: GamepadButton | integer, layer: Layer?): booleanArguments
| Name | Type | Description |
|---|---|---|
borrows self | Gamepad | |
button | GamepadButton | integer | |
layer | Layer? |
Returns
| Type | Description |
|---|---|
boolean |
axismethod#
axis: function(borrows self: Gamepad, axis: GamepadAxis | integer, deadzone: number?, layer: Layer?): numberArguments
| Name | Type | Description |
|---|---|---|
borrows self | Gamepad | |
axis | GamepadAxis | integer | |
deadzone | number? | |
layer | Layer? |
Returns
| Type | Description |
|---|---|
number |
hasButtonmethod#
hasButton: function(borrows self: Gamepad, button: GamepadButton | integer): booleanArguments
| Name | Type | Description |
|---|---|---|
borrows self | Gamepad | |
button | GamepadButton | integer |
Returns
| Type | Description |
|---|---|
boolean |
hasAxismethod#
hasAxis: function(borrows self: Gamepad, axis: GamepadAxis | integer): booleanArguments
| Name | Type | Description |
|---|---|---|
borrows self | Gamepad | |
axis | GamepadAxis | integer |
Returns
| Type | Description |
|---|---|
boolean |
powermethod#
power: function(borrows self: Gamepad): (gamepadbackend.PowerState, integer)Arguments
| Name | Type | Description |
|---|---|---|
borrows self | Gamepad |
Returns
| Type | Description |
|---|---|
gamepadbackend.PowerState | |
integer |
GamepadAxistype#
type GamepadAxis = "leftX" | "leftY" | "rightX" | "rightY" | "leftTrigger" | "rightTrigger"A gamepad axis name.
Compatibility surface: these names are stable. A stick runs -1 to 1 with leftY and rightY growing downward, and a trigger runs zero to one.
GamepadButtontype#
type GamepadButton = "south"
| "east"
| "west"
| "north"
| "back"
| "guide"
| "start"
| "leftStick"
| "rightStick"
| "leftShoulder"
| "rightShoulder"
| "dpadUp"
| "dpadDown"
| "dpadLeft"
| "dpadRight"A positional gamepad button name.
These are what gilrs reports, spelled as Tecs publishes them. They are positional rather than Xbox-only because the hardware label is not part of the input contract. Names without a gilrs counterpart are not published.
Inputrecord#
record Input ...Live, frame-edge, and fixed-latched keyboard and pointer input.
Members
| Name | Kind | Description |
|---|---|---|
mouseX | field | |
mouseY | field | |
mouseDeltaX | field | |
mouseDeltaY | field | |
wheelX | field | |
wheelY | field | |
wheelTicksX | field | |
wheelTicksY | field | |
text | field | |
keysDown | field | |
keysPressed | field | |
keysReleased | field | |
keysLatchedPressed | field | |
keysLatchedReleased | field | |
mouseButtonsDown | field | |
mouseButtonsPressed | field | |
mouseButtonsReleased | field | |
mouseButtonsLatchedPressed | field | |
mouseButtonsLatchedReleased | field | |
modifierMask | field | |
fixed | field | |
layers | field | |
enterFixedSystem | field | |
exitFixedSystem | field | |
pollSystem | field | |
backend | field | |
pads | field | |
padList | field | |
padEvents | field | The drain buffer, allocated once because the seam is crossed per frame. |
fingers | field | |
beginFrame | method | |
enterFixedPhase | method | |
exitFixedPhase | method | |
handleEvent | method | |
keyDown | method | |
keyPressed | method | |
keyReleased | method | |
modifiers | method | |
mouseDown | method | |
mousePressed | method | |
mouseReleased | method | |
pushLayer | method | |
popLayer | method | |
topLayer | method | |
canRead | method | |
touches | method | |
gamepads | method | |
gamepad | method | |
gamepadById | method | |
refreshDevices | method | |
pollDevices | method | |
destroy | method |
mouseXfield#
mouseX: numbermouseYfield#
mouseY: numbermouseDeltaXfield#
mouseDeltaX: numbermouseDeltaYfield#
mouseDeltaY: numberwheelXfield#
wheelX: numberwheelYfield#
wheelY: numberwheelTicksXfield#
wheelTicksX: integerwheelTicksYfield#
wheelTicksY: integerpadEventsfield#
padEvents: {gamepadbackend.Event}The drain buffer, allocated once because the seam is crossed per frame.
beginFramemethod#
beginFrame: function(exclusive self: Input): nilArguments
| Name | Type | Description |
|---|---|---|
exclusive self | Input |
Returns
| Type | Description |
|---|---|
nil |
enterFixedPhasemethod#
enterFixedPhase: function(exclusive self: Input): nilArguments
| Name | Type | Description |
|---|---|---|
exclusive self | Input |
Returns
| Type | Description |
|---|---|
nil |
exitFixedPhasemethod#
exitFixedPhase: function(exclusive self: Input): nilArguments
| Name | Type | Description |
|---|---|---|
exclusive self | Input |
Returns
| Type | Description |
|---|---|
nil |
handleEventmethod#
handleEvent: function(self: Input, event: platformevents.Event): nilArguments
| Name | Type | Description |
|---|---|---|
self | Input | |
event | platformevents.Event |
Returns
| Type | Description |
|---|---|
nil |
keyDownmethod#
Arguments
| Name | Type | Description |
|---|---|---|
borrows self | Input | |
key | string | |
layer | Layer? |
Returns
| Type | Description |
|---|---|
boolean |
keyPressedmethod#
Arguments
| Name | Type | Description |
|---|---|---|
borrows self | Input | |
key | string | |
layer | Layer? |
Returns
| Type | Description |
|---|---|
boolean |
keyReleasedmethod#
Arguments
| Name | Type | Description |
|---|---|---|
borrows self | Input | |
key | string | |
layer | Layer? |
Returns
| Type | Description |
|---|---|
boolean |
modifiersmethod#
modifiers: function(borrows self: Input): integerArguments
| Name | Type | Description |
|---|---|---|
borrows self | Input |
Returns
| Type | Description |
|---|---|
integer |
mouseDownmethod#
mouseDown: function(borrows self: Input, button: MouseButton | integer, layer: Layer?): booleanArguments
| Name | Type | Description |
|---|---|---|
borrows self | Input | |
button | MouseButton | integer | |
layer | Layer? |
Returns
| Type | Description |
|---|---|
boolean |
mousePressedmethod#
mousePressed: function(borrows self: Input, button: MouseButton | integer, layer: Layer?): booleanArguments
| Name | Type | Description |
|---|---|---|
borrows self | Input | |
button | MouseButton | integer | |
layer | Layer? |
Returns
| Type | Description |
|---|---|
boolean |
mouseReleasedmethod#
mouseReleased: function(borrows self: Input, button: MouseButton | integer, layer: Layer?): booleanArguments
| Name | Type | Description |
|---|---|---|
borrows self | Input | |
button | MouseButton | integer | |
layer | Layer? |
Returns
| Type | Description |
|---|---|
boolean |
pushLayermethod#
Arguments
| Name | Type | Description |
|---|---|---|
exclusive self | Input | |
name | string | |
blocking | boolean? |
Returns
| Type | Description |
|---|---|
Layer |
canReadmethod#
Arguments
| Name | Type | Description |
|---|---|---|
borrows self | Input | |
layer | Layer? |
Returns
| Type | Description |
|---|---|
boolean |
touchesmethod#
Arguments
| Name | Type | Description |
|---|---|---|
borrows self | Input | |
layer | Layer? |
Returns
| Type | Description |
|---|---|
{Touch} |
gamepadmethod#
Arguments
| Name | Type | Description |
|---|---|---|
borrows self | Input | |
index | integer? |
Returns
| Type | Description |
|---|---|
Gamepad? |
gamepadByIdmethod#
Arguments
| Name | Type | Description |
|---|---|---|
borrows self | Input | |
id | integer |
Returns
| Type | Description |
|---|---|
Gamepad? |
refreshDevicesmethod#
refreshDevices: function(self: Input): nilArguments
| Name | Type | Description |
|---|---|---|
self | Input |
Returns
| Type | Description |
|---|---|
nil |
Layerrecord#
record Layer ...A position in the input capture stack.
blockingfield#
blocking: booleanindexfield#
index: integerMouseButtontype#
type MouseButton = "left" | "middle" | "right" | "back" | "forward"A conventional pointer button name.
These are winit's names. back and forward identify the two auxiliary pointer buttons.
Touchrecord#
record Touch ...One finger on a touch surface, from the most recent touch event.
Input owns and reuses every field. Callers treat the record as read-only and copy values they need to retain.
Members
| Name | Kind | Description |
|---|---|---|
device | field | Read-only. |
finger | field | Read-only. |
x | field | Read-only. |
y | field | Read-only. |
normalX | field | Read-only. |
normalY | field | Read-only. |
pressure | field | Read-only. |
devicefield#
device: stringRead-only. Names the touch surface's opaque identity, set when the finger first appears.
fingerfield#
finger: stringRead-only. Names the finger's opaque identity, set when it first appears.
pressurefield#
pressure: numberRead-only. Reports the pressure from zero to one, and zero where the surface does not measure it.
Functions#
installfunction#
Installs and returns the input state owned by one world.
Three systems come with it. Two consume the fixed-step latched edges, and tecs.PollGamepads runs in Ingress, ahead of everything a game schedules, so a controller read anywhere in the frame answers from this frame.
This is the call that reaches real hardware. The platform's device source is opened once per process and shared, because opening it per world would give one controller two identities on two hot-plug streams; a build with no libtecsgamepad gets the device-free source and says so once in the log.
Arguments
| Name | Type | Description |
|---|---|---|
exclusive world | ecs.World | the world whose fixed steps consume latched edges |
backend | gamepadbackend.Backend? | the gamepad source, defaulting to the process-wide platform one |
Returns
| Type | Description |
|---|---|
Input | the new input state, also available through |
Raises
| Type | Condition |
|---|---|
string | when the world already owns input state |
newInputfunction#
function newInput(backend: gamepadbackend.Backend?): InputCreates empty input state with the readable base layer.
The state reads no device unless it is given a source. Constructing input state by hand is what a test and a headless tool do, and neither should start the platform's device enumeration as a side effect: on macOS that is a run loop on a thread of its own, and a suite whose result changes when somebody plugs a controller in is worse than one that reads no controller at all. install is the call that reaches real hardware.
Arguments
| Name | Type | Description |
|---|---|---|
backend | gamepadbackend.Backend? | the gamepad source, defaulting to no devices at all |
Returns
| Type | Description |
|---|---|
Input | a standalone input state |
offunction#
Returns the input state installed in a world.
Arguments
| Name | Type | Description |
|---|---|---|
borrows world | ecs.World | the world to inspect |
Returns
| Type | Description |
|---|---|
Input? | its input state, or nil when none is installed |