feat: c++ visualiser

Closes #1126
This commit is contained in:
2 * r + 2 * t 2026-03-30 01:54:06 +11:00
parent 1f3656c2f6
commit 3bdffca061
4 changed files with 285 additions and 75 deletions

View file

@ -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
}
}
}
}
}

View file

@ -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

View file

@ -0,0 +1,198 @@
#include "visualiserbars.hpp"
#include <algorithm>
#include <cmath>
#include <qbrush.h>
#include <qpainter.h>
#include <qpainterpath.h>
#include <qpen.h>
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<qreal>(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<qreal>(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<double> VisualiserBars::values() const {
return m_targetValues;
}
void VisualiserBars::setValues(const QVector<double>& 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

View file

@ -0,0 +1,72 @@
#pragma once
#include <qcolor.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qquickpainteditem.h>
#include <qvector.h>
namespace caelestia::internal {
class VisualiserBars : public QQuickPaintedItem {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QVector<double> 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<double> values() const;
void setValues(const QVector<double>& 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<double> m_targetValues;
QVector<double> 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