Skip to content

fix(app): carry keyboard modifiers on mouse and wheel events - #205

Merged
kolkov merged 1 commit into
gogpu:mainfrom
samyfodil:fix/mouse-modifiers
Aug 3, 2026
Merged

fix(app): carry keyboard modifiers on mouse and wheel events#205
kolkov merged 1 commit into
gogpu:mainfrom
samyfodil:fix/mouse-modifiers

Conversation

@samyfodil

Copy link
Copy Markdown
Contributor

Every MouseEvent and WheelEvent the bridge builds is given event.ModNone, so MouseEvent.Modifiers() is always empty. ⌥click, ⇧click, ⌃click and shift-to-extend-a-selection cannot be expressed by an application at all.

The platform's mouse callbacks report a button and a position and nothing else, so the modifier state has to come from the key events, which do carry it. The bridge now tracks it there and stamps it onto the mouse and wheel events it builds.

The two details that are the whole difficulty

  • A key event reports the modifiers held before it, so the event that presses Alt reports no Alt. Holding a modifier and clicking, with no other key in between, is exactly the gesture — so the key's own bit is folded in on press and taken out on release.
  • Focus loss clears the state. A modifier released while another window had focus was never seen here, and believing it is still held turns the next ordinary click into a modified one.

Found by

A terminal emulator on this toolkit, where ⌥click reveals a masked secret. The click arrived with no modifier, and the app had to reimplement this tracking on top of the key events to tell an ⌥click from a plain one. Doing it in the bridge means every application gets it rather than each one rediscovering it.

Tests

Three, covering held-over-click, cleared-on-release and cleared-on-focus-loss. The first fails on main (click carried None, want Alt). Full suite green (61 packages), golangci-lint clean.

A deeper fix would put the modifiers on gpucontext.EventSource's mouse callbacks — X11 already carries them in the button event — but that changes an interface across repos. This is the non-breaking version and is correct for every current platform backend.

Every MouseEvent and WheelEvent the bridge builds is given event.ModNone, so
MouseEvent.Modifiers() is always empty. ⌥click, ⇧click, ⌃click and
shift-to-extend-a-selection cannot be expressed by an application at all.

The platform's mouse callbacks report a button and a position and nothing else,
so the modifier state has to come from the key events, which do carry it. The
bridge now tracks it there and stamps it onto the mouse and wheel events it
builds.

Two details that are the whole difficulty:

  - A key event reports the modifiers held BEFORE it, so the event that presses
    Alt reports no Alt. Holding a modifier and clicking, with no other key in
    between, is exactly the gesture — so the key's own bit is folded in on press
    and taken out on release.
  - Focus loss clears the state. A modifier released while another window had
    focus was never seen here, and believing it is still held turns the next
    ordinary click into a modified one.

Found in a terminal emulator on this toolkit, where ⌥click reveals a masked
value: the click arrived with no modifier, and the application had to
reimplement this tracking on top of the key events to tell the two clicks apart.
Doing it in the bridge means every application gets it instead of each one
rediscovering it.
@samyfodil
samyfodil requested a review from kolkov as a code owner August 3, 2026 01:28
@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 73.91304% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
app/event_bridge.go 73.91% 6 Missing ⚠️

📢 Thoughts on this report? Let us know!

@kolkov kolkov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validated: matches winit/SDL3 Model B (tracked modifier state stamped onto pointer events). Focus-loss clearing matches winit exactly. modifierForKey OR-folding is safe across all 4 platforms (X11, Windows, Wayland, macOS) due to idempotency. Thread-safe by design (single-threaded event loop). Three tests cover held/released/focus-loss cases.

@kolkov
kolkov merged commit 68b02ff into gogpu:main Aug 3, 2026
9 checks passed
@kolkov kolkov mentioned this pull request Aug 3, 2026
5 tasks
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.

2 participants