// SPDX-FileCopyrightText: 2026 Marco Allegretti // SPDX-License-Identifier: EUPL-1.2 import QtQuick import org.kde.kwin as KWinComponents import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings KWinComponents.SceneEffect { id: effect visible: false readonly property int outerGap: 8 readonly property int floatEscapeMargin: 32 property var dragConnectedWindows: ({}) property var draggingWindow: null property rect dragSourceGeometry: Qt.rect(0, 0, 0, 0) property bool previewVisible: false property string previewMode: "" property string previewScreenName: "" property rect previewGeometry: Qt.rect(0, 0, 0, 0) function isActive() { return ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled && ShellSettings.Settings.dynamicTilingEnabled; } function windowKey(window) { return window && window.internalId !== undefined ? String(window.internalId) : ""; } function shouldIgnore(window) { if (!window || window.deleted) { return true; } if (!window.normalWindow || !window.maximizable || window.fullScreen) { return true; } return window.resourceClass === "xwaylandvideobridge"; } function validRect(geometry) { return geometry && geometry.width > 0 && geometry.height > 0; } function rectContainsPoint(geometry, point) { return validRect(geometry) && point.x >= geometry.x && point.x <= geometry.x + geometry.width && point.y >= geometry.y && point.y <= geometry.y + geometry.height; } function workRect(window) { if (!window || !window.output || window.desktops.length === 0) { return null; } return KWinComponents.Workspace.clientArea( KWinComponents.Workspace.MaximizeArea, window.output, window.desktops[0] ); } function outsideWorkArea(window, cursor) { const area = workRect(window); if (!area) { return false; } const expandedArea = Qt.rect( area.x - floatEscapeMargin, area.y - floatEscapeMargin, area.width + floatEscapeMargin * 2, area.height + floatEscapeMargin * 2 ); return !rectContainsPoint(expandedArea, cursor); } function findTileAtCursor(cursor, ignoredWindow) { const ignoredKey = windowKey(ignoredWindow); const windows = KWinComponents.Workspace.windows; let bestWindow = null; let bestGeometry = Qt.rect(0, 0, 0, 0); let bestArea = 0; for (let index = 0; index < windows.length; index++) { const candidate = windows[index]; if (shouldIgnore(candidate) || windowKey(candidate) === ignoredKey) { continue; } if (!ignoredWindow || !candidate.output || !ignoredWindow.output || candidate.output.name !== ignoredWindow.output.name) { continue; } const geometry = candidate.frameGeometry; if (!rectContainsPoint(geometry, cursor)) { continue; } const area = geometry.width * geometry.height; if (!bestWindow || area < bestArea) { bestWindow = candidate; bestGeometry = geometry; bestArea = area; } } return bestWindow ? bestGeometry : null; } function insetPreviewGeometry(geometry) { const inset = Math.max(2, Math.round(outerGap / 2)); return Qt.rect( geometry.x + inset, geometry.y + inset, Math.max(1, geometry.width - inset * 2), Math.max(1, geometry.height - inset * 2) ); } function showPreview(mode, geometry, screenName) { if (!isActive() || !validRect(geometry)) { hidePreview(); return; } previewMode = mode; previewScreenName = screenName; previewGeometry = insetPreviewGeometry(geometry); previewVisible = true; KWinComponents.Workspace.showOutline(previewGeometry); } function hidePreview() { previewVisible = false; draggingWindow = null; dragSourceGeometry = Qt.rect(0, 0, 0, 0); previewMode = ""; previewScreenName = ""; previewGeometry = Qt.rect(0, 0, 0, 0); KWinComponents.Workspace.hideOutline(); } function updatePreview(window, dragGeometry) { if (!isActive() || draggingWindow !== window || !window.output) { hidePreview(); return; } const cursor = KWinComponents.Workspace.cursorPos; const targetGeometry = findTileAtCursor(cursor, window); if (targetGeometry) { showPreview("swap", targetGeometry, window.output.name); return; } if (outsideWorkArea(window, cursor)) { showPreview("float", validRect(dragGeometry) ? dragGeometry : window.frameGeometry, window.output.name); return; } showPreview("restore", dragSourceGeometry, window.output.name); } function connectDragHandlers(window) { const key = windowKey(window); if (!key || dragConnectedWindows[key]) { return; } dragConnectedWindows[key] = true; window.interactiveMoveResizeStarted.connect(function() { if (!isActive() || shouldIgnore(window)) { return; } draggingWindow = window; dragSourceGeometry = window.frameGeometry; showPreview("restore", dragSourceGeometry, window.output ? window.output.name : ""); }); window.interactiveMoveResizeStepped.connect(function(geometry) { updatePreview(window, geometry); }); window.interactiveMoveResizeFinished.connect(function() { if (draggingWindow === window) { hidePreview(); } }); } function connectExistingWindows() { const windows = KWinComponents.Workspace.windows; for (let index = 0; index < windows.length; index++) { connectDragHandlers(windows[index]); } } Connections { target: KWinComponents.Workspace function onWindowAdded(window) { effect.connectDragHandlers(window); } function onWindowRemoved(window) { const key = effect.windowKey(window); if (key) { delete effect.dragConnectedWindows[key]; } if (effect.draggingWindow === window) { effect.hidePreview(); } } } Connections { target: ShellSettings.Settings function onConvergenceModeEnabledChanged() { if (!effect.isActive()) { effect.hidePreview(); } } function onGamingModeEnabledChanged() { if (!effect.isActive()) { effect.hidePreview(); } } function onDynamicTilingEnabledChanged() { if (!effect.isActive()) { effect.hidePreview(); } } } Component.onCompleted: connectExistingWindows() }