aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/appearance/WallpaperSpread.qml
blob: c133343cdcfc16d0b98e408f203f77e3bcb3d714 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
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
            }
        }
    }
}