feat: replace canvas -> c++ component

Also add c++ ring buffer
This commit is contained in:
2 * r + 2 * t 2026-03-12 22:51:07 +11:00
parent 8e7ce4c6a6
commit 27cb290423
9 changed files with 668 additions and 181 deletions

View file

@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell.Services.UPower import Quickshell.Services.UPower
import Caelestia.Internal
import qs.components import qs.components
import qs.components.misc import qs.components.misc
import qs.config import qs.config
@ -486,51 +487,15 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
Canvas { ArcGauge {
id: gaugeCanvas
anchors.centerIn: parent anchors.centerIn: parent
width: Math.min(parent.width, parent.height) width: Math.min(parent.width, parent.height)
height: width height: width
onPaint: { percentage: gaugeCard.animatedPercentage
const ctx = getContext("2d"); accentColor: gaugeCard.accentColor
ctx.reset(); trackColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)
const cx = width / 2; startAngle: gaugeCard.arcStartAngle
const cy = height / 2; sweepAngle: gaugeCard.arcSweep
const radius = (Math.min(width, height) - 12) / 2;
const lineWidth = 10;
ctx.beginPath();
ctx.arc(cx, cy, radius, gaugeCard.arcStartAngle, gaugeCard.arcStartAngle + gaugeCard.arcSweep);
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.strokeStyle = Colours.layer(Colours.palette.m3surfaceContainerHigh, 2);
ctx.stroke();
if (gaugeCard.animatedPercentage > 0) {
ctx.beginPath();
ctx.arc(cx, cy, radius, gaugeCard.arcStartAngle, gaugeCard.arcStartAngle + gaugeCard.arcSweep * gaugeCard.animatedPercentage);
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.strokeStyle = gaugeCard.accentColor;
ctx.stroke();
}
}
Component.onCompleted: requestPaint()
Connections {
function onAnimatedPercentageChanged() {
gaugeCanvas.requestPaint();
}
target: gaugeCard
}
Connections {
function onPaletteChanged() {
gaugeCanvas.requestPaint();
}
target: Colours
}
} }
StyledText { StyledText {
@ -642,51 +607,15 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
Canvas { ArcGauge {
id: storageGaugeCanvas
anchors.centerIn: parent anchors.centerIn: parent
width: Math.min(parent.width, parent.height) width: Math.min(parent.width, parent.height)
height: width height: width
onPaint: { percentage: storageGaugeCard.animatedPercentage
const ctx = getContext("2d"); accentColor: storageGaugeCard.accentColor
ctx.reset(); trackColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)
const cx = width / 2; startAngle: storageGaugeCard.arcStartAngle
const cy = height / 2; sweepAngle: storageGaugeCard.arcSweep
const radius = (Math.min(width, height) - 12) / 2;
const lineWidth = 10;
ctx.beginPath();
ctx.arc(cx, cy, radius, storageGaugeCard.arcStartAngle, storageGaugeCard.arcStartAngle + storageGaugeCard.arcSweep);
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.strokeStyle = Colours.layer(Colours.palette.m3surfaceContainerHigh, 2);
ctx.stroke();
if (storageGaugeCard.animatedPercentage > 0) {
ctx.beginPath();
ctx.arc(cx, cy, radius, storageGaugeCard.arcStartAngle, storageGaugeCard.arcStartAngle + storageGaugeCard.arcSweep * storageGaugeCard.animatedPercentage);
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.strokeStyle = storageGaugeCard.accentColor;
ctx.stroke();
}
}
Component.onCompleted: requestPaint()
Connections {
function onAnimatedPercentageChanged() {
storageGaugeCanvas.requestPaint();
}
target: storageGaugeCard
}
Connections {
function onPaletteChanged() {
storageGaugeCanvas.requestPaint();
}
target: Colours
}
} }
StyledText { StyledText {
@ -749,96 +678,27 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
Canvas { SparklineItem {
id: sparklineCanvas id: sparkline
property var downHistory: NetworkUsage.downloadHistory property real targetMax: Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024)
property var upHistory: NetworkUsage.uploadHistory
property real targetMax: 1024
property real smoothMax: targetMax property real smoothMax: targetMax
property real slideProgress: 0
property int _tickCount: 0
property int _lastTickCount: -1
function checkAndAnimate(): void {
const currentLength = (downHistory || []).length;
if (currentLength > 0 && _tickCount !== _lastTickCount) {
_lastTickCount = _tickCount;
updateMax();
}
}
function updateMax(): void {
const downHist = downHistory || [];
const upHist = upHistory || [];
const allValues = downHist.concat(upHist);
targetMax = Math.max(...allValues, 1024);
requestPaint();
}
anchors.fill: parent anchors.fill: parent
onDownHistoryChanged: checkAndAnimate() line1: NetworkUsage.uploadBuffer
onUpHistoryChanged: checkAndAnimate() line1Color: Colours.palette.m3secondary
onSmoothMaxChanged: requestPaint() line1FillAlpha: 0.15
onSlideProgressChanged: requestPaint() line2: NetworkUsage.downloadBuffer
line2Color: Colours.palette.m3tertiary
onPaint: { line2FillAlpha: 0.2
const ctx = getContext("2d"); maxValue: smoothMax
ctx.reset(); historyLength: NetworkUsage.historyLength
const w = width;
const h = height;
const downHist = downHistory || [];
const upHist = upHistory || [];
if (downHist.length < 2 && upHist.length < 2)
return;
const maxVal = smoothMax;
const drawLine = (history, color, fillAlpha) => {
if (history.length < 2)
return;
const len = history.length;
const stepX = w / (NetworkUsage.historyLength - 1);
const startX = w - (len - 1) * stepX - stepX * slideProgress + stepX;
ctx.beginPath();
ctx.moveTo(startX, h - (history[0] / maxVal) * h);
for (let i = 1; i < len; i++) {
const x = startX + i * stepX;
const y = h - (history[i] / maxVal) * h;
ctx.lineTo(x, y);
}
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.stroke();
ctx.lineTo(startX + (len - 1) * stepX, h);
ctx.lineTo(startX, h);
ctx.closePath();
ctx.fillStyle = Qt.rgba(Qt.color(color).r, Qt.color(color).g, Qt.color(color).b, fillAlpha);
ctx.fill();
};
drawLine(upHist, Colours.palette.m3secondary.toString(), 0.15);
drawLine(downHist, Colours.palette.m3tertiary.toString(), 0.2);
}
Component.onCompleted: updateMax()
Connections {
function onPaletteChanged() {
sparklineCanvas.requestPaint();
}
target: Colours
}
Timer { Timer {
interval: Config.dashboard.resourceUpdateInterval interval: Config.dashboard.resourceUpdateInterval
running: networkCard.visible running: networkCard.visible
repeat: true repeat: true
onTriggered: sparklineCanvas._tickCount++ onTriggered: sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024)
} }
NumberAnimation on slideProgress { NumberAnimation on slideProgress {
@ -862,7 +722,7 @@ Item {
text: qsTr("Collecting data...") text: qsTr("Collecting data...")
font.pointSize: Appearance.font.size.small font.pointSize: Appearance.font.size.small
color: Colours.palette.m3onSurfaceVariant color: Colours.palette.m3onSurfaceVariant
visible: NetworkUsage.downloadHistory.length < 2 visible: NetworkUsage.downloadBuffer.count < 2
opacity: 0.6 opacity: 0.6
} }
} }

View file

@ -1,11 +1,14 @@
qml_module(caelestia-internal qml_module(caelestia-internal
URI Caelestia.Internal URI Caelestia.Internal
SOURCES SOURCES
arcgauge.hpp arcgauge.cpp
cachingimagemanager.hpp cachingimagemanager.cpp cachingimagemanager.hpp cachingimagemanager.cpp
circularbuffer.hpp circularbuffer.cpp
circularindicatormanager.hpp circularindicatormanager.cpp circularindicatormanager.hpp circularindicatormanager.cpp
hyprdevices.hpp hyprdevices.cpp hyprdevices.hpp hyprdevices.cpp
hyprextras.hpp hyprextras.cpp hyprextras.hpp hyprextras.cpp
logindmanager.hpp logindmanager.cpp logindmanager.hpp logindmanager.cpp
sparklineitem.hpp sparklineitem.cpp
LIBRARIES LIBRARIES
Qt::Gui Qt::Gui
Qt::Quick Qt::Quick

View file

@ -0,0 +1,119 @@
#include "arcgauge.hpp"
#include <QtMath>
#include <qpainter.h>
#include <qpen.h>
namespace caelestia::internal {
ArcGauge::ArcGauge(QQuickItem* parent)
: QQuickPaintedItem(parent) {
setAntialiasing(true);
}
void ArcGauge::paint(QPainter* painter) {
const qreal w = width();
const qreal h = height();
const qreal side = qMin(w, h);
const qreal radius = (side - m_lineWidth - 2.0) / 2.0;
const qreal cx = w / 2.0;
const qreal cy = h / 2.0;
const QRectF arcRect(cx - radius, cy - radius, radius * 2.0, radius * 2.0);
// Convert from Canvas convention (CW radians from 3 o'clock) to QPainter (CCW 1/16th degrees)
const int startAngle16 = qRound(-(m_startAngle * 180.0 / M_PI) * 16.0);
const int sweepAngle16 = qRound(-(m_sweepAngle * 180.0 / M_PI) * 16.0);
painter->setRenderHint(QPainter::Antialiasing, true);
// Draw track arc
QPen trackPen(m_trackColor, m_lineWidth);
trackPen.setCapStyle(Qt::RoundCap);
painter->setPen(trackPen);
painter->setBrush(Qt::NoBrush);
painter->drawArc(arcRect, startAngle16, sweepAngle16);
// Draw value arc
if (m_percentage > 0.0) {
const int valueSweep16 = qRound(static_cast<qreal>(sweepAngle16) * m_percentage);
QPen valuePen(m_accentColor, m_lineWidth);
valuePen.setCapStyle(Qt::RoundCap);
painter->setPen(valuePen);
painter->drawArc(arcRect, startAngle16, valueSweep16);
}
}
qreal ArcGauge::percentage() const {
return m_percentage;
}
void ArcGauge::setPercentage(qreal percentage) {
if (qFuzzyCompare(m_percentage, percentage))
return;
m_percentage = percentage;
emit percentageChanged();
update();
}
QColor ArcGauge::accentColor() const {
return m_accentColor;
}
void ArcGauge::setAccentColor(const QColor& color) {
if (m_accentColor == color)
return;
m_accentColor = color;
emit accentColorChanged();
update();
}
QColor ArcGauge::trackColor() const {
return m_trackColor;
}
void ArcGauge::setTrackColor(const QColor& color) {
if (m_trackColor == color)
return;
m_trackColor = color;
emit trackColorChanged();
update();
}
qreal ArcGauge::startAngle() const {
return m_startAngle;
}
void ArcGauge::setStartAngle(qreal angle) {
if (qFuzzyCompare(m_startAngle, angle))
return;
m_startAngle = angle;
emit startAngleChanged();
update();
}
qreal ArcGauge::sweepAngle() const {
return m_sweepAngle;
}
void ArcGauge::setSweepAngle(qreal angle) {
if (qFuzzyCompare(m_sweepAngle, angle))
return;
m_sweepAngle = angle;
emit sweepAngleChanged();
update();
}
qreal ArcGauge::lineWidth() const {
return m_lineWidth;
}
void ArcGauge::setLineWidth(qreal width) {
if (qFuzzyCompare(m_lineWidth, width))
return;
m_lineWidth = width;
emit lineWidthChanged();
update();
}
} // namespace caelestia::internal

View file

@ -0,0 +1,61 @@
#pragma once
#include <qcolor.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qquickpainteditem.h>
namespace caelestia::internal {
class ArcGauge : public QQuickPaintedItem {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(qreal percentage READ percentage WRITE setPercentage NOTIFY percentageChanged)
Q_PROPERTY(QColor accentColor READ accentColor WRITE setAccentColor NOTIFY accentColorChanged)
Q_PROPERTY(QColor trackColor READ trackColor WRITE setTrackColor NOTIFY trackColorChanged)
Q_PROPERTY(qreal startAngle READ startAngle WRITE setStartAngle NOTIFY startAngleChanged)
Q_PROPERTY(qreal sweepAngle READ sweepAngle WRITE setSweepAngle NOTIFY sweepAngleChanged)
Q_PROPERTY(qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY lineWidthChanged)
public:
explicit ArcGauge(QQuickItem* parent = nullptr);
void paint(QPainter* painter) override;
[[nodiscard]] qreal percentage() const;
void setPercentage(qreal percentage);
[[nodiscard]] QColor accentColor() const;
void setAccentColor(const QColor& color);
[[nodiscard]] QColor trackColor() const;
void setTrackColor(const QColor& color);
[[nodiscard]] qreal startAngle() const;
void setStartAngle(qreal angle);
[[nodiscard]] qreal sweepAngle() const;
void setSweepAngle(qreal angle);
[[nodiscard]] qreal lineWidth() const;
void setLineWidth(qreal width);
signals:
void percentageChanged();
void accentColorChanged();
void trackColorChanged();
void startAngleChanged();
void sweepAngleChanged();
void lineWidthChanged();
private:
qreal m_percentage = 0.0;
QColor m_accentColor;
QColor m_trackColor;
qreal m_startAngle = 0.75 * M_PI;
qreal m_sweepAngle = 1.5 * M_PI;
qreal m_lineWidth = 10.0;
};
} // namespace caelestia::internal

View file

@ -0,0 +1,94 @@
#include "circularbuffer.hpp"
#include <algorithm>
namespace caelestia::internal {
CircularBuffer::CircularBuffer(QObject* parent)
: QObject(parent) {}
int CircularBuffer::capacity() const {
return m_capacity;
}
void CircularBuffer::setCapacity(int capacity) {
if (capacity < 0)
capacity = 0;
if (m_capacity == capacity)
return;
const auto old = values();
m_capacity = capacity;
m_data.resize(capacity);
m_data.fill(0.0);
m_head = 0;
m_count = 0;
// Re-push old values, keeping the most recent ones
const auto start = old.size() > capacity ? old.size() - capacity : 0;
for (auto i = start; i < old.size(); ++i) {
m_data[m_head] = old[i];
m_head = (m_head + 1) % m_capacity;
m_count++;
}
emit capacityChanged();
emit countChanged();
emit valuesChanged();
}
int CircularBuffer::count() const {
return m_count;
}
QList<qreal> CircularBuffer::values() const {
QList<qreal> result;
result.reserve(m_count);
for (int i = 0; i < m_count; ++i)
result.append(at(i));
return result;
}
qreal CircularBuffer::maximum() const {
if (m_count == 0)
return 0.0;
qreal maxVal = at(0);
for (int i = 1; i < m_count; ++i)
maxVal = std::max(maxVal, at(i));
return maxVal;
}
void CircularBuffer::push(qreal value) {
if (m_capacity <= 0)
return;
m_data[m_head] = value;
m_head = (m_head + 1) % m_capacity;
if (m_count < m_capacity) {
m_count++;
emit countChanged();
}
emit valuesChanged();
}
void CircularBuffer::clear() {
if (m_count == 0)
return;
m_head = 0;
m_count = 0;
emit countChanged();
emit valuesChanged();
}
qreal CircularBuffer::at(int index) const {
if (index < 0 || index >= m_count)
return 0.0;
const int actualIndex = (m_head - m_count + index + m_capacity) % m_capacity;
return m_data[actualIndex];
}
} // namespace caelestia::internal

View file

@ -0,0 +1,44 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include <qvector.h>
namespace caelestia::internal {
class CircularBuffer : public QObject {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(int capacity READ capacity WRITE setCapacity NOTIFY capacityChanged)
Q_PROPERTY(int count READ count NOTIFY countChanged)
Q_PROPERTY(QList<qreal> values READ values NOTIFY valuesChanged)
Q_PROPERTY(qreal maximum READ maximum NOTIFY valuesChanged)
public:
explicit CircularBuffer(QObject* parent = nullptr);
[[nodiscard]] int capacity() const;
void setCapacity(int capacity);
[[nodiscard]] int count() const;
[[nodiscard]] QList<qreal> values() const;
[[nodiscard]] qreal maximum() const;
Q_INVOKABLE void push(qreal value);
Q_INVOKABLE void clear();
Q_INVOKABLE [[nodiscard]] qreal at(int index) const;
signals:
void capacityChanged();
void countChanged();
void valuesChanged();
private:
QVector<qreal> m_data;
int m_head = 0;
int m_count = 0;
int m_capacity = 0;
};
} // namespace caelestia::internal

View file

@ -0,0 +1,212 @@
#include "sparklineitem.hpp"
#include <qpainter.h>
#include <qpainterpath.h>
#include <qpen.h>
namespace caelestia::internal {
SparklineItem::SparklineItem(QQuickItem* parent)
: QQuickPaintedItem(parent) {
setAntialiasing(true);
}
void SparklineItem::paint(QPainter* painter) {
const bool has1 = m_line1 && m_line1->count() >= 2;
const bool has2 = m_line2 && m_line2->count() >= 2;
if (!has1 && !has2)
return;
painter->setRenderHint(QPainter::Antialiasing, true);
// Draw line1 first (behind), then line2 (in front)
if (has1)
drawLine(painter, m_line1, m_line1Color, m_line1FillAlpha);
if (has2)
drawLine(painter, m_line2, m_line2Color, m_line2FillAlpha);
}
void SparklineItem::drawLine(QPainter* painter, CircularBuffer* buffer, const QColor& color, qreal fillAlpha) {
const qreal w = width();
const qreal h = height();
const int len = buffer->count();
const qreal stepX = w / static_cast<qreal>(m_historyLength - 1);
const qreal startX = w - (len - 1) * stepX - stepX * m_slideProgress + stepX;
// Build line path
QPainterPath linePath;
linePath.moveTo(startX, h - (buffer->at(0) / m_maxValue) * h);
for (int i = 1; i < len; ++i) {
const qreal x = startX + i * stepX;
const qreal y = h - (buffer->at(i) / m_maxValue) * h;
linePath.lineTo(x, y);
}
// Stroke the line
QPen pen(color, m_lineWidth);
pen.setCapStyle(Qt::RoundCap);
pen.setJoinStyle(Qt::RoundJoin);
painter->setPen(pen);
painter->setBrush(Qt::NoBrush);
painter->drawPath(linePath);
// Fill under the line
QPainterPath fillPath = linePath;
fillPath.lineTo(startX + (len - 1) * stepX, h);
fillPath.lineTo(startX, h);
fillPath.closeSubpath();
QColor fillColor = color;
fillColor.setAlphaF(static_cast<float>(fillAlpha));
painter->setPen(Qt::NoPen);
painter->setBrush(fillColor);
painter->drawPath(fillPath);
}
void SparklineItem::connectBuffer(CircularBuffer* buffer) {
if (!buffer)
return;
connect(buffer, &CircularBuffer::valuesChanged, this, [this]() {
update();
});
connect(buffer, &QObject::destroyed, this, [this, buffer]() {
if (m_line1 == buffer) {
m_line1 = nullptr;
emit line1Changed();
}
if (m_line2 == buffer) {
m_line2 = nullptr;
emit line2Changed();
}
update();
});
}
CircularBuffer* SparklineItem::line1() const {
return m_line1;
}
void SparklineItem::setLine1(CircularBuffer* buffer) {
if (m_line1 == buffer)
return;
if (m_line1)
disconnect(m_line1, nullptr, this, nullptr);
m_line1 = buffer;
connectBuffer(buffer);
emit line1Changed();
update();
}
CircularBuffer* SparklineItem::line2() const {
return m_line2;
}
void SparklineItem::setLine2(CircularBuffer* buffer) {
if (m_line2 == buffer)
return;
if (m_line2)
disconnect(m_line2, nullptr, this, nullptr);
m_line2 = buffer;
connectBuffer(buffer);
emit line2Changed();
update();
}
QColor SparklineItem::line1Color() const {
return m_line1Color;
}
void SparklineItem::setLine1Color(const QColor& color) {
if (m_line1Color == color)
return;
m_line1Color = color;
emit line1ColorChanged();
update();
}
QColor SparklineItem::line2Color() const {
return m_line2Color;
}
void SparklineItem::setLine2Color(const QColor& color) {
if (m_line2Color == color)
return;
m_line2Color = color;
emit line2ColorChanged();
update();
}
qreal SparklineItem::line1FillAlpha() const {
return m_line1FillAlpha;
}
void SparklineItem::setLine1FillAlpha(qreal alpha) {
if (qFuzzyCompare(m_line1FillAlpha, alpha))
return;
m_line1FillAlpha = alpha;
emit line1FillAlphaChanged();
update();
}
qreal SparklineItem::line2FillAlpha() const {
return m_line2FillAlpha;
}
void SparklineItem::setLine2FillAlpha(qreal alpha) {
if (qFuzzyCompare(m_line2FillAlpha, alpha))
return;
m_line2FillAlpha = alpha;
emit line2FillAlphaChanged();
update();
}
qreal SparklineItem::maxValue() const {
return m_maxValue;
}
void SparklineItem::setMaxValue(qreal value) {
if (qFuzzyCompare(m_maxValue, value))
return;
m_maxValue = value;
emit maxValueChanged();
update();
}
qreal SparklineItem::slideProgress() const {
return m_slideProgress;
}
void SparklineItem::setSlideProgress(qreal progress) {
if (qFuzzyCompare(m_slideProgress, progress))
return;
m_slideProgress = progress;
emit slideProgressChanged();
update();
}
int SparklineItem::historyLength() const {
return m_historyLength;
}
void SparklineItem::setHistoryLength(int length) {
if (m_historyLength == length)
return;
m_historyLength = length;
emit historyLengthChanged();
update();
}
qreal SparklineItem::lineWidth() const {
return m_lineWidth;
}
void SparklineItem::setLineWidth(qreal width) {
if (qFuzzyCompare(m_lineWidth, width))
return;
m_lineWidth = width;
emit lineWidthChanged();
update();
}
} // namespace caelestia::internal

View file

@ -0,0 +1,90 @@
#pragma once
#include <qcolor.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qquickpainteditem.h>
#include "circularbuffer.hpp"
namespace caelestia::internal {
class SparklineItem : public QQuickPaintedItem {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(CircularBuffer* line1 READ line1 WRITE setLine1 NOTIFY line1Changed)
Q_PROPERTY(CircularBuffer* line2 READ line2 WRITE setLine2 NOTIFY line2Changed)
Q_PROPERTY(QColor line1Color READ line1Color WRITE setLine1Color NOTIFY line1ColorChanged)
Q_PROPERTY(QColor line2Color READ line2Color WRITE setLine2Color NOTIFY line2ColorChanged)
Q_PROPERTY(qreal line1FillAlpha READ line1FillAlpha WRITE setLine1FillAlpha NOTIFY line1FillAlphaChanged)
Q_PROPERTY(qreal line2FillAlpha READ line2FillAlpha WRITE setLine2FillAlpha NOTIFY line2FillAlphaChanged)
Q_PROPERTY(qreal maxValue READ maxValue WRITE setMaxValue NOTIFY maxValueChanged)
Q_PROPERTY(qreal slideProgress READ slideProgress WRITE setSlideProgress NOTIFY slideProgressChanged)
Q_PROPERTY(int historyLength READ historyLength WRITE setHistoryLength NOTIFY historyLengthChanged)
Q_PROPERTY(qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY lineWidthChanged)
public:
explicit SparklineItem(QQuickItem* parent = nullptr);
void paint(QPainter* painter) override;
[[nodiscard]] CircularBuffer* line1() const;
void setLine1(CircularBuffer* buffer);
[[nodiscard]] CircularBuffer* line2() const;
void setLine2(CircularBuffer* buffer);
[[nodiscard]] QColor line1Color() const;
void setLine1Color(const QColor& color);
[[nodiscard]] QColor line2Color() const;
void setLine2Color(const QColor& color);
[[nodiscard]] qreal line1FillAlpha() const;
void setLine1FillAlpha(qreal alpha);
[[nodiscard]] qreal line2FillAlpha() const;
void setLine2FillAlpha(qreal alpha);
[[nodiscard]] qreal maxValue() const;
void setMaxValue(qreal value);
[[nodiscard]] qreal slideProgress() const;
void setSlideProgress(qreal progress);
[[nodiscard]] int historyLength() const;
void setHistoryLength(int length);
[[nodiscard]] qreal lineWidth() const;
void setLineWidth(qreal width);
signals:
void line1Changed();
void line2Changed();
void line1ColorChanged();
void line2ColorChanged();
void line1FillAlphaChanged();
void line2FillAlphaChanged();
void maxValueChanged();
void slideProgressChanged();
void historyLengthChanged();
void lineWidthChanged();
private:
void drawLine(QPainter* painter, CircularBuffer* buffer, const QColor& color, qreal fillAlpha);
void connectBuffer(CircularBuffer* buffer);
CircularBuffer* m_line1 = nullptr;
CircularBuffer* m_line2 = nullptr;
QColor m_line1Color;
QColor m_line2Color;
qreal m_line1FillAlpha = 0.15;
qreal m_line2FillAlpha = 0.2;
qreal m_maxValue = 1024.0;
qreal m_slideProgress = 0.0;
int m_historyLength = 30;
qreal m_lineWidth = 2.0;
};
} // namespace caelestia::internal

View file

@ -5,6 +5,8 @@ import qs.config
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Caelestia.Internal
import QtQuick import QtQuick
Singleton { Singleton {
@ -20,9 +22,9 @@ Singleton {
readonly property real downloadTotal: _downloadTotal readonly property real downloadTotal: _downloadTotal
readonly property real uploadTotal: _uploadTotal readonly property real uploadTotal: _uploadTotal
// History of speeds for sparkline (most recent at end) // History buffers for sparkline
readonly property var downloadHistory: _downloadHistory readonly property CircularBuffer downloadBuffer: _downloadBuffer
readonly property var uploadHistory: _uploadHistory readonly property CircularBuffer uploadBuffer: _uploadBuffer
readonly property int historyLength: 30 readonly property int historyLength: 30
// Private properties // Private properties
@ -30,8 +32,6 @@ Singleton {
property real _uploadSpeed: 0 property real _uploadSpeed: 0
property real _downloadTotal: 0 property real _downloadTotal: 0
property real _uploadTotal: 0 property real _uploadTotal: 0
property var _downloadHistory: []
property var _uploadHistory: []
// Previous readings for calculating speed // Previous readings for calculating speed
property real _prevRxBytes: 0 property real _prevRxBytes: 0
@ -139,6 +139,16 @@ Singleton {
}; };
} }
CircularBuffer {
id: _downloadBuffer
capacity: root.historyLength + 1
}
CircularBuffer {
id: _uploadBuffer
capacity: root.historyLength + 1
}
FileView { FileView {
id: netDevFile id: netDevFile
path: "/proc/net/dev" path: "/proc/net/dev"
@ -189,17 +199,11 @@ Singleton {
root._downloadSpeed = rxDelta / timeDelta; root._downloadSpeed = rxDelta / timeDelta;
root._uploadSpeed = txDelta / timeDelta; root._uploadSpeed = txDelta / timeDelta;
const maxHistory = root.historyLength + 1; if (root._downloadSpeed >= 0 && isFinite(root._downloadSpeed))
_downloadBuffer.push(root._downloadSpeed);
if (root._downloadSpeed >= 0 && isFinite(root._downloadSpeed)) { if (root._uploadSpeed >= 0 && isFinite(root._uploadSpeed))
const dh = root._downloadHistory; _uploadBuffer.push(root._uploadSpeed);
root._downloadHistory = dh.length >= maxHistory ? [...dh.slice(1), root._downloadSpeed] : [...dh, root._downloadSpeed];
}
if (root._uploadSpeed >= 0 && isFinite(root._uploadSpeed)) {
const uh = root._uploadHistory;
root._uploadHistory = uh.length >= maxHistory ? [...uh.slice(1), root._uploadSpeed] : [...uh, root._uploadSpeed];
}
} }
// Calculate totals with overflow handling // Calculate totals with overflow handling