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/LocalFolderDetail.qml (grid/list). The old
// views/local/LocalSegToggle.qml is GONE — this file is the only copy. // 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 // SIZING. The well derives its width from the real segment row. With the
// call site with more than two segments MUST widen it: with the DEFAULT metrics // DEFAULT metrics that is 26px cells, 2px gaps and 3px of well on each side:
// the segments are a centred Row of 26px cells with 2px gaps, so N segments // 2 segments -> 60px, 3 -> 88px. A caller may still override `width` for a
// need 28N - 2 px (2 -> 54, fits the 60 default; 3 -> 82, hence the detail // roomier reference metric, but adding a segment can no longer leave its
// view's 90). Too narrow does not wrap or elide — `clip: true` silently cuts // button visibly outside a stale 60px background.
// the last segment off, and a segment you cannot see is a segment you cannot
// click.
// //
// ── TWO METRIC SETS, AND WHY THE CELL LOOK IS PARAMETERISED ──────────────── // ── TWO METRIC SETS, AND WHY THE CELL LOOK IS PARAMETERISED ────────────────
// The DEFAULTS below are the Local Library set, unchanged: 26x24 r4 cells with // The DEFAULTS below are the Local Library set, unchanged: 26x24 r4 cells with
@@ -92,7 +90,12 @@ Rectangle {
QbzTheme { id: theme } 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 height: 30
radius: 6 radius: 6
// The well itself: surface-elevated @ 0.5 under the dynamic background // 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). // Track info (shared, homologated across the panels, :173-176).
ImmersiveTrackMeta { ImmersiveTrackMeta {
width: root.width width: root.width
equalizerTint: "#7c3aed"
} }
} }
} }
@@ -354,7 +354,6 @@ Item {
// fan + metadata read as one balanced group (:276-283). // fan + metadata read as one balanced group (:276-283).
ImmersiveTrackMeta { ImmersiveTrackMeta {
width: root.width width: root.width
equalizerTint: "#7c3aed"
} }
} }
} }
+13 -7
View File
@@ -25,12 +25,15 @@
import QtQuick import QtQuick
import com.blitzfc.qbz import com.blitzfc.qbz
import "../theme"
Item { Item {
id: root id: root
/// Bar color — every immersive mount passes #7c3aed. QbzTheme { id: theme }
property color tint: "#7c3aed"
/// Bar color follows the active theme unless a host deliberately overrides it.
property color tint: theme.accent
width: 18 width: 18
height: 14 height: 14
@@ -43,8 +46,12 @@ Item {
// (180 Hz on the owner's panel) and every update is a whole-window // (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 // 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 // ~24 times per loop — indistinguishable. Riding the pulse also means
// ZERO extra presents: the bars move in the same frame as everything // ZERO extra clocks: the bars move in the same frame as everything else
// else the pulse moves. // 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 // The `active` gate belongs to the HOST, because this component mounts
// in always-alive-but-invisible trees (the immersive overlay) where a // in always-alive-but-invisible trees (the immersive overlay) where a
@@ -58,10 +65,9 @@ Item {
property real t: 0 property real t: 0
readonly property int _tickMs: 33 readonly property int _tickMs: 33
Connections { Connections {
target: QbzShell target: root.active && root.visible ? QbzShell : null
function onPulseMsChanged() { 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 QtQuick
import com.blitzfc.qbz import com.blitzfc.qbz
import "../controls" import "../controls"
import "../theme"
Column { Column {
id: root id: root
/// Equalizer tint + "Now Playing" label color (Slint equalizer-tint). QbzTheme { id: theme }
property color equalizerTint: "#7c3aed"
/// Equalizer tint + "Now Playing" label follow the active theme.
property color equalizerTint: theme.accent
spacing: 6 spacing: 6
@@ -541,7 +541,6 @@ Item {
ImmersiveTrackMeta { ImmersiveTrackMeta {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 44 width: parent.width - 44
equalizerTint: "#7c3aed"
} }
} }
} }
@@ -116,7 +116,6 @@ Item {
// Track info (shared, homologated across the panels, :106-109). // Track info (shared, homologated across the panels, :106-109).
ImmersiveTrackMeta { ImmersiveTrackMeta {
width: root.width width: root.width
equalizerTint: "#7c3aed"
} }
} }
} }
+26 -2
View File
@@ -245,6 +245,11 @@ Rectangle {
font: albumText.font font: albumText.font
text: albumText.text text: albumText.text
} }
TextMetrics {
id: ellipsisMetrics
font: artistText.font
text: "…"
}
// Fixed furniture: the context box (+ its 7px gap when shown) // Fixed furniture: the context box (+ its 7px gap when shown)
// and the separator dot with a 7px gap on each side. // 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 // a long album elides before the artist does, and both keep a
// readable slice instead of one pushing the other off the card. // readable slice instead of one pushing the other off the card.
readonly property var budget: { readonly property var budget: {
var a = artistMetrics.width // `width` is the painted bounding box; `advanceWidth` is
var b = albumMetrics.width // 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 var av = metaRow.avail
if (a + b <= av) if (a + b <= av)
return [a, b] return [a, b]
@@ -269,6 +281,18 @@ Rectangle {
qb = b qb = b
qa = av - 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)] return [Math.floor(qa), Math.floor(qb)]
} }