diff options
| -rw-r--r-- | components/controls/CircularIndicator.qml | 108 | ||||
| -rw-r--r-- | components/controls/CircularProgress.qml | 9 | ||||
| -rw-r--r-- | components/controls/StyledBusyIndicator.qml | 151 | ||||
| -rw-r--r-- | modules/bar/popouts/Bluetooth.qml | 2 | ||||
| -rw-r--r-- | modules/bar/popouts/Network.qml | 4 | ||||
| -rw-r--r-- | modules/controlcenter/bluetooth/DeviceList.qml | 2 | ||||
| -rw-r--r-- | modules/lock/Center.qml | 2 | ||||
| -rw-r--r-- | plugin/src/Caelestia/Managers/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | plugin/src/Caelestia/Managers/circularindicatormanager.cpp | 211 | ||||
| -rw-r--r-- | plugin/src/Caelestia/Managers/circularindicatormanager.hpp | 72 |
10 files changed, 400 insertions, 162 deletions
diff --git a/components/controls/CircularIndicator.qml b/components/controls/CircularIndicator.qml new file mode 100644 index 0000000..cbf300a --- /dev/null +++ b/components/controls/CircularIndicator.qml @@ -0,0 +1,108 @@ +import ".." +import qs.services +import qs.config +import Caelestia.Managers +import QtQuick +import QtQuick.Templates + +BusyIndicator { + id: root + + enum AnimType { + Advance = 0, + Retreat + } + + enum AnimState { + Stopped, + Running, + Completing + } + + property real implicitSize: Appearance.font.size.normal * 3 + property real strokeWidth: Appearance.padding.small * 0.8 + property color fgColour: Colours.palette.m3primary + property color bgColour: Colours.palette.m3secondaryContainer + + property alias type: manager.indeterminateAnimationType + readonly property alias progress: manager.progress + + property real internalStrokeWidth: strokeWidth + property int animState + + padding: 0 + implicitWidth: implicitSize + implicitHeight: implicitSize + + Component.onCompleted: { + if (running) { + running = false; + running = true; + } + } + + onRunningChanged: { + if (running) { + manager.completeEndProgress = 0; + animState = CircularIndicator.Running; + } else { + if (animState == CircularIndicator.Running) + animState = CircularIndicator.Completing; + } + } + + states: State { + name: "stopped" + when: !root.running + + PropertyChanges { + root.opacity: 0 + root.internalStrokeWidth: root.strokeWidth / 3 + } + } + + transitions: Transition { + Anim { + properties: "opacity,internalStrokeWidth" + duration: manager.completeEndDuration * Appearance.anim.durations.scale + } + } + + contentItem: CircularProgress { + anchors.fill: parent + strokeWidth: root.internalStrokeWidth + fgColour: root.fgColour + bgColour: root.bgColour + padding: root.padding + rotation: manager.rotation + startAngle: manager.startFraction * 360 + value: manager.endFraction - manager.startFraction + } + + CircularIndicatorManager { + id: manager + } + + NumberAnimation { + running: root.animState !== CircularIndicator.Stopped + loops: Animation.Infinite + target: manager + property: "progress" + from: 0 + to: 1 + duration: manager.duration * Appearance.anim.durations.scale + } + + NumberAnimation { + running: root.animState === CircularIndicator.Completing + target: manager + property: "completeEndProgress" + from: 0 + to: 1 + duration: manager.completeEndDuration * Appearance.anim.durations.scale + onFinished: { + if (root.animState === CircularIndicator.Completing) + root.animState = CircularIndicator.Stopped; + } + } +} diff --git a/components/controls/CircularProgress.qml b/components/controls/CircularProgress.qml index 209d53b..a15cd90 100644 --- a/components/controls/CircularProgress.qml +++ b/components/controls/CircularProgress.qml @@ -1,3 +1,4 @@ +import ".." import qs.services import qs.config import QtQuick @@ -38,10 +39,8 @@ Shape { } Behavior on strokeColor { - ColorAnimation { + CAnim { duration: Appearance.anim.durations.large - easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standard } } } @@ -62,10 +61,8 @@ Shape { } Behavior on strokeColor { - ColorAnimation { + CAnim { duration: Appearance.anim.durations.large - easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standard } } } diff --git a/components/controls/StyledBusyIndicator.qml b/components/controls/StyledBusyIndicator.qml deleted file mode 100644 index 9fd1c3e..0000000 --- a/components/controls/StyledBusyIndicator.qml +++ /dev/null @@ -1,151 +0,0 @@ -import ".." -import qs.services -import qs.config -import QtQuick -import QtQuick.Controls - -BusyIndicator { - id: root - - property real implicitSize: Appearance.font.size.normal * 3 - property real strokeWidth: Appearance.padding.small - property color fgColour: Colours.palette.m3primary - property color bgColour: Colours.palette.m3secondaryContainer - - property real internalStrokeWidth: strokeWidth - property string animState - - padding: 0 - implicitWidth: implicitSize - implicitHeight: implicitSize - - onRunningChanged: { - if (running) { - updater.completeEndProgress = 0; - animState = "running"; - } else { - if (animState == "running") - animState = "completing"; - } - } - - states: State { - name: "stopped" - when: !root.running - - PropertyChanges { - root.opacity: 0 - root.internalStrokeWidth: root.strokeWidth / 3 - } - } - - transitions: Transition { - Anim { - properties: "opacity,internalStrokeWidth" - duration: updater.completeEndDuration - } - } - - background: null - - contentItem: CircularProgress { - anchors.fill: parent - strokeWidth: root.internalStrokeWidth - fgColour: root.fgColour - bgColour: root.bgColour - padding: root.padding - startAngle: updater.startFraction * 360 - value: updater.endFraction - updater.startFraction - } - - Updater { - id: updater - } - - NumberAnimation { - running: root.animState !== "stopped" - loops: Animation.Infinite - target: updater - property: "progress" - from: 0 - to: 1 - duration: updater.duration - } - - NumberAnimation { - running: root.animState === "completing" - target: updater - property: "completeEndProgress" - from: 0 - to: 1 - duration: updater.completeEndDuration - onFinished: { - if (root.animState === "completing") - root.animState = "stopped"; - } - } - - component Updater: QtObject { - readonly property int duration: 5400 * Appearance.anim.durations.scale - readonly property int expandDuration: 667 * Appearance.anim.durations.scale - readonly property int collapseDuration: 667 * Appearance.anim.durations.scale - readonly property int completeEndDuration: 333 * Appearance.anim.durations.scale - readonly property int tailDegOffset: -20 - readonly property int extraDegPerCycle: 250 - readonly property int constantRotDeg: 1520 - readonly property list<int> expandDelay: [0, 1350, 2700, 4050].map(d => d * Appearance.anim.durations.scale) - readonly property list<int> collapseDelay: [667, 2017, 3367, 4717].map(d => d * Appearance.anim.durations.scale) - - property real progress: 0 - property real startFraction: 0 - property real endFraction: 0 - property real rotation: 0 - property real completeEndProgress: 0 - - onProgressChanged: update(progress) - - function update(p: real): void { - const playtime = p * duration; - let startDeg = constantRotDeg * p + tailDegOffset; - let endDeg = constantRotDeg * p; - - for (let i = 0; i < 4; i++) { - const expandFraction = getFractionInRange(playtime, expandDelay[i], expandDuration); - endDeg += fastOutSlowIn(expandFraction) * extraDegPerCycle; - - const collapseFraction = getFractionInRange(playtime, collapseDelay[i], collapseDuration); - startDeg += fastOutSlowIn(collapseFraction) * extraDegPerCycle; - } - - // Gap closing - startDeg += (endDeg - startDeg) * completeEndProgress; - - startFraction = startDeg / 360; - endFraction = endDeg / 360; - } - - function getFractionInRange(currentTime: real, delay: int, duration: int): real { - if (currentTime < delay) - return 0; - if (currentTime > delay + duration) - return 1; - return (currentTime - delay) / duration; - } - - function lerp(a: real, b: real, t: real): real { - return a + (b - a) * t; - } - - function cubic(a: real, b: real, c: real, d: real, t: real): real { - return ((1 - t) ** 3) * a + 3 * ((1 - t) ** 2) * t * b + 3 * (1 - t) * (t ** 2) * c + (t ** 3) * d; - } - - function cubicBezier(p1x: real, p1y: real, p2x: real, p2y: real, t: real): real { - return cubic(0, p1y, p2y, 1, t); - } - - function fastOutSlowIn(t: real): real { - return cubicBezier(0.4, 0.0, 0.2, 1.0, t); - } - } -} diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml index e623eae..53d8b29 100644 --- a/modules/bar/popouts/Bluetooth.qml +++ b/modules/bar/popouts/Bluetooth.qml @@ -110,7 +110,7 @@ ColumnLayout { radius: Appearance.rounding.full color: Qt.alpha(Colours.palette.m3primary, device.modelData.state === BluetoothDeviceState.Connected ? 1 : 0) - StyledBusyIndicator { + CircularIndicator { anchors.fill: parent running: device.loading } diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index 21d1913..f21a92d 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -104,7 +104,7 @@ ColumnLayout { radius: Appearance.rounding.full color: Qt.alpha(Colours.palette.m3primary, networkItem.modelData.active ? 1 : 0) - StyledBusyIndicator { + CircularIndicator { anchors.fill: parent running: networkItem.loading } @@ -183,7 +183,7 @@ ColumnLayout { } } - StyledBusyIndicator { + CircularIndicator { anchors.centerIn: parent strokeWidth: Appearance.padding.small / 2 bgColour: "transparent" diff --git a/modules/controlcenter/bluetooth/DeviceList.qml b/modules/controlcenter/bluetooth/DeviceList.qml index 359886c..020eced 100644 --- a/modules/controlcenter/bluetooth/DeviceList.qml +++ b/modules/controlcenter/bluetooth/DeviceList.qml @@ -242,7 +242,7 @@ ColumnLayout { radius: Appearance.rounding.full color: Qt.alpha(Colours.palette.m3primaryContainer, device.connected ? 1 : 0) - StyledBusyIndicator { + CircularIndicator { anchors.fill: parent running: device.loading } diff --git a/modules/lock/Center.qml b/modules/lock/Center.qml index ce5db92..a827bdd 100644 --- a/modules/lock/Center.qml +++ b/modules/lock/Center.qml @@ -174,7 +174,7 @@ ColumnLayout { } } - StyledBusyIndicator { + CircularIndicator { anchors.fill: parent running: root.lock.pam.passwd.active } diff --git a/plugin/src/Caelestia/Managers/CMakeLists.txt b/plugin/src/Caelestia/Managers/CMakeLists.txt index 9bb5baa..d2083e3 100644 --- a/plugin/src/Caelestia/Managers/CMakeLists.txt +++ b/plugin/src/Caelestia/Managers/CMakeLists.txt @@ -2,6 +2,7 @@ qml_module(caelestia-managers URI Caelestia.Managers SOURCES cachingimagemanager.hpp cachingimagemanager.cpp + circularindicatormanager.hpp circularindicatormanager.cpp LIBRARIES Qt::Gui Qt::Quick diff --git a/plugin/src/Caelestia/Managers/circularindicatormanager.cpp b/plugin/src/Caelestia/Managers/circularindicatormanager.cpp new file mode 100644 index 0000000..ac0c428 --- /dev/null +++ b/plugin/src/Caelestia/Managers/circularindicatormanager.cpp @@ -0,0 +1,211 @@ +#include "circularindicatormanager.hpp" +#include <qeasingcurve.h> +#include <qpoint.h> + +namespace { + +namespace advance { + +constexpr qint32 TOTAL_CYCLES = 4; +constexpr qint32 TOTAL_DURATION_IN_MS = 5400; +constexpr qint32 DURATION_TO_EXPAND_IN_MS = 667; +constexpr qint32 DURATION_TO_COLLAPSE_IN_MS = 667; +constexpr qint32 DURATION_TO_COMPLETE_END_IN_MS = 333; +constexpr qint32 TAIL_DEGREES_OFFSET = -20; +constexpr qint32 EXTRA_DEGREES_PER_CYCLE = 250; +constexpr qint32 CONSTANT_ROTATION_DEGREES = 1520; + +constexpr std::array<qint32, TOTAL_CYCLES> DELAY_TO_EXPAND_IN_MS = { 0, 1350, 2700, 4050 }; +constexpr std::array<qint32, TOTAL_CYCLES> DELAY_TO_COLLAPSE_IN_MS = { 667, 2017, 3367, 4717 }; + +} // namespace advance + +namespace retreat { + +constexpr qint32 TOTAL_DURATION_IN_MS = 6000; +constexpr qint32 DURATION_SPIN_IN_MS = 500; +constexpr qint32 DURATION_GROW_ACTIVE_IN_MS = 3000; +constexpr qint32 DURATION_SHRINK_ACTIVE_IN_MS = 3000; +constexpr std::array DELAY_SPINS_IN_MS = { 0, 1500, 3000, 4500 }; +constexpr qint32 DELAY_GROW_ACTIVE_IN_MS = 0; +constexpr qint32 DELAY_SHRINK_ACTIVE_IN_MS = 3000; +constexpr qint32 DURATION_TO_COMPLETE_END_IN_MS = 500; + +// Constants for animation values. + +// The total degrees that a constant rotation goes by. +constexpr qint32 CONSTANT_ROTATION_DEGREES = 1080; +// Despite of the constant rotation, there are also 5 extra rotations the entire animation. The +// total degrees that each extra rotation goes by. +constexpr qint32 SPIN_ROTATION_DEGREES = 90; +constexpr std::array<qreal, 2> END_FRACTION_RANGE = { 0.10, 0.87 }; + +} // namespace retreat + +inline qreal getFractionInRange(qreal playtime, qreal start, qreal duration) { + const auto fraction = (playtime - start) / duration; + return std::clamp(fraction, 0.0, 1.0); +} + +} // namespace + +namespace caelestia { + +CircularIndicatorManager::CircularIndicatorManager(QObject* parent) + : QObject(parent) + , m_type(IndeterminateAnimationType::Advance) + , m_curve(QEasingCurve(QEasingCurve::BezierSpline)) + , m_progress(0) + , m_startFraction(0) + , m_endFraction(0) + , m_rotation(0) + , m_completeEndProgress(0) { + // Fast out slow in + m_curve.addCubicBezierSegment({ 0.4, 0.0 }, { 0.2, 1.0 }, { 1.0, 1.0 }); +} + +qreal CircularIndicatorManager::startFraction() const { + return m_startFraction; +} + +qreal CircularIndicatorManager::endFraction() const { + return m_endFraction; +} + +qreal CircularIndicatorManager::rotation() const { + return m_rotation; +} + +qreal CircularIndicatorManager::progress() const { + return m_progress; +} + +void CircularIndicatorManager::setProgress(qreal progress) { + update(progress); +} + +qreal CircularIndicatorManager::duration() const { + if (m_type == IndeterminateAnimationType::Advance) { + return advance::TOTAL_DURATION_IN_MS; + } else { + return retreat::TOTAL_DURATION_IN_MS; + } +} + +qreal CircularIndicatorManager::completeEndDuration() const { + if (m_type == IndeterminateAnimationType::Advance) { + return advance::DURATION_TO_COMPLETE_END_IN_MS; + } else { + return retreat::DURATION_TO_COMPLETE_END_IN_MS; + } +} + +CircularIndicatorManager::IndeterminateAnimationType CircularIndicatorManager::indeterminateAnimationType() const { + return m_type; +} + +void CircularIndicatorManager::setIndeterminateAnimationType(IndeterminateAnimationType t) { + if (m_type != t) { + m_type = t; + emit indeterminateAnimationTypeChanged(); + } +} + +qreal CircularIndicatorManager::completeEndProgress() const { + return m_completeEndProgress; +} + +void CircularIndicatorManager::setCompleteEndProgress(qreal progress) { + if (qFuzzyCompare(m_completeEndProgress + 1.0, progress + 1.0)) { + return; + } + + m_completeEndProgress = progress; + emit completeEndProgressChanged(); + + update(m_progress); +} + +void CircularIndicatorManager::update(qreal progress) { + if (qFuzzyCompare(m_progress + 1.0, progress + 1.0)) { + return; + } + + if (m_type == IndeterminateAnimationType::Advance) { + updateAdvance(progress); + } else { + updateRetreat(progress); + } + + m_progress = progress; + emit progressChanged(); +} + +void CircularIndicatorManager::updateRetreat(qreal progress) { + using namespace retreat; + const auto playtime = progress * TOTAL_DURATION_IN_MS; + + // Constant rotation. + const qreal constantRotation = CONSTANT_ROTATION_DEGREES * progress; + // Extra rotation for the faster spinning. + qreal spinRotation = 0; + for (const int spinDelay : DELAY_SPINS_IN_MS) { + spinRotation += m_curve.valueForProgress(getFractionInRange(playtime, spinDelay, DURATION_SPIN_IN_MS)) * + SPIN_ROTATION_DEGREES; + } + m_rotation = constantRotation + spinRotation; + emit rotationChanged(); + + // Grow active indicator. + qreal fraction = + m_curve.valueForProgress(getFractionInRange(playtime, DELAY_GROW_ACTIVE_IN_MS, DURATION_GROW_ACTIVE_IN_MS)); + fraction -= + m_curve.valueForProgress(getFractionInRange(playtime, DELAY_SHRINK_ACTIVE_IN_MS, DURATION_SHRINK_ACTIVE_IN_MS)); + + if (!qFuzzyIsNull(m_startFraction)) { + m_startFraction = 0.0; + emit startFractionChanged(); + } + const auto oldEndFrac = m_endFraction; + m_endFraction = std::lerp(END_FRACTION_RANGE[0], END_FRACTION_RANGE[1], fraction); + + // Completing animation. + if (m_completeEndProgress > 0) { + m_endFraction *= 1 - m_completeEndProgress; + } + + if (!qFuzzyCompare(m_endFraction + 1.0, oldEndFrac + 1.0)) { + emit endFractionChanged(); + } +} + +void CircularIndicatorManager::updateAdvance(qreal progress) { + using namespace advance; + const auto playtime = progress * TOTAL_DURATION_IN_MS; + + // Adds constant rotation to segment positions. + m_startFraction = CONSTANT_ROTATION_DEGREES * progress + TAIL_DEGREES_OFFSET; + m_endFraction = CONSTANT_ROTATION_DEGREES * progress; + + // Adds cycle specific rotation to segment positions. + for (size_t cycleIndex = 0; cycleIndex < TOTAL_CYCLES; ++cycleIndex) { + // While expanding. + qreal fraction = getFractionInRange(playtime, DELAY_TO_EXPAND_IN_MS[cycleIndex], DURATION_TO_EXPAND_IN_MS); + m_endFraction += m_curve.valueForProgress(fraction) * EXTRA_DEGREES_PER_CYCLE; + + // While collapsing. + fraction = getFractionInRange(playtime, DELAY_TO_COLLAPSE_IN_MS[cycleIndex], DURATION_TO_COLLAPSE_IN_MS); + m_startFraction += m_curve.valueForProgress(fraction) * EXTRA_DEGREES_PER_CYCLE; + } + + // Closes the gap between head and tail for complete end. + m_startFraction += (m_endFraction - m_startFraction) * m_completeEndProgress; + + m_startFraction /= 360.0; + m_endFraction /= 360.0; + + emit startFractionChanged(); + emit endFractionChanged(); +} + +} // namespace caelestia diff --git a/plugin/src/Caelestia/Managers/circularindicatormanager.hpp b/plugin/src/Caelestia/Managers/circularindicatormanager.hpp new file mode 100644 index 0000000..71da93d --- /dev/null +++ b/plugin/src/Caelestia/Managers/circularindicatormanager.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include <qeasingcurve.h> +#include <qobject.h> +#include <qqmlintegration.h> + +namespace caelestia { + +class CircularIndicatorManager : public QObject { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(qreal startFraction READ startFraction NOTIFY startFractionChanged) + Q_PROPERTY(qreal endFraction READ endFraction NOTIFY endFractionChanged) + Q_PROPERTY(qreal rotation READ rotation NOTIFY rotationChanged) + Q_PROPERTY(qreal progress READ progress WRITE setProgress NOTIFY progressChanged) + Q_PROPERTY(qreal completeEndProgress READ completeEndProgress WRITE setCompleteEndProgress NOTIFY + completeEndProgressChanged) + Q_PROPERTY(qreal duration READ duration NOTIFY indeterminateAnimationTypeChanged) + Q_PROPERTY(qreal completeEndDuration READ completeEndDuration NOTIFY indeterminateAnimationTypeChanged) + Q_PROPERTY(IndeterminateAnimationType indeterminateAnimationType READ indeterminateAnimationType WRITE + setIndeterminateAnimationType NOTIFY indeterminateAnimationTypeChanged) + +public: + explicit CircularIndicatorManager(QObject* parent = nullptr); + + enum IndeterminateAnimationType { + Advance = 0, + Retreat + }; + Q_ENUM(IndeterminateAnimationType) + + [[nodiscard]] qreal startFraction() const; + [[nodiscard]] qreal endFraction() const; + [[nodiscard]] qreal rotation() const; + + [[nodiscard]] qreal progress() const; + void setProgress(qreal progress); + + [[nodiscard]] qreal completeEndProgress() const; + void setCompleteEndProgress(qreal progress); + + [[nodiscard]] qreal duration() const; + [[nodiscard]] qreal completeEndDuration() const; + + [[nodiscard]] IndeterminateAnimationType indeterminateAnimationType() const; + void setIndeterminateAnimationType(IndeterminateAnimationType t); + +signals: + void startFractionChanged(); + void endFractionChanged(); + void rotationChanged(); + void progressChanged(); + void completeEndProgressChanged(); + void indeterminateAnimationTypeChanged(); + +private: + IndeterminateAnimationType m_type; + QEasingCurve m_curve; + + qreal m_progress; + qreal m_startFraction; + qreal m_endFraction; + qreal m_rotation; + qreal m_completeEndProgress; + + void update(qreal progress); + void updateAdvance(qreal progress); + void updateRetreat(qreal progress); +}; + +} // namespace caelestia |