]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_group.py
abfe80795a5370460fbdecb4fe26256e16f26c14
[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 recordclass import recordclass
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 Transform = recordclass("Transform", "x y dx dy scale transpose_xy mirror_x mirror_y")
30
31
32 class Group:
33     """
34     Manage a group of sprites and groups and how they are inter-related.
35
36     Create a Group of a given scale. Scale is in one dimension. For example, scale=2
37     leads to a layer's pixel being 2x2 pixels when in the group.
38     """
39
40     def __init__(self, *, scale: int = 1, x: int = 0, y: int = 0):
41         """
42         :param int scale: Scale of layer pixels in one dimension.
43         :param int x: Initial x position within the parent.
44         :param int y: Initial y position within the parent.
45         """
46
47         if not isinstance(scale, int) or scale < 1:
48             raise ValueError("Scale must be >= 1")
49         self._scale = 1  # Use the setter below to actually set the scale
50         self._group_x = x
51         self._group_y = y
52         self._hidden_group = False
53         self._layers = []
54         self._supported_types = (TileGrid, Group)
55         self.in_group = False
56         self._absolute_transform = Transform(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:
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=-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) -> 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 __len__(self) -> int:
126         """Returns the number of layers in a Group"""
127         return len(self._layers)
128
129     def __getitem__(self, index) -> Union[Group, TileGrid]:
130         """Returns the value at the given index."""
131         return self._layers[index]
132
133     def __setitem__(self, index, value) -> None:
134         """Sets the value at the given index."""
135         self._removal_cleanup(index)
136         self._layers[index] = value
137         self._layer_update(index)
138
139     def __delitem__(self, index) -> None:
140         """Deletes the value at the given index."""
141         del self._layers[index]
142
143     def _fill_area(self, buffer):
144         if self._hidden_group:
145             return
146
147         for layer in self._layers:
148             if isinstance(layer, (Group, TileGrid)):
149                 layer._fill_area(buffer)  # pylint: disable=protected-access
150
151     def sort(self, key: Callable, reverse: bool) -> None:
152         """Sort the members of the group."""
153         self._layers.sort(key=key, reverse=reverse)
154
155     @property
156     def hidden(self) -> bool:
157         """True when the Group and all of it’s layers are not visible. When False, the
158         Group’s layers are visible if they haven’t been hidden.
159         """
160         return self._hidden_group
161
162     @hidden.setter
163     def hidden(self, value: bool):
164         if not isinstance(value, (bool, int)):
165             raise ValueError("Expecting a boolean or integer value")
166         self._hidden_group = bool(value)
167
168     @property
169     def scale(self) -> int:
170         """Scales each pixel within the Group in both directions. For example, when
171         scale=2 each pixel will be represented by 2x2 pixels.
172         """
173         return self._scale
174
175     @scale.setter
176     def scale(self, value: int):
177         self._set_scale(value)
178
179     def _set_scale(self, value: int):
180         # This is method allows the scale to be set by this class even when
181         # the scale property is over-ridden by a subclass.
182         if not isinstance(value, int) or value < 1:
183             raise ValueError("Scale must be >= 1")
184         if self._scale != value:
185             parent_scale = self._absolute_transform.scale / self._scale
186             self._absolute_transform.dx = (
187                 self._absolute_transform.dx / self._scale * value
188             )
189             self._absolute_transform.dy = (
190                 self._absolute_transform.dy / self._scale * value
191             )
192             self._absolute_transform.scale = parent_scale * value
193
194             self._scale = value
195             self._update_child_transforms()
196
197     @property
198     def x(self) -> int:
199         """X position of the Group in the parent."""
200         return self._group_x
201
202     @x.setter
203     def x(self, value: int):
204         if not isinstance(value, int):
205             raise ValueError("x must be an integer")
206         if self._group_x != value:
207             if self._absolute_transform.transpose_xy:
208                 dy_value = self._absolute_transform.dy / self._scale
209                 self._absolute_transform.y += dy_value * (value - self._group_x)
210             else:
211                 dx_value = self._absolute_transform.dx / self._scale
212                 self._absolute_transform.x += dx_value * (value - self._group_x)
213             self._group_x = value
214             self._update_child_transforms()
215
216     @property
217     def y(self) -> int:
218         """Y position of the Group in the parent."""
219         return self._group_y
220
221     @y.setter
222     def y(self, value: int):
223         if not isinstance(value, int):
224             raise ValueError("y must be an integer")
225         if self._group_y != value:
226             if self._absolute_transform.transpose_xy:
227                 dx_value = self._absolute_transform.dx / self._scale
228                 self._absolute_transform.x += dx_value * (value - self._group_y)
229             else:
230                 dy_value = self._absolute_transform.dy / self._scale
231                 self._absolute_transform.y += dy_value * (value - self._group_y)
232             self._group_y = value
233             self._update_child_transforms()