mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-09 22:13:43 +08:00
UI Consistancy, Tighten Boxes
This commit is contained in:
@@ -11,10 +11,13 @@ metadata.width = 2880
|
||||
metadata.bounds.width = 2820
|
||||
metadata.widgets.current_speed.default.x += 510
|
||||
metadata.widgets.steering_wheel.default.x += 1020
|
||||
metadata.widgets.driver_monitor.width = metadata.widgets.driver_monitor.height = 128
|
||||
metadata.widgets.driver_monitor.default = {x: 120, y: 840, enabled: true, size: 128}
|
||||
metadata.widgets.driver_monitor.resizable = {min: 64, default: 128, max: 192}
|
||||
metadata.widgets.car_exit = { label: "Exit to car", kind: "car_exit", width: 96, height: 96, colors: {}, required: true, frontmost: true,
|
||||
opacity: { min: 25, max: 100, default: 100 }, backgroundToggle: true,
|
||||
opacity: { min: 25, max: 100, default: 75 }, backgroundToggle: true,
|
||||
note: "Returns to the car's own screen without disconnecting Android Auto. Always enabled and above other widgets.",
|
||||
default: { x: 30, y: 954, enabled: true, opacity: 100, background: true } }
|
||||
default: { x: 30, y: 954, enabled: true, opacity: 75, background: false } }
|
||||
metadata.widgetOrder = [...(metadata.widgetOrder || Object.keys(native.metadata.profiles.large.widgets)), "car_exit"]
|
||||
const widgets = Object.fromEntries(Object.entries(metadata.widgets).map(([id, widget]) => [id,
|
||||
{ ...widget.default, ...(widget.resizable ? { size: widget.resizable.default } : {}) }]))
|
||||
@@ -26,6 +29,16 @@ const raw = { version: 1, document: doc, defaults: copy(doc), metadata,
|
||||
const data = editorSnapshot(raw)
|
||||
assert.equal(validSnapshot(data), true)
|
||||
assert.equal(validDocument(data.document, data.metadata), true)
|
||||
assert.equal(data.document.layouts.large.car_exit.opacity, 75)
|
||||
assert.equal(data.document.layouts.large.car_exit.background, false)
|
||||
const resizedMonitor = copy(data.document)
|
||||
resizedMonitor.layouts.large.driver_monitor.size = 192
|
||||
assert.equal(validDocument(resizedMonitor, data.metadata), true)
|
||||
assert.equal(projectionPayload({document: resizedMonitor}, data.metadata).document.widgets.driver_monitor.size, 192)
|
||||
resizedMonitor.layouts.large.driver_monitor.size = 193
|
||||
assert.equal(validDocument(resizedMonitor, data.metadata), false)
|
||||
assert.match(OnroadLayoutPage.template, /switchRow\('Show background', selectedPosition.background === true\)/)
|
||||
assert.doesNotMatch(OnroadLayoutPage.template, /type="checkbox" :checked="selectedPosition.background"/)
|
||||
const exitOpacity = copy(data.document)
|
||||
exitOpacity.layouts.large.car_exit.opacity = 24
|
||||
assert.equal(validDocument(exitOpacity, data.metadata), false)
|
||||
|
||||
@@ -95,6 +95,7 @@ const vm = {
|
||||
editable: true, $refs: { preview: { getBoundingClientRect: () => ({ left: 0, top: 0, width: 2880, height: 1080 }),
|
||||
setPointerCapture() {}, hasPointerCapture: () => false, releasePointerCapture() {} } },
|
||||
finishColorEdit() { this.state.history.group = null },
|
||||
sceneHidden() { return false },
|
||||
}
|
||||
Object.defineProperties(vm, {
|
||||
canUndo: { get: () => vm.state.history.undo.length > 0 },
|
||||
@@ -182,7 +183,7 @@ for (const [index, key] of ['nav_home', 'nav_work', 'bookmark', 'car_exit'].entr
|
||||
vm.finishColorEdit()
|
||||
const before = copy(vm.state.draft)
|
||||
vm.opacityInput({target: {value: String(25 + index * 10)}})
|
||||
vm.setWidgetBackground({target: {checked: false}})
|
||||
vm.setWidgetBackground('Off')
|
||||
assert.equal(vm.layout[key].opacity, 25 + index * 10)
|
||||
assert.equal(vm.layout[key].background, false)
|
||||
vm.undo()
|
||||
@@ -206,7 +207,7 @@ for (const [index, key] of ['nav_home', 'nav_work', 'bookmark', 'car_exit'].entr
|
||||
}
|
||||
for (const blocked of [{editable: false}, {state: {...vm.state, drag: {}}}, {state: {...vm.state, layerDrag: {}}}]) {
|
||||
OnroadLayoutPage.methods.opacityInput.call({...vm, ...blocked}, {target: {value: '90'}})
|
||||
OnroadLayoutPage.methods.setWidgetBackground.call({...vm, ...blocked}, {target: {checked: true}})
|
||||
OnroadLayoutPage.methods.setWidgetBackground.call({...vm, ...blocked}, 'On')
|
||||
assert.equal(vm.layout[key].opacity, 25 + index * 10)
|
||||
assert.equal(vm.layout[key].background, false)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { widgetArtMetadata } from "./widget-art.js"
|
||||
import { widgetSize } from "./onroad-layout-document.js"
|
||||
// hiddenOn: screens where the scene looks exactly like another one. The Small screen has no text for these
|
||||
// speed-limit states, so they match Engaged there.
|
||||
export const PREVIEW_SCENES = [
|
||||
@@ -44,7 +45,11 @@ export function sceneHidesWidget(scene, profile, id, layout = {}, widgets = {})
|
||||
if (id === "speed_limit_actions") return !longitudinal || scene === "slc_unavailable" || scene === "slc_display" ||
|
||||
profile === "compact" && scene !== "slc_pending"
|
||||
const overlaps = (a, b) => a && b && a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y
|
||||
const box = key => layout[key]?.enabled && widgets[key] ? { ...widgets[key], ...layout[key] } : null
|
||||
const box = key => {
|
||||
if (!layout[key]?.enabled || !widgets[key]) return null
|
||||
const [width, height] = widgetSize(widgets[key], layout[key])
|
||||
return { ...widgets[key], ...layout[key], width, height }
|
||||
}
|
||||
if (profile === "compact" && id === "driver_monitor" && longitudinal) return !!overlaps(box(id), box("max_speed"))
|
||||
if (profile === "compact" && id === "speed_limit") {
|
||||
if (scene === "slc_unavailable") return true
|
||||
|
||||
@@ -217,7 +217,7 @@ export function validSnapshot(data) {
|
||||
widget.opacity.default > widget.opacity.max)) return false
|
||||
if (widget.backgroundToggle != null && (!projection || !["nav_home", "nav_work", "bookmark", "car_exit"].includes(widget.kind) ||
|
||||
widget.backgroundToggle !== true || !widget.opacity)) return false
|
||||
if (widget.resizable && (widget.kind !== "steering_wheel" || !Number.isInteger(widget.resizable.min) ||
|
||||
if (widget.resizable && (!["steering_wheel", "driver_monitor"].includes(widget.kind) || !Number.isInteger(widget.resizable.min) ||
|
||||
!Number.isInteger(widget.resizable.default) || !Number.isInteger(widget.resizable.max) ||
|
||||
widget.resizable.default !== widget.width || widget.width !== widget.height ||
|
||||
widget.resizable.min < 24 || widget.resizable.max > area.width || widget.resizable.max > area.height ||
|
||||
|
||||
@@ -1011,11 +1011,11 @@ export const OnroadLayoutPage = {
|
||||
this.selectedPosition.opacity = value
|
||||
this.recordChange(before, this.state.profile + ":" + this.state.selected + ":opacity")
|
||||
},
|
||||
setWidgetBackground(event) {
|
||||
setWidgetBackground(value) {
|
||||
if (!this.editable || this.state.drag || this.state.layerDrag || !this.selectedWidget?.backgroundToggle) return
|
||||
this.finishColorEdit()
|
||||
const before = clone(this.state.draft)
|
||||
this.selectedPosition.background = event.target.checked
|
||||
this.selectedPosition.background = value === 'On'
|
||||
this.recordChange(before)
|
||||
this.state.notice = ""
|
||||
},
|
||||
@@ -1365,7 +1365,7 @@ export const OnroadLayoutPage = {
|
||||
<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">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 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>
|
||||
<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>
|
||||
<p v-if="!projection" class="gx-note">Small speed-limit signs can overlap confirmation actions. Live confirmations hide an overlapping sign until the decision ends. Select either widget in the list to edit it.</p>
|
||||
<p v-if="!projection && state.showFavoriteZones" class="gx-note">Favorite tap areas apply to enabled slots. Speed-limit controls and the steering wheel take priority when the drawer is closed.</p>
|
||||
</div>
|
||||
@@ -1441,8 +1441,8 @@ export const OnroadLayoutPage = {
|
||||
<input id="layout-widget-opacity" class="gx-slider" type="range" step="1" :min="selectedWidget.opacity.min" :max="selectedWidget.opacity.max"
|
||||
:value="selectedPosition.opacity" :disabled="!editAvailable || !!state.drag || !!state.layerDrag" @input="opacityInput($event)" @change="finishColorEdit" @blur="finishColorEdit">
|
||||
</template>
|
||||
<label v-if="selectedWidget.backgroundToggle"><input type="checkbox" :checked="selectedPosition.background"
|
||||
:disabled="!editAvailable || !!state.drag || !!state.layerDrag" @change="setWidgetBackground"> Show background</label>
|
||||
<GalaxySettingRow v-if="selectedWidget.backgroundToggle" :row="switchRow('Show background', selectedPosition.background === true)" :index="0"
|
||||
:disabled="!editAvailable || !!state.drag || !!state.layerDrag" :save-value="(index, value) => setWidgetBackground(value)" />
|
||||
<p v-if="selectedWidget.defaultAnchor === 'driver_side'" class="gx-note">The default position follows the driver’s side. A moved position stays where you place it.</p>
|
||||
<p v-if="selectedWidget.note" class="gx-note">{{ selectedWidget.note }}</p>
|
||||
<label v-if="selectedWidget.kind === 'clock'" class="gx-layout__clock-format"><input type="checkbox" :checked="!!state.draft.clock24Hour" :disabled="!editAvailable || !!state.drag || !!state.layerDrag" @change="setClock24Hour"> Use 24-hour time</label>
|
||||
|
||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
||||
|
||||
from openpilot.starpilot.system.android_auto.identity import DATA_DIR
|
||||
from openpilot.starpilot.system.android_auto.display_profile import screen_geometry
|
||||
from openpilot.starpilot.ui.onroad_customization import (CEM_GAUGE_WIDGET, CLOCK_WIDGET, MODE_WIDGET, STOPPED_TIMER_WIDGET, WHEEL_SIZES,
|
||||
from openpilot.starpilot.ui.onroad_customization import (CEM_GAUGE_WIDGET, CLOCK_WIDGET, MODE_WIDGET, STOPPED_TIMER_WIDGET,
|
||||
CEM_GAUGE_FRAME, CEM_GAUGE_INSET, SPEED_ALIGNMENTS, customization_metadata, validate_widget_order)
|
||||
from openpilot.starpilot.ui.onroad_stopped_timer import timer_size
|
||||
from openpilot.starpilot.ui.onroad_torque_geometry import maximum_footprint
|
||||
@@ -111,8 +111,9 @@ def layout_metadata_for_viewport(viewport):
|
||||
'note': "Returns to the car's own screen without disconnecting Android Auto. Always enabled and above other widgets.",
|
||||
'default': {'x': 30, 'y': height - 30 - exit_h, 'enabled': True}}
|
||||
for key in ACTION_WIDGETS:
|
||||
widgets[key].update(opacity={'min': CAR_EXIT_MIN_OPACITY if key == CAR_EXIT else 0, 'max': 100, 'default': 100}, backgroundToggle=True)
|
||||
widgets[key]['default'].update(opacity=100, background=True)
|
||||
opacity = 75 if key == CAR_EXIT else 100
|
||||
widgets[key].update(opacity={'min': CAR_EXIT_MIN_OPACITY if key == CAR_EXIT else 0, 'max': 100, 'default': opacity}, backgroundToggle=True)
|
||||
widgets[key]['default'].update(opacity=opacity, background=key != CAR_EXIT)
|
||||
# Background opacity for the cruise and speed-limit card; its text stays solid.
|
||||
widgets['cruise_limits'].update(opacity={'min': 0, 'max': 100, 'default': 100})
|
||||
widgets['cruise_limits']['default'].update(opacity=100)
|
||||
@@ -174,8 +175,8 @@ def default_layout_for_viewport(viewport, display=None):
|
||||
def placement_size(key, widget, placement):
|
||||
if key in DISPLAY_WIDGETS and placement.get('display') == 'icons':
|
||||
return FAVORITE_ICON_SIZE, FAVORITE_ICON_SIZE
|
||||
if key == 'steering_wheel':
|
||||
return placement.get('size', 192), placement.get('size', 192)
|
||||
if widget.get('resizable'):
|
||||
return (placement.get('size', widget['resizable']['default']),) * 2
|
||||
if key == NAV_MAP:
|
||||
return placement['width'], placement['height']
|
||||
return widget['width'], widget['height']
|
||||
@@ -241,7 +242,12 @@ def validate_layout_for_viewport(value, viewport, *, _legacy_geometry=False):
|
||||
placement.setdefault('background', True)
|
||||
if key == 'cruise_limits' and type(placement) is dict:
|
||||
placement.setdefault('opacity', 100)
|
||||
fields = ({'x', 'y', 'enabled'} | ({'size'} if key == 'steering_wheel' else set()) |
|
||||
if key == 'driver_monitor' and type(placement) is dict and 'size' not in placement:
|
||||
for axis in ('x', 'y'):
|
||||
if type(placement.get(axis)) in (int, float):
|
||||
placement[axis] += 32
|
||||
placement['size'] = widget['resizable']['default']
|
||||
fields = ({'x', 'y', 'enabled'} | ({'size'} if widget.get('resizable') else set()) |
|
||||
({'display'} if key in DISPLAY_WIDGETS else set()) |
|
||||
({'opacity', 'background'} if key in ACTION_WIDGETS else set()) |
|
||||
({'opacity'} if key == 'cruise_limits' else set()) |
|
||||
@@ -259,9 +265,10 @@ def validate_layout_for_viewport(value, viewport, *, _legacy_geometry=False):
|
||||
raise ValueError('Invalid cruise card opacity')
|
||||
if key == CAR_EXIT and placement['opacity'] < CAR_EXIT_MIN_OPACITY:
|
||||
raise ValueError('Exit to car opacity must be at least 25%')
|
||||
size = placement.get('size', 192)
|
||||
if key == 'steering_wheel' and (type(size) is not int or not WHEEL_SIZES['large'][0] <= size <= WHEEL_SIZES['large'][2]):
|
||||
raise ValueError('Invalid steering wheel size')
|
||||
if widget.get('resizable'):
|
||||
size, limits = placement['size'], widget['resizable']
|
||||
if type(size) is not int or not limits['min'] <= size <= limits['max']:
|
||||
raise ValueError('Invalid widget size')
|
||||
if key == NAV_MAP:
|
||||
box, opacity = widget['box'], widget['opacity']
|
||||
if (type(placement['width']) is not int or not box['minWidth'] <= placement['width'] <= box['maxWidth'] or
|
||||
|
||||
@@ -18,6 +18,33 @@ SCREEN = {'version': 1, 'width': 1280, 'height': 720, 'margin_width': 0,
|
||||
|
||||
|
||||
class TestProjectionLayout(unittest.TestCase):
|
||||
def test_exit_defaults_and_monitor_scaling_preserve_saved_appearance(self):
|
||||
document = default_layout(SCREEN)
|
||||
metadata = layout_metadata(SCREEN)
|
||||
self.assertEqual(document['widgets'][CAR_EXIT]['opacity'], 75)
|
||||
self.assertIs(document['widgets'][CAR_EXIT]['background'], False)
|
||||
self.assertIs(document['widgets'][CAR_EXIT]['enabled'], True)
|
||||
self.assertEqual(metadata['widgets'][CAR_EXIT]['opacity']['default'], 75)
|
||||
document['widgets'][CAR_EXIT].update(opacity=60, background=True)
|
||||
monitor = document['widgets']['driver_monitor']
|
||||
self.assertEqual(monitor['size'], 128)
|
||||
monitor.update(x=700, y=600)
|
||||
del monitor['size']
|
||||
restored = validate_layout(document, SCREEN)
|
||||
self.assertEqual(restored['widgets']['driver_monitor'], {'x': 732, 'y': 632, 'enabled': True, 'size': 128})
|
||||
self.assertEqual(restored['widgets'][CAR_EXIT], document['widgets'][CAR_EXIT])
|
||||
self.assertEqual(validate_layout(restored, SCREEN), restored)
|
||||
for size in (64, 192):
|
||||
restored['widgets']['driver_monitor']['size'] = size
|
||||
decoded = decode_layout(json.dumps(restored).encode(), SCREEN)
|
||||
projected = projection_customization(decoded, default_document())
|
||||
self.assertEqual(projected['layouts']['large']['driver_monitor']['size'], size)
|
||||
self.assertEqual(placement_size('driver_monitor', metadata['widgets']['driver_monitor'], decoded['widgets']['driver_monitor']), (size, size))
|
||||
for size in (63, 193, True, 128.5):
|
||||
restored['widgets']['driver_monitor']['size'] = size
|
||||
with self.assertRaises(ValueError):
|
||||
validate_layout(restored, SCREEN)
|
||||
|
||||
def test_stopped_timer_migrates_and_keeps_independent_placement_and_order(self):
|
||||
document = default_layout(SCREEN)
|
||||
metadata = layout_metadata(SCREEN)
|
||||
|
||||
@@ -21,7 +21,7 @@ from openpilot.starpilot.curve_speed.status import CurveObservation
|
||||
from openpilot.starpilot.ui.onroad_state import ConditionalLead
|
||||
from openpilot.starpilot.ui.onroad_state import AlertSize, ObservationKind, OnroadAlert, OnroadState, SourceReading, SpeedLimitObservation
|
||||
from openpilot.starpilot.ui.appearance_preferences import CameraViewChoice, OnroadAppearance
|
||||
from openpilot.starpilot.ui.onroad_dm import monitor_visible
|
||||
from openpilot.starpilot.ui.onroad_dm import draw_monitor_art, monitor_bounds, monitor_visible
|
||||
from openpilot.starpilot.ui.onroad_stopped_timer import PREVIEW_DURATION_S
|
||||
from openpilot.starpilot.ui.layout_preview_sidebar import SIDEBAR_WIDTH, render_sidebar
|
||||
from openpilot.starpilot.ui.presentation import BitmapFonts, FontRole, Profile, default_font_directory
|
||||
@@ -181,17 +181,11 @@ class _DriverMonitorArt:
|
||||
raise
|
||||
|
||||
def render(self, _content: rl.Rectangle, state: OnroadState, *, top_icons: bool = False) -> None:
|
||||
position = placement(state.customization, self.profile, "driver_monitor")
|
||||
if not monitor_visible(self.profile, state, fresh=True, onroad=True, top_icons=top_icons):
|
||||
return
|
||||
size = 128 if self.profile == Profile.LARGE else 60
|
||||
x = position["x"] + (192 - size) / 2 if self.profile == Profile.LARGE else position["x"]
|
||||
y = position["y"] + (192 - size) / 2 if self.profile == Profile.LARGE else position["y"]
|
||||
self.renderer._rect = rl.Rectangle(x, y, size, size)
|
||||
widget = PROFILES[str(self.profile)]["widgets"]["driver_monitor"]
|
||||
draw_widget_frame(rl.Rectangle(position["x"], position["y"], widget["width"], widget["height"]),
|
||||
state.customization, self.profile, "driver_monitor")
|
||||
self.renderer._render(self.renderer._rect)
|
||||
bounds = monitor_bounds(state.customization, self.profile, False)
|
||||
draw_widget_frame(bounds, state.customization, self.profile, "driver_monitor")
|
||||
draw_monitor_art(self.renderer, bounds, self.profile)
|
||||
|
||||
def close(self) -> None:
|
||||
if rl.is_window_ready():
|
||||
|
||||
@@ -19,6 +19,8 @@ PALETTE = {"cardFill": "#000000A6", "cardBorder": "#C4CDD0B4", "text": "#FFFFFFF
|
||||
ROAD_COLORS = {"path": "#30FF9CFF", "pathEdge": "#00FF40FF", "laneLines": "#FFFFFFFF"}
|
||||
PATH_MODES = ("default", "color", "rainbow", "acceleration")
|
||||
WHEEL_SIZES = {"large": (144, 192, 240), "compact": (40, 50, 70)}
|
||||
DM_SIZES = {"large": (64, 128, 192), "compact": (40, 60, 90)}
|
||||
WIDGET_SIZES = {"steering_wheel": WHEEL_SIZES, "driver_monitor": DM_SIZES}
|
||||
|
||||
|
||||
class Bounds(TypedDict):
|
||||
@@ -32,6 +34,7 @@ class WidgetPlacement(TypedDict):
|
||||
x: float
|
||||
y: float
|
||||
enabled: bool
|
||||
size: NotRequired[int]
|
||||
|
||||
|
||||
class Widget(TypedDict):
|
||||
@@ -77,7 +80,7 @@ PROFILES: dict[str, LayoutProfile] = {
|
||||
"cruise_limits": _widget("Cruise and speed limit", "cruise_limits", WIDTH, CARD_HEIGHT, 88, 75),
|
||||
"speed_limit_actions": _widget("Speed limit actions", "speed_limit_actions", WIDTH, ACTION_HEIGHT, ACTION_X, ACTION_Y),
|
||||
"steering_wheel": _widget("Steering wheel", "steering_wheel", 192, 192, 1588, 75),
|
||||
"driver_monitor": _widget("Driver monitoring", "driver_monitor", 192, 192, 88, 808)}},
|
||||
"driver_monitor": _widget("Driver monitoring", "driver_monitor", 128, 128, 120, 840)}},
|
||||
"compact": {"label": "Small UI", "width": 536, "height": 240,
|
||||
"bounds": {"x": 0, "y": 0, "width": 476, "height": 240},
|
||||
"protectedWidget": "speed_limit_actions",
|
||||
@@ -92,6 +95,9 @@ PROFILES: dict[str, LayoutProfile] = {
|
||||
# Coordinates remain anchored at the buttons, preserving saved v4 placements.
|
||||
PROFILES["compact"]["widgets"]["speed_limit_actions"]["visualInsetTop"] = 32
|
||||
|
||||
for profile, sizes in DM_SIZES.items():
|
||||
PROFILES[profile]['widgets']['driver_monitor']['default']['size'] = sizes[1]
|
||||
|
||||
LEGACY_WIDGETS = {"large": {"current_speed", "cruise_limits", "steering_wheel"},
|
||||
"compact": {"max_speed", "speed_limit", "steering_wheel"}}
|
||||
DM_WIDGETS = {profile: keys | {"driver_monitor"} for profile, keys in LEGACY_WIDGETS.items()}
|
||||
@@ -226,8 +232,10 @@ def customization_metadata():
|
||||
for name, profile in profiles.items():
|
||||
for key, widget in profile["widgets"].items():
|
||||
widget["colors"] = dict(WIDGET_COLORS[name].get(key, {}))
|
||||
profile["widgets"]["steering_wheel"]["resizable"] = {
|
||||
"min": WHEEL_SIZES[name][0], "default": WHEEL_SIZES[name][1], "max": WHEEL_SIZES[name][2]}
|
||||
for key, sizes in WIDGET_SIZES.items():
|
||||
minimum, default, maximum = sizes[name]
|
||||
profile["widgets"][key]["resizable"] = {"min": minimum, "default": default, "max": maximum}
|
||||
profile["widgets"][key]["default"]["size"] = default
|
||||
for profile, data in profiles.items():
|
||||
data["widgetOrder"] = list(DEFAULT_WIDGET_ORDER[profile])
|
||||
data["runtimePreviewWidgets"] = list(RUNTIME_PREVIEW_WIDGETS[profile])
|
||||
@@ -241,7 +249,7 @@ def customization_metadata():
|
||||
def default_document():
|
||||
return {"version": 7, "clock24Hour": False, "palette": dict(PALETTE), "roadColors": {profile: {} for profile in PROFILES},
|
||||
"widgetColors": {profile: {} for profile in PROFILES}, "layouts": {
|
||||
profile: {key: {**widget["default"], **({"size": WHEEL_SIZES[profile][1]} if key == "steering_wheel" else {})}
|
||||
profile: {key: {**widget["default"], **({"size": WIDGET_SIZES[key][profile][1]} if key in WIDGET_SIZES else {})}
|
||||
for key, widget in data["widgets"].items()}
|
||||
for profile, data in PROFILES.items()}}
|
||||
|
||||
@@ -348,6 +356,14 @@ def validate_document(value, *, _legacy_geometry=False):
|
||||
layouts = copy.deepcopy(layouts)
|
||||
for profile, data in PROFILES.items():
|
||||
if type(layouts[profile]) is dict:
|
||||
monitor = layouts[profile].get('driver_monitor')
|
||||
if type(monitor) is dict and 'size' not in monitor:
|
||||
# Tighten the old 192px box around its 128px icon without moving the icon.
|
||||
if profile == 'large':
|
||||
for axis in ('x', 'y'):
|
||||
if type(monitor.get(axis)) in (int, float):
|
||||
monitor[axis] += 32
|
||||
monitor['size'] = DM_SIZES[profile][1]
|
||||
missing = set(data["widgets"]) - set(layouts[profile])
|
||||
if missing and missing <= OPTIONAL_WIDGETS:
|
||||
for optional in missing:
|
||||
@@ -392,15 +408,16 @@ def validate_document(value, *, _legacy_geometry=False):
|
||||
for key, widget in data["widgets"].items():
|
||||
placement = layout[key]
|
||||
bounds = widget_bounds(profile, key)
|
||||
expected = {"x", "y", "enabled"} | ({"size"} if key == "steering_wheel" and value["version"] >= 4 else set())
|
||||
scalable = key == 'driver_monitor' or key == 'steering_wheel' and value['version'] >= 4
|
||||
expected = {"x", "y", "enabled"} | ({"size"} if scalable else set())
|
||||
if type(placement) is not dict or set(placement) != expected or type(placement["enabled"]) is not bool:
|
||||
raise ValueError("Invalid widget placement")
|
||||
size = WHEEL_SIZES[profile][1]
|
||||
if key == "steering_wheel" and value["version"] >= 4:
|
||||
if scalable:
|
||||
size = placement["size"]
|
||||
minimum, _, maximum = WHEEL_SIZES[profile]
|
||||
minimum, _, maximum = WIDGET_SIZES[key][profile]
|
||||
if type(size) is not int or not minimum <= size <= maximum:
|
||||
raise ValueError("Invalid steering wheel size")
|
||||
raise ValueError("Invalid widget size")
|
||||
# Only a gauge the saved layout actually placed moves; one filled in from defaults is already in the new box.
|
||||
saved_layout = value.get("layouts", {}).get(profile) if type(value.get("layouts")) is dict else None
|
||||
old_gauge = key == CEM_GAUGE_WIDGET and value["version"] < 7 and type(saved_layout) is dict and key in saved_layout
|
||||
@@ -410,12 +427,12 @@ def validate_document(value, *, _legacy_geometry=False):
|
||||
{'cruise_limits': (280, 412), 'speed_limit_actions': (280, 96)})
|
||||
dimensions = legacy.get(key) if _legacy_geometry and profile == 'large' else None
|
||||
dimension = (dimensions[0 if axis == 'x' else 1] if dimensions else
|
||||
size if key == 'steering_wheel' else widget[extent])
|
||||
size if key in WIDGET_SIZES else widget[extent])
|
||||
if old_gauge:
|
||||
dimension = CEM_GAUGE_FRAME[0 if axis == 'x' else 1]
|
||||
if type(number) not in (int, float) or not math.isfinite(number) or not bounds[axis] <= number <= bounds[axis] + bounds[extent] - dimension:
|
||||
raise ValueError("Widget outside profile bounds")
|
||||
result["layouts"][profile][key] = {**placement, **({"size": size} if key == "steering_wheel" else {})}
|
||||
result["layouts"][profile][key] = {**placement, **({"size": size} if key in WIDGET_SIZES else {})}
|
||||
if old_gauge:
|
||||
result["layouts"][profile][key].update(x=placement["x"] + CEM_GAUGE_INSET[0], y=placement["y"] + CEM_GAUGE_INSET[1])
|
||||
if profile == "compact":
|
||||
@@ -427,7 +444,7 @@ def validate_document(value, *, _legacy_geometry=False):
|
||||
placement = result["layouts"][profile][key]
|
||||
if key in OPTIONAL_WIDGETS and not placement["enabled"]:
|
||||
continue
|
||||
width, height = ((placement["size"],) * 2 if key == "steering_wheel" else
|
||||
width, height = ((placement["size"],) * 2 if key in WIDGET_SIZES else
|
||||
(widget["width"], widget["height"]))
|
||||
if (placement["x"] < actions["x"] + area["width"] and placement["x"] + width > actions["x"] and
|
||||
placement["y"] < actions["y"] + area["height"] and placement["y"] + height > actions["y"]):
|
||||
@@ -486,7 +503,7 @@ def widget_size(document, profile, widget):
|
||||
position = placement(document, profile, widget)
|
||||
if widget == "cruise_limits":
|
||||
return default["width"], default["height"] + sources_extent(document)
|
||||
return (position.get("size", WHEEL_SIZES[str(profile)][1]),) * 2 if widget == "steering_wheel" else (default["width"], default["height"])
|
||||
return (position.get("size", WIDGET_SIZES[widget][str(profile)][1]),) * 2 if widget in WIDGET_SIZES else (default["width"], default["height"])
|
||||
|
||||
|
||||
def offset(document, profile, widget):
|
||||
|
||||
@@ -6,7 +6,7 @@ import pyray as rl
|
||||
|
||||
from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.starpilot.ui.onroad_customization import PROFILES, placement
|
||||
from openpilot.starpilot.ui.onroad_customization import PROFILES, placement, widget_size
|
||||
from openpilot.starpilot.ui.onroad_state import AlertSize
|
||||
from openpilot.starpilot.ui.onroad_widget_style import draw_widget_frame
|
||||
from openpilot.starpilot.ui.presentation import Profile
|
||||
@@ -26,11 +26,26 @@ def valid_observation(monitor, driver):
|
||||
def monitor_bounds(document, profile, is_rhd):
|
||||
position = placement(document, profile, 'driver_monitor')
|
||||
widget = PROFILES[str(profile)]['widgets']['driver_monitor']
|
||||
width, height = widget_size(document, profile, 'driver_monitor')
|
||||
x, y = position['x'], position['y']
|
||||
if profile == Profile.LARGE and is_rhd and (x, y) == (widget['default']['x'], widget['default']['y']):
|
||||
bounds = PROFILES['large']['bounds']
|
||||
x = 2 * bounds['x'] + bounds['width'] - x - widget['width']
|
||||
return rl.Rectangle(x, y, widget['width'], widget['height'])
|
||||
x = 2 * bounds['x'] + bounds['width'] - x - width
|
||||
return rl.Rectangle(x, y, width, height)
|
||||
|
||||
|
||||
def draw_monitor_art(renderer, bounds, profile):
|
||||
"""Scale the native icon and cone together inside the saved square."""
|
||||
base = 128 if profile == Profile.LARGE else 60
|
||||
renderer._rect = rl.Rectangle(bounds.x, bounds.y, base, base)
|
||||
rl.rl_push_matrix()
|
||||
try:
|
||||
rl.rl_translatef(bounds.x, bounds.y, 0)
|
||||
rl.rl_scalef(bounds.width / base, bounds.height / base, 1)
|
||||
rl.rl_translatef(-bounds.x, -bounds.y, 0)
|
||||
renderer._render(renderer._rect)
|
||||
finally:
|
||||
rl.rl_pop_matrix()
|
||||
|
||||
|
||||
def monitor_visible(profile, state, *, fresh, onroad, top_icons):
|
||||
@@ -77,8 +92,7 @@ class DriverMonitorLayer:
|
||||
self.renderer.set_should_draw(visible)
|
||||
is_rhd = monitor.isRHD if ready else getattr(self.renderer, '_is_rhd', False)
|
||||
bounds = monitor_bounds(state.customization, self.profile, is_rhd)
|
||||
size = 128 if self.profile == Profile.LARGE else 60
|
||||
self.renderer.set_position(bounds.x + (bounds.width - size) / 2, bounds.y + (bounds.height - size) / 2)
|
||||
self.renderer.set_position(bounds.x, bounds.y)
|
||||
if ready:
|
||||
self.renderer._update_state()
|
||||
else:
|
||||
@@ -87,4 +101,4 @@ class DriverMonitorLayer:
|
||||
self.renderer._fade_filter.update(0.35 if visible else 0.0)
|
||||
if visible:
|
||||
draw_widget_frame(bounds, state.customization, self.profile, "driver_monitor")
|
||||
self.renderer._render(self.renderer._rect)
|
||||
draw_monitor_art(self.renderer, bounds, self.profile)
|
||||
|
||||
@@ -6,7 +6,7 @@ import unittest
|
||||
from dataclasses import replace
|
||||
from types import SimpleNamespace as NS
|
||||
|
||||
from openpilot.starpilot.ui.onroad_customization import default_document, validate_document
|
||||
from openpilot.starpilot.ui.onroad_customization import DM_SIZES, customization_metadata, default_document, validate_document, widget_size
|
||||
from openpilot.starpilot.ui.onroad_dm import DriverMonitorLayer, StableDriverStateRenderer, monitor_bounds, monitor_visible, valid_observation
|
||||
from openpilot.starpilot.ui.onroad_state import AlertSize, OnroadAlert, OnroadState, SpeedLimitObservation
|
||||
from openpilot.starpilot.ui.appearance_preferences import CameraViewChoice
|
||||
@@ -86,14 +86,36 @@ class TestDriverMonitor(unittest.TestCase):
|
||||
document = default_document()
|
||||
left = monitor_bounds(document, Profile.LARGE, False)
|
||||
right = monitor_bounds(document, Profile.LARGE, True)
|
||||
self.assertEqual((left.x, left.y, left.width, left.height), (88, 808, 192, 192))
|
||||
self.assertEqual((right.x, right.y), (1580, 808))
|
||||
self.assertEqual((left.x, left.y, left.width, left.height), (120, 840, 128, 128))
|
||||
self.assertEqual((right.x, right.y), (1612, 840))
|
||||
document['layouts']['large']['driver_monitor'].update(x=700.5, y=600.25)
|
||||
custom = monitor_bounds(validate_document(document), Profile.LARGE, True)
|
||||
self.assertEqual((custom.x, custom.y), (700.5, 600.25))
|
||||
compact = monitor_bounds(default_document(), Profile.COMPACT, True)
|
||||
self.assertEqual((compact.x, compact.y, compact.width, compact.height), (16, 10, 60, 60))
|
||||
|
||||
def test_saved_monitor_box_tightens_without_moving_icon_and_round_trips_size(self):
|
||||
document = default_document()
|
||||
document['layouts']['large']['driver_monitor'] = {'x': 700, 'y': 600, 'enabled': True}
|
||||
migrated = validate_document(document)
|
||||
self.assertEqual(migrated['layouts']['large']['driver_monitor'], {'x': 732, 'y': 632, 'enabled': True, 'size': 128})
|
||||
self.assertEqual(document['layouts']['large']['driver_monitor']['x'], 700)
|
||||
self.assertEqual(validate_document(migrated), migrated)
|
||||
for profile, (minimum, default, maximum) in DM_SIZES.items():
|
||||
metadata = customization_metadata()['profiles'][profile]['widgets']['driver_monitor']
|
||||
self.assertEqual(metadata['resizable'], {'min': minimum, 'default': default, 'max': maximum})
|
||||
for size in (minimum, default, maximum):
|
||||
saved = default_document()
|
||||
saved['layouts'][profile]['driver_monitor']['size'] = size
|
||||
restored = validate_document(saved)
|
||||
self.assertEqual(widget_size(restored, profile, 'driver_monitor'), (size, size))
|
||||
bounds = monitor_bounds(restored, Profile(profile), False)
|
||||
self.assertEqual((bounds.width, bounds.height), (size, size))
|
||||
for size in (minimum - 1, maximum + 1, True, 64.5):
|
||||
saved['layouts'][profile]['driver_monitor']['size'] = size
|
||||
with self.assertRaises(ValueError):
|
||||
validate_document(saved)
|
||||
|
||||
def test_visibility_reserves_critical_alerts_and_respects_saved_choice(self):
|
||||
state = road()
|
||||
for profile in Profile:
|
||||
@@ -178,7 +200,7 @@ class TestDriverMonitor(unittest.TestCase):
|
||||
patch.object(layer, 'renderer', NS(set_should_draw=Mock(), set_position=Mock(), _update_state=Mock(), _render=Mock(), _rect=object()), create=True)
|
||||
)
|
||||
layer.render(replace(road(), customization=document), monitor=monitor, driver=driver, fresh=True, onroad=True)
|
||||
layer.renderer.set_position.assert_called_once_with(732.5, 632.25)
|
||||
layer.renderer.set_position.assert_called_once_with(700.5, 600.25)
|
||||
layer.renderer._update_state.assert_called_once()
|
||||
layer.renderer._render.assert_called_once_with(layer.renderer._rect)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user