Skip to content

Commit ff6d63b

Browse files
committed
AccentLabel class
1 parent 727a102 commit ff6d63b

File tree

3 files changed

+261
-0
lines changed

3 files changed

+261
-0
lines changed
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
2+
# SPDX-License-Identifier: MIT
3+
import bitmaptools
4+
from displayio import Bitmap, Palette
5+
from micropython import const
6+
7+
from adafruit_display_text.bitmap_label import Label as BitmapLabel
8+
9+
try:
10+
from typing import Optional, Tuple
11+
12+
from fontio import FontProtocol
13+
except ImportError:
14+
pass
15+
16+
# constant indexes for accent_ranges
17+
ACCENT_START = const(0)
18+
ACCENT_END = const(1)
19+
ACCENT_FG = const(2)
20+
ACCENT_BG = const(3)
21+
22+
23+
class AccentLabel(BitmapLabel):
24+
"""
25+
Subclass of BitmapLabel that allows accenting ranges of text with different
26+
foreground and background colors.
27+
28+
:param font: A font class that has ``get_bounding_box`` and ``get_glyph``.
29+
Must include a capital M for measuring character size.
30+
:type font: ~fontio.FontProtocol
31+
:param displayio.Palette color_palette: The palette to use for the Label.
32+
Indexes 0 and 1 will be filled with background and foreground colors automatically.
33+
Indexes 2 and above can be used for accent colors.
34+
"""
35+
36+
def __init__(
37+
self, font: FontProtocol, color_palette: Palette, save_text: bool = True, **kwargs
38+
) -> None:
39+
super().__init__(font, save_text, **kwargs)
40+
41+
if len(color_palette) <= 2:
42+
raise ValueError(
43+
"color_palette should be at least 3 colors to "
44+
"provide enough for normal and accented text"
45+
)
46+
47+
self._palette = color_palette
48+
self.color = self._color
49+
self.background_color = self._background_color
50+
51+
self._accent_ranges = []
52+
53+
self._tmp_glyph_bitmap = None
54+
55+
def add_accent_range(self, start, end, foreground_color, background_color):
56+
"""
57+
Set a range of text to get accented with the specified colors.
58+
59+
:param start: The start index of the range of text to accent, inclusive.
60+
:param end: The end index of the range of text to accent, exclusive.
61+
:param foreground_color: The color index within ``color_palette`` to use for
62+
the accent foreground color.
63+
:param background_color: The color index within ``color_palette`` to use for
64+
the accent background color.
65+
:return: None
66+
"""
67+
self._accent_ranges.append((start, end, foreground_color, background_color))
68+
self._reset_text(text=str(self._text))
69+
70+
def remove_accent_range(self, start, end, foreground_color, background_color):
71+
"""
72+
Remove the accent for the specified range and colors.
73+
74+
:param start: The start index of the range of accented text, inclusive.
75+
:param end: The end index of the range of accented text, exclusive.
76+
:param foreground_color: The color index within ``color_palette`` used for
77+
the accent foreground color.
78+
:param background_color: The color index within ``color_palette`` used for
79+
the accent background color.
80+
:return: None
81+
"""
82+
self._accent_ranges.remove((start, end, foreground_color, background_color))
83+
self._reset_text(text=str(self._text))
84+
85+
def clear_accent_ranges(self):
86+
"""
87+
Remove all accents from the text. All text will return to default
88+
foreground and background colors.
89+
90+
:return: None
91+
"""
92+
self._accent_ranges = []
93+
self._reset_text(text=str(self._text))
94+
95+
def _place_text(
96+
self,
97+
bitmap: Bitmap,
98+
text: str,
99+
font: FontProtocol,
100+
xposition: int,
101+
yposition: int,
102+
skip_index: int = 0, # set to None to write all pixels, other wise skip this palette index
103+
# when copying glyph bitmaps (this is important for slanted text
104+
# where rectangular glyph boxes overlap)
105+
) -> Tuple[int, int, int, int]:
106+
"""
107+
Overridden from parent class BitmapLabel with accent color
108+
functionality added.
109+
"""
110+
# placeText - Writes text into a bitmap at the specified location.
111+
#
112+
# Note: scale is pushed up to Group level
113+
114+
x_start = xposition # starting x position (left margin)
115+
y_start = yposition
116+
117+
left = None
118+
right = x_start
119+
top = bottom = y_start
120+
line_spacing = self._line_spacing
121+
122+
for char_idx in range(len(text)):
123+
char = text[char_idx]
124+
if char == "\n": # newline
125+
xposition = x_start # reset to left column
126+
yposition = yposition + self._line_spacing_ypixels(
127+
font, line_spacing
128+
) # Add a newline
129+
130+
else:
131+
my_glyph = font.get_glyph(ord(char))
132+
if self._tmp_glyph_bitmap is None:
133+
self._tmp_glyph_bitmap = Bitmap(
134+
my_glyph.bitmap.width, my_glyph.bitmap.height, len(self._palette)
135+
)
136+
137+
if my_glyph is None: # Error checking: no glyph found
138+
print(f"Glyph not found: {repr(char)}")
139+
else:
140+
if xposition == x_start:
141+
if left is None:
142+
left = 0
143+
else:
144+
left = min(left, my_glyph.dx)
145+
146+
right = max(
147+
right,
148+
xposition + my_glyph.shift_x,
149+
xposition + my_glyph.width + my_glyph.dx,
150+
)
151+
if yposition == y_start: # first line, find the Ascender height
152+
top = min(top, -my_glyph.height - my_glyph.dy)
153+
bottom = max(bottom, yposition - my_glyph.dy)
154+
155+
glyph_offset_x = (
156+
my_glyph.tile_index * my_glyph.width
157+
) # for type BuiltinFont, this creates the x-offset in the glyph bitmap.
158+
# for BDF loaded fonts, this should equal 0
159+
160+
y_blit_target = yposition - my_glyph.height - my_glyph.dy
161+
162+
# Clip glyph y-direction if outside the font ascent/descent metrics.
163+
# Note: bitmap.blit will automatically clip the bottom of the glyph.
164+
y_clip = 0
165+
if y_blit_target < 0:
166+
y_clip = -y_blit_target # clip this amount from top of bitmap
167+
y_blit_target = 0 # draw the clipped bitmap at y=0
168+
if self._verbose:
169+
print(f'Warning: Glyph clipped, exceeds Ascent property: "{char}"')
170+
171+
if (y_blit_target + my_glyph.height) > bitmap.height:
172+
if self._verbose:
173+
print(f'Warning: Glyph clipped, exceeds descent property: "{char}"')
174+
175+
accented = False
176+
if len(self._accent_ranges) > 0:
177+
for accent_range in self._accent_ranges:
178+
if (
179+
char_idx >= accent_range[ACCENT_START]
180+
and char_idx < accent_range[ACCENT_END]
181+
):
182+
self._tmp_glyph_bitmap.fill(accent_range[ACCENT_BG])
183+
184+
bitmaptools.blit(
185+
self._tmp_glyph_bitmap,
186+
my_glyph.bitmap,
187+
0,
188+
0,
189+
skip_source_index=0,
190+
)
191+
bitmaptools.replace_color(
192+
self._tmp_glyph_bitmap, 1, accent_range[ACCENT_FG]
193+
)
194+
accented = True
195+
break
196+
197+
self._blit(
198+
bitmap,
199+
max(xposition + my_glyph.dx, 0),
200+
y_blit_target,
201+
my_glyph.bitmap if not accented else self._tmp_glyph_bitmap,
202+
x_1=glyph_offset_x,
203+
y_1=y_clip,
204+
x_2=glyph_offset_x + my_glyph.width,
205+
y_2=my_glyph.height,
206+
skip_index=skip_index
207+
if not accented
208+
else None, # do not copy over any 0 background pixels
209+
)
210+
211+
xposition = xposition + my_glyph.shift_x
212+
213+
# bounding_box
214+
return left, top, right - left, bottom - top

docs/examples.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,15 @@ Wrap Pixel Test
133133
:caption: examples/display_text_wrap_pixels_test.py
134134
:linenos:
135135

136+
AccentLabel Example
137+
-------------------
138+
139+
Illustrates the AccentLabel feature
140+
141+
.. literalinclude:: ../examples/display_text_accent_label_simpletest.py
142+
:caption: examples/display_text_accent_label_simpletest.py
143+
:linenos:
144+
136145
Library Features Example
137146
------------------------
138147

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
2+
# SPDX-License-Identifier: MIT
3+
"""
4+
Demonstrates how to use the AccentLabel to highlight part of the text
5+
with different foreground and background color.
6+
"""
7+
8+
import time
9+
10+
import displayio
11+
import supervisor
12+
import terminalio
13+
14+
from adafruit_display_text.accent_label import AccentLabel
15+
16+
display = supervisor.runtime.display
17+
18+
main_group = displayio.Group()
19+
20+
accent_palette = displayio.Palette(4)
21+
accent_palette[2] = 0x000000
22+
accent_palette[3] = 0xDDDD00
23+
24+
quote_lbl = AccentLabel(terminalio.FONT, color_palette=accent_palette, text="", color=0xAAAAAA)
25+
quote_lbl.anchor_point = (0, 0)
26+
quote_lbl.anchored_position = (4, 4)
27+
main_group.append(quote_lbl)
28+
display.root_group = main_group
29+
30+
text = "CircuitPython is amazing!"
31+
start_index = text.find("amazing!")
32+
end_index = start_index + len("amazing!")
33+
34+
quote_lbl.text = text
35+
quote_lbl.add_accent_range(start_index, end_index, 2, 3)
36+
37+
while True:
38+
time.sleep(1)

0 commit comments

Comments
 (0)