diff --git a/openpilot/starpilot/galaxy/tests/test_onroad_layout.mjs b/openpilot/starpilot/galaxy/tests/test_onroad_layout.mjs index c5fb0f2761..3139b0720d 100644 --- a/openpilot/starpilot/galaxy/tests/test_onroad_layout.mjs +++ b/openpilot/starpilot/galaxy/tests/test_onroad_layout.mjs @@ -483,6 +483,9 @@ vm.colorAlpha("text", { target: { value: "128" } }) assert.equal(vm.selectedColors.text, "#ABCDEF80") // The CEM gauge is marked outside CEM scenes, as an editing aid only. assert.match(OnroadLayoutPage.template, /v-if="widget\.kind === 'cem_gauge' && state\.showEditingAids && !state\.scene\.startsWith\('cem_'\)"/) +// The Small screen shows the tap-areas chip under the layout instead of over it. +assert.match(OnroadLayoutPage.template, /state\.profile !== 'compact'" class="gx-layout__stage-chip" aria-hidden="true"><\/i>Tap areas/) +assert.match(OnroadLayoutPage.template, /state\.profile === 'compact'" class="gx-layout__stage-chip gx-layout__stage-chip--below"/) // One scene dropdown, grouped, with every scene exactly once. assert.deepEqual(vm.sceneGroups.map(group => group.label), ["Driving", "Conditional experimental", "Speed limit"]) assert.deepEqual(vm.sceneGroups.flatMap(group => group.scenes.map(scene => scene.id)).sort(), vm.scenes.map(scene => scene.id).sort()) diff --git a/openpilot/starpilot/galaxy/web/css/settings.css b/openpilot/starpilot/galaxy/web/css/settings.css index ff1e935235..99d325a67d 100644 --- a/openpilot/starpilot/galaxy/web/css/settings.css +++ b/openpilot/starpilot/galaxy/web/css/settings.css @@ -643,6 +643,7 @@ input[type="range"].gx-slider:active::-moz-range-thumb { /* Tap-area overlay label sits on the preview while the grid toggle is on. */ .gx-layout__preview-card > .gx-layout__stage { position: relative; margin-inline: 0; min-width: 0; } .gx-layout__stage-chip { position: absolute; top: 6px; right: 6px; z-index: 1; display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border: 1px solid #b799ff88; border-radius: var(--radius-full); background: rgba(22, 18, 31, .82); color: #d8c8ff; font-size: var(--fs-xs); font-weight: var(--fw-bold); white-space: nowrap; pointer-events: none; } +.gx-layout__stage-chip--below { position: static; justify-self: end; } .gx-layout__stage-chip--start { right: auto; left: 6px; max-width: calc(100% - 12px); overflow: hidden; text-overflow: ellipsis; display: block; } /* Covers the whole bar, buttons included, so its size never changes mid-drag. */ .gx-layout__savebar-snap { position: absolute; inset: 0; z-index: 1; display: flex; align-items: center; gap: 6px; padding: 0 14px; border-radius: inherit; background: inherit; color: var(--primary); font-weight: var(--fw-bold); pointer-events: none; } diff --git a/openpilot/starpilot/galaxy/web/js/onroad-layout.js b/openpilot/starpilot/galaxy/web/js/onroad-layout.js index f775058d95..c963aba85e 100644 --- a/openpilot/starpilot/galaxy/web/js/onroad-layout.js +++ b/openpilot/starpilot/galaxy/web/js/onroad-layout.js @@ -1249,8 +1249,8 @@ export const OnroadLayoutPage = {

{{ state.backdrop.url ? 'The comma screen could not refresh. Showing the last one.' : 'The comma screen is unavailable. Showing a sketch.' }}

{{ state.art.url ? 'Widget preview could not refresh. Showing the last preview.' : 'Widget preview unavailable. Placement controls remain available.' }}

- - + + @@ -1275,7 +1275,6 @@ export const OnroadLayoutPage = { opacity=".7" :stroke-width="state.profile === 'compact' ? 2 : 5" />
+ +

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.

Widgets snap to the screen edges, the centre and other widgets. Turn snapping off with the magnet button, or hold Option (Alt) while dragging.

diff --git a/openpilot/starpilot/ui/developer_preview.py b/openpilot/starpilot/ui/developer_preview.py index 744bbbafd4..75c634c59e 100644 --- a/openpilot/starpilot/ui/developer_preview.py +++ b/openpilot/starpilot/ui/developer_preview.py @@ -15,6 +15,8 @@ class OnroadVisualPreview: cem_reason: str | None = None curve_curvature: float | None = None label: str = "SYNTHETIC REPLAY PREVIEW" + # The Galaxy layout editor's sample: draw exactly as the car would, without the replay labels. + layout_sample: bool = False def parse_flags(raw: str) -> frozenset[str]: diff --git a/openpilot/starpilot/ui/layout_preview_renderer.py b/openpilot/starpilot/ui/layout_preview_renderer.py index 68b1a26f8e..2966363b5c 100644 --- a/openpilot/starpilot/ui/layout_preview_renderer.py +++ b/openpilot/starpilot/ui/layout_preview_renderer.py @@ -93,7 +93,7 @@ def sample_state(scene: str, document: dict) -> OnroadState: longitudinal_active=longitudinal, stock_cruise_active=False, slc_system_long_available=longitudinal, torque_utilization=0.78 if lateral else 0.0, customization=document, - visual_preview=OnroadVisualPreview(cem_reason=CEM_SCENE_REASONS[scene]) if scene in CEM_SCENE_REASONS else None) + visual_preview=OnroadVisualPreview(cem_reason=CEM_SCENE_REASONS[scene], layout_sample=True) if scene in CEM_SCENE_REASONS else None) if scene == 'cem_lead': state = replace(state, conditional_lead=ConditionalLead(12.0, 0.0)) @@ -281,7 +281,7 @@ class _Canvas: finally: rl.rl_pop_matrix() - def render(self, state: OnroadState, scene: str, *, label: bool = True) -> bytes: + def render(self, state: OnroadState, scene: str) -> bytes: if self.view is None or self.fonts is None or self.target is None: raise RuntimeError("Layout preview resources are closed") self._settle(state) @@ -299,8 +299,6 @@ class _Canvas: self.fonts.draw('StarPilot', FontRole.BRAND, 30, width - 210, height - 90) if self.profile == Profile.LARGE and self.viewport is None: render_sidebar(self.fonts, rl.Rectangle(width - SIDEBAR_WIDTH, 0, SIDEBAR_WIDTH, height)) - if label: - self._label(scene) finally: rl.end_texture_mode() image = rl.load_image_from_texture(self.target.texture) @@ -393,21 +391,6 @@ class _Canvas: self.view.torque_bar._torque_filter.x = state.torque_utilization if state.lateral_active else 0.0 self.view.torque_bar._alpha_filter.x = 1.0 if state.lateral_active else 0.0 - def _label(self, scene: str) -> None: - if self.fonts is None: - return - large = self.profile == Profile.LARGE - names = {"experimental": "MANUAL EXPERIMENTAL", "cem_stop_light": "CEM TRAFFIC LIGHT", - "cem_lead": "CEM STOPPED LEAD", "cem_curve": "CEM CURVE"} - label = "SAMPLE: " + names.get(scene, scene.replace("_", " ").upper()) - size = 27 if large else 14 - measured = self.fonts.measure(label, FontRole.SEMI_BOLD, size) - x = (1860 if large else 476) - measured.width - (38 if large else 8) - y = 1000 if large else 213 - rl.draw_rectangle_rounded(rl.Rectangle(x - 8, y - 3, measured.width + 16, measured.height + 6), - 0.2, 4, rl.Color(0, 0, 0, 220)) - self.fonts.draw(label, FontRole.SEMI_BOLD, size, x, y, rl.Color(255, 255, 255, 255)) - def close(self) -> None: if self.map is not None: self.map.close() @@ -470,7 +453,7 @@ class LayoutPreviewRenderer: # Everything the car draws except the movable widgets, which the editor places over it. # A non-empty order that names no widget keeps the ordered path and draws none of them. order = {**state.customization.get('widgetOrder', {}), str(profile): ['backdrop']} - return self._canvas.render(replace(state, customization={**state.customization, 'widgetOrder': order}), scene, label=False) + return self._canvas.render(replace(state, customization={**state.customization, 'widgetOrder': order}), scene) return self._canvas.render(state, scene) def expire(self) -> None: diff --git a/openpilot/starpilot/ui/layout_preview_sidebar.py b/openpilot/starpilot/ui/layout_preview_sidebar.py index a27df5d22d..47cb7e7bd7 100644 --- a/openpilot/starpilot/ui/layout_preview_sidebar.py +++ b/openpilot/starpilot/ui/layout_preview_sidebar.py @@ -65,5 +65,4 @@ def render_sidebar(fonts: BitmapFonts, frame: rl.Rectangle, fonts.draw(line, FontRole.SEMI_BOLD, font_size, rect.x + (rect.width - measured.width) / 2, y, rl.WHITE) y += measured.height - fonts.draw("SAMPLE DATA", FontRole.SEMI_BOLD, 14, frame.x + 14, frame.y + 3, - rl.Color(255, 255, 255, 210)) + diff --git a/openpilot/starpilot/ui/onroad.py b/openpilot/starpilot/ui/onroad.py index 0728242735..8b385bb48c 100644 --- a/openpilot/starpilot/ui/onroad.py +++ b/openpilot/starpilot/ui/onroad.py @@ -198,7 +198,7 @@ class OnroadView: def _preview_badge(self, state: OnroadState) -> None: """Keep synthetic replay visibly identified even over a full alert.""" preview = state.visual_preview - if preview is None: + if preview is None or preview.layout_sample: return flags = [] if preview.cem_reason is not None: @@ -422,7 +422,9 @@ class OnroadView: self.fonts.draw(label, FontRole.SEMI_BOLD, 29, 1390 + right_shift, 56, curve_color) mode = conditional_status(state) - if mode is not None and state.alert.size != AlertSize.FULL: + # The layout editor leaves out the mode caption; its CEM scenes are named in the editor itself. + sample = state.visual_preview is not None and state.visual_preview.layout_sample + if mode is not None and state.alert.size != AlertSize.FULL and not sample: family, reason, mode_color = mode mode_y = 101 if label is not None else 56 self.fonts.draw(f'{family} {reason}', FontRole.SEMI_BOLD, 25, 1390 + right_shift, mode_y, mode_color) diff --git a/openpilot/starpilot/ui/onroad_conditional.py b/openpilot/starpilot/ui/onroad_conditional.py index a77673fe5e..d08cbe0714 100644 --- a/openpilot/starpilot/ui/onroad_conditional.py +++ b/openpilot/starpilot/ui/onroad_conditional.py @@ -48,6 +48,10 @@ def status(state: OnroadState) -> tuple[str, str, rl.Color] | None: if preview is not None and preview.cem_reason in ('CURVE', 'LEAD', 'STOP LIGHT', 'SPEED'): if state.alert.size == AlertSize.FULL: return None + if preview.layout_sample: + # The layout editor's sample keeps the car's own reason (the gauge draws from it), not replay wording. + detail = _REASONS.get(reason(state)) + return ('CEM', detail, rl.Color(112, 192, 216, 255)) if detail else None return 'CEM', f'PREVIEW {preview.cem_reason}', rl.Color(112, 192, 216, 255) value = (state.conditional_effective if state.longitudinal_active else None) or getattr(state, 'conditional_perception', None) if value is None or value.choice not in (ModeChoice.CEM, ModeChoice.CCM): diff --git a/openpilot/starpilot/ui/tests/test_developer_preview.py b/openpilot/starpilot/ui/tests/test_developer_preview.py index 208d439f5e..75176609dd 100644 --- a/openpilot/starpilot/ui/tests/test_developer_preview.py +++ b/openpilot/starpilot/ui/tests/test_developer_preview.py @@ -32,7 +32,7 @@ class DeveloperPreviewTests(unittest.TestCase): value = preview_at(frozenset(("cem", "csc")), 3.0) with self.assertRaises(FrozenInstanceError): value.__setattr__("cem_reason", "ACTUAL") - self.assertEqual(set(vars(value)), {"cem_reason", "curve_curvature", "label"}) + self.assertEqual(set(vars(value)), {"cem_reason", "curve_curvature", "label", "layout_sample"}) def test_invalid_or_duplicate_flags_and_time_rejected(self): for raw in ("cem,cem", "csc,bogus", "CEM", ",cem"): diff --git a/openpilot/starpilot/ui/tests/test_layout_preview_renderer.py b/openpilot/starpilot/ui/tests/test_layout_preview_renderer.py index 82c2856f7e..9462693d58 100644 --- a/openpilot/starpilot/ui/tests/test_layout_preview_renderer.py +++ b/openpilot/starpilot/ui/tests/test_layout_preview_renderer.py @@ -80,7 +80,12 @@ class LayoutPreviewRendererTests(unittest.TestCase): self.assertTrue(sample.experimental_enabled) self.assertEqual(sample.visual_preview.cem_reason, reason) self.assertIsNone(sample.conditional_effective) - self.assertEqual(sample.visual_preview.label, "SYNTHETIC REPLAY PREVIEW") + 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) + self.assertEqual(family, "CEM") + self.assertFalse(detail.startswith("PREVIEW")) self.assertEqual(sample_state("cem_lead", document).speed_mps, 0.0) with self.assertRaises(ValueError): sample_state("live", document) @@ -120,7 +125,7 @@ class LayoutPreviewRendererTests(unittest.TestCase): self.assertEqual(renderer({"document": document, "profile": "compact", "scene": "braking", "art": "backdrop"}), b"png") state, scene = canvas.render.call_args.args self.assertEqual(scene, "braking") - self.assertEqual(canvas.render.call_args.kwargs, {"label": False}) + self.assertEqual(canvas.render.call_args.kwargs, {}) # An order naming no widget keeps the ordered path, so no movable widget is drawn. self.assertEqual(state.customization["widgetOrder"]["compact"], ["backdrop"]) self.assertEqual(state.customization["layouts"], document["layouts"]) @@ -200,7 +205,7 @@ class LayoutPreviewRendererTests(unittest.TestCase): patch("openpilot.starpilot.ui.layout_preview_sidebar.rl.draw_rectangle_rounded_lines_ex"): render_sidebar(fonts, rl.Rectangle(1860, 0, 300, 1080), metric) labels = [call.args[0] for call in fonts.draw.call_args_list] - self.assertEqual(labels, ["ACCEL", "1.08 ft/s²", "SAMPLE DATA"]) + self.assertEqual(labels, ["ACCEL", "1.08 ft/s²"]) if __name__ == "__main__":