def _background():
"""Main thread function to loop through all displays and update them"""
- for display in displays:
- display.background()
+ while True:
+ for display in displays:
+ display.background()
def release_displays() -> None:
def __str__(self):
return f"Area TL({self.x1},{self.y1}) BR({self.x2},{self.y2})"
- def _copy_into(self, dst) -> None:
+ def copy_into(self, dst) -> None:
+ """Copy the area into another area."""
dst.x1 = self.x1
dst.y1 = self.y1
dst.x2 = self.x2
dst.y2 = self.y2
- def _scale(self, scale: int) -> None:
+ def scale(self, scale: int) -> None:
+ """Scale the area by scale."""
self.x1 *= scale
self.y1 *= scale
self.x2 *= scale
self.y2 *= scale
- def _shift(self, dx: int, dy: int) -> None:
+ def shift(self, dx: int, dy: int) -> None:
+ """Shift the area by dx and dy."""
self.x1 += dx
self.y1 += dy
self.x2 += dx
return overlap.y1 < overlap.y2
- def _empty(self):
+ def empty(self):
+ """Return True if the area is empty."""
return (self.x1 == self.x2) or (self.y1 == self.y2)
- def _canon(self):
+ def canon(self):
+ """Make sure the area is in canonical form."""
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):
- if self._empty():
- self._copy_into(union)
+ def union(self, other, union):
+ """Combine this area along with another into union"""
+ if self.empty():
+ self.copy_into(union)
return
- if other._empty(): # pylint: disable=protected-access
- other._copy_into(union) # pylint: disable=protected-access
+ if other.empty():
+ other.copy_into(union)
return
union.x1 = min(self.x1, other.x1)
from typing import Union, Tuple
from PIL import Image
from ._structs import RectangleStruct
+from ._area import Area
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
if x > self._image.width or y > self._image.height:
raise ValueError(f"Index {index} is out of range")
+ return self._get_pixel(x, y)
+
+ def _get_pixel(self, x: int, y: int) -> int:
return self._image.getpixel((x, y))
def __setitem__(self, index: Union[Tuple[int, int], int], value: int) -> None:
y2: int,
skip_index: int,
) -> None:
- # pylint: disable=unnecessary-pass, invalid-name
"""Inserts the source_bitmap region defined by rectangular boundaries"""
+ # pylint: disable=invalid-name
if x2 is None:
x2 = source_bitmap.width
if y2 is None:
break
def dirty(self, x1: int = 0, y1: int = 0, x2: int = -1, y2: int = -1) -> None:
- # pylint: disable=unnecessary-pass, invalid-name
"""Inform displayio of bitmap updates done via the buffer protocol."""
- pass
+ # pylint: disable=invalid-name
+ if x2 == -1:
+ x2 = self._bmp_width
+ if y2 == -1:
+ y2 = self._bmp_height
+ area = Area(x1, y1, x2, y2)
+ area.canon()
+ area.union(self._dirty_area, area)
+ bitmap_area = Area(0, 0, self._bmp_width, self._bmp_height)
+ area.compute_overlap(bitmap_area, self._dirty_area)
+
+ def _finish_refresh(self):
+ if self._read_only:
+ return
+ self._dirty_area.x1 = 0
+ self._dirty_area.x2 = 0
+
+ def _get_refresh_areas(self, areas: list[Area]) -> None:
+ if self._dirty_area.x1 == self._dirty_area.x2 or self._read_only:
+ return
+ areas.append(self._dirty_area)
@property
def width(self) -> int:
self._cached_colorspace = None
self._cached_input_pixel = None
self._cached_output_color = None
+ self._needs_refresh = False
@staticmethod
def _dither_noise_1(noise):
red8 = color_rgb888 >> 16
grn8 = (color_rgb888 >> 8) & 0xFF
blu8 = color_rgb888 & 0xFF
- return (red8 * 19 + grn8 * 182 + blu8 + 54) / 255
+ return (red8 * 19 + grn8 * 182 + blu8 + 54) // 255
@staticmethod
def _compute_chroma(color_rgb888: int):
return 0
hue = 0
if max_color == red8:
- hue = (((grn8 - blu8) * 40) / chroma) % 240
+ hue = (((grn8 - blu8) * 40) // chroma) % 240
elif max_color == grn8:
- hue = (((blu8 - red8) + (2 * chroma)) * 40) / chroma
+ hue = (((blu8 - red8) + (2 * chroma)) * 40) // chroma
elif max_color == blu8:
- hue = (((red8 - grn8) + (4 * chroma)) * 40) / chroma
+ hue = (((red8 - grn8) + (4 * chroma)) * 40) // chroma
if hue < 0:
hue += 240
allocate_display,
)
- allocate_display(cls)
- return super().__new__(cls)
+ display_instance = super().__new__(cls)
+ allocate_display(display_instance)
+ return display_instance
def _initialize(self, init_sequence):
i = 0
return True
def _refresh_display(self):
- # pylint: disable=protected-access
if not self._core.start_refresh():
return False
# TODO: Likely move this to _refresh_area()
# Go through groups and and add each to buffer
+ """
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
) # pylint: disable=protected-access
# save image to buffer (or probably refresh buffer so we can compare)
self._buffer.paste(buffer)
-
+ """
areas_to_refresh = self._get_refresh_areas()
for area in areas_to_refresh:
def _get_refresh_areas(self) -> list[Area]:
"""Get a list of areas to be refreshed"""
areas = []
- if self._core.current_group is not None:
- # Eventually calculate dirty rectangles here
- areas.append(Area(0, 0, self._core.width, self._core.height))
+ if self._core.full_refresh:
+ areas.append(self._core.area)
+ elif self._core.current_group is not None:
+ self._core.current_group._get_refresh_areas( # pylint: disable=protected-access
+ areas
+ )
return areas
def background(self):
)
return False
- """
- def _clip(self, rectangle):
- if self._core.rotation in (90, 270):
- width, height = self._core.height, self._core.width
- else:
- width, height = self._core.width, self._core.height
-
- rectangle.x1 = max(rectangle.x1, 0)
- rectangle.y1 = max(rectangle.y1, 0)
- rectangle.x2 = min(rectangle.x2, width)
- rectangle.y2 = min(rectangle.y2, height)
-
- return rectangle
- """
-
def clip_area(self, area: Area, clipped: Area) -> bool:
"""Shrink the area to the region shared by the two areas"""
if self.colorspace.depth < 8:
pixels_per_byte = 8 // self.colorspace.depth
if self.colorspace.pixels_in_byte_share_row:
- region_x1 /= pixels_per_byte * self.colorspace.bytes_per_cell
- region_x2 /= pixels_per_byte * self.colorspace.bytes_per_cell
+ region_x1 //= pixels_per_byte * self.colorspace.bytes_per_cell
+ region_x2 //= pixels_per_byte * self.colorspace.bytes_per_cell
else:
- region_y1 /= pixels_per_byte * self.colorspace.bytes_per_cell
- region_y2 /= pixels_per_byte * self.colorspace.bytes_per_cell
+ region_y1 //= pixels_per_byte * self.colorspace.bytes_per_cell
+ region_y2 //= pixels_per_byte * self.colorspace.bytes_per_cell
region_x2 -= 1
region_y2 -= 1
)
else:
data_type = DISPLAY_COMMAND
- """
- self._core.send(
- DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command]) + data
- )
- self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
- """
if self.ram_width < 0x100: # Single Byte Bounds
data = struct.pack(">BB", region_x1, region_x2)
"""
Send the data to the current bus
"""
+ print(data_type, chip_select, data)
self._send(data_type, chip_select, data)
def begin_transaction(self) -> None:
self._layers = []
self._supported_types = (TileGrid, Group)
self._in_group = False
+ self._item_removed = False
self._absolute_transform = TransformStruct(0, 0, 1, 1, 1, False, False, False)
self._set_scale(scale) # Set the scale via the setter
if isinstance(layer, (Group, TileGrid)):
layer._finish_refresh() # pylint: disable=protected-access
+ def _get_refresh_areas(self, areas: list[Area]) -> None:
+ for layer in self._layers:
+ if isinstance(layer, (Group, TileGrid)):
+ if not layer.hidden:
+ layer._get_refresh_areas(areas) # 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
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
+
def clamp(value, min_value, max_value):
"""Clamp a value between a minimum and maximum value"""
return max(min(max_value, value), min_value)
+
def bswap16(value):
"""Swap the bytes in a 16 bit value"""
- return (value & 0xFF00) >> 8 | (value & 0x00FF) << 8
\ No newline at end of file
+ return (value & 0xFF00) >> 8 | (value & 0x00FF) << 8
allocate_display_bus,
)
- allocate_display_bus(cls)
- return super().__new__(cls)
+ display_bus_instance = super().__new__(cls)
+ allocate_display_bus(display_bus_instance)
+ return display_bus_instance
def _release(self):
self.reset()
"""Returns True if the palette index is transparent. Returns False if opaque."""
return self._colors[palette_index].transparent
+ def _finish_refresh(self):
+ pass
+
@property
def dither(self) -> bool:
"""When true the palette dithers the output by adding
"""
+import struct
from ._bitmap import Bitmap
+from ._area import Area
+from ._helpers import clamp
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
def __init__(
self, width: int, height: int, *, mirror_x: bool = False, mirror_y: bool = False
):
- # pylint: disable=unused-argument
"""Create a Shape object with the given fixed size. Each pixel is one bit and is
stored by the column boundaries of the shape on each row. Each row’s boundary
defaults to the full row.
"""
+ self._mirror_x = mirror_x
+ self._mirror_y = mirror_y
+ self._width = width
+ self._height = height
+ if self._mirror_x:
+ width //= 2
+ width += self._width % 2
+ self._half_width = width
+ if self._mirror_y:
+ height //= 2
+ height += self._height % 2
+ self._half_height = height
+ self._data = bytearray(height * struct.calcsize("HH"))
+ for i in range(height):
+ self._data[2 * i] = 0
+ self._data[2 * i + 1] = width
+
+ self._dirty_area = Area(0, 0, width, height)
super().__init__(width, height, 2)
def set_boundary(self, y: int, start_x: int, end_x: int) -> None:
- # pylint: disable=unnecessary-pass
"""Loads pre-packed data into the given row."""
- pass
+ max_y = self._height - 1
+ if self._mirror_y:
+ max_y = self._half_height - 1
+ y = clamp(y, 0, max_y)
+ max_x = self._width - 1
+ if self._mirror_x:
+ max_x = self._half_width - 1
+ start_x = clamp(start_x, 0, max_x)
+ end_x = clamp(end_x, 0, max_x)
+
+ # find x-boundaries for updating based on current data and start_x, end_x, and mirror_x
+ lower_x = min(start_x, self._data[2 * y])
+
+ if self._mirror_x:
+ upper_x = (
+ self._width - lower_x + 1
+ ) # dirty rectangles are treated with max value exclusive
+ else:
+ upper_x = max(
+ end_x, self._data[2 * y + 1]
+ ) # dirty rectangles are treated with max value exclusive
+
+ # find y-boundaries based on y and mirror_y
+ lower_y = y
+
+ if self._mirror_y:
+ upper_y = (
+ self._height - lower_y + 1
+ ) # dirty rectangles are treated with max value exclusive
+ else:
+ upper_y = y + 1 # dirty rectangles are treated with max value exclusive
+
+ self._data[2 * y] = start_x # update the data array with the new boundaries
+ self._data[2 * y + 1] = end_x
+
+ if self._dirty_area.x1 == self._dirty_area.x2: # dirty region is empty
+ self._dirty_area.x1 = lower_x
+ self._dirty_area.x2 = upper_x
+ self._dirty_area.y1 = lower_y
+ self._dirty_area.y2 = upper_y
+ else:
+ self._dirty_area.x1 = min(lower_x, self._dirty_area.x1)
+ self._dirty_area.x2 = max(upper_x, self._dirty_area.x2)
+ self._dirty_area.y1 = min(lower_y, self._dirty_area.y1)
+ self._dirty_area.y2 = max(upper_y, self._dirty_area.y2)
+
+ def _get_pixel(self, x: int, y: int) -> int:
+ if x >= self._width or x < 0 or y >= self._height or y < 0:
+ return 0
+ if self._mirror_x and x >= self._half_width:
+ x = self._width - x - 1
+ if self._mirror_y and y >= self._half_height:
+ y = self._height - y - 1
+ start_x = self._data[2 * y]
+ end_x = self._data[2 * y + 1]
+ if x < start_x or x >= end_x:
+ return 0
+ return 1
+
+ def _finish_refresh(self):
+ self._dirty_area.x1 = 0
+ self._dirty_area.x2 = 0
+
+ def _get_refresh_areas(self, areas: list[Area]) -> None:
+ if self._dirty_area.x1 != self._dirty_area.x2:
+ areas.append(self._dirty_area)
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._x = x
self._y = y
self._width_in_tiles = width
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._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
def _update_transform(self, absolute_transform):
"""Update the parent transform and child transforms"""
width = self._pixel_height
else:
width = self._pixel_width
+
if self._absolute_transform.transpose_xy:
self._current_area.y1 = (
self._absolute_transform.y + self._absolute_transform.dy * self._x
height = self._pixel_width
else:
height = self._pixel_height
+
if self._absolute_transform.transpose_xy:
self._current_area.x1 = (
self._absolute_transform.x + self._absolute_transform.dx * self._y
# If no tiles are present we have no impact
tiles = self._tiles
- if self._hidden_tilegrid:
+ if self._hidden_tilegrid or self._hidden_by_parent:
return False
overlap = Area()
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
full_coverage = area == overlap
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]
+ 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:
return full_coverage
def _finish_refresh(self):
- pass
+ 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, Shape)):
+ 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)
+ 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)
+ return
+ areas.append(self._current_area)
+ areas.append(self._previous_area)
+ return
+
+ tail = areas[-1]
+ # 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
+ if tail != areas[-1]:
+ # 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:
+ areas[-1].copy_into(self._dirty_area)
+ self._partial_change = True
+ else:
+ self._full_change = True
+ elif isinstance(self._bitmap, Shape):
+ self._bitmap._get_refresh_areas(areas) # pylint: disable=protected-access
+ if areas[-1] != tail:
+ areas[-1].copy_into(self._dirty_area)
+ self._partial_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)
+ 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)
@property
def hidden(self) -> bool: