"""Adapter to bridge platform input events to simvx.core.Input singleton.
Backend-agnostic — works with GLFW, SDL3, or any backend that delivers the
same integer key codes and action constants.
"""
from __future__ import annotations
import logging
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from simvx.core import Input
log = logging.getLogger(__name__)
# Note: Cursor position callbacks deliver coordinates in logical (screen)
# pixels, not physical (framebuffer) pixels. On HiDPI displays the framebuffer
# may be larger, but mouse coordinates and tree.screen_size both use logical
# units — no DPI scaling is needed in the input layer.
# Action constants (shared by GLFW and SDL3 backends)
_PRESS = 1
_RELEASE = 0
_REPEAT = 2
# Key code → string name mapping. Integer values match both GLFW and SDL3
# backends (same mapping as web_app.py _KEY_MAP for the Pyodide path).
_KEY_MAP: dict[int, str] = {
# Letters
65: "a", 66: "b", 67: "c", 68: "d", 69: "e", 70: "f", 71: "g", 72: "h",
73: "i", 74: "j", 75: "k", 76: "l", 77: "m", 78: "n", 79: "o", 80: "p",
81: "q", 82: "r", 83: "s", 84: "t", 85: "u", 86: "v", 87: "w", 88: "x",
89: "y", 90: "z",
# Numbers
48: "0", 49: "1", 50: "2", 51: "3", 52: "4", 53: "5", 54: "6", 55: "7", 56: "8", 57: "9",
# Special keys
32: "space", 257: "enter", 256: "escape", 258: "tab", 259: "backspace",
261: "delete", 260: "insert", 268: "home", 269: "end", 266: "pageup", 267: "pagedown",
# Arrow keys
265: "up", 264: "down", 263: "left", 262: "right",
# Modifiers
340: "shift", 344: "shift", 341: "ctrl", 345: "ctrl", 342: "alt", 346: "alt",
# Punctuation / symbols
47: "/", 92: "\\", 45: "-", 61: "=", 91: "[", 93: "]",
59: ";", 39: "'", 44: ",", 46: ".", 96: "`",
# Function keys
290: "f1", 291: "f2", 292: "f3", 293: "f4", 294: "f5", 295: "f6",
296: "f7", 297: "f8", 298: "f9", 299: "f10", 300: "f11", 301: "f12",
}
[docs]
def key_callback(key: int, action: int, mods: int) -> None:
"""Process key event and update Input state.
Args:
key: Platform key code (integer, shared by GLFW/SDL3).
action: _PRESS (1), _RELEASE (0), or _REPEAT (2).
mods: Modifier bits (shift, ctrl, alt, etc.)
"""
key_name = _KEY_MAP.get(key)
if not key_name:
return
if action == _PRESS:
if not Input._keys.get(key_name):
Input._keys_just_pressed[key_name] = True
Input._keys[key_name] = True
Input._on_key(key, True)
elif action == _RELEASE:
Input._keys[key_name] = False
Input._keys_just_released[key_name] = True
Input._on_key(key, False)
@dataclass
class _UICallbacks:
char: Callable | None = None
key: Callable | None = None
mouse: Callable | None = None
motion: Callable | None = None
scroll: Callable | None = None
touch: Callable | None = None
_ui = _UICallbacks()
# Track which finger is the "primary" pointer (first finger to touch down)
_primary_finger: int | None = None
[docs]
def set_ui_callbacks(*, char=None, key=None, mouse=None, motion=None, scroll=None, touch=None):
"""Install UI routing callbacks (called by App._run_with_tree)."""
_ui.char = char
_ui.key = key
_ui.mouse = mouse
_ui.motion = motion
_ui.scroll = scroll
_ui.touch = touch
[docs]
def clear_ui_callbacks():
"""Reset all UI routing callbacks to None."""
global _primary_finger
_primary_finger = None
set_ui_callbacks()
[docs]
def reset_primary_finger():
"""Reset primary finger tracking (call on scene change)."""
global _primary_finger
_primary_finger = None
[docs]
def cursor_pos_callback(x: float, y: float) -> None:
"""Process cursor position event and update Input state."""
old = Input._mouse_pos
Input._mouse_pos = (x, y)
Input._mouse_delta = (x - old[0], y - old[1])
# Route motion to UI immediately (critical for drag responsiveness)
if _ui.motion:
_ui.motion((x, y))
[docs]
def char_callback(codepoint: int) -> None:
"""Process character input event (typed characters)."""
if _ui.char:
_ui.char(chr(codepoint))
[docs]
def key_callback_with_ui(key: int, action: int, mods: int) -> None:
"""Key callback that also routes key events to UI."""
key_callback(key, action, mods)
if _ui.key:
key_name = _KEY_MAP.get(key)
if key_name:
pressed = action in (_PRESS, _REPEAT)
_ui.key(key_name, pressed)
[docs]
def touch_callback(finger_id: int, action: int, x: float, y: float, pressure: float) -> None:
"""Process touch event — updates Input state AND routes primary finger through UI.
The primary finger (first finger to touch down) is emulated as mouse input so
all existing UI widgets work with touch out of the box. Multi-touch data still
flows through Input._touches for game code and GestureRecognizer.
Args:
finger_id: Unique finger identifier.
action: 0=down, 1=up, 2=move.
x, y: Position in window pixel coordinates.
pressure: Touch pressure 0.0-1.0.
"""
global _primary_finger
# Always store raw touch for game code / GestureRecognizer
Input._update_touch(finger_id, action, x, y, pressure)
# Route multi-touch to UI (for controls with touch_mode="multi")
if _ui.touch:
_ui.touch(finger_id, action, x, y)
# Primary finger emulates mouse for standard UI interaction
if action == 0: # down
if _primary_finger is None:
_primary_finger = finger_id
Input._mouse_pos = (x, y)
Input._mouse_delta = (0.0, 0.0)
if _ui.motion:
_ui.motion((x, y))
if _ui.mouse:
_ui.mouse(1, True) # button=1 (left), pressed=True
elif action == 1: # up
if finger_id == _primary_finger:
_primary_finger = None
Input._mouse_pos = (x, y)
if _ui.motion:
_ui.motion((x, y))
if _ui.mouse:
_ui.mouse(1, False) # left-button release
elif action == 2: # move
if finger_id == _primary_finger:
old = Input._mouse_pos
Input._mouse_pos = (x, y)
Input._mouse_delta = (x - old[0], y - old[1])
if _ui.motion:
_ui.motion((x, y))
[docs]
def poll_gamepads(window_backend: Any) -> None:
"""Poll gamepads via the window backend and update Input state."""
if not hasattr(window_backend, "poll_gamepads"):
return
for pad_id, buttons, axes in window_backend.poll_gamepads():
Input._update_gamepad(pad_id, buttons, axes)
__all__ = [
"key_callback",
"mouse_button_callback",
"cursor_pos_callback",
"scroll_callback",
"char_callback",
"poll_gamepads",
"key_callback_with_ui",
"mouse_button_callback_with_ui",
"set_ui_callbacks",
"clear_ui_callbacks",
"reset_primary_finger",
]