Files
arthur-os/default/quickshell/omarchy-shell/plugins/background/Background.qml
T

334 lines
9.5 KiB
QML

import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import QtQuick
import qs.Commons as NoctaliaCommons
Item {
id: root
property string omarchyPath: ""
property var shell: null
property var manifest: null
readonly property string home: Quickshell.env("HOME")
readonly property string currentBackgroundLink: home + "/.config/omarchy/current/background"
property string currentBackground: ""
property string displayedBackground: ""
property string incomingBackground: ""
property string oldBackground: ""
property bool finishingTransition: false
property int backgroundVersion: 0
property int revealStartedVersion: -1
property int pendingThemeVersion: -1
property string pendingColorsRaw: ""
property string pendingShellRaw: ""
property real revealProgress: 1
function imageUrl(path) {
if (!path) return ""
return "file://" + path
}
function refreshBackground() {
if (!readlinkProc.running) readlinkProc.running = true
}
function setBackground(path, instant) {
transitionBackground("", path, instant)
}
function transitionBackground(fromPath, path, instant) {
path = String(path || "").trim()
fromPath = String(fromPath || "").trim()
if (!path || path === currentBackground) return
currentBackground = path
backgroundVersion += 1
revealStartedVersion = -1
revealAnimation.stop()
finishingTransition = false
if (instant || !displayedBackground) {
oldBackground = ""
incomingBackground = ""
displayedBackground = path
revealProgress = 1
return
}
oldBackground = fromPath || displayedBackground
incomingBackground = path
revealProgress = 0
}
function decodePayload(payload) {
try { return Qt.atob(String(payload || "")) } catch (e) { return "" }
}
function setPendingTheme(colorsB64, shellB64) {
pendingColorsRaw = decodePayload(colorsB64)
pendingShellRaw = decodePayload(shellB64)
pendingThemeVersion = backgroundVersion
}
function applyPendingTheme() {
if (pendingThemeVersion !== backgroundVersion) return
NoctaliaCommons.Color.resumeThemeReloads()
NoctaliaCommons.Color.loadColors(pendingColorsRaw)
NoctaliaCommons.Color.loadShell(pendingShellRaw)
pendingThemeVersion = -1
pendingColorsRaw = ""
pendingShellRaw = ""
}
function transitionBackgroundWithTheme(fromPath, path, colorsB64, shellB64) {
transitionBackground(fromPath, path, false)
setPendingTheme(colorsB64, shellB64)
if (!incomingBackground || revealProgress >= 1) applyPendingTheme()
}
function startReveal(panel) {
if (!incomingBackground) return
panel.maskReady = true
if (revealStartedVersion === backgroundVersion) return
revealStartedVersion = backgroundVersion
applyPendingTheme()
revealAnimation.restart()
}
function openSelector() {
if (!bgSwitchProc.running) bgSwitchProc.running = true
}
function openThemeSwitcher() {
if (!themeSwitchProc.running) themeSwitchProc.running = true
}
Process {
id: bgSwitchProc
command: ["bash", "-lc", "background=$(omarchy-theme-bg-switcher); [[ -n $background ]] && omarchy-theme-bg-set \"$background\""]
onExited: root.refreshBackground()
}
Process {
id: themeSwitchProc
command: ["bash", "-lc", "theme=$(omarchy-theme-switcher); [[ -n $theme ]] && omarchy-theme-set \"$theme\""]
onExited: root.refreshBackground()
}
Process {
id: readlinkProc
command: ["readlink", "-f", root.currentBackgroundLink]
stdout: StdioCollector {
onStreamFinished: root.setBackground(String(text || "").trim(), false)
}
}
IpcHandler {
target: "background"
function refresh(): void {
root.refreshBackground()
}
function set(path: string): void {
root.setBackground(path, false)
}
function setInstant(path: string): void {
root.setBackground(path, true)
}
function transition(fromPath: string, path: string): void {
root.transitionBackground(fromPath, path, false)
}
function themeTransition(fromPath: string, path: string, colorsB64: string, shellB64: string): void {
root.transitionBackgroundWithTheme(fromPath, path, colorsB64, shellB64)
}
}
Timer {
interval: 100
running: true
repeat: true
onTriggered: root.refreshBackground()
}
NumberAnimation {
id: revealAnimation
target: root
property: "revealProgress"
from: 0
to: 1
duration: 420
easing.type: Easing.InOutCubic
onFinished: {
if (root.incomingBackground) {
root.displayedBackground = root.incomingBackground
root.finishingTransition = true
}
root.revealProgress = 1
}
}
Component.onCompleted: refreshBackground()
Variants {
model: Quickshell.screens
PanelWindow {
id: panel
required property var modelData
screen: modelData
visible: true
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
property bool maskReady: false
WlrLayershell.namespace: "omarchy-background"
WlrLayershell.layer: WlrLayer.Background
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
exclusionMode: ExclusionMode.Ignore
Image {
id: base
anchors.fill: parent
source: root.imageUrl(root.displayedBackground)
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: false
onStatusChanged: {
if (status === Image.Ready && root.finishingTransition) {
root.incomingBackground = ""
root.oldBackground = ""
root.finishingTransition = false
}
}
}
Image {
id: incomingFrame
anchors.fill: parent
source: root.imageUrl(root.incomingBackground)
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: false
visible: root.incomingBackground !== "" && status === Image.Ready
opacity: root.revealProgress >= 1 ? 1 : 0.001
onStatusChanged: if (status === Image.Ready && root.incomingBackground) revealCanvas.prepareImage()
}
Image {
id: oldFrame
anchors.fill: parent
source: root.imageUrl(root.oldBackground)
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: false
visible: root.oldBackground !== "" && root.revealProgress < 1
onStatusChanged: if (status === Image.Ready && root.incomingBackground) revealCanvas.requestPaint()
}
Canvas {
id: revealCanvas
anchors.fill: parent
visible: root.incomingBackground !== "" && root.revealProgress < 1 && incomingFrame.status === Image.Ready && oldFrame.status === Image.Ready
opacity: panel.maskReady ? 1 : 0
renderTarget: Canvas.FramebufferObject
renderStrategy: Canvas.Immediate
readonly property real slant: -0.18
function prepareImage() {
var src = root.imageUrl(root.incomingBackground)
if (!src) return
if (isImageLoaded(src)) {
requestPaint()
} else if (!isImageLoading(src)) {
loadImage(src)
}
}
onImageLoaded: function(url) {
if (url === root.imageUrl(root.incomingBackground)) requestPaint()
}
onPaint: {
var ctx = getContext("2d")
ctx.reset()
ctx.clearRect(0, 0, width, height)
var src = root.imageUrl(root.incomingBackground)
if (!src || incomingFrame.status !== Image.Ready || root.revealProgress >= 1) return
if (!isImageLoaded(src)) {
prepareImage()
return
}
var iw = incomingFrame.sourceSize.width
var ih = incomingFrame.sourceSize.height
if (iw <= 0 || ih <= 0 || width <= 0 || height <= 0) return
var sx = 0
var sy = 0
var sw = iw
var sh = ih
if (iw / ih > width / height) {
sw = ih * width / height
sx = (iw - sw) / 2
} else {
sh = iw * height / width
sy = (ih - sh) / 2
}
var centerTop = width / 2 - slant * height / 2
var centerBottom = width / 2 + slant * height / 2
var reach = width / 2 + Math.abs(slant) * height / 2 + 4
var spread = reach * root.revealProgress
var leftTop = centerTop - spread
var leftBottom = centerBottom - spread
var rightTop = centerTop + spread
var rightBottom = centerBottom + spread
ctx.save()
ctx.beginPath()
ctx.moveTo(leftTop, 0)
ctx.lineTo(rightTop, 0)
ctx.lineTo(rightBottom, height)
ctx.lineTo(leftBottom, height)
ctx.closePath()
ctx.clip()
ctx.drawImage(src, sx, sy, sw, sh, 0, 0, width, height)
ctx.restore()
if (!panel.maskReady && root.incomingBackground && root.revealProgress === 0) {
Qt.callLater(function() { root.startReveal(panel) })
}
}
Connections {
target: root
function onRevealProgressChanged() { revealCanvas.requestPaint() }
function onIncomingBackgroundChanged() {
panel.maskReady = false
revealCanvas.prepareImage()
}
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: function(mouse) {
if (mouse.button === Qt.RightButton) root.openThemeSwitcher()
else root.openSelector()
mouse.accepted = true
}
}
}
}
}