nosignal-shell/components/controls/CircularProgress.qml
2026-04-23 22:42:41 +10:00

121 lines
3.7 KiB
QML

pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Shapes
import Caelestia.Components
import Caelestia.Config
import qs.components
import qs.services
Item {
id: root
property real value
property int startAngle: -90
property int sweepAngle: 360
property int strokeWidth: Tokens.padding.small
property int padding: 0
property int spacing: Tokens.spacing.small
property color fgColour: Colours.palette.m3primary
property color bgColour: Colours.palette.m3secondaryContainer
property alias hasEndIndicator: dot.active
property bool wavy: false
property alias waveFrequency: wave.frequency
property alias waveAmplitude: wave.amplitudeMultiplier
property alias wavePaused: waveProgAnim.paused
property alias waveDuration: waveProgAnim.duration
readonly property real size: Math.min(width, height)
readonly property real arcRadius: (size - padding - strokeWidth * (1 + waveAmplitude * 2)) / 2
readonly property real clampedVal: Math.max(1 / 360, Math.min(1, value))
readonly property real gapAngle: ((spacing + strokeWidth) / (arcRadius || 1)) * (180 / Math.PI)
readonly property real dotAngleRad: (startAngle + sweepAngle - gapAngle * (sweepAngle < 360 ? 0 : 1)) * Math.PI / 180
readonly property real thickness: strokeWidth * (1 + waveAmplitude) * 2 // For consumers
property real implicitSize
implicitWidth: implicitSize
implicitHeight: implicitSize
Shape {
preferredRendererType: Shape.CurveRenderer
asynchronous: true
opacity: Math.min(1, remainingArc.sweepAngle)
ShapePath {
fillColor: "transparent"
strokeColor: root.bgColour
strokeWidth: Math.min(1, remainingArc.sweepAngle) * root.strokeWidth
capStyle: ShapePath.RoundCap
PathAngleArc {
id: remainingArc
radiusX: root.arcRadius
radiusY: root.arcRadius
centerX: root.size / 2
centerY: root.size / 2
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))
}
Behavior on strokeColor {
CAnim {}
}
}
}
WavyLine {
id: wave
anchors.fill: parent
anchors.margins: -lineWidth * amplitudeMultiplier
lineWidth: root.strokeWidth
color: root.fgColour
pathType: WavyLine.Arc
radius: root.arcRadius
startAngle: root.startAngle
fullAngle: root.sweepAngle
value: root.clampedVal
frequency: 8
amplitudeMultiplier: root.wavy ? 0.5 : 0
Anim on waveProgress {
id: waveProgAnim
running: true
from: 0
to: 1
duration: 2000
easing.type: Easing.Linear
loops: Animation.Infinite
}
Behavior on color {
CAnim {}
}
Behavior on amplitudeMultiplier {
Anim {
type: Anim.DefaultEffects
}
}
}
Loader {
id: dot
x: root.size / 2 + root.arcRadius * Math.cos(root.dotAngleRad) - width / 2
y: root.size / 2 + root.arcRadius * Math.sin(root.dotAngleRad) - height / 2
sourceComponent: StyledRect {
radius: Tokens.rounding.full
color: root.fgColour
opacity: Math.min(1, remainingArc.sweepAngle)
implicitWidth: Math.min(1, remainingArc.sweepAngle) * Math.min(4, root.strokeWidth)
implicitHeight: Math.min(1, remainingArc.sweepAngle) * Math.min(4, root.strokeWidth)
}
}
}