Source code for arcade.gui.nine_patch

from __future__ import annotations

from typing import Optional, Tuple

import arcade
import arcade.gl as gl


[docs] class NinePatchTexture: """ Keeps borders & corners at constant widths while stretching the middle. It can be used with new or existing :py:class:`~arcade.gui.UIWidget` subclasses wherever an ordinary :py:class:`arcade.Texture` is supported. This is useful for GUI elements which must grow or shrink while keeping their border decorations constant, such as dialog boxes or text boxes. The diagram below explains the stretching behavior of this class: * Numbered regions with arrows (``<--->``) stretch along the direction(s) of any arrows present * Bars (``|---|``) mark the distances specified by the border parameters (``left``, etc) .. code-block:: :caption: Stretch Axes & Border Parameters left right |------| |------| top +------+-----------------+------+ --- | (1) | (2) | (3) | | | | <-------------> | | | +------+-----------------+------+ --- | (4) | (5) ^ | (6) | | ^ | | | ^ | | | | | | | | | | | <------+------> | | | | | | | | | | | | | | | | | | v | v | v | +------+-----------------+------+ --- | (7) | (8) | (9) | | | | <-------------> | | | +------+-----------------+------+ --- bottom As the texture is stretched, the numbered slices of the texture behave as follows: * Areas ``(1)``, ``(3)``, ``(7)`` and ``(9)`` never stretch. * Area ``(5)`` stretches both horizontally and vertically. * Areas ``(2)`` and ``(8)`` only stretch horizontally. * Areas ``(4)`` and ``(6)`` only stretch vertically. :param left: The width of the left border of the 9-patch (in pixels) :param right: The width of the right border of the 9-patch (in pixels) :param bottom: The height of the bottom border of the 9-patch (in pixels) :param top: The height of the top border of the 9-patch (in pixels) :param texture: The raw texture to use for the 9-patch :param atlas: Specify an atlas other than arcade's default texture atlas """ def __init__( self, *, left: int, right: int, bottom: int, top: int, texture: arcade.Texture, atlas: Optional[arcade.TextureAtlas] = None ): self._ctx = arcade.get_window().ctx # TODO: Cache in context? self._program = self.ctx.load_program( vertex_shader=":system:shaders/gui/nine_patch_vs.glsl", geometry_shader=":system:shaders/gui/nine_patch_gs.glsl", fragment_shader=":system:shaders/gui/nine_patch_fs.glsl", ) # Configure texture channels self.program.set_uniform_safe("uv_texture", 0) self.program["sprite_texture"] = 1 # TODO: Cache in context self._geometry = self.ctx.geometry() # References for the texture self._atlas = atlas or self.ctx.default_atlas self._texture = texture self._set_texture(texture) # pixel texture co-ordinate start and end of central box. self._left = left self._right = right self._bottom = bottom self._top = top self._check_sizes() @property def ctx(self) -> arcade.ArcadeContext: """The OpenGL context.""" return self._ctx @property def texture(self) -> arcade.Texture: """Get or set the texture.""" return self._texture @texture.setter def texture(self, texture: arcade.Texture): self._set_texture(texture) @property def program(self) -> gl.program.Program: """ Get or set the shader program. Returns the default shader if no other shader is assigned. """ return self._program @program.setter def program(self, program: gl.program.Program): self._program = program def _set_texture(self, texture: arcade.Texture): """ Internal method for setting the texture. It ensures the texture is added to the global atlas. """ if not self._atlas.has_texture(texture): self._atlas.add(texture) self._texture = texture @property def left(self) -> int: """Get or set the left border of the 9-patch.""" return self._left @left.setter def left(self, left: int): self._left = left @property def right(self) -> int: """Get or set the right border of the 9-patch.""" return self._right @right.setter def right(self, right: int): self._right = right @property def bottom(self) -> int: """Get or set the bottom border of the 9-patch.""" return self._bottom @bottom.setter def bottom(self, bottom: int): self._bottom = bottom @property def top(self) -> int: """Get or set the top border of the 9-patch.""" return self._top @top.setter def top(self, top: int): self._top = top @property def size(self) -> Tuple[int, int]: """The size of texture as a width, height tuple in pixels.""" return self.texture.size @property def width(self) -> int: """The width of the texture in pixels.""" return self.texture.width @property def height(self) -> int: """The height of the texture in pixels.""" return self.texture.height
[docs] def draw_sized( self, *, position: Tuple[float, float] = (0.0, 0.0), size: Tuple[float, float], pixelated: bool = False, **kwargs ): """ Draw the 9-patch texture with a specific size. .. warning:: This method assumes the passed dimensions are proper! Unexpected behavior may occur if you specify a size smaller than the total size of the border areas. :param position: Bottom left offset of the texture in pixels :param size: Size of the 9-patch as width, height in pixels :param pixelated: Whether to draw with nearest neighbor interpolation """ self.program.set_uniform_safe( "texture_id", self._atlas.get_texture_id(self._texture) ) if pixelated: self._atlas.texture.filter = self._ctx.NEAREST, self._ctx.NEAREST else: self._atlas.texture.filter = self._ctx.LINEAR, self._ctx.LINEAR self.program["position"] = position self.program["start"] = self._left, self._bottom self.program["end"] = self.width - self._right, self.height - self._top self.program["size"] = size self.program["t_size"] = self._texture.size self._atlas.use_uv_texture(0) self._atlas.texture.use(1) self._geometry.render(self._program, vertices=1)
def _check_sizes(self): """Raise a ValueError if any dimension is invalid.""" # Sanity check values if self._left < 0: raise ValueError("Left border must be a positive integer") if self._right < 0: raise ValueError("Right border must be a positive integer") if self._bottom < 0: raise ValueError("Bottom border must be a positive integer") if self._top < 0: raise ValueError("Top border must be a positive integer") # Sanity check texture size if self._left + self._right > self._texture.width: raise ValueError( "Left and right border must be smaller than texture width") if self._bottom + self._top > self._texture.height: raise ValueError( "Bottom and top border must be smaller than texture height")