Source code for pySimBlocks.gui.graphics.theme

# ******************************************************************************
#                                  pySimBlocks
#                     Copyright (c) 2026 Université de Lille & INRIA
# ******************************************************************************
#  This program is free software: you can redistribute it and/or modify it
#  under the terms of the GNU Lesser General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or (at your
#  option) any later version.
#
#  This program is distributed in the hope that it will be useful, but WITHOUT
#  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
#  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
#  for more details.
#
#  You should have received a copy of the GNU Lesser General Public License
#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
# ******************************************************************************
#  Authors: see Authors.txt
# ******************************************************************************

from dataclasses import dataclass

from PySide6.QtGui import QColor, QPalette
from PySide6.QtWidgets import QApplication


def _luma(c: QColor) -> float:
    """Return the perceived luminance of a color."""
    r, g, b, _ = c.getRgb()
    return 0.2126 * r + 0.7152 * g + 0.0722 * b


def _ensure_contrast(fg: QColor, bg: QColor, min_delta: float = 80.0) -> QColor:
    """Ensure |luma(fg) - luma(bg)| >= min_delta by pushing fg to black/white."""
    if abs(_luma(fg) - _luma(bg)) >= min_delta:
        return fg

    # push toward white on dark bg, toward black on light bg
    if _luma(bg) < 128:
        return QColor(235, 235, 235)
    return QColor(25, 25, 25)


def _separate_bg(block: QColor, scene: QColor, delta: float = 35.0) -> QColor:
    """Make sure block background is separated from scene background."""
    if abs(_luma(block) - _luma(scene)) >= delta:
        return block

    # If scene is dark -> lighten block a bit; if scene is light -> darken block a bit
    h, s, l, a = block.getHsl()
    if _luma(scene) < 128:
        l = min(255, l + 30)
    else:
        l = max(0, l - 30)
    out = QColor()
    out.setHsl(h, s, l, a)
    return out


[docs] @dataclass(frozen=True) class Theme: """Store the GUI color palette used by the graphics layer.""" #: Scene background color. scene_bg: QColor #: Default block background color. block_bg: QColor #: Selected block background color. block_bg_selected: QColor #: Default block border color. block_border: QColor #: Selected block border color. block_border_selected: QColor #: Default text color. text: QColor text_type: QColor #: Selected text color. text_selected: QColor text_type_selected: QColor #: Connection wire color. wire: QColor #: Input port color. port_in: QColor #: Output port color. port_out: QColor
[docs] def make_theme() -> Theme: """Build a theme derived from the current Qt application palette. Returns: Theme adapted to the current light or dark palette. """ pal = QApplication.palette() scene_bg = pal.color(QPalette.Window) is_dark = _luma(scene_bg) < 128 # base colors if is_dark: block_bg = QColor("#30343A") block_bg_selected = QColor("#3A6FB0") block_border = QColor("#ACACAC") block_border_selected = QColor("#6FAEFF") else: block_bg = QColor("#ECECEC") block_bg_selected = QColor("#C7DBFF") block_border = QColor("#6E6E6E") block_border_selected = QColor("#4A78FF") text = QColor("#F0F0F0") if is_dark else QColor("#1E1E1E") text_type = QColor("#7EB8D4") if is_dark else QColor("#2E7CA8") text_selected = QColor("#FFFFFF") if is_dark else QColor("#000000") text_type_selected = QColor("#A8D4E8") if is_dark else QColor("#1A5C80") wire = QColor("#E6E6E6") if is_dark else QColor("#1A1A1A") text = _ensure_contrast(text, block_bg, min_delta=90.0) text_type = _ensure_contrast(text_type, block_bg, min_delta=90.0) text_selected = _ensure_contrast(text_selected, block_bg_selected, min_delta=90.0) text_type_selected = _ensure_contrast(text_type_selected, block_bg_selected, min_delta=90.0) block_border = _ensure_contrast(block_border, block_bg, min_delta=60.0) block_border_selected = _ensure_contrast(block_border_selected, block_bg_selected, min_delta=60.0) wire = _ensure_contrast(wire, scene_bg, min_delta=110.0) port_in = QColor("#5FBF73") port_out = QColor("#E5534B") return Theme( scene_bg=scene_bg, block_bg=block_bg, block_bg_selected=block_bg_selected, block_border=block_border, block_border_selected=block_border_selected, text=text, text_type=text_type, text_selected=text_selected, text_type_selected=text_type_selected, wire=wire, port_in=port_in, port_out=port_out, )