Cockatrice/cockatrice/src/interface/palette_editor/palette_generator.cpp
BruebachL 117ea543c5
[App/Theme] Palette Editor (#6877)
* [App/Theme] Palette Editor

Took 1 minute

Took 1 hour 47 minutes

Took 6 seconds

Took 3 minutes

Took 5 minutes


Took 3 minutes

* Add oracle, add palette files and configs.

Took 10 minutes

* Fix a stupid include mistake, thanks IDE

Took 3 minutes

Took 20 seconds

* Includes.

Took 4 minutes

* Fix ampersand not displaying correctly.

Took 14 minutes

* Longer variable names.

Took 10 minutes

Took 5 seconds

* Change ampersand everywhere

Took 23 seconds

* Doxygen properly.

Took 1 minute

* Remove namespace, fold I/O into structs.

Took 12 minutes

* Remove namespace, fold I/O into structs.

Took 33 seconds

* Alphabetize.

Took 35 seconds

* Lint.

Took 49 seconds

* Add a combo box to quick switch settings.

Took 19 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-05-17 10:08:00 +02:00

200 lines
No EOL
8.7 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "palette_generator.h"
#include <QColor>
// ════════════════════════════════════════════════════════════════════════════
// PaletteGenerator::fromAccent
//
// Three intensity bands:
// 030 Subtle — Highlight / links / BrightText take on the hue;
// backgrounds stay neutral grey.
// 3070 Accented — Button, ToolTipBase, AlternateBase, shading tint in;
// backgrounds get a faint hue wash.
// 70100 Chromatic— Window and Base pick up real saturation; the whole
// application reads as that colour, text stays readable.
//
// Key principles:
// • Quadratic saturation curve: low end feels truly subtle, top is vivid.
// • Each role has its own saturation ceiling; buttons always pop above window.
// • Base stays near-white / near-black regardless of intensity for legibility.
// • Button text contrast is computed from the real button color, not assumed.
// • Tooltip blends from classic yellow to hue-tinted above intensity ~25.
// • 3D shading ladder (Light→Shadow) carries the same hue for coherence.
// • Disabled: text → mid-gray, backgrounds → match Window.
// • Inactive Highlight fades to a near-bg tone so it doesn't compete.
// ════════════════════════════════════════════════════════════════════════════
namespace PaletteGenerator
{
PaletteConfig fromAccent(const QColor &accent, int intensity, const QString &scheme)
{
PaletteConfig cfg;
const bool dark = scheme.compare("Dark", Qt::CaseInsensitive) == 0;
const double t = intensity / 100.0; // 0.0 1.0
int h = accent.hslHue();
const bool achromatic = (h < 0);
if (achromatic) {
h = 0;
}
// Saturation budgets
// Quadratic ease-in means the subtle end is genuinely subtle and the
// full end is bold without being garish.
auto sat = [&](double maxSat) -> int {
if (achromatic) {
return 0;
}
return qRound(maxSat * t * t);
};
const int satWindow = sat(dark ? 80.0 : 90.0);
const int satBase = sat(dark ? 25.0 : 20.0); // text areas stay near-white/black
const int satAlt = sat(dark ? 90.0 : 100.0);
const int satButton = sat(dark ? 120.0 : 130.0); // buttons pop above the bg
const int satTooltip = sat(dark ? 90.0 : 80.0);
const int satHighlight = achromatic ? 0 : qRound(accent.hslSaturation() * (0.45 + 0.55 * t));
const int satShadeHi = sat(dark ? 60.0 : 50.0); // Light / Midlight
const int satShadeLo = sat(dark ? 90.0 : 70.0); // Mid / Dark
// Per-role lightness
// Nudge lightness slightly as saturation rises to compensate for the
// Helmholtz-Kohlrausch effect (saturated colors look lighter/heavier).
const int winL = dark ? (28 + qRound(t * 8)) : (242 - qRound(t * 6));
const int baseL = dark ? (43 + qRound(t * 6)) : 252;
const int altL = dark ? (36 + qRound(t * 9)) : (234 - qRound(t * 5));
const int btnL = dark ? (56 + qRound(t * 10)) : (230 - qRound(t * 10));
const int tipL = dark ? (52 + qRound(t * 8)) : (248 - qRound(t * 6));
// Highlight color
QColor hl;
if (achromatic) {
hl = dark ? QColor(105, 105, 105) : QColor(95, 95, 95);
} else if (dark) {
int L = qBound(105, accent.lightness() + qRound(45.0 * (1.0 - t)), 215);
hl = QColor::fromHsl(h, qMin(255, satHighlight), L);
} else {
int L = qBound(50, accent.lightness() - qRound(25.0 * t), 180);
hl = QColor::fromHsl(h, qMin(255, satHighlight), L);
}
const double hlLuma = 0.299 * hl.red() + 0.587 * hl.green() + 0.114 * hl.blue();
const QColor hlText = (hlLuma > 135) ? Qt::black : Qt::white;
// Local helpers
using CR = QPalette::ColorRole;
using CG = QPalette::ColorGroup;
auto hsl = [&](int lightness, int s) -> QColor {
return QColor::fromHsl(h, qBound(0, s, 255), qBound(0, lightness, 255));
};
auto textOn = [](const QColor &bg) -> QColor {
double luma = 0.299 * bg.red() + 0.587 * bg.green() + 0.114 * bg.blue();
return (luma > 135) ? Qt::black : Qt::white;
};
auto set3 = [&](CR role, QColor active, QColor disabled, QColor inactive) {
cfg.colors[CG::Active][role] = active;
cfg.colors[CG::Disabled][role] = disabled;
cfg.colors[CG::Inactive][role] = inactive;
};
auto setAll = [&](CR role, QColor c) { set3(role, c, c, c); };
// Tooltip: blend classic yellow → hue-tinted above t≈0.20
QColor bg_tip;
if (achromatic || t < 0.20) {
bg_tip = QColor(255, 255, 220);
} else {
QColor tinted = hsl(tipL, satTooltip);
double blend = qMin(1.0, (t - 0.20) / 0.55);
QColor yellow(255, 255, 220);
bg_tip = QColor(qRound(yellow.red() * (1.0 - blend) + tinted.red() * blend),
qRound(yellow.green() * (1.0 - blend) + tinted.green() * blend),
qRound(yellow.blue() * (1.0 - blend) + tinted.blue() * blend));
}
// Backgrounds
const QColor bg_win = hsl(winL, satWindow);
const QColor bg_base = hsl(baseL, satBase);
const QColor bg_alt = hsl(altL, satAlt);
const QColor bg_btn = hsl(btnL, satButton);
set3(CR::Window, bg_win, bg_win, bg_win);
set3(CR::Base, bg_base, bg_win, bg_base);
set3(CR::AlternateBase, bg_alt, bg_alt, bg_alt);
set3(CR::Button, bg_btn, bg_win, bg_btn);
set3(CR::ToolTipBase, bg_tip, bg_tip, bg_tip);
// Foreground text
const QColor winText = dark ? Qt::white : Qt::black;
const QColor disText = dark ? QColor(157, 157, 157) : QColor(120, 120, 120);
const QColor disBtnText = dark ? QColor(120, 120, 120) : QColor(150, 150, 150);
set3(CR::WindowText, winText, disText, winText);
set3(CR::Text, winText, disText, winText);
set3(CR::ButtonText, textOn(bg_btn), disBtnText, textOn(bg_btn));
setAll(CR::ToolTipText, textOn(bg_tip));
const QColor phAlpha = dark ? QColor(255, 255, 255, 110) : QColor(0, 0, 0, 110);
const QColor phDis = dark ? QColor(255, 255, 255, 70) : QColor(0, 0, 0, 70);
set3(CR::PlaceholderText, phAlpha, phDis, phAlpha);
// Highlight / selection
const QColor inactiveHl = hsl(winL + (dark ? 14 : -10), satWindow);
cfg.colors[CG::Active][CR::Highlight] = hl;
cfg.colors[CG::Disabled][CR::Highlight] = inactiveHl;
cfg.colors[CG::Inactive][CR::Highlight] = inactiveHl;
cfg.colors[CG::Active][CR::HighlightedText] = hlText;
cfg.colors[CG::Disabled][CR::HighlightedText] = disText;
cfg.colors[CG::Inactive][CR::HighlightedText] = dark ? Qt::white : Qt::black;
// BrightText
QColor bright;
if (achromatic) {
bright = dark ? Qt::white : Qt::black;
} else if (dark) {
bright = QColor::fromHsl(h, qMin(255, satHighlight + 25), qMin(235, hl.lightness() + 50));
} else {
bright = Qt::white;
}
setAll(CR::BrightText, bright);
// Links
QColor link, linkV;
if (achromatic) {
link = dark ? QColor(100, 200, 255) : QColor(0, 0, 210);
linkV = dark ? QColor(200, 100, 255) : QColor(128, 0, 180);
} else if (dark) {
link = QColor::fromHsl(h, qMin(255, satHighlight), qMin(230, hl.lightness() + 75));
linkV = QColor::fromHsl((h + 30) % 360, qMin(255, satHighlight), qMin(215, hl.lightness() + 55));
} else {
link = QColor::fromHsl(h, qMin(255, satHighlight), qMax(40, hl.lightness() - 75));
linkV = QColor::fromHsl((h + 30) % 360, qMin(255, satHighlight), qMax(30, hl.lightness() - 95));
}
set3(CR::Link, link, dark ? QColor(48, 140, 198) : QColor(0, 0, 255), link);
set3(CR::LinkVisited, linkV, dark ? QColor(180, 80, 255) : QColor(255, 0, 255), linkV);
// 3D / frame shading
if (dark) {
setAll(CR::Light, hsl(115, qMin(255, satShadeHi)));
setAll(CR::Midlight, hsl(82, qMin(255, satShadeHi)));
setAll(CR::Mid, hsl(37, satShadeLo));
setAll(CR::Dark, hsl(22, satShadeLo));
setAll(CR::Shadow, Qt::black);
} else {
setAll(CR::Light, Qt::white);
setAll(CR::Midlight, hsl(226, qMin(255, satShadeHi)));
setAll(CR::Mid, hsl(158, satShadeLo));
setAll(CR::Dark, hsl(148, satShadeLo));
// Shadow stays neutral — tinting it makes the UI look bruised
cfg.colors[CG::Active][CR::Shadow] = QColor(105, 105, 105);
cfg.colors[CG::Disabled][CR::Shadow] = Qt::black;
cfg.colors[CG::Inactive][CR::Shadow] = QColor(105, 105, 105);
}
return cfg;
}
} // namespace PaletteGenerator