mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
235 lines
6.1 KiB
QML
235 lines
6.1 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import qs.Commons
|
|
|
|
Item {
|
|
id: root
|
|
clip: true
|
|
|
|
// Primary line
|
|
property var values: []
|
|
property color color: Color.mPrimary
|
|
|
|
// Optional secondary line
|
|
property var values2: []
|
|
property color color2: Color.mError
|
|
|
|
// Range settings for primary line
|
|
property real minValue: 0
|
|
property real maxValue: 100
|
|
|
|
// Range settings for secondary line (defaults to primary range)
|
|
property real minValue2: minValue
|
|
property real maxValue2: maxValue
|
|
|
|
// Style settings
|
|
property real strokeWidth: 1
|
|
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
|
|
|
|
// Animate scale changes (for network graphs with dynamic max)
|
|
property bool animateScale: false
|
|
|
|
// Vertical padding (percentage of range) to keep values from touching edges
|
|
readonly property real curvePadding: 0.12
|
|
|
|
readonly property bool hasData: values.length >= 4
|
|
readonly property bool hasData2: values2.length >= 4
|
|
|
|
// Scale animation state
|
|
property real _targetMax1: maxValue
|
|
property real _targetMax2: maxValue2
|
|
property real _animMax1: maxValue
|
|
property real _animMax2: maxValue2
|
|
|
|
onMaxValueChanged: {
|
|
_targetMax1 = maxValue;
|
|
if (animateScale && _ready1) {
|
|
_scaleTimer.start();
|
|
} else {
|
|
_animMax1 = maxValue;
|
|
}
|
|
}
|
|
|
|
onMaxValue2Changed: {
|
|
_targetMax2 = maxValue2;
|
|
if (animateScale && _ready2) {
|
|
_scaleTimer.start();
|
|
} else {
|
|
_animMax2 = maxValue2;
|
|
}
|
|
}
|
|
|
|
// Effective max values (animated or direct)
|
|
readonly property real _effectiveMax1: animateScale ? _animMax1 : maxValue
|
|
readonly property real _effectiveMax2: animateScale ? _animMax2 : maxValue2
|
|
|
|
// Scroll state (driven by NumberAnimation)
|
|
property real _t1: 1.0
|
|
property bool _ready1: false
|
|
property real _pred1: 0
|
|
|
|
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) * 0.5);
|
|
|
|
if (!_ready1)
|
|
_ready1 = true;
|
|
_scrollAnim1.restart();
|
|
}
|
|
|
|
onValues2Changed: {
|
|
if (values2.length < 4)
|
|
return;
|
|
|
|
const last = values2[values2.length - 1];
|
|
const prev = values2[values2.length - 2];
|
|
_pred2 = Math.max(minValue2, last + (last - prev) * 0.5);
|
|
|
|
if (!_ready2)
|
|
_ready2 = true;
|
|
_scrollAnim2.restart();
|
|
}
|
|
|
|
// Scale animation timer (only needed for animateScale mode)
|
|
Timer {
|
|
id: _scaleTimer
|
|
interval: 16
|
|
repeat: true
|
|
|
|
onTriggered: {
|
|
const scaleLerp = 0.15;
|
|
const threshold = 0.5;
|
|
let stillAnimating = false;
|
|
|
|
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)
|
|
stop();
|
|
}
|
|
}
|
|
|
|
// Normalize a value to [0, 1] with padding applied
|
|
function _normalize(val, minVal, maxVal) {
|
|
let range = maxVal - minVal;
|
|
if (range <= 0)
|
|
return 0.5;
|
|
let padding = range * curvePadding;
|
|
let paddedMin = minVal - padding;
|
|
let paddedRange = (maxVal + padding) - paddedMin;
|
|
return Math.max(0, Math.min(1, (val - paddedMin) / paddedRange));
|
|
}
|
|
|
|
// 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
|
|
|
|
Repeater {
|
|
model: _dataRow.width
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ShaderEffectSource {
|
|
id: _dataTex
|
|
sourceItem: _dataRow
|
|
textureSize: Qt.size(_dataRow.width, 1)
|
|
live: true
|
|
smooth: false
|
|
hideSource: true
|
|
}
|
|
|
|
ShaderEffect {
|
|
anchors.fill: parent
|
|
visible: (root.hasData || root.hasData2) && width > 0 && height > 0
|
|
|
|
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
|
|
|
|
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/graph.frag.qsb")
|
|
blending: true
|
|
}
|
|
}
|