Unified label: add scrolling (#36717)

* almost

* works!

* clean up

* fix

* trash

* Revert "trash"

This reverts commit 951d63382810d444fe08103f406a8c490cfcbe25.

* fix some bugs and use

* clean up

* clean up

* fix clipping

* clean up

* fix
This commit is contained in:
Shane Smiskol
2025-11-29 02:15:10 -08:00
committed by GitHub
parent cb718618d1
commit 088fc1cab1
4 changed files with 76 additions and 6 deletions

View File

@@ -3,7 +3,7 @@ import time
from cereal import log
import pyray as rl
from collections.abc import Callable
from openpilot.system.ui.widgets.label import gui_label, MiciLabel
from openpilot.system.ui.widgets.label import gui_label, MiciLabel, UnifiedLabel
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR, MousePos
from openpilot.selfdrive.ui.ui_state import ui_state
@@ -113,7 +113,7 @@ class MiciHomeLayout(Widget):
self._version_label = MiciLabel("", font_size=36, font_weight=FontWeight.ROMAN)
self._large_version_label = MiciLabel("", font_size=64, color=rl.GRAY, font_weight=FontWeight.ROMAN)
self._date_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN)
self._branch_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN, elide_right=False, scroll=True)
self._branch_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, scroll=True)
self._version_commit_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN)
def show_event(self):
@@ -195,7 +195,7 @@ class MiciHomeLayout(Widget):
self._date_label.set_position(version_pos.x + self._version_label.rect.width + 10, version_pos.y)
self._date_label.render()
self._branch_label.set_width(gui_app.width - self._version_label.rect.width - self._date_label.rect.width - 32)
self._branch_label.set_max_width(gui_app.width - self._version_label.rect.width - self._date_label.rect.width - 32)
self._branch_label.set_text(" " + ("release" if release_branch else self._version_text[1]))
self._branch_label.set_position(version_pos.x + self._version_label.rect.width + self._date_label.rect.width + 20, version_pos.y)
self._branch_label.render()

View File

@@ -207,7 +207,7 @@ class NetworkInfoPage(NavWidget):
self._connect_btn.set_click_callback(lambda: connect_callback(self._network.ssid) if self._network is not None else None)
self._title = UnifiedLabel("", 64, FontWeight.DISPLAY, rl.Color(255, 255, 255, int(255 * 0.9)),
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, scroll=True)
self._subtitle = UnifiedLabel("", 36, FontWeight.ROMAN, rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)),
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
@@ -217,6 +217,10 @@ class NetworkInfoPage(NavWidget):
self._network: Network | None = None
self._connecting: Callable[[], str | None] | None = None
def show_event(self):
super().show_event()
self._title.reset_scroll()
def update_networks(self, networks: dict[str, Network]):
# update current network from latest scan results
for ssid, network in networks.items():

View File

@@ -282,7 +282,8 @@ class BigDialogOptionButton(Widget):
self._selected = False
self._label = UnifiedLabel(option, font_size=70, text_color=rl.Color(255, 255, 255, int(255 * 0.58)),
font_weight=FontWeight.DISPLAY_REGULAR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
font_weight=FontWeight.DISPLAY_REGULAR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
scroll=True)
def set_selected(self, selected: bool):
self._selected = selected

View File

@@ -412,6 +412,7 @@ class UnifiedLabel(Widget):
max_width: int | None = None,
elide: bool = True,
wrap_text: bool = True,
scroll: bool = False,
line_height: float = 1.0,
letter_spacing: float = 0.0):
super().__init__()
@@ -426,10 +427,23 @@ class UnifiedLabel(Widget):
self._max_width = max_width
self._elide = elide
self._wrap_text = wrap_text
self._scroll = scroll
self._line_height = line_height * 0.9
self._letter_spacing = letter_spacing # 0.1 = 10%
self._spacing_pixels = font_size * letter_spacing
# Scroll state
self._scroll = scroll
self._needs_scroll = False
self._scroll_offset = 0
self._scroll_pause_t: float | None = None
self._scroll_state: ScrollState = ScrollState.STARTING
# Scroll mode does not support eliding or multiline wrapping
if self._scroll:
self._elide = False
self._wrap_text = False
# Cached data
self._cached_text: str | None = None
self._cached_wrapped_lines: list[str] = []
@@ -490,6 +504,12 @@ class UnifiedLabel(Widget):
"""Update the vertical text alignment."""
self._alignment_vertical = alignment_vertical
def reset_scroll(self):
"""Reset scroll state to initial position."""
self._scroll_offset = 0
self._scroll_pause_t = None
self._scroll_state = ScrollState.STARTING
def set_max_width(self, max_width: int | None):
"""Set the maximum width constraint for wrapping/eliding."""
if self._max_width != max_width:
@@ -528,6 +548,9 @@ class UnifiedLabel(Widget):
# Elide lines if needed (for width constraint)
self._cached_wrapped_lines = [self._elide_line(line, content_width) for line in self._cached_wrapped_lines]
if self._scroll:
self._cached_wrapped_lines = self._cached_wrapped_lines[:1] # Only first line for scrolling
# Process each line: measure and find emojis
self._cached_line_sizes = []
self._cached_line_emojis = []
@@ -540,6 +563,11 @@ class UnifiedLabel(Widget):
size = rl.Vector2(0, self._font_size * FONT_SCALE)
else:
size = measure_text_cached(self._font, line, self._font_size, self._spacing_pixels)
# This is the only line
if self._scroll:
self._needs_scroll = size.x > content_width
self._cached_line_sizes.append(size)
# Calculate total height
@@ -683,17 +711,53 @@ class UnifiedLabel(Widget):
else: # TEXT_ALIGN_MIDDLE
start_y = self._rect.y + (self._rect.height - total_visible_height) / 2
# Only scissor when we know there is a single scrolling line
if self._needs_scroll:
rl.begin_scissor_mode(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height))
# Render each line
current_y = start_y
for idx, (line, size, emojis) in enumerate(zip(visible_lines, visible_sizes, visible_emojis, strict=True)):
if self._needs_scroll:
if self._scroll_state == ScrollState.STARTING:
if self._scroll_pause_t is None:
self._scroll_pause_t = rl.get_time() + 2.0
if rl.get_time() >= self._scroll_pause_t:
self._scroll_state = ScrollState.SCROLLING
self._scroll_pause_t = None
elif self._scroll_state == ScrollState.SCROLLING:
self._scroll_offset -= 0.8 / 60. * gui_app.target_fps
# don't fully hide
if self._scroll_offset <= -size.x - self._rect.width / 3:
self._scroll_offset = 0
self._scroll_state = ScrollState.STARTING
self._scroll_pause_t = None
else:
self.reset_scroll()
self._render_line(line, size, emojis, 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)
# Move to next line (if not last line)
if idx < len(visible_lines) - 1:
# Use current line's height * line_height for spacing to next line
current_y += size.y * self._line_height
def _render_line(self, line, size, emojis, current_y):
if self._needs_scroll:
# draw black fade on left and right
fade_width = 20
rl.draw_rectangle_gradient_h(int(self._rect.x + self._rect.width - fade_width), int(self._rect.y), fade_width, int(self._rect.height), rl.BLANK, rl.BLACK)
if self._scroll_state != ScrollState.STARTING:
rl.draw_rectangle_gradient_h(int(self._rect.x), int(self._rect.y), fade_width, int(self._rect.height), rl.BLACK, rl.BLANK)
rl.end_scissor_mode()
def _render_line(self, line, size, emojis, 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
@@ -703,6 +767,7 @@ class UnifiedLabel(Widget):
line_x = self._rect.x + self._rect.width - size.x - self._text_padding
else:
line_x = self._rect.x + self._text_padding
line_x += self._scroll_offset + x_offset
# Render line with emojis
line_pos = rl.Vector2(line_x, current_y)