]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_group.py
Merge pull request #75 from makermelissa/add-typing
[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: 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(self, buffer):
148         if self._hidden_group:
149             return
150
151         for layer in self._layers:
152             if isinstance(layer, (Group, TileGrid)):
153                 layer._fill_area(buffer)  # pylint: disable=protected-access
154
155     def sort(self, key: Callable, reverse: bool) -> None:
156         """Sort the members of the group."""
157         self._layers.sort(key=key, reverse=reverse)
158
159     @property
160     def hidden(self) -> bool:
161         """True when the Group and all of it's layers are not visible. When False, the
162         Group’s layers are visible if they haven't been hidden.
163         """
164         return self._hidden_group
165
166     @hidden.setter
167     def hidden(self, value: bool):
168         if not isinstance(value, (bool, int)):
169             raise ValueError("Expecting a boolean or integer value")
170         self._hidden_group = bool(value)
171
172     @property
173     def scale(self) -> int:
174         """Scales each pixel within the Group in both directions. For example, when
175         scale=2 each pixel will be represented by 2x2 pixels.
176         """
177         return self._scale
178
179     @scale.setter
180     def scale(self, value: int):
181         self._set_scale(value)
182
183     def _set_scale(self, value: int):
184         # This is method allows the scale to be set by this class even when
185         # the scale property is over-ridden by a subclass.
186         if not isinstance(value, int) or value < 1:
187             raise ValueError("Scale must be >= 1")
188         if self._scale != value:
189             parent_scale = self._absolute_transform.scale / self._scale
190             self._absolute_transform.dx = (
191                 self._absolute_transform.dx / self._scale * value
192             )
193             self._absolute_transform.dy = (
194                 self._absolute_transform.dy / self._scale * value
195             )
196             self._absolute_transform.scale = parent_scale * value
197
198             self._scale = value
199             self._update_child_transforms()
200
201     @property
202     def x(self) -> int:
203         """X position of the Group in the parent."""
204         return self._group_x
205
206     @x.setter
207     def x(self, value: int):
208         if not isinstance(value, int):
209             raise ValueError("x must be an integer")
210         if self._group_x != value:
211             if self._absolute_transform.transpose_xy:
212                 dy_value = self._absolute_transform.dy / self._scale
213                 self._absolute_transform.y += dy_value * (value - self._group_x)
214             else:
215                 dx_value = self._absolute_transform.dx / self._scale
216                 self._absolute_transform.x += dx_value * (value - self._group_x)
217             self._group_x = value
218             self._update_child_transforms()
219
220     @property
221     def y(self) -> int:
222         """Y position of the Group in the parent."""
223         return self._group_y
224
225     @y.setter
226     def y(self, value: int):
227         if not isinstance(value, int):
228             raise ValueError("y must be an integer")
229         if self._group_y != value:
230             if self._absolute_transform.transpose_xy:
231                 dx_value = self._absolute_transform.dx / self._scale
232                 self._absolute_transform.x += dx_value * (value - self._group_y)
233             else:
234                 dy_value = self._absolute_transform.dy / self._scale
235                 self._absolute_transform.y += dy_value * (value - self._group_y)
236             self._group_y = value
237             self._update_child_transforms()