Resync terminal portals after sidebar changes (#1253)
* Add regression test for portal ancestor shifts * Resync terminal portals after sidebar changes * Restore safeHelp view helper * Fix portal geometry regression test harness
This commit is contained in:
parent
292359f600
commit
8d5a1f611d
4 changed files with 100 additions and 1 deletions
|
|
@ -7,6 +7,15 @@ struct Backport<Content> {
|
||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
var backport: Backport<Self> { Backport(content: self) }
|
var backport: Backport<Self> { Backport(content: self) }
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func safeHelp(_ text: String) -> some View {
|
||||||
|
if text.isEmpty {
|
||||||
|
self
|
||||||
|
} else {
|
||||||
|
self.help(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Scene {
|
extension Scene {
|
||||||
|
|
|
||||||
|
|
@ -2591,10 +2591,14 @@ struct ContentView: View {
|
||||||
if abs(sidebarState.persistedWidth - sanitized) > 0.5 {
|
if abs(sidebarState.persistedWidth - sanitized) > 0.5 {
|
||||||
sidebarState.persistedWidth = sanitized
|
sidebarState.persistedWidth = sanitized
|
||||||
}
|
}
|
||||||
|
// Sidebar width changes are pure SwiftUI layout updates, so portal-hosted
|
||||||
|
// terminals need an explicit post-layout geometry resync.
|
||||||
|
TerminalWindowPortalRegistry.scheduleExternalGeometrySynchronizeForAllWindows()
|
||||||
updateSidebarResizerBandState()
|
updateSidebarResizerBandState()
|
||||||
})
|
})
|
||||||
|
|
||||||
view = AnyView(view.onChange(of: sidebarState.isVisible) { _ in
|
view = AnyView(view.onChange(of: sidebarState.isVisible) { _ in
|
||||||
|
TerminalWindowPortalRegistry.scheduleExternalGeometrySynchronizeForAllWindows()
|
||||||
updateSidebarResizerBandState()
|
updateSidebarResizerBandState()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -724,7 +724,7 @@ final class WindowTerminalPortal: NSObject {
|
||||||
return frameInContainer.width > 1 && frameInContainer.height > 1
|
return frameInContainer.width > 1 && frameInContainer.height > 1
|
||||||
}
|
}
|
||||||
|
|
||||||
private func synchronizeAllEntriesFromExternalGeometryChange() {
|
fileprivate func synchronizeAllEntriesFromExternalGeometryChange() {
|
||||||
guard ensureInstalled() else { return }
|
guard ensureInstalled() else { return }
|
||||||
synchronizeLayoutHierarchy()
|
synchronizeLayoutHierarchy()
|
||||||
synchronizeAllHostedViews(excluding: nil)
|
synchronizeAllHostedViews(excluding: nil)
|
||||||
|
|
@ -1635,6 +1635,7 @@ final class WindowTerminalPortal: NSObject {
|
||||||
enum TerminalWindowPortalRegistry {
|
enum TerminalWindowPortalRegistry {
|
||||||
private static var portalsByWindowId: [ObjectIdentifier: WindowTerminalPortal] = [:]
|
private static var portalsByWindowId: [ObjectIdentifier: WindowTerminalPortal] = [:]
|
||||||
private static var hostedToWindowId: [ObjectIdentifier: ObjectIdentifier] = [:]
|
private static var hostedToWindowId: [ObjectIdentifier: ObjectIdentifier] = [:]
|
||||||
|
private static var hasPendingExternalGeometrySyncForAllWindows = false
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
private static var blockedBindCount: Int = 0
|
private static var blockedBindCount: Int = 0
|
||||||
private static var blockedBindReasons: [String: Int] = [:]
|
private static var blockedBindReasons: [String: Int] = [:]
|
||||||
|
|
@ -1780,6 +1781,17 @@ enum TerminalWindowPortalRegistry {
|
||||||
portal.synchronizeHostedViewForAnchor(anchorView)
|
portal.synchronizeHostedViewForAnchor(anchorView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func scheduleExternalGeometrySynchronizeForAllWindows() {
|
||||||
|
guard !Self.hasPendingExternalGeometrySyncForAllWindows else { return }
|
||||||
|
Self.hasPendingExternalGeometrySyncForAllWindows = true
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
Self.hasPendingExternalGeometrySyncForAllWindows = false
|
||||||
|
for portal in Self.portalsByWindowId.values {
|
||||||
|
portal.synchronizeAllEntriesFromExternalGeometryChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static func hideHostedView(_ hostedView: GhosttySurfaceScrollView) {
|
static func hideHostedView(_ hostedView: GhosttySurfaceScrollView) {
|
||||||
let hostedId = ObjectIdentifier(hostedView)
|
let hostedId = ObjectIdentifier(hostedView)
|
||||||
guard let windowId = hostedToWindowId[hostedId],
|
guard let windowId = hostedToWindowId[hostedId],
|
||||||
|
|
|
||||||
|
|
@ -11861,6 +11861,80 @@ final class TerminalWindowPortalLifecycleTests: XCTestCase {
|
||||||
portal.synchronizeHostedViewForAnchor(anchor)
|
portal.synchronizeHostedViewForAnchor(anchor)
|
||||||
XCTAssertFalse(hosted.isHidden, "Portal should unhide after geometry is usable")
|
XCTAssertFalse(hosted.isHidden, "Portal should unhide after geometry is usable")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testScheduledExternalGeometrySyncRefreshesAncestorLayoutShift() {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
realizeWindowLayout(window)
|
||||||
|
guard let contentView = window.contentView else {
|
||||||
|
XCTFail("Expected content view")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let shiftedContainer = NSView(frame: NSRect(x: 120, y: 60, width: 220, height: 160))
|
||||||
|
contentView.addSubview(shiftedContainer)
|
||||||
|
let anchor = NSView(frame: NSRect(x: 24, y: 28, width: 72, height: 56))
|
||||||
|
shiftedContainer.addSubview(anchor)
|
||||||
|
|
||||||
|
let surface = TerminalSurface(
|
||||||
|
tabId: UUID(),
|
||||||
|
context: GHOSTTY_SURFACE_CONTEXT_SPLIT,
|
||||||
|
configTemplate: nil,
|
||||||
|
workingDirectory: nil
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
XCTAssertNotNil(
|
||||||
|
TerminalWindowPortalRegistry.terminalViewAtWindowPoint(originalWindowPoint, in: window),
|
||||||
|
"Initial hit-testing should resolve the portal-hosted terminal at its original window position"
|
||||||
|
)
|
||||||
|
|
||||||
|
shiftedContainer.frame.origin.x += 96
|
||||||
|
contentView.layoutSubtreeIfNeeded()
|
||||||
|
window.displayIfNeeded()
|
||||||
|
|
||||||
|
let shiftedWindowPoint = anchor.convert(anchorCenter, to: nil)
|
||||||
|
XCTAssertNotEqual(originalWindowPoint.x, shiftedWindowPoint.x, accuracy: 0.5)
|
||||||
|
XCTAssertNil(
|
||||||
|
TerminalWindowPortalRegistry.terminalViewAtWindowPoint(shiftedWindowPoint, in: window),
|
||||||
|
"Ancestor-only layout shifts should leave the portal stale until an external geometry sync runs"
|
||||||
|
)
|
||||||
|
XCTAssertNotNil(
|
||||||
|
TerminalWindowPortalRegistry.terminalViewAtWindowPoint(originalWindowPoint, in: window),
|
||||||
|
"Before the external geometry sync, hit-testing should still point at the stale portal location"
|
||||||
|
)
|
||||||
|
|
||||||
|
TerminalWindowPortalRegistry.scheduleExternalGeometrySynchronizeForAllWindows()
|
||||||
|
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
|
||||||
|
|
||||||
|
XCTAssertNil(
|
||||||
|
TerminalWindowPortalRegistry.terminalViewAtWindowPoint(originalWindowPoint, in: window),
|
||||||
|
"The stale portal position should be cleared after the scheduled external geometry sync"
|
||||||
|
)
|
||||||
|
XCTAssertNotNil(
|
||||||
|
TerminalWindowPortalRegistry.terminalViewAtWindowPoint(shiftedWindowPoint, in: window),
|
||||||
|
"The scheduled external geometry sync should move the portal-hosted terminal to the anchor's new window position"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue