diff --git a/quickshell/Common/ConnectedModeState.qml b/quickshell/Common/ConnectedModeState.qml index fe06cd6f..64685f48 100644 --- a/quickshell/Common/ConnectedModeState.qml +++ b/quickshell/Common/ConnectedModeState.qml @@ -19,7 +19,7 @@ Singleton { }) // Popout state (updated by DankPopout when connectedFrameModeActive) - property string popoutOwnerToken: "" + property string popoutOwnerId: "" property bool popoutVisible: false property string popoutBarSide: "top" property real popoutBodyX: 0 @@ -36,20 +36,20 @@ Singleton { // Dock slide offsets — hot-path updates separated from full geometry state property var dockSlides: ({}) - function hasPopoutOwner(token) { - return !!token && popoutOwnerToken === token; + function hasPopoutOwner(claimId) { + return !!claimId && popoutOwnerId === claimId; } - function claimPopout(token, state) { - if (!token) + function claimPopout(claimId, state) { + if (!claimId) return false; - popoutOwnerToken = token; - return updatePopout(token, state); + popoutOwnerId = claimId; + return updatePopout(claimId, state); } - function updatePopout(token, state) { - if (!hasPopoutOwner(token) || !state) + function updatePopout(claimId, state) { + if (!hasPopoutOwner(claimId) || !state) return false; if (state.visible !== undefined) @@ -74,11 +74,11 @@ Singleton { return true; } - function releasePopout(token) { - if (!hasPopoutOwner(token)) + function releasePopout(claimId) { + if (!hasPopoutOwner(claimId)) return false; - popoutOwnerToken = ""; + popoutOwnerId = ""; popoutVisible = false; popoutBarSide = "top"; popoutBodyX = 0; @@ -150,4 +150,55 @@ Singleton { dockSlides = next; return true; } + + // ─── Notification state (per screen, updated by NotificationSurface) ────── + + readonly property var emptyNotificationState: ({ + "visible": false, + "barSide": "top", + "bodyX": 0, + "bodyY": 0, + "bodyW": 0, + "bodyH": 0 + }) + + property var notificationStates: ({}) + + function _cloneNotificationStates() { + const next = {}; + for (const screenName in notificationStates) + next[screenName] = notificationStates[screenName]; + return next; + } + + function _normalizeNotificationState(state) { + return { + "visible": !!(state && state.visible), + "barSide": state && state.barSide ? state.barSide : "top", + "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) + }; + } + + function setNotificationState(screenName, state) { + if (!screenName || !state) + return false; + + const next = _cloneNotificationStates(); + next[screenName] = _normalizeNotificationState(state); + notificationStates = next; + return true; + } + + function clearNotificationState(screenName) { + if (!screenName || !notificationStates[screenName]) + return false; + + const next = _cloneNotificationStates(); + delete next[screenName]; + notificationStates = next; + return true; + } } diff --git a/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml b/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml index 2f76cc78..2d50403e 100644 --- a/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml +++ b/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml @@ -64,11 +64,19 @@ DankModal { activeImageLoads = 0; shouldHaveFocus = true; ClipboardService.reset(); - if (clipboardAvailable) - ClipboardService.refresh(); keyboardController.reset(); Qt.callLater(function () { + if (clipboardAvailable) { + if (Theme.isConnectedEffect) { + Qt.callLater(() => { + if (clipboardHistoryModal.shouldBeVisible) + ClipboardService.refresh(); + }); + } else { + ClipboardService.refresh(); + } + } if (contentLoader.item?.searchField) { contentLoader.item.searchField.text = ""; contentLoader.item.searchField.forceActiveFocus(); diff --git a/quickshell/Modals/Clipboard/ClipboardHistoryPopout.qml b/quickshell/Modals/Clipboard/ClipboardHistoryPopout.qml index c4f0296f..924a3d18 100644 --- a/quickshell/Modals/Clipboard/ClipboardHistoryPopout.qml +++ b/quickshell/Modals/Clipboard/ClipboardHistoryPopout.qml @@ -53,8 +53,6 @@ DankPopout { open(); activeImageLoads = 0; ClipboardService.reset(); - if (clipboardAvailable) - ClipboardService.refresh(); keyboardController.reset(); Qt.callLater(function () { @@ -121,8 +119,16 @@ DankPopout { onShouldBeVisibleChanged: { if (!shouldBeVisible) return; - if (clipboardAvailable) - ClipboardService.refresh(); + if (clipboardAvailable) { + if (Theme.isConnectedEffect) { + Qt.callLater(() => { + if (root.shouldBeVisible) + ClipboardService.refresh(); + }); + } else { + ClipboardService.refresh(); + } + } keyboardController.reset(); Qt.callLater(function () { if (contentLoader.item?.searchField) { diff --git a/quickshell/Modals/Common/DankModal.qml b/quickshell/Modals/Common/DankModal.qml index adab2fbd..e492657f 100644 --- a/quickshell/Modals/Common/DankModal.qml +++ b/quickshell/Modals/Common/DankModal.qml @@ -26,11 +26,12 @@ Item { property bool closeOnEscapeKey: true property bool closeOnBackgroundClick: true property string animationType: "scale" - property int animationDuration: Theme.modalAnimationDuration + readonly property bool connectedMotionParity: Theme.isConnectedEffect + property int animationDuration: connectedMotionParity ? Theme.popoutAnimationDuration : Theme.modalAnimationDuration property real animationScaleCollapsed: Theme.effectScaleCollapsed property real animationOffset: Theme.effectAnimOffset - property list animationEnterCurve: Theme.variantModalEnterCurve - property list animationExitCurve: Theme.variantModalExitCurve + property list animationEnterCurve: connectedMotionParity ? Theme.variantPopoutEnterCurve : Theme.variantModalEnterCurve + property list animationExitCurve: connectedMotionParity ? Theme.variantPopoutExitCurve : Theme.variantModalExitCurve property color backgroundColor: Theme.surfaceContainer property color borderColor: Theme.outlineMedium property real borderWidth: 0 diff --git a/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml b/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml index df171985..848fa1eb 100644 --- a/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml +++ b/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml @@ -15,7 +15,7 @@ Item { property bool spotlightOpen: false property bool keyboardActive: false property bool contentVisible: false - readonly property bool launcherMotionVisible: Theme.isDirectionalEffect ? spotlightOpen : _motionActive + readonly property bool launcherMotionVisible: Theme.isConnectedEffect ? _motionActive : (Theme.isDirectionalEffect ? spotlightOpen : _motionActive) property var spotlightContent: launcherContentLoader.item property bool openedFromOverview: false property bool isClosing: false @@ -67,6 +67,9 @@ Item { readonly property real modalY: (screenHeight - modalHeight) / 2 readonly property bool connectedSurfaceOverride: Theme.isConnectedEffect + readonly property int launcherAnimationDuration: Theme.isConnectedEffect ? Theme.popoutAnimationDuration : Theme.modalAnimationDuration + readonly property list launcherEnterCurve: Theme.isConnectedEffect ? Theme.variantPopoutEnterCurve : Theme.variantModalEnterCurve + readonly property list launcherExitCurve: Theme.isConnectedEffect ? Theme.variantPopoutExitCurve : Theme.variantModalExitCurve readonly property color backgroundColor: connectedSurfaceOverride ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) readonly property real cornerRadius: connectedSurfaceOverride ? Theme.connectedSurfaceRadius : Theme.cornerRadius readonly property color borderColor: { @@ -271,7 +274,7 @@ Item { Timer { id: closeCleanupTimer - interval: Theme.variantCloseInterval(Theme.modalAnimationDuration) + interval: Theme.variantCloseInterval(root.launcherAnimationDuration) repeat: false onTriggered: { isClosing = false; @@ -394,8 +397,8 @@ Item { Behavior on opacity { enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect) DankAnim { - duration: Math.round(Theme.variantDuration(Theme.modalAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale) - easing.bezierCurve: launcherMotionVisible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve + duration: Math.round(Theme.variantDuration(root.launcherAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale) + easing.bezierCurve: launcherMotionVisible ? root.launcherEnterCurve : root.launcherExitCurve } } } @@ -512,47 +515,32 @@ Item { return -Math.max((root.shadowPad || 0) + Theme.effectAnimOffset, 40); } - // animX/animY are Behavior-animated — DankPopout pattern - property real animX: 0 - property real animY: 0 - property real scaleValue: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed) - - Component.onCompleted: { - animX = Theme.snap(root._motionActive ? 0 : collapsedMotionX, root.dpr); - animY = Theme.snap(root._motionActive ? 0 : collapsedMotionY, root.dpr); - scaleValue = root._motionActive ? 1.0 : (Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed)); - } - - Connections { - target: root - function on_MotionActiveChanged() { - contentContainer.animX = Theme.snap(root._motionActive ? 0 : root._frozenMotionX, root.dpr); - contentContainer.animY = Theme.snap(root._motionActive ? 0 : root._frozenMotionY, root.dpr); - contentContainer.scaleValue = root._motionActive ? 1.0 : (Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed)); - } - } + // Declarative bindings — snap applied at render layer (contentWrapper x/y) + property real animX: root._motionActive ? 0 : root._frozenMotionX + 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)) Behavior on animX { enabled: root.animationsEnabled DankAnim { - duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive) - easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve + duration: Theme.variantDuration(root.launcherAnimationDuration, root._motionActive) + easing.bezierCurve: root._motionActive ? root.launcherEnterCurve : root.launcherExitCurve } } Behavior on animY { enabled: root.animationsEnabled DankAnim { - duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive) - easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve + duration: Theme.variantDuration(root.launcherAnimationDuration, root._motionActive) + easing.bezierCurve: root._motionActive ? root.launcherEnterCurve : root.launcherExitCurve } } Behavior on scaleValue { enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2)) DankAnim { - duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive) - easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve + duration: Theme.variantDuration(root.launcherAnimationDuration, root._motionActive) + easing.bezierCurve: root._motionActive ? root.launcherEnterCurve : root.launcherExitCurve } } @@ -608,8 +596,8 @@ Item { Behavior on opacity { enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect) DankAnim { - duration: Math.round(Theme.variantDuration(Theme.modalAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale) - easing.bezierCurve: launcherMotionVisible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve + duration: Math.round(Theme.variantDuration(root.launcherAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale) + easing.bezierCurve: launcherMotionVisible ? root.launcherEnterCurve : root.launcherExitCurve } } diff --git a/quickshell/Modules/Frame/FrameWindow.qml b/quickshell/Modules/Frame/FrameWindow.qml index e70882b8..47b28b99 100644 --- a/quickshell/Modules/Frame/FrameWindow.qml +++ b/quickshell/Modules/Frame/FrameWindow.qml @@ -112,8 +112,8 @@ PanelWindow { 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 + readonly property real _dyClamp: (ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom") ? Math.max(-ConnectedModeState.popoutBodyH, Math.min(ConnectedModeState.popoutAnimY * 1.02, ConnectedModeState.popoutBodyH)) : 0 + readonly property real _dxClamp: (ConnectedModeState.popoutBarSide === "left" || ConnectedModeState.popoutBarSide === "right") ? Math.max(-ConnectedModeState.popoutBodyW, Math.min(ConnectedModeState.popoutAnimX * 1.02, ConnectedModeState.popoutBodyW)) : 0 x: _active ? ConnectedModeState.popoutBodyX + (ConnectedModeState.popoutBarSide === "right" ? _dxClamp : 0) : 0 y: _active ? ConnectedModeState.popoutBodyY + (ConnectedModeState.popoutBarSide === "bottom" ? _dyClamp : 0) : 0 @@ -218,60 +218,6 @@ PanelWindow { 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 @@ -289,31 +235,11 @@ PanelWindow { // ── 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 + radius: 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 { @@ -343,37 +269,7 @@ PanelWindow { } } - // ─── 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; - } + // ─── Connector position helpers (dock) ───────────────────────────────── function _dockBodyBlurRadius() { return _dockBodyBlurAnchor._active ? Math.max(0, Math.min(win._surfaceRadius, _dockBodyBlurAnchor.width / 2, _dockBodyBlurAnchor.height / 2)) : win._surfaceRadius; @@ -660,56 +556,27 @@ PanelWindow { Item { id: _popoutClip - x: win._popoutClipX() - y: win._popoutClipY() - width: win._popoutClipWidth() - height: win._popoutClipHeight() + readonly property bool _barHoriz: ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom" + // Expand clip by ccr on bar axis to include arc columns + x: win._popoutClipX() - (_barHoriz ? win._effectivePopoutCcr : 0) + y: win._popoutClipY() - (_barHoriz ? 0 : win._effectivePopoutCcr) + width: win._popoutClipWidth() + (_barHoriz ? win._effectivePopoutCcr * 2 : 0) + height: win._popoutClipHeight() + (_barHoriz ? 0 : win._effectivePopoutCcr * 2) 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 + ConnectedShape { + id: _popoutShape + visible: _popoutBodyBlurAnchor._active && _popoutBodyBlurAnchor.width > 0 && _popoutBodyBlurAnchor.height > 0 + barSide: ConnectedModeState.popoutBarSide + bodyWidth: win._popoutClipWidth() + bodyHeight: win._popoutClipHeight() + connectorRadius: win._effectivePopoutCcr + surfaceRadius: win._surfaceRadius + fillColor: win._opaqueSurfaceColor + x: 0 + y: 0 } } - - 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 { diff --git a/quickshell/Modules/Notifications/Center/NotificationCard.qml b/quickshell/Modules/Notifications/Center/NotificationCard.qml index 7e404df5..e83d35e0 100644 --- a/quickshell/Modules/Notifications/Center/NotificationCard.qml +++ b/quickshell/Modules/Notifications/Center/NotificationCard.qml @@ -34,11 +34,12 @@ Rectangle { readonly property real actionButtonHeight: compactMode ? 20 : 24 readonly property real collapsedContentHeight: Math.max(iconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2)) readonly property real baseCardHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing + readonly property bool connectedFrameMode: SettingsData.connectedFrameModeActive width: parent ? parent.width : 400 height: expanded ? (expandedContent.height + cardPadding * 2) : (baseCardHeight + collapsedContent.extraHeight) readonly property real targetHeight: expanded ? (expandedContent.height + cardPadding * 2) : (baseCardHeight + collapsedContent.extraHeight) - radius: Theme.cornerRadius + radius: connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius scale: (cardHoverHandler.hovered ? 1.004 : 1.0) * listLevelAdjacentScaleInfluence readonly property bool shadowsAllowed: Theme.elevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" readonly property var shadowElevation: Theme.elevationLevel1 @@ -100,6 +101,8 @@ Rectangle { if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) { return Theme.primaryHoverLight; } + if (connectedFrameMode) + return Theme.popupLayerColor(Theme.surfaceContainerHigh); return Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency); } border.color: { @@ -959,9 +962,9 @@ Rectangle { Behavior on height { enabled: root.__initialized && root.userInitiatedExpansion && root.animateExpansion NumberAnimation { - duration: root.expanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration + duration: root.connectedFrameMode ? Theme.variantDuration(Theme.popoutAnimationDuration, root.expanded) : (root.expanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration) easing.type: Easing.BezierSpline - easing.bezierCurve: Theme.expressiveCurves.emphasized + easing.bezierCurve: root.connectedFrameMode ? (root.expanded ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : Theme.expressiveCurves.emphasized onRunningChanged: { if (running) { root.isAnimating = true; diff --git a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml index ca36867a..83712555 100644 --- a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml +++ b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml @@ -16,7 +16,7 @@ PanelWindow { blurY: content.y + content.cardInset + swipeTx.y + tx.y blurWidth: !win._finalized ? Math.max(0, content.width - content.cardInset * 2) : 0 blurHeight: !win._finalized ? Math.max(0, content.height - content.cardInset * 2) : 0 - blurRadius: Theme.cornerRadius + blurRadius: SettingsData.connectedFrameModeActive ? Theme.connectedSurfaceRadius : Theme.cornerRadius } WlrLayershell.namespace: "dms:notification-popup" diff --git a/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml b/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml index 02bfc0fe..283f5edf 100644 --- a/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml +++ b/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml @@ -8,11 +8,12 @@ QtObject { property var modelData property int topMargin: 0 readonly property bool compactMode: SettingsData.notificationCompactMode + readonly property bool connectedFrameMode: SettingsData.connectedFrameModeActive readonly property real cardPadding: compactMode ? Theme.notificationCardPaddingCompact : Theme.notificationCardPadding readonly property real popupIconSize: compactMode ? Theme.notificationIconSizeCompact : Theme.notificationIconSizeNormal readonly property real actionButtonHeight: compactMode ? 20 : 24 readonly property real contentSpacing: compactMode ? Theme.spacingXS : Theme.spacingS - readonly property real popupSpacing: compactMode ? 0 : Theme.spacingXS + readonly property real popupSpacing: connectedFrameMode ? 0 : (compactMode ? 0 : Theme.spacingXS) readonly property real collapsedContentHeight: Math.max(popupIconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2)) readonly property int baseNotificationHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing + popupSpacing property var popupWindows: [] diff --git a/quickshell/Widgets/ConnectedCorner.qml b/quickshell/Widgets/ConnectedCorner.qml index 020d9522..25383b2e 100644 --- a/quickshell/Widgets/ConnectedCorner.qml +++ b/quickshell/Widgets/ConnectedCorner.qml @@ -3,6 +3,10 @@ import QtQuick.Shapes import qs.Common // Concave arc connector filling the gap between a bar corner and an adjacent surface. +// +// NOTE: FrameWindow now uses ConnectedShape.qml for frame-owned connected chrome +// (unified single-path rendering). This component is still used by DankPopout's +// own shadow source for non-frame-owned chrome (popouts on non-frame screens). Item { id: root diff --git a/quickshell/Widgets/ConnectedShape.qml b/quickshell/Widgets/ConnectedShape.qml new file mode 100644 index 00000000..a9fb36d8 --- /dev/null +++ b/quickshell/Widgets/ConnectedShape.qml @@ -0,0 +1,286 @@ +import QtQuick +import QtQuick.Shapes +import qs.Common + +// Unified connected silhouette: body + concave arcs as one ShapePath. +// PathArc pattern — 4 arcs + 4 lines, no sibling alignment. + +Item { + id: root + + property string barSide: "top" + + property real bodyWidth: 0 + property real bodyHeight: 0 + + property real connectorRadius: 12 + + property real surfaceRadius: 12 + + property color fillColor: "transparent" + + // ── Derived layout ── + readonly property bool _horiz: barSide === "top" || barSide === "bottom" + readonly property real _cr: Math.max(0, connectorRadius) + readonly property real _sr: Math.max(0, Math.min(surfaceRadius, (_horiz ? bodyWidth : bodyHeight) / 2, (_horiz ? bodyHeight : bodyWidth) / 2)) + + // Root-level aliases — PathArc/PathLine elements can't use `parent`. + readonly property real _bw: bodyWidth + readonly property real _bh: bodyHeight + readonly property real _totalW: _horiz ? _bw + _cr * 2 : _bw + readonly property real _totalH: _horiz ? _bh : _bh + _cr * 2 + + width: _totalW + height: _totalH + + readonly property real bodyX: _horiz ? _cr : 0 + readonly property real bodyY: _horiz ? 0 : _cr + + Shape { + anchors.fill: parent + asynchronous: false + preferredRendererType: Shape.CurveRenderer + + ShapePath { + fillColor: root.fillColor + strokeWidth: -1 + fillRule: ShapePath.WindingFill + + // CW path: bar edge → concave arc → body → convex arc → far edge → convex arc → body → concave arc + + startX: root.barSide === "right" ? root._totalW : 0 + startY: { + switch (root.barSide) { + case "bottom": + return root._totalH; + case "left": + return root._totalH; + case "right": + return 0; + default: + return 0; + } + } + + // Bar edge + PathLine { + x: { + switch (root.barSide) { + case "left": + return 0; + case "right": + return root._totalW; + default: + return root._totalW; + } + } + y: { + switch (root.barSide) { + case "bottom": + return root._totalH; + case "left": + return 0; + case "right": + return root._totalH; + default: + return 0; + } + } + } + + // Concave arc 1 + PathArc { + relativeX: { + switch (root.barSide) { + case "left": + return root._cr; + case "right": + return -root._cr; + default: + return -root._cr; + } + } + relativeY: { + switch (root.barSide) { + case "bottom": + return -root._cr; + case "left": + return root._cr; + case "right": + return -root._cr; + default: + return root._cr; + } + } + radiusX: root._cr + radiusY: root._cr + direction: PathArc.Counterclockwise + } + + // Body edge to first convex corner + PathLine { + x: { + switch (root.barSide) { + case "left": + return root._bw - root._sr; + case "right": + return root._sr; + default: + return root._totalW - root._cr; + } + } + y: { + switch (root.barSide) { + case "bottom": + return root._sr; + case "left": + return root._cr; + case "right": + return root._cr + root._bh; + default: + return root._totalH - root._sr; + } + } + } + + // Convex arc 1 + PathArc { + relativeX: { + switch (root.barSide) { + case "left": + return root._sr; + case "right": + return -root._sr; + default: + return -root._sr; + } + } + relativeY: { + switch (root.barSide) { + case "bottom": + return -root._sr; + case "left": + return root._sr; + case "right": + return -root._sr; + default: + return root._sr; + } + } + radiusX: root._sr + radiusY: root._sr + direction: PathArc.Clockwise + } + + // Far edge + PathLine { + x: { + switch (root.barSide) { + case "left": + return root._bw; + case "right": + return 0; + default: + return root._cr + root._sr; + } + } + y: { + switch (root.barSide) { + case "bottom": + return 0; + case "left": + return root._cr + root._bh - root._sr; + case "right": + return root._cr + root._sr; + default: + return root._totalH; + } + } + } + + // Convex arc 2 + PathArc { + relativeX: { + switch (root.barSide) { + case "left": + return -root._sr; + case "right": + return root._sr; + default: + return -root._sr; + } + } + relativeY: { + switch (root.barSide) { + case "bottom": + return root._sr; + case "left": + return root._sr; + case "right": + return -root._sr; + default: + return -root._sr; + } + } + radiusX: root._sr + radiusY: root._sr + direction: PathArc.Clockwise + } + + // Body edge to second concave arc + PathLine { + x: { + switch (root.barSide) { + case "left": + return root._cr; + case "right": + return root._bw - root._cr; + default: + return root._cr; + } + } + y: { + switch (root.barSide) { + case "bottom": + return root._totalH - root._cr; + case "left": + return root._cr + root._bh; + case "right": + return root._cr; + default: + return root._cr; + } + } + } + + // Concave arc 2 + PathArc { + relativeX: { + switch (root.barSide) { + case "left": + return -root._cr; + case "right": + return root._cr; + default: + return -root._cr; + } + } + relativeY: { + switch (root.barSide) { + case "bottom": + return root._cr; + case "left": + return root._cr; + case "right": + return -root._cr; + default: + return -root._cr; + } + } + radiusX: root._cr + radiusY: root._cr + direction: PathArc.Counterclockwise + } + } + } +} diff --git a/quickshell/Widgets/DankPopout.qml b/quickshell/Widgets/DankPopout.qml index 518c1448..0f2f5930 100644 --- a/quickshell/Widgets/DankPopout.qml +++ b/quickshell/Widgets/DankPopout.qml @@ -34,7 +34,7 @@ Item { property bool _resizeActive: false property real _surfaceMarginLeft: 0 property real _surfaceW: 0 - property string _connectedChromeToken: "" + property string _chromeClaimId: "" property int _connectedChromeSerial: 0 property real storedBarThickness: Theme.barHeight - 4 @@ -153,7 +153,7 @@ Item { setBarContext(pos, bottomGap); } - function _nextConnectedChromeToken() { + function _nextChromeClaimId() { _connectedChromeSerial += 1; return layerNamespace + ":" + _connectedChromeSerial + ":" + (new Date()).getTime(); } @@ -174,21 +174,21 @@ Item { } function _publishConnectedChromeState(forceClaim, visibleOverride) { - if (!SettingsData.connectedFrameModeActive || !root.screen || !_connectedChromeToken) + if (!root.frameOwnsConnectedChrome || !root.screen || !_chromeClaimId) return; const state = _connectedChromeState(visibleOverride); - if (forceClaim || !ConnectedModeState.hasPopoutOwner(_connectedChromeToken)) { - ConnectedModeState.claimPopout(_connectedChromeToken, state); + if (forceClaim || !ConnectedModeState.hasPopoutOwner(_chromeClaimId)) { + ConnectedModeState.claimPopout(_chromeClaimId, state); } else { - ConnectedModeState.updatePopout(_connectedChromeToken, state); + ConnectedModeState.updatePopout(_chromeClaimId, state); } } function _releaseConnectedChromeState() { - if (_connectedChromeToken) - ConnectedModeState.releasePopout(_connectedChromeToken); - _connectedChromeToken = ""; + if (_chromeClaimId) + ConnectedModeState.releasePopout(_chromeClaimId); + _chromeClaimId = ""; } // ─── Exposed animation state for ConnectedModeState ──────────────────── @@ -197,7 +197,7 @@ Item { // ─── ConnectedModeState sync ──────────────────────────────────────────── function _syncPopoutChromeState() { - if (!SettingsData.connectedFrameModeActive) { + if (!root.frameOwnsConnectedChrome) { _releaseConnectedChromeState(); return; } @@ -207,9 +207,9 @@ Item { } if (!contentWindow.visible && !shouldBeVisible) return; - if (!_connectedChromeToken) - _connectedChromeToken = _nextConnectedChromeToken(); - _publishConnectedChromeState(contentWindow.visible && !ConnectedModeState.hasPopoutOwner(_connectedChromeToken)); + if (!_chromeClaimId) + _chromeClaimId = _nextChromeClaimId(); + _publishConnectedChromeState(contentWindow.visible && !ConnectedModeState.hasPopoutOwner(_chromeClaimId)); } onAlignedXChanged: _syncPopoutChromeState() @@ -233,10 +233,10 @@ Item { Connections { target: SettingsData function onConnectedFrameModeActiveChanged() { - if (SettingsData.connectedFrameModeActive) { + if (root.frameOwnsConnectedChrome) { if (contentWindow.visible || root.shouldBeVisible) { - if (!root._connectedChromeToken) - root._connectedChromeToken = root._nextConnectedChromeToken(); + if (!root._chromeClaimId) + root._chromeClaimId = root._nextChromeClaimId(); root._publishConnectedChromeState(true); } } else { @@ -246,6 +246,9 @@ Item { } readonly property bool useBackgroundWindow: !CompositorService.isHyprland || CompositorService.useHyprlandFocusGrab + readonly property bool frameOwnsConnectedChrome: SettingsData.connectedFrameModeActive + && !!root.screen + && SettingsData.isScreenInPreferences(root.screen, SettingsData.frameScreenPreferences) function updateSurfacePosition() { if (useBackgroundWindow && shouldBeVisible) { @@ -282,11 +285,11 @@ Item { contentContainer.scaleValue = root.animationScaleCollapsed; } - if (SettingsData.connectedFrameModeActive) { - _connectedChromeToken = _nextConnectedChromeToken(); + if (root.frameOwnsConnectedChrome) { + _chromeClaimId = _nextChromeClaimId(); _publishConnectedChromeState(true, true); } else { - _connectedChromeToken = ""; + _chromeClaimId = ""; } if (useBackgroundWindow) { @@ -641,7 +644,7 @@ Item { WindowBlur { id: popoutBlur targetWindow: contentWindow - blurEnabled: root.effectiveSurfaceBlurEnabled && !SettingsData.connectedFrameModeActive + blurEnabled: root.effectiveSurfaceBlurEnabled && !root.frameOwnsConnectedChrome readonly property real s: Math.min(1, contentContainer.scaleValue) readonly property bool trackBlurFromBarEdge: Theme.isConnectedEffect || (typeof SettingsData !== "undefined" && Theme.isDirectionalEffect && SettingsData.directionalAnimationMode !== 2) @@ -984,7 +987,7 @@ Item { Item { anchors.fill: parent - visible: Theme.isConnectedEffect && !SettingsData.connectedFrameModeActive + visible: Theme.isConnectedEffect && !root.frameOwnsConnectedChrome clip: false Rectangle {