simvx.core.input.events

Input bindings, and the typed event family every input flows through.

One class per device family, all of them subclasses of :class:InputEvent. A handler’s filter decides which one it receives, so a typed signature needs no narrowing and match event: reads as ordinary Python::

match event:
    case KeyEvent(key=Key.ESCAPE, pressed=True):
        self.pause()
    case PointerEvent(phase=PointerPhase.DOWN, position=pos):
        self.aim_at(pos)

Every class is a @dataclass(slots=True, kw_only=True): no per-instance __dict__, a free repr, and keyword-only fields so a subclass can declare a required one after the base’s defaulted ones. kw_only also leaves __match_args__ empty, which is why the patterns above are keyword patterns.

The base carries what every input has: when it happened, the modifier state at that moment, and whether something has claimed it.

Module Contents

Classes

InputBinding

One input an action reads: a control pressed while other controls are held.

InputEvent

What every input carries, whatever device produced it.

KeyEvent

A key going down or coming up.

TextEvent

Text the player typed, after the layout and the IME have had their say.

PointerEvent

A mouse, a finger or a pen doing something.

PointerState

One live pointer as it stands this frame, as Input.pointers holds it.

ScrollEvent

A wheel notch or a two-finger scroll.

ButtonEvent

A button with no position going down or coming up: a pad’s button, a tablet’s express key.

AxisEvent

An axis with no position moving: a pad’s stick or trigger, a tablet’s ring or strip.

ActionEvent

An action edge, resolved from whatever device binding produced it.

PickEvent

A ray through the pointer hitting a node, delivered to on_picked.

ControllerConnectionEvent

A controller arriving or leaving.

Functions

key_part

How one key of a combo is written: a modifier group by its name, any other key by its own.

combo_for_key

Spell a key and what it holds as a combo ("ctrl+s"): the one place the spelling is written.

modifiers_held

The modifier groups event says were held, in combo order: (Key.CTRL, Key.SHIFT).

key_named

The key a combo part names: a modifier group ("ctrl"), a key name, or a key’s constant name.

key_combo_to_binding

Parse a combo such as "ctrl+s" or "g+x" into a binding; None when spec is not one.

key_event_for_combo

The key event a written combo names: "escape", "ctrl+s".

Data

API

simvx.core.input.events.MODIFIER_NAMES

(‘ctrl’, ‘shift’, ‘alt’, ‘meta’)

class simvx.core.input.events.InputBinding[source]

One input an action reads: a control pressed while other controls are held.

control is any control: a key (Key.SPACE), a pointer, tablet or pad button (Mouse.RIGHT, Tablet.button(0), Controller.A), a group (Pointer.SECONDARY, Key.CTRL), or one half of an axis (Controller.LEFT_X.negative). A control pinned to a unit with .on(device) hears that device alone.

held is what must be down for the press to count, which is how a shortcut, a chord or a modifier is written, all alike::

Key.CTRL + Key.S               # Ctrl+S, either Control key
Key.RIGHT_CONTROL + Key.S      # the right Control key only
Key.META + Key.S               # Cmd+S on a Mac, Super+S elsewhere
Key.G + Key.X                  # a two-key chord
Controller.LEFT_BUMPER + Controller.A

+ builds the binding; the last control is the one pressed.

exact. Whether modifiers the binding does not hold may also be down. Left unset it follows what the binding holds: a binding holding a modifier is exact, because Ctrl+S must not also fire on Ctrl+Shift+S, and a binding holding none is not, because a jump on Space must still fire while the player holds Shift to run. exact=False makes Ctrl+S fire under any extra modifier; exact=True makes a bare S fire only with no modifier down. A held control that is not a modifier (Key.G in G+X) is required either way, and the pressed control’s own modifier never counts against it.

deadzone governs an axis: how far past rest its half must travel to count.

Units. scale multiplies what the binding reads into the action’s unit, and rate, when set, makes a held value motion: the value times rate per second, over the frame or tick it is read in. So sources with different units share one action, each written in the action’s unit::

"zoom": [Touch.PINCH,                                      # the unit: a fraction of scale
         Mouse.WHEEL * 0.1,                                # a notch is a tenth
         Controller.RIGHT_TRIGGER.per_second(1.0)]         # held fully, doubles a second

The deadzone is measured before either, on what the device reports.

control: simvx.core.input.controls.DeviceButton | simvx.core.input.controls.DeviceAxis | None

None

deadzone: float

0.2

held: tuple[simvx.core.input.controls.DeviceButton, ...]

()

exact: bool | None

None

scale: float

1.0

rate: float | None

None

__post_init__() → None[source]
holding(*controls: simvx.core.input.controls.DeviceButton) → simvx.core.input.events.InputBinding[source]

This binding with controls held as well, before the ones it already holds.

__add__(other: simvx.core.input.controls.DeviceControl) → simvx.core.input.events.InputBinding[source]

Extend a chord: Key.CTRL + Key.SHIFT + Key.S holds both and presses S.

__mul__(factor: float) → simvx.core.input.events.InputBinding[source]

This binding with its value multiplied into the action’s unit: (Key.CTRL + Pointer.PRIMARY) * 2.

__rmul__

None

per_second(rate: float) → simvx.core.input.events.InputBinding[source]

This binding as motion: its held value times rate each second, over the step it is read in.

property modifiers: frozenset[simvx.core.input.controls.DeviceButton][source]

The modifiers this binding holds, as groups: {Key.CTRL} for either Control key or one side.

property is_exact: bool[source]

Whether this binding refuses modifiers it does not hold: exact, or its default when unset.

property is_key: bool[source]

Whether this binding is a key on the keyboard.

property key_combo: str | None[source]

The combo spelling of a key binding ("ctrl+shift+s"), or None if it is not keys alone.

matches_key(event: simvx.core.input.events.KeyEvent) → bool[source]

Whether the key event is this binding, by the rule a binding is read by.

The key, every modifier the binding holds, and where it is exact (:attr:is_exact) no other modifier, the pressed key’s own excepted. So the engine’s Tab, which is exact, is not Shift+Tab, and a plain Enter still accepts while Shift is held. The event says which modifiers were down, not which side, so a side reads as its modifier, and a held control that is not a modifier is not something a key event can say, which no event matches.

simvx.core.input.events.key_part(key: simvx.core.input.controls.DeviceButton) → str[source]

How one key of a combo is written: a modifier group by its name, any other key by its own.

simvx.core.input.events.combo_for_key(key: simvx.core.input.controls.DeviceButton, *, held: tuple[simvx.core.input.controls.DeviceButton, ...] = ()) → str[source]

Spell a key and what it holds as a combo ("ctrl+s"): the one place the spelling is written.

For the interfaces spelled in text: a project file, a rebind screen, a menu’s shortcut column. Nothing on the input path compares combos.

simvx.core.input.events.modifiers_held(event: InputEvent) → tuple[simvx.core.input.controls.DeviceButton, ...][source]

The modifier groups event says were held, in combo order: (Key.CTRL, Key.SHIFT).

simvx.core.input.events.key_named(name: str) → simvx.core.input.controls.DeviceButton | None[source]

The key a combo part names: a modifier group ("ctrl"), a key name, or a key’s constant name.

simvx.core.input.events.key_combo_to_binding(spec: str) → simvx.core.input.events.InputBinding | None[source]

Parse a combo such as "ctrl+s" or "g+x" into a binding; None when spec is not one.

The inverse of :attr:InputBinding.key_combo: every part but the last is held, the last is pressed, and "ctrl", "shift", "alt" and "meta" are either side. A bare key name is not a combo and returns None, leaving it to the caller’s own key lookup.

simvx.core.input.events.key_event_for_combo(spec: str, *, pressed: bool = True) → KeyEvent[source]

The key event a written combo names: "escape", "ctrl+s".

For the drivers that are themselves written in text (a test harness, a scripted demo). Raises ValueError naming the spec when no key answers to it, or when the combo holds something a key event cannot carry.

class simvx.core.input.events.InputEvent[source]

What every input carries, whatever device produced it.

Attributes: time: When the input happened, on :func:time.monotonic’s clock, which is the clock the frame loop measures with. A backend that has a platform event time converts it onto that clock; one that does not lets :class:~simvx.core.input.router.InputRouter stamp the moment it received the event. :attr:Input.capabilities.event_timestamps         <simvx.core.input.capabilities.InputCapabilities.event_timestamps> says whether the source can supply one at all. Not the scene clock: that one stops while paused and stretches under slow motion, so a double click would stop resolving on a pause menu. source_id: Which input source delivered this, or 0 for one nobody attributed. A source is a window, not a screen: two windows onto one tree have two pointer positions, two hover answers and two capture tables, and this is what tells them apart. Each :class:~simvx.core.input.router.InputRouter takes an id at construction and stamps it on everything it publishes, so an event carries its source rather than the caller having to pass one alongside it. ctrl, shift, alt, meta: Modifier state at the moment of the event. A backend that does not report them leaves them unset and the router fills them from held keys.

time: float

‘field(…)’

source_id: int

0

ctrl: bool

False

shift: bool

False

alt: bool

False

meta: bool

False

class simvx.core.input.events.KeyEvent[source]

Bases: simvx.core.input.events.InputEvent

A key going down or coming up.

Attributes: key: The physical key position, Key.W: the named key where the engine’s table names the platform’s code, Key.button(code) where it does not. A binding and a saved rebind file both name this, so WASD is the same four positions on QWERTY and on AZERTY. label: What that key prints under the player’s layout, for a prompt or a rebind screen. The router fills it from the engine’s US table when the backend cannot say. pressed: True on press, False on release. echo: True for an OS auto-repeat of a key already held. cancelled: True on a release that did not happen: the control that owned the press was hidden, disabled, paused or raised while the key was down, and is told the press is over without it counting as a keystroke, as a pointer’s CANCEL phase tells a drag. device_id: Which physical keyboard, or 0 for the aggregate device.

key: simvx.core.input.controls.DeviceButton

None

pressed: bool

None

label: str = <Multiline-String>
echo: bool

False

device_id: int

0

cancelled: bool

False

time: float

‘field(…)’

source_id: int

0

ctrl: bool

False

shift: bool

False

alt: bool

False

meta: bool

False

class simvx.core.input.events.TextEvent[source]

Bases: simvx.core.input.events.InputEvent

Text the player typed, after the layout and the IME have had their say.

Attributes: text: The characters produced. composing: True while an IME composition is still in flight, so a text field can underline the preedit rather than commit it.

text: str

None

composing: bool

False

time: float

‘field(…)’

source_id: int

0

ctrl: bool

False

shift: bool

False

alt: bool

False

meta: bool

False

class simvx.core.input.events.PointerEvent[source]

Bases: simvx.core.input.events.InputEvent

A mouse, a finger or a pen doing something.

Every finger is a pointer of its own and so is a pen, each delivered through the same door the mouse is. One of them at a time is primary: the one that stands in for the mouse, so a control that takes one pointer and, while Input.emulate_mouse is on, the mouse state and the left-button bindings follow it.

A button is one control. :attr:button is what the device’s layout calls the button pressed, Pen.BARREL_1, or its index form where the layout has no name. A role is a group, asked with in::

if event.phase is PointerPhase.DOWN and event.button in Pointer.PRIMARY:
    self.press()

Contact is not the phase. :attr:contact is the primary role’s bit: a finger on the glass, a pen tip on the tablet, the left mouse button held. A pen hovering over the tablet produces MOVE events with no contact and a pressure of 0.0, and a pen pressing a barrel button while it hovers is a DOWN with no contact, which is how a hover press is bound at all.

Attributes: pointer_id: 0 is the mouse. Every other pointer’s id is opaque, allocated by the router per device and backend id: a finger gets a fresh one each time it touches down and a pen keeps one while it is in range. :attr:pointer_type says what kind it is. pointer_type: Which kind of pointer this is. is_primary: True for the pointer that stands in for the mouse: the mouse itself, the first finger down while no other pointer holds primacy, and a pen likewise. Primacy moves on a press only. phase: What the pointer did. position: Where it is, in screen pixels, sub-pixel. delta: How far it moved since the previous event for this pointer. button: The button this event is about, as the layout in force names it (Mouse.LEFT, Pen.BARREL_1, Touch.CONTACT), or its index form (Pen.button(5)) where the layout names none; None for a pure move. An event built by hand may name either, or a role; publishing it resolves the button through the device’s layout. buttons: Bitmask of the buttons this pointer holds once the event has happened, bit index each, in the device’s own numbering. For the mouse, the buttons the machine holds. contact_mask: The bits of :attr:buttons that are contact: the primary role’s, from the device’s layout. click_count: 1 for a single click, 2 for a double, and so on, on the press and on the release that closes it; 0 on a pure move and wherever nothing has resolved it. dragging: True once the pointer has travelled past its type’s drag threshold since the press, and on every event of that press after. A release still carrying False is a click rather than a drag. pressure: 0.0 whenever the pointer is not in contact. In contact, what the device measured, or 1.0 on hardware that cannot measure it; Input.capabilities says which (PenAxis.PRESSURE in         pen_axes for a pen, touch_pressure for a finger). tilt: The barrel’s tilt from upright as (x, y), radians in [-pi/2, pi/2]; zero for a device without one. twist: The barrel’s rotation about its own axis, radians in [0, 2pi) clockwise; zero where not reported. distance: How far a hovering pen is from the surface, 0 to 1, 0 at the surface; zero where not reported. tangential_pressure: An airbrush wheel or barrel squeeze, -1 to 1 with 0 neutral; zero where not reported. tool: Which end of the pointer is working: the tip, or a pen’s eraser. tool_id: Which physical tool, for a per-tool preset; 0 where not reported. contact_size: The width and height of the contact in screen pixels, a finger or a palm on the glass; (0, 0) where not reported. locked: True while the pointer is locked and position is stale. viewport: The space position is in, or None for the root. device_id: The engine’s id for the device, the key in Input.devices, allocated per type and backend id; 0 on an event built by hand, which publishing resolves to the type’s aggregate device.

phase: simvx.core.input.enums.PointerPhase

None

pointer_id: int

0

pointer_type: simvx.core.input.enums.PointerType

None

is_primary: bool

True

position: tuple[float, float]

(0.0, 0.0)

delta: tuple[float, float]

(0.0, 0.0)

button: simvx.core.input.controls.DeviceButton | None

None

buttons: int

0

contact_mask: int

1

click_count: int

0

dragging: bool

False

pressure: float

0.0

tilt: tuple[float, float]

(0.0, 0.0)

twist: float

0.0

distance: float

0.0

tangential_pressure: float

0.0

tool: simvx.core.input.enums.PointerTool

None

tool_id: int

0

contact_size: tuple[float, float]

(0.0, 0.0)

locked: bool

False

viewport: simvx.core.viewport.SubViewport | None

None

device_id: int

0

__post_init__() → None[source]
property contact: bool[source]

Whether the pointer is touching: a finger down, a pen tip down, the mouse’s primary held.

Read off :attr:buttons through :attr:contact_mask. An event built by hand and published is completed by the router first, so a press that named only its button holds it here too.

property altitude: float[source]

The angle between the barrel and the surface, radians: pi/2 upright, 0 flat.

property azimuth: float[source]

The direction the barrel leans, radians in [0, 2pi); 0 at zero tilt.

time: float

‘field(…)’

source_id: int

0

ctrl: bool

False

shift: bool

False

alt: bool

False

meta: bool

False

class simvx.core.input.events.PointerState[source]

One live pointer as it stands this frame, as Input.pointers holds it.

The polled companion of :class:PointerEvent, with the same field names, so a game can poll every finger and a pen the way it polls a key. A finger is live from its press until the frame after it lifts; a pen from the moment it is seen until the frame after it leaves the digitiser’s range; the mouse once it has done anything at all.

Attributes: pointer_id: 0 is the mouse; every other id is opaque, as :attr:PointerEvent.pointer_id. pointer_type: Which kind of pointer this is. position: Where it is, in screen pixels. contact: Whether it is touching, as :attr:PointerEvent.contact. is_primary: Whether it stands in for the mouse. buttons: The buttons it holds, bit index each in the device’s own numbering. pressure, tilt, twist, distance, tangential_pressure, tool, tool_id, contact_size: As on :class:PointerEvent, from its latest event. just_pressed: Contact began this frame. just_released: Contact ended this frame, lifted or cancelled. cancelled: The contact that ended was taken away by the system rather than lifted, so it completed nothing: a gesture reading a tap off a release checks this first. device_id: The engine’s id for the device, as :attr:PointerEvent.device_id.

pointer_id: int

None

pointer_type: simvx.core.input.enums.PointerType

None

position: tuple[float, float]

None

contact: bool

False

is_primary: bool

False

buttons: int

0

pressure: float

0.0

tilt: tuple[float, float]

(0.0, 0.0)

twist: float

0.0

distance: float

0.0

tangential_pressure: float

0.0

tool: simvx.core.input.enums.PointerTool

None

tool_id: int

0

contact_size: tuple[float, float]

(0.0, 0.0)

just_pressed: bool

False

just_released: bool

False

cancelled: bool

False

device_id: int

0

property altitude: float[source]

The angle between the barrel and the surface, radians: pi/2 upright, 0 flat.

property azimuth: float[source]

The direction the barrel leans, radians in [0, 2pi); 0 at zero tilt.

class simvx.core.input.events.ScrollEvent[source]

Bases: simvx.core.input.events.InputEvent

A wheel notch or a two-finger scroll.

Attributes: pointer_id: The pointer the scroll came from. position: Where the pointer was, in screen pixels. scroll: The offset, positive y scrolling up. A wheel notch is one unit; a trackpad reports fractions of one. viewport: The space position is in, or None for the root.

scroll: tuple[float, float]

None

pointer_id: int

0

position: tuple[float, float]

(0.0, 0.0)

viewport: simvx.core.viewport.SubViewport | None

None

time: float

‘field(…)’

source_id: int

0

ctrl: bool

False

shift: bool

False

alt: bool

False

meta: bool

False

class simvx.core.input.events.ButtonEvent[source]

Bases: simvx.core.input.events.InputEvent

A button with no position going down or coming up: a pad’s button, a tablet’s express key.

A pointer’s buttons arrive on its :class:PointerEvent, because a press there has a place. This is the same model for a device that has no place to press: one control in :attr:button.

Attributes: button: What the layout in force calls the button, Controller.A, or its index form, Tablet.button(3). pressed: True on press, False on release. device_id: Which device, the engine’s id for it.

button: simvx.core.input.controls.DeviceButton

None

pressed: bool

None

device_id: int

0

time: float

‘field(…)’

source_id: int

0

ctrl: bool

False

shift: bool

False

alt: bool

False

meta: bool

False

class simvx.core.input.events.AxisEvent[source]

Bases: simvx.core.input.events.InputEvent

An axis with no position moving: a pad’s stick or trigger, a tablet’s ring or strip.

Both halves of what an axis can say, whichever its hardware reports: where it is and how far it moved. A ring that reports only motion has no position, and :attr:value says so with None rather than a number that would mean nothing.

Attributes: axis: What the layout in force calls the axis, Controller.LEFT_X or Tablet.RING, or its index form, Tablet.axis(0). value: Where it is now, or None for an axis that reports only motion. delta: How far it moved in this report, the short way round a ring. device_id: Which device, the engine’s id for it.

axis: simvx.core.input.controls.DeviceAxis

None

value: float | None

None

delta: float

0.0

device_id: int

0

time: float

‘field(…)’

source_id: int

0

ctrl: bool

False

shift: bool

False

alt: bool

False

meta: bool

False

class simvx.core.input.events.ActionEvent[source]

Bases: simvx.core.input.events.InputEvent

An action edge, resolved from whatever device binding produced it.

What an action handler, @on_input("jump"), receives. It asked about an action, so it is given the action: which one, which way it went, what it reads, whose it is, and the device event underneath it.

Attributes: action: The action’s name. pressed: True as the action opens, False as it closes. value: What the action reads at this edge, in the type its bindings gave it: bool for a button action, float for one carrying an Axis1D or a bare axis, Vec2 for one carrying an Axis2D or a stick. player: Whose action this is. 0 is the default player, which owns every device nobody else was paired to, so a single-player game sees 0 throughout. source: The device event the binding resolved from, or None. context: The input context that declared the action, or None.

action: str

None

pressed: bool

None

value: bool | float | simvx.core.math.types.Vec2

False

player: int

0

source: simvx.core.input.events.InputEvent | None

None

context: simvx.core.input.contexts.InputContext | None

None

time: float

‘field(…)’

source_id: int

0

ctrl: bool

False

shift: bool

False

alt: bool

False

meta: bool

False

class simvx.core.input.events.PickEvent[source]

Bases: simvx.core.input.events.InputEvent

A ray through the pointer hitting a node, delivered to on_picked.

Attributes: source: The pointer event the ray was cast for, or None. viewport: The space the pointer was in, or None for the root. button: Which button was involved, as :attr:PointerEvent.button. pressed: True on press, False on release. origin: World-space ray origin. direction: World-space ray direction, normalised. distance: Distance along the ray to the hit. node: The node that was hit, or None. point: The world-space hit point, or None where it is not computed. normal: The surface normal there, or None where it is not computed.

origin: simvx.core.math.types.Vec3

None

direction: simvx.core.math.types.Vec3

None

source: simvx.core.input.events.InputEvent | None

None

viewport: simvx.core.viewport.SubViewport | None

None

button: simvx.core.input.controls.DeviceButton | None

None

pressed: bool

True

distance: float

0.0

node: simvx.core.node.Node | None

None

point: simvx.core.math.types.Vec3 | None

None

normal: simvx.core.math.types.Vec3 | None

None

time: float

‘field(…)’

source_id: int

0

ctrl: bool

False

shift: bool

False

alt: bool

False

meta: bool

False

class simvx.core.input.events.ControllerConnectionEvent[source]

Bases: simvx.core.input.events.InputEvent

A controller arriving or leaving.

Attributes: device_id: Which pad, the key it has in Input.controllers. connected: True when it arrived, False when it left.

device_id: int

None

connected: bool

None

time: float

‘field(…)’

source_id: int

0

ctrl: bool

False

shift: bool

False

alt: bool

False

meta: bool

False

simvx.core.input.events.GuiInputEvent

None