This commit is contained in:
rav4kumar
2026-03-15 11:56:30 -07:00
parent 37ac33fbcc
commit cce352cccb
3 changed files with 504 additions and 3 deletions
+13 -3
View File
@@ -3,6 +3,7 @@ import cereal.messaging as messaging
from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout
from openpilot.selfdrive.ui.mici.layouts.settings.settings import SettingsLayout
from openpilot.selfdrive.ui.mici.layouts.offroad_alerts import MiciOffroadAlerts
from openpilot.selfdrive.ui.mici.layouts.mapd_panel import MapdInfoPanel
from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView
from openpilot.selfdrive.ui.ui_state import device, ui_state
from openpilot.selfdrive.ui.mici.layouts.onboarding import OnboardingWindow
@@ -33,15 +34,17 @@ class MiciMainLayout(Scroller):
self._alerts_layout = MiciOffroadAlerts()
self._settings_layout = SettingsLayout()
self._onroad_layout = AugmentedRoadView(bookmark_callback=self._on_bookmark_clicked)
self._mapd_panel = MapdInfoPanel()
# Initialize widget rects
for widget in (self._home_layout, self._settings_layout, self._alerts_layout, self._onroad_layout):
for widget in (self._home_layout, self._settings_layout, self._alerts_layout, self._onroad_layout,self._mapd_panel):
# TODO: set parent rect and use it if never passed rect from render (like in Scroller)
widget.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
self._scroller.add_widgets([
self._alerts_layout,
self._home_layout,
self._mapd_panel,
self._onroad_layout,
])
self._scroller.set_reset_scroll_at_show(False)
@@ -65,6 +68,11 @@ class MiciMainLayout(Scroller):
self._onroad_layout.set_click_callback(lambda: self._scroll_to(self._home_layout))
device.add_interactive_timeout_callback(self._on_interactive_timeout)
def _is_on_side_panel(self) -> bool: # TODO: remove sunny not gonna like it ?
onroad_x = self._onroad_layout.rect.x
current_scroll = self._scroller.scroll_panel.get_offset()
return abs(current_scroll - onroad_x) > self._rect.width / 2
def _scroll_to(self, layout: Widget):
layout_x = int(layout.rect.x)
self._scroller.scroll_to(layout_x, smooth=True)
@@ -97,13 +105,15 @@ class MiciMainLayout(Scroller):
# FIXME: these two pops can interrupt user interacting in the settings
if self._onroad_time_delay is not None and rl.get_time() - self._onroad_time_delay >= ONROAD_DELAY:
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
if not self._is_on_side_panel():
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
self._onroad_time_delay = None
# When car leaves standstill, pop nav stack and scroll to onroad
CS = ui_state.sm["carState"]
if not CS.standstill and self._prev_standstill:
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
if not self._is_on_side_panel():
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
self._prev_standstill = CS.standstill
def _on_interactive_timeout(self):
+281
View File
@@ -0,0 +1,281 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from dataclasses import dataclass
from openpilot.common.constants import CV
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets import Widget
METER_TO_KM = 0.001
METER_TO_MILE = 0.000621371
@dataclass(frozen=True)
class MapdPanelColors:
white: rl.Color = rl.WHITE
black: rl.Color = rl.BLACK
red: rl.Color = rl.Color(255, 0, 0, 255)
green: rl.Color = rl.Color(0, 255, 0, 255)
grey: rl.Color = rl.Color(190, 195, 190, 255)
light_grey: rl.Color = rl.Color(200, 200, 200, 255)
dark_grey: rl.Color = rl.Color(100, 100, 100, 255)
bg_dark: rl.Color = rl.Color(0, 0, 0, 255)
card_bg: rl.Color = rl.Color(50, 50, 50, 200)
COLORS = MapdPanelColors()
class MapdInfoPanel(Widget):
def __init__(self):
super().__init__()
self.speed_limit: float = 0.0
self.speed_limit_valid: bool = False
self.speed_limit_offset: float = 0.0
self.next_speed_limit: float = 0.0
self.next_speed_limit_distance: float = 0.0
self.road_name: str = ""
self.current_speed: float = 0.0
self._sign_slide: float = 0.0
self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
self._font_semi_bold: rl.Font = gui_app.font(FontWeight.SEMI_BOLD)
self._font_medium: rl.Font = gui_app.font(FontWeight.MEDIUM)
self._marquee_offset: float = 0.0
self._marquee_direction: int = 1
self._marquee_pause_timer: float = 0.0
self._marquee_speed: float = 40.0
self._marquee_pause_duration: float = 1.5
def _update_state(self) -> None:
sm = ui_state.sm
speed_conv = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
if sm.valid["longitudinalPlanSP"]:
lp_sp = sm["longitudinalPlanSP"]
resolver = lp_sp.speedLimit.resolver
self.speed_limit = resolver.speedLimit * speed_conv
self.speed_limit_valid = resolver.speedLimitValid
self.speed_limit_offset = resolver.speedLimitOffset * speed_conv
if sm.valid["liveMapDataSP"]:
lmd = sm["liveMapDataSP"]
self.next_speed_limit = lmd.speedLimitAhead * speed_conv
self.next_speed_limit_distance = lmd.speedLimitAheadDistance
self.road_name = lmd.roadName
if sm.updated["carState"]:
self.current_speed = sm["carState"].vEgo * speed_conv
def _render(self, rect: rl.Rectangle) -> None:
self._update_state()
rl.draw_rectangle(int(rect.x), int(rect.y), int(rect.width), int(rect.height), COLORS.bg_dark)
margin = 20
mid_y = rect.y + rect.height / 2
left_x = rect.x + margin
unit = tr("km/h") if ui_state.is_metric else tr("MPH")
rl.draw_text_ex(self._font_semi_bold, unit, rl.Vector2(left_x, mid_y - 95), 38, 0, COLORS.grey)
speed_val = str(round(self.current_speed))
if self.speed_limit_valid and self.current_speed > self.speed_limit:
speed_color = COLORS.red
else:
speed_color = COLORS.white
rl.draw_text_ex(self._font_bold, speed_val, rl.Vector2(left_x, mid_y - 60), 110, 0, speed_color)
sign_width = 135 if ui_state.is_metric else 135
sign_height = 135 if ui_state.is_metric else 175
has_next = self.next_speed_limit > 0 and self.next_speed_limit != self.speed_limit
target_slide = 1.0 if has_next else 0.0
slide_speed = 3.0 * rl.get_frame_time()
if self._sign_slide < target_slide:
self._sign_slide = min(self._sign_slide + slide_speed, target_slide)
elif self._sign_slide > target_slide:
self._sign_slide = max(self._sign_slide - slide_speed, target_slide)
next_w = int(sign_width * 0.7)
next_h = int(sign_height * 0.7)
next_peek = int(next_w * 0.85) + 5
centered_x = rect.x + rect.width - sign_width - margin
shifted_x = rect.x + rect.width - sign_width - margin - next_peek
sign_x = centered_x + (shifted_x - centered_x) * self._sign_slide
sign_y = rect.y + (rect.height - sign_height) / 2
road_y = mid_y + 55
road_width = sign_x - left_x - margin
self._draw_road_name(left_x, road_y, road_width)
if has_next and self._sign_slide > 0.01:
next_val = str(round(self.next_speed_limit))
dist_str = self._format_distance(self.next_speed_limit_distance)
next_x = sign_x + sign_width - int(next_w * 0.15)
next_y = sign_y + (sign_height - next_h) / 2
next_speed_color = COLORS.black
if ui_state.is_metric:
self._draw_vienna_sign(next_x, next_y, next_w, next_h, next_val, next_speed_color, is_upcoming=True)
else:
self._draw_mutcd_sign(next_x, next_y, next_w, next_h, next_val, next_speed_color, is_upcoming=True)
dist_size = measure_text_cached(self._font_medium, dist_str, 24)
rl.draw_text_ex(self._font_medium, dist_str, rl.Vector2(next_x + next_w / 2 - dist_size.x / 2, next_y + next_h + 4), 24, 0, COLORS.grey)
self._draw_speed_limit_sign(sign_x, sign_y, sign_width, sign_height)
if self.speed_limit_offset != 0 and self.speed_limit_valid:
offset_val = str(abs(round(self.speed_limit_offset)))
badge_sz = 34
badge_x = sign_x + sign_width - badge_sz * 0.85
badge_y = sign_y - badge_sz * 0.15
if ui_state.is_metric:
badge_r = badge_sz / 2
badge_cx = badge_x + badge_r
badge_cy = badge_y + badge_r
rl.draw_circle(int(badge_cx), int(badge_cy), badge_r + 2, COLORS.dark_grey)
rl.draw_circle(int(badge_cx), int(badge_cy), badge_r, rl.Color(60, 60, 60, 255))
self._draw_text_centered(self._font_bold, offset_val, 20, rl.Vector2(badge_cx, badge_cy), COLORS.white)
else:
mutcd_badge_x = sign_x + sign_width - badge_sz * 0.65
mutcd_badge_y = sign_y - badge_sz * 0.35
badge_rect = rl.Rectangle(mutcd_badge_x, mutcd_badge_y, badge_sz, badge_sz)
rl.draw_rectangle_rounded(badge_rect, 0.25, 10, rl.Color(60, 60, 60, 255))
rl.draw_rectangle_rounded_lines_ex(badge_rect, 0.25, 10, 2, COLORS.dark_grey)
self._draw_text_centered(self._font_bold, offset_val, 20, rl.Vector2(mutcd_badge_x + badge_sz / 2, mutcd_badge_y + badge_sz / 2), COLORS.white)
# SCC
speed_size = measure_text_cached(self._font_bold, speed_val, 110)
scc_x = left_x + speed_size.x + 14
scc_y = mid_y - 50
self._draw_scc_icons(scc_x, scc_y)
def _draw_scc_icons(self, x: float, y: float) -> None:
sm = ui_state.sm
if not sm.valid["longitudinalPlanSP"]:
return
scc = sm["longitudinalPlanSP"].smartCruiseControl
box_w, box_h = 56, 36
gap = 6
drawn = 0
for label, active in [("V", scc.vision.active), ("M", scc.map.active)]:
if not active:
continue
bx = x
by = y + drawn * (box_h + gap)
rl.draw_rectangle_rounded(rl.Rectangle(bx, by, box_w, box_h), 0.3, 10, COLORS.green)
self._draw_text_centered(self._font_bold, label, 24, rl.Vector2(bx + box_w / 2, by + box_h / 2), COLORS.black)
drawn += 1
def _draw_speed_limit_sign(self, x: float, y: float, sign_width: float, sign_height: float) -> None:
speed_str = str(round(self.speed_limit)) if self.speed_limit_valid else "--"
speed_color = COLORS.black if not self.speed_limit_valid or self.current_speed <= self.speed_limit else COLORS.red
if ui_state.is_metric:
self._draw_vienna_sign(x, y, sign_width, sign_height, speed_str, speed_color, is_upcoming=False)
else:
self._draw_mutcd_sign(x, y, sign_width, sign_height, speed_str, speed_color, is_upcoming=False)
def _draw_road_name(self, x: float, y: float, width: float) -> None:
road_display = self.road_name if self.road_name else "--"
font_size = 30
road_size = measure_text_cached(self._font_semi_bold, road_display, font_size)
text_width = road_size.x
if text_width <= width:
self._marquee_offset = 0.0
self._marquee_direction = 1
self._marquee_pause_timer = 0.0
rl.draw_text_ex(self._font_semi_bold, road_display, rl.Vector2(x, y), font_size, 0, COLORS.white)
else:
overflow = text_width - width
dt = rl.get_frame_time()
if self._marquee_pause_timer > 0:
self._marquee_pause_timer -= dt
else:
self._marquee_offset += self._marquee_direction * self._marquee_speed * dt
if self._marquee_offset >= overflow:
self._marquee_offset = overflow
self._marquee_direction = -1
self._marquee_pause_timer = self._marquee_pause_duration
elif self._marquee_offset <= 0:
self._marquee_offset = 0
self._marquee_direction = 1
self._marquee_pause_timer = self._marquee_pause_duration
rl.begin_scissor_mode(int(x), int(y), int(width), int(road_size.y + 4))
text_pos = rl.Vector2(x - self._marquee_offset, y)
rl.draw_text_ex(self._font_semi_bold, road_display, text_pos, font_size, 0, COLORS.white)
rl.end_scissor_mode()
def _draw_vienna_sign(self, x: float, y: float, width: float, height: float, speed_str: str, speed_color: rl.Color, is_upcoming: bool = False) -> None:
center = rl.Vector2(x + width / 2, y + height / 2)
outer_radius = min(width, height) / 2
rl.draw_circle_v(center, outer_radius, COLORS.white)
ring_width = outer_radius * 0.18
rl.draw_ring(center, outer_radius - ring_width, outer_radius, 0, 360, 36, COLORS.red)
font_size = outer_radius * (0.7 if len(speed_str) >= 3 else 0.9)
text_size = measure_text_cached(self._font_bold, speed_str, int(font_size))
text_pos = rl.Vector2(center.x - text_size.x / 2, center.y - text_size.y / 2)
rl.draw_text_ex(self._font_bold, speed_str, text_pos, font_size, 0, speed_color)
def _draw_mutcd_sign(self, x: float, y: float, width: float, height: float, speed_str: str, speed_color: rl.Color, is_upcoming: bool = False) -> None:
sign_rect = rl.Rectangle(x, y, width, height)
rl.draw_rectangle_rounded(sign_rect, 0.35, 10, COLORS.white)
inset = max(4, width * 0.05)
inner_rect = rl.Rectangle(x + inset, y + inset, width - inset * 2, height - inset * 2)
outer_radius = 0.35 * width / 2.0
inner_radius = outer_radius - inset
inner_roundness = inner_radius / (inner_rect.width / 2.0)
rl.draw_rectangle_rounded_lines_ex(inner_rect, inner_roundness, 10, 3, COLORS.black)
mid_x = x + width / 2
label_size = max(16, int(width * 0.22))
if is_upcoming:
self._draw_text_centered(self._font_bold, tr("AHEAD"), label_size, rl.Vector2(mid_x, y + height * 0.27), COLORS.black)
else:
self._draw_text_centered(self._font_bold, tr("SPEED"), label_size, rl.Vector2(mid_x, y + height * 0.18), COLORS.black)
self._draw_text_centered(self._font_bold, tr("LIMIT"), label_size, rl.Vector2(mid_x, y + height * 0.36), COLORS.black)
speed_font_size = int(width * 0.42) if len(speed_str) >= 3 else int(width * 0.50)
self._draw_text_centered(self._font_bold, speed_str, speed_font_size, rl.Vector2(mid_x, y + height * 0.66), speed_color)
def _draw_text_centered(self, font, text, size, pos_center, color):
sz = measure_text_cached(font, text, size)
rl.draw_text_ex(font, text, rl.Vector2(pos_center.x - sz.x / 2, pos_center.y - sz.y / 2), size, 0, color)
def _format_distance(self, distance: float) -> str:
if ui_state.is_metric:
if distance < 50:
return tr("Near")
if distance >= 1000:
return f"{distance * METER_TO_KM:.1f}" + tr("km")
if distance < 200:
rounded = max(10, int(distance / 10) * 10)
else:
rounded = int(distance / 100) * 100
return str(rounded) + tr("m")
else:
distance_mi = distance * METER_TO_MILE
if distance_mi < 0.1:
return tr("Near")
return f"{distance_mi:.1f}" + tr("mi")
+210
View File
@@ -0,0 +1,210 @@
import pyray as rl
from dataclasses import dataclass
from openpilot.common.constants import CV
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets import Widget
METER_TO_KM = 0.001
METER_TO_MILE = 0.000621371
SPEED_LIMIT_WIDTH_METRIC = 200
SPEED_LIMIT_WIDTH_IMPERIAL = 172
SPEED_LIMIT_HEIGHT = 204
@dataclass(frozen=True)
class SpeedLimitColors:
white: rl.Color = rl.WHITE
black: rl.Color = rl.BLACK
red: rl.Color = rl.Color(255, 0, 0, 255)
grey: rl.Color = rl.Color(145, 155, 149, 241)
green: rl.Color = rl.Color(0, 255, 0, 255)
light_grey: rl.Color = rl.Color(200, 200, 200, 255)
dark_grey: rl.Color = rl.Color(180, 180, 180, 255)
border_grey: rl.Color = rl.Color(77, 77, 77, 255)
black_translucent: rl.Color = rl.Color(0, 0, 0, 166)
border_translucent: rl.Color = rl.Color(255, 255, 255, 75)
COLORS = SpeedLimitColors()
class SpeedLimitRenderer(Widget):
def __init__(self):
super().__init__()
self.speed_limit: float = 0.0
self.speed_limit_valid: bool = False
self.speed_limit_offset: float = 0.0
self.next_speed_limit: float = 0.0
self.next_speed_limit_distance: float = 0.0
self.road_name: str = ""
self.current_speed: float = 0.0
self._hide_for_hud: bool = False
self._font_semi_bold: rl.Font = gui_app.font(FontWeight.SEMI_BOLD)
self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
self._font_medium: rl.Font = gui_app.font(FontWeight.MEDIUM)
def set_hide_for_hud(self, hide: bool):
self._hide_for_hud = hide
def _update_state(self) -> None:
sm = ui_state.sm
if sm.recv_frame["carState"] < ui_state.started_frame:
return
speed_conv = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
if sm.updated["longitudinalPlanSP"]:
lp_sp = sm["longitudinalPlanSP"]
resolver = lp_sp.speedLimit.resolver
self.speed_limit = resolver.speedLimit * speed_conv
self.speed_limit_valid = resolver.speedLimitValid
self.speed_limit_offset = resolver.speedLimitOffset * speed_conv
if sm.updated["liveMapDataSP"]:
lmd = sm["liveMapDataSP"]
self.next_speed_limit = lmd.speedLimitAhead * speed_conv
self.next_speed_limit_distance = lmd.speedLimitAheadDistance
self.road_name = lmd.roadName
if sm.updated["carState"]:
self.current_speed = sm["carState"].vEgo * speed_conv
def _render(self, rect: rl.Rectangle) -> None:
self._update_state()
if self._hide_for_hud:
return
if not self.speed_limit_valid:
return
sign_width = SPEED_LIMIT_WIDTH_METRIC if ui_state.is_metric else SPEED_LIMIT_WIDTH_IMPERIAL
sign_height = SPEED_LIMIT_HEIGHT
sign_margin = 12
sign_x = rect.x + sign_margin
sign_y = rect.y + 45
sign_rect = rl.Rectangle(sign_x, sign_y, sign_width, sign_height)
speed_str = str(round(self.speed_limit))
speed_color = COLORS.white if self.current_speed <= self.speed_limit else COLORS.red
if ui_state.is_metric:
self._draw_vienna_sign(sign_rect, speed_str, speed_color)
else:
self._draw_mutcd_sign(sign_rect, speed_str, speed_color)
if self.speed_limit_offset != 0:
self._draw_offset_badge(sign_rect)
if self.next_speed_limit > 0 and self.next_speed_limit != self.speed_limit:
self._draw_upcoming_speed_limit(sign_rect)
def _draw_vienna_sign(self, sign_rect: rl.Rectangle, speed_str: str, speed_color: rl.Color) -> None:
"""Draw EU/Vienna sign."""
center = rl.Vector2(sign_rect.x + sign_rect.width / 2, sign_rect.y + sign_rect.height / 2)
outer_radius = min(sign_rect.width, sign_rect.height) / 2
circle_size = outer_radius * 2
rl.draw_circle_v(center, outer_radius, COLORS.white)
ring_width = outer_radius * 0.18
rl.draw_ring(center, outer_radius - ring_width, outer_radius, 0, 360, 36, COLORS.red)
font_size = circle_size * (0.35 if len(speed_str) >= 3 else 0.45)
text_size = measure_text_cached(self._font_bold, speed_str, int(font_size))
text_pos = rl.Vector2(center.x - text_size.x / 2, center.y - text_size.y / 2)
rl.draw_text_ex(self._font_bold, speed_str, text_pos, font_size, 0, speed_color)
def _draw_mutcd_sign(self, sign_rect: rl.Rectangle, speed_str: str, speed_color: rl.Color) -> None:
"""Draw US/Canada MUTCD sign."""
padding = 8
inner_rect = rl.Rectangle(sign_rect.x + padding, sign_rect.y + padding, sign_rect.width - (padding * 2), sign_rect.height - (padding * 2))
rl.draw_rectangle_rounded(inner_rect, 0.35, 10, COLORS.white)
rl.draw_rectangle_rounded_lines_ex(inner_rect, 0.35, 10, 4, COLORS.black)
border_width = 4
border_rect = rl.Rectangle(
inner_rect.x + border_width, inner_rect.y + border_width, inner_rect.width - (border_width * 2), inner_rect.height - (border_width * 2)
)
speed_text = tr("SPEED")
limit_text = tr("LIMIT")
speed_size = measure_text_cached(self._font_semi_bold, speed_text, 40)
limit_size = measure_text_cached(self._font_semi_bold, limit_text, 40)
speed_pos = rl.Vector2(border_rect.x + (border_rect.width - speed_size.x) / 2, sign_rect.y + 27)
limit_pos = rl.Vector2(border_rect.x + (border_rect.width - limit_size.x) / 2, sign_rect.y + 65)
rl.draw_text_ex(self._font_semi_bold, speed_text, speed_pos, 40, 0, COLORS.black)
rl.draw_text_ex(self._font_semi_bold, limit_text, limit_pos, 40, 0, COLORS.black)
font_size = 70 if len(speed_str) >= 3 else 90
speed_value_size = measure_text_cached(self._font_bold, speed_str, font_size)
speed_value_pos = rl.Vector2(border_rect.x + (border_rect.width - speed_value_size.x) / 2, sign_rect.y + 100)
rl.draw_text_ex(self._font_bold, speed_str, speed_value_pos, font_size, 0, speed_color)
def _draw_offset_badge(self, sign_rect: rl.Rectangle) -> None:
"""Draw speed limit offset"""
offset_str = ("+" if self.speed_limit_offset > 0 else "") + str(round(self.speed_limit_offset))
badge_width = 80
badge_height = 36
badge_x = sign_rect.x + (sign_rect.width - badge_width) / 2
badge_y = sign_rect.y + sign_rect.height + 4
badge_rect = rl.Rectangle(badge_x, badge_y, badge_width, badge_height)
rl.draw_rectangle_rounded(badge_rect, 0.5, 10, COLORS.black_translucent)
offset_font_size = 28
offset_size = measure_text_cached(self._font_semi_bold, offset_str, offset_font_size)
offset_pos = rl.Vector2(badge_x + (badge_width - offset_size.x) / 2, badge_y + (badge_height - offset_size.y) / 2)
rl.draw_text_ex(self._font_semi_bold, offset_str, offset_pos, offset_font_size, 0, COLORS.white)
def _draw_upcoming_speed_limit(self, sign_rect: rl.Rectangle) -> None:
"""Draw upcoming speed limit."""
speed_str = str(round(self.next_speed_limit))
distance_str = self._format_distance(self.next_speed_limit_distance)
ahead_width = 150
ahead_height = 100
ahead_x = sign_rect.x + (sign_rect.width - ahead_width) / 2
ahead_y = sign_rect.y + sign_rect.height + 46
ahead_rect = rl.Rectangle(ahead_x, ahead_y, ahead_width, ahead_height)
rl.draw_rectangle_rounded(ahead_rect, 0.2, 10, rl.Color(0, 0, 0, 180))
rl.draw_rectangle_rounded_lines_ex(ahead_rect, 0.2, 10, 3, rl.Color(255, 255, 255, 100))
ahead_label = tr("AHEAD")
ahead_size = measure_text_cached(self._font_semi_bold, ahead_label, 28)
ahead_pos = rl.Vector2(ahead_rect.x + (ahead_rect.width - ahead_size.x) / 2, ahead_rect.y + 4)
rl.draw_text_ex(self._font_semi_bold, ahead_label, ahead_pos, 28, 0, COLORS.light_grey)
speed_size = measure_text_cached(self._font_bold, speed_str, 48)
speed_pos = rl.Vector2(ahead_rect.x + (ahead_rect.width - speed_size.x) / 2, ahead_rect.y + 26)
rl.draw_text_ex(self._font_bold, speed_str, speed_pos, 48, 0, COLORS.white)
distance_size = measure_text_cached(self._font_medium, distance_str, 28)
distance_pos = rl.Vector2(ahead_rect.x + (ahead_rect.width - distance_size.x) / 2, ahead_rect.y + 70)
rl.draw_text_ex(self._font_medium, distance_str, distance_pos, 28, 0, COLORS.dark_grey)
def _format_distance(self, distance: float) -> str:
if ui_state.is_metric:
if distance < 50:
return tr("Near")
if distance >= 1000:
return f"{distance * METER_TO_KM:.1f}" + tr("km")
if distance < 200:
rounded = max(10, int(distance / 10) * 10)
else:
rounded = int(distance / 100) * 100
return str(rounded) + tr("m")
else:
distance_mi = distance * METER_TO_MILE
if distance_mi < 0.1:
return tr("Near")
return f"{distance_mi:.1f}" + tr("mi")