From 235c61bf802583c6cdf2f0375aef417e3975c59b Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Wed, 20 May 2026 22:23:53 -0600 Subject: [PATCH] feat(slint): drag-to-scroll the slim grid carousels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applies the same Flickable drag pattern to SlimCarousel (Recently Played Tracks, Popular albums). The 4x3 page blocks now live inside a horizontal Flickable — each block is one viewport width wide, cards positioned absolutely within it — so the grid drags between pages with kinetic momentum while slim-card taps still register. prev/next nudge viewport-x by a page width; enabled state derives from viewport-x. Two-page (24-item) cap unchanged. cc --- .../qbz-slint/ui/discover/SlimCarousel.slint | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/crates/qbz-slint/ui/discover/SlimCarousel.slint b/crates/qbz-slint/ui/discover/SlimCarousel.slint index eb351898a..736dbe72f 100644 --- a/crates/qbz-slint/ui/discover/SlimCarousel.slint +++ b/crates/qbz-slint/ui/discover/SlimCarousel.slint @@ -56,9 +56,8 @@ export component SlimCarousel inherits VerticalLayout { // Cap at two pages (24 items); excess is not shown. property total: Math.min(root.items.length, 2 * root.per-page); property page-count: Math.max(1, Math.ceil(root.total / root.per-page)); - property page: 0; - // Self-correcting page index — stays valid if the window resizes. - property current-page: Math.min(Math.max(0, root.page), root.page-count - 1); + // Max leftward scroll (viewport-x is 0 at start, negative scrolled). + property max-scroll: Math.max(0px, (root.page-count - 1) * flick.width); spacing: 12px; @@ -79,9 +78,9 @@ export component SlimCarousel inherits VerticalLayout { alignment: center; NavButton { icon: @image-url("../assets/icons/chevron-left.svg"); - enabled: root.current-page > 0; + enabled: flick.viewport-x < -1px; clicked => { - root.page = root.current-page - 1; + flick.viewport-x = Math.min(0px, flick.viewport-x + flick.width); } } } @@ -89,28 +88,31 @@ export component SlimCarousel inherits VerticalLayout { alignment: center; NavButton { icon: @image-url("../assets/icons/chevron-right.svg"); - enabled: root.current-page < root.page-count - 1; + enabled: flick.viewport-x > -root.max-scroll + 1px; clicked => { - root.page = root.current-page + 1; + flick.viewport-x = Math.max(-root.max-scroll, flick.viewport-x - flick.width); } } } } } - // --- Track: clipped viewport, slides one page width at a time -------- + // --- Track: a horizontal Flickable so the grid drags by page like + // the album carousel. Each page block is one viewport width wide; the + // 4x3 cards are positioned absolutely inside their page block. Card + // taps still register (Flickable separates flick from tap). viewport := Rectangle { height: root.rows * root.row-height + (root.rows - 1) * root.gap; clip: true; - property cell-width: - (self.width - (root.columns - 1) * root.gap) / root.columns; - - Rectangle { - x: -root.current-page * viewport.width; - width: root.page-count * viewport.width; + flick := Flickable { + width: 100%; height: parent.height; - animate x { duration: 220ms; easing: ease-in-out; } + viewport-width: root.page-count * self.width; + viewport-height: self.height; + + property cell-width: + (self.width - (root.columns - 1) * root.gap) / root.columns; for item[i] in root.items: SlimCard { // Only the first `total` items are laid out; the rest @@ -121,10 +123,10 @@ export component SlimCarousel inherits VerticalLayout { property slot-col: Math.mod(self.slot, root.columns); property slot-row: Math.floor(self.slot / root.columns); visible: self.shown; - width: self.shown ? viewport.cell-width : 0px; + width: self.shown ? flick.cell-width : 0px; height: root.row-height; - x: self.page-idx * viewport.width - + self.slot-col * (viewport.cell-width + root.gap); + x: self.page-idx * flick.width + + self.slot-col * (flick.cell-width + root.gap); y: self.slot-row * (root.row-height + root.gap); item: item; clicked(id) => {