Source code for simvx.graphics.platform._glfw

"""GLFW windowing backend."""


from __future__ import annotations

import logging
from collections.abc import Callable
from typing import Any

log = logging.getLogger(__name__)

__all__ = ["GlfwBackend"]


[docs] class GlfwBackend: """WindowBackend implementation using GLFW.""" def __init__(self) -> None: import glfw # noqa: F401 self._window: Any = None self._key_callback: Callable[[int, int, int], None] | None = None self._mouse_button_callback: Callable[[int, int, int], None] | None = None self._cursor_pos_callback: Callable[[float, float], None] | None = None self._scroll_callback: Callable[[float, float], None] | None = None self._char_callback: Callable[[int], None] | None = None self._cursors: dict[int, Any] = {}
[docs] def create_window(self, width: int, height: int, title: str, *, visible: bool = True) -> None: import glfw if not glfw.init(): raise RuntimeError("Failed to initialize GLFW") glfw.window_hint(glfw.CLIENT_API, glfw.NO_API) if not visible: glfw.window_hint(glfw.VISIBLE, glfw.FALSE) self._window = glfw.create_window(width, height, title, None, None) if not self._window: glfw.terminate() raise RuntimeError("Failed to create GLFW window") self._initial_size = (width, height)
[docs] def create_graphics_surface(self, instance: Any) -> Any: import glfw import vulkan as vk surface = vk.ffi.new("VkSurfaceKHR*") glfw.create_window_surface(instance, self._window, None, surface) return surface[0]
[docs] def get_required_instance_extensions(self) -> list[str]: import glfw return list(glfw.get_required_instance_extensions() or [])
[docs] def poll_events(self) -> None: import glfw glfw.poll_events()
[docs] def should_close(self) -> bool: import glfw return bool(glfw.window_should_close(self._window))
[docs] def get_framebuffer_size(self) -> tuple[int, int]: import glfw return glfw.get_framebuffer_size(self._window)
[docs] def get_window_size(self) -> tuple[int, int]: import glfw return glfw.get_window_size(self._window)
[docs] def set_title(self, title: str) -> None: import glfw if self._window: glfw.set_window_title(self._window, title)
[docs] def get_content_scale(self) -> tuple[float, float]: import glfw return glfw.get_window_content_scale(self._window)
[docs] def set_key_callback(self, callback: Callable[[int, int, int], None] | None) -> None: import glfw self._key_callback = callback if self._window: if callback: glfw.set_key_callback(self._window, self._glfw_key_cb) else: glfw.set_key_callback(self._window, None)
def _glfw_key_cb(self, _window: Any, key: int, scancode: int, action: int, mods: int) -> None: if self._key_callback: self._key_callback(key, action, mods)
[docs] def set_mouse_button_callback(self, callback: Callable[[int, int, int], None] | None) -> None: import glfw self._mouse_button_callback = callback if self._window: if callback: glfw.set_mouse_button_callback(self._window, self._glfw_mouse_cb) else: glfw.set_mouse_button_callback(self._window, None)
def _glfw_mouse_cb(self, _window: Any, button: int, action: int, mods: int) -> None: if self._mouse_button_callback: self._mouse_button_callback(button, action, mods)
[docs] def set_cursor_pos_callback(self, callback: Callable[[float, float], None] | None) -> None: import glfw self._cursor_pos_callback = callback if self._window: if callback: glfw.set_cursor_pos_callback(self._window, self._glfw_cursor_cb) else: glfw.set_cursor_pos_callback(self._window, None)
def _glfw_cursor_cb(self, _window: Any, x: float, y: float) -> None: if self._cursor_pos_callback: self._cursor_pos_callback(x, y)
[docs] def set_scroll_callback(self, callback: Callable[[float, float], None] | None) -> None: import glfw self._scroll_callback = callback if self._window: if callback: glfw.set_scroll_callback(self._window, self._glfw_scroll_cb) else: glfw.set_scroll_callback(self._window, None)
def _glfw_scroll_cb(self, _window: Any, x_offset: float, y_offset: float) -> None: if self._scroll_callback: self._scroll_callback(x_offset, y_offset)
[docs] def set_char_callback(self, callback: Callable[[int], None] | None) -> None: import glfw self._char_callback = callback if self._window: if callback: glfw.set_char_callback(self._window, self._glfw_char_cb) else: glfw.set_char_callback(self._window, None)
def _glfw_char_cb(self, _window: Any, codepoint: int) -> None: if self._char_callback: self._char_callback(codepoint)
[docs] def set_cursor_shape(self, shape: int) -> None: """Set cursor shape. 0=arrow, 1=ibeam, 2=crosshair, 3=hand, 4=hresize, 5=vresize.""" import glfw shape_map = { 0: glfw.ARROW_CURSOR, 1: glfw.IBEAM_CURSOR, 2: glfw.CROSSHAIR_CURSOR, 3: glfw.HAND_CURSOR, 4: glfw.HRESIZE_CURSOR, 5: glfw.VRESIZE_CURSOR, } glfw_shape = shape_map.get(shape, glfw.ARROW_CURSOR) if glfw_shape not in self._cursors: self._cursors[glfw_shape] = glfw.create_standard_cursor(glfw_shape) glfw.set_cursor(self._window, self._cursors[glfw_shape])
[docs] def get_cursor_pos(self) -> tuple[float, float]: import glfw return glfw.get_cursor_pos(self._window)
[docs] def poll_gamepads(self) -> list[tuple[int, dict[str, bool], dict[str, float]]]: """Poll all connected gamepads. Returns list of (id, buttons, axes).""" import glfw results = [] for joy_id in range(16): if not glfw.joystick_is_gamepad(joy_id): continue state = glfw.get_gamepad_state(joy_id) if state is None: continue buttons = { "a": bool(state.buttons[glfw.GAMEPAD_BUTTON_A]), "b": bool(state.buttons[glfw.GAMEPAD_BUTTON_B]), "x": bool(state.buttons[glfw.GAMEPAD_BUTTON_X]), "y": bool(state.buttons[glfw.GAMEPAD_BUTTON_Y]), "lb": bool(state.buttons[glfw.GAMEPAD_BUTTON_LEFT_BUMPER]), "rb": bool(state.buttons[glfw.GAMEPAD_BUTTON_RIGHT_BUMPER]), "back": bool(state.buttons[glfw.GAMEPAD_BUTTON_BACK]), "start": bool(state.buttons[glfw.GAMEPAD_BUTTON_START]), "guide": bool(state.buttons[glfw.GAMEPAD_BUTTON_GUIDE]), "l3": bool(state.buttons[glfw.GAMEPAD_BUTTON_LEFT_THUMB]), "r3": bool(state.buttons[glfw.GAMEPAD_BUTTON_RIGHT_THUMB]), "dpad_up": bool(state.buttons[glfw.GAMEPAD_BUTTON_DPAD_UP]), "dpad_right": bool(state.buttons[glfw.GAMEPAD_BUTTON_DPAD_RIGHT]), "dpad_down": bool(state.buttons[glfw.GAMEPAD_BUTTON_DPAD_DOWN]), "dpad_left": bool(state.buttons[glfw.GAMEPAD_BUTTON_DPAD_LEFT]), } axes = { "left_x": float(state.axes[glfw.GAMEPAD_AXIS_LEFT_X]), "left_y": float(state.axes[glfw.GAMEPAD_AXIS_LEFT_Y]), "right_x": float(state.axes[glfw.GAMEPAD_AXIS_RIGHT_X]), "right_y": float(state.axes[glfw.GAMEPAD_AXIS_RIGHT_Y]), "lt": float(state.axes[glfw.GAMEPAD_AXIS_LEFT_TRIGGER]), "rt": float(state.axes[glfw.GAMEPAD_AXIS_RIGHT_TRIGGER]), } results.append((joy_id, buttons, axes)) return results
[docs] def set_window_size(self, width: int, height: int) -> None: import glfw if self._window: glfw.set_window_size(self._window, width, height)
[docs] def request_close(self) -> None: """Signal the window to close at the next poll_events cycle.""" import glfw if self._window: glfw.set_window_should_close(self._window, True)
[docs] def set_fullscreen(self, fullscreen: bool) -> None: """Toggle fullscreen mode. Stores windowed position/size for restore.""" import glfw if not self._window: return monitor = glfw.get_primary_monitor() if fullscreen: # Store windowed pos/size for restore self._windowed_pos = glfw.get_window_pos(self._window) self._windowed_size = glfw.get_window_size(self._window) mode = glfw.get_video_mode(monitor) glfw.set_window_monitor(self._window, monitor, 0, 0, mode.size.width, mode.size.height, mode.refresh_rate) else: pos = getattr(self, '_windowed_pos', (100, 100)) size = getattr(self, '_windowed_size', self._initial_size) glfw.set_window_monitor(self._window, None, pos[0], pos[1], size[0], size[1], 0) self._is_fullscreen = fullscreen
[docs] def is_fullscreen(self) -> bool: return getattr(self, '_is_fullscreen', False)
[docs] def destroy(self) -> None: import glfw if self._window: glfw.destroy_window(self._window) glfw.terminate()