aboutsummaryrefslogtreecommitdiffstats
path: root/src/tagchip.cpp
blob: 1ac7623bd957dc093793a5218afd0f99b15233d7 (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
/*
 * 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.
 */

#include "tagchip.h"

#include <QApplication>
#include <QFontMetrics>
#include <QPainter>

#include "tagcolors.h"
#include "threadlistmodel.h"

namespace TagChip {

QSize sizeFor(const QFontMetrics &metrics, const QString &text)
{
    return QSize(metrics.horizontalAdvance(text) + kPaddingX * 2,
                 metrics.height() + kPaddingY * 2);
}

void paint(QPainter *painter, const QRect &rect, const QString &text,
           const QColor &background)
{
    painter->save();
    painter->setRenderHint(QPainter::Antialiasing, true);
    painter->setPen(Qt::NoPen);
    painter->setBrush(background);
    painter->drawRoundedRect(rect, kRadius, kRadius);

    painter->setPen(TagColors::textColourOn(background));
    painter->drawText(rect, Qt::AlignCenter, text);
    painter->restore();
}

}  // namespace TagChip

void SubjectDelegate::initStyleOption(QStyleOptionViewItem *option,
                                      const QModelIndex &index) const
{
    QStyledItemDelegate::initStyleOption(option, index);

    // Qt resolves Qt::ForegroundRole into the palette's Text roles, and its
    // own painting then prefers those over HighlightedText: a model that
    // supplies a foreground wins even on a selected row.
    //
    // That is wrong for the read/unread dimming. A read thread's colour is
    // blended against the UNSELECTED background, so over the selection
    // highlight it lands as grey on purple, near unreadable. The highlight
    // already says "this row", so the dimming can yield to it while selected.
    //
    // Doomed threads are unaffected in practice: their fill is drawn beneath
    // the selection and their white is a contrast requirement, which
    // HighlightedText also satisfies.
    if (option->state & QStyle::State_Selected) {
        const QColor highlighted =
            option->palette.color(QPalette::HighlightedText);
        option->palette.setColor(QPalette::Text, highlighted);
        option->palette.setColor(QPalette::WindowText, highlighted);
    }
}

void SubjectDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
                            const QModelIndex &index) const
{
    const QString account =
        index.data(ThreadListModel::AccountLabelRole).toString();
    if (account.isEmpty()) {
        QStyledItemDelegate::paint(painter, option, index);
        return;
    }

    // Draw the row's own background and selection first, then the chip and the
    // subject on top, so a selected or struck-through row still looks right.
    QStyleOptionViewItem chrome = option;
    initStyleOption(&chrome, index);
    chrome.text.clear();
    const QWidget *widget = option.widget;
    QStyle *style = widget ? widget->style() : QApplication::style();
    style->drawControl(QStyle::CE_ItemViewItem, &chrome, painter, widget);

    const QFontMetrics metrics(option.font);
    const QSize chipSize = TagChip::sizeFor(metrics, account);
    const QRect chipRect(option.rect.left() + TagChip::kSpacing,
                         option.rect.top()
                             + (option.rect.height() - chipSize.height()) / 2,
                         chipSize.width(), chipSize.height());

    const QColor colour =
        index.data(ThreadListModel::AccountColourRole).value<QColor>();
    TagChip::paint(painter, chipRect, account,
                   colour.isValid() ? colour : QColor(0x55, 0x55, 0x5f));

    // The subject follows the chip, elided so a long one cannot overflow.
    QRect textRect = option.rect;
    textRect.setLeft(chipRect.right() + TagChip::kSpacing * 2);
    if (textRect.width() <= 0)
        return;

    painter->save();
    // Selection outranks the model's colour, and that order matters. A read
    // thread carries a dimmed foreground blended against the UNSELECTED
    // background, so painting it over the highlight leaves grey-on-purple,
    // which is close to unreadable. The highlight already carries the "this
    // row" signal, so the read/unread distinction can yield to it for as long
    // as the row is selected.
    //
    // A doomed thread is the exception that proves the rule: its white is not
    // a dimming but a contrast requirement against its own fill, and the fill
    // is drawn under the selection too.
    const QVariant foreground = index.data(Qt::ForegroundRole);
    if (option.state & QStyle::State_Selected)
        painter->setPen(option.palette.highlightedText().color());
    else if (foreground.isValid())
        painter->setPen(foreground.value<QBrush>().color());
    else
        painter->setPen(option.palette.text().color());

    // The model's font carries bold for unread and strike-out for deleted.
    // initStyleOption() already resolved it into chrome.font; using it rather
    // than option.font is what keeps those cues on a delegate-drawn subject.
    const QVariant fontData = index.data(Qt::FontRole);
    const QFont rowFont = fontData.isValid() ? fontData.value<QFont>()
                                             : chrome.font;
    painter->setFont(rowFont);
    const QFontMetrics rowMetrics(rowFont);
    painter->drawText(textRect, Qt::AlignVCenter | Qt::AlignLeft,
                      rowMetrics.elidedText(index.data(Qt::DisplayRole).toString(),
                                            Qt::ElideRight, textRect.width()));
    painter->restore();
}

QSize SubjectDelegate::sizeHint(const QStyleOptionViewItem &option,
                                const QModelIndex &index) const
{
    QSize size = QStyledItemDelegate::sizeHint(option, index);
    const QString account =
        index.data(ThreadListModel::AccountLabelRole).toString();
    if (!account.isEmpty()) {
        const QFontMetrics metrics(option.font);
        size.setWidth(size.width() + TagChip::sizeFor(metrics, account).width()
                      + TagChip::kSpacing * 3);
    }
    return size;
}