feat(qt): Wallpaper follows the window on Plasma Wayland; its blurred twin drifts
"Wallpaper" now behaves like a window that sees straight through to the desktop: it always paints the part of the wallpaper that lies under the window, wherever the window is moved, with a stronger blur so nothing in the picture competes with the text. X11, macOS and Windows already told Qt where the window is. Wayland does not, so on KDE Plasma a tracker reads the window's geometry from org_kde_plasma_window_management on its own client connection and thread (wayland-client + the plasma protocol crate, pure Rust backend). It follows this process's windows only, drops every other window's object after its initial state, and republishes only when the geometry list changes; the field picks the window by size and ignores updates while another background is showing. Where the protocol is not offered the crop stays centred and one line is logged. KWin hands that protocol only to a binary whose desktop entry names it by an absolute Exec and lists it in X-KDE-Wayland-Interfaces, so the Linux desktop entries now carry both (the deb/rpm /usr/bin/qbz symlink resolves to the same binary). The AppImage step restores the bare Exec name that linuxdeploy resolves inside the AppDir, and the Nix package rewrites Exec to its store path. "Wallpaper, blurred" is Blurred art with the wallpaper in the cover's place, so it now drifts while the transport plays and holds still when it does not. cc
This commit is contained in:
12 files changed
+610
-39
No files matched your search
@@ -121,13 +121,17 @@ Rectangle {
|
||||
opacity: root.veilStrength
|
||||
}
|
||||
|
||||
// Mode 3 — Wallpaper: the desktop wallpaper, the part of it that lies
|
||||
// BEHIND this window (where the platform tells the window its place:
|
||||
// X11, macOS, Windows — Wayland hides it, so a centred crop stands in),
|
||||
// lightly blurred under the half-alpha chrome. Static: it costs a frame
|
||||
// only when the window moves or resizes (shell/WallpaperField.qml).
|
||||
// Mode 4 — Wallpaper, blurred: the wallpaper through the SAME atmosphere
|
||||
// pass Blurred art gives the cover, held still (no transport to follow).
|
||||
// Mode 3 — Wallpaper: the window reads as translucent to the desktop:
|
||||
// ALWAYS the part of the wallpaper that lies under it, wherever it is
|
||||
// moved (X11, macOS and Windows say where the window is; KDE Plasma
|
||||
// Wayland says so through its window-management protocol; anywhere
|
||||
// else a centred crop stands in), lightly blurred so nothing in the
|
||||
// picture competes with the text, yet still recognisable. Static: it
|
||||
// costs a frame only when the window moves or resizes
|
||||
// (shell/WallpaperField.qml).
|
||||
// Mode 4 — Wallpaper, blurred: Blurred art with the wallpaper in the
|
||||
// cover's place — the SAME atmosphere pass, the SAME drift while the
|
||||
// transport plays, the same still pose when it does not.
|
||||
// Neither waits for a playing track (QbzTheme.ambientOn).
|
||||
readonly property bool wallpaperModeOn: root.ambientOn && QbzShell.ambientMode === 3
|
||||
readonly property bool wallpaperBlurOn: root.ambientOn && QbzShell.ambientMode === 4
|
||||
@@ -136,7 +140,7 @@ Rectangle {
|
||||
visible: root.wallpaperModeOn
|
||||
source: root.wallpaperModeOn ? QbzShell.wallpaperUrl : ""
|
||||
hostWindow: root.hostWindow
|
||||
blur: 0.35
|
||||
blur: 0.6
|
||||
dim: theme.isDark ? QbzShell.ambientDim : 0.0
|
||||
}
|
||||
Rectangle {
|
||||
@@ -149,7 +153,7 @@ Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: root.wallpaperBlurOn
|
||||
source: root.wallpaperBlurOn ? QbzShell.wallpaperAtmosphereUrl : ""
|
||||
animated: false
|
||||
animated: root.wallpaperBlurOn && QbzPlayer.npPlaying
|
||||
dim: theme.isDark ? QbzShell.ambientDim : 0.0
|
||||
}
|
||||
Rectangle {
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
// WallpaperField — the desktop wallpaper behind the window (2026-09-14): the
|
||||
// part of it that lies UNDER this window, so moving the window slides the
|
||||
// picture the way a translucent window would, lightly blurred beneath the
|
||||
// half-alpha chrome. 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).
|
||||
// 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 report the window's position;
|
||||
// Wayland hides it from clients by design, so there the crop is centred on
|
||||
// the screen until the Plasma window-management protocol is wired
|
||||
// (wallpaper_qt.rs header). `hostWindow` is the ApplicationWindow.
|
||||
// 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
|
||||
@@ -27,17 +36,69 @@ Item {
|
||||
|
||||
clip: true
|
||||
|
||||
readonly property bool positionKnown: root.hostWindow !== null
|
||||
&& Qt.platform.pluginName !== "wayland"
|
||||
readonly property bool onWayland: 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.onWayland && root.source !== "")
|
||||
QbzShell.trackWindowPosition()
|
||||
}
|
||||
onVisibleChanged: _track()
|
||||
onSourceChanged: _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.onWayland)
|
||||
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.onWayland || 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
|
||||
? root.hostWindow.x - Screen.virtualX
|
||||
: Math.max(0, (root.screenW - root.width) / 2)
|
||||
readonly property real offY: root.positionKnown
|
||||
? root.hostWindow.y - Screen.virtualY
|
||||
: Math.max(0, (root.screenH - root.height) / 2)
|
||||
readonly property real offX: !root.positionKnown
|
||||
? Math.max(0, (root.screenW - root.width) / 2)
|
||||
: (root.onWayland ? root.compositorRect.x : root.hostWindow.x) - Screen.virtualX
|
||||
readonly property real offY: !root.positionKnown
|
||||
? Math.max(0, (root.screenH - root.height) / 2)
|
||||
: (root.onWayland ? root.compositorRect.y : root.hostWindow.y) - Screen.virtualY
|
||||
|
||||
Image {
|
||||
id: picture
|
||||
@@ -63,7 +124,7 @@ Item {
|
||||
autoPaddingEnabled: false
|
||||
blurEnabled: true
|
||||
blur: root.blur
|
||||
blurMax: 48
|
||||
blurMax: 64
|
||||
}
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
|
||||
Reference in new issue
Block a user