From f455af45414fec1751d7f08b489173c6b1851178 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Fri, 20 Feb 2026 14:51:44 -0800 Subject: [PATCH] Fix issue #185 overlay recursion hardening and scroll regression (#193) --- Sources/ContentView.swift | 14 ++++++-- tests/test_real_click_overlay_forwarding.py | 37 ++++++++++++++++++++- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 87e67ab5..239fe76f 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -171,6 +171,7 @@ final class SidebarState: ObservableObject { final class FileDropOverlayView: NSView { /// Fallback handler when no terminal is found under the drop point. var onDrop: (([URL]) -> Bool)? + private var isForwardingMouseEvent = false override var acceptsFirstResponder: Bool { false } @@ -207,12 +208,19 @@ final class FileDropOverlayView: NSView { // window.sendEvent(), which caches the mouse target and causes infinite recursion. private func forwardEvent(_ event: NSEvent) { + guard !isForwardingMouseEvent else { return } guard let window, let contentView = window.contentView else { return } + + isForwardingMouseEvent = true isHidden = true + defer { + isHidden = false + isForwardingMouseEvent = false + } + let point = contentView.convert(event.locationInWindow, from: nil) let target = contentView.hitTest(point) - isHidden = false - guard let target else { return } + guard let target, target !== self else { return } switch event.type { case .leftMouseDown: target.mouseDown(with: event) @@ -273,9 +281,9 @@ final class FileDropOverlayView: NSView { guard let window, let contentView = window.contentView else { return nil } isHidden = true + defer { isHidden = false } let point = contentView.convert(windowPoint, from: nil) let hitView = contentView.hitTest(point) - isHidden = false var current: NSView? = hitView while let view = current { diff --git a/tests/test_real_click_overlay_forwarding.py b/tests/test_real_click_overlay_forwarding.py index f38d7381..0e52eb85 100644 --- a/tests/test_real_click_overlay_forwarding.py +++ b/tests/test_real_click_overlay_forwarding.py @@ -104,6 +104,34 @@ up?.post(tap: .cghidEventTap) ) +def post_scroll_with_cgevent(x: float, y: float, delta_y: int = 3) -> None: + ix = int(round(x)) + iy = int(round(y)) + code = f""" +import CoreGraphics +let p = CGPoint(x: {ix}, y: {iy}) +let source = CGEventSource(stateID: .hidSystemState) +if let scroll = CGEvent( + scrollWheelEvent2Source: source, + units: .line, + wheelCount: 1, + wheel1: Int32({delta_y}), + wheel2: 0, + wheel3: 0 +) {{ + scroll.location = p + scroll.post(tap: .cghidEventTap) +}} +""" + subprocess.run( + ["swift", "-e", code], + check=True, + capture_output=True, + text=True, + timeout=10, + ) + + def pick_top_bottom_terminal_panels(layout: dict) -> tuple[dict, dict]: candidates = [] for panel in layout.get("selectedPanels", []): @@ -282,7 +310,14 @@ def main() -> int: print("FAIL: real right click disrupted terminal focus routing") return 1 - print("PASS: stale file-drag overlay forwards real left/right clicks") + for _ in range(6): + post_scroll_with_cgevent(click_x, click_y, delta_y=2) + time.sleep(0.25) + if not client.is_terminal_focused(bottom_id): + print("FAIL: real scroll wheel disrupted terminal focus routing") + return 1 + + print("PASS: stale file-drag overlay forwards real left/right clicks and scroll") print(f" focused_panel={bottom_id}") return 0 finally: