mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
Merge pull request #2265 from tibssy/feat/launcher-category-animations
Feat/launcher category animations
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user