You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
177 lines
6.9 KiB
177 lines
6.9 KiB
from enum import IntEnum
|
|
from itertools import zip_longest
|
|
|
|
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.lib.wrap_text import wrap_text
|
|
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._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_color = text_color
|
|
self._icon = icon
|
|
self.set_text(text)
|
|
|
|
def set_text(self, text):
|
|
self._text_raw = text
|
|
self._update_text(self._text_raw)
|
|
|
|
def set_text_color(self, color):
|
|
self._text_color = color
|
|
|
|
def _update_layout_rects(self):
|
|
self._update_text(self._text_raw)
|
|
|
|
def _update_text(self, text):
|
|
self._emojis = []
|
|
self._text_size = []
|
|
self._text = wrap_text(self._font, text, self._font_size, self._rect.width - (self._text_padding*2))
|
|
for t in self._text:
|
|
self._emojis.append(find_emoji(t))
|
|
self._text_size.append(measure_text_cached(self._font, t, self._font_size))
|
|
|
|
def _render(self, _):
|
|
text = self._text[0] if self._text else None
|
|
text_size = self._text_size[0] if self._text_size else rl.Vector2(0.0, 0.0)
|
|
text_pos = rl.Vector2(0, (self._rect.y + (self._rect.height - (text_size.y)) // 2))
|
|
|
|
if self._icon:
|
|
icon_y = self._rect.y + (self._rect.height - self._icon.height) / 2
|
|
if text:
|
|
if self._text_alignment == TextAlignment.LEFT:
|
|
icon_x = self._rect.x + self._text_padding
|
|
text_pos.x = self._icon.width + ICON_PADDING
|
|
elif self._text_alignment == TextAlignment.CENTER:
|
|
total_width = self._icon.width + ICON_PADDING + text_size.x
|
|
icon_x = self._rect.x + (self._rect.width - total_width) / 2
|
|
text_pos.x = self._icon.width + ICON_PADDING
|
|
else:
|
|
icon_x = (self._rect.x + self._rect.width - text_size.x - self._text_padding) - 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)
|
|
|
|
for text, text_size, emojis in zip_longest(self._text, self._text_size, self._emojis, fillvalue=[]):
|
|
line_pos = rl.Vector2(text_pos.x, text_pos.y)
|
|
if self._text_alignment == TextAlignment.LEFT:
|
|
line_pos.x += self._rect.x + self._text_padding
|
|
elif self._text_alignment == TextAlignment.CENTER:
|
|
line_pos.x += self._rect.x + (self._rect.width - text_size.x) // 2
|
|
elif self._text_alignment == TextAlignment.RIGHT:
|
|
line_pos.x += self._rect.x + self._rect.width - text_size.x - self._text_padding
|
|
|
|
prev_index = 0
|
|
for start, end, emoji in emojis:
|
|
text_before = text[prev_index:start]
|
|
width_before = measure_text_cached(self._font, text_before, self._font_size)
|
|
rl.draw_text_ex(self._font, text_before, line_pos, self._font_size, 0, self._text_color)
|
|
line_pos.x += width_before.x
|
|
|
|
tex = emoji_tex(emoji)
|
|
rl.draw_texture_ex(tex, line_pos, 0.0, self._font_size / tex.height, self._text_color)
|
|
line_pos.x += self._font_size
|
|
prev_index = end
|
|
rl.draw_text_ex(self._font, text[prev_index:], line_pos, self._font_size, 0, self._text_color)
|
|
text_pos.y += text_size.y or self._font_size
|
|
|