Compare commits

...

35 commits

Author SHA1 Message Date
foamyguy
bcd7c653ec docstring fix for new sphinx 2021-01-07 18:40:36 -06:00
foamyguy
28b3879f5b fallback for builtin font 2021-01-07 18:21:25 -06:00
Jeff Epler
eaeed21773 label: Use new, optional 'ascent', 'descent' properties
Each time new glyphs have to be loaded from a font, it can take a long
time (hundreds of milliseconds).  In a parallel commit, 'ascent' and
'descent' properties will be added to BDF font objects, are essentially
free to compute, and are in any case much quicker than entering
load_glyphs.

This may change the layout of text slightly.  For instance, the height
of the "M" glyph in GothamBlack-50.bdf is 35, but the Ascent of the font
is 40 (and some characters, such as Å, are taller than the ascent at 45
pixels high)
2020-12-28 09:26:00 -06:00
Melissa LeBlanc-Williams
9bc4d2b5c8
Merge pull request #99 from FoamyGuy/wrapping
Wrapping helper function
2020-12-04 15:28:31 -07:00
Limor "Ladyada" Fried
f86e1afc94
Merge pull request #101 from FoamyGuy/add_magtag_example
Adding MagTag example
2020-11-22 15:43:32 -05:00
foamyguy
7fcd2065ac remove extra empty lines 2020-11-22 13:00:08 -06:00
foamyguy
62e7cdc584 adding magtag example 2020-11-22 12:59:37 -06:00
foamyguy
256380c71e fix words longer than max_chars 2020-11-21 19:10:48 -06:00
foamyguy
006fee4a6c black format 2020-11-16 22:03:05 -06:00
foamyguy
f3c565c359 add text wrapping helper function and example that uses it. 2020-11-16 21:47:53 -06:00
foamyguy
aedb89cc8c Merge remote-tracking branch 'origin/master' 2020-11-14 14:13:20 -06:00
foamyguy
ff7115131e
Merge pull request #98 from kmatch98/blank_text
Correct bug of modifying self rather than self.local_group when text …
2020-11-14 14:05:01 -06:00
Kevin Matocha
7f9dc63ed5 Fix long comment line for pylint 2020-11-14 13:30:44 -06:00
Kevin Matocha
1f42993525 Correct bug of modifying self rather than self.local_group when text is blank 2020-11-14 13:27:17 -06:00
foamyguy
0f337b892e
Merge pull request #2 from adafruit/master
merge from adafruit
2020-11-06 10:04:54 -06:00
foamyguy
957335cf9e
Merge pull request #96 from FoamyGuy/add_matrixportal_example
Add matrixportal example
2020-11-06 09:56:52 -06:00
foamyguy
856e6d2860 adding matrix portal example 2020-11-06 09:38:29 -06:00
foamyguy
2ab443f6e8
Merge pull request #1 from adafruit/master
merge from adafruit
2020-10-22 21:50:52 -05:00
foamyguy
0c57ae8728
Merge pull request #93 from eteq/add-init
add init
2020-10-19 18:08:56 -05:00
Erik Tollerud
6513a95867 add init 2020-10-17 23:59:24 -04:00
foamyguy
df94e2fbd9
Merge pull request #92 from kmatch98/bitmap_mutable
Correct documentation errors related to mutability
2020-09-05 13:25:14 -05:00
Kevin Matocha
444a8adb73 Correct documentation errors related to mutability 2020-09-05 11:35:28 -05:00
Scott Shawcroft
a836740924
Merge pull request #90 from kmatch98/bitmap_mutable
`bitmap_label`: Make text, line_spacing and scale mutable
2020-08-31 14:15:38 -07:00
Kevin Matocha
f23910cdce Breakout blit function, improve handling of builtin builtin bitmap.blit, add back kwargs passing to self Group instance 2020-08-28 15:34:01 -05:00
Kevin Matocha
4bab6cf4c0 Update docs file to include bitmap_label 2020-08-28 10:20:55 -05:00
Kevin Matocha
5d299cc0e8 ran black and pylint 2020-08-28 10:13:52 -05:00
Kevin Matocha
9ef676ef6d Fix scale bug in label.py, remove kwargs from both and add scale input parameter 2020-08-28 10:12:09 -05:00
Kevin Matocha
df1c8eafd2 fix bug with scale initialization 2020-08-27 16:48:06 -05:00
Kevin Matocha
98467964bf pylint fixes 2020-08-27 14:55:06 -05:00
Kevin Matocha
1a47464430 ran black and pylint updates 2020-08-27 14:45:35 -05:00
Kevin Matocha
7ca736f7e1 Add scale mutability, bug fix on blit range 2020-08-27 13:58:29 -05:00
Kevin Matocha
bece1ad28d Bug fix in try/except in _place_text 2020-08-26 14:07:49 -05:00
Kevin Matocha
a47afc06c2 Add try/except backward compatibility for bitmap.blit function in _place_text 2020-08-26 08:16:19 -05:00
Kevin Matocha
3e8f35f897 Added getter/setters for text, line_spacing and temporary fix for set_scale, some performance speedups by deleting duplication in bounding box calculations 2020-08-25 22:26:30 -05:00
Kevin Matocha
a404b09fd7 first commit with mutable text, font and line_spacing 2020-08-25 15:56:46 -05:00
7 changed files with 584 additions and 210 deletions

View file

@ -0,0 +1,69 @@
"""
Display Text module helper functions
"""
# The MIT License (MIT)
#
# Copyright (c) 2020 Tim C for Adafruit Industries LLC
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
def wrap_text_to_lines(string, max_chars):
"""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 list the_lines: A list of lines where each line is separated based on the amount
of max_chars provided
"""
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:
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
else:
the_lines.append(the_line)
the_line = "" + w
if the_line: # Last line remaining
the_lines.append(the_line)
# Remove first space from first line:
if the_lines[0][0] == " ":
the_lines[0] = the_lines[0][1:]
return the_lines

View file

@ -50,13 +50,12 @@ class Label(displayio.Group):
"""A label displaying a string of text that is stored in a bitmap.
Note: This ``bitmap_label.py`` library utilizes a bitmap to display the text.
This method is memory-conserving relative to ``label.py``.
For the bitmap_label library, the font, text, and line_spacing must be set at
instancing and are immutable. The ``max_glyphs`` parameter is ignored and is present
The ``max_glyphs`` parameter is ignored and is present
only for direct compatability with label.py.
For use cases where text changes are required after the initial instancing, please
use the `label.py` library.
For further reduction in memory usage, set save_text to False (text string will not
be stored).
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
@ -67,28 +66,28 @@ class Label(displayio.Group):
Must include a capital M for measuring character size.
:param str text: Text to display
:param int max_glyphs: Unnecessary parameter (provided only for direct compability
with label.py)
with label.py)
:param int color: Color of all text in RGB hex
:param int background_color: Color of the background, use `None` for transparent
:param double line_spacing: Line spacing of text to display
:param boolean background_tight: Set `True` only if you want background box to tightly
surround text
surround text
: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 (double,double) 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.)
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.
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.
label structure. Set False to reduce memory use.
"""
# pylint: disable=unused-argument, too-many-instance-attributes, too-many-locals, too-many-arguments
# pylint: disable=too-many-branches, no-self-use
# pylint: disable=too-many-branches, no-self-use, too-many-statements
# Note: max_glyphs parameter is unnecessary, this is used for direct
# compatibility with label.py
@ -111,128 +110,217 @@ class Label(displayio.Group):
anchor_point=None,
anchored_position=None,
save_text=True, # can reduce memory use if save_text = False
**kwargs
scale=1,
**kwargs,
):
if text == "":
raise RuntimeError(
"Please provide text string, or use label.py for mutable text"
)
# instance the Group
# self Group will contain a single local_group which contains a Group (self.local_group)
# which contains a TileGrid (self.tilegrid) which contains the text bitmap (self.bitmap)
super().__init__(
max_size=1, x=x, y=y, scale=1, **kwargs,
)
# the self group scale should always remain at 1, the self.local_group will
# be used to set the scale
# **kwargs will pass any additional arguments provided to the Label
self.local_group = displayio.Group(
max_size=1, scale=scale
) # local_group holds the tileGrid and sets the scaling
self.append(
self.local_group
) # the local_group will always stay in the self Group
self._font = font
self._text = text
# Scale will be passed to Group using kwargs.
if "scale" in kwargs.keys():
self._scale = kwargs["scale"]
else:
self._scale = 1
# Create the two-color palette
self.palette = displayio.Palette(2)
self.color = color
self.background_color = background_color
self._line_spacing = line_spacing
self._save_text = save_text
self._anchor_point = anchor_point
self._anchored_position = anchored_position
# call the text updater with all the arguments.
self._reset_text(
font=font,
x=x,
y=y,
text=text,
line_spacing=line_spacing,
background_tight=background_tight,
padding_top=padding_top,
padding_bottom=padding_bottom,
padding_left=padding_left,
padding_right=padding_right,
anchor_point=anchor_point,
anchored_position=anchored_position,
save_text=save_text,
scale=scale,
)
def _reset_text(
self,
font=None,
x=None,
y=None,
text=None,
line_spacing=None,
background_tight=None,
padding_top=None,
padding_bottom=None,
padding_left=None,
padding_right=None,
anchor_point=None,
anchored_position=None,
save_text=None,
scale=None,
):
# Store all the instance variables
if font is not None:
self._font = font
if x is not None:
self.x = x
if y is not None:
self.y = y
if line_spacing is not None:
self._line_spacing = line_spacing
if background_tight is not None:
self._background_tight = background_tight
if padding_top is not None:
self._padding_top = max(0, padding_top)
if padding_bottom is not None:
self._padding_bottom = max(0, padding_bottom)
if padding_left is not None:
self._padding_left = max(0, padding_left)
if padding_right is not None:
self._padding_right = max(0, padding_right)
if anchor_point is not None:
self.anchor_point = anchor_point
if anchored_position is not None:
self._anchored_position = anchored_position
if save_text is not None:
self._save_text = save_text
if (
scale is not None
): # Scale will be defined in local_group (Note: self should have scale=1)
self.scale = scale # call the setter
# 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 = text
else:
self._text = None # save a None value since text string is not saved
# limit padding to >= 0
padding_top = max(0, padding_top)
padding_bottom = max(0, padding_bottom)
padding_left = max(0, padding_left)
padding_right = max(0, padding_right)
# Check for empty string
if (text == "") or (
text is None
): # If empty string, just create a zero-sized bounding box and that's it.
# Calculate the text bounding box
# Calculate tight box to provide bounding box dimensions to match label for
# anchor_position calculations
(
tight_box_x,
tight_box_y,
tight_x_offset,
tight_y_offset,
) = self._text_bounding_box(
text, font, self._line_spacing, background_tight=True,
)
if background_tight:
box_x = tight_box_x
box_y = tight_box_y
y_offset = tight_y_offset
x_offset = tight_x_offset
else:
(box_x, box_y, x_offset, y_offset) = self._text_bounding_box(
text, font, self._line_spacing, background_tight=background_tight,
self._bounding_box = (
0,
0,
0, # zero width with text == ""
0, # zero height with text == ""
)
# Calculate the background size including padding
box_x = box_x + padding_left + padding_right
box_y = box_y + padding_top + padding_bottom
# 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)
# Create the two-color palette
self.palette = displayio.Palette(2)
else: # The text string is not empty, so create the Bitmap and TileGrid and
# append to the self Group
self.background_color = background_color
self.color = color
# Calculate the text bounding box
# Create the bitmap and TileGrid
self.bitmap = displayio.Bitmap(box_x, box_y, len(self.palette))
# 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, self._line_spacing,
) # calculate the box size for a tight and loose backgrounds
# Place the text into the Bitmap
self._place_text(
self.bitmap,
text,
font,
self._line_spacing,
padding_left - x_offset,
padding_top + y_offset,
)
if self._background_tight:
box_y = tight_box_y
y_offset = tight_y_offset
label_position_yoffset = int( # To calibrate with label.py positioning
(font.get_glyph(ord("M")).height) / 2
)
else: # calculate the box size for a loose background
box_y = loose_box_y
y_offset = loose_y_offset
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=-padding_left + x_offset,
y=label_position_yoffset - y_offset - padding_top,
)
# Calculate the background size including padding
box_x = box_x + self._padding_left + self._padding_right
box_y = box_y + self._padding_top + self._padding_bottom
# instance the Group
# this Group will contain just one TileGrid with one contained bitmap
super().__init__(
max_size=1, x=x, y=y, **kwargs
) # this will include any arguments, including scale
self.append(self.tilegrid) # add the bitmap's tilegrid to the group
# Create the bitmap and TileGrid
self.bitmap = displayio.Bitmap(box_x, box_y, len(self.palette))
# Update bounding_box values. Note: To be consistent with label.py,
# this is the bounding box for the text only, not including the background.
# Place the text into the Bitmap
self._place_text(
self.bitmap,
text,
self._font,
self._line_spacing,
self._padding_left - x_offset,
self._padding_top + y_offset,
)
self._bounding_box = (
self.tilegrid.x,
self.tilegrid.y,
tight_box_x,
tight_box_y,
)
label_position_yoffset = int( # To calibrate with label.py positioning
(self._font.get_glyph(ord("M")).height) / 2
)
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
# Update bounding_box values. Note: To be consistent with label.py,
# this is the bounding box for the text only, not including the background.
self._bounding_box = (
self.tilegrid.x,
self.tilegrid.y,
box_x,
tight_box_y,
)
self._anchored_position = anchored_position
self.anchor_point = anchor_point
self.anchored_position = (
self._anchored_position
) # sets anchored_position with setter after bitmap is created
) # set the anchored_position with setter after bitmap is created, sets the
# x,y positions of the label
@staticmethod
def _line_spacing_ypixels(font, line_spacing):
# Note: Scale is not implemented at this time, any scaling is pushed up to the Group level
# 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, font, line_spacing, background_tight=False):
def _text_bounding_box(self, text, font, line_spacing):
# This empirical approach checks several glyphs for maximum ascender and descender height
# (consistent with label.py)
@ -240,7 +328,7 @@ class Label(displayio.Group):
# descender, will depend upon font used
try:
self._font.load_glyphs(text + glyphs)
font.load_glyphs(text + glyphs)
except AttributeError:
# ignore if font does not have load_glyphs
pass
@ -263,8 +351,6 @@ class Label(displayio.Group):
top = bottom = y_start
y_offset_tight = int((font.get_glyph(ord("M")).height) / 2)
# this needs to be reviewed (also in label.py), since it doesn't respond
# properly to the number of newlines.
newline = False
@ -305,17 +391,25 @@ class Label(displayio.Group):
left = 0
final_box_width = right - left
if background_tight:
final_box_height = bottom - top
final_y_offset = -top + y_offset_tight
else:
final_box_height = (lines - 1) * self._line_spacing_ypixels(
font, line_spacing
) + (ascender_max + descender_max)
final_y_offset = ascender_max
final_box_height_tight = bottom - top
final_y_offset_tight = -top + y_offset_tight
return (final_box_width, final_box_height, left, final_y_offset)
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-nested-blocks
def _place_text(
@ -328,20 +422,13 @@ class Label(displayio.Group):
yposition,
text_palette_index=1,
background_palette_index=0,
print_only_pixels=True, # print_only_pixels = True: only update the bitmap where the glyph
# pixel color is > 0. This is especially useful for script fonts where glyph
# bounding boxes overlap
# Set `print_only_pixels=False` to write all pixels
skip_index=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 rectangulary glyph boxes overlap)
):
# placeText - Writes text into a bitmap at the specified location.
#
# Verify paletteIndex is working properly with * operator, especially
# if accommodating multicolored fonts
#
# Note: Scale is not implemented at this time, is pushed up to Group level
bitmap_width = bitmap.width
bitmap_height = bitmap.height
# Note: scale is pushed up to Group level
x_start = xposition # starting x position (left margin)
y_start = yposition
@ -385,47 +472,111 @@ class Label(displayio.Group):
) # for type BuiltinFont, this creates the x-offset in the glyph bitmap.
# for BDF loaded fonts, this should equal 0
for y in range(my_glyph.height):
for x in range(my_glyph.width):
x_placement = x + xposition + my_glyph.dx
y_placement = y + yposition - my_glyph.height - my_glyph.dy
if (bitmap_width > x_placement >= 0) and (
bitmap_height > y_placement >= 0
):
# Allows for remapping the bitmap indexes using paletteIndex
# for background and text.
palette_indexes = (
background_palette_index,
text_palette_index,
)
this_pixel_color = palette_indexes[
my_glyph.bitmap[
y * my_glyph.bitmap.width + x + glyph_offset_x
]
]
if not print_only_pixels or this_pixel_color > 0:
# write all characters if printOnlyPixels = False,
# or if thisPixelColor is > 0
bitmap[
y_placement * bitmap_width + x_placement
] = this_pixel_color
elif y_placement > bitmap_height:
break
self._blit(
bitmap,
xposition + my_glyph.dx,
yposition - my_glyph.height - my_glyph.dy,
my_glyph.bitmap,
x_1=glyph_offset_x,
y_1=0,
x_2=glyph_offset_x + my_glyph.width,
y_2=0 + my_glyph.height,
skip_index=skip_index, # do not copy over any 0 background pixels
)
xposition = xposition + my_glyph.shift_x
return (left, top, right - left, bottom - top) # bounding_box
def _blit(
self,
bitmap, # target bitmap
x, # target x upper left corner
y, # target y upper left corner
source_bitmap, # source bitmap
x_1=0, # source x start
y_1=0, # source y start
x_2=None, # source x end
y_2=None, # source y end
skip_index=None, # palette index that will not be copied
# (for example: the background color of a glyph)
):
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,
)
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
@property
def bounding_box(self):
"""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 self._bounding_box
@property
def scale(self):
"""Set the scaling of the label, in integer values"""
return self._scale
@scale.setter
def scale(self, new_scale):
self.local_group.scale = new_scale
self._scale = new_scale
self.anchored_position = self._anchored_position # update the anchored_position
@property
def line_spacing(self):
"""The amount of space between lines of text, in multiples of the font's
@ -434,9 +585,10 @@ class Label(displayio.Group):
@line_spacing.setter
def line_spacing(self, new_line_spacing):
raise RuntimeError(
"line_spacing is immutable for bitmap_label.py; use label.py for mutable line_spacing"
)
if self._save_text:
self._reset_text(line_spacing=new_line_spacing)
else:
raise RuntimeError("line_spacing is immutable when save_text is False")
@property
def color(self):
@ -473,22 +625,22 @@ class Label(displayio.Group):
"""Text to displayed."""
return self._text
@text.setter
@text.setter # Cannot set color or background color with text setter, use separate setter
def text(self, new_text):
raise RuntimeError(
"text is immutable for bitmap_label.py; use label.py library for mutable text"
)
self._reset_text(text=new_text)
@property
def font(self):
"""Font to use for text display."""
return self.font
return self._font
@font.setter
def font(self, new_font):
raise RuntimeError(
"font is immutable for bitmap_label.py; use label.py library for mutable font"
)
self._font = new_font
if self._save_text:
self._reset_text(font=new_font)
else:
raise RuntimeError("font is immutable when save_text is False")
@property
def anchor_point(self):
@ -513,16 +665,15 @@ class Label(displayio.Group):
@anchored_position.setter
def anchored_position(self, new_position):
self._anchored_position = new_position
# Set anchored_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))
- (self._bounding_box[0] * self.scale)
- round(self._anchor_point[0] * (self._bounding_box[2] * self.scale))
)
self.y = int(
new_position[1]
- (self._bounding_box[1] * self._scale)
- round(self._anchor_point[1] * self._bounding_box[3] * self._scale)
- (self._bounding_box[1] * self.scale)
- round(self._anchor_point[1] * self._bounding_box[3] * self.scale)
)

View file

@ -79,18 +79,25 @@ class Label(displayio.Group):
padding_right=0,
anchor_point=None,
anchored_position=None,
scale=1,
**kwargs
):
if "scale" in kwargs.keys():
self._scale = kwargs["scale"]
else:
self._scale = 1
if not max_glyphs and not text:
raise RuntimeError("Please provide a max size, or initial text")
if not max_glyphs:
max_glyphs = len(text)
# add one to max_size for the background bitmap tileGrid
super().__init__(max_size=max_glyphs + 1, **kwargs)
# instance the Group
# self Group will contain a single local_group which contains a Group (self.local_group)
# which contains a TileGrid
super().__init__(
max_size=1, scale=1, **kwargs
) # The self scale should always be 1
self.local_group = displayio.Group(
max_size=max_glyphs + 1, scale=scale
) # local_group will set the scale
self.append(self.local_group)
self.width = max_glyphs
self._font = font
@ -122,6 +129,8 @@ class Label(displayio.Group):
self._padding_left = padding_left
self._padding_right = padding_right
self._scale = scale
if text is not None:
self._update_text(str(text))
if (anchored_position is not None) and (anchor_point is not None):
@ -138,28 +147,17 @@ class Label(displayio.Group):
y_box_offset = self._boundingbox[1]
else: # draw a "loose" bounding box to include any ascenders/descenders.
# check a few glyphs for maximum ascender and descender height
# Enhancement: it would be preferred to access the font "FONT_ASCENT" and
# "FONT_DESCENT" in the imported BDF file
glyphs = "M j'" # choose glyphs with highest ascender and lowest
# 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)
ascent, descent = self._get_ascent_descent()
box_width = self._boundingbox[2] + self._padding_left + self._padding_right
x_box_offset = -self._padding_left
box_height = (
(ascender_max + descender_max)
(ascent + descent)
+ int((lines - 1) * self.height * self._line_spacing)
+ self._padding_top
+ self._padding_bottom
)
y_box_offset = -ascender_max + y_offset - self._padding_top
y_box_offset = -ascent + y_offset - self._padding_top
box_width = max(0, box_width) # remove any negative values
box_height = max(0, box_height) # remove any negative values
@ -174,12 +172,35 @@ class Label(displayio.Group):
return tile_grid
def _get_ascent_descent(self):
if hasattr(self.font, "ascent"):
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
def _get_ascent(self):
return self._get_ascent_descent()[0]
def _update_background_color(self, new_color):
if new_color is None:
self._background_palette.make_transparent(0)
if self._added_background_tilegrid:
self.pop(0)
self.local_group.pop(0)
self._added_background_tilegrid = False
else:
self._background_palette.make_opaque(0)
@ -187,7 +208,7 @@ class Label(displayio.Group):
self._background_color = new_color
lines = self._text.rstrip("\n").count("\n") + 1
y_offset = int((self._font.get_glyph(ord("M")).height) / 2)
y_offset = self._get_ascent() // 2
if not self._added_background_tilegrid: # no bitmap is in the self Group
# add bitmap if text is present and bitmap sizes > 0 pixels
@ -200,10 +221,16 @@ class Label(displayio.Group):
self._boundingbox[3] + self._padding_top + self._padding_bottom > 0
)
):
if len(self) > 0:
self.insert(0, self._create_background_box(lines, y_offset))
if (
len(self.local_group) > 0
): # This can be simplified in CP v6.0, when group.append(0) bug is corrected
self.local_group.insert(
0, self._create_background_box(lines, y_offset)
)
else:
self.append(self._create_background_box(lines, y_offset))
self.local_group.append(
self._create_background_box(lines, y_offset)
)
self._added_background_tilegrid = True
else: # a bitmap is present in the self Group
@ -217,9 +244,9 @@ class Label(displayio.Group):
self._boundingbox[3] + self._padding_top + self._padding_bottom > 0
)
):
self[0] = self._create_background_box(lines, y_offset)
self.local_group[0] = self._create_background_box(lines, y_offset)
else: # delete the existing bitmap
self.pop(0)
self.local_group.pop(0)
self._added_background_tilegrid = False
def _update_text(
@ -233,13 +260,7 @@ class Label(displayio.Group):
i = 0
tilegrid_count = i
try:
self._font.load_glyphs(new_text + "M")
except AttributeError:
# ignore if font does not have load_glyphs
pass
y_offset = int((self._font.get_glyph(ord("M")).height) / 2)
y_offset = self._get_ascent() // 2
right = top = bottom = 0
left = None
@ -284,10 +305,10 @@ class Label(displayio.Group):
x=position_x,
y=position_y,
)
if tilegrid_count < len(self):
self[tilegrid_count] = face
if tilegrid_count < len(self.local_group):
self.local_group[tilegrid_count] = face
else:
self.append(face)
self.local_group.append(face)
tilegrid_count += 1
x += glyph.shift_x
i += 1
@ -296,8 +317,8 @@ class Label(displayio.Group):
if left is None:
left = 0
while len(self) > tilegrid_count: # i:
self.pop()
while len(self.local_group) > tilegrid_count: # i:
self.local_group.pop()
self._text = new_text
self._boundingbox = (left, top, right - left, bottom - top)
@ -319,6 +340,7 @@ class Label(displayio.Group):
@line_spacing.setter
def line_spacing(self, spacing):
self._line_spacing = spacing
self.text = self._text # redraw the box
@property
def color(self):
@ -358,6 +380,18 @@ class Label(displayio.Group):
except RuntimeError as run_error:
raise RuntimeError("Text length exceeds max_glyphs") from run_error
@property
def scale(self):
"""Set the scaling of the label, in integer values"""
return self._scale
@scale.setter
def scale(self, new_scale):
current_anchored_position = self.anchored_position
self._scale = new_scale
self.local_group.scale = new_scale
self.anchored_position = current_anchored_position
@property
def font(self):
"""Font to use for text display."""

View file

@ -6,3 +6,6 @@
.. automodule:: adafruit_display_text.label
:members:
.. automodule:: adafruit_display_text.bitmap_label
:members:

View file

@ -0,0 +1,68 @@
"""
Basic display_text.label example script
adapted for use on MagTag.
"""
import time
import board
import displayio
import terminalio
from adafruit_display_text import label
# use built in display (PyPortal, PyGamer, PyBadge, CLUE, etc.)
# see guide for setting up external displays (TFT / OLED breakouts, RGB matrices, etc.)
# https://learn.adafruit.com/circuitpython-display-support-using-displayio/display-and-display-bus
display = board.DISPLAY
# wait until we can draw
time.sleep(display.time_to_refresh)
# main group to hold everything
main_group = displayio.Group()
# white background. Scaled to save RAM
bg_bitmap = displayio.Bitmap(display.width // 8, display.height // 8, 1)
bg_palette = displayio.Palette(1)
bg_palette[0] = 0xFFFFFF
bg_sprite = displayio.TileGrid(bg_bitmap, x=0, y=0, pixel_shader=bg_palette)
bg_group = displayio.Group(scale=8)
bg_group.append(bg_sprite)
main_group.append(bg_group)
# first example label
TEXT = "Hello world"
text_area = label.Label(
terminalio.FONT,
text=TEXT,
color=0xFFFFFF,
background_color=0x666666,
padding_top=1,
padding_bottom=3,
padding_right=4,
padding_left=4,
)
text_area.x = 10
text_area.y = 14
main_group.append(text_area)
# second example label
another_text = label.Label(
terminalio.FONT,
scale=2,
text="MagTag display_text\nexample",
color=0x000000,
background_color=0x999999,
padding_top=1,
padding_bottom=3,
padding_right=4,
padding_left=4,
)
# centered
another_text.anchor_point = (0.5, 0.5)
another_text.anchored_position = (display.width // 2, display.height // 2)
main_group.append(another_text)
# show the main group and refresh.
display.show(main_group)
display.refresh()
while True:
pass

View file

@ -0,0 +1,23 @@
"""
This example shows how to create a display_text label and show it
with a Matrix Portal
Requires:
adafruit_matrixportal - https://github.com/adafruit/Adafruit_CircuitPython_MatrixPortal
Copy it from the current libraries bundle into the lib folder on your device.
"""
import terminalio
from adafruit_matrixportal.matrix import Matrix
from adafruit_display_text import label
matrix = Matrix()
display = matrix.display
text = "Hello\nworld"
text_area = label.Label(terminalio.FONT, text=text)
text_area.x = 1
text_area.y = 4
display.show(text_area)
while True:
pass

View file

@ -0,0 +1,26 @@
"""
This example illustrates how to use the wrap_text_to_lines
helper function.
"""
import board
import terminalio
from adafruit_display_text import label, wrap_text_to_lines
# use built in display (PyPortal, PyGamer, PyBadge, CLUE, etc.)
# see guide for setting up external displays (TFT / OLED breakouts, RGB matrices, etc.)
# https://learn.adafruit.com/circuitpython-display-support-using-displayio/display-and-display-bus
display = board.DISPLAY
text = (
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, "
"sed do eiusmod tempor incididunt ut labore et dolore magna "
"aliqua. Ut enim ad minim veniam, quis nostrud exercitation "
"ullamco laboris nisi ut aliquip ex ea commodo consequat."
)
text = "\n".join(wrap_text_to_lines(text, 28))
text_area = label.Label(terminalio.FONT, text=text)
text_area.x = 10
text_area.y = 10
display.show(text_area)
while True:
pass