mirror of
https://github.com/stenzek/duckstation.git
synced 2026-10-12 02:23:23 +00:00
Qt: Dynamically adjust log window colours to theme
Fixes unreadable text in some themes.
This commit is contained in:
@@ -14,6 +14,9 @@
|
||||
#include <QtWidgets/QMenuBar>
|
||||
#include <QtWidgets/QScrollBar>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#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<QColor, static_cast<size_t>(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<size_t>(Log::Color::Default)] = text_color;
|
||||
m_message_colors[static_cast<size_t>(Log::Color::Black)] = text_color;
|
||||
m_message_colors[static_cast<size_t>(Log::Color::White)] = m_timestamp_color;
|
||||
m_message_colors[static_cast<size_t>(Log::Color::StrongBlack)] = text_color;
|
||||
m_message_colors[static_cast<size_t>(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<size_t>(Log::Level::MaxCount)] = {'X', 'E', 'W', 'I',
|
||||
'V', 'D', 'B', 'T'};
|
||||
static constexpr const QColor message_colors[2][static_cast<size_t>(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<size_t>(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<size_t>(level)]).arg(channel) :
|
||||
QStringLiteral("%1/%2: ").arg(level_characters[static_cast<size_t>(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<size_t>(color)]));
|
||||
format.setForeground(QBrush(m_message_colors[static_cast<size_t>(color)]));
|
||||
temp_cursor.setCharFormat(format);
|
||||
temp_cursor.insertText(message);
|
||||
temp_cursor.endEditBlock();
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
|
||||
#include "common/log.h"
|
||||
|
||||
#include <QtGui/QColor>
|
||||
#include <QtWidgets/QMainWindow>
|
||||
#include <QtWidgets/QPlainTextEdit>
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <span>
|
||||
|
||||
@@ -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<QColor, static_cast<size_t>(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};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user