presentation/KeyOverlayLayout.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "KeyOverlayLayout.h" | ||
| 2 | |||
| 3 | #include <QHash> | ||
| 4 | #include <QLineF> | ||
| 5 | #include <algorithm> | ||
| 6 | #include <cmath> | ||
| 7 | |||
| 8 | namespace { | ||
| 9 | constexpr qreal kGap = 2.0; // between two badges | ||
| 10 | constexpr qreal kCalloutGap = 12.0; // between a badge and its callout | ||
| 11 | constexpr qreal kSpotPadding = 6.0; | ||
| 12 | constexpr qreal kSpotMerge = 8.0; | ||
| 13 | constexpr int kStaggerStepMs = 6; | ||
| 14 | constexpr int kStaggerMaxMs = 80; | ||
| 15 | constexpr int kCalloutLineMs = 180; | ||
| 16 | constexpr int kCalloutStepMs = 30; | ||
| 17 | |||
| 18 |
1/1✓ Branch 3 → 4 taken 48921 times.
|
48921 | qreal distance(const QPointF &a, const QPointF &b) { return QLineF(a, b).length(); } |
| 19 | |||
| 20 | 26278 | bool overlapsAny(const QRectF &rect, const QList<QRectF> &others, qreal gap) { | |
| 21 | 26278 | const QRectF grown = rect.adjusted(-gap, -gap, gap, gap); | |
| 22 |
1/1✓ Branch 5 → 6 taken 26278 times.
|
323900 | return std::any_of(others.cbegin(), others.cend(), [&](const QRectF &other) { return grown.intersects(other); }); |
| 23 | } | ||
| 24 | |||
| 25 | // Corner anchors in preference order per element kind (left/right as in a | ||
| 26 | // left-to-right layout; mirrored for right-to-left). | ||
| 27 | enum class Corner { TopLeft, TopRight, BottomLeft, BottomRight, RightCentre, LeftCentre }; | ||
| 28 | |||
| 29 | 5765 | QList<Corner> corners(KeyOverlayLayout::Kind kind) { | |
| 30 |
4/6✓ Branch 2 → 3 taken 5340 times.
✓ Branch 2 → 4 taken 104 times.
✗ Branch 2 → 5 not taken.
✓ Branch 2 → 6 taken 172 times.
✓ Branch 2 → 8 taken 149 times.
✗ Branch 2 → 9 not taken.
|
5765 | switch (kind) { |
| 31 | 5340 | case KeyOverlayLayout::Kind::Button: return {Corner::BottomRight, Corner::TopRight, Corner::BottomLeft, Corner::TopLeft}; | |
| 32 | 104 | case KeyOverlayLayout::Kind::Chip: return {Corner::TopRight, Corner::BottomRight, Corner::TopLeft, Corner::BottomLeft}; | |
| 33 | ✗ | case KeyOverlayLayout::Kind::Field: return {Corner::RightCentre, Corner::BottomRight, Corner::TopRight}; | |
| 34 |
1/1✓ Branch 6 → 7 taken 172 times.
|
172 | case KeyOverlayLayout::Kind::Pane: return {Corner::TopLeft, Corner::TopRight}; |
| 35 | 149 | case KeyOverlayLayout::Kind::Row: return {Corner::LeftCentre, Corner::RightCentre}; | |
| 36 | } | ||
| 37 | ✗ | return {Corner::BottomRight}; | |
| 38 | } | ||
| 39 | |||
| 40 | 4 | Corner mirrored(Corner corner) { | |
| 41 |
4/7✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 1 time.
✓ Branch 2 → 5 taken 1 time.
✓ Branch 2 → 6 taken 1 time.
✗ Branch 2 → 7 not taken.
✗ Branch 2 → 8 not taken.
✗ Branch 2 → 9 not taken.
|
4 | switch (corner) { |
| 42 | 1 | case Corner::TopLeft: return Corner::TopRight; | |
| 43 | 1 | case Corner::TopRight: return Corner::TopLeft; | |
| 44 | 1 | case Corner::BottomLeft: return Corner::BottomRight; | |
| 45 | 1 | case Corner::BottomRight: return Corner::BottomLeft; | |
| 46 | ✗ | case Corner::RightCentre: return Corner::LeftCentre; | |
| 47 | ✗ | case Corner::LeftCentre: return Corner::RightCentre; | |
| 48 | } | ||
| 49 | ✗ | return corner; | |
| 50 | } | ||
| 51 | |||
| 52 | // Outside kinds (buttons, chips) overlap their element by 30 % of the | ||
| 53 | // badge at a corner; inside kinds (panes, rows, fields) keep the badge 4 px | ||
| 54 | // inside the element. | ||
| 55 | 22418 | QRectF placeAt(const QRectF &element, const QSizeF &size, Corner corner, KeyOverlayLayout::Kind kind) { | |
| 56 | 22418 | const qreal w = size.width(), h = size.height(); | |
| 57 |
2/2✓ Branch 5 → 6 taken 21776 times.
✓ Branch 5 → 7 taken 298 times.
|
22074 | const bool inside = kind == KeyOverlayLayout::Kind::Pane || kind == KeyOverlayLayout::Kind::Row |
| 58 |
3/4✓ Branch 4 → 5 taken 22074 times.
✓ Branch 4 → 7 taken 344 times.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 21776 times.
|
44492 | || kind == KeyOverlayLayout::Kind::Field; |
| 59 |
2/2✓ Branch 9 → 10 taken 642 times.
✓ Branch 9 → 12 taken 21776 times.
|
22418 | const qreal left = inside ? element.left() + 4.0 : element.left() - 0.7 * w; |
| 60 |
2/2✓ Branch 14 → 15 taken 642 times.
✓ Branch 14 → 17 taken 21776 times.
|
22418 | const qreal right = inside ? element.right() - w - 4.0 : element.right() - 0.3 * w; |
| 61 |
2/2✓ Branch 19 → 20 taken 642 times.
✓ Branch 19 → 22 taken 21776 times.
|
22418 | const qreal top = inside ? element.top() + 4.0 : element.top() - 0.7 * h; |
| 62 |
2/2✓ Branch 24 → 25 taken 642 times.
✓ Branch 24 → 27 taken 21776 times.
|
22418 | const qreal bottom = inside ? element.bottom() - h - 4.0 : element.bottom() - 0.3 * h; |
| 63 | 22418 | const qreal middle = element.center().y() - h / 2; | |
| 64 |
6/7✓ Branch 31 → 32 taken 5616 times.
✓ Branch 31 → 33 taken 5616 times.
✓ Branch 31 → 34 taken 5444 times.
✓ Branch 31 → 35 taken 5444 times.
✓ Branch 31 → 36 taken 149 times.
✓ Branch 31 → 38 taken 149 times.
✗ Branch 31 → 40 not taken.
|
22418 | switch (corner) { |
| 65 | 5616 | case Corner::TopLeft: return {left, top, w, h}; | |
| 66 | 5616 | case Corner::TopRight: return {right, top, w, h}; | |
| 67 | 5444 | case Corner::BottomLeft: return {left, bottom, w, h}; | |
| 68 | 5444 | case Corner::BottomRight: return {right, bottom, w, h}; | |
| 69 | 149 | case Corner::RightCentre: return {element.right() - w - 4.0, middle, w, h}; | |
| 70 | 149 | case Corner::LeftCentre: return {element.left() + 4.0, middle, w, h}; | |
| 71 | } | ||
| 72 | ✗ | return {element.topLeft(), size}; | |
| 73 | } | ||
| 74 | |||
| 75 | 90956 | QRectF clampInto(QRectF rect, const QRectF &bounds) { | |
| 76 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 90956 times.
|
90956 | if (bounds.isEmpty()) return rect; |
| 77 |
1/2✓ Branch 7 → 8 taken 90956 times.
✗ Branch 7 → 18 not taken.
|
90956 | if (rect.width() <= bounds.width()) { |
| 78 |
2/2✓ Branch 10 → 11 taken 1030 times.
✓ Branch 10 → 13 taken 89926 times.
|
90956 | if (rect.left() < bounds.left()) rect.moveLeft(bounds.left()); |
| 79 |
2/2✓ Branch 15 → 16 taken 1491 times.
✓ Branch 15 → 18 taken 89465 times.
|
90956 | if (rect.right() > bounds.right()) rect.moveRight(bounds.right()); |
| 80 | } | ||
| 81 |
2/2✓ Branch 20 → 21 taken 90868 times.
✓ Branch 20 → 31 taken 88 times.
|
90956 | if (rect.height() <= bounds.height()) { |
| 82 |
2/2✓ Branch 23 → 24 taken 1535 times.
✓ Branch 23 → 26 taken 89333 times.
|
90868 | if (rect.top() < bounds.top()) rect.moveTop(bounds.top()); |
| 83 |
2/2✓ Branch 28 → 29 taken 2137 times.
✓ Branch 28 → 31 taken 88731 times.
|
90868 | if (rect.bottom() > bounds.bottom()) rect.moveBottom(bounds.bottom()); |
| 84 | } | ||
| 85 | 90956 | return rect; | |
| 86 | } | ||
| 87 | |||
| 88 | 10795 | bool segmentsCross(const QLineF &a, const QLineF &b) { | |
| 89 | 10795 | QPointF point; | |
| 90 |
2/3✓ Branch 2 → 3 taken 10795 times.
✓ Branch 3 → 4 taken 10795 times.
✗ Branch 3 → 5 not taken.
|
10795 | if (a.intersects(b, &point) != QLineF::BoundedIntersection) return false; |
| 91 | // Lines that only touch at their ends (two callouts of one badge) do not cross. | ||
| 92 | ✗ | const auto near = [&](const QPointF &p) { return distance(point, p) < 0.5; }; | |
| 93 | ✗ | return !(near(a.p1()) || near(a.p2())); | |
| 94 | } | ||
| 95 | |||
| 96 | 10058 | QPointF nearestPoint(const QRectF &rect, const QPointF &point) { | |
| 97 |
2/2✓ Branch 5 → 6 taken 10058 times.
✓ Branch 9 → 10 taken 10058 times.
|
10058 | return {std::clamp(point.x(), rect.left(), rect.right()), std::clamp(point.y(), rect.top(), rect.bottom())}; |
| 98 | } | ||
| 99 | } // namespace | ||
| 100 | |||
| 101 | 282 | int KeyOverlayLayout::badgeBudget(const QSizeF &window) { | |
| 102 | 282 | const qreal extra = window.width() * window.height() - 1280.0 * 800.0; | |
| 103 |
2/2✓ Branch 4 → 5 taken 104 times.
✓ Branch 4 → 6 taken 178 times.
|
282 | return 24 + (extra > 0 ? int(extra / 40000.0) : 0); |
| 104 | } | ||
| 105 | |||
| 106 | 281 | QList<QRectF> KeyOverlayLayout::spotlights(const QList<QRectF> &elements) { | |
| 107 | 281 | QList<QRectF> rects; | |
| 108 |
2/2✓ Branch 9 → 4 taken 5676 times.
✓ Branch 9 → 10 taken 281 times.
|
5957 | for (const QRectF &element : elements) |
| 109 |
1/1✓ Branch 6 → 7 taken 5676 times.
|
5676 | rects.append(element.adjusted(-kSpotPadding, -kSpotPadding, kSpotPadding, kSpotPadding)); |
| 110 | 281 | bool merged = true; | |
| 111 |
2/2✓ Branch 40 → 11 taken 2821 times.
✓ Branch 40 → 41 taken 281 times.
|
3102 | while (merged) { |
| 112 | 2821 | merged = false; | |
| 113 |
6/6✓ Branch 35 → 36 taken 14421 times.
✓ Branch 35 → 38 taken 376 times.
✓ Branch 36 → 37 taken 11976 times.
✓ Branch 36 → 38 taken 2445 times.
✓ Branch 39 → 12 taken 11976 times.
✓ Branch 39 → 40 taken 2821 times.
|
14797 | for (int i = 0; i < rects.size() && !merged; ++i) |
| 114 |
6/6✓ Branch 28 → 29 taken 150511 times.
✓ Branch 28 → 31 taken 9802 times.
✓ Branch 29 → 30 taken 148337 times.
✓ Branch 29 → 31 taken 2174 times.
✓ Branch 32 → 13 taken 148337 times.
✓ Branch 32 → 33 taken 11976 times.
|
160313 | for (int j = i + 1; j < rects.size() && !merged; ++j) { |
| 115 |
1/1✓ Branch 13 → 14 taken 148337 times.
|
148337 | const QRectF grown = rects[i].adjusted(-kSpotMerge, -kSpotMerge, kSpotMerge, kSpotMerge); |
| 116 |
3/3✓ Branch 15 → 16 taken 148337 times.
✓ Branch 17 → 18 taken 145797 times.
✓ Branch 17 → 19 taken 2540 times.
|
148337 | if (!grown.intersects(rects[j])) continue; |
| 117 |
3/3✓ Branch 19 → 20 taken 2540 times.
✓ Branch 20 → 21 taken 2540 times.
✓ Branch 22 → 23 taken 2540 times.
|
2540 | rects[i] = rects[i].united(rects[j]); |
| 118 |
1/1✓ Branch 23 → 24 taken 2540 times.
|
2540 | rects.removeAt(j); |
| 119 | 2540 | merged = true; | |
| 120 | } | ||
| 121 | } | ||
| 122 | 281 | return rects; | |
| 123 | ✗ | } | |
| 124 | |||
| 125 | 280 | KeyOverlayLayout::Result KeyOverlayLayout::compute(const QList<Entry> &entries, const Input &input) { | |
| 126 | 280 | Result result; | |
| 127 | 280 | const QRectF windowRect(QPointF(0, 0), input.window); | |
| 128 | 280 | const QRectF inner = windowRect.adjusted(2, 2, -2, -2); | |
| 129 |
2/2✓ Branch 6 → 7 taken 6 times.
✓ Branch 6 → 8 taken 274 times.
|
280 | const QPointF focusCentre = input.focus.isEmpty() ? windowRect.center() : input.focus.center(); |
| 130 | |||
| 131 | struct Ranked { | ||
| 132 | const Entry *entry; | ||
| 133 | QRectF visible; | ||
| 134 | qreal score; | ||
| 135 | }; | ||
| 136 | 280 | QList<Ranked> ranked; | |
| 137 |
2/2✓ Branch 32 → 11 taken 7843 times.
✓ Branch 32 → 33 taken 280 times.
|
8123 | for (const Entry &entry : entries) { |
| 138 |
2/2✓ Branch 13 → 14 taken 7251 times.
✓ Branch 13 → 15 taken 592 times.
|
7843 | const QRectF clip = entry.clip.isEmpty() ? windowRect : entry.clip & windowRect; |
| 139 | 7843 | const QRectF visible = entry.rect & clip; | |
| 140 |
5/6✓ Branch 18 → 19 taken 7841 times.
✓ Branch 18 → 21 taken 2 times.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 7841 times.
✓ Branch 23 → 24 taken 2 times.
✓ Branch 23 → 25 taken 7841 times.
|
7843 | if (visible.isEmpty() || entry.badgeSize.isEmpty()) continue; |
| 141 |
1/1✓ Branch 26 → 27 taken 7841 times.
|
7841 | const qreal nearness = 1.0 / (1.0 + distance(visible.center(), focusCentre) / 400.0); |
| 142 |
1/1✓ Branch 27 → 28 taken 7841 times.
|
7841 | ranked.append({&entry, visible, entry.priority * nearness}); |
| 143 | } | ||
| 144 | // The way around (areas, rules, settings, menu) is never crowded out by | ||
| 145 | // the many daily keys near the focus (user review 2026-10-12). | ||
| 146 |
3/3✓ Branch 33 → 34 taken 280 times.
✓ Branch 34 → 35 taken 280 times.
✓ Branch 35 → 36 taken 280 times.
|
280 | std::stable_sort(ranked.begin(), ranked.end(), [](const Ranked &a, const Ranked &b) { |
| 147 | 35391 | const bool pinnedA = a.entry->priority >= kPinned, pinnedB = b.entry->priority >= kPinned; | |
| 148 |
2/2✓ Branch 2 → 3 taken 1741 times.
✓ Branch 2 → 4 taken 33650 times.
|
35391 | if (pinnedA != pinnedB) return pinnedA; |
| 149 |
1/2✓ Branch 4 → 5 taken 33650 times.
✗ Branch 4 → 6 not taken.
|
33650 | if (a.score != b.score) return a.score > b.score; |
| 150 | ✗ | return a.entry->id < b.entry->id; | |
| 151 | }); | ||
| 152 | // Then each pane's best kPerPane, then everything else (each by score). | ||
| 153 | 280 | QHash<QString, int> perPane; | |
| 154 | 280 | QList<Ranked> pinned, reserved, rest; | |
| 155 |
2/2✓ Branch 57 → 39 taken 7841 times.
✓ Branch 57 → 58 taken 280 times.
|
8121 | for (const Ranked &rank : std::as_const(ranked)) { |
| 156 |
2/2✓ Branch 40 → 41 taken 421 times.
✓ Branch 40 → 42 taken 7420 times.
|
7841 | if (rank.entry->priority >= kPinned) { |
| 157 |
1/1✓ Branch 41 → 55 taken 421 times.
|
421 | pinned.append(rank); |
| 158 |
8/8✓ Branch 43 → 44 taken 675 times.
✓ Branch 43 → 48 taken 6745 times.
✓ Branch 44 → 45 taken 503 times.
✓ Branch 44 → 47 taken 172 times.
✓ Branch 46 → 47 taken 260 times.
✓ Branch 46 → 48 taken 243 times.
✓ Branch 49 → 50 taken 432 times.
✓ Branch 49 → 54 taken 6988 times.
|
7420 | } else if (!rank.entry->pane.isEmpty() && (rank.entry->kind == Kind::Pane || perPane.value(rank.entry->pane) < kPerPane)) { |
| 159 |
3/3✓ Branch 50 → 51 taken 260 times.
✓ Branch 50 → 53 taken 172 times.
✓ Branch 51 → 52 taken 260 times.
|
432 | if (rank.entry->kind != Kind::Pane) ++perPane[rank.entry->pane]; // the pane's own label is extra |
| 160 |
1/1✓ Branch 53 → 55 taken 432 times.
|
432 | reserved.append(rank); |
| 161 | } else { | ||
| 162 |
1/1✓ Branch 54 → 55 taken 6988 times.
|
6988 | rest.append(rank); |
| 163 | } | ||
| 164 | } | ||
| 165 |
2/2✓ Branch 58 → 59 taken 280 times.
✓ Branch 59 → 60 taken 280 times.
|
280 | ranked = pinned + reserved + rest; |
| 166 | |||
| 167 |
2/2✓ Branch 63 → 64 taken 1 time.
✓ Branch 63 → 65 taken 279 times.
|
280 | const int budget = input.maxBadges >= 0 ? input.maxBadges : badgeBudget(input.window); |
| 168 | 280 | QList<QRectF> placed; | |
| 169 | 280 | QList<QRectF> placedElements; | |
| 170 | 280 | QList<const Ranked *> placedRanks; | |
| 171 | 280 | QList<QRectF> obstacles; | |
| 172 |
3/3✓ Branch 67 → 68 taken 73 times.
✓ Branch 67 → 69 taken 207 times.
✓ Branch 68 → 69 taken 73 times.
|
280 | if (!input.legend.isEmpty()) obstacles.append(input.legend); |
| 173 |
1/3✗ Branch 72 → 73 not taken.
✗ Branch 75 → 71 not taken.
✓ Branch 75 → 76 taken 280 times.
|
280 | for (const QRectF &card : input.paneLegends) obstacles.append(card); |
| 174 |
4/4✓ Branch 76 → 77 taken 280 times.
✓ Branch 77 → 78 taken 280 times.
✓ Branch 161 → 79 taken 7841 times.
✓ Branch 161 → 162 taken 280 times.
|
8121 | for (const Ranked &rank : ranked) { |
| 175 |
2/2✓ Branch 81 → 82 taken 2076 times.
✓ Branch 81 → 83 taken 5765 times.
|
7841 | if (result.badges.size() >= budget) { ++result.droppedBadges; continue; } |
| 176 | 5765 | const Entry &entry = *rank.entry; | |
| 177 | 5765 | const QSizeF size = entry.badgeSize; | |
| 178 |
2/2✓ Branch 84 → 85 taken 5220 times.
✓ Branch 84 → 86 taken 545 times.
|
5765 | const QRectF clip = (entry.clip.isEmpty() ? windowRect : entry.clip & windowRect) & inner; |
| 179 | // The focused element is an obstacle only for elements outside it. | ||
| 180 | 5765 | QList<QRectF> blocked = obstacles; | |
| 181 |
7/7✓ Branch 90 → 91 taken 5752 times.
✓ Branch 90 → 94 taken 13 times.
✓ Branch 92 → 93 taken 5336 times.
✓ Branch 92 → 94 taken 416 times.
✓ Branch 95 → 96 taken 5336 times.
✓ Branch 95 → 97 taken 429 times.
✓ Branch 96 → 97 taken 5336 times.
|
5765 | if (!input.focus.isEmpty() && !input.focus.intersects(rank.visible)) blocked.append(input.focus); |
| 182 | 5765 | QList<QRectF> candidates; | |
| 183 |
5/5✓ Branch 97 → 98 taken 5765 times.
✓ Branch 98 → 99 taken 5765 times.
✓ Branch 99 → 100 taken 5765 times.
✓ Branch 111 → 101 taken 22418 times.
✓ Branch 111 → 112 taken 5765 times.
|
28183 | for (Corner corner : corners(entry.kind)) { |
| 184 |
2/2✓ Branch 102 → 103 taken 4 times.
✓ Branch 102 → 104 taken 22414 times.
|
22418 | const Corner side = input.rtl ? mirrored(corner) : corner; |
| 185 |
1/1✓ Branch 108 → 109 taken 22418 times.
|
22418 | candidates.append(clampInto(clampInto(placeAt(rank.visible, size, side, entry.kind), clip), inner)); |
| 186 | 5765 | } | |
| 187 | // Nudges of the first corner along x, then y (overlay.md §4.3). | ||
| 188 |
1/1✓ Branch 113 → 114 taken 5765 times.
|
5765 | const QRectF first = candidates.value(0); |
| 189 |
2/2✓ Branch 130 → 116 taken 11530 times.
✓ Branch 130 → 131 taken 5765 times.
|
17295 | for (const qreal step : {0.5, 1.0}) { |
| 190 | 11530 | const qreal dx = size.height() * step, dy = size.height() * step; | |
| 191 |
2/2✓ Branch 128 → 124 taken 46120 times.
✓ Branch 128 → 129 taken 11530 times.
|
57650 | for (const QPointF offset : {QPointF(-dx, 0), QPointF(dx, 0), QPointF(0, -dy), QPointF(0, dy)}) |
| 192 |
1/1✓ Branch 126 → 127 taken 46120 times.
|
46120 | candidates.append(clampInto(first.translated(offset), inner)); |
| 193 | } | ||
| 194 | 5765 | bool done = false; | |
| 195 |
2/2✓ Branch 152 → 134 taken 7596 times.
✓ Branch 152 → 153 taken 92 times.
|
7688 | for (const QRectF &candidate : std::as_const(candidates)) { |
| 196 |
8/8✓ Branch 135 → 136 taken 7596 times.
✓ Branch 136 → 137 taken 6496 times.
✓ Branch 136 → 139 taken 1100 times.
✓ Branch 137 → 138 taken 6496 times.
✓ Branch 138 → 139 taken 823 times.
✓ Branch 138 → 140 taken 5673 times.
✓ Branch 141 → 142 taken 1923 times.
✓ Branch 141 → 143 taken 5673 times.
|
7596 | if (overlapsAny(candidate, placed, kGap) || overlapsAny(candidate, blocked, 0.0)) continue; |
| 197 |
1/1✓ Branch 143 → 144 taken 5673 times.
|
5673 | placed.append(candidate); |
| 198 |
1/1✓ Branch 144 → 145 taken 5673 times.
|
5673 | placedElements.append(rank.visible); |
| 199 |
1/1✓ Branch 145 → 146 taken 5673 times.
|
5673 | placedRanks.append(&rank); |
| 200 | 5673 | result.badges.append({entry.id, candidate, 0}); | |
| 201 | 5673 | done = true; | |
| 202 | 5673 | break; | |
| 203 | } | ||
| 204 |
2/2✓ Branch 153 → 154 taken 92 times.
✓ Branch 153 → 155 taken 5673 times.
|
5765 | if (!done) ++result.droppedBadges; |
| 205 | 5765 | } | |
| 206 | |||
| 207 | // Stagger from the focus outwards. | ||
| 208 |
1/1✓ Branch 163 → 164 taken 280 times.
|
280 | QList<int> order(result.badges.size()); |
| 209 |
3/3✓ Branch 165 → 166 taken 5673 times.
✓ Branch 168 → 165 taken 5673 times.
✓ Branch 168 → 169 taken 280 times.
|
5953 | for (int i = 0; i < order.size(); ++i) order[i] = i; |
| 210 |
3/3✓ Branch 169 → 170 taken 280 times.
✓ Branch 170 → 171 taken 280 times.
✓ Branch 171 → 172 taken 280 times.
|
280 | std::stable_sort(order.begin(), order.end(), [&](int a, int b) { |
| 211 |
4/4✓ Branch 2 → 3 taken 20540 times.
✓ Branch 4 → 5 taken 20540 times.
✓ Branch 5 → 6 taken 20540 times.
✓ Branch 7 → 8 taken 20540 times.
|
20540 | return distance(result.badges[a].rect.center(), focusCentre) < distance(result.badges[b].rect.center(), focusCentre); |
| 212 | }); | ||
| 213 |
2/2✓ Branch 178 → 173 taken 5673 times.
✓ Branch 178 → 179 taken 280 times.
|
5953 | for (int rank = 0; rank < order.size(); ++rank) |
| 214 |
2/2✓ Branch 174 → 175 taken 5673 times.
✓ Branch 175 → 176 taken 5673 times.
|
5673 | result.badges[order[rank]].delayMs = std::min(kStaggerMaxMs, rank * kStaggerStepMs); |
| 215 |
1/1✓ Branch 179 → 180 taken 280 times.
|
280 | result.spotlights = spotlights(placedElements); |
| 216 | |||
| 217 |
3/4✓ Branch 182 → 183 taken 264 times.
✓ Branch 182 → 184 taken 16 times.
✗ Branch 183 → 184 not taken.
✓ Branch 183 → 185 taken 264 times.
|
280 | if (!input.explanations || input.maxCallouts <= 0) return result; |
| 218 | // Callouts: the placed badges with an explanation, by score. | ||
| 219 | 264 | QList<QRectF> calloutRects; | |
| 220 | 264 | QList<QLineF> lines; | |
| 221 |
1/1✓ Branch 185 → 186 taken 264 times.
|
264 | QList<QRectF> calloutBlocked = placed + obstacles; |
| 222 |
2/3✓ Branch 187 → 188 taken 264 times.
✗ Branch 187 → 189 not taken.
✓ Branch 188 → 189 taken 264 times.
|
264 | if (!input.focus.isEmpty()) calloutBlocked.append(input.focus); |
| 223 |
2/2✓ Branch 341 → 190 taken 5470 times.
✓ Branch 341 → 342 taken 264 times.
|
5734 | for (int index = 0; index < result.badges.size(); ++index) { |
| 224 |
1/1✓ Branch 190 → 191 taken 5470 times.
|
5470 | const Badge &badge = result.badges[index]; |
| 225 |
1/1✓ Branch 191 → 192 taken 5470 times.
|
5470 | const Entry &entry = *placedRanks[index]->entry; |
| 226 |
2/2✓ Branch 193 → 194 taken 3932 times.
✓ Branch 193 → 195 taken 1538 times.
|
5964 | if (entry.calloutSize.isEmpty()) continue; |
| 227 |
3/3✓ Branch 196 → 197 taken 147 times.
✓ Branch 196 → 199 taken 1391 times.
✓ Branch 197 → 198 taken 147 times.
|
1538 | if (result.callouts.size() >= input.maxCallouts) { result.droppedCallouts.append(entry.id); continue; } |
| 228 | 1391 | const QSizeF size = entry.calloutSize; | |
| 229 | 1391 | const QRectF b = badge.rect; | |
| 230 | 1391 | const qreal g = kCalloutGap; | |
| 231 | const QList<QRectF> options = { | ||
| 232 | 2782 | {b.right() + g, b.center().y() - size.height() / 2, size.width(), size.height()}, | |
| 233 | 2782 | {b.left() - g - size.width(), b.center().y() - size.height() / 2, size.width(), size.height()}, | |
| 234 | 1391 | {b.center().x() - size.width() / 2, b.bottom() + g, size.width(), size.height()}, | |
| 235 | 2782 | {b.center().x() - size.width() / 2, b.top() - g - size.height(), size.width(), size.height()}, | |
| 236 | 1391 | {b.right() + g, b.bottom() + g, size.width(), size.height()}, | |
| 237 | 1391 | {b.left() - g - size.width(), b.bottom() + g, size.width(), size.height()}, | |
| 238 | 2782 | {b.right() + g, b.top() - g - size.height(), size.width(), size.height()}, | |
| 239 | 2782 | {b.left() - g - size.width(), b.top() - g - size.height(), size.width(), size.height()}, | |
| 240 |
1/1✓ Branch 253 → 254 taken 1391 times.
|
18083 | }; |
| 241 | 1391 | qreal bestScore = 0; | |
| 242 | 1391 | int best = -1; | |
| 243 |
2/2✓ Branch 294 → 255 taken 11128 times.
✓ Branch 294 → 295 taken 1391 times.
|
12519 | for (int option = 0; option < options.size(); ++option) { |
| 244 |
1/2✗ Branch 255 → 256 not taken.
✓ Branch 255 → 265 taken 11128 times.
|
11128 | const QRectF rect = input.rtl ? QRectF(b.center().x() * 2 - options[option].right(), options[option].top(), |
| 245 | size.width(), size.height()) | ||
| 246 | 11128 | : options[option]; | |
| 247 |
2/2✓ Branch 268 → 269 taken 3396 times.
✓ Branch 268 → 270 taken 7732 times.
|
14875 | if (!inner.contains(rect)) continue; |
| 248 |
8/8✓ Branch 270 → 271 taken 7732 times.
✓ Branch 271 → 272 taken 4454 times.
✓ Branch 271 → 274 taken 3278 times.
✓ Branch 272 → 273 taken 4454 times.
✓ Branch 273 → 274 taken 469 times.
✓ Branch 273 → 275 taken 3985 times.
✓ Branch 276 → 277 taken 3747 times.
✓ Branch 276 → 278 taken 3985 times.
|
7732 | if (overlapsAny(rect, calloutBlocked, 0.0) || overlapsAny(rect, calloutRects, 4.0)) continue; |
| 249 |
2/2✓ Branch 279 → 280 taken 3985 times.
✓ Branch 281 → 282 taken 3985 times.
|
3985 | const QLineF line(nearestPoint(b, rect.center()), nearestPoint(rect, b.center())); |
| 250 |
1/1✓ Branch 285 → 286 taken 3985 times.
|
3985 | const int crossings = int(std::count_if(lines.cbegin(), lines.cend(), |
| 251 | 10795 | [&](const QLineF &other) { return segmentsCross(line, other); })); | |
| 252 |
1/1✓ Branch 286 → 287 taken 3985 times.
|
3985 | const qreal score = line.length() + 500.0 * crossings + option; // option: a stable tie-break |
| 253 |
3/4✓ Branch 287 → 288 taken 2941 times.
✓ Branch 287 → 289 taken 1044 times.
✗ Branch 288 → 289 not taken.
✓ Branch 288 → 290 taken 2941 times.
|
3985 | if (best < 0 || score < bestScore) { best = option; bestScore = score; } |
| 254 | } | ||
| 255 |
3/3✓ Branch 295 → 296 taken 347 times.
✓ Branch 295 → 298 taken 1044 times.
✓ Branch 296 → 297 taken 347 times.
|
1391 | if (best < 0) { result.droppedCallouts.append(entry.id); continue; } |
| 256 |
1/2✗ Branch 298 → 299 not taken.
✓ Branch 298 → 308 taken 1044 times.
|
1044 | const QRectF rect = input.rtl ? QRectF(b.center().x() * 2 - options[best].right(), options[best].top(), |
| 257 | size.width(), size.height()) | ||
| 258 | 1044 | : options[best]; | |
| 259 |
1/1✓ Branch 311 → 312 taken 1044 times.
|
1044 | const QPointF from = nearestPoint(b, rect.center()); |
| 260 |
1/1✓ Branch 313 → 314 taken 1044 times.
|
1044 | const QPointF to = nearestPoint(rect, b.center()); |
| 261 | 1044 | const QLineF line(from, to); | |
| 262 | // A gentle curve: the control point sits 20 % of the length off the middle. | ||
| 263 |
3/4✓ Branch 315 → 316 taken 1044 times.
✓ Branch 316 → 317 taken 1044 times.
✗ Branch 316 → 322 not taken.
✓ Branch 317 → 318 taken 1044 times.
|
1044 | const QPointF normal = line.length() > 0 ? QPointF(-line.dy(), line.dx()) / line.length() : QPointF(); |
| 264 |
1/1✓ Branch 323 → 324 taken 1044 times.
|
1044 | const QPointF control = line.center() + normal * (0.2 * line.length()); |
| 265 |
1/1✓ Branch 327 → 328 taken 1044 times.
|
1044 | calloutRects.append(rect); |
| 266 |
1/1✓ Branch 328 → 329 taken 1044 times.
|
1044 | lines.append(line); |
| 267 | 1044 | result.callouts.append({entry.id, rect, from, control, to, kCalloutLineMs + int(result.callouts.size()) * kCalloutStepMs}); | |
| 268 | 1391 | } | |
| 269 | 264 | return result; | |
| 270 | 6997 | } | |
| 271 |