From 773ac9c72424183a65630232ba580970d19832a5 Mon Sep 17 00:00:00 2001 From: defia Date: Sun, 9 Aug 2026 11:01:42 +0800 Subject: [PATCH] fix(pane): avoid white flash when closing a split --- Glint/Pane/PaneSurfaceRepresentable.swift | 41 +++++++++++++++++++-- GlintTests/PerformanceRegressionTests.swift | 26 +++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/Glint/Pane/PaneSurfaceRepresentable.swift b/Glint/Pane/PaneSurfaceRepresentable.swift index 2c0c720..c01aa96 100644 --- a/Glint/Pane/PaneSurfaceRepresentable.swift +++ b/Glint/Pane/PaneSurfaceRepresentable.swift @@ -17,6 +17,17 @@ enum SurfaceHostClaimPolicy { isSameHost: Bool) -> Bool { isSameHost || !currentHostIsAttached || candidateGeneration >= currentGeneration } + + static func shouldDeferUntilAfterCommit(candidateGeneration: UInt64, + currentGeneration: UInt64, + currentHostExists: Bool, + currentHostIsAttached: Bool, + isSameHost: Bool) -> Bool { + !isSameHost && + currentHostExists && + !currentHostIsAttached && + candidateGeneration < currentGeneration + } } /// Hosts a stable, store-owned `GhosttySurfaceView` inside a fresh container @@ -145,14 +156,38 @@ struct PaneSurfaceRepresentable: NSViewRepresentable { GhosttyManager.shared.applyTerminalBacking(to: container.layer) } - private func attach(_ surface: GhosttySurfaceView, to container: NoDragContainerView) { + private func attach(_ surface: GhosttySurfaceView, + to container: NoDragContainerView, + isPostCommitRetry: Bool = false) { let currentHost = surface.paneHostView - guard SurfaceHostClaimPolicy.shouldClaim( + if !isPostCommitRetry, + SurfaceHostClaimPolicy.shouldDeferUntilAfterCommit( + candidateGeneration: container.hostGeneration, + currentGeneration: surface.paneHostGeneration, + currentHostExists: currentHost != nil, + currentHostIsAttached: currentHost?.window != nil, + isSameHost: currentHost === container + ) { + // During a split collapse the incoming host can claim the surface + // just before SwiftUI gives the outgoing tree one last update. + // The incoming container is not in the window yet, so the old host + // would otherwise mistake it for abandoned and steal the surface + // back. Resolve that ambiguity after the current commit: the new + // host will be attached if it survived, or still detached if the + // older host genuinely needs to recover it. + DispatchQueue.main.async { + guard container.window != nil, isPaneVisible() else { return } + attach(surface, to: container, isPostCommitRetry: true) + } + return + } + let shouldClaim = SurfaceHostClaimPolicy.shouldClaim( candidateGeneration: container.hostGeneration, currentGeneration: surface.paneHostGeneration, currentHostIsAttached: currentHost?.window != nil, isSameHost: currentHost === container - ) else { return } + ) + guard shouldClaim else { return } surface.paneHostView = container surface.paneHostGeneration = container.hostGeneration diff --git a/GlintTests/PerformanceRegressionTests.swift b/GlintTests/PerformanceRegressionTests.swift index e1ccc06..7736b52 100644 --- a/GlintTests/PerformanceRegressionTests.swift +++ b/GlintTests/PerformanceRegressionTests.swift @@ -187,6 +187,32 @@ final class PerformanceRegressionTests: XCTestCase { )) } + func testOlderOutgoingHostDefersWhileNewerClaimIsPreCommit() { + XCTAssertTrue(SurfaceHostClaimPolicy.shouldDeferUntilAfterCommit( + candidateGeneration: 10, + currentGeneration: 11, + currentHostExists: true, + currentHostIsAttached: false, + isSameHost: false + )) + + // Once the incoming host survives the commit and attaches, the stale + // outgoing host must not steal the surface back. + XCTAssertFalse(SurfaceHostClaimPolicy.shouldDeferUntilAfterCommit( + candidateGeneration: 10, + currentGeneration: 11, + currentHostExists: true, + currentHostIsAttached: true, + isSameHost: false + )) + XCTAssertFalse(SurfaceHostClaimPolicy.shouldClaim( + candidateGeneration: 10, + currentGeneration: 11, + currentHostIsAttached: true, + isSameHost: false + )) + } + func testNewestHostWinsDeterministicOutgoingIncomingOutgoingRace() { let outgoingHost = NSView() let incomingHost = NSView()