From f88cc45e0dc4ab07e4839abae16e714afd572ac4 Mon Sep 17 00:00:00 2001 From: purian23 Date: Fri, 17 Apr 2026 00:30:35 -0400 Subject: [PATCH] (frameMode): New Modal & Launcher connections --- quickshell/Common/ConnectedModeState.qml | 189 ++++++++++ quickshell/Common/SettingsData.qml | 9 + quickshell/Common/settings/SettingsSpec.js | 3 +- quickshell/Modals/Common/DankModal.qml | 214 ++++++++++- .../DankLauncherV2/DankLauncherV2Modal.qml | 207 ++++++++++- .../Modals/DankLauncherV2/LauncherContent.qml | 9 +- quickshell/Modules/Dock/Dock.qml | 17 +- quickshell/Modules/Frame/FrameWindow.qml | 340 ++++++++++++++++++ .../Notifications/Popup/NotificationPopup.qml | 19 +- quickshell/Modules/Settings/FrameTab.qml | 16 + 10 files changed, 991 insertions(+), 32 deletions(-) diff --git a/quickshell/Common/ConnectedModeState.qml b/quickshell/Common/ConnectedModeState.qml index 0938c848..d00a3b4f 100644 --- a/quickshell/Common/ConnectedModeState.qml +++ b/quickshell/Common/ConnectedModeState.qml @@ -278,4 +278,193 @@ Singleton { notificationStates = next; return true; } + + // DankModal / DankLauncherV2Modal State + readonly property var emptyModalState: ({ + "visible": false, + "barSide": "bottom", + "bodyX": 0, + "bodyY": 0, + "bodyW": 0, + "bodyH": 0, + "animX": 0, + "animY": 0, + "omitStartConnector": false, + "omitEndConnector": false + }) + + property var modalStates: ({}) + + function _cloneModalStates() { + const next = {}; + for (const screenName in modalStates) + next[screenName] = modalStates[screenName]; + return next; + } + + function _normalizeModalState(state) { + return { + "visible": !!(state && state.visible), + "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), + "animX": Number(state && state.animX !== undefined ? state.animX : 0), + "animY": Number(state && state.animY !== undefined ? state.animY : 0), + "omitStartConnector": !!(state && state.omitStartConnector), + "omitEndConnector": !!(state && state.omitEndConnector) + }; + } + + function _sameModalGeometry(a, b) { + if (!a || !b) + return false; + return Math.abs(Number(a.bodyX) - Number(b.bodyX)) < 0.5 + && Math.abs(Number(a.bodyY) - Number(b.bodyY)) < 0.5 + && Math.abs(Number(a.bodyW) - Number(b.bodyW)) < 0.5 + && Math.abs(Number(a.bodyH) - Number(b.bodyH)) < 0.5 + && Math.abs(Number(a.animX) - Number(b.animX)) < 0.5 + && Math.abs(Number(a.animY) - Number(b.animY)) < 0.5; + } + + function _sameModalState(a, b) { + if (!a || !b) + return false; + return a.visible === b.visible + && a.barSide === b.barSide + && a.omitStartConnector === b.omitStartConnector + && a.omitEndConnector === b.omitEndConnector + && _sameModalGeometry(a, b); + } + + function setModalState(screenName, state) { + if (!screenName || !state) + return false; + + const normalized = _normalizeModalState(state); + if (_sameModalState(modalStates[screenName], normalized)) + return true; + + const next = _cloneModalStates(); + next[screenName] = normalized; + modalStates = next; + return true; + } + + function clearModalState(screenName) { + if (!screenName || !modalStates[screenName]) + return false; + + const next = _cloneModalStates(); + delete next[screenName]; + modalStates = next; + return true; + } + + function setModalAnim(screenName, animX, animY) { + if (!screenName) + return false; + const cur = modalStates[screenName]; + if (!cur) + return false; + let changed = false; + const nextAnimX = animX !== undefined ? Number(animX) : cur.animX; + const nextAnimY = animY !== undefined ? Number(animY) : cur.animY; + if (Math.abs(nextAnimX - cur.animX) >= 0.5 || Math.abs(nextAnimY - cur.animY) >= 0.5) { + const updated = { + "visible": cur.visible, + "barSide": cur.barSide, + "bodyX": cur.bodyX, + "bodyY": cur.bodyY, + "bodyW": cur.bodyW, + "bodyH": cur.bodyH, + "animX": nextAnimX, + "animY": nextAnimY, + "omitStartConnector": cur.omitStartConnector, + "omitEndConnector": cur.omitEndConnector + }; + const next = _cloneModalStates(); + next[screenName] = updated; + modalStates = next; + changed = true; + } + return changed; + } + + function setModalBody(screenName, bodyX, bodyY, bodyW, bodyH) { + if (!screenName) + return false; + const cur = modalStates[screenName]; + if (!cur) + return false; + const nx = bodyX !== undefined ? Number(bodyX) : cur.bodyX; + const ny = bodyY !== undefined ? Number(bodyY) : cur.bodyY; + const nw = bodyW !== undefined ? Number(bodyW) : cur.bodyW; + const nh = bodyH !== undefined ? Number(bodyH) : cur.bodyH; + if (Math.abs(nx - cur.bodyX) < 0.5 + && Math.abs(ny - cur.bodyY) < 0.5 + && Math.abs(nw - cur.bodyW) < 0.5 + && Math.abs(nh - cur.bodyH) < 0.5) + return false; + const updated = { + "visible": cur.visible, + "barSide": cur.barSide, + "bodyX": nx, + "bodyY": ny, + "bodyW": nw, + "bodyH": nh, + "animX": cur.animX, + "animY": cur.animY, + "omitStartConnector": cur.omitStartConnector, + "omitEndConnector": cur.omitEndConnector + }; + const next = _cloneModalStates(); + next[screenName] = updated; + modalStates = next; + return true; + } + + // ─── Dock retract coordination ──────────────────────────────── + + property var dockRetractRequests: ({}) + + function _cloneRetractRequests() { + const next = {}; + for (const k in dockRetractRequests) + next[k] = dockRetractRequests[k]; + return next; + } + + function requestDockRetract(requesterId, screenName, side) { + if (!requesterId || !screenName || !side) + return false; + const existing = dockRetractRequests[requesterId]; + if (existing && existing.screenName === screenName && existing.side === side) + return true; + const next = _cloneRetractRequests(); + next[requesterId] = { "screenName": screenName, "side": side }; + dockRetractRequests = next; + return true; + } + + function releaseDockRetract(requesterId) { + if (!requesterId || !dockRetractRequests[requesterId]) + return false; + const next = _cloneRetractRequests(); + delete next[requesterId]; + dockRetractRequests = next; + return true; + } + + function dockRetractActiveForSide(screenName, side) { + if (!screenName || !side) + return false; + for (const k in dockRetractRequests) { + const r = dockRetractRequests[k]; + if (r && r.screenName === screenName && r.side === side) + return true; + } + return false; + } } diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index a4897463..8b9beeb7 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -237,6 +237,9 @@ Singleton { onFrameBlurEnabledChanged: saveSettings() property bool frameCloseGaps: false onFrameCloseGapsChanged: saveSettings() + property string frameLauncherEmergeSide: "bottom" + onFrameLauncherEmergeSideChanged: saveSettings() + readonly property string frameModalEmergeSide: frameLauncherEmergeSide === "top" ? "bottom" : "top" property int previousDirectionalMode: 1 onPreviousDirectionalModeChanged: saveSettings() property var connectedFrameBarStyleBackups: ({}) @@ -2206,6 +2209,12 @@ Singleton { return edges; } + function frameEdgeInsetForSide(screen, side) { + if (!frameEnabled || !screen) return 0; + const edges = getActiveBarEdgesForScreen(screen); + return edges.includes(side) ? frameBarSize : frameThickness; + } + function getActiveBarThicknessForScreen(screen) { if (frameEnabled) return frameBarSize; if (!screen) return frameThickness; diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 19815e05..39efd510 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -553,7 +553,8 @@ var SPEC = { frameBarSize: { def: 40 }, frameShowOnOverview: { def: false }, frameBlurEnabled: { def: true }, - frameCloseGaps: { def: false } + frameCloseGaps: { def: false }, + frameLauncherEmergeSide: { def: "bottom" } }; function getValidKeys() { diff --git a/quickshell/Modals/Common/DankModal.qml b/quickshell/Modals/Common/DankModal.qml index e492657f..1ab72da6 100644 --- a/quickshell/Modals/Common/DankModal.qml +++ b/quickshell/Modals/Common/DankModal.qml @@ -26,6 +26,33 @@ Item { property bool closeOnEscapeKey: true property bool closeOnBackgroundClick: true property string animationType: "scale" + + // Opposite side from the launcher by default; subclasses may override + property string preferredConnectedBarSide: SettingsData.frameModalEmergeSide + + readonly property bool frameConnectedMode: SettingsData.frameEnabled + && Theme.isConnectedEffect + && !!effectiveScreen + && SettingsData.isScreenInPreferences(effectiveScreen, SettingsData.frameScreenPreferences) + + readonly property string resolvedConnectedBarSide: frameConnectedMode ? preferredConnectedBarSide : "" + + readonly property bool frameOwnsConnectedChrome: frameConnectedMode && resolvedConnectedBarSide !== "" + + function _dockOccupiesSide(side) { + if (!SettingsData.showDock) return false; + switch (side) { + case "top": return SettingsData.dockPosition === SettingsData.Position.Top; + case "bottom": return SettingsData.dockPosition === SettingsData.Position.Bottom; + case "left": return SettingsData.dockPosition === SettingsData.Position.Left; + case "right": return SettingsData.dockPosition === SettingsData.Position.Right; + } + return false; + } + + readonly property bool _dockBlocksEmergence: frameOwnsConnectedChrome + && _dockOccupiesSide(resolvedConnectedBarSide) + readonly property bool connectedMotionParity: Theme.isConnectedEffect property int animationDuration: connectedMotionParity ? Theme.popoutAnimationDuration : Theme.modalAnimationDuration property real animationScaleCollapsed: Theme.effectScaleCollapsed @@ -66,6 +93,107 @@ Item { property bool animationsEnabled: true + // ─── Connected chrome sync ──────────────────────────────────────────────── + property string _chromeClaimId: "" + property bool _fullSyncPending: false + + function _nextChromeClaimId() { + return layerNamespace + ":modal:" + (new Date()).getTime() + ":" + Math.floor(Math.random() * 1000); + } + + function _currentScreenName() { + return effectiveScreen ? effectiveScreen.name : ""; + } + + function _publishModalChromeState() { + const screenName = _currentScreenName(); + if (!screenName) return; + ConnectedModeState.setModalState(screenName, { + "visible": shouldBeVisible || contentWindow.visible, + "barSide": resolvedConnectedBarSide, + "bodyX": alignedX, + "bodyY": alignedY, + "bodyW": alignedWidth, + "bodyH": alignedHeight, + "animX": modalContainer ? modalContainer.animX : 0, + "animY": modalContainer ? modalContainer.animY : 0, + "omitStartConnector": false, + "omitEndConnector": false + }); + } + + function _syncModalChromeState() { + if (!frameOwnsConnectedChrome) { + _releaseModalChrome(); + return; + } + if (!_chromeClaimId) + _chromeClaimId = _nextChromeClaimId(); + _publishModalChromeState(); + if (_dockBlocksEmergence && (shouldBeVisible || contentWindow.visible)) + ConnectedModeState.requestDockRetract(_chromeClaimId, _currentScreenName(), resolvedConnectedBarSide); + else + ConnectedModeState.releaseDockRetract(_chromeClaimId); + } + + function _flushFullSync() { + _fullSyncPending = false; + _syncModalChromeState(); + } + + function _queueFullSync() { + if (_fullSyncPending) return; + _fullSyncPending = true; + Qt.callLater(() => { + if (root && typeof root._flushFullSync === "function") + root._flushFullSync(); + }); + } + + function _syncModalAnim() { + if (!frameOwnsConnectedChrome || !_chromeClaimId) return; + const screenName = _currentScreenName(); + if (!screenName || !modalContainer) return; + ConnectedModeState.setModalAnim(screenName, modalContainer.animX, modalContainer.animY); + } + + function _syncModalBody() { + if (!frameOwnsConnectedChrome || !_chromeClaimId) return; + const screenName = _currentScreenName(); + if (!screenName) return; + ConnectedModeState.setModalBody(screenName, alignedX, alignedY, alignedWidth, alignedHeight); + } + + function _releaseModalChrome() { + if (_chromeClaimId) { + ConnectedModeState.releaseDockRetract(_chromeClaimId); + _chromeClaimId = ""; + } + const screenName = _currentScreenName(); + if (screenName) + ConnectedModeState.clearModalState(screenName); + } + + onFrameOwnsConnectedChromeChanged: _syncModalChromeState() + onResolvedConnectedBarSideChanged: _queueFullSync() + onShouldBeVisibleChanged: _queueFullSync() + onAlignedXChanged: _syncModalBody() + onAlignedYChanged: _syncModalBody() + onAlignedWidthChanged: _syncModalBody() + onAlignedHeightChanged: _syncModalBody() + + Component.onDestruction: _releaseModalChrome() + + Connections { + target: contentWindow + function onVisibleChanged() { + if (contentWindow.visible) + root._syncModalChromeState(); + else + root._releaseModalChrome(); + } + } + function open() { closeTimer.stop(); animationsEnabled = false; @@ -167,9 +295,11 @@ Item { } } + // shadowRenderPadding is zeroed when frame owns the chrome + // Wayland then clips any content translating past readonly property var shadowLevel: Theme.elevationLevel3 readonly property real shadowFallbackOffset: 6 - readonly property real shadowRenderPadding: (root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0 + readonly property real shadowRenderPadding: (!frameOwnsConnectedChrome && root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0 readonly property real shadowMotionPadding: { if (Theme.isConnectedEffect) return 0; @@ -187,7 +317,47 @@ Item { readonly property real alignedWidth: Theme.px(modalWidth, dpr) readonly property real alignedHeight: Theme.px(modalHeight, dpr) - readonly property real alignedX: Theme.snap((() => { + function _frameEdgeInset(side) { + if (!effectiveScreen) return 0; + return SettingsData.frameEdgeInsetForSide(effectiveScreen, side); + } + + // frameEdgeInsetForSide is the full inset; do not add frameBarSize + readonly property real _connectedAlignedX: { + switch (resolvedConnectedBarSide) { + case "top": + case "bottom": { + const insetL = _frameEdgeInset("left"); + const insetR = _frameEdgeInset("right"); + const usable = Math.max(0, screenWidth - insetL - insetR); + return insetL + Math.max(0, (usable - alignedWidth) / 2); + } + case "left": + return _frameEdgeInset("left"); + case "right": + return screenWidth - alignedWidth - _frameEdgeInset("right"); + } + return 0; + } + + readonly property real _connectedAlignedY: { + switch (resolvedConnectedBarSide) { + case "top": + return _frameEdgeInset("top"); + case "bottom": + return screenHeight - alignedHeight - _frameEdgeInset("bottom"); + case "left": + case "right": { + const insetT = _frameEdgeInset("top"); + const insetB = _frameEdgeInset("bottom"); + const usable = Math.max(0, screenHeight - insetT - insetB); + return insetT + Math.max(0, (usable - alignedHeight) / 2); + } + } + return 0; + } + + readonly property real alignedX: Theme.snap(frameOwnsConnectedChrome ? _connectedAlignedX : (() => { switch (positioning) { case "center": return (screenWidth - alignedWidth) / 2; @@ -200,7 +370,7 @@ Item { } })(), dpr) - readonly property real alignedY: Theme.snap((() => { + readonly property real alignedY: Theme.snap(frameOwnsConnectedChrome ? _connectedAlignedY : (() => { switch (positioning) { case "center": return (screenHeight - alignedHeight) / 2; @@ -271,12 +441,12 @@ Item { WindowBlur { targetWindow: contentWindow - blurEnabled: root.effectiveBlurEnabled + blurEnabled: root.effectiveBlurEnabled && !root.frameOwnsConnectedChrome readonly property real s: Math.min(1, modalContainer.scaleValue) blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr) blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr) - blurWidth: (root.shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.width * s : 0 - blurHeight: (root.shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.height * s : 0 + blurWidth: (root.shouldBeVisible && animatedContent.opacity > 0 && !root.frameOwnsConnectedChrome) ? modalContainer.width * s : 0 + blurHeight: (root.shouldBeVisible && animatedContent.opacity > 0 && !root.frameOwnsConnectedChrome) ? modalContainer.height * s : 0 blurRadius: root.effectiveCornerRadius } @@ -392,7 +562,17 @@ Item { readonly property real customDistRight: root.screenWidth - customAnchorX readonly property real customDistTop: customAnchorY readonly property real customDistBottom: root.screenHeight - customAnchorY + // Connected emergence: travel from the resolved bar edge, matching DankPopout cadence. + readonly property real connectedEmergenceTravelX: Math.max(root.animationOffset, root.alignedWidth + Theme.spacingL) + readonly property real connectedEmergenceTravelY: Math.max(root.animationOffset, root.alignedHeight + Theme.spacingL) readonly property real offsetX: { + if (root.frameOwnsConnectedChrome) { + switch (root.resolvedConnectedBarSide) { + case "left": return -connectedEmergenceTravelX; + case "right": return connectedEmergenceTravelX; + } + return 0; + } if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) return 0; if (slide && !directionalEffect && !depthEffect) @@ -428,6 +608,13 @@ Item { return 0; } readonly property real offsetY: { + if (root.frameOwnsConnectedChrome) { + switch (root.resolvedConnectedBarSide) { + case "top": return -connectedEmergenceTravelY; + case "bottom": return connectedEmergenceTravelY; + } + return 0; + } if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) return 0; if (slide && !directionalEffect && !depthEffect) @@ -467,6 +654,9 @@ Item { property real animX: root.shouldBeVisible ? 0 : root.frozenMotionOffsetX property real animY: root.shouldBeVisible ? 0 : root.frozenMotionOffsetY + onAnimXChanged: if (root.frameOwnsConnectedChrome) root._syncModalAnim() + onAnimYChanged: if (root.frameOwnsConnectedChrome) root._syncModalAnim() + readonly property real computedScaleCollapsed: (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) ? 0.0 : root.animationScaleCollapsed property real scaleValue: root.shouldBeVisible ? 1.0 : computedScaleCollapsed @@ -527,18 +717,18 @@ Item { level: root.shadowLevel fallbackOffset: root.shadowFallbackOffset targetRadius: root.effectiveCornerRadius - targetColor: root.effectiveBackgroundColor - borderColor: root.effectiveBorderColor - borderWidth: root.effectiveBorderWidth - shadowEnabled: root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" + targetColor: root.frameOwnsConnectedChrome ? "transparent" : root.effectiveBackgroundColor + borderColor: root.frameOwnsConnectedChrome ? "transparent" : root.effectiveBorderColor + borderWidth: root.frameOwnsConnectedChrome ? 0 : root.effectiveBorderWidth + shadowEnabled: !root.frameOwnsConnectedChrome && root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" } Rectangle { anchors.fill: parent radius: root.effectiveCornerRadius color: "transparent" - border.color: root.connectedSurfaceOverride ? "transparent" : BlurService.borderColor - border.width: root.connectedSurfaceOverride ? 0 : BlurService.borderWidth + border.color: (root.connectedSurfaceOverride || root.frameOwnsConnectedChrome) ? "transparent" : BlurService.borderColor + border.width: (root.connectedSurfaceOverride || root.frameOwnsConnectedChrome) ? 0 : BlurService.borderWidth z: 100 } diff --git a/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml b/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml index 848fa1eb..bdf55006 100644 --- a/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml +++ b/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml @@ -63,8 +63,72 @@ Item { } readonly property int modalWidth: Math.min(baseWidth, screenWidth - 100) readonly property int modalHeight: Math.min(baseHeight, screenHeight - 100) - readonly property real modalX: (screenWidth - modalWidth) / 2 - readonly property real modalY: (screenHeight - modalHeight) / 2 + + readonly property string preferredConnectedBarSide: SettingsData.frameLauncherEmergeSide + + readonly property bool frameConnectedMode: SettingsData.frameEnabled + && Theme.isConnectedEffect + && !!effectiveScreen + && SettingsData.isScreenInPreferences(effectiveScreen, SettingsData.frameScreenPreferences) + + readonly property string resolvedConnectedBarSide: frameConnectedMode ? preferredConnectedBarSide : "" + + readonly property bool frameOwnsConnectedChrome: frameConnectedMode && resolvedConnectedBarSide !== "" + + function _dockOccupiesSide(side) { + if (!SettingsData.showDock) return false; + switch (side) { + case "top": return SettingsData.dockPosition === SettingsData.Position.Top; + case "bottom": return SettingsData.dockPosition === SettingsData.Position.Bottom; + case "left": return SettingsData.dockPosition === SettingsData.Position.Left; + case "right": return SettingsData.dockPosition === SettingsData.Position.Right; + } + return false; + } + readonly property bool _dockBlocksEmergence: frameOwnsConnectedChrome && _dockOccupiesSide(resolvedConnectedBarSide) + + function _frameEdgeInset(side) { + if (!effectiveScreen) return 0; + return SettingsData.frameEdgeInsetForSide(effectiveScreen, side); + } + + // frameEdgeInsetForSide is the full inset; do not add frameBarSize + readonly property real _connectedModalX: { + switch (resolvedConnectedBarSide) { + case "top": + case "bottom": { + const insetL = _frameEdgeInset("left"); + const insetR = _frameEdgeInset("right"); + const usable = Math.max(0, screenWidth - insetL - insetR); + return insetL + Math.max(0, (usable - modalWidth) / 2); + } + case "left": + return _frameEdgeInset("left"); + case "right": + return screenWidth - modalWidth - _frameEdgeInset("right"); + } + return (screenWidth - modalWidth) / 2; + } + + readonly property real _connectedModalY: { + switch (resolvedConnectedBarSide) { + case "top": + return _frameEdgeInset("top"); + case "bottom": + return screenHeight - modalHeight - _frameEdgeInset("bottom"); + case "left": + case "right": { + const insetT = _frameEdgeInset("top"); + const insetB = _frameEdgeInset("bottom"); + const usable = Math.max(0, screenHeight - insetT - insetB); + return insetT + Math.max(0, (usable - modalHeight) / 2); + } + } + return (screenHeight - modalHeight) / 2; + } + + readonly property real modalX: frameOwnsConnectedChrome ? _connectedModalX : ((screenWidth - modalWidth) / 2) + readonly property real modalY: frameOwnsConnectedChrome ? _connectedModalY : ((screenHeight - modalHeight) / 2) readonly property bool connectedSurfaceOverride: Theme.isConnectedEffect readonly property int launcherAnimationDuration: Theme.isConnectedEffect ? Theme.popoutAnimationDuration : Theme.modalAnimationDuration @@ -93,10 +157,11 @@ Item { readonly property int effectiveBorderWidth: connectedSurfaceOverride ? 0 : borderWidth readonly property bool effectiveBlurEnabled: Theme.connectedSurfaceBlurEnabled - // Shadow padding for the content window (render padding only, no motion padding) + // Shadow padding for the content window (render padding only, no motion padding). + // Zeroed when frame owns the chrome and Wayland clips past the bar edge readonly property var shadowLevel: Theme.elevationLevel3 readonly property real shadowFallbackOffset: 6 - readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0 + readonly property real shadowRenderPadding: (!frameOwnsConnectedChrome && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0 readonly property real shadowPad: Theme.snap(shadowRenderPadding, dpr) readonly property real alignedWidth: Theme.px(modalWidth, dpr) readonly property real alignedHeight: Theme.px(modalHeight, dpr) @@ -123,6 +188,107 @@ Item { signal dialogClosed + // ─── Connected chrome sync ──────────────────────────────────────────────── + property string _chromeClaimId: "" + property bool _fullSyncPending: false + + function _nextChromeClaimId() { + return "dms:launcher-v2:" + (new Date()).getTime() + ":" + Math.floor(Math.random() * 1000); + } + + function _currentScreenName() { + return effectiveScreen ? effectiveScreen.name : ""; + } + + function _publishModalChromeState() { + const screenName = _currentScreenName(); + if (!screenName) return; + ConnectedModeState.setModalState(screenName, { + "visible": spotlightOpen || contentWindow.visible, + "barSide": resolvedConnectedBarSide, + "bodyX": alignedX, + "bodyY": alignedY, + "bodyW": alignedWidth, + "bodyH": alignedHeight, + "animX": contentContainer ? contentContainer.animX : 0, + "animY": contentContainer ? contentContainer.animY : 0, + "omitStartConnector": false, + "omitEndConnector": false + }); + } + + function _syncModalChromeState() { + if (!frameOwnsConnectedChrome) { + _releaseModalChrome(); + return; + } + if (!_chromeClaimId) + _chromeClaimId = _nextChromeClaimId(); + _publishModalChromeState(); + if (_dockBlocksEmergence && (spotlightOpen || contentWindow.visible)) + ConnectedModeState.requestDockRetract(_chromeClaimId, _currentScreenName(), resolvedConnectedBarSide); + else + ConnectedModeState.releaseDockRetract(_chromeClaimId); + } + + function _flushFullSync() { + _fullSyncPending = false; + _syncModalChromeState(); + } + + function _queueFullSync() { + if (_fullSyncPending) return; + _fullSyncPending = true; + Qt.callLater(() => { + if (root && typeof root._flushFullSync === "function") + root._flushFullSync(); + }); + } + + function _syncModalAnim() { + if (!frameOwnsConnectedChrome || !_chromeClaimId) return; + const screenName = _currentScreenName(); + if (!screenName || !contentContainer) return; + ConnectedModeState.setModalAnim(screenName, contentContainer.animX, contentContainer.animY); + } + + function _syncModalBody() { + if (!frameOwnsConnectedChrome || !_chromeClaimId) return; + const screenName = _currentScreenName(); + if (!screenName) return; + ConnectedModeState.setModalBody(screenName, alignedX, alignedY, alignedWidth, alignedHeight); + } + + function _releaseModalChrome() { + if (_chromeClaimId) { + ConnectedModeState.releaseDockRetract(_chromeClaimId); + _chromeClaimId = ""; + } + const screenName = _currentScreenName(); + if (screenName) + ConnectedModeState.clearModalState(screenName); + } + + onFrameOwnsConnectedChromeChanged: _syncModalChromeState() + onResolvedConnectedBarSideChanged: _queueFullSync() + onSpotlightOpenChanged: _queueFullSync() + onAlignedXChanged: _syncModalBody() + onAlignedYChanged: _syncModalBody() + onAlignedWidthChanged: _syncModalBody() + onAlignedHeightChanged: _syncModalBody() + + Component.onDestruction: _releaseModalChrome() + + Connections { + target: contentWindow + function onVisibleChanged() { + if (contentWindow.visible) + root._syncModalChromeState(); + else + root._releaseModalChrome(); + } + } + function _ensureContentLoadedAndInitialize(query, mode) { _pendingQuery = query || ""; _pendingMode = mode || ""; @@ -418,12 +584,12 @@ Item { WindowBlur { targetWindow: contentWindow - blurEnabled: root.effectiveBlurEnabled + blurEnabled: root.effectiveBlurEnabled && !root.frameOwnsConnectedChrome readonly property real s: Math.min(1, contentContainer.scaleValue) blurX: root._ccX + root.alignedWidth * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr) blurY: root._ccY + root.alignedHeight * (1 - s) * 0.5 + Theme.snap(contentContainer.animY, root.dpr) - blurWidth: (root.spotlightOpen || root.isClosing) && contentWrapper.opacity > 0 ? root.alignedWidth * s : 0 - blurHeight: (root.spotlightOpen || root.isClosing) && contentWrapper.opacity > 0 ? root.alignedHeight * s : 0 + blurWidth: (root.spotlightOpen || root.isClosing) && contentWrapper.opacity > 0 && !root.frameOwnsConnectedChrome ? root.alignedWidth * s : 0 + blurHeight: (root.spotlightOpen || root.isClosing) && contentWrapper.opacity > 0 && !root.frameOwnsConnectedChrome ? root.alignedHeight * s : 0 blurRadius: root.cornerRadius } @@ -491,7 +657,16 @@ Item { readonly property bool directionalEffect: Theme.isDirectionalEffect readonly property bool depthEffect: Theme.isDepthEffect + readonly property real _connectedTravelX: Math.max(Theme.effectAnimOffset, root.alignedWidth + Theme.spacingL) + readonly property real _connectedTravelY: Math.max(Theme.effectAnimOffset, root.alignedHeight + Theme.spacingL) readonly property real collapsedMotionX: { + if (root.frameOwnsConnectedChrome) { + switch (root.resolvedConnectedBarSide) { + case "left": return -_connectedTravelX; + case "right": return _connectedTravelX; + } + return 0; + } if (directionalEffect) { if (dockLeft) return -(root._ccX + root.alignedWidth + Theme.effectAnimOffset); @@ -503,6 +678,13 @@ Item { return 0; } readonly property real collapsedMotionY: { + if (root.frameOwnsConnectedChrome) { + switch (root.resolvedConnectedBarSide) { + case "top": return -_connectedTravelY; + case "bottom": return _connectedTravelY; + } + return 0; + } if (directionalEffect) { if (dockTop) return -(root._ccY + root.alignedHeight + Theme.effectAnimOffset); @@ -520,6 +702,9 @@ Item { property real animY: root._motionActive ? 0 : root._frozenMotionY property real scaleValue: root._motionActive ? 1.0 : (Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed)) + onAnimXChanged: if (root.frameOwnsConnectedChrome) root._syncModalAnim() + onAnimYChanged: if (root.frameOwnsConnectedChrome) root._syncModalAnim() + Behavior on animX { enabled: root.animationsEnabled DankAnim { @@ -575,11 +760,11 @@ Item { y: contentWrapper.y level: root.shadowLevel fallbackOffset: root.shadowFallbackOffset - targetColor: root.backgroundColor - borderColor: root.effectiveBorderColor - borderWidth: root.effectiveBorderWidth + targetColor: root.frameOwnsConnectedChrome ? "transparent" : root.backgroundColor + borderColor: root.frameOwnsConnectedChrome ? "transparent" : root.effectiveBorderColor + borderWidth: root.frameOwnsConnectedChrome ? 0 : root.effectiveBorderWidth targetRadius: root.cornerRadius - shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" + shadowEnabled: !root.frameOwnsConnectedChrome && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" } // contentWrapper moves inside static contentContainer — DankPopout pattern diff --git a/quickshell/Modals/DankLauncherV2/LauncherContent.qml b/quickshell/Modals/DankLauncherV2/LauncherContent.qml index 18034ad3..563b1fdc 100644 --- a/quickshell/Modals/DankLauncherV2/LauncherContent.qml +++ b/quickshell/Modals/DankLauncherV2/LauncherContent.qml @@ -281,13 +281,16 @@ FocusScope { anchors.rightMargin: root.parentModal?.borderWidth ?? 1 anchors.bottomMargin: root.parentModal?.borderWidth ?? 1 readonly property bool showFooter: SettingsData.dankLauncherV2Size !== "micro" && SettingsData.dankLauncherV2ShowFooter - height: showFooter ? 36 : 0 + readonly property bool _connectedArcAtFooter: (root.parentModal?.frameOwnsConnectedChrome ?? false) && (root.parentModal?.resolvedConnectedBarSide === "bottom") + height: showFooter ? (_connectedArcAtFooter ? 76 : 36) : 0 visible: showFooter clip: true Rectangle { anchors.fill: parent anchors.topMargin: -Theme.cornerRadius + // In connected mode the launcher provides the surface so update the toolbar for arcs + visible: !(root.parentModal?.frameOwnsConnectedChrome ?? false) color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) radius: Theme.cornerRadius } @@ -295,7 +298,7 @@ FocusScope { Row { id: modeButtonsRow anchors.left: parent.left - anchors.leftMargin: Theme.spacingXS + anchors.leftMargin: Theme.spacingM anchors.verticalCenter: parent.verticalCenter layoutDirection: I18n.isRtl ? Qt.RightToLeft : Qt.LeftToRight spacing: 2 @@ -367,7 +370,7 @@ FocusScope { Row { id: hintsRow anchors.right: parent.right - anchors.rightMargin: Theme.spacingS + anchors.rightMargin: Theme.spacingM anchors.verticalCenter: parent.verticalCenter layoutDirection: I18n.isRtl ? Qt.RightToLeft : Qt.LeftToRight spacing: Theme.spacingM diff --git a/quickshell/Modules/Dock/Dock.qml b/quickshell/Modules/Dock/Dock.qml index 6bf67756..a0bfb543 100644 --- a/quickshell/Modules/Dock/Dock.qml +++ b/quickshell/Modules/Dock/Dock.qml @@ -46,9 +46,11 @@ Variants { 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; + if (Theme.isConnectedEffect) + return connectedBarActiveOnEdge ? SettingsData.frameBarSize : SettingsData.frameThickness; + if (SettingsData.frameEnabled) + return SettingsData.frameEdgeInsetForSide(dock.screen || modelData, dock.connectedBarSide); + return 0; } readonly property real surfaceRadius: Theme.connectedSurfaceRadius readonly property color surfaceColor: Theme.isConnectedEffect ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency) @@ -379,7 +381,16 @@ Variants { onTriggered: dock.revealSticky = false } + // Flip `reveal` false when a modal claims this edge; reuses the slide animation + readonly property bool _modalRetractActive: { + if (!dock._dockScreenName) return false; + return ConnectedModeState.dockRetractActiveForSide(dock._dockScreenName, dock.connectedBarSide); + } + property bool reveal: { + if (_modalRetractActive) + return false; + if (CompositorService.isNiri && NiriService.inOverview && SettingsData.dockOpenOnOverview) { return true; } diff --git a/quickshell/Modules/Frame/FrameWindow.qml b/quickshell/Modules/Frame/FrameWindow.qml index 3ef6d4b9..9ec89ab8 100644 --- a/quickshell/Modules/Frame/FrameWindow.qml +++ b/quickshell/Modules/Frame/FrameWindow.qml @@ -46,6 +46,7 @@ PanelWindow { "y": 0 }) readonly property var _notifState: ConnectedModeState.notificationStates[win._screenName] || ConnectedModeState.emptyNotificationState + readonly property var _modalState: ConnectedModeState.modalStates[win._screenName] || ConnectedModeState.emptyModalState // ─── Connected chrome convenience properties ────────────────────────────── readonly property bool _connectedActive: win._frameActive && SettingsData.connectedFrameModeActive @@ -94,6 +95,22 @@ PanelWindow { readonly property real _effectiveNotifFarEndCcr: win._notifState.omitEndConnector ? win._effectiveNotifFarCcr : 0 readonly property real _effectiveNotifMaxCcr: Math.max(win._effectiveNotifStartCcr, win._effectiveNotifEndCcr) readonly property real _effectiveNotifFarExtent: Math.max(win._effectiveNotifFarStartCcr, win._effectiveNotifFarEndCcr) + readonly property real _effectiveModalCcr: { + const isHoriz = win._modalState.barSide === "top" || win._modalState.barSide === "bottom"; + const crossSize = isHoriz ? _modalBodyBlurAnchor.width : _modalBodyBlurAnchor.height; + const extent = isHoriz ? _modalBodyBlurAnchor.height : _modalBodyBlurAnchor.width; + return Theme.snap(Math.max(0, Math.min(win._ccr, win._surfaceRadius, extent, crossSize / 2)), win._dpr); + } + readonly property real _effectiveModalFarCcr: { + const isHoriz = win._modalState.barSide === "top" || win._modalState.barSide === "bottom"; + const crossSize = isHoriz ? _modalBodyBlurAnchor.width : _modalBodyBlurAnchor.height; + return Theme.snap(Math.max(0, Math.min(win._ccr, win._surfaceRadius, crossSize / 2)), win._dpr); + } + readonly property real _effectiveModalStartCcr: win._modalState.omitStartConnector ? 0 : win._effectiveModalCcr + readonly property real _effectiveModalEndCcr: win._modalState.omitEndConnector ? 0 : win._effectiveModalCcr + readonly property real _effectiveModalFarStartCcr: win._modalState.omitStartConnector ? win._effectiveModalFarCcr : 0 + readonly property real _effectiveModalFarEndCcr: win._modalState.omitEndConnector ? win._effectiveModalFarCcr : 0 + readonly property real _effectiveModalFarExtent: Math.max(win._effectiveModalFarStartCcr, win._effectiveModalFarEndCcr) 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) @@ -403,6 +420,180 @@ PanelWindow { height: _active ? Theme.snap(win._notifState.bodyH, win._dpr) : 0 } + Item { + id: _modalBodyBlurAnchor + visible: false + + readonly property bool _active: win._frameActive && win._modalState.visible && win._modalState.bodyW > 0 && win._modalState.bodyH > 0 + + // Clamp animX/Y so the blur body shrinks toward the bar edge (same as _popoutBodyBlurAnchor). + readonly property real _dyClamp: (win._modalState.barSide === "top" || win._modalState.barSide === "bottom") ? Math.max(-win._modalState.bodyH, Math.min(win._modalState.animY, win._modalState.bodyH)) : 0 + readonly property real _dxClamp: (win._modalState.barSide === "left" || win._modalState.barSide === "right") ? Math.max(-win._modalState.bodyW, Math.min(win._modalState.animX, win._modalState.bodyW)) : 0 + + x: _active ? Theme.snap(win._modalState.bodyX + (win._modalState.barSide === "right" ? _dxClamp : 0), win._dpr) : 0 + y: _active ? Theme.snap(win._modalState.bodyY + (win._modalState.barSide === "bottom" ? _dyClamp : 0), win._dpr) : 0 + width: _active ? Theme.snap(Math.max(0, win._modalState.bodyW - Math.abs(_dxClamp)), win._dpr) : 0 + height: _active ? Theme.snap(Math.max(0, win._modalState.bodyH - Math.abs(_dyClamp)), win._dpr) : 0 + } + + Item { + id: _modalBodyBlurCap + opacity: 0 + + readonly property string _side: win._modalState.barSide + readonly property real _capThickness: win._modalBlurCapThickness() + readonly property bool _active: _modalBodyBlurAnchor._active && _capThickness > 0 && _modalBodyBlurAnchor.width > 0 && _modalBodyBlurAnchor.height > 0 + readonly property real _capWidth: (_side === "left" || _side === "right") ? Math.min(_capThickness, _modalBodyBlurAnchor.width) : _modalBodyBlurAnchor.width + readonly property real _capHeight: (_side === "top" || _side === "bottom") ? Math.min(_capThickness, _modalBodyBlurAnchor.height) : _modalBodyBlurAnchor.height + + x: !_active ? 0 : (_side === "right" ? _modalBodyBlurAnchor.x + _modalBodyBlurAnchor.width - _capWidth : _modalBodyBlurAnchor.x) + y: !_active ? 0 : (_side === "bottom" ? _modalBodyBlurAnchor.y + _modalBodyBlurAnchor.height - _capHeight : _modalBodyBlurAnchor.y) + width: _active ? _capWidth : 0 + height: _active ? _capHeight : 0 + } + + Item { + id: _modalLeftConnectorBlurAnchor + opacity: 0 + + readonly property real _radius: win._modalConnectorRadius("left") + readonly property bool _active: _modalBodyBlurAnchor._active && _radius > 0 + readonly property real _w: win._modalConnectorWidth(0, "left") + readonly property real _h: win._modalConnectorHeight(0, "left") + + x: _active ? Theme.snap(win._modalConnectorX(_modalBodyBlurAnchor.x, _modalBodyBlurAnchor.width, "left", 0), win._dpr) : 0 + y: _active ? Theme.snap(win._modalConnectorY(_modalBodyBlurAnchor.y, _modalBodyBlurAnchor.height, "left", 0), win._dpr) : 0 + width: _active ? _w : 0 + height: _active ? _h : 0 + } + + Item { + id: _modalRightConnectorBlurAnchor + opacity: 0 + + readonly property real _radius: win._modalConnectorRadius("right") + readonly property bool _active: _modalBodyBlurAnchor._active && _radius > 0 + readonly property real _w: win._modalConnectorWidth(0, "right") + readonly property real _h: win._modalConnectorHeight(0, "right") + + x: _active ? Theme.snap(win._modalConnectorX(_modalBodyBlurAnchor.x, _modalBodyBlurAnchor.width, "right", 0), win._dpr) : 0 + y: _active ? Theme.snap(win._modalConnectorY(_modalBodyBlurAnchor.y, _modalBodyBlurAnchor.height, "right", 0), win._dpr) : 0 + width: _active ? _w : 0 + height: _active ? _h : 0 + } + + Item { + id: _modalLeftConnectorCutout + opacity: 0 + + readonly property bool _active: _modalLeftConnectorBlurAnchor.width > 0 && _modalLeftConnectorBlurAnchor.height > 0 + readonly property string _arcCorner: win._connectorArcCorner(win._modalState.barSide, "left") + readonly property real _radius: win._modalConnectorRadius("left") + + x: _active ? win._connectorCutoutX(_modalLeftConnectorBlurAnchor.x, _modalLeftConnectorBlurAnchor.width, _arcCorner, _radius) : 0 + y: _active ? win._connectorCutoutY(_modalLeftConnectorBlurAnchor.y, _modalLeftConnectorBlurAnchor.height, _arcCorner, _radius) : 0 + width: _active ? _radius * 2 : 0 + height: _active ? _radius * 2 : 0 + } + + Item { + id: _modalRightConnectorCutout + opacity: 0 + + readonly property bool _active: _modalRightConnectorBlurAnchor.width > 0 && _modalRightConnectorBlurAnchor.height > 0 + readonly property string _arcCorner: win._connectorArcCorner(win._modalState.barSide, "right") + readonly property real _radius: win._modalConnectorRadius("right") + + x: _active ? win._connectorCutoutX(_modalRightConnectorBlurAnchor.x, _modalRightConnectorBlurAnchor.width, _arcCorner, _radius) : 0 + y: _active ? win._connectorCutoutY(_modalRightConnectorBlurAnchor.y, _modalRightConnectorBlurAnchor.height, _arcCorner, _radius) : 0 + width: _active ? _radius * 2 : 0 + height: _active ? _radius * 2 : 0 + } + + Item { + id: _modalFarStartConnectorBlurAnchor + opacity: 0 + + readonly property real _radius: win._effectiveModalFarStartCcr + readonly property bool _active: _modalBodyBlurAnchor._active && _radius > 0 + + x: _active ? Theme.snap(win._farConnectorX(_modalBodyBlurAnchor.x, _modalBodyBlurAnchor.y, _modalBodyBlurAnchor.width, _modalBodyBlurAnchor.height, win._modalState.barSide, "left", _radius), win._dpr) : 0 + y: _active ? Theme.snap(win._farConnectorY(_modalBodyBlurAnchor.x, _modalBodyBlurAnchor.y, _modalBodyBlurAnchor.width, _modalBodyBlurAnchor.height, win._modalState.barSide, "left", _radius), win._dpr) : 0 + width: _active ? _radius : 0 + height: _active ? _radius : 0 + } + + Item { + id: _modalFarStartBodyBlurCap + opacity: 0 + + readonly property real _radius: win._effectiveModalFarStartCcr + readonly property bool _active: _modalBodyBlurAnchor._active && _radius > 0 + + x: _active ? Theme.snap(win._farBodyCapX(_modalBodyBlurAnchor.x, _modalBodyBlurAnchor.width, win._modalState.barSide, "left", _radius), win._dpr) : 0 + y: _active ? Theme.snap(win._farBodyCapY(_modalBodyBlurAnchor.y, _modalBodyBlurAnchor.height, win._modalState.barSide, "left", _radius), win._dpr) : 0 + width: _active ? _radius : 0 + height: _active ? _radius : 0 + } + + Item { + id: _modalFarEndBodyBlurCap + opacity: 0 + + readonly property real _radius: win._effectiveModalFarEndCcr + readonly property bool _active: _modalBodyBlurAnchor._active && _radius > 0 + + x: _active ? Theme.snap(win._farBodyCapX(_modalBodyBlurAnchor.x, _modalBodyBlurAnchor.width, win._modalState.barSide, "right", _radius), win._dpr) : 0 + y: _active ? Theme.snap(win._farBodyCapY(_modalBodyBlurAnchor.y, _modalBodyBlurAnchor.height, win._modalState.barSide, "right", _radius), win._dpr) : 0 + width: _active ? _radius : 0 + height: _active ? _radius : 0 + } + + Item { + id: _modalFarEndConnectorBlurAnchor + opacity: 0 + + readonly property real _radius: win._effectiveModalFarEndCcr + readonly property bool _active: _modalBodyBlurAnchor._active && _radius > 0 + + x: _active ? Theme.snap(win._farConnectorX(_modalBodyBlurAnchor.x, _modalBodyBlurAnchor.y, _modalBodyBlurAnchor.width, _modalBodyBlurAnchor.height, win._modalState.barSide, "right", _radius), win._dpr) : 0 + y: _active ? Theme.snap(win._farConnectorY(_modalBodyBlurAnchor.x, _modalBodyBlurAnchor.y, _modalBodyBlurAnchor.width, _modalBodyBlurAnchor.height, win._modalState.barSide, "right", _radius), win._dpr) : 0 + width: _active ? _radius : 0 + height: _active ? _radius : 0 + } + + Item { + id: _modalFarStartConnectorCutout + opacity: 0 + + readonly property bool _active: _modalFarStartConnectorBlurAnchor.width > 0 && _modalFarStartConnectorBlurAnchor.height > 0 + readonly property string _barSide: win._farConnectorBarSide(win._modalState.barSide, "left") + readonly property string _placement: win._farConnectorPlacement(win._modalState.barSide, "left") + readonly property string _arcCorner: win._connectorArcCorner(_barSide, _placement) + readonly property real _radius: win._effectiveModalFarStartCcr + + x: _active ? win._connectorCutoutX(_modalFarStartConnectorBlurAnchor.x, _modalFarStartConnectorBlurAnchor.width, _arcCorner, _radius) : 0 + y: _active ? win._connectorCutoutY(_modalFarStartConnectorBlurAnchor.y, _modalFarStartConnectorBlurAnchor.height, _arcCorner, _radius) : 0 + width: _active ? _radius * 2 : 0 + height: _active ? _radius * 2 : 0 + } + + Item { + id: _modalFarEndConnectorCutout + opacity: 0 + + readonly property bool _active: _modalFarEndConnectorBlurAnchor.width > 0 && _modalFarEndConnectorBlurAnchor.height > 0 + readonly property string _barSide: win._farConnectorBarSide(win._modalState.barSide, "right") + readonly property string _placement: win._farConnectorPlacement(win._modalState.barSide, "right") + readonly property string _arcCorner: win._connectorArcCorner(_barSide, _placement) + readonly property real _radius: win._effectiveModalFarEndCcr + + x: _active ? win._connectorCutoutX(_modalFarEndConnectorBlurAnchor.x, _modalFarEndConnectorBlurAnchor.width, _arcCorner, _radius) : 0 + y: _active ? win._connectorCutoutY(_modalFarEndConnectorBlurAnchor.y, _modalFarEndConnectorBlurAnchor.height, _arcCorner, _radius) : 0 + width: _active ? _radius * 2 : 0 + height: _active ? _radius * 2 : 0 + } + Item { id: _notifBodySceneBlurAnchor visible: false @@ -704,6 +895,53 @@ PanelWindow { radius: win._effectiveNotifFarEndCcr } } + + // ── Connected modal blur regions ── + Region { + item: _modalBodyBlurAnchor + radius: win._surfaceRadius + } + Region { + item: _modalBodyBlurCap + } + Region { + item: _modalLeftConnectorBlurAnchor + Region { + item: _modalLeftConnectorCutout + intersection: Intersection.Subtract + radius: win._modalConnectorRadius("left") + } + } + Region { + item: _modalRightConnectorBlurAnchor + Region { + item: _modalRightConnectorCutout + intersection: Intersection.Subtract + radius: win._modalConnectorRadius("right") + } + } + Region { + item: _modalFarStartBodyBlurCap + } + Region { + item: _modalFarEndBodyBlurCap + } + Region { + item: _modalFarStartConnectorBlurAnchor + Region { + item: _modalFarStartConnectorCutout + intersection: Intersection.Subtract + radius: win._effectiveModalFarStartCcr + } + } + Region { + item: _modalFarEndConnectorBlurAnchor + Region { + item: _modalFarEndConnectorCutout + intersection: Intersection.Subtract + radius: win._effectiveModalFarEndCcr + } + } } // ─── Connector position helpers ──────────────────────────────────────── @@ -894,6 +1132,52 @@ PanelWindow { return (win._dockState.barSide === "left" || win._dockState.barSide === "right") ? win._seamOverlap : 0; } + function _modalArcExtent() { + return (win._modalState.barSide === "top" || win._modalState.barSide === "bottom") ? _modalBodyBlurAnchor.height : _modalBodyBlurAnchor.width; + } + + function _modalBlurCapThickness() { + const extent = win._modalArcExtent(); + return Math.max(0, Math.min(win._effectiveModalCcr, extent - win._surfaceRadius)); + } + + function _modalConnectorRadius(placement) { + return placement === "right" ? win._effectiveModalEndCcr : win._effectiveModalStartCcr; + } + + function _modalConnectorWidth(spacing, placement) { + const isVert = win._modalState.barSide === "left" || win._modalState.barSide === "right"; + const radius = win._modalConnectorRadius(placement); + return isVert ? (spacing + radius) : radius; + } + + function _modalConnectorHeight(spacing, placement) { + const isVert = win._modalState.barSide === "left" || win._modalState.barSide === "right"; + const radius = win._modalConnectorRadius(placement); + return isVert ? radius : (spacing + radius); + } + + function _modalConnectorX(baseX, bodyWidth, placement, spacing) { + const barSide = win._modalState.barSide; + const isVert = barSide === "left" || barSide === "right"; + const seamX = !isVert ? (placement === "left" ? baseX : baseX + bodyWidth) : (barSide === "left" ? baseX : baseX + bodyWidth); + const w = _modalConnectorWidth(spacing, placement); + if (!isVert) + return placement === "left" ? seamX - w : seamX; + return barSide === "left" ? seamX : seamX - w; + } + + function _modalConnectorY(baseY, bodyHeight, placement, spacing) { + const barSide = win._modalState.barSide; + const seamY = barSide === "top" ? baseY : barSide === "bottom" ? baseY + bodyHeight : (placement === "left" ? baseY : baseY + bodyHeight); + const h = _modalConnectorHeight(spacing, placement); + if (barSide === "top") + return seamY; + if (barSide === "bottom") + return seamY - h; + return placement === "left" ? seamY - h : seamY; + } + function _popoutArcExtent() { return (ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom") ? _popoutBodyBlurAnchor.height : _popoutBodyBlurAnchor.width; } @@ -1343,5 +1627,61 @@ PanelWindow { y: 0 } } + + // Bar-side-bounded clip so modal chrome retracts behind the bar on exit + // instead of sliding over bar widgets (mirrors the popout `_popoutClip`). + Item { + id: _modalClip + visible: _modalBodyBlurAnchor._active + z: 1 + + readonly property string _modalSide: win._modalState.barSide + readonly property real _inset: _modalBodyBlurAnchor._active && win.screen ? SettingsData.frameEdgeInsetForSide(win.screen, _modalSide) : 0 + readonly property real _topBound: _modalSide === "top" ? _inset : 0 + readonly property real _bottomBound: _modalSide === "bottom" ? (win.height - _inset) : win.height + readonly property real _leftBound: _modalSide === "left" ? _inset : 0 + readonly property real _rightBound: _modalSide === "right" ? (win.width - _inset) : win.width + + x: _leftBound + y: _topBound + width: Math.max(0, _rightBound - _leftBound) + height: Math.max(0, _bottomBound - _topBound) + clip: true + + Item { + id: _modalChrome + + readonly property string _modalSide: win._modalState.barSide + readonly property bool _isHoriz: _modalSide === "top" || _modalSide === "bottom" + readonly property real _startCcr: win._effectiveModalStartCcr + readonly property real _endCcr: win._effectiveModalEndCcr + readonly property real _farExtent: win._effectiveModalFarExtent + readonly property real _bodyOffsetX: _isHoriz ? _startCcr : (_modalSide === "right" ? _farExtent : 0) + readonly property real _bodyOffsetY: _isHoriz ? (_modalSide === "bottom" ? _farExtent : 0) : _startCcr + readonly property real _bodyW: Theme.snap(_modalBodyBlurAnchor.width, win._dpr) + readonly property real _bodyH: Theme.snap(_modalBodyBlurAnchor.height, win._dpr) + + x: Theme.snap(_modalBodyBlurAnchor.x - _bodyOffsetX - _modalClip.x, win._dpr) + y: Theme.snap(_modalBodyBlurAnchor.y - _bodyOffsetY - _modalClip.y, win._dpr) + width: _isHoriz ? Theme.snap(_bodyW + _startCcr + _endCcr, win._dpr) : Theme.snap(_bodyW + _farExtent, win._dpr) + height: _isHoriz ? Theme.snap(_bodyH + _farExtent, win._dpr) : Theme.snap(_bodyH + _startCcr + _endCcr, win._dpr) + + ConnectedShape { + visible: _modalBodyBlurAnchor._active && _modalChrome._bodyW > 0 && _modalChrome._bodyH > 0 + barSide: _modalChrome._modalSide + bodyWidth: _modalChrome._bodyW + bodyHeight: _modalChrome._bodyH + connectorRadius: win._effectiveModalCcr + startConnectorRadius: _modalChrome._startCcr + endConnectorRadius: _modalChrome._endCcr + farStartConnectorRadius: win._effectiveModalFarStartCcr + farEndConnectorRadius: win._effectiveModalFarEndCcr + surfaceRadius: win._surfaceRadius + fillColor: win._opaqueSurfaceColor + x: 0 + y: 0 + } + } + } } } diff --git a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml index 1045aa35..521d930e 100644 --- a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml +++ b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml @@ -360,11 +360,18 @@ PanelWindow { function _frameEdgeInset(side) { if (!screen) return 0; - const edges = SettingsData.getActiveBarEdgesForScreen(screen); - const raw = edges.includes(side) ? SettingsData.frameBarSize : SettingsData.frameThickness; + const raw = SettingsData.frameEdgeInsetForSide(screen, side); return Math.max(0, Math.round(Theme.px(raw, dpr))); } + readonly property bool frameOnlyNoConnected: SettingsData.frameEnabled && !connectedFrameMode && !!screen + && SettingsData.isScreenInPreferences(screen, SettingsData.frameScreenPreferences) + + // Frame ON + Connected OFF. frameEdgeInset is the full bar/frame inset + function _frameGapMargin(side) { + return _frameEdgeInset(side) + Theme.popupDistance; + } + function getTopMargin() { const popupPos = SettingsData.notificationPopupPosition; const isTop = isTopCenter || popupPos === SettingsData.Position.Top || popupPos === SettingsData.Position.Left; @@ -377,6 +384,8 @@ PanelWindow { : (Theme.px(SettingsData.frameRounding, dpr) + Theme.px(Theme.connectedCornerRadius, dpr)); return _frameEdgeInset("top") + cornerClear + screenY; } + if (frameOnlyNoConnected) + return _frameGapMargin("top") + screenY; const barInfo = getBarInfo(); const base = barInfo.topBar > 0 ? barInfo.topBar : Theme.popupDistance; return base + screenY; @@ -394,6 +403,8 @@ PanelWindow { : (Theme.px(SettingsData.frameRounding, dpr) + Theme.px(Theme.connectedCornerRadius, dpr)); return _frameEdgeInset("bottom") + cornerClear + screenY; } + if (frameOnlyNoConnected) + return _frameGapMargin("bottom") + screenY; const barInfo = getBarInfo(); const base = barInfo.bottomBar > 0 ? barInfo.bottomBar : Theme.popupDistance; return base + screenY; @@ -410,6 +421,8 @@ PanelWindow { if (connectedFrameMode) return _frameEdgeInset("left"); + if (frameOnlyNoConnected) + return _frameGapMargin("left"); const barInfo = getBarInfo(); return barInfo.leftBar > 0 ? barInfo.leftBar : Theme.popupDistance; } @@ -425,6 +438,8 @@ PanelWindow { if (connectedFrameMode) return _frameEdgeInset("right"); + if (frameOnlyNoConnected) + return _frameGapMargin("right"); const barInfo = getBarInfo(); return barInfo.rightBar > 0 ? barInfo.rightBar : Theme.popupDistance; } diff --git a/quickshell/Modules/Settings/FrameTab.qml b/quickshell/Modules/Settings/FrameTab.qml index 037b0518..07968840 100644 --- a/quickshell/Modules/Settings/FrameTab.qml +++ b/quickshell/Modules/Settings/FrameTab.qml @@ -314,6 +314,22 @@ Item { opacity: enabled ? 1.0 : 0.5 onToggled: checked => SettingsData.set("frameCloseGaps", checked) } + + SettingsButtonGroupRow { + visible: SettingsData.frameEnabled + settingKey: "frameLauncherEmergeSide" + tags: ["frame", "connected", "launcher", "modal", "emerge", "direction", "bottom", "top"] + text: I18n.tr("Launcher Emerge Side") + description: I18n.tr("Which frame edge the Launcher slides in from. Other modals emerge from the opposite side.") + model: [I18n.tr("Bottom"), I18n.tr("Top")] + currentIndex: SettingsData.frameLauncherEmergeSide === "top" ? 1 : 0 + enabled: SettingsData.connectedFrameModeActive + opacity: enabled ? 1.0 : 0.5 + onSelectionChanged: (index, selected) => { + if (!selected) return; + SettingsData.set("frameLauncherEmergeSide", index === 1 ? "top" : "bottom"); + } + } } // ── Display Assignment ────────────────────────────────────────────