Initial Commit
This commit is contained in:
parent
549cbef93b
commit
4fa256c11c
|
@ -15,8 +15,6 @@ dist/
|
||||||
downloads/
|
downloads/
|
||||||
eggs/
|
eggs/
|
||||||
.eggs/
|
.eggs/
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
parts/
|
||||||
sdist/
|
sdist/
|
||||||
var/
|
var/
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
import time
|
||||||
|
import board
|
||||||
|
import displayio
|
||||||
|
#import fourwire
|
||||||
|
import adafruit_ssd1680
|
||||||
|
import busio
|
||||||
|
import terminalio
|
||||||
|
from adafruit_display_text import label
|
||||||
|
|
||||||
|
BLACK = 0x000000
|
||||||
|
WHITE = 0xFFFFFF
|
||||||
|
RED = 0xFF0000
|
||||||
|
|
||||||
|
FOREGROUND_COLOR = BLACK
|
||||||
|
BACKGROUND_COLOR = WHITE
|
||||||
|
|
||||||
|
# Create the display object - the third color is red (0xff0000)
|
||||||
|
DISPLAY_WIDTH = 296
|
||||||
|
DISPLAY_HEIGHT = 127
|
||||||
|
|
||||||
|
# For 8.x.x and 9.x.x. When 8.x.x is discontinued as a stable release, change this.
|
||||||
|
try:
|
||||||
|
from fourwire import FourWire
|
||||||
|
except ImportError:
|
||||||
|
from displayio import FourWire
|
||||||
|
|
||||||
|
displayio.release_displays()
|
||||||
|
|
||||||
|
# Pin order defined based on WeAct Header order
|
||||||
|
edp_busy = board.IO0
|
||||||
|
epd_reset = board.IO1
|
||||||
|
epd_dc = board.IO2
|
||||||
|
epd_cs = board.IO3
|
||||||
|
edp_clk = board.IO4
|
||||||
|
edp_mosi = board.IO5
|
||||||
|
#edp_miso =
|
||||||
|
spi = busio.SPI(clock=edp_clk, MOSI=edp_mosi)
|
||||||
|
|
||||||
|
display_bus = FourWire(
|
||||||
|
spi, command=epd_dc, chip_select=epd_cs, reset=epd_reset, baudrate=1000000
|
||||||
|
)
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
display = adafruit_ssd1680.SSD1680(
|
||||||
|
display_bus,
|
||||||
|
colstart=1,
|
||||||
|
width=DISPLAY_WIDTH,
|
||||||
|
height=DISPLAY_HEIGHT,
|
||||||
|
busy_pin=edp_busy,
|
||||||
|
highlight_color=FOREGROUND_COLOR,
|
||||||
|
rotation=270,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a display group for our screen objects
|
||||||
|
g = displayio.Group()
|
||||||
|
|
||||||
|
# Set a background
|
||||||
|
background_bitmap = displayio.Bitmap(DISPLAY_WIDTH, DISPLAY_HEIGHT, 1)
|
||||||
|
# Map colors in a palette
|
||||||
|
palette = displayio.Palette(1)
|
||||||
|
palette[0] = BACKGROUND_COLOR
|
||||||
|
|
||||||
|
# Create a Tilegrid with the background and put in the displayio group
|
||||||
|
t = displayio.TileGrid(background_bitmap, pixel_shader=palette)
|
||||||
|
g.append(t)
|
||||||
|
|
||||||
|
# Draw simple text using the built-in font into a displayio group
|
||||||
|
text_group = displayio.Group(scale=3, x=20, y=80)
|
||||||
|
text = "Hello World2!"
|
||||||
|
text_area = label.Label(terminalio.FONT, text=text, color=FOREGROUND_COLOR)
|
||||||
|
text_group.append(text_area) # Add this text to the text group
|
||||||
|
g.append(text_group)
|
||||||
|
|
||||||
|
# Place the display group on the screen
|
||||||
|
display.root_group = g
|
||||||
|
|
||||||
|
# Refresh the display to have everything show on the display
|
||||||
|
# NOTE: Do not refresh eInk displays more often than 180 seconds!
|
||||||
|
display.refresh()
|
||||||
|
|
||||||
|
time.sleep(120)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,476 @@
|
||||||
|
# SPDX-FileCopyrightText: 2020 Tim C, 2021 Jeff Epler for Adafruit Industries
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
`adafruit_display_text`
|
||||||
|
=======================
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "0.0.0+auto.0"
|
||||||
|
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git"
|
||||||
|
|
||||||
|
from displayio import Group, Palette
|
||||||
|
|
||||||
|
try:
|
||||||
|
from typing import Optional, List, Tuple
|
||||||
|
from fontio import FontProtocol
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_text_to_pixels(
|
||||||
|
string: str,
|
||||||
|
max_width: int,
|
||||||
|
font: Optional[FontProtocol] = None,
|
||||||
|
indent0: str = "",
|
||||||
|
indent1: str = "",
|
||||||
|
) -> List[str]:
|
||||||
|
# pylint: disable=too-many-branches, too-many-locals, too-many-nested-blocks, too-many-statements
|
||||||
|
|
||||||
|
"""wrap_text_to_pixels function
|
||||||
|
A helper that will return a list of lines with word-break wrapping.
|
||||||
|
Leading and trailing whitespace in your string will be removed. If
|
||||||
|
you wish to use leading whitespace see ``indent0`` and ``indent1``
|
||||||
|
parameters.
|
||||||
|
|
||||||
|
:param str string: The text to be wrapped.
|
||||||
|
:param int max_width: The maximum number of pixels on a line before wrapping.
|
||||||
|
:param font: The font to use for measuring the text.
|
||||||
|
:type font: ~fontio.FontProtocol
|
||||||
|
:param str indent0: Additional character(s) to add to the first line.
|
||||||
|
:param str indent1: Additional character(s) to add to all other lines.
|
||||||
|
|
||||||
|
:return: A list of the lines resulting from wrapping the
|
||||||
|
input text at ``max_width`` pixels size
|
||||||
|
:rtype: List[str]
|
||||||
|
|
||||||
|
"""
|
||||||
|
if font is None:
|
||||||
|
|
||||||
|
def measure(text):
|
||||||
|
return len(text)
|
||||||
|
|
||||||
|
else:
|
||||||
|
if hasattr(font, "load_glyphs"):
|
||||||
|
font.load_glyphs(string)
|
||||||
|
|
||||||
|
def measure(text):
|
||||||
|
total_len = 0
|
||||||
|
for char in text:
|
||||||
|
this_glyph = font.get_glyph(ord(char))
|
||||||
|
if this_glyph:
|
||||||
|
total_len += this_glyph.shift_x
|
||||||
|
return total_len
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
partial = [indent0]
|
||||||
|
width = measure(indent0)
|
||||||
|
swidth = measure(" ")
|
||||||
|
firstword = True
|
||||||
|
for line_in_input in string.split("\n"):
|
||||||
|
newline = True
|
||||||
|
for index, word in enumerate(line_in_input.split(" ")):
|
||||||
|
wwidth = measure(word)
|
||||||
|
word_parts = []
|
||||||
|
cur_part = ""
|
||||||
|
|
||||||
|
if wwidth > max_width:
|
||||||
|
for char in word:
|
||||||
|
if newline:
|
||||||
|
extraspace = 0
|
||||||
|
leadchar = ""
|
||||||
|
else:
|
||||||
|
extraspace = swidth
|
||||||
|
leadchar = " "
|
||||||
|
if (
|
||||||
|
measure("".join(partial))
|
||||||
|
+ measure(cur_part)
|
||||||
|
+ measure(char)
|
||||||
|
+ measure("-")
|
||||||
|
+ extraspace
|
||||||
|
> max_width
|
||||||
|
):
|
||||||
|
if cur_part:
|
||||||
|
word_parts.append(
|
||||||
|
"".join(partial) + leadchar + cur_part + "-"
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
word_parts.append("".join(partial))
|
||||||
|
cur_part = char
|
||||||
|
partial = [indent1]
|
||||||
|
newline = True
|
||||||
|
else:
|
||||||
|
cur_part += char
|
||||||
|
if cur_part:
|
||||||
|
word_parts.append(cur_part)
|
||||||
|
for line in word_parts[:-1]:
|
||||||
|
lines.append(line)
|
||||||
|
partial.append(word_parts[-1])
|
||||||
|
width = measure(word_parts[-1])
|
||||||
|
if firstword:
|
||||||
|
firstword = False
|
||||||
|
else:
|
||||||
|
if firstword:
|
||||||
|
partial.append(word)
|
||||||
|
firstword = False
|
||||||
|
width += wwidth
|
||||||
|
elif width + swidth + wwidth < max_width:
|
||||||
|
if index > 0:
|
||||||
|
partial.append(" ")
|
||||||
|
partial.append(word)
|
||||||
|
width += wwidth + swidth
|
||||||
|
else:
|
||||||
|
lines.append("".join(partial))
|
||||||
|
partial = [indent1, word]
|
||||||
|
width = measure(indent1) + wwidth
|
||||||
|
if newline:
|
||||||
|
newline = False
|
||||||
|
|
||||||
|
lines.append("".join(partial))
|
||||||
|
partial = [indent1]
|
||||||
|
width = measure(indent1)
|
||||||
|
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_text_to_lines(string: str, max_chars: int) -> List[str]:
|
||||||
|
"""wrap_text_to_lines function
|
||||||
|
A helper that will return a list of lines with word-break wrapping
|
||||||
|
|
||||||
|
:param str string: The text to be wrapped
|
||||||
|
:param int max_chars: The maximum number of characters on a line before wrapping
|
||||||
|
|
||||||
|
:return: A list of lines where each line is separated based on the amount
|
||||||
|
of ``max_chars`` provided
|
||||||
|
:rtype: List[str]
|
||||||
|
"""
|
||||||
|
|
||||||
|
def chunks(lst, n):
|
||||||
|
"""Yield successive n-sized chunks from lst."""
|
||||||
|
for i in range(0, len(lst), n):
|
||||||
|
yield lst[i : i + n]
|
||||||
|
|
||||||
|
string = string.replace("\n", "").replace("\r", "") # Strip confusing newlines
|
||||||
|
words = string.split(" ")
|
||||||
|
the_lines = []
|
||||||
|
the_line = ""
|
||||||
|
for w in words:
|
||||||
|
if len(w) > max_chars:
|
||||||
|
if the_line: # add what we had stored
|
||||||
|
the_lines.append(the_line)
|
||||||
|
parts = []
|
||||||
|
for part in chunks(w, max_chars - 1):
|
||||||
|
parts.append("{}-".format(part))
|
||||||
|
the_lines.extend(parts[:-1])
|
||||||
|
the_line = parts[-1][:-1]
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(the_line + " " + w) <= max_chars:
|
||||||
|
the_line += " " + w
|
||||||
|
elif not the_line and len(w) == max_chars:
|
||||||
|
the_lines.append(w)
|
||||||
|
else:
|
||||||
|
the_lines.append(the_line)
|
||||||
|
the_line = "" + w
|
||||||
|
if the_line: # Last line remaining
|
||||||
|
the_lines.append(the_line)
|
||||||
|
# Remove any blank lines
|
||||||
|
while not the_lines[0]:
|
||||||
|
del the_lines[0]
|
||||||
|
# Remove first space from first line:
|
||||||
|
if the_lines[0][0] == " ":
|
||||||
|
the_lines[0] = the_lines[0][1:]
|
||||||
|
return the_lines
|
||||||
|
|
||||||
|
|
||||||
|
class LabelBase(Group):
|
||||||
|
# pylint: disable=too-many-instance-attributes
|
||||||
|
|
||||||
|
"""Superclass that all other types of labels will extend. This contains
|
||||||
|
all of the properties and functions that work the same way in all labels.
|
||||||
|
|
||||||
|
**Note:** This should be treated as an abstract base class.
|
||||||
|
|
||||||
|
Subclasses should implement ``_set_text``, ``_set_font``, and ``_set_line_spacing`` to
|
||||||
|
have the correct behavior for that type of label.
|
||||||
|
|
||||||
|
:param font: A font class that has ``get_bounding_box`` and ``get_glyph``.
|
||||||
|
Must include a capital M for measuring character size.
|
||||||
|
:type font: ~fontio.FontProtocol
|
||||||
|
:param str text: Text to display
|
||||||
|
:param int color: Color of all text in RGB hex
|
||||||
|
:param int background_color: Color of the background, use `None` for transparent
|
||||||
|
:param float line_spacing: Line spacing of text to display
|
||||||
|
:param bool background_tight: Set `True` only if you want background box to tightly
|
||||||
|
surround text. When set to 'True' Padding parameters will be ignored.
|
||||||
|
:param int padding_top: Additional pixels added to background bounding box at top
|
||||||
|
:param int padding_bottom: Additional pixels added to background bounding box at bottom
|
||||||
|
:param int padding_left: Additional pixels added to background bounding box at left
|
||||||
|
:param int padding_right: Additional pixels added to background bounding box at right
|
||||||
|
:param (float,float) anchor_point: Point that anchored_position moves relative to.
|
||||||
|
Tuple with decimal percentage of width and height.
|
||||||
|
(E.g. (0,0) is top left, (1.0, 0.5): is middle right.)
|
||||||
|
:param (int,int) anchored_position: Position relative to the anchor_point. Tuple
|
||||||
|
containing x,y pixel coordinates.
|
||||||
|
:param int scale: Integer value of the pixel scaling
|
||||||
|
:param bool base_alignment: when True allows to align text label to the baseline.
|
||||||
|
This is helpful when two or more labels need to be aligned to the same baseline
|
||||||
|
:param (int,str) tab_replacement: tuple with tab character replace information. When
|
||||||
|
(4, " ") will indicate a tab replacement of 4 spaces, defaults to 4 spaces by
|
||||||
|
tab character
|
||||||
|
:param str label_direction: string defining the label text orientation. See the
|
||||||
|
subclass documentation for the possible values.
|
||||||
|
:param bool verbose: print debugging information in some internal functions. Default to False
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
font: FontProtocol,
|
||||||
|
x: int = 0,
|
||||||
|
y: int = 0,
|
||||||
|
text: str = "",
|
||||||
|
color: int = 0xFFFFFF,
|
||||||
|
background_color: int = None,
|
||||||
|
line_spacing: float = 1.25,
|
||||||
|
background_tight: bool = False,
|
||||||
|
padding_top: int = 0,
|
||||||
|
padding_bottom: int = 0,
|
||||||
|
padding_left: int = 0,
|
||||||
|
padding_right: int = 0,
|
||||||
|
anchor_point: Tuple[float, float] = None,
|
||||||
|
anchored_position: Tuple[int, int] = None,
|
||||||
|
scale: int = 1,
|
||||||
|
base_alignment: bool = False,
|
||||||
|
tab_replacement: Tuple[int, str] = (4, " "),
|
||||||
|
label_direction: str = "LTR",
|
||||||
|
verbose: bool = False,
|
||||||
|
**kwargs, # pylint: disable=unused-argument
|
||||||
|
) -> None:
|
||||||
|
# pylint: disable=too-many-arguments, too-many-locals
|
||||||
|
|
||||||
|
super().__init__(x=x, y=y, scale=1)
|
||||||
|
|
||||||
|
self._font = font
|
||||||
|
self._text = text
|
||||||
|
self._palette = Palette(2)
|
||||||
|
self._color = 0xFFFFFF
|
||||||
|
self._background_color = None
|
||||||
|
self._line_spacing = line_spacing
|
||||||
|
self._background_tight = background_tight
|
||||||
|
self._padding_top = padding_top
|
||||||
|
self._padding_bottom = padding_bottom
|
||||||
|
self._padding_left = padding_left
|
||||||
|
self._padding_right = padding_right
|
||||||
|
self._anchor_point = anchor_point
|
||||||
|
self._anchored_position = anchored_position
|
||||||
|
self._base_alignment = base_alignment
|
||||||
|
self._label_direction = label_direction
|
||||||
|
self._tab_replacement = tab_replacement
|
||||||
|
self._tab_text = self._tab_replacement[1] * self._tab_replacement[0]
|
||||||
|
self._verbose = verbose
|
||||||
|
|
||||||
|
if "max_glyphs" in kwargs:
|
||||||
|
print("Please update your code: 'max_glyphs' is not needed anymore.")
|
||||||
|
|
||||||
|
self._ascent, self._descent = self._get_ascent_descent()
|
||||||
|
self._bounding_box = None
|
||||||
|
|
||||||
|
self.color = color
|
||||||
|
self.background_color = background_color
|
||||||
|
|
||||||
|
# local group will hold background and text
|
||||||
|
# the self group scale should always remain at 1, the self._local_group will
|
||||||
|
# be used to set the scale of the label
|
||||||
|
self._local_group = Group(scale=scale)
|
||||||
|
self.append(self._local_group)
|
||||||
|
|
||||||
|
self._baseline = -1.0
|
||||||
|
|
||||||
|
if self._base_alignment:
|
||||||
|
self._y_offset = 0
|
||||||
|
else:
|
||||||
|
self._y_offset = self._ascent // 2
|
||||||
|
|
||||||
|
def _get_ascent_descent(self) -> Tuple[int, int]:
|
||||||
|
"""Private function to calculate ascent and descent font values"""
|
||||||
|
if hasattr(self.font, "ascent") and hasattr(self.font, "descent"):
|
||||||
|
return self.font.ascent, self.font.descent
|
||||||
|
|
||||||
|
# check a few glyphs for maximum ascender and descender height
|
||||||
|
glyphs = "M j'" # choose glyphs with highest ascender and lowest
|
||||||
|
try:
|
||||||
|
self._font.load_glyphs(glyphs)
|
||||||
|
except AttributeError:
|
||||||
|
# Builtin font doesn't have or need load_glyphs
|
||||||
|
pass
|
||||||
|
# descender, will depend upon font used
|
||||||
|
ascender_max = descender_max = 0
|
||||||
|
for char in glyphs:
|
||||||
|
this_glyph = self._font.get_glyph(ord(char))
|
||||||
|
if this_glyph:
|
||||||
|
ascender_max = max(ascender_max, this_glyph.height + this_glyph.dy)
|
||||||
|
descender_max = max(descender_max, -this_glyph.dy)
|
||||||
|
return ascender_max, descender_max
|
||||||
|
|
||||||
|
@property
|
||||||
|
def font(self) -> FontProtocol:
|
||||||
|
"""Font to use for text display."""
|
||||||
|
return self._font
|
||||||
|
|
||||||
|
def _set_font(self, new_font: FontProtocol) -> None:
|
||||||
|
raise NotImplementedError("{} MUST override '_set_font'".format(type(self)))
|
||||||
|
|
||||||
|
@font.setter
|
||||||
|
def font(self, new_font: FontProtocol) -> None:
|
||||||
|
self._set_font(new_font)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color(self) -> int:
|
||||||
|
"""Color of the text as an RGB hex number."""
|
||||||
|
return self._color
|
||||||
|
|
||||||
|
@color.setter
|
||||||
|
def color(self, new_color: int):
|
||||||
|
self._color = new_color
|
||||||
|
if new_color is not None:
|
||||||
|
self._palette[1] = new_color
|
||||||
|
self._palette.make_opaque(1)
|
||||||
|
else:
|
||||||
|
self._palette[1] = 0
|
||||||
|
self._palette.make_transparent(1)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def background_color(self) -> int:
|
||||||
|
"""Color of the background as an RGB hex number."""
|
||||||
|
return self._background_color
|
||||||
|
|
||||||
|
def _set_background_color(self, new_color):
|
||||||
|
raise NotImplementedError(
|
||||||
|
"{} MUST override '_set_background_color'".format(type(self))
|
||||||
|
)
|
||||||
|
|
||||||
|
@background_color.setter
|
||||||
|
def background_color(self, new_color: int) -> None:
|
||||||
|
self._set_background_color(new_color)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def anchor_point(self) -> Tuple[float, float]:
|
||||||
|
"""Point that anchored_position moves relative to.
|
||||||
|
Tuple with decimal percentage of width and height.
|
||||||
|
(E.g. (0,0) is top left, (1.0, 0.5): is middle right.)"""
|
||||||
|
return self._anchor_point
|
||||||
|
|
||||||
|
@anchor_point.setter
|
||||||
|
def anchor_point(self, new_anchor_point: Tuple[float, float]) -> None:
|
||||||
|
if new_anchor_point[1] == self._baseline:
|
||||||
|
self._anchor_point = (new_anchor_point[0], -1.0)
|
||||||
|
else:
|
||||||
|
self._anchor_point = new_anchor_point
|
||||||
|
|
||||||
|
# update the anchored_position using setter
|
||||||
|
self.anchored_position = self._anchored_position
|
||||||
|
|
||||||
|
@property
|
||||||
|
def anchored_position(self) -> Tuple[int, int]:
|
||||||
|
"""Position relative to the anchor_point. Tuple containing x,y
|
||||||
|
pixel coordinates."""
|
||||||
|
return self._anchored_position
|
||||||
|
|
||||||
|
@anchored_position.setter
|
||||||
|
def anchored_position(self, new_position: Tuple[int, int]) -> None:
|
||||||
|
self._anchored_position = new_position
|
||||||
|
# Calculate (x,y) position
|
||||||
|
if (self._anchor_point is not None) and (self._anchored_position is not None):
|
||||||
|
self.x = int(
|
||||||
|
new_position[0]
|
||||||
|
- (self._bounding_box[0] * self.scale)
|
||||||
|
- round(self._anchor_point[0] * (self._bounding_box[2] * self.scale))
|
||||||
|
)
|
||||||
|
if self._anchor_point[1] == self._baseline:
|
||||||
|
self.y = int(new_position[1] - (self._y_offset * self.scale))
|
||||||
|
else:
|
||||||
|
self.y = int(
|
||||||
|
new_position[1]
|
||||||
|
- (self._bounding_box[1] * self.scale)
|
||||||
|
- round(self._anchor_point[1] * self._bounding_box[3] * self.scale)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scale(self) -> int:
|
||||||
|
"""Set the scaling of the label, in integer values"""
|
||||||
|
return self._local_group.scale
|
||||||
|
|
||||||
|
@scale.setter
|
||||||
|
def scale(self, new_scale: int) -> None:
|
||||||
|
self._local_group.scale = new_scale
|
||||||
|
self.anchored_position = self._anchored_position # update the anchored_position
|
||||||
|
|
||||||
|
def _set_text(self, new_text: str, scale: int) -> None:
|
||||||
|
raise NotImplementedError("{} MUST override '_set_text'".format(type(self)))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def text(self) -> str:
|
||||||
|
"""Text to be displayed."""
|
||||||
|
return self._text
|
||||||
|
|
||||||
|
@text.setter # Cannot set color or background color with text setter, use separate setter
|
||||||
|
def text(self, new_text: str) -> None:
|
||||||
|
self._set_text(new_text, self.scale)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bounding_box(self) -> Tuple[int, int]:
|
||||||
|
"""An (x, y, w, h) tuple that completely covers all glyphs. The
|
||||||
|
first two numbers are offset from the x, y origin of this group"""
|
||||||
|
return tuple(self._bounding_box)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def height(self) -> int:
|
||||||
|
"""The height of the label determined from the bounding box."""
|
||||||
|
return self._bounding_box[3]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def width(self) -> int:
|
||||||
|
"""The width of the label determined from the bounding box."""
|
||||||
|
return self._bounding_box[2]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def line_spacing(self) -> float:
|
||||||
|
"""The amount of space between lines of text, in multiples of the font's
|
||||||
|
bounding-box height. (E.g. 1.0 is the bounding-box height)"""
|
||||||
|
return self._line_spacing
|
||||||
|
|
||||||
|
def _set_line_spacing(self, new_line_spacing: float) -> None:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"{} MUST override '_set_line_spacing'".format(type(self))
|
||||||
|
)
|
||||||
|
|
||||||
|
@line_spacing.setter
|
||||||
|
def line_spacing(self, new_line_spacing: float) -> None:
|
||||||
|
self._set_line_spacing(new_line_spacing)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def label_direction(self) -> str:
|
||||||
|
"""Set the text direction of the label"""
|
||||||
|
return self._label_direction
|
||||||
|
|
||||||
|
def _set_label_direction(self, new_label_direction: str) -> None:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"{} MUST override '_set_label_direction'".format(type(self))
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_valid_label_directions(self) -> Tuple[str, ...]:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"{} MUST override '_get_valid_label_direction'".format(type(self))
|
||||||
|
)
|
||||||
|
|
||||||
|
@label_direction.setter
|
||||||
|
def label_direction(self, new_label_direction: str) -> None:
|
||||||
|
"""Set the text direction of the label"""
|
||||||
|
if new_label_direction not in self._get_valid_label_directions():
|
||||||
|
raise RuntimeError("Please provide a valid text direction")
|
||||||
|
self._set_label_direction(new_label_direction)
|
||||||
|
|
||||||
|
def _replace_tabs(self, text: str) -> str:
|
||||||
|
return text if text.find("\t") < 0 else self._tab_text.join(text.split("\t"))
|
|
@ -0,0 +1,594 @@
|
||||||
|
# SPDX-FileCopyrightText: 2020 Kevin Matocha
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
`adafruit_display_text.bitmap_label`
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
Text graphics handling for CircuitPython, including text boxes
|
||||||
|
|
||||||
|
|
||||||
|
* Author(s): Kevin Matocha
|
||||||
|
|
||||||
|
Implementation Notes
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
**Hardware:**
|
||||||
|
|
||||||
|
**Software and Dependencies:**
|
||||||
|
|
||||||
|
* Adafruit CircuitPython firmware for the supported boards:
|
||||||
|
https://circuitpython.org/downloads
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "0.0.0+auto.0"
|
||||||
|
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git"
|
||||||
|
|
||||||
|
import displayio
|
||||||
|
from adafruit_display_text import LabelBase
|
||||||
|
|
||||||
|
try:
|
||||||
|
import bitmaptools
|
||||||
|
except ImportError:
|
||||||
|
# We have a slower fallback for bitmaptools
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
from fontio import FontProtocol
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-instance-attributes
|
||||||
|
class Label(LabelBase):
|
||||||
|
"""A label displaying a string of text that is stored in a bitmap.
|
||||||
|
Note: This ``bitmap_label.py`` library utilizes a :py:class:`~displayio.Bitmap`
|
||||||
|
to display the text. This method is memory-conserving relative to ``label.py``.
|
||||||
|
|
||||||
|
For further reduction in memory usage, set ``save_text=False`` (text string will not
|
||||||
|
be stored and ``line_spacing`` and ``font`` are immutable with ``save_text``
|
||||||
|
set to ``False``).
|
||||||
|
|
||||||
|
The origin point set by ``x`` and ``y``
|
||||||
|
properties will be the left edge of the bounding box, and in the center of a M
|
||||||
|
glyph (if its one line), or the (number of lines * linespacing + M)/2. That is,
|
||||||
|
it will try to have it be center-left as close as possible.
|
||||||
|
|
||||||
|
:param font: A font class that has ``get_bounding_box`` and ``get_glyph``.
|
||||||
|
Must include a capital M for measuring character size.
|
||||||
|
:type font: ~fontio.FontProtocol
|
||||||
|
:param str text: Text to display
|
||||||
|
:param int|Tuple(int, int, int) color: Color of all text in HEX or RGB
|
||||||
|
:param int|Tuple(int, int, int)|None background_color: Color of the background, use `None`
|
||||||
|
for transparent
|
||||||
|
:param float line_spacing: Line spacing of text to display
|
||||||
|
:param bool background_tight: Set `True` only if you want background box to tightly
|
||||||
|
surround text. When set to 'True' Padding parameters will be ignored.
|
||||||
|
:param int padding_top: Additional pixels added to background bounding box at top
|
||||||
|
:param int padding_bottom: Additional pixels added to background bounding box at bottom
|
||||||
|
:param int padding_left: Additional pixels added to background bounding box at left
|
||||||
|
:param int padding_right: Additional pixels added to background bounding box at right
|
||||||
|
:param Tuple(float, float) anchor_point: Point that anchored_position moves relative to.
|
||||||
|
Tuple with decimal percentage of width and height.
|
||||||
|
(E.g. (0,0) is top left, (1.0, 0.5): is middle right.)
|
||||||
|
:param Tuple(int, int) anchored_position: Position relative to the anchor_point. Tuple
|
||||||
|
containing x,y pixel coordinates.
|
||||||
|
:param int scale: Integer value of the pixel scaling
|
||||||
|
:param bool save_text: Set True to save the text string as a constant in the
|
||||||
|
label structure. Set False to reduce memory use.
|
||||||
|
:param bool base_alignment: when True allows to align text label to the baseline.
|
||||||
|
This is helpful when two or more labels need to be aligned to the same baseline
|
||||||
|
:param Tuple(int, str) tab_replacement: tuple with tab character replace information. When
|
||||||
|
(4, " ") will indicate a tab replacement of 4 spaces, defaults to 4 spaces by
|
||||||
|
tab character
|
||||||
|
:param str label_direction: string defining the label text orientation. There are 5
|
||||||
|
configurations possibles ``LTR``-Left-To-Right ``RTL``-Right-To-Left
|
||||||
|
``UPD``-Upside Down ``UPR``-Upwards ``DWR``-Downwards. It defaults to ``LTR``
|
||||||
|
:param bool verbose: print debugging information in some internal functions. Default to False
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# This maps label_direction to TileGrid's transpose_xy, flip_x, flip_y
|
||||||
|
_DIR_MAP = {
|
||||||
|
"UPR": (True, True, False),
|
||||||
|
"DWR": (True, False, True),
|
||||||
|
"UPD": (False, True, True),
|
||||||
|
"LTR": (False, False, False),
|
||||||
|
"RTL": (False, False, False),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, font: FontProtocol, save_text: bool = True, **kwargs) -> None:
|
||||||
|
self._bitmap = None
|
||||||
|
self._tilegrid = None
|
||||||
|
self._prev_label_direction = None
|
||||||
|
|
||||||
|
super().__init__(font, **kwargs)
|
||||||
|
|
||||||
|
self._save_text = save_text
|
||||||
|
self._text = self._replace_tabs(self._text)
|
||||||
|
|
||||||
|
# call the text updater with all the arguments.
|
||||||
|
self._reset_text(
|
||||||
|
font=font,
|
||||||
|
text=self._text,
|
||||||
|
line_spacing=self._line_spacing,
|
||||||
|
scale=self.scale,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _reset_text(
|
||||||
|
self,
|
||||||
|
font: Optional[FontProtocol] = None,
|
||||||
|
text: Optional[str] = None,
|
||||||
|
line_spacing: Optional[float] = None,
|
||||||
|
scale: Optional[int] = None,
|
||||||
|
) -> None:
|
||||||
|
# pylint: disable=too-many-branches, too-many-statements, too-many-locals
|
||||||
|
|
||||||
|
# Store all the instance variables
|
||||||
|
if font is not None:
|
||||||
|
self._font = font
|
||||||
|
if line_spacing is not None:
|
||||||
|
self._line_spacing = line_spacing
|
||||||
|
|
||||||
|
# if text is not provided as a parameter (text is None), use the previous value.
|
||||||
|
if (text is None) and self._save_text:
|
||||||
|
text = self._text
|
||||||
|
|
||||||
|
if self._save_text: # text string will be saved
|
||||||
|
self._text = self._replace_tabs(text)
|
||||||
|
else:
|
||||||
|
self._text = None # save a None value since text string is not saved
|
||||||
|
|
||||||
|
# Check for empty string
|
||||||
|
if (text == "") or (
|
||||||
|
text is None
|
||||||
|
): # If empty string, just create a zero-sized bounding box and that's it.
|
||||||
|
self._bounding_box = (
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0, # zero width with text == ""
|
||||||
|
0, # zero height with text == ""
|
||||||
|
)
|
||||||
|
# Clear out any items in the self._local_group Group, in case this is an
|
||||||
|
# update to the bitmap_label
|
||||||
|
for _ in self._local_group:
|
||||||
|
self._local_group.pop(0)
|
||||||
|
|
||||||
|
# Free the bitmap and tilegrid since they are removed
|
||||||
|
self._bitmap = None
|
||||||
|
self._tilegrid = None
|
||||||
|
|
||||||
|
else: # The text string is not empty, so create the Bitmap and TileGrid and
|
||||||
|
# append to the self Group
|
||||||
|
|
||||||
|
# Calculate the text bounding box
|
||||||
|
|
||||||
|
# Calculate both "tight" and "loose" bounding box dimensions to match label for
|
||||||
|
# anchor_position calculations
|
||||||
|
(
|
||||||
|
box_x,
|
||||||
|
tight_box_y,
|
||||||
|
x_offset,
|
||||||
|
tight_y_offset,
|
||||||
|
loose_box_y,
|
||||||
|
loose_y_offset,
|
||||||
|
) = self._text_bounding_box(
|
||||||
|
text,
|
||||||
|
self._font,
|
||||||
|
) # calculate the box size for a tight and loose backgrounds
|
||||||
|
|
||||||
|
if self._background_tight:
|
||||||
|
box_y = tight_box_y
|
||||||
|
y_offset = tight_y_offset
|
||||||
|
self._padding_left = 0
|
||||||
|
self._padding_right = 0
|
||||||
|
self._padding_top = 0
|
||||||
|
self._padding_bottom = 0
|
||||||
|
|
||||||
|
else: # calculate the box size for a loose background
|
||||||
|
box_y = loose_box_y
|
||||||
|
y_offset = loose_y_offset
|
||||||
|
|
||||||
|
# Calculate the background size including padding
|
||||||
|
tight_box_x = box_x
|
||||||
|
box_x = box_x + self._padding_left + self._padding_right
|
||||||
|
box_y = box_y + self._padding_top + self._padding_bottom
|
||||||
|
|
||||||
|
# Create the Bitmap unless it can be reused
|
||||||
|
new_bitmap = None
|
||||||
|
if (
|
||||||
|
self._bitmap is None
|
||||||
|
or self._bitmap.width != box_x
|
||||||
|
or self._bitmap.height != box_y
|
||||||
|
):
|
||||||
|
new_bitmap = displayio.Bitmap(box_x, box_y, len(self._palette))
|
||||||
|
self._bitmap = new_bitmap
|
||||||
|
else:
|
||||||
|
self._bitmap.fill(0)
|
||||||
|
|
||||||
|
# Place the text into the Bitmap
|
||||||
|
self._place_text(
|
||||||
|
self._bitmap,
|
||||||
|
text if self._label_direction != "RTL" else "".join(reversed(text)),
|
||||||
|
self._font,
|
||||||
|
self._padding_left - x_offset,
|
||||||
|
self._padding_top + y_offset,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._base_alignment:
|
||||||
|
label_position_yoffset = 0
|
||||||
|
else:
|
||||||
|
label_position_yoffset = self._ascent // 2
|
||||||
|
|
||||||
|
# Create the TileGrid if not created bitmap unchanged
|
||||||
|
if self._tilegrid is None or new_bitmap:
|
||||||
|
self._tilegrid = displayio.TileGrid(
|
||||||
|
self._bitmap,
|
||||||
|
pixel_shader=self._palette,
|
||||||
|
width=1,
|
||||||
|
height=1,
|
||||||
|
tile_width=box_x,
|
||||||
|
tile_height=box_y,
|
||||||
|
default_tile=0,
|
||||||
|
x=-self._padding_left + x_offset,
|
||||||
|
y=label_position_yoffset - y_offset - self._padding_top,
|
||||||
|
)
|
||||||
|
# Clear out any items in the local_group Group, in case this is an update to
|
||||||
|
# the bitmap_label
|
||||||
|
for _ in self._local_group:
|
||||||
|
self._local_group.pop(0)
|
||||||
|
self._local_group.append(
|
||||||
|
self._tilegrid
|
||||||
|
) # add the bitmap's tilegrid to the group
|
||||||
|
|
||||||
|
# Set TileGrid properties based on label_direction
|
||||||
|
if self._label_direction != self._prev_label_direction:
|
||||||
|
tg1 = self._tilegrid
|
||||||
|
tg1.transpose_xy, tg1.flip_x, tg1.flip_y = self._DIR_MAP[
|
||||||
|
self._label_direction
|
||||||
|
]
|
||||||
|
|
||||||
|
# Update bounding_box values. Note: To be consistent with label.py,
|
||||||
|
# this is the bounding box for the text only, not including the background.
|
||||||
|
if self._label_direction in ("UPR", "DWR"):
|
||||||
|
if self._label_direction == "UPR":
|
||||||
|
top = self._padding_right
|
||||||
|
left = self._padding_top
|
||||||
|
if self._label_direction == "DWR":
|
||||||
|
top = self._padding_left
|
||||||
|
left = self._padding_bottom
|
||||||
|
self._bounding_box = (
|
||||||
|
self._tilegrid.x + left,
|
||||||
|
self._tilegrid.y + top,
|
||||||
|
tight_box_y,
|
||||||
|
tight_box_x,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._bounding_box = (
|
||||||
|
self._tilegrid.x + self._padding_left,
|
||||||
|
self._tilegrid.y + self._padding_top,
|
||||||
|
tight_box_x,
|
||||||
|
tight_box_y,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
scale is not None
|
||||||
|
): # Scale will be defined in local_group (Note: self should have scale=1)
|
||||||
|
self.scale = scale # call the setter
|
||||||
|
|
||||||
|
# set the anchored_position with setter after bitmap is created, sets the
|
||||||
|
# x,y positions of the label
|
||||||
|
self.anchored_position = self._anchored_position
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _line_spacing_ypixels(font: FontProtocol, line_spacing: float) -> int:
|
||||||
|
# Note: Scaling is provided at the Group level
|
||||||
|
return_value = int(line_spacing * font.get_bounding_box()[1])
|
||||||
|
return return_value
|
||||||
|
|
||||||
|
def _text_bounding_box(
|
||||||
|
self, text: str, font: FontProtocol
|
||||||
|
) -> Tuple[int, int, int, int, int, int]:
|
||||||
|
# pylint: disable=too-many-locals
|
||||||
|
|
||||||
|
ascender_max, descender_max = self._ascent, self._descent
|
||||||
|
|
||||||
|
lines = 1
|
||||||
|
|
||||||
|
xposition = (
|
||||||
|
x_start
|
||||||
|
) = yposition = y_start = 0 # starting x and y position (left margin)
|
||||||
|
|
||||||
|
left = None
|
||||||
|
right = x_start
|
||||||
|
top = bottom = y_start
|
||||||
|
|
||||||
|
y_offset_tight = self._ascent // 2
|
||||||
|
|
||||||
|
newlines = 0
|
||||||
|
line_spacing = self._line_spacing
|
||||||
|
|
||||||
|
for char in text:
|
||||||
|
if char == "\n": # newline
|
||||||
|
newlines += 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
my_glyph = font.get_glyph(ord(char))
|
||||||
|
|
||||||
|
if my_glyph is None: # Error checking: no glyph found
|
||||||
|
print("Glyph not found: {}".format(repr(char)))
|
||||||
|
else:
|
||||||
|
if newlines:
|
||||||
|
xposition = x_start # reset to left column
|
||||||
|
yposition += (
|
||||||
|
self._line_spacing_ypixels(font, line_spacing) * newlines
|
||||||
|
) # Add the newline(s)
|
||||||
|
lines += newlines
|
||||||
|
newlines = 0
|
||||||
|
if xposition == x_start:
|
||||||
|
if left is None:
|
||||||
|
left = 0
|
||||||
|
else:
|
||||||
|
left = min(left, my_glyph.dx)
|
||||||
|
xright = xposition + my_glyph.width + my_glyph.dx
|
||||||
|
xposition += my_glyph.shift_x
|
||||||
|
|
||||||
|
right = max(right, xposition, xright)
|
||||||
|
|
||||||
|
if yposition == y_start: # first line, find the Ascender height
|
||||||
|
top = min(top, -my_glyph.height - my_glyph.dy + y_offset_tight)
|
||||||
|
bottom = max(bottom, yposition - my_glyph.dy + y_offset_tight)
|
||||||
|
|
||||||
|
if left is None:
|
||||||
|
left = 0
|
||||||
|
|
||||||
|
final_box_width = right - left
|
||||||
|
|
||||||
|
final_box_height_tight = bottom - top
|
||||||
|
final_y_offset_tight = -top + y_offset_tight
|
||||||
|
|
||||||
|
final_box_height_loose = (lines - 1) * self._line_spacing_ypixels(
|
||||||
|
font, line_spacing
|
||||||
|
) + (ascender_max + descender_max)
|
||||||
|
final_y_offset_loose = ascender_max
|
||||||
|
|
||||||
|
# return (final_box_width, final_box_height, left, final_y_offset)
|
||||||
|
|
||||||
|
return (
|
||||||
|
final_box_width,
|
||||||
|
final_box_height_tight,
|
||||||
|
left,
|
||||||
|
final_y_offset_tight,
|
||||||
|
final_box_height_loose,
|
||||||
|
final_y_offset_loose,
|
||||||
|
)
|
||||||
|
|
||||||
|
# pylint: disable = too-many-branches
|
||||||
|
def _place_text(
|
||||||
|
self,
|
||||||
|
bitmap: displayio.Bitmap,
|
||||||
|
text: str,
|
||||||
|
font: FontProtocol,
|
||||||
|
xposition: int,
|
||||||
|
yposition: int,
|
||||||
|
skip_index: int = 0, # set to None to write all pixels, other wise skip this palette index
|
||||||
|
# when copying glyph bitmaps (this is important for slanted text
|
||||||
|
# where rectangular glyph boxes overlap)
|
||||||
|
) -> Tuple[int, int, int, int]:
|
||||||
|
# pylint: disable=too-many-arguments, too-many-locals
|
||||||
|
|
||||||
|
# placeText - Writes text into a bitmap at the specified location.
|
||||||
|
#
|
||||||
|
# Note: scale is pushed up to Group level
|
||||||
|
|
||||||
|
x_start = xposition # starting x position (left margin)
|
||||||
|
y_start = yposition
|
||||||
|
|
||||||
|
left = None
|
||||||
|
right = x_start
|
||||||
|
top = bottom = y_start
|
||||||
|
line_spacing = self._line_spacing
|
||||||
|
|
||||||
|
for char in text:
|
||||||
|
if char == "\n": # newline
|
||||||
|
xposition = x_start # reset to left column
|
||||||
|
yposition = yposition + self._line_spacing_ypixels(
|
||||||
|
font, line_spacing
|
||||||
|
) # Add a newline
|
||||||
|
|
||||||
|
else:
|
||||||
|
my_glyph = font.get_glyph(ord(char))
|
||||||
|
|
||||||
|
if my_glyph is None: # Error checking: no glyph found
|
||||||
|
print("Glyph not found: {}".format(repr(char)))
|
||||||
|
else:
|
||||||
|
if xposition == x_start:
|
||||||
|
if left is None:
|
||||||
|
left = 0
|
||||||
|
else:
|
||||||
|
left = min(left, my_glyph.dx)
|
||||||
|
|
||||||
|
right = max(
|
||||||
|
right,
|
||||||
|
xposition + my_glyph.shift_x,
|
||||||
|
xposition + my_glyph.width + my_glyph.dx,
|
||||||
|
)
|
||||||
|
if yposition == y_start: # first line, find the Ascender height
|
||||||
|
top = min(top, -my_glyph.height - my_glyph.dy)
|
||||||
|
bottom = max(bottom, yposition - my_glyph.dy)
|
||||||
|
|
||||||
|
glyph_offset_x = (
|
||||||
|
my_glyph.tile_index * my_glyph.width
|
||||||
|
) # for type BuiltinFont, this creates the x-offset in the glyph bitmap.
|
||||||
|
# for BDF loaded fonts, this should equal 0
|
||||||
|
|
||||||
|
y_blit_target = yposition - my_glyph.height - my_glyph.dy
|
||||||
|
|
||||||
|
# Clip glyph y-direction if outside the font ascent/descent metrics.
|
||||||
|
# Note: bitmap.blit will automatically clip the bottom of the glyph.
|
||||||
|
y_clip = 0
|
||||||
|
if y_blit_target < 0:
|
||||||
|
y_clip = -y_blit_target # clip this amount from top of bitmap
|
||||||
|
y_blit_target = 0 # draw the clipped bitmap at y=0
|
||||||
|
if self._verbose:
|
||||||
|
print(
|
||||||
|
'Warning: Glyph clipped, exceeds Ascent property: "{}"'.format(
|
||||||
|
char
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (y_blit_target + my_glyph.height) > bitmap.height:
|
||||||
|
if self._verbose:
|
||||||
|
print(
|
||||||
|
'Warning: Glyph clipped, exceeds descent property: "{}"'.format(
|
||||||
|
char
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._blit(
|
||||||
|
bitmap,
|
||||||
|
max(xposition + my_glyph.dx, 0),
|
||||||
|
y_blit_target,
|
||||||
|
my_glyph.bitmap,
|
||||||
|
x_1=glyph_offset_x,
|
||||||
|
y_1=y_clip,
|
||||||
|
x_2=glyph_offset_x + my_glyph.width,
|
||||||
|
y_2=my_glyph.height,
|
||||||
|
skip_index=skip_index, # do not copy over any 0 background pixels
|
||||||
|
)
|
||||||
|
|
||||||
|
xposition = xposition + my_glyph.shift_x
|
||||||
|
|
||||||
|
# bounding_box
|
||||||
|
return left, top, right - left, bottom - top
|
||||||
|
|
||||||
|
def _blit(
|
||||||
|
self,
|
||||||
|
bitmap: displayio.Bitmap, # target bitmap
|
||||||
|
x: int, # target x upper left corner
|
||||||
|
y: int, # target y upper left corner
|
||||||
|
source_bitmap: displayio.Bitmap, # source bitmap
|
||||||
|
x_1: int = 0, # source x start
|
||||||
|
y_1: int = 0, # source y start
|
||||||
|
x_2: int = None, # source x end
|
||||||
|
y_2: int = None, # source y end
|
||||||
|
skip_index: int = None, # palette index that will not be copied
|
||||||
|
# (for example: the background color of a glyph)
|
||||||
|
) -> None:
|
||||||
|
# pylint: disable=no-self-use, too-many-arguments
|
||||||
|
|
||||||
|
if hasattr(bitmap, "blit"): # if bitmap has a built-in blit function, call it
|
||||||
|
# this function should perform its own input checks
|
||||||
|
bitmap.blit(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
source_bitmap,
|
||||||
|
x1=x_1,
|
||||||
|
y1=y_1,
|
||||||
|
x2=x_2,
|
||||||
|
y2=y_2,
|
||||||
|
skip_index=skip_index,
|
||||||
|
)
|
||||||
|
elif hasattr(bitmaptools, "blit"):
|
||||||
|
bitmaptools.blit(
|
||||||
|
bitmap,
|
||||||
|
source_bitmap,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
x1=x_1,
|
||||||
|
y1=y_1,
|
||||||
|
x2=x_2,
|
||||||
|
y2=y_2,
|
||||||
|
skip_source_index=skip_index,
|
||||||
|
)
|
||||||
|
|
||||||
|
else: # perform pixel by pixel copy of the bitmap
|
||||||
|
# Perform input checks
|
||||||
|
|
||||||
|
if x_2 is None:
|
||||||
|
x_2 = source_bitmap.width
|
||||||
|
if y_2 is None:
|
||||||
|
y_2 = source_bitmap.height
|
||||||
|
|
||||||
|
# Rearrange so that x_1 < x_2 and y1 < y2
|
||||||
|
if x_1 > x_2:
|
||||||
|
x_1, x_2 = x_2, x_1
|
||||||
|
if y_1 > y_2:
|
||||||
|
y_1, y_2 = y_2, y_1
|
||||||
|
|
||||||
|
# Ensure that x2 and y2 are within source bitmap size
|
||||||
|
x_2 = min(x_2, source_bitmap.width)
|
||||||
|
y_2 = min(y_2, source_bitmap.height)
|
||||||
|
|
||||||
|
for y_count in range(y_2 - y_1):
|
||||||
|
for x_count in range(x_2 - x_1):
|
||||||
|
x_placement = x + x_count
|
||||||
|
y_placement = y + y_count
|
||||||
|
|
||||||
|
if (bitmap.width > x_placement >= 0) and (
|
||||||
|
bitmap.height > y_placement >= 0
|
||||||
|
): # ensure placement is within target bitmap
|
||||||
|
# get the palette index from the source bitmap
|
||||||
|
this_pixel_color = source_bitmap[
|
||||||
|
y_1
|
||||||
|
+ (
|
||||||
|
y_count * source_bitmap.width
|
||||||
|
) # Direct index into a bitmap array is speedier than [x,y] tuple
|
||||||
|
+ x_1
|
||||||
|
+ x_count
|
||||||
|
]
|
||||||
|
|
||||||
|
if (skip_index is None) or (this_pixel_color != skip_index):
|
||||||
|
bitmap[ # Direct index into a bitmap array is speedier than [x,y] tuple
|
||||||
|
y_placement * bitmap.width + x_placement
|
||||||
|
] = this_pixel_color
|
||||||
|
elif y_placement > bitmap.height:
|
||||||
|
break
|
||||||
|
|
||||||
|
def _set_line_spacing(self, new_line_spacing: float) -> None:
|
||||||
|
if self._save_text:
|
||||||
|
self._reset_text(line_spacing=new_line_spacing, scale=self.scale)
|
||||||
|
else:
|
||||||
|
raise RuntimeError("line_spacing is immutable when save_text is False")
|
||||||
|
|
||||||
|
def _set_font(self, new_font: FontProtocol) -> None:
|
||||||
|
self._font = new_font
|
||||||
|
if self._save_text:
|
||||||
|
self._reset_text(font=new_font, scale=self.scale)
|
||||||
|
else:
|
||||||
|
raise RuntimeError("font is immutable when save_text is False")
|
||||||
|
|
||||||
|
def _set_text(self, new_text: str, scale: int) -> None:
|
||||||
|
self._reset_text(text=self._replace_tabs(new_text), scale=self.scale)
|
||||||
|
|
||||||
|
def _set_background_color(self, new_color: Optional[int]):
|
||||||
|
self._background_color = new_color
|
||||||
|
if new_color is not None:
|
||||||
|
self._palette[0] = new_color
|
||||||
|
self._palette.make_opaque(0)
|
||||||
|
else:
|
||||||
|
self._palette[0] = 0
|
||||||
|
self._palette.make_transparent(0)
|
||||||
|
|
||||||
|
def _set_label_direction(self, new_label_direction: str) -> None:
|
||||||
|
# Only make changes if new direction is different
|
||||||
|
# to prevent errors in the _reset_text() direction checks
|
||||||
|
if self._label_direction != new_label_direction:
|
||||||
|
self._prev_label_direction = self._label_direction
|
||||||
|
self._label_direction = new_label_direction
|
||||||
|
self._reset_text(text=str(self._text)) # Force a recalculation
|
||||||
|
|
||||||
|
def _get_valid_label_directions(self) -> Tuple[str, ...]:
|
||||||
|
return "LTR", "RTL", "UPD", "UPR", "DWR"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bitmap(self) -> displayio.Bitmap:
|
||||||
|
"""
|
||||||
|
The Bitmap object that the text and background are drawn into.
|
||||||
|
|
||||||
|
:rtype: displayio.Bitmap
|
||||||
|
"""
|
||||||
|
return self._bitmap
|
|
@ -0,0 +1,447 @@
|
||||||
|
# SPDX-FileCopyrightText: 2019 Scott Shawcroft for Adafruit Industries
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
`adafruit_display_text.label`
|
||||||
|
====================================================
|
||||||
|
|
||||||
|
Displays text labels using CircuitPython's displayio.
|
||||||
|
|
||||||
|
* Author(s): Scott Shawcroft
|
||||||
|
|
||||||
|
Implementation Notes
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
**Hardware:**
|
||||||
|
|
||||||
|
**Software and Dependencies:**
|
||||||
|
|
||||||
|
* Adafruit CircuitPython firmware for the supported boards:
|
||||||
|
https://circuitpython.org/downloads
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "0.0.0+auto.0"
|
||||||
|
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git"
|
||||||
|
|
||||||
|
|
||||||
|
from displayio import Bitmap, Palette, TileGrid
|
||||||
|
from adafruit_display_text import LabelBase
|
||||||
|
|
||||||
|
try:
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
from fontio import FontProtocol
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Label(LabelBase):
|
||||||
|
# pylint: disable=too-many-instance-attributes
|
||||||
|
|
||||||
|
"""A label displaying a string of text. The origin point set by ``x`` and ``y``
|
||||||
|
properties will be the left edge of the bounding box, and in the center of a M
|
||||||
|
glyph (if its one line), or the (number of lines * linespacing + M)/2. That is,
|
||||||
|
it will try to have it be center-left as close as possible.
|
||||||
|
|
||||||
|
:param font: A font class that has ``get_bounding_box`` and ``get_glyph``.
|
||||||
|
Must include a capital M for measuring character size.
|
||||||
|
:type font: ~fontio.FontProtocol
|
||||||
|
:param str text: Text to display
|
||||||
|
:param int|Tuple(int, int, int) color: Color of all text in HEX or RGB
|
||||||
|
:param int|Tuple(int, int, int)|None background_color: Color of the background, use `None`
|
||||||
|
for transparent
|
||||||
|
:param float line_spacing: Line spacing of text to display
|
||||||
|
:param bool background_tight: Set `True` only if you want background box to tightly
|
||||||
|
surround text. When set to 'True' Padding parameters will be ignored.
|
||||||
|
:param int padding_top: Additional pixels added to background bounding box at top.
|
||||||
|
This parameter could be negative indicating additional pixels subtracted from the
|
||||||
|
background bounding box.
|
||||||
|
:param int padding_bottom: Additional pixels added to background bounding box at bottom.
|
||||||
|
This parameter could be negative indicating additional pixels subtracted from the
|
||||||
|
background bounding box.
|
||||||
|
:param int padding_left: Additional pixels added to background bounding box at left.
|
||||||
|
This parameter could be negative indicating additional pixels subtracted from the
|
||||||
|
background bounding box.
|
||||||
|
:param int padding_right: Additional pixels added to background bounding box at right.
|
||||||
|
This parameter could be negative indicating additional pixels subtracted from the
|
||||||
|
background bounding box.
|
||||||
|
:param Tuple(float, float) anchor_point: Point that anchored_position moves relative to.
|
||||||
|
Tuple with decimal percentage of width and height.
|
||||||
|
(E.g. (0,0) is top left, (1.0, 0.5): is middle right.)
|
||||||
|
:param Tuple(int, int) anchored_position: Position relative to the anchor_point. Tuple
|
||||||
|
containing x,y pixel coordinates.
|
||||||
|
:param int scale: Integer value of the pixel scaling
|
||||||
|
:param bool base_alignment: when True allows to align text label to the baseline.
|
||||||
|
This is helpful when two or more labels need to be aligned to the same baseline
|
||||||
|
:param Tuple(int, str) tab_replacement: tuple with tab character replace information. When
|
||||||
|
(4, " ") will indicate a tab replacement of 4 spaces, defaults to 4 spaces by
|
||||||
|
tab character
|
||||||
|
:param str label_direction: string defining the label text orientation. There are 5
|
||||||
|
configurations possibles ``LTR``-Left-To-Right ``RTL``-Right-To-Left
|
||||||
|
``TTB``-Top-To-Bottom ``UPR``-Upwards ``DWR``-Downwards. It defaults to ``LTR``"""
|
||||||
|
|
||||||
|
def __init__(self, font: FontProtocol, **kwargs) -> None:
|
||||||
|
self._background_palette = Palette(1)
|
||||||
|
self._added_background_tilegrid = False
|
||||||
|
|
||||||
|
super().__init__(font, **kwargs)
|
||||||
|
|
||||||
|
text = self._replace_tabs(self._text)
|
||||||
|
|
||||||
|
self._width = len(text)
|
||||||
|
self._height = self._font.get_bounding_box()[1]
|
||||||
|
|
||||||
|
# Create the two-color text palette
|
||||||
|
self._palette[0] = 0
|
||||||
|
self._palette.make_transparent(0)
|
||||||
|
|
||||||
|
if text is not None:
|
||||||
|
self._reset_text(str(text))
|
||||||
|
|
||||||
|
# pylint: disable=too-many-branches
|
||||||
|
def _create_background_box(self, lines: int, y_offset: int) -> TileGrid:
|
||||||
|
"""Private Class function to create a background_box
|
||||||
|
:param lines: int number of lines
|
||||||
|
:param y_offset: int y pixel bottom coordinate for the background_box"""
|
||||||
|
|
||||||
|
left = self._bounding_box[0]
|
||||||
|
if self._background_tight: # draw a tight bounding box
|
||||||
|
box_width = self._bounding_box[2]
|
||||||
|
box_height = self._bounding_box[3]
|
||||||
|
x_box_offset = 0
|
||||||
|
y_box_offset = self._bounding_box[1]
|
||||||
|
|
||||||
|
else: # draw a "loose" bounding box to include any ascenders/descenders.
|
||||||
|
ascent, descent = self._ascent, self._descent
|
||||||
|
|
||||||
|
if self._label_direction in ("DWR", "UPR"):
|
||||||
|
box_height = (
|
||||||
|
self._bounding_box[3] + self._padding_right + self._padding_left
|
||||||
|
)
|
||||||
|
x_box_offset = -self._padding_left
|
||||||
|
box_width = (
|
||||||
|
(ascent + descent)
|
||||||
|
+ int((lines - 1) * self._width * self._line_spacing)
|
||||||
|
+ self._padding_top
|
||||||
|
+ self._padding_bottom
|
||||||
|
)
|
||||||
|
elif self._label_direction == "TTB":
|
||||||
|
box_height = (
|
||||||
|
self._bounding_box[3] + self._padding_top + self._padding_bottom
|
||||||
|
)
|
||||||
|
x_box_offset = -self._padding_left
|
||||||
|
box_width = (
|
||||||
|
(ascent + descent)
|
||||||
|
+ int((lines - 1) * self._height * self._line_spacing)
|
||||||
|
+ self._padding_right
|
||||||
|
+ self._padding_left
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
box_width = (
|
||||||
|
self._bounding_box[2] + self._padding_left + self._padding_right
|
||||||
|
)
|
||||||
|
x_box_offset = -self._padding_left
|
||||||
|
box_height = (
|
||||||
|
(ascent + descent)
|
||||||
|
+ int((lines - 1) * self._height * self._line_spacing)
|
||||||
|
+ self._padding_top
|
||||||
|
+ self._padding_bottom
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._label_direction == "DWR":
|
||||||
|
padding_to_use = self._padding_bottom
|
||||||
|
elif self._label_direction == "TTB":
|
||||||
|
padding_to_use = self._padding_top
|
||||||
|
y_offset = 0
|
||||||
|
ascent = 0
|
||||||
|
else:
|
||||||
|
padding_to_use = self._padding_top
|
||||||
|
|
||||||
|
if self._base_alignment:
|
||||||
|
y_box_offset = -ascent - padding_to_use
|
||||||
|
else:
|
||||||
|
y_box_offset = -ascent + y_offset - padding_to_use
|
||||||
|
|
||||||
|
box_width = max(0, box_width) # remove any negative values
|
||||||
|
box_height = max(0, box_height) # remove any negative values
|
||||||
|
|
||||||
|
if self._label_direction == "UPR":
|
||||||
|
movx = y_box_offset
|
||||||
|
movy = -box_height - x_box_offset
|
||||||
|
elif self._label_direction == "DWR":
|
||||||
|
movx = y_box_offset
|
||||||
|
movy = x_box_offset
|
||||||
|
elif self._label_direction == "TTB":
|
||||||
|
movx = x_box_offset
|
||||||
|
movy = y_box_offset
|
||||||
|
else:
|
||||||
|
movx = left + x_box_offset
|
||||||
|
movy = y_box_offset
|
||||||
|
|
||||||
|
background_bitmap = Bitmap(box_width, box_height, 1)
|
||||||
|
tile_grid = TileGrid(
|
||||||
|
background_bitmap,
|
||||||
|
pixel_shader=self._background_palette,
|
||||||
|
x=movx,
|
||||||
|
y=movy,
|
||||||
|
)
|
||||||
|
|
||||||
|
return tile_grid
|
||||||
|
|
||||||
|
# pylint: enable=too-many-branches
|
||||||
|
def _set_background_color(self, new_color: Optional[int]) -> None:
|
||||||
|
"""Private class function that allows updating the font box background color
|
||||||
|
|
||||||
|
:param int new_color: Color as an RGB hex number, setting to None makes it transparent
|
||||||
|
"""
|
||||||
|
|
||||||
|
if new_color is None:
|
||||||
|
self._background_palette.make_transparent(0)
|
||||||
|
if self._added_background_tilegrid:
|
||||||
|
self._local_group.pop(0)
|
||||||
|
self._added_background_tilegrid = False
|
||||||
|
else:
|
||||||
|
self._background_palette.make_opaque(0)
|
||||||
|
self._background_palette[0] = new_color
|
||||||
|
self._background_color = new_color
|
||||||
|
|
||||||
|
lines = self._text.rstrip("\n").count("\n") + 1
|
||||||
|
y_offset = self._ascent // 2
|
||||||
|
|
||||||
|
if self._bounding_box is None:
|
||||||
|
# Still in initialization
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self._added_background_tilegrid: # no bitmap is in the self Group
|
||||||
|
# add bitmap if text is present and bitmap sizes > 0 pixels
|
||||||
|
if (
|
||||||
|
(len(self._text) > 0)
|
||||||
|
and (
|
||||||
|
self._bounding_box[2] + self._padding_left + self._padding_right > 0
|
||||||
|
)
|
||||||
|
and (
|
||||||
|
self._bounding_box[3] + self._padding_top + self._padding_bottom > 0
|
||||||
|
)
|
||||||
|
):
|
||||||
|
self._local_group.insert(
|
||||||
|
0, self._create_background_box(lines, y_offset)
|
||||||
|
)
|
||||||
|
self._added_background_tilegrid = True
|
||||||
|
|
||||||
|
else: # a bitmap is present in the self Group
|
||||||
|
# update bitmap if text is present and bitmap sizes > 0 pixels
|
||||||
|
if (
|
||||||
|
(len(self._text) > 0)
|
||||||
|
and (
|
||||||
|
self._bounding_box[2] + self._padding_left + self._padding_right > 0
|
||||||
|
)
|
||||||
|
and (
|
||||||
|
self._bounding_box[3] + self._padding_top + self._padding_bottom > 0
|
||||||
|
)
|
||||||
|
):
|
||||||
|
self._local_group[0] = self._create_background_box(
|
||||||
|
lines, self._y_offset
|
||||||
|
)
|
||||||
|
else: # delete the existing bitmap
|
||||||
|
self._local_group.pop(0)
|
||||||
|
self._added_background_tilegrid = False
|
||||||
|
|
||||||
|
def _update_text(self, new_text: str) -> None:
|
||||||
|
# pylint: disable=too-many-branches,too-many-statements
|
||||||
|
|
||||||
|
x = 0
|
||||||
|
y = 0
|
||||||
|
if self._added_background_tilegrid:
|
||||||
|
i = 1
|
||||||
|
else:
|
||||||
|
i = 0
|
||||||
|
tilegrid_count = i
|
||||||
|
if self._base_alignment:
|
||||||
|
self._y_offset = 0
|
||||||
|
else:
|
||||||
|
self._y_offset = self._ascent // 2
|
||||||
|
|
||||||
|
if self._label_direction == "RTL":
|
||||||
|
left = top = bottom = 0
|
||||||
|
right = None
|
||||||
|
elif self._label_direction == "LTR":
|
||||||
|
right = top = bottom = 0
|
||||||
|
left = None
|
||||||
|
else:
|
||||||
|
top = right = left = 0
|
||||||
|
bottom = 0
|
||||||
|
|
||||||
|
for character in new_text:
|
||||||
|
if character == "\n":
|
||||||
|
y += int(self._height * self._line_spacing)
|
||||||
|
x = 0
|
||||||
|
continue
|
||||||
|
glyph = self._font.get_glyph(ord(character))
|
||||||
|
if not glyph:
|
||||||
|
continue
|
||||||
|
|
||||||
|
position_x, position_y = 0, 0
|
||||||
|
|
||||||
|
if self._label_direction in ("LTR", "RTL"):
|
||||||
|
bottom = max(bottom, y - glyph.dy + self._y_offset)
|
||||||
|
if y == 0: # first line, find the Ascender height
|
||||||
|
top = min(top, -glyph.height - glyph.dy + self._y_offset)
|
||||||
|
position_y = y - glyph.height - glyph.dy + self._y_offset
|
||||||
|
|
||||||
|
if self._label_direction == "LTR":
|
||||||
|
right = max(right, x + glyph.shift_x, x + glyph.width + glyph.dx)
|
||||||
|
if x == 0:
|
||||||
|
if left is None:
|
||||||
|
left = 0
|
||||||
|
else:
|
||||||
|
left = min(left, glyph.dx)
|
||||||
|
position_x = x + glyph.dx
|
||||||
|
else:
|
||||||
|
left = max(
|
||||||
|
left, abs(x) + glyph.shift_x, abs(x) + glyph.width + glyph.dx
|
||||||
|
)
|
||||||
|
if x == 0:
|
||||||
|
if right is None:
|
||||||
|
right = 0
|
||||||
|
else:
|
||||||
|
right = max(right, glyph.dx)
|
||||||
|
position_x = x - glyph.width
|
||||||
|
|
||||||
|
elif self._label_direction == "TTB":
|
||||||
|
if x == 0:
|
||||||
|
if left is None:
|
||||||
|
left = 0
|
||||||
|
else:
|
||||||
|
left = min(left, glyph.dx)
|
||||||
|
if y == 0:
|
||||||
|
top = min(top, -glyph.dy)
|
||||||
|
|
||||||
|
bottom = max(bottom, y + glyph.height, y + glyph.height + glyph.dy)
|
||||||
|
right = max(
|
||||||
|
right, x + glyph.width + glyph.dx, x + glyph.shift_x + glyph.dx
|
||||||
|
)
|
||||||
|
position_y = y + glyph.dy
|
||||||
|
position_x = x - glyph.width // 2 + self._y_offset
|
||||||
|
|
||||||
|
elif self._label_direction == "UPR":
|
||||||
|
if x == 0:
|
||||||
|
if bottom is None:
|
||||||
|
bottom = -glyph.dx
|
||||||
|
|
||||||
|
if y == 0: # first line, find the Ascender height
|
||||||
|
bottom = min(bottom, -glyph.dy)
|
||||||
|
left = min(left, x - glyph.height + self._y_offset)
|
||||||
|
top = min(top, y - glyph.width - glyph.dx, y - glyph.shift_x)
|
||||||
|
right = max(right, x + glyph.height, x + glyph.height - glyph.dy)
|
||||||
|
position_y = y - glyph.width - glyph.dx
|
||||||
|
position_x = x - glyph.height - glyph.dy + self._y_offset
|
||||||
|
|
||||||
|
elif self._label_direction == "DWR":
|
||||||
|
if y == 0:
|
||||||
|
if top is None:
|
||||||
|
top = -glyph.dx
|
||||||
|
top = min(top, -glyph.dx)
|
||||||
|
if x == 0:
|
||||||
|
left = min(left, -glyph.dy)
|
||||||
|
left = min(left, x, x - glyph.dy - self._y_offset)
|
||||||
|
bottom = max(bottom, y + glyph.width + glyph.dx, y + glyph.shift_x)
|
||||||
|
right = max(right, x + glyph.height)
|
||||||
|
position_y = y + glyph.dx
|
||||||
|
position_x = x + glyph.dy - self._y_offset
|
||||||
|
|
||||||
|
if glyph.width > 0 and glyph.height > 0:
|
||||||
|
face = TileGrid(
|
||||||
|
glyph.bitmap,
|
||||||
|
pixel_shader=self._palette,
|
||||||
|
default_tile=glyph.tile_index,
|
||||||
|
tile_width=glyph.width,
|
||||||
|
tile_height=glyph.height,
|
||||||
|
x=position_x,
|
||||||
|
y=position_y,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._label_direction == "UPR":
|
||||||
|
face.transpose_xy = True
|
||||||
|
face.flip_x = True
|
||||||
|
if self._label_direction == "DWR":
|
||||||
|
face.transpose_xy = True
|
||||||
|
face.flip_y = True
|
||||||
|
|
||||||
|
if tilegrid_count < len(self._local_group):
|
||||||
|
self._local_group[tilegrid_count] = face
|
||||||
|
else:
|
||||||
|
self._local_group.append(face)
|
||||||
|
tilegrid_count += 1
|
||||||
|
|
||||||
|
if self._label_direction == "RTL":
|
||||||
|
x = x - glyph.shift_x
|
||||||
|
if self._label_direction == "TTB":
|
||||||
|
if glyph.height < 2:
|
||||||
|
y = y + glyph.shift_x
|
||||||
|
else:
|
||||||
|
y = y + glyph.height + 1
|
||||||
|
if self._label_direction == "UPR":
|
||||||
|
y = y - glyph.shift_x
|
||||||
|
if self._label_direction == "DWR":
|
||||||
|
y = y + glyph.shift_x
|
||||||
|
if self._label_direction == "LTR":
|
||||||
|
x = x + glyph.shift_x
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if self._label_direction == "LTR" and left is None:
|
||||||
|
left = 0
|
||||||
|
if self._label_direction == "RTL" and right is None:
|
||||||
|
right = 0
|
||||||
|
if self._label_direction == "TTB" and top is None:
|
||||||
|
top = 0
|
||||||
|
|
||||||
|
while len(self._local_group) > tilegrid_count: # i:
|
||||||
|
self._local_group.pop()
|
||||||
|
|
||||||
|
if self._label_direction == "RTL":
|
||||||
|
# pylint: disable=invalid-unary-operand-type
|
||||||
|
# type-checkers think left can be None
|
||||||
|
self._bounding_box = (-left, top, left - right, bottom - top)
|
||||||
|
if self._label_direction == "TTB":
|
||||||
|
self._bounding_box = (left, top, right - left, bottom - top)
|
||||||
|
if self._label_direction == "UPR":
|
||||||
|
self._bounding_box = (left, top, right, bottom - top)
|
||||||
|
if self._label_direction == "DWR":
|
||||||
|
self._bounding_box = (left, top, right, bottom - top)
|
||||||
|
if self._label_direction == "LTR":
|
||||||
|
self._bounding_box = (left, top, right - left, bottom - top)
|
||||||
|
|
||||||
|
self._text = new_text
|
||||||
|
|
||||||
|
if self._background_color is not None:
|
||||||
|
self._set_background_color(self._background_color)
|
||||||
|
|
||||||
|
def _reset_text(self, new_text: str) -> None:
|
||||||
|
current_anchored_position = self.anchored_position
|
||||||
|
self._update_text(str(self._replace_tabs(new_text)))
|
||||||
|
self.anchored_position = current_anchored_position
|
||||||
|
|
||||||
|
def _set_font(self, new_font: FontProtocol) -> None:
|
||||||
|
old_text = self._text
|
||||||
|
current_anchored_position = self.anchored_position
|
||||||
|
self._text = ""
|
||||||
|
self._font = new_font
|
||||||
|
self._height = self._font.get_bounding_box()[1]
|
||||||
|
self._update_text(str(old_text))
|
||||||
|
self.anchored_position = current_anchored_position
|
||||||
|
|
||||||
|
def _set_line_spacing(self, new_line_spacing: float) -> None:
|
||||||
|
self._line_spacing = new_line_spacing
|
||||||
|
self.text = self._text # redraw the box
|
||||||
|
|
||||||
|
def _set_text(self, new_text: str, scale: int) -> None:
|
||||||
|
self._reset_text(new_text)
|
||||||
|
|
||||||
|
def _set_label_direction(self, new_label_direction: str) -> None:
|
||||||
|
self._label_direction = new_label_direction
|
||||||
|
self._update_text(str(self._text))
|
||||||
|
|
||||||
|
def _get_valid_label_directions(self) -> Tuple[str, ...]:
|
||||||
|
return "LTR", "RTL", "UPR", "DWR", "TTB"
|
|
@ -0,0 +1,188 @@
|
||||||
|
# SPDX-FileCopyrightText: 2023 Tim C
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
`adafruit_display_text.outlined_label`
|
||||||
|
====================================================
|
||||||
|
|
||||||
|
Subclass of BitmapLabel that adds outline color and stroke size
|
||||||
|
functionalities.
|
||||||
|
|
||||||
|
* Author(s): Tim Cocks
|
||||||
|
|
||||||
|
Implementation Notes
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
**Hardware:**
|
||||||
|
|
||||||
|
**Software and Dependencies:**
|
||||||
|
|
||||||
|
* Adafruit CircuitPython firmware for the supported boards:
|
||||||
|
https://circuitpython.org/downloads
|
||||||
|
|
||||||
|
"""
|
||||||
|
__version__ = "0.0.0+auto.0"
|
||||||
|
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git"
|
||||||
|
|
||||||
|
import bitmaptools
|
||||||
|
from displayio import Palette, Bitmap
|
||||||
|
from adafruit_display_text import bitmap_label
|
||||||
|
|
||||||
|
try:
|
||||||
|
from typing import Optional, Tuple, Union
|
||||||
|
from fontio import FontProtocol
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class OutlinedLabel(bitmap_label.Label):
|
||||||
|
"""
|
||||||
|
OutlinedLabel - A BitmapLabel subclass that includes arguments and properties for specifying
|
||||||
|
outline_size and outline_color to get drawn as a stroke around the text.
|
||||||
|
|
||||||
|
:param Union[Tuple, int] outline_color: The color of the outline stroke as RGB tuple, or hex.
|
||||||
|
:param int outline_size: The size in pixels of the outline stroke.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
font,
|
||||||
|
outline_color: Union[int, Tuple] = 0x999999,
|
||||||
|
outline_size: int = 1,
|
||||||
|
padding_top: Optional[int] = None,
|
||||||
|
padding_bottom: Optional[int] = None,
|
||||||
|
padding_left: Optional[int] = None,
|
||||||
|
padding_right: Optional[int] = None,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
if padding_top is None:
|
||||||
|
padding_top = outline_size + 0
|
||||||
|
if padding_bottom is None:
|
||||||
|
padding_bottom = outline_size + 2
|
||||||
|
if padding_left is None:
|
||||||
|
padding_left = outline_size + 0
|
||||||
|
if padding_right is None:
|
||||||
|
padding_right = outline_size + 0
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
font,
|
||||||
|
padding_top=padding_top,
|
||||||
|
padding_bottom=padding_bottom,
|
||||||
|
padding_left=padding_left,
|
||||||
|
padding_right=padding_right,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
_background_color = self._palette[0]
|
||||||
|
_foreground_color = self._palette[1]
|
||||||
|
_background_is_transparent = self._palette.is_transparent(0)
|
||||||
|
self._palette = Palette(3)
|
||||||
|
self._palette[0] = _background_color
|
||||||
|
self._palette[1] = _foreground_color
|
||||||
|
self._palette[2] = outline_color
|
||||||
|
if _background_is_transparent:
|
||||||
|
self._palette.make_transparent(0)
|
||||||
|
|
||||||
|
self._outline_size = outline_size
|
||||||
|
self._stamp_source = Bitmap((outline_size * 2) + 1, (outline_size * 2) + 1, 3)
|
||||||
|
self._stamp_source.fill(2)
|
||||||
|
|
||||||
|
self._bitmap = None
|
||||||
|
|
||||||
|
self._reset_text(
|
||||||
|
font=font,
|
||||||
|
text=self._text,
|
||||||
|
line_spacing=self._line_spacing,
|
||||||
|
scale=self.scale,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _add_outline(self):
|
||||||
|
"""
|
||||||
|
Blit the outline into the labels Bitmap. We will stamp self._stamp_source for each
|
||||||
|
pixel of the foreground color but skip the foreground color when we blit.
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
if hasattr(self, "_stamp_source"):
|
||||||
|
for y in range(self.bitmap.height):
|
||||||
|
for x in range(self.bitmap.width):
|
||||||
|
if self.bitmap[x, y] == 1:
|
||||||
|
try:
|
||||||
|
bitmaptools.blit(
|
||||||
|
self.bitmap,
|
||||||
|
self._stamp_source,
|
||||||
|
x - self._outline_size,
|
||||||
|
y - self._outline_size,
|
||||||
|
skip_dest_index=1,
|
||||||
|
)
|
||||||
|
except ValueError as value_error:
|
||||||
|
raise ValueError(
|
||||||
|
"Padding must be big enough to fit outline_size "
|
||||||
|
"all the way around the text. "
|
||||||
|
"Try using either larger padding sizes, or smaller outline_size."
|
||||||
|
) from value_error
|
||||||
|
|
||||||
|
def _place_text(
|
||||||
|
self,
|
||||||
|
bitmap: Bitmap,
|
||||||
|
text: str,
|
||||||
|
font: FontProtocol,
|
||||||
|
xposition: int,
|
||||||
|
yposition: int,
|
||||||
|
skip_index: int = 0, # set to None to write all pixels, other wise skip this palette index
|
||||||
|
# when copying glyph bitmaps (this is important for slanted text
|
||||||
|
# where rectangular glyph boxes overlap)
|
||||||
|
) -> Tuple[int, int, int, int]:
|
||||||
|
"""
|
||||||
|
Copy the glpyphs that represent the value of the string into the labels Bitmap.
|
||||||
|
:param bitmap: The bitmap to place text into
|
||||||
|
:param text: The text to render
|
||||||
|
:param font: The font to render the text in
|
||||||
|
:param xposition: x location of the starting point within the bitmap
|
||||||
|
:param yposition: y location of the starting point within the bitmap
|
||||||
|
:param skip_index: Color index to skip during rendering instead of covering up
|
||||||
|
:return Tuple bounding_box: tuple with x, y, width, height values of the bitmap
|
||||||
|
"""
|
||||||
|
parent_result = super()._place_text(
|
||||||
|
bitmap, text, font, xposition, yposition, skip_index=skip_index
|
||||||
|
)
|
||||||
|
|
||||||
|
self._add_outline()
|
||||||
|
|
||||||
|
return parent_result
|
||||||
|
|
||||||
|
@property
|
||||||
|
def outline_color(self):
|
||||||
|
"""Color of the outline to draw around the text."""
|
||||||
|
return self._palette[2]
|
||||||
|
|
||||||
|
@outline_color.setter
|
||||||
|
def outline_color(self, new_outline_color):
|
||||||
|
self._palette[2] = new_outline_color
|
||||||
|
|
||||||
|
@property
|
||||||
|
def outline_size(self):
|
||||||
|
"""Stroke size of the outline to draw around the text."""
|
||||||
|
return self._outline_size
|
||||||
|
|
||||||
|
@outline_size.setter
|
||||||
|
def outline_size(self, new_outline_size):
|
||||||
|
self._outline_size = new_outline_size
|
||||||
|
|
||||||
|
self._padding_top = new_outline_size + 0
|
||||||
|
self._padding_bottom = new_outline_size + 2
|
||||||
|
self._padding_left = new_outline_size + 0
|
||||||
|
self._padding_right = new_outline_size + 0
|
||||||
|
|
||||||
|
self._stamp_source = Bitmap(
|
||||||
|
(new_outline_size * 2) + 1, (new_outline_size * 2) + 1, 3
|
||||||
|
)
|
||||||
|
self._stamp_source.fill(2)
|
||||||
|
self._reset_text(
|
||||||
|
font=self._font,
|
||||||
|
text=self._text,
|
||||||
|
line_spacing=self._line_spacing,
|
||||||
|
scale=self.scale,
|
||||||
|
)
|
|
@ -0,0 +1,158 @@
|
||||||
|
# SPDX-FileCopyrightText: 2019 Scott Shawcroft for Adafruit Industries
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
`adafruit_display_text.scrolling_label`
|
||||||
|
====================================================
|
||||||
|
|
||||||
|
Displays text into a fixed-width label that scrolls leftward
|
||||||
|
if the full_text is large enough to need it.
|
||||||
|
|
||||||
|
* Author(s): Tim Cocks
|
||||||
|
|
||||||
|
Implementation Notes
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
**Hardware:**
|
||||||
|
|
||||||
|
**Software and Dependencies:**
|
||||||
|
|
||||||
|
* Adafruit CircuitPython firmware for the supported boards:
|
||||||
|
https://circuitpython.org/downloads
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "0.0.0+auto.0"
|
||||||
|
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git"
|
||||||
|
|
||||||
|
import adafruit_ticks
|
||||||
|
from adafruit_display_text import bitmap_label
|
||||||
|
|
||||||
|
try:
|
||||||
|
from typing import Optional
|
||||||
|
from fontio import FontProtocol
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ScrollingLabel(bitmap_label.Label):
|
||||||
|
"""ScrollingLabel - A fixed-width label that will scroll to the left
|
||||||
|
in order to show the full text if it's larger than the fixed-width.
|
||||||
|
|
||||||
|
:param font: The font to use for the label.
|
||||||
|
:type: ~fontio.FontProtocol
|
||||||
|
:param int max_characters: The number of characters that sets the fixed-width. Default is 10.
|
||||||
|
:param str text: The full text to show in the label. If this is longer than
|
||||||
|
``max_characters`` then the label will scroll to show everything.
|
||||||
|
:param float animate_time: The number of seconds in between scrolling animation
|
||||||
|
frames. Default is 0.3 seconds.
|
||||||
|
:param int current_index: The index of the first visible character in the label.
|
||||||
|
Default is 0, the first character. Will increase while scrolling."""
|
||||||
|
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
font: FontProtocol,
|
||||||
|
max_characters: int = 10,
|
||||||
|
text: Optional[str] = "",
|
||||||
|
animate_time: Optional[float] = 0.3,
|
||||||
|
current_index: Optional[int] = 0,
|
||||||
|
**kwargs
|
||||||
|
) -> None:
|
||||||
|
super().__init__(font, **kwargs)
|
||||||
|
self.animate_time = animate_time
|
||||||
|
self._current_index = current_index
|
||||||
|
self._last_animate_time = -1
|
||||||
|
self.max_characters = max_characters
|
||||||
|
|
||||||
|
if text[-1] != " ":
|
||||||
|
text = "{} ".format(text)
|
||||||
|
self._full_text = text
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def update(self, force: bool = False) -> None:
|
||||||
|
"""Attempt to update the display. If ``animate_time`` has elapsed since
|
||||||
|
previews animation frame then move the characters over by 1 index.
|
||||||
|
Must be called in the main loop of user code.
|
||||||
|
|
||||||
|
:param bool force: whether to ignore ``animation_time`` and force the update.
|
||||||
|
Default is False.
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
_now = adafruit_ticks.ticks_ms()
|
||||||
|
if force or adafruit_ticks.ticks_less(
|
||||||
|
self._last_animate_time + int(self.animate_time * 1000), _now
|
||||||
|
):
|
||||||
|
if len(self.full_text) <= self.max_characters:
|
||||||
|
super()._set_text(self.full_text, self.scale)
|
||||||
|
self._last_animate_time = _now
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.current_index + self.max_characters <= len(self.full_text):
|
||||||
|
_showing_string = self.full_text[
|
||||||
|
self.current_index : self.current_index + self.max_characters
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
_showing_string_start = self.full_text[self.current_index :]
|
||||||
|
_showing_string_end = "{}".format(
|
||||||
|
self.full_text[
|
||||||
|
: (self.current_index + self.max_characters)
|
||||||
|
% len(self.full_text)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
_showing_string = "{}{}".format(
|
||||||
|
_showing_string_start, _showing_string_end
|
||||||
|
)
|
||||||
|
super()._set_text(_showing_string, self.scale)
|
||||||
|
self.current_index += 1
|
||||||
|
self._last_animate_time = _now
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_index(self) -> int:
|
||||||
|
"""Index of the first visible character.
|
||||||
|
|
||||||
|
:return int: The current index
|
||||||
|
"""
|
||||||
|
return self._current_index
|
||||||
|
|
||||||
|
@current_index.setter
|
||||||
|
def current_index(self, new_index: int) -> None:
|
||||||
|
if new_index < len(self.full_text):
|
||||||
|
self._current_index = new_index
|
||||||
|
else:
|
||||||
|
self._current_index = new_index % len(self.full_text)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_text(self) -> str:
|
||||||
|
"""The full text to be shown. If it's longer than ``max_characters`` then
|
||||||
|
scrolling will occur as needed.
|
||||||
|
|
||||||
|
:return str: The full text of this label.
|
||||||
|
"""
|
||||||
|
return self._full_text
|
||||||
|
|
||||||
|
@full_text.setter
|
||||||
|
def full_text(self, new_text: str) -> None:
|
||||||
|
if new_text[-1] != " ":
|
||||||
|
new_text = "{} ".format(new_text)
|
||||||
|
self._full_text = new_text
|
||||||
|
self.current_index = 0
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def text(self):
|
||||||
|
"""The full text to be shown. If it's longer than ``max_characters`` then
|
||||||
|
scrolling will occur as needed.
|
||||||
|
|
||||||
|
:return str: The full text of this label.
|
||||||
|
"""
|
||||||
|
return self.full_text
|
||||||
|
|
||||||
|
@text.setter
|
||||||
|
def text(self, new_text):
|
||||||
|
self.full_text = new_text
|
|
@ -0,0 +1,639 @@
|
||||||
|
# SPDX-FileCopyrightText: <text> 2018 Kattni Rembor, Melissa LeBlanc-Williams
|
||||||
|
# and Tony DiCola, for Adafruit Industries.
|
||||||
|
# Original file created by Damien P. George </text>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
`adafruit_framebuf`
|
||||||
|
====================================================
|
||||||
|
|
||||||
|
CircuitPython pure-python framebuf module, based on the micropython framebuf module.
|
||||||
|
|
||||||
|
Implementation Notes
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
**Hardware:**
|
||||||
|
|
||||||
|
* `Adafruit SSD1306 OLED displays <https://www.adafruit.com/?q=ssd1306>`_
|
||||||
|
* `Adafruit HT16K33 Matrix displays <https://www.adafruit.com/?q=ht16k33>`_
|
||||||
|
|
||||||
|
**Software and Dependencies:**
|
||||||
|
|
||||||
|
* Adafruit CircuitPython firmware for the supported boards:
|
||||||
|
https://github.com/adafruit/circuitpython/releases
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "0.0.0+auto.0"
|
||||||
|
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_framebuf.git"
|
||||||
|
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
|
||||||
|
# Framebuf format constants:
|
||||||
|
MVLSB = 0 # Single bit displays (like SSD1306 OLED)
|
||||||
|
RGB565 = 1 # 16-bit color displays
|
||||||
|
GS4_HMSB = 2 # Unimplemented!
|
||||||
|
MHMSB = 3 # Single bit displays like the Sharp Memory
|
||||||
|
RGB888 = 4 # Neopixels and Dotstars
|
||||||
|
GS2_HMSB = 5 # 2-bit color displays like the HT16K33 8x8 Matrix
|
||||||
|
|
||||||
|
|
||||||
|
class GS2HMSBFormat:
|
||||||
|
"""GS2HMSBFormat"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def set_pixel(framebuf, x, y, color):
|
||||||
|
"""Set a given pixel to a color."""
|
||||||
|
index = (y * framebuf.stride + x) >> 2
|
||||||
|
pixel = framebuf.buf[index]
|
||||||
|
|
||||||
|
shift = (x & 0b11) << 1
|
||||||
|
mask = 0b11 << shift
|
||||||
|
color = (color & 0b11) << shift
|
||||||
|
|
||||||
|
framebuf.buf[index] = color | (pixel & (~mask))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_pixel(framebuf, x, y):
|
||||||
|
"""Get the color of a given pixel"""
|
||||||
|
index = (y * framebuf.stride + x) >> 2
|
||||||
|
pixel = framebuf.buf[index]
|
||||||
|
|
||||||
|
shift = (x & 0b11) << 1
|
||||||
|
return (pixel >> shift) & 0b11
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fill(framebuf, color):
|
||||||
|
"""completely fill/clear the buffer with a color"""
|
||||||
|
if color:
|
||||||
|
bits = color & 0b11
|
||||||
|
fill = (bits << 6) | (bits << 4) | (bits << 2) | (bits << 0)
|
||||||
|
else:
|
||||||
|
fill = 0x00
|
||||||
|
|
||||||
|
framebuf.buf = [fill for i in range(len(framebuf.buf))]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def rect(framebuf, x, y, width, height, color):
|
||||||
|
"""Draw the outline of a rectangle at the given location, size and color."""
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
|
for _x in range(x, x + width):
|
||||||
|
for _y in range(y, y + height):
|
||||||
|
if _x in [x, x + width] or _y in [y, y + height]:
|
||||||
|
GS2HMSBFormat.set_pixel(framebuf, _x, _y, color)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fill_rect(framebuf, x, y, width, height, color):
|
||||||
|
"""Draw the outline and interior of a rectangle at the given location, size and color."""
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
|
for _x in range(x, x + width):
|
||||||
|
for _y in range(y, y + height):
|
||||||
|
GS2HMSBFormat.set_pixel(framebuf, _x, _y, color)
|
||||||
|
|
||||||
|
|
||||||
|
class MHMSBFormat:
|
||||||
|
"""MHMSBFormat"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def set_pixel(framebuf, x, y, color):
|
||||||
|
"""Set a given pixel to a color."""
|
||||||
|
index = (y * framebuf.stride + x) // 8
|
||||||
|
offset = 7 - x & 0x07
|
||||||
|
framebuf.buf[index] = (framebuf.buf[index] & ~(0x01 << offset)) | (
|
||||||
|
(color != 0) << offset
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_pixel(framebuf, x, y):
|
||||||
|
"""Get the color of a given pixel"""
|
||||||
|
index = (y * framebuf.stride + x) // 8
|
||||||
|
offset = 7 - x & 0x07
|
||||||
|
return (framebuf.buf[index] >> offset) & 0x01
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fill(framebuf, color):
|
||||||
|
"""completely fill/clear the buffer with a color"""
|
||||||
|
if color:
|
||||||
|
fill = 0xFF
|
||||||
|
else:
|
||||||
|
fill = 0x00
|
||||||
|
for i in range(len(framebuf.buf)): # pylint: disable=consider-using-enumerate
|
||||||
|
framebuf.buf[i] = fill
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fill_rect(framebuf, x, y, width, height, color):
|
||||||
|
"""Draw a rectangle at the given location, size and color. The ``fill_rect`` method draws
|
||||||
|
both the outline and interior."""
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
|
for _x in range(x, x + width):
|
||||||
|
offset = 7 - _x & 0x07
|
||||||
|
for _y in range(y, y + height):
|
||||||
|
index = (_y * framebuf.stride + _x) // 8
|
||||||
|
framebuf.buf[index] = (framebuf.buf[index] & ~(0x01 << offset)) | (
|
||||||
|
(color != 0) << offset
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MVLSBFormat:
|
||||||
|
"""MVLSBFormat"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def set_pixel(framebuf, x, y, color):
|
||||||
|
"""Set a given pixel to a color."""
|
||||||
|
index = (y >> 3) * framebuf.stride + x
|
||||||
|
offset = y & 0x07
|
||||||
|
framebuf.buf[index] = (framebuf.buf[index] & ~(0x01 << offset)) | (
|
||||||
|
(color != 0) << offset
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_pixel(framebuf, x, y):
|
||||||
|
"""Get the color of a given pixel"""
|
||||||
|
index = (y >> 3) * framebuf.stride + x
|
||||||
|
offset = y & 0x07
|
||||||
|
return (framebuf.buf[index] >> offset) & 0x01
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fill(framebuf, color):
|
||||||
|
"""completely fill/clear the buffer with a color"""
|
||||||
|
if color:
|
||||||
|
fill = 0xFF
|
||||||
|
else:
|
||||||
|
fill = 0x00
|
||||||
|
for i in range(len(framebuf.buf)): # pylint: disable=consider-using-enumerate
|
||||||
|
framebuf.buf[i] = fill
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fill_rect(framebuf, x, y, width, height, color):
|
||||||
|
"""Draw a rectangle at the given location, size and color. The ``fill_rect`` method draws
|
||||||
|
both the outline and interior."""
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
|
while height > 0:
|
||||||
|
index = (y >> 3) * framebuf.stride + x
|
||||||
|
offset = y & 0x07
|
||||||
|
for w_w in range(width):
|
||||||
|
framebuf.buf[index + w_w] = (
|
||||||
|
framebuf.buf[index + w_w] & ~(0x01 << offset)
|
||||||
|
) | ((color != 0) << offset)
|
||||||
|
y += 1
|
||||||
|
height -= 1
|
||||||
|
|
||||||
|
|
||||||
|
class RGB565Format:
|
||||||
|
"""
|
||||||
|
This class implements the RGB565 format
|
||||||
|
It assumes a little-endian byte order in the frame buffer
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def color_to_rgb565(color):
|
||||||
|
"""Convert a color in either tuple or 24 bit integer form to RGB565,
|
||||||
|
and return as two bytes"""
|
||||||
|
if isinstance(color, tuple):
|
||||||
|
hibyte = (color[0] & 0xF8) | (color[1] >> 5)
|
||||||
|
lobyte = ((color[1] << 5) & 0xE0) | (color[2] >> 3)
|
||||||
|
else:
|
||||||
|
hibyte = ((color >> 16) & 0xF8) | ((color >> 13) & 0x07)
|
||||||
|
lobyte = ((color >> 5) & 0xE0) | ((color >> 3) & 0x1F)
|
||||||
|
return bytes([lobyte, hibyte])
|
||||||
|
|
||||||
|
def set_pixel(self, framebuf, x, y, color):
|
||||||
|
"""Set a given pixel to a color."""
|
||||||
|
index = (y * framebuf.stride + x) * 2
|
||||||
|
framebuf.buf[index : index + 2] = self.color_to_rgb565(color)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_pixel(framebuf, x, y):
|
||||||
|
"""Get the color of a given pixel"""
|
||||||
|
index = (y * framebuf.stride + x) * 2
|
||||||
|
lobyte, hibyte = framebuf.buf[index : index + 2]
|
||||||
|
r = hibyte & 0xF8
|
||||||
|
g = ((hibyte & 0x07) << 5) | ((lobyte & 0xE0) >> 5)
|
||||||
|
b = (lobyte & 0x1F) << 3
|
||||||
|
return (r << 16) | (g << 8) | b
|
||||||
|
|
||||||
|
def fill(self, framebuf, color):
|
||||||
|
"""completely fill/clear the buffer with a color"""
|
||||||
|
rgb565_color = self.color_to_rgb565(color)
|
||||||
|
for i in range(0, len(framebuf.buf), 2):
|
||||||
|
framebuf.buf[i : i + 2] = rgb565_color
|
||||||
|
|
||||||
|
def fill_rect(self, framebuf, x, y, width, height, color):
|
||||||
|
"""Draw a rectangle at the given location, size and color. The ``fill_rect`` method draws
|
||||||
|
both the outline and interior."""
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
|
rgb565_color = self.color_to_rgb565(color)
|
||||||
|
for _y in range(2 * y, 2 * (y + height), 2):
|
||||||
|
offset2 = _y * framebuf.stride
|
||||||
|
for _x in range(2 * x, 2 * (x + width), 2):
|
||||||
|
index = offset2 + _x
|
||||||
|
framebuf.buf[index : index + 2] = rgb565_color
|
||||||
|
|
||||||
|
|
||||||
|
class RGB888Format:
|
||||||
|
"""RGB888Format"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def set_pixel(framebuf, x, y, color):
|
||||||
|
"""Set a given pixel to a color."""
|
||||||
|
index = (y * framebuf.stride + x) * 3
|
||||||
|
if isinstance(color, tuple):
|
||||||
|
framebuf.buf[index : index + 3] = bytes(color)
|
||||||
|
else:
|
||||||
|
framebuf.buf[index : index + 3] = bytes(
|
||||||
|
((color >> 16) & 255, (color >> 8) & 255, color & 255)
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_pixel(framebuf, x, y):
|
||||||
|
"""Get the color of a given pixel"""
|
||||||
|
index = (y * framebuf.stride + x) * 3
|
||||||
|
return (
|
||||||
|
(framebuf.buf[index] << 16)
|
||||||
|
| (framebuf.buf[index + 1] << 8)
|
||||||
|
| framebuf.buf[index + 2]
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fill(framebuf, color):
|
||||||
|
"""completely fill/clear the buffer with a color"""
|
||||||
|
fill = (color >> 16) & 255, (color >> 8) & 255, color & 255
|
||||||
|
for i in range(0, len(framebuf.buf), 3):
|
||||||
|
framebuf.buf[i : i + 3] = bytes(fill)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fill_rect(framebuf, x, y, width, height, color):
|
||||||
|
"""Draw a rectangle at the given location, size and color. The ``fill_rect`` method draws
|
||||||
|
both the outline and interior."""
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
|
fill = (color >> 16) & 255, (color >> 8) & 255, color & 255
|
||||||
|
for _x in range(x, x + width):
|
||||||
|
for _y in range(y, y + height):
|
||||||
|
index = (_y * framebuf.stride + _x) * 3
|
||||||
|
framebuf.buf[index : index + 3] = bytes(fill)
|
||||||
|
|
||||||
|
|
||||||
|
class FrameBuffer:
|
||||||
|
"""FrameBuffer object.
|
||||||
|
|
||||||
|
:param buf: An object with a buffer protocol which must be large enough to contain every
|
||||||
|
pixel defined by the width, height and format of the FrameBuffer.
|
||||||
|
:param width: The width of the FrameBuffer in pixel
|
||||||
|
:param height: The height of the FrameBuffer in pixel
|
||||||
|
:param buf_format: Specifies the type of pixel used in the FrameBuffer; permissible values
|
||||||
|
are listed under Constants below. These set the number of bits used to
|
||||||
|
encode a color value and the layout of these bits in ``buf``. Where a
|
||||||
|
color value c is passed to a method, c is a small integer with an encoding
|
||||||
|
that is dependent on the format of the FrameBuffer.
|
||||||
|
:param stride: The number of pixels between each horizontal line of pixels in the
|
||||||
|
FrameBuffer. This defaults to ``width`` but may need adjustments when
|
||||||
|
implementing a FrameBuffer within another larger FrameBuffer or screen. The
|
||||||
|
``buf`` size must accommodate an increased step size.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, buf, width, height, buf_format=MVLSB, stride=None):
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
|
self.buf = buf
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.stride = stride
|
||||||
|
self._font = None
|
||||||
|
if self.stride is None:
|
||||||
|
self.stride = width
|
||||||
|
if buf_format == MVLSB:
|
||||||
|
self.format = MVLSBFormat()
|
||||||
|
elif buf_format == MHMSB:
|
||||||
|
self.format = MHMSBFormat()
|
||||||
|
elif buf_format == RGB888:
|
||||||
|
self.format = RGB888Format()
|
||||||
|
elif buf_format == RGB565:
|
||||||
|
self.format = RGB565Format()
|
||||||
|
elif buf_format == GS2_HMSB:
|
||||||
|
self.format = GS2HMSBFormat()
|
||||||
|
else:
|
||||||
|
raise ValueError("invalid format")
|
||||||
|
self._rotation = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rotation(self):
|
||||||
|
"""The rotation setting of the display, can be one of (0, 1, 2, 3)"""
|
||||||
|
return self._rotation
|
||||||
|
|
||||||
|
@rotation.setter
|
||||||
|
def rotation(self, val):
|
||||||
|
if not val in (0, 1, 2, 3):
|
||||||
|
raise RuntimeError("Bad rotation setting")
|
||||||
|
self._rotation = val
|
||||||
|
|
||||||
|
def fill(self, color):
|
||||||
|
"""Fill the entire FrameBuffer with the specified color."""
|
||||||
|
self.format.fill(self, color)
|
||||||
|
|
||||||
|
def fill_rect(self, x, y, width, height, color):
|
||||||
|
"""Draw a rectangle at the given location, size and color. The ``fill_rect`` method draws
|
||||||
|
both the outline and interior."""
|
||||||
|
# pylint: disable=too-many-arguments, too-many-boolean-expressions
|
||||||
|
self.rect(x, y, width, height, color, fill=True)
|
||||||
|
|
||||||
|
def pixel(self, x, y, color=None):
|
||||||
|
"""If ``color`` is not given, get the color value of the specified pixel. If ``color`` is
|
||||||
|
given, set the specified pixel to the given color."""
|
||||||
|
if self.rotation == 1:
|
||||||
|
x, y = y, x
|
||||||
|
x = self.width - x - 1
|
||||||
|
if self.rotation == 2:
|
||||||
|
x = self.width - x - 1
|
||||||
|
y = self.height - y - 1
|
||||||
|
if self.rotation == 3:
|
||||||
|
x, y = y, x
|
||||||
|
y = self.height - y - 1
|
||||||
|
|
||||||
|
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
||||||
|
return None
|
||||||
|
if color is None:
|
||||||
|
return self.format.get_pixel(self, x, y)
|
||||||
|
self.format.set_pixel(self, x, y, color)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def hline(self, x, y, width, color):
|
||||||
|
"""Draw a horizontal line up to a given length."""
|
||||||
|
self.rect(x, y, width, 1, color, fill=True)
|
||||||
|
|
||||||
|
def vline(self, x, y, height, color):
|
||||||
|
"""Draw a vertical line up to a given length."""
|
||||||
|
self.rect(x, y, 1, height, color, fill=True)
|
||||||
|
|
||||||
|
def circle(self, center_x, center_y, radius, color):
|
||||||
|
"""Draw a circle at the given midpoint location, radius and color.
|
||||||
|
The ```circle``` method draws only a 1 pixel outline."""
|
||||||
|
x = radius - 1
|
||||||
|
y = 0
|
||||||
|
d_x = 1
|
||||||
|
d_y = 1
|
||||||
|
err = d_x - (radius << 1)
|
||||||
|
while x >= y:
|
||||||
|
self.pixel(center_x + x, center_y + y, color)
|
||||||
|
self.pixel(center_x + y, center_y + x, color)
|
||||||
|
self.pixel(center_x - y, center_y + x, color)
|
||||||
|
self.pixel(center_x - x, center_y + y, color)
|
||||||
|
self.pixel(center_x - x, center_y - y, color)
|
||||||
|
self.pixel(center_x - y, center_y - x, color)
|
||||||
|
self.pixel(center_x + y, center_y - x, color)
|
||||||
|
self.pixel(center_x + x, center_y - y, color)
|
||||||
|
if err <= 0:
|
||||||
|
y += 1
|
||||||
|
err += d_y
|
||||||
|
d_y += 2
|
||||||
|
if err > 0:
|
||||||
|
x -= 1
|
||||||
|
d_x += 2
|
||||||
|
err += d_x - (radius << 1)
|
||||||
|
|
||||||
|
def rect(self, x, y, width, height, color, *, fill=False):
|
||||||
|
"""Draw a rectangle at the given location, size and color. The ```rect``` method draws only
|
||||||
|
a 1 pixel outline."""
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
|
if self.rotation == 1:
|
||||||
|
x, y = y, x
|
||||||
|
width, height = height, width
|
||||||
|
x = self.width - x - width
|
||||||
|
if self.rotation == 2:
|
||||||
|
x = self.width - x - width
|
||||||
|
y = self.height - y - height
|
||||||
|
if self.rotation == 3:
|
||||||
|
x, y = y, x
|
||||||
|
width, height = height, width
|
||||||
|
y = self.height - y - height
|
||||||
|
|
||||||
|
# pylint: disable=too-many-boolean-expressions
|
||||||
|
if (
|
||||||
|
width < 1
|
||||||
|
or height < 1
|
||||||
|
or (x + width) <= 0
|
||||||
|
or (y + height) <= 0
|
||||||
|
or y >= self.height
|
||||||
|
or x >= self.width
|
||||||
|
):
|
||||||
|
return
|
||||||
|
x_end = min(self.width - 1, x + width - 1)
|
||||||
|
y_end = min(self.height - 1, y + height - 1)
|
||||||
|
x = max(x, 0)
|
||||||
|
y = max(y, 0)
|
||||||
|
if fill:
|
||||||
|
self.format.fill_rect(self, x, y, x_end - x + 1, y_end - y + 1, color)
|
||||||
|
else:
|
||||||
|
self.format.fill_rect(self, x, y, x_end - x + 1, 1, color)
|
||||||
|
self.format.fill_rect(self, x, y, 1, y_end - y + 1, color)
|
||||||
|
self.format.fill_rect(self, x, y_end, x_end - x + 1, 1, color)
|
||||||
|
self.format.fill_rect(self, x_end, y, 1, y_end - y + 1, color)
|
||||||
|
|
||||||
|
def line(self, x_0, y_0, x_1, y_1, color):
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
|
"""Bresenham's line algorithm"""
|
||||||
|
d_x = abs(x_1 - x_0)
|
||||||
|
d_y = abs(y_1 - y_0)
|
||||||
|
x, y = x_0, y_0
|
||||||
|
s_x = -1 if x_0 > x_1 else 1
|
||||||
|
s_y = -1 if y_0 > y_1 else 1
|
||||||
|
if d_x > d_y:
|
||||||
|
err = d_x / 2.0
|
||||||
|
while x != x_1:
|
||||||
|
self.pixel(x, y, color)
|
||||||
|
err -= d_y
|
||||||
|
if err < 0:
|
||||||
|
y += s_y
|
||||||
|
err += d_x
|
||||||
|
x += s_x
|
||||||
|
else:
|
||||||
|
err = d_y / 2.0
|
||||||
|
while y != y_1:
|
||||||
|
self.pixel(x, y, color)
|
||||||
|
err -= d_x
|
||||||
|
if err < 0:
|
||||||
|
x += s_x
|
||||||
|
err += d_y
|
||||||
|
y += s_y
|
||||||
|
self.pixel(x, y, color)
|
||||||
|
|
||||||
|
def blit(self):
|
||||||
|
"""blit is not yet implemented"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def scroll(self, delta_x, delta_y):
|
||||||
|
"""shifts framebuf in x and y direction"""
|
||||||
|
if delta_x < 0:
|
||||||
|
shift_x = 0
|
||||||
|
xend = self.width + delta_x
|
||||||
|
dt_x = 1
|
||||||
|
else:
|
||||||
|
shift_x = self.width - 1
|
||||||
|
xend = delta_x - 1
|
||||||
|
dt_x = -1
|
||||||
|
if delta_y < 0:
|
||||||
|
y = 0
|
||||||
|
yend = self.height + delta_y
|
||||||
|
dt_y = 1
|
||||||
|
else:
|
||||||
|
y = self.height - 1
|
||||||
|
yend = delta_y - 1
|
||||||
|
dt_y = -1
|
||||||
|
while y != yend:
|
||||||
|
x = shift_x
|
||||||
|
while x != xend:
|
||||||
|
self.format.set_pixel(
|
||||||
|
self, x, y, self.format.get_pixel(self, x - delta_x, y - delta_y)
|
||||||
|
)
|
||||||
|
x += dt_x
|
||||||
|
y += dt_y
|
||||||
|
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
|
def text(self, string, x, y, color, *, font_name="font5x8.bin", size=1):
|
||||||
|
"""Place text on the screen in variables sizes. Breaks on \n to next line.
|
||||||
|
|
||||||
|
Does not break on line going off screen.
|
||||||
|
"""
|
||||||
|
# determine our effective width/height, taking rotation into account
|
||||||
|
frame_width = self.width
|
||||||
|
frame_height = self.height
|
||||||
|
if self.rotation in (1, 3):
|
||||||
|
frame_width, frame_height = frame_height, frame_width
|
||||||
|
|
||||||
|
for chunk in string.split("\n"):
|
||||||
|
if not self._font or self._font.font_name != font_name:
|
||||||
|
# load the font!
|
||||||
|
self._font = BitmapFont(font_name)
|
||||||
|
width = self._font.font_width
|
||||||
|
height = self._font.font_height
|
||||||
|
for i, char in enumerate(chunk):
|
||||||
|
char_x = x + (i * (width + 1)) * size
|
||||||
|
if (
|
||||||
|
char_x + (width * size) > 0
|
||||||
|
and char_x < frame_width
|
||||||
|
and y + (height * size) > 0
|
||||||
|
and y < frame_height
|
||||||
|
):
|
||||||
|
self._font.draw_char(char, char_x, y, self, color, size=size)
|
||||||
|
y += height * size
|
||||||
|
|
||||||
|
# pylint: enable=too-many-arguments
|
||||||
|
|
||||||
|
def image(self, img):
|
||||||
|
"""Set buffer to value of Python Imaging Library image. The image should
|
||||||
|
be in 1 bit mode and a size equal to the display size."""
|
||||||
|
# determine our effective width/height, taking rotation into account
|
||||||
|
width = self.width
|
||||||
|
height = self.height
|
||||||
|
if self.rotation in (1, 3):
|
||||||
|
width, height = height, width
|
||||||
|
|
||||||
|
if isinstance(self.format, (RGB565Format, RGB888Format)) and img.mode != "RGB":
|
||||||
|
raise ValueError("Image must be in mode RGB.")
|
||||||
|
if isinstance(self.format, (MHMSBFormat, MVLSBFormat)) and img.mode != "1":
|
||||||
|
raise ValueError("Image must be in mode 1.")
|
||||||
|
|
||||||
|
imwidth, imheight = img.size
|
||||||
|
if imwidth != width or imheight != height:
|
||||||
|
raise ValueError(
|
||||||
|
f"Image must be same dimensions as display ({width}x{height})."
|
||||||
|
)
|
||||||
|
# Grab all the pixels from the image, faster than getpixel.
|
||||||
|
pixels = img.load()
|
||||||
|
# Clear buffer
|
||||||
|
for i in range(len(self.buf)): # pylint: disable=consider-using-enumerate
|
||||||
|
self.buf[i] = 0
|
||||||
|
# Iterate through the pixels
|
||||||
|
for x in range(width): # yes this double loop is slow,
|
||||||
|
for y in range(height): # but these displays are small!
|
||||||
|
if img.mode == "RGB":
|
||||||
|
self.pixel(x, y, pixels[(x, y)])
|
||||||
|
elif pixels[(x, y)]:
|
||||||
|
self.pixel(x, y, 1) # only write if pixel is true
|
||||||
|
|
||||||
|
|
||||||
|
# MicroPython basic bitmap font renderer.
|
||||||
|
# Author: Tony DiCola
|
||||||
|
# License: MIT License (https://opensource.org/licenses/MIT)
|
||||||
|
class BitmapFont:
|
||||||
|
"""A helper class to read binary font tiles and 'seek' through them as a
|
||||||
|
file to display in a framebuffer. We use file access so we dont waste 1KB
|
||||||
|
of RAM on a font!"""
|
||||||
|
|
||||||
|
def __init__(self, font_name="font5x8.bin"):
|
||||||
|
# Specify the drawing area width and height, and the pixel function to
|
||||||
|
# call when drawing pixels (should take an x and y param at least).
|
||||||
|
# Optionally specify font_name to override the font file to use (default
|
||||||
|
# is font5x8.bin). The font format is a binary file with the following
|
||||||
|
# format:
|
||||||
|
# - 1 unsigned byte: font character width in pixels
|
||||||
|
# - 1 unsigned byte: font character height in pixels
|
||||||
|
# - x bytes: font data, in ASCII order covering all 255 characters.
|
||||||
|
# Each character should have a byte for each pixel column of
|
||||||
|
# data (i.e. a 5x8 font has 5 bytes per character).
|
||||||
|
self.font_name = font_name
|
||||||
|
|
||||||
|
# Open the font file and grab the character width and height values.
|
||||||
|
# Note that only fonts up to 8 pixels tall are currently supported.
|
||||||
|
try:
|
||||||
|
self._font = open( # pylint: disable=consider-using-with
|
||||||
|
self.font_name, "rb"
|
||||||
|
)
|
||||||
|
self.font_width, self.font_height = struct.unpack("BB", self._font.read(2))
|
||||||
|
# simple font file validation check based on expected file size
|
||||||
|
if 2 + 256 * self.font_width != os.stat(font_name)[6]:
|
||||||
|
raise RuntimeError("Invalid font file: " + font_name)
|
||||||
|
except OSError:
|
||||||
|
print("Could not find font file", font_name)
|
||||||
|
raise
|
||||||
|
except OverflowError:
|
||||||
|
# os.stat can throw this on boards without long int support
|
||||||
|
# just hope the font file is valid and press on
|
||||||
|
pass
|
||||||
|
|
||||||
|
def deinit(self):
|
||||||
|
"""Close the font file as cleanup."""
|
||||||
|
self._font.close()
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
"""Initialize/open the font file"""
|
||||||
|
self.__init__()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exception_type, exception_value, traceback):
|
||||||
|
"""cleanup on exit"""
|
||||||
|
self.deinit()
|
||||||
|
|
||||||
|
def draw_char(
|
||||||
|
self, char, x, y, framebuffer, color, size=1
|
||||||
|
): # pylint: disable=too-many-arguments
|
||||||
|
"""Draw one character at position (x,y) to a framebuffer in a given color"""
|
||||||
|
size = max(size, 1)
|
||||||
|
# Don't draw the character if it will be clipped off the visible area.
|
||||||
|
# if x < -self.font_width or x >= framebuffer.width or \
|
||||||
|
# y < -self.font_height or y >= framebuffer.height:
|
||||||
|
# return
|
||||||
|
# Go through each column of the character.
|
||||||
|
for char_x in range(self.font_width):
|
||||||
|
# Grab the byte for the current column of font data.
|
||||||
|
self._font.seek(2 + (ord(char) * self.font_width) + char_x)
|
||||||
|
try:
|
||||||
|
line = struct.unpack("B", self._font.read(1))[0]
|
||||||
|
except RuntimeError:
|
||||||
|
continue # maybe character isnt there? go to next
|
||||||
|
# Go through each row in the column byte.
|
||||||
|
for char_y in range(self.font_height):
|
||||||
|
# Draw a pixel for each bit that's flipped on.
|
||||||
|
if (line >> char_y) & 0x1:
|
||||||
|
framebuffer.fill_rect(
|
||||||
|
x + char_x * size, y + char_y * size, size, size, color
|
||||||
|
)
|
||||||
|
|
||||||
|
def width(self, text):
|
||||||
|
"""Return the pixel width of the specified text message."""
|
||||||
|
return len(text) * (self.font_width + 1)
|
||||||
|
|
||||||
|
|
||||||
|
class FrameBuffer1(FrameBuffer): # pylint: disable=abstract-method
|
||||||
|
"""FrameBuffer1 object. Inherits from FrameBuffer."""
|
|
@ -0,0 +1,110 @@
|
||||||
|
# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
|
||||||
|
# SPDX-FileCopyrightText: Copyright (c) 2021 Melissa LeBlanc-Williams for Adafruit Industries
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
"""
|
||||||
|
`adafruit_ssd1680`
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
CircuitPython `displayio` driver for SSD1680-based ePaper displays
|
||||||
|
|
||||||
|
|
||||||
|
* Author(s): Melissa LeBlanc-Williams
|
||||||
|
|
||||||
|
Implementation Notes
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
**Hardware:**
|
||||||
|
|
||||||
|
* `Adafruit 2.13" Tri-Color eInk Display Breakout <https://www.adafruit.com/product/4947>`_
|
||||||
|
* `Adafruit 2.13" Tri-Color eInk Display FeatherWing <https://www.adafruit.com/product/4814>`_
|
||||||
|
* `Adafruit 2.13" Mono eInk Display FeatherWing <https://www.adafruit.com/product/4195>`_
|
||||||
|
|
||||||
|
|
||||||
|
**Software and Dependencies:**
|
||||||
|
|
||||||
|
* Adafruit CircuitPython firmware for the supported boards:
|
||||||
|
https://github.com/adafruit/circuitpython/releases
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
from epaperdisplay import EPaperDisplay
|
||||||
|
from fourwire import FourWire
|
||||||
|
except ImportError:
|
||||||
|
from displayio import EPaperDisplay
|
||||||
|
from displayio import FourWire
|
||||||
|
|
||||||
|
|
||||||
|
__version__ = "0.0.0+auto.0"
|
||||||
|
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_SSD1680.git"
|
||||||
|
|
||||||
|
_START_SEQUENCE = (
|
||||||
|
b"\x12\x80\x14" # soft reset and wait 20ms
|
||||||
|
b"\x11\x01\x03" # Ram data entry mode
|
||||||
|
b"\x3C\x01\x05" # border color
|
||||||
|
b"\x2c\x01\x36" # Set vcom voltage
|
||||||
|
b"\x03\x01\x17" # Set gate voltage
|
||||||
|
b"\x04\x03\x41\x00\x32" # Set source voltage
|
||||||
|
b"\x4e\x01\x01" # ram x count
|
||||||
|
b"\x4f\x02\x00\x00" # ram y count
|
||||||
|
b"\x01\x03\x00\x00\x00" # set display size
|
||||||
|
b"\x22\x01\xf4" # display update mode
|
||||||
|
)
|
||||||
|
|
||||||
|
_STOP_SEQUENCE = b"\x10\x81\x01\x64" # Deep Sleep
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-few-public-methods
|
||||||
|
class SSD1680(EPaperDisplay):
|
||||||
|
r"""SSD1680 driver
|
||||||
|
|
||||||
|
:param bus: The data bus the display is on
|
||||||
|
:param \**kwargs:
|
||||||
|
See below
|
||||||
|
|
||||||
|
:Keyword Arguments:
|
||||||
|
* *width* (``int``) --
|
||||||
|
Display width
|
||||||
|
* *height* (``int``) --
|
||||||
|
Display height
|
||||||
|
* *rotation* (``int``) --
|
||||||
|
Display rotation
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bus: FourWire, column_correction=1,**kwargs) -> None:
|
||||||
|
if "colstart" not in kwargs:
|
||||||
|
kwargs["colstart"] = 8
|
||||||
|
stop_sequence = bytearray(_STOP_SEQUENCE)
|
||||||
|
try:
|
||||||
|
bus.reset()
|
||||||
|
except RuntimeError:
|
||||||
|
# No reset pin defined, so no deep sleeping
|
||||||
|
stop_sequence = b""
|
||||||
|
|
||||||
|
start_sequence = bytearray(_START_SEQUENCE)
|
||||||
|
width = kwargs["width"]
|
||||||
|
height = kwargs["height"]
|
||||||
|
if "rotation" in kwargs and kwargs["rotation"] % 180 != 90:
|
||||||
|
width, height = height, width
|
||||||
|
start_sequence[29] = (width - 1) & 0xFF
|
||||||
|
start_sequence[30] = ((width - 1) >> 8) & 0xFF
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
bus,
|
||||||
|
start_sequence,
|
||||||
|
stop_sequence,
|
||||||
|
**kwargs,
|
||||||
|
ram_width=250,
|
||||||
|
ram_height=296,
|
||||||
|
busy_state=True,
|
||||||
|
write_black_ram_command=0x24,
|
||||||
|
write_color_ram_command=0x26,
|
||||||
|
set_column_window_command=0x44,
|
||||||
|
set_row_window_command=0x45,
|
||||||
|
set_current_column_command=0x4E,
|
||||||
|
set_current_row_command=0x4F,
|
||||||
|
refresh_display_command=0x20,
|
||||||
|
always_toggle_chip_select=False,
|
||||||
|
address_little_endian=True
|
||||||
|
)
|
Binary file not shown.
After Width: | Height: | Size: 352 KiB |
|
@ -0,0 +1,180 @@
|
||||||
|
# SPDX-FileCopyrightText: 2016 Damien P. George
|
||||||
|
# SPDX-FileCopyrightText: 2017 Scott Shawcroft for Adafruit Industries
|
||||||
|
# SPDX-FileCopyrightText: 2019 Carter Nelson
|
||||||
|
# SPDX-FileCopyrightText: 2019 Roy Hooper
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
`neopixel` - NeoPixel strip driver
|
||||||
|
====================================================
|
||||||
|
|
||||||
|
* Author(s): Damien P. George, Scott Shawcroft, Carter Nelson, Rose Hooper
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import board
|
||||||
|
import digitalio
|
||||||
|
from neopixel_write import neopixel_write
|
||||||
|
|
||||||
|
import adafruit_pixelbuf
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Used only for typing
|
||||||
|
from typing import Optional, Type
|
||||||
|
from types import TracebackType
|
||||||
|
import microcontroller
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
__version__ = "0.0.0+auto.0"
|
||||||
|
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_NeoPixel.git"
|
||||||
|
|
||||||
|
|
||||||
|
# Pixel color order constants
|
||||||
|
RGB = "RGB"
|
||||||
|
"""Red Green Blue"""
|
||||||
|
GRB = "GRB"
|
||||||
|
"""Green Red Blue"""
|
||||||
|
RGBW = "RGBW"
|
||||||
|
"""Red Green Blue White"""
|
||||||
|
GRBW = "GRBW"
|
||||||
|
"""Green Red Blue White"""
|
||||||
|
|
||||||
|
|
||||||
|
class NeoPixel(adafruit_pixelbuf.PixelBuf):
|
||||||
|
"""
|
||||||
|
A sequence of neopixels.
|
||||||
|
|
||||||
|
:param ~microcontroller.Pin pin: The pin to output neopixel data on.
|
||||||
|
:param int n: The number of neopixels in the chain
|
||||||
|
:param int bpp: Bytes per pixel. 3 for RGB and 4 for RGBW pixels.
|
||||||
|
:param float brightness: Brightness of the pixels between 0.0 and 1.0 where 1.0 is full
|
||||||
|
brightness
|
||||||
|
:param bool auto_write: True if the neopixels should immediately change when set. If False,
|
||||||
|
`show` must be called explicitly.
|
||||||
|
:param str pixel_order: Set the pixel color channel order. GRBW is set by default.
|
||||||
|
|
||||||
|
Example for Circuit Playground Express:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import neopixel
|
||||||
|
from board import *
|
||||||
|
|
||||||
|
RED = 0x100000 # (0x10, 0, 0) also works
|
||||||
|
|
||||||
|
pixels = neopixel.NeoPixel(NEOPIXEL, 10)
|
||||||
|
for i in range(len(pixels)):
|
||||||
|
pixels[i] = RED
|
||||||
|
|
||||||
|
Example for Circuit Playground Express setting every other pixel red using a slice:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import neopixel
|
||||||
|
from board import *
|
||||||
|
import time
|
||||||
|
|
||||||
|
RED = 0x100000 # (0x10, 0, 0) also works
|
||||||
|
|
||||||
|
# Using ``with`` ensures pixels are cleared after we're done.
|
||||||
|
with neopixel.NeoPixel(NEOPIXEL, 10) as pixels:
|
||||||
|
pixels[::2] = [RED] * (len(pixels) // 2)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
.. py:method:: NeoPixel.show()
|
||||||
|
|
||||||
|
Shows the new colors on the pixels themselves if they haven't already
|
||||||
|
been autowritten.
|
||||||
|
|
||||||
|
The colors may or may not be showing after this function returns because
|
||||||
|
it may be done asynchronously.
|
||||||
|
|
||||||
|
.. py:method:: NeoPixel.fill(color)
|
||||||
|
|
||||||
|
Colors all pixels the given ***color***.
|
||||||
|
|
||||||
|
.. py:attribute:: brightness
|
||||||
|
|
||||||
|
Overall brightness of the pixel (0 to 1.0)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
pin: microcontroller.Pin,
|
||||||
|
n: int,
|
||||||
|
*,
|
||||||
|
bpp: int = 3,
|
||||||
|
brightness: float = 1.0,
|
||||||
|
auto_write: bool = True,
|
||||||
|
pixel_order: str = None
|
||||||
|
):
|
||||||
|
if not pixel_order:
|
||||||
|
pixel_order = GRB if bpp == 3 else GRBW
|
||||||
|
elif isinstance(pixel_order, tuple):
|
||||||
|
order_list = [RGBW[order] for order in pixel_order]
|
||||||
|
pixel_order = "".join(order_list)
|
||||||
|
|
||||||
|
self._power = None
|
||||||
|
if (
|
||||||
|
sys.implementation.version[0] >= 7
|
||||||
|
and getattr(board, "NEOPIXEL", None) == pin
|
||||||
|
):
|
||||||
|
power = getattr(board, "NEOPIXEL_POWER_INVERTED", None)
|
||||||
|
polarity = power is None
|
||||||
|
if not power:
|
||||||
|
power = getattr(board, "NEOPIXEL_POWER", None)
|
||||||
|
if power:
|
||||||
|
try:
|
||||||
|
self._power = digitalio.DigitalInOut(power)
|
||||||
|
self._power.switch_to_output(value=polarity)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
n, brightness=brightness, byteorder=pixel_order, auto_write=auto_write
|
||||||
|
)
|
||||||
|
|
||||||
|
self.pin = digitalio.DigitalInOut(pin)
|
||||||
|
self.pin.direction = digitalio.Direction.OUTPUT
|
||||||
|
|
||||||
|
def deinit(self) -> None:
|
||||||
|
"""Blank out the NeoPixels and release the pin."""
|
||||||
|
self.fill(0)
|
||||||
|
self.show()
|
||||||
|
self.pin.deinit()
|
||||||
|
if self._power:
|
||||||
|
self._power.deinit()
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(
|
||||||
|
self,
|
||||||
|
exception_type: Optional[Type[BaseException]],
|
||||||
|
exception_value: Optional[BaseException],
|
||||||
|
traceback: Optional[TracebackType],
|
||||||
|
):
|
||||||
|
self.deinit()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "[" + ", ".join([str(x) for x in self]) + "]"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def n(self) -> int:
|
||||||
|
"""
|
||||||
|
The number of neopixels in the chain (read-only)
|
||||||
|
"""
|
||||||
|
return len(self)
|
||||||
|
|
||||||
|
def write(self) -> None:
|
||||||
|
""".. deprecated: 1.0.0
|
||||||
|
|
||||||
|
Use ``show`` instead. It matches Micro:Bit and Arduino APIs."""
|
||||||
|
self.show()
|
||||||
|
|
||||||
|
def _transmit(self, buffer: bytearray) -> None:
|
||||||
|
neopixel_write(self.pin, buffer)
|
Loading…
Reference in New Issue