Merge pull request #2265 from tibssy/feat/launcher-category-animations

Feat/launcher category animations
This commit is contained in:
Lysec
2026-03-24 12:47:41 +01:00
committed by GitHub
2 changed files with 175 additions and 8 deletions
+41 -8
View File
@@ -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: ""
@@ -189,6 +189,8 @@ Rectangle {
function onClosed() {
searchText = "";
ignoreMouseHover = true;
if (resultsSwapView)
resultsSwapView.resetVisuals();
for (let provider of providers) {
if (provider.onClosed)
provider.onClosed();
@@ -199,6 +201,38 @@ Rectangle {
requestClose();
}
function applyCategorySelection(tabIndex, categories) {
const categoryList = categories || providerCategories;
if (!categoryList || tabIndex < 0 || tabIndex >= categoryList.length)
return false;
currentProvider.selectCategory(categoryList[tabIndex]);
categoryTabs.currentIndex = tabIndex;
return true;
}
function selectCategoryWithSlide(tabIndex) {
if (!showProviderCategories || !currentProvider || !currentProvider.selectCategory)
return;
const cats = providerCategories;
if (!cats || tabIndex < 0 || tabIndex >= cats.length)
return;
const currentIdx = cats.indexOf(currentProvider.selectedCategory);
if (tabIndex === currentIdx)
return;
const canAnimate = !animationsDisabled && resultsSwapView.width > 0 && resultsSwapView.height > 0;
if (!canAnimate) {
applyCategorySelection(tabIndex, cats);
return;
}
const direction = tabIndex > currentIdx ? 1 : -1;
resultsSwapView.swap(direction, () => applyCategorySelection(tabIndex, providerCategories));
}
// Public API
function setSearchText(text) {
searchText = text;
@@ -454,8 +488,7 @@ Rectangle {
var cats = providerCategories;
var idx = cats.indexOf(currentProvider.selectedCategory);
var nextIdx = (idx + 1) % cats.length;
currentProvider.selectCategory(cats[nextIdx]);
categoryTabs.currentIndex = nextIdx;
selectCategoryWithSlide(nextIdx);
} else {
selectNextWrapped();
}
@@ -466,8 +499,7 @@ Rectangle {
var cats2 = providerCategories;
var idx2 = cats2.indexOf(currentProvider.selectedCategory);
var prevIdx = ((idx2 - 1) % cats2.length + cats2.length) % cats2.length;
currentProvider.selectCategory(cats2[prevIdx]);
categoryTabs.currentIndex = prevIdx;
selectCategoryWithSlide(prevIdx);
} else {
selectPreviousWrapped();
}
@@ -672,18 +704,19 @@ Rectangle {
tooltipText: root.currentProvider.getCategoryName ? root.currentProvider.getCategoryName(modelData) : modelData
tabIndex: index
checked: categoryTabs.currentIndex === index
onClicked: root.currentProvider.selectCategory(modelData)
onClicked: root.selectCategoryWithSlide(index)
}
}
}
// Results view
Loader {
id: resultsViewLoader
NSlideSwapView {
id: resultsSwapView
Layout.fillWidth: true
Layout.leftMargin: Style.marginL
Layout.rightMargin: Style.marginL
Layout.fillHeight: true
animationsEnabled: !root.animationsDisabled
sourceComponent: root.isSingleView ? singleViewComponent : (root.isGridView ? gridViewComponent : listViewComponent)
}
+134
View File
@@ -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()
}
}