From e2fb37faadc6b1d55c0e8894e2ad1e69d7d24e63 Mon Sep 17 00:00:00 2001 From: Micah Alpern Date: Thu, 30 Jul 2026 17:31:20 -0700 Subject: [PATCH] Let pointer presses reach the keyboard stage's hit targets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Metal stage is a real NSView, and without a hitTest override it returned itself for every point inside the keyboard — swallowing mouse events before the semantic overlay's per-key hit targets stacked above it could see them. Clicking a key worked in the SwiftUI fallback and silently did nothing whenever Metal was active, which is the shipped default. That contradicts the plan's own boundary: the renderer is visual output, and the native overlay owns pointer interaction and accessibility. Return nil from hitTest so events fall through to the overlay, and pin the contract with a regression test (verified failing without the fix). Co-Authored-By: Claude Fable 5 --- .../Metal/KeyboardStageMetalView.swift | 10 ++++++++++ .../KeyboardStageInteractionTests.swift | 15 +++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/Sources/KeyPathAppKit/UI/KeyboardStage/Metal/KeyboardStageMetalView.swift b/Sources/KeyPathAppKit/UI/KeyboardStage/Metal/KeyboardStageMetalView.swift index 699e26ca9..71484035b 100644 --- a/Sources/KeyPathAppKit/UI/KeyboardStage/Metal/KeyboardStageMetalView.swift +++ b/Sources/KeyPathAppKit/UI/KeyboardStage/Metal/KeyboardStageMetalView.swift @@ -244,6 +244,16 @@ final class KeyboardStageMTKView: MTKView { private var retryTask: Task? private var reportedWindowX = SIMD2(0, 1) + /// The renderer is visual output only; the native semantic overlay stacked + /// above it owns pointer interaction and accessibility. As a real `NSView` + /// this would otherwise return itself for every point inside the keyboard + /// and swallow clicks before those hit targets could see them, so pointer + /// presses worked in the SwiftUI fallback and silently failed whenever + /// Metal was active. + override func hitTest(_: NSPoint) -> NSView? { + nil + } + override func viewDidMoveToWindow() { super.viewDidMoveToWindow() stopObservingWindow() diff --git a/Tests/KeyPathTests/KeyboardStage/KeyboardStageInteractionTests.swift b/Tests/KeyPathTests/KeyboardStage/KeyboardStageInteractionTests.swift index 13f89bb62..7da309c72 100644 --- a/Tests/KeyPathTests/KeyboardStage/KeyboardStageInteractionTests.swift +++ b/Tests/KeyPathTests/KeyboardStage/KeyboardStageInteractionTests.swift @@ -1,7 +1,22 @@ +import AppKit @testable import KeyPathAppKit import XCTest final class KeyboardStageInteractionTests: XCTestCase { + /// The Metal view must stay transparent to the mouse. It is a real NSView, + /// so without this it returns itself for every point inside the keyboard + /// and swallows clicks before the semantic overlay's per-key hit targets + /// receive them — pointer presses then work only in the SwiftUI fallback. + @MainActor + func testMetalStageViewPassesPointerEventsToTheOverlayAbove() { + let view = KeyboardStageMTKView( + frame: NSRect(x: 0, y: 0, width: 400, height: 300) + ) + + XCTAssertNil(view.hitTest(NSPoint(x: 200, y: 150))) + XCTAssertNil(view.hitTest(NSPoint(x: 1, y: 1))) + } + func testKeyDownFeedbackIsImmediate() { var presentation = KeyboardStageInteractionPresentation() let pressed = KeyboardStageInteractionState(