Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Fix on-window-detected floating rule causing brief tiling flash
When exec-and-forget launches an app, multiple notifications fire
rapidly (kAXWindowCreatedNotification, didActivateApplicationNotification,
etc.), each calling scheduleRefreshSession(). Due to Swift cooperative
cancellation, a session already running past checkCancellation() continues
executing even after being "cancelled" by a newer session. Two sessions
end up interleaved on @mainactor at every await suspension point.

Race 1 (tiling flash): Session A places the new window in the tiling
container (synchronous), then suspends inside tryOnWindowDetected() while
evaluating rules via async AX calls. Session B gets CPU, runs
layoutWorkspaces(), sees the window in the tiling container, and calls
setAxFrame() -- producing a visible flash at the tiled position.

Race 2 (sibling shift): layoutTiles() distributes tile space among all
children by weight. While the new window sits in the tiling container
awaiting rule evaluation, every concurrent layoutWorkspaces() call shrinks
sibling windows to reserve a tile slot for it -- a slot that is never
visually filled -- causing siblings to briefly shift.

Fix: add isAwaitingOnWindowDetected flag to Window, set when the window
enters allWindowsMap, cleared via defer after tryOnWindowDetected completes
(covers both the normal path and the closedWindowsCache restore path).
layoutWorkspaces() skips flagged windows entirely so no concurrent session
can setAxFrame() the window while its rules are still being evaluated.
layoutTiles() also excludes flagged windows from space allocation so
siblings keep their full tile width during detection.
Session A's own layoutWorkspaces() runs after the flag is cleared and
positions the window correctly in its final container the first time.

#2016
  • Loading branch information
willzeng274 committed Mar 25, 2026
commit b41287af5a8a3270885c8ef1fb065efe5ea2a322
14 changes: 10 additions & 4 deletions Sources/AppBundle/layout/layoutRecursive.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ extension TreeNode {
lastAppliedLayoutVirtualRect = virtual
try await workspace.rootTilingContainer.layoutRecursive(point, width: width, height: height, virtual: virtual, context)
for window in workspace.children.filterIsInstance(of: Window.self) {
if window.isAwaitingOnWindowDetected { continue }
window.lastAppliedLayoutPhysicalRect = nil
window.lastAppliedLayoutVirtualRect = nil
try await window.layoutFloatingWindow(context)
}
case .window(let window):
if window.isAwaitingOnWindowDetected { break }
if window.windowId != currentlyManipulatedWithMouseWindowId {
lastAppliedLayoutVirtualRect = virtual
if window.isFullscreen && window == context.workspace.rootTilingContainer.mostRecentWindowRecursive {
Expand Down Expand Up @@ -108,11 +110,15 @@ extension TilingContainer {
var point = point
var virtualPoint = virtual.topLeftCorner

guard let delta = ((orientation == .h ? width : height) - CGFloat(children.sumOfDouble { $0.getWeight(orientation) }))
.div(children.count) else { return }
// Exclude windows awaiting on-window-detected from space allocation --
// their slot would otherwise shrink siblings while they sit invisible.
let effectiveChildren = children.filter { ($0 as? Window)?.isAwaitingOnWindowDetected != true }

let lastIndex = children.indices.last
for (i, child) in children.enumerated() {
guard let delta = ((orientation == .h ? width : height) - CGFloat(effectiveChildren.sumOfDouble { $0.getWeight(orientation) }))
.div(effectiveChildren.count) else { return }

let lastIndex = effectiveChildren.indices.last
for (i, child) in effectiveChildren.enumerated() {
child.setWeight(orientation, child.getWeight(orientation) + delta)
let rawGap = context.resolvedGaps.inner.get(orientation).toDouble()
// Gaps. Consider 4 cases:
Expand Down
2 changes: 2 additions & 0 deletions Sources/AppBundle/tree/MacWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ final class MacWindow: Window {
// atomic synchronous section
if let existing = allWindowsMap[windowId] { return existing }
let window = MacWindow(windowId, macApp, lastFloatingSize: rect?.size, parent: data.parent, adaptiveWeight: data.adaptiveWeight, index: data.index)
window.isAwaitingOnWindowDetected = true
allWindowsMap[windowId] = window

try await debugWindowsIfRecording(window)
defer { window.isAwaitingOnWindowDetected = false }
if try await !restoreClosedWindowsCacheIfNeeded(newlyDetectedWindow: window) {
try await tryOnWindowDetected(window)
}
Expand Down
1 change: 1 addition & 0 deletions Sources/AppBundle/tree/Window.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ open class Window: TreeNode, Hashable {
let windowId: UInt32
let app: any AbstractApp
var lastFloatingSize: CGSize?
var isAwaitingOnWindowDetected: Bool = false
var isFullscreen: Bool = false
var noOuterGapsInFullscreen: Bool = false
var layoutReason: LayoutReason = .standard
Expand Down
Loading