"""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)
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)
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()