Merge origin/main into pr-ssh-stack-main
This commit is contained in:
commit
93bc5ea78b
3 changed files with 123 additions and 9 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue