Move terminal drop overlay outside hosted view
This commit is contained in:
parent
c7bbb72e09
commit
379150a6c1
1 changed files with 128 additions and 20 deletions
|
|
@ -3670,9 +3670,6 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
|
|||
}
|
||||
|
||||
private func activeSurfaceResizeDeferralReason() -> String? {
|
||||
if terminalSurface?.hostedView.hasActiveDropZoneOverlay == true {
|
||||
return "dropOverlay"
|
||||
}
|
||||
return Self.shouldDeferSurfaceResizeForActiveDrag() ? "tabDrag" : nil
|
||||
}
|
||||
|
||||
|
|
@ -5623,8 +5620,8 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
private var searchOverlayHostingView: NSHostingView<SurfaceSearchOverlay>?
|
||||
private var lastSearchOverlayStateID: ObjectIdentifier?
|
||||
private var observers: [NSObjectProtocol] = []
|
||||
private var windowObservers: [NSObjectProtocol] = []
|
||||
private var isLiveScrolling = false
|
||||
private var windowObservers: [NSObjectProtocol] = []
|
||||
private var isLiveScrolling = false
|
||||
private var lastSentRow: Int?
|
||||
private var isActive = true
|
||||
private var lastFocusRefreshAt: CFTimeInterval = 0
|
||||
|
|
@ -5649,13 +5646,14 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
|
||||
#if DEBUG
|
||||
private var lastDropZoneOverlayLogSignature: String?
|
||||
private var lastDragGeometryLogSignature: String?
|
||||
private var dragLayoutLogSequence: UInt64 = 0
|
||||
private static let tabTransferPasteboardType = NSPasteboard.PasteboardType("com.splittabbar.tabtransfer")
|
||||
private static let sidebarTabReorderPasteboardType = NSPasteboard.PasteboardType("com.cmux.sidebar-tab-reorder")
|
||||
private static var flashCounts: [UUID: Int] = [:]
|
||||
private static var drawCounts: [UUID: Int] = [:]
|
||||
private static var lastDrawTimes: [UUID: CFTimeInterval] = [:]
|
||||
private static var presentCounts: [UUID: Int] = [:]
|
||||
private static var flashCounts: [UUID: Int] = [:]
|
||||
private static var drawCounts: [UUID: Int] = [:]
|
||||
private static var lastDrawTimes: [UUID: CFTimeInterval] = [:]
|
||||
private static var presentCounts: [UUID: Int] = [:]
|
||||
private static var dropOverlayShowCounts: [UUID: Int] = [:]
|
||||
private static var lastPresentTimes: [UUID: CFTimeInterval] = [:]
|
||||
private static var lastContentsKeys: [UUID: String] = [:]
|
||||
|
|
@ -5836,7 +5834,6 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
dropZoneOverlayView.layer?.borderWidth = 2
|
||||
dropZoneOverlayView.layer?.cornerRadius = 8
|
||||
dropZoneOverlayView.isHidden = true
|
||||
addSubview(dropZoneOverlayView)
|
||||
notificationRingOverlayView.wantsLayer = true
|
||||
notificationRingOverlayView.layer?.backgroundColor = NSColor.clear.cgColor
|
||||
notificationRingOverlayView.layer?.masksToBounds = false
|
||||
|
|
@ -6007,6 +6004,7 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
#endif
|
||||
observers.forEach { NotificationCenter.default.removeObserver($0) }
|
||||
windowObservers.forEach { NotificationCenter.default.removeObserver($0) }
|
||||
dropZoneOverlayView.removeFromSuperview()
|
||||
cancelFocusRequest()
|
||||
}
|
||||
|
||||
|
|
@ -6020,6 +6018,15 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
synchronizeGeometryAndContent()
|
||||
}
|
||||
|
||||
override func viewDidMoveToSuperview() {
|
||||
super.viewDidMoveToSuperview()
|
||||
guard activeDropZone != nil || pendingDropZone != nil else { return }
|
||||
attachDropZoneOverlayIfNeeded()
|
||||
if let zone = activeDropZone ?? pendingDropZone {
|
||||
applyDropZoneOverlayFrame(dropZoneOverlayFrame(for: zone, in: bounds.size))
|
||||
}
|
||||
}
|
||||
|
||||
/// Reconcile AppKit geometry with ghostty surface geometry synchronously.
|
||||
/// Used after split topology mutations (close/split) to prevent a stale one-frame
|
||||
/// IOSurface size from being presented after pane expansion.
|
||||
|
|
@ -6063,6 +6070,7 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
_ = setFrameIfNeeded(documentView, to: targetDocumentFrame)
|
||||
_ = setFrameIfNeeded(inactiveOverlayView, to: bounds)
|
||||
if let zone = activeDropZone {
|
||||
attachDropZoneOverlayIfNeeded()
|
||||
_ = setFrameIfNeeded(
|
||||
dropZoneOverlayView,
|
||||
to: dropZoneOverlayFrame(for: zone, in: bounds.size)
|
||||
|
|
@ -6105,6 +6113,42 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
abs(lhs.x - rhs.x) <= epsilon && abs(lhs.y - rhs.y) <= epsilon
|
||||
}
|
||||
|
||||
private func dropZoneOverlayContainerView() -> NSView {
|
||||
superview ?? self
|
||||
}
|
||||
|
||||
private func attachDropZoneOverlayIfNeeded() {
|
||||
// Keep the hover indicator outside the hosted terminal subtree so it stays purely additive
|
||||
// and cannot invalidate the scroll/surface layout that Ghostty renders into.
|
||||
let container = dropZoneOverlayContainerView()
|
||||
if dropZoneOverlayView.superview !== container {
|
||||
dropZoneOverlayView.removeFromSuperview()
|
||||
if container === self {
|
||||
addSubview(dropZoneOverlayView, positioned: .above, relativeTo: nil)
|
||||
} else {
|
||||
container.addSubview(dropZoneOverlayView, positioned: .above, relativeTo: self)
|
||||
}
|
||||
#if DEBUG
|
||||
logDropZoneOverlay(event: "attach", zone: activeDropZone ?? pendingDropZone, frame: dropZoneOverlayView.frame)
|
||||
#endif
|
||||
return
|
||||
}
|
||||
|
||||
guard container !== self else { return }
|
||||
guard let hostedIndex = container.subviews.firstIndex(of: self),
|
||||
let overlayIndex = container.subviews.firstIndex(of: dropZoneOverlayView),
|
||||
overlayIndex <= hostedIndex else { return }
|
||||
container.addSubview(dropZoneOverlayView, positioned: .above, relativeTo: self)
|
||||
}
|
||||
|
||||
private func applyDropZoneOverlayFrame(_ frame: CGRect) {
|
||||
if Self.rectApproximatelyEqual(dropZoneOverlayView.frame, frame) { return }
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
dropZoneOverlayView.frame = frame
|
||||
CATransaction.commit()
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private static func isDragMouseEvent(_ eventType: NSEvent.EventType?) -> Bool {
|
||||
switch eventType {
|
||||
|
|
@ -6115,6 +6159,36 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
}
|
||||
}
|
||||
|
||||
private func hasActiveDragLoggingContext() -> Bool {
|
||||
let pasteboardTypes = NSPasteboard(name: .drag).types
|
||||
let hasTabDrag = pasteboardTypes?.contains(Self.tabTransferPasteboardType) == true
|
||||
let hasSidebarDrag = pasteboardTypes?.contains(Self.sidebarTabReorderPasteboardType) == true
|
||||
let eventType = NSApp.currentEvent?.type
|
||||
return activeDropZone != nil ||
|
||||
pendingDropZone != nil ||
|
||||
((hasTabDrag || hasSidebarDrag) && Self.isDragMouseEvent(eventType))
|
||||
}
|
||||
|
||||
private func logDragGeometryChange(event: String, old: CGPoint, new: CGPoint) {
|
||||
guard hasActiveDragLoggingContext() else { return }
|
||||
|
||||
let surface = surfaceView.terminalSurface?.id.uuidString.prefix(5) ?? "nil"
|
||||
let overlaySuperviewClass = dropZoneOverlayView.superview.map { String(describing: type(of: $0)) } ?? "nil"
|
||||
let signature =
|
||||
"\(event)|\(surface)|\(String(format: "%.1f,%.1f", old.x, old.y))|" +
|
||||
"\(String(format: "%.1f,%.1f", new.x, new.y))|\(overlaySuperviewClass)|\(dropZoneOverlayView.isHidden ? 1 : 0)"
|
||||
guard lastDragGeometryLogSignature != signature else { return }
|
||||
lastDragGeometryLogSignature = signature
|
||||
dlog(
|
||||
"terminal.dragGeometry event=\(event) surface=\(surface) " +
|
||||
"old=\(String(format: "%.1f,%.1f", old.x, old.y)) " +
|
||||
"new=\(String(format: "%.1f,%.1f", new.x, new.y)) " +
|
||||
"overlaySuper=\(overlaySuperviewClass) " +
|
||||
"overlayExternal=\(dropZoneOverlayView.superview === self ? 0 : 1) " +
|
||||
"overlayHidden=\(dropZoneOverlayView.isHidden ? 1 : 0)"
|
||||
)
|
||||
}
|
||||
|
||||
private func logLayoutDuringActiveDrag(targetSize: CGSize) {
|
||||
let pasteboardTypes = NSPasteboard(name: .drag).types
|
||||
let hasTabDrag = pasteboardTypes?.contains(Self.tabTransferPasteboardType) == true
|
||||
|
|
@ -6131,11 +6205,15 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
let activeZone = activeDropZone.map { String(describing: $0) } ?? "none"
|
||||
let pendingZone = pendingDropZone.map { String(describing: $0) } ?? "none"
|
||||
let event = eventType.map { String(describing: $0) } ?? "nil"
|
||||
let overlaySuperviewClass = dropZoneOverlayView.superview.map { String(describing: type(of: $0)) } ?? "nil"
|
||||
dlog(
|
||||
"terminal.layout.drag surface=\(surface) seq=\(dragLayoutLogSequence) " +
|
||||
"activeZone=\(activeZone) pendingZone=\(pendingZone) " +
|
||||
"hasTabDrag=\(hasTabDrag ? 1 : 0) hasSidebarDrag=\(hasSidebarDrag ? 1 : 0) " +
|
||||
"event=\(event) inWindow=\(window != nil ? 1 : 0) " +
|
||||
"overlaySuper=\(overlaySuperviewClass) overlayExternal=\(dropZoneOverlayView.superview === self ? 0 : 1) " +
|
||||
"scrollOrigin=\(String(format: "%.1f,%.1f", scrollView.contentView.bounds.origin.x, scrollView.contentView.bounds.origin.y)) " +
|
||||
"surfaceOrigin=\(String(format: "%.1f,%.1f", surfaceView.frame.origin.x, surfaceView.frame.origin.y)) " +
|
||||
"bounds=\(String(format: "%.1fx%.1f", bounds.width, bounds.height)) " +
|
||||
"target=\(String(format: "%.1fx%.1f", targetSize.width, targetSize.height))"
|
||||
)
|
||||
|
|
@ -6378,18 +6456,23 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
|
||||
private func dropZoneOverlayFrame(for zone: DropZone, in size: CGSize) -> CGRect {
|
||||
let padding: CGFloat = 4
|
||||
let localFrame: CGRect
|
||||
switch zone {
|
||||
case .center:
|
||||
return CGRect(x: padding, y: padding, width: size.width - padding * 2, height: size.height - padding * 2)
|
||||
localFrame = CGRect(x: padding, y: padding, width: size.width - padding * 2, height: size.height - padding * 2)
|
||||
case .left:
|
||||
return CGRect(x: padding, y: padding, width: size.width / 2 - padding, height: size.height - padding * 2)
|
||||
localFrame = CGRect(x: padding, y: padding, width: size.width / 2 - padding, height: size.height - padding * 2)
|
||||
case .right:
|
||||
return CGRect(x: size.width / 2, y: padding, width: size.width / 2 - padding, height: size.height - padding * 2)
|
||||
localFrame = CGRect(x: size.width / 2, y: padding, width: size.width / 2 - padding, height: size.height - padding * 2)
|
||||
case .top:
|
||||
return CGRect(x: padding, y: size.height / 2, width: size.width - padding * 2, height: size.height / 2 - padding)
|
||||
localFrame = CGRect(x: padding, y: size.height / 2, width: size.width - padding * 2, height: size.height / 2 - padding)
|
||||
case .bottom:
|
||||
return CGRect(x: padding, y: padding, width: size.width - padding * 2, height: size.height / 2 - padding)
|
||||
localFrame = CGRect(x: padding, y: padding, width: size.width - padding * 2, height: size.height / 2 - padding)
|
||||
}
|
||||
|
||||
let container = dropZoneOverlayView.superview ?? superview
|
||||
guard let container, container !== self else { return localFrame }
|
||||
return container.convert(localFrame, from: self)
|
||||
}
|
||||
|
||||
private static func rectApproximatelyEqual(_ lhs: CGRect, _ rhs: CGRect, epsilon: CGFloat = 0.5) -> Bool {
|
||||
|
|
@ -6419,15 +6502,15 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
activeDropZone = zone
|
||||
pendingDropZone = nil
|
||||
|
||||
let previousFrame = dropZoneOverlayView.frame
|
||||
|
||||
if let zone {
|
||||
#if DEBUG
|
||||
if window == nil {
|
||||
logDropZoneOverlay(event: "showNoWindow", zone: zone, frame: nil)
|
||||
}
|
||||
#endif
|
||||
attachDropZoneOverlayIfNeeded()
|
||||
let targetFrame = dropZoneOverlayFrame(for: zone, in: bounds.size)
|
||||
let previousFrame = dropZoneOverlayView.frame
|
||||
let isSameFrame = Self.rectApproximatelyEqual(previousFrame, targetFrame)
|
||||
let needsFrameUpdate = !isSameFrame
|
||||
let zoneChanged = previousZone != zone
|
||||
|
|
@ -6440,7 +6523,7 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
dropZoneOverlayView.layer?.removeAllAnimations()
|
||||
|
||||
if dropZoneOverlayView.isHidden {
|
||||
dropZoneOverlayView.frame = targetFrame
|
||||
applyDropZoneOverlayFrame(targetFrame)
|
||||
dropZoneOverlayView.alphaValue = 0
|
||||
dropZoneOverlayView.isHidden = false
|
||||
#if DEBUG
|
||||
|
|
@ -6510,6 +6593,17 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
let surface = surfaceView.terminalSurface?.id.uuidString.prefix(5) ?? "nil"
|
||||
let zoneText = zone.map { String(describing: $0) } ?? "none"
|
||||
let boundsText = String(format: "%.1fx%.1f", bounds.width, bounds.height)
|
||||
let overlaySuperviewClass = dropZoneOverlayView.superview.map { String(describing: type(of: $0)) } ?? "nil"
|
||||
let scrollOriginText = String(
|
||||
format: "%.1f,%.1f",
|
||||
scrollView.contentView.bounds.origin.x,
|
||||
scrollView.contentView.bounds.origin.y
|
||||
)
|
||||
let surfaceOriginText = String(
|
||||
format: "%.1f,%.1f",
|
||||
surfaceView.frame.origin.x,
|
||||
surfaceView.frame.origin.y
|
||||
)
|
||||
let frameText: String
|
||||
if let frame {
|
||||
frameText = String(
|
||||
|
|
@ -6519,12 +6613,16 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
} else {
|
||||
frameText = "-"
|
||||
}
|
||||
let signature = "\(event)|\(surface)|\(zoneText)|\(boundsText)|\(frameText)|\(dropZoneOverlayView.isHidden ? 1 : 0)"
|
||||
let signature =
|
||||
"\(event)|\(surface)|\(zoneText)|\(boundsText)|\(frameText)|\(overlaySuperviewClass)|" +
|
||||
"\(scrollOriginText)|\(surfaceOriginText)|\(dropZoneOverlayView.isHidden ? 1 : 0)"
|
||||
guard lastDropZoneOverlayLogSignature != signature else { return }
|
||||
lastDropZoneOverlayLogSignature = signature
|
||||
dlog(
|
||||
"terminal.dropOverlay event=\(event) surface=\(surface) zone=\(zoneText) " +
|
||||
"hidden=\(dropZoneOverlayView.isHidden ? 1 : 0) bounds=\(boundsText) frame=\(frameText)"
|
||||
"hidden=\(dropZoneOverlayView.isHidden ? 1 : 0) bounds=\(boundsText) frame=\(frameText) " +
|
||||
"overlaySuper=\(overlaySuperviewClass) overlayExternal=\(dropZoneOverlayView.superview === self ? 0 : 1) " +
|
||||
"scrollOrigin=\(scrollOriginText) surfaceOrigin=\(surfaceOriginText)"
|
||||
)
|
||||
}
|
||||
#endif
|
||||
|
|
@ -7523,6 +7621,9 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
private func synchronizeSurfaceView() {
|
||||
let visibleRect = scrollView.contentView.documentVisibleRect
|
||||
guard !pointApproximatelyEqual(surfaceView.frame.origin, visibleRect.origin) else { return }
|
||||
#if DEBUG
|
||||
logDragGeometryChange(event: "surfaceOrigin", old: surfaceView.frame.origin, new: visibleRect.origin)
|
||||
#endif
|
||||
surfaceView.frame.origin = visibleRect.origin
|
||||
}
|
||||
|
||||
|
|
@ -7617,6 +7718,13 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
CGFloat(scrollbar.total - scrollbar.offset - scrollbar.len) * cellHeight
|
||||
let targetOrigin = CGPoint(x: 0, y: offsetY)
|
||||
if !pointApproximatelyEqual(scrollView.contentView.bounds.origin, targetOrigin) {
|
||||
#if DEBUG
|
||||
logDragGeometryChange(
|
||||
event: "scrollOrigin",
|
||||
old: scrollView.contentView.bounds.origin,
|
||||
new: targetOrigin
|
||||
)
|
||||
#endif
|
||||
scrollView.contentView.scroll(to: targetOrigin)
|
||||
didChangeGeometry = true
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue