feat(slint): scroll-position restore on back/forward (Local Library)

Scroll-position restore was never implemented (only a TODO in nav.rs);
back/forward tracked which page was visited, not where it was scrolled.

Add per-history-entry scroll memory: the mounted scroll container reports
its live viewport-y via NavState.report-scroll, so every navigation (fresh
record + back/forward) stamps the outgoing entry without touching the ~30
record call sites. go_back/go_forward hand the saved scroll back to the
shell, which arms NavState.restore-scope + scroll-restore before switching
the view. A container restores only when its scope matches, once per mount,
and only after content has laid out (viewport overflows) to dodge the
clamp-to-0 on async data load; it clears the scope once consumed.

Wired the four Local Library lists (albums, tracks, folders, artists rail).
Tracks is paginated, so its restore is best-effort within loaded pages.

cc
This commit is contained in:
vicrodh committed 2026-06-08 15:38:35 -06:00
1 parent b37c29af14
commit 641aa22c17
4 files changed
+243 -36

No files matched your search

@@ -18,7 +18,7 @@
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { LocalLibraryState, LocalLibraryActions, FolderNode, FolderSubcardItem, LocalArtistItem, AlphaJump, LibAlbumFilterState, AppearanceState } from "../state.slint";
import { LocalLibraryState, LocalLibraryActions, FolderNode, FolderSubcardItem, LocalArtistItem, AlphaJump, LibAlbumFilterState, AppearanceState, NavState } from "../state.slint";
import { NavButtons } from "../shell/NavButtons.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { CircleAction } from "../primitives/CircleAction.slint";
@@ -1163,6 +1163,21 @@ export component LocalLibraryView inherits Rectangle {
albums-flick := Flickable {
viewport-width: self.width;
viewport-height: albums-page.preferred-height;
// Scroll-position restore on back/forward (nav::scope_for → "ll:albums").
property <bool> restored: false;
function restore-scroll() {
if (!self.restored && NavState.restore-scope == "ll:albums"
&& self.viewport-height > self.height) {
self.viewport-y = NavState.scroll-restore;
self.restored = true;
// Consume the arm so a later fresh re-mount of this
// tab does not replay a stale position.
NavState.restore-scope = "";
}
}
init => { self.restore-scroll(); }
changed viewport-height => { self.restore-scroll(); }
changed viewport-y => { NavState.report-scroll(self.viewport-y); }
albums-page := VerticalLayout {
width: parent.width;
padding-left: 32px;
@@ -1325,7 +1340,22 @@ export component LocalLibraryView inherits Rectangle {
tracks-list := ListView {
vertical-stretch: 1;
vertical-scrollbar-policy: ScrollBarPolicy.always-off;
// Scroll-position restore on back/forward (nav::scope_for → "ll:tracks").
// Best-effort on this paginated list: restores when the
// saved position falls within already-loaded pages.
property <bool> restored: false;
function restore-scroll() {
if (!self.restored && NavState.restore-scope == "ll:tracks"
&& self.viewport-height > self.height) {
self.viewport-y = NavState.scroll-restore;
self.restored = true;
NavState.restore-scope = "";
}
}
init => { self.restore-scroll(); }
changed viewport-height => { self.restore-scroll(); }
changed viewport-y => {
NavState.report-scroll(self.viewport-y);
if (-self.viewport-y + self.visible-height >= self.viewport-height - 600px
&& LocalLibraryState.tracks-has-more
&& !LocalLibraryState.tracks-loading-more
@@ -1462,6 +1492,19 @@ export component LocalLibraryView inherits Rectangle {
folders-flick := Flickable {
viewport-width: self.width;
viewport-height: folders-page.preferred-height;
// Scroll-position restore on back/forward (nav::scope_for → "ll:folders").
property <bool> restored: false;
function restore-scroll() {
if (!self.restored && NavState.restore-scope == "ll:folders"
&& self.viewport-height > self.height) {
self.viewport-y = NavState.scroll-restore;
self.restored = true;
NavState.restore-scope = "";
}
}
init => { self.restore-scroll(); }
changed viewport-height => { self.restore-scroll(); }
changed viewport-y => { NavState.report-scroll(self.viewport-y); }
folders-page := VerticalLayout {
width: parent.width;
padding-left: 32px;
@@ -2114,6 +2157,19 @@ export component LocalLibraryView inherits Rectangle {
rail-flick := Flickable {
viewport-width: self.width;
viewport-height: rail-col.preferred-height;
// Scroll-position restore on back/forward (nav::scope_for → "ll:artists").
property <bool> restored: false;
function restore-scroll() {
if (!self.restored && NavState.restore-scope == "ll:artists"
&& self.viewport-height > self.height) {
self.viewport-y = NavState.scroll-restore;
self.restored = true;
NavState.restore-scope = "";
}
}
init => { self.restore-scroll(); }
changed viewport-height => { self.restore-scroll(); }
changed viewport-y => { NavState.report-scroll(self.viewport-y); }
rail-col := VerticalLayout {
width: parent.width;
padding-right: 22px;
+13
View File
@@ -1692,6 +1692,19 @@ export global NavState {
callback request-back();
callback request-forward();
callback request-settings();
// ---- Scroll-position restore (browser-like, per history entry) ----
// The mounted scroll container reports its live viewport-y here on every
// `changed viewport-y`; the Rust nav module stamps it onto the outgoing
// entry so back/forward can put it back.
callback report-scroll(length /* viewport-y */);
// Armed by Rust right before a back/forward view switch: `restore-scope`
// names which scroll container should restore (a stable per-list id, see
// nav::scope_for), and `scroll-restore` is the viewport-y to apply. A
// container restores only when its own scope matches, exactly once per
// mount, and only after its content has laid out (viewport overflows).
in-out property <string> restore-scope: "";
in-out property <length> scroll-restore: 0;
}
// Artist detail screen state.