Lands the four Step 1b inspector widgets in shell-core (per spec
§1.4 — widget logic lives here so shell-native + shell-web reuse
it; only the RenderBackend impl + DOM event mapping + accesskit
DOM mirror are platform-owned).
Widgets:
- `widgets::TreeWidget` (Role::Tree, label "Layers") — sample
3-item tree (Frame / Title / Button) with selection-aware
blue-row paint and depth-indented labels. WidgetIds 100-103.
- `widgets::PropertyRow` (Role::Group, label "{label} {value}")
— single-row label/value pair. PropertyRow uses Role::Group
rather than Role::GenericContainer so the row label survives
ARIA filtering on the way to VoiceOver / NVDA (codex B2 R1
CONCERN). WidgetIds 200-299.
- `widgets::Dropdown` (Role::ComboBox, label "Blend") — sample
blend-mode picker with 3 options. Phase B static slice does
NOT yet pop a menu when `state.open == true`; Phase C wires
click + keyboard handling. WidgetIds 300-399.
- `widgets::TextInput` (Role::TextInput) — single-line input
with CJK IME preview. Paints `state.preedit` (in-progress
composition) when present, else `state.value`; non-empty
preedit also draws an 80px underline. Phase C lands
compositionstart / update / end → state mutation in shell-web.
WidgetIds 400-499.
State separation:
- `DropdownState { selected, open }` and `TextInputState { value,
preedit }` live as their own structs so Phase C event handlers
can swap them without taking ownership of the surrounding
widget. `TextInputState::default()` returns the empty state.
Tests (`tests/widgets_static.rs`):
- `four_inspector_widgets_paint_static_content` — paints all
four widgets through one RecordingBackend, asserts
≥5 fills / ≥3 strokes / ≥7 text dispatches (each tightened
vs the plan sketch to actually catch per-widget regressions).
- Per-widget role + label assertions (Tree / Group / ComboBox /
TextInput).
- `text_input_paints_preedit_underline_when_composing` —
drives `state.preedit = "你好"`, paints, asserts the IME
branch emits 1 fill + 2 strokes (border + underline) + 1
text run.
- `dropdown_state_independent_state_struct` +
`text_input_state_default_is_empty` — verify state structs
are independent + default-constructible.
Plan-vs-implementation deviations (deliberate):
- `WidgetId::new(N)` instead of the plan's `WidgetId(N)` tuple
literal so the sample/new constructors exercise the B1
debug_assert non-zero check. Tuple stays public for pattern
matching + `const` contexts.
- WidgetId range conventions per widget kind (Tree=100s /
PropertyRow=200s / Dropdown=300s / TextInput=400s) added as
doc-only comments. Real Phase C host will allocate from a
counter; the conventions just keep the B-phase fixtures
predictable.
- Per-widget paint counts in `four_inspector_widgets_…` test
tightened to ≥5/≥3/≥7 (plan sketch had ≥4/≥3/≥4 which
wouldn't catch a regression in Tree's selection-row fill).
Verification:
- `cargo test -p openpencil-shell-core` — 10/10 passing
- `cargo check -p openpencil-shell-core --target
wasm32-unknown-unknown` — green (shell-core stays
wasm32-clean per spec §1.2 — no platform deps creeping in)
- `cargo check -p openpencil-shell-native` — green
Codex iterate review: 2 rounds → GO.