"""Pyglet display backend implementation."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
import pyglet
from .base import BaseDisplay
if TYPE_CHECKING:
from pathlib import Path
[docs]
class PygletDisplay(BaseDisplay):
"""Pyglet implementation of display window management.
Manages pyglet Window creation, event handling, and drawing operations.
Provides both direct access to pyglet.window.Window and backend-agnostic helpers.
"""
[docs]
def __init__(self, settings: object, shutdown_handler: object = None) -> None:
"""Initialize pyglet display.
Args:
settings: Settings object with display configuration
shutdown_handler: Callable for graceful shutdown (optional)
"""
super().__init__(settings, shutdown_handler=shutdown_handler)
self._event_queue = []
self._setup_event_handlers()
@property
def backend_name(self) -> str:
"""Get backend identifier."""
return "pyglet"
def _create_window(self, settings: object) -> pyglet.window.Window: # noqa: PLR6301
"""Create pyglet window.
Args:
settings: Settings object with SCREEN_RES, FULLSCREEN, DISPLAY_INDEX
Returns:
pyglet.window.Window object
"""
display = pyglet.display.get_display()
screens = display.get_screens()
screen = screens[0] if len(screens) <= settings.display_index else screens[settings.display_index]
window = pyglet.window.Window(
width=settings.screen_res[0],
height=settings.screen_res[1],
fullscreen=settings.fullscreen,
screen=screen,
caption="PyELink - Pyglet Backend",
)
return window
def _setup_event_handlers(self) -> None:
"""Setup pyglet event handlers to capture events."""
@self._window.event
def on_key_press(symbol: int, modifiers: int) -> None:
"""Handle key press events."""
# Check for Ctrl+C (when window has focus, SIGINT won't fire)
if symbol == pyglet.window.key.C and (modifiers & pyglet.window.key.MOD_CTRL):
# Call shutdown handler directly
if self.shutdown_handler:
self.shutdown_handler(None, None)
return
key_name = pyglet.window.key.symbol_string(symbol).lower().replace("_", "")
self._event_queue.append({
"type": "keydown",
"key": key_name,
"unicode": chr(symbol) if 32 <= symbol < 127 else "",
"mod": modifiers,
})
@self._window.event
def on_key_release(symbol: int, modifiers: int) -> None:
"""Handle key release events."""
key_name = pyglet.window.key.symbol_string(symbol).lower().replace("_", "")
self._event_queue.append({"type": "keyup", "key": key_name, "mod": modifiers})
@self._window.event
def on_mouse_press(x: int, y: int, button: int, modifiers: int) -> None: # noqa: ARG001
"""Handle mouse button press events."""
self._event_queue.append({"type": "mousebuttondown", "pos": (x, y), "button": button})
@self._window.event
def on_mouse_release(x: int, y: int, button: int, modifiers: int) -> None: # noqa: ARG001
"""Handle mouse button release events."""
self._event_queue.append({"type": "mousebuttonup", "pos": (x, y), "button": button})
@self._window.event
def on_close() -> None:
"""Handle window close event."""
self._event_queue.append({"type": "quit"})
[docs]
def flip(self) -> None:
"""Update display."""
self._window.flip()
[docs]
def close(self) -> None:
"""Close pyglet window."""
# Exit fullscreen mode first
if self._window.fullscreen:
self._window.set_fullscreen(False)
self._window.close()
[docs]
def get_events(self) -> list[dict[str, Any]]:
"""Get pyglet events as unified dicts.
Returns:
List of event dicts with unified keys
"""
self._window.dispatch_events()
events = self._event_queue.copy()
self._event_queue.clear()
return events
[docs]
def fill(self, color: tuple[int, int, int]) -> None:
"""Fill window with color.
Args:
color: RGB tuple (0-255, 0-255, 0-255)
"""
self._window.clear()
pyglet.gl.glClearColor(color[0] / 255.0, color[1] / 255.0, color[2] / 255.0, 1.0)
pyglet.gl.glClear(pyglet.gl.GL_COLOR_BUFFER_BIT)
[docs]
def clear(self) -> None:
"""Clear window to black."""
self._window.clear()
[docs]
def get_size(self) -> tuple[int, int]:
"""Get window dimensions.
Returns:
(width, height) in pixels
"""
return (self._window.width, self._window.height)
[docs]
def draw_text(
self,
text: str,
pos: tuple[int, int] | None = None,
center: bool = False,
size: int = 24,
color: tuple[int, int, int] = (255, 255, 255),
) -> None:
"""Draw text on window.
Args:
text: Text string to display
pos: (x, y) position in pixels
center: If True, center text on screen
size: Font size in points
color: RGB color tuple
"""
if center:
text_pos = (self._window.width // 2, self._window.height // 2)
anchor_x = "center"
anchor_y = "center"
else:
text_pos = pos
anchor_x = "left"
anchor_y = "bottom"
label = pyglet.text.Label(
text,
font_name="Arial",
font_size=size,
x=text_pos[0],
y=text_pos[1],
anchor_x=anchor_x,
anchor_y=anchor_y,
color=(*color, 255),
)
label.draw()
[docs]
def draw_image(
self,
image_path: str | Path,
pos: tuple[int, int] | None = None,
center: bool = False,
scale: float | None = None,
) -> None:
"""Draw image on window.
Args:
image_path: Path to image file
pos: (x, y) position in pixels
center: If True, center image on screen
scale: Scale factor for image size
"""
image = pyglet.image.load(str(image_path))
if scale is not None:
image.width = int(image.width * scale)
image.height = int(image.height * scale)
if center:
image.anchor_x = image.width // 2
image.anchor_y = image.height // 2
sprite_pos = (self._window.width // 2, self._window.height // 2)
else:
sprite_pos = pos
sprite = pyglet.sprite.Sprite(image, x=sprite_pos[0], y=sprite_pos[1])
sprite.draw()