summaryrefslogtreecommitdiffstats
path: root/src/cardlayout.h
blob: ef6a5631365e858b69b7fddc5d4408802fde9be7 (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
/*
 * 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.
    };

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

    /// 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.
    static QString formatDate(const QDateTime &date);

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

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