pynput icon indicating copy to clipboard operation
pynput copied to clipboard

mouse listener on_scroll not accurately capturing scroll dx dy values

Open simplicity360 opened this issue 7 months ago • 1 comments

Description pynput's mouse Listener for on_scroll does not accurately capture the dx and dy values. When previously captured values are replayed through mouse.Controller, the resulting scroll falls short of expectations by a big margin.

Platform and pynput version MacOS 15.3 Sequoia pynput=1.8.1 Bluetooth Mouse with scroll wheel

To Reproduce

record_scroll.py

from pynput import mouse, keyboard
import json, time

events = []
startTime = time.time()

def write_events_local():
    print("Writing events")
    print(json.dumps(events))
    with open(f"scroll_events.json", "w") as f:
            json.dump(events, f)

def writeEventsAndClose():
    # Stop listeners
    mouse_listener.stop()
    keyboard_listener.stop()
    write_events_local()

def getTimeDiff():
    global startTime
    timeDiff = time.time() - startTime
    startTime = time.time()
    return timeDiff

def on_scroll(x, y, dx, dy):
    print("on scroll")
    print(f"x: {x}, y: {y}, dx: {dx}, dy: {dy}")
    events.append({"type": "scroll", "x": x, "y": y, "dx": dx, "dy": dy, "time": getTimeDiff()})

def on_release(key):
    global multi_keys
    multi_keys = ""
    if key == keyboard.Key.esc:
        writeEventsAndClose()
        return False

mouse_listener = mouse.Listener(on_move=None, on_scroll=on_scroll)
keyboard_listener = keyboard.Listener(on_release=on_release)

mouse_listener.start()
keyboard_listener.start()

mouse_listener.join()
keyboard_listener.join()

replay_scroll.py

import json, time
from pynput.mouse import Controller as MouseController


mouse = MouseController()

events = []

def get_events_local(filepath):
    global events
    events = []
    with open(filepath, 'r') as file:
        obj = json.load(file)
        events = obj

get_events_local('scroll_events.json')

for event in events:
    time.sleep(event['time'])
    print(f"{(event['x'], event['y'])} scroll by {(event['dx'], event['dy'])}")
    mouse.position = (event['x'], event['y'])
    # time.sleep(0.1)
    mouse.scroll(event['dx'], event['dy'])

time.sleep(3)

When the replay_scroll.py is run with the captured events array for scrolling, it is observed that the actual scroll distance on an application does not match up with the one while recording. Tried with various applications like Google Chrome, Sublime, Visual Studio code etc. - result is the same, i.e. falls short of original scroll expectation.

Also tried changing the Mouse "Scroll Speed" in MacOS settings. It does not help.

simplicity360 avatar Mar 31 '25 06:03 simplicity360

Got the same issue, please fix ❤️❤️❤️

dat-lequoc avatar Apr 05 '25 17:04 dat-lequoc

Thank you for your report.

Unfortunately I do not have access to a macOS system, so I cannot really test any code for that platform. However, the handling in the controller and the one in the listener are suspiciously different.

Is it possible for you to test different values, @simplicity360 or @dat-lequoc? I believe one possible fix would be to store this event in a variable, and then call Quartz.CGEventSetIntegerValueField with values analogous to those in the listener.

moses-palmer avatar Aug 12 '25 19:08 moses-palmer