Align speed HUD for clearer SLC prompts

This commit is contained in:
firestarsdog
2026-11-07 22:49:52 -05:00
parent 63ad1785ed
commit 9bcc7bdd2d
15 changed files with 308 additions and 202 deletions
@@ -10,11 +10,11 @@ print(json.dumps(dict(document=default_document(), defaults=default_document(),
revision="native", editable=True, valid=True, activeProfile="large", supportedProfiles=["large"])))
`], { encoding: "utf8" }))
assert.equal(validSnapshot(data), true)
for (const version of [11, 12, 13]) {
for (const version of [11, 12, 13, 14]) {
assert.equal(validSnapshot({ ...data, document: { ...data.document, version } }), true)
assert.equal(validSnapshot({ ...data, defaults: { ...data.defaults, version } }), true)
}
for (const version of [9, 10, 14]) {
for (const version of [9, 10, 15]) {
assert.equal(validSnapshot({ ...data, document: { ...data.document, version } }), false)
assert.equal(validSnapshot({ ...data, defaults: { ...data.defaults, version } }), false)
}
@@ -1220,15 +1220,17 @@ console.log('Display options, target switching and reload during polling passed'
assert.equal(documentHelpers.overlapsReserved(profile, 'cruise_limits', 88, 500, sample.document.layouts.large), true)
}
// Portrait sizing and its attached chooser use the same compact bounds as native input.
// Reserve the taller notice while keeping the opened buttons at their existing size.
{
const sample = snapshot(), profile = sample.metadata.profiles.compact
profile.widgets.max_speed.width = 146
profile.widgets.speed_limit = { ...profile.widgets.speed_limit, width: 128, height: 144,
confirmation: { x: -172, y: -8, width: 328, height: 152 } }
sample.document.version = 13
confirmation: { x: -172, y: -8, width: 328, height: 159 } }
sample.document.version = 14
sample.document.layouts.compact.speed_limit = { x: 320, y: 16, enabled: true }
assert.equal(documentHelpers.overlapsReserved(profile, 'speed_limit', 320, 16, sample.document.layouts.compact), false)
assert.equal(validDocument(sample.document, sample.metadata), true)
assert.equal(documentHelpers.overlapsReserved(profile, 'speed_limit', 320, 20, sample.document.layouts.compact), true)
assert.equal(documentHelpers.overlapsReserved(profile, 'speed_limit', 320, 24, sample.document.layouts.compact), true)
assert.equal(documentHelpers.overlapsReserved(profile, 'speed_limit', 317, 16, sample.document.layouts.compact), true)
assert.equal(clampPlacement(profile, 'speed_limit', 330, 16, sample.document.layouts.compact).x, 320)
@@ -140,7 +140,7 @@ export const PROJECTION_DISPLAY_SWITCHES = ["blindSpotBorder", "signalBorder", "
export function validDocument(document, metadata) {
if (!object(document)) return false
const profiles = metadata?.projection === true ? ["large"] : PROFILES
if (!keysEqual(document, ["version", "palette", "layouts", "widgetColors", "roadColors", ...(Object.hasOwn(document, "widgetOrder") ? ["widgetOrder"] : []), ...(Object.hasOwn(document, "speedSources") ? ["speedSources"] : []), ...(Object.hasOwn(document, "speedSourcesCard") ? ["speedSourcesCard"] : []), ...(Object.hasOwn(document, "speedAlign") ? ["speedAlign"] : []), ...(Object.hasOwn(document, "clock24Hour") ? ["clock24Hour"] : []), ...(metadata?.projection !== true && Object.hasOwn(document, "reduceMotion") ? ["reduceMotion"] : []), ...(Object.hasOwn(document, "showMaxSpeed") ? ["showMaxSpeed"] : []), ...(metadata?.projection !== true && Object.hasOwn(document, "display") ? ["display"] : []), ...(metadata?.projection === true && Object.hasOwn(document, "largeUiGammaTrial") ? ["largeUiGammaTrial"] : []), ...(metadata?.projection === true ? PROJECTION_DISPLAY_SWITCHES.filter(key => Object.hasOwn(document, key)) : []), ...(metadata?.projection === true && Object.hasOwn(document, "leadInfo") ? ["leadInfo"] : [])]) || (metadata?.projection === true ? document.version !== 8 : ![11, 12, 13].includes(document.version)) ||
if (!keysEqual(document, ["version", "palette", "layouts", "widgetColors", "roadColors", ...(Object.hasOwn(document, "widgetOrder") ? ["widgetOrder"] : []), ...(Object.hasOwn(document, "speedSources") ? ["speedSources"] : []), ...(Object.hasOwn(document, "speedSourcesCard") ? ["speedSourcesCard"] : []), ...(Object.hasOwn(document, "speedAlign") ? ["speedAlign"] : []), ...(Object.hasOwn(document, "clock24Hour") ? ["clock24Hour"] : []), ...(metadata?.projection !== true && Object.hasOwn(document, "reduceMotion") ? ["reduceMotion"] : []), ...(Object.hasOwn(document, "showMaxSpeed") ? ["showMaxSpeed"] : []), ...(metadata?.projection !== true && Object.hasOwn(document, "display") ? ["display"] : []), ...(metadata?.projection === true && Object.hasOwn(document, "largeUiGammaTrial") ? ["largeUiGammaTrial"] : []), ...(metadata?.projection === true ? PROJECTION_DISPLAY_SWITCHES.filter(key => Object.hasOwn(document, key)) : []), ...(metadata?.projection === true && Object.hasOwn(document, "leadInfo") ? ["leadInfo"] : [])]) || (metadata?.projection === true ? document.version !== 8 : ![11, 12, 13, 14].includes(document.version)) ||
(Object.hasOwn(document, "speedSources") && typeof document.speedSources !== "boolean") ||
(Object.hasOwn(document, "speedSourcesCard") && typeof document.speedSourcesCard !== "boolean") ||
(Object.hasOwn(document, "speedAlign") && !SPEED_ALIGNMENTS.includes(document.speedAlign)) ||
@@ -38,7 +38,7 @@ def confirmation_bounds(profile, x, y, *, opened=False):
width = 2 * CONFIRMATION_PADDING + 2 * CONFIRMATION_BUTTON[profile] + CONFIRMATION_GAP if opened else 240
left = x + 172 if profile == 'large' else x - 172 if opened else x - 100
top = y - (12 if profile == 'large' else 8)
height = CONFIRMATION_HEIGHT[profile] if opened else 152
height = CONFIRMATION_HEIGHT[profile] if opened else (168 if profile == 'large' else 159)
return left, top, left + width * scale, top + height * scale
@@ -52,6 +52,8 @@ def confirmation_buttons(profile, bounds):
def confirmation_footprint(profile):
left, top, right, bottom = confirmation_bounds(profile, 0, 0, opened=True)
# The notice is taller than the open buttons on Compact.
bottom = max(bottom, confirmation_bounds(profile, 0, 0)[3])
return {'x': left, 'y': top, 'width': right - left, 'height': bottom - top}
@@ -18,7 +18,7 @@ from openpilot.starpilot.ui.onroad_conditional import reason as conditional_reas
from openpilot.starpilot.ui.presentation import BitmapFonts, FontRole, Profile
from openpilot.starpilot.ui.onroad_widget_style import draw_widget_frame
from openpilot.starpilot.ui.wheel_feedback import WheelFeedbackFade
from openpilot.starpilot.ui.speed_limit_widget import SpeedLimitWidget, draw_caption, draw_ink, set_speed_status, speed_status
from openpilot.starpilot.ui.speed_limit_widget import SpeedLimitWidget, draw_set_speed, draw_set_status, set_speed_status, speed_status
from openpilot.starpilot.ui.unified_speed_presentation import resolve_unified_speed
from openpilot.starpilot.ui.moon import draw_moon
from openpilot.starpilot.ui.onroad_personality import PersonalityWidget
@@ -139,24 +139,15 @@ class CompactHudRenderer:
status = set_speed_status(state, Profile.COMPACT, shown)
persistent = shown.slc_target_text is not None or shown.override_text is not None or speed_status(state, Profile.COMPACT, shown) is not None
set_speed_alpha = self._set_speed_opacity(state, (state.observed_ns or time.monotonic_ns()), slc_active=persistent)
cruise = shown.max_text
mx, my = widget_offset(state.customization, "compact", "max_speed")
center = widget_size(state.customization, 'compact', 'max_speed')[0] / 2 + mx
if set_speed_alpha > 0.01 and placement(state.customization, "compact", "max_speed")["enabled"]:
red, green, blue, alpha = rgba(state.customization, "text", "compact", "max_speed")
color = rl.Color(red, green, blue, int(alpha * set_speed_alpha))
draw_caption(self.fonts, 'SET', center, 20 + my, color, unit=shown.unit, size=24, centered=True)
size = 96
left, right = self.fonts.horizontal_ink(cruise, FontRole.SET_SPEED, size)
size *= min(1, 128 / max(1, right - left))
left, right = self.fonts.horizontal_ink(cruise, FontRole.SET_SPEED, size)
draw_ink(self.fonts, cruise, FontRole.SET_SPEED, size, center - (left + right) / 2, 46 + my, color)
draw_set_speed(self.fonts, shown, center, 16 + my, color)
if status and placement(state.customization, 'compact', 'max_speed')['enabled']:
color = rl.Color(241, 246, 243, 255)
left, right = self.fonts.horizontal_ink(status, FontRole.SEMI_BOLD, 24)
size = 24 * min(1, 128 / max(1, right - left))
left, right = self.fonts.horizontal_ink(status, FontRole.SEMI_BOLD, size)
draw_ink(self.fonts, status, FontRole.SEMI_BOLD, size, center - (left + right) / 2, 128 + my, color)
draw_set_status(self.fonts, status, center, 16 + my, color)
def render_steering_wheel(self, state: OnroadState) -> None:
color = self._feedback.update(state.wheel_acceleration, state.appearance.wheel_pedal_feedback, state.observed_ns, state.drive_frame)
@@ -9,7 +9,7 @@ from typing import Any, NotRequired, TypedDict
from openpilot.starpilot.saved_source import read_saved
from openpilot.starpilot.ui.onroad_torque_geometry import maximum_footprint
from openpilot.starpilot.ui.large_speed_geometry import LIMIT_RECTS, confirmation_bounds, confirmation_footprint
from openpilot.starpilot.ui.large_speed_geometry import LIMIT_RECTS, confirmation_footprint
from openpilot.starpilot.ui.onroad_stopped_timer import timer_size
from openpilot.starpilot.ui.onroad_favorites_geometry import shelf_metadata
from openpilot.starpilot.ui.large_speed_geometry import (WIDTH, CARD_HEIGHT, ACTION_WIDTH, ACTION_HEIGHT, ACTION_X, ACTION_Y,
@@ -342,7 +342,7 @@ def customization_metadata():
def default_document():
return {"version": 13, "clock24Hour": False, "reduceMotion": False, "palette": dict(PALETTE), "roadColors": {profile: {} for profile in PROFILES},
return {"version": 14, "clock24Hour": False, "reduceMotion": False, "palette": dict(PALETTE), "roadColors": {profile: {} for profile in PROFILES},
"widgetColors": {profile: {} for profile in PROFILES}, "layouts": {
profile: {key: {**widget["default"], **({"size": WIDGET_SIZES[key][profile][1]} if key in WIDGET_SIZES else {})}
for key, widget in data["widgets"].items()}
@@ -431,7 +431,9 @@ def confirmation_layout_valid(layout, profile, widgets, bounds, *, extra_height=
at = layout[owner]
if not at['enabled'] or not layout['speed_limit_actions']['enabled']:
return True
left, top, right, bottom = confirmation_bounds(profile, at['x'], at['y'], opened=True)
area = confirmation_footprint(profile)
left, top = at['x'] + area['x'], at['y'] + area['y']
right, bottom = left + area['width'], top + area['height']
if extra_height:
left, right = min(left, at['x']), max(right, at['x'] + widgets[owner]['width'])
bottom = max(bottom, at['y'] + widgets[owner]['height'] + extra_height)
@@ -473,7 +475,7 @@ def migrate_confirmation_layout(layout, profile, widgets, bounds, *, extra_heigh
def validate_document(value):
if type(value) is not dict or type(value.get("version")) is not int or value["version"] not in range(1, 14):
if type(value) is not dict or type(value.get("version")) is not int or value["version"] not in range(1, 15):
raise ValueError("Invalid customization version")
fields = {"version", "palette", "layouts"} | ({"widgetColors"} if value["version"] >= 2 else set())
if value["version"] >= 3:
@@ -745,6 +747,7 @@ def validate_document(value):
old_gap += GAP if result.get('speedSourcesCard') is True else 0
if actions['x'] == card['x'] + ACTION_X - 88 and actions['y'] == card['y'] + old_gap:
actions['y'] = card['y'] + ACTION_Y - 75
if value['version'] < 14:
migrate_confirmation_layout(result['layouts'][profile], profile, data['widgets'], data['bounds'],
extra_height=sources_extent(result) if profile == 'large' else 0)
elif not confirmation_layout_valid(result['layouts'][profile], profile, data['widgets'], data['bounds'],
+6 -27
View File
@@ -11,12 +11,12 @@ import json
from openpilot.starpilot.ui.raylib import rl
from openpilot.starpilot.ui.onroad_customization import offset, rgba, widget_size
from openpilot.starpilot.ui.onroad_state import OnroadState, manual_slc_confirmation, speed_limit_bounds, slc_action_occludes
from openpilot.starpilot.ui.onroad_state import OnroadState, manual_slc_confirmation, slc_action_occludes
from openpilot.starpilot.ui.onroad_widget_style import draw_control_card
from openpilot.starpilot.ui.presentation import BitmapFonts, FontRole, Profile
from openpilot.starpilot.ui.wheel_feedback import WheelFeedbackFade
from openpilot.starpilot.ui.unified_speed_presentation import resolve_unified_speed
from openpilot.starpilot.ui.speed_limit_widget import MUTED, SpeedLimitWidget, draw_caption, draw_ink, draw_line, draw_text, set_speed_status
from openpilot.starpilot.ui.speed_limit_widget import SpeedLimitWidget, draw_set_speed, draw_set_status, draw_text, set_speed_status
ENGAGED = rl.Color(128, 216, 166, 255)
@@ -65,7 +65,7 @@ class UnifiedSpeedWidget:
draw_text(self.fonts, text, role, font_size, box.x + (box.width - width) / 2, y, color)
def render(self, content: rl.Rectangle, state: OnroadState) -> None:
from openpilot.starpilot.ui.large_speed_geometry import GAP, WIDTH, PADDING, SOURCE_ROW_HEIGHT, SOURCE_TOP, card_height
from openpilot.starpilot.ui.large_speed_geometry import GAP, WIDTH, SOURCE_ROW_HEIGHT, SOURCE_TOP, card_height
from openpilot.starpilot.ui.onroad_customization import placement
shown = resolve_unified_speed(state)
if shown.mode == 'hidden':
@@ -79,31 +79,10 @@ class UnifiedSpeedWidget:
border = rl.Color(*rgba(state.customization, 'cardBorder', 'large', 'cruise_limits'))
text = rl.Color(*rgba(state.customization, 'text', 'large', 'cruise_limits'))
if shown.mode != 'limit_only':
confirming = manual_slc_confirmation(Profile.LARGE, state)
center = rect.x + 120
x = rect.x + PADDING if confirming else center
draw_caption(self.fonts, 'SET', x, rect.y + 12, unit=shown.unit, centered=not confirming)
if shown.max_text == '–':
draw_line(Profile.LARGE, rl.Vector2(center - 28, rect.y + 90), rl.Vector2(center + 28, rect.y + 90), 8, MUTED)
else:
size = 100
size *= min(1, (148 if confirming else 200) / max(1, self.fonts.measure(shown.max_text, FontRole.SET_SPEED, size).width))
left, right = self.fonts.horizontal_ink(shown.max_text, FontRole.SET_SPEED, size)
top, bottom = self.fonts.vertical_ink(shown.max_text, FontRole.SET_SPEED, size)
number_x = x + self.fonts.horizontal_ink('SET', FontRole.MEDIUM, 34)[0] - left if confirming else center - (left + right) / 2
draw_ink(self.fonts, shown.max_text, FontRole.SET_SPEED, size, number_x, rect.y + 90 - (bottom - top) / 2, text)
if shown.mode != 'limit_only' and (label := set_speed_status(state, Profile.LARGE, shown)):
confirming = manual_slc_confirmation(Profile.LARGE, state)
size = 24
left, right = self.fonts.horizontal_ink(label, FontRole.SEMI_BOLD, size)
size *= min(1, (148 if confirming else 200) / max(1, right - left))
left, right = self.fonts.horizontal_ink(label, FontRole.SEMI_BOLD, size)
x = rect.x + (PADDING + self.fonts.horizontal_ink('SET', FontRole.MEDIUM, 34)[0] - left if confirming else 120 - (left + right) / 2)
draw_ink(self.fonts, label, FontRole.SEMI_BOLD, size, x, rect.y + 136, text)
draw_set_speed(self.fonts, shown, rect.x + 92, rect.y, text)
if label := set_speed_status(state, Profile.LARGE, shown):
draw_set_status(self.fonts, label, rect.x + 92, rect.y, text)
self.sign.render(state, defer_confirmation=True)
if (shown.mode == 'limit_only' and not manual_slc_confirmation(Profile.LARGE, state) and
(bounds := speed_limit_bounds(Profile.LARGE, state)) is not None):
draw_ink(self.fonts, shown.unit, FontRole.NORMAL, 24, bounds[2], bounds[3] - 32)
if sources:
row = rl.Rectangle(rect.x, rect.y + max(height, SOURCE_TOP) + (GAP if separate else 0), WIDTH, SOURCE_ROW_HEIGHT)
if separate:
+80 -64
View File
@@ -21,6 +21,8 @@ IN_USE = rl.Color(128, 216, 166, 255)
OVERRIDE = rl.Color(214, 188, 240, 255)
REJECT = rl.Color(242, 139, 139, 255)
SOURCE_LABELS = {'dashboard': 'DASH', 'map': 'OSM', 'vision': 'VISION', 'mapbox': 'NAV'}
NUMBER_SIZE, HEADING_SIZE = 70, 26
HEADING_ROW, NUMBER_ROW, SOURCE_ROW = 25, 68, 120
def draw_text(fonts, text, role, size, x, y, color=INK):
@@ -45,24 +47,32 @@ def draw_ink(fonts, text, role, size, x, top, color=INK):
return fonts.measure(text, role, size).width, ink_bottom - ink_top
def draw_caption(fonts, label, x, top, color=MUTED, *, unit=None, size=None, centered=False):
large = fonts.profile == Profile.LARGE
label_size = size or (34 if large else 24)
unit_size, gap = ((32 if centered else 28), 12) if large else (20, 8)
if centered:
width = fonts.measure(label, FontRole.MEDIUM, label_size).width
if unit:
width += gap + fonts.measure(unit, FontRole.NORMAL, unit_size).width
x -= width / 2
width, height = draw_ink(fonts, label, FontRole.MEDIUM, label_size, x, top, color)
if unit:
unit_top, _ = fonts.vertical_ink(unit, FontRole.NORMAL, unit_size)
# Exclude the p descender when aligning mph to the caption baseline.
_, baseline = fonts.vertical_ink('mh' if unit == 'mph' else unit, FontRole.NORMAL, unit_size)
unit_width, _ = draw_ink(fonts, unit, FontRole.NORMAL, unit_size, x + width + gap,
top + height - (baseline - unit_top), color)
width += gap + unit_width
return width, height
def draw_speed_row(fonts, text, role, size, center, row, color, reference):
# Shared glyph references keep SET and SLC on the same baseline as values change.
left, right = fonts.horizontal_ink(text, role, size)
top, bottom = fonts.vertical_ink(reference, role, size)
draw_text(fonts, text, role, size, center - (left + right) / 2, row - (top + bottom) / 2, color)
def draw_set_speed(fonts, shown, center, top, color):
scale = LARGE_SIGN_SCALE if fonts.profile == Profile.LARGE else 1.
text_scale = scale * Profile.COMPACT.font_scale / fonts.profile.font_scale
draw_speed_row(fonts, 'SET', FontRole.SEMI_BOLD, HEADING_SIZE * text_scale, center,
top + HEADING_ROW * scale, color, 'H')
if shown.max_text == '–':
draw_line(fonts.profile, rl.Vector2(center - 20 * scale, top + NUMBER_ROW * scale),
rl.Vector2(center + 20 * scale, top + NUMBER_ROW * scale), 5 * scale, color)
else:
draw_speed_row(fonts, shown.max_text, FontRole.SET_SPEED, NUMBER_SIZE * text_scale, center,
top + NUMBER_ROW * scale, color, '0123456789')
def draw_set_status(fonts, label, center, top, color):
scale = LARGE_SIGN_SCALE if fonts.profile == Profile.LARGE else 1.
size = 22 * scale * Profile.COMPACT.font_scale / fonts.profile.font_scale
left, right = fonts.horizontal_ink(label, FontRole.MEDIUM, size)
size *= min(1, 108 * scale / max(1, right - left))
draw_speed_row(fonts, label, FontRole.MEDIUM, size, center, top + SOURCE_ROW * scale, color, label)
def speed_status(state, profile, shown):
@@ -155,11 +165,14 @@ class SpeedLimitWidget:
y + cy * scale - (top + bottom) / 2, color)
if state.slc_prompt_open:
context = (f'Now: {current_status}' + (f' {shown.unit}' if current_status != 'PEDAL' else '')
if current_status else 'New limit')
context = f'Now: {current_status}' if current_status else 'NEW LIMIT'
context_size = 26 if self.profile == Profile.LARGE else 24
context_size *= min(1, (width - 24) / max(1, ink_width(context, context_size)))
text(context, context_size, width / 2, 24, secondary)
label_size, number_size, detail_size, gap = (30, 58, 26, 8) if self.profile == Profile.LARGE else (26, 50, 24, 6)
# Reserve the full label height so capitalization does not shift either row.
heading_height = max(ink_height(label, label_size, FontRole.SEMI_BOLD) for label in ('Accept', 'Reject'))
number_height = ink_height(shown.proposal_text, number_size, FontRole.SET_SPEED)
for control in slc_controls(self.profile, state):
accent = (IN_USE if control.label == 'Accept' else REJECT) if control.enabled else MUTED
selected = control.enabled and pressed == control.request
@@ -169,70 +182,66 @@ class SpeedLimitWidget:
w, h = (control.bounds[2] - control.bounds[0]) / scale, (control.bounds[3] - control.bounds[1]) / scale
self.shapes.button(x + (left + w / 2) * scale, y + (top + h / 2) * scale, accent,
profile=self.profile, scale=scale, pressed=selected)
uw = ink_width(shown.unit, 20)
size = 60 if self.profile == Profile.LARGE else 52
padding, gap = (28, 6) if self.profile == Profile.LARGE else (24, 4)
size *= min(1, (w - padding - gap - uw) / max(1, ink_width(shown.proposal_text, size, FontRole.BOLD)))
nh, uh = ink_height(shown.proposal_text, size, FontRole.BOLD), ink_height(shown.unit, 20)
caption_height = ink_height('new limit', 22)
row_height = max(nh, uh, caption_height)
# Center each visible text group while keeping both action headings on one row.
label_y = top + (h - 8 - row_height) / 2
detail_bottom = label_y + ink_height(control.label, 37) / 2 + 8 + row_height
text(control.label, 37, left + w / 2, label_y, ink)
start = top + (h - heading_height - gap - number_height) / 2
text(control.label.upper(), label_size, left + w / 2, start + heading_height / 2, ink, FontRole.SEMI_BOLD)
detail_y = start + heading_height + gap + number_height / 2
if control.label == 'Accept':
# Bind the adjusted proposal to its action, not to the current override or source reading.
nw = ink_width(shown.proposal_text, size, FontRole.BOLD)
start = left + (w - nw - gap - uw) / 2
text(shown.proposal_text, size, start + nw / 2, detail_bottom - nh / 2, ink, FontRole.BOLD)
text(shown.unit, 20, start + nw + gap + uw / 2, detail_bottom - uh / 2, ink)
text(shown.proposal_text, number_size, left + w / 2, detail_y, ink, FontRole.SET_SPEED)
else:
text('new limit', 22, left + w / 2, detail_bottom - caption_height / 2, ink)
text('NEW LIMIT', detail_size, left + w / 2, detail_y, ink)
return
content_top = y + (12 if self.profile == Profile.LARGE else 8)
row_origin = (content_top - y) / scale
caption_row, hint_row = (112, 142) if self.profile == Profile.LARGE else (110, 136.75)
source_color = rl.Color(197, 212, 208, 255)
if current_status:
source += f' / {current_status}'
label_size = 22 * min(1, (width - 40) / max(1, ink_width(source, 22, FontRole.SEMI_BOLD)))
label_size = HEADING_SIZE * min(1, (width - 40) / max(1, ink_width(source, HEADING_SIZE, FontRole.SEMI_BOLD)))
label_width = ink_width(source, label_size, FontRole.SEMI_BOLD)
target = state.speed_limit.pending_adjusted_limit_mps
if not positive(target):
target = displayed_adjusted_limit_mps(state.speed_limit)
direction = (-1 if target > state.speed_mps else 1) if target is not None and state.speed_mps is not None and target != state.speed_mps else 0
start = (width - label_width - (19.2 if direction else 0)) / 2
text(source, label_size, start + label_width / 2, 22, source_color, FontRole.SEMI_BOLD)
draw_speed_row(self.fonts, source, FontRole.SEMI_BOLD, text_size(label_size),
x + (start + label_width / 2) * scale, content_top + HEADING_ROW * scale, source_color, 'H')
if direction:
self.shapes.arrow('compact', x + (start + label_width + 12.6) * scale, y + 22.5 * scale,
self.shapes.arrow('compact', x + (start + label_width + 12.6) * scale, content_top + (HEADING_ROW + .5) * scale,
direction, source_color, scale=1.1 * scale)
nw = ink_width(shown.posted_text, 64, FontRole.BOLD)
nw = ink_width(shown.posted_text, NUMBER_SIZE, FontRole.SET_SPEED)
ow = ink_width(shown.offset_text, 32) if shown.offset_text else 0
start = (width - nw - (10 + ow if ow else 0)) / 2
# Align the posted value and offset at their visible lower edge.
number_top, number_bottom = self.fonts.vertical_ink(shown.posted_text, FontRole.BOLD, text_size(64))
text(shown.posted_text, 64, start + nw / 2, 84 - (number_bottom - number_top) / scale / 2, white, FontRole.BOLD)
draw_speed_row(self.fonts, shown.posted_text, FontRole.SET_SPEED, text_size(NUMBER_SIZE),
x + (start + nw / 2) * scale, content_top + NUMBER_ROW * scale, white, '0123456789')
if ow:
offset_text = cast(str, shown.offset_text)
ref_top, ref_bottom = self.fonts.vertical_ink('0123456789', FontRole.SET_SPEED, text_size(NUMBER_SIZE))
_, ink_bottom = self.fonts.vertical_ink(shown.posted_text, FontRole.SET_SPEED, text_size(NUMBER_SIZE))
number_bottom = row_origin + NUMBER_ROW + (ink_bottom - (ref_top + ref_bottom) / 2) / scale
top, bottom = self.fonts.vertical_ink(offset_text, FontRole.MEDIUM, text_size(32))
text(offset_text, 32, start + nw + 10 + ow / 2, 84 - (bottom - top) / scale / 2, secondary)
label = 'New Limit' if slc_accept_available(state) else 'Unavailable'
text(offset_text, 32, start + nw + 10 + ow / 2, number_bottom - (bottom - top) / scale / 2, secondary)
label = 'NEW LIMIT' if slc_accept_available(state) else 'Unavailable'
caption = ink_width(label, 30, FontRole.SEMI_BOLD)
cx = (width - caption - 10) / 2
strength = self.pulse.confirmation_strength(now)
accent = rl.Color(214, round(188 + 35 * strength), 240, 255)
text(label, 30, cx + caption / 2, 104, accent, FontRole.SEMI_BOLD)
ax, ay = x + (cx + caption + 8) * scale, y + 104 * scale
text(label, 30, cx + caption / 2, row_origin + caption_row, accent, FontRole.SEMI_BOLD)
ax, ay = x + (cx + caption + 8) * scale, content_top + caption_row * scale
draw_line(self.profile, rl.Vector2(ax - 2 * scale, ay - 4 * scale), rl.Vector2(ax + 2 * scale, ay), 1.5 * scale, accent)
draw_line(self.profile, rl.Vector2(ax + 2 * scale, ay), rl.Vector2(ax - 2 * scale, ay + 4 * scale), 1.5 * scale, accent)
if state.slc_wheel_confirmation_available:
accept, reject = ink_width('Accept', 26), ink_width('Reject', 26)
hint_size = 23
accept, reject = ink_width('ACCEPT', hint_size), ink_width('REJECT', hint_size)
start = (width - 18 - 4 - accept - 10 - 18 - 4 - reject) / 2
for left, plus in ((start, True), (start + 18 + 4 + accept + 10, False)):
self.shapes.wheel_key(plus, x + (left + 9) * scale, y + 135.5 * scale, scale=scale)
self.shapes.wheel_key(plus, x + (left + 9) * scale, content_top + hint_row * scale, scale=scale)
hint_ink = rl.Color(210, 216, 216, 255)
text('Accept', 26, start + 22 + accept / 2, 135.5, hint_ink if slc_accept_available(state) else MUTED)
text('Reject', 26, start + 18 + 4 + accept + 10 + 22 + reject / 2, 135.5, hint_ink)
text('ACCEPT', hint_size, start + 22 + accept / 2, row_origin + hint_row, hint_ink if slc_accept_available(state) else MUTED)
text('REJECT', hint_size, start + 18 + 4 + accept + 10 + 22 + reject / 2, row_origin + hint_row, hint_ink)
else:
text('Tap to review', 22, width / 2, 135.5, secondary)
text('Tap to review', 22, width / 2, row_origin + hint_row, secondary)
def _sign(self, state: OnroadState, bounds, now: float) -> None:
shown, obs = resolve_unified_speed(state), state.speed_limit
@@ -282,21 +291,24 @@ class SpeedLimitWidget:
label = 'Override' if shown.override_text is not None else 'Limit'
active_text = active_text or shown.slc_target_text or shown.posted_text
target_rows = (('Pedal', FontRole.SEMI_BOLD, 44, 68, OVERRIDE),) if pedal else (
(label, FontRole.SEMI_BOLD, 34, 25, OVERRIDE if label == 'Override' else white),
(active_text, FontRole.SET_SPEED if self.profile == Profile.LARGE else FontRole.BOLD, 64, 68, white))
(label, FontRole.SEMI_BOLD, HEADING_SIZE, HEADING_ROW, OVERRIDE if label == 'Override' else white),
(active_text, FontRole.SET_SPEED, NUMBER_SIZE, NUMBER_ROW, white))
posted = shown.posted_text + (f' {shown.offset_text}' if shown.offset_text else '')
detail = f'{heading} {posted}' if pedal or shown.override_text is not None or active_text != shown.posted_text else heading
left, right, _, _ = ink(detail, FontRole.MEDIUM, 22)
source_rows = ((detail, FontRole.MEDIUM, 22, 120, secondary),) if detail == heading or right - left <= 108 else (
(heading, FontRole.SEMI_BOLD, 18, 104, secondary), (posted, FontRole.MEDIUM, 20, 125, secondary))
source_rows = ((detail, FontRole.MEDIUM, 22, SOURCE_ROW, secondary),) if detail == heading or right - left <= 108 else (
(heading, FontRole.SEMI_BOLD, 18, 106, secondary), (posted, FontRole.MEDIUM, 20, 125, secondary))
for value, role, size, cy, color in (*target_rows, *source_rows):
left, right, _, _ = ink(value, role, size)
size *= min(1, 108 / max(1, right - left))
_, _, a, b = ink(value, role, size)
centered(value, role, size, cy - (b - a) / 2, color)
if role == FontRole.SET_SPEED or value in ('Limit', 'Override'):
draw_speed_row(self.fonts, value, role, size * self.text_scale, x + center_x * scale, top + cy * scale,
color, '0123456789' if role == FontRole.SET_SPEED else 'H')
else:
centered(value, role, size, cy - (b - a) / 2, color)
return
large_steady = self.profile == Profile.LARGE and not vienna and not shown.pending
color = white
if vienna:
x, top = x + 5 * scale, top + 5 * scale
@@ -305,7 +317,7 @@ class SpeedLimitWidget:
else:
self.shapes.outline(x + center_x * scale, top + sign_center_y * scale, IN_USE if shown.limit_in_use else white)
heading_size = 28 if not vienna else 18
heading_size = HEADING_SIZE if not vienna else 18
if vienna and self.profile == Profile.LARGE and not shown.pending:
heading_size = 21
left, right, a, b = ink(heading, FontRole.SEMI_BOLD, heading_size)
@@ -314,7 +326,7 @@ class SpeedLimitWidget:
left, right, a, b = ink(heading, FontRole.SEMI_BOLD, heading_size)
# Lift the Vienna stack to keep the offset clear of the bottom rim.
lift = (12 if shown.limit_in_use else 7 if shown.offset_text else 2) if vienna else 0
header_y = 47 - lift if vienna else 26
header_y = 47 - lift if vienna else HEADING_ROW
arrow_scale = 1.
if vienna:
# Fit the heading and arrow inside the circle at the top of their ink.
@@ -335,10 +347,10 @@ class SpeedLimitWidget:
self.shapes.arrow('compact', round(x + (header_left + heading_width + 6 + arrow_width / 2) * scale),
round(top + header_y * scale), arrow_direction, color, scale=scale * arrow_scale)
role = FontRole.SET_SPEED if large_steady else FontRole.BOLD
size = 58 if vienna else 64
role = FontRole.BOLD if vienna else FontRole.SET_SPEED
size = 58 if vienna else NUMBER_SIZE
offset_size = 30 if vienna else 27
number_center = (73 if shown.offset_text else 80) - lift if vienna else (68 if shown.offset_text else 80)
number_center = (73 if shown.offset_text else 80) - lift if vienna else NUMBER_ROW
while True:
left, right, a, b = ink(shown.posted_text, role, size)
width, height = right - left, b - a
@@ -356,7 +368,11 @@ class SpeedLimitWidget:
size *= .95
if vienna:
offset_size *= .95
centered(shown.posted_text, role, size, number_top, color)
if vienna:
centered(shown.posted_text, role, size, number_top, color)
else:
draw_speed_row(self.fonts, shown.posted_text, role, size * self.text_scale, x + center_x * scale,
top + NUMBER_ROW * scale, color, '0123456789')
if shown.offset_text:
if not vienna:
left, right, a, b = ink(shown.offset_text, FontRole.MEDIUM, offset_size)
+104 -47
View File
@@ -71,7 +71,7 @@ def test_approved_limit_offset_and_action_sizes_fit_actual_fonts():
rl.close_window()
def test_large_sign_is_secondary_to_set_speed_at_the_saved_position():
def test_large_set_and_limit_share_typography_at_the_saved_position():
from unittest.mock import patch
from openpilot.starpilot.ui.onroad_large_widgets import UnifiedSpeedWidget
from openpilot.starpilot.ui.layout_preview_renderer import sample_state
@@ -99,7 +99,7 @@ def test_large_sign_is_secondary_to_set_speed_at_the_saved_position():
assert right-left <= 108*LARGE_SIGN_SCALE
assert abs(posted[3] + (left + right) / 2 - (418 + SIGN_WIDTH/2*LARGE_SIGN_SCALE)) <= .5
a, b = fonts.vertical_ink('130', FontRole.SET_SPEED, posted[2])
assert 30 <= b - a <= 64
assert 60 <= b - a <= 70
assert abs(posted[4]+(a+b)/2-(95+68*LARGE_SIGN_SCALE)) <= .5
assert {'Limit', '120 +10'} <= {call[0] for call in calls}
set_calls = [call for call in calls if call[1] == FontRole.SET_SPEED and call[3] < 388]
@@ -108,19 +108,20 @@ def test_large_sign_is_secondary_to_set_speed_at_the_saved_position():
text, role, size, x, y, _ = set_calls[0]
assert fonts.measure(text, role, size).width <= 200
left, right = fonts.horizontal_ink(text, role, size)
assert x + (left + right) / 2 == pytest.approx(308)
assert x + (left + right) / 2 == pytest.approx(280)
assert x + right + 20 <= 418
top, bottom = fonts.vertical_ink(text, role, size)
assert bottom - top == pytest.approx(77, abs=1)
assert y + (top + bottom) / 2 == pytest.approx(185)
assert next(call for call in calls if call[0] == 'SET')[2] == 34
unit = next(call for call in calls if call[0] == ('km/h' if metric else 'mph'))
assert unit[2] == 32
assert unit[4] + fonts.vertical_ink(unit[0], unit[1], unit[2])[1] + 6 <= y + top
assert (role, size, y) == (posted[1], posted[2], posted[4])
heading = next(call for call in calls if call[0] == 'SET')
limit = next(call for call in calls if call[0] == 'Limit')
assert (heading[1:3], heading[4]) == (limit[1:3], limit[4])
assert not any('mph' in call[0] or 'km/h' in call[0] for call in calls)
if show_set and value is None:
start, end, stroke, color = lines.call_args.args
assert (start.x, start.y, end.x, end.y, stroke) == (280, 185, 336, 185, 8)
assert (color.r, color.g, color.b, color.a) == (190, 208, 202, 255)
assert (start.x, start.y, end.x, end.y, stroke) == pytest.approx(
(280 - 20 * LARGE_SIGN_SCALE, 95 + 68 * LARGE_SIGN_SCALE,
280 + 20 * LARGE_SIGN_SCALE, 95 + 68 * LARGE_SIGN_SCALE, 5 * LARGE_SIGN_SCALE))
assert (color.r, color.g, color.b, color.a) == (255, 255, 255, 255)
assert all(call[0] not in ('–', '?') for call in calls)
else:
lines.assert_not_called()
@@ -129,6 +130,51 @@ def test_large_sign_is_secondary_to_set_speed_at_the_saved_position():
rl.close_window()
@pytest.mark.parametrize('pending', [False, True])
def test_set_and_slc_match_font_and_baseline_on_both_displays(pending):
from unittest.mock import patch
from openpilot.starpilot.ui.onroad_compact_widgets import CompactHudRenderer
from openpilot.starpilot.ui.onroad_large_widgets import UnifiedSpeedWidget
from openpilot.starpilot.ui.tests.test_unified_speed_native import state
owns_context = not rl.is_window_ready()
if owns_context:
rl.init_window(128, 128, 'Shared SET and SLC typography')
try:
for profile in Profile:
with BitmapFonts(profile, default_font_directory()) as fonts:
widget = UnifiedSpeedWidget(fonts) if profile == Profile.LARGE else CompactHudRenderer(fonts, Path('/unused'))
try:
for value in (9, 65, 128):
current = state(cruise_kph=140)
current = replace(current, speed_limit=replace(current.speed_limit, speed_limit_mps=value / 3.6,
pending_speed_limit_mps=value / 3.6 if pending else None,
pending_adjusted_limit_mps=value / 3.6 if pending else None,
presentation_adjusted_limit_mps=value / 3.6,
limiting_max_set=True, effective_cluster_target_mps=value / 3.6))
calls = []
with patch.object(fonts, 'draw', lambda *args, sink=calls: sink.append(args)):
if isinstance(widget, UnifiedSpeedWidget):
widget.render(rl.Rectangle(0, 0, 2160, 1080), current)
else:
with patch.object(widget, '_set_speed_opacity', return_value=1.):
widget.render_max_speed(current)
widget._speed_limit_sign(current)
if pending:
sign = widget.sign if isinstance(widget, UnifiedSpeedWidget) else widget._speed_limit_widget
sign.render_confirmation(current)
rows = {row[0]: row for row in calls if row[-1].a == 255}
for a, b in (('SET', 'OSM' if pending else 'Limit'), ('140', str(value))):
assert rows[a][1] == rows[b][1]
assert rows[a][2] == pytest.approx(rows[b][2])
assert rows[a][4] == pytest.approx(rows[b][4])
assert not any('mph' in text or 'km/h' in text for text in rows)
finally:
widget.close()
finally:
if owns_context:
rl.close_window()
def test_pending_confirmation_keeps_three_digit_set_speed_clear():
from unittest.mock import patch
from openpilot.starpilot.ui.onroad_large_widgets import UnifiedSpeedWidget
@@ -190,18 +236,17 @@ def test_status_below_set_has_real_ink_clearance_and_fits_its_widget():
saved = placement(current.customization, profile, key)
width, height = widget_size(current.customization, profile, key)
assert caption[4] + fonts.vertical_ink(*caption[:3])[1] <= saved['y'] + height - 12
assert 18 <= caption[2] <= 24
scale = LARGE_SIGN_SCALE if profile == Profile.LARGE else 1
assert 16 <= caption[2] * profile.font_scale / Profile.COMPACT.font_scale / scale <= 22 + 1e-9
left, right = fonts.horizontal_ink(*caption[:3])
number_left, number_right = fonts.horizontal_ink(*number[:3])
if profile == Profile.COMPACT:
assert saved['x'] + 8 <= caption[3] + left < caption[3] + right <= saved['x'] + width - 8
assert caption[3] + (left + right) / 2 == pytest.approx(saved['x'] + width / 2)
assert number[3] + (number_left + number_right) / 2 == pytest.approx(saved['x'] + width / 2)
elif status != 'PEDAL':
label = next(row for row in rows if row[0] == 'SET')
label_left = label[3] + fonts.horizontal_ink(*label[:3])[0]
assert caption[3] + left == pytest.approx(label_left)
assert number[3] + number_left == pytest.approx(label_left)
else:
assert caption[3] + (left + right) / 2 == pytest.approx(saved['x'] + 92)
assert number[3] + (number_left + number_right) / 2 == pytest.approx(saved['x'] + 92)
widget.close()
finally:
if owns_context:
@@ -388,7 +433,7 @@ def test_compact_preview_sprite_keeps_the_full_confirmation_and_override_sign():
rl.init_window(128, 128, 'SLC preview crop fixture')
canvas = _Canvas(Profile.COMPACT)
try:
for y, scene, vienna in product((16, 20), ('slc_pending', 'slc_retained', 'slc_display'), (False, True)):
for y, scene, vienna in product((12, 16), ('slc_notice', 'slc_pending', 'slc_retained', 'slc_display'), (False, True)):
document = default_document()
document['layouts']['compact']['speed_limit']['y'] = y
assert validate_document(document)['layouts']['compact']['speed_limit']['y'] == y
@@ -408,8 +453,8 @@ def test_compact_preview_sprite_keeps_the_full_confirmation_and_override_sign():
finally:
rl.unload_image(image)
assert np.array_equal(direct[..., 3], np.asarray(rebuilt)[..., 3])
if scene == 'slc_pending':
assert (slot['offsetX'], slot['offsetY'], slot['width'], slot['height']) == (-172, -8, 328, 152)
if scene in ('slc_notice', 'slc_pending'):
assert (slot['offsetX'], slot['offsetY'], slot['width'], slot['height']) == (-172, -8, 328, 159)
else:
assert (slot['offsetX'], slot['offsetY'], slot['width'], slot['height']) == (-2, 0, SIGN_WIDTH+4, SIGN_HEIGHT)
assert np.any(direct[..., 3]), (y, scene)
@@ -448,7 +493,7 @@ def test_large_sources_and_confirmation_text_have_clearance_below_both_signs():
view._slc_actions(current)
foreground = [call for call in calls if call[-1].a == 255]
action_bottom = max(y+fonts.vertical_ink(text, role, size)[1]
for text, role, size, _, y, _ in foreground if text in ('Accept', 'Reject'))
for text, role, size, _, y, _ in foreground if text in ('ACCEPT', 'REJECT'))
source_top = min(y+fonts.vertical_ink(text, role, size)[0]
for text, role, size, _, y, _ in foreground if text in ('Dash', 'Map', 'Vision'))
assert action_bottom+8 <= source_top
@@ -608,7 +653,7 @@ def test_both_sign_styles_contain_text_and_arrows_at_each_profile_scale():
rl.close_window()
def test_sign_profiles_share_layout_and_snap_only_final_pixels():
def test_sign_profiles_share_layout_and_keep_aligned_numbers():
from unittest.mock import Mock, patch
from openpilot.starpilot.ui.speed_limit_widget import SpeedLimitWidget
from openpilot.starpilot.ui.tests.test_unified_speed_native import state
@@ -631,7 +676,8 @@ def test_sign_profiles_share_layout_and_snap_only_final_pixels():
with patch.object(fonts, 'draw', lambda *args, sink=calls: sink.append(args)):
widget.render(current)
calls = [call for call in calls if call[-1].a == 255]
assert all(float(call[3]).is_integer() and float(call[4]).is_integer() for call in calls)
assert all(float(call[3]).is_integer() and float(call[4]).is_integer()
for call in calls if call[1] != FontRole.SET_SPEED)
if not vienna:
assert calls[0][2]/widget.text_scale <= 28
elif fonts.profile == Profile.LARGE and not pending:
@@ -644,10 +690,8 @@ def test_sign_profiles_share_layout_and_snap_only_final_pixels():
small_widget, small_calls = drawn[0]
large_widget, large_calls = drawn[1]
assert [call[0] for call in small_calls] == [call[0] for call in large_calls]
if not vienna and not pending:
assert small_calls[1][1] == FontRole.BOLD
assert large_calls[1][1] == FontRole.SET_SPEED
continue
if not vienna:
assert small_calls[1][1] == large_calls[1][1] == FontRole.SET_SPEED
start = int(vienna and not pending)
for a, b in zip(small_calls[start:], large_calls[start:], strict=True):
assert abs(a[2]/small_widget.text_scale-b[2]/large_widget.text_scale) < 1e-9
@@ -673,7 +717,7 @@ def test_v5_geometry_migrates_and_keeps_custom_actions_independent():
doc['layouts']['large']['cruise_limits'].update(x=1550, y=638)
doc['layouts']['large']['speed_limit_actions'].update(x=1550, y=954)
migrated = validate_document(doc)
assert migrated['version'] == 13
assert migrated['version'] == 14
assert migrated['layouts']['large']['cruise_limits'] == default_document()['layouts']['large']['cruise_limits']
assert migrated['layouts']['large']['speed_limit_actions'] == {'x': 1550, 'y': 882, 'enabled': True}
assert migrated['speedSources'] is True
@@ -720,10 +764,12 @@ def test_committed_projection_v1_right_edge_is_preserved_and_clamped():
def test_chooser_binds_the_target_to_accept_and_keeps_all_native_ink_inside_its_control():
@pytest.mark.parametrize('wheel_confirmation', [False, True])
def test_chooser_binds_the_target_to_accept_and_keeps_all_native_ink_inside_its_control(wheel_confirmation):
from unittest.mock import patch
from openpilot.starpilot.ui.large_speed_geometry import CONFIRMATION_SCALE
from openpilot.starpilot.ui.speed_limit_widget import SpeedLimitWidget
from openpilot.starpilot.ui.speed_limit_shapes import ATLAS_PATH, SPRITES, SPRITE_DENSITIES
from openpilot.starpilot.ui.tests.test_slc_ui_actions import state
owns_context = not rl.is_window_ready()
if owns_context:
@@ -746,7 +792,7 @@ def test_chooser_binds_the_target_to_accept_and_keeps_all_native_ink_inside_its_
pending_adjusted_limit_mps=(posted + offset) / factor,
presentation_adjusted_limit_mps=(posted + offset) / factor)
current = replace(current, speed_limit=obs, metric=metric, show_slc_offset=show_offset,
slc_wheel_confirmation_available=True)
slc_wheel_confirmation_available=wheel_confirmation)
notice_calls = []
with patch.object(fonts, 'draw', lambda *args, sink=notice_calls: sink.append(args)):
widget.render_confirmation(current)
@@ -764,8 +810,8 @@ def test_chooser_binds_the_target_to_accept_and_keeps_all_native_ink_inside_its_
right = boxes[f'{offset:+d}'][2] if show_offset else number[2]
assert (number[0] + right) / 2 == pytest.approx((notice_bounds[0] + notice_bounds[2]) / 2)
assert boxes['VISION'][3] + 6 * scale <= number[1]
assert number[3] + 6 * scale <= boxes['New Limit'][1]
assert boxes['New Limit'][3] + 6 * scale <= boxes['Accept'][1]
assert number[3] + 6 * scale <= boxes['NEW LIMIT'][1]
assert boxes['NEW LIMIT'][3] + 6 * scale <= boxes['ACCEPT' if wheel_confirmation else 'Tap to review'][1]
if show_offset:
assert boxes[f'{offset:+d}'][3] == pytest.approx(number[3])
else:
@@ -781,9 +827,8 @@ def test_chooser_binds_the_target_to_accept_and_keeps_all_native_ink_inside_its_
with patch.object(fonts, 'draw', lambda *args, sink=calls: sink.append(args)), patch.object(rl, 'draw_rectangle_rounded'):
widget.render_confirmation(current)
calls = [call for call in calls if call[-1].a == 255]
context = 'Now: OVERRIDE 128 km/h' if posted == 110 else 'New limit'
assert [call[0] for call in calls] == [context, 'Accept', str(posted + offset),
'km/h' if metric else 'mph', 'Reject', 'new limit']
context = 'Now: OVERRIDE 128' if posted == 110 else 'NEW LIMIT'
assert [call[0] for call in calls] == [context, 'ACCEPT', str(posted + offset), 'REJECT', 'NEW LIMIT']
bounds = speed_limit_bounds(profile, current)
assert bounds is not None
for text, role, size, x, y, _ in calls[:1]:
@@ -792,31 +837,43 @@ def test_chooser_binds_the_target_to_accept_and_keeps_all_native_ink_inside_its_
assert x + left >= bounds[0] + 10 * scale
assert x + right <= bounds[2] - 10 * scale
assert bounds[1] + 8 * scale <= y + top < y + bottom <= bounds[1] + 40 * scale
heading_centers = []
for control, content in zip(slc_controls(profile, current), (calls[1:4], calls[4:]), strict=True):
# Measure from the painted outline, which is smaller than the touch area.
name = f'button_{profile}'
sx, sy, sw, sh = SPRITES[name]
with Image.open(ATLAS_PATH) as atlas:
alpha = np.asarray(atlas.crop((sx, sy, sx + sw, sy + sh)).getchannel('A'))
horizontal = np.flatnonzero(alpha[sh // 2])
vertical = np.flatnonzero(alpha[:, sw // 2])
inner_left, inner_right = horizontal[horizontal < sw / 2][-1] + 1, horizontal[horizontal > sw / 2][0]
inner_top, inner_bottom = vertical[vertical < sh / 2][-1] + 1, vertical[vertical > sh / 2][0]
density = SPRITE_DENSITIES[name]
row_centers = []
for control, content in zip(slc_controls(profile, current), (calls[1:3], calls[3:]), strict=True):
label_bottom = 0
boxes = []
cx, cy = (control.bounds[0] + control.bounds[2]) / 2, (control.bounds[1] + control.bounds[3]) / 2
interior = (cx + (inner_left - sw / 2) * scale / density, cy + (inner_top - sh / 2) * scale / density,
cx + (inner_right - sw / 2) * scale / density, cy + (inner_bottom - sh / 2) * scale / density)
contour = 1.15 if profile == Profile.LARGE else 1.
padding = (14 if profile == Profile.LARGE else 9) * scale
for text, role, size, x, y, _ in content:
left, right = fonts.horizontal_ink(text, role, size)
top, bottom = fonts.vertical_ink(text, role, size)
boxes.append((x + left, y + top, x + right, y + bottom))
assert x + left - control.bounds[0] >= 12 * scale - 1e-6
assert control.bounds[2] - x - right >= 12 * scale - 1e-6
assert control.bounds[1] + 10 * scale <= y + top < y + bottom <= control.bounds[3] - 10 * scale
if text == control.label:
assert min(x + left - interior[0], interior[2] - x - right,
y + top - interior[1], interior[3] - y - bottom) - contour >= padding
if text == control.label.upper():
label_bottom = y + bottom
assert (bottom - top) / scale * 25.4 / 300 >= 2.8
assert (bottom - top) / scale * 25.4 / 300 >= (1.7 if profile == Profile.LARGE else 1.5)
else:
assert y + top >= label_bottom + 6 * scale
cx, cy = (control.bounds[0] + control.bounds[2]) / 2, (control.bounds[1] + control.bounds[3]) / 2
assert (boxes[0][0] + boxes[0][2]) / 2 == pytest.approx(cx)
assert (min(box[0] for box in boxes[1:]) + max(box[2] for box in boxes[1:])) / 2 == pytest.approx(cx)
assert (min(box[1] for box in boxes) + max(box[3] for box in boxes)) / 2 == pytest.approx(cy)
heading_centers.append((boxes[0][1] + boxes[0][3]) / 2)
row_centers.append(tuple((box[1] + box[3]) / 2 for box in boxes))
if control.label == 'Accept':
assert boxes[1][3] == pytest.approx(boxes[2][3]), 'Speed and unit share a lower edge'
assert abs((boxes[0][1] + boxes[1][3]) / 2 - cy) < 3 * scale
assert (boxes[1][3] - boxes[1][1]) / scale * 25.4 / 300 >= (3.3 if profile == Profile.LARGE else 2.7)
assert heading_centers[0] == pytest.approx(heading_centers[1])
assert row_centers[0] == pytest.approx(row_centers[1])
finally:
widget.close()
finally:
@@ -54,7 +54,7 @@ def test_v12_portrait_upgrade_preserves_preferences_and_is_idempotent():
before = copy.deepcopy(old)
updated = validate_document(old)
expected = copy.deepcopy(old)
expected['version'] = 13
expected['version'] = 14
expected['layouts']['compact']['speed_limit'].update(x=320, y=16)
assert old == before and updated == expected
assert validate_document(updated) == updated
@@ -74,6 +74,33 @@ def test_v12_edge_placements_remain_bounded_even_with_confirmation_disabled():
assert all(not layout['speed_limit_actions']['enabled'] for layout in updated['layouts'].values())
@pytest.mark.parametrize('sign_y', [12, 16, 20])
def test_v13_notice_upgrade_preserves_preferences_and_only_moves_a_conflicting_sign(sign_y):
import copy
old = default_document()
old['version'] = 13
old['layouts']['compact']['speed_limit']['y'] = sign_y
old['layouts']['large']['current_speed'].update(x=700, enabled=False)
old['widgetColors']['large']['current_speed'] = {'text': '#ABCDEF80'}
old['reduceMotion'] = True
before = copy.deepcopy(old)
updated = validate_document(old)
expected = copy.deepcopy(old)
expected['version'] = 14
expected['layouts']['compact']['speed_limit']['y'] = min(sign_y, 16)
assert old == before and updated == expected
assert validate_document(updated) == updated
def test_compact_notice_is_reserved_even_when_the_shorter_buttons_would_fit():
document = default_document()
document['layouts']['compact']['speed_limit']['y'] = 20
with pytest.raises(ValueError, match='confirmation'):
validate_document(document)
document['layouts']['compact']['speed_limit_actions']['enabled'] = False
assert validate_document(document) == document
@pytest.mark.parametrize('separate', [False, True])
def test_v12_source_attachment_follows_the_new_row_without_moving_custom_actions(separate):
old = default_document()
@@ -165,7 +192,7 @@ def test_v7_speed_layout_migration_is_bounded_non_mutating_and_idempotent():
original = copy.deepcopy(document)
migrated = validate_document(document)
assert document == original
assert migrated['version'] == 13 and validate_document(migrated) == migrated
assert migrated['version'] == 14 and validate_document(migrated) == migrated
assert migrated['layouts']['large']['cruise_limits']['x'] == 1437
assert migrated['layouts']['large']['speed_limit_actions'] == {'x': 1510, 'y': 882, 'enabled': True}
assert migrated['layouts']['large']['current_speed'] == original['layouts']['large']['current_speed']
@@ -195,7 +222,7 @@ def test_v8_restores_only_compact_speed_defaults_and_preserves_customizations(cu
original = copy.deepcopy(document)
migrated = validate_document(document)
assert document == original
assert migrated['version'] == 13 and validate_document(migrated) == migrated
assert migrated['version'] == 14 and validate_document(migrated) == migrated
assert migrated['layouts']['large'] == document['layouts']['large']
assert migrated['widgetColors'] == document['widgetColors']
assert migrated['widgetOrder'] == document['widgetOrder']
@@ -236,7 +263,7 @@ def test_v8_actionless_layout_preserves_customizations_when_adding_compact_actio
original = copy.deepcopy(document)
migrated = validate_document(document)
assert document == original
assert migrated['version'] == 13 and validate_document(migrated) == migrated
assert migrated['version'] == 14 and validate_document(migrated) == migrated
assert migrated['layouts']['large'] == {**original['layouts']['large'], 'speed_limit_actions': large_actions}
assert migrated['layouts']['compact']['speed_limit'] == default_document()['layouts']['compact']['speed_limit']
assert migrated['layouts']['compact']['speed_limit_actions'] == {'x': 146, 'y': 44, 'enabled': True}
@@ -281,7 +308,7 @@ def test_protected_compact_actions_reject_actual_overlap(widget, x, y):
validate_document(document)
@pytest.mark.parametrize("widget,x,y", [("speed_limit", 320, 23), ("max_speed", 0, 0), ("steering_wheel", 78, 174)])
@pytest.mark.parametrize("widget,x,y", [("speed_limit", 320, 16), ("max_speed", 0, 0), ("steering_wheel", 78, 174)])
def test_protected_zone_edge_touch_is_valid(widget, x, y):
document = default_document()
document["layouts"]["compact"][widget].update(x=x, y=y)
@@ -355,7 +382,7 @@ def test_v3_layout_migrates_wheel_size_without_changing_saved_placement_or_color
for profile in ("large", "compact"):
del document["layouts"][profile]["steering_wheel"]["size"]
migrated = validate_document(document)
assert migrated["version"] == 13
assert migrated["version"] == 14
assert migrated["palette"] == document["palette"]
assert migrated["layouts"]["large"]["steering_wheel"] == {"x": 1300, "y": 100, "enabled": False, "size": 192}
assert migrated["layouts"]["compact"]["steering_wheel"] == {"x": 90, "y": 130, "enabled": True, "size": 50}
@@ -407,7 +434,7 @@ def test_malformed_pre_action_cruise_placement_is_rejected():
def test_duplicate_key_cannot_apply_otherwise_valid_placement():
document = default_document()
document["layouts"]["large"]["current_speed"]["x"] = 100
raw = json.dumps(document).replace('"version": 13', '"version": 11, "version": 11').encode()
raw = json.dumps(document).replace('"version": 14', '"version": 11, "version": 11').encode()
with patch("openpilot.starpilot.ui.onroad_customization.read_saved", return_value=(raw, True)):
assert read_customization(Mock()) == default_document()
@@ -423,7 +450,7 @@ def test_v9_large_sign_migration_preserves_customizations_and_clamps_bottom_edge
before = deepcopy(old)
updated = validate_document(old)
assert old == before
assert updated['version'] == 13
assert updated['version'] == 14
assert updated['layouts']['large']['cruise_limits']['y'] == default_document()['layouts']['large']['cruise_limits']['y']
assert updated['layouts']['large']['speed_limit_actions'] == before['layouts']['large']['speed_limit_actions']
assert updated['layouts']['compact'] == before['layouts']['compact']
@@ -533,7 +560,7 @@ def test_large_draw_origins_and_wheel_input_move_together():
with patch("openpilot.starpilot.ui.onroad_large_widgets.draw_control_card") as card, patch.object(rl, "draw_line_ex"):
UnifiedSpeedWidget(Mock(spec=BitmapFonts, **vars(fonts))).render(content, state)
card.assert_not_called()
assert foreground_calls(fonts.draw)[0].args[3:5] == (292, 107)
assert foreground_calls(fonts.draw)[0].args[3:5] == pytest.approx((275, 117.14), abs=.01)
CurrentSpeedHud(Mock(spec=BitmapFonts, **vars(fonts))).render(content, state)
assert foreground_calls(fonts.draw)[-2].args[3:5] == (965, 112)
wheel = SteeringWheelWidget(Path('/unused'))
@@ -566,7 +593,7 @@ def test_frozen_default_draw_geometry_and_neutral_colors():
UnifiedSpeedWidget(Mock(spec=BitmapFonts, **vars(fonts))).render(content, state)
card.assert_not_called()
assert fonts.draw.call_args_list[0].args[0] == 'SET'
assert foreground_calls(fonts.draw)[0].args[3:5] == (192, 87)
assert foreground_calls(fonts.draw)[0].args[3:5] == pytest.approx((175, 97.14), abs=.01)
CurrentSpeedHud(Mock(spec=BitmapFonts, **vars(fonts))).render(content, state)
assert foreground_calls(fonts.draw)[-2].args[3:5] == (925, 72)
assert foreground_calls(fonts.draw)[-1].args[3:5] == pytest.approx((925, 215.2))
@@ -610,7 +637,7 @@ def test_compact_drag_keeps_animation_state_and_separate_origins():
with patch.object(hud, "_speed_limit_sign"), patch.object(rl, "draw_circle_gradient"), patch.object(rl, "draw_texture_pro") as wheel:
hud.render(state)
hud.render(state)
assert foreground_calls(fonts.draw)[-1].args[3:5] == (125, 56)
assert foreground_calls(fonts.draw)[-1].args[3:5] == (138, 69.5)
rect = wheel.call_args.args[2]
assert (rect.x, rect.y) == (115, 175)
alpha = hud._set_speed_alpha.x
@@ -679,7 +706,7 @@ def test_v1_palette_positions_and_visibility_migrate_without_mutating_source():
original = copy.deepcopy(old)
result = decode_document(json.dumps(old).encode())
assert old == original
assert result['version'] == 13
assert result['version'] == 14
assert result['palette'] == old['palette']
for profile, layout in old['layouts'].items():
for key, position in layout.items():
@@ -219,7 +219,7 @@ def test_pending_confirmation_omits_redundant_speed_heading():
with patch.object(rl, 'draw_line_ex'), patch.object(rl, 'draw_rectangle_rounded'), \
patch.object(SpeedLimitShapes, 'button'):
view._slc_actions(state)
assert [call.args[0] for call in foreground_calls(view.fonts.draw)] == ['New limit', 'Accept', '35', 'mph', 'Reject', 'new limit']
assert [call.args[0] for call in foreground_calls(view.fonts.draw)] == ['NEW LIMIT', 'ACCEPT', '35', 'REJECT', 'NEW LIMIT']
def test_compact_pending_label_and_button_follow_saved_action_placement():
@@ -242,7 +242,7 @@ def test_compact_pending_label_and_button_follow_saved_action_placement():
with patch.object(rl, 'draw_rectangle_rounded') as button, patch.object(rl, 'draw_line_ex'), \
patch.object(SpeedLimitShapes, 'button'):
view._slc_actions(state)
assert [call.args[0] for call in foreground_calls(view.fonts.draw) if call.args[0] in ('Accept', 'Reject')] == ['Accept', 'Reject']
assert [call.args[0] for call in foreground_calls(view.fonts.draw) if call.args[0] in ('ACCEPT', 'REJECT')] == ['ACCEPT', 'REJECT']
button.assert_not_called()
assert slc_controls(Profile.COMPACT, state)[0].bounds == (156., 48., 306., 160.)
@@ -50,7 +50,8 @@ def test_closed_and_open_card_share_anchor_and_physically_scaled_targets(profile
opened = touch.present(current)
after = speed_limit_bounds(profile, opened)
assert before is not None and after is not None
assert before[1] == after[1] and before[3] <= after[3]
assert before[1] == after[1]
assert before[3] - after[3] == pytest.approx(7 if profile == Profile.COMPACT else 0)
assert after[0] <= before[0] < before[2] <= after[2]
first, last = slc_controls(profile, opened)
scale = CONFIRMATION_SCALE[profile]
@@ -60,12 +61,29 @@ def test_closed_and_open_card_share_anchor_and_physically_scaled_targets(profile
assert (first.bounds[0] - after[0]) / scale == pytest.approx(8)
assert (after[2] - last.bounds[2]) / scale == pytest.approx(8)
torque = current.customization['layouts'][str(profile)]['torque_bar']
assert after[3] < torque['y']
assert max(before[3], after[3]) <= torque['y']
if profile == Profile.COMPACT:
assert after == (148., 8, 476., 160.)
assert after[0] > 146
@pytest.mark.parametrize('profile', list(Profile))
@pytest.mark.parametrize('wheel_confirmation', [False, True])
def test_notice_footer_opens_choices_without_accepting_or_rejecting(profile, wheel_confirmation):
current = replace(state(), slc_wheel_confirmation_available=wheel_confirmation)
emitted = []
touch = OnroadInput(emitted.append, profile)
bounds = speed_limit_bounds(profile, current)
assert bounds is not None
left, _, right, bottom = bounds
x, y = (left + right) / 2, bottom - 2 * CONFIRMATION_SCALE[profile]
touch.press(x, y, current)
touch.release(x, y, current)
assert touch.present(current).slc_prompt_open
assert len(slc_controls(profile, touch.present(current))) == 2
assert not emitted
@pytest.mark.parametrize('profile', list(Profile))
@pytest.mark.parametrize('lateral', [False, True])
def test_disengaged_and_aol_views_cancel_confirmation_but_keep_the_sign(profile, lateral):
@@ -136,7 +154,7 @@ def test_new_layouts_reject_torque_overlap_and_legacy_migration_is_targeted(prof
original = deepcopy(doc)
migrated = validate_document(doc)
assert doc == original
assert migrated['version'] == 13
assert migrated['version'] == 14
assert migrated['palette'] == doc['palette']
assert migrated['layouts']['large']['current_speed'] == doc['layouts']['large']['current_speed']
assert migrated['layouts'][str(profile)][owner]['y'] == default_document()['layouts'][str(profile)][owner]['y']
@@ -63,7 +63,7 @@ def test_hidden_set_keeps_control_context_inside_moved_confirmation(profile, mod
document = default_document()
document['showMaxSpeed'] = False
document['layouts']['compact']['max_speed']['enabled'] = False
document['layouts']['compact']['speed_limit'].update(x=200, y=20)
document['layouts']['compact']['speed_limit'].update(x=200, y=16)
document = validate_document(document)
current = state(customization=document, slc_prompt_open=True)
current = replace(current, longitudinal_overridden=mode == 'pedal', speed_limit=replace(current.speed_limit,
@@ -79,8 +79,8 @@ def test_hidden_set_keeps_control_context_inside_moved_confirmation(profile, mod
assert not foreground_calls(font.draw)
SpeedLimitWidget(font, profile).render_confirmation(current)
rows = [call.args for call in foreground_calls(font.draw)]
expected = {'limit': 'Now: LIMIT 128 km/h', 'override': 'Now: OVERRIDE 128 km/h', 'pedal': 'Now: PEDAL'}[mode]
assert [row[0] for row in rows] == [expected, 'Accept', '120', 'km/h', 'Reject', 'new limit']
expected = {'limit': 'Now: LIMIT 128', 'override': 'Now: OVERRIDE 128', 'pedal': 'Now: PEDAL'}[mode]
assert [row[0] for row in rows] == [expected, 'ACCEPT', '120', 'REJECT', 'NEW LIMIT']
status, accept = rows[:2]
assert status[4] + font.vertical_ink(*status[:3])[1] < accept[4]
assert not monitor_visible(Profile.COMPACT, current, fresh=True, onroad=True, top_icons=False)
@@ -110,7 +110,7 @@ def test_displayed_proposal_matches_the_decision_during_a_held_override(profile,
metric=True, longitudinal_active=True, slc_prompt_open=True)
font = fonts(profile)
SpeedLimitWidget(font, profile).render_confirmation(current)
assert [call.args[0] for call in foreground_calls(font.draw)] == ['Now: OVERRIDE 128 km/h', 'Accept', '120', 'km/h', 'Reject', 'new limit']
assert [call.args[0] for call in foreground_calls(font.draw)] == ['Now: OVERRIDE 128', 'ACCEPT', '120', 'REJECT', 'NEW LIMIT']
assert resolve_unified_speed(current).override_text == '128'
action = Action(runtime.session_id, 1, pending.decisionId, pending.presentationId, choice)
result = tick(runtime, sm, cp, 4, request=action).message.slcState
@@ -142,7 +142,7 @@ def test_hidden_set_prompt_suppresses_only_overlapping_optional_text(widget, met
monkeypatch.setattr('openpilot.starpilot.ui.onroad_weather.draw_weather_icon', Mock())
document = default_document()
document['layouts']['compact']['max_speed']['enabled'] = False
document['layouts']['compact']['speed_limit'].update(x=200, y=20)
document['layouts']['compact']['speed_limit'].update(x=200, y=16)
document['layouts']['compact'][widget].update(enabled=True, x=330 if widget == 'weather' else 200, y=0)
current = state(customization=validate_document(document), slc_prompt_open=True)
view = OnroadView.__new__(OnroadView)
@@ -52,7 +52,7 @@ def test_v4_migration_preserves_settings_and_clamps_only_enlarged_widgets():
doc['layouts']['large']['speed_limit_actions'].update(x=1654, y=500)
old_speed = dict(doc['layouts']['large']['current_speed'])
migrated = validate_document(doc)
assert migrated['version'] == 13
assert migrated['version'] == 14
assert migrated['layouts']['large']['cruise_limits']['x'] == 1437
assert migrated['layouts']['large']['speed_limit_actions'] == {'x': 1678, 'y': 267, 'enabled': True}
assert migrated['layouts']['large']['current_speed'] == old_speed
@@ -123,7 +123,7 @@ def test_set_and_binding_slc_stay_visible_beside_sign_or_prompt(profile, stage):
texts = [call.args[0] for call in foreground_calls(font.draw)]
assert 'SET' in texts and '35' in texts and not any(text.startswith('SLC ') for text in texts)
assert ('LIMIT 30' in texts) == (stage == 'prompt')
assert ('Now: LIMIT 30 mph' in texts) == (stage == 'expanded')
assert ('Now: LIMIT 30' in texts) == (stage == 'expanded')
assert ('Limit' in texts) == (stage in ('live', 'retained'))
if pending:
assert '25' in texts and not any(text.startswith('LAST') for text in texts)
@@ -181,7 +181,7 @@ def test_sign_heading_identifies_the_displayed_source(profile, pending, vienna,
font = fonts(profile)
render_sign(SpeedLimitWidget(font, profile), current)
expected = [heading, '50']
assert [call.args[0] for call in foreground_calls(font.draw)] == expected + (['+5'] if offset else []) + (['New Limit', 'Tap to review'] if pending else [])
assert [call.args[0] for call in foreground_calls(font.draw)] == expected + (['+5'] if offset else []) + (['NEW LIMIT', 'Tap to review'] if pending else [])
if not pending:
label, speed = foreground_calls(font.draw)[:2]
assert rgba_tuple(label.args[-1]) == rgba_tuple(speed.args[-1])
@@ -217,10 +217,10 @@ def test_pending_notice_pulses_without_changing_ink_and_arrow_follows_speed(prof
assert drawing['arrow'].call_args.args[3] == -1
render_sign(sign, replace(current, observed_ns=10_350_000_000, speed_mps=60 / 3.6))
after = {call.args[0]: call.args for call in foreground_calls(font.draw)}
assert set(before) == {'OSM', '50', '+5', 'New Limit', 'Tap to review'}
assert set(before) == {'OSM', '50', '+5', 'NEW LIMIT', 'Tap to review'}
assert all(after[text][3:5] == args[3:5] for text, args in before.items())
assert all(rgba_tuple(after[text][-1]) == rgba_tuple(args[-1]) for text, args in before.items() if text != 'New Limit')
assert rgba_tuple(after['New Limit'][-1]) != rgba_tuple(before['New Limit'][-1])
assert all(rgba_tuple(after[text][-1]) == rgba_tuple(args[-1]) for text, args in before.items() if text != 'NEW LIMIT')
assert rgba_tuple(after['NEW LIMIT'][-1]) != rgba_tuple(before['NEW LIMIT'][-1])
drawing['circle'].assert_not_called()
drawing['outline'].assert_not_called()
assert drawing['arrow'].call_args.args[3] == 1
@@ -248,12 +248,17 @@ def test_wheel_cues_follow_confirmation_availability(profile, available, drawing
sign.render_confirmation(current)
drawing['draw_rectangle_rounded_lines_ex'].assert_not_called()
drawing['button'].assert_not_called()
texts = [call.args[0] for call in foreground_calls(sign.fonts.draw)]
assert 'NEW LIMIT' in texts
assert ('ACCEPT' in texts) == available
assert ('REJECT' in texts) == available
calls = drawing['wheel_key'].call_args_list
assert [call.args[0] for call in calls] == ([True, False] if available else [])
if available:
bounds = speed_limit_bounds(profile, current)
assert bounds is not None
assert all(call.args[2] == pytest.approx(bounds[1] + 135.5 * CONFIRMATION_SCALE[profile]) for call in calls)
hint_y = bounds[1] + (12 if profile == Profile.LARGE else 8) + (142 if profile == Profile.LARGE else 136.75) * CONFIRMATION_SCALE[profile]
assert all(call.args[2] == pytest.approx(hint_y) for call in calls)
assert bounds[0] < calls[0].args[1] < calls[1].args[1] < bounds[2]
@@ -398,9 +403,11 @@ def test_compact_set_keeps_dinish_number_and_has_no_backplate(drawing):
hud.render_max_speed(current)
hud.render_max_speed(replace(current, observed_ns=current.observed_ns + 20_000_000))
assert [(call.args[0], call.args[1]) for call in foreground_calls(font.draw)] == [
('SET', FontRole.MEDIUM), ('km/h', FontRole.NORMAL), ('80', FontRole.SET_SPEED)]
('SET', FontRole.SEMI_BOLD), ('80', FontRole.SET_SPEED)]
assert font_filename(Profile.COMPACT, FontRole.SET_SPEED) == 'DINish-Speed.fnt'
assert foreground_calls(font.draw)[2].args[4] == 46
number = foreground_calls(font.draw)[1].args
top, bottom = font.vertical_ink(*number[:3])
assert number[4] + (top + bottom) / 2 == 84
drawing['draw_circle_gradient'].assert_not_called()
@@ -416,16 +423,20 @@ def test_compact_unset_max_stays_hidden(metric, engaged, drawing):
drawing['draw_circle_gradient'].assert_not_called()
def test_large_limit_keeps_a_unit_when_set_speed_is_hidden():
profile = Profile.LARGE
current = state()
current = replace(current, speed_limit=replace(current.speed_limit, pending_speed_limit_mps=None))
@pytest.mark.parametrize('profile', list(Profile))
@pytest.mark.parametrize('metric', [False, True])
def test_limit_has_no_unit_when_set_speed_is_hidden(profile, metric):
current = state(metric=metric)
current = replace(current, speed_limit=replace(current.speed_limit, pending_speed_limit_mps=None))
current.customization['showMaxSpeed'] = False
current.customization['layouts']['compact']['max_speed']['enabled'] = False
font = fonts(profile)
UnifiedSpeedWidget(font).render(rl.Rectangle(30, 30, 1800, 1020), current)
assert [call.args[0] for call in foreground_calls(font.draw)].count('km/h') == 1
if profile == Profile.LARGE:
UnifiedSpeedWidget(font).render(rl.Rectangle(30, 30, 1800, 1020), current)
else:
CompactHudRenderer(font, Mock()).render(current)
texts = [call.args[0] for call in foreground_calls(font.draw)]
assert texts and not any('mph' in text or 'km/h' in text for text in texts)
@pytest.mark.parametrize('profile', list(Profile))
@@ -436,26 +447,26 @@ def test_transparent_chooser_keeps_touch_targets_and_pressed_feedback(profile, p
controls = slc_controls(profile, current)
view._slc_actions(current)
rows = [call.args for call in foreground_calls(view.fonts.draw)]
assert [row[0] for row in rows] == ['New limit', 'Accept', '55', 'km/h', 'Reject', 'new limit']
assert [row[0] for row in rows] == ['NEW LIMIT', 'ACCEPT', '55', 'REJECT', 'NEW LIMIT']
drawing['draw_rectangle_rounded'].assert_not_called()
drawing['draw_line_ex'].assert_not_called()
drawing['draw_rectangle_rounded_lines_ex'].assert_not_called()
outlines = drawing['button'].call_args_list
assert [rgba_tuple(call.args[-1]) for call in outlines] == [rgba_tuple(IN_USE), rgba_tuple(REJECT)]
for control, content in zip(controls, (rows[1:4], rows[4:]), strict=True):
for control, content in zip(controls, (rows[1:3], rows[3:]), strict=True):
for row in content:
left, top, right, bottom = control.bounds
a, b = view.fonts.horizontal_ink(*row[:3])
c, d = view.fonts.vertical_ink(*row[:3])
assert left < row[3] + a < row[3] + b < right
assert top < row[4] + c < row[4] + d < bottom
original = [rgba_tuple(row[-1]) for row in rows if row[0] in ('Accept', 'Reject')]
original = [rgba_tuple(row[-1]) for row in rows if row[0] in ('ACCEPT', 'REJECT')]
view.pressed_action = lambda: controls[pressed_index].request
view.fonts.draw.reset_mock()
drawing['button'].reset_mock()
view._slc_actions(current)
assert [call.args[:5] for call in foreground_calls(view.fonts.draw)] == [row[:5] for row in rows]
pressed = [rgba_tuple(call.args[-1]) for call in foreground_calls(view.fonts.draw) if call.args[0] in ('Accept', 'Reject')]
pressed = [rgba_tuple(call.args[-1]) for call in foreground_calls(view.fonts.draw) if call.args[0] in ('ACCEPT', 'REJECT')]
assert pressed[pressed_index] != original[pressed_index] and pressed[1-pressed_index] == original[1-pressed_index]
pressed_outlines = drawing['button'].call_args_list
assert [call.kwargs['pressed'] for call in pressed_outlines] == [pressed_index == 0, pressed_index == 1]