This commit is contained in:
Lysec
2026-03-06 23:57:17 +01:00
9 changed files with 249 additions and 243 deletions
@@ -4,6 +4,7 @@
"os",
"kernel",
"title",
"board",
"host",
"uptime",
"cpu",
+1
View File
@@ -736,6 +736,7 @@
"system-os": "Betriebssystem:",
"system-packages": "Pakete:",
"system-product": "Produkt:",
"system-board": "Board:",
"system-title": "Systeminformationen",
"system-uptime": "Betriebszeit:",
"system-wm": "WM:",
+1
View File
@@ -737,6 +737,7 @@
"system-os": "OS:",
"system-packages": "Packages:",
"system-product": "Product:",
"system-board": "Board:",
"system-title": "System Information",
"system-uptime": "Uptime:",
"system-wm": "WM:",
@@ -198,7 +198,9 @@ DraggableDesktopWidget {
color2: root.color2
fill: true
updateInterval: root.graphUpdateInterval
strokeWidth: 1.5 * Style.uiScaleRatio * root.widgetScale
animateScale: root.statType === "Network"
antialiasing: 0.5 * root.widgetScale
}
}
@@ -150,6 +150,7 @@ ColumnLayout {
const kernel = root.getModule("Kernel");
const title = root.getModule("Title");
const product = root.getModule("Host");
const board = root.getModule("Board");
const cpu = root.getModule("CPU");
const gpu = root.getModule("GPU");
const mem = root.getModule("Memory");
@@ -158,6 +159,7 @@ ColumnLayout {
info += "Kernel: " + (kernel?.result?.release || "N/A") + "\n";
info += "Host: " + (title?.result?.hostName || "N/A") + "\n";
info += "Product: " + (product?.result?.name || "N/A") + "\n";
info += "Board: " + (board?.result?.name || "N/A") + "\n";
info += "CPU: " + (cpu?.result?.cpu || "N/A") + "\n";
if (gpu?.result && Array.isArray(gpu.result) && gpu.result.length > 0) {
info += "GPU: " + gpu.result.map(g => g.name || "Unknown").join(", ") + "\n";
@@ -720,6 +722,23 @@ ColumnLayout {
wrapMode: Text.Wrap
}
// Board name
NText {
text: I18n.tr("panels.about.system-board")
color: Color.mOnSurfaceVariant
pointSize: sysInfo.textSize
}
NText {
text: {
const title = root.getModule("Board");
return title?.result?.name || "N/A";
}
color: Color.mOnSurface
pointSize: sysInfo.textSize
Layout.fillWidth: true
wrapMode: Text.Wrap
}
// Uptime
NText {
text: I18n.tr("panels.about.system-uptime")
@@ -136,6 +136,7 @@ SmartPanel {
maxValue2: Math.max(SystemStatService.cpuTempHistoryMax + 5, 1)
color: Color.mPrimary
color2: Color.mSecondary
strokeWidth: 1.5 * Style.uiScaleRatio
fill: true
fillOpacity: 0.15
updateInterval: SystemStatService.cpuUsageIntervalMs
@@ -189,6 +190,7 @@ SmartPanel {
minValue: 0
maxValue: 100
color: Color.mPrimary
strokeWidth: 1.5 * Style.uiScaleRatio
fill: true
fillOpacity: 0.15
updateInterval: SystemStatService.memIntervalMs
@@ -260,6 +262,7 @@ SmartPanel {
maxValue2: SystemStatService.txMaxSpeed
color: Color.mPrimary
color2: Color.mSecondary
strokeWidth: 1.5 * Style.uiScaleRatio
fill: true
fillOpacity: 0.15
updateInterval: SystemStatService.networkIntervalMs
+114
View File
@@ -0,0 +1,114 @@
#version 450
layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor;
layout(binding = 1) uniform sampler2D dataSource;
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float qt_Opacity;
vec4 lineColor1;
vec4 lineColor2;
float count1;
float count2;
float scroll1;
float scroll2;
float lineWidth;
float graphFillOpacity;
float texWidth;
float resY;
float aaSize;
};
// Sample normalized value from data texture
// channel 0 = primary (R), channel 1 = secondary (G)
float fetchData(float idx, int ch) {
float i = clamp(idx, 0.0, texWidth - 1.0);
float u = (floor(i) + 0.5) / texWidth;
vec4 t = texture(dataSource, vec2(u, 0.5));
return ch == 0 ? t.r : t.g;
}
// Cubic Hermite interpolation with reduced tangent scale for smooth curves
float cubicHermite(float y0, float y1, float y2, float y3, float t) {
float m1 = (y2 - y0) * 0.25;
float m2 = (y3 - y1) * 0.25;
float t2 = t * t;
float t3 = t2 * t;
return (2.0 * t3 - 3.0 * t2 + 1.0) * y1
+ (t3 - 2.0 * t2 + t) * m1
+ (-2.0 * t3 + 3.0 * t2) * y2
+ (t3 - t2) * m2;
}
// Evaluate curve at fractional data index
float evalCurve(float dataIdx, int ch) {
float i = floor(dataIdx);
float t = dataIdx - i;
return cubicHermite(
fetchData(i - 1.0, ch),
fetchData(i, ch),
fetchData(i + 1.0, ch),
fetchData(i + 2.0, ch),
t
);
}
// Premultiplied alpha over compositing
vec4 blendOver(vec4 src, vec4 dst) {
return src + dst * (1.0 - src.a);
}
void main() {
vec2 uv = qt_TexCoord0;
float normY = 1.0 - uv.y; // 0 = bottom, 1 = top
vec4 result = vec4(0.0);
float halfW = lineWidth * 0.5;
// Primary line
if (count1 >= 4.0) {
float segs = count1 - 3.0;
float di = 2.0 + scroll1 + uv.x * segs;
float pixStep1 = dFdx(di);
float cy = evalCurve(di, 0);
float cyNext = evalCurve(di + pixStep1, 0);
// Fill below curve (gradient: opaque at top, transparent at bottom)
if (graphFillOpacity > 0.0 && normY <= cy) {
float a = graphFillOpacity * normY * lineColor1.a;
result = blendOver(vec4(lineColor1.rgb * a, a), result);
}
// Perpendicular distance to the line segment between adjacent samples
float slope1 = (cyNext - cy) * resY;
float vDist1 = (normY - cy) * resY;
float dist1 = abs(vDist1) * inversesqrt(slope1 * slope1 + 1.0);
float sa = smoothstep(halfW + aaSize, halfW, dist1) * lineColor1.a;
result = blendOver(vec4(lineColor1.rgb * sa, sa), result);
}
// Secondary line
if (count2 >= 4.0) {
float segs = count2 - 3.0;
float di = 2.0 + scroll2 + uv.x * segs;
float pixStep2 = dFdx(di);
float cy = evalCurve(di, 1);
float cyNext = evalCurve(di + pixStep2, 1);
if (graphFillOpacity > 0.0 && normY <= cy) {
float a = graphFillOpacity * normY * lineColor2.a;
result = blendOver(vec4(lineColor2.rgb * a, a), result);
}
float slope2 = (cyNext - cy) * resY;
float vDist2 = (normY - cy) * resY;
float dist2 = abs(vDist2) * inversesqrt(slope2 * slope2 + 1.0);
float sa = smoothstep(halfW + aaSize, halfW, dist2) * lineColor2.a;
result = blendOver(vec4(lineColor2.rgb * sa, sa), result);
}
fragColor = result * qt_Opacity;
}
Binary file not shown.
+108 -243
View File
@@ -1,5 +1,5 @@
import QtQuick
import QtQuick.Shapes
import Quickshell
import qs.Commons
Item {
@@ -23,9 +23,10 @@ Item {
property real maxValue2: maxValue
// Style settings
property real strokeWidth: 2 * Style.uiScaleRatio
property real strokeWidth: 1.5
property bool fill: true
property real fillOpacity: 0.15
property real antialiasing: 0.5
// Smooth scrolling interval (how often data updates)
property int updateInterval: 1000
@@ -39,18 +40,16 @@ Item {
readonly property bool hasData: values.length >= 4
readonly property bool hasData2: values2.length >= 4
// Target max values (what we're animating toward)
// Scale animation state
property real _targetMax1: maxValue
property real _targetMax2: maxValue2
// Current animated max values (interpolated in timer when animateScale is true)
property real _animMax1: maxValue
property real _animMax2: maxValue2
onMaxValueChanged: {
_targetMax1 = maxValue;
if (animateScale && _ready1) {
_animTimer.start();
_scaleTimer.start();
} else {
_animMax1 = maxValue;
}
@@ -59,7 +58,7 @@ Item {
onMaxValue2Changed: {
_targetMax2 = maxValue2;
if (animateScale && _ready2) {
_animTimer.start();
_scaleTimer.start();
} else {
_animMax2 = maxValue2;
}
@@ -69,31 +68,45 @@ Item {
readonly property real _effectiveMax1: animateScale ? _animMax1 : maxValue
readonly property real _effectiveMax2: animateScale ? _animMax2 : maxValue2
// Animation state for primary line
// Scroll state (driven by NumberAnimation)
property real _t1: 1.0
property bool _ready1: false
property real _pred1: 0
// Animation state for secondary line
property real _t2: 1.0
property bool _ready2: false
property real _pred2: 0
// Frame-accurate scroll animations tied to Qt's render loop
NumberAnimation {
id: _scrollAnim1
target: root
property: "_t1"
from: 0
to: 1
duration: root.updateInterval
}
NumberAnimation {
id: _scrollAnim2
target: root
property: "_t2"
from: 0
to: 1
duration: root.updateInterval
}
onValuesChanged: {
if (values.length < 4)
return;
const last = values[values.length - 1];
const prev = values[values.length - 2];
_pred1 = Math.max(minValue, last + (last - prev));
_pred1 = Math.max(minValue, last + (last - prev) * 0.5);
if (!_ready1) {
if (!_ready1)
_ready1 = true;
_t1 = 0;
} else {
_t1 = Math.max(0, _t1 - 1.0);
}
_animTimer.start();
_scrollAnim1.restart();
}
onValues2Changed: {
@@ -102,268 +115,120 @@ Item {
const last = values2[values2.length - 1];
const prev = values2[values2.length - 2];
_pred2 = Math.max(minValue2, last + (last - prev));
_pred2 = Math.max(minValue2, last + (last - prev) * 0.5);
if (!_ready2) {
if (!_ready2)
_ready2 = true;
_t2 = 0;
} else {
_t2 = Math.max(0, _t2 - 1.0);
}
_animTimer.start();
_scrollAnim2.restart();
}
// Scale animation timer (only needed for animateScale mode)
Timer {
id: _animTimer
id: _scaleTimer
interval: 16
repeat: true
property real _prevTime: 0
onTriggered: {
const now = Date.now();
const elapsed = _prevTime > 0 ? (now - _prevTime) : 16;
_prevTime = now;
const dt = elapsed / root.updateInterval;
const scaleLerp = 0.15;
const threshold = 0.5;
let stillAnimating = false;
// Scroll animation
if (root._t1 < 1.0) {
root._t1 = Math.min(1.0, root._t1 + dt);
if (Math.abs(root._animMax1 - root._targetMax1) > threshold) {
root._animMax1 += (root._targetMax1 - root._animMax1) * scaleLerp;
stillAnimating = true;
} else if (root._animMax1 !== root._targetMax1) {
root._animMax1 = root._targetMax1;
}
if (root._t2 < 1.0) {
root._t2 = Math.min(1.0, root._t2 + dt);
if (Math.abs(root._animMax2 - root._targetMax2) > threshold) {
root._animMax2 += (root._targetMax2 - root._animMax2) * scaleLerp;
stillAnimating = true;
} else if (root._animMax2 !== root._targetMax2) {
root._animMax2 = root._targetMax2;
}
// Scale animation (lerp toward target) - synchronized with scroll
if (root.animateScale) {
const scaleLerp = 0.15; // Smooth lerp factor per frame
const threshold = 0.5; // Snap when close enough
if (Math.abs(root._animMax1 - root._targetMax1) > threshold) {
root._animMax1 += (root._targetMax1 - root._animMax1) * scaleLerp;
stillAnimating = true;
} else if (root._animMax1 !== root._targetMax1) {
root._animMax1 = root._targetMax1;
}
if (Math.abs(root._animMax2 - root._targetMax2) > threshold) {
root._animMax2 += (root._targetMax2 - root._animMax2) * scaleLerp;
stillAnimating = true;
} else if (root._animMax2 !== root._targetMax2) {
root._animMax2 = root._targetMax2;
}
}
if (!stillAnimating) {
_prevTime = 0;
if (!stillAnimating)
stop();
}
}
}
// Convert a value to Y coordinate (with padding to keep values from touching edges)
// Clamps normalized range to [-10, 10] so coordinates stay within ~10× widget bounds,
// preventing extreme values that crash Qt's CurveRenderer triangulator.
function valueToY(val, minVal, maxVal) {
// Normalize a value to [0, 1] with padding applied
function _normalize(val, minVal, maxVal) {
let range = maxVal - minVal;
if (range <= 0)
return height / 2;
return 0.5;
let padding = range * curvePadding;
let paddedMin = minVal - padding;
let paddedMax = maxVal + padding;
let paddedRange = paddedMax - paddedMin;
let normalized = Math.max(-10, Math.min(10, (val - paddedMin) / paddedRange));
return height - normalized * height;
let paddedRange = (maxVal + padding) - paddedMin;
return Math.max(0, Math.min(1, (val - paddedMin) / paddedRange));
}
// Safe degenerate-path fallback: a valid off-screen line that renders nothing visible.
// "M 0 0" (bare moveto) crashes Qt's CurveRenderer — never use it.
readonly property string _safeFallbackPath: "M -1 -1 L -1 0"
// Data texture built from Rectangles instead of Canvas.
// Each Rectangle is one data point, color-coded with normalized values.
// R channel = primary, G channel = secondary.
Item {
id: _dataRow
width: Math.max(root.values.length + 1, root.values2.length + 1, 4)
height: 1
// Build raw data points for both stroke and fill paths
function _buildRawPoints(vals, pred, minVal, maxVal, t) {
const n = vals.length;
const step = width / (n - 3);
if (!isFinite(step) || step <= 0)
return null;
Repeater {
model: _dataRow.width
let raw = [];
raw.push({
x: (-2 - t) * step,
y: valueToY(vals[0], minVal, maxVal)
});
raw.push({
x: (-1 - t) * step,
y: valueToY(vals[1], minVal, maxVal)
});
for (let i = 2; i < n; i++) {
raw.push({
x: (i - 2 - t) * step,
y: valueToY(vals[i], minVal, maxVal)
});
Rectangle {
required property int index
x: index
width: 1
height: 1
color: {
let r = 0, g = 0;
let n1 = root.values.length;
let n2 = root.values2.length;
let eMax1 = root._effectiveMax1;
let eMax2 = root._effectiveMax2;
if (index < n1)
r = root._normalize(root.values[index], root.minValue, eMax1);
else if (n1 > 0)
r = root._normalize(root._pred1, root.minValue, eMax1);
if (index < n2)
g = root._normalize(root.values2[index], root.minValue2, eMax2);
else if (n2 > 0)
g = root._normalize(root._pred2, root.minValue2, eMax2);
return Qt.rgba(r, g, 0, 1);
}
}
}
raw.push({
x: (n - 2 - t) * step,
y: valueToY(pred, minVal, maxVal)
});
// Validate all points — NaN/Infinity in any coordinate crashes CurveRenderer
for (let i = 0; i < raw.length; i++) {
if (!isFinite(raw[i].x) || !isFinite(raw[i].y))
return null;
}
return raw;
}
// Build SVG stroke path with cubic bezier curves (Catmull-Rom → Bezier)
function buildStrokeSvg(vals, pred, minVal, maxVal, t) {
if (!vals || vals.length < 4 || width <= 0 || height <= 0)
return _safeFallbackPath;
const raw = _buildRawPoints(vals, pred, minVal, maxVal, t);
if (!raw)
return _safeFallbackPath;
let svg = `M ${raw[0].x} ${raw[0].y}`;
// Catmull-Rom to cubic bezier: cp1 = p1 + (p2-p0)/6, cp2 = p2 - (p3-p1)/6
for (let i = 0; i < raw.length - 1; i++) {
const p0 = raw[Math.max(i - 1, 0)];
const p1 = raw[i];
const p2 = raw[i + 1];
const p3 = raw[Math.min(i + 2, raw.length - 1)];
const cp1x = p1.x + (p2.x - p0.x) / 6;
const cp1y = p1.y + (p2.y - p0.y) / 6;
const cp2x = p2.x - (p3.x - p1.x) / 6;
const cp2y = p2.y - (p3.y - p1.y) / 6;
if (!isFinite(cp1x) || !isFinite(cp1y) || !isFinite(cp2x) || !isFinite(cp2y))
return _safeFallbackPath;
svg += ` C ${cp1x} ${cp1y} ${cp2x} ${cp2y} ${p2.x} ${p2.y}`;
}
return svg;
ShaderEffectSource {
id: _dataTex
sourceItem: _dataRow
textureSize: Qt.size(_dataRow.width, 1)
live: true
smooth: false
hideSource: true
}
// Build SVG fill path with LINEAR segments only.
// CurveRenderer's processFill falls back to qTriangulate for complex curves,
// and Qt's ComplexToSimple::removeUnwantedEdgesAndConnect crashes on
// self-intersecting cubic bezier fill polygons. Linear segments produce a
// simple polygon that the triangulator handles safely. The smooth cubic
// stroke overlays this fill, so the linear edges are invisible.
function buildFillSvg(vals, pred, minVal, maxVal, t) {
if (!vals || vals.length < 4 || width <= 0 || height <= 0)
return _safeFallbackPath;
const raw = _buildRawPoints(vals, pred, minVal, maxVal, t);
if (!raw)
return _safeFallbackPath;
let svg = `M ${raw[0].x} ${raw[0].y}`;
for (let i = 1; i < raw.length; i++) {
svg += ` L ${raw[i].x} ${raw[i].y}`;
}
const last = raw[raw.length - 1];
svg += ` L ${last.x} ${height} L ${raw[0].x} ${height} Z`;
return svg;
}
// Reactive SVG paths — re-evaluated when any dependency changes
// Stroke uses smooth cubic bezier curves; fill uses linear segments to avoid
// crashing Qt's CurveRenderer triangulator (QTBUG: qTriangulate SEGV).
readonly property string _strokeSvg1: buildStrokeSvg(values, _pred1, minValue, _effectiveMax1, _t1)
readonly property string _fillSvg1: fill ? buildFillSvg(values, _pred1, minValue, _effectiveMax1, _t1) : _safeFallbackPath
readonly property string _strokeSvg2: buildStrokeSvg(values2, _pred2, minValue2, _effectiveMax2, _t2)
readonly property string _fillSvg2: fill ? buildFillSvg(values2, _pred2, minValue2, _effectiveMax2, _t2) : _safeFallbackPath
// Primary line — only rendered when there is enough data to form a valid path.
// Keeping primary and secondary in separate Shapes allows independent visibility
// gating, so neither ever receives a degenerate path from the other's dataset.
Shape {
ShaderEffect {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
visible: root.hasData && width > 0 && height > 0
visible: (root.hasData || root.hasData2) && width > 0 && height > 0
ShapePath {
fillGradient: LinearGradient {
y1: 0
y2: root.height
property variant dataSource: _dataTex
property color lineColor1: root.color
property color lineColor2: root.color2
property real count1: root.values.length
property real count2: root.values2.length
property real scroll1: root._t1
property real scroll2: root._t2
property real lineWidth: root.strokeWidth
property real graphFillOpacity: root.fill ? root.fillOpacity : 0.0
property real texWidth: _dataRow.width
property real resY: height
property real aaSize: root.antialiasing
GradientStop {
position: 0
color: Qt.rgba(root.color.r, root.color.g, root.color.b, root.fillOpacity)
}
GradientStop {
position: 1
color: "transparent"
}
}
strokeWidth: -1
strokeColor: "transparent"
PathSvg {
path: root._fillSvg1
}
}
ShapePath {
strokeColor: root.color
strokeWidth: root.strokeWidth
fillColor: "transparent"
capStyle: ShapePath.RoundCap
joinStyle: ShapePath.RoundJoin
PathSvg {
path: root._strokeSvg1
}
}
}
// Secondary line — only rendered when there is enough secondary data.
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
visible: root.hasData2 && width > 0 && height > 0
ShapePath {
fillGradient: LinearGradient {
y1: 0
y2: root.height
GradientStop {
position: 0
color: Qt.rgba(root.color2.r, root.color2.g, root.color2.b, root.fillOpacity)
}
GradientStop {
position: 1
color: "transparent"
}
}
strokeWidth: -1
strokeColor: "transparent"
PathSvg {
path: root._fillSvg2
}
}
ShapePath {
strokeColor: root.color2
strokeWidth: root.strokeWidth
fillColor: "transparent"
capStyle: ShapePath.RoundCap
joinStyle: ShapePath.RoundJoin
PathSvg {
path: root._strokeSvg2
}
}
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/graph.frag.qsb")
blending: true
}
}