fix(qt): header atmosphere, NPB button order, small-bar alignment, sidebar collage
The album/artist header band painted the 128x128 atmosphere as ONE Image at anchors.fill + Image.Stretch, mapping the field 1:1 onto the band. That field is vignetted in Rust, so a 1:1 map puts its bright core dead centre and the vignette ring on the edges - the "plasta en medio" the owner reported. ImmersiveAtmosphere.slint:21-67 lays FOUR copies, each larger than the band (+520/+620/+700/+560 wide) and pulled back by a negative offset with image-fit: cover, so what shows is a crop of a magnified field. Ported with the opaque #0a0a0b base and the header's own base/warp opacities. The phase constants are the .slint expressions with tick = 0 (animated: false), signs included: copy 2 subtracts where copy 1 adds, and equalising them stacks the four into one brighter copy. PlayerBar mounted Cast before Connect; PlayerBar.slint has ConnectButton at :401 and cast at :646. NowPlayingBarSmall already matched and is untouched. Seven children of the small bar's right-cluster Row carried no vertical anchor, and a QML Row top-aligns children that do not anchor themselves, so the 32px buttons rendered at y=0 against the 3px seek line with all the slack below. The queue button was among them. The sidebar playlist micro-collage used radius + clip, and clip is a rectangular scissor that ignores radius, so the cover tiles kept hard corners. Rounded with the MultiEffect mask idiom PlaylistCollage.qml documents, on the same _noShaders predicate so a software renderer degrades to square corners rather than a blank tile. Its tile layout also diverged from SidebarPlaylistCollage.slint, which draws ONE full-bleed cover below four covers and only splits into a 2x2 at four, with a 1px seam. cc
This commit is contained in:
4 files changed
+345
-46
No files matched your search
@@ -366,6 +366,23 @@ Rectangle {
|
||||
width: (root.width - 6) * root.sideFrac
|
||||
clip: true
|
||||
|
||||
// VERTICAL CENTRING IS PER-CHILD HERE. This Row is as tall as
|
||||
// the whole ~39px controls row (`height: parent.height`), and
|
||||
// a QML Row TOP-ALIGNS every child that does not anchor itself
|
||||
// — so a 32px button with no anchor rendered at y=0, flush
|
||||
// against the 3px seekbar, with all 7px of slack below it. The
|
||||
// active-state fill then read as "too big" when it was simply
|
||||
// sitting too high (the button IS 32x32, matching
|
||||
// controls/QbzIconButton.qml's btnSize and the reference's
|
||||
// 32x32 ConnectButton Rectangle).
|
||||
// The reference has no such bug because PlayerBarSmall.slint
|
||||
// builds this cluster as a HorizontalLayout, which centres by
|
||||
// default; every wrapper there is a `VerticalLayout {
|
||||
// alignment: center; }` (e.g. Connect :366-368, cast :601-603).
|
||||
// A child of a Row MAY anchor vertically — only horizontal
|
||||
// anchoring is forbidden — which is what AudioStamp and
|
||||
// QbzSlider already did, and what every sibling below now does
|
||||
// so the whole cluster sits on one line with the volume bar.
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -405,6 +422,7 @@ Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: theme.radiusSm
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: qcActive ? Qt.rgba(gold.r, gold.g, gold.b, 0.16)
|
||||
: (smallQcArea.containsMouse ? theme.surfaceHover : "transparent")
|
||||
border.width: qcActive ? 1 : 0
|
||||
@@ -432,6 +450,7 @@ Rectangle {
|
||||
QbzIconButton {
|
||||
name: "cast"
|
||||
active: QbzPlayer.npCastActive
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: QbzCast.openPicker()
|
||||
}
|
||||
// Audio settings — opens the Normalization + Gapless
|
||||
@@ -450,17 +469,20 @@ Rectangle {
|
||||
id: smallAudioBtn
|
||||
name: "settings-2"
|
||||
active: false
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: smallAudioMenu.openBelowRight(smallAudioBtn)
|
||||
}
|
||||
QbzIconButton {
|
||||
id: smallViewBtn
|
||||
name: "layout-grid"
|
||||
active: smallViewMenu.opened
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: smallViewMenu.openBelowRight(smallViewBtn)
|
||||
}
|
||||
QbzIconButton {
|
||||
name: "mic-vocal"
|
||||
active: QbzShell.lyricsOpen
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: QbzShell.toggleLyrics()
|
||||
}
|
||||
|
||||
@@ -472,6 +494,7 @@ Rectangle {
|
||||
name: QbzPlayer.npMuted ? "volume-x" : "volume-2"
|
||||
btnEnabled: !root.volLocked
|
||||
active: QbzPlayer.npMuted
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: QbzPlayer.toggleMute()
|
||||
}
|
||||
// WIDE: the shared QbzSlider (PlayerBarSmall mounts the
|
||||
@@ -495,6 +518,7 @@ Rectangle {
|
||||
QbzIconButton {
|
||||
name: "list-ordered"
|
||||
active: QbzShell.queueOpen
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: QbzShell.toggleQueue()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,9 +31,14 @@
|
||||
// Slint TransportControls/PlayerBar that descend from it):
|
||||
// transport: info · shuffle · prev · play · next · repeat · "+" (Add to…)
|
||||
// (+ the inline favorite heart in Classic, like Tauri)
|
||||
// right: Cast · Connect · Lyrics · Now-Playing view · Audio settings
|
||||
// right: Connect · Cast · Lyrics · Now-Playing view · Audio settings
|
||||
// (normalization) · [6px] · Mute · slider · −/+ steppers ·
|
||||
// [6px] · Queue
|
||||
// Connect BEFORE Cast is the reference's order, not a preference: PlayerBar.
|
||||
// slint mounts the ConnectButton at :395-401 and the cast IconButton at
|
||||
// :646-653. This file had the pair inverted (Cast first) until the order was
|
||||
// homologated; NowPlayingBarSmall.qml always matched (PlayerBarSmall.slint
|
||||
// ConnectButton :368, cast :603).
|
||||
// Tauri's Miniplayer + Full screen buttons live inside the Now-Playing view
|
||||
// flyout (ViewModeMenu) since 2.0 — that ONE button holds both.
|
||||
//
|
||||
@@ -451,17 +456,6 @@ Rectangle {
|
||||
}
|
||||
Item { visible: root.largeActive; width: 12; height: 1 }
|
||||
|
||||
// Cast (Chromecast / DLNA) — Tauri's first right-cluster
|
||||
// button. Opens the picker modal and, with it, discovery
|
||||
// (PlayerBar.slint:646-664: picker-open = true +
|
||||
// CastActions.open()); lit while a renderer is connected.
|
||||
QbzIconButton {
|
||||
name: "cast"
|
||||
active: QbzPlayer.npCastActive
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: QbzCast.openPicker()
|
||||
}
|
||||
|
||||
// Qobuz Connect — the Slint ConnectButton (PlayerBar.
|
||||
// slint:32-77): monitor-speaker glyph, GOLDEN over a soft
|
||||
// golden tint whenever a session is on — whether qbz is
|
||||
@@ -471,6 +465,10 @@ Rectangle {
|
||||
// gold tint + 1px border + gold glyph, which the shared
|
||||
// button's accent-glyph `active` cannot express. The
|
||||
// "amber" tint IS the reference's gold (#e0b341).
|
||||
//
|
||||
// FIRST of the icon cluster, ahead of Cast — the order the
|
||||
// reference lays out (PlayerBar.slint:395-401 Connect,
|
||||
// :646-653 Cast) and the one the Small bar already used.
|
||||
Rectangle {
|
||||
id: qconnectBtn
|
||||
readonly property bool qcActive: QbzQConnect.qconnectConnected
|
||||
@@ -518,6 +516,17 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// Cast (Chromecast / DLNA) — SECOND, straight after
|
||||
// Connect. Opens the picker modal and, with it, discovery
|
||||
// (PlayerBar.slint:646-664: picker-open = true +
|
||||
// CastActions.open()); lit while a renderer is connected.
|
||||
QbzIconButton {
|
||||
name: "cast"
|
||||
active: QbzPlayer.npCastActive
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: QbzCast.openPicker()
|
||||
}
|
||||
|
||||
// Lyrics.
|
||||
QbzIconButton {
|
||||
name: "mic-vocal"
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import com.blitzfc.qbz
|
||||
import "../controls"
|
||||
import "../theme"
|
||||
@@ -157,6 +158,26 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
/// Shader availability for the playlist micro-collage's rounding mask.
|
||||
/// Verbatim from cards/PlaylistCollage.qml:120-122 (and
|
||||
/// theme/RoundedImage.qml:234-236): the ONLY case the retired "effects
|
||||
/// render nothing" doctrine was ever true for is the software/Null
|
||||
/// renderer, and `GraphicsInfo.api` answers it per window at runtime
|
||||
/// (`api` carries `notify: "apiChanged"`, so this re-evaluates when the
|
||||
/// window arrives). Test Software/Null NEGATIVELY — under the RHI an
|
||||
/// OpenGL backend reports `GraphicsInfo.OpenGL`. `QbzShell.forceCanvasArt`
|
||||
/// (env `QBZ_QT_ROUND_MODE=canvas`, default OFF) is honoured here too, so
|
||||
/// a machine where `GraphicsInfo` reports a GPU and the mask still
|
||||
/// misbehaves degrades to square corners instead of losing the covers.
|
||||
/// `typeof` guard mirrors both files: eager binding, and the sidebar must
|
||||
/// stay loadable in an isolated qml6 scene.
|
||||
///
|
||||
/// Declared ONCE on this root rather than per delegate: the answer is a
|
||||
/// per-window constant and the playlist tree can hold hundreds of rows.
|
||||
readonly property bool _noShaders: GraphicsInfo.api === GraphicsInfo.Software
|
||||
|| GraphicsInfo.api === GraphicsInfo.Null
|
||||
|| (typeof QbzShell !== "undefined" && QbzShell.forceCanvasArt)
|
||||
|
||||
QbzTheme { id: theme }
|
||||
|
||||
// The section-nav dropdown (catalog + panel + open/close behaviour) —
|
||||
@@ -887,28 +908,178 @@ Rectangle {
|
||||
y: Math.round((parent.height - height) / 2)
|
||||
tintName: "accent"
|
||||
}
|
||||
// 2x2 micro-collage (or list-music glyph).
|
||||
Rectangle {
|
||||
// Playlist micro-collage (or list-music
|
||||
// glyph) — the port of
|
||||
// primitives/SidebarPlaylistCollage.slint:
|
||||
// a 20px box, 3px radius, surface-elevated
|
||||
// backing, 1px seam.
|
||||
//
|
||||
// TWO arms, not one. The reference splits
|
||||
// on the cover count (.slint:27-68):
|
||||
// 1-3 covers -> ONE full-bleed cover, 4+ ->
|
||||
// the 2x2 grid. This used to be an
|
||||
// unconditional 2x2 of hard-coded 10x10
|
||||
// tiles with no seam, so a playlist with a
|
||||
// single cover drew one 10x10 tile in the
|
||||
// top-left quadrant and left three quarters
|
||||
// of the box empty.
|
||||
//
|
||||
// Covers stay on the host's already-resolved
|
||||
// `root.coverMap` (filled by the ONE
|
||||
// `sidebarArtworkWindow` dispatch in
|
||||
// parseEntries) — this is not a second
|
||||
// artwork seam.
|
||||
Item {
|
||||
id: collage
|
||||
visible: useCollage
|
||||
width: 20
|
||||
height: 20
|
||||
x: Math.round((parent.width - width) / 2)
|
||||
y: Math.round((parent.height - height) / 2)
|
||||
radius: 3
|
||||
color: theme.surfaceElevated
|
||||
clip: true
|
||||
Grid {
|
||||
|
||||
// `n`, straight off SidebarEntry.cover-count
|
||||
// (.slint:24). sidebar_qt.rs already
|
||||
// de-duplicates and caps `covers` at 4
|
||||
// (SidebarEntry::covers), so this is 0..4
|
||||
// and no dedup pass is owed here.
|
||||
readonly property int n: useCollage ? plRow.modelData.covers.length : 0
|
||||
// .slint:25 `gap: 1px`. NOT the mosaic
|
||||
// card's 2px seam — the micro-collage is
|
||||
// its own component with its own number.
|
||||
readonly property real gap: 1
|
||||
|
||||
// Backing surface. A Rectangle's OWN
|
||||
// rounded fill paints correctly; only its
|
||||
// CHILDREN need the mask below, which is
|
||||
// exactly the distinction the old code
|
||||
// missed.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
columns: 2
|
||||
Repeater {
|
||||
model: modelData.covers.slice(0, 4)
|
||||
delegate: Image {
|
||||
required property string modelData
|
||||
width: 10
|
||||
height: 10
|
||||
source: root.coverMap[modelData] || ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
radius: 3
|
||||
color: theme.surfaceElevated
|
||||
}
|
||||
|
||||
// The rounded outer shape as a mask
|
||||
// texture. `clip: true` in Qt Quick is a
|
||||
// RECTANGULAR scissor and IGNORES
|
||||
// `radius`, which is why the old
|
||||
// `Rectangle { radius: 3; clip: true }`
|
||||
// shipped square collages: the fill was
|
||||
// round, the four covers painted on top
|
||||
// of it were not (owner report). The
|
||||
// documented fix is the `layer.enabled` +
|
||||
// `MultiEffect { maskEnabled }` idiom —
|
||||
// same mechanism, same constants, as
|
||||
// cards/PlaylistCollage.qml:264-305 and
|
||||
// theme/RoundedImage.qml.
|
||||
Item {
|
||||
id: collageMask
|
||||
anchors.fill: parent
|
||||
visible: false
|
||||
// A layer renders even when its item
|
||||
// is invisible — that IS the mask
|
||||
// idiom — so the gate has to be
|
||||
// `useCollage`, not `visible`.
|
||||
// PlaylistCollage.qml gates its own
|
||||
// two layers on `tiles.length > 0`
|
||||
// for the same reason, and it matters
|
||||
// MORE here: this delegate is inside
|
||||
// a Repeater over the whole playlist
|
||||
// tree, so an ungated pair of layers
|
||||
// would cost two FBOs on every folder
|
||||
// row and every glyph row too.
|
||||
layer.enabled: useCollage && !root._noShaders
|
||||
layer.smooth: true
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: 3
|
||||
color: "#ffffff"
|
||||
}
|
||||
}
|
||||
|
||||
// ONE layer + ONE mask for the whole
|
||||
// collage, not one per tile. `clip` stays
|
||||
// on the container because
|
||||
// PreserveAspectCrop paints OUTSIDE its
|
||||
// item's bounds unless clipped — it just
|
||||
// cannot round on its own.
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
layer.enabled: useCollage && !root._noShaders
|
||||
layer.smooth: true
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
maskSource: collageMask
|
||||
// 0.5 / 1.0 — MEASURED; the full
|
||||
// table is in
|
||||
// theme/RoundedImage.qml. Qt's
|
||||
// mask is a smoothstep CENTRED on
|
||||
// maskThresholdMin of width
|
||||
// maskSpreadAtMin, so this is the
|
||||
// only pair that maps mask alpha
|
||||
// 0..1 onto 0..1. (0.0, 0.0)
|
||||
// collapses it to step() and the
|
||||
// corners come back HARD; (0.0,
|
||||
// > 0) clamps everything opaque
|
||||
// and disables the mask outright.
|
||||
// Do not "restore the defaults".
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1.0
|
||||
}
|
||||
|
||||
// 1-3 covers -> a single full-bleed
|
||||
// cover (.slint:28-33). `image-fit:
|
||||
// cover` = PreserveAspectCrop.
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
visible: collage.n >= 1 && collage.n < 4
|
||||
// Empty out of arm so the hidden
|
||||
// branch holds no texture. Reading
|
||||
// `visible` back gives the
|
||||
// EFFECTIVE visibility, so this
|
||||
// also drops the texture for a row
|
||||
// the mini rail collapses — and
|
||||
// re-binds when it comes back.
|
||||
source: visible ? (root.coverMap[plRow.modelData.covers[0]] || "") : ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
}
|
||||
|
||||
// 4+ covers -> a 2x2 grid of
|
||||
// (size - gap) / 2 tiles with the 1px
|
||||
// seam (.slint:36-68). Grid `spacing`
|
||||
// IS that seam: 9.5 + 1 + 9.5 = 20.
|
||||
Grid {
|
||||
anchors.fill: parent
|
||||
columns: 2
|
||||
spacing: collage.gap
|
||||
Repeater {
|
||||
// Empty below four — the arm
|
||||
// is not merely hidden, it
|
||||
// holds no tiles and no
|
||||
// textures.
|
||||
model: collage.n >= 4 ? plRow.modelData.covers.slice(0, 4) : []
|
||||
delegate: Image {
|
||||
required property string modelData
|
||||
width: (collage.width - collage.gap) / 2
|
||||
height: (collage.height - collage.gap) / 2
|
||||
source: root.coverMap[modelData] || ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
// Per-tile scissor: a crop
|
||||
// fit overflows its own
|
||||
// bounds, and the
|
||||
// container's clip is 20px
|
||||
// wide — without this a
|
||||
// wide cover bleeds into
|
||||
// the neighbouring cell
|
||||
// instead of being cropped
|
||||
// to it (Slint's
|
||||
// `image-fit: cover`
|
||||
// clips per element).
|
||||
clip: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user