Stabilize automatic tile layouts

This commit is contained in:
Marco Allegretti 2026-05-19 09:55:25 +02:00
parent e0e51d7ffd
commit c6bd37dfc3
2 changed files with 104 additions and 24 deletions

View file

@ -7,9 +7,10 @@
// - One persistent layout tree per output, keyed by output.name
// - Tree nodes are split groups or window leaves; leaf rects are absolute
// in-screen coordinates
// - On add/remove/reorder the tree is updated and then laid out for the
// affected screen, preserving the split structure instead of rebuilding a
// fresh linear BSP from window count
// - Automatic add/remove rebuilds a stable master-stack tree from window
// order, so normal app churn does not leave arbitrary nested splits behind
// - Explicit drag insertion still edits the tree directly because that is a
// deliberate user layout change
// - Drag detection uses interactiveMoveResizeStarted/Stepped/Finished
// - Dragging over a tile shows the exact directional split that will be
// created on drop
@ -25,6 +26,7 @@ Item {
// Configuration
readonly property int outerGap: 8
readonly property int innerGap: 8 // half applied to each edge 4px per tile
readonly property real stablePrimaryRatio: 0.58
// State
@ -258,6 +260,70 @@ Item {
return lastLeafKey(node.second) || lastLeafKey(node.first);
}
function orderedWindowsForScreen(outputName) {
const windows = [];
const seen = {};
const leaves = [];
collectLeaves(screenLayouts[outputName], leaves);
for (let i = 0; i < leaves.length; i++) {
const win = leaves[i].win;
const key = windowKey(win);
if (!key || seen[key] || !isTileable(win)) continue;
if (!win.output || win.output.name !== outputName) continue;
seen[key] = true;
windows.push(win);
}
return windows;
}
function appendWindowIfMissing(windows, win) {
const key = windowKey(win);
if (!key) return false;
for (let i = 0; i < windows.length; i++) {
if (windowKey(windows[i]) === key) {
return false;
}
}
windows.push(win);
return true;
}
function buildStableStack(windows, startIndex) {
const remaining = windows.length - startIndex;
if (remaining <= 0) return null;
if (remaining === 1) return makeLeaf(windows[startIndex]);
return makeSplit(
"horizontal",
makeLeaf(windows[startIndex]),
buildStableStack(windows, startIndex + 1),
1 / remaining
);
}
function buildStableLayout(windows) {
if (!windows || windows.length === 0) return null;
if (windows.length === 1) return makeLeaf(windows[0]);
if (windows.length === 2) {
return makeSplit("vertical", makeLeaf(windows[0]), makeLeaf(windows[1]), 0.5);
}
return makeSplit(
"vertical",
makeLeaf(windows[0]),
buildStableStack(windows, 1),
stablePrimaryRatio
);
}
function setStableLayout(outputName, windows) {
setScreenLayout(outputName, buildStableLayout(windows));
}
function containsLeaf(node, key) {
if (!node || !key) return false;
if (node.kind === "leaf") return windowKey(node.win) === key;
@ -325,24 +391,9 @@ Item {
}
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)));
const windows = orderedWindowsForScreen(outputName);
if (!appendWindowIfMissing(windows, win)) return;
setStableLayout(outputName, windows);
}
function removeWindowFromLayout(outputName, win) {
@ -350,9 +401,28 @@ Item {
const rootNode = screenLayouts[outputName];
if (!key || !rootNode) return false;
const result = removeLeaf(rootNode, key);
if (!result.leaf) return false;
setScreenLayout(outputName, result.node);
const leaves = [];
const seen = {};
const remaining = [];
let removed = false;
collectLeaves(rootNode, leaves);
for (let i = 0; i < leaves.length; i++) {
const leafWindow = leaves[i].win;
const leafKey = windowKey(leafWindow);
if (leafKey === key) {
removed = true;
continue;
}
if (!leafKey || seen[leafKey] || !isTileable(leafWindow)) continue;
if (!leafWindow.output || leafWindow.output.name !== outputName) continue;
seen[leafKey] = true;
remaining.push(leafWindow);
}
if (!removed) return false;
setStableLayout(outputName, remaining);
return true;
}

View file

@ -54,6 +54,16 @@ require_line "$effect_qml" "showPreview(\"insert\""
require_line "$effect_qml" "showPreview(\"float\""
require_line "$effect_qml" "showPreview(\"restore\""
require_line "$tiling_script" "readonly property real stablePrimaryRatio: 0.58"
require_line "$tiling_script" "function orderedWindowsForScreen(outputName)"
require_line "$tiling_script" "function buildStableStack(windows, startIndex)"
require_line "$tiling_script" "function buildStableLayout(windows)"
require_line "$tiling_script" "function setStableLayout(outputName, windows)"
require_line "$tiling_script" "setStableLayout(outputName, windows)"
require_line "$tiling_script" "setStableLayout(outputName, remaining)"
reject_line "$tiling_script" "targetKey = lastLeafKey(rootNode)"
reject_line "$tiling_script" "setScreenLayout(outputName, splitLeaf(rootNode, targetKey"
reject_line "$tiling_script" "KWinComponents.Workspace.showOutline(dragOutlineRect)"
reject_line "$tiling_script" "KWinComponents.Workspace.hideOutline()"