diff --git a/pyproject.toml b/pyproject.toml index b9a9fcab55..ce763d152d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -259,6 +259,7 @@ lint.flake8-implicit-str-concat.allow-multiline = false "tools".msg = "Use openpilot.tools" "pytest.main".msg = "pytest.main requires special handling that is easy to mess up!" "unittest".msg = "Use pytest" +"pyray.measure_text_ex".msg = "Use openpilot.system.ui.lib.text_measure" [tool.coverage.run] concurrency = ["multiprocessing", "thread"] diff --git a/selfdrive/ui/layouts/settings/settings.py b/selfdrive/ui/layouts/settings/settings.py index b3d28b0adf..1afa4f8f4a 100644 --- a/selfdrive/ui/layouts/settings/settings.py +++ b/selfdrive/ui/layouts/settings/settings.py @@ -9,6 +9,7 @@ from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.label import gui_text_box +from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.selfdrive.ui.layouts.network import NetworkLayout # Import individual panels @@ -97,7 +98,7 @@ class SettingsLayout: close_color = CLOSE_BTN_PRESSED if pressed else CLOSE_BTN_COLOR rl.draw_rectangle_rounded(close_btn_rect, 1.0, 20, close_color) - close_text_size = rl.measure_text_ex(self._font_bold, SETTINGS_CLOSE_TEXT, 140, 0) + close_text_size = measure_text_cached(self._font_bold, SETTINGS_CLOSE_TEXT, 140) close_text_pos = rl.Vector2( close_btn_rect.x + (close_btn_rect.width - close_text_size.x) / 2, close_btn_rect.y + (close_btn_rect.height - close_text_size.y) / 2, @@ -124,7 +125,7 @@ class SettingsLayout: is_selected = panel_type == self._current_panel text_color = TEXT_SELECTED if is_selected else TEXT_NORMAL # Draw button text (right-aligned) - text_size = rl.measure_text_ex(self._font_medium, panel_info.name, 65, 0) + text_size = measure_text_cached(self._font_medium, panel_info.name, 65) text_pos = rl.Vector2( button_rect.x + button_rect.width - text_size.x, button_rect.y + (button_rect.height - text_size.y) / 2 ) diff --git a/system/ui/lib/button.py b/system/ui/lib/button.py index 9ca82c9732..024e360448 100644 --- a/system/ui/lib/button.py +++ b/system/ui/lib/button.py @@ -1,6 +1,7 @@ import pyray as rl from enum import IntEnum from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.text_measure import measure_text_cached class ButtonStyle(IntEnum): @@ -99,7 +100,7 @@ def gui_button( # Handle icon and text positioning font = gui_app.font(font_weight) - text_size = rl.measure_text_ex(font, text, font_size, 0) + text_size = measure_text_cached(font, text, font_size) text_pos = rl.Vector2(0, rect.y + (rect.height - text_size.y) // 2) # Vertical centering # Draw icon if provided diff --git a/system/ui/lib/inputbox.py b/system/ui/lib/inputbox.py index 367e0dc7e4..ae036c03c2 100644 --- a/system/ui/lib/inputbox.py +++ b/system/ui/lib/inputbox.py @@ -1,6 +1,7 @@ import pyray as rl import time from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.text_measure import measure_text_cached PASSWORD_MASK_CHAR = "•" @@ -60,7 +61,7 @@ class InputBox: padding = 10 if self._cursor_position > 0: - cursor_x = rl.measure_text_ex(font, display_text[: self._cursor_position], self._font_size, 0).x + cursor_x = measure_text_cached(font, display_text[: self._cursor_position], self._font_size).x else: cursor_x = 0 @@ -141,7 +142,7 @@ class InputBox: if self._show_cursor: cursor_x = rect.x + padding if len(display_text) > 0 and self._cursor_position > 0: - cursor_x += rl.measure_text_ex(font, display_text[: self._cursor_position], font_size, 0).x + cursor_x += measure_text_cached(font, display_text[: self._cursor_position], font_size).x # Apply text offset to cursor position cursor_x -= self._text_offset @@ -182,7 +183,7 @@ class InputBox: min_distance = float('inf') for i in range(len(self._input_text) + 1): - char_width = rl.measure_text_ex(font, display_text[:i], font_size, 0).x + char_width = measure_text_cached(font, display_text[:i], font_size).x distance = abs(relative_x - char_width) if distance < min_distance: min_distance = distance diff --git a/system/ui/lib/label.py b/system/ui/lib/label.py index 00a4eda7c6..82533660de 100644 --- a/system/ui/lib/label.py +++ b/system/ui/lib/label.py @@ -1,5 +1,6 @@ import pyray as rl from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_SIZE, DEFAULT_TEXT_COLOR +from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.utils import GuiStyleContext @@ -14,7 +15,7 @@ def gui_label( elide_right: bool = True ): font = gui_app.font(font_weight) - text_size = rl.measure_text_ex(font, text, font_size, 0) + text_size = measure_text_cached(font, text, font_size) display_text = text # Elide text to fit within the rectangle @@ -24,13 +25,13 @@ def gui_label( while left < right: mid = (left + right) // 2 candidate = text[:mid] + ellipsis - candidate_size = rl.measure_text_ex(font, candidate, font_size, 0) + 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 = rl.measure_text_ex(font, display_text, font_size, 0) + text_size = measure_text_cached(font, display_text, font_size) # Calculate horizontal position based on alignment text_x = rect.x + { diff --git a/system/ui/lib/text_measure.py b/system/ui/lib/text_measure.py index 9cdd566409..c172f94251 100644 --- a/system/ui/lib/text_measure.py +++ b/system/ui/lib/text_measure.py @@ -4,10 +4,11 @@ _cache: dict[int, rl.Vector2] = {} def measure_text_cached(font: rl.Font, text: str, font_size: int, spacing: int = 0) -> rl.Vector2: + """Caches text measurements to avoid redundant calculations.""" key = hash((font.texture.id, text, font_size, spacing)) if key in _cache: return _cache[key] - result = rl.measure_text_ex(font, text, font_size, spacing) + result = rl.measure_text_ex(font, text, font_size, spacing) # noqa: TID251 _cache[key] = result return result diff --git a/system/ui/spinner.py b/system/ui/spinner.py index 2af24c4e51..3825f1300d 100755 --- a/system/ui/spinner.py +++ b/system/ui/spinner.py @@ -4,6 +4,7 @@ import threading import time from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.window import BaseWindow from openpilot.system.ui.text import wrap_text @@ -78,7 +79,7 @@ class SpinnerRenderer: rl.draw_rectangle_rounded(bar, 1, 10, rl.WHITE) elif wrapped_lines: for i, line in enumerate(wrapped_lines): - text_size = rl.measure_text_ex(gui_app.font(), line, FONT_SIZE, 0.0) + text_size = measure_text_cached(gui_app.font(), line, FONT_SIZE) rl.draw_text_ex(gui_app.font(), line, rl.Vector2(center.x - text_size.x / 2, y_pos + i * LINE_HEIGHT), FONT_SIZE, 0.0, rl.WHITE) diff --git a/system/ui/text.py b/system/ui/text.py index 82e64d836f..63d00fbde3 100755 --- a/system/ui/text.py +++ b/system/ui/text.py @@ -3,6 +3,7 @@ import re import time import pyray as rl from openpilot.system.hardware import HARDWARE, PC +from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.button import gui_button, ButtonStyle from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.application import gui_app @@ -33,7 +34,7 @@ def wrap_text(text, font_size, max_width): while len(words): word = words.pop(0) test_line = current_line + word + (words.pop(0) if words else "") - if rl.measure_text_ex(font, test_line, font_size, 0).x <= max_width: + if measure_text_cached(font, test_line, font_size).x <= max_width: current_line = test_line else: lines.append(current_line)