fix(computer): fence macOS HID coordinate clicks

This commit is contained in:
Jinwoo-H
2026-08-06 23:38:26 -04:00
parent 1aaa5cfbd9
commit efc160a6bb
10 changed files with 674 additions and 105 deletions
@@ -6,6 +6,22 @@
/// happens). The window server also drops a mouseUp posted back-to-back
/// with its mouseDown, so consecutive events need a pause between them.
public enum SyntheticMouseClickDelivery {
public static let maxClickCount = 3
public struct Recipient: Equatable, Sendable {
public let ownerPID: Int32
public let windowID: UInt32
public init(ownerPID: Int32, windowID: UInt32) {
self.ownerPID = ownerPID
self.windowID = windowID
}
}
public enum FenceFailure: Error, Equatable {
case recipientChanged(expected: Recipient, actual: Recipient?, deliveredPresses: Int)
}
public enum Step: Equatable {
case move
case buttonDown(pressIndex: Int)
@@ -21,7 +37,7 @@ public enum SyntheticMouseClickDelivery {
/// clicks instead of independent single clicks.
public static func steps(clickCount: Int) -> [Step] {
var steps: [Step] = [.move]
for press in 1...max(clickCount, 1) {
for press in 1...min(max(clickCount, 1), maxClickCount) {
steps.append(.buttonDown(pressIndex: press))
steps.append(.buttonUp(pressIndex: press))
}
@@ -37,4 +53,52 @@ public enum SyntheticMouseClickDelivery {
return Int64(pressIndex)
}
}
public static func uniqueWindowCandidate<Candidate>(
from candidates: [Candidate],
matching predicate: (Candidate) -> Bool
) -> Candidate? {
var match: Candidate?
for candidate in candidates where predicate(candidate) {
guard match == nil else { return nil }
match = candidate
}
return match
}
public static func deliver<Event>(
clickCount: Int,
target: Recipient,
currentRecipient: () -> Recipient?,
makeEvent: (Step) throws -> Event,
post: (Event) -> Void,
pause: (UInt32) -> Void
) throws {
post(try makeEvent(.move))
pause(interEventPauseMicroseconds)
for pressIndex in 1...min(max(clickCount, 1), maxClickCount) {
let beforeDown = currentRecipient()
guard beforeDown == target else {
throw FenceFailure.recipientChanged(
expected: target,
actual: beforeDown,
deliveredPresses: pressIndex - 1
)
}
let down = try makeEvent(.buttonDown(pressIndex: pressIndex))
let up = try makeEvent(.buttonUp(pressIndex: pressIndex))
post(down)
pause(interEventPauseMicroseconds)
post(up)
let afterUp = currentRecipient()
guard afterUp == target else {
throw FenceFailure.recipientChanged(
expected: target,
actual: afterUp,
deliveredPresses: pressIndex
)
}
pause(interEventPauseMicroseconds)
}
}
}