From 4784087dc2957f052338bb3c891e62a39400da8e Mon Sep 17 00:00:00 2001 From: purian23 Date: Tue, 3 Mar 2026 20:02:32 -0500 Subject: [PATCH] Initial staging for Animation & Motion effects --- core/.pre-commit-config.yaml | 21 +- quickshell/Common/AnimVariants.qml | 147 ++++++ quickshell/Common/Anims.qml | 5 + quickshell/Common/SettingsData.qml | 16 + quickshell/Common/Theme.qml | 18 + quickshell/Common/settings/SettingsSpec.js | 2 + quickshell/Modals/Common/DankModal.qml | 241 +++++---- .../DankLauncherV2/DankLauncherV2Modal.qml | 462 +++++++++++------- .../Modals/DankLauncherV2/LauncherContent.qml | 77 +-- .../Modules/AppDrawer/AppDrawerPopout.qml | 47 +- .../ControlCenter/ControlCenterPopout.qml | 6 +- quickshell/Modules/DankBar/DankBarContent.qml | 1 + .../Modules/DankDash/DankDashPopout.qml | 1 - .../Modules/DankDash/MediaDropdownOverlay.qml | 150 ++++-- .../Center/NotificationCenterPopout.qml | 4 +- .../Notifications/Popup/NotificationPopup.qml | 63 ++- .../Modules/Settings/TypographyMotionTab.qml | 138 ++++++ .../WorkspaceOverlays/HyprlandOverview.qml | 52 +- .../WorkspaceOverlays/NiriOverviewOverlay.qml | 44 +- .../WorkspaceOverlays/OverviewWindow.qml | 24 +- quickshell/Widgets/DankPopout.qml | 183 +++++-- 21 files changed, 1206 insertions(+), 496 deletions(-) create mode 100644 quickshell/Common/AnimVariants.qml diff --git a/core/.pre-commit-config.yaml b/core/.pre-commit-config.yaml index 7eb6228b..a2b5ddaf 100644 --- a/core/.pre-commit-config.yaml +++ b/core/.pre-commit-config.yaml @@ -1,26 +1,13 @@ repos: - - repo: local + - repo: https://github.com/golangci/golangci-lint + rev: v2.10.1 hooks: - id: golangci-lint-fmt - name: golangci-lint-fmt - entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 fmt - language: system require_serial: true - types: [go] - pass_filenames: false - id: golangci-lint-full - name: golangci-lint-full - entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 run --fix - language: system - require_serial: true - types: [go] - pass_filenames: false - id: golangci-lint-config-verify - name: golangci-lint-config-verify - entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 config verify - language: system - files: \.golangci\.(?:yml|yaml|toml|json) - pass_filenames: false + - repo: local + hooks: - id: go-test name: go test entry: go test ./... diff --git a/quickshell/Common/AnimVariants.qml b/quickshell/Common/AnimVariants.qml new file mode 100644 index 00000000..3b7d8b1c --- /dev/null +++ b/quickshell/Common/AnimVariants.qml @@ -0,0 +1,147 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import qs.Common + +// AnimVariants — Central tuning for animation and Motion Effects variants +// (Material/Fluent/Dynamic) (Standard/Directional/Depth) + +Singleton { + id: root + + readonly property list variantEnterCurve: { + if (typeof SettingsData === "undefined") + return Anims.expressiveDefaultSpatial; + switch (SettingsData.animationVariant) { + case 1: return Anims.standardDecel; + case 2: return Anims.expressiveFastSpatial; + default: return Anims.expressiveDefaultSpatial; + } + } + + readonly property list variantExitCurve: { + if (typeof SettingsData === "undefined") + return Anims.emphasized; + switch (SettingsData.animationVariant) { + case 1: return Anims.standard; + case 2: return Anims.emphasized; + default: return Anims.emphasized; + } + } + + // Modal-specific entry curve + readonly property list variantModalEnterCurve: { + if (typeof SettingsData === "undefined") + return Anims.expressiveDefaultSpatial; + if (isDirectionalEffect) { + if (SettingsData.animationVariant === 1) + return Anims.standardDecel; + if (SettingsData.animationVariant === 2) + return Anims.expressiveFastSpatial; + } + return variantEnterCurve; + } + + readonly property list variantModalExitCurve: { + if (typeof SettingsData === "undefined") + return Anims.emphasized; + if (isDirectionalEffect) { + if (SettingsData.animationVariant === 1) + return Anims.emphasizedAccel; + if (SettingsData.animationVariant === 2) + return Anims.emphasizedAccel; + } + return variantExitCurve; + } + + // Popout-specific entry curve + readonly property list variantPopoutEnterCurve: { + if (typeof SettingsData === "undefined") + return Anims.expressiveDefaultSpatial; + if (isDirectionalEffect) { + if (SettingsData.animationVariant === 1) + return Anims.standardDecel; + if (SettingsData.animationVariant === 2) + return Anims.standardDecel; + return Anims.standardDecel; + } + return variantEnterCurve; + } + + readonly property list variantPopoutExitCurve: { + if (typeof SettingsData === "undefined") + return Anims.emphasized; + if (isDirectionalEffect) { + if (SettingsData.animationVariant === 1) + return Anims.emphasizedAccel; + if (SettingsData.animationVariant === 2) + return Anims.emphasizedAccel; + } + return variantExitCurve; + } + + readonly property real variantEnterDurationFactor: { + if (typeof SettingsData === "undefined") return 1.0; + switch (SettingsData.animationVariant) { + 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; + switch (SettingsData.animationVariant) { + 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; + return SettingsData.animationVariant === 1 ? 0.55 : 1.0; + } + + function variantDuration(baseDuration, entering) { + const factor = entering ? variantEnterDurationFactor : variantExitDurationFactor; + return Math.max(0, Math.round(baseDuration * factor)); + } + + function variantExitCleanupPadding() { + if (typeof SettingsData === "undefined") return 50; + switch (SettingsData.motionEffect) { + case 1: return 8; + case 2: return 24; + default: return 50; + } + } + + function variantCloseInterval(baseDuration) { + return variantDuration(baseDuration, false) + variantExitCleanupPadding(); + } + + readonly property bool isDirectionalEffect: typeof SettingsData !== "undefined" && SettingsData.motionEffect === 1 + readonly property bool isDepthEffect: typeof SettingsData !== "undefined" && SettingsData.motionEffect === 2 + + readonly property real effectScaleCollapsed: { + if (typeof SettingsData === "undefined") return 0.96; + switch (SettingsData.motionEffect) { + case 1: return 1.0; + case 2: return 0.88; + default: return 0.96; + } + } + + readonly property real effectAnimOffset: { + if (typeof SettingsData === "undefined") return 16; + switch (SettingsData.motionEffect) { + case 1: return 144; + case 2: return 56; + default: return 16; + } + } +} diff --git a/quickshell/Common/Anims.qml b/quickshell/Common/Anims.qml index 349e9916..84ab95c6 100644 --- a/quickshell/Common/Anims.qml +++ b/quickshell/Common/Anims.qml @@ -22,4 +22,9 @@ Singleton { readonly property var standard: [0.20, 0.00, 0.00, 1.00, 1.00, 1.00] readonly property var standardDecel: [0.00, 0.00, 0.00, 1.00, 1.00, 1.00] readonly property var standardAccel: [0.30, 0.00, 1.00, 1.00, 1.00, 1.00] + + // 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 expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1] } diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 77f372a7..5b4fa551 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -37,6 +37,18 @@ Singleton { Custom } + enum AnimationVariant { + Material, + Fluent, + Dynamic + } + + enum AnimationEffect { + Standard, // 0 — M3: scale-in, rises from below + Directional, // 1 — pure large slide, no scale + Depth // 2 — medium slide with deep depth scale pop + } + enum SuspendBehavior { Suspend, Hibernate, @@ -168,6 +180,10 @@ Singleton { property int modalCustomAnimationDuration: 150 property bool enableRippleEffects: true onEnableRippleEffectsChanged: saveSettings() + property int animationVariant: SettingsData.AnimationVariant.Material + onAnimationVariantChanged: saveSettings() + property int motionEffect: SettingsData.AnimationEffect.Standard + onMotionEffectChanged: saveSettings() property bool m3ElevationEnabled: true onM3ElevationEnabledChanged: saveSettings() property int m3ElevationIntensity: 12 diff --git a/quickshell/Common/Theme.qml b/quickshell/Common/Theme.qml index fc0b663e..3442204e 100644 --- a/quickshell/Common/Theme.qml +++ b/quickshell/Common/Theme.qml @@ -960,6 +960,24 @@ Singleton { "expressiveEffects": [0.34, 0.8, 0.34, 1, 1, 1] } + // Delegates to AnimVariants.qml for curves, timing, scale, and offsets. + readonly property list variantEnterCurve: AnimVariants.variantEnterCurve + readonly property list variantExitCurve: AnimVariants.variantExitCurve + readonly property list variantModalEnterCurve: AnimVariants.variantModalEnterCurve + readonly property list variantModalExitCurve: AnimVariants.variantModalExitCurve + readonly property list variantPopoutEnterCurve: AnimVariants.variantPopoutEnterCurve + readonly property list variantPopoutExitCurve: AnimVariants.variantPopoutExitCurve + readonly property real variantEnterDurationFactor: AnimVariants.variantEnterDurationFactor + readonly property real variantExitDurationFactor: AnimVariants.variantExitDurationFactor + readonly property real variantOpacityDurationScale: AnimVariants.variantOpacityDurationScale + readonly property bool isDirectionalEffect: AnimVariants.isDirectionalEffect + readonly property bool isDepthEffect: AnimVariants.isDepthEffect + readonly property real effectScaleCollapsed: AnimVariants.effectScaleCollapsed + readonly property real effectAnimOffset: AnimVariants.effectAnimOffset + function variantDuration(baseDuration, entering) { return AnimVariants.variantDuration(baseDuration, entering); } + function variantExitCleanupPadding() { return AnimVariants.variantExitCleanupPadding(); } + function variantCloseInterval(baseDuration) { return AnimVariants.variantCloseInterval(baseDuration); } + readonly property var animationPresetDurations: { "none": 0, "short": 250, diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index f6029430..e7c79ea1 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -49,6 +49,8 @@ var SPEC = { modalAnimationSpeed: { def: 1 }, modalCustomAnimationDuration: { def: 150 }, enableRippleEffects: { def: true }, + animationVariant: { def: 0 }, + motionEffect: { 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 cafa1336..2ee246ad 100644 --- a/quickshell/Modals/Common/DankModal.qml +++ b/quickshell/Modals/Common/DankModal.qml @@ -3,7 +3,6 @@ import Quickshell import Quickshell.Wayland import qs.Common import qs.Services -import qs.Widgets Item { id: root @@ -27,11 +26,11 @@ Item { property bool closeOnBackgroundClick: true property string animationType: "scale" property int animationDuration: Theme.modalAnimationDuration - property real animationScaleCollapsed: 0.96 - property real animationOffset: Theme.spacingL - property list animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial - property list animationExitCurve: Theme.expressiveCurves.emphasized - property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + property real animationScaleCollapsed: Theme.effectScaleCollapsed + property real animationOffset: Theme.effectAnimOffset + property list animationEnterCurve: Theme.variantModalEnterCurve + property list animationExitCurve: Theme.variantModalExitCurve + property color backgroundColor: Theme.surfaceContainer property color borderColor: Theme.outlineMedium property real borderWidth: 0 property real cornerRadius: Theme.cornerRadius @@ -45,11 +44,14 @@ Item { property bool keepPopoutsOpen: false property var customKeyboardFocus: null property bool useOverlayLayer: false + property real frozenMotionOffsetX: 0 + property real frozenMotionOffsetY: 0 readonly property alias contentWindow: contentWindow 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) signal opened signal dialogClosed @@ -59,33 +61,34 @@ Item { function open() { closeTimer.stop(); - const focusedScreen = CompositorService.getFocusedScreen(); - const screenChanged = focusedScreen && contentWindow.screen !== focusedScreen; - if (focusedScreen) { - if (screenChanged) - contentWindow.visible = false; - contentWindow.screen = focusedScreen; - if (!useSingleWindow) { - if (screenChanged) - clickCatcher.visible = false; - clickCatcher.screen = focusedScreen; - } - } - if (screenChanged) { - Qt.callLater(() => root._finishOpen()); - } else { - _finishOpen(); - } - } + animationsEnabled = false; + frozenMotionOffsetX = modalContainer ? modalContainer.offsetX : 0; + frozenMotionOffsetY = modalContainer ? modalContainer.offsetY : animationOffset; - function _finishOpen() { + const focusedScreen = CompositorService.getFocusedScreen(); + if (focusedScreen) { + contentWindow.screen = focusedScreen; + if (!useSingleWindow && !_needsFullscreenMotion) + clickCatcher.screen = focusedScreen; + } + + if (Theme.isDirectionalEffect) { + if (!useSingleWindow && !_needsFullscreenMotion) + clickCatcher.visible = true; + contentWindow.visible = true; + } ModalManager.openModal(root); - shouldBeVisible = true; - if (!useSingleWindow) - clickCatcher.visible = true; - contentWindow.visible = true; - shouldHaveFocus = false; - Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible)); + + Qt.callLater(() => { + animationsEnabled = true; + shouldBeVisible = true; + if (!useSingleWindow && !_needsFullscreenMotion && !clickCatcher.visible) + clickCatcher.visible = true; + if (!contentWindow.visible) + contentWindow.visible = true; + shouldHaveFocus = false; + Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible)); + }); } function close() { @@ -102,7 +105,7 @@ Item { ModalManager.closeModal(root); closeTimer.stop(); contentWindow.visible = false; - if (!useSingleWindow) + if (!useSingleWindow && !_needsFullscreenMotion) clickCatcher.visible = false; dialogClosed(); Qt.callLater(() => animationsEnabled = true); @@ -138,7 +141,7 @@ Item { const newScreen = CompositorService.getFocusedScreen(); if (newScreen) { contentWindow.screen = newScreen; - if (!useSingleWindow) + if (!useSingleWindow && !_needsFullscreenMotion) clickCatcher.screen = newScreen; } } @@ -146,12 +149,12 @@ Item { Timer { id: closeTimer - interval: animationDuration + 50 + interval: Theme.variantCloseInterval(animationDuration) onTriggered: { if (shouldBeVisible) return; contentWindow.visible = false; - if (!useSingleWindow) + if (!useSingleWindow && !_needsFullscreenMotion) clickCatcher.visible = false; dialogClosed(); } @@ -160,7 +163,17 @@ Item { readonly property var shadowLevel: Theme.elevationLevel3 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: animationType === "slide" ? 30 : Math.max(0, animationOffset) + readonly property real shadowMotionPadding: { + if (_needsFullscreenMotion) + return 0; + if (animationType === "slide") + return 30; + if (Theme.isDirectionalEffect) + return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.9); + if (Theme.isDepthEffect) + return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.35); + return Math.max(0, animationOffset); + } readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr) readonly property real alignedWidth: Theme.px(modalWidth, dpr) readonly property real alignedHeight: Theme.px(modalHeight, dpr) @@ -230,16 +243,6 @@ Item { visible: false color: "transparent" - WindowBlur { - targetWindow: contentWindow - readonly property real s: Math.min(1, modalContainer.scaleValue) - blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr) - blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr) - blurWidth: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.width * s : 0 - blurHeight: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.height * s : 0 - blurRadius: root.cornerRadius - } - WlrLayershell.namespace: root.layerNamespace WlrLayershell.layer: { if (root.useOverlayLayer) @@ -271,19 +274,22 @@ Item { anchors { left: true top: true - right: root.useSingleWindow - bottom: root.useSingleWindow + right: root.useSingleWindow || root._needsFullscreenMotion + bottom: root.useSingleWindow || root._needsFullscreenMotion } + 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)) + WlrLayershell.margins { - left: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr)) - top: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr)) + left: actualMarginLeft + top: actualMarginTop right: 0 bottom: 0 } - implicitWidth: root.useSingleWindow ? 0 : root.alignedWidth + (shadowBuffer * 2) - implicitHeight: root.useSingleWindow ? 0 : root.alignedHeight + (shadowBuffer * 2) + implicitWidth: (root.useSingleWindow || root._needsFullscreenMotion) ? 0 : root.alignedWidth + (shadowBuffer * 2) + implicitHeight: (root.useSingleWindow || root._needsFullscreenMotion) ? 0 : root.alignedHeight + (shadowBuffer * 2) onVisibleChanged: { if (visible) { @@ -298,7 +304,7 @@ Item { MouseArea { anchors.fill: parent - enabled: root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible + enabled: (root.useSingleWindow || root._needsFullscreenMotion) && root.closeOnBackgroundClick && root.shouldBeVisible z: -2 onClicked: root.backgroundClicked() } @@ -311,9 +317,9 @@ Item { visible: root.useBackground Behavior on opacity { - enabled: root.animationsEnabled + enabled: root.animationsEnabled && !Theme.isDirectionalEffect DankAnim { - duration: root.animationDuration + duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale) easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve } } @@ -321,15 +327,15 @@ Item { Item { id: modalContainer - x: root.useSingleWindow ? root.alignedX : shadowBuffer - y: root.useSingleWindow ? root.alignedY : shadowBuffer + x: (root.useSingleWindow ? root.alignedX : (root.alignedX - contentWindow.actualMarginLeft)) + Theme.snap(animX, root.dpr) + y: (root.useSingleWindow ? root.alignedY : (root.alignedY - contentWindow.actualMarginTop)) + Theme.snap(animY, root.dpr) width: root.alignedWidth height: root.alignedHeight MouseArea { anchors.fill: parent - enabled: root.useSingleWindow && root.shouldBeVisible + enabled: (root.useSingleWindow || root._needsFullscreenMotion) && root.shouldBeVisible hoverEnabled: false acceptedButtons: Qt.AllButtons onPressed: mouse.accepted = true @@ -338,29 +344,92 @@ Item { } readonly property bool slide: root.animationType === "slide" - readonly property real offsetX: slide ? 15 : 0 - readonly property real offsetY: slide ? -30 : root.animationOffset - - property real animX: 0 - property real animY: 0 - property real scaleValue: root.animationScaleCollapsed - - onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr) - onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr) - - Connections { - target: root - function onShouldBeVisibleChanged() { - modalContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetX, root.dpr); - modalContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetY, root.dpr); - modalContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed; + readonly property bool directionalEffect: Theme.isDirectionalEffect + readonly property bool depthEffect: Theme.isDepthEffect + readonly property real directionalTravel: Math.max(root.animationOffset, Math.max(root.alignedWidth, root.alignedHeight) * 0.8) + readonly property real depthTravel: Math.max(root.animationOffset * 0.8, 36) + readonly property real customAnchorX: root.alignedX + root.alignedWidth * 0.5 + readonly property real customAnchorY: root.alignedY + root.alignedHeight * 0.5 + readonly property real customDistLeft: customAnchorX + readonly property real customDistRight: root.screenWidth - customAnchorX + readonly property real customDistTop: customAnchorY + readonly property real customDistBottom: root.screenHeight - customAnchorY + readonly property real offsetX: { + if (slide && !directionalEffect && !depthEffect) + return 15; + if (directionalEffect) { + switch (root.positioning) { + case "top-right": + return 0; + case "custom": + if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom) + return -directionalTravel; + if (customDistRight <= customDistTop && customDistRight <= customDistBottom) + return directionalTravel; + return 0; + default: + return 0; + } } + if (depthEffect) { + switch (root.positioning) { + case "top-right": + return 0; + case "custom": + if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom) + return -depthTravel; + if (customDistRight <= customDistTop && customDistRight <= customDistBottom) + return depthTravel; + return 0; + default: + return 0; + } + } + return 0; } + readonly property real offsetY: { + if (slide && !directionalEffect && !depthEffect) + return -30; + if (directionalEffect) { + switch (root.positioning) { + case "top-right": + return -Math.max(directionalTravel * 0.65, 96); + case "custom": + if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight) + return -directionalTravel; + if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight) + return directionalTravel; + return 0; + default: + // Default to sliding down from top when centered + return -Math.max(directionalTravel, root.screenHeight * 0.24); + } + } + if (depthEffect) { + switch (root.positioning) { + case "top-right": + return -depthTravel * 0.75; + case "custom": + if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight) + return -depthTravel; + if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight) + return depthTravel; + return depthTravel * 0.45; + default: + return -depthTravel; + } + } + return root.animationOffset; + } + + 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 Behavior on animX { enabled: root.animationsEnabled DankAnim { - duration: root.animationDuration + duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible) easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve } } @@ -368,7 +437,7 @@ Item { Behavior on animY { enabled: root.animationsEnabled DankAnim { - duration: root.animationDuration + duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible) easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve } } @@ -376,7 +445,7 @@ Item { Behavior on scaleValue { enabled: root.animationsEnabled DankAnim { - duration: root.animationDuration + duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible) easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve } } @@ -392,15 +461,14 @@ Item { id: animatedContent anchors.fill: parent clip: false - opacity: root.shouldBeVisible ? 1 : 0 + opacity: Theme.isDirectionalEffect ? 1 : (root.shouldBeVisible ? 1 : 0) scale: modalContainer.scaleValue - x: Theme.snap(modalContainer.animX, root.dpr) + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5 - y: Theme.snap(modalContainer.animY, root.dpr) + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5 + transformOrigin: Item.Center Behavior on opacity { - enabled: root.animationsEnabled + enabled: root.animationsEnabled && !Theme.isDirectionalEffect NumberAnimation { - duration: animationDuration + duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale) easing.type: Easing.BezierSpline easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve } @@ -418,15 +486,6 @@ Item { shadowEnabled: root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" } - Rectangle { - anchors.fill: parent - radius: root.cornerRadius - color: "transparent" - border.color: BlurService.borderColor - border.width: BlurService.borderWidth - z: 100 - } - FocusScope { anchors.fill: parent focus: root.shouldBeVisible diff --git a/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml b/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml index 859022cc..781875aa 100644 --- a/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml +++ b/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml @@ -1,10 +1,10 @@ import QtQuick +import QtQuick.Effects import Quickshell import Quickshell.Wayland import Quickshell.Hyprland import qs.Common import qs.Services -import qs.Widgets Item { id: root @@ -14,16 +14,24 @@ Item { property bool spotlightOpen: false property bool keyboardActive: false property bool contentVisible: false + readonly property bool launcherMotionVisible: Theme.isDirectionalEffect ? spotlightOpen : _motionActive property var spotlightContent: launcherContentLoader.item property bool openedFromOverview: false property bool isClosing: false + property bool _windowEnabled: true property bool _pendingInitialize: false property string _pendingQuery: "" property string _pendingMode: "" readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose + // Animation state — matches DankPopout/DankModal pattern + property bool animationsEnabled: true + property bool _motionActive: false + property real _frozenMotionX: 0 + property real _frozenMotionY: 0 + readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab - readonly property var effectiveScreen: launcherWindow.screen + readonly property var effectiveScreen: contentWindow.screen readonly property real screenWidth: effectiveScreen?.width ?? 1920 readonly property real screenHeight: effectiveScreen?.height ?? 1080 readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1 @@ -77,6 +85,34 @@ Item { } readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0 + // Shadow padding for the content window (render padding only, no motion padding) + 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) + + // For directional/depth: window extends from screen top (content slides within) + // For standard: small window tightly around the modal + shadow padding + readonly property bool _needsExtendedWindow: Theme.isDirectionalEffect || Theme.isDepthEffect + // Content window geometry + readonly property real _cwMarginLeft: Theme.snap(alignedX - shadowPad, dpr) + readonly property real _cwMarginTop: _needsExtendedWindow ? 0 : Theme.snap(alignedY - shadowPad, dpr) + readonly property real _cwWidth: alignedWidth + shadowPad * 2 + readonly property real _cwHeight: { + if (Theme.isDirectionalEffect) + return screenHeight + shadowPad; + if (Theme.isDepthEffect) + return alignedY + alignedHeight + shadowPad; + return alignedHeight + shadowPad * 2; + } + // Where the content container sits inside the content window + readonly property real _ccX: shadowPad + readonly property real _ccY: _needsExtendedWindow ? alignedY : shadowPad + signal dialogClosed function _ensureContentLoadedAndInitialize(query, mode) { @@ -96,18 +132,11 @@ Item { if (!spotlightContent) return; contentVisible = true; - spotlightContent.searchField.forceActiveFocus(); - - var targetQuery = ""; - - if (query) { - targetQuery = query; - } else if (SettingsData.rememberLastQuery) { - targetQuery = SessionData.launcherLastQuery || ""; - } + // NOTE: forceActiveFocus() is deliberately NOT called here. + // It is deferred to after animation starts to avoid compositor IPC stalls. if (spotlightContent.searchField) { - spotlightContent.searchField.text = targetQuery; + spotlightContent.searchField.text = query; } if (spotlightContent.controller) { var targetMode = mode || SessionData.launcherLastMode || "all"; @@ -122,10 +151,12 @@ Item { spotlightContent.controller.collapsedSections = {}; spotlightContent.controller.selectedFlatIndex = 0; spotlightContent.controller.selectedItem = null; - spotlightContent.controller.historyIndex = -1; - spotlightContent.controller.searchQuery = targetQuery; - - spotlightContent.controller.performSearch(); + if (query) { + spotlightContent.controller.setSearchQuery(query); + } else { + spotlightContent.controller.searchQuery = ""; + spotlightContent.controller.performSearch(); + } } if (spotlightContent.resetScroll) { spotlightContent.resetScroll(); @@ -135,47 +166,59 @@ Item { } } - function _finishShow(query, mode) { - spotlightOpen = true; + function _openCommon(query, mode) { + closeCleanupTimer.stop(); isClosing = false; openedFromOverview = false; - keyboardActive = true; + // Disable animations so the snap is instant + animationsEnabled = false; + + // Freeze the collapsed offsets (they depend on height which could change) + _frozenMotionX = contentContainer ? contentContainer.collapsedMotionX : 0; + _frozenMotionY = contentContainer ? contentContainer.collapsedMotionY : (Theme.isDirectionalEffect ? Math.max(root.screenHeight - root._ccY + root.shadowPad, Theme.effectAnimOffset * 1.1) : -Theme.effectAnimOffset); + + var focusedScreen = CompositorService.getFocusedScreen(); + if (focusedScreen) { + backgroundWindow.screen = focusedScreen; + contentWindow.screen = focusedScreen; + } + + // _motionActive = false ensures motionX/Y snap to frozen collapsed position + _motionActive = false; + + // Make windows visible but do NOT request keyboard focus yet ModalManager.openModal(root); + spotlightOpen = true; + backgroundWindow.visible = true; + contentWindow.visible = true; if (useHyprlandFocusGrab) focusGrab.active = true; + // Load content and initialize (but no forceActiveFocus — that's deferred) _ensureContentLoadedAndInitialize(query || "", mode || ""); + + // Frame 1: enable animations and trigger enter motion + Qt.callLater(() => { + root.animationsEnabled = true; + root._motionActive = true; + + // Frame 2: request keyboard focus + activate search field + // Double-deferred to avoid compositor IPC competing with animation frames + Qt.callLater(() => { + root.keyboardActive = true; + if (root.spotlightContent && root.spotlightContent.searchField) + root.spotlightContent.searchField.forceActiveFocus(); + }); + }); } function show() { - closeCleanupTimer.stop(); - - var focusedScreen = CompositorService.getFocusedScreen(); - if (focusedScreen && launcherWindow.screen !== focusedScreen) { - spotlightOpen = false; - isClosing = false; - launcherWindow.screen = focusedScreen; - Qt.callLater(() => root._finishShow("", "")); - return; - } - - _finishShow("", ""); + _openCommon("", ""); } function showWithQuery(query) { - closeCleanupTimer.stop(); - - var focusedScreen = CompositorService.getFocusedScreen(); - if (focusedScreen && launcherWindow.screen !== focusedScreen) { - spotlightOpen = false; - isClosing = false; - launcherWindow.screen = focusedScreen; - Qt.callLater(() => root._finishShow(query, "")); - return; - } - - _finishShow(query, ""); + _openCommon(query, ""); } function hide() { @@ -183,13 +226,17 @@ Item { return; openedFromOverview = false; isClosing = true; - contentVisible = false; + // For directional effects, defer contentVisible=false so content stays rendered during exit slide + if (!Theme.isDirectionalEffect) + contentVisible = false; + + // Trigger exit animation — Behaviors will animate motionX/Y to frozen collapsed position + _motionActive = false; keyboardActive = false; spotlightOpen = false; focusGrab.active = false; ModalManager.closeModal(root); - closeCleanupTimer.start(); } @@ -198,27 +245,7 @@ Item { } function showWithMode(mode) { - closeCleanupTimer.stop(); - - var focusedScreen = CompositorService.getFocusedScreen(); - if (focusedScreen && launcherWindow.screen !== focusedScreen) { - spotlightOpen = false; - isClosing = false; - launcherWindow.screen = focusedScreen; - Qt.callLater(() => root._finishShow("", mode)); - return; - } - - spotlightOpen = true; - isClosing = false; - openedFromOverview = false; - - keyboardActive = true; - ModalManager.openModal(root); - if (useHyprlandFocusGrab) - focusGrab.active = true; - - _ensureContentLoadedAndInitialize("", mode); + _openCommon("", mode); } function toggleWithMode(mode) { @@ -239,10 +266,13 @@ Item { Timer { id: closeCleanupTimer - interval: Theme.modalAnimationDuration + 50 + interval: Theme.variantCloseInterval(Theme.modalAnimationDuration) repeat: false onTriggered: { isClosing = false; + contentVisible = false; + contentWindow.visible = false; + backgroundWindow.visible = false; if (root.unloadContentOnClose) launcherContentLoader.active = false; dialogClosed(); @@ -251,7 +281,6 @@ Item { Connections { target: spotlightContent?.controller ?? null - function onModeChanged(mode) { if (spotlightContent.controller.autoSwitchedToFiles) return; @@ -261,7 +290,7 @@ Item { HyprlandFocusGrab { id: focusGrab - windows: [launcherWindow] + windows: [contentWindow] active: false onCleared: { @@ -286,55 +315,46 @@ Item { if (Quickshell.screens.length === 0) return; - const screenName = launcherWindow.screen?.name; - if (screenName) { + const screen = contentWindow.screen; + const screenName = screen?.name; + + let needsReset = !screen || !screenName; + if (!needsReset) { + needsReset = true; for (let i = 0; i < Quickshell.screens.length; i++) { - if (Quickshell.screens[i].name === screenName) - return; + if (Quickshell.screens[i].name === screenName) { + needsReset = false; + break; + } } } - if (spotlightOpen) - hide(); + if (!needsReset) + return; const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0]; - if (newScreen) - launcherWindow.screen = newScreen; + if (!newScreen) + return; + + root._windowEnabled = false; + backgroundWindow.screen = newScreen; + contentWindow.screen = newScreen; + Qt.callLater(() => { + root._windowEnabled = true; + }); } } + // ── Background window: fullscreen, handles darkening + click-to-dismiss ── PanelWindow { - id: launcherWindow - visible: spotlightOpen || isClosing + id: backgroundWindow + visible: false color: "transparent" - exclusionMode: ExclusionMode.Ignore - 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 - blurRadius: root.cornerRadius - } - - WlrLayershell.namespace: "dms:spotlight" - WlrLayershell.layer: { - switch (Quickshell.env("DMS_MODAL_LAYER")) { - case "bottom": - console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer."); - return WlrLayershell.Top; - case "background": - console.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer."); - return WlrLayershell.Top; - case "overlay": - return WlrLayershell.Overlay; - default: - return WlrLayershell.Top; - } - } - WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None + WlrLayershell.namespace: "dms:spotlight:bg" + WlrLayershell.layer: WlrLayershell.Top + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None anchors { top: true @@ -344,11 +364,11 @@ Item { } mask: Region { - item: spotlightOpen ? fullScreenMask : null + item: (spotlightOpen || isClosing) ? bgFullScreenMask : null } Item { - id: fullScreenMask + id: bgFullScreenMask anchors.fill: parent } @@ -356,13 +376,14 @@ Item { id: backgroundDarken anchors.fill: parent color: "black" - opacity: contentVisible && SettingsData.modalDarkenBackground ? 0.5 : 0 - visible: contentVisible || opacity > 0 + opacity: launcherMotionVisible && SettingsData.modalDarkenBackground ? 0.5 : 0 + visible: launcherMotionVisible || opacity > 0 Behavior on opacity { + enabled: root.animationsEnabled && !Theme.isDirectionalEffect DankAnim { - duration: Theme.modalAnimationDuration - easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized + duration: Math.round(Theme.variantDuration(Theme.modalAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale) + easing.bezierCurve: launcherMotionVisible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve } } } @@ -370,49 +391,136 @@ Item { MouseArea { anchors.fill: parent enabled: spotlightOpen - onClicked: mouse => { - var contentX = modalContainer.x; - var contentY = modalContainer.y; - var contentW = modalContainer.width; - var contentH = modalContainer.height; + onClicked: root.hide() + } + } - if (mouse.x < contentX || mouse.x > contentX + contentW || mouse.y < contentY || mouse.y > contentY + contentH) { - root.hide(); - } + // ── Content window: SMALL, positioned with margins — only renders the modal area ── + PanelWindow { + id: contentWindow + visible: false + color: "transparent" + + WlrLayershell.namespace: "dms:spotlight" + WlrLayershell.layer: { + switch (Quickshell.env("DMS_MODAL_LAYER")) { + case "bottom": + console.error("DankLauncherV2Modal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer."); + return WlrLayershell.Top; + case "background": + console.error("DankLauncherV2Modal: 'background' layer is not valid for modals. Defaulting to 'top' layer."); + return WlrLayershell.Top; + case "overlay": + return WlrLayershell.Overlay; + default: + return WlrLayershell.Top; } } + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None + + anchors { + left: true + top: true + } + + WlrLayershell.margins { + left: root._cwMarginLeft + top: root._cwMarginTop + } + + implicitWidth: root._cwWidth + implicitHeight: root._cwHeight + + mask: Region { + item: contentInputMask + } Item { - id: modalContainer - x: root.modalX - y: root.modalY - width: root.modalWidth - height: root.modalHeight - visible: contentVisible || opacity > 0 + id: contentInputMask + visible: false + x: contentContainer.x + contentWrapper.x + y: contentContainer.y + contentWrapper.y + width: root.alignedWidth + height: root.alignedHeight + } - opacity: contentVisible ? 1 : 0 - scale: contentVisible ? 1 : 0.96 - transformOrigin: Item.Center + Item { + id: contentContainer - Behavior on opacity { - DankAnim { - duration: Theme.modalAnimationDuration - easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized + // For directional/depth: contentContainer is at alignedY from window top (window starts at screen top) + // For standard: contentContainer is at shadowPad from window top (window starts near modal) + x: root._ccX + y: root._ccY + width: root.alignedWidth + height: root.alignedHeight + + 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 collapsedMotionY: { + if (directionalEffect) + return Math.max(root.screenHeight - root._ccY + root.shadowPad, Theme.effectAnimOffset * 1.1); + if (depthEffect) + return -Math.max(Theme.effectAnimOffset * 0.85, 34); + return 0; + } + + // animX/animY are Behavior-animated — DankPopout pattern + property real animX: 0 + property real animY: 0 + property real scaleValue: 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); + } + + Connections { + target: root + function on_MotionActiveChanged() { + contentContainer.animX = Theme.snap(root._motionActive ? 0 : root._frozenMotionX, root.dpr); + contentContainer.animY = Theme.snap(root._motionActive ? 0 : root._frozenMotionY, root.dpr); + contentContainer.scaleValue = root._motionActive ? 1.0 : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed); } } - Behavior on scale { + Behavior on animX { + enabled: root.animationsEnabled DankAnim { - duration: Theme.modalAnimationDuration - easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized + duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive) + easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve } } + Behavior on animY { + enabled: root.animationsEnabled + DankAnim { + duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive) + easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve + } + } + + Behavior on scaleValue { + enabled: root.animationsEnabled && !Theme.isDirectionalEffect + 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 - anchors.fill: parent - level: Theme.elevationLevel3 - fallbackOffset: 6 + 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 @@ -420,46 +528,58 @@ Item { 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 + 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 } + } - onLoaded: { - if (root._pendingInitialize) { - root._initializeAndShow(root._pendingQuery, root._pendingMode); - root._pendingInitialize = false; + 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; + Keys.onEscapePressed: event => { + root.hide(); + event.accepted = true; + } } } - - Rectangle { - anchors.fill: parent - radius: root.cornerRadius - color: "transparent" - border.color: BlurService.borderColor - border.width: BlurService.borderWidth - } } } } diff --git a/quickshell/Modals/DankLauncherV2/LauncherContent.qml b/quickshell/Modals/DankLauncherV2/LauncherContent.qml index 43725151..18034ad3 100644 --- a/quickshell/Modals/DankLauncherV2/LauncherContent.qml +++ b/quickshell/Modals/DankLauncherV2/LauncherContent.qml @@ -41,7 +41,6 @@ FocusScope { editCommentField.text = existing?.comment || ""; editEnvVarsField.text = existing?.envVars || ""; editExtraFlagsField.text = existing?.extraFlags || ""; - editDgpuToggle.checked = existing?.launchOnDgpu || false; editMode = true; Qt.callLater(() => editNameField.forceActiveFocus()); } @@ -65,8 +64,6 @@ FocusScope { override.envVars = editEnvVarsField.text.trim(); if (editExtraFlagsField.text.trim()) override.extraFlags = editExtraFlagsField.text.trim(); - if (editDgpuToggle.checked) - override.launchOnDgpu = true; SessionData.setAppOverride(editAppId, override); closeEditMode(); } @@ -89,7 +86,7 @@ FocusScope { Controller { id: controller - active: root.parentModal?.spotlightOpen ?? true + active: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true viewModeContext: root.viewModeContext onItemExecuted: { @@ -149,18 +146,10 @@ FocusScope { event.accepted = false; return; case Qt.Key_Down: - if (hasCtrl) { - controller.navigateHistory("down"); - } else { - controller.selectNext(); - } + controller.selectNext(); return; case Qt.Key_Up: - if (hasCtrl) { - controller.navigateHistory("up"); - } else { - controller.selectPrevious(); - } + controller.selectPrevious(); return; case Qt.Key_PageDown: controller.selectPageDown(8); @@ -169,10 +158,6 @@ FocusScope { controller.selectPageUp(8); return; case Qt.Key_Right: - if (hasCtrl) { - controller.cycleMode(); - return; - } if (controller.getCurrentSectionViewMode() !== "list") { controller.selectRight(); return; @@ -180,25 +165,12 @@ FocusScope { event.accepted = false; return; case Qt.Key_Left: - if (hasCtrl) { - const reverse = true; - controller.cycleMode(reverse); - return; - } if (controller.getCurrentSectionViewMode() !== "list") { controller.selectLeft(); return; } event.accepted = false; return; - case Qt.Key_H: - if (hasCtrl) { - const reverse = true; - controller.cycleMode(reverse); - return; - } - event.accepted = false; - return; case Qt.Key_J: if (hasCtrl) { controller.selectNext(); @@ -213,13 +185,6 @@ FocusScope { } event.accepted = false; return; - case Qt.Key_L: - if (hasCtrl) { - controller.cycleMode(); - return; - } - event.accepted = false; - return; case Qt.Key_N: if (hasCtrl) { controller.selectNextSection(); @@ -235,19 +200,13 @@ FocusScope { event.accepted = false; return; case Qt.Key_Tab: - if (hasCtrl && actionPanel.hasActions) { + if (actionPanel.hasActions) { actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show(); - return; } - controller.selectNext(); return; case Qt.Key_Backtab: - if (hasCtrl && actionPanel.expanded) { - const reverse = true; - actionPanel.expanded ? actionPanel.cycleAction(reverse) : actionPanel.show(); - return; - } - controller.selectPrevious(); + if (actionPanel.expanded) + actionPanel.hide(); return; case Qt.Key_Return: case Qt.Key_Enter: @@ -311,7 +270,7 @@ FocusScope { Item { anchors.fill: parent - visible: !editMode && !(root.parentModal?.isClosing ?? false) + visible: !editMode Item { id: footerBar @@ -429,7 +388,7 @@ FocusScope { StyledText { anchors.verticalCenter: parent.verticalCenter - text: "Ctrl-Tab " + I18n.tr("actions") + text: "Tab " + I18n.tr("actions") font.pixelSize: Theme.fontSizeSmall - 1 color: Theme.surfaceVariantText visible: actionPanel.hasActions @@ -503,7 +462,7 @@ FocusScope { showClearButton: true textColor: Theme.surfaceText font.pixelSize: Theme.fontSizeLarge - enabled: root.parentModal ? root.parentModal.spotlightOpen : true + enabled: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true placeholderText: "" ignoreUpDownKeys: true ignoreTabKeys: true @@ -737,6 +696,14 @@ FocusScope { Item { width: parent.width height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2) + opacity: { + if (!root.parentModal) + return 1; + if (Theme.isDirectionalEffect && root.parentModal.isClosing) + return 1; + return root.parentModal.isClosing ? 0 : 1; + } + ResultsList { id: resultsList anchors.fill: parent @@ -769,7 +736,6 @@ FocusScope { } function onSearchQueryRequested(query) { searchField.text = query; - searchField.cursorPosition = query.length; } function onModeChanged() { extFilterField.text = ""; @@ -980,15 +946,6 @@ FocusScope { keyNavigationBacktab: editEnvVarsField } } - - DankToggle { - id: editDgpuToggle - width: parent.width - text: I18n.tr("Launch on dGPU by default") - visible: SessionService.nvidiaCommand.length > 0 - checked: false - onToggled: checked => editDgpuToggle.checked = checked - } } } diff --git a/quickshell/Modules/AppDrawer/AppDrawerPopout.qml b/quickshell/Modules/AppDrawer/AppDrawerPopout.qml index ba07d374..21c30870 100644 --- a/quickshell/Modules/AppDrawer/AppDrawerPopout.qml +++ b/quickshell/Modules/AppDrawer/AppDrawerPopout.qml @@ -8,9 +8,6 @@ DankPopout { layerNamespace: "dms:app-launcher" - readonly property real screenWidth: screen?.width ?? 1920 - readonly property real screenHeight: screen?.height ?? 1080 - property string _pendingMode: "" property string _pendingQuery: "" @@ -44,35 +41,8 @@ DankPopout { openWithQuery(query); } - readonly property int _baseWidth: { - switch (SettingsData.dankLauncherV2Size) { - case "micro": - return 500; - case "medium": - return 720; - case "large": - return 860; - default: - return 620; - } - } - - readonly property int _baseHeight: { - switch (SettingsData.dankLauncherV2Size) { - case "micro": - return 480; - case "medium": - return 720; - case "large": - return 860; - default: - return 600; - } - } - - popupWidth: Math.min(_baseWidth, screenWidth - 100) - popupHeight: Math.min(_baseHeight, screenHeight - 100) - + popupWidth: 560 + popupHeight: 640 triggerWidth: 40 positioning: "" contentHandlesKeys: contentLoader.item?.launcherContent?.editMode ?? false @@ -90,7 +60,7 @@ DankPopout { if (!lc) return; - const query = _pendingQuery || (SettingsData.rememberLastQuery ? SessionData.launcherLastQuery : "") || ""; + const query = _pendingQuery; const mode = _pendingMode || SessionData.appDrawerLastMode || "apps"; _pendingMode = ""; _pendingQuery = ""; @@ -102,9 +72,12 @@ DankPopout { if (lc.controller) { lc.controller.searchMode = mode; lc.controller.pluginFilter = ""; - lc.controller.searchQuery = query; - - lc.controller.performSearch(); + lc.controller.searchQuery = ""; + if (query) { + lc.controller.setSearchQuery(query); + } else { + lc.controller.performSearch(); + } } lc.resetScroll?.(); lc.actionPanel?.hide(); @@ -133,7 +106,7 @@ DankPopout { QtObject { id: modalAdapter property bool spotlightOpen: appDrawerPopout.shouldBeVisible - readonly property bool isClosing: !appDrawerPopout.shouldBeVisible + property bool isClosing: appDrawerPopout.isClosing function hide() { appDrawerPopout.close(); diff --git a/quickshell/Modules/ControlCenter/ControlCenterPopout.qml b/quickshell/Modules/ControlCenter/ControlCenterPopout.qml index d8839aaf..7c2bd634 100644 --- a/quickshell/Modules/ControlCenter/ControlCenterPopout.qml +++ b/quickshell/Modules/ControlCenter/ControlCenterPopout.qml @@ -136,9 +136,11 @@ DankPopout { z: 5000 Behavior on opacity { + enabled: !Theme.isDirectionalEffect NumberAnimation { - duration: 200 - easing.type: Easing.OutCubic + duration: Theme.shortDuration + easing.type: Easing.BezierSpline + easing.bezierCurve: root.shouldBeVisible ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve } } } diff --git a/quickshell/Modules/DankBar/DankBarContent.qml b/quickshell/Modules/DankBar/DankBarContent.qml index b3175b21..0cf14b66 100644 --- a/quickshell/Modules/DankBar/DankBarContent.qml +++ b/quickshell/Modules/DankBar/DankBarContent.qml @@ -1184,6 +1184,7 @@ Item { if (!notificationCenterLoader.item) { return; } + notificationCenterLoader.item.triggerScreen = barWindow.screen; const effectiveBarConfig = topBarContent.barConfig; const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); if (notificationCenterLoader.item.setBarContext) { diff --git a/quickshell/Modules/DankDash/DankDashPopout.qml b/quickshell/Modules/DankDash/DankDashPopout.qml index 97a86554..f03fe4f7 100644 --- a/quickshell/Modules/DankDash/DankDashPopout.qml +++ b/quickshell/Modules/DankDash/DankDashPopout.qml @@ -16,7 +16,6 @@ DankPopout { popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500 triggerWidth: 80 screen: triggerScreen - shouldBeVisible: dashVisible property bool __focusArmed: false property bool __contentReady: false diff --git a/quickshell/Modules/DankDash/MediaDropdownOverlay.qml b/quickshell/Modules/DankDash/MediaDropdownOverlay.qml index 35dfe974..f04d6562 100644 --- a/quickshell/Modules/DankDash/MediaDropdownOverlay.qml +++ b/quickshell/Modules/DankDash/MediaDropdownOverlay.qml @@ -44,6 +44,43 @@ Item { property int __volumeHoverCount: 0 + readonly property bool directionalEffect: Theme.isDirectionalEffect + readonly property bool depthEffect: Theme.isDepthEffect + + function panelMotionX(panelWidth, active) { + if (active) + return 0; + if (directionalEffect) { + const travel = Math.max(Theme.effectAnimOffset, panelWidth * 0.85); + return isRightEdge ? -travel : travel; + } + if (depthEffect) { + const travel = Math.max(Theme.effectAnimOffset * 0.7, panelWidth * 0.32); + return isRightEdge ? -travel : travel; + } + return 0; + } + + function panelMotionY(panelType, panelHeight, active) { + if (active) + return 0; + if (directionalEffect) { + if (panelType === 2) + return panelHeight * 0.08; + if (panelType === 3) + return -panelHeight * 0.08; + return 0; + } + if (depthEffect) { + if (panelType === 2) + return panelHeight * 0.04; + if (panelType === 3) + return -panelHeight * 0.04; + return 0; + } + return 0; + } + function volumeAreaEntered() { __volumeHoverCount++; panelEntered(); @@ -62,30 +99,47 @@ Item { visible: dropdownType === 1 && volumeAvailable width: 60 height: 180 - x: isRightEdge ? anchorPos.x : anchorPos.x - width - y: anchorPos.y - height / 2 + x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 1) + y: anchorPos.y - height / 2 + panelMotionY(1, height, dropdownType === 1) radius: Theme.cornerRadius * 2 color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) border.width: 1 - opacity: dropdownType === 1 ? 1 : 0 - scale: dropdownType === 1 ? 1 : 0.96 + opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : 0) + scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : Theme.effectScaleCollapsed) transformOrigin: isRightEdge ? Item.Left : Item.Right Behavior on opacity { - NumberAnimation { - duration: Theme.expressiveDurations.expressiveDefaultSpatial - easing.type: Easing.BezierSpline - easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial + enabled: !Theme.isDirectionalEffect + DankAnim { + duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1) * Theme.variantOpacityDurationScale) + easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve } } Behavior on scale { + enabled: !Theme.isDirectionalEffect NumberAnimation { - duration: Theme.expressiveDurations.expressiveDefaultSpatial + duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1) easing.type: Easing.BezierSpline - easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial + easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve + } + } + + Behavior on x { + NumberAnimation { + duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1) + easing.type: Easing.BezierSpline + easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve + } + } + + Behavior on y { + NumberAnimation { + duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1) + easing.type: Easing.BezierSpline + easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve } } @@ -197,33 +251,50 @@ Item { Rectangle { id: audioDevicesPanel - visible: dropdownType === 2 + visible: dropdownType === 2 && activePlayer !== null width: 280 height: Math.max(200, Math.min(280, availableDevices.length * 50 + 100)) - x: isRightEdge ? anchorPos.x : anchorPos.x - width - y: anchorPos.y - height / 2 + x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 2) + y: anchorPos.y - height / 2 + panelMotionY(2, height, dropdownType === 2) radius: Theme.cornerRadius * 2 color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6) border.width: 2 - opacity: dropdownType === 2 ? 1 : 0 - scale: dropdownType === 2 ? 1 : 0.96 + opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : 0) + scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : Theme.effectScaleCollapsed) transformOrigin: isRightEdge ? Item.Left : Item.Right Behavior on opacity { - NumberAnimation { - duration: Theme.expressiveDurations.expressiveDefaultSpatial - easing.type: Easing.BezierSpline - easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial + enabled: !Theme.isDirectionalEffect + DankAnim { + duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2) * Theme.variantOpacityDurationScale) + easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve } } Behavior on scale { + enabled: !Theme.isDirectionalEffect NumberAnimation { - duration: Theme.expressiveDurations.expressiveDefaultSpatial + duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2) easing.type: Easing.BezierSpline - easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial + easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve + } + } + + Behavior on x { + NumberAnimation { + duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2) + easing.type: Easing.BezierSpline + easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve + } + } + + Behavior on y { + NumberAnimation { + duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2) + easing.type: Easing.BezierSpline + easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve } } @@ -354,30 +425,47 @@ Item { visible: dropdownType === 3 width: 240 height: Math.max(180, Math.min(240, (allPlayers?.length || 0) * 50 + 80)) - x: isRightEdge ? anchorPos.x : anchorPos.x - width - y: anchorPos.y - height / 2 + x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 3) + y: anchorPos.y - height / 2 + panelMotionY(3, height, dropdownType === 3) radius: Theme.cornerRadius * 2 color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6) border.width: 2 - opacity: dropdownType === 3 ? 1 : 0 - scale: dropdownType === 3 ? 1 : 0.96 + opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : 0) + scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : Theme.effectScaleCollapsed) transformOrigin: isRightEdge ? Item.Left : Item.Right Behavior on opacity { - NumberAnimation { - duration: Theme.expressiveDurations.expressiveDefaultSpatial - easing.type: Easing.BezierSpline - easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial + enabled: !Theme.isDirectionalEffect + DankAnim { + duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3) * Theme.variantOpacityDurationScale) + easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve } } Behavior on scale { + enabled: !Theme.isDirectionalEffect NumberAnimation { - duration: Theme.expressiveDurations.expressiveDefaultSpatial + duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3) easing.type: Easing.BezierSpline - easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial + easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve + } + } + + Behavior on x { + NumberAnimation { + duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3) + easing.type: Easing.BezierSpline + easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve + } + } + + Behavior on y { + NumberAnimation { + duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3) + easing.type: Easing.BezierSpline + easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve } } diff --git a/quickshell/Modules/Notifications/Center/NotificationCenterPopout.qml b/quickshell/Modules/Notifications/Center/NotificationCenterPopout.qml index b7b616b8..c2808653 100644 --- a/quickshell/Modules/Notifications/Center/NotificationCenterPopout.qml +++ b/quickshell/Modules/Notifications/Center/NotificationCenterPopout.qml @@ -39,11 +39,9 @@ DankPopout { } } - popupWidth: triggerScreen ? Math.min(500, Math.max(380, triggerScreen.width - 48)) : 400 + popupWidth: 400 popupHeight: stablePopupHeight positioning: "" - animationScaleCollapsed: 0.94 - animationOffset: 0 suspendShadowWhileResizing: false screen: triggerScreen diff --git a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml index 8c9f7f44..ca36867a 100644 --- a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml +++ b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml @@ -32,6 +32,29 @@ PanelWindow { property real _lastReportedAlignedHeight: -1 property real _storedTopMargin: 0 property real _storedBottomMargin: 0 + readonly property bool directionalEffect: Theme.isDirectionalEffect + readonly property bool depthEffect: Theme.isDepthEffect + readonly property real entryTravel: { + const base = Math.abs(Theme.effectAnimOffset); + if (directionalEffect) { + if (isCenterPosition) + return Math.max(base, Math.round(content.height * 1.1)); + return Math.max(base, Math.round(content.width * 0.95)); + } + if (depthEffect) + return Math.max(base, 44); + return base; + } + readonly property real exitTravel: { + if (directionalEffect) { + if (isCenterPosition) + return content.height + entryTravel; + return content.width + entryTravel; + } + if (depthEffect) + return Math.round(entryTravel * 1.35); + return Anims.slidePx; + } readonly property string clearText: I18n.tr("Dismiss") property bool descriptionExpanded: false readonly property bool hasExpandableBody: (notificationData?.htmlBody || "").replace(/<[^>]*>/g, "").trim().length > 0 @@ -145,9 +168,9 @@ PanelWindow { enabled: !exiting && !_isDestroying NumberAnimation { id: implicitHeightAnim - duration: descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration + duration: Theme.variantDuration(descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration, descriptionExpanded) easing.type: Easing.BezierSpline - easing.bezierCurve: Theme.expressiveCurves.emphasized + easing.bezierCurve: descriptionExpanded ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve } } @@ -929,9 +952,9 @@ PanelWindow { if (isCenterPosition) return 0; const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom; - return isLeft ? -Anims.slidePx : Anims.slidePx; + return isLeft ? -entryTravel : entryTravel; } - y: isTopCenter ? -Anims.slidePx : isBottomCenter ? Anims.slidePx : 0 + y: isTopCenter ? -entryTravel : isBottomCenter ? entryTravel : 0 } ] } @@ -943,16 +966,16 @@ PanelWindow { property: isCenterPosition ? "y" : "x" from: { if (isTopCenter) - return -Anims.slidePx; + return -entryTravel; if (isBottomCenter) - return Anims.slidePx; + return entryTravel; const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom; - return isLeft ? -Anims.slidePx : Anims.slidePx; + return isLeft ? -entryTravel : entryTravel; } to: 0 - duration: Theme.notificationEnterDuration + duration: Theme.variantDuration(Theme.notificationEnterDuration, true) easing.type: Easing.BezierSpline - easing.bezierCurve: isCenterPosition ? Theme.expressiveCurves.standardDecel : Theme.expressiveCurves.emphasizedDecel + easing.bezierCurve: Theme.variantPopoutEnterCurve onStopped: { if (!win.exiting && !win._isDestroying) { if (isCenterPosition) { @@ -977,35 +1000,35 @@ PanelWindow { from: 0 to: { if (isTopCenter) - return -Anims.slidePx; + return -exitTravel; if (isBottomCenter) - return Anims.slidePx; + return exitTravel; const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom; - return isLeft ? -Anims.slidePx : Anims.slidePx; + return isLeft ? -exitTravel : exitTravel; } - duration: Theme.notificationExitDuration + duration: Theme.variantDuration(Theme.notificationExitDuration, false) easing.type: Easing.BezierSpline - easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel + easing.bezierCurve: Theme.variantPopoutExitCurve } NumberAnimation { target: content property: "opacity" from: 1 - to: 0 - duration: Theme.notificationExitDuration + to: Theme.isDirectionalEffect ? 1 : 0 + duration: Theme.variantDuration(Theme.notificationExitDuration, false) easing.type: Easing.BezierSpline - easing.bezierCurve: Theme.expressiveCurves.standardAccel + easing.bezierCurve: Theme.variantPopoutExitCurve } NumberAnimation { target: content property: "scale" from: 1 - to: 0.98 - duration: Theme.notificationExitDuration + to: Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed + duration: Theme.variantDuration(Theme.notificationExitDuration, false) easing.type: Easing.BezierSpline - easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel + easing.bezierCurve: Theme.variantPopoutExitCurve } } diff --git a/quickshell/Modules/Settings/TypographyMotionTab.qml b/quickshell/Modules/Settings/TypographyMotionTab.qml index 8648acb4..6cbd361e 100644 --- a/quickshell/Modules/Settings/TypographyMotionTab.qml +++ b/quickshell/Modules/Settings/TypographyMotionTab.qml @@ -55,6 +55,144 @@ Item { anchors.horizontalCenter: parent.horizontalCenter spacing: Theme.spacingXL + SettingsCard { + tab: "typography" + tags: ["animation", "variant", "style", "slide", "fluent", "dynamic", "motion"] + title: I18n.tr("Animation Style") + settingKey: "animationVariant" + iconName: "auto_awesome_motion" + + Item { + width: parent.width + height: animVariantGroup.implicitHeight + clip: true + + DankButtonGroup { + id: animVariantGroup + anchors.horizontalCenter: parent.horizontalCenter + buttonPadding: parent.width < 480 ? Theme.spacingS : Theme.spacingL + minButtonWidth: parent.width < 480 ? 64 : 96 + textSize: parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium + model: [I18n.tr("Material"), I18n.tr("Fluent"), I18n.tr("Dynamic")] + selectionMode: "single" + currentIndex: SettingsData.animationVariant + onSelectionChanged: (index, selected) => { + if (!selected) + return; + SettingsData.set("animationVariant", index); + } + + Connections { + target: SettingsData + function onAnimationVariantChanged() { + animVariantGroup.currentIndex = SettingsData.animationVariant; + } + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + Item { + width: parent.width + height: variantDescription.implicitHeight + Theme.spacingS * 2 + + StyledText { + id: variantDescription + x: Theme.spacingM + y: Theme.spacingS + width: parent.width - Theme.spacingM * 2 + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + text: { + switch (SettingsData.animationVariant) { + case 1: + return I18n.tr("Fluent: Smooth cubic deceleration in, quick snap out — clean, elegant curves."); + case 2: + return I18n.tr("Dynamic: Spring bezier with overshoot — entry briefly exceeds its target then settles. Expressive and alive."); + default: + return I18n.tr("Material: Material Design 3 Expressive bezier curves. The DMS default feel."); + } + } + } + } + } + + SettingsCard { + tab: "typography" + tags: ["animation", "motion", "effect", "slide", "directional", "depth", "spring", "physics"] + title: I18n.tr("Motion Effects") + settingKey: "motionEffect" + iconName: "motion_photos_on" + + Item { + width: parent.width + height: motionEffectGroup.implicitHeight + clip: true + + DankButtonGroup { + id: motionEffectGroup + anchors.horizontalCenter: parent.horizontalCenter + buttonPadding: parent.width < 480 ? Theme.spacingS : Theme.spacingL + minButtonWidth: parent.width < 480 ? 64 : 96 + textSize: parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium + model: [I18n.tr("Standard"), I18n.tr("Directional"), I18n.tr("Depth")] + selectionMode: "single" + currentIndex: SettingsData.motionEffect + onSelectionChanged: (index, selected) => { + if (!selected) + return; + SettingsData.set("motionEffect", index); + } + + Connections { + target: SettingsData + function onMotionEffectChanged() { + motionEffectGroup.currentIndex = SettingsData.motionEffect; + } + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + Item { + width: parent.width + height: motionEffectDescription.implicitHeight + Theme.spacingS * 2 + + StyledText { + id: motionEffectDescription + x: Theme.spacingM + y: Theme.spacingS + width: parent.width - Theme.spacingM * 2 + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + text: { + switch (SettingsData.motionEffect) { + case 1: + return I18n.tr("Directional: Panels glide in from a larger distance at full size — no scale change, pure clean motion."); + case 2: + return I18n.tr("Depth: Panels scale up from small as they slide in — a dramatic pop-forward depth effect."); + default: + return I18n.tr("Standard: Classic Material Design 3 — panels rise from below with a subtle scale. The DMS default."); + } + } + } + } + } + SettingsCard { tab: "typography" tags: ["font", "family", "text", "typography"] diff --git a/quickshell/Modules/WorkspaceOverlays/HyprlandOverview.qml b/quickshell/Modules/WorkspaceOverlays/HyprlandOverview.qml index 19330352..bc32e853 100644 --- a/quickshell/Modules/WorkspaceOverlays/HyprlandOverview.qml +++ b/quickshell/Modules/WorkspaceOverlays/HyprlandOverview.qml @@ -121,9 +121,9 @@ Scope { Behavior on opacity { NumberAnimation { - duration: Theme.expressiveDurations.expressiveDefaultSpatial + duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen) easing.type: Easing.BezierSpline - easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized + easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve } } @@ -154,45 +154,69 @@ Scope { id: scaleTransform origin.x: contentContainer.width / 2 origin.y: contentContainer.height / 2 - xScale: overviewScope.overviewOpen ? 1 : 0.96 - yScale: overviewScope.overviewOpen ? 1 : 0.96 + xScale: overviewScope.overviewOpen ? 1 : Theme.effectScaleCollapsed + yScale: overviewScope.overviewOpen ? 1 : Theme.effectScaleCollapsed Behavior on xScale { NumberAnimation { - duration: Theme.expressiveDurations.expressiveDefaultSpatial + duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen) easing.type: Easing.BezierSpline - easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized + easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve } } Behavior on yScale { NumberAnimation { - duration: Theme.expressiveDurations.expressiveDefaultSpatial + duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen) easing.type: Easing.BezierSpline - easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized + easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve } } } Translate { id: motionTransform - x: 0 - y: overviewScope.overviewOpen ? 0 : Theme.spacingL + x: { + if (overviewScope.overviewOpen) + return 0; + if (Theme.isDirectionalEffect) + return 0; + if (Theme.isDepthEffect) + return Theme.effectAnimOffset * 0.25; + return 0; + } + y: { + if (overviewScope.overviewOpen) + return 0; + if (Theme.isDirectionalEffect) + return -Math.max(contentContainer.height * 0.8, Theme.effectAnimOffset * 1.1); + if (Theme.isDepthEffect) + return Math.max(Theme.effectAnimOffset * 0.85, 28); + return Theme.effectAnimOffset; + } + + Behavior on x { + NumberAnimation { + duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen) + easing.type: Easing.BezierSpline + easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve + } + } Behavior on y { NumberAnimation { - duration: Theme.expressiveDurations.expressiveDefaultSpatial + duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen) easing.type: Easing.BezierSpline - easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized + easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve } } } Behavior on opacity { NumberAnimation { - duration: Theme.expressiveDurations.expressiveDefaultSpatial + duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen) easing.type: Easing.BezierSpline - easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized + easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve } } diff --git a/quickshell/Modules/WorkspaceOverlays/NiriOverviewOverlay.qml b/quickshell/Modules/WorkspaceOverlays/NiriOverviewOverlay.qml index c7b1bf79..f72c1106 100644 --- a/quickshell/Modules/WorkspaceOverlays/NiriOverviewOverlay.qml +++ b/quickshell/Modules/WorkspaceOverlays/NiriOverviewOverlay.qml @@ -202,8 +202,18 @@ Scope { Item { id: spotlightContainer - x: Theme.snap((parent.width - width) / 2, overlayWindow.dpr) - y: Theme.snap((parent.height - height) / 2, overlayWindow.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 collapsedMotionY: { + if (directionalEffect) + return Math.max(height * 0.85, Theme.effectAnimOffset * 1.1); + if (depthEffect) + return Math.max(Theme.effectAnimOffset * 0.8, 30); + return 0; + } + x: Theme.snap((parent.width - width) / 2 + (overlayWindow.shouldShowSpotlight ? 0 : collapsedMotionX), overlayWindow.dpr) + y: Theme.snap((parent.height - height) / 2 + (overlayWindow.shouldShowSpotlight ? 0 : collapsedMotionY), overlayWindow.dpr) readonly property int baseWidth: { switch (SettingsData.dankLauncherV2Size) { @@ -234,8 +244,8 @@ Scope { readonly property bool animatingOut: niriOverviewScope.isClosing && overlayWindow.isSpotlightScreen - scale: overlayWindow.shouldShowSpotlight ? 1.0 : 0.96 - opacity: overlayWindow.shouldShowSpotlight ? 1 : 0 + scale: Theme.isDirectionalEffect ? 1 : (overlayWindow.shouldShowSpotlight ? 1.0 : Theme.effectScaleCollapsed) + opacity: Theme.isDirectionalEffect ? 1 : (overlayWindow.shouldShowSpotlight ? 1 : 0) visible: overlayWindow.shouldShowSpotlight || animatingOut enabled: overlayWindow.shouldShowSpotlight @@ -245,10 +255,11 @@ Scope { Behavior on scale { id: scaleAnimation + enabled: !Theme.isDirectionalEffect NumberAnimation { - duration: Theme.expressiveDurations.fast + duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight) easing.type: Easing.BezierSpline - easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel + easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve onRunningChanged: { if (running || !spotlightContainer.animatingOut) return; @@ -258,10 +269,27 @@ Scope { } Behavior on opacity { + enabled: !Theme.isDirectionalEffect NumberAnimation { - duration: Theme.expressiveDurations.fast + duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight) easing.type: Easing.BezierSpline - easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel + easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve + } + } + + Behavior on x { + NumberAnimation { + duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight) + easing.type: Easing.BezierSpline + easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve + } + } + + Behavior on y { + NumberAnimation { + duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight) + easing.type: Easing.BezierSpline + easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve } } diff --git a/quickshell/Modules/WorkspaceOverlays/OverviewWindow.qml b/quickshell/Modules/WorkspaceOverlays/OverviewWindow.qml index 43899714..2a4d7f52 100644 --- a/quickshell/Modules/WorkspaceOverlays/OverviewWindow.qml +++ b/quickshell/Modules/WorkspaceOverlays/OverviewWindow.qml @@ -62,30 +62,30 @@ Item { Behavior on x { NumberAnimation { - duration: Theme.expressiveDurations.expressiveDefaultSpatial + duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen) easing.type: Easing.BezierSpline - easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel + easing.bezierCurve: Theme.variantModalEnterCurve } } Behavior on y { NumberAnimation { - duration: Theme.expressiveDurations.expressiveDefaultSpatial + duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen) easing.type: Easing.BezierSpline - easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel + easing.bezierCurve: Theme.variantModalEnterCurve } } Behavior on width { NumberAnimation { - duration: Theme.expressiveDurations.expressiveDefaultSpatial + duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen) easing.type: Easing.BezierSpline - easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel + easing.bezierCurve: Theme.variantModalEnterCurve } } Behavior on height { NumberAnimation { - duration: Theme.expressiveDurations.expressiveDefaultSpatial + duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen) easing.type: Easing.BezierSpline - easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel + easing.bezierCurve: Theme.variantModalEnterCurve } } @@ -124,16 +124,16 @@ Item { Behavior on width { NumberAnimation { - duration: Theme.expressiveDurations.expressiveDefaultSpatial + duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen) easing.type: Easing.BezierSpline - easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel + easing.bezierCurve: Theme.variantModalEnterCurve } } Behavior on height { NumberAnimation { - duration: Theme.expressiveDurations.expressiveDefaultSpatial + duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen) easing.type: Easing.BezierSpline - easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel + easing.bezierCurve: Theme.variantModalEnterCurve } } } diff --git a/quickshell/Widgets/DankPopout.qml b/quickshell/Widgets/DankPopout.qml index fb5f0b09..11b519e2 100644 --- a/quickshell/Widgets/DankPopout.qml +++ b/quickshell/Widgets/DankPopout.qml @@ -20,10 +20,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 @@ -74,6 +74,7 @@ Item { signal backgroundClicked property var _lastOpenedScreen: null + property bool isClosing: false property int effectiveBarPosition: 0 property real effectiveBarBottomGap: 0 @@ -156,10 +157,14 @@ Item { } } + property bool animationsEnabled: true + function open() { if (!screen) return; closeTimer.stop(); + isClosing = false; + animationsEnabled = false; // Snapshot mask geometry _frozenMaskX = maskX; @@ -174,12 +179,22 @@ Item { } _lastOpenedScreen = screen; - shouldBeVisible = true; + if (contentContainer) { + contentContainer.animX = Theme.snap(contentContainer.offsetX, root.dpr); + contentContainer.animY = Theme.snap(contentContainer.offsetY, root.dpr); + contentContainer.scaleValue = root.animationScaleCollapsed; + } + if (useBackgroundWindow) { _surfaceMarginLeft = alignedX - shadowBuffer; _surfaceW = alignedWidth + shadowBuffer * 2; + backgroundWindow.visible = true; } + contentWindow.visible = true; + Qt.callLater(() => { + animationsEnabled = true; + shouldBeVisible = true; if (shouldBeVisible && screen) { if (useBackgroundWindow) backgroundWindow.visible = true; @@ -191,6 +206,7 @@ Item { } function close() { + isClosing = true; shouldBeVisible = false; _primeContent = false; PopoutManager.popoutChanged(); @@ -222,9 +238,10 @@ Item { Timer { id: closeTimer - interval: animationDuration + interval: Theme.variantCloseInterval(animationDuration) onTriggered: { if (!shouldBeVisible) { + isClosing = false; contentWindow.visible = false; if (useBackgroundWindow) backgroundWindow.visible = false; @@ -241,7 +258,13 @@ 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: { + if (Theme.isDirectionalEffect) + return Math.max(0, animationOffset) + 16; + if (Theme.isDepthEffect) + return Math.max(0, animationOffset) + 8; + return 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) @@ -353,6 +376,10 @@ Item { mask: Region { item: maskRect + Region { + item: contentExclusionRect + intersection: Intersection.Subtract + } } Rectangle { @@ -361,26 +388,70 @@ Item { color: "transparent" x: root._frozenMaskX y: root._frozenMaskY - width: (shouldBeVisible && backgroundInteractive) ? root._frozenMaskWidth : 0 - height: (shouldBeVisible && backgroundInteractive) ? root._frozenMaskHeight : 0 + width: (backgroundWindow.visible && backgroundInteractive) ? root._frozenMaskWidth : 0 + height: (backgroundWindow.visible && backgroundInteractive) ? root._frozenMaskHeight : 0 } - MouseArea { + Item { + id: contentExclusionRect + visible: false + x: root.alignedX + y: root.alignedY + width: root.alignedWidth + height: root.alignedHeight + } + + Item { + id: outsideClickCatcher x: root._frozenMaskX y: root._frozenMaskY width: root._frozenMaskWidth height: root._frozenMaskHeight - hoverEnabled: false - enabled: 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; + enabled: root.shouldBeVisible && root.backgroundInteractive - if (!outsideContent) - return; - backgroundClicked(); + readonly property real contentLeft: Math.max(0, root.alignedX - x) + readonly property real contentTop: Math.max(0, root.alignedY - y) + readonly property real contentRight: Math.min(width, contentLeft + root.alignedWidth) + readonly property real contentBottom: Math.min(height, contentTop + root.alignedHeight) + + MouseArea { + x: 0 + y: 0 + width: outsideClickCatcher.width + height: Math.max(0, outsideClickCatcher.contentTop) + enabled: parent.enabled + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onClicked: root.backgroundClicked() + } + + MouseArea { + x: 0 + y: outsideClickCatcher.contentBottom + width: outsideClickCatcher.width + height: Math.max(0, outsideClickCatcher.height - outsideClickCatcher.contentBottom) + enabled: parent.enabled + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onClicked: root.backgroundClicked() + } + + MouseArea { + x: 0 + y: outsideClickCatcher.contentTop + width: Math.max(0, outsideClickCatcher.contentLeft) + height: Math.max(0, outsideClickCatcher.contentBottom - outsideClickCatcher.contentTop) + enabled: parent.enabled + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onClicked: root.backgroundClicked() + } + + MouseArea { + x: outsideClickCatcher.contentRight + y: outsideClickCatcher.contentTop + width: Math.max(0, outsideClickCatcher.width - outsideClickCatcher.contentRight) + height: Math.max(0, outsideClickCatcher.contentBottom - outsideClickCatcher.contentTop) + enabled: parent.enabled + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onClicked: root.backgroundClicked() } } @@ -436,7 +507,6 @@ Item { } readonly property bool _fullHeight: useBackgroundWindow && root.fullHeightSurface - anchors { left: true top: true @@ -494,13 +564,65 @@ Item { 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 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 + 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; + } + onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr) onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr) @@ -514,24 +636,27 @@ Item { } 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 } @@ -555,10 +680,9 @@ Item { Item { id: contentWrapper - anchors.centerIn: parent width: parent.width height: parent.height - opacity: shouldBeVisible ? 1 : 0 + 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) @@ -569,8 +693,9 @@ Item { 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: animationDuration + duration: Math.round(Theme.variantDuration(animationDuration, shouldBeVisible) * Theme.variantOpacityDurationScale) easing.type: Easing.BezierSpline easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve }