Repair UI intake CI typing and spelling

This commit is contained in:
firestar5683
2026-10-15 01:27:14 -05:00
parent cfc0eca07a
commit b16b9fa01f
20 changed files with 58 additions and 40 deletions
@@ -32,7 +32,7 @@ assert.deepEqual(pure(snapMove, { ...box, x: 253 }, targets, 6), { x: 253, y: 11
assert.deepEqual(pure(snapMove, box, bounds, 0), { x: 34, y: 113, guides: [] })
const target = (value, line = "start", kind = "widget", axis = "x", label = "Home") => ({ axis, value, line, kind, label })
result = pure(snapMove, { ...box, x: 100 }, [target(104), target(146, "center", "center")], 6)
assert.equal(result.x, 96) // Equal distance prefers bounds centre.
assert.equal(result.x, 96) // Equal distance prefers bounds center.
assert.equal(result.guides[0].kind, "center")
for (const kinds of [["zone", "widget"], ["widget", "bounds"], ["bounds", "center"]]) {
const first = target(104, "start", kinds[0]), second = target(96, "start", kinds[1])
@@ -236,7 +236,7 @@ invalidSnap.cancelDrag()
const traySnap = editor().vm
traySnap.layout.current_speed.enabled = false
traySnap.startDrag("current_speed", largePoint(2400, 0), true)
traySnap.moveDrag(largePoint(324, 550)) // Widget centre follows the tray pointer.
traySnap.moveDrag(largePoint(324, 550)) // Widget center follows the tray pointer.
assert.equal(traySnap.layout.current_speed.x, 30)
assert.equal(traySnap.layout.current_speed.enabled, true)
assert.ok(traySnap.state.snapGuides.length)
@@ -309,14 +309,14 @@ try {
else delete globalThis.localStorage
}
for (const [guides, expected] of [
[[{ axis: "x", kind: "center" }, { axis: "y", kind: "center" }], "Centred on the screen"],
[[{ axis: "x", kind: "center" }], "Centred across"],
[[{ axis: "y", kind: "center" }], "Centred top to bottom"],
[[{ axis: "x", kind: "center" }, { axis: "y", kind: "center" }], "Centered on the screen"],
[[{ axis: "x", kind: "center" }], "Centered across"],
[[{ axis: "y", kind: "center" }], "Centered top to bottom"],
[[{ label: "Home" }, { label: "Home" }, { label: "Work" }], "Lined up with Home and Work"],
[[{ label: "the edge" }, { label: "Home" }], "At the edge, lined up with Home"],
[[{ axis: "x", kind: "center", label: "the centre" }, { axis: "y", kind: "bounds", label: "the edge" }], "Centred across, at the edge"],
[[{ axis: "y", kind: "center", label: "the centre" }, { axis: "x", kind: "widget", label: "Home" }, { axis: "y", kind: "widget", label: "Work" }],
"Centred top to bottom, lined up with Home"],
[[{ axis: "x", kind: "center", label: "the center" }, { axis: "y", kind: "bounds", label: "the edge" }], "Centered across, at the edge"],
[[{ axis: "y", kind: "center", label: "the center" }, { axis: "x", kind: "widget", label: "Home" }, { axis: "y", kind: "widget", label: "Work" }],
"Centered top to bottom, lined up with Home"],
]) {
snapVm.state.snapGuides = guides
assert.equal(snapVm.snapSummary, expected)
@@ -545,7 +545,7 @@ assert.equal(backdropCalls.at(-1)[3], "braking")
vm.selectProfile("large")
vm.state.scene = "engaged"
vm.color("text", "#ABCDEF81")
assert.equal(backdropCalls.length, backdropCount + 5) // Colour edits change the real screen too.
assert.equal(backdropCalls.length, backdropCount + 5) // Color edits change the real screen too.
vm.color("text", "#ABCDEF80")
const draggingCount = backdropCalls.length
vm.startDrag("current_speed", event(500, 200))
@@ -16,13 +16,13 @@ export function snapTargets(profile, layout, id, visible) {
const add = (box, kind, label, names) => {
for (const axis of ["x", "y"]) {
const values = lines(box, axis)
// Keep the line role so widget centres cannot attract moving edges.
// Keep the line role so widget centers cannot attract moving edges.
for (const line of names) targets.push({ axis, value: values[line], kind, label, line })
}
}
const bounds = profile.widgets[id].bounds || profile.bounds
add(bounds, "bounds", "the edge", ["start", "end"])
add(bounds, "center", "the centre", ["center"])
add(bounds, "center", "the center", ["center"])
for (const [key, widget] of Object.entries(profile.widgets)) {
const at = layout[key]
if (key === id || !at?.enabled || !Number.isFinite(at.x) || !Number.isFinite(at.y) ||
@@ -437,12 +437,12 @@ export const OnroadLayoutPage = {
// A finger covers the preview chip, so touch drags show the alignment in the sticky save bar.
snapInBar() { return !!(this.state.drag?.touch && this.state.snapGuides.length) },
snapSummary() {
const guides = this.state.snapGuides, centred = new Set(guides.filter(guide => guide.kind === "center").map(guide => guide.axis))
if (centred.size === 2) return "Centred on the screen"
// A centred axis is described once; the other axis still says what it lines up with.
const labels = [...new Set(guides.filter(guide => !centred.has(guide.axis)).map(guide => guide.label))]
const guides = this.state.snapGuides, centered = new Set(guides.filter(guide => guide.kind === "center").map(guide => guide.axis))
if (centered.size === 2) return "Centered on the screen"
// A centered axis is described once; the other axis still says what it lines up with.
const labels = [...new Set(guides.filter(guide => !centered.has(guide.axis)).map(guide => guide.label))]
const others = labels.filter(label => label !== "the edge").slice(0, 2)
const text = [centred.has("x") ? "centred across" : centred.has("y") ? "centred top to bottom" : "",
const text = [centered.has("x") ? "centered across" : centered.has("y") ? "centered top to bottom" : "",
labels.includes("the edge") ? "at the edge" : "", others.length ? "lined up with " + others.join(" and ") : ""].filter(Boolean).join(", ")
return text && text[0].toUpperCase() + text.slice(1)
},
@@ -1273,7 +1273,7 @@ export const OnroadLayoutPage = {
<option v-for="scene in group.scenes" :key="scene.id" :value="scene.id">{{ scene.label }}</option>
</optgroup>
</GalaxySelect>
<button class="gx-icon-btn gx-layout__bar-toggle" type="button" :aria-pressed="state.snapEnabled" :disabled="!!state.drag || !!state.layerDrag" aria-label="Snap to edges and centres" title="Snap to edges and centres" @click="toggleSnap"><i class="bi bi-magnet" aria-hidden="true"></i></button>
<button class="gx-icon-btn gx-layout__bar-toggle" type="button" :aria-pressed="state.snapEnabled" :disabled="!!state.drag || !!state.layerDrag" aria-label="Snap to edges and centers" title="Snap to edges and centers" @click="toggleSnap"><i class="bi bi-magnet" aria-hidden="true"></i></button>
<button class="gx-icon-btn gx-layout__bar-toggle" type="button" :aria-pressed="state.showEditingAids" aria-label="Show editing aids" title="Show editing aids" @click="toggleEditingAids"><i class="bi bi-bounding-box" aria-hidden="true"></i></button>
<button v-if="!projection && profile.inputZones?.length" class="gx-icon-btn gx-layout__bar-toggle" type="button" :aria-pressed="state.showFavoriteZones" aria-label="Show Favorite tap areas" title="Show Favorite tap areas" @click="state.showFavoriteZones = !state.showFavoriteZones"><i class="bi bi-grid-3x3-gap" aria-hidden="true"></i></button>
<button class="gx-icon-btn gx-layout__bar-toggle" type="button" :aria-expanded="state.tipsOpen" aria-controls="gx-layout-tips" aria-label="Preview tips and display behavior" title="Preview tips and display behavior" @click="state.tipsOpen = !state.tipsOpen"><i class="bi bi-question-circle" aria-hidden="true"></i></button>
@@ -1362,7 +1362,7 @@ export const OnroadLayoutPage = {
<span v-if="!projection && state.showFavoriteZones && state.showEditingAids && state.profile === 'compact'" class="gx-layout__stage-chip gx-layout__stage-chip--below" aria-hidden="true"><i class="bi bi-grid-3x3-gap"></i>Tap areas</span>
<div v-show="state.tipsOpen" id="gx-layout-tips" class="gx-layout__tips">
<p class="gx-note">Drag widgets to place them. Behind them is the comma's own screen for the chosen scene, including its border, road and alerts. Readings are samples.</p>
<p class="gx-note">Widgets snap to the screen edges, the centre and other widgets. Turn snapping off with the magnet button, or hold Option (Alt) while dragging.</p>
<p class="gx-note">Widgets snap to the screen edges, the center and other widgets. Turn snapping off with the magnet button, or hold Option (Alt) while dragging.</p>
<p class="gx-note">General edit shows samples of all enabled widgets together. Driving scenes hide widgets that would be absent, and the eyes follow the scene. Eye changes stay with that scene; your General edit eyes are remembered.</p>
<p class="gx-note">The stopped timer appears in General edit and Stopped timer. Turn off editing aids to see the screen without outlines and guides.</p>
<p class="gx-note">The steering wheel and driver monitoring icon can be resized in each layout{{ projection ? ', and the map overlay takes any size and opacity' : '' }}. Existing display preferences and driving state still control when widgets appear.</p>
@@ -352,7 +352,7 @@ def projection_customization(document, base_customization):
dx, dy = shifts.get(key, (0, 0))
result['layouts']['large'][key] = {**placement, 'x': placement['x'] - dx, 'y': placement['y'] - dy}
from openpilot.starpilot.ui.onroad_customization import rgba
# Card opacity is a colour, not a placement: fold it into the fill and keep the native placement shape.
# Card opacity is a color, not a placement: fold it into the fill and keep the native placement shape.
opacity = result['layouts']['large'].get('cruise_limits', {}).pop('opacity', 100)
if opacity != 100:
red, green, blue, alpha = rgba(result, 'cardFill', 'large', 'cruise_limits')
@@ -251,7 +251,8 @@ def test_import_job_labels_metadata_free_package_with_offered_version(tmp_path,
def test_status_reports_expiry(tmp_path):
cert, root, key_pem, _ = build_identity(days=5)
directory = tmp_path / "identity"
apk_identity.install_identity({"phone-cert.pem": cert, "phone-key.pem": key_pem, "root-cert.pem": root}, {"subject": "O=CarService", "app_version": "16.2.1-release"}, directory)
apk_identity.install_identity({"phone-cert.pem": cert, "phone-key.pem": key_pem, "root-cert.pem": root},
{"subject": "O=CarService", "app_version": "16.2.1-release"}, directory)
status = apk_identity.identity_status(directory)
assert status["installed"] and status["days_left"] in (4, 5) and "renew" in status["warning"]
assert status["app_version"] == "16.2.1-release"
@@ -168,7 +168,9 @@ def test_refresh_retries_before_giving_up_and_reports_pending(monkeypatch, cache
assert waits == [apkpure.RETRY_DELAYS[0]]
assert pending == [True, True] # the page waits while tries remain
assert not apkpure.lookup_pending()
assert apkpure.download_status()["url"] == LINK
status = apkpure.download_status()
assert status is not None
assert status["url"] == LINK
def test_refresh_is_single_flight(monkeypatch, cache):
@@ -3,6 +3,7 @@ import ast
from pathlib import Path
import tempfile
from types import SimpleNamespace as NS
from typing import Any, cast
from unittest.mock import Mock, patch
import pytest
@@ -29,8 +30,8 @@ def native_poll():
source = Path(__file__).parents[3] / 'ui/runtime_app.py'
tree = ast.parse(source.read_text())
method = next(node for node in ast.walk(tree) if isinstance(node, ast.FunctionDef) and node.name == '_poll_projection_bookmark')
namespace = {'ui_state': NS(started=True, started_frame=1, sm=NS(logMonoTime={'carState': 50})),
'display_message': Mock(return_value=object())}
namespace: dict[str, Any] = {'ui_state': NS(started=True, started_frame=1, sm=NS(logMonoTime={'carState': 50})),
'display_message': Mock(return_value=object()), 'cast': cast}
exec(compile(ast.Module(body=[method], type_ignores=[]), str(source), 'exec'), namespace)
return namespace
@@ -50,6 +50,7 @@ class Widget(TypedDict):
defaultAnchor: NotRequired[str]
colors: NotRequired[dict[str, str | None]]
resizable: NotRequired[dict[str, int]]
sourcesExtent: NotRequired[dict[str, float]]
class LayoutProfile(TypedDict):
@@ -151,7 +152,7 @@ PROFILES["compact"]["widgets"][STOPPED_TIMER_WIDGET] = _timer_widget
# Restore the Dom road graphic in the gap between the speed card and main speed.
# It has no background and appears only with a current selected source.
CEM_GAUGE_WIDGET = 'cem_gauge'
# The gauge draws centred in a 344 x 300 frame; its box is the part it can actually fill, at this offset inside
# The gauge draws centered in a 344 x 300 frame; its box is the part it can actually fill, at this offset inside
# that frame. Version 6 and older layouts saved the frame's corner and move by this offset when they load.
CEM_GAUGE_FRAME = (344, 300)
CEM_GAUGE_INSET = (44, 24)
@@ -5,6 +5,7 @@ HudRenderer and ExpButton. See the adjacent UI LICENSE for retained source.
"""
from pathlib import Path
from typing import cast
import hashlib
import json
@@ -132,12 +133,12 @@ class UnifiedSpeedWidget:
status_size = fit_size(status, FontRole.NORMAL, 30) if status else 30
status_ink = self.fonts.horizontal_ink(status, FontRole.NORMAL, status_size) if status else None
status_v = self.fonts.vertical_ink(status, FontRole.NORMAL, status_size) if status else None
status_h = (status_v[1] - status_v[0]) if status else 0
status_h = (cast(tuple[float, float], status_v)[1] - cast(tuple[float, float], status_v)[0]) if status else 0
footer_size = fit_size(footer, FontRole.NORMAL, 30) if footer else 30
footer_ink = self.fonts.horizontal_ink(footer, FontRole.NORMAL, footer_size) if footer else None
footer_v = self.fonts.vertical_ink(footer, FontRole.NORMAL, footer_size) if footer else None
footer_h = (footer_v[1] - footer_v[0]) if footer else 0
footer_h = (cast(tuple[float, float], footer_v)[1] - cast(tuple[float, float], footer_v)[0]) if footer else 0
if align == 'center':
# Every line shares the card's horizontal center, and the entire stack is visually vertically centered.
@@ -151,16 +152,21 @@ class UnifiedSpeedWidget:
lambda y: self.fonts.draw(value, FontRole.SPEED, size, start(*number_ink), y - number_v[0] + dash_offset, text)
))
if has_value:
unit_v = cast(tuple[float, float], unit_v)
segments.append((
unit_v[0], unit_h,
lambda y: self.fonts.draw(shown.unit, FontRole.NORMAL, unit_size, start(*unit_ink), y - unit_v[0], muted)
))
if status:
status_ink = cast(tuple[float, float], status_ink)
status_v = cast(tuple[float, float], status_v)
segments.append((
status_v[0], status_h,
lambda y: self.fonts.draw(status, FontRole.NORMAL, status_size, start(*status_ink), y - status_v[0], accent if active else muted)
))
if footer:
footer_ink = cast(tuple[float, float], footer_ink)
footer_v = cast(tuple[float, float], footer_v)
segments.append((
footer_v[0], footer_h,
lambda y: self.fonts.draw(footer, FontRole.NORMAL, footer_size, start(*footer_ink), y - footer_v[0], footer_color)
@@ -183,6 +189,7 @@ class UnifiedSpeedWidget:
left = start(*number_ink)
self.fonts.draw(value, FontRole.SPEED, size, left, number_y, text)
if has_value:
unit_v = cast(tuple[float, float], unit_v)
unit_x = left + number_ink[0] - 14 - unit_ink[1] if align == 'right' else left + number_ink[1] + 14 - unit_ink[0]
unit_y = number_y + number_v[1] - unit_v[1]
self.fonts.draw(shown.unit, FontRole.NORMAL, unit_size, unit_x, unit_y, muted)
+2 -2
View File
@@ -13,7 +13,7 @@ import math
from pathlib import Path
import time
import threading
from typing import Any
from typing import Any, cast
from collections.abc import Callable
import pyray as rl
@@ -472,7 +472,7 @@ class StarShellSession:
display_message(ui_state.sm, 'carState', now_ns, after_frame=ui_state.started_frame) is not None)
stamp = int(ui_state.sm.logMonoTime['carState']) if authorized else 0
for _ in range(receiver.drain(now_ns, stamp, authorized=authorized)):
publish()
cast('Callable[[], None]', publish)()
def _poll_wheel(self, now_ns):
if getattr(ui_state, "replay_clock", None) is not None:
@@ -169,9 +169,10 @@ def test_galaxy_card_pixels_align_without_reading_font_textures():
widget = UnifiedSpeedWidget(fonts)
canvas = _Canvas.__new__(_Canvas)
canvas.profile, canvas.viewport, canvas.fonts, canvas.target = Profile.LARGE, viewport, fonts, target
canvas.view = SimpleNamespace(_ordered_widgets=lambda content, state, widget=widget, **kw: widget.render(content, state))
canvas._settle = lambda state: None
with patch.dict('openpilot.starpilot.ui.layout_preview_renderer.RUNTIME_PREVIEW_WIDGETS', large=('cruise_limits',)), \
with patch.object(canvas, 'view',
SimpleNamespace(_ordered_widgets=lambda content, state, widget=widget, **kw: widget.render(content, state)), create=True), \
patch.object(canvas, '_settle', lambda state: None), \
patch.dict('openpilot.starpilot.ui.layout_preview_renderer.RUNTIME_PREVIEW_WIDGETS', large=('cruise_limits',)), \
patch.object(rl, 'load_image_from_texture', side_effect=mobile_readback) as readback:
png = canvas.render_widget_art(state, document['layouts']['large'])
# A saved 50% background must show on the first frame, without a slider
@@ -83,7 +83,9 @@ class LayoutPreviewRendererTests(unittest.TestCase):
self.assertTrue(sample.visual_preview.layout_sample) # Drawn as the car would, without replay labels.
from openpilot.starpilot.ui.onroad_conditional import status
# The gauge still reads the real reason, but the mode caption itself is not drawn in the editor.
family, detail, _ = status(sample)
observed = status(sample)
assert observed is not None
family, detail, _ = observed
self.assertEqual(family, "CEM")
self.assertFalse(detail.startswith("PREVIEW"))
self.assertEqual(sample_state("cem_lead", document).speed_mps, 0.0)
@@ -1,9 +1,11 @@
"""Scene sketches and eye controls must agree with the native driving presentation."""
import json
from collections.abc import Callable
from pathlib import Path
import shutil
import subprocess
from typing import NoReturn, cast
import pytest
@@ -23,7 +25,7 @@ from openpilot.starpilot.ui.unified_speed_presentation import resolve_unified_sp
def test_every_scene_matches_native_border_mode_and_widget_visibility(target):
node = shutil.which('node')
if node is None:
pytest.skip('Node is required for the browser/native scene contract')
cast('Callable[[str], NoReturn]', pytest.skip)('Node is required for the browser/native scene contract')
profile = Profile.COMPACT if target == 'compact' else Profile.LARGE
document = default_document()
if target == 'projection':
@@ -119,10 +119,11 @@ def test_large_wheel_art_is_drawn_inside_its_crop_with_or_without_sidebar(monkey
document = projection_customization(projection, document)
canvas = renderer._Canvas.__new__(renderer._Canvas)
canvas.profile, canvas.viewport = Profile.LARGE, viewport
canvas.fonts = NS(profile=Profile.LARGE)
monkeypatch.setattr(canvas, 'fonts', NS(profile=Profile.LARGE), raising=False)
canvas.target = NS(texture=object())
view = OnroadView.__new__(OnroadView)
view.fonts, view.projection_viewport = canvas.fonts, viewport
monkeypatch.setattr(view, 'fonts', canvas.fonts, raising=False)
view.projection_viewport = viewport
view.steering_wheel = SteeringWheelWidget(Path('/unused'))
view.steering_wheel._texture = object() # Real widget geometry, without loading a GL texture.
canvas.view = view
@@ -116,7 +116,7 @@ def test_custom_placement_disable_layer_migration_preserve_existing_widgets():
sample = state(customization=migrated)
with patch.object(gauge, '_render_unified_road') as draw:
gauge.render(sample)
# The saved box is inset 44 x 24 inside the gauge's drawing frame, which is centred at (172, 145 - 60).
# The saved box is inset 44 x 24 inside the gauge's drawing frame, which is centered at (172, 145 - 60).
assert draw.call_args.args[1:3] == (960. - 44 + 172, 600. - 24 + 85)
migrated['layouts']['large'][CEM_GAUGE_WIDGET]['enabled'] = False
draw.reset_mock()
@@ -155,5 +155,5 @@ class DrawerNativeTest(unittest.TestCase):
widget = UnifiedSpeedWidget(Mock(spec=BitmapFonts, **vars(fonts)))
widget._draw_sources(rl.Rectangle(88, 545, 320, 76), state)
self.assertEqual([text for text, _ in drawn], ['Dash', '56', 'Vision', '56'])
self.assertIn(('56', FontRole.MEDIUM), drawn) # The accepted source's reading is emphasised.
self.assertIn(('56', FontRole.MEDIUM), drawn) # The accepted source's reading is emphasized.
self.assertIn(('56', FontRole.NORMAL), drawn)
@@ -1,6 +1,5 @@
"""Regression checks for qualified values, saved geometry and request feedback."""
from dataclasses import replace
from types import SimpleNamespace as N
from unittest.mock import Mock
from openpilot.starpilot.ui.onroad_state import ObservationKind, OnroadInput, OnroadState, SlcActionKind, SlcUiRequest, SpeedLimitObservation
@@ -1,4 +1,5 @@
from types import SimpleNamespace
from typing import Any, cast
from unittest.mock import Mock
import pytest
@@ -197,7 +198,7 @@ def test_projection_bookmark_is_saved_once_without_rendering_starpilot(frame, mo
source = Path(__file__).parents[3] / 'starpilot/ui/runtime_app.py'
method = next(node for node in ast.walk(ast.parse(source.read_text()))
if isinstance(node, ast.FunctionDef) and node.name == '_poll_projection_bookmark')
namespace = {'ui_state': state, 'display_message': Mock(return_value=object())}
namespace: dict[str, Any] = {'ui_state': state, 'display_message': Mock(return_value=object()), 'cast': cast}
exec(compile(ast.Module(body=[method], type_ignores=[]), str(source), 'exec'), namespace)
# Short Unix socket path for macOS. The real transport and native dispatch run
# while the top widget is unrelated to StarPilot, or no widget is drawn at all.
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "ab34357011d34391030f316138a394c2390c042aabf3cae579a0ac5bd0472920", "schema_version": 1}
{"protected_inventory_sha256": "e10d74d6606301a472810871c5349040d6db434e66388e860ac574d5d1f8d043", "schema_version": 1}