Skip to content

Combine mouse buttons from every pointing device (#287) - #365

Open
mglushko wants to merge 1 commit into
hrvach:mainfrom
mglushko:mouse-button-union
Open

Combine mouse buttons from every pointing device (#287)#365
mglushko wants to merge 1 commit into
hrvach:mainfrom
mglushko:mouse-button-union

Conversation

@mglushko

@mglushko mglushko commented Aug 26, 2026

Copy link
Copy Markdown

Summary

Two pointing devices used at once cancel each other's buttons. Holding a button on one and
moving with the other lets go of the button, so you cannot select text or drag. Fixes #287.

Root cause

A mouse report carries the full button state of the device that sent it, and the newest
report won:

/* update_mouse_position */
state->mouse_buttons = values->buttons;

A trackball reporting movement with nothing pressed says "no buttons" just as clearly as a
keyboard's mouse keys say "left down", so it wins by reporting second. With a pointing
device on each board the report arriving over the link overwrote the state too.

Keyboards already avoid this. combine_kbd_states keeps what each keyboard holds and ORs
them into one report. Mice had no equivalent.

Changes

hid_interface_t gains a mouse_buttons byte for what that device holds, and
process_mouse_report sends the OR across every interface. It sits on the interface rather
than in an array keyed by the device number, because tuh_hid_report_received_cb gives
every mouse interface index 1 and two mice would share the slot. It also means the memset
already in tuh_hid_umount_cb clears a device's buttons when it is unplugged.

Neither board sees the other's reports, so each announces its own half in a new
MOUSE_BUTTONS_MSG when that half changes, and holds local | remote. That also lets the
rule which refuses an output switch while a button is held see a button held on the other
board.

Since the announcement only arrives on a change, the heartbeat carries the same value as a
level once a second. Without it, a dropped packet or a board that restarted mid-drag would
leave the other side holding a button nobody is pressing. Older firmware leaves the field
zero, which is correct for a board that never announces buttons, and ignores it coming the
other way.

Two smaller decode fixes come with it. The fallback for a device that declares buttons
under their own report ID, as the Kensington Expert Mouse does, now reads that interface's
last state instead of the union, which would otherwise leave one device holding another's
buttons. And the value is narrowed to the byte the outgoing report carries before it is
compared or stored, so a sixteen button device is not kept at full width only to be
truncated later.

No config, webconfig or flash layout changes. One new packet type.

Testing

On hardware with two mice, one in the keyboard port and one in the mouse port: holding a
button on one and moving with the other now works as expected.

Builds clean with no new warnings.

There is also a host side test that links src/mouse.c against stubs under ASan and UBSan
and drives two interfaces through process_mouse_report:
test_mouse.c.
It covers the reported case, two devices holding different buttons, unplugging one
mid-hold, a device with more buttons than the report can carry, what the announcement
carries, and the switch being held back by a button on either board.

Note on #357

The packet numbers do not collide. #357 uses 26 and this uses 27, which is why this one
skips 26, so the two can land in either order without renumbering. There may still be a
small textual fixup in the files they both touch, depending on merge order.

They are complementary: #357 fixes the cursor position when a pointing device is attached
to each board, this fixes the buttons for the same setup. Neither depends on the other.

A mouse report carries the full button state of the device that sent it, and
whichever device reported last was taken at its word. With two pointing devices
in use at once, one of them saying "no buttons" cancelled the other one saying
"left down".

The setup in hrvach#287 is mouse keys on a keyboard for the buttons and a trackball
for the movement. Holding a button and then moving released it, so selecting
text and dragging were impossible. The same thing happens with a pointing
device plugged into each board.

Keyboards already avoid this. combine_kbd_states keeps what each keyboard
holds and ORs them into one report. This does the same for mice.

Each interface remembers what it is holding, in hid_interface_t rather than in
an array indexed by the device number process_mouse_report is handed, because
tuh_hid_report_received_cb gives every mouse interface index 1 and two mice
would share the slot. Keeping it on the interface also means the memset in
tuh_hid_umount_cb drops a device's buttons when it is unplugged.

Neither board sees the other's reports, so each announces its own half of the
union in a new MOUSE_BUTTONS_MSG whenever that half changes, and holds
local | remote. Both boards agreeing also repairs the rule that refuses an
output switch while a button is held, which until now could not see a button
held on the other board.

That announcement only arrives on a change, so the heartbeat carries the same
value as a level once a second. Without it, a dropped packet or a board that
restarted while a button was held would leave the other side holding a button
nobody is pressing. Older firmware leaves the field zero, which is the right
answer for a board that never announces buttons, and ignores it in the other
direction.

Two smaller things in the decode path move with it. The fallback for a device
that declares its buttons under a report ID of its own, which the Kensington
Expert Mouse does, now reads that interface's own last state rather than the
union; reading the union would write another device's buttons into this one's
slot, where they would stay held after that device let go. And the button value
is narrowed to the byte the outgoing report carries before it is compared or
stored, so a device declaring sixteen buttons is not held at full width here
only to be truncated on the way out.

Fixes hrvach#287
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.

Two mouses interupt each other

1 participant