mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
"os",
|
||||
"kernel",
|
||||
"title",
|
||||
"board",
|
||||
"host",
|
||||
"uptime",
|
||||
"cpu",
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user