Source code for simvx.core.input.capabilities

"""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"]


[docs] @dataclass(frozen=True, slots=True) class InputCapabilities: """The input facts of the machine the game is running on. Attributes: touch: A touchscreen is present. pen: A pen is present. pen_axes: The pen measurements the hardware reports, as :class:`~.enums.PenAxis` members. An axis absent here reads as its neutral value on every event, and a pressure of 1.0 in contact means the pen cannot measure one rather than that it was pressed as hard as it goes. touch_pressure: The touchscreen measures how hard a finger presses. Without it a finger in contact reports 1.0. hover: A pointer can be over something without pressing it. coarse_pointer: The primary pointer is a finger rather than a cursor, which is what sizes a touch target. pointer_lock: The pointer can be locked for relative motion. controllers: Controllers can be enumerated. rumble: A connected controller can be made to vibrate. event_timestamps: The input source supplies each event's own time. False means :class:`~simvx.core.input.router.InputRouter` stamps events as it receives them, which is accurate to the frame rather than to the input. It stays True once a source has supplied one, so an event that carries no time of its own (a synthetic event, a simulated key, a driver that stamps zero) does not withdraw it. soft_keyboard: An on-screen keyboard can be opened for text entry. screen_insets: The platform reports occluded screen edges (a notch, a soft keyboard, a home indicator). """ touch: bool = False pen: bool = False pen_axes: frozenset[PenAxis] = frozenset() touch_pressure: bool = False hover: bool = False coarse_pointer: bool = False pointer_lock: bool = False controllers: bool = False rumble: bool = False event_timestamps: bool = False soft_keyboard: bool = False screen_insets: bool = False
#: 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()