|
| 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 |
0 commit comments