From a723bbaa6a0d3aa4a964366e7ba7af1966d3771a Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:33:35 -0800 Subject: [PATCH] Fix Finder file drop routing for portal-hosted terminals --- .../CmuxWebViewKeyEquivalentTests.swift | 29 +++++++++++++++++++ Sources/GhosttyTerminalView.swift | 5 ++++ Sources/TerminalWindowPortal.swift | 19 ++++++++---- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/GhosttyTabsTests/CmuxWebViewKeyEquivalentTests.swift b/GhosttyTabsTests/CmuxWebViewKeyEquivalentTests.swift index e083fb1a..daf5706a 100644 --- a/GhosttyTabsTests/CmuxWebViewKeyEquivalentTests.swift +++ b/GhosttyTabsTests/CmuxWebViewKeyEquivalentTests.swift @@ -1858,4 +1858,33 @@ final class TerminalWindowPortalLifecycleTests: XCTestCase { XCTAssertEqual(portal.debugEntryCount(), 1, "Only the live anchored hosted view should remain tracked") XCTAssertEqual(portal.debugHostedSubviewCount(), 1, "Stale anchorless hosted views should be detached from hostView") } + + func testTerminalViewAtWindowPointResolvesPortalHostedSurface() { + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 500, height: 300), + styleMask: [.titled, .closable], + backing: .buffered, + defer: false + ) + let portal = WindowTerminalPortal(window: window) + guard let contentView = window.contentView else { + XCTFail("Expected content view") + return + } + + let anchor = NSView(frame: NSRect(x: 40, y: 50, width: 200, height: 120)) + contentView.addSubview(anchor) + + let hosted = GhosttySurfaceScrollView( + surfaceView: GhosttyNSView(frame: NSRect(x: 0, y: 0, width: 100, height: 80)) + ) + portal.bind(hostedView: hosted, to: anchor, visibleInUI: true) + + let center = NSPoint(x: anchor.bounds.midX, y: anchor.bounds.midY) + let windowPoint = anchor.convert(center, to: nil) + XCTAssertNotNil( + portal.terminalViewAtWindowPoint(windowPoint), + "Portal hit-testing should resolve the terminal view for Finder file drops" + ) + } } diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index b56bcd6b..a0d4bc88 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -3049,6 +3049,11 @@ final class GhosttySurfaceScrollView: NSView { return true } + func terminalViewForDrop(at point: NSPoint) -> GhosttyNSView? { + guard bounds.contains(point), !isHidden else { return nil } + return surfaceView + } + #if DEBUG /// Sends a synthetic Ctrl+D key press directly to the surface view. /// This exercises the same key path as real keyboard input (ghostty_surface_key), diff --git a/Sources/TerminalWindowPortal.swift b/Sources/TerminalWindowPortal.swift index f71197e4..7faf6949 100644 --- a/Sources/TerminalWindowPortal.swift +++ b/Sources/TerminalWindowPortal.swift @@ -261,12 +261,21 @@ final class WindowTerminalPortal: NSObject { } func terminalViewAtWindowPoint(_ windowPoint: NSPoint) -> GhosttyNSView? { - guard let hitView = viewAtWindowPoint(windowPoint) else { return nil } - var current: NSView? = hitView - while let view = current { - if let terminal = view as? GhosttyNSView { return terminal } - current = view.superview + guard ensureInstalled() else { return nil } + let point = hostView.convert(windowPoint, from: nil) + + for subview in hostView.subviews.reversed() { + guard let hostedView = subview as? GhosttySurfaceScrollView else { continue } + let hostedId = ObjectIdentifier(hostedView) + guard entriesByHostedId[hostedId] != nil else { continue } + guard !hostedView.isHidden else { continue } + guard hostedView.frame.contains(point) else { continue } + let localPoint = hostedView.convert(point, from: hostView) + if let terminal = hostedView.terminalViewForDrop(at: localPoint) { + return terminal + } } + return nil } }