]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blobdiff - displayio/_tilegrid.py
Fix indexed bitmaps by correcting argument order at call
[hackapet/Adafruit_Blinka_Displayio.git] / displayio / _tilegrid.py
index 0cfc6d3d304bbe47dfb724f460406cf098e12535..a3f21eb53286aa835fdd9aa6b03b2a5000f8bacd 100644 (file)
@@ -19,12 +19,16 @@ displayio for Blinka
 
 import struct
 from typing import Union, Optional, Tuple
+from circuitpython_typing import WriteableBuffer
 from ._bitmap import Bitmap
 from ._colorconverter import ColorConverter
 from ._ondiskbitmap import OnDiskBitmap
-from ._shape import Shape
 from ._palette import Palette
-from ._structs import TransformStruct, InputPixelStruct, OutputPixelStruct
+from ._structs import (
+    InputPixelStruct,
+    OutputPixelStruct,
+    null_transform,
+)
 from ._colorspace import Colorspace
 from ._area import Area
 
@@ -33,7 +37,7 @@ __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
 
 
 class TileGrid:
-    # pylint: disable=too-many-instance-attributes
+    # pylint: disable=too-many-instance-attributes, too-many-statements
     """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
     grids can share bitmaps and pixel shaders.
 
@@ -42,7 +46,7 @@ class TileGrid:
 
     def __init__(
         self,
-        bitmap: Union[Bitmap, OnDiskBitmap, Shape],
+        bitmap: Union[Bitmap, OnDiskBitmap],
         *,
         pixel_shader: Union[ColorConverter, Palette],
         width: int = 1,
@@ -59,7 +63,7 @@ class TileGrid:
 
         tile_width and tile_height match the height of the bitmap by default.
         """
-        if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)):
+        if not isinstance(bitmap, (Bitmap, OnDiskBitmap)):
             raise ValueError("Unsupported Bitmap type")
         self._bitmap = bitmap
         bitmap_width = bitmap.width
@@ -73,6 +77,9 @@ class TileGrid:
         if isinstance(self._pixel_shader, ColorConverter):
             self._pixel_shader._rgba = True  # pylint: disable=protected-access
         self._hidden_tilegrid = False
+        self._hidden_by_parent = False
+        self._rendered_hidden = False
+        self._name = "Tilegrid"
         self._x = x
         self._y = y
         self._width_in_tiles = width
@@ -98,36 +105,51 @@ class TileGrid:
             raise ValueError("Default Tile is out of range")
         self._pixel_width = width * tile_width
         self._pixel_height = height * tile_height
-        self._tiles = (self._width_in_tiles * self._height_in_tiles) * [default_tile]
+        self._tiles = bytearray(
+            (self._width_in_tiles * self._height_in_tiles) * [default_tile]
+        )
         self._in_group = False
-        self._absolute_transform = TransformStruct(0, 0, 1, 1, 1, False, False, False)
+        self._absolute_transform = None
         self._current_area = Area(0, 0, self._pixel_width, self._pixel_height)
+        self._dirty_area = Area(0, 0, 0, 0)
+        self._previous_area = Area(0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF)
         self._moved = False
+        self._full_change = True
+        self._partial_change = True
         self._bitmap_width_in_tiles = bitmap_width // tile_width
         self._tiles_in_bitmap = self._bitmap_width_in_tiles * (
             bitmap_height // tile_height
         )
-        self.inline_tiles = False  # We have plenty of memory
+        self._needs_refresh = True
 
     def _update_transform(self, absolute_transform):
         """Update the parent transform and child transforms"""
+        self._in_group = absolute_transform is not None
         self._absolute_transform = absolute_transform
         if self._absolute_transform is not None:
+            self._moved = True
             self._update_current_x()
             self._update_current_y()
 
     def _update_current_x(self):
+        self._needs_refresh = True
         if self._transpose_xy:
             width = self._pixel_height
         else:
             width = self._pixel_width
-        if self._absolute_transform.transpose_xy:
+
+        absolute_transform = (
+            null_transform
+            if self._absolute_transform is None
+            else self._absolute_transform
+        )
+
+        if absolute_transform.transpose_xy:
             self._current_area.y1 = (
-                self._absolute_transform.y + self._absolute_transform.dy * self._x
+                absolute_transform.y + absolute_transform.dy * self._x
             )
-            self._current_area.y2 = (
-                self._absolute_transform.y
-                + self._absolute_transform.dy * (self._x + width)
+            self._current_area.y2 = absolute_transform.y + absolute_transform.dy * (
+                self._x + width
             )
             if self._current_area.y2 < self._current_area.y1:
                 self._current_area.y1, self._current_area.y2 = (
@@ -136,11 +158,10 @@ class TileGrid:
                 )
         else:
             self._current_area.x1 = (
-                self._absolute_transform.x + self._absolute_transform.dx * self._x
+                absolute_transform.x + absolute_transform.dx * self._x
             )
-            self._current_area.x2 = (
-                self._absolute_transform.x
-                + self._absolute_transform.dx * (self._x + width)
+            self._current_area.x2 = absolute_transform.x + absolute_transform.dx * (
+                self._x + width
             )
             if self._current_area.x2 < self._current_area.x1:
                 self._current_area.x1, self._current_area.x2 = (
@@ -149,17 +170,24 @@ class TileGrid:
                 )
 
     def _update_current_y(self):
+        self._needs_refresh = True
         if self._transpose_xy:
             height = self._pixel_width
         else:
             height = self._pixel_height
-        if self._absolute_transform.transpose_xy:
+
+        absolute_transform = (
+            null_transform
+            if self._absolute_transform is None
+            else self._absolute_transform
+        )
+
+        if absolute_transform.transpose_xy:
             self._current_area.x1 = (
-                self._absolute_transform.x + self._absolute_transform.dx * self._y
+                absolute_transform.x + absolute_transform.dx * self._y
             )
-            self._current_area.x2 = (
-                self._absolute_transform.x
-                + self._absolute_transform.dx * (self._y + height)
+            self._current_area.x2 = absolute_transform.x + absolute_transform.dx * (
+                self._y + height
             )
             if self._current_area.x2 < self._current_area.x1:
                 self._current_area.x1, self._current_area.x2 = (
@@ -168,11 +196,10 @@ class TileGrid:
                 )
         else:
             self._current_area.y1 = (
-                self._absolute_transform.y + self._absolute_transform.dy * self._y
+                absolute_transform.y + absolute_transform.dy * self._y
             )
-            self._current_area.y2 = (
-                self._absolute_transform.y
-                + self._absolute_transform.dy * (self._y + height)
+            self._current_area.y2 = absolute_transform.y + absolute_transform.dy * (
+                self._y + height
             )
             if self._current_area.y2 < self._current_area.y1:
                 self._current_area.y1, self._current_area.y2 = (
@@ -202,7 +229,11 @@ class TileGrid:
         image.putalpha(alpha.convert("L"))
 
     def _fill_area(
-        self, colorspace: Colorspace, area: Area, mask: bytearray, buffer: bytearray
+        self,
+        colorspace: Colorspace,
+        area: Area,
+        mask: WriteableBuffer,
+        buffer: WriteableBuffer,
     ) -> bool:
         """Draw onto the image"""
         # pylint: disable=too-many-locals,too-many-branches,too-many-statements
@@ -210,12 +241,17 @@ class TileGrid:
         # If no tiles are present we have no impact
         tiles = self._tiles
 
-        if self._hidden_tilegrid:
+        if tiles is None or len(tiles) == 0:
             return False
 
-        overlap = Area()
-        if not self._current_area.compute_overlap(area, overlap):
+        if self._hidden_tilegrid or self._hidden_by_parent:
             return False
+        overlap = Area()  # area, current_area, overlap
+        if not area.compute_overlap(self._current_area, overlap):
+            return False
+        # else:
+        #    print("Checking", area.x1, area.y1, area.x2, area.y2)
+        #    print("Overlap", overlap.x1, overlap.y1, overlap.x2, overlap.y2)
 
         if self._bitmap.width <= 0 or self._bitmap.height <= 0:
             return False
@@ -230,12 +266,14 @@ class TileGrid:
 
         start = 0
         if (self._absolute_transform.dx < 0) != flip_x:
-            start += (area.width() - 1) * x_stride
+            start += (area.x2 - area.x1 - 1) * x_stride
             x_stride *= -1
         if (self._absolute_transform.dy < 0) != flip_y:
-            start += (area.height() - 1) * y_stride
+            start += (area.y2 - area.y1 - 1) * y_stride
             y_stride *= -1
 
+        # Track if this layer finishes filling in the given area. We can ignore any remaining
+        # layers at that point.
         full_coverage = area == overlap
 
         transformed = Area()
@@ -262,6 +300,7 @@ class TileGrid:
         else:
             y_shift = overlap.y1 - area.y1
 
+        # This untransposes x and y so it aligns with bitmap rows
         if self._transpose_xy != self._absolute_transform.transpose_xy:
             x_stride, y_stride = y_stride, x_stride
             x_shift, y_shift = y_shift, x_shift
@@ -276,6 +315,7 @@ class TileGrid:
             )  # In Pixels
             local_y = input_pixel.y // self._absolute_transform.scale
             for input_pixel.x in range(start_x, end_x):
+                # Compute the destination pixel in the buffer and mask based on the transformations
                 offset = (
                     row_start + (input_pixel.x - start_x + x_shift) * x_stride
                 )  # In Pixels
@@ -298,9 +338,19 @@ class TileGrid:
                     input_pixel.tile // self._bitmap_width_in_tiles
                 ) * self._tile_height + local_y % self._tile_height
 
-                input_pixel.pixel = self.bitmap[input_pixel.tile_x, input_pixel.tile_y]
-                output_pixel.opaque = True
+                output_pixel.pixel = 0
+                input_pixel.pixel = 0
+
+                # We always want to read bitmap pixels by row first and then transpose into
+                # the destination buffer because most bitmaps are row associated.
+                if isinstance(self._bitmap, (Bitmap, OnDiskBitmap)):
+                    input_pixel.pixel = (
+                        self._bitmap._get_pixel(  # pylint: disable=protected-access
+                            input_pixel.tile_x, input_pixel.tile_y
+                        )
+                    )
 
+                output_pixel.opaque = True
                 if self._pixel_shader is None:
                     output_pixel.pixel = input_pixel.pixel
                 elif isinstance(self._pixel_shader, Palette):
@@ -317,19 +367,21 @@ class TileGrid:
                 else:
                     mask[offset // 32] |= 1 << (offset % 32)
                     if colorspace.depth == 16:
-                        buffer = (
-                            buffer[:offset]
-                            + struct.pack("H", output_pixel.pixel)
-                            + buffer[offset + 2 :]
+                        struct.pack_into(
+                            "H",
+                            buffer.cast("B"),
+                            offset * 2,
+                            output_pixel.pixel,
                         )
                     elif colorspace.depth == 32:
-                        buffer = (
-                            buffer[:offset]
-                            + struct.pack("I", output_pixel.pixel)
-                            + buffer[offset + 4 :]
+                        struct.pack_into(
+                            "I",
+                            buffer.cast("B"),
+                            offset * 4,
+                            output_pixel.pixel,
                         )
                     elif colorspace.depth == 8:
-                        buffer[offset] = output_pixel.pixel & 0xFF
+                        buffer.cast("B")[offset] = output_pixel.pixel & 0xFF
                     elif colorspace.depth < 8:
                         # Reorder the offsets to pack multiple rows into
                         # a byte (meaning they share a column).
@@ -341,18 +393,179 @@ class TileGrid:
                             # even if we multiply it back out
                             offset = (
                                 col * pixels_per_byte
-                                + (row // pixels_per_byte) * width
+                                + (row // pixels_per_byte) * pixels_per_byte * width
                                 + (row % pixels_per_byte)
                             )
                         shift = (offset % pixels_per_byte) * colorspace.depth
                         if colorspace.reverse_pixels_in_byte:
                             # Reverse the shift by subtracting it from the leftmost shift
                             shift = (pixels_per_byte - 1) * colorspace.depth - shift
-                        buffer[offset // pixels_per_byte] |= output_pixel.pixel << shift
+                        buffer.cast("B")[offset // pixels_per_byte] |= (
+                            output_pixel.pixel << shift
+                        )
+
         return full_coverage
 
     def _finish_refresh(self):
-        pass
+        if not self._needs_refresh:
+            first_draw = self._previous_area.x1 == self._previous_area.x2
+            hidden = self._hidden_tilegrid or self._hidden_by_parent
+            if not first_draw and hidden:
+                self._previous_area.x2 = self._previous_area.x1
+            elif self._moved or first_draw:
+                self._current_area.copy_into(self._previous_area)
+
+            self._moved = False
+            self._full_change = False
+            self._partial_change = False
+            if isinstance(self._pixel_shader, (Palette, ColorConverter)):
+                self._pixel_shader._finish_refresh()  # pylint: disable=protected-access
+            if isinstance(self._bitmap, Bitmap):
+                self._bitmap._finish_refresh()  # pylint: disable=protected-access
+
+    def _get_refresh_areas(self, areas: list[Area]) -> None:
+        # pylint: disable=invalid-name, too-many-branches, too-many-statements
+        first_draw = self._previous_area.x1 == self._previous_area.x2
+        hidden = self._hidden_tilegrid or self._hidden_by_parent
+
+        # Check hidden first because it trumps all other changes
+        if hidden:
+            self._rendered_hidden = True
+            if not first_draw:
+                areas.append(self._previous_area)
+            self._needs_refresh = False
+            return
+        if self._moved and not first_draw:
+            self._previous_area.union(self._current_area, self._dirty_area)
+            if self._dirty_area.size() < 2 * self._pixel_width * self._pixel_height:
+                areas.append(self._dirty_area)
+                self._needs_refresh = False
+                return
+            areas.append(self._current_area)
+            areas.append(self._previous_area)
+            self._needs_refresh = False
+            return
+
+        tail = areas[-1] if areas else None
+        # If we have an in-memory bitmap, then check it for modifications
+        if isinstance(self._bitmap, Bitmap):
+            self._bitmap._get_refresh_areas(areas)  # pylint: disable=protected-access
+            refresh_area = areas[-1] if areas else None
+            if refresh_area != tail:
+                # Special case a TileGrid that shows a full bitmap and use its
+                # dirty area. Copy it to ours so we can transform it.
+                if self._tiles_in_bitmap == 1:
+                    refresh_area.copy_into(self._dirty_area)
+                    self._partial_change = True
+                else:
+                    self._full_change = True
+
+        self._full_change = self._full_change or (
+            isinstance(self._pixel_shader, (Palette, ColorConverter))
+            and self._pixel_shader._needs_refresh  # pylint: disable=protected-access
+        )
+        if self._full_change or first_draw:
+            areas.append(self._current_area)
+            self._needs_refresh = False
+            return
+
+        if self._partial_change:
+            x = self._x
+            y = self._y
+            if self._absolute_transform.transpose_xy:
+                x, y = y, x
+            x1 = self._dirty_area.x1
+            x2 = self._dirty_area.x2
+            if self._flip_x:
+                x1 = self._pixel_width - x1
+                x2 = self._pixel_width - x2
+            y1 = self._dirty_area.y1
+            y2 = self._dirty_area.y2
+            if self._flip_y:
+                y1 = self._pixel_height - y1
+                y2 = self._pixel_height - y2
+            if self._transpose_xy != self._absolute_transform.transpose_xy:
+                x1, y1 = y1, x1
+                x2, y2 = y2, x2
+            self._dirty_area.x1 = (
+                self._absolute_transform.x + self._absolute_transform.dx * (x + x1)
+            )
+            self._dirty_area.y1 = (
+                self._absolute_transform.y + self._absolute_transform.dy * (y + y1)
+            )
+            self._dirty_area.x2 = (
+                self._absolute_transform.x + self._absolute_transform.dx * (x + x2)
+            )
+            self._dirty_area.y2 = (
+                self._absolute_transform.y + self._absolute_transform.dy * (y + y2)
+            )
+            if self._dirty_area.y2 < self._dirty_area.y1:
+                self._dirty_area.y1, self._dirty_area.y2 = (
+                    self._dirty_area.y2,
+                    self._dirty_area.y1,
+                )
+            if self._dirty_area.x2 < self._dirty_area.x1:
+                self._dirty_area.x1, self._dirty_area.x2 = (
+                    self._dirty_area.x2,
+                    self._dirty_area.x1,
+                )
+            areas.append(self._dirty_area)
+        self._needs_refresh = False
+
+    def _set_hidden(self, hidden: bool) -> None:
+        self._needs_refresh = True
+        self._hidden_tilegrid = hidden
+        self._rendered_hidden = False
+        if not hidden:
+            self._full_change = True
+
+    def _set_hidden_by_parent(self, hidden: bool) -> None:
+        self._needs_refresh = True
+        self._hidden_by_parent = hidden
+        self._rendered_hidden = False
+        if not hidden:
+            self._full_change = True
+
+    def _get_rendered_hidden(self) -> bool:
+        return self._rendered_hidden
+
+    def _set_all_tiles(self, tile_index: int) -> None:
+        """Set all tiles to the given tile index"""
+        if tile_index >= self._tiles_in_bitmap:
+            raise ValueError("Tile index out of bounds")
+        self._tiles = bytearray(
+            (self._width_in_tiles * self._height_in_tiles) * [tile_index]
+        )
+        self._full_change = True
+
+    def _set_tile(self, x: int, y: int, tile_index: int) -> None:
+        self._needs_refresh = True
+        self._tiles[y * self._width_in_tiles + x] = tile_index
+        temp_area = Area()
+        if not self._partial_change:
+            tile_area = self._dirty_area
+        else:
+            tile_area = temp_area
+        top_x = (x - self._top_left_x) % self._width_in_tiles
+        if top_x < 0:
+            top_x += self._width_in_tiles
+        tile_area.x1 = top_x * self._tile_width
+        tile_area.x2 = tile_area.x1 + self._tile_width
+        top_y = (y - self._top_left_y) % self._height_in_tiles
+        if top_y < 0:
+            top_y += self._height_in_tiles
+        tile_area.y1 = top_y * self._tile_height
+        tile_area.y2 = tile_area.y1 + self._tile_height
+
+        if self._partial_change:
+            self._dirty_area.union(temp_area, self._dirty_area)
+
+        self._partial_change = True
+
+    def _set_top_left(self, x: int, y: int) -> None:
+        self._top_left_x = x
+        self._top_left_y = y
+        self._full_change = True
 
     @property
     def hidden(self) -> bool:
@@ -364,7 +577,8 @@ class TileGrid:
     def hidden(self, value: bool):
         if not isinstance(value, (bool, int)):
             raise ValueError("Expecting a boolean or integer value")
-        self._hidden_tilegrid = bool(value)
+        value = bool(value)
+        self._set_hidden(value)
 
     @property
     def x(self) -> int:
@@ -376,8 +590,10 @@ class TileGrid:
         if not isinstance(value, int):
             raise TypeError("X should be a integer type")
         if self._x != value:
+            self._moved = True
             self._x = value
-            self._update_current_x()
+            if self._absolute_transform is not None:
+                self._update_current_x()
 
     @property
     def y(self) -> int:
@@ -389,8 +605,10 @@ class TileGrid:
         if not isinstance(value, int):
             raise TypeError("Y should be a integer type")
         if self._y != value:
+            self._moved = True
             self._y = value
-            self._update_current_y()
+            if self._absolute_transform is not None:
+                self._update_current_y()
 
     @property
     def flip_x(self) -> bool:
@@ -402,7 +620,9 @@ class TileGrid:
         if not isinstance(value, bool):
             raise TypeError("Flip X should be a boolean type")
         if self._flip_x != value:
+            self._needs_refresh = True
             self._flip_x = value
+            self._full_change = True
 
     @property
     def flip_y(self) -> bool:
@@ -414,7 +634,9 @@ class TileGrid:
         if not isinstance(value, bool):
             raise TypeError("Flip Y should be a boolean type")
         if self._flip_y != value:
+            self._needs_refresh = True
             self._flip_y = value
+            self._full_change = True
 
     @property
     def transpose_xy(self) -> bool:
@@ -424,13 +646,18 @@ class TileGrid:
         return self._transpose_xy
 
     @transpose_xy.setter
-    def transpose_xy(self, value: bool):
+    def transpose_xy(self, value: bool) -> None:
         if not isinstance(value, bool):
             raise TypeError("Transpose XY should be a boolean type")
         if self._transpose_xy != value:
+            self._needs_refresh = True
             self._transpose_xy = value
+            if self._pixel_width == self._pixel_height:
+                self._full_change = True
+                return
             self._update_current_x()
             self._update_current_y()
+            self._moved = True
 
     @property
     def pixel_shader(self) -> Union[ColorConverter, Palette]:
@@ -447,21 +674,21 @@ class TileGrid:
             )
 
         self._pixel_shader = new_pixel_shader
+        self._full_change = True
+        self._needs_refresh = True
 
     @property
-    def bitmap(self) -> Union[Bitmap, OnDiskBitmap, Shape]:
-        """The Bitmap, OnDiskBitmap, or Shape that is assigned to this TileGrid"""
+    def bitmap(self) -> Union[Bitmap, OnDiskBitmap]:
+        """The Bitmap or OnDiskBitmap that is assigned to this TileGrid"""
         return self._bitmap
 
     @bitmap.setter
-    def bitmap(self, new_bitmap: Union[Bitmap, OnDiskBitmap, Shape]) -> None:
-        if (
-            not isinstance(new_bitmap, Bitmap)
-            and not isinstance(new_bitmap, OnDiskBitmap)
-            and not isinstance(new_bitmap, Shape)
+    def bitmap(self, new_bitmap: Union[Bitmap, OnDiskBitmap]) -> None:
+        if not isinstance(new_bitmap, Bitmap) and not isinstance(
+            new_bitmap, OnDiskBitmap
         ):
             raise TypeError(
-                "Unsupported Type: new_bitmap must be Bitmap, OnDiskBitmap, or Shape"
+                "Unsupported Type: new_bitmap must be Bitmap or OnDiskBitmap"
             )
 
         if (
@@ -470,7 +697,9 @@ class TileGrid:
         ):
             raise ValueError("New bitmap must be same size as old bitmap")
 
+        self._needs_refresh = True
         self._bitmap = new_bitmap
+        self._full_change = True
 
     def _extract_and_check_index(self, index):
         if isinstance(index, (tuple, list)):
@@ -486,23 +715,23 @@ class TileGrid:
             or index >= len(self._tiles)
         ):
             raise ValueError("Tile index out of bounds")
-        return index
+        return x, y
 
     def __getitem__(self, index: Union[Tuple[int, int], int]) -> int:
         """Returns the tile index at the given index. The index can either be
         an x,y tuple or an int equal to ``y * width + x``'.
         """
-        index = self._extract_and_check_index(index)
-        return self._tiles[index]
+        x, y = self._extract_and_check_index(index)
+        return self._tiles[y * self._width_in_tiles + x]
 
     def __setitem__(self, index: Union[Tuple[int, int], int], value: int) -> None:
         """Sets the tile index at the given index. The index can either be
         an x,y tuple or an int equal to ``y * width + x``.
         """
-        index = self._extract_and_check_index(index)
+        x, y = self._extract_and_check_index(index)
         if not 0 <= value <= 255:
             raise ValueError("Tile value out of bounds")
-        self._tiles[index] = value
+        self._set_tile(x, y, value)
 
     @property
     def width(self) -> int: