diff --git a/Components/ClipRect.qml b/Components/ClipRect.qml new file mode 100644 index 0000000..39ac012 --- /dev/null +++ b/Components/ClipRect.qml @@ -0,0 +1,41 @@ +import QtQuick +import ZShell.Components + +Item { + id: root + + property real radius + property real topLeftRadius: radius + property real topRightRadius: radius + property real bottomRightRadius: radius + property real bottomLeftRadius: radius + property color color: "transparent" + default property alias contentData: contentItem.data + + Item { + id: contentItem + + anchors.fill: parent + } + + ShaderEffectSource { + id: source + + hideSource: true + sourceItem: contentItem + } + + RoundedMask { + anchors.fill: parent + source: source + topLeftRadius: root.topLeftRadius + topRightRadius: root.topRightRadius + bottomRightRadius: root.bottomRightRadius + bottomLeftRadius: root.bottomLeftRadius + color: root.color + } + + Behavior on color { + CAnim {} + } +} diff --git a/Components/TextFieldBase.qml b/Components/TextFieldBase.qml index 0edd409..2a7e018 100644 --- a/Components/TextFieldBase.qml +++ b/Components/TextFieldBase.qml @@ -12,7 +12,7 @@ TextField { implicitHeight: contentHeight + topPadding + bottomPadding implicitWidth: contentWidth + leftPadding + rightPadding placeholderTextColor: Colors.palette.m3onSurfaceVariant // No anim cause placeholder is custom - renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.NativeRendering + renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.QtRendering selectedTextColor: color selectionColor: Qt.alpha(Colors.palette.m3primary, 0.4) verticalAlignment: TextInput.AlignVCenter diff --git a/Components/VerticalFadeFlickable.qml b/Components/VerticalFadeFlickable.qml index 8cc61d7..099ecf7 100644 --- a/Components/VerticalFadeFlickable.qml +++ b/Components/VerticalFadeFlickable.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import QtQuick +import Quickshell import qs.Effects CustomFlickable { @@ -9,6 +10,7 @@ CustomFlickable { property real bottomFadeOpacity: fadeShouldBeActive(false) ? 0 : 1 property real fadeAmount: 0.1 property real topFadeOpacity: fadeShouldBeActive(true) ? 0 : 1 + readonly property real dpr: Math.ceil((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1) function fadeShouldBeActive(isStart: bool): bool { // When content is smaller than flickable size, hide fade when rebound starts @@ -28,6 +30,7 @@ CustomFlickable { type: Anim.SlowEffects } } + // layer.textureSize: Qt.size(width * dpr, height * dpr) layer.effect: Mask { maskSource: mask diff --git a/Modules/Bar/Components/Tray/TrayItem.qml b/Modules/Bar/Components/Tray/TrayItem.qml index 9a1e228..6faf170 100644 --- a/Modules/Bar/Components/Tray/TrayItem.qml +++ b/Modules/Bar/Components/Tray/TrayItem.qml @@ -19,7 +19,7 @@ Item { required property Wrapper popouts function resolveIcon(app: string, icon: string): string { - if (app === "chrome_status_icon_1") { + if (app === "discord_status_icon_1" && Config.bar.tray.replaceIcons) { return Quickshell.iconPath("discord-tray"); } else if (app === "AyuGramDesktop") { if (icon === Quickshell.iconPath("com.ayugram.desktop-attention-symbolic")) @@ -50,6 +50,8 @@ Item { radius: Tokens.rounding.full StateLayer { + id: layer + acceptedButtons: Qt.LeftButton | Qt.RightButton anchors.fill: parent color: icon.layer.enabled && root.current ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface diff --git a/Modules/Bar/Popouts/AudioPopout.qml b/Modules/Bar/Popouts/AudioPopout.qml index 33e3daa..c218e72 100644 --- a/Modules/Bar/Popouts/AudioPopout.qml +++ b/Modules/Bar/Popouts/AudioPopout.qml @@ -33,8 +33,7 @@ Item { y: parent.y Behavior on implicitHeight { - Anim { - } + Anim {} } } @@ -101,8 +100,7 @@ Item { value: Audio.volume Behavior on value { - Anim { - } + Anim {} } onInteraction: value => Audio.setVolume(value) @@ -123,7 +121,7 @@ Item { } } - CustomClippingRect { + CustomRect { Layout.fillWidth: true Layout.preferredHeight: 65 + Tokens.spacing.small * 2 Layout.topMargin: root.topMargin @@ -136,17 +134,21 @@ Item { node: Audio.source } - CustomRect { - id: sourcePeakFill + CustomClippingRect { + anchors.fill: parent + radius: parent.radius - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.top: parent.top - color: Qt.alpha(Colors.palette.m3primary, 0.15) - implicitWidth: parent.width * sourcePeak.peak + CustomRect { + id: sourcePeakFill - Behavior on implicitWidth { - Anim { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.top: parent.top + color: Qt.alpha(Colors.palette.m3primary, 0.15) + implicitWidth: parent.width * sourcePeak.peak + + Behavior on implicitWidth { + Anim {} } } } @@ -195,8 +197,7 @@ Item { value: Audio.sourceVolume Behavior on value { - Anim { - } + Anim {} } onInteraction: value => Audio.setSourceVolume(value) @@ -230,7 +231,7 @@ Item { model: Audio.streams.filter(s => s.isSink) - CustomClippingRect { + CustomRect { id: appBox required property int index @@ -248,17 +249,21 @@ Item { node: appBox.modelData } - CustomRect { - id: peakFill + CustomClippingRect { + anchors.fill: parent + radius: parent.radius - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.top: parent.top - color: Qt.alpha(Colors.palette.m3primary, 0.15) - implicitWidth: parent.width * peak.peak + CustomRect { + id: peakFill - Behavior on implicitWidth { - Anim { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.top: parent.top + color: Qt.alpha(Colors.palette.m3primary, 0.15) + implicitWidth: parent.width * peak.peak + + Behavior on implicitWidth { + Anim {} } } } @@ -316,8 +321,7 @@ Item { value: appBox.modelData.audio.volume Behavior on value { - Anim { - } + Anim {} } onInteraction: value => Audio.setStreamVolume(appBox.modelData, value) diff --git a/Modules/Bar/Popouts/UpdatesPopout.qml b/Modules/Bar/Popouts/UpdatesPopout.qml index a5b380d..3217de7 100644 --- a/Modules/Bar/Popouts/UpdatesPopout.qml +++ b/Modules/Bar/Popouts/UpdatesPopout.qml @@ -9,7 +9,7 @@ import qs.Modules import qs.Helpers import qs.Services -CustomClippingRect { +CustomRect { id: root readonly property bool hasUpdates: Object.keys(Updates.updates)?.length > 0 @@ -73,6 +73,7 @@ CustomClippingRect { implicitHeight: Math.min(contentHeight, (root.itemHeight + spacing) * 5 - spacing) implicitWidth: contentWidth spacing: Tokens.spacing.medium + clip: true delegate: CustomRect { id: update diff --git a/Modules/Bar/Popouts/Wrapper.qml b/Modules/Bar/Popouts/Wrapper.qml index 1f46061..92f2833 100644 --- a/Modules/Bar/Popouts/Wrapper.qml +++ b/Modules/Bar/Popouts/Wrapper.qml @@ -1,6 +1,4 @@ import Quickshell -import Quickshell.Wayland -import Quickshell.Hyprland import QtQuick import qs.Components import ZShell.Config diff --git a/Modules/Notifications/Content.qml b/Modules/Notifications/Content.qml index c2b6c48..284a002 100644 --- a/Modules/Notifications/Content.qml +++ b/Modules/Notifications/Content.qml @@ -3,8 +3,9 @@ pragma ComponentBehavior: Bound import Quickshell import Quickshell.Widgets import QtQuick -import qs.Components import ZShell.Config +import ZShell.Components +import qs.Components import qs.Daemons import qs.Helpers @@ -27,7 +28,7 @@ Item { for (let i = 0; i < count; i++) height += (list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0; - if (panels.popouts.hasCurrent && (panels.popouts.currentCenter + (panels.popouts.current?.width / 2)) > panels.notifications.x) + if (panels.popouts.hasCurrent && (panels.popouts.currentCenter + (panels.popouts.current?.width / 2)) > panels.notifications.x || screenState.dashboard) return 0; if (screenState.osd) { @@ -36,25 +37,20 @@ Item { height = h; } - if (screenState.session) { - const h = panels.session.y - Tokens.spacing.small * 2 - padding * 2; - if (height > h) - height = h; - } - return Math.min(((QsWindow.window as QsWindow)?.screen?.height ?? 0) - 1 * 2, height + padding * 2); } + implicitWidth: Config.notifs.sizes.width + padding * 2 Behavior on implicitHeight { Anim {} } - ClippingWrapperRectangle { + Item { anchors.fill: parent anchors.margins: root.padding - color: "transparent" - radius: Tokens.rounding.large - root.padding + clip: true + // radius: Tokens.rounding.large - root.padding CustomListView { id: list diff --git a/Modules/Notifications/Sidebar/Chat/ChatDelegate.qml b/Modules/Notifications/Sidebar/Chat/ChatDelegate.qml index 3480d08..c4b594c 100644 --- a/Modules/Notifications/Sidebar/Chat/ChatDelegate.qml +++ b/Modules/Notifications/Sidebar/Chat/ChatDelegate.qml @@ -6,7 +6,7 @@ import qs.Effects import qs.Components import qs.Services -CustomClippingRect { +CustomRect { id: root property bool expanded: false diff --git a/Modules/Notifications/Sidebar/Chat/ChatList.qml b/Modules/Notifications/Sidebar/Chat/ChatList.qml index adf488c..3b2d327 100644 --- a/Modules/Notifications/Sidebar/Chat/ChatList.qml +++ b/Modules/Notifications/Sidebar/Chat/ChatList.qml @@ -185,7 +185,7 @@ Item { } } - CustomClippingRect { + CustomRect { id: fabMenuItem required property var modelData @@ -197,6 +197,7 @@ Item { radius: sLayer.pressed ? Tokens.rounding.small : implicitHeight / 2 color: Colors.palette.m3primaryContainer visible: !(modelData.name === "pop out in window" && ChatState.isWindow) + clip: true Behavior on radius { Anim { diff --git a/Modules/Notifications/Sidebar/Chat/Content/ContentBubble.qml b/Modules/Notifications/Sidebar/Chat/Content/ContentBubble.qml index 572970a..191fd6f 100644 --- a/Modules/Notifications/Sidebar/Chat/Content/ContentBubble.qml +++ b/Modules/Notifications/Sidebar/Chat/Content/ContentBubble.qml @@ -24,7 +24,7 @@ Item { implicitHeight: bubble.implicitHeight + (actionsRow.shouldBeActive ? actionsRow.implicitHeight + actionsRow.anchors.topMargin : 0) - CustomClippingRect { + CustomRect { id: bubble property real liveTargetWidth: root.isUser ? Math.min(msgText.implicitWidth + Tokens.padding.medium * 2, root.contentMaxWidth) : root.contentMaxWidth @@ -37,8 +37,7 @@ Item { implicitWidth: layoutFrozen ? frozenWidth : layoutWidth implicitHeight: root.isUser ? msgText.contentHeight + Tokens.padding.medium * 2 : markdown.implicitHeight + markdown.anchors.topMargin * 2 anchors.right: root.isUser ? parent.right : undefined - layer.enabled: false - layer.smooth: true + clip: true transform: Scale { id: stretchScale diff --git a/Modules/Notifications/Sidebar/Chat/Content/MessageDelegate.qml b/Modules/Notifications/Sidebar/Chat/Content/MessageDelegate.qml index 20e2533..7439a94 100644 --- a/Modules/Notifications/Sidebar/Chat/Content/MessageDelegate.qml +++ b/Modules/Notifications/Sidebar/Chat/Content/MessageDelegate.qml @@ -223,12 +223,13 @@ MouseArea { height: width opacity: modelData ? 1 : 0 - CustomClippingRect { + CustomRect { anchors.fill: parent anchors.margins: Tokens.spacing.extraSmall radius: Tokens.rounding.medium color: Colors.tPalette.m3surfaceContainer + clip: true FadeImage { id: attImage diff --git a/Modules/Notifications/Sidebar/NotifDock.qml b/Modules/Notifications/Sidebar/NotifDock.qml index e0cbdf0..e9b9227 100644 --- a/Modules/Notifications/Sidebar/NotifDock.qml +++ b/Modules/Notifications/Sidebar/NotifDock.qml @@ -63,7 +63,7 @@ Item { } } - ClippingRectangle { + Item { id: clipRect anchors.bottom: parent.bottom @@ -71,8 +71,6 @@ Item { anchors.right: parent.right anchors.top: title.bottom anchors.topMargin: 10 - color: "transparent" - radius: 6 Loader { active: opacity > 0 diff --git a/Modules/Resources/Cards/HeroCard.qml b/Modules/Resources/Cards/HeroCard.qml index 2b488de..913dd03 100644 --- a/Modules/Resources/Cards/HeroCard.qml +++ b/Modules/Resources/Cards/HeroCard.qml @@ -4,7 +4,7 @@ import ZShell.Config import qs.Components import qs.Services -CustomClippingRect { +CustomRect { id: root required property color accent @@ -19,15 +19,20 @@ CustomClippingRect { implicitWidth: 450 radius: Tokens.rounding.largeIncreased - CustomRect { - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.top: parent.top - color: Qt.alpha(root.accent, 0.05) - implicitWidth: parent.width * root.usage + CustomClippingRect { + anchors.fill: parent + radius: parent.radius - Behavior on implicitWidth { - Anim {} + CustomRect { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.top: parent.top + color: Qt.alpha(root.accent, 0.05) + implicitWidth: parent.width * root.usage + + Behavior on implicitWidth { + Anim {} + } } } diff --git a/Modules/Settings/Common/PageBase.qml b/Modules/Settings/Common/PageBase.qml index 991fde1..6b96653 100644 --- a/Modules/Settings/Common/PageBase.qml +++ b/Modules/Settings/Common/PageBase.qml @@ -149,7 +149,7 @@ ColumnLayout { } } - VerticalFadeFlickable { + Flickable { id: flickable Layout.fillHeight: true @@ -158,7 +158,8 @@ ColumnLayout { bottomMargin: Tokens.padding.extraLarge topMargin: Tokens.padding.large - fadeAmount: 0.1 + clip: true + // fadeAmount: 0.1 contentHeight: root.contentChild?.implicitHeight ?? 0 contentItem.children: [root.contentChild] diff --git a/Modules/Settings/Content.qml b/Modules/Settings/Content.qml index 75a05c2..544b21e 100644 --- a/Modules/Settings/Content.qml +++ b/Modules/Settings/Content.qml @@ -6,7 +6,7 @@ import qs.Components import ZShell.Config import qs.Services -CustomClippingRect { +Item { id: root property color blobColor: Colors.tPalette.m3surfaceContainerLow @@ -21,7 +21,6 @@ CustomClippingRect { implicitHeight: sState.screen.height * 0.7 implicitWidth: implicitHeight * ratio - radius: Tokens.rounding.largeIncreased + Tokens.padding.small Behavior on blobColor { CAnim {} @@ -46,6 +45,8 @@ CustomClippingRect { borderTop: Tokens.padding.small group: blobGroup opacity: root.blobColor.a + // outerRadius: 10 + outerRadius: Tokens.rounding.largeIncreased + Tokens.padding.small radius: Tokens.rounding.largeIncreased } diff --git a/Modules/Settings/Wrapper.qml b/Modules/Settings/Wrapper.qml index a096857..da80db8 100644 --- a/Modules/Settings/Wrapper.qml +++ b/Modules/Settings/Wrapper.qml @@ -2,8 +2,9 @@ pragma ComponentBehavior: Bound import Quickshell import QtQuick -import qs.Components import ZShell.Config +import ZShell.Components +import qs.Components Item { id: root @@ -27,21 +28,17 @@ Item { } } - CustomClippingRect { - anchors.fill: parent + Loader { + id: content - Loader { - id: content + active: root.shouldBeActive || root.visible + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter - active: root.shouldBeActive || root.visible - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - - sourceComponent: Content { - sState.animatingContainer: content.opacity < 1 - sState.currentPageIdx: ["wallpaper"].indexOf(root.queuedMode) - sState.screen: root.screen - } + sourceComponent: Content { + sState.animatingContainer: content.opacity < 1 + sState.currentPageIdx: ["wallpaper"].indexOf(root.queuedMode) + sState.screen: root.screen } } } diff --git a/Plugins/ZShell/Blobs/blobinvertedrect.cpp b/Plugins/ZShell/Blobs/blobinvertedrect.cpp index 328cbf3..0e4336c 100644 --- a/Plugins/ZShell/Blobs/blobinvertedrect.cpp +++ b/Plugins/ZShell/Blobs/blobinvertedrect.cpp @@ -5,6 +5,7 @@ #include #include +#include #include BlobInvertedRect::BlobInvertedRect(QQuickItem* parent) : BlobShape(parent) {} @@ -129,6 +130,10 @@ QSGNode* BlobInvertedRect::updatePaintNode( material->m_invertedInner, m_cachedInvertedInner, sizeof(m_cachedInvertedInner)); + memcpy( + material->m_invertedOuterRadii, + m_cachedInvertedOuterRadii, + sizeof(m_cachedInvertedOuterRadii)); const int count = static_cast(qMin(m_cachedRects.size(), qsizetype(16))); @@ -173,6 +178,64 @@ void BlobInvertedRect::setBorderBottom(qreal v) { if (m_group) m_group->markDirty(); } +void BlobInvertedRect::setOuterRadius(qreal v) { + if (qFuzzyCompare(m_outerRadius, v)) return; + m_outerRadius = v; + emit outerRadiusChanged(); + if (m_group) m_group->markDirty(); +} + +void BlobInvertedRect::setOuterTopLeftRadius(qreal v) { + if (qFuzzyCompare(m_outerTopLeftRadius, v)) return; + m_outerTopLeftRadius = v; + emit outerTopLeftRadiusChanged(); + if (m_group) m_group->markDirty(); +} + +void BlobInvertedRect::setOuterTopRightRadius(qreal v) { + if (qFuzzyCompare(m_outerTopRightRadius, v)) return; + m_outerTopRightRadius = v; + emit outerTopRightRadiusChanged(); + if (m_group) m_group->markDirty(); +} + +void BlobInvertedRect::setOuterBottomLeftRadius(qreal v) { + if (qFuzzyCompare(m_outerBottomLeftRadius, v)) return; + m_outerBottomLeftRadius = v; + emit outerBottomLeftRadiusChanged(); + if (m_group) m_group->markDirty(); +} + +void BlobInvertedRect::setOuterBottomRightRadius(qreal v) { + if (qFuzzyCompare(m_outerBottomRightRadius, v)) return; + m_outerBottomRightRadius = v; + emit outerBottomRightRadiusChanged(); + if (m_group) m_group->markDirty(); +} + +void BlobInvertedRect::outerCornerRadii(float out[4]) const { + const auto maxR = static_cast(std::min(width(), height())) * 0.5f; + const auto base = std::min(static_cast(m_outerRadius), maxR); + out[0] = std::min( + m_outerTopRightRadius >= 0 ? static_cast(m_outerTopRightRadius) + : base, + maxR); + out[1] = std::min( + m_outerBottomRightRadius >= 0 + ? static_cast(m_outerBottomRightRadius) + : base, + maxR); + out[2] = std::min( + m_outerBottomLeftRadius >= 0 + ? static_cast(m_outerBottomLeftRadius) + : base, + maxR); + out[3] = std::min( + m_outerTopLeftRadius >= 0 ? static_cast(m_outerTopLeftRadius) + : base, + maxR); +} + void BlobInvertedRect::registerWithGroup() { if (m_group) m_group->setInvertedRect(this); } diff --git a/Plugins/ZShell/Blobs/blobinvertedrect.hpp b/Plugins/ZShell/Blobs/blobinvertedrect.hpp index 0fe0695..60121d0 100644 --- a/Plugins/ZShell/Blobs/blobinvertedrect.hpp +++ b/Plugins/ZShell/Blobs/blobinvertedrect.hpp @@ -19,6 +19,21 @@ class BlobInvertedRect : public BlobShape { Q_PROPERTY( qreal borderBottom READ borderBottom WRITE setBorderBottom NOTIFY borderBottomChanged) + Q_PROPERTY( + qreal outerRadius READ outerRadius WRITE setOuterRadius NOTIFY + outerRadiusChanged) + Q_PROPERTY( + qreal outerTopLeftRadius READ outerTopLeftRadius WRITE + setOuterTopLeftRadius NOTIFY outerTopLeftRadiusChanged) + Q_PROPERTY( + qreal outerTopRightRadius READ outerTopRightRadius WRITE + setOuterTopRightRadius NOTIFY outerTopRightRadiusChanged) + Q_PROPERTY( + qreal outerBottomLeftRadius READ outerBottomLeftRadius WRITE + setOuterBottomLeftRadius NOTIFY outerBottomLeftRadiusChanged) + Q_PROPERTY( + qreal outerBottomRightRadius READ outerBottomRightRadius WRITE + setOuterBottomRightRadius NOTIFY outerBottomRightRadiusChanged) public: explicit BlobInvertedRect(QQuickItem* parent = nullptr); @@ -40,15 +55,47 @@ class BlobInvertedRect : public BlobShape { void setBorderBottom(qreal v); + [[nodiscard]] qreal outerRadius() const { return m_outerRadius; } + + void setOuterRadius(qreal v); + + // Per-corner outer radii. -1 falls back to outerRadius (mirrors BlobRect). + [[nodiscard]] qreal outerTopLeftRadius() const { return m_outerTopLeftRadius; } + + void setOuterTopLeftRadius(qreal v); + + [[nodiscard]] qreal outerTopRightRadius() const { return m_outerTopRightRadius; } + + void setOuterTopRightRadius(qreal v); + + [[nodiscard]] qreal outerBottomLeftRadius() const { + return m_outerBottomLeftRadius; + } + + void setOuterBottomLeftRadius(qreal v); + + [[nodiscard]] qreal outerBottomRightRadius() const { + return m_outerBottomRightRadius; + } + + void setOuterBottomRightRadius(qreal v); + signals: void borderLeftChanged(); void borderRightChanged(); void borderTopChanged(); void borderBottomChanged(); + void outerRadiusChanged(); + void outerTopLeftRadiusChanged(); + void outerTopRightRadiusChanged(); + void outerBottomLeftRadiusChanged(); + void outerBottomRightRadiusChanged(); protected: [[nodiscard]] bool isInvertedRect() const override { return true; } + void outerCornerRadii(float out[4]) const override; + QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override; void registerWithGroup() override; @@ -59,4 +106,10 @@ class BlobInvertedRect : public BlobShape { qreal m_borderRight = 0; qreal m_borderTop = 0; qreal m_borderBottom = 0; + + qreal m_outerRadius = 0; + qreal m_outerTopLeftRadius = -1; + qreal m_outerTopRightRadius = -1; + qreal m_outerBottomLeftRadius = -1; + qreal m_outerBottomRightRadius = -1; }; diff --git a/Plugins/ZShell/Blobs/blobmaterial.cpp b/Plugins/ZShell/Blobs/blobmaterial.cpp index 528d617..e3b276f 100644 --- a/Plugins/ZShell/Blobs/blobmaterial.cpp +++ b/Plugins/ZShell/Blobs/blobmaterial.cpp @@ -33,7 +33,7 @@ bool BlobMaterialShader::updateUniformData( Q_UNUSED(oldMaterial); auto* mat = static_cast(newMaterial); QByteArray* buf = state.uniformData(); - Q_ASSERT(buf->size() >= 1472); + Q_ASSERT(buf->size() >= 1488); if (state.isMatrixDirty()) { const QMatrix4x4 m = state.combinedMatrix(); @@ -105,5 +105,8 @@ bool BlobMaterialShader::updateUniformData( memcpy(buf->data() + 1440, &mat->m_shadowRadius, 4); memcpy(buf->data() + 1456, mat->m_shadowColor, 16); + // Outer edge corner radii, after shadowColor, at 1472. + memcpy(buf->data() + 1472, mat->m_invertedOuterRadii, 16); + return true; } diff --git a/Plugins/ZShell/Blobs/blobmaterial.hpp b/Plugins/ZShell/Blobs/blobmaterial.hpp index 19536a5..7cc35de 100644 --- a/Plugins/ZShell/Blobs/blobmaterial.hpp +++ b/Plugins/ZShell/Blobs/blobmaterial.hpp @@ -43,6 +43,9 @@ class BlobMaterial : public QSGMaterial { // puts these after rectData. float m_shadowRadius = 0.0f; float m_shadowColor[4] = {0.0f, 0.0f, 0.0f, 0.5f}; + // Outer-edge corner radii (tr, br, bl, tl). Matches the uniform block, which + // puts this after shadowColor. + float m_invertedOuterRadii[4] = {}; BlobRectData m_rects[16] = {}; }; diff --git a/Plugins/ZShell/Blobs/blobshape.cpp b/Plugins/ZShell/Blobs/blobshape.cpp index c73d02c..33fd5b4 100644 --- a/Plugins/ZShell/Blobs/blobshape.cpp +++ b/Plugins/ZShell/Blobs/blobshape.cpp @@ -240,6 +240,8 @@ void BlobShape::updatePolish() { m_cachedInvertedRadius = 0; memset(m_cachedInvertedOuter, 0, sizeof(m_cachedInvertedOuter)); memset(m_cachedInvertedInner, 0, sizeof(m_cachedInvertedInner)); + memset( + m_cachedInvertedOuterRadii, 0, sizeof(m_cachedInvertedOuterRadii)); auto* inv = m_group->invertedRect(); if (inv) { @@ -290,6 +292,9 @@ void BlobShape::updatePolish() { m_cachedInvertedInner[1] = innerCY; m_cachedInvertedInner[2] = innerHW; m_cachedInvertedInner[3] = innerHH; + + static_cast(inv)->outerCornerRadii( + m_cachedInvertedOuterRadii); } } @@ -417,6 +422,10 @@ QSGNode* BlobShape::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { material->m_invertedInner, m_cachedInvertedInner, sizeof(m_cachedInvertedInner)); + memcpy( + material->m_invertedOuterRadii, + m_cachedInvertedOuterRadii, + sizeof(m_cachedInvertedOuterRadii)); const int count = static_cast(qMin(m_cachedRects.size(), qsizetype(16))); diff --git a/Plugins/ZShell/Blobs/blobshape.hpp b/Plugins/ZShell/Blobs/blobshape.hpp index 8bdc4da..71b0510 100644 --- a/Plugins/ZShell/Blobs/blobshape.hpp +++ b/Plugins/ZShell/Blobs/blobshape.hpp @@ -61,6 +61,16 @@ class BlobShape : public QQuickItem { virtual void cornerRadii(float out[4]) const; + // Outer-edge corner radii. Only the inverted rect draws an outer frame edge, + // so the base has no outer rounding and returns zeros. Subclasses that expose + // outer corner properties override this (order: tr, br, bl, tl). + virtual void outerCornerRadii(float out[4]) const { + out[0] = 0; + out[1] = 0; + out[2] = 0; + out[3] = 0; + } + virtual void updatePhysics() {} virtual void registerWithGroup(); @@ -88,4 +98,5 @@ class BlobShape : public QQuickItem { float m_cachedInvertedRadius = 0; float m_cachedInvertedOuter[4] = {}; float m_cachedInvertedInner[4] = {}; + float m_cachedInvertedOuterRadii[4] = {}; }; diff --git a/Plugins/ZShell/Blobs/shaders/blob.frag b/Plugins/ZShell/Blobs/shaders/blob.frag index 1231bc6..5e6fdfc 100644 --- a/Plugins/ZShell/Blobs/shaders/blob.frag +++ b/Plugins/ZShell/Blobs/shaders/blob.frag @@ -21,6 +21,7 @@ layout(std140, binding = 0) uniform buf { vec4 rectData[80]; float shadowRadius; vec4 shadowColor; + vec4 invertedOuterRadii; }; float sdRoundedBox(vec2 p, vec2 center, vec2 halfSize, float radius) { @@ -55,7 +56,8 @@ float smax(float a, float b, float k) { 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. + // Used for the frame inner side, so the outer edge keeps its own shape and + // 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); @@ -182,7 +184,12 @@ void main() { } if (hasInverted != 0) { - float dOuter = sdBox(pixel, invertedOuter.xy, invertedOuter.zw) - 1.0; + // Outer edge rounds with its own per-corner radii. With all-zero radii + // sdRoundedBox4 equals sdBox, so a frame that sets no outer radius is + // unchanged. smaxSharpA keeps this boundary authoritative for the fill. + float dOuter = + sdRoundedBox4(pixel, invertedOuter.xy, invertedOuter.zw, + invertedOuterRadii) - 1.0; float dInner = sdRoundedBox(pixel, invertedInner.xy, invertedInner.zw, invertedRadius); @@ -248,7 +255,13 @@ void main() { dInner -= sinkValue; - float dFrame = smaxSharpA(dOuter, -dInner, smoothFactor); + // dOuter is already a true circle (sdRoundedBox4). Keep it exact whenever the + // outer arc is the active boundary; a smax term added there would pull the + // arc's sides inward (curvature/tangent mismatch), so the corner would not be + // a circle. Only blend on the inner side, where the inner boundary is active. + float negInner = -dInner; + float dFrame = dOuter >= negInner ? dOuter + : smaxSharpA(dOuter, negInner, smoothFactor); mergedSdf = smin(mergedSdf, dFrame, smoothFactor); if (dFrame < minDist) { diff --git a/Plugins/ZShell/Components/CMakeLists.txt b/Plugins/ZShell/Components/CMakeLists.txt index 6d21cc0..f732ea2 100644 --- a/Plugins/ZShell/Components/CMakeLists.txt +++ b/Plugins/ZShell/Components/CMakeLists.txt @@ -10,6 +10,8 @@ qml_module(ZShell-components sparklinecontent.hpp sparklinecontent.cpp sparklineitem.hpp sparklineitem.cpp sparklinesceneitem.hpp sparklinesceneitem.cpp + roundedmask.hpp roundedmask.cpp + roundedclip.hpp roundedclip.cpp wheelinverter.hpp LIBRARIES Qt::Quick @@ -17,3 +19,12 @@ qml_module(ZShell-components Qt::CanvasPainter ZShell-internal ) + +qt_add_shaders(ZShell-components "components_shaders" + BATCHABLE OPTIMIZED NOHLSL NOMSL + GLSL "300es,330" + PREFIX "/" + FILES + shaders/roundedmask.frag + shaders/roundedmask.vert +) diff --git a/Plugins/ZShell/Components/roundedclip.cpp b/Plugins/ZShell/Components/roundedclip.cpp new file mode 100644 index 0000000..c1eb122 --- /dev/null +++ b/Plugins/ZShell/Components/roundedclip.cpp @@ -0,0 +1,131 @@ +#include "roundedclip.hpp" + +#include +#include + +#include + +#include +#include + +namespace ZShell::components { + +RoundedClip::RoundedClip(QQuickItem* parent) : QQuickItem(parent) { + setFlag(ItemHasContents); + setClip(true); +} + +bool RoundedClip::updateRadius(qreal& member, qreal radius) { + if (qFuzzyCompare(member + 1.0, radius + 1.0)) return false; + + member = radius; + update(); + return true; +} + +void RoundedClip::geometryChange( + const QRectF& newGeometry, const QRectF& oldGeometry) { + QQuickItem::geometryChange(newGeometry, oldGeometry); + if (newGeometry.size() != oldGeometry.size()) update(); +} + +QSGNode* RoundedClip::updatePaintNode( + QSGNode* oldNode, UpdatePaintNodeData* data) { + Q_UNUSED(oldNode) + Q_UNUSED(data) + + QQuickItemPrivate* priv = QQuickItemPrivate::get(this); + QQuickDefaultClipNode* clip = priv->clipNode(); + + if (!clip) return nullptr; + + // Clip node geometry is in item-local coordinates. The clip node is a + // child of the item's transform node, so the clip rect is + // QRectF(0, 0, width, height), i.e. boundingRect()/clipRect(). + const QRectF r(0, 0, width(), height()); + + // Per-corner radii fall back to the shared `radius` shorthand. + const qreal tl = m_topLeftRadius > 0 ? m_topLeftRadius : m_radius; + const qreal tr = m_topRightRadius > 0 ? m_topRightRadius : m_radius; + const qreal br = m_bottomRightRadius > 0 ? m_bottomRightRadius : m_radius; + const qreal bl = m_bottomLeftRadius > 0 ? m_bottomLeftRadius : m_radius; + + // Nothing rounded: plain rectangular clip. + if (tl <= 0 && tr <= 0 && br <= 0 && bl <= 0) { + clip->setRect(r); + clip->update(); + return nullptr; + } + + // All corners equal: let QQuickDefaultClipNode build its own known-correct + // scanline geometry. This is the working path. + if (qFuzzyCompare(tl, tr) && qFuzzyCompare(tr, br) && + qFuzzyCompare(br, bl)) { + clip->setRect(r); + clip->setRadius(tl); + clip->update(); + return nullptr; + } + + // Per-corner radii. setRadius() supports only a single value, so we + // must supply the geometry ourselves. Build it as a triangle fan from an + // interior point so the polygon interior fills for the stencil clip. + const qreal half = std::min(r.width() / 2, r.height() / 2); + const qreal rtl = std::min(tl, half); + const qreal rtr = std::min(tr, half); + const qreal rbr = std::min(br, half); + const qreal rbl = std::min(bl, half); + + auto segments = [](qreal radius) { + return qMin(30, int(std::ceil(radius))); + }; + + const int s_tl = segments(rtl); + const int s_tr = segments(rtr); + const int s_br = segments(rbr); + const int s_bl = segments(rbl); + + clip->setRect(r); + clip->setIsRectangular(false); + + QSGGeometry* g = clip->geometry(); + g->setDrawingMode(QSGGeometry::DrawTriangleFan); + + // One apex + one point per arc step + one point per zero-radius corner, + // plus a closing apex to complete the fan. + g->allocate(1 + s_tl + s_tr + s_br + s_bl + 5); + + QSGGeometry::Point2D* vertices = g->vertexDataAsPoint2D(); + int vi = 0; + + // Apex at an interior point. + vertices[vi++] = + QSGGeometry::Point2D(float(r.center().x()), float(r.center().y())); + + // Each corner contributes its arc points (i = 0..segments) from its + // edge-entry tangent to its edge-exit tangent. Consecutive corners are + // joined by the implicit straight edges, forming the rounded rectangle. + auto addArc = + [&](int segs, qreal cx, qreal cy, qreal rad, qreal a0, qreal a1) { + for (int i = 0; i <= segs; ++i) { + const qreal angle = a0 + (a1 - a0) * qreal(i) / qreal(segs); + vertices[vi++] = QSGGeometry::Point2D( + float(cx + rad * std::cos(angle)), + float(cy + rad * std::sin(angle))); + } + }; + + addArc(s_tl, rtl, rtl, rtl, M_PI, 3.0 * M_PI / 2.0); + addArc(s_tr, r.width() - rtr, rtr, rtr, -M_PI / 2.0, 0.0); + addArc(s_br, r.width() - rbr, r.height() - rbr, rbr, 0.0, M_PI / 2.0); + addArc(s_bl, rbl, r.height() - rbl, rbl, M_PI / 2.0, M_PI); + + // Close the fan back to the first perimeter vertex. + vertices[vi] = vertices[1]; + + clip->markDirty(QSGNode::DirtyGeometry); + + return nullptr; +} + +} // namespace ZShell::components diff --git a/Plugins/ZShell/Components/roundedclip.hpp b/Plugins/ZShell/Components/roundedclip.hpp new file mode 100644 index 0000000..7c95d67 --- /dev/null +++ b/Plugins/ZShell/Components/roundedclip.hpp @@ -0,0 +1,88 @@ +#pragma once + +#include +#include +#include + +namespace ZShell::components { + +class RoundedClip : public QQuickItem { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged) + Q_PROPERTY( + qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY + topLeftRadiusChanged) + Q_PROPERTY( + qreal topRightRadius READ topRightRadius WRITE setTopRightRadius NOTIFY + topRightRadiusChanged) + Q_PROPERTY( + qreal bottomRightRadius READ bottomRightRadius WRITE + setBottomRightRadius NOTIFY bottomRightRadiusChanged) + Q_PROPERTY( + qreal bottomLeftRadius READ bottomLeftRadius WRITE setBottomLeftRadius + NOTIFY bottomLeftRadiusChanged) + + public: + explicit RoundedClip(QQuickItem* parent = nullptr); + + qreal radius() const { return m_radius; } + + void setRadius(qreal radius) { + if (updateRadius(m_radius, radius)) emit radiusChanged(); + } + + [[nodiscard]] qreal topLeftRadius() const { return m_topLeftRadius; } + + void setTopLeftRadius(qreal radius) { + if (updateRadius(m_topLeftRadius, radius)) emit topLeftRadiusChanged(); + } + + [[nodiscard]] qreal topRightRadius() const { return m_topRightRadius; } + + void setTopRightRadius(qreal radius) { + if (updateRadius(m_topRightRadius, radius)) + emit topRightRadiusChanged(); + } + + [[nodiscard]] qreal bottomRightRadius() const { + return m_bottomRightRadius; + } + + void setBottomRightRadius(qreal radius) { + if (updateRadius(m_bottomRightRadius, radius)) + emit bottomRightRadiusChanged(); + } + + [[nodiscard]] qreal bottomLeftRadius() const { return m_bottomLeftRadius; } + + void setBottomLeftRadius(qreal radius) { + if (updateRadius(m_bottomLeftRadius, radius)) + emit bottomLeftRadiusChanged(); + } + + signals: + void radiusChanged(); + void topLeftRadiusChanged(); + void topRightRadiusChanged(); + void bottomRightRadiusChanged(); + void bottomLeftRadiusChanged(); + + protected: + QSGNode* updatePaintNode( + QSGNode* oldNode, UpdatePaintNodeData* data) override; + void geometryChange( + const QRectF& newGeometry, const QRectF& oldGeometry) override; + + private: + bool updateRadius(qreal& member, qreal radius); + + qreal m_radius = 0; + qreal m_topLeftRadius = 0; + qreal m_topRightRadius = 0; + qreal m_bottomRightRadius = 0; + qreal m_bottomLeftRadius = 0; +}; + +} // namespace ZShell::components diff --git a/Plugins/ZShell/Components/roundedmask.cpp b/Plugins/ZShell/Components/roundedmask.cpp new file mode 100644 index 0000000..ecc2979 --- /dev/null +++ b/Plugins/ZShell/Components/roundedmask.cpp @@ -0,0 +1,268 @@ +#include "roundedmask.hpp" + +#include +#include +#include + +#include +#include + +namespace ZShell::components { + +using Qt::StringLiterals::operator""_s; + +RoundedMask::RoundedMask(QQuickItem* parent) : QQuickItem(parent) { + setFlag(ItemHasContents); +} + +QQuickItem* RoundedMask::source() const { + return m_source; +} + +void RoundedMask::setSource(QQuickItem* source) { + if (m_source == source) return; + + m_source = source; + emit sourceChanged(); + update(); +} + +qreal RoundedMask::topLeftRadius() const { + return m_topLeftRadius; +} + +void RoundedMask::setTopLeftRadius(qreal radius) { + if (updateRadius(m_topLeftRadius, radius)) emit topLeftRadiusChanged(); +} + +qreal RoundedMask::topRightRadius() const { + return m_topRightRadius; +} + +void RoundedMask::setTopRightRadius(qreal radius) { + if (updateRadius(m_topRightRadius, radius)) emit topRightRadiusChanged(); +} + +qreal RoundedMask::bottomRightRadius() const { + return m_bottomRightRadius; +} + +void RoundedMask::setBottomRightRadius(qreal radius) { + if (updateRadius(m_bottomRightRadius, radius)) + emit bottomRightRadiusChanged(); +} + +qreal RoundedMask::bottomLeftRadius() const { + return m_bottomLeftRadius; +} + +void RoundedMask::setBottomLeftRadius(qreal radius) { + if (updateRadius(m_bottomLeftRadius, radius)) + emit bottomLeftRadiusChanged(); +} + +bool RoundedMask::updateRadius(qreal& member, qreal radius) { + if (qFuzzyCompare(member + 1.0, radius + 1.0)) return false; + + member = radius; + update(); + return true; +} + +QColor RoundedMask::color() const { + return m_color; +} + +void RoundedMask::setColor(const QColor& color) { + if (m_color == color) return; + + m_color = color; + emit colorChanged(); + update(); +} + +void RoundedMask::geometryChange( + const QRectF& newGeometry, const QRectF& oldGeometry) { + QQuickItem::geometryChange(newGeometry, oldGeometry); + if (newGeometry.size() != oldGeometry.size()) update(); +} + +void RoundedMask::releaseResources() { + m_node = nullptr; +} + +void RoundedMask::invalidateSceneGraph() { + m_node = nullptr; +} + +void RoundedMask::watchProvider(QSGTextureProvider* provider) { + if (m_provider == provider) return; + + if (m_provider) QObject::disconnect(m_provider, nullptr, this, nullptr); + + m_provider = provider; + if (!m_provider) return; + + QObject::connect( + m_provider, + &QSGTextureProvider::textureChanged, + this, + &QQuickItem::update); + // Emitted on the render thread by the provider's cleanup job; drop the texture before it is rendered again + QObject::connect( + m_provider, + &QObject::destroyed, + this, + [this] { + m_provider = nullptr; + if (m_node) { + static_cast(m_node->material()) + ->m_texture = nullptr; + m_node->markDirty(QSGNode::DirtyMaterial); + } + update(); + }, + Qt::DirectConnection); +} + +QSGNode* RoundedMask::updatePaintNode( + QSGNode* oldNode, UpdatePaintNodeData* data) { + Q_UNUSED(data); + + auto* const provider = m_source && m_source->isTextureProvider() + ? m_source->textureProvider() + : nullptr; + watchProvider(provider); + + auto* const texture = provider ? provider->texture() : nullptr; + if (!texture || width() <= 0 || height() <= 0) { + delete oldNode; + m_node = nullptr; + return nullptr; + } + + auto* node = static_cast(oldNode); + if (!node) node = new RoundedMaskNode; + m_node = node; + + QSGGeometry::updateTexturedRectGeometry( + node->geometry(), boundingRect(), QRectF(0, 0, 1, 1)); + node->markDirty(QSGNode::DirtyGeometry); + + const auto dpr = static_cast(window()->effectiveDevicePixelRatio()); + auto* const material = static_cast(node->material()); + if (!material->m_fallback) { + QImage blank(1, 1, QImage::Format_RGBA8888_Premultiplied); + blank.fill(Qt::transparent); + material->m_fallback.reset(window()->createTextureFromImage(blank)); + } + material->m_texture = texture; + material->m_size[0] = static_cast(width()) * dpr; + material->m_size[1] = static_cast(height()) * dpr; + const auto maxRadius = static_cast(std::min(width(), height()) / 2); + const auto clampRadius = [&](qreal radius) { + return std::clamp(static_cast(radius), 0.0f, maxRadius) * dpr; + }; + material->m_radii[0] = clampRadius(m_topLeftRadius); + material->m_radii[1] = clampRadius(m_topRightRadius); + material->m_radii[2] = clampRadius(m_bottomRightRadius); + material->m_radii[3] = clampRadius(m_bottomLeftRadius); + const auto alpha = m_color.alphaF(); + material->m_color[0] = m_color.redF() * alpha; + material->m_color[1] = m_color.greenF() * alpha; + material->m_color[2] = m_color.blueF() * alpha; + material->m_color[3] = alpha; + node->markDirty(QSGNode::DirtyMaterial); + + return node; +} + +QSGMaterialType* RoundedMaskMaterial::type() const { + static QSGMaterialType s_type; + return &s_type; +} + +QSGMaterialShader* RoundedMaskMaterial::createShader( + QSGRendererInterface::RenderMode mode) const { + Q_UNUSED(mode); + return new RoundedMaskMaterialShader; +} + +int RoundedMaskMaterial::compare(const QSGMaterial* other) const { + const auto* const o = static_cast(other); + if (m_texture != o->m_texture) return m_texture < o->m_texture ? -1 : 1; + + const auto fuzzyEqual = [](float a, float b) { + return qFuzzyCompare(a + 1.0f, b + 1.0f); + }; + if (!std::ranges::equal(m_size, o->m_size, fuzzyEqual) || + !std::ranges::equal(m_radii, o->m_radii, fuzzyEqual) || + !std::ranges::equal(m_color, o->m_color, fuzzyEqual)) + return this < other ? -1 : 1; + return 0; +} + +RoundedMaskMaterialShader::RoundedMaskMaterialShader() { + setShaderFileName(VertexStage, u":/shaders/roundedmask.vert.qsb"_s); + setShaderFileName(FragmentStage, u":/shaders/roundedmask.frag.qsb"_s); +} + +bool RoundedMaskMaterialShader::updateUniformData( + RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) { + Q_UNUSED(oldMaterial); + auto* const mat = static_cast(newMaterial); + QByteArray* const buf = state.uniformData(); + Q_ASSERT(buf->size() >= 112); + + if (state.isMatrixDirty()) { + const QMatrix4x4 m = state.combinedMatrix(); + std::memcpy(buf->data(), m.constData(), 64); + } + if (state.isOpacityDirty()) { + const float opacity = state.opacity(); + std::memcpy(buf->data() + 64, &opacity, 4); + } + + std::memcpy(buf->data() + 72, mat->m_size, 8); + std::memcpy(buf->data() + 80, mat->m_radii, 16); + std::memcpy(buf->data() + 96, mat->m_color, 16); + return true; +} + +void RoundedMaskMaterialShader::updateSampledImage( + RenderState& state, + int binding, + QSGTexture** texture, + QSGMaterial* newMaterial, + QSGMaterial* oldMaterial) { + Q_UNUSED(oldMaterial); + if (binding != 1) return; + + const auto* const mat = + static_cast(newMaterial); + auto* const tex = mat->m_texture ? mat->m_texture : mat->m_fallback.data(); + tex->commitTextureOperations(state.rhi(), state.resourceUpdateBatch()); + *texture = tex; +} + +RoundedMaskNode::RoundedMaskNode() { + auto* const geometry = + new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 4); + geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip); + setGeometry(geometry); + setFlag(OwnsGeometry); + + auto* const material = new RoundedMaskMaterial; + material->setFlag(QSGMaterial::Blending); + setMaterial(material); + setFlag(OwnsMaterial); + setFlag(UsePreprocess); +} + +void RoundedMaskNode::preprocess() { + auto* const texture = qobject_cast( + static_cast(material())->m_texture); + if (texture && texture->updateTexture()) markDirty(DirtyMaterial); +} + +} // namespace ZShell::components diff --git a/Plugins/ZShell/Components/roundedmask.hpp b/Plugins/ZShell/Components/roundedmask.hpp new file mode 100644 index 0000000..5eaaa8f --- /dev/null +++ b/Plugins/ZShell/Components/roundedmask.hpp @@ -0,0 +1,128 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ZShell::components { + +class RoundedMaskNode; + +class RoundedMask : public QQuickItem { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY( + QQuickItem* source READ source WRITE setSource NOTIFY sourceChanged) + Q_PROPERTY( + qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY + topLeftRadiusChanged) + Q_PROPERTY( + qreal topRightRadius READ topRightRadius WRITE setTopRightRadius NOTIFY + topRightRadiusChanged) + Q_PROPERTY( + qreal bottomRightRadius READ bottomRightRadius WRITE + setBottomRightRadius NOTIFY bottomRightRadiusChanged) + Q_PROPERTY( + qreal bottomLeftRadius READ bottomLeftRadius WRITE setBottomLeftRadius + NOTIFY bottomLeftRadiusChanged) + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) + + public: + explicit RoundedMask(QQuickItem* parent = nullptr); + + [[nodiscard]] QQuickItem* source() const; + void setSource(QQuickItem* source); + + [[nodiscard]] qreal topLeftRadius() const; + void setTopLeftRadius(qreal radius); + + [[nodiscard]] qreal topRightRadius() const; + void setTopRightRadius(qreal radius); + + [[nodiscard]] qreal bottomRightRadius() const; + void setBottomRightRadius(qreal radius); + + [[nodiscard]] qreal bottomLeftRadius() const; + void setBottomLeftRadius(qreal radius); + + [[nodiscard]] QColor color() const; + void setColor(const QColor& color); + + signals: + void sourceChanged(); + void topLeftRadiusChanged(); + void topRightRadiusChanged(); + void bottomRightRadiusChanged(); + void bottomLeftRadiusChanged(); + void colorChanged(); + + protected: + QSGNode* updatePaintNode( + QSGNode* oldNode, UpdatePaintNodeData* data) override; + void geometryChange( + const QRectF& newGeometry, const QRectF& oldGeometry) override; + void releaseResources() override; + + private slots: + void invalidateSceneGraph(); + + private: + void watchProvider(QSGTextureProvider* provider); + bool updateRadius(qreal& member, qreal radius); + + QPointer m_source; + qreal m_topLeftRadius = 0; + qreal m_topRightRadius = 0; + qreal m_bottomRightRadius = 0; + qreal m_bottomLeftRadius = 0; + QColor m_color = Qt::transparent; + QSGTextureProvider* m_provider = nullptr; + RoundedMaskNode* m_node = nullptr; +}; + +class RoundedMaskMaterial : public QSGMaterial { + public: + [[nodiscard]] QSGMaterialType* type() const override; + [[nodiscard]] QSGMaterialShader* createShader( + QSGRendererInterface::RenderMode mode) const override; + int compare(const QSGMaterial* other) const override; + + QSGTexture* m_texture = nullptr; + QScopedPointer m_fallback; + float m_size[2] = {}; + float m_radii[4] = {}; + float m_color[4] = {}; +}; + +class RoundedMaskMaterialShader : public QSGMaterialShader { + public: + RoundedMaskMaterialShader(); + + bool updateUniformData( + RenderState& state, + QSGMaterial* newMaterial, + QSGMaterial* oldMaterial) override; + void updateSampledImage( + RenderState& state, + int binding, + QSGTexture** texture, + QSGMaterial* newMaterial, + QSGMaterial* oldMaterial) override; +}; + +class RoundedMaskNode : public QSGGeometryNode { + public: + RoundedMaskNode(); + + void preprocess() override; +}; + +} // namespace ZShell::components diff --git a/Plugins/ZShell/Components/shaders/roundedmask.frag b/Plugins/ZShell/Components/shaders/roundedmask.frag new file mode 100644 index 0000000..8ded1e3 --- /dev/null +++ b/Plugins/ZShell/Components/shaders/roundedmask.frag @@ -0,0 +1,26 @@ +#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; + vec2 size; + vec4 radii; + vec4 color; +}; + +layout(binding = 1) uniform sampler2D source; + +void main() { + vec2 p = qt_TexCoord0 * size - size * 0.5; + float radius = p.x < 0.0 ? (p.y < 0.0 ? radii.x : radii.w) : (p.y < 0.0 ? radii.y : radii.z); + vec2 q = abs(p) - (size * 0.5 - radius); + float d = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - radius; + float mask = 1.0 - smoothstep(-0.5, 0.5, d); + + vec4 content = texture(source, qt_TexCoord0); + vec4 blended = content + color * (1.0 - content.a); + fragColor = blended * mask * qt_Opacity; +} diff --git a/Plugins/ZShell/Components/shaders/roundedmask.vert b/Plugins/ZShell/Components/shaders/roundedmask.vert new file mode 100644 index 0000000..de28d11 --- /dev/null +++ b/Plugins/ZShell/Components/shaders/roundedmask.vert @@ -0,0 +1,19 @@ +#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; + vec2 size; + vec4 radii; + vec4 color; +}; + +void main() { + gl_Position = qt_Matrix * qt_VertexPosition; + qt_TexCoord0 = qt_VertexTexCoord; +} diff --git a/Plugins/ZShell/Config/bar.hpp b/Plugins/ZShell/Config/bar.hpp index eb08a50..ed40ae8 100644 --- a/Plugins/ZShell/Config/bar.hpp +++ b/Plugins/ZShell/Config/bar.hpp @@ -21,6 +21,7 @@ class Popouts : public settings::ObjectNode { class Tray : public settings::ObjectNode { CONFIG_NODE(Tray, settings::ObjectNode) + CONFIG_PROPERTY(bool, replaceIcons, false) CONFIG_PROPERTY(bool, recolorIcons, false) CONFIG_PROPERTY(bool, showOnHover, true) CONFIG_PROPERTY(int, trayIconSize, 24)