401 lines
20 KiB
C++
401 lines
20 KiB
C++
#include "UI/OmarchyTheme.h"
|
|
#include "Document/ProjectWorkspace.h"
|
|
#include "UI/ProjectWorkspaceView.h"
|
|
#include <QApplication>
|
|
#include <QDir>
|
|
#include <QLabel>
|
|
#include <QMessageBox>
|
|
#include <QStyle>
|
|
#include <QStyleFactory>
|
|
#include <QTemporaryDir>
|
|
#include <QToolButton>
|
|
#include <cstdio>
|
|
#include <sys/syscall.h>
|
|
#include <unistd.h>
|
|
#include <QtTest>
|
|
|
|
// The desktop's theme: read, applied, followed through a switch.
|
|
namespace {
|
|
const char *latte = R"(# Catppuccin Latte
|
|
mode = "light"
|
|
|
|
accent = "#1e66f5" # blue
|
|
selection = "#ccd0da"
|
|
muted = "#acb0be"
|
|
background = "#eff1f5"
|
|
dark_background = "#e3e4e8"
|
|
darker_background = "#d7d8dc"
|
|
lighter_background = "#dce0e8"
|
|
foreground = "#4c4f69"
|
|
dark_foreground = "#9ca0b0"
|
|
light_foreground = "#5c5f77"
|
|
bright_foreground = "#4c4f69"
|
|
red = "#d20f39"
|
|
yellow = "#df8e1d"
|
|
orange = "#d84e2b"
|
|
green = "#40a02b"
|
|
)";
|
|
|
|
const char *macchiato = R"(mode = "dark"
|
|
accent = "#8aadf4"
|
|
selection = "#494d64"
|
|
muted = "#5b6078"
|
|
background = "#24273a"
|
|
dark_background = "#1e2030"
|
|
darker_background = "#181926"
|
|
lighter_background = "#363a4f"
|
|
foreground = "#cad3f5"
|
|
dark_foreground = "#6e738d"
|
|
light_foreground = "#b8c0e0"
|
|
bright_foreground = "#cad3f5"
|
|
red = "#ed8796"
|
|
yellow = "#eed49f"
|
|
)";
|
|
|
|
void write(const QString &path, const QString &text)
|
|
{
|
|
QDir().mkpath(QFileInfo(path).absolutePath());
|
|
QFile file(path);
|
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate) || file.write(text.toUtf8()) < 0)
|
|
throw std::runtime_error("could not write " + path.toStdString());
|
|
}
|
|
|
|
// While set, a colors.toml read gives 64 bytes, then fails.
|
|
bool shortReads = false;
|
|
|
|
// A window filled with the palette's window colour.
|
|
struct Swatch {
|
|
QLabel label;
|
|
Swatch()
|
|
{
|
|
label.setAutoFillBackground(true);
|
|
label.resize(40, 40);
|
|
label.show();
|
|
}
|
|
QRgb pixel() { return label.grab().toImage().pixel(20, 20); }
|
|
};
|
|
}
|
|
|
|
extern "C" ssize_t read(int descriptor, void *buffer, size_t count)
|
|
{
|
|
char path[512] = {};
|
|
const QByteArray link = "/proc/self/fd/" + QByteArray::number(descriptor);
|
|
const bool theme = shortReads && readlink(link.constData(), path, sizeof(path) - 1) > 0 && QByteArray(path).endsWith("colors.toml");
|
|
static int reads = 0;
|
|
if (theme && ++reads > 1) {
|
|
errno = EIO;
|
|
return -1;
|
|
}
|
|
if (!theme)
|
|
reads = 0;
|
|
return syscall(SYS_read, descriptor, buffer, theme ? std::min<size_t>(count, 64) : count);
|
|
}
|
|
|
|
class OmarchyThemeTests : public QObject {
|
|
Q_OBJECT
|
|
private slots:
|
|
void theFileParsesIntoColoursAndRefusesWhatIsMissingOrMalformed();
|
|
void lightAndDarkThemesFillThePalette();
|
|
void withoutAThemeTheBuiltInDarkApplies();
|
|
void aSwitchRetintsTheWindowsWhileTheyShow();
|
|
void anUnreadableSwitchKeepsThePalette();
|
|
void aMalformedFileAtStartupLeavesTheBuiltInDark();
|
|
void aFileThatCannotBeOpenedKeepsThePalette();
|
|
void anAbsentStateDirectoryIsWatchedForItsBirth();
|
|
void aBarredStateDirectoryIsWatchedFromAbove();
|
|
void aReadCutShortKeepsThePalette();
|
|
void aSwitchReachesTheWindowsTabsAndCanvas();
|
|
};
|
|
|
|
void OmarchyThemeTests::theFileParsesIntoColoursAndRefusesWhatIsMissingOrMalformed()
|
|
{
|
|
const OmarchyColors colours = OmarchyColors::parse(QString::fromLatin1(latte));
|
|
QVERIFY(!colours.dark);
|
|
QCOMPARE(colours.accent, QColor(0x1e, 0x66, 0xf5));
|
|
QCOMPARE(colours.selection, QColor(0xcc, 0xd0, 0xda));
|
|
QCOMPARE(colours.muted, QColor(0xac, 0xb0, 0xbe));
|
|
QCOMPARE(colours.background, QColor(0xef, 0xf1, 0xf5));
|
|
QCOMPARE(colours.darkBackground, QColor(0xe3, 0xe4, 0xe8));
|
|
QCOMPARE(colours.darkerBackground, QColor(0xd7, 0xd8, 0xdc));
|
|
QCOMPARE(colours.lighterBackground, QColor(0xdc, 0xe0, 0xe8));
|
|
QCOMPARE(colours.foreground, QColor(0x4c, 0x4f, 0x69));
|
|
QCOMPARE(colours.darkForeground, QColor(0x9c, 0xa0, 0xb0));
|
|
QCOMPARE(colours.lightForeground, QColor(0x5c, 0x5f, 0x77));
|
|
QCOMPARE(colours.brightForeground, QColor(0x4c, 0x4f, 0x69));
|
|
QCOMPARE(colours.red, QColor(0xd2, 0x0f, 0x39));
|
|
// Orange is the warning where present, else yellow.
|
|
QCOMPARE(colours.warning, QColor(0xd8, 0x4e, 0x2b));
|
|
const OmarchyColors dark = OmarchyColors::parse(QString::fromLatin1(macchiato));
|
|
QVERIFY(dark.dark);
|
|
QCOMPARE(dark.warning, QColor(0xee, 0xd4, 0x9f));
|
|
QCOMPARE(dark.background, QColor(0x24, 0x27, 0x3a));
|
|
const auto refused = [](const QString &toml) {
|
|
try {
|
|
OmarchyColors::parse(toml);
|
|
} catch (const std::runtime_error &error) {
|
|
return QString::fromLatin1(error.what());
|
|
}
|
|
return QString();
|
|
};
|
|
QVERIFY(refused(QString::fromLatin1(macchiato).replace("mode = \"dark\"", "mode = \"dim\"")).contains("mode"));
|
|
QVERIFY(refused(QString::fromLatin1(macchiato).replace("accent = \"#8aadf4\"\n", "")).contains("accent"));
|
|
QVERIFY(refused(QString::fromLatin1(macchiato).replace("#ed8796", "#ed879")).contains("red"));
|
|
QVERIFY(refused(QString::fromLatin1(macchiato).replace("#ed8796", "ed8796")).contains("red"));
|
|
QVERIFY(refused(QString()).contains("mode"));
|
|
// Single quotes read the same.
|
|
QCOMPARE(OmarchyColors::parse(QString::fromLatin1(latte).replace('"', '\'')).accent, QColor(0x1e, 0x66, 0xf5));
|
|
QCOMPARE(OmarchyColors::parse(QString::fromLatin1(macchiato).replace('"', '\'')).dark, true);
|
|
QVERIFY(refused(QString::fromLatin1(macchiato).replace("\"dark\"", "'dark\"")).contains("mode"));
|
|
// Quoted keys read too; bare or half-quoted values refuse.
|
|
QCOMPARE(OmarchyColors::parse(QString::fromLatin1(macchiato).replace("mode = ", "\"mode\" = ")).dark, true);
|
|
QCOMPARE(OmarchyColors::parse(QString::fromLatin1(macchiato).replace("accent = ", "'accent' = ")).accent, QColor(0x8a, 0xad, 0xf4));
|
|
QVERIFY(refused(QString::fromLatin1(macchiato).replace("mode = \"dark\"", "mode = dark")).contains("mode is no quoted"));
|
|
QVERIFY(refused(QString::fromLatin1(latte).replace("orange = \"#d84e2b\"", "orange = 123")).contains("orange is no quoted"));
|
|
QVERIFY(refused(QString::fromLatin1(latte).replace("orange = \"#d84e2b\"", "orange = \"#d84e2b'")).contains("orange"));
|
|
QVERIFY(refused(QString::fromLatin1(latte).replace("red = \"#d20f39\"", "red = \"#d20f39\" \"twice\"")).contains("red is no quoted"));
|
|
// A trailing comment and a bare unknown key are fine.
|
|
QCOMPARE(OmarchyColors::parse(QString::fromLatin1(macchiato) + "hyprland_border = 1\n").red, QColor(0xed, 0x87, 0x96));
|
|
// A later line wins; an unknown key is ignored.
|
|
const OmarchyColors twice = OmarchyColors::parse(QString::fromLatin1(macchiato) + "\nred = \"#000000\"\nhyprland_active_border = \"#123456\"\n");
|
|
QCOMPARE(twice.red, QColor(0, 0, 0));
|
|
}
|
|
|
|
void OmarchyThemeTests::lightAndDarkThemesFillThePalette()
|
|
{
|
|
const QPalette light = OmarchyColors::parse(QString::fromLatin1(latte)).palette();
|
|
QCOMPARE(light.color(QPalette::Window), QColor(0xef, 0xf1, 0xf5));
|
|
QCOMPARE(light.color(QPalette::WindowText), QColor(0x4c, 0x4f, 0x69));
|
|
QCOMPARE(light.color(QPalette::Base), QColor(0xd7, 0xd8, 0xdc));
|
|
QCOMPARE(light.color(QPalette::AlternateBase), QColor(0xe3, 0xe4, 0xe8));
|
|
QCOMPARE(light.color(QPalette::Text), QColor(0x4c, 0x4f, 0x69));
|
|
QCOMPARE(light.color(QPalette::Button), QColor(0xdc, 0xe0, 0xe8));
|
|
QCOMPARE(light.color(QPalette::ButtonText), QColor(0x4c, 0x4f, 0x69));
|
|
QCOMPARE(light.color(QPalette::Highlight), QColor(0x1e, 0x66, 0xf5));
|
|
QCOMPARE(light.color(QPalette::HighlightedText), QColor(0xef, 0xf1, 0xf5));
|
|
QCOMPARE(light.color(QPalette::Link), QColor(0x1e, 0x66, 0xf5));
|
|
QCOMPARE(light.color(QPalette::PlaceholderText), QColor(0x9c, 0xa0, 0xb0));
|
|
QCOMPARE(light.color(QPalette::BrightText), QColor(0xd8, 0x4e, 0x2b));
|
|
QCOMPARE(light.color(QPalette::Mid), QColor(0xac, 0xb0, 0xbe));
|
|
QCOMPARE(light.color(QPalette::Dark), QColor(0xd7, 0xd8, 0xdc));
|
|
QCOMPARE(light.color(QPalette::Light), QColor(0xdc, 0xe0, 0xe8));
|
|
QCOMPARE(light.color(QPalette::ToolTipBase), QColor(0xe3, 0xe4, 0xe8));
|
|
QCOMPARE(light.color(QPalette::ToolTipText), QColor(0x4c, 0x4f, 0x69));
|
|
for (const QPalette::ColorRole role : {QPalette::WindowText, QPalette::Text, QPalette::ButtonText})
|
|
QCOMPARE(light.color(QPalette::Disabled, role), QColor(0xac, 0xb0, 0xbe));
|
|
const QPalette dark = OmarchyColors::parse(QString::fromLatin1(macchiato)).palette();
|
|
QCOMPARE(dark.color(QPalette::Window), QColor(0x24, 0x27, 0x3a));
|
|
QCOMPARE(dark.color(QPalette::HighlightedText), QColor(0x24, 0x27, 0x3a));
|
|
QCOMPARE(dark.color(QPalette::BrightText), QColor(0xee, 0xd4, 0x9f));
|
|
QCOMPARE(dark.color(QPalette::PlaceholderText), QColor(0x6e, 0x73, 0x8d));
|
|
}
|
|
|
|
void OmarchyThemeTests::withoutAThemeTheBuiltInDarkApplies()
|
|
{
|
|
QTemporaryDir state;
|
|
// Whatever style the desktop set, the palette needs Fusion.
|
|
QApplication::setStyle(QStyleFactory::create(QStringLiteral("Windows")));
|
|
QCOMPARE(QApplication::style()->objectName(), QString("windows"));
|
|
QTest::ignoreMessage(QtInfoMsg, QRegularExpression("no Omarchy theme at .*colors.toml - built-in dark"));
|
|
OmarchyTheme theme(state.path());
|
|
QCOMPARE(theme.colors(), OmarchyColors::builtInDark());
|
|
QCOMPARE(QApplication::palette().color(QPalette::Window), QColor(0x24, 0x24, 0x24));
|
|
QCOMPARE(QApplication::palette().color(QPalette::Base), QColor(0x1b, 0x1b, 0x1b));
|
|
QCOMPARE(QApplication::palette().color(QPalette::PlaceholderText), QColor(0x8a, 0x8a, 0x8a));
|
|
QCOMPARE(QApplication::palette().color(QPalette::BrightText), QColor(0xff, 0x9f, 0x0a));
|
|
QCOMPARE(QApplication::style()->objectName(), QString("fusion"));
|
|
// Swift's d62a132: alerts are dark too.
|
|
QMessageBox alert(QMessageBox::Warning, QStringLiteral("Title"), QStringLiteral("Text"));
|
|
QCOMPARE(alert.palette().color(QPalette::Window), QColor(0x24, 0x24, 0x24));
|
|
QCOMPARE(alert.style()->objectName(), QString("fusion"));
|
|
Swatch swatch;
|
|
QCOMPARE(swatch.pixel(), qRgb(0x24, 0x24, 0x24));
|
|
// A theme appearing later is taken up, unnamed or not.
|
|
QTest::failOnWarning(QRegularExpression("QIODevice"));
|
|
QTest::ignoreMessage(QtInfoMsg, QRegularExpression("Omarchy theme \\(unnamed\\) light from"));
|
|
write(state.path() + "/theme/colors.toml", QString::fromLatin1(latte));
|
|
QTRY_COMPARE(swatch.pixel(), qRgb(0xef, 0xf1, 0xf5));
|
|
}
|
|
|
|
void OmarchyThemeTests::aSwitchRetintsTheWindowsWhileTheyShow()
|
|
{
|
|
QTemporaryDir state;
|
|
write(state.path() + "/theme/colors.toml", QString::fromLatin1(macchiato));
|
|
write(state.path() + "/theme.name", "catppuccin-macchiato\n");
|
|
QTest::ignoreMessage(QtInfoMsg, QRegularExpression("Omarchy theme catppuccin-macchiato dark from .*colors.toml"));
|
|
OmarchyTheme theme(state.path());
|
|
Swatch swatch;
|
|
QCOMPARE(swatch.pixel(), qRgb(0x24, 0x27, 0x3a));
|
|
// omarchy-theme-set: remove, move the next in, write the name.
|
|
write(state.path() + "/next-theme/colors.toml", QString::fromLatin1(latte));
|
|
QVERIFY(QDir(state.path() + "/theme").removeRecursively());
|
|
QVERIFY(QDir().rename(state.path() + "/next-theme", state.path() + "/theme"));
|
|
write(state.path() + "/theme.name", "catppuccin-latte\n");
|
|
QTRY_COMPARE(swatch.pixel(), qRgb(0xef, 0xf1, 0xf5));
|
|
QVERIFY(!theme.colors().dark);
|
|
QCOMPARE(QApplication::palette().color(QPalette::Highlight), QColor(0x1e, 0x66, 0xf5));
|
|
// Edited in place the file counts too, twice running.
|
|
write(state.path() + "/theme/colors.toml", QString::fromLatin1(macchiato));
|
|
QTRY_COMPARE(swatch.pixel(), qRgb(0x24, 0x27, 0x3a));
|
|
write(state.path() + "/theme/colors.toml", QString::fromLatin1(latte));
|
|
QTRY_COMPARE(swatch.pixel(), qRgb(0xef, 0xf1, 0xf5));
|
|
// Replaced under a held handle, then edited: still seen.
|
|
QFile held(state.path() + "/theme/colors.toml");
|
|
QVERIFY(held.open(QIODevice::ReadOnly));
|
|
write(state.path() + "/theme/next.toml", QString::fromLatin1(macchiato));
|
|
QCOMPARE(::rename(QFile::encodeName(state.path() + "/theme/next.toml").constData(), QFile::encodeName(state.path() + "/theme/colors.toml").constData()), 0);
|
|
QTRY_COMPARE(swatch.pixel(), qRgb(0x24, 0x27, 0x3a));
|
|
write(state.path() + "/theme/colors.toml", QString::fromLatin1(latte));
|
|
QTRY_COMPARE(swatch.pixel(), qRgb(0xef, 0xf1, 0xf5));
|
|
}
|
|
|
|
void OmarchyThemeTests::anUnreadableSwitchKeepsThePalette()
|
|
{
|
|
QTemporaryDir state;
|
|
write(state.path() + "/theme/colors.toml", QString::fromLatin1(macchiato));
|
|
OmarchyTheme theme(state.path());
|
|
Swatch swatch;
|
|
QCOMPARE(swatch.pixel(), qRgb(0x24, 0x27, 0x3a));
|
|
QTest::ignoreMessage(QtWarningMsg, QRegularExpression("Omarchy theme unreadable, keeping the palette: colors.toml lacks accent"));
|
|
write(state.path() + "/theme/colors.toml", QString::fromLatin1(macchiato).replace("accent = \"#8aadf4\"\n", ""));
|
|
QTest::qWait(400);
|
|
QCOMPARE(swatch.pixel(), qRgb(0x24, 0x27, 0x3a));
|
|
QVERIFY(theme.colors().dark);
|
|
// Mended, it applies again.
|
|
write(state.path() + "/theme/colors.toml", QString::fromLatin1(latte));
|
|
QTRY_COMPARE(swatch.pixel(), qRgb(0xef, 0xf1, 0xf5));
|
|
}
|
|
|
|
void OmarchyThemeTests::aMalformedFileAtStartupLeavesTheBuiltInDark()
|
|
{
|
|
QTemporaryDir state;
|
|
write(state.path() + "/theme/colors.toml", QString::fromLatin1(macchiato).replace("mode = \"dark\"\n", ""));
|
|
QTest::ignoreMessage(QtWarningMsg, QRegularExpression("Omarchy theme unreadable, keeping the palette: colors.toml lacks mode"));
|
|
OmarchyTheme theme(state.path());
|
|
QCOMPARE(theme.colors(), OmarchyColors::builtInDark());
|
|
Swatch swatch;
|
|
QCOMPARE(swatch.pixel(), qRgb(0x24, 0x24, 0x24));
|
|
write(state.path() + "/theme/colors.toml", QString::fromLatin1(latte));
|
|
QTRY_COMPARE(swatch.pixel(), qRgb(0xef, 0xf1, 0xf5));
|
|
}
|
|
|
|
void OmarchyThemeTests::aFileThatCannotBeOpenedKeepsThePalette()
|
|
{
|
|
QTemporaryDir state;
|
|
const QString path = state.path() + "/theme/colors.toml";
|
|
write(path, QString::fromLatin1(latte));
|
|
OmarchyTheme theme(state.path());
|
|
Swatch swatch;
|
|
QCOMPARE(swatch.pixel(), qRgb(0xef, 0xf1, 0xf5));
|
|
// A file that will not open is no absent file.
|
|
QVERIFY(QFile::setPermissions(path, QFileDevice::Permissions()));
|
|
QTest::ignoreMessage(QtWarningMsg, QRegularExpression("Omarchy theme unreadable, keeping the palette: .*colors.toml"));
|
|
theme.apply();
|
|
QCOMPARE(QApplication::palette().color(QPalette::Window), QColor(0xef, 0xf1, 0xf5));
|
|
QVERIFY(!theme.colors().dark);
|
|
QVERIFY(QFile::setPermissions(path, QFileDevice::ReadOwner | QFileDevice::WriteOwner));
|
|
// An unsearchable folder hides the file: kept too.
|
|
const QString folder = state.path() + "/theme";
|
|
QVERIFY(QFile::setPermissions(folder, QFileDevice::Permissions()));
|
|
QTest::ignoreMessage(QtWarningMsg, QRegularExpression("Omarchy theme unreadable, keeping the palette: .*colors.toml"));
|
|
theme.apply();
|
|
QCOMPARE(QApplication::palette().color(QPalette::Window), QColor(0xef, 0xf1, 0xf5));
|
|
QVERIFY(QFile::setPermissions(folder, QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner));
|
|
}
|
|
|
|
void OmarchyThemeTests::anAbsentStateDirectoryIsWatchedForItsBirth()
|
|
{
|
|
QTemporaryDir root;
|
|
const QString state = root.path() + "/omarchy/current";
|
|
QTest::ignoreMessage(QtInfoMsg, QRegularExpression("no Omarchy theme at .*colors.toml - built-in dark"));
|
|
OmarchyTheme theme(state);
|
|
Swatch swatch;
|
|
QCOMPARE(swatch.pixel(), qRgb(0x24, 0x24, 0x24));
|
|
// Omarchy installed later: the theme arrives with its folders.
|
|
auto &watcher = *theme.findChild<QFileSystemWatcher *>();
|
|
QCOMPARE(watcher.directories(), QStringList{root.path()});
|
|
write(state + "/theme/colors.toml", QString::fromLatin1(latte));
|
|
QTRY_COMPARE(swatch.pixel(), qRgb(0xef, 0xf1, 0xf5));
|
|
// Found, the state directory is watched, its ancestor let go.
|
|
QVERIFY(watcher.directories().contains(state) && !watcher.directories().contains(root.path()));
|
|
write(state + "/theme/colors.toml", QString::fromLatin1(macchiato));
|
|
QTRY_COMPARE(swatch.pixel(), qRgb(0x24, 0x27, 0x3a));
|
|
}
|
|
|
|
void OmarchyThemeTests::aBarredStateDirectoryIsWatchedFromAbove()
|
|
{
|
|
QTemporaryDir root;
|
|
const QString state = root.path() + "/current";
|
|
write(state + "/theme/colors.toml", QString::fromLatin1(latte));
|
|
QVERIFY(QFile::setPermissions(state, QFileDevice::Permissions()));
|
|
QTest::ignoreMessage(QtWarningMsg, QRegularExpression("Omarchy theme unreadable, keeping the palette: .*colors.toml"));
|
|
OmarchyTheme theme(state);
|
|
Swatch swatch;
|
|
QCOMPARE(swatch.pixel(), qRgb(0x24, 0x24, 0x24));
|
|
auto &watcher = *theme.findChild<QFileSystemWatcher *>();
|
|
QCOMPARE(watcher.directories(), QStringList{root.path()});
|
|
// Opened up again, the theme is found and followed.
|
|
QVERIFY(QFile::setPermissions(state, QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner));
|
|
QTRY_COMPARE(swatch.pixel(), qRgb(0xef, 0xf1, 0xf5));
|
|
QTRY_VERIFY(watcher.directories().contains(state) && !watcher.directories().contains(root.path()));
|
|
write(state + "/theme/colors.toml", QString::fromLatin1(macchiato));
|
|
QTRY_COMPARE(swatch.pixel(), qRgb(0x24, 0x27, 0x3a));
|
|
}
|
|
|
|
void OmarchyThemeTests::aReadCutShortKeepsThePalette()
|
|
{
|
|
QTemporaryDir state;
|
|
write(state.path() + "/theme/colors.toml", QString::fromLatin1(latte));
|
|
OmarchyTheme theme(state.path());
|
|
Swatch swatch;
|
|
QCOMPARE(swatch.pixel(), qRgb(0xef, 0xf1, 0xf5));
|
|
// Sixty-four bytes, then an error: a prefix, not the file.
|
|
write(state.path() + "/theme/colors.toml", QString::fromLatin1(macchiato));
|
|
shortReads = true;
|
|
QTest::ignoreMessage(QtWarningMsg, QRegularExpression("Omarchy theme unreadable, keeping the palette: read 64 of [0-9]+ bytes"));
|
|
theme.apply();
|
|
QCOMPARE(QApplication::palette().color(QPalette::Window), QColor(0xef, 0xf1, 0xf5));
|
|
QCOMPARE(swatch.pixel(), qRgb(0xef, 0xf1, 0xf5));
|
|
shortReads = false;
|
|
theme.apply();
|
|
QCOMPARE(QApplication::palette().color(QPalette::Window), QColor(0x24, 0x27, 0x3a));
|
|
// Widgets take the new palette from the event loop.
|
|
QTRY_COMPARE(swatch.pixel(), qRgb(0x24, 0x27, 0x3a));
|
|
}
|
|
|
|
void OmarchyThemeTests::aSwitchReachesTheWindowsTabsAndCanvas()
|
|
{
|
|
QTemporaryDir state;
|
|
write(state.path() + "/theme/colors.toml", QString::fromLatin1(macchiato));
|
|
OmarchyTheme theme(state.path());
|
|
ProjectWorkspace workspace;
|
|
ProjectWorkspaceView window(workspace);
|
|
window.show();
|
|
QVERIFY(QTest::qWaitForWindowExposed(&window));
|
|
// The lightest or darkest pixel of a widget: its text.
|
|
const auto extreme = [](QWidget &widget, bool lightest) {
|
|
const QImage image = widget.grab().toImage();
|
|
int best = lightest ? 0 : 255;
|
|
for (int y = 0; y < image.height(); ++y) {
|
|
for (int x = 0; x < image.width(); ++x)
|
|
best = lightest ? std::max(best, qGray(image.pixel(x, y))) : std::min(best, qGray(image.pixel(x, y)));
|
|
}
|
|
return best;
|
|
};
|
|
auto &tab = *window.findChild<QToolButton *>("selectTab");
|
|
auto &canvas = *window.findChild<QWidget *>("editorCanvas");
|
|
QVERIFY(std::abs(extreme(tab, true) - qGray(qRgb(0xca, 0xd3, 0xf5))) <= 12);
|
|
QCOMPARE(canvas.grab().toImage().pixelColor(5, 5), QColor(0x18, 0x19, 0x26));
|
|
// Light: the tab's title turns dark, the canvas surround light.
|
|
write(state.path() + "/theme/colors.toml", QString::fromLatin1(latte));
|
|
QTRY_COMPARE(canvas.grab().toImage().pixelColor(5, 5), QColor(0xd7, 0xd8, 0xdc));
|
|
QVERIFY(std::abs(extreme(tab, false) - qGray(qRgb(0x4c, 0x4f, 0x69))) <= 12);
|
|
// The row's margin shows the toolbar, not a fill.
|
|
auto &strip = *window.findChild<QWidget *>("projectTabs");
|
|
const QImage bar = window.grab().toImage();
|
|
const QPoint inRow = strip.mapTo(&window, QPoint(6, 1)), beside = strip.mapTo(&window, QPoint(strip.width() + 8, 1));
|
|
QCOMPARE(bar.pixelColor(inRow), bar.pixelColor(beside));
|
|
}
|
|
|
|
QTEST_MAIN(OmarchyThemeTests)
|
|
#include "OmarchyThemeTests.moc"
|