"""The backend mapping: which raw index on a device each named control is.
A device reports its buttons and axes by index, which is the only thing the
engine truly knows about hardware it has never seen. A :class:`DeviceLayout`
is the backend's table for one kind of device (SDL's controller database, the
browser's standard mapping, GLFW's mouse numbering, the engine's own pen
table), and it is what lets an event carry ``Pen.BARREL_1`` rather than
"button 2".
It is internal. A game never writes to it: every change a game or a player
makes to what a control means is a rebind of an entry in the input map
(``InputMap.rebind(Controller.A, Controller.button(1))``), which is per tree and
survives with the player's other rebinds.
"""
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass, field
from types import MappingProxyType
from typing import Any
from .controls import (
_KEY_CODES,
ROLE_MEMBER,
Controller,
DeviceAxis,
DeviceButton,
DeviceControl,
DeviceType,
Mouse,
Pen,
Pointer,
Touch,
_check_name,
)
__all__ = ["DeviceLayout"]
def _names(device: DeviceType, table: Mapping[Any, int] | None, kind: type[DeviceControl]) -> dict[str, int]:
"""A table keyed by controls (or, from a file, by their names) as name to index."""
out: dict[str, int] = {}
for control, index in (table or {}).items():
if isinstance(control, DeviceControl):
if not isinstance(control, kind) or control.device is not device or control.name is None:
raise ValueError(
f"a {device.value} layout names {kind.__name__}s of the {device.value}, got {control!r}"
)
name = control.name
else:
name = _check_name(str(control))
if not isinstance(index, int) or isinstance(index, bool) or index < 0:
raise ValueError(f"layout index for {device.value}:{name} must be an int of 0 or more, got {index!r}")
out[name] = index
return out
def _check_unique(device: DeviceType, table: Mapping[str, int], what: str) -> None:
seen: dict[int, str] = {}
for name, index in table.items():
if index in seen:
raise ValueError(
f"a {device.value} layout puts {seen[index]!r} and {name!r} both at {what} {index}: "
"a device reports one control per index"
)
seen[index] = name
[docs]
@dataclass(frozen=True, slots=True, eq=False)
class DeviceLayout:
"""The names one device gives its indices, and what it prints on each.
Keyed by the controls themselves, so a misspelt name is an
``AttributeError`` where it is written::
DeviceLayout(DeviceType.PEN, buttons={Pen.TIP: 0, Pen.BARREL_1: 2, Pen.BARREL_2: 1},
labels={Pen.BARREL_1: "Lower barrel"})
Each index has at most one name. A label is keyed by name, so it moves
with the name when an override moves it.
"""
device: DeviceType
buttons: Mapping[Any, int] = field(default_factory=dict)
axes: Mapping[Any, int] = field(default_factory=dict)
labels: Mapping[Any, str] = field(default_factory=dict)
_button_controls: dict[int, DeviceButton] = field(default_factory=dict, init=False, repr=False)
_axis_controls: dict[int, DeviceAxis] = field(default_factory=dict, init=False, repr=False)
[docs]
def __post_init__(self) -> None:
device = DeviceType(self.device)
object.__setattr__(self, "device", device)
buttons = _names(device, self.buttons, DeviceButton)
axes = _names(device, self.axes, DeviceAxis)
_check_unique(device, buttons, "button")
_check_unique(device, axes, "axis")
labels = {(k.name if isinstance(k, DeviceControl) else str(k)): str(v) for k, v in self.labels.items()}
object.__setattr__(self, "buttons", MappingProxyType(buttons))
object.__setattr__(self, "axes", MappingProxyType(axes))
object.__setattr__(self, "labels", MappingProxyType(labels))
[docs]
def __eq__(self, other: object) -> bool:
if not isinstance(other, DeviceLayout):
return NotImplemented
return (self.device, dict(self.buttons), dict(self.axes), dict(self.labels)) == (
other.device,
dict(other.buttons),
dict(other.axes),
dict(other.labels),
)
__hash__ = None # type: ignore[assignment]
[docs]
def moved(self, buttons: Mapping[str, int] | None = None, axes: Mapping[str, int] | None = None) -> DeviceLayout:
"""This layout with the given names moved to the given indices.
A name already at a target index that is not itself moved loses its
index, because an index has one name. :meth:`DeviceLayouts.override`
refuses that case with the fix; this is what applies a layer that was
checked against a different base.
"""
def move(table: Mapping[str, int], changes: Mapping[str, int] | None) -> dict[str, int]:
if not changes:
return dict(table)
taken = set(changes.values())
kept = {name: index for name, index in table.items() if name not in changes and index not in taken}
return {**kept, **changes}
return DeviceLayout(self.device, move(self.buttons, buttons), move(self.axes, axes), self.labels)
[docs]
def axis_control(self, index: int) -> DeviceAxis:
"""The control motion of *index* is: its name where the layout has one, else ``X.axis(index)``."""
found = self._axis_controls.get(index)
if found is None:
name = next((n for n, at in self.axes.items() if at == index), None)
found = DeviceAxis(self.device, name) if name is not None else DeviceAxis(self.device, index=index)
self._axis_controls[index] = found
return found
[docs]
def axis_index(self, axis: DeviceAxis) -> int | None:
"""The index *axis* is on this layout, whichever half or form it names, or None."""
if axis.device is not self.device:
return None
if axis.index is not None:
return axis.index
return self.axes.get(axis.name or "")
[docs]
@property
def primary(self) -> int | None:
"""The index of this device type's member of ``Pointer.PRIMARY``, which is what contact means."""
return self.button_index(Pointer.PRIMARY)
[docs]
def axis_label(self, index: int) -> str:
"""What to print for axis *index*: its name's label, else its number."""
name = next((n for n, at in self.axes.items() if at == index), None)
if name is not None and name in self.labels:
return self.labels[name]
return f"Axis {index + 1}"
#: The keyboard's fixed layout: the engine's key table.
KEYBOARD_LAYOUT = DeviceLayout(DeviceType.KEYBOARD, buttons=dict(_KEY_CODES))
#: The mouse in GLFW's numbering, which every desktop backend reports.
MOUSE_LAYOUT = DeviceLayout(
DeviceType.MOUSE,
buttons={Mouse.LEFT: 0, Mouse.RIGHT: 1, Mouse.MIDDLE: 2, Mouse.BACK: 3, Mouse.FORWARD: 4},
axes={Mouse.WHEEL: 0},
labels={
Mouse.LEFT: "Left Click",
Mouse.RIGHT: "Right Click",
Mouse.MIDDLE: "Middle Click",
Mouse.BACK: "Mouse Back",
Mouse.FORWARD: "Mouse Forward",
Mouse.WHEEL: "Mouse Wheel",
},
)
#: The mouse in DOM order, which the browser reports: middle before right.
DOM_MOUSE_LAYOUT = MOUSE_LAYOUT.moved(buttons={"middle": 1, "right": 2})
#: A finger's contact, and the gestures the engine recognises from fingers together.
TOUCH_LAYOUT = DeviceLayout(
DeviceType.TOUCH,
buttons={Touch.CONTACT: 0, Touch.LONG_PRESS: 1, Touch.TWO_FINGER_TAP: 2},
axes={Touch.PINCH: 0, Touch.ROTATE: 1, Touch.PAN_X: 2, Touch.PAN_Y: 3},
labels={
Touch.CONTACT: "Tap",
Touch.LONG_PRESS: "Long Press",
Touch.TWO_FINGER_TAP: "Two-Finger Tap",
Touch.PINCH: "Pinch",
Touch.ROTATE: "Twist",
Touch.PAN_X: "Two-Finger Pan X",
Touch.PAN_Y: "Two-Finger Pan Y",
},
)
#: A pen with its tip at 0 and its barrel buttons numbered from 1, as SDL3 and Qt report them.
PEN_LAYOUT = DeviceLayout(
DeviceType.PEN,
buttons={Pen.TIP: 0, Pen.BARREL_1: 1, Pen.BARREL_2: 2, Pen.BARREL_3: 3},
labels={
Pen.TIP: "Pen Tip",
Pen.BARREL_1: "Barrel Button 1",
Pen.BARREL_2: "Barrel Button 2",
Pen.BARREL_3: "Barrel Button 3",
},
)
#: A pen in the W3C Pointer Events numbering: the barrel is 2. The
#: specification assigns 1 to no pen button, and a second barrel reported
#: there is named barrel 2 as the likeliest reading; a player whose pen
#: disagrees overrides it.
W3C_PEN_LAYOUT = PEN_LAYOUT.moved(buttons={"barrel_1": 2, "barrel_2": 1})
#: A tablet names nothing by default: its keys are bound by index, and a
#: backend that knows the device names its rings and strips.
TABLET_LAYOUT = DeviceLayout(DeviceType.TABLET)
_G = Controller
#: A pad the backend's mapping database laid out: the standard positions, in
#: the order SDL, GLFW and the browser's standard mapping all report them.
CONTROLLER_LAYOUT = DeviceLayout(
DeviceType.CONTROLLER,
buttons={
_G.A: 0,
_G.B: 1,
_G.X: 2,
_G.Y: 3,
_G.LEFT_BUMPER: 4,
_G.RIGHT_BUMPER: 5,
_G.BACK: 6,
_G.START: 7,
_G.GUIDE: 8,
_G.LEFT_THUMB: 9,
_G.RIGHT_THUMB: 10,
_G.DPAD_UP: 11,
_G.DPAD_RIGHT: 12,
_G.DPAD_DOWN: 13,
_G.DPAD_LEFT: 14,
},
axes={_G.LEFT_X: 0, _G.LEFT_Y: 1, _G.RIGHT_X: 2, _G.RIGHT_Y: 3, _G.LEFT_TRIGGER: 4, _G.RIGHT_TRIGGER: 5},
labels={
_G.A: "A",
_G.B: "B",
_G.X: "X",
_G.Y: "Y",
_G.LEFT_BUMPER: "L1",
_G.RIGHT_BUMPER: "R1",
_G.BACK: "Select",
_G.START: "Start",
_G.GUIDE: "Guide",
_G.LEFT_THUMB: "L3",
_G.RIGHT_THUMB: "R3",
_G.DPAD_UP: "D-Pad Up",
_G.DPAD_RIGHT: "D-Pad Right",
_G.DPAD_DOWN: "D-Pad Down",
_G.DPAD_LEFT: "D-Pad Left",
_G.LEFT_X: "Left Stick X",
_G.LEFT_Y: "Left Stick Y",
_G.RIGHT_X: "Right Stick X",
_G.RIGHT_Y: "Right Stick Y",
_G.LEFT_TRIGGER: "LT",
_G.RIGHT_TRIGGER: "RT",
},
)
#: A pad no mapping database knows: its indices are the driver's, and the
#: backend names none of them.
UNRECOGNISED_CONTROLLER_LAYOUT = DeviceLayout(DeviceType.CONTROLLER)
#: The engine's default for each device type, which a backend's own replaces.
DEFAULT_LAYOUTS: dict[DeviceType, DeviceLayout] = {
DeviceType.KEYBOARD: KEYBOARD_LAYOUT,
DeviceType.MOUSE: MOUSE_LAYOUT,
DeviceType.TOUCH: TOUCH_LAYOUT,
DeviceType.PEN: PEN_LAYOUT,
DeviceType.TABLET: TABLET_LAYOUT,
DeviceType.CONTROLLER: CONTROLLER_LAYOUT,
}
[docs]
def type_layout(device_type: DeviceType, backend: Mapping[DeviceType, DeviceLayout]) -> DeviceLayout:
"""The table for *device_type*: the backend's own where it gave one, else the engine's default."""
return backend.get(device_type) or DEFAULT_LAYOUTS.get(device_type) or DeviceLayout(device_type)
[docs]
def default_raw(control: DeviceControl) -> DeviceControl | None:
"""The raw control the engine entry *control* resolves to by default, unit-free.
A raw control (an index form, or a key, which is raw by nature) is its own.
A named one resolves through the engine's own table for its device type,
which is the numbering raw indices are given in on every backend. A role
and a name the table does not place have none.
"""
bare = control.on(None)
if bare.device is DeviceType.KEYBOARD or bare.index is not None:
return bare
layout = DEFAULT_LAYOUTS.get(bare.device)
if layout is None:
return None
if isinstance(bare, DeviceButton):
index = layout.button_index(bare)
return DeviceButton(bare.device, index=index) if index is not None else None
if isinstance(bare, DeviceAxis):
whole = bare.whole
index = layout.axis_index(whole)
if index is None:
return None
raw = DeviceAxis(bare.device, index=index)
if bare.half:
raw = raw.positive if bare.half > 0 else raw.negative
return raw.delta if bare.is_delta else raw
return None
[docs]
def raw_axis(layout: DeviceLayout, index: int) -> DeviceAxis:
"""The raw control a backend's axis *index* is, as :func:`raw_button` is for a button."""
named = layout.axis_control(index)
if named.index is None:
raw = default_raw(named)
if isinstance(raw, DeviceAxis):
return raw
return DeviceAxis(layout.device, index=index)