Files
qbz/crates/qbz-qt/qml/controls/QbzJumpNavBar.qml
T
vicrodh c85fbac4c7 fix(artist): keep the jump-nav mounted in library mode and park the filters past the magnifier
With both source switches on over an artist whose purchases are not
hearted, the library tab's section set was empty, the strip unmounted,
and the switches — which live in the strip — went with it, leaving no
way to reset the filter. "About" (the strip's back-to-top) is now always
present in library mode, landing on the page top since the library
column carries no bio anchor; the active tab on a mode switch is still
the first real section.

The trailing slot moves to the far right of the bar, past the search
toggle: the field grows LEFT over the tabs when it opens, so controls
left of the magnifier were covered while typing.

cc
2026-09-04 11:51:59 -06:00

203 lines
8.4 KiB
QML

// QbzJumpNavBar — 1:1 port of primitives/JumpNavBar.slint.
//
// The section-jump strip that sits between a page header and its body:
// [JUMP TO] tab tab tab … [magnifier]
// The Slint mounts it on the artist page and (without the search arm) on the
// label page, which is why it lives in controls/ rather than inside a view.
//
// Numbers, read off primitives/JumpNavBar.slint:
// :59 height 44 :60 background = `bar-bg` (host passes the page fill)
// :65 1px border-subtle hairline pinned to the BOTTOM edge
// :74 padding-left/right 32 :76 spacing 18 between label / tabs / search
// :84 "JUMP TO" — legal (13px), semibold, text-muted, letter-spacing 0.5
// :106 tab spacing 14; :111 32px tall cell, width = text + 4, text x = 2
// :116 active -> text-primary + semibold, hover -> text-primary,
// else text-secondary + regular; :128 2px accent underline when active
// :157 open search field 216px wide, 200ms ease-in-out, surface-card
// :260 closed toggle 32px, radius 6, hover surface-elevated, 16px glyph
//
// The search affordance is the SHARED controls/QbzLineEdit expandable arm
// (ExpandableSearch.slint), not a second inline input: same interaction —
// closed magnifier slot, opens right-anchored growing LEFT over the tabs,
// X clears and closes, Esc closes. Its closed slot is 34px against the
// .slint's 32px; that 2px is the price of not owning a fourth search box.
//
// Presentation only: sticky positioning and what a tab scrolls to are the
// host's business (the .slint says the same at :12).
import QtQuick
import com.blitzfc.qbz
import "../theme"
Rectangle {
id: root
QbzTheme { id: theme }
/// [{ id, label }] — the sections present on the page.
property var tabs: []
property string activeTabId: ""
/// Page fill, so the bar reads as part of the page (and can go
/// translucent under the dynamic background, like the .slint's bar-bg).
property color barBg: theme.surfaceMain
/// The label page mounts the bar WITHOUT the search affordance.
property bool showSearch: true
/// Horizontal inset. 32 = the .slint's own padding for a full-bleed
/// mount; a host that already pads its column passes 0.
property real padH: 32
/// Top-corner rounding, for a host that PINS this bar to y=0 of the
/// content pane.
///
/// A full-bleed child sitting at the pane's top-left/right has to round
/// ITSELF: Qt's `clip` is a rectangular scissor that ignores `radius`, and
/// AppShell hides its bezel nubs while the dynamic background shows through
/// the corners — so the pane's own rounding cannot reach a child. Square
/// corners then poke out past the bezel, which is exactly what the owner
/// caught in Discover (`DiscoverBrowseView.qml:103`) and again here once
/// this bar became sticky.
///
/// The host must drive it from the PINNED state, not set it once: rounded
/// corners in mid-page, where the bar is surrounded by content on all
/// sides, are a notch out of the strip with nothing behind them.
property real topRadius: 0
signal tabClicked(string id)
signal searchEdited(string text)
/// Host hook: clear the filter when the field closes itself (Esc / X).
property alias searchOpen: searchBox.open
/// Host-owned controls that float between the tab strip and the
/// magnifier — the artist page mounts its "In library" purchases /
/// favourites switches here while that tab is active (#737 round). The
/// slot sizes to its children; empty, it costs nothing and the strip
/// keeps its full width. Same footprint rule as the search slot: the
/// tabs are clipped, never reflowed, when something lives here.
property alias trailing: trailingSlot.data
height: 44
color: root.barBg
topLeftRadius: root.topRadius
topRightRadius: root.topRadius
Rectangle {
x: 0
y: parent.height - 1
width: parent.width
height: 1
color: theme.borderSubtle
}
Text {
id: jumpLabel
x: root.padH
anchors.verticalCenter: parent.verticalCenter
text: QbzSession.tr("JUMP TO", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: theme.fontLegal
font.weight: theme.weightSemibold
font.letterSpacing: 0.5
}
// Tab strip. Clipped so a long discography's tabs never spill under the
// search slot (the .slint's `center-region { clip: true }`).
Item {
id: centerRegion
x: jumpLabel.x + jumpLabel.width + 18
y: 0
width: Math.max(0, root.width - root.padH
- (searchSlot.visible ? searchSlot.width + 18 : 0)
- (trailingSlot.width > 0 ? trailingSlot.width + 18 : 0)
- x)
height: parent.height
clip: true
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: 14
Repeater {
model: root.tabs
delegate: Item {
id: tabCell
required property var modelData
readonly property bool active: modelData.id === root.activeTabId
width: tabText.implicitWidth + 4
height: 32
Text {
id: tabText
x: 2
height: parent.height
verticalAlignment: Text.AlignVCenter
text: tabCell.modelData.label
color: (tabCell.active || tabArea.containsMouse)
? theme.textPrimary : theme.textSecondary
font.pixelSize: theme.fontBody
font.weight: tabCell.active ? theme.weightSemibold
: theme.weightRegular
}
Rectangle {
visible: tabCell.active
x: 0
y: parent.height - 2
width: parent.width
height: 2
color: theme.accent
}
MouseArea {
id: tabArea
anchors.fill: parent
// The .slint disables the tabs while the field is
// open (they are behind it).
enabled: !searchBox.open
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.tabClicked(tabCell.modelData.id)
}
}
}
}
}
// Trailing slot (see `trailing`): sized by its children, parked at the
// FAR right — past the magnifier, not before it: the search field grows
// LEFT over the tabs when it opens, so anything left of the magnifier is
// covered while the user types, and a control that resets the page's
// state must stay reachable exactly then. Hidden items still count toward
// `childrenRect`, so a host that toggles its controls should toggle their
// `visible` AND leave the slot empty-width (`width: visible ? n : 0`).
Item {
id: trailingSlot
width: childrenRect.width
height: parent.height
anchors.right: parent.right
anchors.rightMargin: root.padH
}
// Search slot — keeps the CLOSED footprint; the field itself grows left
// over the tabs, so nothing in the bar reflows when it opens.
Item {
id: searchSlot
visible: root.showSearch
width: searchBox.width
height: searchBox.height
anchors.right: trailingSlot.width > 0 ? trailingSlot.left : parent.right
anchors.rightMargin: trailingSlot.width > 0 ? 18 : root.padH
anchors.verticalCenter: parent.verticalCenter
QbzLineEdit {
id: searchBox
searchMode: true
expandable: true
// surface-card overlay fill (JumpNavBar.slint:164), not the
// toolbar's elevated.
elevated: false
openWidth: Math.min(216, Math.max(120, centerRegion.width))
placeholder: QbzSession.tr("Search this page…", QbzSession.trRev)
onEdited: function (v) { root.searchEdited(v) }
}
}
}