1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-24 20:15:21 -04:00
Files
DankMaterialShell/quickshell/Modules/WallpaperBackground.qml
T
hecate cantus 1a39b7f66c shader-based scrolling wallpaper mode (#1802)
* feat: parallax-scroll wallpaper

Add a `Scrolling` wallpaper fill mode that translates the wallpaper crop
with the active workspace — like Android home-screen parallax, but along
niri's vertical workspace axis.

The image is scaled to cover the screen on its non-scroll axis, and the
active workspace index drives a fractional offset into the cropped
overflow along the scroll axis. Scroll position is spring-animated
CPU-side and handed to a minimal single-texture shader as a UV offset.
Per-monitor scroll position is published into SessionData so the lock
screen renders the same crop as the active workspace, keeping visual
continuity across lock/unlock.

Two implementation details worth calling out for review:

- QSG_USE_SIMPLE_ANIMATION_DRIVER=1 is exported to the spawned quickshell
  process. The default animation driver advances in fixed ~16ms steps,
  capping the scroll at 60Hz and desyncing it from compositor motion on
  high-refresh displays; the simple driver advances by real elapsed time,
  restoring native-refresh pacing. Removing it visibly regresses to 60Hz.

- The wallpaper survives wl_output rebind cycles (e.g. OLED image-cleaning
  on DPMS soft-off), which otherwise leave a stuck or void background.
  Recovery re-anchors the scroll target on output-lifecycle signals,
  rebuilds the ShaderEffect against the current render context, and
  re-attaches the wallpaper-layer surface on unlock for parallax-active
  monitors — guarded against lock state so the shader gets reliable frame
  hints.

* simplify bindings and gate lock screen shader in a loader

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-06-24 00:14:42 -04:00

1076 lines
44 KiB
QML

import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Widgets
import qs.Services
Variants {
id: variants
readonly property var log: Log.scoped("WallpaperBackground")
// An entry present in PanelWindow.onCompleted means we're recreating
// after a wl_output rebind, not at initial startup.
property var _seenScreens: ({})
model: {
if (SessionData.isGreeterMode) {
return Quickshell.screens;
}
return SettingsData.getFilteredScreens("wallpaper");
}
PanelWindow {
id: wallpaperWindow
required property var modelData
screen: modelData
WlrLayershell.layer: WlrLayer.Background
WlrLayershell.exclusionMode: ExclusionMode.Ignore
anchors.top: true
anchors.bottom: true
anchors.left: true
anchors.right: true
color: "transparent"
updatesEnabled: root.renderActive || root._settleFrames > 0
mask: Region {
item: Item {}
}
Item {
id: root
anchors.fill: parent
Rectangle {
anchors.fill: parent
color: SettingsData.effectiveWallpaperBackgroundColor
}
function encodeFileUrl(path) {
if (!path)
return "";
return "file://" + path.split('/').map(s => encodeURIComponent(s)).join('/');
}
property string source: SessionData.getMonitorWallpaper(modelData.name) || ""
property bool isColorSource: source.startsWith("#")
property string transitionType: SessionData.wallpaperTransition
property string actualTransitionType: transitionType
property bool isInitialized: false
property string scrollMode: SettingsData.wallpaperFillMode
property bool scrollingEnabled: scrollMode === "Scrolling"
property int currentWorkspaceIndex: 0
property int totalWorkspaces: 1
// Also requires the image to overflow on the compositor's scroll
// axis — niri scrolls Y, Hyprland scrolls X — otherwise the
// currentWallpaper Fill fallback handles it.
property bool effectiveScrolling: scrollingEnabled && totalWorkspaces > 1 && (!imageMetrics.ready || (CompositorService.isNiri && imageMetrics.nativeWidth / imageMetrics.nativeHeight < root.textureWidth / root.textureHeight - 0.01) || (CompositorService.isHyprland && imageMetrics.nativeWidth / imageMetrics.nativeHeight > root.textureWidth / root.textureHeight + 0.01))
Connections {
target: SessionData
function onIsLightModeChanged() {
if (SessionData.perModeWallpaper) {
var newSource = SessionData.getMonitorWallpaper(modelData.name) || "";
if (newSource !== root.source) {
root.source = newSource;
}
}
}
}
Connections {
target: NiriService
enabled: CompositorService.isNiri && root.scrollingEnabled
function onAllWorkspacesChanged() {
root.updateWorkspaceData();
}
}
Connections {
target: CompositorService.isHyprland ? Hyprland : null
enabled: CompositorService.isHyprland && root.scrollingEnabled
function onRawEvent(event) {
if (event.name === "workspace" || event.name === "workspacev2") {
root.updateWorkspaceData();
}
}
}
onTransitionTypeChanged: {
if (transitionType !== "random") {
actualTransitionType = transitionType;
return;
}
actualTransitionType = SessionData.includedTransitions.length === 0 ? "none" : SessionData.includedTransitions[Math.floor(Math.random() * SessionData.includedTransitions.length)];
}
property real transitionProgress: 0
property real shaderFillMode: getFillMode(SessionData.getMonitorWallpaperFillMode(modelData.name))
property vector4d fillColor: Qt.vector4d(0, 0, 0, 1)
property real edgeSmoothness: 0.1
property real wipeDirection: 0
property real discCenterX: 0.5
property real discCenterY: 0.5
property real stripesCount: 16
property real stripesAngle: 0
readonly property bool transitioning: transitionAnimation.running
property bool effectActive: false
property bool useNextForEffect: false
property string pendingWallpaper: ""
property string _deferredSource: ""
readonly property bool overviewBlurActive: CompositorService.isNiri && SettingsData.blurWallpaperOnOverview && NiriService.inOverview && currentWallpaper.source !== ""
readonly property var backingWindow: Window.window
readonly property bool renderActive: !source || effectActive || overviewBlurActive || pendingWallpaper !== "" || _deferredSource !== "" || frameAnim.running || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading
property int _settleFrames: 3
function invalidate() {
_settleFrames = 3;
backingWindow?.update();
}
onRenderActiveChanged: invalidate()
onBackingWindowChanged: invalidate()
Connections {
target: root.backingWindow
function onFrameSwapped() {
if (root._settleFrames > 0)
root._settleFrames--;
}
function onVisibleChanged() {
root.invalidate();
}
function onWidthChanged() {
root.invalidate();
}
function onHeightChanged() {
root.invalidate();
}
}
Connections {
target: Quickshell
function onScreensChanged() {
root.invalidate();
root._onOutputRebind();
}
}
Connections {
target: SettingsData
function onWallpaperFillModeChanged() {
root.invalidate();
}
function onWallpaperBackgroundColorModeChanged() {
root.invalidate();
}
function onWallpaperBackgroundCustomColorChanged() {
root.invalidate();
}
}
Connections {
target: IdleService
function onIsShellLockedChanged() {
if (IdleService.isShellLocked)
return;
root.invalidate();
// Catches silent rebinds during lock that no signal reports.
if (root.effectiveScrolling)
surfaceReattach.restart();
}
}
function _recheckScreenScale() {
const newScale = CompositorService.getScreenScale(modelData);
if (newScale !== root.screenScale) {
log.info("screen scale corrected for", modelData.name + ":", root.screenScale, "->", newScale);
root.screenScale = newScale;
}
}
// Workspace/scale signals don't re-fire when the output list is
// unchanged across a rebind, so re-derive scroll state by hand.
function _onOutputRebind() {
if (!root.scrollingEnabled)
return;
root.firstScrollUpdate = true;
Qt.callLater(root.updateWorkspaceData);
parallaxLoader.rebuild();
if (root.effectiveScrolling && !IdleService.isShellLocked)
surfaceReattach.restart();
}
// Bouncing visible re-attaches a layer surface wedged by a rebind;
// debounced so a burst of signals yields one re-attach.
Timer {
id: surfaceReattach
interval: 0
repeat: false
onTriggered: {
wallpaperWindow.visible = false;
Qt.callLater(() => {
wallpaperWindow.visible = true;
});
}
}
Connections {
target: NiriService
function onDisplayScalesChanged() {
root._recheckScreenScale();
root.invalidate();
root._onOutputRebind();
}
}
Connections {
target: WlrOutputService
function onWlrOutputAvailableChanged() {
root._recheckScreenScale();
root.invalidate();
root._onOutputRebind();
}
}
Connections {
target: CompositorService
function onRandrDataReady() {
if (root._deferredSource) {
const src = root._deferredSource;
root._deferredSource = "";
root.setWallpaperImmediate(src);
} else {
root._recheckScreenScale();
}
root._onOutputRebind();
}
}
function handleTransitionLoadError(failedSource) {
log.warn("failed to load candidate wallpaper for", modelData.name + ":", failedSource);
transitionDelayTimer.stop();
transitionAnimation.stop();
root.useNextForEffect = false;
root.effectActive = false;
root.transitionProgress = 0.0;
currentWallpaper.layer.enabled = false;
nextWallpaper.layer.enabled = false;
nextWallpaper.source = "";
if (!root.pendingWallpaper)
return;
const pending = root.pendingWallpaper;
root.pendingWallpaper = "";
Qt.callLater(() => root.changeWallpaper(pending, true));
}
function getFillMode(modeName) {
switch (modeName) {
case "Scrolling":
return Image.PreserveAspectCrop;
case "Stretch":
return Image.Stretch;
case "Fit":
case "PreserveAspectFit":
return Image.PreserveAspectFit;
case "Fill":
case "PreserveAspectCrop":
return Image.PreserveAspectCrop;
case "Tile":
return Image.Tile;
case "TileVertically":
return Image.TileVertically;
case "TileHorizontally":
return Image.TileHorizontally;
case "Pad":
return Image.Pad;
default:
return Image.PreserveAspectCrop;
}
}
function updateWorkspaceData() {
if (!scrollingEnabled)
return;
let newTargetX = 50.0;
let newTargetY = 50.0;
if (CompositorService.isNiri) {
const outputWorkspaces = NiriService.allWorkspaces.filter(ws => ws.output === modelData.name);
totalWorkspaces = outputWorkspaces.length;
const activeWs = outputWorkspaces.find(ws => ws.is_active);
currentWorkspaceIndex = activeWs ? activeWs.idx : 0;
const scrollPercent = totalWorkspaces > 1 ? ((currentWorkspaceIndex - 1) / (totalWorkspaces - 1)) * 100.0 : 0.0;
newTargetY = scrollPercent;
} else if (CompositorService.isHyprland) {
const workspaces = Hyprland.workspaces?.values || [];
const monitorWorkspaces = workspaces.filter(ws => ws.monitor?.name === modelData.name).sort((a, b) => a.id - b.id);
totalWorkspaces = monitorWorkspaces.length;
const focusedId = Hyprland.focusedWorkspace?.id;
currentWorkspaceIndex = monitorWorkspaces.findIndex(ws => ws.id === focusedId);
if (currentWorkspaceIndex < 0)
currentWorkspaceIndex = 0;
const scrollPercent = totalWorkspaces > 1 ? ((currentWorkspaceIndex - 1) / (totalWorkspaces - 1)) * 100.0 : 0.0;
newTargetX = scrollPercent;
}
scrollAnim.startAnimation(newTargetX, newTargetY);
}
property bool firstScrollUpdate: true
QtObject {
id: scrollAnim
property real startTime: 0
property real startX: 0.0
property real startY: 0.0
property real targetX: 0.0
property real targetY: 0.0
property real damping: CompositorService.isNiri ? 63.25 : 89.44
property real stiffness: CompositorService.isNiri ? 1000.0 : 2000.0
property real mass: 1.0
function springPositionJS(t, from, to) {
if (t <= 0)
return from;
const beta = damping / (2 * mass);
const omega0 = Math.sqrt(stiffness / mass);
const x0 = from - to;
const envelope = Math.exp(-beta * t);
if (Math.abs(x0 * envelope) < 0.01)
return to;
if (Math.abs(beta - omega0) < 0.0001) {
return to + envelope * (x0 + beta * x0 * t);
} else if (beta < omega0) {
const omega1 = Math.sqrt(omega0 * omega0 - beta * beta);
return to + envelope * (x0 * Math.cos(omega1 * t) + (beta * x0 / omega1) * Math.sin(omega1 * t));
} else {
const omega2 = Math.sqrt(beta * beta - omega0 * omega0);
const cosh = x => (Math.exp(x) + Math.exp(-x)) / 2;
const sinh = x => (Math.exp(x) - Math.exp(-x)) / 2;
return to + envelope * (x0 * cosh(omega2 * t) + (beta * x0 / omega2) * sinh(omega2 * t));
}
}
function startAnimation(newTargetX, newTargetY) {
const now = Date.now() / 1000.0;
const t = Math.max(0, frameAnim.currentTime - startTime);
const currentX = springPositionJS(t, startX, targetX);
const currentY = springPositionJS(t, startY, targetY);
if (Math.abs(newTargetX - currentX) < 0.01 && Math.abs(newTargetY - currentY) < 0.01) {
if (root.firstScrollUpdate)
root.firstScrollUpdate = false;
return;
}
// First update: use much stiffer spring for quick snap-to
if (root.firstScrollUpdate) {
root.firstScrollUpdate = false;
damping = 200.0;
stiffness = 8000.0;
} else {
// Restore normal spring parameters
damping = CompositorService.isNiri ? 63.25 : 89.44;
stiffness = CompositorService.isNiri ? 1000.0 : 2000.0;
}
startX = currentX;
startY = currentY;
targetX = newTargetX;
targetY = newTargetY;
startTime = frameAnim.running ? frameAnim.currentTime : now;
if (!frameAnim.running) {
frameAnim.currentTime = now;
frameAnim.running = true;
}
}
}
// CPU-side scroll position - computed once per frame instead of per-pixel in shader
// Initialize at (0, 0) to avoid pillarbox flash; first update will snap to correct position
property real currentScrollX: 0.0
property real currentScrollY: 0.0
function publishScrollPosition() {
if (effectiveScrolling) {
SessionData.setMonitorScrollPosition(modelData.name, currentScrollX, currentScrollY);
} else {
// Not scrolling - publish centered (50, 50)
SessionData.setMonitorScrollPosition(modelData.name, 50, 50);
}
}
FrameAnimation {
id: frameAnim
running: false
property real currentTime: 0
onRunningChanged: {
if (running) {
currentTime = Date.now() / 1000.0;
} else {
root.publishScrollPosition(); // Animation settled
// Hold the render loop open so the final settled frame
// commits before updatesEnabled drops out from under us.
root.invalidate();
}
}
onTriggered: {
// Clamp huge frameTime from a paused-render-loop wakeup;
// otherwise the spring's `t` jumps past settling.
const dt = frameTime > 0.1 ? 0.0 : frameTime;
currentTime += dt;
const t = currentTime - scrollAnim.startTime;
root.currentScrollX = scrollAnim.springPositionJS(t, scrollAnim.startX, scrollAnim.targetX);
root.currentScrollY = scrollAnim.springPositionJS(t, scrollAnim.startY, scrollAnim.targetY);
const settledX = Math.abs(scrollAnim.targetX - root.currentScrollX) < 0.01;
const settledY = Math.abs(scrollAnim.targetY - root.currentScrollY) < 0.01;
if (settledX && settledY) {
running = false;
}
}
}
Component.onCompleted: {
isInitialized = true;
if (scrollingEnabled) {
updateWorkspaceData();
}
Qt.callLater(publishScrollPosition);
// Detect rebind via _seenScreens; schedule surface re-attach
// (deferred to unlock if locked).
const wasSeen = variants._seenScreens[modelData.name] === true;
variants._seenScreens[modelData.name] = true;
// If currently locked, the unlock handler will re-attach;
// otherwise re-attach now.
if (wasSeen && root.effectiveScrolling && !IdleService.isShellLocked) {
surfaceReattach.restart();
}
}
Component.onDestruction: {
SessionData.clearMonitorScrollPosition(modelData.name);
}
onScrollingEnabledChanged: {
if (scrollingEnabled) {
firstScrollUpdate = true;
updateWorkspaceData();
} else {
frameAnim.stop();
}
}
onEffectiveScrollingChanged: {
publishScrollPosition();
}
onSourceChanged: {
if (!source || source.startsWith("#")) {
setWallpaperImmediate("");
return;
}
const formattedSource = source.startsWith("file://") ? source : encodeFileUrl(source);
if (!isInitialized || !currentWallpaper.source) {
if (!CompositorService.randrReady) {
_deferredSource = formattedSource;
return;
}
setWallpaperImmediate(formattedSource);
return;
}
if (CompositorService.isNiri && SessionData.isSwitchingMode) {
setWallpaperImmediate(formattedSource);
return;
}
changeWallpaper(formattedSource);
}
function setWallpaperImmediate(newSource) {
transitionAnimation.stop();
root.transitionProgress = 0.0;
root.effectActive = false;
root.screenScale = CompositorService.getScreenScale(modelData);
currentWallpaper.source = newSource;
nextWallpaper.source = "";
// Reset scroll state for new image - will snap to correct position on first update
if (scrollingEnabled) {
firstScrollUpdate = true;
currentScrollX = 0.0;
currentScrollY = 0.0;
scrollAnim.startX = 0.0;
scrollAnim.startY = 0.0;
scrollAnim.targetX = 0.0;
scrollAnim.targetY = 0.0;
}
}
function startTransition() {
currentWallpaper.layer.enabled = true;
nextWallpaper.layer.enabled = true;
root.useNextForEffect = true;
root.effectActive = true;
if (srcCurrent.scheduleUpdate)
srcCurrent.scheduleUpdate();
if (srcNext.scheduleUpdate)
srcNext.scheduleUpdate();
transitionDelayTimer.start();
}
Timer {
id: transitionDelayTimer
interval: 16
repeat: false
onTriggered: transitionAnimation.start()
}
function changeWallpaper(newPath, force) {
if (!force && newPath === currentWallpaper.source)
return;
if (!newPath || newPath.startsWith("#"))
return;
root.screenScale = CompositorService.getScreenScale(modelData);
if (root.transitioning || root.effectActive) {
root.pendingWallpaper = newPath;
return;
}
if (!currentWallpaper.source) {
setWallpaperImmediate(newPath);
return;
}
if (root.effectiveScrolling) {
setWallpaperImmediate(newPath);
return;
}
if (root.transitionType === "random") {
root.actualTransitionType = SessionData.includedTransitions.length === 0 ? "none" : SessionData.includedTransitions[Math.floor(Math.random() * SessionData.includedTransitions.length)];
}
if (root.actualTransitionType === "none") {
setWallpaperImmediate(newPath);
return;
}
switch (root.actualTransitionType) {
case "wipe":
root.wipeDirection = Math.random() * 4;
break;
case "disc":
case "pixelate":
case "portal":
root.discCenterX = Math.random();
root.discCenterY = Math.random();
break;
case "stripes":
root.stripesCount = Math.round(Math.random() * 20 + 4);
root.stripesAngle = Math.random() * 360;
break;
}
nextWallpaper.source = newPath;
if (nextWallpaper.status === Image.Ready)
root.startTransition();
}
Loader {
anchors.fill: parent
active: !root.source || root.isColorSource || currentWallpaper.status === Image.Error
asynchronous: true
sourceComponent: DankBackdrop {
screenName: modelData.name
}
}
readonly property int maxTextureSize: 8192
property real screenScale: 1
property int textureWidth: Math.min(Math.round(modelData.width * screenScale), maxTextureSize)
property int textureHeight: Math.min(Math.round(modelData.height * screenScale), maxTextureSize)
QtObject {
id: imageMetrics
property real nativeWidth: 0
property real nativeHeight: 0
property bool ready: nativeWidth > 0 && nativeHeight > 0
function capture(w, h) {
if (nativeWidth === 0 && w > 0) {
nativeWidth = w;
nativeHeight = h;
}
}
function reset() {
nativeWidth = 0;
nativeHeight = 0;
}
readonly property real canvasWidth: {
if (!ready || !root.effectiveScrolling)
return root.textureWidth;
const imageAspect = nativeWidth / nativeHeight;
const screenAspect = root.textureWidth / root.textureHeight;
if (imageAspect < screenAspect) {
return root.textureWidth;
} else {
return root.textureHeight * imageAspect;
}
}
readonly property real canvasHeight: {
if (!ready || !root.effectiveScrolling)
return root.textureHeight;
const imageAspect = nativeWidth / nativeHeight;
const screenAspect = root.textureWidth / root.textureHeight;
if (imageAspect < screenAspect) {
return root.textureWidth / imageAspect;
} else {
return root.textureHeight;
}
}
}
Image {
id: currentWallpaper
anchors.fill: parent
visible: !root.effectiveScrolling
opacity: 1
layer.enabled: false
asynchronous: true
retainWhileLoading: true
smooth: true
cache: true
sourceSize: Qt.size(root.textureWidth, root.textureHeight)
fillMode: root.getFillMode(SessionData.getMonitorWallpaperFillMode(modelData.name))
onStatusChanged: {
if (status === Image.Error) {
log.warn("failed to load active wallpaper for", modelData.name + ":", source);
}
if (status === Image.Ready) {
imageMetrics.capture(implicitWidth, implicitHeight);
}
}
onSourceChanged: {
imageMetrics.reset();
}
}
Image {
id: nextWallpaper
anchors.fill: parent
visible: source !== ""
opacity: 0
layer.enabled: false
asynchronous: true
retainWhileLoading: true
smooth: true
cache: true
sourceSize: Qt.size(root.textureWidth, root.textureHeight)
fillMode: root.getFillMode(SessionData.getMonitorWallpaperFillMode(modelData.name))
onStatusChanged: {
if (status === Image.Error) {
root.handleTransitionLoadError(source);
return;
}
if (status !== Image.Ready)
return;
if (root.actualTransitionType === "none") {
currentWallpaper.source = source;
nextWallpaper.source = "";
root.transitionProgress = 0.0;
} else if (!root.transitioning) {
root.startTransition();
}
}
}
ShaderEffectSource {
id: srcCurrent
sourceItem: root.effectActive ? currentWallpaper : null
hideSource: root.effectActive
live: root.effectActive
mipmap: false
recursive: false
textureSize: Qt.size(root.textureWidth, root.textureHeight)
}
ShaderEffectSource {
id: srcNext
sourceItem: root.effectActive ? nextWallpaper : null
hideSource: root.effectActive
live: root.effectActive
mipmap: false
recursive: false
textureSize: Qt.size(root.textureWidth, root.textureHeight)
}
Rectangle {
id: dummyRect
width: 1
height: 1
visible: false
color: "transparent"
}
ShaderEffectSource {
id: srcDummy
sourceItem: dummyRect
hideSource: true
live: false
mipmap: false
recursive: false
}
// Parallax scrolling pipeline — bypasses transition machinery.
Image {
id: parallaxImage
visible: false
width: imageMetrics.canvasWidth
height: imageMetrics.canvasHeight
source: root.effectiveScrolling ? currentWallpaper.source : ""
asynchronous: true
smooth: true
cache: true
sourceSize: Qt.size(imageMetrics.canvasWidth, imageMetrics.canvasHeight)
fillMode: Image.Stretch
}
ShaderEffectSource {
id: srcParallax
sourceItem: root.effectiveScrolling && imageMetrics.ready && parallaxImage.status === Image.Ready ? parallaxImage : null
hideSource: false
live: true
mipmap: false
recursive: false
textureSize: Qt.size(imageMetrics.canvasWidth, imageMetrics.canvasHeight)
}
// Pre-computed UV parameters for shader
QtObject {
id: parallaxUV
readonly property real imageAspect: imageMetrics.ready ? imageMetrics.canvasWidth / imageMetrics.canvasHeight : 1.0
readonly property real screenAspect: root.textureWidth / root.textureHeight
// Scale factor to fit image to screen (preserving aspect, cropping excess)
readonly property real scale: Math.max(root.textureWidth / imageMetrics.canvasWidth, root.textureHeight / imageMetrics.canvasHeight)
readonly property real scaledWidth: imageMetrics.canvasWidth * scale
readonly property real scaledHeight: imageMetrics.canvasHeight * scale
// UV scale: portion of texture visible on screen
readonly property real uvScaleX: root.textureWidth / scaledWidth
readonly property real uvScaleY: root.textureHeight / scaledHeight
// Scroll range: how much UV space we can scroll through
// Only allow scrolling in the dimension where image exceeds screen
readonly property real scrollRangeX: imageAspect > screenAspect + 0.01 ? (1.0 - uvScaleX) : (1.0 - uvScaleX) * 0.5
readonly property real scrollRangeY: imageAspect < screenAspect - 0.01 ? (1.0 - uvScaleY) : (1.0 - uvScaleY) * 0.5
readonly property bool scrollsHorizontal: imageAspect > screenAspect + 0.01
readonly property bool scrollsVertical: imageAspect < screenAspect - 0.01
}
Loader {
id: parallaxLoader
anchors.fill: parent
active: root.effectiveScrolling && !root.effectActive && imageMetrics.ready && parallaxImage.status === Image.Ready
sourceComponent: parallaxScrollComp
// Rebuild after a rebind orphans the texture; callLater defeats
// sourceComponent coalescing.
function rebuild() {
if (!active)
return;
sourceComponent = null;
Qt.callLater(() => {
sourceComponent = parallaxScrollComp;
});
}
}
Component {
id: parallaxScrollComp
ShaderEffect {
anchors.fill: parent
property variant source: srcParallax.sourceItem ? srcParallax : srcDummy
property real scrollX: root.currentScrollX
property real scrollY: root.currentScrollY
property real uvScaleX: parallaxUV.uvScaleX
property real uvScaleY: parallaxUV.uvScaleY
property real scrollRangeX: parallaxUV.scrollsHorizontal ? parallaxUV.scrollRangeX : 0.0
property real scrollRangeY: parallaxUV.scrollsVertical ? parallaxUV.scrollRangeY : 0.0
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wp_parallax_scroll.frag.qsb")
}
}
Loader {
id: effectLoader
anchors.fill: parent
active: root.effectActive
function getTransitionComponent(type) {
switch (type) {
case "fade":
return fadeComp;
case "wipe":
return wipeComp;
case "disc":
return discComp;
case "stripes":
return stripesComp;
case "iris bloom":
return irisComp;
case "pixelate":
return pixelateComp;
case "portal":
return portalComp;
default:
return null;
}
}
sourceComponent: getTransitionComponent(root.actualTransitionType)
}
Component {
id: fadeComp
ShaderEffect {
anchors.fill: parent
property variant source1: srcCurrent
property variant source2: root.useNextForEffect ? srcNext : srcDummy
property real progress: root.transitionProgress
property real fillMode: root.shaderFillMode
property vector4d fillColor: root.fillColor
property real imageWidth1: modelData.width
property real imageHeight1: modelData.height
property real imageWidth2: modelData.width
property real imageHeight2: modelData.height
property real screenWidth: modelData.width
property real screenHeight: modelData.height
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wp_fade.frag.qsb")
}
}
Component {
id: wipeComp
ShaderEffect {
anchors.fill: parent
property variant source1: srcCurrent
property variant source2: root.useNextForEffect ? srcNext : srcDummy
property real progress: root.transitionProgress
property real smoothness: root.edgeSmoothness
property real direction: root.wipeDirection
property real fillMode: root.shaderFillMode
property vector4d fillColor: root.fillColor
property real imageWidth1: modelData.width
property real imageHeight1: modelData.height
property real imageWidth2: modelData.width
property real imageHeight2: modelData.height
property real screenWidth: modelData.width
property real screenHeight: modelData.height
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wp_wipe.frag.qsb")
}
}
Component {
id: discComp
ShaderEffect {
anchors.fill: parent
property variant source1: srcCurrent
property variant source2: root.useNextForEffect ? srcNext : srcDummy
property real progress: root.transitionProgress
property real smoothness: root.edgeSmoothness
property real aspectRatio: root.width / root.height
property real centerX: root.discCenterX
property real centerY: root.discCenterY
property real fillMode: root.shaderFillMode
property vector4d fillColor: root.fillColor
property real imageWidth1: modelData.width
property real imageHeight1: modelData.height
property real imageWidth2: modelData.width
property real imageHeight2: modelData.height
property real screenWidth: modelData.width
property real screenHeight: modelData.height
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wp_disc.frag.qsb")
}
}
Component {
id: stripesComp
ShaderEffect {
anchors.fill: parent
property variant source1: srcCurrent
property variant source2: root.useNextForEffect ? srcNext : srcDummy
property real progress: root.transitionProgress
property real smoothness: root.edgeSmoothness
property real aspectRatio: root.width / root.height
property real stripeCount: root.stripesCount
property real angle: root.stripesAngle
property real fillMode: root.shaderFillMode
property vector4d fillColor: root.fillColor
property real imageWidth1: modelData.width
property real imageHeight1: modelData.height
property real imageWidth2: modelData.width
property real imageHeight2: modelData.height
property real screenWidth: modelData.width
property real screenHeight: modelData.height
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wp_stripes.frag.qsb")
}
}
Component {
id: irisComp
ShaderEffect {
anchors.fill: parent
property variant source1: srcCurrent
property variant source2: root.useNextForEffect ? srcNext : srcDummy
property real progress: root.transitionProgress
property real smoothness: root.edgeSmoothness
property real centerX: 0.5
property real centerY: 0.5
property real aspectRatio: root.width / root.height
property real fillMode: root.shaderFillMode
property vector4d fillColor: root.fillColor
property real imageWidth1: modelData.width
property real imageHeight1: modelData.height
property real imageWidth2: modelData.width
property real imageHeight2: modelData.height
property real screenWidth: modelData.width
property real screenHeight: modelData.height
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wp_iris_bloom.frag.qsb")
}
}
Component {
id: pixelateComp
ShaderEffect {
anchors.fill: parent
property variant source1: srcCurrent
property variant source2: root.useNextForEffect ? srcNext : srcDummy
property real progress: root.transitionProgress
property real smoothness: root.edgeSmoothness
property real fillMode: root.shaderFillMode
property vector4d fillColor: root.fillColor
property real imageWidth1: modelData.width
property real imageHeight1: modelData.height
property real imageWidth2: modelData.width
property real imageHeight2: modelData.height
property real screenWidth: modelData.width
property real screenHeight: modelData.height
property real centerX: root.discCenterX
property real centerY: root.discCenterY
property real aspectRatio: root.width / root.height
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wp_pixelate.frag.qsb")
}
}
Component {
id: portalComp
ShaderEffect {
anchors.fill: parent
property variant source1: srcCurrent
property variant source2: root.useNextForEffect ? srcNext : srcDummy
property real progress: root.transitionProgress
property real smoothness: root.edgeSmoothness
property real aspectRatio: root.width / root.height
property real centerX: root.discCenterX
property real centerY: root.discCenterY
property real fillMode: root.shaderFillMode
property vector4d fillColor: root.fillColor
property real imageWidth1: modelData.width
property real imageHeight1: modelData.height
property real imageWidth2: modelData.width
property real imageHeight2: modelData.height
property real screenWidth: modelData.width
property real screenHeight: modelData.height
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wp_portal.frag.qsb")
}
}
NumberAnimation {
id: transitionAnimation
target: root
property: "transitionProgress"
from: 0.0
to: 1.0
duration: root.actualTransitionType === "none" ? 0 : 1000
easing.type: Easing.InOutCubic
onFinished: {
if (nextWallpaper.source && nextWallpaper.status === Image.Ready) {
currentWallpaper.source = nextWallpaper.source;
}
root.useNextForEffect = false;
nextWallpaper.source = "";
root.transitionProgress = 0.0;
currentWallpaper.layer.enabled = false;
nextWallpaper.layer.enabled = false;
root.effectActive = false;
if (!root.pendingWallpaper)
return;
var pending = root.pendingWallpaper;
root.pendingWallpaper = "";
Qt.callLater(() => root.changeWallpaper(pending, true));
}
}
Loader {
id: overviewBlurLoader
anchors.fill: parent
active: root.overviewBlurActive
sourceComponent: MultiEffect {
anchors.fill: parent
source: effectLoader.active ? effectLoader.item : (parallaxLoader.active ? parallaxLoader.item : currentWallpaper)
blurEnabled: true
blur: 0.8
blurMax: 75
autoPaddingEnabled: false
}
}
}
}
}