]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/commitdiff
Start splitting display functions into display core
authorMelissa LeBlanc-Williams <melissa@adafruit.com>
Tue, 4 Jan 2022 00:14:45 +0000 (16:14 -0800)
committerMelissa LeBlanc-Williams <melissa@adafruit.com>
Tue, 4 Jan 2022 00:14:45 +0000 (16:14 -0800)
displayio/_area.py [new file with mode: 0644]
displayio/_bitmap.py
displayio/_colorspace.py
displayio/_constants.py
displayio/_display.py
displayio/_displaycore.py [new file with mode: 0644]
displayio/_group.py
displayio/_structs.py [new file with mode: 0644]
displayio/_tilegrid.py
requirements.txt
setup.py

diff --git a/displayio/_area.py b/displayio/_area.py
new file mode 100644 (file)
index 0000000..8e50c85
--- /dev/null
@@ -0,0 +1,143 @@
+# SPDX-FileCopyrightText: 2021 Melissa LeBlanc-Williams for Adafruit Industries
+# SPDX-FileCopyrightText: 2021 James Carr
+#
+# SPDX-License-Identifier: MIT
+
+"""
+`displayio._area`
+================================================================================
+
+Area for Blinka Displayio
+
+**Software and Dependencies:**
+
+* Adafruit Blinka:
+  https://github.com/adafruit/Adafruit_Blinka/releases
+
+* Author(s): James Carr, Melissa LeBlanc-Williams
+
+"""
+
+from __future__ import annotations
+
+__version__ = "0.0.0-auto.0"
+__repo__ = "https://github.com/adafruit/Adafruit_Blinka_Displayio.git"
+
+class Area:
+    # pylint: disable=invalid-name,missing-function-docstring
+    def __init__(self, x1: int = 0, y1: int = 0, x2: int = 0, y2: int = 0):
+        self.x1 = x1
+        self.y1 = y1
+        self.x2 = x2
+        self.y2 = y2
+        self.next = None
+
+    def __str__(self):
+        return f"Area TL({self.x1},{self.y1}) BR({self.x2},{self.y2})"
+
+    def _copy_into(self, dst) -> None:
+        dst.x1 = self.x1
+        dst.y1 = self.y1
+        dst.x2 = self.x2
+        dst.y2 = self.y2
+
+    def _scale(self, scale: int) -> None:
+        self.x1 *= scale
+        self.y1 *= scale
+        self.x2 *= scale
+        self.y2 *= scale
+
+    def _shift(self, dx: int, dy: int) -> None:
+        self.x1 += dx
+        self.y1 += dy
+        self.x2 += dx
+        self.y2 += dy
+
+    def _compute_overlap(self, other, overlap) -> bool:
+        a = self
+        overlap.x1 = max(a.x1, other.x1)
+        overlap.x2 = min(a.x2, other.x2)
+
+        if overlap.x1 >= overlap.x2:
+            return False
+
+        overlap.y1 = max(a.y1, other.y1)
+        overlap.y2 = min(a.y2, other.y2)
+
+        return overlap.y1 < overlap.y2
+
+    def _empty(self):
+        return (self.x1 == self.x2) or (self.y1 == self.y2)
+
+    def _canon(self):
+        if self.x1 > self.x2:
+            self.x1, self.x2 = self.x2, self.x1
+        if self.y1 > self.y2:
+            self.y1, self.y2 = self.y2, self.y1
+
+    def _union(self, other, union):
+        # pylint: disable=protected-access
+        if self._empty():
+            self._copy_into(union)
+            return
+        if other._empty():
+            other._copy_into(union)
+            return
+
+        union.x1 = min(self.x1, other.x1)
+        union.y1 = min(self.y1, other.y1)
+        union.x2 = max(self.x2, other.x2)
+        union.y2 = max(self.y2, other.y2)
+
+    def width(self) -> int:
+        return self.x2 - self.x1
+
+    def height(self) -> int:
+        return self.y2 - self.y1
+
+    def size(self) -> int:
+        return self.width() * self.height()
+
+    def __eq__(self, other):
+        if not isinstance(other, Area):
+            return False
+
+        return (
+            self.x1 == other.x1
+            and self.y1 == other.y1
+            and self.x2 == other.x2
+            and self.y2 == other.y2
+        )
+
+    @staticmethod
+    def _transform_within(
+        mirror_x: bool,
+        mirror_y: bool,
+        transpose_xy: bool,
+        original: Area,
+        whole: Area,
+        transformed: Area,
+    ):
+        # pylint: disable=too-many-arguments
+        # Original and whole must be in the same coordinate space.
+        if mirror_x:
+            transformed.x1 = whole.x1 + (whole.x2 - original.x2)
+            transformed.x2 = whole.x2 - (original.x1 - whole.x1)
+        else:
+            transformed.x1 = original.x1
+            transformed.x2 = original.x2
+
+        if mirror_y:
+            transformed.y1 = whole.y1 + (whole.y2 - original.y2)
+            transformed.y2 = whole.y2 - (original.y1 - whole.y1)
+        else:
+            transformed.y1 = original.y1
+            transformed.y2 = original.y2
+
+        if transpose_xy:
+            y1 = transformed.y1
+            y2 = transformed.y2
+            transformed.y1 = whole.y1 + (transformed.x1 - whole.x1)
+            transformed.y2 = whole.y1 + (transformed.x2 - whole.x1)
+            transformed.x1 = whole.x1 + (y1 - whole.y1)
+            transformed.x2 = whole.x1 + (y2 - whole.y1)
index f28e72a15961201994042f92deaaf028b13f71eb..9d06a6364a4ae632a0827849b308b22223c91d9e 100644 (file)
@@ -19,14 +19,12 @@ displayio for Blinka
 
 from __future__ import annotations
 from typing import Union, Tuple
-from recordclass import recordclass
 from PIL import Image
+from ._structs import RectangleStruct
 
 __version__ = "0.0.0-auto.0"
 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
 
-Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
-
 
 class Bitmap:
     """Stores values of a certain size in a 2D array"""
@@ -61,7 +59,7 @@ class Bitmap:
             raise NotImplementedError("Invalid bits per value")
 
         self._image = Image.new("P", (width, height), 0)
-        self._dirty_area = Rectangle(0, 0, width, height)
+        self._dirty_area = RectangleStruct(0, 0, width, height)
 
     def __getitem__(self, index: Union[Tuple[int, int], int]) -> int:
         """
@@ -117,7 +115,7 @@ class Bitmap:
     def fill(self, value: int) -> None:
         """Fills the bitmap with the supplied palette index value."""
         self._image = Image.new("P", (self._bmp_width, self._bmp_height), value)
-        self._dirty_area = Rectangle(0, 0, self._bmp_width, self._bmp_height)
+        self._dirty_area = RectangleStruct(0, 0, self._bmp_width, self._bmp_height)
 
     def blit(
         self,
index 5aacb242900e767c5715532ea9e80fecefa63ae7..b5322a3bbf8cf7db80de8c042bf42152c343dcb0 100644 (file)
@@ -26,8 +26,8 @@ class Colorspace:
     """The colorspace for a ColorConverter to operate in."""
 
     # pylint: disable=too-few-public-methods
-    def __init__(self, colorspace):
-        self._type = colorspace
+    def __init__(self, colorspace_type):
+        self._colorspace_type = colorspace_type
 
 
 Colorspace.RGB888 = Colorspace("RGB888")
index d0a00b7ec1ae750b1fe494b6d2c94b1494371113..11b20c66a7131a1b7012e5ef8301b7dd1c5ad4f7 100644 (file)
@@ -15,3 +15,6 @@ DISPLAY_DATA = 1
 
 CHIP_SELECT_UNTOUCHED = 0
 CHIP_SELECT_TOGGLE_EVERY_BYTE = 1
+
+BACKLIGHT_IN_OUT = 1
+BACKLIGHT_PWM = 2
index e0c4026689c4739c33b251e37692479f4f015b74..80387224811e04f4ba15edfa0282055506a2ad63 100644 (file)
@@ -21,31 +21,31 @@ import time
 import struct
 import threading
 from typing import Optional
+from dataclasses import astuple
 import digitalio
 from PIL import Image
 import numpy
 import microcontroller
-from recordclass import recordclass
 import _typing
+from ._displaycore import _DisplayCore
 from ._displaybus import _DisplayBus
 from ._colorconverter import ColorConverter
 from ._group import Group
+from ._structs import RectangleStruct
 from ._constants import (
     CHIP_SELECT_TOGGLE_EVERY_BYTE,
     CHIP_SELECT_UNTOUCHED,
     DISPLAY_COMMAND,
     DISPLAY_DATA,
+    BACKLIGHT_IN_OUT,
+    BACKLIGHT_PWM,
 )
 
 __version__ = "0.0.0-auto.0"
 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
 
-Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
 displays = []
 
-BACKLIGHT_IN_OUT = 1
-BACKLIGHT_PWM = 2
-
 
 class Display:
     # pylint: disable=too-many-instance-attributes
@@ -72,6 +72,7 @@ class Display:
         pixels_in_byte_share_row: bool = True,
         bytes_per_cell: int = 1,
         reverse_pixels_in_byte: bool = False,
+        reverse_bytes_in_word: bool = True,
         set_column_command: int = 0x2A,
         set_row_command: int = 0x2B,
         write_ram_command: int = 0x2C,
@@ -117,7 +118,28 @@ class Display:
         The initialization sequence should always leave the display memory access inline with
         the scan of the display to minimize tearing artifacts.
         """
-        self._bus = display_bus
+        ram_width = 0x100
+        ram_height = 0x100
+        if single_byte_bounds:
+            ram_width = 0xFF
+            ram_height = 0xFF
+        self._core = _DisplayCore(
+            bus=display_bus,
+            width=width,
+            height=height,
+            ram_width=ram_width,
+            ram_height=ram_height,
+            colstart=colstart,
+            rowstart=rowstart,
+            rotation=rotation,
+            color_depth=color_depth,
+            grayscale=grayscale,
+            pixels_in_byte_share_row=pixels_in_byte_share_row,
+            bytes_per_cell=bytes_per_cell,
+            reverse_pixels_in_byte=reverse_pixels_in_byte,
+            reverse_bytes_in_word=reverse_bytes_in_word,
+        )
+
         self._set_column_command = set_column_command
         self._set_row_command = set_row_command
         self._write_ram_command = write_ram_command
@@ -168,7 +190,21 @@ class Display:
             delay = (data_size & 0x80) > 0
             data_size &= ~0x80
 
-            self._send(command, init_sequence[i + 2 : i + 2 + data_size])
+            if self._data_as_commands:
+                self._core.send(
+                    DISPLAY_COMMAND,
+                    CHIP_SELECT_TOGGLE_EVERY_BYTE,
+                    bytes([command]) + init_sequence[i + 2 : i + 2 + data_size],
+                )
+            else:
+                self._core.send(
+                    DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
+                )
+                self._core.send(
+                    DISPLAY_DATA,
+                    CHIP_SELECT_UNTOUCHED,
+                    init_sequence[i + 2 : i + 2 + data_size],
+                )
             delay_time_ms = 10
             if delay:
                 data_size += 1
@@ -179,38 +215,32 @@ class Display:
             i += 2 + data_size
 
     def _send(self, command, data):
-        # pylint: disable=protected-access
-        self._bus._begin_transaction()
+        self._core.begin_transaction()
         if self._data_as_commands:
-            self._bus._send(
+            self._core.send(
                 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command]) + data
             )
         else:
-            self._bus._send(
+            self._core.send(
                 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
             )
-            self._bus._send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
-        self._bus._end_transaction()
+            self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
+        self._core.end_transaction()
 
     def _send_pixels(self, data):
-        # pylint: disable=protected-access
         if not self._data_as_commands:
-            self._bus._send(
+            self._core.send(
                 DISPLAY_COMMAND,
                 CHIP_SELECT_TOGGLE_EVERY_BYTE,
                 bytes([self._write_ram_command]),
             )
-        self._bus._send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
-
-    def _release(self):
-        self._bus._release()  # pylint: disable=protected-access
-        self._bus = None
+        self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
 
     def show(self, group: Group) -> None:
         """Switches to displaying the given group of layers. When group is None, the
         default CircuitPython terminal will be shown.
         """
-        self._current_group = group
+        self._core.show(group)
 
     def refresh(
         self,
@@ -218,7 +248,7 @@ class Display:
         target_frames_per_second: Optional[int] = None,
         minimum_frames_per_second: int = 0,
     ) -> bool:
-        # pylint: disable=unused-argument
+        # pylint: disable=unused-argument, protected-access
         """When auto refresh is off, waits for the target frame rate and then refreshes the
         display, returning True. If the call has taken too long since the last refresh call
         for the given target frame rate, then the refresh returns False immediately without
@@ -230,23 +260,26 @@ class Display:
         When auto refresh is on, updates the display immediately. (The display will also
         update without calls to this.)
         """
-        self._subrectangles = []
+        if not self._core.start_refresh():
+            return False
 
         # Go through groups and and add each to buffer
-        if self._current_group is not None:
-            buffer = Image.new("RGBA", (self._width, self._height))
+        if self._core._current_group is not None:
+            buffer = Image.new("RGBA", (self._core._width, self._core._height))
             # Recursively have everything draw to the image
-            self._current_group._fill_area(buffer)  # pylint: disable=protected-access
+            self._core._current_group._fill_area(
+                buffer
+            )  # pylint: disable=protected-access
             # save image to buffer (or probably refresh buffer so we can compare)
             self._buffer.paste(buffer)
 
-        if self._current_group is not None:
-            # Eventually calculate dirty rectangles here
-            self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
+        self._subrectangles = self._core.get_refresh_areas()
 
         for area in self._subrectangles:
             self._refresh_display_area(area)
 
+        self._core.finish_refresh()
+
         return True
 
     def _refresh_loop(self):
@@ -255,12 +288,11 @@ class Display:
 
     def _refresh_display_area(self, rectangle):
         """Loop through dirty rectangles and redraw that area."""
-
-        img = self._buffer.convert("RGB").crop(rectangle)
+        img = self._buffer.convert("RGB").crop(astuple(rectangle))
         img = img.rotate(self._rotation, expand=True)
 
         display_rectangle = self._apply_rotation(rectangle)
-        img = img.crop(self._clip(display_rectangle))
+        img = img.crop(astuple(self._clip(display_rectangle)))
 
         data = numpy.array(img).astype("uint16")
         color = (
@@ -288,9 +320,9 @@ class Display:
             ),
         )
 
-        self._bus._begin_transaction()  # pylint: disable=protected-access
+        self._core.begin_transaction()
         self._send_pixels(pixels)
-        self._bus._end_transaction()  # pylint: disable=protected-access
+        self._core.end_transaction()
 
     def _clip(self, rectangle):
         if self._rotation in (90, 270):
@@ -308,21 +340,21 @@ class Display:
     def _apply_rotation(self, rectangle):
         """Adjust the rectangle coordinates based on rotation"""
         if self._rotation == 90:
-            return Rectangle(
+            return RectangleStruct(
                 self._height - rectangle.y2,
                 rectangle.x1,
                 self._height - rectangle.y1,
                 rectangle.x2,
             )
         if self._rotation == 180:
-            return Rectangle(
+            return RectangleStruct(
                 self._width - rectangle.x2,
                 self._height - rectangle.y2,
                 self._width - rectangle.x1,
                 self._height - rectangle.y1,
             )
         if self._rotation == 270:
-            return Rectangle(
+            return RectangleStruct(
                 rectangle.y1,
                 self._width - rectangle.x2,
                 rectangle.y2,
@@ -400,25 +432,23 @@ class Display:
     @property
     def width(self) -> int:
         """Display Width"""
-        return self._width
+        return self._core.get_width()
 
     @property
     def height(self) -> int:
         """Display Height"""
-        return self._height
+        return self._core.get_height()
 
     @property
     def rotation(self) -> int:
         """The rotation of the display as an int in degrees."""
-        return self._rotation
+        return self._core.get_rotation()
 
     @rotation.setter
     def rotation(self, value: int):
-        if value not in (0, 90, 180, 270):
-            raise ValueError("Rotation must be 0/90/180/270")
-        self._rotation = value
+        self._core.set_rotation(value)
 
     @property
     def bus(self) -> _DisplayBus:
         """Current Display Bus"""
-        return self._bus
+        return self._core.get_bus()
diff --git a/displayio/_displaycore.py b/displayio/_displaycore.py
new file mode 100644 (file)
index 0000000..acedf6e
--- /dev/null
@@ -0,0 +1,457 @@
+# SPDX-FileCopyrightText: 2021 Melissa LeBlanc-Williams for Adafruit Industries
+# SPDX-FileCopyrightText: 2021 James Carr
+#
+# SPDX-License-Identifier: MIT
+
+"""
+`displayio._displaycore`
+================================================================================
+
+Super class of the display classes
+
+**Software and Dependencies:**
+
+* Adafruit Blinka:
+  https://github.com/adafruit/Adafruit_Blinka/releases
+
+* Author(s): James Carr, Melissa LeBlanc-Williams
+
+"""
+
+__version__ = "0.0.0-auto.0"
+__repo__ = "https://github.com/adafruit/Adafruit_Blinka_Displayio.git"
+
+
+from typing import Optional, Union
+import _typing
+from ._fourwire import FourWire
+from ._group import Group
+from ._i2cdisplay import I2CDisplay
+from ._structs import ColorspaceStruct, TransformStruct, RectangleStruct
+from ._area import Area
+
+from paralleldisplay import ParallelBus
+from ._constants import (
+    DISPLAY_COMMAND,
+    DISPLAY_DATA,
+    CHIP_SELECT_TOGGLE_EVERY_BYTE,
+    CHIP_SELECT_UNTOUCHED,
+)
+
+displays = []
+
+# Functions
+#* construct
+#  show
+#  set_dither
+#^ bus_reset
+#  set_region_to_update
+#* release
+#  fill_area
+#  clip_area (_clip)
+
+class _DisplayCore:
+    # pylint: disable=too-many-arguments, too-many-instance-attributes
+
+    def __init__(
+        self,
+        bus,
+        width: int,
+        height: int,
+        ram_width: int,
+        ram_height: int,
+        colstart: int,
+        rowstart: int,
+        rotation: int,
+        color_depth: int,
+        grayscale: bool,
+        pixels_in_byte_share_row: bool,
+        bytes_per_cell: int,
+        reverse_pixels_in_byte: bool,
+        reverse_bytes_in_word: bool
+    ):
+        self._colorspace = ColorspaceStruct(
+            depth = color_depth,
+            grayscale = grayscale,
+            grayscale_bit = 8 - color_depth,
+            pixels_in_byte_share_row = pixels_in_byte_share_row,
+            bytes_per_cell = bytes_per_cell,
+            reverse_pixels_in_byte = reverse_pixels_in_byte,
+            reverse_bytes_in_word = reverse_bytes_in_word,
+            dither = False
+        )
+        self._current_group = None
+        self._colstart = colstart
+        self._rowstart = rowstart
+        self._last_refresh = 0
+        self._refresh_in_progress = False
+
+        if bus:
+            if isinstance(bus, (FourWire, I2CDisplay, ParallelBus)):
+                self._bus_reset = bus.reset
+                self._begin_transaction = bus._begin_transaction
+                self._send = bus._send
+                self._end_transaction = bus._end_transaction
+            else:
+                raise ValueError("Unsupported display bus type")
+
+        self._bus = bus
+        self._area = Area(0, 0, width, height)
+
+        self._width = width
+        self._height = height
+        self._ram_width = ram_width
+        self._ram_height = ram_height
+        self._rotation = rotation
+        self._transform = TransformStruct()
+
+    def set_rotation(self, rotation: int) -> None:
+        """
+        Sets the rotation of the display as an int in degrees.
+        """
+        # pylint: disable=protected-access
+        transposed = self._rotation in (90, 270)
+        will_be_transposed = rotation in (90, 270)
+        if transposed != will_be_transposed:
+            self._width, self._height = self._height, self._width
+
+        height = self._height
+        width = self._width
+
+        rotation %= 360
+        self._rotation = rotation
+        self._transform.x = 0
+        self._transform.y = 0
+        self._transform.scale = 1
+        self._transform.mirror_x = False
+        self._transform.mirror_y = False
+        self._transform.transpose_xy = False
+
+        if rotation in (0, 180):
+            if rotation == 180:
+                self._transform.mirror_x = True
+                self._transform.mirror_y = True
+        else:
+            self._transform.transpose_xy = True
+            if rotation == 270:
+                self._transform.mirror_y = True
+            else:
+                self._transform.mirror_x = True
+
+        self._area.x1 = 0
+        self._area.y1 = 0
+        self._area.next = None
+
+        self._transform.dx = 1
+        self._transform.dy = 1
+        if self._transform.transpose_xy:
+            self._area.x2 = height
+            self._area.y2 = width
+            if self._transform.mirror_x:
+                self._transform.x = height
+                self._transform.dx = -1
+            if self._transform.mirror_y:
+                self._transform.y = width
+                self._transform.dy = -1
+        else:
+            self._area.x2 = width
+            self._area.y2 = height
+            if self._transform.mirror_x:
+                self._transform.x = width
+                self._transform.dx = -1
+            if self._transform.mirror_y:
+                self._transform.y = height
+                self._transform.dy = -1
+
+        if self._current_group is not None:
+            self._current_group._update_transform(self._transform)
+
+    def show(self, root_group: Group) -> bool:
+        # pylint: disable=protected-access
+
+        """
+        Switches to displaying the given group of layers. When group is `None`, the
+        default CircuitPython terminal will be shown.
+
+        :param Optional[displayio.Group] root_group: The group to show.
+        """
+
+        """
+        # TODO: Implement Supervisor
+        if root_group is None:
+            circuitpython_splash = _Supervisor().circuitpython_splash
+            if not circuitpython_splash._in_group:
+                root_group = circuitpython_splash
+            elif self._current_group == circuitpython_splash:
+                return True
+        """
+
+        if root_group == self._current_group:
+            return True
+
+        if root_group is not None and root_group._in_group:
+            return False
+
+        if self._current_group is not None:
+            self._current_group._in_group = False
+
+        if root_group is not None:
+            root_group._update_transform(self._transform)
+            root_group._in_group = True
+
+        self._current_group = root_group
+        self._full_refresh = True
+
+        return True
+
+    def set_region_to_update(
+        self,
+        column_command: int,
+        row_command: int,
+        set_current_column_command: Optional[int],
+        set_current_row_command: Optional[int],
+        data_as_commands: bool,
+        always_toggle_chip_select: bool,
+        area: Area,
+        SH1107_addressing: bool,
+    ) -> None:
+        # pylint: disable=invalid-name, too-many-arguments, too-many-locals, too-many-branches,
+        # pylint: disable=too-many-statements
+
+        big_endian = True  # default is True # TODO ????
+
+        x1 = area.x1 + self._colstart
+        x2 = area.x2 + self._colstart
+        y1 = area.y1 + self._rowstart
+        y2 = area.y2 + self._rowstart
+
+        # Collapse down the dimension where multiple pixels are in a byte.
+        if self._colorspace.depth < 8:
+            pixels_per_byte = 8 // self._colorspace.depth
+            if self._colorspace.pixels_in_byte_share_row:
+                x1 //= pixels_per_byte * self._colorspace.bytes_per_cell
+                x2 //= pixels_per_byte * self._colorspace.bytes_per_cell
+            else:
+                y1 //= pixels_per_byte * self._colorspace.bytes_per_cell
+                y2 //= pixels_per_byte * self._colorspace.bytes_per_cell
+
+        x2 -= 1
+        y2 -= 1
+
+        chip_select = CHIP_SELECT_UNTOUCHED
+        if always_toggle_chip_select or data_as_commands:
+            chip_select = CHIP_SELECT_TOGGLE_EVERY_BYTE
+
+        # Set column
+        self._begin_transaction()
+        data = bytearray(5)
+        data[0] = column_command
+        if not data_as_commands:
+            self._send(DISPLAY_COMMAND, CHIP_SELECT_UNTOUCHED, data, 1)
+            data_type = DISPLAY_DATA
+            data_length = 0
+        else:
+            data_type = DISPLAY_COMMAND
+            data_length = 1
+
+        if self._ram_width < 0x100:
+            data[data_length] = x1
+            data_length += 1
+            data[data_length] = x2
+            data_length += 1
+        else:
+            if big_endian:
+                data[data_length] = x1 >> 8
+                data_length += 1
+                data[data_length] = x1 & 0xFF
+                data_length += 1
+                data[data_length] = x2 >> 8
+                data_length += 1
+                data[data_length] = x2 & 0xFF
+                data_length += 1
+            else:
+                data[data_length] = x1 & 0xFF
+                data_length += 1
+                data[data_length] = x1 >> 8
+                data_length += 1
+                data[data_length] = x2 & 0xFF
+                data_length += 1
+                data[data_length] = x2 >> 8
+                data_length += 1
+
+        # Quirk for "SH1107_addressing"
+        #     Column lower command = 0x00, Column upper command = 0x10
+        if SH1107_addressing:
+            data[0] = ((x1 >> 4) & 0x0F) | 0x10  # 0x10 to 0x17
+            data[1] = x1 & 0x0F  # 0x00 to 0x0F
+            data_length = 2
+
+        self._send(data_type, chip_select, data, data_length)
+        self._end_transaction()
+
+        if set_current_column_command is not None:
+            command = bytearray(1)
+            command[0] = set_current_column_command
+            self._begin_transaction()
+            self._send(DISPLAY_COMMAND, chip_select, command, 1)
+            self._send(DISPLAY_DATA, chip_select, data, data_length // 2)
+            self._end_transaction()
+
+        # Set row
+        self._begin_transaction()
+        data[0] = row_command
+        data_length = 1
+        if not data_as_commands:
+            self._send(DISPLAY_COMMAND, CHIP_SELECT_UNTOUCHED, data, 1)
+            data_length = 0
+
+        if self._ram_height < 0x100:
+            data[data_length] = y1
+            data_length += 1
+            data[data_length] = y2
+            data_length += 1
+        else:
+            if big_endian:
+                data[data_length] = y1 >> 8
+                data_length += 1
+                data[data_length] = y1 & 0xFF
+                data_length += 1
+                data[data_length] = y2 >> 8
+                data_length += 1
+                data[data_length] = y2 & 0xFF
+                data_length += 1
+                # TODO Which is right? The core uses above
+            else:
+                data[data_length] = y1 & 0xFF
+                data_length += 1
+                data[data_length] = y1 >> 8
+                data_length += 1
+                data[data_length] = y2 & 0xFF
+                data_length += 1
+                data[data_length] = y2 >> 8
+                data_length += 1
+
+        # Quirk for "SH1107_addressing"
+        #  Page address command = 0xB0
+        if SH1107_addressing:
+            # Set the page to out y value
+            data[0] = 0xB0 | y1
+            data_length = 1
+
+        self._send(data_type, chip_select, data, data_length)
+        self._end_transaction()
+
+        if set_current_row_command is not None:
+            command = bytearray(1)
+            command[0] = set_current_row_command
+            self._begin_transaction()
+            self._send(DISPLAY_COMMAND, chip_select, command, 1)
+            self._send(DISPLAY_DATA, chip_select, data, data_length // 2)
+            self._end_transaction()
+
+    def start_refresh(self) -> bool:
+        # pylint: disable=protected-access
+
+        if self._refresh_in_progress:
+            return False
+
+        self._refresh_in_progress = True
+        #self._last_refresh = _Supervisor()._ticks_ms64()
+        return True
+
+    def finish_refresh(self) -> None:
+        # pylint: disable=protected-access
+
+        if self._current_group is not None:
+            self._current_group._finish_refresh()
+
+        self._full_refresh = False
+        self._refresh_in_progress = False
+        #self._last_refresh = _Supervisor()._ticks_ms64()
+
+    def get_refresh_areas(self):
+        subrectangles = []
+        if self._current_group is not None:
+            # Eventually calculate dirty rectangles here
+            subrectangles.append(RectangleStruct(0, 0, self._width, self._height))
+        return subrectangles
+
+    def release(self) -> None:
+        # pylint: disable=protected-access
+
+        if self._current_group is not None:
+            self._current_group._in_group = False
+
+    def fill_area(
+        self, area: Area, mask: _typing.WriteableBuffer, buffer: _typing.WriteableBuffer
+    ) -> bool:
+        # pylint: disable=protected-access
+
+        return self._current_group._fill_area(self._colorspace, area, mask, buffer)
+
+    def clip_area(self, area: Area, clipped: Area) -> bool:
+        # pylint: disable=protected-access
+
+        overlaps = self._area._compute_overlap(area, clipped)
+        if not overlaps:
+            return False
+
+        # Expand the area if we have multiple pixels per byte and we need to byte align the bounds
+        if self._colorspace.depth < 8:
+            pixels_per_byte = (
+                8 // self._colorspace.depth * self._colorspace.bytes_per_cell
+            )
+            if self._colorspace.pixels_in_byte_share_row:
+                if clipped.x1 % pixels_per_byte != 0:
+                    clipped.x1 -= clipped.x1 % pixels_per_byte
+                if clipped.x2 % pixels_per_byte != 0:
+                    clipped.x2 += pixels_per_byte - clipped.x2 % pixels_per_byte
+            else:
+                if clipped.y1 % pixels_per_byte != 0:
+                    clipped.y1 -= clipped.y1 % pixels_per_byte
+                if clipped.y2 % pixels_per_byte != 0:
+                    clipped.y2 += pixels_per_byte - clipped.y2 % pixels_per_byte
+
+        return True
+
+    def send(self, data_type: int, chip_select: int, data: _typing.ReadableBuffer) -> None:
+        """
+        Send the data to the current bus
+        """
+        self._send(data_type, chip_select, data)
+
+    def begin_transaction(self) -> None:
+        """
+        Begin Bus Transaction
+        """
+        self._begin_transaction()
+
+    def end_transaction(self) -> None:
+        """
+        End Bus Transaction
+        """
+        self._end_transaction()
+
+    def get_width(self) -> int:
+        """
+        Gets the width of the display in pixels.
+        """
+        return self._width
+
+    def get_height(self) -> int:
+        """
+        Gets the height of the display in pixels.
+        """
+        return self._height
+
+    def get_rotation(self) -> int:
+        """
+        Gets the rotation of the display as an int in degrees.
+        """
+        return self._rotation
+            
+    def get_bus(self) -> Union[FourWire,ParallelBus,I2CDisplay]:
+        """
+        The bus being used by the display. [readonly]
+        """
+        return self._bus
\ No newline at end of file
index 6be7c6fb0974602d98e54a0becb6e1d1cd83f018..b38eaffd03d9b6a19daf0a3157ea7c07c36278dd 100644 (file)
@@ -19,16 +19,13 @@ displayio for Blinka
 
 from __future__ import annotations
 from typing import Union, Callable
-from recordclass import recordclass
+from ._structs import TransformStruct
 from ._tilegrid import TileGrid
 
 __version__ = "0.0.0-auto.0"
 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
 
 
-Transform = recordclass("Transform", "x y dx dy scale transpose_xy mirror_x mirror_y")
-
-
 class Group:
     """
     Manage a group of sprites and groups and how they are inter-related.
@@ -52,14 +49,14 @@ class Group:
         self._hidden_group = False
         self._layers = []
         self._supported_types = (TileGrid, Group)
-        self.in_group = False
-        self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
+        self._in_group = False
+        self._absolute_transform = TransformStruct(0, 0, 1, 1, 1, False, False, False)
         self._set_scale(scale)  # Set the scale via the setter
 
     def _update_transform(self, parent_transform):
         """Update the parent transform and child transforms"""
-        self.in_group = parent_transform is not None
-        if self.in_group:
+        self._in_group = parent_transform is not None
+        if self._in_group:
             x = self._group_x
             y = self._group_y
             if parent_transform.transpose_xy:
@@ -76,7 +73,7 @@ class Group:
 
     def _update_child_transforms(self):
         # pylint: disable=protected-access
-        if self.in_group:
+        if self._in_group:
             for layer in self._layers:
                 layer._update_transform(self._absolute_transform)
 
@@ -100,7 +97,7 @@ class Group:
         """Insert a layer into the group."""
         if not isinstance(layer, self._supported_types):
             raise ValueError("Invalid Group Member")
-        if layer.in_group:
+        if layer._in_group:  # pylint: disable=protected-access
             raise ValueError("Layer already in a group.")
         self._layers.insert(index, layer)
         self._layer_update(index)
@@ -156,6 +153,11 @@ class Group:
         """Sort the members of the group."""
         self._layers.sort(key=key, reverse=reverse)
 
+    def _finish_refresh(self):
+        for layer in self._layers:
+            if isinstance(layer, (Group, TileGrid)):
+                layer._finish_refresh()  # pylint: disable=protected-access
+
     @property
     def hidden(self) -> bool:
         """True when the Group and all of it's layers are not visible. When False, the
diff --git a/displayio/_structs.py b/displayio/_structs.py
new file mode 100644 (file)
index 0000000..2f82bb0
--- /dev/null
@@ -0,0 +1,55 @@
+# SPDX-FileCopyrightText: 2021 Melissa LeBlanc-Williams
+#
+# SPDX-License-Identifier: MIT
+
+"""
+`displayio._structs`
+================================================================================
+
+Struct Data Classes for Blinka Displayio
+
+**Software and Dependencies:**
+
+* Adafruit Blinka:
+  https://github.com/adafruit/Adafruit_Blinka/releases
+
+* Author(s): Melissa LeBlanc-Williams
+
+"""
+
+from dataclasses import dataclass
+
+__version__ = "0.0.0-auto.0"
+__repo__ = "https://github.com/adafruit/Adafruit_Blinka_Displayio.git"
+
+@dataclass
+class RectangleStruct:
+    x1: int
+    y1: int
+    x2: int
+    y2: int
+
+@dataclass
+class TransformStruct:
+    x: int = 0
+    y: int = 0
+    dx: int = 1
+    dy: int = 1
+    scale: int = 1
+    transpose_xy: bool = False
+    mirror_x: bool = False
+    mirror_y: bool = False
+
+@dataclass
+class ColorspaceStruct:
+    depth: int
+    bytes_per_cell: int = 0
+    tricolor_hue: int = 0
+    tricolor_luma: int = 0
+    grayscale_bit: int = 0
+    grayscale: bool = False
+    tricolor: bool = False
+    pixels_in_byte_share_row: bool = False
+    reverse_pixels_in_byte: bool = False
+    reverse_bytes_in_word: bool = False
+    dither: bool = False
index f2ccde23ab827dcc0525fe596f157e6e049535bb..25b0c73ce83e0434ab5b8b46a9f91a83663eacb9 100644 (file)
@@ -18,20 +18,17 @@ displayio for Blinka
 """
 
 from typing import Union, Optional, Tuple
-from recordclass import recordclass
 from PIL import Image
 from ._bitmap import Bitmap
 from ._colorconverter import ColorConverter
 from ._ondiskbitmap import OnDiskBitmap
 from ._shape import Shape
 from ._palette import Palette
+from ._structs import RectangleStruct, TransformStruct
 
 __version__ = "0.0.0-auto.0"
 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
 
-Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
-Transform = recordclass("Transform", "x y dx dy scale transpose_xy mirror_x mirror_y")
-
 
 class TileGrid:
     # pylint: disable=too-many-instance-attributes
@@ -100,9 +97,11 @@ class TileGrid:
         self._pixel_width = width * tile_width
         self._pixel_height = height * tile_height
         self._tiles = (self._width * self._height) * [default_tile]
-        self.in_group = False
-        self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
-        self._current_area = Rectangle(0, 0, self._pixel_width, self._pixel_height)
+        self._in_group = False
+        self._absolute_transform = TransformStruct(0, 0, 1, 1, 1, False, False, False)
+        self._current_area = RectangleStruct(
+            0, 0, self._pixel_width, self._pixel_height
+        )
         self._moved = False
 
     def _update_transform(self, absolute_transform):
@@ -296,6 +295,9 @@ class TileGrid:
         ):
             buffer.alpha_composite(image, (x, y), source=(source_x, source_y))
 
+    def _finish_refresh(self):
+        pass
+
     @property
     def hidden(self) -> bool:
         """True when the TileGrid is hidden. This may be False even
index 1355dcea185d037bef868e5340fb951eb9aab19d..10356cbe0bec80eada7351248dbe2a5f1907224c 100644 (file)
@@ -5,4 +5,3 @@
 Adafruit-Blinka
 pillow
 numpy
-recordclass
index fa87d91387c7c40d728f7363693939a55b11362b..64f2711459dca0bc4df98389a93962aded6eba64 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -37,7 +37,6 @@ setup(
         "Adafruit-Blinka",
         "pillow",
         "numpy",
-        "recordclass",
     ],
     # Choose your license
     license="MIT",