From 62bf9c6efe5749c3b11331f7745410afcb5126c1 Mon Sep 17 00:00:00 2001 From: purian23 Date: Wed, 4 Mar 2026 10:14:00 -0500 Subject: [PATCH] Add Directional Motion options --- quickshell/Common/AnimVariants.qml | 83 +++++--- quickshell/Common/Anims.qml | 2 +- quickshell/Common/SettingsData.qml | 2 + quickshell/Common/settings/SettingsSpec.js | 1 + quickshell/Modals/Common/DankModal.qml | 80 ++++--- .../DankLauncherV2/DankLauncherV2Modal.qml | 196 +++++++++++------- .../Modules/Settings/TypographyMotionTab.qml | 36 ++++ quickshell/Widgets/DankPopout.qml | 156 +++++++++----- 8 files changed, 374 insertions(+), 182 deletions(-) diff --git a/quickshell/Common/AnimVariants.qml b/quickshell/Common/AnimVariants.qml index 3b7d8b1c..3b9f8295 100644 --- a/quickshell/Common/AnimVariants.qml +++ b/quickshell/Common/AnimVariants.qml @@ -15,9 +15,12 @@ Singleton { if (typeof SettingsData === "undefined") return Anims.expressiveDefaultSpatial; switch (SettingsData.animationVariant) { - case 1: return Anims.standardDecel; - case 2: return Anims.expressiveFastSpatial; - default: return Anims.expressiveDefaultSpatial; + case 1: + return Anims.standardDecel; + case 2: + return Anims.expressiveFastSpatial; + default: + return Anims.expressiveDefaultSpatial; } } @@ -25,9 +28,12 @@ Singleton { if (typeof SettingsData === "undefined") return Anims.emphasized; switch (SettingsData.animationVariant) { - case 1: return Anims.standard; - case 2: return Anims.emphasized; - default: return Anims.emphasized; + case 1: + return Anims.standard; + case 2: + return Anims.emphasized; + default: + return Anims.emphasized; } } @@ -64,7 +70,7 @@ Singleton { if (SettingsData.animationVariant === 1) return Anims.standardDecel; if (SettingsData.animationVariant === 2) - return Anims.standardDecel; + return Anims.expressiveFastSpatial; return Anims.standardDecel; } return variantEnterCurve; @@ -83,26 +89,35 @@ Singleton { } readonly property real variantEnterDurationFactor: { - if (typeof SettingsData === "undefined") return 1.0; + if (typeof SettingsData === "undefined") + return 1.0; switch (SettingsData.animationVariant) { - case 1: return 0.9; - case 2: return 1.08; - default: return 1.0; + case 1: + return 0.9; + case 2: + return 1.08; + default: + return 1.0; } } readonly property real variantExitDurationFactor: { - if (typeof SettingsData === "undefined") return 1.0; + if (typeof SettingsData === "undefined") + return 1.0; switch (SettingsData.animationVariant) { - case 1: return 0.85; - case 2: return 0.92; - default: return 1.0; + case 1: + return 0.85; + case 2: + return 0.92; + default: + return 1.0; } } // Fluent: opacity at ~55% of duration; Material/Dynamic: 1:1 with position readonly property real variantOpacityDurationScale: { - if (typeof SettingsData === "undefined") return 1.0; + if (typeof SettingsData === "undefined") + return 1.0; return SettingsData.animationVariant === 1 ? 0.55 : 1.0; } @@ -112,11 +127,15 @@ Singleton { } function variantExitCleanupPadding() { - if (typeof SettingsData === "undefined") return 50; + if (typeof SettingsData === "undefined") + return 50; switch (SettingsData.motionEffect) { - case 1: return 8; - case 2: return 24; - default: return 50; + case 1: + return 8; + case 2: + return 24; + default: + return 50; } } @@ -128,20 +147,28 @@ Singleton { readonly property bool isDepthEffect: typeof SettingsData !== "undefined" && SettingsData.motionEffect === 2 readonly property real effectScaleCollapsed: { - if (typeof SettingsData === "undefined") return 0.96; + if (typeof SettingsData === "undefined") + return 0.96; switch (SettingsData.motionEffect) { - case 1: return 1.0; - case 2: return 0.88; - default: return 0.96; + case 1: + return 1.0; + case 2: + return 0.88; + default: + return 0.96; } } readonly property real effectAnimOffset: { - if (typeof SettingsData === "undefined") return 16; + if (typeof SettingsData === "undefined") + return 16; switch (SettingsData.motionEffect) { - case 1: return 144; - case 2: return 56; - default: return 16; + case 1: + return 144; + case 2: + return 56; + default: + return 16; } } } diff --git a/quickshell/Common/Anims.qml b/quickshell/Common/Anims.qml index 84ab95c6..f8e0c9a5 100644 --- a/quickshell/Common/Anims.qml +++ b/quickshell/Common/Anims.qml @@ -25,6 +25,6 @@ Singleton { // Used by AnimVariants for variant/effect logic readonly property var expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1] - readonly property var expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1] + readonly property var expressiveFastSpatial: [0.34, 1.5, 0.2, 1.0, 1.0, 1.0] readonly property var expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1] } diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 547e6e92..5e4f907d 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -182,6 +182,8 @@ Singleton { onAnimationVariantChanged: saveSettings() property int motionEffect: SettingsData.AnimationEffect.Standard onMotionEffectChanged: saveSettings() + property int directionalAnimationMode: 0 + onDirectionalAnimationModeChanged: saveSettings() property bool m3ElevationEnabled: true onM3ElevationEnabledChanged: saveSettings() property int m3ElevationIntensity: 12 diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 19c96f3d..cc80669e 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -49,6 +49,7 @@ var SPEC = { enableRippleEffects: { def: true }, animationVariant: { def: 0 }, motionEffect: { def: 0 }, + directionalAnimationMode: { def: 0 }, m3ElevationEnabled: { def: true }, m3ElevationIntensity: { def: 12 }, m3ElevationOpacity: { def: 30 }, diff --git a/quickshell/Modals/Common/DankModal.qml b/quickshell/Modals/Common/DankModal.qml index 2ee246ad..edb4413e 100644 --- a/quickshell/Modals/Common/DankModal.qml +++ b/quickshell/Modals/Common/DankModal.qml @@ -50,8 +50,7 @@ Item { readonly property alias clickCatcher: clickCatcher readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground - readonly property bool useSingleWindow: CompositorService.isHyprland || useBackground - readonly property bool _needsFullscreenMotion: !useSingleWindow && (Theme.isDirectionalEffect || Theme.isDepthEffect) + readonly property bool useSingleWindow: CompositorService.isHyprland signal opened signal dialogClosed @@ -68,12 +67,12 @@ Item { const focusedScreen = CompositorService.getFocusedScreen(); if (focusedScreen) { contentWindow.screen = focusedScreen; - if (!useSingleWindow && !_needsFullscreenMotion) + if (!useSingleWindow) clickCatcher.screen = focusedScreen; } - if (Theme.isDirectionalEffect) { - if (!useSingleWindow && !_needsFullscreenMotion) + if (Theme.isDirectionalEffect || root.useBackground) { + if (!useSingleWindow) clickCatcher.visible = true; contentWindow.visible = true; } @@ -82,7 +81,7 @@ Item { Qt.callLater(() => { animationsEnabled = true; shouldBeVisible = true; - if (!useSingleWindow && !_needsFullscreenMotion && !clickCatcher.visible) + if (!useSingleWindow && !clickCatcher.visible) clickCatcher.visible = true; if (!contentWindow.visible) contentWindow.visible = true; @@ -105,7 +104,7 @@ Item { ModalManager.closeModal(root); closeTimer.stop(); contentWindow.visible = false; - if (!useSingleWindow && !_needsFullscreenMotion) + if (!useSingleWindow) clickCatcher.visible = false; dialogClosed(); Qt.callLater(() => animationsEnabled = true); @@ -141,7 +140,7 @@ Item { const newScreen = CompositorService.getFocusedScreen(); if (newScreen) { contentWindow.screen = newScreen; - if (!useSingleWindow && !_needsFullscreenMotion) + if (!useSingleWindow) clickCatcher.screen = newScreen; } } @@ -154,7 +153,7 @@ Item { if (shouldBeVisible) return; contentWindow.visible = false; - if (!useSingleWindow && !_needsFullscreenMotion) + if (!useSingleWindow) clickCatcher.visible = false; dialogClosed(); } @@ -164,8 +163,8 @@ Item { readonly property real shadowFallbackOffset: 6 readonly property real shadowRenderPadding: (root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0 readonly property real shadowMotionPadding: { - if (_needsFullscreenMotion) - return 0; + if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0 && Theme.isDirectionalEffect) + return 0; // Wayland native overlap mask if (animationType === "slide") return 30; if (Theme.isDirectionalEffect) @@ -233,9 +232,26 @@ Item { MouseArea { anchors.fill: parent - enabled: root.closeOnBackgroundClick && root.shouldBeVisible + enabled: !root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible onClicked: root.backgroundClicked() } + + Rectangle { + anchors.fill: parent + z: -1 + color: "black" + opacity: (!root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0 + visible: opacity > 0 + + Behavior on opacity { + enabled: root.animationsEnabled && !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 + } + } + } } PanelWindow { @@ -274,12 +290,12 @@ Item { anchors { left: true top: true - right: root.useSingleWindow || root._needsFullscreenMotion - bottom: root.useSingleWindow || root._needsFullscreenMotion + right: root.useSingleWindow + bottom: root.useSingleWindow } - readonly property real actualMarginLeft: (root.useSingleWindow || root._needsFullscreenMotion) ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr)) - readonly property real actualMarginTop: (root.useSingleWindow || root._needsFullscreenMotion) ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr)) + readonly property real actualMarginLeft: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr)) + readonly property real actualMarginTop: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr)) WlrLayershell.margins { left: actualMarginLeft @@ -288,8 +304,8 @@ Item { bottom: 0 } - implicitWidth: (root.useSingleWindow || root._needsFullscreenMotion) ? 0 : root.alignedWidth + (shadowBuffer * 2) - implicitHeight: (root.useSingleWindow || root._needsFullscreenMotion) ? 0 : root.alignedHeight + (shadowBuffer * 2) + implicitWidth: root.useSingleWindow ? 0 : root.alignedWidth + (shadowBuffer * 2) + implicitHeight: root.useSingleWindow ? 0 : root.alignedHeight + (shadowBuffer * 2) onVisibleChanged: { if (visible) { @@ -304,7 +320,7 @@ Item { MouseArea { anchors.fill: parent - enabled: (root.useSingleWindow || root._needsFullscreenMotion) && root.closeOnBackgroundClick && root.shouldBeVisible + enabled: root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible z: -2 onClicked: root.backgroundClicked() } @@ -313,13 +329,14 @@ Item { anchors.fill: parent z: -1 color: "black" - opacity: root.useBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0 - visible: root.useBackground + opacity: (root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0 + visible: opacity > 0 Behavior on opacity { enabled: root.animationsEnabled && !Theme.isDirectionalEffect - DankAnim { + NumberAnimation { duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale) + easing.type: Easing.BezierSpline easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve } } @@ -335,7 +352,7 @@ Item { MouseArea { anchors.fill: parent - enabled: (root.useSingleWindow || root._needsFullscreenMotion) && root.shouldBeVisible + enabled: root.useSingleWindow && root.shouldBeVisible hoverEnabled: false acceptedButtons: Qt.AllButtons onPressed: mouse.accepted = true @@ -355,6 +372,8 @@ Item { readonly property real customDistTop: customAnchorY readonly property real customDistBottom: root.screenHeight - customAnchorY readonly property real offsetX: { + if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) + return 0; if (slide && !directionalEffect && !depthEffect) return 15; if (directionalEffect) { @@ -388,6 +407,8 @@ Item { return 0; } readonly property real offsetY: { + if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) + return 0; if (slide && !directionalEffect && !depthEffect) return -30; if (directionalEffect) { @@ -424,28 +445,33 @@ Item { property real animX: root.shouldBeVisible ? 0 : root.frozenMotionOffsetX property real animY: root.shouldBeVisible ? 0 : root.frozenMotionOffsetY - property real scaleValue: root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed + + readonly property real computedScaleCollapsed: (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) ? 0.0 : root.animationScaleCollapsed + property real scaleValue: root.shouldBeVisible ? 1.0 : computedScaleCollapsed Behavior on animX { enabled: root.animationsEnabled - DankAnim { + NumberAnimation { 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 - DankAnim { + NumberAnimation { 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 - DankAnim { + NumberAnimation { duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible) + easing.type: Easing.BezierSpline easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve } } diff --git a/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml b/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml index 781875aa..a400189d 100644 --- a/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml +++ b/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml @@ -356,6 +356,13 @@ Item { WlrLayershell.exclusiveZone: -1 WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + WlrLayershell.margins { + top: contentContainer.dockTop ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 0 ? Theme.px(42, root.dpr) : 0) + bottom: contentContainer.dockBottom ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 1 ? Theme.px(42, root.dpr) : 0) + left: contentContainer.dockLeft ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 2 ? Theme.px(42, root.dpr) : 0) + right: contentContainer.dockRight ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 3 ? Theme.px(42, root.dpr) : 0) + } + anchors { top: true bottom: true @@ -455,26 +462,49 @@ Item { width: root.alignedWidth height: root.alignedHeight + readonly property int dockEdge: typeof SettingsData !== "undefined" ? SettingsData.dockPosition : 1 + readonly property bool dockTop: dockEdge === 0 + readonly property bool dockBottom: dockEdge === 1 + readonly property bool dockLeft: dockEdge === 2 + readonly property bool dockRight: dockEdge === 3 + + readonly property real dockThickness: typeof SettingsData !== "undefined" && SettingsData.showDock ? Theme.px(SettingsData.dockIconSize + (SettingsData.dockMargin * 2) + SettingsData.dockSpacing + 8, root.dpr) : Theme.px(60, root.dpr) + readonly property bool directionalEffect: Theme.isDirectionalEffect readonly property bool depthEffect: Theme.isDepthEffect - readonly property real collapsedMotionX: depthEffect ? Theme.effectAnimOffset * 0.25 : 0 + readonly property real collapsedMotionX: { + if (directionalEffect) { + if (dockLeft) + return -(root._ccX + root.alignedWidth + Theme.effectAnimOffset); + if (dockRight) + return root.screenWidth - root._ccX + Theme.effectAnimOffset; + } + if (depthEffect) + return Theme.effectAnimOffset * 0.25; + return 0; + } readonly property real collapsedMotionY: { - if (directionalEffect) - return Math.max(root.screenHeight - root._ccY + root.shadowPad, Theme.effectAnimOffset * 1.1); + if (directionalEffect) { + if (dockTop) + return -(root._ccY + root.alignedHeight + Theme.effectAnimOffset); + if (dockBottom) + return root.screenHeight - root._ccY + root.shadowPad + Theme.effectAnimOffset; + return 0; + } if (depthEffect) return -Math.max(Theme.effectAnimOffset * 0.85, 34); - return 0; + return -Math.max((root.shadowPad || 0) + Theme.effectAnimOffset, 40); } // animX/animY are Behavior-animated — DankPopout pattern property real animX: 0 property real animY: 0 - property real scaleValue: Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed + property real scaleValue: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed) Component.onCompleted: { animX = Theme.snap(root._motionActive ? 0 : collapsedMotionX, root.dpr); animY = Theme.snap(root._motionActive ? 0 : collapsedMotionY, root.dpr); - scaleValue = root._motionActive ? 1.0 : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed); + scaleValue = root._motionActive ? 1.0 : (Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed)); } Connections { @@ -482,7 +512,7 @@ Item { function on_MotionActiveChanged() { contentContainer.animX = Theme.snap(root._motionActive ? 0 : root._frozenMotionX, root.dpr); contentContainer.animY = Theme.snap(root._motionActive ? 0 : root._frozenMotionY, root.dpr); - contentContainer.scaleValue = root._motionActive ? 1.0 : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed); + contentContainer.scaleValue = root._motionActive ? 1.0 : (Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed)); } } @@ -503,83 +533,105 @@ Item { } Behavior on scaleValue { - enabled: root.animationsEnabled && !Theme.isDirectionalEffect + enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2)) DankAnim { duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive) easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve } } - // Shadow mirrors contentWrapper position/scale/opacity - ElevationShadow { - id: launcherShadowLayer - width: parent.width - height: parent.height - opacity: contentWrapper.opacity - scale: contentWrapper.scale - x: contentWrapper.x - y: contentWrapper.y - level: root.shadowLevel - fallbackOffset: root.shadowFallbackOffset - targetColor: root.backgroundColor - borderColor: root.borderColor - borderWidth: root.borderWidth - targetRadius: root.cornerRadius - shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" - } - - // contentWrapper moves inside static contentContainer — DankPopout pattern Item { - id: contentWrapper - width: parent.width - height: parent.height - opacity: Theme.isDirectionalEffect ? 1 : (launcherMotionVisible ? 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 + readonly property bool shouldClip: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0 + readonly property real clipOversize: 2000 - Behavior on opacity { - enabled: root.animationsEnabled && !Theme.isDirectionalEffect - DankAnim { - duration: Math.round(Theme.variantDuration(Theme.modalAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale) - easing.bezierCurve: launcherMotionVisible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve + clip: shouldClip + + x: shouldClip ? (contentContainer.dockRight ? -clipOversize : (contentContainer.dockLeft ? contentContainer.dockThickness - root._ccX : -clipOversize)) : 0 + y: shouldClip ? (contentContainer.dockBottom ? -clipOversize : (contentContainer.dockTop ? contentContainer.dockThickness - root._ccY : -clipOversize)) : 0 + + width: shouldClip ? parent.width + clipOversize + (contentContainer.dockRight ? (root.screenWidth - contentContainer.dockThickness - root._ccX - parent.width) : (contentContainer.dockLeft ? clipOversize : clipOversize)) : parent.width + height: shouldClip ? parent.height + clipOversize + (contentContainer.dockBottom ? (root.screenHeight - contentContainer.dockThickness - root._ccY - parent.height) : (contentContainer.dockTop ? clipOversize : clipOversize)) : parent.height + + Item { + id: aligner + x: directionalClipMask.x !== 0 ? -directionalClipMask.x : 0 + y: directionalClipMask.y !== 0 ? -directionalClipMask.y : 0 + width: contentContainer.width + height: contentContainer.height + + // Shadow mirrors contentWrapper position/scale/opacity + ElevationShadow { + id: launcherShadowLayer + width: parent.width + height: parent.height + opacity: contentWrapper.opacity + scale: contentWrapper.scale + x: contentWrapper.x + y: contentWrapper.y + level: root.shadowLevel + fallbackOffset: root.shadowFallbackOffset + targetColor: root.backgroundColor + borderColor: root.borderColor + borderWidth: root.borderWidth + targetRadius: root.cornerRadius + shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" } - } - MouseArea { - anchors.fill: parent - onPressed: mouse => mouse.accepted = true - } + // contentWrapper moves inside static contentContainer — DankPopout pattern + Item { + id: contentWrapper + width: parent.width + height: parent.height + opacity: Theme.isDirectionalEffect ? 1 : (launcherMotionVisible ? 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) - FocusScope { - anchors.fill: parent - focus: keyboardActive - - Loader { - id: launcherContentLoader - anchors.fill: parent - active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize - asynchronous: false - sourceComponent: LauncherContent { - focus: true - parentModal: root - } - - onLoaded: { - if (root._pendingInitialize) { - root._initializeAndShow(root._pendingQuery, root._pendingMode); - root._pendingInitialize = false; + Behavior on opacity { + enabled: root.animationsEnabled && !Theme.isDirectionalEffect + DankAnim { + duration: Math.round(Theme.variantDuration(Theme.modalAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale) + easing.bezierCurve: launcherMotionVisible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve } } - } - Keys.onEscapePressed: event => { - root.hide(); - event.accepted = true; - } - } - } - } - } + MouseArea { + anchors.fill: parent + onPressed: mouse => mouse.accepted = true + } + + FocusScope { + anchors.fill: parent + focus: keyboardActive + + Loader { + id: launcherContentLoader + anchors.fill: parent + active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize + asynchronous: false + sourceComponent: LauncherContent { + focus: true + parentModal: root + } + + onLoaded: { + if (root._pendingInitialize) { + root._initializeAndShow(root._pendingQuery, root._pendingMode); + root._pendingInitialize = false; + } + } + } + + Keys.onEscapePressed: event => { + root.hide(); + event.accepted = true; + } + } + } // contentWrapper + } // aligner + } // directionalClipMask + } // contentContainer + } // PanelWindow } diff --git a/quickshell/Modules/Settings/TypographyMotionTab.qml b/quickshell/Modules/Settings/TypographyMotionTab.qml index 6cbd361e..13042d8c 100644 --- a/quickshell/Modules/Settings/TypographyMotionTab.qml +++ b/quickshell/Modules/Settings/TypographyMotionTab.qml @@ -191,6 +191,42 @@ Item { } } } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + visible: SettingsData.motionEffect === 1 + } + + SettingsDropdownRow { + visible: SettingsData.motionEffect === 1 + tab: "typography" + tags: ["animation", "directional", "behavior", "overlap", "sticky", "roll"] + settingKey: "directionalAnimationMode" + text: I18n.tr("Directional Behavior") + description: I18n.tr("How the popout emerges from the DankBar") + options: [I18n.tr("Overlap"), I18n.tr("Slide"), I18n.tr("Roll")] + currentValue: { + switch (SettingsData.directionalAnimationMode) { + case 1: + return I18n.tr("Slide"); + case 2: + return I18n.tr("Roll"); + default: + return I18n.tr("Overlap"); + } + } + onValueChanged: value => { + if (value === I18n.tr("Slide")) + SettingsData.set("directionalAnimationMode", 1); + else if (value === I18n.tr("Roll")) + SettingsData.set("directionalAnimationMode", 2); + else + SettingsData.set("directionalAnimationMode", 0); + } + } } SettingsCard { diff --git a/quickshell/Widgets/DankPopout.qml b/quickshell/Widgets/DankPopout.qml index 0ef2778e..b2f7ee0f 100644 --- a/quickshell/Widgets/DankPopout.qml +++ b/quickshell/Widgets/DankPopout.qml @@ -259,8 +259,11 @@ Item { 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: { - if (Theme.isDirectionalEffect) + 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. return Math.max(0, animationOffset) + 16; + } if (Theme.isDepthEffect) return Math.max(0, animationOffset) + 8; return Math.max(0, animationOffset); @@ -561,6 +564,8 @@ Item { readonly property real sectionTilt: (triggerSection === "left" ? -1 : (triggerSection === "right" ? 1 : 0)) readonly property real offsetX: { if (directionalEffect) { + if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2) + return 0; if (barLeft) return -directionalTravelX; if (barRight) @@ -582,6 +587,8 @@ Item { } readonly property real offsetY: { if (directionalEffect) { + if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2) + return 0; if (barBottom) return directionalTravelY; if (barTop) @@ -604,12 +611,14 @@ Item { property real animX: 0 property real animY: 0 - property real scaleValue: root.animationScaleCollapsed + + readonly property real computedScaleCollapsed: (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) ? 0.0 : root.animationScaleCollapsed + property real scaleValue: computedScaleCollapsed 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 : root.animationScaleCollapsed; + scaleValue = root.shouldBeVisible ? 1.0 : computedScaleCollapsed; } onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr) @@ -620,7 +629,7 @@ Item { 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; } } @@ -651,61 +660,100 @@ Item { } } - 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 - width: parent.width - height: parent.height - opacity: Theme.isDirectionalEffect ? 1 : (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: typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0 && Theme.isDirectionalEffect + readonly property real clipOversize: 1000 - Behavior on opacity { - enabled: !Theme.isDirectionalEffect - NumberAnimation { - duration: Math.round(Theme.variantDuration(animationDuration, shouldBeVisible) * Theme.variantOpacityDurationScale) - easing.type: Easing.BezierSpline - easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve - } - } + clip: shouldClip - Rectangle { - anchors.fill: parent - radius: Theme.cornerRadius - color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) - border.color: Theme.outlineMedium - border.width: 0 - } + // Bound the clipping strictly to the bar side, allowing massive overflow on the other 3 sides for shadows + x: shouldClip ? (contentContainer.barRight ? -clipOversize : (contentContainer.barLeft ? 0 : -clipOversize)) : 0 + y: shouldClip ? (contentContainer.barBottom ? -clipOversize : (contentContainer.barTop ? 0 : -clipOversize)) : 0 - Loader { - id: contentLoader - anchors.fill: parent - active: root._primeContent || shouldBeVisible || contentWindow.visible - asynchronous: false - } - } - } + width: shouldClip ? parent.width + clipOversize + (contentContainer.barLeft || contentContainer.barRight ? 0 : clipOversize) : parent.width + height: shouldClip ? parent.height + clipOversize + (contentContainer.barTop || contentContainer.barBottom ? 0 : clipOversize) : parent.height + + Item { + id: aligner + 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) + (isRollOut && contentContainer.barRight ? baseWidth * (1 - contentContainer.scaleValue) : 0) + y: (directionalClipMask.y !== 0 ? -directionalClipMask.y : 0) + (isRollOut && contentContainer.barBottom ? baseHeight * (1 - contentContainer.scaleValue) : 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 + + clip: isRollOut + + Item { + id: unrollCounteract + x: aligner.isRollOut && contentContainer.barRight ? -(aligner.baseWidth * (1 - contentContainer.scaleValue)) : 0 + y: aligner.isRollOut && contentContainer.barBottom ? -(aligner.baseHeight * (1 - contentContainer.scaleValue)) : 0 + width: aligner.baseWidth + height: aligner.baseHeight + + 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 + width: parent.width + height: parent.height + opacity: Theme.isDirectionalEffect ? 1 : (shouldBeVisible ? 1 : 0) + visible: opacity > 0 + + scale: aligner.isRollOut ? 1.0 : contentContainer.scaleValue + x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - scale) * 0.5, root.dpr) + y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - scale) * 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(animationDuration, shouldBeVisible) * Theme.variantOpacityDurationScale) + easing.type: Easing.BezierSpline + easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve + } + } + + Rectangle { + anchors.fill: parent + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + border.color: Theme.outlineMedium + border.width: 0 + } + + Loader { + id: contentLoader + anchors.fill: parent + active: root._primeContent || shouldBeVisible || contentWindow.visible + asynchronous: false + } + } // closes contentWrapper + } // closes unrollCounteract + } // closes aligner + } // closes directionalClipMask + } // closes contentContainer Item { id: focusHelper