X-Git-Url: https://git.ayoreis.com/hackapet/Adafruit_Blinka_Displayio.git/blobdiff_plain/3e71b56944b2f39930bd2d50a387042a6c6d6fcc..9174c060a2059d9debc04a1254bbc516f78d0dd7:/displayio/_group.py diff --git a/displayio/_group.py b/displayio/_group.py new file mode 100644 index 0000000..8e006b2 --- /dev/null +++ b/displayio/_group.py @@ -0,0 +1,234 @@ +# SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`displayio.group` +================================================================================ + +displayio for Blinka + +**Software and Dependencies:** + +* Adafruit Blinka: + https://github.com/adafruit/Adafruit_Blinka/releases + +* Author(s): Melissa LeBlanc-Williams + +""" + +from __future__ import annotations +from typing import Union, Callable +from recordclass import recordclass +from ._tilegrid import TileGrid + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git" + + +Transform = recordclass("Transform", "x y dx dy scale transpose_xy mirror_x mirror_y") + + +class Group: + """ + Manage a group of sprites and groups and how they are inter-related. + + Create a Group of a given scale. Scale is in one dimension. For example, scale=2 + leads to a layer's pixel being 2x2 pixels when in the group. + """ + + def __init__(self, *, scale: int = 1, x: int = 0, y: int = 0): + """ + :param int scale: Scale of layer pixels in one dimension. + :param int x: Initial x position within the parent. + :param int y: Initial y position within the parent. + """ + + if not isinstance(scale, int) or scale < 1: + raise ValueError("Scale must be >= 1") + self._scale = 1 # Use the setter below to actually set the scale + self._group_x = x + self._group_y = y + self._hidden_group = False + self._layers = [] + self._supported_types = (TileGrid, Group) + self.in_group = False + self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False) + self._set_scale(scale) # Set the scale via the setter + + def _update_transform(self, parent_transform): + """Update the parent transform and child transforms""" + self.in_group = parent_transform is not None + if self.in_group: + x = self._group_x + y = self._group_y + if parent_transform.transpose_xy: + x, y = y, x + self._absolute_transform.x = parent_transform.x + parent_transform.dx * x + self._absolute_transform.y = parent_transform.y + parent_transform.dy * y + self._absolute_transform.dx = parent_transform.dx * self._scale + self._absolute_transform.dy = parent_transform.dy * self._scale + self._absolute_transform.transpose_xy = parent_transform.transpose_xy + self._absolute_transform.mirror_x = parent_transform.mirror_x + self._absolute_transform.mirror_y = parent_transform.mirror_y + self._absolute_transform.scale = parent_transform.scale * self._scale + self._update_child_transforms() + + def _update_child_transforms(self): + # pylint: disable=protected-access + if self.in_group: + for layer in self._layers: + layer._update_transform(self._absolute_transform) + # pylint: enable=protected-access + + def _removal_cleanup(self, index): + layer = self._layers[index] + layer._update_transform(None) # pylint: disable=protected-access + + def _layer_update(self, index): + # pylint: disable=protected-access + layer = self._layers[index] + layer._update_transform(self._absolute_transform) + # pylint: enable=protected-access + + def append(self, layer: Union[Group, TileGrid]) -> None: + """Append a layer to the group. It will be drawn + above other layers. + """ + self.insert(len(self._layers), layer) + + def insert(self, index: int, layer: Union[Group, TileGrid]) -> None: + """Insert a layer into the group.""" + if not isinstance(layer, self._supported_types): + raise ValueError("Invalid Group Member") + if layer.in_group: + raise ValueError("Layer already in a group.") + self._layers.insert(index, layer) + self._layer_update(index) + + def index(self, layer: Union[Group, TileGrid]) -> int: + """Returns the index of the first copy of layer. + Raises ValueError if not found. + """ + return self._layers.index(layer) + + def pop(self, index=-1) -> Union[Group, TileGrid]: + """Remove the ith item and return it.""" + self._removal_cleanup(index) + return self._layers.pop(index) + + def remove(self, layer) -> None: + """Remove the first copy of layer. Raises ValueError + if it is not present.""" + index = self.index(layer) + self._layers.pop(index) + + def __len__(self) -> int: + """Returns the number of layers in a Group""" + return len(self._layers) + + def __getitem__(self, index) -> Union[Group, TileGrid]: + """Returns the value at the given index.""" + return self._layers[index] + + def __setitem__(self, index, value) -> None: + """Sets the value at the given index.""" + self._removal_cleanup(index) + self._layers[index] = value + self._layer_update(index) + + def __delitem__(self, index) -> None: + """Deletes the value at the given index.""" + del self._layers[index] + + def _fill_area(self, buffer): + if self._hidden_group: + return + + for layer in self._layers: + if isinstance(layer, (Group, TileGrid)): + layer._fill_area(buffer) # pylint: disable=protected-access + + def sort(self, key: Callable, reverse: bool) -> None: + """Sort the members of the group.""" + self._layers.sort(key=key, reverse=reverse) + + @property + def hidden(self) -> bool: + """True when the Group and all of it’s layers are not visible. When False, the + Group’s layers are visible if they haven’t been hidden. + """ + return self._hidden_group + + @hidden.setter + def hidden(self, value: bool): + if not isinstance(value, (bool, int)): + raise ValueError("Expecting a boolean or integer value") + self._hidden_group = bool(value) + + @property + def scale(self) -> int: + """Scales each pixel within the Group in both directions. For example, when + scale=2 each pixel will be represented by 2x2 pixels. + """ + return self._scale + + @scale.setter + def scale(self, value: int): + self._set_scale(value) + + def _set_scale(self, value: int): + # This is method allows the scale to be set by this class even when + # the scale property is over-ridden by a subclass. + if not isinstance(value, int) or value < 1: + raise ValueError("Scale must be >= 1") + if self._scale != value: + parent_scale = self._absolute_transform.scale / self._scale + self._absolute_transform.dx = ( + self._absolute_transform.dx / self._scale * value + ) + self._absolute_transform.dy = ( + self._absolute_transform.dy / self._scale * value + ) + self._absolute_transform.scale = parent_scale * value + + self._scale = value + self._update_child_transforms() + + @property + def x(self) -> int: + """X position of the Group in the parent.""" + return self._group_x + + @x.setter + def x(self, value: int): + if not isinstance(value, int): + raise ValueError("x must be an integer") + if self._group_x != value: + if self._absolute_transform.transpose_xy: + dy_value = self._absolute_transform.dy / self._scale + self._absolute_transform.y += dy_value * (value - self._group_x) + else: + dx_value = self._absolute_transform.dx / self._scale + self._absolute_transform.x += dx_value * (value - self._group_x) + self._group_x = value + self._update_child_transforms() + + @property + def y(self) -> int: + """Y position of the Group in the parent.""" + return self._group_y + + @y.setter + def y(self, value: int): + if not isinstance(value, int): + raise ValueError("y must be an integer") + if self._group_y != value: + if self._absolute_transform.transpose_xy: + dx_value = self._absolute_transform.dx / self._scale + self._absolute_transform.x += dx_value * (value - self._group_y) + else: + dy_value = self._absolute_transform.dy / self._scale + self._absolute_transform.y += dy_value * (value - self._group_y) + self._group_y = value + self._update_child_transforms()