diff --git a/Sources/TerminalWindowPortal.swift b/Sources/TerminalWindowPortal.swift index 437c0d07639..acd05b5c625 100644 --- a/Sources/TerminalWindowPortal.swift +++ b/Sources/TerminalWindowPortal.swift @@ -119,20 +119,21 @@ 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. + // Test seam: production calls read `NSApp.currentEvent`; tests pass 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) { + let resolveHostedTerminalHitView = hostedTerminalHitViewResolver(at: point) + + if shouldPassThroughToTitlebar(at: point, hostedTerminalHitView: resolveHostedTerminalHitView) { clearActiveDividerCursor(restoreArrow: false) return nil } - if shouldPassThroughToPaneTabBar(at: point, eventType: currentEvent?.type) { + if shouldPassThroughToPaneTabBar(at: point, eventType: currentEvent?.type, hostedTerminalHitView: resolveHostedTerminalHitView) { clearActiveDividerCursor(restoreArrow: false) return nil } @@ -142,7 +143,6 @@ final class WindowTerminalHostView: NSView { return nil } - // Compute divider hit once and reuse for both cursor update and pass-through. if let kind = splitDividerCursorKind(at: point) { activeDividerCursorKind = kind kind.cursor.set() @@ -203,15 +203,18 @@ final class WindowTerminalHostView: NSView { return hitView === self ? nil : hitView } - private func shouldPassThroughToTitlebar(at point: NSPoint) -> Bool { + private func shouldPassThroughToTitlebar(at point: NSPoint, hostedTerminalHitView: () -> NSView?) -> 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 } + if isMinimalModeTitlebarControlHit(window: window, locationInWindow: windowPoint) { return true } + return hostedTerminalHitView() == nil } private func shouldPassThroughToPaneTabBar( at point: NSPoint, - eventType: NSEvent.EventType? + eventType: NSEvent.EventType?, + hostedTerminalHitView: () -> NSView? ) -> Bool { guard let decision = BonsplitTabBarPassThrough.passThroughDecision( at: point, @@ -219,27 +222,15 @@ final class WindowTerminalHostView: NSView { eventType: eventType ) else { return false } guard decision.result else { return false } - if decision.registryHit { - return true - } - 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 + if decision.registryHit { return true } + return hostedTerminalHitView() == nil } private func shouldPassThroughToChrome(at point: NSPoint, eventType: NSEvent.EventType?) -> Bool { - shouldPassThroughToTitlebar(at: point) - || shouldPassThroughToPaneTabBar(at: point, eventType: eventType) + let resolveHostedTerminalHitView = hostedTerminalHitViewResolver(at: point) + + return shouldPassThroughToTitlebar(at: point, hostedTerminalHitView: resolveHostedTerminalHitView) + || shouldPassThroughToPaneTabBar(at: point, eventType: eventType, hostedTerminalHitView: resolveHostedTerminalHitView) } private func cursorRectIntersectsChromePassThrough(_ rect: NSRect) -> Bool { diff --git a/Sources/WindowDecorationsController.swift b/Sources/WindowDecorationsController.swift index 733cccde707..331e835081e 100644 --- a/Sources/WindowDecorationsController.swift +++ b/Sources/WindowDecorationsController.swift @@ -219,7 +219,7 @@ final class WindowDecorationsController { lastMinimalModeTitlebarClick = nil return false } - guard !isMinimalModeTitlebarControlHit(window: window, locationInWindow: locationInWindow) else { + guard !minimalModeTitlebarDoubleClickShouldDefer(window: window, locationInWindow: locationInWindow) else { lastMinimalModeTitlebarClick = nil return false } diff --git a/Sources/WindowDragHandleView.swift b/Sources/WindowDragHandleView.swift index 2a121002212..9915d203071 100644 --- a/Sources/WindowDragHandleView.swift +++ b/Sources/WindowDragHandleView.swift @@ -1349,13 +1349,6 @@ struct WindowDragHandleView: NSViewRepresentable { } } -private func titlebarDoubleClickMonitorShouldDeferToRegisteredControl( - window: NSWindow, - locationInWindow: NSPoint -) -> Bool { - isMinimalModeTitlebarControlHit(window: window, locationInWindow: locationInWindow) -} - /// Local monitor that guarantees double-clicks in custom titlebar surfaces trigger /// the standard macOS titlebar action even when the visible strip is hosted by /// higher-level SwiftUI/AppKit container views. @@ -1395,7 +1388,7 @@ struct TitlebarDoubleClickMonitorView: NSViewRepresentable { coordinator.lastClick = nil return event } - guard !titlebarDoubleClickMonitorShouldDeferToRegisteredControl( + guard !minimalModeTitlebarDoubleClickShouldDefer( window: window, locationInWindow: event.locationInWindow ) else { @@ -1686,7 +1679,10 @@ struct MinimalModeTitlebarEventSurfaceView: NSViewRepresentable { lastTitlebarClick = nil return event } - guard !isMinimalModeTitlebarControlHit(window: window, locationInWindow: locationInWindow) else { + guard !minimalModeTitlebarDoubleClickShouldDefer( + window: window, + locationInWindow: locationInWindow + ) else { lastTitlebarClick = nil return event } diff --git a/Sources/WindowTerminalHostViewHitTesting.swift b/Sources/WindowTerminalHostViewHitTesting.swift new file mode 100644 index 00000000000..6a9b3751988 --- /dev/null +++ b/Sources/WindowTerminalHostViewHitTesting.swift @@ -0,0 +1,76 @@ +import AppKit + +extension WindowTerminalHostView { + 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 + } + + func hostedTerminalHitViewResolver(at point: NSPoint) -> () -> NSView? { + var cachedHitView: NSView? + var didResolve = false + return { + if !didResolve { + cachedHitView = self.hostedTerminalHitView(at: point) + didResolve = true + } + return cachedHitView + } + } + + func hasHostedTerminal(at point: NSPoint) -> Bool { + hasHostedTerminal(at: point, in: self) + } + + private func hasHostedTerminal(at point: NSPoint, in view: NSView) -> Bool { + guard !view.isHidden, view.alphaValue > 0 else { return false } + let pointInView = view.convert(point, from: self) + guard view.bounds.contains(pointInView) else { return false } + if view is GhosttySurfaceScrollView { return true } + for subview in view.subviews.reversed() { + if hasHostedTerminal(at: point, in: subview) { return true } + } + return false + } + + static func hasHostedTerminal(atWindowPoint windowPoint: NSPoint, in window: NSWindow) -> Bool { + guard let rootView = window.contentView?.superview ?? window.contentView else { return false } + return hasHostedTerminal(atWindowPoint: windowPoint, in: rootView) + } + + private static func hasHostedTerminal(atWindowPoint windowPoint: NSPoint, in view: NSView) -> Bool { + guard !view.isHidden, view.alphaValue > 0 else { return false } + let pointInView = view.convert(windowPoint, from: nil) + guard view.bounds.contains(pointInView) else { return false } + + if let hostView = view as? WindowTerminalHostView { + let pointInHost = hostView.convert(windowPoint, from: nil) + if hostView.hasHostedTerminal(at: pointInHost) { + return true + } + } + + for subview in view.subviews.reversed() { + if hasHostedTerminal(atWindowPoint: windowPoint, in: subview) { + return true + } + } + + return false + } +} + +func minimalModeTitlebarDoubleClickShouldDefer( + window: NSWindow, + locationInWindow: NSPoint +) -> Bool { + isMinimalModeTitlebarControlHit(window: window, locationInWindow: locationInWindow) + || WindowTerminalHostView.hasHostedTerminal(atWindowPoint: locationInWindow, in: window) +} diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index ca8b600c27b..7894aa47315 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -783,6 +783,8 @@ 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 */; }; + 606600020000000000000001 /* WindowTerminalHostViewHitTesting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 606600020000000000000002 /* WindowTerminalHostViewHitTesting.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 */; }; @@ -1612,6 +1614,8 @@ D0B10025A1B2C3D4E5F60001 /* WindowInputRoutingContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowInputRoutingContext.swift; sourceTree = ""; }; 81B8CFAF8FCA4231C702D16A /* WindowKeyDownReplayGuard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/WindowKeyDownReplayGuard.swift; sourceTree = ""; }; B79482F1ECA54E98BE5C8953 /* WindowKeyDownReplayGuardTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowKeyDownReplayGuardTests.swift; sourceTree = ""; }; + 606600020000000000000002 /* WindowTerminalHostViewHitTesting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowTerminalHostViewHitTesting.swift; sourceTree = ""; }; + 606600010000000000000002 /* WindowTerminalHostViewTitlebarHitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowTerminalHostViewTitlebarHitTests.swift; sourceTree = ""; }; 604500100000000000000002 /* WindowTitleTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/WindowTitleTemplate.swift; sourceTree = ""; }; 604500200000000000000002 /* WindowTitleTemplateContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/WindowTitleTemplateContext.swift; sourceTree = ""; }; 604500100000000000000004 /* WindowTitleTemplateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowTitleTemplateTests.swift; sourceTree = ""; }; @@ -2104,6 +2108,7 @@ D0B1001DA1B2C3D4E5F60001 /* PaneDropRoutingSupport.swift */, A5001531 /* TerminalWindowPortal.swift */, C750500000000000000000B2 /* TerminalSurfaceRuntimeWiring.swift */, + 606600020000000000000002 /* WindowTerminalHostViewHitTesting.swift */, D0B10007A1B2C3D4E5F60001 /* TerminalWindowPortalDebug.swift */, D0B1001FA1B2C3D4E5F60001 /* BrowserPaneDropTargetView.swift */, A5001533 /* BrowserWindowPortal.swift */, @@ -2494,6 +2499,7 @@ D0B1000DA1B2C3D4E5F60001 /* CmuxWebViewDragRoutingTests.swift */, D0B1000FA1B2C3D4E5F60001 /* BrowserPaneDropRoutingTests.swift */, 02FC74F2C27127CC565B3E8C /* TerminalAndGhosttyTests.swift */, + 606600010000000000000002 /* WindowTerminalHostViewTitlebarHitTests.swift */, C0DE53360000000000000002 /* TerminalSearchOverlayMouseReleaseTests.swift */, C35610000000000000000002 /* FinderFileDropRegressionTests.swift */, C44550000000000000000002 /* PanelOwnedNativeViewSessionTests.swift */, @@ -3501,6 +3507,7 @@ 807E058A23061EFB70A1B7F8 /* WindowGlassEffect.swift in Sources */, D0B10024A1B2C3D4E5F60001 /* WindowInputRoutingContext.swift in Sources */, 313585583035A1E685010A97 /* WindowKeyDownReplayGuard.swift in Sources */, + 606600020000000000000001 /* WindowTerminalHostViewHitTesting.swift in Sources */, 604500100000000000000001 /* WindowTitleTemplate.swift in Sources */, 604500200000000000000001 /* WindowTitleTemplateContext.swift in Sources */, A5001209 /* WindowToolbarController.swift in Sources */, @@ -3813,6 +3820,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 */, diff --git a/cmuxTests/TitlebarInteractiveControlTests.swift b/cmuxTests/TitlebarInteractiveControlTests.swift index b3fc8a9c468..978161b3cdb 100644 --- a/cmuxTests/TitlebarInteractiveControlTests.swift +++ b/cmuxTests/TitlebarInteractiveControlTests.swift @@ -85,13 +85,13 @@ struct TitlebarInteractiveControlTests { let insideControl = NSPoint(x: region.frame.midX, y: region.frame.midY) #expect( - isMinimalModeTitlebarControlHit(window: window, locationInWindow: insideControl), + minimalModeTitlebarDoubleClickShouldDefer(window: window, locationInWindow: insideControl), "A double-click on a titlebarInteractiveControl must register as a control hit so the synthetic titlebar double-click (zoom/minimize) is suppressed." ) let emptyTitlebar = NSPoint(x: 220, y: 24) #expect( - !isMinimalModeTitlebarControlHit(window: window, locationInWindow: emptyTitlebar), + !minimalModeTitlebarDoubleClickShouldDefer(window: window, locationInWindow: emptyTitlebar), "Empty titlebar chrome away from any interactive control must still trigger the standard titlebar double-click action." ) } diff --git a/cmuxTests/WindowTerminalHostViewTitlebarHitTests.swift b/cmuxTests/WindowTerminalHostViewTitlebarHitTests.swift new file mode 100644 index 00000000000..0172ce594b9 --- /dev/null +++ b/cmuxTests/WindowTerminalHostViewTitlebarHitTests.swift @@ -0,0 +1,190 @@ +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 titlebarDoubleClickMonitorDefersToTerminalTopRowInsideTitlebarBand() 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 wrapperView = NSView(frame: host.bounds) + wrapperView.autoresizingMask = [.width, .height] + let hostedView = makeHostedTerminalView(frame: host.bounds) + wrapperView.addSubview(hostedView) + host.addSubview(wrapperView) + 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) + + try #require( + pointInWindow.y >= BonsplitTabBarPassThrough.titlebarInteractionBandMinY(in: window), + "The regression point must exercise the fixed-height titlebar pass-through band" + ) + #expect( + minimalModeTitlebarDoubleClickShouldDefer(window: window, locationInWindow: pointInWindow), + "Synthetic titlebar double-click handling must yield to hosted terminal content in the top row" + ) + } + + @Test func windowDecorationsDoubleClickHandlerDefersToTerminalTopRowInsideTitlebarBand() throws { + let defaults = UserDefaults.standard + let savedMode = defaults.object(forKey: WorkspacePresentationModeSettings.modeKey) + defaults.set(WorkspacePresentationModeSettings.Mode.minimal.rawValue, forKey: WorkspacePresentationModeSettings.modeKey) + defer { + if let savedMode { + defaults.set(savedMode, forKey: WorkspacePresentationModeSettings.modeKey) + } else { + defaults.removeObject(forKey: WorkspacePresentationModeSettings.modeKey) + } + } + + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 420, height: 260), + styleMask: [.titled, .closable, .resizable], + backing: .buffered, + defer: false + ) + window.identifier = NSUserInterfaceItemIdentifier("cmux.main.test") + 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 event = try makeMouseDownEvent(at: pointInWindow, window: window, clickCount: 2) + + try #require( + pointInWindow.y >= BonsplitTabBarPassThrough.titlebarInteractionBandMinY(in: window), + "The regression point must exercise the fixed-height titlebar pass-through band" + ) + #expect( + !WindowDecorationsController().handleMinimalModeTitlebarDoubleClickMouseDown(event: event), + "The app-level titlebar double-click handler must not consume terminal top-row double-clicks" + ) + } + + @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, + clickCount: Int = 1 + ) throws -> NSEvent { + try #require(NSEvent.mouseEvent( + with: .leftMouseDown, + location: locationInWindow, + modifierFlags: [], + timestamp: ProcessInfo.processInfo.systemUptime, + windowNumber: window.windowNumber, + context: nil, + eventNumber: 0, + clickCount: clickCount, + 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)) + } +}