Skip to content

Commit c958b0f

Browse files
committed
feat: category and tab content transitions
1 parent 75e7c9b commit c958b0f

8 files changed

Lines changed: 244 additions & 129 deletions

File tree

modules/nexus/ContentArea.qml

Lines changed: 151 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pragma ComponentBehavior: Bound
22

33
import QtQuick
4+
import QtQuick.Controls
45
import QtQuick.Layouts
56
import qs.components
67
import qs.services
@@ -16,6 +17,11 @@ Item {
1617
readonly property var tabs: activeConfig ? activeConfig.tabs : []
1718

1819
property int activeTabIndex: 0
20+
property string _prevCategory: ""
21+
property var _prevConfig: null
22+
property var _prevTabs: []
23+
property bool _categoryTransitioning: false
24+
property real _slideOffset: 0
1925

2026
function updateTabIndicator() {
2127
const item = tabRepeater.itemAt(activeTabIndex);
@@ -42,11 +48,39 @@ Item {
4248
}
4349

4450
onActiveConfigChanged: {
45-
activeTabIndex = 0;
51+
if (session.activeCategory === "") {
52+
_prevCategory = "";
53+
_prevConfig = null;
54+
_prevTabs = [];
55+
activeTabIndex = 0;
56+
tabSwipeView.currentIndex = 0;
57+
contentContainer.opacity = 0;
58+
_slideOffset = 0;
59+
_categoryTransitioning = false;
60+
} else if (_prevCategory === "") {
61+
_prevCategory = session.activeCategory;
62+
_prevConfig = activeConfig;
63+
_prevTabs = tabs;
64+
activeTabIndex = 0;
65+
tabSwipeView.currentIndex = 0;
66+
contentFadeOut.stop();
67+
contentContainer.opacity = 0;
68+
_slideOffset = contentContainer.height * 0.15;
69+
contentFadeIn.restart();
70+
_categoryTransitioning = false;
71+
} else {
72+
activeTabIndex = 0;
73+
tabSwipeView.currentIndex = 0;
74+
_categoryTransitioning = true;
75+
contentFadeOut.start();
76+
}
4677
tabIndicatorUpdate.restart();
4778
}
4879

4980
onActiveTabIndexChanged: {
81+
if (!_categoryTransitioning && _prevCategory === session.activeCategory && _prevCategory !== "") {
82+
tabSwipeView.currentIndex = activeTabIndex;
83+
}
5084
tabIndicatorUpdate.restart();
5185
}
5286

@@ -65,13 +99,77 @@ Item {
6599
target: root.session
66100
}
67101

102+
ParallelAnimation {
103+
id: contentFadeOut
104+
105+
onFinished: {
106+
root._prevCategory = root.session.activeCategory;
107+
root._prevConfig = root.activeConfig;
108+
root._prevTabs = root.tabs;
109+
tabSwipeView.currentIndex = 0;
110+
root._slideOffset = contentContainer.height * 0.15;
111+
contentFadeIn.start();
112+
}
113+
114+
NumberAnimation {
115+
target: contentContainer
116+
property: "opacity"
117+
from: 1
118+
to: 0
119+
duration: 150
120+
easing.type: Easing.InOutQuad
121+
}
122+
123+
NumberAnimation {
124+
target: root
125+
property: "_slideOffset"
126+
from: 0
127+
to: -contentContainer.height * 0.15
128+
duration: 150
129+
easing.type: Easing.InOutQuad
130+
}
131+
}
132+
133+
ParallelAnimation {
134+
id: contentFadeIn
135+
136+
onFinished: {
137+
root._categoryTransitioning = false;
138+
}
139+
140+
NumberAnimation {
141+
target: contentContainer
142+
property: "opacity"
143+
from: 0
144+
to: 1
145+
duration: 250
146+
easing.type: Easing.InOutQuad
147+
}
148+
149+
NumberAnimation {
150+
target: root
151+
property: "_slideOffset"
152+
from: contentContainer.height * 0.15
153+
to: 0
154+
duration: 250
155+
easing.type: Easing.InOutQuad
156+
}
157+
}
158+
68159
ColumnLayout {
160+
id: contentContainer
161+
69162
anchors.fill: parent
70163
anchors.rightMargin: Appearance.padding.large * 2
71164
anchors.leftMargin: Appearance.padding.large * 2
72165
anchors.topMargin: Appearance.padding.large
73166
anchors.bottomMargin: Appearance.padding.large
74167
spacing: Appearance.spacing.normal
168+
opacity: 1
169+
170+
transform: Translate {
171+
y: root._slideOffset // qmllint disable Quick.layout-positioning
172+
}
75173

76174
// Header: title + description
77175
ColumnLayout {
@@ -81,17 +179,17 @@ Item {
81179
spacing: Appearance.spacing.small / 2
82180

83181
StyledText {
84-
Layout.fillWidth: true
85-
text: root.activeConfig ? root.activeConfig.title : ""
86-
font.pointSize: Appearance.font.size.extraLarge
87-
font.weight: Font.Medium
182+
text: root._prevConfig?.label ?? ""
183+
font.pointSize: Appearance.font.size.larger + 4
184+
font.weight: Font.DemiBold
185+
color: Colours.palette.m3onSurface
88186
}
89187

90188
StyledText {
91-
Layout.fillWidth: true
92-
text: root.activeConfig ? root.activeConfig.description : ""
189+
text: root._prevConfig?.description ?? ""
93190
font.pointSize: Appearance.font.size.normal
94-
color: Qt.alpha(Colours.palette.m3onSurface, 0.6)
191+
color: Qt.alpha(Colours.palette.m3onSurface, 0.7)
192+
visible: root._prevConfig && root._prevConfig.description
95193
}
96194
}
97195

@@ -101,7 +199,7 @@ Item {
101199
Layout.fillWidth: true
102200
Layout.topMargin: Appearance.spacing.smaller
103201
Layout.preferredHeight: 48
104-
visible: root.tabs.length > 0
202+
visible: root._prevTabs.length > 0
105203

106204
// Track line
107205
Rectangle {
@@ -123,7 +221,7 @@ Item {
123221
Repeater {
124222
id: tabRepeater
125223

126-
model: root.tabs
224+
model: root._prevTabs
127225

128226
delegate: Rectangle {
129227
id: tabItem
@@ -178,7 +276,7 @@ Item {
178276
height: 3
179277
radius: 1.5
180278
color: Colours.palette.m3primary
181-
visible: root.tabs.length > 0
279+
visible: root._prevTabs.length > 0
182280

183281
x: targetX
184282
width: targetWidth
@@ -201,46 +299,60 @@ Item {
201299
}
202300

203301
// Panel content
204-
Item {
302+
SwipeView {
303+
id: tabSwipeView
304+
205305
Layout.fillWidth: true
206306
Layout.fillHeight: true
207307
Layout.topMargin: Appearance.spacing.normal
208308

209-
Loader {
210-
id: panelLoader
309+
interactive: false
310+
currentIndex: root.activeTabIndex
311+
clip: true
211312

212-
readonly property string targetSource: root.activeConfig ? "panels/" + root.activeConfig.id.charAt(0).toUpperCase() + root.activeConfig.id.slice(1) + "Panel.qml" : ""
213-
property string resolvedSource: targetSource
313+
Repeater {
314+
model: root._prevTabs.length > 0 ? root._prevTabs.length : 1
214315

215-
anchors.fill: parent
216-
asynchronous: true
217-
source: resolvedSource
316+
delegate: Item {
317+
required property int index
218318

219-
onTargetSourceChanged: resolvedSource = targetSource
319+
Loader {
320+
id: tabPanelLoader
220321

221-
onStatusChanged: {
222-
if (status === Loader.Error && resolvedSource !== "panels/PlaceholderPanel.qml") {
223-
Qt.callLater(() => {
224-
resolvedSource = "panels/PlaceholderPanel.qml";
225-
});
226-
}
227-
}
322+
readonly property string targetSource: {
323+
if (!root._prevConfig)
324+
return "";
325+
if (root._prevTabs.length === 0) {
326+
// e.g panels/AppearancePanel.qml, uses lowercase def from registry and matches to Pascal cased file
327+
return "panels/" + root._prevConfig.id.charAt(0).toUpperCase() + root._prevConfig.id.slice(1) + "Panel.qml";
328+
}
329+
return "panels/" + root._prevConfig.id.charAt(0).toUpperCase() + root._prevConfig.id.slice(1) + "Panel.qml";
330+
}
331+
property string resolvedSource: targetSource
228332

229-
onLoaded: {
230-
if (item && item.hasOwnProperty("activeTabIndex")) {
231-
item.activeTabIndex = root.activeTabIndex;
232-
}
233-
}
234-
}
333+
anchors.centerIn: parent
334+
width: parent.width
335+
height: parent.height
336+
asynchronous: true
337+
source: resolvedSource
338+
339+
onTargetSourceChanged: resolvedSource = targetSource
235340

236-
Connections {
237-
function onActiveTabIndexChanged() {
238-
if (panelLoader.item && panelLoader.item.hasOwnProperty("activeTabIndex")) {
239-
panelLoader.item.activeTabIndex = root.activeTabIndex;
341+
onStatusChanged: {
342+
if (status === Loader.Error && resolvedSource !== "panels/PlaceholderPanel.qml") {
343+
Qt.callLater(() => {
344+
resolvedSource = "panels/PlaceholderPanel.qml";
345+
});
346+
}
347+
}
348+
349+
onLoaded: {
350+
if (item && item.hasOwnProperty("activeTabIndex")) {
351+
item.activeTabIndex = parent.index;
352+
}
353+
}
240354
}
241355
}
242-
243-
target: root
244356
}
245357
}
246358
}

modules/nexus/Nexus.qml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ Item {
265265

266266
Behavior on y {
267267
enabled: flyout.open
268-
268+
269269
NumberAnimation {
270270
duration: Appearance.anim.durations.expressiveDefaultSpatial
271271
easing.type: Easing.BezierSpline

modules/nexus/Sidebar.qml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Item {
3838
function cancelFlyoutClose() {
3939
flyoutCloseTimer.stop();
4040
}
41-
41+
4242
Timer {
4343
id: openDelayTimer
4444

@@ -49,8 +49,6 @@ Item {
4949
}
5050
}
5151

52-
53-
5452
Timer {
5553
id: flyoutCloseTimer
5654

modules/nexus/SidebarFlyout.qml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ Item {
104104

105105
NumberAnimation {
106106
id: contentFadeIn
107-
107+
108108
target: contentContainer
109109
property: "opacity"
110110
from: 0

modules/nexus/components/SidebarNavItem.qml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Item {
2121

2222
property bool hovered: false
2323
property bool flyoutActive: false
24-
24+
2525
signal flyoutRequested(real itemY)
2626
signal flyoutCloseRequested
2727

modules/nexus/components/SidebarPopout.qml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
pragma ComponentBehavior: Bound
22

33
import QtQuick
4-
import QtQuick.Layouts
54
import qs.config
65

76
Item {
@@ -109,14 +108,14 @@ Item {
109108

110109
Loader {
111110
id: contentLoader
112-
111+
113112
anchors.fill: parent
114113
sourceComponent: root._prevType === "search" ? root._searchComponent : root._prevType === "config" ? root._configComponent : null
115114
}
116115

117116
Behavior on anchors.leftMargin {
118117
enabled: root.flyoutOpen === (root.flyoutDrawerWidth >= 100)
119-
118+
120119
NumberAnimation {
121120
duration: Appearance.anim.durations.expressiveDefaultSpatial
122121
easing.type: Easing.BezierSpline

0 commit comments

Comments
 (0)