fix: closed panels don't add resistance to bounce + use qtrendering for text

This commit is contained in:
2026-10-07 13:32:37 +02:00
parent 376595cd0b
commit 909b5113fb
14 changed files with 146 additions and 81 deletions
+5 -3
View File
@@ -16,11 +16,12 @@ Text {
color: Colors.palette.m3onSurface color: Colors.palette.m3onSurface
font: Tokens.font.body.small font: Tokens.font.body.small
linkColor: Colors.palette.m3onPrimaryFixedVariant linkColor: Colors.palette.m3onPrimaryFixedVariant
renderType: Text.NativeRendering renderType: Text.QtRendering
textFormat: Text.PlainText textFormat: Text.PlainText
Behavior on color { Behavior on color {
CAnim {} CAnim {
}
} }
Behavior on text { Behavior on text {
enabled: root.animate enabled: root.animate
@@ -32,7 +33,8 @@ Text {
type: Anim.FastEffects type: Anim.FastEffects
} }
PropertyAction {} PropertyAction {
}
TAnim { TAnim {
target: root target: root
+1
View File
@@ -9,5 +9,6 @@ CustomText {
property int grade: Colors.light ? 0 : -25 property int grade: Colors.light ? 0 : -25
property font fontStyle: Tokens.font.icon.small 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() font: Tokens.font.icon.size(fontStyle.pointSize).weight(fontStyle.weight).vaxes(fontStyle.variableAxes).fill(fill.toFixed(1)).grade(grade).build()
} }
+1 -1
View File
@@ -151,7 +151,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top anchors.top: parent.top
clip: true clip: offsetScale > 0
implicitHeight: dashboard.implicitHeight * (1 - offsetScale) implicitHeight: dashboard.implicitHeight * (1 - offsetScale)
implicitWidth: dashboard.implicitWidth implicitWidth: dashboard.implicitWidth
+31 -17
View File
@@ -1,6 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Shapes import Quickshell
import qs.Components import qs.Components
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
@@ -12,6 +12,7 @@ GridLayout {
property real animatedPercentage: 0 property real animatedPercentage: 0
readonly property real arcStartAngle: 0.75 * Math.PI readonly property real arcStartAngle: 0.75 * Math.PI
readonly property real arcSweep: 1.5 * Math.PI readonly property real arcSweep: 1.5 * Math.PI
readonly property real dpr: (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1
required property bool horizontal required property bool horizontal
property string icon property string icon
required property color iconColor required property color iconColor
@@ -22,12 +23,17 @@ GridLayout {
property bool warning: percentage * 100 >= warningThreshold property bool warning: percentage * 100 >= warningThreshold
property int warningThreshold: 80 property int warningThreshold: 80
function snap(v) {
return Math.round(v * dpr) / dpr;
}
columnSpacing: Tokens.spacing.medium columnSpacing: Tokens.spacing.medium
columns: horizontal ? -1 : 1 columns: horizontal ? -1 : 1
percentage: 0 percentage: 0
Behavior on animatedPercentage { Behavior on animatedPercentage {
Anim {} Anim {
}
} }
Component.onCompleted: animatedPercentage = percentage Component.onCompleted: animatedPercentage = percentage
@@ -47,27 +53,35 @@ GridLayout {
text: root.icon text: root.icon
} }
CustomClippingRect { CustomRect {
id: track
Layout.preferredHeight: root.horizontal ? icon.implicitHeight - Tokens.padding.extraSmall : 4 Layout.preferredHeight: root.horizontal ? icon.implicitHeight - Tokens.padding.extraSmall : 4
Layout.preferredWidth: root.horizontal ? 4 : icon.implicitWidth Layout.preferredWidth: root.horizontal ? 4 : icon.implicitWidth
color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2) color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2)
radius: Tokens.rounding.full radius: Math.min(width, height) / 2
Item {
id: clipper
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)
CustomRect { CustomRect {
id: fill anchors.bottom: root.horizontal ? parent.bottom : undefined
anchors.horizontalCenter: root.horizontal ? parent.horizontalCenter : undefined
anchors.bottom: parent.bottom anchors.left: root.horizontal ? undefined : parent.left
anchors.left: parent.left anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
anchors.right: root.horizontal ? parent.right : undefined
anchors.top: root.horizontal ? undefined : parent.top
color: root.mainColor color: root.mainColor
implicitHeight: root.horizontal ? Math.ceil(root.percentage * parent.height) : 0 height: track.height
implicitWidth: root.horizontal ? 0 : Math.ceil(root.percentage * parent.width) radius: track.radius
width: track.width
// Behavior on implicitHeight { }
// Anim {
// }
// }
} }
} }
} }
+27 -36
View File
@@ -1,7 +1,6 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import Quickshell.Widgets
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Config import ZShell.Config
@@ -11,11 +10,6 @@ import qs.Components
Item { Item {
id: root 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: { readonly property var dashboardTabs: {
const allTabs = [ const allTabs = [
{ {
@@ -45,9 +39,12 @@ Item {
]; ];
return allTabs.filter(tab => tab.enabled); 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 implicitHeight: nonAnimHeight
implicitWidth: nonAnimWidth
Behavior on implicitHeight { Behavior on implicitHeight {
Anim { Anim {
@@ -63,15 +60,14 @@ Item {
Tabs { Tabs {
id: tabs id: tabs
anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.margins: Tokens.padding.small
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: ZUtils.clamp(anchors.margins - Config.bar.border, 0, anchors.margins)
nonAnimWidth: view.implicitWidth nonAnimWidth: view.implicitWidth
screenState: root.screenState screenState: root.screenState
tabs: root.dashboardTabs tabs: root.dashboardTabs
anchors.topMargin: ZUtils.clamp(anchors.margins - Config.bar.border, 0, anchors.margins)
anchors.margins: Tokens.padding.small
} }
Item { Item {
@@ -79,13 +75,13 @@ Item {
property int radius: Tokens.rounding.extraLarge - anchors.margins 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.bottom: parent.bottom
anchors.topMargin: Tokens.spacing.small anchors.left: parent.left
anchors.margins: Tokens.padding.small 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 { Flickable {
id: view id: view
@@ -97,16 +93,19 @@ Item {
} }
anchors.fill: parent anchors.fill: parent
implicitWidth: currentItem?.implicitWidth ?? 0
implicitHeight: currentItem?.implicitHeight ?? 0
contentX: currentItem?.x ?? 0
contentWidth: row.implicitWidth
contentHeight: row.implicitHeight contentHeight: row.implicitHeight
contentWidth: row.implicitWidth
contentX: currentItem?.x ?? 0
implicitHeight: currentItem?.implicitHeight ?? 0
implicitWidth: currentItem?.implicitWidth ?? 0
interactive: false interactive: false
Behavior on contentX {
Anim {
type: Anim.DefaultEffects
}
}
onContentXChanged: { onContentXChanged: {
if (!moving || !currentItem) if (!moving || !currentItem)
return; return;
@@ -124,10 +123,6 @@ Item {
Repeater { Repeater {
id: repeater id: repeater
model: ScriptModel {
values: root.dashboardTabs
}
delegate: Loader { delegate: Loader {
id: paneLoader id: paneLoader
@@ -135,7 +130,6 @@ Item {
required property var modelData required property var modelData
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop
sourceComponent: modelData.component sourceComponent: modelData.component
Component.onCompleted: active = Qt.binding(() => { Component.onCompleted: active = Qt.binding(() => {
@@ -146,6 +140,9 @@ Item {
return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth); return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth);
}) })
} }
model: ScriptModel {
values: root.dashboardTabs
}
} }
} }
@@ -153,8 +150,8 @@ Item {
id: dashComponent id: dashComponent
Dash { Dash {
screenState: root.screenState
radius: viewWrapper.radius radius: viewWrapper.radius
screenState: root.screenState
} }
} }
@@ -170,8 +167,8 @@ Item {
id: mediaComponent id: mediaComponent
Media { Media {
screenState: root.screenState
radius: viewWrapper.radius radius: viewWrapper.radius
screenState: root.screenState
} }
} }
@@ -182,12 +179,6 @@ Item {
radius: viewWrapper.radius radius: viewWrapper.radius
} }
} }
Behavior on contentX {
Anim {
type: Anim.DefaultEffects
}
}
} }
} }
} }
@@ -272,6 +272,10 @@ Item {
retainWhileLoading: true retainWhileLoading: true
smooth: true smooth: true
source: Wallpapers.current source: Wallpapers.current
sourceSize.height: {
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return imageSlot.height * dpr;
}
Behavior on opacity { Behavior on opacity {
Anim { Anim {
-12
View File
@@ -42,18 +42,6 @@ Item {
objectName: "PageContainer" objectName: "PageContainer"
Component.onCompleted: root.loadPage(root.sState.currentPageIdx) 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 { Connections {
+27 -2
View File
@@ -189,6 +189,9 @@ void BlobGroup2::ensureDataBuilt() {
const int count = static_cast<int>(m_data.rects.size()); const int count = static_cast<int>(m_data.rects.size());
// Which shape is the source for the bar edge.
resolveSources(entryShapes);
// Exclusion gates only the smooth blend. // Exclusion gates only the smooth blend.
for (int i = 0; i < count; ++i) { for (int i = 0; i < count; ++i) {
int mask = 0; int mask = 0;
@@ -251,8 +254,8 @@ void BlobGroup2::ensureDataBuilt() {
ri.radius[3] = std::max(ri.radius[3] * fTl, minR); ri.radius[3] = std::max(ri.radius[3] * fTl, minR);
} }
// Inverted rect. The frame term is evaluated for every pixel in the bounds, // Inverted rect. The frame term is evaluated for every pixel in the bounds, so the
// so the group flag cannot be gated per shape. // group flag cannot be gated per shape.
if (!m_invertedRect) return; if (!m_invertedRect) return;
auto* inv = static_cast<BlobShape2*>(m_invertedRect); auto* inv = static_cast<BlobShape2*>(m_invertedRect);
@@ -299,6 +302,28 @@ void BlobGroup2::ensureDataBuilt() {
2.0 * (static_cast<double>(outerHH) + extent))); 2.0 * (static_cast<double>(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<BlobShape2*>& entryShapes) {
const int count = static_cast<int>(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) { void BlobGroup2::fillMaterial(BlobMaterial* material) {
material->m_paddedX = static_cast<float>(m_bounds.x()); material->m_paddedX = static_cast<float>(m_bounds.x());
material->m_paddedY = static_cast<float>(m_bounds.y()); material->m_paddedY = static_cast<float>(m_bounds.y());
+6 -3
View File
@@ -22,9 +22,11 @@ class BlobInvertedRect2;
// disagree with the first. That is what makes overlapping shapes read as one // disagree with the first. That is what makes overlapping shapes read as one
// surface - one value, one writer, one alpha per pixel. // surface - one value, one writer, one alpha per pixel.
struct BlobGroupData { struct BlobGroupData {
// The shader reads rectData[80] = 16 entries, and excludeMask is a 32-bit // The shader reads rectData[80] = 16 entries of 5 vec4, and excludeMask is a
// mask. The shared list is capped at 16, in group order, so the cap is the // 32-bit mask. The shared list is capped at 16, in group order, so the cap is the
// same list for every pixel. // 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; static constexpr int kMaxRects = 16;
QVector<BlobRectData> rects; QVector<BlobRectData> rects;
@@ -109,6 +111,7 @@ class BlobGroup2 : public QQuickItem {
private: private:
void ensurePhysicsUpdated(); void ensurePhysicsUpdated();
void ensureDataBuilt(); void ensureDataBuilt();
void resolveSources(const QVector<BlobShape2*>& entryShapes);
void fillMaterial(BlobMaterial* material); void fillMaterial(BlobMaterial* material);
qreal m_smoothing = 32.0; qreal m_smoothing = 32.0;
+6 -1
View File
@@ -92,7 +92,12 @@ bool BlobMaterialShader::updateUniformData(
memcpy(&maskAsFloat, &r.excludeMask, sizeof(float)); memcpy(&maskAsFloat, &r.excludeMask, sizeof(float));
const float d0[4] = {r.cx, r.cy, r.hw, r.hh}; 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 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, d0, 16);
memcpy(buf->data() + base + 16, d1, 16); memcpy(buf->data() + base + 16, d1, 16);
memcpy(buf->data() + base + 32, r.invDeform, 16); memcpy(buf->data() + base + 32, r.invDeform, 16);
+5
View File
@@ -17,6 +17,11 @@ struct BlobRectData {
// Bitmask of indices in this rect's m_cachedRects that mutually exclude (or are excluded by) this rect. // 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. // Used by the shader to skip smin between excluded pairs.
int excludeMask = 0; 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 { class BlobMaterial : public QSGMaterial {
+3
View File
@@ -71,6 +71,9 @@ class BlobRect2 : public BlobShape2 {
bool isExcluded(const BlobShape2* other) const override; bool isExcluded(const BlobShape2* other) const override;
bool isCornerExcluded(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; void cornerRadii(float out[4]) const override;
[[nodiscard]] qreal topLeftRadius() const { return m_topLeftRadius; } [[nodiscard]] qreal topLeftRadius() const { return m_topLeftRadius; }
+4
View File
@@ -56,6 +56,10 @@ class BlobShape2 : public QQuickItem {
virtual bool isExcluded(const BlobShape2*) const { return false; } 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 { virtual bool isCornerExcluded(const BlobShape2* /*other*/) const {
return false; return false;
} }
+26 -6
View File
@@ -65,6 +65,19 @@ float smaxSharpA(float a, float b, float k) {
void main() { void main() {
vec2 pixel = vec2(paddedX, paddedY) + qt_TexCoord0 * vec2(paddedW, paddedH); 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 // 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: // 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 // myIndex stays -2, so the ownership test is skipped and this pass writes every
@@ -78,10 +91,17 @@ void main() {
vec4 props = vec4 props =
rectData[i * 5 + 1]; // excludeMask(int bits), offsetX, offsetY, minEig rectData[i * 5 + 1]; // excludeMask(int bits), offsetX, offsetY, minEig
vec4 invDm = rectData[i * 5 + 2]; // inverse deform matrix 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 = vec4 radii =
rectData[i * 5 + 4]; // effective per-corner radii (tr, br, bl, tl) 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 // Offset center for asymmetric deformation
vec2 center = rect.xy + props.yz; vec2 center = rect.xy + props.yz;
@@ -182,10 +202,6 @@ void main() {
} }
if (hasInverted != 0) { 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 // Border sinks: track the opposite rect edge, clamped to border thickness
float innerTop = invertedInner.y - invertedInner.w; float innerTop = invertedInner.y - invertedInner.w;
float innerBot = invertedInner.y + invertedInner.w; float innerBot = invertedInner.y + invertedInner.w;
@@ -198,6 +214,10 @@ void main() {
float sinkValue = 0.0; float sinkValue = 0.0;
for (int i = 0; i < rectCount; i++) { 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 rect = rectData[i * 5];
vec4 sinkProps = rectData[i * 5 + 1]; vec4 sinkProps = rectData[i * 5 + 1];
vec2 sinkSh = rectData[i * 5 + 3].xy; 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) // Delay sink to absorb smin blend depth (cubic smin max = k/6)
float preOff = smoothFactor * (1.0 / 6.0); 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, float topPen = clamp(innerTop - (ctr.y + sinkSh.y) - preOff, 0.0,
innerTop - outerTop); innerTop - outerTop);