From 6f5343d0908993e1934342c716cdf5037eac5359 Mon Sep 17 00:00:00 2001 From: Abdul Mateen Date: Fri, 13 Mar 2026 22:47:16 +0000 Subject: [PATCH] feat(window): live resize neighboring tiled windows during mouse drag Previously, neighboring tiled windows would only update their position and size after the mouse button was released, causing a jarring snap effect at the end of every resize operation. On Wayland, Mutter suppresses size-changed and position-changed signals during active grab operations, so the existing _handleResizing logic never fired during the drag. Additionally, this.move() exits early when metaWindow.grabbed is true, blocking any neighbor repositioning. Fix by introducing a GLib.timeout_add polling loop (~60fps) that starts on grab-op-begin for resize grabs and stops on grab-op-end: - Each tick calls _handleResizing to recalculate sibling percentages - initRect is updated per-tick so deltas are frame-relative, preventing percent accumulation across ticks - Neighbors are moved via move_resize_frame directly, bypassing the grabbed guard in this.move() - A lastWidth/lastHeight guard skips processing when the window size has not changed, also preventing double-processing on X11 where signals still fire during grabs - Only the affected container subtree is reprocessed via processNode, not the entire tree Tested on Wayland (GNOME 49) and confirmed working on X11. Closes #511 --- lib/extension/window.js | 94 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 85 insertions(+), 9 deletions(-) diff --git a/lib/extension/window.js b/lib/extension/window.js index 0509dcc..d4abe33 100644 --- a/lib/extension/window.js +++ b/lib/extension/window.js @@ -103,13 +103,8 @@ export class WindowManager extends GObject.Object { let wmId = metaWindow.get_id(); for (let override of overrides) { - // ignore already floating and find correct window instance - if (override.wmClass === wmClass && override.mode === "float" && !override.wmTitle) { - if (withWmId && override.wmId !== wmId) { - continue; - } - return; - } + // if the window is already floating + if (override.wmClass === wmClass && override.mode === "float" && !override.wmTitle) return; } overrides.push({ wmClass: wmClass, @@ -120,7 +115,6 @@ export class WindowManager extends GObject.Object { // Save the updated overrides back to the ConfigManager currentProps.overrides = overrides; this.ext.configMgr.windowProps = currentProps; - this.windowProps = currentProps; } removeFloatOverride(metaWindow, withWmId) { @@ -141,7 +135,6 @@ export class WindowManager extends GObject.Object { // Save the updated overrides back to the ConfigManager currentProps.overrides = overrides; this.ext.configMgr.windowProps = currentProps; - this.windowProps = currentProps; } toggleFloatingMode(action, metaWindow) { @@ -2453,10 +2446,61 @@ export class WindowManager extends GObject.Object { focusNodeWindow.initGrabOp = grabOp; focusNodeWindow.initRect = Utils.removeGapOnRect(frameRect, gaps); + + // Start live-resize polling loop for resize grabs + if (focusNodeWindow.grabMode === GRAB_TYPES.RESIZING) { + this._startLiveResizeLoop(focusNodeWindow); + } + } + } + + _startLiveResizeLoop(focusNodeWindow) { + this._stopLiveResizeLoop(); + + // Cache gaps once — they don't change during a resize + const gaps = this.calculateGaps(focusNodeWindow); + let lastWidth = focusNodeWindow.initRect?.width; + let lastHeight = focusNodeWindow.initRect?.height; + + this._liveResizeSrcId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 16, () => { + const metaWindow = focusNodeWindow.nodeValue; + if (!metaWindow || !focusNodeWindow.grabMode) { + this._liveResizeSrcId = 0; + return GLib.SOURCE_REMOVE; + } + + const frameRect = metaWindow.get_frame_rect(); + const currentRect = Utils.removeGapOnRect(frameRect, gaps); + + // Skip if size hasn't changed — also prevents double-processing on X11 + // where size-changed signals fire alongside this loop + if (currentRect.width === lastWidth && currentRect.height === lastHeight) { + return GLib.SOURCE_CONTINUE; + } + lastWidth = currentRect.width; + lastHeight = currentRect.height; + + this._handleResizing(focusNodeWindow); + + // Update initRect so next tick delta is relative to current frame, + // not the grab start (prevents percent accumulation) + focusNodeWindow.initRect = currentRect; + + this._liveResizeNeighbors(focusNodeWindow); + + return GLib.SOURCE_CONTINUE; + }); + } + + _stopLiveResizeLoop() { + if (this._liveResizeSrcId) { + GLib.Source.remove(this._liveResizeSrcId); + this._liveResizeSrcId = 0; } } _handleGrabOpEnd(_display, _metaWindow, grabOp) { + this._stopLiveResizeLoop(); this.unfreezeRender(); let focusMetaWindow = this.focusMetaWindow; if (!focusMetaWindow) return; @@ -2665,6 +2709,38 @@ export class WindowManager extends GObject.Object { } } + /** + * During a mouse-drag resize, immediately re-layout all tiled windows + * EXCEPT the one currently being dragged (GNOME owns its position). + * Bypasses this.move() which is blocked by metaWindow.grabbed on Wayland. + */ + _liveResizeNeighbors(draggingNodeWindow) { + const draggingMetaWin = draggingNodeWindow.nodeValue; + + // Only reprocess the affected container subtree, not the entire tree + const parentNode = draggingNodeWindow.parentNode; + if (parentNode) { + this.tree.processNode(parentNode); + } + + // Move all tiled windows except the one being dragged + const tiledWindows = this.tree.getNodeByType(NODE_TYPES.WINDOW); + tiledWindows.forEach((nodeWin) => { + if (nodeWin.nodeValue === draggingMetaWin) return; // GNOME owns this + if (nodeWin.isFloat()) return; + if (!nodeWin.renderRect) return; + const r = nodeWin.renderRect; + if (r.width > 0 && r.height > 0) { + // Call move_resize_frame directly — this.move() bails out because + // metaWindow.grabbed is true for all windows during a Wayland grab + const actor = nodeWin.nodeValue.get_compositor_private(); + if (!actor) return; + actor.remove_all_transitions(); + nodeWin.nodeValue.move_resize_frame(true, r.x, r.y, r.width, r.height); + } + }); + } + _handleMoving(focusNodeWindow) { if (!focusNodeWindow || focusNodeWindow.mode !== WINDOW_MODES.GRAB_TILE) return;