diff --git a/Sources/TerminalWindowPortal.swift b/Sources/TerminalWindowPortal.swift index c0ba5066e47e..589c97bc38b6 100644 --- a/Sources/TerminalWindowPortal.swift +++ b/Sources/TerminalWindowPortal.swift @@ -217,7 +217,20 @@ final class WindowTerminalHostView: NSView { in: self, eventType: eventType ) else { return false } - return decision.result + guard decision.result else { return false } + return hostedTerminalHitView(at: point) == nil + } + + private func hostedTerminalHitView(at point: NSPoint) -> NSView? { + for subview in subviews.reversed() { + guard let hostedView = subview as? GhosttySurfaceScrollView, + !hostedView.isHidden, + hostedView.alphaValue > 0, + hostedView.frame.contains(point) else { continue } + + return hostedView.hitTest(point) ?? hostedView + } + return nil } private func shouldPassThroughToChrome(at point: NSPoint, eventType: NSEvent.EventType?) -> Bool { diff --git a/cmuxTests/TerminalAndGhosttyTests.swift b/cmuxTests/TerminalAndGhosttyTests.swift index c143a41971ae..b37ee8e5a9ae 100644 --- a/cmuxTests/TerminalAndGhosttyTests.swift +++ b/cmuxTests/TerminalAndGhosttyTests.swift @@ -2257,6 +2257,54 @@ final class WindowTerminalHostViewTests: XCTestCase { ) } + func testHostViewKeepsTerminalTopRowClickableWhenTabStripRegionOverlapsContent() { + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 420, height: 260), + styleMask: [.titled, .closable], + backing: .buffered, + defer: false + ) + defer { window.orderOut(nil) } + guard let contentView = window.contentView, + let container = contentView.superview else { + XCTFail("Expected window content container") + return + } + + let hostFrame = container.convert(contentView.bounds, from: contentView) + let host = WindowTerminalHostView(frame: hostFrame) + host.autoresizingMask = [.width, .height] + + let terminalFrame = host.bounds.insetBy(dx: 0, dy: 32) + let hostedView = makeHostedTerminalView(frame: terminalFrame) + host.addSubview(hostedView) + container.addSubview(host, positioned: .above, relativeTo: contentView) + + let tabStripOverlap: CGFloat = 2 + let terminalTopInContent = contentView.convert(hostedView.frame, from: host).maxY + let tabStrip = FakeTabBarBackgroundNSView( + frame: NSRect( + x: 0, + y: terminalTopInContent - tabStripOverlap, + width: contentView.bounds.width, + height: 44 + ) + ) + tabStrip.autoresizingMask = [.width, .minYMargin] + contentView.addSubview(tabStrip) + + let pointInHostedView = NSPoint(x: hostedView.bounds.midX, y: hostedView.bounds.maxY - 0.5) + let pointInWindow = hostedView.convert(pointInHostedView, to: nil) + let pointInHost = host.convert(pointInWindow, from: nil) + let event = makeMouseDownEvent(at: pointInWindow, window: window) + + assertHitFallsInsideHostedTerminal( + host.performHitTest(at: pointInHost, currentEvent: event), + hostedView: hostedView, + message: "The absolute top row of terminal content should own mouse-down hit-testing even if chrome hit regions overlap it" + ) + } + func testHostViewPassesThroughWhenNoTerminalSubviewIsHit() { let host = WindowTerminalHostView(frame: NSRect(x: 0, y: 0, width: 200, height: 120))