From 7c8ad872361f61b22891a901b20856cae26441af Mon Sep 17 00:00:00 2001 From: Kentaro Ohkouchi Date: Fri, 31 Jul 2026 17:58:18 +0900 Subject: [PATCH] win32: convert WM_MOUSEWHEEL position to client coordinates 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 --- src/apprt/win32/App.zig | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/apprt/win32/App.zig b/src/apprt/win32/App.zig index 2435d6fb467..e6d80d45e5b 100644 --- a/src/apprt/win32/App.zig +++ b/src/apprt/win32/App.zig @@ -49,6 +49,7 @@ const COPYDATASTRUCT = sys.COPYDATASTRUCT; // Additional externs not in sys.zig extern "user32" fn ClientToScreen(hWnd: HWND, lpPoint: *POINT) callconv(.winapi) BOOL; +extern "user32" fn ScreenToClient(hWnd: HWND, lpPoint: *POINT) callconv(.winapi) BOOL; extern "user32" fn SetForegroundWindow(hWnd: HWND) callconv(.winapi) BOOL; extern "user32" fn GetWindowTextW(hWnd: HWND, lpString: [*]u16, nMaxCount: c_int) callconv(.winapi) c_int; extern "user32" fn SetCapture(hWnd: HWND) callconv(.winapi) ?HWND; @@ -1749,9 +1750,21 @@ pub fn surfaceDispatch(app: *App, surface: *Surface, hwnd: HWND, msg: UINT, wpar }, 0x020A => { if (surface.core_surface) |core| { + // Unlike WM_MOUSEMOVE and the button messages, WM_MOUSEWHEEL + // carries the pointer position in *screen* coordinates, so it + // has to be converted before it can be used as a surface + // position. Without this, mouse reporting silently drops every + // wheel event whenever the window is positioned such that the + // screen coordinates fall outside the surface (see + // mouse_encode.posOutOfViewport). + var pt: POINT = .{ + .x = @as(i16, @truncate(lparam & 0xFFFF)), + .y = @as(i16, @truncate((lparam >> 16) & 0xFFFF)), + }; + _ = ScreenToClient(hwnd, &pt); surface.cursor_pos = .{ - .x = @floatFromInt(@as(i16, @truncate(lparam & 0xFFFF))), - .y = @floatFromInt(@as(i16, @truncate((lparam >> 16) & 0xFFFF))), + .x = @floatFromInt(pt.x), + .y = @floatFromInt(pt.y), }; const delta: i16 = @truncate(@as(isize, @bitCast(wparam)) >> 16); const yoff: f64 = @as(f64, @floatFromInt(delta)) / 120.0;