fix(ui): finish pending Qt interaction polish

Keep segmented controls within their visual well, use the active theme for playback indicators, disconnect inactive pulse receivers and improve near-fit song metadata.

cx
This commit is contained in:
vicrodh committed 2026-08-25 18:25:05 -06:00
1 parent 52fd887f15
commit 1bd99aed67
8 files changed
+55 -23

No files matched your search

+11 -8
View File
@@ -18,13 +18,11 @@
// views/local/LocalFolderDetail.qml (grid/list). The old
// views/local/LocalSegToggle.qml is GONE — this file is the only copy.
//
// SIZING. `width` is a plain assignment, not a binding on the content, so a
// call site with more than two segments MUST widen it: with the DEFAULT metrics
// the segments are a centred Row of 26px cells with 2px gaps, so N segments
// need 28N - 2 px (2 -> 54, fits the 60 default; 3 -> 82, hence the detail
// view's 90). Too narrow does not wrap or elide — `clip: true` silently cuts
// the last segment off, and a segment you cannot see is a segment you cannot
// click.
// SIZING. The well derives its width from the real segment row. With the
// DEFAULT metrics that is 26px cells, 2px gaps and 3px of well on each side:
// 2 segments -> 60px, 3 -> 88px. A caller may still override `width` for a
// roomier reference metric, but adding a segment can no longer leave its
// button visibly outside a stale 60px background.
//
// ── TWO METRIC SETS, AND WHY THE CELL LOOK IS PARAMETERISED ────────────────
// The DEFAULTS below are the Local Library set, unchanged: 26x24 r4 cells with
@@ -92,7 +90,12 @@ Rectangle {
QbzTheme { id: theme }
width: 60
readonly property int contentWidth: root.segments.length > 0
? root.segments.length * root.segWidth
+ (root.segments.length - 1) * root.segSpacing
: 0
implicitWidth: Math.max(60, root.contentWidth + 6)
width: implicitWidth
height: 30
radius: 6
// The well itself: surface-elevated @ 0.5 under the dynamic background
@@ -206,7 +206,6 @@ Item {
// Track info (shared, homologated across the panels, :173-176).
ImmersiveTrackMeta {
width: root.width
equalizerTint: "#7c3aed"
}
}
}
@@ -354,7 +354,6 @@ Item {
// fan + metadata read as one balanced group (:276-283).
ImmersiveTrackMeta {
width: root.width
equalizerTint: "#7c3aed"
}
}
}
+13 -7
View File
@@ -25,12 +25,15 @@
import QtQuick
import com.blitzfc.qbz
import "../theme"
Item {
id: root
/// Bar color — every immersive mount passes #7c3aed.
property color tint: "#7c3aed"
QbzTheme { id: theme }
/// Bar color follows the active theme unless a host deliberately overrides it.
property color tint: theme.accent
width: 18
height: 14
@@ -43,8 +46,12 @@ Item {
// (180 Hz on the owner's panel) and every update is a whole-window
// present. The wave's period is 800 ms, so the 30 Hz pulse samples it
// ~24 times per loop — indistinguishable. Riding the pulse also means
// ZERO extra presents: the bars move in the same frame as everything
// else the pulse moves.
// ZERO extra clocks: the bars move in the same frame as everything else
// the pulse moves. A pulse receiver still has CPU cost, though, so an
// inactive indicator disconnects structurally instead of waking a JS
// handler just to return. TrackRow also loads this object only for the
// one active, visible play cell; a long expanded box set therefore owns
// one receiver rather than one receiver per track.
//
// The `active` gate belongs to the HOST, because this component mounts
// in always-alive-but-invisible trees (the immersive overlay) where a
@@ -58,10 +65,9 @@ Item {
property real t: 0
readonly property int _tickMs: 33
Connections {
target: QbzShell
target: root.active && root.visible ? QbzShell : null
function onPulseMsChanged() {
if (root.active && root.visible)
root.t = (root.t + root._tickMs / 800.0) % 1.0
root.t = (root.t + root._tickMs / 800.0) % 1.0
}
}
@@ -20,12 +20,15 @@
import QtQuick
import com.blitzfc.qbz
import "../controls"
import "../theme"
Column {
id: root
/// Equalizer tint + "Now Playing" label color (Slint equalizer-tint).
property color equalizerTint: "#7c3aed"
QbzTheme { id: theme }
/// Equalizer tint + "Now Playing" label follow the active theme.
property color equalizerTint: theme.accent
spacing: 6
@@ -541,7 +541,6 @@ Item {
ImmersiveTrackMeta {
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 44
equalizerTint: "#7c3aed"
}
}
}
@@ -116,7 +116,6 @@ Item {
// Track info (shared, homologated across the panels, :106-109).
ImmersiveTrackMeta {
width: root.width
equalizerTint: "#7c3aed"
}
}
}
+26 -2
View File
@@ -245,6 +245,11 @@ Rectangle {
font: albumText.font
text: albumText.text
}
TextMetrics {
id: ellipsisMetrics
font: artistText.font
text: "…"
}
// Fixed furniture: the context box (+ its 7px gap when shown)
// and the separator dot with a 7px gap on each side.
@@ -255,8 +260,15 @@ Rectangle {
// a long album elides before the artist does, and both keep a
// readable slice instead of one pushing the other off the card.
readonly property var budget: {
var a = artistMetrics.width
var b = albumMetrics.width
// `width` is the painted bounding box; `advanceWidth` is
// what the line layout actually consumes. Budget the
// larger one plus a 2px bearing/rounding guard — assigning
// the exact bounding width made "Metallica" render as
// "Metalli…" despite spare room.
var a = Math.ceil(Math.max(artistMetrics.width,
artistMetrics.advanceWidth)) + 2
var b = Math.ceil(Math.max(albumMetrics.width,
albumMetrics.advanceWidth)) + 2
var av = metaRow.avail
if (a + b <= av)
return [a, b]
@@ -269,6 +281,18 @@ Rectangle {
qb = b
qa = av - b
}
// An ellipsis costs roughly the same runway as the final
// one or two glyphs. If a label is within that distance of
// fitting, finish the word and take the small deficit from
// the other, genuinely long label instead.
var near = Math.ceil(ellipsisMetrics.advanceWidth) + 3
if (a > qa && a - qa <= near && av >= a) {
qa = a
qb = av - a
} else if (b > qb && b - qb <= near && av >= b) {
qb = b
qa = av - b
}
return [Math.floor(qa), Math.floor(qb)]
}