from typing import Optional, Tuple
import arcade
import arcade.gl as gl
[docs]class NinePatchTexture:
"""
A 9-patch texture is a texture that can be stretched in specific ways to keep
the edges a specific width/height. This is useful for GUI elements that need
to be stretched but have a specific border that should not be stretched.
The center content of the texture will be stretched.
Patch structure::
left right
+------+-----------------+------+
| (1) | (2) | (3) |
| | | |
+------+-----------------+------+ top
| (4) | (5) | (6) |
| | | |
| | | |
| | | |
| | | |
| | | |
+------+-----------------+------+ bottom
| (7) | (8) | (9) |
| | | |
+------+-----------------+------+
To summarize, the texture will be stretched in the following ways:
* Areas (1), (3), (7) and (9) will not be stretched.
* Area (5) will be stretched horizontally and vertically.
* Areas (2) and (8) will be stretched horizontally.
* Areas (4) and (6) will be stretched vertically.
:param int left: The left border of the 9-patch (in pixels)
:param int right: The right border of the 9-patch (in pixels)
:param int bottom: The bottom border of the 9-patch (in pixels)
:param int top: The top border of the 9-patch (in pixels)
:param Texture texture: The texture used for the 9-patch
:param TextureAtlas atlas: the atlas which the texture belongs to (defaults to arcades default 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=":resources:shaders/gui/nine_patch_vs.glsl",
geometry_shader=":resources:shaders/gui/nine_patch_gs.glsl",
fragment_shader=":resources:shaders/gui/nine_patch_fs.glsl",
common=(":resources:shaders/lib/sprite.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 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 simply 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]:
"""
Get size of texture.
"""
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),
size: Tuple[float, float],
pixelated: bool = False,
**kwargs
):
"""
Draw the 9-patch.
:param size: size of the 9-patch
"""
# TODO support to draw at a given position
self.program.set_uniform_safe(
"texture_id", self._atlas.get_texture_id(self._texture.name)
)
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):
"""
Check if borders are valid
"""
# 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")