How OpenLogi Implements Gesture Detection and Dispatch for Mouse Buttons

OpenLogi detects mouse-button gestures by diverting HID++ controls to report raw XY motion, then processes those deltas through a three-layer pipeline—binding definitions, capture sessions, and event handlers—to emit high-level CapturedInput::Gesture events.

OpenLogi is an open-source driver for Logitech mice that replicates the gesture functionality found in proprietary tools like Logitech Options+. The implementation treats a mouse-button gesture as a diverted HID++ control that reports raw XY motion rather than standard button clicks. This article examines the complete pipeline from hardware diversion to GUI event emission based on the AprilNEA/OpenLogi source code.

Architecture Overview: The Three-Layer Pipeline

The gesture detection system consists of three distinct layers, each with specific responsibilities:

  • Binding Layer: Defines the five possible gesture directions (↑, ↓, ←, →, Click) and provides stable serialization keys. Implemented in crates/openlogi-core/src/binding/gesture.rs.
  • Capture Session Layer: Opens a dedicated HID++ channel, arms gesture-source controls for raw-XY reporting, and manages the event loop. Lives in crates/openlogi-device/src/session/gesture.rs.
  • Event Handling Layer: Translates raw XY deltas into directional gestures, tracks button holds, and emits click fallbacks. Also located in the session gesture.rs file.

Defining Gesture Directions and Bindings

The foundation of the system is the GestureDirection enum, which enumerates the five logical slots exposed by Logitech mice for their dedicated gesture button or haptic panel.

pub enum GestureDirection {
    Up,
    Down,
    Left,
    Right,
    Click,
}

impl GestureDirection {
    pub const ALL: [Self; 5] = [Self::Up, Self::Down, Self::Left, Self::Right, Self::Click];
    
    pub fn label(self) -> &'static str { /* … */ }
    pub fn translation_key(self) -> &'static str { /* … */ }
}

This enum is serde-serializable, enabling persistence in TOML binding files. The definitions reside in [crates/openlogi-core/src/binding/gesture.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-core/src/binding/gesture.rs#L15-L71).

Initializing the Capture Session

When the GUI requests gesture handling, OpenLogi creates a capture session via run_capture_session. This async function establishes the communication pipeline required for raw motion reporting.

The session initialization follows this sequence:

  1. Open HID++ Channel: Establishes a single route channel using open_route_channel.

  2. Arm Gesture Controls: Marks gesture-source controls (identified by GESTURE_SOURCE_BUTTONS) for raw-XY reporting via arm_reprog_control:

    arm_reprog_control(&rc, cid, true, &mut armed.reporting).await?;
    armed.gesture_cids.push(cid);

    This code appears in [crates/openlogi-device/src/session/gesture.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-device/src/session/gesture.rs#L58-L66).

  3. Register Message Listener: Adds a guarded listener to the channel that distinguishes between reprog-control events (button states) and raw-XY events (gesture motion).

Translating Raw XY Motion into Gestures

The core detection logic lives in handle_raw_xy, which processes diverted raw motion reports and accumulates them into directional gestures.

fn handle_raw_xy(acc: &mut CaptureAccum, dx: i16, dy: i16, sink: &mpsc::UnboundedSender<CapturedInput>) {
    // The current hold owns the motion…
    let HoldState::Holding { button, swipe, overlap, skip_first_raw_xy, .. } = &mut acc.hold else { return };
    
    // …but overlapping holds are ignored.
    if *overlap { return; }
    
    // Discard the first jump from the haptic panel.
    if *skip_first_raw_xy { *skip_first_raw_xy = false; return; }
    
    // Accumulate the delta; when a clean direction appears, emit a gesture.
    if let Some(direction) = swipe.accumulate(i32::from(dx), i32::from(dy)) {
        let _ = sink.send(CapturedInput::Gesture(*button, direction));
    }
}

Key technical details from this implementation:

  • CaptureAccum: Tracks the HoldState to determine which button currently owns the raw-XY stream.
  • SwipeAccumulator: Normalizes motion vectors and determines when a direction is committed (mid-swipe, matching Logitech Options+ behavior).
  • Haptic Panel Compensation: Discards the first XY jump to prevent phantom gestures from the MX Master 4 haptic panel.
  • Overlap Protection: Ignores motion when multiple buttons are held simultaneously to prevent gesture conflicts.

Handling Button Edges and Click Fallbacks

The handle_reprog_with_gesture_buttons function manages the diverted button reports that indicate physical press and release edges. It also determines when a gesture should fall back to a simple click.

When a button release occurs without sufficient motion to trigger a directional gesture, the system emits a click event:

if let HoldState::Holding { button, mut swipe, .. } = previous && swipe.end() {
    let _ = sink.send(CapturedInput::Gesture(button, GestureDirection::Click));
}

This logic ensures that tapping the gesture button without swiping registers as a GestureDirection::Click rather than being lost entirely. The function also updates internal state trackers (acc.gestures_down, acc.dpi_down, acc.buttons_down) to maintain synchronization between physical hardware state and the GUI representation.

All button-edge handling is implemented in [crates/openlogi-device/src/session/gesture.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-device/src/session/gesture.rs#L24-L61).

Restoring Device State After Capture

When the session terminates—whether through shutdown, device disconnect, or channel failure—the finish_capture function restores each diverted control to its original firmware state using ReprogRestore and ArmedThumbwheel. This guarantees that gesture-source buttons return to their default behavior after OpenLogi exits, preventing the hardware from remaining in a diverted, non-functional state.

Summary

OpenLogi's gesture detection and dispatch for mouse buttons operates through a sophisticated pipeline that bridges raw HID++ hardware reports with high-level GUI events:

  • Binding definitions in openlogi-core establish the five possible gesture directions (Up, Down, Left, Right, Click) as serializable enums.
  • Capture sessions in openlogi-device divert HID++ controls to report raw XY motion instead of standard button presses.
  • Event handlers accumulate motion deltas using SwipeAccumulator, filter haptic panel artifacts, and emit CapturedInput::Gesture events only when clear directional intent is detected.
  • Click fallback logic ensures that brief button presses without motion register as explicit Click gestures.
  • State restoration returns hardware controls to their original firmware configuration when the driver shuts down.

Frequently Asked Questions

How does OpenLogi distinguish between a click and a gesture swipe?

OpenLogi uses the SwipeAccumulator to track motion deltas while the gesture button is held. If the button is released before the accumulator determines a clear directional threshold has been crossed, handle_reprog_with_gesture_buttons emits a CapturedInput::Gesture event with GestureDirection::Click. This matches the behavior of Logitech's proprietary Options+ software, where a quick tap without movement registers as a click.

What prevents phantom gestures from the MX Master 4 haptic panel?

The handle_raw_xy function includes a skip_first_raw_xy flag in the HoldState struct. When a gesture button press initiates a new capture session, this flag is set to true, causing the first raw-XY report to be discarded. This compensates for the initial position jump reported by the haptic panel on MX Master 4 devices, ensuring that resting your finger on the button does not trigger an unintended swipe.

Which source files contain the core gesture detection logic?

The primary implementation spans two crates: crates/openlogi-core/src/binding/gesture.rs defines the GestureDirection enum and serialization logic, while crates/openlogi-device/src/session/gesture.rs contains the capture session management, handle_raw_xy processing, and button edge detection. Device state restoration is handled in crates/openlogi-device/src/session/capture_restore.rs.

Can the gesture sensitivity or direction thresholds be customized?

While the provided source analysis focuses on the core detection pipeline, the SwipeAccumulator struct (referenced in handle_raw_xy) handles the normalization and threshold logic for determining when motion constitutes a directional gesture. Customization of sensitivity would involve modifying the accumulator's internal threshold constants or exposing them through the binding configuration system, which uses serde-serializable definitions in the core crate.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →