diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 8c80631f..3cc33adc 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -185,8 +185,15 @@ Singleton { onAnimationVariantChanged: saveSettings() property int motionEffect: SettingsData.AnimationEffect.Standard onMotionEffectChanged: saveSettings() - property int directionalAnimationMode: 0 - onDirectionalAnimationModeChanged: saveSettings() + property int directionalAnimationMode: 1 + onDirectionalAnimationModeChanged: { + const normalized = directionalAnimationMode === 3 ? 3 : 1; + if (directionalAnimationMode !== normalized) { + directionalAnimationMode = normalized; + return; + } + saveSettings(); + } property bool m3ElevationEnabled: true onM3ElevationEnabledChanged: saveSettings() property int m3ElevationIntensity: 12 @@ -246,7 +253,13 @@ Singleton { onFrameLauncherEmergeSideChanged: saveSettings() readonly property string frameModalEmergeSide: frameLauncherEmergeSide === "top" ? "bottom" : "top" property int previousDirectionalMode: 1 - onPreviousDirectionalModeChanged: saveSettings() + onPreviousDirectionalModeChanged: { + if (previousDirectionalMode !== 1) { + previousDirectionalMode = 1; + return; + } + saveSettings(); + } property var connectedFrameBarStyleBackups: ({}) onConnectedFrameBarStyleBackupsChanged: saveSettings() readonly property bool connectedFrameModeActive: frameEnabled && motionEffect === SettingsData.AnimationEffect.Directional && directionalAnimationMode === 3 diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 196cac17..218b1305 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -51,7 +51,7 @@ var SPEC = { enableRippleEffects: { def: true }, animationVariant: { def: 0 }, motionEffect: { def: 0 }, - directionalAnimationMode: { def: 0 }, + directionalAnimationMode: { def: 1 }, previousDirectionalMode: { def: 1 }, m3ElevationEnabled: { def: true }, m3ElevationIntensity: { def: 12 }, diff --git a/quickshell/DMSShell.qml b/quickshell/DMSShell.qml index 0a9f6b73..91083a1a 100644 --- a/quickshell/DMSShell.qml +++ b/quickshell/DMSShell.qml @@ -334,7 +334,6 @@ Item { sourceComponent: Component { DankDashPopout { id: dankDashPopout - onPopoutClosed: PopoutService.unloadDankDash() } } } diff --git a/quickshell/Modals/Common/DankModalConnected.qml b/quickshell/Modals/Common/DankModalConnected.qml index 7cb5ccff..4614194f 100644 --- a/quickshell/Modals/Common/DankModalConnected.qml +++ b/quickshell/Modals/Common/DankModalConnected.qml @@ -586,8 +586,6 @@ Item { } return 0; } - if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) - return 0; if (slide && !directionalEffect && !depthEffect) return 15; if (directionalEffect) { @@ -630,8 +628,6 @@ Item { } return 0; } - if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) - return 0; if (slide && !directionalEffect && !depthEffect) return -30; if (directionalEffect) { @@ -674,7 +670,7 @@ Item { onAnimYChanged: if (root.frameOwnsConnectedChrome) root._syncModalAnim() - readonly property real computedScaleCollapsed: (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) ? 0.0 : root.animationScaleCollapsed + readonly property real computedScaleCollapsed: root.animationScaleCollapsed property real scaleValue: root.shouldBeVisible ? 1.0 : computedScaleCollapsed Behavior on animX { diff --git a/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml b/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml index 5afcdf75..b6dc7380 100644 --- a/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml +++ b/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml @@ -1,5 +1,6 @@ import QtQuick import qs.Common +import qs.Services Item { id: root diff --git a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalConnected.qml b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalConnected.qml index cf6f045f..9431062a 100644 --- a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalConnected.qml +++ b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalConnected.qml @@ -713,7 +713,7 @@ Item { // Declarative bindings — snap applied at render layer (contentWrapper x/y) property real animX: root._motionActive ? 0 : root._frozenMotionX property real animY: root._motionActive ? 0 : root._frozenMotionY - property real scaleValue: root._motionActive ? 1.0 : (Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed)) + property real scaleValue: root._motionActive ? 1.0 : Theme.effectScaleCollapsed onAnimXChanged: if (root.frameOwnsConnectedChrome) root._syncModalAnim() @@ -737,7 +737,7 @@ Item { } Behavior on scaleValue { - enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2)) + enabled: root.animationsEnabled DankAnim { duration: Theme.variantDuration(root.launcherAnimationDuration, root._motionActive) easing.bezierCurve: root._motionActive ? root.launcherEnterCurve : root.launcherExitCurve diff --git a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalStandalone.qml b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalStandalone.qml index 237c967b..17a8031a 100644 --- a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalStandalone.qml +++ b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalStandalone.qml @@ -61,6 +61,20 @@ Item { readonly property int modalHeight: Math.min(baseHeight, screenHeight - 100) readonly property real modalX: (screenWidth - modalWidth) / 2 readonly property real modalY: (screenHeight - modalHeight) / 2 + readonly property var shadowLevel: Theme.elevationLevel3 + readonly property real shadowFallbackOffset: 6 + readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0 + readonly property real shadowPad: Theme.snap(shadowRenderPadding, dpr) + readonly property real alignedWidth: Theme.px(modalWidth, dpr) + readonly property real alignedHeight: Theme.px(modalHeight, dpr) + readonly property real alignedX: Theme.snap(modalX, dpr) + readonly property real alignedY: Theme.snap(modalY, dpr) + readonly property real windowX: Math.max(0, Theme.snap(alignedX - shadowPad, dpr)) + readonly property real windowY: Math.max(0, Theme.snap(alignedY - shadowPad, dpr)) + readonly property real contentX: Theme.snap(alignedX - windowX, dpr) + readonly property real contentY: Theme.snap(alignedY - windowY, dpr) + readonly property real windowWidth: alignedWidth + contentX + shadowPad + readonly property real windowHeight: alignedHeight + contentY + shadowPad readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) readonly property real cornerRadius: Theme.cornerRadius @@ -277,6 +291,57 @@ Item { } } + PanelWindow { + id: clickCatcher + screen: launcherWindow.screen + visible: spotlightOpen + color: "transparent" + updatesEnabled: false + + WlrLayershell.namespace: "dms:spotlight:clickcatcher" + WlrLayershell.layer: WlrLayershell.Top + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + + anchors { + top: true + bottom: true + left: true + right: true + } + + mask: Region { + item: outsideClickMask + + Region { + item: outsideClickHole + intersection: Intersection.Subtract + } + } + + Item { + id: outsideClickMask + visible: false + anchors.fill: parent + } + + Rectangle { + id: outsideClickHole + visible: false + color: "transparent" + x: root.alignedX + y: root.alignedY + width: root.alignedWidth + height: root.alignedHeight + } + + MouseArea { + anchors.fill: parent + enabled: spotlightOpen + onClicked: root.hide() + } + } + PanelWindow { id: launcherWindow visible: spotlightOpen || isClosing @@ -286,10 +351,10 @@ Item { WindowBlur { targetWindow: launcherWindow readonly property real s: Math.min(1, modalContainer.scale) - blurX: root.modalX + root.modalWidth * (1 - s) * 0.5 - blurY: root.modalY + root.modalHeight * (1 - s) * 0.5 - blurWidth: (contentVisible && modalContainer.opacity > 0) ? root.modalWidth * s : 0 - blurHeight: (contentVisible && modalContainer.opacity > 0) ? root.modalHeight * s : 0 + blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + blurWidth: (contentVisible && modalContainer.opacity > 0) ? modalContainer.width * s : 0 + blurHeight: (contentVisible && modalContainer.opacity > 0) ? modalContainer.height * s : 0 blurRadius: root.cornerRadius } @@ -308,60 +373,44 @@ Item { return WlrLayershell.Top; } } + WlrLayershell.exclusiveZone: -1 WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None anchors { top: true - bottom: true left: true - right: true } + WlrLayershell.margins { + left: root.windowX + top: root.windowY + right: 0 + bottom: 0 + } + + implicitWidth: root.windowWidth + implicitHeight: root.windowHeight + mask: Region { - item: spotlightOpen ? fullScreenMask : null - } - - Item { - id: fullScreenMask - anchors.fill: parent + item: launcherInputMask } Rectangle { - id: backgroundDarken - anchors.fill: parent - color: "black" - opacity: contentVisible && SettingsData.modalDarkenBackground ? 0.5 : 0 - visible: contentVisible || opacity > 0 - - Behavior on opacity { - DankAnim { - duration: Theme.modalAnimationDuration - easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized - } - } - } - - MouseArea { - anchors.fill: parent - enabled: spotlightOpen - onClicked: mouse => { - var contentX = modalContainer.x; - var contentY = modalContainer.y; - var contentW = modalContainer.width; - var contentH = modalContainer.height; - - if (mouse.x < contentX || mouse.x > contentX + contentW || mouse.y < contentY || mouse.y > contentY + contentH) { - root.hide(); - } - } + id: launcherInputMask + visible: false + color: "transparent" + x: modalContainer.x + y: modalContainer.y + width: modalContainer.width + height: modalContainer.height } Item { id: modalContainer - x: root.modalX - y: root.modalY - width: root.modalWidth - height: root.modalHeight + x: root.contentX + y: root.contentY + width: root.alignedWidth + height: root.alignedHeight visible: contentVisible || opacity > 0 opacity: contentVisible ? 1 : 0 @@ -385,8 +434,8 @@ Item { ElevationShadow { id: launcherShadowLayer anchors.fill: parent - level: Theme.elevationLevel3 - fallbackOffset: 6 + level: root.shadowLevel + fallbackOffset: root.shadowFallbackOffset targetColor: root.backgroundColor borderColor: root.borderColor borderWidth: root.borderWidth diff --git a/quickshell/Modules/ControlCenter/ControlCenterPopout.qml b/quickshell/Modules/ControlCenter/ControlCenterPopout.qml index 72536ca4..5898406a 100644 --- a/quickshell/Modules/ControlCenter/ControlCenterPopout.qml +++ b/quickshell/Modules/ControlCenter/ControlCenterPopout.qml @@ -86,14 +86,7 @@ DankPopout { } popupWidth: 550 - popupHeight: { - if (SettingsData.connectedFrameModeActive) - return targetPopupHeight; - const screenHeight = (triggerScreen?.height ?? 1080); - const maxHeight = screenHeight - 100; - const contentHeight = contentLoader.item && contentLoader.item.implicitHeight > 0 ? contentLoader.item.implicitHeight + 20 : 400; - return Math.min(maxHeight, contentHeight); - } + popupHeight: targetPopupHeight triggerWidth: 80 positioning: "" screen: triggerScreen diff --git a/quickshell/Modules/DankDash/DankDashPopout.qml b/quickshell/Modules/DankDash/DankDashPopout.qml index 17124090..3136deaa 100644 --- a/quickshell/Modules/DankDash/DankDashPopout.qml +++ b/quickshell/Modules/DankDash/DankDashPopout.qml @@ -53,7 +53,8 @@ DankPopout { function __hideDropdowns() { __volumeCloseTimer.stop(); __dropdownType = 0; - __mediaTabRef?.resetDropdownStates(); + if (__mediaTabRef && typeof __mediaTabRef.resetDropdownStates === "function") + __mediaTabRef.resetDropdownStates(); } function __startCloseTimer() { @@ -74,7 +75,11 @@ DankPopout { } } - overlayContent: Component { + overlayContent: shouldBeVisible ? mediaDropdownOverlayComponent : null + + Component { + id: mediaDropdownOverlayComponent + MediaDropdownOverlay { dropdownType: root.__dropdownType anchorPos: root.__dropdownAnchor @@ -182,11 +187,8 @@ DankPopout { Connections { target: root function onShouldBeVisibleChanged() { - if (root.shouldBeVisible) { - Qt.callLater(function () { - mainContainer.forceActiveFocus(); - }); - } + if (root.shouldBeVisible) + mainContainer.forceActiveFocus(); } } @@ -378,6 +380,10 @@ DankPopout { section: root.triggerSection barPosition: root.effectiveBarPosition Component.onCompleted: root.__mediaTabRef = this + Component.onDestruction: { + if (root.__mediaTabRef === this) + root.__mediaTabRef = null; + } onShowVolumeDropdown: (pos, screen, rightEdge, player, players) => { root.__showVolumeDropdown(pos, rightEdge, player, players); } diff --git a/quickshell/Modules/DankDash/MediaDropdownOverlay.qml b/quickshell/Modules/DankDash/MediaDropdownOverlay.qml index 609f8661..4f9e3fbe 100644 --- a/quickshell/Modules/DankDash/MediaDropdownOverlay.qml +++ b/quickshell/Modules/DankDash/MediaDropdownOverlay.qml @@ -50,11 +50,9 @@ Item { } function volumeAreaExited() { - __volumeHoverCount--; - Qt.callLater(() => { - if (__volumeHoverCount <= 0) - panelExited(); - }); + __volumeHoverCount = Math.max(0, __volumeHoverCount - 1); + if (__volumeHoverCount === 0) + panelExited(); } readonly property Item __activePanel: { diff --git a/quickshell/Modules/Settings/TypographyMotionTab.qml b/quickshell/Modules/Settings/TypographyMotionTab.qml index 56e23a59..5b6e1668 100644 --- a/quickshell/Modules/Settings/TypographyMotionTab.qml +++ b/quickshell/Modules/Settings/TypographyMotionTab.qml @@ -203,7 +203,7 @@ Item { SettingsDropdownRow { visible: SettingsData.motionEffect === 1 tab: "typography" - tags: ["animation", "directional", "behavior", "overlap", "sticky", "roll", "connected"] + tags: ["animation", "directional", "behavior", "fluid", "connected"] settingKey: "directionalAnimationMode" text: I18n.tr("Directional Behavior") description: { @@ -211,30 +211,24 @@ Item { return I18n.tr("Popouts emerge flush from the bar edge as a single continuous piece, with corner connectors bridging the junction"); return I18n.tr("How the popout emerges from the DankBar"); } - options: SettingsData.frameEnabled ? [I18n.tr("Overlap"), I18n.tr("Slide"), I18n.tr("Roll"), I18n.tr("Connected")] : [I18n.tr("Overlap"), I18n.tr("Slide"), I18n.tr("Roll")] + options: SettingsData.frameEnabled ? [I18n.tr("Fluid"), I18n.tr("Connected")] : [I18n.tr("Fluid")] currentValue: { switch (SettingsData.directionalAnimationMode) { - case 1: - return I18n.tr("Slide"); - case 2: - return I18n.tr("Roll"); case 3: - return SettingsData.frameEnabled ? I18n.tr("Connected") : I18n.tr("Slide"); + return SettingsData.frameEnabled ? I18n.tr("Connected") : I18n.tr("Fluid"); default: - return I18n.tr("Overlap"); + return I18n.tr("Fluid"); } } onValueChanged: value => { - if (value === I18n.tr("Slide")) + if (value === I18n.tr("Fluid")) SettingsData.set("directionalAnimationMode", 1); - else if (value === I18n.tr("Roll")) - SettingsData.set("directionalAnimationMode", 2); else if (value === I18n.tr("Connected") && SettingsData.frameEnabled) { if (SettingsData.directionalAnimationMode !== 3) - SettingsData.set("previousDirectionalMode", SettingsData.directionalAnimationMode); + SettingsData.set("previousDirectionalMode", 1); SettingsData.set("directionalAnimationMode", 3); } else - SettingsData.set("directionalAnimationMode", 0); + SettingsData.set("directionalAnimationMode", 1); } } } diff --git a/quickshell/Services/PopoutService.qml b/quickshell/Services/PopoutService.qml index c6e8a096..409c844d 100644 --- a/quickshell/Services/PopoutService.qml +++ b/quickshell/Services/PopoutService.qml @@ -202,10 +202,9 @@ Singleton { } function unloadDankDash() { - if (!dankDashPopoutLoader) - return; - dankDashPopout = null; - dankDashPopoutLoader.active = false; + // DankDash is intentionally kept alive after first use. Destroying this + // lazy popout during its close signal can invalidate connected overlay + // bindings while Qt is still unwinding the signal stack. } function toggleDankDash(tabIndex, x, y, width, section, screen) { diff --git a/quickshell/Widgets/DankPopout.qml b/quickshell/Widgets/DankPopout.qml index 8966f5d3..9b527ec9 100644 --- a/quickshell/Widgets/DankPopout.qml +++ b/quickshell/Widgets/DankPopout.qml @@ -1,5 +1,6 @@ import QtQuick import qs.Common +import qs.Services Item { id: root @@ -16,10 +17,10 @@ Item { property string triggerSection: "" property string positioning: "center" property int animationDuration: Theme.popoutAnimationDuration - property real animationScaleCollapsed: 0.96 - property real animationOffset: Theme.spacingL - property list animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial - property list animationExitCurve: Theme.expressiveCurves.emphasized + property real animationScaleCollapsed: Theme.effectScaleCollapsed + property real animationOffset: Theme.effectAnimOffset + property list animationEnterCurve: Theme.variantPopoutEnterCurve + property list animationExitCurve: Theme.variantPopoutExitCurve property bool suspendShadowWhileResizing: false property bool shouldBeVisible: false property var customKeyboardFocus: null @@ -73,6 +74,7 @@ Item { readonly property real barY: impl.item ? impl.item.barY : 0 readonly property real barWidth: impl.item ? impl.item.barWidth : 0 readonly property real barHeight: impl.item ? impl.item.barHeight : 0 + readonly property bool useConnectedBackend: SettingsData.connectedFrameModeActive && !!screen && SettingsData.isScreenInPreferences(screen, SettingsData.frameScreenPreferences) function open() { if (impl.item) @@ -118,7 +120,8 @@ Item { Loader { id: impl - sourceComponent: SettingsData.connectedFrameModeActive ? connectedComp : standaloneComp + active: root.screen !== null + sourceComponent: root.useConnectedBackend ? connectedComp : standaloneComp onItemChanged: if (item) root._wireBackend(item) } @@ -166,16 +169,7 @@ Item { it.effectiveBarPosition = Qt.binding(() => root.effectiveBarPosition); it.effectiveBarBottomGap = Qt.binding(() => root.effectiveBarBottomGap); - // shouldBeVisible is two-way — backend's open()/close() flips it internally. it.shouldBeVisible = root.shouldBeVisible; - it.shouldBeVisibleChanged.connect(function () { - if (root.shouldBeVisible !== it.shouldBeVisible) - root.shouldBeVisible = it.shouldBeVisible; - }); - - it.opened.connect(root.opened); - it.popoutClosed.connect(root.popoutClosed); - it.backgroundClicked.connect(root.backgroundClicked); } function primeContent() { @@ -197,4 +191,26 @@ Item { impl.item.shouldBeVisible = root.shouldBeVisible; } } -} \ No newline at end of file + + Connections { + target: impl.item + ignoreUnknownSignals: true + + function onShouldBeVisibleChanged() { + if (impl.item && root.shouldBeVisible !== impl.item.shouldBeVisible) + root.shouldBeVisible = impl.item.shouldBeVisible; + } + + function onOpened() { + root.opened(); + } + + function onPopoutClosed() { + root.popoutClosed(); + } + + function onBackgroundClicked() { + root.backgroundClicked(); + } + } +} diff --git a/quickshell/Widgets/DankPopoutConnected.qml b/quickshell/Widgets/DankPopoutConnected.qml index 75f02d8e..2f4d27bb 100644 --- a/quickshell/Widgets/DankPopoutConnected.qml +++ b/quickshell/Widgets/DankPopoutConnected.qml @@ -39,6 +39,7 @@ Item { property int _connectedChromeSerial: 0 property real _chromeAnimTravelX: 1 property real _chromeAnimTravelY: 1 + property bool _fullSyncQueued: false property real storedBarThickness: Theme.barHeight - 4 property real storedBarSpacing: 4 @@ -262,7 +263,14 @@ Item { } function _queueFullSync() { - _syncPopoutChromeState(); + if (_fullSyncQueued) + return; + _fullSyncQueued = true; + Qt.callLater(() => { + root._fullSyncQueued = false; + if (typeof root._syncPopoutChromeState === "function") + root._syncPopoutChromeState(); + }); } onAlignedXChanged: _queueFullSync() @@ -272,8 +280,8 @@ Item { onContentAnimYChanged: _syncPopoutAnim("y") onRenderedAlignedYChanged: _syncPopoutBody() onRenderedAlignedHeightChanged: _syncPopoutBody() - onScreenChanged: _syncPopoutChromeState() - onEffectiveBarPositionChanged: _syncPopoutChromeState() + onScreenChanged: _queueFullSync() + onEffectiveBarPositionChanged: _queueFullSync() Connections { target: contentWindow @@ -493,8 +501,8 @@ Item { if (Theme.isConnectedEffect) return Math.max(storedBarSpacing + Theme.connectedCornerRadius + 4, 40); if (Theme.isDirectionalEffect) { - if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode !== 0) - return 16; // Slide Behind and Roll Out do not add animationOffset, enabling strict Wayland clipping. + if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0) + return 16; // Fluid uses strict Wayland clipping instead of extra motion padding. return Math.max(0, animationOffset) + 16; } if (Theme.isDepthEffect) @@ -662,7 +670,7 @@ Item { blurEnabled: root.effectiveSurfaceBlurEnabled && !root.frameOwnsConnectedChrome readonly property real s: Math.min(1, contentContainer.scaleValue) - readonly property bool trackBlurFromBarEdge: Theme.isConnectedEffect || (typeof SettingsData !== "undefined" && Theme.isDirectionalEffect && SettingsData.directionalAnimationMode !== 2) + readonly property bool trackBlurFromBarEdge: Theme.isConnectedEffect || Theme.isDirectionalEffect // Directional popouts clip to the bar edge, so the blur needs to grow from // that same edge instead of translating through the bar before settling. @@ -790,8 +798,6 @@ Item { readonly property real offsetX: { if (directionalEffect) { - if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2) - return 0; if (barLeft) return -directionalTravelX; if (barRight) @@ -813,8 +819,6 @@ Item { } readonly property real offsetY: { if (directionalEffect) { - if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2) - return 0; if (barBottom) return directionalTravelY; if (barTop) @@ -838,7 +842,7 @@ Item { property real animX: 0 property real animY: 0 - readonly property real computedScaleCollapsed: (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) ? 0.0 : root.animationScaleCollapsed + readonly property real computedScaleCollapsed: root.animationScaleCollapsed property real scaleValue: computedScaleCollapsed Component.onCompleted: { @@ -932,19 +936,17 @@ Item { return parent.height + clipOversize * 2; } - // Roll-out clips a wrapper while content and shadow keep full-size geometry. Item { id: rollOutAdjuster readonly property real baseWidth: contentContainer.width readonly property real baseHeight: contentContainer.height - readonly property bool isRollOut: typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect x: directionalClipMask.x !== 0 ? -directionalClipMask.x : 0 y: directionalClipMask.y !== 0 ? -directionalClipMask.y : 0 - width: isRollOut && (contentContainer.barLeft || contentContainer.barRight) ? Math.max(0, baseWidth * contentContainer.scaleValue) : baseWidth - height: isRollOut && (contentContainer.barTop || contentContainer.barBottom) ? Math.max(0, baseHeight * contentContainer.scaleValue) : baseHeight + width: baseWidth + height: baseHeight - clip: isRollOut + clip: false ElevationShadow { id: shadowSource @@ -1028,7 +1030,7 @@ Item { opacity: Theme.isDirectionalEffect ? 1 : (shouldBeVisible ? 1 : 0) visible: opacity > 0 - scale: rollOutAdjuster.isRollOut ? 1.0 : contentContainer.scaleValue + scale: contentContainer.scaleValue x: Theme.snap(contentContainer.animX + (rollOutAdjuster.baseWidth - width) * (1 - scale) * 0.5, root.dpr) y: Theme.snap(contentContainer.animY + (rollOutAdjuster.baseHeight - height) * (1 - scale) * 0.5, root.dpr) diff --git a/quickshell/Widgets/DankPopoutStandalone.qml b/quickshell/Widgets/DankPopoutStandalone.qml index c0739299..8794ec52 100644 --- a/quickshell/Widgets/DankPopoutStandalone.qml +++ b/quickshell/Widgets/DankPopoutStandalone.qml @@ -22,13 +22,14 @@ Item { property string triggerSection: "" property string positioning: "center" property int animationDuration: Theme.popoutAnimationDuration - property real animationScaleCollapsed: 0.96 - property real animationOffset: Theme.spacingL - property list animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial - property list animationExitCurve: Theme.expressiveCurves.emphasized + property real animationScaleCollapsed: Theme.effectScaleCollapsed + property real animationOffset: Theme.effectAnimOffset + property list animationEnterCurve: Theme.variantPopoutEnterCurve + property list animationExitCurve: Theme.variantPopoutExitCurve property bool suspendShadowWhileResizing: false property bool shouldBeVisible: false property bool isClosing: false + property bool animationsEnabled: true property var customKeyboardFocus: null property bool backgroundInteractive: true property bool contentHandlesKeys: false @@ -36,7 +37,13 @@ Item { property bool _primeContent: false property bool _resizeActive: false property real _surfaceMarginLeft: 0 + property real _surfaceMarginTop: 0 property real _surfaceW: 0 + property real _surfaceH: 0 + property real _surfaceBodyX: 0 + property real _surfaceBodyY: 0 + property real _surfaceBodyW: 0 + property real _surfaceBodyH: 0 property real storedBarThickness: Theme.barHeight - 4 property real storedBarSpacing: 4 @@ -48,6 +55,26 @@ Item { "rightBar": 0 }) property var screen: null + readonly property bool frameOnlyNoConnected: SettingsData.frameEnabled && !!screen && SettingsData.isScreenInPreferences(screen, SettingsData.frameScreenPreferences) + readonly property bool fluidStandaloneActive: Theme.isDirectionalEffect + readonly property bool backgroundDismissWindowRequired: backgroundInteractive + readonly property bool backgroundWindowRequired: backgroundDismissWindowRequired || root.overlayContent !== null + + function _frameEdgeInset(side) { + if (!screen) + return 0; + return SettingsData.frameEdgeInsetForSide(screen, side); + } + + function _frameGapMargin(side) { + return _frameEdgeInset(side) + Theme.popupDistance; + } + + function _edgeClearance(side, popupGap, adjacentInset) { + if (frameOnlyNoConnected) + return Math.max(adjacentInset, _frameGapMargin(side)); + return adjacentInset > 0 ? adjacentInset : popupGap; + } readonly property real effectiveBarThickness: { const padding = storedBarConfig ? (storedBarConfig.innerPadding !== undefined ? storedBarConfig.innerPadding : 4) : 4; @@ -150,13 +177,60 @@ Item { setBarContext(pos, bottomGap); } - readonly property bool useBackgroundWindow: !CompositorService.isHyprland || CompositorService.useHyprlandFocusGrab + function _setSurfaceGeometry(bodyX, bodyY, bodyW, bodyH) { + _surfaceBodyX = Theme.snap(bodyX, dpr); + _surfaceBodyY = Theme.snap(bodyY, dpr); + _surfaceBodyW = Theme.snap(bodyW, dpr); + _surfaceBodyH = Theme.snap(bodyH, dpr); + _surfaceMarginLeft = _surfaceBodyX - shadowBuffer; + _surfaceMarginTop = _surfaceBodyY - shadowBuffer; + _surfaceW = _surfaceBodyW + shadowBuffer * 2; + _surfaceH = _surfaceBodyH + shadowBuffer * 2; + } + + function _setSettledSurfaceGeometry() { + if (shouldBeVisible) { + _setSurfaceGeometry(alignedX, alignedY, alignedWidth, alignedHeight); + } + } + + function _setAnimatedSurfaceEnvelope() { + if (!shouldBeVisible) + return; + if (!fluidStandaloneActive) { + _setSettledSurfaceGeometry(); + return; + } + + const currentY = renderedAlignedY; + const currentBottom = renderedAlignedY + renderedAlignedHeight; + const targetY = alignedY; + const targetBottom = alignedY + alignedHeight; + const existingY = _surfaceBodyH > 0 ? _surfaceBodyY : currentY; + const existingBottom = _surfaceBodyH > 0 ? _surfaceBodyY + _surfaceBodyH : currentBottom; + const envelopeY = Math.min(currentY, targetY, existingY); + const envelopeBottom = Math.max(currentBottom, targetBottom, existingBottom); + _setSurfaceGeometry(alignedX, envelopeY, alignedWidth, Math.max(0, envelopeBottom - envelopeY)); + surfaceSettleTimer.restart(); + } function updateSurfacePosition() { - if (useBackgroundWindow && shouldBeVisible) { - _surfaceMarginLeft = alignedX - shadowBuffer; - _surfaceW = alignedWidth + shadowBuffer * 2; - } + _setSettledSurfaceGeometry(); + } + + onAlignedXChanged: { + if (shouldBeVisible) + _setAnimatedSurfaceEnvelope(); + } + + onAlignedYChanged: { + if (shouldBeVisible) + _setAnimatedSurfaceEnvelope(); + } + + onAlignedWidthChanged: { + if (shouldBeVisible) + _setAnimatedSurfaceEnvelope(); } function open() { @@ -164,6 +238,8 @@ Item { return; closeTimer.stop(); isClosing = false; + animationsEnabled = false; + _primeContent = true; _frozenMaskX = maskX; _frozenMaskY = maskY; @@ -172,20 +248,24 @@ Item { if (_lastOpenedScreen !== null && _lastOpenedScreen !== screen) { contentWindow.visible = false; - if (useBackgroundWindow) - backgroundWindow.visible = false; + backgroundWindow.visible = false; } _lastOpenedScreen = screen; - shouldBeVisible = true; - if (useBackgroundWindow) { - _surfaceMarginLeft = alignedX - shadowBuffer; - _surfaceW = alignedWidth + shadowBuffer * 2; + if (contentContainer) { + contentContainer.animX = Theme.snap(contentContainer.offsetX, root.dpr); + contentContainer.animY = Theme.snap(contentContainer.offsetY, root.dpr); + contentContainer.scaleValue = contentContainer.computedScaleCollapsed; } - if (shouldBeVisible && screen) { - if (useBackgroundWindow) - backgroundWindow.visible = true; - contentWindow.visible = true; + + _setSurfaceGeometry(alignedX, alignedY, alignedWidth, alignedHeight); + if (backgroundWindowRequired) + backgroundWindow.visible = true; + contentWindow.visible = true; + + animationsEnabled = true; + shouldBeVisible = true; + if (screen) { PopoutManager.showPopout(popoutHandle); opened(); } @@ -224,13 +304,12 @@ Item { Timer { id: closeTimer - interval: animationDuration + interval: Theme.variantCloseInterval(animationDuration) onTriggered: { if (!shouldBeVisible) { isClosing = false; contentWindow.visible = false; - if (useBackgroundWindow) - backgroundWindow.visible = false; + backgroundWindow.visible = false; PopoutManager.hidePopout(popoutHandle); popoutClosed(); } @@ -244,12 +323,35 @@ Item { readonly property var shadowLevel: Theme.elevationLevel3 readonly property real shadowFallbackOffset: 6 readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.popoutElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, effectiveShadowDirection, shadowFallbackOffset, 8, 16) : 0 - readonly property real shadowMotionPadding: Math.max(0, animationOffset) + readonly property real shadowMotionPadding: fluidStandaloneActive ? 0 : Math.max(0, animationOffset) readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr) readonly property real alignedWidth: Theme.px(popupWidth, dpr) readonly property real alignedHeight: Theme.px(popupHeight, dpr) + property real renderedAlignedY: alignedY + property real renderedAlignedHeight: alignedHeight + readonly property bool renderedGeometryGrowing: alignedHeight >= renderedAlignedHeight + + Behavior on renderedAlignedY { + enabled: root.animationsEnabled && fluidStandaloneActive && contentWindow.visible && root.shouldBeVisible + NumberAnimation { + duration: Theme.variantDuration(root.animationDuration, root.renderedGeometryGrowing) + easing.type: Easing.BezierSpline + easing.bezierCurve: root.renderedGeometryGrowing ? root.animationEnterCurve : root.animationExitCurve + } + } + + Behavior on renderedAlignedHeight { + enabled: root.animationsEnabled && fluidStandaloneActive && contentWindow.visible && root.shouldBeVisible + NumberAnimation { + duration: Theme.variantDuration(root.animationDuration, root.renderedGeometryGrowing) + easing.type: Easing.BezierSpline + easing.bezierCurve: root.renderedGeometryGrowing ? root.animationEnterCurve : root.animationExitCurve + } + } onAlignedHeightChanged: { + if (shouldBeVisible) + _setAnimatedSurfaceEnvelope(); if (!suspendShadowWhileResizing || !shouldBeVisible) return; _resizeActive = true; @@ -261,6 +363,10 @@ Item { resizeSettleTimer.stop(); } } + onBackgroundWindowRequiredChanged: { + if (shouldBeVisible) + backgroundWindow.visible = backgroundWindowRequired; + } Timer { id: resizeSettleTimer @@ -269,20 +375,29 @@ Item { onTriggered: root._resizeActive = false } + Timer { + id: surfaceSettleTimer + interval: Math.max(0, Theme.variantDuration(root.animationDuration, root.renderedGeometryGrowing) + 32) + repeat: false + onTriggered: root._setSettledSurfaceGeometry() + } + readonly property real alignedX: Theme.snap((() => { const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true; const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 4; const popupGap = useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue; + const leftGap = _edgeClearance("left", popupGap, adjacentBarInfo.leftBar > 0 ? adjacentBarInfo.leftBar : 0); + const rightGap = _edgeClearance("right", popupGap, adjacentBarInfo.rightBar > 0 ? adjacentBarInfo.rightBar : 0); switch (effectiveBarPosition) { case SettingsData.Position.Left: - return Math.max(popupGap, Math.min(screenWidth - popupWidth - popupGap, triggerX)); + return Math.max(leftGap, Math.min(screenWidth - popupWidth - rightGap, triggerX)); case SettingsData.Position.Right: - return Math.max(popupGap, Math.min(screenWidth - popupWidth - popupGap, triggerX - popupWidth)); + return Math.max(leftGap, Math.min(screenWidth - popupWidth - rightGap, triggerX - popupWidth)); default: const rawX = triggerX + (triggerWidth / 2) - (popupWidth / 2); - const minX = adjacentBarInfo.leftBar > 0 ? adjacentBarInfo.leftBar : popupGap; - const maxX = screenWidth - popupWidth - (adjacentBarInfo.rightBar > 0 ? adjacentBarInfo.rightBar : popupGap); + const minX = leftGap; + const maxX = screenWidth - popupWidth - rightGap; return Math.max(minX, Math.min(maxX, rawX)); } })(), dpr) @@ -291,16 +406,18 @@ Item { const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true; const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 4; const popupGap = useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue; + const topGap = _edgeClearance("top", popupGap, adjacentBarInfo.topBar > 0 ? adjacentBarInfo.topBar : 0); + const bottomGap = _edgeClearance("bottom", popupGap, adjacentBarInfo.bottomBar > 0 ? adjacentBarInfo.bottomBar : 0); switch (effectiveBarPosition) { case SettingsData.Position.Bottom: - return Math.max(popupGap, Math.min(screenHeight - popupHeight - popupGap, triggerY - popupHeight)); + return Math.max(topGap, Math.min(screenHeight - popupHeight - bottomGap, triggerY - popupHeight)); case SettingsData.Position.Top: - return Math.max(popupGap, Math.min(screenHeight - popupHeight - popupGap, triggerY)); + return Math.max(topGap, Math.min(screenHeight - popupHeight - bottomGap, triggerY)); default: const rawY = triggerY - (popupHeight / 2); - const minY = adjacentBarInfo.topBar > 0 ? adjacentBarInfo.topBar : popupGap; - const maxY = screenHeight - popupHeight - (adjacentBarInfo.bottomBar > 0 ? adjacentBarInfo.bottomBar : popupGap); + const minY = topGap; + const maxY = screenHeight - popupHeight - bottomGap; return Math.max(minY, Math.min(maxY, rawY)); } })(), dpr) @@ -355,35 +472,38 @@ Item { mask: Region { item: maskRect + Region { + item: contentHoleRect + intersection: Intersection.Subtract + } } Rectangle { id: maskRect visible: false color: "transparent" - x: root._frozenMaskX - y: root._frozenMaskY - width: (shouldBeVisible && backgroundInteractive) ? root._frozenMaskWidth : 0 - height: (shouldBeVisible && backgroundInteractive) ? root._frozenMaskHeight : 0 + x: root.backgroundDismissWindowRequired ? root._frozenMaskX : 0 + y: root.backgroundDismissWindowRequired ? root._frozenMaskY : 0 + width: (root.backgroundDismissWindowRequired && shouldBeVisible && backgroundInteractive) ? root._frozenMaskWidth : 0 + height: (root.backgroundDismissWindowRequired && shouldBeVisible && backgroundInteractive) ? root._frozenMaskHeight : 0 + } + + Rectangle { + id: contentHoleRect + visible: false + color: "transparent" + x: root.backgroundDismissWindowRequired ? root._surfaceBodyX : 0 + y: root.backgroundDismissWindowRequired ? root._surfaceBodyY : 0 + width: (root.backgroundDismissWindowRequired && shouldBeVisible) ? root._surfaceBodyW : 0 + height: (root.backgroundDismissWindowRequired && shouldBeVisible) ? root._surfaceBodyH : 0 } MouseArea { - x: root._frozenMaskX - y: root._frozenMaskY - width: root._frozenMaskWidth - height: root._frozenMaskHeight + anchors.fill: parent hoverEnabled: false - enabled: shouldBeVisible && backgroundInteractive + enabled: root.backgroundDismissWindowRequired && shouldBeVisible && backgroundInteractive acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - onClicked: mouse => { - const clickX = mouse.x + root._frozenMaskX; - const clickY = mouse.y + root._frozenMaskY; - const outsideContent = clickX < root.alignedX || clickX > root.alignedX + root.alignedWidth || clickY < root.alignedY || clickY > root.alignedY + root.alignedHeight; - - if (!outsideContent) - return; - backgroundClicked(); - } + onClicked: backgroundClicked() } Loader { @@ -399,15 +519,18 @@ Item { screen: root.screen visible: false color: "transparent" + readonly property bool closeVisualActive: root.shouldBeVisible || root.isClosing WindowBlur { id: popoutBlur targetWindow: contentWindow readonly property real s: Math.min(1, contentContainer.scaleValue) - blurX: contentContainer.x + contentContainer.width * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr) - blurY: contentContainer.y + contentContainer.height * (1 - s) * 0.5 + Theme.snap(contentContainer.animY, root.dpr) - blurWidth: (shouldBeVisible && contentWrapper.opacity > 0) ? contentContainer.width * s : 0 - blurHeight: (shouldBeVisible && contentWrapper.opacity > 0) ? contentContainer.height * s : 0 + readonly property bool trackBlurFromBarEdge: root.fluidStandaloneActive + + blurX: trackBlurFromBarEdge ? contentContainer.x + contentContainer.revealX : contentContainer.x + contentContainer.width * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr) + blurY: trackBlurFromBarEdge ? contentContainer.y + contentContainer.revealY : contentContainer.y + contentContainer.height * (1 - s) * 0.5 + Theme.snap(contentContainer.animY, root.dpr) + blurWidth: (contentWindow.closeVisualActive && contentWrapper.opacity > 0) ? (trackBlurFromBarEdge ? contentContainer.revealWidth : contentContainer.width * s) : 0 + blurHeight: (contentWindow.closeVisualActive && contentWrapper.opacity > 0) ? (trackBlurFromBarEdge ? contentContainer.revealHeight : contentContainer.height * s) : 0 blurRadius: Theme.cornerRadius } @@ -437,24 +560,20 @@ Item { return WlrKeyboardFocus.Exclusive; } - readonly property bool _fullHeight: useBackgroundWindow && root.fullHeightSurface - anchors { left: true top: true - right: !useBackgroundWindow - bottom: _fullHeight || !useBackgroundWindow } WlrLayershell.margins { - left: useBackgroundWindow ? root._surfaceMarginLeft : 0 - top: (useBackgroundWindow && !_fullHeight) ? (root.alignedY - shadowBuffer) : 0 + left: root._surfaceMarginLeft + top: root._surfaceMarginTop } - implicitWidth: useBackgroundWindow ? root._surfaceW : 0 - implicitHeight: (useBackgroundWindow && !_fullHeight) ? (root.alignedHeight + shadowBuffer * 2) : 0 + implicitWidth: root._surfaceW + implicitHeight: root._surfaceH - mask: useBackgroundWindow ? contentInputMask : null + mask: contentInputMask Region { id: contentInputMask @@ -466,140 +585,234 @@ Item { visible: false x: contentContainer.x y: contentContainer.y - width: shouldBeVisible ? root.alignedWidth : 0 - height: shouldBeVisible ? root.alignedHeight : 0 - } - - MouseArea { - anchors.fill: parent - enabled: !useBackgroundWindow && shouldBeVisible && backgroundInteractive - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - z: -1 - onClicked: mouse => { - const clickX = mouse.x; - const clickY = mouse.y; - const outsideContent = clickX < root.alignedX || clickX > root.alignedX + root.alignedWidth || clickY < root.alignedY || clickY > root.alignedY + root.alignedHeight; - if (!outsideContent) - return; - backgroundClicked(); - } + width: contentWindow.closeVisualActive ? root.alignedWidth : 0 + height: contentWindow.closeVisualActive ? (root.fluidStandaloneActive ? root.renderedAlignedHeight : root.alignedHeight) : 0 } Item { id: contentContainer - x: useBackgroundWindow ? shadowBuffer : root.alignedX - y: (useBackgroundWindow && !contentWindow._fullHeight) ? shadowBuffer : root.alignedY + x: shadowBuffer + root.alignedX - root._surfaceBodyX + y: shadowBuffer + (root.fluidStandaloneActive ? root.renderedAlignedY : root.alignedY) - root._surfaceBodyY width: root.alignedWidth - height: root.alignedHeight + height: root.fluidStandaloneActive ? root.renderedAlignedHeight : root.alignedHeight readonly property bool barTop: effectiveBarPosition === SettingsData.Position.Top readonly property bool barBottom: effectiveBarPosition === SettingsData.Position.Bottom readonly property bool barLeft: effectiveBarPosition === SettingsData.Position.Left readonly property bool barRight: effectiveBarPosition === SettingsData.Position.Right readonly property string connectedBarSide: barTop ? "top" : (barBottom ? "bottom" : (barLeft ? "left" : "right")) - readonly property real offsetX: barLeft ? root.animationOffset : (barRight ? -root.animationOffset : 0) - readonly property real offsetY: barBottom ? -root.animationOffset : (barTop ? root.animationOffset : 0) + readonly property bool directionalEffect: Theme.isDirectionalEffect + readonly property bool depthEffect: Theme.isDepthEffect + readonly property real directionalTravelX: Math.max(root.animationOffset, root.alignedWidth + Theme.spacingL) + readonly property real directionalTravelY: Math.max(root.animationOffset, root.alignedHeight + Theme.spacingL) + readonly property real depthTravel: Math.max(root.animationOffset * 0.7, 28) + readonly property real sectionTilt: (triggerSection === "left" ? -1 : (triggerSection === "right" ? 1 : 0)) + readonly property real offsetX: { + if (directionalEffect) { + if (barLeft) + return -directionalTravelX; + if (barRight) + return directionalTravelX; + if (barTop || barBottom) + return 0; + return sectionTilt * directionalTravelX * 0.2; + } + if (depthEffect) { + if (barLeft) + return -depthTravel; + if (barRight) + return depthTravel; + if (barTop || barBottom) + return 0; + return sectionTilt * depthTravel * 0.2; + } + return barLeft ? root.animationOffset : (barRight ? -root.animationOffset : 0); + } + readonly property real offsetY: { + if (directionalEffect) { + if (barBottom) + return directionalTravelY; + if (barTop) + return -directionalTravelY; + if (barLeft || barRight) + return 0; + return directionalTravelY; + } + if (depthEffect) { + if (barBottom) + return depthTravel; + if (barTop) + return -depthTravel; + if (barLeft || barRight) + return 0; + return depthTravel; + } + return barBottom ? -root.animationOffset : (barTop ? root.animationOffset : 0); + } property real animX: 0 property real animY: 0 - property real scaleValue: root.animationScaleCollapsed + readonly property real computedScaleCollapsed: root.animationScaleCollapsed + property real scaleValue: computedScaleCollapsed + readonly property real clampedAnimX: Math.max(-width, Math.min(animX, width)) + readonly property real clampedAnimY: Math.max(-height, Math.min(animY, height)) + readonly property real revealWidth: { + if (!root.fluidStandaloneActive) + return width; + if (barLeft) + return Theme.snap(Math.max(0, width + clampedAnimX), root.dpr); + if (barRight) + return Theme.snap(Math.max(0, width - clampedAnimX), root.dpr); + return width; + } + readonly property real revealHeight: { + if (!root.fluidStandaloneActive) + return height; + if (barTop) + return Theme.snap(Math.max(0, height + clampedAnimY), root.dpr); + if (barBottom) + return Theme.snap(Math.max(0, height - clampedAnimY), root.dpr); + return height; + } + readonly property real revealX: root.fluidStandaloneActive && barRight ? Theme.snap(width - revealWidth, root.dpr) : 0 + readonly property real revealY: root.fluidStandaloneActive && barBottom ? Theme.snap(height - revealHeight, root.dpr) : 0 - onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr) - onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr) + Component.onCompleted: { + animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr); + animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr); + scaleValue = root.shouldBeVisible ? 1.0 : computedScaleCollapsed; + } + + onOffsetXChanged: { + if (!root.shouldBeVisible) + animX = Theme.snap(offsetX, root.dpr); + } + onOffsetYChanged: { + if (!root.shouldBeVisible) + animY = Theme.snap(offsetY, root.dpr); + } Connections { target: root function onShouldBeVisibleChanged() { contentContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetX, root.dpr); contentContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetY, root.dpr); - contentContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed; + contentContainer.scaleValue = root.shouldBeVisible ? 1.0 : contentContainer.computedScaleCollapsed; } } Behavior on animX { + enabled: root.animationsEnabled NumberAnimation { - duration: root.animationDuration + duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible) easing.type: Easing.BezierSpline easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve } } Behavior on animY { + enabled: root.animationsEnabled NumberAnimation { - duration: root.animationDuration + duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible) easing.type: Easing.BezierSpline easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve } } Behavior on scaleValue { + enabled: root.animationsEnabled NumberAnimation { - duration: root.animationDuration + duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible) easing.type: Easing.BezierSpline easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve } } - ElevationShadow { - id: shadowSource - width: parent.width - height: parent.height - opacity: contentWrapper.opacity - scale: contentWrapper.scale - x: contentWrapper.x - y: contentWrapper.y - level: root.shadowLevel - direction: root.effectiveShadowDirection - fallbackOffset: root.shadowFallbackOffset - targetRadius: Theme.cornerRadius - targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) - shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !(root.suspendShadowWhileResizing && root._resizeActive) - } - Item { - id: contentWrapper - anchors.centerIn: parent - width: parent.width - height: parent.height - opacity: shouldBeVisible ? 1 : 0 - visible: opacity > 0 - scale: contentContainer.scaleValue - x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr) - y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr) + id: directionalClipMask - layer.enabled: contentWrapper.opacity < 1 - layer.smooth: false - layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0) + readonly property bool shouldClip: root.fluidStandaloneActive - Behavior on opacity { - NumberAnimation { - duration: animationDuration - easing.type: Easing.BezierSpline - easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve + clip: shouldClip + x: shouldClip ? contentContainer.revealX : 0 + y: shouldClip ? contentContainer.revealY : 0 + width: shouldClip ? contentContainer.revealWidth : parent.width + height: shouldClip ? contentContainer.revealHeight : parent.height + + Item { + id: rollOutAdjuster + readonly property real baseWidth: contentContainer.width + readonly property real baseHeight: contentContainer.height + + x: directionalClipMask.x !== 0 ? -directionalClipMask.x : 0 + y: directionalClipMask.y !== 0 ? -directionalClipMask.y : 0 + width: baseWidth + height: baseHeight + clip: false + + ElevationShadow { + id: shadowSource + width: rollOutAdjuster.baseWidth + height: rollOutAdjuster.baseHeight + opacity: contentWrapper.opacity + scale: root.fluidStandaloneActive ? 1 : contentWrapper.scale + x: root.fluidStandaloneActive ? 0 : contentWrapper.x + y: root.fluidStandaloneActive ? 0 : contentWrapper.y + level: root.shadowLevel + direction: root.effectiveShadowDirection + fallbackOffset: root.shadowFallbackOffset + targetRadius: Theme.cornerRadius + targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !(root.suspendShadowWhileResizing && root._resizeActive) + } + + Item { + id: contentWrapper + width: rollOutAdjuster.baseWidth + height: rollOutAdjuster.baseHeight + opacity: Theme.isDirectionalEffect ? 1 : (shouldBeVisible ? 1 : 0) + visible: opacity > 0 + scale: contentContainer.scaleValue + transformOrigin: Item.Center + x: Theme.snap(contentContainer.animX + (rollOutAdjuster.baseWidth - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr) + y: Theme.snap(contentContainer.animY + (rollOutAdjuster.baseHeight - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr) + + layer.enabled: contentWrapper.opacity < 1 + layer.smooth: false + layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0) + + Behavior on opacity { + enabled: !Theme.isDirectionalEffect + NumberAnimation { + duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale) + easing.type: Easing.BezierSpline + easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve + } + } + + Loader { + id: contentLoader + anchors.fill: parent + active: root._primeContent || shouldBeVisible || contentWindow.visible + asynchronous: false + } + } + + Rectangle { + width: rollOutAdjuster.baseWidth + height: rollOutAdjuster.baseHeight + x: root.fluidStandaloneActive ? 0 : contentWrapper.x + y: root.fluidStandaloneActive ? 0 : contentWrapper.y + opacity: contentWrapper.opacity + scale: root.fluidStandaloneActive ? 1 : contentWrapper.scale + visible: contentWrapper.visible + radius: Theme.cornerRadius + color: "transparent" + border.color: BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium + border.width: BlurService.borderWidth + z: 100 } } - - Loader { - id: contentLoader - anchors.fill: parent - active: root._primeContent || shouldBeVisible || contentWindow.visible - asynchronous: false - } - } - - Rectangle { - width: parent.width - height: parent.height - x: contentWrapper.x - y: contentWrapper.y - opacity: contentWrapper.opacity - scale: contentWrapper.scale - visible: contentWrapper.visible - radius: Theme.cornerRadius - color: "transparent" - border.color: BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium - border.width: BlurService.borderWidth - z: 100 } }