ui: remove emoji support (#38361)

* remove emoji support, replace with png

* upload icons

* fix icon left/right positioning

* simplify rewrite

* simplify firehose

* simplify custom software set up text

* lint

* fix custom software

* move fix to diff pr

---------

Co-authored-by: elkoled <elkoled@gmail.com>
This commit is contained in:
stef
2026-07-26 14:37:19 -07:00
committed by GitHub
parent 0bf99879d1
commit f29eb5d25c
7 changed files with 44 additions and 140 deletions
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d4ae6b5f666de71c8af94e8dcace3c4ca0259070cb6e8b2bbb9ba648c28a80e1
size 5926
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a3f14ff386f5e3e8e01ac64231b77154729516aa96a8baf85fdd98fa6be20839
size 2341
+4 -1
View File
@@ -18,7 +18,8 @@ class SetupWidget(Widget):
self._pair_device_btn = Button(lambda: tr("Pair device"), self._show_pairing, button_style=ButtonStyle.PRIMARY)
self._open_settings_btn = Button(lambda: tr("Open"), lambda: self._open_settings_callback() if self._open_settings_callback else None,
button_style=ButtonStyle.PRIMARY)
self._firehose_label = Label(lambda: tr("🔥 Firehose Mode 🔥"), font_weight=FontWeight.MEDIUM, font_size=64)
self._firehose_label = Label(lambda: tr("Firehose Mode"), font_weight=FontWeight.MEDIUM, font_size=64)
self._fire_icon = gui_app.texture("icons/fire.png", 64, 64)
def set_open_settings_callback(self, callback):
self._open_settings_callback = callback
@@ -67,6 +68,8 @@ class SetupWidget(Widget):
# Title with fire emojis
self._firehose_label.render(rl.Rectangle(rect.x, y, rect.width, 64))
rl.draw_texture_ex(self._fire_icon, rl.Vector2(x, y), 0.0, 1.0, rl.WHITE)
rl.draw_texture_ex(self._fire_icon, rl.Vector2(x + w - 64, y), 0.0, 1.0, rl.WHITE)
y += 64 + spacing
# Description
-55
View File
@@ -1,55 +0,0 @@
import io
import re
import functools
from importlib.resources import as_file
from PIL import Image, ImageDraw, ImageFont
import pyray as rl
from openpilot.system.ui.lib.application import FONT_DIR
_cache: dict[str, rl.Texture] = {}
EMOJI_REGEX = re.compile(
"""[\U0001F600-\U0001F64F
\U0001F300-\U0001F5FF
\U0001F680-\U0001F6FF
\U0001F1E0-\U0001F1FF
\U00002700-\U000027BF
\U0001F900-\U0001F9FF
\U00002600-\U000026FF
\U00002300-\U000023FF
\U00002B00-\U00002BFF
\U0001FA70-\U0001FAFF
\U0001F700-\U0001F77F
\u2640-\u2642
\u2600-\u2B55
\u200d
\u23cf
\u23e9
\u231a
\ufe0f
\u3030
]+""".replace("\n", ""),
flags=re.UNICODE
)
@functools.cache
def _load_emoji_font() -> ImageFont.FreeTypeFont:
with as_file(FONT_DIR.joinpath("NotoColorEmoji.ttf")) as font_path:
return ImageFont.truetype(io.BytesIO(font_path.read_bytes()), 109)
def find_emoji(text):
return [(m.start(), m.end(), m.group()) for m in EMOJI_REGEX.finditer(text)]
def emoji_tex(emoji):
if emoji not in _cache:
img = Image.new("RGBA", (128, 128), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
draw.text((0, 0), emoji, font=_load_emoji_font(), embedded_color=True)
with io.BytesIO() as buffer:
img.save(buffer, format="PNG")
l = buffer.tell()
buffer.seek(0)
_cache[emoji] = rl.load_texture_from_image(rl.load_image_from_memory(".png", buffer.getvalue(), l))
return _cache[emoji]
+1 -19
View File
@@ -1,6 +1,5 @@
import pyray as rl
from openpilot.system.ui.lib.application import FONT_SCALE, font_fallback
from openpilot.system.ui.lib.emoji import find_emoji
_cache: dict[int, rl.Vector2] = {}
@@ -13,24 +12,7 @@ def measure_text_cached(font: rl.Font, text: str, font_size: int, spacing: float
if key in _cache:
return _cache[key]
# Measure normal characters without emojis, then add standard width for each found emoji
emoji = find_emoji(text)
if emoji:
non_emoji_text = ""
last_index = 0
for start, end, _ in emoji:
non_emoji_text += text[last_index:start]
last_index = end
non_emoji_text += text[last_index:]
else:
non_emoji_text = text
result = rl.measure_text_ex(font, non_emoji_text, font_size * FONT_SCALE, spacing) # noqa: TID251
if emoji:
result.x += len(emoji) * font_size * FONT_SCALE
# If just emoji assume a single line height
if result.y == 0:
result.y = font_size * FONT_SCALE
result = rl.measure_text_ex(font, text, font_size * FONT_SCALE, spacing) # noqa: TID251
_cache[key] = result
return result
+21 -9
View File
@@ -107,13 +107,20 @@ class Setup(Widget):
self._custom_software_warning_title_label = Label("WARNING: Custom Software", 81, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
text_color=rl.Color(255, 89, 79, 255),
text_padding=60)
self._custom_software_warning_body_label = Label("Use caution when installing third-party software.\n\n"
+ "⚠️ It has not been tested by comma.\n\n"
+ "⚠️ It may not comply with relevant safety standards.\n\n"
+ "⚠️ It may cause damage to your device and/or vehicle.\n\n"
+ "If you'd like to proceed, use https://flash.comma.ai "
+ "to restore your device to a factory state later.",
68, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=60)
self._yellow_warning_icon = gui_app.texture("icons/yellow_warning.png", int(68 * FONT_SCALE), int(68 * FONT_SCALE))
self._custom_software_warning_body_labels = [
Label(text, 68, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
text_padding=60, icon=self._yellow_warning_icon if has_icon else None)
for text, has_icon in [
("Use caution when installing third-party software.", False),
("It has not been tested by comma.", True),
("It may not comply with relevant safety standards.", True),
("It may cause damage to your device and/or vehicle.", True),
("If you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later.", False)
]
]
self._custom_software_warning_body_scroll_panel = GuiScrollPanel()
self._downloading_body_label = Label("Downloading...", TITLE_FONT_SIZE, FontWeight.MEDIUM, text_padding=20)
@@ -295,7 +302,7 @@ class Setup(Widget):
self._download_failed_startover_button.render(rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT))
def render_custom_software_warning(self, rect: rl.Rectangle):
warn_rect = rl.Rectangle(rect.x, rect.y, rect.width, 1500)
warn_rect = rl.Rectangle(rect.x, rect.y, rect.width, 1550)
offset = self._custom_software_warning_body_scroll_panel.update(rect, warn_rect)
button_width = (rect.width - MARGIN * 3) / 2
@@ -304,7 +311,12 @@ class Setup(Widget):
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(button_y - BODY_FONT_SIZE * FONT_SCALE))
y_offset = rect.y + offset
self._custom_software_warning_title_label.render(rl.Rectangle(rect.x + 50, y_offset + 150, rect.width - 265, TITLE_FONT_SIZE * FONT_SCALE))
self._custom_software_warning_body_label.render(rl.Rectangle(rect.x + 50, y_offset + 400, rect.width - 50, BODY_FONT_SIZE * FONT_SCALE * 3))
y = y_offset + 300
for label in self._custom_software_warning_body_labels:
label.render(rl.Rectangle(rect.x + 50, y, rect.width - 50, BODY_FONT_SIZE))
y += 160
rl.end_scissor_mode()
self._custom_software_warning_back_button.render(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT))
+12 -56
View File
@@ -8,7 +8,6 @@ from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEX
from openpilot.system.ui.widgets import Widget
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
ICON_PADDING = 15
@@ -103,7 +102,7 @@ def gui_text_box(
rl.gui_set_font(gui_app.font(FontWeight.NORMAL))
# Non-interactive text area. Can render emojis and an optional specified icon.
# Non-interactive text area. Can render an optional specified icon.
class Label(Widget):
def __init__(self,
text: str | Callable[[], str],
@@ -145,7 +144,6 @@ class Label(Widget):
self._update_text(self._text)
def _update_text(self, text):
self._emojis = []
self._text_size = []
text = _resolve_value(text)
@@ -175,7 +173,6 @@ class Label(Widget):
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, _):
@@ -209,7 +206,7 @@ class Label(Widget):
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(self._text_wrapped, self._text_size, self._emojis, strict=True):
for text, text_size in zip(self._text_wrapped, self._text_size, strict=True):
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
@@ -218,18 +215,7 @@ class Label(Widget):
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)
rl.draw_text_ex(self._font, text, line_pos, self._font_size, 0, self._text_color)
text_pos.y += (text_size.y or self._font_size * FONT_SCALE) * self._line_scale
@@ -238,7 +224,6 @@ class UnifiedLabel(Widget):
Unified label widget that combines functionality from gui_label, gui_text_box, and Label.
Supports:
- Emoji rendering
- Text wrapping
- Automatic eliding (single-line or multiline)
- Proper multiline vertical alignment
@@ -303,7 +288,6 @@ class UnifiedLabel(Widget):
self._cached_text: str | None = None
self._cached_wrapped_lines: list[str] = []
self._cached_line_sizes: list[rl.Vector2] = []
self._cached_line_emojis: list[list[tuple[int, int, str]]] = []
self._cached_total_height: float | None = None
self._cached_width: int = -1
@@ -430,13 +414,10 @@ class UnifiedLabel(Widget):
if self._scroll:
self._cached_wrapped_lines = self._cached_wrapped_lines[:1] # Only first line for scrolling
# Process each line: measure and find emojis
# Process each line: measure
self._cached_line_sizes = []
self._cached_line_emojis = []
for line in self._cached_wrapped_lines:
emojis = find_emoji(line)
self._cached_line_emojis.append(emojis)
# Empty lines should still have height (use font size as line height)
if not line:
size = rl.Vector2(0, self._font_size * FONT_SCALE)
@@ -525,14 +506,12 @@ class UnifiedLabel(Widget):
# Calculate which lines fit in the available height
visible_lines: list[str] = []
visible_sizes: list[rl.Vector2] = []
visible_emojis: list[list[tuple[int, int, str]]] = []
current_height = 0.0
broke_early = False
for line, size, emojis in zip(
for line, size in zip(
self._cached_wrapped_lines,
self._cached_line_sizes,
self._cached_line_emojis,
strict=True):
# Calculate height needed for this line
@@ -553,7 +532,6 @@ class UnifiedLabel(Widget):
visible_lines.append(line)
visible_sizes.append(size)
visible_emojis.append(emojis)
current_height += line_height_needed
@@ -597,7 +575,7 @@ class UnifiedLabel(Widget):
# Render each line
current_y = start_y
for idx, (line, size, emojis) in enumerate(zip(visible_lines, visible_sizes, visible_emojis, strict=True)):
for idx, (line, size) in enumerate(zip(visible_lines, visible_sizes, strict=True)):
if self._needs_scroll:
if self._scroll_state == ScrollState.STARTING:
if self._scroll_pause_t is None:
@@ -616,12 +594,12 @@ class UnifiedLabel(Widget):
else:
self.reset_scroll()
self._render_line(line, size, emojis, current_y)
self._render_line(line, size, current_y)
# Draw 2nd instance for scrolling
if self._needs_scroll and self._scroll_state != ScrollState.STARTING:
text2_scroll_offset = size.x + self._rect.width / 3
self._render_line(line, size, emojis, current_y, text2_scroll_offset)
self._render_line(line, size, current_y, text2_scroll_offset)
# Move to next line (if not last line)
if idx < len(visible_lines) - 1:
@@ -660,7 +638,7 @@ class UnifiedLabel(Widget):
shimmer = math.exp(-0.5 * d * d / (sigma * sigma))
return self.SHIMMER_LOW_OPACITY + (1.0 - self.SHIMMER_LOW_OPACITY) * shimmer
def _render_line(self, line, size, emojis, current_y, x_offset=0.0):
def _render_line(self, line, size, current_y, x_offset=0.0):
# Calculate horizontal position
if self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_LEFT:
line_x = self._rect.x + self._text_padding
@@ -675,33 +653,11 @@ class UnifiedLabel(Widget):
if self._shimmer:
self._render_line_shimmer(line, line_x, current_y)
else:
# Render line with emojis
self._render_line_normal(line, emojis, line_x, current_y)
self._render_line_normal(line, line_x, current_y)
def _render_line_normal(self, line, emojis, line_x, current_y):
def _render_line_normal(self, line, line_x, current_y):
line_pos = rl.Vector2(line_x, current_y)
prev_index = 0
for start, end, emoji in emojis:
# Draw text before emoji
text_before = line[prev_index:start]
if text_before:
rl.draw_text_ex(self._font, text_before, line_pos, self._font_size, self._spacing_pixels, self._text_color)
width_before = measure_text_cached(self._font, text_before, self._font_size, self._spacing_pixels)
line_pos.x += width_before.x
# Draw emoji
tex = emoji_tex(emoji)
emoji_scale = self._font_size / tex.height * FONT_SCALE
rl.draw_texture_ex(tex, line_pos, 0.0, emoji_scale, self._text_color)
# Emoji width is font_size * FONT_SCALE (as per measure_text_cached)
line_pos.x += self._font_size * FONT_SCALE
prev_index = end
# Draw remaining text after last emoji
text_after = line[prev_index:]
if text_after:
rl.draw_text_ex(self._font, text_after, line_pos, self._font_size, self._spacing_pixels, self._text_color)
rl.draw_text_ex(self._font, line, line_pos, self._font_size, self._spacing_pixels, self._text_color)
def _render_line_shimmer(self, line, line_x, current_y):
# Shimmer range based on widest line so sweep is even across all lines