// F11 — Scroll fidelity, Win32 implementation (../../../features/f11-scroll.md).
// Extends the scaffold (../scaffold/app.d) into a wheel-message observatory:
//
// * WM_MOUSEWHEEL / WM_MOUSEHWHEEL handlers log every event's raw signed
// wheelDelta (GET_WHEEL_DELTA_WPARAM), key state, and screen position.
// * The ACCUMULATION CONTRACT is implemented and proved: sub-WHEEL_DELTA
// remainders are carried in an accumulator until |acc| >= 120, then turned
// into line scrolls (`acc=… detents=…` in every scroll line). The buggy
// per-event truncation model runs side by side, and a per-gesture summary
// diffs the two; WSI_TRUNCATE=1 makes the BUGGY model drive the ruler.
// * Gestures are injected via SendInput MOUSEEVENTF_WHEEL / _HWHEEL with
// mouseData = ±120 (one detent), ±40 (sub-detent, precision-touchpad
// style), +360 (multi-detent), and a +40/-40/+40 jitter burst (net 40 —
// must produce ZERO lines).
// * Routing probe: a second window is created beside the main one; with
// focus on MAIN and the cursor parked over OTHER, a wheel event is
// injected and the receiving window logged (focus routing vs
// window-under-cursor routing; SPI_GETMOUSEWHEELROUTING is probed).
// * SPI_GETWHEELSCROLLLINES / SPI_GETWHEELSCROLLCHARS are logged; a
// scrollable ruler (tick every line, numbered every 5) renders the
// line-scroll output so over/under-scroll is visible.
//
// WSI_AUTO_EXIT=1 bounds the run (~1.5 s); exit 0 in all modes.
//
// Only druntime's built-in core.sys.windows bindings — no third-party packages.
module (module) appapp;
import (package) corecore.(package) core.syssys.(package) core.sys.windowswindows.(module) core.sys.windows.windowsWindows API header module
Translated from MinGW API for MS-Windows 4.0
Source
core/sys/windows/windows.d
windows;
import instrument;
// Missing from druntime's core.sys.windows.winuser:
enum DWORD (constant) _error_ app.MOUSEEVENTF_HWHEEL = __errorMOUSEEVENTF_HWHEEL = 0x1000; // SendInput horizontal-wheel flag
enum UINT (constant) _error_ app.SPI_GETWHEELSCROLLCHARS = __errorSPI_GETWHEELSCROLLCHARS = 0x006C;
enum UINT (constant) _error_ app.SPI_GETMOUSEWHEELROUTING = __errorSPI_GETMOUSEWHEELROUTING = 0x201C;
// SPI_GETMOUSEWHEELROUTING values:
// 0 = MOUSEWHEEL_ROUTING_FOCUS, 1 = MOUSEWHEEL_ROUTING_HYBRID (Win8/8.1),
// 2 = MOUSEWHEEL_ROUTING_MOUSE_POS (Win10+ "scroll inactive windows" default)
enum UINT_PTR (constant) _error_ app.TIMER_ID = __errorTIMER_ID = 1;
enum (constant) int app.TICK_MS = 16TICK_MS = 16;
enum (constant) int app.LINE_PX = 14LINE_PX = 14; // ruler row height
// ---------------------------------------------------------------------------
// Per-axis scroll state: the correct carry-the-remainder accumulator next to
// the buggy per-event-truncation model.
struct (struct) app.AxisAxis
{
int (field) int app.Axis.accacc; // carried sub-120 remainder (correct model)
int (field) int app.Axis.detentsdetents; // total detents emitted by the correct model
int (field) int app.Axis.truncDetentstruncDetents; // total detents emitted by per-event truncation (buggy)
int (field) int app.Axis.gEventsgEvents, (field) int app.Axis.gDeltagDelta, (field) int app.Axis.gDetentsgDetents, (field) int app.Axis.gTruncDetentsgTruncDetents; // per-gesture counters
}
struct (struct) app.DemoDemo
{
HDC (field) _error_ app.Demo.memDcmemDc;
HBITMAP (field) _error_ app.Demo.dibdib, (field) _error_ app.Demo.stockBmpstockBmp;
uint* (field) uint* app.Demo.pixelspixels;
int (field) int app.Demo.widthwidth, (field) int app.Demo.heightheight;
uint (field) uint app.Demo.frameframe, (field) uint app.Demo.ticksticks;
HWND (field) _error_ app.Demo.hwndhwnd, (field) _error_ app.Demo.otherother;
bool (field) bool app.Demo.autoExitautoExit;
bool (field) bool app.Demo.truncatetruncate; // WSI_TRUNCATE=1: the buggy model drives the ruler
(struct) app.AxisAxis (field) app.Axis app.Demo.vv, (field) app.Axis app.Demo.hh;
int (field) int app.Demo.scrollLinesscrollLines = 3; // SPI_GETWHEELSCROLLLINES
int (field) int app.Demo.rulerLinerulerLine; // ruler offset in lines (driven by v-axis detents)
const(char)* (field) const(char)* app.Demo.gesturegesture = "";
}
__gshared (struct) app.DemoDemo _error_ app.gg;
// ---------------------------------------------------------------------------
// Wheel handling: one handler for both axes and both windows.
void app.onWheelonWheel(HWND (parameter) HWND hwndhwnd, const(char)* axis, ref (struct) app.AxisAxis (parameter) Axis axax, WPARAM wParam, LPARAM lParam) nothrow
{
const _error_ deltadelta = GET_WHEEL_DELTA_WPARAM(wParam); // signed; multiple/fraction of 120
const _error_ targettarget = hwnd is g.g.hwndhwnd ? "main".ptr : "other".ptr;
// Correct model: accumulate, emit detents, CARRY the remainder.
// (D's / truncates toward zero, so the remainder keeps the right sign.)
ax.ax.accacc += delta;
const _error_ dd = ax.ax.accacc / WHEEL_DELTA;
ax.ax.accacc -= d * WHEEL_DELTA;
// Buggy model: truncate each event in isolation — ±40 events vanish.
const _error_ tdtd = delta / WHEEL_DELTA;
ax.ax.detentsdetents += d;
ax.ax.truncDetentstruncDetents += td;
ax.ax.gEventsgEvents++;
ax.ax.gDeltagDelta += delta;
ax.ax.gDetentsgDetents += d;
ax.ax.gTruncDetentsgTruncDetents += td;
logEvent("scroll axis=%s value=%d target=%s screen=%d,%d keys=0x%x acc=%d detents=%d trunc_detents=%d",
axis, delta, target,
cast(int) cast(short)(lParam & 0xffff),
cast(int) cast(short)((lParam >> 16) & 0xffff),
cast(uint)(wParam & 0xffff), ax.ax.accacc, d, td);
if (hwnd is g.g.hwndhwnd && axis[0] == 'v')
{
const _error_ lineslines = (g.g.truncatetruncate ? td : d) * g.g.scrollLinesscrollLines;
if (lines != 0)
{
g.g.rulerLinerulerLine += lines;
logEvent("ruler scroll lines=%d pos_line=%d", lines, g.g.rulerLinerulerLine);
}
}
}
void void app.gestureBegin(const(char)* name) nothrowgestureBegin(const(char)* (parameter) const(char)* namename) nothrow
{
g.g.gesturegesture = name;
g.(field) _error_ g.vv.g.v.gEventsgEvents = g.g.vv.g.v.gDeltagDelta = g.g.vv.g.v.gDetentsgDetents = g.g.vv.g.v.gTruncDetentsgTruncDetents = 0;
g.(field) _error_ g.hh.g.h.gEventsgEvents = g.g.hh.g.h.gDeltagDelta = g.g.hh.g.h.gDetentsgDetents = g.g.hh.g.h.gTruncDetentsgTruncDetents = 0;
logEvent("gesture_begin name=%s", name);
}
void void app.gestureEnd() nothrowgestureEnd() nothrow
{
foreach ((parameter) ii, (parameter) axax; [&g.(field) _error_ g.vv, &g.(field) _error_ g.hh])
if (ax.ax.gEventsgEvents != 0)
logEvent("gesture_summary name=%s axis=%s events=%d delta_total=%d detents=%d trunc_detents=%d acc_left=%d lost_by_truncation=%d",
g.g.gesturegesture, i == 0 ? "v".ptr : "h".ptr, ax.ax.gEventsgEvents, ax.ax.gDeltagDelta,
ax.ax.gDetentsgDetents, ax.ax.gTruncDetentsgTruncDetents, ax.ax.accacc,
ax.ax.gDetentsgDetents - ax.ax.gTruncDetentsgTruncDetents);
}
// ---------------------------------------------------------------------------
// Injection.
void void app.injectWheel(bool horiz, int data) nothrowinjectWheel(bool (parameter) bool horizhoriz, int (parameter) int datadata) nothrow
{
INPUT (local variable) _error_ inpinp;
inp.type = INPUT_MOUSE;
inp.mi.mouseData = cast(DWORD) data;
inp.mi.dwFlags = horiz ? MOUSEEVENTF_HWHEEL : MOUSEEVENTF_WHEEL;
logEvent("inject axis=%s data=%d", horiz ? "h".ptr : "v".ptr, data);
if (SendInput(1, &inp, INPUT.sizeof) != 1)
logEvent("error what=SendInput code=%lu", GetLastError());
}
// ---------------------------------------------------------------------------
// Backbuffer + ruler rendering: a tick row every line, a bright major row
// every 5 lines, offset by the scrolled line count.
void void app.createBackbuffer(int w, int h) nothrowcreateBackbuffer(int (parameter) int ww, int (parameter) int hh) nothrow
{
if (g.(field) _error_ g.dibdib !is null)
{
SelectObject(g.g.memDcmemDc, g.g.stockBmpstockBmp);
DeleteObject(g.g.dibdib);
g.g.dibdib = null;
g.g.pixelspixels = null;
g.g.widthwidth = g.g.heightheight = 0;
}
if ((parameter) int ww <= 0 || (parameter) int hh <= 0)
return;
BITMAPINFO (local variable) _error_ bmibmi;
bmi.bmiHeader.biSize = BITMAPINFOHEADER.sizeof;
bmi.bmiHeader.biWidth = w;
bmi.bmiHeader.biHeight = -h;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
void* (local variable) void* bitsbits;
g.g.dibdib = CreateDIBSection(null, &bmi, DIB_RGB_COLORS, &bits, null, 0);
if (g.(field) _error_ g.dibdib is null)
{
logEvent("error what=CreateDIBSection code=%lu", GetLastError());
return;
}
g.g.pixelspixels = cast(uint*) bits;
g.g.widthwidth = w;
g.g.heightheight = h;
g.g.stockBmpstockBmp = cast(HBITMAP) SelectObject(g.g.memDcmemDc, g.g.dibdib);
}
void void app.drawFrame() nothrowdrawFrame() nothrow
{
if (g.(field) _error_ g.pixelspixels is null)
return;
const (local variable) const(_error_) ww = g.(field) _error_ g.widthwidth, (local variable) const(_error_) hh = g.(field) _error_ g.heightheight;
foreach ((parameter) yy; 0 .. h)
{
uint* _error_ rowrow = g.g.pixelspixels + cast(size_t) y * w;
// worldLine of this row, given the current scroll offset
const _error_ worldworld = y + g.g.rulerLinerulerLine * LINE_PX;
int _error_ mm = world % LINE_PX;
if (m < 0)
m += LINE_PX;
const _error_ lineNolineNo = (world - m) / LINE_PX;
const _error_ isTickisTick = m == 0;
const _error_ majormajor = isTick && lineNo % 5 == 0;
const _error_ bgbg = cast(uint)(0x101820 + ((lineNo & 1) ? 0x080808 : 0));
const _error_ tickLentickLen = major ? w : w / 8;
foreach ((parameter) xx; 0 .. w)
row[x] = isTick && x < tickLen ? (major ? 0xffffff : 0x607080) : bg;
}
}
// ---------------------------------------------------------------------------
// The bounded-run schedule (one entry per WM_TIMER tick).
void app.runSchedulerunSchedule(HWND (parameter) HWND hwndhwnd) nothrow
{
switch (g.g.ticksticks)
{
case 4:
gestureBegin("one_detent_up");
injectWheel(false, WHEEL_DELTA); // +120: away from the user
break;
case 8: gestureEnd(); break;
case 10:
gestureBegin("one_detent_down");
injectWheel(false, -WHEEL_DELTA);
break;
case 14: gestureEnd(); break;
case 16: // 3 x +40: each event is sub-detent; only the SUM is one detent
gestureBegin("sub_detent_x3");
injectWheel(false, 40);
injectWheel(false, 40);
injectWheel(false, 40);
break;
case 22: gestureEnd(); break;
case 24:
gestureBegin("sub_detent_x3_down");
injectWheel(false, -40);
injectWheel(false, -40);
injectWheel(false, -40);
break;
case 30: gestureEnd(); break;
case 32: // one event carrying three detents at once
gestureBegin("multi_detent");
injectWheel(false, 3 * WHEEL_DELTA);
break;
case 36: gestureEnd(); break;
case 38: // net +40: the accumulator must NOT emit a line (and not lose it)
gestureBegin("jitter_no_detent");
injectWheel(false, 40);
injectWheel(false, -40);
injectWheel(false, 40);
break;
case 44: gestureEnd(); break;
case 46:
gestureBegin("horizontal");
injectWheel(true, WHEEL_DELTA);
injectWheel(true, -40);
injectWheel(true, -40);
injectWheel(true, -40);
break;
case 52: gestureEnd(); break;
case 54: // routing: focus on MAIN, cursor over OTHER, who gets the wheel?
gestureBegin("routing_probe");
RECT _error_ rcrc;
GetWindowRect(g.g.otherother, &rc);
const _error_ cxcx = (rc.left + rc.right) / 2, _error_ cycy = (rc.top + rc.bottom) / 2;
SetCursorPos(cx, cy);
POINT _error_ pp;
GetCursorPos(&p);
logEvent("routing_probe focus=%s cursor_over=other requested=%d,%d actual=%d,%d",
GetFocus() is g.g.hwndhwnd ? "main".ptr : "other".ptr,
cast(int) cx, cast(int) cy, cast(int) p.p.xx, cast(int) p.p.yy);
break;
case 56: injectWheel(false, WHEEL_DELTA); break;
case 60:
gestureEnd();
RECT _error_ rcrc;
GetWindowRect(hwnd, &rc);
SetCursorPos((rc.left + rc.right) / 2, (rc.top + rc.bottom) / 2);
break;
case 64:
logEvent("summary v_detents=%d v_trunc_detents=%d v_acc=%d h_detents=%d h_trunc_detents=%d h_acc=%d ruler_pos_line=%d",
g.g.vv.g.v.detentsdetents, g.g.vv.g.v.truncDetentstruncDetents, g.g.vv.g.v.accacc,
g.g.hh.g.h.detentsdetents, g.g.hh.g.h.truncDetentstruncDetents, g.g.hh.g.h.accacc, g.g.rulerLinerulerLine);
DestroyWindow(hwnd);
break;
default:
break;
}
}
// ---------------------------------------------------------------------------
extern (Windows) LRESULT app.wndProcwndProc(HWND (parameter) HWND hwndhwnd, UINT (parameter) UINT msgmsg, WPARAM wParam, LPARAM lParam) nothrow
{
switch (msg)
{
case WM_CREATE:
if (g.g.memDcmemDc is null)
g.g.memDcmemDc = CreateCompatibleDC(null);
return 0;
case WM_MOUSEWHEEL:
onWheel(hwnd, "v", g.g.vv, wParam, lParam);
return 0;
case WM_MOUSEHWHEEL:
onWheel(hwnd, "h", g.g.hh, wParam, lParam);
return 0; // an app that handles WM_MOUSEHWHEEL must return zero
case WM_SIZE:
const _error_ ww = cast(int)(lParam & 0xffff);
const _error_ hh = cast(int)((lParam >> 16) & 0xffff);
if (wParam == SIZE_MINIMIZED)
return 0;
if (hwnd is g.g.hwndhwnd && (w != g.g.widthwidth || h != g.g.heightheight))
createBackbuffer(w, h);
return 0;
case WM_PAINT:
PAINTSTRUCT _error_ psps;
HDC _error_ hdchdc = BeginPaint(hwnd, &ps);
if (hwnd is g.g.hwndhwnd)
{
++g.g.frameframe;
drawFrame();
if (g.g.pixelspixels !is null)
BitBlt(hdc, 0, 0, g.g.widthwidth, g.g.heightheight, g.g.memDcmemDc, 0, 0, SRCCOPY);
}
EndPaint(hwnd, &ps);
return 0;
case WM_TIMER:
if (wParam != TIMER_ID)
return 0;
++g.g.ticksticks;
InvalidateRect(hwnd, null, FALSE);
if (g.g.autoExitautoExit)
runSchedule(hwnd);
return 0;
case WM_CLOSE:
logEvent("close_requested");
goto default;
case WM_DESTROY:
if (hwnd !is g.g.hwndhwnd)
goto default;
KillTimer(hwnd, TIMER_ID);
createBackbuffer(0, 0);
if (g.g.memDcmemDc !is null)
{
DeleteDC(g.g.memDcmemDc);
g.g.memDcmemDc = null;
}
PostQuitMessage(0);
return 0;
default:
return DefWindowProcW(hwnd, msg, wParam, lParam);
}
}
// ---------------------------------------------------------------------------
bool bool app.envFlag(const(wchar)* name) nothrowenvFlag(const(wchar)* (parameter) const(wchar)* namename) nothrow
{
WCHAR[16] (local variable) _error_ bufbuf;
const (local variable) const(_error_) nn = GetEnvironmentVariableW(name, buf.ptr, buf.length);
return n >= 1 && n < buf.length && buf[0] == '1';
}
int int D main()main()
{
instrumentInit("f11_scroll_win32");
logEvent("init_start");
g.g.autoExitautoExit = envFlag("WSI_AUTO_EXIT"w.ptr);
g.g.truncatetruncate = envFlag("WSI_TRUNCATE"w.ptr);
logEvent("mode auto_exit=%d truncate=%d", g.g.autoExitautoExit ? 1 : 0, g.g.truncatetruncate ? 1 : 0);
// System scroll parameters.
UINT (local variable) _error_ lineslines = 3, (local variable) _error_ charschars = 3;
const (local variable) const(_error_) okLokL = SystemParametersInfoW(SPI_GETWHEELSCROLLLINES, 0, &lines, 0);
const (local variable) const(_error_) okCokC = SystemParametersInfoW(SPI_GETWHEELSCROLLCHARS, 0, &chars, 0);
if (okL && lines > 0 && lines != WHEEL_PAGESCROLL)
g.g.scrollLinesscrollLines = cast(int) lines;
logEvent("wheel_params scroll_lines=%u ok=%d scroll_chars=%u ok=%d wheel_delta=%d",
lines, okL, chars, okC, WHEEL_DELTA);
DWORD (local variable) _error_ routingrouting = 0xdead;
SetLastError(0);
const (local variable) const(_error_) okRokR = SystemParametersInfoW(SPI_GETMOUSEWHEELROUTING, 0, &routing, 0);
logEvent("wheel_routing spi=0x201C ok=%d value=%lu err=%lu",
okR, routing, okR ? 0 : GetLastError());
HINSTANCE (local variable) _error_ hInsthInst = GetModuleHandleW(null);
HCURSOR (local variable) _error_ arrowarrow = LoadCursorW(null, IDC_ARROW);
auto (local variable) wstring clsNameclsName = "wsi-f11-class"w;
WNDCLASSEXW (local variable) _error_ wcwc;
wc.cbSize = WNDCLASSEXW.sizeof;
wc.lpfnWndProc = &wndProc;
wc.hInstance = hInst;
wc.lpszClassName = clsName.ptr;
wc.hCursor = arrow;
if (!RegisterClassExW(&wc))
{
logEvent("error what=RegisterClassExW code=%lu", GetLastError());
return 1;
}
// Positions chosen to fit a 640x480 Xvfb screen with no overlap, so the
// routing probe's cursor-over-other warp stays on-screen under winex11.
g.g.hwndhwnd = CreateWindowExW(0, clsName.ptr, "wsi-f11-scroll"w.ptr,
WS_OVERLAPPEDWINDOW, 20, 140, 480, 320, null, null, hInst, null);
if (g.(field) _error_ g.hwndhwnd is null)
{
logEvent("error what=CreateWindowExW code=%lu", GetLastError());
return 1;
}
logEvent("window_created");
// The routing-probe window: same class (its WM_MOUSEWHEEL logs
// target=other), shown beside the main window without taking activation.
g.g.otherother = CreateWindowExW(0, clsName.ptr, "wsi-f11-other"w.ptr,
WS_OVERLAPPEDWINDOW, 510, 10, 120, 100, null, null, hInst, null);
ShowWindow(g.g.otherother, SW_SHOWNOACTIVATE);
ShowWindow(g.g.hwndhwnd, SW_SHOW);
UpdateWindow(g.g.hwndhwnd);
// Park the cursor over the main window so cursor-pos routing (if active)
// also targets it during the accumulation gestures.
RECT (local variable) _error_ rcrc;
GetWindowRect(g.g.hwndhwnd, &rc);
SetCursorPos((rc.left + rc.right) / 2, (rc.top + rc.bottom) / 2);
SetTimer(g.g.hwndhwnd, TIMER_ID, TICK_MS, null);
MSG (local variable) _error_ msgmsg;
while (GetMessageW(&msg, null, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
logEvent("exit code=%d", cast(int) msg.wParam);
return cast(int) msg.wParam;
}