Switch to circuitpython. Get some basics working...

This commit is contained in:
n loewen 2025-03-04 19:24:26 +00:00
parent fa504685d2
commit fd4ca3e8c8
7 changed files with 2391 additions and 19 deletions

View File

@ -0,0 +1,474 @@
# SPDX-FileCopyrightText: 2020 Tim C, 2021 Jeff Epler for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
`adafruit_display_text`
=======================
"""
__version__ = "3.2.2"
__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,
) -> 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
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:
if new_text == self._text:
return
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"))

View File

@ -0,0 +1,597 @@
# 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__ = "3.2.2"
__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,too-many-branches
bbox = font.get_bounding_box()
if len(bbox) == 4:
ascender_max, descender_max = bbox[1], -bbox[3]
else:
ascender_max, descender_max = self._ascent, self._descent
lines = 1
# starting x and y position (left margin)
xposition = x_start = yposition = y_start = 0
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

View File

@ -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__ = "3.2.2"
__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"

View File

@ -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__ = "3.2.2"
__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,
)

View File

@ -0,0 +1,160 @@
# 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__ = "3.2.2"
__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 and 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:
if self._text != self.full_text:
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 self.full_text:
self._current_index = new_index % len(self.full_text)
else:
self._current_index = 0
@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 and new_text[-1] != " ":
new_text = "{} ".format(new_text)
if new_text != self._full_text:
self._full_text = new_text
self.current_index = 0
self.update(True)
@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

View File

@ -0,0 +1,435 @@
# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
`adafruit_display_text.text_box`
================================================================================
Text graphics handling for CircuitPython, including text boxes
* Author(s): Tim Cocks
Implementation Notes
--------------------
**Hardware:**
**Software and Dependencies:**
* Adafruit CircuitPython firmware for the supported boards:
https://circuitpython.org/downloads
"""
__version__ = "3.2.2"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git"
import displayio
from micropython import const
from adafruit_display_text import wrap_text_to_pixels
from adafruit_display_text import bitmap_label
try:
from typing import Optional, Tuple
from fontio import FontProtocol
except ImportError:
pass
# pylint: disable=too-many-instance-attributes, duplicate-code
class TextBox(bitmap_label.Label):
"""
TextBox has a constrained width and optionally height.
You set the desired size when it's initialized it
will automatically wrap text to fit it within the allotted
size.
Left, Right, and Center alignment of the text within the
box are supported.
:param font: The font to use for the TextBox.
:param width: The width of the TextBox in pixels.
:param height: The height of the TextBox in pixels.
:param align: How to align the text within the box,
valid values are ``ALIGN_LEFT``, ``ALIGN_CENTER``, ``ALIGN_RIGHT``.
"""
ALIGN_LEFT = const(0)
ALIGN_CENTER = const(1)
ALIGN_RIGHT = const(2)
DYNAMIC_HEIGHT = const(-1)
def __init__(
self, font: FontProtocol, width: int, height: int, align=ALIGN_LEFT, **kwargs
) -> None:
self._bitmap = None
self._tilegrid = None
self._prev_label_direction = None
self._width = width
if height != TextBox.DYNAMIC_HEIGHT:
self._height = height
self.dynamic_height = False
else:
self.dynamic_height = True
if align not in (TextBox.ALIGN_LEFT, TextBox.ALIGN_CENTER, TextBox.ALIGN_RIGHT):
raise ValueError(
"Align must be one of: ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT"
)
self._align = align
self._padding_left = kwargs.get("padding_left", 0)
self._padding_right = kwargs.get("padding_right", 0)
self.lines = wrap_text_to_pixels(
kwargs.get("text", ""),
self._width - self._padding_left - self._padding_right,
font,
)
super(bitmap_label.Label, self).__init__(font, **kwargs)
print(f"before reset: {self._text}")
self._text = "\n".join(self.lines)
self._text = self._replace_tabs(self._text)
self._original_text = 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,
)
print(f"after reset: {self._text}")
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, too-many-statements, too-many-branches
# placeText - Writes text into a bitmap at the specified location.
#
# Note: scale is pushed up to Group level
original_xposition = xposition
cur_line_index = 0
cur_line_width = self._text_bounding_box(self.lines[0], self.font)[0]
if self.align == self.ALIGN_LEFT:
x_start = original_xposition # starting x position (left margin)
if self.align == self.ALIGN_CENTER:
unused_space = self._width - cur_line_width
x_start = original_xposition + unused_space // 2
if self.align == self.ALIGN_RIGHT:
unused_space = self._width - cur_line_width
x_start = original_xposition + unused_space - self._padding_right
xposition = x_start # pylint: disable=used-before-assignment
y_start = yposition
# print(f"start loc {x_start}, {y_start}")
left = None
right = x_start
top = bottom = y_start
line_spacing = self._line_spacing
# print(f"cur_line width: {cur_line_width}")
for char in text:
if char == "\n": # newline
cur_line_index += 1
cur_line_width = self._text_bounding_box(
self.lines[cur_line_index], self.font
)[0]
# print(f"cur_line width: {cur_line_width}")
if self.align == self.ALIGN_LEFT:
x_start = original_xposition # starting x position (left margin)
if self.align == self.ALIGN_CENTER:
unused_space = self._width - cur_line_width
x_start = original_xposition + unused_space // 2
if self.align == self.ALIGN_RIGHT:
unused_space = self._width - cur_line_width
x_start = original_xposition + unused_space - self._padding_right
xposition = x_start
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
)
)
try:
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
)
except ValueError:
# ignore index out of bounds error
break
xposition = xposition + my_glyph.shift_x
# bounding_box
return left, top, right - left, bottom - top
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:
text = self._text
self._text = self._replace_tabs(text)
print(f"inside reset_text text: {text}")
# 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
if self.dynamic_height:
print(f"dynamic height, box_y: {box_y}")
self._height = box_y
# Create the Bitmap unless it can be reused
new_bitmap = None
if (
self._bitmap is None
or self._bitmap.width != self._width
or self._bitmap.height != self._height
):
new_bitmap = displayio.Bitmap(
self._width, self._height, len(self._palette)
)
self._bitmap = new_bitmap
else:
self._bitmap.fill(0)
# Place the text into the Bitmap
self._place_text(
self._bitmap,
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=self._width,
tile_height=self._height,
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
self._bounding_box = (
self._tilegrid.x + self._padding_left,
self._tilegrid.y + self._padding_top,
tight_box_x,
tight_box_y,
)
print(f"end of reset_text bounding box: {self._bounding_box}")
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
@property
def height(self) -> int:
"""The height of the label determined from the bounding box."""
return self._height
@property
def width(self) -> int:
"""The width of the label determined from the bounding box."""
return self._width
@width.setter
def width(self, width: int) -> None:
self._width = width
self.text = self._text
@height.setter
def height(self, height: int) -> None:
if height != TextBox.DYNAMIC_HEIGHT:
self._height = height
self.dynamic_height = False
else:
self.dynamic_height = True
self.text = self._text
@bitmap_label.Label.text.setter
def text(self, text: str) -> None:
self.lines = wrap_text_to_pixels(
text, self._width - self._padding_left - self._padding_right, self.font
)
self._text = self._replace_tabs(text)
self._original_text = self._text
self._text = "\n".join(self.lines)
self._set_text(self._text, self.scale)
@property
def align(self):
"""Alignment of the text within the TextBox"""
return self._align
@align.setter
def align(self, align: int) -> None:
if align not in (TextBox.ALIGN_LEFT, TextBox.ALIGN_CENTER, TextBox.ALIGN_RIGHT):
raise ValueError(
"Align must be one of: ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT"
)
self._align = align

View File

@ -1,5 +1,26 @@
from sys import exit
from time import sleep
# from sys import exit
import time
import board
import displayio
# import vectorio
import terminalio # for font
from adafruit_display_text import label
import keypad
from digitalio import DigitalInOut, Direction, Pull
# to list board features: print(board.__dir__)
btna = DigitalInOut(board.BTNA)
btna.direction = Direction.INPUT
btna.pull = Pull.UP # down doesn't work
btnb = DigitalInOut(board.BTNB)
btnb.direction = Direction.INPUT
btnb.pull = Pull.UP # down doesn't work
km = keypad.KeyMatrix(
row_pins = (board.P0, board.P1, board.P2, board.P3),
column_pins = (board.P4, board.P6, board.P8, board.P9) )
class CPU:
def __init__(self):
@ -9,6 +30,12 @@ class CPU:
self.flags = { 'C': False, 'Z': False, 'N': False, 'Eq': False }
self.instruction = { 'opcode': False, 'operand': False }
self.memory = False
self.monitorMode = 'addressEntry' # dataEntry
self.monitorAddressInput = "00"
self.monitorDataInput = "00"
self.monitorAddressDisplay = "00"
self.monitorDataDisplay = "00"
def load_memory(self, bytes):
self.memory = bytes + bytearray(256 - len(bytes))
@ -30,22 +57,61 @@ class CPU:
else:
print("_", end=" ")
print()
def print_monitor(self):
displayGroup = displayio.Group()
board.DISPLAY.root_group = displayGroup
text = self.monitorAddressDisplay + " " + self.monitorDataDisplay
text_area = label.Label(terminalio.FONT, text=text)
text_area.x = 10
text_area.y = 100
displayGroup.append(text_area)
def read_keys(self):
key = input(">").upper()
keypad_event = km.events.get()
if keypad_event and keypad_event.released:
print(keypad_event.key_number)
if self.running:
if key in ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0']:
print(type(self.memory))
self.memory[26] = int(key)
if key in ['A', 'B', 'C', 'D', 'E', 'F']:
self.memory[26] = ord(key) - 55
if key == "H": # halt
self.running = False
else:
# TODO trainer interface
if btna.value == False:
self.running = False
print("HALT PRESSED")
time.sleep(0.5) # lazy debounce
# km.events.clear() # don't track keypresses from during the run
if keypad_event and keypad_event.released:
self.memory[26] = keypad_event.key_number
print("mem26", self.memory[26])
else: # halted
if btna.value == False:
self.running = True
print("STARTING")
time.sleep(0.5) # lazy debounce
if btnb.value == False:
self.monitorMode = 'addressEntry' if self.monitorMode != 'addressEntry' else 'dataEntry'
print(self.monitorMode)
time.sleep(0.5) # lazy debounce
if keypad_event and keypad_event.released:
new_digit_0x = hex(keypad_event.key_number)
new_digit = new_digit_0x[2:3]
if self.monitorMode == 'addressEntry':
cat = self.monitorAddressInput + new_digit
self.monitorAddressInput = cat[1:3]
print("MA", self.monitorAddressInput)
else:
cat = self.monitorDataInput + new_digit
self.monitorDataInput = cat[1:3]
print("MD", self.monitorDataInput)
print("Acc", self.acc, "Addr", self.monitorAddressInput, "Data", self.memory[int(self.monitorAddressInput)])
# TODO: update data with keypad
# then, automatically go to next address (?)
def step(self):
self.read_keys()
if self.IP >= 256:
self.IP = 0
print("IP:", self.IP)
@ -59,7 +125,7 @@ class CPU:
print("acc:", self.acc)
print("running:", self.running)
print()
self.print_screen()
# self.print_screen()
print("byte 26 (keyboard):", self.memory[26])
print()
# if not self.running:
@ -67,9 +133,14 @@ class CPU:
def run(self):
self.start()
while True:
self.step()
sleep(0.1)
t = time.time()
while (time.time() - t) < 30:
self.read_keys()
#self.print_monitor()
#if self.running:
# self.step()
# time.time.sleep(0.5)
print("timeout")
def hlt(self, operand):
self.running = False
@ -193,9 +264,9 @@ class CPU:
cpu = CPU()
prog = '04 FF 04 01 14 01 00 00'
program_bytes = bytearray.fromhex(prog)
program_bytes = bytearray.fromhex(prog.replace(" ", ""))
# Add jmp at addr 254:
program_with_jump = program_bytes + bytearray(254 - len(program_bytes)) + bytearray.fromhex('06 00')
program_with_jump = program_bytes + bytearray(254 - len(program_bytes)) + bytearray.fromhex('0600')
cpu.load_memory(program_with_jump)
# cpu.start()