aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/appearance/WallpaperSpread.qml
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/modules/appearance/WallpaperSpread.qml')
-rw-r--r--desktop/modules/appearance/WallpaperSpread.qml116
1 files changed, 116 insertions, 0 deletions
diff --git a/desktop/modules/appearance/WallpaperSpread.qml b/desktop/modules/appearance/WallpaperSpread.qml
new file mode 100644
index 0000000..c133343
--- /dev/null
+++ b/desktop/modules/appearance/WallpaperSpread.qml
@@ -0,0 +1,116 @@
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+import Quickshell
+import Quickshell.Wayland
+import QtQuick
+
+// The transition when Apply sets a wallpaper. swaybg has none: wallp kills
+// the old one and starts a new one, which can show a frame of nothing. A
+// surface on the bottom layer, above swaybg and below every window, covers
+// the swap: the old picture burns away over the new one, and the
+// surface goes once wallp has finished, by when the new swaybg is drawing
+// the same picture underneath. Sets from a terminal get no transition: by
+// the time wall_h changes, swaybg has already swapped.
+Scope {
+ // ponytail: wallp's default outputs, like Wallpapers.panels; read
+ // wallp.conf if they are ever changed there.
+ Variants {
+ model: [{ which: "H", output: "DP-1" }, { which: "V", output: "DP-3" }]
+
+ PanelWindow {
+ id: win
+ required property var modelData
+ screen: Quickshell.screens.find(s => s.name === modelData.output) ?? null
+
+ property bool running: false
+ property bool done: false
+ property real progress: 0
+ property point origin: Qt.point(0.5, 0.5)
+ visible: running
+
+ anchors { top: true; bottom: true; left: true; right: true }
+ color: "transparent"
+ exclusionMode: ExclusionMode.Ignore
+ mask: Region {}
+ WlrLayershell.layer: WlrLayer.Bottom
+ WlrLayershell.namespace: "keystone-wallpaper-spread"
+ WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
+
+ Connections {
+ target: Wallpapers
+ function onSwapping(which, from, to) {
+ if (which !== win.modelData.which || !win.screen) return;
+ anim.stop();
+ // Behind the char band, so no pixel starts out scorched.
+ win.progress = -0.05;
+ // A different place each time the fire starts.
+ win.origin = Qt.point(Math.random(), Math.random());
+ win.done = false;
+ oldImg.source = from;
+ newImg.source = to;
+ win.running = true;
+ }
+ // A failed set may have left either picture underneath, so
+ // the cover goes at once rather than claiming the new one.
+ function onApplied(ok) {
+ win.done = true;
+ if (!ok) win.running = false;
+ else if (!anim.running) win.running = false;
+ }
+ }
+
+ // Both decoded before the first block drops, or the old one
+ // would pop in late over a half-drawn screen.
+ readonly property bool ready: running && oldImg.status === Image.Ready
+ && newImg.status === Image.Ready
+ onReadyChanged: if (ready) anim.restart()
+
+ // -m fill is a centred crop, which is what PreserveAspectCrop does.
+ Image {
+ id: newImg
+ anchors.fill: parent
+ fillMode: Image.PreserveAspectCrop
+ sourceSize: Qt.size(win.width, win.height)
+ asynchronous: true
+ cache: false
+ }
+
+ Image {
+ id: oldImg
+ anchors.fill: parent
+ fillMode: Image.PreserveAspectCrop
+ sourceSize: Qt.size(win.width, win.height)
+ asynchronous: true
+ cache: false
+ layer.enabled: win.running
+ layer.effect: ShaderEffect {
+ property real progress: win.progress
+ property real aspect: win.width / Math.max(1, win.height)
+ property point origin: win.origin
+ fragmentShader: Qt.resolvedUrl("burn.frag.qsb")
+ }
+ }
+
+ NumberAnimation {
+ id: anim
+ target: win
+ property: "progress"
+ // Past 1: fbm can push a threshold a little over it. Linear,
+ // since an ease spends its slow ends where nothing burns.
+ to: 1.05
+ duration: 3500
+ easing.type: Easing.Linear
+ onFinished: if (win.done) win.running = false
+ }
+ }
+ }
+}