]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_group.py
Fewer bugs, more code, shape done
[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._item_removed = False
56         self._absolute_transform = TransformStruct(0, 0, 1, 1, 1, False, False, False)
57         self._set_scale(scale)  # Set the scale via the setter
58
59     def _update_transform(self, parent_transform):
60         """Update the parent transform and child transforms"""
61         self._in_group = parent_transform is not None
62         if self._in_group:
63             x = self._group_x
64             y = self._group_y
65             if parent_transform.transpose_xy:
66                 x, y = y, x
67             self._absolute_transform.x = parent_transform.x + parent_transform.dx * x
68             self._absolute_transform.y = parent_transform.y + parent_transform.dy * y
69             self._absolute_transform.dx = parent_transform.dx * self._scale
70             self._absolute_transform.dy = parent_transform.dy * self._scale
71             self._absolute_transform.transpose_xy = parent_transform.transpose_xy
72             self._absolute_transform.mirror_x = parent_transform.mirror_x
73             self._absolute_transform.mirror_y = parent_transform.mirror_y
74             self._absolute_transform.scale = parent_transform.scale * self._scale
75         self._update_child_transforms()
76
77     def _update_child_transforms(self):
78         # pylint: disable=protected-access
79         if self._in_group:
80             for layer in self._layers:
81                 layer._update_transform(self._absolute_transform)
82
83     def _removal_cleanup(self, index):
84         # pylint: disable=protected-access
85         layer = self._layers[index]
86         layer._update_transform(None)
87
88     def _layer_update(self, index):
89         # pylint: disable=protected-access
90         layer = self._layers[index]
91         layer._update_transform(self._absolute_transform)
92
93     def append(self, layer: Union[Group, TileGrid]) -> None:
94         """Append a layer to the group. It will be drawn
95         above other layers.
96         """
97         self.insert(len(self._layers), layer)
98
99     def insert(self, index: int, layer: Union[Group, TileGrid]) -> None:
100         """Insert a layer into the group."""
101         if not isinstance(layer, self._supported_types):
102             raise ValueError("Invalid Group Member")
103         if layer._in_group:  # pylint: disable=protected-access
104             raise ValueError("Layer already in a group.")
105         self._layers.insert(index, layer)
106         self._layer_update(index)
107
108     def index(self, layer: Union[Group, TileGrid]) -> int:
109         """Returns the index of the first copy of layer.
110         Raises ValueError if not found.
111         """
112         return self._layers.index(layer)
113
114     def pop(self, index: int = -1) -> Union[Group, TileGrid]:
115         """Remove the ith item and return it."""
116         self._removal_cleanup(index)
117         return self._layers.pop(index)
118
119     def remove(self, layer: Union[Group, TileGrid]) -> None:
120         """Remove the first copy of layer. Raises ValueError
121         if it is not present."""
122         index = self.index(layer)
123         self._layers.pop(index)
124
125     def __bool__(self) -> bool:
126         """Returns if there are any layers"""
127         return len(self._layers) > 0
128
129     def __len__(self) -> int:
130         """Returns the number of layers in a Group"""
131         return len(self._layers)
132
133     def __getitem__(self, index: int) -> Union[Group, TileGrid]:
134         """Returns the value at the given index."""
135         return self._layers[index]
136
137     def __setitem__(self, index: int, value: Union[Group, TileGrid]) -> None:
138         """Sets the value at the given index."""
139         self._removal_cleanup(index)
140         self._layers[index] = value
141         self._layer_update(index)
142
143     def __delitem__(self, index: int) -> None:
144         """Deletes the value at the given index."""
145         del self._layers[index]
146
147     def _fill_area(
148         self, colorspace: Colorspace, area: Area, mask: int, buffer: bytearray
149     ) -> bool:
150         if self._hidden_group:
151             return False
152
153         for layer in self._layers:
154             if isinstance(layer, (Group, TileGrid)):
155                 if layer._fill_area(  # pylint: disable=protected-access
156                     colorspace, area, mask, buffer
157                 ):
158                     return True
159         return False
160
161     def sort(self, key: Callable, reverse: bool) -> None:
162         """Sort the members of the group."""
163         self._layers.sort(key=key, reverse=reverse)
164
165     def _finish_refresh(self):
166         for layer in self._layers:
167             if isinstance(layer, (Group, TileGrid)):
168                 layer._finish_refresh()  # pylint: disable=protected-access
169
170     def _get_refresh_areas(self, areas: list[Area]) -> None:
171         for layer in self._layers:
172             if isinstance(layer, (Group, TileGrid)):
173                 if not layer.hidden:
174                     layer._get_refresh_areas(areas)  # pylint: disable=protected-access
175
176     @property
177     def hidden(self) -> bool:
178         """True when the Group and all of it's layers are not visible. When False, the
179         Group’s layers are visible if they haven't been hidden.
180         """
181         return self._hidden_group
182
183     @hidden.setter
184     def hidden(self, value: bool):
185         if not isinstance(value, (bool, int)):
186             raise ValueError("Expecting a boolean or integer value")
187         self._hidden_group = bool(value)
188
189     @property
190     def scale(self) -> int:
191         """Scales each pixel within the Group in both directions. For example, when
192         scale=2 each pixel will be represented by 2x2 pixels.
193         """
194         return self._scale
195
196     @scale.setter
197     def scale(self, value: int):
198         self._set_scale(value)
199
200     def _set_scale(self, value: int):
201         # This is method allows the scale to be set by this class even when
202         # the scale property is over-ridden by a subclass.
203         if not isinstance(value, int) or value < 1:
204             raise ValueError("Scale must be >= 1")
205         if self._scale != value:
206             parent_scale = self._absolute_transform.scale / self._scale
207             self._absolute_transform.dx = (
208                 self._absolute_transform.dx / self._scale * value
209             )
210             self._absolute_transform.dy = (
211                 self._absolute_transform.dy / self._scale * value
212             )
213             self._absolute_transform.scale = parent_scale * value
214
215             self._scale = value
216             self._update_child_transforms()
217
218     @property
219     def x(self) -> int:
220         """X position of the Group in the parent."""
221         return self._group_x
222
223     @x.setter
224     def x(self, value: int):
225         if not isinstance(value, int):
226             raise ValueError("x must be an integer")
227         if self._group_x != value:
228             if self._absolute_transform.transpose_xy:
229                 dy_value = self._absolute_transform.dy / self._scale
230                 self._absolute_transform.y += dy_value * (value - self._group_x)
231             else:
232                 dx_value = self._absolute_transform.dx / self._scale
233                 self._absolute_transform.x += dx_value * (value - self._group_x)
234             self._group_x = value
235             self._update_child_transforms()
236
237     @property
238     def y(self) -> int:
239         """Y position of the Group in the parent."""
240         return self._group_y
241
242     @y.setter
243     def y(self, value: int):
244         if not isinstance(value, int):
245             raise ValueError("y must be an integer")
246         if self._group_y != value:
247             if self._absolute_transform.transpose_xy:
248                 dx_value = self._absolute_transform.dx / self._scale
249                 self._absolute_transform.x += dx_value * (value - self._group_y)
250             else:
251                 dy_value = self._absolute_transform.dy / self._scale
252                 self._absolute_transform.y += dy_value * (value - self._group_y)
253             self._group_y = value
254             self._update_child_transforms()