This commit is contained in:
firestarsdog
2026-10-03 01:40:36 -04:00
parent 2efe6017bf
commit 189f2aafbb
11 changed files with 93 additions and 43 deletions
+2 -1
View File
@@ -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):
@@ -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
@@ -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,
)
@@ -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,
@@ -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),
@@ -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
@@ -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
@@ -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:
@@ -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 ""
@@ -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,
+29
View File
@@ -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()