diff --git a/quickshell/Common/ConnectedModeState.qml b/quickshell/Common/ConnectedModeState.qml index 7e63bea3..00aa7639 100644 --- a/quickshell/Common/ConnectedModeState.qml +++ b/quickshell/Common/ConnectedModeState.qml @@ -200,8 +200,6 @@ Singleton { return true; } - // ─── Notification state (per screen, updated by NotificationSurface) ────── - readonly property var emptyNotificationState: ({ "visible": false, "barSide": "top", @@ -369,8 +367,6 @@ Singleton { return true; } - // ─── Dock retract coordination ──────────────────────────────── - property var dockRetractRequests: ({}) function requestDockRetract(requesterId, screenName, side) { @@ -407,4 +403,64 @@ Singleton { } return false; } + + // Prune state for screens that are no longer connected. Stale entries + // accumulate across hotplug cycles otherwise — Frame's per-screen + // FrameInstance doesn't notice when its peer dicts go orphan. + function _pruneToLiveScreens() { + const live = {}; + const screens = Quickshell.screens || []; + for (let i = 0; i < screens.length; i++) { + const s = screens[i]; + if (s && s.name) + live[s.name] = true; + } + + function pruneKeyed(dict) { + let changed = false; + const next = {}; + for (const k in dict) { + if (live[k]) + next[k] = dict[k]; + else + changed = true; + } + return changed ? next : null; + } + + const nextDock = pruneKeyed(dockStates); + if (nextDock !== null) + dockStates = nextDock; + const nextSlides = pruneKeyed(dockSlides); + if (nextSlides !== null) + dockSlides = nextSlides; + const nextNotif = pruneKeyed(notificationStates); + if (nextNotif !== null) + notificationStates = nextNotif; + const nextModal = pruneKeyed(modalStates); + if (nextModal !== null) + modalStates = nextModal; + + let retractChanged = false; + const nextRetract = {}; + for (const k in dockRetractRequests) { + const r = dockRetractRequests[k]; + if (r && live[r.screenName]) + nextRetract[k] = r; + else + retractChanged = true; + } + if (retractChanged) + dockRetractRequests = nextRetract; + + if (popoutOwnerId && popoutScreen && !live[popoutScreen]) + releasePopout(popoutOwnerId); + } + + Connections { + target: Quickshell + function onScreensChanged() { + root._pruneToLiveScreens(); + } + } } diff --git a/quickshell/Common/Theme.qml b/quickshell/Common/Theme.qml index a31d700c..cd7cecbd 100644 --- a/quickshell/Common/Theme.qml +++ b/quickshell/Common/Theme.qml @@ -986,7 +986,6 @@ Singleton { "expressiveEffects": [0.34, 0.8, 0.34, 1, 1, 1] } - // ─── Animation variant proxy ────────────────────────────────────────────── // Theme is the canonical access point for animation variant state. The // aliases below forward to AnimVariants.qml so consumers don't need two // imports. ~200 call sites read through Theme.variantEnterCurve / diff --git a/quickshell/Modals/Common/DankModal.qml b/quickshell/Modals/Common/DankModal.qml index 7976932c..11e74a6d 100644 --- a/quickshell/Modals/Common/DankModal.qml +++ b/quickshell/Modals/Common/DankModal.qml @@ -84,9 +84,31 @@ Item { impl.item.toggle(); } + readonly property var _desiredBackend: SettingsData.connectedFrameModeActive ? connectedComp : standaloneComp + property var _resolvedBackend: null + + Component.onCompleted: _resolvedBackend = _desiredBackend + + Connections { + target: SettingsData + function onConnectedFrameModeActiveChanged() { + root._maybeResolveBackend(); + } + } + + // Defer Loader source-component swap until impl is fully closed; avoids + // tearing down a modal mid-animation when frame mode is toggled. + function _maybeResolveBackend() { + if (_resolvedBackend === _desiredBackend) + return; + if (impl.item && (impl.item.shouldBeVisible || impl.item.isClosing)) + return; + _resolvedBackend = _desiredBackend; + } + Loader { id: impl - sourceComponent: SettingsData.connectedFrameModeActive ? connectedComp : standaloneComp + sourceComponent: root._resolvedBackend onItemChanged: if (item) root._wireBackend(item) } @@ -137,20 +159,7 @@ Item { it.useOverlayLayer = Qt.binding(() => root.useOverlayLayer); it.shouldBeVisible = root.shouldBeVisible; - it.shouldBeVisibleChanged.connect(function () { - if (root.shouldBeVisible !== it.shouldBeVisible) - root.shouldBeVisible = it.shouldBeVisible; - }); - it.shouldHaveFocus = root.shouldHaveFocus; - it.shouldHaveFocusChanged.connect(function () { - if (root.shouldHaveFocus !== it.shouldHaveFocus) - root.shouldHaveFocus = it.shouldHaveFocus; - }); - - it.opened.connect(root.opened); - it.dialogClosed.connect(root.dialogClosed); - it.backgroundClicked.connect(root.backgroundClicked); if (it.modalFocusScope) _modalFocusScope.parent = it.modalFocusScope; @@ -167,4 +176,32 @@ Item { impl.item.shouldHaveFocus = root.shouldHaveFocus; } } + + Connections { + target: impl.item + ignoreUnknownSignals: true + + function onShouldBeVisibleChanged() { + if (impl.item && root.shouldBeVisible !== impl.item.shouldBeVisible) + root.shouldBeVisible = impl.item.shouldBeVisible; + } + + function onShouldHaveFocusChanged() { + if (impl.item && root.shouldHaveFocus !== impl.item.shouldHaveFocus) + root.shouldHaveFocus = impl.item.shouldHaveFocus; + } + + function onOpened() { + root.opened(); + } + + function onDialogClosed() { + root.dialogClosed(); + root._maybeResolveBackend(); + } + + function onBackgroundClicked() { + root.backgroundClicked(); + } + } } diff --git a/quickshell/Modals/Common/DankModalConnected.qml b/quickshell/Modals/Common/DankModalConnected.qml index 27d755f5..4329e7a4 100644 --- a/quickshell/Modals/Common/DankModalConnected.qml +++ b/quickshell/Modals/Common/DankModalConnected.qml @@ -95,7 +95,6 @@ Item { property bool animationsEnabled: true - // ─── Connected chrome sync ──────────────────────────────────────────────── property string _chromeClaimId: "" property bool _fullSyncPending: false @@ -341,8 +340,6 @@ Item { readonly property real shadowMotionPadding: { if (Theme.isConnectedEffect) return 0; - if (Theme.isDirectionalEffect) - return 0; if (animationType === "slide") return 30; if (Theme.isDirectionalEffect) diff --git a/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml b/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml index b6dc7380..9e570daf 100644 --- a/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml +++ b/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml @@ -60,9 +60,31 @@ Item { impl.item.toggleWithMode(mode); } + readonly property var _desiredBackend: SettingsData.connectedFrameModeActive ? connectedComp : standaloneComp + property var _resolvedBackend: null + + Component.onCompleted: _resolvedBackend = _desiredBackend + + Connections { + target: SettingsData + function onConnectedFrameModeActiveChanged() { + root._maybeResolveBackend(); + } + } + + // Defer Loader source-component swap until impl is fully closed; avoids + // tearing down the launcher mid-animation when frame mode is toggled. + function _maybeResolveBackend() { + if (_resolvedBackend === _desiredBackend) + return; + if (impl.item && (impl.item.spotlightOpen || impl.item.isClosing)) + return; + _resolvedBackend = _desiredBackend; + } + Loader { id: impl - sourceComponent: SettingsData.connectedFrameModeActive ? connectedComp : standaloneComp + sourceComponent: root._resolvedBackend onItemChanged: if (item) root._wireBackend(item) } @@ -81,6 +103,15 @@ Item { if (!it) return; it.modalHandle = root; - it.dialogClosed.connect(root.dialogClosed); + } + + Connections { + target: impl.item + ignoreUnknownSignals: true + + function onDialogClosed() { + root.dialogClosed(); + root._maybeResolveBackend(); + } } } diff --git a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalConnected.qml b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalConnected.qml index 859e9398..ad07037c 100644 --- a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalConnected.qml +++ b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalConnected.qml @@ -191,7 +191,6 @@ Item { signal dialogClosed - // ─── Connected chrome sync ──────────────────────────────────────────────── property string _chromeClaimId: "" property bool _fullSyncPending: false @@ -558,7 +557,6 @@ Item { } } - // ── Background window: fullscreen, handles darkening + click-to-dismiss ── PanelWindow { id: backgroundWindow visible: false @@ -616,7 +614,6 @@ Item { } } - // ── Content window: SMALL, positioned with margins — only renders the modal area ── PanelWindow { id: contentWindow visible: false diff --git a/quickshell/Modules/Dock/Dock.qml b/quickshell/Modules/Dock/Dock.qml index 9f65a6b2..a3ca6e97 100644 --- a/quickshell/Modules/Dock/Dock.qml +++ b/quickshell/Modules/Dock/Dock.qml @@ -145,7 +145,6 @@ Variants { return Math.round(v * _dpr) / _dpr; } - // ─── ConnectedModeState sync ──────────────────────────────────────── // Dock window origin in screen-relative coordinates (FrameWindow space). function _dockWindowOriginX() { if (!dock.isVertical) diff --git a/quickshell/Modules/Frame/FrameWindow.qml b/quickshell/Modules/Frame/FrameWindow.qml index 7ab49cce..a7aec771 100644 --- a/quickshell/Modules/Frame/FrameWindow.qml +++ b/quickshell/Modules/Frame/FrameWindow.qml @@ -49,7 +49,6 @@ PanelWindow { 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 readonly property string _barSide: { const edges = win.barEdges; @@ -780,7 +779,6 @@ PanelWindow { radius: win._blurCutoutRadius } - // ── Connected popout blur regions ── Region { item: _popoutBodyBlurAnchor radius: win._surfaceRadius @@ -827,7 +825,6 @@ PanelWindow { } } - // ── Connected dock blur regions ── Region { item: _dockBodyBlurAnchor radius: win._dockBodyBlurRadius() @@ -898,7 +895,6 @@ PanelWindow { } } - // ── Connected modal blur regions ── Region { item: _modalBodyBlurAnchor radius: win._surfaceRadius @@ -946,8 +942,6 @@ PanelWindow { } } - // ─── Connector position helpers ──────────────────────────────────────── - function _dockBodyBlurRadius() { return _dockBodyBlurAnchor._active ? Math.max(0, Math.min(win._surfaceRadius, _dockBodyBlurAnchor.width / 2, _dockBodyBlurAnchor.height / 2)) : win._surfaceRadius; } @@ -1219,8 +1213,6 @@ PanelWindow { return (arcCorner === "topLeft" || arcCorner === "topRight") ? connectorY - r : connectorY + connectorHeight - r; } - // ─── Blur build / teardown ──────────────────────────────────────────────── - function _buildBlur() { try { if (!BlurService.enabled || !SettingsData.frameBlurEnabled || !win._frameActive || !win.visible) { @@ -1297,8 +1289,6 @@ PanelWindow { Component.onCompleted: Qt.callLater(() => win._buildBlur()) Component.onDestruction: win._teardownBlur() - // ─── Frame border ───────────────────────────────────────────────────────── - FrameBorder { anchors.fill: parent visible: win._frameActive && !win._connectedActive @@ -1309,8 +1299,6 @@ PanelWindow { cutoutRadius: win.cutoutRadius } - // ─── Connected chrome fills ─────────────────────────────────────────────── - Item { id: _connectedSurfaceLayer anchors.fill: parent diff --git a/quickshell/Modules/WorkspaceOverlays/HyprlandOverview.qml b/quickshell/Modules/WorkspaceOverlays/HyprlandOverview.qml index d0f3efc9..eeac0c94 100644 --- a/quickshell/Modules/WorkspaceOverlays/HyprlandOverview.qml +++ b/quickshell/Modules/WorkspaceOverlays/HyprlandOverview.qml @@ -130,8 +130,8 @@ Scope { MouseArea { anchors.fill: parent onClicked: mouse => { - const localPos = mapToItem(contentContainer, mouse.x, mouse.y); - if (localPos.x < 0 || localPos.x > contentContainer.width || localPos.y < 0 || localPos.y > contentContainer.height) { + const localPos = mapToItem(contentAnchor, mouse.x, mouse.y); + if (localPos.x < 0 || localPos.x > contentAnchor.width || localPos.y < 0 || localPos.y > contentAnchor.height) { overviewScope.overviewOpen = false; closeTimer.restart(); } @@ -140,47 +140,24 @@ Scope { } Item { - id: contentContainer + id: contentAnchor anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.topMargin: 100 - width: childrenRect.width - height: childrenRect.height + width: contentContainer.width + height: contentContainer.height - opacity: overviewScope.overviewOpen ? 1 : 0 - transform: [scaleTransform, motionTransform] + Item { + id: contentContainer + width: childrenRect.width + height: childrenRect.height + transformOrigin: Item.Center - Scale { - id: scaleTransform - origin.x: contentContainer.width / 2 - origin.y: contentContainer.height / 2 - xScale: overviewScope.overviewOpen ? 1 : Theme.effectScaleCollapsed - yScale: overviewScope.overviewOpen ? 1 : Theme.effectScaleCollapsed - - Behavior on xScale { - NumberAnimation { - duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen) - easing.type: Easing.BezierSpline - easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve - } - } - - Behavior on yScale { - NumberAnimation { - duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen) - easing.type: Easing.BezierSpline - easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve - } - } - } - - Translate { - id: motionTransform + opacity: overviewScope.overviewOpen ? 1 : 0 + scale: overviewScope.overviewOpen ? 1 : Theme.effectScaleCollapsed x: { if (overviewScope.overviewOpen) return 0; - if (Theme.isDirectionalEffect) - return 0; if (Theme.isDepthEffect) return Theme.effectAnimOffset * 0.25; return 0; @@ -195,8 +172,24 @@ Scope { return Theme.effectAnimOffset; } + Behavior on opacity { + OpacityAnimator { + duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen) + easing.type: Easing.BezierSpline + easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve + } + } + + Behavior on scale { + ScaleAnimator { + duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen) + easing.type: Easing.BezierSpline + easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve + } + } + Behavior on x { - NumberAnimation { + XAnimator { duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen) easing.type: Easing.BezierSpline easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve @@ -204,30 +197,22 @@ Scope { } Behavior on y { - NumberAnimation { + YAnimator { duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen) easing.type: Easing.BezierSpline easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve } } - } - Behavior on opacity { - OpacityAnimator { - duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen) - easing.type: Easing.BezierSpline - easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve - } - } + Loader { + id: overviewLoader + active: overviewScope.overviewOpen + asynchronous: false - Loader { - id: overviewLoader - active: overviewScope.overviewOpen - asynchronous: false - - sourceComponent: OverviewWidget { - panelWindow: root - overviewOpen: overviewScope.overviewOpen + sourceComponent: OverviewWidget { + panelWindow: root + overviewOpen: overviewScope.overviewOpen + } } } } diff --git a/quickshell/Widgets/ConnectedShape.qml b/quickshell/Widgets/ConnectedShape.qml index 82a822c1..4622a8b3 100644 --- a/quickshell/Widgets/ConnectedShape.qml +++ b/quickshell/Widgets/ConnectedShape.qml @@ -23,7 +23,6 @@ Item { property color fillColor: "transparent" - // ── Derived layout ── readonly property bool _horiz: barSide === "top" || barSide === "bottom" readonly property real _sc: Math.max(0, startConnectorRadius) readonly property real _ec: Math.max(0, endConnectorRadius) diff --git a/quickshell/Widgets/DankPopout.qml b/quickshell/Widgets/DankPopout.qml index 9b527ec9..ed6279d7 100644 --- a/quickshell/Widgets/DankPopout.qml +++ b/quickshell/Widgets/DankPopout.qml @@ -75,6 +75,21 @@ Item { readonly property real barWidth: impl.item ? impl.item.barWidth : 0 readonly property real barHeight: impl.item ? impl.item.barHeight : 0 readonly property bool useConnectedBackend: SettingsData.connectedFrameModeActive && !!screen && SettingsData.isScreenInPreferences(screen, SettingsData.frameScreenPreferences) + readonly property var _desiredBackend: useConnectedBackend ? connectedComp : standaloneComp + property var _resolvedBackend: null + + onUseConnectedBackendChanged: _maybeResolveBackend() + Component.onCompleted: _resolvedBackend = _desiredBackend + + // Defer Loader source-component swap until impl is fully closed; avoids + // tearing down a popout mid-animation when frame mode is toggled. + function _maybeResolveBackend() { + if (_resolvedBackend === _desiredBackend) + return; + if (impl.item && (impl.item.shouldBeVisible || impl.item.isClosing)) + return; + _resolvedBackend = _desiredBackend; + } function open() { if (impl.item) @@ -121,7 +136,7 @@ Item { Loader { id: impl active: root.screen !== null - sourceComponent: root.useConnectedBackend ? connectedComp : standaloneComp + sourceComponent: root._resolvedBackend onItemChanged: if (item) root._wireBackend(item) } @@ -207,6 +222,7 @@ Item { function onPopoutClosed() { root.popoutClosed(); + root._maybeResolveBackend(); } function onBackgroundClicked() { diff --git a/quickshell/Widgets/DankPopoutStandalone.qml b/quickshell/Widgets/DankPopoutStandalone.qml index d0cbd9f7..220d6b00 100644 --- a/quickshell/Widgets/DankPopoutStandalone.qml +++ b/quickshell/Widgets/DankPopoutStandalone.qml @@ -178,15 +178,40 @@ Item { setBarContext(pos, bottomGap); } + // Briefly forces backgroundWindow.updatesEnabled true while the surface + // body changes, so the contentHoleRect mask carve-out commits to the + // compositor — otherwise the input region stays stuck at the popup's + // initial size and clicks in any newly-grown area dismiss the popup. + // Cleared by the frameSwapped Connections below as soon as the dirty + // frame ships, so the bg window goes back to skipping buffer updates. + property bool _bgCommitWindow: false + function _setSurfaceGeometry(bodyX, bodyY, bodyW, bodyH) { - _surfaceBodyX = Theme.snap(bodyX, dpr); - _surfaceBodyY = Theme.snap(bodyY, dpr); - _surfaceBodyW = Theme.snap(bodyW, dpr); - _surfaceBodyH = Theme.snap(bodyH, dpr); + const newX = Theme.snap(bodyX, dpr); + const newY = Theme.snap(bodyY, dpr); + const newW = Theme.snap(bodyW, dpr); + const newH = Theme.snap(bodyH, dpr); + const changed = newX !== _surfaceBodyX || newY !== _surfaceBodyY || newW !== _surfaceBodyW || newH !== _surfaceBodyH; + _surfaceBodyX = newX; + _surfaceBodyY = newY; + _surfaceBodyW = newW; + _surfaceBodyH = newH; _surfaceMarginLeft = _surfaceBodyX - shadowBuffer; _surfaceMarginTop = _surfaceBodyY - shadowBuffer; _surfaceW = _surfaceBodyW + shadowBuffer * 2; _surfaceH = _surfaceBodyH + shadowBuffer * 2; + if (changed && backgroundWindow.visible) { + _bgCommitWindow = true; + } + } + + Connections { + target: backgroundWindow + ignoreUnknownSignals: true + function onFrameSwapped() { + if (root._bgCommitWindow) + root._bgCommitWindow = false; + } } function _setSettledSurfaceGeometry() { @@ -455,9 +480,10 @@ Item { screen: root.screen visible: false color: "transparent" - // When there's no overlay to render, skip buffer updates. Re-evaluates if - // overlayContent is assigned later (e.g., via a dispatcher forwarding it). - updatesEnabled: root.overlayContent !== null + // Skip buffer updates when there's nothing to render. Briefly flipped + // true via _bgCommitWindow when _surfaceBodyW/H changes so the + // contentHoleRect mask carve-out actually commits to the compositor. + updatesEnabled: root.overlayContent !== null || root._bgCommitWindow WlrLayershell.namespace: root.layerNamespace + ":background" WlrLayershell.layer: WlrLayershell.Top