Files
z-bar-qt/Plugins/ZShell/Components/wavylinerenderer.cpp
T
zach 11e7aac195
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 11s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 12s
JS/TS / lint (pull_request) Successful in 10s
Python / static (pull_request) Failing after 27s
Rust / build (pull_request) Successful in 54s
Python / verify (pull_request) Failing after 1m26s
Rust / fmt (pull_request) Successful in 46s
Rust / clippy (pull_request) Successful in 1m6s
feat: better notification storage and image caching + blobs
2026-11-02 19:00:43 +01:00

204 lines
7.1 KiB
C++

#include "wavylinerenderer.hpp"
#include "wavyline.hpp"
#include <qcanvaspainter.h>
#include <algorithm>
#include <cmath>
namespace ZShell::controls {
void WavyLineRenderer::synchronizeData(QCanvasPainterItem* item) {
auto* wavyLine = static_cast<WavyLine*>(item);
m_lineWidth = wavyLine->m_lineWidth;
m_amplitudeMultiplier = wavyLine->m_amplitudeMultiplier;
m_frequency = wavyLine->m_frequency;
m_startX = wavyLine->m_startX;
m_fullLength = wavyLine->m_fullLength;
m_color = wavyLine->m_color;
m_waveProgress = wavyLine->m_waveProgress;
m_pathType = wavyLine->m_pathType;
m_startAngle = wavyLine->m_startAngle;
m_fullAngle = wavyLine->m_fullAngle;
m_radius = wavyLine->m_radius;
m_value = wavyLine->m_value;
m_dpr = wavyLine->m_dpr > 0.0 ? wavyLine->m_dpr : 1.0;
}
namespace {
qint64 toDev(qreal logical, qreal dpr) {
return std::llround(logical * dpr);
}
qreal fromDev(qint64 device, qreal dpr) {
return qreal(device) / dpr;
}
// A center coordinate whose device position shares the parity of the stroke's
// device width: an even width centered on a pixel boundary and an odd width
// centered on a pixel center are the two cases where both edges of the stroke
// land on whole device pixels. Floored, so the caller gets the same value from
// the same device size every time; a rounded half could flip between two grid
// points depending on floating point noise.
qreal snapCenterDev(qint64 sizeDev, qint64 widthDev) {
const double halfParity = (widthDev % 2) == 0 ? 0.0 : 0.5;
return std::floor(static_cast<double>(sizeDev) / 2.0 - halfParity)
+ halfParity;
}
qint64 snapAmplitude(qint64 widthDev, qreal multiplier) {
return std::llround(static_cast<double>(widthDev) * multiplier);
}
} // namespace
void WavyLineRenderer::paint(QCanvasPainter* painter) {
painter->clearRect(0, 0, width(), height());
// The stroke is quantized in the item's own device pixel space, never
// against its position: the parent layout already places the item on the
// device grid, so a local whole-device value stays whole-device wherever
// the item moves, and a move cannot change the painted path.
const qreal dpr = m_dpr;
m_lineWidthDev = (std::max)(qint64(1), toDev(m_lineWidth, dpr));
m_amplitudeDev = snapAmplitude(m_lineWidthDev, m_amplitudeMultiplier);
painter->setStrokeStyle(m_color);
painter->setLineWidth(fromDev(m_lineWidthDev, dpr));
painter->setLineCap(QCanvasPainter::LineCap::Round);
if (m_pathType == WavyLine::Arc) {
paintArc(painter);
} else {
paintLinear(painter);
}
}
void WavyLineRenderer::paintLinear(QCanvasPainter* painter) {
const qreal dpr = m_dpr;
const auto lineWidth = fromDev(m_lineWidthDev, dpr);
const auto phase = m_waveProgress * 2 * M_PI;
const qint64 heightDev = toDev(height(), dpr);
const qreal cyDev = snapCenterDev(heightDev, m_lineWidthDev);
const auto centerY = cyDev / dpr;
const auto len = m_fullLength > 0 ? m_fullLength : 1;
const auto start = lineWidth / 2.0;
const auto fullEnd = width() - lineWidth / 2.0;
const auto drawEnd = start + (fullEnd - start) * m_value;
// Keep the crest inside the item's own texture, with one device pixel for
// the antialiasing feather, so a tight item cannot cut it flat.
const qint64 halfDev = (m_lineWidthDev + 1) / 2;
const qint64 roomDev = static_cast<qint64>(
std::floor((std::min)(cyDev, qreal(heightDev) - cyDev)));
const qint64 amplitudeDev = (std::max)(qint64(0), roomDev - halfDev - 1);
const auto amplitude =
fromDev((std::min)(m_amplitudeDev, amplitudeDev), dpr);
painter->beginPath();
bool first = true;
// One vertex per device pixel column. A segment spanning several columns
// interpolates its edge across all of them, which is what makes a sloped
// stroke look soft rather than crisply anti-aliased.
const qint64 firstCol = toDev(start, dpr);
const qint64 lastCol = toDev(drawEnd, dpr);
for (qint64 col = firstCol; col <= lastCol; ++col) {
const auto x = fromDev(col, dpr);
const auto theta =
m_frequency * 2 * M_PI * (x + m_startX) / len + phase;
const auto waveY = centerY + amplitude * qSin(theta);
if (first) {
painter->moveTo(x, waveY);
first = false;
} else {
painter->lineTo(x, waveY);
}
}
painter->stroke();
}
void WavyLineRenderer::paintArc(QCanvasPainter* painter) {
if (m_fullAngle <= 0) {
return;
}
const qreal dpr = m_dpr;
const auto amplitude = fromDev(m_amplitudeDev, dpr);
const auto startAngleRad = m_startAngle * M_PI / 180.0;
const auto fullAngleRad = m_fullAngle * M_PI / 180.0;
// The center comes from the item's own device size and the stroke parity,
// the same rule Components/CircularProgress.qml applies to its background
// Shape arc, so the two rings share one center.
const qint64 widthDev = toDev(width(), dpr);
const qint64 heightDev = toDev(height(), dpr);
const qreal cxDev = snapCenterDev(widthDev, m_lineWidthDev);
const qreal cyDev = snapCenterDev(heightDev, m_lineWidthDev);
const auto cx = cxDev / dpr;
const auto cy = cyDev / dpr;
// The path is painted into a texture exactly the item's size, so anything
// past its edge is cut flat. Keep the outer stroke edge, plus the wave
// crest, inside that rect and leave one device pixel for the antialiasing
// feather. Derived from the item's own size and its local center, so it
// does not depend on where the item is placed.
const qint64 halfDev = (m_lineWidthDev + 1) / 2;
const qint64 roomDev = static_cast<qint64>(
std::floor((std::min)((std::min)(cxDev, qreal(widthDev) - cxDev),
(std::min)(cyDev, qreal(heightDev) - cyDev))));
const qint64 maxRadiusDev = roomDev - m_amplitudeDev - halfDev - 1;
// The radius is a whole device pixel count. Derived from the item's own
// device size, so it does not depend on where the item is.
qint64 radiusDev;
if (m_radius > 0) {
radiusDev = toDev(m_radius, dpr);
} else {
const qint64 inner = (std::min)(widthDev, heightDev) - m_lineWidthDev -
2 * m_amplitudeDev;
radiusDev = (std::max)(qint64(1), inner / 2);
}
radiusDev = (std::max)(qint64(1), (std::min)(radiusDev, maxRadiusDev));
const auto radius = fromDev(radiusDev, dpr);
if (radius <= 0) {
return;
}
const auto phase = m_waveProgress * 2 * M_PI;
const auto arcLen = radius * fullAngleRad;
const auto len = m_fullLength > 0 ? m_fullLength : arcLen;
const auto drawAngleRad = fullAngleRad * m_value;
if (drawAngleRad <= 0) {
return;
}
// Sample every device pixel of arc length, not every logical one: at a
// fractional ratio a logical step is more than one device pixel wide, and
// the edge would then be interpolated across the pixels it crosses.
const auto N =
qMax(64, qCeil(static_cast<double>(toDev(radius, dpr)) * drawAngleRad));
const auto dTheta = drawAngleRad / N;
painter->beginPath();
for (int i = 0; i <= N; ++i) {
const auto theta = startAngleRad + i * dTheta;
const auto s = i * dTheta * radius;
const auto phi = m_frequency * 2 * M_PI * (s + m_startX) / len + phase;
const auto r = radius + amplitude * qSin(phi);
const auto px = cx + r * qCos(theta);
const auto py = cy + r * qSin(theta);
if (i == 0) {
painter->moveTo(px, py);
} else {
painter->lineTo(px, py);
}
}
painter->stroke();
}
} // namespace ZShell::controls