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
 | |
| 
 |