From 6129c9862448c79ea86a02cd48bc2beb9519fcf6 Mon Sep 17 00:00:00 2001 From: Lucas Oskorep Date: Mon, 2 Mar 2026 23:06:47 -0500 Subject: [PATCH] feat: adding support for multi-monitor window moving --- src/wm/container.ts | 26 ++++++++-- src/wm/monitor.ts | 4 +- src/wm/windowManager.ts | 111 ++++++++++++++++++++++++++-------------- 3 files changed, 95 insertions(+), 46 deletions(-) diff --git a/src/wm/container.ts b/src/wm/container.ts index 3d4a06d..4506731 100644 --- a/src/wm/container.ts +++ b/src/wm/container.ts @@ -55,7 +55,11 @@ export default class WindowContainer { this._splitRatios = equalRatios(this._tiledItems.length); } - private _addRatioForNewWindow(): void { + /** + * Proportionally shrink existing ratios to carve out space for a new item + * at the given index. If no index is supplied the ratio is appended at the end. + */ + private _addRatioForNewWindow(index?: number): void { const n = this._tiledItems.length; if (n <= 1) { this._splitRatios = [1.0]; @@ -66,7 +70,10 @@ export default class WindowContainer { const scaled = this._splitRatios.map(r => r * scale); const partialSum = scaled.reduce((a, b) => a + b, 0) + newRatio; scaled[scaled.length - 1] += (1.0 - partialSum); - this._splitRatios = [...scaled, newRatio]; + + const insertAt = index ?? scaled.length; + scaled.splice(insertAt, 0, newRatio); + this._splitRatios = scaled; } private _totalDimension(): number { @@ -193,10 +200,19 @@ export default class WindowContainer { } } - addWindow(winWrap: WindowWrapper): void { - this._tiledItems.push(winWrap); + /** + * Add a window to this container. + * If `index` is omitted the window is appended at the end. + * A negative index (e.g. -1) is treated as "append at end". + */ + addWindow(winWrap: WindowWrapper, index?: number): void { + const insertAt = (index === undefined || index < 0) + ? this._tiledItems.length + : Math.min(index, this._tiledItems.length); + + this._tiledItems.splice(insertAt, 0, winWrap); this._tiledWindowLookup.set(winWrap.getWindowId(), winWrap); - this._addRatioForNewWindow(); + this._addRatioForNewWindow(insertAt); if (this.isTabbed()) { // TODO: make it so that when tabs are added they are made the current active tab diff --git a/src/wm/monitor.ts b/src/wm/monitor.ts index dc6c9e2..adc9f2f 100644 --- a/src/wm/monitor.ts +++ b/src/wm/monitor.ts @@ -52,9 +52,9 @@ export default class Monitor { } } - addWindow(winWrap: WindowWrapper) { + addWindow(winWrap: WindowWrapper, index?: number) { const window_workspace = winWrap.getWindow().get_workspace().index(); - this._workspaces[window_workspace].addWindow(winWrap); + this._workspaces[window_workspace].addWindow(winWrap, index); } tileWindows(): void { diff --git a/src/wm/windowManager.ts b/src/wm/windowManager.ts index ff85c64..36c7061 100644 --- a/src/wm/windowManager.ts +++ b/src/wm/windowManager.ts @@ -566,11 +566,13 @@ export default class WindowManager implements IWindowManager { } /** - * Move (swap) the active window in the given direction within its container. + * Move the active window in the given direction. * * 1. Find the container holding the active window. - * 2. Ask the container to swap the window with its neighbour in that direction. - * 3. Re-tile to apply the new layout. + * 2. Try to swap within the container (adjacent neighbour). + * 3. If already at the container edge, move the window to the + * nearest monitor in that direction instead. + * 4. Re-tile to apply the new layout. */ public moveInDirection(direction: Direction): void { if (this._activeWindowId === null) { @@ -588,7 +590,10 @@ export default class WindowManager implements IWindowManager { if (swapped) { Logger.info(`Moved window ${this._activeWindowId} ${direction}`); this._tileMonitors(); + return; } + + this._moveWindowCrossMonitor(this._activeWindowId, direction); } /** @@ -640,40 +645,15 @@ export default class WindowManager implements IWindowManager { } /** - * When at the edge of a container, find the nearest window on the adjacent - * monitor in the given direction. - * - * Determines the adjacent monitor by comparing work-area centres: - * - LEFT: monitor whose work-area is to the left of the current one - * - RIGHT: monitor whose work-area is to the right of the current one - * - UP: monitor whose work-area is above the current one - * - DOWN: monitor whose work-area is below the current one - * - * On the target monitor, picks the edge-most window: - * - Navigating LEFT → last (rightmost) window of the target container - * - Navigating RIGHT → first (leftmost) window of the target container - * - Navigating UP → last (bottommost) window - * - Navigating DOWN → first (topmost) window + * Find the adjacent monitor in the given direction from a current monitor. + * Returns the monitor ID or null if none exists in that direction. */ - private _findCrossMonitorWindow(direction: Direction): number | null { - if (this._activeWindowId === null) return null; - - // Find which monitor the active window is on - let currentMonitorId: number | null = null; - for (const [monId, monitor] of this._monitors.entries()) { - if (monitor.getWindow(this._activeWindowId) !== undefined) { - currentMonitorId = monId; - break; - } - } - if (currentMonitorId === null) return null; - + private _findAdjacentMonitorId(currentMonitorId: number, direction: Direction): number | null { const currentMonitor = this._monitors.get(currentMonitorId)!; const currentArea = currentMonitor._workArea; const currentCenterX = currentArea.x + currentArea.width / 2; const currentCenterY = currentArea.y + currentArea.height / 2; - // Find the best adjacent monitor in the given direction let bestMonitorId: number | null = null; let bestDistance = Infinity; @@ -712,21 +692,74 @@ export default class WindowManager implements IWindowManager { } } - if (bestMonitorId === null) return null; + return bestMonitorId; + } - const targetMonitor = this._monitors.get(bestMonitorId)!; + /** + * Return the monitor ID that contains the given window, or null. + */ + private _findMonitorIdForWindow(windowId: number): number | null { + for (const [monId, monitor] of this._monitors.entries()) { + if (monitor.getWindow(windowId) !== undefined) return monId; + } + return null; + } + + /** + * When at the edge of a container, find the nearest window on the adjacent + * monitor in the given direction. + * + * On the target monitor, picks the edge-most window: + * - Navigating LEFT/UP → last (far-edge) leaf window + * - Navigating RIGHT/DOWN → first (near-edge) leaf window + */ + private _findCrossMonitorWindow(direction: Direction): number | null { + if (this._activeWindowId === null) return null; + + const currentMonitorId = this._findMonitorIdForWindow(this._activeWindowId); + if (currentMonitorId === null) return null; + + const targetMonitorId = this._findAdjacentMonitorId(currentMonitorId, direction); + if (targetMonitorId === null) return null; + + const targetMonitor = this._monitors.get(targetMonitorId)!; const activeWorkspaceIndex = global.workspace_manager.get_active_workspace().index(); if (activeWorkspaceIndex >= targetMonitor._workspaces.length) return null; const targetContainer = targetMonitor._workspaces[activeWorkspaceIndex]; if (targetContainer._tiledItems.length === 0) return null; - // Pick the window on the "entry edge" of the target container - if (direction === Direction.LEFT || direction === Direction.UP) { - return targetContainer._lastLeafWindowId(); - } else { - return targetContainer._firstLeafWindowId(); - } + return (direction === Direction.LEFT || direction === Direction.UP) + ? targetContainer._lastLeafWindowId() + : targetContainer._firstLeafWindowId(); + } + + /** + * Move a window to the adjacent monitor in the given direction. + * + * The window is inserted at the "entry edge" of the target container: + * - Moving RIGHT/DOWN → position 0 (near edge) + * - Moving LEFT/UP → end of the container (far edge) + */ + private _moveWindowCrossMonitor(windowId: number, direction: Direction): void { + const currentMonitorId = this._findMonitorIdForWindow(windowId); + if (currentMonitorId === null) return; + + const targetMonitorId = this._findAdjacentMonitorId(currentMonitorId, direction); + if (targetMonitorId === null) return; + + const currentMonitor = this._monitors.get(currentMonitorId)!; + const wrapped = currentMonitor.getWindow(windowId); + if (!wrapped) return; + + const targetMonitor = this._monitors.get(targetMonitorId)!; + const insertIndex = (direction === Direction.RIGHT || direction === Direction.DOWN) ? 0 : undefined; + + currentMonitor.removeWindow(wrapped); + targetMonitor.addWindow(wrapped, insertIndex); + + this._tileMonitors(); + Logger.info(`Moved window ${windowId} to monitor ${targetMonitorId} (${direction})`); } public printTreeStructure(): void {