Merge pull request #2025 from manaflow-ai/issue-1357-ghost-terminal-sidebar-bleed
Fix #1357: hide stale terminal portal after restore churn
This commit is contained in:
commit
9b3a6ba28b
2 changed files with 89 additions and 9 deletions
|
|
@ -1211,18 +1211,33 @@ final class WindowTerminalPortal: NSObject {
|
|||
return
|
||||
}
|
||||
guard let anchorView = entry.anchorView, let window else {
|
||||
// Only hide if the entry is not marked visibleInUI. When a workspace is
|
||||
// remounting, updateNSView sets visibleInUI=true before the deferred bind
|
||||
// provides an anchor — hiding here would race with that and cause a flash.
|
||||
if !entry.visibleInUI {
|
||||
if entry.visibleInUI {
|
||||
let shouldPreserveVisibleOnTransient = !hostedView.isHidden &&
|
||||
scheduleTransientRecoveryRetryIfNeeded(
|
||||
forHostedId: hostedId,
|
||||
entry: &entry,
|
||||
hostedView: hostedView,
|
||||
reason: "missingAnchorOrWindow"
|
||||
)
|
||||
if shouldPreserveVisibleOnTransient {
|
||||
#if DEBUG
|
||||
if !hostedView.isHidden {
|
||||
dlog("portal.hidden hosted=\(portalDebugToken(hostedView)) value=1 reason=missingAnchorOrWindow")
|
||||
}
|
||||
dlog(
|
||||
"portal.hidden.deferKeep hosted=\(portalDebugToken(hostedView)) " +
|
||||
"reason=missingAnchorOrWindow frame=\(portalDebugFrame(hostedView.frame))"
|
||||
)
|
||||
#endif
|
||||
hostedView.isHidden = true
|
||||
resetTransientRecoveryRetryIfNeeded(forHostedId: hostedId, entry: &entry)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
resetTransientRecoveryRetryIfNeeded(forHostedId: hostedId, entry: &entry)
|
||||
}
|
||||
#if DEBUG
|
||||
if !hostedView.isHidden {
|
||||
dlog("portal.hidden hosted=\(portalDebugToken(hostedView)) value=1 reason=missingAnchorOrWindow")
|
||||
}
|
||||
#endif
|
||||
hostedView.isHidden = true
|
||||
if entry.visibleInUI {
|
||||
_ = scheduleTransientRecoveryRetryIfNeeded(
|
||||
forHostedId: hostedId,
|
||||
entry: &entry,
|
||||
|
|
|
|||
|
|
@ -2596,6 +2596,71 @@ final class TerminalWindowPortalLifecycleTests: XCTestCase {
|
|||
XCTAssertEqual(portal.debugHostedSubviewCount(), 1, "Stale anchorless hosted views should be detached from hostView")
|
||||
}
|
||||
|
||||
func testDeferredSyncHidesVisibleHostedViewAfterAnchorDisappears() {
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 520, height: 320),
|
||||
styleMask: [.titled, .closable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
defer { window.orderOut(nil) }
|
||||
realizeWindowLayout(window)
|
||||
|
||||
let portal = WindowTerminalPortal(window: window)
|
||||
guard let contentView = window.contentView else {
|
||||
XCTFail("Expected content view")
|
||||
return
|
||||
}
|
||||
|
||||
var retiredAnchor: NSView? = NSView(frame: NSRect(x: 24, y: 28, width: 96, height: 180))
|
||||
contentView.addSubview(retiredAnchor!)
|
||||
|
||||
let retiredTerminal = GhosttyNSView(frame: NSRect(x: 0, y: 0, width: 96, height: 180))
|
||||
let retiredHosted = GhosttySurfaceScrollView(surfaceView: retiredTerminal)
|
||||
portal.bind(hostedView: retiredHosted, to: retiredAnchor!, visibleInUI: true)
|
||||
portal.synchronizeHostedViewForAnchor(retiredAnchor!)
|
||||
|
||||
let retiredWindowPoint = retiredAnchor!.convert(
|
||||
NSPoint(x: retiredAnchor!.bounds.midX, y: retiredAnchor!.bounds.midY),
|
||||
to: nil
|
||||
)
|
||||
XCTAssertTrue(
|
||||
portal.terminalViewAtWindowPoint(retiredWindowPoint) === retiredTerminal,
|
||||
"Initial hit-testing should resolve the first hosted terminal at its anchor"
|
||||
)
|
||||
|
||||
retiredAnchor?.removeFromSuperview()
|
||||
retiredAnchor = nil
|
||||
|
||||
let activeAnchor = NSView(frame: NSRect(x: 184, y: 28, width: 280, height: 180))
|
||||
contentView.addSubview(activeAnchor)
|
||||
|
||||
let activeTerminal = GhosttyNSView(frame: NSRect(x: 0, y: 0, width: 280, height: 180))
|
||||
let activeHosted = GhosttySurfaceScrollView(surfaceView: activeTerminal)
|
||||
portal.bind(hostedView: activeHosted, to: activeAnchor, visibleInUI: true)
|
||||
portal.synchronizeHostedViewForAnchor(activeAnchor)
|
||||
|
||||
XCTAssertTrue(
|
||||
retiredHosted.isHidden,
|
||||
"A visible hosted terminal whose anchor vanished should hide as soon as the replacement anchor sync runs"
|
||||
)
|
||||
// Drain the queued full-sync turn so the portal clears any stale hit-test region left by the rebind.
|
||||
drainMainQueue()
|
||||
|
||||
let activeWindowPoint = activeAnchor.convert(
|
||||
NSPoint(x: activeAnchor.bounds.midX, y: activeAnchor.bounds.midY),
|
||||
to: nil
|
||||
)
|
||||
XCTAssertNil(
|
||||
portal.terminalViewAtWindowPoint(retiredWindowPoint),
|
||||
"Restore-like rebinds should clear stale portal hit regions on the queued portal resync"
|
||||
)
|
||||
XCTAssertTrue(
|
||||
portal.terminalViewAtWindowPoint(activeWindowPoint) === activeTerminal,
|
||||
"The active terminal should remain visible after the stale hosted view is hidden"
|
||||
)
|
||||
}
|
||||
|
||||
func testSynchronizeReusesInstalledTargetWithoutRepeatedContentViewLookup() {
|
||||
let window = ContentViewCountingWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 500, height: 300),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue