// SimpleDwindle
// KWin 6.8 - Minimal chronological Dwindle
-const VERSION = "0.3.5";
+const VERSION = "0.4.0";
// ------------------------------------------------------------
// Configuration
function insertWindow(layout, w) {
if (!layout.root) {
layout.root = leaf(w);
- layout.last = w;
+ layout.last = layout.root;
return;
}
- let target = findLeaf(layout.root, layout.last);
+ let target = layout.last;
if (!target)
target = rightmost(layout.root);
target.first = leaf(oldWindow, target);
target.second = leaf(w, target);
- layout.last = w;
+ layout.last = target.second;
}
function removeNode(layout, w) {
grandparent.second = sibling;
}
- if (layout.last === w) {
- const lastNode = rightmost(layout.root);
- layout.last = lastNode
- ? lastNode.window
- : null;
+ // A removed leaf may be the growth tip, or its sibling may
+ // replace an ancestor containing the growth tip.
+ let tip = layout.last;
+ let tipRemoved = false;
+
+ while (tip) {
+ if (tip === parent) {
+ tipRemoved = true;
+ break;
+ }
+ tip = tip.parent;
}
+ if (tipRemoved)
+ layout.last = rightmost(layout.root);
+
return true;
}
+function normalizeSplits(node, depth = 0) {
+ if (!node || node.window)
+ return;
+
+ node.vertical = (depth % 2 === 0);
+
+ normalizeSplits(node.first, depth + 1);
+ normalizeSplits(node.second, depth + 1);
+}
+
// ------------------------------------------------------------
// Geometry
// ------------------------------------------------------------
applyNode(layout.root, area);
}
+// ------------------------------------------------------------
+// Spatial swap (tiled windows only)
+// ------------------------------------------------------------
+
+// Use tree-assigned rectangles, not asynchronous frameGeometry updates.
+function collectLeaves(node, rect, result) {
+ if (!node)
+ return;
+
+ if (node.window) {
+ result.push({ node: node, rect: rect });
+ return;
+ }
+
+ if (node.vertical) {
+ const half = rect.width / 2;
+ collectLeaves(node.first, {
+ x: rect.x, y: rect.y,
+ width: half, height: rect.height
+ }, result);
+ collectLeaves(node.second, {
+ x: rect.x + half, y: rect.y,
+ width: rect.width - half, height: rect.height
+ }, result);
+ } else {
+ const half = rect.height / 2;
+ collectLeaves(node.first, {
+ x: rect.x, y: rect.y,
+ width: rect.width, height: half
+ }, result);
+ collectLeaves(node.second, {
+ x: rect.x, y: rect.y + half,
+ width: rect.width, height: rect.height - half
+ }, result);
+ }
+}
+
+function findSwapNeighbor(w, direction) {
+ const layout = findWindowLayout(w);
+ if (!layout)
+ return null;
+
+ const area = workspace.clientArea(
+ KWin.MaximizeArea, layout.output, layout.desktop
+ );
+ const leaves = [];
+ collectLeaves(layout.root, area, leaves);
+
+ const source = leaves.find(item => item.node.window === w);
+ if (!source)
+ return null;
+
+ const a = source.rect;
+ const ax = a.x + a.width / 2;
+ const ay = a.y + a.height / 2;
+ let best = null;
+ let bestScore = null;
+
+ for (const item of leaves) {
+ const candidate = item.node.window;
+ if (candidate === w || candidate.fullScreen || candidate.minimized)
+ continue;
+
+ const b = item.rect;
+ const bx = b.x + b.width / 2;
+ const by = b.y + b.height / 2;
+ const dx = bx - ax;
+ const dy = by - ay;
+
+ if ((direction === "left" && dx >= 0) ||
+ (direction === "right" && dx <= 0) ||
+ (direction === "up" && dy >= 0) ||
+ (direction === "down" && dy <= 0))
+ continue;
+
+ const horizontal = direction === "left" || direction === "right";
+ const overlap = horizontal
+ ? Math.max(0, Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y))
+ : Math.max(0, Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x));
+
+ // Prefer windows sharing a boundary in the requested direction.
+ if (overlap <= 0)
+ continue;
+
+ const edgeDistance = horizontal
+ ? (direction === "left" ? a.x - (b.x + b.width) : b.x - (a.x + a.width))
+ : (direction === "up" ? a.y - (b.y + b.height) : b.y - (a.y + a.height));
+
+ if (edgeDistance < -0.001)
+ continue;
+
+ const centerDistance = dx * dx + dy * dy;
+ const score = [edgeDistance, -overlap, centerDistance];
+ if (!bestScore ||
+ score[0] < bestScore[0] ||
+ (score[0] === bestScore[0] && score[1] < bestScore[1]) ||
+ (score[0] === bestScore[0] && score[1] === bestScore[1] && score[2] < bestScore[2])) {
+ best = item;
+ bestScore = score;
+ }
+ }
+
+ return best ? best.node : null;
+}
+
+function swapDirection(direction) {
+ const w = workspace.activeWindow;
+ if (!w || !contains(managed, w) || w.fullScreen || w.minimized)
+ return;
+
+ const layout = findWindowLayout(w);
+ const neighbor = findSwapNeighbor(w, direction);
+ if (!layout || !neighbor)
+ return;
+
+ const current = findLeaf(layout.root, w);
+ if (!current)
+ return;
+
+ const other = neighbor.window;
+ current.window = other;
+ neighbor.window = w;
+
+ // layout.last points to a leaf, so swapping cannot move the growth tip.
+ arrange(layout);
+ workspace.activeWindow = w;
+ log("SWAPPED " + direction + ": " + w.caption + " <-> " + other.caption);
+}
+
// ------------------------------------------------------------
// Window states
// ------------------------------------------------------------
if (layout) {
removeNode(layout, w);
+ normalizeSplits(layout.root);
arrange(layout);
}
toggleFloating
);
+registerShortcut("SimpleDwindleSwapLeft", "SimpleDwindle: Swap Left", "Meta+Shift+H", function() { swapDirection("left"); });
+registerShortcut("SimpleDwindleSwapDown", "SimpleDwindle: Swap Down", "Meta+Shift+J", function() { swapDirection("down"); });
+registerShortcut("SimpleDwindleSwapUp", "SimpleDwindle: Swap Up", "Meta+Shift+K", function() { swapDirection("up"); });
+registerShortcut("SimpleDwindleSwapRight", "SimpleDwindle: Swap Right", "Meta+Shift+L", function() { swapDirection("right"); });
+
registerShortcut(
"SimpleDwindleListWindows",
"SimpleDwindle: List Windows",