1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
|
/*
* qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs
* Copyright (C) 2026 Danilo M. <danix@danix.xyz>
*
* 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.
*/
#pragma once
#include <QFont>
#include <QRect>
#include <QVector>
/// Where everything on a card goes, with no painting and no widget.
///
/// Split out from CardDelegate on purpose. A delegate needs a live QPainter and
/// an exposed view before it draws anything, which is what makes delegate tests
/// fragile: CLAUDE.md records that viewport()->render() returns a blank image in
/// several ordinary situations, and that a probe reporting "no ink anywhere" is
/// far more likely broken than the code it is testing. Every geometric claim
/// about a card is therefore made here, where a test is a function call.
///
/// The card is three lines, always:
///
/// sender ................................ date <- senderRect/dateRect
/// * subject @ v 3 replies <- subjectRect/expanderRect
/// [tag] [tag] <- tagRect
struct CardLayout
{
/// What the model says about the row. Deliberately plain data: the layout
/// must be computable in a test without a model or a view.
struct Input
{
bool isMessage = false;
int depth = 0; ///< 0 for a thread root, 1 for a direct reply.
int replyCount = 0; ///< 0 means no expander.
};
/// Width of the account accent bar down a thread card's left edge.
///
/// A starting value, not a settled one. Five accounts is enough that two
/// colours distinct as chips can read alike as thin stripes, and that can
/// only be judged against real cards on the user's own screen and theme
/// (Task 10). Widen it there if the accounts are not tellable apart.
static constexpr int kAccentWidth = 3;
/// Horizontal breathing room at the card's edges, measured from the accent
/// bar rather than from the card, so text does not sit on the colour.
static constexpr int kPaddingX = 8;
/// Vertical breathing room above the first line and below the last.
static constexpr int kPaddingY = 4;
/// How far one level of reply nesting indents.
static constexpr int kIndentStep = 18;
/// The depth past which nothing indents further.
///
/// A mailing-list chain can nest a dozen deep, and without a cap the
/// sender is eventually pushed off the right edge. Item 20 accepted that
/// deep chains must be capped in the VIEW rather than flattened in the
/// model, and this is that cap. Rows past it draw at this depth's indent
/// with no marker saying so.
static constexpr int kMaxDepth = 4;
QRect senderRect;
QRect dateRect;
QRect subjectRect;
QRect tagRect;
/// The reply count's rect, and the click target that toggles the thread.
/// Empty when the row has no replies.
QRect expanderRect;
/// The account accent bar down the card's left edge.
///
/// Thread cards only. A reply's account is its thread's, stated once at the
/// head of the conversation, and a second vertical line in a reply's gutter
/// would sit a few pixels from the spine and compete with it. The spine
/// carries the accent instead, so an expansion is bounded by one colour
/// without ever drawing two lines. Empty on a reply.
QRect accentRect;
/// One full-height vertical line per depth level, outermost first.
QVector<QRect> spines;
/// Where the card's text starts, after any indent.
int contentLeft = 0;
int totalHeight = 0;
/// The height EVERY row gets, thread and reply alike.
///
/// Uniform by design: it keeps setUniformRowHeights(true), which is the
/// single cheapest property of this layout, since no scrolling or
/// hit-testing arithmetic has to account for rows of differing size. The
/// cost is a blank third line on a card with no tags, which was accepted
/// explicitly.
static int heightFor(const QFont &font);
/// The font the tag chips and the reply count are drawn in: a size down
/// from the card's own, so they read as annotation rather than as a third
/// column of content.
static QFont smallFont(const QFont &cardFont);
static CardLayout compute(const Input &input, const QRect &rect,
const QFont &font);
};
|