/* * qtmaildir - a Qt6 GUI for a local notmuch-indexed Maildir * Copyright (C) 2026 Danilo M. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "marks.h" #include #include #include #include namespace Marks { QByteArray svg(Mark mark) { // Compiled in rather than loaded from a .qrc, for the linker reason given // in marks.h. Generated from assets/icons/marks/*.svg, which stay the // editable originals: change the asset, regenerate, do not hand-edit here. switch (mark) { case Mark::Attachment: return QByteArray( " "); case Mark::Flagged: return QByteArray( " "); case Mark::Passed: return QByteArray( " "); case Mark::Replied: return QByteArray( " "); case Mark::ExpanderCollapsed: return QByteArray( " "); case Mark::ExpanderExpanded: return QByteArray( " "); } return {}; } namespace { /// Key for the pixmap cache. The colour belongs in it because the mark is /// recoloured per palette, and the ratio because a pixmap rendered for a 1x /// screen is blurry on a 2x one. struct CacheKey { Mark mark; int width; int height; QRgb color; int ratio; ///< devicePixelRatio scaled by 100, so it can be hashed. bool operator==(const CacheKey &other) const { return mark == other.mark && width == other.width && height == other.height && color == other.color && ratio == other.ratio; } }; size_t qHash(const CacheKey &key, size_t seed = 0) { return qHashMulti(seed, static_cast(key.mark), key.width, key.height, key.color, key.ratio); } } // namespace QPixmap pixmap(Mark mark, const QSize &size, const QColor &color, qreal devicePixelRatio) { if (size.isEmpty() || !color.isValid()) return {}; static QHash cache; const CacheKey key{ mark, size.width(), size.height(), color.rgba(), qRound(devicePixelRatio * 100) }; const auto cached = cache.constFind(key); if (cached != cache.constEnd()) return *cached; QPixmap pm(size * devicePixelRatio); pm.setDevicePixelRatio(devicePixelRatio); pm.fill(Qt::transparent); { QSvgRenderer renderer(svg(mark)); QPainter painter(&pm); painter.setRenderHint(QPainter::Antialiasing, true); renderer.render(&painter, QRectF(QPointF(0, 0), QSizeF(size))); // The payloads paint with fill="currentColor", which QSvgRenderer does // not resolve: it renders them black. SourceIn keeps the alpha the // shape just produced and replaces the colour, which is what makes one // asset serve both a light and a dark palette. painter.setCompositionMode(QPainter::CompositionMode_SourceIn); painter.fillRect(QRect(QPoint(0, 0), size), color); } // Unbounded in principle, bounded in practice: the marks are six, the sizes // come from a handful of font heights, and the colours from the palette. cache.insert(key, pm); return pm; } void paint(QPainter *painter, const QRect &rect, Mark mark, const QColor &color) { if (!painter || rect.isEmpty()) return; // Square, sized to the shorter side, so a mark never stretches. The rects // CardLayout reserves are square already; the message pane's are not // necessarily. const int side = qMin(rect.width(), rect.height()); const QSize size(side, side); const QPixmap pm = pixmap(mark, size, color, painter->device()->devicePixelRatioF()); if (pm.isNull()) return; const QPoint at(rect.left() + (rect.width() - side) / 2, rect.top() + (rect.height() - side) / 2); painter->drawPixmap(at, pm); } } // namespace Marks