From 1faf94eb35e8270a659f215d260db73bcaa3f8d7 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 11 Aug 2026 20:24:05 +0200 Subject: feat(panes): draw the pane marks from shipped SVGs, not font glyphs Items 70 and 69, the second folded into the first as item 70's own size note predicted it should be. The panes drew their state marks as font glyphs: U+1F4CE for an attachment and U+2605 for a flagged thread, each with a fallback for a font that cannot render it. Both fell back to "*", so on such a font a flagged thread and one carrying an attachment were indistinguishable, which is a defect the fallback introduced rather than prevented. What a mark looks like was also the desktop's decision rather than this application's, and the panes are exactly where it should not be: the user asked for the toolbar and menus to keep following their icon theme while the panes stop. Six marks now ship in assets/icons/marks/: flagged, attachment, passed, replied and the two expander triangles. QIcon::fromTheme still resolves every toolbar and menu icon and was not touched. Licensing chose the shapes. The look came from a GPL3 icon theme, and this project is GPLv2-only, which are incompatible: GPLv2's "no further restrictions" clause bars shipping GPL3 assets in a v2-only work. The six were drawn fresh in the same idiom instead, with no path data copied. The idiom is generic: solid single-path silhouettes at 16x16 with no strokes. They are compiled in as string literals rather than loaded from a .qrc. src/CMakeLists.txt already records why resources belong to the executable: a qrc in the static library registers itself from a global initialiser the linker drops. The tests link the library, so a resource-based mark would be missing exactly where it needs asserting. assets/icons/marks/ stays the editable source. One asset serves both palettes. Every payload paints with fill="currentColor", which QSvgRenderer renders black rather than resolving, so Marks::pixmap composites the wanted colour with CompositionMode_SourceIn. A mark then takes the card's own pen colour and follows selection and the read/unread dimming without a second variant to keep in step. CardLayout reserves a rect per mark and CardDelegate paints into it. The marks were glyphs inside the subject STRING, so their width came free from the text metrics; as icons the geometry has to know they are there or the subject runs underneath them. The expander pill had the same trap, its triangle being a glyph in expanderLabel(), and now reserves that width explicitly. Item 69's part: passed and replied were words in the tag strip and are marks beside the subject now. The message pane's header carries the flagged and attachment marks next to the subject, per the user's decision that the right pane needs those two and only outside the message area. A duplicate that no test caught is worth recording. Every geometry assertion passed while a card showed passed as BOTH an arrow and a green tag chip: the chip filter had no reason to know a mark had appeared. It was found by rendering real cards to an image and looking at them. isDrawnAsAMark() is now one list consulted by both PillTagsRole and MessageOwnTagsRole, since two copies drifting apart is how a tag ends up drawn twice on one row and not at all on another. Fourteen tests: nine in test_marks, four in test_cardlayout, one in test_threadlistmodel. Mutation-checked at four points, each failing a test: the subject ignoring the marks, the flag not indenting the subject, the pill forgetting the triangle's width, and the recolour composite removed. Co-Authored-By: Claude Opus 5 --- src/marks.h | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/marks.h (limited to 'src/marks.h') diff --git a/src/marks.h b/src/marks.h new file mode 100644 index 0000000..823c129 --- /dev/null +++ b/src/marks.h @@ -0,0 +1,83 @@ +/* + * 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. + */ + +#ifndef QTMAILDIR_MARKS_H +#define QTMAILDIR_MARKS_H + +#include +#include +#include + +class QPainter; +class QRect; + +/// The pane marks: the small state icons drawn on a card and in the message +/// pane's header. +/// +/// These are qtmaildir's own, deliberately, and that is the point of item 70. +/// Toolbar and menu icons come from QIcon::fromTheme and follow the user's icon +/// theme; the panes must not, because a mark that changes shape under a theme +/// change is a mark whose meaning the application cannot state. They are also +/// no longer font glyphs: U+1F4CE and U+2605 render at the mercy of whatever +/// font the desktop supplies, and both fell back to a bare "*" on a font that +/// lacked them, which made a flagged thread and one with an attachment +/// indistinguishable. +/// +/// The SVG payloads are compiled in as string literals rather than loaded from +/// a .qrc, and the reason is in src/CMakeLists.txt: a qrc compiled into the +/// static library registers itself from a global initialiser that the linker +/// drops, so resources belong to the executable. The tests link the library, +/// not the executable, so a resource-based mark would be absent exactly where +/// it needs asserting. The editable originals live in assets/icons/marks/ and +/// are the source these were taken from. +namespace Marks { + +enum class Mark { + Attachment, + Flagged, + Passed, + Replied, + ExpanderCollapsed, + ExpanderExpanded, +}; + +/// The mark rendered at `size`, filled with `color`. +/// +/// Recoloured rather than shipped in light and dark variants: every payload +/// paints with fill="currentColor", which QSvgRenderer does not resolve, so the +/// colour is composited in. One asset then serves both palettes and cannot fall +/// out of step with itself. +/// +/// Cached by (mark, size, colour, devicePixelRatio). A delegate paints these on +/// every row of every repaint, and re-parsing six XML documents per frame is +/// the kind of cost that does not show up until a list is long. +QPixmap pixmap(Mark mark, const QSize &size, const QColor &color, + qreal devicePixelRatio = 1.0); + +/// Paints `mark` centred in `rect`, scaled to fit its shorter side. +void paint(QPainter *painter, const QRect &rect, Mark mark, + const QColor &color); + +/// The raw SVG payload, exposed for tests and for the message pane, which +/// embeds marks as data: URIs in the header label's rich text rather than +/// painting them. +QByteArray svg(Mark mark); + +} // namespace Marks + +#endif // QTMAILDIR_MARKS_H -- cgit v1.2.3