]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_group.py
More displayio code updates
[hackapet/Adafruit_Blinka_Displayio.git] / displayio / _group.py
1 # SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
2 #
3 # SPDX-License-Identifier: MIT
4
5 """
6 `displayio.group`
7 ================================================================================
8
9 displayio for Blinka
10
11 **Software and Dependencies:**
12
13 * Adafruit Blinka:
14   https://github.com/adafruit/Adafruit_Blinka/releases
15
16 * Author(s): Melissa LeBlanc-Williams
17
18 """
19
20 from __future__ import annotations
21 from typing import Union, Callable
22 from ._structs import TransformStruct
23 from ._tilegrid import TileGrid
24 from ._colorspace import Colorspace
25 from ._area import Area
26
27 __version__ = "0.0.0+auto.0"
28 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
29
30
31 class Group:
32     """
33     Manage a group of sprites and groups and how they are inter-related.
34
35     Create a Group of a given scale. Scale is in one dimension. For example, scale=2
36     leads to a layer's pixel being 2x2 pixels when in the group.
37     """
38
39     def __init__(self, *, scale: int = 1, x: int = 0, y: int = 0):
40         """
41         :param int scale: Scale of layer pixels in one dimension.
42         :param int x: Initial x position within the parent.
43         :param int y: Initial y position within the parent.
44         """
45
46         if not isinstance(scale, int) or scale < 1:
47             raise ValueError("Scale must be >= 1")
48         self._scale = 1  # Use the setter below to actually set the scale
49         self._group_x = x
50         self._group_y = y
51         self._hidden_group = False
52         self._layers = []
53         self._supported_types = (TileGrid, Group)
54         self._in_group = False
55         self._absolute_transform = TransformStruct(0, 0, 1, 1, 1, False, False, False)
56         self._set_scale(scale)  # Set the scale via the setter
57
58     def _update_transform(self, parent_transform):
59         """Update the parent transform and child transforms"""
60         self._in_group = parent_transform is not None
61         if self._in_group:
62             x = self._group_x
63             y = self._group_y
64             if parent_transform.transpose_xy:
65                 x, y = y, x
66             self._absolute_transform.x = parent_transform.x + parent_transform.dx * x
67             self._absolute_transform.y = parent_transform.y + parent_transform.dy * y
68             self._absolute_transform.dx = parent_transform.dx * self._scale
69             self._absolute_transform.dy = parent_transform.dy * self._scale
70             self._absolute_transform.transpose_xy = parent_transform.transpose_xy
71             self._absolute_transform.mirror_x = parent_transform.mirror_x
72             self._absolute_transform.mirror_y = parent_transform.mirror_y
73             self._absolute_transform.scale = parent_transform.scale * self._scale
74         self._update_child_transforms()
75
76     def _update_child_transforms(self):
77         # pylint: disable=protected-access
78         if self._in_group:
79             for layer in self._layers:
80                 layer._update_transform(self._absolute_transform)
81
82     def _removal_cleanup(self, index):
83         # pylint: disable=protected-access
84         layer = self._layers[index]
85         layer._update_transform(None)
86
87     def _layer_update(self, index):
88         # pylint: disable=protected-access
89         layer = self._layers[index]
90         layer._update_transform(self._absolute_transform)
91
92     def append(self, layer: Union[Group, TileGrid]) -> None:
93         """Append a layer to the group. It will be drawn
94         above other layers.
95         """
96         self.insert(len(self._layers), layer)
97
98     def insert(self, index: int, layer: Union[Group, TileGrid]) -> None:
99         """Insert a layer into the group."""
100         if not isinstance(layer, self._supported_types):
101             raise ValueError("Invalid Group Member")
102         if layer._in_group:  # pylint: disable=protected-access
103             raise ValueError("Layer already in a group.")
104         self._layers.insert(index, layer)
105         self._layer_update(index)
106
107     def index(self, layer: Union[Group, TileGrid]) -> int:
108         """Returns the index of the first copy of layer.
109         Raises ValueError if not found.
110         """
111         return self._layers.index(layer)
112
113     def pop(self, index: int = -1) -> Union[Group, TileGrid]:
114         """Remove the ith item and return it."""
115         self._removal_cleanup(index)
116         return self._layers.pop(index)
117
118     def remove(self, layer: Union[Group, TileGrid]) -> None:
119         """Remove the first copy of layer. Raises ValueError
120         if it is not present."""
121         index = self.index(layer)
122         self._layers.pop(index)
123
124     def __bool__(self) -> bool:
125         """Returns if there are any layers"""
126         return len(self._layers) > 0
127
128     def __len__(self) -> int:
129         """Returns the number of layers in a Group"""
130         return len(self._layers)
131
132     def __getitem__(self, index: int) -> Union[Group, TileGrid]:
133         """Returns the value at the given index."""
134         return self._layers[index]
135
136     def __setitem__(self, index: int, value: Union[Group, TileGrid]) -> None:
137         """Sets the value at the given index."""
138         self._removal_cleanup(index)
139         self._layers[index] = value
140         self._layer_update(index)
141
142     def __delitem__(self, index: int) -> None:
143         """Deletes the value at the given index."""
144         del self._layers[index]
145
146     def _fill_area(
147         self, colorspace: Colorspace, area: Area, mask: int, buffer: bytearray
148     ) -> bool:
149         if self._hidden_group:
150             return False
151
152         for layer in self._layers:
153             if isinstance(layer, (Group, TileGrid)):
154                 if layer._fill_area(  # pylint: disable=protected-access
155                     colorspace, area, mask, buffer
156                 ):
157                     return True
158         return False
159
160     def sort(self, key: Callable, reverse: bool) -> None:
161         """Sort the members of the group."""
162         self._layers.sort(key=key, reverse=reverse)
163
164     def _finish_refresh(self):
165         for layer in self._layers:
166             if isinstance(layer, (Group, TileGrid)):
167                 layer._finish_refresh()  # pylint: disable=protected-access
168
169     @property
170     def hidden(self) -> bool:
171         """True when the Group and all of it's layers are not visible. When False, the
172         Group’s layers are visible if they haven't been hidden.
173         """
174         return self._hidden_group
175
176     @hidden.setter
177     def hidden(self, value: bool):
178         if not isinstance(value, (bool, int)):
179             raise ValueError("Expecting a boolean or integer value")
180         self._hidden_group = bool(value)
181
182     @property
183     def scale(self) -> int:
184         """Scales each pixel within the Group in both directions. For example, when
185         scale=2 each pixel will be represented by 2x2 pixels.
186         """
187         return self._scale
188
189     @scale.setter
190     def scale(self, value: int):
191         self._set_scale(value)
192
193     def _set_scale(self, value: int):
194         # This is method allows the scale to be set by this class even when
195         # the scale property is over-ridden by a subclass.
196         if not isinstance(value, int) or value < 1:
197             raise ValueError("Scale must be >= 1")
198         if self._scale != value:
199             parent_scale = self._absolute_transform.scale / self._scale
200             self._absolute_transform.dx = (
201                 self._absolute_transform.dx / self._scale * value
202             )
203             self._absolute_transform.dy = (
204                 self._absolute_transform.dy / self._scale * value
205             )
206             self._absolute_transform.scale = parent_scale * value
207
208             self._scale = value
209             self._update_child_transforms()
210
211     @property
212     def x(self) -> int:
213         """X position of the Group in the parent."""
214         return self._group_x
215
216     @x.setter
217     def x(self, value: int):
218         if not isinstance(value, int):
219             raise ValueError("x must be an integer")
220         if self._group_x != value:
221             if self._absolute_transform.transpose_xy:
222                 dy_value = self._absolute_transform.dy / self._scale
223                 self._absolute_transform.y += dy_value * (value - self._group_x)
224             else:
225                 dx_value = self._absolute_transform.dx / self._scale
226                 self._absolute_transform.x += dx_value * (value - self._group_x)
227             self._group_x = value
228             self._update_child_transforms()
229
230     @property
231     def y(self) -> int:
232         """Y position of the Group in the parent."""
233         return self._group_y
234
235     @y.setter
236     def y(self, value: int):
237         if not isinstance(value, int):
238             raise ValueError("y must be an integer")
239         if self._group_y != value:
240             if self._absolute_transform.transpose_xy:
241                 dx_value = self._absolute_transform.dx / self._scale
242                 self._absolute_transform.x += dx_value * (value - self._group_y)
243             else:
244                 dy_value = self._absolute_transform.dy / self._scale
245                 self._absolute_transform.y += dy_value * (value - self._group_y)
246             self._group_y = value
247             self._update_child_transforms()