11# SPDX-FileCopyrightText: 2020 Kevin Matocha
2+ # SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
23#
34# SPDX-License-Identifier: MIT
45
2728__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git"
2829
2930import displayio
31+ from micropython import const
3032
3133from adafruit_display_text import LabelBase
3234
4345except ImportError :
4446 pass
4547
48+ # constant indexes for accent_ranges
49+ ACCENT_START = const (0 )
50+ ACCENT_END = const (1 )
51+ ACCENT_FG = const (2 )
52+ ACCENT_BG = const (3 )
53+
4654
4755class Label (LabelBase ):
4856 """A label displaying a string of text that is stored in a bitmap.
@@ -101,13 +109,34 @@ class Label(LabelBase):
101109 "RTL" : (False , False , False ),
102110 }
103111
104- def __init__ (self , font : FontProtocol , save_text : bool = True , ** kwargs ) -> None :
112+ def __init__ (
113+ self ,
114+ font : FontProtocol ,
115+ save_text : bool = True ,
116+ color_palette : Optional [displayio .Palette ] = None ,
117+ ** kwargs ,
118+ ) -> None :
105119 self ._bitmap = None
106120 self ._tilegrid = None
107121 self ._prev_label_direction = None
108122
109123 super ().__init__ (font , ** kwargs )
110124
125+ if color_palette is not None :
126+ if len (color_palette ) <= 2 :
127+ raise ValueError (
128+ "color_palette should be at least 3 colors to "
129+ "provide enough for normal and accented text. "
130+ "color_palette argument can be omitted if not "
131+ "using accents."
132+ )
133+ self ._palette = color_palette
134+ self .color = self ._color
135+ self .background_color = self ._background_color
136+
137+ self ._accent_ranges = []
138+ self ._tmp_glyph_bitmap = None
139+
111140 self ._save_text = save_text
112141 self ._text = self ._replace_tabs (self ._text )
113142
@@ -381,7 +410,8 @@ def _place_text(
381410 top = bottom = y_start
382411 line_spacing = self ._line_spacing
383412
384- for char in text :
413+ for char_idx in range (len (text )):
414+ char = text [char_idx ]
385415 if char == "\n " : # newline
386416 xposition = x_start # reset to left column
387417 yposition = yposition + self ._line_spacing_ypixels (
@@ -390,6 +420,10 @@ def _place_text(
390420
391421 else :
392422 my_glyph = font .get_glyph (ord (char ))
423+ if self ._tmp_glyph_bitmap is None and len (self ._accent_ranges ) > 0 :
424+ self ._tmp_glyph_bitmap = displayio .Bitmap (
425+ my_glyph .bitmap .width , my_glyph .bitmap .height , len (self ._palette )
426+ )
393427
394428 if my_glyph is None : # Error checking: no glyph found
395429 print (f"Glyph not found: { repr (char )} " )
@@ -429,16 +463,37 @@ def _place_text(
429463 if self ._verbose :
430464 print (f'Warning: Glyph clipped, exceeds descent property: "{ char } "' )
431465
466+ accented = False
467+ if len (self ._accent_ranges ) > 0 :
468+ for accent_range in self ._accent_ranges :
469+ if accent_range [ACCENT_START ] <= char_idx < accent_range [ACCENT_END ]:
470+ self ._tmp_glyph_bitmap .fill (accent_range [ACCENT_BG ])
471+
472+ bitmaptools .blit (
473+ self ._tmp_glyph_bitmap ,
474+ my_glyph .bitmap ,
475+ 0 ,
476+ 0 ,
477+ skip_source_index = 0 ,
478+ )
479+ bitmaptools .replace_color (
480+ self ._tmp_glyph_bitmap , 1 , accent_range [ACCENT_FG ]
481+ )
482+ accented = True
483+ break
484+
432485 self ._blit (
433486 bitmap ,
434487 max (xposition + my_glyph .dx , 0 ),
435488 y_blit_target ,
436- my_glyph .bitmap ,
489+ my_glyph .bitmap if not accented else self . _tmp_glyph_bitmap ,
437490 x_1 = glyph_offset_x ,
438491 y_1 = y_clip ,
439492 x_2 = glyph_offset_x + my_glyph .width ,
440493 y_2 = my_glyph .height ,
441- skip_index = skip_index , # do not copy over any 0 background pixels
494+ skip_index = skip_index
495+ if not accented
496+ else None , # do not copy over any 0 background pixels if not accented
442497 )
443498
444499 xposition = xposition + my_glyph .shift_x
@@ -571,3 +626,88 @@ def bitmap(self) -> displayio.Bitmap:
571626 :rtype: displayio.Bitmap
572627 """
573628 return self ._bitmap
629+
630+ def add_accent_range (self , start , end , foreground_color , background_color ):
631+ """
632+ Set a range of text to get accented with the specified colors.
633+
634+ :param start: The start index of the range of text to accent, inclusive.
635+ :param end: The end index of the range of text to accent, exclusive.
636+ :param foreground_color: The color index within ``color_palette`` to use for
637+ the accent foreground color.
638+ :param background_color: The color index within ``color_palette`` to use for
639+ the accent background color.
640+ :return: None
641+ """
642+ self ._accent_ranges .append ((start , end , foreground_color , background_color ))
643+ self ._reset_text (text = str (self ._text ))
644+
645+ def remove_accent_range (self , start ):
646+ """
647+ Remove the accent that starts at the specified index within the text.
648+
649+ :param start: The start index of the range of accented text, inclusive.
650+ :return: None
651+ """
652+ for accent_range in reversed (self ._accent_ranges ):
653+ if accent_range [0 ] == start :
654+ self ._accent_ranges .remove (accent_range )
655+ self ._reset_text (text = str (self ._text ))
656+
657+ def add_accent_to_substring (self , substring , foreground_color , background_color , start = 0 ):
658+ """
659+ Add accent to the first occurrence of ``substring`` found in the labels text,
660+ starting from ``start``.
661+
662+ :param substring: the substring to accent within the text.
663+ :param foreground_color: The color index within ``color_palette`` to use for
664+ the accent foreground color.
665+ :param background_color: The color index within ``color_palette`` to use for
666+ the accent background color.
667+ :param start: The index within text to start searching for the substring.
668+ Defaults is 0 to search the whole text.
669+ :return: True if the substring was found, False otherwise.
670+ """
671+
672+ index = self ._text .find (substring , start )
673+ if index != - 1 :
674+ self .add_accent_range (index , index + len (substring ), foreground_color , background_color )
675+ return True
676+ else :
677+ return False
678+
679+ def remove_accent_from_substring (self , substring , start = 0 ):
680+ """
681+ Remove the accent for the first instance of the specified ``substring``
682+ starting at ``start``.
683+
684+ :param substring: the substring to accent within the text.
685+ :param start: The index within text to start searching for the substring.
686+ Defaults is 0 to search the whole text.
687+ :return: True if the substring was found, False otherwise.
688+ """
689+
690+ index = self ._text .find (substring , start )
691+ if index != - 1 :
692+ self .remove_accent_range (index )
693+ return True
694+ else :
695+ return False
696+
697+ @property
698+ def accent_ranges (self ):
699+ """
700+ The list of ranges that are accented.
701+ :return: List of Tuples containing (start, end, foreground_color, background_color).
702+ """
703+ return self ._accent_ranges
704+
705+ def clear_accent_ranges (self ):
706+ """
707+ Remove all accents from the text. All text will return to default
708+ foreground and background colors.
709+
710+ :return: None
711+ """
712+ self ._accent_ranges = []
713+ self ._reset_text (text = str (self ._text ))
0 commit comments