feat: gestures and software scroll inversion for MX Vertical - #783
Open
fuloskop wants to merge 4 commits into
Open
feat: gestures and software scroll inversion for MX Vertical#783fuloskop wants to merge 4 commits into
fuloskop wants to merge 4 commits into
Conversation
MX Vertical ships no dedicated gesture button. Its `0x1b04` list carries no `0x00c3` at all, its `0x00d7` (virtual gesture button) is listed but never emits an event, and `0x00fd` — the DPI/ModeShift switch — is the only control there flagged `raw-xy`. That is also the control Logitech's own software gestures from on this model, corroborated by the device's asset metadata, which ships `SLOT_NAME_GESTURE_*_BUTTON` markers next to `SLOT_NAME_DPI_BUTTON`. Add the DPI/ModeShift family to `GESTURE_SOURCE_BUTTONS` and to `ButtonId::is_hidpp_gesture_source`, so a gesture map bound to `ButtonId::DpiToggle` arms a raw-XY divert and dispatches swipes. DPI cycling remains the out-of-the-box behavior: `default_binding_for` returns `Single` for that button and a `Single` shape drops out of the gesture-map lookup, pinned by a test so it cannot regress silently. Also guard the capture session's DPI pass against re-arming a CID already armed as a raw-XY gesture source. That write carries no raw-XY and would strip the reporting the hold depends on — reachable exactly on a device whose DPI button *is* its gesture source. Verified on MX Vertical over Bluetooth-direct. Before: ten presses gave 19 `button=DpiToggle action=Cycle DPI Presets` events and no gestures. After: `gesture_sources=1 dpi_buttons=0`, and all four directions plus click dispatch their bound actions.
`map_slot_name` knew only `SLOT_NAME_MODESHIFT_BUTTON`, the MX Master line's name for that control. MX Vertical calls the same button `SLOT_NAME_DPI_BUTTON`, so it produced no hotspot: the device rendered with no editable control for its only extra button and could not be bound from the GUI at all. The five `SLOT_NAME_GESTURE_*_BUTTON` markers that model also ships stay unmapped on purpose — they mark the control the DPI marker already covers, this builder does not dedupe, and the picker renders swipe directions from the binding's gesture map. Both halves are pinned by tests.
Scroll inversion was gated on HID++ `0x2121` (HiResWheel) reporting an invert capability, so a device without that feature showed a permanently "Unavailable" toggle. MX Vertical reports 30 features and none is `0x2121`, putting the setting out of reach on that hardware. Add `EventDisposition::InvertScroll`. macOS negates the `CGEvent`'s delta fields in place, so the event keeps its phase, momentum and pixel precision and no synthetic replacement re-enters the tap. Only fields the event actually carries are negated: writing an axis the device left empty would introduce a delta an app reading that field would honour. The hook rewrites only devices in `HookMaps::invert_scroll`, which the orchestrator builds from the config while excluding natively-capable ones — for those the setting goes to the firmware, and rewriting on top would invert twice and cancel out. Trackpad scroll is never touched, since macOS already applies its own natural-scrolling preference there. Inversion is keyed per device rather than scoped to the selected one, so disabling the selected device empties the button maps without stopping another device's inversion. Both properties are covered by tests. evdev and `WH_MOUSE_LL` have no rewrite path yet; both pass the new disposition through rather than swallowing the scroll.
Greptile SummaryThis PR adds MX Vertical gesture support and macOS software scroll inversion while preserving native firmware inversion where available.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| crates/openlogi-agent-core/src/orchestrator.rs | Builds software-inversion identities from enabled direct devices, excludes native-capable devices, and handles identical-device setting conflicts explicitly. |
| crates/openlogi-agent-core/src/hook_runtime.rs | Selects software inversion for non-trackpad scroll events whose device identity is present in the published inversion set. |
| crates/openlogi-hook/src/macos.rs | Implements in-place negation of populated vertical and horizontal Core Graphics scroll fields. |
| crates/openlogi-hid/src/session/gesture.rs | Adds DPI-family controls as gesture sources while preserving their raw-XY reporting during capture setup. |
| crates/openlogi-desktop/src/state/scroll.rs | Exposes software inversion for pointer-capable devices on macOS while retaining native-capability gating elsewhere. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
A[Physical mouse event] --> B{Event type}
B -->|DPI gesture source| C[HID++ raw-XY capture]
C --> D[Resolve gesture binding]
D --> E[Action dispatcher]
B -->|Wheel event| F[OS input hook]
F --> G{Trackpad or native inversion?}
G -->|Yes| H[Pass through]
G -->|No| I{VID/PID enabled for software inversion?}
I -->|No| H
I -->|Yes| J[Negate macOS scroll deltas]
J --> H
Reviews (2): Last reviewed commit: "fix(agent): don't invert scroll for iden..." | Re-trigger Greptile
The hook matches a scroll event by vendor/product id, which two identical directly-attached mice share, so one device's inversion setting cannot be applied without also applying it to its twin. Collapsing them inverted a wheel the user never configured, untraceable from the GUI. Group candidates by identity and drop any identity whose devices disagree, logging a warning instead; identities that agree still yield one entry. Reported by Greptile on AprilNEA#783.
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.
What this fixes
MX Vertical can't use gestures or scroll inversion today, for two unrelated reasons.
Gestures. The device ships no dedicated gesture button, and the capture layer knows only two gesture sources (
0x00c3, plus the MX Master 4 haptic panel).diag controlson real hardware:There is no
0x00c3.0x00d7(virtual gesture button) is listed but never emits — armed with raw-XY it produced no events across repeated presses.0x00fd, the DPI switch, is the control that actually fires, and it is raw-XY capable.Logitech's own asset metadata agrees: for this model it ships
SLOT_NAME_GESTURE_{UP,DOWN,LEFT,RIGHT,CLICK}_BUTTONnext toSLOT_NAME_DPI_BUTTON, i.e. it also treats the DPI button as the control that gestures here.The GUI had a second, independent problem:
map_slot_nameknew onlySLOT_NAME_MODESHIFT_BUTTON, so this device's DPI button produced no hotspot and could not be bound at all.Scroll inversion.
Capabilities::scroll_inversionis derived from0x2121(HiResWheel). MX Vertical reports 30 features and none is0x2121, so the toggle read "Unavailable" permanently with no way to reach the setting.What changed
feat(hid)— the DPI/ModeShift family joinsGESTURE_SOURCE_BUTTONSandButtonId::is_hidpp_gesture_source. DPI cycling stays the out-of-the-box behavior: that button's canonical binding isSingle, which drops out of the gesture-map lookup. Also guards the capture session's DPI pass against re-arming a CID already armed with raw-XY — that write carries none and would strip the reporting the hold depends on, reachable exactly on a device whose DPI button is its gesture source.fix(gui)—SLOT_NAME_DPI_BUTTONmaps toButtonId::DpiToggle. The five per-direction gesture markers stay unmapped: they mark the same control, and the hotspot builder does not dedupe, so mapping them would stack duplicate hotspots on one button.feat(hook)—EventDisposition::InvertScroll. macOS negates theCGEventdelta fields in place, so the event keeps its phase, momentum and pixel precision and no synthetic replacement re-enters the tap. Only fields the event actually carries are negated, since writing an axis the device left empty would introduce a delta an app reading that field would honour. Only devices inHookMaps::invert_scrollare rewritten — natively-capable ones are excluded, because their setting goes to the firmware and rewriting on top would invert twice and cancel out. Trackpad scroll is never touched, since macOS already applies its own natural-scrolling preference there. evdev andWH_MOUSE_LLhave no rewrite path yet and pass the disposition through rather than swallowing the scroll.Verification
Real hardware, MX Vertical over Bluetooth-direct:
button=DpiToggle action=Cycle DPI Presets, zero gesturesgesture_sources=1 dpi_buttons=0; Left/Right/Up/Down and click all dispatch their bound actionsFull local gate clean on the final tree:
cargo fmt --all -- --check,cargo clippy --all-targets -- -D warnings,cargo test, andRUSTDOCFLAGS="-D warnings" cargo doc. 635 tests pass, six of them new — covering the DPI-toggleSingledefault, both DPI slot names, the deliberately unmapped direction markers, exclusion of natively-capable devices, and the per-device property of inversion (disabling the selected device must not stop inverting another's wheel).Only macOS was exercised on hardware. The Linux and Windows hook arms are compile-only changes that pass the new disposition through.