pragma ComponentBehavior: Bound import QtQuick import QtQuick.Shapes import ZShell import ZShell.Components import ZShell.Config import qs.Services Item { id: root // Ring geometry, quantized in the item's own device pixel space and never // against its position: the parent layout places the item on the device // grid, so these values cannot change when the item moves (no jitter), and // the ring stays crisp wherever it lands. // Device-pixel center of the ring: an even stroke width wants a pixel // boundary, an odd one a pixel center. Floored, matching // WavyLineRenderer::snapCenterDev, so the wave and this background arc // always land on the same center even when the box's device size is odd. readonly property real centerDev: Math.floor( sizeDev / 2 - strokeParity / 2 ) + strokeParity / 2 readonly property real centerX: centerDev / dpr readonly property real centerY: centerDev / dpr // Whole device pixels, and one pixel of slack on the outer edge: the ring's // antialias feather needs somewhere to fade, otherwise it is cut flat at // the top/left/right/bottom of the item rect. readonly property real arcRadius: Math.max( 1, Math.floor( (sizeDev - Math.round(padding * dpr) - strokeDev * (1 + waveAmplitude * 2) - 2) / 2 ) ) / dpr property color bgColor: Colors.palette.m3secondaryContainer property real clampedVal: Math.max(1 / 360, Math.min(1, isNaN(value) ? 0 : value)) readonly property real dotAngleRad: (startAngle + sweepAngle - gapAngle * (sweepAngle < 360 ? 0 : 1)) * Math.PI / 180 readonly property real dpr: ZUtils.dprFor(root) property color fgColor: Colors.palette.m3primary readonly property real gapAngle: ((spacing + strokeLogical) / (arcRadius || 1)) * (180 / Math.PI) property alias hasEndIndicator: dot.active property real implicitSize property int padding: 0 // The item's own size in whole device pixels. Never adjusted upwards: the // texture is exactly this wide, so ring values derived from it cannot spill // past the item's bounds. readonly property real size: sizeDev / dpr readonly property real sizeDev: Math.round(Math.min(width, height) * dpr) property int spacing: Tokens.spacing.small readonly property real strokeDev: Math.max(1, Math.round(strokeWidth * dpr)) readonly property real strokeLogical: strokeDev / dpr readonly property real strokeParity: strokeDev % 2 property int startAngle: -90 property int strokeWidth: Tokens.padding.extraSmall property int sweepAngle: 360 readonly property real thickness: strokeLogical * (1 + waveAmplitude) * 2 property real value property alias waveAmplitude: wave.amplitudeMultiplier property alias waveDuration: waveProgAnim.duration property alias waveFrequency: wave.frequency property bool wavePaused property bool wavy: false implicitHeight: implicitSize implicitWidth: implicitSize Shape { asynchronous: true opacity: Math.min(1, remainingArc.sweepAngle) preferredRendererType: Shape.CurveRenderer ShapePath { capStyle: ShapePath.RoundCap fillColor: "transparent" strokeColor: root.bgColor strokeWidth: Math.max(1, Math.round(Math.min(1, remainingArc.sweepAngle) * root.strokeDev)) / root.dpr Behavior on strokeColor { CAnim { } } PathAngleArc { id: remainingArc centerX: root.centerX centerY: root.centerY radiusX: root.arcRadius radiusY: root.arcRadius startAngle: root.startAngle + root.clampedVal * root.sweepAngle + root.gapAngle sweepAngle: Math.max(1 / 360, root.sweepAngle * (1 - root.clampedVal) - root.gapAngle * (root.sweepAngle < 360 ? 1 : 2)) } } } WavyLine { id: wave amplitudeMultiplier: root.wavy ? 0.5 : 0 anchors.fill: parent anchors.margins: -Math.round(lineWidth * amplitudeMultiplier * root.dpr) / root.dpr color: root.fgColor frequency: 8 fullAngle: root.sweepAngle lineWidth: root.strokeWidth objectName: "wave" pathType: WavyLine.Arc radius: root.arcRadius startAngle: root.startAngle value: root.clampedVal Behavior on amplitudeMultiplier { Anim { type: Anim.DefaultEffects } } Behavior on color { CAnim { } } Anim on waveProgress { id: waveProgAnim duration: 2000 easing.type: Easing.Linear from: 0 loops: Animation.Infinite paused: root.wavePaused || wave.amplitudeMultiplier === 0 running: true to: 1 } } Loader { id: dot // Rounded in local device pixels: the dot's own size is a whole even // device count, so an integral local position puts its center on the grid // whatever the ring's angle, and moving the gauge moves the dot with it. x: Math.round((root.centerX + root.arcRadius * Math.cos(root.dotAngleRad) - width / 2) * root.dpr) / root.dpr y: Math.round((root.centerY + root.arcRadius * Math.sin(root.dotAngleRad) - height / 2) * root.dpr) / root.dpr sourceComponent: CustomRect { color: root.fgColor implicitHeight: Math.min(1, remainingArc.sweepAngle) * Math.min(4, root.strokeWidth) implicitWidth: Math.min(1, remainingArc.sweepAngle) * Math.min(4, root.strokeWidth) opacity: Math.min(1, remainingArc.sweepAngle) radius: Tokens.rounding.full } } }