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 app-wide scroll wheel preferences (invert direction, strength multiplier, tactility chunking) wired from
Confidence Score: 4/5Safe to merge after fixing the missing default_gesture_binding update — a new test will fail as-is. The scroll preferences wiring, in-place macOS transform, and Linux/Windows re-injection path are all correctly implemented. The one concrete defect is that default_gesture_binding still returns PrevTab/NextTab for horizontal swipes while a newly added test asserts PreviousDesktop/NextDesktop — those tests will fail on cargo test. Everything else looks correct. crates/openlogi-core/src/binding.rs — the default_gesture_binding function body needs to be updated to match the new test assertions. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant HW as Mouse Hardware
participant Hook as OS Hook Callback
participant HR as HookRuntime
participant SS as SharedScrollSettings
participant macOS as macOS CGEvent
participant Linux as post_scroll_delta
HW->>Hook: Scroll event (delta_x, delta_y, is_continuous)
Hook->>HR: cb(MouseEvent::Scroll)
HR->>SS: "read() -> ScrollSettings"
alt "settings == default OR is_continuous"
HR-->>Hook: PassThrough
Hook-->>HW: Original event delivered
else non-default, non-continuous
alt macOS
HR-->>Hook: TransformScroll(inverted, strength, tactility)
Hook->>macOS: transform_scroll_event(CGEvent)
Note over macOS: Mutates AXIS_1/2 line, pixel, fixed-point fields in-place
macOS-->>Hook: CallbackResult::Keep
Hook-->>HW: Modified event delivered
else Linux / Windows
HR->>HR: "transform_scroll(delta_x, delta_y, settings) -> (v, h)"
HR->>Linux: post_scroll_delta(v, h)
Linux-->>HW: Re-injected at session tap
HR-->>Hook: Suppress
Hook-->>HW: Original event dropped
end
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 Mouse Hardware
participant Hook as OS Hook Callback
participant HR as HookRuntime
participant SS as SharedScrollSettings
participant macOS as macOS CGEvent
participant Linux as post_scroll_delta
HW->>Hook: Scroll event (delta_x, delta_y, is_continuous)
Hook->>HR: cb(MouseEvent::Scroll)
HR->>SS: "read() -> ScrollSettings"
alt "settings == default OR is_continuous"
HR-->>Hook: PassThrough
Hook-->>HW: Original event delivered
else non-default, non-continuous
alt macOS
HR-->>Hook: TransformScroll(inverted, strength, tactility)
Hook->>macOS: transform_scroll_event(CGEvent)
Note over macOS: Mutates AXIS_1/2 line, pixel, fixed-point fields in-place
macOS-->>Hook: CallbackResult::Keep
Hook-->>HW: Modified event delivered
else Linux / Windows
HR->>HR: "transform_scroll(delta_x, delta_y, settings) -> (v, h)"
HR->>Linux: post_scroll_delta(v, h)
Linux-->>HW: Re-injected at session tap
HR-->>Hook: Suppress
Hook-->>HW: Original event dropped
end
end
Reviews (8): 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