Files
qbz/crates/qbz-qt/qml/miniplayer/MiniArtworkSurface.qml
T
vicrodh cd89b40945 feat(qt): the miniplayer window, shell and the two display surfaces
Block B2 of the miniplayer/tray contract, and the block that settles the
premise the whole port rests on: a second top-level Window in the same
QQmlApplicationEngine reads the SAME bridge singletons as the shell. Proven
by driving it — a probe window with one Text bound to QbzPlayer.npTitle
tracked the main window across a track change with no mirror code. Slint
needs ~155 lines of mirroring for this; Qt needs none.

qml/miniplayer/ (new, six files) — MiniWindow (frameless, transparent,
transientParent null, its own key host and its own onClosing), MiniShell (the
card, the artwork backdrop, the surface router), MiniCoverArt and
MiniExplicitBadge (the two components MiniBits.slint exports, which cannot
share one .qml file), and the compact and artwork surfaces.

The card is the window minus a 6 px gutter, its radius is 9 for the condensed
surfaces and 10 otherwise, and switching the surface RESIZES THE WINDOW —
540 to 178 and back, driven and measured, not inferred.

The backdrop lives inside a masked container. `clip: true` on a rounded
Rectangle does NOT round its children on this Qt build — theme/RoundedImage.qml
records that as measured fact — so without the mask the scrim and the
atmosphere's own opaque root would paint square corners over the card the
moment the backdrop is on. That is reachable before its toggle exists,
because the pref is co-owned with the shipping Slint app.

src/mini_bridge.rs — enter/exit, the mini's own key dispatcher, and toggle().
Re-entering an open mini re-raises it rather than doing nothing, which is
what the reference does and what present() will want.

src/hotkeys_qt.rs — action_for_mini_key, a SIBLING of action_for_key with the
context arm inverted. The main window still refuses Context::Mini and the
test that pins it is untouched; a new test pins the mirror.

src/hotkeys_bridge.rs — the ui.miniPlayer arm stops logging and toggles.

qml/shell/ViewModeMenu.qml — the Miniplayer row goes live with its own
dispatch arm, and it is hidden under the kiosk profile, 1:1 with
PlayerBarSmall.slint:819. The kiosk port landed first, so that gate was owed
here. Three comments claiming the row is inert are corrected.

Surfaces 0, 3 and 4 are clamped to the artwork surface until their content
exists (B3 and B4), because mini_surface and mini_default_view live in the
prefs file shared with the Slint app: a user whose Slint mini was last on
Queue would otherwise open onto a blank card with no visible way out. Each
block deletes its own entry from that clamp.

Enter does not hide the main window yet — window.hideToTray() is B6's, and
open-coding a bare hide() here is what the one-visibility-owner rule forbids.
So the main window stays visible behind the mini in this commit, by design.

cc
2026-08-04 04:15:09 -06:00

127 lines
4.9 KiB
QML

// MiniArtworkSurface — surface 2, THE DEFAULT (2026-08-03 miniplayer/tray
// contract §4.2.2), port of
// `crates/qbz-ui/ui/miniplayer/MiniArtworkSurface.slint` (73 L, read whole).
//
// A large centred cover over centred metadata, in the resizable expanded
// window. DISPLAY ONLY.
//
// DEFERRED POLISH, REPRODUCED 1:1: the Tauri build ran a hover-triggered
// marquee per overflowing text line; this elides, exactly as the reference does
// and for the reason its own header records (:4-6). Do not add the marquee here
// before it lands there.
//
// Where compact falls back to a "—" string, this surface uses CONDITIONAL
// MOUNTS: an empty artist or album makes the row disappear and the Column's
// 2 px spacing collapse with it. In a QML Column an invisible child is skipped
// along with its spacing, so `visible:` is the exact equivalent of Slint's
// `if artist != "":`.
import QtQuick
import com.blitzfc.qbz
import "../theme"
Item {
id: root
QbzTheme { id: theme }
/// Handed down by MiniShell (see MiniCompactSurface for why it is a
/// property and not a `parent` read).
property bool npExplicit: false
// Max 320, 1:1, shrinking with the resizable window: the card is 40 px
// wider than the cover at the padding, so this is 320 at the 368 px card
// and 288 at the 340 px minimum width (:14). Cached, per §7-M2.
readonly property int coverSize: Math.min(root.width - 40, 320)
// VerticalLayout { padding-left/right 20; padding-top 16;
// padding-bottom 12; spacing 12; alignment: center }
// (:16-24). `alignment: center` centres the content inside the PADDED box,
// and that box is asymmetric (16 above, 12 below), so the stack sits 2 px
// below the item's own centre. Written out rather than anchored to
// verticalCenter so the asymmetry is visible instead of accidental.
Column {
id: stack
x: 20
width: root.width - 40
y: 16 + Math.max(0, (root.height - 28 - stack.height) / 2)
spacing: 12
// HorizontalLayout { alignment: center } (:26-27).
Item {
width: stack.width
height: root.coverSize
MiniCoverArt {
anchors.horizontalCenter: parent.horizontalCenter
width: root.coverSize
height: root.coverSize
// Overrides the 7 px default (:30).
radius: 10
source: QbzPlayer.npArtworkPath
}
}
// The meta block: VerticalLayout { spacing: 2px } — note it carries NO
// `alignment`, unlike compact's (:37-38). Each row centres itself.
Column {
width: stack.width
spacing: 2
// Title + badge row, spacing 5, title at stretch 1 (:40-53). The
// title is centred WITHIN its stretched box while the badge sits at
// the row's right edge — that is what the reference's layout does,
// not an approximation of it.
Item {
width: parent.width
height: title.implicitHeight
Text {
id: title
anchors.left: parent.left
width: parent.width - (badge.visible ? badge.width + 5 : 0)
text: QbzPlayer.npHasTrack
? QbzPlayer.npTitle
: QbzSession.tr("No track playing", QbzSession.trRev)
color: theme.textPrimary
font.pixelSize: 13
font.weight: Font.DemiBold
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
}
MiniExplicitBadge {
id: badge
visible: root.npExplicit
anchors.left: title.right
anchors.leftMargin: 5
anchors.verticalCenter: title.verticalCenter
}
}
// CONDITIONAL (:56-62) — the row disappears when empty, and there
// is no fallback string anywhere on this surface.
Text {
width: parent.width
visible: QbzPlayer.npArtist !== ""
text: QbzPlayer.npArtist
color: theme.textMuted
font.pixelSize: 12
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
}
// CONDITIONAL (:64-70), and it exists ONLY on this surface —
// compact has no album line at all. alphaTier(55), NOT textMuted.
Text {
width: parent.width
visible: QbzPlayer.npAlbum !== ""
text: QbzPlayer.npAlbum
color: theme.alphaTier(55)
font.pixelSize: 12
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
}
}
}
}