Skip to content

Commit 889d52e

Browse files
committed
qml: round block clock confirmation segments
1 parent da95f8e commit 889d52e

3 files changed

Lines changed: 95 additions & 21 deletions

File tree

qml/components/blockclockdial.cpp

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -381,39 +381,41 @@ void BlockClockDial::paintBlocks(QPainter * painter)
381381

382382
QPen pen(m_confirmation_colors.constLast());
383383
pen.setWidthF(m_pen_width);
384-
pen.setCapStyle(Qt::FlatCap);
384+
pen.setCapStyle(Qt::RoundCap);
385385
const QRectF bounds = getBoundsForPen(pen);
386386
painter->setPen(pen);
387387

388-
// Boundaries are explicit: the period starts at zero, each block closes
389-
// one confirmation segment, and the current time closes the newest one.
390-
QList<qreal> boundaries;
391-
boundaries.reserve(m_block_time_fractions.size() + 2);
392-
boundaries.push_back(0.0);
393-
boundaries.append(m_block_time_fractions);
394-
boundaries.push_back(m_current_time_fraction);
395-
396-
const qreal gap{degreesPerPixel()};
388+
// Rounded caps extend half a stroke beyond each path endpoint. Separate
389+
// their center lines by one stroke plus a small visible gap so neighboring
390+
// confirmation segments retain the pill-shaped spacing from the design.
391+
const QList<qreal> boundaries{coalescedBlockBoundaries(bounds)};
392+
const qreal boundary_gap_degrees{
393+
degreesForArcPixels(m_pen_width + blockSegmentGapPixels(), bounds)};
394+
const qreal half_boundary_gap{boundary_gap_degrees / 2.0};
395+
const qreal animated_fraction{qBound<qreal>(0.0, m_animating_max_angle / 360.0, 1.0)};
397396
const qsizetype segment_count{boundaries.size() - 1};
398397
for (qsizetype segment{0}; segment < segment_count; ++segment) {
398+
const qreal segment_start{boundaries.at(segment)};
399+
const qreal segment_end{qMin(boundaries.at(segment + 1), animated_fraction)};
400+
const qreal available_degrees{(segment_end - segment_start) * 360.0};
401+
if (available_degrees <= boundary_gap_degrees) {
402+
if (boundaries.at(segment + 1) > animated_fraction) break;
403+
continue;
404+
}
405+
399406
const qsizetype color_index{qMin<qsizetype>(5, segment_count - segment - 1)};
400407
pen.setColor(m_confirmation_colors.at(color_index));
401408
painter->setPen(pen);
402409

403-
const qreal startAngle{90 - 360 * boundaries.at(segment)};
404-
qreal nextAngle{90 - 360 * boundaries.at(segment + 1)};
410+
const qreal start_angle{90 - 360 * segment_start - half_boundary_gap};
411+
const qreal end_angle{90 - 360 * segment_end + half_boundary_gap};
405412

406413
QPainterPath path;
407-
path.arcMoveTo(bounds, startAngle);
408-
409-
if (-1 * nextAngle + 90 > m_animating_max_angle) {
410-
nextAngle = -1 * m_animating_max_angle + 90;
411-
segment = segment_count;
412-
}
413-
414-
const qreal spanAngle = -1 * (startAngle - nextAngle) + gap;
415-
path.arcTo(bounds, startAngle, spanAngle);
414+
path.arcMoveTo(bounds, start_angle);
415+
path.arcTo(bounds, start_angle, end_angle - start_angle);
416416
painter->drawPath(path);
417+
418+
if (boundaries.at(segment + 1) > animated_fraction) break;
417419
}
418420
}
419421

@@ -512,6 +514,50 @@ double BlockClockDial::degreesPerPixel()
512514
return 360 / circumference;
513515
}
514516

517+
qreal BlockClockDial::degreesForArcPixels(qreal pixels, const QRectF& bounds) const
518+
{
519+
const qreal radius{qMin(bounds.width(), bounds.height()) / 2.0};
520+
if (radius <= 0.0) return 360.0;
521+
return qRadiansToDegrees(pixels / radius);
522+
}
523+
524+
qreal BlockClockDial::blockSegmentGapPixels() const
525+
{
526+
// The Figma dial uses a gap around half the stroke width, with one device
527+
// pixel as the lower bound for small miniatures.
528+
return qMax<qreal>(1.0, m_pen_width / 2.0);
529+
}
530+
531+
QList<qreal> BlockClockDial::coalescedBlockBoundaries(const QRectF& bounds) const
532+
{
533+
const qreal current_fraction{qBound<qreal>(0.0, m_current_time_fraction, 1.0)};
534+
if (current_fraction <= 0.0) return {0.0};
535+
536+
// A rounded segment needs room for both half-caps, the visual gap, and at
537+
// least one pixel of center line. Keep the newest boundary in each cluster
538+
// so simplification affects presentation only and favors recent blocks.
539+
const qreal minimum_interval_fraction{
540+
degreesForArcPixels(m_pen_width + blockSegmentGapPixels() + 1.0, bounds) / 360.0};
541+
QList<qreal> descending_boundaries{current_fraction};
542+
qreal next_boundary{current_fraction};
543+
for (auto it{m_block_time_fractions.crbegin()}; it != m_block_time_fractions.crend(); ++it) {
544+
const qreal boundary{*it};
545+
if (boundary <= 0.0 || boundary >= current_fraction) continue;
546+
if (next_boundary - boundary < minimum_interval_fraction) continue;
547+
548+
descending_boundaries.push_back(boundary);
549+
next_boundary = boundary;
550+
}
551+
552+
// Do not leave an undersized first segment against the period boundary.
553+
if (descending_boundaries.size() > 1 && descending_boundaries.constLast() < minimum_interval_fraction) {
554+
descending_boundaries.removeLast();
555+
}
556+
descending_boundaries.push_back(0.0);
557+
std::reverse(descending_boundaries.begin(), descending_boundaries.end());
558+
return descending_boundaries;
559+
}
560+
515561
void BlockClockDial::paintTimeTicks(QPainter * painter)
516562
{
517563
QPen pen(m_time_tick_color);

qml/components/blockclockdial.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ public Q_SLOTS:
9494
void paintTimeTicks(QPainter * painter);
9595
QRectF getBoundsForPen(const QPen & pen);
9696
double degreesPerPixel();
97+
qreal degreesForArcPixels(qreal pixels, const QRectF& bounds) const;
98+
qreal blockSegmentGapPixels() const;
99+
QList<qreal> coalescedBlockBoundaries(const QRectF& bounds) const;
97100
void setupConnectingGradient(const QPen & pen);
98101
void setupSyncedGradient(const QRectF& bounds);
99102
void invalidateSyncedGradient();

test/test_blockclockdial.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ private Q_SLOTS:
9191
void connectingDelayControlsInitialAnimation();
9292
void inactiveDialStopsAnimationAndRetainsLatestState();
9393
void paintDoesNotAdvanceAnimationState();
94+
void closeBlockBoundariesCoalesceToNewestBoundary();
9495
};
9596

9697
void BlockClockDialTests::ibdProgressRendersImmediateHalfArc()
@@ -230,6 +231,30 @@ void BlockClockDialTests::paintDoesNotAdvanceAnimationState()
230231
QCOMPARE(first, second);
231232
}
232233

234+
void BlockClockDialTests::closeBlockBoundariesCoalesceToNewestBoundary()
235+
{
236+
BlockClockDial clustered_dial;
237+
ConfigureDial(clustered_dial);
238+
clustered_dial.setAnimateDial(false);
239+
clustered_dial.setConnected(true);
240+
clustered_dial.setSynced(true);
241+
clustered_dial.setCurrentTimeFraction(0.75);
242+
clustered_dial.setBlockTimeFractions({0.25, 0.2501, 0.2502});
243+
244+
BlockClockDial simplified_dial;
245+
ConfigureDial(simplified_dial);
246+
simplified_dial.setAnimateDial(false);
247+
simplified_dial.setConnected(true);
248+
simplified_dial.setSynced(true);
249+
simplified_dial.setCurrentTimeFraction(0.75);
250+
simplified_dial.setBlockTimeFractions({0.2502});
251+
252+
// The source data remains complete, while visually overlapping boundaries
253+
// collapse to the newest timestamp in the cluster.
254+
QCOMPARE(clustered_dial.blockTimeFractions().size(), 3);
255+
QCOMPARE(RenderDial(clustered_dial), RenderDial(simplified_dial));
256+
}
257+
233258
#ifdef BITCOINQML_NO_TEST_MAIN
234259
BITCOINQML_REGISTER_QT_TEST(BlockClockDialTests)
235260
#else

0 commit comments

Comments
 (0)