Source code for 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.
"""

from __future__ import annotations

import math
from dataclasses import dataclass, field, replace
from time import monotonic
from typing import TYPE_CHECKING

from ..math.types import Vec2, Vec3
from .controls import (
    MODIFIER_OF,
    MODIFIER_SIDES,
    DeviceAxis,
    DeviceButton,
    DeviceControl,
    DeviceType,
    Key,
    key_to_name,
    name_to_keys,
)
from .enums import PointerPhase, PointerTool, PointerType

if TYPE_CHECKING:
    from ..node import Node
    from ..viewport import SubViewport
    from .contexts import InputContext

#: The written names of the modifiers, as a combo spells them: ``"ctrl+s"``.
MODIFIER_NAMES = ("ctrl", "shift", "alt", "meta")


[docs] @dataclass class InputBinding: """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: DeviceButton | DeviceAxis | None = None deadzone: float = 0.2 held: tuple[DeviceButton, ...] = () exact: bool | None = None scale: float = 1.0 rate: float | None = None
[docs] def __post_init__(self) -> None: held = tuple(self.held) if not isinstance(self.held, DeviceButton) else (self.held,) for control in held: if not isinstance(control, DeviceButton): raise TypeError(f"a binding holds buttons, and {control!r} is not one") self.held = held self.scale = float(self.scale) if self.rate is not None: self.rate = float(self.rate)
[docs] def holding(self, *controls: DeviceButton) -> InputBinding: """This binding with *controls* held as well, before the ones it already holds.""" return replace(self, held=(*controls, *self.held))
[docs] def __add__(self, other: DeviceControl) -> InputBinding: """Extend a chord: ``Key.CTRL + Key.SHIFT + Key.S`` holds both and presses S.""" if self.control is None or not isinstance(self.control, DeviceButton): raise TypeError(f"only a chord of buttons extends with +, and {self.control!r} is not a button") return replace(self, control=other, held=(*self.held, self.control)) # type: ignore[arg-type]
[docs] def __mul__(self, factor: float) -> InputBinding: """This binding with its value multiplied into the action's unit: ``(Key.CTRL + Pointer.PRIMARY) * 2``.""" if not isinstance(factor, int | float) or isinstance(factor, bool): return NotImplemented return replace(self, scale=self.scale * float(factor))
__rmul__ = __mul__
[docs] def per_second(self, rate: float) -> InputBinding: """This binding as motion: its held value times *rate* each second, over the step it is read in.""" return replace(self, rate=float(rate))
[docs] @property def modifiers(self) -> frozenset[DeviceButton]: """The modifiers this binding holds, as groups: ``{Key.CTRL}`` for either Control key or one side.""" return frozenset(MODIFIER_OF.get(h.on(None), h.on(None)) for h in self.held if _is_modifier(h))
[docs] @property def is_exact(self) -> bool: """Whether this binding refuses modifiers it does not hold: ``exact``, or its default when unset.""" return self.exact if self.exact is not None else any(_is_modifier(h) for h in self.held)
[docs] @property def is_key(self) -> bool: """Whether this binding is a key on the keyboard.""" control = self.control return control is not None and control.device is DeviceType.KEYBOARD
[docs] @property def key_combo(self) -> str | None: """The combo spelling of a key binding (``"ctrl+shift+s"``), or None if it is not keys alone.""" control = self.control if not isinstance(control, DeviceButton) or control.device is not DeviceType.KEYBOARD: return None if any(h.device is not DeviceType.KEYBOARD for h in self.held): return None return "+".join([*(key_part(h) for h in self.held), key_part(control)])
[docs] def matches_key(self, event: KeyEvent) -> bool: """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. """ control = self.control if not isinstance(control, DeviceButton) or (event.key is not control and event.key not in control): return False if any(not _is_modifier(h) for h in self.held): return False down = {name for name in MODIFIER_NAMES if getattr(event, name)} named = {_modifier_name(m) for m in self.modifiers} if not named <= down: return False if not self.is_exact: return True own = MODIFIER_OF.get(event.key) return down - ({_modifier_name(own)} if own is not None else set()) == named
def _is_modifier(control: DeviceButton) -> bool: bare = control.on(None) return bare in MODIFIER_SIDES or bare in MODIFIER_OF def _modifier_name(group: DeviceButton) -> str: """The written name of a modifier group: ``"ctrl"`` for ``Key.CTRL``.""" return str(group.name)
[docs] def key_part(key: DeviceButton) -> str: """How one key of a combo is written: a modifier group by its name, any other key by its own.""" bare = key.on(None) if bare in MODIFIER_SIDES: return str(bare.name) return key_to_name(bare) if bare not in MODIFIER_OF else str(bare.name)
[docs] def combo_for_key(key: DeviceButton, *, held: tuple[DeviceButton, ...] = ()) -> str: """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. """ binding = InputBinding(key, held=held).key_combo assert binding is not None return binding
[docs] def modifiers_held(event: InputEvent) -> tuple[DeviceButton, ...]: """The modifier groups *event* says were held, in combo order: ``(Key.CTRL, Key.SHIFT)``.""" return tuple(group for group in MODIFIER_SIDES if getattr(event, str(group.name), False))
[docs] def key_named(name: str) -> DeviceButton | None: """The key a combo part names: a modifier group (``"ctrl"``), a key name, or a key's constant name.""" for group in MODIFIER_SIDES: if group.name == name: return group keys = name_to_keys(name) if keys: return keys[0] found = getattr(Key, name.upper(), None) return found if isinstance(found, DeviceButton) else None
[docs] def key_combo_to_binding(spec: str) -> InputBinding | None: """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. """ parts = spec.lower().split("+") if len(parts) < 2 or not all(parts): return None keys = [key_named(part) for part in parts] if any(key is None for key in keys): return None *held, pressed = keys return InputBinding(pressed, held=tuple(held)) # type: ignore[arg-type]
[docs] def key_event_for_combo(spec: str, *, pressed: bool = True) -> KeyEvent: """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. """ binding = key_combo_to_binding(spec) if binding is None: keys = name_to_keys(spec) if not keys: raise ValueError(f"no key is named {spec!r}; try a name such as 'escape' or a combo such as 'ctrl+s'") binding = InputBinding(keys[0]) key = binding.control assert isinstance(key, DeviceButton) if len(binding.modifiers) != len(binding.held): raise ValueError(f"{spec!r} holds a key that is not a modifier, which one key event cannot carry") names = {_modifier_name(m) for m in binding.modifiers} return KeyEvent( key=key, pressed=pressed, label=key_to_name(key), ctrl="ctrl" in names, shift="shift" in names, alt="alt" in names, meta="meta" in names, )
# ============================================================================ # The event family # ============================================================================
[docs] @dataclass(slots=True, kw_only=True) class InputEvent: """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(default_factory=monotonic) source_id: int = 0 ctrl: bool = False shift: bool = False alt: bool = False meta: bool = False
[docs] @dataclass(slots=True, kw_only=True) class KeyEvent(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: DeviceButton pressed: bool label: str = "" echo: bool = False device_id: int = 0 cancelled: bool = False
[docs] @dataclass(slots=True, kw_only=True) class TextEvent(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 composing: bool = False
def _altitude(tilt: tuple[float, float]) -> float: """The angle between the barrel and the surface, from tilt: pi/2 upright, 0 flat. Every platform reports tilt as x and y, so that is the lossless form stored; a brush engine consumes the polar pair, so it is computed on read with the W3C Pointer Events conversion. """ tx, ty = tilt if tx == 0.0 and ty == 0.0: return math.pi / 2 if tx == 0.0: return math.pi / 2 - abs(ty) if ty == 0.0: return math.pi / 2 - abs(tx) return math.atan(1.0 / math.hypot(math.tan(tx), math.tan(ty))) def _azimuth(tilt: tuple[float, float]) -> float: """The direction the barrel leans, from tilt, in ``[0, 2pi)``; 0 at zero tilt.""" tx, ty = tilt if tx == 0.0: return math.pi / 2 if ty > 0.0 else (3 * math.pi / 2 if ty < 0.0 else 0.0) if ty == 0.0: return 0.0 if tx > 0.0 else math.pi if abs(tx) == math.pi / 2 or abs(ty) == math.pi / 2: return 0.0 angle = math.atan2(math.tan(ty), math.tan(tx)) return angle + 2 * math.pi if angle < 0.0 else angle
[docs] @dataclass(slots=True, kw_only=True) class PointerEvent(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: PointerPhase pointer_id: int = 0 pointer_type: PointerType = PointerType.MOUSE is_primary: bool = True position: tuple[float, float] = (0.0, 0.0) delta: tuple[float, float] = (0.0, 0.0) button: 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: PointerTool = PointerTool.TIP tool_id: int = 0 contact_size: tuple[float, float] = (0.0, 0.0) locked: bool = False viewport: SubViewport | None = None device_id: int = 0
[docs] def __post_init__(self) -> None: button = self.button if button is not None and button.device is DeviceType.POINTER: # Built by hand with a role and not yet through the router: the # role becomes this pointer type's member of it, so a widget test # reads the control a published event carries. from .controls import ROLE_MEMBER self.button = ROLE_MEMBER.get((button, DeviceType(self.pointer_type.value)), button)
[docs] @property def contact(self) -> bool: """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. """ return bool(self.buttons & self.contact_mask)
[docs] @property def altitude(self) -> float: """The angle between the barrel and the surface, radians: pi/2 upright, 0 flat.""" return _altitude(self.tilt)
[docs] @property def azimuth(self) -> float: """The direction the barrel leans, radians in ``[0, 2pi)``; 0 at zero tilt.""" return _azimuth(self.tilt)
[docs] @dataclass(frozen=True, slots=True, kw_only=True) class PointerState: """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 pointer_type: PointerType position: tuple[float, float] 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: PointerTool = PointerTool.TIP 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
[docs] @property def altitude(self) -> float: """The angle between the barrel and the surface, radians: pi/2 upright, 0 flat.""" return _altitude(self.tilt)
[docs] @property def azimuth(self) -> float: """The direction the barrel leans, radians in ``[0, 2pi)``; 0 at zero tilt.""" return _azimuth(self.tilt)
[docs] @dataclass(slots=True, kw_only=True) class ScrollEvent(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] pointer_id: int = 0 position: tuple[float, float] = (0.0, 0.0) viewport: SubViewport | None = None
[docs] @dataclass(slots=True, kw_only=True) class ButtonEvent(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: DeviceButton pressed: bool device_id: int = 0
[docs] @dataclass(slots=True, kw_only=True) class AxisEvent(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: DeviceAxis value: float | None delta: float = 0.0 device_id: int = 0
[docs] @dataclass(slots=True, kw_only=True) class ActionEvent(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 pressed: bool value: bool | float | Vec2 = False player: int = 0 source: InputEvent | None = None context: InputContext | None = None
[docs] @dataclass(slots=True, kw_only=True) class PickEvent(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: Vec3 direction: Vec3 source: InputEvent | None = None viewport: SubViewport | None = None button: DeviceButton | None = None pressed: bool = True distance: float = 0.0 node: Node | None = None point: Vec3 | None = None normal: Vec3 | None = None
[docs] @dataclass(slots=True, kw_only=True) class ControllerConnectionEvent(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 connected: bool
#: Everything ``on_gui_input`` can be handed, which is the same object the #: tree received: a key press or release is a :class:`KeyEvent`, a typed #: character a :class:`TextEvent`, a pointer press, release or move a #: :class:`PointerEvent`, and a wheel notch a :class:`ScrollEvent`. #: #: A handler tells them apart with ``isinstance``, and the guard belongs in #: the branch that reads the field rather than above several of them: a guard #: placed above is only correct while every branch under it returns, and the #: next edit moves one. GuiInputEvent = KeyEvent | TextEvent | PointerEvent | ScrollEvent