Files
qbz/crates/qbz-qt/qml/shell/WallpaperField.qml
T
vicrodh 6b1fcd30ab fix(qt): rename the Wallpaper field's platform flag off the handler namespace
The field's platform flag was named `onWayland`. With no `wayland` member
beside it the binding happens to work, but the on-prefixed property audit
(which qt-run.sh runs) rejects any `on<Upper>` name, and an instance
cannot assign it: `onWayland: false` is parsed as a handler for a
nonexistent signal and the component fails to load. It is `isWayland`
now, writable so a test can take the Wayland branch under the offscreen
platform, and a change of it also asks Rust to follow the window.

tst_wallpaper_field covers the Qt position on X11 / macOS / Windows, the
compositor rectangle picked by this window's size on Wayland (a
miniplayer-sized window is ignored, a move is followed), the centred
fallback, and an invisible field that reads nothing. It runs in the
runtime gate.

cc
2026-09-14 06:07:14 -06:00

139 lines
5.6 KiB
QML

// WallpaperField — the desktop wallpaper behind the window (2026-09-14): the
// part of it that lies UNDER this window, wherever the window is, so the
// window reads as translucent to the desktop — an X-ray straight to the
// wallpaper, never to the windows in between (a really translucent surface
// would show those). Lightly blurred beneath the half-alpha chrome: enough
// that nothing in the picture competes with the text, little enough that the
// picture is still recognisable. Nothing animates: it costs a frame only when
// the window moves or resizes, and a screen-sized texture at most
// (`sourceSize` caps the decode — a 3x-upscaled wallpaper never reaches the
// GPU whole).
//
// WHERE THE WINDOW IS. X11, macOS and Windows tell Qt (`hostWindow.x/y`).
// Wayland hides it from clients by design; on KDE Plasma the compositor
// reports every window's geometry through org_kde_plasma_window_management
// (wallpaper_wayland_qt.rs), published as QbzShell.wallpaperWindowsJson —
// this process's windows with absolute coordinates — and the one whose size
// is this window's is the one the picture sits under (the miniplayer and the
// dialogs are this process's windows too). Where neither can say, the crop
// is centred on the screen. `hostWindow` is the ApplicationWindow.
import QtQuick
import QtQuick.Window
import QtQuick.Effects
import com.blitzfc.qbz
Item {
id: root
/// file:// URL of the wallpaper (QbzShell.wallpaperUrl).
property string source: ""
property var hostWindow: null
/// MultiEffect blur amount, 0 = crisp.
property real blur: 0.0
/// Darkening on top (the dark-theme legibility veil).
property real dim: 0.0
clip: true
/// Where the window's place comes from: the compositor's report on
/// Wayland, Qt everywhere else. Writable only so a test can take the
/// Wayland branch under the offscreen platform.
property bool isWayland: Qt.platform.pluginName === "wayland"
// Ask the compositor as soon as this field is actually painting on a
// Wayland session (idempotent on the Rust side; a no-op elsewhere).
function _track() {
if (root.visible && root.isWayland && root.source !== "")
QbzShell.trackWindowPosition()
}
onVisibleChanged: _track()
onSourceChanged: _track()
onIsWaylandChanged: _track()
Component.onCompleted: _track()
// Read only while this field is the background: the tracker keeps
// reporting moves after the mode is switched away, and an invisible item
// whose geometry changes still dirties the window (the repaint rule).
readonly property var compositorRects: {
if (!root.visible || !root.isWayland)
return []
try {
var a = JSON.parse(QbzShell.wallpaperWindowsJson || "[]")
return Array.isArray(a) ? a : []
} catch (e) {
return []
}
}
/// The compositor's rectangle for THIS window, or null when it has said
/// nothing (no protocol, or nothing our size).
readonly property var compositorRect: root.pickRect(root.compositorRects,
root.hostWindow ? root.hostWindow.width : root.width,
root.hostWindow ? root.hostWindow.height : root.height)
/// The rectangle whose size is closest to `w` x `h`, when it is close
/// enough to be this window at all: a geometry that is not our size
/// belongs to another of our windows, and the centred crop beats that
/// window's place.
function pickRect(rects, w, h) {
var best = null
var bestScore = 0
for (var i = 0; i < rects.length; i++) {
var r = rects[i]
if (!r || typeof r.w !== "number" || typeof r.h !== "number"
|| typeof r.x !== "number" || typeof r.y !== "number")
continue
var score = Math.abs(r.w - w) + Math.abs(r.h - h)
if (best === null || score < bestScore) {
best = r
bestScore = score
}
}
return (best !== null && bestScore <= 64) ? best : null
}
readonly property bool positionKnown: root.visible && root.hostWindow !== null
&& (!root.isWayland || root.compositorRect !== null)
readonly property real screenW: Math.max(1, Screen.width)
readonly property real screenH: Math.max(1, Screen.height)
// The window's origin inside its screen; centred when unknown.
readonly property real offX: !root.positionKnown
? Math.max(0, (root.screenW - root.width) / 2)
: (root.isWayland ? root.compositorRect.x : root.hostWindow.x) - Screen.virtualX
readonly property real offY: !root.positionKnown
? Math.max(0, (root.screenH - root.height) / 2)
: (root.isWayland ? root.compositorRect.y : root.hostWindow.y) - Screen.virtualY
Image {
id: picture
source: root.source
x: -root.offX
y: -root.offY
width: root.screenW
height: root.screenH
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: false
sourceSize.width: Math.round(root.screenW * Screen.devicePixelRatio)
sourceSize.height: Math.round(root.screenH * Screen.devicePixelRatio)
smooth: true
}
MultiEffect {
visible: root.blur > 0 && picture.status === Image.Ready
source: picture
x: picture.x
y: picture.y
width: picture.width
height: picture.height
autoPaddingEnabled: false
blurEnabled: true
blur: root.blur
blurMax: 64
}
Rectangle {
anchors.fill: parent
color: "#000000"
opacity: root.dim
}
}