feat(qbz-slint): replace section scroll with a paginated carousel

Discover sections now use a Carousel: a clipped, sliding track of
album cards with prev/next page controls, paginated by slice like the
Svelte DiscoverySection. With no ScrollView the row height is exactly
the card height — the horizontal scrollbar no longer injects vertical
space, so the earlier internal-scroll problem is resolved structurally
rather than patched.

per-page is derived from the viewport width and self-corrects on
resize. Replaces SectionRow.

cc
This commit is contained in:
vicrodh committed 2026-05-18 13:50:11 -06:00
1 parent 3871143bd3
commit 7730b07ac4
3 files changed
+121 -41

No files matched your search

+119
View File
@@ -0,0 +1,119 @@
// Discover section carousel — a title, prev/next page controls, and a
// clipped, sliding track of album cards.
//
// Pagination by slice (no scrollbar), mirroring the Svelte
// DiscoverySection. Because there is no ScrollView the height is exactly
// the card height — nothing injects extra vertical space.
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { DiscoverSection } from "../state.slint";
import { AlbumCard } from "AlbumCard.slint";
component NavButton inherits Rectangle {
in property <string> glyph;
in property <bool> enabled: true;
callback clicked();
width: 28px;
height: 28px;
border-radius: 14px;
opacity: root.enabled ? 1.0 : 0.4;
background: ta.has-hover && root.enabled
? Theme.surface-hover
: Theme.surface-elevated;
Text {
text: root.glyph;
color: root.enabled ? Theme.text-primary : Theme.text-muted;
font-size: 16px;
horizontal-alignment: center;
vertical-alignment: center;
}
ta := TouchArea {
enabled: root.enabled;
mouse-cursor: root.enabled ? pointer : default;
clicked => {
root.clicked();
}
}
}
export component Carousel inherits VerticalLayout {
in property <DiscoverSection> section;
property <length> card-width: 220px;
property <length> card-height: 266px;
property <length> gap: 32px;
property <int> count: root.section.albums.length;
property <int> per-page: Math.max(
1,
Math.floor((viewport.width + root.gap) / (root.card-width + root.gap)));
property <int> page-count: Math.max(1, Math.ceil(root.count / root.per-page));
property <int> page: 0;
// Self-correcting page index — stays valid when the window resizes.
property <int> current-page: Math.min(Math.max(0, root.page), root.page-count - 1);
property <length> step: root.per-page * (root.card-width + root.gap);
property <length> content-width:
root.count * root.card-width + Math.max(0, root.count - 1) * root.gap;
spacing: 12px;
// --- Header: title + page controls -----------------------------------
HorizontalLayout {
Text {
text: root.section.title;
color: Theme.text-primary;
font-size: 18px;
font-weight: Typography.semibold;
vertical-alignment: center;
horizontal-stretch: 1;
}
HorizontalLayout {
spacing: 4px;
VerticalLayout {
alignment: center;
NavButton {
glyph: "‹";
enabled: root.current-page > 0;
clicked => {
root.page = root.current-page - 1;
}
}
}
VerticalLayout {
alignment: center;
NavButton {
glyph: "›";
enabled: root.current-page < root.page-count - 1;
clicked => {
root.page = root.current-page + 1;
}
}
}
}
}
// --- Track: clipped viewport, slides by page -------------------------
viewport := Rectangle {
height: root.card-height;
clip: true;
Rectangle {
x: -root.current-page * root.step;
y: 0px;
width: root.content-width;
height: root.card-height;
animate x { duration: 220ms; easing: ease-in-out; }
HorizontalLayout {
spacing: root.gap;
for album in root.section.albums: AlbumCard {
album: album;
}
}
}
}
}
+2 -2
View File
@@ -7,7 +7,7 @@
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { HomeState } from "../state.slint";
import { SectionRow } from "SectionRow.slint";
import { Carousel } from "Carousel.slint";
import { ScrollView } from "std-widgets.slint";
component Tab inherits Rectangle {
@@ -70,7 +70,7 @@ export component HomeView inherits Rectangle {
font-size: 14px;
}
for section in HomeState.sections: SectionRow {
for section in HomeState.sections: Carousel {
section: section;
}
}
@@ -1,39 +0,0 @@
// One Discover section — a title and a horizontally scrollable row of
// album cards.
//
// The Svelte original paginates by slice; the MVP uses a horizontal
// scroll, which is simpler and behaviorally equivalent for browsing.
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { DiscoverSection } from "../state.slint";
import { AlbumCard } from "AlbumCard.slint";
import { ScrollView } from "std-widgets.slint";
export component SectionRow inherits VerticalLayout {
in property <DiscoverSection> section;
spacing: 12px;
Text {
text: root.section.title;
color: Theme.text-primary;
font-size: 18px;
font-weight: Typography.semibold;
}
// Pinned to the AlbumCard height: viewport-height equals height, so
// the vertical scroll range is zero — the row cannot scroll vertically
// (visibly or not). Only horizontal scrolling remains.
ScrollView {
height: 266px;
viewport-height: 266px;
vertical-scrollbar-policy: ScrollBarPolicy.always-off;
HorizontalLayout {
spacing: 32px;
for album in root.section.albums: AlbumCard {
album: album;
}
}
}
}