From 13372e8055d7132690127ad3e9127b665641280c Mon Sep 17 00:00:00 2001 From: Damocles Date: Fri, 17 Apr 2026 09:22:15 +0200 Subject: [PATCH] feat(temperature): add hover panel with history sparkline and gauge --- modules/SystemStats.qml | 3 + modules/Temperature.qml | 244 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 244 insertions(+), 3 deletions(-) diff --git a/modules/SystemStats.qml b/modules/SystemStats.qml index 3582a79..9ee7adf 100644 --- a/modules/SystemStats.qml +++ b/modules/SystemStats.qml @@ -32,6 +32,7 @@ QtObject { // ── Temperature ────────────────────────────────────────────────────── property int tempCelsius: 0 + property var tempHistory: [] // 150 samples @ 4s each ≈ 10 min // ── Memory ─────────────────────────────────────────────────────────── property int memPercent: 0 @@ -83,6 +84,8 @@ QtObject { } } else if (ev.type === "temp") { root.tempCelsius = ev.celsius; + const th = root.tempHistory.concat([ev.celsius]); + root.tempHistory = th.length > 150 ? th.slice(th.length - 150) : th; } else if (ev.type === "mem") { root.memPercent = ev.percent; root.memUsedGb = ev.used_gb; diff --git a/modules/Temperature.qml b/modules/Temperature.qml index d6f9ade..0106632 100644 --- a/modules/Temperature.qml +++ b/modules/Temperature.qml @@ -1,27 +1,265 @@ import QtQuick +import Quickshell import "." as M M.BarSection { id: root spacing: Math.max(1, M.Theme.moduleSpacing - 2) - tooltip: "Temperature: " + M.SystemStats.tempCelsius + "\u00B0C" + tooltip: "" - property color _stateColor: M.SystemStats.tempCelsius > (M.Modules.temperature.hot || 80) ? M.Theme.base09 : M.SystemStats.tempCelsius > (M.Modules.temperature.warm || 60) ? M.Theme.base0A : root.accentColor + readonly property int _warm: M.Modules.temperature.warm || 80 + readonly property int _hot: M.Modules.temperature.hot || 90 + readonly property int _temp: M.SystemStats.tempCelsius + + property color _stateColor: _temp > _hot ? M.Theme.base08 : _temp > _warm ? M.Theme.base0A : root.accentColor Behavior on _stateColor { ColorAnimation { duration: 300 } } + property bool _pinned: false + readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered + readonly property bool _showPanel: _anyHover || _pinned + + on_AnyHoverChanged: { + if (_anyHover) + _unpinTimer.stop(); + else if (_pinned) + _unpinTimer.start(); + } + + Timer { + id: _unpinTimer + interval: 500 + onTriggered: root._pinned = false + } + + // Returns a color interpolated green→yellow→red for a given celsius value + function _tempColor(celsius) { + const t = Math.max(0, Math.min(100, celsius)) / 100; + const a = t < 0.5 ? M.Theme.base0B : M.Theme.base0A; + const b = t < 0.5 ? M.Theme.base0A : M.Theme.base08; + const u = t < 0.5 ? t * 2 : (t - 0.5) * 2; + return Qt.rgba(a.r + (b.r - a.r) * u, a.g + (b.g - a.g) * u, a.b + (b.b - a.b) * u, 1); + } + M.BarIcon { icon: "\uF2C9" color: root._stateColor anchors.verticalCenter: parent.verticalCenter + TapHandler { + cursorShape: Qt.PointingHandCursor + onTapped: root._pinned = !root._pinned + } } M.BarLabel { - label: M.SystemStats.tempCelsius + "\u00B0C" + label: root._temp + "\u00B0C" minText: "100\u00B0C" color: root._stateColor anchors.verticalCenter: parent.verticalCenter + TapHandler { + cursorShape: Qt.PointingHandCursor + onTapped: root._pinned = !root._pinned + } + } + + M.HoverPanel { + id: hoverPanel + showPanel: root._showPanel + screen: QsWindow.window?.screen ?? null + anchorItem: root + accentColor: root.accentColor + panelNamespace: "nova-temperature" + panelTitle: "Temperature" + contentWidth: 220 + + // Header — current temp + Item { + width: parent.width + height: 28 + + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: root._temp + "\u00B0C" + color: root._stateColor + font.pixelSize: M.Theme.fontSize + font.family: M.Theme.fontFamily + font.bold: true + width: _tempSizer.implicitWidth + horizontalAlignment: Text.AlignRight + + Text { + id: _tempSizer + visible: false + text: "100\u00B0C" + font: parent.font + } + } + } + + // Gauge bar (0–100°C), with warm/hot threshold markers + Item { + width: parent.width + height: 16 + + Item { + id: _gaugeBar + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + height: 6 + + Rectangle { + anchors.fill: parent + color: M.Theme.base02 + radius: 3 + } + + Rectangle { + width: parent.width * Math.min(1, root._temp / 100) + height: parent.height + color: root._stateColor + radius: 3 + Behavior on width { + enabled: root._showPanel + NumberAnimation { + duration: 300 + easing.type: Easing.OutCubic + } + } + } + + // Warm threshold marker + Rectangle { + x: parent.width * (root._warm / 100) - 1 + width: 1 + height: parent.height + 4 + anchors.verticalCenter: parent.verticalCenter + color: M.Theme.base0A + opacity: 0.6 + } + + // Hot threshold marker + Rectangle { + x: parent.width * (root._hot / 100) - 1 + width: 1 + height: parent.height + 4 + anchors.verticalCenter: parent.verticalCenter + color: M.Theme.base08 + opacity: 0.6 + } + } + } + + // History sparkline (~10 min @ 4s per sample) + Canvas { + id: _sparkline + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.right: parent.right + anchors.rightMargin: 12 + height: 40 + + property var _hist: M.SystemStats.tempHistory + property color _col: root._stateColor + + on_HistChanged: if (root._showPanel) + requestPaint() + on_ColChanged: if (root._showPanel) + requestPaint() + + Connections { + target: root + function on_ShowPanelChanged() { + if (root._showPanel) + _sparkline.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 = M.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 = M.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 ? M.Theme.base08 : d[i] > root._warm ? M.Theme.base0A : _col; + ctx.fillStyle = barColor.toString(); + ctx.fillRect((offset + i) * bw, height - barH, Math.max(1, bw - 0.5), barH); + } + } + } + + // Threshold labels + Item { + width: parent.width + height: 16 + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: "warm " + root._warm + "\u00B0 hot " + root._hot + "\u00B0" + color: M.Theme.base03 + font.pixelSize: M.Theme.fontSize - 3 + font.family: M.Theme.fontFamily + font.letterSpacing: 0.5 + } + + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: "10 min" + color: M.Theme.base03 + font.pixelSize: M.Theme.fontSize - 3 + font.family: M.Theme.fontFamily + } + } + + Item { + width: 1 + height: 4 + } } }