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
This commit is contained in:
vicrodh committed 2026-07-13 01:29:09 -06:00
1 parent 4210ec088a
commit 6e58682aa6
4 files changed
+58 -44

No files matched your search

+31 -41
View File
@@ -338,8 +338,10 @@ export component PlaylistView inherits Rectangle {
: @tr("Default"); : @tr("Default");
// --- Custom-order drag-reorder state (issue #589) ------------------- // --- Custom-order drag-reorder state (issue #589) -------------------
// Component-LOCAL, mirroring QueueSidebar's UP NEXT drag — NOT the // Slot bookkeeping is component-LOCAL; the floating ghost REUSES the
// DragState global (that one drives the track->sidebar-playlist ghost). // 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. // All indices are VISIBLE-list indices into PlaylistState.tracks.
property <bool> reorder-active: false; property <bool> reorder-active: false;
property <int> reorder-from: -1; // visible index of the dragged row property <int> 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 // list-top, the live viewport-y is folded back in below so the slot
// math stays scroll-aware. // math stays scroll-aware.
property <length> reorder-list-top; property <length> reorder-list-top;
// Live pointer window-Y during the drag (positions the floating ghost).
property <length> reorder-pointer-y;
// Pointer window-Y -> insertion slot [0..N]. Rows are a fixed 50px // Pointer window-Y -> insertion slot [0..N]. Rows are a fixed 50px
// stacked with 0 spacing; the +0.5 rounds boundaries to row centers // stacked with 0 spacing; the +0.5 rounds boundaries to row centers
@@ -361,6 +361,9 @@ export component PlaylistView inherits Rectangle {
} }
function commit-reorder() { function commit-reorder() {
root.reorder-active = false; 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 // Fire only a real move. `to == from` and `to == from+1` both drop
// back onto the same gap (no-op). // back onto the same gap (no-op).
if (root.reorder-over != root.reorder-from if (root.reorder-over != root.reorder-from
@@ -1027,17 +1030,32 @@ export component PlaylistView inherits Rectangle {
// reports the gesture; this view owns the state. // reports the gesture; this view owns the state.
reorder-index: i; reorder-index: i;
reorder-source: root.reorder-active && root.reorder-from == 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-active = true;
root.reorder-from = idx; root.reorder-from = idx;
root.reorder-over = idx; root.reorder-over = idx;
root.reorder-list-top = root.reorder-list-top =
row-top - idx * 50px - track-list.viewport-y; 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) => { reorder-update(gx, gy) => {
root.reorder-over = root.slot-from-pointer(pointer-y); root.reorder-over = root.slot-from-pointer(gy);
root.reorder-pointer-y = pointer-y; DragState.pointer-x = gx;
DragState.pointer-y = gy;
// TODO: auto-scroll during drag when the pointer // TODO: auto-scroll during drag when the pointer
// nears the list's top/bottom edge (the drag // nears the list's top/bottom edge (the drag
// currently reorders within the visible viewport). // 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 // The floating reorder ghost is the app-wide DragGhost (AppShell),
// track's title (QueueSidebar parity). A free-positioned child of the // driven through DragState by the reorder-begin/update handlers above —
// view ROOT (NOT a layout child), declared last so it renders above the // no component-local ghost (its absolute-position math double-counted
// list. Converts the live pointer window-Y into this view's local space. // the row offset inside the ListView and drifted off the cursor).
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;
}
}
}
}
} }
+7 -3
View File
@@ -94,8 +94,9 @@ export component TrackRow inherits Rectangle {
// playlists) — the handle has its OWN TouchArea. // playlists) — the handle has its OWN TouchArea.
in property <int> reorder-index; in property <int> reorder-index;
in property <bool> reorder-source: false; // this row is being dragged in property <bool> reorder-source: false; // this row is being dragged
callback reorder-begin(int /* visible index */, length /* row window-top */); callback reorder-begin(int /* visible index */, length /* row window-top */,
callback reorder-update(length /* pointer window-y */); length /* pointer window-x */, length /* pointer window-y */);
callback reorder-update(length /* pointer window-x */, length /* pointer window-y */);
callback reorder-end(); callback reorder-end();
// Whether the row can be dragged onto sidebar playlists. // Whether the row can be dragged onto sidebar playlists.
in property <bool> draggable: true; in property <bool> 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) { && Math.abs(self.absolute-position.y - root.y + self.mouse-y - self.down-y) > 6px) {
self.dragging = true; self.dragging = true;
root.reorder-begin(root.reorder-index, 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) { if (self.dragging) {
root.reorder-update( root.reorder-update(
self.absolute-position.x - root.x + self.mouse-x,
self.absolute-position.y - root.y + self.mouse-y); self.absolute-position.y - root.y + self.mouse-y);
} }
} }
+3
View File
@@ -159,6 +159,9 @@ component SidebarRow inherits Rectangle {
// to get the true window bounds (the drag pointer in DragState is // to get the true window bounds (the drag pointer in DragState is
// already in true window coords — TrackRow corrects it the same way). // already in true window coords — TrackRow corrects it the same way).
root.drop-hot = root.drag-on 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.is-folder
&& root.drag-px >= root.absolute-position.x - root.x && root.drag-px >= root.absolute-position.x - root.x
&& root.drag-px <= root.absolute-position.x - root.x + root.width && root.drag-px <= root.absolute-position.x - root.x + root.width
+17
View File
@@ -968,6 +968,12 @@ export global DragState {
in-out property <length> pointer-y; in-out property <length> pointer-y;
// The sidebar playlist id currently under the pointer (drop target). // The sidebar playlist id currently under the pointer (drop target).
in-out property <string> over-playlist-id; in-out property <string> 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 <bool> reorder: false;
} }
export global DragActions { export global DragActions {
@@ -4054,6 +4060,17 @@ export global NowPlayingState {
in property <int> sample-rate-hz: 0; in property <int> sample-rate-hz: 0;
in property <int> bit-depth: 0; in property <int> 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 <int> effective-sample-rate-hz: 0;
in property <int> effective-bit-depth: 0;
in property <bool> quality-downgraded: false;
in property <string> quality-true-detail;
// Output backend + mode LEDs (Mode C "Small" bar reads these). Mirror of // Output backend + mode LEDs (Mode C "Small" bar reads these). Mirror of
// the same four on SettingsState (:2154-2157) — surfaced here so the // the same four on SettingsState (:2154-2157) — surfaced here so the
// Small now-playing bar has ONE source for the song card + DAC/EXC // Small now-playing bar has ONE source for the song card + DAC/EXC