Fix the Win32 paint/message pipeline and implement GDI/USER/common-control APIs for MFC apps#49
Draft
ohuet wants to merge 69 commits into
Draft
Fix the Win32 paint/message pipeline and implement GDI/USER/common-control APIs for MFC apps#49ohuet wants to merge 69 commits into
ohuet wants to merge 69 commits into
Conversation
Android virtual keyboards (Gboard, etc.) deliver typed characters via
beforeinput events with `e.data` instead of keydown — keydown arrives
with keyCode=229, an empty `e.code` and `e.key="Unidentified"`, so the
existing scancode lookup never matches and nothing reaches the emulator.
Hook a beforeinput handler that synthesises both make and break codes
(Android emits no keyup), and add an `e.key` fallback in keydown for the
sources that do emit a keydown but leave `e.code` blank (Bluetooth
keyboards). On desktop, preventDefault in keydown cancels the would-be
text insertion, so beforeinput never fires for physical frappes — no
behavioural change there.
- src/components/ConsoleView.tsx:
add CHAR_TO_SCANCODE reverse table (US layout) for printable chars
add KEY_TO_SCANCODE / KEY_IS_EXTENDED fallbacks for empty-code events
skip preventDefault on keyCode=229 keydowns to avoid disturbing the
IME composition pipeline
new handleBeforeInput captures insertText / deleteContentBackward /
insertLineBreak and emits scan + scan|0x80 in one go since Android
sends no matching keyup
wire onBeforeInput on the hidden console input plus
autoCapitalize/autoComplete/autoCorrect="off" and spellcheck={false}
so the IME does not transform the typed character
- src/lib/pe/parse.ts: detect UPX0/UPX1 sections, set peInfo.isUpxPacked guard rsrc directory walk against negative offsets - src/lib/pe/types.ts: add isUpxPacked flag - src/lib/emu/upx-runtime.ts (new): runUpxStub ticks the embedded decompressor stub until EIP enters UPX0 wraps GetProcAddress during warmup to auto-stub unknown (dll,name) so CRT init doesn't ExitProcess snapshotUnpackedPE rebuilds a synthetic ArrayBuffer where PointerToRawData==VirtualAddress re-parses the snapshot to recover imports and resources hidden on disk - src/lib/emu/emu-load.ts: invoke unpackUpxInPlace between thunk-build and preloadStrings - src/lib/emu/dll-ordinals.ts (new): shared ordinalMap extracted from emu-thunks-pe.ts fixes WS2_32 ordinals 101-116 (115=WSAStartup, 116=WSACleanup, etc.) adds OLEAUT32 ordinals 2-50, 147, 149-187, 420 - src/lib/emu/emu-thunks-pe.ts: import shared dllOrdinalMap (no behaviour change) - src/lib/emu/emulator.ts: stubDllHandles / stubDllByBase / nextStubDllHandle fields - src/lib/emu/win32/kernel32/module.ts: LoadLibraryA returns unique pseudo-handle per stub-DLL GetProcAddress translates ordinal to name via per-DLL map when hModule is a stub prefers handler from the identified DLL before scanning all apiDefs GetModuleHandleA/W and GetModuleHandleExW resolve stub-DLL names getModuleFileName recognises stub-DLL handles - src/lib/emu/win32/ws2_32.ts: WSAStartup negotiates wsaData.wVersion = min(requested, 0x0202) so MFC AfxSocketInit's MAKEWORD(1,1) check passes - src/lib/emu/win32/user32/dialog.ts: lazy extractDialogs cache so UPX-unpacked dialogs aren't extracted with the on-disk empty UPX0 buffer - src/lib/emu/win32/oledlg.ts (new): full OLEDLG module stubbing OleUI* dialogs - src/lib/emu/win32/kernel32/locale.ts: ConvertDefaultLocale - src/lib/emu/win32/kernel32/resource.ts: EnumResourceTypesA/W, EnumResourceLanguagesA/W, EnumResourceNamesA - src/lib/emu/win32/advapi32.ts: SetFileSecurityA/W, GetFileSecurityA/W - src/lib/emu/win32/comdlg32.ts: PrintDlgA/W, GetFileTitleA - src/lib/emu/win32/ole32.ts: IsAccelerator, OleTranslateAccelerator, CoFreeUnusedLibraries, CLSIDFromProgID, CoGetClassObject, StgOpenStorageOnILockBytes - src/lib/emu/win32/shlwapi.ts: PathIsUNCA/W, PathFindExtensionA, PathFindFileNameA/W, PathStripToRootA - src/lib/emu/win32/winspool.ts: GetJobA/W - src/lib/emu/win32/gdi32/text.ts: CreateDCA, StartDocA, GetTextFaceA, GetCharWidthA, GetCharWidth32A - src/lib/emu/win32/user32/text.ts: DrawTextExA - src/lib/emu/win32/user32/message.ts: PostThreadMessageA - src/lib/emu/win32/user32/resource.ts: real CreateAcceleratorTableA/W, DestroyAcceleratorTable, CopyAcceleratorTableA - src/lib/emu/win32/user32/misc.ts: drop duplicate CreateAcceleratorTableW stub - tests/test-pablodraw.mjs (new): UPX-packed MFC PE end-to-end harness; accepts a clean exit past AfxSocketInit as PARTIAL success Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… stubs
- src/lib/emu/win32/oleaut32.ts :
VarBstrFromR4/R8/I2/I4 (variant-to-BSTR conversions)
SafeArrayDestroy/SafeArrayDestroyData/SafeArrayDestroyDescriptor
SysStringByteLen
OleTranslateColor (OLE_COLOR -> COLORREF)
- src/lib/emu/win32/user32/misc.ts : NotifyWinEvent (accessibility no-op)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CBTProc takes 3 args (nCode, wParam, lParam) but emu.callWndProc treated
it as a 4-arg WindowProc, pushing 4 args + WNDPROC_RETURN_THUNK = 20 bytes.
The hook then `ret 0xC` popping only 16 bytes, leaving a 4-byte stack
imbalance. The leftover bytes shifted the OUTER CreateWindowExA handler's
ESP, causing emuCompleteThunk to read retAddr from a wrong slot — which
the very next callWndProc setup overwrote with a wParam=0 push, sending
the program to EIP=0 and walking through zero memory until SYSTEM:HALT.
Switch CBT hook calls to emu.callCallback with 3 args, matching the
real CBTProc signature.
Generic fix; benefits any app that registers a CBT hook (most MFC apps).
- src/lib/emu/win32/user32/create-window.ts :
CreateWindowExA et CreateWindowExW : utiliser callCallback au lieu de
callWndProc pour les hooks CBT (3 args, pas 4) ; commentaire detaille
expliquant la cause du desequilibre de pile.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ToolbarWindow32 is registered as a built-in class with wndProc=0, so
SendMessage routes to handleBuiltinMessage. Without per-class handling,
TB_ADDBUTTONS returned 0 (FALSE), which made MFC's CToolBar::SetButtons
fail and cascaded to CMainFrame::OnCreate returning -1.
Stubs return TRUE/0 as expected by MFC for the standard TB_* setters
(TB_BUTTONSTRUCTSIZE, TB_ADDBUTTONS, TB_AUTOSIZE, TB_SETBITMAPSIZE,
TB_SETBUTTONSIZE, TB_LOADIMAGES, etc.) and store button state per
toolbar instance for queries (TB_GETBUTTON, TB_BUTTONCOUNT,
TB_COMMANDTOINDEX, TB_GETSTATE/SETSTATE).
Generic fix; benefits any MFC app that uses CToolBar.
- src/lib/emu/win32/user32/message.ts :
nouveau bloc TOOLBARWINDOW32 dans handleBuiltinMessage avec gestion
des principaux messages TB_* (button list, state queries, sizing,
image lists, hit-test). Stocke les boutons dans wnd.tbButtons.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tVersion
PabloDraw 2.0.8.70 (and most MFC apps) call DefWindowProc(TB_*, ...)
directly from CToolBar::SetButtons instead of SendMessage, because the
calling code is already inside the toolbar's wndProc and wants to
forward to the original built-in behaviour. Our DefWindowProcA only
handled WM_NCCREATE/WM_PAINT/WM_CLOSE etc. and returned 0 for unknown
messages — including TB_ADDBUTTONS, which MFC interpreted as FALSE
("button add failed") → CToolBar::LoadToolBar returned FALSE →
CMainFrame::OnCreate returned -1 → InitInstance failed → ExitProcess.
Expose registerMessage's handleBuiltinMessage on emu as
dispatchBuiltinMessage and delegate from DefWindowProcA so control
messages (TB_*, EM_*, LB_*, CB_*, …) work whether the app sends them
via SendMessage or via DefWindowProc. PabloDraw now reaches the
message loop after 21 ticks; main MDI frame, status bar, toolbar,
4 dock bars, and the canvas/colour/charset/preview/chat panes all
get created.
Also add COMCTL32:DllGetVersion (reports v6.0) so MFC and other apps
can detect modern common-control features without falling back to
legacy code paths.
- src/lib/emu/emulator.ts : nouveau champ dispatchBuiltinMessage sur Emulator.
- src/lib/emu/win32/user32/message.ts : exposer handleBuiltinMessage via emu.dispatchBuiltinMessage.
- src/lib/emu/win32/user32/wndproc.ts :
DefWindowProcA delegue d'abord a emu.dispatchBuiltinMessage,
avant de tomber sur le switch des messages WM_* généraux.
- src/lib/emu/win32/comctl32.ts : nouveau stub DllGetVersion (DLLVERSIONINFO v6.0).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MFC timer handlers (e.g. CCanvasView's 200 ms blink) re-arm their timer
from inside WM_TIMER:
void OnTimer(UINT id) {
if (id == ID_BLINK) {
... do work ...
SetTimer(ID_BLINK, BLINK_TIMER_SPEED, NULL); // re-arm
}
}
Real Win32 SetTimer with the same (hwnd, id, elapse) is essentially a
no-op — it doesn't actually reset the underlying countdown if the
parameters are identical. Our previous implementation called
clearInterval+setInterval on every WM_TIMER, which:
- logged "[TIMER] SetTimer ..." 5×/s for every active blink (console spam)
- churned the JS interval scheduler unnecessarily
- made the SetTimer call appear to fire constantly in the dev console
Now we look up the existing entry and reuse it when (hwnd, id, elapse,
timerFunc) all match. The first SetTimer for a given (hwnd, id) is
still logged once.
- src/lib/emu/emulator.ts :
timers Map stores { jsTimer, elapse, timerFunc } instead of just
the JS interval handle. setWin32Timer accepts elapse + timerFunc.
Nouvelle methode getWin32Timer pour la coalescence.
- src/lib/emu/win32/user32/timer.ts :
SetTimer detecte les re-arms identiques et les ignore. Log seulement
le premier arming d'un timer donne.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PabloDraw's CCanvasView blink timer (200 ms WM_TIMER → OnTimer →
UpdateRegion → invalidate cells → WM_PAINT) runs heavy x86 code
inside a single wndProc dispatch. Win32 callStdcall executed the
entire wndProc synchronously with no time budget, so the JS thread
stayed blocked for hundreds of milliseconds at a time — even our
own retrotick taskbar buttons stopped responding promptly.
Add a 16 ms time-based yield to callStdcall, mirroring the existing
mechanism in callStdcall16. When the budget is exhausted, push the
current frame to _wndProcFrames + set _wndProcSetupPending and
return `undefined`. The outer dispatch loop detects this, fills in
the frame's outerStackBytes/outerCompleter, and the wndProc resumes
naturally on the next tick — eventually completing via the
WNDPROC_RETURN_THUNK path.
Yields are gated on emu._allowWndProcYield, set ONLY by
DispatchMessageA which is the one Win32 call site that already
propagates `undefined` correctly. Init-time handlers like
CreateWindowExA chain WM_NCCREATE → WM_NCCALCSIZE → WM_CREATE
synchronously and are NOT async-aware, so yields stay disabled
during init (test still reaches msg loop in 20 ticks).
- src/lib/emu/emulator.ts : new flag _allowWndProcYield (default false).
- src/lib/emu/emu-exec.ts : callStdcall checks the flag + targetDepth==0,
yields after YIELD_MS=16ms by pushing frame and returning undefined.
- src/lib/emu/win32/user32/message.ts : DispatchMessageA enables the
flag around its emu.callWndProc call (saves+restores prev value
so nested dispatch works).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PabloDraw and other MFC apps that build their menu dynamically with
CreateMenu + AppendMenu (no RT_MENU resource) only expose the structure
through SetMenu(hwnd, hMenu). Without a hook, the React MenuBar stays
empty even though the in-memory tree is correctly populated. Also fix
UPX-packed PEs whose resources are not visible until after the
decompression stub runs inside emu.load() — re-extract menus and icons
from the unpacked image so the title bar gets the real app icon.
- src/lib/emu/win32/user32/menu.ts: SetMenu now fires emu.onSetMenu(hwnd, hMenu)
- src/lib/emu/emulator.ts: declare optional onSetMenu callback
- src/components/EmulatorView.tsx:
re-run extractMenus/extractIcons on emu.arrayBuffer post-load when
peInfo.isUpxPacked, so UPX exes get their resources;
register emu.onSetMenu to walk the runtime InternalMenuItem tree of
the main window, convert it into MenuResult[], and feed setMenus +
emu.menuItems so the top-level MenuBar shows File/Edit/View/etc.
A hardcoded 320x240 default crushes any framed MFC app with docked
toolbars (PabloDraw fits its CFrameWnd, status bar, dockable color
bar, char-set bar, preview, and chat into a 320x240 area which makes
every dock 30 px tall). Win32 docs spell this out: the "system
default size" used for CW_USEDEFAULT is screen-relative, not a fixed
constant. Take 75% of emu.screenWidth/screenHeight with a floor of
640x480 so small embedded screens still get a sane default.
- src/lib/emu/win32/user32/create-window.ts: both CreateWindowExA and
CreateWindowExW paths now compute defaultW/defaultH from
emu.screenWidth/screenHeight (floored at 640/480) instead of
falling back to 320x240
Built-in classes (msctls_statusbar32, ToolbarWindow32, ...) have
wndProc=0 in our handle tree. Our CreateWindowEx fires
callWndProc(wndProc, WM_NCCREATE/NCCALCSIZE/WM_CREATE), but callStdcall(0,
...) short-circuits to 0 — so the class-specific NCCREATE handler in
handleBuiltinMessage was never invoked. That's why CStatusBar reached
the message loop with wnd.height=0 (PabloDraw status bar invisible):
the bar's CW_USEDEFAULT height was never replaced by a sensible default.
- src/lib/emu/win32/user32/create-window.ts: CreateWindowExA/W now
route NCCREATE/NCCALCSIZE/WM_CREATE through emu.dispatchBuiltinMessage
when wnd.wndProc is 0 (built-in classes)
- src/lib/emu/win32/user32/message.ts: handleBuiltinMessage for
msctls_statusbar32 now handles WM_NCCREATE by seeding height=22 if
the bar was created with CW_USEDEFAULT; WM_SIZE assigns wnd.height
(it previously only computed it locally) and a few more SB_* msg
constants are kept for completeness
Reusable debug scripts that boot PabloDraw headlessly, run it to the
message loop, and dump its window hierarchy (visible/positions/sizes)
and runtime menu structure. Useful for reasoning about MFC dock layout
issues when the browser MCP screenshot tool cannot capture state from
a busy emulator tab.
- tests/inspect-pablo-tree.mjs : full recursive child tree dump + a
standalone reproduction of the emu-render collectChildren walk to
verify which controls reach the React overlay list
- tests/inspect-pablo-menu.mjs : dumps RT_MENU resource types after
UPX decompression and re-extracts menus from emu.arrayBuffer
ToolbarWindow32 is a built-in control (wndProc=0), so neither the
DOM-overlay branch nor the WM_PAINT branch of renderChildControls
ran for it. Result: a 26 px tall gray strip with no visible buttons
in every MFC app that uses CToolBar — PabloDraw was the most obvious
case. Implement:
1. TB_ADDBITMAP now reads TBADDBITMAP at lParam and stores the HBITMAP
on the toolbar's WindowInfo (when hInst==NULL — the path MFC takes).
2. handleBuiltinMessage rewritten to use the WindowInfo's typed
tb* fields directly rather than a local cast (now declared on
WindowInfo so the renderer can read them).
3. New renderToolbar() in emu-render.ts fills the bar with Win9x grey,
then for each visible TBBUTTON draws the bmpW×bmpH region
`(iBitmap × bmpW, 0)` of the stored bitmap into the bar at the
computed cell position. Pressed/checked buttons get a sunken
background and a top-left dark edge; disabled buttons render with
45 % opacity. Falls back to outlined placeholders if no bitmap
was registered yet so users at least see button slots.
- src/lib/emu/win32/user32/types.ts: extend WindowInfo with tbButtons,
tbButtonStructSize, tbButtonSize, tbBitmapSize, tbBitmapHandle
- src/lib/emu/win32/user32/message.ts: TB_ADDBITMAP stores HBITMAP;
drop the local cast that was making the fields invisible to other
files
- src/lib/emu/emu-render.ts: dispatch renderToolbar() for
TOOLBARWINDOW32 children in renderChildControls; add the painter
Drives PabloDraw to the message loop, posts WM_SIZE to MainFrame with a new larger client size, pumps the emulator, then dumps the recursive window tree before/after. Confirms that MFC's CFrameWnd::OnSize → RecalcLayout → RepositionBars → DeferWindowPos chain runs correctly in our emulator: top-level dock bars and the MDI client redistribute to the new frame size. Inner sub-bars (CColourWindow, etc.) keep their preferred CSize as expected for CBRS_SIZE_DYNAMIC. Useful diagnostic for any "the canvas didn't grow with the frame" regression suspicion.
After dumping the recursive window tree, locate the ToolbarWindow32 and report buttonStructSize, buttonSize (cx/cy), bitmapSize (cx/cy), the cached HBITMAP handle (+ resolved width/height), and every TBBUTTON entry (iBitmap, idCommand, fsState, fsStyle). Confirms that TB_ADDBITMAP capture and TB_ADDBUTTONS parsing both work — PabloDraw yields 9 buttons spanning iBitmap 0..6, bitmap 112×15, button cell 23×22, bitmap glyph 16×15.
renderChildControls fires from main window EndPaint and only paints the toolbar when MainFrame itself is being painted. If MFC dispatches WM_PAINT/WM_ERASEBKGND directly to the toolbar (after InvalidateRect on the toolbar's hwnd, e.g. when a button's state changes), the built- in handler used to return 0 and nothing happened — the toolbar stayed stale. Now WM_PAINT/WM_ERASEBKGND on TOOLBARWINDOW32 flags emu.mainWindow's needsPaint so the message pump synthesises a parent WM_PAINT, and renderChildControls picks up the toolbar in its next pass. The toolbar's own needsPaint is cleared so we don't re-enter on the next GetMessage tick.
PabloDraw (and any CToolBar app) subclasses ToolbarWindow32 — the WindowInfo's wndProc becomes MFC's CToolBar::WindowProc, not 0. With the previous order our renderToolbar() ran first, then the WM_PAINT branch dispatched the same toolbar back to MFC which cleared the bar and never re-painted the button glyphs (it can't find our bitmap through the stock ImageList path). Result: empty 26-px gray strip. Move the TOOLBARWINDOW32 canvas paint to run AFTER the WM_PAINT dispatch in the same allChildren loop iteration. MFC paints the gripper/grey background, then we stamp our bitmap-region buttons on top.
MFC's CToolBar::OnPaint pushes save()/clip rects via SaveDC/IntersectClipRect that may not balance perfectly. If those leak past EndPaint, our renderToolbar drawImage lands inside a degenerate clip and the bitmap regions never reach the visible canvas. Pop 20 levels (same safety net the WM_PAINT branch uses before each custom-wndProc dispatch) before stamping our button glyphs.
After dumping the toolbar state (wndProc, buttons, bitmap), install a mock canvasCtx on the emulator and call renderChildControls() once. Count fillRect / drawImage calls to confirm the paint path actually fires for the toolbar — PabloDraw yields 1 fillRect (the bar background) + 7 drawImage (one per visible button glyph; the 2 separators don't drawImage). Useful for regression-testing toolbar paint without a browser. Also log the toolbar's wndProc so we know whether it's still built-in (0) or MFC-subclassed (non-zero) at message-loop entry.
Add bitmap object key/type dump and a direct loadBitmapResource(127)
comparison. Finding: the bitmap MFC stored on the toolbar via
TB_ADDBITMAP has only { width, height, canvas, ctx } — matches
CreateCompatibleBitmap's BitmapInfo shape exactly. Loading the same
RT_BITMAP id 127 directly via emu.loadBitmapResource() yields the full
{ width, height, canvas, ctx, imageData } with valid pixels.
So MFC's CToolBar::AddReplaceBitmap copies the LoadImage bitmap into a
freshly-created CompatibleBitmap (likely via the BitBlt-into-image-list
path) and stores that copy. Our BitBlt's drawImage from source canvas
should copy the pixels into the dest CompatibleBitmap, but the test
mock canvas doesn't actually carry pixels — and the browser test
revealed the dest canvas is also empty (drawImage from it draws
nothing).
Next: trace why the source-canvas → dest-canvas drawImage in BitBlt
yields zero pixels in the browser, or change TB_ADDBITMAP to also
remember the source LoadImage handle so renderToolbar can fall back
to the original DIB when the captured bitmap is empty.
MFC CToolBar::LoadToolBar loads the toolbar bitmap by reading raw
DIB data via FindResource+LockResource, allocating a CompatibleBitmap,
then calling StretchDIBits to copy the DIB into the compat bitmap.
Our StretchDIBits was a stub returning 0, so the toolbar bitmap stayed
empty and no button glyphs were visible. PabloDraw now shows its 7
toolbar buttons (New/Open/Save/Cut/Copy/Paste/Help).
Also tag resource bitmaps with their resourceId/resourceModule and
propagate through SelectObject and BitBlt SRCCOPY, so renderToolbar
can fall back to a direct resource reload if a bitmap arrives via the
CreateCompatibleBitmap+BitBlt pattern instead.
- src/lib/emu/win32/gdi32/bitmap.ts :
full StretchDIBits implementation (1/4/8/24/32 bpp, palette
resolution, top-down/bottom-up, stretching via intermediate canvas)
BitBlt SRCCOPY now propagates srcDC.selectedBitmapResId to dst bmp
- src/lib/emu/win32/gdi32/select.ts :
SelectObject propagates bmp.resourceId/resourceModule to the DC
- src/lib/emu/win32/gdi32/types.ts :
new BitmapInfo.resourceId/resourceModule
new DCInfo.selectedBitmapResId/selectedBitmapResModule
- src/lib/emu/emu-window.ts :
loadBitmapResource* tag bitmaps with their resourceId
- src/lib/emu/emu-render.ts :
renderToolbar falls back to loadBitmapResource when the captured
compat bitmap carries a propagated resourceId
- tests/inspect-pablo-tree.mjs :
silence console.log during boot (removed verbose trace flags)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PabloDraw and other ANSI MFC apps call SB_SETTEXTA (0x0401) — the ANSI
variant — to set status bar panel text. Only SB_SETTEXTW was handled,
so PabloDraw's "INS CAP NUM SCRL" status was silently dropped.
Also call notifyControlOverlays after both SETTEXTW and SETTEXTA so
the React StatusBar component receives the update without waiting for
the next full WM_PAINT cycle.
- src/lib/emu/win32/user32/message.ts :
new SB_SETTEXTA handler (mirrors SETTEXTW, reads CString)
SB_SETTEXTW / SB_SETTEXTA now call notifyControlOverlays
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two GDI32 stubs returning 0 are now real implementations, and GetDIBits
had a wrong arg count of 5 (it's 7) which corrupted the stdcall stack
on every call.
GetDIBits: reads pixels from a bitmap into the caller's BITMAPINFO +
buffer. Supports the NULL-lpvBits "just fill the header" mode that MFC
uses to size a buffer before allocating, plus 24/32 bpp pixel readback
(BGR/BGRA, bottom-up rows as Windows expects).
GetObjectType: maps our internal handle types (pen/brush/dc/palette/
font/bitmap) to the Win32 OBJ_* constants. Also handles stock object
handles (WHITE_BRUSH..DEFAULT_GUI_FONT) by GetStockObject index.
MFC's CGdiObject::FromHandlePermanent dispatches on this — a 0 return
made MFC treat every valid handle as invalid.
- src/lib/emu/win32/gdi32/bitmap.ts :
GetDIBits: arg count 5 → 7, full implementation
GetObjectType: returns OBJ_PEN/OBJ_BRUSH/OBJ_DC/OBJ_PALETTE/OBJ_FONT/
OBJ_BITMAP from handle type, with stock-object mapping
new STOCK_BASE import
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three more stubs returning 0 now do real work, all benefiting any
MFC-style Win32 app that reads class-level attributes or maps a DC
back to its owning window.
GetClassLong / SetClassLong (A and W) : read or update class fields
via the GCL_* indices — HBRBACKGROUND/HCURSOR/HICON/HICONSM/HMODULE/
CBWNDEXTRA/CBCLSEXTRA/WNDPROC/STYLE/MENUNAME. MFC's CWnd::GetIcon and
CMainFrame::PreCreateWindow call these; a zero return made small-icon
slots in the title bar fall back to a generic default.
WindowFromDC : DCInfo carries the owning hwnd already; just return it.
Memory DCs (hwnd=0) still report 0, matching Windows.
- src/lib/emu/win32/user32/window-long.ts :
new GetClassLong A/W and SetClassLong A/W
maps GCL_* indices to WndClassInfo fields
- src/lib/emu/win32/user32/misc.ts :
WindowFromDC returns DCInfo.hwnd
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All four FindWindow variants were stubs returning 0 (not found). Apps
that use FindWindow to locate the shell taskbar, system tray, a peer
single-instance, or any sibling window simply silently skipped the
call. Now they iterate handles.findByType('window') and match by class
name, title, atom, and parent scope.
FindWindow : top-level windows (parent === 0). NULL class/title means
"any". Class name accepts both atoms (< 0x10000) and string pointers.
FindWindowEx : adds `after` cursor (resume search past a given hwnd)
and `parent` scope. Same name/atom matching rules.
- src/lib/emu/win32/user32/misc.ts :
real FindWindow A/W and FindWindowEx A/W using findByType('window')
- src/lib/emu/win32/user32/create-window.ts :
duplicate FindWindowW/FindWindowExW stubs removed (kept in misc.ts)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EnumChildWindows used to silently report success without invoking the
caller's EnumWindowsProc. Apps that iterate dialog controls, scan
toolbar children, or walk an MDI client's documents got an empty list
and skipped whatever follow-up they needed.
Now builds the list of immediate children (or all top-level windows
when hWndParent is NULL) and calls the user callback per child via
callWndProc, breaking when the callback returns FALSE — mirroring the
existing EnumWindows implementation.
- src/lib/emu/win32/user32/misc.ts :
EnumChildWindows iterates parent.childList (or top-level windows
when hParent is NULL) and invokes the callback
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both were stubs returning 1 unconditionally. That meant apps that cache
a hwnd and check IsWindow before SendMessage (a very common pattern in
MFC and old Win32 code) always saw "valid", then sent to destroyed
windows. We silently dropped the messages — but the caller's follow-up
logic typically ran anyway, doing the wrong thing.
IsWindow : verifies handles.getType(hwnd) === 'window'.
IsWindowVisible : reads WindowInfo.visible (was always 1, even for
windows hidden via ShowWindow(SW_HIDE)).
- src/lib/emu/win32/user32/create-window.ts :
IsWindow now returns 0 for non-window handles
IsWindowVisible returns the actual WindowInfo.visible flag
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…fect)
Three GDI text/coordinate APIs were silent stubs : TextOutW reported
success but drew nothing, and LPtoDP/DPtoLP both reported success but
left the caller's POINT buffer untouched. Apps that read back converted
coordinates saw whatever was already in the buffer (often zero).
TextOutW : mirror of TextOutA but reads UTF-16 from emulator memory.
Honours OPAQUE/TRANSPARENT bkMode like TextOutA. Localised MFC views
and Unicode-built Notepad/Win2k+ apps now paint their text.
LPtoDP / DPtoLP : in MM_TEXT (default) the transform is identity, so
the caller's buffer is already correct — return 1 without touching
memory. In other mapping modes, compute the window/viewport scale
manually : dx = (lx - wOrg) * vExt / wExt + vOrg.
- src/lib/emu/win32/gdi32/text.ts :
TextOutW : real impl (read UTF-16, fill bkMode background, fillText)
LPtoDP : identity in MM_TEXT, scale by window/viewport extents otherwise
- src/lib/emu/win32/gdi32/draw.ts :
DPtoLP : inverse of LPtoDP, same identity-in-MM_TEXT shortcut
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Default-button highlighting, listbox focus indicators, and the dotted
rectangle around a focused link/control all rely on DrawFocusRect. The
stub returned 1 (success) without painting anything, so apps appeared
to have no focused control.
Uses Canvas setLineDash([1,1]) for the classic 1-pixel dashed pattern,
strokeRect with .5 pixel offset to keep the line crisp on integer
coordinates. Restores DC state via save/restore.
- src/lib/emu/win32/user32/paint.ts :
DrawFocusRect : reads RECT, paints dashed black 1px rectangle
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three coupled APIs were returning constants : GetCursorPos always wrote
(0,0), SetCursorPos was a stub→1 ignoring the requested coords, and
GetMessagePos returned 0. Apps that warp the mouse (DOOM-style games)
or query the cursor for tooltip positioning saw the cursor frozen at
the origin.
Added emu.cursorX/Y, updated by the canvas pointer handler on every
mouse event so the cache stays current. SetCursorPos writes the cache
(we can't move the host OS cursor from inside a browser, but the app
will read back the value it requested). GetMessagePos packs LOWORD=x,
HIWORD=y as Windows does.
- src/lib/emu/emulator.ts :
new cursorX / cursorY fields on Emulator
- src/lib/emu/win32/user32/input.ts :
GetCursorPos writes emu.cursorX/Y instead of (0,0)
- src/lib/emu/win32/user32/misc.ts :
SetCursorPos updates emu.cursorX/Y
- src/lib/emu/win32/user32/message.ts :
GetMessagePos returns packed (x,y) from emu.cursorX/Y
- src/components/EmulatorView.tsx :
handlePointerEvent caches emu.cursorX/Y on every pointer event
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LineTo always drew a solid line, ignoring the pen style, so dotted guides — e.g. PabloDraw's 80-column margin line, drawn with a PS_DOT pen in CCanvasView::OnEraseBkgnd — came out solid (or invisible). Stroke dashed pens via setLineDash with the appropriate dash pattern; solid/null pens keep the Bresenham path. - src/lib/emu/win32/gdi32/draw.ts: add dashedLine() helper; LineTo uses it for pen styles PS_DASH(1)/PS_DOT(2)/PS_DASHDOT(3)/PS_DASHDOTDOT(4) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A CScrollView (PabloDraw's editor, preview) showed no scrollbar: the emulator's DefWindowProc never painted the non-client scroll bars, and these views drive their scrollbars from the scroll range set via SetScrollInfo rather than the WS_VSCROLL style bit. Mirror the scroll state onto the WindowInfo and draw a Win2k-style scrollbar (arrows, trough, proportional thumb) at EndPaint whenever a window has a scrollable range (range > page). - src/lib/emu/win32/user32/types.ts: add scrollH/scrollV to WindowInfo - src/lib/emu/win32/user32/scroll.ts: mirror each bar's state object onto the owning WindowInfo (SB_HORZ->scrollH, SB_VERT->scrollV) - src/lib/emu/win32/user32/paint.ts: drawNcScrollbars() draws V/H scrollbars from the scroll range; called after EndPaint Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PabloDraw's preview pane is a CSizingControlBarG docked on the right; the OS paints its caption (a title strip with a close X), but the emulator's DefWindowProc doesn't, so it showed as a plain black rectangle. Detect a vertically-docked control bar generically (the painted inner view's grandparent is a LEFT/RIGHT AfxControlBar dock bar) and draw the caption strip + raised X button on the frame at the inner view's EndPaint. - src/lib/emu/win32/user32/paint.ts: add drawControlBarCaption(); call from EndPaint alongside drawNcScrollbars Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CW_USEDEFAULT sized a window at 75% of the desktop, which on a small desktop made a framed app's document view narrower than its content (640px for an 80-column editor) — forcing a horizontal scrollbar and pushing the right-margin guide out of view. Use 85% of the desktop but at least 1024x640 (the classic minimum resolution), still capped to the desktop so it never overflows. The editor then exceeds its 640px document width: no horizontal scrollbar, and the 80-column dotted guide is visible — matching how the app looks at a normal size. - src/lib/emu/win32/user32/create-window.ts: raise the CW_USEDEFAULT default size floor (CreateWindowExA/W) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Headless harnesses used to diagnose and verify the PabloDraw layout fixes (paint rects, scrollbars, dock geometry, visibility, NcCalcSize, font, timers). They boot PabloDraw headlessly and dump the window tree / paint geometry. - tests/diag-pablo-rects.mjs: window tree dump (sizes, visibility, scroll state, paint rects) + NcCalcSize probe — the main layout diagnostic - tests/diag-pablo-paintpath.mjs: drives real WM_PAINT on the editor view - tests/diag-pablo-rects-1356.mjs / -click / -divzero / -font / -newdoc / -strict / -timer: focused one-off probes Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Real BeginPaint sends WM_ERASEBKGND to the window proc when the update
region needs erasing; our beginPaint only filled the class brush directly,
so any app overriding OnEraseBkgnd (custom backgrounds, grid/column-guide
overlays) was silently bypassed. PabloDraw draws its dotted 80-column guide
in CCanvasView::OnEraseBkgnd with a PS_DOT pen, which never appeared.
- src/lib/emu/emu-window.ts:
beginPaint captures needsErase before clearing it, keeps the existing
direct class-brush fill as a fallback, then dispatches WM_ERASEBKGND to
wnd.wndProc when erase was requested (hadErase) for non-dialog windows
with a wndProc, guarded by emu._inEraseBkgnd against reentrancy. Apps
that do not override fall through to DefWindowProc (fills the class
brush) so they render identically. Added local WM_ERASEBKGND constant.
- src/lib/emu/emulator.ts:
added _inEraseBkgnd reentrancy guard flag.
Verified headless (tests/diag-pablo-guide.mjs): the editor now emits the
PS_DOT 80-col guide stroke; PabloDraw still reaches the message loop and
pumps 400k ticks with no halt. Build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
renderChildControls force-paints custom child views by calling their
wndProc with WM_PAINT directly, then cleared needsPaint. For deeply-nested
MFC views (e.g. a docked CScrollView preview pane) that forced call is a
no-op — MFC routes WM_PAINT to OnEraseBkgnd/OnDraw only through a genuine
GetMessage -> DispatchMessage cycle — so the pane never painted (stayed
blank/grey). PabloDraw's CPreviewWindow is exactly this case.
- src/lib/emu/emu-render.ts:
for Win32 custom child views, after the forced WM_PAINT, leave
needsPaint set and request needsErase so the message loop also delivers
a real WM_ERASEBKGND + WM_PAINT via synthesizePaint (the erase is where
MFC views fill their background — the preview's black). BeginPaint clears
needsPaint, so this is exactly one real paint per parent-frame paint
cycle (which is rare — not the editor's per-cell caret-blink repaints —
so no repaint storm). Win16 behaviour is unchanged.
Verified headless (tests/diag-pablo-guide.mjs): the preview pane (0x105f)
now fills black (174x513); editor still black + dotted 80-col guide;
PabloDraw reaches the message loop and pumps 400k ticks with no halt.
Build clean. Depends on the prior BeginPaint WM_ERASEBKGND-dispatch commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…X strings
The status-bar overlay computed each pane's width from statusParts (absolute
right edges). MFC sometimes hands SB_SETPARTS a transient array whose values
are not real coordinates, which produced nonsensical widths and pushed every
pane (cursor position, indicators) off-screen — the bar looked empty even
though statusTexts was correct ("1,1", indicators).
- src/components/ControlOverlay.tsx:
validate statusParts (non-decreasing, within the bar width); when they are
not usable, fall back to the conventional MFC layout — the message pane (0)
stretches and the indicator panes size to their content — so the text stays
visible. When the parts are valid the previous exact-width layout is kept.
- src/lib/emu/emu-window.ts:
loadStringResource now falls back to the standard MFC framework strings in
the reserved AFX range (AFX_IDS_IDLEMESSAGE 0xE001 = "Ready", etc.) when the
app's own string table lacks the id. MFC apps that link the shared MFC42.DLL
load these by id; our MFC42 stub carries no string table, so pane 0's idle
"Ready" had no source. The app's own resources still take precedence.
Verified in-browser: PabloDraw's status bar now shows the cursor position
"1,1" (was hidden). Build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e moves - src/lib/emu/emu-window.ts : beginPaint fills the class-brush/dialog background only when the update region was invalidated with bErase=TRUE (hadErase), and clips the fill to the accumulated invalidRect instead of the whole window (real BeginPaint/WM_ERASEBKGND semantics). The 200ms caret-blink repaints (InvalidateRect with bErase=FALSE) no longer wipe content drawn during WM_ERASEBKGND, which made PabloDraw's dotted 80-column guide and NC scrollbars flash as a flickering vertical line. - src/lib/emu/emulator.ts : postMessage coalesces WM_MOUSEMOVE into the trailing queued entry, matching Windows (the queue only ever holds the latest mouse position). Fast DOM pointermove streams no longer flood the queue faster than the emulated wndProc drains it, so clicks queued behind them respond immediately instead of seconds late. - tests/test-pablo-toolbar-click.mjs : replace the broken canvas mocks (fixed 4-byte getImageData buffer, incomplete OffscreenCanvas) with the size-correct diag-style mocks; the WM_COMMAND chain it verifies works and the test now PASSes. - tests/diag-pablo-paintloop.mjs : new — counts idle DispatchMessage traffic to detect paint feedback loops. - tests/diag-pablo-paintcost.mjs : new — dumps invalidRect size and wall-time per idle dispatch (blink repaint cost). - tests/diag-pablo-tbhit.mjs : new — traces the toolbar click WM_COMMAND chain end-to-end. - tests/diag-mousemove-coalesce.mjs : new — unit test for WM_MOUSEMOVE coalescing and ordering across button messages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nvas Pixel-level browser tracing (full-row canvas diff + fillRect trap) showed the "flickering vertical line" between PabloDraw's editor and preview pane: the editor's NC scrollbar (drawn at its right edge, x 920-936) and the preview dock pane (a CSizingControlBar created at (-2,-2) inside its dock bar, so it starts at x 934) both repainted the overlap columns 934-935 on every caret blink, alternating scrollbar greys with caption grey 5x per second. On real Windows the upper window in z-order simply clips the one below; our shared canvas had no inter-window clipping, so overlapping siblings fought for the same pixels. - src/lib/emu/emu-window.ts : new clipUpperSiblings(): when arming a child-window DC (translate+clip), additionally clip OUT the canvas rects of all visible windows above it in z-order — the later siblings in each ancestor's childList, including their visible descendants (children may stick out of the parent rect, e.g. MFC sizing control bars at (-2,-2) inside a dock bar). Applied at both DC arm sites (creation and re-arm). - src/lib/emu/win32/user32/paint.ts : drawNcScrollbars and drawControlBarCaption now release the child DC arm (releaseChildDC) after drawing. They previously left their save/clip armed on the SHARED canvas context, which corrupted the save-stack ordering for the next window's DC arm (pops removed the wrong save), defeating any clip correctness across windows. - tests/diag-pablo-overlays.mjs : new — dumps the window tree (class / baseClassName / wndProc / visibility) and the DOM control overlays. - tests/diag-pablo-realtime.mjs : new — pumps the message loop in real time for 6s so setInterval-backed Win32 timers fire like in the browser; reports dispatch traffic, queue depth and posted-click latency. Verified live in the browser (pixel row diff: the contested columns are now stable; the scrollbar fillRects still run but are clipped where the preview overlaps). Headless: build clean, test-pablodraw SUCCESS, test-amoeba SUCCESS, test-pablo-toolbar-click PASS, render-png pixel output unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… WM_ERASEBKGND PabloDraw appeared frozen after b017dad (no caret, clicks without visible effect, only DOM menus alive). Pixel tracing showed two stacked bugs in the synthesized-paint pipeline: 1. synthesizePaint returned WM_ERASEBKGND with wParam=getWindowDC(hwnd), arming a save+clip on the shared canvas that nothing ever released (apps never ReleaseDC a WM_ERASEBKGND wParam). The stale clips intersected every later window's DC arm until painting was fully suppressed. Real Windows never queue-delivers WM_ERASEBKGND — BeginPaint sends it with the paint DC (already implemented since 2a55b0f) and EndPaint releases it. 2. With the erase synthesis removed, a second bug surfaced: synthesizePaint cleared needsPaint on synthesis, so MFC's CWinThread::Run idle loop ate the paint with PeekMessage(PM_NOREMOVE) and the subsequent GetMessage found nothing — WM_PAINT lost every cycle. (Before, the doomed erase message absorbed the PM_NOREMOVE peek and accidentally shielded WM_PAINT, while leaking one DC arm per blink — bug 1 masking bug 2.) Now WM_PAINT follows the real Windows rule: it stays pending until the region is validated (BeginPaint / ValidateRect), so non-removing peeks can't lose it. To avoid infinite re-delivery for wndProcs that never validate, a paint is handed to the app at most once per invalidation (_paintSynthesized flag) and force-validated if it comes back unconsumed; DispatchMessageA also validates after a WM_PAINT dispatch that didn't BeginPaint. - src/lib/emu/win32/user32/message.ts : synthesizePaint no longer emits WM_ERASEBKGND (beginPaint dispatches it); no longer clears needsPaint on synthesis; takes a `consume` flag (GetMessage / PeekMessage PM_REMOVE) driving the exactly-once delivery; built-in brush-erase path now releases the child DC arm it creates; PeekMessageA checks the msg filter BEFORE synthesizing so a consuming call can't mark a paint delivered and then drop it; DispatchMessageA validates needsPaint/needsErase after WM_PAINT dispatch. - src/lib/emu/win32/user32/types.ts : WindowInfo._paintSynthesized — delivered-but-not-yet-validated marker. - src/lib/emu/win32/user32/paint.ts : ValidateRect clears _paintSynthesized. - src/lib/emu/emu-window.ts : beginPaint clears _paintSynthesized (validation point). - tests/test-pablodraw.mjs : replace broken mocks (shared ctx, fixed 4-byte getImageData, missing getTransform) with size-correct per-canvas mocks, same fix as test-pablo-toolbar-click.mjs; the old mocks stalled the boot as soon as paints were actually delivered. - tests/diag-pablo-savedepth.mjs : new — tracks ctx.save/restore balance on the shared canvas per BeginPaint, dumps call sites of stale saves. - tests/diag-pablo-inputgate.mjs : new — checks browser input gates (dialogState/messageBoxes) and samples pixel diffs every 100ms (phase- robust) at idle and after a click. - tests/diag-pablo-dcident.mjs : new — per-paint DC identity dump (main vs offscreen canvas, transform) with a fill-through-clip probe and an InvertRect effect meter. - tests/diag-pablo-clicktrace.mjs : new — dispatch traffic + full API trace around a posted click. - tests/diag-pablo-clickfreeze.mjs : new — before/after pixel diff for click and keypress through the real input path (windowFromPoint). - tests/diag-pablo-flagwatch.mjs : new — watches a window's needsPaint/needsErase flags over time with InvalidateRect logging. Verified: build clean; test-pablodraw SUCCESS (17 ticks); test-pablo- toolbar-click PASS (WM_COMMAND delivered); save/restore depth 0 at every BeginPaint with zero stale saves; idle settles into a steady caret blink and a posted click yields visible pixel changes at the click position. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…subtraction Two user-visible PabloDraw issues, both generic emulator bugs: 1. Typing did nothing. Win32 SetFocus stored the focus in a variable local to focus.ts — never in emu.focusedWindow, the field the UI layer reads to route WM_KEYDOWN/WM_CHAR — so keys always went to the main frame, which ignores them. And nothing ever made the app set focus in the first place: real Windows sends WM_SETFOCUS to a top-level window on activation (MFC's CFrameWnd::OnSetFocus then forwards focus to its active view) and WM_MOUSEACTIVATE to a clicked child (MFC's CView::OnMouseActivate re-focuses the view). Neither was emulated. 2. The editor/preview boundary flicker came back at the scrollbar arrows. clipUpperSiblings subtracted all upper-window rects in ONE evenodd path; evenodd flips per covered rect, so a region covered by TWO overlapping exclusion rects (the preview dock pane and its same-sized inner view, both pushed by pushWithDescendants) flips even-odd-even and becomes paintable again — the editor's NC scrollbar bled through the preview pane edge. Reproduced headless at the browser's exact layout (936px editor): probe pixels at the boundary alternated #404040/#d4d0c8 every 100ms. - src/lib/emu/win32/user32/focus.ts : SetFocus stores into emu.focusedWindow (shared with the UI input layer and Win16) and dispatches WM_KILLFOCUS to the loser / WM_SETFOCUS to the gainer (synchronously below wndProc depth 3, posted above), like Windows; GetFocus reads emu.focusedWindow. - src/lib/emu/emu-window.ts : promoteToMainWindow gives the promoted window focus and queue-delivers WM_SETFOCUS (activation semantics) so MFC routes focus to its view; clipUpperSiblings subtracts each upper rect with its own evenodd clip — intersection of successive clips = window minus the UNION of the rects, immune to exclusion rects overlapping each other (deduped exact repeats). - src/components/EmulatorView.tsx : post WM_MOUSEACTIVATE to the hit window before mouse button-down messages, as the real input system does. - src/lib/emu/win32/user32/wndproc.ts : DefWindowProc returns MA_ACTIVATE for WM_MOUSEACTIVATE (real DefWindowProc behavior; MFC aborts its view-focus path only on MA_NOACTIVATE*). - src/lib/emu/win32/types.ts : WM_MOUSEACTIVATE + MA_ACTIVATE constants. - tests/diag-pablo-keyfocus.mjs : new — click (with WM_MOUSEACTIVATE) must set emu.focusedWindow to the view; typing must change pixels. - tests/diag-pablo-clipland.mjs : new — browser-layout reproduction (screen 1310x814 -> editor 936 wide), probes the boundary pixels for alternation. - tests/diag-pablo-cornerflicker.mjs : new — click+type then 100ms-frame pixel bboxes at idle. Verified: build clean; test-pablodraw SUCCESS; test-pablo-toolbar-click PASS; keyfocus: focusedWindow=view at boot and after click, typing draws the glyph; clipland: boundary pixels stable for 3s (alternated every 100ms before); inputgate: idle settles to caret blink, posted click still reacts. Keyboard input confirmed working by the user in-browser. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PabloDraw's preview pane turned BTNFACE grey, with typed characters showing on black only where the inner view repainted: the preview dock pane (an MFC CSizingControlBar, WS_CLIPCHILDREN set) erases its background on every real WM_PAINT cycle, and our applyClipChildren only protected the MAIN window's children — for any other parent the erase painted straight over its child windows. The docked inner view then only restored the small caret/typed-char invalidations, leaving the rest grey. - src/lib/emu/emu-window.ts : applyClipChildren drops the hwnd === mainWindow restriction — any window with WS_CLIPCHILDREN gets its visible children's rects clipped out before the BeginPaint erase and WM_ERASEBKGND dispatch (real Windows semantics); child rects are subtracted with one evenodd clip PER RECT (successive clips intersect = area minus the union), because a single combined evenodd path flips doubly-covered regions back to paintable when child rects overlap (same trap as the clipUpperSiblings fix in c1037ee); zero-sized children are skipped. - tests/diag-pablo-previewbg.mjs : new — dumps WS_CLIPCHILDREN/CLIPSIBLINGS styles of the dock hierarchy, probes preview-interior pixels at idle, then forces main-window repaint cycles (the browser scenario that re-arms child erases) and probes again. Verified: with the old mainWindow-only behavior the forced repaint cycles turn the preview interior #d4d0c8 (the user's symptom); with the fix it stays #000000 across all cycles. Build clean; test-pablodraw SUCCESS; test-pablo-toolbar-click PASS; diag-pablo-keyfocus (focus + typing) OK; diag-pablo-clipland boundary pixels stable; boot render PNG now shows the preview pane black like the real-Windows reference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ase DC Three Win32 bugs found by auditing for the families fixed in 258bc54/c1037ee: 1. UpdateWindow dispatched WM_ERASEBKGND with wParam=getWindowDC(hwnd) — the armed save+clip was never released (same leak family as the synthesized WM_ERASEBKGND fixed in 258bc54), and real UpdateWindow never sends WM_ERASEBKGND at all: it sends only WM_PAINT, the erase belongs to BeginPaint (which dispatches it with the paint DC, released at EndPaint). 2. GetMessageA ignored its hWnd/wMsgFilterMin/Max arguments entirely (queue.shift() unconditional). A modal loop pumping a specific window (MFC CWnd::RunModalLoop pattern) stole messages posted to other windows (frame WM_TIMER/WM_COMMAND) and dispatched them out of context. 3. The synthesized WM_PAINT ignored the hWnd filter: a window-specific PeekMessage(hwnd, WM_PAINT, WM_PAINT, PM_REMOVE) could receive ANOTHER window's paint and mark it consumed for that window (lost paint). - src/lib/emu/win32/user32/create-window.ts : UpdateWindow sends only WM_PAINT (and only to a wndProc window); leaves needsErase set for BeginPaint instead of dispatching WM_ERASEBKGND with a never-released DC. - src/lib/emu/win32/user32/message.ts : new matchesHwndFilter() — real-Windows hWnd semantics: 0 = any window, -1 = thread messages (hwnd 0), else the window or any of its DESCENDANTS (GetMessage/PeekMessage retrieve messages for hWnd "or any of its children"); new findQueuedMessage() — first queue entry matching hWnd + msg range, with WM_QUIT exempt from both filters (real GetMessage returns it regardless, after draining matching messages — otherwise a filtered modal pump would hang after PostQuitMessage); GetMessageA uses both, including in its blocking path: a posted message that does NOT match the filters re-arms the wait instead of waking the caller with the wrong message; PeekMessageA hWnd matching upgraded from exact-equality to descendant-inclusive; synthesizePaint takes a filterHwnd so filtered pumps can only receive (and mark consumed) paints for their own window subtree. Verified: build clean; test-pablodraw SUCCESS; test-pablo-toolbar-click PASS; diag-pablo-keyfocus (focus chain + typing draws); diag-pablo-clipland (boundary stable); diag-pablo-inputgate (idle caret blink + click reacts); diag-pablo-previewbg (preview stays black through forced repaint cycles). test-notepad-open failure is environmental (hardcoded K:\ path), unrelated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he desktop Typing or moving the caret in an emulated app ALSO moved/activated the desktop icon selection underneath: the desktop div keeps DOM focus (it focuses itself on mount and on icon select), so every keydown targeted it and fired the desktop's onKeyDown (arrow navigation, Enter opening the selected icon, type-ahead) while EmulatorView's window-level listener forwarded the same key to the emulated app. - src/components/EmulatorView.tsx : the window root div is now focusable (tabIndex=-1, outline none) and takes DOM focus when the window is the focused app (effect on `focused`) and on pointer-down inside it (grabDomFocus, which also raises the app via onFocus). Keydown events then target the window div and never reach the desktop's handler. Form fields keep their focus: both paths skip the grab when the pointer target / active element is an INPUT, TEXTAREA or SELECT (DOM EDIT overlays, icon rename box). Clicking the desktop still returns focus to it (tabIndex=-1 divs are click-focusable, and icon selection explicitly refocuses the desktop), restoring icon keyboard navigation. Verified: build clean; test-pablodraw SUCCESS (UI-layer change, behavior needs in-browser validation). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…TTEST Clicking PabloDraw's toolbar Open button hung the app (white window); the menu File>Open worked. The toolbar button click runs MFC's CToolBar hit-testing, which walks every button calling TB_GETITEMRECT + PtInRect to find which one was clicked. Our handler returned a fixed (0,0,22,22) rect for ALL buttons, so every button except the first looked like empty toolbar space — MFC then took that as a click on the toolbar gripper and entered CDockContext's modal drag-tracking loop (SetCapture + nested message pump), which swallowed the WM_LBUTTONUP and never ran the command. The dialog never opened and the app sat in the drag loop. - src/lib/emu/win32/user32/message.ts : new toolbarItemRect(tb, index) computes a button's real client rect from the same layout walk as toolbarHitTest/renderToolbar (separators take their iBitmap width, hidden buttons take none); toolbarHitTest reuses it. TB_GETITEMRECT (wParam = button index) and TB_GETRECT (wParam = command id) now return that rect instead of a constant; out-of-range returns 0. TB_HITTEST now maps the POINT to a button index (was a stub returning -1). Verified: diag-pablo-openbtn drives the toolbar Open button through the real input path — the file-open dialog now appears, the delivered D:\test.ans is read (CreateFile/GetFileSize/ReadFile/CloseHandle, no "Cannot load file" MessageBox), the title becomes "PabloDraw - test.ans", and the canvas repaints the loaded document (13597 px) then resumes the caret blink. Build clean; test-pablodraw SUCCESS; test-pablo-toolbar-click PASS (New button WM_COMMAND still delivered). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Typing accented characters into a DOS program (e.g. QBasic) showed the wrong glyph: 'à' appeared as α, 'é' as Θ. The console keydown handler put the browser's Unicode code point straight into the BIOS keyboard buffer (`e.key.charCodeAt(0)`), so INT 16h returned 'à' as U+00E0 = 0xE0, which the CP437 text-mode font renders as α (CP437 0xE0). Real DOS keyboard drivers deliver the OEM/CP437 byte (à = 0x85, é = 0x82), which renders correctly. - src/lib/emu/cp437.ts : new unicodeToCp437(codePoint) — reverse of the CP437→Unicode table (built from it so they can't drift). ASCII is identity; accented letters / symbols map to their CP437 byte; code points with no CP437 glyph fall back to the low byte (no regression). - src/components/ConsoleView.tsx : the DOS keydown path and the Android `beforeinput` path convert the typed character through unicodeToCp437 before injectHwKey, so the BIOS buffer carries the OEM byte the CP437 font expects. Both sites are inside the `emu.isDOS` guards, so Win32 console apps are untouched. Verified: unicodeToCp437 maps à→0x85, é→0x82, è→0x8a, ç→0x87 (round-trip to the right glyph), ASCII identity, unmapped (€) falls back. Build clean. Note: PabloDraw (a Win32 CP437 art editor) still shows α for a typed 'à' — that is faithful to real Windows. It receives the ANSI WM_CHAR byte (CP1252 0xE0) and renders it with a CP437 font; accented Latin input maps to CP437 glyphs by design. Special glyphs are inserted there via the F-key charset palette, not by typing accented letters. This change is DOS-only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Checked menu items (e.g. PabloDraw's View > Preview Window) showed no checkmark. MFC sets menu checks from its ON_UPDATE_COMMAND_UI handlers during WM_INITMENUPOPUP via CCmdUI::SetCheck, which always calls CheckMenuItem(hSubMenu, position, MF_BYPOSITION | MF_CHECKED) — the position is relative to the submenu. Our CheckMenuItem updated the internal menu item correctly, but then synced the React-facing legacy array with findLegacyByPos(emu.menuItems, position), indexing the FLAT top-level array (File/Edit/View/...) by a submenu-relative position — so the wrong item (or none) got isChecked, and the checkmark never appeared. EnableMenuItem and CheckMenuRadioItem had the same MF_BYPOSITION legacy-sync bug. - src/lib/emu/win32/user32/menu.ts : CheckMenuItem / EnableMenuItem / CheckMenuRadioItem now sync the legacy (React) item by the RESOLVED internal item's command id (findLegacyById, which recurses into submenus) instead of by submenu-relative position against the top-level array. The internal-menu update path (findItemIndex, already submenu-correct) is unchanged. The no-internal-menu fallback branches are left as-is. Verified: diag-pablo-menucheck drives WM_INITMENU + WM_INITMENUPOPUP for the View submenu exactly as the React MenuBar does — MFC issues 11 CheckMenuItem calls, the Preview item's internal flag and the legacy isChecked both become true (legacy was false before the fix). Build clean; test-pablodraw SUCCESS; test-pablo-toolbar-click PASS. The MenuBar already renders a ✓ for isChecked items, so checkmarks now show. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The emulator sent WM_NCCALCSIZE with a null rect and ignored the result, so a window's custom non-client area (anything beyond the standard style borders) was lost. A docked MFC CSizingControlBar reserves edges/gripper via WM_NCCALCSIZE and positions its embedded view in the resulting (smaller) client; without honoring it, the view filled the whole bar. PabloDraw's palette/charset views were 185x50 instead of their content size 175x40, so the swatches — drawn at a fixed top offset — sat at the top of the bar with a large empty strip below (3px above / 18px below) instead of centered like real Windows (now 6px / 13px, matching objectif). This is a generic correctness gap (any window with a custom NC area), surfaced by PabloDraw. - src/lib/emu/win32/user32/types.ts : WindowInfo.ncInset — client margins reserved by the window's own WM_NCCALCSIZE handler; undefined for everything else. - src/lib/emu/win32/user32/_helpers.ts : new computeNcInset() — sends WM_NCCALCSIZE(FALSE, &RECT) with the window rect, reads back the client rect, and stores a sane shrink as ncInset. Gated to WS_CHILD windows without WS_CAPTION whose handler actually shrinks the rect (standard controls use DefWindowProc's no-op NCCALCSIZE → no inset → unchanged). Excludes classes the emulator lays out specially (ToolbarWindow32 / ReBarWindow32 / status bar) and MFC dock-bar containers, whose on-canvas geometry is computed by that special code and would shift if given a generic client inset. new clientSizeOf() returns the ncInset-aware client size (falls back to getClientSize). - src/lib/emu/win32/user32/create-window.ts : CreateWindowEx sends a real WM_NCCALCSIZE via computeNcInset (was a null send); MoveWindow / SetWindowPos recompute ncInset on size/frame change; WM_SIZE and the canvas use clientSizeOf. - src/lib/emu/win32/user32/misc.ts : EndDeferWindowPos recomputes ncInset and uses clientSizeOf for the WM_SIZE it sends to resized control bars. - src/lib/emu/win32/user32/rect.ts : GetClientRect returns the ncInset-aware client size. - src/lib/emu/emu-window.ts : getWindowOrigin offsets a child by its parent's ncInset (the parent's client origin) when present, instead of the style-based border/caption. - tests/diag-pablo-palette.mjs : new — measures the palette view size and the swatch margins within the bottom dock bar. Verified: only the 5 CSizingControlBars get an ncInset (palette/charset/ preview/chat/userlist); the toolbar, dock bars and status bar are untouched. The palette view becomes 175x40 (its real content size) and the swatches center (6/13 vs 3/18). Build clean; no new tsc errors in the touched files; test-pablodraw SUCCESS; test-pablo-toolbar-click PASS; preview stays black; keyboard typing + click still work; amoeba (standard windows + dialog) still dismisses its dialog and runs. Verified live in the browser: palette centered, editor full black, no grey strip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On real Windows, GetDC/BeginPaint return CLIENT-relative DCs; only GetWindowDC is window-relative. The emulator used the window origin for both, which was harmless while client == window for all child windows — but the new WM_NCCALCSIZE inset (ncInset) made them differ: an app's client painting/erasing landed shifted by the inset onto its own NC margin, and nothing ever painted that margin, leaving stale white lines around windows with a custom non-client area (PabloDraw's palette and charset bars). - src/lib/emu/emu-window.ts : getWindowDC takes a windowRelative flag; client DCs of an ncInset window are offset to the client origin and clipped to the client size (new dcArea helper, applied to both DC arm paths) beginPaint paints the NC margin of an ncInset window when the update region needs erasing: class-brush/BTNFACE fill of the four margin strips on a window-relative DC, then WM_NCPAINT so the app draws its own NC chrome (real BeginPaint sends WM_NCPAINT when the frame is in the update region) - src/lib/emu/emulator.ts : getWindowDC facade forwards the windowRelative flag - src/lib/emu/win32/user32/paint.ts : GetWindowDC returns a window-relative DC; GetDCEx honors DCX_WINDOW - src/lib/emu/win32/user32/misc.ts : clientOrigin (ClientToScreen/ScreenToClient/MapWindowPoints) offsets by the window's own ncInset - src/lib/emu/win32/user32/_helpers.ts : clientToScreen (MSG pt) offsets by the window's own ncInset; invalidateForResize uses the ncInset-aware client size Verified: NC margin probes go from stale/overpainted to BTNFACE in the headless render; no white lines around the palette/charset bars in the browser; swatches still centered (6/13); test-pablodraw SUCCESS; test-pablo-toolbar-click PASS; amoeba still dismisses its dialog and runs; build clean, no new tsc errors in touched files. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ExcludeClipRect was a no-op stub. Apps rely on it to PROTECT an area from subsequent painting — e.g. an MFC control bar's OnNcPaint excludes its client rect from the window DC, then fills the whole window rect so that only the non-client margin is painted. With the stub, that fill covered the embedded view too: once WM_NCPAINT started being delivered, PabloDraw's preview pane went grey under the bar's NC background fill. - src/lib/emu/win32/types.ts : region complexity constants (RGN_ERROR, NULLREGION, SIMPLEREGION, COMPLEXREGION) - src/lib/emu/win32/gdi32/dc.ts : ExcludeClipRect intersects the canvas clip with (everything minus the rect) via an evenodd clip — same mechanism as IntersectClipRect; returns COMPLEXREGION, RGN_ERROR for a bad DC, SIMPLEREGION for an empty rect Verified: preview pane is black again with the palette/charset NC margins still grey (headless render + browser); test-pablodraw SUCCESS; test-pablo-toolbar-click PASS; amoeba still dismisses its dialog; build clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three generic gaps left an MFC status bar broken (PabloDraw: no "Ready",
no NUM, panes crushed to the right edge):
1. SB_GETBORDERS fell through the status bar's generic "return 0" stub
without writing the int[3] borders array. MFC's
CStatusBar::UpdateAllPanes bases every pane position on that array,
so stack garbage went into SB_SETPARTS and the pane layout collapsed
(only the layout fallback kept anything visible).
2. SetWindowText stored wnd.title directly instead of SENDING WM_SETTEXT
like real Windows. A subclassing app never saw the message — MFC's
CStatusBar routes the frame's message text ("Ready") into pane 0 from
its WM_SETTEXT handler, so pane 0 stayed empty.
3. GetKeyState had no toggle-state bit (bit 0), so MFC's key indicator
update saw NUM/CAPS/SCRL permanently off and blanked the panes.
- src/lib/emu/win32/user32/message.ts :
SB_GETBORDERS writes {0, 2, 2} (Wine defaults)
SB_GETPARTS / SB_GETRECT / SB_GETTEXT A+W / SB_GETTEXTLENGTH A+W
implemented from the stored parts/texts
WM_SETTEXT on a status bar sets part 0 text (real control behavior)
- src/lib/emu/win32/user32/text.ts :
SetWindowTextA/W send WM_SETTEXT to the window proc (built-in windows
go through dispatchBuiltinMessage, then the default title store)
- src/lib/emu/win32/user32/wndproc.ts :
DefWindowProc WM_SETTEXT stores the title with the usual UI side
effects (title bar refresh, overlay notify, parent repaint)
- src/lib/emu/win32/user32/_helpers.ts :
new applyWindowText() — shared title-store helper
- src/lib/emu/emulator.ts :
keyToggles set — toggle-key state, NumLock on by default like a
Windows desktop
- src/lib/emu/win32/user32/input.ts :
GetKeyState / GetKeyboardState report bit 0 from keyToggles
- src/components/EmulatorView.tsx :
key events mirror the host CapsLock/NumLock/ScrollLock state via
getModifierState
Verified headless: statusParts [772,880,909,940,976,1014] (was stack
garbage), statusTexts ["Ready","1,1","","","NUM",""] matching real
Windows; test-pablodraw SUCCESS (title still set through the new
WM_SETTEXT path); test-pablo-toolbar-click PASS; amoeba unaffected;
build clean, no new tsc errors in touched files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two generic bugs surfaced by FreeCell: 1. The browser input path posted WM_MOUSEACTIVATE before EVERY button-down. Real Windows sends it only when the clicked window is not the active window (Wine win32u/message.c: `if (msg->hwnd != info.hwndActive)`) — child views still always get it (MFC's CView::OnMouseActivate refocuses a view on click), but a click straight on the active top-level window must not. FreeCell answers MA_ACTIVATEANDEAT so the click that activates the window doesn't also play a card; receiving the message on every click made it swallow every click — cards were unclickable while WM_MOUSEMOVE (the king's gaze) still worked. 2. GetWindowDC(mainWindow) returned the CLIENT canvas DC with a client origin. A window-relative DC has its origin at the WINDOW corner; FreeCell paints "Cards Left: N" INTO ITS MENU BAR (disassembly: y = SM_CYCAPTION + SM_CYSIZEFRAME + (SM_CYMENU - tmHeight)/2 = 23, window coords on a GetWindowDC), so the text landed 41 px too low, inside the play area over the foundations. The main window's NC band (caption + menu) is DOM in the emulator, so a transparent overlay canvas over the menu bar now receives that drawing at its real place; without the overlay (headless) the drawing is window-offset on the client canvas and NC-band coordinates clip out. - src/components/EmulatorView.tsx : handlePointerEvent posts WM_MOUSEACTIVATE only when the hit window is not the main window transparent NC overlay canvas (19 px, SM_CYMENU) over the MenuBar, wired to emu.ncCanvas - src/lib/emu/emulator.ts : ncCanvas field - src/lib/emu/emu-window.ts : getWindowDC(mainWindow, windowRelative) draws on the NC overlay canvas with the window origin (transform -bw, -(bw+captionH)), or falls back to the client canvas with the full window offset (armed save/restore, balanced by ReleaseDC); the menu can come from the window (hMenu) or the class (lpszMenuName) Verified headless (tests/_diag_freecell.mjs + render PNGs): card click selects/inverts the card (cyan count 0 -> ~1500; was 0 with the unconditional WM_MOUSEACTIVATE); "Cards Left" no longer painted in the play area (clipped in headless, on the menu-bar overlay in browser). PabloDraw unaffected (views are children, they keep WM_MOUSEACTIVATE; paintNcMargin uses child windows, not the main-window branch): test-pablodraw SUCCESS, test-pablo-toolbar-click PASS. Build clean, no new tsc errors in touched files. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
calc.exe creates a top-level WS_VISIBLE EDIT named "CalcMsgPumpWnd"
(a message-pump utility window) BEFORE its real Calculator dialog. The
"first parentless window with a size becomes the main window" rule
promoted that EDIT — sized by CW_USEDEFAULT to most of the desktop — so
the user got a giant empty edit window and the actual Calculator dialog
(created right after, 30 controls) could never take the slot: the
replacement path only swaps out WS_POPUP main windows.
On real Windows a top-level window of a built-in CONTROL class (EDIT,
STATIC, BUTTON, common controls...) is never an application's main
window — apps create them as utility/pump windows. Dialogs (#32770)
stay promotable: dialog-based apps are legitimate and dialog.ts
promotes them when no main window exists.
- src/lib/emu/win32/user32/create-window.ts :
isUtilityWindowClass() — top-level windows of built-in control
classes are skipped by the main-window promotion in CreateWindowExA,
CreateWindowExW and ShowWindow
Verified headless (tests/_diag_calc.mjs): the Calculator dialog 0x1005
is promoted (was the EDIT), display shows "0. ", pressing 7 then + 8
via WM_COMMAND updates the display ("7. ", "8. "). Non-regression:
test-pablodraw SUCCESS, freecell card click still selects, amoeba's
#32770 config dialog still promoted. Build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- src/components/EmulatorView.tsx:
add flushPendingPersistence() that writes pending debounced registry and
profile saves immediately on unmount, instead of clearing the timer and
dropping them
cleanup now flushes both stores (previously it only cancelled the registry
flush timer and never flushed the profile one)
register a pagehide handler (removed on cleanup) to flush on tab close/refresh
- tests/test-persist-roundtrip.mjs:
new fake-indexeddb round-trip test covering write-then-close persistence for
profiles and registry
- tests/test-sol-e2e-persist.mjs:
new two-session sol.exe test verifying game options survive a close/reopen
- tests/test-sol-scores.mjs:
new sol.exe harness tracing profile/registry traffic on init, option change
and shutdown
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # src/lib/emu/win32/gdi32/text.ts
- win32/gdi32/text.ts: TextOutW called getFontCSS(hdc), a helper that only exists in win16/gdi.ts, throwing ReferenceError on the first text render and halting every Win32 app that draws via TextOutW (sol, freecell, winmine...). Use applyFont(dc.ctx, getDCFont(emu, hdc)) like TextOutA does. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- win32/user32/message.ts: SetWindowTextW now routes through WM_SETTEXT, but handleBuiltinMessage decoded the string from the `wide` param (default false) on the shared DefDlgProc path, truncating UTF-16 titles to their first char ("Windows Task Manager" -> "W"). Prefer the _setTextUnicode flag set by SetWindowTextA/W, then `wide`, then a UTF-16 byte sniff.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- win32/user32/dialog.ts: dialog-template controls were always created visible, so controls the template marks hidden (e.g. Task Manager's spare per-CPU "CPU Usage History" graphs, revealed later via ShowWindow by processor count) rendered at their template positions and overflowed the page once it was resized to fit the tab — controls appeared far to the right, out of view. Initialize `visible` from the item's WS_VISIBLE style bit at all three control-creation sites. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A large batch of Win32 correctness work, driven by getting real MFC
applications (PabloDraw and similar) to render and behave like they do on
Windows. The changes fall into the paint/message pipeline, missing GDI and
USER32 APIs, common-control (status bar / toolbar) rendering, window
layout, and a handful of loader/runtime fixes. No app-specific hacks — each
fix implements the actual documented Win32 behaviour.
Paint & message pipeline
WM_PAINTwith real pending semantics; stop queue-synthesizingWM_ERASEBKGND, and dispatchWM_ERASEBKGNDfromBeginPaintso customOnEraseBkgndruns.GetClipBoxreports the real updateregion; erase only the invalidated region in
BeginPaint.fix overlapping-rect z-order clip subtraction; apply
WS_CLIPCHILDRENtoevery window.
WM_NCCALCSIZE(per-window custom client inset); makeGetDC/BeginPaintDCs client-relative and paint the custom NC margin.ExcludeClipRect.GetMessage/PeekMessagefilters; stopUpdateWindowleaking anerase DC; coalesce queued mouse moves; yield to the browser mid-wndProc
for long handlers.
keys stop reaching the desktop.
GDI32
StretchDIBits(fixes MFCCToolBarbitmaps),GetDIBits+GetObjectType(and fixGetDIBitsarg count).TextOutW,LPtoDP/DPtoLP,DrawFocusRect(were no-op stubs).SetMapMode/Set*Org/Set*Exton the DC; makeInvertRecttransform-aware.
PS_DASH..PS_DASHDOTDOT) inLineTo.USER32
FindWindow/FindWindowEx(A/W),EnumChildWindows,GetClassLong/SetClassLong,WindowFromDC.IsWindow/IsWindowVisibleto actually check handle state.GetCursorPos/SetCursorPos/GetMessagePos.Common controls (status bar & toolbar)
SB_SETPARTSpane layout (incl. garbage/transient values),SB_SETTEXTA, toggle-key indicators, AFX strings.ToolbarWindow32buttons from the toolbar bitmap, returnreal button rects (
TB_GETITEMRECT/TB_GETRECT/TB_HITTEST), wire buttonclicks to the parent via
WM_COMMAND, plusTB_*message stubs.DefWindowProcthrough the built-in control handler + COMCTL32DllGetVersion.Window layout & menus
CW_USEDEFAULTsize for top-level windows; open windows at ausable minimum size; seed builtin-control sizes via
WM_NCCREATE.visibility in dock layout, render non-client scroll bars from the scroll
range, draw the gripper caption of a docked sizing pane.
state by command id (not position).
Loader / runtime / misc
CreateWindowExA's returnaddress.
kernel32:PathIsDirectory/PathFileExists,SetFilePointerEx,GetFileAttributesEx(A/W),IsTextUnicode(BOM + statistics).OLEAUT32: BSTR conversions,SafeArrayDestroy,OleTranslateColor.SetTimerre-arms; flush pending registry/profilewrites on close instead of cancelling them.
Tests
Adds headless PabloDraw harnesses and window-tree/toolbar inspection
helpers used to verify the rendering fixes.