RichEdit Compatibility Task Design

Last updated: 2026-08-14.

Status: active task design for making native RichEdit usable across WordPad, installers, and other Win9x-era apps.

Remaining work snapshot

The ordered repository work that can proceed without waiting for software-GDI changes is tracked in the non-GDI work plan. That plan separates state/protocol completion from the retained visual rejoin tests.

The bounded non-OLE program is implemented and current-tip verified. The final representative gates cover all 13 menu-dialog lifecycles, advanced RTF (17/17), printing/preview (17/17), layout stress (10/10), secondary UI (9/9), selection (8/8), Undo/Find (11/11), scroll/thumb routing (11/11), clipping (11/11), and toolbar layout/color (23/23 and 13/13). Remaining work is optional or broader fidelity rather than a blocker for everyday WordPad:

Static DIB Copy/Cut/Paste remains green at 13/13. Low-level OLE data-object and storage suites remain green at 55/55 and 68/68. The data-object suite now includes caller-owned HGLOBAL, IStream, and recursive IStorage GetDataHere transfers, exact FORMATETC negotiation errors, and coexisting canonical ANSI/OEM/Unicode text beside opaque RTF. Clipboard flush creates deep durable value snapshots independent of subsequent owner mutation. Runtime-owned pUnkForRelease media also follow the HGLOBAL delegation and dual stream/storage-plus-releaser lifetime rules. The public ReleaseStgMedium API now applies those rules to DLL-private interfaces through suspended guest callbacks, validates the complete release sequence before mutation, and keeps malformed media intact. Final IDataObject and static-cache destruction now use the same guest callback bridge for every transferred medium, validating the full teardown before mutation and preserving stream/storage-before-releaser order. Mutation-time IDataObject/cache replacement and IOleCache::Uncache now retire displaced media through that bridge as well. Canonical text synthesis moves unrelated guest ownership intact and consumes guest-released fRelease inputs asynchronously. IDataObject::GetData now returns guest stream/storage media only after a real suspended guest AddRef, publishing no partial output when that method is missing. Non-transferring IDataObject and IOleCache::SetData now use the same guest AddRef bridge, retain the caller's input medium unchanged, roll the acquired reference back if mutation fails, and retire displaced guest media before return. Guest AddRef during IOleObject::GetClipboardData cache snapshots now preflights the complete multi-entry callback set, retains every DLL-private stream/storage in guest x86, and publishes only the fully owned result. CF_DIB render mirroring is already a deep HGLOBAL byte copy. InitFromData now performs the inverse operation with a detached cache: it retains all new guest media, swaps only after complete preflight, and asynchronously retires the displaced guest cache. Durable OleFlushClipboard now deep-copies DLL-private streams through the provider's real guest Clone/Seek/CopyTo methods, preserves the original logical seek position without moving the source, publishes multi-format results atomically, and retires the former owner only after publication. The real guest callback suite passes 74/74, including malformed-later-format rollback and rejection of a Clone that aliases the source seek pointer. DLL-private storage now follows the same durable transaction through guest Stat(STATFLAG_NONAME) and recursive CopyTo, preserving root CLSID/state bits and independent nested contents. The expanded guest suite passes 78/78. This closes the remaining clipboard-flush stream/storage value-lifetime gap.

The first general embedded-object persistence slice is also complete. IPersistStorage now models initialization, normal, no-scribble, and hands-off states; Save As uses a detached recursive storage copy before replacing the destination. This preserves unknown streams, nested storage, CLSID, and state bits for unavailable servers. The focused static-handler suite passes 26/26.

The handler cache now stores multiple exact format/aspect presentations behind stable connection IDs. Media are independently copied or transferred, replacement and removal are scoped to one presentation, and CF_DIB selection continues to feed the existing render path. Stable IEnumSTATDATA snapshots cover complete fields, deep target metadata, cursor movement, and independent clones. GetClipboardData now turns the full cache into an independently owned local IDataObject, while InitFromData stages and atomically imports all of that object's presentations before refreshing the CF_DIB render choice. The focused suite passes 42/42.

The static handler also has nonvisual lifecycle metadata rather than placeholder success returns: independently owned host application/object names, validated content extents with persistent dirty tracking, caller-owned user-type text, static misc flags, close state, running state, nested run locks, last-unlock-close, and contained state. The expanded focused suite passes 52/52. DLL-private client-site, live advisory, and advisory snapshot ownership now use the guest COM callback bridge. General data objects and static caches also use it for final destruction and mutation-time replacement/removal of transferred media.

Client-site fidelity is covered for both local fixtures and DLL-private guest interfaces. Assignment, repeated assignment, retrieval, replacement, dirty-close SaveObject, and final destruction have explicit reference-count assertions. Guest AddRef/Release/SaveObject methods execute as x86 callbacks through a stack-resident continuation context, so the original WAT API frame resumes without synchronous re-entry. The local suite remains 65/65 and the expanded guest callback suite passes 52/52.

Synthetic local advisory sinks now cover the corresponding collection contract: monotonic connections, independently retained sinks, targeted and failure-atomic Unadvise, stable EnumAdvise snapshots, AddRef on returned STATDATA sinks, cursor-preserving clones, and balanced destruction. The focused suite passes 65/65. DLL-private live sinks now execute real guest AddRef, Release, OnSave, and OnClose callbacks through the continuation bridge. Dirty save failure is notification-atomic, final handler release balances all references, and stable connection traversal excludes sinks added during an in-flight notification. Guest-owned EnumAdvise snapshots now add and release their own references across creation, Next, Clone, and destruction. The snapshot remains valid after live Unadvise and source-enumerator destruction; malformed guest methods fail atomically rather than being approximated with unsafe synchronous re-entry.

ASCII TLDR

Whole task:

  make native RichEdit app-useful inside wine-assembly
  without building a full RichEdit clone.

                       +----------------------+
Win app / installer -->| native RichEdit code |
                       +----------+-----------+
                                  |
             +--------------------+--------------------+
             |                    |                    |
             v                    v                    v
      USER messages          GDI rendering        stream callbacks
      focus / keys           text / erase         plain text / RTF
      mouse / caret          clips / scroll       save / load
             |                    |                    |
             +--------------------+--------------------+
                                  |
                                  v
                WordPad and installer license panes
                type, edit, select, scroll, save, redraw
Completed bounded non-OLE                    Deferred optional / breadth

+----------------------------------+          +--------------------------------+
| typing, movement, selection      |          | linked/activated OLE servers   |
| durable text + RTF clipboard     |          | general OLE/drag/drop/verbs    |
| multi-run/paragraph/table RTF    |          | arbitrary nested RTF/layout    |
| save/reopen and format roundtrip |          | exact printer/device fidelity  |
| printing, preview, pagination    |          | uncommon toolbar/ruler states  |
| large docs, resize, wrap, undo   |          | undocumented version quirks    |
| mouse, wheel, shared thumb drag  |          | broader IME/bidi/script editing |
| clipped painting and caret       |          | TOM/accessibility breadth      |
| toolbar icons/state/color menu   |          | current-tip static-OLE reopen  |
| all 13 menu dialog lifecycles    |          | non-DIB object presentations   |
| CREATE_SUSPENDED startup path    |          |                                |
+----------------------------------+          +--------------------------------+

Current baseline

WordPad passes the smallest useful RichEdit probe:

launch WordPad -> click editor -> type "hello world"
              -> WM_GETTEXT returns "hello world"
              -> Backspace, Enter, type "again"
              -> WM_GETTEXT returns "hello worl\r\nagain"
              -> Left, Left, Delete, Home, type "X", End, type "Y"
              -> WM_GETTEXT returns "hello worl\r\nXaganY"
              -> Shift+Left twice, type "Z"
              -> EM_GETSEL reports selected range, WM_GETTEXT returns "hello worl\r\nXagaZ"
              -> Ctrl+A, Ctrl+C, End, Ctrl+V
              -> WM_GETTEXT returns "hello worl\r\nXagaZhello worl\r\nXagaZ"
              -> Ctrl+A, Ctrl+X, Ctrl+V
              -> WM_GETTEXT returns restored duplicated text
              -> menu and keyboard Copy/Cut advertise CF_TEXT plus registered
                 non-OLE "Rich Text Format" data for selected text
              -> menu and keyboard Paste restore basic selected char/paragraph
                 formatting for the bounded same-session clipboard path
              -> mouse drag selects text
              -> 35-line text auto-scrolls, wheel changes first visible line
              -> standard/format toolbar rows are allocated above the editor
              -> toolbar app bitmap strips render visible color-keyed icons
              -> formatting toolbar packs combo/button items so the color
                 button remains visible in the narrow WordPad control bar
              -> first Standard toolbar button opens the New dialog
              -> formatting toolbar B/I/U buttons update selected text
              -> 100 chars of WordPad text produce native line metrics and
                 paint through clipped ExtTextOut rectangles without desktop spill
              -> simple RTF save/reopen preserves Arial Bold Italic Underline
                 24pt Blue
              -> simple RTF save/reopen preserves centered paragraph alignment
              -> visible edited text appears

The Winamp 2.95 NSIS installer also passes the bounded installer/RichEdit probe: its license RichEdit maps to the WAT edit path, streams license text, renders word-wrapped text, clips scroll output to the control rect, supports wheel/arrow/thumb/canvas scrolling, captures the license/options/folder/ installing screenshots, reaches Installing Files, extracts the expected VFS files, and exits cleanly after the final Finish command.

Related common-control scroll status: WAT-native SysTreeView32 and the bounded report-style SysListView32 subset now reuse the same shared vertical scrollbar hit/drag math as the RichEdit/Edit/ListBox paths. The ListView subset also has bounded report-column get/set, item/subitem rectangle queries, subitem hit-test output, selection/click notifications, and a pseudo-header query surface for RegEdit/installer-style report panes, including header item set/get, layout, hit-test, rect, and identity order-array queries. It also covers common query/invalidation messages used by report-pane callers: string width, find-by-text/lParam, item position, origin/view rect, spacing, update, and redraw. ListView background/text color messages round-trip and feed bounded report-mode row painting, including CLR_NONE text-background handling. Single row/column deletion keeps the bounded report state coherent, and ListView image-list handles plus per-item image/lParam metadata now round-trip. The report painter also draws in-range items from bounded bitmap-strip image lists through color-keyed TransparentBlt, without claiming full icon-mode rendering.

That means these pieces are already good enough for basic insertion:

This does not mean RichEdit is feature complete. It only proves the first native-editing path is alive.

2026-08-10 implementation progress

2026-08-03 implementation progress

2026-08-02 implementation progress

2026-08-01 implementation progress

Problem statement

Several important Win9x apps do not use the plain EDIT control for document text. They use RichEdit directly or through dialogs. WordPad is the obvious example, and installers commonly use RichEdit for license text.

The emulator already runs native RichEdit code. The remaining work is mostly the compatibility layer around it:

The browser launcher must preload the native riched20.dll plus usp10.dll for WordPad. Dynamic discoverability alone is too late for WordPad's document view initialization. HWND-less browser keyboard events are resolved to the guest focus owner at the host input boundary, matching the CLI harness and allowing the native RichEdit child to receive WM_CHAR.

The target is app-useful compatibility, not exact implementation parity with every RichEdit version.

Goals

Expanded non-OLE completion program

The 2026-08-12 expansion promotes the following former non-goals into active work. Each area needs an app-level WordPad probe, state assertions, and a screenshot where the result is visual.

1. Advanced RTF
   [x] multiple character and paragraph runs survive Open -> Save -> Open
   [x] stylesheet/font/color-table references resolve without flattening
   [x] simple tables preserve cell boundaries and visible row layout
   [x] tabs, indents, spacing, alignment, and wrapping coexist across paragraphs

2. Printing
   [x] Page Setup edits margins and returns stable PAGESETUPDLG state
   [x] Print returns a printer DC plus Win98-compatible PRINTDLG fields
   [x] StartDoc/StartPage/EndPage/EndDoc execute in the expected order
   [x] EM_FORMATRANGE paginates a multi-page document using printer metrics
   [x] Print Preview displays the preview frame and page-view surface
   [x] Print Preview advances between pages

3. Editing and layout stress
   [x] large multi-paragraph documents remain editable and scrollable
   [x] repeated narrow/wide resize recomputes layout without stale pixels
   [x] selection, replacement, and undo work across paragraph line breaks
   [x] find/replace works across multiline native RichEdit documents
   [x] RichEdit 1.0/2.0 class and message differences have bounded probes

4. WordPad UI fidelity
   [x] ruler tab interaction updates native paragraph state
   [x] toolbar checked/disabled state is covered by focused visual regressions
   [x] Paragraph, Tabs, and Date/Time dialogs route commands correctly
   [x] all 13 non-OLE menu dialogs close through real button input and restore
       the enabled WordPad frame

5. International text
   [x] Unicode input/readback covers BMP and surrogate-pair characters
   [x] browser IME start/update/commit/cancel preserves finalized UTF-16 text
   [x] bidi paragraphs have stable logical readback and visible ordering
   [x] complex-script shaping is covered by a visible WordPad regression

6. Thread fidelity
   [x] CREATE_SUSPENDED creates a non-runnable worker with suspend count one
   [x] SuspendThread/ResumeThread return previous nested suspend counts
   [x] only the final ResumeThread makes a worker runnable
   [x] invalid/exited handles return 0xFFFFFFFF
   [x] app-level WordPad startup regression covers the real ResumeThread path

Still postponed because it is OLE/object work: embedded objects, in-place activation, structured-storage object persistence, and object clipboard transfer. TOM/COM, deep accessibility, and drag/drop editing are also outside this non-OLE completion program unless one of the probes proves they are a hard dependency.

2026-08-14 OLE file-moniker activation foundation

The save-path CreateFileMoniker compatibility object is now a real generic file-moniker value rather than an object carrying the unrelated IMalloc vtable. Its 23 slots follow the inherited IUnknown / IPersist / IPersistStream / IMoniker ABI exactly. It owns its UTF-16 path, validates complete IIDs, returns CLSID_FileMoniker, allocates independent display-name buffers, compares Windows paths case-insensitively with slash equivalence, produces equality-consistent hashes, identifies MKSYS_FILEMONIKER, and frees the path on final release.

test/test-ole-moniker.js passes 32/32 through the public OLE API thunk and the generated COM vtable. It locks down deterministic composition failures, missing-file activation, unregistered IsRunning, and the interoperable file-moniker persistence record described below.

The following bind-context slice replaces the former CreateBindCtx placeholder with a real 13-slot IBindCtx. It stores all BIND_OPTS3 fields, retains duplicate bound-object references, supports individual and bulk revocation, and maintains a case-sensitive replaceable object-parameter table. Parameter lookup AddRefs its result, replacement owns the new value before releasing the displaced value, and final context destruction balances every remaining reference. EnumObjectParam returns a real seven-slot IEnumString with an independently owned key snapshot, caller-owned output strings, exact Next/Skip/Reset behavior, and independent Clone cursors. Both emulator-local and DLL-private COM values are supported; private AddRef/Release methods run through the existing suspended guest-callback continuation rather than being treated as local heap objects. test/test-ole-bind-context.js passes 33/33 through the public API and generated vtables.

The next slice replaces the no-op ROT with retained process-local records keyed by normalized file-moniker value. Registrations own their objects and monikers, use stable cookies, preserve independent duplicates, record change times, and survive release of short-lived ROT interface wrappers. EnumRunning exposes a stable IEnumMoniker snapshot with full Next/Skip/Reset/Clone semantics. File-moniker IsRunning, GetTimeOfLastChange, and BindToObject now consult that table; binding returns a caller-owned interface and also registers it with the supplied bind context. Local emulator COM objects and DLL-private objects both have balanced ownership through the guest callback bridge. test/test-ole-running-object-table.js passes 28/28.

The milestone-closing persistence slice implements the XP-compatible file-moniker stream record: ANSI path bytes, the 0xDEADFFFF marker and padding, and the tagged UTF-16 value when ANSI is lossy or the path names a directory. Loading is bounded and transactional, and both local emulator streams and DLL-private IStream::Read/Write callbacks are supported. GetSizeMax reports the conservative Windows estimate. Exact byte-layout, ANSI/Unicode round-trip, guest-callback, and malformed-input checks are part of the 32/32 moniker regression.

Composite/relative monikers, class factories, verbs, links, and drag/drop remain after these foundations. They are deferred while the next milestone returns to GDI fidelity.

In the isolated real-app gate, WordPad Save As created and wrote the file, called CreateFileMoniker, registered it through the ROT, released the ROT, updated its title, preserved editor text, and remained alive. The combined Save/New/Open regression now reports 32/32. The open operation populated 8,191 visible characters and completed without a runtime crash.

2026-08-13 dialog lifecycle slice

Dialog mouse releases now follow the owner of the modal loop rather than an application-specific close path. Native guest dialogs receive captured button releases through the queued GetMessage / DispatchMessage path, which lets MFC and common-control modal loops resume after their button procedure returns. Emulator-owned WAT modal dialogs retain synchronous dialog_route_mouse delivery through the WAT dialog manager.

test/test-wordpad-dialog-lifecycle.js builds once, then opens and closes 13 WordPad menu dialogs with actual button down/up events: New, Open, Save As, Print, Page Setup, Options, Find, Replace, Date and Time, Font, Paragraph, Tabs, and About WordPad. The regression also asserts that no dialog remains visible and that the main WordPad frame is enabled after every close. The current gate passes 13/13 with no skips.

2026-08-12 OLE storage foundation

The first post-non-OLE slice replaces the inert RichEdit clipboard-storage objects with heap-backed COM implementations. ILockBytes supports binary ReadAt/WriteAt, resizing, stat, and independent offsets. Root IStorage objects hold a persistent CLSID and case-insensitive named IStream children; streams support binary read/write/seek, sizing, commit/stat, and reference-counted storage ownership. StgCreateDocfileOnILockBytes associates the root storage with its lock-bytes object, and StgOpenStorageOnILockBytes can reopen that live in-memory root.

test/test-ole-storage.js provides direct coverage for object creation, binary round-trip including compound-file-signature bytes, EOF semantics, case-insensitive stream lookup, CLSID persistence, and COM lifetime ownership. The stream-core follow-up adds IStream::Clone: clones share mutable backing bytes and size, retain independent cursors, propagate SetSize/write changes, zero-fill data exposed after truncation, and retain the canonical root after the original interface and owning storage are released. HGLOBAL-backed clones also preserve the same backing handle. The storage-tree follow-up adds nested create/open with distinct child/sibling links, case-insensitive lookup, a shared stream/storage element namespace, and retained-subtree lifetime. Rename and delete cover both element types, cross-type collisions, missing-element errors, and retained-interface validity after unlinking. The focused suite now passes 33/33. Deep mixed-tree CopyTo, identity-preserving MoveElementTo, cycle and collision rejection, independent copied bytes, and STGMOVE_COPY raise it to 41/41. A generated seven-method IEnumSTATSTG vtable plus stable snapshot records, partial Next counts, Skip, Reset, and independent cloned cursors raise it to 47/47; the snapshot remains valid after the live tree is released. Atomic deep Commit/Revert checkpoints restore mixed trees, bytes, names, and CLSIDs without invalidating retained pre-revert interfaces, raising the suite to 53/53. Completing IStream adds clone-shared byte checkpoints, owner-scoped region locks, lock-aware resize, buffered/self-safe CopyTo, and exact partial-count behavior, raising the combined suite to 64/64. The bounded storage metadata pass then raises it to 68/68: all three storage interfaces share correct 72-byte STATSTG records, including name ownership, STATFLAG_NONAME, 64-bit size fields, stream lock capabilities, storage CLSID, and masked state bits. Enumeration, deep copy, and commit/revert preserve the same metadata. The persistence slice adds a real CFB v3 writer and defensive reader. It emits DIFAT/FAT, directory sectors with name-ordered red-black sibling trees, mini-FAT and the root mini-stream, nested storage metadata, and exact regular/mini stream bytes. Public IStorage::Commit emits those bytes, and fresh-backed StgOpenStorageOnILockBytes reconstructs a distinct tree only after complete validation. The deterministic/round-trip/malformed-input suite passes 22/22. At this foundation stage it did not serialize a Compound File Binary container, expose an IDataObject, insert a REOBJECT, or render/activate an OLE server. Later sections document the completed bounded IDataObject and static-DIB integration; compound-file serialization and general server activation remain open.

2026-08-12 OLE data-transfer foundation

A bounded multi-format IDataObject now owns a growable collection of deep-copied FORMATETC plus STGMEDIUM entries. It supports QueryGetData, GetData, append/replace SetData, fRelease, independent target-device data, and stable multi-entry IEnumFORMATETC snapshots with complete cursor and clone methods. Advisory connections remain explicitly unsupported. TYMED_HGLOBAL payloads, including opaque CF_DIB bytes, are duplicated for each receiver. TYMED_ISTREAM/TYMED_ISTORAGE retain COM references, and ReleaseStgMedium now releases supported media and clears the structure. OleSetClipboard, OleGetClipboard, OleFlushClipboard, and OleIsCurrentClipboard provide bounded reference-counted clipboard ownership.

test/test-ole-data-object.js covers format acceptance/rejection, independent HGLOBAL copies, binary DIB preservation, medium release, OLE clipboard lifetime, and stream-medium AddRef/Release behavior. The following sections document the now-implemented WordPad-visible static object representation: bridge DIB clipboard data into a RichEdit object slot, paint it inline, and persist that object through RTF/storage save and reopen.

The first native integration probe now publishes a valid 32x24 24-bpp CF_DIB and delivers WM_PASTE to Win98 RichEdit. This exposed and then closed dynamic export crash paths including: CoDisconnectObject, CreateStreamOnHGlobal (with HGLOBAL recovery), and OleCreateDefaultHandler and OleSetContainedObject. A bounded in-process static handler now exposes shared IOleObject/IPersistStorage identity, client-site/extent state, and storage ownership. RichEdit inserts and paints the static DIB without exiting WordPad; WM_GETTEXT preserves the surrounding text and represents its inline object position as a space. test/test-ole-storage.js covers caller- owned and COM-created HGLOBAL stream lifetime, test/test-ole-static-handler.js covers handler identity/refcounts/storage ownership, and the browser smoke covers native CF_DIB insertion plus visible red/blue presentation pixels.

The persistence follow-up is complete for this same static-DIB scope. RichEdit requires IOleCache to retain the streamed RTF presentation, IViewObject2 for extent negotiation, and OleDraw to paint it; the bounded handler now owns the copied STGMEDIUM and exposes those contracts. OleUIUpdateLinksA returns the successful no-links result for static pictures. The focused test/test-wordpad-ole-roundtrip.js regression pastes a 32x24 checker DIB, copies/pastes it into a second inline object, saves/exports two complete native \pict\dibitmap0 RTF presentations, imports them into a fresh WordPad, and verifies both restored U+FFFC positions plus red/blue pixels within each presentation independently (17/17 checks). USER GetClipboardData handles are borrowed, and RichEdit retains a static object's presentation after a later clipboard change, so CF_DIB snapshots remain valid for the WASM instance lifetime rather than entering the reusable heap free list. This does not claim linked objects, executable OLE servers, in-place activation, drag/drop, or arbitrary compound-file fidelity.

Current-tip audit note (2026-08-14): the same gate now reaches both native objects but reports 5/17 because its scheduled Save As command does not open a dialog after the second object is pasted; export/reopen therefore never run. This reproduces in a clean HEAD-plus-file-moniker-persistence overlay before any moniker persistence method is called. The dialog/command regression remains an explicit WordPad follow-up when work returns from the GDI fidelity milestone; the historical 17/17 description above records the coverage the gate is meant to restore, not the current result.

The static-image clipboard follow-up is also complete. WordPad Edit Copy and Cut preserve the eager CF_DIB presentation of a selected inline picture; Paste reconstructs a new RichEdit object and Cut removes the original with WM_CLEAR. RichEdit's DLL-private clipboard IDataObject is deliberately treated as borrowed: native WM_COPY may create it and exercise structured storage, but the emulator clears that pointer after capturing durable CF_TEXT/RTF/DIB values. This prevents a later Clear/Cut from leaving the clipboard pointed at an object whose source control has destroyed it. test/test-wordpad-ole-clipboard.js covers Copy/Paste duplication, Cut/Paste restoration, two native U+FFFC object positions, and two visible red/blue presentations (13/13 checks). Object Copy advertises only CF_DIB, because including RichEdit's ANSI/RTF one-space projection makes this Win98 build prefer placeholder text over the picture during Paste. The selection test uses EM_GETTEXTEX/GT_SELECTION: U+FFFC selects the DIB path, while a genuine space remains text. test/test-wordpad-ole-space-copy.js verifies that ordinary one-space Copy clears the stale DIB format and pastes beside the original object without converting to a picture (5/5 checks).

test/test-wordpad-ole-keyboard-undo.js covers the renderer's Ctrl+C/X/V exports for the same inline object and WordPad's native Ctrl+Z undo path. It verifies that Undo restores both the RichEdit object position and visible presentation pixels after an object Cut (10/10 checks).

Static-object deletion and survivor persistence are covered as well. test/test-wordpad-ole-delete-roundtrip.js creates two inline DIB objects, deletes only the selected first object through WordPad's Edit Clear command, saves exactly one remaining \pict group, and opens that file in a fresh WordPad process. The remaining native U+FFFC position and its red/blue presentation survive independently (8/8 checks).

2026-08-12 advanced RTF slice

test/test-wordpad-advanced-rtf.js imports a handcrafted Win98-era RTF through the VFS, opens it through WordPad's real File Open path, saves it under a new name, reopens it, and verifies range-specific character/paragraph state. The fixture covers an inherited stylesheet heading, two fonts, two colors, mixed inline bold, multiple paragraph alignments, a two-cell table with an italic cell, and a trailing paragraph. Native RichEdit already preserves the direct run/table model. A bounded VFS read projection expands \sN stylesheet inheritance to equivalent direct controls, including \plain at style boundaries, while leaving stored bytes unchanged. The 17-point acceptance matrix passes against the saved artifact and screenshot test/output/wordpad-richedit/advanced-rtf.png.

2026-08-12 printing slice

WordPad now receives a stable default Web Printer through PrintDlgA, with Letter paper, portrait orientation, page range/copy fields, a canvas-backed printer DC, 300-DPI metrics, and 0.25-inch physical offsets. The interactive Print form exposes page range and copies; Page Setup exposes four editable inch margins and commits them to PAGESETUPDLG. The accepted forms were visually verified in /private/tmp/wordpad-print-dialog.png and /private/tmp/wordpad-page-setup.png.

The GDI print lifecycle now validates StartDoc -> StartPage -> EndPage -> EndDoc, and EM_FORMATRANGE returns a bounded next-character index derived from the requested twip rectangle instead of leaking the native structure pointer. A render request first enters the native RichEdit window procedure so the document is drawn into the supplied preview/printer DC; only the unreliable return value is replaced afterward. Print Preview reaches WordPad's real preview mode: it hides the edit view and normal bars, creates the preview toolbar, and installs an AfxFrameOrView42 page surface. Preview also drove implementation of the DC state queries GetBkMode, Get/SetPolyFillMode, and GetStretchBltMode.

The printing acceptance matrix is now complete. MFC's abort procedure exposed a queue-fidelity bug: PeekMessage(PM_NOREMOVE) could expose a native-control paint which GetMessage then retired internally and blocked behind. Native control paints are now retired before both peek variants select an observable message. The physical route reaches EndDoc, destroys its progress dialog, returns the printer state to idle, and re-enables the WordPad frame.

test/test-wordpad-printing.js also opens a 9,673-character fixture and proves monotonic EM_FORMATRANGE page boundaries through EOF. Print Preview's Next command now renders through legacy Win9x Escape printer queries (GETPHYSPAGESIZE, GETPRINTINGOFFSET, and GETSCALINGFACTOR) without a fatal stub. The expanded 17-point regression waits for the real preview rather than accepting a transitional editor frame. It proves Page 1 -> Page 2 status, captures both pages, requires a large white page area plus bounded document-ink density, and requires the page interiors to differ. The upper ink bound guards the anisotropic-font regression where mapped line origins shrank correctly but an 8px creation-time font floor made glyphs overlap into a black block. Mapped Canvas glyphs now use their valid device height while logical CreateFont normalization retains that floor.

2026-08-12 editing/layout stress slice

test/test-wordpad-layout-stress.js opens an 80-paragraph, 10,158-character plain-text document through WordPad's real File Open path. It proves dynamic full-buffer WM_GETTEXT readback beyond the old 8,191-byte test ceiling, downward wheel scrolling, guest WM_SIZE relayout at 320x240 and 640x480, replacement across a CRLF paragraph boundary, and complete native Undo. The test writes and visually verifies narrow/wide screenshots under test/output/wordpad-richedit/. The reusable main-resize harness action commits top-level geometry and delivers the same resize messages as an interactive frame drag.

2026-08-12 ruler and secondary UI slice

test/test-wordpad-ui-advanced.js drives WordPad's real ruler and three less-used dialogs in bounded, isolated emulator runs. Dragging on the ruler changes the focused native RichEdit paragraph from zero tab stops to one 1278-twip stop. The Paragraph dialog exposes left/right/first-line indentation and alignment; the Tabs dialog exposes Set, Clear, and Clear All; and Date and Time enumerates stable short-date, long-date, and time choices. Selecting the time entry and accepting the dialog inserts 12:00:00 AM into the document. Locale-format enumeration, resource-language enumeration, and dialog tab-order traversal now cover the APIs needed by these MFC surfaces. The 9-point regression writes paragraph-dialog.png, tabs-dialog.png, and date-time-dialog.png under test/output/wordpad-richedit/.

2026-08-12 international text slice

test/test-wordpad-international.js inserts Greek, CJK, Hebrew, Arabic, Devanagari, and a supplementary-plane emoji into WordPad's native RichEdit20A, then reads the buffer through EM_GETTEXTEX code page 1200. The browser composition bridge tracks start/update/end, inserts finalized UTF-16 code units exactly once, and treats an empty result as cancellation. The browser continues to own pre-edit and candidate UI: dispatching WM_IME_STARTCOMPOSITION to this Win98 RichEdit without a guest IMM module corrupts its native text state, so guest-owned underlined pre-edit/candidate rendering is intentionally outside the bounded bridge. Logical RTL order and complex-script/RTL rendering are captured in test/output/wordpad-richedit/international-text.png. The nine-point regression passes, including surrogate-pair, commit-once, and cancellation assertions.

2026-08-12 RichEdit version-compatibility slice

The WAT fallback now distinguishes the Win98 RICHEDIT class (Riched32 / RichEdit 1.0) from RichEdit20A and RichEdit20W (Riched20 / RichEdit 2.0+) without changing WordPad's native RichEdit20A execution path. Both fallback generations share the edit-control-compatible selection surface and begin with the documented 32,767-character input limit. A zero legacy EM_LIMITTEXT selects the 64,000-character RichEdit compatibility limit. The 2.0 fallback additionally supports full-width EM_EXGETSEL / EM_EXSETSEL CHARRANGE fields and EM_EXLIMITTEXT values above 64K; those extended messages remain unsupported on the 1.0 fallback.

test/test-richedit-version-compat.js passes 11/11 across exact and case-insensitive class recognition, shared versus version-gated selection messages, and the 32K/64K/greater-than-64K limit transitions. The real WordPad editing regression remains green at 23/23. Exhaustive undocumented DLL-version quirks are not part of the non-OLE compatibility target.

2026-08-14 browser toolbar revalidation

The browser WordPad gate proves that the native riched20.dll is loaded, the process remains live after typing hello world, the default size combobox contains and visibly paints 10, the menu selects the W95FA Win98 UI font, and the complete formatting-button run survives the final MFC layout. The captured button band contains 1,306 non-BTNFACE detail pixels without a forced WM_PAINT; all 14 stored buttons render from the valid 128x16, 32-bpp application bitmap strip.

The resolved defect was generic USER/control paint ordering, not missing WordPad resources. Visible DeferWindowPos operations now create an update region as well as a paint bit while preserving SWP_NOREDRAW and hidden-window suppression. Parent updates propagate recursively before consumption, native children wait behind dirty ancestors, and immediate exposure draws retain one queued final compositor paint so later non-client or ancestor work cannot erase the controls. ToolbarWindow32 layout/state changes likewise queue USER invalidation instead of clearing paint state after a premature direct draw. Companion coverage keeps CBS_DROPDOWN text synchronized with its inner EDIT and installs a fresh client clip for native toolbar painting.

Suggested implementation slice

Implement the next part as a bounded probe plus the first failing edit fixes. This gives a stable loop before touching more RichEdit internals.

Part A: test harness

  test/test-wordpad-richedit.js
       |
       v
  test/run.js --exe=test/binaries/win98-apps/wordpad.exe
       |
       v
  scheduled input:
    click editor
    type text
    Backspace / Delete / Enter
    Arrow / Home / End
    Shift+arrow
    drag selection
    png snapshots
    dump visible edit state
       |
       v
  assertions:
    text state changed correctly
    selection/caret/scroll state sane
    screenshots written
    no crash / no UNIMPLEMENTED API

Deliverables for Part A

Existing test-runner pieces to reuse

test/run.js already supports most of the required input actions:

The first version should reuse these. If WordPad needs better targeting, add a generic selector action instead of a WordPad-specific hack. Example:

focus-visible-edit
dump-visible-edit-state[:LABEL]
drag-visible-edit:X1:Y1:X2:Y2

The selector should find the visible native edit/RichEdit child under the active top-level window, not hard-code 0x10002.

Initial probe flow

1. Launch WordPad.
2. Click inside the RichEdit client area.
3. Type: alpha beta
4. Dump state: text should be "alpha beta".
5. Press Backspace.
6. Dump state: text should be "alpha bet".
7. Press Enter, type gamma.
8. Dump state: multiline text should include alpha bet + gamma.
9. Ctrl+A, type delta.
10. Dump state: text should be "delta".
11. Shift+Left a few chars.
12. Dump state: cursor and selection should differ.
13. Capture screenshot for text/caret/selection evidence.
14. Run no-crash/no-unimplemented checks.

Command shape:

node test/test-wordpad-richedit.js

The test itself should call test/run.js with a bounded child-process timeout, for example execSync(cmd, { timeout: 120000 }).

Fixes likely needed after the probe

1. Delete, Enter, and navigation

Expected touchpoints:

Acceptance:

[x] Backspace removes the previous character
[x] Delete removes the next character
[x] Enter creates a visible new line
[x] Left/Right/Home/End move the caret without corrupting text
[x] typing over selection replaces the selected range

2. Visible selection

Expected touchpoints:

Acceptance:

[x] Shift+arrow produces a non-empty selection
[x] mouse drag produces a non-empty selection
[x] selected text is visibly highlighted in a screenshot
[x] replacing selected text leaves the expected buffer contents

3. Scroll and wrapping

Expected touchpoints:

Acceptance:

[x] long multiline text inserts without truncation
[x] wheel scroll changes first visible line
[x] long WordPad RichEdit text paints through clipped ExtTextOut rectangles
[x] glyph drawing stays inside the tested RichEdit/outer-window paint band
[x] scrollbar thumb drag changes first visible line
[x] bounded wrapping + clip invalidation stays coherent across resize/scroll
    edges for the WAT edit/RichEdit-compatible path

4. Text I/O

Expected message surface:

Acceptance:

[x] WordPad saved RTF reopens with simple plain text content
[x] plain text save/reopen through the text filter works in WordPad
[x] basic RTF save/reopen preserves bold/italic/underline charformat
[x] basic RTF save/reopen preserves one selected run's font face/size/color
[x] basic RTF save/reopen preserves one selected-size run against surrounding
    default-size text and reports the reopened whole selection as mixed
[x] basic RTF save emits selected paragraph center alignment and reopens text
[x] focused PARAFORMAT2 numbering/indents/tabs read back through RichEdit bridge
[x] basic RTF save/reopen preserves paragraph numbering/indents/tabs
[x] installer license RichEdit text streams in and scrolls

5. Clipboard shortcuts and non-OLE RTF data

Expected message surface:

Acceptance:

[x] Ctrl+A selects all focused native RichEdit text
[x] Ctrl+C captures the selected native RichEdit text as plain text
[x] Ctrl+V inserts the captured plain text through `EM_REPLACESEL`
[x] Ctrl+X cuts selected native RichEdit text
[x] menu Edit Select All/Copy/Cut/Paste routes work without the keyboard bridge
[x] menu Copy/Paste preserves basic selected RichEdit char/paragraph formatting
    with CRLF and ANSI high-byte text
[x] menu Copy advertises CF_TEXT plus registered non-OLE RTF clipboard data
[x] keyboard Ctrl+C/Ctrl+X/Ctrl+V preserve basic selected RichEdit
    char/paragraph formatting with CRLF and ANSI high-byte text
[x] keyboard Copy advertises CF_TEXT plus registered non-OLE RTF clipboard data
[x] static CF_DIB embedded-object Copy/Cut/Paste preserves object presentation
[ ] linked/activated and arbitrary non-DIB clipboard objects preserve fidelity

6. Basic formatting

Expected message surface:

Acceptance:

[x] WordPad Ctrl+B / Ctrl+I / Ctrl+U toggle RichEdit charformat effects
[x] bold / italic / underline have explicit visual/pixel assertions
[x] WordPad Format > Font dialog writes selected face/style/point size back to
    WordPad's `EM_SETCHARFORMAT`
[x] WordPad Font dialog face/style application has visible/pixel assertions
[x] WordPad Font dialog 24pt selection visibly increases text height
[x] `EM_GETCHARFORMAT` reports concrete selected size instead of the sentinel
[x] mixed-size selections retain RichEdit's mixed sentinel instead of being
    overwritten by the latest explicit selected-size fallback
[x] text color renders through direct focused RichEdit `EM_SETCHARFORMAT`
[x] WordPad standard/format toolbar rows are allocated and layout RichEdit below them
[x] WordPad toolbar app bitmap strips render visible color-keyed icon pixels
[x] ToolbarWindow32 can load minimal `HINST_COMMCTRL` built-in color strips
[x] WordPad disabled Standard-toolbar commands are state-dumped and visibly dimmed
[x] WordPad checked formatting-toolbar buttons are visibly sunken
[x] WordPad toolbar fallback buttons remain visibly composited through nested
    MFC control-bar containers when no strip is available
[x] WordPad formatting toolbar font/size comboboxes are visible, separated, and populated
[x] WordPad first Standard toolbar button opens the New dialog through app UI
[x] WordPad formatting toolbar B/I/U buttons route through app UI
[x] WordPad toolbar/menu color command route applies Blue through app UI
[x] simple RTF round-trips without losing basic character-format effects
[x] simple RTF round-trips one selected run's font size/color
[x] simple RTF round-trips one selected-size run against default-size text
[x] simple RTF stream-out records selected paragraph center alignment
[x] focused paragraph indents/tabs/numbering read back through RichEdit bridge
[x] paragraph indents/tabs/numbering RTF round-trip correctly

Whole-task acceptance matrix

[x] WordPad accepts focus and inserts visible "hello world"
[x] Browser WordPad preloads native RichEdit, stays running, and inserts
    visible "hello world" through focus-routed browser keyboard events
[x] Automated WordPad/RichEdit probe exists
[x] Backspace edits visible text correctly
[x] Delete-forward edits visible text correctly
[x] Enter creates a visible new line
[x] Arrow/Home/End movement tracks insertion position
[x] Visible caret paint is covered
[x] Caret blink/XOR cadence stays coherent
[x] Shift+arrow selection changes replacement range
[x] Visible selection highlight renders coherently
[x] Mouse-drag selection changes selection range
[x] Plain-text Ctrl+A/C/X/V work for native RichEdit focus
[x] Menu Copy/Cut/Paste has explicit coverage
[x] Native RichEdit Undo works through WordPad's Ctrl+Z accelerator
[x] WordPad Edit > Find opens the modeless common dialog
[x] Find Next selects the matching native RichEdit range while the dialog
    retains focus
[x] FINDMSGSTRING registration and modeless notification routing are stable
[x] WordPad Replace / Replace All common-dialog paths are covered
[x] ReplaceTextA resolves dynamically and exposes the modeless Win98 control set
[x] Single Replace advances to the next native RichEdit match
[x] Replace All and Match Case behavior are asserted with mixed-case text
[x] Native RichEdit wheel changes first visible line
[x] Long WordPad RichEdit text paints through clipped ExtTextOut rectangles
[x] Native RichEdit scrollbar thumb drag changes first visible line
[x] Bounded wrapping/clip invalidation stays coherent across resize/scroll
    edges for the WAT edit/RichEdit-compatible path
[x] WordPad saved RTF reopens with simple plain text content
[x] Plain text save/reopen through the text filter works
[x] Basic RTF save/reopen preserves bold/italic/underline styling state
[x] Basic RTF save/reopen preserves selected font size/color state
[x] Basic RTF save/reopen preserves one selected-size run against default text
[x] Basic RTF save emits selected paragraph alignment state and reopens text
[x] Focused RichEdit PARAFORMAT2 fields read back for numbering/indents/tabs
[x] Basic paragraph numbering/indents/tabs round-trip through WordPad RTF
[x] WordPad menu Copy/Paste preserves basic selected RichEdit formatting
    without byte-counting CRLF positions
[x] WordPad menu Copy advertises registered non-OLE RTF clipboard data
[x] WordPad keyboard Copy/Cut/Paste preserves basic selected RichEdit
    formatting and registered non-OLE RTF clipboard data
[x] Static CF_DIB clipboard insertion, rendering, RTF save, and fresh reopen
    preserve the inline object and presentation
[x] Static CF_DIB object Copy/Cut/Paste preserves object count and presentation
[x] Multiple static CF_DIB objects save and reopen with independent complete
    presentations after clipboard replacement
[ ] Current-tip bounded fresh-process revalidation completes after recent
    GDI/DIB changes (save output is complete; reopen assertions time out)
[x] Deleting one of multiple static CF_DIB objects preserves the survivor
    through RTF save and fresh-process reopen
[ ] Linked/activated and arbitrary non-DIB OLE objects preserve full fidelity
[x] Bold/italic/underline command state toggles in WordPad
[x] Bold/italic/underline are visibly asserted in WordPad
[x] Font dialog face/style handoff is visibly asserted in WordPad
[x] Font-size layout is visibly asserted in WordPad
[x] RichEdit selected-size reporting returns concrete `yHeight`
[x] Direct RichEdit text color rendering is visibly asserted in WordPad
[x] WordPad standard/format toolbar row layout is asserted
[x] WordPad nested toolbar child surfaces visibly composite and clip to WordPad
[x] WordPad toolbar bitmap icon pixels are explicitly asserted
[x] ToolbarWindow32 `TB_ADDBITMAP` covers `HINST_COMMCTRL` built-in strips
[x] WordPad disabled toolbar icon dimming is explicitly asserted
[x] WordPad checked toolbar button visual state is explicitly asserted
[x] WordPad formatting toolbar combo fields are visibly asserted
[x] WordPad formatting toolbar font/size combo text is populated and asserted
[x] Empty-document font-size toolbar pixels and `WM_GETTEXT` report 10pt
    instead of rendering the native mixed-size sentinel as `1638.5`
[x] WordPad formatting toolbar full button run is visible in the narrow row
[x] Browser final-layout repaint naturally shows the formatting button run
    without a forced `WM_PAINT` (generic USER parent/child paint ordering)
[x] ToolbarWindow32 `TB_INSERTBUTTONA` preserves stored TBBUTTON order
[x] ToolbarWindow32 bounded query/style/image-list/padding/delete messages are asserted
[x] WordPad first Standard toolbar command route is explicitly covered
[x] WordPad formatting toolbar B/I/U click route is explicitly covered
[x] WordPad toolbar/menu color route has explicit coverage
[x] Installer/license RichEdit panes render and scroll
[x] WAT TreeView reuses shared vertical scrollbar hit/drag math
[x] WAT TreeView parent/child links and expand/collapse visibility are asserted
[x] WAT TreeView depth-first visible order is asserted
[x] WAT TreeView `TVM_GETITEMA` text buffer copying is asserted
[x] WAT TreeView expand/collapse notifications and `TVIF_CHILDREN` are asserted
[x] Registry root/subkey/value enumeration and metadata are covered for RegEdit-style probes
[x] SysListView32 has bounded report item/header state and reusable scrollbar behavior
[x] SysListView32 report column get/set, item/subitem rects, and subitem hit-test are asserted
[x] SysListView32 selection/click notifications are asserted
[x] SysListView32 pseudo-header query messages are asserted
[x] SysListView32 pseudo-header set/layout/identity-order messages are asserted
[x] SysListView32 row/column deletion is asserted
[x] SysListView32 image-list handles, LVIF_IMAGE/LVIF_PARAM metadata, and bounded report-mode image painting are asserted
[x] SysListView32 report query/invalidation messages are asserted
[x] SysListView32 background/text color messages and paint use are asserted
[ ] Advanced ListView modes/notifications/header fidelity are implemented
[x] App status docs are updated from current screenshots/probes

Implementation order

1. Add `test/test-wordpad-richedit.js` and capture baseline screenshots.
2. Add generic test-runner targeting if existing edit-state actions are not
   enough for WordPad.
3. Fix Backspace, Delete, Enter, and caret navigation.
4. Fix visible selection rendering and replacement.
5. Add plain-text keyboard clipboard bridge for focused native RichEdit.
6. Add mouse-selection and focused-wheel scroll coverage.
7. Fix clipped RichEdit text rendering.
8. Add plain text stream in/out.
9. Add basic RTF stream in/out.
10. Add basic character and paragraph formatting.
11. Fix bounded wrapping/clip invalidation resize edges.
12. Re-run WordPad, Notepad, and installer RichEdit probes.
13. Update app status docs with screenshots and pass/fail state.

Risk controls