# How OpenLogi Dispatches Scroll-Wheel Gestures and Handles Per-Direction Bindings

> Discover how OpenLogi normalizes scroll deltas, maps them to directional ButtonId variants, and resolves per-direction bindings for seamless gesture control.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: internals
- Published: 2026-09-05

---

**OpenLogi normalizes platform-specific scroll deltas into a unified `MouseEvent::Scroll`, maps them to directional `ButtonId` variants, and resolves per-direction bindings through `Config::effective_bindings` before synthesizing output via the inject layer.**

OpenLogi is a cross-platform mouse gesture enhancement tool that intercepts raw hardware events and dispatches custom actions. The **scroll wheel gesture dispatch system** enables users to bind distinct commands to each scroll direction—vertical (up/down) and horizontal (left/right)—independently. This article examines the implementation details in the `AprilNEA/OpenLogi` repository, from platform-specific normalization in the hook crate to binding resolution in the agent.

## The Dispatch Pipeline: From Hardware to Application

The scroll-wheel dispatch chain follows a strict five-stage pipeline across four crates. First, the **hook** (`openlogi-hook`) captures raw OS events and normalizes direction conventions. Second, the **IPC** contract (`openlogi-ipc`) transports a `MouseEvent::Scroll { delta_x, delta_y }` to the agent. Third, the **agent** maps the signed deltas to specific `ButtonId` variants. Fourth, the **core** configuration (`openlogi-core`) resolves the binding via `Config::effective_bindings`. Finally, the **inject** layer (`openlogi-inject`) synthesizes the OS-level scroll event.

## Platform-Specific Normalization in the Hook Layer

Each platform reports scroll deltas with different sign conventions and units. The hook crate unifies these into a consistent representation where positive values indicate "scroll down/right" and negative values indicate "scroll up/left".

### Windows Direction Inversion

Windows reports positive `WM_MOUSEWHEEL` deltas for "scroll up," which contradicts the convention used by macOS and Linux. The helper function `wheel_forward_scrolls_up_like_other_platforms` in [`openlogi-hook/src/windows.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-hook/src/windows.rs) inverts the sign to maintain consistency.

```rust
fn wheel_forward_scrolls_up_like_other_platforms(delta: i16) -> i16 {
    // Windows reports positive delta for "scroll up", we invert it.
    -delta
}

```

### macOS Delta Extraction

On macOS, the hook extracts three possible encodings—pixel, line, and precise deltas—from the native event. The logic in [`openlogi-hook/src/macos.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-hook/src/macos.rs) preserves the correct sign convention without inversion, as the native reporting already aligns with the unified schema.

### Linux Axis Codes

The Linux implementation reads relative axis codes from the input subsystem. In [`openlogi-hook/src/linux.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-hook/src/linux.rs), the hook monitors `REL_WHEEL` for vertical movement and `REL_HWHEEL` for horizontal movement, converting these hardware signals into the standard `MouseEvent::Scroll` structure.

## Mapping Scroll Events to ButtonId Variants

When the agent receives a `MouseEvent::Scroll`, it converts the signed `delta_x` and `delta_y` values into discrete directional identifiers. The `ButtonId` enum, defined in [`openlogi-core/src/binding/action.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-core/src/binding/action.rs), provides four variants for scroll-wheel gestures:

- `ScrollUp` – vertical tick upwards (negative `delta_y`)
- `ScrollDown` – vertical tick downwards (positive `delta_y`)
- `HorizontalScrollLeft` – horizontal tick leftwards (negative `delta_x`)
- `HorizontalScrollRight` – horizontal tick rightwards (positive `delta_x`)

The agent performs the mapping using a simple sign check:

```rust
fn scroll_to_button_id(delta_x: i8, delta_y: i8) -> ButtonId {
    if delta_y > 0 {
        ButtonId::ScrollDown
    } else if delta_y < 0 {
        ButtonId::ScrollUp
    } else if delta_x > 0 {
        ButtonId::HorizontalScrollRight
    } else {
        ButtonId::HorizontalScrollLeft
    }
}

```

## Resolving Per-Direction Bindings via Config::effective_bindings

User configurations are stored as TOML and expanded at runtime. The `Config::effective_bindings` method in `openlogi-core` builds a `HashMap<ButtonId, Action>` that allows O(1) lookup of the command associated with each scroll direction.

```rust
let button = scroll_to_button_id(dx, dy);
let action = config.effective_bindings().get(&button);

```

If the map contains an entry for the resolved `ButtonId`, the associated `Action` (e.g., `Action::ScrollUp`, `Action::HorizontalScrollRight`) is dispatched. This architecture ensures that vertical and horizontal directions, as well as positive and negative deltas, are treated as completely independent input events with distinct binding slots.

## Synthesizing Output with the Inject Layer

Once the agent resolves the action, the **inject** crate executes the final step. The `post_scroll` and `post_smooth_scroll` functions in `openlogi-inject` synthesize the OS-level scroll event, posting it to the target application window. This completes the cycle: raw hardware input was intercepted, mapped to a semantic direction, resolved against user configuration, and finally delivered as a synthetic gesture.

```rust
// Inject → post the scroll tick
openlogi_inject::post_scroll(ScrollDelta { x: dx, y: dy });

```

## Summary

- **Hook normalization**: Platform-specific code in [`openlogi-hook/src/windows.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-hook/src/windows.rs), [`macos.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/macos.rs), and [`linux.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/linux.rs) unifies scroll direction conventions, including special sign inversion on Windows.
- **Delta mapping**: The agent converts signed `delta_x` and `delta_y` values into four discrete `ButtonId` variants (`ScrollUp`, `ScrollDown`, `HorizontalScrollLeft`, `HorizontalScrollRight`).
- **Binding resolution**: `Config::effective_bindings` in `openlogi-core` provides a lookup map that supports independent actions for each scroll direction.
- **Event injection**: The `openlogi-inject` crate synthesizes final scroll events via `post_scroll` and `post_smooth_scroll` to deliver the configured behavior.

## Frequently Asked Questions

### Why does OpenLogi invert scroll direction on Windows?

Windows natively reports positive deltas for "scroll up," while macOS and Linux use positive deltas for "scroll down." To maintain a consistent cross-platform sign convention, the `wheel_forward_scrolls_up_like_other_platforms` helper in [`openlogi-hook/src/windows.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-hook/src/windows.rs) inverts the Windows value, ensuring the agent receives uniform input regardless of operating system.

### How does OpenLogi distinguish between horizontal and vertical scroll events?

The hook layer extracts axis-specific data—`REL_WHEEL` versus `REL_HWHEEL` on Linux, or the corresponding native APIs on Windows and macOS—to populate `delta_y` (vertical) and `delta_x` (horizontal) fields in `MouseEvent::Scroll`. The agent then checks these fields in sequence: if `delta_y` is non-zero, it selects a vertical `ButtonId`; otherwise, it evaluates `delta_x` for horizontal scrolling.

### Can I bind different actions to scroll up versus scroll down?

Yes. The configuration system treats each direction as a distinct `ButtonId` variant. By defining separate entries in your TOML configuration for `ScrollUp` and `ScrollDown` (or the horizontal equivalents), `Config::effective_bindings` will map each to a different `Action`, allowing independent commands for every scroll direction.

### What happens if no binding exists for a scroll direction?

If `Config::effective_bindings` returns `None` for the resolved `ButtonId`, the agent takes no action for that event. The scroll wheel behaves as if unmodified, or the event may be passed through depending on the specific runtime configuration, effectively leaving the direction unbound.