from enum import IntEnum import pyray as rl from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_SIZE, DEFAULT_TEXT_COLOR from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.utils import GuiStyleContext from openpilot.system.ui.lib.emoji import find_emoji, emoji_tex from openpilot.system.ui.widgets import Widget ICON_PADDING = 15 class TextAlignment(IntEnum): LEFT = 0 CENTER = 1 RIGHT = 2 # TODO: This should be a Widget class def gui_label( rect: rl.Rectangle, text: str, font_size: int = DEFAULT_TEXT_SIZE, color: rl.Color = DEFAULT_TEXT_COLOR, font_weight: FontWeight = FontWeight.NORMAL, alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT, alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, elide_right: bool = True ): font = gui_app.font(font_weight) text_size = measure_text_cached(font, text, font_size) display_text = text # Elide text to fit within the rectangle if elide_right and text_size.x > rect.width: ellipsis = "..." left, right = 0, len(text) while left < right: mid = (left + right) // 2 candidate = text[:mid] + ellipsis candidate_size = measure_text_cached(font, candidate, font_size) if candidate_size.x <= rect.width: left = mid + 1 else: right = mid display_text = text[: left - 1] + ellipsis if left > 0 else ellipsis text_size = measure_text_cached(font, display_text, font_size) # Calculate horizontal position based on alignment text_x = rect.x + { rl.GuiTextAlignment.TEXT_ALIGN_LEFT: 0, rl.GuiTextAlignment.TEXT_ALIGN_CENTER: (rect.width - text_size.x) / 2, rl.GuiTextAlignment.TEXT_ALIGN_RIGHT: rect.width - text_size.x, }.get(alignment, 0) # Calculate vertical position based on alignment text_y = rect.y + { rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP: 0, rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE: (rect.height - text_size.y) / 2, rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM: rect.height - text_size.y, }.get(alignment_vertical, 0) # Draw the text in the specified rectangle rl.draw_text_ex(font, display_text, rl.Vector2(text_x, text_y), font_size, 0, color) def gui_text_box( rect: rl.Rectangle, text: str, font_size: int = DEFAULT_TEXT_SIZE, color: rl.Color = DEFAULT_TEXT_COLOR, alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT, alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, font_weight: FontWeight = FontWeight.NORMAL, ): styles = [ (rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_COLOR_NORMAL, rl.color_to_int(color)), (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_SIZE, font_size), (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_LINE_SPACING, font_size), (rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_ALIGNMENT, alignment), (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_ALIGNMENT_VERTICAL, alignment_vertical), (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_WRAP_MODE, rl.GuiTextWrapMode.TEXT_WRAP_WORD) ] if font_weight != FontWeight.NORMAL: rl.gui_set_font(gui_app.font(font_weight)) with GuiStyleContext(styles): rl.gui_label(rect, text) if font_weight != FontWeight.NORMAL: rl.gui_set_font(gui_app.font(FontWeight.NORMAL)) # Non-interactive text area. Can render emojis and an optional specified icon. class Label(Widget): def __init__(self, text: str, font_size: int = DEFAULT_TEXT_SIZE, font_weight: FontWeight = FontWeight.NORMAL, text_alignment: TextAlignment = TextAlignment.CENTER, text_padding: int = 20, text_color: rl.Color = DEFAULT_TEXT_COLOR, icon = None, ): super().__init__() self._text = text self._font_weight = font_weight self._font = gui_app.font(self._font_weight) self._font_size = font_size self._text_alignment = text_alignment self._text_padding = text_padding self._text_size = measure_text_cached(self._font, self._text, self._font_size) self._text_color = text_color self._icon = icon self.emojis = find_emoji(self._text) def set_text(self, text): self._text = text self._text_size = measure_text_cached(self._font, self._text, self._font_size) def set_text_color(self, color): self._text_color = color def _render(self, _): text_pos = rl.Vector2(0, self._rect.y + (self._rect.height - self._text_size.y) // 2) if self._icon: icon_y = self._rect.y + (self._rect.height - self._icon.height) / 2 if self._text: if self._text_alignment == TextAlignment.LEFT: icon_x = self._rect.x + self._text_padding text_pos.x = icon_x + self._icon.width + ICON_PADDING elif self._text_alignment == TextAlignment.CENTER: total_width = self._icon.width + ICON_PADDING + self._text_size.x icon_x = self._rect.x + (self._rect.width - total_width) / 2 text_pos.x = icon_x + self._icon.width + ICON_PADDING else: text_pos.x = self._rect.x + self._rect.width - self._text_size.x - self._text_padding icon_x = text_pos.x - ICON_PADDING - self._icon.width else: icon_x = self._rect.x + (self._rect.width - self._icon.width) / 2 rl.draw_texture_v(self._icon, rl.Vector2(icon_x, icon_y), rl.WHITE) else: if self._text_alignment == TextAlignment.LEFT: text_pos.x = self._rect.x + self._text_padding elif self._text_alignment == TextAlignment.CENTER: text_pos.x = self._rect.x + (self._rect.width - self._text_size.x) // 2 elif self._text_alignment == TextAlignment.RIGHT: text_pos.x = self._rect.x + self._rect.width - self._text_size.x - self._text_padding prev_index = 0 for start, end, emoji in self.emojis: text_before = self._text[prev_index:start] width_before = measure_text_cached(self._font, text_before, self._font_size) rl.draw_text_ex(self._font, text_before, text_pos, self._font_size, 0, self._text_color) text_pos.x += width_before.x tex = emoji_tex(emoji) rl.draw_texture_ex(tex, text_pos, 0.0, self._font_size / tex.height, self._text_color) text_pos.x += self._font_size prev_index = end rl.draw_text_ex(self._font, self._text[prev_index:], text_pos, self._font_size, 0, self._text_color)