From 3e7604213ceacfadd2293bdc7c94207c4f0d8ba4 Mon Sep 17 00:00:00 2001 From: Kyle Date: Fri, 16 Oct 2026 02:24:50 +0800 Subject: [PATCH] fix(appkit): map synthetic drags with the current window frame --- .../EventGenerator/EventGenerator.swift | 18 ++++++++++- Sources/Hammer/Utilties/HammerWindow.swift | 31 +++++++++++++++++-- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/Sources/Hammer/EventGenerator/EventGenerator.swift b/Sources/Hammer/EventGenerator/EventGenerator.swift index bd20218..a850b3c 100644 --- a/Sources/Hammer/EventGenerator/EventGenerator.swift +++ b/Sources/Hammer/EventGenerator/EventGenerator.swift @@ -330,7 +330,23 @@ public final class EventGenerator { } public var isWindowReady: Bool { - return self.hasVisibleWindowContent && self.window.isKeyWindow + return self.hasVisibleWindowContent && self.window.isKeyWindow && self.mapsCGEventLocations + } + + /// Whether `NSEvent(cgEvent:)` maps window locations with the window's current frame. + /// + /// Dragged events go through a `CGEvent` to carry deltas. `NSEvent(cgEvent:)` maps the + /// global location back with window server geometry instead of `NSWindow.frame`, and + /// AppKit updates that geometry asynchronously after the window is created or moved. + private var mapsCGEventLocations: Bool { + let location = NSPoint.zero + guard let event = NSEvent.mouseEvent( + with: .mouseMoved, location: location, modifierFlags: [], timestamp: 0, + windowNumber: self.window.windowNumber, context: nil, eventNumber: 0, clickCount: 0, pressure: 0 + ), let cgEvent = event.cgEvent, let mappedEvent = NSEvent(cgEvent: cgEvent) else { + return false + } + return mappedEvent.locationInWindow == location } private var hasVisibleWindowContent: Bool { diff --git a/Sources/Hammer/Utilties/HammerWindow.swift b/Sources/Hammer/Utilties/HammerWindow.swift index b07117c..9902563 100644 --- a/Sources/Hammer/Utilties/HammerWindow.swift +++ b/Sources/Hammer/Utilties/HammerWindow.swift @@ -149,12 +149,17 @@ import AppKit /// A reusable test window that receives events without taking application focus. /// -/// The window stays outside all screens by default. Set `HAMMER_SHOW_TEST_WINDOW=1` -/// or pass `showWindow: true` to display it while debugging. +/// The window stays at a fixed position outside all screens by default. Set +/// `HAMMER_SHOW_TEST_WINDOW=1` or pass `showWindow: true` to display it while debugging. @MainActor public final class HammerWindow: NSWindow { private var isClosed = false + // The window server applies frame changes asynchronously, and NSEvent(cgEvent:) maps + // locations with its geometry. Like WebKitTestRunnerWindow, keep the position fixed and + // apply only size changes so synthetic events do not race a pending move. + private var keepsOriginFixed = false + // AppKit must treat this test surface as key to dispatch the first mouse down. // Keep this state local to the window instead of changing NSApplication.keyWindow. public override var isKeyWindow: Bool { !self.isClosed && self.isVisible } @@ -169,9 +174,11 @@ public final class HammerWindow: NSWindow { public init(size: CGSize, showWindow: Bool = ProcessInfo.processInfo.environment["HAMMER_SHOW_TEST_WINDOW"] == "1") { let screens = NSScreen.screens.reduce(NSRect.zero) { $0.union($1.frame) } - let origin = NSPoint(x: screens.minX - size.width - 100, y: screens.minY) + // Leave room for later size changes, because the origin no longer follows them. + let origin = NSPoint(x: screens.minX - 10_000, y: screens.minY) super.init(contentRect: NSRect(origin: origin, size: size), styleMask: showWindow ? [.titled] : [.borderless], backing: .buffered, defer: false) + self.keepsOriginFixed = !showWindow self.isReleasedWhenClosed = false self.animationBehavior = .none self.hasShadow = false @@ -190,5 +197,23 @@ public final class HammerWindow: NSWindow { self.isClosed = true super.close() } + + public override func setFrameOrigin(_ point: NSPoint) { + guard !self.keepsOriginFixed else { return } + super.setFrameOrigin(point) + } + + public override func setFrame(_ frameRect: NSRect, display flag: Bool) { + super.setFrame(self.frameKeepingOrigin(frameRect), display: flag) + } + + public override func setFrame(_ frameRect: NSRect, display displayFlag: Bool, animate animateFlag: Bool) { + super.setFrame(self.frameKeepingOrigin(frameRect), display: displayFlag, animate: animateFlag) + } + + private func frameKeepingOrigin(_ frameRect: NSRect) -> NSRect { + guard self.keepsOriginFixed else { return frameRect } + return NSRect(origin: self.frame.origin, size: frameRect.size) + } } #endif