Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions Sources/TerminalWindowPortal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,15 +118,12 @@ final class WindowTerminalHostView: NSView {
performHitTest(at: point, currentEvent: NSApp.currentEvent)
}

// Test seam: production calls go through `hitTest(_:)` which reads
// `NSApp.currentEvent`; tests can call this directly with a synthetic
// pointer event so the typing-latency guard doesn't gate them out.
func performHitTest(at point: NSPoint, currentEvent: NSEvent?) -> NSView? {
let routingContext = WindowInputRoutingContext(event: currentEvent)
let eventType = routingContext.eventType

if routingContext.allowsPortalPointerHitTesting {
if shouldPassThroughToTitlebar(at: point) {
if shouldPassThroughToTitlebar(at: point, preservingHostedTerminal: true) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
clearActiveDividerCursor(restoreArrow: false)
return nil
}
Expand Down Expand Up @@ -202,10 +199,13 @@ final class WindowTerminalHostView: NSView {
return hitView === self ? nil : hitView
}

private func shouldPassThroughToTitlebar(at point: NSPoint) -> Bool {
private func shouldPassThroughToTitlebar(at point: NSPoint, preservingHostedTerminal: Bool = false) -> Bool {
guard let window else { return false }
let windowPoint = convert(point, to: nil)
return windowPoint.y >= BonsplitTabBarPassThrough.titlebarInteractionBandMinY(in: window)
guard windowPoint.y >= BonsplitTabBarPassThrough.titlebarInteractionBandMinY(in: window) else { return false }
return isMinimalModeTitlebarControlHit(window: window, locationInWindow: windowPoint)
|| !preservingHostedTerminal
|| hostedTerminalHitView(at: point) == nil
}

private func shouldPassThroughToPaneTabBar(
Expand Down
4 changes: 4 additions & 0 deletions cmux.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,7 @@
D0B10024A1B2C3D4E5F60001 /* WindowInputRoutingContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B10025A1B2C3D4E5F60001 /* WindowInputRoutingContext.swift */; };
313585583035A1E685010A97 /* WindowKeyDownReplayGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81B8CFAF8FCA4231C702D16A /* WindowKeyDownReplayGuard.swift */; };
8770D0F2D2BB45359D6BE5E3 /* WindowKeyDownReplayGuardTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79482F1ECA54E98BE5C8953 /* WindowKeyDownReplayGuardTests.swift */; };
606600010000000000000001 /* WindowTerminalHostViewTitlebarHitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 606600010000000000000002 /* WindowTerminalHostViewTitlebarHitTests.swift */; };
604500100000000000000001 /* WindowTitleTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 604500100000000000000002 /* WindowTitleTemplate.swift */; };
604500200000000000000001 /* WindowTitleTemplateContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 604500200000000000000002 /* WindowTitleTemplateContext.swift */; };
604500100000000000000003 /* WindowTitleTemplateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 604500100000000000000004 /* WindowTitleTemplateTests.swift */; };
Expand Down Expand Up @@ -1604,6 +1605,7 @@
D0B10025A1B2C3D4E5F60001 /* WindowInputRoutingContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowInputRoutingContext.swift; sourceTree = "<group>"; };
81B8CFAF8FCA4231C702D16A /* WindowKeyDownReplayGuard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/WindowKeyDownReplayGuard.swift; sourceTree = "<group>"; };
B79482F1ECA54E98BE5C8953 /* WindowKeyDownReplayGuardTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowKeyDownReplayGuardTests.swift; sourceTree = "<group>"; };
606600010000000000000002 /* WindowTerminalHostViewTitlebarHitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowTerminalHostViewTitlebarHitTests.swift; sourceTree = "<group>"; };
604500100000000000000002 /* WindowTitleTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/WindowTitleTemplate.swift; sourceTree = "<group>"; };
604500200000000000000002 /* WindowTitleTemplateContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/WindowTitleTemplateContext.swift; sourceTree = "<group>"; };
604500100000000000000004 /* WindowTitleTemplateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowTitleTemplateTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2480,6 +2482,7 @@
D0B1000DA1B2C3D4E5F60001 /* CmuxWebViewDragRoutingTests.swift */,
D0B1000FA1B2C3D4E5F60001 /* BrowserPaneDropRoutingTests.swift */,
02FC74F2C27127CC565B3E8C /* TerminalAndGhosttyTests.swift */,
606600010000000000000002 /* WindowTerminalHostViewTitlebarHitTests.swift */,
C0DE53360000000000000002 /* TerminalSearchOverlayMouseReleaseTests.swift */,
C35610000000000000000002 /* FinderFileDropRegressionTests.swift */,
C44550000000000000000002 /* PanelOwnedNativeViewSessionTests.swift */,
Expand Down Expand Up @@ -3793,6 +3796,7 @@
A59170000000000000000001 /* WindowAppearanceSnapshotPaneBackgroundTests.swift in Sources */,
F4200000A1B2C3D4E5F60718 /* WindowAppearanceSnapshotTests.swift in Sources */,
8770D0F2D2BB45359D6BE5E3 /* WindowKeyDownReplayGuardTests.swift in Sources */,
606600010000000000000001 /* WindowTerminalHostViewTitlebarHitTests.swift in Sources */,
604500100000000000000003 /* WindowTitleTemplateTests.swift in Sources */,
7F0A0E04A8CADC84BB4F1DF7 /* WorkspaceActionDispatcherTests.swift in Sources */,
D7AB00000000000000000011 /* WorkspaceAdjacentPaneMoveTests.swift in Sources */,
Expand Down
112 changes: 112 additions & 0 deletions cmuxTests/WindowTerminalHostViewTitlebarHitTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import AppKit
import Testing

#if canImport(cmux_DEV)
@testable import cmux_DEV
#elseif canImport(cmux)
@testable import cmux
#endif

@MainActor
@Suite("Window terminal host titlebar hit testing")
struct WindowTerminalHostViewTitlebarHitTests {
@Test func hostViewKeepsTerminalTopRowClickableInsideTitlebarBand() throws {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 420, height: 260),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
defer { window.orderOut(nil) }
let contentView = try #require(window.contentView, "Expected window content view")
let container = try #require(contentView.superview, "Expected window content container")

let host = WindowTerminalHostView(frame: container.convert(contentView.bounds, from: contentView))
let hostedView = makeHostedTerminalView(frame: host.bounds)
host.addSubview(hostedView)
container.addSubview(host, positioned: .above, relativeTo: contentView)

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 = try makeMouseDownEvent(at: pointInWindow, window: window)

try #require(
pointInWindow.y >= BonsplitTabBarPassThrough.titlebarInteractionBandMinY(in: window),
"The regression point must exercise the fixed-height titlebar pass-through band"
)
assertHitFallsInsideHostedTerminal(
host.performHitTest(at: pointInHost, currentEvent: event),
hostedView: hostedView,
message: "Terminal content inside the titlebar band should keep receiving top-row mouse-downs"
)
}

@Test func hostViewPassesThroughRegisteredTitlebarControlsAboveTerminal() throws {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 420, height: 260),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
defer { window.orderOut(nil) }
let contentView = try #require(window.contentView, "Expected window content view")
let container = try #require(contentView.superview, "Expected window content container")

let host = WindowTerminalHostView(frame: container.convert(contentView.bounds, from: contentView))
host.addSubview(makeHostedTerminalView(frame: host.bounds))
container.addSubview(host, positioned: .above, relativeTo: contentView)

let region = TitlebarInteractiveControlRegion.RegisteredView(
frame: NSRect(x: 24, y: contentView.bounds.maxY - 24, width: 18, height: 18)
)
contentView.addSubview(region)

let pointInWindow = contentView.convert(NSPoint(x: region.frame.midX, y: region.frame.midY), to: nil)
let pointInHost = host.convert(pointInWindow, from: nil)
let event = try makeMouseDownEvent(at: pointInWindow, window: window)

try #require(
pointInWindow.y >= BonsplitTabBarPassThrough.titlebarInteractionBandMinY(in: window),
"The control point must sit inside the fixed titlebar interaction band"
)
#expect(
host.performHitTest(at: pointInHost, currentEvent: event) == nil,
"Registered titlebar controls must keep receiving clicks even when terminal content underlaps them"
)
}

private func makeHostedTerminalView(frame: NSRect) -> GhosttySurfaceScrollView {
let surfaceView = GhosttyNSView(frame: frame)
let hostedView = GhosttySurfaceScrollView(surfaceView: surfaceView)
hostedView.frame = frame
hostedView.autoresizingMask = [.width, .height]
return hostedView
}

private func makeMouseDownEvent(at locationInWindow: NSPoint, window: NSWindow) throws -> NSEvent {
try #require(NSEvent.mouseEvent(
with: .leftMouseDown,
location: locationInWindow,
modifierFlags: [],
timestamp: ProcessInfo.processInfo.systemUptime,
windowNumber: window.windowNumber,
context: nil,
eventNumber: 0,
clickCount: 1,
pressure: 1.0
), "Failed to create leftMouseDown event")
}

private func assertHitFallsInsideHostedTerminal(
_ hitView: NSView?,
hostedView: GhosttySurfaceScrollView,
message: String
) {
guard let hitView else {
Issue.record(Comment(rawValue: message))
return
}
#expect(hitView === hostedView || hitView.isDescendant(of: hostedView), Comment(rawValue: message))
}
}
Loading