From 851bdc150db5eb4a1314294dc3a786d675dc73b3 Mon Sep 17 00:00:00 2001 From: tibssy Date: Mon, 23 Mar 2026 01:00:08 +0000 Subject: [PATCH] refactor: extract launcher slide transition logic into reusable NSlideSwapView component --- Modules/Panels/Launcher/LauncherCore.qml | 123 ++------------------- Widgets/NSlideSwapView.qml | 134 +++++++++++++++++++++++ 2 files changed, 144 insertions(+), 113 deletions(-) create mode 100644 Widgets/NSlideSwapView.qml diff --git a/Modules/Panels/Launcher/LauncherCore.qml b/Modules/Panels/Launcher/LauncherCore.qml index c9f5c4fb6..fb8f7fc4a 100644 --- a/Modules/Panels/Launcher/LauncherCore.qml +++ b/Modules/Panels/Launcher/LauncherCore.qml @@ -27,7 +27,7 @@ Rectangle { } // Expose for preview panel positioning - readonly property var resultsView: resultsViewLoader.item + readonly property var resultsView: resultsSwapView.item // State property string searchText: "" @@ -44,13 +44,6 @@ Rectangle { property real globalLastMouseY: 0 property bool globalMouseInitialized: false property bool mouseTrackingReady: false // Delay tracking until panel is settled - property bool categoryTransitionRunning: false - property real resultsSlideInOffset: 0 - property real resultsSlideInOpacity: 1 - property real resultsSnapshotOffset: 0 - property real resultsSnapshotOpacity: 0 - property real resultsSnapshotTargetOffset: 0 - property int pendingCategoryTabIndex: -1 readonly property bool animationsDisabled: Settings.data.general.animationDisabled @@ -196,7 +189,8 @@ Rectangle { function onClosed() { searchText = ""; ignoreMouseHover = true; - resetCategoryTransitionVisuals(); + if (resultsSwapView) + resultsSwapView.resetVisuals(); for (let provider of providers) { if (provider.onClosed) provider.onClosed(); @@ -207,17 +201,6 @@ Rectangle { requestClose(); } - function resetCategoryTransitionVisuals() { - categoryTransitionRunning = false; - pendingCategoryTabIndex = -1; - resultsSlideInOffset = 0; - resultsSlideInOpacity = 1; - resultsSnapshotOffset = 0; - resultsSnapshotOpacity = 0; - resultsSnapshot.visible = false; - categorySlideTransition.stop(); - } - function applyCategorySelection(tabIndex, categories) { const categoryList = categories || providerCategories; if (!categoryList || tabIndex < 0 || tabIndex >= categoryList.length) @@ -240,44 +223,14 @@ Rectangle { if (tabIndex === currentIdx) return; - const canAnimate = !animationsDisabled && resultsViewport.width > 0 && resultsViewport.height > 0; + const canAnimate = !animationsDisabled && resultsSwapView.width > 0 && resultsSwapView.height > 0; if (!canAnimate) { applyCategorySelection(tabIndex, cats); return; } - if (categoryTransitionRunning) - resetCategoryTransitionVisuals(); - - const movingForward = tabIndex > currentIdx; - const slideDistance = Math.max(1, resultsViewport.width + Style.marginXL); - - resultsSnapshot.visible = true; - resultsSnapshotOffset = 0; - resultsSnapshotOpacity = 1; - resultsSnapshotTargetOffset = movingForward ? -slideDistance : slideDistance; - - resultsSlideInOffset = movingForward ? slideDistance : -slideDistance; - resultsSlideInOpacity = 0.0; - - pendingCategoryTabIndex = tabIndex; - categoryTransitionRunning = true; - resultsSnapshot.scheduleUpdate(); - - Qt.callLater(() => { - if (!categoryTransitionRunning || pendingCategoryTabIndex < 0) - return; - - const pendingIndex = pendingCategoryTabIndex; - pendingCategoryTabIndex = -1; - - const pendingCategories = providerCategories; - if (!applyCategorySelection(pendingIndex, pendingCategories)) { - resetCategoryTransitionVisuals(); - return; - } - categorySlideTransition.restart(); - }); + const direction = tabIndex > currentIdx ? 1 : -1; + resultsSwapView.swap(direction, () => applyCategorySelection(tabIndex, providerCategories)); } // Public API @@ -757,70 +710,14 @@ Rectangle { } // Results view - Item { - id: resultsViewport + NSlideSwapView { + id: resultsSwapView Layout.fillWidth: true Layout.leftMargin: Style.marginL Layout.rightMargin: Style.marginL Layout.fillHeight: true - clip: true - - ShaderEffectSource { - id: resultsSnapshot - visible: false - width: parent.width - height: parent.height - y: 0 - sourceItem: resultsViewLoader - hideSource: false - live: false - smooth: true - z: 2 - x: root.resultsSnapshotOffset - opacity: root.resultsSnapshotOpacity - } - - Loader { - id: resultsViewLoader - width: parent.width - height: parent.height - x: root.resultsSlideInOffset - opacity: root.resultsSlideInOpacity - sourceComponent: root.isSingleView ? singleViewComponent : (root.isGridView ? gridViewComponent : listViewComponent) - } - } - - ParallelAnimation { - id: categorySlideTransition - NumberAnimation { - target: root - property: "resultsSlideInOffset" - to: 0 - duration: Style.animationNormal - easing.type: Easing.OutCubic - } - NumberAnimation { - target: root - property: "resultsSlideInOpacity" - to: 1 - duration: Style.animationNormal - easing.type: Easing.OutCubic - } - NumberAnimation { - target: root - property: "resultsSnapshotOffset" - to: root.resultsSnapshotTargetOffset - duration: Style.animationNormal - easing.type: Easing.OutCubic - } - NumberAnimation { - target: root - property: "resultsSnapshotOpacity" - to: 0.25 - duration: Style.animationNormal - easing.type: Easing.OutCubic - } - onFinished: root.resetCategoryTransitionVisuals() + animationsEnabled: !root.animationsDisabled + sourceComponent: root.isSingleView ? singleViewComponent : (root.isGridView ? gridViewComponent : listViewComponent) } // -------------------------- diff --git a/Widgets/NSlideSwapView.qml b/Widgets/NSlideSwapView.qml new file mode 100644 index 000000000..73db21fd9 --- /dev/null +++ b/Widgets/NSlideSwapView.qml @@ -0,0 +1,134 @@ +import QtQuick +import qs.Commons + +Item { + id: root + + property Component sourceComponent + property bool animationsEnabled: true + property int duration: Style.animationNormal + property real transitionGap: Style.marginXL + property real incomingStartOpacity: 0.0 + property real outgoingTargetOpacity: 0.25 + + readonly property var item: contentLoader.item + readonly property bool running: _running + + property bool _running: false + property var _pendingApplyChange: null + property real _contentOffset: 0 + property real _contentOpacity: 1 + property real _snapshotOffset: 0 + property real _snapshotOpacity: 0 + property real _snapshotTargetOffset: 0 + + clip: true + + function resetVisuals() { + _running = false; + _pendingApplyChange = null; + _contentOffset = 0; + _contentOpacity = 1; + _snapshotOffset = 0; + _snapshotOpacity = 0; + snapshot.visible = false; + transition.stop(); + } + + function swap(direction, applyChange) { + if (!animationsEnabled || width <= 0 || height <= 0 || direction === 0) { + if (applyChange) + applyChange(); + return; + } + + if (_running) + resetVisuals(); + + const slideDistance = Math.max(1, width + transitionGap); + const movingForward = direction > 0; + + snapshot.visible = true; + _snapshotOffset = 0; + _snapshotOpacity = 1; + _snapshotTargetOffset = movingForward ? -slideDistance : slideDistance; + + _contentOffset = movingForward ? slideDistance : -slideDistance; + _contentOpacity = incomingStartOpacity; + + _pendingApplyChange = applyChange || null; + _running = true; + snapshot.scheduleUpdate(); + + Qt.callLater(() => { + if (!_running) + return; + + const applyFn = _pendingApplyChange; + _pendingApplyChange = null; + const shouldAnimate = applyFn ? applyFn() !== false : true; + if (!shouldAnimate) { + resetVisuals(); + return; + } + transition.restart(); + }); + } + + ShaderEffectSource { + id: snapshot + visible: false + width: parent.width + height: parent.height + y: 0 + sourceItem: contentLoader + hideSource: false + live: false + smooth: true + z: 2 + x: root._snapshotOffset + opacity: root._snapshotOpacity + } + + Loader { + id: contentLoader + width: parent.width + height: parent.height + x: root._contentOffset + opacity: root._contentOpacity + sourceComponent: root.sourceComponent + } + + ParallelAnimation { + id: transition + NumberAnimation { + target: root + property: "_contentOffset" + to: 0 + duration: root.duration + easing.type: Easing.OutCubic + } + NumberAnimation { + target: root + property: "_contentOpacity" + to: 1 + duration: root.duration + easing.type: Easing.OutCubic + } + NumberAnimation { + target: root + property: "_snapshotOffset" + to: root._snapshotTargetOffset + duration: root.duration + easing.type: Easing.OutCubic + } + NumberAnimation { + target: root + property: "_snapshotOpacity" + to: root.outgoingTargetOpacity + duration: root.duration + easing.type: Easing.OutCubic + } + onFinished: root.resetVisuals() + } +}