From 7730b07ac4dba20dbd799c2ed005d96e935a00e7 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Mon, 18 May 2026 13:50:11 -0600 Subject: [PATCH] feat(qbz-slint): replace section scroll with a paginated carousel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- crates/qbz-slint/ui/discover/Carousel.slint | 119 ++++++++++++++++++ crates/qbz-slint/ui/discover/HomeView.slint | 4 +- crates/qbz-slint/ui/discover/SectionRow.slint | 39 ------ 3 files changed, 121 insertions(+), 41 deletions(-) create mode 100644 crates/qbz-slint/ui/discover/Carousel.slint delete mode 100644 crates/qbz-slint/ui/discover/SectionRow.slint diff --git a/crates/qbz-slint/ui/discover/Carousel.slint b/crates/qbz-slint/ui/discover/Carousel.slint new file mode 100644 index 000000000..b4ffa8372 --- /dev/null +++ b/crates/qbz-slint/ui/discover/Carousel.slint @@ -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 glyph; + in property 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 section; + + property card-width: 220px; + property card-height: 266px; + property gap: 32px; + + property count: root.section.albums.length; + property per-page: Math.max( + 1, + Math.floor((viewport.width + root.gap) / (root.card-width + root.gap))); + property page-count: Math.max(1, Math.ceil(root.count / root.per-page)); + property page: 0; + // Self-correcting page index — stays valid when the window resizes. + property current-page: Math.min(Math.max(0, root.page), root.page-count - 1); + property step: root.per-page * (root.card-width + root.gap); + property 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; + } + } + } + } +} diff --git a/crates/qbz-slint/ui/discover/HomeView.slint b/crates/qbz-slint/ui/discover/HomeView.slint index fbc25b2ed..70672b25c 100644 --- a/crates/qbz-slint/ui/discover/HomeView.slint +++ b/crates/qbz-slint/ui/discover/HomeView.slint @@ -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; } } diff --git a/crates/qbz-slint/ui/discover/SectionRow.slint b/crates/qbz-slint/ui/discover/SectionRow.slint deleted file mode 100644 index fd76c8408..000000000 --- a/crates/qbz-slint/ui/discover/SectionRow.slint +++ /dev/null @@ -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 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; - } - } - } -}