Add wheel scroll settings#94
Conversation
There was a problem hiding this comment.
ℹ️ Minor suggestions inline — one observation about the scroll-settings default check.
Reviewed changes — Adds three scroll-preference controls (invert, strength, tactility) to the Settings window and changes default horizontal gesture swipes to desktop/Space switching.
- Default gesture direction update —
PrevTab/NextTabreplaced withPreviousDesktop/NextDesktop. - ScrollSettings shared state —
Arc<RwLock<ScrollSettings>>mirrors the persisted config to the hook runtime. - Scroll event transformation —
transform_scroll/quantize_scrollapply inversion, strength, and chunking to captured wheel events. - Session-tap re-injection — Transformed scroll events are posted at
CGEventTapLocation::Sessionto avoid re-capture by OpenLogi's HID tap. - Settings UI — Invert switch, strength slider (1–10), and tactility slider (0–10) in a new "Scroll" group box.
- Generalized
setting_row— Signature changed fromcontrol: Switchtocontrol: impl IntoElementto accept sliders alongside switches. - Expanded native-click passthrough —
Back→BrowserBackandForward→BrowserForwardadded.
Big Pickle (free) (credentials for Anthropic not configured) | 𝕏
There was a problem hiding this comment.
✅ Prior feedback addressed — no new issues found.
Reviewed changes — Fixed the ScrollSettings::default() mismatch with AppSettings defaults by replacing the derived Default with a manual impl whose strength: 1 matches the app config.
- Manual
Defaultimpl forScrollSettings— Replaced#[derive(Default)]with a manualimpl Defaultwherestrength: 1aligns withAppSettings::wheel_strength. This restores the identity fast-path inhook_runtime.rs.
Big Pickle (free) (credentials for Anthropic not configured) | 𝕏
|
This should close #126 |
|
I'm really interested in the "invert wheel direction" feature. Thanks for bringing it up in this Pull Request, I hope it gets merged into the software soon! |
|
Let's get these conflicts resolved so we can merge this. |
|
for anyone looking for an interim fix, https://pilotmoon.com/scrollreverser/ is compatible with this app |
1813205 to
86fdb0d
Compare
Greptile SummaryThis PR adds user-configurable wheel scroll settings (inversion, strength multiplier, tactility/chunking) across all three platforms, routing discrete mouse wheel events through a
Confidence Score: 4/5Safe to merge; all findings are P2 quality/polish items with no correctness or data-loss risk. No P0 or P1 issues found. The three P2 comments cover dead code in transform_integer_scroll_field, duplicated bound constants, and a theoretical async race in post_symbolic_hotkey. Injection-loop guards on Windows (LLMHF_INJECTED) and macOS (SYNTHETIC_EVENT_USER_DATA) are both confirmed in place. Score is 4 rather than 5 to acknowledge the minor dead-code and duplication issues worth tidying before release. crates/openlogi-core/src/binding.rs (symbolic hotkey async race), crates/openlogi-hook/src/macos.rs (dead tactility branch), crates/openlogi-gui/src/windows/settings.rs + crates/openlogi-gui/src/state.rs (duplicated wheel bound constants) Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant HW as Physical Mouse
participant OS as OS Hook (WH_MOUSE_LL / CGEventTap / evdev)
participant CB as hook callback (mouse_proc / tap_callback / device_thread)
participant HR as hook_runtime (EventDisposition)
participant macOS as macos::transform_scroll_event
participant Post as binding::post_scroll_delta
HW->>OS: Raw scroll event
OS->>CB: Invoke hook callback
CB->>CB: "Translate to MouseEvent::Scroll{delta_x, delta_y, is_continuous}"
CB->>HR: invoke user callback(event)
alt is_continuous (trackpad) OR default settings
HR-->>CB: EventDisposition::PassThrough
CB->>OS: CallNextHook / Keep / forward
else macOS discrete wheel
HR-->>CB: "EventDisposition::TransformScroll(ScrollTransform{inverted, strength, tactility})"
CB->>macOS: transform_scroll_event(cg_event, transform)
macOS->>macOS: modify DELTA_AXIS fields in-place
macOS-->>CB: modified CGEvent
CB->>OS: CallbackResult::Keep (OS delivers modified event)
else non-macOS (Windows / Linux)
HR->>HR: transform_scroll(delta_x, delta_y, settings) → (i32, i32)
HR-->>CB: EventDisposition::Suppress
CB->>OS: Block original event
HR->>Post: post_scroll_delta(v, h)
Post->>OS: Inject new synthetic scroll (LLMHF_INJECTED / uinput write)
OS->>CB: Re-deliver synthetic event
CB->>CB: LLMHF_INJECTED flag / different device node → return None
CB->>OS: PassThrough (synthetic event flows to apps)
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant HW as Physical Mouse
participant OS as OS Hook (WH_MOUSE_LL / CGEventTap / evdev)
participant CB as hook callback (mouse_proc / tap_callback / device_thread)
participant HR as hook_runtime (EventDisposition)
participant macOS as macos::transform_scroll_event
participant Post as binding::post_scroll_delta
HW->>OS: Raw scroll event
OS->>CB: Invoke hook callback
CB->>CB: "Translate to MouseEvent::Scroll{delta_x, delta_y, is_continuous}"
CB->>HR: invoke user callback(event)
alt is_continuous (trackpad) OR default settings
HR-->>CB: EventDisposition::PassThrough
CB->>OS: CallNextHook / Keep / forward
else macOS discrete wheel
HR-->>CB: "EventDisposition::TransformScroll(ScrollTransform{inverted, strength, tactility})"
CB->>macOS: transform_scroll_event(cg_event, transform)
macOS->>macOS: modify DELTA_AXIS fields in-place
macOS-->>CB: modified CGEvent
CB->>OS: CallbackResult::Keep (OS delivers modified event)
else non-macOS (Windows / Linux)
HR->>HR: transform_scroll(delta_x, delta_y, settings) → (i32, i32)
HR-->>CB: EventDisposition::Suppress
CB->>OS: Block original event
HR->>Post: post_scroll_delta(v, h)
Post->>OS: Inject new synthetic scroll (LLMHF_INJECTED / uinput write)
OS->>CB: Re-deliver synthetic event
CB->>CB: LLMHF_INJECTED flag / different device node → return None
CB->>OS: PassThrough (synthetic event flows to apps)
end
Reviews (9): Last reviewed commit: "Merge branch 'master' into proposal/whee..." | Re-trigger Greptile |
86fdb0d to
183eb59
Compare
AprilNEA
left a comment
There was a problem hiding this comment.
Thanks for this — invert-scroll is clearly in demand (#126 plus the comments here). Before merging I want to flag some concerns with the current approach, because the suppress-and-reinject design has a few real problems on macOS, and one of them is that it doesn't actually solve #126 as written.
1. It doesn't solve #126 (per-device inversion)
#126 specifically asks to keep the trackpad on natural scrolling and invert only the mouse. The hook taps at CGEventTapLocation::HID (crates/openlogi-hook/src/macos.rs:257) and captures every ScrollWheel event regardless of source, and transform_scroll never looks at kCGScrollWheelEventIsContinuous. So enabling invert flips the trackpad too — exactly the native-setting behavior #126 wants to avoid. We shouldn't close #126 with this as-is.
2. Modifier flags are dropped
post_scroll_delta builds a fresh CGEvent::new_scroll_event(...) and never copies the original event's flags. Modifier+scroll gestures that apps read off the event flags (zoom, etc.) will stop working while non-default settings are active. (System-level Ctrl+scroll screen zoom reads live key state and may be unaffected — worth verifying on-device.)
3. Momentum / pixel precision are lost
We suppress the original and re-emit ScrollEventUnit::LINE from the rounded AXIS_1/AXIS_2 line deltas. That drops scroll-phase/momentum and ignores the pixel POINT_DELTA_* fields, so continuous input (trackpad, MX free-spin high-res wheel) becomes coarse. Micro-deltas round to 0 → PassThrough, so on a free-spinning wheel small movements aren't inverted while fast ones are — inconsistent. Given our primary devices are MX-class, this matters.
4. Per-event cost on the hottest path
The macOS tap must stay lock-light or it stalls the whole input stream (see the comments in macos.rs). This adds an RwLock read per scroll event and, when active, a fresh CGEventSource::new + CGEvent allocation per event. On a high-rate free-spin stream that risks TapDisabledByTimeout / scroll stutter.
Suggested approach
Instead of suppress + reinject, mutate the event in place and return Keep. We already do in-place field writes elsewhere (crates/openlogi-core/src/binding.rs:1312,1327), and the same set_*_value_field works on the callback's &CGEvent. Negate/scale AXIS_1/AXIS_2 plus the POINT_DELTA_* and fixed-point fields and keep the event — that preserves momentum, pixel precision, flags, and continuity, with zero allocation and no re-entry concern. For #126, branch on kCGScrollWheelEventIsContinuous to apply inversion to discrete (mouse) scroll only. This is essentially how Scroll Reverser does it.
Unrelated change
The default gesture remap (PrevTab/NextTab → PreviousDesktop/NextDesktop) is a separate UX decision — please split it into its own PR so each can be reviewed/reverted independently.
The config plumbing, clamping, and default fast-path are clean; the concern is purely the macOS transform path. Happy to help iterate on the in-place version.

Summary
This proposes two related mouse-control improvements:
Implementation notes
AppSettingsand mirrored into the hook runtime through a sharedScrollSettingsvalue.Verification
Ran locally on macOS:
cargo fmt --all cargo check -p openlogi-gui cargo test -p openlogi-gui -p openlogi-coreResults:
openlogi-core: 34 passedopenlogi-gui: 14 passed