mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-08-23 17:23:44 +08:00
Polish favorite picker
This commit is contained in:
@@ -43,12 +43,28 @@ class FavoriteRadialMenu:
|
||||
PICKER_COLUMNS = 4
|
||||
PICKER_ROWS = 2
|
||||
PICKER_PAGE_SIZE = PICKER_COLUMNS * PICKER_ROWS
|
||||
PICKER_HEADER_HEIGHT = 152.0
|
||||
PICKER_FOOTER_HEIGHT = 168.0
|
||||
PICKER_GRID_GAP = 24.0
|
||||
PICKER_CONTROL_HEIGHT = 96.0
|
||||
PICKER_CONTROL_HIT_PAD_Y = 28.0
|
||||
PICKER_CARD_PADDING = 26.0
|
||||
PICKER_CLOSE_SIZE = 72.0
|
||||
PICKER_CLOSE_HIT_SIZE = 150.0
|
||||
CORNER_HINT_OUTER_RADIUS = 62.0
|
||||
CORNER_HINT_RING_RADIUS = 42.0
|
||||
CORNER_HINT_EDGE_MARGIN = 6.0
|
||||
|
||||
_PURPLE = (161, 112, 255)
|
||||
_PANEL = rl.Color(13, 11, 23, 236)
|
||||
_PANEL_BORDER = rl.Color(214, 192, 255, 166)
|
||||
_TEXT = rl.Color(255, 255, 255, 245)
|
||||
_MUTED_TEXT = rl.Color(213, 202, 232, 192)
|
||||
_MUTED_TEXT = rl.Color(213, 202, 232, 216)
|
||||
_PICKER_SECTION_LABELS = {
|
||||
"Visual (Display & UI)": "Display & UI",
|
||||
"Longitudinal (Speed & Following)": "Speed & Following",
|
||||
"Lateral (Steering)": "Steering",
|
||||
}
|
||||
|
||||
def __init__(self, params: Any, params_memory: Any,
|
||||
option_provider: Callable[[], Iterable[dict[str, Any]]], *,
|
||||
@@ -130,8 +146,9 @@ class FavoriteRadialMenu:
|
||||
self._reset_press_tracking()
|
||||
|
||||
def corner_center(self, rect: rl.Rectangle) -> rl.Vector2:
|
||||
zone = self._corner_zone_for(rect)
|
||||
return rl.Vector2(zone.x + zone.width * 0.40, zone.y + zone.height * 0.60)
|
||||
scale = self._scale_for(rect)
|
||||
inset = (self.CORNER_HINT_RING_RADIUS + self.CORNER_HINT_EDGE_MARGIN) * scale
|
||||
return rl.Vector2(rect.x + inset, rect.y + rect.height - inset)
|
||||
|
||||
def slot_centers(self, rect: rl.Rectangle) -> list[rl.Vector2]:
|
||||
self._layout(rect)
|
||||
@@ -203,6 +220,17 @@ class FavoriteRadialMenu:
|
||||
def _roundness(rect: rl.Rectangle, radius: float) -> float:
|
||||
return min(1.0, radius / max(1.0, min(rect.width, rect.height) / 2.0))
|
||||
|
||||
@staticmethod
|
||||
def _snap_render_rect(rect: rl.Rectangle) -> rl.Rectangle:
|
||||
left = round(rect.x)
|
||||
top = round(rect.y)
|
||||
right = round(rect.x + rect.width)
|
||||
bottom = round(rect.y + rect.height)
|
||||
return rl.Rectangle(
|
||||
float(left), float(top),
|
||||
float(right - left), float(bottom - top),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _distance(a: Any, b: Any) -> float:
|
||||
return math.hypot(float(a.x - b.x), float(a.y - b.y))
|
||||
@@ -299,14 +327,14 @@ class FavoriteRadialMenu:
|
||||
)
|
||||
|
||||
padding_x = 36.0 * scale
|
||||
header_height = 126.0 * scale
|
||||
footer_height = 106.0 * scale
|
||||
gap_x = 20.0 * scale
|
||||
gap_y = 20.0 * scale
|
||||
header_height = self.PICKER_HEADER_HEIGHT * scale
|
||||
footer_height = self.PICKER_FOOTER_HEIGHT * scale
|
||||
gap_x = self.PICKER_GRID_GAP * scale
|
||||
gap_y = self.PICKER_GRID_GAP * scale
|
||||
gap_header_footer = 18.0 * scale
|
||||
|
||||
# Close button visual & hit bounds
|
||||
close_size = 70.0 * scale
|
||||
close_size = self.PICKER_CLOSE_SIZE * scale
|
||||
self._drawer_close_rect = rl.Rectangle(
|
||||
self._drawer_rect.x + self._drawer_rect.width - padding_x - close_size,
|
||||
self._drawer_rect.y + (header_height - close_size) / 2,
|
||||
@@ -314,9 +342,9 @@ class FavoriteRadialMenu:
|
||||
close_size,
|
||||
)
|
||||
self._drawer_close_hit_rect = rl.Rectangle(
|
||||
self._drawer_rect.x + self._drawer_rect.width - padding_x - close_size - 18.0 * scale,
|
||||
self._drawer_rect.x + self._drawer_rect.width - padding_x - self.PICKER_CLOSE_HIT_SIZE * scale,
|
||||
self._drawer_rect.y,
|
||||
close_size + padding_x + 18.0 * scale,
|
||||
self.PICKER_CLOSE_HIT_SIZE * scale,
|
||||
header_height,
|
||||
)
|
||||
|
||||
@@ -340,7 +368,7 @@ class FavoriteRadialMenu:
|
||||
|
||||
# Pager buttons in footer
|
||||
control_width = 200.0 * scale
|
||||
control_height = 74.0 * scale
|
||||
control_height = self.PICKER_CONTROL_HEIGHT * scale
|
||||
controls_y = self._drawer_rect.y + self._drawer_rect.height - footer_height + (footer_height - control_height) / 2
|
||||
self._drawer_next_rect = rl.Rectangle(
|
||||
self._drawer_rect.x + self._drawer_rect.width - padding_x - control_width,
|
||||
@@ -350,9 +378,9 @@ class FavoriteRadialMenu:
|
||||
)
|
||||
self._drawer_next_hit_rect = rl.Rectangle(
|
||||
self._drawer_next_rect.x - 10.0 * scale,
|
||||
self._drawer_next_rect.y - 12.0 * scale,
|
||||
self._drawer_next_rect.y - self.PICKER_CONTROL_HIT_PAD_Y * scale,
|
||||
self._drawer_next_rect.width + 20.0 * scale,
|
||||
self._drawer_next_rect.height + 24.0 * scale,
|
||||
self._drawer_next_rect.height + self.PICKER_CONTROL_HIT_PAD_Y * scale * 2.0,
|
||||
)
|
||||
self._drawer_prev_rect = rl.Rectangle(
|
||||
self._drawer_next_rect.x - gap_x - control_width,
|
||||
@@ -362,9 +390,9 @@ class FavoriteRadialMenu:
|
||||
)
|
||||
self._drawer_prev_hit_rect = rl.Rectangle(
|
||||
self._drawer_prev_rect.x - 10.0 * scale,
|
||||
self._drawer_prev_rect.y - 12.0 * scale,
|
||||
self._drawer_prev_rect.y - self.PICKER_CONTROL_HIT_PAD_Y * scale,
|
||||
self._drawer_prev_rect.width + 20.0 * scale,
|
||||
self._drawer_prev_rect.height + 24.0 * scale,
|
||||
self._drawer_prev_rect.height + self.PICKER_CONTROL_HIT_PAD_Y * scale * 2.0,
|
||||
)
|
||||
|
||||
def _handle_press(self, mouse_pos: Any) -> bool:
|
||||
@@ -428,6 +456,8 @@ class FavoriteRadialMenu:
|
||||
self._reset_press_tracking()
|
||||
return True
|
||||
|
||||
release_target = self._target_at(mouse_pos)
|
||||
|
||||
is_long_press = self._long_press_fired or (
|
||||
self._press_start_time is not None and self._clock() - self._press_start_time >= self.LONG_PRESS_SECONDS
|
||||
)
|
||||
@@ -442,7 +472,7 @@ class FavoriteRadialMenu:
|
||||
self._reset_press_tracking()
|
||||
return True
|
||||
|
||||
if self._pressed_target is not None:
|
||||
if self._pressed_target is not None and release_target == self._pressed_target:
|
||||
self._activate_target(self._pressed_target)
|
||||
elif self._state == self.STATE_PICKER and not self._contains(self._drawer_rect, mouse_pos):
|
||||
self.collapse()
|
||||
@@ -649,13 +679,30 @@ class FavoriteRadialMenu:
|
||||
return label, None
|
||||
return " ".join(line1_words), " ".join(line2_words)
|
||||
|
||||
@classmethod
|
||||
def _picker_section_label(cls, option: dict[str, Any]) -> str:
|
||||
section = str(option.get("section") or "Favorite").strip()
|
||||
return cls._PICKER_SECTION_LABELS.get(section, section)
|
||||
|
||||
@staticmethod
|
||||
def _wrap_picker_text(font: Any, text: str, font_size: int, max_width: float,
|
||||
max_lines: int = 2) -> list[str]:
|
||||
from openpilot.system.ui.lib.wrap_text import wrap_text
|
||||
|
||||
text = " ".join(text.split())
|
||||
lines = list(wrap_text(font, text, font_size, int(max_width)))
|
||||
if len(lines) > max_lines:
|
||||
lines = lines[:max_lines]
|
||||
lines[-1] = FavoriteRadialMenu._append_ellipsis(font, lines[-1], font_size, max_width)
|
||||
return lines
|
||||
|
||||
def _draw_corner_hint(self) -> None:
|
||||
center = self.corner_center(self._rect)
|
||||
scale = self._scale_for(self._rect)
|
||||
purple = self._PURPLE
|
||||
for radius, alpha in ((62, 16), (48, 28), (36, 48)):
|
||||
for radius, alpha in ((self.CORNER_HINT_OUTER_RADIUS, 16), (48, 28), (36, 48)):
|
||||
rl.draw_circle_v(center, radius * scale, rl.Color(*purple, alpha))
|
||||
rl.draw_ring(center, 38 * scale, 42 * scale, 0, 360, 32, rl.Color(*purple, 150))
|
||||
rl.draw_ring(center, 38 * scale, self.CORNER_HINT_RING_RADIUS * scale, 0, 360, 32, rl.Color(*purple, 150))
|
||||
|
||||
tail = rl.Vector2(center.x - 18 * scale, center.y + 18 * scale)
|
||||
tip = rl.Vector2(center.x + 26 * scale, center.y - 26 * scale)
|
||||
@@ -698,13 +745,18 @@ class FavoriteRadialMenu:
|
||||
pad = 2.0 * scale
|
||||
chassis_rect = rl.Rectangle(blade_rect.x + pad, blade_rect.y + pad, blade_rect.width - pad * 2.0, blade_rect.height - pad * 2.0)
|
||||
|
||||
draw_rect = self._snap_render_rect(chassis_rect)
|
||||
# Blades float over the animated road/model layers. Keep their chassis
|
||||
# opaque so bright path pixels cannot read as gaps at curved ends.
|
||||
if configured:
|
||||
rl.draw_rectangle_rounded(chassis_rect, 0.45, 14, rl.Color(14, 10, 26, 248))
|
||||
rl.draw_rectangle_rounded(draw_rect, 0.45, 14, rl.Color(14, 10, 26, 255))
|
||||
border_col = rl.Color(214, 192, 255, 230) if is_pressed else rl.Color(161, 112, 255, 140)
|
||||
rl.draw_rectangle_rounded_lines_ex(chassis_rect, 0.45, 14, 1.8 * scale, border_col)
|
||||
border_width = max(1, int(round(1.8 * scale)))
|
||||
rl.draw_rectangle_rounded_lines_ex(draw_rect, 0.45, 14, border_width, border_col)
|
||||
else:
|
||||
rl.draw_rectangle_rounded(chassis_rect, 0.45, 14, rl.Color(12, 10, 22, 195))
|
||||
rl.draw_rectangle_rounded_lines_ex(chassis_rect, 0.45, 14, 1.4 * scale, rl.Color(161, 112, 255, 80))
|
||||
rl.draw_rectangle_rounded(draw_rect, 0.45, 14, rl.Color(12, 10, 22, 255))
|
||||
border_width = max(1, int(round(1.4 * scale)))
|
||||
rl.draw_rectangle_rounded_lines_ex(draw_rect, 0.45, 14, border_width, rl.Color(161, 112, 255, 80))
|
||||
|
||||
# 2. Left Complication Hub Disc
|
||||
rl.draw_circle_v(center, node_r + 12.0 * scale, rl.Color(*purple, 22 if configured else 12))
|
||||
@@ -872,17 +924,16 @@ class FavoriteRadialMenu:
|
||||
rl.draw_rectangle_rounded_lines_ex(self._drawer_rect, self._roundness(self._drawer_rect, 34 * scale), 16,
|
||||
2 * scale, self._PANEL_BORDER)
|
||||
|
||||
title = f"Choose Favorite {self._selected_slot + 1}" if self._selected_slot is not None else "Choose Favorite"
|
||||
title = f"Assign Favorite {self._selected_slot + 1}" if self._selected_slot is not None else "Assign Favorite"
|
||||
title_pos = rl.Vector2(self._drawer_rect.x + 36 * scale, self._drawer_rect.y + 26 * scale)
|
||||
self._draw_text(self._font(bold=True), title, title_pos, int(50 * scale), self._TEXT)
|
||||
page_count = max(1, math.ceil(len(self._picker_options) / self.PICKER_PAGE_SIZE))
|
||||
subtitle = f"Available Galaxy options - Page {self._picker_page + 1} of {page_count}"
|
||||
subtitle = "Choose a shortcut"
|
||||
self._draw_text(
|
||||
self._font(bold=False), subtitle,
|
||||
rl.Vector2(title_pos.x, title_pos.y + 56 * scale), int(26 * scale), self._MUTED_TEXT,
|
||||
rl.Vector2(title_pos.x, title_pos.y + 72 * scale), int(26 * scale), self._MUTED_TEXT,
|
||||
)
|
||||
|
||||
self._draw_close_icon(self._drawer_close_rect)
|
||||
self._draw_close_icon(self._drawer_close_rect, pressed=self._pressed_target == ("close", None))
|
||||
if not self._option_rects:
|
||||
empty_text = "No selectable favorites are available for this vehicle."
|
||||
self._draw_centered_text(
|
||||
@@ -894,10 +945,17 @@ class FavoriteRadialMenu:
|
||||
page_start = self._picker_page * self.PICKER_PAGE_SIZE
|
||||
for option_index, option_rect in self._option_rects:
|
||||
option = self._picker_options[option_index]
|
||||
self._draw_option_card(option_rect, option, scale)
|
||||
self._draw_option_card(option_rect, option, scale,
|
||||
pressed=self._pressed_target == ("option", option_index))
|
||||
|
||||
self._draw_pager_button(self._drawer_prev_rect, "Previous", enabled=self._picker_page > 0, scale=scale)
|
||||
self._draw_pager_button(self._drawer_next_rect, "Next", enabled=self._has_next_picker_page, scale=scale)
|
||||
self._draw_pager_button(
|
||||
self._drawer_prev_rect, "Previous", enabled=self._picker_page > 0, scale=scale,
|
||||
pressed=self._pressed_target == ("previous", None),
|
||||
)
|
||||
self._draw_pager_button(
|
||||
self._drawer_next_rect, "Next", enabled=self._has_next_picker_page, scale=scale,
|
||||
pressed=self._pressed_target == ("next", None),
|
||||
)
|
||||
first_option = page_start + 1 if self._picker_options else 0
|
||||
last_option = min(page_start + self.PICKER_PAGE_SIZE, len(self._picker_options))
|
||||
page_label = f"{first_option}–{last_option} of {len(self._picker_options)}"
|
||||
@@ -908,11 +966,13 @@ class FavoriteRadialMenu:
|
||||
int(26 * scale), self._font(bold=False), self._MUTED_TEXT,
|
||||
)
|
||||
|
||||
def _draw_option_card(self, rect: rl.Rectangle, option: dict[str, Any], scale: float) -> None:
|
||||
rl.draw_rectangle_rounded(rect, self._roundness(rect, 22 * scale), 12, rl.Color(27, 23, 40, 246))
|
||||
def _draw_option_card(self, rect: rl.Rectangle, option: dict[str, Any], scale: float, *, pressed: bool = False) -> None:
|
||||
card_fill = rl.Color(43, 34, 62, 250) if pressed else rl.Color(27, 23, 40, 246)
|
||||
card_border = rl.Color(214, 192, 255, 220) if pressed else rl.Color(*self._PURPLE, 116)
|
||||
rl.draw_rectangle_rounded(rect, self._roundness(rect, 22 * scale), 12, card_fill)
|
||||
rl.draw_rectangle_rounded_lines_ex(rect, self._roundness(rect, 22 * scale), 12, 1.5 * scale,
|
||||
rl.Color(*self._PURPLE, 116))
|
||||
padding = 24.0 * scale
|
||||
card_border)
|
||||
padding = self.PICKER_CARD_PADDING * scale
|
||||
|
||||
key = str(option.get("key") or "")
|
||||
ui_type = str(option.get("ui_type") or "")
|
||||
@@ -927,7 +987,7 @@ class FavoriteRadialMenu:
|
||||
else:
|
||||
badge_label = "TOGGLE"
|
||||
|
||||
badge_fs = int(17 * scale)
|
||||
badge_fs = int(18 * scale)
|
||||
badge_dim = self._measure_text(self._font(bold=True), badge_label, badge_fs)
|
||||
badge_pad_h = 10.0 * scale
|
||||
badge_pad_v = 5.0 * scale
|
||||
@@ -953,61 +1013,64 @@ class FavoriteRadialMenu:
|
||||
|
||||
content_w = rect.width - 2 * padding
|
||||
section_max_w = content_w - badge_w - 12.0 * scale
|
||||
section = self._fit_text(self._font(bold=False), str(option.get("section") or "Favorite"), int(24 * scale), section_max_w)
|
||||
section = self._fit_picker_text(self._font(bold=False), self._picker_section_label(option), int(22 * scale), section_max_w)
|
||||
self._draw_text(self._font(bold=False), section, rl.Vector2(rect.x + padding, rect.y + 24 * scale),
|
||||
int(24 * scale), rl.Color(200, 172, 255, 214))
|
||||
int(22 * scale), rl.Color(200, 172, 255, 214))
|
||||
|
||||
raw_label = str(option.get("label") or option.get("key") or "Favorite").replace('\\"', '"')
|
||||
raw_label = str(option.get("picker_label") or option.get("label") or option.get("key") or "Favorite").replace('\\"', '"')
|
||||
title_fs = int(36 * scale)
|
||||
title_dim = self._measure_text(self._font(bold=True), raw_label, title_fs)
|
||||
if title_dim.x <= content_w:
|
||||
self._draw_text(self._font(bold=True), raw_label, rl.Vector2(rect.x + padding, rect.y + 84 * scale),
|
||||
title_fs, self._TEXT)
|
||||
else:
|
||||
l1, l2 = self._semantic_split_label(raw_label, max_chars=20)
|
||||
if l2 is None:
|
||||
fitted = self._fit_text(self._font(bold=True), l1, title_fs, content_w)
|
||||
title_lines = self._wrap_picker_text(self._font(bold=True), raw_label, title_fs, content_w)
|
||||
if len(title_lines) <= 1:
|
||||
fitted = self._fit_picker_text(self._font(bold=True), title_lines[0] if title_lines else raw_label, title_fs, content_w)
|
||||
self._draw_text(self._font(bold=True), fitted, rl.Vector2(rect.x + padding, rect.y + 84 * scale),
|
||||
title_fs, self._TEXT)
|
||||
else:
|
||||
fs_l1 = int(34 * scale)
|
||||
fs_l2 = int(30 * scale)
|
||||
f_l1 = self._fit_text(self._font(bold=True), l1, fs_l1, content_w)
|
||||
f_l2 = self._fit_text(self._font(bold=True), l2, fs_l2, content_w)
|
||||
f_l1 = self._fit_picker_text(self._font(bold=True), title_lines[0], fs_l1, content_w)
|
||||
f_l2 = self._fit_picker_text(self._font(bold=True), title_lines[1], fs_l2, content_w)
|
||||
self._draw_text(self._font(bold=True), f_l1, rl.Vector2(rect.x + padding, rect.y + 76 * scale), fs_l1, self._TEXT)
|
||||
self._draw_text(self._font(bold=True), f_l2, rl.Vector2(rect.x + padding, rect.y + 118 * scale), fs_l2, rl.Color(255, 255, 255, 235))
|
||||
|
||||
raw_desc = str(option.get("description") or "").replace('\\"', '"')
|
||||
desc_fs = int(24 * scale)
|
||||
raw_desc = str(option.get("picker_description") or option.get("description") or "").replace('\\"', '"')
|
||||
desc_fs = int(26 * scale)
|
||||
if raw_desc:
|
||||
d1, d2 = self._semantic_split_label(raw_desc, max_chars=28)
|
||||
if d2 is None:
|
||||
fitted_d = self._fit_text(self._font(bold=False), d1, desc_fs, content_w)
|
||||
self._draw_text(self._font(bold=False), fitted_d,
|
||||
rl.Vector2(rect.x + padding, rect.y + rect.height - 48 * scale),
|
||||
desc_fs, self._MUTED_TEXT)
|
||||
else:
|
||||
fd1 = self._fit_text(self._font(bold=False), d1, desc_fs, content_w)
|
||||
fd2 = self._fit_text(self._font(bold=False), d2, desc_fs, content_w)
|
||||
self._draw_text(self._font(bold=False), fd1,
|
||||
rl.Vector2(rect.x + padding, rect.y + rect.height - 72 * scale),
|
||||
desc_fs, self._MUTED_TEXT)
|
||||
self._draw_text(self._font(bold=False), fd2,
|
||||
rl.Vector2(rect.x + padding, rect.y + rect.height - 40 * scale),
|
||||
desc_fs, self._MUTED_TEXT)
|
||||
desc_font = self._font(bold=False)
|
||||
description_lines = self._wrap_picker_text(desc_font, raw_desc, desc_fs, content_w)
|
||||
desc_dim = self._measure_text(desc_font, "Ag", desc_fs)
|
||||
line_step = max(36.0 * scale, desc_dim.y + 8.0 * scale)
|
||||
last_y = rect.y + rect.height - padding - desc_dim.y
|
||||
first_y = last_y - line_step * (len(description_lines) - 1)
|
||||
for line_index, description_line in enumerate(description_lines):
|
||||
self._draw_text(
|
||||
desc_font,
|
||||
description_line,
|
||||
rl.Vector2(rect.x + padding, first_y + line_index * line_step),
|
||||
desc_fs,
|
||||
self._MUTED_TEXT,
|
||||
)
|
||||
|
||||
def _draw_close_icon(self, rect: rl.Rectangle) -> None:
|
||||
def _draw_close_icon(self, rect: rl.Rectangle, *, pressed: bool = False) -> None:
|
||||
scale = self._scale_for(self._rect)
|
||||
rl.draw_rectangle_rounded(rect, self._roundness(rect, 18 * scale), 10, rl.Color(255, 255, 255, 22))
|
||||
background_alpha = 46 if pressed else 22
|
||||
rl.draw_rectangle_rounded(rect, self._roundness(rect, 18 * scale), 10, rl.Color(255, 255, 255, background_alpha))
|
||||
center = rl.Vector2(rect.x + rect.width / 2, rect.y + rect.height / 2)
|
||||
half = rect.width * 0.22
|
||||
color = rl.Color(255, 255, 255, 230)
|
||||
color = rl.Color(255, 255, 255, 250 if pressed else 230)
|
||||
rl.draw_line_ex(rl.Vector2(center.x - half, center.y - half), rl.Vector2(center.x + half, center.y + half), 4.5 * scale, color)
|
||||
rl.draw_line_ex(rl.Vector2(center.x - half, center.y + half), rl.Vector2(center.x + half, center.y - half), 4.5 * scale, color)
|
||||
|
||||
def _draw_pager_button(self, rect: rl.Rectangle, label: str, *, enabled: bool, scale: float) -> None:
|
||||
fill = rl.Color(68, 44, 112, 225) if enabled else rl.Color(255, 255, 255, 14)
|
||||
border = rl.Color(*self._PURPLE, 180 if enabled else 42)
|
||||
def _draw_pager_button(self, rect: rl.Rectangle, label: str, *, enabled: bool, scale: float,
|
||||
pressed: bool = False) -> None:
|
||||
fill = rl.Color(94, 64, 148, 238) if pressed and enabled else (
|
||||
rl.Color(68, 44, 112, 225) if enabled else rl.Color(255, 255, 255, 14)
|
||||
)
|
||||
border = rl.Color(*self._PURPLE, 220 if pressed and enabled else (180 if enabled else 42))
|
||||
text_color = self._TEXT if enabled else rl.Color(255, 255, 255, 88)
|
||||
rl.draw_rectangle_rounded(rect, self._roundness(rect, 20 * scale), 10, fill)
|
||||
rl.draw_rectangle_rounded_lines_ex(rect, self._roundness(rect, 20 * scale), 10, 1.8 * scale, border)
|
||||
@@ -1028,6 +1091,28 @@ class FavoriteRadialMenu:
|
||||
shortened = shortened[:-1]
|
||||
return f"{shortened}{ellipsis}" if shortened else ellipsis
|
||||
|
||||
@staticmethod
|
||||
def _fit_picker_text(font: Any, text: str, font_size: int, max_width: float) -> str:
|
||||
if max_width <= 0:
|
||||
return ""
|
||||
if FavoriteRadialMenu._measure_text(font, text, font_size).x <= max_width:
|
||||
return text
|
||||
return FavoriteRadialMenu._append_ellipsis(font, text, font_size, max_width)
|
||||
|
||||
@staticmethod
|
||||
def _append_ellipsis(font: Any, text: str, font_size: int, max_width: float) -> str:
|
||||
ellipsis = "…"
|
||||
if max_width <= 0 or FavoriteRadialMenu._measure_text(font, ellipsis, font_size).x > max_width:
|
||||
return ""
|
||||
|
||||
words = text.strip().rstrip(".…").split()
|
||||
while words:
|
||||
shortened = " ".join(words)
|
||||
if FavoriteRadialMenu._measure_text(font, f"{shortened}{ellipsis}", font_size).x <= max_width:
|
||||
return f"{shortened}{ellipsis}"
|
||||
words.pop()
|
||||
return ellipsis
|
||||
|
||||
@staticmethod
|
||||
def _draw_centered_text(text: str, center: rl.Vector2, font_size: int, font: Any, color: rl.Color) -> None:
|
||||
size = FavoriteRadialMenu._measure_text(font, text, font_size)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pyray as rl
|
||||
@@ -70,6 +71,12 @@ def _rect_center(rect):
|
||||
return SimpleNamespace(x=rect.x + rect.width / 2, y=rect.y + rect.height / 2)
|
||||
|
||||
|
||||
def _open_picker(menu, rect, slot_index=0):
|
||||
_tap(menu, rect, menu.corner_center(rect))
|
||||
_tap(menu, rect, menu.slot_centers(rect)[slot_index])
|
||||
assert menu.state == FavoriteRadialMenu.STATE_PICKER
|
||||
|
||||
|
||||
def _menu(clock, options=None):
|
||||
params = FakeParams()
|
||||
memory = FakeParams()
|
||||
@@ -94,6 +101,20 @@ def test_radial_menu_opens_from_corner_tap_and_arranges_three_slots_on_an_arc():
|
||||
assert centers[0].y < centers[1].y < centers[2].y
|
||||
|
||||
|
||||
def test_corner_hint_keeps_visible_ring_near_content_corner():
|
||||
clock = [0.0]
|
||||
menu, _params, _memory = _menu(clock)
|
||||
rect = rl.Rectangle(0, 0, 2160, 1080)
|
||||
|
||||
scale = menu._scale_for(rect)
|
||||
center = menu.corner_center(rect)
|
||||
ring_radius = menu.CORNER_HINT_RING_RADIUS * scale
|
||||
edge_margin = menu.CORNER_HINT_EDGE_MARGIN * scale
|
||||
|
||||
assert center.x - ring_radius == rect.x + edge_margin
|
||||
assert rect.y + rect.height - center.y - ring_radius == edge_margin
|
||||
assert menu._contains(menu._corner_touch_zone_for(rect), center)
|
||||
|
||||
|
||||
def test_radial_menu_opens_from_diagonal_inward_swipe_and_auto_collapses_after_six_seconds():
|
||||
clock = [0.0]
|
||||
@@ -213,6 +234,308 @@ def test_picker_pages_keep_all_available_options_selectable():
|
||||
assert params.get(FAVORITE_SLOTS_PARAM)[0]["key"] == "Option08"
|
||||
|
||||
|
||||
def test_picker_geometry_fits_the_production_content_rect_without_card_overlap():
|
||||
clock = [0.0]
|
||||
options = [
|
||||
{"key": f"Option{index}", "label": f"Option {index}", "description": "Test option", "section": "Testing"}
|
||||
for index in range(8)
|
||||
]
|
||||
menu, _params, _memory = _menu(clock, options=options)
|
||||
rect = rl.Rectangle(30, 30, 2100, 1020)
|
||||
|
||||
_open_picker(menu, rect)
|
||||
|
||||
drawer = menu._drawer_rect
|
||||
assert drawer.x >= rect.x
|
||||
assert drawer.y >= rect.y
|
||||
assert drawer.x + drawer.width <= rect.x + rect.width
|
||||
assert drawer.y + drawer.height <= rect.y + rect.height
|
||||
assert len(menu._option_rects) == 8
|
||||
|
||||
cards = [option_rect for _index, option_rect in menu._option_rects]
|
||||
for card in cards:
|
||||
assert card.x >= drawer.x
|
||||
assert card.y >= drawer.y
|
||||
assert card.x + card.width <= drawer.x + drawer.width
|
||||
assert card.y + card.height <= drawer.y + drawer.height
|
||||
assert card.width >= 440
|
||||
assert card.height >= 250
|
||||
|
||||
for index, first in enumerate(cards):
|
||||
for second in cards[index + 1:]:
|
||||
horizontal_gap = max(first.x, second.x) - min(first.x + first.width, second.x + second.width)
|
||||
vertical_gap = max(first.y, second.y) - min(first.y + first.height, second.y + second.height)
|
||||
assert horizontal_gap > 0 or vertical_gap > 0
|
||||
|
||||
footer_top = drawer.y + drawer.height - menu.PICKER_FOOTER_HEIGHT * menu._scale_for(rect)
|
||||
assert menu._drawer_prev_rect.y >= footer_top
|
||||
assert menu._drawer_next_rect.y >= footer_top
|
||||
|
||||
|
||||
def test_picker_control_hit_targets_are_tall_enough_for_production_scale():
|
||||
clock = [0.0]
|
||||
options = [
|
||||
{"key": f"Option{index}", "label": f"Option {index}", "description": "Test option", "section": "Testing"}
|
||||
for index in range(9)
|
||||
]
|
||||
menu, _params, _memory = _menu(clock, options=options)
|
||||
rect = rl.Rectangle(30, 30, 2100, 1020)
|
||||
|
||||
_open_picker(menu, rect)
|
||||
minimum_hit_height = 144.0 * menu._scale_for(rect)
|
||||
|
||||
assert menu._drawer_prev_hit_rect.height >= minimum_hit_height
|
||||
assert menu._drawer_next_hit_rect.height >= minimum_hit_height
|
||||
assert menu._drawer_close_hit_rect.height >= minimum_hit_height
|
||||
|
||||
|
||||
def test_picker_description_uses_measured_wrap_and_truncates_to_two_lines(monkeypatch):
|
||||
clock = [0.0]
|
||||
long_description = "This description is deliberately long enough to require measured wrapping."
|
||||
option = {
|
||||
"key": "MeasuredDescription",
|
||||
"label": "Measured Description",
|
||||
"description": long_description,
|
||||
"section": "Testing",
|
||||
}
|
||||
menu, _params, _memory = _menu(clock, options=[option])
|
||||
rect = rl.Rectangle(30, 30, 2100, 1020)
|
||||
_open_picker(menu, rect)
|
||||
card_rect = menu._option_rects[0][1]
|
||||
|
||||
wrap_calls = []
|
||||
drawn_texts = []
|
||||
|
||||
def fake_wrap(font, text, font_size, max_width, spacing=0):
|
||||
wrap_calls.append((text, font_size, max_width))
|
||||
return ["Measured first line", "Measured second line", "Measured overflow"]
|
||||
|
||||
monkeypatch.setitem(sys.modules, "openpilot.system.ui.lib.wrap_text", SimpleNamespace(wrap_text=fake_wrap))
|
||||
monkeypatch.setattr(menu, "_font", lambda *, bold: object())
|
||||
monkeypatch.setattr(FavoriteRadialMenu, "_measure_text",
|
||||
staticmethod(lambda _font, text, _font_size: rl.Vector2(max(10, len(text) * 4), 20)))
|
||||
monkeypatch.setattr(FavoriteRadialMenu, "_draw_text",
|
||||
staticmethod(lambda _font, text, _pos, _font_size, _color: drawn_texts.append(text)))
|
||||
monkeypatch.setattr(rl, "draw_rectangle_rounded", lambda *args: None)
|
||||
monkeypatch.setattr(rl, "draw_rectangle_rounded_lines_ex", lambda *args: None)
|
||||
|
||||
menu._draw_option_card(card_rect, option, menu._scale_for(rect))
|
||||
|
||||
assert len(wrap_calls) == 1
|
||||
assert wrap_calls[0][0] == long_description
|
||||
assert wrap_calls[0][1] >= 24
|
||||
assert wrap_calls[0][2] > 0
|
||||
assert drawn_texts[-2:] == ["Measured first line", "Measured second line…"]
|
||||
|
||||
|
||||
def test_picker_uses_compact_section_labels_without_mutating_catalog_copy():
|
||||
clock = [0.0]
|
||||
menu, _params, _memory = _menu(clock)
|
||||
|
||||
assert menu._picker_section_label({"section": "Longitudinal (Speed & Following)"}) == "Speed & Following"
|
||||
assert menu._picker_section_label({"section": "Lateral (Steering)"}) == "Steering"
|
||||
assert menu._picker_section_label({"section": "Visual (Display & UI)"}) == "Display & UI"
|
||||
assert menu._picker_section_label({"section": "Testing"}) == "Testing"
|
||||
|
||||
|
||||
def test_picker_fit_text_truncates_at_word_boundary(monkeypatch):
|
||||
monkeypatch.setattr(FavoriteRadialMenu, "_measure_text",
|
||||
staticmethod(lambda _font, text, _font_size: rl.Vector2(len(text) * 10, 20)))
|
||||
|
||||
fitted = FavoriteRadialMenu._fit_picker_text(object(), "Longitudinal Speed Following", 24, 140)
|
||||
|
||||
assert fitted == "Longitudinal…"
|
||||
|
||||
|
||||
def test_picker_wraps_long_titles_by_measured_words(monkeypatch):
|
||||
clock = [0.0]
|
||||
option = {
|
||||
"key": "LongTitle",
|
||||
"label": "Fallback Label",
|
||||
"picker_label": "Adaptive Acceleration Limit",
|
||||
"description": "",
|
||||
"section": "Testing",
|
||||
}
|
||||
menu, _params, _memory = _menu(clock, options=[option])
|
||||
rect = rl.Rectangle(30, 30, 2100, 1020)
|
||||
_open_picker(menu, rect)
|
||||
card_rect = menu._option_rects[0][1]
|
||||
drawn_texts = []
|
||||
|
||||
def fake_measure(_font, text, _font_size):
|
||||
width = 1000 if text == "Adaptive Acceleration Limit" else max(10, len(text) * 4)
|
||||
return rl.Vector2(width, 20)
|
||||
|
||||
def fake_wrap(_font, text, _font_size, _max_width, spacing=0):
|
||||
assert text == "Adaptive Acceleration Limit"
|
||||
return ["Adaptive Acceleration", "Limit"]
|
||||
|
||||
monkeypatch.setitem(sys.modules, "openpilot.system.ui.lib.wrap_text", SimpleNamespace(wrap_text=fake_wrap))
|
||||
monkeypatch.setattr(menu, "_font", lambda *, bold: object())
|
||||
monkeypatch.setattr(FavoriteRadialMenu, "_measure_text", staticmethod(fake_measure))
|
||||
monkeypatch.setattr(FavoriteRadialMenu, "_draw_text",
|
||||
staticmethod(lambda _font, text, _pos, _font_size, _color: drawn_texts.append(text)))
|
||||
monkeypatch.setattr(rl, "draw_rectangle_rounded", lambda *args: None)
|
||||
monkeypatch.setattr(rl, "draw_rectangle_rounded_lines_ex", lambda *args: None)
|
||||
|
||||
menu._draw_option_card(card_rect, option, menu._scale_for(rect))
|
||||
|
||||
assert "Adaptive Acceleration" in drawn_texts
|
||||
assert "Limit" in drawn_texts
|
||||
assert "Fallback Label" not in drawn_texts
|
||||
|
||||
|
||||
def test_picker_prefers_picker_specific_description(monkeypatch):
|
||||
clock = [0.0]
|
||||
option = {
|
||||
"key": "CompactDescription",
|
||||
"label": "Compact Description",
|
||||
"description": "The complete settings description should remain available outside the picker.",
|
||||
"picker_description": "Short picker description.",
|
||||
"section": "Testing",
|
||||
}
|
||||
menu, _params, _memory = _menu(clock, options=[option])
|
||||
rect = rl.Rectangle(30, 30, 2100, 1020)
|
||||
_open_picker(menu, rect)
|
||||
card_rect = menu._option_rects[0][1]
|
||||
wrap_inputs = []
|
||||
drawn_texts = []
|
||||
|
||||
def fake_wrap(font, text, font_size, max_width, spacing=0):
|
||||
wrap_inputs.append(text)
|
||||
return [text]
|
||||
|
||||
monkeypatch.setitem(sys.modules, "openpilot.system.ui.lib.wrap_text", SimpleNamespace(wrap_text=fake_wrap))
|
||||
monkeypatch.setattr(menu, "_font", lambda *, bold: object())
|
||||
monkeypatch.setattr(FavoriteRadialMenu, "_measure_text",
|
||||
staticmethod(lambda _font, text, _font_size: rl.Vector2(max(10, len(text) * 4), 20)))
|
||||
monkeypatch.setattr(FavoriteRadialMenu, "_draw_text",
|
||||
staticmethod(lambda _font, text, _pos, _font_size, _color: drawn_texts.append(text)))
|
||||
monkeypatch.setattr(rl, "draw_rectangle_rounded", lambda *args: None)
|
||||
monkeypatch.setattr(rl, "draw_rectangle_rounded_lines_ex", lambda *args: None)
|
||||
|
||||
menu._draw_option_card(card_rect, option, menu._scale_for(rect))
|
||||
|
||||
assert wrap_inputs == ["Short picker description."]
|
||||
assert "Short picker description." in drawn_texts
|
||||
assert "The complete settings description should remain available outside the picker." not in drawn_texts
|
||||
|
||||
|
||||
def test_picker_pressed_card_has_a_distinct_visual_state(monkeypatch):
|
||||
clock = [0.0]
|
||||
option = {"key": "PressedOption", "label": "Pressed Option", "description": "", "section": "Testing"}
|
||||
menu, _params, _memory = _menu(clock, options=[option])
|
||||
rect = rl.Rectangle(30, 30, 2100, 1020)
|
||||
_open_picker(menu, rect)
|
||||
card_rect = menu._option_rects[0][1]
|
||||
card_colors = []
|
||||
|
||||
def capture_card_fill(draw_rect, *_args):
|
||||
if all(abs(getattr(draw_rect, attr) - getattr(card_rect, attr)) < 0.01 for attr in ("x", "y", "width", "height")):
|
||||
color = _args[-1]
|
||||
card_colors.append((color.r, color.g, color.b, color.a))
|
||||
|
||||
monkeypatch.setattr(menu, "_font", lambda *, bold: object())
|
||||
monkeypatch.setattr(FavoriteRadialMenu, "_measure_text",
|
||||
staticmethod(lambda _font, text, _font_size: rl.Vector2(max(10, len(text) * 4), 20)))
|
||||
monkeypatch.setattr(FavoriteRadialMenu, "_draw_text", staticmethod(lambda *args: None))
|
||||
monkeypatch.setattr(rl, "draw_rectangle_rounded", capture_card_fill)
|
||||
monkeypatch.setattr(rl, "draw_rectangle_rounded_lines_ex", lambda *args: None)
|
||||
|
||||
menu._draw_option_card(card_rect, option, menu._scale_for(rect), pressed=False)
|
||||
menu._draw_option_card(card_rect, option, menu._scale_for(rect), pressed=True)
|
||||
|
||||
assert len(card_colors) == 2
|
||||
assert card_colors[0] != card_colors[1]
|
||||
|
||||
|
||||
def test_picker_release_on_a_different_option_cancels_without_assignment():
|
||||
clock = [0.0]
|
||||
options = [
|
||||
{"key": "OptionA", "label": "Option A", "description": "A", "section": "Testing"},
|
||||
{"key": "OptionB", "label": "Option B", "description": "B", "section": "Testing"},
|
||||
]
|
||||
menu, params, _memory = _menu(clock, options=options)
|
||||
rect = rl.Rectangle(0, 0, 2160, 1080)
|
||||
_open_picker(menu, rect)
|
||||
option_a, option_b = menu.option_centers(rect)
|
||||
|
||||
menu.process_mouse_events([_event(option_a.x, option_a.y, pressed=True)], rect)
|
||||
menu.process_mouse_events([_event(option_b.x, option_b.y, released=True)], rect)
|
||||
|
||||
assert menu.state == FavoriteRadialMenu.STATE_PICKER
|
||||
assert params.get(FAVORITE_SLOTS_PARAM) is None
|
||||
|
||||
|
||||
def test_picker_release_target_must_match_pressed_target(monkeypatch):
|
||||
clock = [0.0]
|
||||
menu, _params, _memory = _menu(clock, options=[
|
||||
{"key": "OptionA", "label": "Option A", "description": "A", "section": "Testing"},
|
||||
{"key": "OptionB", "label": "Option B", "description": "B", "section": "Testing"},
|
||||
])
|
||||
rect = rl.Rectangle(0, 0, 2160, 1080)
|
||||
_open_picker(menu, rect)
|
||||
press_pos = SimpleNamespace(x=100, y=100)
|
||||
activated = []
|
||||
menu._pressed_target = ("option", 0)
|
||||
menu._press_start_time = clock[0]
|
||||
menu._press_start_pos = press_pos
|
||||
monkeypatch.setattr(menu, "_target_at", lambda _mouse_pos: ("option", 1))
|
||||
monkeypatch.setattr(menu, "_activate_target", lambda target: activated.append(target))
|
||||
|
||||
menu._handle_release(press_pos)
|
||||
|
||||
assert activated == []
|
||||
assert menu.state == FavoriteRadialMenu.STATE_PICKER
|
||||
|
||||
|
||||
def test_picker_release_outside_after_pressing_an_option_cancels_without_assignment():
|
||||
clock = [0.0]
|
||||
option = {"key": "OptionA", "label": "Option A", "description": "A", "section": "Testing"}
|
||||
menu, params, _memory = _menu(clock, options=[option])
|
||||
rect = rl.Rectangle(0, 0, 2160, 1080)
|
||||
_open_picker(menu, rect)
|
||||
option_a = menu.option_centers(rect)[0]
|
||||
outside = SimpleNamespace(x=rect.x + 5, y=rect.y + 5)
|
||||
|
||||
menu.process_mouse_events([_event(option_a.x, option_a.y, pressed=True)], rect)
|
||||
menu.process_mouse_events([_event(outside.x, outside.y, released=True)], rect)
|
||||
|
||||
assert menu.state == FavoriteRadialMenu.STATE_PICKER
|
||||
assert params.get(FAVORITE_SLOTS_PARAM) is None
|
||||
|
||||
|
||||
def test_picker_release_on_the_same_option_assigns_it():
|
||||
clock = [0.0]
|
||||
option = {"key": "OptionA", "label": "Option A", "description": "A", "section": "Testing"}
|
||||
menu, params, _memory = _menu(clock, options=[option])
|
||||
rect = rl.Rectangle(0, 0, 2160, 1080)
|
||||
_open_picker(menu, rect)
|
||||
|
||||
_tap(menu, rect, menu.option_centers(rect)[0])
|
||||
|
||||
assert menu.state == FavoriteRadialMenu.STATE_RADIAL
|
||||
assert params.get(FAVORITE_SLOTS_PARAM)[0]["key"] == "OptionA"
|
||||
|
||||
|
||||
def test_picker_disabled_previous_stays_open_but_an_untargeted_outside_tap_dismisses():
|
||||
clock = [0.0]
|
||||
options = [
|
||||
{"key": f"Option{index:02}", "label": f"Option {index:02}", "description": "Test option", "section": "Testing"}
|
||||
for index in range(9)
|
||||
]
|
||||
menu, _params, _memory = _menu(clock, options=options)
|
||||
rect = rl.Rectangle(0, 0, 2160, 1080)
|
||||
_open_picker(menu, rect)
|
||||
|
||||
_tap(menu, rect, _rect_center(menu._drawer_prev_rect))
|
||||
assert menu.state == FavoriteRadialMenu.STATE_PICKER
|
||||
|
||||
outside = SimpleNamespace(x=rect.x + 5, y=rect.y + 5)
|
||||
_tap(menu, rect, outside)
|
||||
assert menu.state == FavoriteRadialMenu.STATE_COLLAPSED
|
||||
|
||||
|
||||
def test_short_tap_toggles_action_and_does_not_enter_edit_mode():
|
||||
clock = [0.0]
|
||||
menu, params, memory = _menu(clock)
|
||||
@@ -485,6 +808,79 @@ def test_render_during_wave_flash_animation(monkeypatch):
|
||||
assert menu._flash_slot is None
|
||||
|
||||
|
||||
def test_radial_render_rect_snaps_each_boundary():
|
||||
rect = rl.Rectangle(10.36, 20.64, 450.75, 103.21)
|
||||
|
||||
snapped = FavoriteRadialMenu._snap_render_rect(rect)
|
||||
|
||||
assert (snapped.x, snapped.y, snapped.width, snapped.height) == (10.0, 21.0, 451.0, 103.0)
|
||||
|
||||
|
||||
def test_radial_blade_render_uses_pixel_aligned_chassis_and_border(monkeypatch):
|
||||
clock = [0.0]
|
||||
options = [
|
||||
{"key": "Compass", "label": "Compass", "ui_type": "toggle", "data_type": "bool"},
|
||||
]
|
||||
menu, params, _memory = _menu(clock, options=options)
|
||||
params.types["Compass"] = ParamKeyType.BOOL
|
||||
params.put("Compass", False)
|
||||
rect = rl.Rectangle(0, 0, 2160, 1080)
|
||||
|
||||
fills = []
|
||||
strokes = []
|
||||
monkeypatch.setattr(menu, "_font", lambda *, bold: object())
|
||||
monkeypatch.setattr(FavoriteRadialMenu, "_measure_text",
|
||||
staticmethod(lambda _font, _text, _font_size: rl.Vector2(100.0, 20.0)))
|
||||
monkeypatch.setattr(FavoriteRadialMenu, "_draw_text", staticmethod(lambda *args: None))
|
||||
monkeypatch.setattr(rl, "draw_ring", lambda *args: None)
|
||||
monkeypatch.setattr(rl, "draw_circle_v", lambda *args: None)
|
||||
monkeypatch.setattr(rl, "draw_line_ex", lambda *args: None)
|
||||
monkeypatch.setattr(rl, "draw_rectangle_rounded",
|
||||
lambda *args: fills.append(args))
|
||||
monkeypatch.setattr(rl, "draw_rectangle_rounded_lines_ex",
|
||||
lambda *args: strokes.append(args))
|
||||
|
||||
cases = (
|
||||
([], None, 1, 255),
|
||||
([{"enabled": True, "show_onroad": True, "key": "Compass", "label": "Compass"}], ("slot", 0), 2, 255),
|
||||
)
|
||||
for slot_payload, pressed_target, expected_border_width, expected_fill_alpha in cases:
|
||||
params.put(FAVORITE_SLOTS_PARAM, slot_payload)
|
||||
menu._pressed_target = pressed_target
|
||||
menu._layout(rect)
|
||||
fills.clear()
|
||||
strokes.clear()
|
||||
|
||||
blade_rect = menu._slot_rects[0][5]
|
||||
chassis_rect = blade_rect
|
||||
if pressed_target is not None:
|
||||
pad = 2.0 * menu._scale_for(rect)
|
||||
chassis_rect = rl.Rectangle(
|
||||
blade_rect.x + pad,
|
||||
blade_rect.y + pad,
|
||||
blade_rect.width - pad * 2.0,
|
||||
blade_rect.height - pad * 2.0,
|
||||
)
|
||||
expected_rect = menu._snap_render_rect(chassis_rect)
|
||||
|
||||
menu._draw_radial_menu()
|
||||
|
||||
assert fills
|
||||
assert strokes
|
||||
expected_tuple = (expected_rect.x, expected_rect.y, expected_rect.width, expected_rect.height)
|
||||
assert (fills[0][0].x, fills[0][0].y, fills[0][0].width, fills[0][0].height) == expected_tuple
|
||||
assert (strokes[0][0].x, strokes[0][0].y, strokes[0][0].width, strokes[0][0].height) == expected_tuple
|
||||
assert strokes[0][3] == expected_border_width
|
||||
assert fills[0][3].a == expected_fill_alpha
|
||||
assert all(float(value).is_integer() for value in (
|
||||
strokes[0][0].x,
|
||||
strokes[0][0].y,
|
||||
strokes[0][0].width,
|
||||
strokes[0][0].height,
|
||||
strokes[0][3],
|
||||
))
|
||||
|
||||
|
||||
def test_render_boolean_toggle_switch_and_picker_badges(monkeypatch):
|
||||
clock = [0.0]
|
||||
options = [
|
||||
@@ -536,6 +932,3 @@ def test_render_boolean_toggle_switch_and_picker_badges(monkeypatch):
|
||||
assert "TOGGLE" in drawn_texts
|
||||
assert "2 STATES" in drawn_texts
|
||||
assert "ACTION" in drawn_texts
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -127,6 +127,7 @@
|
||||
"key": "AlwaysOnLateral",
|
||||
"label": "Always On Lateral",
|
||||
"description": "openpilot's steering remains active even when the accelerator or brake pedals are pressed.",
|
||||
"picker_description": "Keeps steering active while the accelerator or brake pedals are pressed.",
|
||||
"data_type": "bool",
|
||||
"ui_type": "toggle",
|
||||
"is_parent_toggle": true,
|
||||
@@ -1242,6 +1243,7 @@
|
||||
"key": "TacoTune",
|
||||
"label": "\\\"Taco Bell Run\\\" Turn Speed Hack",
|
||||
"description": "The turn-speed hack from comma's 2022 \"Taco Bell Run\". Designed to slow down for left and right turns.",
|
||||
"picker_description": "Slows down for left and right turns using the Taco Bell Run turn-speed hack.",
|
||||
"data_type": "bool",
|
||||
"ui_type": "toggle",
|
||||
"parent_key": "LongitudinalTune",
|
||||
@@ -2852,6 +2854,7 @@
|
||||
"key": "NAPAdaptiveAccel",
|
||||
"label": "Adaptive Acceleration Limit",
|
||||
"description": "Reduce maximum acceleration as you close in on a lead vehicle when Tesla pedal-long is active.",
|
||||
"picker_description": "Reduces maximum acceleration near a lead vehicle when Tesla pedal-long is active.",
|
||||
"data_type": "bool",
|
||||
"ui_type": "toggle",
|
||||
"parent_key": "NAPPedalEnabled",
|
||||
@@ -3961,6 +3964,7 @@
|
||||
"key": "AlwaysAllowUploads",
|
||||
"label": "Always Allow Uploads",
|
||||
"description": "Override upload blocks and always keep uploader enabled. Advanced use only.",
|
||||
"picker_description": "Advanced: keep uploader enabled even when upload blocks are active.",
|
||||
"data_type": "bool",
|
||||
"ui_type": "toggle",
|
||||
"parent_key": "NoUploads",
|
||||
@@ -4328,6 +4332,7 @@
|
||||
"key": "AllowImpossibleAcceleration",
|
||||
"label": "Allow Impossible Acceleration",
|
||||
"description": "WARNING: This suppresses openpilot's excessive longitudinal actuation diagnostic when measured acceleration looks impossible for the requested gas/brake command.\n\nLeave this OFF unless you are intentionally testing edge cases like shifting to neutral and understand that it can hide a real actuation problem.",
|
||||
"picker_description": "WARNING: Hides the impossible-acceleration diagnostic. Leave OFF unless testing edge cases.",
|
||||
"data_type": "bool",
|
||||
"ui_type": "toggle",
|
||||
"settings_tier": "advanced"
|
||||
|
||||
@@ -159,6 +159,10 @@ def build_favorite_slot_options(is_eligible_param: Callable[[str], bool], *,
|
||||
"data_type": data_type,
|
||||
"requiresCapability": str(param_data.get("requires_capability") or ""),
|
||||
}
|
||||
for picker_field in ("picker_label", "picker_description"):
|
||||
picker_value = str(param_data.get(picker_field) or "").strip()
|
||||
if picker_value:
|
||||
opt_dict[picker_field] = picker_value
|
||||
if is_dropdown:
|
||||
opt_dict["options"] = [dict(o) for o in raw_options if isinstance(o, dict)]
|
||||
options.append(opt_dict)
|
||||
@@ -522,4 +526,3 @@ def unassign_favorite_slot(slot_index: int, params: Params | None = None, params
|
||||
saved = save_favorite_slots(slots, params, eligible_keys=eligible_keys)
|
||||
request_starpilot_toggle_refresh(params_memory)
|
||||
return saved
|
||||
|
||||
|
||||
@@ -196,7 +196,14 @@ def test_shared_favorite_option_catalog_uses_layout_metadata_and_capability_gate
|
||||
{
|
||||
"name": "Testing",
|
||||
"params": [
|
||||
{"key": "FeatureToggle", "label": "Feature Toggle", "description": "Visible", "ui_type": "toggle", "data_type": "bool"},
|
||||
{
|
||||
"key": "FeatureToggle",
|
||||
"label": "Feature Toggle",
|
||||
"description": "Visible",
|
||||
"picker_description": "Compact",
|
||||
"ui_type": "toggle",
|
||||
"data_type": "bool",
|
||||
},
|
||||
{"key": "UnsupportedToggle", "label": "Unsupported", "ui_type": "toggle", "data_type": "bool"},
|
||||
{"key": "AlphaLongitudinalEnabled", "label": "Alpha", "ui_type": "toggle", "data_type": "bool"},
|
||||
{"key": "RivianAngleControl", "label": "Rivian", "ui_type": "toggle", "data_type": "bool", "requires_capability": "HasRivianAngleHarness"},
|
||||
@@ -212,6 +219,9 @@ def test_shared_favorite_option_catalog_uses_layout_metadata_and_capability_gate
|
||||
|
||||
assert FAVORITE_ACTION_DISTANCE_DECREASE in {option["key"] for option in options}
|
||||
assert {option["key"] for option in options} >= {"FeatureToggle", "RivianAngleControl"}
|
||||
feature_option = next(option for option in options if option["key"] == "FeatureToggle")
|
||||
assert feature_option["description"] == "Visible"
|
||||
assert feature_option["picker_description"] == "Compact"
|
||||
assert "UnsupportedToggle" not in {option["key"] for option in options}
|
||||
assert "AlphaLongitudinalEnabled" not in {option["key"] for option in options}
|
||||
assert "RivianAngleControl" not in {
|
||||
@@ -372,4 +382,3 @@ def test_get_favorite_param_value_and_get_favorite_values():
|
||||
assert active_idx == 2
|
||||
assert active_label == "Sport"
|
||||
assert len(opts) == 3
|
||||
|
||||
|
||||
Reference in New Issue
Block a user