summaryrefslogtreecommitdiffstats
path: root/src/htmlbuilder.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-07 12:12:18 +0200
committerDanilo M. <danix@danix.xyz>2026-08-07 12:12:18 +0200
commit3826759d5167fb7a6f9449f9a39814c75771f449 (patch)
tree9fb1bc09bb89bd2c77a7c8c46247c577d8666cfd /src/htmlbuilder.cpp
parent5a4d8f5f021dc98b2a7cc471125aa3040c02675c (diff)
downloadqtmaildir-3826759d5167fb7a6f9449f9a39814c75771f449.tar.gz
qtmaildir-3826759d5167fb7a6f9449f9a39814c75771f449.zip
fix(ui): the message pane follows the desktop theme
The stylesheet hardcoded #bbb, #555, #000, #666, #ddd and #4a6f8a, and set no background at all, so on a dark desktop plain-text mail rendered as black on white inside a dark window and the web view's own default showed through. The colours now come from a Palette struct derived from QPalette, and are passed into the builder rather than read from qApp inside it, so the stylesheet can be tested against a known palette with no running application. Base and Text rather than Window and WindowText: the pane is a content surface like a text edit, and on many themes those differ. The secondary colours are blends of text and background, not fixed greys. That is the part that makes it work both ways round, since a #555 chosen to read as subtle on white is nearly invisible on #2b2b2b. The quote colour keeps its hue, because "this is quoted" is carried by being a different colour rather than a dimmer one, but is pulled toward the background so it stays readable instead of glowing on dark. A sender's own HTML is deliberately left alone, and a test asserts that so it cannot drift: rewriting a sender's styling would break layouts that depend on it, and a newsletter setting a white background is entitled to stay white. This themes the plain-text render and the chrome around messages, nothing more. MessageView passes its own widget palette rather than the application's, since a style sheet or a themed parent can give the pane different colours from qApp, and re-renders on PaletteChange: the document's colours are baked into its stylesheet at build time, so unlike a widget it does not restyle itself when the desktop theme changes. The load-bearing test asserts the negative, that no hex colour appears in the style block which the palette did not supply. A test checking only that the palette's colours are present passes with a leftover literal still there, and one leftover literal is the whole defect. Confirmed by mutation. Closes item 12. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'src/htmlbuilder.cpp')
-rw-r--r--src/htmlbuilder.cpp103
1 files changed, 91 insertions, 12 deletions
diff --git a/src/htmlbuilder.cpp b/src/htmlbuilder.cpp
index ccb9171..c8f039f 100644
--- a/src/htmlbuilder.cpp
+++ b/src/htmlbuilder.cpp
@@ -18,25 +18,92 @@
#include "htmlbuilder.h"
+#include <QCoreApplication>
+#include <QGuiApplication>
#include <QRegularExpression>
namespace {
-const char *kStyle = R"CSS(
-body { font-family: sans-serif; font-size: 10pt; margin: 12px; }
+/// The stylesheet, with every colour supplied by the caller.
+///
+/// %1 background, %2 text, %3 quote, %4 border, %5 dim.
+const char *kStyleTemplate = R"CSS(
+body { font-family: sans-serif; font-size: 10pt; margin: 12px;
+ background: %1; color: %2; }
pre.plain { white-space: pre-wrap; word-wrap: break-word;
font-family: monospace; margin: 0; }
-span.quote { color: #4a6f8a; }
-.message { border-top: 1px solid #bbb; padding: 10px 0; }
+span.quote { color: %3; }
+.message { border-top: 1px solid %4; padding: 10px 0; }
.message:first-child { border-top: none; }
-.msg-header { font-size: 9pt; color: #555; margin-bottom: 8px; }
-.msg-header .who { font-weight: bold; color: #000; }
-.stub { font-size: 9pt; color: #666; padding: 4px 0;
- border-top: 1px solid #ddd; }
+.msg-header { font-size: 9pt; color: %5; margin-bottom: 8px; }
+.msg-header .who { font-weight: bold; color: %2; }
+.stub { font-size: 9pt; color: %5; padding: 4px 0;
+ border-top: 1px solid %4; }
)CSS";
+/// Mixes two colours, `weight` being how much of `a` survives.
+///
+/// Blending is what makes the derived colours theme-correct. A fixed grey is
+/// only "subtle" against the background it was chosen for: #555 reads as a
+/// quiet label on white and nearly vanishes on near-black.
+QColor blend(const QColor &a, const QColor &b, qreal weight)
+{
+ const qreal inverse = 1.0 - weight;
+ return QColor::fromRgbF(a.redF() * weight + b.redF() * inverse,
+ a.greenF() * weight + b.greenF() * inverse,
+ a.blueF() * weight + b.blueF() * inverse);
+}
+
} // namespace
+HtmlBuilder::Palette HtmlBuilder::paletteFrom(const QPalette &palette)
+{
+ // Base and Text, not Window and WindowText: the pane is a content surface
+ // like a text edit, and on many themes Base differs from Window.
+ const QColor background = palette.color(QPalette::Base);
+ const QColor text = palette.color(QPalette::Text);
+
+ Palette result;
+ result.background = background;
+ result.text = text;
+
+ // Both derived from the pair, so they land at the right contrast whichever
+ // way round the theme is.
+ result.dim = blend(text, background, 0.6);
+ result.border = blend(text, background, 0.25);
+
+ // The quote colour keeps its hue, since "this is quoted" is carried by the
+ // colour being different rather than by it being dimmer, but it is pulled
+ // toward the background so it stays readable on a dark theme instead of
+ // glowing.
+ const QColor quoteHue(0x4a, 0x6f, 0x8a);
+ result.quote = background.lightnessF() < 0.5
+ ? blend(quoteHue.lighter(160), background, 0.75)
+ : blend(quoteHue, background, 0.85);
+
+ return result;
+}
+
+HtmlBuilder::Palette HtmlBuilder::defaultPalette()
+{
+ if (const QGuiApplication *app =
+ qobject_cast<QGuiApplication *>(QCoreApplication::instance()))
+ return paletteFrom(app->palette());
+
+ // No GUI application: only reachable from a test that did not pass a
+ // palette. A default-constructed QPalette is light, which matches what
+ // this code did before it was themed at all.
+ return paletteFrom(QPalette());
+}
+
+QString HtmlBuilder::styleSheet(const Palette &palette)
+{
+ return QString::fromUtf8(kStyleTemplate)
+ .arg(palette.background.name(), palette.text.name(),
+ palette.quote.name(), palette.border.name(),
+ palette.dim.name());
+}
+
QString HtmlBuilder::renderPlain(const QString &text)
{
QString out;
@@ -61,12 +128,12 @@ QString HtmlBuilder::renderPlain(const QString &text)
return out;
}
-QString HtmlBuilder::document(const QString &bodyHtml)
+QString HtmlBuilder::document(const QString &bodyHtml, const Palette &palette)
{
return QStringLiteral(
"<!DOCTYPE html><html><head><meta charset=\"utf-8\">"
"<style>%1</style></head><body>%2</body></html>")
- .arg(QString::fromUtf8(kStyle), bodyHtml);
+ .arg(styleSheet(palette), bodyHtml);
}
QString HtmlBuilder::namespaceCids(const QString &html, const QString &prefix)
@@ -218,14 +285,26 @@ QString HtmlBuilder::renderStub(const ParsedMessage &message)
QString HtmlBuilder::build(const ParsedMessage &message, Mode mode)
{
+ return build(message, mode, defaultPalette());
+}
+
+QString HtmlBuilder::build(const ParsedMessage &message, Mode mode,
+ const Palette &palette)
+{
ThreadRenderItem item;
item.message = message;
item.expanded = true;
- return document(renderBody(item, mode));
+ return document(renderBody(item, mode), palette);
}
QString HtmlBuilder::buildThread(const QList<ThreadRenderItem> &items, Mode mode)
{
+ return buildThread(items, mode, defaultPalette());
+}
+
+QString HtmlBuilder::buildThread(const QList<ThreadRenderItem> &items, Mode mode,
+ const Palette &palette)
+{
QString body;
for (int i = 0; i < items.size(); ++i) {
@@ -246,5 +325,5 @@ QString HtmlBuilder::buildThread(const QList<ThreadRenderItem> &items, Mode mode
renderBody(item, mode));
}
- return document(body);
+ return document(body, palette);
}