Repository navigation
Conversation
On Mutter 51, unmaximizing a window that is not maximized is no longer a no-op: no maximize state changes, so meta_window_process_config() falls through to meta_window_move_resize() with the window's configured rect. Forge unmaximizes every window it moves, on every render. A tiled window whose configured rect lies elsewhere is therefore sent back there before each move to its tile. This shows with Wayland clients restored by session management (xdg_session_manager_v1, used by Chromium-based browsers): Mutter restores the window tiled on the monitor it was saved on, and the window then keeps bouncing between that tile and its Forge tile, often across monitors. Only unmaximize windows that are maximized on at least one axis. Side-tiled windows are maximized vertically, so they are still untiled, once. On GNOME 49+, unmaximize() is set_unmaximize_flags(BOTH), so a single call is enough. Tested on Ubuntu 26.10 with GNOME Shell 51.0 and Mutter 51.0 (Wayland), two 4K monitors at 150% scaling, Brave 156. This fix was proposed by Claude Code, an AI coding assistant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On GNOME Shell 51 (Wayland), unmaximizing or moving a window that Mutter is unmanaging can crash gnome-shell: its surface is already gone, so the toplevel lookup returns NULL. Flag windows on "unmanaging" and skip them in move() and in the delayed window-create callback, skip windows that have no workspace, and stop rendering once the window manager is disabled, since leaked signal handlers can still reach a disabled instance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With auto-split enabled, trackWindow() splits the focused window before attaching the new one. When the focused window was never tiled (no rect yet), Tree.split() threw "rect is null" (forge-ext#540) and the new window was never added to the tree: it stayed untiled, and toggling it floating then threw "focusNodeWindow is null". Only auto-split a focused node that has a rect and a parent, and stop the float toggle after moving the window when it has no node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The focus-on-hover loop runs every 16 ms and focused and raised the window under the pointer on every tick, even when it already had focus. Whenever Mutter focused another window (a newly opened one, a dialog, Alt+Tab), the loop took the focus back at the frame rate; each focus change re-rendered the tree, so windows flickered. The hit test also considered windows from other workspaces, minimized or not yet mapped windows, and menus, whose frame rects can still cover the pointer. Act only when the window under the pointer changes, like sloppy focus, skip windows that are not shown on the active workspace, focus only windows that can be tiled (menus still occlude what is below them), and do not refocus a window that already has focus. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
queueEvent() runs a single event every 220 ms. Every focus change queued a "focus-update" event, and a "raise-float" one for floating windows, so a burst of focus changes piled up hundreds of identical events that kept re-rendering the tree for minutes afterwards. Add Queue.has() and let events opt in to coalescing: a "focus-update" or "raise-float" event is not queued again while one is pending. Their callbacks read the current focus when they run, so the pending one covers the latest change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every render calls move() on every tiled window, on every workspace. For a window already in its tile this still unmaximized it, removed its actor transitions (cutting open and close animations short) and sent the client a new configure. With fractional scaling, clients round their size (1281 for a 1280 px tile), so such windows never matched exactly and were configured again on every render. Return early when the window is not maximized and its frame is within one pixel of the target rect. Also drop the move_frame() call before move_resize_frame(): on Wayland it applied the new position with the old size while the resize waited for the client, so the window moved twice and could briefly sit mostly on the neighbouring monitor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The float toggle (window-toggle-float) saves a per-window rule, with the window id, in windows.json. Three problems made it fail intermittently: - addFloatOverride() returned early when any float rule existed for the window class, including a rule for another window of that class, so a second terminal, say, could not be floated. - Window ids only hold for a session, but the per-window rules stayed in windows.json across sessions, where they blocked the case above and could match an unrelated window. - reloadWindowOverrides() dropped the per-window rules from the cache, so a reload triggered from the preferences tiled back the windows floated with the toggle. Only treat a class-wide rule, or this window's own rule, as "already floating"; purge per-window rules left by previous sessions at startup; and keep the current session's per-window rules when reloading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With org.gnome.mutter workspaces-only-on-primary (the GNOME default), Mutter keeps the windows of the non-primary monitors on all workspaces, and get_workspace() returns the active workspace for them. Forge keeps one layout per monitor and per workspace, so the windows of a secondary monitor were spread over several layouts that all tile the same screen: they overlapped, and each window joined the layout of whichever workspace was active when it opened, moved or changed monitor. Switching workspaces or dragging a window to that monitor then moved windows around, and drag-and-drop only saw the windows of the active workspace's layout. Add monWsKey(), which maps every non-primary monitor to a single layout stored under the first workspace when the setting is on, and use it wherever a monitor node is looked up. The drag-and-drop targets now come from the monitor nodes shown on the active workspace. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mutter emits window-created for a Wayland toplevel before its first
frame, and again each time a client unmaps its window: the toplevel is
reset and gets a new, unmapped MetaWindow, which may never be shown.
Forge tiled these windows right away, so:
- they took an empty slot in the layout;
- every render tried to move them, which Mutter refuses until the
client commits again ("meta_window_move_resize_internal: runtime check
failed: (priv->is_ready)", thousands of times per session here);
- once shown, the window sat in the slot reserved where the pointer was
when it was created, possibly on another monitor.
Track a Wayland window without a buffer only once it gets mapped
(notify::mapped), both for new windows and when tracking the existing
ones, and never move an unmapped Wayland window. X11 windows are tracked
as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
updateMetaWorkspaceMonitor() moves a window's node to the monitor the window is on whenever Mutter reports a monitor or workspace change. On Wayland a resize applies the new position before the new size, and Mutter may also put a window back at a saved position, so a tiled window can briefly sit mostly on the neighbouring monitor while Forge is placing it. Forge then re-tiled it on that monitor, the next resize crossed back, and the window bounced between monitors on each render. When a tiled window, not being dragged, shows up on another monitor than its node's, keep the node where it is: the next render puts the window back in its tile. Floating windows, windows being dragged, windows sent to another workspace and windows whose monitor is gone still follow the window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tree.split() pushed a window down into a new container by creating a new node for it, leaving the old node detached but with its parentNode still set. The new node lost the window's mode (a window being dragged turned back from GRAB_TILE to TILE) and state, and the old one stayed referenced, e.g. as the drag-and-drop target. When such a detached node was appended elsewhere, removeChild() looked it up by value, found the window's other node, and spliced at node.index, which is null for a detached node: splice(null, 1) removed the parent's first child, another window. The detached node then joined the tree as a second node of the same window, and every render moved the window to both tiles. Move the window's own node into the new container in split(), make removeChild() detach exactly the given node and clear its parentNode, and drop any duplicate window node before rendering. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix: tiling bugs on GNOME Shell 51 (Wayland, multiple monitors)
Summary
This PR fixes several bugs seen with Forge on GNOME Shell 51 under Wayland, mostly with two monitors:
Each fix is a separate commit, so they can be reviewed, reordered or dropped independently.
Fixes
move_frame()call beforemove_resize_frame()is dropped, as on Wayland it moved the window twice.priv->is_readywarnings.Tree.split()replaced the window's node with a new one. When the detached old node was later re-attached,removeChild()spliced at a null index, removing another window and leaving a duplicate node.Testing
How it was tested:
priv->is_readywarnings, no windows bouncing between monitors, and no JS errors.Related
Node.set rectinstead.These fixes were proposed by Claude Code, an AI coding assistant, and tested on the setup above.
Generated with Claude Code