From f15b902c0edbe140402e00de57dabecf9d2d31be Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:04:39 +1100 Subject: [PATCH 01/45] feat: add blobs module --- plugin/src/Caelestia/Blobs/CMakeLists.txt | 19 + plugin/src/Caelestia/Blobs/blobgroup.cpp | 75 ++++ plugin/src/Caelestia/Blobs/blobgroup.hpp | 53 +++ .../src/Caelestia/Blobs/blobinvertedrect.cpp | 56 +++ .../src/Caelestia/Blobs/blobinvertedrect.hpp | 56 +++ plugin/src/Caelestia/Blobs/blobmaterial.cpp | 98 +++++ plugin/src/Caelestia/Blobs/blobmaterial.hpp | 47 +++ plugin/src/Caelestia/Blobs/blobrect.cpp | 132 +++++++ plugin/src/Caelestia/Blobs/blobrect.hpp | 81 ++++ plugin/src/Caelestia/Blobs/blobshape.cpp | 365 ++++++++++++++++++ plugin/src/Caelestia/Blobs/blobshape.hpp | 71 ++++ plugin/src/Caelestia/Blobs/shaders/blob.frag | 277 +++++++++++++ plugin/src/Caelestia/Blobs/shaders/blob.vert | 29 ++ plugin/src/Caelestia/CMakeLists.txt | 3 +- 14 files changed, 1361 insertions(+), 1 deletion(-) create mode 100644 plugin/src/Caelestia/Blobs/CMakeLists.txt create mode 100644 plugin/src/Caelestia/Blobs/blobgroup.cpp create mode 100644 plugin/src/Caelestia/Blobs/blobgroup.hpp create mode 100644 plugin/src/Caelestia/Blobs/blobinvertedrect.cpp create mode 100644 plugin/src/Caelestia/Blobs/blobinvertedrect.hpp create mode 100644 plugin/src/Caelestia/Blobs/blobmaterial.cpp create mode 100644 plugin/src/Caelestia/Blobs/blobmaterial.hpp create mode 100644 plugin/src/Caelestia/Blobs/blobrect.cpp create mode 100644 plugin/src/Caelestia/Blobs/blobrect.hpp create mode 100644 plugin/src/Caelestia/Blobs/blobshape.cpp create mode 100644 plugin/src/Caelestia/Blobs/blobshape.hpp create mode 100644 plugin/src/Caelestia/Blobs/shaders/blob.frag create mode 100644 plugin/src/Caelestia/Blobs/shaders/blob.vert diff --git a/plugin/src/Caelestia/Blobs/CMakeLists.txt b/plugin/src/Caelestia/Blobs/CMakeLists.txt new file mode 100644 index 00000000..f44be008 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/CMakeLists.txt @@ -0,0 +1,19 @@ +qml_module(caelestia-blobs + URI Caelestia.Blobs + SOURCES + blobgroup.cpp + blobshape.cpp + blobrect.cpp + blobinvertedrect.cpp + blobmaterial.cpp + LIBRARIES + Qt::Quick +) + +qt_add_shaders(caelestia-blobs "blob_shaders" + BATCHABLE OPTIMIZED NOHLSL NOMSL + PREFIX "/" + FILES + shaders/blob.frag + shaders/blob.vert +) diff --git a/plugin/src/Caelestia/Blobs/blobgroup.cpp b/plugin/src/Caelestia/Blobs/blobgroup.cpp new file mode 100644 index 00000000..f06c4d51 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobgroup.cpp @@ -0,0 +1,75 @@ +#include "blobgroup.hpp" +#include "blobinvertedrect.hpp" +#include "blobshape.hpp" + +BlobGroup::BlobGroup(QObject* parent) + : QObject(parent) {} + +BlobGroup::~BlobGroup() { + for (auto* shape : std::as_const(m_shapes)) + shape->m_group = nullptr; + if (m_invertedRect) + static_cast(m_invertedRect)->m_group = nullptr; +} + +void BlobGroup::setSmoothing(qreal s) { + if (qFuzzyCompare(m_smoothing, s)) + return; + m_smoothing = s; + emit smoothingChanged(); + markDirty(); +} + +void BlobGroup::setColor(const QColor& c) { + if (m_color == c) + return; + m_color = c; + emit colorChanged(); + markDirty(); +} + +void BlobGroup::addShape(BlobShape* shape) { + if (!shape || m_shapes.contains(shape)) + return; + m_shapes.append(shape); + markDirty(); +} + +void BlobGroup::removeShape(BlobShape* shape) { + m_shapes.removeOne(shape); + markDirty(); +} + +void BlobGroup::setInvertedRect(BlobInvertedRect* rect) { + if (m_invertedRect == rect) + return; + m_invertedRect = rect; + markDirty(); +} + +void BlobGroup::clearInvertedRect(BlobInvertedRect* rect) { + if (m_invertedRect != rect) + return; + m_invertedRect = nullptr; + markDirty(); +} + +void BlobGroup::markDirty() { + m_physicsUpdated = false; + for (auto* shape : std::as_const(m_shapes)) { + shape->polish(); + shape->update(); + } + if (m_invertedRect) { + static_cast(m_invertedRect)->polish(); + static_cast(m_invertedRect)->update(); + } +} + +void BlobGroup::ensurePhysicsUpdated() { + if (m_physicsUpdated) + return; + m_physicsUpdated = true; + for (auto* shape : std::as_const(m_shapes)) + shape->updatePhysics(); +} diff --git a/plugin/src/Caelestia/Blobs/blobgroup.hpp b/plugin/src/Caelestia/Blobs/blobgroup.hpp new file mode 100644 index 00000000..4c9ed691 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobgroup.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include + +class BlobShape; +class BlobInvertedRect; + +class BlobGroup : public QObject { + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(qreal smoothing READ smoothing WRITE setSmoothing NOTIFY + smoothingChanged) + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) + +public: + explicit BlobGroup(QObject* parent = nullptr); + ~BlobGroup() override; + + qreal smoothing() const { return m_smoothing; } + + void setSmoothing(qreal s); + + QColor color() const { return m_color; } + + void setColor(const QColor& c); + + void addShape(BlobShape* shape); + void removeShape(BlobShape* shape); + + void setInvertedRect(BlobInvertedRect* rect); + void clearInvertedRect(BlobInvertedRect* rect); + + const QList& shapes() const { return m_shapes; } + + BlobInvertedRect* invertedRect() const { return m_invertedRect; } + + void markDirty(); + void ensurePhysicsUpdated(); + +signals: + void smoothingChanged(); + void colorChanged(); + +private: + qreal m_smoothing = 32.0; + QColor m_color{ 0x44, 0x88, 0xff }; + QList m_shapes; + BlobInvertedRect* m_invertedRect = nullptr; + bool m_physicsUpdated = false; +}; diff --git a/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp b/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp new file mode 100644 index 00000000..68024738 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp @@ -0,0 +1,56 @@ +#include "blobinvertedrect.hpp" +#include "blobgroup.hpp" + +BlobInvertedRect::BlobInvertedRect(QQuickItem* parent) + : BlobShape(parent) {} + +BlobInvertedRect::~BlobInvertedRect() { + if (m_group) + m_group->clearInvertedRect(this); +} + +void BlobInvertedRect::setBorderLeft(qreal v) { + if (qFuzzyCompare(m_borderLeft, v)) + return; + m_borderLeft = v; + emit borderLeftChanged(); + if (m_group) + m_group->markDirty(); +} + +void BlobInvertedRect::setBorderRight(qreal v) { + if (qFuzzyCompare(m_borderRight, v)) + return; + m_borderRight = v; + emit borderRightChanged(); + if (m_group) + m_group->markDirty(); +} + +void BlobInvertedRect::setBorderTop(qreal v) { + if (qFuzzyCompare(m_borderTop, v)) + return; + m_borderTop = v; + emit borderTopChanged(); + if (m_group) + m_group->markDirty(); +} + +void BlobInvertedRect::setBorderBottom(qreal v) { + if (qFuzzyCompare(m_borderBottom, v)) + return; + m_borderBottom = v; + emit borderBottomChanged(); + if (m_group) + m_group->markDirty(); +} + +void BlobInvertedRect::registerWithGroup() { + if (m_group) + m_group->setInvertedRect(this); +} + +void BlobInvertedRect::unregisterFromGroup() { + if (m_group) + m_group->clearInvertedRect(this); +} diff --git a/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp b/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp new file mode 100644 index 00000000..958e2a8d --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include "blobshape.hpp" + +#include + +class BlobInvertedRect : public BlobShape { + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(qreal borderLeft READ borderLeft WRITE setBorderLeft NOTIFY + borderLeftChanged) + Q_PROPERTY(qreal borderRight READ borderRight WRITE setBorderRight NOTIFY + borderRightChanged) + Q_PROPERTY(qreal borderTop READ borderTop WRITE setBorderTop NOTIFY + borderTopChanged) + Q_PROPERTY(qreal borderBottom READ borderBottom WRITE setBorderBottom NOTIFY + borderBottomChanged) + +public: + explicit BlobInvertedRect(QQuickItem* parent = nullptr); + ~BlobInvertedRect() override; + + qreal borderLeft() const { return m_borderLeft; } + + void setBorderLeft(qreal v); + + qreal borderRight() const { return m_borderRight; } + + void setBorderRight(qreal v); + + qreal borderTop() const { return m_borderTop; } + + void setBorderTop(qreal v); + + qreal borderBottom() const { return m_borderBottom; } + + void setBorderBottom(qreal v); + +signals: + void borderLeftChanged(); + void borderRightChanged(); + void borderTopChanged(); + void borderBottomChanged(); + +protected: + bool isInvertedRect() const override { return true; } + + void registerWithGroup() override; + void unregisterFromGroup() override; + +private: + qreal m_borderLeft = 0; + qreal m_borderRight = 0; + qreal m_borderTop = 0; + qreal m_borderBottom = 0; +}; diff --git a/plugin/src/Caelestia/Blobs/blobmaterial.cpp b/plugin/src/Caelestia/Blobs/blobmaterial.cpp new file mode 100644 index 00000000..f2ead2ed --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobmaterial.cpp @@ -0,0 +1,98 @@ +#include "blobmaterial.hpp" + +#include + +QSGMaterialType* BlobMaterial::type() const { + static QSGMaterialType s_type; + return &s_type; +} + +QSGMaterialShader* BlobMaterial::createShader( + QSGRendererInterface::RenderMode) const { + return new BlobMaterialShader; +} + +int BlobMaterial::compare(const QSGMaterial* other) const { + if (this < other) + return -1; + if (this > other) + return 1; + return 0; +} + +BlobMaterialShader::BlobMaterialShader() { + setShaderFileName(VertexStage, QStringLiteral(":/shaders/blob.vert.qsb")); + setShaderFileName(FragmentStage, QStringLiteral(":/shaders/blob.frag.qsb")); +} + +bool BlobMaterialShader::updateUniformData( + RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) { + Q_UNUSED(oldMaterial); + auto* mat = static_cast(newMaterial); + QByteArray* buf = state.uniformData(); + Q_ASSERT(buf->size() >= 1440); + + if (state.isMatrixDirty()) { + const QMatrix4x4 m = state.combinedMatrix(); + memcpy(buf->data(), m.constData(), 64); + } + if (state.isOpacityDirty()) { + const float opacity = state.opacity(); + memcpy(buf->data() + 64, &opacity, 4); + } + + // Padded rect (offset 68) + memcpy(buf->data() + 68, &mat->m_paddedX, 4); + memcpy(buf->data() + 72, &mat->m_paddedY, 4); + memcpy(buf->data() + 76, &mat->m_paddedW, 4); + memcpy(buf->data() + 80, &mat->m_paddedH, 4); + + // Smooth factor (offset 84) + memcpy(buf->data() + 84, &mat->m_smoothFactor, 4); + + // Rect count (offset 88) + memcpy(buf->data() + 88, &mat->m_rectCount, 4); + + // My index (offset 92) + memcpy(buf->data() + 92, &mat->m_myIndex, 4); + + // Color as vec4 (offset 96, 16 bytes) + const float color[4] = { + static_cast(mat->m_color.redF()), + static_cast(mat->m_color.greenF()), + static_cast(mat->m_color.blueF()), + static_cast(mat->m_color.alphaF()), + }; + memcpy(buf->data() + 96, color, 16); + + // Has inverted (offset 112) + memcpy(buf->data() + 112, &mat->m_hasInverted, 4); + + // Inverted radius (offset 116) + memcpy(buf->data() + 116, &mat->m_invertedRadius, 4); + + // Padding at 120-127 (skip) + + // Inverted outer (offset 128, 16 bytes) + memcpy(buf->data() + 128, mat->m_invertedOuter, 16); + + // Inverted inner (offset 144, 16 bytes) + memcpy(buf->data() + 144, mat->m_invertedInner, 16); + + // Rect data (offset 160, each rect = 5 vec4s = 80 bytes) + const int count = qMin(mat->m_rectCount, 16); + for (int i = 0; i < count; ++i) { + const auto& r = mat->m_rects[i]; + const int base = 160 + i * 80; + const float d0[4] = { r.cx, r.cy, r.hw, r.hh }; + const float d1[4] = { r.radius, r.offsetX, r.offsetY, r.minEig }; + const float d3[4] = { r.screenHalfX, r.screenHalfY, 0.0f, 0.0f }; + memcpy(buf->data() + base, d0, 16); + memcpy(buf->data() + base + 16, d1, 16); + memcpy(buf->data() + base + 32, r.invDeform, 16); + memcpy(buf->data() + base + 48, d3, 16); + memcpy(buf->data() + base + 64, r.cornerFill, 16); + } + + return true; +} diff --git a/plugin/src/Caelestia/Blobs/blobmaterial.hpp b/plugin/src/Caelestia/Blobs/blobmaterial.hpp new file mode 100644 index 00000000..ef80fea1 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobmaterial.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include + +struct BlobRectData { + float cx = 0, cy = 0, hw = 0, hh = 0; + float radius = 0; + float offsetX = 0, offsetY = 0; + float minEig = 1.0f; + // Inverse of 2x2 deformation matrix, column-major for GLSL + float invDeform[4] = { 1, 0, 0, 1 }; + // Screen-space AABB half-extents of the deformed rect + float screenHalfX = 0, screenHalfY = 0; + // Pre-computed corner fill factors (tr, br, bl, tl) + float cornerFill[4] = { 1, 1, 1, 1 }; +}; + +class BlobMaterial : public QSGMaterial { +public: + QSGMaterialType* type() const override; + QSGMaterialShader* createShader( + QSGRendererInterface::RenderMode) const override; + int compare(const QSGMaterial* other) const override; + + float m_paddedX = 0; + float m_paddedY = 0; + float m_paddedW = 0; + float m_paddedH = 0; + float m_smoothFactor = 32.0f; + int m_rectCount = 0; + int m_myIndex = -2; + QColor m_color{ 0x44, 0x88, 0xff }; + int m_hasInverted = 0; + float m_invertedRadius = 0; + float m_invertedOuter[4] = {}; + float m_invertedInner[4] = {}; + BlobRectData m_rects[16] = {}; +}; + +class BlobMaterialShader : public QSGMaterialShader { +public: + BlobMaterialShader(); + bool updateUniformData(RenderState& state, QSGMaterial* newMaterial, + QSGMaterial* oldMaterial) override; +}; diff --git a/plugin/src/Caelestia/Blobs/blobrect.cpp b/plugin/src/Caelestia/Blobs/blobrect.cpp new file mode 100644 index 00000000..e03baa22 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobrect.cpp @@ -0,0 +1,132 @@ +#include "blobrect.hpp" +#include "blobgroup.hpp" + +#include +#include + +BlobRect::BlobRect(QQuickItem* parent) + : BlobShape(parent) {} + +BlobRect::~BlobRect() { + if (m_group) + m_group->removeShape(this); +} + +void BlobRect::updatePolish() { + BlobShape::updatePolish(); + + if (m_physicsActive) { + QMetaObject::invokeMethod( + this, + [this]() { + if (m_physicsActive && m_group) + m_group->markDirty(); + }, + Qt::QueuedConnection); + } +} + +void BlobRect::updatePhysics() { + const QPointF scenePos = mapToScene(QPointF(width() / 2.0, height() / 2.0)); + + if (!m_hasPrevPos) { + m_prevScenePos = scenePos; + m_elapsed.start(); + m_hasPrevPos = true; + return; + } + + const float dt = static_cast(m_elapsed.restart()) / 1000.0f; + if (dt > 0.1f || dt < 0.001f) { + m_prevScenePos = scenePos; + // Still check atRest on skipped frames to avoid getting stuck + if (m_physicsActive) + checkAtRest(0.0f); + return; + } + + const float velX = + static_cast(scenePos.x() - m_prevScenePos.x()) / dt; + const float velY = + static_cast(scenePos.y() - m_prevScenePos.y()) / dt; + m_prevScenePos = scenePos; + + const float speed = std::sqrt(velX * velX + velY * velY); + + if (!m_physicsActive) { + if (speed < 5.0f) + return; + m_physicsActive = true; + } + + // Compute target deformation matrix from velocity + // R(θ) * diag(stretch, compress) * R(θ)^T + const float kStretchFactor = static_cast(m_deformScale); + constexpr float kMaxStretch = 0.35f; + + float target00 = 1.0f; + float target01 = 0.0f; + float target11 = 1.0f; + + if (speed > 5.0f) { + const float targetStretch = + 1.0f + std::min(speed * kStretchFactor, kMaxStretch); + const float targetCompress = 1.0f / targetStretch; + + const float cosA = velX / speed; + const float sinA = velY / speed; + const float cos2 = cosA * cosA; + const float sin2 = sinA * sinA; + const float cs = cosA * sinA; + + target00 = targetStretch * cos2 + targetCompress * sin2; + target01 = (targetStretch - targetCompress) * cs; + target11 = targetStretch * sin2 + targetCompress * cos2; + } + + // Underdamped spring on each matrix component + const float kStiffness = static_cast(m_stiffness); + const float kDamping = static_cast(m_damping); + + const float accel00 = + -kStiffness * (m_dm00 - target00) - kDamping * m_dmVel00; + m_dmVel00 += accel00 * dt; + m_dm00 += m_dmVel00 * dt; + + const float accel01 = + -kStiffness * (m_dm01 - target01) - kDamping * m_dmVel01; + m_dmVel01 += accel01 * dt; + m_dm01 += m_dmVel01 * dt; + + const float accel11 = + -kStiffness * (m_dm11 - target11) - kDamping * m_dmVel11; + m_dmVel11 += accel11 * dt; + m_dm11 += m_dmVel11 * dt; + + m_deformMatrix = QMatrix4x4( + m_dm00, m_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); + updateCenteredDeformMatrix(); + + checkAtRest(speed); +} + +void BlobRect::checkAtRest(float speed) { + constexpr float kEpsilon = 0.002f; + const bool atRest = + std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon && + std::abs(m_dm11 - 1.0f) < kEpsilon && std::abs(m_dmVel00) < kEpsilon && + std::abs(m_dmVel01) < kEpsilon && std::abs(m_dmVel11) < kEpsilon && + speed < 5.0f; + + if (atRest) { + m_dm00 = 1.0f; + m_dm01 = 0.0f; + m_dm11 = 1.0f; + m_dmVel00 = 0.0f; + m_dmVel01 = 0.0f; + m_dmVel11 = 0.0f; + m_deformMatrix = QMatrix4x4(); // identity + updateCenteredDeformMatrix(); + m_physicsActive = false; + } +} diff --git a/plugin/src/Caelestia/Blobs/blobrect.hpp b/plugin/src/Caelestia/Blobs/blobrect.hpp new file mode 100644 index 00000000..86d4666d --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobrect.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include "blobshape.hpp" + +#include +#include + +class BlobRect : public BlobShape { + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(qreal stiffness READ stiffness WRITE setStiffness NOTIFY + stiffnessChanged) + Q_PROPERTY( + qreal damping READ damping WRITE setDamping NOTIFY dampingChanged) + Q_PROPERTY(qreal deformScale READ deformScale WRITE setDeformScale NOTIFY + deformScaleChanged) + +public: + explicit BlobRect(QQuickItem* parent = nullptr); + ~BlobRect() override; + + qreal stiffness() const { return m_stiffness; } + + void setStiffness(qreal s) { + if (!qFuzzyCompare(m_stiffness, s)) { + m_stiffness = s; + emit stiffnessChanged(); + } + } + + qreal damping() const { return m_damping; } + + void setDamping(qreal d) { + if (!qFuzzyCompare(m_damping, d)) { + m_damping = d; + emit dampingChanged(); + } + } + + qreal deformScale() const { return m_deformScale; } + + void setDeformScale(qreal s) { + if (!qFuzzyCompare(m_deformScale, s)) { + m_deformScale = s; + emit deformScaleChanged(); + } + } + +signals: + void stiffnessChanged(); + void dampingChanged(); + void deformScaleChanged(); + +protected: + void updatePolish() override; + void updatePhysics() override; + +private: + void checkAtRest(float speed); + + // Physics state + QPointF m_prevScenePos; + QElapsedTimer m_elapsed; + bool m_physicsActive = false; + bool m_hasPrevPos = false; + + // Symmetric 2x2 deformation matrix components (3 independent: m00, m01, + // m11) Rest state is identity: m00=1, m01=0, m11=1 + float m_dm00 = 1.0f; + float m_dm01 = 0.0f; + float m_dm11 = 1.0f; + + // Spring velocities for each component + float m_dmVel00 = 0.0f; + float m_dmVel01 = 0.0f; + float m_dmVel11 = 0.0f; + + qreal m_stiffness = 200.0; + qreal m_damping = 16.0; + qreal m_deformScale = 0.0005; +}; diff --git a/plugin/src/Caelestia/Blobs/blobshape.cpp b/plugin/src/Caelestia/Blobs/blobshape.cpp new file mode 100644 index 00000000..73b5cfe6 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobshape.cpp @@ -0,0 +1,365 @@ +#include "blobshape.hpp" +#include "blobgroup.hpp" +#include "blobinvertedrect.hpp" + +#include +#include + +#include +#include + +static float deformPadding(const QMatrix4x4& dm, float hw, float hh) { + // Bounding box of the deformed shape: |M * corners| + const float dm00 = dm(0, 0), dm01 = dm(0, 1); + const float dm10 = dm(1, 0), dm11 = dm(1, 1); + const float boundX = std::abs(dm00) * hw + std::abs(dm01) * hh; + const float boundY = std::abs(dm10) * hw + std::abs(dm11) * hh; + const float extraX = std::max(boundX - hw, 0.0f) + std::abs(dm(0, 3)); + const float extraY = std::max(boundY - hh, 0.0f) + std::abs(dm(1, 3)); + return std::max(extraX, extraY); +} + +static float cpuSdBox(float px, float py, float cx, float cy, float hw, + float hh) { + const float dx = std::abs(px - cx) - hw; + const float dy = std::abs(py - cy) - hh; + const float mdx = std::max(dx, 0.0f); + const float mdy = std::max(dy, 0.0f); + return std::sqrt(mdx * mdx + mdy * mdy) + std::min(std::max(dx, dy), 0.0f); +} + +static float cpuSmoothstep(float edge0, float edge1, float x) { + const float t = std::clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); + return t * t * (3.0f - 2.0f * t); +} + +BlobShape::BlobShape(QQuickItem* parent) + : QQuickItem(parent) { + setFlag(ItemHasContents); +} + +void BlobShape::setGroup(BlobGroup* g) { + if (m_group == g) + return; + if (m_group && isComponentComplete()) + unregisterFromGroup(); + m_group = g; + if (m_group && isComponentComplete()) + registerWithGroup(); + emit groupChanged(); + if (m_group) + m_group->markDirty(); +} + +void BlobShape::setRadius(qreal r) { + if (qFuzzyCompare(m_radius, r)) + return; + m_radius = r; + emit radiusChanged(); + if (m_group) + m_group->markDirty(); +} + +void BlobShape::componentComplete() { + QQuickItem::componentComplete(); + if (m_group) + registerWithGroup(); +} + +void BlobShape::geometryChange( + const QRectF& newGeometry, const QRectF& oldGeometry) { + QQuickItem::geometryChange(newGeometry, oldGeometry); + updateCenteredDeformMatrix(); + if (m_group) + m_group->markDirty(); +} + +void BlobShape::updateCenteredDeformMatrix() { + const auto cx = static_cast(width()) * 0.5f; + const auto cy = static_cast(height()) * 0.5f; + QMatrix4x4 result; + result.translate(cx, cy); + result *= m_deformMatrix; + result.translate(-cx, -cy); + m_centeredDeformMatrix = result; + emit deformMatrixChanged(); +} + +void BlobShape::registerWithGroup() { + if (m_group) + m_group->addShape(this); +} + +void BlobShape::unregisterFromGroup() { + if (m_group) + m_group->removeShape(this); +} + +void BlobShape::updatePolish() { + if (!m_group) + return; + + // Ensure all shapes have up-to-date physics (only once per frame) + m_group->ensurePhysicsUpdated(); + + // When inverted rect renders everything, skip spatial query for others + if (!isInvertedRect() && m_group->invertedRect()) + return; + + const QPointF scenePos = mapToScene(QPointF(0, 0)); + const float pad = static_cast(m_group->smoothing()); + + if (isInvertedRect()) { + m_cachedPaddedX = static_cast(scenePos.x()); + m_cachedPaddedY = static_cast(scenePos.y()); + m_cachedPaddedW = static_cast(width()); + m_cachedPaddedH = static_cast(height()); + m_localPaddedRect = QRectF(0, 0, width(), height()); + } else { + const float hw = static_cast(width()) * 0.5f; + const float hh = static_cast(height()) * 0.5f; + const float totalPad = pad + deformPadding(m_deformMatrix, hw, hh); + + m_cachedPaddedX = static_cast(scenePos.x()) - totalPad; + m_cachedPaddedY = static_cast(scenePos.y()) - totalPad; + m_cachedPaddedW = static_cast(width()) + 2.0f * totalPad; + m_cachedPaddedH = static_cast(height()) + 2.0f * totalPad; + m_localPaddedRect = QRectF(static_cast(-totalPad), + static_cast(-totalPad), + width() + 2.0 * static_cast(totalPad), + height() + 2.0 * static_cast(totalPad)); + } + + // Filter nearby normal rects + m_cachedRects.clear(); + m_cachedMyIndex = -2; + const QRectF myPadded(static_cast(m_cachedPaddedX), + static_cast(m_cachedPaddedY), + static_cast(m_cachedPaddedW), + static_cast(m_cachedPaddedH)); + + for (BlobShape* other : m_group->shapes()) { + if (other->isInvertedRect()) + continue; + + const QPointF otherScene = other->mapToScene(QPointF(0, 0)); + + bool include = false; + if (isInvertedRect()) { + include = true; + } else { + const float otherHW = static_cast(other->width()) * 0.5f; + const float otherHH = static_cast(other->height()) * 0.5f; + const float otherPad = + pad + deformPadding(other->m_deformMatrix, otherHW, otherHH); + const QRectF otherPadded( + otherScene.x() - static_cast(otherPad), + otherScene.y() - static_cast(otherPad), + other->width() + 2.0 * static_cast(otherPad), + other->height() + 2.0 * static_cast(otherPad)); + include = myPadded.intersects(otherPadded); + } + + if (include) { + if (other == this) + m_cachedMyIndex = static_cast(m_cachedRects.size()); + + const QMatrix4x4& dm = other->m_deformMatrix; + const float a = dm(0, 0), b = dm(1, 0); + const float c = dm(0, 1), d = dm(1, 1); + + BlobRectData r; + r.cx = static_cast(otherScene.x() + other->width() / 2.0); + r.cy = static_cast(otherScene.y() + other->height() / 2.0); + r.hw = static_cast(other->width() / 2.0); + r.hh = static_cast(other->height() / 2.0); + r.radius = static_cast(other->radius()); + r.offsetX = dm(0, 3); + r.offsetY = dm(1, 3); + + // Pre-compute inverse deformation matrix + const float det = a * d - c * b; + const float invDet = + std::abs(det) > 1e-6f ? 1.0f / det : 1.0f; + r.invDeform[0] = d * invDet; + r.invDeform[1] = -b * invDet; + r.invDeform[2] = -c * invDet; + r.invDeform[3] = a * invDet; + + // Pre-compute minimum eigenvalue (avoids per-pixel sqrt) + const float halfTr = 0.5f * (a + d); + const float halfDiff = 0.5f * (a - d); + r.minEig = halfTr - std::sqrt(halfDiff * halfDiff + c * c); + + // Pre-compute screen-space AABB half-extents + r.screenHalfX = std::abs(a) * r.hw + std::abs(c) * r.hh; + r.screenHalfY = std::abs(b) * r.hw + std::abs(d) * r.hh; + + m_cachedRects.append(r); + } + } + + if (isInvertedRect()) + m_cachedMyIndex = -1; + + // Cache inverted rect data + m_cachedHasInverted = false; + m_cachedInvertedRadius = 0; + memset(m_cachedInvertedOuter, 0, sizeof(m_cachedInvertedOuter)); + memset(m_cachedInvertedInner, 0, sizeof(m_cachedInvertedInner)); + + auto* inv = m_group->invertedRect(); + if (inv) { + m_cachedHasInverted = true; + m_cachedInvertedRadius = static_cast(inv->radius()); + + const QPointF invScene = inv->mapToScene(QPointF(0, 0)); + const float outerCX = + static_cast(invScene.x() + inv->width() / 2.0); + const float outerCY = + static_cast(invScene.y() + inv->height() / 2.0); + const float outerHW = static_cast(inv->width() / 2.0); + const float outerHH = static_cast(inv->height() / 2.0); + + const float innerCX = + outerCX + + static_cast((inv->borderLeft() - inv->borderRight()) / 2.0); + const float innerCY = + outerCY + + static_cast((inv->borderTop() - inv->borderBottom()) / 2.0); + const float innerHW = + outerHW - + static_cast((inv->borderLeft() + inv->borderRight()) / 2.0); + const float innerHH = + outerHH - + static_cast((inv->borderTop() + inv->borderBottom()) / 2.0); + + m_cachedInvertedOuter[0] = outerCX; + m_cachedInvertedOuter[1] = outerCY; + m_cachedInvertedOuter[2] = outerHW; + m_cachedInvertedOuter[3] = outerHH; + + m_cachedInvertedInner[0] = innerCX; + m_cachedInvertedInner[1] = innerCY; + m_cachedInvertedInner[2] = innerHW; + m_cachedInvertedInner[3] = innerHH; + } + + // Pre-compute corner fill factors (moves O(N²) work from GPU to CPU) + const float smoothFactor = pad; + const auto rectCount = m_cachedRects.size(); + for (qsizetype i = 0; i < rectCount; ++i) { + auto& ri = m_cachedRects[i]; + float fTr = 1.0f, fBr = 1.0f, fBl = 1.0f, fTl = 1.0f; + + const float cTrX = ri.cx + ri.hw, cTrY = ri.cy - ri.hh; + const float cBrX = ri.cx + ri.hw, cBrY = ri.cy + ri.hh; + const float cBlX = ri.cx - ri.hw, cBlY = ri.cy + ri.hh; + const float cTlX = ri.cx - ri.hw, cTlY = ri.cy - ri.hh; + + for (qsizetype j = 0; j < rectCount; ++j) { + if (j == i) + continue; + const auto& rj = m_cachedRects[j]; + fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, + cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh))); + fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, + cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh))); + fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, + cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh))); + fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, + cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh))); + } + + if (m_cachedHasInverted) { + const float icx = m_cachedInvertedInner[0]; + const float icy = m_cachedInvertedInner[1]; + const float ihw = m_cachedInvertedInner[2]; + const float ihh = m_cachedInvertedInner[3]; + fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, + -cpuSdBox(cTrX, cTrY, icx, icy, ihw, ihh))); + fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, + -cpuSdBox(cBrX, cBrY, icx, icy, ihw, ihh))); + fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, + -cpuSdBox(cBlX, cBlY, icx, icy, ihw, ihh))); + fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, + -cpuSdBox(cTlX, cTlY, icx, icy, ihw, ihh))); + } + + ri.cornerFill[0] = fTr; + ri.cornerFill[1] = fBr; + ri.cornerFill[2] = fBl; + ri.cornerFill[3] = fTl; + } +} + +QSGNode* BlobShape::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { + if (!m_group) { + delete oldNode; + return nullptr; + } + + // When an inverted rect exists, it renders everything in a single pass + if (!isInvertedRect() && m_group->invertedRect()) { + delete oldNode; + return nullptr; + } + + auto* node = static_cast(oldNode); + if (!node) { + node = new QSGGeometryNode; + + auto* geometry = new QSGGeometry( + QSGGeometry::defaultAttributes_TexturedPoint2D(), 4); + geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip); + node->setGeometry(geometry); + node->setFlag(QSGNode::OwnsGeometry); + + auto* material = new BlobMaterial; + material->setFlag(QSGMaterial::Blending); + node->setMaterial(material); + node->setFlag(QSGNode::OwnsMaterial); + } + + // Update geometry + auto* geometry = node->geometry(); + auto* v = geometry->vertexDataAsTexturedPoint2D(); + + const float x0 = static_cast(m_localPaddedRect.x()); + const float y0 = static_cast(m_localPaddedRect.y()); + const float x1 = x0 + static_cast(m_localPaddedRect.width()); + const float y1 = y0 + static_cast(m_localPaddedRect.height()); + + v[0].set(x0, y0, 0.0f, 0.0f); + v[1].set(x1, y0, 1.0f, 0.0f); + v[2].set(x0, y1, 0.0f, 1.0f); + v[3].set(x1, y1, 1.0f, 1.0f); + + node->markDirty(QSGNode::DirtyGeometry); + + // Update material + auto* material = static_cast(node->material()); + material->m_paddedX = m_cachedPaddedX; + material->m_paddedY = m_cachedPaddedY; + material->m_paddedW = m_cachedPaddedW; + material->m_paddedH = m_cachedPaddedH; + material->m_smoothFactor = static_cast(m_group->smoothing()); + material->m_myIndex = m_cachedMyIndex; + material->m_color = m_group->color(); + material->m_hasInverted = m_cachedHasInverted ? 1 : 0; + material->m_invertedRadius = m_cachedInvertedRadius; + memcpy(material->m_invertedOuter, m_cachedInvertedOuter, + sizeof(m_cachedInvertedOuter)); + memcpy(material->m_invertedInner, m_cachedInvertedInner, + sizeof(m_cachedInvertedInner)); + + const int count = + static_cast(qMin(m_cachedRects.size(), qsizetype(16))); + material->m_rectCount = count; + for (int i = 0; i < count; ++i) + material->m_rects[i] = m_cachedRects[i]; + + node->markDirty(QSGNode::DirtyMaterial); + + return node; +} diff --git a/plugin/src/Caelestia/Blobs/blobshape.hpp b/plugin/src/Caelestia/Blobs/blobshape.hpp new file mode 100644 index 00000000..8c6f1165 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobshape.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include "blobmaterial.hpp" + +#include +#include +#include + +class BlobGroup; + +class BlobShape : public QQuickItem { + Q_OBJECT + Q_PROPERTY(BlobGroup* group READ group WRITE setGroup NOTIFY groupChanged) + Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged) + Q_PROPERTY( + QMatrix4x4 deformMatrix READ deformMatrix NOTIFY deformMatrixChanged) + + friend class BlobGroup; + +public: + explicit BlobShape(QQuickItem* parent = nullptr); + ~BlobShape() override = default; + + BlobGroup* group() const { return m_group; } + + void setGroup(BlobGroup* g); + + qreal radius() const { return m_radius; } + + void setRadius(qreal r); + + QMatrix4x4 deformMatrix() const { return m_centeredDeformMatrix; } + +signals: + void groupChanged(); + void radiusChanged(); + void deformMatrixChanged(); + +protected: + void componentComplete() override; + void geometryChange( + const QRectF& newGeometry, const QRectF& oldGeometry) override; + void updatePolish() override; + QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override; + + virtual bool isInvertedRect() const { return false; } + + virtual void updatePhysics() {} + + virtual void registerWithGroup(); + virtual void unregisterFromGroup(); + void updateCenteredDeformMatrix(); + + BlobGroup* m_group = nullptr; + qreal m_radius = 0; + QMatrix4x4 m_deformMatrix; // identity by default + QMatrix4x4 m_centeredDeformMatrix; + + // Cached data from updatePolish + float m_cachedPaddedX = 0; + float m_cachedPaddedY = 0; + float m_cachedPaddedW = 0; + float m_cachedPaddedH = 0; + QRectF m_localPaddedRect; + QVector m_cachedRects; + int m_cachedMyIndex = -2; + bool m_cachedHasInverted = false; + float m_cachedInvertedRadius = 0; + float m_cachedInvertedOuter[4] = {}; + float m_cachedInvertedInner[4] = {}; +}; diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.frag b/plugin/src/Caelestia/Blobs/shaders/blob.frag new file mode 100644 index 00000000..f4baa731 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/shaders/blob.frag @@ -0,0 +1,277 @@ +#version 440 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float paddedX; + float paddedY; + float paddedW; + float paddedH; + float smoothFactor; + int rectCount; + int myIndex; + vec4 color; + int hasInverted; + float invertedRadius; + vec4 invertedOuter; + vec4 invertedInner; + vec4 rectData[80]; +}; + +float sdRoundedBox(vec2 p, vec2 center, vec2 halfSize, float radius) { + vec2 d = abs(p - center) - halfSize + vec2(radius); + return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0) - radius; +} + +float sdRoundedBox4(vec2 p, vec2 center, vec2 halfSize, vec4 r) { + // r = (topRight, bottomRight, bottomLeft, topLeft) + p -= center; + r.xy = (p.x > 0.0) ? r.xy : r.wz; + r.x = (p.y > 0.0) ? r.y : r.x; + vec2 q = abs(p) - halfSize + r.x; + return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r.x; +} + +float sdBox(vec2 p, vec2 center, vec2 halfSize) { + vec2 d = abs(p - center) - halfSize; + return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0); +} + +float smin(float a, float b, float k) { + // Cubic smooth min (C2 continuous — no curvature kinks at blend boundary) + float h = max(k - abs(a - b), 0.0) / k; + return min(a, b) - h * h * h * k * (1.0/6.0); +} + +float sminNoBulge(float a, float b, float k) { + // Cubic smooth min with reduced outward expansion when shapes overlap + float h = max(k - abs(a - b), 0.0) / k; + float blend = h * h * h * k * (1.0/6.0); + blend *= smoothstep(-k, 0.0, min(a, b)); + return min(a, b) - blend; +} + +float smax(float a, float b, float k) { + float h = max(k - abs(a - b), 0.0) / k; + return max(a, b) + h * h * h * k * (1.0/6.0); +} + +float smaxSharpA(float a, float b, float k) { + // smax variant that keeps a's boundary sharp (no inward rounding at a = 0). + // Used for the frame outer edge so it always fills to the edges. + float h = max(k - abs(a - b), 0.0) / k; + float blend = h * h * h * k * (1.0/6.0); + blend *= smoothstep(0.0, k * 0.5, -a); + return max(a, b) + blend; +} + +void main() { + vec2 pixel = vec2(paddedX, paddedY) + qt_TexCoord0 * vec2(paddedW, paddedH); + + float mergedSdf = 1e10; + int owner = -2; + float minDist = 1e10; + + for (int i = 0; i < rectCount; i++) { + vec4 rect = rectData[i * 5]; // cx, cy, hw, hh + vec4 props = rectData[i * 5 + 1]; // radius, offsetX, offsetY, minEig + vec4 invDm = rectData[i * 5 + 2]; // inverse deform matrix + vec4 sh = rectData[i * 5 + 3]; // screenHalfX, screenHalfY, 0, 0 + vec4 fills = rectData[i * 5 + 4]; // f_tr, f_br, f_bl, f_tl + + // Offset center for asymmetric deformation + vec2 center = rect.xy + props.yz; + + // Apply pre-computed inverse deformation to the evaluation point + mat2 invDeform = mat2(invDm.xy, invDm.zw); + vec2 transformedPixel = center + invDeform * (pixel - center); + + // Use pre-computed corner fill factors + float br = props.x; + float minR = 2.0; + vec4 radii = max(br * fills, vec4(minR)); + float d = sdRoundedBox4(transformedPixel, center, rect.zw, radii); + + // Use pre-computed minimum eigenvalue for SDF correction + d *= max(props.w, 0.01); + + // Scale SDF on the axis facing a nearby border to narrow the smin blend zone + // in that direction only, without reducing k (which would cause sharp edges). + if (hasInverted != 0) { + vec2 screenHalf = sh.xy; + + float distY0 = (center.y + screenHalf.y) - (invertedInner.y - invertedInner.w); + float distY1 = (invertedInner.y + invertedInner.w) - (center.y - screenHalf.y); + float distX0 = (center.x + screenHalf.x) - (invertedInner.x - invertedInner.z); + float distX1 = (invertedInner.x + invertedInner.z) - (center.x - screenHalf.x); + + // 0 = far from border, 1 = at border (max compression) + float yProx = 1.0 - min( + smoothstep(0.0, smoothFactor, distY0), + smoothstep(0.0, smoothFactor, distY1) + ); + float xProx = 1.0 - min( + smoothstep(0.0, smoothFactor, distX0), + smoothstep(0.0, smoothFactor, distX1) + ); + + // Smooth axis weights: gradient-based at corners, face-based inside. + vec2 q = abs(pixel - center) - screenHalf; + vec2 qp = max(q, vec2(0.0)); + float cornerLen = length(qp); + + // Gradient direction in corner region (smooth 90-degree rotation) + float gradX = qp.x / max(cornerLen, 0.001); + float gradY = qp.y / max(cornerLen, 0.001); + + // Smooth face weights for inside/edge (no hard branch) + float faceY = smoothstep(-4.0, 4.0, q.y - q.x); + float faceX = 1.0 - faceY; + + // Blend: gradient in corner region, face-based inside + float t = smoothstep(0.0, 2.0, cornerLen); + float xWeight = mix(faceX, gradX, t); + float yWeight = mix(faceY, gradY, t); + + float boost = 3.0; + float scale = 1.0 + (xProx * xWeight + yProx * yWeight) * boost; + d *= scale; + } + + // Rect-to-rect edge sink: indent this rect's edge where another + // rect is slightly past it, fading once past threshold. + if (rectCount > 1) { + vec2 iSh = sh.xy; + float sinkT = smoothFactor * 0.75; + float sinkOff = smoothFactor * (1.0 / 6.0); + float rectSinkVal = 0.0; + + for (int j = 0; j < rectCount; j++) { + if (j == i) continue; + + vec4 jR = rectData[j * 5]; + vec4 jP = rectData[j * 5 + 1]; + vec2 jSh = rectData[j * 5 + 3].xy; + vec2 jC = jR.xy + jP.yz; + + // Penetration of j past i's edges (positive = past) + float pT = (jC.y + jSh.y) - (center.y - iSh.y) - sinkOff; + float pB = (center.y + iSh.y) - (jC.y - jSh.y) - sinkOff; + float pL = (jC.x + jSh.x) - (center.x - iSh.x) - sinkOff; + float pR = (center.x + iSh.x) - (jC.x - jSh.x) - sinkOff; + + // Smooth bump: rises then falls, zero outside [0, sinkT] + float aT = smoothstep(0.0, sinkT * 0.4, pT) * (1.0 - smoothstep(sinkT * 0.5, sinkT, pT)); + float aB = smoothstep(0.0, sinkT * 0.4, pB) * (1.0 - smoothstep(sinkT * 0.5, sinkT, pB)); + float aL = smoothstep(0.0, sinkT * 0.4, pL) * (1.0 - smoothstep(sinkT * 0.5, sinkT, pL)); + float aR = smoothstep(0.0, sinkT * 0.4, pR) * (1.0 - smoothstep(sinkT * 0.5, sinkT, pR)); + + // Lateral falloff from rect j's extent + float hLat = max(abs(pixel.x - jC.x) - jSh.x, 0.0); + float vLat = max(abs(pixel.y - jC.y) - jSh.y, 0.0); + float latF = smoothFactor * 2.0; + + // Perpendicular zone (near rect i's edge only) + float zT = 1.0 - smoothstep(center.y - iSh.y, center.y - iSh.y + smoothFactor, pixel.y); + float zB = smoothstep(center.y + iSh.y - smoothFactor, center.y + iSh.y, pixel.y); + float zL = 1.0 - smoothstep(center.x - iSh.x, center.x - iSh.x + smoothFactor, pixel.x); + float zR = smoothstep(center.x + iSh.x - smoothFactor, center.x + iSh.x, pixel.x); + + float s = max( + max(aT * smoothstep(latF, 0.0, hLat) * zT, + aB * smoothstep(latF, 0.0, hLat) * zB), + max(aL * smoothstep(latF, 0.0, vLat) * zL, + aR * smoothstep(latF, 0.0, vLat) * zR) + ); + rectSinkVal = max(rectSinkVal, s); + } + d += rectSinkVal * smoothFactor * 0.25; + } + + mergedSdf = sminNoBulge(mergedSdf, d, smoothFactor); + if (d < minDist) { + minDist = d; + owner = i; + } + } + + if (hasInverted != 0) { + float dOuter = sdBox(pixel, invertedOuter.xy, invertedOuter.zw) - 1.0; + float dInner = sdRoundedBox(pixel, invertedInner.xy, invertedInner.zw, invertedRadius); + + // Border sinks: track the opposite rect edge, clamped to border thickness + float innerTop = invertedInner.y - invertedInner.w; + float innerBot = invertedInner.y + invertedInner.w; + float innerLeft = invertedInner.x - invertedInner.z; + float innerRight = invertedInner.x + invertedInner.z; + float outerTop = invertedOuter.y - invertedOuter.w; + float outerBot = invertedOuter.y + invertedOuter.w; + float outerLeft = invertedOuter.x - invertedOuter.z; + float outerRight = invertedOuter.x + invertedOuter.z; + + float sinkValue = 0.0; + for (int i = 0; i < rectCount; i++) { + vec4 rect = rectData[i * 5]; + vec4 sinkProps = rectData[i * 5 + 1]; + vec2 sinkSh = rectData[i * 5 + 3].xy; + + // Screen-space center (with offset) and pre-computed AABB half-extents + vec2 ctr = rect.xy + sinkProps.yz; + + // Delay sink to absorb smin blend depth (cubic smin max = k/6) + float preOff = smoothFactor * (1.0/6.0); + + // Top border: track rect's BOTTOM edge, only within border thickness + float topPen = clamp(innerTop - (ctr.y + sinkSh.y) - preOff, 0.0, innerTop - outerTop); + + // Bottom border: track rect's TOP edge + float botPen = clamp((ctr.y - sinkSh.y) - innerBot - preOff, 0.0, outerBot - innerBot); + + // Left border: track rect's RIGHT edge + float leftPen = clamp(innerLeft - (ctr.x + sinkSh.x) - preOff, 0.0, innerLeft - outerLeft); + + // Right border: track rect's LEFT edge + float rightPen = clamp((ctr.x - sinkSh.x) - innerRight - preOff, 0.0, outerRight - innerRight); + + // Lateral distance from pixel to rect's extent along each edge + float hLat = max(abs(pixel.x - ctr.x) - sinkSh.x, 0.0); + float vLat = max(abs(pixel.y - ctr.y) - sinkSh.y, 0.0); + + // Perpendicular proximity: full strength in border, fade inside inner area + float topZone = 1.0 - smoothstep(innerTop, innerTop + smoothFactor, pixel.y); + float botZone = smoothstep(innerBot - smoothFactor, innerBot, pixel.y); + float leftZone = 1.0 - smoothstep(innerLeft, innerLeft + smoothFactor, pixel.x); + float rightZone = smoothstep(innerRight - smoothFactor, innerRight, pixel.x); + + float s = smoothFactor * 2.0; + float sink = max( + max(topPen * smoothstep(s, 0.0, hLat) * topZone, + botPen * smoothstep(s, 0.0, hLat) * botZone), + max(leftPen * smoothstep(s, 0.0, vLat) * leftZone, + rightPen * smoothstep(s, 0.0, vLat) * rightZone) + ); + sinkValue = max(sinkValue, sink); + } + + dInner -= sinkValue; + + float dFrame = smaxSharpA(dOuter, -dInner, smoothFactor); + + mergedSdf = smin(mergedSdf, dFrame, smoothFactor); + if (dFrame < minDist) { + owner = -1; + } + } + + // myIndex == -1: inverted rect renders everything (frame + blobs) + // myIndex >= 0: individual rect renders only its owned pixels + if (myIndex >= 0 && owner != myIndex) + discard; + + float fw = fwidth(mergedSdf); + float alpha = 1.0 - smoothstep(-fw, fw, mergedSdf); + fragColor = vec4(color.rgb * alpha, alpha) * qt_Opacity; +} diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.vert b/plugin/src/Caelestia/Blobs/shaders/blob.vert new file mode 100644 index 00000000..e71d8104 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/shaders/blob.vert @@ -0,0 +1,29 @@ +#version 440 + +layout(location = 0) in vec4 qt_VertexPosition; +layout(location = 1) in vec2 qt_VertexTexCoord; + +layout(location = 0) out vec2 qt_TexCoord0; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float paddedX; + float paddedY; + float paddedW; + float paddedH; + float smoothFactor; + int rectCount; + int myIndex; + vec4 color; + int hasInverted; + float invertedRadius; + vec4 invertedOuter; + vec4 invertedInner; + vec4 rectData[80]; +}; + +void main() { + gl_Position = qt_Matrix * qt_VertexPosition; + qt_TexCoord0 = qt_VertexTexCoord; +} diff --git a/plugin/src/Caelestia/CMakeLists.txt b/plugin/src/Caelestia/CMakeLists.txt index 1b7d0e49..e8b72479 100644 --- a/plugin/src/Caelestia/CMakeLists.txt +++ b/plugin/src/Caelestia/CMakeLists.txt @@ -1,4 +1,4 @@ -find_package(Qt6 REQUIRED COMPONENTS Core Qml Gui Quick Concurrent Sql Network DBus) +find_package(Qt6 REQUIRED COMPONENTS ShaderTools Core Qml Gui Quick Concurrent Sql Network DBus) find_package(PkgConfig REQUIRED) pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED) pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED) @@ -60,3 +60,4 @@ qml_module(caelestia add_subdirectory(Internal) add_subdirectory(Models) add_subdirectory(Services) +add_subdirectory(Blobs) From 87f33d55ee8ef3ab3351d02493d810acad833ee9 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:04:55 +1100 Subject: [PATCH 02/45] feat: sdf blob based drawers --- modules/bar/popouts/Wrapper.qml | 3 +- modules/dashboard/Wrapper.qml | 38 ++++---- modules/drawers/Drawers.qml | 145 +++++++++++++++++++++++++++++- modules/drawers/Interactions.qml | 2 +- modules/drawers/Panels.qml | 8 +- modules/launcher/Wrapper.qml | 33 +++---- modules/notifications/Wrapper.qml | 11 +-- modules/osd/Wrapper.qml | 37 ++++---- modules/session/Wrapper.qml | 37 ++++---- modules/sidebar/Wrapper.qml | 37 ++++---- modules/utilities/Wrapper.qml | 36 ++++---- 11 files changed, 273 insertions(+), 114 deletions(-) diff --git a/modules/bar/popouts/Wrapper.qml b/modules/bar/popouts/Wrapper.qml index 06bec9b0..f58b56da 100644 --- a/modules/bar/popouts/Wrapper.qml +++ b/modules/bar/popouts/Wrapper.qml @@ -15,7 +15,8 @@ Item { required property ShellScreen screen - readonly property real nonAnimWidth: x > 0 || hasCurrent ? children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth : 0 + readonly property real shownWidth: children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth + readonly property real nonAnimWidth: x > 0 || hasCurrent ? shownWidth : 0 readonly property real nonAnimHeight: children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight readonly property Item current: (content.item as Content)?.current ?? null diff --git a/modules/dashboard/Wrapper.qml b/modules/dashboard/Wrapper.qml index 596c21b7..8448faa6 100644 --- a/modules/dashboard/Wrapper.qml +++ b/modules/dashboard/Wrapper.qml @@ -30,8 +30,9 @@ Item { readonly property real nonAnimHeight: state === "visible" ? ((content.item as Content)?.nonAnimHeight ?? 0) : 0 - visible: height > 0 - implicitHeight: 0 + visible: anchors.topMargin > -implicitHeight - 5 + anchors.topMargin: -implicitHeight - 5 + implicitHeight: content.implicitHeight implicitWidth: content.implicitWidth onStateChanged: { @@ -46,32 +47,35 @@ Item { when: root.visibilities.dashboard && Config.dashboard.enabled PropertyChanges { - root.implicitHeight: content.implicitHeight + // root.implicitHeight: content.implicitHeight + root.anchors.topMargin: 0 } } transitions: [ Transition { - from: "" - to: "visible" + // from: "" + // to: "visible" Anim { - target: root - property: "implicitHeight" + target: root.anchors + property: "topMargin" duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } - }, - Transition { - from: "visible" - to: "" - - Anim { - target: root - property: "implicitHeight" - easing.bezierCurve: Appearance.anim.curves.emphasized - } } + // Transition { + // from: "visible" + // to: "" + + // Anim { + // target: root.anchors + // property: "topMargin" + // easing.bezierCurve: Appearance.anim.curves.emphasized + // } + // } + + ] Timer { diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 864ad5c4..1ae98c7a 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -6,6 +6,7 @@ import QtQuick.Effects import Quickshell import Quickshell.Hyprland import Quickshell.Wayland +import Caelestia.Blobs import qs.components import qs.components.containers import qs.services @@ -123,13 +124,113 @@ Variants { shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7) } - Border { + // Border { + // bar: bar + // } + + // Backgrounds { + // panels: panels + // bar: bar + // } + + BlobGroup { + id: blobGroup + + color: Colours.palette.m3surface + } + + BlobInvertedRect { + anchors.fill: parent + anchors.margins: -50 // Make border thicker to smooth out bulge from closed drawers + group: blobGroup + radius: Config.border.rounding + borderLeft: bar.implicitWidth - anchors.margins + borderRight: Config.border.thickness - anchors.margins + borderTop: Config.border.thickness - anchors.margins + borderBottom: Config.border.thickness - anchors.margins + } + + PanelBg { + id: dashBg + + group: blobGroup + panel: panels.dashboard bar: bar } - Backgrounds { - panels: panels + PanelBg { + id: launcherBg + + group: blobGroup + panel: panels.launcher bar: bar + deformAmount: 0.1 + } + + PanelBg { + id: sessionBg + + group: blobGroup + panel: panels.session + bar: bar + } + + PanelBg { + id: sidebarBg + + group: blobGroup + panel: panels.sidebar + bar: bar + deformAmount: 0 + } + + PanelBg { + id: osdBg + + group: blobGroup + panel: panels.osd + bar: bar + } + + PanelBg { + id: notifsBg + + group: blobGroup + panel: panels.notifications + bar: bar + } + + PanelBg { + id: utilsBg + + group: blobGroup + panel: panels.utilities + bar: bar + } + + PanelBg { + id: popoutBg + + group: blobGroup + panel: panels.popouts + bar: bar + + x: bar.implicitWidth - (panels.popouts.isDetached ? -(win.width - panels.popouts.shownWidth) / 2 : panels.popouts.hasCurrent ? 0 : panels.popouts.shownWidth + 5) + implicitWidth: panels.popouts.shownWidth + + Behavior on x { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + Behavior on implicitWidth { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } } } @@ -152,6 +253,31 @@ Variants { screen: scope.modelData visibilities: visibilities bar: bar + + dashboard.transform: Matrix4x4 { + matrix: dashBg.deformMatrix + } + launcher.transform: Matrix4x4 { + matrix: launcherBg.deformMatrix + } + session.transform: Matrix4x4 { + matrix: sessionBg.deformMatrix + } + sidebar.transform: Matrix4x4 { + matrix: sidebarBg.deformMatrix + } + osd.transform: Matrix4x4 { + matrix: osdBg.deformMatrix + } + notifications.transform: Matrix4x4 { + matrix: notifsBg.deformMatrix + } + utilities.transform: Matrix4x4 { + matrix: utilsBg.deformMatrix + } + popouts.transform: Matrix4x4 { + matrix: popoutBg.deformMatrix + } } BarWrapper { @@ -171,4 +297,17 @@ Variants { } } } + + component PanelBg: BlobRect { + required property Item panel + required property Item bar + property real deformAmount: 0.15 + + x: panel.x + bar.implicitWidth + y: panel.y + Config.border.thickness + implicitWidth: panel.width + implicitHeight: panel.height + radius: Config.border.rounding + deformScale: deformAmount / 10000 + } } diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index fcb128a9..6008a853 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -102,7 +102,7 @@ CustomMouseArea { visibilities.bar = false; } - if (panels.sidebar.width === 0) { + if (panels.sidebar.anchors.rightMargin === -panels.sidebar.implicitWidth - 5) { // Show osd on hover const showOsd = inRightPanel(panels.osd, x, y); diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index f2531cfa..006205fd 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -42,8 +42,8 @@ Item { visibilities: root.visibilities anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - anchors.rightMargin: session.width + sidebar.width + anchors.right: session.left + // anchors.rightMargin: session.width + sidebar.width } Notifications.Wrapper { @@ -66,8 +66,8 @@ Item { panels: root anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - anchors.rightMargin: sidebar.width + anchors.right: sidebar.left + // anchors.rightMargin: sidebar.width } Launcher.Wrapper { diff --git a/modules/launcher/Wrapper.qml b/modules/launcher/Wrapper.qml index cc5e86c6..f5c866af 100644 --- a/modules/launcher/Wrapper.qml +++ b/modules/launcher/Wrapper.qml @@ -24,8 +24,9 @@ Item { onMaxHeightChanged: timer.start() - visible: height > 0 - implicitHeight: 0 + visible: anchors.bottomMargin > -implicitHeight - 5 + anchors.bottomMargin: -implicitHeight - 5 + implicitHeight: content.implicitHeight implicitWidth: content.implicitWidth onShouldBeActiveChanged: { @@ -43,28 +44,30 @@ Item { id: showAnim Anim { - target: root - property: "implicitHeight" - to: root.contentHeight + target: root.anchors + property: "bottomMargin" + to: 0 duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } - ScriptAction { - script: root.implicitHeight = Qt.binding(() => content.implicitHeight) - } + // ScriptAction { + // script: root.implicitHeight = Qt.binding(() => content.implicitHeight) + // } } SequentialAnimation { id: hideAnim - ScriptAction { - script: root.implicitHeight = root.implicitHeight - } + // ScriptAction { + // script: root.implicitHeight = root.implicitHeight + // } Anim { - target: root - property: "implicitHeight" - to: 0 - easing.bezierCurve: Appearance.anim.curves.emphasized + target: root.anchors + property: "bottomMargin" + to: -content.implicitHeight - 5 + // easing.bezierCurve: Appearance.anim.curves.emphasized + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } diff --git a/modules/notifications/Wrapper.qml b/modules/notifications/Wrapper.qml index 2b581e93..c0756b9a 100644 --- a/modules/notifications/Wrapper.qml +++ b/modules/notifications/Wrapper.qml @@ -10,23 +10,24 @@ Item { property alias osdPanel: content.osdPanel property alias sessionPanel: content.sessionPanel - visible: height > 0 + visible: anchors.topMargin > -implicitHeight - 5 + anchors.topMargin: -5 implicitWidth: Math.max(sidebarPanel.width, content.implicitWidth) implicitHeight: content.implicitHeight states: State { name: "hidden" - when: root.visibilities.sidebar && Config.sidebar.enabled + // when: root.visibilities.sidebar && Config.sidebar.enabled PropertyChanges { - root.implicitHeight: 0 + root.anchors.topMargin: -implicitHeight - 5 } } transitions: Transition { Anim { - target: root - property: "implicitHeight" + target: root.anchors + property: "topMargin" duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } diff --git a/modules/osd/Wrapper.qml b/modules/osd/Wrapper.qml index 939b57de..87350d92 100644 --- a/modules/osd/Wrapper.qml +++ b/modules/osd/Wrapper.qml @@ -34,8 +34,9 @@ Item { brightness = root.monitor?.brightness ?? 0; } - visible: width > 0 - implicitWidth: 0 + visible: anchors.rightMargin > -implicitWidth + anchors.rightMargin: -implicitWidth + implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight states: State { @@ -43,31 +44,33 @@ Item { when: root.shouldBeActive PropertyChanges { - root.implicitWidth: content.implicitWidth + root.anchors.rightMargin: 0 } } transitions: [ Transition { - from: "" - to: "visible" + // from: "" + // to: "visible" Anim { - target: root - property: "implicitWidth" + target: root.anchors + property: "rightMargin" easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } - }, - Transition { - from: "visible" - to: "" - - Anim { - target: root - property: "implicitWidth" - easing.bezierCurve: Appearance.anim.curves.emphasized - } } + // Transition { + // from: "visible" + // to: "" + + // Anim { + // target: root + // property: "implicitWidth" + // easing.bezierCurve: Appearance.anim.curves.emphasized + // } + // } + + ] Connections { diff --git a/modules/session/Wrapper.qml b/modules/session/Wrapper.qml index 2924f776..b829409b 100644 --- a/modules/session/Wrapper.qml +++ b/modules/session/Wrapper.qml @@ -11,8 +11,9 @@ Item { required property var panels readonly property real nonAnimWidth: content.implicitWidth - visible: width > 0 - implicitWidth: 0 + visible: anchors.rightMargin > -implicitWidth - 1 + anchors.rightMargin: -implicitWidth - 1 + implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight states: State { @@ -20,31 +21,33 @@ Item { when: root.visibilities.session && Config.session.enabled PropertyChanges { - root.implicitWidth: root.nonAnimWidth + root.anchors.rightMargin: 0 } } transitions: [ Transition { - from: "" - to: "visible" + // from: "" + // to: "visible" Anim { - target: root - property: "implicitWidth" + target: root.anchors + property: "rightMargin" easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } - }, - Transition { - from: "visible" - to: "" - - Anim { - target: root - property: "implicitWidth" - easing.bezierCurve: root.panels.osd.width > 0 ? Appearance.anim.curves.expressiveDefaultSpatial : Appearance.anim.curves.emphasized - } } + // Transition { + // from: "visible" + // to: "" + + // Anim { + // target: root + // property: "implicitWidth" + // easing.bezierCurve: root.panels.osd.width > 0 ? Appearance.anim.curves.expressiveDefaultSpatial : Appearance.anim.curves.emphasized + // } + // } + + ] Loader { diff --git a/modules/sidebar/Wrapper.qml b/modules/sidebar/Wrapper.qml index ad256413..67929bd7 100644 --- a/modules/sidebar/Wrapper.qml +++ b/modules/sidebar/Wrapper.qml @@ -11,40 +11,43 @@ Item { required property var panels readonly property Props props: Props {} - visible: width > 0 - implicitWidth: 0 + visible: anchors.rightMargin > -implicitWidth - 5 + anchors.rightMargin: -implicitWidth - 5 + implicitWidth: Config.sidebar.sizes.width states: State { name: "visible" when: root.visibilities.sidebar && Config.sidebar.enabled PropertyChanges { - root.implicitWidth: Config.sidebar.sizes.width + root.anchors.rightMargin: 0 } } transitions: [ Transition { - from: "" - to: "visible" + // from: "" + // to: "visible" Anim { - target: root - property: "implicitWidth" + target: root.anchors + property: "rightMargin" duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } - }, - Transition { - from: "visible" - to: "" - - Anim { - target: root - property: "implicitWidth" - easing.bezierCurve: root.panels.osd.width > 0 || root.panels.session.width > 0 ? Appearance.anim.curves.expressiveDefaultSpatial : Appearance.anim.curves.emphasized - } } + // Transition { + // from: "visible" + // to: "" + + // Anim { + // target: root + // property: "implicitWidth" + // easing.bezierCurve: root.panels.osd.width > 0 || root.panels.session.width > 0 ? Appearance.anim.curves.expressiveDefaultSpatial : Appearance.anim.curves.emphasized + // } + // } + + ] Loader { diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index 66a616f0..59288f5d 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -22,8 +22,9 @@ Item { } readonly property bool shouldBeActive: visibilities.sidebar || (visibilities.utilities && Config.utilities.enabled && !(visibilities.session && Config.session.enabled)) - visible: height > 0 - implicitHeight: 0 + visible: anchors.bottomMargin > -implicitHeight - 5 + anchors.bottomMargin: -implicitHeight - 5 + implicitHeight: content.implicitHeight implicitWidth: sidebar.visible ? sidebar.width : Config.utilities.sizes.width onStateChanged: { @@ -38,32 +39,33 @@ Item { when: root.shouldBeActive PropertyChanges { - root.implicitHeight: content.implicitHeight + Appearance.padding.large * 2 + root.anchors.bottomMargin: 0 } } transitions: [ Transition { - from: "" - to: "visible" + // from: "" + // to: "visible" Anim { - target: root - property: "implicitHeight" + target: root.anchors + property: "bottomMargin" duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } - }, - Transition { - from: "visible" - to: "" - - Anim { - target: root - property: "implicitHeight" - easing.bezierCurve: Appearance.anim.curves.emphasized - } } + // Transition { + // from: "visible" + // to: "" + + // Anim { + // target: root + // property: "implicitHeight" + // easing.bezierCurve: Appearance.anim.curves.emphasized + // } + // } + ] Timer { From f6d7e40f35597997fe7ec8aa43d15a13b671b521 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 25 Mar 2026 21:13:00 +1100 Subject: [PATCH 03/45] fix: tweak deform amounts --- modules/drawers/Drawers.qml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 1ae98c7a..ed435d9a 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -156,6 +156,7 @@ Variants { group: blobGroup panel: panels.dashboard bar: bar + deformAmount: 0.1 } PanelBg { @@ -173,6 +174,7 @@ Variants { group: blobGroup panel: panels.session bar: bar + deformAmount: 0.25 } PanelBg { @@ -190,6 +192,7 @@ Variants { group: blobGroup panel: panels.osd bar: bar + deformAmount: 0.3 } PanelBg { From 6103ce19e8937fb1729715a24abafc69c6618090 Mon Sep 17 00:00:00 2001 From: Robin Seger Date: Wed, 25 Mar 2026 19:45:45 +0100 Subject: [PATCH 04/45] improve performace of SDF MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Avoid fullscreen shading every pixel - Evaluate each BlobRect: localized quad, only nearby rects, instead of all 8 rects - O(1-3) per pixel for most passes, rather than O(N²) with N=8(~72 iterations). - Closed panels excluded entirely from loop. --- modules/drawers/Drawers.qml | 9 +- .../src/Caelestia/Blobs/blobinvertedrect.cpp | 117 ++++++++++++++++++ .../src/Caelestia/Blobs/blobinvertedrect.hpp | 2 + plugin/src/Caelestia/Blobs/blobshape.cpp | 14 +-- plugin/src/Caelestia/Blobs/shaders/blob.frag | 18 ++- 5 files changed, 139 insertions(+), 21 deletions(-) diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index ed435d9a..ff1d6a4d 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -153,7 +153,6 @@ Variants { PanelBg { id: dashBg - group: blobGroup panel: panels.dashboard bar: bar deformAmount: 0.1 @@ -162,7 +161,6 @@ Variants { PanelBg { id: launcherBg - group: blobGroup panel: panels.launcher bar: bar deformAmount: 0.1 @@ -171,7 +169,6 @@ Variants { PanelBg { id: sessionBg - group: blobGroup panel: panels.session bar: bar deformAmount: 0.25 @@ -180,7 +177,6 @@ Variants { PanelBg { id: sidebarBg - group: blobGroup panel: panels.sidebar bar: bar deformAmount: 0 @@ -189,7 +185,6 @@ Variants { PanelBg { id: osdBg - group: blobGroup panel: panels.osd bar: bar deformAmount: 0.3 @@ -198,7 +193,6 @@ Variants { PanelBg { id: notifsBg - group: blobGroup panel: panels.notifications bar: bar } @@ -206,7 +200,6 @@ Variants { PanelBg { id: utilsBg - group: blobGroup panel: panels.utilities bar: bar } @@ -214,7 +207,6 @@ Variants { PanelBg { id: popoutBg - group: blobGroup panel: panels.popouts bar: bar @@ -306,6 +298,7 @@ Variants { required property Item bar property real deformAmount: 0.15 + group: panel.width > 0 && panel.height > 0 ? blobGroup : null x: panel.x + bar.implicitWidth y: panel.y + Config.border.thickness implicitWidth: panel.width diff --git a/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp b/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp index 68024738..3e392b4a 100644 --- a/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp +++ b/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp @@ -1,9 +1,126 @@ #include "blobinvertedrect.hpp" #include "blobgroup.hpp" +#include "blobmaterial.hpp" + +#include +#include + +#include +#include BlobInvertedRect::BlobInvertedRect(QQuickItem* parent) : BlobShape(parent) {} +static void setFrameIndices(quint16* idx) { + // Top strip: 0-1-4, 1-5-4 + idx[0] = 0; idx[1] = 1; idx[2] = 4; + idx[3] = 1; idx[4] = 5; idx[5] = 4; + // Right strip: 1-2-5, 2-6-5 + idx[6] = 1; idx[7] = 2; idx[8] = 5; + idx[9] = 2; idx[10] = 6; idx[11] = 5; + // Bottom strip: 2-3-6, 3-7-6 + idx[12] = 2; idx[13] = 3; idx[14] = 6; + idx[15] = 3; idx[16] = 7; idx[17] = 6; + // Left strip: 3-0-7, 0-4-7 + idx[18] = 3; idx[19] = 0; idx[20] = 7; + idx[21] = 0; idx[22] = 4; idx[23] = 7; +} + +QSGNode* BlobInvertedRect::updatePaintNode( + QSGNode* oldNode, UpdatePaintNodeData*) { + if (!m_group) { + delete oldNode; + return nullptr; + } + + const float pad = static_cast(m_group->smoothing()); + + // Compute inner hole boundary in local coords + // Inset past the inner border edge by 2x smoothing to cover the blend zone + const float inset = pad * 2.0f; + const float holeLeft = static_cast(m_borderLeft) + inset; + const float holeTop = static_cast(m_borderTop) + inset; + const float holeRight = static_cast(width() - m_borderRight) - inset; + const float holeBot = static_cast(height() - m_borderBottom) - inset; + + // If the hole is too small or invalid, fall back to full quad + if (holeLeft >= holeRight || holeTop >= holeBot) + return BlobShape::updatePaintNode(oldNode, nullptr); + + auto* node = static_cast(oldNode); + + const bool needsRebuild = !node || node->geometry()->vertexCount() != 8; + + if (needsRebuild) { + delete oldNode; + node = new QSGGeometryNode; + + auto* geometry = new QSGGeometry( + QSGGeometry::defaultAttributes_TexturedPoint2D(), 8, 24, + QSGGeometry::UnsignedShortType); + geometry->setDrawingMode(QSGGeometry::DrawTriangles); + node->setGeometry(geometry); + node->setFlag(QSGNode::OwnsGeometry); + + setFrameIndices(geometry->indexDataAsUShort()); + + auto* material = new BlobMaterial; + material->setFlag(QSGMaterial::Blending); + node->setMaterial(material); + node->setFlag(QSGNode::OwnsMaterial); + } + + // Outer bounds (local coords) + const float x0 = static_cast(m_localPaddedRect.x()); + const float y0 = static_cast(m_localPaddedRect.y()); + const float x1 = x0 + static_cast(m_localPaddedRect.width()); + const float y1 = y0 + static_cast(m_localPaddedRect.height()); + const float w = x1 - x0; + const float h = y1 - y0; + + // Update vertex positions and texture coordinates + auto* v = node->geometry()->vertexDataAsTexturedPoint2D(); + + // Outer corners + v[0].set(x0, y0, 0.0f, 0.0f); + v[1].set(x1, y0, 1.0f, 0.0f); + v[2].set(x1, y1, 1.0f, 1.0f); + v[3].set(x0, y1, 0.0f, 1.0f); + // Inner corners (hole) + v[4].set(holeLeft, holeTop, (holeLeft - x0) / w, (holeTop - y0) / h); + v[5].set(holeRight, holeTop, (holeRight - x0) / w, (holeTop - y0) / h); + v[6].set(holeRight, holeBot, (holeRight - x0) / w, (holeBot - y0) / h); + v[7].set(holeLeft, holeBot, (holeLeft - x0) / w, (holeBot - y0) / h); + + node->markDirty(QSGNode::DirtyGeometry); + + // Update material uniforms + auto* material = static_cast(node->material()); + material->m_paddedX = m_cachedPaddedX; + material->m_paddedY = m_cachedPaddedY; + material->m_paddedW = m_cachedPaddedW; + material->m_paddedH = m_cachedPaddedH; + material->m_smoothFactor = pad; + material->m_myIndex = m_cachedMyIndex; + material->m_color = m_group->color(); + material->m_hasInverted = m_cachedHasInverted ? 1 : 0; + material->m_invertedRadius = m_cachedInvertedRadius; + memcpy(material->m_invertedOuter, m_cachedInvertedOuter, + sizeof(m_cachedInvertedOuter)); + memcpy(material->m_invertedInner, m_cachedInvertedInner, + sizeof(m_cachedInvertedInner)); + + const int count = + static_cast(qMin(m_cachedRects.size(), qsizetype(16))); + material->m_rectCount = count; + for (int i = 0; i < count; ++i) + material->m_rects[i] = m_cachedRects[i]; + + node->markDirty(QSGNode::DirtyMaterial); + + return node; +} + BlobInvertedRect::~BlobInvertedRect() { if (m_group) m_group->clearInvertedRect(this); diff --git a/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp b/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp index 958e2a8d..207244de 100644 --- a/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp +++ b/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp @@ -45,6 +45,8 @@ signals: protected: bool isInvertedRect() const override { return true; } + QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override; + void registerWithGroup() override; void unregisterFromGroup() override; diff --git a/plugin/src/Caelestia/Blobs/blobshape.cpp b/plugin/src/Caelestia/Blobs/blobshape.cpp index 73b5cfe6..b99bd8e1 100644 --- a/plugin/src/Caelestia/Blobs/blobshape.cpp +++ b/plugin/src/Caelestia/Blobs/blobshape.cpp @@ -102,10 +102,6 @@ void BlobShape::updatePolish() { // Ensure all shapes have up-to-date physics (only once per frame) m_group->ensurePhysicsUpdated(); - // When inverted rect renders everything, skip spatial query for others - if (!isInvertedRect() && m_group->invertedRect()) - return; - const QPointF scenePos = mapToScene(QPointF(0, 0)); const float pad = static_cast(m_group->smoothing()); @@ -142,6 +138,10 @@ void BlobShape::updatePolish() { if (other->isInvertedRect()) continue; + // Skip zero-size rects + if (other->width() <= 0 || other->height() <= 0) + continue; + const QPointF otherScene = other->mapToScene(QPointF(0, 0)); bool include = false; @@ -299,12 +299,6 @@ QSGNode* BlobShape::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { return nullptr; } - // When an inverted rect exists, it renders everything in a single pass - if (!isInvertedRect() && m_group->invertedRect()) { - delete oldNode; - return nullptr; - } - auto* node = static_cast(oldNode); if (!node) { node = new QSGGeometryNode; diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.frag b/plugin/src/Caelestia/Blobs/shaders/blob.frag index f4baa731..66912609 100644 --- a/plugin/src/Caelestia/Blobs/shaders/blob.frag +++ b/plugin/src/Caelestia/Blobs/shaders/blob.frag @@ -85,6 +85,11 @@ void main() { // Offset center for asymmetric deformation vec2 center = rect.xy + props.yz; + // AABB early-out: skip rects far from this pixel + vec2 extent = sh.xy + vec2(smoothFactor * 1.5); + if (abs(pixel.x - center.x) > extent.x || abs(pixel.y - center.y) > extent.y) + continue; + // Apply pre-computed inverse deformation to the evaluation point mat2 invDeform = mat2(invDm.xy, invDm.zw); vec2 transformedPixel = center + invDeform * (pixel - center); @@ -157,6 +162,12 @@ void main() { vec2 jSh = rectData[j * 5 + 3].xy; vec2 jC = jR.xy + jP.yz; + // Skip non-adjacent rects + float sinkRange = smoothFactor * 1.5; + if (abs(center.x - jC.x) > iSh.x + jSh.x + sinkRange || + abs(center.y - jC.y) > iSh.y + jSh.y + sinkRange) + continue; + // Penetration of j past i's edges (positive = past) float pT = (jC.y + jSh.y) - (center.y - iSh.y) - sinkOff; float pB = (center.y + iSh.y) - (jC.y - jSh.y) - sinkOff; @@ -266,9 +277,10 @@ void main() { } } - // myIndex == -1: inverted rect renders everything (frame + blobs) - // myIndex >= 0: individual rect renders only its owned pixels - if (myIndex >= 0 && owner != myIndex) + // Each renderer only outputs pixels it owns + // myIndex == -1: inverted rect renders border-owned pixels + // myIndex >= 0: individual rect renders its owned pixels + if (owner != myIndex) discard; float fw = fwidth(mergedSdf); From ebccf801f465f391691e37524acb224d00c6cca1 Mon Sep 17 00:00:00 2001 From: Robin Seger Date: Wed, 25 Mar 2026 20:27:56 +0100 Subject: [PATCH 05/45] physics throttling, targeted dirty marking, proximity-based border - Snap deformation to rest when imperceptible instead of pumping frames - Only trigger markDirty on geometry changes exceeding 0.5px - Only include inverted rect data when rect is near the border --- plugin/src/Caelestia/Blobs/blobgroup.cpp | 34 ++++++++++++++++ plugin/src/Caelestia/Blobs/blobgroup.hpp | 1 + plugin/src/Caelestia/Blobs/blobrect.cpp | 31 +++++++++++---- plugin/src/Caelestia/Blobs/blobshape.cpp | 50 ++++++++++++++++++------ 4 files changed, 96 insertions(+), 20 deletions(-) diff --git a/plugin/src/Caelestia/Blobs/blobgroup.cpp b/plugin/src/Caelestia/Blobs/blobgroup.cpp index f06c4d51..ab4a31e6 100644 --- a/plugin/src/Caelestia/Blobs/blobgroup.cpp +++ b/plugin/src/Caelestia/Blobs/blobgroup.cpp @@ -66,6 +66,40 @@ void BlobGroup::markDirty() { } } +void BlobGroup::markShapeDirty(BlobShape* source) { + m_physicsUpdated = false; + + source->polish(); + source->update(); + + // Use cached padded rects to find spatial neighbors + const float pad = static_cast(m_smoothing) * 2.0f; + const QRectF srcRect( + static_cast(source->m_cachedPaddedX - pad), + static_cast(source->m_cachedPaddedY - pad), + static_cast(source->m_cachedPaddedW + pad * 2.0f), + static_cast(source->m_cachedPaddedH + pad * 2.0f)); + + for (auto* shape : std::as_const(m_shapes)) { + if (shape == source) + continue; + const QRectF otherRect( + static_cast(shape->m_cachedPaddedX), + static_cast(shape->m_cachedPaddedY), + static_cast(shape->m_cachedPaddedW), + static_cast(shape->m_cachedPaddedH)); + if (srcRect.intersects(otherRect)) { + shape->polish(); + shape->update(); + } + } + + if (m_invertedRect && static_cast(m_invertedRect) != source) { + static_cast(m_invertedRect)->polish(); + static_cast(m_invertedRect)->update(); + } +} + void BlobGroup::ensurePhysicsUpdated() { if (m_physicsUpdated) return; diff --git a/plugin/src/Caelestia/Blobs/blobgroup.hpp b/plugin/src/Caelestia/Blobs/blobgroup.hpp index 4c9ed691..b2e83edb 100644 --- a/plugin/src/Caelestia/Blobs/blobgroup.hpp +++ b/plugin/src/Caelestia/Blobs/blobgroup.hpp @@ -38,6 +38,7 @@ public: BlobInvertedRect* invertedRect() const { return m_invertedRect; } void markDirty(); + void markShapeDirty(BlobShape* source); void ensurePhysicsUpdated(); signals: diff --git a/plugin/src/Caelestia/Blobs/blobrect.cpp b/plugin/src/Caelestia/Blobs/blobrect.cpp index e03baa22..ff021203 100644 --- a/plugin/src/Caelestia/Blobs/blobrect.cpp +++ b/plugin/src/Caelestia/Blobs/blobrect.cpp @@ -16,13 +16,30 @@ void BlobRect::updatePolish() { BlobShape::updatePolish(); if (m_physicsActive) { - QMetaObject::invokeMethod( - this, - [this]() { - if (m_physicsActive && m_group) - m_group->markDirty(); - }, - Qt::QueuedConnection); + // Check if deformation is visually imperceptible + float totalDelta = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + + std::abs(m_dm11 - 1.0f); + float totalVel = + std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11); + + if (totalDelta < 0.004f && totalVel < 0.05f) { + // Snap to rest, no visible deformation + m_dm00 = 1.0f; + m_dm01 = 0.0f; + m_dm11 = 1.0f; + m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f; + m_deformMatrix = QMatrix4x4(); + updateCenteredDeformMatrix(); + m_physicsActive = false; + } else { + QMetaObject::invokeMethod( + this, + [this]() { + if (m_physicsActive && m_group) + m_group->markDirty(); + }, + Qt::QueuedConnection); + } } } diff --git a/plugin/src/Caelestia/Blobs/blobshape.cpp b/plugin/src/Caelestia/Blobs/blobshape.cpp index b99bd8e1..a7938a50 100644 --- a/plugin/src/Caelestia/Blobs/blobshape.cpp +++ b/plugin/src/Caelestia/Blobs/blobshape.cpp @@ -70,8 +70,15 @@ void BlobShape::geometryChange( const QRectF& newGeometry, const QRectF& oldGeometry) { QQuickItem::geometryChange(newGeometry, oldGeometry); updateCenteredDeformMatrix(); - if (m_group) - m_group->markDirty(); + if (m_group) { + // Only trigger redraw if the change is visually meaningful + const auto dx = std::abs(newGeometry.x() - oldGeometry.x()); + const auto dy = std::abs(newGeometry.y() - oldGeometry.y()); + const auto dw = std::abs(newGeometry.width() - oldGeometry.width()); + const auto dh = std::abs(newGeometry.height() - oldGeometry.height()); + if (dx > 0.5 || dy > 0.5 || dw > 0.5 || dh > 0.5) + m_group->markShapeDirty(this); + } } void BlobShape::updateCenteredDeformMatrix() { @@ -210,9 +217,6 @@ void BlobShape::updatePolish() { auto* inv = m_group->invertedRect(); if (inv) { - m_cachedHasInverted = true; - m_cachedInvertedRadius = static_cast(inv->radius()); - const QPointF invScene = inv->mapToScene(QPointF(0, 0)); const float outerCX = static_cast(invScene.x() + inv->width() / 2.0); @@ -234,15 +238,35 @@ void BlobShape::updatePolish() { outerHH - static_cast((inv->borderTop() + inv->borderBottom()) / 2.0); - m_cachedInvertedOuter[0] = outerCX; - m_cachedInvertedOuter[1] = outerCY; - m_cachedInvertedOuter[2] = outerHW; - m_cachedInvertedOuter[3] = outerHH; + // Check if this rect is near the border (within 2x smoothing of inner edge) + bool nearBorder = isInvertedRect(); + if (!nearBorder) { + const float margin = pad * 2.0f; + const float myCX = m_cachedPaddedX + m_cachedPaddedW * 0.5f; + const float myCY = m_cachedPaddedY + m_cachedPaddedH * 0.5f; + const float myHW = m_cachedPaddedW * 0.5f; + const float myHH = m_cachedPaddedH * 0.5f; + // Near border if any edge of padded rect is within margin of inner edge + nearBorder = (myCX - myHW < innerCX - innerHW + margin) || + (myCX + myHW > innerCX + innerHW - margin) || + (myCY - myHH < innerCY - innerHH + margin) || + (myCY + myHH > innerCY + innerHH - margin); + } - m_cachedInvertedInner[0] = innerCX; - m_cachedInvertedInner[1] = innerCY; - m_cachedInvertedInner[2] = innerHW; - m_cachedInvertedInner[3] = innerHH; + if (nearBorder) { + m_cachedHasInverted = true; + m_cachedInvertedRadius = static_cast(inv->radius()); + + m_cachedInvertedOuter[0] = outerCX; + m_cachedInvertedOuter[1] = outerCY; + m_cachedInvertedOuter[2] = outerHW; + m_cachedInvertedOuter[3] = outerHH; + + m_cachedInvertedInner[0] = innerCX; + m_cachedInvertedInner[1] = innerCY; + m_cachedInvertedInner[2] = innerHW; + m_cachedInvertedInner[3] = innerHH; + } } // Pre-compute corner fill factors (moves O(N²) work from GPU to CPU) From 7d01180f5c51022d96fb9c3d8e1c1ffa051026a7 Mon Sep 17 00:00:00 2001 From: Robin Seger Date: Wed, 25 Mar 2026 22:08:42 +0100 Subject: [PATCH 06/45] Only assign ownership if pixel is within blend zone --- plugin/src/Caelestia/Blobs/shaders/blob.frag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.frag b/plugin/src/Caelestia/Blobs/shaders/blob.frag index 66912609..b5fb7b4c 100644 --- a/plugin/src/Caelestia/Blobs/shaders/blob.frag +++ b/plugin/src/Caelestia/Blobs/shaders/blob.frag @@ -203,7 +203,7 @@ void main() { } mergedSdf = sminNoBulge(mergedSdf, d, smoothFactor); - if (d < minDist) { + if (d < smoothFactor && d < minDist) { minDist = d; owner = i; } From 51a12193c3fdd63873f494484267214647ba1d99 Mon Sep 17 00:00:00 2001 From: Robin Seger Date: Wed, 25 Mar 2026 22:33:13 +0100 Subject: [PATCH 07/45] correction, discard only if owner != myIndex AND mergedSdf > smoothFactor --- plugin/src/Caelestia/Blobs/shaders/blob.frag | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.frag b/plugin/src/Caelestia/Blobs/shaders/blob.frag index b5fb7b4c..90133563 100644 --- a/plugin/src/Caelestia/Blobs/shaders/blob.frag +++ b/plugin/src/Caelestia/Blobs/shaders/blob.frag @@ -203,7 +203,7 @@ void main() { } mergedSdf = sminNoBulge(mergedSdf, d, smoothFactor); - if (d < smoothFactor && d < minDist) { + if (d < minDist) { minDist = d; owner = i; } @@ -277,10 +277,11 @@ void main() { } } - // Each renderer only outputs pixels it owns + // Each renderer only outputs pixels it owns, but allow rendering + // blend zones to prevent gaps (mergedSdf < smoothFactor means in blend) // myIndex == -1: inverted rect renders border-owned pixels // myIndex >= 0: individual rect renders its owned pixels - if (owner != myIndex) + if (owner != myIndex && mergedSdf > smoothFactor) discard; float fw = fwidth(mergedSdf); From 8a5905c9dfd0e0b7d8ea0eaefd0e818e5650efa8 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 26 Mar 2026 15:31:35 +1100 Subject: [PATCH 08/45] fix unqual access --- modules/drawers/Drawers.qml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index ff1d6a4d..7a5b15b3 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -153,6 +153,7 @@ Variants { PanelBg { id: dashBg + blobGroup: blobGroup panel: panels.dashboard bar: bar deformAmount: 0.1 @@ -161,6 +162,7 @@ Variants { PanelBg { id: launcherBg + blobGroup: blobGroup panel: panels.launcher bar: bar deformAmount: 0.1 @@ -169,6 +171,7 @@ Variants { PanelBg { id: sessionBg + blobGroup: blobGroup panel: panels.session bar: bar deformAmount: 0.25 @@ -177,6 +180,7 @@ Variants { PanelBg { id: sidebarBg + blobGroup: blobGroup panel: panels.sidebar bar: bar deformAmount: 0 @@ -185,6 +189,7 @@ Variants { PanelBg { id: osdBg + blobGroup: blobGroup panel: panels.osd bar: bar deformAmount: 0.3 @@ -193,6 +198,7 @@ Variants { PanelBg { id: notifsBg + blobGroup: blobGroup panel: panels.notifications bar: bar } @@ -200,6 +206,7 @@ Variants { PanelBg { id: utilsBg + blobGroup: blobGroup panel: panels.utilities bar: bar } @@ -207,6 +214,7 @@ Variants { PanelBg { id: popoutBg + blobGroup: blobGroup panel: panels.popouts bar: bar @@ -294,6 +302,7 @@ Variants { } component PanelBg: BlobRect { + required property BlobGroup blobGroup required property Item panel required property Item bar property real deformAmount: 0.15 From d7d53260a7b78740673a384e1faf6773c65eb97b Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:13:00 +1100 Subject: [PATCH 09/45] feat: use behavior for launcher anim No need for initial load --- modules/dashboard/Wrapper.qml | 66 ++++++----------------------------- 1 file changed, 10 insertions(+), 56 deletions(-) diff --git a/modules/dashboard/Wrapper.qml b/modules/dashboard/Wrapper.qml index 8448faa6..cb87bf6f 100644 --- a/modules/dashboard/Wrapper.qml +++ b/modules/dashboard/Wrapper.qml @@ -29,63 +29,18 @@ Item { } readonly property real nonAnimHeight: state === "visible" ? ((content.item as Content)?.nonAnimHeight ?? 0) : 0 + readonly property bool shouldBeActive: visibilities.dashboard && Config.dashboard.enabled + property real offsetScale: shouldBeActive ? 0 : 1 - visible: anchors.topMargin > -implicitHeight - 5 - anchors.topMargin: -implicitHeight - 5 + visible: offsetScale < 1 + anchors.topMargin: (-implicitHeight - 5) * offsetScale implicitHeight: content.implicitHeight - implicitWidth: content.implicitWidth + implicitWidth: content.implicitWidth || 854 // Hard coded fallback for first open - onStateChanged: { - if (state === "visible" && timer.running) { - timer.triggered(); - timer.stop(); - } - } - - states: State { - name: "visible" - when: root.visibilities.dashboard && Config.dashboard.enabled - - PropertyChanges { - // root.implicitHeight: content.implicitHeight - root.anchors.topMargin: 0 - } - } - - transitions: [ - Transition { - // from: "" - // to: "visible" - - Anim { - target: root.anchors - property: "topMargin" - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - // Transition { - // from: "visible" - // to: "" - - // Anim { - // target: root.anchors - // property: "topMargin" - // easing.bezierCurve: Appearance.anim.curves.emphasized - // } - // } - - - ] - - Timer { - id: timer - - running: true - interval: Appearance.anim.durations.extraLarge - onTriggered: { - content.active = Qt.binding(() => (root.visibilities.dashboard && Config.dashboard.enabled) || root.visible); - content.visible = true; + Behavior on offsetScale { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } @@ -95,8 +50,7 @@ Item { anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom - visible: false - active: true + active: root.shouldBeActive || root.visible sourceComponent: Content { visibilities: root.visibilities From 528a282e4591d456d7ece792e8c14545bdadf334 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:40:12 +1100 Subject: [PATCH 10/45] feat: improve launcher open No janky stuff, use same Behavior pattern as launcher, and fix height changes breaking. Also allows for no initial load (though we still need to load Apps) And for async launcher loader --- modules/launcher/Wrapper.qml | 100 ++++++----------------------------- 1 file changed, 15 insertions(+), 85 deletions(-) diff --git a/modules/launcher/Wrapper.qml b/modules/launcher/Wrapper.qml index f5c866af..6c9e09f3 100644 --- a/modules/launcher/Wrapper.qml +++ b/modules/launcher/Wrapper.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell +import qs.modules.launcher.services import qs.components import qs.config @@ -13,7 +14,6 @@ Item { required property var panels readonly property bool shouldBeActive: visibilities.launcher && Config.launcher.enabled - property int contentHeight readonly property real maxHeight: { let max = screen.height - Config.border.thickness * 2 - Appearance.spacing.large; @@ -22,94 +22,27 @@ Item { return max; } - onMaxHeightChanged: timer.start() - - visible: anchors.bottomMargin > -implicitHeight - 5 - anchors.bottomMargin: -implicitHeight - 5 - implicitHeight: content.implicitHeight - implicitWidth: content.implicitWidth + property real offsetScale: shouldBeActive ? 0 : 1 onShouldBeActiveChanged: { - if (shouldBeActive) { - timer.stop(); - hideAnim.stop(); - showAnim.start(); - } else { - showAnim.stop(); - hideAnim.start(); - } + if (shouldBeActive) + implicitHeight = Qt.binding(() => content.implicitHeight); + else + implicitHeight = implicitHeight; // Break binding during close anim } - SequentialAnimation { - id: showAnim + visible: offsetScale < 1 + anchors.bottomMargin: (-implicitHeight - 5) * offsetScale + implicitHeight: content.implicitHeight + implicitWidth: content.implicitWidth || 630 // Hard coded fallback for first open + Component.onCompleted: Qt.callLater(() => Apps) // Load apps on init + + Behavior on offsetScale { Anim { - target: root.anchors - property: "bottomMargin" - to: 0 duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } - // ScriptAction { - // script: root.implicitHeight = Qt.binding(() => content.implicitHeight) - // } - } - - SequentialAnimation { - id: hideAnim - - // ScriptAction { - // script: root.implicitHeight = root.implicitHeight - // } - Anim { - target: root.anchors - property: "bottomMargin" - to: -content.implicitHeight - 5 - // easing.bezierCurve: Appearance.anim.curves.emphasized - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - - Connections { - function onEnabledChanged(): void { - timer.start(); - } - - function onMaxShownChanged(): void { - timer.start(); - } - - target: Config.launcher - } - - Connections { - function onValuesChanged(): void { - if (DesktopEntries.applications.values.length < Config.launcher.maxShown) - timer.start(); - } - - target: DesktopEntries.applications - } - - Timer { - id: timer - - interval: Appearance.anim.durations.extraLarge - onRunningChanged: { - if (running && !root.shouldBeActive) { - content.visible = false; - content.active = true; - } else { - root.contentHeight = Math.min(root.maxHeight, content.implicitHeight); - content.active = Qt.binding(() => root.shouldBeActive || root.visible); - content.visible = true; - if (showAnim.running) { - showAnim.stop(); - showAnim.start(); - } - } - } } Loader { @@ -118,16 +51,13 @@ Item { anchors.top: parent.top anchors.horizontalCenter: parent.horizontalCenter - visible: false - active: false - Component.onCompleted: timer.start() + asynchronous: true + active: root.shouldBeActive || root.visible sourceComponent: Content { visibilities: root.visibilities panels: root.panels maxHeight: root.maxHeight - - Component.onCompleted: root.contentHeight = implicitHeight } } } From 600182ae002b21919f4c376348df88bee8194bfc Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:46:28 +1100 Subject: [PATCH 11/45] feat: improve utilities open/close --- modules/utilities/Wrapper.qml | 67 ++++++----------------------------- 1 file changed, 11 insertions(+), 56 deletions(-) diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index 59288f5d..048de1b4 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -21,77 +21,32 @@ Item { reloadableId: "utilities" } readonly property bool shouldBeActive: visibilities.sidebar || (visibilities.utilities && Config.utilities.enabled && !(visibilities.session && Config.session.enabled)) + property real offsetScale: shouldBeActive ? 0 : 1 - visible: anchors.bottomMargin > -implicitHeight - 5 - anchors.bottomMargin: -implicitHeight - 5 - implicitHeight: content.implicitHeight + visible: offsetScale < 1 + anchors.bottomMargin: (-implicitHeight - 5) * offsetScale + implicitHeight: content.implicitHeight + content.anchors.margins * 2 implicitWidth: sidebar.visible ? sidebar.width : Config.utilities.sizes.width - onStateChanged: { - if (state === "visible" && timer.running) { - timer.triggered(); - timer.stop(); - } - } - - states: State { - name: "visible" - when: root.shouldBeActive - - PropertyChanges { - root.anchors.bottomMargin: 0 - } - } - - transitions: [ - Transition { - // from: "" - // to: "visible" - - Anim { - target: root.anchors - property: "bottomMargin" - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - // Transition { - // from: "visible" - // to: "" - - // Anim { - // target: root - // property: "implicitHeight" - // easing.bezierCurve: Appearance.anim.curves.emphasized - // } - // } - - ] - - Timer { - id: timer - - running: true - interval: Appearance.anim.durations.extraLarge - onTriggered: { - content.active = Qt.binding(() => root.shouldBeActive || root.visible); - content.visible = true; + Behavior on offsetScale { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } Loader { id: content - asynchronous: true anchors.top: parent.top anchors.left: parent.left anchors.margins: Appearance.padding.large - visible: false - active: true + asynchronous: true + active: root.shouldBeActive || root.visible sourceComponent: Content { - implicitWidth: root.implicitWidth - Appearance.padding.large * 2 + implicitWidth: root.implicitWidth - content.anchors.margins * 2 props: root.props visibilities: root.visibilities popouts: root.popouts From ea2ed3e5c4f17569c9cd3ebfd0e53680205ebcc4 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:22:51 +1100 Subject: [PATCH 12/45] fix: graphical artifact during sdf deformation --- plugin/src/Caelestia/Blobs/shaders/blob.frag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.frag b/plugin/src/Caelestia/Blobs/shaders/blob.frag index 90133563..f6387a2f 100644 --- a/plugin/src/Caelestia/Blobs/shaders/blob.frag +++ b/plugin/src/Caelestia/Blobs/shaders/blob.frag @@ -203,7 +203,7 @@ void main() { } mergedSdf = sminNoBulge(mergedSdf, d, smoothFactor); - if (d < minDist) { + if (d < smoothFactor && d < minDist) { minDist = d; owner = i; } From 0075d64ca27d3ffdd396827a6c0b6143542e983f Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 26 Mar 2026 21:01:40 +1100 Subject: [PATCH 13/45] fix: osd and session clipping --- modules/drawers/Drawers.qml | 6 ++-- modules/drawers/Interactions.qml | 16 ++++----- modules/drawers/Panels.qml | 56 ++++++++++++++++++++++---------- modules/osd/Wrapper.qml | 48 +++++++++------------------ modules/session/Wrapper.qml | 52 ++++++++++------------------- modules/sidebar/Wrapper.qml | 47 ++++++--------------------- 6 files changed, 95 insertions(+), 130 deletions(-) diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 7a5b15b3..f4f4fc71 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -172,9 +172,10 @@ Variants { id: sessionBg blobGroup: blobGroup - panel: panels.session + panel: panels.sessionWrapper bar: bar deformAmount: 0.25 + x: panels.sessionWrapper.x + panels.session.x + bar.implicitWidth } PanelBg { @@ -190,9 +191,10 @@ Variants { id: osdBg blobGroup: blobGroup - panel: panels.osd + panel: panels.osdWrapper bar: bar deformAmount: 0.3 + x: panels.osdWrapper.x + panels.osd.x + bar.implicitWidth } PanelBg { diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index 6008a853..c906b9d1 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -104,7 +104,7 @@ CustomMouseArea { if (panels.sidebar.anchors.rightMargin === -panels.sidebar.implicitWidth - 5) { // Show osd on hover - const showOsd = inRightPanel(panels.osd, x, y); + const showOsd = inRightPanel(panels.osdWrapper, x, y); // Always update visibility based on hover if not in shortcut mode if (!osdShortcutActive) { @@ -119,23 +119,23 @@ CustomMouseArea { const showSidebar = pressed && dragStart.x > Math.min(width - Config.border.minThickness, bar.implicitWidth + panels.sidebar.x); // Show/hide session on drag - if (pressed && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) { + if (pressed && inRightPanel(panels.sessionWrapper, dragStart.x, dragStart.y) && withinPanelHeight(panels.sessionWrapper, x, y)) { if (dragX < -Config.session.dragThreshold) visibilities.session = true; else if (dragX > Config.session.dragThreshold) visibilities.session = false; // Show sidebar on drag if in session area and session is nearly fully visible - if (showSidebar && panels.session.width >= panels.session.nonAnimWidth && dragX < -Config.sidebar.dragThreshold) + if (showSidebar && panels.session.offsetScale <= 0 && dragX < -Config.sidebar.dragThreshold) visibilities.sidebar = true; } else if (showSidebar && dragX < -Config.sidebar.dragThreshold) { // Show sidebar on drag if not in session area visibilities.sidebar = true; } } else { - const outOfSidebar = x < width - panels.sidebar.width; + const outOfSidebar = x < width - panels.sidebar.width * (1 - panels.sidebar.offsetScale); // Show osd on hover - const showOsd = outOfSidebar && inRightPanel(panels.osd, x, y); + const showOsd = outOfSidebar && inRightPanel(panels.osdWrapper, x, y); // Always update visibility based on hover if not in shortcut mode if (!osdShortcutActive) { @@ -148,7 +148,7 @@ CustomMouseArea { } // Show/hide session on drag - if (pressed && outOfSidebar && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) { + if (pressed && outOfSidebar && inRightPanel(panels.sessionWrapper, dragStart.x, dragStart.y) && withinPanelHeight(panels.sessionWrapper, x, y)) { if (dragX < -Config.session.dragThreshold) visibilities.session = true; else if (dragX > Config.session.dragThreshold) @@ -221,7 +221,7 @@ CustomMouseArea { // Also hide dashboard and OSD if they're not being hovered const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY); - const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); + const inOsdArea = root.inRightPanel(root.panels.osdWrapper, root.mouseX, root.mouseY); if (!inDashboardArea) { root.visibilities.dashboard = false; @@ -249,7 +249,7 @@ CustomMouseArea { function onOsdChanged() { if (root.visibilities.osd) { // OSD became visible, immediately check if this should be shortcut mode - const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); + const inOsdArea = root.inRightPanel(root.panels.osdWrapper, root.mouseX, root.mouseY); if (!inOsdArea) { root.osdShortcutActive = true; } diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index 006205fd..15140103 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -21,8 +21,10 @@ Item { required property Bar.BarWrapper bar readonly property alias osd: osd + readonly property alias osdWrapper: osdWrapper readonly property alias notifications: notifications readonly property alias session: session + readonly property alias sessionWrapper: sessionWrapper readonly property alias launcher: launcher readonly property alias dashboard: dashboard readonly property alias popouts: popouts @@ -34,16 +36,27 @@ Item { anchors.margins: Config.border.thickness anchors.leftMargin: bar.implicitWidth - Osd.Wrapper { - id: osd - - clip: session.width > 0 || sidebar.width > 0 - screen: root.screen - visibilities: root.visibilities + Item { + id: osdWrapper anchors.verticalCenter: parent.verticalCenter - anchors.right: session.left - // anchors.rightMargin: session.width + sidebar.width + anchors.right: parent.right + anchors.rightMargin: sessionWrapper.anchors.rightMargin + session.width * (1 - session.offsetScale) + clip: sidebar.visible || session.visible + + implicitWidth: osd.implicitWidth + implicitHeight: osd.implicitHeight + + Osd.Wrapper { + id: osd + + screen: root.screen + visibilities: root.visibilities + sidebarOrSessionVisible: sidebar.visible || session.visible + + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + } } Notifications.Wrapper { @@ -58,16 +71,26 @@ Item { anchors.right: parent.right } - Session.Wrapper { - id: session - - clip: sidebar.width > 0 - visibilities: root.visibilities - panels: root + Item { + id: sessionWrapper anchors.verticalCenter: parent.verticalCenter - anchors.right: sidebar.left - // anchors.rightMargin: sidebar.width + anchors.right: parent.right + anchors.rightMargin: sidebar.width * (1 - sidebar.offsetScale) + clip: sidebar.visible + + implicitWidth: session.implicitWidth + implicitHeight: session.implicitHeight + + Session.Wrapper { + id: session + + visibilities: root.visibilities + sidebarVisible: sidebar.visible + + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + } } Launcher.Wrapper { @@ -131,7 +154,6 @@ Item { id: sidebar visibilities: root.visibilities - panels: root anchors.top: notifications.bottom anchors.bottom: utilities.top diff --git a/modules/osd/Wrapper.qml b/modules/osd/Wrapper.qml index 87350d92..39215cd8 100644 --- a/modules/osd/Wrapper.qml +++ b/modules/osd/Wrapper.qml @@ -11,9 +11,13 @@ Item { required property ShellScreen screen required property DrawerVisibilities visibilities + required property bool sidebarOrSessionVisible + property bool hovered readonly property Brightness.Monitor monitor: Brightness.getMonitorForScreen(root.screen) readonly property bool shouldBeActive: visibilities.osd && Config.osd.enabled && !(visibilities.utilities && Config.utilities.enabled) + property real offsetScale: shouldBeActive ? 0 : 1 + property real sidebarOffset: !shouldBeActive && sidebarOrSessionVisible ? 16 : 0 property real volume property bool muted @@ -34,44 +38,24 @@ Item { brightness = root.monitor?.brightness ?? 0; } - visible: anchors.rightMargin > -implicitWidth - anchors.rightMargin: -implicitWidth + visible: offsetScale < 1 + anchors.rightMargin: (-implicitWidth - 5 - sidebarOffset) * offsetScale implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight - states: State { - name: "visible" - when: root.shouldBeActive - - PropertyChanges { - root.anchors.rightMargin: 0 + Behavior on offsetScale { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } - transitions: [ - Transition { - // from: "" - // to: "visible" - - Anim { - target: root.anchors - property: "rightMargin" - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } + Behavior on sidebarOffset { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } - // Transition { - // from: "visible" - // to: "" - - // Anim { - // target: root - // property: "implicitWidth" - // easing.bezierCurve: Appearance.anim.curves.emphasized - // } - // } - - - ] + } Connections { function onMutedChanged(): void { @@ -122,7 +106,7 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left - Component.onCompleted: active = Qt.binding(() => root.shouldBeActive || root.visible) + active: root.shouldBeActive || root.visible sourceComponent: Content { monitor: root.monitor diff --git a/modules/session/Wrapper.qml b/modules/session/Wrapper.qml index b829409b..32d6fea2 100644 --- a/modules/session/Wrapper.qml +++ b/modules/session/Wrapper.qml @@ -8,47 +8,31 @@ Item { id: root required property DrawerVisibilities visibilities - required property var panels + required property bool sidebarVisible readonly property real nonAnimWidth: content.implicitWidth - visible: anchors.rightMargin > -implicitWidth - 1 - anchors.rightMargin: -implicitWidth - 1 + readonly property bool shouldBeActive: visibilities.session && Config.session.enabled + property real offsetScale: shouldBeActive ? 0 : 1 + property real sidebarOffset: !shouldBeActive && sidebarVisible ? 14 : 0 // TODO: there is clearly something wrong with the rect to rect edge sink + + visible: offsetScale < 1 + anchors.rightMargin: (-implicitWidth - 5 - sidebarOffset) * offsetScale implicitWidth: content.implicitWidth - implicitHeight: content.implicitHeight + implicitHeight: content.implicitHeight || 510 // Hard coded fallback for first open - states: State { - name: "visible" - when: root.visibilities.session && Config.session.enabled - - PropertyChanges { - root.anchors.rightMargin: 0 + Behavior on offsetScale { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } - transitions: [ - Transition { - // from: "" - // to: "visible" - - Anim { - target: root.anchors - property: "rightMargin" - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } + Behavior on sidebarOffset { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } - // Transition { - // from: "visible" - // to: "" - - // Anim { - // target: root - // property: "implicitWidth" - // easing.bezierCurve: root.panels.osd.width > 0 ? Appearance.anim.curves.expressiveDefaultSpatial : Appearance.anim.curves.emphasized - // } - // } - - - ] + } Loader { id: content @@ -56,7 +40,7 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left - Component.onCompleted: active = Qt.binding(() => (root.visibilities.session && Config.session.enabled) || root.visible) + active: root.shouldBeActive || root.visible sourceComponent: Content { visibilities: root.visibilities diff --git a/modules/sidebar/Wrapper.qml b/modules/sidebar/Wrapper.qml index 67929bd7..4a777ca3 100644 --- a/modules/sidebar/Wrapper.qml +++ b/modules/sidebar/Wrapper.qml @@ -8,48 +8,22 @@ Item { id: root required property DrawerVisibilities visibilities - required property var panels readonly property Props props: Props {} - visible: anchors.rightMargin > -implicitWidth - 5 - anchors.rightMargin: -implicitWidth - 5 + readonly property bool shouldBeActive: visibilities.sidebar && Config.sidebar.enabled + property real offsetScale: shouldBeActive ? 0 : 1 + + visible: offsetScale < 1 + anchors.rightMargin: (-implicitWidth - 5) * offsetScale implicitWidth: Config.sidebar.sizes.width - states: State { - name: "visible" - when: root.visibilities.sidebar && Config.sidebar.enabled - - PropertyChanges { - root.anchors.rightMargin: 0 + Behavior on offsetScale { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } - transitions: [ - Transition { - // from: "" - // to: "visible" - - Anim { - target: root.anchors - property: "rightMargin" - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - // Transition { - // from: "visible" - // to: "" - - // Anim { - // target: root - // property: "implicitWidth" - // easing.bezierCurve: root.panels.osd.width > 0 || root.panels.session.width > 0 ? Appearance.anim.curves.expressiveDefaultSpatial : Appearance.anim.curves.emphasized - // } - // } - - - ] - Loader { id: content @@ -59,8 +33,7 @@ Item { anchors.margins: Appearance.padding.large anchors.bottomMargin: 0 - active: true - Component.onCompleted: active = Qt.binding(() => (root.visibilities.sidebar && Config.sidebar.enabled) || root.visible) + active: root.shouldBeActive || root.visible sourceComponent: Content { implicitWidth: Config.sidebar.sizes.width - Appearance.padding.large * 2 From e8ffd51442fa5e190a6c3972c92b03ed9c2a1fc2 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 27 Mar 2026 15:21:04 +1100 Subject: [PATCH 14/45] fix: window mask regions during overshoot The window mask regions followed the panel positions exactly, so when they overshoot due to the open anim there will be a gap at the top. --- modules/drawers/Drawers.qml | 28 ++----------- modules/drawers/Regions.qml | 80 +++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 24 deletions(-) create mode 100644 modules/drawers/Regions.qml diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index f4f4fc71..c1d684f2 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -58,14 +58,10 @@ Variants { WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None - mask: Region { - x: bar.clampedWidth + win.dragMaskPadding - y: Config.border.clampedThickness + win.dragMaskPadding - width: win.width - bar.clampedWidth - Config.border.clampedThickness - win.dragMaskPadding * 2 - height: win.height - Config.border.clampedThickness * 2 - win.dragMaskPadding * 2 - intersection: Intersection.Xor - - regions: regions.instances // qmllint disable stale-property-read + mask: Regions { + bar: bar + panels: panels + win: win } anchors.top: true @@ -73,22 +69,6 @@ Variants { anchors.left: true anchors.right: true - Variants { - id: regions - - model: panels.children - - Region { - required property Item modelData - - x: modelData.x + bar.implicitWidth - y: modelData.y + Config.border.thickness - width: modelData.width - height: modelData.height - intersection: Intersection.Subtract - } - } - HyprlandFocusGrab { id: focusGrab diff --git a/modules/drawers/Regions.qml b/modules/drawers/Regions.qml new file mode 100644 index 00000000..c646809f --- /dev/null +++ b/modules/drawers/Regions.qml @@ -0,0 +1,80 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import qs.config +import qs.modules.bar as Bar + +Region { + id: root + + required property Bar.BarWrapper bar + required property Panels panels + required property var win + + x: bar.clampedWidth + win.dragMaskPadding + y: Config.border.clampedThickness + win.dragMaskPadding + width: win.width - bar.clampedWidth - Config.border.clampedThickness - win.dragMaskPadding * 2 + height: win.height - Config.border.clampedThickness * 2 - win.dragMaskPadding * 2 + intersection: Intersection.Xor + + R { + panel: root.panels.dashboard + y: 0 + height: panel.height * (1 - root.panels.dashboard.offsetScale) + Config.border.thickness + } + + R { + panel: root.panels.launcher + y: root.win.height - height + height: panel.height * (1 - root.panels.launcher.offsetScale) + Config.border.thickness + } + + R { + id: sessionRegion + + panel: root.panels.sessionWrapper + x: root.win.width - width + width: panel.width * (1 - root.panels.session.offsetScale) + Config.border.thickness + sidebarRegion.width + } + + R { + id: sidebarRegion + + panel: root.panels.sidebar + x: root.win.width - width + width: panel.width * (1 - root.panels.sidebar.offsetScale) + Config.border.thickness + } + + R { + panel: root.panels.osdWrapper + x: root.win.width - width + width: panel.width * (1 - root.panels.osd.offsetScale) + Config.border.thickness + sessionRegion.width + } + + R { + panel: root.panels.notifications + y: 0 + height: panel.height + Config.border.thickness + } + + R { + panel: root.panels.utilities + y: root.win.height - height + height: panel.height * (1 - root.panels.utilities.offsetScale) + Config.border.thickness + } + + R { + panel: root.panels.popouts + } + + component R: Region { + required property Item panel + + x: panel.x + root.bar.implicitWidth + y: panel.y + Config.border.thickness + width: panel.width + height: panel.height + intersection: Intersection.Subtract + } +} From 2c5e433b73c2001c6c7d084757b8e2f9a4ef715f Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 27 Mar 2026 15:21:32 +1100 Subject: [PATCH 15/45] fix: clean up notif wrapper and format --- modules/launcher/Wrapper.qml | 2 +- modules/notifications/Wrapper.qml | 21 +-------------------- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/modules/launcher/Wrapper.qml b/modules/launcher/Wrapper.qml index 6c9e09f3..472667e0 100644 --- a/modules/launcher/Wrapper.qml +++ b/modules/launcher/Wrapper.qml @@ -2,9 +2,9 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell -import qs.modules.launcher.services import qs.components import qs.config +import qs.modules.launcher.services Item { id: root diff --git a/modules/notifications/Wrapper.qml b/modules/notifications/Wrapper.qml index c0756b9a..97417e9c 100644 --- a/modules/notifications/Wrapper.qml +++ b/modules/notifications/Wrapper.qml @@ -1,6 +1,5 @@ import QtQuick import qs.components -import qs.config Item { id: root @@ -10,29 +9,11 @@ Item { property alias osdPanel: content.osdPanel property alias sessionPanel: content.sessionPanel - visible: anchors.topMargin > -implicitHeight - 5 + visible: height > 0 anchors.topMargin: -5 implicitWidth: Math.max(sidebarPanel.width, content.implicitWidth) implicitHeight: content.implicitHeight - states: State { - name: "hidden" - // when: root.visibilities.sidebar && Config.sidebar.enabled - - PropertyChanges { - root.anchors.topMargin: -implicitHeight - 5 - } - } - - transitions: Transition { - Anim { - target: root.anchors - property: "topMargin" - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - Content { id: content From f2d023b13141541046faf6aadd9ca8d7d3e2382e Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:56:55 +1100 Subject: [PATCH 16/45] fix: osd hover area + popup notif osd/session dodging --- modules/drawers/Drawers.qml | 2 ++ modules/drawers/Interactions.qml | 2 +- modules/drawers/Panels.qml | 8 ++++---- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index c1d684f2..5211fdcf 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -156,6 +156,7 @@ Variants { bar: bar deformAmount: 0.25 x: panels.sessionWrapper.x + panels.session.x + bar.implicitWidth + implicitWidth: panels.session.width } PanelBg { @@ -175,6 +176,7 @@ Variants { bar: bar deformAmount: 0.3 x: panels.osdWrapper.x + panels.osd.x + bar.implicitWidth + implicitWidth: panels.osd.width } PanelBg { diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index c906b9d1..fbf8ccad 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -102,7 +102,7 @@ CustomMouseArea { visibilities.bar = false; } - if (panels.sidebar.anchors.rightMargin === -panels.sidebar.implicitWidth - 5) { + if (panels.sidebar.offsetScale === 1) { // Show osd on hover const showOsd = inRightPanel(panels.osdWrapper, x, y); diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index 15140103..01144d09 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -44,7 +44,7 @@ Item { anchors.rightMargin: sessionWrapper.anchors.rightMargin + session.width * (1 - session.offsetScale) clip: sidebar.visible || session.visible - implicitWidth: osd.implicitWidth + implicitWidth: osd.implicitWidth * (1 - osd.offsetScale) implicitHeight: osd.implicitHeight Osd.Wrapper { @@ -64,8 +64,8 @@ Item { visibilities: root.visibilities sidebarPanel: sidebar - osdPanel: osd - sessionPanel: session + osdPanel: osdWrapper + sessionPanel: sessionWrapper anchors.top: parent.top anchors.right: parent.right @@ -79,7 +79,7 @@ Item { anchors.rightMargin: sidebar.width * (1 - sidebar.offsetScale) clip: sidebar.visible - implicitWidth: session.implicitWidth + implicitWidth: session.implicitWidth * (1 - session.offsetScale) implicitHeight: session.implicitHeight Session.Wrapper { From 14437cc1edf4b6417536227eb9c89be5dd3a1b7b Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:03:55 +1100 Subject: [PATCH 17/45] fix: remove osd and session wrapper hacks The panels are ignored if they are closed, so they don't produce bulges. This does remove the sink effect on overshoot, but that didn't work anyways. (also that change was part of the previous commit) --- modules/osd/Wrapper.qml | 10 +--------- modules/session/Wrapper.qml | 10 +--------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/modules/osd/Wrapper.qml b/modules/osd/Wrapper.qml index 39215cd8..db72c3e5 100644 --- a/modules/osd/Wrapper.qml +++ b/modules/osd/Wrapper.qml @@ -17,7 +17,6 @@ Item { readonly property Brightness.Monitor monitor: Brightness.getMonitorForScreen(root.screen) readonly property bool shouldBeActive: visibilities.osd && Config.osd.enabled && !(visibilities.utilities && Config.utilities.enabled) property real offsetScale: shouldBeActive ? 0 : 1 - property real sidebarOffset: !shouldBeActive && sidebarOrSessionVisible ? 16 : 0 property real volume property bool muted @@ -39,7 +38,7 @@ Item { } visible: offsetScale < 1 - anchors.rightMargin: (-implicitWidth - 5 - sidebarOffset) * offsetScale + anchors.rightMargin: (-implicitWidth - 5) * offsetScale implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight @@ -50,13 +49,6 @@ Item { } } - Behavior on sidebarOffset { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - Connections { function onMutedChanged(): void { root.show(); diff --git a/modules/session/Wrapper.qml b/modules/session/Wrapper.qml index 32d6fea2..ab929c01 100644 --- a/modules/session/Wrapper.qml +++ b/modules/session/Wrapper.qml @@ -13,10 +13,9 @@ Item { readonly property bool shouldBeActive: visibilities.session && Config.session.enabled property real offsetScale: shouldBeActive ? 0 : 1 - property real sidebarOffset: !shouldBeActive && sidebarVisible ? 14 : 0 // TODO: there is clearly something wrong with the rect to rect edge sink visible: offsetScale < 1 - anchors.rightMargin: (-implicitWidth - 5 - sidebarOffset) * offsetScale + anchors.rightMargin: (-implicitWidth - 5) * offsetScale implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight || 510 // Hard coded fallback for first open @@ -27,13 +26,6 @@ Item { } } - Behavior on sidebarOffset { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - Loader { id: content From c4399efbe4f2583dea977018ec6d387744ad3559 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:17:49 +1100 Subject: [PATCH 18/45] fix: launcher anim Async launcher makes anim worse cause height gets recalculated during anim, removing overshoot and slowing down curve --- modules/launcher/Wrapper.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/launcher/Wrapper.qml b/modules/launcher/Wrapper.qml index 472667e0..0c9871aa 100644 --- a/modules/launcher/Wrapper.qml +++ b/modules/launcher/Wrapper.qml @@ -51,7 +51,6 @@ Item { anchors.top: parent.top anchors.horizontalCenter: parent.horizontalCenter - asynchronous: true active: root.shouldBeActive || root.visible sourceComponent: Content { From ff574737818112487333b0ad4f1fdd0b1bbdcaa5 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:23:54 +1100 Subject: [PATCH 19/45] fix: account for bottom panel offset when checking hover Basically only affects utilities panel cause it's in the corner --- modules/drawers/Interactions.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index fbf8ccad..15958ea1 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -44,7 +44,8 @@ CustomMouseArea { } function inBottomPanel(panel: Item, x: real, y: real, isCorner = false): bool { - return y > height - Math.max(Config.border.minThickness, Config.border.thickness + panel.height) - (isCorner ? Config.border.rounding : 0) && withinPanelWidth(panel, x, y); + const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0)); // qmllint disable missing-property + return y > height - Math.max(Config.border.minThickness, Config.border.thickness + panelHeight) - (isCorner ? Config.border.rounding : 0) && withinPanelWidth(panel, x, y); } function onWheel(event: WheelEvent): void { From 0f5d2ddc16046437f229117804da5d5f6b7c18b3 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 27 Mar 2026 20:54:52 +1100 Subject: [PATCH 20/45] feat: fade in/out drawers --- modules/dashboard/Wrapper.qml | 1 + modules/launcher/Wrapper.qml | 1 + modules/osd/Wrapper.qml | 2 ++ modules/session/Wrapper.qml | 1 + modules/sidebar/Wrapper.qml | 1 + modules/utilities/Wrapper.qml | 1 + 6 files changed, 7 insertions(+) diff --git a/modules/dashboard/Wrapper.qml b/modules/dashboard/Wrapper.qml index cb87bf6f..d411d498 100644 --- a/modules/dashboard/Wrapper.qml +++ b/modules/dashboard/Wrapper.qml @@ -36,6 +36,7 @@ Item { anchors.topMargin: (-implicitHeight - 5) * offsetScale implicitHeight: content.implicitHeight implicitWidth: content.implicitWidth || 854 // Hard coded fallback for first open + opacity: 1 - offsetScale Behavior on offsetScale { Anim { diff --git a/modules/launcher/Wrapper.qml b/modules/launcher/Wrapper.qml index 0c9871aa..5dfffeae 100644 --- a/modules/launcher/Wrapper.qml +++ b/modules/launcher/Wrapper.qml @@ -35,6 +35,7 @@ Item { anchors.bottomMargin: (-implicitHeight - 5) * offsetScale implicitHeight: content.implicitHeight implicitWidth: content.implicitWidth || 630 // Hard coded fallback for first open + opacity: 1 - offsetScale Component.onCompleted: Qt.callLater(() => Apps) // Load apps on init diff --git a/modules/osd/Wrapper.qml b/modules/osd/Wrapper.qml index db72c3e5..585fb959 100644 --- a/modules/osd/Wrapper.qml +++ b/modules/osd/Wrapper.qml @@ -41,6 +41,7 @@ Item { anchors.rightMargin: (-implicitWidth - 5) * offsetScale implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight + opacity: 1 - offsetScale Behavior on offsetScale { Anim { @@ -98,6 +99,7 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left + asynchronous: true active: root.shouldBeActive || root.visible sourceComponent: Content { diff --git a/modules/session/Wrapper.qml b/modules/session/Wrapper.qml index ab929c01..e48fff60 100644 --- a/modules/session/Wrapper.qml +++ b/modules/session/Wrapper.qml @@ -18,6 +18,7 @@ Item { anchors.rightMargin: (-implicitWidth - 5) * offsetScale implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight || 510 // Hard coded fallback for first open + opacity: 1 - offsetScale Behavior on offsetScale { Anim { diff --git a/modules/sidebar/Wrapper.qml b/modules/sidebar/Wrapper.qml index 4a777ca3..eefd11a0 100644 --- a/modules/sidebar/Wrapper.qml +++ b/modules/sidebar/Wrapper.qml @@ -16,6 +16,7 @@ Item { visible: offsetScale < 1 anchors.rightMargin: (-implicitWidth - 5) * offsetScale implicitWidth: Config.sidebar.sizes.width + opacity: 1 - offsetScale Behavior on offsetScale { Anim { diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index 048de1b4..b0aec96e 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -27,6 +27,7 @@ Item { anchors.bottomMargin: (-implicitHeight - 5) * offsetScale implicitHeight: content.implicitHeight + content.anchors.margins * 2 implicitWidth: sidebar.visible ? sidebar.width : Config.utilities.sizes.width + opacity: 1 - offsetScale Behavior on offsetScale { Anim { From e8555061aba2708198e43d2eeb09038500271436 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 27 Mar 2026 20:58:00 +1100 Subject: [PATCH 21/45] fix: sidebar bulge at connection with utilities Basically make sidebar and utilities exclude each other from merging and manually animate corner radius Also format c++ --- modules/drawers/Drawers.qml | 17 +++ plugin/src/Caelestia/Blobs/blobgroup.cpp | 13 +- plugin/src/Caelestia/Blobs/blobgroup.hpp | 3 +- .../src/Caelestia/Blobs/blobinvertedrect.cpp | 49 ++++--- .../src/Caelestia/Blobs/blobinvertedrect.hpp | 12 +- plugin/src/Caelestia/Blobs/blobmaterial.cpp | 10 +- plugin/src/Caelestia/Blobs/blobmaterial.hpp | 11 +- plugin/src/Caelestia/Blobs/blobrect.cpp | 138 +++++++++++++++--- plugin/src/Caelestia/Blobs/blobrect.hpp | 55 ++++++- plugin/src/Caelestia/Blobs/blobshape.cpp | 119 +++++++-------- plugin/src/Caelestia/Blobs/blobshape.hpp | 10 +- plugin/src/Caelestia/Blobs/shaders/blob.frag | 7 +- 12 files changed, 286 insertions(+), 158 deletions(-) diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 5211fdcf..23be6f03 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -166,6 +166,15 @@ Variants { panel: panels.sidebar bar: bar deformAmount: 0 + height: panel.height + 1 + exclude: [utilsBg] + bottomLeftRadius: panels.sidebar.visible ? 0 : radius + + Behavior on bottomLeftRadius { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + } + } } PanelBg { @@ -193,6 +202,14 @@ Variants { blobGroup: blobGroup panel: panels.utilities bar: bar + exclude: [sidebarBg] + topLeftRadius: panels.sidebar.visible ? 0 : radius + + Behavior on topLeftRadius { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + } + } } PanelBg { diff --git a/plugin/src/Caelestia/Blobs/blobgroup.cpp b/plugin/src/Caelestia/Blobs/blobgroup.cpp index ab4a31e6..a4703c86 100644 --- a/plugin/src/Caelestia/Blobs/blobgroup.cpp +++ b/plugin/src/Caelestia/Blobs/blobgroup.cpp @@ -74,20 +74,15 @@ void BlobGroup::markShapeDirty(BlobShape* source) { // Use cached padded rects to find spatial neighbors const float pad = static_cast(m_smoothing) * 2.0f; - const QRectF srcRect( - static_cast(source->m_cachedPaddedX - pad), - static_cast(source->m_cachedPaddedY - pad), - static_cast(source->m_cachedPaddedW + pad * 2.0f), + const QRectF srcRect(static_cast(source->m_cachedPaddedX - pad), + static_cast(source->m_cachedPaddedY - pad), static_cast(source->m_cachedPaddedW + pad * 2.0f), static_cast(source->m_cachedPaddedH + pad * 2.0f)); for (auto* shape : std::as_const(m_shapes)) { if (shape == source) continue; - const QRectF otherRect( - static_cast(shape->m_cachedPaddedX), - static_cast(shape->m_cachedPaddedY), - static_cast(shape->m_cachedPaddedW), - static_cast(shape->m_cachedPaddedH)); + const QRectF otherRect(static_cast(shape->m_cachedPaddedX), static_cast(shape->m_cachedPaddedY), + static_cast(shape->m_cachedPaddedW), static_cast(shape->m_cachedPaddedH)); if (srcRect.intersects(otherRect)) { shape->polish(); shape->update(); diff --git a/plugin/src/Caelestia/Blobs/blobgroup.hpp b/plugin/src/Caelestia/Blobs/blobgroup.hpp index b2e83edb..e09125a9 100644 --- a/plugin/src/Caelestia/Blobs/blobgroup.hpp +++ b/plugin/src/Caelestia/Blobs/blobgroup.hpp @@ -11,8 +11,7 @@ class BlobInvertedRect; class BlobGroup : public QObject { Q_OBJECT QML_ELEMENT - Q_PROPERTY(qreal smoothing READ smoothing WRITE setSmoothing NOTIFY - smoothingChanged) + Q_PROPERTY(qreal smoothing READ smoothing WRITE setSmoothing NOTIFY smoothingChanged) Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) public: diff --git a/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp b/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp index 3e392b4a..46ee73a4 100644 --- a/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp +++ b/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp @@ -13,21 +13,36 @@ BlobInvertedRect::BlobInvertedRect(QQuickItem* parent) static void setFrameIndices(quint16* idx) { // Top strip: 0-1-4, 1-5-4 - idx[0] = 0; idx[1] = 1; idx[2] = 4; - idx[3] = 1; idx[4] = 5; idx[5] = 4; + idx[0] = 0; + idx[1] = 1; + idx[2] = 4; + idx[3] = 1; + idx[4] = 5; + idx[5] = 4; // Right strip: 1-2-5, 2-6-5 - idx[6] = 1; idx[7] = 2; idx[8] = 5; - idx[9] = 2; idx[10] = 6; idx[11] = 5; + idx[6] = 1; + idx[7] = 2; + idx[8] = 5; + idx[9] = 2; + idx[10] = 6; + idx[11] = 5; // Bottom strip: 2-3-6, 3-7-6 - idx[12] = 2; idx[13] = 3; idx[14] = 6; - idx[15] = 3; idx[16] = 7; idx[17] = 6; + idx[12] = 2; + idx[13] = 3; + idx[14] = 6; + idx[15] = 3; + idx[16] = 7; + idx[17] = 6; // Left strip: 3-0-7, 0-4-7 - idx[18] = 3; idx[19] = 0; idx[20] = 7; - idx[21] = 0; idx[22] = 4; idx[23] = 7; + idx[18] = 3; + idx[19] = 0; + idx[20] = 7; + idx[21] = 0; + idx[22] = 4; + idx[23] = 7; } -QSGNode* BlobInvertedRect::updatePaintNode( - QSGNode* oldNode, UpdatePaintNodeData*) { +QSGNode* BlobInvertedRect::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { if (!m_group) { delete oldNode; return nullptr; @@ -55,9 +70,8 @@ QSGNode* BlobInvertedRect::updatePaintNode( delete oldNode; node = new QSGGeometryNode; - auto* geometry = new QSGGeometry( - QSGGeometry::defaultAttributes_TexturedPoint2D(), 8, 24, - QSGGeometry::UnsignedShortType); + auto* geometry = + new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 8, 24, QSGGeometry::UnsignedShortType); geometry->setDrawingMode(QSGGeometry::DrawTriangles); node->setGeometry(geometry); node->setFlag(QSGNode::OwnsGeometry); @@ -105,13 +119,10 @@ QSGNode* BlobInvertedRect::updatePaintNode( material->m_color = m_group->color(); material->m_hasInverted = m_cachedHasInverted ? 1 : 0; material->m_invertedRadius = m_cachedInvertedRadius; - memcpy(material->m_invertedOuter, m_cachedInvertedOuter, - sizeof(m_cachedInvertedOuter)); - memcpy(material->m_invertedInner, m_cachedInvertedInner, - sizeof(m_cachedInvertedInner)); + memcpy(material->m_invertedOuter, m_cachedInvertedOuter, sizeof(m_cachedInvertedOuter)); + memcpy(material->m_invertedInner, m_cachedInvertedInner, sizeof(m_cachedInvertedInner)); - const int count = - static_cast(qMin(m_cachedRects.size(), qsizetype(16))); + const int count = static_cast(qMin(m_cachedRects.size(), qsizetype(16))); material->m_rectCount = count; for (int i = 0; i < count; ++i) material->m_rects[i] = m_cachedRects[i]; diff --git a/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp b/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp index 207244de..f7fa6c0a 100644 --- a/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp +++ b/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp @@ -7,14 +7,10 @@ class BlobInvertedRect : public BlobShape { Q_OBJECT QML_ELEMENT - Q_PROPERTY(qreal borderLeft READ borderLeft WRITE setBorderLeft NOTIFY - borderLeftChanged) - Q_PROPERTY(qreal borderRight READ borderRight WRITE setBorderRight NOTIFY - borderRightChanged) - Q_PROPERTY(qreal borderTop READ borderTop WRITE setBorderTop NOTIFY - borderTopChanged) - Q_PROPERTY(qreal borderBottom READ borderBottom WRITE setBorderBottom NOTIFY - borderBottomChanged) + Q_PROPERTY(qreal borderLeft READ borderLeft WRITE setBorderLeft NOTIFY borderLeftChanged) + Q_PROPERTY(qreal borderRight READ borderRight WRITE setBorderRight NOTIFY borderRightChanged) + Q_PROPERTY(qreal borderTop READ borderTop WRITE setBorderTop NOTIFY borderTopChanged) + Q_PROPERTY(qreal borderBottom READ borderBottom WRITE setBorderBottom NOTIFY borderBottomChanged) public: explicit BlobInvertedRect(QQuickItem* parent = nullptr); diff --git a/plugin/src/Caelestia/Blobs/blobmaterial.cpp b/plugin/src/Caelestia/Blobs/blobmaterial.cpp index f2ead2ed..f71ad1bd 100644 --- a/plugin/src/Caelestia/Blobs/blobmaterial.cpp +++ b/plugin/src/Caelestia/Blobs/blobmaterial.cpp @@ -7,8 +7,7 @@ QSGMaterialType* BlobMaterial::type() const { return &s_type; } -QSGMaterialShader* BlobMaterial::createShader( - QSGRendererInterface::RenderMode) const { +QSGMaterialShader* BlobMaterial::createShader(QSGRendererInterface::RenderMode) const { return new BlobMaterialShader; } @@ -25,8 +24,7 @@ BlobMaterialShader::BlobMaterialShader() { setShaderFileName(FragmentStage, QStringLiteral(":/shaders/blob.frag.qsb")); } -bool BlobMaterialShader::updateUniformData( - RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) { +bool BlobMaterialShader::updateUniformData(RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) { Q_UNUSED(oldMaterial); auto* mat = static_cast(newMaterial); QByteArray* buf = state.uniformData(); @@ -85,13 +83,13 @@ bool BlobMaterialShader::updateUniformData( const auto& r = mat->m_rects[i]; const int base = 160 + i * 80; const float d0[4] = { r.cx, r.cy, r.hw, r.hh }; - const float d1[4] = { r.radius, r.offsetX, r.offsetY, r.minEig }; + const float d1[4] = { 0.0f, r.offsetX, r.offsetY, r.minEig }; const float d3[4] = { r.screenHalfX, r.screenHalfY, 0.0f, 0.0f }; memcpy(buf->data() + base, d0, 16); memcpy(buf->data() + base + 16, d1, 16); memcpy(buf->data() + base + 32, r.invDeform, 16); memcpy(buf->data() + base + 48, d3, 16); - memcpy(buf->data() + base + 64, r.cornerFill, 16); + memcpy(buf->data() + base + 64, r.radius, 16); } return true; diff --git a/plugin/src/Caelestia/Blobs/blobmaterial.hpp b/plugin/src/Caelestia/Blobs/blobmaterial.hpp index ef80fea1..85dbedaa 100644 --- a/plugin/src/Caelestia/Blobs/blobmaterial.hpp +++ b/plugin/src/Caelestia/Blobs/blobmaterial.hpp @@ -6,22 +6,20 @@ struct BlobRectData { float cx = 0, cy = 0, hw = 0, hh = 0; - float radius = 0; float offsetX = 0, offsetY = 0; float minEig = 1.0f; // Inverse of 2x2 deformation matrix, column-major for GLSL float invDeform[4] = { 1, 0, 0, 1 }; // Screen-space AABB half-extents of the deformed rect float screenHalfX = 0, screenHalfY = 0; - // Pre-computed corner fill factors (tr, br, bl, tl) - float cornerFill[4] = { 1, 1, 1, 1 }; + // Effective per-corner radii (tr, br, bl, tl), pre-computed on CPU + float radius[4] = { 0, 0, 0, 0 }; }; class BlobMaterial : public QSGMaterial { public: QSGMaterialType* type() const override; - QSGMaterialShader* createShader( - QSGRendererInterface::RenderMode) const override; + QSGMaterialShader* createShader(QSGRendererInterface::RenderMode) const override; int compare(const QSGMaterial* other) const override; float m_paddedX = 0; @@ -42,6 +40,5 @@ public: class BlobMaterialShader : public QSGMaterialShader { public: BlobMaterialShader(); - bool updateUniformData(RenderState& state, QSGMaterial* newMaterial, - QSGMaterial* oldMaterial) override; + bool updateUniformData(RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) override; }; diff --git a/plugin/src/Caelestia/Blobs/blobrect.cpp b/plugin/src/Caelestia/Blobs/blobrect.cpp index ff021203..efc1e7f0 100644 --- a/plugin/src/Caelestia/Blobs/blobrect.cpp +++ b/plugin/src/Caelestia/Blobs/blobrect.cpp @@ -17,10 +17,8 @@ void BlobRect::updatePolish() { if (m_physicsActive) { // Check if deformation is visually imperceptible - float totalDelta = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) - + std::abs(m_dm11 - 1.0f); - float totalVel = - std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11); + float totalDelta = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + std::abs(m_dm11 - 1.0f); + float totalVel = std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11); if (totalDelta < 0.004f && totalVel < 0.05f) { // Snap to rest, no visible deformation @@ -62,10 +60,8 @@ void BlobRect::updatePhysics() { return; } - const float velX = - static_cast(scenePos.x() - m_prevScenePos.x()) / dt; - const float velY = - static_cast(scenePos.y() - m_prevScenePos.y()) / dt; + const float velX = static_cast(scenePos.x() - m_prevScenePos.x()) / dt; + const float velY = static_cast(scenePos.y() - m_prevScenePos.y()) / dt; m_prevScenePos = scenePos; const float speed = std::sqrt(velX * velX + velY * velY); @@ -86,8 +82,7 @@ void BlobRect::updatePhysics() { float target11 = 1.0f; if (speed > 5.0f) { - const float targetStretch = - 1.0f + std::min(speed * kStretchFactor, kMaxStretch); + const float targetStretch = 1.0f + std::min(speed * kStretchFactor, kMaxStretch); const float targetCompress = 1.0f / targetStretch; const float cosA = velX / speed; @@ -105,35 +100,132 @@ void BlobRect::updatePhysics() { const float kStiffness = static_cast(m_stiffness); const float kDamping = static_cast(m_damping); - const float accel00 = - -kStiffness * (m_dm00 - target00) - kDamping * m_dmVel00; + const float accel00 = -kStiffness * (m_dm00 - target00) - kDamping * m_dmVel00; m_dmVel00 += accel00 * dt; m_dm00 += m_dmVel00 * dt; - const float accel01 = - -kStiffness * (m_dm01 - target01) - kDamping * m_dmVel01; + const float accel01 = -kStiffness * (m_dm01 - target01) - kDamping * m_dmVel01; m_dmVel01 += accel01 * dt; m_dm01 += m_dmVel01 * dt; - const float accel11 = - -kStiffness * (m_dm11 - target11) - kDamping * m_dmVel11; + const float accel11 = -kStiffness * (m_dm11 - target11) - kDamping * m_dmVel11; m_dmVel11 += accel11 * dt; m_dm11 += m_dmVel11 * dt; - m_deformMatrix = QMatrix4x4( - m_dm00, m_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); + m_deformMatrix = QMatrix4x4(m_dm00, m_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); updateCenteredDeformMatrix(); checkAtRest(speed); } +void BlobRect::setTopLeftRadius(qreal r) { + if (!qFuzzyCompare(m_topLeftRadius, r)) { + m_topLeftRadius = r; + emit topLeftRadiusChanged(); + if (m_group) + m_group->markDirty(); + } +} + +void BlobRect::setTopRightRadius(qreal r) { + if (!qFuzzyCompare(m_topRightRadius, r)) { + m_topRightRadius = r; + emit topRightRadiusChanged(); + if (m_group) + m_group->markDirty(); + } +} + +void BlobRect::setBottomLeftRadius(qreal r) { + if (!qFuzzyCompare(m_bottomLeftRadius, r)) { + m_bottomLeftRadius = r; + emit bottomLeftRadiusChanged(); + if (m_group) + m_group->markDirty(); + } +} + +void BlobRect::setBottomRightRadius(qreal r) { + if (!qFuzzyCompare(m_bottomRightRadius, r)) { + m_bottomRightRadius = r; + emit bottomRightRadiusChanged(); + if (m_group) + m_group->markDirty(); + } +} + +void BlobRect::cornerRadii(float out[4]) const { + const auto base = static_cast(m_radius); + out[0] = m_topRightRadius >= 0 ? static_cast(m_topRightRadius) : base; + out[1] = m_bottomRightRadius >= 0 ? static_cast(m_bottomRightRadius) : base; + out[2] = m_bottomLeftRadius >= 0 ? static_cast(m_bottomLeftRadius) : base; + out[3] = m_topLeftRadius >= 0 ? static_cast(m_topLeftRadius) : base; +} + +bool BlobRect::isExcluded(const BlobShape* other) const { + for (const auto& ptr : m_exclude) { + if (ptr == other) + return true; + } + return false; +} + +QQmlListProperty BlobRect::exclude() { + return QQmlListProperty( + this, nullptr, &excludeAppend, &excludeCount, &excludeAt, &excludeClear, &excludeReplace, &excludeRemoveLast); +} + +void BlobRect::excludeAppend(QQmlListProperty* prop, BlobRect* rect) { + auto* self = static_cast(prop->object); + self->m_exclude.append(rect); + if (self->m_group) + self->m_group->markDirty(); + emit self->excludeChanged(); +} + +qsizetype BlobRect::excludeCount(QQmlListProperty* prop) { + auto* self = static_cast(prop->object); + return self->m_exclude.size(); +} + +BlobRect* BlobRect::excludeAt(QQmlListProperty* prop, qsizetype index) { + auto* self = static_cast(prop->object); + return self->m_exclude.at(index); +} + +void BlobRect::excludeClear(QQmlListProperty* prop) { + auto* self = static_cast(prop->object); + if (self->m_exclude.isEmpty()) + return; + self->m_exclude.clear(); + if (self->m_group) + self->m_group->markDirty(); + emit self->excludeChanged(); +} + +void BlobRect::excludeReplace(QQmlListProperty* prop, qsizetype index, BlobRect* rect) { + auto* self = static_cast(prop->object); + self->m_exclude[index] = rect; + if (self->m_group) + self->m_group->markDirty(); + emit self->excludeChanged(); +} + +void BlobRect::excludeRemoveLast(QQmlListProperty* prop) { + auto* self = static_cast(prop->object); + if (self->m_exclude.isEmpty()) + return; + self->m_exclude.removeLast(); + if (self->m_group) + self->m_group->markDirty(); + emit self->excludeChanged(); +} + void BlobRect::checkAtRest(float speed) { constexpr float kEpsilon = 0.002f; - const bool atRest = - std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon && - std::abs(m_dm11 - 1.0f) < kEpsilon && std::abs(m_dmVel00) < kEpsilon && - std::abs(m_dmVel01) < kEpsilon && std::abs(m_dmVel11) < kEpsilon && - speed < 5.0f; + const bool atRest = std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon && + std::abs(m_dm11 - 1.0f) < kEpsilon && std::abs(m_dmVel00) < kEpsilon && + std::abs(m_dmVel01) < kEpsilon && std::abs(m_dmVel11) < kEpsilon && speed < 5.0f; if (atRest) { m_dm00 = 1.0f; diff --git a/plugin/src/Caelestia/Blobs/blobrect.hpp b/plugin/src/Caelestia/Blobs/blobrect.hpp index 86d4666d..d2d6ad45 100644 --- a/plugin/src/Caelestia/Blobs/blobrect.hpp +++ b/plugin/src/Caelestia/Blobs/blobrect.hpp @@ -3,17 +3,22 @@ #include "blobshape.hpp" #include +#include #include +#include class BlobRect : public BlobShape { Q_OBJECT QML_ELEMENT - Q_PROPERTY(qreal stiffness READ stiffness WRITE setStiffness NOTIFY - stiffnessChanged) + Q_PROPERTY(qreal stiffness READ stiffness WRITE setStiffness NOTIFY stiffnessChanged) + Q_PROPERTY(qreal damping READ damping WRITE setDamping NOTIFY dampingChanged) + Q_PROPERTY(qreal deformScale READ deformScale WRITE setDeformScale NOTIFY deformScaleChanged) + Q_PROPERTY(QQmlListProperty exclude READ exclude NOTIFY excludeChanged) + Q_PROPERTY(qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY topLeftRadiusChanged) + Q_PROPERTY(qreal topRightRadius READ topRightRadius WRITE setTopRightRadius NOTIFY topRightRadiusChanged) + Q_PROPERTY(qreal bottomLeftRadius READ bottomLeftRadius WRITE setBottomLeftRadius NOTIFY bottomLeftRadiusChanged) Q_PROPERTY( - qreal damping READ damping WRITE setDamping NOTIFY dampingChanged) - Q_PROPERTY(qreal deformScale READ deformScale WRITE setDeformScale NOTIFY - deformScaleChanged) + qreal bottomRightRadius READ bottomRightRadius WRITE setBottomRightRadius NOTIFY bottomRightRadiusChanged) public: explicit BlobRect(QQuickItem* parent = nullptr); @@ -46,10 +51,36 @@ public: } } + QQmlListProperty exclude(); + + bool isExcluded(const BlobShape* other) const override; + void cornerRadii(float out[4]) const override; + + qreal topLeftRadius() const { return m_topLeftRadius; } + + void setTopLeftRadius(qreal r); + + qreal topRightRadius() const { return m_topRightRadius; } + + void setTopRightRadius(qreal r); + + qreal bottomLeftRadius() const { return m_bottomLeftRadius; } + + void setBottomLeftRadius(qreal r); + + qreal bottomRightRadius() const { return m_bottomRightRadius; } + + void setBottomRightRadius(qreal r); + signals: void stiffnessChanged(); void dampingChanged(); void deformScaleChanged(); + void excludeChanged(); + void topLeftRadiusChanged(); + void topRightRadiusChanged(); + void bottomLeftRadiusChanged(); + void bottomRightRadiusChanged(); protected: void updatePolish() override; @@ -78,4 +109,18 @@ private: qreal m_stiffness = 200.0; qreal m_damping = 16.0; qreal m_deformScale = 0.0005; + + qreal m_topLeftRadius = -1; + qreal m_topRightRadius = -1; + qreal m_bottomLeftRadius = -1; + qreal m_bottomRightRadius = -1; + + QList> m_exclude; + + static void excludeAppend(QQmlListProperty* prop, BlobRect* rect); + static qsizetype excludeCount(QQmlListProperty* prop); + static BlobRect* excludeAt(QQmlListProperty* prop, qsizetype index); + static void excludeClear(QQmlListProperty* prop); + static void excludeReplace(QQmlListProperty* prop, qsizetype index, BlobRect* rect); + static void excludeRemoveLast(QQmlListProperty* prop); }; diff --git a/plugin/src/Caelestia/Blobs/blobshape.cpp b/plugin/src/Caelestia/Blobs/blobshape.cpp index a7938a50..b048c57e 100644 --- a/plugin/src/Caelestia/Blobs/blobshape.cpp +++ b/plugin/src/Caelestia/Blobs/blobshape.cpp @@ -19,8 +19,7 @@ static float deformPadding(const QMatrix4x4& dm, float hw, float hh) { return std::max(extraX, extraY); } -static float cpuSdBox(float px, float py, float cx, float cy, float hw, - float hh) { +static float cpuSdBox(float px, float py, float cx, float cy, float hw, float hh) { const float dx = std::abs(px - cx) - hw; const float dy = std::abs(py - cy) - hh; const float mdx = std::max(dx, 0.0f); @@ -66,8 +65,7 @@ void BlobShape::componentComplete() { registerWithGroup(); } -void BlobShape::geometryChange( - const QRectF& newGeometry, const QRectF& oldGeometry) { +void BlobShape::geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) { QQuickItem::geometryChange(newGeometry, oldGeometry); updateCenteredDeformMatrix(); if (m_group) { @@ -92,6 +90,14 @@ void BlobShape::updateCenteredDeformMatrix() { emit deformMatrixChanged(); } +void BlobShape::cornerRadii(float out[4]) const { + const auto r = static_cast(m_radius); + out[0] = r; + out[1] = r; + out[2] = r; + out[3] = r; +} + void BlobShape::registerWithGroup() { if (m_group) m_group->addShape(this); @@ -127,19 +133,15 @@ void BlobShape::updatePolish() { m_cachedPaddedY = static_cast(scenePos.y()) - totalPad; m_cachedPaddedW = static_cast(width()) + 2.0f * totalPad; m_cachedPaddedH = static_cast(height()) + 2.0f * totalPad; - m_localPaddedRect = QRectF(static_cast(-totalPad), - static_cast(-totalPad), - width() + 2.0 * static_cast(totalPad), - height() + 2.0 * static_cast(totalPad)); + m_localPaddedRect = QRectF(static_cast(-totalPad), static_cast(-totalPad), + width() + 2.0 * static_cast(totalPad), height() + 2.0 * static_cast(totalPad)); } // Filter nearby normal rects m_cachedRects.clear(); m_cachedMyIndex = -2; - const QRectF myPadded(static_cast(m_cachedPaddedX), - static_cast(m_cachedPaddedY), - static_cast(m_cachedPaddedW), - static_cast(m_cachedPaddedH)); + const QRectF myPadded(static_cast(m_cachedPaddedX), static_cast(m_cachedPaddedY), + static_cast(m_cachedPaddedW), static_cast(m_cachedPaddedH)); for (BlobShape* other : m_group->shapes()) { if (other->isInvertedRect()) @@ -149,6 +151,9 @@ void BlobShape::updatePolish() { if (other->width() <= 0 || other->height() <= 0) continue; + if (isExcluded(other)) + continue; + const QPointF otherScene = other->mapToScene(QPointF(0, 0)); bool include = false; @@ -157,12 +162,9 @@ void BlobShape::updatePolish() { } else { const float otherHW = static_cast(other->width()) * 0.5f; const float otherHH = static_cast(other->height()) * 0.5f; - const float otherPad = - pad + deformPadding(other->m_deformMatrix, otherHW, otherHH); - const QRectF otherPadded( - otherScene.x() - static_cast(otherPad), - otherScene.y() - static_cast(otherPad), - other->width() + 2.0 * static_cast(otherPad), + const float otherPad = pad + deformPadding(other->m_deformMatrix, otherHW, otherHH); + const QRectF otherPadded(otherScene.x() - static_cast(otherPad), + otherScene.y() - static_cast(otherPad), other->width() + 2.0 * static_cast(otherPad), other->height() + 2.0 * static_cast(otherPad)); include = myPadded.intersects(otherPadded); } @@ -180,14 +182,13 @@ void BlobShape::updatePolish() { r.cy = static_cast(otherScene.y() + other->height() / 2.0); r.hw = static_cast(other->width() / 2.0); r.hh = static_cast(other->height() / 2.0); - r.radius = static_cast(other->radius()); + other->cornerRadii(r.radius); r.offsetX = dm(0, 3); r.offsetY = dm(1, 3); // Pre-compute inverse deformation matrix const float det = a * d - c * b; - const float invDet = - std::abs(det) > 1e-6f ? 1.0f / det : 1.0f; + const float invDet = std::abs(det) > 1e-6f ? 1.0f / det : 1.0f; r.invDeform[0] = d * invDet; r.invDeform[1] = -b * invDet; r.invDeform[2] = -c * invDet; @@ -218,25 +219,15 @@ void BlobShape::updatePolish() { auto* inv = m_group->invertedRect(); if (inv) { const QPointF invScene = inv->mapToScene(QPointF(0, 0)); - const float outerCX = - static_cast(invScene.x() + inv->width() / 2.0); - const float outerCY = - static_cast(invScene.y() + inv->height() / 2.0); + const float outerCX = static_cast(invScene.x() + inv->width() / 2.0); + const float outerCY = static_cast(invScene.y() + inv->height() / 2.0); const float outerHW = static_cast(inv->width() / 2.0); const float outerHH = static_cast(inv->height() / 2.0); - const float innerCX = - outerCX + - static_cast((inv->borderLeft() - inv->borderRight()) / 2.0); - const float innerCY = - outerCY + - static_cast((inv->borderTop() - inv->borderBottom()) / 2.0); - const float innerHW = - outerHW - - static_cast((inv->borderLeft() + inv->borderRight()) / 2.0); - const float innerHH = - outerHH - - static_cast((inv->borderTop() + inv->borderBottom()) / 2.0); + const float innerCX = outerCX + static_cast((inv->borderLeft() - inv->borderRight()) / 2.0); + const float innerCY = outerCY + static_cast((inv->borderTop() - inv->borderBottom()) / 2.0); + const float innerHW = outerHW - static_cast((inv->borderLeft() + inv->borderRight()) / 2.0); + const float innerHH = outerHH - static_cast((inv->borderTop() + inv->borderBottom()) / 2.0); // Check if this rect is near the border (within 2x smoothing of inner edge) bool nearBorder = isInvertedRect(); @@ -247,10 +238,8 @@ void BlobShape::updatePolish() { const float myHW = m_cachedPaddedW * 0.5f; const float myHH = m_cachedPaddedH * 0.5f; // Near border if any edge of padded rect is within margin of inner edge - nearBorder = (myCX - myHW < innerCX - innerHW + margin) || - (myCX + myHW > innerCX + innerHW - margin) || - (myCY - myHH < innerCY - innerHH + margin) || - (myCY + myHH > innerCY + innerHH - margin); + nearBorder = (myCX - myHW < innerCX - innerHW + margin) || (myCX + myHW > innerCX + innerHW - margin) || + (myCY - myHH < innerCY - innerHH + margin) || (myCY + myHH > innerCY + innerHH - margin); } if (nearBorder) { @@ -269,8 +258,9 @@ void BlobShape::updatePolish() { } } - // Pre-compute corner fill factors (moves O(N²) work from GPU to CPU) + // Pre-compute effective per-corner radii (moves O(N²) work from GPU to CPU) const float smoothFactor = pad; + constexpr float minR = 2.0f; const auto rectCount = m_cachedRects.size(); for (qsizetype i = 0; i < rectCount; ++i) { auto& ri = m_cachedRects[i]; @@ -285,14 +275,10 @@ void BlobShape::updatePolish() { if (j == i) continue; const auto& rj = m_cachedRects[j]; - fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, - cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh))); - fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, - cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh))); - fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, - cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh))); - fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, - cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh))); + fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh))); + fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh))); + fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh))); + fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh))); } if (m_cachedHasInverted) { @@ -300,20 +286,17 @@ void BlobShape::updatePolish() { const float icy = m_cachedInvertedInner[1]; const float ihw = m_cachedInvertedInner[2]; const float ihh = m_cachedInvertedInner[3]; - fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, - -cpuSdBox(cTrX, cTrY, icx, icy, ihw, ihh))); - fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, - -cpuSdBox(cBrX, cBrY, icx, icy, ihw, ihh))); - fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, - -cpuSdBox(cBlX, cBlY, icx, icy, ihw, ihh))); - fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, - -cpuSdBox(cTlX, cTlY, icx, icy, ihw, ihh))); + fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cTrX, cTrY, icx, icy, ihw, ihh))); + fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cBrX, cBrY, icx, icy, ihw, ihh))); + fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cBlX, cBlY, icx, icy, ihw, ihh))); + fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cTlX, cTlY, icx, icy, ihw, ihh))); } - ri.cornerFill[0] = fTr; - ri.cornerFill[1] = fBr; - ri.cornerFill[2] = fBl; - ri.cornerFill[3] = fTl; + // Combine base radii with fill factors into effective per-corner radii + ri.radius[0] = std::max(ri.radius[0] * fTr, minR); + ri.radius[1] = std::max(ri.radius[1] * fBr, minR); + ri.radius[2] = std::max(ri.radius[2] * fBl, minR); + ri.radius[3] = std::max(ri.radius[3] * fTl, minR); } } @@ -327,8 +310,7 @@ QSGNode* BlobShape::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { if (!node) { node = new QSGGeometryNode; - auto* geometry = new QSGGeometry( - QSGGeometry::defaultAttributes_TexturedPoint2D(), 4); + auto* geometry = new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 4); geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip); node->setGeometry(geometry); node->setFlag(QSGNode::OwnsGeometry); @@ -366,13 +348,10 @@ QSGNode* BlobShape::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { material->m_color = m_group->color(); material->m_hasInverted = m_cachedHasInverted ? 1 : 0; material->m_invertedRadius = m_cachedInvertedRadius; - memcpy(material->m_invertedOuter, m_cachedInvertedOuter, - sizeof(m_cachedInvertedOuter)); - memcpy(material->m_invertedInner, m_cachedInvertedInner, - sizeof(m_cachedInvertedInner)); + memcpy(material->m_invertedOuter, m_cachedInvertedOuter, sizeof(m_cachedInvertedOuter)); + memcpy(material->m_invertedInner, m_cachedInvertedInner, sizeof(m_cachedInvertedInner)); - const int count = - static_cast(qMin(m_cachedRects.size(), qsizetype(16))); + const int count = static_cast(qMin(m_cachedRects.size(), qsizetype(16))); material->m_rectCount = count; for (int i = 0; i < count; ++i) material->m_rects[i] = m_cachedRects[i]; diff --git a/plugin/src/Caelestia/Blobs/blobshape.hpp b/plugin/src/Caelestia/Blobs/blobshape.hpp index 8c6f1165..6383b644 100644 --- a/plugin/src/Caelestia/Blobs/blobshape.hpp +++ b/plugin/src/Caelestia/Blobs/blobshape.hpp @@ -12,8 +12,7 @@ class BlobShape : public QQuickItem { Q_OBJECT Q_PROPERTY(BlobGroup* group READ group WRITE setGroup NOTIFY groupChanged) Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged) - Q_PROPERTY( - QMatrix4x4 deformMatrix READ deformMatrix NOTIFY deformMatrixChanged) + Q_PROPERTY(QMatrix4x4 deformMatrix READ deformMatrix NOTIFY deformMatrixChanged) friend class BlobGroup; @@ -38,13 +37,16 @@ signals: protected: void componentComplete() override; - void geometryChange( - const QRectF& newGeometry, const QRectF& oldGeometry) override; + void geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) override; void updatePolish() override; QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override; virtual bool isInvertedRect() const { return false; } + virtual bool isExcluded(const BlobShape* /*other*/) const { return false; } + + virtual void cornerRadii(float out[4]) const; + virtual void updatePhysics() {} virtual void registerWithGroup(); diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.frag b/plugin/src/Caelestia/Blobs/shaders/blob.frag index f6387a2f..808cf309 100644 --- a/plugin/src/Caelestia/Blobs/shaders/blob.frag +++ b/plugin/src/Caelestia/Blobs/shaders/blob.frag @@ -80,7 +80,7 @@ void main() { vec4 props = rectData[i * 5 + 1]; // radius, offsetX, offsetY, minEig vec4 invDm = rectData[i * 5 + 2]; // inverse deform matrix vec4 sh = rectData[i * 5 + 3]; // screenHalfX, screenHalfY, 0, 0 - vec4 fills = rectData[i * 5 + 4]; // f_tr, f_br, f_bl, f_tl + vec4 radii = rectData[i * 5 + 4]; // effective per-corner radii (tr, br, bl, tl) // Offset center for asymmetric deformation vec2 center = rect.xy + props.yz; @@ -94,10 +94,7 @@ void main() { mat2 invDeform = mat2(invDm.xy, invDm.zw); vec2 transformedPixel = center + invDeform * (pixel - center); - // Use pre-computed corner fill factors - float br = props.x; - float minR = 2.0; - vec4 radii = max(br * fills, vec4(minR)); + // Use pre-computed effective per-corner radii float d = sdRoundedBox4(transformedPixel, center, rect.zw, radii); // Use pre-computed minimum eigenvalue for SDF correction From b739e74247057e6a65ec7d3de639f9161067ba06 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 27 Mar 2026 21:03:39 +1100 Subject: [PATCH 22/45] ci: add qt shadertools dep to ci image --- .github/workflows/update-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-image.yml b/.github/workflows/update-image.yml index 23006dd0..74fe5435 100644 --- a/.github/workflows/update-image.yml +++ b/.github/workflows/update-image.yml @@ -23,7 +23,7 @@ jobs: run: | cat > /tmp/Dockerfile <> /etc/sudoers && \ sudo -u builder git clone https://aur.archlinux.org/yay-bin.git /home/builder/yay-bin && \ From 611f8de4001ad8ec886e2e79ebf9c3c232af2654 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 28 Mar 2026 00:49:48 +1100 Subject: [PATCH 23/45] fix: sidebar close anim & gap w/ utilities Don't exclude each other when not flush to fix close overshoot not looking right Also increase sidebar height by 2 instead of 1 to fix gap --- modules/drawers/Drawers.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 23be6f03..2e1598d4 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -166,8 +166,8 @@ Variants { panel: panels.sidebar bar: bar deformAmount: 0 - height: panel.height + 1 - exclude: [utilsBg] + height: panel.height + 2 + exclude: panels.sidebar.offsetScale > 0 ? [] : [utilsBg] bottomLeftRadius: panels.sidebar.visible ? 0 : radius Behavior on bottomLeftRadius { @@ -202,7 +202,7 @@ Variants { blobGroup: blobGroup panel: panels.utilities bar: bar - exclude: [sidebarBg] + exclude: panels.sidebar.offsetScale > 0 ? [] : [sidebarBg] topLeftRadius: panels.sidebar.visible ? 0 : radius Behavior on topLeftRadius { From 04efa4d39da737b6662cb3a574b5b58068ce6022 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 28 Mar 2026 21:06:49 +1100 Subject: [PATCH 24/45] feat: make utils anim to sidebar width --- modules/drawers/Drawers.qml | 23 ++++++---------------- modules/utilities/Wrapper.qml | 36 +++++++++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 2e1598d4..4d37a530 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -165,16 +165,10 @@ Variants { blobGroup: blobGroup panel: panels.sidebar bar: bar - deformAmount: 0 + deformAmount: 0.05 height: panel.height + 2 - exclude: panels.sidebar.offsetScale > 0 ? [] : [utilsBg] - bottomLeftRadius: panels.sidebar.visible ? 0 : radius - - Behavior on bottomLeftRadius { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - } - } + exclude: panels.sidebar.offsetScale > 0.08 ? [] : [utilsBg] + bottomLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius } PanelBg { @@ -202,14 +196,9 @@ Variants { blobGroup: blobGroup panel: panels.utilities bar: bar - exclude: panels.sidebar.offsetScale > 0 ? [] : [sidebarBg] - topLeftRadius: panels.sidebar.visible ? 0 : radius - - Behavior on topLeftRadius { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - } - } + deformAmount: panels.sidebar.visible ? 0.1 : 0.15 + exclude: panels.sidebar.offsetScale > 0.08 ? [] : [sidebarBg] + topLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius } PanelBg { diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index b0aec96e..23199aa2 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -5,12 +5,13 @@ import Quickshell import qs.components import qs.config import qs.modules.bar.popouts as BarPopouts +import qs.modules.sidebar as Sidebar Item { id: root required property DrawerVisibilities visibilities - required property Item sidebar + required property Sidebar.Wrapper sidebar required property BarPopouts.Wrapper popouts readonly property PersistentProperties props: PersistentProperties { @@ -22,13 +23,44 @@ Item { } readonly property bool shouldBeActive: visibilities.sidebar || (visibilities.utilities && Config.utilities.enabled && !(visibilities.session && Config.session.enabled)) property real offsetScale: shouldBeActive ? 0 : 1 + property real sidebarLerp visible: offsetScale < 1 anchors.bottomMargin: (-implicitHeight - 5) * offsetScale implicitHeight: content.implicitHeight + content.anchors.margins * 2 - implicitWidth: sidebar.visible ? sidebar.width : Config.utilities.sizes.width + implicitWidth: sidebar.width * (1 - sidebar.offsetScale) * sidebarLerp + Config.utilities.sizes.width * (1 - sidebarLerp) opacity: 1 - offsetScale + states: State { + name: "attachedToSidebar" + when: root.visibilities.sidebar + + PropertyChanges { + root.sidebarLerp: 1 + } + } + + transitions: [ + Transition { + from: "" + + Anim { + property: "sidebarLerp" + duration: Appearance.anim.durations.expressiveDefaultSpatial / 2 + easing.bezierCurve: Appearance.anim.curves.standardAccel + } + }, + Transition { + to: "" + + Anim { + property: "sidebarLerp" + duration: Appearance.anim.durations.expressiveDefaultSpatial / 2 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + } + ] + Behavior on offsetScale { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial From 6cdecb88259cdc1a4140de8fb3d022b156dab5eb Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 28 Mar 2026 21:28:21 +1100 Subject: [PATCH 25/45] fix: sidebar gap with utils due to deform Also better sync utils width with sidebar width --- modules/drawers/Drawers.qml | 4 +++- modules/utilities/Wrapper.qml | 3 ++- plugin/src/Caelestia/Blobs/blobrect.cpp | 3 +++ plugin/src/Caelestia/Blobs/blobshape.cpp | 6 ++++-- plugin/src/Caelestia/Blobs/blobshape.hpp | 3 +++ 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 4d37a530..b18ecc84 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -166,7 +166,7 @@ Variants { panel: panels.sidebar bar: bar deformAmount: 0.05 - height: panel.height + 2 + implicitHeight: panel.height * (1 / rawDeformMatrix.m22) + 2 exclude: panels.sidebar.offsetScale > 0.08 ? [] : [utilsBg] bottomLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius } @@ -247,6 +247,8 @@ Variants { visibilities: visibilities bar: bar + utilities.horizontalStretch: (sidebarBg.rawDeformMatrix.m11 - 1) / 2 + 1 + dashboard.transform: Matrix4x4 { matrix: dashBg.deformMatrix } diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index 23199aa2..107f1f93 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -13,6 +13,7 @@ Item { required property DrawerVisibilities visibilities required property Sidebar.Wrapper sidebar required property BarPopouts.Wrapper popouts + property real horizontalStretch readonly property PersistentProperties props: PersistentProperties { property bool recordingListExpanded: false @@ -28,7 +29,7 @@ Item { visible: offsetScale < 1 anchors.bottomMargin: (-implicitHeight - 5) * offsetScale implicitHeight: content.implicitHeight + content.anchors.margins * 2 - implicitWidth: sidebar.width * (1 - sidebar.offsetScale) * sidebarLerp + Config.utilities.sizes.width * (1 - sidebarLerp) + implicitWidth: sidebar.width * (1 - sidebar.offsetScale) * horizontalStretch * sidebarLerp + Config.utilities.sizes.width * (1 - sidebarLerp) opacity: 1 - offsetScale states: State { diff --git a/plugin/src/Caelestia/Blobs/blobrect.cpp b/plugin/src/Caelestia/Blobs/blobrect.cpp index efc1e7f0..fa7b4b1e 100644 --- a/plugin/src/Caelestia/Blobs/blobrect.cpp +++ b/plugin/src/Caelestia/Blobs/blobrect.cpp @@ -27,6 +27,7 @@ void BlobRect::updatePolish() { m_dm11 = 1.0f; m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f; m_deformMatrix = QMatrix4x4(); + emit rawDeformMatrixChanged(); updateCenteredDeformMatrix(); m_physicsActive = false; } else { @@ -113,6 +114,7 @@ void BlobRect::updatePhysics() { m_dm11 += m_dmVel11 * dt; m_deformMatrix = QMatrix4x4(m_dm00, m_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); + emit rawDeformMatrixChanged(); updateCenteredDeformMatrix(); checkAtRest(speed); @@ -235,6 +237,7 @@ void BlobRect::checkAtRest(float speed) { m_dmVel01 = 0.0f; m_dmVel11 = 0.0f; m_deformMatrix = QMatrix4x4(); // identity + emit rawDeformMatrixChanged(); updateCenteredDeformMatrix(); m_physicsActive = false; } diff --git a/plugin/src/Caelestia/Blobs/blobshape.cpp b/plugin/src/Caelestia/Blobs/blobshape.cpp index b048c57e..96662150 100644 --- a/plugin/src/Caelestia/Blobs/blobshape.cpp +++ b/plugin/src/Caelestia/Blobs/blobshape.cpp @@ -86,8 +86,10 @@ void BlobShape::updateCenteredDeformMatrix() { result.translate(cx, cy); result *= m_deformMatrix; result.translate(-cx, -cy); - m_centeredDeformMatrix = result; - emit deformMatrixChanged(); + if (m_centeredDeformMatrix != result) { + m_centeredDeformMatrix = result; + emit deformMatrixChanged(); + } } void BlobShape::cornerRadii(float out[4]) const { diff --git a/plugin/src/Caelestia/Blobs/blobshape.hpp b/plugin/src/Caelestia/Blobs/blobshape.hpp index 6383b644..9165579c 100644 --- a/plugin/src/Caelestia/Blobs/blobshape.hpp +++ b/plugin/src/Caelestia/Blobs/blobshape.hpp @@ -13,6 +13,7 @@ class BlobShape : public QQuickItem { Q_PROPERTY(BlobGroup* group READ group WRITE setGroup NOTIFY groupChanged) Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged) Q_PROPERTY(QMatrix4x4 deformMatrix READ deformMatrix NOTIFY deformMatrixChanged) + Q_PROPERTY(QMatrix4x4 rawDeformMatrix READ rawDeformMatrix NOTIFY rawDeformMatrixChanged) friend class BlobGroup; @@ -29,11 +30,13 @@ public: void setRadius(qreal r); QMatrix4x4 deformMatrix() const { return m_centeredDeformMatrix; } + QMatrix4x4 rawDeformMatrix() const { return m_deformMatrix; } signals: void groupChanged(); void radiusChanged(); void deformMatrixChanged(); + void rawDeformMatrixChanged(); protected: void componentComplete() override; From 99dec4ea6bccc962de80ae4c43fed21b56176299 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 28 Mar 2026 21:31:28 +1100 Subject: [PATCH 26/45] chore: fix format --- modules/utilities/Wrapper.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index 107f1f93..cb4bcaa0 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -4,8 +4,8 @@ import QtQuick import Quickshell import qs.components import qs.config -import qs.modules.bar.popouts as BarPopouts import qs.modules.sidebar as Sidebar +import qs.modules.bar.popouts as BarPopouts Item { id: root From 4ed544948577179a8c61b33799300c4b8b73baff Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 28 Mar 2026 21:35:31 +1100 Subject: [PATCH 27/45] fix: animate background colour --- modules/drawers/Drawers.qml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index b18ecc84..c775aed5 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -117,6 +117,10 @@ Variants { id: blobGroup color: Colours.palette.m3surface + + Behavior on color { + CAnim {} + } } BlobInvertedRect { From c670302fc1038295853b1c9db1dfabfcde5a6a30 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 28 Mar 2026 21:36:00 +1100 Subject: [PATCH 28/45] chore: format c++ --- plugin/src/Caelestia/Blobs/blobshape.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin/src/Caelestia/Blobs/blobshape.hpp b/plugin/src/Caelestia/Blobs/blobshape.hpp index 9165579c..c9d98504 100644 --- a/plugin/src/Caelestia/Blobs/blobshape.hpp +++ b/plugin/src/Caelestia/Blobs/blobshape.hpp @@ -30,6 +30,7 @@ public: void setRadius(qreal r); QMatrix4x4 deformMatrix() const { return m_centeredDeformMatrix; } + QMatrix4x4 rawDeformMatrix() const { return m_deformMatrix; } signals: From c2221a6d8e100a8e9b827b6d0f196fe202d87163 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 2 Apr 2026 15:57:24 +1100 Subject: [PATCH 29/45] fix: use height scaled region for top panel detection Same as bottom panel, doesn't take into account the 5px offset this way --- modules/drawers/Interactions.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index 464d7777..aeabf401 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -42,7 +42,8 @@ CustomMouseArea { } function inTopPanel(panel: Item, x: real, y: real): bool { - return y < Math.max(Config.border.minThickness, Config.border.thickness + panel.height) + panel.y && withinPanelWidth(panel, x, y); + const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0)); // qmllint disable missing-property + return y < Math.max(Config.border.minThickness, Config.border.thickness + panelHeight) && withinPanelWidth(panel, x, y); } function inBottomPanel(panel: Item, x: real, y: real, isCorner = false): bool { From 0e50b624138c437e578fa51914f70ce9e8a52929 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 6 Apr 2026 23:32:17 +1000 Subject: [PATCH 30/45] refactor: move drawers window -> ContentWindow.qml --- modules/bar/BarWrapper.qml | 4 +- modules/drawers/ContentWindow.qml | 322 +++++++++++++++++++++++++++ modules/drawers/Drawers.qml | 346 +----------------------------- 3 files changed, 328 insertions(+), 344 deletions(-) create mode 100644 modules/drawers/ContentWindow.qml diff --git a/modules/bar/BarWrapper.qml b/modules/bar/BarWrapper.qml index f29e23f9..ec9df2d1 100644 --- a/modules/bar/BarWrapper.qml +++ b/modules/bar/BarWrapper.qml @@ -4,6 +4,7 @@ import QtQuick import Quickshell import qs.components import qs.config +import qs.utils import qs.modules.bar.popouts as BarPopouts Item { @@ -12,9 +13,10 @@ Item { required property ShellScreen screen required property DrawerVisibilities visibilities required property BarPopouts.Wrapper popouts - required property bool disabled required property bool fullscreen + readonly property bool disabled: Strings.testRegexList(Config.bar.excludedScreens, screen.name) + readonly property int clampedWidth: Math.max(Config.border.minThickness, implicitWidth) readonly property int padding: Math.max(Appearance.padding.smaller, Config.border.thickness) readonly property int contentWidth: Config.bar.sizes.innerWidth + padding * 2 diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml new file mode 100644 index 00000000..7171c8c5 --- /dev/null +++ b/modules/drawers/ContentWindow.qml @@ -0,0 +1,322 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import Quickshell +import Quickshell.Hyprland +import Quickshell.Wayland +import Caelestia.Blobs +import qs.components +import qs.components.containers +import qs.services +import qs.config +import qs.modules.bar + +StyledWindow { + id: root + + readonly property alias bar: bar + + readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen) + readonly property bool hasSpecialWorkspace: (monitor?.lastIpcObject.specialWorkspace?.name.length ?? 0) > 0 + readonly property bool hasFullscreen: { + if (hasSpecialWorkspace) { + const specialName = monitor?.lastIpcObject.specialWorkspace?.name; + if (!specialName) + return false; + const specialWs = Hypr.workspaces.values.find(ws => ws.name === specialName); + return specialWs?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; + } + return monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; + } + property real borderThickness: hasFullscreen ? 0 : Config.border.thickness + readonly property real borderLayoutThickness: hasFullscreen ? 0 : Config.border.thickness + property real borderRounding: hasFullscreen ? 0 : Config.border.rounding + property real shadowOpacity: hasFullscreen ? 0 : 0.7 + + readonly property int dragMaskPadding: { + if (focusGrab.active || panels.popouts.isDetached) + return 0; + + if (monitor?.lastIpcObject.specialWorkspace?.name || monitor?.activeWorkspace.lastIpcObject.windows > 0) + return 0; + + const thresholds = []; + for (const panel of ["dashboard", "launcher", "session", "sidebar"]) + if (Config[panel].enabled) + thresholds.push(Config[panel].dragThreshold); + return Math.max(...thresholds); + } + + onHasFullscreenChanged: { + visibilities.launcher = false; + visibilities.session = false; + visibilities.dashboard = false; + } + + name: "drawers" + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None + + mask: Regions { + bar: bar + panels: panels + win: root + } + + anchors.top: true + anchors.bottom: true + anchors.left: true + anchors.right: true + + Behavior on borderThickness { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + Behavior on borderRounding { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + Behavior on shadowOpacity { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + HyprlandFocusGrab { + id: focusGrab + + active: (visibilities.launcher && Config.launcher.enabled) || (visibilities.session && Config.session.enabled) || (visibilities.sidebar && Config.sidebar.enabled) || (!Config.dashboard.showOnHover && visibilities.dashboard && Config.dashboard.enabled) || (panels.popouts.currentName.startsWith("traymenu") && (panels.popouts.current as StackView)?.depth > 1) + windows: [root] + onCleared: { + visibilities.launcher = false; + visibilities.session = false; + visibilities.sidebar = false; + visibilities.dashboard = false; + panels.popouts.hasCurrent = false; + bar.closeTray(); + } + } + + StyledRect { + anchors.fill: parent + opacity: visibilities.session && Config.session.enabled ? 0.5 : 0 + color: Colours.palette.m3scrim + + Behavior on opacity { + Anim {} + } + } + + Item { + anchors.fill: parent + opacity: Colours.transparency.enabled ? Colours.transparency.base : 1 + layer.enabled: true + layer.effect: MultiEffect { + shadowEnabled: true + blurMax: 15 + shadowColor: Qt.alpha(Colours.palette.m3shadow, Math.max(0, root.shadowOpacity)) + } + + // Border { + // bar: bar + // } + + // Backgrounds { + // panels: panels + // bar: bar + // } + + BlobGroup { + id: blobGroup + + color: Colours.palette.m3surface + + Behavior on color { + CAnim {} + } + } + + BlobInvertedRect { + anchors.fill: parent + anchors.margins: -50 // Make border thicker to smooth out bulge from closed drawers + group: blobGroup + radius: root.borderRounding + borderLeft: bar.implicitWidth - anchors.margins + borderRight: root.borderThickness - anchors.margins + borderTop: root.borderThickness - anchors.margins + borderBottom: root.borderThickness - anchors.margins + } + + PanelBg { + id: dashBg + + panel: panels.dashboard + deformAmount: 0.1 + } + + PanelBg { + id: launcherBg + + panel: panels.launcher + deformAmount: 0.1 + } + + PanelBg { + id: sessionBg + + panel: panels.sessionWrapper + deformAmount: 0.25 + x: panels.sessionWrapper.x + panels.session.x + bar.implicitWidth + implicitWidth: panels.session.width + } + + PanelBg { + id: sidebarBg + + panel: panels.sidebar + deformAmount: 0.05 + implicitHeight: panel.height * (1 / rawDeformMatrix.m22) + 2 + exclude: panels.sidebar.offsetScale > 0.08 ? [] : [utilsBg] + bottomLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius + } + + PanelBg { + id: osdBg + + panel: panels.osdWrapper + deformAmount: 0.3 + x: panels.osdWrapper.x + panels.osd.x + bar.implicitWidth + implicitWidth: panels.osd.width + } + + PanelBg { + id: notifsBg + + panel: panels.notifications + } + + PanelBg { + id: utilsBg + + panel: panels.utilities + deformAmount: panels.sidebar.visible ? 0.1 : 0.15 + exclude: panels.sidebar.offsetScale > 0.08 ? [] : [sidebarBg] + topLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius + } + + PanelBg { + id: popoutBg + + panel: panels.popouts + x: bar.implicitWidth - (panels.popouts.isDetached ? -(root.width - panels.popouts.shownWidth) / 2 : panels.popouts.hasCurrent ? 0 : panels.popouts.shownWidth + 5) + implicitWidth: panels.popouts.shownWidth + + Behavior on x { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + Behavior on implicitWidth { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + } + } + + DrawerVisibilities { + id: visibilities + + Component.onCompleted: Visibilities.load(root.screen, this) + } + + Interactions { + screen: root.screen + popouts: panels.popouts + visibilities: visibilities + panels: panels + bar: bar + borderThickness: root.borderLayoutThickness + fullscreen: root.hasFullscreen + + Panels { + id: panels + + screen: root.screen + visibilities: visibilities + bar: bar + borderThickness: root.borderThickness + + utilities.horizontalStretch: (sidebarBg.rawDeformMatrix.m11 - 1) / 2 + 1 + + dashboard.transform: Matrix4x4 { + matrix: dashBg.deformMatrix + } + launcher.transform: Matrix4x4 { + matrix: launcherBg.deformMatrix + } + session.transform: Matrix4x4 { + matrix: sessionBg.deformMatrix + } + sidebar.transform: Matrix4x4 { + matrix: sidebarBg.deformMatrix + } + osd.transform: Matrix4x4 { + matrix: osdBg.deformMatrix + } + notifications.transform: Matrix4x4 { + matrix: notifsBg.deformMatrix + } + utilities.transform: Matrix4x4 { + matrix: utilsBg.deformMatrix + } + popouts.transform: Matrix4x4 { + matrix: popoutBg.deformMatrix + } + } + + BarWrapper { + id: bar + + anchors.top: parent.top + anchors.bottom: parent.bottom + + screen: root.screen + visibilities: visibilities + popouts: panels.popouts + + fullscreen: root.hasFullscreen + + Component.onCompleted: Visibilities.bars.set(root.screen, this) + } + } + + component PanelBg: BlobRect { + required property Item panel + property real deformAmount: 0.15 + + group: panel.width > 0 && panel.height > 0 ? blobGroup : null + x: panel.x + bar.implicitWidth + y: panel.y + root.borderThickness + implicitWidth: panel.width + implicitHeight: panel.height + radius: Config.border.rounding + deformScale: deformAmount / 10000 + } +} diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 0c801193..ed886b31 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -1,18 +1,9 @@ pragma ComponentBehavior: Bound import QtQuick -import QtQuick.Controls -import QtQuick.Effects import Quickshell -import Quickshell.Hyprland -import Quickshell.Wayland -import Caelestia.Blobs -import qs.components -import qs.components.containers import qs.services import qs.config -import qs.utils -import qs.modules.bar Variants { model: Screens.screens @@ -21,348 +12,17 @@ Variants { id: scope required property ShellScreen modelData - readonly property bool barDisabled: Strings.testRegexList(Config.bar.excludedScreens, modelData.name) Exclusions { screen: scope.modelData - bar: bar + bar: content.bar borderThickness: Config.border.thickness } - StyledWindow { - id: win - - readonly property var monitor: Hypr.monitorFor(screen) - readonly property bool hasSpecialWorkspace: (monitor?.lastIpcObject?.specialWorkspace?.name.length ?? 0) > 0 - readonly property bool hasFullscreen: { - if (hasSpecialWorkspace) { - const specialName = monitor?.lastIpcObject?.specialWorkspace?.name; - if (!specialName) - return false; - const specialWs = Hypr.workspaces.values.find(ws => ws.name === specialName); - return specialWs?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; - } - return monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; - } - property real borderThickness: hasFullscreen ? 0 : Config.border.thickness - readonly property real borderLayoutThickness: hasFullscreen ? 0 : Config.border.thickness - property real borderRounding: hasFullscreen ? 0 : Config.border.rounding - property real shadowOpacity: hasFullscreen ? 0 : 0.7 - readonly property int dragMaskPadding: { - if (focusGrab.active || panels.popouts.isDetached) - return 0; - - const mon = Hypr.monitorFor(screen); - if (mon?.lastIpcObject.specialWorkspace?.name || mon?.activeWorkspace.lastIpcObject.windows > 0) - return 0; - - const thresholds = []; - for (const panel of ["dashboard", "launcher", "session", "sidebar"]) - if (Config[panel].enabled) - thresholds.push(Config[panel].dragThreshold); - return Math.max(...thresholds); - } - - onHasFullscreenChanged: { - visibilities.launcher = false; - visibilities.session = false; - visibilities.dashboard = false; - } + ContentWindow { + id: content screen: scope.modelData - name: "drawers" - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None - - mask: Regions { - bar: bar - panels: panels - win: win - } - - anchors.top: true - anchors.bottom: true - anchors.left: true - anchors.right: true - - Behavior on borderThickness { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - - Behavior on borderRounding { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - - Behavior on shadowOpacity { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - - HyprlandFocusGrab { - id: focusGrab - - active: (visibilities.launcher && Config.launcher.enabled) || (visibilities.session && Config.session.enabled) || (visibilities.sidebar && Config.sidebar.enabled) || (!Config.dashboard.showOnHover && visibilities.dashboard && Config.dashboard.enabled) || (panels.popouts.currentName.startsWith("traymenu") && (panels.popouts.current as StackView)?.depth > 1) - windows: [win] - onCleared: { - visibilities.launcher = false; - visibilities.session = false; - visibilities.sidebar = false; - visibilities.dashboard = false; - panels.popouts.hasCurrent = false; - bar.closeTray(); - } - } - - StyledRect { - anchors.fill: parent - opacity: visibilities.session && Config.session.enabled ? 0.5 : 0 - color: Colours.palette.m3scrim - - Behavior on opacity { - Anim {} - } - } - - Item { - anchors.fill: parent - opacity: Colours.transparency.enabled ? Colours.transparency.base : 1 - layer.enabled: true - layer.effect: MultiEffect { - shadowEnabled: true - blurMax: 15 - shadowColor: Qt.alpha(Colours.palette.m3shadow, Math.max(0, win.shadowOpacity)) - } - - // Border { - // bar: bar - // } - - // Backgrounds { - // panels: panels - // bar: bar - // } - - BlobGroup { - id: blobGroup - - color: Colours.palette.m3surface - - Behavior on color { - CAnim {} - } - } - - BlobInvertedRect { - anchors.fill: parent - anchors.margins: -50 // Make border thicker to smooth out bulge from closed drawers - group: blobGroup - radius: win.borderRounding - borderLeft: bar.implicitWidth - anchors.margins - borderRight: win.borderThickness - anchors.margins - borderTop: win.borderThickness - anchors.margins - borderBottom: win.borderThickness - anchors.margins - } - - PanelBg { - id: dashBg - - blobGroup: blobGroup - panel: panels.dashboard - bar: bar - borderThickness: win.borderThickness - deformAmount: 0.1 - } - - PanelBg { - id: launcherBg - - blobGroup: blobGroup - panel: panels.launcher - bar: bar - borderThickness: win.borderThickness - deformAmount: 0.1 - } - - PanelBg { - id: sessionBg - - blobGroup: blobGroup - panel: panels.sessionWrapper - bar: bar - borderThickness: win.borderThickness - deformAmount: 0.25 - x: panels.sessionWrapper.x + panels.session.x + bar.implicitWidth - implicitWidth: panels.session.width - } - - PanelBg { - id: sidebarBg - - blobGroup: blobGroup - panel: panels.sidebar - bar: bar - borderThickness: win.borderThickness - deformAmount: 0.05 - implicitHeight: panel.height * (1 / rawDeformMatrix.m22) + 2 - exclude: panels.sidebar.offsetScale > 0.08 ? [] : [utilsBg] - bottomLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius - } - - PanelBg { - id: osdBg - - blobGroup: blobGroup - panel: panels.osdWrapper - bar: bar - borderThickness: win.borderThickness - deformAmount: 0.3 - x: panels.osdWrapper.x + panels.osd.x + bar.implicitWidth - implicitWidth: panels.osd.width - } - - PanelBg { - id: notifsBg - - blobGroup: blobGroup - panel: panels.notifications - bar: bar - borderThickness: win.borderThickness - } - - PanelBg { - id: utilsBg - - blobGroup: blobGroup - panel: panels.utilities - bar: bar - borderThickness: win.borderThickness - deformAmount: panels.sidebar.visible ? 0.1 : 0.15 - exclude: panels.sidebar.offsetScale > 0.08 ? [] : [sidebarBg] - topLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius - } - - PanelBg { - id: popoutBg - - blobGroup: blobGroup - panel: panels.popouts - bar: bar - borderThickness: win.borderThickness - - x: bar.implicitWidth - (panels.popouts.isDetached ? -(win.width - panels.popouts.shownWidth) / 2 : panels.popouts.hasCurrent ? 0 : panels.popouts.shownWidth + 5) - implicitWidth: panels.popouts.shownWidth - - Behavior on x { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - - Behavior on implicitWidth { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - } - } - - DrawerVisibilities { - id: visibilities - - Component.onCompleted: Visibilities.load(scope.modelData, this) - } - - Interactions { - screen: scope.modelData - popouts: panels.popouts - visibilities: visibilities - panels: panels - bar: bar - borderThickness: win.borderLayoutThickness - fullscreen: win.hasFullscreen - - Panels { - id: panels - - screen: scope.modelData - visibilities: visibilities - bar: bar - borderThickness: win.borderThickness - - utilities.horizontalStretch: (sidebarBg.rawDeformMatrix.m11 - 1) / 2 + 1 - - dashboard.transform: Matrix4x4 { - matrix: dashBg.deformMatrix - } - launcher.transform: Matrix4x4 { - matrix: launcherBg.deformMatrix - } - session.transform: Matrix4x4 { - matrix: sessionBg.deformMatrix - } - sidebar.transform: Matrix4x4 { - matrix: sidebarBg.deformMatrix - } - osd.transform: Matrix4x4 { - matrix: osdBg.deformMatrix - } - notifications.transform: Matrix4x4 { - matrix: notifsBg.deformMatrix - } - utilities.transform: Matrix4x4 { - matrix: utilsBg.deformMatrix - } - popouts.transform: Matrix4x4 { - matrix: popoutBg.deformMatrix - } - } - - BarWrapper { - id: bar - - anchors.top: parent.top - anchors.bottom: parent.bottom - - screen: scope.modelData - visibilities: visibilities - popouts: panels.popouts - - disabled: scope.barDisabled - fullscreen: win.hasFullscreen - - Component.onCompleted: Visibilities.bars.set(scope.modelData, this) - } - } } } - - component PanelBg: BlobRect { - required property BlobGroup blobGroup - required property Item panel - required property Item bar - required property real borderThickness - property real deformAmount: 0.15 - - group: panel.width > 0 && panel.height > 0 ? blobGroup : null - x: panel.x + bar.implicitWidth - y: panel.y + borderThickness - implicitWidth: panel.width - implicitHeight: panel.height - radius: Config.border.rounding - deformScale: deformAmount / 10000 - } } From c126075d40d670c38ddb00898c70134e6e23c5bc Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 6 Apr 2026 23:41:23 +1000 Subject: [PATCH 31/45] chore: clean out legacy background code --- modules/bar/popouts/Background.qml | 73 ---------------------- modules/dashboard/Background.qml | 66 -------------------- modules/drawers/Backgrounds.qml | 91 ---------------------------- modules/drawers/Border.qml | 45 -------------- modules/drawers/ContentWindow.qml | 9 --- modules/launcher/Background.qml | 60 ------------------ modules/notifications/Background.qml | 53 ---------------- modules/osd/Background.qml | 59 ------------------ modules/session/Background.qml | 60 ------------------ modules/sidebar/Background.qml | 50 --------------- 10 files changed, 566 deletions(-) delete mode 100644 modules/bar/popouts/Background.qml delete mode 100644 modules/dashboard/Background.qml delete mode 100644 modules/drawers/Backgrounds.qml delete mode 100644 modules/drawers/Border.qml delete mode 100644 modules/launcher/Background.qml delete mode 100644 modules/notifications/Background.qml delete mode 100644 modules/osd/Background.qml delete mode 100644 modules/session/Background.qml delete mode 100644 modules/sidebar/Background.qml diff --git a/modules/bar/popouts/Background.qml b/modules/bar/popouts/Background.qml deleted file mode 100644 index cfba86d3..00000000 --- a/modules/bar/popouts/Background.qml +++ /dev/null @@ -1,73 +0,0 @@ -import QtQuick -import QtQuick.Shapes -import qs.components -import qs.services -import qs.config - -ShapePath { - id: root - - required property Wrapper wrapper - required property bool invertBottomRounding - readonly property real rounding: wrapper.isDetached ? Appearance.rounding.normal : Config.border.rounding - readonly property bool flatten: wrapper.width < rounding * 2 - readonly property real roundingX: flatten ? wrapper.width / 2 : rounding - property real ibr: invertBottomRounding ? -1 : 1 - - property real sideRounding: startX > 0 ? -1 : 1 - - strokeWidth: -1 - fillColor: Colours.palette.m3surface - - PathArc { - relativeX: root.roundingX - relativeY: root.rounding * root.sideRounding - radiusX: Math.min(root.rounding, root.wrapper.width) - radiusY: root.rounding - direction: root.sideRounding < 0 ? PathArc.Clockwise : PathArc.Counterclockwise - } - PathLine { - relativeX: root.wrapper.width - root.roundingX * 2 - relativeY: 0 - } - PathArc { - relativeX: root.roundingX - relativeY: root.rounding - radiusX: Math.min(root.rounding, root.wrapper.width) - radiusY: root.rounding - } - PathLine { - relativeX: 0 - relativeY: root.wrapper.height - root.rounding * 2 - } - PathArc { - relativeX: -root.roundingX * root.ibr - relativeY: root.rounding - radiusX: Math.min(root.rounding, root.wrapper.width) - radiusY: root.rounding - direction: root.ibr < 0 ? PathArc.Counterclockwise : PathArc.Clockwise - } - PathLine { - relativeX: -(root.wrapper.width - root.roundingX - root.roundingX * root.ibr) - relativeY: 0 - } - PathArc { - relativeX: -root.roundingX - relativeY: root.rounding * root.sideRounding - radiusX: Math.min(root.rounding, root.wrapper.width) - radiusY: root.rounding - direction: root.sideRounding < 0 ? PathArc.Clockwise : PathArc.Counterclockwise - } - - Behavior on fillColor { - CAnim {} - } - - Behavior on ibr { - Anim {} - } - - Behavior on sideRounding { - Anim {} - } -} diff --git a/modules/dashboard/Background.qml b/modules/dashboard/Background.qml deleted file mode 100644 index c6223eb6..00000000 --- a/modules/dashboard/Background.qml +++ /dev/null @@ -1,66 +0,0 @@ -import QtQuick -import QtQuick.Shapes -import qs.components -import qs.services -import qs.config - -ShapePath { - id: root - - required property Wrapper wrapper - readonly property real rounding: Config.border.rounding - readonly property bool flatten: wrapper.height < rounding * 2 - readonly property real roundingY: flatten ? wrapper.height / 2 : rounding - - strokeWidth: -1 - fillColor: Colours.palette.m3surface - - PathArc { - relativeX: root.rounding - relativeY: root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - } - - PathLine { - relativeX: 0 - relativeY: root.wrapper.height - root.roundingY * 2 - } - - PathArc { - relativeX: root.rounding - relativeY: root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - direction: PathArc.Counterclockwise - } - - PathLine { - relativeX: root.wrapper.width - root.rounding * 2 - relativeY: 0 - } - - PathArc { - relativeX: root.rounding - relativeY: -root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - direction: PathArc.Counterclockwise - } - - PathLine { - relativeX: 0 - relativeY: -(root.wrapper.height - root.roundingY * 2) - } - - PathArc { - relativeX: root.rounding - relativeY: -root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - } - - Behavior on fillColor { - CAnim {} - } -} diff --git a/modules/drawers/Backgrounds.qml b/modules/drawers/Backgrounds.qml deleted file mode 100644 index 7592411c..00000000 --- a/modules/drawers/Backgrounds.qml +++ /dev/null @@ -1,91 +0,0 @@ -import QtQuick -import QtQuick.Shapes -import qs.config -import qs.modules.dashboard as Dashboard -import qs.modules.launcher as Launcher -import qs.modules.notifications as Notifications -import qs.modules.osd as Osd -import qs.modules.session as Session -import qs.modules.sidebar as Sidebar -import qs.modules.utilities as Utilities -import qs.modules.bar.popouts as BarPopouts - -Shape { - id: root - - required property Panels panels - required property Item bar - required property real borderThickness - required property real borderRounding - - anchors.fill: parent - anchors.margins: root.borderThickness - anchors.leftMargin: bar.implicitWidth - preferredRendererType: Shape.CurveRenderer - - Osd.Background { - wrapper: root.panels.osd // qmllint disable incompatible-type - rounding: Config.border.rounding - - startX: root.width - root.panels.session.width - root.panels.sidebar.width - startY: (root.height - wrapper.height) / 2 - rounding - } - - Notifications.Background { - wrapper: root.panels.notifications // qmllint disable incompatible-type - sidebar: sidebar - rounding: Config.border.rounding - - startX: root.width - startY: 0 - } - - Session.Background { - wrapper: root.panels.session // qmllint disable incompatible-type - - startX: root.width - root.panels.sidebar.width - startY: (root.height - wrapper.height) / 2 - rounding - } - - Launcher.Background { - wrapper: root.panels.launcher // qmllint disable incompatible-type - - startX: (root.width - wrapper.width) / 2 - rounding - startY: root.height - } - - Dashboard.Background { - wrapper: root.panels.dashboard // qmllint disable incompatible-type - - startX: (root.width - wrapper.width) / 2 - rounding - startY: 0 - } - - BarPopouts.Background { - wrapper: root.panels.popouts // qmllint disable incompatible-type - invertBottomRounding: wrapper.y + wrapper.height + 1 >= root.height - - startX: wrapper.x - startY: wrapper.y - rounding * sideRounding - } - - Utilities.Background { - wrapper: root.panels.utilities // qmllint disable incompatible-type - sidebar: sidebar - rounding: root.borderRounding - - startX: root.width - startY: root.height - } - - Sidebar.Background { - id: sidebar - - wrapper: root.panels.sidebar // qmllint disable incompatible-type - panels: root.panels - rounding: root.borderRounding - - startX: root.width - startY: root.panels.notifications.height - } -} diff --git a/modules/drawers/Border.qml b/modules/drawers/Border.qml deleted file mode 100644 index a638479d..00000000 --- a/modules/drawers/Border.qml +++ /dev/null @@ -1,45 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Effects -import qs.components -import qs.services - -Item { - id: root - - required property Item bar - required property real borderThickness - required property real borderRounding - - anchors.fill: parent - - StyledRect { - anchors.fill: parent - color: Colours.palette.m3surface - - layer.enabled: true - layer.effect: MultiEffect { - maskSource: mask - maskEnabled: true - maskInverted: true - maskThresholdMin: 0.5 - maskSpreadAtMin: 1 - } - } - - Item { - id: mask - - anchors.fill: parent - layer.enabled: true - visible: false - - Rectangle { - anchors.fill: parent - anchors.margins: root.borderThickness - anchors.leftMargin: root.bar.implicitWidth - radius: root.borderRounding - } - } -} diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index 7171c8c5..c161869a 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -130,15 +130,6 @@ StyledWindow { shadowColor: Qt.alpha(Colours.palette.m3shadow, Math.max(0, root.shadowOpacity)) } - // Border { - // bar: bar - // } - - // Backgrounds { - // panels: panels - // bar: bar - // } - BlobGroup { id: blobGroup diff --git a/modules/launcher/Background.qml b/modules/launcher/Background.qml deleted file mode 100644 index 508c75d4..00000000 --- a/modules/launcher/Background.qml +++ /dev/null @@ -1,60 +0,0 @@ -import QtQuick -import QtQuick.Shapes -import qs.components -import qs.services -import qs.config - -ShapePath { - id: root - - required property Wrapper wrapper - readonly property real rounding: Config.border.rounding - readonly property bool flatten: wrapper.height < rounding * 2 - readonly property real roundingY: flatten ? wrapper.height / 2 : rounding - - strokeWidth: -1 - fillColor: Colours.palette.m3surface - - PathArc { - relativeX: root.rounding - relativeY: -root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - direction: PathArc.Counterclockwise - } - PathLine { - relativeX: 0 - relativeY: -(root.wrapper.height - root.roundingY * 2) - } - PathArc { - relativeX: root.rounding - relativeY: -root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - } - PathLine { - relativeX: root.wrapper.width - root.rounding * 2 - relativeY: 0 - } - PathArc { - relativeX: root.rounding - relativeY: root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - } - PathLine { - relativeX: 0 - relativeY: root.wrapper.height - root.roundingY * 2 - } - PathArc { - relativeX: root.rounding - relativeY: root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - direction: PathArc.Counterclockwise - } - - Behavior on fillColor { - CAnim {} - } -} diff --git a/modules/notifications/Background.qml b/modules/notifications/Background.qml deleted file mode 100644 index 740cda10..00000000 --- a/modules/notifications/Background.qml +++ /dev/null @@ -1,53 +0,0 @@ -import QtQuick -import QtQuick.Shapes -import qs.components -import qs.services - -ShapePath { - id: root - - required property Wrapper wrapper - required property var sidebar - required property real rounding - readonly property bool flatten: wrapper.height < rounding * 2 - readonly property real roundingY: flatten ? wrapper.height / 2 : rounding - - strokeWidth: -1 - fillColor: Colours.palette.m3surface - - PathLine { - relativeX: -(root.wrapper.width + root.rounding) - relativeY: 0 - } - PathArc { - relativeX: root.rounding - relativeY: root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - } - PathLine { - relativeX: 0 - relativeY: root.wrapper.height - root.roundingY * 2 - } - PathArc { - relativeX: root.rounding - relativeY: root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - direction: PathArc.Counterclockwise - } - PathLine { - relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding * 2 : root.wrapper.width - relativeY: 0 - } - PathArc { - relativeX: root.rounding - relativeY: root.rounding - radiusX: root.rounding - radiusY: root.rounding - } - - Behavior on fillColor { - CAnim {} - } -} diff --git a/modules/osd/Background.qml b/modules/osd/Background.qml deleted file mode 100644 index 330c703e..00000000 --- a/modules/osd/Background.qml +++ /dev/null @@ -1,59 +0,0 @@ -import QtQuick -import QtQuick.Shapes -import qs.components -import qs.services - -ShapePath { - id: root - - required property Wrapper wrapper - required property real rounding - readonly property bool flatten: wrapper.width < rounding * 2 - readonly property real roundingX: flatten ? wrapper.width / 2 : rounding - - strokeWidth: -1 - fillColor: Colours.palette.m3surface - - PathArc { - relativeX: -root.roundingX - relativeY: root.rounding - radiusX: Math.min(root.rounding, root.wrapper.width) - radiusY: root.rounding - } - PathLine { - relativeX: -(root.wrapper.width - root.roundingX * 2) - relativeY: 0 - } - PathArc { - relativeX: -root.roundingX - relativeY: root.rounding - radiusX: Math.min(root.rounding, root.wrapper.width) - radiusY: root.rounding - direction: PathArc.Counterclockwise - } - PathLine { - relativeX: 0 - relativeY: root.wrapper.height - root.rounding * 2 - } - PathArc { - relativeX: root.roundingX - relativeY: root.rounding - radiusX: Math.min(root.rounding, root.wrapper.width) - radiusY: root.rounding - direction: PathArc.Counterclockwise - } - PathLine { - relativeX: root.wrapper.width - root.roundingX * 2 - relativeY: 0 - } - PathArc { - relativeX: root.roundingX - relativeY: root.rounding - radiusX: Math.min(root.rounding, root.wrapper.width) - radiusY: root.rounding - } - - Behavior on fillColor { - CAnim {} - } -} diff --git a/modules/session/Background.qml b/modules/session/Background.qml deleted file mode 100644 index a609f460..00000000 --- a/modules/session/Background.qml +++ /dev/null @@ -1,60 +0,0 @@ -import QtQuick -import QtQuick.Shapes -import qs.components -import qs.services -import qs.config - -ShapePath { - id: root - - required property Wrapper wrapper - readonly property real rounding: Config.border.rounding - readonly property bool flatten: wrapper.width < rounding * 2 - readonly property real roundingX: flatten ? wrapper.width / 2 : rounding - - strokeWidth: -1 - fillColor: Colours.palette.m3surface - - PathArc { - relativeX: -root.roundingX - relativeY: root.rounding - radiusX: Math.min(root.rounding, root.wrapper.width) - radiusY: root.rounding - } - PathLine { - relativeX: -(root.wrapper.width - root.roundingX * 2) - relativeY: 0 - } - PathArc { - relativeX: -root.roundingX - relativeY: root.rounding - radiusX: Math.min(root.rounding, root.wrapper.width) - radiusY: root.rounding - direction: PathArc.Counterclockwise - } - PathLine { - relativeX: 0 - relativeY: root.wrapper.height - root.rounding * 2 - } - PathArc { - relativeX: root.roundingX - relativeY: root.rounding - radiusX: Math.min(root.rounding, root.wrapper.width) - radiusY: root.rounding - direction: PathArc.Counterclockwise - } - PathLine { - relativeX: root.wrapper.width - root.roundingX * 2 - relativeY: 0 - } - PathArc { - relativeX: root.roundingX - relativeY: root.rounding - radiusX: Math.min(root.rounding, root.wrapper.width) - radiusY: root.rounding - } - - Behavior on fillColor { - CAnim {} - } -} diff --git a/modules/sidebar/Background.qml b/modules/sidebar/Background.qml deleted file mode 100644 index c7d42123..00000000 --- a/modules/sidebar/Background.qml +++ /dev/null @@ -1,50 +0,0 @@ -import QtQuick -import QtQuick.Shapes -import qs.components -import qs.services - -ShapePath { - id: root - - required property Wrapper wrapper - required property var panels - required property real rounding - - readonly property real notifsWidthDiff: panels.notifications.width - wrapper.width - readonly property real notifsRoundingX: panels.notifications.height > 0 && notifsWidthDiff < rounding * 2 ? notifsWidthDiff / 2 : rounding - - readonly property real utilsWidthDiff: panels.utilities.width - wrapper.width - readonly property real utilsRoundingX: utilsWidthDiff < rounding * 2 ? utilsWidthDiff / 2 : rounding - - strokeWidth: -1 - fillColor: Colours.palette.m3surface - - PathLine { - relativeX: -root.wrapper.width - root.notifsRoundingX - relativeY: 0 - } - PathArc { - relativeX: root.notifsRoundingX - relativeY: root.rounding - radiusX: root.notifsRoundingX - radiusY: root.rounding - } - PathLine { - relativeX: 0 - relativeY: root.wrapper.height - root.rounding * 2 - } - PathArc { - relativeX: -root.utilsRoundingX - relativeY: root.rounding - radiusX: root.utilsRoundingX - radiusY: root.rounding - } - PathLine { - relativeX: root.wrapper.width + root.utilsRoundingX - relativeY: 0 - } - - Behavior on fillColor { - CAnim {} - } -} From ed33b3d8fffd01454f24c76d3daaaaf55f442dee Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Tue, 7 Apr 2026 03:13:07 +1000 Subject: [PATCH 32/45] dev: update direnv watch paths --- .envrc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.envrc b/.envrc index c90b500c..5a259560 100644 --- a/.envrc +++ b/.envrc @@ -3,10 +3,11 @@ if has nix; then fi shopt -s globstar -watch_file assets/cpp/**/*.cpp -watch_file assets/cpp/**/*.hpp watch_file plugin/**/*.cpp watch_file plugin/**/*.hpp +watch_file plugin/**/*.qml +watch_file plugin/**/*.vert +watch_file plugin/**/*.frag watch_file **/CMakeLists.txt cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_CXX_COMPILER=clazy -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DDISTRIBUTOR=direnv From 31e29c16a8cc1d25d326a968b768f1ebfef6c334 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:01:41 +1000 Subject: [PATCH 33/45] fix: blob sdf rect-rect edge sink --- plugin/src/Caelestia/Blobs/shaders/blob.frag | 78 ++++++++++---------- 1 file changed, 37 insertions(+), 41 deletions(-) diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.frag b/plugin/src/Caelestia/Blobs/shaders/blob.frag index 808cf309..fd412e1c 100644 --- a/plugin/src/Caelestia/Blobs/shaders/blob.frag +++ b/plugin/src/Caelestia/Blobs/shaders/blob.frag @@ -143,60 +143,56 @@ void main() { d *= scale; } - // Rect-to-rect edge sink: indent this rect's edge where another - // rect is slightly past it, fading once past threshold. - if (rectCount > 1) { - vec2 iSh = sh.xy; - float sinkT = smoothFactor * 0.75; - float sinkOff = smoothFactor * (1.0 / 6.0); - float rectSinkVal = 0.0; + // Rect-to-rect edge sinks: track the same edge of neighboring rects + { + float rectSinkValue = 0.0; + vec2 iHalf = sh.xy; + float preOff = smoothFactor * (1.0/6.0); for (int j = 0; j < rectCount; j++) { if (j == i) continue; - vec4 jR = rectData[j * 5]; - vec4 jP = rectData[j * 5 + 1]; + vec4 jRect = rectData[j * 5]; + vec4 jProps = rectData[j * 5 + 1]; vec2 jSh = rectData[j * 5 + 3].xy; - vec2 jC = jR.xy + jP.yz; + vec2 jCtr = jRect.xy + jProps.yz; - // Skip non-adjacent rects - float sinkRange = smoothFactor * 1.5; - if (abs(center.x - jC.x) > iSh.x + jSh.x + sinkRange || - abs(center.y - jC.y) > iSh.y + jSh.y + sinkRange) - continue; + // Per-edge containment: the other rect's full span on the + // perpendicular axis must be inside this rect for that edge. + bool hInside = (jCtr.y - jSh.y) >= (center.y - iHalf.y) && + (jCtr.y + jSh.y) <= (center.y + iHalf.y); + bool vInside = (jCtr.x - jSh.x) >= (center.x - iHalf.x) && + (jCtr.x + jSh.x) <= (center.x + iHalf.x); - // Penetration of j past i's edges (positive = past) - float pT = (jC.y + jSh.y) - (center.y - iSh.y) - sinkOff; - float pB = (center.y + iSh.y) - (jC.y - jSh.y) - sinkOff; - float pL = (jC.x + jSh.x) - (center.x - iSh.x) - sinkOff; - float pR = (center.x + iSh.x) - (jC.x - jSh.x) - sinkOff; + // Top/Bottom: other rect's height must be inside this rect + float topPen = hInside ? clamp((center.y - iHalf.y) - (jCtr.y - jSh.y) - preOff, 0.0, smoothFactor) : 0.0; + float botPen = hInside ? clamp((jCtr.y + jSh.y) - (center.y + iHalf.y) - preOff, 0.0, smoothFactor) : 0.0; - // Smooth bump: rises then falls, zero outside [0, sinkT] - float aT = smoothstep(0.0, sinkT * 0.4, pT) * (1.0 - smoothstep(sinkT * 0.5, sinkT, pT)); - float aB = smoothstep(0.0, sinkT * 0.4, pB) * (1.0 - smoothstep(sinkT * 0.5, sinkT, pB)); - float aL = smoothstep(0.0, sinkT * 0.4, pL) * (1.0 - smoothstep(sinkT * 0.5, sinkT, pL)); - float aR = smoothstep(0.0, sinkT * 0.4, pR) * (1.0 - smoothstep(sinkT * 0.5, sinkT, pR)); + // Left/Right: other rect's width must be inside this rect + float leftPen = vInside ? clamp((center.x - iHalf.x) - (jCtr.x - jSh.x) - preOff, 0.0, smoothFactor) : 0.0; + float rightPen = vInside ? clamp((jCtr.x + jSh.x) - (center.x + iHalf.x) - preOff, 0.0, smoothFactor) : 0.0; - // Lateral falloff from rect j's extent - float hLat = max(abs(pixel.x - jC.x) - jSh.x, 0.0); - float vLat = max(abs(pixel.y - jC.y) - jSh.y, 0.0); - float latF = smoothFactor * 2.0; + // Lateral distance from pixel to other rect's extent along each edge + float hLat = max(abs(pixel.x - jCtr.x) - jSh.x, 0.0); + float vLat = max(abs(pixel.y - jCtr.y) - jSh.y, 0.0); - // Perpendicular zone (near rect i's edge only) - float zT = 1.0 - smoothstep(center.y - iSh.y, center.y - iSh.y + smoothFactor, pixel.y); - float zB = smoothstep(center.y + iSh.y - smoothFactor, center.y + iSh.y, pixel.y); - float zL = 1.0 - smoothstep(center.x - iSh.x, center.x - iSh.x + smoothFactor, pixel.x); - float zR = smoothstep(center.x + iSh.x - smoothFactor, center.x + iSh.x, pixel.x); + // Perpendicular proximity: full strength at edge, fade inside + float topZone = 1.0 - smoothstep(center.y - iHalf.y, center.y - iHalf.y + smoothFactor, pixel.y); + float botZone = smoothstep(center.y + iHalf.y - smoothFactor, center.y + iHalf.y, pixel.y); + float leftZone = 1.0 - smoothstep(center.x - iHalf.x, center.x - iHalf.x + smoothFactor, pixel.x); + float rightZone = smoothstep(center.x + iHalf.x - smoothFactor, center.x + iHalf.x, pixel.x); - float s = max( - max(aT * smoothstep(latF, 0.0, hLat) * zT, - aB * smoothstep(latF, 0.0, hLat) * zB), - max(aL * smoothstep(latF, 0.0, vLat) * zL, - aR * smoothstep(latF, 0.0, vLat) * zR) + float s = smoothFactor * 2.0; + float sink = max( + max(topPen * smoothstep(s, 0.0, hLat) * topZone, + botPen * smoothstep(s, 0.0, hLat) * botZone), + max(leftPen * smoothstep(s, 0.0, vLat) * leftZone, + rightPen * smoothstep(s, 0.0, vLat) * rightZone) ); - rectSinkVal = max(rectSinkVal, s); + rectSinkValue = max(rectSinkValue, sink); } - d += rectSinkVal * smoothFactor * 0.25; + + d -= rectSinkValue; } mergedSdf = sminNoBulge(mergedSdf, d, smoothFactor); From 9ce0224d0adf89ca05f540122a4f65d6a17fc46c Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:02:17 +1000 Subject: [PATCH 34/45] fix: reduce deform amount --- modules/drawers/ContentWindow.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index c161869a..cc09bbbd 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -169,7 +169,7 @@ StyledWindow { id: sessionBg panel: panels.sessionWrapper - deformAmount: 0.25 + deformAmount: 0.2 x: panels.sessionWrapper.x + panels.session.x + bar.implicitWidth implicitWidth: panels.session.width } @@ -178,7 +178,7 @@ StyledWindow { id: sidebarBg panel: panels.sidebar - deformAmount: 0.05 + deformAmount: 0.03 implicitHeight: panel.height * (1 / rawDeformMatrix.m22) + 2 exclude: panels.sidebar.offsetScale > 0.08 ? [] : [utilsBg] bottomLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius @@ -188,7 +188,7 @@ StyledWindow { id: osdBg panel: panels.osdWrapper - deformAmount: 0.3 + deformAmount: 0.25 x: panels.osdWrapper.x + panels.osd.x + bar.implicitWidth implicitWidth: panels.osd.width } From d778734c679404595377959fd1885052f4cd880d Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:14:50 +1000 Subject: [PATCH 35/45] fix: remove sminNoBulge cause useless --- plugin/src/Caelestia/Blobs/shaders/blob.frag | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.frag b/plugin/src/Caelestia/Blobs/shaders/blob.frag index fd412e1c..e7a33496 100644 --- a/plugin/src/Caelestia/Blobs/shaders/blob.frag +++ b/plugin/src/Caelestia/Blobs/shaders/blob.frag @@ -46,14 +46,6 @@ float smin(float a, float b, float k) { return min(a, b) - h * h * h * k * (1.0/6.0); } -float sminNoBulge(float a, float b, float k) { - // Cubic smooth min with reduced outward expansion when shapes overlap - float h = max(k - abs(a - b), 0.0) / k; - float blend = h * h * h * k * (1.0/6.0); - blend *= smoothstep(-k, 0.0, min(a, b)); - return min(a, b) - blend; -} - float smax(float a, float b, float k) { float h = max(k - abs(a - b), 0.0) / k; return max(a, b) + h * h * h * k * (1.0/6.0); @@ -195,7 +187,7 @@ void main() { d -= rectSinkValue; } - mergedSdf = sminNoBulge(mergedSdf, d, smoothFactor); + mergedSdf = smin(mergedSdf, d, smoothFactor); if (d < smoothFactor && d < minDist) { minDist = d; owner = i; From 77ab4b835013b93bce50805894aef089156773d3 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:39:48 +1000 Subject: [PATCH 36/45] fix: blobs ignoring updates if moving too slow --- plugin/src/Caelestia/Blobs/blobshape.cpp | 11 +++++++---- plugin/src/Caelestia/Blobs/blobshape.hpp | 2 ++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/plugin/src/Caelestia/Blobs/blobshape.cpp b/plugin/src/Caelestia/Blobs/blobshape.cpp index 96662150..718c03e7 100644 --- a/plugin/src/Caelestia/Blobs/blobshape.cpp +++ b/plugin/src/Caelestia/Blobs/blobshape.cpp @@ -69,13 +69,16 @@ void BlobShape::geometryChange(const QRectF& newGeometry, const QRectF& oldGeome QQuickItem::geometryChange(newGeometry, oldGeometry); updateCenteredDeformMatrix(); if (m_group) { - // Only trigger redraw if the change is visually meaningful - const auto dx = std::abs(newGeometry.x() - oldGeometry.x()); - const auto dy = std::abs(newGeometry.y() - oldGeometry.y()); + // Accumulate sub-pixel drift so slow movements don't desync the shader + m_pendingDx += static_cast(newGeometry.x() - oldGeometry.x()); + m_pendingDy += static_cast(newGeometry.y() - oldGeometry.y()); const auto dw = std::abs(newGeometry.width() - oldGeometry.width()); const auto dh = std::abs(newGeometry.height() - oldGeometry.height()); - if (dx > 0.5 || dy > 0.5 || dw > 0.5 || dh > 0.5) + if (std::abs(m_pendingDx) > 0.5f || std::abs(m_pendingDy) > 0.5f || dw > 0.5 || dh > 0.5) { + m_pendingDx = 0; + m_pendingDy = 0; m_group->markShapeDirty(this); + } } } diff --git a/plugin/src/Caelestia/Blobs/blobshape.hpp b/plugin/src/Caelestia/Blobs/blobshape.hpp index c9d98504..c05a40d8 100644 --- a/plugin/src/Caelestia/Blobs/blobshape.hpp +++ b/plugin/src/Caelestia/Blobs/blobshape.hpp @@ -70,6 +70,8 @@ protected: QRectF m_localPaddedRect; QVector m_cachedRects; int m_cachedMyIndex = -2; + float m_pendingDx = 0; + float m_pendingDy = 0; bool m_cachedHasInverted = false; float m_cachedInvertedRadius = 0; float m_cachedInvertedOuter[4] = {}; From 293b6f4b2bdc3e43dd4323c7d547612d13b60163 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Tue, 7 Apr 2026 22:00:38 +1000 Subject: [PATCH 37/45] feat: expressive effects -> slow spatial We don't use the effect curve anyways, standard is better --- config/AppearanceConfig.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/AppearanceConfig.qml b/config/AppearanceConfig.qml index 3d590dca..cee140f6 100644 --- a/config/AppearanceConfig.qml +++ b/config/AppearanceConfig.qml @@ -65,7 +65,7 @@ JsonObject { property list standardDecel: [0, 0, 0, 1, 1, 1] property list expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1] property list expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1] - property list expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1] + property list expressiveSlowSpatial: [0.39, 1.29, 0.35, 0.98, 1, 1] } component AnimDurations: JsonObject { @@ -76,7 +76,7 @@ JsonObject { property int extraLarge: 1000 * scale property int expressiveFastSpatial: 350 * scale property int expressiveDefaultSpatial: 500 * scale - property int expressiveEffects: 200 * scale + property int expressiveSlowSpatial: 650 * scale } component Anim: JsonObject { From df27c93224f3577b9d9e0e31d5ba1e662c499a90 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:06:37 +1000 Subject: [PATCH 38/45] feat: improve popouts Fix bg tracking, use slow spatial for detach, default spatial for everything else, adjust deform, move instead of width scale --- modules/bar/popouts/ClipWrapper.qml | 55 +++++++++++++++++++ modules/bar/popouts/Wrapper.qml | 70 +++++++++---------------- modules/controlcenter/ControlCenter.qml | 3 +- modules/controlcenter/WindowFactory.qml | 5 +- modules/drawers/ContentWindow.qml | 21 ++------ modules/drawers/Interactions.qml | 2 +- modules/drawers/Panels.qml | 21 ++++---- modules/drawers/Regions.qml | 3 +- 8 files changed, 101 insertions(+), 79 deletions(-) create mode 100644 modules/bar/popouts/ClipWrapper.qml diff --git a/modules/bar/popouts/ClipWrapper.qml b/modules/bar/popouts/ClipWrapper.qml new file mode 100644 index 00000000..74dec841 --- /dev/null +++ b/modules/bar/popouts/ClipWrapper.qml @@ -0,0 +1,55 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import qs.components +import qs.config + +Item { + id: root + + required property ShellScreen screen + + readonly property alias content: content + property real offsetScale: x > 0 || content.hasCurrent ? 0 : 1 + + visible: width > 0 && height > 0 + clip: true + + implicitWidth: content.implicitWidth * (1 - offsetScale) + implicitHeight: content.implicitHeight + + Behavior on offsetScale { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + Behavior on x { + Anim { + duration: content.animLength + easing.bezierCurve: content.animCurve + } + } + + Behavior on y { + enabled: root.offsetScale < 1 + + Anim { + duration: content.animLength + easing.bezierCurve: content.animCurve + } + } + + Wrapper { + id: content + + screen: root.screen + offsetScale: root.offsetScale + + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: (-implicitWidth - 5) * root.offsetScale + } +} diff --git a/modules/bar/popouts/Wrapper.qml b/modules/bar/popouts/Wrapper.qml index 99440865..a04a5e8a 100644 --- a/modules/bar/popouts/Wrapper.qml +++ b/modules/bar/popouts/Wrapper.qml @@ -14,46 +14,50 @@ Item { id: root required property ShellScreen screen + required property real offsetScale - readonly property real shownWidth: children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth - readonly property real nonAnimWidth: x > 0 || hasCurrent ? shownWidth : 0 + readonly property alias content: content + readonly property alias winfo: winfo + readonly property alias controlCenter: controlCenter + + readonly property real nonAnimWidth: children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth readonly property real nonAnimHeight: children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight readonly property Item current: (content.item as Content)?.current ?? null + readonly property bool isDetached: detachedMode.length > 0 property alias currentName: popoutState.currentName - property real currentCenter property alias hasCurrent: popoutState.hasCurrent - readonly property PopoutState popState: popoutState + property real currentCenter property string detachedMode property string queuedMode - readonly property bool isDetached: detachedMode.length > 0 - property int animLength: Appearance.anim.durations.normal - property list animCurve: Appearance.anim.curves.emphasized + property int animLength: Appearance.anim.durations.expressiveDefaultSpatial + property list animCurve: Appearance.anim.curves.expressiveDefaultSpatial + + function setAnims(detach: bool): void { + const type = `expressive${detach ? "Slow" : "Default"}Spatial`; + animLength = Appearance.anim.durations[type]; + animCurve = Appearance.anim.curves[type]; + } function detach(mode: string): void { - animLength = Appearance.anim.durations.large; + setAnims(true); if (mode === "winfo") { detachedMode = mode; } else { queuedMode = mode; detachedMode = "any"; } + setAnims(false); focus = true; } function close(): void { hasCurrent = false; - animCurve = Appearance.anim.curves.emphasizedAccel; - animLength = Appearance.anim.durations.normal; detachedMode = ""; - animCurve = Appearance.anim.curves.emphasized; } - visible: width > 0 && height > 0 - clip: true - implicitWidth: nonAnimWidth implicitHeight: nonAnimHeight @@ -90,15 +94,7 @@ Item { } Binding { - when: root.isDetached - - target: QsWindow.window - property: "WlrLayershell.keyboardFocus" - value: WlrKeyboardFocus.OnDemand - } - - Binding { - when: root.hasCurrent && root.currentName === "wirelesspassword" + when: root.isDetached || (root.hasCurrent && root.currentName === "wirelesspassword") target: QsWindow.window property: "WlrLayershell.keyboardFocus" @@ -118,6 +114,8 @@ Item { } Comp { + id: winfo + shouldBeActive: root.detachedMode === "winfo" anchors.centerIn: parent @@ -128,32 +126,15 @@ Item { } Comp { + id: controlCenter + shouldBeActive: root.detachedMode === "any" anchors.centerIn: parent sourceComponent: ControlCenter { - function close(): void { - root.close(); - } - screen: root.screen active: root.queuedMode - } - } - - Behavior on x { - Anim { - duration: root.animLength - easing.bezierCurve: root.animCurve - } - } - - Behavior on y { - enabled: root.implicitWidth > 0 - - Anim { - duration: root.animLength - easing.bezierCurve: root.animCurve + onClose: root.close() } } @@ -165,7 +146,7 @@ Item { } Behavior on implicitHeight { - enabled: root.implicitWidth > 0 + enabled: root.offsetScale < 1 Anim { duration: root.animLength @@ -181,6 +162,7 @@ Item { active: false opacity: 0 + // Makes the loader load on the same frame shouldBeActive becomes true, which ensures size is set states: State { name: "active" when: comp.shouldBeActive diff --git a/modules/controlcenter/ControlCenter.qml b/modules/controlcenter/ControlCenter.qml index f542b597..5b067fc0 100644 --- a/modules/controlcenter/ControlCenter.qml +++ b/modules/controlcenter/ControlCenter.qml @@ -25,8 +25,7 @@ Item { root: root } - function close(): void { - } + signal close implicitWidth: implicitHeight * Config.controlCenter.sizes.ratio implicitHeight: screen.height * Config.controlCenter.sizes.heightMult diff --git a/modules/controlcenter/WindowFactory.qml b/modules/controlcenter/WindowFactory.qml index 266af909..dc0dc4a0 100644 --- a/modules/controlcenter/WindowFactory.qml +++ b/modules/controlcenter/WindowFactory.qml @@ -45,12 +45,9 @@ Singleton { ControlCenter { id: cc - function close(): void { - win.destroy(); - } - anchors.fill: parent screen: win.screen + onClose: win.destroy() floating: true } diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index cc09bbbd..f914be2d 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -211,23 +211,10 @@ StyledWindow { PanelBg { id: popoutBg - panel: panels.popouts - x: bar.implicitWidth - (panels.popouts.isDetached ? -(root.width - panels.popouts.shownWidth) / 2 : panels.popouts.hasCurrent ? 0 : panels.popouts.shownWidth + 5) - implicitWidth: panels.popouts.shownWidth - - Behavior on x { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - - Behavior on implicitWidth { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } + panel: panels.popoutsWrapper + deformAmount: panels.popouts.isDetached ? 0.05 : panels.popouts.hasCurrent ? 0.15 : 0.1 + x: panels.popoutsWrapper.x + panels.popouts.x + bar.implicitWidth + implicitWidth: panels.popouts.width } } diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index aeabf401..ad17a0f2 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -211,7 +211,7 @@ CustomMouseArea { // Show popouts on hover if (x < bar.implicitWidth) { bar.checkPopout(y); - } else if ((!popouts.currentName.startsWith("traymenu") || ((popouts.current as StackView)?.depth ?? 0) <= 1) && !inLeftPanel(panels.popouts, x, y)) { + } else if ((!popouts.currentName.startsWith("traymenu") || ((popouts.current as StackView)?.depth ?? 0) <= 1) && !inLeftPanel(panels.popoutsWrapper, x, y)) { popouts.hasCurrent = false; bar.closeTray(); } diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index b018b94d..3cfabaab 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -28,13 +28,14 @@ Item { readonly property alias sessionWrapper: sessionWrapper readonly property alias launcher: launcher readonly property alias dashboard: dashboard - readonly property alias popouts: popouts + readonly property alias popouts: popoutsWrapper.content + readonly property alias popoutsWrapper: popoutsWrapper readonly property alias utilities: utilities readonly property alias toasts: toasts readonly property alias sidebar: sidebar anchors.fill: parent - anchors.margins: root.borderThickness + anchors.margins: borderThickness anchors.leftMargin: bar.implicitWidth Item { @@ -114,18 +115,18 @@ Item { anchors.top: parent.top } - BarPopouts.Wrapper { - id: popouts + BarPopouts.ClipWrapper { + id: popoutsWrapper screen: root.screen - x: isDetached ? (root.width - nonAnimWidth) / 2 : 0 + x: content.isDetached ? (root.width - content.nonAnimWidth) / 2 : 0 y: { - if (isDetached) - return (root.height - nonAnimHeight) / 2; + if (content.isDetached) + return (root.height - content.nonAnimHeight) / 2; - const off = currentCenter - root.borderThickness - nonAnimHeight / 2; - const diff = root.height - Math.floor(off + nonAnimHeight); + const off = content.currentCenter - root.borderThickness - content.nonAnimHeight / 2; + const diff = root.height - Math.floor(off + content.nonAnimHeight); if (diff < 0) return off + diff; return Math.max(off, 0); @@ -137,7 +138,7 @@ Item { visibilities: root.visibilities sidebar: sidebar - popouts: popouts + popouts: popoutsWrapper.content anchors.bottom: parent.bottom anchors.right: parent.right diff --git a/modules/drawers/Regions.qml b/modules/drawers/Regions.qml index c646809f..d4281762 100644 --- a/modules/drawers/Regions.qml +++ b/modules/drawers/Regions.qml @@ -65,7 +65,8 @@ Region { } R { - panel: root.panels.popouts + panel: root.panels.popoutsWrapper + width: panel.width * (1 - root.panels.popoutsWrapper.offsetScale) } component R: Region { From ec4a15c1d17853064228ac228f7b336d805bfd84 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:06:49 +1000 Subject: [PATCH 39/45] fix: undef warning in winfo --- modules/windowinfo/Buttons.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/windowinfo/Buttons.qml b/modules/windowinfo/Buttons.qml index 15f71956..3f0bf4de 100644 --- a/modules/windowinfo/Buttons.qml +++ b/modules/windowinfo/Buttons.qml @@ -120,7 +120,7 @@ ColumnLayout { Loader { asynchronous: true - active: root.client?.lastIpcObject.floating + active: root.client?.lastIpcObject.floating ?? false Layout.fillWidth: active Layout.leftMargin: active ? 0 : -parent.spacing Layout.rightMargin: active ? 0 : -parent.spacing From ff5c46a4294e1bd99809635797d9c763c191f1f5 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:07:35 +1000 Subject: [PATCH 40/45] fix: don't exclude panels from group based on size Most panels do not change size either way, and doing this removes the close sink effect --- modules/drawers/ContentWindow.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index f914be2d..113a3fea 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -289,7 +289,7 @@ StyledWindow { required property Item panel property real deformAmount: 0.15 - group: panel.width > 0 && panel.height > 0 ? blobGroup : null + group: blobGroup x: panel.x + bar.implicitWidth y: panel.y + root.borderThickness implicitWidth: panel.width From b1380235ebe549865b235b1b3433c4a00d16ea8b Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:20:58 +1000 Subject: [PATCH 41/45] fix: prevent deform gap between bar and popouts --- modules/drawers/ContentWindow.qml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index 113a3fea..ff0e847a 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -211,10 +211,20 @@ StyledWindow { PanelBg { id: popoutBg + // Extra width to prevent vertical movement deformation partially detaching panel from bar + property real extraWidth: panels.popouts.isDetached ? 0 : 0.2 + panel: panels.popoutsWrapper deformAmount: panels.popouts.isDetached ? 0.05 : panels.popouts.hasCurrent ? 0.15 : 0.1 - x: panels.popoutsWrapper.x + panels.popouts.x + bar.implicitWidth - implicitWidth: panels.popouts.width + x: panels.popoutsWrapper.x + panels.popouts.x + bar.implicitWidth - panels.popouts.width * extraWidth + implicitWidth: panels.popouts.width * (1 + extraWidth) + + Behavior on extraWidth { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } } } From eaa6aefa6c7b9f526829a62d7357bf1b1724b48a Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 8 Apr 2026 18:44:38 +1000 Subject: [PATCH 42/45] fix: popout transition anim --- modules/bar/popouts/Content.qml | 5 +---- modules/bar/popouts/Wrapper.qml | 3 +-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml index 1c4792e1..71a8c5eb 100644 --- a/modules/bar/popouts/Content.qml +++ b/modules/bar/popouts/Content.qml @@ -14,8 +14,6 @@ Item { readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null readonly property Item current: currentPopout?.item ?? null - anchors.centerIn: parent - implicitWidth: (currentPopout?.implicitWidth ?? 0) + Appearance.padding.large * 2 implicitHeight: (currentPopout?.implicitHeight ?? 0) + Appearance.padding.large * 2 @@ -171,8 +169,7 @@ Item { required property string name readonly property bool shouldBeActive: root.popouts.currentName === name - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right + anchors.centerIn: parent opacity: 0 scale: 0.8 diff --git a/modules/bar/popouts/Wrapper.qml b/modules/bar/popouts/Wrapper.qml index a04a5e8a..04212e28 100644 --- a/modules/bar/popouts/Wrapper.qml +++ b/modules/bar/popouts/Wrapper.qml @@ -105,8 +105,7 @@ Item { id: content shouldBeActive: root.hasCurrent && !root.detachedMode - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter + anchors.fill: parent sourceComponent: Content { popouts: popoutState From e1e0c314c656e0f8d623a81b4999876c5bcc593b Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 8 Apr 2026 19:35:57 +1000 Subject: [PATCH 43/45] refactor: move popout position logic to wrapper --- modules/bar/popouts/ClipWrapper.qml | 13 +++++++++++++ modules/drawers/Panels.qml | 13 +------------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/modules/bar/popouts/ClipWrapper.qml b/modules/bar/popouts/ClipWrapper.qml index 74dec841..a69dff12 100644 --- a/modules/bar/popouts/ClipWrapper.qml +++ b/modules/bar/popouts/ClipWrapper.qml @@ -9,6 +9,7 @@ Item { id: root required property ShellScreen screen + required property real borderThickness readonly property alias content: content property real offsetScale: x > 0 || content.hasCurrent ? 0 : 1 @@ -19,6 +20,18 @@ Item { implicitWidth: content.implicitWidth * (1 - offsetScale) implicitHeight: content.implicitHeight + x: content.isDetached ? (parent.width - content.nonAnimWidth) / 2 : 0 + y: { + if (content.isDetached) + return (parent.height - content.nonAnimHeight) / 2; + + const off = content.currentCenter - borderThickness - content.nonAnimHeight / 2; + const diff = parent.height - Math.floor(off + content.nonAnimHeight); + if (diff < 0) + return off + diff; + return Math.max(off, 0); + } + Behavior on offsetScale { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index 3cfabaab..0d567414 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -119,18 +119,7 @@ Item { id: popoutsWrapper screen: root.screen - - x: content.isDetached ? (root.width - content.nonAnimWidth) / 2 : 0 - y: { - if (content.isDetached) - return (root.height - content.nonAnimHeight) / 2; - - const off = content.currentCenter - root.borderThickness - content.nonAnimHeight / 2; - const diff = root.height - Math.floor(off + content.nonAnimHeight); - if (diff < 0) - return off + diff; - return Math.max(off, 0); - } + borderThickness: root.borderThickness } Utilities.Wrapper { From 7f0acf15b3afebe05f866a3c253354c6ce9492c5 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 8 Apr 2026 19:42:11 +1000 Subject: [PATCH 44/45] chore: fix linter warnings --- modules/bar/popouts/ClipWrapper.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/bar/popouts/ClipWrapper.qml b/modules/bar/popouts/ClipWrapper.qml index a69dff12..bf2a0049 100644 --- a/modules/bar/popouts/ClipWrapper.qml +++ b/modules/bar/popouts/ClipWrapper.qml @@ -4,6 +4,7 @@ import QtQuick import Quickshell import qs.components import qs.config +import qs.modules.bar.popouts // Need to import this module so the Wrapper type is the same as others Item { id: root From c623b9b688b8e8ef557af4f01c5801ceafd61460 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 9 Apr 2026 00:34:26 +1000 Subject: [PATCH 45/45] fix: osd and session bulges Also remove rect-rect edge sink cause it doesn't work --- modules/osd/Wrapper.qml | 3 +- modules/session/Wrapper.qml | 3 +- plugin/src/Caelestia/Blobs/shaders/blob.frag | 52 -------------------- 3 files changed, 4 insertions(+), 54 deletions(-) diff --git a/modules/osd/Wrapper.qml b/modules/osd/Wrapper.qml index 585fb959..487b611f 100644 --- a/modules/osd/Wrapper.qml +++ b/modules/osd/Wrapper.qml @@ -17,6 +17,7 @@ Item { readonly property Brightness.Monitor monitor: Brightness.getMonitorForScreen(root.screen) readonly property bool shouldBeActive: visibilities.osd && Config.osd.enabled && !(visibilities.utilities && Config.utilities.enabled) property real offsetScale: shouldBeActive ? 0 : 1 + property real sidebarOffset: sidebarOrSessionVisible ? 12 : 0 property real volume property bool muted @@ -38,7 +39,7 @@ Item { } visible: offsetScale < 1 - anchors.rightMargin: (-implicitWidth - 5) * offsetScale + anchors.rightMargin: (-implicitWidth - 5 - sidebarOffset) * offsetScale implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight opacity: 1 - offsetScale diff --git a/modules/session/Wrapper.qml b/modules/session/Wrapper.qml index e48fff60..0d2d3b42 100644 --- a/modules/session/Wrapper.qml +++ b/modules/session/Wrapper.qml @@ -13,9 +13,10 @@ Item { readonly property bool shouldBeActive: visibilities.session && Config.session.enabled property real offsetScale: shouldBeActive ? 0 : 1 + property real sidebarOffset: sidebarVisible ? 14 : 0 visible: offsetScale < 1 - anchors.rightMargin: (-implicitWidth - 5) * offsetScale + anchors.rightMargin: (-implicitWidth - 5 - sidebarOffset) * offsetScale implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight || 510 // Hard coded fallback for first open opacity: 1 - offsetScale diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.frag b/plugin/src/Caelestia/Blobs/shaders/blob.frag index e7a33496..e78531b6 100644 --- a/plugin/src/Caelestia/Blobs/shaders/blob.frag +++ b/plugin/src/Caelestia/Blobs/shaders/blob.frag @@ -135,58 +135,6 @@ void main() { d *= scale; } - // Rect-to-rect edge sinks: track the same edge of neighboring rects - { - float rectSinkValue = 0.0; - vec2 iHalf = sh.xy; - float preOff = smoothFactor * (1.0/6.0); - - for (int j = 0; j < rectCount; j++) { - if (j == i) continue; - - vec4 jRect = rectData[j * 5]; - vec4 jProps = rectData[j * 5 + 1]; - vec2 jSh = rectData[j * 5 + 3].xy; - vec2 jCtr = jRect.xy + jProps.yz; - - // Per-edge containment: the other rect's full span on the - // perpendicular axis must be inside this rect for that edge. - bool hInside = (jCtr.y - jSh.y) >= (center.y - iHalf.y) && - (jCtr.y + jSh.y) <= (center.y + iHalf.y); - bool vInside = (jCtr.x - jSh.x) >= (center.x - iHalf.x) && - (jCtr.x + jSh.x) <= (center.x + iHalf.x); - - // Top/Bottom: other rect's height must be inside this rect - float topPen = hInside ? clamp((center.y - iHalf.y) - (jCtr.y - jSh.y) - preOff, 0.0, smoothFactor) : 0.0; - float botPen = hInside ? clamp((jCtr.y + jSh.y) - (center.y + iHalf.y) - preOff, 0.0, smoothFactor) : 0.0; - - // Left/Right: other rect's width must be inside this rect - float leftPen = vInside ? clamp((center.x - iHalf.x) - (jCtr.x - jSh.x) - preOff, 0.0, smoothFactor) : 0.0; - float rightPen = vInside ? clamp((jCtr.x + jSh.x) - (center.x + iHalf.x) - preOff, 0.0, smoothFactor) : 0.0; - - // Lateral distance from pixel to other rect's extent along each edge - float hLat = max(abs(pixel.x - jCtr.x) - jSh.x, 0.0); - float vLat = max(abs(pixel.y - jCtr.y) - jSh.y, 0.0); - - // Perpendicular proximity: full strength at edge, fade inside - float topZone = 1.0 - smoothstep(center.y - iHalf.y, center.y - iHalf.y + smoothFactor, pixel.y); - float botZone = smoothstep(center.y + iHalf.y - smoothFactor, center.y + iHalf.y, pixel.y); - float leftZone = 1.0 - smoothstep(center.x - iHalf.x, center.x - iHalf.x + smoothFactor, pixel.x); - float rightZone = smoothstep(center.x + iHalf.x - smoothFactor, center.x + iHalf.x, pixel.x); - - float s = smoothFactor * 2.0; - float sink = max( - max(topPen * smoothstep(s, 0.0, hLat) * topZone, - botPen * smoothstep(s, 0.0, hLat) * botZone), - max(leftPen * smoothstep(s, 0.0, vLat) * leftZone, - rightPen * smoothstep(s, 0.0, vLat) * rightZone) - ); - rectSinkValue = max(rectSinkValue, sink); - } - - d -= rectSinkValue; - } - mergedSdf = smin(mergedSdf, d, smoothFactor); if (d < smoothFactor && d < minDist) { minDist = d;