from pathlib import Path
from typing import Any
import pygame
import pygame._sdl2
import pygame.display
import pygame.event
from moderngl_window.context.base import BaseWindow
from moderngl_window.context.pygame2.keys import Keys
[docs]
class Window(BaseWindow):
"""
Basic window implementation using pygame2.
"""
#: Name of the window
name = "pygame2"
#: pygame specific key constants
keys = Keys
_mouse_button_map = {
1: 1,
3: 2,
2: 3,
}
def __init__(self, **kwargs: Any):
super().__init__(**kwargs)
pygame.display.init()
pygame.display.gl_set_attribute(pygame.GL_CONTEXT_MAJOR_VERSION, self.gl_version[0])
pygame.display.gl_set_attribute(pygame.GL_CONTEXT_MINOR_VERSION, self.gl_version[1])
pygame.display.gl_set_attribute(
pygame.GL_CONTEXT_PROFILE_MASK, pygame.GL_CONTEXT_PROFILE_CORE
)
pygame.display.gl_set_attribute(pygame.GL_CONTEXT_FORWARD_COMPATIBLE_FLAG, 1)
pygame.display.gl_set_attribute(pygame.GL_DOUBLEBUFFER, 1)
pygame.display.gl_set_attribute(pygame.GL_DEPTH_SIZE, 24)
pygame.display.gl_set_attribute(pygame.GL_STENCIL_SIZE, 8)
if self.samples > 1:
pygame.display.gl_set_attribute(pygame.GL_MULTISAMPLEBUFFERS, 1)
pygame.display.gl_set_attribute(pygame.GL_MULTISAMPLESAMPLES, self.samples)
self._depth = 24
self._flags = pygame.OPENGL | pygame.DOUBLEBUF
if self.resizable:
self._flags |= pygame.RESIZABLE
if not self._visible:
self._flags |= pygame.HIDDEN
self._set_mode()
self.title = self._title
self.cursor = self._cursor
# Get the reference for the internal sdl2 window
# Makes us able to control window position and other properties.
self._sdl_window = pygame._sdl2.video.Window.from_display_module()
if self.fullscreen:
self._set_fullscreen(True)
self.init_mgl_context()
self.set_default_viewport()
def _set_mode(self) -> None:
self._surface = pygame.display.set_mode(
size=(self._width, self._height),
flags=self._flags,
depth=self._depth,
vsync=self._vsync,
)
def _set_fullscreen(self, value: bool) -> None:
if value:
self._sdl_window.set_fullscreen(True)
else:
self._sdl_window.set_windowed()
def _set_vsync(self, value: bool) -> None:
self._vsync = value
self._set_mode()
@property
def size(self) -> tuple[int, int]:
"""tuple[int, int]: current window size.
This property also support assignment::
# Resize the window to 1000 x 1000
window.size = 1000, 1000
"""
return self._width, self._height
@size.setter
def size(self, value: tuple[int, int]) -> None:
self._width, self._height = value
self._set_mode()
self.resize(value[0], value[1])
@property
def position(self) -> tuple[int, int]:
"""tuple[int, int]: The current window position.
This property can also be set to move the window::
# Move window to 100, 100
window.position = 100, 100
"""
return self._sdl_window.position
@position.setter
def position(self, value: tuple[int, int]) -> None:
self._sdl_window.position = value
@property
def visible(self) -> bool:
"""bool: Is the window visible?
This property can also be set::
# Hide or show the window
window.visible = False
"""
return self._visible
@visible.setter
def visible(self, value: bool) -> None:
self._visible = value
if value:
self._sdl_window.show()
else:
self._sdl_window.hide()
@property
def cursor(self) -> bool:
"""bool: Should the mouse cursor be visible inside the window?
This property can also be assigned to::
# Disable cursor
window.cursor = False
"""
return self._cursor
@cursor.setter
def cursor(self, value: bool) -> None:
pygame.mouse.set_visible(value)
self._cursor = value
@property
def mouse_exclusivity(self) -> bool:
"""bool: If mouse exclusivity is enabled.
When you enable mouse-exclusive mode, the mouse cursor is no longer
available. It is not merely hidden – no amount of mouse movement
will make it leave your application. This is for example useful
when you don't want the mouse leaving the screen when rotating
a 3d scene.
This property can also be set::
window.mouse_exclusivity = True
"""
return self._mouse_exclusivity
@mouse_exclusivity.setter
def mouse_exclusivity(self, value: bool) -> None:
if self._cursor:
self.cursor = False
pygame.event.set_grab(value)
self._mouse_exclusivity = value
@property
def title(self) -> str:
"""str: Window title.
This property can also be set::
window.title = "New Title"
"""
return self._title
@title.setter
def title(self, value: str) -> None:
pygame.display.set_caption(value)
self._title = value
[docs]
def swap_buffers(self) -> None:
"""Swap buffers, set viewport, trigger events and increment frame counter"""
pygame.display.flip()
self.set_default_viewport()
self.process_events()
self._frames += 1
def _set_icon(self, icon_path: Path) -> None:
icon = pygame.image.load(icon_path)
pygame.display.set_icon(icon)
[docs]
def resize(self, width: int, height: int) -> None:
"""Resize callback
Args:
width: New window width
height: New window height
"""
self._width = width
self._height = height
self._buffer_width, self._buffer_height = self._width, self._height
self.set_default_viewport()
super().resize(self._buffer_width, self._buffer_height)
[docs]
def close(self) -> None:
"""Close the window"""
super().close()
self._close_func()
def _handle_mods(self) -> None:
"""Update key mods"""
mods = pygame.key.get_mods()
self._modifiers.shift = mods & pygame.KMOD_SHIFT
self._modifiers.ctrl = mods & pygame.KMOD_CTRL
self._modifiers.alt = mods & pygame.KMOD_ALT
[docs]
def process_events(self) -> None:
"""Handle all queued events in pygame2 dispatching events to standard methods"""
for event in pygame.event.get():
if event.type == pygame.MOUSEMOTION:
self._handle_mods()
if self.mouse_states.any:
self._mouse_drag_event_func(
event.pos[0],
event.pos[1],
event.rel[0],
event.rel[1],
)
else:
self._mouse_position_event_func(
event.pos[0],
event.pos[1],
event.rel[0],
event.rel[1],
)
elif event.type == pygame.MOUSEBUTTONDOWN:
self._handle_mods()
button = self._mouse_button_map.get(event.button, None)
if button is not None:
self._handle_mouse_button_state_change(button, True)
self._mouse_press_event_func(
event.pos[0],
event.pos[1],
button,
)
elif event.type == pygame.MOUSEBUTTONUP:
self._handle_mods()
button = self._mouse_button_map.get(event.button, None)
if button is not None:
self._handle_mouse_button_state_change(button, False)
self._mouse_release_event_func(
event.pos[0],
event.pos[1],
button,
)
elif event.type in [pygame.KEYDOWN, pygame.KEYUP]:
self._handle_mods()
if self._exit_key is not None and event.key == self._exit_key:
self.close()
# Pygame can't do fullscreen yet, but this would toggle it.
if (
event.type == pygame.KEYUP
and self._fs_key is not None
and event.key == self._fs_key
):
self.fullscreen = not self.fullscreen
if event.type == pygame.KEYDOWN:
self._key_pressed_map[event.key] = True
elif event.type == pygame.KEYUP:
self._key_pressed_map[event.key] = False
self._key_event_func(event.key, event.type, self._modifiers)
elif event.type == pygame.TEXTINPUT:
self._handle_mods()
self._unicode_char_entered_func(event.text)
elif event.type == pygame.MOUSEWHEEL:
self._handle_mods()
self._mouse_scroll_event_func(float(event.x), float(event.y))
elif event.type == pygame.QUIT:
self.close()
elif event.type == pygame.VIDEORESIZE:
self.resize(event.size[0], event.size[1])
elif event.type == pygame.ACTIVEEVENT:
# # We might support these in the future
# Mouse cursor state
# if event.state == 0:
# if event.gain:
# print("Mouse enters viewport")
# else:
# print("Mouse leaves viewport")
# Window focus state
# if event.state == 1:
# if event.gain:
# print("Window gained focus")
# else:
# print("Window lost focus")
# Window iconify state
if getattr(event, "state", None) == 2:
if event.gain:
self._visible = True
self._iconify_func(False)
else:
self._visible = False
self._iconify_func(True)
# This is also a problem on linux, but is too disruptive during resize events
# elif event.type == pygame.VIDEOEXPOSE:
# # On OS X we only get VIDEOEXPOSE when restoring the windoe
# self._iconify_func(False)
elif event.type == pygame.USEREVENT:
self._on_generic_event_func(event)
[docs]
def destroy(self) -> None:
"""Gracefully close the window"""
pygame.quit()