From 7c1ae0ce593b7a8b4bb7b2f2b600300307b23241 Mon Sep 17 00:00:00 2001 From: modfox Date: Wed, 4 Nov 2026 17:09:46 -0500 Subject: [PATCH] Treat eraser-end contact as a pen press The pen reports eraser contact via the Eraser status bit (bit 3), not Tip Switch (bit 0). EventEmitter only checked tipDown, so flipping the pen switched apps to the eraser tool (via the proximity event) but pressing never produced a mouse-down, and nothing was erased. Contact is now tipDown || eraser for the down/drag/up state machine and the tablet button mask. Co-Authored-By: Claude Opus 5.5 --- driver/Sources/NibCore/EventEmitter.swift | 12 +++++++----- .../Tests/NibCoreTests/EventEmitterTests.swift | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/driver/Sources/NibCore/EventEmitter.swift b/driver/Sources/NibCore/EventEmitter.swift index d2a80f9..5d5ad05 100644 --- a/driver/Sources/NibCore/EventEmitter.swift +++ b/driver/Sources/NibCore/EventEmitter.swift @@ -136,12 +136,14 @@ public final class EventEmitter { private func postPointer(_ s: PenSample) { let pt = mapping.point(x: s.x, y: s.y) + // Contact: the eraser end reports touch via the Eraser bit, not Tip Switch. + let contact = s.tipDown || s.eraser let type: CGEventType - if s.tipDown && !wasTipDown { type = .leftMouseDown } - else if !s.tipDown && wasTipDown { type = .leftMouseUp } - else if s.tipDown { type = .leftMouseDragged } + if contact && !wasTipDown { type = .leftMouseDown } + else if !contact && wasTipDown { type = .leftMouseUp } + else if contact { type = .leftMouseDragged } else { type = .mouseMoved } - wasTipDown = s.tipDown + wasTipDown = contact if type == .leftMouseDown { tipClickState = tipClicks.down(at: pt, time: now()) } guard let e = CGEvent(mouseEventSource: nil, mouseType: type, @@ -167,7 +169,7 @@ public final class EventEmitter { e.setIntegerValueField(.tabletEventDeviceID, value: Int64(deviceID)) // Button mask: bit0 tip, bit1 barrel, bit2 secondary barrel. var buttons: Int64 = 0 - if s.tipDown { buttons |= 0x1 } + if contact { buttons |= 0x1 } if s.barrelButton { buttons |= 0x2 } if s.secondaryBarrel { buttons |= 0x4 } e.setIntegerValueField(.tabletEventPointButtons, value: buttons) diff --git a/driver/Tests/NibCoreTests/EventEmitterTests.swift b/driver/Tests/NibCoreTests/EventEmitterTests.swift index c3b6e25..7f04b38 100644 --- a/driver/Tests/NibCoreTests/EventEmitterTests.swift +++ b/driver/Tests/NibCoreTests/EventEmitterTests.swift @@ -14,6 +14,22 @@ final class EventEmitterTests: XCTestCase { sink: sink) } + func testEraserContactIsAClick() { + let sink = RecordingSink(), em = makeEmitter(sink) + var s = sample(); s.inverted = true + em.handle(s) // eraser hovering + var down = s; down.eraser = true; down.pressure = 3000 + em.handle(down); em.handle(down) // eraser pressed (Tip bit stays 0) + em.handle(s) // lifted + XCTAssertEqual(sink.clicks.map { $0.type }, [.leftMouseDown, .leftMouseUp]) + XCTAssertTrue(sink.types.contains(.leftMouseDragged)) + let prox = sink.events.first { $0.type == .tabletProximity }! + XCTAssertEqual(prox.getIntegerValueField(.tabletProximityEventPointerType), 3) + let press = sink.events.first { $0.type == .leftMouseDown }! + XCTAssertGreaterThan(press.getDoubleValueField(.tabletEventPointPressure), 0) + XCTAssertEqual(press.getIntegerValueField(.tabletEventPointButtons) & 0x1, 1) + } + func testLeavingProximityWithTipDownReleasesTip() { let sink = RecordingSink(), em = makeEmitter(sink) em.handle(sample(tip: true))