fix(macos): correct inverted scroll direction (wl_pointer/libei sign) - #470
Open
rengers wants to merge 2 commits into
Open
fix(macos): correct inverted scroll direction (wl_pointer/libei sign)#470rengers wants to merge 2 commits into
rengers wants to merge 2 commits into
Conversation
added 2 commits
July 18, 2026 00:04
The macOS capture backend forwarded the raw CGEvent scroll delta, whose sign is inverted relative to wl_pointer.axis / libei (where positive means scroll down / right) -- the convention the wire protocol and the Linux and Windows capture backends already use. A macOS sender therefore scrolled backwards on non-macOS receivers, and the wire sign was inconsistent across capture backends. Negate the delta at capture time on both the continuous (PIXEL) and discrete (v120) paths and both axes, so scroll direction stays source-agnostic on the wire. This is the capture-side counterpart to the macOS emulation conversion in the following commit; together they also resolve the both-macOS double-inversion. Same approach as the closed PR feschber#415 / commit bb17cf0 by @jondkinney.
The macOS emulation backend replayed the wire scroll delta straight into
CGEvent, but CGEvent's scroll sign is inverted relative to the wire's
wl_pointer/libei convention (where positive means scroll down / right).
Scroll from a non-macOS sender (e.g. a Linux/Hyprland layer-shell or libei
capture) was therefore reversed on the receiving Mac, on both the continuous
(PIXEL) and discrete (v120/LINE) paths and both axes.
Convert the wire sign to the CGEvent sign with a single fixed negation
(wire_scroll_to_cgevent). The receiver's own com.apple.swipescrolldirection
("natural scrolling") preference is deliberately NOT consulted: events posted
at the HID event tap bypass the window server's natural-scroll transform, so
toggling that preference has no effect on emulated scrolling (verified: a
receiver scrolls identically with natural scrolling ON and OFF). The wire
protocol is source-agnostic, so a single fixed sign is correct for every
sender.
This differs from the receiver-preference approach in the open PR feschber#460 and the
config toggle in PR feschber#347: since injected scroll bypasses the preference, a
fixed conversion is both simpler and correct regardless of the receiver's
setting.
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
Scrolling from a non-macOS sender (e.g. Linux/Hyprland via the layer-shell or
libei capture backend, or Windows) onto a macOS receiver running the
macosemulation backend is inverted — vertically and horizontally, on both
mouse-wheel (discrete) and trackpad (continuous) input.
Symmetrically, a macOS sender scrolls backwards on non-macOS receivers,
because the two sides of the wire disagree on the scroll-sign convention.
Root cause
wl_pointer.axis/ libei use positive = scroll down / right. That is theconvention the wire protocol and the Linux and Windows capture backends already
use (
input-capture/src/windows/event_thread.rseven negatesWM_MOUSEWHEELto match).
CGEventscroll deltas use the opposite sign, and two placesnever reconcile it:
input-emulation/src/macos.rs) posts the wire delta straightinto
CGEvent, so a wl-convention sender scrolls backwards on the Mac.input-capture/src/macos.rs) forwards the rawCGEventdeltaun-negated, so the wire sign is inconsistent across capture backends and a
Mac sender scrolls backwards on everyone else.
Fix
source-agnostic (both PIXEL and v120 paths, both axes).
CGEventsignwith a single fixed negation (
wire_scroll_to_cgevent), on both paths andboth axes.
Together these make the wire consistent for every capture backend and fix
scrolling in every direction (non-mac → mac, mac → non-mac, and mac → mac
without double-inversion).
Why not read
com.apple.swipescrolldirection?An alternative (#460) flips the sign based on the receiver's natural scrolling
preference. That turns out to be unnecessary and only correct at one toggle
position: scroll events posted at the HID event tap bypass the window
server's natural-scroll transform, so the receiver's preference has no effect
on injected scrolling. I confirmed this on hardware — a receiving Mac scrolls
identically with natural scrolling ON and OFF. Because the wire is
source-agnostic, one fixed conversion is correct for every sender regardless of
the receiver's setting, and no CoreFoundation preference read / FFI is needed.
Testing
cargo checkandcargo clippy --all-targets -- -D warningsare clean forinput-captureandinput-emulation; the headless daemon(
cargo check --no-default-features) builds.inverted on
main, and that toggling the receiver's natural-scrollingsetting makes no difference — which is what motivates the fixed conversion.
Manual matrix worth confirming before merge (non-mac sender → mac receiver):
With two Macs, mac → mac should no longer double-invert (the limitation
documented in #460).
Prior art / coordination
This consolidates scroll-direction work that's been split across several PRs:
bb17cf0(@jondkinney) — the capture-side negation; reproduced here ascommit 1.
preference; see the note above on why a fixed conversion is used instead.
invert_scrollconfig toggle; orthogonal, and couldsit on top of this as a manual override.
Happy to adjust the approach (keep it emulation-only, add a config override,
etc.) if you'd prefer.