Source code for moderngl_window

"""
General helper functions aiding in the boostrapping of this library.
"""

import argparse
import logging
import os
import sys
import time
import weakref
from pathlib import Path
from typing import Any, Optional

import moderngl

from moderngl_window.conf import settings
from moderngl_window.context.base import BaseWindow, WindowConfig
from moderngl_window.timers.clock import Timer
from moderngl_window.utils.keymaps import AZERTY, QWERTY, KeyMap, KeyMapFactory  # noqa
from moderngl_window.utils.module_loading import import_string

__version__ = "3.1.1"

IGNORE_DIRS = [
    "__pycache__",
    "base",
]

# Add new windows classes here to be recognized by the command line option --window
WINDOW_CLASSES = [
    "glfw",
    "headless",
    "pygame2",
    "pyglet",
    "pyqt5",
    "pyside2",
    "sdl2",
    "tk",
]

OPTIONS_TRUE = ["yes", "on", "true", "t", "y", "1"]
OPTIONS_FALSE = ["no", "off", "false", "f", "n", "0"]
OPTIONS_ALL = OPTIONS_TRUE + OPTIONS_FALSE

# Quick and dirty debug logging setup by default
# See: https://docs.python.org/3/howto/logging.html#logging-advanced-tutorial
logger = logging.getLogger(__name__)


[docs] def setup_basic_logging(level: int) -> None: """Set up basic logging Args: level (int): The log level """ if level is None: return # Do not add a new handler if we already have one if not logger.handlers: logger.propagate = False logger.setLevel(level) ch = logging.StreamHandler() ch.setLevel(logging.DEBUG) ch.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")) logger.addHandler(ch)
[docs] class ContextRefs: """Namespace for window/context references""" WINDOW: Optional[BaseWindow] = None CONTEXT: Optional[moderngl.Context] = None
[docs] def activate_context( window: Optional[BaseWindow] = None, ctx: Optional[moderngl.Context] = None ) -> None: """ Register the active window and context. If only a window is supplied the context is taken from the window. Only a context can also be passed in. Keyword Args: window (window): The window to activate ctx (moderngl.Context): The moderngl context to activate """ ContextRefs.WINDOW = window ContextRefs.CONTEXT = ctx if ctx is None: ContextRefs.CONTEXT = window.ctx
[docs] def window() -> BaseWindow: """Obtain the active window""" if ContextRefs.WINDOW: return ContextRefs.WINDOW raise ValueError("No active window and context. Call activate_window.")
[docs] def ctx() -> moderngl.Context: """Obtain the active context""" if ContextRefs.CONTEXT: return ContextRefs.CONTEXT raise ValueError("No active window and context. Call activate_window.")
[docs] def get_window_cls(window: str = "") -> type[BaseWindow]: """ Attempt to obtain a window class using the full dotted python path. This can be used to import custom or modified window classes. Args: window (str): Name of the window Returns: A reference to the requested window class. Raises exception if not found. """ logger.info("Attempting to load window class: %s", window) win = import_string(window) # assert issubclass( # win, BaseWindow # ), f"{win} is not derived from moderngl_window.context.base.BaseWindow" return win
[docs] def get_local_window_cls(window: Optional[str] = None) -> type[BaseWindow]: """ Attempt to obtain a window class in the moderngl_window package using short window names such as ``pyglet`` or ``glfw``. Args: window (str): Name of the window Returns: A reference to the requested window class. Raises exception if not found. """ window = os.environ.get("MODERNGL_WINDOW") or window if window is None: window = "pyglet" return get_window_cls("moderngl_window.context.{}.Window".format(window))
[docs] def find_window_classes() -> list[str]: """ Find available window packages Returns: A list of available window packages """ # In some environments we cannot rely on introspection # and instead return a hardcoded list try: return [ path.parts[-1] for path in Path(__file__).parent.joinpath("context").iterdir() if path.is_dir() and path.parts[-1] not in IGNORE_DIRS ] except Exception: return WINDOW_CLASSES
[docs] def create_window_from_settings() -> BaseWindow: """ Creates a window using configured values in :py:attr:`moderngl_window.conf.Settings.WINDOW`. This will also activate the window/context. Returns: The Window instance """ window_cls = import_string(settings.WINDOW["class"]) window = window_cls(**settings.WINDOW) assert isinstance( window, BaseWindow ), f"{type(window)} is not derived from moderngl_window.context.base.BaseWindow" activate_context(window=window) return window
# --- The simple window config system ---
[docs] def run_window_config( config_cls: type[WindowConfig], timer: Optional[Timer] = None, args: Any = None ) -> None: """ Run an WindowConfig entering a blocking main loop Args: config_cls: The WindowConfig class to render Keyword Args: timer: A custom timer instance args: Override sys.args """ config = create_window_config_instance(config_cls, timer=timer, args=args) run_window_config_instance(config)
[docs] def create_window_config_instance( config_cls: type[WindowConfig], timer: Optional[Timer] = None, args: Any = None ) -> WindowConfig: """ Create and initialize a instance of a WindowConfig class. Quite a bit of boilerplate is required to create a WindowConfig instance and this function aims to simplify that. Args: window_config: The WindowConfig class to create an instance of Keyword Args: kwargs: Arguments to pass to the WindowConfig constructor Returns: An instance of the WindowConfig class """ setup_basic_logging(config_cls.log_level) parser = create_parser() config_cls.add_arguments(parser) values = parse_args(args=args, parser=parser) config_cls.argv = values window_cls = get_local_window_cls(values.window) # Calculate window size size = values.size or config_cls.window_size size = int(size[0] * values.size_mult), int(size[1] * values.size_mult) # Resolve cursor show_cursor = values.cursor if show_cursor is None: show_cursor = config_cls.cursor window = window_cls( title=config_cls.title, size=size, fullscreen=config_cls.fullscreen or values.fullscreen, resizable=(values.resizable if values.resizable is not None else config_cls.resizable), visible=config_cls.visible, gl_version=config_cls.gl_version, aspect_ratio=config_cls.aspect_ratio, vsync=values.vsync if values.vsync is not None else config_cls.vsync, samples=values.samples if values.samples is not None else config_cls.samples, cursor=show_cursor if show_cursor is not None else True, backend=values.backend, context_creation_func=config_cls.init_mgl_context, ) window.print_context_info() activate_context(window=window) if timer is None: timer = Timer() config = config_cls(ctx=window.ctx, wnd=window, timer=timer) # Avoid the event assigning in the property setter for now # We want the even assigning to happen in WindowConfig.__init__ # so users are free to assign them in their own __init__. window._config = weakref.ref(config) # Swap buffers once before staring the main loop. # This can trigger additional resize events reporting # a more accurate buffer size window.swap_buffers() window.set_default_viewport() return config
[docs] def run_window_config_instance(config: WindowConfig) -> None: """ Run an WindowConfig instance entering a blocking main loop. Args: window_config: The WindowConfig instance """ window = config.wnd timer = config.timer timer.start() while not window.is_closing: current_time, delta = timer.next_frame() # Framerate limit for hidden windows if not window.visible and config.hidden_window_framerate_limit > 0: expected_delta_time = 1.0 / config.hidden_window_framerate_limit sleep_time = expected_delta_time - delta if sleep_time > 0: time.sleep(sleep_time) if config.clear_color is not None: window.clear(*config.clear_color) # Always bind the window framebuffer before calling render window.use() window.render(current_time, delta) if not window.is_closing: window.swap_buffers() _, duration = timer.stop() window.destroy() if duration > 0: logger.info("Duration: {0:.2f}s @ {1:.2f} FPS".format(duration, timer.fps_average))
[docs] def create_parser() -> argparse.ArgumentParser: """Create an argparser parsing the standard arguments for WindowConfig""" parser = argparse.ArgumentParser() parser.add_argument( "-wnd", "--window", choices=find_window_classes(), help="Name for the window type to use", ) parser.add_argument( "-fs", "--fullscreen", action="store_true", help="Open the window in fullscreen mode", ) parser.add_argument( "-vs", "--vsync", type=valid_bool, help="Enable or disable vsync", ) parser.add_argument( "-r", "--resizable", type=valid_bool, default=None, help="Enable/disable window resize", ) parser.add_argument( "-hd", "--hidden", type=valid_bool, default=False, help="Start the window in hidden mode", ) parser.add_argument( "-s", "--samples", type=int, help="Specify the desired number of samples to use for multisampling", ) parser.add_argument( "-c", "--cursor", type=valid_bool, help="Enable or disable displaying the mouse cursor", ) parser.add_argument( "--size", type=valid_window_size, help="Window size", ) parser.add_argument( "--size_mult", type=valid_window_size_multiplier, default=1.0, help="Multiplier for the window size making it easy scale the window", ) parser.add_argument( "--backend", help="Specify context backend. This is mostly used to enable EGL in headless mode", ) return parser
[docs] def parse_args( args: Optional[Any] = None, parser: Optional[argparse.ArgumentParser] = None ) -> argparse.Namespace: """Parse arguments from sys.argv Passing in your own argparser can be user to extend the parser. Keyword Args: args: override for sys.argv parser: Supply your own argparser instance """ parser = parser or create_parser() return parser.parse_args(args or sys.argv[1:])
# --- Validators ---
[docs] def valid_bool(value: Optional[str]) -> Optional[bool]: """Validator for bool values""" if value is None: return None value = value.lower() if value in OPTIONS_TRUE: return True if value in OPTIONS_FALSE: return False raise argparse.ArgumentTypeError(f"Boolean value expected. Options: {OPTIONS_ALL}")
[docs] def valid_window_size(value: str) -> tuple[int, int]: """ Validator for window size parameter. Valid format is "[int]x[int]". For example "1920x1080". """ try: width, height = value.split("x") return int(width), int(height) except ValueError: pass raise argparse.ArgumentTypeError( "Valid size format: int]x[int]. Example '1920x1080'", )
[docs] def valid_window_size_multiplier(value: str) -> float: """Validates window size multiplier Must be an integer or float greater than 0 """ try: val = float(value) if val > 0: return val except ValueError: pass raise argparse.ArgumentTypeError( "Must be a positive int or float", )