// Copyright (C) 2026 Danilo M. // // 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 } } } }