Files
qbz/crates/qbz-qt/qml/rows/TrackListHeader.qml
T
vicrodh a3b89107fc fix(qt): table headers line up, the mini rail is centred, tooltips have a background
Three of the owner's visual reports, one theme: geometry that was derived
in two places and drifted.

The track table laid its header out from the LABELLED columns only, so
the heart and the overflow menu took no space there and every label sat
right of the column it names. Header and rows now come from one shared
set of widths, including the responsive arms -- if the header hid columns
on a different condition than the row, the drift would come back only on
narrow windows.

The collapsed rail goes back to Slint's 64px. It had been 50 by owner
request, and the measurement showed why that was the actual defect: the
rail centred its row at 25 while the header's own sidebar toggle, sitting
directly above it, centres at 30. Symmetric padding inside a 50px rail
still reads as misaligned because the eye compares the glyphs to the
header control, not to the rail; chasing it from inside bought the
alignment at the price of a visibly asymmetric hover pill. At 64 both
references land 2px apart and the padding is symmetric again. The row
stays a literal 34 rather than being derived from the width -- deriving
it is what coupled the two tokens and would have silently grown every row
to 48.

Tooltips: the port had none, while Slint has a shell-level overlay. This
adds the overlay rather than a Popup per call site.

cc
2026-07-29 16:25:57 -06:00

180 lines
7.0 KiB
QML

// TrackListHeader — the column header of the track table, the labelled twin
// of rows/TrackRow.qml. Both take their geometry from rows/TrackCols.qml, so
// a label sits over the column it names by construction rather than by two
// people typing the same numbers.
//
// --- THE CONTRACT (two lines, both checkable) ---------------------------
// 1. `width` MUST be the width the TrackRows below get. Not the page
// width — the ROW width. A list inset for a scrollbar gutter
// (PlaylistView's `anchors.rightMargin: 14`) or padded by its page
// (MixView's `parent.width - 64`) hands its rows a narrower width, and
// the header has to be narrowed the same way or every label after the
// stretch column slides right by the difference.
// 2. The five arms MUST be the arms the delegate sets. They gate BOTH the
// cell and its gap, so a header that reserves a column the row does not
// draw (or vice versa) shifts everything after it by width + 14.
//
// The owner's report was the second half of rule 2: the trailing unlabelled
// columns (heart, offline cloud, ⋯) were missing from some headers, so they
// took no space and every label was pushed right of its column. They are
// reserved here as empty slots — `favoriteGlyph` / `downloadGlyph` draw the
// 14px heart / cloud-download that AlbumPageView.slint:853-877 puts in the
// heads of those two columns, the only labelling Slint gives them.
//
// `trailingReserve` covers the one surface whose row draws controls OUTSIDE
// the shared TrackRow: views/local/LocalTrackRow.qml narrows the shared row
// and re-draws the ⋯ plus a source glyph in the gutter it freed. The header
// reserves the same slice so it stays the same width as the row it labels.
import QtQuick
import com.blitzfc.qbz
import "../theme"
Item {
id: root
// --- Arms: MIRROR the TrackRow arms of the list underneath ----------
// Defaults are TrackRow's own defaults, so a host that sets nothing gets
// a header for a row that sets nothing.
property bool showArtwork: false
property bool showAlbum: false
property bool showFavorite: true
property bool showDownload: false
property bool showMenu: true
/// Draw the 14px heart / cloud-download in the head of those two
/// reserved columns (AlbumPageView.slint). Off = an empty reserved slot,
/// which is what PlaylistView.slint and MixView.slint do.
property bool favoriteGlyph: false
property bool downloadGlyph: false
/// Label band height. 0 = the label's own line height, which is what the
/// playlist / mix headers use (Slint gives those HorizontalLayouts no
/// height, so they take the text's — 15px at Typography.legal, and
/// hardcoding an 18 here would have added 3px to those pages' rhythm).
/// The album headers set an explicit 40px band (AlbumPageView.slint:812).
property int bandHeight: 0
/// `letter-spacing: 0.5px` on the album headers; the playlist / mix
/// headers have none.
property real labelSpacing: 0
/// Width the ROW draws outside the shared TrackRow (LocalTrackRow only).
property int trailingReserve: 0
TrackCols { id: cols }
QbzTheme { id: theme }
width: parent ? parent.width : 0
height: bandH
/// Resolved band height (see `bandHeight`). The metric below is the same
/// font the labels use, so "natural" tracks a font-size change.
readonly property int bandH: root.bandHeight > 0
? root.bandHeight : Math.ceil(metric.implicitHeight)
Text {
id: metric
visible: false
text: "Xg"
font.pixelSize: theme.fontLegal
}
/// The row's own outer width minus whatever it spends outside the shared
/// TrackRow — i.e. exactly the width the shared row lays its cells in.
readonly property int innerWidth: Math.max(0, root.width - root.trailingReserve)
component ColLabel: Text {
color: theme.textMuted
font.pixelSize: theme.fontLegal
font.letterSpacing: root.labelSpacing
height: root.bandH
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
Row {
anchors.fill: parent
anchors.leftMargin: cols.padH
anchors.rightMargin: cols.padH
spacing: cols.gap
// "#" — centred, because the cell it labels is TrackRow's play cell
// and its number is centred in the same 32px. (Slint's album headers
// left-align this one over a centred number; the playlist header
// centres it. Centred is the one that lines up.)
ColLabel {
width: cols.colNumber
text: "#"
horizontalAlignment: Text.AlignHCenter
}
// Artwork cell — unlabelled, reserved (PlaylistView.slint:990).
Item {
visible: root.showArtwork
width: cols.colArt
height: root.bandH
}
ColLabel {
width: cols.titleWidth(root.innerWidth, root.showArtwork, root.showAlbum,
root.showFavorite, root.showDownload, root.showMenu)
text: QbzSession.tr("Title", QbzSession.trRev)
}
ColLabel {
visible: root.showAlbum
width: cols.colAlbum
text: QbzSession.tr("Album", QbzSession.trRev)
}
ColLabel {
width: cols.colDuration
text: QbzSession.tr("Duration", QbzSession.trRev)
horizontalAlignment: Text.AlignHCenter
}
ColLabel {
width: cols.colQuality
text: QbzSession.tr("Quality", QbzSession.trRev)
horizontalAlignment: Text.AlignHCenter
}
// Heart column. `height: root.bandH` (not 1) so `centerIn`
// centres the glyph on the band instead of pinning it to the top —
// the bug AlbumView.qml carried and documented.
Item {
visible: root.showFavorite
width: cols.colFavorite
height: root.bandH
QbzIcon {
visible: root.favoriteGlyph
anchors.centerIn: parent
name: "heart"
width: 14
height: 14
tintName: "muted"
}
}
// Offline / cloud column.
Item {
visible: root.showDownload
width: cols.colDownload
height: root.bandH
QbzIcon {
visible: root.downloadGlyph
anchors.centerIn: parent
name: "cloud-download"
width: 14
height: 14
tintName: "muted"
}
}
// ⋯ column.
Item {
visible: root.showMenu
width: cols.colMenu
height: root.bandH
}
// Whatever the row draws outside the shared TrackRow.
Item {
visible: root.trailingReserve > 0
// The gap before it is already spent by the Row, so the slot
// itself is the reserve minus that gap.
width: Math.max(0, root.trailingReserve - cols.gap)
height: root.bandH
}
}
}