Source code for tcod.context

"""This module is used to create and handle libtcod contexts.

See :ref:`getting-started` for beginner examples on how to use this module.

:any:`Context`'s are intended to replace several libtcod functions such as
:any:`tcod.console_init_root`, :any:`tcod.console_flush`,
:any:`tcod.console.recommended_size`, and many other functions which rely on
hidden global objects within libtcod.  If you begin using contexts then
most of these functions will no longer work properly.

Instead of calling :any:`tcod.console_init_root` you can call
:any:`tcod.context.new` with different keywords depending on how you plan
to setup the size of the console.  You should use
:any:`tcod.tileset` to load the font for a context.

.. note::
    If you use contexts then you should expect the following functions to no
    longer be available, because these functions rely on a global console,
    tileset, or some other kind of global state:

    :any:`tcod.console_init_root`,
    :any:`tcod.console_set_custom_font`,
    :any:`tcod.console_flush`,
    :any:`tcod.console_is_fullscreen`,
    :any:`tcod.console_is_window_closed`,
    :any:`tcod.console_set_fade`,
    :any:`tcod.console_set_fullscreen`,
    :any:`tcod.console_set_window_title`,
    :any:`tcod.sys_set_fps`,
    :any:`tcod.sys_get_last_frame_length`,
    :any:`tcod.sys_set_renderer`,
    :any:`tcod.sys_save_screenshot`,
    :any:`tcod.sys_force_fullscreen_resolution`,
    :any:`tcod.sys_get_current_resolution`,
    :any:`tcod.sys_register_SDL_renderer`,
    :any:`tcod.console_map_ascii_code_to_font`,
    :any:`tcod.sys_get_char_size`,
    :any:`tcod.sys_update_char`,
    :any:`tcod.console.recommended_size`,
    :any:`tcod.tileset.get_default`,
    :any:`tcod.tileset.set_default`.

    Some event functions can no longer return tile coordinates for the mouse:
    :any:`tcod.sys_check_for_event`,
    :any:`tcod.sys_wait_for_event`,
    :any:`tcod.mouse_get_status`.

.. versionadded:: 11.12
"""  # noqa: E501
import os
import sys
from typing import Any, Iterable, List, Optional, Tuple, Union

from typing_extensions import Literal

import tcod
import tcod.event
import tcod.tileset
from tcod._internal import _check, _check_warn, deprecate, pending_deprecate
from tcod.loader import ffi, lib

__all__ = (
    "Context",
    "new",
    "new_window",
    "new_terminal",
    "SDL_WINDOW_FULLSCREEN",
    "SDL_WINDOW_FULLSCREEN_DESKTOP",
    "SDL_WINDOW_HIDDEN",
    "SDL_WINDOW_BORDERLESS",
    "SDL_WINDOW_RESIZABLE",
    "SDL_WINDOW_MINIMIZED",
    "SDL_WINDOW_MAXIMIZED",
    "SDL_WINDOW_INPUT_GRABBED",
    "SDL_WINDOW_ALLOW_HIGHDPI",
    "RENDERER_OPENGL",
    "RENDERER_OPENGL2",
    "RENDERER_SDL",
    "RENDERER_SDL2",
)

SDL_WINDOW_FULLSCREEN = lib.SDL_WINDOW_FULLSCREEN
"""Exclusive fullscreen mode.

It's generally not recommended to use this flag unless you know what you're
doing.
`SDL_WINDOW_FULLSCREEN_DESKTOP` should be used instead whenever possible.
"""
SDL_WINDOW_FULLSCREEN_DESKTOP = lib.SDL_WINDOW_FULLSCREEN_DESKTOP
"""A borderless fullscreen window at the desktop resolution."""
SDL_WINDOW_HIDDEN = lib.SDL_WINDOW_HIDDEN
"""Window is hidden."""
SDL_WINDOW_BORDERLESS = lib.SDL_WINDOW_BORDERLESS
"""Window has no decorative border."""
SDL_WINDOW_RESIZABLE = lib.SDL_WINDOW_RESIZABLE
"""Window can be resized."""
SDL_WINDOW_MINIMIZED = lib.SDL_WINDOW_MINIMIZED
"""Window is minimized."""
SDL_WINDOW_MAXIMIZED = lib.SDL_WINDOW_MAXIMIZED
"""Window is maximized."""
SDL_WINDOW_INPUT_GRABBED = lib.SDL_WINDOW_INPUT_GRABBED
"""Window has grabbed the input."""
SDL_WINDOW_ALLOW_HIGHDPI = lib.SDL_WINDOW_ALLOW_HIGHDPI
"""High DPI mode, see the SDL documentation."""

RENDERER_OPENGL = lib.TCOD_RENDERER_OPENGL
"""A renderer for older versions of OpenGL.

Should support OpenGL 1 and GLES 1
"""
RENDERER_OPENGL2 = lib.TCOD_RENDERER_OPENGL2
"""An SDL2/OPENGL2 renderer.  Usually faster than regular SDL2.

Recommended if you need a high performance renderer.

Should support OpenGL 2.0 and GLES 2.0.
"""
RENDERER_SDL = lib.TCOD_RENDERER_SDL
"""Same as RENDERER_SDL2, but forces SDL2 into software mode."""
RENDERER_SDL2 = lib.TCOD_RENDERER_SDL2
"""The main SDL2 renderer.

Rendering is decided by SDL2 and can be changed by using an SDL2 hint:
https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER
"""


def _handle_tileset(tileset: Optional[tcod.tileset.Tileset]) -> Any:
    """Get the TCOD_Tileset pointer from a Tileset or return a NULL pointer."""
    return tileset._tileset_p if tileset else ffi.NULL


def _handle_title(title: Optional[str]) -> Any:
    """Return title as a CFFI string.

    If title is None then return a decent default title is returned.
    """
    if title is None:
        title = os.path.basename(sys.argv[0])
    return ffi.new("char[]", title.encode("utf-8"))


[docs]class Context: """Context manager for libtcod context objects. You should use :any:`tcod.context.new_terminal` or :any:`tcod.context.new_window` to create a new context. """ def __init__(self, context_p: Any): """Creates a context from a cffi pointer.""" self._context_p = context_p @classmethod def _claim(cls, context_p: Any) -> "Context": return cls(ffi.gc(context_p, lib.TCOD_context_delete))
[docs] def __enter__(self) -> "Context": """This context can be used as a context manager.""" return self
[docs] def close(self) -> None: """Delete the context, closing any windows opened by this context. This instance is invalid after this call.""" if hasattr(self, "_context_p"): ffi.release(self._context_p) del self._context_p
[docs] def __exit__(self, *args: Any) -> None: """The libtcod context is closed as this context manager exits.""" self.close()
[docs] def present( self, console: tcod.console.Console, *, keep_aspect: bool = False, integer_scaling: bool = False, clear_color: Tuple[int, int, int] = (0, 0, 0), align: Tuple[float, float] = (0.5, 0.5) ) -> None: """Present a console to this context's display. `console` is the console you want to present. If `keep_aspect` is True then the console aspect will be preserved with a letterbox. Otherwise the console will be stretched to fill the screen. If `integer_scaling` is True then the console will be scaled in integer increments. This will have no effect if the console must be shrunk. You can use :any:`tcod.console.recommended_size` to create a console which will fit the window without needing to be scaled. `clear_color` is an RGB tuple used to clear the screen before the console is presented, this will affect the border/letterbox color. `align` is an (x, y) tuple determining where the console will be placed when letter-boxing exists. Values of 0 will put the console at the upper-left corner. Values of 0.5 will center the console. """ clear_rgba = (clear_color[0], clear_color[1], clear_color[2], 255) viewport_args = ffi.new( "TCOD_ViewportOptions*", { "tcod_version": lib.TCOD_COMPILEDVERSION, "keep_aspect": keep_aspect, "integer_scaling": integer_scaling, "clear_color": clear_rgba, "align_x": align[0], "align_y": align[1], }, ) _check( lib.TCOD_context_present( self._context_p, console.console_c, viewport_args ) )
[docs] def pixel_to_tile(self, x: int, y: int) -> Tuple[int, int]: """Convert window pixel coordinates to tile coordinates.""" with ffi.new("int[2]", (x, y)) as xy: _check( lib.TCOD_context_screen_pixel_to_tile_i( self._context_p, xy, xy + 1 ) ) return xy[0], xy[1]
[docs] def pixel_to_subtile(self, x: int, y: int) -> Tuple[float, float]: """Convert window pixel coordinates to sub-tile coordinates.""" with ffi.new("double[2]", (x, y)) as xy: _check( lib.TCOD_context_screen_pixel_to_tile_d( self._context_p, xy, xy + 1 ) ) return xy[0], xy[1]
[docs] def convert_event(self, event: tcod.event.Event) -> None: """Fill in the tile coordinates of a mouse event using this context.""" if isinstance(event, (tcod.event.MouseState, tcod.event.MouseMotion)): event.tile = tcod.event.Point(*self.pixel_to_tile(*event.pixel)) if isinstance(event, tcod.event.MouseMotion): prev_tile = self.pixel_to_tile( event.pixel[0] - event.pixel_motion[0], event.pixel[1] - event.pixel_motion[1], ) event.tile_motion = tcod.event.Point( event.tile[0] - prev_tile[0], event.tile[1] - prev_tile[1] )
[docs] def save_screenshot(self, path: Optional[str] = None) -> None: """Save a screen-shot to the given file path.""" c_path = path.encode("utf-8") if path is not None else ffi.NULL _check(lib.TCOD_context_save_screenshot(self._context_p, c_path))
[docs] def change_tileset(self, tileset: Optional[tcod.tileset.Tileset]) -> None: """Change the active tileset used by this context.""" _check( lib.TCOD_context_change_tileset( self._context_p, _handle_tileset(tileset) ) )
[docs] def new_console( self, min_columns: int = 1, min_rows: int = 1, magnification: float = 1.0, order: Union[Literal["C"], Literal["F"]] = "C", ) -> tcod.console.Console: """Return a new console sized for this context. `min_columns` and `min_rows` are the minimum size to use for the new console. `magnification` determines the apparent size of the tiles on the output display. A `magnification` larger then 1.0 will output smaller consoles, which will show as larger tiles when presented. `magnification` must be greater than zero. `order` is passed to :any:`tcod.console.Console` to determine the memory order of its NumPy attributes. The times where it is the most useful to call this method are: * After the context is created, even if the console was given a specific size. * After the :any:`change_tileset` method is called. * After any window resized event, or any manual resizing of the window. .. versionadded:: 11.18 .. versionchanged:: 11.19 Added `order` parameter. .. seealso:: :any:`tcod.console.Console` """ if magnification < 0: raise ValueError( "Magnification must be greater than zero. (Got %f)" % magnification ) size = ffi.new("int[2]") _check( lib.TCOD_context_recommended_console_size( self._context_p, magnification, size, size + 1 ) ) width, height = max(min_columns, size[0]), max(min_rows, size[1]) return tcod.console.Console(width, height, order=order)
[docs] def recommended_console_size( self, min_columns: int = 1, min_rows: int = 1 ) -> Tuple[int, int]: """Return the recommended (columns, rows) of a console for this context. `min_columns`, `min_rows` are the lowest values which will be returned. If result is only used to create a new console then you may want to call :any:`Context.new_console` instead. """ with ffi.new("int[2]") as size: _check( lib.TCOD_context_recommended_console_size( self._context_p, 1.0, size, size + 1 ) ) return max(min_columns, size[0]), max(min_rows, size[1])
@property def renderer_type(self) -> int: """Return the libtcod renderer type used by this context.""" return _check(lib.TCOD_context_get_renderer_type(self._context_p)) @property def sdl_window_p(self) -> Any: '''A cffi `SDL_Window*` pointer. This pointer might be NULL. This pointer will become invalid if the context is closed or goes out of scope. Python-tcod's FFI provides most SDL functions. So it's possible for anyone with the SDL2 documentation to work directly with SDL's pointers. Example:: import tcod def toggle_fullscreen(context: tcod.context.Context) -> None: """Toggle a context window between fullscreen and windowed modes.""" if not context.sdl_window_p: return fullscreen = tcod.lib.SDL_GetWindowFlags(context.sdl_window_p) & ( tcod.lib.SDL_WINDOW_FULLSCREEN | tcod.lib.SDL_WINDOW_FULLSCREEN_DESKTOP ) tcod.lib.SDL_SetWindowFullscreen( context.sdl_window_p, 0 if fullscreen else tcod.lib.SDL_WINDOW_FULLSCREEN_DESKTOP, ) ''' # noqa: E501 return lib.TCOD_context_get_sdl_window(self._context_p)
@ffi.def_extern() # type: ignore def _pycall_cli_output(catch_reference: Any, output: Any) -> None: """Callback for the libtcod context CLI. Catches the CLI output.""" catch = ffi.from_handle(catch_reference) # type: List[str] catch.append(ffi.string(output).decode("utf-8"))
[docs]def new( *, x: Optional[int] = None, y: Optional[int] = None, width: Optional[int] = None, height: Optional[int] = None, columns: Optional[int] = None, rows: Optional[int] = None, renderer: Optional[int] = None, tileset: Optional[tcod.tileset.Tileset] = None, vsync: bool = True, sdl_window_flags: Optional[int] = None, title: Optional[str] = None, argv: Optional[Iterable[str]] = None ) -> Context: """Create a new context with the desired pixel size. `x`, `y`, `width`, and `height` are the desired position and size of the window. If these are None then they will be derived from `columns` and `rows`. So if you plan on having a console of a fixed size then you should set `columns` and `rows` instead of the window keywords. `columns` and `rows` is the desired size of the console. Can be left as `None` when you're setting a context by a window size instead of a console. Providing no size information at all is also acceptable. `renderer` is the desired libtcod renderer to use. Typical options are :any:`tcod.context.RENDERER_OPENGL2` for a faster renderer or :any:`tcod.context.RENDERER_SDL2` for a reliable renderer. `tileset` is the font/tileset for the new context to render with. The fall-back tileset available from passing None is useful for prototyping, but will be unreliable across platforms. `vsync` is the Vertical Sync option for the window. The default of True is recommended but you may want to use False for benchmarking purposes. `sdl_window_flags` is a bit-field of SDL window flags, if None is given then a default of :any:`tcod.context.SDL_WINDOW_RESIZABLE` is used. There's more info on the SDL documentation: https://wiki.libsdl.org/SDL_CreateWindow#Remarks `title` is the desired title of the window. `argv` these arguments are passed to libtcod and allow an end-user to make last second changes such as forcing fullscreen or windowed mode, or changing the libtcod renderer. By default this will pass in `sys.argv` but you can disable this feature by providing an empty list instead. Certain commands such as ``-help`` will raise a SystemExit exception from this function with the output message. When a window size is given instead of a console size then you can use :any:`Context.recommended_console_size` to automatically find the size of the console which should be used. .. versionadded:: 11.16 """ if renderer is None: renderer = RENDERER_OPENGL2 if sdl_window_flags is None: sdl_window_flags = SDL_WINDOW_RESIZABLE if argv is None: argv = sys.argv argv_encoded = [ # Needs to be kept alive for argv_c. ffi.new("char[]", arg.encode("utf-8")) for arg in argv ] argv_c = ffi.new("char*[]", argv_encoded) catch_msg = [] # type: List[str] catch_handle = ffi.new_handle(catch_msg) # Keep alive. title_p = _handle_title(title) # Keep alive. params = ffi.new( "struct TCOD_ContextParams*", { "tcod_version": lib.TCOD_COMPILEDVERSION, "window_x": x if x is not None else lib.SDL_WINDOWPOS_UNDEFINED, "window_y": y if y is not None else lib.SDL_WINDOWPOS_UNDEFINED, "pixel_width": width or 0, "pixel_height": height or 0, "columns": columns or 0, "rows": rows or 0, "renderer_type": renderer, "tileset": _handle_tileset(tileset), "vsync": vsync, "sdl_window_flags": sdl_window_flags, "window_title": title_p, "argc": len(argv_c), "argv": argv_c, "cli_output": ffi.addressof(lib, "_pycall_cli_output"), "cli_userdata": catch_handle, "window_xy_defined": True, }, ) context_pp = ffi.new("TCOD_Context**") error = lib.TCOD_context_new(params, context_pp) if error == lib.TCOD_E_REQUIRES_ATTENTION: raise SystemExit(catch_msg[0]) _check_warn(error) return Context._claim(context_pp[0])
[docs]@pending_deprecate( "Call tcod.context.new with width and height as keyword parameters." ) def new_window( width: int, height: int, *, renderer: Optional[int] = None, tileset: Optional[tcod.tileset.Tileset] = None, vsync: bool = True, sdl_window_flags: Optional[int] = None, title: Optional[str] = None ) -> Context: """Create a new context with the desired pixel size. .. deprecated:: 11.16 :any:`tcod.context.new` provides more options, such as window position. """ return new( width=width, height=height, renderer=renderer, tileset=tileset, vsync=vsync, sdl_window_flags=sdl_window_flags, title=title, )
[docs]@pending_deprecate( "Call tcod.context.new with columns and rows as keyword parameters." ) def new_terminal( columns: int, rows: int, *, renderer: Optional[int] = None, tileset: Optional[tcod.tileset.Tileset] = None, vsync: bool = True, sdl_window_flags: Optional[int] = None, title: Optional[str] = None ) -> Context: """Create a new context with the desired console size. .. deprecated:: 11.16 :any:`tcod.context.new` provides more options. """ return new( columns=columns, rows=rows, renderer=renderer, tileset=tileset, vsync=vsync, sdl_window_flags=sdl_window_flags, title=title, )