mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
636 lines
22 KiB
QML
636 lines
22 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Wayland
|
|
import qs.Commons
|
|
import qs.Services.Compositor
|
|
import qs.Services.UI
|
|
|
|
Variants {
|
|
id: backgroundVariants
|
|
model: Quickshell.screens
|
|
|
|
delegate: Loader {
|
|
|
|
required property ShellScreen modelData
|
|
|
|
active: modelData && Settings.data.wallpaper.enabled
|
|
|
|
sourceComponent: PanelWindow {
|
|
id: root
|
|
|
|
// Internal state management
|
|
property string transitionType: "fade"
|
|
property real transitionProgress: 0
|
|
property bool isStartupTransition: true
|
|
|
|
readonly property real edgeSmoothness: Settings.data.wallpaper.transitionEdgeSmoothness
|
|
readonly property var allTransitions: WallpaperService.allTransitions
|
|
readonly property bool transitioning: transitionAnimation.running
|
|
|
|
// Wipe direction: 0=left, 1=right, 2=up, 3=down
|
|
property real wipeDirection: 0
|
|
|
|
// Disc
|
|
property real discCenterX: 0.5
|
|
property real discCenterY: 0.5
|
|
|
|
// Stripe
|
|
property real stripesCount: 16
|
|
property real stripesAngle: 0
|
|
|
|
// Used to debounce wallpaper changes
|
|
property string futureWallpaper: ""
|
|
// Track the original wallpaper path being transitioned to (before caching)
|
|
property string transitioningToOriginalPath: ""
|
|
|
|
// Fillmode default is "crop"
|
|
property real fillMode: WallpaperService.getFillModeUniform()
|
|
property vector4d fillColor: Qt.vector4d(Settings.data.wallpaper.fillColor.r, Settings.data.wallpaper.fillColor.g, Settings.data.wallpaper.fillColor.b, 1.0)
|
|
|
|
// Solid color mode - track whether current/next are solid colors
|
|
property bool isSolid1: false
|
|
property bool isSolid2: false
|
|
property color _solidColor1: Settings.data.wallpaper.solidColor
|
|
property color _solidColor2: Settings.data.wallpaper.solidColor
|
|
property vector4d solidColor1: Qt.vector4d(_solidColor1.r, _solidColor1.g, _solidColor1.b, 1.0)
|
|
property vector4d solidColor2: Qt.vector4d(_solidColor2.r, _solidColor2.g, _solidColor2.b, 1.0)
|
|
|
|
Component.onCompleted: setWallpaperInitial()
|
|
|
|
Component.onDestruction: {
|
|
transitionAnimation.stop();
|
|
startupTransitionTimer.stop();
|
|
debounceTimer.stop();
|
|
shaderLoader.active = false;
|
|
currentWallpaper.source = "";
|
|
nextWallpaper.source = "";
|
|
}
|
|
|
|
Connections {
|
|
target: Settings.data.wallpaper
|
|
function onFillModeChanged() {
|
|
fillMode = WallpaperService.getFillModeUniform();
|
|
}
|
|
}
|
|
|
|
// External state management
|
|
Connections {
|
|
target: WallpaperService
|
|
function onWallpaperChanged(screenName, path) {
|
|
if (screenName === modelData.name) {
|
|
requestPreprocessedWallpaper(path);
|
|
}
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: CompositorService
|
|
function onDisplayScalesChanged() {
|
|
if (isStartupTransition) {
|
|
return;
|
|
}
|
|
const currentPath = WallpaperService.getWallpaper(modelData.name);
|
|
if (currentPath) {
|
|
requestPreprocessedWallpaper(currentPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
color: "transparent"
|
|
screen: modelData
|
|
WlrLayershell.layer: WlrLayer.Background
|
|
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
|
WlrLayershell.namespace: "noctalia-wallpaper-" + (screen?.name || "unknown")
|
|
|
|
anchors {
|
|
bottom: true
|
|
top: true
|
|
right: true
|
|
left: true
|
|
}
|
|
|
|
Timer {
|
|
id: debounceTimer
|
|
interval: 333
|
|
running: false
|
|
repeat: false
|
|
onTriggered: changeWallpaper()
|
|
}
|
|
|
|
// Delay startup transition to ensure the compositor has mapped the window
|
|
Timer {
|
|
id: startupTransitionTimer
|
|
interval: 100
|
|
running: false
|
|
repeat: false
|
|
onTriggered: _executeStartupTransition()
|
|
}
|
|
|
|
Image {
|
|
id: currentWallpaper
|
|
|
|
source: ""
|
|
smooth: true
|
|
mipmap: false
|
|
visible: false
|
|
cache: true // Cached so Overview can share the same texture
|
|
asynchronous: true
|
|
onStatusChanged: {
|
|
if (status === Image.Error) {
|
|
Logger.w("Current wallpaper failed to load:", source);
|
|
}
|
|
}
|
|
}
|
|
|
|
Image {
|
|
id: nextWallpaper
|
|
|
|
property bool pendingTransition: false
|
|
|
|
source: ""
|
|
smooth: true
|
|
mipmap: false
|
|
visible: false
|
|
cache: false // Not cached - temporary during transitions
|
|
asynchronous: true
|
|
onStatusChanged: {
|
|
if (status === Image.Error) {
|
|
Logger.w("Next wallpaper failed to load:", source);
|
|
pendingTransition = false;
|
|
} else if (status === Image.Ready) {
|
|
if (pendingTransition) {
|
|
pendingTransition = false;
|
|
currentWallpaper.asynchronous = false;
|
|
transitionAnimation.start();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Dynamic shader loader - only loads the active transition shader
|
|
Loader {
|
|
id: shaderLoader
|
|
anchors.fill: parent
|
|
active: true
|
|
|
|
sourceComponent: {
|
|
switch (transitionType) {
|
|
case "wipe":
|
|
return wipeShaderComponent;
|
|
case "disc":
|
|
return discShaderComponent;
|
|
case "stripes":
|
|
return stripesShaderComponent;
|
|
case "fade":
|
|
case "none":
|
|
default:
|
|
return fadeShaderComponent;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fade or None transition shader component
|
|
Component {
|
|
id: fadeShaderComponent
|
|
ShaderEffect {
|
|
anchors.fill: parent
|
|
|
|
property variant source1: currentWallpaper
|
|
property variant source2: nextWallpaper
|
|
property real progress: root.transitionProgress
|
|
|
|
// Fill mode properties
|
|
property real fillMode: root.fillMode
|
|
property vector4d fillColor: root.fillColor
|
|
property real imageWidth1: source1.sourceSize.width
|
|
property real imageHeight1: source1.sourceSize.height
|
|
property real imageWidth2: source2.sourceSize.width
|
|
property real imageHeight2: source2.sourceSize.height
|
|
property real screenWidth: width
|
|
property real screenHeight: height
|
|
|
|
// Solid color mode
|
|
property real isSolid1: root.isSolid1 ? 1.0 : 0.0
|
|
property real isSolid2: root.isSolid2 ? 1.0 : 0.0
|
|
property vector4d solidColor1: root.solidColor1
|
|
property vector4d solidColor2: root.solidColor2
|
|
|
|
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_fade.frag.qsb")
|
|
}
|
|
}
|
|
|
|
// Wipe transition shader component
|
|
Component {
|
|
id: wipeShaderComponent
|
|
ShaderEffect {
|
|
anchors.fill: parent
|
|
|
|
property variant source1: currentWallpaper
|
|
property variant source2: nextWallpaper
|
|
property real progress: root.transitionProgress
|
|
property real smoothness: root.edgeSmoothness
|
|
property real direction: root.wipeDirection
|
|
|
|
// Fill mode properties
|
|
property real fillMode: root.fillMode
|
|
property vector4d fillColor: root.fillColor
|
|
property real imageWidth1: source1.sourceSize.width
|
|
property real imageHeight1: source1.sourceSize.height
|
|
property real imageWidth2: source2.sourceSize.width
|
|
property real imageHeight2: source2.sourceSize.height
|
|
property real screenWidth: width
|
|
property real screenHeight: height
|
|
|
|
// Solid color mode
|
|
property real isSolid1: root.isSolid1 ? 1.0 : 0.0
|
|
property real isSolid2: root.isSolid2 ? 1.0 : 0.0
|
|
property vector4d solidColor1: root.solidColor1
|
|
property vector4d solidColor2: root.solidColor2
|
|
|
|
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_wipe.frag.qsb")
|
|
}
|
|
}
|
|
|
|
// Disc reveal transition shader component
|
|
Component {
|
|
id: discShaderComponent
|
|
ShaderEffect {
|
|
anchors.fill: parent
|
|
|
|
property variant source1: currentWallpaper
|
|
property variant source2: nextWallpaper
|
|
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
|
|
|
|
// Fill mode properties
|
|
property real fillMode: root.fillMode
|
|
property vector4d fillColor: root.fillColor
|
|
property real imageWidth1: source1.sourceSize.width
|
|
property real imageHeight1: source1.sourceSize.height
|
|
property real imageWidth2: source2.sourceSize.width
|
|
property real imageHeight2: source2.sourceSize.height
|
|
property real screenWidth: width
|
|
property real screenHeight: height
|
|
|
|
// Solid color mode
|
|
property real isSolid1: root.isSolid1 ? 1.0 : 0.0
|
|
property real isSolid2: root.isSolid2 ? 1.0 : 0.0
|
|
property vector4d solidColor1: root.solidColor1
|
|
property vector4d solidColor2: root.solidColor2
|
|
|
|
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_disc.frag.qsb")
|
|
}
|
|
}
|
|
|
|
// Diagonal stripes transition shader component
|
|
Component {
|
|
id: stripesShaderComponent
|
|
ShaderEffect {
|
|
anchors.fill: parent
|
|
|
|
property variant source1: currentWallpaper
|
|
property variant source2: nextWallpaper
|
|
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
|
|
|
|
// Fill mode properties
|
|
property real fillMode: root.fillMode
|
|
property vector4d fillColor: root.fillColor
|
|
property real imageWidth1: source1.sourceSize.width
|
|
property real imageHeight1: source1.sourceSize.height
|
|
property real imageWidth2: source2.sourceSize.width
|
|
property real imageHeight2: source2.sourceSize.height
|
|
property real screenWidth: width
|
|
property real screenHeight: height
|
|
|
|
// Solid color mode
|
|
property real isSolid1: root.isSolid1 ? 1.0 : 0.0
|
|
property real isSolid2: root.isSolid2 ? 1.0 : 0.0
|
|
property vector4d solidColor1: root.solidColor1
|
|
property vector4d solidColor2: root.solidColor2
|
|
|
|
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_stripes.frag.qsb")
|
|
}
|
|
}
|
|
|
|
// Animation for the transition progress
|
|
NumberAnimation {
|
|
id: transitionAnimation
|
|
target: root
|
|
property: "transitionProgress"
|
|
from: 0.0
|
|
to: 1.0
|
|
duration: Settings.data.wallpaper.transitionDuration
|
|
easing.type: Easing.InOutCubic
|
|
onFinished: {
|
|
// Clear the tracking of what we're transitioning to
|
|
transitioningToOriginalPath = "";
|
|
|
|
// Transfer solid color state from next to current
|
|
isSolid1 = isSolid2;
|
|
_solidColor1 = _solidColor2;
|
|
|
|
// Assign new image to current BEFORE clearing to prevent flicker
|
|
const tempSource = nextWallpaper.source;
|
|
currentWallpaper.source = tempSource;
|
|
transitionProgress = 0.0;
|
|
|
|
// Now clear nextWallpaper after currentWallpaper has the new source
|
|
// Force complete cleanup to free texture memory
|
|
Qt.callLater(() => {
|
|
nextWallpaper.source = "";
|
|
isSolid2 = false;
|
|
Qt.callLater(() => {
|
|
currentWallpaper.asynchronous = true;
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------
|
|
function setWallpaperInitial() {
|
|
// On startup, defer assigning wallpaper until the services are ready
|
|
if (!WallpaperService || !WallpaperService.isInitialized) {
|
|
Qt.callLater(setWallpaperInitial);
|
|
return;
|
|
}
|
|
if (!ImageCacheService || !ImageCacheService.initialized) {
|
|
Qt.callLater(setWallpaperInitial);
|
|
return;
|
|
}
|
|
|
|
// Check if we're in solid color mode
|
|
if (Settings.data.wallpaper.useSolidColor) {
|
|
var solidPath = WallpaperService.createSolidColorPath(Settings.data.wallpaper.solidColor.toString());
|
|
futureWallpaper = solidPath;
|
|
performStartupTransition();
|
|
WallpaperService.wallpaperProcessingComplete(modelData.name, solidPath, "");
|
|
return;
|
|
}
|
|
|
|
const wallpaperPath = WallpaperService.getWallpaper(modelData.name);
|
|
|
|
// Check if the path is a solid color
|
|
if (WallpaperService.isSolidColorPath(wallpaperPath)) {
|
|
futureWallpaper = wallpaperPath;
|
|
performStartupTransition();
|
|
WallpaperService.wallpaperProcessingComplete(modelData.name, wallpaperPath, "");
|
|
return;
|
|
}
|
|
|
|
const compositorScale = CompositorService.getDisplayScale(modelData.name);
|
|
const targetWidth = Math.round(modelData.width * compositorScale);
|
|
const targetHeight = Math.round(modelData.height * compositorScale);
|
|
|
|
ImageCacheService.getLarge(wallpaperPath, targetWidth, targetHeight, function (cachedPath, success) {
|
|
if (success) {
|
|
futureWallpaper = cachedPath;
|
|
} else {
|
|
// Fallback to original
|
|
futureWallpaper = wallpaperPath;
|
|
}
|
|
performStartupTransition();
|
|
// Pass cached path for blur optimization (already resized)
|
|
WallpaperService.wallpaperProcessingComplete(modelData.name, wallpaperPath, success ? cachedPath : "");
|
|
});
|
|
}
|
|
|
|
// ------------------------------------------------------
|
|
function requestPreprocessedWallpaper(originalPath) {
|
|
// If we're already transitioning to this exact wallpaper, skip the request
|
|
if (transitioning && originalPath === transitioningToOriginalPath) {
|
|
return;
|
|
}
|
|
|
|
// Store the original path we're working towards
|
|
transitioningToOriginalPath = originalPath;
|
|
|
|
// Handle solid color paths - no preprocessing needed
|
|
if (WallpaperService.isSolidColorPath(originalPath)) {
|
|
futureWallpaper = originalPath;
|
|
debounceTimer.restart();
|
|
WallpaperService.wallpaperProcessingComplete(modelData.name, originalPath, "");
|
|
return;
|
|
}
|
|
|
|
const compositorScale = CompositorService.getDisplayScale(modelData.name);
|
|
const targetWidth = Math.round(modelData.width * compositorScale);
|
|
const targetHeight = Math.round(modelData.height * compositorScale);
|
|
|
|
ImageCacheService.getLarge(originalPath, targetWidth, targetHeight, function (cachedPath, success) {
|
|
// Ignore stale callback if we've moved on to a different wallpaper
|
|
if (originalPath !== transitioningToOriginalPath) {
|
|
return;
|
|
}
|
|
if (success) {
|
|
futureWallpaper = cachedPath;
|
|
} else {
|
|
futureWallpaper = originalPath;
|
|
}
|
|
debounceTimer.restart();
|
|
// Pass cached path for blur optimization (already resized)
|
|
WallpaperService.wallpaperProcessingComplete(modelData.name, originalPath, success ? cachedPath : "");
|
|
});
|
|
}
|
|
|
|
// ------------------------------------------------------
|
|
function setWallpaperImmediate(source) {
|
|
transitionAnimation.stop();
|
|
transitionProgress = 0.0;
|
|
|
|
// Check if this is a solid color
|
|
var isSolidSource = WallpaperService.isSolidColorPath(source);
|
|
isSolid1 = isSolidSource;
|
|
isSolid2 = false;
|
|
|
|
if (isSolidSource) {
|
|
var colorStr = WallpaperService.getSolidColor(source);
|
|
_solidColor1 = colorStr;
|
|
// Clear image sources for memory efficiency
|
|
currentWallpaper.source = "";
|
|
nextWallpaper.source = "";
|
|
return;
|
|
}
|
|
|
|
// Clear nextWallpaper completely to free texture memory
|
|
nextWallpaper.source = "";
|
|
nextWallpaper.sourceSize = undefined;
|
|
|
|
currentWallpaper.source = "";
|
|
|
|
Qt.callLater(() => {
|
|
currentWallpaper.source = source;
|
|
});
|
|
}
|
|
|
|
// ------------------------------------------------------
|
|
function setWallpaperWithTransition(source) {
|
|
// Check if this is a solid color transition
|
|
var isSolidSource = WallpaperService.isSolidColorPath(source);
|
|
|
|
// For solid colors, check if we're already showing the same color
|
|
if (isSolidSource && isSolid1) {
|
|
var newColor = WallpaperService.getSolidColor(source);
|
|
if (newColor === _solidColor1.toString()) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// For images, check if source matches
|
|
if (!isSolidSource && source === currentWallpaper.source) {
|
|
return;
|
|
}
|
|
|
|
// If we're already transitioning to this same wallpaper, skip
|
|
if (transitioning && source === nextWallpaper.source) {
|
|
return;
|
|
}
|
|
|
|
if (transitioning) {
|
|
// We are interrupting a transition - handle cleanup properly
|
|
transitionAnimation.stop();
|
|
transitionProgress = 0;
|
|
|
|
// Transfer next state to current
|
|
isSolid1 = isSolid2;
|
|
_solidColor1 = _solidColor2;
|
|
const newCurrentSource = nextWallpaper.source;
|
|
currentWallpaper.source = newCurrentSource;
|
|
|
|
// Now clear nextWallpaper after current has the new source
|
|
Qt.callLater(() => {
|
|
nextWallpaper.source = "";
|
|
isSolid2 = false;
|
|
|
|
// Now set the next wallpaper after a brief delay
|
|
Qt.callLater(() => {
|
|
_startTransitionTo(source, isSolidSource);
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
_startTransitionTo(source, isSolidSource);
|
|
}
|
|
|
|
// Helper to start transition to a new source
|
|
function _startTransitionTo(source, isSolidSource) {
|
|
isSolid2 = isSolidSource;
|
|
|
|
if (isSolidSource) {
|
|
var colorStr = WallpaperService.getSolidColor(source);
|
|
_solidColor2 = colorStr;
|
|
// No image to load, start transition immediately
|
|
nextWallpaper.source = "";
|
|
currentWallpaper.asynchronous = false;
|
|
transitionAnimation.start();
|
|
} else {
|
|
nextWallpaper.source = source;
|
|
if (nextWallpaper.status === Image.Ready) {
|
|
currentWallpaper.asynchronous = false;
|
|
transitionAnimation.start();
|
|
} else {
|
|
nextWallpaper.pendingTransition = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------
|
|
// Main method that actually trigger the wallpaper change
|
|
function changeWallpaper() {
|
|
// Get the transitionType from the settings
|
|
transitionType = Settings.data.wallpaper.transitionType;
|
|
|
|
if (transitionType == "random") {
|
|
var index = Math.floor(Math.random() * allTransitions.length);
|
|
transitionType = allTransitions[index];
|
|
}
|
|
|
|
// Ensure the transition type really exists
|
|
if (transitionType !== "none" && !allTransitions.includes(transitionType)) {
|
|
transitionType = "fade";
|
|
}
|
|
|
|
//Logger.i("Background", "New wallpaper: ", futureWallpaper, "On:", modelData.name, "Transition:", transitionType)
|
|
switch (transitionType) {
|
|
case "none":
|
|
setWallpaperImmediate(futureWallpaper);
|
|
break;
|
|
case "wipe":
|
|
wipeDirection = Math.random() * 4;
|
|
setWallpaperWithTransition(futureWallpaper);
|
|
break;
|
|
case "disc":
|
|
discCenterX = Math.random();
|
|
discCenterY = Math.random();
|
|
setWallpaperWithTransition(futureWallpaper);
|
|
break;
|
|
case "stripes":
|
|
stripesCount = Math.round(Math.random() * 20 + 4);
|
|
stripesAngle = Math.random() * 360;
|
|
setWallpaperWithTransition(futureWallpaper);
|
|
break;
|
|
default:
|
|
setWallpaperWithTransition(futureWallpaper);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------
|
|
// Dedicated function for startup animation
|
|
// Sets up transition params, then defers the actual animation
|
|
// to allow the compositor time to map the window.
|
|
function performStartupTransition() {
|
|
// Get the transitionType from the settings
|
|
transitionType = Settings.data.wallpaper.transitionType;
|
|
|
|
if (transitionType == "random") {
|
|
var index = Math.floor(Math.random() * allTransitions.length);
|
|
transitionType = allTransitions[index];
|
|
}
|
|
|
|
// Ensure the transition type really exists
|
|
if (transitionType !== "none" && !allTransitions.includes(transitionType)) {
|
|
transitionType = "fade";
|
|
}
|
|
|
|
// Pre-compute per-type params so the shader is ready
|
|
switch (transitionType) {
|
|
case "wipe":
|
|
wipeDirection = Math.random() * 4;
|
|
break;
|
|
case "disc":
|
|
// Force center origin for elegant startup animation
|
|
discCenterX = 0.5;
|
|
discCenterY = 0.5;
|
|
break;
|
|
case "stripes":
|
|
stripesCount = Math.round(Math.random() * 20 + 4);
|
|
stripesAngle = Math.random() * 360;
|
|
break;
|
|
}
|
|
|
|
// Defer the actual transition start so the compositor can map the window
|
|
startupTransitionTimer.start();
|
|
}
|
|
|
|
// Actually kick off the startup transition after the delay
|
|
function _executeStartupTransition() {
|
|
if (transitionType === "none") {
|
|
setWallpaperImmediate(futureWallpaper);
|
|
} else {
|
|
setWallpaperWithTransition(futureWallpaper);
|
|
}
|
|
// Mark startup transition complete
|
|
isStartupTransition = false;
|
|
}
|
|
}
|
|
}
|
|
}
|