"""What the device under the player can actually do.
Read through ``Input.capabilities``. These are process-wide device facts, not
per-tree state: an editor running a game is on the same hardware as the game,
so both read the same answers.
Every field defaults to the conservative answer, which is "no", and the only
way to a True is :func:`declare`. A capability that reads True was reported by
something that knew; nothing is assumed on anyone's behalf.
**Declaring is one-way.** A capability is a claim about what the input source
*can* do, and the two directions are not symmetrical. One event carrying a
platform time proves the source can supply one. An event without a time proves
nothing: it is equally consistent with a backend that has no clock, with a
synthetic event, and with a test harness injecting a key. So a source states
what it has and stays quiet about what it has not, and a fact never goes back
down except through :func:`reset`, which exists for a test and for a source
tearing down. A capability that could be withdrawn by the next event would not
be a device fact at all: it would report whatever published last.
**Who declares what.** Each fact has exactly one place that knows it, and the
answer is the same on every backend:
================ =======================================================
Fact Declared by
================ =======================================================
touch the router, on a pointer event whose type is touch
pen the router, on a pointer event whose type is pen
pen_axes the router, on a pen event carrying a measured axis:
a backend passes a value for what the platform
measured and leaves out what it did not
touch_pressure the router, on a touch event carrying a measured
pressure, under the same rule
hover the router, on a pointer moving or crossing a boundary
with no button held
pointer_lock the router, on a locked pointer event; and by a backend
that can lock one, at ``create_window``. SDL3 and GLFW
do; Qt grabs and confines the cursor without stopping
it, which is not a lock; a browser page declares it
from ``requestPointerLock``
coarse_pointer a backend, from the platform's own query (a
``pointer: coarse`` media query in the browser, which
is the only one of the three that has such a query).
Never inferred from a touch: a laptop with a
touchscreen delivers primary touches and still has a
cursor
controllers the state layer, the first time a pad is recorded
rumble the state layer, when a poller installs its rumble
callback (``_Input._install_controller_rumble``), so the
fact and the mechanism land together
event_timestamps the router, on an event that carried its own time
soft_keyboard a backend that can open one for text entry
screen_insets a backend that reports occluded screen edges
================ =======================================================
A backend declares at ``create_window`` rather than at construction, because
``resolve_backend`` constructs each candidate in turn until one imports, and a
backend that was never used must not speak for the machine. A browser page has
no such call and declares on the same wire it sends input on
(:mod:`~simvx.core.input.web_events`), which reaches the streaming client as
well as the exported page: in both the facts describe the machine the player
is sitting at rather than the one running the engine.
"""
from dataclasses import dataclass, replace
from .enums import PenAxis
__all__ = ["InputCapabilities", "current", "declare", "reset"]
#: The machine's input facts, shared by every tree in the process.
_current = InputCapabilities()
[docs]
def current() -> InputCapabilities:
"""The capabilities as they stand. ``Input.capabilities`` reads this."""
return _current
[docs]
def declare(**facts: bool | frozenset[PenAxis]) -> None:
"""State what the input source can do. Turns facts on; never off.
Called by whatever knows: a windowing backend as it wires itself up, the
browser runtime for the page it is running in, the router for a fact an
event has just proved. Passing False is not an error and does nothing,
which lets a caller pass a computed value without having to guard it. A
set-valued fact (``pen_axes``) is unioned with what is already known, so
it too only ever grows.
Raises:
TypeError: naming any field that is not a capability.
"""
global _current
unknown = [name for name in facts if not hasattr(_current, name)]
if unknown:
known = ", ".join(f.name for f in InputCapabilities.__dataclass_fields__.values())
raise TypeError(f"not an input capability: {', '.join(sorted(unknown))}. Known capabilities: {known}")
gained: dict[str, bool | frozenset[PenAxis]] = {}
for name, value in facts.items():
held = getattr(_current, name)
if isinstance(held, frozenset):
grown = held | value if isinstance(value, frozenset) else held
if grown != held:
gained[name] = grown
elif value and not held:
gained[name] = True
if gained:
_current = replace(_current, **gained) # type: ignore[arg-type]
[docs]
def reset() -> None:
"""Back to the conservative defaults, as at the start of the process."""
global _current
_current = InputCapabilities()