presentation/DesignTokens.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "DesignTokens.h" | ||
| 2 | #include <QFontDatabase> | ||
| 3 | #include <QFile> | ||
| 4 | #include <QFont> | ||
| 5 | #include <QFontMetrics> | ||
| 6 | #include <QGuiApplication> | ||
| 7 | #include <QJsonDocument> | ||
| 8 | #include <QStyleHints> | ||
| 9 | #include <QColor> | ||
| 10 | #include <QRegularExpression> | ||
| 11 | #include <array> | ||
| 12 | #include <cmath> | ||
| 13 | |||
| 14 | namespace { | ||
| 15 | 494500 | double luminance(const QColor &color) { | |
| 16 |
2/2✓ Branch 2 → 3 taken 10013 times.
✓ Branch 2 → 4 taken 1473487 times.
|
1483500 | const auto linear = [](double c) { return c <= .04045 ? c / 12.92 : std::pow((c + .055) / 1.055, 2.4); }; |
| 17 | 494500 | return .2126 * linear(color.redF()) + .7152 * linear(color.greenF()) + .0722 * linear(color.blueF()); | |
| 18 | } | ||
| 19 | 247250 | double contrast(const QColor &a, const QColor &b) { | |
| 20 | 247250 | const double first = luminance(a), second = luminance(b); | |
| 21 | 247250 | return (qMax(first, second) + .05) / (qMin(first, second) + .05); | |
| 22 | } | ||
| 23 | 231636 | QColor blend(const QColor &base, const QColor &tint, double amount) { | |
| 24 | 463272 | return QColor(qRound(base.red() * (1 - amount) + tint.red() * amount), | |
| 25 | 231636 | qRound(base.green() * (1 - amount) + tint.green() * amount), | |
| 26 | 694908 | qRound(base.blue() * (1 - amount) + tint.blue() * amount)); | |
| 27 | } | ||
| 28 | // OKLab distance × 100 (Björn Ottosson's matrices), the measure of | ||
| 29 | // docs/design/sprint-101/chart_palette.py. | ||
| 30 | 3083 | double oklabDistance(const QColor &a, const QColor &b) { | |
| 31 | 6166 | const auto lab = [](const QColor &color) { | |
| 32 |
2/2✓ Branch 2 → 3 taken 1508 times.
✓ Branch 2 → 4 taken 16990 times.
|
18498 | const auto linear = [](double c) { return c <= .04045 ? c / 12.92 : std::pow((c + .055) / 1.055, 2.4); }; |
| 33 | 6166 | const double r = linear(color.redF()), g = linear(color.greenF()), bl = linear(color.blueF()); | |
| 34 | 6166 | const double l = std::cbrt(.4122214708 * r + .5363325363 * g + .0514459929 * bl); | |
| 35 | 6166 | const double m = std::cbrt(.2119034982 * r + .6806995451 * g + .1073969566 * bl); | |
| 36 | 6166 | const double s = std::cbrt(.0883024619 * r + .2817188376 * g + .6299787005 * bl); | |
| 37 | 6166 | return std::array<double, 3>{.2104542553 * l + .7936177850 * m - .0040720468 * s, | |
| 38 | 6166 | 1.9779984951 * l - 2.4285922050 * m + .4505937099 * s, | |
| 39 | 6166 | .0259040371 * l + .7827717662 * m - .8086757660 * s}; | |
| 40 | }; | ||
| 41 | 3083 | const auto x = lab(a), y = lab(b); | |
| 42 | 3083 | return 100 * std::sqrt(std::pow(x[0] - y[0], 2) + std::pow(x[1] - y[1], 2) + std::pow(x[2] - y[2], 2)); | |
| 43 | } | ||
| 44 | // SD-14: the timeline's bar colour for an accent on the timeline card — | ||
| 45 | // the lightest tint with 3:1 against the card, desaturated towards the | ||
| 46 | // neutral `secondary` only as far as needed to stay ΔE ≥ 16 from the accent | ||
| 47 | // (hover, focus and the selected bucket use the accent itself). | ||
| 48 | 1432 | QColor chartBarFor(const QColor &card, const QColor &accent, const QColor &neutral) { | |
| 49 | 1432 | QColor bar; | |
| 50 | 1432 | double tint = .05; | |
| 51 |
2/2✓ Branch 18 → 3 taken 3083 times.
✓ Branch 18 → 19 taken 83 times.
|
3166 | for (int step = 0; step <= 12; ++step) { |
| 52 | 3083 | const QColor base = blend(accent, neutral, step * .05); | |
| 53 | 3083 | tint = .05; | |
| 54 |
6/7✓ Branch 6 → 7 taken 184481 times.
✗ Branch 6 → 11 not taken.
✓ Branch 8 → 9 taken 184481 times.
✓ Branch 9 → 10 taken 181398 times.
✓ Branch 9 → 11 taken 3083 times.
✓ Branch 12 → 5 taken 181398 times.
✓ Branch 12 → 13 taken 3083 times.
|
184481 | while (tint < 1 && contrast(blend(card, base, tint), card) < 3.0) tint += .01; |
| 55 | 3083 | bar = blend(card, base, tint); | |
| 56 |
3/3✓ Branch 14 → 15 taken 3083 times.
✓ Branch 15 → 16 taken 1349 times.
✓ Branch 15 → 17 taken 1734 times.
|
3083 | if (oklabDistance(bar, accent) >= 16) break; |
| 57 | } | ||
| 58 | 1432 | return bar; | |
| 59 | } | ||
| 60 | } | ||
| 61 | |||
| 62 |
1/1✓ Branch 15 → 16 taken 894 times.
|
2682 | DesignTokens::DesignTokens(QObject *parent) : QObject(parent) { |
| 63 |
1/1✓ Branch 16 → 17 taken 894 times.
|
894 | Q_INIT_RESOURCE(precision_tokens); |
| 64 |
1/1✓ Branch 20 → 21 taken 894 times.
|
894 | QFile file(QStringLiteral(":/themes/precision.json")); |
| 65 |
2/3✓ Branch 24 → 25 taken 894 times.
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 28 taken 894 times.
|
894 | if (!file.open(QIODevice::ReadOnly)) qFatal("Missing Precision design tokens"); |
| 66 |
4/4✓ Branch 28 → 29 taken 894 times.
✓ Branch 29 → 30 taken 894 times.
✓ Branch 30 → 31 taken 894 times.
✓ Branch 31 → 32 taken 894 times.
|
894 | m_palettes = QJsonDocument::fromJson(file.readAll()).toVariant().toMap(); |
| 67 |
9/18✓ Branch 37 → 38 taken 894 times.
✓ Branch 38 → 39 taken 894 times.
✓ Branch 39 → 40 taken 894 times.
✗ Branch 39 → 43 not taken.
✓ Branch 40 → 41 taken 894 times.
✓ Branch 41 → 42 taken 894 times.
✗ Branch 42 → 43 not taken.
✓ Branch 42 → 44 taken 894 times.
✓ Branch 45 → 46 taken 894 times.
✗ Branch 45 → 47 not taken.
✓ Branch 47 → 48 taken 894 times.
✗ Branch 47 → 49 not taken.
✗ Branch 49 → 50 not taken.
✓ Branch 49 → 52 taken 894 times.
✗ Branch 79 → 80 not taken.
✗ Branch 79 → 81 not taken.
✗ Branch 83 → 84 not taken.
✗ Branch 83 → 85 not taken.
|
894 | if (!m_palettes.contains("light") || !m_palettes.contains("dark")) qFatal("Invalid Precision design tokens"); |
| 68 |
1/1✓ Branch 53 → 54 taken 894 times.
|
894 | connect(QGuiApplication::styleHints(), &QStyleHints::colorSchemeChanged, this, |
| 69 |
1/3✗ Branch 3 → 4 not taken.
✗ Branch 3 → 5 not taken.
✓ Branch 52 → 53 taken 894 times.
|
894 | [this] { if (m_theme == "system") emit changed(); }); |
| 70 |
2/2✓ Branch 55 → 56 taken 894 times.
✓ Branch 56 → 57 taken 894 times.
|
894 | connect(QGuiApplication::styleHints(), &QStyleHints::wheelScrollLinesChanged, this, &DesignTokens::changed); |
| 71 | // B3: a live desktop preference change re-evaluates a "system" policy. | ||
| 72 | 894 | connect(&m_motion, &SystemMotionPreference::changed, | |
| 73 |
1/1✓ Branch 58 → 59 taken 894 times.
|
894 | this, &DesignTokens::refreshMotionFromSystem); |
| 74 | 894 | } | |
| 75 | 2840499 | QVariantMap DesignTokens::palette() const { | |
| 76 |
4/4✓ Branch 3 → 4 taken 2545300 times.
✓ Branch 3 → 9 taken 295199 times.
✓ Branch 5 → 6 taken 1942113 times.
✓ Branch 5 → 10 taken 603187 times.
|
4782612 | const bool dark = m_theme == "dark" || (m_theme == "system" && |
| 77 |
3/4✓ Branch 6 → 7 taken 1942113 times.
✓ Branch 7 → 8 taken 1942113 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 1942113 times.
|
1942113 | QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark); |
| 78 |
7/11✓ Branch 11 → 12 taken 295199 times.
✓ Branch 11 → 15 taken 2545300 times.
✓ Branch 18 → 19 taken 2840499 times.
✓ Branch 20 → 21 taken 2545300 times.
✓ Branch 20 → 22 taken 295199 times.
✓ Branch 22 → 23 taken 295199 times.
✓ Branch 22 → 24 taken 2545300 times.
✗ Branch 313 → 314 not taken.
✗ Branch 313 → 315 not taken.
✗ Branch 317 → 318 not taken.
✗ Branch 317 → 319 not taken.
|
5680998 | const QString key = (dark ? QStringLiteral("dark") : QStringLiteral("light")) + m_accentColor; |
| 79 |
2/2✓ Branch 25 → 26 taken 2839067 times.
✓ Branch 25 → 27 taken 1432 times.
|
2840499 | if (key == m_paletteKey) return m_cachedPalette; |
| 80 |
5/5✓ Branch 28 → 29 taken 313 times.
✓ Branch 28 → 30 taken 1119 times.
✓ Branch 31 → 32 taken 1432 times.
✓ Branch 32 → 33 taken 1432 times.
✓ Branch 33 → 34 taken 1432 times.
|
1432 | auto result = m_palettes.value(dark ? "dark" : "light").toMap(); |
| 81 |
2/2✓ Branch 38 → 39 taken 449 times.
✓ Branch 38 → 133 taken 983 times.
|
1432 | if (!m_accentColor.isEmpty()) { |
| 82 | 449 | const QColor requested(m_accentColor); | |
| 83 |
3/3✓ Branch 41 → 42 taken 449 times.
✓ Branch 42 → 43 taken 449 times.
✓ Branch 43 → 44 taken 449 times.
|
449 | const auto extremes = m_palettes.value("contrast").toMap(); |
| 84 |
6/6✓ Branch 48 → 49 taken 449 times.
✓ Branch 49 → 50 taken 449 times.
✓ Branch 50 → 51 taken 449 times.
✓ Branch 57 → 58 taken 449 times.
✓ Branch 58 → 59 taken 449 times.
✓ Branch 59 → 60 taken 449 times.
|
449 | const QColor light(extremes.value("light").toString()), darkEndpoint(extremes.value("dark").toString()); |
| 85 |
6/6✓ Branch 65 → 66 taken 449 times.
✓ Branch 66 → 67 taken 449 times.
✓ Branch 67 → 68 taken 449 times.
✓ Branch 70 → 71 taken 449 times.
✓ Branch 72 → 73 taken 449 times.
✓ Branch 73 → 74 taken 449 times.
|
449 | result["selection"] = blend(QColor(result["surface"].toString()), requested, .12).name(); |
| 86 | // Keep the chosen hue while adapting its luminance to every surface | ||
| 87 | // on which accent text or focus indicators are drawn. Cache the result; | ||
| 88 | // QML palette reads must not repeat this bounded contrast calculation. | ||
| 89 | 449 | QColor accent = requested; | |
| 90 |
1/2✓ Branch 105 → 81 taken 29296 times.
✗ Branch 105 → 106 not taken.
|
29296 | for (int step = 0; step <= 255; ++step) { |
| 91 |
2/2✓ Branch 81 → 82 taken 14211 times.
✓ Branch 81 → 83 taken 15085 times.
|
29296 | accent = blend(requested, dark ? light : darkEndpoint, step / 255.0); |
| 92 | 29296 | bool readable = true; | |
| 93 |
2/2✓ Branch 101 → 87 taken 117184 times.
✓ Branch 101 → 102 taken 29296 times.
|
146480 | for (const auto *background : {"surface", "sidebar", "selection", "hover"}) |
| 94 |
12/16✓ Branch 87 → 88 taken 54663 times.
✓ Branch 87 → 95 taken 62521 times.
✓ Branch 88 → 89 taken 54663 times.
✓ Branch 89 → 90 taken 54663 times.
✓ Branch 90 → 91 taken 54663 times.
✓ Branch 92 → 93 taken 54663 times.
✓ Branch 93 → 94 taken 25816 times.
✓ Branch 93 → 95 taken 28847 times.
✓ Branch 96 → 97 taken 54663 times.
✓ Branch 96 → 98 taken 62521 times.
✓ Branch 98 → 99 taken 54663 times.
✓ Branch 98 → 100 taken 62521 times.
✗ Branch 378 → 379 not taken.
✗ Branch 378 → 380 not taken.
✗ Branch 382 → 383 not taken.
✗ Branch 382 → 384 not taken.
|
117184 | readable = readable && contrast(accent, QColor(result[background].toString())) >= 4.5; |
| 95 |
2/2✓ Branch 102 → 103 taken 449 times.
✓ Branch 102 → 104 taken 28847 times.
|
29296 | if (readable) break; |
| 96 | } | ||
| 97 |
6/6✓ Branch 106 → 107 taken 449 times.
✓ Branch 108 → 109 taken 449 times.
✓ Branch 109 → 110 taken 449 times.
✓ Branch 111 → 112 taken 449 times.
✓ Branch 112 → 113 taken 449 times.
✓ Branch 113 → 114 taken 449 times.
|
449 | result["accent"] = result["focus"] = accent.name(); |
| 98 |
4/4✓ Branch 118 → 119 taken 449 times.
✓ Branch 119 → 120 taken 449 times.
✓ Branch 125 → 126 taken 449 times.
✓ Branch 126 → 127 taken 449 times.
|
1347 | result["onAccent"] = (contrast(accent, light) >= contrast(accent, darkEndpoint) |
| 99 |
3/3✓ Branch 120 → 121 taken 222 times.
✓ Branch 120 → 122 taken 227 times.
✓ Branch 123 → 124 taken 449 times.
|
1347 | ? light : darkEndpoint).name(); |
| 100 | 449 | } | |
| 101 |
8/8✓ Branch 133 → 134 taken 1432 times.
✓ Branch 134 → 135 taken 1432 times.
✓ Branch 135 → 136 taken 1432 times.
✓ Branch 136 → 137 taken 1432 times.
✓ Branch 137 → 138 taken 1432 times.
✓ Branch 138 → 139 taken 1432 times.
✓ Branch 139 → 140 taken 1432 times.
✓ Branch 140 → 141 taken 1432 times.
|
1432 | result["taskDoneFill"] = result["dueToday"] = result["accent"]; |
| 102 | // Sprint 101 SD-14 (timeline redesign, revises D-10): the bars are a | ||
| 103 | // quiet, solid tint of the accent on the timeline's card (surfaceSubtle); | ||
| 104 | // the full accent marks hover, keyboard focus and the selected bucket. | ||
| 105 | // docs/design/sprint-101/chart_palette.py checks every preset. | ||
| 106 |
3/3✓ Branch 144 → 145 taken 1432 times.
✓ Branch 145 → 146 taken 1432 times.
✓ Branch 146 → 147 taken 1432 times.
|
1432 | const QColor card(result["surfaceSubtle"].toString()); |
| 107 |
10/10✓ Branch 150 → 151 taken 1432 times.
✓ Branch 151 → 152 taken 1432 times.
✓ Branch 152 → 153 taken 1432 times.
✓ Branch 154 → 155 taken 1432 times.
✓ Branch 155 → 156 taken 1432 times.
✓ Branch 156 → 157 taken 1432 times.
✓ Branch 158 → 159 taken 1432 times.
✓ Branch 159 → 160 taken 1432 times.
✓ Branch 161 → 162 taken 1432 times.
✓ Branch 162 → 163 taken 1432 times.
|
1432 | result["chartBar"] = chartBarFor(card, QColor(result["accent"].toString()), QColor(result["secondary"].toString())).name(); |
| 108 |
8/8✓ Branch 171 → 172 taken 1432 times.
✓ Branch 172 → 173 taken 1432 times.
✓ Branch 173 → 174 taken 1432 times.
✓ Branch 174 → 175 taken 1432 times.
✓ Branch 175 → 176 taken 1432 times.
✓ Branch 176 → 177 taken 1432 times.
✓ Branch 177 → 178 taken 1432 times.
✓ Branch 178 → 179 taken 1432 times.
|
1432 | result["chartBarHover"] = result["chartBarSelected"] = result["accent"]; |
| 109 |
5/5✓ Branch 182 → 183 taken 1432 times.
✓ Branch 183 → 184 taken 1432 times.
✓ Branch 184 → 185 taken 1432 times.
✓ Branch 185 → 186 taken 1432 times.
✓ Branch 186 → 187 taken 1432 times.
|
1432 | result["chartBrush"] = result["selection"]; |
| 110 |
5/5✓ Branch 189 → 190 taken 1432 times.
✓ Branch 190 → 191 taken 1432 times.
✓ Branch 191 → 192 taken 1432 times.
✓ Branch 192 → 193 taken 1432 times.
✓ Branch 193 → 194 taken 1432 times.
|
1432 | result["chartAxis"] = result["divider"]; |
| 111 | // F94-60: every second task row, a quiet tint of the surface towards the | ||
| 112 | // text colour (3 %), so rows separate without divider lines. | ||
| 113 |
9/9✓ Branch 196 → 197 taken 1432 times.
✓ Branch 197 → 198 taken 1432 times.
✓ Branch 198 → 199 taken 1432 times.
✓ Branch 200 → 201 taken 1432 times.
✓ Branch 201 → 202 taken 1432 times.
✓ Branch 202 → 203 taken 1432 times.
✓ Branch 205 → 206 taken 1432 times.
✓ Branch 207 → 208 taken 1432 times.
✓ Branch 208 → 209 taken 1432 times.
|
1432 | result["rowAlternate"] = blend(QColor(result["surface"].toString()), QColor(result["text"].toString()), .03).name(); |
| 114 | // Sprint 108 §4.13: the composer's tokens, derived so every preset and | ||
| 115 | // custom accent keeps them readable (contrast values in DESIGN.md §16). | ||
| 116 |
6/6✓ Branch 217 → 218 taken 1432 times.
✓ Branch 218 → 219 taken 1432 times.
✓ Branch 219 → 220 taken 1432 times.
✓ Branch 223 → 224 taken 1432 times.
✓ Branch 224 → 225 taken 1432 times.
✓ Branch 225 → 226 taken 1432 times.
|
1432 | const QColor surface(result["surface"].toString()), accentColor(result["accent"].toString()); |
| 117 |
8/8✓ Branch 229 → 230 taken 1432 times.
✓ Branch 230 → 231 taken 1432 times.
✓ Branch 231 → 232 taken 1432 times.
✓ Branch 232 → 233 taken 1432 times.
✓ Branch 233 → 234 taken 1432 times.
✓ Branch 234 → 235 taken 1432 times.
✓ Branch 235 → 236 taken 1432 times.
✓ Branch 236 → 237 taken 1432 times.
|
1432 | result["spellError"] = result["chipInvalid"] = result["danger"]; |
| 118 |
5/5✓ Branch 240 → 241 taken 313 times.
✓ Branch 240 → 242 taken 1119 times.
✓ Branch 244 → 245 taken 1432 times.
✓ Branch 246 → 247 taken 1432 times.
✓ Branch 247 → 248 taken 1432 times.
|
1432 | result["freeBusyBusy"] = blend(accentColor, surface, dark ? .35 : .25).name(); |
| 119 |
3/3✓ Branch 253 → 254 taken 1432 times.
✓ Branch 255 → 256 taken 1432 times.
✓ Branch 256 → 257 taken 1432 times.
|
1432 | result["freeBusyTentative"] = blend(accentColor, surface, .55).name(); |
| 120 |
5/5✓ Branch 261 → 262 taken 1432 times.
✓ Branch 262 → 263 taken 1432 times.
✓ Branch 263 → 264 taken 1432 times.
✓ Branch 264 → 265 taken 1432 times.
✓ Branch 265 → 266 taken 1432 times.
|
1432 | result["freeBusyUnknown"] = result["border"]; |
| 121 |
5/5✓ Branch 268 → 269 taken 1432 times.
✓ Branch 269 → 270 taken 1432 times.
✓ Branch 270 → 271 taken 1432 times.
✓ Branch 271 → 272 taken 1432 times.
✓ Branch 272 → 273 taken 1432 times.
|
1432 | result["meetingSlot"] = result["accent"]; |
| 122 |
5/5✓ Branch 275 → 276 taken 1432 times.
✓ Branch 276 → 277 taken 1432 times.
✓ Branch 277 → 278 taken 1432 times.
✓ Branch 278 → 279 taken 1432 times.
✓ Branch 279 → 280 taken 1432 times.
|
1432 | result["codeBackground"] = result["surfaceSubtle"]; |
| 123 |
4/4✓ Branch 283 → 284 taken 1432 times.
✓ Branch 284 → 285 taken 1432 times.
✓ Branch 285 → 286 taken 1432 times.
✓ Branch 286 → 287 taken 1432 times.
|
1432 | result["composeTextColors"] = m_palettes.value("composeTextColors"); |
| 124 |
4/4✓ Branch 293 → 294 taken 1432 times.
✓ Branch 294 → 295 taken 1432 times.
✓ Branch 295 → 296 taken 1432 times.
✓ Branch 296 → 297 taken 1432 times.
|
1432 | result["composeHighlightColors"] = m_palettes.value("composeHighlightColors"); |
| 125 | 1432 | m_paletteKey = key; | |
| 126 | 1432 | m_cachedPalette = result; | |
| 127 | 1432 | return result; | |
| 128 | 2840499 | } | |
| 129 | 260 | double DesignTokens::contrastRatio(const QString &first, const QString &second) { | |
| 130 |
1/1✓ Branch 4 → 5 taken 260 times.
|
260 | return contrast(QColor(first), QColor(second)); |
| 131 | } | ||
| 132 | |||
| 133 | 1737 | QVariantMap DesignTokens::eventColors(const QString &calendarColor) const { | |
| 134 |
1/1✓ Branch 2 → 3 taken 1737 times.
|
1737 | const auto palette = this->palette(); |
| 135 |
3/3✓ Branch 3 → 4 taken 1737 times.
✓ Branch 4 → 5 taken 1737 times.
✓ Branch 5 → 6 taken 1737 times.
|
1737 | const bool dark = QColor(palette["surface"].toString()).lightnessF() < .5; |
| 136 |
3/3✓ Branch 12 → 13 taken 1737 times.
✓ Branch 13 → 14 taken 1737 times.
✓ Branch 14 → 15 taken 1737 times.
|
1737 | const auto tokens = m_palettes.value("calendar").toMap(); |
| 137 |
10/10✓ Branch 19 → 20 taken 1737 times.
✓ Branch 20 → 21 taken 1737 times.
✓ Branch 21 → 22 taken 1737 times.
✓ Branch 22 → 23 taken 335 times.
✓ Branch 22 → 24 taken 1402 times.
✓ Branch 26 → 27 taken 335 times.
✓ Branch 26 → 28 taken 1402 times.
✓ Branch 29 → 30 taken 1737 times.
✓ Branch 30 → 31 taken 1737 times.
✓ Branch 31 → 32 taken 1737 times.
|
1737 | const double alpha = tokens.value("eventTintAlpha").toMap().value(dark ? "dark" : "light", dark ? .28 : .18).toDouble(); |
| 138 |
3/3✓ Branch 40 → 41 taken 1737 times.
✓ Branch 41 → 42 taken 1737 times.
✓ Branch 42 → 43 taken 1737 times.
|
1737 | const double hoverStep = tokens.value("hoverTintStep", .06).toDouble(); |
| 139 | 1737 | QColor bar(calendarColor); | |
| 140 |
5/5✓ Branch 48 → 49 taken 22 times.
✓ Branch 48 → 57 taken 1715 times.
✓ Branch 49 → 50 taken 22 times.
✓ Branch 50 → 51 taken 22 times.
✓ Branch 51 → 52 taken 22 times.
|
1737 | if (!bar.isValid()) bar = QColor(palette["accent"].toString()); |
| 141 |
3/3✓ Branch 57 → 58 taken 1737 times.
✓ Branch 58 → 59 taken 1737 times.
✓ Branch 59 → 60 taken 1737 times.
|
1737 | const QColor surface(palette["surface"].toString()); |
| 142 | 1737 | const QColor background = blend(surface, bar, alpha); | |
| 143 | 1737 | const QColor hover = blend(surface, bar, alpha + hoverStep); | |
| 144 |
3/3✓ Branch 67 → 68 taken 1737 times.
✓ Branch 68 → 69 taken 1737 times.
✓ Branch 69 → 70 taken 1737 times.
|
1737 | const auto extremes = m_palettes.value("contrast").toMap(); |
| 145 |
20/32✓ Branch 73 → 74 taken 335 times.
✓ Branch 73 → 78 taken 1402 times.
✓ Branch 75 → 76 taken 335 times.
✓ Branch 76 → 77 taken 335 times.
✓ Branch 77 → 82 taken 335 times.
✓ Branch 79 → 80 taken 1402 times.
✓ Branch 80 → 81 taken 1402 times.
✓ Branch 81 → 82 taken 1402 times.
✓ Branch 84 → 85 taken 1402 times.
✓ Branch 84 → 86 taken 335 times.
✓ Branch 86 → 87 taken 1402 times.
✓ Branch 86 → 88 taken 335 times.
✓ Branch 88 → 89 taken 1402 times.
✓ Branch 88 → 90 taken 335 times.
✓ Branch 90 → 91 taken 335 times.
✓ Branch 90 → 92 taken 1402 times.
✓ Branch 92 → 93 taken 335 times.
✓ Branch 92 → 94 taken 1402 times.
✓ Branch 94 → 95 taken 335 times.
✓ Branch 94 → 96 taken 1402 times.
✗ Branch 207 → 208 not taken.
✗ Branch 207 → 209 not taken.
✗ Branch 211 → 212 not taken.
✗ Branch 211 → 213 not taken.
✗ Branch 215 → 216 not taken.
✗ Branch 215 → 217 not taken.
✗ Branch 219 → 220 not taken.
✗ Branch 219 → 221 not taken.
✗ Branch 223 → 224 not taken.
✗ Branch 223 → 225 not taken.
✗ Branch 227 → 228 not taken.
✗ Branch 227 → 229 not taken.
|
1737 | const QColor endpoint(dark ? extremes.value("light").toString() : extremes.value("dark").toString()); |
| 146 | // Move a candidate towards the theme's extreme until it reads on both | ||
| 147 | // backgrounds (bounded loop, at most 256 steps). | ||
| 148 | 3474 | const auto readable = [&](const QColor &candidate) { | |
| 149 |
1/2✓ Branch 14 → 3 taken 3474 times.
✗ Branch 14 → 15 not taken.
|
3474 | for (int step = 0; step <= 255; ++step) { |
| 150 | 3474 | const QColor color = blend(candidate, endpoint, step / 255.0); | |
| 151 |
5/8✓ Branch 4 → 5 taken 3474 times.
✓ Branch 5 → 6 taken 3474 times.
✗ Branch 5 → 9 not taken.
✓ Branch 6 → 7 taken 3474 times.
✓ Branch 7 → 8 taken 3474 times.
✗ Branch 7 → 9 not taken.
✓ Branch 10 → 11 taken 3474 times.
✗ Branch 10 → 12 not taken.
|
3474 | if (contrast(color, background) >= 4.5 && contrast(color, hover) >= 4.5) return color; |
| 152 | } | ||
| 153 | ✗ | return endpoint; | |
| 154 | 1737 | }; | |
| 155 | 1737 | return {{"background", background.name()}, {"hover", hover.name()}, {"bar", bar.name()}, | |
| 156 |
4/4✓ Branch 103 → 104 taken 1737 times.
✓ Branch 104 → 105 taken 1737 times.
✓ Branch 106 → 107 taken 1737 times.
✓ Branch 107 → 108 taken 1737 times.
|
3474 | {"text", readable(QColor(palette["text"].toString())).name()}, |
| 157 |
6/8✓ Branch 110 → 111 taken 1737 times.
✓ Branch 111 → 112 taken 1737 times.
✓ Branch 113 → 114 taken 1737 times.
✓ Branch 114 → 115 taken 1737 times.
✓ Branch 118 → 119 taken 8685 times.
✓ Branch 118 → 120 taken 1737 times.
✗ Branch 233 → 234 not taken.
✗ Branch 233 → 235 not taken.
|
13896 | {"secondaryText", readable(QColor(palette["secondary"].toString())).name()}}; |
| 158 | 6948 | } | |
| 159 | |||
| 160 | 1965 | bool DesignTokens::validAccentColor(const QString &color) { | |
| 161 |
4/7✓ Branch 2 → 3 taken 22 times.
✓ Branch 2 → 15 taken 1943 times.
✓ Branch 4 → 5 taken 22 times.
✗ Branch 4 → 15 not taken.
✓ Branch 9 → 10 taken 22 times.
✗ Branch 37 → 38 not taken.
✗ Branch 37 → 39 not taken.
|
1987 | static const QRegularExpression hex(QStringLiteral("^#[0-9a-fA-F]{6}$")); |
| 162 |
1/1✓ Branch 15 → 16 taken 1965 times.
|
1965 | const auto trimmed = color.trimmed(); |
| 163 |
8/10✓ Branch 17 → 18 taken 478 times.
✓ Branch 17 → 22 taken 1487 times.
✓ Branch 19 → 20 taken 478 times.
✓ Branch 20 → 21 taken 478 times.
✓ Branch 21 → 22 taken 464 times.
✓ Branch 21 → 23 taken 14 times.
✓ Branch 25 → 26 taken 478 times.
✓ Branch 25 → 27 taken 1487 times.
✗ Branch 40 → 41 not taken.
✗ Branch 40 → 42 not taken.
|
4408 | return trimmed.isEmpty() || hex.match(trimmed).hasMatch(); |
| 164 | 1965 | } | |
| 165 | 1239 | bool DesignTokens::applyAccentColor(const QString &color) { | |
| 166 |
3/3✓ Branch 2 → 3 taken 1239 times.
✓ Branch 3 → 4 taken 10 times.
✓ Branch 3 → 5 taken 1229 times.
|
1239 | if (!validAccentColor(color)) return false; |
| 167 |
2/2✓ Branch 5 → 6 taken 1229 times.
✓ Branch 6 → 7 taken 1229 times.
|
1229 | const auto canonical = color.trimmed().toLower(); |
| 168 |
2/2✓ Branch 9 → 10 taken 774 times.
✓ Branch 9 → 11 taken 455 times.
|
1229 | if (canonical == m_accentColor) return true; |
| 169 | 455 | m_accentColor = canonical; | |
| 170 |
1/1✓ Branch 12 → 13 taken 455 times.
|
455 | emit changed(); |
| 171 | 455 | return true; | |
| 172 | 1229 | } | |
| 173 | 1210 | void DesignTokens::setAccentColor(const QString &color) { applyAccentColor(color); } | |
| 174 | 1015 | void DesignTokens::setTheme(const QString &theme) { | |
| 175 |
9/10✓ Branch 3 → 4 taken 972 times.
✓ Branch 3 → 10 taken 43 times.
✓ Branch 5 → 6 taken 860 times.
✓ Branch 5 → 11 taken 112 times.
✓ Branch 7 → 8 taken 771 times.
✓ Branch 7 → 11 taken 89 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 771 times.
✓ Branch 12 → 13 taken 43 times.
✓ Branch 12 → 14 taken 972 times.
|
1015 | if (theme == m_theme || (theme != "light" && theme != "dark" && theme != "system")) return; |
| 176 | 972 | m_theme = theme; | |
| 177 | 972 | emit changed(); | |
| 178 | } | ||
| 179 | 1088 | void DesignTokens::setFontScale(double scale) { | |
| 180 |
2/2✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 1087 times.
|
1088 | if (!std::isfinite(scale)) return; |
| 181 | 1087 | scale = qBound(1.0, scale, 2.0); | |
| 182 |
2/2✓ Branch 7 → 8 taken 935 times.
✓ Branch 7 → 9 taken 152 times.
|
1087 | if (qFuzzyCompare(scale, m_scale)) return; |
| 183 | 152 | m_scale = scale; | |
| 184 | 152 | emit changed(); | |
| 185 | } | ||
| 186 | 46 | void DesignTokens::setReducedMotion(bool reduced) { | |
| 187 | // The manual boolean remains the legacy entry point; it always pins an | ||
| 188 | // explicit policy (even when the effective value already matches) so | ||
| 189 | // persistence records the user's deliberate choice. | ||
| 190 |
6/6✓ Branch 2 → 3 taken 42 times.
✓ Branch 2 → 6 taken 4 times.
✓ Branch 9 → 10 taken 4 times.
✓ Branch 9 → 11 taken 42 times.
✓ Branch 11 → 12 taken 42 times.
✓ Branch 11 → 13 taken 4 times.
|
92 | const QString policy = reduced ? QStringLiteral("reduced") : QStringLiteral("full"); |
| 191 |
5/6✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 17 taken 45 times.
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 17 not taken.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 20 taken 45 times.
|
46 | if (policy == m_motionPolicy && reduced == m_reducedMotion) return; |
| 192 | 45 | m_motionPolicy = policy; | |
| 193 | 45 | m_reducedMotion = reduced; | |
| 194 |
1/1✓ Branch 21 → 22 taken 45 times.
|
45 | emit changed(); |
| 195 | 46 | } | |
| 196 | |||
| 197 | 782 | void DesignTokens::applyMotionPolicy(const QString &policy) { | |
| 198 |
10/12✓ Branch 6 → 7 taken 8 times.
✓ Branch 6 → 18 taken 774 times.
✓ Branch 11 → 12 taken 2 times.
✓ Branch 11 → 18 taken 6 times.
✓ Branch 25 → 26 taken 8 times.
✓ Branch 25 → 27 taken 774 times.
✓ Branch 27 → 28 taken 782 times.
✗ Branch 27 → 29 not taken.
✓ Branch 29 → 30 taken 782 times.
✗ Branch 29 → 31 not taken.
✓ Branch 31 → 32 taken 1 time.
✓ Branch 31 → 33 taken 781 times.
|
2356 | if (policy != QStringLiteral("system") && policy != QStringLiteral("reduced") && |
| 199 |
8/8✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 18 taken 1 time.
✓ Branch 19 → 20 taken 2 times.
✓ Branch 19 → 21 taken 780 times.
✓ Branch 21 → 22 taken 2 times.
✓ Branch 21 → 23 taken 780 times.
✓ Branch 23 → 24 taken 8 times.
✓ Branch 23 → 25 taken 774 times.
|
784 | policy != QStringLiteral("full")) |
| 200 | 1 | return; | |
| 201 | 781 | bool effective = m_reducedMotion; | |
| 202 |
2/2✓ Branch 39 → 40 taken 6 times.
✓ Branch 39 → 41 taken 775 times.
|
781 | if (policy == QStringLiteral("reduced")) effective = true; |
| 203 |
2/2✓ Branch 47 → 48 taken 1 time.
✓ Branch 47 → 49 taken 774 times.
|
775 | else if (policy == QStringLiteral("full")) effective = false; |
| 204 |
1/1✓ Branch 49 → 50 taken 774 times.
|
774 | else effective = m_motion.systemReducedMotion().value_or(false); |
| 205 |
6/6✓ Branch 53 → 54 taken 773 times.
✓ Branch 53 → 56 taken 8 times.
✓ Branch 54 → 55 taken 772 times.
✓ Branch 54 → 56 taken 1 time.
✓ Branch 57 → 58 taken 772 times.
✓ Branch 57 → 59 taken 9 times.
|
781 | if (policy == m_motionPolicy && effective == m_reducedMotion) return; |
| 206 | 9 | m_motionPolicy = policy; | |
| 207 | 9 | m_reducedMotion = effective; | |
| 208 | 9 | emit changed(); | |
| 209 | } | ||
| 210 | |||
| 211 | 5 | void DesignTokens::refreshMotionFromSystem() { | |
| 212 |
2/2✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 10 taken 3 times.
|
5 | if (m_motionPolicy != QStringLiteral("system")) return; |
| 213 |
1/1✓ Branch 10 → 11 taken 3 times.
|
3 | const auto system = m_motion.systemReducedMotion(); |
| 214 | 3 | const bool reduced = system.value_or(false); | |
| 215 |
1/2✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 3 times.
|
3 | if (reduced == m_reducedMotion) return; |
| 216 | 3 | m_reducedMotion = reduced; | |
| 217 |
1/1✓ Branch 14 → 15 taken 3 times.
|
3 | emit changed(); |
| 218 | } | ||
| 219 | 1532841 | int DesignTokens::textSize() const { return qRound(13 * m_scale); } | |
| 220 | 463922 | int DesignTokens::metadataSize() const { return qRound(12 * m_scale); } | |
| 221 | // Typography scale (DESIGN.md §3 / decisions.md): 20 window brand, 18 view | ||
| 222 | // and reader headings, 15 secondary titles. QML must consume these tokens | ||
| 223 | // instead of repeating Math.round(N * fontScale). | ||
| 224 | 6276 | int DesignTokens::sectionSize() const { return qRound(15 * m_scale); } | |
| 225 | 27617 | int DesignTokens::headingSize() const { return qRound(18 * m_scale); } | |
| 226 | 1151 | int DesignTokens::titleSize() const { return qRound(20 * m_scale); } | |
| 227 | 815656 | int DesignTokens::rowHeight() const { | |
| 228 |
1/1✓ Branch 2 → 3 taken 815656 times.
|
815656 | QFont font = QGuiApplication::font(); |
| 229 |
1/1✓ Branch 4 → 5 taken 815656 times.
|
815656 | font.setPixelSize(textSize()); |
| 230 |
2/2✓ Branch 5 → 6 taken 815656 times.
✓ Branch 6 → 7 taken 815656 times.
|
1631312 | return qMax(20, QFontMetrics(font).height() + 4); |
| 231 | 815656 | } | |
| 232 |
1/1✓ Branch 2 → 3 taken 630381 times.
|
630381 | int DesignTokens::controlHeight() const { return qMax(28, rowHeight() + 8); } |
| 233 | 32022 | QString DesignTokens::monoFamily() const { | |
| 234 | 24 | static const QString family = [] { | |
| 235 |
1/1✓ Branch 2 → 3 taken 24 times.
|
24 | const QStringList installed = QFontDatabase::families(); |
| 236 |
2/2✓ Branch 18 → 13 taken 48 times.
✓ Branch 18 → 19 taken 24 times.
|
192 | for (const QString &candidate : {QStringLiteral("JetBrains Mono"), QStringLiteral("Cascadia Code")}) |
| 237 |
3/4✗ Branch 14 → 15 not taken.
✓ Branch 14 → 17 taken 48 times.
✓ Branch 21 → 22 taken 48 times.
✓ Branch 21 → 24 taken 24 times.
|
120 | if (installed.contains(candidate)) return candidate; |
| 238 |
2/2✓ Branch 25 → 27 taken 24 times.
✓ Branch 27 → 28 taken 24 times.
|
24 | return QFontDatabase::systemFont(QFontDatabase::FixedFont).family(); |
| 239 |
4/7✓ Branch 2 → 3 taken 24 times.
✓ Branch 2 → 9 taken 31998 times.
✓ Branch 4 → 5 taken 24 times.
✗ Branch 4 → 9 not taken.
✓ Branch 5 → 6 taken 24 times.
✗ Branch 12 → 13 not taken.
✗ Branch 12 → 14 not taken.
|
32046 | }(); |
| 240 | 32022 | return family; | |
| 241 | } | ||
| 242 |
2/2✓ Branch 2 → 3 taken 9 times.
✓ Branch 3 → 4 taken 9 times.
|
9 | int DesignTokens::wheelScrollLines() const { return qMax(0, QGuiApplication::styleHints()->wheelScrollLines()); } |
| 243 |