From 974c4d3167579e523dfbff90cdd280c74fde65e1 Mon Sep 17 00:00:00 2001 From: purian23 Date: Thu, 29 Oct 2026 13:55:16 -0400 Subject: [PATCH] control center: one surface under the edit strip & backdrops/frame sheet fixes (cherry picked from commit e48c1221e743c36eecd203a6c2808f50a40af223) --- dux-qml-common | 2 +- flake.lock | 6 +-- quickshell/Common/ConnectedSurfaceGeometry.js | 7 ++- .../Components/CcEditActions.qml | 9 ++-- .../Components/CcWidgetSheet.qml | 5 +- .../ControlCenter/ControlCenterContent.qml | 16 +++++-- .../ControlCenter/Widgets/CcSheetDialog.qml | 2 + quickshell/Modules/Frame/FrameWindow.qml | 8 +--- .../WorkspaceOverlays/HyprlandOverview.qml | 2 +- quickshell/Widgets/DPopoutHost.qml | 46 +++++++++++-------- .../tests/connected-surface-geometry.test.mjs | 28 +++++++++++ 11 files changed, 86 insertions(+), 45 deletions(-) create mode 100644 quickshell/tests/connected-surface-geometry.test.mjs diff --git a/dux-qml-common b/dux-qml-common index 09ac4aba5..2065795d9 160000 --- a/dux-qml-common +++ b/dux-qml-common @@ -1 +1 @@ -Subproject commit 09ac4aba511f10636c273e11028fe092196c78ba +Subproject commit 2065795d9382b03c71e6f316b4955c6a74051934 diff --git a/flake.lock b/flake.lock index 1e4b9dd72..b5a75cdf5 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "dank-qml-common": { "flake": false, "locked": { - "lastModified": 1793224384, - "narHash": "sha256-mJaCE9rdBd/HyYSCPomQM/uX+1YNmFfV64bMIdUSiL0=", + "lastModified": 1793296417, + "narHash": "sha256-gCK9N6GPq+3HdCXrikVSRcE3/u7HWRJHJpcfE2cpH3Q=", "owner": "duxlabs-dev", "repo": "dux-qml-common", - "rev": "09ac4aba511f10636c273e11028fe092196c78ba", + "rev": "2065795d9382b03c71e6f316b4955c6a74051934", "type": "github" }, "original": { diff --git a/quickshell/Common/ConnectedSurfaceGeometry.js b/quickshell/Common/ConnectedSurfaceGeometry.js index 3ce42bd53..57b1551b3 100644 --- a/quickshell/Common/ConnectedSurfaceGeometry.js +++ b/quickshell/Common/ConnectedSurfaceGeometry.js @@ -18,13 +18,16 @@ function isVertical(side) { return side === "left" || side === "right"; } +// Overflow chrome (the edit strip) is part of the silhouette, so it rolls out with the body. function bodyRect(descriptor, dpr) { var source = descriptor && descriptor.bodyRect ? descriptor.bodyRect : descriptor || {}; + var overflowTop = Math.max(0, _number(descriptor && descriptor.overflowTop, 0)); + var overflowBottom = Math.max(0, _number(descriptor && descriptor.overflowBottom, 0)); return { "x": snap(source.x !== undefined ? source.x : source.bodyX, dpr), - "y": snap(source.y !== undefined ? source.y : source.bodyY, dpr), + "y": snap(_number(source.y !== undefined ? source.y : source.bodyY, 0) - overflowTop, dpr), "width": Math.max(0, snap(source.width !== undefined ? source.width : source.bodyW, dpr)), - "height": Math.max(0, snap(source.height !== undefined ? source.height : source.bodyH, dpr)) + "height": Math.max(0, snap(_number(source.height !== undefined ? source.height : source.bodyH, 0) + overflowTop + overflowBottom, dpr)) }; } diff --git a/quickshell/Modules/ControlCenter/Components/CcEditActions.qml b/quickshell/Modules/ControlCenter/Components/CcEditActions.qml index f357ad673..d797d5489 100644 --- a/quickshell/Modules/ControlCenter/Components/CcEditActions.qml +++ b/quickshell/Modules/ControlCenter/Components/CcEditActions.qml @@ -22,7 +22,6 @@ Item { property bool overTrash: false property color backgroundColor: CcMetrics.dialogColor property vector4d cornerRadii: Qt.vector4d(Theme.windowRadius, Theme.windowRadius, Theme.windowRadius, Theme.windowRadius) - // Shoulders fill outside the panel's corner arcs so a translucent fill never doubles up. property bool joined: false property bool above: false readonly property real leftRadius: above ? cornerRadii.x : cornerRadii.w @@ -32,13 +31,11 @@ Item { readonly property string outline: { const w = width; const h = height; - const lw = Math.min(leftRadius, w / 2); - const rw = Math.min(rightRadius, w / 2); - const lf = Math.min(lw, h / 2); - const rf = Math.min(rw, h / 2); + const lf = Math.min(leftRadius, w / 2, h / 2); + const rf = Math.min(rightRadius, w / 2, h / 2); const y = v => above ? h - v : v; const s = flag => above ? 1 - flag : flag; - const joinedEdge = joined ? `M 0 ${y(-lw)} A ${lw} ${lw} 0 0 ${s(0)} ${lw} ${y(0)} L ${w - rw} ${y(0)} A ${rw} ${rw} 0 0 ${s(0)} ${w} ${y(-rw)}` : `M 0 ${y(lf)} A ${lf} ${lf} 0 0 ${s(1)} ${lf} ${y(0)} L ${w - rf} ${y(0)} A ${rf} ${rf} 0 0 ${s(1)} ${w} ${y(rf)}`; + const joinedEdge = joined ? `M 0 ${y(0)} L ${w} ${y(0)}` : `M 0 ${y(lf)} A ${lf} ${lf} 0 0 ${s(1)} ${lf} ${y(0)} L ${w - rf} ${y(0)} A ${rf} ${rf} 0 0 ${s(1)} ${w} ${y(rf)}`; return `${joinedEdge} L ${w} ${y(h - rf)} A ${rf} ${rf} 0 0 ${s(1)} ${w - rf} ${y(h)} L ${lf} ${y(h)} A ${lf} ${lf} 0 0 ${s(1)} 0 ${y(h - lf)} Z`; } diff --git a/quickshell/Modules/ControlCenter/Components/CcWidgetSheet.qml b/quickshell/Modules/ControlCenter/Components/CcWidgetSheet.qml index f9e0e1cac..219e62366 100644 --- a/quickshell/Modules/ControlCenter/Components/CcWidgetSheet.qml +++ b/quickshell/Modules/ControlCenter/Components/CcWidgetSheet.qml @@ -98,7 +98,8 @@ DBottomSheet { width: CcMetrics.previewSize height: width radius: item.modelData.id === "user" ? Theme.fullRadius(width, height) : Theme.cornerRadiusLIncreased - color: CcMetrics.iconBoxInactiveColor + // Solid: translucent, it vanishes against the card in light mode. + color: Theme.chipSurfaceNested DIcon { anchors.centerIn: parent @@ -186,7 +187,7 @@ DBottomSheet { width: Theme.avatarSize height: width radius: Theme.fullRadius(width, height) - color: CcMetrics.iconBoxInactiveColor + color: Theme.chipSurfaceNested DIcon { anchors.centerIn: parent diff --git a/quickshell/Modules/ControlCenter/ControlCenterContent.qml b/quickshell/Modules/ControlCenter/ControlCenterContent.qml index 5e67900a3..a31d80e1b 100644 --- a/quickshell/Modules/ControlCenter/ControlCenterContent.qml +++ b/quickshell/Modules/ControlCenter/ControlCenterContent.qml @@ -68,6 +68,8 @@ FocusScope { readonly property bool panelResizing: panelResizer.resizing readonly property real sheetContentWidth: host.sheetContentWidth ?? CcMetrics.sheetWidthFor(gridColumns) readonly property vector4d surfaceCornerRadii: host.surfaceCornerRadii ?? Qt.vector4d(Theme.windowRadius, Theme.windowRadius, Theme.windowRadius, Theme.windowRadius) + // A dim can't follow a frame-attached body past its square corners; the covered content is faded out anyway. + readonly property bool sheetsDim: !CcMetrics.hideCoveredContent || Math.min(surfaceCornerRadii.x, surfaceCornerRadii.y, surfaceCornerRadii.z, surfaceCornerRadii.w) > 0 readonly property int gridColumnCap: host.gridColumnCap ?? CcMetrics.columnCapFor((host.triggerScreen?.width ?? CcMetrics.sheetWidthDefault + Theme.spacingL * 2) - Theme.spacingL * 2) readonly property int gridColumns: host.gridColumns ?? Math.min(CcMetrics.gridColumns, gridColumnCap) readonly property real availableGridHeight: (host.availableHeight ?? (host.triggerScreen?.height ?? CcMetrics.fallbackScreenHeight) - CcMetrics.maxHeightInset) - CcMetrics.sheetPadding * 2 - editRoom @@ -377,8 +379,8 @@ FocusScope { cornerResize: root.panelResizing ? resizeSide > 0 : root.panelResizer.sideMovable(1, root.gridColumns) horizontalResize: true removable: false - cornerRadius: Theme.windowRadius - cornerRadii: root.surfaceCornerRadii + // Frame-attached corners are square, but the ring keeps the surface radius. + cornerRadius: Math.max(root.surfaceCornerRadii.x, root.surfaceCornerRadii.y, root.surfaceCornerRadii.z, root.surfaceCornerRadii.w) buttonSize: Theme.iconSize iconSize: PopoutMetrics.chromeIconSize resizing: root.panelResizing @@ -533,7 +535,7 @@ FocusScope { y: root.editRoom > 0 ? Math.max(root.height - root.editRoom, CcMetrics.sheetPadding * 2 + root.heldGridHeight) + (root.editRoom - height) / 2 : (root.stripAbove ? -height : root.height) width: root.sheetContentWidth height: implicitHeight - // Below the edit rings so the shoulder fill never covers them. + // Below the edit rings so the trash fill never covers them. z: -1 editMode: root.host.editMode enabled: detailBand.shownSection === "" && !root.widgetSheetOpen @@ -543,7 +545,8 @@ FocusScope { transientSurfaceTracker: root.host.transientSurfaceTracker joined: root.editRoom === 0 above: root.stripAbove - backgroundColor: joined ? (root.host.surfaceColor ?? CcMetrics.dialogColor) : Theme.foregroundColor(Theme.cardSurface) + // Joined, the popout host paints the surface under the strip. + backgroundColor: joined ? "transparent" : Theme.foregroundColor(Theme.cardSurface) cornerRadii: root.surfaceCornerRadii opacity: body.opacity canUndo: root.canUndo @@ -563,6 +566,7 @@ FocusScope { active: false sourceComponent: BluetoothCodecSelector { cornerRadii: root.surfaceCornerRadii + dimsBackdrop: root.sheetsDim onDismissed: Qt.callLater(root.releaseSheet, codecSelectorLoader) } } @@ -575,6 +579,7 @@ FocusScope { active: false sourceComponent: AudioPortSelector { cornerRadii: root.surfaceCornerRadii + dimsBackdrop: root.sheetsDim onDismissed: Qt.callLater(root.releaseSheet, portSelectorLoader) } } @@ -591,6 +596,8 @@ FocusScope { id: widgetSheetLoader anchors.fill: parent + anchors.topMargin: editActions.joined && root.stripAbove ? -editActions.height : 0 + anchors.bottomMargin: editActions.joined && !root.stripAbove ? -editActions.height : 0 z: CcMetrics.overlayZ active: root.host.editMode asynchronous: true @@ -602,6 +609,7 @@ FocusScope { } sourceComponent: CcWidgetSheet { scrimRadii: root.surfaceCornerRadii + scrimVisible: root.sheetsDim surfaceColor: CcMetrics.dialogColor widgets: { const existingIds = root.placedWidgetIds.split(","); diff --git a/quickshell/Modules/ControlCenter/Widgets/CcSheetDialog.qml b/quickshell/Modules/ControlCenter/Widgets/CcSheetDialog.qml index c20d815b1..390a8c9e6 100644 --- a/quickshell/Modules/ControlCenter/Widgets/CcSheetDialog.qml +++ b/quickshell/Modules/ControlCenter/Widgets/CcSheetDialog.qml @@ -17,6 +17,7 @@ Item { property real panelWidth: CcMetrics.dialogWidth property vector4d cornerRadii: Qt.vector4d(Theme.windowRadius, Theme.windowRadius, Theme.windowRadius, Theme.windowRadius) property Item backdrop: null + property bool dimsBackdrop: true readonly property real presence: panel.opacity default property alias content: contentSlot.data property alias showScrollBar: contentFlickable.showScrollBar @@ -53,6 +54,7 @@ Item { topRightRadius: root.cornerRadii.y bottomRightRadius: root.cornerRadii.z bottomLeftRadius: root.cornerRadii.w + visible: root.dimsBackdrop color: Theme.withAlpha(Theme.backdropColor, Theme.scrimAlpha) opacity: root.shown ? 1 : 0 diff --git a/quickshell/Modules/Frame/FrameWindow.qml b/quickshell/Modules/Frame/FrameWindow.qml index 73cba5a9e..fef12e6f6 100644 --- a/quickshell/Modules/Frame/FrameWindow.qml +++ b/quickshell/Modules/Frame/FrameWindow.qml @@ -563,13 +563,7 @@ PanelWindow { readonly property bool _active: win._blurSurfacesActive && win._popoutDescriptor.visible - // Covers chrome hung off the body; the cap and connectors stay on the body. - readonly property var _blur: win._seamPaddedBlurRect({ - "x": win._popoutBodyGeometry.x, - "y": win._popoutBodyGeometry.y - win._popoutDescriptor.overflowTop, - "width": win._popoutBodyGeometry.width, - "height": win._popoutBodyGeometry.height + win._popoutDescriptor.overflowTop + win._popoutDescriptor.overflowBottom - }) + readonly property var _blur: win._seamPaddedBlurRect(win._popoutBodyGeometry) radius: win._surfaceRadius x: _active ? _blur.x : 0 diff --git a/quickshell/Modules/WorkspaceOverlays/HyprlandOverview.qml b/quickshell/Modules/WorkspaceOverlays/HyprlandOverview.qml index 1430f27ca..7bae14a76 100644 --- a/quickshell/Modules/WorkspaceOverlays/HyprlandOverview.qml +++ b/quickshell/Modules/WorkspaceOverlays/HyprlandOverview.qml @@ -92,7 +92,7 @@ Scope { Rectangle { id: background anchors.fill: parent - color: "black" + color: Theme.backdropColor opacity: overviewScope.overviewOpen ? 0.5 : 0 Behavior on opacity { diff --git a/quickshell/Widgets/DPopoutHost.qml b/quickshell/Widgets/DPopoutHost.qml index dda4598b7..30d14024d 100644 --- a/quickshell/Widgets/DPopoutHost.qml +++ b/quickshell/Widgets/DPopoutHost.qml @@ -233,7 +233,7 @@ Item { if (barSide !== "top" && barSide !== "bottom") return contentContainer.animY; - const extent = Math.max(0, root.pubBodyH); + const extent = Math.max(0, root.pubBodyH + root.topOverflow + root.bottomOverflow); const progress = Math.min(1, Math.abs(contentContainer.animY) / Math.max(1, _chromeAnimTravelY)); const offset = Theme.snap(extent * progress, root.dpr); return contentContainer.animY < 0 ? -offset : offset; @@ -995,7 +995,11 @@ Item { return; if ((!hoverDismissEnabled && !isClosing) || !ConnectedModeState.hasSurfaceOwner(root.screen.name, "popout", chromeLease.claimId)) return; - const body = SurfaceGeometry.animatedBodyRect(_connectedChromeState(), root.dpr); + // Hand off the bare body; the overflow stays on this descriptor. + const state = _connectedChromeState(); + state.overflowTop = 0; + state.overflowBottom = 0; + const body = SurfaceGeometry.animatedBodyRect(state, root.dpr); const traveling = morphTravelEnabled && !morph.running; const rect = traveling ? root._boundTravel(travelSpring.value) : Qt.rect(body.x, body.y, Math.max(1, body.width), Math.max(1, body.height)); const velocity = traveling ? travelSpring.velocity : Qt.vector4d(0, 0, 0, 0); @@ -1681,7 +1685,7 @@ Item { readonly property bool directionalEffect: Theme.isDirectionalEffect readonly property bool depthEffect: Theme.isDepthEffect readonly property real directionalTravelX: Math.max(root.animationOffset, root.renderedAlignedWidth + Theme.spacingL) - readonly property real directionalTravelY: Math.max(root.animationOffset, root.renderedAlignedHeight + Theme.spacingL) + readonly property real directionalTravelY: Math.max(root.animationOffset, root.renderedAlignedHeight + root.topOverflow + root.bottomOverflow + Theme.spacingL) readonly property real depthTravel: Math.max(root.animationOffset * 0.7, 28) readonly property real sectionTilt: (triggerSection === "left" ? -1 : (triggerSection === "right" ? 1 : 0)) readonly property real horizontalConnectorExtent: root.usesConnectedSurfaceChrome && (barTop || barBottom) ? Theme.connectedCornerRadius : 0 @@ -1850,13 +1854,14 @@ Item { readonly property real s: Math.min(1, contentContainer.scaleValue) readonly property bool trackBlurFromBarEdge: root.usesConnectedSurfaceChrome - readonly property real _dyClamp: (contentContainer.barTop || contentContainer.barBottom) ? Math.max(-contentContainer.height, Math.min(contentContainer.animY, contentContainer.height)) : 0 + readonly property real _overflowExtent: root.topOverflow + root.bottomOverflow + readonly property real _dyClamp: (contentContainer.barTop || contentContainer.barBottom) ? Math.max(-contentContainer.height - _overflowExtent, Math.min(contentContainer.animY, contentContainer.height + _overflowExtent)) : 0 readonly property real _dxClamp: (contentContainer.barLeft || contentContainer.barRight) ? Math.max(-contentContainer.width, Math.min(contentContainer.animX, contentContainer.width)) : 0 blurX: trackBlurFromBarEdge ? contentContainer.x + (contentContainer.barRight ? _dxClamp : 0) : contentContainer.x + contentContainer.width * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr) - contentContainer.horizontalConnectorExtent * s blurY: (trackBlurFromBarEdge ? contentContainer.y + (contentContainer.barBottom ? _dyClamp : 0) : contentContainer.y + contentContainer.height * (1 - s) * 0.5 + Theme.snap(contentContainer.animY, root.dpr) - contentContainer.verticalConnectorExtent * s) - root.topOverflow blurWidth: root.shouldBeVisible ? (trackBlurFromBarEdge ? Math.max(0, contentContainer.width - Math.abs(_dxClamp)) : (contentContainer.width + contentContainer.horizontalConnectorExtent * 2) * s) : 0 - blurHeight: root.shouldBeVisible ? (trackBlurFromBarEdge ? Math.max(0, contentContainer.height - Math.abs(_dyClamp)) : (contentContainer.height + contentContainer.verticalConnectorExtent * 2) * s) + root.topOverflow + root.bottomOverflow : 0 + blurHeight: root.shouldBeVisible ? (trackBlurFromBarEdge ? Math.max(0, contentContainer.height + _overflowExtent - Math.abs(_dyClamp)) : (contentContainer.height + contentContainer.verticalConnectorExtent * 2) * s + _overflowExtent) : 0 blurRadius: root.usesConnectedSurfaceChrome ? Theme.connectedCornerRadius : Theme.windowRadius } @@ -1914,11 +1919,11 @@ Item { ElevationShadow { visible: !root.usesConnectedSurfaceChrome width: rollOutAdjuster.baseWidth - height: rollOutAdjuster.baseHeight + height: rollOutAdjuster.baseHeight + root.topOverflow + root.bottomOverflow opacity: contentWrapper.publishedOpacity scale: contentWrapper.scale x: contentWrapper.x - y: contentWrapper.y + y: contentWrapper.y - root.topOverflow level: root.shadowLevel direction: root.effectiveShadowDirection fallbackOffset: root.shadowFallbackOffset @@ -1927,9 +1932,8 @@ Item { topRightRadius: chrome.surfaceTopRightRadius bottomLeftRadius: chrome.surfaceBottomLeftRadius bottomRightRadius: chrome.surfaceBottomRightRadius - targetColor: chrome.surfaceColor - borderColor: chrome.surfaceBorderColor - borderWidth: chrome.surfaceBorderWidth + // Shadow only; contentWrapper paints the fill. + targetColor: "transparent" shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !(root.suspendShadowWhileResizing && root._resizeActive) && !root.frameOwnsConnectedChrome } @@ -1950,11 +1954,11 @@ Item { readonly property real pad: shadowsOn ? Math.ceil(Math.max(shadowBlurPx + shadowSpreadPx + Math.max(Math.abs(shadowOffsetX), Math.abs(shadowOffsetY)), ambient.blurPx + ambient.spreadPx) + 2) : 0 width: rollOutAdjuster.baseWidth + extraLeft * 2 - height: rollOutAdjuster.baseHeight + extraTop * 2 + height: rollOutAdjuster.baseHeight + extraTop * 2 + root.topOverflow + root.bottomOverflow opacity: contentWrapper.publishedOpacity scale: contentWrapper.scale x: contentWrapper.x - extraLeft - y: contentWrapper.y - extraTop + y: contentWrapper.y - extraTop - root.topOverflow ShaderEffect { anchors.fill: parent @@ -1970,7 +1974,7 @@ Item { property vector4d shadowColor: Qt.vector4d(localChrome.shadowTint.r, localChrome.shadowTint.g, localChrome.shadowTint.b, localChrome.shadowsOn ? localChrome.shadowTint.a : 0) property vector4d shadowParam: Qt.vector4d(Math.max(0, localChrome.shadowBlurPx), localChrome.shadowSpreadPx, localChrome.shadowOffsetX, localChrome.shadowOffsetY) property vector4d ambientParam: Qt.vector4d(localChrome.ambient.blurPx, localChrome.ambient.spreadPx, localChrome.shadowsOn ? localChrome.ambient.alpha : 0, 0) - property vector4d bodyRect: Qt.vector4d((contentContainer.barLeft ? 0 : localChrome.pad) + localChrome.extraLeft, (contentContainer.barTop ? 0 : localChrome.pad) + localChrome.extraTop, rollOutAdjuster.baseWidth, rollOutAdjuster.baseHeight) + property vector4d bodyRect: Qt.vector4d((contentContainer.barLeft ? 0 : localChrome.pad) + localChrome.extraLeft, (contentContainer.barTop ? 0 : localChrome.pad) + localChrome.extraTop, rollOutAdjuster.baseWidth, rollOutAdjuster.baseHeight + root.topOverflow + root.bottomOverflow) property vector4d cornerRadius: Qt.vector4d(chrome.surfaceTopLeftRadius, chrome.surfaceTopRightRadius, chrome.surfaceBottomRightRadius, chrome.surfaceBottomLeftRadius) property vector4d edgeParam: Qt.vector4d(contentContainer.barTop ? 0 : (contentContainer.barBottom ? 1 : (contentContainer.barLeft ? 2 : 3)), Theme.connectedCornerRadius, 0, 0) } @@ -2017,7 +2021,9 @@ Item { visible: !root.usesConnectedSurfaceChrome Rectangle { - anchors.fill: parent + y: -root.topOverflow + width: parent.width + height: parent.height + root.topOverflow + root.bottomOverflow antialiasing: true topLeftRadius: chrome.surfaceTopLeftRadius topRightRadius: chrome.surfaceTopRightRadius @@ -2126,11 +2132,11 @@ Item { parent: root._fluidMotionActive ? chrome : contentClip.contentItem z: root._fluidMotionActive ? -1 : 0 width: root._fluidMotionActive ? chrome.fluidBody.width : chrome.width - height: root._fluidMotionActive ? chrome.fluidBody.height : chrome.height + root.topOverflow + root.bottomOverflow + height: (root._fluidMotionActive ? chrome.fluidBody.height : chrome.height) + root.topOverflow + root.bottomOverflow opacity: root._fluidMotionActive ? FluidGeometry.chromeOpacity(morph.value) : contentWrapper.publishedOpacity scale: root.directionalRevealActive ? 1 : contentWrapper.scale x: root._fluidMotionActive ? chrome.fluidBody.x : (root.directionalRevealActive ? 0 : contentWrapper.x) - y: root._fluidMotionActive ? chrome.fluidBody.y : (root.directionalRevealActive ? 0 : contentWrapper.y) - root.topOverflow + y: (root._fluidMotionActive ? chrome.fluidBody.y : (root.directionalRevealActive ? 0 : contentWrapper.y)) - root.topOverflow level: root.shadowLevel direction: root.effectiveShadowDirection fallbackOffset: root.shadowFallbackOffset @@ -2172,7 +2178,9 @@ Item { } Rectangle { - anchors.fill: parent + y: -root.topOverflow + width: parent.width + height: parent.height + root.topOverflow + root.bottomOverflow visible: !root._fluidMotionActive radius: Theme.windowRadius color: Theme.readableSurface @@ -2182,9 +2190,9 @@ Item { Rectangle { parent: root._fluidMotionActive ? chrome : contentClip.contentItem width: root._fluidMotionActive ? chrome.fluidBody.width : chrome.width - height: root._fluidMotionActive ? chrome.fluidBody.height : chrome.height + root.topOverflow + root.bottomOverflow + height: (root._fluidMotionActive ? chrome.fluidBody.height : chrome.height) + root.topOverflow + root.bottomOverflow x: root._fluidMotionActive ? chrome.fluidBody.x : (root.directionalRevealActive ? 0 : contentWrapper.x) - y: root._fluidMotionActive ? chrome.fluidBody.y : (root.directionalRevealActive ? 0 : contentWrapper.y) - root.topOverflow + y: (root._fluidMotionActive ? chrome.fluidBody.y : (root.directionalRevealActive ? 0 : contentWrapper.y)) - root.topOverflow opacity: root._fluidMotionActive ? FluidGeometry.chromeOpacity(morph.value) : contentWrapper.publishedOpacity scale: root.directionalRevealActive ? 1 : contentWrapper.scale visible: contentWrapper.visible diff --git a/quickshell/tests/connected-surface-geometry.test.mjs b/quickshell/tests/connected-surface-geometry.test.mjs new file mode 100644 index 000000000..d0e23f4e5 --- /dev/null +++ b/quickshell/tests/connected-surface-geometry.test.mjs @@ -0,0 +1,28 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; +import vm from "node:vm"; + +const source = readFileSync(new URL("../Common/ConnectedSurfaceGeometry.js", import.meta.url), "utf8").replace(".pragma library", ""); +const geometry = vm.createContext({}); +vm.runInContext(source, geometry); + +test("an edit strip hung off the body rolls out with it", () => { + const body = { x: 10, y: 40, width: 300, height: 200 }; + const cases = [ + { side: "top", overflowTop: 0, overflowBottom: 50, dy: -125, rect: { x: 10, y: 40, width: 300, height: 125 } }, + { side: "top", overflowTop: 0, overflowBottom: 50, dy: -250, rect: { x: 10, y: 40, width: 300, height: 0 } }, + { side: "bottom", overflowTop: 50, overflowBottom: 0, dy: 125, rect: { x: 10, y: 115, width: 300, height: 125 } }, + { side: "left", overflowTop: 0, overflowBottom: 50, dy: 0, rect: { x: 10, y: 40, width: 300, height: 250 } } + ]; + for (const c of cases) { + const rect = geometry.animatedBodyRect({ + barSide: c.side, + bodyRect: body, + animationOffset: { x: 0, y: c.dy }, + overflowTop: c.overflowTop, + overflowBottom: c.overflowBottom + }, 1); + assert.deepEqual({ x: rect.x, y: rect.y, width: rect.width, height: rect.height }, c.rect, `${c.side} dy=${c.dy}`); + } +});