From 3bdffca061f0a2b55728f15f20334b5740858f08 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:54:06 +1100 Subject: [PATCH] feat: c++ visualiser Closes #1126 --- modules/background/Visualiser.qml | 89 ++------ plugin/src/Caelestia/Internal/CMakeLists.txt | 1 + .../src/Caelestia/Internal/visualiserbars.cpp | 198 ++++++++++++++++++ .../src/Caelestia/Internal/visualiserbars.hpp | 72 +++++++ 4 files changed, 285 insertions(+), 75 deletions(-) create mode 100644 plugin/src/Caelestia/Internal/visualiserbars.cpp create mode 100644 plugin/src/Caelestia/Internal/visualiserbars.hpp diff --git a/modules/background/Visualiser.qml b/modules/background/Visualiser.qml index 780abdf4..2f59a300 100644 --- a/modules/background/Visualiser.qml +++ b/modules/background/Visualiser.qml @@ -3,7 +3,7 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Effects import Quickshell -import Quickshell.Widgets +import Caelestia.Internal import Caelestia.Services import qs.components import qs.services @@ -55,25 +55,29 @@ Item { service: Audio.cava } - Item { - id: content + VisualiserBars { + id: bars anchors.fill: parent anchors.margins: Config.border.thickness anchors.leftMargin: Visibilities.bars.get(root.screen).exclusiveZone + Appearance.spacing.small * Config.background.visualiser.spacing - Side { - content: content - } - Side { - content: content - isRight: true - } + values: Audio.cava.values + primaryColor: Qt.alpha(Colours.palette.m3primary, 0.7) + secondaryColor: Qt.alpha(Colours.palette.m3inversePrimary, 0.7) + rounding: Appearance.rounding.small * Config.background.visualiser.rounding + spacing: Appearance.spacing.small * Config.background.visualiser.spacing + animationDuration: Appearance.anim.durations.normal Behavior on anchors.leftMargin { Anim {} } } + + FrameAnimation { + running: root.opacity > 0 && !bars.settled + onTriggered: bars.advance(frameTime) + } } } } @@ -85,69 +89,4 @@ Item { Behavior on opacity { Anim {} } - - component Side: Repeater { - id: side - - required property Item content - property bool isRight - - model: Config.services.visualiserBars - - ClippingRectangle { - id: bar - - required property int modelData - property real value: Math.max(0, Math.min(1, Audio.cava.values[side.isRight ? modelData : side.count - modelData - 1])) - - clip: true - - x: modelData * ((side.content.width * 0.4) / Config.services.visualiserBars) + (side.isRight ? side.content.width * 0.6 : 0) - implicitWidth: (side.content.width * 0.4) / Config.services.visualiserBars - Appearance.spacing.small * Config.background.visualiser.spacing - - y: side.content.height - height - implicitHeight: bar.value * side.content.height * 0.4 - - color: "transparent" - topLeftRadius: Appearance.rounding.small * Config.background.visualiser.rounding - topRightRadius: Appearance.rounding.small * Config.background.visualiser.rounding - - Rectangle { - topLeftRadius: parent.topLeftRadius - topRightRadius: parent.topRightRadius - - gradient: Gradient { - orientation: Gradient.Vertical - - GradientStop { - position: 0 - color: Qt.alpha(Colours.palette.m3primary, 0.7) - - Behavior on color { - CAnim {} - } - } - GradientStop { - position: 1 - color: Qt.alpha(Colours.palette.m3inversePrimary, 0.7) - - Behavior on color { - CAnim {} - } - } - } - - anchors.left: parent.left - anchors.right: parent.right - y: parent.height - height - implicitHeight: side.content.height * 0.4 - } - - Behavior on value { - Anim { - duration: Appearance.anim.durations.small - } - } - } - } } diff --git a/plugin/src/Caelestia/Internal/CMakeLists.txt b/plugin/src/Caelestia/Internal/CMakeLists.txt index 85e85c85..bc4a6948 100644 --- a/plugin/src/Caelestia/Internal/CMakeLists.txt +++ b/plugin/src/Caelestia/Internal/CMakeLists.txt @@ -9,6 +9,7 @@ qml_module(caelestia-internal hyprextras.hpp hyprextras.cpp logindmanager.hpp logindmanager.cpp sparklineitem.hpp sparklineitem.cpp + visualiserbars.hpp visualiserbars.cpp LIBRARIES Qt::Gui Qt::Quick diff --git a/plugin/src/Caelestia/Internal/visualiserbars.cpp b/plugin/src/Caelestia/Internal/visualiserbars.cpp new file mode 100644 index 00000000..926468b0 --- /dev/null +++ b/plugin/src/Caelestia/Internal/visualiserbars.cpp @@ -0,0 +1,198 @@ +#include "visualiserbars.hpp" + +#include +#include +#include +#include +#include +#include + +namespace caelestia::internal { + +VisualiserBars::VisualiserBars(QQuickItem* parent) + : QQuickPaintedItem(parent) { + setAntialiasing(true); +} + +void VisualiserBars::advance(qreal dt) { + if (m_displayValues.isEmpty() || m_settled) + return; + + // dt is in seconds (from FrameAnimation.frameTime), convert to ms + const qreal dtMs = dt * 1000.0; + const qreal tau = m_animationDuration / 3.0; + const qreal alpha = 1.0 - std::exp(-dtMs / tau); + + bool allSettled = true; + + for (qsizetype i = 0; i < m_displayValues.size(); ++i) { + const double diff = m_targetValues[i] - m_displayValues[i]; + + if (std::abs(diff) > 0.001) { + m_displayValues[i] += diff * alpha; + allSettled = false; + } else { + m_displayValues[i] = m_targetValues[i]; + } + } + + update(); + + if (allSettled && !m_settled) { + m_settled = true; + emit settledChanged(); + } +} + +void VisualiserBars::paint(QPainter* painter) { + if (m_displayValues.isEmpty()) + return; + + painter->setRenderHint(QPainter::Antialiasing, true); + painter->setPen(Qt::NoPen); + + const qreal h = height(); + const qreal maxBarHeight = h * 0.4; + + QLinearGradient gradient(0, h - maxBarHeight, 0, h); + gradient.setColorAt(0, m_primaryColor); + gradient.setColorAt(1, m_secondaryColor); + painter->setBrush(gradient); + + drawSide(painter, false); + drawSide(painter, true); +} + +void VisualiserBars::drawSide(QPainter* painter, bool rightSide) { + const qreal w = width(); + const qreal h = height(); + const auto count = m_displayValues.size(); + + if (count == 0) + return; + + const qreal sideWidth = w * 0.4; + const qreal slotWidth = sideWidth / static_cast(count); + const qreal barWidth = slotWidth - m_spacing; + + if (barWidth <= 0) + return; + + const qreal sideOffset = rightSide ? w * 0.6 : 0; + const qreal maxBarHeight = h * 0.4; + + for (qsizetype i = 0; i < count; ++i) { + const qsizetype valueIndex = rightSide ? i : (count - i - 1); + const qreal value = std::clamp(m_displayValues[valueIndex], 0.0, 1.0); + const qreal barHeight = value * maxBarHeight; + + if (barHeight <= 0) + continue; + + const qreal x = static_cast(i) * slotWidth + sideOffset; + const qreal y = h - barHeight; + const qreal r = std::min({ m_rounding, barWidth / 2.0, barHeight }); + + QPainterPath path; + path.moveTo(x, h); + path.lineTo(x, y + r); + + if (r > 0) { + path.arcTo(x, y, r * 2, r * 2, 180, -90); + path.lineTo(x + barWidth - r, y); + path.arcTo(x + barWidth - r * 2, y, r * 2, r * 2, 90, -90); + } else { + path.lineTo(x, y); + path.lineTo(x + barWidth, y); + } + + path.lineTo(x + barWidth, h); + path.closeSubpath(); + + painter->drawPath(path); + } +} + +QVector VisualiserBars::values() const { + return m_targetValues; +} + +void VisualiserBars::setValues(const QVector& values) { + m_targetValues = values; + + if (m_displayValues.size() != values.size()) { + m_displayValues.resize(values.size(), 0.0); + } + + if (m_settled) { + m_settled = false; + emit settledChanged(); + } + + emit valuesChanged(); +} + +bool VisualiserBars::settled() const { + return m_settled; +} + +QColor VisualiserBars::primaryColor() const { + return m_primaryColor; +} + +void VisualiserBars::setPrimaryColor(const QColor& color) { + if (m_primaryColor == color) + return; + m_primaryColor = color; + emit primaryColorChanged(); + update(); +} + +QColor VisualiserBars::secondaryColor() const { + return m_secondaryColor; +} + +void VisualiserBars::setSecondaryColor(const QColor& color) { + if (m_secondaryColor == color) + return; + m_secondaryColor = color; + emit secondaryColorChanged(); + update(); +} + +qreal VisualiserBars::rounding() const { + return m_rounding; +} + +void VisualiserBars::setRounding(qreal rounding) { + if (qFuzzyCompare(m_rounding, rounding)) + return; + m_rounding = rounding; + emit roundingChanged(); + update(); +} + +qreal VisualiserBars::spacing() const { + return m_spacing; +} + +void VisualiserBars::setSpacing(qreal spacing) { + if (qFuzzyCompare(m_spacing, spacing)) + return; + m_spacing = spacing; + emit spacingChanged(); + update(); +} + +int VisualiserBars::animationDuration() const { + return m_animationDuration; +} + +void VisualiserBars::setAnimationDuration(int duration) { + if (m_animationDuration == duration) + return; + m_animationDuration = duration; + emit animationDurationChanged(); +} + +} // namespace caelestia::internal diff --git a/plugin/src/Caelestia/Internal/visualiserbars.hpp b/plugin/src/Caelestia/Internal/visualiserbars.hpp new file mode 100644 index 00000000..95c07124 --- /dev/null +++ b/plugin/src/Caelestia/Internal/visualiserbars.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace caelestia::internal { + +class VisualiserBars : public QQuickPaintedItem { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(QVector values READ values WRITE setValues NOTIFY valuesChanged) + Q_PROPERTY(QColor primaryColor READ primaryColor WRITE setPrimaryColor NOTIFY primaryColorChanged) + Q_PROPERTY(QColor secondaryColor READ secondaryColor WRITE setSecondaryColor NOTIFY secondaryColorChanged) + Q_PROPERTY(qreal rounding READ rounding WRITE setRounding NOTIFY roundingChanged) + Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged) + Q_PROPERTY(int animationDuration READ animationDuration WRITE setAnimationDuration NOTIFY animationDurationChanged) + Q_PROPERTY(bool settled READ settled NOTIFY settledChanged) + +public: + explicit VisualiserBars(QQuickItem* parent = nullptr); + + void paint(QPainter* painter) override; + + Q_INVOKABLE void advance(qreal dt); + + [[nodiscard]] QVector values() const; + void setValues(const QVector& values); + + [[nodiscard]] QColor primaryColor() const; + void setPrimaryColor(const QColor& color); + + [[nodiscard]] QColor secondaryColor() const; + void setSecondaryColor(const QColor& color); + + [[nodiscard]] qreal rounding() const; + void setRounding(qreal rounding); + + [[nodiscard]] qreal spacing() const; + void setSpacing(qreal spacing); + + [[nodiscard]] int animationDuration() const; + void setAnimationDuration(int duration); + + [[nodiscard]] bool settled() const; + +signals: + void valuesChanged(); + void primaryColorChanged(); + void secondaryColorChanged(); + void roundingChanged(); + void spacingChanged(); + void animationDurationChanged(); + void settledChanged(); + +private: + void drawSide(QPainter* painter, bool rightSide); + + QVector m_targetValues; + QVector m_displayValues; + QColor m_primaryColor; + QColor m_secondaryColor; + qreal m_rounding = 0.0; + qreal m_spacing = 0.0; + int m_animationDuration = 200; + bool m_settled = true; +}; + +} // namespace caelestia::internal