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