Big UI: Fix Source Bubble

This commit is contained in:
firestarsdog
2026-07-20 10:25:15 -04:00
parent 2f0864e215
commit 3e5bb853cb
2 changed files with 64 additions and 41 deletions
@@ -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
@@ -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']):