Prevent stale host visibility thrash after tab move
This commit is contained in:
parent
5c584dd7f7
commit
b0b73e8878
3 changed files with 91 additions and 3 deletions
|
|
@ -5061,6 +5061,16 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
Coordinator()
|
||||
}
|
||||
|
||||
static func shouldApplyImmediateHostedStateUpdate(
|
||||
hostWindowAttached: Bool,
|
||||
hostedViewHasSuperview: Bool,
|
||||
isBoundToCurrentHost: Bool
|
||||
) -> Bool {
|
||||
if !hostWindowAttached { return true }
|
||||
if isBoundToCurrentHost { return true }
|
||||
return !hostedViewHasSuperview
|
||||
}
|
||||
|
||||
func makeNSView(context: Context) -> NSView {
|
||||
let container = HostContainerView()
|
||||
container.wantsLayer = false
|
||||
|
|
@ -5103,8 +5113,6 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
|
||||
// Keep the surface lifecycle and handlers updated even if we defer re-parenting.
|
||||
hostedView.attachSurface(terminalSurface)
|
||||
hostedView.setVisibleInUI(isVisibleInUI)
|
||||
hostedView.setActive(isActive)
|
||||
hostedView.setInactiveOverlay(
|
||||
color: inactiveOverlayColor,
|
||||
opacity: CGFloat(inactiveOverlayOpacity),
|
||||
|
|
@ -5139,7 +5147,8 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
coordinator.attachGeneration += 1
|
||||
let generation = coordinator.attachGeneration
|
||||
|
||||
if let host = nsView as? HostContainerView {
|
||||
let hostContainer = nsView as? HostContainerView
|
||||
if let host = hostContainer {
|
||||
host.onDidMoveToWindow = { [weak host, weak hostedView, weak coordinator] in
|
||||
guard let host, let hostedView, let coordinator else { return }
|
||||
guard coordinator.attachGeneration == generation else { return }
|
||||
|
|
@ -5190,6 +5199,28 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
let hostWindowAttached = hostContainer?.window != nil
|
||||
let isBoundToCurrentHost = hostContainer.map { host in
|
||||
TerminalWindowPortalRegistry.isHostedView(hostedView, boundTo: host)
|
||||
} ?? true
|
||||
let shouldApplyImmediateHostedState = Self.shouldApplyImmediateHostedStateUpdate(
|
||||
hostWindowAttached: hostWindowAttached,
|
||||
hostedViewHasSuperview: hostedView.superview != nil,
|
||||
isBoundToCurrentHost: isBoundToCurrentHost
|
||||
)
|
||||
|
||||
if shouldApplyImmediateHostedState {
|
||||
hostedView.setVisibleInUI(isVisibleInUI)
|
||||
hostedView.setActive(isActive)
|
||||
} else {
|
||||
// Preserve portal entry visibility while a stale host is still receiving SwiftUI updates.
|
||||
// The currently bound host remains authoritative for immediate visible/active state.
|
||||
TerminalWindowPortalRegistry.updateEntryVisibility(
|
||||
for: hostedView,
|
||||
visibleInUI: isVisibleInUI
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
static func dismantleNSView(_ nsView: NSView, coordinator: Coordinator) {
|
||||
|
|
|
|||
|
|
@ -934,6 +934,12 @@ final class WindowTerminalPortal: NSObject {
|
|||
entriesByHostedId[hostedId] = entry
|
||||
}
|
||||
|
||||
func isHostedViewBoundToAnchor(withId hostedId: ObjectIdentifier, anchorView: NSView) -> Bool {
|
||||
guard let entry = entriesByHostedId[hostedId],
|
||||
let boundAnchor = entry.anchorView else { return false }
|
||||
return boundAnchor === anchorView
|
||||
}
|
||||
|
||||
func bind(hostedView: GhosttySurfaceScrollView, to anchorView: NSView, visibleInUI: Bool, zPriority: Int = 0) {
|
||||
guard ensureInstalled() else { return }
|
||||
|
||||
|
|
@ -1462,6 +1468,15 @@ enum TerminalWindowPortalRegistry {
|
|||
portal.updateEntryVisibility(forHostedId: hostedId, visibleInUI: visibleInUI)
|
||||
}
|
||||
|
||||
static func isHostedView(_ hostedView: GhosttySurfaceScrollView, boundTo anchorView: NSView) -> Bool {
|
||||
let hostedId = ObjectIdentifier(hostedView)
|
||||
guard let window = anchorView.window else { return false }
|
||||
let windowId = ObjectIdentifier(window)
|
||||
guard hostedToWindowId[hostedId] == windowId,
|
||||
let portal = portalsByWindowId[windowId] else { return false }
|
||||
return portal.isHostedViewBoundToAnchor(withId: hostedId, anchorView: anchorView)
|
||||
}
|
||||
|
||||
static func viewAtWindowPoint(_ windowPoint: NSPoint, in window: NSWindow) -> NSView? {
|
||||
let portal = portal(for: window)
|
||||
return portal.viewAtWindowPoint(windowPoint)
|
||||
|
|
|
|||
|
|
@ -6289,3 +6289,45 @@ final class BrowserOmnibarFocusPolicyTests: XCTestCase {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
final class GhosttyTerminalViewVisibilityPolicyTests: XCTestCase {
|
||||
func testImmediateStateUpdateAllowedWhenHostNotInWindow() {
|
||||
XCTAssertTrue(
|
||||
GhosttyTerminalView.shouldApplyImmediateHostedStateUpdate(
|
||||
hostWindowAttached: false,
|
||||
hostedViewHasSuperview: true,
|
||||
isBoundToCurrentHost: false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testImmediateStateUpdateAllowedWhenBoundToCurrentHost() {
|
||||
XCTAssertTrue(
|
||||
GhosttyTerminalView.shouldApplyImmediateHostedStateUpdate(
|
||||
hostWindowAttached: true,
|
||||
hostedViewHasSuperview: true,
|
||||
isBoundToCurrentHost: true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testImmediateStateUpdateSkippedForStaleHostBoundElsewhere() {
|
||||
XCTAssertFalse(
|
||||
GhosttyTerminalView.shouldApplyImmediateHostedStateUpdate(
|
||||
hostWindowAttached: true,
|
||||
hostedViewHasSuperview: true,
|
||||
isBoundToCurrentHost: false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testImmediateStateUpdateAllowedWhenUnboundAndNotAttachedAnywhere() {
|
||||
XCTAssertTrue(
|
||||
GhosttyTerminalView.shouldApplyImmediateHostedStateUpdate(
|
||||
hostWindowAttached: true,
|
||||
hostedViewHasSuperview: false,
|
||||
isBoundToCurrentHost: false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue