Source code for rubato.utils.hardware.rb_input

"""
An abstraction for checking for hardware input from a user.
"""
import ctypes
import math
import sdl2
from ctypes import c_char_p, c_float, c_int

from . import Display
from .. import Vector, Math, InitError


# THIS IS A STATIC CLASS
[docs]class Input: """ The input class, handling keyboard, mouse, and controller functionality. Go :doc:`here <key-names>` for a list of all the available keys. """ def __init__(self) -> None: raise InitError(self) # CONTROLLER METHODS _controllers: dict[int, sdl2.SDL_Joystick] = {} _joystick_max: int = 32768
[docs] @classmethod def controllers(cls) -> list[int]: """ A list of the controllers currently registered. If non-zero, the controllers are registered from 0 to n-1 where n is the number of controllers. This number index is passed to events that are propagated when controllers are inputted to. Returns: A list of the integer indexes of the controllers currently registered. """ return list(cls._controllers.keys())
[docs] @classmethod def controller_name(cls, controller: int) -> str: """ Get the name of the controller at the given index. Args: index: The index of the controller to get the name of. Raises: IndexError: The controller for the given index is not registered. Returns: The name of the controller. """ if controller not in cls._controllers: raise IndexError(f"Controller {controller} is not registered.") return sdl2.SDL_JoystickNameForIndex(controller)
[docs] @classmethod def controller_axis(cls, controller: int, axis: int) -> float: """ Get the value of a given joystick axis on a controller. Args: controller: The index of the controller. axis: The index of the joystick axis. Raises: IndexError: The controller for the given index is not registered. Returns: The value of the axis. """ if controller not in cls._controllers: raise IndexError(f"Controller {controller} is not registered.") return sdl2.SDL_JoystickGetAxis(cls._controllers[controller], axis) / cls._joystick_max
[docs] @classmethod def axis_centered(cls, val: float) -> bool: """ Check whether a given axis value is within the +/-10% bounds of deadzone considered the "center". Args: val: The value of the axis. Returns: Whether the axis is centered. """ return -0.1 < val < 0.1
[docs] @classmethod def controller_button(cls, controller: int, button: int) -> bool: """ Check whether a given button on a controller is pressed. Args: controller: The index of the controller. button: The index of the button. Raises: IndexError: The controller for the given index is not registered. Returns: Whether the button is pressed. """ if controller not in cls._controllers: raise IndexError(f"Controller {controller} is not registered.") return sdl2.SDL_JoystickGetButton(cls._controllers[controller], button) == 1
[docs] @classmethod def controller_hat(cls, controller: int, hat: int) -> int: """ Get the value of a given hat on a controller. Args: controller: The index of the controller. hat: The index of the hat. Raises: IndexError: The controller for the given index is not registered. Returns: int: The value of the hat, which you can translate with `translate_hat()`. """ if controller not in cls._controllers: raise IndexError(f"Controller {controller} is not registered.") return sdl2.SDL_JoystickGetHat(cls._controllers[controller], hat)
[docs] @classmethod def translate_hat(cls, val: int) -> str: """ Translate a hat value to a string. Args: val: The hat value. Returns: str: The string representation of the hat value. """ if val == sdl2.SDL_HAT_CENTERED: return "center" elif val == sdl2.SDL_HAT_UP: return "up" elif val == sdl2.SDL_HAT_RIGHT: return "right" elif val == sdl2.SDL_HAT_DOWN: return "down" elif val == sdl2.SDL_HAT_LEFT: return "left" elif val == sdl2.SDL_HAT_RIGHTUP: return "right up" elif val == sdl2.SDL_HAT_RIGHTDOWN: return "right down" elif val == sdl2.SDL_HAT_LEFTUP: return "left up" elif val == sdl2.SDL_HAT_LEFTDOWN: return "left down" return "unknown"
# KEYBOARD METHODS _mods: dict[str, int] = { "shift": sdl2.KMOD_SHIFT, "left shift": sdl2.KMOD_LSHIFT, "right shift": sdl2.KMOD_RSHIFT, "alt": sdl2.KMOD_ALT, "left alt": sdl2.KMOD_LALT, "right alt": sdl2.KMOD_RALT, "ctrl": sdl2.KMOD_CTRL, "left ctrl": sdl2.KMOD_LCTRL, "right ctrl": sdl2.KMOD_RCTRL, "gui": sdl2.KMOD_GUI, "left gui": sdl2.KMOD_LGUI, "right gui": sdl2.KMOD_RGUI, "numlock": sdl2.KMOD_NUM, "caps lock": sdl2.KMOD_CAPS, "altgr": sdl2.KMOD_MODE, }
[docs] @classmethod def key_pressed(cls, *keys: str) -> bool: """ Checks if keys are pressed. Case insensitive. Args: *keys: The names of the keys to check. Returns: bool: Whether the keys are pressed. Example: .. code-block:: python if rb.Input.key_pressed("a"): # handle the "a" keypress if rb.Input.key_pressed("shift", "w"): # handle the "shift+w" keypress """ state = cls.get_keyboard_state() for key in keys: key = key.lower() if key in cls._mods and len(keys) > 1: if not sdl2.SDL_GetModState() & cls._mods[key]: return False else: if key == "shift": key1, key2 = "left shift", "right shift" elif key == "ctrl": key1, key2 = "left ctrl", "right ctrl" elif key == "alt": key1, key2 = "left alt", "right alt" elif key == "gui": key1, key2 = "left gui", "right gui" else: key1, key2 = key, key if not (state[cls.scancode_from_name(key1)] or state[cls.scancode_from_name(key2)]): return False return True
[docs] @classmethod def get_keyboard_state(cls): """Returns a list with the current SDL keyboard state.""" numkeys = ctypes.c_int() keystate = sdl2.SDL_GetKeyboardState(ctypes.byref(numkeys)) ptr_t = ctypes.POINTER(ctypes.c_uint8 * numkeys.value) return ctypes.cast(keystate, ptr_t)[0]
[docs] @classmethod def get_name(cls, code: int) -> str: """ Gets the name of a key from its keycode. Args: code: A keycode. Returns: str: The corresponding key. """ return sdl2.SDL_GetKeyName(code).decode("utf-8").lower()
[docs] @classmethod def mods_from_code(cls, code: int) -> list[str]: """ Gets the modifier names from a mod code. Args: code: The mod code. Returns: list[str]: A list with the names of the currently pressed modifiers. """ return [name for name, val in cls._mods.items() if code & val]
[docs] @classmethod def key_from_name(cls, char: str) -> int: """ Gets the keycode of a key from its name. Args: char: The name of the key. Returns: int: The corresponding keycode. """ return sdl2.SDL_GetKeyFromName(c_char_p(bytes(char, "utf-8")))
[docs] @classmethod def scancode_from_name(cls, char: str) -> int: """ Gets the scancode of a key from its name. Args: char: The name of the key. Returns: int: The corresponding scancode. """ return sdl2.SDL_GetScancodeFromName(c_char_p(bytes(char, "utf-8")))
[docs] @classmethod def window_focused(cls) -> bool: """ Checks if the display has keyboard focus. Returns: bool: True if the window is focused, false otherwise. """ return sdl2.SDL_GetKeyboardFocus() == Display.window or sdl2.SDL_GetMouseFocus() == Display.window
# MOUSE FUNCTIONS
[docs] @classmethod def mouse_state(cls) -> tuple[bool, bool, bool, bool, bool]: """ Checks which mouse buttons are pressed. Returns: A tuple with 5 booleans representing the state of each mouse button. (button1, button2, button3, button4, button5) """ info = sdl2.SDL_GetMouseState(ctypes.byref(c_int(0)), ctypes.byref(c_int(0))) return ( (info & sdl2.SDL_BUTTON_LMASK) != 0, (info & sdl2.SDL_BUTTON_MMASK) != 0, (info & sdl2.SDL_BUTTON_RMASK) != 0, (info & sdl2.SDL_BUTTON_X1MASK) != 0, (info & sdl2.SDL_BUTTON_X2MASK) != 0, )
[docs] @classmethod def mouse_pressed(cls) -> bool: """ Checks if any mouse button is pressed. Returns: True if any button is pressed, false otherwise. """ return any(cls.mouse_state())
@staticmethod def _display_to_screen(x: float, y: float) -> tuple[float, float]: """Converts display coordinates to screen coordinates.""" x_window, y_window = c_int(round(x)), c_int(round(y)) x_render, y_render = c_float(0), c_float(0) size = Display.border_size if Display.has_x_border(): x_window.value = math.floor(Math.clamp(x_window.value, size, Display.window_size.x - size)) elif Display.has_y_border(): y_window.value = math.floor(Math.clamp(y_window.value, size, Display.window_size.y - size)) sdl2.SDL_RenderWindowToLogical(Display.renderer.sdlrenderer, x_window, y_window, x_render, y_render) return x_render.value, y_render.value
[docs] @classmethod def get_mouse_pos(cls) -> Vector: """ The current position of the mouse, in screen-coordinates. Returns: A Vector representing position. """ return Vector.create(Display._sdl_to_cartesian(cls._display_to_screen(*cls.get_mouse_abs_pos())))
[docs] @staticmethod def get_mouse_abs_pos() -> Vector: """ The current absolute position of the mouse, in display coordinates. Returns: A Vector representing position. """ x_window, y_window = c_int(0), c_int(0) sdl2.SDL_GetMouseState(ctypes.byref(x_window), ctypes.byref(y_window)) return Vector(x_window.value, y_window.value)
[docs] @staticmethod def set_mouse_pos(v: Vector | tuple[float, float]): """ Sets the position of the mouse. Args: v: The position to set the mouse to. """ sdl2.SDL_WarpMouseInWindow(Display.window.window, c_int(round(v[0])), c_int(round(v[1])))
[docs] @classmethod def mouse_is_visible(cls) -> bool: """ Checks if the mouse is currently visible. Returns: bool: True for visible, false otherwise. """ return sdl2.SDL_ShowCursor(sdl2.SDL_QUERY) == sdl2.SDL_ENABLE
[docs] @classmethod def set_mouse_visibility(cls, toggle: bool): """ Sets the mouse visibility. Args: toggle: True to show the mouse and false to hide the mouse. """ sdl2.SDL_ShowCursor(sdl2.SDL_ENABLE if toggle else sdl2.SDL_DISABLE)
[docs] @staticmethod def pt_in_poly(pt: Vector | tuple[float, float], verts: list[Vector] | list[tuple[float, float]]) -> bool: """ Checks if a point is inside a polygon. Args: pt: The point to check. verts: The polygon representation as a list of Vector | tuple[float, float]s (vertices) Returns: bool: Whether the point is inside the polygon. """ last, now, odd = verts[-1], verts[0], False for now in verts: if ((now[1] > pt[1]) != (last[1] > pt[1])) and \ (pt[0] < (last[0] - now[0]) * (pt[1] - now[1]) / (last[1] - now[1]) + now[0]): odd = not odd last = now return odd
[docs] @classmethod def mouse_in( cls, center: Vector | tuple[float, float], dims: Vector | tuple[float, float] = (1, 1), angle: float = 0 ) -> bool: """ Checks if the mouse is inside a rectangle defined by its center and dimensions Args: center: The center of the rectangle. dims: The dimensions of the rectangle. Defaults to (1, 1). angle: The angle of the rectangle in degrees. Defaults to 0. Returns: bool: Whether the mouse is in the defined rectangle. """ center = Vector.create(center) dims = Vector.create(dims) mo = Input.get_mouse_pos() # mouse if angle == 0: lt = (center - dims / 2).ceil() # left top rb = (center + dims / 2).ceil() # right bottom return lt.x <= mo.x <= rb.x and lt.y <= mo.y <= rb.y else: lt = (-dims / 2).rotate(angle) + center # left top # pylint: disable=invalid-unary-operand-type rt = (Vector(dims.x, -dims.y) / 2).rotate(angle) + center # right top rb = (dims / 2).rotate(angle) + center # right bottom lb = (Vector(-dims.x, dims.y) / 2).rotate(angle) + center # left bottom return ( cls._is_left(lt, rt, mo) and cls._is_left(rt, rb, mo) and cls._is_left(rb, lb, mo) and cls._is_left(lb, lt, mo) )
@staticmethod def _is_left(p0: Vector, p1: Vector, p2: Vector) -> bool: # not sure what this does but I got it from: # https://gamedev.stackexchange.com/a/110233 return ((p1.x - p0.x) * (p2.y - p0.y) - (p2.x - p0.x) * (p1.y - p0.y)) > 0