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