From 9a606f3e5823d3c31b4ed86133ec92300d26f9dc Mon Sep 17 00:00:00 2001 From: zach Date: Thu, 16 Apr 2026 01:50:29 +0200 Subject: [PATCH 1/7] test popouts --- Drawers/Panels.qml | 2 +- Drawers/Windows.qml | 115 ++++++- Modules/ClipWrapper.qml | 50 +++ Modules/Content.qml | 4 +- Modules/Wrapper.qml | 131 ++++---- Plugins/ZShell/Blobs/CMakeLists.txt | 19 ++ Plugins/ZShell/Blobs/blobgroup.cpp | 105 +++++++ Plugins/ZShell/Blobs/blobgroup.hpp | 61 ++++ Plugins/ZShell/Blobs/blobinvertedrect.cpp | 185 +++++++++++ Plugins/ZShell/Blobs/blobinvertedrect.hpp | 64 ++++ Plugins/ZShell/Blobs/blobmaterial.cpp | 96 ++++++ Plugins/ZShell/Blobs/blobmaterial.hpp | 44 +++ Plugins/ZShell/Blobs/blobrect.cpp | 245 +++++++++++++++ Plugins/ZShell/Blobs/blobrect.hpp | 140 +++++++++ Plugins/ZShell/Blobs/blobshape.cpp | 367 ++++++++++++++++++++++ Plugins/ZShell/Blobs/blobshape.hpp | 92 ++++++ Plugins/ZShell/Blobs/shaders/blob.frag | 302 ++++++++++++++++++ Plugins/ZShell/Blobs/shaders/blob.vert | 29 ++ Plugins/ZShell/CMakeLists.txt | 1 + 19 files changed, 1981 insertions(+), 71 deletions(-) create mode 100644 Modules/ClipWrapper.qml create mode 100644 Plugins/ZShell/Blobs/CMakeLists.txt create mode 100644 Plugins/ZShell/Blobs/blobgroup.cpp create mode 100644 Plugins/ZShell/Blobs/blobgroup.hpp create mode 100644 Plugins/ZShell/Blobs/blobinvertedrect.cpp create mode 100644 Plugins/ZShell/Blobs/blobinvertedrect.hpp create mode 100644 Plugins/ZShell/Blobs/blobmaterial.cpp create mode 100644 Plugins/ZShell/Blobs/blobmaterial.hpp create mode 100644 Plugins/ZShell/Blobs/blobrect.cpp create mode 100644 Plugins/ZShell/Blobs/blobrect.hpp create mode 100644 Plugins/ZShell/Blobs/blobshape.cpp create mode 100644 Plugins/ZShell/Blobs/blobshape.hpp create mode 100644 Plugins/ZShell/Blobs/shaders/blob.frag create mode 100644 Plugins/ZShell/Blobs/shaders/blob.vert diff --git a/Drawers/Panels.qml b/Drawers/Panels.qml index fd79941..6d09072 100644 --- a/Drawers/Panels.qml +++ b/Drawers/Panels.qml @@ -68,7 +68,7 @@ Item { visibilities: root.visibilities } - Modules.Wrapper { + Modules.ClipWrapper { id: popouts anchors.top: parent.top diff --git a/Drawers/Windows.qml b/Drawers/Windows.qml index 52cecf4..9cc51ee 100644 --- a/Drawers/Windows.qml +++ b/Drawers/Windows.qml @@ -144,16 +144,80 @@ Variants { shadowEnabled: true } - Border { - bar: bar - visibilities: visibilities + BlobGroup { + id: blobGroup + + color: DynamicColors.palette.m3surface + + Behavior on color { + CAnim { + } + } } - Backgrounds { - bar: bar - panels: panels - visibilities: visibilities - z: 1 + BlobInvertedRect { + anchors.fill: parent + anchors.margins: -50 + borderBottom: Config.barConfig.border - anchors.margins + borderLeft: Config.barConfig.border - anchors.margins + borderRight: Config.barConfig.border - anchors.margins + borderTop: Config.barConfig.border - anchors.margins + group: blobGroup + radius: Config.barConfig.rounding + } + + PanelBg { + id: dashBg + + deformAmount: 0.1 + panel: panels.dashboard + } + + PanelBg { + id: launcherBg + + deformAmount: 0.1 + panel: panels.launcher + } + + PanelBg { + id: sidebarBg + + bottomLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius + deformAmount: 0.03 + exclude: panels.sidebar.offsetscale > 0.08 ? [] : [utilsBg] + implicitHeight: panel.height * (1 / rawDeformMatrix.m22) + 2 + panel: panels.sidebar + } + + PanelBg { + id: osdBg + + deformAmount: 0.25 + panel: panels.osd + } + + PanelBg { + id: notifsBg + + panel: panels.notifications + } + + PanelBg { + id: utilsBg + + deformAmount: panels.sidebar.visible ? 0.1 : 0.15 + exclude: panels.sidebar.offsetScale > 0.08 ? [] : [sidebarBg] + panel: panels.utilities + topLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius + } + + PanelBg { + id: popoutBg + + deformAmount: 0.15 + implicitWidth: panels.popouts.width + panel: panels.popouts } } @@ -195,6 +259,28 @@ Variants { drawingItem: drawing screen: scope.modelData visibilities: visibilities + + dashboard.transform: Matrix4x4 { + matrix: dashBg.deformMatrix + } + launcher.transform: Matrix4x4 { + matrix: launcherBg.deformMatrix + } + notifications.transform: Matrix4x4 { + matrix: notifsBg.deformMatrix + } + osd.transform: Matrix4x4 { + matrix: osdBg.deformMatrix + } + popouts.transform: Matrix4x4 { + matrix: popoutBg.deformMatrix + } + sidebar.transform: Matrix4x4 { + matrix: sidebarBg.deformMatrix + } + utilities.transform: Matrix4x4 { + matrix: utilsBg.deformMatrix + } } BarLoader { @@ -209,4 +295,17 @@ Variants { } } } + + component PanelBg: BlobRect { + property real deformAmount: 0.15 + required property Item panel + + deformScale: deformAmount / 10000 + group: panel.width > 0 && panel.height > 0 ? blobGroup : null + implicitHeight: panel.height + implicitWidth: panel.width + radius: Appearance.rounding.smallest + x: panel.x + Config.barConfig.border + y: panel.y + bar.implicitHeight + } } diff --git a/Modules/ClipWrapper.qml b/Modules/ClipWrapper.qml new file mode 100644 index 0000000..6919f57 --- /dev/null +++ b/Modules/ClipWrapper.qml @@ -0,0 +1,50 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import qs.Components +import qs.Config + +Item { + id: root + + readonly property alias content: content + property real offsetScale: x > 0 || content.hasCurrent ? 0 : 1 + required property ShellScreen screen + + clip: true + implicitHeight: content.implicitHeight + implicitWidth: content.implicitWidth * (1 - offsetScale) + visible: width > 0 && height > 0 + + 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 + + anchors.left: parent.left + anchors.leftMargin: (-implicitWidth - 5) * root.offsetScale + anchors.verticalCenter: parent.verticalCenter + offsetScale: root.offsetScale + screen: root.screen + } +} diff --git a/Modules/Content.qml b/Modules/Content.qml index f52280b..1f17be3 100644 --- a/Modules/Content.qml +++ b/Modules/Content.qml @@ -110,9 +110,7 @@ Item { readonly property bool shouldBeActive: root.wrapper.currentName === name active: false - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.topMargin: 5 + anchors.centerIn: parent opacity: 0 scale: 0.8 diff --git a/Modules/Wrapper.qml b/Modules/Wrapper.qml index 337522d..88a0f59 100644 --- a/Modules/Wrapper.qml +++ b/Modules/Wrapper.qml @@ -8,100 +8,88 @@ import qs.Config Item { id: root - property list animCurve: MaterialEasing.emphasized - property int animLength: MaterialEasing.emphasizedDecelTime - readonly property Item current: content.item?.current ?? null + property list animCurve: Appearance.anim.curves.expressiveDefaultSpatial + property int animLength: Appearance.anim.durations.expressiveDefaultSpatial + readonly property alias content: content + readonly property alias controlCenter: controlCenter + readonly property Item current: (content.item as Content)?.current ?? null property real currentCenter - property string currentName + property alias currentName: popoutState.currentName property string detachedMode - property bool hasCurrent + property alias hasCurrent: popoutState.hasCurrent readonly property bool isDetached: detachedMode.length > 0 - readonly property real nonAnimHeight: hasCurrent ? children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight : 0 + readonly property real nonAnimHeight: children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight readonly property real nonAnimWidth: children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth + required property real offsetScale property string queuedMode required property ShellScreen screen + readonly property alias winfo: winfo function close(): void { hasCurrent = false; - animCurve = MaterialEasing.emphasizedDecel; - animLength = MaterialEasing.emphasizedDecelTime; detachedMode = ""; - animCurve = MaterialEasing.emphasized; } function detach(mode: string): void { - animLength = 600; + setAnims(true); if (mode === "winfo") { detachedMode = mode; } else { - detachedMode = "any"; queuedMode = mode; + detachedMode = "any"; } + setAnims(false); focus = true; } - clip: true + function setAnims(detach: bool): void { + const type = `expressive${detach ? "Slow" : "Default"}Spatial`; + animLength = Appearance.anim.durations[type]; + animCurve = Appearance.anim.curves[type]; + } + + focus: hasCurrent implicitHeight: nonAnimHeight implicitWidth: nonAnimWidth - visible: width > 0 && height > 0 Behavior on implicitHeight { + enabled: root.offsetScale < 1 + Anim { duration: root.animLength easing.bezierCurve: root.animCurve } } Behavior on implicitWidth { - enabled: root.implicitHeight > 0 - Anim { duration: root.animLength easing.bezierCurve: root.animCurve } } - // Comp { - // shouldBeActive: root.detachedMode === "winfo" - // asynchronous: true - // anchors.centerIn: parent - // - // sourceComponent: WindowInfo { - // screen: root.screen - // client: Hypr.activeToplevel - // } - // } - - // Comp { - // shouldBeActive: root.detachedMode === "any" - // asynchronous: true - // anchors.centerIn: parent - // - // sourceComponent: ControlCenter { - // screen: root.screen - // active: root.queuedMode - // - // function close(): void { - // root.close(); - // } - // } - // } - - Behavior on x { - enabled: root.implicitHeight > 0 - - Anim { - duration: root.animLength - easing.bezierCurve: root.animCurve + Keys.onEscapePressed: { + // Forward escape to password popout if active, otherwise close + if (currentName === "wirelesspassword" && content.item) { + const passwordPopout = (content.item as Content)?.children.find(c => c.name === "wirelesspassword"); + if (passwordPopout && passwordPopout.item) { + passwordPopout.item.closeDialog(); + return; + } } + close(); } - Behavior on y { - Anim { - duration: root.animLength - easing.bezierCurve: root.animCurve + Keys.onPressed: event => { + // Don't intercept keys when password popout is active - let it handle them + if (currentName === "wirelesspassword") { + event.accepted = false; } } - Keys.onEscapePressed: close() + PopoutState { + id: popoutState + + onDetachRequested: mode => root.detach(mode) + } HyprlandFocusGrab { active: root.isDetached @@ -114,19 +102,44 @@ Item { property: "WlrLayershell.keyboardFocus" target: QsWindow.window value: WlrKeyboardFocus.OnDemand - when: root.isDetached + when: root.isDetached || (root.hasCurrent && root.currentName === "wirelesspassword") } Comp { id: content - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - asynchronous: true - shouldBeActive: root.hasCurrent + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + shouldBeActive: root.hasCurrent && !root.detachedMode sourceComponent: Content { - wrapper: root + popouts: popoutState + } + } + + Comp { + id: winfo + + anchors.centerIn: parent + shouldBeActive: root.detachedMode === "winfo" + + sourceComponent: WindowInfo { + client: Hypr.activeToplevel + screen: root.screen + } + } + + Comp { + id: controlCenter + + anchors.centerIn: parent + shouldBeActive: root.detachedMode === "any" + + sourceComponent: ControlCenter { + active: root.queuedMode + screen: root.screen + + onClose: root.close() } } @@ -136,9 +149,9 @@ Item { property bool shouldBeActive active: false - asynchronous: true 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/Plugins/ZShell/Blobs/CMakeLists.txt b/Plugins/ZShell/Blobs/CMakeLists.txt new file mode 100644 index 0000000..bf12181 --- /dev/null +++ b/Plugins/ZShell/Blobs/CMakeLists.txt @@ -0,0 +1,19 @@ +qml_module(ZShell-blobs + URI ZShell.Blobs + SOURCES + blobgroup.cpp + blobshape.cpp + blobrect.cpp + blobinvertedrect.cpp + blobmaterial.cpp + LIBRARIES + Qt::Quick +) + +qt_add_shaders(ZShell-blobs "blob_shaders" + BATCHABLE OPTIMIZED NOHLSL NOMSL + PREFIX "/" + FILES + shaders/blob.frag + shaders/blob.vert +) diff --git a/Plugins/ZShell/Blobs/blobgroup.cpp b/Plugins/ZShell/Blobs/blobgroup.cpp new file mode 100644 index 0000000..e77cf05 --- /dev/null +++ b/Plugins/ZShell/Blobs/blobgroup.cpp @@ -0,0 +1,105 @@ +#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::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; + m_physicsUpdated = true; + for (auto* shape : std::as_const(m_shapes)) + shape->updatePhysics(); +} diff --git a/Plugins/ZShell/Blobs/blobgroup.hpp b/Plugins/ZShell/Blobs/blobgroup.hpp new file mode 100644 index 0000000..ce5d933 --- /dev/null +++ b/Plugins/ZShell/Blobs/blobgroup.hpp @@ -0,0 +1,61 @@ +#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 markShapeDirty(BlobShape* source); +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/Plugins/ZShell/Blobs/blobinvertedrect.cpp b/Plugins/ZShell/Blobs/blobinvertedrect.cpp new file mode 100644 index 0000000..a8b85f4 --- /dev/null +++ b/Plugins/ZShell/Blobs/blobinvertedrect.cpp @@ -0,0 +1,185 @@ +#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); +} + +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/Plugins/ZShell/Blobs/blobinvertedrect.hpp b/Plugins/ZShell/Blobs/blobinvertedrect.hpp new file mode 100644 index 0000000..d85913e --- /dev/null +++ b/Plugins/ZShell/Blobs/blobinvertedrect.hpp @@ -0,0 +1,64 @@ +#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; +} + +QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override; + +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/Plugins/ZShell/Blobs/blobmaterial.cpp b/Plugins/ZShell/Blobs/blobmaterial.cpp new file mode 100644 index 0000000..688315a --- /dev/null +++ b/Plugins/ZShell/Blobs/blobmaterial.cpp @@ -0,0 +1,96 @@ +#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] = { 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.radius, 16); + } + + return true; +} diff --git a/Plugins/ZShell/Blobs/blobmaterial.hpp b/Plugins/ZShell/Blobs/blobmaterial.hpp new file mode 100644 index 0000000..9e3518b --- /dev/null +++ b/Plugins/ZShell/Blobs/blobmaterial.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include + +struct BlobRectData { + float cx = 0, cy = 0, hw = 0, hh = 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; + // 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; +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/Plugins/ZShell/Blobs/blobrect.cpp b/Plugins/ZShell/Blobs/blobrect.cpp new file mode 100644 index 0000000..dc03432 --- /dev/null +++ b/Plugins/ZShell/Blobs/blobrect.cpp @@ -0,0 +1,245 @@ +#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) { + // 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(); + emit rawDeformMatrixChanged(); + updateCenteredDeformMatrix(); + m_physicsActive = false; + } else { + 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); + emit rawDeformMatrixChanged(); + 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; + + 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 + emit rawDeformMatrixChanged(); + updateCenteredDeformMatrix(); + m_physicsActive = false; + } +} diff --git a/Plugins/ZShell/Blobs/blobrect.hpp b/Plugins/ZShell/Blobs/blobrect.hpp new file mode 100644 index 0000000..ce8abaf --- /dev/null +++ b/Plugins/ZShell/Blobs/blobrect.hpp @@ -0,0 +1,140 @@ +#pragma once + +#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 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 bottomRightRadius READ bottomRightRadius WRITE setBottomRightRadius NOTIFY bottomRightRadiusChanged) + +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(); + } +} + +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; +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; + +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/Plugins/ZShell/Blobs/blobshape.cpp b/Plugins/ZShell/Blobs/blobshape.cpp new file mode 100644 index 0000000..b6103fd --- /dev/null +++ b/Plugins/ZShell/Blobs/blobshape.cpp @@ -0,0 +1,367 @@ +#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) { + // 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 (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); + } + } +} + +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); + if (m_centeredDeformMatrix != result) { + m_centeredDeformMatrix = result; + 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); +} + +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(); + + 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; + + // Skip zero-size rects + if (other->width() <= 0 || other->height() <= 0) + continue; + + if (isExcluded(other)) + 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); + 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; + 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) { + 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); + + // 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); + } + + 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 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]; + 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))); + } + + // 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); + } +} + +QSGNode* BlobShape::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { + if (!m_group) { + 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/Plugins/ZShell/Blobs/blobshape.hpp b/Plugins/ZShell/Blobs/blobshape.hpp new file mode 100644 index 0000000..aa1d018 --- /dev/null +++ b/Plugins/ZShell/Blobs/blobshape.hpp @@ -0,0 +1,92 @@ +#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) +Q_PROPERTY(QMatrix4x4 rawDeformMatrix READ rawDeformMatrix NOTIFY rawDeformMatrixChanged) + +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; +} + +QMatrix4x4 rawDeformMatrix() const { + return m_deformMatrix; +} + +signals: +void groupChanged(); +void radiusChanged(); +void deformMatrixChanged(); +void rawDeformMatrixChanged(); + +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 bool isExcluded(const BlobShape* /*other*/) const { + return false; +} + +virtual void cornerRadii(float out[4]) const; + +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; +float m_pendingDx = 0; +float m_pendingDy = 0; +bool m_cachedHasInverted = false; +float m_cachedInvertedRadius = 0; +float m_cachedInvertedOuter[4] = {}; +float m_cachedInvertedInner[4] = {}; +}; diff --git a/Plugins/ZShell/Blobs/shaders/blob.frag b/Plugins/ZShell/Blobs/shaders/blob.frag new file mode 100644 index 0000000..f9d8503 --- /dev/null +++ b/Plugins/ZShell/Blobs/shaders/blob.frag @@ -0,0 +1,302 @@ +#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 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 radii = + rectData[i * 5 + 4]; // effective per-corner radii (tr, br, bl, tl) + + // 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); + + // Use pre-computed effective per-corner radii + 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 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; + 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; + } + } + + // 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 && mergedSdf > smoothFactor) + discard; + + float fw = fwidth(mergedSdf); + float alpha = 1.0 - smoothstep(-fw, fw, mergedSdf); + fragColor = vec4(color.rgb * alpha, alpha) * qt_Opacity; +} diff --git a/Plugins/ZShell/Blobs/shaders/blob.vert b/Plugins/ZShell/Blobs/shaders/blob.vert new file mode 100644 index 0000000..b2b94e0 --- /dev/null +++ b/Plugins/ZShell/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/Plugins/ZShell/CMakeLists.txt b/Plugins/ZShell/CMakeLists.txt index f540859..edbc226 100644 --- a/Plugins/ZShell/CMakeLists.txt +++ b/Plugins/ZShell/CMakeLists.txt @@ -57,3 +57,4 @@ add_subdirectory(Models) add_subdirectory(Internal) add_subdirectory(Services) add_subdirectory(Components) +add_subdirectory(Blobs) From 0df32b9e955b927a7867130b51371af98f3dfc93 Mon Sep 17 00:00:00 2001 From: zach Date: Thu, 16 Apr 2026 01:51:37 +0200 Subject: [PATCH 2/7] test popouts --- Plugins/ZShell/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/ZShell/CMakeLists.txt b/Plugins/ZShell/CMakeLists.txt index edbc226..c5f041e 100644 --- a/Plugins/ZShell/CMakeLists.txt +++ b/Plugins/ZShell/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) From 55b497f132af1ea63c2ea3f9684b3c8855f86216 Mon Sep 17 00:00:00 2001 From: zach Date: Thu, 16 Apr 2026 03:10:20 +0200 Subject: [PATCH 3/7] test popouts --- Drawers/Interactions.qml | 3 - Drawers/Panels.qml | 10 +-- Drawers/Windows.qml | 24 +++++- Modules/Bar/Bar.qml | 1 + Modules/Bar/BarLoader.qml | 2 + Modules/ClipWrapper.qml | 19 +++-- Modules/Content.qml | 25 ++---- Modules/PopoutState.qml | 8 ++ Modules/SysTray/Popouts/TrayMenuPopout.qml | 7 +- Modules/Wrapper.qml | 90 +++++++--------------- 10 files changed, 84 insertions(+), 105 deletions(-) create mode 100644 Modules/PopoutState.qml diff --git a/Drawers/Interactions.qml b/Drawers/Interactions.qml index 3371f0d..6d6ae56 100644 --- a/Drawers/Interactions.qml +++ b/Drawers/Interactions.qml @@ -72,9 +72,6 @@ CustomMouseArea { } } onPositionChanged: event => { - if (popouts.isDetached) - return; - const x = event.x; const y = event.y; const dragX = x - dragStart.x; diff --git a/Drawers/Panels.qml b/Drawers/Panels.qml index 6d09072..e23e3d3 100644 --- a/Drawers/Panels.qml +++ b/Drawers/Panels.qml @@ -26,7 +26,8 @@ Item { readonly property alias launcher: launcher readonly property alias notifications: notifications readonly property alias osd: osd - readonly property alias popouts: popouts + readonly property alias popouts: popouts.content + readonly property alias popoutsWrapper: popouts readonly property alias resources: resources required property ShellScreen screen readonly property alias settings: settings @@ -73,13 +74,6 @@ Item { anchors.top: parent.top screen: root.screen - x: { - const off = currentCenter - nonAnimWidth / 2; - const diff = root.width - Math.floor(off + nonAnimWidth); - if (diff < 0) - return off + diff; - return Math.floor(Math.max(off, 0)); - } } Toasts.Toasts { diff --git a/Drawers/Windows.qml b/Drawers/Windows.qml index 9cc51ee..48c329f 100644 --- a/Drawers/Windows.qml +++ b/Drawers/Windows.qml @@ -5,6 +5,7 @@ import QtQuick.Effects import Quickshell import Quickshell.Wayland import Quickshell.Hyprland +import ZShell.Blobs import qs.Daemons import qs.Components import qs.Modules @@ -161,7 +162,7 @@ Variants { borderBottom: Config.barConfig.border - anchors.margins borderLeft: Config.barConfig.border - anchors.margins borderRight: Config.barConfig.border - anchors.margins - borderTop: Config.barConfig.border - anchors.margins + borderTop: bar.implicitHeight - anchors.margins group: blobGroup radius: Config.barConfig.rounding } @@ -171,6 +172,7 @@ Variants { deformAmount: 0.1 panel: panels.dashboard + radius: Appearance.rounding.normal } PanelBg { @@ -178,12 +180,13 @@ Variants { deformAmount: 0.1 panel: panels.launcher + radius: Appearance.rounding.smallest + 5 } PanelBg { id: sidebarBg - bottomLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius + bottomLeftRadius: 0 deformAmount: 0.03 exclude: panels.sidebar.offsetscale > 0.08 ? [] : [utilsBg] implicitHeight: panel.height * (1 / rawDeformMatrix.m22) + 2 @@ -195,6 +198,7 @@ Variants { deformAmount: 0.25 panel: panels.osd + radius: 20 } PanelBg { @@ -209,7 +213,7 @@ Variants { deformAmount: panels.sidebar.visible ? 0.1 : 0.15 exclude: panels.sidebar.offsetScale > 0.08 ? [] : [sidebarBg] panel: panels.utilities - topLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius + topLeftRadius: 0 } PanelBg { @@ -217,7 +221,15 @@ Variants { deformAmount: 0.15 implicitWidth: panels.popouts.width - panel: panels.popouts + panel: panels.popoutsWrapper + } + + PanelBg { + id: resourcesBg + + deformAmount: 0.15 + panel: panels.resources + radius: Appearance.rounding.normal } } @@ -275,6 +287,9 @@ Variants { popouts.transform: Matrix4x4 { matrix: popoutBg.deformMatrix } + resources.transform: Matrix4x4 { + matrix: resourcesBg.deformMatrix + } sidebar.transform: Matrix4x4 { matrix: sidebarBg.deformMatrix } @@ -289,6 +304,7 @@ Variants { anchors.left: parent.left anchors.right: parent.right popouts: panels.popouts + popoutsWrapper: panels.popoutsWrapper screen: scope.modelData visibilities: visibilities } diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 74459f4..9ac299b 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -16,6 +16,7 @@ RowLayout { id: root required property Wrapper popouts + required property ClipWrapper popoutsWrapper required property ShellScreen screen readonly property int vPadding: 6 required property PersistentProperties visibilities diff --git a/Modules/Bar/BarLoader.qml b/Modules/Bar/BarLoader.qml index 6c16698..b5e0d75 100644 --- a/Modules/Bar/BarLoader.qml +++ b/Modules/Bar/BarLoader.qml @@ -20,6 +20,7 @@ Item { property bool isHovered readonly property int padding: Math.max(Appearance.padding.smaller, Config.barConfig.border) required property Wrapper popouts + required property ClipWrapper popoutsWrapper required property ShellScreen screen readonly property bool shouldBeVisible: (!Config.barConfig.autoHide || visibilities.bar || isHovered) readonly property int vPadding: 6 @@ -76,6 +77,7 @@ Item { sourceComponent: Bar { height: root.contentHeight popouts: root.popouts + popoutsWrapper: root.popoutsWrapper screen: root.screen visibilities: root.visibilities } diff --git a/Modules/ClipWrapper.qml b/Modules/ClipWrapper.qml index 6919f57..df891e5 100644 --- a/Modules/ClipWrapper.qml +++ b/Modules/ClipWrapper.qml @@ -9,13 +9,20 @@ Item { id: root readonly property alias content: content - property real offsetScale: x > 0 || content.hasCurrent ? 0 : 1 + property real offsetScale: y > 0 || content.hasCurrent ? 0 : 1 required property ShellScreen screen clip: true - implicitHeight: content.implicitHeight - implicitWidth: content.implicitWidth * (1 - offsetScale) + implicitHeight: content.implicitHeight * (1 - offsetScale) + implicitWidth: content.implicitWidth visible: width > 0 && height > 0 + x: { + const off = content.currentCenter - content.nonAnimWidth / 2; + const diff = parent.width - Math.floor(off + content.nonAnimWidth); + if (diff < 0) + return off + diff; + return Math.floor(Math.max(off, 0)); + } Behavior on offsetScale { Anim { @@ -41,9 +48,9 @@ Item { Wrapper { id: content - anchors.left: parent.left - anchors.leftMargin: (-implicitWidth - 5) * root.offsetScale - anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: (-implicitHeight - 5) * root.offsetScale offsetScale: root.offsetScale screen: root.screen } diff --git a/Modules/Content.qml b/Modules/Content.qml index 1f17be3..9ab78a2 100644 --- a/Modules/Content.qml +++ b/Modules/Content.qml @@ -15,7 +15,7 @@ Item { readonly property Item current: currentPopout?.item ?? null readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null - required property Item wrapper + required property PopoutState popouts implicitHeight: (currentPopout?.implicitHeight ?? 0) + 5 * 2 implicitWidth: (currentPopout?.implicitWidth ?? 0) + 5 * 2 @@ -49,40 +49,31 @@ Item { Connections { function onHasCurrentChanged(): void { - if (root.wrapper.hasCurrent && trayMenu.shouldBeActive) { + if (root.popouts.hasCurrent && trayMenu.shouldBeActive) { trayMenu.sourceComponent = null; trayMenu.sourceComponent = trayMenuComponent; } } - target: root.wrapper + target: root.popouts } Component { id: trayMenuComponent TrayMenuPopout { - popouts: root.wrapper + popouts: root.popouts trayItem: trayMenu.modelData.menu } } } } - Popout { - name: "overview" - - sourceComponent: OverviewPopout { - screen: root.wrapper.screen - wrapper: root.wrapper - } - } - Popout { name: "upower" sourceComponent: UPowerPopout { - wrapper: root.wrapper + wrapper: root.popouts } } @@ -90,7 +81,7 @@ Item { name: "network" sourceComponent: NetworkPopout { - wrapper: root.wrapper + wrapper: root.popouts } } @@ -98,7 +89,7 @@ Item { name: "updates" sourceComponent: UpdatesPopout { - wrapper: root.wrapper + wrapper: root.popouts } } } @@ -107,7 +98,7 @@ Item { id: popout required property string name - readonly property bool shouldBeActive: root.wrapper.currentName === name + readonly property bool shouldBeActive: root.popouts.currentName === name active: false anchors.centerIn: parent diff --git a/Modules/PopoutState.qml b/Modules/PopoutState.qml new file mode 100644 index 0000000..a5df520 --- /dev/null +++ b/Modules/PopoutState.qml @@ -0,0 +1,8 @@ +import QtQuick + +QtObject { + property string currentName + property bool hasCurrent + + signal detachRequested(mode: string) +} diff --git a/Modules/SysTray/Popouts/TrayMenuPopout.qml b/Modules/SysTray/Popouts/TrayMenuPopout.qml index 41a7d1a..f8cea7f 100644 --- a/Modules/SysTray/Popouts/TrayMenuPopout.qml +++ b/Modules/SysTray/Popouts/TrayMenuPopout.qml @@ -1,18 +1,19 @@ pragma ComponentBehavior: Bound -import qs.Components -import qs.Config import Quickshell import Quickshell.Widgets import QtQuick import QtQuick.Controls import QtQuick.Effects +import qs.Components +import qs.Modules +import qs.Config StackView { id: root property int biggestWidth: 0 - required property Item popouts + required property PopoutState popouts property int rootWidth: 0 required property QsMenuHandle trayItem diff --git a/Modules/Wrapper.qml b/Modules/Wrapper.qml index 88a0f59..c51a360 100644 --- a/Modules/Wrapper.qml +++ b/Modules/Wrapper.qml @@ -11,19 +11,16 @@ Item { property list animCurve: Appearance.anim.curves.expressiveDefaultSpatial property int animLength: Appearance.anim.durations.expressiveDefaultSpatial readonly property alias content: content - readonly property alias controlCenter: controlCenter readonly property Item current: (content.item as Content)?.current ?? null property real currentCenter property alias currentName: popoutState.currentName property string detachedMode property alias hasCurrent: popoutState.hasCurrent - readonly property bool isDetached: detachedMode.length > 0 readonly property real nonAnimHeight: children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight readonly property real nonAnimWidth: children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth required property real offsetScale property string queuedMode required property ShellScreen screen - readonly property alias winfo: winfo function close(): void { hasCurrent = false; @@ -67,49 +64,14 @@ Item { } } - Keys.onEscapePressed: { - // Forward escape to password popout if active, otherwise close - if (currentName === "wirelesspassword" && content.item) { - const passwordPopout = (content.item as Content)?.children.find(c => c.name === "wirelesspassword"); - if (passwordPopout && passwordPopout.item) { - passwordPopout.item.closeDialog(); - return; - } - } - close(); - } - Keys.onPressed: event => { - // Don't intercept keys when password popout is active - let it handle them - if (currentName === "wirelesspassword") { - event.accepted = false; - } - } - PopoutState { id: popoutState - - onDetachRequested: mode => root.detach(mode) - } - - HyprlandFocusGrab { - active: root.isDetached - windows: [QsWindow.window] - - onCleared: root.close() - } - - Binding { - property: "WlrLayershell.keyboardFocus" - target: QsWindow.window - value: WlrKeyboardFocus.OnDemand - when: root.isDetached || (root.hasCurrent && root.currentName === "wirelesspassword") } Comp { id: content - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter + anchors.centerIn: parent shouldBeActive: root.hasCurrent && !root.detachedMode sourceComponent: Content { @@ -117,31 +79,31 @@ Item { } } - Comp { - id: winfo - - anchors.centerIn: parent - shouldBeActive: root.detachedMode === "winfo" - - sourceComponent: WindowInfo { - client: Hypr.activeToplevel - screen: root.screen - } - } - - Comp { - id: controlCenter - - anchors.centerIn: parent - shouldBeActive: root.detachedMode === "any" - - sourceComponent: ControlCenter { - active: root.queuedMode - screen: root.screen - - onClose: root.close() - } - } + // Comp { + // id: winfo + // + // anchors.centerIn: parent + // shouldBeActive: root.detachedMode === "winfo" + // + // sourceComponent: WindowInfo { + // client: Hypr.activeToplevel + // screen: root.screen + // } + // } + // + // Comp { + // id: controlCenter + // + // anchors.centerIn: parent + // shouldBeActive: root.detachedMode === "any" + // + // sourceComponent: ControlCenter { + // active: root.queuedMode + // screen: root.screen + // + // onClose: root.close() + // } + // } component Comp: Loader { id: comp From 6d78a0165970d8e9011352422ee3f1adac343637 Mon Sep 17 00:00:00 2001 From: zach Date: Thu, 16 Apr 2026 12:57:31 +0200 Subject: [PATCH 4/7] add background for all popouts --- Drawers/Windows.qml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Drawers/Windows.qml b/Drawers/Windows.qml index 48c329f..750a717 100644 --- a/Drawers/Windows.qml +++ b/Drawers/Windows.qml @@ -231,6 +231,22 @@ Variants { panel: panels.resources radius: Appearance.rounding.normal } + + PanelBg { + id: settingsBg + + deformAmount: 0.15 + panel: panels.settings + radius: Appearance.rounding.large + } + + PanelBg { + id: dockBg + + deformAmount: 0.1 + panel: panels.dock + radius: Appearance.rounding.normal + } } Drawing { @@ -275,6 +291,9 @@ Variants { dashboard.transform: Matrix4x4 { matrix: dashBg.deformMatrix } + dock.transform: Matrix4x4 { + matrix: dockBg.deformMatrix + } launcher.transform: Matrix4x4 { matrix: launcherBg.deformMatrix } @@ -290,6 +309,9 @@ Variants { resources.transform: Matrix4x4 { matrix: resourcesBg.deformMatrix } + settings.transform: Matrix4x4 { + matrix: settingsBg.deformMatrix + } sidebar.transform: Matrix4x4 { matrix: sidebarBg.deformMatrix } From 47b8d68d4b53bc95ea610526b39d58f7decce629 Mon Sep 17 00:00:00 2001 From: zach Date: Thu, 16 Apr 2026 13:16:31 +0200 Subject: [PATCH 5/7] fix popout anims --- Modules/ClipWrapper.qml | 4 ++-- Modules/Wrapper.qml | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Modules/ClipWrapper.qml b/Modules/ClipWrapper.qml index df891e5..d9265d0 100644 --- a/Modules/ClipWrapper.qml +++ b/Modules/ClipWrapper.qml @@ -31,14 +31,14 @@ Item { } } Behavior on x { + enabled: root.offsetScale < 1 + Anim { duration: content.animLength easing.bezierCurve: content.animCurve } } Behavior on y { - enabled: root.offsetScale < 1 - Anim { duration: content.animLength easing.bezierCurve: content.animCurve diff --git a/Modules/Wrapper.qml b/Modules/Wrapper.qml index c51a360..bb0ebe9 100644 --- a/Modules/Wrapper.qml +++ b/Modules/Wrapper.qml @@ -58,6 +58,8 @@ Item { } } Behavior on implicitWidth { + enabled: root.offsetScale < 1 + Anim { duration: root.animLength easing.bezierCurve: root.animCurve From 9418a92e9955ea10357b94ab95acee492482467a Mon Sep 17 00:00:00 2001 From: zach Date: Fri, 17 Apr 2026 19:53:57 +0200 Subject: [PATCH 6/7] test popouts --- Drawers/Panels.qml | 3 +-- Drawers/Windows.qml | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Drawers/Panels.qml b/Drawers/Panels.qml index e23e3d3..a328fa7 100644 --- a/Drawers/Panels.qml +++ b/Drawers/Panels.qml @@ -134,8 +134,7 @@ Item { Settings.Wrapper { id: settings - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top + anchors.centerIn: parent panels: root screen: root.screen visibilities: root.visibilities diff --git a/Drawers/Windows.qml b/Drawers/Windows.qml index 750a717..76b6f9b 100644 --- a/Drawers/Windows.qml +++ b/Drawers/Windows.qml @@ -235,7 +235,7 @@ Variants { PanelBg { id: settingsBg - deformAmount: 0.15 + deformAmount: 0.05 panel: panels.settings radius: Appearance.rounding.large } From a7457c57c081511ec848d1ba3a44095b9d0d4ceb Mon Sep 17 00:00:00 2001 From: zach Date: Sat, 18 Apr 2026 00:15:52 +0200 Subject: [PATCH 7/7] dock settings work now woo --- Drawers/Interactions.qml | 8 ++- .../Settings/Controls/WallpaperCropper.qml | 54 +++++++++---------- 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/Drawers/Interactions.qml b/Drawers/Interactions.qml index 6d6ae56..e09228f 100644 --- a/Drawers/Interactions.qml +++ b/Drawers/Interactions.qml @@ -54,7 +54,7 @@ CustomMouseArea { anchors.fill: parent cursorShape: (pressed && dragStart.y < bar.implicitHeight) ? Qt.ClosedHandCursor : undefined hoverEnabled: true - propagateComposedEvents: true + propagateComposedEvents: false onContainsMouseChanged: { if (!containsMouse) { @@ -92,6 +92,10 @@ CustomMouseArea { visibilities.settings = false; } + if (Config.dock.hoverToReveal && pressed && dragStart.y > root.screen.height - root.bar.implicitHeight) + if (dragY < -10) + visibilities.dock = true; + if (panels.sidebar.width === 0) { const showOsd = inRightPanel(panels.osd, x, y); @@ -112,7 +116,7 @@ CustomMouseArea { } } - if (!visibilities.dock && !visibilities.launcher && inBottomPanel(panels.dock, x, y)) + if (Config.dock.enable && !Config.dock.hoverToReveal && !visibilities.dock && !visibilities.launcher && inBottomPanel(panels.dock, x, y)) visibilities.dock = true; if (y < root.bar.implicitHeight) { diff --git a/Modules/Settings/Controls/WallpaperCropper.qml b/Modules/Settings/Controls/WallpaperCropper.qml index ee355a8..d0f5faa 100644 --- a/Modules/Settings/Controls/WallpaperCropper.qml +++ b/Modules/Settings/Controls/WallpaperCropper.qml @@ -9,15 +9,16 @@ import qs.Helpers ColumnLayout { id: root - width: Math.min(parent ? parent.width : 600, 600) spacing: 15 + width: Math.min(parent ? parent.width : 600, 600) Rectangle { id: previewContainer - Layout.fillWidth: true - Layout.preferredHeight: width * (Quickshell.screens.length > 0 ? (Quickshell.screens[0].height / Math.max(1, Quickshell.screens[0].width)) : 9/16) + + Layout.fillHeight: true + Layout.preferredWidth: height * (Quickshell.screens.length > 0 ? (Quickshell.screens[0].height / Math.max(1, Quickshell.screens[0].width)) : 16 / 9) clip: true - color: DynamicColors.surfaceContainer + color: DynamicColors.palette.m3surfaceContainer radius: Config.appearance.rounding.scale * 10 Image { @@ -30,51 +31,50 @@ ColumnLayout { Rectangle { id: cropRect - - property real paintedWidth: img.paintedWidth > 0 ? img.paintedWidth : img.width + + property real cropHeight: (imageAspect > screenAspect ? paintedHeight : paintedWidth / screenAspect) / Config.background.zoom + property real cropWidth: (imageAspect > screenAspect ? paintedHeight * screenAspect : paintedWidth) / Config.background.zoom + property real imageAspect: Math.max(1, paintedWidth) / Math.max(1, paintedHeight) property real paintedHeight: img.paintedHeight > 0 ? img.paintedHeight : img.height + property real paintedWidth: img.paintedWidth > 0 ? img.paintedWidth : img.width property real paintedX: (img.width - paintedWidth) / 2 property real paintedY: (img.height - paintedHeight) / 2 - - property real screenAspect: Quickshell.screens.length > 0 ? (Quickshell.screens[0].width / Math.max(1, Quickshell.screens[0].height)) : 16/9 - property real imageAspect: Math.max(1, paintedWidth) / Math.max(1, paintedHeight) - - property real cropWidth: (imageAspect > screenAspect ? paintedHeight * screenAspect : paintedWidth) / Config.background.zoom - property real cropHeight: (imageAspect > screenAspect ? paintedHeight : paintedWidth / screenAspect) / Config.background.zoom - - border.color: DynamicColors.primary + property real screenAspect: Quickshell.screens.length > 0 ? (Quickshell.screens[0].width / Math.max(1, Quickshell.screens[0].height)) : 16 / 9 + + border.color: DynamicColors.palette.m3primary border.width: 2 - color: DynamicColors.primaryContainer.withAlpha(0.3) - - width: cropWidth + color: Qt.alpha(DynamicColors.palette.m3primaryContainer, 0.3) height: cropHeight - + width: cropWidth x: paintedX + (paintedWidth - width) * Config.background.alignX y: paintedY + (paintedHeight - height) * Config.background.alignY - + DragHandler { target: null + onActiveTranslationChanged: { if (active) { let newX = cropRect.x - cropRect.paintedX + translation.x; let newY = cropRect.y - cropRect.paintedY + translation.y; - + let rangeX = cropRect.paintedWidth - cropRect.width; let rangeY = cropRect.paintedHeight - cropRect.height; - + if (rangeX > 0) { let valX = newX / rangeX; Config.background.alignX = Math.max(0.0, Math.min(1.0, valX)); + Config.save(); } - + if (rangeY > 0) { let valY = newY / rangeY; Config.background.alignY = Math.max(0.0, Math.min(1.0, valY)); + Config.save(); } } } } - + PinchHandler { maximumScale: 5.0 minimumScale: 1.0 @@ -90,13 +90,13 @@ ColumnLayout { } } } - + SettingSpinBox { + max: 5.0 + min: 1.0 name: "Zoom" object: Config.background setting: "zoom" - min: 1.0 - max: 5.0 step: 0.1 } -} \ No newline at end of file +}