Skip to content

win32: convert WM_MOUSEWHEEL position to client coordinates - #4

Open
nanasess wants to merge 1 commit into
Codavo:mainfrom
nanasess:fix/win32-mousewheel-client-coords
Open

win32: convert WM_MOUSEWHEEL position to client coordinates#4
nanasess wants to merge 1 commit into
Codavo:mainfrom
nanasess:fix/win32-mousewheel-client-coords

Conversation

@nanasess

@nanasess nanasess commented Aug 5, 2026

Copy link
Copy Markdown

Problem

With mouse reporting active (any fullscreen TUI — reproduced with Claude Code), the mouse wheel does nothing once the window sits far enough right/down on the screen. Moving the window towards the top-left corner makes it work again.

Scrollback scrolling (mouse reporting off) is unaffected.

Cause

Unlike WM_MOUSEMOVE and the button messages, WM_MOUSEWHEEL carries the pointer position in screen coordinates. The 0x020A arm of surfaceDispatch (src/apprt/win32/App.zig) reuses the WM_MOUSEMOVE conversion verbatim and stores those screen coordinates into surface.cursor_pos as if they were surface coordinates.

From there:

  1. Surface.getCursorPos() (src/apprt/win32/Surface.zig) returns self.cursor_pos unchanged.
  2. Surface.scrollCallback() (src/Surface.zig) reports wheel events at rt_surface.getCursorPos() when isMouseReporting().
  3. mouse_encode.encode() (src/input/mouse_encode.zig) returns without writing anything when posOutOfViewport(event.pos, opts.size) holds and the mode is not motion-tracking.

posOutOfViewport is pos.x > size.screen.width or pos.y > size.screen.height, so as soon as the screen coordinates exceed the client area, every wheel event is classified as outside the viewport and silently dropped — the application never receives a scroll report at all.

Concretely: a 1054px-wide client area with the window at x=1299 reproduces it every time. This also explains the odd repro condition — moving the window towards the screen origin makes the coordinates happen to land inside the viewport again.

Scrollback scrolling is unaffected because it goes through scrollViewport(), which does not use the position.

Secondarily, every wheel event leaves cursor_pos holding a bogus value until the next WM_MOUSEMOVE arrives.

Fix

Convert with ScreenToClient() before storing into cursor_pos. The POINT + @as(i16, @truncate(...)) shape matches the existing ClientToScreen usage in the same file.

The WM_MOUSEWHEEL that the scrollbar child window forwards to its parent via SendMessageW (src/apprt/win32/Surface.zig) is fixed by the same change, since forwarding preserves the original screen coordinates.

Testing

Upstream status

This code comes from mattn/ghostty's win32-apprt branch and is still present there in the same form. ghostty-org/ghostty main has no src/apprt/win32/ yet, so upstream Ghostty is unaffected.

AI disclosure

Per AI_POLICY.md: Claude Code assisted the investigation, and the code comment and this description were drafted with its help and edited by me. I diagnosed the behaviour, verified the fix on real hardware, and I understand and stand behind the change.

🤖 Generated with Claude Code

Unlike WM_MOUSEMOVE and the button messages, WM_MOUSEWHEEL carries the
pointer position in screen coordinates. The 0x020A handler in
surfaceDispatch reused the WM_MOUSEMOVE conversion verbatim and stored
those screen coordinates into surface.cursor_pos as if they were surface
coordinates.

When mouse reporting is active (a fullscreen TUI such as Claude Code),
Surface.scrollCallback reports wheel events at rt_surface.getCursorPos(),
so the bogus position reaches posOutOfViewport() in
input/mouse_encode.zig. Whenever the window sits far enough from the
screen origin that the screen coordinates exceed the surface size (for
example a 1054px wide client area with the window at x=1299), every wheel
event is classified as outside the viewport and silently dropped, so the
application never receives a scroll report at all.

This also explains the odd repro: moving the window towards the top-left
of the screen makes the coordinates happen to land inside the viewport
again. Scrollback scrolling is unaffected because it calls
scrollViewport() without using the position.

Convert with ScreenToClient() before storing into cursor_pos. The
WM_MOUSEWHEEL that the scrollbar child window forwards to its parent via
SendMessageW is fixed by the same change, since forwarding preserves the
original screen coordinates.

Verified on Windows with
`zig build -Doptimize=ReleaseFast -Dtarget=x86_64-windows-gnu`: with the
window positioned on the right half of the screen, a Claude Code session
can be scrolled with the mouse wheel again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant