feat: Preserve hover tooltips in live capture via event-tap passthrough#416
Open
duongductrong wants to merge 4 commits into
Open
feat: Preserve hover tooltips in live capture via event-tap passthrough#416duongductrong wants to merge 4 commits into
duongductrong wants to merge 4 commits into
Conversation
Snapzy's live capture overlay previously intercepted all mouse events, causing hover UI (tooltips, hover cards) in the captured app to disappear. Adopt CleanShot-style passthrough capture: - Drive the capture UI from an active CGEventTap (.cgSessionEventTap), consuming every mouse/button/key event while observing pointer motion. - Render the overlay click-transparent so apps beneath retain hover state. - Draw the crosshair cursor via a proxy CALayer instead of relying on NSCursor/cursor rects, which don't work for a background LSUIElement. - Add screenshot.livePassthrough preference (default on) with Accessibility gating and silent fallback to legacy behavior. - Add hover coalescing and OSSignpost instrumentation for performance. - Update docs/CAPTURE.md and localization strings. Known limitation: CGDisplayHideCursor requires a foreground application, so the system cursor may remain visible alongside the proxy crosshair during passthrough capture. This matches CleanShot X's actual behavior. Tests: CaptureEventTapControllerTests, LivePassthroughInputLogicTests, plus existing AreaSelection* suites (146 passes).
…sion - Introduce BackgroundCursorControl to grant background agent cursor control via CGS connection property - Improve system cursor hiding and restoration during live passthrough capture - Add unit tests for BackgroundCursorControl and AreaSelectionOverlay cursor handling - Add performance profiling scripts and update CAPTURE.md documentation
…e-passthrough Resolve conflict in AreaSelectionWindow.swift cacheBackdropPixels: keep master's freeze-screen backdrop-cache downscale optimization (#411); the passthrough branch only had cosmetic self.-prefix changes to that function. All live-capture passthrough code preserved (verified: diff vs branch tip = exactly master's 3 hunks). Master's window-ordering and magnifier signature changes integrate cleanly. Merged tree: TEST BUILD SUCCEEDED.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
In live (non-frozen) capture mode, the full-screen
AreaSelectionWindowpanel intercepted every mouse event. Because the overlay was hit-testable and consumed allmouseMovedevents, the app being captured never received hover tracking events, so tooltips, hover cards, and other hover-driven UI disappeared as soon as capture started.Solution
Adopt a CleanShot-style passthrough capture architecture: consume mouse events at the WindowServer level with an active
CGEventTap, keep the overlay click-transparent so apps beneath retain their hover state, and draw the crosshair ourselves instead of relying onNSCursor/cursor rects (which don't work for a backgroundLSUIElementapp).Key changes
New
CaptureEventTapController(Snapzy/Services/Capture/CaptureEventTapController.swift).cgSessionEventTap, head-insert, consumes buttons/drags/keys/mouseMoved.LivePassthroughInputLogichelpers (Snapzy/Services/Capture/LivePassthroughInputLogic.swift)<1pxskip), dim visibility driver, and a per-display cursor-hide balance wrapper.AreaSelectionWindowpassthrough modeignoresMouseEvents = true) during live capture so WindowServer never synthesizes tracking-exit events to apps beneath.NSEvent.mouseLocationtimer is disabled in passthrough mode.Preferences
screenshotLivePassthroughtoggle (defaulton) in Screenshot settings.Instrumentation & docs
OSSignpostcategoryCapturePassthroughfor profiling.docs/CAPTURE.mdto describe the new input architecture.Tests
CaptureEventTapControllerTests(16 tests): lifecycle, permission-denied path, consume/pass routing, tap-disabled re-enable.LivePassthroughInputLogicTests(14 tests): coordinate flip, hover coalescing, dim driver, cursor-hide balance.Known limitation
CGDisplayHideCursorrequires the calling application to be in the foreground. Snapzy deliberately stays a backgroundLSUIElementagent during capture (activating would itself dismiss the target app's hover UI), so the system cursor may remain visible alongside the drawn proxy crosshair. This matches CleanShot X's actual behavior — its binary imports no cursor-hiding API and lets the arrow show next to its drawn crosshair.Two alternative public-API approaches were prototyped and rejected because they broke other requirements:
Testing
xcodebuild buildDebug → BUILD SUCCEEDED.CaptureEventTapControllerTests,LivePassthroughInputLogicTests,AreaSelectionOverlayTests,AreaSelectionOverlayDimmingTests,AreaSelectionSessionLifecycleTests,AreaSelectionOverlayCursorDragTests,LiveAreaMouseUpSnapshotTests,FrozenAreaCaptureSessionTests) → 0 failures.Manual QA checklist