refactor(colors): consolidate theme helpers

This commit is contained in:
2026-06-09 10:58:53 +02:00
parent e28074192c
commit 86bb062d18
2 changed files with 58 additions and 71 deletions
+35 -65
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
import re
from functools import lru_cache
from typing import Literal
from typing import Any, Literal
import numpy as np
import pyqtgraph as pg
@@ -21,8 +21,7 @@ logger = bec_logger.logger
def get_theme_name():
if QApplication.instance() is None or not hasattr(QApplication.instance(), "theme"):
return "dark"
else:
return QApplication.instance().theme.theme
return QApplication.instance().theme.theme
def get_theme_palette():
@@ -58,6 +57,26 @@ def apply_theme(theme: Literal["dark", "light"]):
process_all_deferred_deletes(QApplication.instance())
def theme_color(theme: Any | None, key: str, fallback: QColor | str) -> QColor:
"""
Return a QColor from a BEC theme with a robust fallback.
"""
fallback_color = fallback if isinstance(fallback, QColor) else QColor(str(fallback))
if theme is None or not hasattr(theme, "color"):
return fallback_color
color = theme.color(key, fallback_color.name())
return color if isinstance(color, QColor) else QColor(str(color))
def rgba(color: QColor | str, alpha: int) -> str:
"""
Return a QSS-compatible rgba string with alpha clamped to the 0-255 range.
"""
qcolor = color if isinstance(color, QColor) else QColor(str(color))
alpha = max(0, min(255, alpha))
return f"rgba({qcolor.red()}, {qcolor.green()}, {qcolor.blue()}, {alpha})"
class Colors:
@staticmethod
def list_available_colormaps() -> list[str]:
@@ -150,25 +169,6 @@ class Colors:
return ge.colorMap()
@staticmethod
def golden_ratio(num: int) -> list:
"""Calculate the golden ratio for a given number of angles.
Args:
num (int): Number of angles
Returns:
list: List of angles calculated using the golden ratio.
"""
phi = 2 * np.pi * ((1 + np.sqrt(5)) / 2)
angles = []
for ii in range(num):
x = np.cos(ii * phi)
y = np.sin(ii * phi)
angle = np.arctan2(y, x)
angles.append(angle)
return angles
@staticmethod
def set_theme_offset(theme: Literal["light", "dark"] | None = None, offset=0.2) -> tuple:
"""
@@ -239,20 +239,7 @@ class Colors:
else:
positions = np.linspace(min_pos, max_pos, num)
# Sample colors from the colormap at the calculated positions
colors = cmap.map(positions, mode="float")
color_list = []
for color in colors:
if format.upper() == "HEX":
color_list.append(QColor.fromRgbF(*color).name())
elif format.upper() == "RGB":
color_list.append(tuple((np.array(color) * 255).astype(int)))
elif format.upper() == "QCOLOR":
color_list.append(QColor.fromRgbF(*color))
else:
raise ValueError("Unsupported format. Please choose 'RGB', 'HEX', or 'QColor'.")
return color_list
return Colors._format_mapped_colors(cmap.map(positions, mode="float"), format)
@staticmethod
def golden_angle_color(
@@ -288,20 +275,19 @@ class Colors:
positions = np.mod(np.arange(num) * golden_angle_conjugate, 1)
positions = min_pos + positions * (max_pos - min_pos)
# Sample colors from the colormap at the calculated positions
colors = cmap.map(positions, mode="float")
color_list = []
return Colors._format_mapped_colors(cmap.map(positions, mode="float"), format)
for color in colors:
if format.upper() == "HEX":
color_list.append(QColor.fromRgbF(*color).name())
elif format.upper() == "RGB":
color_list.append(tuple((np.array(color) * 255).astype(int)))
elif format.upper() == "QCOLOR":
color_list.append(QColor.fromRgbF(*color))
else:
raise ValueError("Unsupported format. Please choose 'RGB', 'HEX', or 'QColor'.")
return color_list
@staticmethod
def _format_mapped_colors(colors: np.ndarray, format: Literal["QColor", "HEX", "RGB"]) -> list:
color_format = format.upper()
if color_format not in {"QCOLOR", "HEX", "RGB"}:
raise ValueError("Unsupported format. Please choose 'RGB', 'HEX', or 'QColor'.")
if color_format == "QCOLOR":
return [QColor.fromRgbF(*color) for color in colors]
if color_format == "HEX":
return [QColor.fromRgbF(*color).name() for color in colors]
return [tuple((np.array(color) * 255).astype(int)) for color in colors]
@staticmethod
def hex_to_rgba(hex_color: str, alpha=255) -> tuple:
@@ -325,22 +311,6 @@ class Colors:
raise ValueError("HEX color must be 6 or 8 characters long.")
return (r, g, b, alpha)
@staticmethod
def rgba_to_hex(r: int, g: int, b: int, a: int = 255) -> str:
"""
Convert RGBA color to HEX.
Args:
r(int): Red value (0-255).
g(int): Green value (0-255).
b(int): Blue value (0-255).
a(int): Alpha value (0-255). Default is 255 (opaque).
Returns:
hec_color(str): HEX color string.
"""
return "#{:02X}{:02X}{:02X}{:02X}".format(r, g, b, a)
@staticmethod
def validate_color(color: tuple | str) -> tuple | str:
"""
+23 -6
View File
@@ -2,11 +2,11 @@ import pyqtgraph as pg
import pytest
from pydantic import ValidationError
from qtpy.QtGui import QColor
from qtpy.QtWidgets import QVBoxLayout, QWidget
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
from bec_widgets.utils.bec_connector import ConnectionConfig
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.colors import Colors, apply_theme
from bec_widgets.utils.colors import Colors, apply_theme, get_theme_name, rgba, theme_color
from bec_widgets.widgets.plots.waveform.curve import CurveConfig
from tests.unit_tests.client_mocks import mocked_client
from tests.unit_tests.conftest import create_widget
@@ -76,10 +76,27 @@ def test_hex_to_rgba():
Colors.hex_to_rgba("#FF573")
def test_rgba_to_hex():
assert Colors.rgba_to_hex(255, 87, 51, 255) == "#FF5733FF"
assert Colors.rgba_to_hex(255, 87, 51, 128) == "#FF573380"
assert Colors.rgba_to_hex(255, 87, 51) == "#FF5733FF"
def test_get_theme_name_uses_application_theme():
app = QApplication.instance()
assert app.theme.theme == "light"
assert get_theme_name() == "light"
def test_theme_color_uses_theme_color_method():
app = QApplication.instance()
fallback = QColor("#ffffff")
expected = app.theme.color("FG", fallback.name())
expected = expected if isinstance(expected, QColor) else QColor(str(expected))
assert theme_color(app.theme, "FG", fallback).name() == expected.name()
def test_theme_color_returns_fallback_without_theme_color_method():
assert theme_color("light", "FG", QColor("#ffffff")).name() == "#ffffff"
def test_qss_rgba_and_blend_helpers():
assert rgba(QColor("#010203"), 300) == "rgba(1, 2, 3, 255)"
def test_canonical_colormap_name_case_insensitive():