summaryrefslogtreecommitdiffstats
path: root/src/cardlayout.h
blob: de4c41f99c2b34a8f1c31da9b02ed5638e3ae397 (plain)
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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
/*
 * 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 <QDateTime>
#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.

        /// A QDateTime::toString() pattern from [general] date_format, or empty
        /// for the system's short format.
        ///
        /// It lives on the INPUT rather than being read where the date is
        /// drawn, because the width reserved for the date is computed from the
        /// same format inside compute(). A pattern reaching the painter but not
        /// the geometry is exactly how a longer date gets elided into a rect
        /// sized for a shorter one.
        QString dateFormat;

        /// Which marks line two carries (item 70).
        ///
        /// On the INPUT for the same reason dateFormat is: the marks were
        /// glyphs inside the subject STRING until item 70, so their width came
        /// free from the text metrics. Drawn as icons they occupy rects of
        /// their own, and a subject sized as though they were absent would run
        /// underneath them. The layout has to know they are there.
        bool flagged = false;
        bool hasAttachment = false;
        bool passed = false;
        bool replied = false;
    };

    /// 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 flagged mark, at the start of line two before the subject. Empty
    /// when the row is not flagged.
    QRect flagRect;

    /// The state marks after the subject, in this order: attachment, passed,
    /// replied. Each is empty when its state does not apply.
    ///
    /// Separate rects rather than one strip, because each is independently
    /// present or absent and a strip would have to encode which. They are laid
    /// out right to left from the expander, so the subject keeps whatever is
    /// left.
    QRect attachmentRect;
    QRect passedRect;
    QRect repliedRect;

    /// The side of a square mark on line two, derived from the card's font so
    /// the marks scale with the user's text size rather than being pinned to a
    /// pixel count that is right on one desktop only.
    static int markSide(const QFont &font);

    /// Gap between two adjacent marks, and between a mark and the subject.
    static constexpr int kMarkGap = 4;

    /// 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);

    /// The font a SIBLING's tag chip is drawn in: a size down again from
    /// smallFont().
    ///
    /// A card stands for one message but sits above a conversation, and shows
    /// both tiers (item 111). Size is what says which is which, so the two
    /// must be visibly different; taking one more step from the same base
    /// keeps it following the desktop's font rather than being fixed.
    ///
    /// Floored like smallFont(), and the floor really is reachable: a desktop
    /// at the minimum size gives both tiers the same size, which is a legible
    /// degradation rather than an illegible chip.
    static QFont siblingFont(const QFont &cardFont);

    static CardLayout compute(const Input &input, const QRect &rect,
                              const QFont &font);

    /// How a card writes a date, in the user's own locale.
    ///
    /// Never a hardcoded pattern. "yyyy-MM-dd hh:mm" is a US-looking format
    /// that an Italian desktop does not use, and the whole point of asking the
    /// system locale is that the user reads dates the way their desktop writes
    /// them everywhere else.
    ///
    /// Shared with the layout so the width reserved for the date and the text
    /// drawn into it come from one place: a locale whose short format is
    /// longer than the reserved rect would clip, which is exactly the fault
    /// bold text produced.
    /// `format` is a QDateTime::toString() pattern, or empty for the system's
    /// short format. Config validates it, so an unusable pattern never gets
    /// this far.
    static QString formatDate(const QDateTime &date,
                              const QString &format = QString());

    /// The widest string formatDate() can return, for reserving space.
    static QString widestDateSample(const QString &format = QString());

    /// The expander's label: the reply count with its glyph, as drawn.
    ///
    /// Shared with the layout for the same reason as formatDate: the rect
    /// reserved for the pill and the text put inside it must come from one
    /// place, or a count wider than the sample the layout guessed at spills
    /// out of its own background.
    ///
    /// `expanded` chooses which way the triangle points.
    static QString expanderLabel(int replyCount, bool expanded);

    /// Padding inside the expander pill, matching a tag chip's, so the two read
    /// as the same kind of object on the card.
    static constexpr int kPillPaddingX = 8;
};