This commit is contained in:
2026-03-31 01:34:10 +02:00
parent 35e6207ad2
commit 883bac89cb
8 changed files with 563 additions and 313 deletions
+22 -28
View File
@@ -1,17 +1,14 @@
pragma Singleton pragma Singleton
import qs.Config import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import ZShell.Internal
import QtQuick import qs.Config
Singleton { Singleton {
id: root id: root
property var _downloadHistory: []
// Private properties // Private properties
property real _downloadSpeed: 0 property real _downloadSpeed: 0
property real _downloadTotal: 0 property real _downloadTotal: 0
@@ -25,12 +22,11 @@ Singleton {
property real _prevRxBytes: 0 property real _prevRxBytes: 0
property real _prevTimestamp: 0 property real _prevTimestamp: 0
property real _prevTxBytes: 0 property real _prevTxBytes: 0
property var _uploadHistory: []
property real _uploadSpeed: 0 property real _uploadSpeed: 0
property real _uploadTotal: 0 property real _uploadTotal: 0
// History of speeds for sparkline (most recent at end) // History buffers for sparkline
readonly property var downloadHistory: _downloadHistory readonly property CircularBuffer downloadBuffer: _downloadBuffer
// Current speeds in bytes per second // Current speeds in bytes per second
readonly property real downloadSpeed: _downloadSpeed readonly property real downloadSpeed: _downloadSpeed
@@ -39,7 +35,7 @@ Singleton {
readonly property real downloadTotal: _downloadTotal readonly property real downloadTotal: _downloadTotal
readonly property int historyLength: 30 readonly property int historyLength: 30
property int refCount: 0 property int refCount: 0
readonly property var uploadHistory: _uploadHistory readonly property CircularBuffer uploadBuffer: _uploadBuffer
readonly property real uploadSpeed: _uploadSpeed readonly property real uploadSpeed: _uploadSpeed
readonly property real uploadTotal: _uploadTotal readonly property real uploadTotal: _uploadTotal
@@ -139,6 +135,18 @@ Singleton {
}; };
} }
CircularBuffer {
id: _downloadBuffer
capacity: root.historyLength + 1
}
CircularBuffer {
id: _uploadBuffer
capacity: root.historyLength + 1
}
FileView { FileView {
id: netDevFile id: netDevFile
@@ -190,25 +198,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))
let newDownHist = root._downloadHistory.slice(); _uploadBuffer.push(root._uploadSpeed);
newDownHist.push(root._downloadSpeed);
if (newDownHist.length > maxHistory) {
newDownHist.shift();
}
root._downloadHistory = newDownHist;
}
if (root._uploadSpeed >= 0 && isFinite(root._uploadSpeed)) {
let newUpHist = root._uploadHistory.slice();
newUpHist.push(root._uploadSpeed);
if (newUpHist.length > maxHistory) {
newUpHist.shift();
}
root._uploadHistory = newUpHist;
}
} }
// Calculate totals with overflow handling // Calculate totals with overflow handling
+61 -252
View File
@@ -1,75 +1,30 @@
import Quickshell
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell.Services.UPower import Quickshell.Services.UPower
import ZShell.Internal
import qs.Components import qs.Components
import qs.Config
import qs.Helpers import qs.Helpers
import qs.Config
Item { Item {
id: root id: root
readonly property int minWidth: 400 + 400 + Appearance.spacing.normal + 120 + Appearance.padding.large * 2 readonly property int minWidth: 400 + 400 + Appearance.spacing.normal + 120 + Appearance.padding.large * 2
readonly property real nonAnimHeight: (placeholder.visible ? placeholder.height : content.implicitHeight) + Appearance.padding.normal * 2
readonly property real nonAnimWidth: Math.max(minWidth, content.implicitWidth) + Appearance.padding.normal * 2
required property real padding
required property PersistentProperties visibilities
function displayTemp(temp: real): string { function displayTemp(temp: real): string {
return `${Math.ceil(temp)}°C`; return `${Math.ceil(temp)}°C`;
} }
implicitHeight: nonAnimHeight implicitHeight: content.implicitHeight
implicitWidth: nonAnimWidth implicitWidth: Math.max(minWidth, content.implicitWidth)
CustomRect {
id: placeholder
anchors.centerIn: parent
color: DynamicColors.tPalette.m3surfaceContainer
height: 350
radius: Appearance.rounding.large - 10
visible: false
width: 400
ColumnLayout {
anchors.centerIn: parent
spacing: Appearance.spacing.normal
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.extraLarge * 2
text: "tune"
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: DynamicColors.palette.m3onSurface
font.pointSize: Appearance.font.size.large
text: qsTr("No widgets enabled")
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
text: qsTr("Enable widgets in dashboard settings")
}
}
}
RowLayout { RowLayout {
id: content id: content
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: root.padding
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: root.padding
anchors.verticalCenter: parent.verticalCenter
spacing: Appearance.spacing.normal spacing: Appearance.spacing.normal
visible: !placeholder.visible
Ref { Ref {
service: SystemUsage service: SystemUsage
@@ -84,7 +39,7 @@ Item {
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Appearance.spacing.normal spacing: Appearance.spacing.normal
visible: true visible: Config.dashboard.performance.showCpu || (Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE")
HeroCard { HeroCard {
Layout.fillWidth: true Layout.fillWidth: true
@@ -172,7 +127,7 @@ Item {
property real percentage: UPower.displayDevice.percentage property real percentage: UPower.displayDevice.percentage
color: DynamicColors.tPalette.m3surfaceContainer color: DynamicColors.tPalette.m3surfaceContainer
radius: Appearance.rounding.large - 10 radius: Appearance.rounding.large
Behavior on animatedPercentage { Behavior on animatedPercentage {
Anim { Anim {
@@ -318,7 +273,7 @@ Item {
clip: true clip: true
color: DynamicColors.tPalette.m3surfaceContainer color: DynamicColors.tPalette.m3surfaceContainer
radius: Appearance.rounding.large - 10 radius: Appearance.rounding.large
Behavior on animatedPercentage { Behavior on animatedPercentage {
Anim { Anim {
@@ -344,52 +299,15 @@ Item {
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
Canvas { ArcGauge {
id: gaugeCanvas accentColor: gaugeCard.accentColor
anchors.centerIn: parent anchors.centerIn: parent
height: width height: width
percentage: gaugeCard.animatedPercentage
startAngle: gaugeCard.arcStartAngle
sweepAngle: gaugeCard.arcSweep
trackColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
width: Math.min(parent.width, parent.height) width: Math.min(parent.width, parent.height)
Component.onCompleted: requestPaint()
onPaint: {
const ctx = getContext("2d");
ctx.reset();
const cx = width / 2;
const cy = height / 2;
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 = DynamicColors.layer(DynamicColors.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();
}
}
Connections {
function onAnimatedPercentageChanged() {
gaugeCanvas.requestPaint();
}
target: gaugeCard
}
Connections {
function onPaletteChanged() {
gaugeCanvas.requestPaint();
}
target: DynamicColors
}
} }
CustomText { CustomText {
@@ -427,7 +345,7 @@ Item {
property real usage: 0 property real usage: 0
color: DynamicColors.tPalette.m3surfaceContainer color: DynamicColors.tPalette.m3surfaceContainer
radius: Appearance.rounding.large - 10 radius: Appearance.rounding.large
Behavior on animatedTemp { Behavior on animatedTemp {
Anim { Anim {
@@ -452,31 +370,26 @@ Item {
anchors.left: parent.left anchors.left: parent.left
anchors.top: parent.top anchors.top: parent.top
color: Qt.alpha(heroCard.accentColor, 0.15) color: Qt.alpha(heroCard.accentColor, 0.15)
width: parent.width * heroCard.animatedUsage implicitWidth: parent.width * heroCard.animatedUsage
} }
ColumnLayout {
anchors.bottomMargin: Appearance.padding.normal
anchors.fill: parent
anchors.leftMargin: Appearance.padding.large
anchors.rightMargin: Appearance.padding.large
anchors.topMargin: Appearance.padding.normal
spacing: Appearance.spacing.small
CardHeader { CardHeader {
accentColor: heroCard.accentColor accentColor: heroCard.accentColor
anchors.left: parent.left
anchors.leftMargin: Appearance.padding.large
anchors.top: parent.top
anchors.topMargin: Math.round(Appearance.padding.large * 1.2)
icon: heroCard.icon icon: heroCard.icon
title: heroCard.title title: heroCard.title
width: parent.width - anchors.leftMargin - usageColumn.anchors.rightMargin - usageLabel.width - Appearance.spacing.normal
} }
RowLayout {
Layout.fillHeight: true
Layout.fillWidth: true
spacing: Appearance.spacing.normal
Column { Column {
Layout.alignment: Qt.AlignBottom anchors.bottom: parent.bottom
Layout.fillWidth: true anchors.bottomMargin: Math.round(Appearance.padding.large * 1.3)
anchors.left: parent.left
anchors.margins: Math.round(Appearance.padding.large * 1.2)
anchors.right: parent.right
spacing: Appearance.spacing.small spacing: Appearance.spacing.small
Row { Row {
@@ -499,19 +412,15 @@ Item {
ProgressBar { ProgressBar {
bgColor: Qt.alpha(heroCard.accentColor, 0.2) bgColor: Qt.alpha(heroCard.accentColor, 0.2)
fgColor: heroCard.accentColor fgColor: heroCard.accentColor
height: 6 implicitHeight: 6
implicitWidth: parent.width * 0.5
value: heroCard.tempProgress value: heroCard.tempProgress
width: parent.width * 0.5
}
}
Item {
Layout.fillWidth: true
}
} }
} }
Column { Column {
id: usageColumn
anchors.margins: Appearance.padding.large anchors.margins: Appearance.padding.large
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 32 anchors.rightMargin: 32
@@ -519,6 +428,8 @@ Item {
spacing: 0 spacing: 0
CustomText { CustomText {
id: usageLabel
anchors.right: parent.right anchors.right: parent.right
color: DynamicColors.palette.m3onSurfaceVariant color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.normal font.pointSize: Appearance.font.size.normal
@@ -541,7 +452,7 @@ Item {
clip: true clip: true
color: DynamicColors.tPalette.m3surfaceContainer color: DynamicColors.tPalette.m3surfaceContainer
radius: Appearance.rounding.large - 10 radius: Appearance.rounding.large
Ref { Ref {
service: NetworkUsage service: NetworkUsage
@@ -563,109 +474,45 @@ Item {
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
Canvas { SparklineItem {
id: sparklineCanvas id: sparkline
property int _lastTickCount: -1
property int _tickCount: 0
property var downHistory: NetworkUsage.downloadHistory
property real slideProgress: 0
property real smoothMax: targetMax property real smoothMax: targetMax
property real targetMax: 1024 property real targetMax: 1024
property var upHistory: NetworkUsage.uploadHistory
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
historyLength: NetworkUsage.historyLength
line1: NetworkUsage.uploadBuffer // qmllint disable missing-type
line1Color: DynamicColors.palette.m3secondary
line1FillAlpha: 0.15
line2: NetworkUsage.downloadBuffer // qmllint disable missing-type
line2Color: DynamicColors.palette.m3tertiary
line2FillAlpha: 0.2
maxValue: smoothMax
NumberAnimation on slideProgress {
duration: Config.dashboard.resourceUpdateInterval
from: 0
loops: Animation.Infinite
running: true
to: 1
}
Behavior on smoothMax { Behavior on smoothMax {
Anim { Anim {
duration: Appearance.anim.durations.large duration: Appearance.anim.durations.large
} }
} }
Component.onCompleted: updateMax()
onDownHistoryChanged: checkAndAnimate()
onPaint: {
const ctx = getContext("2d");
ctx.reset();
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, DynamicColors.palette.m3secondary.toString(), 0.15);
drawLine(downHist, DynamicColors.palette.m3tertiary.toString(), 0.2);
}
onSlideProgressChanged: requestPaint()
onSmoothMaxChanged: requestPaint()
onUpHistoryChanged: checkAndAnimate()
Connections { Connections {
function onPaletteChanged() { function onValuesChanged(): void {
sparklineCanvas.requestPaint(); sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024);
slideAnim.restart();
} }
target: DynamicColors target: NetworkUsage.downloadBuffer
} }
Timer { NumberAnimation {
interval: Config.dashboard.resourceUpdateInterval id: slideAnim
repeat: true
running: true
onTriggered: sparklineCanvas._tickCount++ duration: Config.dashboard.resourceUpdateInterval
from: 0
property: "slideProgress"
target: sparkline
to: 1
} }
} }
@@ -676,7 +523,7 @@ Item {
font.pointSize: Appearance.font.size.small font.pointSize: Appearance.font.size.small
opacity: 0.6 opacity: 0.6
text: qsTr("Collecting data...") text: qsTr("Collecting data...")
visible: NetworkUsage.downloadHistory.length < 2 visible: NetworkUsage.downloadBuffer.count < 2
} }
} }
@@ -819,7 +666,7 @@ Item {
clip: true clip: true
color: DynamicColors.tPalette.m3surfaceContainer color: DynamicColors.tPalette.m3surfaceContainer
radius: Appearance.rounding.large - 10 radius: Appearance.rounding.large
Behavior on animatedPercentage { Behavior on animatedPercentage {
Anim { Anim {
@@ -891,7 +738,6 @@ Item {
HoverHandler { HoverHandler {
id: hintHover id: hintHover
} }
} }
} }
@@ -900,52 +746,15 @@ Item {
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
Canvas { ArcGauge {
id: storageGaugeCanvas accentColor: storageGaugeCard.accentColor
anchors.centerIn: parent anchors.centerIn: parent
height: width height: width
percentage: storageGaugeCard.animatedPercentage
startAngle: storageGaugeCard.arcStartAngle
sweepAngle: storageGaugeCard.arcSweep
trackColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
width: Math.min(parent.width, parent.height) width: Math.min(parent.width, parent.height)
Component.onCompleted: requestPaint()
onPaint: {
const ctx = getContext("2d");
ctx.reset();
const cx = width / 2;
const cy = height / 2;
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 = DynamicColors.layer(DynamicColors.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();
}
}
Connections {
function onAnimatedPercentageChanged() {
storageGaugeCanvas.requestPaint();
}
target: storageGaugeCard
}
Connections {
function onPaletteChanged() {
storageGaugeCanvas.requestPaint();
}
target: DynamicColors
}
} }
CustomText { CustomText {
+10 -8
View File
@@ -103,17 +103,18 @@ CustomClippingRect {
Layout.fillHeight: true Layout.fillHeight: true
Layout.preferredWidth: 300 Layout.preferredWidth: 300
CustomText { MarqueeText {
id: versionFrom id: versionFrom
Layout.fillHeight: true Layout.fillHeight: true
Layout.preferredWidth: 125 Layout.preferredWidth: 125
animate: true
color: DynamicColors.palette.m3tertiary color: DynamicColors.palette.m3tertiary
elide: Text.ElideRight
font.pointSize: Appearance.font.size.large font.pointSize: Appearance.font.size.large
horizontalAlignment: Text.AlignHCenter marqueeEnabled: true
pauseMs: 4000
text: update.sections[1] text: update.sections[1]
verticalAlignment: Text.AlignVCenter width: 125
} }
MaterialIcon { MaterialIcon {
@@ -125,17 +126,18 @@ CustomClippingRect {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }
CustomText { MarqueeText {
id: versionTo id: versionTo
Layout.fillHeight: true Layout.fillHeight: true
Layout.preferredWidth: 120 Layout.preferredWidth: 120
animate: true
color: DynamicColors.palette.m3primary color: DynamicColors.palette.m3primary
elide: Text.ElideRight
font.pointSize: Appearance.font.size.large font.pointSize: Appearance.font.size.large
horizontalAlignment: Text.AlignHCenter marqueeEnabled: true
pauseMs: 4000
text: update.sections[3] text: update.sections[3]
verticalAlignment: Text.AlignVCenter width: 125
} }
} }
} }
+1
View File
@@ -5,6 +5,7 @@ qml_module(ZShell-internal
hyprdevices.hpp hyprdevices.cpp hyprdevices.hpp hyprdevices.cpp
cachingimagemanager.hpp cachingimagemanager.cpp cachingimagemanager.hpp cachingimagemanager.cpp
circularindicatormanager.hpp circularindicatormanager.cpp circularindicatormanager.hpp circularindicatormanager.cpp
circularbuffer.hpp circularbuffer.cpp
LIBRARIES LIBRARIES
Qt::Gui Qt::Gui
Qt::Quick Qt::Quick
@@ -0,0 +1,95 @@
#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
@@ -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
+215
View File
@@ -0,0 +1,215 @@
#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) {
if (m_historyLength < 2)
return;
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
+90
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