diff --git a/kwin/scripts/shift-tiling/contents/ui/main.qml b/kwin/scripts/shift-tiling/contents/ui/main.qml index ab0e5b4c..58610d37 100644 --- a/kwin/scripts/shift-tiling/contents/ui/main.qml +++ b/kwin/scripts/shift-tiling/contents/ui/main.qml @@ -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; } diff --git a/tests/check-dynamic-tiles-motion.sh b/tests/check-dynamic-tiles-motion.sh index 254a7699..39eb31b1 100644 --- a/tests/check-dynamic-tiles-motion.sh +++ b/tests/check-dynamic-tiles-motion.sh @@ -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()"