From 2c9866ef9e6a3bd1803f0cd5dffd294c047d74b9 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Tue, 22 Sep 2026 16:26:53 +1000 Subject: [PATCH] Qt: Dynamically adjust log window colours to theme Fixes unreadable text in some themes. --- src/duckstation-qt/logwindow.cpp | 203 ++++++++++++++++++++++--------- src/duckstation-qt/logwindow.h | 7 +- 2 files changed, 152 insertions(+), 58 deletions(-) diff --git a/src/duckstation-qt/logwindow.cpp b/src/duckstation-qt/logwindow.cpp index be2393333..fd3ed9297 100644 --- a/src/duckstation-qt/logwindow.cpp +++ b/src/duckstation-qt/logwindow.cpp @@ -14,6 +14,9 @@ #include #include +#include +#include + #include "moc_logwindow.cpp" using namespace Qt::StringLiterals; @@ -22,7 +25,7 @@ using namespace Qt::StringLiterals; // But once I get rid of that, there will be. LogWindow* g_log_window; -LogWidget::LogWidget(QWidget* parent) : QPlainTextEdit(parent), m_is_dark_theme(QtHost::IsDarkApplicationTheme()) +LogWidget::LogWidget(QWidget* parent) : QPlainTextEdit(parent) { setReadOnly(true); setUndoRedoEnabled(false); @@ -31,6 +34,9 @@ LogWidget::LogWidget(QWidget* parent) : QPlainTextEdit(parent), m_is_dark_theme( setMaximumBlockCount(MAX_LINES); setFont(QtHost::GetFixedFont()); + ensurePolished(); + updateColors(); + Log::RegisterCallback(&LogWidget::logCallback, this); } @@ -41,10 +47,143 @@ LogWidget::~LogWidget() void LogWidget::changeEvent(QEvent* event) { - if (event->type() == QEvent::StyleChange) - m_is_dark_theme = QtHost::IsDarkApplicationTheme(); - QPlainTextEdit::changeEvent(event); + + if (event->type() == QEvent::StyleChange || event->type() == QEvent::PaletteChange || + event->type() == QEvent::ApplicationPaletteChange) + { + updateColors(); + } +} + +static constexpr float MINIMUM_TEXT_CONTRAST = 4.5; + +static float GetLinearColorComponent(float component) +{ + return (component <= 0.04045f) ? (component / 12.92f) : std::pow((component + 0.055f) / 1.055f, 2.4f); +} + +static float GetRelativeLuminance(const QColor& color) +{ + return (0.2126f * GetLinearColorComponent(color.redF())) + (0.7152f * GetLinearColorComponent(color.greenF())) + + (0.0722f * GetLinearColorComponent(color.blueF())); +} + +static float GetContrastRatio(const QColor& first, const QColor& second) +{ + const float first_luminance = GetRelativeLuminance(first); + const float second_luminance = GetRelativeLuminance(second); + const float lighter = std::max(first_luminance, second_luminance); + const float darker = std::min(first_luminance, second_luminance); + return (lighter + 0.05f) / (darker + 0.05f); +} + +static QColor BlendColors(const QColor& first, const QColor& second, float amount) +{ + const float inverse_amount = 1.0f - amount; + return QColor::fromRgbF((first.redF() * inverse_amount) + (second.redF() * amount), + (first.greenF() * inverse_amount) + (second.greenF() * amount), + (first.blueF() * inverse_amount) + (second.blueF() * amount)); +} + +static QColor MakeOpaque(const QColor& color, const QColor& background) +{ + return color.alphaF() < 1.0f ? BlendColors(background, color, color.alphaF()) : color; +} + +static QColor EnsureTextContrast(const QColor& color, const QColor& background) +{ + const QColor opaque_color = MakeOpaque(color, background); + if (GetContrastRatio(opaque_color, background) >= MINIMUM_TEXT_CONTRAST) + return opaque_color; + + static constexpr QColor black(0x00, 0x00, 0x00); + static constexpr QColor white(0xFF, 0xFF, 0xFF); + const bool lighten = (GetContrastRatio(white, background) > GetContrastRatio(black, background)); + + float hue; + float saturation; + float lightness; + opaque_color.getHslF(&hue, &saturation, &lightness); + + // Find the smallest lightness adjustment which reaches the contrast target. At either lightness endpoint the + // color becomes black or white, so one endpoint is guaranteed to meet the target for any opaque background. + float low = 0.0f; + float high = 1.0f; + for (u32 i = 0; i < 16; i++) + { + const float amount = (low + high) * 0.5f; + const float adjusted_lightness = lightness + ((lighten ? 1.0f : 0.0f) - lightness) * amount; + const QColor adjusted = QColor::fromHslF(hue, saturation, adjusted_lightness); + if (GetContrastRatio(adjusted, background) >= MINIMUM_TEXT_CONTRAST) + high = amount; + else + low = amount; + } + + return QColor::fromHslF(hue, saturation, lightness + ((lighten ? 1.0f : 0.0f) - lightness) * high); +} + +static QColor MakeMutedTextColor(const QColor& text, const QColor& background) +{ + if (GetContrastRatio(text, background) <= MINIMUM_TEXT_CONTRAST) + return text; + + // Move the text color towards the background as far as possible while keeping it readable. + float low = 0.0; + float high = 1.0; + for (u32 i = 0; i < 16; i++) + { + const float amount = (low + high) * 0.5f; + if (GetContrastRatio(BlendColors(text, background, amount), background) >= MINIMUM_TEXT_CONTRAST) + low = amount; + else + high = amount; + } + + return BlendColors(text, background, low); +} + +void LogWidget::updateColors() +{ + static constexpr std::array(Log::Color::MaxCount)> message_color_seeds = { + QColor(0x00, 0x00, 0x00), // Default (replaced with the theme text color below) + QColor(0x00, 0x00, 0x00), // Black (replaced with the theme text color below) + QColor(0xB4, 0x00, 0x00), // Red + QColor(0x13, 0xA1, 0x0E), // Green + QColor(0x00, 0x37, 0xDA), // Blue + QColor(0xA0, 0x00, 0xA0), // Magenta + QColor(0xA0, 0x78, 0x00), // Orange + QColor(0x80, 0xB4, 0xB4), // Cyan + QColor(0xB4, 0xB4, 0x80), // Yellow + QColor(0xCC, 0xCC, 0xCC), // White (replaced with the muted theme text color below) + QColor(0x76, 0x76, 0x76), // StrongBlack (replaced with the theme text color below) + QColor(0xE7, 0x48, 0x56), // StrongRed + QColor(0x16, 0xC6, 0x0C), // StrongGreen + QColor(0x3B, 0x78, 0xFF), // StrongBlue + QColor(0xB4, 0x00, 0x9E), // StrongMagenta + QColor(0xB4, 0x96, 0x00), // StrongOrange + QColor(0x61, 0xD6, 0xD6), // StrongCyan + QColor(0xF9, 0xF1, 0xA5), // StrongYellow + QColor(0xFF, 0xFF, 0xFF), // StrongWhite (replaced with the theme text color below) + }; + + const QPalette widget_palette = palette(); + const QColor window_color = widget_palette.color(QPalette::Window); + const QColor background_color = MakeOpaque(widget_palette.color(QPalette::Base), window_color); + const QColor text_color = EnsureTextContrast(widget_palette.color(QPalette::Text), background_color); + + for (size_t i = 0; i < m_message_colors.size(); i++) + m_message_colors[i] = EnsureTextContrast(message_color_seeds[i], background_color); + + m_timestamp_color = MakeMutedTextColor(text_color, background_color); + m_channel_color = text_color; + + m_message_colors[static_cast(Log::Color::Default)] = text_color; + m_message_colors[static_cast(Log::Color::Black)] = text_color; + m_message_colors[static_cast(Log::Color::White)] = m_timestamp_color; + m_message_colors[static_cast(Log::Color::StrongBlack)] = text_color; + m_message_colors[static_cast(Log::Color::StrongWhite)] = text_color; } void LogWidget::appendMessage(const QLatin1StringView& channel, quint32 cat, const QString& message) @@ -96,64 +235,14 @@ void LogWidget::realAppendMessage(const QLatin1StringView& channel, quint32 cat, { static constexpr const QChar level_characters[static_cast(Log::Level::MaxCount)] = {'X', 'E', 'W', 'I', 'V', 'D', 'B', 'T'}; - static constexpr const QColor message_colors[2][static_cast(Log::Color::MaxCount)] = { - // Light theme - { - QColor(0x00, 0x00, 0x00), // Default - QColor(0x00, 0x00, 0x00), // Black - QColor(0x70, 0x00, 0x00), // Red - QColor(0xec, 0x5e, 0xf1), // Green - QColor(0xe9, 0x39, 0xf3), // Blue - QColor(0xA0, 0x00, 0xA0), // Magenta - QColor(0xA0, 0x78, 0x00), // Orange - QColor(0x80, 0xB4, 0xB4), // Cyan - QColor(0xB4, 0xB4, 0x80), // Yellow - QColor(0x70, 0x70, 0x70), // White - QColor(0x00, 0x00, 0x00), // StrongBlack - QColor(0x80, 0x00, 0x00), // StrongRed - QColor(0x00, 0x80, 0x00), // StrongGreen - QColor(0x00, 0x00, 0x80), // StrongBlue - QColor(0xA0, 0x00, 0xA0), // StrongMagenta - QColor(0xA0, 0x78, 0x00), // StrongOrange - QColor(0x80, 0xB4, 0xB4), // StrongCyan - QColor(0xb4, 0xb4, 0x00), // StrongYellow - QColor(0x0D, 0x0d, 0x0D) // StrongWhite - }, - // Dark theme - { - QColor(0xD0, 0xD0, 0xD0), // Default - QColor(0xFF, 0xFF, 0xFF), // Black - QColor(0xB4, 0x00, 0x00), // Red - QColor(0x13, 0xA1, 0x0E), // Green - QColor(0x00, 0x37, 0xDA), // Blue - QColor(0xA0, 0x00, 0xA0), // Magenta - QColor(0xA0, 0x78, 0x00), // Orange - QColor(0x80, 0xB4, 0xB4), // Cyan - QColor(0xB4, 0xB4, 0x80), // Yellow - QColor(0xCC, 0xCC, 0xCC), // White - QColor(0xFF, 0xFF, 0xFF), // StrongBlack - QColor(0xE7, 0x48, 0x56), // StrongRed - QColor(0x16, 0xC6, 0x0C), // StrongGreen - QColor(0x20, 0x20, 0xCC), // StrongBlue - QColor(0xA0, 0x00, 0xA0), // StrongMagenta - QColor(0xB4, 0x96, 0x00), // StrongOrange - QColor(0x80, 0xB4, 0xB4), // StrongCyan - QColor(0xF9, 0xF1, 0xA5), // StrongYellow - QColor(0xFF, 0xFF, 0xFF), // StrongWhite - }, - }; - static constexpr const QColor timestamp_color[2] = {QColor(0x60, 0x60, 0x60), QColor(0xcc, 0xcc, 0xcc)}; - static constexpr const QColor channel_color[2] = {QColor(0x30, 0x30, 0x30), QColor(0xf2, 0xf2, 0xf2)}; - QTextCharFormat format = temp_cursor.charFormat(); - const size_t dark = static_cast(m_is_dark_theme); temp_cursor.beginEditBlock(); if (Log::AreConsoleOutputTimestampsEnabled()) { const float message_time = Log::GetCurrentMessageTime(); const QString qtimestamp = QStringLiteral("[%1] ").arg(message_time, 10, 'f', 4); - format.setForeground(QBrush(timestamp_color[dark])); + format.setForeground(QBrush(m_timestamp_color)); temp_cursor.setCharFormat(format); temp_cursor.insertText(qtimestamp); } @@ -166,12 +255,12 @@ void LogWidget::realAppendMessage(const QLatin1StringView& channel, quint32 cat, (level <= Log::Level::Warning) ? QStringLiteral("%1(%2): ").arg(level_characters[static_cast(level)]).arg(channel) : QStringLiteral("%1/%2: ").arg(level_characters[static_cast(level)]).arg(channel); - format.setForeground(QBrush(channel_color[dark])); + format.setForeground(QBrush(m_channel_color)); temp_cursor.setCharFormat(format); temp_cursor.insertText(qchannel); // message has \n already - format.setForeground(QBrush(message_colors[dark][static_cast(color)])); + format.setForeground(QBrush(m_message_colors[static_cast(color)])); temp_cursor.setCharFormat(format); temp_cursor.insertText(message); temp_cursor.endEditBlock(); diff --git a/src/duckstation-qt/logwindow.h b/src/duckstation-qt/logwindow.h index 08bb99250..0afec995c 100644 --- a/src/duckstation-qt/logwindow.h +++ b/src/duckstation-qt/logwindow.h @@ -5,9 +5,11 @@ #include "common/log.h" +#include #include #include +#include #include #include @@ -28,6 +30,7 @@ private: static constexpr int MAX_LINES = 1000; static constexpr int BLOCK_UPDATES_THRESHOLD = 100; + void updateColors(); void realAppendMessage(const QLatin1StringView& channel, quint32 cat, const QString& message); static void logCallback(void* pUserParam, Log::MessageCategory cat, const char* functionName, @@ -35,7 +38,9 @@ private: int m_lines_to_skip = 0; - bool m_is_dark_theme = false; + std::array(Log::Color::MaxCount)> m_message_colors; + QColor m_timestamp_color; + QColor m_channel_color; ALIGN_TO_CACHE_LINE std::atomic_int m_lines_pending{0}; };