Source code for tuxemon.ui.draw

from __future__ import annotations

import logging
import math
from itertools import product
from typing import (
    Callable,
    Generator,
    Iterable,
    List,
    Optional,
    Sequence,
    Tuple,
)

import pygame
from pygame.rect import Rect

from tuxemon import prepare
from tuxemon.graphics import ColorLike
from tuxemon.sprite import Sprite

logger = logging.getLogger(__name__)

__all__ = ("GraphicBox",)


def create_layout(
    scale: float,
) -> Callable[[Sequence[float]], Sequence[float]]:
    def func(area: Sequence[float]) -> Sequence[float]:
        return [scale * i for i in area]

    return func


layout = create_layout(prepare.SCALE)


[docs]class GraphicBox(Sprite): """ Generic class for drawing graphical boxes. Draws a border and can fill in the box with a _color from the border file, an external file, or a solid _color. box = GraphicBox('border.png') box.draw(surface, rect) The border graphic must contain 9 tiles laid out in a box. """ def __init__( self, border: Optional[pygame.surface.Surface] = None, background: Optional[pygame.surface.Surface] = None, color: Optional[ColorLike] = None, fill_tiles: bool = False, ) -> None: super().__init__() self._background = background self._color = color self._fill_tiles = fill_tiles self._tiles: List[pygame.surface.Surface] = [] self._tile_size = 0, 0 if border: self._set_border(border)
[docs] def calc_inner_rect(self, rect: Rect) -> Rect: if self._tiles: tw, th = self._tile_size return rect.inflate(-tw * 2, -th * 2) else: return rect
def _set_border(self, image: pygame.surface.Surface) -> None: iw, ih = image.get_size() tw, th = iw // 3, ih // 3 self._tile_size = tw, th self._tiles = [ image.subsurface((x, y, tw, th)) for x, y in product(range(0, iw, tw), range(0, ih, th)) ]
[docs] def update_image(self) -> None: rect = Rect((0, 0), self._rect.size) surface = pygame.Surface(rect.size, pygame.SRCALPHA) self._draw(surface, rect) self.image = surface
def _draw( self, surface: pygame.surface.Surface, rect: Rect, ) -> Rect: inner = self.calc_inner_rect(rect) # fill center with a _background surface if self._background: surface.blit( pygame.transform.scale(self._background, inner.size), inner, ) # fill center with solid _color elif self._color: surface.fill(self._color, inner) # fill center with tiles from the border file elif self._fill_tiles: tw, th = self._tile_size p = product( range(inner.left, inner.right, tw), range(inner.top, inner.bottom, th), ) [surface.blit(self._tiles[4], pos) for pos in p] # draw the border if self._tiles: surface_blit = surface.blit ( tile_nw, tile_w, tile_sw, tile_n, tile_c, tile_s, tile_ne, tile_e, tile_se, ) = self._tiles left, top = rect.topleft tw, th = self._tile_size # draw top and bottom tiles area: Optional[Tuple[int, int, int, int]] for x in range(inner.left, inner.right, tw): if x + tw >= inner.right: area = 0, 0, tw - (x + tw - inner.right), th else: area = None surface_blit(tile_n, (x, top), area) surface_blit(tile_s, (x, inner.bottom), area) # draw left and right tiles for y in range(inner.top, inner.bottom, th): if y + th >= inner.bottom: area = 0, 0, tw, th - (y + th - inner.bottom) else: area = None surface_blit(tile_w, (left, y), area) surface_blit(tile_e, (inner.right, y), area) # draw corners surface_blit(tile_nw, (left, top)) surface_blit(tile_sw, (left, inner.bottom)) surface_blit(tile_ne, (inner.right, top)) surface_blit(tile_se, (inner.right, inner.bottom)) return rect
def guest_font_height(font: pygame.font.Font) -> int: return guess_rendered_text_size("Tg", font)[1] def guess_rendered_text_size( text: str, font: pygame.font.Font, ) -> Tuple[int, int]: return font.size(text) def shadow_text( font: pygame.font.Font, fg: ColorLike, bg: ColorLike, text: str, ) -> pygame.surface.Surface: top = font.render(text, True, fg) shadow = font.render(text, True, bg) offset = layout((0.5, 0.5)) size = [int(math.ceil(a + b)) for a, b in zip(offset, top.get_size())] image = pygame.Surface(size, pygame.SRCALPHA) image.blit(shadow, offset) image.blit(top, (0, 0)) return image def iter_render_text( text: str, font: pygame.font.Font, fg: ColorLike, bg: ColorLike, rect: Rect, ) -> Generator[Tuple[Rect, pygame.surface.Surface], None, None]: line_height = guest_font_height(font) for line_index, line in enumerate(constrain_width(text, font, rect.width)): top = rect.top + line_index * line_height for scrap in build_line(line): if scrap[-1] == " ": # No need to blit a white sprite onto a white background continue dirty_length = font.size(scrap[:-1])[0] surface = shadow_text(font, fg, bg, scrap[-1]) update_rect = surface.get_rect( top=top, left=rect.left + dirty_length, ) yield update_rect, surface def build_line(text: str) -> Generator[str, None, None]: for index in range(1, len(text) + 1): yield text[:index] def constrain_width( text: str, font: pygame.font.Font, width: int, ) -> Generator[str, None, None]: for line in iterate_word_lines(text): scrap = None for word in line: if scrap: test = scrap + " " + word else: test = word token_width = font.size(test)[0] if token_width >= width: if scrap is None: raise RuntimeError("message is too large for width", text) yield scrap scrap = word else: scrap = test else: # executed when line is too large yield scrap if scrap else "" def iterate_words(text: str) -> Generator[str, None, None]: yield from text.split(" ") def iterate_lines(text: str) -> Generator[str, None, None]: yield from text.strip().split("\n") def iterate_word_lines(text: str) -> Generator[Iterable[str], None, None]: for line in iterate_lines(text): yield iterate_words(line) def blit_alpha( target: pygame.surface.Surface, source: pygame.surface.Surface, location: Tuple[int, int], opacity: int, ) -> None: """ Blits a surface with alpha that can also have it's overall transparency modified. Taken from http://nerdparadise.com/tech/python/pygame/blitopacity/ NOTE: This should be removed because of the performance implications. """ x = location[0] y = location[1] temp = pygame.Surface((source.get_width(), source.get_height())).convert() temp.blit(target, (-x, -y)) temp.blit(source, (0, 0)) temp.set_alpha(opacity) target.blit(temp, location)