Merge origin/main into pr-ssh-stack-main

This commit is contained in:
Lawrence Chen 2026-03-17 00:40:39 -07:00
commit 93bc5ea78b
No known key found for this signature in database
3 changed files with 123 additions and 9 deletions

View file

@ -8662,6 +8662,23 @@ struct GhosttyTerminalView: NSViewRepresentable {
return !hostedViewHasSuperview
}
private static func synchronizePortalGeometry(
for host: HostContainerView,
coordinator: Coordinator
) {
let geometryRevision = host.geometryRevision
guard coordinator.lastSynchronizedHostGeometryRevision != geometryRevision else { return }
coordinator.lastSynchronizedHostGeometryRevision = geometryRevision
if host.inLiveResize || host.window?.inLiveResize == true {
TerminalWindowPortalRegistry.synchronizeForAnchor(host)
return
}
// Avoid synchronizing the terminal portal while AppKit is still inside
// the current layout turn. Re-entrant syncs here can wedge window resize
// handling and leave the app spinning on the wait cursor.
TerminalWindowPortalRegistry.scheduleExternalGeometrySynchronizeForAllWindows()
}
func makeNSView(context: Context) -> NSView {
let container = HostContainerView()
container.wantsLayer = false
@ -8829,8 +8846,10 @@ struct GhosttyTerminalView: NSViewRepresentable {
hostedView.setActive(coordinator.desiredIsActive)
hostedView.setNotificationRing(visible: coordinator.desiredShowsUnreadNotificationRing)
}
TerminalWindowPortalRegistry.synchronizeForAnchor(host)
coordinator.lastSynchronizedHostGeometryRevision = host.geometryRevision
Self.synchronizePortalGeometry(
for: host,
coordinator: coordinator
)
}
if host.window != nil, hostOwnsPortalNow {
@ -8866,8 +8885,10 @@ struct GhosttyTerminalView: NSViewRepresentable {
coordinator.lastBoundHostId = hostId
coordinator.lastSynchronizedHostGeometryRevision = geometryRevision
} else if portalBindingLive && coordinator.lastSynchronizedHostGeometryRevision != geometryRevision {
TerminalWindowPortalRegistry.synchronizeForAnchor(host)
coordinator.lastSynchronizedHostGeometryRevision = geometryRevision
Self.synchronizePortalGeometry(
for: host,
coordinator: coordinator
)
}
} else if hostOwnsPortalNow, portalBindingStillLive() {
// Bind is deferred until host moves into a window. Update the

View file

@ -680,10 +680,18 @@ final class WindowTerminalPortal: NSObject {
private func scheduleExternalGeometrySynchronize() {
guard !hasExternalGeometrySyncScheduled else { return }
hasExternalGeometrySyncScheduled = true
let requiresSettledLayout = !(hostView.inLiveResize || window?.inLiveResize == true)
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.hasExternalGeometrySyncScheduled = false
self.synchronizeAllEntriesFromExternalGeometryChange()
let performSync = {
self.hasExternalGeometrySyncScheduled = false
self.synchronizeAllEntriesFromExternalGeometryChange()
}
if requiresSettledLayout {
DispatchQueue.main.async(execute: performSync)
} else {
performSync()
}
}
}
@ -1785,9 +1793,11 @@ enum TerminalWindowPortalRegistry {
guard !Self.hasPendingExternalGeometrySyncForAllWindows else { return }
Self.hasPendingExternalGeometrySyncForAllWindows = true
DispatchQueue.main.async {
Self.hasPendingExternalGeometrySyncForAllWindows = false
for portal in Self.portalsByWindowId.values {
portal.synchronizeAllEntriesFromExternalGeometryChange()
DispatchQueue.main.async {
Self.hasPendingExternalGeometrySyncForAllWindows = false
for portal in Self.portalsByWindowId.values {
portal.synchronizeAllEntriesFromExternalGeometryChange()
}
}
}
}

View file

@ -13236,6 +13236,89 @@ final class TerminalWindowPortalLifecycleTests: XCTestCase {
"The scheduled external geometry sync should move the portal-hosted terminal to the anchor's new window position"
)
}
func testScheduledExternalGeometrySyncWaitsForQueuedLayoutShift() {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 700, height: 420),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
defer {
NotificationCenter.default.post(name: NSWindow.willCloseNotification, object: window)
window.orderOut(nil)
}
let surface = TerminalSurface(
tabId: UUID(),
context: GHOSTTY_SURFACE_CONTEXT_SPLIT,
configTemplate: nil,
workingDirectory: nil
)
guard let contentView = window.contentView else {
XCTFail("Expected content view")
return
}
let shiftedContainer = NSView(frame: NSRect(x: 40, y: 60, width: 260, height: 180))
contentView.addSubview(shiftedContainer)
let anchor = NSView(frame: NSRect(x: 0, y: 0, width: 260, height: 180))
shiftedContainer.addSubview(anchor)
let hosted = surface.hostedView
TerminalWindowPortalRegistry.bind(
hostedView: hosted,
to: anchor,
visibleInUI: true,
expectedSurfaceId: surface.id,
expectedGeneration: surface.portalBindingGeneration()
)
TerminalWindowPortalRegistry.synchronizeForAnchor(anchor)
let anchorCenter = NSPoint(x: anchor.bounds.midX, y: anchor.bounds.midY)
let originalWindowPoint = anchor.convert(anchorCenter, to: nil)
let originalAnchorFrameInWindow = anchor.convert(anchor.bounds, to: nil)
XCTAssertNotNil(
TerminalWindowPortalRegistry.terminalViewAtWindowPoint(originalWindowPoint, in: window),
"Initial hit-testing should resolve the portal-hosted terminal at its original window position"
)
TerminalWindowPortalRegistry.scheduleExternalGeometrySynchronizeForAllWindows()
DispatchQueue.main.async {
shiftedContainer.frame.origin.x += 72
contentView.layoutSubtreeIfNeeded()
window.displayIfNeeded()
}
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
let shiftedAnchorFrameInWindow = anchor.convert(anchor.bounds, to: nil)
XCTAssertGreaterThan(
shiftedAnchorFrameInWindow.minX,
originalAnchorFrameInWindow.minX + 1,
"The queued layout shift should move the anchor to the right"
)
XCTAssertGreaterThan(
shiftedAnchorFrameInWindow.maxX,
originalAnchorFrameInWindow.maxX + 1,
"The shifted anchor should expose a new trailing region outside the stale portal frame"
)
let retiredStaleWindowPoint = NSPoint(
x: (originalAnchorFrameInWindow.minX + shiftedAnchorFrameInWindow.minX) / 2,
y: shiftedAnchorFrameInWindow.midY
)
let shiftedWindowPoint = NSPoint(
x: (originalAnchorFrameInWindow.maxX + shiftedAnchorFrameInWindow.maxX) / 2,
y: shiftedAnchorFrameInWindow.midY
)
XCTAssertNil(
TerminalWindowPortalRegistry.terminalViewAtWindowPoint(retiredStaleWindowPoint, in: window),
"The queued external sync should wait until the later layout shift settles, clearing the stale portal location"
)
XCTAssertNotNil(
TerminalWindowPortalRegistry.terminalViewAtWindowPoint(shiftedWindowPoint, in: window),
"The delayed external sync should move the portal-hosted terminal to the queued layout shift position"
)
}
}
@MainActor