feat: add styled progress bar
This commit is contained in:
parent
21b5972d46
commit
28a84a9611
6 changed files with 480 additions and 15 deletions
253
components/controls/StyledProgressBar.qml
Normal file
253
components/controls/StyledProgressBar.qml
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Templates
|
||||
import Caelestia
|
||||
import Caelestia.Components
|
||||
import Caelestia.Config
|
||||
import Caelestia.Internal
|
||||
import qs.components
|
||||
import qs.services
|
||||
|
||||
ProgressBar {
|
||||
id: root
|
||||
|
||||
enum IndeterminateAnimState {
|
||||
Running,
|
||||
Completing,
|
||||
Stopped
|
||||
}
|
||||
|
||||
property color fgColour: Colours.palette.m3primary
|
||||
property color bgColour: Colours.palette.m3secondaryContainer
|
||||
|
||||
property bool wavy
|
||||
property bool wavePaused
|
||||
property int waveFrequency: 6
|
||||
property real waveAmplitude: 0.5
|
||||
property int waveDuration: 1000
|
||||
property bool animateIndeterminate
|
||||
|
||||
property int indeterminateAnimState: StyledProgressBar.Stopped
|
||||
|
||||
function toBounds(startFrac: real, endFrac: real, gapSize: real): point {
|
||||
startFrac = CUtils.clamp(startFrac, 0, 1);
|
||||
endFrac = CUtils.clamp(endFrac, 0, 1);
|
||||
|
||||
// Ramp down gap size
|
||||
const GAP_RAMP_DOWN_THRESHOLD = 0.01;
|
||||
gapSize += height / 2;
|
||||
const startGapSize = (gapSize * CUtils.clamp(startFrac, 0, GAP_RAMP_DOWN_THRESHOLD) / GAP_RAMP_DOWN_THRESHOLD);
|
||||
const endGapSize = (gapSize * (1 - CUtils.clamp(endFrac, 1 - GAP_RAMP_DOWN_THRESHOLD, 1)) / GAP_RAMP_DOWN_THRESHOLD);
|
||||
const start = width * startFrac + startGapSize;
|
||||
const end = width * endFrac - endGapSize;
|
||||
|
||||
return start >= end ? Qt.point(0, 0) : Qt.point(start, end);
|
||||
}
|
||||
|
||||
function updateIAnimState(): void {
|
||||
if (indeterminate && animateIndeterminate) {
|
||||
manager.completeEndProgress = 0;
|
||||
indeterminateAnimState = StyledProgressBar.Running;
|
||||
} else if (indeterminateAnimState === StyledProgressBar.Running) {
|
||||
indeterminateAnimState = StyledProgressBar.Completing;
|
||||
}
|
||||
}
|
||||
|
||||
onIndeterminateChanged: updateIAnimState()
|
||||
onAnimateIndeterminateChanged: updateIAnimState()
|
||||
Component.onCompleted: updateIAnimState()
|
||||
|
||||
implicitWidth: 200
|
||||
implicitHeight: 4
|
||||
|
||||
contentItem: Loader {
|
||||
anchors.fill: parent
|
||||
sourceComponent: root.indeterminate || root.indeterminateAnimState !== StyledProgressBar.Stopped ? indeterminateComp : determinateComp
|
||||
}
|
||||
|
||||
LinearIndicatorManager {
|
||||
id: manager
|
||||
|
||||
gap: Tokens.spacing.extraSmall
|
||||
|
||||
Anim on progress {
|
||||
running: root.indeterminateAnimState !== StyledProgressBar.Stopped
|
||||
from: 0
|
||||
to: 1
|
||||
duration: manager.duration
|
||||
easing.type: Easing.Linear
|
||||
loops: Animation.Infinite
|
||||
}
|
||||
|
||||
Anim on completeEndProgress {
|
||||
running: root.indeterminateAnimState === StyledProgressBar.Completing
|
||||
to: 1
|
||||
duration: manager.completeEndDuration
|
||||
onFinished: {
|
||||
if (root.indeterminateAnimState === StyledProgressBar.Completing)
|
||||
root.indeterminateAnimState = StyledProgressBar.Stopped;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on value {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: determinateComp
|
||||
|
||||
Item {
|
||||
Line {
|
||||
id: remaining
|
||||
|
||||
anchors.right: parent.right
|
||||
implicitWidth: parent.width - wave.implicitWidth - Tokens.spacing.extraSmall
|
||||
}
|
||||
|
||||
Line {
|
||||
property real implicitSize
|
||||
|
||||
Component.onCompleted: implicitSize = Qt.binding(() => parent.width * (1 - root.visualPosition) < parent.height ? parent.height : 4)
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.rightMargin: (parent.height - implicitHeight) / 2
|
||||
|
||||
implicitWidth: implicitSize
|
||||
implicitHeight: implicitSize
|
||||
|
||||
radius: Tokens.rounding.full
|
||||
color: root.fgColour
|
||||
|
||||
Behavior on implicitSize {
|
||||
Anim {
|
||||
type: Anim.FastSpatial
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Wave {
|
||||
id: wave
|
||||
|
||||
readonly property real targetWidth: parent.width * root.visualPosition
|
||||
|
||||
anchors.left: parent.left
|
||||
|
||||
Anim on implicitWidth {
|
||||
running: true
|
||||
to: wave.targetWidth
|
||||
onFinished: wave.implicitWidth = Qt.binding(() => wave.targetWidth)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: indeterminateComp
|
||||
|
||||
Item {
|
||||
id: content
|
||||
|
||||
Line {
|
||||
bounds: {
|
||||
const i = manager.activeIndicators[0]; // qmllint disable unresolved-type
|
||||
return i ? root.toBounds(0, i.startFraction, i.gapSize / 2) : Qt.point(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
Line {
|
||||
bounds: {
|
||||
const i = manager.activeIndicators[manager.activeIndicators.length - 1]; // qmllint disable unresolved-type
|
||||
return i ? root.toBounds(i.endFraction, 1, i.gapSize / 2) : Qt.point(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
Instantiator {
|
||||
model: Math.max(manager.activeIndicators.length, 1) - 1 // qmllint disable unresolved-type
|
||||
|
||||
delegate: Line {
|
||||
required property int index
|
||||
readonly property LinearIndicatorSegment cur: manager.activeIndicators[index] // qmllint disable unresolved-type
|
||||
readonly property LinearIndicatorSegment next: manager.activeIndicators[index + 1 % manager.activeIndicators.length] // qmllint disable unresolved-type
|
||||
|
||||
bounds: root.toBounds(cur.endFraction, next.startFraction, cur.gapSize / 2)
|
||||
}
|
||||
|
||||
onObjectAdded: (_, obj) => content.data.push(obj)
|
||||
onObjectRemoved: (_, obj) => {
|
||||
const idx = content.data.indexOf(obj);
|
||||
if (idx !== -1)
|
||||
content.data.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
Instantiator {
|
||||
model: manager.activeIndicators // qmllint disable unresolved-type
|
||||
|
||||
delegate: Wave {
|
||||
required property LinearIndicatorSegment modelData
|
||||
readonly property point bounds: root.toBounds(modelData.startFraction, modelData.endFraction, modelData.gapSize / 2)
|
||||
|
||||
x: bounds.x
|
||||
implicitWidth: bounds.y - bounds.x
|
||||
|
||||
color: root.fgColour
|
||||
}
|
||||
|
||||
onObjectAdded: (_, obj) => content.data.push(obj)
|
||||
onObjectRemoved: (_, obj) => {
|
||||
const idx = content.data.indexOf(obj);
|
||||
if (idx !== -1)
|
||||
content.data.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Line: StyledRect {
|
||||
property point bounds
|
||||
|
||||
x: bounds.x
|
||||
implicitWidth: bounds.y - bounds.x
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
implicitHeight: parent.height
|
||||
|
||||
radius: Tokens.rounding.full
|
||||
color: root.fgColour
|
||||
}
|
||||
|
||||
component Wave: WavyLine {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
implicitHeight: lineWidth * amplitudeMultiplier * 2 + lineWidth
|
||||
|
||||
lineWidth: parent.height
|
||||
amplitudeMultiplier: root.wavy ? root.waveAmplitude : 0
|
||||
frequency: root.waveFrequency
|
||||
startX: x
|
||||
fullLength: parent.width
|
||||
color: root.fgColour
|
||||
|
||||
Anim on waveProgress {
|
||||
running: true
|
||||
paused: root.wavePaused
|
||||
from: 0
|
||||
to: 1
|
||||
duration: root.waveDuration
|
||||
easing.type: Easing.Linear
|
||||
loops: Animation.Infinite
|
||||
}
|
||||
|
||||
Behavior on amplitudeMultiplier {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
qml_module(caelestia-internal
|
||||
URI Caelestia.Internal
|
||||
SOURCES
|
||||
arcgauge.hpp arcgauge.cpp
|
||||
cachingimagemanager.hpp cachingimagemanager.cpp
|
||||
circularbuffer.hpp circularbuffer.cpp
|
||||
circularindicatormanager.hpp circularindicatormanager.cpp
|
||||
hyprdevices.hpp hyprdevices.cpp
|
||||
hyprextras.hpp hyprextras.cpp
|
||||
logindmanager.hpp logindmanager.cpp
|
||||
sparklineitem.hpp sparklineitem.cpp
|
||||
visualiserbars.hpp visualiserbars.cpp
|
||||
arcgauge.cpp
|
||||
cachingimagemanager.cpp
|
||||
circularbuffer.cpp
|
||||
circularindicatormanager.cpp
|
||||
linearindicatormanager.cpp
|
||||
hyprdevices.cpp
|
||||
hyprextras.cpp
|
||||
logindmanager.cpp
|
||||
sparklineitem.cpp
|
||||
visualiserbars.cpp
|
||||
LIBRARIES
|
||||
Qt::Gui
|
||||
Qt::Quick
|
||||
|
|
|
|||
119
plugin/src/Caelestia/Internal/linearindicatormanager.cpp
Normal file
119
plugin/src/Caelestia/Internal/linearindicatormanager.cpp
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
#include "linearindicatormanager.hpp"
|
||||
|
||||
#include <qpoint.h>
|
||||
|
||||
namespace {
|
||||
|
||||
// See
|
||||
// https://github.com/material-components/material-components-android/blob/master/lib/java/com/google/android/material/progressindicator/LinearIndeterminateDisjointAnimatorDelegate.java#L44-L46
|
||||
constexpr int TOTAL_DURATION_IN_MS = 1800;
|
||||
constexpr std::array DURATION_TO_MOVE_SEGMENT_ENDS = { 533, 567, 850, 750 };
|
||||
constexpr std::array DELAY_TO_MOVE_SEGMENT_ENDS = { 1267, 1000, 333, 0 };
|
||||
|
||||
QEasingCurve curve(const QPointF& c1, const QPointF& c2) {
|
||||
QEasingCurve curve(QEasingCurve::BezierSpline);
|
||||
curve.addCubicBezierSegment(c1, c2, { 1.0, 1.0 });
|
||||
return curve;
|
||||
}
|
||||
|
||||
qreal getFractionInRange(qreal playtime, int start, int duration) {
|
||||
const auto fraction = static_cast<qreal>(playtime - start) / duration;
|
||||
return std::clamp(fraction, 0.0, 1.0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace caelestia::controls {
|
||||
|
||||
LinearIndicatorSegment::LinearIndicatorSegment(int gap, QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_startFraction(0)
|
||||
, m_endFraction(0)
|
||||
, m_gapSize(gap) {}
|
||||
|
||||
qreal LinearIndicatorSegment::startFraction() const {
|
||||
return m_startFraction;
|
||||
}
|
||||
|
||||
qreal LinearIndicatorSegment::endFraction() const {
|
||||
return m_endFraction;
|
||||
}
|
||||
|
||||
int LinearIndicatorSegment::gapSize() const {
|
||||
return m_gapSize;
|
||||
}
|
||||
|
||||
LinearIndicatorManager::LinearIndicatorManager(QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_interpolators({
|
||||
curve({ 0.2, 0.0 }, { 0.8, 1.0 }),
|
||||
curve({ 0.4, 0.0 }, { 1.0, 1.0 }),
|
||||
curve({ 0.0, 0.0 }, { 0.65, 1.0 }),
|
||||
curve({ 0.1, 0.0 }, { 0.45, 1.0 }),
|
||||
})
|
||||
, m_progress(0)
|
||||
, m_completeEndProgress(0)
|
||||
, m_gap(4)
|
||||
, m_activeIndicators({
|
||||
new LinearIndicatorSegment(m_gap, this),
|
||||
new LinearIndicatorSegment(m_gap, this),
|
||||
}) {
|
||||
for (auto el : m_activeIndicators)
|
||||
QObject::connect(this, &LinearIndicatorManager::updated, el, &LinearIndicatorSegment::updated);
|
||||
}
|
||||
|
||||
QList<LinearIndicatorSegment*> LinearIndicatorManager::activeIndicators() const {
|
||||
return { m_activeIndicators.cbegin(), m_activeIndicators.cend() };
|
||||
}
|
||||
|
||||
qreal LinearIndicatorManager::progress() const {
|
||||
return m_progress;
|
||||
}
|
||||
|
||||
qreal LinearIndicatorManager::completeEndProgress() const {
|
||||
return m_completeEndProgress;
|
||||
}
|
||||
|
||||
int LinearIndicatorManager::gap() const {
|
||||
return m_gap;
|
||||
}
|
||||
|
||||
void LinearIndicatorManager::setGap(int gap) {
|
||||
m_gap = gap;
|
||||
for (auto el : m_activeIndicators)
|
||||
el->m_gapSize = m_gap;
|
||||
update(m_progress);
|
||||
}
|
||||
|
||||
int LinearIndicatorManager::duration() const {
|
||||
return TOTAL_DURATION_IN_MS;
|
||||
}
|
||||
|
||||
int LinearIndicatorManager::completeEndDuration() const {
|
||||
return TOTAL_DURATION_IN_MS;
|
||||
}
|
||||
|
||||
void LinearIndicatorManager::update(qreal progress) {
|
||||
const auto playtime = progress * TOTAL_DURATION_IN_MS;
|
||||
for (size_t i = 0; i < SEGMENTS; i++) {
|
||||
const auto di = i * 2;
|
||||
auto* const indicator = m_activeIndicators[i];
|
||||
|
||||
auto fraction = getFractionInRange(playtime, DELAY_TO_MOVE_SEGMENT_ENDS[di], DURATION_TO_MOVE_SEGMENT_ENDS[di]);
|
||||
indicator->m_startFraction = std::clamp(m_interpolators[di].valueForProgress(fraction), 0.0, 1.0);
|
||||
|
||||
fraction =
|
||||
getFractionInRange(playtime, DELAY_TO_MOVE_SEGMENT_ENDS[di + 1], DURATION_TO_MOVE_SEGMENT_ENDS[di + 1]);
|
||||
indicator->m_endFraction = std::clamp(m_interpolators[di + 1].valueForProgress(fraction), 0.0, 1.0);
|
||||
}
|
||||
|
||||
m_progress = progress;
|
||||
emit updated();
|
||||
}
|
||||
|
||||
void LinearIndicatorManager::updateCompleteEndProgress(qreal progress) {
|
||||
m_completeEndProgress = progress;
|
||||
update(m_progress);
|
||||
}
|
||||
|
||||
} // namespace caelestia::controls
|
||||
86
plugin/src/Caelestia/Internal/linearindicatormanager.hpp
Normal file
86
plugin/src/Caelestia/Internal/linearindicatormanager.hpp
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
#pragma once
|
||||
|
||||
#include <qcolor.h>
|
||||
#include <qeasingcurve.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace caelestia::controls {
|
||||
|
||||
class LinearIndicatorManager;
|
||||
|
||||
class LinearIndicatorSegment : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("LinearIndicatorSegments can only be retrieved from a "
|
||||
"LinearIndicatorManager.")
|
||||
|
||||
Q_PROPERTY(qreal startFraction READ startFraction NOTIFY updated FINAL)
|
||||
Q_PROPERTY(qreal endFraction READ endFraction NOTIFY updated FINAL)
|
||||
Q_PROPERTY(int gapSize READ gapSize NOTIFY updated FINAL)
|
||||
|
||||
public:
|
||||
explicit LinearIndicatorSegment(int gap, QObject* parent = nullptr);
|
||||
|
||||
qreal startFraction() const;
|
||||
qreal endFraction() const;
|
||||
int gapSize() const;
|
||||
|
||||
signals:
|
||||
void updated();
|
||||
|
||||
private:
|
||||
qreal m_startFraction;
|
||||
qreal m_endFraction;
|
||||
int m_gapSize;
|
||||
|
||||
friend LinearIndicatorManager;
|
||||
};
|
||||
|
||||
class LinearIndicatorManager : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(
|
||||
QList<caelestia::controls::LinearIndicatorSegment*> activeIndicators READ activeIndicators CONSTANT FINAL)
|
||||
|
||||
Q_PROPERTY(qreal progress READ progress WRITE update NOTIFY updated FINAL)
|
||||
Q_PROPERTY(qreal completeEndProgress READ completeEndProgress WRITE updateCompleteEndProgress NOTIFY updated FINAL)
|
||||
Q_PROPERTY(int gap READ gap WRITE setGap NOTIFY updated FINAL)
|
||||
|
||||
Q_PROPERTY(qreal duration READ duration CONSTANT FINAL)
|
||||
Q_PROPERTY(qreal completeEndDuration READ completeEndDuration CONSTANT FINAL)
|
||||
|
||||
public:
|
||||
explicit LinearIndicatorManager(QObject* parent = nullptr);
|
||||
|
||||
QList<LinearIndicatorSegment*> activeIndicators() const;
|
||||
|
||||
qreal progress() const;
|
||||
qreal completeEndProgress() const;
|
||||
|
||||
int gap() const;
|
||||
void setGap(int gap);
|
||||
|
||||
int duration() const;
|
||||
int completeEndDuration() const;
|
||||
|
||||
void update(qreal progress);
|
||||
void updateCompleteEndProgress(qreal progress);
|
||||
|
||||
signals:
|
||||
void updated();
|
||||
|
||||
private:
|
||||
static constexpr int SEGMENTS = 2;
|
||||
|
||||
std::array<QEasingCurve, 4> m_interpolators;
|
||||
qreal m_progress;
|
||||
qreal m_completeEndProgress;
|
||||
int m_gap;
|
||||
|
||||
std::array<LinearIndicatorSegment*, SEGMENTS> m_activeIndicators;
|
||||
};
|
||||
|
||||
} // namespace caelestia::controls
|
||||
|
|
@ -100,7 +100,7 @@ void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, Q
|
|||
});
|
||||
}
|
||||
|
||||
bool CUtils::copyFile(const QUrl& source, const QUrl& target, bool overwrite) const {
|
||||
bool CUtils::copyFile(const QUrl& source, const QUrl& target, bool overwrite) {
|
||||
if (!source.isLocalFile()) {
|
||||
qCWarning(lcCUtils) << "copyFile: source" << source << "is not a local file";
|
||||
return false;
|
||||
|
|
@ -120,7 +120,7 @@ bool CUtils::copyFile(const QUrl& source, const QUrl& target, bool overwrite) co
|
|||
return QFile::copy(source.toLocalFile(), target.toLocalFile());
|
||||
}
|
||||
|
||||
bool CUtils::deleteFile(const QUrl& path) const {
|
||||
bool CUtils::deleteFile(const QUrl& path) {
|
||||
if (!path.isLocalFile()) {
|
||||
qCWarning(lcCUtils) << "deleteFile: path" << path << "is not a local file";
|
||||
return false;
|
||||
|
|
@ -129,7 +129,7 @@ bool CUtils::deleteFile(const QUrl& path) const {
|
|||
return QFile::remove(path.toLocalFile());
|
||||
}
|
||||
|
||||
QString CUtils::toLocalFile(const QUrl& url) const {
|
||||
QString CUtils::toLocalFile(const QUrl& url) {
|
||||
if (!url.isLocalFile()) {
|
||||
qCWarning(lcCUtils) << "toLocalFile: given url is not a local file" << url;
|
||||
return QString();
|
||||
|
|
@ -138,4 +138,8 @@ QString CUtils::toLocalFile(const QUrl& url) const {
|
|||
return url.toLocalFile();
|
||||
}
|
||||
|
||||
qreal CUtils::clamp(qreal value, qreal min, qreal max) {
|
||||
return qBound(min, value, max);
|
||||
}
|
||||
|
||||
} // namespace caelestia
|
||||
|
|
|
|||
|
|
@ -21,9 +21,11 @@ public:
|
|||
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed);
|
||||
// clang-format on
|
||||
|
||||
Q_INVOKABLE bool copyFile(const QUrl& source, const QUrl& target, bool overwrite = true) const;
|
||||
Q_INVOKABLE bool deleteFile(const QUrl& path) const;
|
||||
Q_INVOKABLE QString toLocalFile(const QUrl& url) const;
|
||||
Q_INVOKABLE static bool copyFile(const QUrl& source, const QUrl& target, bool overwrite = true);
|
||||
Q_INVOKABLE static bool deleteFile(const QUrl& path);
|
||||
Q_INVOKABLE static QString toLocalFile(const QUrl& url);
|
||||
|
||||
Q_INVOKABLE static qreal clamp(qreal value, qreal min, qreal max);
|
||||
};
|
||||
|
||||
} // namespace caelestia
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue