Files
StarPilot/openpilot/starpilot/ui/onroad_customization.py
T
2026-10-13 23:07:13 -05:00

499 lines
25 KiB
Python

"""Validated onroad colors and independent logical-pixel layouts."""
import copy
import json
import math
import re
from functools import lru_cache
from typing import Any, NotRequired, TypedDict
from openpilot.starpilot.saved_source import read_saved
from openpilot.starpilot.ui.onroad_torque_geometry import maximum_footprint
from openpilot.starpilot.ui.large_speed_geometry import WIDTH, CARD_HEIGHT, ACTION_HEIGHT, ACTION_X, ACTION_Y
PARAM_KEY = "OnroadCustomizations"
MAX_BYTES = 16384
PALETTE = {"cardFill": "#000000A6", "cardBorder": "#C4CDD0B4", "text": "#FFFFFFFF"}
ROAD_COLORS = {"path": "#30FF9CFF", "pathEdge": "#00FF40FF", "laneLines": "#FFFFFFFF"}
PATH_MODES = ("default", "color", "rainbow", "acceleration")
WHEEL_SIZES = {"large": (144, 192, 240), "compact": (40, 50, 70)}
class Bounds(TypedDict):
x: float
y: float
width: float
height: float
class WidgetPlacement(TypedDict):
x: float
y: float
enabled: bool
class Widget(TypedDict):
label: str
kind: str
width: float
height: float
default: WidgetPlacement
bounds: NotRequired[Bounds]
visualInsetTop: NotRequired[float]
layer: NotRequired[str]
note: NotRequired[str]
defaultAnchor: NotRequired[str]
colors: NotRequired[dict[str, str | None]]
resizable: NotRequired[dict[str, int]]
class LayoutProfile(TypedDict):
label: str
width: float
height: float
bounds: Bounds
widgets: dict[str, Widget]
reservedZones: list[Bounds]
protectedWidget: NotRequired[str]
inputZones: NotRequired[list[dict[str, Any]]]
inputZonePriority: NotRequired[str]
widgetOrder: NotRequired[list[str]]
runtimePreviewWidgets: NotRequired[list[str]]
def _widget(label: str, kind: str, width: float, height: float, x: float, y: float) -> Widget:
return {"label": label, "kind": kind, "width": width, "height": height,
"default": {"x": x, "y": y, "enabled": True}}
PROFILES: dict[str, LayoutProfile] = {
"large": {"label": "Large UI", "width": 2160, "height": 1080,
"bounds": {"x": 30, "y": 30, "width": 1800, "height": 1020},
"reservedZones": [],
"widgets": {
"current_speed": _widget("Current speed", "current_speed", 580, 300, 640, 30),
"cruise_limits": _widget("Cruise and speed limit", "cruise_limits", WIDTH, CARD_HEIGHT + 52, 88, 75),
"speed_limit_actions": _widget("Speed limit actions", "speed_limit_actions", WIDTH, ACTION_HEIGHT, ACTION_X, ACTION_Y),
"steering_wheel": _widget("Steering wheel", "steering_wheel", 192, 192, 1588, 75),
"driver_monitor": _widget("Driver monitoring", "driver_monitor", 192, 192, 88, 808)}},
"compact": {"label": "Small UI", "width": 536, "height": 240,
"bounds": {"x": 0, "y": 0, "width": 476, "height": 240},
"protectedWidget": "speed_limit_actions",
"reservedZones": [],
"widgets": {
"max_speed": _widget("Maximum speed", "max_speed", 162, 162, 0, 0),
"speed_limit": _widget("Speed limit", "speed_limit", 118, 132, 330, 20),
"speed_limit_actions": _widget("Speed limit actions", "speed_limit_actions", 282, 54, 174, 180),
"steering_wheel": _widget("Steering wheel", "steering_wheel", 50, 50, 21, 176),
"driver_monitor": _widget("Driver monitoring", "driver_monitor", 60, 60, 16, 10)}}}
# Coordinates remain anchored at the buttons, preserving saved v4 placements.
PROFILES["compact"]["widgets"]["speed_limit_actions"]["visualInsetTop"] = 32
LEGACY_WIDGETS = {"large": {"current_speed", "cruise_limits", "steering_wheel"},
"compact": {"max_speed", "speed_limit", "steering_wheel"}}
DM_WIDGETS = {profile: keys | {"driver_monitor"} for profile, keys in LEGACY_WIDGETS.items()}
for _profile, _data in PROFILES.items():
_bounds = _data["bounds"]
_x, _y, _width, _height = maximum_footprint(_bounds["x"], _bounds["y"], _bounds["width"], _bounds["height"], _data["width"])
_data["widgets"]["torque_bar"] = {
**_widget("Torque bar", "torque_bar", _width, _height, _x, _y), "layer": "underlay",
"note": "Torque output, or steering-effort estimate on angle-controlled vehicles"}
ACTIONLESS_WIDGETS = {profile: set(data["widgets"]) - {"speed_limit_actions"} for profile, data in PROFILES.items()}
RAILLESS_WIDGETS = {profile: set(data["widgets"]) for profile, data in PROFILES.items()}
RAIL_WIDGETS = ("model_confidence", "conditional_mode", "following_distance")
for _index, (_key, _label) in enumerate(zip(RAIL_WIDGETS, ("Model confidence", "CEM and driving mode", "Following distance"), strict=True)):
PROFILES["compact"]["widgets"][_key] = {
**_widget(_label, _key, 60, 80, 476, _index * 80),
"bounds": {"x": 0, "y": 0, "width": 536, "height": 240},
}
# Keep layouts saved before camera widgets valid when the registry grows.
CAMERALESS_WIDGETS = {profile: set(data["widgets"]) for profile, data in PROFILES.items()}
CAMERA_WIDGETS = ("pip_left", "pip_right")
for _profile, (_size, _margin) in {"large": (600, 24), "compact": (80, 16)}.items():
_bounds = PROFILES[_profile]["bounds"]
for _side, _key in zip(("left", "right"), CAMERA_WIDGETS, strict=True):
_x = _bounds["x"] + (_margin if _side == "left" else _bounds["width"] - _margin - _size)
_y = _bounds["y"] + _bounds["height"] - _margin - _size
PROFILES[_profile]["widgets"][_key] = {
**_widget(f"PiP {_side} side camera", "pip_camera", _size, _size, _x, _y), "layer": "underlay"}
MODE_WIDGET = "driving_mode_descriptions"
for _profile, _geometry in {"large": (580, 68, 640, 344), "compact": (168, 60, 162, 22)}.items():
_mode_widget = _widget("Driving Mode Descriptions", MODE_WIDGET, *_geometry)
_mode_widget["default"]["enabled"] = False
PROFILES[_profile]["widgets"][MODE_WIDGET] = _mode_widget
# It starts disabled so an upgrade never adds a new onroad element without the
# driver's choice; validation below adds the placement to older documents.
CLOCK_WIDGET = "clock"
for _profile, _geometry in {"large": (300, 72, 780, 430), "compact": (140, 44, 168, 92)}.items():
_clock_widget = _widget("Clock", CLOCK_WIDGET, *_geometry)
_clock_widget["default"]["enabled"] = False
PROFILES[_profile]["widgets"][CLOCK_WIDGET] = _clock_widget
# Small-UI only; the box matches the old fixed, centered position.
STOPPED_TIMER_WIDGET = "stopped_timer"
_timer_widget = _widget("Stopped timer", STOPPED_TIMER_WIDGET, 440, 112, 18, 48)
_timer_widget["default"]["enabled"] = False
_timer_widget["note"] = "Counts how long you have been stopped"
PROFILES["compact"]["widgets"][STOPPED_TIMER_WIDGET] = _timer_widget
# Restore the Dom road graphic in the gap between the speed card and main speed.
# It has no background and appears only with a current selected source.
CEM_GAUGE_WIDGET = 'cem_gauge'
PROFILES['large']['widgets'][CEM_GAUGE_WIDGET] = _widget('CEM road gauge', CEM_GAUGE_WIDGET, 344, 300, 436, 75)
# Older documents retain every saved placement and gain this independent widget.
OPTIONAL_WIDGETS = frozenset({MODE_WIDGET, CLOCK_WIDGET, STOPPED_TIMER_WIDGET, CEM_GAUGE_WIDGET})
# These editor sprites use the runtime components and their canonical assets/fonts.
RUNTIME_PREVIEW_WIDGETS = {
"large": ("current_speed", "cruise_limits", CEM_GAUGE_WIDGET, "steering_wheel", "driver_monitor"),
"compact": ("max_speed", "steering_wheel", "conditional_mode", "following_distance", "model_confidence", "driver_monitor"),
}
_FRAME_COLORS = {"cardFill": "#00000000", "cardBorder": "#00000000"}
_ACTION_COLORS = {"cardFill": "#0C1820EB", "cardBorder": "#A6DFBEFF", "text": "#FFFFFFFF"}
WIDGET_COLORS = {
"large": {
MODE_WIDGET: {"text": "#FFFFFFFF"},
CLOCK_WIDGET: {"text": "#FFFFFFFF"},
CEM_GAUGE_WIDGET: {},
"current_speed": {"text": None},
"cruise_limits": {"cardFill": "#10191FFF", "cardBorder": "#35434DFF", "text": None},
"speed_limit_actions": {"cardFill": "#10191FFF", "cardBorder": "#35434DFF", "text": "#FFFFFFFF"},
"steering_wheel": {"cardFill": None, "cardBorder": "#00000000"},
"driver_monitor": _FRAME_COLORS,
},
"compact": {
MODE_WIDGET: {"text": "#FFFFFFFF"},
CLOCK_WIDGET: {"text": "#FFFFFFFF"},
"max_speed": {"text": None},
"speed_limit_actions": _ACTION_COLORS,
"driver_monitor": _FRAME_COLORS,
"model_confidence": _FRAME_COLORS,
"conditional_mode": _FRAME_COLORS,
"following_distance": {**_FRAME_COLORS, "text": "#FFFFFFFF"},
},
}
# Back-to-front order used when a layout first opts into custom layers.
DEFAULT_WIDGET_ORDER = {
"large": ("pip_left", "pip_right", "cruise_limits", "speed_limit_actions", CEM_GAUGE_WIDGET, "current_speed",
"steering_wheel", "torque_bar", "driver_monitor", MODE_WIDGET, CLOCK_WIDGET),
"compact": ("pip_left", "pip_right", "speed_limit", "max_speed", "steering_wheel", "torque_bar",
"speed_limit_actions", "driver_monitor", *RAIL_WIDGETS, MODE_WIDGET, CLOCK_WIDGET, STOPPED_TIMER_WIDGET),
}
def validate_widget_order(order, widgets):
if (type(order) is not list or any(type(key) is not str for key in order) or
len(order) != len(widgets) or set(order) != set(widgets)):
raise ValueError("Invalid widget layer order")
return list(order)
def widget_order(document, profile):
return document.get("widgetOrder", {}).get(str(profile), DEFAULT_WIDGET_ORDER[str(profile)])
def widget_bounds(profile, widget):
return PROFILES[profile]["widgets"][widget].get("bounds", PROFILES[profile]["bounds"])
def customization_metadata():
profiles = copy.deepcopy(PROFILES)
profiles["large"]["widgets"]["driver_monitor"]["defaultAnchor"] = "driver_side"
profiles["large"]["inputZones"] = [{"id": "favorites_drawer", "label": "Quick Select drawer",
"x": 30, "y": 900, "width": 150, "height": 150}]
profiles["compact"]["inputZones"] = [
{"id": f"favorite_{index + 1}", "label": f"Quick Select {index + 1}",
"x": index * 476 / 3, "y": 0, "width": 476 / 3, "height": 240} for index in range(3)]
for profile in profiles.values():
profile["inputZonePriority"] = "Enabled Quick Select slots; speed-limit controls and steering wheel take priority"
for name, profile in profiles.items():
for key, widget in profile["widgets"].items():
widget["colors"] = dict(WIDGET_COLORS[name].get(key, {}))
profile["widgets"]["steering_wheel"]["resizable"] = {
"min": WHEEL_SIZES[name][0], "default": WHEEL_SIZES[name][1], "max": WHEEL_SIZES[name][2]}
for profile, data in profiles.items():
data["widgetOrder"] = list(DEFAULT_WIDGET_ORDER[profile])
data["runtimePreviewWidgets"] = list(RUNTIME_PREVIEW_WIDGETS[profile])
return {"profiles": profiles, "roadColorFields": [
{"id": key, "label": label, "default": ROAD_COLORS[key]}
for key, label in (("path", "Path"), ("pathEdge", "Path edges"), ("laneLines", "Outer lane lines"))], "paletteFields": [
{"id": key, "label": label, "default": PALETTE[key]}
for key, label in (("cardFill", "Card fill"), ("cardBorder", "Card border"), ("text", "Text"))]}
def default_document():
return {"version": 6, "clock24Hour": False, "palette": dict(PALETTE), "roadColors": {profile: {} for profile in PROFILES},
"widgetColors": {profile: {} for profile in PROFILES}, "layouts": {
profile: {key: {**widget["default"], **({"size": WHEEL_SIZES[profile][1]} if key == "steering_wheel" else {})}
for key, widget in data["widgets"].items()}
for profile, data in PROFILES.items()}}
def validate_document(value, *, _legacy_geometry=False):
if type(value) is not dict or type(value.get("version")) is not int or value["version"] not in (1, 2, 3, 4, 5, 6):
raise ValueError("Invalid customization version")
if value['version'] < 6 and not _legacy_geometry:
result = validate_document(value, _legacy_geometry=True)
if value['version'] < 5 and value['palette'] != PALETTE:
colors = result['widgetColors']['large'].setdefault('cruise_limits', {})
for key in ('cardFill', 'cardBorder'):
colors.setdefault(key, value['palette'][key])
layouts = result['layouts']['large']
card, actions = layouts['cruise_limits'], layouts['speed_limit_actions']
# Follow the card only when actions kept their old relative placement.
if actions['x'] == card['x'] and actions['y'] - card['y'] == (425 if value['version'] < 5 else 372):
actions['y'] = card['y'] + CARD_HEIGHT + 22
for key in ('cruise_limits', 'speed_limit_actions'):
widget, saved = PROFILES['large']['widgets'][key], layouts[key]
bounds = widget_bounds('large', key)
for axis, extent in (('x', 'width'), ('y', 'height')):
saved[axis] = min(saved[axis], bounds[axis] + bounds[extent] - widget[extent])
return validate_document(result)
fields = {"version", "palette", "layouts"} | ({"widgetColors"} if value["version"] >= 2 else set())
if value["version"] >= 3:
fields.add("roadColors")
if value['version'] >= 4 and 'speedSources' in value:
fields.add('speedSources')
if type(value['speedSources']) is not bool:
raise ValueError('Invalid speed source drawer preference')
if value['version'] >= 4 and 'widgetOrder' in value:
fields.add('widgetOrder')
if 'clock24Hour' in value:
fields.add('clock24Hour')
if type(value['clock24Hour']) is not bool:
raise ValueError('Invalid clock format preference')
if 'showMaxSpeed' in value:
fields.add('showMaxSpeed')
if type(value['showMaxSpeed']) is not bool:
raise ValueError('Invalid MAX speed preference')
if set(value) != fields:
raise ValueError("Invalid customization fields")
palette, layouts = value["palette"], value["layouts"]
if type(palette) is not dict or set(palette) != set(PALETTE) or type(layouts) is not dict or set(layouts) != set(PROFILES):
raise ValueError("Invalid customization fields")
if any(all(type(layouts[profile]) is dict and
(set(layouts[profile]) - {"vasm", *OPTIONAL_WIDGETS, *CAMERA_WIDGETS}) == keys
for profile, keys in shape.items())
for shape in (LEGACY_WIDGETS, DM_WIDGETS, ACTIONLESS_WIDGETS)):
layouts = copy.deepcopy(layouts)
cruise = layouts["large"]["cruise_limits"]
if (type(cruise) is not dict or set(cruise) != {"x", "y", "enabled"} or type(cruise["enabled"]) is not bool or
any(type(cruise[axis]) not in (int, float) for axis in ("x", "y"))):
raise ValueError("Invalid widget placement")
try:
if not all(math.isfinite(cruise[axis]) for axis in ("x", "y")):
raise ValueError("Invalid widget placement")
except OverflowError as exc:
raise ValueError("Invalid widget placement") from exc
cruise_default = PROFILES["large"]["widgets"]["cruise_limits"]["default"]
actions_default = ({'x': 88, 'y': 500} if _legacy_geometry else
PROFILES["large"]["widgets"]["speed_limit_actions"]["default"])
layouts["large"]["speed_limit_actions"] = {
"x": actions_default["x"] + cruise["x"] - cruise_default["x"],
"y": actions_default["y"] + cruise["y"] - cruise_default["y"],
"enabled": cruise["enabled"],
}
for profile in PROFILES:
for key in ("driver_monitor", "torque_bar", "speed_limit_actions"):
layouts[profile].setdefault(key, dict(PROFILES[profile]["widgets"][key]["default"]))
if all(type(layouts[profile]) is dict and (set(layouts[profile]) - {"vasm", *OPTIONAL_WIDGETS, *CAMERA_WIDGETS}) == keys
for profile, keys in RAILLESS_WIDGETS.items()):
layouts = copy.deepcopy(layouts)
for key in RAIL_WIDGETS:
layouts["compact"][key] = dict(PROFILES["compact"]["widgets"][key]["default"])
if all(type(layouts[profile]) is dict and (set(layouts[profile]) - {"vasm", *OPTIONAL_WIDGETS}) == keys
for profile, keys in CAMERALESS_WIDGETS.items()):
layouts = copy.deepcopy(layouts)
for profile in PROFILES:
has_legacy = "vasm" in layouts[profile]
legacy = layouts[profile].pop("vasm", None)
if has_legacy and (type(legacy) is not dict or set(legacy) != {"x", "y", "enabled"} or
type(legacy["enabled"]) is not bool or
any(type(legacy[axis]) not in (int, float) or not math.isfinite(legacy[axis]) for axis in ("x", "y"))):
raise ValueError("Invalid legacy camera placement")
for side, key in enumerate(CAMERA_WIDGETS):
widget = PROFILES[profile]["widgets"][key]
position = dict(widget["default"])
if legacy is not None:
bounds = widget_bounds(profile, key)
position.update(enabled=legacy["enabled"],
x=max(bounds["x"], min(legacy["x"], bounds["x"] + bounds["width"] - 2 * widget["width"])) + side * widget["width"],
y=max(bounds["y"], min(legacy["y"], bounds["y"] + bounds["height"] - widget["height"])))
layouts[profile][key] = position
layouts = copy.deepcopy(layouts)
for profile, data in PROFILES.items():
if type(layouts[profile]) is dict:
missing = set(data["widgets"]) - set(layouts[profile])
if missing and missing <= OPTIONAL_WIDGETS:
for optional in missing:
layouts[profile][optional] = dict(data["widgets"][optional]["default"])
result = default_document()
for key, color in palette.items():
if type(color) is not str or re.fullmatch(r"#[0-9a-fA-F]{8}", color) is None:
raise ValueError("Colors must be #RRGGBBAA")
result["palette"][key] = color.upper()
if value["version"] >= 2:
colors = value["widgetColors"]
if type(colors) is not dict or set(colors) != set(PROFILES):
raise ValueError("Invalid widget color profiles")
for profile, widgets in colors.items():
if type(widgets) is not dict or not widgets.keys() <= WIDGET_COLORS[profile].keys():
raise ValueError("Invalid widget color IDs")
for key, overrides in widgets.items():
if type(overrides) is not dict or not overrides.keys() <= WIDGET_COLORS[profile][key].keys():
raise ValueError("Invalid widget color fields")
if any(type(color) is not str or re.fullmatch(r"#[0-9a-fA-F]{8}", color) is None for color in overrides.values()):
raise ValueError("Colors must be #RRGGBBAA")
if overrides:
result["widgetColors"][profile][key] = {field: color.upper() for field, color in overrides.items()}
if value["version"] >= 3:
colors = value["roadColors"]
if type(colors) is not dict or set(colors) != set(PROFILES):
raise ValueError("Invalid road color profiles")
for profile, overrides in colors.items():
if type(overrides) is not dict or not overrides.keys() <= ROAD_COLORS.keys() | {"pathMode"}:
raise ValueError("Invalid road color fields")
for key, color in overrides.items():
if key == "pathMode":
if type(color) is not str or color not in PATH_MODES:
raise ValueError("Invalid path color mode")
elif type(color) is not str or re.fullmatch(r"#[0-9a-fA-F]{8}", color) is None:
raise ValueError("Colors must be #RRGGBBAA")
result["roadColors"][profile][key] = color if key == "pathMode" else color.upper()
for profile, data in PROFILES.items():
layout = layouts[profile]
if type(layout) is not dict or set(layout) != set(data["widgets"]):
raise ValueError("Invalid widget IDs")
for key, widget in data["widgets"].items():
placement = layout[key]
bounds = widget_bounds(profile, key)
expected = {"x", "y", "enabled"} | ({"size"} if key == "steering_wheel" and value["version"] >= 4 else set())
if type(placement) is not dict or set(placement) != expected or type(placement["enabled"]) is not bool:
raise ValueError("Invalid widget placement")
size = WHEEL_SIZES[profile][1]
if key == "steering_wheel" and value["version"] >= 4:
size = placement["size"]
minimum, _, maximum = WHEEL_SIZES[profile]
if type(size) is not int or not minimum <= size <= maximum:
raise ValueError("Invalid steering wheel size")
for axis, extent in (("x", "width"), ("y", "height")):
number = placement[axis]
legacy = ({'cruise_limits': (176, 483), 'speed_limit_actions': (176, 58)} if value['version'] < 5 else
{'cruise_limits': (280, 412), 'speed_limit_actions': (280, 96)})
dimensions = legacy.get(key) if _legacy_geometry and profile == 'large' else None
dimension = (dimensions[0 if axis == 'x' else 1] if dimensions else
size if key == 'steering_wheel' else widget[extent])
if type(number) not in (int, float) or not math.isfinite(number) or not bounds[axis] <= number <= bounds[axis] + bounds[extent] - dimension:
raise ValueError("Widget outside profile bounds")
result["layouts"][profile][key] = {**placement, **({"size": size} if key == "steering_wheel" else {})}
if profile == "compact":
actions = result["layouts"][profile]["speed_limit_actions"]
area = data["widgets"]["speed_limit_actions"]
for key, widget in data["widgets"].items():
if key in ("speed_limit_actions", "speed_limit") or widget.get("layer") == "underlay":
continue
placement = result["layouts"][profile][key]
if key in OPTIONAL_WIDGETS and not placement["enabled"]:
continue
width, height = ((placement["size"],) * 2 if key == "steering_wheel" else
(widget["width"], widget["height"]))
if (placement["x"] < actions["x"] + area["width"] and placement["x"] + width > actions["x"] and
placement["y"] < actions["y"] + area["height"] and placement["y"] + height > actions["y"]):
raise ValueError("Widget overlaps protected speed limit actions")
if 'widgetOrder' in value:
orders = value['widgetOrder']
if type(orders) is not dict or not orders.keys() <= PROFILES.keys():
raise ValueError("Invalid widget layer profiles")
result['widgetOrder'] = {}
for profile, order in orders.items():
if type(order) is list:
order = [*order, *(key for key in PROFILES[profile]['widgets'] if key in OPTIONAL_WIDGETS and key not in order)]
result['widgetOrder'][profile] = validate_widget_order(order, PROFILES[profile]['widgets'])
if 'speedSources' in value:
result['speedSources'] = value['speedSources']
if 'clock24Hour' in value:
result['clock24Hour'] = value['clock24Hour']
if 'showMaxSpeed' in value:
result['showMaxSpeed'] = value['showMaxSpeed']
if len(json.dumps(result, separators=(",", ":")).encode()) > MAX_BYTES:
raise ValueError("Customization too large")
return result
def _unique_object(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError("Duplicate customization field")
result[key] = value
return result
def decode_document(raw):
return validate_document(json.loads(raw, object_pairs_hook=_unique_object))
def read_customization(params):
try:
raw, readable = read_saved(params, PARAM_KEY, MAX_BYTES)
return decode_document(raw) if readable and raw is not None else default_document()
except (AttributeError, OSError, TypeError, ValueError, UnicodeError, RecursionError):
return default_document()
def placement(document, profile, widget):
return document["layouts"][str(profile)][widget]
def widget_size(document, profile, widget):
default = PROFILES[str(profile)]["widgets"][widget]
position = placement(document, profile, widget)
return (position.get("size", WHEEL_SIZES[str(profile)][1]),) * 2 if widget == "steering_wheel" else (default["width"], default["height"])
def offset(document, profile, widget):
position = placement(document, profile, widget)
default = PROFILES[str(profile)]["widgets"][widget]["default"]
return position["x"] - default["x"], position["y"] - default["y"]
def widget_palette(document, profile, widget):
defaults = WIDGET_COLORS[str(profile)].get(widget, {})
colors = {key: document["palette"][key] if value is None else value for key, value in defaults.items()}
colors.update(document.get("widgetColors", {}).get(str(profile), {}).get(widget, {}))
return colors
def rgba(document, key, profile=None, widget=None):
if profile is None:
color = document["palette"][key]
else:
assert widget is not None
default = WIDGET_COLORS[str(profile)][widget][key]
color = document.get("widgetColors", {}).get(str(profile), {}).get(widget, {}).get(key, default or document["palette"][key])
return _rgba_value(color)
@lru_cache(maxsize=256)
def _rgba_value(color):
return tuple(int(color[index:index + 2], 16) for index in (1, 3, 5, 7))
def set_speed_sources(params, opened):
"""Change only a valid saved document; display fallback is not write authority."""
if type(opened) is not bool:
raise ValueError('Invalid speed source drawer preference')
raw, readable = read_saved(params, PARAM_KEY, MAX_BYTES)
if not readable:
raise ValueError('Saved layout is unreadable')
document = decode_document(raw) if raw is not None else default_document()
document['speedSources'] = opened
params.put(PARAM_KEY, json.dumps(validate_document(document), separators=(',', ':')))