Source code for simvx.graphics.platform._qt

"""PySide6 windowing backend."""


from __future__ import annotations

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

import vulkan as vk

log = logging.getLogger(__name__)

__all__ = ["QtBackend"]


[docs] class QtBackend: """WindowBackend implementation using PySide6.""" def __init__(self) -> None: from PySide6.QtCore import Qt from PySide6.QtGui import QSurfaceFormat, QWindow from PySide6.QtWidgets import QApplication self._window: QWindow | None = None self._closed: bool = False self._mouse_button_callback: Callable[[int, int, int], None] | None = None self._QWindow = QWindow self._QApplication = QApplication self._QSurfaceFormat = QSurfaceFormat self._Qt = Qt # Ensure Qt application exists if not QApplication.instance(): QApplication([])
[docs] def create_window(self, width: int, height: int, title: str) -> None: """Create a QWindow with Vulkan surface.""" fmt = self._QSurfaceFormat() # Note: setRenderingAPI may not be available in all PySide6 versions if hasattr(fmt, "setRenderingAPI"): fmt.setRenderingAPI(self._QSurfaceFormat.RenderingAPI.OpenGL) self._QSurfaceFormat.setDefaultFormat(fmt) self._window = self._QWindow() self._window.setTitle(title) self._window.setGeometry(100, 100, width, height) # Set surface type for Vulkan if hasattr(self._QWindow, "VulkanSurface"): self._window.setSurfaceType(self._QWindow.VulkanSurface) self._window.show() # Connect close event if hasattr(self._window, "destroyed"): self._window.destroyed.connect(self._on_window_closed)
def _on_window_closed(self) -> None: """Handle window close event.""" self._closed = True
[docs] def create_graphics_surface(self, instance: Any) -> Any: """Create a Vulkan surface from the QWindow.""" if not self._window: raise RuntimeError("Window not created") native_handle = self._window.winId() system = platform.system() if system == "Linux": # Linux: use XCB or Xlib try: # Try XCB first import xcb # noqa: F401 conn = xcb.xcb.connect() surface_info = vk.VkXcbSurfaceCreateInfoKHR( connection=conn.connection, window=native_handle, ) fn = vk.vkGetInstanceProcAddr(instance, "vkCreateXcbSurfaceKHR") if fn: surface = vk.ffi.new("VkSurfaceKHR*") vk.vkCreateXcbSurfaceKHR(instance, surface_info, None, surface) return surface[0] except (ImportError, Exception) as e: log.debug("XCB surface creation failed: %s", e) # Xlib fallback try: surface_info = vk.VkXlibSurfaceCreateInfoKHR( dpy=vk.ffi.NULL, window=native_handle, ) fn = vk.vkGetInstanceProcAddr(instance, "vkCreateXlibSurfaceKHR") if fn: surface = vk.ffi.new("VkSurfaceKHR*") vk.vkCreateXlibSurfaceKHR(instance, surface_info, None, surface) return surface[0] except Exception as e: log.debug("Xlib surface creation failed: %s", e) elif system == "Windows": # Windows: use Win32 try: surface_info = vk.VkWin32SurfaceCreateInfoKHR( hinstance=vk.ffi.NULL, hwnd=native_handle, ) surface = vk.ffi.new("VkSurfaceKHR*") vk.vkCreateWin32SurfaceKHR(instance, surface_info, None, surface) return surface[0] except Exception as e: log.debug("Win32 surface creation failed: %s", e) elif system == "Darwin": # macOS: use Metal try: surface_info = vk.VkMetalSurfaceCreateInfoEXT( pLayer=vk.ffi.NULL, ) surface = vk.ffi.new("VkSurfaceKHR*") vk.vkCreateMetalSurfaceEXT(instance, surface_info, None, surface) return surface[0] except Exception as e: log.debug("Metal surface creation failed: %s", e) raise RuntimeError(f"Unsupported platform for Vulkan surface creation: {system}")
[docs] def get_required_instance_extensions(self) -> list[str]: """Return platform-specific Vulkan extensions needed.""" system = platform.system() extensions = [ "VK_KHR_surface", ] if system == "Linux": extensions.append("VK_KHR_xcb_surface") extensions.append("VK_KHR_xlib_surface") elif system == "Windows": extensions.append("VK_KHR_win32_surface") elif system == "Darwin": extensions.append("VK_EXT_metal_surface") return extensions
[docs] def poll_events(self) -> None: """Process Qt events.""" from PySide6.QtWidgets import QApplication app = QApplication.instance() if app: app.processEvents()
[docs] def should_close(self) -> bool: """Check if window should close.""" return self._closed or self._window is None or not self._window.isVisible()
[docs] def get_framebuffer_size(self) -> tuple[int, int]: """Get window framebuffer size.""" if not self._window: return (0, 0) return (self._window.width(), self._window.height())
[docs] def set_mouse_button_callback(self, callback: Callable[[int, int, int], None] | None) -> None: """Register mouse button callback.""" self._mouse_button_callback = callback if self._window and callback: self._window.mousePressEvent = self._make_mouse_press_handler()
def _make_mouse_press_handler(self) -> Any: """Create a mouse press event handler.""" def handler(event: Any) -> None: if self._mouse_button_callback: button = 0 # Left button if hasattr(event, "button"): from PySide6.QtCore import Qt if event.button() == Qt.MouseButton.LeftButton: button = 0 elif event.button() == Qt.MouseButton.RightButton: button = 1 elif event.button() == Qt.MouseButton.MiddleButton: button = 2 action = 1 # Press (Qt has different values but we simplify) mods = 0 self._mouse_button_callback(button, action, mods) return handler
[docs] def get_cursor_pos(self) -> tuple[float, float]: """Get current cursor position.""" if not self._window: return (0.0, 0.0) from PySide6.QtGui import QCursor pos = QCursor.pos() if self._window: local_pos = self._window.mapFromGlobal(pos) return (float(local_pos.x()), float(local_pos.y())) return (float(pos.x()), float(pos.y()))
[docs] def set_window_size(self, width: int, height: int) -> None: """Resize the window.""" if self._window: self._window.resize(width, height)
[docs] def get_window_size(self) -> tuple[int, int]: """Get window size (may differ from framebuffer size on HiDPI).""" if not self._window: return (0, 0) return (self._window.width(), self._window.height())
[docs] def get_content_scale(self) -> tuple[float, float]: if not self._window: return (1.0, 1.0) ratio = self._window.devicePixelRatio() return (ratio, ratio)
[docs] def set_title(self, title: str) -> None: """Update the window title bar.""" if self._window: self._window.setWindowTitle(title)
[docs] def set_key_callback(self, callback: Callable[[int, int, int], None] | None) -> None: """Register keyboard callback (key, action, mods).""" self._key_callback = callback
[docs] def set_cursor_pos_callback(self, callback: Callable[[float, float], None] | None) -> None: """Register cursor position callback.""" self._cursor_pos_callback = callback
[docs] def set_char_callback(self, callback: Callable[[int], None] | None) -> None: """Register character input callback.""" self._char_callback = callback
[docs] def set_cursor_shape(self, shape: int) -> None: """Set cursor shape. 0=arrow, 1=ibeam, 2=crosshair, 3=hand, 4=hresize, 5=vresize.""" if not self._window: return from PySide6.QtCore import Qt from PySide6.QtGui import QCursor shape_map = { 0: Qt.CursorShape.ArrowCursor, 1: Qt.CursorShape.IBeamCursor, 2: Qt.CursorShape.CrossCursor, 3: Qt.CursorShape.PointingHandCursor, 4: Qt.CursorShape.SizeHorCursor, 5: Qt.CursorShape.SizeVerCursor, } qt_shape = shape_map.get(shape, Qt.CursorShape.ArrowCursor) self._window.setCursor(QCursor(qt_shape))
[docs] def request_close(self) -> None: """Request the window to close.""" self._closed = True if self._window: self._window.close()
[docs] def poll_gamepads(self) -> list[tuple[int, dict[str, bool], dict[str, float]]]: """Poll gamepads — not supported in Qt backend.""" return []
[docs] def destroy(self) -> None: """Clean up Qt resources.""" if self._window: self._window.close() self._window = None