From 3ed48163aaf77003659cf6e53c2594be51acddd7 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Sun, 23 Aug 2026 16:58:16 -0600 Subject: [PATCH] fix(library): correct Tracks viewport restoration Restore the semantic anchor in viewport coordinates and suppress lazy-page requests while the ListView is being repositioned. This avoids treating content coordinates as screen offsets and prevents recursive page loads during restoration. cx --- crates/qbz-qt/qml/views/local/LocalTracksTab.qml | 16 +++++++++++++--- crates/qbz-qt/src/local_state.rs | 7 ++++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/crates/qbz-qt/qml/views/local/LocalTracksTab.qml b/crates/qbz-qt/qml/views/local/LocalTracksTab.qml index b45cc2734..497045c05 100644 --- a/crates/qbz-qt/qml/views/local/LocalTracksTab.qml +++ b/crates/qbz-qt/qml/views/local/LocalTracksTab.qml @@ -33,6 +33,9 @@ Item { // contentY change before that property turns true. All of those changes // describe the same page miss and must collapse into one bridge call. property bool pageRequestPending: false + // Programmatic viewport restoration emits the same Flickable signals as + // user input. Keep those signals from recursively requesting another page. + property bool restoringPageAnchor: false // Invalidates a deferred viewport restore when another document/model // replacement wins before Qt's next event-loop turn. property int entriesEpoch: 0 @@ -40,6 +43,7 @@ Item { function requestNextPage(force) { if (root.nativeModelActive || root.pageRequestPending + || root.restoringPageAnchor || !QbzLocal.localTracksHasMore || QbzLocal.localTracksLoadingMore || QbzLocal.localTracksLoading) @@ -88,8 +92,12 @@ Item { return { "id": String(root.entries[i].row.id), // Position inside the viewport, including a partially clipped - // first row or a group header immediately above it. - "screenY": cell ? cell.mapToItem(list, 0, 0).y : 0 + // first row or a group header immediately above it. A ListView + // delegate's y is in contentItem coordinates, so subtracting the + // viewport's contentY is the visual coordinate. mapToItem(list) + // preserves the content coordinate here and would add thousands + // of rows back into the restore calculation. + "screenY": cell ? cell.y - list.contentY : 0 } } @@ -115,15 +123,17 @@ Item { Qt.callLater(function () { if (epoch !== root.entriesEpoch || root.nativeModelActive) return + root.restoringPageAnchor = true list.forceLayout() list.positionViewAtIndex(target, ListView.Beginning) list.forceLayout() var cell = list.itemAtIndex(target) - var currentScreenY = cell ? cell.mapToItem(list, 0, 0).y : 0 + var currentScreenY = cell ? cell.y - list.contentY : 0 var wanted = list.contentY + currentScreenY - anchor.screenY var minY = list.originY var maxY = minY + Math.max(0, list.contentHeight - list.height) list.contentY = Math.max(minY, Math.min(wanted, maxY)) + root.restoringPageAnchor = false root.reportSoon() }) } diff --git a/crates/qbz-qt/src/local_state.rs b/crates/qbz-qt/src/local_state.rs index 6a86c15a5..f4f2eeb26 100644 --- a/crates/qbz-qt/src/local_state.rs +++ b/crates/qbz-qt/src/local_state.rs @@ -528,7 +528,12 @@ mod phase_a_tests { let qml = include_str!("../qml/views/local/LocalTracksTab.qml"); assert!(qml.contains("function capturePageAnchor()")); assert!(qml.contains("String(root.entries[i].row.id)")); - assert!(qml.contains("cell.mapToItem(list, 0, 0).y")); + assert!(qml.contains("cell.y - list.contentY")); + assert!(!qml.contains("cell.mapToItem(list")); + assert!(qml.contains("|| root.restoringPageAnchor")); + assert!(qml.contains( + "var wanted = list.contentY + currentScreenY - anchor.screenY" + )); assert!(qml.contains("function restorePageAnchor(anchor, nextEntries, epoch)")); assert!(qml.contains("Qt.callLater(function ()")); assert!(qml.contains("list.positionViewAtIndex(target, ListView.Beginning)"));