diff --git a/selfdrive/ui/layouts/settings/device.py b/selfdrive/ui/layouts/settings/device.py index 1655cdc8f9..f41d9932ed 100644 --- a/selfdrive/ui/layouts/settings/device.py +++ b/selfdrive/ui/layouts/settings/device.py @@ -20,6 +20,7 @@ from openpilot.system.hardware import HARDWARE, PC, TICI from openpilot.system.hardware.hw import Paths from openpilot.system.ui.lib.application import FontWeight, gui_app from openpilot.system.ui.lib.multilang import multilang, tr, tr_noop +from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets import Widget, DialogResult from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog from openpilot.system.ui.widgets.html_render import HtmlModal @@ -80,7 +81,7 @@ class GalaxyQRDialog(Widget): def _render_centered_text(self, rect: rl.Rectangle, text: str, y: float, font_size: int, color: rl.Color, font_weight: FontWeight = FontWeight.NORMAL) -> None: font = gui_app.font(font_weight) - size = rl.measure_text_ex(font, text, font_size, 0) + size = measure_text_cached(font, text, font_size) rl.draw_text_ex(font, text, rl.Vector2(rect.x + (rect.width - size.x) / 2, y), font_size, 0, color) def _render(self, rect: rl.Rectangle): diff --git a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py index 4bc5c6ea13..6b688a9e68 100644 --- a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py +++ b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py @@ -9,6 +9,7 @@ from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, M from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.lib.wrap_text import wrap_text as wrap_body_text from openpilot.system.ui.widgets import Widget, DialogResult from openpilot.system.ui.widgets.label import gui_label @@ -214,7 +215,7 @@ def draw_text_fit_common( render_width = measure_text_cached(font, text, actual_font_size, spacing=spacing).x else: render_width = size.x - nudge_y = (font_size - actual_font_size) / 2 + nudge_y = (font_size - actual_font_size) * FONT_SCALE / 2 draw_x = pos.x if align_center: draw_x = pos.x + (max_width - render_width) / 2 @@ -763,7 +764,7 @@ class PanelManagerView(AetherInteractiveMixin, Widget): PAGE_COMMIT_RATIO = 0.20 PAGE_ANIM_DURATION = 0.28 PAGE_SNAP_DURATION = 0.20 - PAGE_INDICATOR_HEIGHT = 44 + PAGE_INDICATOR_HEIGHT = 56 @@ -2032,12 +2033,12 @@ def draw_section_header( ): if title: trailing_reserved = min(320.0, rect.width * 0.38) if trailing_text else 0.0 - title_rect = rl.Rectangle(rect.x, rect.y + (rect.height - title_size) / 2, max(1.0, rect.width - trailing_reserved), title_size + 4) + title_rect = rl.Rectangle(rect.x, rect.y, max(1.0, rect.width - trailing_reserved), rect.height) alignment = rl.GuiTextAlignment.TEXT_ALIGN_CENTER if align_center else rl.GuiTextAlignment.TEXT_ALIGN_LEFT gui_label(title_rect, title, title_size, title_color or style.subtitle_color, FontWeight.SEMI_BOLD, alignment=alignment) if trailing_text: - trailing_rect = rl.Rectangle(rect.x, rect.y + (rect.height - trailing_size) / 2, rect.width, trailing_size + 4) + trailing_rect = rect gui_label( trailing_rect, trailing_text, @@ -2084,14 +2085,28 @@ def draw_empty_state_card( FontWeight.MEDIUM, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, ) - gui_label( - rl.Rectangle(card_rect.x + inset_x, body_y, max(1.0, card_rect.width - inset_x * 2), resolved_body_h), - body, - body_size, - style.subtitle_color, - FontWeight.NORMAL, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, - ) + resolved_body_h = min(resolved_body_h, max(0.0, card_rect.y + card_rect.height - body_y)) + font = gui_app.font(FontWeight.NORMAL) + line_height = body_size * FONT_SCALE + line_gap = 4.0 + max_lines = int((resolved_body_h + line_gap) // (line_height + line_gap)) + if not body or max_lines < 1: + return + body_width = max(1.0, card_rect.width - inset_x * 2) + if "\n" not in body and measure_text_cached(font, body, body_size).x <= body_width: + lines = [body] + else: + lines = wrap_body_text(font, body, body_size, int(body_width)) + if len(lines) > max_lines: + lines = lines[:max_lines - 1] + [truncate_text_ellipsis(font, " ".join(lines[max_lines - 1:]), body_width, body_size)] + text_y = body_y + (resolved_body_h - len(lines) * line_height - (len(lines) - 1) * line_gap) / 2 + for line in lines: + gui_label( + rl.Rectangle(card_rect.x + inset_x, text_y, body_width, line_height), + line, body_size, style.subtitle_color, FontWeight.NORMAL, + alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, + ) + text_y += line_height + line_gap def draw_list_group_shell( @@ -2573,7 +2588,7 @@ class AetherInlineRangeControl(Widget): draw_text_fit_common( self._font, label, - rl.Vector2(rect.x + 10, rect.y + (rect.height - 22) / 2), + rl.Vector2(rect.x + 10, rect.y + (rect.height - 22 * FONT_SCALE) / 2), max(1.0, rect.width - 20), 22, # font_size in draw_button align_center=True, @@ -3114,15 +3129,14 @@ class AetherButton(Widget): accent = self._accent_color or AetherListColors.PRIMARY bg = accent if enabled else rl.Color(accent.r, accent.g, accent.b, 80) border = with_alpha(accent, 190 if enabled else 70) + if hovered: + bg = rl.Color(min(bg.r + 10, 255), min(bg.g + 10, 255), min(bg.b + 10, 255), bg.a) + if pressed: + bg = rl.Color(max(bg.r - 8, 0), max(bg.g - 8, 0), max(bg.b - 8, 0), bg.a) else: - bg = rl.Color(255, 255, 255, 10 if enabled else 5) + bg = rl.Color(255, 255, 255, (20 if pressed else 14 if hovered else 10) if enabled else 5) border = rl.Color(255, 255, 255, 22 if enabled else 10) - if hovered: - bg = rl.Color(min(bg.r + 10, 255), min(bg.g + 10, 255), min(bg.b + 10, 255), bg.a) - if pressed: - bg = rl.Color(max(bg.r - 8, 0), max(bg.g - 8, 0), max(bg.b - 8, 0), bg.a) - rl.draw_rectangle_rounded(rect, 0.18, 12, bg) rl.draw_rectangle_rounded_lines_ex(rect, 0.18, 12, 1, border) rl.draw_rectangle_rec(rl.Rectangle(rect.x, rect.y, rect.width, 1), with_alpha(AetherListColors.HEADER, 18 if enabled else 8)) @@ -3159,7 +3173,7 @@ class AetherChip: draw_text_fit_common( gui_app.font(FontWeight.MEDIUM), self.text, - rl.Vector2(rect.x + 12, rect.y + (rect.height - self._font_size) / 2), + rl.Vector2(rect.x + 12, rect.y + (rect.height - self._font_size * FONT_SCALE) / 2), max(1.0, rect.width - 24), self._font_size, align_center=True, @@ -5605,4 +5619,3 @@ class TileGrid(Widget): tile.set_parent_rect(parent_rect) tile.render(snap_rect(rl.Rectangle(row_x + c * (row_tile_w + self._gap), rect.y + y_offset + r * (tile_h + self._gap), row_tile_w, tile_h))) tile_idx += 1 - diff --git a/selfdrive/ui/layouts/settings/starpilot/driving_model.py b/selfdrive/ui/layouts/settings/starpilot/driving_model.py index ebea4d6ac7..a3fe5c5069 100644 --- a/selfdrive/ui/layouts/settings/starpilot/driving_model.py +++ b/selfdrive/ui/layouts/settings/starpilot/driving_model.py @@ -140,7 +140,7 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget): enabled=lambda: self._controller.primary_header_button_state()[1], emphasized=True, font_size=SETTINGS_ROW_TITLE_FONT_SIZE, - accent_color=rl.Color(139, 92, 246, 92), + accent_color=with_alpha(AetherListColors.PRIMARY, 92), ) ) self._secondary_header_button = self._child( @@ -478,7 +478,7 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget): body_size=26, body_inset_x=48, title_top_padding=32, - body_height=60, + body_height=80, style=PANEL_STYLE, ) diff --git a/selfdrive/ui/layouts/settings/starpilot/lateral.py b/selfdrive/ui/layouts/settings/starpilot/lateral.py index e7e57dbd12..91f9b9ec31 100644 --- a/selfdrive/ui/layouts/settings/starpilot/lateral.py +++ b/selfdrive/ui/layouts/settings/starpilot/lateral.py @@ -211,7 +211,7 @@ class StarPilotLateralLayout(_SettingsPage): ), SettingRow( "LaneChangeCloseGapSeconds", "value", tr_noop("Temporary Follow Distance"), - subtitle=tr_noop("Follow distance to hold while changing lanes. Only applied when shorter than your normal gap."), + subtitle=tr_noop("Gap to use during lane changes, only when shorter than your normal gap."), get_value=self._get_lane_change_close_gap_display, on_click=lambda: self._show_slider("LaneChangeCloseGapSeconds", 0.25, 1.0, step=0.05, unit="s", value_type="float"), visible=close_gap_on, diff --git a/selfdrive/ui/layouts/settings/starpilot/longitudinal.py b/selfdrive/ui/layouts/settings/starpilot/longitudinal.py index 70b90afab3..7dad336035 100644 --- a/selfdrive/ui/layouts/settings/starpilot/longitudinal.py +++ b/selfdrive/ui/layouts/settings/starpilot/longitudinal.py @@ -520,7 +520,7 @@ class StarPilotLongitudinalLayout(_SettingsPage): on_click=lambda: self._show_slider("LeadDetectionThreshold", 25, 50, unit="%"), visible=self._longitudinal_enabled), SettingRow("NavLongitudinalAllowed", "toggle", tr_noop("Use Route Speed Control"), - subtitle=tr_noop("Allow an active navigation route to reduce cruise speed for upcoming turns, ramps, and roundabouts."), + subtitle=tr_noop("Use the active route to reduce cruise speed for upcoming turns, ramps, and roundabouts."), get_state=lambda: self._params.get_bool("NavLongitudinalAllowed"), set_state=lambda s: self._params.put_bool("NavLongitudinalAllowed", s), visible=self._longitudinal_enabled), @@ -654,7 +654,7 @@ class StarPilotLongitudinalLayout(_SettingsPage): # ── 4. Vision Speed Limits Rows ── self._vision_speed_limit_rows = [ SettingRow("VisionSpeedLimit", "toggle", tr_noop("Vision Detection"), - subtitle=tr_noop("Use the road camera to detect and display speed-limit signs, with optional use by Speed Limit Controller."), + subtitle=tr_noop("Display camera-detected speed-limit signs; optionally use them with Speed Limit Controller."), get_state=lambda: self._params.get_bool("VisionSpeedLimitDetection"), set_state=lambda s: self._params.put_bool("VisionSpeedLimitDetection", s)), ] @@ -729,7 +729,7 @@ class StarPilotLongitudinalLayout(_SettingsPage): current_value=max(1, self._params.get_float("CustomCruiseLong"))), visible=lambda: self._params.get_bool("QOLLongitudinal") and not starpilot_state.car_state.isToyota), SettingRow("ReverseCruise", "toggle", tr_noop("Reverse Cruise Increase"), - subtitle=tr_noop("Swap Toyota/Lexus cruise increments: short press changes the dash set speed by 5; hold changes it by 1."), + subtitle=tr_noop("Swap Toyota/Lexus steps: short press changes dash set speed by 5; hold changes it by 1."), get_state=lambda: self._params.get_bool("ReverseCruise"), set_state=lambda s: self._params.put_bool("ReverseCruise", s), visible=lambda: self._params.get_bool("QOLLongitudinal") and starpilot_state.car_state.isToyota), @@ -744,7 +744,7 @@ class StarPilotLongitudinalLayout(_SettingsPage): on_click=lambda: self._show_slider("ForceStopDistanceOffset", -20, 20, unit=" ft"), visible=lambda: self._params.get_bool("QOLLongitudinal") and self._params.get_bool("ForceStops")), SettingRow("RadarTakeoffs", "toggle", tr_noop("Radar for Takeoffs"), - subtitle=tr_noop("Turns on/off using radar data to track leads at standstill, making following/takeoffs more responsive once leads move."), + subtitle=tr_noop("Use radar to track leads at standstill and respond faster when they move."), get_state=lambda: self._params.get_bool("RadarTakeoffs"), set_state=lambda s: self._params.put_bool("RadarTakeoffs", s), visible=lambda: self._params.get_bool("QOLLongitudinal") and starpilot_state.car_state.hasRadar), diff --git a/selfdrive/ui/layouts/settings/starpilot/maps.py b/selfdrive/ui/layouts/settings/starpilot/maps.py index a6ec82f3a0..26a8331d7a 100644 --- a/selfdrive/ui/layouts/settings/starpilot/maps.py +++ b/selfdrive/ui/layouts/settings/starpilot/maps.py @@ -94,7 +94,7 @@ STATUS_CARD_HEIGHT = 232.0 SEGMENTED_CONTROL_HEIGHT = 68.0 BROWSER_SECTION_HEADER_HEIGHT = 56.0 BROWSER_REGION_ROW_HEIGHT = 104.0 -BROWSER_EMPTY_STATE_HEIGHT = 128.0 +BROWSER_EMPTY_STATE_HEIGHT = 208.0 BROWSER_INSET = 18.0 HEADER_GAP = 16.0 diff --git a/selfdrive/ui/layouts/settings/starpilot/navigation.py b/selfdrive/ui/layouts/settings/starpilot/navigation.py index 4fce414e63..9f959a1022 100644 --- a/selfdrive/ui/layouts/settings/starpilot/navigation.py +++ b/selfdrive/ui/layouts/settings/starpilot/navigation.py @@ -229,7 +229,7 @@ NAV_SUMMARY_HEIGHT = 124.0 NAV_ACTION_HEIGHT = 78.0 NAV_SECTION_HEIGHT = 72.0 NAV_ROW_HEIGHT = 124.0 -NAV_EMPTY_HEIGHT = 132.0 +NAV_EMPTY_HEIGHT = 208.0 NAV_ACTION_COLUMNS = 3 NAV_ACTION_GAP = 12.0 diff --git a/selfdrive/ui/layouts/settings/starpilot/system_settings.py b/selfdrive/ui/layouts/settings/starpilot/system_settings.py index 1198988379..fd8e9a2d17 100644 --- a/selfdrive/ui/layouts/settings/starpilot/system_settings.py +++ b/selfdrive/ui/layouts/settings/starpilot/system_settings.py @@ -50,6 +50,7 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import ( point_hits, draw_text_fit_common, wrap_text, + with_alpha, SECTION_GAP, ROW_HEIGHT, SPACING, @@ -466,8 +467,8 @@ class SystemSettingsManagerView(PanelManagerView): hovered, pressed = self._interactive_state("static:first_aid", btn_rect, pad_y=6) if pressed: - fill = rl.Color(139, 92, 246, 8) - border = rl.Color(139, 92, 246, 28) + fill = with_alpha(AetherListColors.PRIMARY, 8) + border = with_alpha(AetherListColors.PRIMARY, 28) elif hovered: fill = rl.Color(255, 255, 255, 4) border = rl.Color(255, 255, 255, 10) @@ -481,7 +482,7 @@ class SystemSettingsManagerView(PanelManagerView): icon_x = btn_rect.x + (btn_rect.width - 60.0 * s) / 2.0 icon_y = btn_rect.y + (btn_rect.height - 60.0 * s) / 2.0 if pressed: - icon_color = rl.Color(139, 92, 246, 190) + icon_color = with_alpha(AetherListColors.PRIMARY, 190) elif hovered: icon_color = rl.Color(160, 170, 185, 170) else: diff --git a/selfdrive/ui/layouts/settings/starpilot/vehicle.py b/selfdrive/ui/layouts/settings/starpilot/vehicle.py index dbc994a0b0..ce52f383ba 100644 --- a/selfdrive/ui/layouts/settings/starpilot/vehicle.py +++ b/selfdrive/ui/layouts/settings/starpilot/vehicle.py @@ -22,6 +22,9 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import ( PanelManagerView, RowToggleTile, SPACING, + SETTINGS_ROW_TITLE_FONT_SIZE, + SETTINGS_ROW_SUBTITLE_FONT_SIZE, + SETTINGS_ROW_VALUE_FONT_SIZE, SettingRow, TileGrid, TOGGLE_MIN_HEIGHT, @@ -199,9 +202,9 @@ class VehicleSettingsManagerView(PanelManagerView): subtitle_size = 26 value_size = 28 else: - title_size = 40 - subtitle_size = 28 - value_size = 34 + title_size = SETTINGS_ROW_TITLE_FONT_SIZE + subtitle_size = SETTINGS_ROW_SUBTITLE_FONT_SIZE + value_size = SETTINGS_ROW_VALUE_FONT_SIZE if row.type == "value" or row.id.startswith("combo:"): value_text = row.get_value() if row.get_value else "" diff --git a/selfdrive/ui/onroad/starpilot/navigation_card.py b/selfdrive/ui/onroad/starpilot/navigation_card.py index e22e6c4f8d..402d7d7f92 100644 --- a/selfdrive/ui/onroad/starpilot/navigation_card.py +++ b/selfdrive/ui/onroad/starpilot/navigation_card.py @@ -9,7 +9,7 @@ import pyray as rl from openpilot.common.params import UnknownKeyName from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.ui.lib.application import FontWeight, gui_app +from openpilot.system.ui.lib.application import FONT_SCALE, FontWeight, gui_app from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets import Widget @@ -341,17 +341,20 @@ class NavigationCardRenderer(Widget): title_lines, title_font_size = self._fit_title(self._primary_text, text_area_width, preferred_font_size, minimum_font_size) secondary_font_size = 26 - text_block_h = len(title_lines) * title_font_size + title_line_height = title_font_size * FONT_SCALE + secondary_height = secondary_font_size * FONT_SCALE + title_lines = title_lines[:2] + text_block_h = len(title_lines) * title_line_height if self._secondary_text: - text_block_h += 8 + secondary_font_size + text_block_h += 8 + secondary_height text_block_y = container_y + (card_height - text_block_h) // 2 - for index, line in enumerate(title_lines[:2]): - rl.draw_text_ex(self._font_bold, line, rl.Vector2(text_x, text_block_y + index * title_font_size), title_font_size, 0, rl.WHITE) + for index, line in enumerate(title_lines): + rl.draw_text_ex(self._font_bold, line, rl.Vector2(text_x, text_block_y + index * title_line_height), title_font_size, 0, rl.WHITE) if self._secondary_text: secondary_text = self._truncate_text(self._secondary_text, text_area_width, secondary_font_size) - secondary_y = text_block_y + len(title_lines) * title_font_size + 8 + secondary_y = text_block_y + len(title_lines) * title_line_height + 8 rl.draw_text_ex( self._font_medium, secondary_text, diff --git a/selfdrive/ui/tests/test_aethergrid.py b/selfdrive/ui/tests/test_aethergrid.py index cf47eabe18..fed3ec2370 100644 --- a/selfdrive/ui/tests/test_aethergrid.py +++ b/selfdrive/ui/tests/test_aethergrid.py @@ -71,6 +71,7 @@ def _install_aethergrid_stubs(): app_mod.MousePos = type("MousePos", (), {}) app_mod.MouseEvent = type("MouseEvent", (), {}) app_mod.FONT_SCALE = 1.0 + app_mod.font_fallback = lambda font: font app_mod.gui_app = types.SimpleNamespace( width=1920, height=1080, @@ -220,6 +221,34 @@ class RenderSpy: self.parent_rect = rect +@pytest.mark.parametrize("height, body_size, body_height", [(208, 26, None), (208, 24, None), (400, 26, 80), (128, 26, None)]) +@pytest.mark.parametrize("body", ["A long explanation " * 30, "unbroken" * 50, "First line\nSecond line\nThird line"]) +def test_empty_state_body_wraps_within_card(height, body_size, body_height, body): + mod = _import_aethergrid() + wrapper = importlib.import_module("openpilot.system.ui.lib.wrap_text") + mod.FONT_SCALE = 1.242 + font = types.SimpleNamespace(texture=types.SimpleNamespace(id=1)) + mod.gui_app.font = lambda *_: font + labels = [] + + def measure(_font, text, size, spacing=0): + return types.SimpleNamespace(x=len(text) * size * mod.FONT_SCALE / 2, y=size * mod.FONT_SCALE) + + with patch.object(mod, "measure_text_cached", measure), patch.object(wrapper, "measure_text_cached", measure), \ + patch.object(mod, "gui_label", lambda rect, text, *a, **kw: labels.append((rect, text))): + mod.draw_empty_state_card(mod.rl.Rectangle(0, 0, 500, height), "Title", body, + title_size=30, body_size=body_size, body_height=body_height) + + body_labels = labels[1:] + assert body_labels + for rect, text in body_labels: + assert rect.y >= labels[0][0].y + labels[0][0].height + assert rect.y + rect.height <= height + assert measure(font, text, body_size).x <= rect.width + if body.startswith(("A long", "unbroken")): + assert body_labels[-1][1].endswith("...") + + class TestAethergridContracts(unittest.TestCase): def test_aethergrid_module_imports_with_headless_stubs(self): mod = _import_aethergrid()