diff options
| author | 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> | 2025-09-13 15:54:01 +1000 |
|---|---|---|
| committer | 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> | 2025-09-13 15:54:01 +1000 |
| commit | c2aa63b99abb6d1388f0e4f5e64a20ee24dd7551 (patch) | |
| tree | e9b146773dda02d0ec20a299469b2ddd839ae870 /plugin/src/Caelestia/Internal | |
| parent | plugin/managers: add CircularIndicatorManager (diff) | |
| download | caelestia-shell-c2aa63b99abb6d1388f0e4f5e64a20ee24dd7551.tar.gz caelestia-shell-c2aa63b99abb6d1388f0e4f5e64a20ee24dd7551.tar.bz2 caelestia-shell-c2aa63b99abb6d1388f0e4f5e64a20ee24dd7551.zip | |
plugin: managers -> internal
Diffstat (limited to 'plugin/src/Caelestia/Internal')
5 files changed, 581 insertions, 0 deletions
diff --git a/plugin/src/Caelestia/Internal/CMakeLists.txt b/plugin/src/Caelestia/Internal/CMakeLists.txt new file mode 100644 index 0000000..62aa516 --- /dev/null +++ b/plugin/src/Caelestia/Internal/CMakeLists.txt @@ -0,0 +1,10 @@ +qml_module(caelestia-internal + URI Caelestia.Internal + SOURCES + cachingimagemanager.hpp cachingimagemanager.cpp + circularindicatormanager.hpp circularindicatormanager.cpp + LIBRARIES + Qt::Gui + Qt::Quick + Qt::Concurrent +) diff --git a/plugin/src/Caelestia/Internal/cachingimagemanager.cpp b/plugin/src/Caelestia/Internal/cachingimagemanager.cpp new file mode 100644 index 0000000..3394f89 --- /dev/null +++ b/plugin/src/Caelestia/Internal/cachingimagemanager.cpp @@ -0,0 +1,223 @@ +#include "cachingimagemanager.hpp" + +#include <QtQuick/qquickwindow.h> +#include <qcryptographichash.h> +#include <qdir.h> +#include <qfileinfo.h> +#include <qfuturewatcher.h> +#include <qimagereader.h> +#include <qpainter.h> +#include <qtconcurrentrun.h> + +namespace caelestia { + +qreal CachingImageManager::effectiveScale() const { + if (m_item && m_item->window()) { + return m_item->window()->devicePixelRatio(); + } + + return 1.0; +} + +QSize CachingImageManager::effectiveSize() const { + if (!m_item) { + return QSize(); + } + + const qreal scale = effectiveScale(); + const QSize size = QSizeF(m_item->width() * scale, m_item->height() * scale).toSize(); + m_item->setProperty("sourceSize", size); + return size; +} + +QQuickItem* CachingImageManager::item() const { + return m_item; +} + +void CachingImageManager::setItem(QQuickItem* item) { + if (m_item == item) { + return; + } + + if (m_widthConn) { + disconnect(m_widthConn); + } + if (m_heightConn) { + disconnect(m_heightConn); + } + + m_item = item; + emit itemChanged(); + + if (item) { + m_widthConn = connect(item, &QQuickItem::widthChanged, this, [this]() { + updateSource(); + }); + m_heightConn = connect(item, &QQuickItem::heightChanged, this, [this]() { + updateSource(); + }); + updateSource(); + } +} + +QUrl CachingImageManager::cacheDir() const { + return m_cacheDir; +} + +void CachingImageManager::setCacheDir(const QUrl& cacheDir) { + if (m_cacheDir == cacheDir) { + return; + } + + m_cacheDir = cacheDir; + if (!m_cacheDir.path().endsWith("/")) { + m_cacheDir.setPath(m_cacheDir.path() + "/"); + } + emit cacheDirChanged(); +} + +QString CachingImageManager::path() const { + return m_path; +} + +void CachingImageManager::setPath(const QString& path) { + if (m_path == path) { + return; + } + + m_path = path; + emit pathChanged(); + + if (!path.isEmpty()) { + updateSource(path); + } +} + +void CachingImageManager::updateSource() { + updateSource(m_path); +} + +void CachingImageManager::updateSource(const QString& path) { + if (path.isEmpty() || path == m_shaPath) { + // Path is empty or already calculating sha for path + return; + } + + m_shaPath = path; + + const auto future = QtConcurrent::run(&CachingImageManager::sha256sum, path); + + const auto watcher = new QFutureWatcher<QString>(this); + + connect(watcher, &QFutureWatcher<QString>::finished, this, [watcher, path, this]() { + if (m_path != path) { + // Object is destroyed or path has changed, ignore + watcher->deleteLater(); + return; + } + + const QSize size = effectiveSize(); + + if (!m_item || !size.width() || !size.height()) { + watcher->deleteLater(); + return; + } + + const QString fillMode = m_item->property("fillMode").toString(); + // clang-format off + const QString filename = QString("%1@%2x%3-%4.png") + .arg(watcher->result()).arg(size.width()).arg(size.height()) + .arg(fillMode == "PreserveAspectCrop" ? "crop" : fillMode == "PreserveAspectFit" ? "fit" : "stretch"); + // clang-format on + + const QUrl cache = m_cacheDir.resolved(QUrl(filename)); + if (m_cachePath == cache) { + watcher->deleteLater(); + return; + } + + m_cachePath = cache; + emit cachePathChanged(); + + if (!cache.isLocalFile()) { + qWarning() << "CachingImageManager::updateSource: cachePath" << cache << "is not a local file"; + watcher->deleteLater(); + return; + } + + const QImageReader reader(cache.toLocalFile()); + if (reader.canRead()) { + m_item->setProperty("source", cache); + } else { + m_item->setProperty("source", QUrl::fromLocalFile(path)); + createCache(path, cache.toLocalFile(), fillMode, size); + } + + // Clear current running sha if same + if (m_shaPath == path) { + m_shaPath = QString(); + } + + watcher->deleteLater(); + }); + + watcher->setFuture(future); +} + +QUrl CachingImageManager::cachePath() const { + return m_cachePath; +} + +void CachingImageManager::createCache( + const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const { + QThreadPool::globalInstance()->start([path, cache, fillMode, size] { + QImage image(path); + + if (image.isNull()) { + qWarning() << "CachingImageManager::createCache: failed to read" << path; + return; + } + + image.convertTo(QImage::Format_ARGB32); + + if (fillMode == "PreserveAspectCrop") { + image = image.scaled(size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); + } else if (fillMode == "PreserveAspectFit") { + image = image.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation); + } else { + image = image.scaled(size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + } + + if (fillMode == "PreserveAspectCrop" || fillMode == "PreserveAspectFit") { + QImage canvas(size, QImage::Format_ARGB32); + canvas.fill(Qt::transparent); + + QPainter painter(&canvas); + painter.drawImage((size.width() - image.width()) / 2, (size.height() - image.height()) / 2, image); + painter.end(); + + image = canvas; + } + + const QString parent = QFileInfo(cache).absolutePath(); + if (!QDir().mkpath(parent) || !image.save(cache)) { + qWarning() << "CachingImageManager::createCache: failed to save to" << cache; + } + }); +} + +QString CachingImageManager::sha256sum(const QString& path) { + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) { + qWarning() << "CachingImageManager::sha256sum: failed to open" << path; + return ""; + } + + QCryptographicHash hash(QCryptographicHash::Sha256); + hash.addData(&file); + file.close(); + + return hash.result().toHex(); +} + +} // namespace caelestia diff --git a/plugin/src/Caelestia/Internal/cachingimagemanager.hpp b/plugin/src/Caelestia/Internal/cachingimagemanager.hpp new file mode 100644 index 0000000..f05ea34 --- /dev/null +++ b/plugin/src/Caelestia/Internal/cachingimagemanager.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include <QtQuick/qquickitem.h> +#include <qobject.h> +#include <qqmlintegration.h> + +namespace caelestia { + +class CachingImageManager : public QObject { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(QQuickItem* item READ item WRITE setItem NOTIFY itemChanged REQUIRED) + Q_PROPERTY(QUrl cacheDir READ cacheDir WRITE setCacheDir NOTIFY cacheDirChanged REQUIRED) + + Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged) + Q_PROPERTY(QUrl cachePath READ cachePath NOTIFY cachePathChanged) + +public: + explicit CachingImageManager(QObject* parent = nullptr) + : QObject(parent) + , m_item(nullptr) {} + + [[nodiscard]] QQuickItem* item() const; + void setItem(QQuickItem* item); + + [[nodiscard]] QUrl cacheDir() const; + void setCacheDir(const QUrl& cacheDir); + + [[nodiscard]] QString path() const; + void setPath(const QString& path); + + [[nodiscard]] QUrl cachePath() const; + + Q_INVOKABLE void updateSource(); + Q_INVOKABLE void updateSource(const QString& path); + +signals: + void itemChanged(); + void cacheDirChanged(); + + void pathChanged(); + void cachePathChanged(); + void usingCacheChanged(); + +private: + QString m_shaPath; + + QQuickItem* m_item; + QUrl m_cacheDir; + + QString m_path; + QUrl m_cachePath; + + QMetaObject::Connection m_widthConn; + QMetaObject::Connection m_heightConn; + + [[nodiscard]] qreal effectiveScale() const; + [[nodiscard]] QSize effectiveSize() const; + + void createCache(const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const; + [[nodiscard]] static QString sha256sum(const QString& path); +}; + +} // namespace caelestia diff --git a/plugin/src/Caelestia/Internal/circularindicatormanager.cpp b/plugin/src/Caelestia/Internal/circularindicatormanager.cpp new file mode 100644 index 0000000..ac0c428 --- /dev/null +++ b/plugin/src/Caelestia/Internal/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/Internal/circularindicatormanager.hpp b/plugin/src/Caelestia/Internal/circularindicatormanager.hpp new file mode 100644 index 0000000..71da93d --- /dev/null +++ b/plugin/src/Caelestia/Internal/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 |