Skip to content

ThumbWheel left direction not working (MX Master 3S) - fix included #536

@WEDM-MINT

Description

@WEDM-MINT

Problem

The thumbwheel on Logitech MX Master 3S only responds to right scrolling. Left scrolling produces no output.


Environment

  • Device: Logitech MX Master 3S
  • OS: Linux Mint
  • logiops version: latest from main branch

Configuration

thumbwheel: {
    divert: true;
    invert: false;
    right: { mode: "Axis"; axis: "REL_HWHEEL"; axis_multiplier: 1.0; };
    left:  { mode: "Axis"; axis: "REL_HWHEEL"; axis_multiplier: 1.0; };
};

Root Cause

The issue stems from three problems in the code:

1. ThumbWheel.cpp - Line 203: Resetting accumulator

// BEFORE:
if (scroll_action) {
    scroll_action->press(false);      // ← Resets _axis to 0
    scroll_action->move(event.rotation);
}

// AFTER:
if (scroll_action) {
    scroll_action->move(event.rotation);
}

Why: press(false) was resetting the accumulator to 0 before each move, preventing negative values from accumulating.

2. ThumbWheel.cpp - Lines 187-192: Asymmetric threshold initialization

// REMOVE this entire block:
if (event.rotationStatus == hidpp20::ThumbWheel::Start) {
    if (_right_gesture)
        _right_gesture->press(true);    // Sets _axis = +50
    if (_left_gesture)
        _left_gesture->press(true);     // Sets _axis = +50
}

Why: press(true) set _axis = +50 for both gestures, creating asymmetric behavior:

  • Right (+2): 50 + 2 = 52 > 50 → Works immediately
  • Left (-2): 50 - 2 = 48 < 50 → Requires ~51 clicks to reach -51

3. AxisGesture.cpp - Missing negative value support

// BEFORE (Line 86):
if (new_axis > threshold) {

// AFTER:
if (std::abs(new_axis) > threshold) {
// BEFORE (Lines 88-94):
if (_axis < threshold)
    move = new_axis - threshold;

// AFTER:
if (std::abs(_axis) < threshold) {
    if (new_axis > 0)
        move = new_axis - threshold;
    else
        move = new_axis + threshold;
}
// BEFORE (Lines 96-106): Complex negative_multiplier logic
bool negative_multiplier = _config.axis_multiplier.value_or(1) < 0;
if (negative_multiplier)
    move *= -_config.axis_multiplier.value_or(1);
else
    move *= _config.axis_multiplier.value_or(1);
// ... later ...
if (negative_multiplier)
    move_floor = -move_floor;

// AFTER: Simplified
move *= _config.axis_multiplier.value_or(1);

Why: Original code didn't handle negative input values and had flawed sign-flipping logic.


Solution - Complete Patch

diff --git a/src/logid/features/ThumbWheel.cpp b/src/logid/features/ThumbWheel.cpp
--- a/src/logid/features/ThumbWheel.cpp
+++ b/src/logid/features/ThumbWheel.cpp
@@ -184,12 +184,6 @@
         // Make right positive unless inverted
         event.rotation *= _wheel_info.defaultDirection;
 
-        if (event.rotationStatus == hidpp20::ThumbWheel::Start) {
-            if (_right_gesture)
-                _right_gesture->press(true);
-            if (_left_gesture)
-                _left_gesture->press(true);
-        }
 
         if (event.rotation) {
             std::shared_ptr<actions::Gesture> scroll_action;
@@ -200,7 +194,6 @@
                 scroll_action = _left_gesture;
 
             if (scroll_action) {
-                scroll_action->press(false);
                 scroll_action->move(event.rotation);
             }
         }

diff --git a/src/logid/actions/gesture/AxisGesture.cpp b/src/logid/actions/gesture/AxisGesture.cpp
--- a/src/logid/actions/gesture/AxisGesture.cpp
+++ b/src/logid/actions/gesture/AxisGesture.cpp
@@ -82,10 +82,16 @@
     int low_res_axis = InputDevice::getLowResAxis(axis);
     int hires_remainder = _hires_remainder;
 
-    if (new_axis > threshold) {
+    if (std::abs(new_axis) > threshold) {
         double move = axis;
-        if (_axis < threshold)
-            move = new_axis - threshold;
+        if (std::abs(_axis) < threshold) {
+            if (new_axis > 0)
+                move = new_axis - threshold;
+            else
+                move = new_axis + threshold;
+        }
+
-        bool negative_multiplier = _config.axis_multiplier.value_or(1) < 0;
-        if (negative_multiplier)
-            move *= -_config.axis_multiplier.value_or(1);
-        else
-            move *= _config.axis_multiplier.value_or(1);
+        move *= _config.axis_multiplier.value_or(1);
         // Handle hi-res multiplier
         move *= _multiplier;
 
@@ -107,9 +103,6 @@
             _axis_remainder -= int_remainder;
         }
 
-        if (negative_multiplier)
-            move_floor = -move_floor;
-
         if (low_res_axis != -1) {

Testing

After applying the patch:

  • ✅ Both directions respond immediately
  • axis_multiplier can be positive or negative to control direction
  • ✅ No threshold delay
  • ✅ Symmetric behavior in both directions

Tested and confirmed working on MX Master 3S.


Happy to provide more details or create a PR if this approach is acceptable!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions