/* Copyright 2006-2026 The QElectroTech Team This file is part of QElectroTech. QElectroTech is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. QElectroTech 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 General Public License for more details. You should have received a copy of the GNU General Public License along with QElectroTech. If not, see . */ #include "qetpalette.h" #include #include #include namespace { /** Set one role for the Active and Inactive groups (same color: Fusion does not draw an inactive window differently, and a differing Inactive color is what turned combo box text black on macOS) and for the Disabled group. */ void setRole(QPalette &p, QPalette::ColorRole role, const QColor &active, const QColor &disabled) { p.setColor(QPalette::Active, role, active); p.setColor(QPalette::Inactive, role, active); p.setColor(QPalette::Disabled, role, disabled); } /// Same color in all three groups. void setRole(QPalette &p, QPalette::ColorRole role, const QColor &c) { setRole(p, role, c, c); } /// sRGB channel to linear light, as defined by WCAG 2. double linear(int channel) { const double c = channel / 255.0; return c <= 0.03928 ? c / 12.92 : std::pow((c + 0.055) / 1.055, 2.4); } double relativeLuminance(const QColor &c) { return 0.2126 * linear(c.red()) + 0.7152 * linear(c.green()) + 0.0722 * linear(c.blue()); } /** The selection color shared by both palettes: a blue that reads at better than 6:1 under white text. Fusion's own default (48,140,198) only reaches 3.7:1. */ const QColor kHighlight(30, 96, 176); } bool QET::Palette::styleIsFusion(const QStyle *style) { return style && style->objectName().compare(QLatin1String("fusion"), Qt::CaseInsensitive) == 0; } bool QET::Palette::isDark(const QPalette &palette) { return palette.color(QPalette::Active, QPalette::Window).lightness() < 128; } double QET::Palette::contrastRatio(const QColor &a, const QColor &b) { double lighter = relativeLuminance(a); double darker = relativeLuminance(b); if (lighter < darker) std::swap(lighter, darker); return (lighter + 0.05) / (darker + 0.05); } QPalette QET::Palette::fusionLight() { QPalette p; const QColor window(239, 239, 239); const QColor text(Qt::black); const QColor disabled_text(128, 128, 128); setRole(p, QPalette::Window, window); setRole(p, QPalette::WindowText, text, disabled_text); setRole(p, QPalette::Base, Qt::white, window); setRole(p, QPalette::AlternateBase, QColor(247, 247, 247)); setRole(p, QPalette::Text, text, disabled_text); setRole(p, QPalette::Button, window); setRole(p, QPalette::ButtonText, text, disabled_text); setRole(p, QPalette::BrightText, Qt::white); setRole(p, QPalette::Highlight, kHighlight, QColor(145, 145, 145)); setRole(p, QPalette::HighlightedText, Qt::white); setRole(p, QPalette::ToolTipBase, QColor(255, 255, 220)); setRole(p, QPalette::ToolTipText, text); setRole(p, QPalette::Link, QColor(0, 90, 170)); setRole(p, QPalette::LinkVisited, QColor(110, 40, 140)); setRole(p, QPalette::PlaceholderText, QColor(118, 118, 118)); // The 3D roles Fusion shades frames and gradients with. setRole(p, QPalette::Light, Qt::white); setRole(p, QPalette::Midlight, QColor(202, 202, 202)); setRole(p, QPalette::Mid, QColor(184, 184, 184)); setRole(p, QPalette::Dark, QColor(159, 159, 159)); setRole(p, QPalette::Shadow, QColor(118, 118, 118)); #if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) setRole(p, QPalette::Accent, kHighlight); #endif return p; } QPalette QET::Palette::fusionDark() { QPalette p; const QColor window(53, 53, 53); const QColor text(220, 220, 220); // Fusion lifts a button's fill to about (89,89,89) at the top of its // gradient; 175 is the darkest grey that still reads 3:1 on that. const QColor disabled_text(175, 175, 175); setRole(p, QPalette::Window, window); setRole(p, QPalette::WindowText, text, disabled_text); setRole(p, QPalette::Base, QColor(30, 30, 30), QColor(45, 45, 45)); setRole(p, QPalette::AlternateBase, QColor(45, 45, 45)); setRole(p, QPalette::Text, text, disabled_text); setRole(p, QPalette::Button, QColor(66, 66, 66)); setRole(p, QPalette::ButtonText, text, disabled_text); setRole(p, QPalette::BrightText, QColor(255, 90, 90)); setRole(p, QPalette::Highlight, kHighlight, QColor(80, 80, 80)); setRole(p, QPalette::HighlightedText, Qt::white); setRole(p, QPalette::ToolTipBase, QColor(66, 66, 66)); setRole(p, QPalette::ToolTipText, text); setRole(p, QPalette::Link, QColor(100, 170, 255)); setRole(p, QPalette::LinkVisited, QColor(190, 130, 255)); setRole(p, QPalette::PlaceholderText, QColor(150, 150, 150)); setRole(p, QPalette::Light, QColor(90, 90, 90)); setRole(p, QPalette::Midlight, QColor(75, 75, 75)); setRole(p, QPalette::Mid, QColor(35, 35, 35)); setRole(p, QPalette::Dark, QColor(20, 20, 20)); setRole(p, QPalette::Shadow, QColor(10, 10, 10)); #if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) setRole(p, QPalette::Accent, kHighlight); #endif return p; } QPalette QET::Palette::withPlatformAccent(QPalette palette, const QPalette &platform) { const QColor highlight = platform.color(QPalette::Active, QPalette::Highlight); const QColor highlighted_text = platform.color(QPalette::Active, QPalette::HighlightedText); if (!highlight.isValid() || !highlighted_text.isValid()) return palette; if (contrastRatio(highlight, highlighted_text) < 4.5) return palette; palette.setColor(QPalette::Active, QPalette::Highlight, highlight); palette.setColor(QPalette::Inactive, QPalette::Highlight, highlight); palette.setColor(QPalette::Active, QPalette::HighlightedText, highlighted_text); palette.setColor(QPalette::Inactive, QPalette::HighlightedText, highlighted_text); #if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) palette.setColor(QPalette::Active, QPalette::Accent, highlight); palette.setColor(QPalette::Inactive, QPalette::Accent, highlight); #endif return palette; } QPalette QET::Palette::forFusion(const QPalette &platform) { return withPlatformAccent(isDark(platform) ? fusionDark() : fusionLight(), platform); } bool QET::Palette::isLineArt(const QImage &image) { const QImage source = image.convertToFormat(QImage::Format_ARGB32); int visible = 0; int saturated = 0; for (int y = 0; y < source.height(); ++y) { const QRgb *line = reinterpret_cast(source.constScanLine(y)); for (int x = 0; x < source.width(); ++x) { if (qAlpha(line[x]) <= 64) continue; ++visible; const QColor color(line[x]); if (color.hslSaturationF() > 0.25 && color.value() > 60) ++saturated; } } return visible > 0 && saturated < visible * 0.20; } QImage QET::Palette::invertedLightness(const QImage &image) { // Lightness of pure black after inversion: the dark palette's text. const qreal ink = 220.0 / 255.0; QImage result = image.convertToFormat(QImage::Format_ARGB32); qreal darkest = 1.0; for (int y = 0; y < result.height(); ++y) { const QRgb *line = reinterpret_cast(result.constScanLine(y)); for (int x = 0; x < result.width(); ++x) if (qAlpha(line[x]) > 64) darkest = qMin(darkest, QColor(line[x]).lightnessF()); } const qreal span = qMax(1.0 - darkest, 1e-6); for (int y = 0; y < result.height(); ++y) { QRgb *line = reinterpret_cast(result.scanLine(y)); for (int x = 0; x < result.width(); ++x) { const int alpha = qAlpha(line[x]); if (alpha == 0) continue; const QColor color(line[x]); const qreal lightness = qBound(0.0, ink * (1.0 - (color.lightnessF() - darkest) / span), 1.0); QColor out = QColor::fromHslF(qMax(color.hslHueF(), 0.0), color.hslSaturationF(), lightness); out.setAlpha(alpha); line[x] = out.rgba(); } } return result; } QPixmap QET::Palette::forPalette(const QPixmap &pixmap, const QPalette &palette) { if (pixmap.isNull() || !isDark(palette)) return pixmap; const QImage image = pixmap.toImage(); if (!isLineArt(image)) return pixmap; QPixmap result = QPixmap::fromImage(invertedLightness(image)); result.setDevicePixelRatio(pixmap.devicePixelRatio()); return result; }