Fix panel resize hitbox and stale portal frame behavior (#114)

* ok

* Drop GhosttyTerminalView changes from resize PR
This commit is contained in:
Austin Wang 2026-02-19 18:32:57 -08:00 committed by GitHub
parent fc1de08561
commit 3b50c6594c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 109 additions and 5 deletions

View file

@ -23,9 +23,70 @@ final class WindowTerminalHostView: NSView {
override var isOpaque: Bool { false }
override func hitTest(_ point: NSPoint) -> NSView? {
if shouldPassThroughToSplitDivider(at: point) {
return nil
}
let hitView = super.hitTest(point)
return hitView === self ? nil : hitView
}
private func shouldPassThroughToSplitDivider(at point: NSPoint) -> Bool {
guard let window else { return false }
let windowPoint = convert(point, to: nil)
guard let rootView = window.contentView else { return false }
return Self.containsSplitDivider(at: windowPoint, in: rootView)
}
private static func containsSplitDivider(at windowPoint: NSPoint, in view: NSView) -> Bool {
guard !view.isHidden else { return false }
if let splitView = view as? NSSplitView {
let pointInSplit = splitView.convert(windowPoint, from: nil)
if splitView.bounds.contains(pointInSplit) {
// Keep divider interactions reliable even when portal-hosted terminal frames
// temporarily overlap divider edges during rapid layout churn.
let expansion: CGFloat = 5
let dividerCount = max(0, splitView.arrangedSubviews.count - 1)
for dividerIndex in 0..<dividerCount {
let first = splitView.arrangedSubviews[dividerIndex].frame
let second = splitView.arrangedSubviews[dividerIndex + 1].frame
let thickness = splitView.dividerThickness
let dividerRect: NSRect
if splitView.isVertical {
guard first.width > 1, second.width > 1 else { continue }
let x = max(0, first.maxX)
dividerRect = NSRect(
x: x,
y: 0,
width: thickness,
height: splitView.bounds.height
)
} else {
guard first.height > 1, second.height > 1 else { continue }
let y = max(0, first.maxY)
dividerRect = NSRect(
x: 0,
y: y,
width: splitView.bounds.width,
height: thickness
)
}
let expandedDividerRect = dividerRect.insetBy(dx: -expansion, dy: -expansion)
if expandedDividerRect.contains(pointInSplit) {
return true
}
}
}
}
for subview in view.subviews.reversed() {
if containsSplitDivider(at: windowPoint, in: subview) {
return true
}
}
return false
}
}
@MainActor
@ -35,6 +96,7 @@ final class WindowTerminalPortal: NSObject {
private weak var installedContainerView: NSView?
private weak var installedReferenceView: NSView?
private var installConstraints: [NSLayoutConstraint] = []
private var hasDeferredFullSyncScheduled = false
private struct Entry {
weak var hostedView: GhosttySurfaceScrollView?
@ -226,8 +288,37 @@ final class WindowTerminalPortal: NSObject {
func synchronizeHostedViewForAnchor(_ anchorView: NSView) {
pruneDeadEntries()
guard let hostedId = hostedByAnchorId[ObjectIdentifier(anchorView)] else { return }
synchronizeHostedView(withId: hostedId)
let anchorId = ObjectIdentifier(anchorView)
let primaryHostedId = hostedByAnchorId[anchorId]
if let primaryHostedId {
synchronizeHostedView(withId: primaryHostedId)
}
// Failsafe: during aggressive divider drags/structural churn, one anchor can miss a
// geometry callback while another fires. Reconcile all mapped hosted views so no stale
// frame remains "stuck" onscreen until the next interaction.
synchronizeAllHostedViews(excluding: primaryHostedId)
scheduleDeferredFullSynchronizeAll()
}
private func scheduleDeferredFullSynchronizeAll() {
guard !hasDeferredFullSyncScheduled else { return }
hasDeferredFullSyncScheduled = true
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.hasDeferredFullSyncScheduled = false
self.synchronizeAllHostedViews(excluding: nil)
}
}
private func synchronizeAllHostedViews(excluding hostedIdToSkip: ObjectIdentifier?) {
guard ensureInstalled() else { return }
pruneDeadEntries()
let hostedIds = Array(entriesByHostedId.keys)
for hostedId in hostedIds {
if hostedId == hostedIdToSkip { continue }
synchronizeHostedView(withId: hostedId)
}
}
private func synchronizeHostedView(withId hostedId: ObjectIdentifier) {
@ -261,12 +352,20 @@ final class WindowTerminalPortal: NSObject {
let frameInWindow = anchorView.convert(anchorView.bounds, to: nil)
let frameInHost = hostView.convert(frameInWindow, from: nil)
let hasFiniteFrame =
frameInHost.origin.x.isFinite &&
frameInHost.origin.y.isFinite &&
frameInHost.size.width.isFinite &&
frameInHost.size.height.isFinite
let anchorHidden = Self.isHiddenOrAncestorHidden(anchorView)
let tinyFrame = frameInHost.width <= 1 || frameInHost.height <= 1
let outsideHostBounds = !frameInHost.intersects(hostView.bounds)
let shouldHide =
!entry.visibleInUI ||
anchorHidden ||
tinyFrame
tinyFrame ||
!hasFiniteFrame ||
outsideHostBounds
let oldFrame = hostedView.frame
#if DEBUG
@ -301,7 +400,8 @@ final class WindowTerminalPortal: NSObject {
dlog(
"portal.hidden hosted=\(portalDebugToken(hostedView)) value=\(shouldHide ? 1 : 0) " +
"visibleInUI=\(entry.visibleInUI ? 1 : 0) anchorHidden=\(anchorHidden ? 1 : 0) " +
"tiny=\(tinyFrame ? 1 : 0) frame=\(portalDebugFrame(frameInHost))"
"tiny=\(tinyFrame ? 1 : 0) finite=\(hasFiniteFrame ? 1 : 0) " +
"outside=\(outsideHostBounds ? 1 : 0) frame=\(portalDebugFrame(frameInHost))"
)
#endif
hostedView.isHidden = shouldHide
@ -316,6 +416,10 @@ final class WindowTerminalPortal: NSObject {
if anchor.window !== currentWindow || anchor.superview == nil {
return hostedId
}
if let reference = installedReferenceView,
!anchor.isDescendant(of: reference) {
return hostedId
}
return nil
}

2
vendor/bonsplit vendored

@ -1 +1 @@
Subproject commit 2bd60ba40dc3350bd3c774b5f2de9f9b9c1b39fb
Subproject commit 748d9c0fe12edebd5448b946ce2c23d7549cd073