Commit graph

2 commits

Author SHA1 Message Date
Kayshen-X a936d22a6f feat(shell-core): Phase C2.1 — widget event handlers + WidgetHost forwarding
Lands the data-flow piece of plan C2: shell-core widgets gain pure
state-mutation methods (`TextInputState::apply_ime`,
`DropdownState::apply_key`) that the WidgetHost forwards to from the
two new `// glue:` marked methods. Phase C2.2 will land the browser
closure registration that drives these methods from real DOM
events.

shell-core (widgets stay platform-agnostic per spec §1.4):
- `TextInputState::apply_ime(&ImeEvent)` — CompositionStart clears
  preedit, CompositionUpdate replaces preedit with `event.text`
  (selection deferred to Phase D DOM mirror), CompositionEnd
  appends the commit text to value and clears preedit.
- `DropdownState::apply_key(&KeyEvent, option_count)` — ArrowDown
  advances + opens (saturates at last option, no wrap), ArrowUp
  retreats with `saturating_sub` + opens, Enter / Escape close
  without mutating selection. Skips on Released or empty options.

shell-web (glue file widget_host.rs, both methods carry `// glue:`
markers per spec §1.4 boundary check):
- `WidgetHost::apply_ime(&ImeEvent)` — forwards to text_input.state
- `WidgetHost::apply_key(&KeyEvent)` — forwards to dropdown.state
  with `dropdown.options.len()` for the option count

Tests (`tests/widgets_static.rs`, +10 → 20 total):
- text_input apply_ime: Start clears preedit (value untouched);
  Update replaces preedit (value untouched); End commits +
  clears; double-Start without End each clears (codex C2.1 R1
  CONCERN-1 — pathological host state machine)
- dropdown apply_key: ArrowDown advances + saturates; ArrowUp
  retreats + saturating_sub; Enter / Escape close + Escape
  preserves selection (codex C2.1 R1 CONCERN-2); Released = no-op;
  zero options = no-op; unrelated NamedKey (Tab) = no-op
- Helper `keydown(named)` / `keyup(named)` build minimal KeyEvents;
  apply_key reads only `key` + `state` so the harness's KeyCode
  field is intentionally Unknown(String::new())

Plan-vs-implementation deviations (deliberate):
- WidgetHost forwarding methods carry `// glue:` markers. F3 in
  the boundary script's exemption set covered `fn paint(...)`;
  the same exemption applies here because these methods import +
  invoke `openpencil_shell_core::{ImeEvent, KeyEvent}` at the
  signature line. Verified `bash tools/check-widget-boundary.sh`
  still PASS.
- Codex C2.1 R1 CONCERN-3 (WidgetHost forwarding has no direct
  test coverage) deferred to C2.2 — the browser closure
  registration there exercises the forwarding end-to-end via
  real DOM events, which is more meaningful than mocking
  WidgetHost in shell-web tests with read-only accessors that
  would only exist for testing.

Verification:
- `cargo test -p openpencil-shell-core --test widgets_static` —
  20/20 passing
- `cargo check -p openpencil-shell-core --target
  wasm32-unknown-unknown` — green (shell-core stays wasm32-clean
  per spec §1.2)
- `bash tools/check-widget-boundary.sh` — PASS

Codex iterate review: 2 rounds → GO.
2026-05-09 21:50:00 +08:00
Kayshen-X f214f4b4e3 feat(shell-core): Phase B2 — four static inspector widgets
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.
2026-05-09 21:46:00 +08:00