The track's "ShaderEffect / ColorOverlay / MultiEffect render NOTHING on the
software path this port runs on" doctrine was measured FALSE on 2026-07-29:
qbz-qt runs on the GPU (OpenGL RHI, threaded render loop, vsync 5.56ms). The
claim came from an offscreen session, and QT_QPA_PLATFORM=offscreen forces
software by definition. Every design it justified needed re-deciding.
- RoundedImage was a Canvas (renderTarget: Canvas.Image) that rasterised with
QPainter on the CPU AND uploaded a texture per repaint, for every cover in
the app, purely to round a corner identical in every frame of that image's
life. It is now layer.enabled + MultiEffect { maskSource } — an FBO rendered
once and cached for static content. Rectangle { radius; clip: true } cannot
do this: clip in Qt Quick is a RECTANGULAR scissor and ignores radius.
maskThresholdMin/maskSpreadAtMin are 0.5/1.0, NOT the 0.0/0.0 that looks
natural: 0.0 collapses Qt's mask smoothstep to step(0, alpha) and every
pixel with any alpha snaps fully opaque — measured 0 antialiased pixels at
radius 40 against the Canvas's 292. At 0.5/1.0 it is 308, ramp [0,51,219,255].
A software machine keeps the Canvas arm (GraphicsInfo.api), and
QBZ_QT_ROUND_MODE=canvas pins it on a GPU machine where the mask misbehaves.
- VizSettle drove the spectrum from FrameAnimation, i.e. every rendered frame:
180fps on this box for data the producer publishes at TARGET_FPS = 30. Now a
16ms Timer; the wall-clock maths is unchanged.
- SpectrumBand's per-bar y was a SECOND JS binding derived from height. An
anchor does the same arithmetic in C++ with no binding at all, and each FFT
frame is materialised into a plain array once per publish instead of being
read element by element across the JS/C++ boundary. Bindings per tick: bars
28 (was 56), waveform 48 (was 96), energy 5 (was 10).
QQuickPaintedItem was investigated and rejected with evidence: cxx-qt 0.7.3
emits X(QObject* parent) : base(parent), which does not compile against
QQuickPaintedItem(QQuickItem*), paint() is pure virtual, and there is zero
precedent for a custom base class in this workspace. Qt 6 ShaderEffect takes
no GLSL, only precompiled .qsb, which would make the build depend on a Qt
tool whose path differs on macOS.
- PlaylistCollage point-sampled its mosaic in its own Canvas
(Context2D.drawImage does not filter when it scales — measured RMSE 0.000
against nearest-neighbour). It now draws through RoundedImage.
- The scaled-derivative cache had no eviction at all and the .jpg -> .png key
change had orphaned 1251 files. Boot housekeeping sweeps the orphans once and
bounds the directory at 64 MB, LRU by mtime. Verified by effect: first run
deleted 1259 files / 10.05 MB, every run since reports 0, and the directory
is 14 MB. The eviction is only survivable because RoundedImage clears
_scaled on Image.Error and re-requests.
- For You artwork patched the rails in place instead of republishing the
document, which was resetting contentY. HomeView is destroyed on every
navigation, so the patch map is re-handed on mount through
QbzHome.refreshForyouArt() — otherwise the Radio rail and the Spotlight hero
went blank for the rest of the session after one navigation, and the skeleton
pulse Timer stayed pinned ON, defeating the fix it belonged to.
- Spotlight's placeholder gated on the local path instead of the remote url, so
it flashed a glyph on a cold cache where the Slint does not.
The retired doctrine is corrected wherever it was load-bearing for a decision.
The pre-baked icon tints are deliberately NOT reopened and no drop-shadows are
added: both are visual changes, not perf fixes.
cc
169 lines
6.2 KiB
QML
169 lines
6.2 KiB
QML
// SidebarNowPlayingDock — the Large-NPB (mode 3) cover + spectrum dock: the
|
|
// L's vertical arm. QML port of crates/qbz-ui/ui/shell/SidebarNowPlayingDock.slint.
|
|
//
|
|
// Mounted as a ROOT overlay by AppShell, pinned flush to the window
|
|
// bottom-left (NOT inside the sidebar — that floated it above the bar).
|
|
// Layout, bottom-up: a square album cover (= the fed width) at the very
|
|
// bottom; ABOVE it a compact spectrum band the user can hide with the
|
|
// top-right eye button on the cover.
|
|
//
|
|
// HEIGHT IS LOAD-BEARING. `QbzShell.largeDockHeight` (shell_bridge.rs) is the
|
|
// single source of truth and the layout below is derived from the SAME
|
|
// constants:
|
|
// ON = 9 top pad + 42 band + 10 gap + 208 art + 4 bottom gap = 273
|
|
// OFF = 9 top pad + 208 art + 4 bottom gap = 221
|
|
// Sidebar.qml reserves `largeDockHeight - npb height` so the playlist list
|
|
// stops ABOVE the cover, and AppShell pins this overlay at
|
|
// `height - largeDockHeight`. Change the arithmetic in one place and the
|
|
// cover slides off the window edge.
|
|
//
|
|
// The spectrum strip itself lives in SpectrumBand.qml (render modes + motion);
|
|
// its perf contract is documented there. This file owns the gating and the
|
|
// layout arithmetic only.
|
|
//
|
|
// GATING RULE (owner, 2026-07-28): freeze on NOT VISIBLE, never on lost
|
|
// focus — a tiling desktop keeps windows visible and unfocused.
|
|
|
|
import QtQuick
|
|
import QtQuick.Window
|
|
import com.blitzfc.qbz
|
|
import "../theme"
|
|
|
|
Item {
|
|
id: root
|
|
|
|
QbzTheme { id: theme }
|
|
|
|
// Fed by AppShell (sidebar content width). The cover is square.
|
|
// height comes from the bridge — never recompute it here.
|
|
height: QbzShell.largeDockHeight
|
|
|
|
// Fed by AppShell so the ambient-vs-solid surface rule lives in ONE place.
|
|
property bool ambientOn: false
|
|
|
|
readonly property bool bandOn: QbzShell.largeVisualizerOn
|
|
readonly property int bandHeight: 42
|
|
readonly property int padTop: 9
|
|
readonly property int bandGap: 10
|
|
// The cover's top edge: below the band when it is shown.
|
|
readonly property int artY: padTop + (bandOn ? bandHeight + bandGap : 0)
|
|
|
|
// True unless the window is minimized or hidden — see the GATING RULE.
|
|
readonly property bool windowShowing: root.Window.window
|
|
? (root.Window.window.visibility !== Window.Minimized
|
|
&& root.Window.window.visibility !== Window.Hidden)
|
|
: true
|
|
|
|
// Capture gate — the Slint AppShell's `viz-should-run`. OFF stops the FFT
|
|
// producer outright (viz_qt.rs parks it), so leaving Large or hiding the
|
|
// band costs nothing. The transport is mirrored separately (a paused
|
|
// player parks the producer via now_playing.rs).
|
|
readonly property bool vizShouldRun: root.visible && root.bandOn && root.windowShowing
|
|
onVizShouldRunChanged: QbzViz.setEnabled(root.vizShouldRun)
|
|
Component.onCompleted: QbzViz.setEnabled(root.vizShouldRun)
|
|
Component.onDestruction: QbzViz.setEnabled(false)
|
|
|
|
// Top hairline between the playlists and the dock.
|
|
Rectangle {
|
|
x: 0
|
|
y: 0
|
|
width: root.width
|
|
height: 1
|
|
color: theme.borderSubtle
|
|
}
|
|
|
|
// ---- Spectrum band -----------------------------------------------------
|
|
SpectrumBand {
|
|
x: 0
|
|
y: root.padTop
|
|
width: root.width
|
|
height: root.bandHeight
|
|
shown: root.bandOn
|
|
capturing: root.vizShouldRun
|
|
}
|
|
|
|
// ---- Album cover -------------------------------------------------------
|
|
// Drop-shadow approximation of the Slint dock's 24px blur (two offset
|
|
// rects).
|
|
// SUPERSEDED (2026-07-29): this used to say "QtQuick.Effects renders
|
|
// nothing on the software path". Effects need shaders, and this port runs
|
|
// on the GPU (OpenGL RHI, measured); that note came from an offscreen
|
|
// session, which forces the software renderer by definition — see
|
|
// theme/RoundedImage.qml, which now DETECTS the software path with
|
|
// `GraphicsInfo.api`. A real blurred shadow is possible; it is a visual
|
|
// change needing its own parity pass, so the approximation stays for now.
|
|
Rectangle {
|
|
x: 0
|
|
y: root.artY + 4
|
|
width: root.width
|
|
height: root.width
|
|
radius: theme.radiusMd
|
|
color: "#66000000"
|
|
}
|
|
|
|
Rectangle {
|
|
id: art
|
|
x: 0
|
|
y: root.artY
|
|
width: root.width
|
|
height: root.width
|
|
radius: theme.radiusMd
|
|
color: root.ambientOn ? theme.surfaceMainA22 : theme.surfaceMain
|
|
clip: true
|
|
|
|
RoundedImage {
|
|
visible: QbzPlayer.npHasTrack
|
|
anchors.fill: parent
|
|
source: QbzPlayer.npArtworkPath
|
|
radius: theme.radiusMd
|
|
}
|
|
QbzIcon {
|
|
visible: !QbzPlayer.npHasTrack
|
|
name: "music"
|
|
width: root.width * 0.32
|
|
height: root.width * 0.32
|
|
anchors.centerIn: parent
|
|
tintName: "muted"
|
|
}
|
|
|
|
// Hover tracker over the whole cover — reveals the toggle. The toggle
|
|
// ALSO stays visible while the band is hidden, so it can be brought back.
|
|
MouseArea {
|
|
id: coverHover
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
}
|
|
|
|
// Single top-right toggle: show/hide the spectrum band.
|
|
Rectangle {
|
|
width: 28
|
|
height: 28
|
|
x: parent.width - width - 8
|
|
y: 8
|
|
radius: 8
|
|
color: eyeHover.containsMouse ? "#2effffff" : "#80000000"
|
|
border.width: 1
|
|
border.color: "#33ffffff"
|
|
opacity: (coverHover.containsMouse || eyeHover.containsMouse || !root.bandOn) ? 1.0 : 0.0
|
|
Behavior on opacity { NumberAnimation { duration: 140 } }
|
|
|
|
QbzIcon {
|
|
anchors.centerIn: parent
|
|
name: root.bandOn ? "eye" : "eye-off"
|
|
width: 15
|
|
height: 15
|
|
// On the #80000000 / #2effffff chip over the artwork —
|
|
// theme-independent, so a fixed white glyph is correct.
|
|
tintName: "white"
|
|
}
|
|
MouseArea {
|
|
id: eyeHover
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: QbzShell.largeToggleVisualizer()
|
|
}
|
|
}
|
|
}
|
|
}
|