239 lines
7.7 KiB
Swift
239 lines
7.7 KiB
Swift
import AppKit
|
|
import CoreGraphics
|
|
import Foundation
|
|
|
|
struct MouseScrollEventSnapshot: Equatable, Sendable {
|
|
var isContinuous: Bool
|
|
var scrollPhase: Int64
|
|
var momentumPhase: Int64
|
|
|
|
static let discreteWheel = MouseScrollEventSnapshot(
|
|
isContinuous: false,
|
|
scrollPhase: 0,
|
|
momentumPhase: 0
|
|
)
|
|
|
|
static let phaseLessContinuousWheel = MouseScrollEventSnapshot(
|
|
isContinuous: true,
|
|
scrollPhase: 0,
|
|
momentumPhase: 0
|
|
)
|
|
}
|
|
|
|
struct MouseScrollDeltas: Equatable, Sendable {
|
|
var deltaAxis1: Int64
|
|
var deltaAxis2: Int64
|
|
var pointDeltaAxis1: Int64
|
|
var pointDeltaAxis2: Int64
|
|
var fixedPointDeltaAxis1: Double
|
|
var fixedPointDeltaAxis2: Double
|
|
}
|
|
|
|
struct MouseScrollProcessingResult: Equatable, Sendable {
|
|
var source: MouseEnhancerDevice
|
|
var shouldReverse: Bool
|
|
var reverseHorizontal: Bool
|
|
var reverseVertical: Bool
|
|
var deltas: MouseScrollDeltas
|
|
}
|
|
|
|
final class MouseScrollEventProcessor: @unchecked Sendable {
|
|
private enum Timing {
|
|
static let touchRecentThreshold: UInt64 = 222_000_000
|
|
static let touchStaleThreshold: UInt64 = 333_000_000
|
|
}
|
|
|
|
nonisolated(unsafe) var configuration: MouseEnhancerConfiguration
|
|
nonisolated(unsafe) private var touchingCount = 0
|
|
nonisolated(unsafe) private var lastTouchTime: UInt64 = 0
|
|
nonisolated(unsafe) private var lastSource: MouseEnhancerDevice = .mouse
|
|
nonisolated(unsafe) private var hasSeenGestureEvent = false
|
|
nonisolated(unsafe) private var gestureMonitoringAvailable = false
|
|
|
|
init(configuration: MouseEnhancerConfiguration) {
|
|
self.configuration = configuration
|
|
}
|
|
|
|
func resetClassificationState() {
|
|
touchingCount = 0
|
|
lastTouchTime = 0
|
|
lastSource = .mouse
|
|
hasSeenGestureEvent = false
|
|
}
|
|
|
|
func recordGestureTouchingCount(_ count: Int, timestamp: UInt64 = DispatchTime.now().uptimeNanoseconds) {
|
|
hasSeenGestureEvent = true
|
|
guard count >= 2 else {
|
|
return
|
|
}
|
|
|
|
lastTouchTime = timestamp
|
|
touchingCount = max(touchingCount, count)
|
|
}
|
|
|
|
func setGestureMonitoringAvailable(_ isAvailable: Bool) {
|
|
gestureMonitoringAvailable = isAvailable
|
|
if !isAvailable {
|
|
resetClassificationState()
|
|
}
|
|
}
|
|
|
|
func classify(
|
|
snapshot: MouseScrollEventSnapshot,
|
|
timestamp: UInt64 = DispatchTime.now().uptimeNanoseconds
|
|
) -> MouseEnhancerDevice {
|
|
let touching = touchingCount
|
|
touchingCount = 0
|
|
|
|
guard snapshot.isContinuous else {
|
|
lastSource = .mouse
|
|
return .mouse
|
|
}
|
|
|
|
let elapsed = lastTouchTime == 0 ? UInt64.max : timestamp &- lastTouchTime
|
|
if touching >= 2, elapsed < Timing.touchRecentThreshold {
|
|
lastSource = .trackpad
|
|
return .trackpad
|
|
}
|
|
|
|
if snapshot.hasNoGesturePhase {
|
|
lastSource = .mouse
|
|
return .mouse
|
|
}
|
|
|
|
guard gestureMonitoringAvailable || hasSeenGestureEvent else {
|
|
lastSource = .trackpad
|
|
return .trackpad
|
|
}
|
|
|
|
if snapshot.isNormalScroll, elapsed > Timing.touchStaleThreshold {
|
|
lastSource = .mouse
|
|
return .mouse
|
|
}
|
|
|
|
return lastSource
|
|
}
|
|
|
|
func process(
|
|
snapshot: MouseScrollEventSnapshot,
|
|
deltas: MouseScrollDeltas,
|
|
timestamp: UInt64 = DispatchTime.now().uptimeNanoseconds
|
|
) -> MouseScrollProcessingResult {
|
|
let source = classify(snapshot: snapshot, timestamp: timestamp)
|
|
let shouldReverse = configuration.shouldReverse(device: source)
|
|
guard shouldReverse else {
|
|
return MouseScrollProcessingResult(
|
|
source: source,
|
|
shouldReverse: false,
|
|
reverseHorizontal: false,
|
|
reverseVertical: false,
|
|
deltas: deltas
|
|
)
|
|
}
|
|
|
|
let reverseHorizontal = configuration.shouldReverseHorizontal(device: source)
|
|
let reverseVertical = configuration.shouldReverseVertical(device: source)
|
|
return MouseScrollProcessingResult(
|
|
source: source,
|
|
shouldReverse: true,
|
|
reverseHorizontal: reverseHorizontal,
|
|
reverseVertical: reverseVertical,
|
|
deltas: Self.reversed(
|
|
deltas: deltas,
|
|
reverseHorizontal: reverseHorizontal,
|
|
reverseVertical: reverseVertical
|
|
)
|
|
)
|
|
}
|
|
|
|
@discardableResult
|
|
func process(event: CGEvent) -> MouseScrollProcessingResult {
|
|
let snapshot = MouseScrollEventSnapshot(event: event)
|
|
let deltas = MouseScrollDeltas(event: event)
|
|
let result = process(snapshot: snapshot, deltas: deltas)
|
|
|
|
guard result.shouldReverse else {
|
|
return result
|
|
}
|
|
|
|
event.applyScrollDeltas(
|
|
result.deltas,
|
|
reverseHorizontal: result.reverseHorizontal,
|
|
reverseVertical: result.reverseVertical
|
|
)
|
|
return result
|
|
}
|
|
|
|
private static func reversed(
|
|
deltas: MouseScrollDeltas,
|
|
reverseHorizontal: Bool,
|
|
reverseVertical: Bool
|
|
) -> MouseScrollDeltas {
|
|
var next = deltas
|
|
if reverseVertical {
|
|
next.deltaAxis1 = -next.deltaAxis1
|
|
next.pointDeltaAxis1 = -next.pointDeltaAxis1
|
|
next.fixedPointDeltaAxis1 = -next.fixedPointDeltaAxis1
|
|
}
|
|
if reverseHorizontal {
|
|
next.deltaAxis2 = -next.deltaAxis2
|
|
next.pointDeltaAxis2 = -next.pointDeltaAxis2
|
|
next.fixedPointDeltaAxis2 = -next.fixedPointDeltaAxis2
|
|
}
|
|
return next
|
|
}
|
|
}
|
|
|
|
private extension MouseScrollEventSnapshot {
|
|
init(event: CGEvent) {
|
|
self.init(
|
|
isContinuous: event.getIntegerValueField(.scrollWheelEventIsContinuous) != 0,
|
|
scrollPhase: event.getIntegerValueField(.scrollWheelEventScrollPhase),
|
|
momentumPhase: event.getIntegerValueField(.scrollWheelEventMomentumPhase)
|
|
)
|
|
}
|
|
|
|
var hasNoGesturePhase: Bool {
|
|
scrollPhase == 0 && momentumPhase == 0
|
|
}
|
|
|
|
var isNormalScroll: Bool {
|
|
momentumPhase == 0
|
|
}
|
|
}
|
|
|
|
private extension MouseScrollDeltas {
|
|
init(event: CGEvent) {
|
|
self.init(
|
|
deltaAxis1: event.getIntegerValueField(.scrollWheelEventDeltaAxis1),
|
|
deltaAxis2: event.getIntegerValueField(.scrollWheelEventDeltaAxis2),
|
|
pointDeltaAxis1: event.getIntegerValueField(.scrollWheelEventPointDeltaAxis1),
|
|
pointDeltaAxis2: event.getIntegerValueField(.scrollWheelEventPointDeltaAxis2),
|
|
fixedPointDeltaAxis1: event.getDoubleValueField(.scrollWheelEventFixedPtDeltaAxis1),
|
|
fixedPointDeltaAxis2: event.getDoubleValueField(.scrollWheelEventFixedPtDeltaAxis2)
|
|
)
|
|
}
|
|
}
|
|
|
|
private extension CGEvent {
|
|
func applyScrollDeltas(
|
|
_ deltas: MouseScrollDeltas,
|
|
reverseHorizontal: Bool,
|
|
reverseVertical: Bool
|
|
) {
|
|
// Set line deltas first. macOS may derive point/fixed deltas from them,
|
|
// so point and fixed values are restored afterwards to preserve smooth scrolling.
|
|
if reverseVertical {
|
|
setIntegerValueField(.scrollWheelEventDeltaAxis1, value: deltas.deltaAxis1)
|
|
setDoubleValueField(.scrollWheelEventFixedPtDeltaAxis1, value: deltas.fixedPointDeltaAxis1)
|
|
setIntegerValueField(.scrollWheelEventPointDeltaAxis1, value: deltas.pointDeltaAxis1)
|
|
}
|
|
|
|
if reverseHorizontal {
|
|
setIntegerValueField(.scrollWheelEventDeltaAxis2, value: deltas.deltaAxis2)
|
|
setDoubleValueField(.scrollWheelEventFixedPtDeltaAxis2, value: deltas.fixedPointDeltaAxis2)
|
|
setIntegerValueField(.scrollWheelEventPointDeltaAxis2, value: deltas.pointDeltaAxis2)
|
|
}
|
|
}
|
|
}
|