Skip to content

Add wheel scroll settings#94

Open
klovinad wants to merge 7 commits into
AprilNEA:masterfrom
klovinad:proposal/wheel-scroll-settings
Open

Add wheel scroll settings#94
klovinad wants to merge 7 commits into
AprilNEA:masterfrom
klovinad:proposal/wheel-scroll-settings

Conversation

@klovinad

@klovinad klovinad commented Jun 2, 2026

Copy link
Copy Markdown

Summary

This proposes two related mouse-control improvements:

  • Change the default horizontal gesture-button swipes to switch macOS desktops/spaces:
    • Left → Previous Desktop
    • Right → Next Desktop
  • Add app-wide scroll wheel preferences:
    • invert wheel direction
    • scroll strength multiplier
    • tactility/chunking

Implementation notes

  • Scroll preferences are persisted in AppSettings and mirrored into the hook runtime through a shared ScrollSettings value.
  • The hook transforms captured scroll events only when non-default scroll settings are active; default settings pass through natively.
  • Synthetic transformed scroll events are posted at the session tap to avoid being re-captured by OpenLogi's own HID event tap.
  • Added unit coverage for scroll axis ordering, inversion, and chunking.

Verification

Ran locally on macOS:

cargo fmt --all
cargo check -p openlogi-gui
cargo test -p openlogi-gui -p openlogi-core

Results:

  • openlogi-core: 34 passed
  • openlogi-gui: 14 passed

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ 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 updatePrevTab/NextTab replaced with PreviousDesktop/NextDesktop.
  • ScrollSettings shared stateArc<RwLock<ScrollSettings>> mirrors the persisted config to the hook runtime.
  • Scroll event transformationtransform_scroll/quantize_scroll apply inversion, strength, and chunking to captured wheel events.
  • Session-tap re-injection — Transformed scroll events are posted at CGEventTapLocation::Session to 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 from control: Switch to control: impl IntoElement to accept sliders alongside switches.
  • Expanded native-click passthroughBackBrowserBack and ForwardBrowserForward added.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Big Pickle (free) (credentials for Anthropic not configured) | 𝕏

Comment thread crates/openlogi-gui/src/hook_runtime.rs Outdated

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 Default impl for ScrollSettings — Replaced #[derive(Default)] with a manual impl Default where strength: 1 aligns with AppSettings::wheel_strength. This restores the identity fast-path in hook_runtime.rs.

Pullfrog  | View workflow run | Using Big Pickle (free) (credentials for Anthropic not configured) | 𝕏

@averypelle

Copy link
Copy Markdown

This should close #126

@phuocleoceo

Copy link
Copy Markdown

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!

@shawnflanagan

Copy link
Copy Markdown

Let's get these conflicts resolved so we can merge this. Invert wheel direction is sorely needed in the app

@averypelle

Copy link
Copy Markdown

for anyone looking for an interim fix, https://pilotmoon.com/scrollreverser/ is compatible with this app

@klovinad klovinad force-pushed the proposal/wheel-scroll-settings branch from 1813205 to 86fdb0d Compare June 14, 2026 09:09
@greptile-apps

greptile-apps Bot commented Jun 14, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds app-wide scroll wheel preferences (invert direction, strength multiplier, tactility chunking) wired from AppSettingsSharedScrollSettings → the OS-hook callback. A large portion of binding.rs was also refactored to bring Action::execute with platform-specific implementations (macOS CGEvent, Linux uinput, Windows SendInput) directly into openlogi-core, and new PreviousDesktop/NextDesktop actions were added.

  • Scroll settings: ScrollSettings is shared via Arc<RwLock> into the hook callback; on macOS scroll events are transformed in-place via TransformScroll, while Linux/Windows suppress the original and re-inject via post_scroll_delta. Continuous (trackpad) events are intentionally bypassed.
  • Default gesture binding: A new test asserts that horizontal swipes default to PreviousDesktop/NextDesktop, but default_gesture_binding still returns PrevTab/NextTab — the function body was not updated and the test will fail.
  • Platform execute refactor: Action::execute_macos, execute_linux, and execute_windows are well-structured with appropriate fallbacks and safe logging on unsupported platforms.

Confidence Score: 4/5

Safe 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

Filename Overview
crates/openlogi-core/src/binding.rs Large refactor moving Action::execute into openlogi-core with per-platform implementations; adds PreviousDesktop/NextDesktop actions but default_gesture_binding was not updated — test asserting the new defaults will fail.
crates/openlogi-agent-core/src/hook_runtime.rs Adds ScrollSettings and scroll event transformation; macOS uses in-place TransformScroll disposition, non-macOS re-injects via post_scroll_delta and suppresses. Continuous events correctly bypassed.
crates/openlogi-hook/src/macos.rs Adds transform_scroll_event that mutates all six scroll CGEvent fields (line, pixel, fixed-point for both axes) in-place; quantize_scroll handles tactility chunking correctly.
crates/openlogi-hook/src/linux.rs Adds is_continuous: false to scroll events; fixes the disposition check from PassThrough-only to !Suppress so TransformScroll falls back to pass-through on Linux.
crates/openlogi-core/src/config.rs Adds wheel_inverted, wheel_strength, wheel_tactility fields to AppSettings with serde defaults; clean and minimal.
crates/openlogi-gui/src/windows/settings.rs Adds wheel inversion toggle, strength and tactility sliders; DEFAULT_WHEEL_STRENGTH is a standalone constant not sourced from config.rs, creating a drift risk.
crates/openlogi-gui/src/state.rs Adds set_wheel_inverted, set_wheel_strength, set_wheel_tactility with correct clamping and persist_and_reload calls; consistent with existing thumbwheel sensitivity pattern.
crates/openlogi-hook/src/lib.rs Adds is_continuous field to MouseEvent::Scroll and new TransformScroll(ScrollTransform) disposition; well-documented interface change.
crates/openlogi-agent-core/src/orchestrator.rs Adds scroll_settings to SharedRuntime and wires it through update_from_config; follows the existing thumbwheel_sensitivity pattern correctly.

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
Loading
%%{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
Loading

Reviews (8): Last reviewed commit: "Merge branch 'master' into proposal/whee..." | Re-trigger Greptile

Comment thread crates/openlogi-agent-core/src/hook_runtime.rs Outdated
Comment thread crates/openlogi-core/src/binding.rs Outdated
@klovinad klovinad force-pushed the proposal/wheel-scroll-settings branch from 86fdb0d to 183eb59 Compare June 14, 2026 12:57

@AprilNEA AprilNEA left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/NextTabPreviousDesktop/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.

@klovinad klovinad requested a review from AprilNEA June 16, 2026 20:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants