diff --git a/Components/CustomText.qml b/Components/CustomText.qml index 144665e..5445f5f 100644 --- a/Components/CustomText.qml +++ b/Components/CustomText.qml @@ -16,11 +16,12 @@ Text { color: Colors.palette.m3onSurface font: Tokens.font.body.small linkColor: Colors.palette.m3onPrimaryFixedVariant - renderType: Text.NativeRendering + renderType: Text.QtRendering textFormat: Text.PlainText Behavior on color { - CAnim {} + CAnim { + } } Behavior on text { enabled: root.animate @@ -32,7 +33,8 @@ Text { type: Anim.FastEffects } - PropertyAction {} + PropertyAction { + } TAnim { target: root diff --git a/Components/MaterialIcon.qml b/Components/MaterialIcon.qml index b93e38c..73ca9d9 100644 --- a/Components/MaterialIcon.qml +++ b/Components/MaterialIcon.qml @@ -9,5 +9,6 @@ CustomText { property int grade: Colors.light ? 0 : -25 property font fontStyle: Tokens.font.icon.small + renderType: Text.NativeRendering font: Tokens.font.icon.size(fontStyle.pointSize).weight(fontStyle.weight).vaxes(fontStyle.variableAxes).fill(fill.toFixed(1)).grade(grade).build() } diff --git a/Drawers/Panels.qml b/Drawers/Panels.qml index 50dd9c8..f6542dd 100644 --- a/Drawers/Panels.qml +++ b/Drawers/Panels.qml @@ -151,7 +151,7 @@ Item { anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top - clip: true + clip: offsetScale > 0 implicitHeight: dashboard.implicitHeight * (1 - offsetScale) implicitWidth: dashboard.implicitWidth diff --git a/Modules/Bar/Components/Resource.qml b/Modules/Bar/Components/Resource.qml index cce4285..b22d9e7 100644 --- a/Modules/Bar/Components/Resource.qml +++ b/Modules/Bar/Components/Resource.qml @@ -1,6 +1,6 @@ import QtQuick import QtQuick.Layouts -import QtQuick.Shapes +import Quickshell import qs.Components import ZShell.Config import qs.Services @@ -12,6 +12,7 @@ GridLayout { property real animatedPercentage: 0 readonly property real arcStartAngle: 0.75 * Math.PI readonly property real arcSweep: 1.5 * Math.PI + readonly property real dpr: (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1 required property bool horizontal property string icon required property color iconColor @@ -22,12 +23,17 @@ GridLayout { property bool warning: percentage * 100 >= warningThreshold property int warningThreshold: 80 + function snap(v) { + return Math.round(v * dpr) / dpr; + } + columnSpacing: Tokens.spacing.medium columns: horizontal ? -1 : 1 percentage: 0 Behavior on animatedPercentage { - Anim {} + Anim { + } } Component.onCompleted: animatedPercentage = percentage @@ -47,27 +53,35 @@ GridLayout { text: root.icon } - CustomClippingRect { + CustomRect { + id: track + Layout.preferredHeight: root.horizontal ? icon.implicitHeight - Tokens.padding.extraSmall : 4 Layout.preferredWidth: root.horizontal ? 4 : icon.implicitWidth color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2) - radius: Tokens.rounding.full + radius: Math.min(width, height) / 2 - CustomRect { - id: fill + Item { + id: clipper - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: root.horizontal ? parent.right : undefined - anchors.top: root.horizontal ? undefined : parent.top - color: root.mainColor - implicitHeight: root.horizontal ? Math.ceil(root.percentage * parent.height) : 0 - implicitWidth: root.horizontal ? 0 : Math.ceil(root.percentage * parent.width) + anchors.bottom: root.horizontal ? parent.bottom : undefined + anchors.horizontalCenter: root.horizontal ? parent.horizontalCenter : undefined + anchors.left: root.horizontal ? undefined : parent.left + anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter + clip: true + height: root.horizontal ? root.snap(root.percentage * track.height) : track.height * 2 + width: root.horizontal ? track.width * 2 : root.snap(root.percentage * track.width) - // Behavior on implicitHeight { - // Anim { - // } - // } + CustomRect { + anchors.bottom: root.horizontal ? parent.bottom : undefined + anchors.horizontalCenter: root.horizontal ? parent.horizontalCenter : undefined + anchors.left: root.horizontal ? undefined : parent.left + anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter + color: root.mainColor + height: track.height + radius: track.radius + width: track.width + } } } } diff --git a/Modules/Dashboard/Content.qml b/Modules/Dashboard/Content.qml index 0ca16cc..97c322d 100644 --- a/Modules/Dashboard/Content.qml +++ b/Modules/Dashboard/Content.qml @@ -1,7 +1,6 @@ pragma ComponentBehavior: Bound import Quickshell -import Quickshell.Widgets import QtQuick import QtQuick.Layouts import ZShell.Config @@ -11,11 +10,6 @@ import qs.Components Item { id: root - readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2 - readonly property real nonAnimHeight: tabs.implicitHeight + tabs.anchors.topMargin + view.implicitHeight + viewWrapper.anchors.margins + viewWrapper.anchors.topMargin - - required property ScreenState screenState - readonly property var dashboardTabs: { const allTabs = [ { @@ -45,9 +39,12 @@ Item { ]; return allTabs.filter(tab => tab.enabled); } + readonly property real nonAnimHeight: tabs.implicitHeight + tabs.anchors.topMargin + view.implicitHeight + viewWrapper.anchors.margins + viewWrapper.anchors.topMargin + readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2 + required property ScreenState screenState - implicitWidth: nonAnimWidth implicitHeight: nonAnimHeight + implicitWidth: nonAnimWidth Behavior on implicitHeight { Anim { @@ -63,15 +60,14 @@ Item { Tabs { id: tabs - anchors.top: parent.top anchors.left: parent.left + anchors.margins: Tokens.padding.small anchors.right: parent.right - + anchors.top: parent.top + anchors.topMargin: ZUtils.clamp(anchors.margins - Config.bar.border, 0, anchors.margins) nonAnimWidth: view.implicitWidth screenState: root.screenState tabs: root.dashboardTabs - anchors.topMargin: ZUtils.clamp(anchors.margins - Config.bar.border, 0, anchors.margins) - anchors.margins: Tokens.padding.small } Item { @@ -79,13 +75,13 @@ Item { property int radius: Tokens.rounding.extraLarge - anchors.margins - anchors.top: tabs.bottom - anchors.left: parent.left - anchors.right: parent.right anchors.bottom: parent.bottom - anchors.topMargin: Tokens.spacing.small + anchors.left: parent.left anchors.margins: Tokens.padding.small - clip: true + anchors.right: parent.right + anchors.top: tabs.bottom + anchors.topMargin: Tokens.spacing.small + clip: view.currentIndex !== 3 || view.contentX !== (view.currentItem?.x ?? 0) Flickable { id: view @@ -97,16 +93,19 @@ Item { } anchors.fill: parent - - implicitWidth: currentItem?.implicitWidth ?? 0 - implicitHeight: currentItem?.implicitHeight ?? 0 - - contentX: currentItem?.x ?? 0 - contentWidth: row.implicitWidth contentHeight: row.implicitHeight - + contentWidth: row.implicitWidth + contentX: currentItem?.x ?? 0 + implicitHeight: currentItem?.implicitHeight ?? 0 + implicitWidth: currentItem?.implicitWidth ?? 0 interactive: false + Behavior on contentX { + Anim { + type: Anim.DefaultEffects + } + } + onContentXChanged: { if (!moving || !currentItem) return; @@ -124,10 +123,6 @@ Item { Repeater { id: repeater - model: ScriptModel { - values: root.dashboardTabs - } - delegate: Loader { id: paneLoader @@ -135,7 +130,6 @@ Item { required property var modelData Layout.alignment: Qt.AlignTop - sourceComponent: modelData.component Component.onCompleted: active = Qt.binding(() => { @@ -146,6 +140,9 @@ Item { return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth); }) } + model: ScriptModel { + values: root.dashboardTabs + } } } @@ -153,8 +150,8 @@ Item { id: dashComponent Dash { - screenState: root.screenState radius: viewWrapper.radius + screenState: root.screenState } } @@ -170,8 +167,8 @@ Item { id: mediaComponent Media { - screenState: root.screenState radius: viewWrapper.radius + screenState: root.screenState } } @@ -182,12 +179,6 @@ Item { radius: viewWrapper.radius } } - - Behavior on contentX { - Anim { - type: Anim.DefaultEffects - } - } } } } diff --git a/Modules/Settings/Common/WallpaperCropper.qml b/Modules/Settings/Common/WallpaperCropper.qml index 393b3d9..27eaa47 100644 --- a/Modules/Settings/Common/WallpaperCropper.qml +++ b/Modules/Settings/Common/WallpaperCropper.qml @@ -272,6 +272,10 @@ Item { retainWhileLoading: true smooth: true source: Wallpapers.current + sourceSize.height: { + const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; + return imageSlot.height * dpr; + } Behavior on opacity { Anim { diff --git a/Modules/Settings/Pages.qml b/Modules/Settings/Pages.qml index 4aacc6d..3235e04 100644 --- a/Modules/Settings/Pages.qml +++ b/Modules/Settings/Pages.qml @@ -42,18 +42,6 @@ Item { objectName: "PageContainer" Component.onCompleted: root.loadPage(root.sState.currentPageIdx) - - // CustomRect { - // anchors.fill: parent - // color: Qt.alpha(Colors.palette.m3shadow, 0.3) - // opacity: root.sState.dimmed ? 1 : 0 - // visible: opacity > 0 - // z: 0 - // - // Behavior on opacity { - // Anim {} - // } - // } } Connections { diff --git a/Plugins/ZShell/Blobs/blobgroup2.cpp b/Plugins/ZShell/Blobs/blobgroup2.cpp index 219d48c..2273733 100644 --- a/Plugins/ZShell/Blobs/blobgroup2.cpp +++ b/Plugins/ZShell/Blobs/blobgroup2.cpp @@ -189,6 +189,9 @@ void BlobGroup2::ensureDataBuilt() { const int count = static_cast(m_data.rects.size()); + // Which shape is the source for the bar edge. + resolveSources(entryShapes); + // Exclusion gates only the smooth blend. for (int i = 0; i < count; ++i) { int mask = 0; @@ -251,8 +254,8 @@ void BlobGroup2::ensureDataBuilt() { ri.radius[3] = std::max(ri.radius[3] * fTl, minR); } - // Inverted rect. The frame term is evaluated for every pixel in the bounds, - // so the group flag cannot be gated per shape. + // Inverted rect. The frame term is evaluated for every pixel in the bounds, so the + // group flag cannot be gated per shape. if (!m_invertedRect) return; auto* inv = static_cast(m_invertedRect); @@ -299,6 +302,28 @@ void BlobGroup2::ensureDataBuilt() { 2.0 * (static_cast(outerHH) + extent))); } +// A point on the bar edge can only have one source. A shape that is not pushing is not +// the source: it still fills its own area, but inside the border strip it must not add +// its own outline to a span another shape is already shaping. That is the case that +// matters — one panel at rest next to one that is bouncing — where the edge showed +// several steps instead of one smooth indent of the closing panel. Only one shape can be +// pushing at a time, so the flag is just that, and the shader gates the strip with it. +void BlobGroup2::resolveSources(const QVector& entryShapes) { + const int count = static_cast(m_data.rects.size()); + + // With nothing pushing there is no span being shaped, so a shape at rest keeps its + // own outline and nothing is gated. + bool anyPushing = false; + for (int i = 0; i < count; ++i) + if (entryShapes[i]->isMoving()) { + anyPushing = true; + break; + } + + for (int i = 0; i < count; ++i) + m_data.rects[i].source = !anyPushing || entryShapes[i]->isMoving(); +} + void BlobGroup2::fillMaterial(BlobMaterial* material) { material->m_paddedX = static_cast(m_bounds.x()); material->m_paddedY = static_cast(m_bounds.y()); diff --git a/Plugins/ZShell/Blobs/blobgroup2.hpp b/Plugins/ZShell/Blobs/blobgroup2.hpp index 9f922e6..988816e 100644 --- a/Plugins/ZShell/Blobs/blobgroup2.hpp +++ b/Plugins/ZShell/Blobs/blobgroup2.hpp @@ -22,9 +22,11 @@ class BlobInvertedRect2; // disagree with the first. That is what makes overlapping shapes read as one // surface - one value, one writer, one alpha per pixel. struct BlobGroupData { - // The shader reads rectData[80] = 16 entries, and excludeMask is a 32-bit - // mask. The shared list is capped at 16, in group order, so the cap is the - // same list for every pixel. + // The shader reads rectData[80] = 16 entries of 5 vec4, and excludeMask is a + // 32-bit mask. The shared list is capped at 16, in group order, so the cap is the + // same list for every pixel. Each entry is either a source for its span or not; the + // flag is in the third vec4 alongside the screen half-extents, so no extra data is + // needed. static constexpr int kMaxRects = 16; QVector rects; @@ -109,6 +111,7 @@ class BlobGroup2 : public QQuickItem { private: void ensurePhysicsUpdated(); void ensureDataBuilt(); + void resolveSources(const QVector& entryShapes); void fillMaterial(BlobMaterial* material); qreal m_smoothing = 32.0; diff --git a/Plugins/ZShell/Blobs/blobmaterial.cpp b/Plugins/ZShell/Blobs/blobmaterial.cpp index 528d617..cc21de9 100644 --- a/Plugins/ZShell/Blobs/blobmaterial.cpp +++ b/Plugins/ZShell/Blobs/blobmaterial.cpp @@ -92,7 +92,12 @@ bool BlobMaterialShader::updateUniformData( memcpy(&maskAsFloat, &r.excludeMask, sizeof(float)); const float d0[4] = {r.cx, r.cy, r.hw, r.hh}; const float d1[4] = {maskAsFloat, r.offsetX, r.offsetY, r.minEig}; - const float d3[4] = {r.screenHalfX, r.screenHalfY, 0.0f, 0.0f}; + const float d3[4] = { + r.screenHalfX, + r.screenHalfY, + r.source ? 1.0f : 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); diff --git a/Plugins/ZShell/Blobs/blobmaterial.hpp b/Plugins/ZShell/Blobs/blobmaterial.hpp index 19536a5..659a7e6 100644 --- a/Plugins/ZShell/Blobs/blobmaterial.hpp +++ b/Plugins/ZShell/Blobs/blobmaterial.hpp @@ -17,6 +17,11 @@ struct BlobRectData { // Bitmask of indices in this rect's m_cachedRects that mutually exclude (or are excluded by) this rect. // Used by the shader to skip smin between excluded pairs. int excludeMask = 0; + // Whether this entry is pushing on the bar edge. Only the shape that is pushing can + // shape the edge; a shape at rest still fills its own area, but the shader gives it + // no contribution inside the border strip, so it cannot add its own outline to a span + // another shape is shaping. + bool source = true; }; class BlobMaterial : public QSGMaterial { diff --git a/Plugins/ZShell/Blobs/blobrect2.hpp b/Plugins/ZShell/Blobs/blobrect2.hpp index 800b37b..2cd29dc 100644 --- a/Plugins/ZShell/Blobs/blobrect2.hpp +++ b/Plugins/ZShell/Blobs/blobrect2.hpp @@ -71,6 +71,9 @@ class BlobRect2 : public BlobShape2 { bool isExcluded(const BlobShape2* other) const override; bool isCornerExcluded(const BlobShape2* other) const override; + // The shape that is pushing is the one that can shape the bar edge. A shape that is + // retiring still counts, because the retire curve is still moving the surface. + bool isMoving() const override { return m_physicsActive || m_retiring; } void cornerRadii(float out[4]) const override; [[nodiscard]] qreal topLeftRadius() const { return m_topLeftRadius; } diff --git a/Plugins/ZShell/Blobs/blobshape2.hpp b/Plugins/ZShell/Blobs/blobshape2.hpp index 3035082..caadd8f 100644 --- a/Plugins/ZShell/Blobs/blobshape2.hpp +++ b/Plugins/ZShell/Blobs/blobshape2.hpp @@ -56,6 +56,10 @@ class BlobShape2 : public QQuickItem { virtual bool isExcluded(const BlobShape2*) const { return false; } + // Whether this shape is currently pushing on a border. Only the shape that is pushing + // can shape the bar edge, so this decides the source flag. + virtual bool isMoving() const { return false; } + virtual bool isCornerExcluded(const BlobShape2* /*other*/) const { return false; } diff --git a/Plugins/ZShell/Blobs/shaders/blob.frag b/Plugins/ZShell/Blobs/shaders/blob.frag index 1231bc6..791f272 100644 --- a/Plugins/ZShell/Blobs/shaders/blob.frag +++ b/Plugins/ZShell/Blobs/shaders/blob.frag @@ -65,6 +65,19 @@ float smaxSharpA(float a, float b, float k) { void main() { vec2 pixel = vec2(paddedX, paddedY) + qt_TexCoord0 * vec2(paddedW, paddedH); + // The frame distances are needed before the per-rect pass. In the border strip the + // boundary is the frame, displaced only by the shape that is pushing, so a shape that + // is not the source must not define the boundary there. It still fills the inner area, + // so this only gates the strip, which is the bar edge. + float dOuter = 0.0; + float dInner = 0.0; + bool inStrip = false; + if (hasInverted != 0) { + dOuter = sdBox(pixel, invertedOuter.xy, invertedOuter.zw) - 1.0; + dInner = sdRoundedBox(pixel, invertedInner.xy, invertedInner.zw, invertedRadius); + inStrip = dOuter < 0.0 && dInner > 0.0; + } + // Phase 1: compute per-rect SDF, and track which rect owns the pixel for the // per-shape path (BlobGroup). The single group draw (BlobGroup2) has no owner: // myIndex stays -2, so the ownership test is skipped and this pass writes every @@ -78,10 +91,17 @@ void main() { vec4 props = rectData[i * 5 + 1]; // excludeMask(int bits), offsetX, offsetY, minEig vec4 invDm = rectData[i * 5 + 2]; // inverse deform matrix - vec4 sh = rectData[i * 5 + 3]; // screenHalfX, screenHalfY, 0, 0 + vec4 sh = rectData[i * 5 + 3]; // screenHalfX, screenHalfY, isSource, 0 vec4 radii = rectData[i * 5 + 4]; // effective per-corner radii (tr, br, bl, tl) + // A shape that is not pushing does not shape the bar edge, so inside the strip it + // must not add its own outline to a span another shape is already shaping. + if (inStrip && sh.z < 0.5) { + dArr[i] = 1e10; + continue; + } + // Offset center for asymmetric deformation vec2 center = rect.xy + props.yz; @@ -182,10 +202,6 @@ void main() { } 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; @@ -198,6 +214,10 @@ void main() { float sinkValue = 0.0; for (int i = 0; i < rectCount; i++) { + // Only a shape that is pushing can move the border. + if (rectData[i * 5 + 3].z < 0.5) + continue; + vec4 rect = rectData[i * 5]; vec4 sinkProps = rectData[i * 5 + 1]; vec2 sinkSh = rectData[i * 5 + 3].xy; @@ -208,7 +228,7 @@ void main() { // 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 + // Top border: track rect's BOTTOM edge, clamped to border thickness float topPen = clamp(innerTop - (ctr.y + sinkSh.y) - preOff, 0.0, innerTop - outerTop);