Lands the four pure W3C → Jian gesture mappers that Phase C2's
browser listeners will consume:
- `event:⌨️:map_keyboard_parts(key, code, location, repeat,
pressed, modifiers, is_composing) -> KeyEvent` — W3C
KeyboardEvent.key/code/location string lookups produce KeyValue
(Char / Named / Unidentified) + KeyCode enum + KeyLocation enum.
Phase C1 covers KeyA-Z, Digit0-9, Enter / Escape / Tab / Space /
Backspace / Delete / arrows; Home/End/PageUp/PageDown/F-keys/
modifier physical codes (ShiftLeft etc) extend the table in C2
(codex C1 NIT-2 deferred).
- `event::ime::{composition_start, composition_update, composition_end}`
+ `utf16_selection_to_utf8` helper. The helper walks
`text.char_indices()` accumulating `len_utf16()` to remap UTF-16
code-unit offsets (what `CompositionEvent.getTargetRanges()`
hands us) to UTF-8 byte offsets (what jian-core's `ImeKind::
CompositionUpdate { selection: Range<usize> }` requires per spec
§2.4). Mis-ordered range returns None; out-of-range bounds clamp
to text.len(). CJK (你好), surrogate pairs (🙂), mixed-encoding
(aé), zero-length selections, and empty text are all covered.
- `event::pointer::map_wheel(position, dx, dy, dz, mode, mods,
timestamp: Instant) -> WheelEvent` — flips W3C deltaY sign so
widget code reads Jian-internal positive-up. Phase C1
intentionally takes `timestamp` as a parameter rather than
calling `Instant::now()` internally; `std::time::Instant::now()`
panics on wasm32-unknown-unknown ("time not implemented on this
platform") and the C2 listener will fill the timestamp from a
polyfill (web_time::Instant). Made the mapper pure to keep the
panic locus in the listener glue, where the polyfill lives.
- `event::focus::map_focus(gained, node_id_hint,
related_node_id_hint) -> FocusEvent` — pure pass-through; the
W3C target → WidgetId correlation work lives in C2 alongside
the DOM mirror id registry (Phase D groundwork).
Tests (`tests/dom_event_mapping.rs`, 22 tests, all native):
- keyboard: 5 tests + 1 empty-string-key fallback test (codex C1
NIT-3) — key/code/location preservation, named key, location
decoding, is_composing propagation, multi-codepoint &
empty-string Unidentified fallback
- ime: 8 tests — start/update/end shapes, UTF-16→UTF-8 remap for
CJK / surrogate pair / mixed encoding / zero-length / no-selection
/ mis-ordered range / out-of-range clamp / empty text
- wheel: 4 tests — Y sign flip, X no-flip, mode decoding, deltaZ
passthrough
- focus: 2 tests — gained=true with both hints, blur with no
related target
Plumbing:
- shell-web grows a direct path-with-version `jian-core` dep.
shell-core re-exports `gesture::*` but not `geometry::*`, and
the wheel mapper builds `WheelEvent.position` from
`jian_core::geometry::Point::new(...)`. Same path-with-version
pattern as shell-core's own jian-core dep.
- `pub mod event;` is NOT cfg-gated to skia — the mappers are pure
and useful on the wasm32-clean stub baseline too.
Plan-vs-implementation deviations (deliberate, all kept narrow):
- `map_keyboard_parts` adds `is_composing: bool` parameter (jian-core
KeyEvent struct REQUIRES the field per spec §2.4).
- `map_focus` adds `related_node_id_hint: Option<u64>` parameter
(jian-core FocusEvent struct field; plan body missed it).
- `map_wheel` takes `timestamp: Instant` parameter instead of
calling `Instant::now()` internally (avoids wasm32-unknown-unknown
runtime panic; pure mapper).
- `event/pointer.rs` covers ONLY wheel; full PointerEvent mapping
(kind / phase / buttons / pressure) lives in C2 alongside listener
registration since that's where the W3C PointerEvent surface meets
the runtime context.
Verification:
- `cargo test -p openpencil-shell-web --test dom_event_mapping` —
22/22 passing
- `cargo check -p openpencil-shell-web --target
wasm32-unknown-unknown --no-default-features --features web` —
green (compile guard)
- `EMSDK=$HOME/.emsdk bash tools/check-wasm-bundle.sh` — PASS
- 0 env.* imports
- 615 616 bytes gzip = 58% of 1 MiB ceiling (no growth — event
modules dead-code-eliminated when not called)
- `bash tools/check-widget-boundary.sh` — PASS (event/ doesn't
violate F1-F4)
Codex iterate review: 1 round → GO with 1 deferred CONCERN
(Instant source for C2 — already documented inline) + 3 NITs
(deltaX comment wording, additional KeyCode entries, extra
edge-case tests). NIT-1 and NIT-3 fixed in this commit; NIT-2
deferred to C2.