aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/appearance/WallpaperSpread.qml
blob: f9e484b217893be1f31e97d5de7141b01f14ea1b (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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
// 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 gives way to the new one, through the transition
// Transitions picked, 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 string effect: "burn"
            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, effect) {
                    if (which !== win.modelData.which || !win.screen) return;
                    anim.stop();
                    win.effect = effect;
                    win.progress = 0;
                    // A different place each time it starts.
                    win.origin = Qt.point(Math.random(), Math.random());
                    win.done = false;
                    oldImg.source = from;
                    newImg.source = to;
                    win.running = true;
                }
                // Any run's end, Apply's or the engine's. A failed set may
                // have left either picture underneath, so the cover goes at
                // once rather than claiming the new one.
                function onFinished(ok) {
                    win.done = true;
                    if (!ok) win.running = false;
                    else if (!anim.running) win.running = false;
                }
            }

            // Both decoded before anything moves, 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();
                drawn.restart();
            }
            // A few frames for the cover to reach the screen, then wallp may
            // swap swaybg underneath it.
            Timer {
                id: drawn
                interval: 50
                onTriggered: Wallpapers.covered()
            }

            // -m fill is a centred crop, which is what PreserveAspectCrop does.
            // Both hidden until both are decoded: the new one usually lands
            // first, and drawn under an old one still loading it flashed
            // the new picture before the transition. Until then the surface is
            // clear and swaybg shows the old picture itself.
            Image {
                id: newImg
                visible: win.ready
                anchors.fill: parent
                fillMode: Image.PreserveAspectCrop
                sourceSize: Qt.size(win.width, win.height)
                asynchronous: true
                cache: false
                // A layer, so the shaders that read it get the crop.
                layer.enabled: win.running
            }

            Image {
                id: oldImg
                visible: win.ready
                anchors.fill: parent
                fillMode: Image.PreserveAspectCrop
                sourceSize: Qt.size(win.width, win.height)
                asynchronous: true
                cache: false
                layer.enabled: win.running
                layer.effect: WallShader {
                    name: win.effect
                    target: newImg
                    progress: win.progress
                    aspect: win.width / Math.max(1, win.height)
                    origin: win.origin
                }
            }

            NumberAnimation {
                id: anim
                target: win
                property: "progress"
                // Linear, since an ease spends its slow ends where nothing
                // happens.
                to: 1
                duration: Transitions.duration
                easing.type: Easing.Linear
                onFinished: if (win.done) win.running = false
            }
        }
    }
}