"""The devices under the player, and the pads among them as objects.
``Input.devices`` is a mapping of the engine's id to every device the machine
has, each an :class:`InputDevice`, and ``Input.controllers`` is the pads among
them, each a :class:`ControllerDevice`. A ``ControllerDevice`` answers the questions
a game asks of one pad: what it is called, which family
of controller it belongs to (so a prompt can say Cross rather than A), whether
its buttons carry the engine's standard meanings, what is held on it, where its
sticks are, and whether it can be made to vibrate.
**A device is a live view, not a snapshot.** It holds the store and its own
id and reads through on every query, so the object a game stored at connection
never goes stale and a per-frame poll allocates nothing beyond the
:class:`~simvx.core.math.types.Vec2` a stick has always returned. A snapshot
would have to be rebuilt every frame by everyone who kept one, and the cost of
getting that wrong is a game reading last frame's stick forever.
**A pad that disconnects answers, it does not raise.** It leaves
``Input.controllers`` the moment the poll that lost it closes, so a game
enumerating pads never sees one mid-teardown; but a ``ControllerDevice`` a game is still
holding keeps working and reports a pad with nothing held: :attr:`connected` is
False, every button is up, every axis is zero, and :meth:`rumble` does nothing
and returns False. The release edges the disconnect produced are readable for
the rest of that frame, which is what lets a game stop what the player was
doing rather than run on the last reading.
**The device record is process-wide**, held once in :data:`store` and read
through every tree's ``Input``: the held keys and mouse buttons, the fingers on
the glass, the pads and the input method. A key is down on the machine and a
controller is plugged into the machine, not into a scene tree, so an editor and
the game it is running agree about both. Action state, contexts and focus are
the other half and stay per tree, resolved from the tree's own **delivered
record** of what actually reached it (see
:class:`~simvx.core.input.state._Input`), which is why a key the editor never
forwarded presses nothing in the game while ``Input.is_key_pressed`` there
still says the machine is holding it.
**A test isolates devices in time, not in space.** There is no second store to
run a test against, in the same way that there is no second machine; what a
test has instead is :func:`reset`, which puts the pads and the input method
back as they were at process start so an injected pad cannot leak into the next
test. ``Input._reset()`` calls it, so a test that already resets input needs
nothing further, and a test that only wants the hardware facts cleared calls it
directly. Its sibling is
:func:`simvx.core.input.capabilities.reset`, which does the same for the
capability flags and is deliberately separate: a capability is declared once by
a backend and a pad arrives and leaves many times.
"""
from __future__ import annotations
from collections.abc import Callable, Collection, Iterator, Sequence
from contextlib import AbstractContextManager, contextmanager
from dataclasses import dataclass, field
from enum import StrEnum
from itertools import count
from typing import cast
from ..math.types import Vec2
from ..signals import Signal
from .controls import Controller, DeviceAxis, DeviceButton, DeviceControl, DeviceStick, DeviceType
from .enums import ControllerFamily, ControllerSource, InputMethod
from .layouts import DeviceLayout, type_layout
__all__ = [
"AxisInfo",
"AxisReport",
"ButtonInfo",
"ControllerDevice",
"InputDevice",
"family_for",
"hold_frame",
"reset",
"store",
]
#: Engine device ids, one per device for the life of the process. Never reused,
#: so an id a game kept cannot come to mean another device.
_device_ids = count(1)
#: Default radial deadzone for :meth:`Controller.stick`. A stick at rest rarely
#: reads exactly zero, and a game that wants the raw value asks for
#: ``deadzone=0.0``.
STICK_DEADZONE = 0.15
#: Name fragments that identify a controller family, most specific first. A pad
#: names itself and the driver passes that name through, so matching on it is
#: the only identification available without a per-device database.
_FAMILY_KEYWORDS: tuple[tuple[ControllerFamily, tuple[str, ...]], ...] = (
(
ControllerFamily.PLAYSTATION,
("playstation", "dualshock", "dualsense", "ps3", "ps4", "ps5", "sony", "wireless controller"),
),
(ControllerFamily.NINTENDO, ("nintendo", "switch", "joy-con", "joycon", "pro controller", "wii", "gamecube")),
(ControllerFamily.XBOX, ("xbox", "x-box", "xinput", "microsoft")),
)
[docs]
def family_for(name: str, guid: str = "", *, recognised: bool = True) -> ControllerFamily:
"""Which controller family a pad belongs to, from the name its driver reports.
The family decides what a prompt draws, and nothing else: the engine's
controls are positional either way, so ``Controller.A`` is the bottom face
button on every pad and only its printed label moves.
A pad the engine cannot lay out as a standard controller (``recognised`` False) is
:attr:`~simvx.core.input.enums.ControllerFamily.JOYSTICK` whatever it is
called, because naming its buttons after a controller's would be a guess. One
that is laid out but whose name reveals no family is
:attr:`~simvx.core.input.enums.ControllerFamily.GENERIC`, which prompts with
the engine's own names.
"""
if not recognised:
return ControllerFamily.JOYSTICK
haystack = f"{name} {guid}".lower()
for family, keywords in _FAMILY_KEYWORDS:
if any(word in haystack for word in keywords):
return family
return ControllerFamily.GENERIC
[docs]
class AxisReport(StrEnum):
"""What an axis's hardware reports: where it is, how far it moved, or both.
A ring reports an absolute angle on some drivers and deltas on others, so
this is declared per axis by whatever connects the device.
"""
#: A position. The engine derives the delta from successive reports.
ABSOLUTE = "absolute"
#: Motion only. There is no position, so the value reads ``None``.
RELATIVE = "relative"
#: Both, each as reported.
BOTH = "both"
[docs]
@dataclass(frozen=True, slots=True)
class AxisInfo:
"""One of a device's axes, as a rebind screen lists it.
Attributes:
index: The device's own index.
control: What the layout in force calls it.
label: What to print for it.
report: What the hardware reports for it.
wraps: Whether its position wraps at 1.0, as a ring's does, which is
what makes a delta across the wrap the short way round.
"""
index: int
control: DeviceAxis
label: str
report: AxisReport
wraps: bool
[docs]
def wrapped_delta(now: float, before: float) -> float:
"""The change from *before* to *now* on a scale that wraps at 1.0, the short way round."""
return ((now - before + 0.5) % 1.0) - 0.5
[docs]
def check_axes(
reports: Sequence[AxisReport | str], wraps: Collection[int]
) -> tuple[tuple[AxisReport, ...], frozenset[int]]:
"""Validate what a backend announced for a device's axes."""
parsed = tuple(AxisReport(r) for r in reports)
wrapping = frozenset(int(i) for i in wraps)
bad = sorted(i for i in wrapping if not 0 <= i < len(parsed))
if bad:
raise ValueError(f"wraps names axes {bad}, but the device announced {len(parsed)} axes")
return parsed, wrapping
@dataclass(slots=True)
class _DeviceRecord:
"""One device on the button mechanism, as the device record keeps it.
Keyed by the engine's own id, allocated per ``(type, backend id)``, so a
pen and a touchscreen that both call themselves device 0 are two records.
What is held and where each axis is are the device's indices: the names a
layout gives them are resolved by whoever asks.
"""
id: int
type: DeviceType
backend_id: int
name: str = ""
guid: str = ""
#: The layout the backend announced for this device, or None for the
#: type's default.
layout: DeviceLayout | None = None
#: How many buttons the backend announced, or None where it announced none.
button_count: int | None = None
axis_reports: tuple[AxisReport, ...] = ()
wraps: frozenset[int] = frozenset()
connected: bool = True
held: set[int] = field(default_factory=set)
#: Where each axis is; None for one that reports only motion.
axis_values: dict[int, float | None] = field(default_factory=dict)
#: How far each axis moved this frame, summed. Spent at the frame boundary.
axis_deltas: dict[int, float] = field(default_factory=dict)
@dataclass(slots=True)
class _PadRecord(_DeviceRecord):
"""One pad, as the device record keeps it: a device, and what a pad has besides.
What is held and where each axis is are the pad's indices, as they are for
every device, named by its layout: the standard positions for a pad a
mapping database laid out, the driver's own for one it did not. The edges
are kept too, because a pad is polled and ``ControllerDevice.just_pressed``
asks about a change the poll found.
"""
source: ControllerSource = ControllerSource.DEVICE
family: ControllerFamily = ControllerFamily.GENERIC
recognised: bool = True
just_pressed: set[int] = field(default_factory=set)
just_released: set[int] = field(default_factory=set)
#: Where each axis stood before this frame's first change.
axes_prev: dict[int, float] = field(default_factory=dict)
def clear_edges(self) -> None:
"""Drop this frame's edges."""
self.just_pressed.clear()
self.just_released.clear()
self.axes_prev.clear()
[docs]
class ControllerDevice(InputDevice):
"""One connected pad. Read it through ``Input.controllers[device_id]``.
A game normally binds actions and never touches this; it is here for what a
binding cannot express: which pad a local-multiplayer player is holding,
what to draw as a prompt, and rumble. It is a device like any other in
``Input.devices``, with what only a pad has besides.
"""
__slots__ = ()
[docs]
def __repr__(self) -> str:
pad = self._pad
if pad is None:
return f"ControllerDevice(id={self._id}, connected=False)"
return (
f"ControllerDevice(id={self._id}, name={pad.name!r}, source={pad.source}, "
f"family={pad.family}, recognised={pad.recognised}, connected={pad.connected})"
)
@property
def _pad(self) -> _PadRecord | None:
return self._store.pads.get(self._id)
[docs]
@property
def source(self) -> ControllerSource:
"""Where the pad comes from: real hardware, a test, or the on-screen pad."""
pad = self._pad
return pad.source if pad is not None else ControllerSource.DEVICE
[docs]
@property
def family(self) -> ControllerFamily:
"""Which controller family it belongs to, which is what a prompt draws."""
pad = self._pad
return pad.family if pad is not None else ControllerFamily.GENERIC
[docs]
@property
def recognised(self) -> bool:
"""Whether a controller database recognised this device and laid it out in the standard positions.
Recognised, ``Controller.A`` is its bottom face button. Not, it has
only the driver's numbered controls, ``Controller.button(i)`` and
``Controller.axis(i)``, until the player maps names onto them:
``InputMap.rebind(Controller.A, Controller.button(3).on(device))``
makes ``Controller.A`` that device's button 3, for actions, handlers
and these queries alike, and is saved with the player's rebinds. Until
then a named query on it raises, naming what to do, and an action
bound to a name reads at rest on it.
"""
pad = self._pad
return pad.recognised if pad is not None else False
[docs]
@property
def connected(self) -> bool:
"""Whether the pad is still there.
False for a pad that has gone: every query below then answers as an
untouched pad rather than raising, so a game holding this object across
the disconnect keeps running.
"""
pad = self._pad
return pad.connected if pad is not None else False
def _index(self, control: DeviceButton) -> int | None:
control = self._own(control)
pad = self._pad
return None if pad is None else self._store.layout_of(pad).button_index(control)
def _own[C: DeviceControl](self, control: C) -> C:
"""*control* as this controller answers to it: what the player mapped it to here, or itself.
A named control on a controller no database recognised, and that the
player has not mapped, is refused, saying what to ask instead. Such a
device has buttons and axes and no names for them: its A is not
known, so ``pressed(Controller.A)`` has no honest answer, and False
would read as a button nobody pressed. An action bound to the name
reads at rest on it instead, because an action is shared by every
device and one unnamed pad must not break it for the others.
"""
if not control.is_named:
return control
from .map import current_input_map
try:
own = current_input_map()._unit_redirects.get(self._id)
except RuntimeError:
own = None
if own:
mapped = own.get(control.on(None))
if mapped is not None:
return cast(C, mapped)
pad = self._pad
if pad is None or pad.recognised:
return control
raise ValueError(
f"{pad.name or 'this controller'!r} is not a recognised controller, so it has no {control!r}: "
"its controls are Controller.button(i) and Controller.axis(i) until the player maps names onto them "
"with InputMap.rebind(Controller.A, Controller.button(i).on(device))."
)
[docs]
def pressed(self, button: DeviceButton) -> bool:
"""Whether *button* is held on this controller right now.
Raises ``ValueError`` for a named control (``Controller.A``) on a
controller that is not :attr:`recognised` and that the player has not
mapped it on.
"""
return super().pressed(self._own(button))
[docs]
def axis_delta(self, axis: DeviceAxis) -> float:
"""How far *axis* moved this frame; a named axis on an unrecognised controller raises, as :meth:`pressed`."""
return super().axis_delta(self._own(axis))
[docs]
def just_pressed(self, button: DeviceButton) -> bool:
"""Did *button* go down on this pad during this frame?
Inside a physics tick, during the frame that tick opens: the frame's
edges belong to its first tick and to no other, the same rule every
other edge query follows, so a press fires once whatever the display
and the tick rate are doing.
"""
pad = self._pad
index = self._index(button)
return pad is not None and index is not None and not self._store.tick_edges_hidden and index in pad.just_pressed
[docs]
def just_released(self, button: DeviceButton) -> bool:
"""Did *button* come up on this pad during this frame?
True for every button a pad was holding when it was unplugged, for the
frame the disconnect happened on. Tick-scoped inside a tick, as
:meth:`just_pressed`.
"""
pad = self._pad
index = self._index(button)
return (
pad is not None and index is not None and not self._store.tick_edges_hidden and index in pad.just_released
)
[docs]
def axis(self, axis: DeviceAxis) -> float:
"""One axis in the engine's convention, with no deadzone applied.
Sticks run -1.0 to 1.0 with +y down the screen; triggers run 0.0
released to 1.0 fully pulled. Raw because a deadzone is a gameplay
decision: :meth:`stick` applies the radial one a stick wants, and an
action binding applies its own. A named axis on a controller that is
not :attr:`recognised` raises, as :meth:`pressed` does.
"""
value = super().axis(self._own(axis))
return 0.0 if value is None else value
[docs]
def stick(self, stick: DeviceStick = Controller.LEFT_STICK, *, deadzone: float = STICK_DEADZONE) -> Vec2:
"""One stick as a vector, zeroed inside a radial deadzone.
Radial rather than per-axis, so a stick pushed diagonally is not
clipped into a square: the whole vector is discarded when it is shorter
than *deadzone*, and passed through unchanged when it is not.
"""
v = Vec2(self.axis(stick.x), self.axis(stick.y))
return Vec2(0.0, 0.0) if v.length() < deadzone else v
[docs]
def rumble(self, strong: float = 1.0, weak: float = 0.0, duration: float = 0.2) -> bool:
"""Vibrate the pad, returning whether anything happened.
Args:
strong: The heavy low-frequency motor, 0.0 to 1.0.
weak: The light high-frequency motor, 0.0 to 1.0.
duration: Seconds to run for.
Returns:
False when this pad cannot vibrate, when the backend supplies no
rumble at all (``Input.capabilities.rumble`` is then False), or when
the pad has disconnected. A game may call it unconditionally.
"""
pad = self._pad
callback = self._store.rumble
if pad is None or not pad.connected or callback is None:
return False
return bool(
callback(pad.backend_id, min(1.0, max(0.0, strong)), min(1.0, max(0.0, weak)), max(0.0, float(duration)))
)
[docs]
def label(self, control: DeviceControl, *, positive: bool | None = None) -> str:
"""What *this* pad prints on a button or an axis.
``"A"`` on an Xbox pad, ``"Cross"`` on a PlayStation one and ``"B"`` on
a Nintendo one, all for ``Controller.A``: the controls are positional and
only the printing moves. ``positive`` names one half of a stick axis,
which is what a binding splits it into. A control the pad's layout
does not name prints as its number.
"""
from .prompts import axis_label, button_label
family = self.family
if isinstance(control, DeviceAxis):
name = control.whole
if name.name is not None and name in Controller:
half = {1: True, -1: False}.get(control.half, positive)
return axis_label(name, family, positive=half)
elif isinstance(control, DeviceButton) and control.name is not None and control in Controller:
return button_label(control, family)
return super().label(control)
class _DeviceStore:
"""The device record: what the machine is doing, held once for the process.
One instance exists, :data:`store`, and every
:class:`~simvx.core.input.state._Input` reads and writes it. The state
layer keeps the behaviour: polling a pad, delivering what it found, and
deciding when the input method has changed. What lives here is the state those operations act
on, because it describes the machine rather than a tree.
This is the bottom of the three layers. It answers what is physically
down, whoever the input went to, which is what ``Input.is_key_pressed``
and ``Input.is_mouse_button_pressed`` report. The middle layer is the
delivered record, which each :class:`~simvx.core.input.state._Input`
holds: the elements that actually reached *that* tree, and the only thing
an action resolves from. The two differ exactly where they should: a key
the editor never forwarded to the game it is running is down on the
machine and was never delivered to the game, so the machine says so and
the game's actions do not.
The connection and input-method signals are here for the same reason a pad
is. A pad arrives once however many trees are running, so the signal that
says so must be one object: a handler connected through one tree's
``Input`` hears the pad that another tree's poll found.
"""
#: A pad arrived. Handlers receive the :class:`ControllerDevice`, which is
#: already in ``Input.controllers`` by the time this fires.
controller_connected = Signal()
#: A pad left. Handlers receive the :class:`ControllerDevice`, which has already
#: gone from ``Input.controllers`` and reads as an untouched pad.
controller_disconnected = Signal()
#: The player picked up a different kind of device. Handlers receive the
#: new :class:`~.enums.InputMethod`; connect to reskin prompts.
input_method_changed = Signal()
#: A backend announced a device on the button mechanism. Handlers receive
#: the :class:`InputDevice`, already in ``Input.devices``.
device_connected = Signal()
def __init__(self) -> None:
# What is physically down, whoever it was delivered to. Written by
# every router that sees the input, which is why a hosted tree's
# router writing a forwarded key again costs nothing: the same key
# goes into the same set.
self.keys_pressed: set[int] = set()
self.keys_just_pressed: set[int] = set()
self.keys_just_released: set[int] = set()
self.mouse_buttons_pressed: set[int] = set()
self.mouse_buttons_just_pressed: set[int] = set()
self.mouse_buttons_just_released: set[int] = set()
# One record per pad, keyed by the engine's id for it like every other
# device, and the single source of truth for everything a pad reports.
# A disconnected pad stays here with ``connected`` False until the
# frame boundary, so the release edges the disconnect produced are
# readable for the frame they happened on; ``objects`` drops it at
# once, so nothing enumerating pads sees one mid-teardown.
self.pads: dict[int, _PadRecord] = {}
self.objects: dict[int, ControllerDevice] = {}
# Connection changes waiting for the poll transaction to close, as
# ``(device id, connected)``. Emitting inside the transaction would show a
# handler half a frame's pads.
self.notifications: list[tuple[int, bool]] = []
self.poll_depth: int = 0
#: How the backend makes a pad vibrate, as ``(backend pad id, strong, weak,
#: duration) -> bool``, or None where it cannot. Installed by whatever
#: polls the pads, alongside ``capabilities.declare(rumble=True)``.
self.rumble: Callable[[int, float, float, float], bool] | None = None
# Pad changes since the last flush, as ``("button" or "axis", device
# id, index, value)``. A pad arrives as a polled snapshot rather than as one
# callback per change, so the changes a poll produced are recorded here
# and turned into tree events by ``InputRouter.flush_joy_events``. Only
# a real change lands, which is what keeps an idle pad free of
# per-frame events, and the pad it happened on travels with it so an
# event can say which controller moved.
self.pending_joy_events: list[tuple[str, int, int, float]] = []
# None until something asks, because the startup answer depends on
# facts a backend declares after this object exists.
self.input_method: InputMethod | None = None
# Mouse travel since the method was last something other than keyboard
# and mouse, against ``mouse_method_threshold``.
self.mouse_method_travel: float = 0.0
#: How far the mouse must travel, in engine units, before it counts as
#: the player reaching for it. A nudged desk moves a cursor a pixel or
#: two, and switching on that would hide an on-screen pad under the
#: player's thumb; reaching for the mouse moves it much further. Raise
#: it for a game played on a shaking surface, lower it to nothing to
#: switch on any motion at all.
self.mouse_method_threshold: float = 16.0
# The devices on the control mechanism: the keyboard, the mouse, each
# touchscreen, each pen and each tablet, by engine id, and the id each
# ``(type, backend id)`` was given. A device that disconnects stays
# here with ``connected`` False until the frame boundary.
self.device_records: dict[int, _DeviceRecord] = {}
self.device_ids: dict[tuple[DeviceType, int], int] = {}
self.device_objects: dict[int, InputDevice] = {}
#: The layout a backend declared for every device of a type, over the
#: engine's default for it: the browser's mouse is in DOM order.
self.type_layouts: dict[DeviceType, DeviceLayout] = {}
# Depth of open ``hold_frame`` spans; see that method.
self._held: int = 0
#: Whether the physics tick now running is one the frame's edges do
#: not belong to. A frame's edges go to its first tick and no other,
#: so a pad edge read from a later tick of the same frame would fire
#: a second and a third time on a frame that happened to run three.
#: Set and restored by the tick span
#: (:meth:`~simvx.core.input.state._Input._fixed_tick`), which is why
#: it lives on the record the pads do rather than on one tree: a
#: :class:`ControllerDevice` is a live view over this store and has no tree.
#: False outside a tick, where an edge is the frame's and is read as
#: it always was.
self.tick_edges_hidden: bool = False
def device(self, device_type: DeviceType, backend_id: int = 0) -> _DeviceRecord:
"""The record for ``(device_type, backend_id)``, registered on first sight.
A device that arrives without having been announced is registered under
its type's layout, which is the aggregate mouse, keyboard and
touchscreen on every backend that does not enumerate them.
"""
if device_type is DeviceType.CONTROLLER:
return self.pad(backend_id)
engine_id = self.device_ids.get((device_type, backend_id))
if engine_id is not None:
rec = self.device_records.get(engine_id)
if rec is not None:
if not rec.connected:
rec.connected = True
self.device_objects[engine_id] = InputDevice(self, engine_id)
return rec
engine_id = next(_device_ids)
rec = _DeviceRecord(engine_id, device_type, backend_id)
self.device_ids[(device_type, backend_id)] = engine_id
self.device_records[engine_id] = rec
self.device_objects[engine_id] = InputDevice(self, engine_id)
return rec
def pad(self, backend_id: int) -> _PadRecord:
"""The record of the pad the backend calls *backend_id*, made on first sight.
Registered but not yet connected: a pad is in ``Input.controllers`` once
whatever reported it says so, which is the poll or a simulated write.
"""
engine_id = self.device_ids.get((DeviceType.CONTROLLER, backend_id))
rec = self.pads.get(engine_id) if engine_id is not None else None
if rec is not None:
return rec
engine_id = next(_device_ids)
rec = _PadRecord(engine_id, DeviceType.CONTROLLER, backend_id, connected=False)
self.device_ids[(DeviceType.CONTROLLER, backend_id)] = engine_id
self.device_records[engine_id] = rec
self.pads[engine_id] = rec
return rec
def record_for(self, device_type: DeviceType, device_id: int) -> _DeviceRecord:
"""The record an event's ``device_id`` names, or the type's aggregate device for 0.
0 is "nobody said", which is what a hand-built event carries.
"""
if device_id:
rec = self.device_records.get(device_id)
if rec is not None and rec.type is device_type:
return rec
return self.device(device_type, 0)
def layout_of(self, rec: _DeviceRecord) -> DeviceLayout:
"""The backend's table for *rec*: its own where the backend gave one, else its type's."""
return rec.layout or type_layout(rec.type, self.type_layouts)
def type_layout(self, device_type: DeviceType) -> DeviceLayout:
"""The backend's table every device of *device_type* has unless it brought its own."""
return type_layout(device_type, self.type_layouts)
def disconnect_device(self, device_type: DeviceType, backend_id: int) -> _DeviceRecord | None:
"""Mark ``(device_type, backend_id)`` gone. Its record stays until the frame boundary."""
engine_id = self.device_ids.get((device_type, backend_id))
rec = self.device_records.get(engine_id) if engine_id is not None else None
if rec is None or not rec.connected:
return None
rec.connected = False
self.device_objects.pop(rec.id, None)
return rec
def spend_device_frame(self) -> None:
"""The frame boundary for the button mechanism: motion spent, the departed buried."""
for engine_id, rec in list(self.device_records.items()):
if rec.axis_deltas:
rec.axis_deltas.clear()
if not rec.connected:
del self.device_records[engine_id]
self.pads.pop(engine_id, None)
if self.device_ids.get((rec.type, rec.backend_id)) == engine_id:
del self.device_ids[(rec.type, rec.backend_id)]
@property
def frame_held(self) -> bool:
"""Whether a guest tree is running inside somebody else's frame."""
return self._held > 0
@contextmanager
def hold_frame(self) -> Iterator[None]:
"""Run a guest tree's frame without spending the devices' frame.
A pad edge lasts one frame, and the frame it lasts is the process's.
A second tree stepped inside that frame -- the editor's play mode runs
the game this way -- opens and closes its own input frame so that its
keys and mouse buttons get their own edges, and must not take the pad
edges down with it: the poll that produced them ran once, at the top of
the frame both trees are inside, and whoever steps second would
otherwise read a pad nobody is touching.
"""
self._held += 1
try:
yield
finally:
self._held -= 1
def reset(self) -> None:
"""Back to the state at process start: nothing held, no settled method.
The device record's half of ``Input._reset()``, and what a test calls
on its own to stop an injected key, button or pad reaching the
next test. Connections to the signals survive, as they do across a
real unplug: a handler is wired up by the game, not by the hardware.
"""
self.keys_pressed.clear()
self.keys_just_pressed.clear()
self.keys_just_released.clear()
self.mouse_buttons_pressed.clear()
self.mouse_buttons_just_pressed.clear()
self.mouse_buttons_just_released.clear()
self.pads.clear()
self.objects.clear()
self.notifications.clear()
self.poll_depth = 0
self.rumble = None
self.pending_joy_events.clear()
self.input_method = None
self.mouse_method_travel = 0.0
self.mouse_method_threshold = 16.0
self.device_records.clear()
self.device_ids.clear()
self.device_objects.clear()
self.type_layouts.clear()
self._held = 0
self.tick_edges_hidden = False
#: The machine's devices, shared by every tree in the process.
store = _DeviceStore()
[docs]
def reset() -> None:
"""Put the pads and the input method back as they were at process start."""
store.reset()
[docs]
def hold_frame() -> AbstractContextManager[None]:
"""Run a guest tree's frame without spending the devices' frame.
See :meth:`_DeviceStore.hold_frame`, which this stands in front of so a
caller needs the module and not the object.
"""
return store.hold_frame()