BigUI WIP: 4 Jon: His Eyes Are Saved

Pass 1 of collapsing sidebar, adjustor size increases, onroad bar size increase
This commit is contained in:
firestarsdog
2026-07-13 01:44:00 -04:00
parent b49c21e5aa
commit c306fc7cfd
4 changed files with 222 additions and 124 deletions
+170 -51
View File
@@ -15,7 +15,9 @@ from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.network import NetworkUI
# Constants
SIDEBAR_WIDTH = 500
COLLAPSED_WIDTH = 0
EXPANDED_WIDTH = 500
SWIPE_THRESHOLD = 80
CLOSE_BTN_SIZE = 200
CLOSE_ICON_SIZE = 70
NAV_BTN_HEIGHT = 110
@@ -23,11 +25,13 @@ PANEL_MARGIN = 10
# Colors
SIDEBAR_COLOR = rl.BLACK
ACCENT_LINE_COLOR = rl.Color(139, 92, 246, 55)
PANEL_COLOR = rl.BLACK
CLOSE_BTN_COLOR = rl.Color(41, 41, 41, 255)
CLOSE_BTN_PRESSED = rl.Color(59, 59, 59, 255)
TEXT_NORMAL = rl.Color(128, 128, 128, 255)
TEXT_SELECTED = rl.WHITE
DARK_CORE_COLOR = rl.Color(12, 10, 18, 190)
class PanelType(IntEnum):
@@ -55,6 +59,12 @@ class SettingsLayout(Widget):
# 0 = top level (settings main), 1+ = nested custom panels
self._panel_depth = 0
# Collapse / swipe state (always start collapsed)
self._sidebar_expanded = False
self._swipe_start = None
self._collapse_btn_rect = rl.Rectangle(0, 0, 0, 0)
self._back_btn_rect = rl.Rectangle(0, 0, 0, 0)
# Panel configuration
wifi_manager = WifiManager()
wifi_manager.set_active(False)
@@ -78,6 +88,10 @@ class SettingsLayout(Widget):
# Callbacks
self._close_callback: Callable | None = None
@property
def _sidebar_width(self) -> int:
return EXPANDED_WIDTH if self._sidebar_expanded else COLLAPSED_WIDTH
def set_callbacks(self, on_close: Callable):
self._close_callback = on_close
@@ -100,81 +114,186 @@ class SettingsLayout(Widget):
self._close_callback()
def _render(self, rect: rl.Rectangle):
# Calculate layout
sidebar_rect = rl.Rectangle(rect.x, rect.y, SIDEBAR_WIDTH, rect.height)
panel_rect = rl.Rectangle(rect.x + SIDEBAR_WIDTH, rect.y, rect.width - SIDEBAR_WIDTH, rect.height)
# Draw components
w = self._sidebar_width
sidebar_rect = rl.Rectangle(rect.x, rect.y, w, rect.height)
panel_rect = rl.Rectangle(rect.x + w, rect.y, rect.width - w, rect.height)
self._draw_sidebar(sidebar_rect)
self._draw_current_panel(panel_rect)
def _draw_chevron(self, cx: float, cy: float, right: bool, color: rl.Color, size: int = 14, bloom: bool = False):
half = size / 2
if right:
p1 = rl.Vector2(cx - half, cy - size)
p2 = rl.Vector2(cx + half, cy)
p3 = rl.Vector2(cx - half, cy + size)
else:
p1 = rl.Vector2(cx + half, cy - size)
p2 = rl.Vector2(cx - half, cy)
p3 = rl.Vector2(cx + half, cy + size)
if bloom:
bloom_col = rl.Color(color.r, color.g, color.b, 35)
rl.draw_line_ex(p1, p2, 7.0, bloom_col)
rl.draw_line_ex(p2, p3, 7.0, bloom_col)
rl.draw_line_ex(p1, p2, 2.8, color)
rl.draw_line_ex(p2, p3, 2.8, color)
def _draw_sidebar(self, rect: rl.Rectangle):
rl.draw_rectangle_rec(rect, SIDEBAR_COLOR)
# Back/Close button - hierarchical navigation
back_btn_rect = rl.Rectangle(rect.x + (rect.width - CLOSE_BTN_SIZE) / 2, rect.y + 60, CLOSE_BTN_SIZE, CLOSE_BTN_SIZE)
# 2px purple accent line — persistent in both collapsed and expanded states
line_rect = rl.Rectangle(rect.x, rect.y, 2, rect.height)
rl.draw_rectangle_rec(line_rect, ACCENT_LINE_COLOR)
pressed = gui_app.last_mouse_event.left_down and rl.check_collision_point_rec(gui_app.last_mouse_event.pos, back_btn_rect)
close_color = CLOSE_BTN_PRESSED if pressed else CLOSE_BTN_COLOR
rl.draw_rectangle_rounded(back_btn_rect, 1.0, 20, close_color)
# Orb position: fixed anchor that stays at the same screen position
# in both states — visual indicator when collapsed, collapse trigger when expanded
orb_cx = int(rect.x + 5)
orb_cy = int(rect.y + 581)
depth_boost = 0.8 if self._panel_depth > 0 else 1.0
icon_color = rl.Color(255, 255, 255, 255) if not pressed else rl.Color(220, 220, 220, 255)
icon_dest = rl.Rectangle(
back_btn_rect.x + (back_btn_rect.width - self._close_icon.width) / 2,
back_btn_rect.y + (back_btn_rect.height - self._close_icon.height) / 2,
self._close_icon.width,
self._close_icon.height,
)
rl.draw_texture_pro(
self._close_icon,
rl.Rectangle(0, 0, self._close_icon.width, self._close_icon.height),
icon_dest,
rl.Vector2(0, 0),
0,
icon_color,
)
if self._sidebar_expanded:
# ── EXPANDED ──
# Store back button rect for click detection
self._back_btn_rect = back_btn_rect
# Collapse trigger: purple glow orb at the same position as the collapsed indicator
# Now with << inside and a 60x60 tap zone centered on the orb
self._collapse_btn_rect = rl.Rectangle(rect.x, orb_cy - 30, 60, 60)
col_pressed = (
gui_app.last_mouse_event.left_down
and rl.check_collision_point_rec(gui_app.last_mouse_event.pos, self._collapse_btn_rect)
)
boost = 1.3 if col_pressed else 1.0
accent = rl.Color(139, 92, 246, 255)
rings = [
(32, 8),
(24, 14),
(17, 24),
(8, 38),
]
for radius, base_alpha in rings:
a = max(2, min(255, int(base_alpha * boost)))
rl.draw_circle(int(orb_cx), int(orb_cy), radius, rl.Color(accent.r, accent.g, accent.b, a))
# Navigation buttons
y = rect.y + 300
for panel_type, panel_info in self._panels.items():
button_rect = rl.Rectangle(rect.x + 50, y, rect.width - 150, NAV_BTN_HEIGHT)
# Dark core mask — creates dark interior matching HubTile glow-border pattern
rl.draw_circle(int(orb_cx), int(orb_cy), 11, DARK_CORE_COLOR)
# Button styling
is_selected = panel_type == self._current_panel
text_color = TEXT_SELECTED if is_selected else TEXT_NORMAL
# Draw button text (right-aligned)
panel_name = tr(panel_info.name)
text_size = measure_text_cached(self._font_medium, panel_name, 65)
text_pos = rl.Vector2(button_rect.x + button_rect.width - text_size.x, button_rect.y + (button_rect.height - text_size.y) / 2)
rl.draw_text_ex(self._font_medium, panel_name, rl.Vector2(round(text_pos.x), round(text_pos.y)), 65, 0, text_color)
self._draw_chevron(orb_cx, orb_cy, False, accent, size=16, bloom=True)
# Store button rect for click detection
panel_info.button_rect = button_rect
# Back/Close button - hierarchical navigation
back_btn_rect = rl.Rectangle(rect.x + (rect.width - CLOSE_BTN_SIZE) / 2, rect.y + 60, CLOSE_BTN_SIZE, CLOSE_BTN_SIZE)
pressed = gui_app.last_mouse_event.left_down and rl.check_collision_point_rec(gui_app.last_mouse_event.pos, back_btn_rect)
close_color = CLOSE_BTN_PRESSED if pressed else CLOSE_BTN_COLOR
rl.draw_rectangle_rounded(back_btn_rect, 1.0, 20, close_color)
y += NAV_BTN_HEIGHT
icon_color = rl.Color(255, 255, 255, 255) if not pressed else rl.Color(220, 220, 220, 255)
icon_dest = rl.Rectangle(
back_btn_rect.x + (back_btn_rect.width - self._close_icon.width) / 2,
back_btn_rect.y + (back_btn_rect.height - self._close_icon.height) / 2,
self._close_icon.width,
self._close_icon.height,
)
rl.draw_texture_pro(
self._close_icon,
rl.Rectangle(0, 0, self._close_icon.width, self._close_icon.height),
icon_dest,
rl.Vector2(0, 0),
0,
icon_color,
)
# Store back button rect for click detection
self._back_btn_rect = back_btn_rect
# Navigation buttons
y = rect.y + 300
for panel_type, panel_info in self._panels.items():
button_rect = rl.Rectangle(rect.x + 50, y, rect.width - 150, NAV_BTN_HEIGHT)
# Button styling
is_selected = panel_type == self._current_panel
text_color = TEXT_SELECTED if is_selected else TEXT_NORMAL
# Draw button text (right-aligned)
panel_name = tr(panel_info.name)
text_size = measure_text_cached(self._font_medium, panel_name, 65)
text_pos = rl.Vector2(button_rect.x + button_rect.width - text_size.x, button_rect.y + (button_rect.height - text_size.y) / 2)
rl.draw_text_ex(self._font_medium, panel_name, rl.Vector2(round(text_pos.x), round(text_pos.y)), 65, 0, text_color)
# Store button rect for click detection
panel_info.button_rect = button_rect
y += NAV_BTN_HEIGHT
else:
# ── COLLAPSED: the orb is purely visual, indicating the swipe gesture zone ──
accent = rl.Color(139, 92, 246, 255)
rings = [
(32, 8),
(24, 14),
(17, 24),
(8, 38),
]
for radius, base_alpha in rings:
a = max(2, min(255, int(base_alpha * depth_boost)))
rl.draw_circle(int(orb_cx), int(orb_cy), radius, rl.Color(accent.r, accent.g, accent.b, a))
# Dark core mask — visual continuity with expanded state
rl.draw_circle(int(orb_cx), int(orb_cy), 11, DARK_CORE_COLOR)
# Dim > chevron — shares shape language with expanded state, but muted to match passive affordance
self._draw_chevron(orb_cx, orb_cy, True, rl.Color(139, 92, 246, 120), size=10, bloom=False)
# Pull handle: brighter accent-line segment near orb as tap/swipe affordance
rl.draw_rectangle_rec(rl.Rectangle(rect.x, orb_cy - 20, 2, 40), rl.Color(139, 92, 246, 100))
def _draw_current_panel(self, rect: rl.Rectangle):
rl.draw_rectangle_rounded(rl.Rectangle(rect.x + 10, rect.y + 10, rect.width - 20, rect.height - 20), 0.04, 30, PANEL_COLOR)
content_rect = rl.Rectangle(rect.x + PANEL_MARGIN, rect.y + 10, rect.width - (PANEL_MARGIN * 2), rect.height - 20)
# rl.draw_rectangle_rounded(content_rect, 0.03, 30, PANEL_COLOR)
panel = self._panels[self._current_panel]
if panel.instance:
panel.instance.render(content_rect)
def _handle_mouse_press(self, mouse_pos: MousePos) -> None:
if not self._sidebar_expanded:
# Only record swipe start when touch is within the 10px left margin
# (gesture zone — no overlap with panel content which starts at x=10)
gesture_zone = rl.Rectangle(self._rect.x, self._rect.y, 10, self._rect.height)
if rl.check_collision_point_rec(mouse_pos, gesture_zone):
self._swipe_start = mouse_pos
else:
self._swipe_start = None
def _handle_mouse_release(self, mouse_pos: MousePos) -> None:
# Check back/close button - hierarchical navigation
if rl.check_collision_point_rec(mouse_pos, self._back_btn_rect):
start = self._swipe_start
self._swipe_start = None
# ── Collapsed: tap or swipe from left edge ──
if not self._sidebar_expanded and start is not None:
dx = mouse_pos.x - start.x
dy = abs(mouse_pos.y - start.y)
if dx > SWIPE_THRESHOLD:
if self._panel_depth > 0:
self._handle_back_navigation()
else:
self._sidebar_expanded = True
return
elif dy < 20:
self._sidebar_expanded = True
return
# ── Expanded: collapse button ──
if self._sidebar_expanded and rl.check_collision_point_rec(mouse_pos, self._collapse_btn_rect):
self._sidebar_expanded = False
return
# ── Expanded: back button ──
if self._sidebar_expanded and rl.check_collision_point_rec(mouse_pos, self._back_btn_rect):
self._handle_back_navigation()
return
# Check navigation buttons
for panel_type, panel_info in self._panels.items():
if rl.check_collision_point_rec(mouse_pos, panel_info.button_rect):
self.set_current_panel(panel_type)
return
# ── Expanded: nav items ──
if self._sidebar_expanded:
for panel_type, panel_info in self._panels.items():
if rl.check_collision_point_rec(mouse_pos, panel_info.button_rect):
self.set_current_panel(panel_type)
return
def set_current_panel(self, panel_type: PanelType):
if panel_type != self._current_panel:
@@ -1923,7 +1923,7 @@ def draw_metric_strip(
)
GROUP_HEADER_HEIGHT = 26.0
GROUP_HEADER_HEIGHT = 30.0
GROUP_HEADER_GAP = 1.0
GROUP_HEADER_LINE_GAP = 1.0
GROUP_OVERHEAD = 8.0 + GROUP_HEADER_HEIGHT + GROUP_HEADER_LINE_GAP + GROUP_HEADER_GAP
@@ -1932,7 +1932,7 @@ GROUP_HEADER_COLOR = AetherListColors.HEADER
def draw_group_header(x: float, y: float, width: float, label: str) -> float:
gui_label(rl.Rectangle(x, y, max(1.0, width), GROUP_HEADER_HEIGHT), label, 20, GROUP_HEADER_COLOR, FontWeight.MEDIUM)
gui_label(rl.Rectangle(x, y, max(1.0, width), GROUP_HEADER_HEIGHT), label, 26, GROUP_HEADER_COLOR, FontWeight.MEDIUM)
y += GROUP_HEADER_HEIGHT + GROUP_HEADER_LINE_GAP
rl.draw_line(int(x), int(y), int(x + width), int(y), GROUP_HAIRLINE_COLOR)
return y + GROUP_HEADER_GAP
@@ -2620,27 +2620,16 @@ class AetherAdjustorRow(Widget):
current_border=current_border,
)
title_fs = 28
sub_fs = 24
bar_val_fs = 20
bar_h = max(74, min(94, int(rect.height * 0.87)))
title_fs = max(38, int(bar_h * 0.53))
value_fs = max(24, int(bar_h * 0.34))
content_left = rect.x + 24
content_width = max(120.0, rect.width - 48)
title_y = rect.y + 10
title_h = 30
gui_label(rl.Rectangle(content_left, title_y, content_width, title_h), self._title, title_fs, self._style.title_color, FontWeight.MEDIUM)
sub_y = rect.y + 44
sub_h = 28
if self._subtitle:
gui_label(rl.Rectangle(content_left, sub_y, content_width, sub_h), self._subtitle, sub_fs, self._style.subtitle_color, FontWeight.NORMAL)
bar_h = 30
bar_y = (sub_y + sub_h + 6) if self._subtitle else (title_y + title_h + 8)
bar_rect = snap_rect(rl.Rectangle(content_left, bar_y, rect.x + rect.width - 24 - content_left, bar_h))
bar_width = max(120.0, rect.width - 48)
bar_y = rect.y + (rect.height - bar_h) / 2
bar_rect = snap_rect(rl.Rectangle(content_left, bar_y, bar_width, bar_h))
self._progress_bar_rect = bar_rect
self._header_rect = rl.Rectangle(rect.x, rect.y, rect.width, min(rect.height, bar_y + bar_h - rect.y))
self._header_rect = bar_rect
draw_rounded_fill(bar_rect, rl.Color(255, 255, 255, 8), radius_px=bar_h // 2)
draw_rounded_stroke(bar_rect, rl.Color(255, 255, 255, 14), radius_px=bar_h // 2)
@@ -2648,17 +2637,27 @@ class AetherAdjustorRow(Widget):
fill_frac = self._scrubber._value_fraction(self._current_value())
if fill_frac > 0:
fill_rect = snap_rect(rl.Rectangle(bar_rect.x, bar_rect.y, max(1.0, bar_rect.width * fill_frac), bar_h))
draw_rounded_fill(fill_rect, with_alpha(self._color, 180 if active else 120), radius_px=bar_h // 2)
fill_alpha = 200 if self._pressed_zone == "header" else (180 if active else 140)
draw_rounded_fill(fill_rect, with_alpha(self._color, fill_alpha), radius_px=bar_h // 2)
draw_text_fit_common(
self._font_value,
self.formatted_value(),
rl.Vector2(bar_rect.x + 12, bar_rect.y + (bar_h - bar_val_fs) / 2),
max(1.0, bar_rect.width - 24),
bar_val_fs,
align_center=True,
color=self._style.title_color,
)
inset = 18
title_y = bar_rect.y + (bar_h - title_fs) / 2
rl.draw_text_ex(self._font_title, self._title,
rl.Vector2(bar_rect.x + inset, title_y),
title_fs, 0, self._style.title_color)
value_str = self.formatted_value()
value_w = measure_text_cached(self._font_value, value_str, value_fs).x
rl.draw_text_ex(self._font_value, value_str,
rl.Vector2(bar_rect.x + bar_rect.width - inset - value_w,
bar_rect.y + (bar_h - value_fs) / 2),
value_fs, 0, self._style.title_color)
if self._subtitle:
sub_fs = 20
sub_y = bar_rect.y - sub_fs - 4
gui_label(rl.Rectangle(content_left, sub_y, bar_width, sub_fs),
self._subtitle, sub_fs, self._style.subtitle_color, FontWeight.NORMAL)
if not active:
return
@@ -5550,7 +5549,7 @@ class AetherSegmentedControl(Widget):
label = str(_resolve_value(option, ""))
status = str(_resolve_value(self._statuses[i], ""))
title_size = max(26, min(35, int(face_rect.height * (0.28 if has_status else 0.36))))
title_size = max(26, min(50, int(face_rect.height * (0.28 if has_status else 0.36))))
status_size = max(20, min(25, int(face_rect.height * 0.22)))
if has_status:
@@ -239,19 +239,16 @@ class SoundsManagerView(PanelManagerView):
self._adjustor_rows[self._controller.COOLDOWN_KEY].custom_row_height = None
hdr_h = GROUP_HEADER_HEIGHT + GROUP_HEADER_GAP + GROUP_HEADER_LINE_GAP
vol_overhead = 4 + 24 + 4 # top pad + "Reset All" label + gap
if self._scroll_rect:
available_container_h = self._scroll_rect.height - 6.0
else:
available_container_h = 0.0
left_available_for_rows = available_container_h - 12.0 - 4.0 - hdr_h
left_row_h = max(80.0, min(107.0, left_available_for_rows / (len(self._controller.VOLUME_KEYS) + 1)))
available_h = max(72.0, (self._scroll_rect.height if self._scroll_rect else 0.0) - 6.0)
rows_available = max(72.0 * (len(self._controller.VOLUME_KEYS) + 1), available_h - vol_overhead)
ROW_HEIGHT = rows_available / (len(self._controller.VOLUME_KEYS) + 1)
for key in self._controller.VOLUME_KEYS:
self._adjustor_rows[key].custom_row_height = left_row_h
self._adjustor_rows[self._controller.COOLDOWN_KEY].custom_row_height = left_row_h
self._adjustor_rows[key].custom_row_height = ROW_HEIGHT
self._adjustor_rows[self._controller.COOLDOWN_KEY].custom_row_height = ROW_HEIGHT
left_content_h = (len(self._controller.VOLUME_KEYS) + 1) * left_row_h + 12 + 4 + hdr_h
left_content_h = (len(self._controller.VOLUME_KEYS) + 1) * ROW_HEIGHT + vol_overhead
tiles_needed_h = self.measure_page_grid_height(self._toggle_grid, col_width - 24) + 24 + 4 + hdr_h
max_content_h = max(left_content_h, tiles_needed_h)
@@ -271,15 +268,7 @@ class SoundsManagerView(PanelManagerView):
self._draw_utility_column(y, rect.x + col_width + SECTION_GAP, col_width)
def _draw_volume_column(self, y: float, x: float, width: float):
safety_keys = ["WarningImmediateVolume", "WarningSoftVolume", "RefuseVolume", "PromptDistractedVolume"]
system_keys = ["EngageVolume", "DisengageVolume"]
info_keys = ["PromptVolume", "BelowSteerSpeedVolume"]
groups = [
(None, safety_keys),
(tr("SYSTEM STATE"), system_keys),
(tr("INFORMATIONAL"), info_keys + [self._controller.COOLDOWN_KEY]),
]
all_keys = self._controller.VOLUME_KEYS + [self._controller.COOLDOWN_KEY]
draw_list_group_shell(
rl.Rectangle(x, y, width, self._left_container_h),
@@ -287,30 +276,21 @@ class SoundsManagerView(PanelManagerView):
)
current_y = y + 4
current_y = draw_group_header(x + 24, current_y, width - 48, tr("VOLUME"))
label_rect = rl.Rectangle(x + 24, current_y - GROUP_HEADER_HEIGHT - GROUP_HEADER_LINE_GAP - GROUP_HEADER_GAP, width - 48, GROUP_HEADER_HEIGHT)
gui_label(label_rect, tr("Reset All"), 20, AetherListColors.MUTED, FontWeight.NORMAL,
label_rect = rl.Rectangle(x + 24, current_y, width - 48, 24)
gui_label(label_rect, tr("Reset All"), 24, AetherListColors.MUTED, FontWeight.NORMAL,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
self._reset_rect = rl.Rectangle(label_rect.x + label_rect.width - 80, label_rect.y, 80, 45)
self._reset_rect = rl.Rectangle(label_rect.x + label_rect.width - 140, label_rect.y, 140, 24)
self._interactive_rects["action:restore_defaults"] = self._reset_rect
for i, (label, keys) in enumerate(groups):
if label is not None:
divider_h = 3
divider_color = rl.Color(173, 78, 90, 30) if i == 1 else rl.Color(139, 92, 246, 25)
rl.draw_rectangle_rec(
rl.Rectangle(x + 24, current_y, width - 48, divider_h),
divider_color
)
current_y += divider_h + 3
for index, key in enumerate(keys):
adjustor = self._adjustor_rows[key]
row_h = adjustor.measure_height(width)
row_rect = rl.Rectangle(x, current_y, width, row_h)
adjustor.set_is_last(index == len(keys) - 1)
adjustor.set_parent_rect(self._scroll_rect)
adjustor.render(row_rect)
current_y += row_h
current_y += 28
for index, key in enumerate(all_keys):
adjustor = self._adjustor_rows[key]
row_h = adjustor.measure_height(width)
row_rect = rl.Rectangle(x, current_y, width, row_h)
adjustor.set_is_last(index == len(all_keys) - 1)
adjustor.set_parent_rect(self._scroll_rect)
adjustor.render(row_rect)
current_y += row_h
def _draw_utility_column(self, y: float, x: float, width: float):
draw_list_group_shell(rl.Rectangle(x, y, width, self._tiles_container_h), style=PANEL_STYLE)
@@ -107,8 +107,8 @@ class SystemSettingsManagerView(PanelManagerView):
TAB_BOTTOM_GAP = 26
ACTION_PILL_WIDTH = 132
DANGER_PILL_WIDTH = 112
_TOPBAR_HEIGHT = 76.0
_TOPBAR_GAP = 16.0
_TOPBAR_HEIGHT = 120.0
_TOPBAR_GAP = 0.0
METRICS = SYSTEM_PANEL_METRICS
@property