diff --git a/quickshell/Common/ConnectedModalChrome.qml b/quickshell/Common/ConnectedModalChrome.qml index e11ef30a..5ab2b750 100644 --- a/quickshell/Common/ConnectedModalChrome.qml +++ b/quickshell/Common/ConnectedModalChrome.qml @@ -7,6 +7,7 @@ Item { required property var modalHandle required property string claimPrefix + property string surfaceKind: "modal" property string screenName: "" property bool enabled: false property bool active: false @@ -14,112 +15,97 @@ Item { property bool dockBlocked: false property string dockSide: "" - property string claimId: "" - property string claimedScreenName: "" + property alias claimId: lease.claimId + property alias claimedScreenName: lease.claimedScreenName signal recoveryRequested visible: false - function _nextClaimId() { - return claimPrefix + ":" + (new Date()).getTime() + ":" + Math.floor(Math.random() * 1000); - } - function _isCurrentModal(name) { return !!name && ModalManager.isCurrentModal(modalHandle, name); } - function _shouldRecover() { - return active && enabled && _isCurrentModal(screenName); - } - - function _requestRecovery() { - if (_shouldRecover()) - recoveryRequested(); + ConnectedSurfaceLease { + id: lease + claimPrefix: root.claimPrefix + screenName: root.screenName + enabled: root.enabled + active: root.active + presented: root.presented + dockBlocked: root.dockBlocked + dockSide: root.dockSide + isCurrentOwner: function(name) { + return root._isCurrentModal(name); + } + hasOwner: function(name, ownerId) { + return ConnectedModeState.hasModalOwner(name, ownerId); + } + statePresent: function(name, ownerId) { + return ConnectedModeState.hasModalOwner(name, ownerId) && ConnectedModeState.hasSurfaceDescriptor(name, root.surfaceKind, ownerId); + } + claimState: function(name, state, ownerId) { + return ConnectedModeState.claimModalState(name, state, ownerId); + } + ensureState: function(name, state, ownerId) { + return ConnectedModeState.ensureModalState(name, state, ownerId); + } + releaseState: function(name, ownerId) { + return ConnectedModeState.clearModalState(name, ownerId); + } + updateAnimationState: function(name, ownerId, animX, animY) { + return ConnectedModeState.setModalAnim(name, animX, animY, ownerId); + } + updateBodyState: function(name, ownerId, bodyX, bodyY, bodyW, bodyH) { + return ConnectedModeState.setModalBody(name, bodyX, bodyY, bodyW, bodyH, ownerId); + } + requestDockRetract: function(ownerId, name, side) { + return ConnectedModeState.requestDockRetract(ownerId, name, side); + } + releaseDockRetract: function(ownerId) { + return ConnectedModeState.releaseDockRetract(ownerId); + } + onRecoveryRequested: root.recoveryRequested() } function publish(state) { - if (!enabled || !screenName || !state) { - release(); - return false; - } - if (claimedScreenName && claimedScreenName !== screenName) - release(); - - const isCurrent = _isCurrentModal(screenName); - let isClaim = !claimId; - if (isClaim && !isCurrent) - return false; - if (isClaim) - claimId = _nextClaimId(); - - let published = isClaim ? ConnectedModeState.claimModalState(screenName, state, claimId) : ConnectedModeState.ensureModalState(screenName, state, claimId); - if (!published && !isClaim && isCurrent) { - ConnectedModeState.releaseDockRetract(claimId); - claimId = _nextClaimId(); - published = ConnectedModeState.claimModalState(screenName, state, claimId); - } - if (!published) - return false; - - claimedScreenName = screenName; - if (dockBlocked && presented) - ConnectedModeState.requestDockRetract(claimId, screenName, dockSide); - else - ConnectedModeState.releaseDockRetract(claimId); - return true; + return lease.publish(Object.assign({}, state, { + "kind": root.surfaceKind, + "screenName": root.screenName, + "presented": root.presented, + "dockRetractSide": root.dockBlocked ? root.dockSide : "" + }), false); } function updateAnim(animX, animY) { - if (!enabled || !claimId || !claimedScreenName) - return false; - if (!ConnectedModeState.hasModalOwner(claimedScreenName, claimId)) { - _requestRecovery(); - return false; - } - return ConnectedModeState.setModalAnim(claimedScreenName, animX, animY, claimId); + return lease.updateAnim(animX, animY); } function updateBody(bodyX, bodyY, bodyW, bodyH) { - if (!enabled || !claimId || !claimedScreenName) - return false; - if (!ConnectedModeState.hasModalOwner(claimedScreenName, claimId)) { - _requestRecovery(); - return false; - } - return ConnectedModeState.setModalBody(claimedScreenName, bodyX, bodyY, bodyW, bodyH, claimId); + return lease.updateBody(bodyX, bodyY, bodyW, bodyH); } function release() { - if (!claimId) - return; - ConnectedModeState.releaseDockRetract(claimId); - const releasedClaimId = claimId; - const releasedScreenName = claimedScreenName; - claimId = ""; - claimedScreenName = ""; - if (releasedScreenName) - ConnectedModeState.clearModalState(releasedScreenName, releasedClaimId); + return lease.release(); } - Component.onDestruction: release() - Connections { target: ModalManager function onModalChanged() { - root._requestRecovery(); + lease.requestRecovery(); } } Connections { target: ConnectedModeState function onModalOwnersChanged() { - if (!ConnectedModeState.hasModalOwner(root.screenName, root.claimId)) - root._requestRecovery(); + lease.checkOwnershipRecovery(); } function onModalStatesChanged() { - if (!ConnectedModeState.modalStates[root.screenName]) - root._requestRecovery(); + lease.checkStateRecovery(); + } + function onSurfaceDescriptorsChanged() { + lease.checkStateRecovery(); } } } diff --git a/quickshell/Common/ConnectedModeState.qml b/quickshell/Common/ConnectedModeState.qml index 80e5353b..e62e3e4a 100644 --- a/quickshell/Common/ConnectedModeState.qml +++ b/quickshell/Common/ConnectedModeState.qml @@ -3,10 +3,141 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell +import "ConnectedSurfaceDescriptor.js" as SurfaceDescriptor Singleton { id: root + property var surfaceDescriptors: ({}) + + function _surfaceSlot(kind) { + return SurfaceDescriptor.slotForKind(kind); + } + + function surfaceDescriptor(screenName, kind) { + const slot = _surfaceSlot(kind); + const screenDescriptors = screenName ? surfaceDescriptors[screenName] : null; + const descriptor = screenDescriptors && screenDescriptors[slot] ? screenDescriptors[slot] : SurfaceDescriptor.empty(kind, screenName); + let bodyRect = descriptor.bodyRect; + let animationOffset = descriptor.animationOffset; + if (slot === "popout" && popoutScreen === screenName) { + bodyRect = { + "x": popoutBodyX, + "y": popoutBodyY, + "width": popoutBodyW, + "height": popoutBodyH + }; + animationOffset = { + "x": popoutAnimX, + "y": popoutAnimY + }; + } else if (slot === "modal" && modalStates[screenName]) { + const modal = modalStates[screenName]; + bodyRect = { + "x": modal.bodyX, + "y": modal.bodyY, + "width": modal.bodyW, + "height": modal.bodyH + }; + animationOffset = { + "x": modal.animX, + "y": modal.animY + }; + } else if (slot === "dock" && dockStates[screenName]) { + const dock = dockStates[screenName]; + const slide = dockSlides[screenName] || { + "x": dock.slideX, + "y": dock.slideY + }; + bodyRect = { + "x": dock.bodyX, + "y": dock.bodyY, + "width": dock.bodyW, + "height": dock.bodyH + }; + animationOffset = { + "x": slide.x, + "y": slide.y + }; + } else if (slot === "notification" && notificationStates[screenName]) { + const notification = notificationStates[screenName]; + bodyRect = { + "x": notification.bodyX, + "y": notification.bodyY, + "width": notification.bodyW, + "height": notification.bodyH + }; + } + return SurfaceDescriptor.normalize({ + "bodyRect": bodyRect, + "animationOffset": animationOffset + }, descriptor); + } + + function legacySurfaceState(screenName, kind) { + return SurfaceDescriptor.toLegacyState(surfaceDescriptor(screenName, kind)); + } + + function hasSurfaceDescriptor(screenName, kind, ownerId) { + const descriptor = surfaceDescriptor(screenName, kind); + return descriptor.phase !== "hidden" && (!ownerId || descriptor.ownerId === ownerId); + } + + function _setSurfaceDescriptor(screenName, slotKind, state, ownerId) { + if (!screenName || !state) + return false; + const slot = _surfaceSlot(slotKind); + const currentScreen = surfaceDescriptors[screenName] || {}; + const previous = currentScreen[slot] || SurfaceDescriptor.empty(state.kind || slotKind, screenName); + let normalized = SurfaceDescriptor.normalize(Object.assign({}, state, { + "ownerId": ownerId !== undefined ? ownerId : previous.ownerId, + "screenName": screenName, + "revision": previous.revision + }), previous); + if (SurfaceDescriptor.same(previous, normalized)) + return true; + normalized = SurfaceDescriptor.withRevision(normalized, previous.revision + 1); + const nextScreen = _cloneDict(currentScreen); + nextScreen[slot] = normalized; + const next = _cloneDict(surfaceDescriptors); + next[screenName] = nextScreen; + surfaceDescriptors = next; + return true; + } + + function _clearSurfaceDescriptor(screenName, kind, ownerId) { + if (!screenName) + return false; + const slot = _surfaceSlot(kind); + const currentScreen = surfaceDescriptors[screenName]; + const current = currentScreen ? currentScreen[slot] : null; + if (!current || (ownerId && current.ownerId !== ownerId)) + return false; + const nextScreen = _cloneDict(currentScreen); + delete nextScreen[slot]; + const next = _cloneDict(surfaceDescriptors); + if (Object.keys(nextScreen).length > 0) + next[screenName] = nextScreen; + else + delete next[screenName]; + surfaceDescriptors = next; + return true; + } + + function _setSurfaceAnimation(screenName, kind, ownerId, x, y) { + const current = surfaceDescriptor(screenName, kind); + if (current.phase === "hidden" || (ownerId && current.ownerId !== ownerId)) + return false; + return true; + } + + function _setSurfaceBody(screenName, kind, ownerId, x, y, width, height) { + const current = surfaceDescriptor(screenName, kind); + if (current.phase === "hidden" || (ownerId && current.ownerId !== ownerId)) + return false; + return true; + } + readonly property var emptyDockState: ({ "reveal": false, "barSide": "bottom", @@ -69,8 +200,10 @@ Singleton { popoutOwnerId = claimId; const ok = updatePopout(claimId, state); if (ok) { - if (previousScreen && previousScreen !== popoutScreen) + if (previousScreen && previousScreen !== popoutScreen) { + _clearSurfaceDescriptor(previousScreen, "popout"); _bumpSurfaceRevision(previousScreen); + } _bumpSurfaceRevision(popoutScreen); } return ok; @@ -103,6 +236,21 @@ Singleton { if (state.omitEndConnector !== undefined) popoutOmitEndConnector = !!state.omitEndConnector; + _setSurfaceDescriptor(popoutScreen, "popout", Object.assign({}, state, { + "kind": "popout", + "screenName": popoutScreen, + "visible": popoutVisible, + "presented": state.presented !== undefined ? !!state.presented : popoutVisible, + "barSide": popoutBarSide, + "bodyX": popoutBodyX, + "bodyY": popoutBodyY, + "bodyW": popoutBodyW, + "bodyH": popoutBodyH, + "animX": popoutAnimX, + "animY": popoutAnimY, + "omitStartConnector": popoutOmitStartConnector, + "omitEndConnector": popoutOmitEndConnector + }), claimId); return true; } @@ -123,6 +271,7 @@ Singleton { popoutScreen = ""; popoutOmitStartConnector = false; popoutOmitEndConnector = false; + _clearSurfaceDescriptor(releasedScreen, "popout", claimId); _bumpSurfaceRevision(releasedScreen); return true; } @@ -140,6 +289,7 @@ Singleton { if (!isNaN(nextY) && popoutAnimY !== nextY) popoutAnimY = nextY; } + _setSurfaceAnimation(popoutScreen, "popout", claimId, animX, animY); return true; } @@ -166,6 +316,7 @@ Singleton { if (!isNaN(nextH) && popoutBodyH !== nextH) popoutBodyH = nextH; } + _setSurfaceBody(popoutScreen, "popout", claimId, bodyX, bodyY, bodyW, bodyH); return true; } @@ -193,13 +344,21 @@ Singleton { return false; const normalized = _normalizeDockState(state); - if (_sameDockState(dockStates[screenName], normalized)) - return true; + const descriptorState = Object.assign({}, state, normalized, { + "kind": "dock", + "screenName": screenName, + "visible": normalized.reveal, + "presented": normalized.reveal, + "phase": normalized.reveal ? (state.phase || "open") : "hidden" + }); const previous = dockStates[screenName] || emptyDockState; - - const next = _cloneDict(dockStates); - next[screenName] = normalized; - dockStates = next; + const legacyChanged = !_sameDockState(dockStates[screenName], normalized); + if (legacyChanged) { + const next = _cloneDict(dockStates); + next[screenName] = normalized; + dockStates = next; + } + _setSurfaceDescriptor(screenName, "dock", descriptorState, "dock:" + screenName); if (!!previous.reveal !== !!normalized.reveal) _bumpSurfaceRevision(screenName); return true; @@ -212,6 +371,7 @@ Singleton { const next = _cloneDict(dockStates); delete next[screenName]; dockStates = next; + _clearSurfaceDescriptor(screenName, "dock"); // Also clear corresponding slide if (dockSlides[screenName]) { @@ -237,6 +397,7 @@ Singleton { "y": numY }; dockSlides = next; + _setSurfaceAnimation(screenName, "dock", "dock:" + screenName, numX, numY); return true; } @@ -283,13 +444,20 @@ Singleton { return false; const normalized = _normalizeNotificationState(state); - if (_sameNotificationState(notificationStates[screenName], normalized)) - return true; + const descriptorState = Object.assign({}, state, normalized, { + "kind": "notification", + "screenName": screenName, + "presented": normalized.visible, + "phase": normalized.visible ? (state.phase || "open") : "hidden" + }); const previous = notificationStates[screenName] || emptyNotificationState; - - const next = _cloneDict(notificationStates); - next[screenName] = normalized; - notificationStates = next; + const legacyChanged = !_sameNotificationState(notificationStates[screenName], normalized); + if (legacyChanged) { + const next = _cloneDict(notificationStates); + next[screenName] = normalized; + notificationStates = next; + } + _setSurfaceDescriptor(screenName, "notification", descriptorState, "notification:" + screenName); if (!!previous.visible !== !!normalized.visible) _bumpSurfaceRevision(screenName); return true; @@ -302,6 +470,7 @@ Singleton { const next = _cloneDict(notificationStates); delete next[screenName]; notificationStates = next; + _clearSurfaceDescriptor(screenName, "notification"); _bumpSurfaceRevision(screenName); return true; } @@ -362,6 +531,10 @@ Singleton { const next = _cloneDict(modalStates); next[screenName] = normalized; modalStates = next; + _setSurfaceDescriptor(screenName, "modal", Object.assign({}, state, normalized, { + "kind": state.kind || "modal", + "screenName": screenName + }), ownerId || ""); _bumpSurfaceRevision(screenName); return true; } @@ -372,11 +545,16 @@ Singleton { if (ownerId && modalOwners[screenName] !== ownerId) return false; const normalized = _normalizeModalState(state); - if (_sameModalState(modalStates[screenName], normalized)) - return true; - const next = _cloneDict(modalStates); - next[screenName] = normalized; - modalStates = next; + const descriptorState = Object.assign({}, state, normalized, { + "kind": state.kind || (surfaceDescriptor(screenName, "modal").kind || "modal"), + "screenName": screenName + }); + if (!_sameModalState(modalStates[screenName], normalized)) { + const next = _cloneDict(modalStates); + next[screenName] = normalized; + modalStates = next; + } + _setSurfaceDescriptor(screenName, "modal", descriptorState, ownerId || modalOwners[screenName] || ""); return true; } @@ -418,6 +596,7 @@ Singleton { delete nextOwners[screenName]; modalOwners = nextOwners; } + _clearSurfaceDescriptor(screenName, "modal", ownerId); _bumpSurfaceRevision(screenName); return true; } @@ -438,6 +617,7 @@ Singleton { "animY": nay }); modalStates = next; + _setSurfaceAnimation(screenName, "modal", ownerId, animX, animY); return true; } @@ -461,6 +641,7 @@ Singleton { "bodyH": nh }); modalStates = next; + _setSurfaceBody(screenName, "modal", ownerId, bodyX, bodyY, bodyW, bodyH); return true; } @@ -543,6 +724,9 @@ Singleton { const nextSurfaceRevisions = pruneKeyed(surfaceRevisions); if (nextSurfaceRevisions !== null) surfaceRevisions = nextSurfaceRevisions; + const nextDescriptors = pruneKeyed(surfaceDescriptors); + if (nextDescriptors !== null) + surfaceDescriptors = nextDescriptors; let retractChanged = false; const nextRetract = {}; diff --git a/quickshell/Common/ConnectedSurfaceDescriptor.js b/quickshell/Common/ConnectedSurfaceDescriptor.js new file mode 100644 index 00000000..7a739789 --- /dev/null +++ b/quickshell/Common/ConnectedSurfaceDescriptor.js @@ -0,0 +1,179 @@ +.pragma library + +var VALID_KINDS = { + "popout": true, + "modal": true, + "launcher": true, + "dock": true, + "notification": true +}; + +var VALID_PHASES = { + "opening": true, + "open": true, + "closing": true, + "hidden": true, + "recovering": true +}; + +function _number(value, fallback) { + var n = Number(value); + return isNaN(n) ? fallback : n; +} + +function _bool(value, fallback) { + return value === undefined ? fallback : !!value; +} + +function _kind(value, fallback) { + if (VALID_KINDS[value]) + return value; + return VALID_KINDS[fallback] ? fallback : "modal"; +} + +function _defaultBarSide(kind) { + return kind === "popout" || kind === "notification" ? "top" : "bottom"; +} + +function _barSide(value, fallback) { + if (value === "top" || value === "bottom" || value === "left" || value === "right") + return value; + return fallback; +} + +function slotForKind(kind) { + return kind === "launcher" ? "modal" : _kind(kind, "modal"); +} + +function inferPhase(visible, presented, requestedPhase) { + if (VALID_PHASES[requestedPhase]) + return requestedPhase; + if (!visible && !presented) + return "hidden"; + if (!visible && presented) + return "closing"; + return "open"; +} + +function normalize(input, defaults) { + var source = input || {}; + var base = defaults || {}; + var kind = _kind(source.kind, base.kind); + var defaultSide = _defaultBarSide(kind); + var sourceRect = source.bodyRect || {}; + var baseRect = base.bodyRect || {}; + var sourceOffset = source.animationOffset || {}; + var baseOffset = base.animationOffset || {}; + var visible = _bool(source.visible !== undefined ? source.visible : source.reveal, _bool(base.visible !== undefined ? base.visible : base.reveal, false)); + var presented = _bool(source.presented, _bool(base.presented, visible)); + var bodyRect = { + "x": _number(sourceRect.x !== undefined ? sourceRect.x : source.bodyX, _number(baseRect.x !== undefined ? baseRect.x : base.bodyX, 0)), + "y": _number(sourceRect.y !== undefined ? sourceRect.y : source.bodyY, _number(baseRect.y !== undefined ? baseRect.y : base.bodyY, 0)), + "width": Math.max(0, _number(sourceRect.width !== undefined ? sourceRect.width : source.bodyW, _number(baseRect.width !== undefined ? baseRect.width : base.bodyW, 0))), + "height": Math.max(0, _number(sourceRect.height !== undefined ? sourceRect.height : source.bodyH, _number(baseRect.height !== undefined ? baseRect.height : base.bodyH, 0))) + }; + var animationOffset = { + "x": _number(sourceOffset.x !== undefined ? sourceOffset.x : (source.animX !== undefined ? source.animX : source.slideX), _number(baseOffset.x !== undefined ? baseOffset.x : (base.animX !== undefined ? base.animX : base.slideX), 0)), + "y": _number(sourceOffset.y !== undefined ? sourceOffset.y : (source.animY !== undefined ? source.animY : source.slideY), _number(baseOffset.y !== undefined ? baseOffset.y : (base.animY !== undefined ? base.animY : base.slideY), 0)) + }; + var screenName = source.screenName !== undefined ? source.screenName : (source.screen !== undefined ? source.screen : (base.screenName !== undefined ? base.screenName : base.screen)); + var opacity = Math.max(0, Math.min(1, _number(source.opacity, _number(base.opacity, 1)))); + + return { + "ownerId": String(source.ownerId !== undefined ? source.ownerId : (base.ownerId || "")), + "kind": kind, + "screenName": String(screenName || ""), + "phase": inferPhase(visible, presented, source.phase !== undefined ? source.phase : base.phase), + "visible": visible, + "presented": presented, + "barSide": _barSide(source.barSide, _barSide(base.barSide, defaultSide)), + "bodyRect": bodyRect, + "animationOffset": animationOffset, + "scale": Math.max(0, _number(source.scale, _number(base.scale, 1))), + "opacity": opacity, + "omitStartConnector": _bool(source.omitStartConnector, _bool(base.omitStartConnector, false)), + "omitEndConnector": _bool(source.omitEndConnector, _bool(base.omitEndConnector, false)), + "dockRetractSide": String(source.dockRetractSide !== undefined ? source.dockRetractSide : (base.dockRetractSide || "")), + "revision": Math.max(0, Math.floor(_number(source.revision, _number(base.revision, 0)))) + }; +} + +function empty(kind, screenName) { + return normalize({ + "kind": kind, + "screenName": screenName || "", + "phase": "hidden", + "visible": false, + "presented": false + }); +} + +function withRevision(descriptor, revision) { + var next = normalize(descriptor); + next.revision = Math.max(0, Math.floor(_number(revision, next.revision))); + return next; +} + +function withAnimationOffset(descriptor, x, y) { + var next = normalize(descriptor); + next.animationOffset = { + "x": x === undefined ? next.animationOffset.x : _number(x, next.animationOffset.x), + "y": y === undefined ? next.animationOffset.y : _number(y, next.animationOffset.y) + }; + return next; +} + +function withBodyRect(descriptor, x, y, width, height) { + var next = normalize(descriptor); + next.bodyRect = { + "x": x === undefined ? next.bodyRect.x : _number(x, next.bodyRect.x), + "y": y === undefined ? next.bodyRect.y : _number(y, next.bodyRect.y), + "width": width === undefined ? next.bodyRect.width : Math.max(0, _number(width, next.bodyRect.width)), + "height": height === undefined ? next.bodyRect.height : Math.max(0, _number(height, next.bodyRect.height)) + }; + return next; +} + +function same(a, b, threshold) { + if (!a || !b) + return false; + var epsilon = threshold === undefined ? 0.5 : Math.max(0, Number(threshold)); + return a.ownerId === b.ownerId + && a.kind === b.kind + && a.screenName === b.screenName + && a.phase === b.phase + && a.visible === b.visible + && a.presented === b.presented + && a.barSide === b.barSide + && Math.abs(a.bodyRect.x - b.bodyRect.x) < epsilon + && Math.abs(a.bodyRect.y - b.bodyRect.y) < epsilon + && Math.abs(a.bodyRect.width - b.bodyRect.width) < epsilon + && Math.abs(a.bodyRect.height - b.bodyRect.height) < epsilon + && Math.abs(a.animationOffset.x - b.animationOffset.x) < epsilon + && Math.abs(a.animationOffset.y - b.animationOffset.y) < epsilon + && Math.abs(a.scale - b.scale) < 0.0001 + && Math.abs(a.opacity - b.opacity) < 0.0001 + && a.omitStartConnector === b.omitStartConnector + && a.omitEndConnector === b.omitEndConnector + && a.dockRetractSide === b.dockRetractSide; +} + +function toLegacyState(descriptor) { + var d = normalize(descriptor); + return { + "visible": d.visible, + "reveal": d.visible, + "barSide": d.barSide, + "bodyX": d.bodyRect.x, + "bodyY": d.bodyRect.y, + "bodyW": d.bodyRect.width, + "bodyH": d.bodyRect.height, + "animX": d.animationOffset.x, + "animY": d.animationOffset.y, + "slideX": d.animationOffset.x, + "slideY": d.animationOffset.y, + "screen": d.screenName, + "omitStartConnector": d.omitStartConnector, + "omitEndConnector": d.omitEndConnector + }; +} diff --git a/quickshell/Common/ConnectedSurfaceGeometry.js b/quickshell/Common/ConnectedSurfaceGeometry.js new file mode 100644 index 00000000..fcdf30f0 --- /dev/null +++ b/quickshell/Common/ConnectedSurfaceGeometry.js @@ -0,0 +1,232 @@ +.pragma library + +function _number(value, fallback) { + var n = Number(value); + return isNaN(n) ? fallback : n; +} + +function snap(value, dpr) { + var scale = dpr || 1; + return Math.round(_number(value, 0) * scale) / scale; +} + +function isHorizontal(side) { + return side === "top" || side === "bottom"; +} + +function isVertical(side) { + return side === "left" || side === "right"; +} + +function bodyRect(descriptor, dpr) { + var source = descriptor && descriptor.bodyRect ? descriptor.bodyRect : descriptor || {}; + return { + "x": snap(source.x !== undefined ? source.x : source.bodyX, dpr), + "y": snap(source.y !== undefined ? source.y : source.bodyY, dpr), + "width": Math.max(0, snap(source.width !== undefined ? source.width : source.bodyW, dpr)), + "height": Math.max(0, snap(source.height !== undefined ? source.height : source.bodyH, dpr)) + }; +} + +function animatedBodyRect(descriptor, dpr) { + var rect = bodyRect(descriptor, dpr); + var offset = descriptor && descriptor.animationOffset ? descriptor.animationOffset : descriptor || {}; + var side = descriptor && descriptor.barSide ? descriptor.barSide : "bottom"; + var dx = isVertical(side) ? Math.max(-rect.width, Math.min(_number(offset.x !== undefined ? offset.x : offset.animX, 0), rect.width)) : 0; + var dy = isHorizontal(side) ? Math.max(-rect.height, Math.min(_number(offset.y !== undefined ? offset.y : offset.animY, 0), rect.height)) : 0; + + return { + "x": snap(rect.x + (side === "right" ? dx : 0), dpr), + "y": snap(rect.y + (side === "bottom" ? dy : 0), dpr), + "width": Math.max(0, snap(rect.width - Math.abs(dx), dpr)), + "height": Math.max(0, snap(rect.height - Math.abs(dy), dpr)), + "dx": snap(dx, dpr), + "dy": snap(dy, dpr) + }; +} + +function translatedBodyRect(descriptor, dpr) { + var rect = bodyRect(descriptor, dpr); + var offset = descriptor && descriptor.animationOffset ? descriptor.animationOffset : {}; + return { + "x": snap(rect.x + _number(offset.x, 0), dpr), + "y": snap(rect.y + _number(offset.y, 0), dpr), + "width": rect.width, + "height": rect.height + }; +} + +function connectorRadii(descriptor, rect, connectedRadius, surfaceRadius, dpr, nearIncludesSurface) { + var side = descriptor && descriptor.barSide ? descriptor.barSide : "bottom"; + var horizontal = isHorizontal(side); + var extent = horizontal ? rect.height : rect.width; + var crossSize = horizontal ? rect.width : rect.height; + var nearLimit = nearIncludesSurface ? Math.min(connectedRadius, surfaceRadius, extent, crossSize / 2) : Math.min(connectedRadius, extent, crossSize / 2); + var farLimit = Math.min(connectedRadius, surfaceRadius, crossSize / 2); + var near = snap(Math.max(0, nearLimit), dpr); + var far = snap(Math.max(0, farLimit), dpr); + var omitStart = !!(descriptor && descriptor.omitStartConnector); + var omitEnd = !!(descriptor && descriptor.omitEndConnector); + return { + "near": near, + "far": far, + "start": omitStart ? 0 : near, + "end": omitEnd ? 0 : near, + "farStart": omitStart ? far : 0, + "farEnd": omitEnd ? far : 0, + "farExtent": Math.max(omitStart ? far : 0, omitEnd ? far : 0) + }; +} + +function _connectorWidth(side, spacing, radius) { + return isVertical(side) ? spacing + radius : radius; +} + +function _connectorHeight(side, spacing, radius) { + return isVertical(side) ? radius : spacing + radius; +} + +function connectorRect(side, rect, placement, spacing, radius, dpr) { + var width = _connectorWidth(side, spacing, radius); + var height = _connectorHeight(side, spacing, radius); + var seamX = isVertical(side) ? (side === "left" ? rect.x : rect.x + rect.width) : (placement === "left" ? rect.x : rect.x + rect.width); + var seamY = side === "top" ? rect.y : (side === "bottom" ? rect.y + rect.height : (placement === "left" ? rect.y : rect.y + rect.height)); + var x = isVertical(side) ? (side === "left" ? seamX : seamX - width) : (placement === "left" ? seamX - width : seamX); + var y = side === "top" ? seamY : (side === "bottom" ? seamY - height : (placement === "left" ? seamY - height : seamY)); + return { + "x": snap(x, dpr), + "y": snap(y, dpr), + "width": Math.max(0, snap(width, dpr)), + "height": Math.max(0, snap(height, dpr)) + }; +} + +function farConnectorRect(side, rect, placement, radius, dpr) { + var x; + var y; + if (isHorizontal(side)) { + x = placement === "left" ? rect.x : rect.x + rect.width - radius; + y = side === "top" ? rect.y + rect.height : rect.y - radius; + } else { + x = side === "left" ? rect.x + rect.width : rect.x - radius; + y = placement === "left" ? rect.y : rect.y + rect.height - radius; + } + return { + "x": snap(x, dpr), + "y": snap(y, dpr), + "width": Math.max(0, snap(radius, dpr)), + "height": Math.max(0, snap(radius, dpr)) + }; +} + +function farBodyCapRect(side, rect, placement, radius, dpr) { + var x; + var y; + if (isHorizontal(side)) { + x = placement === "left" ? rect.x : rect.x + rect.width - radius; + y = side === "top" ? rect.y + rect.height - radius : rect.y; + } else { + x = side === "left" ? rect.x + rect.width - radius : rect.x; + y = placement === "left" ? rect.y : rect.y + rect.height - radius; + } + return { + "x": snap(x, dpr), + "y": snap(y, dpr), + "width": Math.max(0, snap(radius, dpr)), + "height": Math.max(0, snap(radius, dpr)) + }; +} + +function chromeBounds(rect, side, startRadius, endRadius, farExtent, dpr) { + var horizontal = isHorizontal(side); + var bodyOffsetX = horizontal ? startRadius : (side === "right" ? farExtent : 0); + var bodyOffsetY = horizontal ? (side === "bottom" ? farExtent : 0) : startRadius; + return { + "x": snap(rect.x - bodyOffsetX, dpr), + "y": snap(rect.y - bodyOffsetY, dpr), + "width": Math.max(0, snap(horizontal ? rect.width + startRadius + endRadius : rect.width + farExtent, dpr)), + "height": Math.max(0, snap(horizontal ? rect.height + farExtent : rect.height + startRadius + endRadius, dpr)), + "bodyOffsetX": snap(bodyOffsetX, dpr), + "bodyOffsetY": snap(bodyOffsetY, dpr) + }; +} + +function fillBounds(rect, side, seamOverlap, dpr) { + var overlapX = isHorizontal(side) ? seamOverlap : 0; + var overlapY = isVertical(side) ? seamOverlap : 0; + return { + "x": snap(rect.x - overlapX, dpr), + "y": snap(rect.y - overlapY, dpr), + "width": Math.max(0, snap(rect.width + overlapX * 2, dpr)), + "height": Math.max(0, snap(rect.height + overlapY * 2, dpr)) + }; +} + +function clipEnvelope(rect, side, radii, seamOverlap, dpr) { + var fill = fillBounds(rect, side, seamOverlap, dpr); + var chrome = chromeBounds(fill, side, radii.start, radii.end, radii.farExtent, dpr); + return { + "x": chrome.x, + "y": chrome.y, + "width": chrome.width, + "height": chrome.height, + "bodyX": snap(fill.x - chrome.x, dpr), + "bodyY": snap(fill.y - chrome.y, dpr), + "bodyWidth": fill.width, + "bodyHeight": fill.height + }; +} + +function blurRegions(descriptor, rect, radii, dpr) { + var side = descriptor.barSide; + var regions = [bodyRect(rect, dpr)]; + if (radii.start > 0) + regions.push(connectorRect(side, rect, "left", 0, radii.start, dpr)); + if (radii.end > 0) + regions.push(connectorRect(side, rect, "right", 0, radii.end, dpr)); + if (radii.farStart > 0) { + regions.push(farConnectorRect(side, rect, "left", radii.farStart, dpr)); + regions.push(farBodyCapRect(side, rect, "left", radii.farStart, dpr)); + } + if (radii.farEnd > 0) { + regions.push(farConnectorRect(side, rect, "right", radii.farEnd, dpr)); + regions.push(farBodyCapRect(side, rect, "right", radii.farEnd, dpr)); + } + return regions; +} + +function unionBounds(rects, padding, dpr) { + var minX = Infinity; + var minY = Infinity; + var maxX = -Infinity; + var maxY = -Infinity; + for (var i = 0; i < rects.length; i++) { + var rect = rects[i]; + if (!rect || rect.width <= 0 || rect.height <= 0) + continue; + minX = Math.min(minX, rect.x); + minY = Math.min(minY, rect.y); + maxX = Math.max(maxX, rect.x + rect.width); + maxY = Math.max(maxY, rect.y + rect.height); + } + if (minX === Infinity) + return {"x": 0, "y": 0, "width": 0, "height": 0}; + var pad = Math.max(0, _number(padding, 0)); + return { + "x": snap(minX - pad, dpr), + "y": snap(minY - pad, dpr), + "width": Math.max(0, snap(maxX - minX + pad * 2, dpr)), + "height": Math.max(0, snap(maxY - minY + pad * 2, dpr)) + }; +} + +function shadowSourceBounds(descriptor, rect, radii, padding, dpr) { + return unionBounds(blurRegions(descriptor, rect, radii, dpr), padding, dpr); +} + +function stableEqual(a, b, dpr) { + if (!a || !b) + return false; + var threshold = 0.5 / (dpr || 1); + return Math.abs(a.x - b.x) < threshold && Math.abs(a.y - b.y) < threshold && Math.abs(a.width - b.width) < threshold && Math.abs(a.height - b.height) < threshold; +} diff --git a/quickshell/Common/ConnectedSurfaceLease.qml b/quickshell/Common/ConnectedSurfaceLease.qml new file mode 100644 index 00000000..f5a7a97c --- /dev/null +++ b/quickshell/Common/ConnectedSurfaceLease.qml @@ -0,0 +1,176 @@ +pragma ComponentBehavior: Bound + +import QtQuick + +Item { + id: root + + required property string claimPrefix + required property var isCurrentOwner + required property var hasOwner + required property var claimState + required property var ensureState + required property var releaseState + + property var statePresent: null + property var updateAnimationState: null + property var updateBodyState: null + property var requestDockRetract: null + property var releaseDockRetract: null + + property string screenName: "" + property bool enabled: false + property bool active: false + property bool presented: false + property bool dockBlocked: false + property string dockSide: "" + property bool renewTokenOnRecovery: true + + property string claimId: "" + property string claimedScreenName: "" + property int _claimSerial: 0 + + signal recoveryRequested + + visible: false + + function _nextClaimId() { + _claimSerial += 1; + return claimPrefix + ":" + (new Date()).getTime() + ":" + _claimSerial + ":" + Math.floor(Math.random() * 1000000); + } + + function _isCurrent(name) { + return !!name && !!isCurrentOwner && !!isCurrentOwner(name); + } + + function _hasOwner(name, ownerId) { + return !!name && !!ownerId && !!hasOwner && !!hasOwner(name, ownerId); + } + + function _hasState(name, ownerId) { + return !statePresent || !!statePresent(name, ownerId); + } + + function _shouldRecover() { + return active && enabled && _isCurrent(screenName); + } + + function requestRecovery() { + if (!_shouldRecover()) + return false; + recoveryRequested(); + return true; + } + + function checkOwnershipRecovery() { + if (!_shouldRecover()) + return false; + if (claimedScreenName === screenName && _hasOwner(screenName, claimId)) + return false; + recoveryRequested(); + return true; + } + + function checkStateRecovery() { + if (!_shouldRecover()) + return false; + if (claimedScreenName === screenName && _hasOwner(screenName, claimId) && _hasState(screenName, claimId)) + return false; + recoveryRequested(); + return true; + } + + function checkRecovery() { + return checkStateRecovery(); + } + + function beginClaim() { + if (claimId && releaseDockRetract) + releaseDockRetract(claimId); + claimId = _nextClaimId(); + claimedScreenName = ""; + return claimId; + } + + function _syncDockRetract() { + if (!claimId) + return; + if (dockBlocked && presented && dockSide && requestDockRetract) + requestDockRetract(claimId, screenName, dockSide); + else if (releaseDockRetract) + releaseDockRetract(claimId); + } + + function publish(state, forceClaim) { + if (!enabled || !screenName || !state) { + release(); + return false; + } + + if (claimedScreenName && claimedScreenName !== screenName) + release(); + + const current = _isCurrent(screenName); + let claiming = !!forceClaim || !claimId; + if (claiming && !current) + return false; + if (!claimId) + beginClaim(); + + let published = claiming ? claimState(screenName, state, claimId) : ensureState(screenName, state, claimId); + if (!published && !claiming && current) { + if (renewTokenOnRecovery) { + beginClaim(); + } else if (releaseDockRetract) { + releaseDockRetract(claimId); + } + published = claimState(screenName, state, claimId); + } + if (!published) + return false; + + claimedScreenName = screenName; + _syncDockRetract(); + return true; + } + + function updateAnim(animX, animY) { + if (!enabled || !claimId || !claimedScreenName || !updateAnimationState) + return false; + if (!_hasOwner(claimedScreenName, claimId)) { + requestRecovery(); + return false; + } + return updateAnimationState(claimedScreenName, claimId, animX, animY); + } + + function updateBody(bodyX, bodyY, bodyW, bodyH) { + if (!enabled || !claimId || !claimedScreenName || !updateBodyState) + return false; + if (!_hasOwner(claimedScreenName, claimId)) { + requestRecovery(); + return false; + } + return updateBodyState(claimedScreenName, claimId, bodyX, bodyY, bodyW, bodyH); + } + + function release() { + if (!claimId) { + claimedScreenName = ""; + return false; + } + + const releasedClaimId = claimId; + const releasedScreenName = claimedScreenName; + claimId = ""; + claimedScreenName = ""; + + if (releaseDockRetract) + releaseDockRetract(releasedClaimId); + if (releasedScreenName) + return !!releaseState(releasedScreenName, releasedClaimId); + return false; + } + + Component.onDestruction: release() +} diff --git a/quickshell/Modals/Common/DankModalConnected.qml b/quickshell/Modals/Common/DankModalConnected.qml index 30618bb1..5c1f372b 100644 --- a/quickshell/Modals/Common/DankModalConnected.qml +++ b/quickshell/Modals/Common/DankModalConnected.qml @@ -115,6 +115,7 @@ Item { id: modalChrome modalHandle: root.modalHandle claimPrefix: root.layerNamespace + ":modal" + surfaceKind: "modal" screenName: root._currentScreenName() enabled: root.frameOwnsConnectedChrome active: root.shouldBeVisible @@ -125,17 +126,38 @@ Item { } function _publishModalChromeState() { + const presented = shouldBeVisible || contentWindow.visible; + const phase = !presented ? "hidden" : (!shouldBeVisible && contentWindow.visible ? "closing" : (!contentWindow.visible ? "opening" : "open")); + const bodyRect = { + "x": alignedX, + "y": alignedY, + "width": alignedWidth, + "height": alignedHeight + }; + const animationOffset = { + "x": modalContainer ? modalContainer.animX : 0, + "y": modalContainer ? modalContainer.animY : 0 + }; const state = { - "visible": shouldBeVisible || contentWindow.visible, + "kind": "modal", + "screenName": root._currentScreenName(), + "phase": phase, + "visible": presented, + "presented": presented, "barSide": resolvedConnectedBarSide, + "bodyRect": bodyRect, + "animationOffset": animationOffset, + "scale": 1, + "opacity": Theme.connectedSurfaceColor.a, "bodyX": alignedX, "bodyY": alignedY, "bodyW": alignedWidth, "bodyH": alignedHeight, - "animX": modalContainer ? modalContainer.animX : 0, - "animY": modalContainer ? modalContainer.animY : 0, + "animX": animationOffset.x, + "animY": animationOffset.y, "omitStartConnector": false, - "omitEndConnector": false + "omitEndConnector": false, + "dockRetractSide": root._dockBlocksEmergence ? resolvedConnectedBarSide : "" }; return modalChrome.publish(state); } diff --git a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalConnected.qml b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalConnected.qml index fef28659..bc4cd745 100644 --- a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalConnected.qml +++ b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalConnected.qml @@ -242,6 +242,7 @@ Item { id: modalChrome modalHandle: root.modalHandle claimPrefix: "dms:launcher-v2" + surfaceKind: "launcher" screenName: root._currentScreenName() enabled: root.frameOwnsConnectedChrome active: root.spotlightOpen @@ -252,17 +253,38 @@ Item { } function _publishModalChromeState() { + const presented = spotlightOpen || contentWindow.visible; + const phase = !presented ? "hidden" : (isClosing ? "closing" : (!contentWindow.visible ? "opening" : "open")); + const bodyRect = { + "x": _connectedChromeX, + "y": _connectedChromeY, + "width": _connectedChromeWidth, + "height": _connectedChromeHeight + }; + const animationOffset = { + "x": contentContainer ? contentContainer.animX : 0, + "y": contentContainer ? contentContainer.animY : 0 + }; const state = { - "visible": spotlightOpen || contentWindow.visible, + "kind": "launcher", + "screenName": root._currentScreenName(), + "phase": phase, + "visible": presented, + "presented": presented, "barSide": resolvedConnectedBarSide, + "bodyRect": bodyRect, + "animationOffset": animationOffset, + "scale": 1, + "opacity": Theme.connectedSurfaceColor.a, "bodyX": _connectedChromeX, "bodyY": _connectedChromeY, "bodyW": _connectedChromeWidth, "bodyH": _connectedChromeHeight, - "animX": contentContainer ? contentContainer.animX : 0, - "animY": contentContainer ? contentContainer.animY : 0, + "animX": animationOffset.x, + "animY": animationOffset.y, "omitStartConnector": false, - "omitEndConnector": false + "omitEndConnector": false, + "dockRetractSide": root._dockBlocksEmergence ? resolvedConnectedBarSide : "" }; return modalChrome.publish(state); } diff --git a/quickshell/Modules/Dock/Dock.qml b/quickshell/Modules/Dock/Dock.qml index 413d4dfb..4385a887 100644 --- a/quickshell/Modules/Dock/Dock.qml +++ b/quickshell/Modules/Dock/Dock.qml @@ -186,13 +186,36 @@ Variants { return; } + const presented = dock.visible && (dock.reveal || slideXAnimation.running || slideYAnimation.running) && dock.hasApps; + const phase = !presented ? "hidden" : ((!dock.reveal && (slideXAnimation.running || slideYAnimation.running)) ? "closing" : ((slideXAnimation.running || slideYAnimation.running) ? "opening" : "open")); + const bodyX = dock._dockWindowOriginX() + dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x; + const bodyY = dock._dockWindowOriginY() + dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y; + const bodyW = dock.hasApps ? dockBackground.width : 0; + const bodyH = dock.hasApps ? dockBackground.height : 0; ConnectedModeState.setDockState(dock._dockScreenName, { - "reveal": dock.visible && (dock.reveal || slideXAnimation.running || slideYAnimation.running) && dock.hasApps, + "kind": "dock", + "screenName": dock._dockScreenName, + "phase": phase, + "visible": presented, + "presented": presented, + "reveal": presented, "barSide": dock.connectedBarSide, - "bodyX": dock._dockWindowOriginX() + dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x, - "bodyY": dock._dockWindowOriginY() + dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y, - "bodyW": dock.hasApps ? dockBackground.width : 0, - "bodyH": dock.hasApps ? dockBackground.height : 0, + "bodyRect": { + "x": bodyX, + "y": bodyY, + "width": bodyW, + "height": bodyH + }, + "animationOffset": { + "x": dockSlide.x, + "y": dockSlide.y + }, + "scale": 1, + "opacity": Theme.connectedSurfaceColor.a, + "bodyX": bodyX, + "bodyY": bodyY, + "bodyW": bodyW, + "bodyH": bodyH, "slideX": dockSlide.x, "slideY": dockSlide.y }); diff --git a/quickshell/Modules/Frame/FrameBorder.qml b/quickshell/Modules/Frame/FrameBorder.qml index 31112382..a59eefcf 100644 --- a/quickshell/Modules/Frame/FrameBorder.qml +++ b/quickshell/Modules/Frame/FrameBorder.qml @@ -1,9 +1,13 @@ pragma ComponentBehavior: Bound import QtQuick -import QtQuick.Effects +import QtQuick.Shapes import qs.Common +// Frame perimeter ring: the full window rectangle with a rounded-rectangle +// cutout. Drawn as a single even-odd Shape so the ring is one primitive: no +// full-output mask textures, and a translucent fill never double-blends at the +// corners. Item { id: root @@ -16,39 +20,42 @@ Item { required property real cutoutRadius property color borderColor: Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity) - Rectangle { - id: borderRect + // Path elements can't reference `parent`; expose cutout edges as root props. + readonly property real _left: cutoutLeftInset + readonly property real _top: cutoutTopInset + readonly property real _right: width - cutoutRightInset + readonly property real _bottom: height - cutoutBottomInset + readonly property real _radius: Math.max(0, Math.min(cutoutRadius, (_right - _left) / 2, (_bottom - _top) / 2)) + Shape { anchors.fill: parent - // Bake frameOpacity into the color alpha rather than using the `opacity` property - color: root.borderColor + asynchronous: false + preferredRendererType: Shape.CurveRenderer + antialiasing: true - layer.enabled: true - layer.effect: MultiEffect { - maskSource: cutoutMask - maskEnabled: true - maskInverted: true - maskThresholdMin: 0.5 - maskSpreadAtMin: 1 - } - } + ShapePath { + fillColor: root.borderColor + strokeWidth: -1 + fillRule: ShapePath.OddEvenFill - Item { - id: cutoutMask + // Outer rectangle (window edge, square corners) + startX: 0 + startY: 0 + PathLine { x: root.width; y: 0 } + PathLine { x: root.width; y: root.height } + PathLine { x: 0; y: root.height } + PathLine { x: 0; y: 0 } - anchors.fill: parent - layer.enabled: true - visible: false - - Rectangle { - anchors { - fill: parent - topMargin: root.cutoutTopInset - bottomMargin: root.cutoutBottomInset - leftMargin: root.cutoutLeftInset - rightMargin: root.cutoutRightInset - } - radius: root.cutoutRadius + // Inner rounded-rectangle cutout (second subpath → even-odd hole) + PathMove { x: root._left + root._radius; y: root._top } + PathLine { x: root._right - root._radius; y: root._top } + PathArc { x: root._right; y: root._top + root._radius; radiusX: root._radius; radiusY: root._radius } + PathLine { x: root._right; y: root._bottom - root._radius } + PathArc { x: root._right - root._radius; y: root._bottom; radiusX: root._radius; radiusY: root._radius } + PathLine { x: root._left + root._radius; y: root._bottom } + PathArc { x: root._left; y: root._bottom - root._radius; radiusX: root._radius; radiusY: root._radius } + PathLine { x: root._left; y: root._top + root._radius } + PathArc { x: root._left + root._radius; y: root._top; radiusX: root._radius; radiusY: root._radius } } } } diff --git a/quickshell/Modules/Frame/FrameWindow.qml b/quickshell/Modules/Frame/FrameWindow.qml index c67c8d43..fcf8dd4d 100644 --- a/quickshell/Modules/Frame/FrameWindow.qml +++ b/quickshell/Modules/Frame/FrameWindow.qml @@ -8,6 +8,7 @@ import qs.Common import qs.Services import qs.Widgets import "../../Common/ConnectorGeometry.js" as ConnectorGeometry +import "../../Common/ConnectedSurfaceGeometry.js" as SurfaceGeometry PanelWindow { id: win @@ -46,13 +47,14 @@ PanelWindow { readonly property int _windowRegionHeight: win._regionInt(win.height) readonly property string _screenName: win.targetScreen ? win.targetScreen.name : "" readonly property int _surfaceRevision: Number(ConnectedModeState.surfaceRevisions[win._screenName] || 0) - readonly property var _dockState: ConnectedModeState.dockStates[win._screenName] || ConnectedModeState.emptyDockState - readonly property var _dockSlide: ConnectedModeState.dockSlides[win._screenName] || ({ - "x": 0, - "y": 0 - }) - readonly property var _notifState: ConnectedModeState.notificationStates[win._screenName] || ConnectedModeState.emptyNotificationState - readonly property var _modalState: ConnectedModeState.modalStates[win._screenName] || ConnectedModeState.emptyModalState + readonly property var _popoutDescriptor: ConnectedModeState.surfaceDescriptor(win._screenName, "popout") + readonly property var _dockDescriptor: ConnectedModeState.surfaceDescriptor(win._screenName, "dock") + readonly property var _notifDescriptor: ConnectedModeState.surfaceDescriptor(win._screenName, "notification") + readonly property var _modalDescriptor: ConnectedModeState.surfaceDescriptor(win._screenName, "modal") + readonly property var _popoutState: ConnectedModeState.legacySurfaceState(win._screenName, "popout") + readonly property var _dockState: ConnectedModeState.legacySurfaceState(win._screenName, "dock") + readonly property var _notifState: ConnectedModeState.legacySurfaceState(win._screenName, "notification") + readonly property var _modalState: ConnectedModeState.legacySurfaceState(win._screenName, "modal") readonly property bool _connectedActive: CompositorService.usesConnectedFrameChromeForScreen(win.targetScreen) readonly property string _barSide: { @@ -67,10 +69,15 @@ PanelWindow { } readonly property real _ccr: Theme.connectedCornerRadius - readonly property bool _popoutHorizontal: ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom" + readonly property bool _popoutHorizontal: SurfaceGeometry.isHorizontal(win._popoutDescriptor.barSide) readonly property bool _notifHorizontal: ConnectorGeometry.isHorizontal(win._notifState.barSide) readonly property bool _modalHorizontal: ConnectorGeometry.isHorizontal(win._modalState.barSide) readonly property bool _dockHorizontal: ConnectorGeometry.isHorizontal(win._dockState.barSide) + readonly property var _popoutBodyGeometry: SurfaceGeometry.animatedBodyRect(win._popoutDescriptor, win._dpr) + readonly property var _popoutStaticBodyGeometry: SurfaceGeometry.bodyRect(win._popoutDescriptor, win._dpr) + readonly property var _modalBodyGeometry: SurfaceGeometry.animatedBodyRect(win._modalDescriptor, win._dpr) + readonly property var _notifBodyGeometry: SurfaceGeometry.bodyRect(win._notifDescriptor, win._dpr) + readonly property var _dockBodyGeometry: SurfaceGeometry.translatedBodyRect(win._dockDescriptor, win._dpr) readonly property real _popoutArcExtent: win._popoutHorizontal ? _popoutBodyBlurAnchor.height : _popoutBodyBlurAnchor.width readonly property real _modalArcExtent: win._modalHorizontal ? _modalBodyBlurAnchor.height : _modalBodyBlurAnchor.width @@ -91,7 +98,7 @@ PanelWindow { } readonly property real _popoutFillOverlapXValue: win._popoutHorizontal ? win._seamOverlap : 0 - readonly property real _popoutFillOverlapYValue: (ConnectedModeState.popoutBarSide === "left" || ConnectedModeState.popoutBarSide === "right") ? win._seamOverlap : 0 + readonly property real _popoutFillOverlapYValue: (win._popoutState.barSide === "left" || win._popoutState.barSide === "right") ? win._seamOverlap : 0 readonly property real _dockFillOverlapXValue: win._dockHorizontal ? win._seamOverlap : 0 readonly property real _dockFillOverlapYValue: (win._dockState.barSide === "left" || win._dockState.barSide === "right") ? win._seamOverlap : 0 readonly property real _dockJoinOverlapXValue: ConnectorGeometry.isVertical(win._dockState.barSide) ? win._seamOverlap : 0 @@ -102,55 +109,109 @@ PanelWindow { // Theme.snap rounds to integer pixel: equal rounded values suppress // downstream Changed during sub-pixel morph jitter. - readonly property real _effectivePopoutCcr: { - const crossSize = win._popoutHorizontal ? _popoutBodyBlurAnchor.width : _popoutBodyBlurAnchor.height; - return Theme.snap(Math.max(0, Math.min(win._ccr, win._popoutArcExtent, crossSize / 2)), win._dpr); - } - readonly property real _effectivePopoutFarCcr: { - const crossSize = win._popoutHorizontal ? _popoutBodyBlurAnchor.width : _popoutBodyBlurAnchor.height; - return Theme.snap(Math.max(0, Math.min(win._ccr, win._surfaceRadius, crossSize / 2)), win._dpr); - } - readonly property real _effectivePopoutStartCcr: ConnectedModeState.popoutOmitStartConnector ? 0 : win._effectivePopoutCcr - readonly property real _effectivePopoutEndCcr: ConnectedModeState.popoutOmitEndConnector ? 0 : win._effectivePopoutCcr - readonly property real _effectivePopoutFarStartCcr: ConnectedModeState.popoutOmitStartConnector ? win._effectivePopoutFarCcr : 0 - readonly property real _effectivePopoutFarEndCcr: ConnectedModeState.popoutOmitEndConnector ? win._effectivePopoutFarCcr : 0 + readonly property var _popoutRadii: SurfaceGeometry.connectorRadii(win._popoutDescriptor, win._popoutBodyGeometry, win._ccr, win._surfaceRadius, win._dpr, false) + readonly property real _effectivePopoutCcr: win._popoutRadii.near + readonly property real _effectivePopoutFarCcr: win._popoutRadii.far + readonly property real _effectivePopoutStartCcr: win._popoutRadii.start + readonly property real _effectivePopoutEndCcr: win._popoutRadii.end + readonly property real _effectivePopoutFarStartCcr: win._popoutRadii.farStart + readonly property real _effectivePopoutFarEndCcr: win._popoutRadii.farEnd readonly property real _effectivePopoutMaxCcr: Math.max(win._effectivePopoutStartCcr, win._effectivePopoutEndCcr) readonly property real _effectivePopoutFarExtent: Math.max(win._effectivePopoutFarStartCcr, win._effectivePopoutFarEndCcr) - readonly property real _effectiveNotifCcr: { - const crossSize = win._notifHorizontal ? _notifBodyBlurAnchor.width : _notifBodyBlurAnchor.height; - const extent = win._notifHorizontal ? _notifBodyBlurAnchor.height : _notifBodyBlurAnchor.width; - return Theme.snap(Math.max(0, Math.min(win._ccr, win._surfaceRadius, extent, crossSize / 2)), win._dpr); - } - readonly property real _effectiveNotifFarCcr: { - const crossSize = win._notifHorizontal ? _notifBodySceneBlurAnchor.width : _notifBodySceneBlurAnchor.height; - return Theme.snap(Math.max(0, Math.min(win._ccr, win._surfaceRadius, crossSize / 2)), win._dpr); - } - readonly property real _effectiveNotifStartCcr: win._notifState.omitStartConnector ? 0 : win._effectiveNotifCcr - readonly property real _effectiveNotifEndCcr: win._notifState.omitEndConnector ? 0 : win._effectiveNotifCcr - readonly property real _effectiveNotifFarStartCcr: win._notifState.omitStartConnector ? win._effectiveNotifFarCcr : 0 - readonly property real _effectiveNotifFarEndCcr: win._notifState.omitEndConnector ? win._effectiveNotifFarCcr : 0 + readonly property var _popoutChromeGeometry: SurfaceGeometry.chromeBounds(win._popoutStaticBodyGeometry, win._popoutDescriptor.barSide, win._effectivePopoutStartCcr, win._effectivePopoutEndCcr, 0, win._dpr) + readonly property var _notifNearRadii: SurfaceGeometry.connectorRadii(win._notifDescriptor, win._notifBodyGeometry, win._ccr, win._surfaceRadius, win._dpr, true) + readonly property var _notifFarRadii: SurfaceGeometry.connectorRadii(win._notifDescriptor, win._notifBodyScene(), win._ccr, win._surfaceRadius, win._dpr, true) + readonly property real _effectiveNotifCcr: win._notifNearRadii.near + readonly property real _effectiveNotifFarCcr: win._notifFarRadii.far + readonly property real _effectiveNotifStartCcr: win._notifNearRadii.start + readonly property real _effectiveNotifEndCcr: win._notifNearRadii.end + readonly property real _effectiveNotifFarStartCcr: win._notifFarRadii.farStart + readonly property real _effectiveNotifFarEndCcr: win._notifFarRadii.farEnd 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 crossSize = win._modalHorizontal ? _modalBodyBlurAnchor.width : _modalBodyBlurAnchor.height; - const extent = win._modalHorizontal ? _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 crossSize = win._modalHorizontal ? _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 var _modalRadii: SurfaceGeometry.connectorRadii(win._modalDescriptor, win._modalBodyGeometry, win._ccr, win._surfaceRadius, win._dpr, true) + readonly property real _effectiveModalCcr: win._modalRadii.near + readonly property real _effectiveModalFarCcr: win._modalRadii.far + readonly property real _effectiveModalStartCcr: win._modalRadii.start + readonly property real _effectiveModalEndCcr: win._modalRadii.end + readonly property real _effectiveModalFarStartCcr: win._modalRadii.farStart + readonly property real _effectiveModalFarEndCcr: win._modalRadii.farEnd readonly property real _effectiveModalFarExtent: Math.max(win._effectiveModalFarStartCcr, win._effectiveModalFarEndCcr) + readonly property var _dockChromeGeometry: SurfaceGeometry.chromeBounds(win._dockBodyGeometry, win._dockDescriptor.barSide, win._dockConnectorRadiusValue, win._dockConnectorRadiusValue, 0, win._dpr) readonly property color _surfaceColor: Theme.connectedSurfaceColor readonly property real _surfaceOpacity: _surfaceColor.a readonly property color _opaqueSurfaceColor: Qt.rgba(_surfaceColor.r, _surfaceColor.g, _surfaceColor.b, 1) readonly property real _surfaceRadius: Theme.connectedSurfaceRadius readonly property real _seamOverlap: Theme.hairline(win._dpr) readonly property bool _disableLayer: Quickshell.env("DMS_DISABLE_LAYER") === "true" || Quickshell.env("DMS_DISABLE_LAYER") === "1" + // The connected silhouette is drawn directly as one translucent Shape; the + // full-output layer/FBO is allocated only when it must source the elevation + // shadow. Translucency no longer needs a flatten (single primitive). + readonly property bool _elevationShadow: win._connectedActive && Theme.elevationEnabled && !win._disableLayer + // Active surfaces packed into four fixed SDF-shader slots. Each near (bar) + // edge is clamped to the cutout edge so the smooth-min connector attaches + // there; connR (the smin radius) is the connector fillet. + readonly property var _sdfSlots: { + const T = win.cutoutTopInset; + const L = win.cutoutLeftInset; + const R = win.width - win.cutoutRightInset; + const B = win.height - win.cutoutBottomInset; + const clampNear = function (side, b) { + const r = {"x": b.x, "y": b.y, "width": b.width, "height": b.height}; + if (side === "top") { + r.height = Math.max(0, b.y + b.height - T); + r.y = T; + } else if (side === "bottom") { + r.height = Math.max(0, B - b.y); + } else if (side === "left") { + r.width = Math.max(0, b.x + b.width - L); + r.x = L; + } else if (side === "right") { + r.width = Math.max(0, R - b.x); + } + return r; + }; + const src = win._unifiedSurfaces(); + const out = []; + for (let i = 0; i < 4; i++) { + if (i < src.length) { + const s = src[i]; + const b = clampNear(s.side, s.body); + // smin radius = the near connector fillet only; an omitted + // connector contributes no flare (its corner just rounds). + const connR = Math.max(s.radii.startCr, s.radii.endCr); + const active = b.width > 0 && b.height > 0 ? 1 : 0; + // A bar-side corner is sharp only where its connector is present; + // an omitted connector (farStart/farEnd set) keeps that corner + // rounded. Far corners always round. (start=left/top, end=right/bottom.) + const bodyR = s.radii.surfaceRadius; + const omitS = s.radii.farStartCr > 0; + const omitE = s.radii.farEndCr > 0; + let tl = bodyR, tr = bodyR, br = bodyR, bl = bodyR; + if (s.side === "top") { + if (!omitS) tl = 0; + if (!omitE) tr = 0; + } else if (s.side === "bottom") { + if (!omitS) bl = 0; + if (!omitE) br = 0; + } else if (s.side === "left") { + if (!omitS) tl = 0; + if (!omitE) bl = 0; + } else { + if (!omitS) tr = 0; + if (!omitE) br = 0; + } + out.push({ + "rect": Qt.vector4d(b.x, b.y, b.width, b.height), + "corner": Qt.vector4d(tl, tr, br, bl), + "param": Qt.vector4d(connR, active, 0, 0) + }); + } else { + out.push({"rect": Qt.vector4d(0, 0, 0, 0), "corner": Qt.vector4d(0, 0, 0, 0), "param": Qt.vector4d(0, 0, 0, 0)}); + } + } + return out; + } property bool _surfaceRefreshNeedsLayerRecreate: false property bool _surfaceLayerRecoveryActive: false @@ -194,34 +255,33 @@ PanelWindow { id: _popoutBodyBlurAnchor visible: false - readonly property bool _active: ConnectedModeState.popoutVisible && ConnectedModeState.popoutScreen === win._screenName + readonly property bool _active: win._popoutDescriptor.visible + readonly property real _dyClamp: win._popoutBodyGeometry.dy + readonly property real _dxClamp: win._popoutBodyGeometry.dx - readonly property real _dyClamp: (ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom") ? Math.max(-ConnectedModeState.popoutBodyH, Math.min(ConnectedModeState.popoutAnimY, ConnectedModeState.popoutBodyH)) : 0 - readonly property real _dxClamp: (ConnectedModeState.popoutBarSide === "left" || ConnectedModeState.popoutBarSide === "right") ? Math.max(-ConnectedModeState.popoutBodyW, Math.min(ConnectedModeState.popoutAnimX, ConnectedModeState.popoutBodyW)) : 0 - - x: _active ? ConnectedModeState.popoutBodyX + (ConnectedModeState.popoutBarSide === "right" ? _dxClamp : 0) : 0 - y: _active ? ConnectedModeState.popoutBodyY + (ConnectedModeState.popoutBarSide === "bottom" ? _dyClamp : 0) : 0 - width: _active ? Math.max(0, ConnectedModeState.popoutBodyW - Math.abs(_dxClamp)) : 0 - height: _active ? Math.max(0, ConnectedModeState.popoutBodyH - Math.abs(_dyClamp)) : 0 + x: _active ? win._popoutBodyGeometry.x : 0 + y: _active ? win._popoutBodyGeometry.y : 0 + width: _active ? win._popoutBodyGeometry.width : 0 + height: _active ? win._popoutBodyGeometry.height : 0 } Item { id: _dockBodyBlurAnchor visible: false - readonly property bool _active: win._connectedActive && win._dockState.reveal && win._dockState.bodyW > 0 && win._dockState.bodyH > 0 + readonly property bool _active: win._connectedActive && win._dockDescriptor.visible && win._dockBodyGeometry.width > 0 && win._dockBodyGeometry.height > 0 - x: _active ? win._dockState.bodyX + (win._dockSlide.x || 0) : 0 - y: _active ? win._dockState.bodyY + (win._dockSlide.y || 0) : 0 - width: _active ? win._dockState.bodyW : 0 - height: _active ? win._dockState.bodyH : 0 + x: _active ? win._dockBodyGeometry.x : 0 + y: _active ? win._dockBodyGeometry.y : 0 + width: _active ? win._dockBodyGeometry.width : 0 + height: _active ? win._dockBodyGeometry.height : 0 } Item { id: _popoutBodyBlurCap opacity: 0 - readonly property string _side: ConnectedModeState.popoutBarSide + readonly property string _side: win._popoutState.barSide readonly property real _capThickness: win._popoutBlurCapThickness() readonly property bool _active: _popoutBodyBlurAnchor._active && _capThickness > 0 && _popoutBodyBlurAnchor.width > 0 && _popoutBodyBlurAnchor.height > 0 readonly property real _capWidth: (_side === "left" || _side === "right") ? Math.min(_capThickness, _popoutBodyBlurAnchor.width) : _popoutBodyBlurAnchor.width @@ -254,13 +314,12 @@ PanelWindow { readonly property real _radius: win._popoutConnectorRadiusLeft readonly property bool _active: _popoutBodyBlurAnchor._active && _radius > 0 - readonly property real _w: ConnectorGeometry.connectorWidth(ConnectedModeState.popoutBarSide, 0, win._popoutConnectorRadiusLeft) - readonly property real _h: ConnectorGeometry.connectorHeight(ConnectedModeState.popoutBarSide, 0, win._popoutConnectorRadiusLeft) + readonly property var _rect: SurfaceGeometry.connectorRect(win._popoutDescriptor.barSide, win._popoutBodyGeometry, "left", 0, _radius, win._dpr) - x: _active ? Theme.snap(ConnectorGeometry.connectorX(ConnectedModeState.popoutBarSide, _popoutBodyBlurAnchor.x, _popoutBodyBlurAnchor.width, "left", 0, win._popoutConnectorRadiusLeft), win._dpr) : 0 - y: _active ? Theme.snap(ConnectorGeometry.connectorY(ConnectedModeState.popoutBarSide, _popoutBodyBlurAnchor.y, _popoutBodyBlurAnchor.height, "left", 0, win._popoutConnectorRadiusLeft), win._dpr) : 0 - width: _active ? _w : 0 - height: _active ? _h : 0 + x: _active ? _rect.x : 0 + y: _active ? _rect.y : 0 + width: _active ? _rect.width : 0 + height: _active ? _rect.height : 0 } Item { @@ -269,13 +328,12 @@ PanelWindow { readonly property real _radius: win._popoutConnectorRadiusRight readonly property bool _active: _popoutBodyBlurAnchor._active && _radius > 0 - readonly property real _w: ConnectorGeometry.connectorWidth(ConnectedModeState.popoutBarSide, 0, win._popoutConnectorRadiusRight) - readonly property real _h: ConnectorGeometry.connectorHeight(ConnectedModeState.popoutBarSide, 0, win._popoutConnectorRadiusRight) + readonly property var _rect: SurfaceGeometry.connectorRect(win._popoutDescriptor.barSide, win._popoutBodyGeometry, "right", 0, _radius, win._dpr) - x: _active ? Theme.snap(ConnectorGeometry.connectorX(ConnectedModeState.popoutBarSide, _popoutBodyBlurAnchor.x, _popoutBodyBlurAnchor.width, "right", 0, win._popoutConnectorRadiusRight), win._dpr) : 0 - y: _active ? Theme.snap(ConnectorGeometry.connectorY(ConnectedModeState.popoutBarSide, _popoutBodyBlurAnchor.y, _popoutBodyBlurAnchor.height, "right", 0, win._popoutConnectorRadiusRight), win._dpr) : 0 - width: _active ? _w : 0 - height: _active ? _h : 0 + x: _active ? _rect.x : 0 + y: _active ? _rect.y : 0 + width: _active ? _rect.width : 0 + height: _active ? _rect.height : 0 } Item { @@ -283,7 +341,7 @@ PanelWindow { opacity: 0 readonly property bool _active: _popoutLeftConnectorBlurAnchor.width > 0 && _popoutLeftConnectorBlurAnchor.height > 0 - readonly property string _arcCorner: ConnectorGeometry.arcCorner(ConnectedModeState.popoutBarSide, "left") + readonly property string _arcCorner: ConnectorGeometry.arcCorner(win._popoutState.barSide, "left") readonly property real _radius: win._popoutConnectorRadiusLeft x: _active ? win._connectorCutoutX(_popoutLeftConnectorBlurAnchor.x, _popoutLeftConnectorBlurAnchor.width, _arcCorner, _radius) : 0 @@ -297,7 +355,7 @@ PanelWindow { opacity: 0 readonly property bool _active: _popoutRightConnectorBlurAnchor.width > 0 && _popoutRightConnectorBlurAnchor.height > 0 - readonly property string _arcCorner: ConnectorGeometry.arcCorner(ConnectedModeState.popoutBarSide, "right") + readonly property string _arcCorner: ConnectorGeometry.arcCorner(win._popoutState.barSide, "right") readonly property real _radius: win._popoutConnectorRadiusRight x: _active ? win._connectorCutoutX(_popoutRightConnectorBlurAnchor.x, _popoutRightConnectorBlurAnchor.width, _arcCorner, _radius) : 0 @@ -312,11 +370,12 @@ PanelWindow { readonly property real _radius: win._effectivePopoutFarStartCcr readonly property bool _active: _popoutBodyBlurAnchor._active && _radius > 0 + readonly property var _rect: SurfaceGeometry.farConnectorRect(win._popoutDescriptor.barSide, win._popoutBodyGeometry, "left", _radius, win._dpr) - x: _active ? Theme.snap(win._farConnectorX(_popoutBodyBlurAnchor.x, _popoutBodyBlurAnchor.y, _popoutBodyBlurAnchor.width, _popoutBodyBlurAnchor.height, ConnectedModeState.popoutBarSide, "left", _radius), win._dpr) : 0 - y: _active ? Theme.snap(win._farConnectorY(_popoutBodyBlurAnchor.x, _popoutBodyBlurAnchor.y, _popoutBodyBlurAnchor.width, _popoutBodyBlurAnchor.height, ConnectedModeState.popoutBarSide, "left", _radius), win._dpr) : 0 - width: _active ? _radius : 0 - height: _active ? _radius : 0 + x: _active ? _rect.x : 0 + y: _active ? _rect.y : 0 + width: _active ? _rect.width : 0 + height: _active ? _rect.height : 0 } Item { @@ -325,11 +384,12 @@ PanelWindow { readonly property real _radius: win._effectivePopoutFarStartCcr readonly property bool _active: _popoutBodyBlurAnchor._active && _radius > 0 + readonly property var _rect: SurfaceGeometry.farBodyCapRect(win._popoutDescriptor.barSide, win._popoutBodyGeometry, "left", _radius, win._dpr) - x: _active ? Theme.snap(win._farBodyCapX(_popoutBodyBlurAnchor.x, _popoutBodyBlurAnchor.width, ConnectedModeState.popoutBarSide, "left", _radius), win._dpr) : 0 - y: _active ? Theme.snap(win._farBodyCapY(_popoutBodyBlurAnchor.y, _popoutBodyBlurAnchor.height, ConnectedModeState.popoutBarSide, "left", _radius), win._dpr) : 0 - width: _active ? _radius : 0 - height: _active ? _radius : 0 + x: _active ? _rect.x : 0 + y: _active ? _rect.y : 0 + width: _active ? _rect.width : 0 + height: _active ? _rect.height : 0 } Item { @@ -338,11 +398,12 @@ PanelWindow { readonly property real _radius: win._effectivePopoutFarEndCcr readonly property bool _active: _popoutBodyBlurAnchor._active && _radius > 0 + readonly property var _rect: SurfaceGeometry.farBodyCapRect(win._popoutDescriptor.barSide, win._popoutBodyGeometry, "right", _radius, win._dpr) - x: _active ? Theme.snap(win._farBodyCapX(_popoutBodyBlurAnchor.x, _popoutBodyBlurAnchor.width, ConnectedModeState.popoutBarSide, "right", _radius), win._dpr) : 0 - y: _active ? Theme.snap(win._farBodyCapY(_popoutBodyBlurAnchor.y, _popoutBodyBlurAnchor.height, ConnectedModeState.popoutBarSide, "right", _radius), win._dpr) : 0 - width: _active ? _radius : 0 - height: _active ? _radius : 0 + x: _active ? _rect.x : 0 + y: _active ? _rect.y : 0 + width: _active ? _rect.width : 0 + height: _active ? _rect.height : 0 } Item { @@ -351,11 +412,12 @@ PanelWindow { readonly property real _radius: win._effectivePopoutFarEndCcr readonly property bool _active: _popoutBodyBlurAnchor._active && _radius > 0 + readonly property var _rect: SurfaceGeometry.farConnectorRect(win._popoutDescriptor.barSide, win._popoutBodyGeometry, "right", _radius, win._dpr) - x: _active ? Theme.snap(win._farConnectorX(_popoutBodyBlurAnchor.x, _popoutBodyBlurAnchor.y, _popoutBodyBlurAnchor.width, _popoutBodyBlurAnchor.height, ConnectedModeState.popoutBarSide, "right", _radius), win._dpr) : 0 - y: _active ? Theme.snap(win._farConnectorY(_popoutBodyBlurAnchor.x, _popoutBodyBlurAnchor.y, _popoutBodyBlurAnchor.width, _popoutBodyBlurAnchor.height, ConnectedModeState.popoutBarSide, "right", _radius), win._dpr) : 0 - width: _active ? _radius : 0 - height: _active ? _radius : 0 + x: _active ? _rect.x : 0 + y: _active ? _rect.y : 0 + width: _active ? _rect.width : 0 + height: _active ? _rect.height : 0 } Item { @@ -363,8 +425,8 @@ PanelWindow { opacity: 0 readonly property bool _active: _popoutFarStartConnectorBlurAnchor.width > 0 && _popoutFarStartConnectorBlurAnchor.height > 0 - readonly property string _barSide: win._farConnectorBarSide(ConnectedModeState.popoutBarSide, "left") - readonly property string _placement: win._farConnectorPlacement(ConnectedModeState.popoutBarSide, "left") + readonly property string _barSide: win._farConnectorBarSide(win._popoutState.barSide, "left") + readonly property string _placement: win._farConnectorPlacement(win._popoutState.barSide, "left") readonly property string _arcCorner: ConnectorGeometry.arcCorner(_barSide, _placement) readonly property real _radius: win._effectivePopoutFarStartCcr @@ -379,8 +441,8 @@ PanelWindow { opacity: 0 readonly property bool _active: _popoutFarEndConnectorBlurAnchor.width > 0 && _popoutFarEndConnectorBlurAnchor.height > 0 - readonly property string _barSide: win._farConnectorBarSide(ConnectedModeState.popoutBarSide, "right") - readonly property string _placement: win._farConnectorPlacement(ConnectedModeState.popoutBarSide, "right") + readonly property string _barSide: win._farConnectorBarSide(win._popoutState.barSide, "right") + readonly property string _placement: win._farConnectorPlacement(win._popoutState.barSide, "right") readonly property string _arcCorner: ConnectorGeometry.arcCorner(_barSide, _placement) readonly property real _radius: win._effectivePopoutFarEndCcr @@ -395,13 +457,12 @@ PanelWindow { opacity: 0 readonly property bool _active: _dockBodyBlurAnchor._active && win._dockConnectorRadiusValue > 0 - readonly property real _w: ConnectorGeometry.connectorWidth(win._dockState.barSide, 0, win._dockConnectorRadiusValue) - readonly property real _h: ConnectorGeometry.connectorHeight(win._dockState.barSide, 0, win._dockConnectorRadiusValue) + readonly property var _rect: SurfaceGeometry.connectorRect(win._dockDescriptor.barSide, win._dockBodyGeometry, "left", 0, win._dockConnectorRadiusValue, win._dpr) - x: _active ? Theme.snap(ConnectorGeometry.connectorX(win._dockState.barSide, _dockBodyBlurAnchor.x, _dockBodyBlurAnchor.width, "left", 0, win._dockConnectorRadiusValue), win._dpr) : 0 - y: _active ? Theme.snap(ConnectorGeometry.connectorY(win._dockState.barSide, _dockBodyBlurAnchor.y, _dockBodyBlurAnchor.height, "left", 0, win._dockConnectorRadiusValue), win._dpr) : 0 - width: _active ? _w : 0 - height: _active ? _h : 0 + x: _active ? _rect.x : 0 + y: _active ? _rect.y : 0 + width: _active ? _rect.width : 0 + height: _active ? _rect.height : 0 } Item { @@ -409,13 +470,12 @@ PanelWindow { opacity: 0 readonly property bool _active: _dockBodyBlurAnchor._active && win._dockConnectorRadiusValue > 0 - readonly property real _w: ConnectorGeometry.connectorWidth(win._dockState.barSide, 0, win._dockConnectorRadiusValue) - readonly property real _h: ConnectorGeometry.connectorHeight(win._dockState.barSide, 0, win._dockConnectorRadiusValue) + readonly property var _rect: SurfaceGeometry.connectorRect(win._dockDescriptor.barSide, win._dockBodyGeometry, "right", 0, win._dockConnectorRadiusValue, win._dpr) - x: _active ? Theme.snap(ConnectorGeometry.connectorX(win._dockState.barSide, _dockBodyBlurAnchor.x, _dockBodyBlurAnchor.width, "right", 0, win._dockConnectorRadiusValue), win._dpr) : 0 - y: _active ? Theme.snap(ConnectorGeometry.connectorY(win._dockState.barSide, _dockBodyBlurAnchor.y, _dockBodyBlurAnchor.height, "right", 0, win._dockConnectorRadiusValue), win._dpr) : 0 - width: _active ? _w : 0 - height: _active ? _h : 0 + x: _active ? _rect.x : 0 + y: _active ? _rect.y : 0 + width: _active ? _rect.width : 0 + height: _active ? _rect.height : 0 } Item { @@ -448,28 +508,26 @@ PanelWindow { id: _notifBodyBlurAnchor visible: false - readonly property bool _active: win._frameActive && win._notifState.visible && win._notifState.bodyW > 0 && win._notifState.bodyH > 0 + readonly property bool _active: win._frameActive && win._notifDescriptor.visible && win._notifBodyGeometry.width > 0 && win._notifBodyGeometry.height > 0 - x: _active ? Theme.snap(win._notifState.bodyX, win._dpr) : 0 - y: _active ? Theme.snap(win._notifState.bodyY, win._dpr) : 0 - width: _active ? Theme.snap(win._notifState.bodyW, win._dpr) : 0 - height: _active ? Theme.snap(win._notifState.bodyH, win._dpr) : 0 + x: _active ? win._notifBodyGeometry.x : 0 + y: _active ? win._notifBodyGeometry.y : 0 + width: _active ? win._notifBodyGeometry.width : 0 + height: _active ? win._notifBodyGeometry.height : 0 } Item { id: _modalBodyBlurAnchor visible: false - readonly property bool _active: win._frameActive && win._modalState.visible && win._modalState.bodyW > 0 && win._modalState.bodyH > 0 + readonly property bool _active: win._frameActive && win._modalDescriptor.visible && win._modalBodyGeometry.width > 0 && win._modalBodyGeometry.height > 0 + readonly property real _dyClamp: win._modalBodyGeometry.dy + readonly property real _dxClamp: win._modalBodyGeometry.dx - // Clamp animX/Y so the blur body shrinks toward the bar edge (same as _popoutBodyBlurAnchor). - readonly property real _dyClamp: ConnectorGeometry.isHorizontal(win._modalState.barSide) ? 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 + x: _active ? win._modalBodyGeometry.x : 0 + y: _active ? win._modalBodyGeometry.y : 0 + width: _active ? win._modalBodyGeometry.width : 0 + height: _active ? win._modalBodyGeometry.height : 0 } Item { @@ -494,13 +552,12 @@ PanelWindow { readonly property real _radius: win._modalConnectorRadiusLeft readonly property bool _active: _modalBodyBlurAnchor._active && _radius > 0 - readonly property real _w: ConnectorGeometry.connectorWidth(win._modalState.barSide, 0, win._modalConnectorRadiusLeft) - readonly property real _h: ConnectorGeometry.connectorHeight(win._modalState.barSide, 0, win._modalConnectorRadiusLeft) + readonly property var _rect: SurfaceGeometry.connectorRect(win._modalDescriptor.barSide, win._modalBodyGeometry, "left", 0, _radius, win._dpr) - x: _active ? Theme.snap(ConnectorGeometry.connectorX(win._modalState.barSide, _modalBodyBlurAnchor.x, _modalBodyBlurAnchor.width, "left", 0, win._modalConnectorRadiusLeft), win._dpr) : 0 - y: _active ? Theme.snap(ConnectorGeometry.connectorY(win._modalState.barSide, _modalBodyBlurAnchor.y, _modalBodyBlurAnchor.height, "left", 0, win._modalConnectorRadiusLeft), win._dpr) : 0 - width: _active ? _w : 0 - height: _active ? _h : 0 + x: _active ? _rect.x : 0 + y: _active ? _rect.y : 0 + width: _active ? _rect.width : 0 + height: _active ? _rect.height : 0 } Item { @@ -509,13 +566,12 @@ PanelWindow { readonly property real _radius: win._modalConnectorRadiusRight readonly property bool _active: _modalBodyBlurAnchor._active && _radius > 0 - readonly property real _w: ConnectorGeometry.connectorWidth(win._modalState.barSide, 0, win._modalConnectorRadiusRight) - readonly property real _h: ConnectorGeometry.connectorHeight(win._modalState.barSide, 0, win._modalConnectorRadiusRight) + readonly property var _rect: SurfaceGeometry.connectorRect(win._modalDescriptor.barSide, win._modalBodyGeometry, "right", 0, _radius, win._dpr) - x: _active ? Theme.snap(ConnectorGeometry.connectorX(win._modalState.barSide, _modalBodyBlurAnchor.x, _modalBodyBlurAnchor.width, "right", 0, win._modalConnectorRadiusRight), win._dpr) : 0 - y: _active ? Theme.snap(ConnectorGeometry.connectorY(win._modalState.barSide, _modalBodyBlurAnchor.y, _modalBodyBlurAnchor.height, "right", 0, win._modalConnectorRadiusRight), win._dpr) : 0 - width: _active ? _w : 0 - height: _active ? _h : 0 + x: _active ? _rect.x : 0 + y: _active ? _rect.y : 0 + width: _active ? _rect.width : 0 + height: _active ? _rect.height : 0 } Item { @@ -552,11 +608,12 @@ PanelWindow { readonly property real _radius: win._effectiveModalFarStartCcr readonly property bool _active: _modalBodyBlurAnchor._active && _radius > 0 + readonly property var _rect: SurfaceGeometry.farConnectorRect(win._modalDescriptor.barSide, win._modalBodyGeometry, "left", _radius, win._dpr) - 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 + x: _active ? _rect.x : 0 + y: _active ? _rect.y : 0 + width: _active ? _rect.width : 0 + height: _active ? _rect.height : 0 } Item { @@ -565,11 +622,12 @@ PanelWindow { readonly property real _radius: win._effectiveModalFarStartCcr readonly property bool _active: _modalBodyBlurAnchor._active && _radius > 0 + readonly property var _rect: SurfaceGeometry.farBodyCapRect(win._modalDescriptor.barSide, win._modalBodyGeometry, "left", _radius, win._dpr) - 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 + x: _active ? _rect.x : 0 + y: _active ? _rect.y : 0 + width: _active ? _rect.width : 0 + height: _active ? _rect.height : 0 } Item { @@ -578,11 +636,12 @@ PanelWindow { readonly property real _radius: win._effectiveModalFarEndCcr readonly property bool _active: _modalBodyBlurAnchor._active && _radius > 0 + readonly property var _rect: SurfaceGeometry.farBodyCapRect(win._modalDescriptor.barSide, win._modalBodyGeometry, "right", _radius, win._dpr) - 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 + x: _active ? _rect.x : 0 + y: _active ? _rect.y : 0 + width: _active ? _rect.width : 0 + height: _active ? _rect.height : 0 } Item { @@ -591,11 +650,12 @@ PanelWindow { readonly property real _radius: win._effectiveModalFarEndCcr readonly property bool _active: _modalBodyBlurAnchor._active && _radius > 0 + readonly property var _rect: SurfaceGeometry.farConnectorRect(win._modalDescriptor.barSide, win._modalBodyGeometry, "right", _radius, win._dpr) - 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 + x: _active ? _rect.x : 0 + y: _active ? _rect.y : 0 + width: _active ? _rect.width : 0 + height: _active ? _rect.height : 0 } Item { @@ -665,13 +725,12 @@ PanelWindow { readonly property real _radius: win._notifConnectorRadiusLeft readonly property bool _active: _notifBodySceneBlurAnchor._active && _radius > 0 - readonly property real _w: ConnectorGeometry.connectorWidth(win._notifState.barSide, 0, win._notifConnectorRadiusLeft) - readonly property real _h: ConnectorGeometry.connectorHeight(win._notifState.barSide, 0, win._notifConnectorRadiusLeft) + readonly property var _rect: SurfaceGeometry.connectorRect(win._notifDescriptor.barSide, _notifBodySceneBlurAnchor, "left", 0, _radius, win._dpr) - x: _active ? Theme.snap(ConnectorGeometry.connectorX(win._notifState.barSide, _notifBodySceneBlurAnchor.x, _notifBodySceneBlurAnchor.width, "left", 0, win._notifConnectorRadiusLeft), win._dpr) : 0 - y: _active ? Theme.snap(ConnectorGeometry.connectorY(win._notifState.barSide, _notifBodySceneBlurAnchor.y, _notifBodySceneBlurAnchor.height, "left", 0, win._notifConnectorRadiusLeft), win._dpr) : 0 - width: _active ? _w : 0 - height: _active ? _h : 0 + x: _active ? _rect.x : 0 + y: _active ? _rect.y : 0 + width: _active ? _rect.width : 0 + height: _active ? _rect.height : 0 } Item { @@ -680,13 +739,12 @@ PanelWindow { readonly property real _radius: win._notifConnectorRadiusRight readonly property bool _active: _notifBodySceneBlurAnchor._active && _radius > 0 - readonly property real _w: ConnectorGeometry.connectorWidth(win._notifState.barSide, 0, win._notifConnectorRadiusRight) - readonly property real _h: ConnectorGeometry.connectorHeight(win._notifState.barSide, 0, win._notifConnectorRadiusRight) + readonly property var _rect: SurfaceGeometry.connectorRect(win._notifDescriptor.barSide, _notifBodySceneBlurAnchor, "right", 0, _radius, win._dpr) - x: _active ? Theme.snap(ConnectorGeometry.connectorX(win._notifState.barSide, _notifBodySceneBlurAnchor.x, _notifBodySceneBlurAnchor.width, "right", 0, win._notifConnectorRadiusRight), win._dpr) : 0 - y: _active ? Theme.snap(ConnectorGeometry.connectorY(win._notifState.barSide, _notifBodySceneBlurAnchor.y, _notifBodySceneBlurAnchor.height, "right", 0, win._notifConnectorRadiusRight), win._dpr) : 0 - width: _active ? _w : 0 - height: _active ? _h : 0 + x: _active ? _rect.x : 0 + y: _active ? _rect.y : 0 + width: _active ? _rect.width : 0 + height: _active ? _rect.height : 0 } Item { @@ -723,11 +781,12 @@ PanelWindow { readonly property real _radius: win._effectiveNotifFarStartCcr readonly property bool _active: _notifBodySceneBlurAnchor._active && _radius > 0 + readonly property var _rect: SurfaceGeometry.farConnectorRect(win._notifDescriptor.barSide, _notifBodySceneBlurAnchor, "left", _radius, win._dpr) - x: _active ? Theme.snap(win._farConnectorX(_notifBodySceneBlurAnchor.x, _notifBodySceneBlurAnchor.y, _notifBodySceneBlurAnchor.width, _notifBodySceneBlurAnchor.height, win._notifState.barSide, "left", _radius), win._dpr) : 0 - y: _active ? Theme.snap(win._farConnectorY(_notifBodySceneBlurAnchor.x, _notifBodySceneBlurAnchor.y, _notifBodySceneBlurAnchor.width, _notifBodySceneBlurAnchor.height, win._notifState.barSide, "left", _radius), win._dpr) : 0 - width: _active ? _radius : 0 - height: _active ? _radius : 0 + x: _active ? _rect.x : 0 + y: _active ? _rect.y : 0 + width: _active ? _rect.width : 0 + height: _active ? _rect.height : 0 } Item { @@ -736,11 +795,12 @@ PanelWindow { readonly property real _radius: win._effectiveNotifFarStartCcr readonly property bool _active: _notifBodySceneBlurAnchor._active && _radius > 0 + readonly property var _rect: SurfaceGeometry.farBodyCapRect(win._notifDescriptor.barSide, _notifBodySceneBlurAnchor, "left", _radius, win._dpr) - x: _active ? Theme.snap(win._farBodyCapX(_notifBodySceneBlurAnchor.x, _notifBodySceneBlurAnchor.width, win._notifState.barSide, "left", _radius), win._dpr) : 0 - y: _active ? Theme.snap(win._farBodyCapY(_notifBodySceneBlurAnchor.y, _notifBodySceneBlurAnchor.height, win._notifState.barSide, "left", _radius), win._dpr) : 0 - width: _active ? _radius : 0 - height: _active ? _radius : 0 + x: _active ? _rect.x : 0 + y: _active ? _rect.y : 0 + width: _active ? _rect.width : 0 + height: _active ? _rect.height : 0 } Item { @@ -749,11 +809,12 @@ PanelWindow { readonly property real _radius: win._effectiveNotifFarEndCcr readonly property bool _active: _notifBodySceneBlurAnchor._active && _radius > 0 + readonly property var _rect: SurfaceGeometry.farBodyCapRect(win._notifDescriptor.barSide, _notifBodySceneBlurAnchor, "right", _radius, win._dpr) - x: _active ? Theme.snap(win._farBodyCapX(_notifBodySceneBlurAnchor.x, _notifBodySceneBlurAnchor.width, win._notifState.barSide, "right", _radius), win._dpr) : 0 - y: _active ? Theme.snap(win._farBodyCapY(_notifBodySceneBlurAnchor.y, _notifBodySceneBlurAnchor.height, win._notifState.barSide, "right", _radius), win._dpr) : 0 - width: _active ? _radius : 0 - height: _active ? _radius : 0 + x: _active ? _rect.x : 0 + y: _active ? _rect.y : 0 + width: _active ? _rect.width : 0 + height: _active ? _rect.height : 0 } Item { @@ -762,11 +823,12 @@ PanelWindow { readonly property real _radius: win._effectiveNotifFarEndCcr readonly property bool _active: _notifBodySceneBlurAnchor._active && _radius > 0 + readonly property var _rect: SurfaceGeometry.farConnectorRect(win._notifDescriptor.barSide, _notifBodySceneBlurAnchor, "right", _radius, win._dpr) - x: _active ? Theme.snap(win._farConnectorX(_notifBodySceneBlurAnchor.x, _notifBodySceneBlurAnchor.y, _notifBodySceneBlurAnchor.width, _notifBodySceneBlurAnchor.height, win._notifState.barSide, "right", _radius), win._dpr) : 0 - y: _active ? Theme.snap(win._farConnectorY(_notifBodySceneBlurAnchor.x, _notifBodySceneBlurAnchor.y, _notifBodySceneBlurAnchor.width, _notifBodySceneBlurAnchor.height, win._notifState.barSide, "right", _radius), win._dpr) : 0 - width: _active ? _radius : 0 - height: _active ? _radius : 0 + x: _active ? _rect.x : 0 + y: _active ? _rect.y : 0 + width: _active ? _rect.width : 0 + height: _active ? _rect.height : 0 } Item { @@ -1017,23 +1079,19 @@ PanelWindow { } function _popoutChromeX() { - const barSide = ConnectedModeState.popoutBarSide; - return ConnectedModeState.popoutBodyX - ((barSide === "top" || barSide === "bottom") ? win._effectivePopoutStartCcr : 0); + return win._popoutChromeGeometry.x; } function _popoutChromeY() { - const barSide = ConnectedModeState.popoutBarSide; - return ConnectedModeState.popoutBodyY - ((barSide === "left" || barSide === "right") ? win._effectivePopoutStartCcr : 0); + return win._popoutChromeGeometry.y; } function _popoutChromeWidth() { - const barSide = ConnectedModeState.popoutBarSide; - return ConnectedModeState.popoutBodyW + ((barSide === "top" || barSide === "bottom") ? win._effectivePopoutStartCcr + win._effectivePopoutEndCcr : 0); + return win._popoutChromeGeometry.width; } function _popoutChromeHeight() { - const barSide = ConnectedModeState.popoutBarSide; - return ConnectedModeState.popoutBodyH + ((barSide === "left" || barSide === "right") ? win._effectivePopoutStartCcr + win._effectivePopoutEndCcr : 0); + return win._popoutChromeGeometry.height; } function _popoutClipX() { @@ -1053,67 +1111,123 @@ PanelWindow { } function _popoutShapeBodyOffsetX() { - const side = ConnectedModeState.popoutBarSide; + const side = win._popoutState.barSide; if (ConnectorGeometry.isHorizontal(side)) return win._effectivePopoutStartCcr; return side === "right" ? win._effectivePopoutFarExtent : 0; } function _popoutShapeBodyOffsetY() { - const side = ConnectedModeState.popoutBarSide; + const side = win._popoutState.barSide; if (ConnectorGeometry.isHorizontal(side)) return side === "bottom" ? win._effectivePopoutFarExtent : 0; return win._effectivePopoutStartCcr; } function _popoutShapeWidth() { - const side = ConnectedModeState.popoutBarSide; + const side = win._popoutState.barSide; if (ConnectorGeometry.isHorizontal(side)) return win._popoutClipWidth() + win._effectivePopoutStartCcr + win._effectivePopoutEndCcr; return win._popoutClipWidth() + win._effectivePopoutFarExtent; } function _popoutShapeHeight() { - const side = ConnectedModeState.popoutBarSide; + const side = win._popoutState.barSide; if (ConnectorGeometry.isHorizontal(side)) return win._popoutClipHeight() + win._effectivePopoutFarExtent; return win._popoutClipHeight() + win._effectivePopoutStartCcr + win._effectivePopoutEndCcr; } function _popoutBodyXInClip() { - return (ConnectedModeState.popoutBarSide === "left" ? _popoutBodyBlurAnchor._dxClamp : 0) - win._popoutFillOverlapXValue; + return (win._popoutState.barSide === "left" ? _popoutBodyBlurAnchor._dxClamp : 0) - win._popoutFillOverlapXValue; } function _popoutBodyYInClip() { - return (ConnectedModeState.popoutBarSide === "top" ? _popoutBodyBlurAnchor._dyClamp : 0) - win._popoutFillOverlapYValue; + return (win._popoutState.barSide === "top" ? _popoutBodyBlurAnchor._dyClamp : 0) - win._popoutFillOverlapYValue; } function _popoutBodyFullWidth() { - return ConnectedModeState.popoutBodyW + win._popoutFillOverlapXValue * 2; + return win._popoutState.bodyW + win._popoutFillOverlapXValue * 2; } function _popoutBodyFullHeight() { - return ConnectedModeState.popoutBodyH + win._popoutFillOverlapYValue * 2; + return win._popoutState.bodyH + win._popoutFillOverlapYValue * 2; + } + + // Active connected surfaces fed to the unified silhouette path. Raw animated + // body rects (no seam/fill overlap); the builder anchors each to the cutout + // edge. Connector radii match what each ConnectedShape would receive. + function _unifiedSurfaces() { + const arr = []; + const p = win._popoutBodyGeometry; + if (win._popoutDescriptor.visible && win._popoutState.screen === win._screenName && p.width > 0 && p.height > 0) + arr.push({ + "side": win._popoutDescriptor.barSide, + "body": {"x": p.x, "y": p.y, "width": p.width, "height": p.height}, + "radii": { + "startCr": win._effectivePopoutStartCcr, + "endCr": win._effectivePopoutEndCcr, + "farStartCr": win._effectivePopoutFarStartCcr, + "farEndCr": win._effectivePopoutFarEndCcr, + "surfaceRadius": win._surfaceRadius + } + }); + const m = win._modalBodyGeometry; + if (win._frameActive && win._modalDescriptor.visible && m.width > 0 && m.height > 0) + arr.push({ + "side": win._modalDescriptor.barSide, + "body": {"x": m.x, "y": m.y, "width": m.width, "height": m.height}, + "radii": { + "startCr": win._effectiveModalStartCcr, + "endCr": win._effectiveModalEndCcr, + "farStartCr": win._effectiveModalFarStartCcr, + "farEndCr": win._effectiveModalFarEndCcr, + "surfaceRadius": win._surfaceRadius + } + }); + const n = win._notifBodyGeometry; + if (win._frameActive && win._notifDescriptor.visible && n.width > 0 && n.height > 0) + arr.push({ + "side": win._notifDescriptor.barSide, + "body": {"x": n.x, "y": n.y, "width": n.width, "height": n.height}, + "radii": { + "startCr": win._effectiveNotifStartCcr, + "endCr": win._effectiveNotifEndCcr, + "farStartCr": win._effectiveNotifFarStartCcr, + "farEndCr": win._effectiveNotifFarEndCcr, + "surfaceRadius": win._surfaceRadius + } + }); + const dk = win._dockBodyGeometry; + if (win._connectedActive && win._dockDescriptor.visible && dk.width > 0 && dk.height > 0) + arr.push({ + "side": win._dockDescriptor.barSide, + "body": {"x": dk.x, "y": dk.y, "width": dk.width, "height": dk.height}, + "radii": { + "startCr": win._dockConnectorRadiusValue, + "endCr": win._dockConnectorRadiusValue, + "farStartCr": 0, + "farEndCr": 0, + "surfaceRadius": win._dockBodyBlurRadiusValue + } + }); + return arr; } function _dockChromeX() { - const dockSide = win._dockState.barSide; - return _dockBodyBlurAnchor.x - ((dockSide === "top" || dockSide === "bottom") ? win._dockConnectorRadiusValue : 0); + return win._dockChromeGeometry.x; } function _dockChromeY() { - const dockSide = win._dockState.barSide; - return _dockBodyBlurAnchor.y - ((dockSide === "left" || dockSide === "right") ? win._dockConnectorRadiusValue : 0); + return win._dockChromeGeometry.y; } function _dockChromeWidth() { - const dockSide = win._dockState.barSide; - return _dockBodyBlurAnchor.width + ((dockSide === "top" || dockSide === "bottom") ? win._dockConnectorRadiusValue * 2 : 0); + return win._dockChromeGeometry.width; } function _dockChromeHeight() { - const dockSide = win._dockState.barSide; - return _dockBodyBlurAnchor.height + ((dockSide === "left" || dockSide === "right") ? win._dockConnectorRadiusValue * 2 : 0); + return win._dockChromeGeometry.height; } function _dockBodyXInChrome() { @@ -1148,38 +1262,6 @@ PanelWindow { return "left"; } - function _farConnectorX(baseX, baseY, bodyWidth, bodyHeight, sourceSide, placement, radius) { - if (sourceSide === "top" || sourceSide === "bottom") - return placement === "left" ? baseX : baseX + bodyWidth - radius; - if (sourceSide === "left") - return baseX + bodyWidth; - return baseX - radius; - } - - function _farConnectorY(baseX, baseY, bodyWidth, bodyHeight, sourceSide, placement, radius) { - if (sourceSide === "top") - return baseY + bodyHeight; - if (sourceSide === "bottom") - return baseY - radius; - return placement === "left" ? baseY : baseY + bodyHeight - radius; - } - - function _farBodyCapX(baseX, bodyWidth, sourceSide, placement, radius) { - if (sourceSide === "top" || sourceSide === "bottom") - return placement === "left" ? baseX : baseX + bodyWidth - radius; - if (sourceSide === "left") - return baseX + bodyWidth - radius; - return baseX; - } - - function _farBodyCapY(baseY, bodyHeight, sourceSide, placement, radius) { - if (sourceSide === "top") - return baseY + bodyHeight - radius; - if (sourceSide === "bottom") - return baseY; - return placement === "left" ? baseY : baseY + bodyHeight - radius; - } - function _connectorCutoutX(connectorX, connectorWidth, arcCorner, radius) { const r = radius === undefined ? win._effectivePopoutCcr : radius; return (arcCorner === "topLeft" || arcCorner === "bottomLeft") ? connectorX - r : connectorX + connectorWidth - r; @@ -1246,7 +1328,7 @@ PanelWindow { return; if (_surfaceRefreshNeedsLayerRecreate) { _surfaceRefreshNeedsLayerRecreate = false; - if (win._connectedActive && !win._disableLayer && (Theme.elevationEnabled || win._surfaceOpacity < 1)) { + if (win._elevationShadow) { _surfaceLayerRecoveryActive = true; surfaceLayerRestoreAction.restart(); } @@ -1332,7 +1414,7 @@ PanelWindow { surfaceRefreshAction.cancel(); surfaceLayerRestoreAction.cancel(); _surfaceRefreshNeedsLayerRecreate = true; - if (win._connectedActive && !win._disableLayer && (Theme.elevationEnabled || win._surfaceOpacity < 1)) + if (win._elevationShadow) _surfaceLayerRecoveryActive = true; win._teardownBlur(); } @@ -1367,9 +1449,12 @@ PanelWindow { id: _connectedSurfaceLayer anchors.fill: parent visible: win._connectedActive - opacity: win._surfaceOpacity - // Skip FBO when disabled, invisible, or when neither elevation nor alpha blend is active - layer.enabled: win._connectedActive && !win._surfaceLayerRecoveryActive && !win._disableLayer && (Theme.elevationEnabled || win._surfaceOpacity < 1) + // Elevation-off draws the silhouette directly at the surface color, so + // group opacity is 1. Elevation-on draws children opaque and applies the + // surface alpha to the whole group beneath the shadow. + opacity: win._elevationShadow ? win._surfaceOpacity : 1 + // FBO is allocated only to source the elevation shadow. + layer.enabled: win._elevationShadow && !win._surfaceLayerRecoveryActive layer.smooth: false layer.effect: MultiEffect { @@ -1390,8 +1475,38 @@ PanelWindow { shadowOpacity: 1 } + // Elevation-off: the entire connected silhouette (frame ring + every + // active chrome) as one SDF in a fragment shader. Analytic fwidth AA → + // crisp at any scale, no FBO; the smooth-min radius is the connector. + ShaderEffect { + anchors.fill: parent + visible: win._connectedActive && !win._elevationShadow + fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/connected_arc.frag.qsb") + + property real widthPx: width + property real heightPx: height + property real cutoutRadius: win.cutoutRadius + property vector4d cutout: Qt.vector4d(win.cutoutLeftInset, win.cutoutTopInset, win.width - win.cutoutRightInset, win.height - win.cutoutBottomInset) + property vector4d surfaceColor: Qt.vector4d(win._surfaceColor.r, win._surfaceColor.g, win._surfaceColor.b, win._surfaceColor.a) + property vector4d chromeRect0: win._sdfSlots[0].rect + property vector4d chromeCorner0: win._sdfSlots[0].corner + property vector4d chromeParam0: win._sdfSlots[0].param + property vector4d chromeRect1: win._sdfSlots[1].rect + property vector4d chromeCorner1: win._sdfSlots[1].corner + property vector4d chromeParam1: win._sdfSlots[1].param + property vector4d chromeRect2: win._sdfSlots[2].rect + property vector4d chromeCorner2: win._sdfSlots[2].corner + property vector4d chromeParam2: win._sdfSlots[2].param + property vector4d chromeRect3: win._sdfSlots[3].rect + property vector4d chromeCorner3: win._sdfSlots[3].corner + property vector4d chromeParam3: win._sdfSlots[3].param + } + + // Elevation-on: opaque children flattened in the FBO so the MultiEffect + // can derive one shadow; the layer applies the surface alpha. FrameBorder { anchors.fill: parent + visible: win._elevationShadow borderColor: win._opaqueSurfaceColor cutoutTopInset: win.cutoutTopInset cutoutBottomInset: win.cutoutBottomInset @@ -1403,11 +1518,11 @@ PanelWindow { Item { id: _connectedChrome anchors.fill: parent - visible: win._connectedActive + visible: win._elevationShadow Item { id: _popoutChrome - visible: ConnectedModeState.popoutVisible && ConnectedModeState.popoutScreen === win._screenName + visible: win._popoutState.visible && win._popoutState.screen === win._screenName x: win._popoutChromeX() y: win._popoutChromeY() width: win._popoutChromeWidth() @@ -1424,7 +1539,7 @@ PanelWindow { ConnectedShape { id: _popoutShape visible: _popoutBodyBlurAnchor._active && _popoutBodyBlurAnchor.width > 0 && _popoutBodyBlurAnchor.height > 0 - barSide: ConnectedModeState.popoutBarSide + barSide: win._popoutState.barSide bodyWidth: win._popoutClipWidth() bodyHeight: win._popoutClipHeight() connectorRadius: win._effectivePopoutCcr @@ -1474,8 +1589,8 @@ PanelWindow { connectorRadius: win._dockConnectorRadiusValue color: win._opaqueSurfaceColor dpr: win._dpr - x: Theme.snap(ConnectorGeometry.connectorX(win._dockState.barSide, _dockBodyBlurAnchor.x, _dockBodyBlurAnchor.width, "left", 0, win._dockConnectorRadiusValue) - _dockChrome.x, win._dpr) - y: Theme.snap(ConnectorGeometry.connectorY(win._dockState.barSide, _dockBodyBlurAnchor.y, _dockBodyBlurAnchor.height, "left", 0, win._dockConnectorRadiusValue) - _dockChrome.y, win._dpr) + x: Theme.snap(_dockLeftConnectorBlurAnchor.x - _dockChrome.x, win._dpr) + y: Theme.snap(_dockLeftConnectorBlurAnchor.y - _dockChrome.y, win._dpr) } ConnectedCorner { @@ -1487,31 +1602,30 @@ PanelWindow { connectorRadius: win._dockConnectorRadiusValue color: win._opaqueSurfaceColor dpr: win._dpr - x: Theme.snap(ConnectorGeometry.connectorX(win._dockState.barSide, _dockBodyBlurAnchor.x, _dockBodyBlurAnchor.width, "right", 0, win._dockConnectorRadiusValue) - _dockChrome.x, win._dpr) - y: Theme.snap(ConnectorGeometry.connectorY(win._dockState.barSide, _dockBodyBlurAnchor.y, _dockBodyBlurAnchor.height, "right", 0, win._dockConnectorRadiusValue) - _dockChrome.y, win._dpr) + x: Theme.snap(_dockRightConnectorBlurAnchor.x - _dockChrome.x, win._dpr) + y: Theme.snap(_dockRightConnectorBlurAnchor.y - _dockChrome.y, win._dpr) } } } Item { id: _notifChrome - visible: _notifBodySceneBlurAnchor._active + visible: win._elevationShadow && _notifBodySceneBlurAnchor._active readonly property string _notifSide: win._notifState.barSide readonly property bool _isHoriz: _notifSide === "top" || _notifSide === "bottom" readonly property real _startCcr: win._effectiveNotifStartCcr readonly property real _endCcr: win._effectiveNotifEndCcr readonly property real _farExtent: win._effectiveNotifFarExtent - readonly property real _bodyOffsetX: _isHoriz ? _startCcr : (_notifSide === "right" ? _farExtent : 0) - readonly property real _bodyOffsetY: _isHoriz ? (_notifSide === "bottom" ? _farExtent : 0) : _startCcr readonly property real _bodyW: Theme.snap(_notifBodySceneBlurAnchor.width, win._dpr) readonly property real _bodyH: Theme.snap(_notifBodySceneBlurAnchor.height, win._dpr) + readonly property var _geometry: SurfaceGeometry.chromeBounds(_notifBodySceneBlurAnchor, _notifSide, _startCcr, _endCcr, _farExtent, win._dpr) z: _isHoriz ? 0 : -1 - x: Theme.snap(_notifBodySceneBlurAnchor.x - _bodyOffsetX, win._dpr) - y: Theme.snap(_notifBodySceneBlurAnchor.y - _bodyOffsetY, 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) + x: _geometry.x + y: _geometry.y + width: _geometry.width + height: _geometry.height ConnectedShape { visible: _notifBodySceneBlurAnchor._active && _notifBodySceneBlurAnchor.width > 0 && _notifBodySceneBlurAnchor.height > 0 @@ -1534,7 +1648,7 @@ PanelWindow { // instead of sliding over bar widgets (mirrors the popout `_popoutClip`). Item { id: _modalClip - visible: _modalBodyBlurAnchor._active + visible: win._elevationShadow && _modalBodyBlurAnchor._active z: 1 readonly property string _modalSide: win._modalState.barSide @@ -1558,15 +1672,14 @@ PanelWindow { 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) + readonly property var _geometry: SurfaceGeometry.chromeBounds(_modalBodyBlurAnchor, _modalSide, _startCcr, _endCcr, _farExtent, 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) + x: Theme.snap(_geometry.x - _modalClip.x, win._dpr) + y: Theme.snap(_geometry.y - _modalClip.y, win._dpr) + width: _geometry.width + height: _geometry.height ConnectedShape { visible: _modalBodyBlurAnchor._active && _modalChrome._bodyW > 0 && _modalChrome._bodyH > 0 diff --git a/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml b/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml index 41094650..c9676296 100644 --- a/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml +++ b/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml @@ -513,13 +513,30 @@ QtObject { ConnectedModeState.clearNotificationState(screenName); return; } + const bodyRect = { + x: minX, + y: minY, + width: maxXEnd - minX, + height: maxYEnd - minY + }; ConnectedModeState.setNotificationState(screenName, { + kind: "notification", + screenName: screenName, + phase: "open", visible: true, + presented: true, barSide: notifBarSide, + bodyRect: bodyRect, + animationOffset: { + x: 0, + y: 0 + }, + scale: 1, + opacity: Theme.connectedSurfaceColor.a, bodyX: minX, bodyY: minY, - bodyW: maxXEnd - minX, - bodyH: maxYEnd - minY, + bodyW: bodyRect.width, + bodyH: bodyRect.height, omitStartConnector: _notificationOmitStartConnector(), omitEndConnector: _notificationOmitEndConnector() }); diff --git a/quickshell/Shaders/frag/connected_arc.frag b/quickshell/Shaders/frag/connected_arc.frag new file mode 100644 index 00000000..c19a8d0a --- /dev/null +++ b/quickshell/Shaders/frag/connected_arc.frag @@ -0,0 +1,92 @@ +#version 450 + +// Connected Frame Mode silhouette as a signed-distance field: the frame ring +// (an inverted rounded rectangle) smooth-unioned with each active chrome +// (popout/modal, dock, notification). The smooth-min radius IS the connector +// fillet. Antialiasing is analytic via fwidth -> crisp at any scale, no FBO. + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float widthPx; + float heightPx; + float cutoutRadius; + vec4 cutout; // inner cutout edges in px: x=left y=top z=right w=bottom + vec4 surfaceColor; // straight (non-premultiplied) rgba + // Up to four chrome slots. rect = x,y,w,h (px). corner = per-corner radii + // (topLeft, topRight, bottomRight, bottomLeft). param = connectorR, active, 0, 0 + vec4 chromeRect0; + vec4 chromeCorner0; + vec4 chromeParam0; + vec4 chromeRect1; + vec4 chromeCorner1; + vec4 chromeParam1; + vec4 chromeRect2; + vec4 chromeCorner2; + vec4 chromeParam2; + vec4 chromeRect3; + vec4 chromeCorner3; + vec4 chromeParam3; +} ubuf; + +float sdBox(vec2 p, vec2 c, vec2 hs) { + vec2 q = abs(p - c) - hs; + return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))); +} + +float sdRoundBox(vec2 p, vec2 c, vec2 hs, float r) { + r = min(r, min(hs.x, hs.y)); + vec2 q = abs(p - c) - hs + r; + return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - r; +} + +// Per-corner rounded box. r = (topLeft, topRight, bottomRight, bottomLeft). +float sdRoundBox4(vec2 p, vec2 c, vec2 hs, vec4 r) { + p -= c; + float rr = (p.x >= 0.0) ? (p.y >= 0.0 ? r.z : r.y) : (p.y >= 0.0 ? r.w : r.x); + rr = min(rr, min(hs.x, hs.y)); + vec2 q = abs(p) - hs + rr; + return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - rr; +} + +// Circular smooth-min: blends two SDFs with a fillet of radius k. +float smin(float a, float b, float k) { + if (k <= 0.0) + return min(a, b); + return max(k, min(a, b)) - length(max(vec2(k) - vec2(a, b), vec2(0.0))); +} + +float chromeDist(vec2 px, vec4 rect, vec4 corner) { + vec2 c = rect.xy + rect.zw * 0.5; + return sdRoundBox4(px, c, rect.zw * 0.5, corner); +} + +void main() { + vec2 px = qt_TexCoord0 * vec2(ubuf.widthPx, ubuf.heightPx); + + // Frame ring: inside the screen rect AND outside the rounded cutout (hole). + vec2 sc = vec2(ubuf.widthPx, ubuf.heightPx) * 0.5; + float dOuter = sdBox(px, sc, sc); + vec2 cutC = vec2((ubuf.cutout.x + ubuf.cutout.z) * 0.5, (ubuf.cutout.y + ubuf.cutout.w) * 0.5); + vec2 cutH = vec2((ubuf.cutout.z - ubuf.cutout.x) * 0.5, (ubuf.cutout.w - ubuf.cutout.y) * 0.5); + float dCut = sdRoundBox(px, cutC, cutH, ubuf.cutoutRadius); + float d = max(dOuter, -dCut); + + // Smooth-union the active chrome surfaces; smin radius = connector fillet. + if (ubuf.chromeParam0.y > 0.5) + d = smin(d, chromeDist(px, ubuf.chromeRect0, ubuf.chromeCorner0), ubuf.chromeParam0.x); + if (ubuf.chromeParam1.y > 0.5) + d = smin(d, chromeDist(px, ubuf.chromeRect1, ubuf.chromeCorner1), ubuf.chromeParam1.x); + if (ubuf.chromeParam2.y > 0.5) + d = smin(d, chromeDist(px, ubuf.chromeRect2, ubuf.chromeCorner2), ubuf.chromeParam2.x); + if (ubuf.chromeParam3.y > 0.5) + d = smin(d, chromeDist(px, ubuf.chromeRect3, ubuf.chromeCorner3), ubuf.chromeParam3.x); + + float fw = max(fwidth(d), 1e-4); + float cov = 1.0 - smoothstep(-fw, fw, d); + float a = ubuf.surfaceColor.a * cov * ubuf.qt_Opacity; + fragColor = vec4(ubuf.surfaceColor.rgb * a, a); +} diff --git a/quickshell/Shaders/qsb/connected_arc.frag.qsb b/quickshell/Shaders/qsb/connected_arc.frag.qsb new file mode 100644 index 00000000..5e15a97d Binary files /dev/null and b/quickshell/Shaders/qsb/connected_arc.frag.qsb differ diff --git a/quickshell/Widgets/DankPopoutConnected.qml b/quickshell/Widgets/DankPopoutConnected.qml index 9f7a2f54..53e4ab37 100644 --- a/quickshell/Widgets/DankPopoutConnected.qml +++ b/quickshell/Widgets/DankPopoutConnected.qml @@ -38,8 +38,6 @@ Item { property bool fullHeightSurface: false property bool _primeContent: false property bool _resizeActive: false - property string _chromeClaimId: "" - property int _connectedChromeSerial: 0 property real _chromeAnimTravelX: 1 property real _chromeAnimTravelY: 1 property bool _fullSyncQueued: false @@ -100,6 +98,43 @@ Item { onTriggered: root._flushSync() } + ConnectedSurfaceLease { + id: chromeLease + claimPrefix: root.layerNamespace + screenName: root.screen ? root.screen.name : "" + enabled: root.frameOwnsConnectedChrome + active: contentWindow.visible || root.shouldBeVisible + presented: contentWindow.visible || root.shouldBeVisible + renewTokenOnRecovery: false + isCurrentOwner: function(name) { + return PopoutManager.isCurrentPopout(root.popoutHandle, name); + } + hasOwner: function(_name, ownerId) { + return ConnectedModeState.hasPopoutOwner(ownerId); + } + statePresent: function(name, ownerId) { + return ConnectedModeState.hasPopoutOwner(ownerId) && ConnectedModeState.hasSurfaceDescriptor(name, "popout", ownerId); + } + claimState: function(_name, state, ownerId) { + return ConnectedModeState.claimPopout(ownerId, state); + } + ensureState: function(_name, state, ownerId) { + if (!ConnectedModeState.hasPopoutOwner(ownerId)) + return false; + return ConnectedModeState.updatePopout(ownerId, state); + } + releaseState: function(_name, ownerId) { + return ConnectedModeState.releasePopout(ownerId); + } + updateAnimationState: function(_name, ownerId, animX, animY) { + return ConnectedModeState.setPopoutAnim(ownerId, animX, animY); + } + updateBodyState: function(_name, ownerId, bodyX, bodyY, bodyW, bodyH) { + return ConnectedModeState.setPopoutBody(ownerId, bodyX, bodyY, bodyW, bodyH); + } + onRecoveryRequested: root._queueFullSync() + } + property var _lastOpenedScreen: null property bool isClosing: false @@ -169,11 +204,6 @@ Item { setBarContext(pos, bottomGap); } - function _nextChromeClaimId() { - _connectedChromeSerial += 1; - return layerNamespace + ":" + _connectedChromeSerial + ":" + (new Date()).getTime(); - } - function _captureChromeAnimTravel() { _chromeAnimTravelX = Math.max(1, Math.abs(contentContainer.offsetX)); _chromeAnimTravelY = Math.max(1, Math.abs(contentContainer.offsetY)); @@ -203,15 +233,35 @@ Item { function _connectedChromeState(visibleOverride) { const visible = visibleOverride !== undefined ? !!visibleOverride : contentWindow.visible; + const presented = contentWindow.visible || root.shouldBeVisible; + const phase = root.isClosing ? "closing" : (!presented ? "hidden" : (!contentWindow.visible && root.shouldBeVisible ? "opening" : "open")); + const bodyRect = { + "x": root.alignedX, + "y": root.renderedAlignedY, + "width": root.alignedWidth, + "height": root.renderedAlignedHeight + }; + const animationOffset = { + "x": _connectedChromeAnimX(), + "y": _connectedChromeAnimY() + }; return { + "kind": "popout", + "screenName": root.screen ? root.screen.name : "", + "phase": phase, "visible": visible, + "presented": presented, "barSide": contentContainer.connectedBarSide, + "bodyRect": bodyRect, + "animationOffset": animationOffset, + "scale": 1, + "opacity": Theme.connectedSurfaceColor.a, "bodyX": root.alignedX, "bodyY": root.renderedAlignedY, "bodyW": root.alignedWidth, "bodyH": root.renderedAlignedHeight, - "animX": _connectedChromeAnimX(), - "animY": _connectedChromeAnimY(), + "animX": animationOffset.x, + "animY": animationOffset.y, "screen": root.screen ? root.screen.name : "", "omitStartConnector": root._closeGapOmitStartConnector(), "omitEndConnector": root._closeGapOmitEndConnector() @@ -219,27 +269,13 @@ Item { } function _publishConnectedChromeState(forceClaim, visibleOverride) { - if (!root.frameOwnsConnectedChrome || !root.screen || !_chromeClaimId) + if (!root.frameOwnsConnectedChrome || !root.screen) return false; - - const screenName = root.screen.name; - const isCurrent = PopoutManager.isCurrentPopout(popoutHandle, screenName); - if (!ConnectedModeState.hasPopoutOwner(_chromeClaimId)) { - if (!isCurrent) - return false; - forceClaim = true; - } else if (forceClaim && !isCurrent) { - return false; - } - - const state = _connectedChromeState(visibleOverride); - return forceClaim ? ConnectedModeState.claimPopout(_chromeClaimId, state) : ConnectedModeState.updatePopout(_chromeClaimId, state); + return chromeLease.publish(_connectedChromeState(visibleOverride), !!forceClaim); } function _releaseConnectedChromeState() { - if (_chromeClaimId) - ConnectedModeState.releasePopout(_chromeClaimId); - _chromeClaimId = ""; + chromeLease.release(); } // ─── Exposed animation state for ConnectedModeState ──────────────────── @@ -258,16 +294,11 @@ Item { } if (!contentWindow.visible && !shouldBeVisible) return; - if (!_chromeClaimId) { - if (!PopoutManager.isCurrentPopout(popoutHandle, root.screen.name)) - return; - _chromeClaimId = _nextChromeClaimId(); - } - _publishConnectedChromeState(!ConnectedModeState.hasPopoutOwner(_chromeClaimId)); + _publishConnectedChromeState(false); } function _syncPopoutAnim(axis) { - if (!root.frameOwnsConnectedChrome || !_chromeClaimId) + if (!root.frameOwnsConnectedChrome || !chromeLease.claimId) return; if (!contentWindow.visible && !shouldBeVisible) return; @@ -276,25 +307,15 @@ Item { const syncY = axis === "y" && (barSide === "top" || barSide === "bottom"); if (!syncX && !syncY) return; - if (!ConnectedModeState.hasPopoutOwner(_chromeClaimId)) { - if (root.screen && PopoutManager.isCurrentPopout(popoutHandle, root.screen.name)) - _queueFullSync(); - return; - } - ConnectedModeState.setPopoutAnim(_chromeClaimId, syncX ? _connectedChromeAnimX() : undefined, syncY ? _connectedChromeAnimY() : undefined); + chromeLease.updateAnim(syncX ? _connectedChromeAnimX() : undefined, syncY ? _connectedChromeAnimY() : undefined); } function _syncPopoutBody() { - if (!root.frameOwnsConnectedChrome || !_chromeClaimId) + if (!root.frameOwnsConnectedChrome || !chromeLease.claimId) return; if (!contentWindow.visible && !shouldBeVisible) return; - if (!ConnectedModeState.hasPopoutOwner(_chromeClaimId)) { - if (root.screen && PopoutManager.isCurrentPopout(popoutHandle, root.screen.name)) - _queueFullSync(); - return; - } - ConnectedModeState.setPopoutBody(_chromeClaimId, root.alignedX, root.renderedAlignedY, root.alignedWidth, root.renderedAlignedHeight); + chromeLease.updateBody(root.alignedX, root.renderedAlignedY, root.alignedWidth, root.renderedAlignedHeight); } property bool _animSyncQueued: false @@ -345,13 +366,10 @@ Item { Connections { target: contentWindow function onVisibleChanged() { - if (contentWindow.visible) { - if (!root._chromeClaimId) - root._chromeClaimId = root._nextChromeClaimId(); + if (contentWindow.visible) root._publishConnectedChromeState(true); - } else { + else root._releaseConnectedChromeState(); - } } } @@ -359,11 +377,8 @@ Item { target: SettingsData function onConnectedFrameModeActiveChanged() { if (root.frameOwnsConnectedChrome) { - if ((contentWindow.visible || root.shouldBeVisible) && root.screen && PopoutManager.isCurrentPopout(root.popoutHandle, root.screen.name)) { - if (!root._chromeClaimId) - root._chromeClaimId = root._nextChromeClaimId(); + if ((contentWindow.visible || root.shouldBeVisible) && root.screen && PopoutManager.isCurrentPopout(root.popoutHandle, root.screen.name)) root._publishConnectedChromeState(true); - } } else { root._releaseConnectedChromeState(); } @@ -376,16 +391,17 @@ Item { Connections { target: ConnectedModeState function onPopoutOwnerIdChanged() { - if ((contentWindow.visible || root.shouldBeVisible) && root.screen && PopoutManager.isCurrentPopout(root.popoutHandle, root.screen.name) && !ConnectedModeState.hasPopoutOwner(root._chromeClaimId)) - root._queueFullSync(); + chromeLease.checkOwnershipRecovery(); + } + function onSurfaceDescriptorsChanged() { + chromeLease.checkStateRecovery(); } } Connections { target: PopoutManager function onPopoutChanged() { - if ((contentWindow.visible || root.shouldBeVisible) && root.screen && PopoutManager.isCurrentPopout(root.popoutHandle, root.screen.name)) - root._queueFullSync(); + chromeLease.requestRecovery(); } } @@ -422,10 +438,10 @@ Item { } if (root.frameOwnsConnectedChrome) { - _chromeClaimId = _nextChromeClaimId(); + chromeLease.beginClaim(); _publishConnectedChromeState(true, true); } else { - _chromeClaimId = ""; + chromeLease.release(); } if (screenChanged) {