From bd4eb0cea124bac73429d8d37bb8ce7fbb29f48a Mon Sep 17 00:00:00 2001 From: purian23 Date: Tue, 7 Apr 2026 23:28:00 -0400 Subject: [PATCH] (frame): implement ConnectedModeState to better handle component sync --- quickshell/Common/ConnectedModeState.qml | 153 ++++ quickshell/Common/Theme.qml | 2 +- quickshell/Modules/Dock/Dock.qml | 133 +++- quickshell/Modules/Frame/FrameWindow.qml | 705 ++++++++++++++++-- quickshell/Modules/Settings/FrameTab.qml | 3 + .../Modules/Settings/ThemeColorsTab.qml | 8 +- quickshell/Widgets/ConnectedCorner.qml | 55 +- quickshell/Widgets/DankPopout.qml | 184 +++-- 8 files changed, 1067 insertions(+), 176 deletions(-) create mode 100644 quickshell/Common/ConnectedModeState.qml diff --git a/quickshell/Common/ConnectedModeState.qml b/quickshell/Common/ConnectedModeState.qml new file mode 100644 index 00000000..fe06cd6f --- /dev/null +++ b/quickshell/Common/ConnectedModeState.qml @@ -0,0 +1,153 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell + +Singleton { + id: root + + readonly property var emptyDockState: ({ + "reveal": false, + "barSide": "bottom", + "bodyX": 0, + "bodyY": 0, + "bodyW": 0, + "bodyH": 0, + "slideX": 0, + "slideY": 0 + }) + + // Popout state (updated by DankPopout when connectedFrameModeActive) + property string popoutOwnerToken: "" + property bool popoutVisible: false + property string popoutBarSide: "top" + property real popoutBodyX: 0 + property real popoutBodyY: 0 + property real popoutBodyW: 0 + property real popoutBodyH: 0 + property real popoutAnimX: 0 + property real popoutAnimY: 0 + property string popoutScreen: "" + + // Dock state (updated by Dock when connectedFrameModeActive), keyed by screen.name + property var dockStates: ({}) + + // Dock slide offsets — hot-path updates separated from full geometry state + property var dockSlides: ({}) + + function hasPopoutOwner(token) { + return !!token && popoutOwnerToken === token; + } + + function claimPopout(token, state) { + if (!token) + return false; + + popoutOwnerToken = token; + return updatePopout(token, state); + } + + function updatePopout(token, state) { + if (!hasPopoutOwner(token) || !state) + return false; + + if (state.visible !== undefined) + popoutVisible = !!state.visible; + if (state.barSide !== undefined) + popoutBarSide = state.barSide || "top"; + if (state.bodyX !== undefined) + popoutBodyX = Number(state.bodyX); + if (state.bodyY !== undefined) + popoutBodyY = Number(state.bodyY); + if (state.bodyW !== undefined) + popoutBodyW = Number(state.bodyW); + if (state.bodyH !== undefined) + popoutBodyH = Number(state.bodyH); + if (state.animX !== undefined) + popoutAnimX = Number(state.animX); + if (state.animY !== undefined) + popoutAnimY = Number(state.animY); + if (state.screen !== undefined) + popoutScreen = state.screen || ""; + + return true; + } + + function releasePopout(token) { + if (!hasPopoutOwner(token)) + return false; + + popoutOwnerToken = ""; + popoutVisible = false; + popoutBarSide = "top"; + popoutBodyX = 0; + popoutBodyY = 0; + popoutBodyW = 0; + popoutBodyH = 0; + popoutAnimX = 0; + popoutAnimY = 0; + popoutScreen = ""; + return true; + } + + function _cloneDockStates() { + const next = {}; + for (const screenName in dockStates) + next[screenName] = dockStates[screenName]; + return next; + } + + function _normalizeDockState(state) { + return { + "reveal": !!(state && state.reveal), + "barSide": state && state.barSide ? state.barSide : "bottom", + "bodyX": Number(state && state.bodyX !== undefined ? state.bodyX : 0), + "bodyY": Number(state && state.bodyY !== undefined ? state.bodyY : 0), + "bodyW": Number(state && state.bodyW !== undefined ? state.bodyW : 0), + "bodyH": Number(state && state.bodyH !== undefined ? state.bodyH : 0), + "slideX": Number(state && state.slideX !== undefined ? state.slideX : 0), + "slideY": Number(state && state.slideY !== undefined ? state.slideY : 0) + }; + } + + function setDockState(screenName, state) { + if (!screenName || !state) + return false; + + const next = _cloneDockStates(); + next[screenName] = _normalizeDockState(state); + dockStates = next; + return true; + } + + function clearDockState(screenName) { + if (!screenName || !dockStates[screenName]) + return false; + + const next = _cloneDockStates(); + delete next[screenName]; + dockStates = next; + + // Also clear corresponding slide + if (dockSlides[screenName]) { + const nextSlides = {}; + for (const k in dockSlides) + nextSlides[k] = dockSlides[k]; + delete nextSlides[screenName]; + dockSlides = nextSlides; + } + return true; + } + + function setDockSlide(screenName, x, y) { + if (!screenName) + return false; + const next = {}; + for (const k in dockSlides) + next[k] = dockSlides[k]; + next[screenName] = { "x": Number(x), "y": Number(y) }; + dockSlides = next; + return true; + } +} diff --git a/quickshell/Common/Theme.qml b/quickshell/Common/Theme.qml index cbbacc85..bc124cb9 100644 --- a/quickshell/Common/Theme.qml +++ b/quickshell/Common/Theme.qml @@ -975,7 +975,7 @@ Singleton { readonly property bool isConnectedEffect: AnimVariants.isConnectedEffect readonly property real connectedCornerRadius: { if (typeof SettingsData === "undefined") return 12; - return SettingsData.frameEnabled ? SettingsData.frameRounding : cornerRadius; + return SettingsData.connectedFrameModeActive ? SettingsData.frameRounding : cornerRadius; } readonly property color connectedSurfaceColor: { if (typeof SettingsData === "undefined") diff --git a/quickshell/Modules/Dock/Dock.qml b/quickshell/Modules/Dock/Dock.qml index 13804b2e..3bac8f6f 100644 --- a/quickshell/Modules/Dock/Dock.qml +++ b/quickshell/Modules/Dock/Dock.qml @@ -19,12 +19,12 @@ Variants { WindowBlur { targetWindow: dock - blurEnabled: dock.effectiveBlurEnabled - blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x - dock.horizontalConnectorExtent - blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y - dock.verticalConnectorExtent - blurWidth: dock.hasApps && dock.reveal ? dockBackground.width + dock.horizontalConnectorExtent * 2 : 0 - blurHeight: dock.hasApps && dock.reveal ? dockBackground.height + dock.verticalConnectorExtent * 2 : 0 - blurRadius: dock.surfaceRadius + blurEnabled: dock.effectiveBlurEnabled && !SettingsData.connectedFrameModeActive + blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x + blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y + blurWidth: dock.hasApps && dock.reveal ? dockBackground.width : 0 + blurHeight: dock.hasApps && dock.reveal ? dockBackground.height : 0 + blurRadius: Theme.isConnectedEffect ? Theme.connectedCornerRadius : dock.surfaceRadius } WlrLayershell.namespace: "dms:dock" @@ -43,21 +43,15 @@ Variants { property real backgroundTransparency: SettingsData.dockTransparency property bool groupByApp: SettingsData.dockGroupByApp readonly property int borderThickness: SettingsData.dockBorderEnabled ? SettingsData.dockBorderThickness : 0 - readonly property string connectedBarSide: SettingsData.dockPosition === SettingsData.Position.Top ? "top" - : SettingsData.dockPosition === SettingsData.Position.Bottom ? "bottom" - : SettingsData.dockPosition === SettingsData.Position.Left ? "left" : "right" - readonly property bool connectedBarActiveOnEdge: Theme.isConnectedEffect - && !!(dock.screen || modelData) - && SettingsData.getActiveBarEdgesForScreen(dock.screen || modelData).includes(connectedBarSide) + readonly property string connectedBarSide: SettingsData.dockPosition === SettingsData.Position.Top ? "top" : SettingsData.dockPosition === SettingsData.Position.Bottom ? "bottom" : SettingsData.dockPosition === SettingsData.Position.Left ? "left" : "right" + readonly property bool connectedBarActiveOnEdge: Theme.isConnectedEffect && !!(dock.screen || modelData) && SettingsData.getActiveBarEdgesForScreen(dock.screen || modelData).includes(connectedBarSide) readonly property real connectedJoinInset: { if (!Theme.isConnectedEffect) return 0; return connectedBarActiveOnEdge ? SettingsData.frameBarSize : SettingsData.frameThickness; } readonly property real surfaceRadius: Theme.connectedSurfaceRadius - readonly property color surfaceColor: Theme.isConnectedEffect - ? Theme.connectedSurfaceColor - : Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency) + readonly property color surfaceColor: Theme.isConnectedEffect ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency) readonly property color surfaceBorderColor: Theme.isConnectedEffect ? "transparent" : BlurService.borderColor readonly property real surfaceBorderWidth: Theme.isConnectedEffect ? 0 : BlurService.borderWidth readonly property real surfaceTopLeftRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius @@ -188,6 +182,51 @@ Variants { return placement === "left" ? seamY - height : seamY; } + // ─── ConnectedModeState sync ──────────────────────────────────────── + // Dock window origin in screen-relative coordinates (FrameWindow space). + function _dockWindowOriginX() { + if (!dock.isVertical) + return 0; + if (SettingsData.dockPosition === SettingsData.Position.Right) + return (dock.screen ? dock.screen.width : 0) - dock.width; + return 0; + } + function _dockWindowOriginY() { + if (dock.isVertical) + return 0; + if (SettingsData.dockPosition === SettingsData.Position.Bottom) + return (dock.screen ? dock.screen.height : 0) - dock.height; + return 0; + } + + readonly property string _dockScreenName: dock.modelData ? dock.modelData.name : (dock.screen ? dock.screen.name : "") + + function _syncDockChromeState() { + if (!dock._dockScreenName) + return; + if (!SettingsData.connectedFrameModeActive) { + ConnectedModeState.clearDockState(dock._dockScreenName); + return; + } + + ConnectedModeState.setDockState(dock._dockScreenName, { + "reveal": dock.visible && (dock.reveal || slideXAnimation.running || slideYAnimation.running) && dock.hasApps, + "barSide": dock.connectedBarSide, + "bodyX": dock._dockWindowOriginX() + dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x, + "bodyY": dock._dockWindowOriginY() + dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y, + "bodyW": dock.hasApps ? dockBackground.width : 0, + "bodyH": dock.hasApps ? dockBackground.height : 0, + "slideX": dockSlide.x, + "slideY": dockSlide.y + }); + } + + function _syncDockSlide() { + if (!dock._dockScreenName || !SettingsData.connectedFrameModeActive) + return; + ConnectedModeState.setDockSlide(dock._dockScreenName, dockSlide.x, dockSlide.y); + } + property bool contextMenuOpen: (dockVariants.contextMenu && dockVariants.contextMenu.visible && dockVariants.contextMenu.screen === modelData) property bool revealSticky: false @@ -349,6 +388,23 @@ Variants { } } + Component.onCompleted: Qt.callLater(() => dock._syncDockChromeState()) + Component.onDestruction: ConnectedModeState.clearDockState(dock._dockScreenName) + + onRevealChanged: dock._syncDockChromeState() + onWidthChanged: dock._syncDockChromeState() + onHeightChanged: dock._syncDockChromeState() + onVisibleChanged: dock._syncDockChromeState() + onHasAppsChanged: dock._syncDockChromeState() + onConnectedBarSideChanged: dock._syncDockChromeState() + + Connections { + target: SettingsData + function onConnectedFrameModeActiveChanged() { + dock._syncDockChromeState(); + } + } + Connections { target: SettingsData function onDockTransparencyChanged() { @@ -573,6 +629,10 @@ Variants { return 0; if (dock.reveal) return 0; + if (Theme.isConnectedEffect) { + const retractDist = dockBackground.width + SettingsData.dockSpacing + 10; + return SettingsData.dockPosition === SettingsData.Position.Right ? retractDist : -retractDist; + } const hideDistance = dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin + 10; if (SettingsData.dockPosition === SettingsData.Position.Right) { return hideDistance; @@ -585,6 +645,10 @@ Variants { return 0; if (dock.reveal) return 0; + if (Theme.isConnectedEffect) { + const retractDist = dockBackground.height + SettingsData.dockSpacing + 10; + return SettingsData.dockPosition === SettingsData.Position.Bottom ? retractDist : -retractDist; + } const hideDistance = dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin + 10; if (SettingsData.dockPosition === SettingsData.Position.Bottom) { return hideDistance; @@ -596,28 +660,29 @@ Variants { Behavior on x { NumberAnimation { id: slideXAnimation - duration: Theme.isConnectedEffect - ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) - : Theme.shortDuration + duration: Theme.isConnectedEffect ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration easing.type: Theme.isConnectedEffect ? Easing.BezierSpline : Easing.OutCubic - easing.bezierCurve: Theme.isConnectedEffect - ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) - : [] + easing.bezierCurve: Theme.isConnectedEffect ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : [] + onRunningChanged: if (!running) dock._syncDockChromeState() } } Behavior on y { NumberAnimation { id: slideYAnimation - duration: Theme.isConnectedEffect - ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) - : Theme.shortDuration + duration: Theme.isConnectedEffect ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration easing.type: Theme.isConnectedEffect ? Easing.BezierSpline : Easing.OutCubic - easing.bezierCurve: Theme.isConnectedEffect - ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) - : [] + easing.bezierCurve: Theme.isConnectedEffect ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : [] + onRunningChanged: if (!running) dock._syncDockChromeState() } } + + onXChanged: { + dock._syncDockSlide(); + } + onYChanged: { + dock._syncDockSlide(); + } } Item { @@ -647,6 +712,7 @@ Variants { Rectangle { anchors.fill: parent + visible: !SettingsData.connectedFrameModeActive color: dock.surfaceColor topLeftRadius: dock.surfaceTopLeftRadius topRightRadius: dock.surfaceTopRightRadius @@ -656,6 +722,7 @@ Variants { Rectangle { anchors.fill: parent + visible: !SettingsData.connectedFrameModeActive color: "transparent" topLeftRadius: dock.surfaceTopLeftRadius topRightRadius: dock.surfaceTopRightRadius @@ -665,26 +732,34 @@ Variants { border.width: dock.surfaceBorderWidth z: 100 } + + // Sync dockBackground geometry to ConnectedModeState + onXChanged: dock._syncDockChromeState() + onYChanged: dock._syncDockChromeState() + onWidthChanged: dock._syncDockChromeState() + onHeightChanged: dock._syncDockChromeState() } ConnectedCorner { - visible: Theme.isConnectedEffect && dock.reveal + visible: Theme.isConnectedEffect && dock.reveal && !SettingsData.connectedFrameModeActive barSide: dock.connectedBarSide placement: "left" spacing: 0 connectorRadius: Theme.connectedCornerRadius color: dock.surfaceColor + dpr: dock._dpr x: Theme.snap(dock.connectorX(dockBackground.x, dockBackground.width, placement, spacing), dock._dpr) y: Theme.snap(dock.connectorY(dockBackground.y, dockBackground.height, placement, spacing), dock._dpr) } ConnectedCorner { - visible: Theme.isConnectedEffect && dock.reveal + visible: Theme.isConnectedEffect && dock.reveal && !SettingsData.connectedFrameModeActive barSide: dock.connectedBarSide placement: "right" spacing: 0 connectorRadius: Theme.connectedCornerRadius color: dock.surfaceColor + dpr: dock._dpr x: Theme.snap(dock.connectorX(dockBackground.x, dockBackground.width, placement, spacing), dock._dpr) y: Theme.snap(dock.connectorY(dockBackground.y, dockBackground.height, placement, spacing), dock._dpr) } diff --git a/quickshell/Modules/Frame/FrameWindow.qml b/quickshell/Modules/Frame/FrameWindow.qml index ab710052..e70882b8 100644 --- a/quickshell/Modules/Frame/FrameWindow.qml +++ b/quickshell/Modules/Frame/FrameWindow.qml @@ -5,6 +5,7 @@ import Quickshell import Quickshell.Wayland import qs.Common import qs.Services +import qs.Widgets PanelWindow { id: win @@ -26,8 +27,6 @@ PanelWindow { } color: "transparent" - - // No input — pass everything through to apps and bar mask: Region {} readonly property var barEdges: { @@ -36,20 +35,50 @@ PanelWindow { } readonly property real _dpr: CompositorService.getScreenScale(win.screen) - readonly property bool _frameActive: SettingsData.frameEnabled - && SettingsData.isScreenInPreferences(win.screen, SettingsData.frameScreenPreferences) - readonly property int _windowRegionWidth: win._regionInt(win.width) + readonly property bool _frameActive: SettingsData.frameEnabled && SettingsData.isScreenInPreferences(win.screen, SettingsData.frameScreenPreferences) + readonly property int _windowRegionWidth: win._regionInt(win.width) readonly property int _windowRegionHeight: win._regionInt(win.height) + readonly property string _screenName: win.screen ? win.screen.name : "" + readonly property var _dockState: ConnectedModeState.dockStates[win._screenName] || ConnectedModeState.emptyDockState + readonly property var _dockSlide: ConnectedModeState.dockSlides[win._screenName] || ({ + "x": 0, + "y": 0 + }) + + // ─── Connected chrome convenience properties ────────────────────────────── + readonly property bool _connectedActive: win._frameActive && SettingsData.connectedFrameModeActive + readonly property string _barSide: { + const edges = win.barEdges; + if (edges.includes("top")) + return "top"; + if (edges.includes("bottom")) + return "bottom"; + if (edges.includes("left")) + return "left"; + return "right"; + } + readonly property real _ccr: Theme.connectedCornerRadius + readonly property real _effectivePopoutCcr: { + const extent = win._popoutArcExtent(); + const isHoriz = ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom"; + const crossSize = isHoriz ? _popoutBodyBlurAnchor.width : _popoutBodyBlurAnchor.height; + return Math.max(0, Math.min(win._ccr, extent, crossSize / 2)); + } + readonly property color _surfaceColor: Theme.connectedSurfaceColor + readonly property real _surfaceOpacity: _surfaceColor.a + readonly property color _opaqueSurfaceColor: Qt.rgba(_surfaceColor.r, _surfaceColor.g, _surfaceColor.b, 1) + readonly property real _surfaceRadius: Theme.connectedSurfaceRadius + readonly property real _seamOverlap: Theme.hairline(win._dpr) function _regionInt(value) { return Math.max(0, Math.round(Theme.px(value, win._dpr))); } - readonly property int cutoutTopInset: win._regionInt(barEdges.includes("top") ? SettingsData.frameBarSize : SettingsData.frameThickness) + readonly property int cutoutTopInset: win._regionInt(barEdges.includes("top") ? SettingsData.frameBarSize : SettingsData.frameThickness) readonly property int cutoutBottomInset: win._regionInt(barEdges.includes("bottom") ? SettingsData.frameBarSize : SettingsData.frameThickness) - readonly property int cutoutLeftInset: win._regionInt(barEdges.includes("left") ? SettingsData.frameBarSize : SettingsData.frameThickness) - readonly property int cutoutRightInset: win._regionInt(barEdges.includes("right") ? SettingsData.frameBarSize : SettingsData.frameThickness) - readonly property int cutoutWidth: Math.max(0, win._windowRegionWidth - win.cutoutLeftInset - win.cutoutRightInset) + readonly property int cutoutLeftInset: win._regionInt(barEdges.includes("left") ? SettingsData.frameBarSize : SettingsData.frameThickness) + readonly property int cutoutRightInset: win._regionInt(barEdges.includes("right") ? SettingsData.frameBarSize : SettingsData.frameThickness) + readonly property int cutoutWidth: Math.max(0, win._windowRegionWidth - win.cutoutLeftInset - win.cutoutRightInset) readonly property int cutoutHeight: Math.max(0, win._windowRegionHeight - win.cutoutTopInset - win.cutoutBottomInset) readonly property int cutoutRadius: { const requested = win._regionInt(SettingsData.frameRounding); @@ -57,7 +86,6 @@ PanelWindow { return Math.max(0, Math.min(requested, maxRadius)); } - // Slightly expand the subtractive blur cutout at very low opacity levels readonly property int _blurCutoutCompensation: SettingsData.frameOpacity <= 0.2 ? 1 : 0 readonly property int _blurCutoutLeft: Math.max(0, win.cutoutLeftInset - win._blurCutoutCompensation) readonly property int _blurCutoutTop: Math.max(0, win.cutoutTopInset - win._blurCutoutCompensation) @@ -69,7 +97,7 @@ PanelWindow { return Math.max(0, Math.min(requested, maxRadius)); } - // Must stay visible so Region.item can resolve scene coordinates. + // Invisible items providing scene coordinates for blur Region anchors Item { id: _blurCutout x: win._blurCutoutLeft @@ -78,48 +106,471 @@ PanelWindow { height: Math.max(0, win._blurCutoutBottom - win._blurCutoutTop) } - property var _frameBlurRegion: null + Item { + id: _popoutBodyBlurAnchor + visible: false + + readonly property bool _active: ConnectedModeState.popoutVisible && ConnectedModeState.popoutScreen === win._screenName + + readonly property real _dyClamp: (ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom") ? Math.max(-ConnectedModeState.popoutBodyH, Math.min(ConnectedModeState.popoutAnimY, ConnectedModeState.popoutBodyH)) : 0 + readonly property real _dxClamp: (ConnectedModeState.popoutBarSide === "left" || ConnectedModeState.popoutBarSide === "right") ? Math.max(-ConnectedModeState.popoutBodyW, Math.min(ConnectedModeState.popoutAnimX, ConnectedModeState.popoutBodyW)) : 0 + + x: _active ? ConnectedModeState.popoutBodyX + (ConnectedModeState.popoutBarSide === "right" ? _dxClamp : 0) : 0 + y: _active ? ConnectedModeState.popoutBodyY + (ConnectedModeState.popoutBarSide === "bottom" ? _dyClamp : 0) : 0 + width: _active ? Math.max(0, ConnectedModeState.popoutBodyW - Math.abs(_dxClamp)) : 0 + height: _active ? Math.max(0, ConnectedModeState.popoutBodyH - Math.abs(_dyClamp)) : 0 + } + + Item { + id: _dockBodyBlurAnchor + visible: false + + readonly property bool _active: win._dockState.reveal && win._dockState.bodyW > 0 && win._dockState.bodyH > 0 + + x: _active ? win._dockState.bodyX + (win._dockSlide.x || 0) : 0 + y: _active ? win._dockState.bodyY + (win._dockSlide.y || 0) : 0 + width: _active ? win._dockState.bodyW : 0 + height: _active ? win._dockState.bodyH : 0 + } + + Item { + id: _popoutBodyBlurCap + opacity: 0 + + readonly property string _side: ConnectedModeState.popoutBarSide + readonly property real _capThickness: win._popoutBlurCapThickness() + readonly property bool _active: _popoutBodyBlurAnchor._active && _capThickness > 0 && _popoutBodyBlurAnchor.width > 0 && _popoutBodyBlurAnchor.height > 0 + readonly property real _capWidth: (_side === "left" || _side === "right") ? Math.min(_capThickness, _popoutBodyBlurAnchor.width) : _popoutBodyBlurAnchor.width + readonly property real _capHeight: (_side === "top" || _side === "bottom") ? Math.min(_capThickness, _popoutBodyBlurAnchor.height) : _popoutBodyBlurAnchor.height + + x: !_active ? 0 : (_side === "right" ? _popoutBodyBlurAnchor.x + _popoutBodyBlurAnchor.width - _capWidth : _popoutBodyBlurAnchor.x) + y: !_active ? 0 : (_side === "bottom" ? _popoutBodyBlurAnchor.y + _popoutBodyBlurAnchor.height - _capHeight : _popoutBodyBlurAnchor.y) + width: _active ? _capWidth : 0 + height: _active ? _capHeight : 0 + } + + Item { + id: _dockBodyBlurCap + opacity: 0 + + readonly property string _side: win._dockState.barSide + readonly property bool _active: _dockBodyBlurAnchor._active && _dockBodyBlurAnchor.width > 0 && _dockBodyBlurAnchor.height > 0 + readonly property real _capWidth: (_side === "left" || _side === "right") ? Math.min(win._dockConnectorRadius(), _dockBodyBlurAnchor.width) : _dockBodyBlurAnchor.width + readonly property real _capHeight: (_side === "top" || _side === "bottom") ? Math.min(win._dockConnectorRadius(), _dockBodyBlurAnchor.height) : _dockBodyBlurAnchor.height + + x: !_active ? 0 : (_side === "right" ? _dockBodyBlurAnchor.x + _dockBodyBlurAnchor.width - _capWidth : _dockBodyBlurAnchor.x) + y: !_active ? 0 : (_side === "bottom" ? _dockBodyBlurAnchor.y + _dockBodyBlurAnchor.height - _capHeight : _dockBodyBlurAnchor.y) + width: _active ? _capWidth : 0 + height: _active ? _capHeight : 0 + } + + Item { + id: _dockLeftConnectorBlurAnchor + opacity: 0 + + readonly property bool _active: _dockBodyBlurAnchor._active && win._dockConnectorRadius() > 0 + readonly property real _w: win._dockConnectorWidth(0) + readonly property real _h: win._dockConnectorHeight(0) + + x: _active ? Theme.snap(win._dockConnectorX(_dockBodyBlurAnchor.x, _dockBodyBlurAnchor.width, "left", 0), win._dpr) : 0 + y: _active ? Theme.snap(win._dockConnectorY(_dockBodyBlurAnchor.y, _dockBodyBlurAnchor.height, "left", 0), win._dpr) : 0 + width: _active ? _w : 0 + height: _active ? _h : 0 + } + + Item { + id: _dockRightConnectorBlurAnchor + opacity: 0 + + readonly property bool _active: _dockBodyBlurAnchor._active && win._dockConnectorRadius() > 0 + readonly property real _w: win._dockConnectorWidth(0) + readonly property real _h: win._dockConnectorHeight(0) + + x: _active ? Theme.snap(win._dockConnectorX(_dockBodyBlurAnchor.x, _dockBodyBlurAnchor.width, "right", 0), win._dpr) : 0 + y: _active ? Theme.snap(win._dockConnectorY(_dockBodyBlurAnchor.y, _dockBodyBlurAnchor.height, "right", 0), win._dpr) : 0 + width: _active ? _w : 0 + height: _active ? _h : 0 + } + + Item { + id: _dockLeftConnectorCutout + opacity: 0 + + readonly property bool _active: _dockLeftConnectorBlurAnchor.width > 0 && _dockLeftConnectorBlurAnchor.height > 0 + readonly property string _arcCorner: win._connectorArcCorner(win._dockState.barSide, "left") + + x: _active ? win._connectorCutoutX(_dockLeftConnectorBlurAnchor.x, _dockLeftConnectorBlurAnchor.width, _arcCorner, win._dockConnectorRadius()) : 0 + y: _active ? win._connectorCutoutY(_dockLeftConnectorBlurAnchor.y, _dockLeftConnectorBlurAnchor.height, _arcCorner, win._dockConnectorRadius()) : 0 + width: _active ? win._dockConnectorRadius() * 2 : 0 + height: _active ? win._dockConnectorRadius() * 2 : 0 + } + + Item { + id: _dockRightConnectorCutout + opacity: 0 + + readonly property bool _active: _dockRightConnectorBlurAnchor.width > 0 && _dockRightConnectorBlurAnchor.height > 0 + readonly property string _arcCorner: win._connectorArcCorner(win._dockState.barSide, "right") + + x: _active ? win._connectorCutoutX(_dockRightConnectorBlurAnchor.x, _dockRightConnectorBlurAnchor.width, _arcCorner, win._dockConnectorRadius()) : 0 + y: _active ? win._connectorCutoutY(_dockRightConnectorBlurAnchor.y, _dockRightConnectorBlurAnchor.height, _arcCorner, win._dockConnectorRadius()) : 0 + width: _active ? win._dockConnectorRadius() * 2 : 0 + height: _active ? win._dockConnectorRadius() * 2 : 0 + } + + Item { + id: _popoutLeftConnectorBlurAnchor + opacity: 0 + + readonly property bool _active: win._popoutArcVisible() + readonly property real _w: win._popoutConnectorWidth(0) + readonly property real _h: win._popoutConnectorHeight(0) + + x: _active ? Theme.snap(win._popoutConnectorX(ConnectedModeState.popoutBodyX, ConnectedModeState.popoutBodyW, "left", 0), win._dpr) : 0 + y: _active ? Theme.snap(win._popoutConnectorY(ConnectedModeState.popoutBodyY, ConnectedModeState.popoutBodyH, "left", 0), win._dpr) : 0 + width: _active ? _w : 0 + height: _active ? _h : 0 + } + + Item { + id: _popoutRightConnectorBlurAnchor + opacity: 0 + + readonly property bool _active: win._popoutArcVisible() + readonly property real _w: win._popoutConnectorWidth(0) + readonly property real _h: win._popoutConnectorHeight(0) + + x: _active ? Theme.snap(win._popoutConnectorX(ConnectedModeState.popoutBodyX, ConnectedModeState.popoutBodyW, "right", 0), win._dpr) : 0 + y: _active ? Theme.snap(win._popoutConnectorY(ConnectedModeState.popoutBodyY, ConnectedModeState.popoutBodyH, "right", 0), win._dpr) : 0 + width: _active ? _w : 0 + height: _active ? _h : 0 + } + + Item { + id: _popoutLeftConnectorCutout + opacity: 0 + + readonly property bool _active: _popoutLeftConnectorBlurAnchor.width > 0 && _popoutLeftConnectorBlurAnchor.height > 0 + readonly property string _arcCorner: win._connectorArcCorner(ConnectedModeState.popoutBarSide, "left") + + x: _active ? win._connectorCutoutX(_popoutLeftConnectorBlurAnchor.x, _popoutLeftConnectorBlurAnchor.width, _arcCorner) : 0 + y: _active ? win._connectorCutoutY(_popoutLeftConnectorBlurAnchor.y, _popoutLeftConnectorBlurAnchor.height, _arcCorner) : 0 + width: _active ? win._effectivePopoutCcr * 2 : 0 + height: _active ? win._effectivePopoutCcr * 2 : 0 + } + + Item { + id: _popoutRightConnectorCutout + opacity: 0 + + readonly property bool _active: _popoutRightConnectorBlurAnchor.width > 0 && _popoutRightConnectorBlurAnchor.height > 0 + readonly property string _arcCorner: win._connectorArcCorner(ConnectedModeState.popoutBarSide, "right") + + x: _active ? win._connectorCutoutX(_popoutRightConnectorBlurAnchor.x, _popoutRightConnectorBlurAnchor.width, _arcCorner) : 0 + y: _active ? win._connectorCutoutY(_popoutRightConnectorBlurAnchor.y, _popoutRightConnectorBlurAnchor.height, _arcCorner) : 0 + width: _active ? win._effectivePopoutCcr * 2 : 0 + height: _active ? win._effectivePopoutCcr * 2 : 0 + } + + Region { + id: _staticBlurRegion + x: 0 + y: 0 + width: win._windowRegionWidth + height: win._windowRegionHeight + + // Frame cutout (always active when frame is on) + Region { + item: _blurCutout + intersection: Intersection.Subtract + radius: win._blurCutoutRadius + } + + // ── Connected popout blur regions ── + Region { + item: _popoutBodyBlurAnchor + readonly property string _bs: ConnectedModeState.popoutBarSide + topLeftRadius: (_bs === "top" || _bs === "left") ? win._effectivePopoutCcr : win._surfaceRadius + topRightRadius: (_bs === "top" || _bs === "right") ? win._effectivePopoutCcr : win._surfaceRadius + bottomLeftRadius: (_bs === "bottom" || _bs === "left") ? win._effectivePopoutCcr : win._surfaceRadius + bottomRightRadius: (_bs === "bottom" || _bs === "right") ? win._effectivePopoutCcr : win._surfaceRadius + } + Region { + item: _popoutBodyBlurCap + } + Region { + item: _popoutLeftConnectorBlurAnchor + Region { + item: _popoutLeftConnectorCutout + intersection: Intersection.Subtract + radius: win._effectivePopoutCcr + } + } + Region { + item: _popoutRightConnectorBlurAnchor + Region { + item: _popoutRightConnectorCutout + intersection: Intersection.Subtract + radius: win._effectivePopoutCcr + } + } + + // ── Connected dock blur regions ── + Region { + item: _dockBodyBlurAnchor + radius: win._dockBodyBlurRadius() + } + Region { + item: _dockBodyBlurCap + } + Region { + item: _dockLeftConnectorBlurAnchor + radius: win._dockConnectorRadius() + Region { + item: _dockLeftConnectorCutout + intersection: Intersection.Subtract + radius: win._dockConnectorRadius() + } + } + Region { + item: _dockRightConnectorBlurAnchor + radius: win._dockConnectorRadius() + Region { + item: _dockRightConnectorCutout + intersection: Intersection.Subtract + radius: win._dockConnectorRadius() + } + } + } + + // ─── Connector position helpers (mirror DankPopout / Dock logic) ────────── + + function _popoutConnectorWidth(spacing) { + const barSide = ConnectedModeState.popoutBarSide; + return (barSide === "top" || barSide === "bottom") ? win._effectivePopoutCcr : (spacing + win._effectivePopoutCcr); + } + + function _popoutConnectorHeight(spacing) { + const barSide = ConnectedModeState.popoutBarSide; + return (barSide === "top" || barSide === "bottom") ? (spacing + win._effectivePopoutCcr) : win._effectivePopoutCcr; + } + + function _popoutConnectorX(baseX, bodyWidth, placement, spacing) { + const barSide = ConnectedModeState.popoutBarSide; + const seamX = (barSide === "top" || barSide === "bottom") ? (placement === "left" ? baseX : baseX + bodyWidth) : (barSide === "left" ? baseX : baseX + bodyWidth); + const w = _popoutConnectorWidth(spacing); + if (barSide === "top" || barSide === "bottom") + return placement === "left" ? seamX - w : seamX; + return barSide === "left" ? seamX : seamX - w; + } + + function _popoutConnectorY(baseY, bodyHeight, placement, spacing) { + const barSide = ConnectedModeState.popoutBarSide; + const seamY = barSide === "top" ? baseY : barSide === "bottom" ? baseY + bodyHeight : (placement === "left" ? baseY : baseY + bodyHeight); + const h = _popoutConnectorHeight(spacing); + if (barSide === "top") + return seamY; + if (barSide === "bottom") + return seamY - h; + return placement === "left" ? seamY - h : seamY; + } + + function _dockBodyBlurRadius() { + return _dockBodyBlurAnchor._active ? Math.max(0, Math.min(win._surfaceRadius, _dockBodyBlurAnchor.width / 2, _dockBodyBlurAnchor.height / 2)) : win._surfaceRadius; + } + + function _dockConnectorRadius() { + if (!_dockBodyBlurAnchor._active) + return win._ccr; + const dockSide = win._dockState.barSide; + const thickness = (dockSide === "left" || dockSide === "right") ? _dockBodyBlurAnchor.width : _dockBodyBlurAnchor.height; + const bodyRadius = win._dockBodyBlurRadius(); + const maxConnectorRadius = Math.max(0, thickness - bodyRadius - win._seamOverlap); + return Math.max(0, Math.min(win._ccr, bodyRadius, maxConnectorRadius)); + } + + function _dockConnectorWidth(spacing) { + const isVert = win._dockState.barSide === "left" || win._dockState.barSide === "right"; + const radius = win._dockConnectorRadius(); + return isVert ? (spacing + radius) : radius; + } + + function _dockConnectorHeight(spacing) { + const isVert = win._dockState.barSide === "left" || win._dockState.barSide === "right"; + const radius = win._dockConnectorRadius(); + return isVert ? radius : (spacing + radius); + } + + function _dockConnectorX(baseX, bodyWidth, placement, spacing) { + const dockSide = win._dockState.barSide; + const isVert = dockSide === "left" || dockSide === "right"; + const seamX = !isVert ? (placement === "left" ? baseX : baseX + bodyWidth) : (dockSide === "left" ? baseX : baseX + bodyWidth); + const w = _dockConnectorWidth(spacing); + if (!isVert) + return placement === "left" ? seamX - w : seamX; + return dockSide === "left" ? seamX : seamX - w; + } + + function _dockConnectorY(baseY, bodyHeight, placement, spacing) { + const dockSide = win._dockState.barSide; + const seamY = dockSide === "top" ? baseY : dockSide === "bottom" ? baseY + bodyHeight : (placement === "left" ? baseY : baseY + bodyHeight); + const h = _dockConnectorHeight(spacing); + if (dockSide === "top") + return seamY; + if (dockSide === "bottom") + return seamY - h; + return placement === "left" ? seamY - h : seamY; + } + + function _popoutFillOverlapX() { + return (ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom") ? win._seamOverlap : 0; + } + + function _popoutFillOverlapY() { + return (ConnectedModeState.popoutBarSide === "left" || ConnectedModeState.popoutBarSide === "right") ? win._seamOverlap : 0; + } + + function _dockFillOverlapX() { + return (win._dockState.barSide === "top" || win._dockState.barSide === "bottom") ? win._seamOverlap : 0; + } + + function _dockFillOverlapY() { + return (win._dockState.barSide === "left" || win._dockState.barSide === "right") ? win._seamOverlap : 0; + } + + function _popoutArcExtent() { + return (ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom") ? _popoutBodyBlurAnchor.height : _popoutBodyBlurAnchor.width; + } + + function _popoutArcVisible() { + if (!_popoutBodyBlurAnchor._active || _popoutBodyBlurAnchor.width <= 0 || _popoutBodyBlurAnchor.height <= 0) + return false; + return win._popoutArcExtent() >= win._ccr * (1 + win._ccr * 0.02); + } + + function _popoutBlurCapThickness() { + const extent = win._popoutArcExtent(); + return Math.max(0, Math.min(win._effectivePopoutCcr, extent - win._surfaceRadius)); + } + + function _popoutChromeX() { + const barSide = ConnectedModeState.popoutBarSide; + return ConnectedModeState.popoutBodyX - ((barSide === "top" || barSide === "bottom") ? win._effectivePopoutCcr : 0); + } + + function _popoutChromeY() { + const barSide = ConnectedModeState.popoutBarSide; + return ConnectedModeState.popoutBodyY - ((barSide === "left" || barSide === "right") ? win._effectivePopoutCcr : 0); + } + + function _popoutChromeWidth() { + const barSide = ConnectedModeState.popoutBarSide; + return ConnectedModeState.popoutBodyW + ((barSide === "top" || barSide === "bottom") ? win._effectivePopoutCcr * 2 : 0); + } + + function _popoutChromeHeight() { + const barSide = ConnectedModeState.popoutBarSide; + return ConnectedModeState.popoutBodyH + ((barSide === "left" || barSide === "right") ? win._effectivePopoutCcr * 2 : 0); + } + + function _popoutClipX() { + return _popoutBodyBlurAnchor.x - win._popoutChromeX() - win._popoutFillOverlapX(); + } + + function _popoutClipY() { + return _popoutBodyBlurAnchor.y - win._popoutChromeY() - win._popoutFillOverlapY(); + } + + function _popoutClipWidth() { + return _popoutBodyBlurAnchor.width + win._popoutFillOverlapX() * 2; + } + + function _popoutClipHeight() { + return _popoutBodyBlurAnchor.height + win._popoutFillOverlapY() * 2; + } + + function _popoutBodyXInClip() { + return (ConnectedModeState.popoutBarSide === "left" ? _popoutBodyBlurAnchor._dxClamp : 0) - win._popoutFillOverlapX(); + } + + function _popoutBodyYInClip() { + return (ConnectedModeState.popoutBarSide === "top" ? _popoutBodyBlurAnchor._dyClamp : 0) - win._popoutFillOverlapY(); + } + + function _popoutBodyFullWidth() { + return ConnectedModeState.popoutBodyW + win._popoutFillOverlapX() * 2; + } + + function _popoutBodyFullHeight() { + return ConnectedModeState.popoutBodyH + win._popoutFillOverlapY() * 2; + } + + function _dockChromeX() { + const dockSide = win._dockState.barSide; + return _dockBodyBlurAnchor.x - ((dockSide === "top" || dockSide === "bottom") ? win._dockConnectorRadius() : 0); + } + + function _dockChromeY() { + const dockSide = win._dockState.barSide; + return _dockBodyBlurAnchor.y - ((dockSide === "left" || dockSide === "right") ? win._dockConnectorRadius() : 0); + } + + function _dockChromeWidth() { + const dockSide = win._dockState.barSide; + return _dockBodyBlurAnchor.width + ((dockSide === "top" || dockSide === "bottom") ? win._dockConnectorRadius() * 2 : 0); + } + + function _dockChromeHeight() { + const dockSide = win._dockState.barSide; + return _dockBodyBlurAnchor.height + ((dockSide === "left" || dockSide === "right") ? win._dockConnectorRadius() * 2 : 0); + } + + function _dockBodyXInChrome() { + return ((win._dockState.barSide === "top" || win._dockState.barSide === "bottom") ? win._dockConnectorRadius() : 0) - win._dockFillOverlapX(); + } + + function _dockBodyYInChrome() { + return ((win._dockState.barSide === "left" || win._dockState.barSide === "right") ? win._dockConnectorRadius() : 0) - win._dockFillOverlapY(); + } + + function _connectorArcCorner(barSide, placement) { + if (barSide === "top") + return placement === "left" ? "bottomLeft" : "bottomRight"; + if (barSide === "bottom") + return placement === "left" ? "topLeft" : "topRight"; + if (barSide === "left") + return placement === "left" ? "topRight" : "bottomRight"; + return placement === "left" ? "topLeft" : "bottomLeft"; + } + + function _connectorCutoutX(connectorX, connectorWidth, arcCorner, radius) { + const r = radius === undefined ? win._effectivePopoutCcr : radius; + return (arcCorner === "topLeft" || arcCorner === "bottomLeft") ? connectorX - r : connectorX + connectorWidth - r; + } + + function _connectorCutoutY(connectorY, connectorHeight, arcCorner, radius) { + const r = radius === undefined ? win._effectivePopoutCcr : radius; + return (arcCorner === "topLeft" || arcCorner === "topRight") ? connectorY - r : connectorY + connectorHeight - r; + } + + // ─── Blur build / teardown ──────────────────────────────────────────────── function _buildBlur() { - _teardownBlur(); - // Follow the global blur toggle - if (!BlurService.enabled || !SettingsData.frameBlurEnabled || !win._frameActive || !win.visible) - return; try { - const region = Qt.createQmlObject( - 'import QtQuick; import Quickshell; Region {' + - ' property Item cutoutItem;' + - ' property int cutoutRadius: 0;' + - ' Region {' + - ' item: cutoutItem;' + - ' intersection: Intersection.Subtract;' + - ' radius: cutoutRadius;' + - ' }' + - '}', - win, "FrameBlurRegion"); - - region.x = Qt.binding(() => 0); - region.y = Qt.binding(() => 0); - region.width = Qt.binding(() => win._windowRegionWidth); - region.height = Qt.binding(() => win._windowRegionHeight); - region.cutoutItem = _blurCutout; - region.cutoutRadius = Qt.binding(() => win._blurCutoutRadius); - - win.BackgroundEffect.blurRegion = region; - win._frameBlurRegion = region; + if (!BlurService.enabled || !SettingsData.frameBlurEnabled || !win._frameActive || !win.visible) { + win.BackgroundEffect.blurRegion = null; + return; + } + win.BackgroundEffect.blurRegion = _staticBlurRegion; } catch (e) { - console.warn("FrameWindow: Failed to create blur region:", e); + console.warn("FrameWindow: Failed to set blur region:", e); } } function _teardownBlur() { - if (!win._frameBlurRegion) - return; try { win.BackgroundEffect.blurRegion = null; } catch (e) {} - win._frameBlurRegion.destroy(); - win._frameBlurRegion = null; } Timer { @@ -130,24 +581,44 @@ PanelWindow { Connections { target: SettingsData - function onFrameBlurEnabledChanged() { _blurRebuildTimer.restart(); } - function onFrameEnabledChanged() { _blurRebuildTimer.restart(); } - function onFrameThicknessChanged() { _blurRebuildTimer.restart(); } - function onFrameBarSizeChanged() { _blurRebuildTimer.restart(); } - function onFrameOpacityChanged() { _blurRebuildTimer.restart(); } - function onFrameRoundingChanged() { _blurRebuildTimer.restart(); } - function onFrameScreenPreferencesChanged() { _blurRebuildTimer.restart(); } - function onBarConfigsChanged() { _blurRebuildTimer.restart(); } + function onFrameBlurEnabledChanged() { + _blurRebuildTimer.restart(); + } + function onFrameEnabledChanged() { + _blurRebuildTimer.restart(); + } + function onFrameThicknessChanged() { + _blurRebuildTimer.restart(); + } + function onFrameBarSizeChanged() { + _blurRebuildTimer.restart(); + } + function onFrameOpacityChanged() { + _blurRebuildTimer.restart(); + } + function onFrameRoundingChanged() { + _blurRebuildTimer.restart(); + } + function onFrameScreenPreferencesChanged() { + _blurRebuildTimer.restart(); + } + function onBarConfigsChanged() { + _blurRebuildTimer.restart(); + } + function onConnectedFrameModeActiveChanged() { + _blurRebuildTimer.restart(); + } } Connections { target: BlurService - function onEnabledChanged() { _blurRebuildTimer.restart(); } + function onEnabledChanged() { + _blurRebuildTimer.restart(); + } } onVisibleChanged: { if (visible) { - win._frameBlurRegion = null; _blurRebuildTimer.restart(); } else { _teardownBlur(); @@ -157,6 +628,8 @@ PanelWindow { Component.onCompleted: Qt.callLater(() => win._buildBlur()) Component.onDestruction: win._teardownBlur() + // ─── Frame border ───────────────────────────────────────────────────────── + FrameBorder { anchors.fill: parent visible: win._frameActive @@ -166,4 +639,132 @@ PanelWindow { cutoutRightInset: win.cutoutRightInset cutoutRadius: win.cutoutRadius } + + // ─── Connected chrome fills ─────────────────────────────────────────────── + + Item { + id: _connectedChrome + anchors.fill: parent + visible: win._connectedActive + + Item { + id: _popoutChrome + visible: ConnectedModeState.popoutVisible && ConnectedModeState.popoutScreen === win._screenName + x: win._popoutChromeX() + y: win._popoutChromeY() + width: win._popoutChromeWidth() + height: win._popoutChromeHeight() + opacity: win._surfaceOpacity + layer.enabled: opacity < 1 + layer.smooth: false + + Item { + id: _popoutClip + x: win._popoutClipX() + y: win._popoutClipY() + width: win._popoutClipWidth() + height: win._popoutClipHeight() + clip: true + + Rectangle { + id: _popoutFill + x: win._popoutBodyXInClip() + y: win._popoutBodyYInClip() + width: win._popoutBodyFullWidth() + height: win._popoutBodyFullHeight() + color: win._opaqueSurfaceColor + z: 1 + topLeftRadius: (ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "left") ? 0 : win._surfaceRadius + topRightRadius: (ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "right") ? 0 : win._surfaceRadius + bottomLeftRadius: (ConnectedModeState.popoutBarSide === "bottom" || ConnectedModeState.popoutBarSide === "left") ? 0 : win._surfaceRadius + bottomRightRadius: (ConnectedModeState.popoutBarSide === "bottom" || ConnectedModeState.popoutBarSide === "right") ? 0 : win._surfaceRadius + } + } + + ConnectedCorner { + id: _connPopoutLeft + visible: win._popoutArcVisible() + barSide: ConnectedModeState.popoutBarSide + placement: "left" + spacing: 0 + connectorRadius: win._effectivePopoutCcr + color: win._opaqueSurfaceColor + edgeStrokeWidth: win._seamOverlap + edgeStrokeColor: win._opaqueSurfaceColor + dpr: win._dpr + x: Theme.snap(win._popoutConnectorX(ConnectedModeState.popoutBodyX, ConnectedModeState.popoutBodyW, "left", 0) - _popoutChrome.x, win._dpr) + y: Theme.snap(win._popoutConnectorY(ConnectedModeState.popoutBodyY, ConnectedModeState.popoutBodyH, "left", 0) - _popoutChrome.y, win._dpr) + } + + ConnectedCorner { + id: _connPopoutRight + visible: win._popoutArcVisible() + barSide: ConnectedModeState.popoutBarSide + placement: "right" + spacing: 0 + connectorRadius: win._effectivePopoutCcr + color: win._opaqueSurfaceColor + edgeStrokeWidth: win._seamOverlap + edgeStrokeColor: win._opaqueSurfaceColor + dpr: win._dpr + x: Theme.snap(win._popoutConnectorX(ConnectedModeState.popoutBodyX, ConnectedModeState.popoutBodyW, "right", 0) - _popoutChrome.x, win._dpr) + y: Theme.snap(win._popoutConnectorY(ConnectedModeState.popoutBodyY, ConnectedModeState.popoutBodyH, "right", 0) - _popoutChrome.y, win._dpr) + } + } + + Item { + id: _dockChrome + visible: _dockBodyBlurAnchor._active + x: win._dockChromeX() + y: win._dockChromeY() + width: win._dockChromeWidth() + height: win._dockChromeHeight() + opacity: win._surfaceOpacity + layer.enabled: opacity < 1 + layer.smooth: false + + Rectangle { + id: _dockFill + x: win._dockBodyXInChrome() + y: win._dockBodyYInChrome() + width: _dockBodyBlurAnchor.width + win._dockFillOverlapX() * 2 + height: _dockBodyBlurAnchor.height + win._dockFillOverlapY() * 2 + color: win._opaqueSurfaceColor + z: 1 + + readonly property string _dockSide: win._dockState.barSide + readonly property real _dockRadius: win._dockBodyBlurRadius() + topLeftRadius: (_dockSide === "top" || _dockSide === "left") ? 0 : _dockRadius + topRightRadius: (_dockSide === "top" || _dockSide === "right") ? 0 : _dockRadius + bottomLeftRadius: (_dockSide === "bottom" || _dockSide === "left") ? 0 : _dockRadius + bottomRightRadius: (_dockSide === "bottom" || _dockSide === "right") ? 0 : _dockRadius + } + + ConnectedCorner { + id: _connDockLeft + visible: _dockBodyBlurAnchor._active + barSide: win._dockState.barSide + placement: "left" + spacing: 0 + connectorRadius: win._dockConnectorRadius() + color: win._opaqueSurfaceColor + dpr: win._dpr + x: Theme.snap(win._dockConnectorX(_dockBodyBlurAnchor.x, _dockBodyBlurAnchor.width, "left", 0) - _dockChrome.x, win._dpr) + y: Theme.snap(win._dockConnectorY(_dockBodyBlurAnchor.y, _dockBodyBlurAnchor.height, "left", 0) - _dockChrome.y, win._dpr) + } + + ConnectedCorner { + id: _connDockRight + visible: _dockBodyBlurAnchor._active + barSide: win._dockState.barSide + placement: "right" + spacing: 0 + connectorRadius: win._dockConnectorRadius() + color: win._opaqueSurfaceColor + dpr: win._dpr + x: Theme.snap(win._dockConnectorX(_dockBodyBlurAnchor.x, _dockBodyBlurAnchor.width, "right", 0) - _dockChrome.x, win._dpr) + y: Theme.snap(win._dockConnectorY(_dockBodyBlurAnchor.y, _dockBodyBlurAnchor.height, "right", 0) - _dockChrome.y, win._dpr) + } + } + } } diff --git a/quickshell/Modules/Settings/FrameTab.qml b/quickshell/Modules/Settings/FrameTab.qml index 95066a8b..6d4644e1 100644 --- a/quickshell/Modules/Settings/FrameTab.qml +++ b/quickshell/Modules/Settings/FrameTab.qml @@ -56,6 +56,9 @@ Item { settingKey: "frameRounding" tags: ["frame", "border", "rounding", "radius", "corner"] text: I18n.tr("Border Radius") + description: SettingsData.connectedFrameModeActive + ? I18n.tr("Controls the radius of the frame and all connected popout, dock, and modal surfaces while Connected Mode is active") + : I18n.tr("Controls the frame border radius. This also becomes the connected surface radius whenever Connected Mode is active") unit: "px" minimum: 0 maximum: 100 diff --git a/quickshell/Modules/Settings/ThemeColorsTab.qml b/quickshell/Modules/Settings/ThemeColorsTab.qml index 1aac93bf..dee51e0d 100644 --- a/quickshell/Modules/Settings/ThemeColorsTab.qml +++ b/quickshell/Modules/Settings/ThemeColorsTab.qml @@ -11,9 +11,7 @@ import qs.Modules.Settings.Widgets Item { id: themeColorsTab - readonly property bool connectedFrameModeActive: SettingsData.frameEnabled - && SettingsData.motionEffect === 1 - && SettingsData.directionalAnimationMode === 3 + readonly property bool connectedFrameModeActive: SettingsData.connectedFrameModeActive property var cachedIconThemes: SettingsData.availableIconThemes property var cachedCursorThemes: SettingsData.availableCursorThemes property var cachedMatugenSchemes: Theme.availableMatugenSchemes.map(option => option.label) @@ -1639,7 +1637,9 @@ Item { tags: ["corner", "radius", "rounded", "square"] settingKey: "cornerRadius" text: I18n.tr("Corner Radius") - description: I18n.tr("0 = square corners") + description: themeColorsTab.connectedFrameModeActive + ? I18n.tr("Controls general UI rounding. Connected frame popouts, docks, and modal surfaces follow Border Radius in the Frame tab while Connected Frame mode is active") + : I18n.tr("0 = square corners") value: SettingsData.cornerRadius minimum: 0 maximum: 32 diff --git a/quickshell/Widgets/ConnectedCorner.qml b/quickshell/Widgets/ConnectedCorner.qml index 558c8a67..020d9522 100644 --- a/quickshell/Widgets/ConnectedCorner.qml +++ b/quickshell/Widgets/ConnectedCorner.qml @@ -2,17 +2,7 @@ import QtQuick import QtQuick.Shapes import qs.Common -// ConnectedCorner — Seam-complement connector that fills the void between -// a bar's rounded corner and a popout's flush edge, creating a seamless junction. -// -// Usage: Place as a sibling to contentWrapper inside unrollCounteract (DankPopout) -// or as a sibling to dockBackground (Dock). Position using contentWrapper.x/y. -// -// barSide: "top" | "bottom" | "left" | "right" — which edge the bar is on -// placement: "left" | "right" — which lateral end of that edge -// spacing: gap between bar surface and popout surface (storedBarSpacing, ~4px) -// connectorRadius: bar corner radius to match (frameRounding or Theme.cornerRadius) -// color: fill color matching the popout surface +// Concave arc connector filling the gap between a bar corner and an adjacent surface. Item { id: root @@ -22,9 +12,13 @@ Item { property real spacing: 4 property real connectorRadius: 12 property color color: "transparent" + property real edgeStrokeWidth: 0 + property color edgeStrokeColor: color + property real dpr: 1 readonly property bool isHorizontalBar: barSide === "top" || barSide === "bottom" readonly property bool isPlacementLeft: placement === "left" + readonly property real _edgeStrokeWidth: Math.max(0, edgeStrokeWidth) readonly property string arcCorner: { if (barSide === "top") return isPlacementLeft ? "bottomLeft" : "bottomRight"; @@ -113,37 +107,46 @@ Item { } } - // Horizontal bar: connector is tall (bridges vertical gap), narrow (corner radius wide) - // Vertical bar: connector is wide (bridges horizontal gap), short (corner radius tall) width: isHorizontalBar ? connectorRadius : (spacing + connectorRadius) height: isHorizontalBar ? (spacing + connectorRadius) : connectorRadius Shape { - anchors.fill: parent + x: -root._edgeStrokeWidth + y: -root._edgeStrokeWidth + width: root.width + root._edgeStrokeWidth * 2 + height: root.height + root._edgeStrokeWidth * 2 + asynchronous: false + antialiasing: true preferredRendererType: Shape.CurveRenderer + layer.enabled: true + layer.smooth: true + layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0) ShapePath { fillColor: root.color - strokeColor: "transparent" - strokeWidth: 0 - startX: root.pathStartX - startY: root.pathStartY + strokeColor: root._edgeStrokeWidth > 0 ? root.edgeStrokeColor : "transparent" + strokeWidth: root._edgeStrokeWidth * 2 + joinStyle: ShapePath.RoundJoin + capStyle: ShapePath.RoundCap + fillRule: ShapePath.WindingFill + startX: root.pathStartX + root._edgeStrokeWidth + startY: root.pathStartY + root._edgeStrokeWidth PathLine { - x: root.firstLineX - y: root.firstLineY + x: root.firstLineX + root._edgeStrokeWidth + y: root.firstLineY + root._edgeStrokeWidth } PathLine { - x: root.secondLineX - y: root.secondLineY + x: root.secondLineX + root._edgeStrokeWidth + y: root.secondLineY + root._edgeStrokeWidth } PathAngleArc { - centerX: root.arcCenterX - centerY: root.arcCenterY - radiusX: root.width - radiusY: root.height + centerX: root.arcCenterX + root._edgeStrokeWidth + centerY: root.arcCenterY + root._edgeStrokeWidth + radiusX: root.connectorRadius + radiusY: root.connectorRadius startAngle: root.arcStartAngle sweepAngle: root.arcSweepAngle } diff --git a/quickshell/Widgets/DankPopout.qml b/quickshell/Widgets/DankPopout.qml index 494a5c2b..518c1448 100644 --- a/quickshell/Widgets/DankPopout.qml +++ b/quickshell/Widgets/DankPopout.qml @@ -34,6 +34,8 @@ Item { property bool _resizeActive: false property real _surfaceMarginLeft: 0 property real _surfaceW: 0 + property string _connectedChromeToken: "" + property int _connectedChromeSerial: 0 property real storedBarThickness: Theme.barHeight - 4 property real storedBarSpacing: 4 @@ -151,6 +153,98 @@ Item { setBarContext(pos, bottomGap); } + function _nextConnectedChromeToken() { + _connectedChromeSerial += 1; + return layerNamespace + ":" + _connectedChromeSerial + ":" + (new Date()).getTime(); + } + + function _connectedChromeState(visibleOverride) { + const visible = visibleOverride !== undefined ? !!visibleOverride : contentWindow.visible; + return { + "visible": visible, + "barSide": contentContainer.connectedBarSide, + "bodyX": root.alignedX, + "bodyY": root.alignedY, + "bodyW": root.alignedWidth, + "bodyH": root.alignedHeight, + "animX": contentContainer.animX, + "animY": contentContainer.animY, + "screen": root.screen ? root.screen.name : "" + }; + } + + function _publishConnectedChromeState(forceClaim, visibleOverride) { + if (!SettingsData.connectedFrameModeActive || !root.screen || !_connectedChromeToken) + return; + + const state = _connectedChromeState(visibleOverride); + if (forceClaim || !ConnectedModeState.hasPopoutOwner(_connectedChromeToken)) { + ConnectedModeState.claimPopout(_connectedChromeToken, state); + } else { + ConnectedModeState.updatePopout(_connectedChromeToken, state); + } + } + + function _releaseConnectedChromeState() { + if (_connectedChromeToken) + ConnectedModeState.releasePopout(_connectedChromeToken); + _connectedChromeToken = ""; + } + + // ─── Exposed animation state for ConnectedModeState ──────────────────── + readonly property real contentAnimX: contentContainer.animX + readonly property real contentAnimY: contentContainer.animY + + // ─── ConnectedModeState sync ──────────────────────────────────────────── + function _syncPopoutChromeState() { + if (!SettingsData.connectedFrameModeActive) { + _releaseConnectedChromeState(); + return; + } + if (!root.screen) { + _releaseConnectedChromeState(); + return; + } + if (!contentWindow.visible && !shouldBeVisible) + return; + if (!_connectedChromeToken) + _connectedChromeToken = _nextConnectedChromeToken(); + _publishConnectedChromeState(contentWindow.visible && !ConnectedModeState.hasPopoutOwner(_connectedChromeToken)); + } + + onAlignedXChanged: _syncPopoutChromeState() + onAlignedYChanged: _syncPopoutChromeState() + onAlignedWidthChanged: _syncPopoutChromeState() + onContentAnimXChanged: _syncPopoutChromeState() + onContentAnimYChanged: _syncPopoutChromeState() + onScreenChanged: _syncPopoutChromeState() + onEffectiveBarPositionChanged: _syncPopoutChromeState() + + Connections { + target: contentWindow + function onVisibleChanged() { + if (contentWindow.visible) + root._publishConnectedChromeState(true); + else + root._releaseConnectedChromeState(); + } + } + + Connections { + target: SettingsData + function onConnectedFrameModeActiveChanged() { + if (SettingsData.connectedFrameModeActive) { + if (contentWindow.visible || root.shouldBeVisible) { + if (!root._connectedChromeToken) + root._connectedChromeToken = root._nextConnectedChromeToken(); + root._publishConnectedChromeState(true); + } + } else { + root._releaseConnectedChromeState(); + } + } + } + readonly property bool useBackgroundWindow: !CompositorService.isHyprland || CompositorService.useHyprlandFocusGrab function updateSurfacePosition() { @@ -188,6 +282,13 @@ Item { contentContainer.scaleValue = root.animationScaleCollapsed; } + if (SettingsData.connectedFrameModeActive) { + _connectedChromeToken = _nextConnectedChromeToken(); + _publishConnectedChromeState(true, true); + } else { + _connectedChromeToken = ""; + } + if (useBackgroundWindow) { _surfaceMarginLeft = alignedX - shadowBuffer; _surfaceW = alignedWidth + shadowBuffer * 2; @@ -254,11 +355,14 @@ Item { } } + Component.onDestruction: _releaseConnectedChromeState() + readonly property real screenWidth: screen ? screen.width : 0 readonly property real screenHeight: screen ? screen.height : 0 readonly property real dpr: screen ? screen.devicePixelRatio : 1 readonly property real frameInset: { - if (!SettingsData.frameEnabled) return 0; + if (!SettingsData.frameEnabled) + return 0; const ft = SettingsData.frameThickness; const fr = SettingsData.frameRounding; const ccr = Theme.connectedCornerRadius; @@ -324,6 +428,7 @@ Item { } onAlignedHeightChanged: { + _syncPopoutChromeState(); if (!suspendShadowWhileResizing || !shouldBeVisible) return; _resizeActive = true; @@ -536,43 +641,20 @@ Item { WindowBlur { id: popoutBlur targetWindow: contentWindow - blurEnabled: root.effectiveSurfaceBlurEnabled + blurEnabled: root.effectiveSurfaceBlurEnabled && !SettingsData.connectedFrameModeActive readonly property real s: Math.min(1, contentContainer.scaleValue) - readonly property bool trackBlurFromBarEdge: Theme.isConnectedEffect - || (typeof SettingsData !== "undefined" - && Theme.isDirectionalEffect - && SettingsData.directionalAnimationMode !== 2) + readonly property bool trackBlurFromBarEdge: Theme.isConnectedEffect || (typeof SettingsData !== "undefined" && Theme.isDirectionalEffect && SettingsData.directionalAnimationMode !== 2) // Directional popouts clip to the bar edge, so the blur needs to grow from // that same edge instead of translating through the bar before settling. - readonly property real _dyClamp: (contentContainer.barTop || contentContainer.barBottom) - ? Math.max(-contentContainer.height, Math.min(contentContainer.animY, contentContainer.height)) - : 0 - readonly property real _dxClamp: (contentContainer.barLeft || contentContainer.barRight) - ? Math.max(-contentContainer.width, Math.min(contentContainer.animX, contentContainer.width)) - : 0 + readonly property real _dyClamp: (contentContainer.barTop || contentContainer.barBottom) ? Math.max(-contentContainer.height, Math.min(contentContainer.animY, contentContainer.height)) : 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 - blurWidth: (shouldBeVisible && contentWrapper.opacity > 0) - ? (trackBlurFromBarEdge - ? Math.max(0, contentContainer.width - Math.abs(_dxClamp)) - : (contentContainer.width + contentContainer.horizontalConnectorExtent * 2) * s) - : 0 - blurHeight: (shouldBeVisible && contentWrapper.opacity > 0) - ? (trackBlurFromBarEdge - ? Math.max(0, contentContainer.height - Math.abs(_dyClamp)) - : (contentContainer.height + contentContainer.verticalConnectorExtent * 2) * s) - : 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 + blurWidth: (shouldBeVisible && contentWrapper.opacity > 0) ? (trackBlurFromBarEdge ? Math.max(0, contentContainer.width - Math.abs(_dxClamp)) : (contentContainer.width + contentContainer.horizontalConnectorExtent * 2) * s) : 0 + blurHeight: (shouldBeVisible && contentWrapper.opacity > 0) ? (trackBlurFromBarEdge ? Math.max(0, contentContainer.height - Math.abs(_dyClamp)) : (contentContainer.height + contentContainer.verticalConnectorExtent * 2) * s) : 0 blurRadius: Theme.isConnectedEffect ? Theme.connectedCornerRadius : Theme.connectedSurfaceRadius } @@ -663,9 +745,7 @@ Item { readonly property string connectedBarSide: barTop ? "top" : (barBottom ? "bottom" : (barLeft ? "left" : "right")) readonly property real surfaceRadius: Theme.connectedSurfaceRadius readonly property color surfaceColor: Theme.popupLayerColor(Theme.surfaceContainer) - readonly property color surfaceBorderColor: Theme.isConnectedEffect - ? "transparent" - : (BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium) + readonly property color surfaceBorderColor: Theme.isConnectedEffect ? "transparent" : (BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium) readonly property real surfaceBorderWidth: Theme.isConnectedEffect ? 0 : BlurService.borderWidth readonly property real surfaceTopLeftRadius: Theme.isConnectedEffect && (barTop || barLeft) ? 0 : surfaceRadius readonly property real surfaceTopRightRadius: Theme.isConnectedEffect && (barTop || barRight) ? 0 : surfaceRadius @@ -821,13 +901,9 @@ Item { Item { id: directionalClipMask - readonly property bool shouldClip: Theme.isDirectionalEffect - && typeof SettingsData !== "undefined" - && SettingsData.directionalAnimationMode > 0 + readonly property bool shouldClip: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0 readonly property real clipOversize: 1000 - readonly property real connectedClipAllowance: Theme.isConnectedEffect - ? Math.ceil(root.shadowRenderPadding + BlurService.borderWidth + 2) - : 0 + readonly property real connectedClipAllowance: Theme.isConnectedEffect ? Math.ceil(root.shadowRenderPadding + BlurService.borderWidth + 2) : 0 clip: shouldClip @@ -908,7 +984,7 @@ Item { Item { anchors.fill: parent - visible: Theme.isConnectedEffect + visible: Theme.isConnectedEffect && !SettingsData.connectedFrameModeActive clip: false Rectangle { @@ -930,6 +1006,7 @@ Item { spacing: 0 connectorRadius: Theme.connectedCornerRadius color: contentContainer.surfaceColor + dpr: root.dpr x: Theme.snap(contentContainer.connectorX(shadowSource.bodyX, shadowSource.bodyWidth, placement, spacing), root.dpr) y: Theme.snap(contentContainer.connectorY(shadowSource.bodyY, shadowSource.bodyHeight, placement, spacing), root.dpr) } @@ -941,6 +1018,7 @@ Item { spacing: 0 connectorRadius: Theme.connectedCornerRadius color: contentContainer.surfaceColor + dpr: root.dpr x: Theme.snap(contentContainer.connectorX(shadowSource.bodyX, shadowSource.bodyWidth, placement, spacing), root.dpr) y: Theme.snap(contentContainer.connectorY(shadowSource.bodyY, shadowSource.bodyHeight, placement, spacing), root.dpr) } @@ -986,28 +1064,6 @@ Item { border.color: contentContainer.surfaceBorderColor border.width: contentContainer.surfaceBorderWidth } - - ConnectedCorner { - visible: Theme.isConnectedEffect - barSide: contentContainer.connectedBarSide - placement: "left" - spacing: 0 - connectorRadius: Theme.connectedCornerRadius - color: contentContainer.surfaceColor - x: Theme.snap(contentContainer.connectorX(0, contentWrapper.width, placement, spacing), root.dpr) - y: Theme.snap(contentContainer.connectorY(0, contentWrapper.height, placement, spacing), root.dpr) - } - - ConnectedCorner { - visible: Theme.isConnectedEffect - barSide: contentContainer.connectedBarSide - placement: "right" - spacing: 0 - connectorRadius: Theme.connectedCornerRadius - color: contentContainer.surfaceColor - x: Theme.snap(contentContainer.connectorX(0, contentWrapper.width, placement, spacing), root.dpr) - y: Theme.snap(contentContainer.connectorY(0, contentWrapper.height, placement, spacing), root.dpr) - } } Loader {