From f29eb5d25c2a0b774ca5935960b12675a89b224b Mon Sep 17 00:00:00 2001 From: stef <19478336+stefpi@users.noreply.github.com> Date: Sun, 26 Jul 2026 14:37:19 -0700 Subject: [PATCH] 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 --- openpilot/selfdrive/assets/icons/fire.png | 3 + .../selfdrive/assets/icons/yellow_warning.png | 3 + openpilot/selfdrive/ui/widgets/setup.py | 5 +- openpilot/system/ui/lib/emoji.py | 55 --------------- openpilot/system/ui/lib/text_measure.py | 20 +----- openpilot/system/ui/tici_setup.py | 30 +++++--- openpilot/system/ui/widgets/label.py | 68 ++++--------------- 7 files changed, 44 insertions(+), 140 deletions(-) create mode 100644 openpilot/selfdrive/assets/icons/fire.png create mode 100644 openpilot/selfdrive/assets/icons/yellow_warning.png delete mode 100644 openpilot/system/ui/lib/emoji.py diff --git a/openpilot/selfdrive/assets/icons/fire.png b/openpilot/selfdrive/assets/icons/fire.png new file mode 100644 index 0000000000..a9a1a9babe --- /dev/null +++ b/openpilot/selfdrive/assets/icons/fire.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4ae6b5f666de71c8af94e8dcace3c4ca0259070cb6e8b2bbb9ba648c28a80e1 +size 5926 diff --git a/openpilot/selfdrive/assets/icons/yellow_warning.png b/openpilot/selfdrive/assets/icons/yellow_warning.png new file mode 100644 index 0000000000..4300b5728a --- /dev/null +++ b/openpilot/selfdrive/assets/icons/yellow_warning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3f14ff386f5e3e8e01ac64231b77154729516aa96a8baf85fdd98fa6be20839 +size 2341 diff --git a/openpilot/selfdrive/ui/widgets/setup.py b/openpilot/selfdrive/ui/widgets/setup.py index c9452fc535..b00d1a42d4 100644 --- a/openpilot/selfdrive/ui/widgets/setup.py +++ b/openpilot/selfdrive/ui/widgets/setup.py @@ -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 diff --git a/openpilot/system/ui/lib/emoji.py b/openpilot/system/ui/lib/emoji.py deleted file mode 100644 index ad4c272c8d..0000000000 --- a/openpilot/system/ui/lib/emoji.py +++ /dev/null @@ -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] diff --git a/openpilot/system/ui/lib/text_measure.py b/openpilot/system/ui/lib/text_measure.py index dee4b419ff..60945b7f05 100644 --- a/openpilot/system/ui/lib/text_measure.py +++ b/openpilot/system/ui/lib/text_measure.py @@ -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 diff --git a/openpilot/system/ui/tici_setup.py b/openpilot/system/ui/tici_setup.py index 2b2c1c39f8..36f47ccf1b 100755 --- a/openpilot/system/ui/tici_setup.py +++ b/openpilot/system/ui/tici_setup.py @@ -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)) diff --git a/openpilot/system/ui/widgets/label.py b/openpilot/system/ui/widgets/label.py index fdaf6f3148..a3e827321c 100644 --- a/openpilot/system/ui/widgets/label.py +++ b/openpilot/system/ui/widgets/label.py @@ -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