feat(shell): resizable playlist sidebar and queue/lyrics column (#771)
Both panels were fixed-width (240 / 300); playlist names elided on wide monitors. Each panel's visible edge (the content pane's edge, 8px past the panel — the bezel between them shares the panel colour) now carries a 5px grab strip with a 2x28 notch, split cursor, and a one-shot accent pulse when a drag lands on a snap point. Rules live in `panel_resize.rs` (pure, unit-tested), the bridge applies them live and persists on release (`ui_prefs` `sidebar_width` / `queue_panel_width`, clamped on read): - sidebar: 240..480 while open; below 200 it snaps to the mini rail, below 24 it closes through the same state the header button uses (with the Large-NPB fallback); the mini rail reopens at 220 — a 20px hysteresis band so no pointer x can flip open<->mini on every move (the flapping seen in the 2026-09-11 screencast). The stored width never drops below 240, so reopening restores the last open width. A state snap during a drag still animates: the bridge raises `sidebarSnapping` before the state change and Sidebar's width Behavior honours it while the drag otherwise disables it. - column: 300..600; below 260 it closes queue and lyrics as their X would. With the queue in the column and the width parked at 600, the notch shows a steady larger glow toward the content; pushing 60px further opens the Listen List (queue-view) and the queue leaves the column, lyrics staying if open. A lyrics-only column never pushes through. The Large-NPB dock is untouched: it is mounted with absolute x/width in AppShell and reads nothing from the sidebar width. Verified: panel_resize 9/9, qmllint + the six qt-run.sh audits, release builds v1-v4, four owner smoke rounds. cc
This commit is contained in:
8 files changed
+536
-2
No files matched your search
@@ -264,6 +264,7 @@ Rectangle {
|
||||
// AppShell level rather than inside Sidebar.slint.
|
||||
tooltip: tooltipOverlay
|
||||
// Animated 3-state width lives inside the component.
|
||||
resizing: sidebarHandle.active
|
||||
}
|
||||
|
||||
// QueueView temporarily takes the queue drawer's visual slot without
|
||||
@@ -282,12 +283,14 @@ Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.top: header.bottom
|
||||
anchors.bottom: npb.top
|
||||
// #771: the open width is the user's (300..600, persisted).
|
||||
width: (root.queueSidebarVisible || QbzShell.lyricsOpen)
|
||||
? theme.queuePanelWidth : 0
|
||||
? QbzShell.queuePanelWidth : 0
|
||||
clip: true
|
||||
color: root.ambientOn ? theme.surfaceCardA50 : theme.surfaceCard
|
||||
|
||||
Behavior on width {
|
||||
enabled: !queueHandle.active
|
||||
NumberAnimation { duration: 160; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
|
||||
@@ -325,6 +328,70 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// --- #771 resize handles ------------------------------------------------
|
||||
// Siblings of the panels, not children: Sidebar clips and the column
|
||||
// animates, and a handle inside either would be clipped or dragged along.
|
||||
// Each sits on the border the user SEES — the content PANE's edge, 8px
|
||||
// past the panel (the bezel between them is panel-coloured, so the
|
||||
// panel's own edge is invisible) — nudged 1-2px toward its panel, above
|
||||
// the content frame in z. The bridge (`panel_resize.rs`) decides
|
||||
// clamp/snap/close; these only report the pointer, and pulse their notch
|
||||
// when a drag lands on a snap point.
|
||||
PanelResizeHandle {
|
||||
id: sidebarHandle
|
||||
// No edge to grab once the sidebar is closed; the header button
|
||||
// reopens it, as before.
|
||||
visible: QbzShell.sidebarState !== 2
|
||||
// Pane edge at sidebar.width + 8; strip 245..249 at 240, centre 247.
|
||||
x: sidebar.width + 5
|
||||
anchors.top: header.bottom
|
||||
anchors.bottom: npb.top
|
||||
z: 5
|
||||
onDragged: function (px) { QbzShell.sidebarDrag(px) }
|
||||
onReleased: QbzShell.sidebarDragEnd()
|
||||
}
|
||||
PanelResizeHandle {
|
||||
id: queueHandle
|
||||
visible: queueColumn.width > 0
|
||||
// Pane edge at queueColumn.x - 8; strip x-9..x-4, centre x-6.5.
|
||||
x: queueColumn.x - 9
|
||||
anchors.top: header.bottom
|
||||
anchors.bottom: npb.top
|
||||
z: 5
|
||||
// The column hangs off the window's RIGHT edge: a pointer at x means
|
||||
// a width of (window width - x).
|
||||
onDragged: function (px) { QbzShell.queuePanelDrag(root.width - px) }
|
||||
onReleased: QbzShell.queuePanelDragEnd()
|
||||
// Parked at the maximum with the QUEUE in the column: there is more
|
||||
// past here (push through -> Listen List). Lyrics-only: nothing.
|
||||
beyond: active && root.queueSidebarVisible
|
||||
&& QbzShell.queuePanelWidth === theme.queuePanelWidth * 2
|
||||
beyondSide: -1
|
||||
}
|
||||
// Snap feedback: the bridge clamps and snaps; a width landing EXACTLY on
|
||||
// a range end, or a state snap, while the pointer is down is the moment
|
||||
// the notch pulses. A drag inside the range changes the width on every
|
||||
// move and never hits these equalities twice in a row.
|
||||
Connections {
|
||||
target: QbzShell
|
||||
function onSidebarWidthChanged() {
|
||||
if (sidebarHandle.active
|
||||
&& (QbzShell.sidebarWidth === theme.sidebarOpenWidth
|
||||
|| QbzShell.sidebarWidth === theme.sidebarOpenWidth * 2))
|
||||
sidebarHandle.pulse()
|
||||
}
|
||||
function onSidebarStateChanged() {
|
||||
if (sidebarHandle.active)
|
||||
sidebarHandle.pulse()
|
||||
}
|
||||
function onQueuePanelWidthChanged() {
|
||||
if (queueHandle.active
|
||||
&& (QbzShell.queuePanelWidth === theme.queuePanelWidth
|
||||
|| QbzShell.queuePanelWidth === theme.queuePanelWidth * 2))
|
||||
queueHandle.pulse()
|
||||
}
|
||||
}
|
||||
|
||||
// Content FRAME — the chrome slab the content pane is inset INTO
|
||||
// (AppShell.slint:357-362). It is the same tier as the sidebar, the header
|
||||
// and the player bar, and under the dynamic background it takes the same
|
||||
|
||||
@@ -486,9 +486,14 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// #771: the open width is the user's (240..480, persisted); the other
|
||||
// two states keep their fixed rails.
|
||||
width: QbzShell.sidebarState === 2 ? 0
|
||||
: QbzShell.sidebarState === 1 ? theme.sidebarMiniWidth
|
||||
: theme.sidebarOpenWidth
|
||||
: QbzShell.sidebarWidth
|
||||
/// Set by AppShell while its resize handle is pressed: the width follows
|
||||
/// the pointer directly, the state snaps still animate.
|
||||
property bool resizing: false
|
||||
// surface-card @ 0.5 while the ambient background is active (phase 14).
|
||||
color: ambientOn ? theme.surfaceCardA50 : theme.surfaceCard
|
||||
readonly property bool ambientOn: theme.ambientOn
|
||||
@@ -497,7 +502,12 @@ Rectangle {
|
||||
// animates (same as the Slint root).
|
||||
clip: true
|
||||
|
||||
// While the pointer drives the width the Behavior is off (it would lag
|
||||
// the mouse); a STATE snap during that drag (open <-> mini <-> closed)
|
||||
// still animates, because the bridge raises `sidebarSnapping` right
|
||||
// before it changes the state and lowers it on the next pointer-follow.
|
||||
Behavior on width {
|
||||
enabled: !root.resizing || QbzShell.sidebarSnapping
|
||||
NumberAnimation { duration: 160; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user