From 8d76df6ef5188b7fc640518b5c80687c52fcec55 Mon Sep 17 00:00:00 2001 From: Damocles Date: Fri, 24 Apr 2026 00:43:40 +0200 Subject: [PATCH] extract SparklineCanvas component from 5 applets --- shell/applets/BatteryApplet.qml | 95 ++++------------------- shell/applets/CpuApplet.qml | 33 +------- shell/applets/GpuApplet.qml | 34 ++------- shell/applets/MemoryApplet.qml | 57 ++++++-------- shell/applets/SparklineCanvas.qml | 112 ++++++++++++++++++++++++++++ shell/applets/TemperatureApplet.qml | 76 ++++--------------- shell/applets/qmldir | 3 +- test/qmllint-baseline.txt | 1 + 8 files changed, 178 insertions(+), 233 deletions(-) create mode 100644 shell/applets/SparklineCanvas.qml diff --git a/shell/applets/BatteryApplet.qml b/shell/applets/BatteryApplet.qml index f71e3c2..e6f3dc4 100644 --- a/shell/applets/BatteryApplet.qml +++ b/shell/applets/BatteryApplet.qml @@ -87,91 +87,28 @@ Column { } // 24h history sparkline (area chart) - Canvas { - id: _sparkline + SparklineCanvas { anchors.left: parent.left anchors.leftMargin: 12 anchors.right: parent.right anchors.rightMargin: 12 height: 44 - - property var _hist: S.BatteryService.history - property color _col: root._stateColor - - on_HistChanged: if (root.active) - requestPaint() - on_ColChanged: if (root.active) - requestPaint() - - onVisibleChanged: if (visible) - requestPaint() - - onPaint: { - const ctx = getContext("2d"); - if (!ctx) - return; - ctx.clearRect(0, 0, width, height); - const d = _hist; - if (d.length < 2) - return; - - const maxSamples = 1440; - const xScale = width / maxSamples; - const xOffset = (maxSamples - d.length) * xScale; - - // Background tint - ctx.fillStyle = Qt.rgba(_col.r, _col.g, _col.b, 0.07).toString(); - ctx.fillRect(0, 0, width, height); - - // Warning threshold line - const warnY = height - height * (S.BatteryService.warnThresh / 100); - ctx.strokeStyle = S.Theme.base0A.toString(); - ctx.globalAlpha = 0.3; - ctx.lineWidth = 1; - ctx.setLineDash([3, 3]); - ctx.beginPath(); - ctx.moveTo(0, warnY); - ctx.lineTo(width, warnY); - ctx.stroke(); - - // Critical threshold line - const critY = height - height * (S.BatteryService.critThresh / 100); - ctx.strokeStyle = S.Theme.base08.toString(); - ctx.beginPath(); - ctx.moveTo(0, critY); - ctx.lineTo(width, critY); - ctx.stroke(); - - ctx.setLineDash([]); - ctx.globalAlpha = 1.0; - - // Filled area under the curve - ctx.beginPath(); - ctx.moveTo(xOffset, height); - for (let i = 0; i < d.length; i++) { - const x = xOffset + i * xScale; - const y = height - height * (d[i] / 100); - ctx.lineTo(x, y); + history: S.BatteryService.history + color: root._stateColor + active: root.active + maxSamples: 1440 + backgroundTint: 0.07 + areaMode: true + thresholds: [ + { + value: S.BatteryService.warnThresh, + color: S.Theme.base0A + }, + { + value: S.BatteryService.critThresh, + color: S.Theme.base08 } - ctx.lineTo(xOffset + (d.length - 1) * xScale, height); - ctx.closePath(); - ctx.fillStyle = Qt.rgba(_col.r, _col.g, _col.b, 0.18).toString(); - ctx.fill(); - - // Top line - ctx.beginPath(); - for (let i = 0; i < d.length; i++) { - const x = xOffset + i * xScale; - const y = height - height * (d[i] / 100); - if (i === 0) - ctx.moveTo(x, y); - else - ctx.lineTo(x, y); - } - ctx.strokeStyle = _col.toString(); - ctx.lineWidth = 1.5; - ctx.stroke(); - } + ] } // Footer: thresholds + time label diff --git a/shell/applets/CpuApplet.qml b/shell/applets/CpuApplet.qml index ca14fc5..1ea97ec 100644 --- a/shell/applets/CpuApplet.qml +++ b/shell/applets/CpuApplet.qml @@ -96,41 +96,16 @@ Column { } } - // Sparkline - Canvas { + SparklineCanvas { id: sparkline anchors.right: freqLabel.left anchors.rightMargin: 6 anchors.verticalCenter: parent.verticalCenter width: 32 height: 10 - - property var _hist: root.cores[parent.parent.index]?.history ?? [] - property color _col: parent.parent._barColor - - on_HistChanged: if (root.active) - requestPaint() - on_ColChanged: if (root.active) - requestPaint() - - onVisibleChanged: if (visible) - requestPaint() - - onPaint: { - const ctx = getContext("2d"); - if (!ctx) - return; - ctx.clearRect(0, 0, width, height); - const d = _hist; - if (!d.length) - return; - const bw = width / d.length; - ctx.fillStyle = _col.toString(); - for (let i = 0; i < d.length; i++) { - const h = Math.max(1, height * d[i] / 100); - ctx.fillRect(i * bw, height - h, Math.max(1, bw - 0.5), h); - } - } + history: root.cores[parent.parent.index]?.history ?? [] + color: parent.parent._barColor + active: root.active } Text { diff --git a/shell/applets/GpuApplet.qml b/shell/applets/GpuApplet.qml index cf5b739..65164be 100644 --- a/shell/applets/GpuApplet.qml +++ b/shell/applets/GpuApplet.qml @@ -76,40 +76,16 @@ Column { } // Usage history sparkline - Canvas { - id: _sparkline + SparklineCanvas { anchors.left: parent.left anchors.leftMargin: 12 anchors.right: parent.right anchors.rightMargin: 12 height: 36 - - property var _hist: S.SystemStats.gpuHistory - - on_HistChanged: if (root.active) - requestPaint() - - onVisibleChanged: if (visible) - requestPaint() - - onPaint: { - const ctx = getContext("2d"); - if (!ctx) - return; - ctx.clearRect(0, 0, width, height); - const d = _hist; - if (!d.length) - return; - const maxSamples = 60; - const bw = width / maxSamples; - const offset = maxSamples - d.length; - for (let i = 0; i < d.length; i++) { - const barH = Math.max(1, height * d[i] / 100); - const col = S.Theme.loadColor(d[i]); - ctx.fillStyle = col.toString(); - ctx.fillRect((offset + i) * bw, height - barH, Math.max(1, bw - 0.5), barH); - } - } + history: S.SystemStats.gpuHistory + active: root.active + maxSamples: 60 + colorFunction: v => S.Theme.loadColor(v) } // VRAM section diff --git a/shell/applets/MemoryApplet.qml b/shell/applets/MemoryApplet.qml index 61bca81..6aa0005 100644 --- a/shell/applets/MemoryApplet.qml +++ b/shell/applets/MemoryApplet.qml @@ -71,49 +71,36 @@ Column { } // Memory history sparkline - Canvas { - id: memSparkline + SparklineCanvas { anchors.left: parent.left anchors.leftMargin: 12 anchors.right: parent.right anchors.rightMargin: 12 height: 18 - - property var _hist: S.SystemStats.memHistory - property color _col: root.accentColor - - on_HistChanged: if (root.active) - requestPaint() - on_ColChanged: if (root.active) - requestPaint() - - onVisibleChanged: if (visible) - requestPaint() - - onPaint: { - const ctx = getContext("2d"); - if (!ctx) - return; - ctx.clearRect(0, 0, width, height); - const d = _hist; - if (!d.length) - return; - const bw = width / 30; - ctx.fillStyle = Qt.rgba(_col.r, _col.g, _col.b, 0.15).toString(); - ctx.fillRect(0, 0, width, height); - ctx.fillStyle = _col.toString(); - for (let i = 0; i < d.length; i++) { - const h = Math.max(1, height * d[i] / 100); - ctx.fillRect((30 - d.length + i) * bw, height - h, Math.max(1, bw - 0.5), h); - } - } + history: S.SystemStats.memHistory + color: root.accentColor + active: root.active + maxSamples: 30 + backgroundTint: 0.15 } // Breakdown rows - InfoRow { label: "Used"; value: root._fmt(root.usedGb) } - InfoRow { label: "Cached"; value: root._fmt(root.cachedGb) } - InfoRow { label: "Available"; value: root._fmt(root.availGb) } - InfoRow { label: "Total"; value: root._fmt(root.totalGb) } + InfoRow { + label: "Used" + value: root._fmt(root.usedGb) + } + InfoRow { + label: "Cached" + value: root._fmt(root.cachedGb) + } + InfoRow { + label: "Available" + value: root._fmt(root.availGb) + } + InfoRow { + label: "Total" + value: root._fmt(root.totalGb) + } Separator {} diff --git a/shell/applets/SparklineCanvas.qml b/shell/applets/SparklineCanvas.qml new file mode 100644 index 0000000..a33ad46 --- /dev/null +++ b/shell/applets/SparklineCanvas.qml @@ -0,0 +1,112 @@ +import QtQuick + +Canvas { + id: root + + // Data - array of 0-100 percentage values + property var history: [] + property color color: "white" + property bool active: true + + // Max x-axis slots (0 = use history.length, right-aligns shorter data) + property int maxSamples: 0 + + // Optional background tint using `color` at low opacity + property real backgroundTint: 0 // 0 = off, e.g. 0.15 for memory, 0.08 for temperature + + // Area chart mode (battery-style filled curve + stroke line) + property bool areaMode: false + property real areaOpacity: 0.18 + property real lineWidth: 1.5 + + // Per-bar color override: function(value) => color. Null = uniform `color`. + property var colorFunction: null + + // Horizontal threshold lines: [{value: 70, color: "#..."}] + property var thresholds: [] + + onHistoryChanged: if (root.active) + requestPaint() + onColorChanged: if (root.active) + requestPaint() + + onVisibleChanged: if (visible) + requestPaint() + + onPaint: { + const ctx = getContext("2d"); + if (!ctx) + return; + ctx.clearRect(0, 0, width, height); + const d = history; + if (!d.length || (areaMode && d.length < 2)) + return; + + const samples = maxSamples > 0 ? maxSamples : d.length; + const step = width / samples; + const offset = samples - d.length; + + // Background tint + if (backgroundTint > 0) { + ctx.fillStyle = Qt.rgba(color.r, color.g, color.b, backgroundTint).toString(); + ctx.fillRect(0, 0, width, height); + } + + // Threshold lines + if (thresholds.length) { + ctx.globalAlpha = 0.3; + ctx.lineWidth = 1; + ctx.setLineDash([3, 3]); + for (let t = 0; t < thresholds.length; t++) { + const th = thresholds[t]; + const ty = height - height * (th.value / 100); + ctx.strokeStyle = th.color.toString(); + ctx.beginPath(); + ctx.moveTo(0, ty); + ctx.lineTo(width, ty); + ctx.stroke(); + } + ctx.setLineDash([]); + ctx.globalAlpha = 1.0; + } + + if (areaMode) { + // Filled area under curve + const xOff = offset * step; + ctx.beginPath(); + ctx.moveTo(xOff, height); + for (let i = 0; i < d.length; i++) { + ctx.lineTo(xOff + i * step, height - height * (d[i] / 100)); + } + ctx.lineTo(xOff + (d.length - 1) * step, height); + ctx.closePath(); + ctx.fillStyle = Qt.rgba(color.r, color.g, color.b, areaOpacity).toString(); + ctx.fill(); + + // Top stroke + ctx.beginPath(); + for (let i = 0; i < d.length; i++) { + const x = xOff + i * step; + const y = height - height * (d[i] / 100); + if (i === 0) + ctx.moveTo(x, y); + else + ctx.lineTo(x, y); + } + ctx.strokeStyle = color.toString(); + ctx.lineWidth = root.lineWidth; + ctx.stroke(); + } else { + // Bar chart + const hasCF = typeof colorFunction === "function"; + if (!hasCF) + ctx.fillStyle = color.toString(); + for (let i = 0; i < d.length; i++) { + const barH = Math.max(1, height * d[i] / 100); + if (hasCF) + ctx.fillStyle = colorFunction(d[i]).toString(); + ctx.fillRect((offset + i) * step, height - barH, Math.max(1, step - 0.5), barH); + } + } + } +} diff --git a/shell/applets/TemperatureApplet.qml b/shell/applets/TemperatureApplet.qml index 5216580..98ad539 100644 --- a/shell/applets/TemperatureApplet.qml +++ b/shell/applets/TemperatureApplet.qml @@ -104,72 +104,28 @@ Column { } // History sparkline - Canvas { - id: _sparkline + SparklineCanvas { anchors.left: parent.left anchors.leftMargin: 12 anchors.right: parent.right anchors.rightMargin: 12 height: 40 - - property var _hist: root.history - property color _col: root.stateColor - - on_HistChanged: if (root.active) - requestPaint() - on_ColChanged: if (root.active) - requestPaint() - - onVisibleChanged: if (visible) - requestPaint() - - onPaint: { - const ctx = getContext("2d"); - if (!ctx) - return; - ctx.clearRect(0, 0, width, height); - const d = _hist; - if (!d.length) - return; - - const maxSamples = 150; - const bw = width / maxSamples; - - // Background tint - ctx.fillStyle = Qt.rgba(_col.r, _col.g, _col.b, 0.08).toString(); - ctx.fillRect(0, 0, width, height); - - // Warm threshold line - const warmY = height - height * (root.warm / 100); - ctx.strokeStyle = S.Theme.base0A.toString(); - ctx.globalAlpha = 0.3; - ctx.lineWidth = 1; - ctx.setLineDash([3, 3]); - ctx.beginPath(); - ctx.moveTo(0, warmY); - ctx.lineTo(width, warmY); - ctx.stroke(); - - // Hot threshold line - const hotY = height - height * (root.hot / 100); - ctx.strokeStyle = S.Theme.base08.toString(); - ctx.beginPath(); - ctx.moveTo(0, hotY); - ctx.lineTo(width, hotY); - ctx.stroke(); - - ctx.setLineDash([]); - ctx.globalAlpha = 1.0; - - // Bars - const offset = maxSamples - d.length; - for (let i = 0; i < d.length; i++) { - const barH = Math.max(1, height * d[i] / 100); - const barColor = d[i] > root.hot ? S.Theme.base08 : d[i] > root.warm ? S.Theme.base0A : _col; - ctx.fillStyle = barColor.toString(); - ctx.fillRect((offset + i) * bw, height - barH, Math.max(1, bw - 0.5), barH); + history: root.history + color: root.stateColor + active: root.active + maxSamples: 150 + backgroundTint: 0.08 + thresholds: [ + { + value: root.warm, + color: S.Theme.base0A + }, + { + value: root.hot, + color: S.Theme.base08 } - } + ] + colorFunction: v => v > root.hot ? S.Theme.base08 : v > root.warm ? S.Theme.base0A : root.stateColor } // Threshold labels diff --git a/shell/applets/qmldir b/shell/applets/qmldir index caee41f..b6e1e2d 100644 --- a/shell/applets/qmldir +++ b/shell/applets/qmldir @@ -12,8 +12,9 @@ InfoRow 1.0 InfoRow.qml MemoryApplet 1.0 MemoryApplet.qml MprisApplet 1.0 MprisApplet.qml NetworkApplet 1.0 NetworkApplet.qml -Separator 1.0 Separator.qml NotifApplet 1.0 NotifApplet.qml +Separator 1.0 Separator.qml +SparklineCanvas 1.0 SparklineCanvas.qml TemperatureApplet 1.0 TemperatureApplet.qml VolumeApplet 1.0 VolumeApplet.qml WeatherApplet 1.0 WeatherApplet.qml diff --git a/test/qmllint-baseline.txt b/test/qmllint-baseline.txt index 7014c15..05382fc 100644 --- a/test/qmllint-baseline.txt +++ b/test/qmllint-baseline.txt @@ -14,6 +14,7 @@ shell/applets/NetworkApplet.qml: Unqualified access [unqualified] shell/applets/NotifApplet.qml: Member "_notif" not found on type "QQuickItem" [missing-property] shell/applets/NotifApplet.qml: Member "_type" not found on type "QQuickItem" [missing-property] shell/applets/NotifApplet.qml: Unqualified access [unqualified] +shell/applets/SparklineCanvas.qml: Property "colorFunction" is a var property. It may or may not be a method. Use a regular function instead. [use-proper-function] shell/applets/TemperatureApplet.qml: Unqualified access [unqualified] shell/applets/VolumeApplet.qml: Unqualified access [unqualified] shell/lock/Lock.qml: Unqualified access [unqualified]