diff --git a/selfdrive/ui/onroad/starpilot/slc_speed_limit.py b/selfdrive/ui/onroad/starpilot/slc_speed_limit.py index c138cbe64..421ee6867 100644 --- a/selfdrive/ui/onroad/starpilot/slc_speed_limit.py +++ b/selfdrive/ui/onroad/starpilot/slc_speed_limit.py @@ -30,11 +30,11 @@ PENDING_BLINK_MS = 500 # Source display metadata: title, abbreviation, raw-value key (display order). SOURCE_DEFS = [ - ("Dashboard", "Dash", "dashboard_sl"), - ("Map Data", "MapD", "map_sl"), - ("Vision", "Vis", "vision_sl"), - ("Mapbox", "MapB", "mapbox_sl"), - ("Upcoming", "Next", "next_sl"), + ("Dashboard", "Dash", "dashboard_sl"), + ("Map Data", "Maps", "map_sl"), + ("Vision", "Vision", "vision_sl"), + ("Mapbox", "Mapbox", "mapbox_sl"), + ("Upcoming", "Next", "next_sl"), ] # Fonts @@ -353,23 +353,29 @@ def _draw_sign(state: dict, rect: rl.Rectangle, *, pending: bool = False): # ── Active Source Label (single-line mode when sources panel is off) ── _SOURCE_ABBREV = {"Dashboard": "DASH", "Map Data": "MAPS", "Vision": "VISION", - "Mapbox": "MAPB", "Upcoming": "NAV"} + "Mapbox": "MAPBOX", "Upcoming": "NEXT"} -def _draw_active_source_label(state: dict, cx: float, bottom_y: float, expanded: bool = False) -> Optional[rl.Rectangle]: +def _draw_active_source_label(state: dict, cx: float, bottom_y: float, sign_width: float, expanded: bool = False) -> Optional[rl.Rectangle]: """Draw the single active-source pill below the sign. Returns pill rect for hit-testing.""" source = state.get('speed_limit_source') if not source or source == "None" or source == "": return None label = _SOURCE_ABBREV.get(source, source.upper()) - indicator = " \u25bc" if expanded else " \u25b6" + indicator = " \u25c0" if expanded else " \u25b6" full_label = label + indicator font = _get_semi_bold() - font_size = 20 + font_size = 24 sz = measure_text_cached(font, full_label, font_size) - rect = rl.Rectangle(cx - sz.x / 2 - 8, bottom_y + 8, sz.x + 16, font_size + 8) + + pill_w = sign_width + pill_h = 32 + rect = rl.Rectangle(cx - pill_w / 2, bottom_y + 8, pill_w, pill_h) + rl.draw_rectangle_rounded(rect, 0.4, 8, rl.Color(0, 0, 0, 180)) rl.draw_rectangle_rounded_lines_ex(rect, 0.4, 8, 1, rl.Color(255, 255, 255, 100)) - rl.draw_text_ex(font, full_label, rl.Vector2(cx - sz.x / 2, bottom_y + 12), font_size, 0, rl.WHITE) + + text_y = bottom_y + 8 + (pill_h - font_size) / 2 + rl.draw_text_ex(font, full_label, rl.Vector2(cx - sz.x / 2, text_y), font_size, 0, rl.WHITE) return rect @@ -385,13 +391,13 @@ def _draw_text_outlined(font, text: str, pos: rl.Vector2, font_size: int, fill: _BUBBLE_PAD_X = 24 _BUBBLE_ROW_H = 48 -_BUBBLE_GAP = 8 -_BUBBLE_FONT = 28 +_BUBBLE_GAP = 2 +_BUBBLE_FONT = 30 _BUBBLE_BG = rl.Color(0, 0, 0, 200) _BUBBLE_BORDER = rl.Color(255, 255, 255, 80) _BUBBLE_ACCENT = rl.Color(201, 34, 49, 255) -def _draw_sources_bubble(state: dict, anchor_rect: rl.Rectangle, sign_right: float): +def _draw_sources_bubble(state: dict, anchor_rect: rl.Rectangle, sign_rect: rl.Rectangle): """Draw the expanded sources bubble to the right of the anchor pill.""" font_bold = _get_bold() font_semi = _get_semi_bold() @@ -407,18 +413,11 @@ def _draw_sources_bubble(state: dict, anchor_rect: rl.Rectangle, sign_right: flo if not rows: return - max_w = 0 - for _, abbrev, value, _ in rows: - label = f"{abbrev} {int(round(value))}" - needed = measure_text_cached(font_bold, label, _BUBBLE_FONT).x + _BUBBLE_PAD_X - if needed > max_w: - max_w = needed + bubble_w = sign_rect.width + bubble_h = sign_rect.height - bubble_w = max_w - bubble_h = len(rows) * _BUBBLE_ROW_H + (len(rows) - 1) * _BUBBLE_GAP + 16 - - bubble_x = max(anchor_rect.x + anchor_rect.width + 12, sign_right + 6) - bubble_y = anchor_rect.y + anchor_rect.height / 2 - bubble_h / 2 + bubble_x = sign_rect.x + sign_rect.width + 12 + bubble_y = sign_rect.y bg_rect = rl.Rectangle(bubble_x, bubble_y, bubble_w, bubble_h) rl.draw_rectangle_rounded(bg_rect, 0.25, 8, _BUBBLE_BG) @@ -432,10 +431,13 @@ def _draw_sources_bubble(state: dict, anchor_rect: rl.Rectangle, sign_right: flo _BUBBLE_BG, ) - y = bubble_y + 8 + row_h = min(40.0, (bubble_h - 8 - (len(rows) - 1) * _BUBBLE_GAP) / len(rows)) + total_content_h = len(rows) * row_h + (len(rows) - 1) * _BUBBLE_GAP + y = bubble_y + (bubble_h - total_content_h) / 2 + for title, abbrev, value, is_active in rows: if is_active: - rl.draw_rectangle(int(bubble_x), int(y), 4, _BUBBLE_ROW_H, _BUBBLE_ACCENT) + rl.draw_rectangle(int(bubble_x), int(y), 4, int(row_h), _BUBBLE_ACCENT) text_font = font_bold if is_active else font_semi abbrev_text = abbrev @@ -444,19 +446,27 @@ def _draw_sources_bubble(state: dict, anchor_rect: rl.Rectangle, sign_right: flo abbrev_size = measure_text_cached(text_font, abbrev_text, _BUBBLE_FONT) value_size = measure_text_cached(text_font, value_text, _BUBBLE_FONT) - abbrev_pos = rl.Vector2(bubble_x + 16, y + (_BUBBLE_ROW_H - _BUBBLE_FONT) / 2) - value_pos = rl.Vector2(bubble_x + bubble_w - 12 - value_size.x, y + (_BUBBLE_ROW_H - _BUBBLE_FONT) / 2) + font_size = _BUBBLE_FONT + available_w = bubble_w - 16 + needed_w = abbrev_size.x + value_size.x + 8 + if needed_w > available_w: + font_size = int(_BUBBLE_FONT * (available_w / needed_w)) + abbrev_size = measure_text_cached(text_font, abbrev_text, font_size) + value_size = measure_text_cached(text_font, value_text, font_size) + + abbrev_pos = rl.Vector2(bubble_x + 8, y + (row_h - font_size) / 2) + value_pos = rl.Vector2(bubble_x + bubble_w - 8 - value_size.x, y + (row_h - font_size) / 2) text_color = rl.WHITE if is_active else rl.Color(255, 255, 255, 180) if is_active: - _draw_text_outlined(text_font, abbrev_text, abbrev_pos, _BUBBLE_FONT, text_color, rl.Color(0, 0, 0, 255)) - _draw_text_outlined(text_font, value_text, value_pos, _BUBBLE_FONT, text_color, rl.Color(0, 0, 0, 255)) + _draw_text_outlined(text_font, abbrev_text, abbrev_pos, font_size, text_color, rl.Color(0, 0, 0, 255)) + _draw_text_outlined(text_font, value_text, value_pos, font_size, text_color, rl.Color(0, 0, 0, 255)) else: - rl.draw_text_ex(text_font, abbrev_text, abbrev_pos, _BUBBLE_FONT, 0, text_color) - rl.draw_text_ex(text_font, value_text, value_pos, _BUBBLE_FONT, 0, text_color) + rl.draw_text_ex(text_font, abbrev_text, abbrev_pos, font_size, 0, text_color) + rl.draw_text_ex(text_font, value_text, value_pos, font_size, 0, text_color) - y += _BUBBLE_ROW_H + _BUBBLE_GAP + y += row_h + _BUBBLE_GAP # ── Public API ──────────────────────────────────────────────────────── @@ -479,10 +489,15 @@ def render_speed_limit_at(state: dict, rect: rl.Rectangle, expanded: bool = Fals cx_center = rect.x + rect.width / 2 sign_h = EU_SIGN_SIZE if use_vienna else US_SIGN_HEIGHT bottom_y = rect.y + sign_h - pill_rect = _draw_active_source_label(state, cx_center, bottom_y, expanded) + + visual_w = EU_SIGN_SIZE if use_vienna else rect.width - 2 * US_INSET + visual_x = rect.x if use_vienna else rect.x + US_INSET + visual_rect = rl.Rectangle(visual_x, rect.y, visual_w, sign_h) + + pill_rect = _draw_active_source_label(state, cx_center, bottom_y, visual_w, expanded) if expanded and pill_rect: - _draw_sources_bubble(state, pill_rect, rect.x + rect.width) + _draw_sources_bubble(state, pill_rect, visual_rect) return pill_rect diff --git a/selfdrive/ui/onroad/starpilot/widgets/speed_limit.py b/selfdrive/ui/onroad/starpilot/widgets/speed_limit.py index 7fb1287a9..da3909a9c 100644 --- a/selfdrive/ui/onroad/starpilot/widgets/speed_limit.py +++ b/selfdrive/ui/onroad/starpilot/widgets/speed_limit.py @@ -15,7 +15,6 @@ class SpeedLimitWidget(LayoutWidget): super().__init__("speed_limit", priority=2) self._slc_state: dict | None = None self._pill_rect: Optional[rl.Rectangle] = None - self._expanded = False @property def is_visible(self) -> bool: @@ -48,12 +47,21 @@ class SpeedLimitWidget(LayoutWidget): def _render(self, rect: rl.Rectangle) -> None: if self._slc_state is None: return - self._pill_rect = render_speed_limit_at(self._slc_state, rect, self._expanded) + expanded = ui_state.params.get_bool("SpeedLimitSources") + self._pill_rect = render_speed_limit_at(self._slc_state, rect, expanded) def _handle_mouse_press(self, mouse_pos) -> None: - if self._pill_rect and rl.check_collision_point_rec(mouse_pos, self._pill_rect): - self._expanded = not self._expanded - return + if self._pill_rect: + hit_rect = rl.Rectangle( + self._pill_rect.x - 20, + self._pill_rect.y - 20, + self._pill_rect.width + 40, + self._pill_rect.height + 40 + ) + if rl.check_collision_point_rec(mouse_pos, hit_rect): + current = ui_state.params.get_bool("SpeedLimitSources") + Params().put_bool("SpeedLimitSources", not current) + return state = _get_slc_state() if state is None or not (state['speed_limit_changed'] and state['unconfirmed_valid']):