Files
StarPilot/selfdrive/ui/lib/starpilot_theme.py
T
firestar5683 fd1ea78a62 theme
2026-04-21 16:14:02 -05:00

158 lines
4.6 KiB
Python

from __future__ import annotations
import json
from pathlib import Path
import pyray as rl
from openpilot.common.basedir import BASEDIR
ACTIVE_THEME_COLORS_PATH = Path(BASEDIR) / "starpilot/assets/active_theme/colors/colors.json"
STOCK_THEME_COLORS_PATH = Path(BASEDIR) / "starpilot/assets/stock_theme/colors/colors.json"
_FALLBACK_THEME_COLORS = {
"LaneLines": (255, 255, 255, 178),
"LeadMarker": (201, 34, 49, 255),
"Path": (48, 255, 156, 255),
"PathEdge": (38, 209, 125, 255),
"Sidebar1": (255, 255, 255, 178),
"Sidebar2": (255, 255, 255, 255),
"Sidebar3": (255, 255, 255, 255),
}
_THEME_COLOR_CACHE: dict[str, object] = {
"stamp": None,
"colors": None,
}
def _as_color(value: object, fallback: tuple[int, int, int, int] = (255, 255, 255, 255)) -> rl.Color:
if all(hasattr(value, channel) for channel in ("r", "g", "b", "a")):
return rl.Color(
_clamp_channel(getattr(value, "r"), fallback[0]),
_clamp_channel(getattr(value, "g"), fallback[1]),
_clamp_channel(getattr(value, "b"), fallback[2]),
_clamp_channel(getattr(value, "a"), fallback[3]),
)
if isinstance(value, (tuple, list)):
items = list(value)
if len(items) == 3:
items.append(fallback[3])
if len(items) >= 4:
return rl.Color(
_clamp_channel(items[0], fallback[0]),
_clamp_channel(items[1], fallback[1]),
_clamp_channel(items[2], fallback[2]),
_clamp_channel(items[3], fallback[3]),
)
return rl.Color(*fallback)
def _file_stamp(path: Path) -> tuple[str, int | None, int | None]:
try:
stat = path.stat()
return (str(path), stat.st_mtime_ns, stat.st_size)
except OSError:
return (str(path), None, None)
def _clamp_channel(value: object, fallback: int) -> int:
try:
return max(0, min(255, int(value)))
except (TypeError, ValueError):
return fallback
def _load_color_map(path: Path) -> dict[str, dict[str, int]]:
try:
with path.open() as f:
data = json.load(f)
if isinstance(data, dict):
return data
except (OSError, ValueError, TypeError):
pass
return {}
def _build_color(entry: object, fallback: tuple[int, int, int, int]) -> rl.Color:
if isinstance(entry, dict):
red, green, blue, alpha = fallback
return rl.Color(
_clamp_channel(entry.get("red"), red),
_clamp_channel(entry.get("green"), green),
_clamp_channel(entry.get("blue"), blue),
_clamp_channel(entry.get("alpha"), alpha),
)
return rl.Color(*fallback)
def _load_theme_colors() -> dict[str, rl.Color]:
stamp = (_file_stamp(STOCK_THEME_COLORS_PATH), _file_stamp(ACTIVE_THEME_COLORS_PATH))
if stamp == _THEME_COLOR_CACHE["stamp"] and _THEME_COLOR_CACHE["colors"] is not None:
return _THEME_COLOR_CACHE["colors"] # type: ignore[return-value]
stock_colors = _load_color_map(STOCK_THEME_COLORS_PATH)
active_colors = _load_color_map(ACTIVE_THEME_COLORS_PATH)
colors = {}
for key, fallback in _FALLBACK_THEME_COLORS.items():
colors[key] = _build_color(active_colors.get(key, stock_colors.get(key)), fallback)
_THEME_COLOR_CACHE["stamp"] = stamp
_THEME_COLOR_CACHE["colors"] = colors
return colors
def get_theme_color(key: str, fallback: rl.Color | None = None) -> rl.Color:
color = _load_theme_colors().get(key)
if color is not None:
return _as_color(color)
return _as_color(fallback if fallback is not None else rl.WHITE)
def get_param_color(params, key: str, fallback_alpha: int = 255) -> rl.Color | None:
value = params.get(key, encoding="utf-8") if params is not None else None
if not value:
return None
color = value.strip()
if not color or color.lower() == "stock":
return None
if color.startswith("#"):
color = color[1:]
if len(color) not in (6, 8):
return None
try:
red = int(color[0:2], 16)
green = int(color[2:4], 16)
blue = int(color[4:6], 16)
alpha = int(color[6:8], 16) if len(color) == 8 else fallback_alpha
except ValueError:
return None
return rl.Color(red, green, blue, alpha)
def is_stock_color_scheme(params) -> bool:
if params is None:
return True
scheme = params.get("ColorScheme", encoding="utf-8", default="stock")
return (scheme or "stock").lower() == "stock"
def get_visual_color(params, param_key: str, theme_key: str, fallback: rl.Color | None = None) -> rl.Color:
base = _as_color(fallback if fallback is not None else rl.WHITE)
override = get_param_color(params, param_key, base.a)
if override is not None:
return override
return get_theme_color(theme_key, base)
def with_alpha(color: rl.Color, alpha: int) -> rl.Color:
color = _as_color(color)
return rl.Color(color.r, color.g, color.b, max(0, min(255, int(alpha))))