Files
2026-10-22 19:44:47 -05:00

562 lines
30 KiB
Python

"""Two-profile native onroad composition over a caller-owned camera layer.
The host supplies state and, when a frame is available, the current camera/model
renderer as a callback. Offline previews create no camera or IPC resources.
The visual widgets retain the frozen StarPilot geometry under the adjacent
LICENSE, while the application owns all state and action transport.
"""
from collections.abc import Callable
from datetime import datetime
from pathlib import Path
from typing import Protocol
import hashlib
import json
import pyray as rl
from openpilot.starpilot.ui import clip
from openpilot.system.ui.lib.application import gui_app
from openpilot.starpilot.ui.onroad_customization import (CLOCK_WIDGET, MODE_WIDGET, RAIL_WIDGETS, STOPPED_TIMER_WIDGET, offset, placement, rgba,
widget_order, widget_size)
from openpilot.starpilot.ui.appearance_preferences import CameraViewChoice
from openpilot.starpilot.ui.onroad_alerts import AlertRenderer
from openpilot.starpilot.ui.onroad_border import render_compact_half_borders, render_large_half_borders
from openpilot.starpilot.ui.onroad_compact_widgets import CompactHudRenderer, MiciSidebarWidgets
from openpilot.starpilot.ui.onroad_curve import controlling as curve_controlling, glow_color, intensity, render_glow, status_label
from openpilot.starpilot.ui.onroad_conditional import stop_active
from openpilot.starpilot.ui.onroad_corner import CornerHintCache, render_corner_hint
from openpilot.starpilot.ui.onroad_large_widgets import CurrentSpeedHud, UnifiedSpeedWidget, SteeringWheelWidget
from openpilot.starpilot.ui.onroad_cem_gauge import AetherGauge
from openpilot.starpilot.ui.live_developer_sidebar import render_sidebar as render_live_sidebar
from openpilot.starpilot.ui.layout_preview_sidebar import SIDEBAR_WIDTH
from openpilot.starpilot.ui.onroad_state import AlertSize, OnroadState, slc_controls
from openpilot.starpilot.ui.onroad_stopped_timer import duration_color, duration_text, timer_size
from openpilot.starpilot.ui.onroad_torque import TorqueBarWidget
from openpilot.starpilot.ui.onroad_navigation import NavigationCard
from openpilot.starpilot.ui.onroad_navigation_favorites import NavigationFavorites
from openpilot.starpilot.ui.presentation import BitmapFonts, FontRole, Profile
from openpilot.starpilot.ui.large_speed_geometry import ACTION_TEXT
from openpilot.starpilot.ui.speed_limit_widget import MUTED, draw_ink, draw_line, draw_text
from openpilot.starpilot.conditional_mode.policy import ModeChoice
CameraLayer = Callable[[rl.Rectangle, OnroadState], None]
OverlayLayer = Callable[[rl.Rectangle, OnroadState], None]
PipLayer = Callable[[rl.Rectangle, OnroadState], None]
ProjectionControlLayer = Callable[[OnroadState], None]
def _load_fade(path: Path) -> rl.Texture:
entry = next(item for item in json.loads(Path(__file__).with_name('onroad-assets.json').read_text())['files']
if Path(item['file']).name == path.name)
with path.open('rb') as stream:
data = stream.read(entry['bytes'] + 1)
if len(data) != entry['bytes'] or hashlib.sha256(data).hexdigest() != entry['sha256']:
raise ValueError(f'Unreviewed onroad fade art: {path.name}')
texture = rl.load_texture(str(path))
if not texture.id:
raise RuntimeError(f'Unable to load onroad fade art: {path.name}')
return texture
def driving_mode_description(state: OnroadState) -> str:
if state.longitudinal_overridden:
return "ACC - Override"
if state.lateral_active and state.switchback_mode:
return "Switchback Mode"
if state.longitudinal_active:
if state.traffic_mode:
return "Traffic Mode"
mode = state.conditional_effective
experimental = (mode.effective_experimental if mode is not None else
state.experimental_enabled and state.conditional_configured in (None, ModeChoice.STOCK))
return "ACC - Experimental" if experimental else "ACC - Chill"
if state.lateral_active:
return "Always On Lateral"
return "Stock ACC" if state.stock_cruise_active else "Disengaged"
def clock_text(now: datetime | None = None, use_24_hour: bool = False) -> str:
value = now or datetime.now().astimezone()
return value.strftime('%H:%M') if use_24_hour else value.strftime('%I:%M %p').lstrip('0')
def axis_status_color(state: OnroadState) -> rl.Color:
"""Retain StarPilot's existing off, AOL, long-only, and combined edge colors."""
if state.driver_override_active or state.longitudinal_overridden:
return rl.Color(145, 155, 149, 255)
if state.lateral_active and state.switchback_mode:
return rl.Color(139, 108, 197, 255)
if stop_active(state):
return rl.Color(218, 111, 37, 255)
if state.longitudinal_active and state.traffic_mode:
return rl.Color(201, 34, 49, 255)
mode = state.conditional_effective if state.longitudinal_active else None
if mode is not None and mode.reason == 'manual_chill':
return rl.Color(255, 214, 0, 255)
if mode is not None and mode.effective_experimental:
return rl.Color(218, 111, 37, 255)
if state.lateral_active and state.longitudinal_active:
if state.experimental_enabled and state.conditional_configured in (None, ModeChoice.STOCK):
return rl.Color(218, 111, 37, 255)
return rl.Color(22, 127, 64, 255)
if state.lateral_active:
return rl.Color(10, 186, 181, 255)
if state.longitudinal_active:
return rl.Color(255, 105, 180, 255)
return rl.Color(18, 40, 57, 255)
class JetlinkRenderLayer(Protocol):
def render(self, state: OnroadState) -> None: ...
class OnroadView:
preview_pip_layer: PipLayer | None
jetlink_status: JetlinkRenderLayer
def __init__(self, fonts: BitmapFonts, asset_directory: Path, *, camera_layer: CameraLayer | None = None,
extra_overlays: OverlayLayer | None = None, pip_layer: PipLayer | None = None,
background_layer: OverlayLayer | None = None, projection_viewport: tuple[float, float] | None = None,
wall_clock: Callable[[], datetime | None] | None = None):
self.projection_viewport = projection_viewport
self.wall_clock = wall_clock or (lambda: datetime.now().astimezone())
self._corner_cache = CornerHintCache()
gui_app.add_render_prepare(self._corner_cache.prepare, self._corner_cache.close)
self.fonts = fonts
self.camera_layer = camera_layer
self.background_layer = background_layer
self.extra_overlays = extra_overlays
self.driver_monitor_layer: OverlayLayer | None = None
self.map_layer: OverlayLayer | None = None # Android Auto map overlay, under the HUD widgets
self.projection_exit_layer: ProjectionControlLayer | None = None
self.pip_layer = pip_layer
self.stock_confidence_layer: OverlayLayer | None = None
self.stock_confidence_reset: Callable[[], None] | None = None
self._stock_confidence_last_source_ns: int | None = None
self._stock_confidence_last_drive_frame: int | None = None
self.alert = AlertRenderer(fonts)
self.navigation = NavigationCard(fonts, clock=self.wall_clock)
self.navigation_favorites = NavigationFavorites(fonts)
if projection_viewport is not None:
from openpilot.starpilot.ui.onroad_now_playing import NowPlayingWidget
self.now_playing = NowPlayingWidget(fonts)
self.torque_bar = TorqueBarWidget()
if fonts.profile == Profile.LARGE:
self.unified_speed = UnifiedSpeedWidget(fonts)
self.current_speed = CurrentSpeedHud(fonts)
self.cem_gauge = AetherGauge(fonts)
self.steering_wheel = SteeringWheelWidget(asset_directory)
from openpilot.starpilot.ui.onroad_jetlink import JetlinkStatusWidget
self.jetlink_status = JetlinkStatusWidget(fonts)
else:
self.compact_hud = CompactHudRenderer(fonts, asset_directory)
self.compact_sidebar = MiciSidebarWidgets(fonts)
self._fade_path = asset_directory / "icons_mici/onroad/onroad_fade.png"
self._fade: rl.Texture | None = None
def _prepare_compact_fade(self) -> None:
if self._fade is None:
self._fade = _load_fade(self._fade_path)
def render(self, state: OnroadState) -> None:
if state.camera_available and self.camera_layer is None:
raise ValueError("Camera observation requires a camera renderer")
if self.fonts.profile == Profile.LARGE:
self._large(state)
else:
self._compact(state)
def _slc_actions(self, state: OnroadState) -> None:
controls = slc_controls(self.fonts.profile, state)
feedback = state.speed_limit.action_feedback
if (self.fonts.profile == Profile.LARGE and feedback and state.alert.size == AlertSize.NONE and
placement(state.customization, self.fonts.profile, 'speed_limit_actions')['enabled']):
saved = placement(state.customization, self.fonts.profile, 'speed_limit_actions')
draw_ink(self.fonts, feedback, FontRole.MEDIUM, 30, saved['x'], saved['y'] + (122 if controls else 48), MUTED)
if not controls:
return
large = self.fonts.profile == Profile.LARGE
size, cy = ACTION_TEXT[self.fonts.profile]
_, dy = offset(state.customization, self.fonts.profile, 'speed_limit_actions')
cy += dy
role = FontRole.MEDIUM
a, b = self.fonts.vertical_ink('Accept Decline', role, size)
color = rl.Color(*rgba(state.customization, 'text', self.fonts.profile, 'speed_limit_actions'))
if len(controls) == 2:
center_x = (controls[0].bounds[2] + controls[1].bounds[0]) / 2
extent = 26 if large else 18
draw_line(self.fonts.profile, rl.Vector2(center_x, cy - extent), rl.Vector2(center_x, cy + extent),
2 if large else 1, rl.Color(155, 178, 173, 170))
for control in controls:
left, _, right, _ = control.bounds
measured = self.fonts.measure(control.label, role, size)
x = (left + right - measured.width) / 2
draw_text(self.fonts, control.label, role, size, x, cy - (b - a) / 2 - a, color)
if getattr(self, 'pressed_action', lambda: None)() == control.request:
y = cy + (b - a) / 2 + (8 if large else 4)
draw_line(self.fonts.profile, rl.Vector2(x, y), rl.Vector2(x + measured.width, y), 3 if large else 2, color)
def _preview_badge(self, state: OnroadState) -> None:
"""Keep synthetic replay visibly identified even over a full alert."""
preview = state.visual_preview
if preview is None or preview.layout_sample:
return
flags = []
if preview.cem_reason is not None:
flags.append('CEM')
if preview.curve_curvature is not None:
flags.append('CSC')
if not flags:
return
large = self.fonts.profile == Profile.LARGE
panel = rl.Rectangle(65, 966, 570, 74) if large else rl.Rectangle(8, 194, 310, 40)
rl.draw_rectangle_rounded(panel, 0.18, 6, rl.Color(12, 24, 32, 245))
rl.draw_rectangle_rounded_lines_ex(panel, 0.18, 6, 2, rl.Color(112, 192, 216, 255))
self.fonts.draw('SYNTHETIC REPLAY PREVIEW', FontRole.SEMI_BOLD,
22 if large else 14, panel.x + 12, panel.y + (8 if large else 4))
self.fonts.draw(' + '.join(flags), FontRole.SEMI_BOLD,
18 if large else 11, panel.x + 12, panel.y + (39 if large else 23))
def _traffic_badge(self, state: OnroadState, y: int) -> None:
if state.traffic_display is None or state.alert.size == AlertSize.FULL:
return
traffic_color = (rl.Color(200, 32, 48, 255) if state.traffic_display.state == 'active' else
rl.Color(255, 155, 63, 255) if state.traffic_display.state in ('paused', 'unavailable_profile', 'unavailable_source') else
rl.Color(165, 175, 180, 255))
x = 1390 + (self.projection_viewport[0] if self.projection_viewport else state.viewport_width) - 1860
self.fonts.draw(state.traffic_display.label, FontRole.SEMI_BOLD, 25, x, y, traffic_color)
def _driving_mode(self, state: OnroadState, center_shift: float = 0) -> None:
profile = self.fonts.profile
position = placement(state.customization, profile, MODE_WIDGET)
if (not position["enabled"] or state.alert.size != AlertSize.NONE or
state.appearance.camera_view == CameraViewChoice.DRIVER or state.reverse_driver_camera):
return
width, height = widget_size(state.customization, profile, MODE_WIDGET)
size = 40 if profile == Profile.LARGE else 18
role = FontRole.SEMI_BOLD
lines = [""]
for word in driving_mode_description(state).split():
candidate = f"{lines[-1]} {word}".strip()
if lines[-1] and self.fonts.measure(candidate, role, size).width > width - 4:
lines.append(word)
else:
lines[-1] = candidate
line_height = size * profile.font_scale * 1.2
top = position["y"] + (height - len(lines) * line_height) / 2
color = rl.Color(*rgba(state.customization, "text", profile, MODE_WIDGET))
shadow = rl.Color(0, 0, 0, min(color.a, 170))
shadow_offset = 2 if profile == Profile.LARGE else 1
for index, text in enumerate(lines):
x = position["x"] + center_shift + (width - self.fonts.measure(text, role, size).width) / 2
ink_top, ink_bottom = self.fonts.vertical_ink(text, role, size)
y = top + index * line_height + (line_height - ink_bottom + ink_top) / 2 - ink_top
self.fonts.draw(text, role, size, x + shadow_offset, y + shadow_offset, shadow)
self.fonts.draw(text, role, size, x, y, color)
def _clock(self, state: OnroadState, center_shift: float = 0) -> None:
profile = self.fonts.profile
position = placement(state.customization, profile, CLOCK_WIDGET)
if (not position["enabled"] or state.alert.size != AlertSize.NONE or
state.appearance.camera_view == CameraViewChoice.DRIVER or state.reverse_driver_camera):
return
width, height = widget_size(state.customization, profile, CLOCK_WIDGET)
size = 42 if profile == Profile.LARGE else 20
role = FontRole.SEMI_BOLD
now = self.wall_clock()
if now is None:
return
text = clock_text(now, use_24_hour=state.customization.get("clock24Hour", False))
measured = self.fonts.measure(text, role, size)
x = position["x"] + center_shift + (width - measured.width) / 2
y = position["y"] + (height - measured.height) / 2
color = rl.Color(*rgba(state.customization, "text", profile, CLOCK_WIDGET))
offset_px = 2 if profile == Profile.LARGE else 1
self.fonts.draw(text, role, size, x + offset_px, y + offset_px, rl.Color(0, 0, 0, min(color.a, 170)))
self.fonts.draw(text, role, size, x, y, color)
def _ordered_widgets(self, content, state, *, driver_camera=False, stock_layer=None, only_widget=None):
profile = str(self.fonts.profile)
large = profile == "large"
width = self.projection_viewport[0] if self.projection_viewport else state.viewport_width
callbacks = {}
def submit(key, draw):
callbacks[key] = draw
pip = getattr(self, "pip_layer", None) if state.camera_available else getattr(self, "preview_pip_layer", None)
if pip is not None and (large or not driver_camera and state.appearance.camera_view != CameraViewChoice.NONE):
pip(content, state, submit=submit)
if large:
if "nav_map" in state.customization["layouts"]["large"] and state.alert.size != AlertSize.FULL:
if map_layer := getattr(self, "map_layer", None):
submit("nav_map", lambda: map_layer(content, state))
if "nav_card" in state.customization["layouts"]["large"]:
submit("nav_card", lambda: self.navigation.render(state))
for key in ("nav_home", "nav_work", "bookmark"):
if key in state.customization["layouts"]["large"]:
submit(key, lambda key=key: self.navigation_favorites.render(key, state))
if "now_playing" in state.customization["layouts"]["large"]:
if now_playing := getattr(self, "now_playing", None):
submit("now_playing", lambda: now_playing.render(state))
if "jetlink_status" in state.customization["layouts"]["large"]:
if jetlink := getattr(self, "jetlink_status", None):
submit("jetlink_status", lambda: jetlink.render(state))
if "car_exit" in state.customization["layouts"]["large"]:
if exit_layer := getattr(self, "projection_exit_layer", None):
submit("car_exit", lambda: exit_layer(state))
if state.alert.size != AlertSize.FULL:
if placement(state.customization, profile, "cruise_limits")["enabled"]:
submit("cruise_limits", lambda: self.unified_speed.render(content, state))
submit("speed_limit_actions", lambda: self._slc_actions(state))
if gauge := getattr(self, "cem_gauge", None):
submit("cem_gauge", lambda: gauge.render(state))
if placement(state.customization, profile, "current_speed")["enabled"]:
speed_content = rl.Rectangle(content.x + (width - 1860) / 2, content.y, content.width, content.height)
submit("current_speed", lambda: self.current_speed.render(speed_content, state))
if state.alert.size == AlertSize.NONE:
if placement(state.customization, profile, "steering_wheel")["enabled"]:
submit("steering_wheel", lambda: self.steering_wheel.render(content, state))
if placement(state.customization, profile, "torque_bar")["enabled"]:
submit("torque_bar", lambda: self.torque_bar.render(content, state, width if self.projection_viewport else 2160))
if state.stopped_duration_s is not None:
submit(STOPPED_TIMER_WIDGET, lambda: self._stopped_timer(state, state.stopped_duration_s))
else:
if state.alert.size == AlertSize.NONE and not driver_camera:
submit("speed_limit", lambda: self.compact_hud._speed_limit_sign(state))
submit("max_speed", lambda: self.compact_hud.render_max_speed(state))
submit("steering_wheel", lambda: self.compact_hud.render_steering_wheel(state))
submit("speed_limit_actions", lambda: self._slc_actions(state))
if placement(state.customization, profile, "torque_bar")["enabled"]:
submit("torque_bar", lambda: self.torque_bar.render(content, state, 536))
if state.stopped_duration_s is not None:
submit(STOPPED_TIMER_WIDGET, lambda: self._stopped_timer(state, state.stopped_duration_s))
if not driver_camera:
if stock_layer is not None:
submit("model_confidence", lambda: stock_layer(rl.Rectangle(0, 0, 536, 240), state))
submit("following_distance", lambda: self.compact_sidebar.render_personality(rl.Rectangle(0, 0, 536, 240), state))
else:
for key in RAIL_WIDGETS:
submit(key, lambda key=key: self.compact_sidebar.render(rl.Rectangle(0, 0, 536, 240), state, widget=key))
if monitor := getattr(self, "driver_monitor_layer", None):
submit("driver_monitor", lambda: monitor(content, state))
submit(MODE_WIDGET, lambda: self._driving_mode(state, (width - 1860) / 2 if large else 0))
submit(CLOCK_WIDGET, lambda: self._clock(state, (width - 1860) / 2 if large else 0))
for key in widget_order(state.customization, profile):
if only_widget is not None and key != only_widget:
continue
if draw := callbacks.get(key):
draw()
def _large(self, state: OnroadState) -> None:
width, height = self.projection_viewport or (state.viewport_width, 1080)
right_shift = width - 1860
frame = rl.Rectangle(0, 0, width, height)
content = rl.Rectangle(30, 30, width - 60, height - 60)
# Current CameraView's no-frame placeholder stays at the disengaged blue.
color = rl.Color(18, 40, 57, 255)
edge = axis_status_color(state)
rl.draw_rectangle_rec(content, color)
clip.begin_scissor_mode(30, 30, int(content.width), int(content.height))
try:
if self.camera_layer is not None and state.camera_available:
self.camera_layer(content, state)
elif self.background_layer is not None:
self.background_layer(content, state)
ordered = bool(state.customization.get("widgetOrder", {}).get("large"))
if ordered:
rl.draw_rectangle_gradient_v(30, 30, int(content.width), 300, rl.Color(0, 0, 0, 114), rl.BLANK)
self._ordered_widgets(content, state)
if self.extra_overlays is not None:
self.extra_overlays(content, state)
else:
pip_layer = getattr(self, "pip_layer", None)
if pip_layer is not None and state.camera_available:
pip_layer(content, state)
map_layer = getattr(self, "map_layer", None)
if map_layer is not None and state.alert.size != AlertSize.FULL:
map_layer(content, state)
rl.draw_rectangle_gradient_v(30, 30, int(content.width), 300, rl.Color(0, 0, 0, 114), rl.BLANK)
if state.alert.size != AlertSize.FULL:
if placement(state.customization, "large", "cruise_limits")["enabled"]:
self.unified_speed.render(content, state)
self._slc_actions(state)
if gauge := getattr(self, "cem_gauge", None):
gauge.render(state)
if placement(state.customization, "large", "current_speed")["enabled"]:
speed_content = rl.Rectangle(content.x + right_shift / 2, content.y, content.width, content.height)
self.current_speed.render(speed_content, state)
if state.alert.size == AlertSize.NONE:
if placement(state.customization, "large", "steering_wheel")["enabled"]:
self.steering_wheel.render(content, state)
if placement(state.customization, "large", "torque_bar")["enabled"]:
self.torque_bar.render(content, state, width if self.projection_viewport else 2160)
if state.stopped_duration_s is not None:
self._stopped_timer(state, state.stopped_duration_s)
if self.extra_overlays is not None:
self.extra_overlays(content, state)
if monitor_layer := getattr(self, "driver_monitor_layer", None):
monitor_layer(content, state)
if self.projection_viewport and "nav_card" not in state.customization["layouts"]["large"]:
rl.rl_push_matrix()
try:
rl.rl_translatef(right_shift, 0, 0)
self.navigation.render(state)
finally:
rl.rl_pop_matrix()
elif not ordered or "nav_card" not in state.customization["layouts"]["large"]:
self.navigation.render(state)
if not ordered:
for key in ("nav_home", "nav_work", "bookmark"):
self.navigation_favorites.render(key, state)
if now_playing := getattr(self, "now_playing", None):
now_playing.render(state)
if jetlink := getattr(self, "jetlink_status", None):
jetlink.render(state)
self._driving_mode(state, right_shift / 2)
self._clock(state, right_shift / 2)
if exit_layer := getattr(self, "projection_exit_layer", None):
exit_layer(state)
self.alert.render(content, state.alert)
finally:
clip.end_scissor_mode()
if not self.projection_viewport and state.appearance.show_developer_sidebar:
render_live_sidebar(self.fonts, rl.Rectangle(width, 0, SIDEBAR_WIDTH, height), state.developer_metrics)
# Projection has its own permanent escape control in this corner; the native
# favorite-menu hint would overlap it and does not control projected input.
if not self.projection_viewport and state.alert.size != AlertSize.FULL:
render_corner_hint(content, cache=self._corner_cache)
label = status_label(state)
if label is not None:
preview = state.visual_preview
curve_color = glow_color(intensity(preview.curve_curvature)) if preview is not None and preview.curve_curvature is not None else \
glow_color(intensity(state.curve.road_curvature)) if curve_controlling(state) and state.curve is not None else \
rl.Color(112, 192, 216, 255)
self.fonts.draw(label, FontRole.SEMI_BOLD, 29, 1390 + right_shift, 56,
curve_color)
self._traffic_badge(state, 56 + 45 * int(label is not None))
render_glow(content, state, border_width=30, time_s=state.observed_ns / 1e9 if state.observed_ns else rl.get_time())
rl.draw_rectangle_lines_ex(frame, 30, rl.BLACK)
rl.draw_rectangle_rounded_lines_ex(content, 0.12, 10, 30, edge)
render_large_half_borders(state, state.observed_ns / 1e9 if state.observed_ns else rl.get_time(), content, int(height))
self._preview_badge(state)
def _compact(self, state: OnroadState) -> None:
frame = rl.Rectangle(0, 0, 476, 240)
driver_camera = state.appearance.camera_view == CameraViewChoice.DRIVER or state.reverse_driver_camera
stock_layer = getattr(self, "stock_confidence_layer", None)
source_ns = state.stock_confidence_source_stamp_ns if state.stock_confidence_source_fresh else None
use_stock = (not driver_camera and state.appearance.camera_view != CameraViewChoice.NONE and
state.appearance.show_stock_confidence_ball and source_ns is not None and stock_layer is not None)
if use_stock:
last_source_ns = getattr(self, "_stock_confidence_last_source_ns", None)
last_drive_frame = getattr(self, "_stock_confidence_last_drive_frame", None)
if (last_source_ns is None or last_drive_frame != state.stock_confidence_drive_frame or
source_ns < last_source_ns or source_ns - last_source_ns > 200_000_000):
reset = getattr(self, "stock_confidence_reset", None)
if reset is not None:
reset()
self._stock_confidence_last_source_ns = source_ns
self._stock_confidence_last_drive_frame = state.stock_confidence_drive_frame
else:
self._stock_confidence_last_source_ns = None
color = axis_status_color(state)
if state.alert.size != AlertSize.NONE and (
not state.driver_override_active or state.alert.critical or state.alert.size == AlertSize.FULL):
color = rl.Color(18, 40, 57, 255)
rl.draw_rectangle(0, 0, 536, 240, rl.BLACK)
clip.begin_scissor_mode(0, 0, 476, 240)
try:
if self.camera_layer is not None and state.camera_available:
self.camera_layer(frame, state)
elif self.background_layer is not None:
self.background_layer(frame, state)
ordered = bool(state.customization.get("widgetOrder", {}).get("compact"))
if ordered:
self._prepare_compact_fade()
rl.draw_texture_ex(self._fade, rl.Vector2(0, 0), 0, 1, rl.WHITE)
clip.end_scissor_mode()
clip.begin_scissor_mode(0, 0, 536, 240)
self._ordered_widgets(frame, state, driver_camera=driver_camera, stock_layer=stock_layer if use_stock else None)
clip.end_scissor_mode()
clip.begin_scissor_mode(0, 0, 476, 240)
if self.extra_overlays is not None:
self.extra_overlays(frame, state)
else:
pip_layer = getattr(self, "pip_layer", None)
if pip_layer is not None and state.camera_available and not driver_camera and \
state.appearance.camera_view != CameraViewChoice.NONE:
pip_layer(frame, state)
self._prepare_compact_fade()
rl.draw_texture_ex(self._fade, rl.Vector2(0, 0), 0, 1, rl.WHITE)
if state.alert.size == AlertSize.NONE and not driver_camera:
self.compact_hud.render(state)
if placement(state.customization, "compact", "torque_bar")["enabled"]:
self.torque_bar.render(frame, state, 536)
self._slc_actions(state)
if state.stopped_duration_s is not None:
self._stopped_timer(state, state.stopped_duration_s)
if self.extra_overlays is not None:
self.extra_overlays(frame, state)
if monitor_layer := getattr(self, "driver_monitor_layer", None):
monitor_layer(frame, state)
self.navigation.render(state)
if not ordered:
self._driving_mode(state)
self._clock(state)
signals = state.border_signals
direction = (-1 if signals.left_blinker else 1 if signals.right_blinker else 0) if signals else 0
self.alert.render(frame, state.alert, signal_direction=direction)
finally:
clip.end_scissor_mode()
self._preview_badge(state)
if not driver_camera and not state.customization.get("widgetOrder", {}).get("compact"):
rail = rl.Rectangle(0, 0, 536, 240)
if use_stock:
stock_layer(rail, state)
self.compact_sidebar.render_personality(rail, state)
else:
self.compact_sidebar.render(rail, state)
clip.begin_scissor_mode(0, 0, 476, 240)
try:
rl.draw_rectangle_rounded_lines_ex(rl.Rectangle(4, 4, 468, 232), 0.12, 16, 8, color)
finally:
clip.end_scissor_mode()
render_compact_half_borders(state, state.observed_ns / 1e9 if state.observed_ns else rl.get_time())
def _stopped_timer(self, state: OnroadState, seconds: int) -> None:
profile = str(self.fonts.profile)
position = state.customization["layouts"][profile].get(STOPPED_TIMER_WIDGET)
if position is None or not position["enabled"]:
return
width, height = timer_size(profile)
scale = height / 112
minutes, remainder = duration_text(seconds)
color = rl.Color(*duration_color(seconds), 255)
# Line centers sit 53 and 101 px into the widget box.
for text, role, initial_size, minimum_size, center_y, tint in (
(minutes, FontRole.BOLD, 78, 28, 53, color),
(remainder, FontRole.MEDIUM, 35, 16, 101, rl.Color(255, 255, 255, 242)),
):
size = round(initial_size * scale)
while size > round(minimum_size * scale) and self.fonts.measure(text, role, size).width > width:
size -= 2
measured = self.fonts.measure(text, role, size)
x = position["x"] + (width - measured.width) / 2
y = position["y"] + center_y * scale - measured.height / 2
self.fonts.draw(text, role, size, x + 2, y + 2, rl.Color(0, 0, 0, 170))
self.fonts.draw(text, role, size, x, y, tint)
def close(self) -> None:
gui_app.remove_render_prepare(self._corner_cache.prepare)
self._corner_cache.close()
self.navigation.close()
if self.fonts.profile == Profile.LARGE:
self.unified_speed.close()
self.steering_wheel.close()
else:
self.compact_hud.close()
if self._fade is not None and rl.is_window_ready():
rl.unload_texture(self._fade)
self._fade = None