mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 00:53:45 +08:00
347 lines
22 KiB
Python
347 lines
22 KiB
Python
"""Large saved-feature editor inside the established Settings right pane."""
|
|
|
|
from math import copysign
|
|
from collections.abc import Callable
|
|
import time
|
|
|
|
import pyray as rl
|
|
|
|
from openpilot.starpilot.ui import clip
|
|
from openpilot.starpilot.ui.feature_settings_state import (
|
|
FeatureRow, FeatureSettingsState, FeatureUiAction, FEATURE_CONFIRM_ACTIONS, is_long_confirm_action, boolean_value as _boolean_value,
|
|
FEATURE_ROW_HEIGHT, FEATURE_VISIBLE_ROWS, FEATURE_CONTROL_LEFT, FEATURE_CONTROL_RIGHT,
|
|
FEATURE_BUTTON_TOP, FEATURE_BUTTON_HEIGHT, FEATURE_ACTION_MARGIN, FEATURE_HEADER_HEIGHT,
|
|
FEATURE_TAP_SLOP, FEATURE_FLICK_DISTANCE, FEATURE_PAGE_COUNTER_WIDTH,
|
|
feature_scroll, feature_action_left, feature_action_rects, feature_row_top, feature_page_counter_left, feature_footer_buttons,
|
|
value_buttons, value_editor_rect, value_done_rect, value_text, value_reason,
|
|
)
|
|
from openpilot.starpilot.ui.presentation import BitmapFonts, FontRole, Profile
|
|
from openpilot.starpilot.ui.settings_geometry import (
|
|
ACCENT, ACTIVE_ROW_BG, ACTIVE_ROW_BORDER, CONTROL_BG, CONTROL_BORDER, CONTROL_PRESSED_BG, DANGER,
|
|
PANEL_BG, PANEL_BORDER, PANEL_INNER_BORDER, ROW_SEPARATOR, TEXT_MUTED, TEXT_PRIMARY, TEXT_SECONDARY,
|
|
draw_aether_toggle, draw_rounded_fill, draw_rounded_stroke, draw_settings_header, elide_text,
|
|
)
|
|
|
|
LABEL_SIZE = 50
|
|
DETAIL_SIZE = 35
|
|
PAGE_TRANSITION_TIME = 0.18
|
|
EDGE_ACCENT = rl.color_lerp(PANEL_BG, ACCENT, 0.4)
|
|
EDGE_FADE_HEIGHT = 96
|
|
EDGE_FADE_CLEAR = rl.Color(PANEL_BG.r, PANEL_BG.g, PANEL_BG.b, 0)
|
|
VALUE_SELECTED_BORDER = rl.Color(ACCENT.r, ACCENT.g, ACCENT.b, 100)
|
|
VALUE_DONE_BG = rl.Color(ACCENT.r, ACCENT.g, ACCENT.b, 34)
|
|
DRIVE_MODE_BORDER = rl.Color(108, 102, 122, 255)
|
|
|
|
|
|
def _confirmation(row: FeatureRow) -> bool:
|
|
return row.key in FEATURE_CONFIRM_ACTIONS or row.key.startswith("pip:format:") or is_long_confirm_action(row.key)
|
|
|
|
|
|
def feature_pull(drag_x: float, can_page: bool) -> float:
|
|
excess = max(0, abs(drag_x) - FEATURE_TAP_SLOP)
|
|
limit = FEATURE_FLICK_DISTANCE if can_page else FEATURE_TAP_SLOP / 2
|
|
return copysign(min(excess, limit), drag_x) if excess else 0.0
|
|
|
|
|
|
class FeatureSettingsView:
|
|
def __init__(self, fonts: BitmapFonts):
|
|
if fonts.profile != Profile.LARGE:
|
|
raise ValueError("Large profile only")
|
|
self.fonts = fonts
|
|
self.pressed_action: Callable[[FeatureSettingsState], FeatureUiAction | None] = lambda _state: None
|
|
self._last_state: FeatureSettingsState | None = None
|
|
self._last_pull = 0.0
|
|
self._awaiting_render = False
|
|
self._transition: tuple[FeatureSettingsState, float, float] | None = None
|
|
|
|
def reset(self, state: FeatureSettingsState | None = None) -> bool:
|
|
"""Finish presentation; taps on moving or replaced rows must be suppressed."""
|
|
moving = self._transition is not None
|
|
pending = state is not None and self._last_state is not None and state.scroll != self._last_state.scroll
|
|
blocked = self._awaiting_render or moving or pending
|
|
# Keep interrupted row taps blocked until the settled page is drawn.
|
|
self._awaiting_render = blocked if state is not None else False
|
|
self._last_state = state if self._last_state is not None else None
|
|
self._last_pull = 0.0
|
|
self._transition = None
|
|
return blocked
|
|
|
|
def _page_frame(self, state: FeatureSettingsState, pull_x: float, dragging: bool) -> tuple[FeatureSettingsState, float, int]:
|
|
previous = self._last_state
|
|
same_context = previous is not None and (previous.page, previous.title, previous.subtitle, previous.sidebar_expanded) == (
|
|
state.page, state.title, state.subtitle, state.sidebar_expanded)
|
|
if not same_context or dragging:
|
|
self._transition = None
|
|
elif previous.scroll != state.scroll:
|
|
direction = 1 if state.scroll > previous.scroll else -1
|
|
self._transition = ((previous, time.monotonic(), self._last_pull)
|
|
if previous.rows == state.rows and feature_scroll(previous.scroll, direction, len(state.rows)) == state.scroll else None)
|
|
self._last_state = state
|
|
rows, cover = state, 0
|
|
if self._transition is not None:
|
|
outgoing, started, start_pull = self._transition
|
|
phase = (time.monotonic() - started) / (PAGE_TRANSITION_TIME / 2)
|
|
if phase >= 2 or outgoing.rows != state.rows:
|
|
self._transition = None
|
|
else:
|
|
direction = 1 if state.scroll > outgoing.scroll else -1
|
|
t = max(0.0, phase if phase < 1 else phase - 1)
|
|
t = t * t * (3 - 2 * t)
|
|
if phase < 1:
|
|
rows = outgoing
|
|
pull_x = start_pull * (1 - t) - direction * FEATURE_FLICK_DISTANCE * t
|
|
cover = round(255 * t)
|
|
else:
|
|
pull_x = direction * FEATURE_FLICK_DISTANCE * (1 - t)
|
|
cover = round(255 * (1 - t))
|
|
self._last_pull = pull_x
|
|
self._awaiting_render = False
|
|
return rows, pull_x, cover
|
|
|
|
def render(self, state: FeatureSettingsState, drag_x: float = 0.0, *, draw_row=None) -> None:
|
|
pressed = self.pressed_action(state)
|
|
left = 520 if state.sidebar_expanded else 20
|
|
row_top = feature_row_top(state)
|
|
shell = rl.Rectangle(left - 10, 10, 2160 - left, 1060)
|
|
draw_rounded_fill(shell, PANEL_BG, radius_px=32)
|
|
draw_rounded_stroke(shell, PANEL_BORDER, radius_px=32)
|
|
draw_rounded_stroke(rl.Rectangle(shell.x + 2, 12, shell.width - 4, 1056), PANEL_INNER_BORDER, radius_px=29)
|
|
clip.begin_scissor_mode(left - 10, 2, 2160 - left, FEATURE_HEADER_HEIGHT + 20)
|
|
try:
|
|
draw_settings_header(self.fonts, state.sidebar_expanded, state.title, state.parent_title, back=True)
|
|
finally:
|
|
clip.end_scissor_mode()
|
|
subtitle = self._elide(state.subtitle, FontRole.NORMAL, DETAIL_SIZE, 2094 - left - 55)
|
|
if subtitle:
|
|
top, _ = self.fonts.vertical_ink(subtitle, FontRole.NORMAL, DETAIL_SIZE)
|
|
self.fonts.draw(subtitle, FontRole.NORMAL, DETAIL_SIZE, left + 55, 116 - top, TEXT_SECONDARY)
|
|
if len(state.rows) == 1 and (buttons := value_buttons(state, state.rows[0], row_top)):
|
|
self._page_frame(state, 0, False)
|
|
self._value_row(state, state.rows[0], rl.Rectangle(*value_editor_rect(state)), buttons, pressed)
|
|
self._value_done(state, pressed)
|
|
return
|
|
can_page = drag_x != 0 and feature_scroll(state.scroll, 1 if drag_x < 0 else -1, len(state.rows)) != state.scroll
|
|
rows_state, pull_x, cover = self._page_frame(state, feature_pull(drag_x, can_page), drag_x != 0)
|
|
body = rl.Rectangle(left + 25, row_top, 2100 - left, FEATURE_VISIBLE_ROWS * FEATURE_ROW_HEIGHT)
|
|
clip.begin_scissor_mode(body.x, body.y, body.width, body.height)
|
|
try:
|
|
if pull_x and can_page:
|
|
gap = abs(pull_x)
|
|
gap_x = body.x + body.width + pull_x if pull_x < 0 else body.x
|
|
edge = rl.color_lerp(PANEL_BG, EDGE_ACCENT, gap / FEATURE_FLICK_DISTANCE)
|
|
color_left, color_right = (PANEL_BG, edge) if pull_x < 0 else (edge, PANEL_BG)
|
|
rl.draw_rectangle_gradient_ex(rl.Rectangle(gap_x, body.y, gap, body.height),
|
|
color_left, color_left, color_right, color_right)
|
|
# Feather the exposed background before drawing the moving rows and their text.
|
|
rl.draw_rectangle_gradient_ex(rl.Rectangle(gap_x, body.y, gap, EDGE_FADE_HEIGHT),
|
|
PANEL_BG, EDGE_FADE_CLEAR, EDGE_FADE_CLEAR, PANEL_BG)
|
|
rl.draw_rectangle_gradient_ex(rl.Rectangle(gap_x, body.y + body.height - EDGE_FADE_HEIGHT, gap, EDGE_FADE_HEIGHT),
|
|
EDGE_FADE_CLEAR, PANEL_BG, PANEL_BG, EDGE_FADE_CLEAR)
|
|
edge_x = body.x + body.width - FEATURE_FLICK_DISTANCE if pull_x < 0 else body.x
|
|
cx, cy = edge_x + FEATURE_FLICK_DISTANCE / 2, body.y + body.height / 2
|
|
direction = 1 if pull_x < 0 else -1
|
|
tip = rl.Vector2(cx + direction * 8, cy)
|
|
rl.draw_line_ex(rl.Vector2(cx - direction * 8, cy - 16), tip, 4, TEXT_PRIMARY)
|
|
rl.draw_line_ex(tip, rl.Vector2(cx - direction * 8, cy + 16), 4, TEXT_PRIMARY)
|
|
if pull_x:
|
|
rl.rl_push_matrix()
|
|
try:
|
|
if pull_x:
|
|
rl.rl_translatef(pull_x, 0, 0)
|
|
rl.draw_rectangle_rec(body, PANEL_BG)
|
|
for visible, row in enumerate(rows_state.rows[rows_state.scroll:rows_state.scroll + FEATURE_VISIBLE_ROWS]):
|
|
y = row_top + visible * FEATURE_ROW_HEIGHT
|
|
if draw_row is not None:
|
|
draw_row(row, y, left, pressed)
|
|
continue
|
|
value = _boolean_value(row)
|
|
heading = (bool(row.label) and not (row.key or row.page or row.value or row.reason or
|
|
row.choices or row.step or row.repair_value or row.actions))
|
|
if value is True and row.available:
|
|
bounds = rl.Rectangle(left + 28, y + 2, 2098 - left, FEATURE_ROW_HEIGHT - 8)
|
|
draw_rounded_fill(bounds, ACTIVE_ROW_BG, radius_px=12)
|
|
draw_rounded_stroke(bounds, ACTIVE_ROW_BORDER, radius_px=12)
|
|
rl.draw_line(left + 55, y + FEATURE_ROW_HEIGHT - 3, 2094, y + FEATURE_ROW_HEIGHT - 3, ROW_SEPARATOR)
|
|
if rows_state.page == "sounds:overview" or row.presets and not rows_state.editor:
|
|
self._value_summary(row, y, left)
|
|
continue
|
|
has_control = bool(row.actions or row.page or _confirmation(row) or row.available or value is not None)
|
|
width = (feature_action_left(row) - 24 if has_control else 2094) - (left + 55)
|
|
detail = value_text(row) if row.key == "LaneCenteringStrength" else row.value + (" " + row.unit if row.unit and row.value != "Auto" else "")
|
|
if value is not None or (row.available and not row.actions and not row.page and not _confirmation(row) and not row.repair_value):
|
|
detail = ""
|
|
if row.page and detail in ("Saved settings", "Saved preferences", "Saved profiles"):
|
|
detail = ""
|
|
reason = value_reason(row)
|
|
detail += (" - " if detail and reason else "") + reason
|
|
label = self._elide(row.label, FontRole.NORMAL, LABEL_SIZE, width)
|
|
detail = self._elide(detail, FontRole.NORMAL, DETAIL_SIZE, width)
|
|
label_top, label_bottom = self.fonts.vertical_ink(label, FontRole.NORMAL, LABEL_SIZE)
|
|
detail_top, detail_bottom = self.fonts.vertical_ink(detail, FontRole.NORMAL, DETAIL_SIZE) if detail else (0, 0)
|
|
height = label_bottom - label_top + (12 + detail_bottom - detail_top if detail else 0)
|
|
top = y + (FEATURE_ROW_HEIGHT - height) / 2
|
|
self.fonts.draw(label, FontRole.NORMAL, LABEL_SIZE, left + 55, top - label_top, TEXT_MUTED if heading else TEXT_PRIMARY)
|
|
if detail:
|
|
self.fonts.draw(detail, FontRole.NORMAL, DETAIL_SIZE, left + 55,
|
|
top + label_bottom - label_top + 12 - detail_top, TEXT_SECONDARY)
|
|
if heading:
|
|
continue
|
|
action = rl.Rectangle(FEATURE_CONTROL_LEFT, y + FEATURE_ACTION_MARGIN, FEATURE_CONTROL_RIGHT - FEATURE_CONTROL_LEFT,
|
|
FEATURE_ROW_HEIGHT - 2 * FEATURE_ACTION_MARGIN)
|
|
if row.actions:
|
|
for index, ((text, enabled), rect) in enumerate(zip(row.actions, feature_action_rects(row, y), strict=True)):
|
|
button = rl.Rectangle(*rect)
|
|
active = row.available and enabled and pressed == FeatureUiAction("action", row, index)
|
|
selected = row.key == "drive_state" and row.available and row.value == text
|
|
draw_rounded_fill(button, CONTROL_PRESSED_BG if active else rl.color_lerp(PANEL_BG, ACCENT, 0.22) if selected else CONTROL_BG)
|
|
draw_rounded_stroke(button, ACCENT if active or selected else DRIVE_MODE_BORDER if row.key == "drive_state" else CONTROL_BORDER)
|
|
color = TEXT_PRIMARY if row.available and (enabled or selected) else TEXT_MUTED
|
|
if row.key == "drive_state":
|
|
self.fonts.draw(text, FontRole.MEDIUM, 36, button.x + 22, button.y + 19, color)
|
|
description = ("Follows ignition", "Stops driving services", "Park & disengage")[index]
|
|
self.fonts.draw(description, FontRole.NORMAL, 24, button.x + 22, button.y + 79, TEXT_SECONDARY if color == TEXT_PRIMARY else TEXT_MUTED)
|
|
if selected:
|
|
rl.draw_line_ex(rl.Vector2(button.x + button.width - 47, button.y + 32),
|
|
rl.Vector2(button.x + button.width - 38, button.y + 41), 3, ACCENT)
|
|
rl.draw_line_ex(rl.Vector2(button.x + button.width - 38, button.y + 41),
|
|
rl.Vector2(button.x + button.width - 22, button.y + 22), 3, ACCENT)
|
|
else:
|
|
self._center(self._elide(text, FontRole.MEDIUM, DETAIL_SIZE, button.width - 24), DETAIL_SIZE, button, color)
|
|
elif row.page and value is None:
|
|
self._center("Open >", DETAIL_SIZE, action, TEXT_PRIMARY if row.available or row.choices == ("Off", "On") else TEXT_MUTED)
|
|
elif _confirmation(row):
|
|
if row.key in ("torque_adopt", "slc_adopt"):
|
|
action = "Adopt…"
|
|
elif row.key in ("torque_rebase", "torque_gain_rebase"):
|
|
action = "Review…"
|
|
elif row.key == "torque_prepare_firestar":
|
|
action = "Prepare…"
|
|
elif row.key.startswith("pip:format:"):
|
|
action = "Use…"
|
|
else:
|
|
action = "Restore…" if row.key.startswith("long_repair:") else "Reset…"
|
|
button = rl.Rectangle(FEATURE_CONTROL_LEFT, y + FEATURE_ACTION_MARGIN, FEATURE_CONTROL_RIGHT - FEATURE_CONTROL_LEFT,
|
|
FEATURE_ROW_HEIGHT - 2 * FEATURE_ACTION_MARGIN)
|
|
active = row.available and pressed == FeatureUiAction("reset", row)
|
|
draw_rounded_fill(button, CONTROL_PRESSED_BG if active else CONTROL_BG)
|
|
draw_rounded_stroke(button, ACCENT if active else CONTROL_BORDER)
|
|
self._center(action, DETAIL_SIZE, button, DANGER if row.available else TEXT_MUTED)
|
|
elif value is not None:
|
|
if row.page:
|
|
button = rl.Rectangle(feature_action_left(row), y + FEATURE_ACTION_MARGIN,
|
|
FEATURE_CONTROL_LEFT - 24 - feature_action_left(row), FEATURE_ROW_HEIGHT - 2 * FEATURE_ACTION_MARGIN)
|
|
active = pressed == FeatureUiAction("open", row)
|
|
draw_rounded_fill(button, CONTROL_PRESSED_BG if active else CONTROL_BG)
|
|
draw_rounded_stroke(button, ACCENT if active else CONTROL_BORDER)
|
|
self._center("Open >", DETAIL_SIZE, button)
|
|
action = rl.Rectangle(FEATURE_CONTROL_LEFT, y + 18, FEATURE_CONTROL_RIGHT - FEATURE_CONTROL_LEFT, FEATURE_ROW_HEIGHT - 36)
|
|
self._center("On" if value else "Off", LABEL_SIZE, rl.Rectangle(1760, action.y, 220, action.height),
|
|
TEXT_PRIMARY if row.available else TEXT_MUTED, role=FontRole.NORMAL)
|
|
draw_aether_toggle(rl.Rectangle(1993, y + FEATURE_ROW_HEIGHT / 2 - 30.5, 113, 61), value,
|
|
available=row.available, seed_id=f"{state.page}:{row.key}")
|
|
elif row.available:
|
|
if row.repair_value:
|
|
active = pressed == FeatureUiAction("change", row)
|
|
draw_rounded_fill(action, CONTROL_PRESSED_BG if active else CONTROL_BG)
|
|
draw_rounded_stroke(action, ACCENT if active else CONTROL_BORDER)
|
|
text = f"Set {row.repair_value}"
|
|
if self.fonts.measure(text, FontRole.MEDIUM, DETAIL_SIZE).width <= action.width - 24:
|
|
self._center(text, DETAIL_SIZE, action)
|
|
else:
|
|
self._center("Set", DETAIL_SIZE, rl.Rectangle(action.x, action.y, action.width, action.height / 2))
|
|
self._center(self._elide(row.repair_value, FontRole.MEDIUM, DETAIL_SIZE, action.width - 24), DETAIL_SIZE,
|
|
rl.Rectangle(action.x, action.y + action.height / 2, action.width, action.height / 2))
|
|
else:
|
|
display_value = (value_text(row) if row.key == "LaneCenteringStrength" else
|
|
row.value + (" " + row.unit if row.unit and row.value != "Auto" else ""))
|
|
self._center(self._elide(display_value, FontRole.NORMAL, LABEL_SIZE, action.width - 24), LABEL_SIZE,
|
|
rl.Rectangle(action.x, y + 12, action.width, FEATURE_BUTTON_TOP - 24), role=FontRole.NORMAL)
|
|
for x, text in ((1760, "-"), (1940, "+")):
|
|
button = rl.Rectangle(x, y + FEATURE_BUTTON_TOP, 155, FEATURE_BUTTON_HEIGHT)
|
|
active = pressed == FeatureUiAction("change", row, -1 if text == "-" else 1)
|
|
draw_rounded_fill(button, CONTROL_PRESSED_BG if active else CONTROL_BG)
|
|
draw_rounded_stroke(button, ACCENT if active else CONTROL_BORDER)
|
|
self._center(text, DETAIL_SIZE, button)
|
|
finally:
|
|
if pull_x:
|
|
rl.rl_pop_matrix()
|
|
if cover:
|
|
rl.draw_rectangle_rec(body, rl.Color(PANEL_BG.r, PANEL_BG.g, PANEL_BG.b, cover))
|
|
finally:
|
|
clip.end_scissor_mode()
|
|
if state.editor:
|
|
self._value_done(state, pressed)
|
|
return
|
|
can_page = (state.scroll > 0, state.scroll + FEATURE_VISIBLE_ROWS < len(state.rows))
|
|
for label, bounds, enabled in zip(("Previous", "Next"), feature_footer_buttons(state), can_page, strict=True):
|
|
button = rl.Rectangle(*bounds)
|
|
active = enabled and pressed == FeatureUiAction("scroll", direction=-1 if label == "Previous" else 1)
|
|
if enabled:
|
|
draw_rounded_fill(button, CONTROL_PRESSED_BG if active else CONTROL_BG)
|
|
draw_rounded_stroke(button, ACCENT if active else CONTROL_BORDER)
|
|
self._center(label, DETAIL_SIZE, button, TEXT_PRIMARY if enabled else TEXT_MUTED)
|
|
current_page = state.scroll // FEATURE_VISIBLE_ROWS + 1
|
|
total_pages = max(1, (len(state.rows) + FEATURE_VISIBLE_ROWS - 1) // FEATURE_VISIBLE_ROWS)
|
|
self._center(f"{current_page}/{total_pages}", DETAIL_SIZE,
|
|
rl.Rectangle(feature_page_counter_left(state), 980, FEATURE_PAGE_COUNTER_WIDTH, 70), TEXT_SECONDARY)
|
|
|
|
def _value_summary(self, row, y, left) -> None:
|
|
value = value_text(row)
|
|
value = self._elide(value, FontRole.SEMI_BOLD, DETAIL_SIZE, (2094 - left - 55) / 2)
|
|
value_width = self.fonts.measure(value, FontRole.SEMI_BOLD, DETAIL_SIZE).width
|
|
label_width = 2050 - value_width - 48 - left - 55
|
|
label = self._elide(row.label, FontRole.NORMAL, LABEL_SIZE, label_width)
|
|
reason = self._elide(row.reason, FontRole.NORMAL, DETAIL_SIZE, label_width) if not row.available else ""
|
|
top, bottom = self.fonts.vertical_ink(label, FontRole.NORMAL, LABEL_SIZE)
|
|
label_y = y + (FEATURE_ROW_HEIGHT - top - bottom) / 2
|
|
if reason:
|
|
detail_top, detail_bottom = self.fonts.vertical_ink(reason, FontRole.NORMAL, DETAIL_SIZE)
|
|
height = bottom - top + 12 + detail_bottom - detail_top
|
|
label_y = y + (FEATURE_ROW_HEIGHT - height) / 2 - top
|
|
self.fonts.draw(reason, FontRole.NORMAL, DETAIL_SIZE, left + 55, label_y + bottom + 12 - detail_top, TEXT_MUTED)
|
|
self.fonts.draw(label, FontRole.NORMAL, LABEL_SIZE, left + 55, label_y, TEXT_PRIMARY if row.available else TEXT_MUTED)
|
|
for text, role, size, x, color in ((value, FontRole.SEMI_BOLD, DETAIL_SIZE, 2050 - value_width, TEXT_SECONDARY if row.available else TEXT_MUTED),
|
|
(">", FontRole.MEDIUM, DETAIL_SIZE, 2070, TEXT_SECONDARY if row.available else TEXT_MUTED)):
|
|
top, bottom = self.fonts.vertical_ink(text, role, size)
|
|
self.fonts.draw(text, role, size, x, y + (FEATURE_ROW_HEIGHT - top - bottom) / 2, color)
|
|
|
|
def _value_done(self, state, pressed) -> None:
|
|
button = rl.Rectangle(*value_done_rect(state))
|
|
active = pressed == FeatureUiAction("back")
|
|
draw_rounded_fill(button, CONTROL_PRESSED_BG if active else VALUE_DONE_BG, radius_px=32)
|
|
draw_rounded_stroke(button, ACCENT if active else VALUE_SELECTED_BORDER, radius_px=32)
|
|
self._center("Done", 44, button)
|
|
|
|
def _value_row(self, state, row, editor, buttons, pressed) -> None:
|
|
value = value_text(row)
|
|
self._center("Saved level" if row.unit == "%" else "Saved value", DETAIL_SIZE, rl.Rectangle(editor.x, editor.y, editor.width, 52),
|
|
TEXT_SECONDARY, role=FontRole.NORMAL)
|
|
value_width = editor.width - 464
|
|
value_size = min(140, 140 * value_width / max(value_width, self.fonts.measure(value, FontRole.SEMI_BOLD, 140).width))
|
|
self._center(value, value_size, rl.Rectangle(editor.x + 232, buttons[0][1][1], value_width, buttons[0][1][3]),
|
|
ACCENT if row.available else TEXT_MUTED, role=FontRole.SEMI_BOLD)
|
|
self._center(state.save_hint if row.available else "Editing unavailable", 32,
|
|
rl.Rectangle(editor.x, editor.y + 580, editor.width, 44), TEXT_SECONDARY, role=FontRole.NORMAL)
|
|
for index, (value, bounds, enabled) in enumerate(buttons):
|
|
button = rl.Rectangle(*bounds)
|
|
selected = value == row.value
|
|
step = value in ("-", "+")
|
|
text = value if step else dict(row.presets)[value]
|
|
target = FeatureUiAction("change", row, -1 if value == "-" else 1) if step else FeatureUiAction("action", row, index)
|
|
active = enabled and pressed == target
|
|
background = ACTIVE_ROW_BG if selected and row.available else CONTROL_BG
|
|
border = VALUE_SELECTED_BORDER if selected and row.available else CONTROL_BORDER
|
|
draw_rounded_fill(button, CONTROL_PRESSED_BG if active else background, radius_px=32)
|
|
draw_rounded_stroke(button, ACCENT if active else border, radius_px=32)
|
|
size = 64 if step else min(44, 44 * (button.width - 24) / max(button.width - 24, self.fonts.measure(text, FontRole.MEDIUM, 44).width))
|
|
self._center(text, size, button,
|
|
TEXT_PRIMARY if row.available and (enabled or selected) else TEXT_MUTED)
|
|
|
|
def _center(self, text: str, size: float, rect: rl.Rectangle, color: rl.Color = TEXT_PRIMARY, *, role: FontRole = FontRole.MEDIUM) -> None:
|
|
top, bottom = self.fonts.vertical_ink(text, role, size)
|
|
self.fonts.draw(text, role, size, rect.x + (rect.width - self.fonts.measure(text, role, size).width) / 2,
|
|
rect.y + rect.height / 2 - (top + bottom) / 2, color)
|
|
|
|
def _elide(self, text: str, role: FontRole, size: float, width: float) -> str:
|
|
return elide_text(self.fonts, text, role, size, width)
|