diff --git a/docs/features/wx-parity-fixes/README.md b/docs/features/wx-parity-fixes/README.md new file mode 100644 index 000000000..a579ac0e4 --- /dev/null +++ b/docs/features/wx-parity-fixes/README.md @@ -0,0 +1,82 @@ +# wxWidgets DOM-port ↔ native parity fixes (`wxwidgets-diff`) + +Six defects where the wxWidgets **WASM DOM port** diverges from native +wxWidgets (wxGTK), each found in a parity audit, reproduced with a failing +test, and fixed in the **wasm layer only** — no wxWidgets core file and no +KiCad source was changed. + +The common root cause is the DOM port's two-sided design (see +[`../wx-dom-port/README.md`](../wx-dom-port/README.md)): every control is a real +HTML element mirrored from a C++ state cache. Most of these bugs are the two +sides drifting apart, or the port skipping something native wxWidgets does. + +Status: **all six fixed and green** (2026-06-30), plus a **seventh fix, H-1**, +implemented and KiCad-verified (2026-07-03). **H-5**, **C-1**, and **H-1** are +additionally verified through real KiCad pcbnew dialogs (the Plot dialog, the +Track & Via Properties dialog, and the Page Settings dialog). Work lives on branch +`wxwidgets-diff` (root + wxwidgets submodule); not merged to `main`. + +## The fixes + +| # | Sev | Bug (native behavior → DOM-port bug) | Fix location | +|---|-----|---|---| +| **C-1** | Critical | `wxEVT_CHOICE`/`LISTBOX`/`COMBOBOX` selection events never attach per-item client data → `event.GetClientData()` is always NULL → KiCad's Track & Via Properties dialog dereferences NULL on a normal selection | `src/wasm/{choice,listbox,combobox}.cpp` — call `InitCommandEventWithItems(event, n)` | +| **H-4** | High | `wxTextCtrl::Clear()`/`Remove()` update only the C++ cache; the `` keeps the old text (next keystroke resurrects it) | `src/wasm/textctrl.{h,cpp}` — override `Remove()` to push to the DOM (covers `Clear()`) | +| **H-5** | High | `wxCheckListBox` check marks wiped on every item rebuild — an `Append`-then-`Check` loop loses every check but the last | `include/wx/wasm/listbox.h`, `src/wasm/checklst.{h,cpp}` — virtual `WasmSyncSelection`, override to re-apply `m_itemsChecked` | +| **H-6** | High | `wxSlider` fires only `wxEVT_SLIDER`, never the `wxEVT_SCROLL_*` family → handlers bound only to the scroll family (KiCad colour picker) never run | `src/wasm/slider.cpp` — fire `THUMBTRACK`/`CHANGED`/`THUMBRELEASE` | +| **H-9** | High | `wxConfig` read-back truncates non-ASCII at the first multi-byte char (UTF-16 `.length` used as a UTF-8 byte budget) | `build/wasm/wx.js` — length helpers return UTF-8 byte length | +| **#36** | Medium | `wxStaticText` renders the mnemonic `&` literally (`&File` shows an `&`) | `src/wasm/stattext.cpp` — `RemoveMnemonics()` before the label push | + +Plus a **seventh fix added later** (2026-07-03), a distinct audit finding that had +never been implemented — KiCad e2e only, no standalone repro: + +| # | Sev | Bug (native behavior → DOM-port bug) | Fix location | +|---|-----|---|---| +| **H-1** | High | `wxDialog::ShowModal` never created a `wxWindowDisabler`, so a "modal" dialog left the parent editor frame fully input-live (menubar/toolbar/canvas keep firing) | `src/wasm/dialog.cpp` — `new wxWindowDisabler(this)` in `ShowModal` (the upstream-univ mechanism the port had dropped) | + +Plus a **test-harness fix**: `tests/apps/Makefile.wasm` exports `stringToNewUTF8` +(the DOM string-read bridge needs it; the production build gets it via embind +`--bind`, the standalone apps don't). + +## Two test layers + +1. **Standalone wxWidgets e2e** — one tiny WASM app per bug under + `tests/apps/standalone/`, driven by `tests/e2e/parity-audit.spec.ts`. Each app + isolates the exact buggy path and self-reports `[REPRO] : PASS/FAIL` (or + exposes live DOM state). All six are red before the fix, green after. +2. **KiCad e2e (product proof)** — three specs drive real pcbnew dialogs: + `tests/kicad/plot-checklist.spec.ts` (the Plot dialog's layer checklist) for + **H-5**, `tests/kicad/via-clientdata.spec.ts` (the Track & Via Properties + dialog's predefined-via-size choice) for **C-1**, and + `tests/kicad/modal-input-lock.spec.ts` (the Page Settings dialog disabling the + main frame) for **H-1**. See + [`kicad-e2e-reachability.md`](kicad-e2e-reachability.md) for why the other four + of the original six resist a clean KiCad e2e — and why H-1's test must use a + *real*-modal dialog (Page Settings), not a quasi-modal one (Plot / Track & Via + already disable the parent via KiCad's own `WINDOW_DISABLER`). + +See [`redgreen.md`](redgreen.md) for the exact measured red→green ledger. + +## Build & run + +```bash +# 1. Build the patched wxWidgets (standalone) and the repro apps +./scripts/build-wx-wasm.sh +cd tests/apps && make -f Makefile.wasm parity-repros stattext-ellipsize && cd ../.. + +# 2. Standalone parity tests +cd tests && npx playwright test e2e/parity-audit.spec.ts + +# 3. KiCad e2e for H-5 (needs a pcbnew build) +BINARYEN_OPT_LEVEL=-O1 ./docker/build.sh pcbnew # -O1: never -O2 +cd tests && npm run setup:kicad +npx playwright test --config=playwright-kicad.config.ts --project=chromium --headed kicad/plot-checklist.spec.ts +``` + +## Files + +- Fixes: `wxwidgets/{src/wasm/*.cpp, include/wx/wasm/*.h, build/wasm/wx.js}` (11 files). +- Standalone repros: `tests/apps/standalone/{selevent-clientdata,textctrl-clear,checklist-checks,slider-scroll,config-utf8,stattext-mnemonic}/`. +- H-1 fix: `wxwidgets/src/wasm/dialog.cpp` (`wxWindowDisabler` in `ShowModal`). +- Specs: `tests/e2e/parity-audit.spec.ts`; KiCad e2e `tests/kicad/{plot-checklist,via-clientdata,modal-input-lock}.spec.ts`. +- Patches snapshot: `features/wxwidgets-diff/{root,wxwidgets}.patch`. diff --git a/docs/features/wx-parity-fixes/kicad-e2e-reachability.md b/docs/features/wx-parity-fixes/kicad-e2e-reachability.md new file mode 100644 index 000000000..81d766508 --- /dev/null +++ b/docs/features/wx-parity-fixes/kicad-e2e-reachability.md @@ -0,0 +1,126 @@ +# KiCad e2e reachability of the parity bugs + +All six fixes are proven red→green by the **standalone** wxWidgets e2e tests. A +separate question is which can *also* be reproduced through a **real KiCad app** +as a product-level e2e test. This documents that investigation (done against a +live pcbnew build) so nobody has to re-derive it. + +**Answer:** **H-5**, **C-1**, and **H-1** have product-level KiCad e2e proofs +(`tests/kicad/plot-checklist.spec.ts`, `tests/kicad/via-clientdata.spec.ts`, +`tests/kicad/modal-input-lock.spec.ts`). The remaining four of the original six +each have a specific, verified blocker. + +> **Update (2026-07-01):** C-1 was originally logged here as "Blocked-ish". That +> was overcautious — it *is* reachable and cleanly DOM-assertable. See the C-1 row +> and "How C-1 is reproduced" below. +> +> **Update (2026-07-03):** **H-1** (a separate audit finding, not one of the six — +> "modal dialogs are not input-modal") was **implemented** and given a KiCad e2e. +> The audit called it unsurfaceable ("e2e tests drive only the dialog"); it isn't. +> See "How H-1 is reproduced" below — the catch is that most KiCad dialogs are +> *quasi-modal* and already disable the parent, so the test must pick a real +> `wxDialog::ShowModal` dialog (Page Settings). + +## Which pcbnew dialogs actually open in WASM + +Probed by driving the File menu and inspecting the DOM: + +| Dialog | Opens? | Notable widgets seen | +|---|---|---| +| **Plot** (`File → Plot…`) | ✅ | `wxCheckListBox` (48 layer rows), choices, text fields | +| **Print** (`File → Print…`) | ✅ | `wxCheckListBox`, `m_colorTheme` `wxChoice` with client data | +| **Board Setup** (`File → Board Setup…`) | ✅ | 13 choices, 28 text fields (treebook of panels) | +| **Page Settings** (`File → Page Settings…`) | ✅ | 16-opt page-size choice, ~21 title-block text fields | +| **Colour picker** (swatch → picker) | ❌ | does not open in WASM (blocks H-6 via that path) | + +Menus (menubar + popups) are always present. + +## Per-bug verdict + +| Bug | Reachable surface | Cleanly assertable? | Verdict | +|---|---|---|---| +| **H-5** checklist | Plot dialog ✅ | ✅ count checked `[data-wx-check-list] input:checked` | **DONE** — `tests/kicad/plot-checklist.spec.ts` | +| **C-1** client-data | Track & Via Properties dialog | ✅ Via-diameter field value | **DONE** — `tests/kicad/via-clientdata.spec.ts`. Print's `m_colorTheme` is read via the *control* (`GetClientData(n)`), not the event, so it does *not* exercise C-1; the via dialog's `onViaSelect` is the one true event-path consumer (an exhaustive tree sweep found exactly one). | +| **#30** caret/`SelectAll` | UNIT_BINDER numeric fields (everywhere) | ✅ `input.selectionStart/End` | **Fragile**: KiCad only calls `SelectAll()` on *indeterminate multi-select* or *validation error* (`unit_binder.cpp:233,302,395,410`) — not on plain focus. Needs a specific multi-select or out-of-range setup. This one is *unfixed* (deferred backlog) so it would be RED on the current build (1 build to fix). | +| **H-6** slider | Appearance panel's 6 `STEPPED_SLIDER` opacity sliders (always visible) | ❌ effect is a **canvas opacity change** | **Blocked**: only assertable by canvas pixel-diff (inherently flaky) — same problem as the colour picker. | +| **#36** stattext `&` | — | ✅ span text | **Unreachable**: KiCad `&` mnemonics are on buttons/menu items, **not `wxStaticText`**. No `&` static label in any dialog that opens (Plot/Print/Page Settings all checked). | +| **H-4** textctrl Clear | needs a dialog with Clear/Reset on a text field | ✅ input value | **No clean surface found** in the dialogs that open. | +| **H-9** config UTF-8 | — | — | **Unreachable**: no UI action writes then reads back a non-ASCII config value. | + +## Why H-5 works where Print's checklist doesn't + +The bug only manifests when `Append` and `Check` are **interleaved** in one loop: +each `Append` rebuilds the DOM rows unchecked, so a `Check` from an earlier +iteration is wiped by a later `Append`. + +- **Plot** (`dialog_plot.cpp:351-354`): `int i = Append(name); if(sel) Check(i);` + in one loop → interleaved → **bug manifests** (0 of 48 checked). +- **Print** (`dialog_print_pcbnew.cpp:118` then `:156`): appends *all* layers in + the constructor, then checks selected layers in a *separate* loop in + `TransferDataToWindow()` → no `Append` after the `Check`s → **bug does not + manifest**. + +So Print is not a valid second H-5 surface — a useful confirmation that the fix's +trigger is understood precisely. + +## How C-1 is reproduced (`tests/kicad/via-clientdata.spec.ts`) + +The perceived fragility came from two assumptions that turned out to be avoidable: + +- **"needs a configured predefined via size"** — true, but the list is seeded + straight from the board file: `(setup (user_via ) …)` populates + `m_ViasDimensionsList` (`pcb_io_kicad_sexpr_parser.cpp:2585`), so the dialog's + `m_predefinedViaSizesCtrl` choice is pre-filled on load — no Board Setup clicks. +- **"needs board + via + fragile 5-step canvas interaction"** — the board contains + *only* the via, so **Edit → Select All (Ctrl+A)** selects exactly the via (which + satisfies the tracks/vias-only condition `EDIT_TOOL::Properties` requires) with no + canvas-pixel math. Press **E** to open the dialog. + +The rest is a clean DOM assertion. The via's own diameter is `0.7 mm` (distinct from +the two predefined sizes and from 0). Picking predefined `"0.9 / 0.45"` fires the +`` goes `0.7 → 0.9`. +- **RED (bug):** `GetClientData()` is NULL, so `viaDimension->m_Diameter` reads 0 → + the field goes `0.7 → 0` (memory `wasm-null-deref-reads-zero`). + +The symptom is *silent wrong values*, not a crash — so the assertion is the field +value, plus a WASM-abort guard for the (rarer) trap case. + +## How H-1 is reproduced (`tests/kicad/modal-input-lock.spec.ts`) + +H-1: `wxDialog::ShowModal` never created a `wxWindowDisabler`, so a "modal" dialog +left the parent editor frame fully input-live. The audit deemed this unsurfaceable +by e2e ("e2e tests drive only the dialog"). Two facts make it surfaceable — and one +made the first attempt a false green: + +- **Signal.** Disabling the parent frame flips its `IsEnabled()`, and the port + re-emits `enabled` into `window.wxElementRegistry` on every `DoEnable()` + (`window.cpp:1438`). So the pcbnew main frame's registry entry flips + `enabled: true → false` the instant a real modal opens — and that same + `IsEnabled()` walk is exactly what every input gate consults. The test just reads + `registry.getElement().enabled` before / during / after the modal. +- **Dialog choice is load-bearing** (the trap). Most KiCad dialogs are opened via + KiCad's **`ShowQuasiModal()`**, which runs its own nested loop and *already* + creates a `WINDOW_DISABLER(parent)` (`dialog_shim.cpp:1431`) — so the parent is + disabled with or without the wx fix. **Plot** (`board_editor_control.cpp:565`) and + **Track & Via Properties** (`edit_tool.cpp:2306`) are both quasi-modal: driving + Plot on the *unpatched* binary already reported the frame disabled (a false green + that flushed this out). The test therefore drives **Page Settings**, shown via a + *real* `wxDialog::ShowModal()` (`board_editor_control.cpp:534`; `DIALOG_SHIM::ShowModal` + forwards straight through, adding no disabler of its own). So H-1's real user + impact in KiCad is limited to the real-`ShowModal` dialogs. + +Measured RED (frame `enabled` stays `true` while Page Settings is open → fail) / +GREEN (`false` while open, `true` after close → pass); see `redgreen.md`. Residual: +a menubar dropdown still *visually* opens over the modal (pure-JS popup, no C++ +gate) though its items do nothing — cosmetic, would need a DOM shield, out of scope. + +## Recommendation + +The cleanly-reachable Critical/High surfaces now have product-level proofs: +**H-5** (`plot-checklist.spec.ts`), **C-1** (`via-clientdata.spec.ts`), and **H-1** +(`modal-input-lock.spec.ts`, which also *implemented* the fix). The remaining four +of the original six are best left to the standalone suite (their KiCad surfaces need +a validation-error setup, a canvas pixel-diff, or don't exist — see the table). diff --git a/docs/features/wx-parity-fixes/native-parity-audit.md b/docs/features/wx-parity-fixes/native-parity-audit.md new file mode 100644 index 000000000..4019dba9e --- /dev/null +++ b/docs/features/wx-parity-fixes/native-parity-audit.md @@ -0,0 +1,620 @@ +# wxWidgets WASM/DOM Port — Parity Audit vs Native (wxGTK) + +*Definitive engineering report for the maintainer of an in-browser KiCad (PCBJam). 244 verified/carried findings folded with three inventory ground-truths (compiled-sources coverage map, setup.h honesty audit, DOM-bridge architecture). Every medium-and-above finding survived an adjudicator that read both the WASM and native/contract source; low items are carried (tagged).* + +--- + +## 1. Executive summary + +The wxWidgets WASM/DOM port is a real, broadly complete toolkit — not a thin shim. It is a genuine `TOOLKIT=WASM` build that compiles the full `GUI_CMN_SRC` set (grid, dataview, propgrid, AUI, STC, scrolled windows, all generic dialogs) into a 293-member core archive, plus 62 hand-written `src/wasm/*.cpp` port files, all verified present in the shipped `.a` files. The DOM-bridge design (three handle namespaces, fresh-entry event re-entry under an Asyncify-suspended stack, LIFO nested-modal pumps) is sound and is what makes in-browser KiCad work at all. **That said, real defects exist** — this is not a "zero findings" situation, and the prior report's "0 confirmed findings" headline was a tooling artifact, not the truth. + +The gaps cluster into a small number of root causes rather than 244 independent bugs. The single highest-risk item is a **critical null-dereference crash**: selection command events (`wxEVT_CHOICE`/`wxEVT_COMBOBOX`/`wxEVT_LISTBOX`) are hand-rolled and never attach per-item client data, so KiCad's Track & Via Properties dialog traps the WASM module on a normal user action. The largest *class* of real gaps is **state desynchronization between C++ caches and the live DOM** — text-control caret/selection/clipboard, menu/toolbar enable-check-label staleness (no `wxEVT_MENU_OPEN`, `wxMenuItem` is a pure data stub), and check-list/radio state — where the C++ side is right but the user sees stale or wrong UI. A second structural class is **the Canvas2D backend lacking raster-ops and clip-box state** (XOR/`wxINVERT` no-op, `SetClippingRegion` silently ignored, masked/stretched blits broken). Beyond correctness, **modal dialogs are not input-modal** (no `wxWindowDisabler` is ever created), which is a genuine re-entrancy/use-after-free hazard on the Asyncify-parked stack. Rounding out the picture: partial event families (no horizontal wheel, no slider scroll-event family, no `wxEVT_CHAR` to validators), HiDPI quantized to {1×, 2×}, no working printing, accessibility essentially absent, and a `setup.h` that misrepresents the shipped configuration. None of the medium/low items are crashes; most are degraded-UX or latent. The honest verdict: **functionally usable and impressively far along, with one must-fix crash, a coherent set of attackable state-sync root causes, and a long tail of cosmetic/inherent-platform items.** + +--- + +## 2. Severity scoreboard + +| Severity | Count | +|---|---| +| Critical | 1 | +| High | 11 (9 distinct after dedup: #11→#2, #12→#8) | +| Medium | 58 (≈50 distinct; inv-stub-todo #58–64,#69 duplicate higher items) | +| Low / carried | 174 | +| **Total** | **244** | + +Dismissed/refuted candidates (not counted above): **11** (see §10). + +### Subsystem × severity matrix + +| Subsystem | Crit | High | Med | Low | Total | +|---|---:|---:|---:|---:|---:| +| choices-lists | 1 | 1 | 1 | 4 | 7 | +| textctrl | – | 2 | 3 | 3 | 8 | +| menus | – | 2 | 3 | 1 | 6 | +| dc-core | – | 1 | 3 | 7 | 11 | +| toplevel-frame-dialog | – | 1 | 2 | 3 | 6 | +| static-range (slider/static) | – | 1 | 1 | 7 | 9 | +| timer-config-settings | – | 1 | 4 | 3 | 8 | +| critic-uncovered | – | 2 | 1 | 2 | 5 | +| window-core | – | – | 3 | 4 | 7 | +| input | – | – | 3 | 9 | 12 | +| fonts | – | – | 3 | 4 | 7 | +| cc-accessibility | – | – | 3 | 5 | 8 | +| bitmap-image | – | – | 2 | 7 | 9 | +| gdi-resources | – | – | 2 | 2 | 4 | +| glcanvas | – | – | 2 | 6 | 8 | +| cc-hidpi-scaling | – | – | 2 | 1 | 3 | +| containers | – | – | 1 | 10 | 11 | +| clipboard-dnd | – | – | 1 | 6 | 7 | +| buttons | – | – | 1 | 6 | 7 | +| cc-printing | – | – | 1 | 4 | 5 | +| cc-net-ipc-process | – | – | 1 | 4 | 5 | +| cc-locale-mime-stdpaths-fs | – | – | – | 7 | 7 | +| tooltip-misc | – | – | – | 3 | 3 | +| app-eventloop | – | – | – | 8 | 8 | +| inv-known-limitations | – | – | 2 | 22 | 24 | +| inv-stub-todo | – | – | 13 | 36 | 49 | +| **Total** | **1** | **11** | **58** | **174** | **244** | + +--- + +## 3. Critical & High findings + +### C-1 — Selection command events carry no per-item client data → null-deref crash in KiCad (#1) +- **Severity:** Critical +- **Gap:** `wxEVT_CHOICE`/`wxEVT_COMBOBOX`/`wxEVT_LISTBOX` are constructed by hand and never call `InitCommandEventWithItems`/`SetClientData`/`SetClientObject`. +- **Evidence:** `src/wasm/choice.cpp:221-239` (sets only `SetInt`/`SetString`/`SetEventObject`), `src/wasm/combobox.cpp:107-127`, `src/wasm/listbox.cpp:289-321`. Native contract: `src/common/ctrlsub.cpp:276-302` (`InitCommandEventWithItems`/`SendSelectionChangedEvent` copy `GetClientObject(n)`/`GetClientData(n)`), `src/common/lboxcmn.cpp:186-201`; ports route through these (`src/gtk/choice.cpp:31-34`, `src/gtk/combobox.cpp:31-35`). +- **Native vs WASM:** Native always attaches the selected item's per-item client data to the event; the WASM hand-rolled events never do, so `event.GetClientData()/GetClientObject()` always return NULL. +- **Impact:** Any handler reading the event's client data gets NULL. `pcbnew/dialogs/dialog_track_via_properties.cpp:963-968 onViaSelect` does `static_cast(aEvent.GetClientData())->m_Diameter` with no null guard → **null pointer dereference / WASM module trap** on a normal action (double-click a track/via, pick a predefined via size). The trap is unrecoverable. +- **KiCad relevance:** Confirmed crash path in a core, easily-reached pcbnew dialog (`dialog_track_via_properties_base.cpp:278/660` wires `wxEVT_COMMAND_CHOICE_SELECTED`; `:424/:525` populate via `Append(msg, viaDimension)`). All `Append(item, clientData)` + read-event-client-data handlers are equally affected. +- **Verifier note:** Confirmed on both sides; no base/generic/DOM/JS-shim path attaches the data because the WASM handlers bypass `SendSelectionChangedEvent`/`SendEvent`. Fix: replace the hand-rolled events with `SendSelectionChangedEvent(wxEVT_*)` (or call `InitCommandEventWithItems` before `HandleWindowEvent`). + +--- + +### H-1 — Modal dialogs are not input-modal; no `wxWindowDisabler` is ever created (#2, merges #11) +- **Severity:** High +- **Gap:** `ShowModal()` never disables other top-level windows; the port replaces a file (`src/univ/dialog.cpp`) that did this in one line. +- **Evidence:** `src/wasm/dialog.cpp:270-302` (`ShowModal` = set flag, `Show(true)`, `startModal()`); `m_windowDisabler` is NULL-inited at `dialog.cpp:59`, `wxDELETE`d at `:157`, and **never `new`'d** (grep clean). Native: `src/univ/dialog.cpp:193 m_windowDisabler = new wxWindowDisabler(this)`; `wxWindowDisabler::DoDisable` (`src/common/utilscmn.cpp:1555`) disables every other shown TLW; wxGTK uses `gtk_window_set_modal` (`src/gtk/dialog.cpp:159`). +- **Native vs WASM:** Native locks input to the dialog until `EndModal`; WASM leaves the parent frame's menubar/toolbar/canvas/controls fully enabled. The gate that *would* block them — `domevents.cpp:94 if(!window->IsEnabled()) return;` and the `IsEnabled()` checks in `app.cpp:152/187/298/367` — never trips because nothing is disabled. There is also no DOM-level backdrop/`showModal()`/`pointer-events` shield (`wx.js` only does z-index stacking). +- **Impact:** While a "modal" dialog is parked under Asyncify the user can still click the GAL canvas (firing tools), menubar titles, and toolbar buttons → re-entrant dialog/tool invocation, editing the document behind a settings dialog, or `Close()`/`Destroy()` of the parent while a child's `ShowModal` frame is suspended → **use-after-free / crash**. This is exactly the nested/consecutive-modal Asyncify-corruption class the dialog.cpp comments warn about. +- **KiCad relevance:** Every modal dialog (Board Setup, Preferences, DRC, Plot, Net Classes…). e2e tests drive only the dialog, so they don't surface it. +- **Verifier note:** Confirmed; the enforcement machinery is wired and waiting — the one-line fix (create the disabler) would immediately engage the existing `IsEnabled()` guards. Related low items: focus is also not saved/restored across a modal (#186). +- **STATUS (2026-07-03):** FIXED — `wxwidgets/src/wasm/dialog.cpp` now does `m_windowDisabler = new wxWindowDisabler(this)` in `ShowModal`. Reproduced by a KiCad e2e (`tests/kicad/modal-input-lock.spec.ts`), measured RED (parent frame `enabled` stays `true` during a modal) → GREEN (`false`). Nuance this entry missed: the fix only changes behavior for *real* `wxDialog::ShowModal` dialogs (e.g. **Page Settings**). Most KiCad dialogs are opened via **`ShowQuasiModal()`**, which already disables the parent through KiCad's own `WINDOW_DISABLER` (`dialog_shim.cpp:1431`) — so "Plot / Board Setup / DRC…" listed above are *not* affected in practice (they were already input-modal). See `redgreen.md`. + +--- + +### H-2 — `SetClippingRegion(x,y,w,h)` is a silent no-op (#3) +- **Severity:** High +- **Gap:** The rectangular DC clip reads clip members that the modern base class never populates, so the requested rectangle is never applied. +- **Evidence:** `src/wasm/dc.cpp:292-304` calls `wxDCImpl::DoSetClippingRegion(x,y,w,h)` then feeds `m_clipX1..m_clipY2` to JS `clipRect`. Base `src/common/dcbase.cpp:371-411` stores the box in `m_devClipX1..` (device units) and **never writes** `m_clipX1..` (ctor inits them to 0 at `:343`); `include/wx/dc.h:766` documents that the derived port must fill `m_clipX1..` itself (gtk/msw/qt/x11/dfb/dcpsg all do; wasm does not). JS `clipRect` (`wx.js:1194-1199`) treats `width<=0` as "uninitialized" and resets the clip to the full context. +- **Native vs WASM:** GTK applies the intersected rectangle as a real GC clip; WASM passes `clipRect(id, 0,0,0,0)` → clip reset to whole context. +- **Impact:** Every `wxDC`-level rectangular clip is ignored → silent overdraw/text bleed. (The `wxRegion` overload works; only the `(x,y,w,h)` coordinate overload is broken.) +- **KiCad relevance:** `wxGrid` cells, generic `wxDataViewCtrl`/`wxPropertyGrid` renderers, AUI tab/caption art, owner-drawn controls — all compiled-in and reachable; e.g. long cell text bleeds into neighbors. +- **Verifier note:** Confirmed; `m_clipX1` is only ever read in `src/wasm`, written nowhere. Fix: populate `m_clipX1..m_clipY2` in the port (as the other ports do). +- **STATUS (2026-07-03):** FIXED — `wxwidgets/src/wasm/dc.cpp` `DoSetClippingRegion` now mirrors the base's device-unit clip box back into `m_clipX1..m_clipY2` via `DoGetClippingRect()` (and `DestroyClippingRegion` chains to the base). Reproduced by a KiCad e2e (`tests/kicad/grid-clip-bleed.spec.ts`): Board Setup → Net Classes, 70-char netclass name next to an empty Clearance cell; measured RED (325 ink pixels bleeding across the neighbor) → GREEN (0, text clipped at the cell edge; name-cell ink identical 413/413). Nuance this entry missed: the *Text Variables* grid can't repro it — `WX_GRID::SetupColumnAutosizer` auto-fits the column to its content, masking the missing clip; Net Classes has fixed widths. See `redgreen.md`. + +--- + +### H-3 — Text-control caret/selection accessors return a stale cache reset to 0 on every keystroke (#4, merges #62) +- **Severity:** High +- **Gap:** `GetInsertionPoint`/`GetSelection`/`GetStringSelection`/`HasSelection` return a pure C++ cache that is hard-reset to 0 on each typed character and never reads the real DOM caret/selection. +- **Evidence:** `src/wasm/textentry.cpp:139-143` (`GetInsertionPoint`), `:180-199` (`GetSelection`); reset path `textctrl.cpp:109-120` (`OnDomEvent` INPUT) → `textentry.cpp:220-232` (`DoSetValue` sets `m_insertionPoint=0`, `m_selectionStart/End=-1`). There is **no DOM bridge** to read `selectionStart/selectionEnd` (grep of `dom.h`/`wx-dom.js` returns nothing). Native: `src/gtk/textentry.cpp:771-774` (`gtk_editable_get_position`), `:818-843` (`gtk_editable_get_selection_bounds`). +- **Native vs WASM:** GTK queries the live widget; WASM returns cache that is wrong after any typing or mouse/arrow caret move. +- **Impact:** After typing `abc` (DOM caret at 3) `GetInsertionPoint()` returns 0 and `GetSelection()` returns (0,0). Because `CanCopy/CanCut` are built on `HasSelection()` (`textentrycmn.cpp:303-311`), Edit-menu Copy/Cut enable state is also wrong; insert-at-cursor lands at 0 (see H-4/#31). +- **KiCad relevance:** Pervasive — live-filter/eval `EVT_TEXT` handlers, select-on-focus, clipboard-UI enable logic. Degraded, not crashing (native browser editing still works visually). +- **Verifier note:** Confirmed; the only DOM mirror is value (`wxDomGetValue/SetValue`). No selection read-back exists anywhere in the port. +- **STATUS (2026-07-06):** FIXED (covers #30 SetSelection/SelectAll-not-pushed and #31 WriteText-at-stale-caret). Added a DOM caret/selection bridge — `wxDomGetSelectionStart/End` + `wxDomSetSelection` in `include/wx/wasm/private/dom.h` + `build/wasm/wx-dom.js` — and made `src/wasm/textentry.cpp` live-read/write it (cache fallback pre-Create); `WriteText` now inserts at the live caret. **Required a one-line KiCad change too:** `SelectAllInTextCtrls`'s `SelectAll()` was gated `#if defined(__WXMAC__)||defined(__WXMSW__)` (`common/dialog_shim.cpp:901`) and thus compiled out for the wasm toolkit — widened with `|| defined(__WXWASM__)`. Reproduced by a KiCad e2e (`tests/kicad/dialog-select-on-open.spec.ts`): Track Properties opens with the width field focused + pre-filled `0.25`, typing `5` measured RED `"0.255"` (inserted) → GREEN `"5"` (replaced). Read-path proven by the standalone `textsel` app (no KiCad-observable consumer). Load-bearing nuance: a `setSelectionRange` on a *blurred* input is dropped on the next focus, so `wxDomSetSelection` registers a one-shot focus listener to re-apply it — matching KiCad's select-then-focus order. See `redgreen.md`. + +--- + +### H-4 — `Remove()`/`Clear()`/`RemoveSelection()` update the C++ value cache but never push to the DOM → visible text desync (#5, merges #60) +- **Severity:** High +- **Gap:** Base `wxTextEntry::Remove` erases `m_value` only; `wxTextCtrl` does not override `Remove`/`Clear`, so `wxDomSetValue` is never called. +- **Evidence:** `src/wasm/textentry.cpp:73-93` (`// TODO(dom-phase-2): mirror the new value into the DOM element.`); `include/wx/wasm/textctrl.h` overrides only `WriteText`/`OnDomEvent`/`DoSetValue`. Native: `src/gtk/textentry.cpp:695-698` (`gtk_editable_delete_text`); base `Clear(){Remove(0,-1);}` (`wx/textentry.h:60`). +- **Native vs WASM:** GTK visibly empties/shortens the control; WASM leaves the `/