feat: better notification storage and image caching + blobs
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
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
This commit is contained in:
@@ -3,23 +3,120 @@
|
||||
#include <qmath.h>
|
||||
#include <qpainter.h>
|
||||
#include <qpen.h>
|
||||
#include <qquickwindow.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace ZShell::components {
|
||||
|
||||
namespace {
|
||||
qint64 toDev(qreal logical, qreal dpr) {
|
||||
return std::llround(logical * dpr);
|
||||
}
|
||||
|
||||
qreal fromDev(qint64 device, qreal dpr) {
|
||||
return qreal(device) / dpr;
|
||||
}
|
||||
|
||||
// A line center whose device position leaves a whole-pixel-wide stroke: an even
|
||||
// width sits on pixel boundaries, an odd width on pixel centers. Same rule as
|
||||
// snapLine() in Components/LineGraph.qml.
|
||||
qreal snapLine(qreal logical, qint64 widthDev, qreal dpr) {
|
||||
const qreal half = qreal(widthDev) / 2.0;
|
||||
return (static_cast<qreal>(std::llround(logical * dpr - half)) + half) /
|
||||
dpr;
|
||||
}
|
||||
|
||||
// y of a cubic bezier at targetX, found by bisection on the parameter. Used to
|
||||
// place one vertex per device pixel column, so a segment never interpolates its
|
||||
// edge across several columns.
|
||||
qreal cubicY(
|
||||
const QPointF& p1,
|
||||
const QPointF& c1,
|
||||
const QPointF& c2,
|
||||
const QPointF& p2,
|
||||
qreal targetX) {
|
||||
qreal lo = 0.0;
|
||||
qreal hi = 1.0;
|
||||
for (int it = 0; it < 16; ++it) {
|
||||
const qreal t = (lo + hi) / 2.0;
|
||||
const qreal u = 1.0 - t;
|
||||
const qreal x = u * u * u * p1.x() + 3 * u * u * t * c1.x() +
|
||||
3 * u * t * t * c2.x() + t * t * t * p2.x();
|
||||
if (x < targetX)
|
||||
lo = t;
|
||||
else
|
||||
hi = t;
|
||||
}
|
||||
|
||||
const qreal t = (lo + hi) / 2.0;
|
||||
const qreal u = 1.0 - t;
|
||||
return u * u * u * p1.y() + 3 * u * u * t * c1.y() +
|
||||
3 * u * t * t * c2.y() + t * t * t * p2.y();
|
||||
}
|
||||
|
||||
// Catmull-Rom controls with the control y clamped to the segment's own y span,
|
||||
// so a curve never overshoots past its endpoints.
|
||||
void segmentControls(
|
||||
const QVector<QPointF>& points,
|
||||
int i,
|
||||
bool curveSmoothing,
|
||||
QPointF& c1,
|
||||
QPointF& c2) {
|
||||
const QPointF& p0 = points[qMax(i - 1, 0)];
|
||||
const QPointF& p1 = points[i];
|
||||
const QPointF& p2 = points[i + 1];
|
||||
const QPointF& p3 = points[qMin(i + 2, points.size() - 1)];
|
||||
|
||||
if (curveSmoothing && points.size() > 2) {
|
||||
const qreal k = 1.0 / 6.0;
|
||||
c1 = p1 + (p2 - p0) * k;
|
||||
c2 = p2 - (p3 - p1) * k;
|
||||
|
||||
const qreal minY = qMin(p1.y(), p2.y());
|
||||
const qreal maxY = qMax(p1.y(), p2.y());
|
||||
c1.setY(qBound(minY, c1.y(), maxY));
|
||||
c2.setY(qBound(minY, c2.y(), maxY));
|
||||
return;
|
||||
}
|
||||
|
||||
const qreal cx = (p1.x() + p2.x()) * 0.5;
|
||||
c1 = QPointF(cx, p1.y());
|
||||
c2 = QPointF(cx, p2.y());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
SparklineContent::SparklineContent(QQuickItem* parent)
|
||||
: QQuickPaintedItem(parent) {
|
||||
setAntialiasing(true);
|
||||
}
|
||||
|
||||
void SparklineContent::itemChange(
|
||||
ItemChange change, const ItemChangeData& value) {
|
||||
QQuickPaintedItem::itemChange(change, value);
|
||||
if (change == ItemSceneChange && value.window)
|
||||
m_dpr = value.window->effectiveDevicePixelRatio();
|
||||
else if (change == ItemDevicePixelRatioHasChanged && window())
|
||||
m_dpr = window()->effectiveDevicePixelRatio();
|
||||
else
|
||||
return;
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
qreal SparklineContent::stepX() const {
|
||||
if (m_historyLength < 2 || m_viewportW <= 0.0) return 0.0;
|
||||
if (m_sliding) return m_viewportW / static_cast<qreal>(m_historyLength - 1);
|
||||
|
||||
const int len = qMax(
|
||||
2,
|
||||
(m_line1 && m_line1->count() >= 2) ? m_line1->count()
|
||||
: (m_line2 ? m_line2->count() : 2));
|
||||
return m_viewportW / static_cast<qreal>(len - 1);
|
||||
const qreal logical =
|
||||
m_sliding ? m_viewportW / static_cast<qreal>(m_historyLength - 1)
|
||||
: m_viewportW / static_cast<qreal>(len - 1);
|
||||
// A whole device pixel per step: every sample lands on a pixel column, and
|
||||
// one slide step moves the content by an exact pixel count.
|
||||
return fromDev((std::max)(qint64(1), toDev(logical, m_dpr)), m_dpr);
|
||||
}
|
||||
|
||||
void SparklineContent::setViewportSize(
|
||||
@@ -130,79 +227,97 @@ void SparklineContent::applyGeometry() {
|
||||
SparklineContent::LineGeometry SparklineContent::buildGeometry(
|
||||
internal::CircularBuffer* buffer) const {
|
||||
LineGeometry geom;
|
||||
if (!buffer || buffer->count() < 2 || m_maxValue <= 0.0 ||
|
||||
m_viewportW <= 0.0)
|
||||
// Every input has to be a usable number: a NaN from QML (an animated maxValue
|
||||
// fed by an empty buffer, for instance) would otherwise reach llround and the
|
||||
// path stroker as garbage, which draws a shape that ignores the curve.
|
||||
if (!buffer || buffer->count() < 2 || !std::isfinite(m_maxValue) ||
|
||||
m_maxValue <= 0.0 || !std::isfinite(m_viewportW) ||
|
||||
m_viewportW <= 0.0 || !std::isfinite(m_viewportH) || m_viewportH <= 0.0)
|
||||
return geom;
|
||||
if (m_historyLength < 2) return geom;
|
||||
|
||||
const qreal dpr = m_dpr > 0.0 ? m_dpr : 1.0;
|
||||
const int len = buffer->count();
|
||||
const qreal step = stepX();
|
||||
if (step <= 0.0) return geom;
|
||||
if (!std::isfinite(step) || step <= 0.0) return geom;
|
||||
|
||||
const qint64 stepDev = toDev(step, dpr);
|
||||
const qint64 lineDev = std::isfinite(m_lineWidth)
|
||||
? (std::max)(qint64(1), toDev(m_lineWidth, dpr))
|
||||
: qint64(1);
|
||||
const qint64 heightDev = toDev(m_viewportH, dpr);
|
||||
|
||||
qreal startX = 0.0;
|
||||
if (m_sliding)
|
||||
startX = m_viewportW - (len - 1) * step + step;
|
||||
else
|
||||
startX = 0.0;
|
||||
const qint64 startDev = toDev(startX, dpr);
|
||||
|
||||
const qreal h = m_viewportH;
|
||||
const qreal strokePad = qCeil(m_lineWidth / 2);
|
||||
const qreal curvePad = 3.0;
|
||||
const qreal plotTop = strokePad + curvePad;
|
||||
const qreal plotBottom = h - strokePad;
|
||||
const qreal fillBottom = h;
|
||||
const qreal plotH = qMax<qreal>(1.0, plotBottom - plotTop);
|
||||
// Plot bounds in device pixels: half a stroke plus the curve overshoot at
|
||||
// the top, a half stroke and one pixel of antialiasing feather at the
|
||||
// bottom, so neither edge of the stroke is cut by the item's own rect.
|
||||
const qint64 curvePadDev = toDev(m_curveSmoothing ? 3.0 : 0.0, dpr);
|
||||
const qint64 plotTopDev = lineDev / 2 + curvePadDev;
|
||||
const qint64 plotBottomDev =
|
||||
(std::max)(plotTopDev + 1, heightDev - (lineDev + 1) / 2 - 1);
|
||||
const qint64 plotHDev = plotBottomDev - plotTopDev;
|
||||
const qreal fillBottom = fromDev(heightDev, dpr);
|
||||
|
||||
QVector<QPointF> points;
|
||||
points.reserve(len);
|
||||
|
||||
for (int i = 0; i < len; ++i) {
|
||||
const qreal value =
|
||||
qBound<qreal>(0.0, buffer->smoothedAt(i), m_maxValue);
|
||||
const qreal raw = buffer->smoothedAt(i);
|
||||
|
||||
const qreal x = startX + i * step;
|
||||
const qreal y = plotTop + (1.0 - (value / m_maxValue)) * plotH;
|
||||
points.append(QPointF(x, y));
|
||||
// A non-finite sample poisons the whole path: llround of it is undefined,
|
||||
// and the stroke and fill then ignore everything after that vertex. Treat
|
||||
// it as no traffic instead.
|
||||
const qreal value =
|
||||
std::isfinite(raw) ? qBound<qreal>(0.0, raw, m_maxValue) : 0.0;
|
||||
|
||||
// Vertices on a pixel column, and on the y grid the stroke width needs.
|
||||
// Item-local, so the parent's placement cannot move the shape. A flat run
|
||||
// then lands on whole rows; slopes are interpolated, not snapped.
|
||||
const qreal y = fromDev(
|
||||
plotTopDev + static_cast<qint64>(std::llround(
|
||||
(1.0 - value / m_maxValue) *
|
||||
static_cast<double>(plotHDev))),
|
||||
dpr);
|
||||
points.append(QPointF(
|
||||
fromDev(startDev + qint64(i) * stepDev, dpr),
|
||||
snapLine(y, lineDev, dpr)));
|
||||
}
|
||||
|
||||
QPainterPath linePath;
|
||||
linePath.moveTo(points[0]);
|
||||
linePath.moveTo(points.first());
|
||||
|
||||
if (m_curveSmoothing && points.size() > 2) {
|
||||
const qreal tension = 1.0;
|
||||
const qreal k = tension / 6.0;
|
||||
const int n = static_cast<int>(points.size());
|
||||
for (int i = 0; i < points.size() - 1; ++i) {
|
||||
const QPointF& p1 = points[i];
|
||||
const QPointF& p2 = points[i + 1];
|
||||
QPointF c1;
|
||||
QPointF c2;
|
||||
segmentControls(points, i, m_curveSmoothing, c1, c2);
|
||||
|
||||
for (int i = 0; i < n - 1; ++i) {
|
||||
const QPointF& p0 = points[qMax(i - 1, 0)];
|
||||
const QPointF& p1 = points[i];
|
||||
const QPointF& p2 = points[i + 1];
|
||||
const QPointF& p3 = points[qMin(i + 2, n - 1)];
|
||||
|
||||
QPointF c1 = p1 + (p2 - p0) * k;
|
||||
QPointF c2 = p2 - (p3 - p1) * k;
|
||||
|
||||
const qreal segMinY = qMin(p1.y(), p2.y());
|
||||
const qreal segMaxY = qMax(p1.y(), p2.y());
|
||||
c1.setY(qBound(segMinY, c1.y(), segMaxY));
|
||||
c2.setY(qBound(segMinY, c2.y(), segMaxY));
|
||||
|
||||
linePath.cubicTo(c1, c2, p2);
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < points.size() - 1; ++i) {
|
||||
const QPointF& p0 = points[i];
|
||||
const QPointF& p1 = points[i + 1];
|
||||
const qreal ctrlX = (p0.x() + p1.x()) * 0.5;
|
||||
linePath.cubicTo(QPointF(ctrlX, p0.y()), QPointF(ctrlX, p1.y()), p1);
|
||||
// One vertex per device pixel column, as Components/LineGraph.qml does: a
|
||||
// segment spanning several columns would otherwise interpolate its edge
|
||||
// across all of them. y stays continuous here - snapping it per column
|
||||
// would step the slope one pixel at a time and read as a staircase.
|
||||
const qint64 firstCol = std::llround(p1.x() * dpr) + 1;
|
||||
const qint64 lastCol = std::llround(p2.x() * dpr) - 1;
|
||||
for (qint64 col = firstCol; col <= lastCol; ++col) {
|
||||
const qreal x = fromDev(col, dpr);
|
||||
linePath.lineTo(x, cubicY(p1, c1, c2, p2, x));
|
||||
}
|
||||
|
||||
linePath.lineTo(p2);
|
||||
}
|
||||
|
||||
geom.stroke = linePath;
|
||||
|
||||
QPainterPath fillPath = linePath;
|
||||
fillPath.lineTo(startX + (len - 1) * step, fillBottom);
|
||||
fillPath.lineTo(startX, fillBottom);
|
||||
fillPath.lineTo(points.last().x(), fillBottom);
|
||||
fillPath.lineTo(points.first().x(), fillBottom);
|
||||
fillPath.closeSubpath();
|
||||
geom.fill = fillPath;
|
||||
geom.hasData = true;
|
||||
@@ -223,6 +338,12 @@ void SparklineContent::paint(QPainter* painter) {
|
||||
|
||||
painter->setRenderHint(QPainter::Antialiasing, true);
|
||||
|
||||
const qreal dpr = m_dpr > 0.0 ? m_dpr : 1.0;
|
||||
const qreal lineWidth =
|
||||
std::isfinite(m_lineWidth)
|
||||
? fromDev((std::max)(qint64(1), toDev(m_lineWidth, dpr)), dpr)
|
||||
: 1.0;
|
||||
|
||||
auto drawOne =
|
||||
[&](const LineGeometry& geom, const QColor& color, qreal fillAlpha) {
|
||||
if (!geom.hasData) return;
|
||||
@@ -233,7 +354,7 @@ void SparklineContent::paint(QPainter* painter) {
|
||||
painter->setBrush(fillColor);
|
||||
painter->drawPath(geom.fill);
|
||||
|
||||
QPen pen(color, m_lineWidth);
|
||||
QPen pen(color, lineWidth);
|
||||
pen.setCapStyle(Qt::RoundCap);
|
||||
pen.setJoinStyle(Qt::RoundJoin);
|
||||
painter->setPen(pen);
|
||||
|
||||
Reference in New Issue
Block a user