refactored the way the game carousel scrolls and animates. added snapping.

This commit is contained in:
2026-08-11 22:55:05 -05:00
parent 670a7d744e
commit 32df106ab3
+114 -10
View File
@@ -100,17 +100,111 @@ QtObject {
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
// TODO: Tune this stuff !!! it feels just slightly janky and off for now, game cards don't align when scrolling, they go all the way out // these properties are primarily for the gamesList snapping
// to the edge with no spacing at all when on the leftmost or rightmost cards, and vertical navigation from the dock feels *wrong*, idk, readonly property int cardWidth: 120
// fix it sometime. readonly property int cardSpacing: 16
height: 140 readonly property int cardPitch: cardWidth + cardSpacing
readonly property int safeLeft: 90
readonly property int safeRight: width - 90
readonly property int edgePeek: 98
readonly property int snapOffset: cardWidth - edgePeek
property real scrollTarget: contentX
height: 200
orientation: ListView.Horizontal orientation: ListView.Horizontal
preferredHighlightBegin: 200 spacing: 16
preferredHighlightEnd: 420 leftMargin: 16
highlightRangeMode: ListView.ApplyRange rightMargin: 16
spacing: 20
model: 20 // this is a placeholder for now, I don't know how I really want to handle max game carousel size yet. another time. model: 20 // this is a placeholder for now, I don't know how I really want to handle max game carousel size yet. another time.
// provides the smoothing animation for game selection
NumberAnimation {
id: scrollAnimation
target: gamesList
property: "contentX"
duration: 200
easing.type: Easing.OutCubic
}
// used to scroll to a new game on the game list
function scrollTo(targetX) {
if (Math.abs(targetX - gamesList.contentX) < 0.5) {
return
}
scrollAnimation.stop()
scrollAnimation.from = gamesList.contentX
scrollAnimation.to = targetX
scrollAnimation.start()
}
// provides snapping logic so that the games list is snapped to a card pitch
function snappedContentX(snapTargetX) {
// set up a relative coord for our snapping logic, so we line up right
let relativeTarget = snapTargetX - gamesList.originX
// finds the nearest point on the snap grid
// it's pretty obtuse, so as an explanation: we subtract snapOffset first so the grid becomes a
// simple 0, cardPitch, 2*cardPitch, etc grid. Divide by cardPitch to get the nearest grid index,
// round it, then multiply by cardPitch to convert that index back to a position. Finally we
// wanna add snapOffset (and originX) back to line it up with our true grid.
let snappedTargetX = gamesList.originX + Math.round((relativeTarget - gamesList.snapOffset) / gamesList.cardPitch) * gamesList.cardPitch + gamesList.snapOffset
// calculate the furthest possible ends of the carousel
// Math.max is for clamping it to make sure even if there are very few games on the board that it
// wont return something stupid
let minimum = gamesList.originX - gamesList.leftMargin
let maximum = gamesList.originX + Math.max(0, gamesList.contentWidth + gamesList.leftMargin + gamesList.rightMargin - gamesList.width) - gamesList.leftMargin
// this section of code is here to make sure that when the game carousel is flicked all the way
// to the leftmost or rightmost ends, it doesn't snap and cutoff the leftmost or rightmost
// card in a way that feels janky. it works by calculating the distance from the current target
// to the very edges, and if it's closer to one of the edges, just snap to the edge.
let bestTarget = snappedTargetX
let bestDistance = Math.abs(snapTargetX - snappedTargetX)
let distanceToMinimum = Math.abs(snapTargetX - minimum)
if (distanceToMinimum < bestDistance) {
bestTarget = minimum
bestDistance = distanceToMinimum
}
let distanceToMaximum = Math.abs(snapTargetX - maximum)
if (distanceToMaximum < bestDistance) {
bestTarget = maximum
}
return Math.max(minimum, Math.min(bestTarget, maximum))
}
// this is a function to make sure the games list scrolls, but in a predictable, nice-feeling way.
// specifically, it scrolls but makes sure the games list is snapped to a specific card pitch.
function ensureCurrentInSafeZone() {
if (!gamesList.currentItem) {
return
}
let item = gamesList.currentItem
let itemLeft = item.x - gamesList.contentX
let itemRight = itemLeft + item.width
let targetContentX = gamesList.contentX
if (itemLeft < gamesList.safeLeft) {
targetContentX -= gamesList.safeLeft - itemLeft
} else if (itemRight > gamesList.safeRight) {
targetContentX += itemRight - gamesList.safeRight
} else {
return
}
gamesList.scrollTo(gamesList.snappedContentX(targetContentX))
}
Component.onCompleted: { // on startup, this should select the first game in the list. Component.onCompleted: { // on startup, this should select the first game in the list.
if (currentItem) { if (currentItem) {
@@ -118,11 +212,21 @@ QtObject {
} }
} }
// every time the index changes, we wanna re-calculate the game carousel's position
onCurrentIndexChanged: {
ensureCurrentInSafeZone()
}
// after flicking, we re-snap the carousel wherever it landed
onMovementEnded: {
gamesList.scrollTo(gamesList.snappedContentX(gamesList.contentX))
}
delegate: Rectangle { // TODO: design a real game card QML to replace this placeholder delegate: Rectangle { // TODO: design a real game card QML to replace this placeholder
id: gameTile id: gameTile
width: 110 width: 120
height: 110 height: 180
color: navigationManager.currentItem === gameTile ? "blue" : "white" color: navigationManager.currentItem === gameTile ? "blue" : "white"