From 6e58682aa6286f352a560531f172335e10d8205a Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Mon, 13 Jul 2026 01:29:09 -0600 Subject: [PATCH] fix(playlist): reuse the shared drag ghost for custom-order reorder The bespoke reorder ghost read a row's absolute-position inside a ListView, which double-counts the row's own offset, so it only sat at the cursor for row 0 and flew off for the rest. Drive the app-wide DragState directly from the reorder gesture so the shared DragGhost (positioned in AppShell at the proven window coords) renders the reorder ghost; a new DragState.reorder flag keeps the sidebar from claiming it as an add-to-playlist drop and keeps the Rust DragActions path out of it. Removed the buggy local ghost. Reorder logic + persistence unchanged. (state.slint also carries the quality-badge fields for the next commit.) cc --- crates/qbz-ui/ui/playlist/PlaylistView.slint | 72 +++++++++----------- crates/qbz-ui/ui/primitives/TrackRow.slint | 10 ++- crates/qbz-ui/ui/shell/Sidebar.slint | 3 + crates/qbz-ui/ui/state.slint | 17 +++++ 4 files changed, 58 insertions(+), 44 deletions(-) diff --git a/crates/qbz-ui/ui/playlist/PlaylistView.slint b/crates/qbz-ui/ui/playlist/PlaylistView.slint index 9cc770f8c..d9006ea8b 100644 --- a/crates/qbz-ui/ui/playlist/PlaylistView.slint +++ b/crates/qbz-ui/ui/playlist/PlaylistView.slint @@ -338,8 +338,10 @@ export component PlaylistView inherits Rectangle { : @tr("Default"); // --- Custom-order drag-reorder state (issue #589) ------------------- - // Component-LOCAL, mirroring QueueSidebar's UP NEXT drag — NOT the - // DragState global (that one drives the track->sidebar-playlist ghost). + // Slot bookkeeping is component-LOCAL; the floating ghost REUSES the + // app-wide DragGhost via the DragState global (with DragState.reorder + // set, so sidebar rows skip drop-target detection). DragActions is + // never called, so the Rust add-to-playlist drop path never runs. // All indices are VISIBLE-list indices into PlaylistState.tracks. property reorder-active: false; property reorder-from: -1; // visible index of the dragged row @@ -349,8 +351,6 @@ export component PlaylistView inherits Rectangle { // list-top, the live viewport-y is folded back in below so the slot // math stays scroll-aware. property reorder-list-top; - // Live pointer window-Y during the drag (positions the floating ghost). - property reorder-pointer-y; // Pointer window-Y -> insertion slot [0..N]. Rows are a fixed 50px // stacked with 0 spacing; the +0.5 rounds boundaries to row centers @@ -361,6 +361,9 @@ export component PlaylistView inherits Rectangle { } function commit-reorder() { root.reorder-active = false; + // Release the shared ghost. + DragState.active = false; + DragState.reorder = false; // Fire only a real move. `to == from` and `to == from+1` both drop // back onto the same gap (no-op). if (root.reorder-over != root.reorder-from @@ -1027,17 +1030,32 @@ export component PlaylistView inherits Rectangle { // reports the gesture; this view owns the state. reorder-index: i; reorder-source: root.reorder-active && root.reorder-from == i; - reorder-begin(idx, row-top) => { + reorder-begin(idx, row-top, gx, gy) => { root.reorder-active = true; root.reorder-from = idx; root.reorder-over = idx; root.reorder-list-top = row-top - idx * 50px - track-list.viewport-y; - root.reorder-pointer-y = row-top; + // Reuse the app-wide DragGhost (proven + // window-coord positioning) for the floating + // pill; reorder=true keeps sidebar rows from + // claiming a drop target. Pointer first, then + // active, so the ghost never flashes at a + // stale position. + DragState.count = 1; + DragState.ghost-title = track.title; + DragState.ghost-subtitle = track.album != "" + ? track.artist + " · " + track.album + : track.artist; + DragState.pointer-x = gx; + DragState.pointer-y = gy; + DragState.reorder = true; + DragState.active = true; } - reorder-update(pointer-y) => { - root.reorder-over = root.slot-from-pointer(pointer-y); - root.reorder-pointer-y = pointer-y; + reorder-update(gx, gy) => { + root.reorder-over = root.slot-from-pointer(gy); + DragState.pointer-x = gx; + DragState.pointer-y = gy; // TODO: auto-scroll during drag when the pointer // nears the list's top/bottom edge (the drag // currently reorders within the visible viewport). @@ -1429,36 +1447,8 @@ export component PlaylistView inherits Rectangle { } } - // Floating drag ghost — a pill following the pointer with the dragged - // track's title (QueueSidebar parity). A free-positioned child of the - // view ROOT (NOT a layout child), declared last so it renders above the - // list. Converts the live pointer window-Y into this view's local space. - if root.reorder-active - && root.reorder-from >= 0 - && root.reorder-from < PlaylistState.tracks.length: Rectangle { - x: 44px; - y: root.reorder-pointer-y - root.absolute-position.y - self.height / 2; - width: Math.min(360px, root.width - 88px); - height: 34px; - background: Theme.surface-elevated; - border-radius: Radius.sm; - border-width: 1px; - border-color: Theme.accent; - opacity: 0.95; - HorizontalLayout { - padding-left: 10px; - padding-right: 10px; - VerticalLayout { - alignment: center; - horizontal-stretch: 1; - Text { - text: PlaylistState.tracks[root.reorder-from].title; - color: Theme.text-primary; - font-size: 12px; - font-weight: Typography.medium; - overflow: elide; - } - } - } - } + // The floating reorder ghost is the app-wide DragGhost (AppShell), + // driven through DragState by the reorder-begin/update handlers above — + // no component-local ghost (its absolute-position math double-counted + // the row offset inside the ListView and drifted off the cursor). } diff --git a/crates/qbz-ui/ui/primitives/TrackRow.slint b/crates/qbz-ui/ui/primitives/TrackRow.slint index 805ba1227..a914bd6b5 100644 --- a/crates/qbz-ui/ui/primitives/TrackRow.slint +++ b/crates/qbz-ui/ui/primitives/TrackRow.slint @@ -94,8 +94,9 @@ export component TrackRow inherits Rectangle { // playlists) — the handle has its OWN TouchArea. in property reorder-index; in property reorder-source: false; // this row is being dragged - callback reorder-begin(int /* visible index */, length /* row window-top */); - callback reorder-update(length /* pointer window-y */); + callback reorder-begin(int /* visible index */, length /* row window-top */, + length /* pointer window-x */, length /* pointer window-y */); + callback reorder-update(length /* pointer window-x */, length /* pointer window-y */); callback reorder-end(); // Whether the row can be dragged onto sidebar playlists. in property draggable: true; @@ -305,10 +306,13 @@ export component TrackRow inherits Rectangle { && Math.abs(self.absolute-position.y - root.y + self.mouse-y - self.down-y) > 6px) { self.dragging = true; root.reorder-begin(root.reorder-index, - root.absolute-position.y - root.y); + root.absolute-position.y - root.y, + self.absolute-position.x - root.x + self.mouse-x, + self.absolute-position.y - root.y + self.mouse-y); } if (self.dragging) { root.reorder-update( + self.absolute-position.x - root.x + self.mouse-x, self.absolute-position.y - root.y + self.mouse-y); } } diff --git a/crates/qbz-ui/ui/shell/Sidebar.slint b/crates/qbz-ui/ui/shell/Sidebar.slint index c5108b0f5..1d69db846 100644 --- a/crates/qbz-ui/ui/shell/Sidebar.slint +++ b/crates/qbz-ui/ui/shell/Sidebar.slint @@ -159,6 +159,9 @@ component SidebarRow inherits Rectangle { // to get the true window bounds (the drag pointer in DragState is // already in true window coords — TrackRow corrects it the same way). root.drop-hot = root.drag-on + // A within-list reorder drag (playlist custom order) reuses the + // DragState ghost but is NOT an add-to-playlist drop. + && !DragState.reorder && !root.is-folder && root.drag-px >= root.absolute-position.x - root.x && root.drag-px <= root.absolute-position.x - root.x + root.width diff --git a/crates/qbz-ui/ui/state.slint b/crates/qbz-ui/ui/state.slint index bcbb4adbb..373a8938c 100644 --- a/crates/qbz-ui/ui/state.slint +++ b/crates/qbz-ui/ui/state.slint @@ -968,6 +968,12 @@ export global DragState { in-out property pointer-y; // The sidebar playlist id currently under the pointer (drop target). in-out property over-playlist-id; + // WITHIN-LIST reorder drag (playlist custom order, issue #589): the + // gesture reuses this global purely for the floating ghost (DragGhost's + // window-coord positioning), driven from .slint only — DragActions is + // never called, so the Rust add-to-playlist drop path never runs. + // Sidebar rows skip drop-target detection while this is set. + in-out property reorder: false; } export global DragActions { @@ -4054,6 +4060,17 @@ export global NowPlayingState { in property sample-rate-hz: 0; in property bit-depth: 0; + // EFFECTIVE (delivered) stream quality — what the engine is actually + // decoding after the streaming-quality setting downgrade (#590 follow-up). + // The badge keeps showing the catalog MAX (quality-tier/detail above); + // when the delivered stream is below it, `quality-downgraded` turns on + // the AudioStamp's downgrade arrow and `quality-true-detail` carries the + // true "16-bit / 44.1 kHz" line for the hover tooltip. 0 / "" = unknown. + in property effective-sample-rate-hz: 0; + in property effective-bit-depth: 0; + in property quality-downgraded: false; + in property quality-true-detail; + // Output backend + mode LEDs (Mode C "Small" bar reads these). Mirror of // the same four on SettingsState (:2154-2157) — surfaced here so the // Small now-playing bar has ONE source for the song card + DAC/EXC