1 # SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
3 # SPDX-License-Identifier: MIT
7 ================================================================================
11 **Software and Dependencies:**
14 https://github.com/adafruit/Adafruit_Blinka/releases
16 * Author(s): Melissa LeBlanc-Williams
20 from __future__ import annotations
21 from typing import Union, Callable
22 from circuitpython_typing import WriteableBuffer
23 from vectorio._rectangle import _VectorShape
24 from ._structs import TransformStruct
25 from ._tilegrid import TileGrid
26 from ._colorspace import Colorspace
27 from ._area import Area
29 __version__ = "0.0.0+auto.0"
30 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
34 # pylint: disable=too-many-instance-attributes
36 Manage a group of sprites and groups and how they are inter-related.
38 Create a Group of a given scale. Scale is in one dimension. For example, scale=2
39 leads to a layer's pixel being 2x2 pixels when in the group.
42 def __init__(self, *, scale: int = 1, x: int = 0, y: int = 0):
44 :param int scale: Scale of layer pixels in one dimension.
45 :param int x: Initial x position within the parent.
46 :param int y: Initial y position within the parent.
49 if not isinstance(scale, int) or scale < 1:
50 raise ValueError("Scale must be >= 1")
51 self._scale = 1 # Use the setter below to actually set the scale
55 self._hidden_group = False
56 self._hidden_by_parent = False
58 self._supported_types = (TileGrid, Group, _VectorShape)
59 self._in_group = False
60 self._item_removed = False
61 self._absolute_transform = TransformStruct(0, 0, 1, 1, 1, False, False, False)
62 self._set_scale(scale) # Set the scale via the setter
64 def _update_transform(self, parent_transform):
65 """Update the parent transform and child transforms"""
66 self._in_group = parent_transform is not None
70 if parent_transform.transpose_xy:
72 self._absolute_transform.x = parent_transform.x + parent_transform.dx * x
73 self._absolute_transform.y = parent_transform.y + parent_transform.dy * y
74 self._absolute_transform.dx = parent_transform.dx * self._scale
75 self._absolute_transform.dy = parent_transform.dy * self._scale
76 self._absolute_transform.transpose_xy = parent_transform.transpose_xy
77 self._absolute_transform.mirror_x = parent_transform.mirror_x
78 self._absolute_transform.mirror_y = parent_transform.mirror_y
79 self._absolute_transform.scale = parent_transform.scale * self._scale
80 self._update_child_transforms()
82 def _update_child_transforms(self):
83 # pylint: disable=protected-access
85 for layer in self._layers:
86 layer._update_transform(self._absolute_transform)
88 def _removal_cleanup(self, index):
89 # pylint: disable=protected-access
90 layer = self._layers[index]
91 layer._update_transform(None)
93 def _layer_update(self, index):
94 # pylint: disable=protected-access
95 layer = self._layers[index]
96 layer._update_transform(self._absolute_transform)
98 def append(self, layer: Union[Group, TileGrid, _VectorShape]) -> None:
99 """Append a layer to the group. It will be drawn
102 self.insert(len(self._layers), layer)
104 def insert(self, index: int, layer: Union[Group, TileGrid, _VectorShape]) -> None:
105 """Insert a layer into the group."""
106 if not isinstance(layer, self._supported_types):
107 raise ValueError("Invalid Group Member")
108 if isinstance(layer, (Group, TileGrid)):
109 if layer._in_group: # pylint: disable=protected-access
110 raise ValueError("Layer already in a group.")
111 self._layers.insert(index, layer)
112 self._layer_update(index)
114 def index(self, layer: Union[Group, TileGrid, _VectorShape]) -> int:
115 """Returns the index of the first copy of layer.
116 Raises ValueError if not found.
118 return self._layers.index(layer)
120 def pop(self, index: int = -1) -> Union[Group, TileGrid, _VectorShape]:
121 """Remove the ith item and return it."""
122 self._removal_cleanup(index)
123 return self._layers.pop(index)
125 def remove(self, layer: Union[Group, TileGrid, _VectorShape]) -> None:
126 """Remove the first copy of layer. Raises ValueError
127 if it is not present."""
128 index = self.index(layer)
129 self._layers.pop(index)
131 def __bool__(self) -> bool:
132 """Returns if there are any layers"""
133 return len(self._layers) > 0
135 def __len__(self) -> int:
136 """Returns the number of layers in a Group"""
137 return len(self._layers)
139 def __getitem__(self, index: int) -> Union[Group, TileGrid, _VectorShape]:
140 """Returns the value at the given index."""
141 return self._layers[index]
144 self, index: int, value: Union[Group, TileGrid, _VectorShape]
146 """Sets the value at the given index."""
147 self._removal_cleanup(index)
148 self._layers[index] = value
149 self._layer_update(index)
151 def __delitem__(self, index: int) -> None:
152 """Deletes the value at the given index."""
153 del self._layers[index]
157 colorspace: Colorspace,
159 mask: WriteableBuffer,
160 buffer: WriteableBuffer,
162 if not self._hidden_group:
163 for layer in reversed(self._layers):
164 if isinstance(layer, (Group, TileGrid, _VectorShape)):
165 if layer._fill_area( # pylint: disable=protected-access
166 colorspace, area, mask, buffer
171 def sort(self, key: Callable, reverse: bool) -> None:
172 """Sort the members of the group."""
173 self._layers.sort(key=key, reverse=reverse)
175 def _finish_refresh(self):
176 for layer in reversed(self._layers):
177 if isinstance(layer, (Group, TileGrid, _VectorShape)):
178 layer._finish_refresh() # pylint: disable=protected-access
180 def _get_refresh_areas(self, areas: list[Area]) -> None:
181 # pylint: disable=protected-access
182 for layer in reversed(self._layers):
183 if isinstance(layer, (Group, _VectorShape)):
184 layer._get_refresh_areas(areas)
185 elif isinstance(layer, TileGrid):
186 if not layer._get_rendered_hidden():
187 layer._get_refresh_areas(areas)
189 def _set_hidden(self, hidden: bool) -> None:
190 if self._hidden_group == hidden:
192 self._hidden_group = hidden
193 if self._hidden_by_parent:
195 for layer in self._layers:
196 if isinstance(layer, (Group, TileGrid)):
197 layer._set_hidden_by_parent(hidden) # pylint: disable=protected-access
198 elif isinstance(layer, _VectorShape):
199 layer._shape_set_dirty() # pylint: disable=protected-access
201 def _set_hidden_by_parent(self, hidden: bool) -> None:
202 if self._hidden_by_parent == hidden:
204 self._hidden_by_parent = hidden
205 if self._hidden_group:
207 for layer in self._layers:
208 if isinstance(layer, (Group, TileGrid)):
209 layer._set_hidden_by_parent(hidden) # pylint: disable=protected-access
210 elif isinstance(layer, _VectorShape):
211 layer._shape_set_dirty() # pylint: disable=protected-access
214 def hidden(self) -> bool:
215 """True when the Group and all of it's layers are not visible. When False, the
216 Group’s layers are visible if they haven't been hidden.
218 return self._hidden_group
221 def hidden(self, value: bool) -> None:
222 if not isinstance(value, (bool, int)):
223 raise ValueError("Expecting a boolean or integer value")
225 self._set_hidden(value)
228 def scale(self) -> int:
229 """Scales each pixel within the Group in both directions. For example, when
230 scale=2 each pixel will be represented by 2x2 pixels.
235 def scale(self, value: int):
236 self._set_scale(value)
238 def _set_scale(self, value: int):
239 # This is method allows the scale to be set by this class even when
240 # the scale property is over-ridden by a subclass.
241 if not isinstance(value, int) or value < 1:
242 raise ValueError("Scale must be >= 1")
243 if self._scale != value:
244 parent_scale = self._absolute_transform.scale / self._scale
245 self._absolute_transform.dx = (
246 self._absolute_transform.dx / self._scale * value
248 self._absolute_transform.dy = (
249 self._absolute_transform.dy / self._scale * value
251 self._absolute_transform.scale = parent_scale * value
254 self._update_child_transforms()
258 """X position of the Group in the parent."""
262 def x(self, value: int):
263 if not isinstance(value, int):
264 raise ValueError("x must be an integer")
265 if self._group_x != value:
266 if self._absolute_transform.transpose_xy:
267 dy_value = self._absolute_transform.dy / self._scale
268 self._absolute_transform.y += dy_value * (value - self._group_x)
270 dx_value = self._absolute_transform.dx / self._scale
271 self._absolute_transform.x += dx_value * (value - self._group_x)
272 self._group_x = value
273 self._update_child_transforms()
277 """Y position of the Group in the parent."""
281 def y(self, value: int):
282 if not isinstance(value, int):
283 raise ValueError("y must be an integer")
284 if self._group_y != value:
285 if self._absolute_transform.transpose_xy:
286 dx_value = self._absolute_transform.dx / self._scale
287 self._absolute_transform.x += dx_value * (value - self._group_y)
289 dy_value = self._absolute_transform.dy / self._scale
290 self._absolute_transform.y += dy_value * (value - self._group_y)
291 self._group_y = value
292 self._update_child_transforms()
295 circuitpython_splash = Group(scale=2, x=0, y=0)