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.
		
		
		
		
		
			
		
			
				
					
					
						
							182 lines
						
					
					
						
							7.4 KiB
						
					
					
				
			
		
		
	
	
							182 lines
						
					
					
						
							7.4 KiB
						
					
					
				from itertools import zip_longest
 | 
						|
from typing import Union
 | 
						|
import pyray as rl
 | 
						|
 | 
						|
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_SIZE, DEFAULT_TEXT_COLOR, FONT_SCALE
 | 
						|
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
 | 
						|
 | 
						|
 | 
						|
# 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, round(font_size * FONT_SCALE)),
 | 
						|
    (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_LINE_SPACING, round(font_size * FONT_SCALE)),
 | 
						|
    (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: int = rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
 | 
						|
               text_alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
 | 
						|
               text_padding: int = 0,
 | 
						|
               text_color: rl.Color = DEFAULT_TEXT_COLOR,
 | 
						|
               icon: Union[rl.Texture, None] = None,  # noqa: UP007
 | 
						|
               ):
 | 
						|
 | 
						|
    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_alignment_vertical = text_alignment_vertical
 | 
						|
    self._text_padding = text_padding
 | 
						|
    self._text_color = text_color
 | 
						|
    self._icon = icon
 | 
						|
 | 
						|
    self._text = text
 | 
						|
    self.set_text(text)
 | 
						|
 | 
						|
  def set_text(self, text):
 | 
						|
    self._text = text
 | 
						|
    self._update_text(self._text)
 | 
						|
 | 
						|
  def set_text_color(self, color):
 | 
						|
    self._text_color = color
 | 
						|
 | 
						|
  def set_font_size(self, size):
 | 
						|
    self._font_size = size
 | 
						|
    self._update_text(self._text)
 | 
						|
 | 
						|
  def _update_layout_rects(self):
 | 
						|
    self._update_text(self._text)
 | 
						|
 | 
						|
  def _update_text(self, text):
 | 
						|
    self._emojis = []
 | 
						|
    self._text_size = []
 | 
						|
    self._text_wrapped = wrap_text(self._font, text, self._font_size, round(self._rect.width - (self._text_padding * 2)))
 | 
						|
    for t in self._text_wrapped:
 | 
						|
      self._emojis.append(find_emoji(t))
 | 
						|
      self._text_size.append(measure_text_cached(self._font, t, self._font_size))
 | 
						|
 | 
						|
  def _render(self, _):
 | 
						|
    text_size = self._text_size[0] if self._text_size else rl.Vector2(0.0, 0.0)
 | 
						|
    if self._text_alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE:
 | 
						|
      text_pos = rl.Vector2(self._rect.x, (self._rect.y + (self._rect.height - text_size.y) // 2))
 | 
						|
    else:
 | 
						|
      text_pos = rl.Vector2(self._rect.x, self._rect.y)
 | 
						|
 | 
						|
    if self._icon:
 | 
						|
      icon_y = self._rect.y + (self._rect.height - self._icon.height) / 2
 | 
						|
      if len(self._text_wrapped) > 0:
 | 
						|
        if self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_LEFT:
 | 
						|
          icon_x = self._rect.x + self._text_padding
 | 
						|
          text_pos.x = self._icon.width + ICON_PADDING
 | 
						|
        elif self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_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_wrapped, self._text_size, self._emojis, fillvalue=[]):
 | 
						|
      line_pos = rl.Vector2(text_pos.x, text_pos.y)
 | 
						|
      if self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_LEFT:
 | 
						|
        line_pos.x += self._text_padding
 | 
						|
      elif self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_CENTER:
 | 
						|
        line_pos.x += (self._rect.width - text_size.x) // 2
 | 
						|
      elif self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_RIGHT:
 | 
						|
        line_pos.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 * FONT_SCALE, self._text_color)
 | 
						|
        line_pos.x += self._font_size * FONT_SCALE
 | 
						|
        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 * FONT_SCALE
 | 
						|
 |