Improve dynamic tiling layout handling

Replace the flat per-screen tiling order with a persistent layout
tree so split orientation and window relationships survive retiles.
Derive the per-screen tile cache from the tree when applying
geometries.

Make drag insertion deterministic by mapping the cursor position
over a target tile to the nearest split direction and previewing
the exact half that will be assigned.

Retile on Workspace.onScreensChanged so screen rotation and output
changes recompute tiled window geometry from the updated work area,
and reconcile tiled windows that KWin moved to another output.
This commit is contained in:
Marco Allegretti 2026-05-17 18:26:28 +02:00
parent 40d0828343
commit a338369457

View file

@ -4,13 +4,15 @@
// SHIFT Dynamic Tiling KWin declarative script // SHIFT Dynamic Tiling KWin declarative script
// //
// Architecture: // Architecture:
// - One ScreenState per output, keyed by output.name // - One persistent layout tree per output, keyed by output.name
// - Each ScreenState holds an ordered list of TileNodes // - Tree nodes are split groups or window leaves; leaf rects are absolute
// - A TileNode is { win, rect } where rect is absolute in-screen coordinates // in-screen coordinates
// - On any change (add/remove/resize) the layout is recomputed from scratch // - On add/remove/reorder the tree is updated and then laid out for the
// for the affected screen using a BSP algorithm // affected screen, preserving the split structure instead of rebuilding a
// fresh linear BSP from window count
// - Drag detection uses interactiveMoveResizeStarted/Stepped/Finished // - Drag detection uses interactiveMoveResizeStarted/Stepped/Finished
// - Snap zones are the 6 screen-edge regions (left/right/top/corners) // - Dragging over a tile shows the exact directional split that will be
// created on drop
// - Gap: outer 8px on screen edges, inner 8px between tiles (4px each side) // - Gap: outer 8px on screen edges, inner 8px between tiles (4px each side)
import QtQuick import QtQuick
@ -23,12 +25,14 @@ Item {
// Configuration // Configuration
readonly property int outerGap: 8 readonly property int outerGap: 8
readonly property int innerGap: 8 // half applied to each edge 4px per tile readonly property int innerGap: 8 // half applied to each edge 4px per tile
readonly property int insertZoneThickness: 48
// State // State
// Per-screen tile list. Key: output.name Value: [{win, rect}] // Per-screen layout tree. Key: output.name. Value: split/leaf node.
// rect is a Qt.rect in absolute screen coordinates. property var screenLayouts: ({})
// Per-screen tile list derived from screenLayouts. Key: output.name.
// Value: [{win, rect}], where rect is absolute in-screen coordinates.
property var screenTiles: ({}) property var screenTiles: ({})
// Windows the user has manually floated (by UUID string). // Windows the user has manually floated (by UUID string).
@ -41,14 +45,14 @@ Item {
// Drag state. // Drag state.
// //
// Behaviour: dragging a tile and dropping it onto ANOTHER tile swaps or // Behaviour: dragging a tile over another tile previews the exact
// inserts based on the cursor zone. Dropping inside the work area but // left/right/up/down split that will be created on drop. Dropping inside
// outside another tile restores the original slot. Dropping outside the // the work area but outside another tile restores the original slot.
// work area floats the window, giving users a visible escape hatch from // Dropping outside the work area floats the window, giving users a visible
// the tiled layout. // escape hatch from the tiled layout.
property var draggingWindow: null property var draggingWindow: null
property bool dragOutlineActive: false property bool dragOutlineActive: false
property string dragDropMode: "" // "swap", "insert", "restore", or "float" property string dragDropMode: "" // "insert", "restore", or "float"
property rect dragOutlineRect: Qt.rect(0, 0, 0, 0) property rect dragOutlineRect: Qt.rect(0, 0, 0, 0)
// Reorder state kept stable while dragging so the rest of the layout // Reorder state kept stable while dragging so the rest of the layout
@ -56,8 +60,7 @@ Item {
property string dragSourceScreen: "" property string dragSourceScreen: ""
property int dragSourceIndex: -1 property int dragSourceIndex: -1
property rect dragSourceRect: Qt.rect(0, 0, 0, 0) property rect dragSourceRect: Qt.rect(0, 0, 0, 0)
property var dragSwapTarget: null // {screen, index, rect} of tile under cursor property var dragInsertTarget: null // {screen, key, direction, rect} preview slot
property var dragInsertTarget: null // {screen, index, insertIndex, rect} preview slot
readonly property int floatEscapeMargin: 32 readonly property int floatEscapeMargin: 32
// Deferred retile queue. // Deferred retile queue.
@ -177,72 +180,299 @@ Item {
return result; return result;
} }
// Binary-space-partition layout. function setScreenLayout(outputName, layout) {
// Splits `area` recursively for `n` windows. const layouts = Object.assign({}, screenLayouts);
// Returns an ordered array of Qt.rect (without gaps applied). if (layout) {
function bspRects(area, n) { layouts[outputName] = layout;
if (n <= 0) return [];
if (n === 1) return [area];
// Pick split axis: split the longer dimension
const splitHorizontally = area.width >= area.height;
const rects = [];
if (splitHorizontally) {
// Left half gets one window; right half gets (n-1)
const leftW = Math.round(area.width / 2);
const left = Qt.rect(area.x, area.y, leftW, area.height);
const right = Qt.rect(area.x + leftW, area.y, area.width - leftW, area.height);
rects.push(left);
const sub = bspRects(right, n - 1);
for (let i = 0; i < sub.length; i++) rects.push(sub[i]);
} else { } else {
// Top half gets one window; bottom half gets (n-1) delete layouts[outputName];
const topH = Math.round(area.height / 2); }
const top = Qt.rect(area.x, area.y, area.width, topH); screenLayouts = layouts;
const bottom = Qt.rect(area.x, area.y + topH, area.width, area.height - topH); }
rects.push(top);
const sub = bspRects(bottom, n - 1); function setScreenTiles(outputName, tiles) {
for (let i = 0; i < sub.length; i++) rects.push(sub[i]); const allTiles = Object.assign({}, screenTiles);
if (tiles && tiles.length > 0) {
allTiles[outputName] = tiles;
} else {
delete allTiles[outputName];
}
screenTiles = allTiles;
}
function makeLeaf(win) {
return { kind: "leaf", win: win, rect: Qt.rect(0, 0, 0, 0) };
}
function makeSplit(orientation, first, second, ratio) {
return {
kind: "split",
orientation: orientation,
first: first,
second: second,
ratio: ratio || 0.5,
rect: Qt.rect(0, 0, 0, 0)
};
}
function splitOrientation(rect) {
return rect && rect.width >= rect.height ? "vertical" : "horizontal";
}
function directionOrientation(direction, rect) {
if (direction === "left" || direction === "right") return "vertical";
if (direction === "up" || direction === "down") return "horizontal";
return splitOrientation(rect);
}
function splitRect(rect, orientation, ratio) {
const splitRatio = ratio || 0.5;
if (orientation === "vertical") {
const firstWidth = Math.round(rect.width * splitRatio);
return [
Qt.rect(rect.x, rect.y, firstWidth, rect.height),
Qt.rect(rect.x + firstWidth, rect.y, rect.width - firstWidth, rect.height)
];
}
const firstHeight = Math.round(rect.height * splitRatio);
return [
Qt.rect(rect.x, rect.y, rect.width, firstHeight),
Qt.rect(rect.x, rect.y + firstHeight, rect.width, rect.height - firstHeight)
];
}
function collectLeaves(node, leaves) {
if (!node) return;
if (node.kind === "leaf") {
leaves.push(node);
return;
}
collectLeaves(node.first, leaves);
collectLeaves(node.second, leaves);
}
function lastLeafKey(node) {
if (!node) return "";
if (node.kind === "leaf") return windowKey(node.win);
return lastLeafKey(node.second) || lastLeafKey(node.first);
}
function containsLeaf(node, key) {
if (!node || !key) return false;
if (node.kind === "leaf") return windowKey(node.win) === key;
return containsLeaf(node.first, key) || containsLeaf(node.second, key);
}
function splitLeaf(node, targetKey, newLeaf, direction, fallbackRect) {
if (!node || !targetKey || !newLeaf) return node;
if (node.kind === "leaf") {
if (windowKey(node.win) !== targetKey) return node;
const baseRect = validRect(node.rect) ? node.rect : fallbackRect;
const orientation = directionOrientation(direction, baseRect);
if (direction === "left" || direction === "up") {
return makeSplit(orientation, newLeaf, node, 0.5);
}
return makeSplit(orientation, node, newLeaf, 0.5);
}
node.first = splitLeaf(node.first, targetKey, newLeaf, direction, fallbackRect);
node.second = splitLeaf(node.second, targetKey, newLeaf, direction, fallbackRect);
return node;
}
function removeLeaf(node, key) {
if (!node || !key) return { node: node, leaf: null };
if (node.kind === "leaf") {
if (windowKey(node.win) === key) {
return { node: null, leaf: node };
}
return { node: node, leaf: null };
}
let first = removeLeaf(node.first, key);
if (first.leaf) {
node.first = first.node;
if (!node.first) return { node: node.second, leaf: first.leaf };
return { node: node, leaf: first.leaf };
}
let second = removeLeaf(node.second, key);
if (second.leaf) {
node.second = second.node;
if (!node.second) return { node: node.first, leaf: second.leaf };
return { node: node, leaf: second.leaf };
}
return { node: node, leaf: null };
}
function layoutTree(node, area, leaves) {
if (!node || !area) return;
node.rect = Qt.rect(area.x, area.y, area.width, area.height);
if (node.kind === "leaf") {
leaves.push(node);
return;
}
const rects = splitRect(area, node.orientation, node.ratio);
layoutTree(node.first, rects[0], leaves);
layoutTree(node.second, rects[1], leaves);
}
function insertWindowInLayout(outputName, win) {
const rootNode = screenLayouts[outputName];
const newLeaf = makeLeaf(win);
if (!rootNode) {
setScreenLayout(outputName, newLeaf);
return;
}
let targetKey = "";
const active = KWinComponents.Workspace.activeWindow;
if (active && active.output && active.output.name === outputName && windowKey(active) !== windowKey(win)) {
targetKey = windowKey(active);
}
if (!containsLeaf(rootNode, targetKey)) {
targetKey = lastLeafKey(rootNode);
}
setScreenLayout(outputName, splitLeaf(rootNode, targetKey, newLeaf, "", workRect(win)));
}
function removeWindowFromLayout(outputName, win) {
const key = windowKey(win);
const rootNode = screenLayouts[outputName];
if (!key || !rootNode) return false;
const result = removeLeaf(rootNode, key);
if (!result.leaf) return false;
setScreenLayout(outputName, result.node);
return true;
}
function moveWindowToSplit(target) {
if (!target || !draggingWindow || !dragSourceScreen) return;
const sourceKey = windowKey(draggingWindow);
if (!sourceKey || sourceKey === target.key) return;
const sourceRoot = screenLayouts[dragSourceScreen];
const detached = removeLeaf(sourceRoot, sourceKey);
if (!detached.leaf) return;
setScreenLayout(dragSourceScreen, detached.node);
const targetRoot = screenLayouts[target.screen];
if (!targetRoot || !containsLeaf(targetRoot, target.key)) {
setScreenLayout(dragSourceScreen, sourceRoot);
return;
}
setScreenLayout(target.screen, splitLeaf(targetRoot, target.key, detached.leaf, target.direction, target.rect));
retileScreen(dragSourceScreen);
if (target.screen !== dragSourceScreen) {
retileScreen(target.screen);
} }
return rects;
} }
// Recompute and apply layout for a single screen. // Recompute and apply layout for a single screen.
function retileScreen(outputName) { function retileScreen(outputName) {
const tiles = screenTiles[outputName]; const rootNode = screenLayouts[outputName];
if (!tiles || tiles.length === 0) return; if (!rootNode) {
setScreenTiles(outputName, []);
return;
}
const leaves = [];
collectLeaves(rootNode, leaves);
if (leaves.length === 0) {
setScreenTiles(outputName, []);
return;
}
// Get work area from the first window's output // Get work area from the first window's output
let area = null; let area = null;
for (let i = 0; i < tiles.length; i++) { for (let i = 0; i < leaves.length; i++) {
const r = workRect(tiles[i].win); const r = workRect(leaves[i].win);
if (r) { area = r; break; } if (r) { area = r; break; }
} }
if (!area) return; if (!area) return;
const n = tiles.length; const rawLeaves = [];
const rawRects = bspRects(area, n); layoutTree(rootNode, area, rawLeaves);
const rawRects = [];
for (let i = 0; i < rawLeaves.length; i++) {
rawRects.push(rawLeaves[i].rect);
}
const gappedRects = applyGaps(rawRects, area); const gappedRects = applyGaps(rawRects, area);
for (let i = 0; i < tiles.length; i++) { for (let i = 0; i < rawLeaves.length; i++) {
const win = tiles[i].win; const win = rawLeaves[i].win;
if (!win || win.deleted) continue; if (!win || win.deleted) continue;
const r = gappedRects[i]; const r = gappedRects[i];
tiles[i].rect = r; rawLeaves[i].rect = r;
win.frameGeometry = r; win.frameGeometry = r;
} }
// Trigger a binding update setScreenLayout(outputName, rootNode);
screenTiles[outputName] = tiles.slice(); setScreenTiles(outputName, rawLeaves.slice());
} }
// Retile all screens. // Retile all layout trees.
function retileAll() { function retileAll() {
for (const name in screenTiles) { for (const name in screenLayouts) {
retileScreen(name); retileScreen(name);
} }
} }
function scheduleRetileAll() {
for (const name in screenLayouts) {
scheduleRetile(name);
}
}
function screenNameForWindow(win) {
const key = windowKey(win);
if (!key) return "";
for (const name in screenLayouts) {
if (containsLeaf(screenLayouts[name], key)) {
return name;
}
}
return "";
}
function reconcileWindowOutput(win) {
if (!isTileable(win)) return;
connectDragHandlers(win);
const output = win.output;
if (!output) return;
const currentName = screenNameForWindow(win);
const targetName = output.name;
if (currentName !== "" && currentName !== targetName) {
removeWindowFromLayout(currentName, win);
scheduleRetile(currentName);
}
if (!containsLeaf(screenLayouts[targetName], windowKey(win))) {
insertWindowInLayout(targetName, win);
}
if (win.maximizable) win.setMaximize(false, false);
win.noBorder = false;
scheduleRetile(targetName);
}
// Add a window to its screen's tile list and retile. // Add a window to its screen's tile list and retile.
function addWindow(win) { function addWindow(win) {
if (!isTileable(win)) return; if (!isTileable(win)) return;
@ -253,18 +483,16 @@ Item {
if (!output) return; if (!output) return;
const name = output.name; const name = output.name;
if (!screenTiles[name]) {
screenTiles[name] = [];
}
// Avoid duplicates // Avoid duplicates
const tiles = screenTiles[name]; const key = windowKey(win);
if (containsLeaf(screenLayouts[name], key)) return;
const tiles = screenTiles[name] || [];
for (let i = 0; i < tiles.length; i++) { for (let i = 0; i < tiles.length; i++) {
if (windowKey(tiles[i].win) === windowKey(win)) return; if (windowKey(tiles[i].win) === key) return;
} }
tiles.push({ win: win, rect: Qt.rect(0, 0, 0, 0) }); insertWindowInLayout(name, win);
screenTiles[name] = tiles;
// Un-maximize now so the exclusive-zone Wayland roundtrip begins; // Un-maximize now so the exclusive-zone Wayland roundtrip begins;
// retileScreen runs 200 ms later when MaximizeArea has settled. // retileScreen runs 200 ms later when MaximizeArea has settled.
@ -277,19 +505,13 @@ Item {
// Remove a window from its screen's tile list and retile. // Remove a window from its screen's tile list and retile.
function removeWindow(win) { function removeWindow(win) {
if (!win) return; if (!win) return;
const output = win.output;
const name = output ? output.name : null;
// Search all screens (window may have been moved) // Search all layout trees (window may have been moved or queued before
for (const sName in screenTiles) { // the deferred tile list has refreshed).
const tiles = screenTiles[sName]; for (const sName in screenLayouts) {
for (let i = 0; i < tiles.length; i++) { if (removeWindowFromLayout(sName, win)) {
if (windowKey(tiles[i].win) === windowKey(win)) { retileScreen(sName);
tiles.splice(i, 1); return;
screenTiles[sName] = tiles;
retileScreen(sName);
return;
}
} }
} }
} }
@ -411,6 +633,19 @@ Item {
if (key) delete root.dragConnectedWindows[key]; if (key) delete root.dragConnectedWindows[key];
root.removeWindow(win); root.removeWindow(win);
} }
function onScreensChanged() {
if (!root.isConvergence()) {
return;
}
const wins = KWinComponents.Workspace.windows;
for (let i = 0; i < wins.length; i++) {
root.reconcileWindowOutput(wins[i]);
}
root.scheduleRetileAll();
}
} }
Connections { Connections {
@ -425,12 +660,14 @@ Item {
} }
} else { } else {
// Clear all tiles the convergentwindows script will re-maximize // Clear all tiles the convergentwindows script will re-maximize
screenLayouts = {};
screenTiles = {}; screenTiles = {};
} }
} }
function onGamingModeEnabledChanged() { function onGamingModeEnabledChanged() {
if (ShellSettings.Settings.gamingModeEnabled) { if (ShellSettings.Settings.gamingModeEnabled) {
screenLayouts = {};
screenTiles = {}; screenTiles = {};
} else if (isConvergence()) { } else if (isConvergence()) {
const wins = KWinComponents.Workspace.windows; const wins = KWinComponents.Workspace.windows;
@ -448,6 +685,7 @@ Item {
} }
} else { } else {
// Tiling turned off leave windows where they are. // Tiling turned off leave windows where they are.
screenLayouts = {};
screenTiles = {}; screenTiles = {};
} }
} }
@ -483,82 +721,39 @@ Item {
if (!r || r.width <= 0 || r.height <= 0) continue; if (!r || r.width <= 0 || r.height <= 0) continue;
if (cursor.x >= r.x && cursor.x <= r.x + r.width && if (cursor.x >= r.x && cursor.x <= r.x + r.width &&
cursor.y >= r.y && cursor.y <= r.y + r.height) { cursor.y >= r.y && cursor.y <= r.y + r.height) {
return { screen: sName, index: i, rect: r }; return { screen: sName, index: i, rect: r, key: windowKey(t.win), win: t.win };
} }
} }
} }
return null; return null;
} }
function tileInsertPosition(cursor, rect) { function tileInsertDirection(cursor, rect) {
if (!validRect(rect)) return ""; if (!validRect(rect)) return "";
const zone = Math.min(insertZoneThickness, Math.max(16, Math.floor(Math.min(rect.width, rect.height) / 4)));
const left = Math.abs(cursor.x - rect.x); const left = Math.abs(cursor.x - rect.x);
const right = Math.abs((rect.x + rect.width) - cursor.x); const right = Math.abs((rect.x + rect.width) - cursor.x);
const top = Math.abs(cursor.y - rect.y); const top = Math.abs(cursor.y - rect.y);
const bottom = Math.abs((rect.y + rect.height) - cursor.y); const bottom = Math.abs((rect.y + rect.height) - cursor.y);
const nearest = Math.min(left, right, top, bottom); const nearest = Math.min(left, right, top, bottom);
if (nearest > zone) return ""; if (nearest === left) return "left";
if (nearest === left || nearest === top) return "before"; if (nearest === right) return "right";
return "after"; if (nearest === top) return "up";
} return "down";
function insertIndexForTarget(target, position) {
if (!target) return -1;
let targetIndex = target.index;
if (dragSourceScreen === target.screen && dragSourceIndex >= 0 && dragSourceIndex < targetIndex) {
targetIndex--;
}
return position === "before" ? targetIndex : targetIndex + 1;
} }
function previewInsertRect(win, target, position) { function previewInsertRect(win, target, position) {
if (!target || !target.rect || dragSourceIndex < 0) return null; if (!target || !validRect(target.rect) || dragSourceIndex < 0) return null;
const tiles = screenTiles[target.screen]; const orientation = directionOrientation(position, target.rect);
if (!tiles || !tiles[target.index]) return null; const rects = splitRect(target.rect, orientation, 0.5);
if (position === "left" || position === "up") return rects[0];
const area = workRect(tiles[target.index].win || win); return rects[1];
if (!area) return null;
const count = tiles.length + (dragSourceScreen === target.screen ? 0 : 1);
const insertIndex = insertIndexForTarget(target, position);
if (insertIndex < 0 || insertIndex >= count) return null;
const rawRects = bspRects(area, count);
const gappedRects = applyGaps(rawRects, area);
return gappedRects[insertIndex] || null;
} }
function insertDraggedWindow(target) { function insertDraggedWindow(target) {
if (!target || dragSourceIndex < 0 || !dragSourceScreen) return; moveWindowToSplit(target);
if (dragSourceScreen === target.screen) {
const tiles = screenTiles[dragSourceScreen] ? screenTiles[dragSourceScreen].slice() : null;
if (!tiles || !tiles[dragSourceIndex]) return;
const node = tiles.splice(dragSourceIndex, 1)[0];
const insertIndex = Math.max(0, Math.min(target.insertIndex, tiles.length));
tiles.splice(insertIndex, 0, node);
screenTiles[dragSourceScreen] = tiles;
retileScreen(dragSourceScreen);
return;
}
const sourceTiles = screenTiles[dragSourceScreen] ? screenTiles[dragSourceScreen].slice() : null;
const targetTiles = screenTiles[target.screen] ? screenTiles[target.screen].slice() : null;
if (!sourceTiles || !targetTiles || !sourceTiles[dragSourceIndex]) return;
const node = sourceTiles.splice(dragSourceIndex, 1)[0];
const insertIndex = Math.max(0, Math.min(target.insertIndex, targetTiles.length));
targetTiles.splice(insertIndex, 0, node);
screenTiles[dragSourceScreen] = sourceTiles;
screenTiles[target.screen] = targetTiles;
retileScreen(dragSourceScreen);
retileScreen(target.screen);
} }
function validRect(rect) { function validRect(rect) {
@ -615,7 +810,6 @@ Item {
function resetDragState() { function resetDragState() {
clearDragOutline(); clearDragOutline();
dragSwapTarget = null;
dragInsertTarget = null; dragInsertTarget = null;
dragSourceScreen = ""; dragSourceScreen = "";
dragSourceIndex = -1; dragSourceIndex = -1;
@ -628,28 +822,21 @@ Item {
const target = findTileAtCursor(cursor, win); const target = findTileAtCursor(cursor, win);
if (target) { if (target) {
const position = tileInsertPosition(cursor, target.rect); const position = tileInsertDirection(cursor, target.rect);
const insertRect = position !== "" ? previewInsertRect(win, target, position) : null; const insertRect = previewInsertRect(win, target, position);
if (validRect(insertRect)) { if (validRect(insertRect)) {
dragSwapTarget = null;
dragInsertTarget = { dragInsertTarget = {
screen: target.screen, screen: target.screen,
index: target.index, key: target.key,
insertIndex: insertIndexForTarget(target, position), direction: position,
rect: insertRect, rect: insertRect,
position: position position: position
}; };
showDragOutline("insert", insertRect); showDragOutline("insert", insertRect);
return; return;
} }
dragSwapTarget = target;
dragInsertTarget = null;
showDragOutline("swap", target.rect);
return;
} }
dragSwapTarget = null;
dragInsertTarget = null; dragInsertTarget = null;
if (outsideWorkArea(win, cursor)) { if (outsideWorkArea(win, cursor)) {
showDragOutline("float", validRect(geo) ? geo : win.frameGeometry); showDragOutline("float", validRect(geo) ? geo : win.frameGeometry);
@ -662,11 +849,10 @@ Item {
if (!isConvergence()) return; if (!isConvergence()) return;
draggingWindow = win; draggingWindow = win;
clearDragOutline(); clearDragOutline();
dragSwapTarget = null;
dragInsertTarget = null; dragInsertTarget = null;
dragSourceRect = Qt.rect(0, 0, 0, 0); dragSourceRect = Qt.rect(0, 0, 0, 0);
// Remember the source slot so we can swap or insert on drop. // Remember the source slot so we can insert on drop.
// The tile stays in screenTiles[] during the drag so the rest of // The tile stays in screenTiles[] during the drag so the rest of
// the layout doesn't shuffle. // the layout doesn't shuffle.
const slot = findTileSlot(win); const slot = findTileSlot(win);
@ -712,33 +898,6 @@ Item {
insertDraggedWindow(dragInsertTarget); insertDraggedWindow(dragInsertTarget);
} }
// Dropped on another tile swap source/target slots.
else if (dragSwapTarget && dragSourceScreen && dragSourceIndex >= 0) {
const sScreen = dragSourceScreen;
const sIdx = dragSourceIndex;
const tScreen = dragSwapTarget.screen;
const tIdx = dragSwapTarget.index;
const sTiles = screenTiles[sScreen];
const tTiles = screenTiles[tScreen];
if (sTiles && tTiles && sTiles[sIdx] && tTiles[tIdx]) {
const a = sTiles[sIdx];
const b = tTiles[tIdx];
if (sScreen === tScreen) {
sTiles[sIdx] = b;
sTiles[tIdx] = a;
screenTiles[sScreen] = sTiles.slice();
retileScreen(sScreen);
} else {
sTiles[sIdx] = b;
tTiles[tIdx] = a;
screenTiles[sScreen] = sTiles.slice();
screenTiles[tScreen] = tTiles.slice();
retileScreen(sScreen);
retileScreen(tScreen);
}
}
}
// Dropped elsewhere restore the source tile to its original slot. // Dropped elsewhere restore the source tile to its original slot.
// (KWin's native quick-tile may have moved the window; retileScreen // (KWin's native quick-tile may have moved the window; retileScreen
// sets frameGeometry back to the BSP rect so the layout stays intact.) // sets frameGeometry back to the BSP rect so the layout stays intact.)