diff --git a/adafruit_display_text/__init__.py b/adafruit_display_text/__init__.py index 2a63aee..5d3edb0 100644 --- a/adafruit_display_text/__init__.py +++ b/adafruit_display_text/__init__.py @@ -183,7 +183,10 @@ class LabelBase(Group): 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""" + 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``""" # pylint: disable=unused-argument, too-many-instance-attributes, too-many-locals, too-many-arguments def __init__( @@ -207,6 +210,7 @@ def __init__( scale: int = 1, base_alignment: bool = False, tab_replacement: Tuple[int, str] = (4, " "), + label_direction: str = "LTR", **kwargs, ) -> None: super().__init__(max_size=1, x=x, y=y, scale=1) @@ -226,6 +230,11 @@ def __init__( self.local_group = None self._text = text + + if label_direction not in ["LTR", "RTL", "UPR", "DWR", "TTB"]: + raise RuntimeError("Please provide a valid text direction") + self._label_direction = label_direction + self.baseline = -1.0 self.base_alignment = base_alignment @@ -384,3 +393,19 @@ def _set_line_spacing(self, new_line_spacing: float) -> None: @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: + # subclass should override this. + pass + + @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 ["LTR", "RTL", "UPR", "DWR", "TTB"]: + raise RuntimeError("Please provide a valid text direction") + self._set_label_direction(new_label_direction) diff --git a/adafruit_display_text/label.py b/adafruit_display_text/label.py index a68ca48..4e7373a 100755 --- a/adafruit_display_text/label.py +++ b/adafruit_display_text/label.py @@ -66,7 +66,10 @@ class Label(LabelBase): 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""" + 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``""" # pylint: disable=too-many-instance-attributes, too-many-locals # This has a lot of getters/setters, maybe it needs cleanup. @@ -122,6 +125,7 @@ def __init__(self, font, **kwargs) -> None: self._padding_left = kwargs.get("padding_left", 0) self._padding_right = kwargs.get("padding_right", 0) self.base_alignment = kwargs.get("base_alignment", False) + self._label_direction = kwargs.get("label_direction", "LTR") if text is not None: self._update_text(str(text)) @@ -136,7 +140,6 @@ def _create_background_box(self, lines: int, y_offset: int) -> None: :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] @@ -146,14 +149,33 @@ def _create_background_box(self, lines: int, y_offset: int) -> None: else: # draw a "loose" bounding box to include any ascenders/descenders. ascent, descent = self._get_ascent_descent() - 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 == "UPR" + or self._label_direction == "DWR" + or self._label_direction == "TTB" + ): + box_height = ( + self._bounding_box[3] + self._padding_top + self._padding_bottom + ) + x_box_offset = -self._padding_bottom + box_width = ( + (ascent + descent) + + int((lines - 1) * self.width * self._line_spacing) + + self._padding_left + + self._padding_right + ) + 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.base_alignment: y_box_offset = -ascent - self._padding_top else: @@ -162,12 +184,25 @@ def _create_background_box(self, lines: int, y_offset: int) -> None: 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 = left + x_box_offset + movy = -box_height - x_box_offset + elif self._label_direction == "DWR": + movx = left + x_box_offset + movy = x_box_offset + elif self._label_direction == "TTB": + movx = left + x_box_offset + movy = x_box_offset + else: + movx = left + x_box_offset + movy = y_box_offset + background_bitmap = displayio.Bitmap(box_width, box_height, 1) tile_grid = displayio.TileGrid( background_bitmap, pixel_shader=self._background_palette, - x=left + x_box_offset, - y=y_box_offset, + x=movx, + y=movy, ) return tile_grid @@ -222,7 +257,7 @@ def _update_background_color(self, new_color: int) -> None: self._bounding_box[3] + self._padding_top + self._padding_bottom > 0 ) ): - self.local_group[0] = self._create_background_box(lines, y_offset) + 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 @@ -243,8 +278,15 @@ def _update_text( else: self._y_offset = self._get_ascent() // 2 - right = top = bottom = 0 - left = None + 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": @@ -254,17 +296,74 @@ def _update_text( glyph = self._font.get_glyph(ord(character)) if not glyph: continue - right = max(right, x + glyph.shift_x, x + glyph.width + glyph.dx) - if x == 0: - if left is None: - left = glyph.dx + + if self._label_direction == "LTR" or self._label_direction == "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 = glyph.dx + else: + left = min(left, glyph.dx) + position_x = x + glyph.dx else: - left = min(left, glyph.dx) - if y == 0: # first line, find the Ascender height - top = min(top, -glyph.height - glyph.dy + self._y_offset) - bottom = max(bottom, y - glyph.dy + self._y_offset) - position_y = y - glyph.height - glyph.dy + self._y_offset - position_x = x + glyph.dx + left = max( + left, abs(x) + glyph.shift_x, abs(x) + glyph.width + glyph.dx + ) + if x == 0: + if right is None: + right = glyph.dx + else: + right = max(right, glyph.dx) + position_x = x - glyph.width + + if self._label_direction == "TTB": + if x == 0: + if left is None: + left = glyph.dx + 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 + + if 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 + + if 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: try: # pylint: disable=unexpected-keyword-arg @@ -286,22 +385,58 @@ def _update_text( 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 - x += glyph.shift_x + + 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 - # Remove the rest - if left is None: + 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() + # pylint: disable=invalid-unary-operand-type + if self._label_direction == "RTL": + 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 - self._bounding_box = (left, top, right - left, bottom - top) if self.background_color is not None: self._update_background_color(self._background_color) @@ -331,5 +466,10 @@ def _set_line_spacing(self, new_line_spacing: float) -> None: def _set_text(self, new_text: str, scale: int) -> None: self._reset_text(new_text) - def _set_background_color(self, new_color): + def _set_background_color(self, new_color: int) -> None: self._update_background_color(new_color) + + def _set_label_direction(self, new_label_direction: str) -> None: + self._label_direction = new_label_direction + old_text = self._text + self._update_text(str(old_text))